03-数学与代码最小可运行例子
# MIT 6.S191 数学与代码最小可运行例子
目标:把课前必须会的数学概念转成能运行、能解释、能写进笔记的最小代码。你不需要在第一轮掌握复杂推导,但必须能把公式和代码对应起来。
1. 从线性模型开始
深度学习的第一块积木是线性变换:
Y = XW + b
直觉解释:X 是输入数据,W 是模型学到的权重,b 是偏置,Y 是输出。权重决定每个输入特征对输出的影响。
import numpy as np
X = np.array([
[1.0, 2.0, 3.0],
[2.0, 0.0, 1.0],
[0.0, 1.0, 2.0],
[1.0, 1.0, 1.0],
])
W = np.array([
[0.2, -0.1],
[0.5, 0.3],
[0.1, 0.7],
])
b = np.array([0.1, -0.2])
Y = X @ W + b
print(Y)
print("shape:", Y.shape)
你要能解释:4 个样本,每个 3 个特征,经过 W 后变成每个样本 2 个输出。因此 Y.shape 是 (4, 2)。
2. 激活函数为什么必要
如果模型只有线性变换,多层线性层叠加仍然是线性的,表达能力有限。激活函数让模型能表示弯曲边界和复杂关系。
def relu(x):
return np.maximum(0, x)
z = np.array([-2.0, -0.5, 0.0, 1.0, 3.0])
print(relu(z))
ReLU 的直觉是:负数信号被关掉,正数信号保留。它简单、计算快、在深度网络中常用。
3. Loss 的最小例子
回归任务常用 MSE:
MSE = mean((y_pred - y_true)^2)
y_true = np.array([3.0, 5.0, 7.0])
y_pred = np.array([2.5, 5.5, 8.0])
loss = np.mean((y_pred - y_true) ** 2)
print(loss)
分类任务常用 cross entropy。直觉是:正确类别概率越低,惩罚越大。
probs = np.array([0.1, 0.7, 0.2]) # 三个类别的预测概率
true_class = 1
loss = -np.log(probs[true_class])
print(loss)
如果正确类别概率从 0.7 降到 0.1,loss 会显著变大。
4. 梯度下降手算版
假设模型是:
y_pred = w * x
loss = (y_pred - y_true)^2
对 w 的梯度是:
d loss / d w = 2 * (w*x - y_true) * x
代码:
w = 1.0
x = 2.0
y_true = 5.0
lr = 0.1
for step in range(8):
y_pred = w * x
loss = (y_pred - y_true) ** 2
grad_w = 2 * (y_pred - y_true) * x
w = w - lr * grad_w
print(f"step={step}, w={w:.3f}, y_pred={y_pred:.3f}, loss={loss:.3f}")
你要观察三件事:
loss是否整体下降。w是否朝合适方向改变。- 学习率如果过大会发生什么。
可以把 lr 改成 1.0 看看训练是否震荡。
5. PyTorch 中的自动求导
真实深度学习不会手写所有梯度,而是用自动求导。
import torch
w = torch.tensor(1.0, requires_grad=True)
x = torch.tensor(2.0)
y_true = torch.tensor(5.0)
y_pred = w * x
loss = (y_pred - y_true) ** 2
loss.backward()
print("loss:", loss.item())
print("grad:", w.grad.item())
requires_grad=True 表示 PyTorch 要追踪这个变量上的计算。loss.backward() 会自动计算梯度。你要知道:这就是 backpropagation 在代码中的表现。
6. 一个最小神经网络
import torch
import torch.nn as nn
X = torch.randn(16, 4) # 16 个样本,每个 4 维
y = torch.randint(0, 3, (16,)) # 3 分类标签
model = nn.Sequential(
nn.Linear(4, 8),
nn.ReLU(),
nn.Linear(8, 3)
)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for step in range(20):
logits = model(X)
loss = criterion(logits, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if step % 5 == 0:
print(step, loss.item(), logits.shape)
解释:
X.shape = (16, 4):16 个样本,每个 4 个特征。- 第一层
Linear(4, 8):把 4 维输入变成 8 维隐藏表示。 - 第二层
Linear(8, 3):输出 3 个类别的 logits。 CrossEntropyLoss:内部会处理 softmax 和 negative log likelihood。optimizer.zero_grad():清空上一轮梯度。loss.backward():计算梯度。optimizer.step():更新参数。
7. Shape 检查表
| 任务 | 输入 shape | 输出 shape | 常见 loss |
|---|---|---|---|
| 表格二分类 | (batch, features) | (batch, 2) 或 (batch, 1) | cross entropy / BCE |
| 图像分类 | (batch, channels, height, width) | (batch, classes) | cross entropy |
| 语言模型 | (batch, seq_len) | (batch, seq_len, vocab) | token cross entropy |
| 回归 | (batch, features) | (batch, 1) | MSE |
| 去噪生成 | noisy sample | predicted noise | MSE |
当代码报错时,先打印 shape:
print("X", X.shape)
print("logits", logits.shape)
print("y", y.shape)
8. 训练循环流程
flowchart TD
A["准备 batch 数据"] --> B["model(X) 得到 logits 或预测"]
B --> C["计算 loss"]
C --> D["optimizer.zero_grad()"]
D --> E["loss.backward()"]
E --> F["optimizer.step()"]
F --> G["记录 loss/metric"]
G --> A
这段流程是你看任何 lab 时都要找的结构。如果 notebook 很长,你先找到这几行,就能理解训练主线。
9. 常见错误
| 报错现象 | 可能原因 | 检查方式 |
|---|---|---|
| matrix shape mismatch | 线性层输入维度不对 | 打印 X.shape 和 Linear(in_features, ...) |
| expected Long but got Float | 分类标签 dtype 错 | y.dtype |
| CUDA not available | 本机没有 GPU 或环境没配好 | 先用 CPU 跑小样本 |
| loss 不下降 | 学习率、标签、模型、数据有问题 | 先用很小数据过拟合测试 |
| accuracy 很高但不可信 | 数据泄漏或样本太少 | 检查 train/test split |
10. 写进报告的表达
我先用一个最小神经网络确认训练流程。输入张量形状为(batch, features),模型输出为(batch, classes)的 logits。训练时使用 cross entropy 作为分类损失,通过 backpropagation 计算梯度,并由 Adam optimizer 更新参数。这个例子不能证明模型在真实数据上有效,但可以验证我理解了 forward、loss、gradient 和 optimizer 的基本关系。