使用 PyTorch 从零开始构建 ResNet18

在深度学习的历史长河中,ResNet 无疑是一座里程碑。即使在 2026 年,面对 Vision Transformer (ViT) 和状态空间模型(SSM)的激烈竞争,ResNet18 凭借其卓越的性价比和简洁的架构,依然是边缘计算设备和快速原型验证中的首选。在这篇文章中,我们将深入探讨如何从零开始构建 ResNet18。不仅如此,我们还会结合最新的 2026 年 AI 辅助编程实践,展示我们如何在现代开发流程中高效地实现、调试并优化这一经典模型。

01 为什么在 2026 年还要重写 ResNet18?

你可能会问,既然现在有 torchvision 库可以直接调用,为什么还要我们手动去写每一层代码?在我们的实际工程经验中,手动实现经典架构是理解梯度流动和特征提取机制的最佳途径。更重要的是,这赋予了我们“完全掌控权”。当我们需要修改激活函数、替换归一化层(例如从 BatchNorm 换成 LayerNorm 或 TinyBatchNorm)以适应边缘设备时,这种从零构建的能力就显得尤为关键。

02 环境准备与 AI 辅助开发工作流

在开始编码之前,让我们先准备好 2026 年标准的开发环境。现在的我们不再局限于简单的 pip 安装,更注重依赖的隔离和环境的可复现性。

# !pip install torch torchvision matplotlib -q
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

# 检查硬件可用性,这在混合精度训练中至关重要
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Training on device: {device}")

提示: 在我们最近的开发中,强烈推荐使用 Python 3.11+ 的类型注解。这不仅能让代码更易读,还能让 Cursor 或 GitHub Copilot 这样的 AI 结对编程伙伴更准确地理解我们的意图,从而提供更智能的补全建议。

03 数据加载与现代增强策略

CIFAR-10 作为一个经典数据集,其图像分辨率仅为 32×32。在处理低分辨率图像时,我们需要非常小心数据增强策略,以免过度破坏图像特征。让我们来看一个实际的例子,我们将结合传统的增强方法与现代的自动增强思想。

# 针对 CIFAR-10 优化的预处理流程
# 我们使用硬编码的均值和标准差,这是经过验证的最佳实践
transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    # transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), # 2026视角:可选,视模型容量而定
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])

transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])

# 使用多线程加载器加速 I/O 瓶颈
trainset = torchvision.datasets.CIFAR10(root=‘./data‘, train=True, download=True, transform=transform_train)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2, pin_memory=True)

testset = torchvision.datasets.CIFAR10(root=‘./data‘, train=False, download=True, transform=transform_test)
testloader = torch.utils.data.DataLoader(testset, batch_size=100, shuffle=False, num_workers=2, pin_memory=True)

04 核心构建:生产级残差块实现

残差块是 ResNet 的灵魂。在这里,我们不仅要实现它,还要确保它是“生产就绪”的。这意味着我们需要小心处理维度的匹配问题(捷径连接),并考虑到内存效率。

在设计 BasicBlock 时,你可能会遇到这样的情况:当输入输出维度不一致时,直接相加会导致报错。我们可以通过以下方式解决这个问题:在捷径连接中加入一个 1×1 卷积来调整维度。

class BasicBlock(nn.Module):
    """
    ResNet18 的基础构建块。
    包含两个 3x3 卷积层,并带有捷径连接。
    """
    expansion = 1

    def __init__(self, in_channels, out_channels, stride=1, reduction=None):
        super(BasicBlock, self).__init__()
        # 主路径:Conv3x3 -> BN -> ReLU -> Conv3x3 -> BN
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True) # inplace 操作节省显存
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        
        # 捷径连接:处理维度或步长不匹配的情况
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        identity = x
        
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        
        out = self.conv2(out)
        out = self.bn2(out)
        
        # 核心操作:将输入加到输出上(残差学习)
        out += self.shortcut(identity)
        out = self.relu(out)
        return out

05 构建 ResNet18 架构

现在,让我们将这些块组装成完整的网络。ResNet18 的结构非常规整:初始卷积层 + 4 个残差阶段。在这里,我们展示了如何通过 _make_layer 方法来简化重复代码,这也是我们在工程中避免冗余的标准做法。

