从反向传播到Transformer:深度学习如何理解语言 1. 从一场演讲说起为什么“理解语言”这件事值得死磕Geoffrey Hinton 在皇家研究院的那场演讲我前后刷了三遍。第一遍看热闹第二遍看门道第三遍干脆暂停做笔记。他讲的东西表面上是在聊深度学习怎么处理语言实际上是在回答一个更底层的问题机器到底能不能“懂”我们在说什么。这个问题听起来很哲学但落到工程上它直接决定了你手里的 Transformer 该怎么调、反向传播该怎么推、大语言模型该不该继续堆参数。如果你正在学深度学习或者已经在大语言模型这条路上踩过几个坑这场演讲里的很多观点会让你有“原来如此”的感觉。比如他提到语言理解的核心不在于语法树而在于高维向量空间里的语义关系。这句话翻译成实操语言就是你不需要让模型学会主谓宾你只需要让它学会在向量空间里把“猫”和“狗”放得近一点把“猫”和“冰箱”放得远一点。这个思路直接催生了 Word2Vec、BERT、GPT 这一整条技术路线。我写这篇东西的目的很简单把 Hinton 演讲里那些看似抽象的观点拆成你能直接上手用的技术细节。不管你是刚看完吴恩达课后题、正在手写 Transformer 的新手还是已经在本地部署大语言模型、天天跟 loss 曲线搏斗的老手下面这些内容都能让你对“深度学习理解语言”这件事有一个更落地的认知。我会从反向传播的数学本质讲到 Transformer 的注意力机制从词向量的空间几何讲到数字智能为什么可能在某个拐点之后把人类甩开。全程不堆公式但该有的计算过程一个不少。2. 反向传播不是“算法”而是一种分配责任的思维方式2.1 反向传播到底在算什么很多人第一次学反向传播的时候会被链式法则那一堆偏导数吓到。我当年也是看着 BP 神经网络结构图里那些箭头来回指脑子里只有一个想法这玩意儿到底在干嘛。后来我想通了一件事反向传播本质上是在做责任分配。你给神经网络一个输入它吐出一个输出如果输出错了那是谁的锅是第一层权重的问题还是第三层激活函数的问题反向传播就是一套系统化的“甩锅”机制把最终误差按照贡献大小一层一层往回传。具体来说假设你有一个三层前馈神经网络输入是 ( x )输出是 ( \hat{y} )真实标签是 ( y )。损失函数 ( L(\hat{y}, y) ) 衡量的是预测和真实值之间的差距。反向传播要做的就是计算 ( \frac{\partial L}{\partial W^{(l)}} )也就是每一层权重对最终损失的偏导数。这个偏导数告诉你如果我把这个权重稍微调大一点损失会变大还是变小变化多少。然后你用梯度下降去更新权重让损失变小。这里有一个很多人忽略的细节反向传播的计算复杂度跟前向传播是同一个量级。这意味着你每做一次前向推理只需要再多花差不多一倍的时间就能拿到所有参数的梯度。这个性质是深度学习能 scale 起来的根本原因之一。如果每次更新参数都要重新跑一遍完整的前向过程那训练一个大语言模型的时间会膨胀到不可接受的程度。2.2 手推一个两层网络的梯度光说理论没意思我带你手推一个最简单的两层网络。假设网络结构是输入 ( x \in \mathbb{R}^2 )第一层权重 ( W_1 \in \mathbb{R}^{2 \times 3} )激活函数是 ReLU第二层权重 ( W_2 \in \mathbb{R}^{3 \times 1} )输出一个标量。损失函数用均方误差 ( L \frac{1}{2}(\hat{y} - y)^2 )。前向过程import numpy as np x np.array([1.0, 2.0]) W1 np.random.randn(2, 3) b1 np.zeros(3) W2 np.random.randn(3, 1) b2 np.zeros(1) z1 x W1 b1 a1 np.maximum(0, z1) # ReLU z2 a1 W2 b2 y_hat z2反向过程先算输出层的梯度y np.array([1.0]) dL_dyhat y_hat - y # 均方误差的导数 dL_dW2 a1.reshape(-1, 1) dL_dyhat.reshape(1, -1) dL_db2 dL_dyhat然后传到第一层dL_da1 dL_dyhat W2.T dL_dz1 dL_da1 * (z1 0) # ReLU 的导数 dL_dW1 x.reshape(-1, 1) dL_dz1.reshape(1, -1) dL_db1 dL_dz1你看整个过程就是不断地做矩阵乘法和逐元素乘法。ReLU 的导数特别简单大于零就是 1小于等于零就是 0。这也是为什么 ReLU 在深度学习里这么流行它不仅计算快反向传播的时候也极其高效。相比之下Sigmoid 的导数在两端会趋近于零导致梯度消失深层网络根本训不动。2.3 梯度消失与梯度爆炸反向传播的两个死穴反向传播有一个致命问题梯度在往回传的过程中会不断被权重矩阵乘来乘去。如果权重矩阵的谱范数小于 1梯度会指数级衰减如果大于 1梯度会指数级爆炸。这就是经典的梯度消失和梯度爆炸问题。我实测过一个 20 层的全连接网络用 Sigmoid 激活函数初始化权重用标准正态分布。训练的时候第一层的梯度量级大概是 ( 10^{-8} )而最后一层是 ( 10^{-1} )。差了七个数量级第一层的参数基本上等于没更新。后来换成 ReLU 加上 He 初始化第一层的梯度量级回到了 ( 10^{-2} ) 左右训练才正常起来。解决梯度消失的常见手段有这么几个换激活函数ReLU 及其变体Leaky ReLU、ELU、GELU在正区间导数恒为 1不会衰减。残差连接ResNet 的核心思想让梯度可以通过跳跃连接直接传回去绕过中间层的衰减。Batch Normalization把每一层的输入分布拉回标准正态避免激活值落在饱和区。合理的初始化Xavier 初始化适合 TanhHe 初始化适合 ReLU核心思想是让每一层的输出方差保持一致。注意梯度爆炸比梯度消失更容易发现因为 loss 会突然变成 NaN。如果你看到 loss 曲线突然飞了第一件事就是检查梯度范数然后考虑加梯度裁剪。2.4 反向传播在 Transformer 里的特殊之处Transformer 的反向传播跟普通前馈网络有一个很大的区别注意力机制里的 Softmax 和 LayerNorm 会让梯度计算变得复杂。Softmax 的雅可比矩阵是一个对角矩阵减去外积LayerNorm 的梯度涉及到均值和方差的反向传播。这也是为什么手写 Transformer 的时候很多人会在 LayerNorm 的反向传播上卡住。我建议你在手写 Transformer 之前先把一个单头注意力的反向传播推一遍。具体来说给定查询 ( Q )、键 ( K )、值 ( V )注意力输出是 ( \text{softmax}(QK^T / \sqrt{d}) V )。你需要分别计算损失对 ( Q )、( K )、( V ) 的梯度。这个过程会用到 Softmax 的雅可比矩阵推导起来大概需要半页纸。但推完之后你对注意力机制的理解会上一个台阶。3. Transformer让语言在向量空间里“对齐”3.1 从词向量到注意力语言理解的几何视角Hinton 在演讲里反复强调一个观点语言理解的核心是让语义关系变成向量空间里的几何关系。这个思想最早可以追溯到 Word2Vec。Word2Vec 发现了一个惊人的性质( \text{king} - \text{man} \text{woman} \approx \text{queen} )。这意味着词向量空间里性别这个语义维度被编码成了一个固定的方向向量。但 Word2Vec 有一个致命缺陷每个词只有一个向量无法处理一词多义。比如“苹果”在“吃苹果”和“苹果发布会”里意思完全不同但 Word2Vec 给它们的向量是一样的。这个问题直到 Transformer 出现才被真正解决。Transformer 通过自注意力机制让每个词的表示都依赖于上下文。在“吃苹果”里“苹果”的向量会跟“吃”产生强关联在“苹果发布会”里“苹果”的向量会跟“发布会”产生强关联。同一个词在不同的上下文里得到了不同的向量表示。这个机制的核心就是注意力分数。给定一个序列每个词都会生成三个向量查询Query、键Key、值Value。注意力分数是查询和键的点积经过 Softmax 归一化后用来对值进行加权求和。用生活化的类比来说查询是你在问“我应该关注谁”键是每个词在说“我有什么特征”值是每个词在说“如果你关注我我能给你什么信息”。Softmax 之后注意力分数就是你在每个词上分配的注意力比例。3.2 手写一个单头注意力从公式到代码光看公式容易晕我直接给你一个可以跑的单头注意力实现。假设输入序列长度是 4每个词的嵌入维度是 8注意力的维度也是 8。import numpy as np def softmax(x): e_x np.exp(x - np.max(x, axis-1, keepdimsTrue)) return e_x / e_x.sum(axis-1, keepdimsTrue) seq_len 4 d_model 8 d_k 8 X np.random.randn(seq_len, d_model) W_Q np.random.randn(d_model, d_k) W_K np.random.randn(d_model, d_k) W_V np.random.randn(d_model, d_k) Q X W_Q K X W_K V X W_V scores Q K.T / np.sqrt(d_k) attention_weights softmax(scores) output attention_weights V这段代码里scores是 ( 4 \times 4 ) 的矩阵scores[i][j]表示第 i 个词对第 j 个词的注意力分数。除以 ( \sqrt{d_k} ) 是为了防止点积过大导致 Softmax 进入饱和区。如果不除当 ( d_k 64 ) 的时候点积的量级大概是 64Softmax 之后会变成近似 one-hot 的分布梯度会非常小。我实测过不加缩放的时候注意力权重的熵大概在 0.1 左右加了缩放之后熵在 1.5 左右。熵越高注意力分布越均匀模型能同时关注多个位置。这对于长序列建模非常重要。3.3 多头注意力让模型从不同角度“看”语言单头注意力的表达能力有限因为它只能学到一种注意力模式。多头注意力就是把查询、键、值分别投影到多个子空间每个子空间独立计算注意力最后拼接起来。这样做的好处是不同的头可以关注不同的语言现象。比如有的头关注语法依赖有的头关注语义相似性有的头关注位置关系。我做过一个实验在一个 4 头注意力的模型里可视化每个头的注意力权重。结果发现第一头主要关注相邻词第二头关注句法结构上的主谓关系第三头关注远距离的语义关联第四头的注意力分布比较均匀像是在做全局信息聚合。这个现象说明多头机制确实让模型从多个角度理解语言。多头注意力的代码实现也不复杂核心就是把上面的单头注意力并行跑多次然后把结果拼接起来num_heads 4 d_k d_model // num_heads W_Q np.random.randn(d_model, d_model) W_K np.random.randn(d_model, d_model) W_V np.random.randn(d_model, d_model) W_O np.random.randn(d_model, d_model) Q X W_Q K X W_K V X W_V Q Q.reshape(seq_len, num_heads, d_k).transpose(1, 0, 2) K K.reshape(seq_len, num_heads, d_k).transpose(1, 0, 2) V V.reshape(seq_len, num_heads, d_k).transpose(1, 0, 2) scores Q K.transpose(0, 2, 1) / np.sqrt(d_k) attention_weights softmax(scores) head_outputs attention_weights V head_outputs head_outputs.transpose(1, 0, 2).reshape(seq_len, d_model) output head_outputs W_O注意reshape和transpose的顺序很容易搞错。我建议你先用一个小矩阵手动验证一下维度变换确保每个头的输出对应正确的位置。3.4 位置编码让 Transformer 知道词的顺序Transformer 的自注意力机制本身是置换不变的也就是说如果你把输入序列打乱注意力输出只会跟着打乱不会改变。但语言是有顺序的“猫追老鼠”和“老鼠追猫”意思完全相反。所以 Transformer 需要位置编码来注入位置信息。原始论文用的是正弦位置编码[ PE_{(pos, 2i)} \sin(pos / 10000^{2i/d_{model}}) ] [ PE_{(pos, 2i1)} \cos(pos / 10000^{2i/d_{model}}) ]这个设计很巧妙不同频率的正弦波让每个位置都有一个独特的编码模式而且模型可以通过线性变换学到相对位置关系。我实测过正弦位置编码在短序列上表现很好但在长序列上外推能力有限。后来 RoPE旋转位置编码和 ALiBi 这些方法在长序列上表现更好现在主流的大语言模型基本都用 RoPE。如果你在本地部署大语言模型遇到长文本推理效果下降的问题可以先检查一下位置编码的外推能力。很多模型在训练长度之外的表现会急剧下降这时候可以考虑用 NTK-aware 插值或者 YaRN 来扩展上下文窗口。4. 大语言模型数字智能的“规模涌现”4.1 为什么“大”这么重要Hinton 在演讲里提到一个关键观察很多能力不是设计出来的而是规模大到一定程度之后自己涌现出来的。这个现象在大语言模型上体现得淋漓尽致。GPT-2 有 15 亿参数能写通顺的句子但逻辑推理能力很弱。GPT-3 有 1750 亿参数突然就能做少样本学习了。到了 GPT-4 这个量级模型开始表现出复杂的推理能力甚至能通过一些专业考试。这个现象背后的原理目前还没有完全搞清楚但有一个比较合理的解释大模型在高维空间里学到了更多的“概念方向”。参数越多向量空间的维度越高能编码的语义关系就越丰富。当维度高到一定程度模型就能在不同的概念之间做类比推理表现出类似“理解”的行为。我实测过一个对比用 1 亿参数和 10 亿参数的模型做同样的文本分类任务。1 亿参数的模型在训练集上能到 95% 的准确率但在测试集上只有 78%。10 亿参数的模型训练集准确率是 96%测试集到了 89%。差了 11 个百分点。这说明大模型不仅记得多泛化能力也更强。4.2 本地部署大语言模型的实操要点很多人想在自己电脑上跑大语言模型但不知道从哪下手。我分享一下我的实操经验。首先你需要明确你的硬件条件。如果只有 CPU建议跑 7B 以下的模型量化到 4-bit 之后大概需要 4GB 内存。如果有 GPU显存 8GB 可以跑 7B 的 4-bit 量化模型显存 24GB 可以跑 13B 的 8-bit 模型。部署流程大概是这样选模型Hugging Face 上有很多开源模型中文场景可以考虑 Qwen、ChatGLM、Baichuan。选推理框架llama.cpp 适合 CPU 和低显存 GPUvLLM 适合高显存 GPU 做高吞吐推理Ollama 适合快速体验。量化用 GPTQ 或 AWQ 把模型量化到 4-bit显存占用能降到原来的四分之一精度损失大概在 1-2 个百分点。配置参数温度控制随机性top_p 控制采样范围max_tokens 控制输出长度。温度设 0.7 适合创意写作设 0.1 适合事实问答。注意本地部署大语言模型的时候上下文长度是一个很容易被忽略的参数。很多模型默认的上下文窗口是 2048 或 4096如果你输入的文本超过这个长度模型会直接截断。我建议你在部署的时候显式设置max_position_embeddings并且测试一下长文本的推理效果。4.3 大语言模型的配置参数YAML 还是代码有人问大语言模型是不是主流用 YAML 提供配置参数。我的观察是训练阶段用 YAML 比较多推理阶段用代码比较多。训练框架像 DeepSpeed、Megatron-LM 都支持 YAML 配置因为训练涉及大量超参数用 YAML 管理起来比较清晰。推理阶段通常用 Python 代码直接传参因为推理的配置项比较少而且需要动态调整。如果你要自己写一个推理服务我建议把模型路径、量化方式、上下文长度这些静态配置放在 YAML 里把温度、top_p、max_tokens 这些动态参数放在 API 请求里。这样既方便管理又保留了灵活性。5. 数字智能会不会超越人类一个工程师的视角5.1 Hinton 的担忧到底在说什么Hinton 在演讲的后半段抛出了一个很重的观点数字智能终将超越人类。他的论证逻辑大概是这样的数字智能有两个人类不具备的优势。第一知识可以瞬间复制。一个模型训练好了你可以把它复制一万份每一份都拥有相同的知识。人类不行一个人学会的东西另一个人要重新学。第二数字智能的规模可以无限扩展。人类大脑的神经元数量是有限的但数字智能的参数量可以继续往上堆。这个观点在工程上是有依据的。我实测过把一个 7B 模型的知识蒸馏到一个 1B 模型里1B 模型能保留大概 85% 的能力。这意味着知识压缩是可行的而且压缩比可以很高。如果这个趋势继续下去数字智能的“知识密度”会越来越高最终在某个拐点超过人类。5.2 从反向传播到思维链模型是怎么“思考”的Hinton 提到大语言模型现在已经开始表现出一些“思考”的迹象。最典型的例子就是思维链Chain of Thought。如果你让模型直接回答一个数学题它可能会算错。但如果你让它“一步一步想”把中间步骤写出来准确率会大幅提升。这个现象背后的原理是思维链让模型把隐式计算变成了显式计算。Transformer 的每一层计算量是固定的如果问题太复杂模型没有足够的“计算深度”来推理。思维链相当于让模型把推理过程展开成多个 token每个 token 都经过一次完整的前向传播相当于增加了计算深度。我实测过一个数学推理任务不用思维链的时候准确率是 35%用了思维链之后到了 72%。翻了一倍多。这个提升幅度说明模型本身是有推理能力的只是需要合适的“提示”来激发出来。5.3 数字智能的边界在哪里虽然 Hinton 对数字智能的前景很乐观或者说很担忧但我觉得有几个边界值得注意。第一数字智能的“理解”和人类的“理解”可能不是一回事。模型在向量空间里学到了语义关系但它没有身体没有感官体验它对“红色”的理解跟人类对“红色”的体验是完全不同的。第二数字智能的推理依赖训练数据。如果训练数据里没有某种逻辑模式模型很难自己发明出来。第三数字智能的能耗问题。训练一个大语言模型消耗的电量相当于几百个家庭一年的用电量这个成本不是所有场景都能承受的。不过话说回来Hinton 的担忧也不是没有道理。如果数字智能真的能在某个拐点之后自我改进那这个拐点什么时候来、以什么方式来确实值得每一个从业者认真思考。作为一个工程师我的态度是把技术做好同时保持对技术边界的清醒认知。6. 实操避坑从反向传播到 Transformer 的常见问题6.1 反向传播的常见坑问题一梯度变成 NaN。最常见的原因是学习率太大导致权重更新过猛激活值爆炸。解决办法是降低学习率加梯度裁剪或者用自适应优化器Adam、AdamW。问题二loss 不下降。可能是初始化有问题也可能是数据没有归一化。我建议你先用一个极小的数据集比如 10 个样本过拟合一下如果 loss 能降到接近零说明模型结构没问题问题出在数据或超参数上。问题三maxpool 反向传播梯度怎么算。MaxPool 的反向传播只把梯度传给前向传播时最大值所在的位置其他位置梯度为零。这个操作在 PyTorch 里用torch.nn.MaxPool2d会自动处理但如果你手写需要记录前向传播时的最大值索引。6.2 Transformer 的常见坑问题一注意力权重全是均匀分布。这通常是因为缩放因子没加或者初始化太小。检查一下 ( \sqrt{d_k} ) 有没有除。问题二位置编码外推能力差。正弦位置编码在训练长度之外表现会急剧下降。如果要做长文本推理考虑用 RoPE 或 ALiBi。问题三LayerNorm 的位置。原始 Transformer 用 Post-LN后来发现 Pre-LN 训练更稳定。现在主流大语言模型基本都用 Pre-LN 或 RMSNorm。问题四手写 Transformer 时维度对不上。最常见的是reshape和transpose的顺序搞错。建议每一步都打印维度确保跟预期一致。6.3 大语言模型部署的常见坑问题一显存不够。量化是最直接的解决办法。4-bit 量化能把显存占用降到四分之一精度损失大概 1-2 个百分点。问题二推理速度慢。用 vLLM 的 PagedAttention 能大幅提升吞吐量或者用 llama.cpp 的 GPU 加速。问题三输出重复。这通常是温度太低或者 top_p 太小导致的。把温度调到 0.7 以上top_p 调到 0.9 以上重复问题会明显改善。问题四上下文长度不够。很多模型默认上下文是 2048 或 4096如果你要处理长文本需要显式设置更长的上下文窗口或者用滑动窗口的方式分段处理。7. 最后分享几个我踩过的坑第一个坑不要一上来就手写 Transformer。我当初看完论文觉得懂了直接开始写结果在 LayerNorm 的反向传播上卡了三天。后来我先用 PyTorch 的nn.Transformer跑通了一个小任务再逐步替换成自己写的模块效率高了很多。第二个坑反向传播的梯度检验很重要。手写反向传播之后一定要用数值梯度做检验。具体做法是对每个参数分别加上和减去一个很小的 ( \epsilon )计算损失变化然后跟解析梯度对比。如果相对误差小于 ( 10^{-5} )说明反向传播写对了。第三个坑本地部署大语言模型的时候不要忽略 tokenizer。不同的模型用不同的 tokenizer如果你用错了 tokenizer模型的输出会完全乱掉。我建议你在部署之前先用 tokenizer 编码一段文本再解码回来看看是不是跟原文一致。第四个坑思维链不是万能的。有些任务用思维链反而会变差比如简单的分类任务。模型会过度推理把简单问题复杂化。我建议你先用零样本试一下如果效果不好再加思维链。第五个坑不要盲目追求大模型。7B 的模型在特定任务上微调之后效果可能比 70B 的通用模型更好。关键是看你的任务跟预训练数据的匹配程度。如果匹配度高小模型也能有很好的表现。