问题

有哪些相见恨晚的 TensorFlow 小技巧?

回答
那些让我相见恨晚的 TensorFlow 小技巧

在数据科学和机器学习的世界里,TensorFlow 就像一位老朋友,陪伴我走过了不少征程。然而,随着项目越来越复杂,我总觉得在某个地方卡壳,优化效率、调试代码都耗费了不少精力。直到最近,我才挖掘出一些堪称“相见恨晚”的 TensorFlow 小技巧,它们就像隐藏的宝藏,一旦发现,便让我的开发体验焕然一新。今天,我就想好好跟大伙儿聊聊这些让我拍案叫绝的“秘籍”,希望也能帮到正在 TensorFlow 之路上摸索的你。

1. `tf.function`:别让 Python 成为性能瓶颈

坦白讲,最开始使用 TensorFlow 时,我其实挺依赖 Python 的动态性和灵活性,写起代码来顺畅无比。但是,当模型开始膨胀,训练数据量激增,我发现 Python 的解释执行模式成了一个巨大的性能瓶颈。神经网络的计算过程往往是海量的矩阵乘法和向量操作,这些操作交给 Python 来逐个执行,简直就是“慢如蜗牛”。

然后,我“偶然”发现了 `tf.function`。这个装饰器简直是救星!它能将 Python 函数“编译”成一个 TensorFlow 的计算图。一旦编译完成,后续的执行就如同在 C++ 或 Java 中一样高效。想象一下,一个原本需要几十秒才能跑完的训练步,在 `tf.function` 的加持下,可能只需要几秒钟。

怎么用? 别以为它有多么复杂,只需要在你的模型训练函数、前向传播函数前加上 `@tf.function`。

```python
import tensorflow as tf
import time

假设这是你的训练步函数
@tf.function
def train_step(model, optimizer, inputs, targets):
with tf.GradientTape() as tape:
predictions = model(inputs, training=True)
loss = tf.keras.losses.categorical_crossentropy(targets, predictions)
gradients = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
return loss

模拟一个简单的模型
class SimpleModel(tf.keras.Model):
def __init__(self):
super(SimpleModel, self).__init__()
self.dense1 = tf.keras.layers.Dense(64, activation='relu')
self.dense2 = tf.keras.layers.Dense(10, activation='softmax')

def call(self, inputs):
x = self.dense1(inputs)
return self.dense2(x)

model = SimpleModel()
optimizer = tf.keras.optimizers.Adam()

第一次调用时会进行编译,之后就很快了
dummy_inputs = tf.random.normal((32, 100))
dummy_targets = tf.one_hot([i % 10 for i in range(32)], 10)

start_time = time.time()
for _ in range(100):
loss = train_step(model, optimizer, dummy_inputs, dummy_targets)
end_time = time.time()
print(f"Total time with tf.function: {end_time start_time:.4f} seconds")

对比一下没有 @tf.function 的情况(请自行尝试,会明显慢很多)
```

小提示:

首次调用慢: 第一次调用带有 `@tf.function` 的函数时,TensorFlow 会花一些时间进行追踪(tracing)和图的构建,所以会感觉比普通 Python 函数慢。但这是值得的,因为后续的调用会快很多。
`training=True/False`: 在模型的前向传播中,务必根据 `training` 参数来决定是否使用 `Dropout` 或 `BatchNormalization` 等会影响训练过程的层。`tf.function` 会为 `training=True` 和 `training=False` 分别生成不同的图,确保逻辑正确。
Python 变量陷阱: 在 `@tf.function` 内部,尽量避免使用 Python 的可变对象(如列表、字典)来存储模型训练过程中产生的中间结果,因为这些对象在图的追踪过程中可能导致问题。如果需要动态地收集信息,考虑使用 `tf.Tensor` 或者 `tf.Variable`。

2. `tf.data` API:数据流水线的艺术

处理大量数据时,我曾经遇到过数据加载速度跟不上模型训练速度的尴尬局面。这就像发动机再强劲,也得有足够的燃油供应。 `tf.data` API 就是我找到的“解决方案”。它提供了一套非常强大且灵活的工具,用于构建高效的数据输入流水线。

`tf.data` 的核心思想是将数据加载、预处理、批处理、乱序等操作解耦,并且可以利用多线程并行化,让数据准备的过程“润物细无声”。

关键组件:

