从随机权重到收敛模型,详细讲解损失函数、梯度下降和反向传播的完整过程,附完整代码实现。
Written by Syed Muhammad Ali Raza
上篇文章我们从零实现了自注意力机制——让一个 token 看清所有其他 token 并判断真正重要的是什么。但我故意留了一个悬念:那篇文章里每一个权重矩阵——把 embedding 变成 query、key、value 的那些——我都是随手填的随机数。这可不是真实模型的运作方式。那些数字起点是随机的,没错,但它们不会一直随机下去。它们会被反复塑造,百万次、千万次,最终变成真正能生成连贯语言的数值。
这篇文章要讲的就是这个塑造过程。一堆随机数是怎么变成能替你补完句子、写可用代码、解读书中笑话的东西的?实话实说,答案靠两个思路配合:一个是损失函数,用来度量模型当前有多错;另一个是梯度下降,一个真正简单、近乎机械的过程,把每一个权重往让模型少犯错的方向轻轻推一下。足够多次,足够多数据,语言理解就从中产生了。让我们真正动手实现一遍,亲眼看到每个齿轮的转动。
上数学之前的真实例子,浓雾中下山
想象你夜间站在半山腰,四周是真正的浓雾。你看不见下面的山谷,任意方向只能看到几步远。你的全部目标就是走到最低点,但没有地图,也无法看清地形的整体轮廓。
但你能做的事是这样的:你能感受脚下地面。你能判断从你此刻站立的确切位置,哪一个方向是下坡的。于是你往那个下坡方向迈出一小步。然后从新位置再感受地面,再往下坡方向迈一小步。重复这个过程——小步、感受坡度、小步、感受坡度——最终你会慢慢走到一个低点,尽管你从未一览整座山,没有地图,每一步只知道脚下的局部坡度。
这完完全全就是梯度下降。"山"叫损失景观(loss landscape),是模型在所有可能的权重配置下有多错的度量。"下坡方向"在任意一点叫梯度,是一个数学量,精确告诉你每个权重该往哪个方向、推多少才能减少模型当前的错误。"小步"就是实际的权重更新,反复应用,百万次,在一片你根本无法直观想象的多维山脉中,但每一步的机制就是这么简单:感受局部坡度,往下走,重复。
第二个真实例子,学习罚球
同一个思路的另一个角度,专门用来解释为什么我们需要反向传播,而不仅仅是梯度下降。
想象你正在学习投篮球罚球,你投篮动作的每一个部分——肘部角度、手腕弹腕、膝盖弯曲、跟随动作——都是独立的可调旋钮。你投了,球偏左了两英尺。现在是真正的难题:你该拧哪个旋钮、拧多少,才能修正这个特定的偏失?
一个差劲的篮球教练只会说"再试一次",不给具体反馈。一个真正的好教练会把偏失从你的动作链路上往回追溯——球往左偏是因为出手时手腕有一个轻微的侧向抖动,而手腕抖动是因为肘部没有完全对齐,而肘部不对齐又是因为起始时脚的位置就不对。动作的每个部分按其实际造成偏失的比例分担责任,相应地各自调整,每个环节都做一点修正,而不是整个动作做一次随机的大改动。
这种从最终错误开始,沿着每一个参与环节按其实际贡献量往回追溯责任的过程,就是反向传播在神经网络里做的事。损失函数告诉你球偏了多少。反向传播是教练把这个偏失从网络的每一个权重往回追溯,找出每个权重究竟对这个错误贡献了多少,这样梯度下降就知道每个权重该往哪个方向推、推多远了。
把各部分正式定义清楚
两个类比已经在脑子里了,现在来学真正的术语。
损失函数是一个单一数值,用来度量模型当前输出与期望输出的偏差。损失低,模型在这个例子上表现好;损失高,就确实跑偏了。每个训练步骤都会重新计算这个数值。
梯度下降是调整每一个权重的过程,让它们朝着减少损失值的方向,每次一点点,利用的是模型当前位置的斜率——梯度。
反向传播是针对网络中每一个权重实际计算这个梯度的专用算法,通过从最终损失出发、沿着每一个参与计算的层反向走,利用微积分中的链式法则高效完成——链式法则本质上就是"把偏失从动作的每个环节往回追溯"这个想法的形式化表达。
从零实现这三部分
我要构建一个真正极简的神经网络,在一个小而真实的任务上训练它,亲眼看损失往下掉,全程只用 numpy,没有任何框架藏着机制不让你看。
第一步,任务,以及为什么它需要不止一条直线
我们要训练一个网络解决 XOR,这是一个经典的小问题,选它是有意为之——因为它无法被简单的直线决策边界解决,这会迫使网络在这个过程中真正学到非平凡的东西,而不是只拟合一个显而易见的模式。
import numpy as np
np.random.seed(42)
# XOR truth table, output is 1 only when inputs differ
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])
print("Inputs:")
print(X)
print("Expected outputs:")
print(y)
第二步,初始化一个极简的两层网络,权重是完全随机的
和那篇注意力文章一样,从随机数开始。这一次我们真的要通过真实训练来改变它们,而不是任由它们保持随机。
input_size = 2
hidden_size = 4
output_size = 1
W1 = np.random.randn(input_size, hidden_size) * 0.5
b1 = np.zeros((1, hidden_size))
W2 = np.random.randn(hidden_size, output_size) * 0.5
b2 = np.zeros((1, output_size))
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return x * (1 - x)
第三步,前向传播,做一次真实的预测
def forward_pass(X, W1, b1, W2, b2):
hidden_input = X @ W1 + b1
hidden_output = sigmoid(hidden_input)
final_input = hidden_output @ W2 + b2
final_output = sigmoid(final_input)
return hidden_output, final_output
hidden_output, prediction = forward_pass(X, W1, b1, W2, b2)
print("Predictions before any training, just random noise:")
print(prediction)
训练前来跑一下这段代码,预测结果毫无意义,接近随机猜测——这正是一个从未学习过的网络该有的样子。
第四步,损失函数,精确度量我们有多错
def mean_squared_error(predictions, targets):
return np.mean((predictions - targets) ** 2)
loss = mean_squared_error(prediction, y)
print(f"Initial loss: {loss:.4f}")
第五步,反向传播,手动追溯误差
这是大多数教程跳过、直接调用库函数的部分。我们要显式地做,让你看到责任链条真正被往回追溯的过程,就像罚球教练做的那样。
def backward_pass(X, y, hidden_output, prediction, W2):
# how wrong was the final output, and in which direction
output_error = prediction - y
output_delta = output_error * sigmoid_derivative(prediction)
# trace that error backward through W2, to see how much
# the hidden layer contributed to the final mistake
hidden_error = output_delta @ W2.T
hidden_delta = hidden_error * sigmoid_derivative(hidden_output)
# now compute the actual gradient for every single weight,
# how much each one should be blamed, and in which direction
grad_W2 = hidden_output.T @ output_delta
grad_b2 = np.sum(output_delta, axis=0, keepdims=True)
grad_W1 = X.T @ hidden_delta
grad_b1 = np.sum(hidden_delta, axis=0, keepdims=True)
return grad_W1, grad_b1, grad_W2, grad_b2
再读一遍那个 hidden_error 行,那就是"把偏失从肘部往回追溯、到手腕"的时刻。输出端的误差通过 W2 反向传播回去,以弄清楚隐藏层的输出对最终错误贡献了多少——这恰恰就是反向传播的全部职责,把一个错误的责任沿着每一个参与的层按比例往回分配。
第六步,梯度下降,实际迈出下坡那一步
learning_rate = 0.5
def train_step(X, y, W1, b1, W2, b2, learning_rate):
hidden_output, prediction = forward_pass(X, W1, b1, W2, b2)
loss = mean_squared_error(prediction, y)
grad_W1, grad_b1, grad_W2, grad_b2 = backward_pass(X, y, hidden_output, prediction, W2)
# the actual downhill step, nudge every weight a small amount
# in the direction that reduces the loss
W1 -= learning_rate * grad_W1
b1 -= learning_rate * grad_b1
W2 -= learning_rate * grad_W2
b2 -= learning_rate * grad_b2
return W1, b1, W2, b2, loss
第七步,实际训练,亲眼看损失往下掉
for epoch in range(10000):
W1, b1, W2, b2, loss = train_step(X, y, W1, b1, W2, b2, learning_rate)
if epoch % 1000 == 0:
print(f"Epoch {epoch}, loss: {loss:.4f}")
_, final_prediction = forward_pass(X, W1, b1, W2, b2)
print("\nFinal predictions after training:")
print(np.round(final_prediction, 3))
print("\nExpected:")
print(y)
自己跑一下,看损失数字真正在下降,一个 epoch 接一个 epoch,从接近随机猜测一路走到对这个根本无法用直线解决的问题的准确预测。文件中没有任何地方手工写入了 XOR 逻辑。网络自己发现的,全凭反复的小步下坡,由损失和从中计算的梯度引导。这就是整个机制的完整真相,只是按数量级放大了——这个系列中每一个模型就是这样学会一切的。

