☰
深度学习入门路线:从神经网络到Transformer的TensorFlow实战
2026/10/20 7:27:39 网站建设 项目流程

深度学习这个领域,刚入门的时候最容易犯的一个错误就是“贪多嚼不烂”。我见过太多人一上来就想搞懂 Transformer 的注意力机制,结果连张量在内存里长什么样都没搞清楚,最后只能对着论文干瞪眼。这篇内容就是写给那些想踏踏实实把地基打牢的人——不管你是刚转行的开发者,还是被课程作业逼着上手的学生,只要跟着把神经网络、计算机视觉、自然语言处理和 Transformer 这几块骨头啃下来,后面再去看那些花里胡哨的模型,心里就有底了。我会把每个环节里最容易踩的坑、最值得花时间理解的原理,以及可以直接抄作业的代码配置都摊开来讲,尽量让你少走我当年走过的弯路。

1. 整体学习路线与框架选型思路

1.1 为什么从 TensorFlow 切入而不是其他框架

很多人纠结第一个框架选什么,我的建议很直接:如果你打算把模型部署到移动端或者浏览器里,TensorFlow 这套生态目前仍然是省心程度最高的选择。它的 SavedModel 格式在跨平台推理时基本不需要你做太多额外适配,TFLite 和 TensorFlow.js 两条路都铺好了。相比之下,另一个主流框架在科研圈更流行,但你要把它塞进手机里,中间转换的折腾程度会明显上升一个台阶。

从学习曲线来看,TensorFlow 2.x 把 Eager Execution 作为默认模式之后,写代码的体验已经和写普通 Python 脚本没太大区别了。你可以随时打印中间张量的值,不用再像 1.x 时代那样先建图再跑会话。这对新手来说太重要了——调试成本直接砍掉一大半。我个人的习惯是,入门阶段先用 Keras 的高层 API 把模型跑通,等对整体流程有感觉了,再逐步往下钻,去看 GradientTape 怎么记录梯度、自定义层怎么写。

还有一个很实际的原因:TensorFlow 的官方文档和社区示例覆盖面非常广。你遇到的大多数报错,基本都能在 Stack Overflow 或者 GitHub Issues 里找到对应的讨论。这不是说其他框架社区不好,而是 TensorFlow 因为历史积累久,很多“坑”已经被踩过无数遍了,你搜到的答案往往更直接。

1.2 四个模块的递进关系与时间分配建议

神经网络是地基,计算机视觉和自然语言处理是两个主要应用方向,Transformer 则是当前这两个方向共同的前沿架构。这个顺序不是随便排的,它对应着认知负荷的合理递增。

我建议的时间分配大致是这样的:神经网络基础部分花 30% 的时间,重点把反向传播、损失函数、优化器这几个概念吃透;计算机视觉和自然语言处理各占 25%,这两个方向可以并行推进,因为它们共享很多底层操作,比如嵌入层、归一化、注意力机制的变体;最后 20% 留给 Transformer,因为前面打好了基础,到这里更多是架构层面的组合创新,而不是全新的数学原理。

注意:不要跳过神经网络基础直接去搞 Transformer。我见过有人直接抄了一个 Transformer 的代码跑通了,但问他位置编码为什么要用正弦函数就答不上来。这种“跑通”没有意义,换个数据集或者改个任务就露馅了。

1.3 环境搭建的极简方案与版本锁定策略

环境这块我踩过最大的坑就是版本冲突。TensorFlow 对 CUDA 和 cuDNN 的版本要求非常严格,你装错一个小版本号,可能就是“GPU 不可用”或者“找不到动态库”的报错。我的建议是,直接用 conda 创建一个独立环境,然后按照官方兼容性表格来锁定版本。

conda create -n tf-deep-learning python=3.10 conda activate tf-deep-learning pip install tensorflow==2.15.0 pip install matplotlib numpy pandas jupyter

如果你没有 GPU,用 CPU 版本完全没问题。入门阶段的模型规模都不大,CPU 跑 MNIST 或者 IMDB 这种数据集,一轮 epoch 也就几十秒的事。等你要训练 ResNet 或者 BERT 这种级别的模型时,再考虑上 GPU 也不迟。

