news 2026/4/18 16:39:40

从零到一:用PyTorch手搓VGG16模型(附完整代码与逐行解析)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零到一:用PyTorch手搓VGG16模型(附完整代码与逐行解析)

1. 为什么选择VGG16作为入门模型

VGG16是计算机视觉领域的经典卷积神经网络架构,由牛津大学视觉几何组(Visual Geometry Group)在2014年提出。这个模型虽然现在看来不算最先进,但它有几个特别适合初学者的特点。首先,它的结构非常规整,全部使用3x3的小卷积核和2x2的最大池化层,这种"乐高积木式"的设计让网络构建过程变得直观。其次,VGG16在ImageNet等大型数据集上表现优异,证明了其设计的有效性。

我第一次接触深度学习时就是从VGG16入手的,当时最大的感受是:这个模型把复杂的卷积神经网络拆解成了可重复使用的标准模块。每个卷积块都遵循相同的模式 - 连续几个卷积层后接一个池化层。这种设计哲学对理解现代CNN架构特别有帮助,比如ResNet、DenseNet等后续模型都可以看作是在这个基础上的改进。

相比更复杂的模型,VGG16的参数和计算量确实偏大,但这反而让它成为学习的好选择。因为在实现过程中,你会清楚地看到每一层是如何影响特征图尺寸的,如何通过堆叠卷积层来增加感受野。这些直观感受对建立深度学习直觉非常重要。

2. 搭建开发环境与准备数据

在开始编码前,我们需要准备好Python环境和必要的数据集。我推荐使用Anaconda创建独立的Python环境,这样可以避免包版本冲突。以下是具体步骤:

conda create -n vgg16 python=3.8 conda activate vgg16 pip install torch torchvision matplotlib

数据集方面,我们使用CIFAR-10而不是原始的ImageNet,原因很简单:ImageNet太大(超过100GB),而CIFAR-10只有约170MB,但同样能验证模型的正确性。CIFAR-10包含10个类别的6万张32x32彩色图片,非常适合教学和实验。

数据增强是训练深度学习模型的关键技巧。下面这段代码展示了如何对CIFAR-10进行标准化和增强:

transform_train = transforms.Compose([ transforms.RandomHorizontalFlip(), # 随机水平翻转 transforms.ToTensor(), transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)) # ImageNet均值标准差 ])

这里有个细节需要注意:Normalize的均值和标准差使用的是ImageNet的统计值,而不是CIFAR-10的。这是因为我们后面可能会用预训练的VGG16权重,而预训练模型是在ImageNet上训练的。虽然用CIFAR-10自己的统计值理论上更合理,但实际差异不大。

3. 逐层构建VGG16网络结构

现在进入最核心的部分 - 用PyTorch搭建VGG16。我们先从整体上理解VGG16的结构:它由5个卷积块和3个全连接层组成,每个卷积块包含多个卷积层和一个池化层。具体来说:

  • 块1:2个卷积层(64通道) + 最大池化
  • 块2:2个卷积层(128通道) + 最大池化
  • 块3:3个卷积层(256通道) + 最大池化
  • 块4:3个卷积层(512通道) + 最大池化
  • 块5:3个卷积层(512通道) + 最大池化

让我们用nn.Sequential来实现第一个卷积块:

self.layer1 = nn.Sequential( nn.Conv2d(3, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2) )

这里有几个关键点需要注意:

  1. 所有卷积层都使用padding=1,配合3x3的kernel,可以保持特征图尺寸不变
  2. BatchNorm层放在卷积之后、ReLU之前,这是标准做法
  3. ReLU的inplace=True可以节省内存,但只适用于没有后续分支的情况
  4. 最大池化的stride=2会使特征图尺寸减半

特征图尺寸的变化是理解CNN的关键。对于输入32x32的图片,经过layer1后:

  • 两个卷积层保持32x32尺寸(因为(32-3+2)/1+1=32)
  • 池化层将尺寸减半到16x16

4. 完整实现VGG16类

将五个卷积块组合起来,再加上全连接层,就得到了完整的VGG16。下面是完整的类实现:

class VGG16(nn.Module): def __init__(self, num_classes=10): super(VGG16, self).__init__() self.features = nn.Sequential( # 块1 nn.Conv2d(3, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), # 块2-5省略... ) self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.classifier = nn.Sequential( nn.Linear(512, 512), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(512, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): x = self.features(x) x = self.avgpool(x) x = torch.flatten(x, 1) x = self.classifier(x) return x

这里我做了两个改进:

  1. 添加了AdaptiveAvgPool2d,这使得网络可以接受不同尺寸的输入
  2. 全连接层使用了Dropout来防止过拟合,这是原论文中的技巧

forward方法展示了数据流动的完整路径:先经过特征提取部分(features),然后全局平均池化,展平后送入分类器。这种模块化的设计让网络结构非常清晰。

5. 训练技巧与参数调优

有了模型后,训练过程同样重要。VGG16虽然结构简单,但训练时还是有些技巧的:

model = VGG16().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) for epoch in range(30): model.train() for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step()

关键训练技巧包括:

  1. 使用带动量的SGD优化器,动量设为0.9
  2. 添加L2正则化(weight_decay=5e-4)
  3. 每5个epoch将学习率减半
  4. 训练前调用model.train(),测试前调用model.eval()

我在实际训练中发现,batch size设为128时效果最好。学习率初始设为0.01,如果发现loss出现NaN,可以尝试降低到0.001。另外,使用混合精度训练可以显著减少显存占用:

scaler = torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): outputs = model(inputs) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

6. 模型评估与结果分析

训练完成后,我们需要评估模型在测试集上的表现:

model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, labels in test_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f'Accuracy: {100 * correct / total:.2f}%')