`tf.data.Dataset.from_tensor_slices()`: 从 NumPy 数组或 TensorFlow 张量创建数据集。
`.map()`: 对数据集中的每个元素应用一个函数(例如,图像的缩放、归一化、数据增强)。
`.shuffle()`: 乱序数据集。
`.batch()`: 将数据分组成批次。
`.prefetch()`: 预取下一批数据,以便在当前批次训练时,下一批数据已经在 GPU/CPU 上准备好。这是提升吞吐量最关键的一步!
`.cache()`: 将预处理过的数据缓存在内存或磁盘中,避免重复计算。

示例:

```python
import tensorflow as tf
import os

假设我们有一些图像文件路径和对应的标签
image_paths = [f"path/to/image_{i}.jpg" for i in range(1000)]
labels = [i % 10 for i in range(1000)]

def load_and_preprocess_image(image_path, label):
这是一个占位符,实际应用中会读取图像文件
image = tf.io.read_file(image_path)
image = tf.image.decode_jpeg(image, channels=3)
image = tf.image.resize(image, [128, 128])
image = tf.image.convert_image_dtype(image, tf.float32)
简单的随机翻转作为数据增强
image = tf.image.random_flip_left_right(image)
return image, label

创建数据集
dataset = tf.data.Dataset.from_tensor_slices((image_paths, labels))

构建数据流水线
BUFFER_SIZE = 1000 乱序缓冲区大小
BATCH_SIZE = 32

dataset = dataset.map(load_and_preprocess_image, num_parallel_calls=tf.data.AUTOTUNE) 并行处理
dataset = dataset.cache() 缓存预处理结果
dataset = dataset.shuffle(BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE)
dataset = dataset.prefetch(tf.data.AUTOTUNE) 预取数据

遍历数据集(在训练循环中使用)
for images, batch_labels in dataset:
训练模型...
pass
```

核心要义:

`tf.data.AUTOTUNE`: 这个值简直太好用了!它允许 TensorFlow 根据你的硬件情况自动调整并行度和预取数量,省去了手动调优的麻烦。
`prefetch()` 的魔力: `prefetch(tf.data.AUTOTUNE)` 是提升效率的关键。它让数据加载和模型训练可以并行进行,最大限度地利用 GPU/CPU 的计算能力。
`cache()` 的妙用: 如果你的数据集不大,或者预处理过程比较耗时,`cache()` 可以显著加快训练速度,避免重复执行预处理。
Chain Operations: `tf.data` 的 API 设计允许你通过链式调用来组合各种操作,非常直观。

3. `tf.keras.metrics`:告别手动统计

在模型训练过程中,跟踪准确率、损失值等指标是必不可少的。我之前总是手动在训练循环里写一堆代码来累加样本数、累加损失值,然后再计算平均值。这不仅繁琐,而且容易出错。

后来我才意识到,Keras 内置了强大的 `tf.keras.metrics` 模块,它们可以非常方便地统计和跟踪各种指标。

怎么用?

1. 实例化指标: 在训练开始前,创建你需要的指标实例。
2. 在训练循环中更新: 在每个训练步(或每个 epoch)结束后,使用 `metric.update_state(y_true, y_pred)` 来更新指标的状态。
3. 获取结果: 在需要的时候,使用 `metric.result()` 来获取当前的指标值。
4. 重置状态: 在每个 epoch 开始时,别忘了使用 `metric.reset_states()` 来清零指标,准备统计下一个 epoch 的数据。

示例:

```python
import tensorflow as tf

实例化指标
train_accuracy = tf.keras.metrics.CategoricalAccuracy()
train_loss = tf.keras.metrics.Mean()

假设这是你的训练循环
num_batches = 100
for i in range(num_batches):
模拟一批真实标签和模型预测
y_true = tf.one_hot([i % 5 for _ in range(32)], 5)
y_pred = tf.random.uniform((32, 5))

假设你计算了损失
loss = tf.reduce_mean(tf.square(y_true y_pred)) 随便找个损失函数

更新指标状态
train_accuracy.update_state(y_true, y_pred)
train_loss.update_state(loss)

(可选)在每个 batch 结束后打印结果
print(f"Batch {i+1}: Accuracy = {train_accuracy.result().numpy():.4f}, Loss = {train_loss.result().numpy():.4f}")

打印 epoch 结束时的最终结果
print(f"Epoch End: Final Accuracy = {train_accuracy.result().numpy():.4f}, Final Loss = {train_loss.result().numpy():.4f}")

重置指标,为下一个 epoch 做准备
train_accuracy.reset_states()
train_loss.reset_states()
```