验证安装是否成功,跑这几行就够了:

import tensorflow as tf print(tf.__version__) print(tf.config.list_physical_devices('GPU'))

如果 GPU 列表是空的但你有显卡,先别急着怀疑人生,大概率是 CUDA 版本没对上。去 TensorFlow 官网查一下“Tested build configurations”那个表格,对照着装就行。

2. 神经网络核心原理与实操细节

2.1 从张量到自动微分:理解 TensorFlow 的运算本质

张量这个词听起来唬人,其实就是一个多维数组。标量是 0 维张量,向量是 1 维,矩阵是 2 维,再往上就是高维张量。TensorFlow 的所有运算都是围绕张量展开的,你写的每一行模型代码,最终都会被翻译成张量之间的加减乘除。

自动微分是 TensorFlow 最核心的能力之一。它的原理并不神秘:在前向传播的过程中,框架会记录下所有参与运算的操作,形成一个“计算图”的轨迹;反向传播时,沿着这个轨迹反向应用链式法则,就能自动算出每个参数的梯度。GradientTape 就是干这个的。

x = tf.Variable(3.0) with tf.GradientTape() as tape: y = x ** 2 + 2 * x + 1 dy_dx = tape.gradient(y, x) print(dy_dx.numpy()) # 输出 8.0

这段代码里,tape.gradient返回的就是 y 对 x 的导数。你可以手动算一下:y = x² + 2x + 1 的导数是 2x + 2,代入 x=3 得到 8。框架算出来的结果和手算一致,说明自动微分是可靠的。

实操心得:GradientTape 默认只监控 tf.Variable 类型的变量。如果你用 tf.constant 创建了一个张量,它是不会被自动追踪的。这个细节在自定义训练循环时特别容易忽略,导致梯度返回 None。

2.2 损失函数与优化器的选择逻辑

损失函数衡量的是模型预测值和真实值之间的差距,优化器则负责根据这个差距来调整参数。这两个东西的选择不是拍脑袋决定的,而是由任务类型和数据分布决定的。

分类任务常用交叉熵损失。二分类用 BinaryCrossentropy,多分类用 CategoricalCrossentropy 或者 SparseCategoricalCrossentropy。这两者的区别在于标签的格式:前者要求标签是 one-hot 编码,后者接受整数标签。我个人的习惯是优先用 Sparse 版本,因为省去了手动转换标签的步骤,内存占用也更小。

回归任务用均方误差(MSE)或者平均绝对误差(MAE)。MSE 对大误差更敏感,因为误差被平方了;MAE 对异常值更鲁棒。如果你的数据里离群点比较多,MAE 可能是更稳妥的选择。

优化器方面,Adam 是目前最常用的默认选择。它结合了动量和自适应学习率的优点,在大多数任务上都能给出不错的结果。学习率一般从 1e-3 开始试,如果损失震荡得厉害就降到 1e-4,如果下降太慢就升到 1e-2。SGD 加动量在某些视觉任务上能比 Adam 收敛到更好的局部最优,但需要更精细的学习率调度,入门阶段可以先不折腾。

任务类型推荐损失函数推荐优化器初始学习率
二分类BinaryCrossentropyAdam1e-3
多分类SparseCategoricalCrossentropyAdam1e-3
回归MSE / MAEAdam1e-3
序列生成CategoricalCrossentropyAdam5e-4

2.3 过拟合的识别与正则化手段

过拟合的典型表现是:训练集上的损失持续下降,但验证集上的损失在某个点之后开始反弹。这时候模型已经不是在学规律,而是在背答案了。

识别过拟合最简单的方法就是画学习曲线。把每个 epoch 的训练损失和验证损失都记录下来,画在同一张图上。两条曲线分叉的那个点,就是过拟合的起点。

