从零开始的
神经网络
神经网络如何真正学习的数学原理、直觉与代码——从单个神经元构建到一个可运行的训练循环。基于 Andrej Karpathy 的 micrograd 教程。
- 概念
- 8
- Python 代码行数
- ~50
- 演示
- 5
- 来源
- 2h
本文为 第一部分:LLM 如何工作 的配套文章。所有概念和代码均可直接追溯至 Karpathy 的 micrograd 讲座。
神经网络如何真正学习的数学原理、直觉与代码——从单个神经元构建到一个可运行的训练循环。基于 Andrej Karpathy 的 micrograd 教程。
本文为 第一部分:LLM 如何工作 的配套文章。所有概念和代码均可直接追溯至 Karpathy 的 micrograd 讲座。
在构建任何东西之前,让我们先理解我们要做什么。我们有一些输入,想要预测一个输出。例如:给定关于一个人的 4 个测量值,预测他是否会喜欢一部电影。
挑战在于:我们不知道公式。我们无法手动编写规则。相反,我们想要一个从示例中学习公式的系统——只需看到大量的输入/输出对。
神经网络就是这个系统。它一开始是一个完全随机的函数。然后它查看成千上万的示例并调整自己——微小的推动,一次又一次——直到它的预测变得准确。这个过程叫做训练。
一个微小的例子使其具体化。四个输入,一个目标输出:
xs 是我们的输入数据——4 个示例,每个有 4 个数字。ys 是每个示例的"正确答案":1.0 或 -1.0。网络一开始什么都不知道,必须学会将 xs 映射到 ys。
神经元只是一个微小的数学函数。可以把它想象成一个调光开关:它接收一堆输入,决定每个输入有多重要(权重),加上一个个人默认倾向(偏置),然后将结果压缩到一个有界范围内。
公式:output = tanh(w₁·x₁ + w₂·x₂ + b)
其中 w₁、w₂ 是权重("这个输入有多重要?"),b 是偏置("当输入为零时我的默认倾向是什么?"),tanh 将结果压缩到始终落在 -1 和 1 之间。
self.data 只是一个数字——比如 0.5 或 -1.3。self.grad 从零开始,在反向传播期间被填充(§7)。__mul__ 和 __add__ 方法让我们可以对 Value 对象使用正常的 Python 数学运算符(+、*)。tanh 将任何数字压缩到 (-1, 1) 范围内。
拖动滑块改变权重和偏置。观察神经元的输出实时更新。固定输入:x₁ = 0.5,x₂ = −0.3。
一个神经元不足以学习复杂模式。我们将它们堆叠成层,再将层堆叠成多层感知器(MLP)。可以把它想象成一条流水线:第一层查看原始输入,下一层查看第一层发现的内容,以此类推。
神经元之间的每个连接就是一个权重。一个 4 输入 → 3 神经元 → 1 输出的网络有 (4×3) + (3×1) = 15 个权重,再加上偏置。GPT-4 的结构相同,只是有 4050 亿个权重。
Neuron(nin) 创建一个有 nin 个随机权重的神经元。Layer(nin, nout) 创建 nout 个神经元,每个接受 nin 个输入。MLP(4, [3,1]) 创建一个 4→3→1 网络。__call__ 是 Python 让对象可调用的方式——所以 model(x) 就会运行前向传播。
一个 4→3→1 网络:4 个输入,3 个神经元的隐藏层,1 个输出。
前向传播很简单:将数据从左到右通过网络运行。每个神经元依次激活,将其输出传递给下一层,最终我们在末端得到一个预测值。
一开始,由于权重是随机的,预测毫无意义。但我们仍然可以运行前向传播——我们需要它来计算我们错得有多离谱(§5),这告诉我们如何改进。
n(x) 调用 MLP 的 __call__ 方法,它循环遍历每一层并将其应用于 x。每一层调用应用每个神经元:计算 w·x + b,应用 tanh,将输出传递给下一层。最终值就是我们的预测。它只是一连串的乘法和加法。
点击"运行前向传播"观看数据流过一个 2→3→1 网络。每个节点亮起并显示其计算值。
前向传播之后,我们有了预测值。现在我们需要衡量它们错得有多离谱。损失函数就是一份成绩单:一个单一的数字,概括了"你现在的预测有多糟糕"。
我们使用均方误差(MSE):对于每个示例,将预测值与目标值的差值平方,然后将它们全部平均。平方确保损失始终为正,并且对大错误惩罚更重。
训练的目标:让这个损失数字尽可能小。
yout 是我们网络的预测值,ygt 是"真实值"目标。(yout - ygt)**2 将误差平方。如果我们预测了 0.23 但目标是 1.0,误差就是 (0.23−1.0)² = 0.59。我们将所有 4 个示例的误差求和得到一个数字。训练的任务就是将这个数字推向零。
损失是所有权重的函数。把它想象成一片丘陵地形——我们想把球滚到最低点。点击"步进"运行一次梯度下降更新。
我们想把损失降下来。为此,我们需要知道:对于每个权重,增加它会让损失上升还是下降?这正是导数告诉我们的。
想象你站在一座山上。你看不到整个地形,但你能感觉到脚下的方向哪个是下坡。导数就是那种"感觉"——你当前位置损失的斜率。
我们将使用的关键规则:
沿 f(x) = x² 拖动点。切线显示该位置的斜率(导数)。注意:在 x=0 处斜率为 0;当 x 远离中心时斜率变得更陡。
我们知道损失。我们知道导数。但我们有数百个权重——我们怎么知道哪些权重该负责,以及该负多大责任?
反向传播高效地解决了这个问题。它沿着计算图反向遍历——从损失开始,经过每个运算,回到每个权重——使用链式法则计算每个权重的梯度。
Karpathy 的核心洞见:"反向传播做的唯一一件事就是递归地应用链式法则。"仅此而已。它是机械的,不是魔法。
p.grad = 0.0 清除上一步训练留下的梯度——否则它们会累积。loss.backward() 反向遍历产生 loss 的每个运算,在每个节点应用链式法则。调用之后,每个权重都有一个 .grad,表示"增加我 → 损失增加这么多"。
一个简单的表达式:e = tanh((a·b) + a)。点击"前向传播"计算值,然后点击"反向传播"观看梯度反向流动。
我们有了梯度。现在来使用它们。更新规则很简单:对于每个权重,沿其梯度的相反方向移动一小步。相反是因为梯度指向上坡——我们要走下坡。
.grad。更新——p.data -= 0.01 * p.grad 将每个权重沿其梯度相反方向移动一小步。0.01 是学习率。重复 100 次:损失从 ~4 降到接近 0。
基于 Andrej Karpathy 的 "The spelled-out intro to neural networks and backpropagation: building micrograd" 讲座。现在你在代码层面理解了 第一部分训练章节 背后的数学原理。