Skip to content

1. 基础概念 ​

一个模型往往从下面两个维度上表现:

  • 泛化能力:模型在训练数据上表现良好,更希望它能够在从未见过的新数据上保持较高的预测准确度
  • 过拟合:模型在训练集上表现很好,但在测试集上表现很差

特别是在神经网络中,由于其极强的表达能力,更容易出现过拟合。

为了减少过拟合、提升泛化能力,人们设计了一系列策略来:

  • 限制模型的复杂度
  • 提高模型对噪声的鲁棒性
  • 让模型更稳健

这些策略被称作正则化,使得模型在不同数据上保持良好的表现。


2. Dropout 正则化 ​

2.1 公式定义 ​

在神经网络中模型参数较多,在数据量不足的情况下,多个神经元相互依赖,形成只对训练集有效的复杂特征组合,很容易导致模型过拟合。

使用 Dropout 方法,在训练过程中,随机“失活”一部分神经元,迫使网络在不稳定结构下学习鲁棒、可复用的特征。

在训练阶段,对每一层(通常是全连接层):

对每个神经元独立采样一个伯努利随机变量:

\[r_i \sim \text{Bernoulli}(p) \]

神经元输出被随机屏蔽:

\[\tilde{h}_i = r_i \cdot h_i \]

其中:

  • \(p\):保留概率(keep probability)
  • \(1-p\):dropout rate

每一次 mini-batch 都对应一个不同的子网络结构。

在推理阶段,不再丢弃神经元,使用完整网络,并进行尺度补偿:

训练时:

\[\tilde{h}_i = \frac{r_i}{p} \cdot h_i \]

测试时:

\[\tilde{h}_i = h_i \]

2.2 计算示例 ​

实验:

  • 训练 不使用 Dropout 的模型
  • 训练 使用 Dropout 的模型
  • 记录训练 / 验证损失
python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt

# 固定随机种子,保证实验可复现
torch.manual_seed(42)

# =========================================================
# 1. 构造一个小样本、带噪声的数据集(容易过拟合)
# =========================================================
def make_dataset(n_samples=200, input_dim=20):
    X = torch.randn(n_samples, input_dim)
    true_w = torch.randn(input_dim, 1)
    y = (X @ true_w + 0.5 * torch.randn(n_samples, 1) > 0).float()
    return X, y

X, y = make_dataset()

# 划分训练集 / 验证集
X_train, X_val = X[:150], X[150:]
y_train, y_val = y[:150], y[150:]

train_loader = DataLoader(
    TensorDataset(X_train, y_train),
    batch_size=32,
    shuffle=True
)

val_loader = DataLoader(
    TensorDataset(X_val, y_val),
    batch_size=32
)

# =========================================================
# 2. 定义 MLP 模型(是否使用 Dropout 作为参数)
# =========================================================
class MLP(nn.Module):
    def __init__(self, input_dim, use_dropout=False):
        super().__init__()

        self.net = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.ReLU(),
            nn.Dropout(p=0.5) if use_dropout else nn.Identity(),

            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Dropout(p=0.5) if use_dropout else nn.Identity(),

            nn.Linear(64, 1),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.net(x)

# =========================================================
# 3. 训练 / 验证函数
# =========================================================
def train_one_epoch(model, dataloader, optimizer, criterion):
    model.train()
    total_loss = 0.0

    for x, y in dataloader:
        optimizer.zero_grad()
        y_pred = model(x)
        loss = criterion(y_pred, y)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

    return total_loss / len(dataloader)


def evaluate(model, dataloader, criterion):
    model.eval()
    total_loss = 0.0

    with torch.no_grad():
        for x, y in dataloader:
            y_pred = model(x)
            loss = criterion(y_pred, y)
            total_loss += loss.item()

    return total_loss / len(dataloader)

# =========================================================
# 4. 运行实验(是否使用 Dropout)
# =========================================================
def run_experiment(use_dropout, num_epochs=50):
    model = MLP(input_dim=20, use_dropout=use_dropout)
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    criterion = nn.BCELoss()

    train_losses = []
    val_losses = []

    for epoch in range(num_epochs):
        train_loss = train_one_epoch(
            model, train_loader, optimizer, criterion
        )
        val_loss = evaluate(
            model, val_loader, criterion
        )

        train_losses.append(train_loss)
        val_losses.append(val_loss)

        print(
            f"[{'Dropout' if use_dropout else 'No Dropout'}] "
            f"Epoch {epoch:02d} | "
            f"Train Loss: {train_loss:.4f} | "
            f"Val Loss: {val_loss:.4f}"
        )

    return train_losses, val_losses

# =========================================================
# 5. 运行对比实验
# =========================================================
print("==== Training model WITHOUT Dropout ====")
train_no_dp, val_no_dp = run_experiment(use_dropout=False)

print("\n==== Training model WITH Dropout ====")
train_dp, val_dp = run_experiment(use_dropout=True)

# =========================================================
# 6. 可视化收敛曲线
# =========================================================
epochs = range(1, len(train_no_dp) + 1)

plt.figure(figsize=(10, 6))

# 无 Dropout
plt.plot(epochs, train_no_dp, '--', label="Train Loss (No Dropout)")
plt.plot(epochs, val_no_dp, '--', label="Val Loss (No Dropout)")