正则化手段有好几种,我按使用频率排个序:

  • Dropout:在训练时随机丢弃一部分神经元的输出,迫使网络不依赖某些特定的路径。一般放在全连接层后面,丢弃率 0.2 到 0.5 之间。注意,推理阶段 Dropout 是不生效的,TensorFlow 会自动处理这个切换。
  • L2 正则化:在损失函数里加上权重的平方和乘以一个系数,惩罚过大的权重。系数一般取 1e-4 到 1e-2。
  • 早停:验证损失连续几个 epoch 不下降就停止训练。这是最省事也最有效的手段之一,配合 ModelCheckpoint 回调一起用,可以自动保存验证损失最低的那个模型。
  • 数据增强:对训练样本做随机变换来扩充数据集。这个在计算机视觉里特别常用,后面会展开讲。
from tensorflow.keras import layers, regularizers model = tf.keras.Sequential([ layers.Dense(128, activation='relu', kernel_regularizer=regularizers.l2(1e-4)), layers.Dropout(0.3), layers.Dense(64, activation='relu'), layers.Dropout(0.3), layers.Dense(10, activation='softmax') ])

注意:Dropout 和 L2 正则化不要同时堆太多。我试过在一个小模型上同时加了三层 Dropout 和 L2,结果模型欠拟合了,训练损失都降不下去。正则化是药,不是饭,适量就行。

3. 计算机视觉实战:从卷积到迁移学习

3.1 卷积神经网络的直观理解与关键参数

卷积操作的本质是用一个小窗口(卷积核)在图像上滑动,每次滑动都做一次加权求和。这个操作能提取局部特征,比如边缘、纹理、角点。随着层数加深,后面的卷积层会组合前面的低级特征,形成更高级的语义特征,比如眼睛、轮子、文字。

几个关键参数需要搞清楚:

  • 卷积核大小:常用 3x3 和 5x5。3x3 是最常见的,因为两个 3x3 卷积的感受野等于一个 5x5,但参数更少,非线性更强。
  • 步长:控制卷积核滑动的距离。步长为 1 时输出尺寸基本不变,步长为 2 时输出尺寸减半,相当于下采样。
  • 填充:Same 填充让输出尺寸和输入一致,Valid 填充不补零,输出会缩小。
  • 通道数:每个卷积层输出的特征图数量。一般从 32 开始,逐层翻倍到 64、128、256。

输出尺寸的计算公式是:output = (input - kernel + 2 * padding) / stride + 1。举个例子,输入 32x32,卷积核 3x3,padding=1,stride=1,输出就是 (32-3+2)/1+1 = 32,尺寸不变。如果 stride=2,输出就是 (32-3+2)/2+1 = 16.5,向下取整得到 16。

3.2 一个完整的图像分类流程:以 CIFAR-10 为例

CIFAR-10 是入门计算机视觉的经典数据集,包含 10 个类别的 32x32 彩色图像,训练集 50000 张,测试集 10000 张。数据量适中,CPU 也能跑,非常适合练手。

import tensorflow as tf from tensorflow.keras import layers, models # 加载数据 (x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data() # 归一化 x_train = x_train.astype('float32') / 255.0 x_test = x_test.astype('float32') / 255.0 # 数据增强 data_augmentation = tf.keras.Sequential([ layers.RandomFlip('horizontal'), layers.RandomRotation(0.1), layers.RandomZoom(0.1), ]) # 构建模型 model = models.Sequential([ data_augmentation, layers.Conv2D(32, 3, padding='same', activation='relu'), layers.MaxPooling2D(), layers.Conv2D(64, 3, padding='same', activation='relu'), layers.MaxPooling2D(), layers.Conv2D(128, 3, padding='same', activation='relu'), layers.MaxPooling2D(), layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dropout(0.5), layers.Dense(10, activation='softmax') ]) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) history = model.fit(x_train, y_train, epochs=20, validation_split=0.1, batch_size=64)

这个模型大概能跑到 70% 左右的测试准确率。想再往上提,就得靠迁移学习了。

3.3 迁移学习的正确打开方式

迁移学习的思路是:拿一个在大规模数据集上预训练好的模型,把它的卷积基拿过来当特征提取器,只训练自己加的分类头。这样做的好处是,预训练模型已经学会了如何提取通用的视觉特征,你只需要让它适应你的特定任务就行。

TensorFlow 里用预训练模型非常方便:

base_model = tf.keras.applications.MobileNetV2( input_shape=(128, 128, 3), include_top=False, weights='imagenet' ) base_model.trainable = False # 冻结卷积基 model = models.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dense(128, activation='relu'), layers.Dropout(0.3), layers.Dense(10, activation='softmax') ])

实操心得:冻结卷积基训练几轮之后,可以解冻最后几层做微调,学习率要调小,比如 1e-5。直接解冻全部层用大学习率去训,会把预训练学到的特征全破坏掉,效果反而不如不迁移。

还有一个细节:预训练模型对输入尺寸有要求。MobileNetV2 默认是 224x224,你改成 128x128 也能跑,但特征提取能力会打折扣。如果计算资源允许,尽量用默认尺寸。

4. 自然语言处理入门:从词嵌入到序列建模

4.1 文本预处理的标准流程

自然语言处理的原始输入是文本,模型只认数字,所以第一步永远是文本转数字。这个流程包括分词、构建词表、序列填充。

分词最简单的方式是按空格切分,但中文需要专门的分词工具。入门阶段可以用字符级分词,就是把每个字当成一个 token,这样词表小,也不用担心未登录词的问题。

from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences texts = ['这个电影太好看了', '剧情很糟糕', '演员表现不错'] tokenizer = Tokenizer(char_level=True, oov_token='<OOV>') tokenizer.fit_on_texts(texts) sequences = tokenizer.texts_to_sequences(texts) padded = pad_sequences(sequences, maxlen=20, padding='post', truncating='post')

maxlen的选择要看数据分布。你可以先统计一下所有文本长度的中位数和 95 分位数,取 95 分位数作为 maxlen,这样能覆盖绝大多数样本,又不至于让填充太多。

4.2 词嵌入:让模型理解语义关系

词嵌入把每个词映射到一个稠密向量,语义相近的词在向量空间里的距离也相近。经典的例子是“国王 - 男人 + 女人 ≈ 女王”,这说明词嵌入确实捕捉到了某种语义关系。

TensorFlow 里用 Embedding 层来实现:

