从零开始的
神经网络

神经网络如何真正学习的数学原理、直觉与代码——从单个神经元构建到一个可运行的训练循环。基于 Andrej Karpathy 的 micrograd 教程。

概念
8
Python 代码行数
~50
演示
5
来源
2h

本文为 第一部分:LLM 如何工作 的配套文章。所有概念和代码均可直接追溯至 Karpathy 的 micrograd 讲座。

问:神经网络到底是什么?
第 1 章 · 动机

我们要解决的
问题

在构建任何东西之前,让我们先理解我们要做什么。我们有一些输入,想要预测一个输出。例如:给定关于一个人的 4 个测量值,预测他是否会喜欢一部电影。

挑战在于:我们不知道公式。我们无法手动编写规则。相反,我们想要一个从示例中学习公式的系统——只需看到大量的输入/输出对。

神经网络就是这个系统。它一开始是一个完全随机的函数。然后它查看成千上万的示例并调整自己——微小的推动,一次又一次——直到它的预测变得准确。这个过程叫做训练

与第一部分的联系 GPT 做的正是这件事——只是规模巨大。它的"输入"是 token,"输出"是下一个 token,它在 15 万亿个示例上训练。数学原理与我们这里构建的完全相同,只是数量更多。

我们的训练数据集

一个微小的例子使其具体化。四个输入,一个目标输出:

训练示例 (xs → ys)
# 输入: [x1, x2, x3, x4] xs = [ [2.0, 3.0, -1.0, 1.0], [3.0, -1.0, 0.5, 1.0], [0.5, 1.0, 1.0, 1.0], [1.0, 1.0, -1.0, 1.0], ] # 目标: 我们希望网络输出的值 ys = [1.0, -1.0, -1.0, 1.0]
xs 是我们的输入数据——4 个示例,每个有 4 个数字。ys 是每个示例的"正确答案":1.0 或 -1.0。网络一开始什么都不知道,必须学会将 xs 映射到 ys。
为什么是 -1 和 1? 我们使用 tanh 作为激活函数(详见 §2),它输出 -1 到 1 之间的值。因此我们的目标值是 -1 和 1,以匹配该范围。
第 2 章 · 基本构件

什么是
神经元?

神经元只是一个微小的数学函数。可以把它想象成一个调光开关:它接收一堆输入,决定每个输入有多重要(权重),加上一个个人默认倾向(偏置),然后将结果压缩到一个有界范围内。

公式:output = tanh(w₁·x₁ + w₂·x₂ + b)

其中 w₁、w₂ 是权重("这个输入有多重要?"),b 是偏置("当输入为零时我的默认倾向是什么?"),tanh 将结果压缩到始终落在 -1 和 1 之间。

class Value: def __init__(self, data): self.data = data # 实际数值 self.grad = 0.0 # 梯度(稍后填充) def __mul__(self, other): return Value(self.data * other.data) def __add__(self, other): return Value(self.data + other.data) def tanh(self): import math t = math.tanh(self.data) return Value(t)
self.data 只是一个数字——比如 0.5 或 -1.3。self.grad 从零开始,在反向传播期间被填充(§7)。__mul____add__ 方法让我们可以对 Value 对象使用正常的 Python 数学运算符(+、*)。tanh 将任何数字压缩到 (-1, 1) 范围内。
为什么用 tanh? 如果没有激活函数,堆叠神经元只会产生一个线性函数——无论你加多少层。tanh 引入了非线性,这正是让网络能够学习复杂模式的关键。

神经元实验场

拖动滑块改变权重和偏置。观察神经元的输出实时更新。固定输入:x₁ = 0.5,x₂ = −0.3。

交互式神经元
0.50
-0.30
0.00
原始加权和: 0.00 tanh 输出: 0.00
第 3 章 · 架构

层与
MLP

一个神经元不足以学习复杂模式。我们将它们堆叠成,再将层堆叠成多层感知器(MLP)。可以把它想象成一条流水线:第一层查看原始输入,下一层查看第一层发现的内容,以此类推。

神经元之间的每个连接就是一个权重。一个 4 输入 → 3 神经元 → 1 输出的网络有 (4×3) + (3×1) = 15 个权重,再加上偏置。GPT-4 的结构相同,只是有 4050 亿个权重。

