DAY 43 训练和测试的规范写法

知识点回顾:

  1. 彩色和灰度图片测试和训练的规范写法:封装在函数中
  2. 展平操作:除第一个维度 batchsize 外全部展平
  3. dropout 操作:训练阶段随机丢弃神经元,测试阶段 eval模式关闭 dropout

零基础学 Python 机器学习:训练 / 测试的规范写法

先跟你说个大前提:咱们今天学的内容就像 “做饭的固定流程”—— 不管做番茄炒蛋(灰度图)还是青椒肉丝(彩色图),都有 “备料→烹饪→装盘” 的规范步骤,封装成函数就不用每次都从头想;展平是把 “叠好的菜板” 摊开,dropout 是 “训练时故意藏起几个厨具,测试时全拿出来用”。全程用 PyTorch(最常用的机器学习框架)举例,代码每行都讲透,保证你能懂。

前置小概念(先吃透,不然后面懵)

知识点 1:彩色 / 灰度图的训练 & 测试(封装成函数)

为什么要封装函数?

就像你把 “早上刷牙→洗脸→吃早饭” 写成一个 “晨起流程” 清单,每次执行就行,不用每次都想步骤。机器学习里训练 / 测试的步骤固定,封装成函数:

  1. 代码整洁,不容易漏步骤;
  2. 想换数据集(灰度→彩色),改几行就行;
  3. 后续调参、复现结果更方便。
函数的核心结构(通用模板)
训练函数(train_fn)测试函数(test_fn)
1. 模型切到训练模式(开启 dropout)1. 模型切到测试模式(关闭 dropout)
2. 遍历数据批次2. 遍历数据批次
3. 清空梯度(避免累计)3. 关闭梯度计算(节省内存)
4. 图片输入模型→得到预测结果4. 图片输入模型→得到预测结果
5. 计算损失(预测和真实值的差距)5. 计算损失 / 准确率
6. 反向传播(让模型学错在哪)6. 不更新参数(只评估)
7. 更新模型参数7. 返回测试损失 / 准确率
8. 返回训练损失 / 准确率-

知识点 2:展平操作(除 batchsize 外全展平)

什么是展平?

把多维的图片 “摊成一维的线”。比如:

  • 灰度图(10,1,28,28):除了 batchsize=10,剩下的1×28×28=784,展平后变成(10,784)
  • 彩色图(10,3,28,28):剩下的3×28×28=2352,展平后变成(10,2352)
为什么要展平?

简单的神经网络(全连接网络)只能 “吃” 一维向量,就像你把叠好的床单(二维)摊开成一条直线(一维),才能塞进收纳袋(网络)里。

展平的写法(关键!)

PyTorch 里用torch.flatten(数据, start_dim=1)

  • start_dim=1:从第 1 个维度开始展平(第 0 维是 batchsize,必须保留);
  • 千万别写成flatten()(默认从第 0 维展平,会把 batchsize 也拆了,比如(10,1,28,28)变成(7840,),完全错了!)。
举个栗子(展平实操)
import torch

# 模拟10张28×28的灰度图
gray_img = torch.randn(10, 1, 28, 28)  # 形状:(10,1,28,28)
# 模拟10张28×28的彩色图
color_img = torch.randn(10, 3, 28, 28)  # 形状:(10,3,28,28)

# 展平:从第1维开始
gray_flat = torch.flatten(gray_img, start_dim=1)
color_flat = torch.flatten(color_img, start_dim=1)

print("灰度图展平后形状:", gray_flat.shape)  # 输出:torch.Size([10, 784])
print("彩色图展平后形状:", color_flat.shape)  # 输出:torch.Size([10, 2352])

知识点 3:Dropout 操作(训练丢,测试关)

什么是 Dropout?

防止模型 “偷懒” 的技巧:训练时随机让一部分神经元 “闭嘴”(不参与计算),比如 100 个神经元随机丢 50 个,剩下的 50 个必须更努力学习,避免模型只依赖少数神经元(就像球队训练时故意不让主力上,让替补也练一练)。

测试时要把所有神经元都打开(主力全上),因为测试是要模型发挥全部能力,不是训练了。

Dropout 的核心规则
阶段模型模式Dropout 状态额外操作
训练model.train()开启(随机丢弃)计算梯度
测试model.eval()关闭(全部启用)torch.no_grad ()(关闭梯度,省内存)
举个栗子(Dropout 实操)
import torch
import torch.nn as nn

# 定义一个包含Dropout的简单模型
class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.dropout = nn.Dropout(p=0.5)  # 训练时丢弃50%的神经元

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

# 初始化模型
model = SimpleModel()
# 模拟一个输入(1个样本,10个神经元)
x = torch.ones(1, 10)  # 输入:[1,1,1,1,1,1,1,1,1,1]

# 训练模式:Dropout生效
model.train()
train_out = model(x)
print("训练模式输出(有神经元被丢弃):", train_out)
# 输出示例:[1., 0., 1., 0., 1., 1., 0., 1., 0., 1.](0就是被丢弃的)

# 测试模式:Dropout关闭
model.eval()
with torch.no_grad():  # 关闭梯度
    test_out = model(x)
print("测试模式输出(所有神经元都在):", test_out)
# 输出:[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]

整合所有知识点:完整示例代码(灰度 + 彩色图通用)

咱们用经典数据集做例子:

  • 灰度图:MNIST(手写数字,28×28 灰度)
  • 彩色图:CIFAR10(日常物品,32×32 彩色)
步骤 1:导入必备库
# 导入PyTorch核心库
import torch
import torch.nn as nn
import torch.optim as optim
# 导入数据集和数据加载工具
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# 设备选择:有GPU用GPU,没有用CPU(新手不用管,复制就行)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
步骤 2:准备数据集(灰度 + 彩色)
# 数据预处理:把图片转成Tensor(PyTorch能处理的格式)
transform = transforms.Compose([
    transforms.ToTensor(),  # 转Tensor,维度变成(通道,高,宽)
    transforms.Normalize((0.5,), (0.5,))  # 归一化(新手不用懂,复制就行)
])

