神经网络基础
神经网络基础
简介
神经网络是深度学习的核心计算模型,通过多层可微分的非线性变换将输入映射到输出。从感知机到多层前馈网络,再到现代的 Transformer,所有架构都建立在反向传播、梯度下降和链式法则的基础上。无论你是做计算机视觉、自然语言处理还是推荐系统,神经网络都是绕不开的基础。理解前向传播、反向传播、激活函数和优化器的工作原理,是掌握所有高级架构(CNN、RNN、Transformer)的前提。不要跳过基础直接用框架,否则遇到训练问题时将无从下手。
特点
核心概念
神经元与感知机
神经网络的最基本单元是神经元(Neuron),也称为感知机(Perceptron)。一个神经元接收多个输入信号,每个输入乘以对应的权重,然后求和并加上偏置,最后通过激活函数产生输出。数学表达为:
y = f(w1*x1 + w2*x2 + ... + wn*xn + b)其中 w 是权重,b 是偏置,f 是激活函数。单个感知机只能解决线性可分问题,但通过将多个感知机按层堆叠,并引入非线性激活函数,多层网络就能拟合任意复杂的函数映射关系——这就是万能逼近定理(Universal Approximation Theorem)的核心思想。
前向传播
前向传播(Forward Propagation)是数据从输入层逐层计算到输出层的过程。在每一层中,输入数据与权重矩阵相乘,加上偏置向量,然后通过激活函数,得到该层的输出,作为下一层的输入。以一个三层网络为例:
h1 = activation(W1 * x + b1)
h2 = activation(W2 * h1 + b2)
output = W3 * h2 + b3前向传播的结果是网络的预测值,需要与真实标签计算损失(Loss),作为反向传播的起点。
反向传播与链式法则
反向传播(Backpropagation)是训练神经网络的核心算法。它利用微积分中的链式法则(Chain Rule),从输出层开始,逐层计算损失函数对每个参数的偏导数(梯度),然后将梯度传递给优化器更新参数。
链式法则的核心思想:如果 y = f(g(x)),则 dy/dx = (dy/df) * (df/dg) * (dg/dx)。在深层网络中,梯度从输出层向输入层逐层传递,每一层的梯度是后续所有层梯度的乘积。这也解释了为什么深层网络容易出现梯度消失或梯度爆炸的问题。
PyTorch 的 autograd 模块自动完成反向传播,无需手动推导梯度公式,但理解其原理对调试训练问题至关重要。
实现
基础:手动实现一个两层前馈神经网络
# 示例1:手动实现一个两层前馈神经网络
import torch
import torch.nn as nn
class SimpleMLP(nn.Module):
def __init__(self, input_dim=784, hidden_dim=256, output_dim=10):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = x.view(x.size(0), -1) # 展平
x = self.relu(self.fc1(x))
x = self.fc2(x)
return x
model = SimpleMLP()
print(f"参数总量: {sum(p.numel() for p in model.parameters()):,}")完整的训练循环
# 示例2:完整的训练循环
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# 构造模拟数据
X = torch.randn(1000, 784)
y = torch.randint(0, 10, (1000,))
dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=64, shuffle=True)
model = SimpleMLP()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(10):
total_loss = 0
for batch_x, batch_y in loader:
optimizer.zero_grad()
output = model(batch_x)
loss = criterion(output, batch_y)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}, Loss: {total_loss/len(loader):.4f}")常见激活函数对比
# 示例3:常见激活函数对比
import torch
import torch.nn.functional as F
x = torch.linspace(-3, 3, 100)
activations = {
"ReLU": F.relu(x),
"Sigmoid": torch.sigmoid(x),
"Tanh": torch.tanh(x),
"GELU": F.gelu(x),
"LeakyReLU": F.leaky_relu(x, negative_slope=0.01),
}
for name, y in activations.items():
print(f"{name}: range=[{y.min().item():.3f}, {y.max().item():.3f}]")激活函数的选择直接影响训练效果:
- ReLU:计算简单、缓解梯度消失,最常用。但存在 Dead ReLU 问题(负区间神经元永远不激活)。
- Sigmoid:输出 (0, 1),适合二分类输出层。缺点是梯度消失严重、输出非零中心。
- Tanh:输出 (-1, 1),零中心化。但仍有梯度消失问题。
- GELU:Transformer 模型中的标准选择,平滑的 ReLU 变体。
- LeakyReLU:负区间有小斜率,解决 Dead ReLU 问题。
梯度消失与梯度爆炸实验
# 示例4:梯度消失与梯度爆炸的简单实验
import torch.nn as nn
deep_model = nn.Sequential()
dim = 256
for i in range(50):
deep_model.add_module(f"linear_{i}", nn.Linear(dim, dim))
deep_model.add_module(f"relu_{i}", nn.ReLU())
x = torch.randn(2, dim, requires_grad=True)
output = deep_model(x).sum()
output.backward()
print(f"输入梯度范数: {x.grad.norm().item():.6f}")
# 不使用残差连接时,50层网络梯度通常接近0使用残差连接缓解梯度消失
# 示例5:残差连接(ResNet 的核心思想)
import torch
import torch.nn as nn
class ResidualBlock(nn.Module):
def __init__(self, dim):
super().__init__()
self.fc1 = nn.Linear(dim, dim)
self.bn1 = nn.BatchNorm1d(dim)
self.fc2 = nn.Linear(dim, dim)
self.bn2 = nn.BatchNorm1d(dim)
def forward(self, x):
residual = x
out = torch.relu(self.bn1(self.fc1(x)))
out = self.bn2(self.fc2(out))
return torch.relu(out + residual) # 残差连接
# 深层网络带残差连接 — 梯度可以更稳定地传播
deep_resnet = nn.Sequential()
for i in range(20):
deep_resnet.add_module(f"block_{i}", ResidualBlock(256))
x = torch.randn(4, 256, requires_grad=True)
output = deep_resnet(x).sum()
output.backward()
print(f"残差网络输入梯度范数: {x.grad.norm().item():.6f}")BatchNorm 与 LayerNorm 的作用
# 示例6:归一化层对比
import torch
import torch.nn as nn
# BatchNorm:在 batch 维度上归一化,适合 CNN
batch_norm = nn.BatchNorm1d(64)
x_batch = torch.randn(32, 64) # (batch_size, features)
out_batch = batch_norm(x_batch)
print(f"BatchNorm 输出均值: {out_batch.mean(dim=0).mean():.6f}")
# LayerNorm:在特征维度上归一化,适合 Transformer/RNN
layer_norm = nn.LayerNorm(64)
x_seq = torch.randn(8, 16, 64) # (batch, seq_len, features)
out_layer = layer_norm(x_seq)
print(f"LayerNorm 输出均值: {out_layer.mean(dim=-1).mean():.6f}")BatchNorm 在训练时计算 mini-batch 的均值和方差进行归一化,推理时使用训练阶段累积的滑动均值和方差。LayerNorm 则对每个样本独立归一化,不依赖 batch 统计量,因此在序列模型和小 batch 场景中更稳定。
学习率调度与 Early Stopping
# 示例7:学习率调度与 Early Stopping
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR
model = nn.Linear(100, 10)
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = CosineAnnealingLR(optimizer, T_max=50)
best_val_loss = float('inf')
patience = 5
wait = 0
for epoch in range(50):
# 模拟训练
train_loss = 1.0 / (epoch + 1)
val_loss = train_loss + 0.1 * torch.randn(1).item()
# Early Stopping
if val_loss < best_val_loss:
best_val_loss = val_loss
wait = 0
torch.save(model.state_dict(), 'best_model.pt')
else:
wait += 1
if wait >= patience:
print(f"Early stopping at epoch {epoch}")
break
scheduler.step()
print(f"Epoch {epoch}: lr={scheduler.get_last_lr()[0]:.6f}, "
f"train_loss={train_loss:.4f}, val_loss={val_loss:.4f}")Dropout 正则化
# 示例8:Dropout 正则化
import torch
import torch.nn as nn
class DropoutNet(nn.Module):
def __init__(self, input_dim=784, hidden_dim=512, output_dim=10, dropout_rate=0.3):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.dropout1 = nn.Dropout(dropout_rate)
self.fc2 = nn.Linear(hidden_dim, hidden_dim)
self.dropout2 = nn.Dropout(dropout_rate)
self.fc3 = nn.Linear(hidden_dim, output_dim)
def forward(self, x, training=True):
x = x.view(x.size(0), -1)
x = torch.relu(self.fc1(x))
x = self.dropout1(x) if training else x
x = torch.relu(self.fc2(x))
x = self.dropout2(x) if training else x
x = self.fc3(x)
return x
model = DropoutNet()
model.train() # Dropout 生效
model.eval() # Dropout 不生效(推理模式)Dropout 在训练时随机丢弃一定比例的神经元,等效于训练多个子网络的集成。推理时不使用 Dropout,但输出需要按 dropout 比例缩放(PyTorch 的 nn.Dropout 自动处理)。
优化器选择指南
优化器决定了如何利用梯度来更新模型参数。不同的优化器在收敛速度、稳定性和最终效果上各有差异。
常见优化器对比
# 示例9:不同优化器的行为对比
import torch
import torch.nn as nn
import torch.optim as optim
def train_with_optimizer(optimizer_class, optimizer_name, lr=1e-3):
model = nn.Sequential(nn.Linear(100, 50), nn.ReLU(), nn.Linear(50, 10))
optimizer = optimizer_class(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
losses = []
for step in range(200):
x = torch.randn(32, 100)
y = torch.randint(0, 10, (32,))
optimizer.zero_grad()
loss = criterion(model(x), y)
loss.backward()
optimizer.step()
losses.append(loss.item())
return losses[-1]
# SGD: 收敛慢但泛化可能更好
sgd_loss = train_with_optimizer(
lambda p, lr: optim.SGD(p, lr=lr, momentum=0.9), "SGD+Momentum")
# Adam: 自适应学习率,收敛快,默认首选
adam_loss = train_with_optimizer(
lambda p, lr: optim.Adam(p, lr=lr), "Adam")
# AdamW: 带解耦权重衰减的 Adam,Transformer 训练标配
adamw_loss = train_with_optimizer(
lambda p, lr: optim.AdamW(p, lr=lr, weight_decay=1e-4), "AdamW")
print(f"SGD+Momentum 最终 loss: {sgd_loss:.4f}")
print(f"Adam 最终 loss: {adam_loss:.4f}")
print(f"AdamW 最终 loss: {adamw_loss:.4f}")优化器选择建议:
- 入门和快速实验:直接使用 Adam(学习率 1e-3),几乎不用调参。
- 追求最佳效果:AdamW + Cosine Annealing + Warmup,Transformer 训练标配。
- 追求极致泛化:SGD + Momentum + 学习率调度,收敛慢但最终精度可能更高。
- 避免的陷阱:不同优化器需要不同的学习率范围,Adam 通常 1e-3~1e-4,SGD 通常 1e-1~1e-2。
损失函数选择
损失函数定义了模型预测与真实标签之间的差距,不同的任务需要不同的损失函数:
# 示例10:常见损失函数
import torch
import torch.nn as nn
import torch.nn.functional as F
# 分类任务
criterion_ce = nn.CrossEntropyLoss() # 多分类(内置 softmax)
logits = torch.randn(4, 10) # 原始输出,不需要 softmax
labels = torch.tensor([0, 3, 5, 7])
loss_ce = criterion_ce(logits, labels)
# 二分类
criterion_bce = nn.BCEWithLogitsLoss() # 内置 sigmoid
binary_logits = torch.randn(4, 1)
binary_labels = torch.tensor([1.0, 0.0, 1.0, 0.0]).unsqueeze(1)
loss_bce = criterion_bce(binary_logits, binary_labels)
# 回归任务
criterion_mse = nn.MSELoss()
predictions = torch.randn(4, 1)
targets = torch.randn(4, 1)
loss_mse = criterion_mse(predictions, targets)
criterion_mae = nn.L1Loss() # 对异常值更鲁棒
loss_mae = criterion_mae(predictions, targets)
print(f"CrossEntropy: {loss_ce.item():.4f}")
print(f"BCEWithLogits: {loss_bce.item():.4f}")
print(f"MSE: {loss_mse.item():.4f}")
print(f"MAE: {loss_mae.item():.4f}")权重初始化
好的初始化可以让训练更快收敛并避免梯度消失/爆炸。Xavier 初始化适合 Sigmoid/Tanh,He 初始化适合 ReLU:
# 示例11:权重初始化
import torch
import torch.nn as nn
# Xavier 初始化 — 适合 Sigmoid/Tanh
def xavier_init(m):
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
if m.bias is not None:
nn.init.zeros_(m.bias)
# He (Kaiming) 初始化 — 适合 ReLU
def he_init(m):
if isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.zeros_(m.bias)
model = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10))
model.apply(he_init) # 对整个模型应用初始化数据加载与预处理
# 示例12:自定义 Dataset 与数据增强
import torch
from torch.utils.data import Dataset, DataLoader
class CustomImageDataset(Dataset):
def __init__(self, data, labels, transform=None):
self.data = data
self.labels = labels
self.transform = transform
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
sample = self.data[idx]
label = self.labels[idx]
if self.transform:
sample = self.transform(sample)
return sample, label
# 模拟数据
train_data = torch.randn(500, 3, 32, 32)
train_labels = torch.randint(0, 10, (500,))
dataset = CustomImageDataset(train_data, train_labels)
loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=0)
for batch_idx, (images, labels) in enumerate(loader):
print(f"Batch {batch_idx}: images {images.shape}, labels {labels.shape}")
if batch_idx >= 2:
breakGPU 训练与混合精度
# 示例13:GPU 训练与自动混合精度(AMP)
import torch
from torch.cuda.amp import autocast, GradScaler
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SimpleMLP().to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
scaler = GradScaler() # 混合精度缩放器
for epoch in range(5):
for batch_x, batch_y in loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
# 自动混合精度:前向传播使用 FP16,反向传播使用 FP32
with autocast():
output = model(batch_x)
loss = criterion(output, batch_y)
# 缩放 loss 防止 FP16 梯度下溢
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
print(f"训练完成,设备: {device}")混合精度训练利用 GPU 的 Tensor Core 加速 FP16 计算,可以将训练速度提升 1.5-3 倍,同时减少显存占用。GradScaler 通过动态缩放防止 FP16 梯度下溢。
模型保存与加载
# 示例14:模型保存与加载
import torch
# 方式1:只保存参数(推荐)
torch.save(model.state_dict(), 'model_weights.pt')
loaded_model = SimpleMLP()
loaded_model.load_state_dict(torch.load('model_weights.pt'))
loaded_model.eval()
# 方式2:保存整个模型(包含结构,不推荐跨版本使用)
torch.save(model, 'model_full.pt')
restored_model = torch.load('model_full.pt')
# 方式3:保存训练检查点(包含优化器状态,支持断点续训)
checkpoint = {
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': loss.item(),
}
torch.save(checkpoint, 'checkpoint.pt')
# 恢复训练
checkpoint = torch.load('checkpoint.pt')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
start_epoch = checkpoint['epoch'] + 1优点
缺点
性能优化技巧
梯度裁剪
# 示例15:梯度裁剪防止梯度爆炸
for batch_x, batch_y in loader:
optimizer.zero_grad()
output = model(batch_x)
loss = criterion(output, batch_y)
loss.backward()
# 方式1:按范数裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 方式2:按值裁剪
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=0.5)
optimizer.step()冻结部分层进行迁移学习
# 示例16:冻结预训练模型的部分层
import torchvision.models as models
# 加载预训练 ResNet
pretrained = models.resnet18(pretrained=True)
# 冻结所有卷积层,只训练全连接层
for name, param in pretrained.named_parameters():
if "fc" not in name: # 冻结除最后全连接层外的所有层
param.requires_grad = False
# 只优化需要梯度的参数
optimizer = optim.Adam(filter(lambda p: p.requires_grad, pretrained.parameters()), lr=1e-3)
trainable_params = sum(p.numel() for p in pretrained.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in pretrained.parameters())
print(f"可训练参数: {trainable_params:,} / 总参数: {total_params:,}")总结
神经网络是现代 AI 的基石,理解前向传播、反向传播、激活函数和优化器的工作原理,是掌握所有高级架构的前提。核心要点包括:前向传播是数据从输入层逐层计算到输出层的过程,反向传播则是梯度从输出层逐层传回输入层;学习率是最关键的超参数之一;BatchNorm 和 LayerNorm 通过归一化中间层的激活值来加速训练和稳定梯度;正则化手段(Dropout、Weight Decay、Early Stopping)是防止过拟合的核心工具。
关键知识点
- 前向传播是数据从输入层逐层计算到输出层的过程,反向传播则是梯度从输出层逐层传回输入层
- 学习率是最关键的超参数之一,过大会导致训练不稳定,过小则收敛缓慢
- BatchNorm 和 LayerNorm 通过归一化中间层的激活值来加速训练和稳定梯度
- 正则化手段(Dropout、Weight Decay、Early Stopping)是防止过拟合的核心工具
项目落地视角
- 训练前先跑通一个最小可复现实验,确认 loss 能正常下降、梯度没有异常
- 把数据划分、模型配置、超参数和评估指标统一记录,保证实验可复现
- 线上推理时要关注显存占用、推理延迟和模型版本管理
- 上线前准备兜底策略,比如置信度阈值过滤和异常输入检测
常见误区
- 认为更深的网络一定更好——残差连接、归一化和合理初始化比单纯堆层数更重要
- 忽略数据质量,把精力全部放在调模型结构上
- 用测试集调参,导致报告的指标虚高
- 不验证梯度是否正常就直接增大学习率或训练轮数
进阶路线
- 掌握 PyTorch 的自定义 Module、Dataset 和 DataLoader 机制
- 学习学习率调度(Cosine Annealing、Warmup)、混合精度训练等工程技巧
- 深入理解 Transformer 架构,它是当前大多数 SOTA 模型的基础
- 阅读 "Deep Learning"(Goodfellow 等著)和 "Dive into Deep Learning" 系统构建理论基础
适用场景
- 有足够标注数据的分类、回归和序列建模任务
- 需要从原始数据(图像、文本、音频)中自动学习特征的场景
- 传统规则方法难以建模的复杂非线性关系问题
落地建议
- 从小模型和小数据开始验证管线正确性,再逐步扩大规模
- 固定随机种子、划分固定评估集,保证每次实验可比
- 训练过程中同步监控训练集和验证集的 loss 曲线,及时发现过拟合
排错清单
- loss 不下降:检查学习率、数据标签是否正确、梯度是否为零或 NaN
- 训练集效果好但验证集差:增加正则化、减少模型容量、增加数据增强
- 梯度爆炸/消失:检查初始化方法、尝试 BatchNorm/LayerNorm、使用残差连接
复盘问题
- 你的模型在验证集上的 loss 曲线是什么形状?是否出现过拟合的信号?
- 如果把模型容量减半,效果变化有多大?这说明什么?
- 当前训练使用的哪个优化器和学习率策略?是否做过系统对比?
