从一个神经元到多层神经网络:线性代数、求导、Loss 与 PyTorch 实现

开头:神经网络到底在学什么?#
很多人第一次接触神经网络,会被一堆词吓住:张量、矩阵、梯度、反向传播、Loss、优化器、激活函数、全连接层、深度网络。
其实如果先不看复杂模型,神经网络最核心的事情可以压缩成一句话:
它先用一组参数算出预测值,再用 Loss 衡量预测错了多少,然后通过求导知道参数应该往哪个方向改,反复迭代后让预测越来越准。
这篇文章从最简单的一个神经元开始,不急着上复杂公式,而是一步一步把它拆开:
- 一个神经元如何计算预测值。
- 为什么线性代数里的向量点积刚好适合表达神经元。
- Loss 是怎么计算的。
- 求导为什么能告诉我们参数该怎么改。
- 梯度下降如何让模型越来越好。
- 多个神经元如何组成一层,多层又如何组成深度网络。
- 最后用 PyTorch 写一个完整的多层神经网络训练案例。
如果你能理解小学里的“代入公式算结果”,再理解一点点“斜率表示变化快慢”,就可以读懂这篇文章的主线。
第一阶段:先从一个最简单的神经元开始#
假设我们想做一个特别简单的预测:根据学习时长预测考试分数。
比如:
| 学习小时数 x | 真实分数 y |
|---|---|
| 1 | 50 |
| 2 | 60 |
| 3 | 70 |
| 4 | 80 |
肉眼看起来,学习时间每多 1 小时,分数大概多 10 分。我们可以写一个最简单的公式:
y_hat = w * x + b
这里:
x是输入,比如学习 3 小时。y_hat是模型预测出来的分数。w是权重,表示 x 的影响有多大。b是偏置,可以理解为基础分。
如果 w = 10,b = 40,那么:
x = 3
y_hat = 10 * 3 + 40 = 70
预测值正好是 70。
这个公式看起来不像“智能”,但它已经是一个神经元的核心形态:
输入 -> 乘以权重 -> 加上偏置 -> 输出
也就是:
y_hat = w * x + b
神经网络训练的第一件事,就是找到合适的 w 和 b。
第二阶段:用线性代数描述多个输入#
真实问题通常不会只有一个输入。
比如预测房价,可能要看:
- 房屋面积
- 房间数量
- 距离地铁的距离
- 房龄
如果我们把这些输入写成一组数字:
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,会有两个问题:
- 正误差和负误差可能互相抵消。
- 我们更希望大错误受到更重的惩罚。
所以回归任务里常用平方误差:
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 已经算出来了,但我们怎么知道 w 和 b 应该变大还是变小?
这就需要求导。
你可以先把导数理解为一句很朴素的话:
当某个参数稍微变化一点点时,结果会怎么变?
在训练神经元时,我们关心的是:
w 变一点,loss 会怎么变?
b 变一点,loss 会怎么变?
如果 w 变大,Loss 也变大,那说明 w 应该往小调。
如果 w 变大,Loss 变小,那说明 w 应该继续往大调。
这个“Loss 对参数的变化方向和变化速度”,就是梯度。
第六阶段:对单神经元做一次完整求导#
单神经元公式是:
y_hat = w*x + b
loss = 1/2 * (y_hat - y)^2
为了求 loss 对 w 的导数,我们用链式法则。链式法则的意思是:如果一个结果不是直接由参数得到的,而是经过了好几步中间计算,那么求导时也要沿着这些中间步骤一层一层乘起来。
先把计算过程拆成三层:
y_hat = w*x + b
error = y_hat - y
loss = 1/2 * error^2
也就是说,w 不是直接影响 loss,而是先影响 y_hat,y_hat 再影响 error,error 最后影响 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
这里要求的是 error 对 y_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_hat 对 w 的导数时,输入 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_hat 对 b 的导数时,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
要求 loss 对 w 的导数,就沿着这条路径相乘:
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
要求 loss 对 b 的导数,就沿着这条路径相乘:
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)
这两个值就是 w 和 b 的梯度。
来看一个具体数字。
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 一路下降,w 和 b 慢慢靠近合理值。
不过要注意:数据里 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
每一层都在做三件事:
- 线性变换:矩阵乘法加偏置。
- 非线性变换:激活函数。
- 把结果交给下一层。
如果把它想象成加工流水线:
原始输入 -> 第一层提取简单组合 -> 第二层提取更复杂组合 -> 输出层给出预测
深度学习里的“深度”,通常就是指这种层数更多的结构。
第十二阶段:反向传播是什么?#
前面我们手动推导了单神经元的梯度。
但多层网络里有很多参数:
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、大模型训练,都会更容易找到入口。