# ========== 灰度图数据集(MNIST) ==========
train_gray_dataset = datasets.MNIST(
    root="./data",  # 数据保存路径
    train=True,     # 训练集
    download=True,  # 自动下载
    transform=transform
)
test_gray_dataset = datasets.MNIST(
    root="./data",
    train=False,    # 测试集
    download=True,
    transform=transform
)

# ========== 彩色图数据集(CIFAR10) ==========
train_color_dataset = datasets.CIFAR10(
    root="./data",
    train=True,
    download=True,
    transform=transform
)
test_color_dataset = datasets.CIFAR10(
    root="./data",
    train=False,
    download=True,
    transform=transform
)

# 数据加载器(按批次取数据)
batch_size = 64  # 一次处理64张图片
train_gray_loader = DataLoader(train_gray_dataset, batch_size=batch_size, shuffle=True)
test_gray_loader = DataLoader(test_gray_dataset, batch_size=batch_size, shuffle=False)
train_color_loader = DataLoader(train_color_dataset, batch_size=batch_size, shuffle=True)
test_color_loader = DataLoader(test_color_dataset, batch_size=batch_size, shuffle=False)
步骤 3:定义模型(包含展平 + Dropout)
class ImageModel(nn.Module):
    def __init__(self, input_dim):  # input_dim:展平后的维度(灰度=784,彩色=3072)
        super().__init__()
        self.fc1 = nn.Linear(input_dim, 128)  # 全连接层1
        self.dropout = nn.Dropout(0.5)        # Dropout层(丢弃50%)
        self.fc2 = nn.Linear(128, 10)         # 全连接层2(10分类:MNIST是0-9,CIFAR10是10类物品)

    def forward(self, x):
        # 步骤1:展平(除batchsize外全展平)
        x = torch.flatten(x, start_dim=1)
        # 步骤2:过全连接层+激活函数
        x = torch.relu(self.fc1(x))
        # 步骤3:Dropout(训练生效,测试关闭)
        x = self.dropout(x)
        # 步骤4:输出结果
        x = self.fc2(x)
        return x

# 初始化模型:灰度图input_dim=28×28×1=784;彩色图input_dim=32×32×3=3072
gray_model = ImageModel(input_dim=784).to(device)
color_model = ImageModel(input_dim=3072).to(device)
步骤 4:定义训练 / 测试函数(核心!)
# 训练函数(封装)
def train_model(model, train_loader, optimizer, criterion, epoch):
    model.train()  # 切换到训练模式(开启Dropout)
    total_loss = 0.0
    correct = 0
    total = 0

    for batch_idx, (data, target) in enumerate(train_loader):
        # 把数据放到GPU/CPU上
        data, target = data.to(device), target.to(device)
        
        # 1. 清空梯度(必须!不然梯度会累计)
        optimizer.zero_grad()
        # 2. 前向传播:输入图片→得到预测结果
        output = model(data)
        # 3. 计算损失(预测值和真实值的差距)
        loss = criterion(output, target)
        # 4. 反向传播:让模型知道错在哪
        loss.backward()
        # 5. 更新模型参数:模型学习改正
        optimizer.step()

        # 统计损失和准确率
        total_loss += loss.item()
        _, predicted = torch.max(output.data, 1)  # 取预测概率最大的类别
        total += target.size(0)
        correct += (predicted == target).sum().item()

        # 每100批次打印一次进度
        if batch_idx % 100 == 0:
            print(f'Epoch {epoch} | Batch {batch_idx} | Loss: {loss.item():.3f} | Acc: {100*correct/total:.2f}%')

    # 返回本轮训练的平均损失和准确率
    avg_loss = total_loss / len(train_loader)
    avg_acc = 100 * correct / total
    return avg_loss, avg_acc

# 测试函数(封装)
def test_model(model, test_loader, criterion):
    model.eval()  # 切换到测试模式(关闭Dropout)
    total_loss = 0.0
    correct = 0
    total = 0

    # 关闭梯度计算(测试不需要更新参数,省内存)
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            # 前向传播
            output = model(data)
            # 计算损失
            loss = criterion(output, target)
            # 统计
            total_loss += loss.item()
            _, predicted = torch.max(output.data, 1)
            total += target.size(0)
            correct += (predicted == target).sum().item()

    # 返回测试的平均损失和准确率
    avg_loss = total_loss / len(test_loader)
    avg_acc = 100 * correct / total
    print(f'Test Loss: {avg_loss:.3f} | Test Acc: {avg_acc:.2f}%')
    return avg_loss, avg_acc
步骤 5:运行训练和测试(灰度图为例,彩色图只需换加载器)
# 定义损失函数和优化器(通用)
criterion = nn.CrossEntropyLoss()  # 分类任务的损失函数
optimizer = optim.SGD(gray_model.parameters(), lr=0.01, momentum=0.9)  # 优化器

# 训练3轮(新手不用多,看效果就行)
epochs = 3
for epoch in range(1, epochs+1):
    print(f"\n===== 训练轮数 {epoch} =====")
    # 训练
    train_loss, train_acc = train_model(gray_model, train_gray_loader, optimizer, criterion, epoch)
    # 测试
    test_loss, test_acc = test_model(gray_model, test_gray_loader, criterion)

