一,过拟合和欠拟合

当我们比较训练和验证误差时,我们要注意两种常见的情况。 首先,我们要注意这样的情况:训练误差和验证误差都很严重, 但它们之间仅有一点差距。 如果模型不能降低训练误差,这可能意味着模型过于简单(即表达能力不足), 无法捕获试图学习的模式。 此外,由于我们的训练和验证误差之间的泛化误差很小, 我们有理由相信可以用一个更复杂的模型降低训练误差。 这种现象被称为欠拟合(underfitting)。

另一方面,当我们的训练误差明显低于验证误差时要小心, 这表明严重的过拟合(overfitting)。 注意,过拟合并不总是一件坏事。 特别是在深度学习领域,众所周知, 最好的预测模型在训练数据上的表现往往比在保留(验证)数据上好得多。 最终,我们通常更关心验证误差,而不是训练误差和验证误差之间的差距。

是否过拟合或欠拟合可能取决于模型复杂性和可用训练数据集的大小, 这两个点将在下面进行讨论。

训练误差(training error): 模型在训练数据集上计算得到的误差。

泛化误差(generalization error): 模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望。

  • 欠拟合:模型复杂度不足,无法捕捉数据中的模式,导致训练和测试损失都很高。
  • 过拟合:模型复杂度过高,能够很好地拟合训练数据(包括噪声),但无法泛化到测试数据,导致测试损失很高。
  • 正常拟合:模型复杂度与数据的真实复杂度匹配,能够有效降低训练和测试损失。

在这里插入图片描述

1.1权重衰退

范数(Norm)是衡量向量大小的一种方法

  • L1 范数 :对于一个 n 维向量 x = [x₁, x₂, …, xₙ],其 L1 范数定义为 ||x||₁ = |x₁| + |x₂| + … + |xₙ|,即向量各元素绝对值之和。
  • L2 范数 :对于一个 n 维向量 x = [x₁, x₂, …, xₙ],其 L2 范数定义为 ||x||₂ = √(x₁² + x₂² + … + xₙ²),即向量各元素平方和的平方根,也称为欧几里得范数。
  • Lp 范数 :对于一个 n 维向量 x = [x₁, x₂, …, xₙ] 和一个正整数 p,其 Lp 范数定义为 ||x||ₚ = (|x₁|ᵖ + |x₂|ᵖ + … + |xₙ|ᵖ)^(1/p)。

首先使用L2范数,而不是L1范数。 事实上,这个选择在整个统计领域中都是有效的和受欢迎的。 L2正则化线性模型构成经典的岭回归(ridge regression)算法, L1正则化线性回归是统计学中类似的基本模型, 通常被称为套索回归(lasso regression)。 使用L2范数的一个原因是它对权重向量的大分量施加了巨大的惩罚。 这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型。 在实践中,这可能使它们对单个变量中的观测误差更为稳定。 相比之下,L1惩罚会导致模型将权重集中在一小部分特征上, 而将其他权重清除为零。 这称为特征选择(feature selection),这可能是其他场景下需要的。

1.2.1 L1 正则化

  • 定义 :L1 正则化是在损失函数的基础上加上模型参数的 L1 范数,即 ||w||₁,其中 w 是模型的权重向量。其目的是使模型的参数尽可能稀疏,即让一些参数变为 0,从而实现特征选择的功能。
  • 数学表达式 :假设损失函数为 L(w),则加入 L1 正则化后的目标函数为 J(w) = L(w) + λ||w||₁,其中 λ 是正则化系数,用于控制正则化项的权重。

1.2.2 L2 正则化

  • 定义 :L2 正则化是在损失函数的基础上加上模型参数的 L2 范数的平方,即 ||w||₂²,其目的是防止模型过拟合,使模型的参数值尽可能小,从而使模型更加平滑。
  • 数学表达式 :假设损失函数为 L(w),则加入 L2 正则化后的目标函数为 J(w) = L(w) + λ||w||₂² = L(w) + λ(w₀² + w₁² + … + wₙ²),其中 λ 是正则化系数。

