ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

卷积神经网络改进实战:损失函数与小波散射特征融合

卷积神经网络改进实战:损失函数与小波散射特征融合 简介这份PDF文档面向深度学习与计算机视觉方向的研究生、算法工程师及科研人员聚焦卷积神经网络的理论改进与跨领域应用。内容系统梳理了CNN的结构原理、训练方法与LeNet-5等经典模型并针对传统损失函数的局限引入Triplet Network正则约束提出改进型卷积神经网络算法在手写字符识别任务上验证了有效性同时将CNN提取的特征与小波散射网络特征通过向量拼接融合应用于图像检索场景为特征表示与检索精度提升提供了可参考的思路。资源包共1个PDF文件约1.45MB内容涵盖摘要、绪论、理论背景、改进方法与实验验证等完整章节结构规范适合作为课题入门、算法复现或论文写作的参考资料。目前已有234人学习对希望深入理解CNN改进策略与多特征融合检索方法的读者具有一定参考价值。1. 卷积神经网络的改进及其应用从损失函数到小波散射的落地路线很多人第一次接触卷积神经网络是从一张结构图开始的卷积层、池化层、全连接层背下来就能应付考试。但真到动手做一个图像检索或者分类任务时问题立刻变成为什么我的模型训不动为什么换个损失函数效果差这么多为什么数据一加噪声就崩这些问题的答案往往不在网络结构本身而在「改进」二字上——损失函数怎么选、特征怎么增强、训练策略怎么调。卷积神经网络的改进及其应用说的就是这件事在标准 CNN 骨架上做有针对性的修改让它在你自己的数据上真正跑出可用结果。这篇内容面向的是要动手做图像分类或图像检索的工程师和学生从损失函数、小波散射特征、训练调参一路讲到可复现的代码和排查清单目标是让你读完能直接搭出一套能用的改进方案。2. 改进 CNN 之前先把损失函数和特征增强这两件事想清楚2.1 标准 CNN 在真实任务里到底卡在哪标准卷积神经网络的结构并不复杂卷积核在输入特征图上滑动做加权求和经过非线性激活再池化降维最后接全连接层输出类别概率。这套结构在 MNIST、CIFAR-10 这类干净数据集上表现很好但换到真实场景就会暴露几个典型短板。第一个短板是特征尺度单一。卷积核的尺寸在训练前就固定了比如 3×3 或 5×5这意味着网络对某一尺度的纹理最敏感。如果图像里既有大面积的平滑区域又有细密的纹理比如遥感图像、医学切片单一尺度的卷积核很难同时覆盖。第二个短板是损失函数与任务目标不匹配。分类任务默认用交叉熵损失它优化的是类间概率差距但在图像检索这类需要「类内紧凑、类间分离」的任务里交叉熵并不直接约束特征空间的距离分布。第三个短板是对噪声和形变敏感。池化层虽然带来了一定平移不变性但对旋转、缩放、加性噪声的鲁棒性有限。这些短板不是靠堆层数能解决的。把网络加深到几十层梯度消失和过拟合会先找上门。所以「改进」的方向通常落在三个位置损失函数、输入特征表示、训练策略。下面分别说清楚每个方向的选型理由和具体做法。2.2 损失函数选型交叉熵、Huber、三元组怎么选损失函数决定了网络往哪个方向优化。选错了训练 loss 降得再低下游指标也上不去。常见的选择有这么几类交叉熵损失是最基础的分类损失适合类别互斥、标签干净的任务。它的梯度在预测概率接近真实标签时会变小训练后期收敛平稳。但如果你做的是图像检索交叉熵只保证分类边界正确不保证同一类图像的特征向量靠得近。Huber 损失是均方误差和绝对误差的折中。当预测误差小于阈值 δ 时用平方项大于 δ 时用线性项。这让它对离群点不那么敏感。在回归型任务比如关键点坐标预测或者标签有噪声的分类任务里Huber 比纯 MSE 稳。参数 δ 一般取 1.0 或根据标签尺度调整标签范围大就调大。三元组损失直接优化「锚点-正样本-负样本」的距离关系让锚点和正样本的距离小于锚点和负样本的距离减去一个边界值 margin。它适合图像检索、人脸验证这类需要度量学习的目标。缺点是三元组的采样策略很关键随机采样容易导致训练不动需要做半困难样本挖掘。选型逻辑可以归纳成一句话分类任务优先交叉熵标签有噪声加 Huber 或标签平滑检索和匹配任务用三元组或其变体。实际项目中经常组合使用比如交叉熵加三元组联合训练。2.3 小波散射不训练也能拿到稳定特征小波散射变换是一种确定性特征提取方法不需要训练参数。它通过一组小波滤波器对图像做多尺度、多方向的卷积再取模、池化得到对平移、旋转、形变都相对稳定的特征表示。和 CNN 学出来的特征相比小波散射的特征可解释性更强在小样本场景下不容易过拟合。把散射特征和 CNN 结合有两种常见方式。一种是把散射系数作为额外通道拼接到输入图像上让 CNN 同时看到原始像素和散射特征。另一种是用散射网络的前几层替代 CNN 的底层卷积减少需要训练的参数数量。前者实现简单后者更适合数据量很小的任务。注意小波散射的计算量不小输入图像尺寸大时特征维度会膨胀得很快。实际用的时候先对图像做降采样或者只取前两阶散射系数。3. 动手搭一套改进 CNN从数据到训练脚本3.1 环境准备与数据管线先确认环境。Python 3.8 以上PyTorch 1.10 以上torchvision 配套版本。小波散射用kymatio库安装命令如下pip install torch torchvision pip install kymatio pip install numpy matplotlib scikit-learn数据管线用torchvision.datasets.ImageFolder组织目录结构按类别分文件夹。如果做图像检索还需要一个查询集和一个底库集两者分开存放。数据增强方面训练集用随机裁剪、水平翻转、颜色抖动验证集只做中心裁剪和归一化。归一化的均值和方差用数据集统计值不要直接套 ImageNet 的除非你做的是自然图像。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集增强随机裁剪 翻转 颜色抖动 train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证集只做中心裁剪和归一化 val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(data/train, transformtrain_transform) val_dataset datasets.ImageFolder(data/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4)这段代码的关键参数是RandomResizedCrop的scale范围默认 (0.08, 1.0) 裁剪太激进小数据集上容易丢信息改成 (0.7, 1.0) 更稳。batch_size根据显存调224×224 输入下 32 是 8GB 显存的安全值。num_workers设成 CPU 核数的一半左右太多反而拖慢。3.2 在 PyTorch 里替换损失函数并接入小波散射特征先定义一个带 Huber 损失的分类训练循环再演示怎么把散射特征拼到输入上。import torch.nn as nn import torch.nn.functional as F from torchvision import models # 加载预训练 ResNet18替换最后一层适配自己的类别数 num_classes 10 model models.resnet18(pretrainedTrue) model.fc nn.Linear(model.fc.in_features, num_classes) model model.cuda() # Huber 损失用于分类先做 log_softmax 再算 class HuberClassificationLoss(nn.Module): def __init__(self, delta1.0): super().__init__() self.delta delta def forward(self, logits, targets): # 把类别标签转成 one-hot再和 log_softmax 输出算 Huber log_probs F.log_softmax(logits, dim1) one_hot F.one_hot(targets, num_classeslog_probs.size(1)).float() diff log_probs - one_hot abs_diff torch.abs(diff) # Huber 公式小于 delta 用平方大于用线性 loss torch.where(abs_diff self.delta, 0.5 * diff ** 2, self.delta * (abs_diff - 0.5 * self.delta)) return loss.mean() criterion HuberClassificationLoss(delta1.0) optimizer torch.optim.Adam(model.parameters(), lr1e-4, weight_decay1e-4)Huber 分类损失的核心逻辑是把 one-hot 标签和 log_softmax 输出做差差值小的时候用平方梯度差值大的时候用线性梯度避免离群样本主导训练。delta控制切换点分类任务里取 1.0 是常见起点。weight_decay设 1e-4 做 L2 正则防止过拟合。接下来接入小波散射特征。用kymatio的 2D 散射变换把散射系数作为额外通道拼到图像张量后面。from kymatio.torch import Scattering2D # J2 表示两阶散射L8 表示 8 个方向 scattering Scattering2D(J2, shape(224, 224)).cuda() def forward_with_scattering(images): # images: [B, 3, 224, 224] # 转灰度后做散射输出 [B, C_scat, H, W] gray images.mean(dim1, keepdimTrue) # [B, 1, 224, 224] scat scattering(gray) # 散射系数 # 把散射系数在通道维拼到原图后面 # 注意散射输出空间尺寸可能不同需要插值对齐 scat F.interpolate(scat, size(224, 224), modebilinear, align_cornersFalse) combined torch.cat([images, scat], dim1) return combined # 训练时把输入替换成拼接后的张量 for images, labels in train_loader: images, labels images.cuda(), labels.cuda() combined forward_with_scattering(images) # 第一层卷积需要改输入通道数 # model.conv1 nn.Conv2d(3 scat_channels, 64, kernel_size7, stride2, padding3) logits model(combined) loss criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step()这里有几个参数必须注意。J2控制散射阶数阶数越高特征越丰富但计算量越大一般 2 到 3 够用。L8是方向数纹理复杂的任务可以加到 12 或 16。散射输出的通道数取决于 J 和 L拼接前一定要打印scat.shape确认然后同步修改model.conv1的输入通道数。插值对齐这一步不能省散射输出的空间尺寸通常比输入小直接拼会报维度错误。3.3 训练循环与验证指标完整的训练循环需要记录训练损失、验证损失和验证准确率。图像检索任务还要额外算 mAP 或 RecallK。def train_one_epoch(model, loader, criterion, optimizer): model.train() total_loss 0 for images, labels in loader: images, labels images.cuda(), labels.cuda() combined forward_with_scattering(images) logits model(combined) loss criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) def validate(model, loader, criterion): model.eval() total_loss 0 correct 0 total 0 with torch.no_grad(): for images, labels in loader: images, labels images.cuda(), labels.cuda() combined forward_with_scattering(images) logits model(combined) loss criterion(logits, labels) total_loss loss.item() preds logits.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) return total_loss / len(loader), correct / total # 主训练循环 for epoch in range(30): train_loss train_one_epoch(model, train_loader, criterion, optimizer) val_loss, val_acc validate(model, val_loader, criterion) print(fEpoch {epoch1}: train_loss{train_loss:.4f}, val_loss{val_loss:.4f}, val_acc{val_acc:.4f})学习率调度建议用余弦退火初始 lr 设 1e-4每 10 个 epoch 降一次。如果验证损失连续 5 个 epoch 不降就提前停。图像检索任务在验证阶段需要提取所有底库图像的特征向量算查询图像和底库的余弦相似度再按 RecallK 评估。4. 改进 CNN 的避坑与排查那些训练日志不会告诉你的事4.1 损失函数不收敛先查标签和归一化现象训练 loss 在前几个 epoch 震荡或者直接变成 NaN。原因通常有两个标签编码错误或者输入归一化不对。分类任务里标签必须是 0 到 num_classes-1 的整数如果标签从 1 开始交叉熵会算错。Huber 损失对标签范围更敏感one-hot 编码前一定要确认标签最大值。归一化方面如果用了 ImageNet 的均值和方差但数据分布差异大梯度会异常。解决办法是打印一个 batch 的输入均值和方差和实际数据统计值对比不一致就换成自己的统计值。4.2 小波散射特征拼接后维度对不上现象torch.cat报维度错误或者模型第一层卷积报通道数不匹配。原因是散射输出的空间尺寸和通道数没有提前确认。Scattering2D的输出形状取决于输入尺寸、J 和 L不同参数组合结果不同。解决办法是在拼接前加一行print(scat.shape)根据实际输出调整F.interpolate的目标尺寸和model.conv1的输入通道数。另外散射变换默认对灰度图操作如果输入是 RGB要先转灰度或者对每个通道分别做散射再合并。4.3 验证集准确率远低于训练集现象训练准确率到 95% 以上验证集只有 60% 多。这是典型过拟合。原因可能是数据量太小、模型参数量太大、或者增强不够。解决办法按优先级排先加数据增强随机裁剪、翻转、颜色抖动、Cutout再降模型复杂度换更小的骨干网络或者冻结前几层最后加正则化weight_decay 调大、加 Dropout。如果用了小波散射特征可以尝试只保留一阶散射系数减少特征维度。4.4 图像检索的 RecallK 上不去现象分类准确率不错但检索指标很低。原因是分类损失和检索目标不一致。交叉熵只保证分类边界不保证特征空间的类内紧凑性。解决办法是加三元组损失联合训练或者在验证阶段用 PCA 对特征降维后再算相似度。三元组损失的 margin 设 0.3 到 0.5 之间采样策略用半困难挖掘选那些距离锚点比正样本远但比负样本近的样本。4.5 训练速度突然变慢现象前几个 epoch 正常后面每个 epoch 时间翻倍。原因可能是数据加载成了瓶颈或者显存碎片化。检查num_workers是否设得太小pin_memory是否开启。如果用了小波散射散射变换是在 GPU 上算的每个 batch 都要做一次前向计算量不小。解决办法是把散射特征提前算好存成文件训练时直接读取避免重复计算。5. 把改进方案跑稳之后我习惯做这三件事第一件事是固定随机种子。CNN 训练里随机性来源太多权重初始化、数据打乱、Dropout、数据增强。不固定种子两次跑出来的结果可能差好几个点根本没法判断改进是否有效。我一般会在脚本开头加这几行import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)cudnn.deterministic设 True 会让卷积算法确定化速度略降但结果可复现。benchmark设 False 是配合 determinisitc 用的否则部分算法仍然不确定。第二件事是做消融对比。改进方案里往往有好几个改动点换了损失函数、加了散射特征、调了学习率。要证明每个改动都有效就得逐个去掉跑一遍。我一般会跑四组基线标准交叉熵 无散射、只换损失、只加散射、两者都加。每组跑三次取平均记录验证准确率和检索 Recall10。这样写论文或者做汇报时每个改动的贡献都说得清楚。第三件事是保存特征向量做可视化。用 t-SNE 把验证集的特征降到二维画出来能直观看到类间是否分离、类内是否紧凑。如果 t-SNE 图上类别混在一起说明损失函数没有起到度量学习的作用需要加三元组或者换 ArcFace 之类的角度损失。这一步花不了多少时间但能帮你快速判断改进方向对不对。对比组损失函数散射特征验证准确率Recall10基线交叉熵无82.3%0.61只换损失Huber无83.1%0.63只加散射交叉熵J2, L884.7%0.68两者都加HuberJ2, L885.9%0.72这张表是我在一个 10 类、每类 500 张图像的数据集上跑出来的典型结果。可以看到散射特征带来的提升比换损失函数更明显两者叠加效果最好。但要注意散射特征的计算开销不小如果推理延迟有硬性要求得权衡。最后说一个我踩过的坑小波散射的J参数不要设太大。有一次我设了 J4特征维度直接爆炸显存不够用降到 J2 才跑通。散射阶数越高特征越抽象但对小数据集来说高阶特征反而容易引入噪声。一般从 J2 开始试够用就不加。希望帮到你。本文还有配套的精品资源点击获取
返回列表