神经网络基础
2635 字约 9 分钟
2026-05-20
神经网络的灵感来自大脑,但理解它不需要懂生物学,只需要理解矩阵运算和微积分。本文从第一性原理出发,系统讲解神经网络的完整数学和实现。
1. 从感知机到神经网络
1.1 单个神经元
一个神经元(Neuron)做三件事:
- 接受多个输入 x₁, x₂, ..., xₙ(加上偏置 bias)
- 加权求和:z = w₁x₁ + w₂x₂ + ... + wₙxₙ + b = wᵀx + b
- 通过激活函数 f 输出:a = f(z)
1.2 为什么需要激活函数?
非线性性:没有激活函数,无论多少层,多层神经网络等价于单层线性变换:
W3(W2(W1x))=(W3W2W1)x=Wx
激活函数引入非线性,使网络能逼近任意复杂函数(通用近似定理:包含足够多神经元的单隐层网络可以以任意精度逼近任意连续函数)。
1.3 神经网络的层次结构
输入层 隐藏层1 隐藏层2 输出层
x₁ ──┐
x₂ ──┼──── [neurons] ──── [neurons] ──── [neurons] ──── ŷ
x₃ ──┘- 输入层:接受原始特征,不做计算
- 隐藏层:提取特征,层数越多提取越抽象的特征
- 输出层:产生最终预测
2. 激活函数
2.1 Sigmoid
σ(z)=1+e−z1
导数:σ'(z) = σ(z)(1-σ(z))(可由函数值直接计算,不需要重新求导)
问题:梯度消失——当 z 很大或很小时,σ'(z) → 0,反向传播时梯度极小,深层网络学不动。
适用:二分类的输出层(输出概率)。
2.2 Tanh
tanh(z)=ez+e−zez−e−z
导数:tanh'(z) = 1 - tanh²(z)
输出范围 [-1, 1],相比 Sigmoid 均值更接近0(零均值),收敛略快。但同样有梯度消失问题。
2.3 ReLU(Rectified Linear Unit)⭐ 默认首选
ReLU(z)=max(0,z)
导数:z > 0 时为1,z < 0 时为0
优势:
- 计算极其简单(一个 max 操作)
- 正区间梯度恒为1,不存在梯度消失
- 实践中收敛快
- 稀疏激活(约50%神经元为0,更高效)
问题:
- 梯度爆炸(正区间梯度为1,可能在多层后累积)
- 死神经元:如果神经元的输出长期为负,梯度为0,永远无法被激活
2.4 Leaky ReLU
Leaky ReLU(z)={zαzz>0z≤0
通常 α = 0.01,解决 ReLU 的死神经元问题。
2.5 GELU(高斯误差线性单元)
GELU(z)=z⋅Φ(z)
其中 Φ(z) 是标准正态分布的累积分布函数。
现代 Transformer(BERT、GPT)的默认激活函数,比 ReLU 在 NLP 任务上更好。
2.6 SwiGLU
SwiGLU(x,W,V,b,c)=Swish(xW+b)⊗(xV+c)
LLaMA、GPT-4等现代大模型使用,结合了门控机制,效果优于 GELU。
2.7 Softmax(输出层)
Softmax(zi)=∑j=1Kezjezi
将 K 个实数转换为概率分布(所有值之和为1),用于多分类的输出层。
数值稳定性:实现时要减去最大值防止溢出:
def stable_softmax(z):
z = z - np.max(z) # 防止 exp 溢出
return np.exp(z) / np.sum(np.exp(z))3. 前向传播
3.1 数学推导
设第 l 层的参数为 W[l](形状:n[l] × n[l-1])和 b[l](形状:n[l] × 1):
线性变换:Z[l] = W[l] A[l-1] + b[l]
激活:A[l] = gl(在新窗口打开)
其中 A[0] = X(输入)。
3.2 代码实现
import numpy as np
def forward_propagation(X, parameters):
"""
参数格式: {'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}
"""
W1, b1 = parameters['W1'], parameters['b1']
W2, b2 = parameters['W2'], parameters['b2']
# 第一层:隐藏层(ReLU)
Z1 = W1 @ X + b1 # (n1, m)
A1 = np.maximum(0, Z1) # ReLU
# 第二层:输出层(Sigmoid)
Z2 = W2 @ A1 + b2 # (n2, m)
A2 = 1 / (1 + np.exp(-Z2)) # Sigmoid
cache = (Z1, A1, Z2, A2)
return A2, cache4. 反向传播(Backpropagation)
4.1 核心思想
通过链式法则计算损失函数对每个参数的梯度,从输出层向输入层反向传递梯度信息。
链式法则:
∂W[l]∂L=∂Z[l]∂L⋅∂W[l]∂Z[l]
设 δ[l] = ∂L/∂Z[l](该层的"误差信号"),则:
δ[l]=(W[l+1])Tδ[l+1]⊙g′[l](Z[l])
∂W[l]∂L=m1δ[l](A[l−1])T
∂b[l]∂L=m1∑δ[l]
4.2 二层网络的完整推导
输出层(Sigmoid + Binary Cross-Entropy):
δ[2]=A[2]−Y
隐藏层(ReLU):
δ[1]=(W[2])Tδ[2]⊙1[Z[1]>0]
(ReLU 导数:Z>0 时为1,否则为0)
def backward_propagation(X, Y, parameters, cache):
m = X.shape[1]
W1, W2 = parameters['W1'], parameters['W2']
Z1, A1, Z2, A2 = cache
# 输出层梯度
dZ2 = A2 - Y # 二分类的损失梯度
dW2 = (1/m) * dZ2 @ A1.T
db2 = (1/m) * np.sum(dZ2, axis=1, keepdims=True)
# 隐藏层梯度(ReLU 的导数)
dA1 = W2.T @ dZ2
dZ1 = dA1 * (Z1 > 0) # ReLU 导数
dW1 = (1/m) * dZ1 @ X.T
db1 = (1/m) * np.sum(dZ1, axis=1, keepdims=True)
gradients = {'dW1': dW1, 'db1': db1, 'dW2': dW2, 'db2': db2}
return gradients4.3 梯度消失与爆炸
梯度消失:在反向传播中,梯度在每一层都乘以激活函数的导数。如果导数 < 1(如 Sigmoid 最大值 0.25),经过多层后梯度趋近0,前面的层几乎无法更新。
梯度爆炸:如果权重矩阵的特征值大于1,梯度在反向传播中指数增长,参数更新幅度极大。
解决方案:
| 问题 | 解决方法 |
|---|---|
| 梯度消失 | 使用 ReLU、残差连接(ResNet)、Batch Normalization |
| 梯度爆炸 | 梯度裁剪(Gradient Clipping) |
| 两者 | 谨慎的权重初始化、学习率调度 |
# 梯度裁剪(PyTorch)
import torch.nn as nn
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()5. 权重初始化
5.1 为什么初始化很重要
- 全部初始化为0:对称性问题——所有神经元计算相同的梯度,永远一样,网络失去多样性
- 过大的随机初始化:激活值饱和(Sigmoid/Tanh区间),梯度消失
- 过小的随机初始化:激活值消失
5.2 各种初始化方法
Xavier/Glorot 初始化(适合 Tanh 激活):
W∼N(0,nin+nout2)
He 初始化(适合 ReLU 激活):
W∼N(0,nin2)
import torch.nn as nn
# PyTorch 默认 He 初始化 for Linear/Conv
# 显式设置
def init_weights(m):
if isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
model.apply(init_weights)6. 优化器
6.1 SGD with Momentum
标准 SGD 有震荡问题,Momentum 通过累积历史梯度平滑更新:
v=βv+(1−β)∇WL
W=W−αv
β 通常为 0.9,相当于取近似指数加权移动平均。
6.2 RMSProp
自适应学习率——对更新频繁的参数减小学习率,对更新稀少的参数增大学习率:
s=βs+(1−β)(∇WL)2
W=W−s+ϵα∇WL
6.3 Adam(Adaptive Moment Estimation)⭐ 最常用
结合 Momentum + RMSProp,是目前默认推荐的优化器:
m=β1m+(1−β1)∇L
v=β2v+(1−β2)(∇L)2
偏差校正(初始阶段 m 和 v 偏向0):
m^=1−β1tm,v^=1−β2tv
参数更新:
W=W−v^+ϵαm^
默认超参数:α=0.001, β₁=0.9, β₂=0.999, ε=1e-8
import torch.optim as optim
optimizer = optim.Adam(model.parameters(), lr=1e-3, betas=(0.9, 0.999))
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01) # Adam + L2正则6.4 学习率调度
from torch.optim.lr_scheduler import (
StepLR, CosineAnnealingLR, ReduceLROnPlateau, OneCycleLR
)
# 余弦退火(目前最常用)
scheduler = CosineAnnealingLR(optimizer, T_max=100, eta_min=1e-6)
# 验证集不提升时降低学习率
scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=10)
# Warmup + 余弦退火(Transformer 最常用)
scheduler = OneCycleLR(optimizer, max_lr=1e-3, epochs=100, steps_per_epoch=len(train_loader))
# 在训练循环中使用
for epoch in range(epochs):
train_one_epoch(...)
scheduler.step()7. 正则化技术
7.1 Dropout
训练时随机将一定比例的神经元输出置为0:
model = nn.Sequential(
nn.Linear(784, 512),
nn.ReLU(),
nn.Dropout(p=0.5), # 50% 的神经元随机丢弃
nn.Linear(512, 256),
nn.ReLU(),
nn.Dropout(p=0.3),
nn.Linear(256, 10)
)注意:Dropout 只在训练时激活,推理时关闭。PyTorch 在 model.eval() 时自动关闭 Dropout。
7.2 Batch Normalization
对每个 mini-batch 的每个特征进行标准化,加速训练,有一定正则化效果:
x^=σB2+ϵx−μB
y=γx^+β
其中 γ(scale)和 β(shift)是可学习参数。
model = nn.Sequential(
nn.Linear(256, 256),
nn.BatchNorm1d(256), # 放在线性层后、激活函数前
nn.ReLU(),
)
# CNN 中
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),BatchNorm vs LayerNorm:
- BatchNorm:对 batch 维度归一化,适合 CNN;batch size 小时不稳定
- LayerNorm:对特征维度归一化,适合 Transformer;不依赖 batch size
7.3 Weight Decay(L2 正则化)
在损失函数中加入权重的 L2 惩罚,防止权重过大:
# PyTorch 中通过 optimizer 的 weight_decay 参数实现
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)8. 用 PyTorch 搭建完整训练流程
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# 1. 定义模型
class MLP(nn.Module):
def __init__(self, input_dim, hidden_dims, output_dim, dropout=0.3):
super().__init__()
layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
layers.extend([
nn.Linear(prev_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),
nn.Dropout(dropout)
])
prev_dim = hidden_dim
layers.append(nn.Linear(prev_dim, output_dim))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
model = MLP(input_dim=784, hidden_dims=[512, 256, 128], output_dim=10)
# 2. 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
# 3. 训练循环
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
def train_epoch(model, loader, optimizer, criterion, device):
model.train()
total_loss, correct, total = 0, 0, 0
for X_batch, y_batch in loader:
X_batch, y_batch = X_batch.to(device), y_batch.to(device)
optimizer.zero_grad()
outputs = model(X_batch)
loss = criterion(outputs, y_batch)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item()
predicted = outputs.argmax(dim=1)
correct += (predicted == y_batch).sum().item()
total += y_batch.size(0)
return total_loss / len(loader), correct / total
def evaluate(model, loader, criterion, device):
model.eval()
total_loss, correct, total = 0, 0, 0
with torch.no_grad():
for X_batch, y_batch in loader:
X_batch, y_batch = X_batch.to(device), y_batch.to(device)
outputs = model(X_batch)
loss = criterion(outputs, y_batch)
total_loss += loss.item()
predicted = outputs.argmax(dim=1)
correct += (predicted == y_batch).sum().item()
total += y_batch.size(0)
return total_loss / len(loader), correct / total
# 4. 主训练循环(含早停)
best_val_loss = float('inf')
patience, patience_counter = 10, 0
for epoch in range(100):
train_loss, train_acc = train_epoch(model, train_loader, optimizer, criterion, device)
val_loss, val_acc = evaluate(model, val_loader, criterion, device)
scheduler.step()
print(f"Epoch {epoch+1}: train_loss={train_loss:.4f}, train_acc={train_acc:.4f}, "
f"val_loss={val_loss:.4f}, val_acc={val_acc:.4f}")
# 早停
if val_loss < best_val_loss:
best_val_loss = val_loss
torch.save(model.state_dict(), 'best_model.pth')
patience_counter = 0
else:
patience_counter += 1
if patience_counter >= patience:
print(f"早停,在第 {epoch+1} 轮")
break
# 5. 加载最优模型并评估
model.load_state_dict(torch.load('best_model.pth'))
test_loss, test_acc = evaluate(model, test_loader, criterion, device)
print(f"测试集准确率: {test_acc:.4f}")9. 常见问题排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 训练 loss 不下降 | 学习率太小/梯度消失 | 增大学习率,换 ReLU,检查初始化 |
| 训练 loss 发散/NaN | 学习率太大/梯度爆炸 | 降低学习率,加梯度裁剪 |
| 训练好验证差 | 过拟合 | Dropout, L2正则, 更多数据, 早停 |
| GPU 利用率低 | batch size 太小/数据加载瓶颈 | 增大 batch size, 多进程数据加载 |
| 某些层梯度为0 | 死神经元(ReLU) | 换 Leaky ReLU,检查初始化 |