# 如果要跑彩色图,只需要替换模型和加载器:
# optimizer_color = optim.SGD(color_model.parameters(), lr=0.01, momentum=0.9)
# for epoch in range(1, epochs+1):
#     train_model(color_model, train_color_loader, optimizer_color, criterion, epoch)
#     test_model(color_model, test_color_loader, criterion)
关键易错点总结(新手必看)
  1. 展平别写错维度:必须torch.flatten(x, start_dim=1),别漏start_dim=1
  2. Dropout 别忘切模式:训练用model.train(),测试用model.eval(),否则测试结果会错;
  3. 测试别忘关梯度with torch.no_grad()一定要加,不然内存会爆;
  4. 训练前清空梯度optimizer.zero_grad()必须在每次批次前执行,不然梯度会累计;
  5. 彩色 / 灰度图只改输入维度:模型不用大改,只需要改input_dim(灰度 784,彩色 3072)。

最终效果

运行代码后,你会看到:

  • 训练时 Loss(损失)慢慢下降,Acc(准确率)慢慢上升;
  • 测试时 Acc 能达到 90% 左右(MNIST 很简单);
  • 如果换成彩色图(CIFAR10),准确率会低一些(因为更复杂),但流程完全一样。

现在你可以把代码复制到 PyCharm/Colab 里运行,改一改 batchsize、epochs,看看效果变化,慢慢就理解了。

用通俗易懂的语言解释一下什么是Dropout?

咱们用 “学霸复习考试” 的生活例子,把 Dropout 讲得明明白白,全程不用复杂术语:

先想个问题:为什么需要 Dropout?

假设你是个学霸,要备考数学(对应模型要 “学习识别图片”)。如果复习时,你只依赖 3 个 “王牌知识点”(比如只背公式 A、公式 B、公式 C),考试时题目稍微变一下(比如公式 A 换了个场景用),你就懵了 —— 这就是模型的 “过拟合”(死记硬背,不会灵活运用)。

Dropout 的核心作用就是:逼你不能只依赖少数 “王牌知识点”,必须让所有知识点都学会,这样考试时再怎么变题都不怕

什么是 Dropout?(通俗版)

把模型里的每个 “神经元”(可以理解成 “知识点” 或 “学习小组成员”),想象成一个 “学霸的复习帮手”。

  • 训练阶段(复习时):Dropout 会随机让一部分 “帮手放假”(比如 50% 的帮手不干活)。

比如你本来有 10 个复习帮手(对应 10 个神经元),Dropout 每次随机挑 5 个让他们休息,剩下的 5 个必须一起完成复习任务。

这样一来,你没法只靠那 3 个 “王牌帮手”(比如只靠公式 A、B、C 的帮手),必须让每个帮手都学扎实 —— 哪怕下次王牌帮手放假,其他帮手也能顶上去。

  • 测试阶段(考试时):Dropout 让所有 “帮手都上班”(不放假了)。

考试是要拿满分的,不能再让帮手放假了,得让所有知识点都发挥作用,用全部实力答题。

再举个更具体的例子:

比如模型要识别 “猫” 的图片,训练时:

  • 没有 Dropout:模型可能只依赖 “猫有尖耳朵”“猫有尾巴” 这两个特征(两个神经元),其他特征(比如猫的眼睛、毛发)都不学;
  • 有 Dropout(p=0.5):训练时随机让 “尖耳朵” 或 “尾巴” 的神经元放假,模型被迫去学 “眼睛形状”“毛发纹理” 这些其他特征 —— 久而久之,模型记住了 “猫的所有关键特征”,哪怕遇到一只耳朵被挡住的猫(对应某特征没出现),也能认出来。

总结 Dropout 的本质:

就是训练时 “故意制造困难”,让模型不能 “偷懒只靠少数神经元”,必须让所有神经元都参与学习,从而变得更 “灵活”,避免考试时(测试时)因为 “某几个关键神经元没发挥作用” 而出错。

对应到代码里的逻辑:

  • model.train():告诉模型 “现在是复习时间,Dropout 开始随机让帮手放假”;
  • model.eval():告诉模型 “现在是考试时间,所有帮手都回来上班,不许放假”;
  • nn.Dropout(p=0.5):规定 “复习时每次让 50% 的帮手放假”(p 是放假比例)。

一句话记住:Dropout 就是 “训练时故意藏起一半工具,逼模型学会用所有工具;测试时把所有工具都拿出来,发挥全部实力”

dropout 操作的代码示例

为了让你彻底吃透 Dropout 操作,我专门整理了 极简入门版 和 实战结合版 两个代码示例,所有代码可直接复制运行,每一行都加了通俗注释,重点对比「训练模式(Dropout 生效)」和「测试模式(Dropout 关闭)」的差异。

前置准备:导入必备库

所有示例都需要先运行这行代码(就像做饭先拿厨具):

import torch
import torch.nn as nn  # 神经网络核心库,包含Dropout

示例 1:极简版 Dropout 演示(直观看效果)

这个示例只聚焦 Dropout 本身,不涉及复杂网络,能一眼看出训练 / 测试模式的区别:

# 1. 定义一个只有Dropout层的简单模块
# p=0.5 表示训练时随机丢弃50%的神经元
dropout_layer = nn.Dropout(p=0.5)  

# 2. 模拟输入:1个样本(batchsize=1),包含10个神经元的输出(比如网络某一层的结果)
# 用全1的张量,方便观察哪些神经元被丢弃(丢弃后值为0)
input_data = torch.ones(1, 10)  
print("原始输入:", input_data)
# 输出:tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])

# ==================== 训练模式:Dropout生效 ====================
dropout_layer.train()  # 切换到训练模式(必须!否则Dropout不生效)
train_output = dropout_layer(input_data)
print("\n训练模式输出(50%神经元被丢弃,值为0):", train_output)
# 示例输出(每次运行结果不同,因为随机丢弃):
# tensor([[2., 0., 2., 0., 2., 2., 0., 2., 0., 2.]])
# 🔔 注意:被保留的神经元值会乘以 1/(1-p)(这里是2),是PyTorch的默认补偿机制,不用管,只看0即可