class ResNet18(nn.Module):
    def __init__(self, num_classes=10):
        super(ResNet18, self).__init__()
        self.in_channels = 64
        
        # 初始卷积:对于 CIFAR-10 (32x32),通常使用 3x3 卷积,kernel_size=7 会过于激进丢失信息
        self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        # 注意:ResNet18 原论文针对 ImageNet 使用了 MaxPool,但在 CIFAR-10 上通常省略
        
        # 四个残差层,通道数分别为 64, 128, 256, 512
        self.layer1 = self._make_layer(BasicBlock, 64, blocks=2, stride=1)
        self.layer2 = self._make_layer(BasicBlock, 128, blocks=2, stride=2)
        self.layer3 = self._make_layer(BasicBlock, 256, blocks=2, stride=2)
        self.layer4 = self._make_layer(BasicBlock, 512, blocks=2, stride=2)
        
        # 分类头
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * BasicBlock.expansion, num_classes)

    def _make_layer(self, block, out_channels, blocks, stride):
        """构建包含多个残差块的层"""
        layers = []
        layers.append(block(self.in_channels, out_channels, stride))
        self.in_channels = out_channels
        for _ in range(1, blocks):
            layers.append(block(out_channels, out_channels, stride=1))
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)
        return x

06 2026 视角的训练循环与优化策略

仅仅写出模型架构是不够的。在 2026 年,我们更加关注训练的效率和动态调整策略。比如,使用 OneCycleLR 学习率调度器通常比传统的 StepLR 收敛得更快。同时,混合精度训练(AMP)是标配,它能显著加速计算并减少显存占用。

model = ResNet18().to(device)

# 损失函数:对于分类任务,交叉熵是标准选择
criterion = nn.CrossEntropyLoss()

# 优化器:AdamW 是当前首选,相比 Adam 增加了权重衰减的正确解耦
optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=5e-4)

# 学习率调度器:OneCycle 策略在 2026 年非常流行
scheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr=0.1, steps_per_epoch=len(trainloader), epochs=20)

# 启用混合精度训练以加速 (需要 CUDA)
scaler = torch.cuda.amp.GradScaler() if torch.cuda.is_available() else None

下面是我们的训练循环,其中包含了自动混合精度(AMP)的使用,这是现代高性能训练的标志。

import time

def train_one_epoch(epoch):
    model.train()
    running_loss = 0.0
    start_time = time.time()
    
    for i, (inputs, labels) in enumerate(trainloader):
        inputs, labels = inputs.to(device), labels.to(device)
        
        optimizer.zero_grad()
        
        # 前向传播(使用 autocast 进行混合精度)
        if scaler:
            with torch.cuda.amp.autocast():
                outputs = model(inputs)
                loss = criterion(outputs, labels)
            # 反向传播
            scaler.scale(loss).backward()
            scaler.step(optimizer)
            scaler.update()
        else:
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            
        scheduler.step() # 更新学习率
        
        running_loss += loss.item()
        
    end_time = time.time()
    print(f"Epoch [{epoch+1}], Loss: {running_loss / len(trainloader):.4f}, Time: {end_time - start_time:.2f}s")

# 简单的训练调用
# for epoch in range(20):
#     train_one_epoch(epoch)

07 真实场景分析与常见陷阱

在我们过去的项目中,我们经常看到开发者陷入 BatchNorm 的陷阱。关键经验:如果你在训练后使用 INLINECODE9b74ffe7 切换到评估模式,必须确保 BatchNorm 的统计信息(runningmean 和 runningvar)被正确冻结。如果你发现测试集准确率突然暴跌,第一件事就是检查是否忘记调用 INLINECODE3403cb09 或者 BatchNorm 层的 track_running_stats 设置。

另一个趋势是边缘计算优化。如果你打算将这个 ResNet18 部署到树莓派或移动端,建议将第一层的 INLINECODE6ae71744 改为 INLINECODEf9425bc7(如上文代码所示),并考虑使用量化感知训练(QAT)来减小模型体积。

08 总结

通过这篇文章,我们不仅从零构建了 ResNet18,还融入了 2026 年的开发理念:使用 AI 辅助思考、采用更高效的优化器、利用混合精度加速以及关注边缘部署的细节。ResNet18 虽然经典,但在这些新工具和新思维的加持下,它依然焕发着强大的生命力。希望你能在自己的项目中尝试这些技巧,感受深度学习工程进化的乐趣。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/33018.html
点赞
0.00 平均评分 (0% 分数) - 0