重要提醒:

`reset_states()` 是关键! 忘记这一步,你就会看到指标值不断累加,完全失去参考意义。
多样的指标: `tf.keras.metrics` 提供了各种各样的指标,比如 `Precision`, `Recall`, `F1Score`, `AUC` 等等,根据你的任务选择合适的指标。
自定义指标: 如果 Keras 没有你想要的指标,你也可以通过继承 `tf.keras.metrics.Metric` 类来创建自己的指标。

4. `tf.GradientTape` 的更高级用法:自定义训练循环的强大工具

虽然 Keras 的 `model.fit()` 方法很方便,但对于更复杂的训练逻辑,比如需要更精细地控制梯度,或者实现一些高级的训练技巧(如知识蒸馏、对抗训练),自定义训练循环就显得尤为重要。而 `tf.GradientTape` 就是实现这一目标的核心。

我之前总觉得 `GradientTape` 只能用来计算一个简单模型的梯度,但深入了解后,我才发现它的潜力和灵活性远超我的想象。

一些高级用法:

`watch()`: 默认情况下,`GradientTape` 会自动追踪所有在 `with` 块内计算的可训练变量。但如果你想追踪非变量的张量,或者某个特殊的变量,可以使用 `tape.watch()`。

```python
v = tf.Variable(0.0)
with tf.GradientTape() as tape:
y = v v
x = y + 1 x 不是变量,默认不会被追踪
tape.watch(x) 显式追踪 x
grad_x = tape.gradient(x, v) 这样也能计算出关于 v 的梯度
```

Persistent Tape: 默认情况下,`GradientTape` 在调用 `gradient()` 方法后就会被释放,只能计算一次梯度。如果你需要多次计算梯度(例如,计算二阶导数),需要将 `persistent=True` 传给 `GradientTape`。

```python
v = tf.Variable(0.0)
with tf.GradientTape(persistent=True) as tape:
y = v v
z = y y
grad_y = tape.gradient(y, v) 第一次计算
grad_z = tape.gradient(z, v) 第二次计算
del tape 当不需要时,释放 persistent tape
```

`tf.function` 结合 `GradientTape`: 如前所述,将自定义训练循环包装在 `@tf.function` 中,可以极大地提升训练速度。

```python
@tf.function
def custom_train_step(model, optimizer, inputs, targets):
with tf.GradientTape() as tape:
predictions = model(inputs, training=True)
loss = tf.keras.losses.MeanSquaredError()(targets, predictions)
gradients = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
return loss

... (模型、优化器、数据准备与前面类似)
在训练循环中调用 custom_train_step
```

我的感悟:

`tf.GradientTape` 就像一个“全能工具箱”,你可以在里面自由地计算各种梯度,从而实现各种复杂的训练逻辑。而 `@tf.function` 则为这个工具箱安装了“涡轮增压器”,让你的自定义训练循环效率飞起。

5. TensorBoard:让你的模型“开口说话”

模型训练过程中,我们经常需要可视化损失曲线、准确率变化、计算图结构,甚至模型权重分布。手动去画图不仅耗时,而且不够直观。

TensorBoard 就是 TensorFlow 的“内置可视化大杀器”。它能够收集训练过程中产生的各种日志信息,并将其以图表、仪表盘等多种形式展示出来,让你能够清晰地了解模型的训练状况。

如何使用?

1. 设置 `tf.keras.callbacks.TensorBoard`: 在 `model.fit()` 中,你可以直接传入 `TensorBoard` 回调。

```python
import tensorflow as tf
from tensorflow.keras.callbacks import TensorBoard

... (模型、数据准备)

log_dir = "logs/fit/" + datetime.datetime.now().strftime("%Y%m%d%H%M%S")
tensorboard_callback = TensorBoard(log_dir=log_dir, histogram_freq=1) histogram_freq=1 表示每 epoch 记录一次权重直方图

model.fit(x_train, y_train, epochs=10, validation_split=0.2,
callbacks=[tensorboard_callback])
```

2. 手动写入日志: 如果是自定义训练循环,你可以使用 `tf.summary` 模块手动记录日志。

