开头:神经网络到底在学什么?#

很多人第一次接触神经网络,会被一堆词吓住:张量、矩阵、梯度、反向传播、Loss、优化器、激活函数、全连接层、深度网络。

其实如果先不看复杂模型,神经网络最核心的事情可以压缩成一句话:

它先用一组参数算出预测值,再用 Loss 衡量预测错了多少,然后通过求导知道参数应该往哪个方向改,反复迭代后让预测越来越准。

这篇文章从最简单的一个神经元开始,不急着上复杂公式,而是一步一步把它拆开:

  1. 一个神经元如何计算预测值。
  2. 为什么线性代数里的向量点积刚好适合表达神经元。
  3. Loss 是怎么计算的。
  4. 求导为什么能告诉我们参数该怎么改。
  5. 梯度下降如何让模型越来越好。
  6. 多个神经元如何组成一层,多层又如何组成深度网络。
  7. 最后用 PyTorch 写一个完整的多层神经网络训练案例。

如果你能理解小学里的“代入公式算结果”,再理解一点点“斜率表示变化快慢”,就可以读懂这篇文章的主线。

第一阶段:先从一个最简单的神经元开始#

假设我们想做一个特别简单的预测:根据学习时长预测考试分数。

比如:

学习小时数 x真实分数 y
150
260
370
480

肉眼看起来,学习时间每多 1 小时,分数大概多 10 分。我们可以写一个最简单的公式:

y_hat = w * x + b

这里:

  • x 是输入,比如学习 3 小时。
  • y_hat 是模型预测出来的分数。
  • w 是权重,表示 x 的影响有多大。
  • b 是偏置,可以理解为基础分。

如果 w = 10b = 40,那么:

x = 3
y_hat = 10 * 3 + 40 = 70

预测值正好是 70。

这个公式看起来不像“智能”,但它已经是一个神经元的核心形态:

输入 -> 乘以权重 -> 加上偏置 -> 输出

也就是:

y_hat = w * x + b

神经网络训练的第一件事,就是找到合适的 wb

第二阶段:用线性代数描述多个输入#

真实问题通常不会只有一个输入。

比如预测房价,可能要看:

  • 房屋面积
  • 房间数量
  • 距离地铁的距离
  • 房龄

如果我们把这些输入写成一组数字:

x = [面积, 房间数, 距离地铁, 房龄]

每个输入都有自己的影响程度,也就是每个输入都有一个权重:

w = [面积权重, 房间数权重, 距离地铁权重, 房龄权重]

神经元要做的事情就是:每个输入乘以自己的权重,再全部加起来,最后加上偏置。

y_hat = x1*w1 + x2*w2 + x3*w3 + x4*w4 + b

线性代数里有一个刚好匹配这个操作的概念,叫向量点积:

y_hat = x · w + b

展开后就是:

x · w = x1*w1 + x2*w2 + x3*w3 + x4*w4

所以,一个神经元的计算,本质上就是一次向量点积加偏置。

这也是为什么深度学习里到处都是向量、矩阵和张量。它们不是为了显得高级,而是因为模型需要同时处理很多输入、很多样本、很多神经元,用线性代数表达最自然。

第三阶段:先手写一个单神经元预测#

下面不用 PyTorch,先用纯 Python 写一个神经元。

# 一个输入 x,一个权重 w,一个偏置 b
x = 3.0
w = 10.0
b = 40.0

# 神经元的预测
y_hat = w * x + b

print(y_hat)  # 70.0

如果真实分数是 80 呢?

y = 80.0
error = y_hat - y
print(error)  # -10.0

预测值是 70,真实值是 80,说明模型低估了 10 分。

但是训练模型不能只看“差了多少”,还需要一个统一的指标来衡量模型好坏,这个指标就是 Loss。

第四阶段:Loss 是什么?为什么要平方?#

Loss 可以翻译成“损失”或“误差”。它表示模型这次预测有多糟糕。

回到刚才的例子:

预测值 y_hat = 70
真实值 y = 80
误差 = y_hat - y = -10

如果直接把误差作为 Loss,会有两个问题:

  1. 正误差和负误差可能互相抵消。
  2. 我们更希望大错误受到更重的惩罚。

所以回归任务里常用平方误差:

loss = (y_hat - y)^2

为了后面求导更整洁,也经常写成:

loss = 1/2 * (y_hat - y)^2

这个 1/2 不改变 Loss 的核心含义,只是求导时会把平方带下来的 2 抵消掉。

用 Python 写:

y_hat = 70.0
y = 80.0
loss = 0.5 * (y_hat - y) ** 2
print(loss)  # 50.0

这就是一次预测的损失。

如果有多个样本,就把每个样本的损失求平均:

MSE = 所有样本的平方误差平均值

MSE 的英文是 Mean Squared Error,中文叫均方误差。

第五阶段:求导到底在帮我们做什么?#

现在关键问题来了:Loss 已经算出来了,但我们怎么知道 wb 应该变大还是变小?

这就需要求导。

你可以先把导数理解为一句很朴素的话:

当某个参数稍微变化一点点时,结果会怎么变?

在训练神经元时,我们关心的是:

w 变一点,loss 会怎么变?
b 变一点,loss 会怎么变?

如果 w 变大,Loss 也变大,那说明 w 应该往小调。

如果 w 变大,Loss 变小,那说明 w 应该继续往大调。

这个“Loss 对参数的变化方向和变化速度”,就是梯度。

第六阶段:对单神经元做一次完整求导#

单神经元公式是:

y_hat = w*x + b
loss = 1/2 * (y_hat - y)^2

为了求 lossw 的导数,我们用链式法则。链式法则的意思是:如果一个结果不是直接由参数得到的,而是经过了好几步中间计算,那么求导时也要沿着这些中间步骤一层一层乘起来。

先把计算过程拆成三层:

y_hat = w*x + b
error = y_hat - y
loss = 1/2 * error^2

也就是说,w 不是直接影响 loss,而是先影响 y_haty_hat 再影响 errorerror 最后影响 loss

w -> y_hat -> error -> loss
b -> y_hat -> error -> loss

1. 先求 loss 对 error 的导数#

从最后一层开始:

loss = 1/2 * error^2

这里用到最基础的幂函数求导规则:

d(z^2)/dz = 2*z

z 换成 error

d(error^2)/d(error) = 2*error

因为前面还有一个 1/2,所以:

d(loss)/d(error)
= d(1/2 * error^2)/d(error)
= 1/2 * 2 * error
= error

这一步的含义是:误差越大,Loss 对误差越敏感;误差为正,梯度为正,误差为负,梯度也为负。

2. 再求 error 对 y_hat 的导数#

第二层是:

error = y_hat - y

这里要求的是 errory_hat 的导数。注意,真实值 y 是数据里的答案,不是模型参数,所以在求导时把它看成常数。

d(error)/d(y_hat)
= d(y_hat - y)/d(y_hat)
= d(y_hat)/d(y_hat) - d(y)/d(y_hat)
= 1 - 0
= 1

这一步说明:预测值 y_hat 增加 1,误差 error 也会增加 1。

3. 再求 y_hat 对 w 和 b 的导数#

第一层是神经元本身:

y_hat = w*x + b

先看 w

y_hatw 的导数时,输入 x 和偏置 b 都看成常数:

d(y_hat)/d(w)
= d(w*x + b)/d(w)
= d(w*x)/d(w) + d(b)/d(w)
= x + 0
= x

这一步说明:如果输入 x 比较大,那么 w 稍微变化一点,对预测值的影响也会更大。

再看 b

y_hatb 的导数时,w*x 看成常数:

d(y_hat)/d(b)
= d(w*x + b)/d(b)
= d(w*x)/d(b) + d(b)/d(b)
= 0 + 1
= 1

这一步说明:b 增加 1,预测值 y_hat 就直接增加 1。