把这个放大到真实的 LLM
上面的玩具网络大概只有十三个权重。一个真实 LLM 有数十亿个。机制是完全一样的——前向传播、计算损失、反向传播误差、迈出下坡一步、重复——只是规模大到难以凭直觉想象。
规模大了之后有几件事变了,值得知道名字。语言模型的损失函数通常是交叉熵损失,而不是我们上面用的均方误差,更适合在一个巨大词表中预测下一个 token,但它扮演的角色完全相同——一个数字说明预测有多错。优化器(真正把梯度应用到来更新权重的东西)通常比我们手写的朴素梯度下降更精巧,一般是 Adam,它根据近期的梯度历史为每个权重自适应调整步长,但在各种精巧的外衣下,底层根本还是"往减少损失的方向迈步"。而且不是用四个微小的 XOR 例子训练,模型是在巨大数据集上训练的,每个步骤处理一批随机的小批量数据,在训练过程中走上百万、上十亿次这样的小步,每次都把数十亿个权重轻轻推一下。
def scale_intuition():
toy_network_weights = 13
gpt_scale_weights = 175_000_000_000 # roughly, for reference
print(f"Our toy network: {toy_network_weights} weights")
print(f"A large LLM: ~{gpt_scale_weights:,} weights")
print(f"That's roughly {gpt_scale_weights / toy_network_weights:,.0f}x more dials being turned")
print("Same mechanism though, forward pass, loss, backward pass, small step, repeat")
scale_intuition()
机制完全相同,只是这四步在规模上从"在你的笔记本上几秒学会 XOR"变成了"在大型计算集群上训练数周后学会写可用代码和对话"。机制没有变得更复杂,只是应用次数多到难以想象,覆盖的权重数量也多到难以想象。

