神经网络是深度学习的基石。本文从感知机出发,逐步构建多层感知机 (MLP),详解反向传播算法、激活函数选择、优化器对比与稳定训练的关键技巧。
1. 感知机与神经元模型
1.1 感知机 (Perceptron)
感知机是最简单的线性分类器:
$$\hat{y} = \sigma(w_1x_1 + w_2x_2 + … + w_nx_n + b) = \sigma(w^Tx + b)$$
import numpy as np
class Perceptron:
def __init__(self, n_inputs, lr=0.01):
self.weights = np.zeros(n_inputs)
self.bias = 0
self.lr = lr
def predict(self, X):
return np.where(np.dot(X, self.weights) + self.bias >= 0, 1, 0)
def fit(self, X, y, epochs=100):
for _ in range(epochs):
for xi, target in zip(X, y):
prediction = self.predict(xi.reshape(1, -1))[0]
error = target - prediction
self.weights += self.lr * error * xi
self.bias += self.lr * error
局限:感知机只能处理线性可分问题(如 XOR 问题无法解决)。
1.2 多层感知机 (MLP)
通过隐藏层引入非线性变换,解决线性不可分问题:
输入层 → 隐藏层1 → 隐藏层2 → ... → 输出层
import torch
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super(MLP, self).__init__()
self.layer1 = nn.Linear(input_size, hidden_size)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.2)
self.layer2 = nn.Linear(hidden_size, hidden_size // 2)
self.layer3 = nn.Linear(hidden_size // 2, num_classes)
def forward(self, x):
x = self.layer1(x)
x = self.relu(x)
x = self.dropout(x)
x = self.layer2(x)
x = self.relu(x)
x = self.layer3(x)
return x
# 初始化模型
model = MLP(input_size=784, hidden_size=256, num_classes=10)
print(model)
2. 激活函数
激活函数引入非线性,使神经网络能够拟合任意复杂函数。
2.1 常用激活函数对比
| 激活函数 | 公式 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| Sigmoid | $\frac{1}{1+e^{-x}}$ | 输出 (0,1),概率解释 | 梯度消失、非零均值 | 二分类输出层 |
| Tanh | $\frac{e^x-e^{-x}}{e^x+e^{-x}}$ | 零均值 | 仍可能梯度消失 | 隐藏层 |
| ReLU | $\max(0, x)$ | 计算快、缓解梯度消失 | Dead ReLU 问题 | 隐藏层默认 |
| Leaky ReLU | $\max(\alpha x, x)$ | 解决 Dead ReLU | 增加一个超参 | ReLU 改进 |
| PReLU | $\max(\alpha x, x)$ (可学习) | 自适应负斜率 | 参数量增加 | 深层网络 |
| ELU | $x (x>0), \alpha(e^x-1) (x<0)$ | 平滑、负值区域 | 计算略慢 | 需要平滑输出 |
| Swish | $x \cdot \sigma(x)$ | 自门控、平滑 | 计算量稍大 | Google 推荐 |
| GELU | $x \cdot \Phi(x)$ | Transformer 标配 | - | BERT/GPT |
| Softmax | $\frac{e^{z_i}}{\sum e^{z_j}}$ | 多类概率归一化 | - | 多分类输出层 |
# PyTorch 激活函数
import torch.nn.functional as F
x = torch.randn(2, 5)
# ReLU
h = F.relu(x)
# Leaky ReLU
h = F.leaky_relu(x, negative_slope=0.01)
# GELU(Transformer 使用)
h = F.gelu(x)
# Softmax(输出层)
probs = F.softmax(x, dim=1)
2.2 梯度消失与梯度爆炸
梯度消失:反向传播中梯度逐层衰减(Sigmoid 导数最大 0.25),深层网络前面层几乎不更新。
梯度爆炸:梯度逐层放大,权重更新过大导致 NaN。
解决方案:
- 使用 ReLU 替代 Sigmoid
- 权重初始化(Xavier / He)
- 批归一化
- 梯度裁剪
3. 反向传播算法
3.1 链式法则
反向传播利用链式法则高效计算梯度:
$$\frac{\partial L}{\partial w_i} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z} \cdot \frac{\partial z}{\partial w_i}$$
3.2 手动实现反向传播
class SimpleNN:
"""手动实现两层神经网络,含前向传播和反向传播"""
def __init__(self, input_size, hidden_size, output_size):
# Xavier 初始化
self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size)
self.b1 = np.zeros((1, hidden_size))
self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size)
self.b2 = np.zeros((1, output_size))
def forward(self, X):
self.z1 = X @ self.W1 + self.b1
self.a1 = np.maximum(0, self.z1) # ReLU
self.z2 = self.a1 @ self.W2 + self.b2
return self.z2
def backward(self, X, y, output, lr):
m = X.shape[0]
# 输出层梯度
dz2 = output - y # Softmax + CrossEntropy 的简化梯度
dW2 = (self.a1.T @ dz2) / m
db2 = np.sum(dz2, axis=0, keepdims=True) / m
# 隐藏层梯度
da1 = dz2 @ self.W2.T
dz1 = da1 * (self.z1 > 0) # ReLU 导数
dW1 = (X.T @ dz1) / m
db1 = np.sum(dz1, axis=0, keepdims=True) / m
# 参数更新
self.W2 -= lr * dW2
self.b2 -= lr * db2
self.W1 -= lr * dW1
self.b1 -= lr * db1
3.3 PyTorch 自动求导
x = torch.randn(3, requires_grad=True)
y = x ** 2 + 3 * x + 1
loss = y.sum()
# 自动反向传播
loss.backward()
# 查看梯度
print(x.grad) # dy/dx = 2x + 3
4. 损失函数
| 损失函数 | 公式 | 适用任务 | PyTorch |
|---|---|---|---|
| MSE | $\frac{1}{n}\sum(y-\hat{y})^2$ | 回归 | nn.MSELoss() |
| MAE | $\frac{1}{n}\sum|y-\hat{y}|$ | 回归(鲁棒) | nn.L1Loss() |
| 交叉熵 | $-\sum y\log(\hat{y})$ | 多分类 | nn.CrossEntropyLoss() |
| BCE | $-\sum[y\log(\hat{y}) + (1-y)\log(1-\hat{y})]$ | 二分类 | nn.BCEWithLogitsLoss() |
| Huber | 混合 MSE/MAE | 回归(异常值鲁棒) | nn.SmoothL1Loss() |
# 分类损失
criterion = nn.CrossEntropyLoss() # 内置 Softmax
output = model(images)
loss = criterion(output, labels)
# 回归损失
criterion = nn.MSELoss()
loss = criterion(predictions, targets)
# 对不平衡数据加权的交叉熵
weights = torch.tensor([1.0, 2.0, 3.0]) # 少类别的权重更高
criterion = nn.CrossEntropyLoss(weight=weights)
5. 优化器
5.1 梯度下降变体
# SGD + 动量
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# Adam(自适应学习率,最常用)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001,
betas=(0.9, 0.999), eps=1e-8)
# AdamW(权重衰减解耦,Transformer 标配)
optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)
# RMSprop
optimizer = torch.optim.RMSprop(model.parameters(), lr=0.001, alpha=0.99)
5.2 优化器对比
| 优化器 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| SGD | 泛化好、收敛稳定 | 慢、需调学习率 | 大规模数据 |
| SGD+Momentum | 加速收敛、减少震荡 | 动量超参 | 通用 |
| AdaGrad | 自适应学习率 | 学习率单调递减至 0 | 稀疏梯度 |
| RMSprop | 解决 AdaGrad 学习率衰减 | 仍需调参 | RNN |
| Adam | 快速收敛,默认好 | 泛化可能不如 SGD | 默认首选 |
| AdamW | 更好的权重衰减 | - | Transformer/NLP |
| LAMB | 大批量训练稳定 | - | BERT 预训练 |
5.3 学习率调度
from torch.optim.lr_scheduler import StepLR, CosineAnnealingLR, ReduceLROnPlateau
# 每 10 个 epoch 衰减 0.1 倍
scheduler = StepLR(optimizer, step_size=10, gamma=0.1)
# 余弦退火
scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=0)
# 验证 loss 不下降时降低 LR
scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5,
patience=5, verbose=True)
# Warmup + 余弦退火(Transformer 标准配置)
from transformers import get_cosine_schedule_with_warmup
num_training_steps = len(train_loader) * epochs
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=500,
num_training_steps=num_training_steps
)
6. 训练稳定技巧
6.1 权重初始化
# Xavier 初始化(tanh/sigmoid)
for m in model.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
nn.init.zeros_(m.bias)
# He 初始化(ReLU)
for m in model.modules():
if isinstance(m, nn.Linear):
nn.init.kaiming_uniform_(m.weight, nonlinearity='relu')
6.2 批归一化 (BatchNorm)
对每个 mini-batch 归一化,稳定训练、允许更大学习率、有轻微正则化效果。
class BN_MLP(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super().__init__()
self.layer1 = nn.Linear(input_size, hidden_size)
self.bn1 = nn.BatchNorm1d(hidden_size)
self.relu = nn.ReLU()
self.layer2 = nn.Linear(hidden_size, num_classes)
def forward(self, x):
x = self.layer1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.layer2(x)
return x
注意:
- 批归一化在 CNN 中接卷积后;在 NLP 中常用 LayerNorm 替代
- 推理时保存运行均值/方差,eval 模式自动切换
6.3 Dropout 正则化
随机丢弃神经元,防止共适应,是强正则化手段。
# Dropout 层
self.dropout = nn.Dropout(p=0.5) # 50% 丢弃概率
# Dropout 仅在训练时生效
def forward(self, x):
x = self.fc(x)
x = self.dropout(x) # 训练时丢弃,推理时自动缩放
return x
变体:
- Spatial Dropout(CNN):按通道丢弃
- Dropout2d/3d:特征图空间维度一致丢弃
- DropBlock:结构化丢弃,更适合 CNN
6.4 梯度裁剪
防止 RNN/Transformer 中的梯度爆炸。
# 全局梯度范数裁剪
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
7. 完整训练流程
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# 超参数
EPOCHS = 50
BATCH_SIZE = 64
LR = 0.001
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 数据
X_train = torch.randn(1000, 784)
y_train = torch.randint(0, 10, (1000,))
train_dataset = TensorDataset(X_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
# 模型、损失、优化器
model = MLP(784, 256, 10).to(DEVICE)
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=LR, weight_decay=0.01)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)
# 训练循环
for epoch in range(EPOCHS):
model.train()
total_loss = 0
correct = 0
total = 0
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(DEVICE), batch_y.to(DEVICE)
optimizer.zero_grad()
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
total_loss += loss.item()
_, predicted = outputs.max(1)
total += batch_y.size(0)
correct += predicted.eq(batch_y).sum().item()
scheduler.step()
acc = 100. * correct / total
avg_loss = total_loss / len(train_loader)
print(f"Epoch {epoch+1}/{EPOCHS}: Loss={avg_loss:.4f}, Acc={acc:.2f}%")
# 评估
model.eval()
with torch.no_grad():
outputs = model(X_test.to(DEVICE))
_, predicted = outputs.max(1)
test_acc = predicted.eq(y_test.to(DEVICE)).sum().item() / len(y_test)
print(f"Test Accuracy: {test_acc*100:.2f}%")
8. 训练诊断
| 现象 | 原因 | 解决 |
|---|---|---|
| Loss 不降 | 学习率太小/梯度消失 | 增大 LR / 换激活函数 / 检查初始化 |
| Loss 震荡 | 学习率太大 / batch 太小 | 降低 LR / 增大 batch / 加 momentum |
| Loss 为 NaN | 梯度爆炸 / 学习率过大 | 梯度裁剪 / 降低 LR / 检查数据归一化 |
| 训练 loss 降,测试 loss 升 | 过拟合 | Dropout / 正则化 / 早停 / 更多数据 |
| 训练测试 loss 都高 | 欠拟合 | 增加模型容量 / 更多特征 / 减少正则化 |
# 早停 Early Stopping
class EarlyStopping:
def __init__(self, patience=7, min_delta=0):
self.patience = patience
self.min_delta = min_delta
self.counter = 0
self.best_loss = None
self.early_stop = False
def __call__(self, val_loss):
if self.best_loss is None:
self.best_loss = val_loss
elif val_loss > self.best_loss - self.min_delta:
self.counter += 1
if self.counter >= self.patience:
self.early_stop = True
else:
self.best_loss = val_loss
self.counter = 0
总结
神经网络训练的关键环节:
- 架构设计:输入/输出维度对齐,隐藏层数与宽度由任务复杂度决定
- 激活函数:隐藏层默认 ReLU,输出层用 Softmax/Sigmoid 匹配任务
- 初始化:Xavier/He 初始化防止梯度消失/爆炸
- 归一化:BatchNorm 加速训练,LayerNorm 用于 NLP
- 正则化:Dropout + Weight Decay 防止过拟合
- 优化器:Adam/AdamW 为默认选择,SGD+动量泛化更好
- 学习率:Warmup + 余弦退火是 Transformer 标准配置
- 监控:同时看训练/验证 Loss 与准确率,设置早停
理解这些原理后,可以灵活调参,使神经网络稳定收敛到理想性能。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。