4. 用链式法则把它们串起来#

现在每一小段都求完了:

d(loss)/d(error) = error

d(error)/d(y_hat) = 1

d(y_hat)/d(w) = x

d(y_hat)/d(b) = 1

要求 lossw 的导数,就沿着这条路径相乘:

w -> y_hat -> error -> loss

所以:

d(loss)/d(w)
= d(loss)/d(error) * d(error)/d(y_hat) * d(y_hat)/d(w)
= error * 1 * x
= error * x

要求 lossb 的导数,就沿着这条路径相乘:

b -> y_hat -> error -> loss

所以:

d(loss)/d(b)
= d(loss)/d(error) * d(error)/d(y_hat) * d(y_hat)/d(b)
= error * 1 * 1
= error

也就是说:

grad_w = (y_hat - y) * x
grad_b = (y_hat - y)

这两个值就是 wb 的梯度。

来看一个具体数字。

x = 3
y = 80
w = 10
b = 40

先做前向计算:

y_hat = w*x + b
      = 10*3 + 40
      = 70

error = y_hat - y
      = 70 - 80
      = -10

loss = 1/2 * error^2
     = 1/2 * (-10)^2
     = 1/2 * 100
     = 50

再做反向求导:

grad_w = error * x
       = -10 * 3
       = -30

grad_b = error
       = -10

这里的 grad_w = -30 不是说 w 要变成 -30,而是说:在当前这个点上,Loss 对 w 的斜率是 -30。梯度是负数,表示如果 w 往增大的方向走一点,Loss 会下降。

同理,grad_b = -10 表示如果 b 往增大的方向走一点,Loss 也会下降。

更新参数时,梯度下降使用这个公式:

参数 = 参数 - 学习率 * 梯度

假设学习率 lr = 0.01

w_new = 10 - 0.01 * (-30) = 10.3
b_new = 40 - 0.01 * (-10) = 40.1

更新后再预测:

y_hat = 10.3*3 + 40.1 = 71.0

从 70 变成 71,离真实值 80 更近了。

这就是神经网络训练的最小闭环:

预测 -> 计算 Loss -> 求梯度 -> 更新参数 -> 再预测

第七阶段:手写训练一个单神经元#

下面用纯 Python 训练一个神经元,让它学习 y = 10*x + 40 这个规律。

# 训练数据:学习小时数 -> 考试分数
xs = [1.0, 2.0, 3.0, 4.0]
ys = [50.0, 60.0, 70.0, 80.0]

# 随便初始化参数
w = 0.0
b = 0.0

lr = 0.01

for epoch in range(1000):
    total_loss = 0.0
    grad_w_sum = 0.0
    grad_b_sum = 0.0

    for x, y in zip(xs, ys):
        # 1. 前向计算:预测
        y_hat = w * x + b

        # 2. 计算 loss
        error = y_hat - y
        loss = 0.5 * error ** 2
        total_loss += loss

        # 3. 求梯度
        grad_w_sum += error * x
        grad_b_sum += error

    # 4. 对一个 batch 的梯度取平均
    n = len(xs)
    grad_w = grad_w_sum / n
    grad_b = grad_b_sum / n
    avg_loss = total_loss / n

    # 5. 梯度下降更新参数
    w -= lr * grad_w
    b -= lr * grad_b

    if epoch % 100 == 0:
        print(f"epoch={epoch}, loss={avg_loss:.4f}, w={w:.4f}, b={b:.4f}")

print("最终参数:", w, b)
print("学习 5 小时预测分数:", w * 5 + b)

你会看到 Loss 一路下降,wb 慢慢靠近合理值。

不过要注意:数据里 x 只有 1 到 4,样本很少,所以最终参数不一定刚好是 w=10, b=40。这不影响理解,因为我们真正关心的是训练过程:模型通过 Loss 和梯度不断修正参数。

第八阶段:从一个样本到一个矩阵#

如果我们一次只算一个样本,效率很低。