在CIFAR-10上,经过30个epoch的训练,VGG16通常能达到85%-88%的准确率。这个结果虽然不如最新的ResNet等模型,但对于教学目的已经足够。我们可以通过以下方法进一步提升性能:

  1. 添加更多的数据增强:随机裁剪、颜色抖动等
  2. 使用学习率预热(learning rate warmup)
  3. 尝试不同的优化器如AdamW
  4. 加入标签平滑(label smoothing)正则化

一个有趣的实验是可视化中间层的特征图,这能帮助我们理解CNN是如何逐步提取特征的:

# 获取第一个卷积层的输出 activation = {} def get_activation(name): def hook(model, input, output): activation[name] = output.detach() return hook model.features[0].register_forward_hook(get_activation('conv1')) # 可视化 with torch.no_grad(): output = model(inputs[0:1]) plt.imshow(activation['conv1'][0, 0].cpu().numpy())

7. 常见问题与调试技巧

在实现VGG16的过程中,我遇到过不少坑,这里分享几个典型问题和解决方法:

问题1:显存不足解决方案:

  • 减小batch size(从128降到64)
  • 使用梯度累积:每累积几个小batch再更新一次权重
  • 尝试混合精度训练

问题2:训练loss不下降可能原因:

  • 学习率设置不当,尝试增大或减小
  • 数据预处理有问题,检查Normalize的参数
  • 模型初始化问题,尝试使用He初始化

问题3:过拟合解决方法:

  • 增加Dropout的比例
  • 加强数据增强
  • 添加更多的权重衰减

一个实用的调试技巧是在模型开头添加一个小的子网络,快速验证数据流是否正确:

class DebugNet(nn.Module): def __init__(self): super().__init__() self.conv = nn.Conv2d(3, 64, 3) def forward(self, x): print(x.shape) # 调试输入尺寸 x = self.conv(x) print(x.shape) # 调试输出尺寸 return x

另一个常见困惑是特征图尺寸的计算。记住这个公式:

输出尺寸 = (输入尺寸 - kernel_size + 2*padding) / stride + 1

8. 扩展与进阶方向

掌握了基础VGG16实现后,你可以尝试以下进阶方向:

  1. 实现其他VGG变体:比如VGG19,或者减少通道数的精简版VGG
  2. 添加注意力机制:在卷积块之间插入SE或CBAM模块
  3. 迁移学习:加载预训练的VGG16权重,微调最后几层
  4. 模型压缩:对VGG16进行剪枝和量化,减小模型大小

预训练权重的使用示例:

from torchvision.models import vgg16_bn pretrained_model = vgg16_bn(pretrained=True) # 替换最后一层 pretrained_model.classifier[-1] = nn.Linear(4096, 10)

对于想深入理解CNN的同学,我建议尝试从零实现卷积操作(不使用nn.Conv2d),这能让你真正理解卷积的本质。比如:

def conv2d(input, kernel, stride=1, padding=0): # 手动实现卷积运算 batch, in_c, h, w = input.shape out_h = (h + 2*padding - kernel.shape[2]) // stride + 1 out_w = (w + 2*padding - kernel.shape[3]) // stride + 1 output = torch.zeros(batch, kernel.shape[0], out_h, out_w) # 具体卷积计算... return output

最后,虽然现在Transformer在CV领域很火,但CNN仍然是很多实际应用的首选,特别是在计算资源有限的场景下。VGG16作为CNN的经典代表,它的设计思想永远不会过时。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 16:37:43

3分钟上手QtScrcpy:跨平台安卓投屏的终极解决方案

3分钟上手QtScrcpy:跨平台安卓投屏的终极解决方案 【免费下载链接】QtScrcpy Android实时投屏软件,此应用程序提供USB(或通过TCP/IP)连接的Android设备的显示和控制。它不需要任何root访问权限 项目地址: https://gitcode.com/barry-ran/QtScrcpy …

作者头像 李华
网站建设 2026/4/18 16:37:42

【MATLAB】三维曲面可视化进阶:从基础绘制到高级美化

1. 三维曲面绘制基础:从网格生成到初步成型 第一次用MATLAB画三维曲面时,我被meshgrid函数搞得一头雾水。直到有天盯着工作区的变量值看了半小时,突然就开窍了——原来它就像织毛衣的针脚,把一维的x和y坐标编织成二维的网格布。举…

作者头像 李华
网站建设 2026/4/18 16:36:46

Windows系统优化工具终极指南:Winhance完全免费解决方案

Windows系统优化工具终极指南:Winhance完全免费解决方案 【免费下载链接】Winhance-zh_CN A Chinese version of Winhance. C# application designed to optimize and customize your Windows experience. 项目地址: https://gitcode.com/gh_mirrors/wi/Winhance-…

作者头像 李华
网站建设 2026/4/18 16:36:34

Calibre-Douban插件:豆瓣图书元数据自动获取终极指南

Calibre-Douban插件:豆瓣图书元数据自动获取终极指南 【免费下载链接】calibre-douban Calibre new douban metadata source plugin. Douban no longer provides book APIs to the public, so it can only use web crawling to obtain data. This is a calibre Doub…

作者头像 李华
网站建设 2026/4/18 16:34:42

Axure RP中文界面汉化:5分钟告别英文困扰,开启高效设计之旅

Axure RP中文界面汉化:5分钟告别英文困扰,开启高效设计之旅 【免费下载链接】axure-cn Chinese language file for Axure RP. Axure RP 简体中文语言包。支持 Axure 11、10、9。不定期更新。 项目地址: https://gitcode.com/gh_mirrors/ax/axure-cn …

作者头像 李华