# ==================== 测试模式:Dropout关闭 ====================
dropout_layer.eval()  # 切换到测试模式(必须!关闭Dropout)
# with torch.no_grad(): 测试时关闭梯度(非必须,但能省内存,建议加)
with torch.no_grad():
    test_output = dropout_layer(input_data)
print("\n测试模式输出(所有神经元保留,无0值):", test_output)
# 输出:tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])

关键说明:

  • nn.Dropout(p=0.5)p是丢弃概率,比如p=0.3就是丢弃 30% 的神经元;
  • train():告诉 Dropout 层 “现在是训练阶段,要随机丢弃”;
  • eval():告诉 Dropout 层 “现在是测试阶段,不要丢弃任何神经元”;
  • torch.no_grad():测试时不需要计算梯度(梯度是训练时更新参数用的),加了能节省内存、加快速度。

示例 2:实战版 Dropout(结合神经网络 + 训练 / 测试流程)

这个示例模拟实际机器学习场景,把 Dropout 融入神经网络,完整展示训练和测试时的用法:

# 1. 定义一个包含Dropout的完整神经网络(手写数字识别为例)
class MNISTNet(nn.Module):
    def __init__(self):
        super().__init__()
        # 第一层:全连接层(输入是展平后的28×28灰度图=784维)
        self.fc1 = nn.Linear(784, 256)  
        # Dropout层(训练丢弃50%)
        self.dropout = nn.Dropout(p=0.5)  
        # 第二层:全连接层(输出10类:0-9)
        self.fc2 = nn.Linear(256, 10)  

    # 前向传播(数据走网络的路径)
    def forward(self, x):
        # 展平:把(批量数,1,28,28)变成(批量数,784)
        x = torch.flatten(x, start_dim=1)  
        # 激活函数(让网络能学复杂规律)
        x = torch.relu(self.fc1(x))  
        # Dropout(训练生效,测试关闭)
        x = self.dropout(x)  
        # 输出最终结果
        x = self.fc2(x)  
        return x

# 2. 初始化模型、损失函数、优化器(训练必备)
model = MNISTNet()
# 分类任务的损失函数
criterion = nn.CrossEntropyLoss()  
# 优化器(更新模型参数)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)  

# 3. 模拟训练过程(1个批次为例)
print("===== 训练阶段 =====")
model.train()  # 全局切换到训练模式(所有Dropout层生效)
# 模拟输入:批量数=8,1通道,28×28的灰度图
train_x = torch.randn(8, 1, 28, 28)  
# 模拟真实标签(8个样本,标签是0-9的整数)
train_y = torch.tensor([0,1,2,3,4,5,6,7])  

# 训练步骤(固定流程):
optimizer.zero_grad()  # 清空梯度(避免累计)
train_pred = model(train_x)  # 模型预测
loss = criterion(train_pred, train_y)  # 计算损失
loss.backward()  # 反向传播(算梯度)
optimizer.step()  # 更新参数(学知识)
print(f"训练损失:{loss.item():.4f}")

# 4. 模拟测试过程
print("\n===== 测试阶段 =====")
model.eval()  # 全局切换到测试模式(所有Dropout层关闭)
with torch.no_grad():  # 关闭梯度计算
    # 模拟测试输入(批量数=4)
    test_x = torch.randn(4, 1, 28, 28)  
    # 模拟测试标签
    test_y = torch.tensor([0,1,2,3])  
    # 模型预测
    test_pred = model(test_x)  
    # 计算测试损失(只评估,不更新参数)
    test_loss = criterion(test_pred, test_y)  
    print(f"测试损失:{test_loss.item():.4f}")

# 验证Dropout是否真的关闭:对比两次测试的输出(如果Dropout开着,结果会随机;关着则完全一样)
with torch.no_grad():
    test_pred1 = model(test_x)
    test_pred2 = model(test_x)
print("\n两次测试输出是否完全一致?", torch.equal(test_pred1, test_pred2))
# 输出:True(因为Dropout关闭,没有随机丢弃,结果固定)

实战关键要点:

  1. model.train()/model.eval():是 “全局开关”,会影响模型中所有的 Dropout 层(不用给每个 Dropout 单独设置);
  2. 训练流程固定 4 步:清空梯度→预测→算损失→反向传播+更新参数
  3. 测试流程固定:切eval模式→关梯度→预测→算损失/准确率
  4. 测试时两次预测结果一致,证明 Dropout 确实关闭了(如果没切 eval,结果会随机)。

常见错误提醒(新手必避坑)

  1. 忘记切模式:训练时没写model.train(),或测试时没写model.eval(),导致 Dropout 效果错误;
  2. 测试时没关梯度:虽然不影响结果,但会浪费内存,尤其是大数据量时;
  3. Dropout 加错位置:不要加在输入层或输出层,一般加在中间的全连接层之间;
  4. p 值设置过大:比如p=0.8(丢弃 80%),模型学不到足够特征,效果会变差,一般设 0.3-0.5。

你可以把这些代码复制到 Python 环境(比如 PyCharm、Jupyter)里运行,改一改p值、训练 / 测试模式,看看结果变化,就能彻底理解 Dropout 的用法了。

如何选择合适的Dropout概率p?

选择 Dropout 的概率p(训练时丢弃神经元的比例),核心原则是:既不让模型 “丢太多神经元学不到东西(欠拟合)”,也不让 “丢太少没起到防过拟合作用”。咱们用「学霸复习」的例子 +「实操步骤」讲透,新手能直接照做。

先明确:p的本质是什么?

p是训练时随机 “放假” 的神经元比例:

  • p=0:不丢弃任何神经元(相当于没用 Dropout);
  • p=0.5:50% 神经元放假;
  • p=1:所有神经元都放假(模型学不到任何东西)。