真实训练通常会把多个样本放在一起计算,这叫 batch。

假设有 4 个样本,每个样本有 3 个特征:

X = [
  [x11, x12, x13],
  [x21, x22, x23],
  [x31, x32, x33],
  [x41, x42, x43]
]

这个 X 是一个矩阵,形状是:

4 x 3

含义是:

4 个样本,每个样本 3 个特征

如果权重是:

w = [w1, w2, w3]

那么所有样本的预测可以一次性写成:

y_hat = X @ w + b

这里的 @ 是矩阵乘法。

线性代数的价值就在这里:它可以把一堆重复的乘加运算,写成简洁的矩阵计算,而且计算机可以非常高效地执行。

第九阶段:为什么需要激活函数?#

到目前为止,我们的神经元都是:

y_hat = x · w + b

这是线性函数。

如果一层是线性的,两层还是线性的,十层叠起来本质上仍然可以合并成一个线性函数。

这意味着:如果没有激活函数,多层神经网络并不会真正变强。

激活函数的作用是引入非线性,让网络可以表达弯曲的、复杂的关系。

常见激活函数包括:

ReLU(x) = max(0, x)

它的含义非常简单:

  • 如果输入大于 0,就原样输出。
  • 如果输入小于 0,就输出 0。

比如:

ReLU(3) = 3
ReLU(-2) = 0

虽然 ReLU 很简单,但它让神经网络从“只能画直线”,变成“可以拼出复杂曲线”。

第十阶段:从一个神经元到一层神经元#

一个神经元只能输出一个数字。

如果我们同时放多个神经元,就得到一层。

假设输入有 3 个特征,我们想让这一层输出 5 个数字,那么就需要 5 个神经元。

每个神经元都有自己的一组权重和偏置。

用矩阵表示就是:

输入 X:        batch_size x 3
权重 W:        3 x 5
偏置 b:        5
输出 H:        batch_size x 5

H = X @ W + b

这就是全连接层,也叫 Linear 层。

在 PyTorch 里,对应的是:

nn.Linear(in_features=3, out_features=5)

它内部其实就维护了一个权重矩阵和一个偏置向量。

第十一阶段:从一层到多层深度网络#

多层神经网络可以写成:

第一层:h1 = ReLU(X @ W1 + b1)
第二层:h2 = ReLU(h1 @ W2 + b2)
输出层:y_hat = h2 @ W3 + b3

每一层都在做三件事:

  1. 线性变换:矩阵乘法加偏置。
  2. 非线性变换:激活函数。
  3. 把结果交给下一层。

如果把它想象成加工流水线:

原始输入 -> 第一层提取简单组合 -> 第二层提取更复杂组合 -> 输出层给出预测

深度学习里的“深度”,通常就是指这种层数更多的结构。

第十二阶段:反向传播是什么?#

前面我们手动推导了单神经元的梯度。

但多层网络里有很多参数:

W1, b1, W2, b2, W3, b3 ...

手动推导每一个参数会非常麻烦。

反向传播做的事情,就是从最后的 Loss 开始,沿着计算图反着走,把每个参数对 Loss 的影响都算出来。

它本质上还是链式法则,只是由程序自动完成。

你可以这样理解:

前向传播:输入一路算到预测值,再算 Loss。
反向传播:从 Loss 反过来,计算每个参数的梯度。
参数更新:优化器根据梯度修改参数。

PyTorch 的 autograd 就是自动求导系统。只要你用张量搭建计算过程,PyTorch 就能记录计算图,然后通过:

loss.backward()

自动算出所有需要训练参数的梯度。

第十三阶段:用 PyTorch 实现一个多层神经网络#

下面写一个完整例子。

任务:用两个输入特征预测一个连续值。

我们人为构造一个规律:

y = 2*x1 - 3*x2 + 5 + 一点点噪声

模型一开始不知道这个规律,它只能通过数据、Loss 和梯度下降自己学。

1. 安装依赖#