例子

损失由下式给出
L(w,b)=1n∑i=1n12(w⊤x(i)+b−y(i))2. L(\mathbf{w}, b) = \frac{1}{n}\sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right)^2. L(w,b)=n1i=1n21(wx(i)+by(i))2.
x(i)是样本i的特征, y(i)是样本i的标签, (w,b)是权重和偏置参数。 为了惩罚权重向量的大小, 我们必须以某种方式在损失函数中添加‖w‖2
L(w,b)+λ2∥w∥2, L(\mathbf{w}, b) + \frac{\lambda}{2} \|\mathbf{w}\|^2, L(w,b)+2λw2,
对于λ=0,我们恢复了原来的损失函数。 对于λ>0,我们限制‖w‖的大小。 这里我们仍然除以2:当我们取一个二次函数的导数时, 2和1/2会抵消,以确保更新表达式看起来既漂亮又简单。

L2正则化回归的小批量随机梯度下降更新如下式
w←(1−ηλ)w−η∣B∣∑i∈Bx(i)(w⊤x(i)+b−y(i)). \begin{aligned} \mathbf{w} & \leftarrow \left(1- \eta\lambda \right) \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right). \end{aligned} w(1ηλ)wBηiBx(i)(wx(i)+by(i)).
如w1,w2,…,wd)更具可读性。 |B|表示每个小批量中的样本数,这也称为批量大小(batch size)。 η表示学习率(learning rate)。

1.2.3 例子(L2)

目标拟合的函数
y=0.05+∑i=1d0.01xi+ϵ where ϵ∼N(0,0.012). y = 0.05 + \sum_{i = 1}^d 0.01 x_i + \epsilon \text{ where } \epsilon \sim \mathcal{N}(0, 0.01^2). y=0.05+i=1d0.01xi+ϵ where ϵN(0,0.012).
标签同时被均值为0,标准差为0.01高斯噪声破坏。 为了使过拟合的效果更加明显,我们可以将问题的维数增加到d=200, 并使用一个只包含20个样本的小训练集**。数据越少,模型越复杂(wi,i越多),越容易过拟合**

%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l

n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
train_data = d2l.synthetic_data(true_w, true_b, n_train)
train_iter = d2l.load_array(train_data, batch_size)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)

def init_params():
    w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
    b = torch.zeros(1, requires_grad=True)
    return [w, b]

def l2_penalty(w):
    return torch.sum(w.pow(2)) / 2
    
def train(lambd):
    w, b = init_params()
    net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
    num_epochs, lr = 100, 0.003
    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:
            # 增加了L2范数惩罚项,
            # 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
            l = loss(net(X), y) + lambd * l2_penalty(w)
            l.sum().backward()
            d2l.sgd([w, b], lr, batch_size)
        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范数是:', torch.norm(w).item())

train(lambd=0) #输出:w的L2范数是: 12.963241577148438
train(lambd=3)#输出:w的L2范数是: 0.3556520938873291

  • 不使用权重衰减:当 λ=0 时,模型的权重向量的 L2 范数较大(例如,12.9632),训练误差较低,但测试误差较高,表明模型过拟合。
  • 使用权重衰减:当 λ=3 时,模型的权重向量的 L2 范数较小(例如,0.3556),训练误差略有增加,但测试误差显著降低,表明模型的泛化能力提高。
    在这里插入图片描述

二, Dropout