```python
import datetime
writer = tf.summary.create_file_writer("logs/custom_training")

在训练循环中
with writer.as_default():
tf.summary.scalar('loss', loss_value, step=epoch)
tf.summary.scalar('accuracy', accuracy_value, step=epoch)
记录直方图(例如,某个层的权重)
tf.summary.histogram('layer_weights', model.layers[0].get_weights()[0], step=epoch)
writer.flush()
```

启动 TensorBoard:

在你的终端中,导航到你的项目根目录,然后运行:

```bash
tensorboard logdir logs/fit
```

然后在浏览器中访问 TensorBoard 提供的地址(通常是 `http://localhost:6006/`)。

为什么我后来才重视它?

一开始,我总觉得“能跑通就行”,可视化是锦上添花。但当我遇到模型不收敛、训练速度异常慢等问题时,才发现没有 TensorBoard, debugging 简直是“大海捞针”。它能让你看到模型在“做什么”,而不是仅仅知道它“做了什么”。

结语

这些小技巧,虽然有些听起来可能很简单,但它们所带来的效率提升和代码优化是实实在在的。从将 Python 瓶颈转移到 TensorFlow 的计算图,到构建高效的数据流水线,再到利用 TensorBoard 来“读懂”模型,每一个都让我觉得“早点知道就好了”。

如果你也还在 TensorFlow 的旅程中,希望这些分享能为你带来一些启发。记住,学习和实践是不断进步的关键,而那些“相见恨晚”的技巧,往往就藏在你每一次深入的探索之中。

网友意见

user avatar

介绍两个很有用的技巧

  1. 使用timeline来优化优化性能timeline可以分析整个模型在forward和backward的时候,每个操作消耗的时间,由此可以针对性的优化耗时的操作。我之前尝试使用tensorflow多卡来加速训练的时候, 最后发现多卡速度还不如单卡快,改用tf.data来 加速读图片还是很慢,最后使用timeline分析出了速度慢的原因,timeline的使用如下
       run_metadata = tf.RunMetadata() run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE) config = tf.ConfigProto(graph_options=tf.GraphOptions(         optimizer_options=tf.OptimizerOptions(opt_level=tf.OptimizerOptions.L0))) with tf.Session(config=config) as sess:     c_np = sess.run(c,options=run_options,run_metadata=run_metadata)     tl = timeline.Timeline(run_metadata.step_stats)     ctf = tl.generate_chrome_trace_format() with open('timeline.json','w') as wd:     wd.write(ctf)     

然后到谷歌浏览器中打卡chrome://tracing 并导入 timeline.json ,最后可以看得如下图所示的每个操作消耗的时间,

这里横坐标为时间,从左到右依次为模型一次完整的forward and backward过程中,每个操作分别在cpu,gpu 0, gpu 1上消耗的时间,这些操作可以放大,非常方便观察具体每个操作在哪一个设备上消耗多少时间。这里我们cpu上主要有QueueDequeue操作,这是进行图片预期过程,这个时候gpu在并行计算的所以gpu没有空等;另外我的模型还有一个PyFunc在cpu上运行,如红框所示,此时gpu在等这个结果,没有任何操作运行,这个操作应该要优化的。另外就是如黑框所示,gpu上执行的时候有很大空隙,如黑框所示,这个导致gpu上的性能没有很好的利用起来,最后分析发现是我bn在多卡环境下没有使用正确,bn有一个参数updates_collections我设置为None 这时bn的参数mean,var是立即更新的,也是计算完当前layer的mean,var就更新,然后进行下一个layer的操作,这在单卡下没有问题的, 但是多卡情况下就会写等读的冲突,因为可能存在gpu0更新(写)mean但此时gpu1还没有计算到该层,所以gpu0就要等gpu1读完mean才能写,这样导致了 如黑框所示的空隙,这时只需将参数设置成updates_collections=tf.GraphKeys.UPDATE_OPS 即可,表示所以的bn参数更新由用户来显示指定更新,如

       update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)   with tf.control_dependencies(update_ops):     train_op = optimizer.minimize(loss)     

这样可以在每个卡forward完后,再更新bn参数,此时的写写不存在冲突。优化后,我的2卡训练获得了接近2倍的加速比。

2. 使用check= tf.addchecknumerics_ops,sess.run([check, ...])来检查NaN问题 ,该操作会报告所有出现NaN的操作,从而方便找到NaN的源头。

