news 2026/10/6 3:24:41

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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卷积核,参数量是64x3x3x3+64=1792。
  • 后面的卷积层通道数逐渐翻倍: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,图片尺寸是32x32,VGG16原始输入要求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), padding='same', activation='relu')(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, activation='relu')(x) x = Dropout(0.5)(x) x = Dense(4096, activation='relu')(x) x = Dropout(0.5)(x) outputs = Dense(1000, activation='softmax')(x) model = Model(inputs, outputs)

这段代码用的是padding='same',也就是说卷积不会改变feature map的尺寸,每次下采样全靠MaxPooling层。原始VGG论文里用的是padding='same'吗?实际原论文是padding='valid',不填充,但后续官方实现为了保持特征图尺寸方便串联,使用了padding='same'。手写时用same更省心,否则要自己算输出尺寸。

步长在卷积层里默认是1,池化层的步长是2。这两个参数的直观作用:卷积步长决定扫描的粒度,池化步长决定下采样的倍数。VGG16里所有池化窗口都是2x2、步长2,每经过一次池化,feature map的宽高减半,通道数翻倍,这是一种非常规整的“压缩-扩张”节奏。

2.3 加载预训练权重与输入预处理

如果你不想从零训练,可以直接加载ImageNet预训练权重:

from tensorflow.keras.applications import VGG16 base_model = VGG16(weights='imagenet', include_top=False, input_shape=(224, 224, 3))

include_top=False很关键,这样会去掉最后三个全连接层和分类层,只保留卷积部分。输出是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_size=target_size) x = tf.keras.preprocessing.image.img_to_array(img) x = np.expand_dims(x, axis=0) x = preprocess_input(x) return x

注意preprocess_input对VGG16执行的是“去均值”而非归一化到[0,1],具体做法是从RGB三通道减去固定的均值。很多新手直接把图片除以255扔进去,结果特征提取效果很差,就是这个原因。

3. 训练、微调与特征提取的实操细节

3.1 三种使用方式:从头训练、冻结微调、特征提取

VGG16有1.38亿参数,在普通数据上从头训练几乎不现实。实际项目中,你一般只有三种选择:

第一种是直接做特征提取。把include_top=False的模型跑一遍,得到7x7x512的特征图,然后展平,丢给一个简单的分类器,比如逻辑回归。这种方式最快,适合小数据集。

第二种是冻结卷积基,只训练自己添加的上层。这其实是特征提取的“可微”版本:把base_model.trainable = False,然后在上面接全局平均池化和自己的Dense层,用分类交叉熵训练。由于底层权重已经包含了通用视觉特征,只需学习怎么组合这些特征。

第三种是微调(Fine-tune)。在第二种的基础上,解冻部分高层卷积层,用较小的学习率继续训练。微调能让高层特征自适应你的数据集,但需要小心过拟合和破坏预训练权重。

我给你的建议是:数据量少于几千张时,老老实实用第一种或第二种;数据量大且和ImageNet图像风格差异较大时,再考虑微调。

3.2 关键超参数:学习率、batch size、数据增强

Keras里训练VGG16迁移模型,我最推荐的经验值是这样的:

超参数推荐值说明
优化器Adam或SGD+momentum微调用SGD更稳,特征提取用Adam即可
学习率1e-4到1e-3微调时建议1e-5到1e-4
batch 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), strides=2, padding='same')配合手动记录索引,这一点和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(weights='imagenet', include_top=False, 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 训练和推理中遇到的典型问题

我把这几年使用VGG16+Keras时遇到的典型问题整理成了一张表,方便你直接对照:

问题现象可能原因解决方法
加载预训练模型时下载超时网络问题或缓存损坏手动下载h5文件,放到~/.keras/models/目录
输入图片维度报错没有加batch维度np.expand_dims(x, axis=0)
图片颜色偏色或特征差预处理方式错误使用preprocess_input,不要单纯除以255
训练Loss不降学习率过大或数据标签错调低学习率,检查标签交叉熵是否匹配
微调后模型反而不如冻结解冻层数太多、学习率太大只解冻最后一个block,学习率降到1e-5
GPU OOMbatch size太大或输入尺寸太大减小batch size,或使用model.save_weights中途保存检查点
Keras预测结果全是同一类全连接层过拟合或类别不平衡做数据均衡,或改用特征提取+分类器
剪枝后模型完全失效剪枝时通道没对齐重新构建模型结构时仔细对应每层filter数量

每一个问题我都实际遇到过,最坑的是预处理。VGG16的preprocess_input是去均值,和其他模型比如ResNet的缩放归一化不一样,如果你在多个模型之间切换,复用同一个预处理函数就会出大问题。

5.2 我的调试小技巧

第一,给模型命名规范一点。Keras的自动命名在剪枝时会让你疯掉,建议手写网络时给每层加name参数,比如Conv2D(64, (3, 3), name='block1_conv1')。这样在剪枝、微调、特征提取时都能精准定位层。

第二,尽量用Functional API而不是Sequential。VGG16虽然是线性结构,但Functional API方便你后续加跳跃连接、多输出等扩展。而且Sequential在剪枝时几乎没法操作。

第三,用ModelCheckpoint回调保存最优权重,而不是只保存最终epoch的权重:

checkpoint = tf.keras.callbacks.ModelCheckpoint( 'best_model.h5', monitor='val_acc', mode='max', save_best_only=True, verbose=1 )

VGG16训练慢,如果中途断掉或者过拟合,没有检查点就得从头再跑一遍,非常浪费时间。

5.3 后续还能怎么扩展

如果你吃透了VGG16,下一步我建议试试两个方向:一是把VGG16里面的全连接层换成全局平均池化(GAP),可以直接把参数量砍掉一大截,这也是ResNet、MobileNet等现代网络的常见做法。二是用VGG16作为教师网络,做知识蒸馏,把它的“知识”转移给小模型,这比直接剪枝效果更平滑,适合部署场景。

另外,Keras官方还提供了VGG19,结构和VGG16差不多,只是更厚。实际效果在多数任务上和VGG16差别不大,参数量却更大,所以我很少用VGG19。如果你在比赛中见到有人用VGG19,多半是为了集成或多尺度特征,而不是单模型精度。

我自己在实际项目里使用VGG16最多的场景,还是拿它当特征提取器。比如做人脸属性分析时,先用VGG16把每张人脸转成4096维特征,再训练一个轻量分类器,效果比直接端到端训练要好很多。这也是VGG16至今没有被淘汰的原因——它虽然臃肿,但特征质量确实高,作为“视觉词典”非常称职。

最后再分享一个经验:想要真正理解卷积、池化、步长、核、填充这些概念,只看理论永远隔着一层。把VGG16在Keras里手写一遍,跑一次summary,再试着剪掉几个卷积核看看效果,很多疑惑就自然通了。这条经典网络也许已经不是最先进的,但它仍然是最好的“教学模型”。

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

用iFlow CLI自定义Command实现网页抓取与自动翻译

最近用 iFlow CLI 折腾了一个特别顺手的小工具:一条命令抓取网页文章正文,再自动翻译成指定语言。这个需求我其实惦记很久了——每天要读不少英文技术博客,浏览器自带翻译体验一般,把全文复制到对话窗口又总是被上下文长度卡住。最…

作者头像 李华
网站建设 2026/10/6 3:20:47

P2P AI伴侣架构实战:点对点直连与本地模型部署全解析

自己一个人做产品,最怕的不是代码写不出来,而是半夜三更对着屏幕突然问自己:这东西到底有没有人用?说实话,做凤希AI伴侣这个项目,最初就是这么一个让人辗转反侧的想法——我想要一个真正“属于自己的”AI&a…

作者头像 李华
网站建设 2026/10/6 3:18:55

OpenClaw+Java:为老系统装个AI数字员工

先说个我上个月的实际经历。一套跑了七八年的 Java 订单管理系统,功能稳定,但业务侧每天都要手动处理三件事:审异常单、盯竞品公开价格、发日报。我当时想的就是——能不能用 OpenClaw 给它装个"数字员工",把这些重复劳…

作者头像 李华
网站建设 2026/10/6 3:18:17

Spring Boot智慧农作物种植系统:从毕设选题到答辩全攻略

每年毕业季后台私信问得最多的就是一句:“Java毕设我到底该选什么题,才不被老师说太简单?”我看了太多人交上来的选题,要么是图书管理、学生管理这种做了八百年的经典CRUD,要么是XX商城、XX论坛这种答辩时老师闭着眼睛…

作者头像 李华
网站建设 2026/10/6 3:18:00

macOS上安装Redis:从Homebrew到配置与避坑指南

1. macOS安装Redis到底该选哪条路先说结论:在macOS上安装Redis,绝大多数人不需要自己编译源码,也没有必要折腾复杂的容器化方案,最快最稳的方式就是直接用Homebrew装。我见过太多新手一上来就去看官网的"Redis下载"页面…

作者头像 李华
网站建设 2026/10/6 3:16:36

Android Studio入门教程:从环境配置到构建第一个App

如果你决定做安卓App,不管是为了交课程作业、验证一个点子,还是认认真真想入门移动开发,Android Studio这套工具链迟早要过一遍。很多新手真正卡住的往往不是代码逻辑本身,而是环境那一堆事:下载装哪个版本、首次启动怎…

作者头像 李华