class Neuron: def __init__(self, nin): self.w = [Value(random.uniform(-1,1)) for _ in range(nin)] self.b = Value(0) def __call__(self, x): act = sum(wi*xi for wi,xi in zip(self.w, x)) + self.b return act.tanh() class Layer: def __init__(self, nin, nout): self.neurons = [Neuron(nin) for _ in range(nout)] def __call__(self, x): return [n(x) for n in self.neurons] class MLP: def __init__(self, nin, nouts): sizes = [nin] + nouts self.layers = [Layer(sizes[i], sizes[i+1]) for i in range(len(nouts))] def __call__(self, x): for layer in self.layers: x = layer(x) return x[0] if len(x) == 1 else x
Neuron(nin) 创建一个有 nin 个随机权重的神经元。Layer(nin, nout) 创建 nout 个神经元,每个接受 nin 个输入。MLP(4, [3,1]) 创建一个 4→3→1 网络。__call__ 是 Python 让对象可调用的方式——所以 model(x) 就会运行前向传播。

MLP 架构

一个 4→3→1 网络:4 个输入,3 个神经元的隐藏层,1 个输出。

一行代码创建模型
n = MLP(4, [3, 1]) # 4 输入 → 3 隐藏 → 1 输出
就这样。我们现在有了一个随机初始化的网络,有 4×3 + 3×1 = 15 个权重加上 4 个偏置 = 19 个可学习参数。
第 4 章 · 计算输出

前向
传播

前向传播很简单:将数据从左到右通过网络运行。每个神经元依次激活,将其输出传递给下一层,最终我们在末端得到一个预测值。

一开始,由于权重是随机的,预测毫无意义。但我们仍然可以运行前向传播——我们需要它来计算我们错得有多离谱(§5),这告诉我们如何改进。

# 将一个训练示例通过网络 x = [Value(2.0), Value(3.0), Value(-1.0), Value(1.0)] prediction = n(x) print(prediction.data) # 例如 0.23
n(x) 调用 MLP 的 __call__ 方法,它循环遍历每一层并将其应用于 x。每一层调用应用每个神经元:计算 w·x + b,应用 tanh,将输出传递给下一层。最终值就是我们的预测。它只是一连串的乘法和加法。
一切都是数字 在每一步,我们只是在计算数字。复杂性来自于同时对数十亿个参数执行此操作,但每个参数上的操作只是简单的算术。

前向传播可视化

点击"运行前向传播"观看数据流过一个 2→3→1 网络。每个节点亮起并显示其计算值。

交互式前向传播
第 5 章 · 衡量误差

损失 — 我们
错得有多离谱?

前向传播之后,我们有了预测值。现在我们需要衡量它们错得有多离谱。损失函数就是一份成绩单:一个单一的数字,概括了"你现在的预测有多糟糕"。

我们使用均方误差(MSE):对于每个示例,将预测值与目标值的差值平方,然后将它们全部平均。平方确保损失始终为正,并且对大错误惩罚更重。

训练的目标:让这个损失数字尽可能小。

# 将所有训练示例通过网络 ypred = [n(x) for x in xs] loss = sum((yout - ygt)**2 for ygt, yout in zip(ys, ypred)) print(loss.data) # 例如 4.73 — 一开始错得很离谱
对于每个示例:yout 是我们网络的预测值,ygt 是"真实值"目标。(yout - ygt)**2 将误差平方。如果我们预测了 0.23 但目标是 1.0,误差就是 (0.23−1.0)² = 0.59。我们将所有 4 个示例的误差求和得到一个数字。训练的任务就是将这个数字推向零。

损失地形

损失是所有权重的函数。把它想象成一片丘陵地形——我们想把球滚到最低点。点击"步进"运行一次梯度下降更新。

损失曲线上的梯度下降
0.10
w = 3.00 · 损失 = 1.00
损失始终是一个数字 这一点至关重要。我们需要一个数字,这样才有一个优化的方向。如果我们有一个损失向量,我们就不知道该往哪个方向移动。
第 6 章 · 变化的数学

导数 —
哪个方向是下坡?

我们想把损失降下来。为此,我们需要知道:对于每个权重,增加它会让损失上升还是下降?这正是导数告诉我们的。

想象你站在一座山上。你看不到整个地形,但你能感觉到脚下的方向哪个是下坡。导数就是那种"感觉"——你当前位置损失的斜率。

我们将使用的关键规则:

  • 加法:d/dx(a + b) = 1 — 加法将梯度原样传递
  • 乘法:d/dx(a · b) = b — 梯度按另一个因子缩放
  • tanh:d/dx(tanh(x)) = 1 − tanh²(x) — 由链式法则处理
一句话理解链式法则 如果 A 影响 B,B 影响损失,那么 A 对损失的影响 = (A 对 B 的影响) × (B 对损失的影响)。我们将在 §7 中使用这个法则将梯度向后传播到整个网络。

导数可视化