Dropout 是一种在深度学习中常用的正则化技术,用于防止神经网络过拟合。它的核心思想是在训练过程中随机“关闭”一部分神经元(即将这些神经元的输出设置为 0),从而迫使网络学习更加鲁棒的特征表示。
h′={0 概率为 ph1−p 其他情况 \begin{split}\begin{aligned} h' = \begin{cases} 0 & \text{ 概率为 } p \\ \frac{h}{1-p} & \text{ 其他情况} \end{cases} \end{aligned}\end{split} h={01ph 概率为 p 其他情况

2.1 Dropout 的基本原理

  • 随机性:在每次训练迭代中,随机选择一部分神经元(通常是按照一定的概率 p)将其输出设置为 0,而保留的神经元则按比例放大其输出(通常放大比例为 1/(1−p)),以保持网络的总体输出期望值不变。
  • 防止过拟合:通过随机关闭神经元,Dropout 减少了神经元之间的协同适应性(co-adaptation),迫使网络中的每个神经元独立地学习有用的特征,从而提高模型的泛化能力。
    在这里插入图片描述

2.2 Dropout 的实现步骤

假设有一个输入张量 X,Dropout 的实现可以分为以下几个步骤:

  1. 生成随机掩码
    • 对于每个神经元,以概率 p 随机生成一个二值掩码(0 或 1)。
    • 例如,假设 p=0.5,则每个神经元有 50% 的概率被设置为 0,50% 的概率保持不变。
  2. 应用掩码
    • 将生成的掩码与输入张量逐元素相乘,被掩码为 0 的神经元输出将被置为 0,保留的神经元输出保持不变。
  3. 缩放保留的神经元
    • 为了保持网络的总体输出期望值不变,通常会对保留的神经元进行缩放,缩放系数为 1/(1−p)。
      在这里插入图片描述

2.3代码

import torch
from torch import nn, optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import torchvision
import torch.nn.functional as F
from d2l import torch as d2l
import numpy as np 

def dropout_layer(X, dropout):
    assert 0 <= dropout <= 1
    # 在本情况中,所有元素都被丢弃
    if dropout == 1:
        return torch.zeros_like(X)
    # 在本情况中,所有元素都被保留
    if dropout == 0:
        return X
    mask = (torch.rand(X.shape) > dropout).float()
    return mask * X / (1.0 - dropout)
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
dropout1, dropout2 = 0.3, 0.5
net = nn.Sequential(nn.Flatten(),
        nn.Linear(784, 256),
        nn.ReLU(),
        # 在第一个全连接层之后添加一个dropout层
        #nn.Dropout(dropout1),
        nn.Linear(256, 256),
        nn.ReLU(),
        # 在第二个全连接层之后添加一个dropout层
        #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);
# 数据预处理
transform = transforms.ToTensor()

batch_size = 256  # 批量大小
train_dataset = torchvision.datasets.FashionMNIST(
    root="./data", train=True, transform=transform, download=True)
test_dataset = torchvision.datasets.FashionMNIST(
    root="./data", train=False, transform=transform, download=True)

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
len(train_dataset)
num_epochs, lr, batch_size = 10, 0.05, 256
criterion= nn.CrossEntropyLoss(reduction='mean')
optimizer = optim.SGD(net.parameters(), lr=lr)
train_losses = []
train_accuracies = []
test_accuracies = []
device = 'cpu'
def train(model, device, train_loader, optimizer, criterion):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
    return loss.item()  
def test(model, device, test_loader, criterion):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += criterion(output, target).item()
            pred = output.argmax(dim=1, keepdim=True)
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    accuracy =  correct / len(test_loader.dataset)
    return accuracy, test_loss  
for epoch in range(1, 11):
    train_loss = train(net, device, train_loader, optimizer, criterion)
    train_losses.append(train_loss)
    train_correct = 0
    train_total = 0
    with torch.no_grad():
        for data, target in train_loader:
            data, target = data.to(device), target.to(device)
            output = net(data)
            _, predicted = torch.max(output, 1)
            train_total += target.size(0)
            train_correct += (predicted == target).sum().item()
    train_accuracy = train_correct / train_total
    train_accuracies.append(train_accuracy)
    test_accuracy, test_loss = test(net, device, test_loader, criterion)
    test_accuracies.append(test_accuracy)

    print(f"Epoch [{epoch}/{10}], Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.4f}, Test Accuracy: {test_accuracy:.4f}")

更多推荐