核心目标:让p刚好能 “逼模型学通用规律”,又不 “剥夺模型学习的能力”。


第一步:先记「通用经验值」(新手直接用,不用瞎试)

不同网络层的p有默认最优范围,这是行业总结的 “捷径”:

网络层类型推荐p范围原因(通俗解释)
全连接层(FC)0.3 ~ 0.5全连接层参数多、容易过拟合,丢 30%-50% 刚好
卷积层(CNN)0.1 ~ 0.3卷积层学的是局部特征(比如猫的耳朵),丢多了学不到核心特征
循环层(RNN/LSTM)0.2 ~ 0.4文本 / 时序数据依赖上下文,丢多了会破坏语义
输入层 / 输出层0(不用)输入层丢了会破坏原始数据,输出层丢了会影响预测结果
新手首选:
  • 做图片分类 / 简单任务(比如 MNIST 手写数字):全连接层直接设p=0.5(最常用的默认值);
  • 做复杂任务(比如 CIFAR10 彩色图):全连接层p=0.4,卷积层p=0.2

第二步:根据「训练 / 测试表现」微调p(核心调参方法)

经验值只是起点,最终要靠 “训练集 + 验证集(或测试集)的表现” 判断p是否合适。

先看 3 种典型现象,对应调整策略:

现象(核心看 “训练准确率” 和 “测试准确率” 的差距)说明调整p的方法
训练准确率很高(95%+),测试准确率很低(70%)过拟合(丢少了)适当增大p(比如从 0.5→0.6)
训练准确率低(70%),测试准确率也低(68%)欠拟合(丢多了)减小p(比如从 0.5→0.3),甚至设p=0
训练 / 测试准确率都不错(差距 < 5%)刚好合适保持当前p不动
生活化例子:

就像学霸复习时藏笔记:

  • 藏太少(p=0.1):学霸还是能靠少数笔记记题(过拟合);
  • 藏太多(p=0.8):学霸连核心知识点都看不到(欠拟合);
  • 藏一半(p=0.5):学霸必须记住所有核心知识点,才能答题(刚好)。
实操调参步骤(示例):

比如你做 MNIST 手写数字识别,模型是全连接层:

  1. 先设p=0.5,训练后:训练准确率 98%,测试准确率 90%(过拟合);
  2. 调大p到 0.6,训练后:训练准确率 96%,测试准确率 92%(差距缩小,合适);
  3. 再试p=0.7,训练后:训练准确率 90%,测试准确率 89%(欠拟合)→ 最终选p=0.6

第三步:结合「数据 / 模型情况」调整p

p的选择还要适配你的数据量和模型复杂度,这是容易被忽略的点:

1. 数据量越少 → p可以稍大
  • 比如只有 100 张训练图片(数据极少):全连接层p=0.5~0.6,逼模型学通用规律;
  • 比如有 10 万张训练图片(数据充足):p=0.2~0.3即可,甚至不用 Dropout(数据多本身就不容易过拟合)。
2. 模型越复杂 → p可以稍大
  • 比如模型有 10 层全连接层(超复杂):p=0.5~0.6
  • 比如模型只有 2 层全连接层(简单):p=0.3~0.4,甚至p=0
3. 不同层可以设不同p(进阶技巧)

不用所有层都设一样的p

  • 靠近输入的层(比如第一层全连接):p=0.3(少丢,保留原始特征);
  • 靠近输出的层(比如最后一层全连接):p=0.5(多丢,防过拟合)。

第四步:避坑要点(新手别踩)

  1. 输出层绝对别加 Dropout:输出层是模型的 “最终答案”,丢神经元会直接导致预测结果出错;
  2. 别盲目追求 “大 p”:不是 p 越大防过拟合效果越好,超过 0.7 大概率会欠拟合;
  3. 测试时不用管 p:只要切换到model.eval(),Dropout 会自动关闭,p 的值不影响测试;
  4. 结合其他防过拟合方法时,p 要减小:比如同时用了 “权重衰减 + 数据增强”,p 可以从 0.5 降到 0.3(多重防护,避免过度限制模型)。

总结:选择p的极简流程(新手照做)

1. 先按层类型选经验值:全连接层 0.5,卷积层 0.2;

2. 训练后看训练 / 测试准确率差距:

  • 过拟合→增大 p(每次 + 0.1);
  • 欠拟合→减小 p(每次 - 0.1);

3. 结合数据量 / 模型复杂度微调:数据少 / 模型复杂→p 稍大,反之稍小;

4. 最终选 “测试准确率最高” 的 p(不是训练准确率)。

代码示例(不同 p 的对比测试)

import torch
import torch.nn as nn

# 定义不同p的模型
def build_model(p):
    class Net(nn.Module):
        def __init__(self):
            super().__init__()
            self.fc1 = nn.Linear(784, 256)
            self.dropout = nn.Dropout(p=p)  # 不同p值
            self.fc2 = nn.Linear(256, 10)
        def forward(self, x):
            x = torch.flatten(x, 1)
            x = torch.relu(self.fc1(x))
            x = self.dropout(x)
            return self.fc2(x)
    return Net()

# 测试不同p的效果(伪代码)
p_list = [0.3, 0.5, 0.7]
best_p = 0
best_test_acc = 0

for p in p_list:
    model = build_model(p)
    # 训练模型(省略训练代码)
    train_model(model)
    # 测试模型
    test_acc = test_model(model)
    print(f"p={p},测试准确率:{test_acc}")
    if test_acc > best_test_acc:
        best_test_acc = test_acc
        best_p = p

print(f"最优p值:{best_p},对应测试准确率:{best_test_acc}")

按这个流程,新手不用死记硬背,试 2-3 次就能找到合适的p,核心是 “看效果调参”,而不是凭感觉。

除了dropout,还有哪些防止过拟合的方法?