embedding_layer = layers.Embedding( input_dim=10000, # 词表大小 output_dim=64, # 嵌入维度 input_length=20 # 序列长度 )

嵌入维度一般取 50 到 300 之间。太小了表达力不够,太大了参数多容易过拟合。对于小数据集,64 或 128 是比较稳妥的选择。

Embedding 层有两种用法:一种是自己随机初始化然后跟着模型一起训练,适合数据量大的场景;另一种是加载预训练好的词向量,然后冻结或者微调,适合数据量小的场景。入门阶段先用随机初始化就行,等有经验了再去折腾预训练词向量。

4.3 循环神经网络与 LSTM 的实操要点

循环神经网络的核心思想是让网络有“记忆”,当前时刻的输出不仅取决于当前输入,还取决于上一时刻的隐藏状态。这让它天然适合处理序列数据。

但朴素的 RNN 有个致命问题:梯度消失。当序列很长时,反向传播的梯度会指数级衰减,导致网络学不到长距离的依赖关系。LSTM 通过引入门控机制来解决这个问题,它有三个门:遗忘门决定丢弃哪些旧信息,输入门决定写入哪些新信息,输出门决定输出哪些信息。

model = tf.keras.Sequential([ layers.Embedding(10000, 64), layers.Bidirectional(layers.LSTM(64, return_sequences=True)), layers.Bidirectional(layers.LSTM(32)), layers.Dense(64, activation='relu'), layers.Dropout(0.5), layers.Dense(1, activation='sigmoid') ])

双向 LSTM 会同时从前往后和从后往前处理序列,然后把两个方向的输出拼接起来。对于文本分类这种不需要实时生成的场景,双向结构通常比单向效果好。

注意:LSTM 的训练速度比卷积慢很多,因为它的计算是串行的,没法像卷积那样并行化。如果你的序列特别长,可以考虑用一维卷积或者 Transformer 来替代。

5. Transformer 架构拆解与实现要点

5.1 自注意力机制到底在算什么

自注意力是 Transformer 的核心。它的计算过程可以这样理解:对于序列中的每个位置,都去问其他所有位置“你和我有多相关”,然后根据相关程度加权求和,得到当前位置的新表示。

具体来说,每个输入向量会被映射成三个向量:Query、Key、Value。Query 代表“我在找什么”,Key 代表“我有什么”,Value 代表“我能提供什么”。注意力分数就是 Query 和 Key 的点积,经过缩放和 Softmax 之后,得到一组权重,再用这些权重对 Value 加权求和。

class SelfAttention(layers.Layer): def __init__(self, d_model): super().__init__() self.d_model = d_model self.wq = layers.Dense(d_model) self.wk = layers.Dense(d_model) self.wv = layers.Dense(d_model) def call(self, x): q = self.wq(x) k = self.wk(x) v = self.wv(x) scores = tf.matmul(q, k, transpose_b=True) scores = scores / tf.math.sqrt(tf.cast(self.d_model, tf.float32)) weights = tf.nn.softmax(scores, axis=-1) return tf.matmul(weights, v)

缩放这一步很关键。如果不除以根号 d_model,当维度很大时,点积的结果会非常大,Softmax 之后会变成接近 one-hot 的分布,梯度几乎为零,训练不动。

5.2 多头注意力与位置编码的必要性

多头注意力就是把自注意力并行地做多次,每次用不同的投影矩阵,最后把结果拼接起来再投影一次。这样做的好处是,不同的头可以关注不同的模式,比如有的头关注语法关系,有的头关注语义相似度。

位置编码是因为自注意力本身没有顺序概念。你把输入序列打乱,自注意力的输出只会跟着打乱,不会意识到顺序变了。所以需要额外注入位置信息。原始论文用的是正弦和余弦函数:

def positional_encoding(max_len, d_model): positions = np.arange(max_len)[:, np.newaxis] dims = np.arange(d_model)[np.newaxis, :] angles = positions / np.power(10000, (2 * (dims // 2)) / d_model) angles[:, 0::2] = np.sin(angles[:, 0::2]) angles[:, 1::2] = np.cos(angles[:, 1::2]) return tf.cast(angles[np.newaxis, ...], dtype=tf.float32)

这种编码方式的好处是,对于任意固定的偏移量 k,位置 p+k 的编码可以表示为位置 p 编码的线性函数。这让模型能够泛化到训练时没见过的序列长度。

5.3 搭建一个迷你 Transformer 做文本分类

把前面几块拼起来,就是一个完整的 Transformer 编码器。下面是一个简化版的实现:

class TransformerBlock(layers.Layer): def __init__(self, d_model, num_heads, ff_dim, dropout=0.1): super().__init__() self.att = layers.MultiHeadAttention(num_heads=num_heads, key_dim=d_model) self.ffn = tf.keras.Sequential([ layers.Dense(ff_dim, activation='relu'), layers.Dense(d_model) ]) self.layernorm1 = layers.LayerNormalization(epsilon=1e-6) self.layernorm2 = layers.LayerNormalization(epsilon=1e-6) self.dropout1 = layers.Dropout(dropout) self.dropout2 = layers.Dropout(dropout) def call(self, inputs, training): attn_output = self.att(inputs, inputs) attn_output = self.dropout1(attn_output, training=training) out1 = self.layernorm1(inputs + attn_output) ffn_output = self.ffn(out1) ffn_output = self.dropout2(ffn_output, training=training) return self.layernorm2(out1 + ffn_output)

这个块里有两个残差连接,分别包住注意力层和前馈网络层。残差连接的作用是缓解梯度消失,让深层网络也能训练。LayerNormalization 放在残差相加之后,这是原始论文的做法,虽然后来有些变体把它放在前面,但入门阶段先按标准来。

用这个块堆几层,加上嵌入层和位置编码,再接一个全局平均池化和分类头,就是一个能跑的文本分类模型了。在 IMDB 情感分析数据集上,两三层的迷你 Transformer 就能跑到 85% 以上的准确率。

实操心得:Transformer 对小数据集容易过拟合,Dropout 和权重衰减都要加上。另外,学习率预热(warmup)对 Transformer 的训练稳定性帮助很大,前几百步从很小的学习率线性增加到设定值,之后再正常衰减。

6. 常见问题与排查技巧实录

6.1 损失不下降或变成 NaN 的排查顺序

这个问题太常见了,我按排查优先级列一下:

  1. 检查学习率:学习率太大是最常见的原因。先把学习率降到 1e-4 甚至 1e-5 试试。
  2. 检查数据归一化:输入数据没有归一化到合理范围,会导致梯度爆炸。图像数据除以 255,数值特征做标准化。
  3. 检查损失函数和标签格式:多分类用了 BinaryCrossentropy,或者标签没有做 one-hot,都会导致损失计算错误。
  4. 检查是否有 log(0):交叉熵损失里如果预测概率是 0,log(0) 就是负无穷。TensorFlow 的损失函数内部做了裁剪,但自定义损失函数时要注意。
  5. 梯度裁剪:在优化器里加clipnorm=1.0或clipvalue=0.5,能有效防止梯度爆炸。

6.2 显存不足的应对策略

显存不够用的时候,按这个顺序尝试:

  • 减小 batch size,这是最直接有效的。
  • 用混合精度训练,tf.keras.mixed_precision.set_global_policy('mixed_float16'),能省将近一半显存。
  • 用梯度累积模拟大 batch,就是跑几个小 batch 再更新一次参数。
  • 检查有没有不必要的张量被保留在内存里,比如在训练循环里不断往列表里 append 损失值,时间长了会撑爆内存。

6.3 模型在验证集上表现远差于训练集

这就是过拟合的典型症状。除了前面说的 Dropout 和 L2 正则化,还可以:

  • 增加数据量,或者用数据增强。
  • 减小模型容量,减少层数或每层的单元数。
  • 早停,用 ModelCheckpoint 保存验证损失最低的模型。
  • 检查训练集和验证集的分布是否一致,有时候是数据划分出了问题。
问题现象可能原因优先尝试的解决方案
损失 NaN学习率过大降低学习率到 1e-4
损失不降数据未归一化检查输入范围并标准化
验证损失反弹过拟合加 Dropout 或早停
显存溢出batch 太大减小 batch 或混合精度
梯度为 None变量未被追踪确认用 tf.Variable

6.4 训练速度慢的优化方向

如果你觉得训练太慢,先确认瓶颈在哪。用tf.data的prefetch和cache能显著提升数据加载速度:

train_ds = train_ds.cache().shuffle(1000).batch(64).prefetch(tf.data.AUTOTUNE)

cache把数据缓存在内存里,避免每个 epoch 都重新读盘。prefetch让数据加载和模型计算重叠进行。这两个操作基本是免费的加速,不用白不用。

另外,检查一下是不是在用 CPU 跑本来该用 GPU 跑的模型。tf.config.list_physical_devices('GPU')返回空列表的话,说明 GPU 没被识别到,得去查 CUDA 和 cuDNN 的版本匹配问题。

7. 一些个人体会与后续扩展方向

这套入门路线我自己走过一遍,也带过几个人走。最大的感受是:不要追求一次把所有细节都搞懂。第一遍先把代码跑通,看到损失在降、准确率在升,建立正反馈;第二遍再回头去看每个 API 的参数含义;第三遍才去深挖背后的数学原理。这个顺序比反过来要高效得多。

后续如果想继续深入,有几个方向可以选:一是把 Transformer 扩展到序列到序列的任务,比如机器翻译或文本摘要,这需要理解解码器和交叉注意力;二是把计算机视觉里的目标检测和图像分割啃下来,这需要理解锚框和非极大值抑制;三是学模型部署,把训练好的模型转成 TFLite 放到移动端,或者用 TensorFlow Serving 做在线推理。每个方向都够写好几篇内容了,但只要你把这篇里的基础打牢了,后面无非是换不同的层和损失函数而已。

最后分享一个小技巧:养成写实验日志的习惯。每次跑完一个模型,把超参数、训练曲线、最终指标都记下来。我一开始觉得麻烦,后来发现当你想复现三个月前的一个结果时,没有日志简直就是灾难。用 TensorBoard 也行,但简单的 Markdown 表格有时候更直接。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询