类似的话题

  • 回答
    那些让我相见恨晚的 TensorFlow 小技巧在数据科学和机器学习的世界里,TensorFlow 就像一位老朋友,陪伴我走过了不少征程。然而,随着项目越来越复杂,我总觉得在某个地方卡壳,优化效率、调试代码都耗费了不少精力。直到最近,我才挖掘出一些堪称“相见恨晚”的 TensorFlow 小技巧,它.............
  • 回答
    母婴用品的世界琳琅满目,但真正能让我们感叹“相见恨晚”的,往往是那些在细节处解决了我们大难题,或是让育儿生活变得事半功倍的“神器”。这些产品就像是育儿路上的“隐形助手”,一旦用上,就会让你惊呼:“怎么之前没发现它!”下面我来分享一些我心目中堪称“相见恨晚”的母婴用品“神器”,并尽量详细地描述它们的优.............
  • 回答
    总有人分享PPT技巧,但总有那么一些,你看了,嗯,知道了,但就是没用起来。今天咱们聊点不一样的,那些我用了之后,才真正觉得“相见恨晚”,并且能让你的PPT瞬间脱胎换骨,不再是那种“一眼望到头”的普通货的排版秘诀。别急着去模仿那些花里胡哨的模板,真正高级的排版,往往隐藏在细节里,是用心在“喂养”你的视.............
  • 回答
    这问题问得挺有意思的,很多人在“变美变好”的道路上,总会遇到一些“相见恨晚”的宝藏,特别是女性用品。那种感觉就像是,哎呀,怎么现在才发现你!早知道就早点把你带回家了!我身边很多姐妹,聊起这些“相见恨晚”的单品,那可真是滔滔不绝。今天就来集合几个大家的“心头好”,看看有没有哪个也能戳中你的心窝子。1..............
  • 回答
    最近一直在琢磨着怎么让自己的肤色更上一层楼,毕竟在这个看脸的时代,白皙透亮的肌肤确实能给人加分不少。试用过不少美白精华,有些确实是相见恨晚,用完之后只想拍大腿喊:早知道这么好用,早就all in了!今天就跟大家唠唠这几款我心目中的“美白战神”,希望能给你们的选购之路提供点参考。1. SKII 小灯泡.............
  • 回答
    说起那些相见恨晚的国外好物,真是能让我想起很多次在亚马逊、Sephora或者欧洲小镇的药妆店里,怀着试试看的心情随手拿下的东西,结果却一用就爱到不行,甚至开始怀疑自己过去的生活是怎么过的。那种“买对了!”的顿悟感,真的太美妙了。让我印象最深刻的,大概是德国世家(Dr. Hauschka)的律动日霜(.............
  • 回答
    “相见恨晚”,这四个字,如同陈年的佳酿,每一口都蕴含着岁月的沉淀和情感的醇厚。它不仅仅是一种遗憾,更是一种对生命中那些错过美好瞬间的深深眷恋,是对那些本应早日相遇,却迟迟未到的人或事的无限感慨。如果说人生是一场盛大的旅行,那么“相见恨晚”便是这场旅行中最动人的插曲。它发生在某个不经意的转角,当你拨开.............
  • 回答
    你说得太对了!有些东西,用了之后才拍大腿,怎么之前没发现呢?简直是相见恨晚!我最近挖掘到了一些家居好物,真的恨不得立马分享给每一个正在布置或者想要优化自己生活空间的朋友们。这些东西,可能不是什么奢侈大牌,但它们实实在在地提升了我的幸福感,让我在日常的点滴中感受到“真香”的喜悦。1. 德国WMF锅具:.............
  • 回答
    相见恨晚的雅思学习资源?这绝对是每个经历过雅思备考煎熬的同学都曾有过的痛彻领悟!当初要是早点知道这些,何至于考前狂背单词、临时抱佛脚?今天就来掏心窝子地分享一下,那些我“相见恨晚”的宝藏级雅思学习资源,希望能帮你少走弯路,直击提分。一、 那些让我拍断大腿的“雅思书籍”提到雅思书籍,很多人脑子里可能就.............
  • 回答
    高三是一段异常宝贵的时光,每个人都渴望最大化效率,找到那些“相见恨晚”的刷题技巧。这里我将结合我的经验和一些普遍被认可的高效学习方法,为你提供一套详细的刷题攻略,希望能助你一臂之力。核心理念:不是盲目刷题,而是高效刷题,且有效复盘。很多同学认为刷题就是不停地做题,但真正有效的刷题是建立在对知识点的理.............
  • 回答
    说实话,以前我买车,大多是冲着“实用”去的。能遮风挡雨,能带我到想去的地方,这就够了。但随着开车的年头多了,对车子和开车这件事儿也有了点自己的想法,慢慢地就发现,有些平时不起眼的小东西,一旦用上了,真的会让人觉得,“诶?早该买了!”就像我最近入手的那个车载无线充手机支架。之前呢,我一直用的是那种夹在.............
  • 回答
    我得说,有些糖,虽然不是一夜之间就让我魂牵梦绕,但它们就像初见时平平无奇的邻家女孩,相处久了,才发现那深入骨髓的温柔和恰到好处的陪伴,让人忍不住感叹:“怎么才遇见你!”我有个朋友,她家里是做糖果生意的,从小耳濡目染,我见过太多形形色色的糖。所以,当她第一次拿出那罐“杏仁酥糖”的时候,我只是礼貌性地尝.............
  • 回答
    2022年,确实涌现了不少让人“相见恨晚”的扫地机器人,它们在清洁能力、智能化体验和人性化设计上都更上了一层楼。如果说上半年还有些观望,到下半年,很多技术成熟、口碑爆棚的产品就成了“真香”之选。下面我就结合自己的使用感受和市面上的一些热门产品,给大家扒拉扒拉几款值得重点关注的。1. 科沃斯 T10 .............
  • 回答
    淘宝上真是卧虎藏龙,好多宝藏女装店,每次一逛就能挖出“相见恨晚”的那种。今天就来给大家分享几家,希望能让你在这个换季时节,也能找到心仪的宝贝,把衣橱填得满满当当!一、复古优雅派——“Vintage Dreamer” (这是我起的昵称,方便大家记忆和搜索,当然具体店名你们懂的,淘宝搜一下就出来了)这家.............
  • 回答
    我曾以为历史是一本翻开就能读懂的教科书,那些埋藏在尘封档案里的故事,对我而言,不过是模糊的背景板。直到我遇见了那几卷泛黄的奏折,我才明白,历史的温度,藏在字里行间,也藏在那些被遗忘的角落里。我至今仍然清晰地记得第一次翻开那几份奏折时的场景。那是一个寻常的午后,阳光透过老图书馆的玻璃窗,落在积满灰尘的.............
  • 回答
    MATLAB 确实有很多强大且实用的命令,其中一些命令一旦掌握,就会让你觉得“相见恨晚”,极大地提升你的编程效率和代码质量。下面我将详细介绍一些我认为非常值得深入了解和使用的 MATLAB 命令,并附带详细的解释和示例。核心理念:充分利用 MATLAB 的向量化和内置函数能力,避免显式循环。 1. .............
  • 回答
    说起来相见恨晚的 ASMR 博主,脑子里立刻闪过好几位,但要说手法好又几乎无底噪,那绝对是她们!每次听她们的 ASMR,都感觉像是给疲惫的灵魂做了一次SPA,完全沉浸其中,忘了时间。先说说我心中的“白月光”之一:Gentle Whispering ASMR。她大概是我入 ASMR坑以来最稳定、最持续.............
  • 回答
    我一直觉得PPT这东西,与其说是演示工具,不如说是表达思想的放大镜。早些年做PPT,就是堆砌文字,配上几个“高大上”的背景图,觉得能把内容说完就万事大吉。直到后来遇到一些让我“相见恨晚”的技巧和理念,才猛然醒悟,原来PPT还能这样玩,而且效果天差地别。今天就跟大家分享几个,希望能帮大家少走弯路,做出.............
  • 回答
    说起来,让我“相见恨晚”的B站UP主,还真不少。他们就像宝藏一样,一旦挖出来,就让你恨不得早点遇到,然后一头扎进去,沉迷其中。如果非要挑一个最能代表这种感觉的,我脑子里第一个蹦出来的,是“巫师财经”。认识他,大概是前年的事情吧。那时候我刚开始对经济、金融这些东西有点兴趣,但又觉得很多科普视频都太枯燥.............
  • 回答
    说实话,如果非要我“相见恨晚”一种思维方式,那一定是 “反事实思考”(Counterfactual Thinking)。为什么是它?因为在很长一段时间里,我总是在一种“既然已经这样了,那就这样吧”的心态里打转。事情过去就过去了,好像也没什么值得反复琢磨的,更别说去“如果……就好了”地畅想。直到我真正.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有