如果本地还没有 PyTorch,可以先安装。不同系统和 CUDA 环境安装命令可能不同,最稳妥的方式是去 PyTorch 官网选择对应命令。

CPU 版本通常可以这样安装:

pip install torch

2. 完整训练代码#

import torch
from torch import nn

# 为了让每次运行结果更稳定
torch.manual_seed(42)

# 1. 构造训练数据
# X 的形状是 [样本数, 特征数],这里是 200 个样本,每个样本 2 个特征
X = torch.randn(200, 2)

# 人为制造真实规律:y = 2*x1 - 3*x2 + 5 + noise
true_w = torch.tensor([[2.0], [-3.0]])
true_b = 5.0
noise = 0.1 * torch.randn(200, 1)
y = X @ true_w + true_b + noise

# 2. 定义一个多层神经网络
class SimpleMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 16),  # 输入 2 个特征,输出 16 个隐藏特征
            nn.ReLU(),
            nn.Linear(16, 8),  # 第二个隐藏层
            nn.ReLU(),
            nn.Linear(8, 1),   # 输出 1 个预测值
        )

    def forward(self, x):
        return self.net(x)

model = SimpleMLP()

# 3. 定义 Loss 和优化器
loss_fn = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

# 4. 训练循环
for epoch in range(1000):
    # 前向传播:算预测值
    y_pred = model(X)

    # 计算 Loss:预测值和真实值的差距
    loss = loss_fn(y_pred, y)

    # 清空上一次的梯度
    optimizer.zero_grad()

    # 反向传播:自动求导,计算每个参数的梯度
    loss.backward()

    # 参数更新:根据梯度修改权重和偏置
    optimizer.step()

    if epoch % 100 == 0:
        print(f"epoch={epoch}, loss={loss.item():.6f}")

# 5. 测试一个新样本
x_test = torch.tensor([[1.0, 2.0]])
with torch.no_grad():
    pred = model(x_test)

real = 2 * 1.0 - 3 * 2.0 + 5
print("模型预测:", pred.item())
print("真实公式结果:", real)

3. 这段代码每一步在做什么?#

先看数据形状:

X = torch.randn(200, 2)

意思是创建 200 个样本,每个样本有 2 个特征。

true_w = torch.tensor([[2.0], [-3.0]])
y = X @ true_w + true_b + noise

这里的 X @ true_w 是矩阵乘法。

形状变化是:

X:       200 x 2
true_w:  2 x 1
结果:    200 x 1

也就是每个样本都会得到一个真实值。

再看模型:

nn.Linear(2, 16)
nn.ReLU()
nn.Linear(16, 8)
nn.ReLU()
nn.Linear(8, 1)

它表示:

2 个输入特征
  -> 16 个隐藏神经元
  -> ReLU 激活
  -> 8 个隐藏神经元
  -> ReLU 激活
  -> 1 个输出值

虽然这个任务本身是线性的,用一个神经元也能学,但我们故意用多层网络,是为了展示深度网络的基本写法和训练路径。

训练循环里最重要的是这几行:

y_pred = model(X)
loss = loss_fn(y_pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()

它们对应的含义是:

预测 -> 算 Loss -> 清空旧梯度 -> 反向传播求新梯度 -> 更新参数

这就是 PyTorch 训练模型最经典的五步。

第十四阶段:为什么要 zero_grad?#

很多新手会疑惑:为什么每次反向传播之前都要写:

optimizer.zero_grad()

原因是 PyTorch 默认会累加梯度。

也就是说,如果你不清空,第二次 loss.backward() 算出来的梯度会加到第一次的梯度上,第三次又继续加。

在普通训练循环里,我们通常希望每一轮都根据当前 Loss 单独计算梯度,所以要先清空旧梯度。

固定套路就是:

optimizer.zero_grad()
loss.backward()
optimizer.step()

第十五阶段:学习率是什么?为什么不能太大也不能太小?#

学习率 lr 决定每次参数更新走多大一步。

梯度下降公式是:

参数 = 参数 - 学习率 * 梯度

如果学习率太小,模型会学得很慢。

如果学习率太大,模型可能一步跨过最优点,Loss 来回震荡,甚至越来越大。

可以把训练想象成下山:

  • 梯度告诉你下山方向。
  • 学习率告诉你每一步迈多大。

在刚开始学习时,常见选择可以是:

0.1, 0.01, 0.001

如果 Loss 不下降,可以先试试把学习率调小。如果下降太慢,可以适当调大。

第十六阶段:Adam 优化器和普通梯度下降有什么不同?#

前面手写的参数更新是最普通的梯度下降:

w = w - lr * grad_w

PyTorch 例子里用了 Adam:

optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

Adam 仍然依赖梯度,但它会根据历史梯度自动调整不同参数的更新幅度。

粗略理解:

  • 普通梯度下降:所有参数按同一个学习率走。
  • Adam:每个参数会根据自己的梯度历史,自适应地调整步子。

所以在很多入门实验里,Adam 更容易得到比较稳定的训练效果。

第十七阶段:把整条路径串起来#

从最简单的神经元到多层网络,路径其实很自然。

第一步,一个输入:

y_hat = w*x + b

第二步,多个输入:

y_hat = x · w + b

第三步,多个样本一起算:

y_hat = X @ W + b

第四步,多个神经元组成一层:

H = X @ W1 + b1

第五步,加上激活函数:

H = ReLU(X @ W1 + b1)

第六步,多层堆叠:

y_hat = W3(ReLU(W2(ReLU(W1X + b1)) + b2)) + b3

写成更符合代码的形式:

输入 -> Linear -> ReLU -> Linear -> ReLU -> Linear -> 输出

训练过程始终没有变:

前向传播 -> 计算 Loss -> 反向传播 -> 更新参数

第十八阶段:新手最容易混淆的几个点#

1. 权重不是人工指定的吗?#

初始权重通常是随机的,训练过程中由优化器自动调整。

我们人工决定的是网络结构、Loss 类型、优化器和学习率,而不是每个权重的最终数值。

2. Loss 越小就一定越好吗?#

在训练集上通常是这样,但不能只看训练集。

如果模型在训练集 Loss 很低,在新数据上表现很差,就叫过拟合。后续需要验证集、测试集、正则化、数据增强等方法。

3. 多层网络一定比单层好吗?#

不一定。

简单线性问题,一个线性层就够了。多层网络更适合复杂非线性关系,比如图像、语音、文本和复杂传感器数据。

4. PyTorch 的 backward 是不是魔法?#

不是魔法,本质是链式法则加计算图。

你前向计算时,PyTorch 记录每一步操作。调用 loss.backward() 时,它按相反方向依次应用求导规则,把梯度算出来。

5. 为什么深度学习这么依赖矩阵计算?#

因为神经网络里绝大多数计算都是大量乘法和加法。

矩阵可以把这些计算批量表示出来,GPU 又特别擅长并行做矩阵计算,所以深度学习和线性代数天然绑定在一起。

总结#

这篇文章的核心不是记住所有公式,而是建立一条清晰的理解链路:

神经元 = 输入乘权重再加偏置
线性代数 = 用向量和矩阵批量表达神经元计算
Loss = 衡量预测和真实答案差多远
求导 = 判断参数变化会如何影响 Loss
梯度下降 = 沿着让 Loss 变小的方向更新参数
反向传播 = 在多层网络里自动高效地计算梯度
PyTorch = 帮我们管理张量、自动求导、参数更新和模型结构

当你再看到下面这段训练代码时,它就不再是一组固定模板,而是一条完整的算法链路:

y_pred = model(X)
loss = loss_fn(y_pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()

它背后的含义就是:

先预测,算错多少,再求每个参数该怎么改,最后更新参数。

从一个神经元到多层深度网络,复杂度确实增加了,但底层逻辑一直是这条线。理解了这条线,再去学习卷积神经网络、Transformer、大模型训练,都会更容易找到入口。