咱们还是用「学霸备考」的生活化例子,把除 Dropout 外最常用、最好理解的7 种防止过拟合方法讲透 —— 核心逻辑都是:不让模型 “死记硬背训练题(训练数据)”,而是 “理解解题思路(通用规律)”,这样遇到新题(测试数据)也能做对。

先复习下 “过拟合” 的本质:模型把训练数据里的 “无关细节” 也当成了规律(比如背题时记住了题目在卷子的第 3 行,而不是题目本身的解法),导致训练时分数很高,测试时分数暴跌。


方法 1:数据增强(Data Augmentation)—— 给训练题加 “花样”

通俗解释

就像学霸复习时,不仅看原题,还看 “换了字体的题、排版歪了的题、稍微改了数字的题”,逼自己理解核心解法,而不是记题目外观。

对应到机器学习:给训练图片 “做手脚”(但不改变核心特征),让模型见更多 “变种”,比如:

  • 图片类:随机旋转(比如把猫的图片转 10 度)、随机裁剪(裁掉一点边缘)、随机翻转(左右翻)、调整亮度 / 对比度;
  • 文本类:同义词替换(“开心” 换成 “高兴”)、随机插入 / 删除少量文字。
生活例子

学认 “猫”:不仅看正面、正脸的猫,还看侧着的猫、歪头的猫、光线暗的猫、只露半张脸的猫 —— 哪怕考试遇到一只趴着的猫,也能认出来。

实操示例(图片增强,PyTorch 代码)
from torchvision import transforms

# 定义增强规则:随机翻转+随机旋转+转Tensor
train_transform = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),  # 50%概率左右翻转
    transforms.RandomRotation(10),           # 随机旋转±10度
    transforms.ToTensor(),                   # 转成模型能认的格式
])
# 测试时不增强(用原图考模型)
test_transform = transforms.Compose([
    transforms.ToTensor(),
])
核心要点
  • 只给训练数据做增强,测试数据必须用原图(不然相当于改了考试题,测不准);
  • 增强的 “幅度” 要合理:比如猫的图片旋转 180 度就变成倒猫了,反而误导模型。

方法 2:早停(Early Stopping)—— 学到 “刚好会” 就停,别学傻了

通俗解释

学霸背单词:背到 “能默写出 80%” 就停,再背下去就开始记 “单词在笔记本的第 5 行” 这种无关细节,反而考试遇到陌生排版的单词就不会了。对应到机器学习:训练时盯着 “测试集分数”,如果测试分数连续好几轮不涨、甚至开始跌,就立刻停止训练 —— 哪怕训练集分数还在涨。

生活例子

玩游戏练操作:练到 “能稳定通关普通难度” 就停,再死磕 “无伤通关”,反而会养成 “只适应某一种怪的走位” 的坏习惯,换个难度就翻车。

实操逻辑(伪代码,一看就懂)
best_test_acc = 0  # 记录最好的测试准确率
patience = 5       # 连续5轮没进步就停
no_improve = 0     # 统计连续没进步的轮数

for epoch in range(100):  # 计划训练100轮
    # 训练模型(省略训练代码)
    train_model()
    # 测试模型
    current_test_acc = test_model()
    
    if current_test_acc > best_test_acc:
        best_test_acc = current_test_acc
        no_improve = 0  # 有进步,重置计数
        save_model()    # 保存最好的模型
    else:
        no_improve += 1
        if no_improve >= patience:
            print("测试分数不涨了,停止训练!")
            break  # 提前终止训练
核心要点
  • 关键是 “看测试集表现”,不是训练集;
  • 要保存 “测试分数最好时” 的模型,而不是最后一轮的模型。

方法 3:权重衰减(Weight Decay)—— 不让模型 “钻牛角尖”

通俗解释

学霸做题时,老师规定 “不许用太极端的解法”(比如不许用超纲的偏门公式),逼他用通用解法 —— 这样换个题目,通用解法依然能用。对应到机器学习:模型的 “参数(权重)” 相当于 “解题公式的系数”,权重衰减会惩罚 “太大的参数”,让参数都保持在较小的范围,避免模型依赖 “极端系数” 拟合训练数据的噪声。

生活例子

做菜时,不许放 “10 勺盐” 这种极端调料(不然只适合某一种食材),要求用 “1 勺盐” 这种温和的量 —— 这样做青菜、做豆腐都适用。

实操示例(PyTorch 代码)
import torch.optim as optim

# 优化器里加weight_decay就是权重衰减(L2正则的一种)
optimizer = optim.SGD(
    model.parameters(),
    lr=0.01,          # 学习率
    weight_decay=1e-4 # 权重衰减系数(越小越温和,常用1e-4~1e-5)
)
核心要点
  • 权重衰减是L2 正则的简化写法,是最常用的正则化方式;
  • 系数别设太大:比如设成 1,会把参数压得太小,模型学不到任何规律(欠拟合)。

方法 4:L1/L2 正则化 —— 让模型 “轻装上阵”

通俗解释
  • L1 正则:逼模型 “少用解题公式”(只留核心公式),比如 10 个公式里只留 3 个最关键的,其他都清零 —— 相当于学霸只记核心知识点,不记杂七杂八的细节;
  • L2 正则:就是上面的 “权重衰减”,逼模型 “解题公式的系数别太大”,比如系数都控制在 ±1 以内,避免极端解法。
生活例子
  • L1:整理书包,只留笔、本子、尺子(核心工具),把贴纸、玩具都扔掉;
  • L2:用尺子时,不许把刻度拉到最极限(比如只用到 0-10cm,不用 10-20cm 的极端刻度)。
实操示例(PyTorch 实现 L1 正则)

PyTorch 没有直接的 L1 参数,需要手动加损失:

# 计算普通损失
loss = criterion(output, target)