# 使用 Dropout
plt.plot(epochs, train_dp, label="Train Loss (Dropout)")
plt.plot(epochs, val_dp, label="Val Loss (Dropout)")

plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Dropout vs No Dropout: Training and Validation Loss")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

3. 批量归一化 ​

3.1 公式定义 ​

在深度网络中,随着层数加深,每一层参数更新,会导致后续层输入分布不断变化,这被称为内部协变量偏移,这样会导致模型:

  • 训练不稳定
  • 对初始化和学习率敏感
  • 收敛速度慢

而批量归一化(Batch Normalization)的核心目标是在每一层中,强制中间激活保持稳定分布。

设某一层的输入(线性变换后、激活前)为:

\[z = Wx + b \]

首先计算 mini-batch 的均值与方差:

\[\mu_B = \frac{1}{m} \sum_{i=1}^{m} z_i \]
\[\sigma_B^2 = \frac{1}{m} \sum_{i=1}^{m} (z_i - \mu_B)^2 \]

对每一层进行标准化:

\[\hat{z}_i = \frac{z_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} \]

其中 \(\epsilon\) 是一个很小的常数,防止分母为 \(0\)

然后执行仿射变换,对标准化的数据进行重构:

\[y_i = \gamma \hat{z}_i + \beta \]

其中:

  • \(\gamma\):表示缩放参数
  • \(\beta\):表示平移参数

3.2 计算示例 ​

批量归一化主要分为三类:

  • BatchNorm1d:主要用来处理一维数据,例如文本。输入数据的形状要求是(N条样本,num_features多个特征)
  • BatchNorm2d:主要用来处理二维数据,例如图片。输入数据的形状要求是(N,C,H,W)。 N表示N张图片,C表示颜色通道,H图片的高度,W图片的宽度
  • BatchNorm3d:主要处理三维数据,例如:视频、医学影像等。输入数据的形状要求是(N,C,D,H,W)

这里以一维批量归一化进行实验:

python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt

torch.manual_seed(42)

# =====================================================
# 1. 构造数据(同样容易过拟合)
# =====================================================
def make_dataset(n_samples=300, input_dim=20):
    X = torch.randn(n_samples, input_dim)
    true_w = torch.randn(input_dim, 1)
    y = (X @ true_w + 0.8 * torch.randn(n_samples, 1) > 0).float()
    return X, y

X, y = make_dataset()

X_train, X_val = X[:200], X[200:]
y_train, y_val = y[:200], y[200:]

train_loader = DataLoader(
    TensorDataset(X_train, y_train),
    batch_size=32,
    shuffle=True
)

val_loader = DataLoader(
    TensorDataset(X_val, y_val),
    batch_size=32
)

# =====================================================
# 2. MLP 模型(是否使用 BatchNorm)
# =====================================================
class MLP(nn.Module):
    def __init__(self, input_dim, use_bn=False):
        super().__init__()

        self.net = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.BatchNorm1d(128) if use_bn else nn.Identity(),
            nn.ReLU(),

            nn.Linear(128, 64),
            nn.BatchNorm1d(64) if use_bn else nn.Identity(),
            nn.ReLU(),

            nn.Linear(64, 1),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.net(x)

# =====================================================
# 3. 训练 / 验证逻辑
# =====================================================
def train_one_epoch(model, loader, optimizer, criterion):
    model.train()
    total_loss = 0.0

    for x, y in loader:
        optimizer.zero_grad()
        y_pred = model(x)
        loss = criterion(y_pred, y)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

    return total_loss / len(loader)


def evaluate(model, loader, criterion):
    model.eval()
    total_loss = 0.0

    with torch.no_grad():
        for x, y in loader:
            y_pred = model(x)
            loss = criterion(y_pred, y)
            total_loss += loss.item()

    return total_loss / len(loader)

# =====================================================
# 4. 运行实验
# =====================================================
def run_experiment(use_bn, epochs=40):
    model = MLP(input_dim=20, use_bn=use_bn)
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    criterion = nn.BCELoss()

    train_losses, val_losses = [], []

    for epoch in range(epochs):
        train_loss = train_one_epoch(
            model, train_loader, optimizer, criterion
        )
        val_loss = evaluate(
            model, val_loader, criterion
        )

        train_losses.append(train_loss)
        val_losses.append(val_loss)

        print(
            f"[{'BatchNorm' if use_bn else 'No BN'}] "
            f"Epoch {epoch:02d} | "
            f"Train: {train_loss:.4f} | "
            f"Val: {val_loss:.4f}"
        )

    return train_losses, val_losses

# =====================================================
# 5. 对比运行
# =====================================================
print("==== Training WITHOUT Batch Normalization ====")
train_no_bn, val_no_bn = run_experiment(use_bn=False)

print("\n==== Training WITH Batch Normalization ====")
train_bn, val_bn = run_experiment(use_bn=True)

# =====================================================
# 6. 可视化
# =====================================================
epochs = range(1, len(train_bn) + 1)

plt.figure(figsize=(10, 6))

plt.plot(epochs, train_no_bn, '--', label="Train (No BN)")
plt.plot(epochs, val_no_bn, '--', label="Val (No BN)")

plt.plot(epochs, train_bn, label="Train (BN)")
plt.plot(epochs, val_bn, label="Val (BN)")

plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("Batch Normalization vs No Normalization")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()