从论文到代码:手把手教你用Keras从零实现VGG网络

发布时间:2026/6/2 11:50:40

从论文到代码:手把手教你用Keras从零实现VGG网络 1. 从论文到代码手把手教你用Keras从零实现VGG网络在深度学习领域Keras以其简洁的API和丰富的示例代码成为了无数开发者和研究者的入门首选。然而一个普遍的现象是我们常常习惯于从GitHub或博客上直接复制粘贴一段现成的模型代码比如经典的VGG16然后匆匆投入到自己的项目中。代码跑起来了但你真的理解每一层卷积、每一个池化背后的设计逻辑吗你知道为什么卷积核要选3x3而不是5x5为什么全连接层要设置4096个神经元这些参数又是如何计算出来的这正是我们这次实践的核心价值所在。我将带你做一件看似“重复造轮子”的事抛开所有现成的keras.applications.VGG16仅凭一篇学术论文——《Very Deep Convolutional Networks for Large-Scale Image Recognition》用Keras从零开始亲手搭建出VGG网络。这个过程远不止是敲几行代码。它是一次对卷积神经网络CNN架构的深度解剖一次将严谨的学术描述转化为可执行代码的实战演练更是一次培养“读论文、复现模型”这一核心科研能力的绝佳训练。如果你对深度学习有热情却总感觉停留在调包层面那么这次从论文到实现的旅程正是你突破瓶颈的关键一步。2. 项目整体思路与准备工作2.1 为什么选择VGG作为实现目标在众多经典CNN模型中如AlexNet, GoogLeNet, ResNet我选择VGG作为我们第一次“从论文实现”的对象主要基于以下几点考量架构的清晰与优雅VGG网络的核心思想极其简洁——堆叠多个小型3x3卷积核来替代大型卷积核如5x5, 7x7。这种设计在论文中有严谨的论证两个3x3卷积层的堆叠其有效感受野相当于一个5x5卷积层同时参数量更少并引入了更多的非线性变换多了一层ReLU激活。这种模块化、规则化的设计使得其网络结构像乐高积木一样清晰非常适合初学者理解和实现。论文的可读性极高牛津大学Visual Geometry Group发表的这篇论文在深度学习领域是公认的写作典范。它结构清晰图表详实将复杂的网络配置用一张表格Table 1完整呈现所有关键超参数卷积核尺寸、通道数、池化方式都明确列出。对于初次尝试“论文复现”的人来说这是一篇友好的“说明书”。在Keras中有权威参照Keras官方提供了预训练的VGG16和VGG19模型。这为我们实现后的验证提供了黄金标准。我们可以通过对比自己搭建的模型与官方模型的层结构、参数数量来检验我们实现的正确性这种即时反馈对学习至关重要。目标与收获本次实践的目标不是训练一个在ImageNet上达到state-of-the-art的模型那需要巨大的计算资源和时间而是完整走通“阅读论文 - 理解架构 - 计算参数 - 代码实现 - 验证对比”这个闭环。你将收获1) 深度理解VGG的架构设计哲学2) 掌握CNN中参数计算与特征图尺寸变换的硬核技能3) 熟练使用Keras的SequentialAPI或函数式API构建复杂模型4) 建立阅读和实现学术论文的信心与方法论。2.2 环境准备与知识储备在开始动手之前我们需要确保“弹药”充足。1. 基础环境搭建你需要一个安装了Python的编程环境。强烈建议使用Anaconda来管理Python环境和包依赖它能避免很多令人头疼的版本冲突问题。核心需要安装的库如下Keras 我们构建模型的主要框架。注意Keras现在已整合到TensorFlow中可以通过pip install tensorflow来安装然后使用from tensorflow import keras进行导入。NumPy 用于基本的数值计算Keras底层依赖它。 安装命令通常很简单pip install numpy tensorflow。为了保证一致性建议固定版本例如pip install tensorflow2.10.0。2. 核心知识预习虽然我们会详细解释但提前理解以下概念会让你事半功倍卷积层Convolutional Layer 理解卷积运算、滤波器通道数、卷积核尺寸、步长Stride、填充Padding的概念。重点弄清“same”和“valid”两种填充模式的区别。池化层Pooling Layer 特别是最大池化MaxPooling理解其下采样降低空间维度的作用。全连接层Dense Layer 理解如何将卷积层输出的多维特征图“压平”Flatten后输入全连接层进行分类。激活函数Activation Function VGG中全部使用ReLURectified Linear Unit。如果你对上述概念感到陌生我强烈建议你花1-2小时阅读斯坦福CS231n课程Convolutional Neural Networks for Visual Recognition的相关笔记。它被誉为CNN的“圣经”讲解直观透彻。不必通读重点看“卷积神经网络”、“卷积层”、“池化层”这几个章节即可。3. 获取并预读论文我们的“蓝图”是论文《Very Deep Convolutional Networks for Large-Scale Image Recognition》Simonyan Zisserman, 2014。你可以直接在网上搜索标题找到PDF版本。在第一轮阅读时采用“三步法”读摘要Abstract 了解文章核心贡献提出了深度增加至16-19层的CNN用小卷积核在ImageNet上取得好效果。读结论Conclusion 把握作者最终的总结和未来展望。速览图表 重点看Table 1网络配置表和Table 2/3/4实验结果。这一步的目的是在10分钟内对VGG有个全局印象并选定我们要实现的具体配置。注意初次接触学术论文可能会被复杂的公式和论述吓到。我们的策略是“带着问题去读”比如“第三层卷积的通道数是多少”然后像查字典一样在论文中寻找答案而不是试图一次性理解每一个句子。3. 深度解析VGG网络架构与设计逻辑3.1 精读论文定位关键信息现在让我们拿起论文化身“侦探”去提取构建模型所需的所有信息。我们的焦点是Section 2: ConvNet Configurations和Section 2.3: Discussion。首先选择配置。查看Table 1你会发现VGG有A到E共6种配置A-LRN可视为A的变种。其中D16层常称VGG16和E19层VGG19是性能最好的。为了平衡学习复杂度和代表性我们选择配置DVGG16。它在当年取得了优异的成绩且结构比VGG19稍简单是绝佳的学习范本。其次解析网络结构表Table 1。这是我们的核心“建筑图纸”。表中每一行代表一个网络层。你需要看懂它的描述方式conv3-64 这是一个卷积层conv。3表示卷积核的尺寸receptive field是3x3。64表示该层有64个滤波器即输出通道数为64。maxpool 这是一个最大池化层窗口尺寸为2x2步长为2。fc-4096 这是一个全连接层fully-connected有4096个神经元。fc-1000 最后一个全连接层对应ImageNet的1000个类别后面接Softmax激活函数。然后挖掘论文细节中的超参数。Table 1没有写明步长Stride和填充Padding。这需要我们在论文正文中寻找。在Section 2.1中论文明确指出卷积层的步长固定为1像素stride1。为了在卷积后保持空间分辨率即宽高不变对3x3卷积核进行了1像素的填充padding1。这在我们的代码中对应paddingsame。最大池化层使用2x2的窗口步长为2stride2。这会将特征图的宽和高各缩小至一半。所有隐藏层都使用ReLU作为激活函数。输入图像被固定缩放至224x224的RGB图像深度为3。最后理解设计哲学。为什么全是3x3卷积论文在Section 2.2和2.3进行了精彩论述。两个3x3卷积堆叠中间有ReLU vs 一个5x5卷积参数量更少两个3x3卷积的参数为2*(3*3*C^2)18C^2而一个5x5卷积的参数为25C^2C为通道数。参数更少意味着模型更简洁过拟合风险更低。非线性更强多了一层ReLU激活增加了模型的判别能力。感受野等效两个3x3卷积堆叠其有效感受野确实是5x5。这种“用小核构建大感受野”的思想影响深远。3.2 手动计算参数量与特征图尺寸变换这是将理论转化为代码前最关键的一步也是检验你是否真正理解架构的试金石。请拿出纸笔或打开一个文本编辑器我们一起来推导。计算规则卷积层参数量(kernel_height * kernel_width * input_channels 1) * output_channels。其中的1是每个滤波器的偏置项bias。卷积层输出尺寸 当paddingsame且stride1时输出宽高等于输入宽高。输出通道数等于该层滤波器数量。池化层参数量 池化是固定操作没有可学习的参数因此参数量为0。池化层输出尺寸output_size floor((input_size - pool_size) / stride) 1。对于pool_size2, stride2输出尺寸为输入尺寸的一半。全连接层参数量(input_units 1) * output_units。这里的input_units是上一层输出的总元素个数即height * width * channels经过Flatten后的值。让我们以VGG16配置D的前几层为例进行实战计算输入层 尺寸为224 (H) x 224 (W) x 3 (C)。参数量0。第一卷积层 (conv3-64)输入224x224x3卷积核3x3 输出通道64参数量(3*3*3 1) * 64 (271)*64 1792输出尺寸same填充步长1224x224x64第二卷积层 (conv3-64)输入224x224x64卷积核3x3 输出通道64参数量(3*3*64 1) * 64 (5761)*64 36928输出尺寸224x224x64第一池化层 (maxpool)输入224x224x64池化核2x2 步长2参数量0输出尺寸floor((224-2)/2)1 112 即112x112x64按照这个逻辑你可以依次计算所有层。当计算到最后一个卷积层conv3-512 在第二个maxpool之后时其输出尺寸会变为7x7x512。这是因为经过5次maxpool每次尺寸减半224 - 112 - 56 - 28 - 14 - 7。全连接层的计算是关键Flatten层 将7x7x512的特征图展平为一维向量长度为7*7*512 25088。第一个全连接层 (fc-4096)输入单元数25088输出单元数4096参数量(25088 1) * 4096 102,764,544。这是一个巨大的数字也是VGG模型参数主要集中的地方。第二个全连接层 (fc-4096)输入单元数4096输出单元数4096参数量(4096 1) * 4096 16,781,312第三个全连接层 (fc-1000)输入单元数4096输出单元数1000参数量(4096 1) * 1000 4,097,000将所有这些参数累加你会得到一个总数约1.38亿这与论文Table 2中VGG16的参数总量138M是吻合的。完成这个计算过程你对VGG模型的理解就从“看图表”深入到了“知细节”的层面。实操心得一定要亲手算一遍这个过程能帮你深刻理解每一层是如何改变数据形状的以及模型的容量参数量分布在哪里。很多初学者卡在Flatten层向全连接层过渡的地方就是因为没有理清三维张量到一维向量的转换。当你自己算出的参数总量和论文对得上时那种成就感是无可替代的。4. 使用Keras从零构建VGG16模型4.1 构建模型骨架Sequential API的运用理解了架构和计算现在我们可以用代码将蓝图变为现实。Keras提供了两种主要的模型构建方式Sequential顺序模型和函数式API。对于VGG这种简单的线性堆叠结构Sequential模型是最直观的选择。首先导入必要的模块。注意由于我们是从零构建不应该导入keras.applications中的VGG。from tensorflow import keras from tensorflow.keras import layers接下来我们初始化一个Sequential模型并按照Table 1的配置D一层一层地添加网络层。def build_vgg16(input_shape(224, 224, 3)): 根据论文《Very Deep Convolutional Networks for Large-Scale Image Recognition》 中的配置DVGG16构建模型。 参数: input_shape: 输入图像的形状默认为(224, 224, 3) 返回: 一个未编译的Keras Sequential模型 model keras.Sequential(nameVGG16_From_Scratch) # Block 1 model.add(layers.Conv2D(filters64, kernel_size(3, 3), paddingsame, activationrelu, input_shapeinput_shape)) model.add(layers.Conv2D(filters64, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.MaxPooling2D(pool_size(2, 2), strides(2, 2))) # 输出变为112x112 # Block 2 model.add(layers.Conv2D(filters128, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.Conv2D(filters128, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.MaxPooling2D(pool_size(2, 2), strides(2, 2))) # 输出变为56x56 # Block 3 model.add(layers.Conv2D(filters256, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.Conv2D(filters256, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.Conv2D(filters256, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.MaxPooling2D(pool_size(2, 2), strides(2, 2))) # 输出变为28x28 # Block 4 model.add(layers.Conv2D(filters512, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.Conv2D(filters512, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.Conv2D(filters512, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.MaxPooling2D(pool_size(2, 2), strides(2, 2))) # 输出变为14x14 # Block 5 model.add(layers.Conv2D(filters512, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.Conv2D(filters512, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.Conv2D(filters512, kernel_size(3, 3), paddingsame, activationrelu)) model.add(layers.MaxPooling2D(pool_size(2, 2), strides(2, 2))) # 输出变为7x7 # 分类头Classification Head model.add(layers.Flatten()) # 将7x7x512展平为25088维向量 model.add(layers.Dense(units4096, activationrelu)) model.add(layers.Dense(units4096, activationrelu)) model.add(layers.Dense(units1000, activationsoftmax)) # ImageNet有1000个类别 return model这段代码几乎是对论文Table 1的直译。每个Conv2D层的filters参数对应表格中的通道数kernel_size(3,3)对应conv3。paddingsame确保了卷积后空间尺寸不变。MaxPooling2D的strides(2,2)实现了下采样。4.2 关键参数详解与避坑指南在编写上述代码时有几个关键点需要特别注意它们直接关系到模型是否能被正确构建和后续训练。1.input_shape的指定input_shape参数只在模型的第一层需要提供。它是一个元组例如(224, 224, 3)。这里有一个常见的坑Keras的默认数据格式data_format在TensorFlow后端是channels_last即(height, width, channels)。如果你使用的是其他后端或旧代码可能会遇到channels_first即(channels, height, width)的情况。不一致会导致模型构建失败。你可以在~/.keras/keras.json配置文件中查看和修改image_data_format设置但最稳妥的方法是在代码中保持一致性。我们的代码遵循channels_last。2. 填充Padding模式的选择这是实现中最容易出错的地方之一。Keras的Conv2D层提供了两种padding模式paddingvalid 表示不进行任何填充。卷积操作只发生在图像内部因此输出尺寸会变小。计算公式为output_size floor((input_size - kernel_size) / stride) 1。paddingsame 表示进行填充使得输出尺寸与输入尺寸当stride1时相同。Keras会自动计算需要填充的行数和列数。 根据论文描述为了保持分辨率3x3卷积应使用1像素的填充。这在Keras中对应paddingsame。务必不要用paddingvalid否则特征图尺寸会迅速缩小导致最终Flatten前的尺寸不是7x7从而与全连接层不匹配。3. 池化层的步长Stride对于MaxPooling2Dstrides参数默认等于pool_size。也就是说如果你只写了pool_size(2,2)Keras会默认strides(2,2)。虽然在这个例子中结果一样但显式地写出strides(2,2)是一个好习惯这能使你的意图更清晰避免未来API变更或阅读代码时产生误解。4. 全连接层前的Flatten操作这是一个至关重要的衔接层。卷积块输出的特征图是三维的例如(batch_size, 7, 7, 512)而全连接层Dense期望的输入是二维的(batch_size, features)。Flatten()层的作用就是将(7,7,512)直接拉平成(25088,)的一维向量。忘记添加Flatten()层是初学者最常见的错误之一会导致Keras抛出维度不匹配的错误。5. 关于Dropout和权重初始化细心的你可能会发现我们的实现代码和论文原文以及Keras官方实现相比缺少了Dropout层和特定的权重初始化如he_normal。这是有意为之。论文中提到在训练时前两个全连接层后面跟有Dropout层p0.5以防止过拟合。权重初始化也对训练稳定性至关重要。然而我们当前阶段的目标是复现网络架构即前向传播的路径而不是完全复现训练细节。Dropout和特定的初始化是在模型编译和训练时才需要关注的。在构建模型骨架时省略它们可以让结构更加清晰。当然你完全可以按照论文把它们加进去这会是很好的扩展练习。5. 模型验证、对比与深入探索5.1 使用model.summary()进行验证模型构建完成后我们如何知道它是否正确model.summary()方法是你的最佳朋友。它会打印出模型每一层的类型、输出形状和参数数量。# 构建模型 vgg16_model build_vgg16() # 打印模型摘要 vgg16_model.summary()输出结果会是一个详细的表格。你需要重点核对以下几点输出形状Output Shape 检查每个卷积层和池化层之后的特征图尺寸是否符合我们的计算尤其是经过5次池化后是否变为(None, 7, 7, 512)。参数数量Param # 核对每一层的参数量是否与我们手动计算的一致。重点检查第一个全连接层Dense的参数是否约为1.02亿102,764,544这能验证Flatten层的输入是否正确。总参数量Total params 模型的总参数应该接近1.38亿138,357,544。如果相差巨大说明架构实现有误。5.2 与Keras官方实现对比为了获得最权威的验证我们可以将亲手搭建的模型与Keras内置的VGG16进行对比。注意Keras内置的模型包含了ImageNet上预训练的权重并且其全连接层通常包含Dropout不过在summary()中Dropout层不显示参数。from tensorflow.keras.applications import VGG16 # 加载官方的VGG16模型不包括顶部的全连接层以便对比结构 official_vgg16 VGG16(include_topTrue, weightsNone, input_shape(224,224,3)) print( Keras Official VGG16 ) official_vgg16.summary() print(\n Our VGG16 From Scratch ) vgg16_model.summary()运行这段代码你会看到两个summary()输出。你需要逐层对比层类型和顺序 是否完全一致卷积、池化、全连接输出形状 每一层的输出是否相同参数数量 每一层的参数是否相同注意因为权重初始化方式可能不同summary()只显示结构参数数量这个数量应该完全一致如果所有层的输出形状和参数数量都匹配那么恭喜你你已经成功地从论文中复现了VGG16网络架构注意事项Keras官方VGG16的include_topTrue时最后一个全连接层是1000个单元但有时为了迁移学习我们会设置include_topFalse来移除这些全连接层。在我们对比时使用include_topTrue和weightsNone不加载预训练权重来确保结构一致。5.3 常见问题与排查技巧实录在实现和验证过程中你可能会遇到一些典型问题。下面是一个快速排查指南问题现象可能原因解决方案model.summary()中全连接层参数对不上Flatten层之前的特征图尺寸计算错误。回溯检查每个Conv2D和MaxPooling2D层的输出尺寸。确保所有卷积层paddingsamestride1所有池化层pool_size和strides均为2。构建模型时出现维度错误层与层之间的输入输出维度不匹配。仔细检查每一层的filters和kernel_size参数是否正确。使用model.summary()或print(model.output_shape)在中间层调试。参数量远小于预期可能漏掉了某些层或者filters数量设置错误。对照论文Table 1逐行检查代码确保没有遗漏任何卷积层。特别是VGG16有13个卷积层和3个全连接层。与官方模型对比时层数或类型不一致官方模型可能包含Input层、Dropout层或使用了不同的命名。忽略Input层它没有参数关注核心的卷积、池化、全连接层。Dropout层在推理时不生效且无参数不影响结构对比。运行代码时内存不足OOM尝试在前向传播时批处理batch过大或者模型本身很大。VGG16模型很大在CPU上即使做前向传播如果输入批量batch size太大也可能内存不足。尝试将batch_size设为1进行测试。一个实用的调试技巧如果你不确定某一步的输出形状可以构建一个子模型进行测试。例如你想看第二个池化层之后的输出from tensorflow.keras.models import Model # 假设vgg16_model是你构建的完整模型 # 创建一个新模型其输出为原模型第6层的输出根据summary()的索引 debug_model Model(inputsvgg16_model.input, outputsvgg16_model.layers[6].output) debug_model.summary() # 这会显示到该层为止的结构和最终输出形状5.4 超越架构复现下一步的探索方向成功复现网络架构只是一个开始。要真正让这个模型“活”起来还有更多有趣且富有挑战性的步骤1. 模型编译与虚拟训练即使不进行真实训练了解如何配置训练过程也是必要的。你可以尝试编译模型设置损失函数、优化器和评估指标。vgg16_model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy])这行代码告诉Keras如何训练模型。adam是常用的自适应学习率优化器categorical_crossentropy是多分类任务常用的损失函数。2. 添加论文中的训练细节根据论文Section 3.1尝试在对应的全连接层后添加Dropout层。model.add(layers.Dense(4096, activationrelu)) model.add(layers.Dropout(0.5)) # 添加Dropout丢弃率为0.5 model.add(layers.Dense(4096, activationrelu)) model.add(layers.Dropout(0.5)) model.add(layers.Dense(1000, activationsoftmax))同时论文使用了特定的权重初始化方法他们提到了“随机初始化”但现代实践中常用he_normal初始化配合ReLU。你可以在Conv2D和Dense层中通过kernel_initializerhe_normal参数来设置。3. 加载预训练权重进行特征提取或微调Fine-tuning这是VGG等经典模型在现代应用中最常见的用法。你可以加载在ImageNet上预训练好的权重移除顶部的分类头include_topFalse然后接上你自己的小型分类器例如一个全局平均池化层和一个10分类的Dense层在新的小数据集上进行微调。这能让你用很小的代价获得一个强大的图像特征提取器。base_model VGG16(weightsimagenet, include_topFalse, input_shape(224, 224, 3)) # 冻结基模型权重只训练新添加的层 base_model.trainable False x layers.GlobalAveragePooling2D()(base_model.output) x layers.Dense(256, activationrelu)(x) predictions layers.Dense(10, activationsoftmax)(x) # 假设你有10个新类别 new_model Model(inputsbase_model.input, outputspredictions) new_model.compile(...) # ... 然后在你的数据集上训练new_model4. 尝试实现更复杂的架构如果你已经吃透了VGG可以挑战一下同一时期另一篇著名的论文——Google的InceptionGoogLeNet。它的核心是“Inception模块”在同一层内并行使用不同尺寸的卷积核和池化然后合并结果。实现Inception模块会极大地加深你对CNN结构灵活性的理解。从阅读一篇论文开始到手动计算每一个参数再到用代码一行行地构建出模型最后通过对比验证其正确性——这个过程带给你的远不止一个可运行的VGG16模型。它培养的是一种严谨的工程实现思维和深入理解模型本质的能力。下一次当你再看到一篇新的架构论文比如ResNet, Transformer时你将不再畏惧因为你知道只要抓住输入输出、层间变换、参数计算这几个关键点你就有能力将它从纸面变为代码。这才是本次实践最大的价值。

相关新闻