沿 f(x) = x² 拖动点。切线显示该位置的斜率(导数)。注意:在 x=0 处斜率为 0;当 x 远离中心时斜率变得更陡。

f(x) = x² — 拖动点
x = 1.00  ·  f(x) = 1.00  ·  f'(x) = 2.00
梯度 vs 导数 导数是针对一个变量的函数。梯度是同一概念在多变量下的推广——每个权重一个斜率。损失的梯度告诉我们同时在每个权重方向上的斜率。
第 7 章 · 归因

反向传播

我们知道损失。我们知道导数。但我们有数百个权重——我们怎么知道哪些权重该负责,以及该负多大责任?

反向传播高效地解决了这个问题。它沿着计算图反向遍历——从损失开始,经过每个运算,回到每个权重——使用链式法则计算每个权重的梯度。

Karpathy 的核心洞见:"反向传播做的唯一一件事就是递归地应用链式法则。"仅此而已。它是机械的,不是魔法。

for p in n.parameters(): p.grad = 0.0 ypred = [n(x) for x in xs] loss = sum((yout - ygt)**2 for ygt, yout in zip(ys, ypred)) loss.backward() # 现在每个 p 都有 p.grad:"增加我 → 损失变化 p.grad 这么多"
p.grad = 0.0 清除上一步训练留下的梯度——否则它们会累积。loss.backward() 反向遍历产生 loss 的每个运算,在每个节点应用链式法则。调用之后,每个权重都有一个 .grad,表示"增加我 → 损失增加这么多"。

表达式图

一个简单的表达式:e = tanh((a·b) + a)。点击"前向传播"计算值,然后点击"反向传播"观看梯度反向流动。

计算图 — a=2, b=-3
为什么要反向? 我们已经知道损失的梯度(它是 1.0——损失对自身的导数)。我们反向工作是因为每个节点只需要从它前面的节点(更接近损失的节点)获取梯度来计算自己的梯度。这是链式法则的干净递归应用。
第 8 章 · 学习

梯度下降 —
训练循环

我们有了梯度。现在来使用它们。更新规则很简单:对于每个权重,沿其梯度的相反方向移动一小步。相反是因为梯度指向上坡——我们要走下坡

# 创建网络 n = MLP(4, [3, 1]) for step in range(100): ypred = [n(x) for x in xs] # 1. 前向传播 loss = sum((yout-ygt)**2 for ygt,yout in zip(ys,ypred)) # 2. 计算损失 for p in n.parameters(): p.grad=0.0 # 3. 清零梯度 loss.backward() # 4. 反向传播 for p in n.parameters(): p.data -= 0.01 * p.grad # 5. 更新权重 print(f"step {step} loss {loss.data:.4f}")
逐行解释:前向传播——运行所有 4 个示例,得到 4 个预测值。损失——将误差平方并求和;一个数字。清零梯度——梯度会累积,每次反向传播前重置。反向传播——通过链式法则填充每个 .grad更新——p.data -= 0.01 * p.grad 将每个权重沿其梯度相反方向移动一小步。0.01 是学习率。重复 100 次:损失从 ~4 降到接近 0。

训练过程中的损失

用文字描述完整循环
  1. 将数据前向传播——得到预测值
  2. 衡量预测有多错误(损失)
  3. 反向遍历——计算每个权重对误差的贡献
  4. 将每个权重朝减少误差的方向微调
  5. 重复
完整流程

从随机权重到预测

01
定义神经元
每个神经元:输入的加权和 + 偏置 → tanh。权重随机初始化。Value 类跟踪数字及其梯度。
Value 类权重偏置tanh
02
堆叠成 MLP
神经元 → 层 → MLP。每层的输出馈入下一层。每个连接是一个权重。我们的 4→3→1 网络有 19 个参数。
NeuronLayerMLP
03
前向传播
将训练数据从左到右通过网络。每个神经元计算并将输出传递给下一层。结果:每个示例一个预测值。
__call__激活值
04
计算损失
均方误差:(预测 − 目标)² 的总和。一个标量数字。一开始很大,随着训练进行逐渐趋近零。
MSE标量损失
05
反向传播
沿计算图反向遍历。每个节点应用链式法则。每个权重获得一个 .grad:"增加我 → 损失变化这么多"。
backward()链式法则梯度
06
梯度下降 → 重复
对每个权重执行 p.data -= lr * p.grad。向下走一小步。清零梯度。重复 前向传播 → 损失 → 反向传播 → 更新 数百步。损失趋近 ~0。
学习率权重更新训练循环
转载 作者: ynarwal · 基于 Andrej Karpathy 的讲座 查看原文 ↗
← 返回转载列表 ⚠️ 翻译转载,版权归原作者所有