资讯动态

Keras实战:VGG16特征图可视化,看懂卷积层如何逐层提取特征

发布时间:2026/10/14 8:43:32 来源:尧图企业网站定制
确实对很多学深度学习的朋友来说VGG16是绕不过去的经典模型。但“能用Keras加载VGG16做分类”和“真正理解VGG16为什么这么设计”中间隔着一道巨大的鸿沟。模型内部几十个卷积层、上千个通道在你眼里如果只是一串数字在矩阵里翻滚那后面调参、迁移学习、改网络结构都会很吃力。特征图可视化就是把模型内部那堆“焦虑的数学”变成一张张肉眼能分析的图像直接看看每一层到底关注了什么。这篇总结整理了我用Keras对VGG16做特征图可视化的完整实现过程包括层输出的提取逻辑、特征图的归一化处理、不同深度层的对比解读以及我实际跑代码时踩过的坑。无论是刚入门CNN的新手还是想给模型写“检查报告”的老手这篇都值得收藏。1. 为什么要对VGG16做特征图可视化1.1 卷积层到底在学什么VGG16是2014年左右提出的深度卷积网络结构核心思路非常朴素用3x3小卷积核堆叠深度靠16层的深度换表达能力。但它内部每层具体在干嘛很多人其实并不清楚。我给你打个比方VGG16的浅层卷积层像是一个刚进公司的实习生看到任何东西先找边缘、找线条、找颜色突变的地方中层Conv层像是一个干了两三年的熟手开始懂得把边缘组合成纹理比如毛发方向、轮毂弧度、窗户格子到了深层它已经变成老师傅看到的是“眼睛”“车灯”“腿”这类完整的语义部件。这整个过程就是视觉信息从像素逐级抽象成类别特征的过程。但问题是这些“边缘”“纹理”“语义部件”在模型里存的是什么不过是一张张二维的数值矩阵。每个通道的每个位置记录的是这个卷积核扫描输入时产生的激活响应。数值越大表示该位置和这个卷积核想要的“模式”越匹配。我们把这些矩阵画成热图就能直观看到每个卷积核关注输入的什么位置、什么形状。1.2 可视化帮你解决了哪些实际问题我用特征图可视化解决过几个实实在在的痛点不是附庸风雅地“看看好看”。第一类是模型诊断。比如你发现VGG16在某个数据集上分类效果不好但你不知道问题出在哪个楼层。是浅层根本没提取到有效边缘还是中层纹理信息不够还是深层语义混在一起了特征图可视化能一层一层排查把问题定位到具体层比盲目改学习率、换优化器靠谱得多。第二类是特征复用判断。做迁移学习时你要决定冻结VGG16的前多少层。通过看不同层特征图的质量就能判断这个层次的特征对当前任务是否通用。如果浅层特征图信息丰富、结构清晰说明它学到了通用视觉特征适合作为迁移特征的一部分。第三类是对模型“信任”的建立。训练好的模型光看测试集准确率心里总有点不放心。看到block5最后一层卷积的特征图已经在狗图上激活出类似“头部区域”的模式你会比较踏实知道它确实学到了语义信息而不是纯粹靠像素记忆。说白了特征图可视化是少数能把“模型内部的思考过程”变成人可以审查的东西的技术手段不靠猜。1.3 方案选型为什么是Keras加VGG16现在深度学习框架五花八门PyTorch、TensorFlow、JAX都有人用但我这次选择Keras核心原因是Keras在“中间层输出与可视化”这件事上实在太顺手了。让我给你对比一下。在PyTorch里提取中间层特征你需要在forward里注册hook或者自己改forward函数把中间层输出保存下来。这是一种还挺“侵入”的做法因为你得动模型逻辑。而Keras的Model是函数式的天然支持从一个输入节点拿到任意层的输出。你只需要用Model(inputs..., outputs...)重新包一层中间层特征就直接成了新模型的输出干净利落不需要改模型内部结构。至于选VGG16而非ResNet或者Inception我自己的考量是这样VGG16结构非常规整所有卷积都是3x3、stride1、same padding通道数倍数关系也清晰64、128、256、512特征图尺寸变化规律好讲。对于可视化教学或者理解CNN原理VGG16是最适合的“教材级”模型。ResNet有shortcut结构特征图里是残差叠加后的结果新手容易误解成“这怎么还带横线的”而VGG16没有这些花活它的每一层输出就是纯粹的卷积加激活干净也更容易讲清楚。当然如果你要部署或追求极致精度VGG16笨重、参数量大约1.38亿参数不是最优解。但如果是理解、迁移、可视化VGG16是首选。2. 动手前必须搞懂的几个关键细节2.1 VGG16对输入数据有一些“小脾气”做特征图可视化之前很多人直接把图片resize到224x224就扔进模型出来的图全是黑的或者乱七八糟的第一反应是模型坏了。其实大概率是预处理没做对。Keras里VGG16预训练模型使用的预处理函数是preprocess_input它内部做了两件事。第一件事是通道反转把图片从RGB顺序转成BGR。这是因为VGG16在训练时数据加载沿用了早期Caffe时代读图的习惯图片矩阵在送入网络之前已经变成了BGR排列所以Keras的VGG16也就保留了这个约定。你用OpenCV读图本来就是BGR用matplotlib读的图是RGB结果完全不一样。第二件事是减均值。模型训练时ImageNet数据集的各通道均值大略是B通道103.939、G通道116.779、R通道123.68。训练的过程从某种程度上可以理解为模型在拟合“像素减去均值之后的分布”一套接近训练数据的统计分布预测时效果才好。preprocess_input会把每个像素减去对应的均值。顺手说一下这里减均值顺序要保持和BGR反转一致这也是我经常见到有人搞混的地方。如果用preprocess_input就没必要自己手动调通道它会一次处理到位。2.2 模型重构从分类器到特征提取器VGG16原本的Model(inputs, outputs)输出是1000类的softmax概率但我们不想要分类结果我们要的是某层卷积层的输出。我见过一些人用“偷梁换柱”的办法可视化直接拿model.layers[idx].output当新输出但是自己写for循环把每一层的结果单独跑一遍。这个办法可以跑通但是非常不高效而且容易出错。正确思路是构建一个全新的Model输入沿用原模型的输入输出指向目标层。这样新模型的目标输出就是该层的特征图并且前向过程只跑一遍。这个方案可行得益于Keras的函数式API。你不需要定义新的神经网络层结构只需要像搭积木一样把已有模型的中间层“接出来”就能得到一个特征提取器。这里有一个细节值得注意如果你需要同时可视化多个层比如block1_conv1和block5_conv3不要分别建两个模型而是在一个Model里让outputs是一个列表同时输出多个层的结果。这样一次前向过程就能拿回所有层的数据省时间单独建几个模型跑多次前向每次都重新计算前面的层浪费算力对CPU用户尤其不友好。2.3 特征图的形状与维度——别把shape搞错了Keras中卷积层输出的特征图数据和图像格式还不一样它有自己的维度顺序。默认情况下一张图片经过卷积层后输出的张量形状是(1, height, width, channels)其中1是批次维度batch size。初次可视化的人最容易在这里栽跟头把特征图数组直接拿去画图结果提示Invalid shape或者画出来是一张颜色怪异的“照片”。原因就是你是拿(1, 224, 224, 64)这种四维数组去画二维图像的画图库不知道你想表达什么。正确操作是先取出第一个批次的内容也就是features[0]得到(224, 224, 64)表示高224、宽224、64个通道。然后逐通道取出[..., i]这是单通道的二维矩阵再画图。另外特征图的数值范围不是0到255而是经过卷积加ReLU激活之后的响应值通常在0到几十或者几百不等不同层差异很大。直接拿原始数值画图浅层可能一片亮白因为最大值太大把动态范围压扁了深层可能一片死黑因为响应太稀疏。所以我一般在画图前对每个通道做一次min-max归一化把数值拉伸到0-1之间再给imshow设置cmapjet之类颜色映射出来的效果才会有层次感。这里再强调一点所谓“特征图”是同一个卷积核也就是同一个通道在整个图片上各位置的响应而不是一个通道对应整张图像的区域划分。你需要58个特征图表示有58个不同模式的卷积核它们在全图上各自扫描了一份响应图。3. 完整实现特征图提取与可视化3.1 环境与依赖库准备我的环境基本是深度学习标配Python 3.7及以上TensorFlow 2.xKeras已经作为内置模块包含在TensorFlow中numpy、matplotlib可选Pillow要确认Keras和TensorFlow版本兼容建议直接安装tensorflow然后用from tensorflow import keras导入比单独用老版本Keras报错的概率小很多。安装依赖的命令很简单我用的是镜像源安装速度还可以pip install tensorflow2.10.0 numpy matplotlib pillow这里顶多提一句如果你的机器有NVIDIA显卡并且装好了CUDAGPU推理会比CPU快不少如果没有也没有关系VGG16对单张图做一次前向在CPU上也就是几秒钟的事不会等太久。3.2 核心代码加载模型与可视化函数先说写博客环境下的代码骨架我把完整过程拆开解释。第一步加载预训练模型。from tensorflow.keras.applications.vgg16 import VGG16 # 加载带ImageNet权重的模型包括全连接层 model VGG16(weightsimagenet)这里说明一下VGG16默认include_topTrue包含最后的全连接层和softmax层。虽然我们可视化用不到全连接层但保留它不影响中间层输出而且加载完整权重更省事如果只想用卷积部分也可以设include_topFalse权重会缺失全连接部分但卷积部分完全够用。首次运行会从网络下载约几百兆的权重文件到本地缓存网络环境一般的朋友可能要等一下后面就会直接走缓存。第二步构建从指定层输出的新模型。from tensorflow.keras import Model layer_dict dict([(layer.name, layer) for layer in model.layers]) # 选择你关注的层 target_layers [ block1_conv1, block2_conv1, block3_conv3, block4_conv3, block5_conv3, ] outputs [model.get_layer(name).output for name in target_layers] visual_model Model(inputsmodel.input, outputsoutputs)这里我选了5个不同语义阶段的卷积层能很完整地看到信息从边缘到语义的演变过程。如果只想看一层灵活取一个即可。第三步读取和预处理图片。from tensorflow.keras.preprocessing import image from tensorflow.keras.applications.vgg16 import preprocess_input img_path demo_dog.jpg img image.load_img(img_path, target_size(224, 224)) x image.img_to_array(img) x np.expand_dims(x, axis0) # 创建batch维度 x preprocess_input(x)这里最关键的就是preprocess_input。前文说了它负责BGR转换和减均值。第四步执行前向计算得到特征图。feature_maps visual_model.predict(x)因为visual_model的输出是列表形式feature_maps也是一个列表每个元素对应一个目标层的输出。第五步定义一个可视化绘制函数。import matplotlib.pyplot as plt import numpy as np def show_feature_maps(feature_maps, save_path, columns8, dpi120): # feature_maps 形状 (1, H, W, C) feature_maps feature_maps[0] num_channels feature_maps.shape[-1] rows (num_channels columns - 1) // columns fig, axes plt.subplots(rows, columns, figsize(columns * 2, rows * 2)) for i in range(num_channels): ax axes[i // columns, i % columns] fm feature_maps[:, :, i] # 归一化到 0~1 fm_min fm.min() fm_max fm.max() if fm_max - fm_min 1e-8: fm (fm - fm_min) / (fm_max - fm_min) else: fm np.zeros_like(fm) ax.imshow(fm, cmapjet) ax.axis(off) ax.set_title(fch {i}, fontsize8) # 隐藏多余的子图 total rows * columns for i in range(num_channels, total): ax axes[i // columns, i % columns] ax.axis(off) plt.tight_layout() plt.savefig(save_path, dpidpi) plt.show()这个函数有一个容易被忽略的细节有些通道可能全零或者几乎全零这时候max - min等于0直接归一化会导致除零。我给了一个保护条件遇到这种通道直接显示为全黑不报错。这个保护在实际跑的时候很有用因为VGG16深层有大量稀疏通道甚至确实存在死通道。第六步逐层调用。for i, layer_name in enumerate(target_layers): show_feature_maps( feature_maps[i], save_pathffeature_map_{layer_name}.png, columns8 )到这里你就拿到了VGG16不同层级的特征图可以直观地看图说话了。3.3 看一张图片经过各层的“肉眼变化”我在测试时用的是一张带明显轮廓的狗的照片背景简单目标大而居中。第一层特征图block1_conv1的64个通道里可以明显看到有些通道只对水平线条有响应对应狗的背脊、地面边缘有些通道只对竖直物体有响应对应桌腿、植物茎干还有一些通道对强颜色反差敏感。整体图的特点是结构非常接近原图的边缘轮廓但同时带有大量细节和颗粒感。到block3_conv3特征图开始变得抽象有些通道上狗的整个轮廓已经糊成一团出现了大面积激活区域有些通道会单独激活狗腿、耳朵等部位。你大致能猜出某个通道可能是在寻找某些纹理的排列方式比如毛发的纹理方向。到block5_conv3这是最后一层卷积也是我建议你重点看的层。它的512个通道里大多数通道都只对某类语义部件响应有的通道只在狗的头部区域亮有的通道只在四条腿位置亮还有的通道只关注画面下方区域的整体形状。信息已经高度浓缩空间分辨率降到14x14但每个点的语义含义很强。这种变化过程完美说明了CNN的信息流浅层保留空间细节深层保留语义信息空间细节和语义表达此消彼长。3.4 进阶玩法显示单个“神经元”的偏好模式特征图一次性画几十张图每张图很小看不出每个通道到底“喜欢”什么。有一个进阶操作我觉得很有价值对某层所有通道的特征图统计每个通道的全局平均激活值然后挑出激活值最大的几个通道单独放大画出来。这个做法相当于在问给定这张输入图像哪个卷积核被激活得最强烈它在哪块区域被强烈激活这能帮你看清楚某个通道对应的具体形状模式。def top_active_channels(feature_map, k8): # feature_map shape (H, W, C) channel_mean feature_map.mean(axis(0, 1)) top_idx np.argsort(channel_mean)[::-1][:k] for idx in top_idx: fm feature_map[:, :, idx] fm_min, fm_max fm.min(), fm.max() fm (fm - fm_min) / (fm_max - fm_min 1e-8) plt.figure() plt.imshow(fm, cmapviridis) plt.title(fchannel {idx}, mean_active{channel_mean[idx]:.2f}) plt.axis(off) plt.show()有的通道激活最大值的中心点正对着图片里狗狗眼睛的位置你就知道这个通道已经在响应某种“眼睛状”的局部模式了。虽然它不是专门训练来认眼睛的但通过层层组合它确实发展出了对这类模式的敏感度这就是深度网络形成特征层次结构的证据。4. 从结果里读出VGG16的语义层次4.1 浅层特征边缘、线条与颜色block1_conv1和block1_conv2产出的特征图尺寸仍是224x224跟输入图大小一致。它们的特征图非常容易解读因为基本上就是对原图做了边缘检测、颜色对比检测这些底层操作。具体在看图时建议你注意几点一是感受野小。3x3卷积每次只能覆盖输入中很小的一个邻域所以每个像素的响应都只跟附近几个像素相关特征图看起来和原图有种“局部对应”的关系。看在某个位置亮起你完全可以拿它对照原图同一位置有什么边缘或颜色。二是通道分工明确。有的通道专门激活横向边缘、有的专门激活竖向边缘、有的专门激活红色和绿色交界。这种分工是训练过程中自然涌现的没有任何人手工指定它这样做这是让我觉得convnet“有那么点智能”的地方。三是浅层特征图基本不体现“物体类别”的信息。不管输入的是狗还是船只要边缘轮廓相似浅层特征图就很相似。这是好事说明浅层特征通用性强这也是迁移学习可以冻结前几层的原因之一。4.2 中层特征纹理逐步成形看block2和block3的特征图我的感觉是模型开始产生“纹理概念”了。比如在block3_conv3的一张特征图上狗的毛发区域整体激活成了一片有方向感的纹理带另一张特征图上背景的草地呈现出某种规则分布的激活。这些通道已经能把原图中的某种材质分离出来。对这种中层特征的理解可以参照图像纹理分析里的经典方法局部二值模式、Gabor滤波等。VGG16的中层卷积核本质上是在学习一种比简单边缘复杂的局部模式组合。如果你对“CNN可解释性”有进一步了解的欲望可以从这里入手中层特征的通道往往对特定的纹理方向、频率、重复性敏感。有个比较反直觉的观察某些通道的特征图光看外观看不出“这是什么物体的什么部位”但是它们对最终分类的贡献很大。这说明模型的分层特征表达并没有强制做到每层都能被人类直观理解有些中间表达更接近“有效计算”而非“可解释”。4.3 深层特征语义组件与分类线索到block4和block5特征图的空间分辨率降到了28x28甚至14x14但个别通道的“语义指向性”变得非常明显。我们做可视化时经常看到一个通道对应一大片连续的激活区域例如在狗图的头部、在车的轮子上方、在鸟的翅膀位置。它不是细节纹理而是对“这个位置有某个部件”这种抽象概念做出的响应。这里要提醒你不要误解深层特征图就是把原图不同区域做语义分割。VGG16没有显式训练来做分割这些激活只是为分类服务的副产品。但正因为它最后是拿全局平均池化或者flatten接全连接分类那些对分类有用的区域会被强化于是深层特征图上就呈现出类似“注意区域”的性质。有一个常见的可视化误区就是把深层特征图的激活区域直接等同于模型“注意力”。实际上特征图高响应只代表该卷积核在此处找到它偏好的模式而分类注意力通常由后续全连接层加权组合。视觉上相似但它俩不是一回事。4.4 VGG16各层特征图的尺寸与语义对照表聊了这么多我用一张表梳理一下VGG16卷积部分的层次结构、输出尺寸以及每层的语义阶段方便你写报告或者做演示时直接参考。层级范围Keras中的典型层名输出尺寸高x宽x通道数语义阶段第一卷积块block1_conv1 / block1_conv2224 x 224 x 64边缘、线条、颜色变化第二卷积块block2_conv1 / block2_conv2112 x 112 x 128纹理、局部图案第三卷积块block3_conv1 / block3_conv2 / block3_conv356 x 56 x 256纹理组合、物体局部轮廓第四卷积块block4_conv1 / block4_conv2 / block4_conv328 x 28 x 512物体部件、语义组件第五卷积块block5_conv1 / block5_conv2 / block5_conv314 x 14 x 512类别级语义区分性信息这张表非常有价值。你不需要跑代码光根据输出尺寸变化就能理解VGG16是如何一步步把224x224的图片压缩成14x14的“语义图”的。深层的14x14虽然分辨率很低但每个位置所含的信息抽象程度极高。5. 常见问题与排查技巧5.1 特征图全黑或全白这个是出现频率最高的问题甚至说十个做特征图可视化的人有八个人会遇到。全黑一般有两种原因。第一种是数值范围问题深层特征中大部分值很小少数通道出现大值如果你不做归一化直接画imshow会把这些小值当作黑色于是整张图黑得彻底。解决方法就是我前面代码里写的min-max归一化。第二种原因是激活稀疏问题有些通道在特定输入下就是没有强响应这种情况不用太担心属于正常现象换一张输入图或者看其他通道即可。全白一般也有两个原因。要么是归一化时出现了除零或者极小值你把值全部乘了一个超大倍率要么就是你误把特征图当作图像直接硬编码uint8导致数据截断。排查的时候别上来就怀疑模型先打印一下特征图的数值区间print(feature_map.min(), feature_map.max(), feature_map.mean())看到数值范围基本心里就有数了。5.2 通道数量太多排版乱内存占用高VGG16的block5层有512个通道如果全部画在一个大图上子图尺寸会非常小根本看不清。我建议的做法有这么几个一是只挑有代表性的通道做密集展示比如按最大激活值排序取前24个或48个。二是将自动画图函数中的columns参数调小一点让每张子图更大。三是如果内存吃紧不需要一次性把所有特征图取回来可以在visual_model里只取目标层的一个处理完保存后再取下一个。另外对于深层特征因为14x14的图本身就很小如果放大到可视化尺寸插值会让细节虚化。我建议保存图片时设高一点dpi比如150到200方便后续仔细看。5.3 模型输出尺寸和预期对不上这也是新手很容易遇到的问题。比如你想看block1_conv1的输出却发现得到的张量形状是(1, 224, 224, 64)但你以为会是(224, 224, 64)。这不是Bug而是因为Keras模型中数据总是带batch维度的。还有一种情况是某些教程里用了老式Keras写法输出的顺序可能是(1, 64, 224, 224)channels_first这个跟你的数据格式配置有关。现在TensorFlow默认是channels_last即通道在后面。遇到尺寸不对时你只需要根据K.image_data_format()确认一下当前的格式然后相应地reshape或transpose即可。5.4 分类准确率高但特征图看起来“乱”有人会问模型在ImageNet上top-5准确率那么高为什么block4或者block5的特征图看起来毫无规律、甚至像噪声这里要理解一个核心概念特征的“稀疏性”。深层卷积核设计出来是为了捕捉特定语义部件的但并不是每一张输入图都包含所有这些部件。比如一张图片里没有狗腿那么专门响应狗腿形状的卷积核就会完全不被激活它对应的特征图就是一团噪声或全黑。另外有些卷积核学到的是组合型特征比如“在一个大的平坦区域边缘出现一条细长亮线”这种模式不好用人类的语言描述看起来自然就“乱”。这种乱不是bug反而是特征表达丰富性的体现。如果你希望特征图看起来“更好懂”可以换一种思路用多个不同的输入图片做对比找到每个通道的“最爱”。比如给模型展示100张不同图片统计每个通道的平均激活值然后在激活最强的图片上回看该通道的特征图这就叫“最大激活样本分析”是解释单个通道语义的常用手段。6. 踩坑心得与进一步扩展6.1 实操中那些容易让你烦躁的细节VGG16权重文件比较大首次下载容易中断。我的做法是先手动下载权重到用户目录下的Keras缓存目录这样后续加载不会卡在联网环节。另外注意不同Keras版本权重文件名可能不同如果自定义下载过需要手动指定weights_path。CPU跑多张图确实慢但是也有优化空间。我这里给出的visual_model.predict(x)默认是单张图输入如果你想一次性跑整个验证集可以改成传一个batch的输入比如64张图一次除理速度会快很多。不过我建议先跑一张图把结果看明白再决定要不要批量。绘制特征图时我强烈建议使用cmapjet或viridis这类伪彩色映射。灰度图虽然显示数值分布“原生”但人眼对彩色梯度更敏感尤其是深层特征图灰度图会显得白茫茫一片很难看出激活差异。如果你要发论文用灰度更正式如果是自己分析用伪彩色体验更好。6.2 特征图可视化与CAM类热力图的区别很多朋友容易把这篇讲的特征图可视化和CAM、Grad-CAM这类“类激活热力图”混在一起实际上它们完全不同。特征图可视化展示的是网络中间层所有通道的响应不依赖类别标签。它回答的问题是“这个卷积核在图片上找到了什么模式”。而CAM类方法是拿全连接层的权重对最后一层特征图做加权求和生成一张针对某个特定类别的热力图。它回答的问题是“为了判断图片属于猫这一类模型主要看了哪些区域”。如果你想要的是对分类决策的解释那么应该做Grad-CAM如果你想观察网络内部特征逐层变化那么今天的特征图可视化就是正确路径。这两者可以结合使用先看特征图了解网络在哪里编码信息再做Grad-CAM看到分类时模型“眼神聚焦”的地方。6.3 这个内容还能怎么扩展顺着特征图可视化这条路至少还有三个方向值得继续玩。第一个方向是特征图的统计与分析。不只画图而是把每层特征图计算一些统计量平均激活、稀疏度、通道间的相似度。这些指标能帮你定量评估网络在不同任务上的特征质量。第二个方向是做“特征图的可视化监测面板”。搭建一个脚本任意给一张图自动打印所有卷积层特征图的统计和缩略图做成一个类似“模型体检”的工具。我在实际做模型迭代时就保留了一个这样的面板脚本非常方便。第三个方向是结合风格迁移或者生成模型。VGG16的浅层特征图保存了内容结构深层特征图保存了语义风格迁移算法正是利用这两层特性的差异实现“内容保留、风格替换”。理解了特征图的可视化结构你再看风格迁移的原理就会顺畅很多。就我个人体会而言特征图可视化是性价比很高的学习方法。你不需要去啃复杂的数学证明只需要跑一次代码、盯几分钟特征图就会对卷积网络产生完全不同的直觉。建议你拿一张自己拍的图片把上面代码原样跑一遍亲眼看一看VGG16每一层都发生了什么那种“原来如此”的感觉比读十篇理论文章都来得实在。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