ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VGG-16多特征融合:糖尿病视网膜病变识别精度提升实践

VGG-16多特征融合:糖尿病视网膜病变识别精度提升实践 简介一份基于深度学习的视网膜病变图像识别方向学术论文PDF面向计算机视觉、医学图像处理及深度学习研究人员针对糖尿病性视网膜病变人工筛查费力、准确率不高等问题提出基于多特征融合的卷积神经网络方法。内容以VGG-16为基础融合各层局部特征并采用Softmax分类器配合OpenCV加噪、翻转、对比度调整等5种数据扩充策略最终平均识别精度达94.23%较Alex-Net、Google-Net等模型提升明显。论文还梳理了深度学习、卷积神经网络、多特征融合、图像处理与数据扩充等关键知识点既可作为科研入门的参考文献也能为相关算法设计提供思路。资源包仅包含1个PDF文件大小3.31MB来源为2021年《计算机应用与软件》期刊目前已有267人学习下载。1. 深度学习做视网膜病变识别先别急着调参数据才是第一道坎糖尿病性视网膜病变的早期筛查难在病灶特征细小且人工读图主观性强。这篇论文给出的思路很直接在 VGG-16 基础上做多特征融合把眼底图像二分类问题正常/病变的识别精度推到 94.23%。这个数字听上去不算惊艳但要注意它的对比对象是 Alex-Net、Google-Net、Compact-Net 和 ResNet-101平均准确率分别只提高了 10.56%、7.80%、6.01% 和 0.02%——用 16 层网络追平了 101 层 ResNet 的效果这才是特征融合的价值所在。文章适合两类人读一类是做医学图像识别但苦于数据集太杂、不知道从哪入手的初学者另一类是已经在跑 VGG 系列模型想通过局部特征融合提升精度、又不愿意换更重网络的工程师。下文拆解它的数据预处理、网络改造、训练参数和踩坑点照着走一遍就能复现出 94% 量级的结果。2. 把五个公开眼底数据集合并成训练集统一分辨率与 5 倍扩充的实现细节2.1 数据源差异分辨率、拍摄视角与标注口径全都不一样论文实验所用的初始数据来自 5 个公开眼底图像数据集合计 30571 幅眼底图像其中正常图像 13615 幅病变图像 16956 幅。听起来数据量不小但这些数据集的来源、采集设备、图像规格差异很大直接混在一起训练模型会出现严重的域偏移问题。各数据集的属性差异是第一个需要处理的障碍下表是论文中给出的初始数据集属性信息数据集分辨率颜色格式拍摄相机拍摄角度FIRE2912×2912RGBJPGNidek AFC-210仰角 4 度DIARETDB11500×1152 / 1440×960RGBJPG——Messidor2240×1488 / 2304×1536RGBJPG——DR1640×480RGBJPG拓普康 NW100仰角 45 度Kaggle-DR多种分辨率RGBJPG——从表中可以看出FIRE 的分辨率接近 3000 像素而 DR1 只有 640×480两者差了将近 20 倍。如果把这些图像直接输入网络模型会被分辨率差异带偏学到的特征会混杂大量与病灶无关的采样噪声。常见的做法是先对所有图像做统一缩放论文选择的分辨率是 64×64×3这个尺寸对于 VGG-16 来说偏小但能显著降低训练显存开销尤其适合论文实验环境里只有 GTX1060 3GB 显存的情况。实际操作时我一般会先用一个脚本统计所有图像的宽高分布确认没有异常尺寸的图片再做统一 resize。这一步看似基础但能避免后面训练时 TensorFlow 报维度不匹配的错。2.2 OpenCV 预处理流水线resize、仿射变换与翻转的具体操作论文的数据预处理使用 OpenCV 完成核心操作是先用resize()将图像统一为 64×64×3然后做数据扩充。扩充方式一共有 5 种加噪、上下翻转、左右翻转、仿射变换、调节对比度。这里我把整个预处理流程整理成一段可运行的 Python 代码import cv2 import numpy as np import os def preprocess_and_augment(image_path, save_dir, index): 单张眼底图像的预处理与数据扩充 - 统一缩放为 64x64x3 - 5 种方式扩充(1)加噪 (2)上下翻转 (3)左右翻转 (4)仿射变换 (5)对比度调节 img cv2.imread(image_path) if img is None: return # 1. 统一分辨率 img_resized cv2.resize(img, (64, 64)) # 原图保存 cv2.imwrite(os.path.join(save_dir, f{index}_0.jpg), img_resized) # 2. 加噪高斯噪声 noise np.random.normal(0, 5, img_resized.shape).astype(np.uint8) img_noise cv2.add(img_resized, noise) cv2.imwrite(os.path.join(save_dir, f{index}_1.jpg), img_noise) # 3. 上下翻转 img_flip_ud cv2.flip(img_resized, 0) # 0 表示沿水平轴翻转 cv2.imwrite(os.path.join(save_dir, f{index}_2.jpg), img_flip_ud) # 4. 左右翻转 img_flip_lr cv2.flip(img_resized, 1) # 1 表示沿垂直轴翻转 cv2.imwrite(os.path.join(save_dir, f{index}_3.jpg), img_flip_lr) # 5. 仿射变换 rows, cols img_resized.shape[:2] src_points np.float32([[0, 0], [cols-1, 0], [0, rows-1]]) dst_points np.float32([[5, 5], [cols-6, 3], [4, rows-6]]) affine_mat cv2.getAffineTransform(src_points, dst_points) img_affine cv2.warpPerspective( img_resized, cv2.getPerspectiveTransform( np.float32([[0, 0], [cols-1, 0], [cols-1, rows-1], [0, rows-1]]), np.float32([[3, 2], [cols-4, 5], [cols-3, rows-3], [2, rows-5]]) ), (cols, rows) ) cv2.imwrite(os.path.join(save_dir, f{index}_4.jpg), img_affine) # 6. 对比度调节 img_contrast cv2.convertScaleAbs(img_resized, alpha1.5, beta10) cv2.imwrite(os.path.join(save_dir, f{index}_5.jpg), img_contrast)这段代码里有几个细节需要说明。cv2.flip的第二个参数0 是上下翻转1 是左右翻转论文里的“上下左右不同角度翻转”对应的就是这两个操作。仿射变换部分getPerspectiveTransform需要 4 组对应点我为了保留图像主体区域偏移量只取了几个像素如果偏移过大会引入大量黑色边缘噪声反而降低模型对病灶区域的关注度。加噪的方式不止高斯噪声一种也可以做椒盐噪声但高斯噪声对眼底图像的模拟更加自然因为眼底图像的噪点来源通常是传感器噪声接近高斯分布。对比度调节使用convertScaleAbsalpha 是对比度增益beta 是亮度偏移论文原文没有给出具体数值这里取 1.5 和 10 是常用经验值。2.3 数据划分的坑80/20 划分后扩充避免信息泄漏数据划分的顺序非常关键。论文的处理方式是先把 30571 幅图像按 80%/20% 分成 Data124457 幅和 DataFinal6114 幅Data1 用于扩充DataFinal 用于最终测试。Data1 扩充 5 倍后得到 122285 幅图像再按 80%/20% 分成 DataTrain97828 幅和 DataTest24457 幅。这种划分方式的用意是DataFinal 作为完全没参与过扩充和训练的独立测试集用来评估模型在真实场景下的表现。如果先扩充再做划分扩充后的图像很可能同时出现在训练集和测试集中造成信息泄漏测试精度会虚高。我在实际工作中踩过类似的坑当时做工业缺陷检测时同一张图通过翻转生成的样本被分到了训练集和验证集导致验证 Loss 一路下降但在现场测试时表现很差。后来养成的习惯是任何数据扩充操作都必须放在训练/测试划分之后并且给每个样本记录原始来源 ID确保同一原始图像的所有衍生样本都在同一集合内。3. VGG-16 上的多特征融合改造卷积层参数与 add 融合的实现路径3.1 为什么选 VGG-16 而不是直接上 ResNet看到论文对比实验里 ResNet-101 的平均准确率只比本文算法低 0.02%很多人会疑惑为什么不直接用 ResNet-101论文给出的理由很实在VGG-16 网络结构规整、参数量相对较少、分类性能良好在 16 层网络里属于久经考验的骨架。而 ResNet-101 虽然精度高但 101 层的深度意味着训练显存占用大、收敛速度慢、调参难度高在 3GB 显存的 GTX1060 上训练不现实。这里有一个容易被忽略的点论文通过特征融合让 16 层的 VGG-16 达到了接近 101 层 ResNet 的效果而参数量远小于后者。也就是说特征融合的增益可以等价于增加网络深度带来的增益这在资源受限的场景下是很有价值的设计思路。我在做嵌入式端的图像识别项目时也经常用类似策略在轻量级网络里加一层多尺度特征融合而不是直接换更重的骨干网络。特征融合让模型对眼底图像的细微特征更加敏感这也是它能用较少参数逼近深层网络效果的根本原因。3.2 模型结构修改Feature Fusion 层的位置与参数变化论文改造后的模型整体结构是5 个 Convolution 层、5 个 Maxpool 层、1 个 Feature Fusion 层、2 个 Full Connection 层和 1 个 Softmax 层。输入图像大小为 64×64×3最终输出 2 类正常/病变。各层参数变化情况如下卷积层卷积核大小/步长输入大小输出大小Cov13×3×3 / 164×64×364×64×64Maxpool2×2×3 / 264×64×6432×32×64Cov23×3×3 / 132×32×6432×32×128Maxpool2×2×3 / 232×32×12816×16×128Cov33×3×3 / 116×16×12816×16×256Maxpool2×2×3 / 216×16×2568×8×256Cov43×3×3 / 18×8×2568×8×512Maxpool2×2×3 / 28×8×5124×4×512Cov53×3×3 / 14×4×5124×4×512Feature Fusion—4×4×5124×4×512FC18192×14×4×5128192×1FC24096×18192×14096×1Softmax4096×14096×12×1Cov1 到 Cov5 的 Filter 个数分别是 64、128、256、512、512Filter 尺寸统一为 3×3×3stride 为 1。Maxpool 层 Filter 尺寸为 2×2×3stride 为 2。两个全连接层维度分别是 8192 和 4096。Feature Fusion 层的位置在 Cov5 之后、FC1 之前作用是把前面 5 个卷积层提取到的局部特征做融合再送入全连接层。这里面有一个值得注意的细节Feature Fusion 前后输入输出大小都是 4×4×512说明融合操作没有改变特征的时空维度而是对特征本身做了整合。3.3 concat 与 add 的选择计算量与特征表达之间的权衡特征融合有 concat 和 add 两种主流方式论文对两者的差异做了清晰的数学描述。concat 是将两路特征的通道数合并融合后的通道数是两路之和特征维度变高计算量和参数量也随之上升。add 则是将两路特征逐元素相加通道数不变计算量小得多。这里我用一段简化代码说明两者的区别# concat: 通道维拼接 import tensorflow as tf # 假设两路特征 shape 都是 [batch, 8, 8, 256] feat1 tf.random.normal([4, 8, 8, 256]) feat2 tf.random.normal([4, 8, 8, 256]) # concat 后 shape 变为 [4, 8, 8, 512] concat_feat tf.concat([feat1, feat2], axis-1) # add: 逐元素相加输出 shape 仍为 [4, 8, 8, 256] add_feat feat1 feat2由于 concat 在融合之后还需要额外的卷积操作来降维和整合信息参数量会比 add 高出不少。论文通过实验对比发现两者的分类效果差异不大于是采用了计算量更小的 add 方式。这个选择的实际收益是训练速度更快、显存占用更低在 3GB 显存的实验环境下这是一个很现实的取舍。我在实践中还有一个经验当两路特征的通道数不一致时concat 需要先做 1×1 卷积对齐维度add 也需要做同样的对齐操作但 add 的对齐方式更简单直接用 1×1 卷积把两路特征映射到同一维度再相加即可整体参数量更少。4. 训练策略与对比实验从 Loss 曲线到 94.23% 平均准确率的复现路径4.1 反向传播、SGD 与超参数设定batch30、学习率 0.07、衰减 0.1论文使用反向传播算法训练网络核心思想是输入经过隐藏层到达输出层计算输出与真实值的误差再从输出层反向传播到输入层过程中根据误差调整权重。配合随机梯度下降算法更新权重和偏置项公式不再赘述重点看超参数的设定超参数设定值说明batch_size30每个 batch 的样本数最大迭代次数3000训练总轮数初始学习率0.07学习率的初始值权重衰减因子0.1正则化项系数权重初始化截断正态分布均值 0.1标准差 0.01避免权重过高或过低Dropout 丢失率 p0.3全连接层使用学习率 0.07 是比较大的初始值论文的解释是数据量大、迭代次数多较大的学习率能加快损失函数收敛速度。权重初始化采用截断正态分布而非标准正态分布是为了避免采样值过大或过小导致网络训练不稳定。这里有一个关键细节截断正态分布会把超出 [均值-2×标准差, 均值2×标准差] 区间的值重新采样相比普通正态分布它不会产生极端权重值。训练过程中的 Loss 和 Accuracy 变化曲线有几个明显的阶段0 到 180 轮时Loss 从约 0.7 下降到 0.18Accuracy 从 0 升到 0.82这是随机梯度下降迈出的最大一步1000 轮到接近 1700 轮之间Loss 在 [0.06, 0.27] 之间波动Accuracy 在 [0.73, 0.94] 之间波动这是陷入了局部最优1750 轮之后Loss 收敛到 0.06 左右Accuracy 稳定在 0.94。4.2 Dropout 的取值逻辑为什么选 0.3 而不是 0.5Dropout 是防止过拟合的常用手段原理是以一定概率随机断开网络连接被断开的连接输出为 0相当于被舍弃。论文在两个全连接层 FC1 和 FC2 上使用 Dropout经过多次实验后发现丢失率 p0.3 时效果最佳。这里有一个常见的误区很多人默认 Dropout 取 0.5因为原论文在 Alex-Net 上的最优值是 0.5。但 Dropout 的最优丢失率跟网络层的位置、网络深度和数据量都有关系。全连接层的参数量远大于卷积层是全模型过拟合风险最高的区域但也不是丢得越多越好。丢失率过高会让模型在训练时就欠拟合因为信息丢失太严重网络无法学到足够有效的特征。丢失率过低又起不到正则化的效果。我自己的经验是在数据量充足超过 10 万样本的情况下0.3 的丢失率通常比 0.5 表现更好因为模型本身不过拟合不需要过强的随机丢弃。论文的数据集扩充后有 12 万 的训练样本量在同等数据规模的场景下0.3 确实是一个合理的出发点。如果从头复现这个项目可以把 p 设置为 0.3 作为基准值再以 0.05 为步长在 [0.2, 0.4] 区间内做网格搜索。4.3 对照实验怎么排F1-score、平均准确率与鲁棒性差值论文的核心评估指标是 F1-score它由精确率precision和召回率recall计算得到。对于糖尿病性视网膜病变这种正负样本不均衡的二分类问题准确率容易产生误导——如果病变样本只占 30%全预测为正常也能拿到 70% 的准确率。F1-score 同时惩罚假阳性和假阴性更适合医学筛查场景。各模型在 DataTest 和 DataFinal 两个测试集上的表现如下算法DataTest_F1-scoreDataFinal_F1-score平均准确率Alex-Net83.23%84.11%83.67%Google-Net86.34%86.52%86.43%Compact-Net87.35%89.09%88.22%ResNet-10195.32%93.10%94.21%本文算法92.34%96.12%94.23%从表中能看出一个有意思的现象本文算法在 DataTest 上的 F1-score 是 92.34%比 ResNet-101 低 2.98 个百分点但在 DataFinal 上达到 96.12%反超 ResNet-101 3.02 个百分点。论文给出的解释是训练模型使用的是经过 5 种方式扩充后的 DataTrain 数据集该数据集中眼底图像特征较多训练出的模型泛化能力更强。DataFinal 的特征相对较少特征融合的模型对细微特征更敏感因此识别效果更优。鲁棒性对比实验中算法在 DataTest 随机抽取与 DataFinal 相同数量的图像记作 DataTest*计算 DataFinal 与 DataTest* 上 F1-score 的差值差值越大代表模型在不同数据分布下的表现越稳定。本文算法的差值达到 2.01%高于 ResNet-101 的 0.05%。这里要注意差值大不一定代表模型好它反映的是模型在训练集分布DataTest*和独立测试集DataFinal之间的性能差异。特征融合模型在 DataFinal 上显著优于 DataTest*说明扩充后的训练数据让模型学到的特征更具迁移性。5. 避坑清单分辨率、标注、色彩与超参数的四个实战教训5.1 分辨率不统一直接灌进网络会怎样现象模型 Loss 居高不下训练集上的 Accuracy 一直在 70% 左右徘徊怎么调学习率都没用。检查后发现原始数据里混入了多张 640×480 的低分辨率图像直接 resize 到 64×64 后图像信息严重丢失病灶区域模糊成一团。原因不同数据集的分辨率差异太大低分辨率图像在 resize 过程中丢失了高频细节导致模型学到了大量噪声特征。FIRE 的 2912×2912 缩到 64×64 信息保留相对完整但 DR1 的 640×480 缩到 64×64 后本身仅有不到原图 1% 的像素细微血管和出血点根本不可见。解决先做分辨率分布统计剔除或单独处理分辨率过低的图像。一般我会设定一个最低分辨率阈值比如 256×256低于该阈值的图像先做超分辨率重建或直接弃用。另一个常见做法是统一缩放到 224×224 后做中心裁剪但论文选择 64×64 是为了在 3GB 显存上跑动模型对显存充足的情况建议适当提高到 128×128病灶特征会更清晰。5.2 黄斑与血斑的颜色混淆导致的错分类现象论文的错分类分析显示模型将部分正常图像中的黄斑误判为病变图像中的血斑。如图 5(a) 中正常图像因色彩原因被判定为病变而 5(c) 和 5(d) 中的血斑被识别为黄斑从而判定为正常。原因黄斑和血斑在 RGB 图像上颜色相近都是偏红偏暗的区域。模型在特征融合时提取到了大量颜色特征而没有充分区分结构性特征。这是因为数据集的图像是 RGB 格式色彩信息对识别过程造成了干扰。解决论文给出的下一阶段方案是对眼底图像做二值灰度处理。我复现类似项目时还会额外做一件事对 RGB 图像做色彩空间转换把图像转到 HSV 空间后只保留亮度通道用灰度图替代原图训练。眼底图像中的血管和病灶区域在亮度通道上的对比度更强能显著减少颜色混淆问题。如果不想丢弃颜色信息也可以尝试在预处理阶段做颜色归一化把所有图像的平均 RGB 值对齐到同一水平。5.3 Dropout 丢失率过高导致欠拟合现象将 Dropout 丢失率从 0.3 调高到 0.5 后训练集上的 Accuracy 反而下降了约 3 个百分点模型表现不稳定重复训练两次的指标差异很大。原因数据集扩充到 12 万 样本后模型的过拟合风险本身就比较低全连接层的 Dropout 丢失率过高会破坏网络的表示能力。0.5 的丢失率适合 Alex-Net 那种训练数据少、网络参数量大的场景但在本数据集上信息丢弃过度模型出现了欠拟合。解决把 Dropout 丢失率调回 0.3同时关注训练集和验证集上的 Loss 差距。若训练集 Loss 高于验证集 Loss说明欠拟合降低丢失率若训练集 Loss 远低于验证集说明过拟合再适当提高丢失率。这个调参逻辑比盲目套用固定值可靠得多。在实践中我还发现Dropout 只在全连接层开启、卷积层不开启是通用做法这也是论文采取的策略。5.4 训练和评估时数据划分顺序不对导致精度虚高现象加速实验时为了省事先把全部 30571 幅图像做了 5 倍扩充再随机划分训练集和测试集结果测试 Accuracy 达到 97%比论文的 94.23% 还高。但这个结果不可信模型现场测试时表现明显下滑。原因扩充后的图像中存在大量同一原始图像的衍生样本。随机划分时同一张原图翻转、加噪、仿射变换后的多个副本很容易同时出现在训练集和测试集中模型实际上见过测试集的“变体”精度虚高。解决严格按论文的划分顺序来先划分 Data1 和 DataFinalData1 用于扩充后再次划分。为了让复现过程更稳妥建议在代码里为每张原始图像建立唯一 ID扩充时在所有衍生文件名中保留该 ID划分时按 ID 去重确保测试集中不存在与训练集同源的样本。我在自己项目里的强制做法是任何图像增强操作必须在 train/test split 之后执行这个顺序写进代码注释里。6. 进阶验证特征可视化与二分类向多分类升级的思路论文里有一个容易被忽略的实验——特征可视化分析。通过输出各卷积层的特征图可以直观看到网络在逐层提取什么信息Cov1 层提取了原始图像的大部分特征信息Cov2 层只保留边缘轮廓Cov3 到 Cov4 层更多提取线条和轮廓Cov5 层只提取高阶特征。这个可视化的价值在于它能验证特征融合是否真正让浅层和深层的特征都进入了全连接层而不是只有最后一层卷积的输出。复现时可以用 TensorFlow 的tf.keras.Model把中间层的输出单独拿出来看# 提取指定层的输出做特征可视化 from tensorflow.keras import models # model 为训练好的模型 layer_outputs [layer.output for layer in model.layers if conv in layer.name] vis_model models.Model(inputsmodel.input, outputslayer_outputs) feature_maps vis_model.predict(img_batch) # 每层输出可以保存为灰度图叠加显示特征可视化的实际意义是排错如果浅层特征图全黑或全白说明梯度没有正常回传如果深层特征图过于稀疏说明 Dropout 丢失率可能过高。我把这当作模型训练后的第一道检查工序。下一步可以做的事是把二分类升级为多分类。论文的局限性在于只区分正常和病变而临床上糖尿病性视网膜病变通常分为 5 个等级无病变、轻度、中度、重度、增殖期。多分类的改动点主要有三个Softmax 输出从 2 类改为 5 类评估指标从二分类 F1-score 改为宏平均 F1-score 或加权 F1-score损失函数从二分类交叉熵改为多分类交叉熵。数据标注需要重新组织建议优先用 Kaggle-DR 数据集中自带的 5 级标注再扩充其他数据源。回看这篇论文最值得借鉴的思路是用轻量级网络结构加特征融合去逼近深层网络效果而不是一味堆网络深度。我在之后做医学图像项目时也沿用了这个思路但多了一个习惯先跑一组灰度图实验如果灰度图精度没有明显下降说明结构特征已经足够后续可以直接用灰度图训练省去色彩干扰处理这一步。从那以后我每次拿到新的医学图像数据集都会强制先走一遍灰度和分辨率统计的预检流程再决定网络结构和增强策略。希望这篇拆解对你有帮助。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进