# 手动加L1正则损失:把所有参数的绝对值加起来,乘以系数
l1_lambda = 1e-5
l1_loss = 0
for param in model.parameters():
    l1_loss += torch.sum(torch.abs(param))  # 所有参数的绝对值之和
total_loss = loss + l1_lambda * l1_loss    # 总损失=普通损失+L1惩罚

方法 5:简化模型结构 —— 别用 “太复杂的脑子” 记细节

通俗解释

学霸用 “简单笔记本” 记知识点(只写核心),而不是 “厚本子记满所有细节”—— 复杂本子容易记无关信息,简单本子只能记关键规律。对应到机器学习:如果模型太复杂(比如有 100 层神经网络、10 万个参数),哪怕是简单的任务,也会 “硬记” 训练数据的细节;把模型改简单(比如减少层数、减少神经元数量),模型只能学核心规律。

生活例子

用计算器算加减法:普通计算器(简单模型)只会算核心规则,不会记 “上次算的是 2+3 还是 5+6”;超级计算机(复杂模型)反而会记住这些无关细节。

实操示例
# 复杂模型(容易过拟合)
class ComplexModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 1024)  # 1024个神经元
        self.fc2 = nn.Linear(1024, 512)  # 512个神经元
        self.fc3 = nn.Linear(512, 10)

# 简化模型(避免过拟合)
class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 128)   # 减少到128个神经元
        self.fc2 = nn.Linear(128, 10)    # 去掉多余的层
核心要点
  • 模型复杂度要匹配任务难度:识别手写数字用简单模型就行,识别复杂场景用稍复杂的模型;
  • 先从简单模型开始试,不够用再慢慢加复杂度。

方法 6:扩充数据集 —— 让模型见更多 “不同的题”

通俗解释

学霸刷 1000 道不同的数学题,比刷 100 道重复的题学得更透彻 —— 见的题型多了,自然能总结通用规律,而不是记某几道题的答案。对应到机器学习:训练数据越多,模型越难 “记完所有数据”,只能被迫学规律;如果数据少,模型很容易把所有数据都记下来(过拟合)。

生活例子

学认 “苹果”:看 100 个不同品种(红富士、青苹果、冰糖心)、不同大小、不同角度的苹果,比只看 10 个红富士苹果,更能认得出所有苹果。

实操思路
  1. 手动收集更多数据(比如多找图片、多爬取文本);
  2. 用 “公开数据集” 补充(比如 MNIST、CIFAR10);
  3. 用 “数据合成”(比如 AI 生成相似的图片 / 文本)。

方法 7:交叉验证(Cross Validation)—— 选 “真学会” 的模型,不是 “记题” 模型

通俗解释

学霸考完模拟考 1,又考模拟考 2、模拟考 3,不是只看一次模拟考的成绩 —— 如果每次模拟考都考得好,说明真学会了;如果只有一次考得好,可能是刚好记了那套题。对应到机器学习:把训练数据分成几份(比如 5 份),用 4 份训练、1 份验证,轮流换着验证,最后选 “平均验证分数最高” 的模型,避免选到 “刚好记了某一份数据” 的模型。

生活例子

面试时,面试官出 3 套不同的题,都答得好才录用 —— 如果只答好 1 套,可能是提前背了这道题。

实操示例(5 折交叉验证,伪代码)
from sklearn.model_selection import KFold

# 把数据分成5份
kf = KFold(n_splits=5, shuffle=True)
fold_acc = []

for fold, (train_idx, val_idx) in enumerate(kf.split(all_data)):
    # 取当前折的训练/验证数据
    train_data = all_data[train_idx]
    val_data = all_data[val_idx]
    
    # 训练模型、验证模型
    train_model(train_data)
    val_acc = test_model(val_data)
    fold_acc.append(val_acc)

# 选平均准确率最高的模型
avg_acc = sum(fold_acc)/5
print(f"5折交叉验证平均准确率:{avg_acc}")

总结:防止过拟合的核心思路(一句话记)

所有方法都是为了:要么让模型 “学不到无关细节”(简化模型、权重衰减),要么让模型 “见更多样的内容”(数据增强、扩充数据集),要么 “及时止损”(早停),要么 “选对模型”(交叉验证)

新手怎么选?

  1. 先试数据增强 + 早停(最通用,几乎所有任务都能用);
  2. 效果还不好,加权重衰减(代码改一行就行);
  3. 再不好,简化模型结构或扩充数据集;
  4. Dropout 可以和上面的方法叠加用(比如 Dropout + 数据增强,效果更好)。

权重衰减的原理是什么?

咱们还是用「生活化例子 + 极简数学」的方式讲透权重衰减(Weight Decay)的原理 —— 核心就一句话:通过 “惩罚模型中过大的参数(权重)”,逼模型用 “均衡、温和的规则” 学规律,而不是 “极端规则” 记细节

先补两个前置小概念(不用记公式,懂意思就行):

  • 权重(Weight):模型里的 “参数”,可以理解成「模型给每个特征的 “关注度打分”」。比如识别猫时,“尖耳朵” 特征的权重是 5,“尾巴” 特征的权重是 3,说明模型更关注尖耳朵;
  • 损失函数:模型的 “错题本”,数值越小说明模型预测得越准。训练的目标就是让这个数值尽可能小。

第一步:先搞懂「为什么权重太大会导致过拟合?」

用「做菜」的例子最直观:假设你学做 “番茄炒蛋”(对应模型学识别猫),训练数据是 “妈妈做的番茄炒蛋”(对应训练图片):

  • 妈妈做的番茄炒蛋:盐放 1 勺,糖放 0.5 勺(对应训练数据的 “正常特征”);
  • 你记成:盐放 10 勺,糖放 0.1 勺(对应模型给 “盐味” 特征的权重过大,“甜味” 权重过小);
  • 结果:你做的番茄炒蛋只适合妈妈的配方(训练数据),换个番茄品种(测试数据),放 10 勺盐就齁了(模型识别不出变异的猫)—— 这就是「权重过大导致过拟合」。

