多层感知机
多层感知机
多层感知机 (MLP) 由多层神经元组成, 每一层与它的上一层相连, 从中接收输入; 同时每一层也与它的下一层相连, 影响当前层的神经元.
需要激活函数, 否则仍然是线性.
激活函数
ReLU 线性整流函数:
ReLU(x)=max(0,x)
Sigmoid 函数:
σ(x)=1+e−x1
tanh 双曲正切函数:
tanh(x)=ex+e−xex−e−x
通用近似定理.
多层感知机的实现
import torch from torch import nn from torch.utils.data import DataLoader from torchvision import datasets, transforms
device = torch.device( "cuda" if torch.cuda.is_available() else "cpu" )
print("使用设备: ", device)
transform = transforms.ToTensor()
train_dataset = datasets.FashionMNIST( root="./data", train=True, transform=transform, download=True )
test_dataset = datasets.FashionMNIST( root="./data", train=False, transform=transform, download=True )
batch_size = 256
train_loader = DataLoader( train_dataset, batch_size=batch_size, shuffle=True, num_workers=0 )
test_loader = DataLoader( test_dataset, batch_size=batch_size, shuffle=False, num_workers=0 )
model = nn.Sequential( nn.Flatten(), nn.Linear(28*28, 64), nn.ReLU(), nn.Linear(64, 10) )
model = model.to(device)
print(model)
def init_weights(module): if isinstance(module, nn.Linear): nn.init.normal_( module.weight, mean=0, std=0.01 ) nn.init.zeros_(module.bias)
model.apply(init_weights)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD( model.parameters(), lr=0.1 )
def evaluate_accuracy(model, data_loader, device): model.eval()
correct = 0 total = 0
with torch.no_grad(): for X, y in data_loader: X = X.to(device) y = y.to(device)
logits = model(X) predictions = logits.argmax(dim=1)
correct += (predictions == y).sum().item() total += y.numel()
return correct / total
num_epochs = 10
for epoch in range(num_epochs): model.train() total_loss = 0.0 correct = 0 total = 0
for x, y in train_loader: x = x.to(device) y = y.to(device)
logits = model(x)
loss = loss_fn(logits, y)
optimizer.zero_grad() loss.backward() optimizer.step()
total_loss += loss.item() * x.shape[0]
predictions = logits.argmax(dim=1) correct += (predictions == y).sum().item() total += y.numel()
train_loss = total_loss / total train_acc = correct / total test_acc = evaluate_accuracy(model, test_loader, device)
print( f"epoch {epoch + 1:2d}, " f"loss = {train_loss:.4f}, " f"train acc = {train_acc:.4f}, " f"test acc = {test_acc:.4f}" )
|
模型选择, 欠拟合与过拟合
训练误差: 模型在训练数据集上计算得到的误差.
泛化误差: 模型应用在同样从原始样本的分布中抽取的无限多数据样本时, 模型误差的期望.
显然我们无法计算泛化误差, 因此我们将模型应用于一个独立的测试集来估计泛化误差.
欠拟合: 训练误差和验证误差都很严重, 但它们之间仅有一点差距. 如果模型不能降低训练误差, 这可能意味着模型过于简单 (即表达能力不足), 无法捕获试图学习的模式. 此外, 由于我们的训练和验证误差之间的泛化误差很小, 我们有理由相信可以用一个更复杂的模型降低训练误差.
过拟合: 训练误差明显低于验证误差.
对抗过拟合的技术称为正则化.
权重衰减
权重衰减会鼓励模型使用较小的权重.
原本我们只最小化训练损失 L(w) , 加入权重衰减后, 目标函数变为:
Ltotal=L(w)+2λ∥w∥22
其中:
∥w∥22=w12+w22+⋯+wn2
λ 控制惩罚强度.
- λ=0: 不使用权重衰减;
- λ 较小: 轻微限制权重;
- λ 很大: 强迫权重接近 0, 可能欠拟合.
这通常也称为 L2 正则化.
权重较小时, 模型通常变化得更加平滑, 不容易为了少数训练样本做出极端调整.
所以可以粗略理解为: 大权重倾向于复杂, 敏感的模型, 小权重倾向于平滑, 稳定的模型.
对总损失求梯度:
∇wLtotal=∇wL+λw
使用梯度下降:
w←w−η(∇wL+λw)
展开:
w←(1−ηλ)w−η∇wL
可以看到每次更新时, 原来的权重都会乘上一个略小于 1 的数, 所以叫权重衰减.
偏置通常不做权重衰减, 因为参数量很少, 且主要控制整体平移.
实现如下:
def train_concise(wd): net = nn.Sequential(nn.Linear(num_inputs, 1)) for param in net.parameters(): param.data.normal_() loss = nn.MSELoss(reduction='none') num_epochs, lr = 100, 0.003 trainer = torch.optim.SGD([ {"params":net[0].weight,'weight_decay': wd}, {"params":net[0].bias}], lr=lr) animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log', xlim=[5, num_epochs], legend=['train', 'test']) for epoch in range(num_epochs): for X, y in train_iter: trainer.zero_grad() l = loss(net(X), y) l.mean().backward() trainer.step() if (epoch + 1) % 5 == 0: animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss), d2l.evaluate_loss(net, test_iter, loss))) print('w的L2范数: ', net[0].weight.norm().item())
|
暂退法
Dropout 暂退法会会在训练过程中, 随机将一部分隐藏层神经元的输出设为 0. , 随机将一部分隐藏层神经元的输出设为 0, 以减少神经元之间的过度协同适应, 迫使模型在部分信息缺失时也能完成任务, 模型会学习更分散, 更稳健的特征.
丢弃概率为 p , 则输出时需要除以 1−p , 保证输出期望不变. 这种实现称为反向 Dropout, PyTorch 使用的就是这种方法.
当然是用在隐藏层不是输出层.
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(), nn.Dropout(dropout1), nn.Linear(256, 256), nn.ReLU(), nn.Dropout(dropout2), nn.Linear(256, 10))
def init_weights(m): if type(m) == nn.Linear: nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);
|
前向传播, 反向传播和计算图
计算图示例:
w ──┐ × ── a ──┐ x ──┘ + ── z ── 平方 ── y b ──┘
|
前向传播: 从输入开始, 按照计算图从左向右依次计算, 最后得到模型输出和损失.
作用:
- 根据输入得到预测结果
- 根据预测结果计算损失
- 保存中间结果, 供反向传播使用
反向传播: 从最终的损失开始, 沿计算图反方向, 利用链式法则逐层计算梯度.
import torch
x = torch.tensor(3.0) w = torch.tensor(2.0, requires_grad=True) b = torch.tensor(1.0, requires_grad=True)
z = w * x + b y = z ** 2
y.backward()
print(y) print(w.grad) print(b.grad)
|
数值稳定性和模型初始化
数值稳定性: 在有限精度的计算机上进行计算时, 尽量避免溢出, 下溢, 除零, 精度丢失等问题, 使结果可靠.
梯度消失: 参数几乎不更新, 前面的层学不到东西, 训练非常慢.
梯度爆炸: loss 剧烈波动, 参数变成 inf, loss 变成 nan 等.
解决方法:
- 合理初始化参数;
- 使用 ReLU, GELU 等激活函数 (sigmoid 容易导致梯度消失)
- 使用 BatchNorm, LayerNorm
- 使用残差连接
- 梯度裁剪
- 调整学习率
模型初始化: 在训练开始前为权重和偏置设置初始数值.
初始化可以打破对称性, 防止两个神经元一模一样.
如果初始化权重的方差固定不变, 则随着输入维度增大, 输出的方差会增大, 因此为保证输出方差和输入方差大致相同, 我们一般使权重的标准差大致为 n1 .
Xavier 初始化希望同时考虑前向传播和反向传播, 因此令权重方差大约为
Var(W)=fan_in+fan_out2
标准差即开根号.