我没有覆盖的真正难点,得诚实说
我要把本文覆盖了什么和没覆盖什么讲清楚,因为把训练过程过度简化对读者没有帮助。我们用了固定学习率的朴素梯度下降,真实训练几乎总是用自适应优化器和在训练过程中会变化的学习率调度器。我们每一步都在整个数据集上训练,真实 LLM 训练用的是小批量——每步处理数据的一个小型随机子集,既是出于计算原因,也因为这确实有助于训练过程。我们没有涉及前一篇论文中的 Transformer 架构具体如何训练,注意力层、前馈层以及其他所有层都通过完全相同的反向传播和梯度下降机制各自计算梯度和更新权重,只是比我们的极简两层网络多了很多运动部件。我们也没有涉及不同的训练阶段,一个 LLM 通常先在原始文本上做预训练,然后还有指令微调、基于人类反馈的强化学习等后续阶段,每个阶段都使用相同的核心机制,但用不同的数据和不同的损失信号——这确实值得在这个系列的后续文章中单独展开。
把本文和整个系列串起来
上篇文章中,注意力让模型在单次前向传播中关联 token 与 token之间的关系,而权重矩阵是我们随手填的随机数。本文中,那些权重不再随机了,通过百万次小步下坡被塑造出来,每一步都由反向传播计算的梯度驱动,目标是降低那个精确度量模型当前有多错的损失。把这两篇文章合起来,你就真正拥有了前面整个系列中每一个模型之所以有能力的两个核心思路:一种在输入内部关联信息的机制,和一种从数据中学习这些关系实际该是什么样子的机制。
如果你自己跑一下训练循环,在屏幕上亲眼看那个损失数字下降,即使你纸上谈兵时已经相信我了,这仍然值得做——看着十三个随机数在你眼前变成一个可用的 XOR 求解器,感觉是真实不一样的。