核心问题:权重太大,说明模型过度依赖某几个 “极端特征”,而不是 “通用特征”。比如模型给 “猫的尖耳朵” 权重 100,哪怕图片里只有一个尖耳朵的污点,模型也会认成猫;而正常的猫应该是 “耳朵 + 眼睛 + 尾巴” 等特征的综合判断。


第二步:权重衰减的核心原理 —— 给 “大权重” 加 “罚款”

权重衰减的本质是:在模型的 “错题本(损失函数)” 里,加一笔 “罚款”—— 权重越大,罚款越重。模型为了减少总损失(错题 + 罚款),会主动把权重压小,不敢过度依赖某几个特征。

1. 公式拆解(极简版,不用算,看逻辑)

原本的损失函数(只看预测对错):损失 = 模型预测值和真实值的差距(比如分类任务的交叉熵损失)

加了权重衰减后的总损失:总损失 = 预测差距损失 + 衰减系数 × 所有权重的平方和

关键细节:
  • 「权重的平方和」:对大权重的惩罚是 “加倍的”!比如权重 = 10,平方 = 100;权重 = 1,平方 = 1——10 倍的权重,罚款差 100 倍,逼模型不敢把权重搞太大;
  • 「衰减系数(比如 1e-4)」:控制罚款的 “力度”,系数越小,罚款越轻;系数越大,罚款越重(新手常用 1e-4~1e-5)。
2. 生活化例子(对应公式)

还是做菜的例子:

  • 原本的损失:你做的菜和妈妈的菜的味道差距(比如差 5 分);
  • 权重衰减的罚款:0.0001 ×(盐的用量 ² + 糖的用量 ²);
    • 如果你放 10 勺盐:罚款 = 0.0001×(10²+0.1²)=0.010001,总损失 = 5+0.010001=5.010001;
    • 如果你放 1 勺盐:罚款 = 0.0001×(1²+0.5²)=0.000125,总损失 = 5+0.000125=5.000125;
  • 模型(你)会选总损失更小的方案 —— 主动把盐的用量从 10 勺降到 1 勺,权重(调料用量)自然变小。
3. 训练时的实际效果(梯度下降视角)

模型训练的核心是 “梯度下降”:每次让权重往 “减少损失” 的方向挪一小步。

加了权重衰减后,权重的更新公式会变成:新权重 = 旧权重 - 学习率×(预测误差的梯度 + 衰减系数×旧权重)

拆解一下:

  • 没有权重衰减:权重只跟着 “预测误差” 走,可能越走越大;
  • 有权重衰减:多了一项「- 学习率 × 衰减系数 × 旧权重」—— 相当于每次更新时,都把权重往 0 的方向 “拉一把”,不让它变大。

比如旧权重 = 10,学习率 = 0.01,衰减系数 = 1e-4:

  • 这一项的影响是:-0.01×1e-4×10 = -0.00001;
  • 新权重会比原来小一点点,久而久之,所有权重都会被控制在较小范围。

第三步:为什么是 “平方和”(L2 正则),而不是 “绝对值和”(L1)?

权重衰减本质是「L2 正则化」的简化写法,之所以用平方和而不是绝对值和:

  • 平方和(L2):对 “超大权重” 惩罚狠,对 “小权重” 惩罚轻(温和)。比如权重 = 10 罚 100,权重 = 1 罚 1—— 既压大权重,又不把小权重直接清零,模型还能学特征;
  • 绝对值和(L1):对所有权重惩罚一样狠(比如权重 = 10 罚 10,权重 = 1 罚 1),容易把很多小权重直接压到 0,模型学不到足够特征(欠拟合)。

用例子理解:

  • L2(权重衰减):让模型 “雨露均沾”,每个特征都关注一点(比如耳朵权重 1,眼睛权重 0.8,尾巴权重 0.7);
  • L1:让模型 “只挑几个特征”(比如只留耳朵权重 1,眼睛和尾巴权重 0)。

新手记住:权重衰减(L2)更通用,几乎所有场景都能用;L1 适合需要 “精简特征” 的场景(比如选最重要的特征)


第四步:权重衰减的代码对应(看原理如何落地)

之前的代码里,权重衰减只需要在优化器里加一个参数,现在结合原理看:

import torch.optim as optim

# 权重衰减系数weight_decay=1e-4(罚款力度)
optimizer = optim.SGD(
    model.parameters(),  # 模型的所有权重
    lr=0.01,             # 学习率(每次挪步的大小)
    weight_decay=1e-4    # 衰减系数(罚款力度)
)

关键参数怎么调?

  • 系数太小(比如 1e-6):罚款太轻,权重还是会变大,防过拟合效果差;
  • 系数太大(比如 1e-2):罚款太重,所有权重都被压到接近 0,模型学不到任何规律(欠拟合);
  • 新手默认用 1e-4,效果不好再微调。

总结:权重衰减的原理(一句话记)

通过在损失函数中加入「权重平方和 × 衰减系数」的惩罚项,让模型训练时主动降低所有权重的大小,避免过度依赖某几个 “极端特征” 拟合训练数据的噪声,从而学会 “均衡关注所有核心特征” 的通用规律,防止过拟合。

核心对比(有无权重衰减)

无权重衰减有权重衰减
权重可能极端大(比如 100)权重被控制在较小范围(比如 1)
依赖少数特征,易过拟合均衡关注多特征,泛化性强
只看 “预测对不对”既看 “预测对不对”,又看 “权重合不合理”

作业:仔细学习下测试和训练代码的逻辑,这是基础,这个代码框架后续会一直沿用,后续的重点慢慢就是转向模型定义阶段了。

浙大疏锦行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值