ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

VGG16卷积神经网络详解:Keras实现、迁移学习与剪枝

2026/10/6 3:24:42 拓冰建站 浏览量
VGG16卷积神经网络详解:Keras实现、迁移学习与剪枝 几年前我第一次在Keras里跑VGG16时第一反应是“这网络怎么这么憨”——所有卷积层几乎都是3x3卷积后面接一个接一个的池化最后几层全连接大得吓人。可就是这么一个“憨憨”的网络却成了计算机视觉入门绕不开的经典ImageNet竞赛里拿过亚军很多迁移学习项目直接把它的权重拿来当特征提取器用。这篇文章我想带着你走近VGG16卷积神经网络从架构设计、Keras实现、训练微调再到剪枝瘦身把这条经典路线完整过一遍。不管你是刚接触卷积神经网络的新手还是想拿VGG16做迁移学习、模型压缩的老手这篇文章都有一份可以直接上手的参考。VGG16最厉害的地方不是“深”而是它把卷积神经网络里最核心的几个概念——卷积、池化、步长、核、填充——用最简单的方式组合出了极强的表达能力。理解了VGG16你基本上就理解了CNN的骨架用Keras把它实现一遍后续再看ResNet、MobileNet都会轻松很多。我在实际项目中用VGG16做过图像分类、风格迁移前的特征提取也做过剪枝实验踩过不少坑这篇文章会把能复用的经验都写出来。1. VGG16到底在解决什么问题1.1 从LeNet-5到VGG16为什么“深”才是王道最早接触卷积神经网络很多人都是从LeNet-5开始的。LeNet-5用了两个卷积层加三个全连接层在手写数字识别上表现很好但一到复杂自然图像就不太行了。原因很简单自然图像里的模式非常多样大到整体轮廓小到纹理边缘都需要网络在不同的抽象层级上去捕捉。浅层网络拿不下这种多尺度的表达。VGG16的思路很直白把网络加深用更多非线性层去拟合复杂映射。它一共有16个权重层其中13个卷积层、3个全连接层因此叫VGG16。这里的“16”只统计带权重的层池化层和激活函数层不算。加深带来的好处是每一层可以学习到更抽象的特征前几层学边缘、颜色块中间层学纹理、局部形状后面几层学物体的部件甚至整体轮廓。李宏毅老师在讲CNN时经常用这种“逐层抽象”的视角VGG16就是最典型的例子。但是“深”不是无脑加深就行。VGG之前的网络喜欢用大的卷积核比如AlexNet用的11x11和5x5参数多、计算量大层数也难做深。VGG做出了一个关键改变全部用3x3小卷积核。这看起来像是退步实际上是一个聪明的正则化策略——用更少的参数达到同样的感受野同时塞进更多的非线性层。1.2 为什么VGG16钟爱3x3卷积核两个3x3卷积堆叠感受野相当于一个5x5卷积三个3x3卷积堆叠感受野相当于一个7x7卷积。但参数数量差很多。假设输入输出通道都是C一个5x5卷积需要25C²个参数而两个3x3卷积只需要2x9C²18C²个参数少了28%。更重要的是两个3x3卷积中间有一次ReLU激活相当于在这个感受野内做了两次非线性变换表达能力比单个5x5卷积更强。这个设计思路后来几乎成了CNN的“通用语言”。你会发现很多现代网络依然在用3x3卷积打底甚至用1x1卷积去控制通道数。VGG16没有用1x1卷积它就把“小卷积核堆叠”这个原则贯彻到底。实际操作里3x3卷积还有一个隐藏好处计算更规整GPU的矩阵运算库对这种尺寸的卷积优化得特别好。Keras在实现时也完全保留了这种结构使用起来非常直接。1.3 从16层到参数量算一笔数学账VGG16的参数总量大约是1.38亿其中全连接层占了绝大部分。来算一下卷积层的参数其实不算夸张。以第一个卷积块为例输入是三通道RGB图像第一个卷积层是64个3x3x3卷积核参数量是64x3x3x3641792。后面的卷积层通道数逐渐翻倍64、128、256、512最后两段保持512不变。卷积层参数加起来大约是1470万。真正的“胖子”在最后三个全连接层。最后一个卷积层输出的feature map是7x7x512展平后长度为25088。第一个全连接层是4096个神经元光这一层的权重就是25088x4096约1.03亿个参数。这就是VGG16“笨重”的主要原因。很多项目里大家只保留卷积部分作为特征提取器丢掉全连接层原因之一就在这里——全连接层的参数占比太高而且很容易过拟合。Keras里查看每一层参数量的方式很简单用model.summary()就能看到完整的层级结构和参数量。我建议你先跑一遍打印出来感受一下那三个全连接层有多“肥”。这个“肥”会在后文讲剪枝时成为重点对象。2. 用Keras搭建VGG16从零手写一遍2.1 安装环境与准备数据Keras现在作为TensorFlow的官方高阶API集成在tensorflow.keras里所以正确做法是安装TensorFlow。如果你用的是纯Keras老版本迁移到新项目时建议改成tensorflow.keras导入。pip install tensorflow装好后检查版本顺便看一眼GPU是否可用import tensorflow as tf print(tf.__version__) print(tf.config.list_physical_devices(GPU))如果GPU显示为空说明你在用CPU跑训练VGG16这种大网络在CPU上训练会非常痛苦。建议数据量小就用预训练权重做特征提取或者干脆在云端租GPU环境。数据准备上最简单的入门数据集是CIFAR-10图片尺寸是32x32VGG16原始输入要求224x224所以直接喂给VGG16会有尺寸问题。有两个选择一是把输入尺寸改小比如input_shape(32, 32, 3)但这样会导致全连接层的输入维度对不上因为VGG16的最后一个卷积输出尺寸依赖输入尺寸。二是在数据预处理时做Resize把图像缩放到224x224。我在做迁移学习时更推荐第二种因为预训练权重是基于224x224训练出来的。如果只是想快速搭建结构看效果可以直接用一个假的输入张量import tensorflow as tf input_tensor tf.keras.Input(shape(224, 224, 3))后续层层连接最后model.summary()看结构。2.2 手写VGG16网络结构不建议一上来就from tensorflow.keras.applications import VGG16那样虽然快但你会错过理解网络结构的机会。我建议先用Functional API手写一遍代码很简洁from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout from tensorflow.keras.models import Model def vgg16_block(x, filters, conv_count): for _ in range(conv_count): x Conv2D(filters, (3, 3), paddingsame, activationrelu)(x) x MaxPooling2D((2, 2), strides(2, 2))(x) return x inputs tf.keras.Input(shape(224, 224, 3)) x vgg16_block(inputs, 64, 2) x vgg16_block(x, 128, 2) x vgg16_block(x, 256, 3) x vgg16_block(x, 512, 3) x vgg16_block(x, 512, 3) x Flatten()(x) x Dense(4096, activationrelu)(x) x Dropout(0.5)(x) x Dense(4096, activationrelu)(x) x Dropout(0.5)(x) outputs Dense(1000, activationsoftmax)(x) model Model(inputs, outputs)这段代码用的是paddingsame也就是说卷积不会改变feature map的尺寸每次下采样全靠MaxPooling层。原始VGG论文里用的是paddingsame吗实际原论文是paddingvalid不填充但后续官方实现为了保持特征图尺寸方便串联使用了paddingsame。手写时用same更省心否则要自己算输出尺寸。步长在卷积层里默认是1池化层的步长是2。这两个参数的直观作用卷积步长决定扫描的粒度池化步长决定下采样的倍数。VGG16里所有池化窗口都是2x2、步长2每经过一次池化feature map的宽高减半通道数翻倍这是一种非常规整的“压缩-扩张”节奏。2.3 加载预训练权重与输入预处理如果你不想从零训练可以直接加载ImageNet预训练权重from tensorflow.keras.applications import VGG16 base_model VGG16(weightsimagenet, include_topFalse, input_shape(224, 224, 3))include_topFalse很关键这样会去掉最后三个全连接层和分类层只保留卷积部分。输出是7x7x512的特征图。这种用法适合下游任务比如你自己接一个全局池化加分类头来做迁移学习。另外VGG16官方预处理的细节很讲究。图像输入时像素值不是简单除以255而是要按通道减去均值。Keras的preprocess_input函数会帮你做转换from tensorflow.keras.applications.vgg16 import preprocess_input import numpy as np def load_and_preprocess_image(path, target_size(224, 224)): img tf.keras.preprocessing.image.load_img(path, target_sizetarget_size) x tf.keras.preprocessing.image.img_to_array(img) x np.expand_dims(x, axis0) x preprocess_input(x) return x注意preprocess_input对VGG16执行的是“去均值”而非归一化到[0,1]具体做法是从RGB三通道减去固定的均值。很多新手直接把图片除以255扔进去结果特征提取效果很差就是这个原因。3. 训练、微调与特征提取的实操细节3.1 三种使用方式从头训练、冻结微调、特征提取VGG16有1.38亿参数在普通数据上从头训练几乎不现实。实际项目中你一般只有三种选择第一种是直接做特征提取。把include_topFalse的模型跑一遍得到7x7x512的特征图然后展平丢给一个简单的分类器比如逻辑回归。这种方式最快适合小数据集。第二种是冻结卷积基只训练自己添加的上层。这其实是特征提取的“可微”版本把base_model.trainable False然后在上面接全局平均池化和自己的Dense层用分类交叉熵训练。由于底层权重已经包含了通用视觉特征只需学习怎么组合这些特征。第三种是微调Fine-tune。在第二种的基础上解冻部分高层卷积层用较小的学习率继续训练。微调能让高层特征自适应你的数据集但需要小心过拟合和破坏预训练权重。我给你的建议是数据量少于几千张时老老实实用第一种或第二种数据量大且和ImageNet图像风格差异较大时再考虑微调。3.2 关键超参数学习率、batch size、数据增强Keras里训练VGG16迁移模型我最推荐的经验值是这样的超参数推荐值说明优化器Adam或SGDmomentum微调用SGD更稳特征提取用Adam即可学习率1e-4到1e-3微调时建议1e-5到1e-4batch size32或64取决于GPU显存VGG16较大取小值轮数10到50配合早停和模型检查点数据增强RandomFlip、RandomRotation、RandomZoomKeras内置tf.keras.layers直接可用微调时最怕的是学习率过大一下把预训练权重冲坏。我会在解冻后把学习率调到原来的十分之一甚至百分之一。这种做法背后的原理是底层卷积学到的边缘和纹理特征非常通用不需要大幅更新顶层特征偏任务相关可以稍微放开一点。数据增强不要过度。VGG16本身过拟合风险高适度翻转、平移就够了。我见过有人给CIFAR-10用AutoAugment网络结构还是老VGG16结果训练时间翻了好几倍精度提升却有限。3.3 踩坑记录BatchNorm、池化索引与过拟合VGG16原版没有BatchNorm层但很多复现版本会加。加了之后收敛更快但也改变了原始网络的“脾性”。如果你在用预训练权重微调千万不要私自往卷积基里插入BatchNorm层否则预训练权重直接失效。Keras的tf.keras.layers.MaxPooling2D默认不返回池化索引所以别指望像SegNet那样做上采样反池化。如果项目需要反池化得自己用MaxPool2D(pool_size(2,2), strides2, paddingsame)配合手动记录索引这一点和PyTorch的return_indices不一样。我在做图像分割时就被这个坑过Keras里处理起来比较绕建议非必要不用。过拟合是VGG16的常态。Dropout在Keras里默认只对Dense层起效如果你把Dropout(0.5)放在卷积层之间训练时没问题但推理时它会被自动跳过不过卷积层的Dropout效果不如在Dense层前面明显。VGG16的设计里Dropout本来就只放在两个全连接层后面别乱加。还有一个小坑Keras的data_format默认是channels_last也就是TensorFlow风格shape是(batch, height, width, channels)。如果你之前用过Theano或PyTorch很容易把(batch, channels, height, width)传进去导致卷积核数量对不上。4. VGG16的剪枝与瘦身让巨人跑得更快4.1 剪枝的基本思路VGG16太大在移动端或边缘设备上根本跑不动。剪枝的本质是去掉网络中对输出贡献小的权重或通道让模型更小、更快同时尽量不损失精度。相比权重稀疏化把小数置零结构化剪枝更常用因为它能真正减少计算量不需要特殊的稀疏矩阵库支持。通道剪枝的思路很简单对每个卷积核计算某种重要性指标把不重要的卷积核去掉同时也要去掉它对应的下一层输入通道。这个操作在Keras里做起来并不像PyTorch那样方便因为Keras的模型是静态的剪枝后需要重新构建模型结构。我在做剪枝时常用三种重要性指标权重绝对值之和认为卷积核权重绝对值之和越小对输出的影响越小。BN层缩放因子如果网络里有BatchNorm那么BN的γ参数越小对应通道越不重要。激活值稀疏度统计验证集上每个通道输出为0的比例稀疏度高的通道更可剪。VGG16没有BN层所以第一种和第三种更好用。4.2 基于Keras实现权重剪枝的简单流程先明确一点Keras没有内置的剪枝API需要自己动手。下面是一个可跑的通道剪枝思路我用的是最小化代码示例import tensorflow as tf import numpy as np # 假设model是已经训练好的VGG16特征提取器 model tf.keras.applications.VGG16(weightsimagenet, include_topFalse, input_shape(224, 224, 3)) # 选一个卷积层做剪枝比如第3个卷积块 layer_name block3_conv3 layer model.get_layer(layer_name) weights, biases layer.get_weights() # 计算每个卷积核的L2范数作为重要性分数 l2_norm np.sqrt(np.sum(weights ** 2, axis(1, 2, 3))) # weights shape: (kernel_h, kernel_w, in_ch, out_ch) # 设定保留比例比如保留90%的通道 keep_ratio 0.9 num_keep int(len(l2_norm) * keep_ratio) indices np.argsort(l2_norm)[::-1][:num_keep] # 从大到小排序保留前面的 # 重新构建新的卷积层 new_weights weights[:, :, :, indices] new_biases biases[indices]这只是一个非常粗糙的示意。真正的剪枝还要考虑后续层的通道对齐因为你剪掉了这一层的输入通道下一层卷积的in_ch也必须同步剪掉。这就是Keras剪枝麻烦的地方你需要手动追踪每一层的输入输出通道关系。实际操作中我一般会把模型构造成一个函数记录每一层的输出shape然后在剪枝后重新构建一个更窄的模型再把剪枝后的权重填充回去。整个过程类似“拆积木再搭积木”很繁琐但逻辑清晰。4.3 剪枝后的精度恢复与评估剪枝之后精度通常会掉这时需要微调来恢复。恢复训练的套路和迁移学习微调很相似小学习率少量epoch先用验证集上的结果评估剪枝损失。剪枝比例不是越高越好我实测下来剪枝比例参数量减少精度下降推理加速0.3约30%几乎不变约10%-20%0.5约50%1%-2%约30%-40%0.7约70%5%-8%约50%0.9约90%15%以上需要大量微调这里说的推理加速不能只看参数量因为卷积层的计算量主要由FLOPs决定而FLOPs和输入尺寸、通道数、卷积核大小有关。VGG16的全连接层参数占了大头但剪掉全连接层的计算量提升并不显著真正耗时的还是前面的卷积层。我在一次实验里对block4和block5做了50%的通道剪枝再用CIFAR-10做微调精度大概掉了2%。当然这个结果依赖具体数据集但可以说明一点VGG16存在大量冗余剪枝是有效的。5. 常见问题与排查技巧实录5.1 训练和推理中遇到的典型问题我把这几年使用VGG16Keras时遇到的典型问题整理成了一张表方便你直接对照问题现象可能原因解决方法加载预训练模型时下载超时网络问题或缓存损坏手动下载h5文件放到~/.keras/models/目录输入图片维度报错没有加batch维度np.expand_dims(x, axis0)图片颜色偏色或特征差预处理方式错误使用preprocess_input不要单纯除以255训练Loss不降学习率过大或数据标签错调低学习率检查标签交叉熵是否匹配微调后模型反而不如冻结解冻层数太多、学习率太大只解冻最后一个block学习率降到1e-5GPU OOMbatch size太大或输入尺寸太大减小batch size或使用model.save_weights中途保存检查点Keras预测结果全是同一类全连接层过拟合或类别不平衡做数据均衡或改用特征提取分类器剪枝后模型完全失效剪枝时通道没对齐重新构建模型结构时仔细对应每层filter数量每一个问题我都实际遇到过最坑的是预处理。VGG16的preprocess_input是去均值和其他模型比如ResNet的缩放归一化不一样如果你在多个模型之间切换复用同一个预处理函数就会出大问题。5.2 我的调试小技巧第一给模型命名规范一点。Keras的自动命名在剪枝时会让你疯掉建议手写网络时给每层加name参数比如Conv2D(64, (3, 3), nameblock1_conv1)。这样在剪枝、微调、特征提取时都能精准定位层。第二尽量用Functional API而不是Sequential。VGG16虽然是线性结构但Functional API方便你后续加跳跃连接、多输出等扩展。而且Sequential在剪枝时几乎没法操作。第三用ModelCheckpoint回调保存最优权重而不是只保存最终epoch的权重checkpoint tf.keras.callbacks.ModelCheckpoint( best_model.h5, monitorval_acc, modemax, save_best_onlyTrue, verbose1 )VGG16训练慢如果中途断掉或者过拟合没有检查点就得从头再跑一遍非常浪费时间。5.3 后续还能怎么扩展如果你吃透了VGG16下一步我建议试试两个方向一是把VGG16里面的全连接层换成全局平均池化GAP可以直接把参数量砍掉一大截这也是ResNet、MobileNet等现代网络的常见做法。二是用VGG16作为教师网络做知识蒸馏把它的“知识”转移给小模型这比直接剪枝效果更平滑适合部署场景。另外Keras官方还提供了VGG19结构和VGG16差不多只是更厚。实际效果在多数任务上和VGG16差别不大参数量却更大所以我很少用VGG19。如果你在比赛中见到有人用VGG19多半是为了集成或多尺度特征而不是单模型精度。我自己在实际项目里使用VGG16最多的场景还是拿它当特征提取器。比如做人脸属性分析时先用VGG16把每张人脸转成4096维特征再训练一个轻量分类器效果比直接端到端训练要好很多。这也是VGG16至今没有被淘汰的原因——它虽然臃肿但特征质量确实高作为“视觉词典”非常称职。最后再分享一个经验想要真正理解卷积、池化、步长、核、填充这些概念只看理论永远隔着一层。把VGG16在Keras里手写一遍跑一次summary再试着剪掉几个卷积核看看效果很多疑惑就自然通了。这条经典网络也许已经不是最先进的但它仍然是最好的“教学模型”。