ARTICLE DETAIL

资讯详情

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

深度学习图像分类————食物分类(理论与实战记录)

深度学习图像分类————食物分类(理论与实战记录) 理论部分分类任务前提——卷积神经网络CNN卷积神经网络是什么核心原理模拟人脑的视觉机制CNN 的设计灵感直接来自于人类大脑的视觉皮层。科学家发现人脑在识别图像时并不是一眼就看清所有像素而是分层级来识别的初级层先识别边缘、线条、颜色等简单特征。高级层再把这些简单特征组合起来识别出眼睛、鼻子、形状等复杂特征。最终层把复杂特征组合起来识别出这是 “一只猫” 还是 “一个人”。CNN 就是在数学上模拟了这个过程。它通过一个个小小的过滤器Filter在输入的数据比如一张图片上滑动像筛子一样提取出不同的特征。图例卷积核3*3的矩阵会和特征图每一个3*3的矩阵相乘得出的值放在新特征图的相应位置数值越大特征越像。卷积层卷积层 (Convolutional Layer)特征提取器。它是 CNN 的核心通过卷积操作提取输入数据中的特征。图中红色框框圈起来的小特征图部分就是卷积核卷积核是模型用来分类图片特征的关键。卷积过程图中的4*4特征矩阵会与2*2的卷积核想成变成3*3的新特征图可见在卷积一次之后特征图尺寸会变小。保持尺寸不变的方法若在原特征图上加入0padding填充则相对地在卷积之后新图不会发生尺寸缩小不是必定需要满足一定数量条件参数计算训练计算逃课方法1.卷积核为357padding为123时卷出来的特征图大小不变2.无padding则可以从后开始算如特征图100*100卷积核为3最后可卷到第98 99 100列即卷出来最大为98.池化层池化层 (Pooling Layer)降维与压缩。对卷积层输出的特征图进行压缩保留主要特征减少计算量。就像把图片从高清压缩成标清虽然细节少了但核心的 “这是个苹果” 的信息还在且运行更快了。扩大步长的办法知道了所有的参数之后可以根据公式计算卷积尺寸224-32*1/11224输出尺寸仍为224不变池化(最大池化示例分割将输入的特征图切成一个个互不重叠的 2×2 小块。采样在每个小块内只保留最大的那个数值作为代表丢弃其他数值。理论上平均池化是更好的但是为了减少计算量实际更常使用最大池化需要用到卷积尺寸计算公式此处P为0则池化尺寸为OI-K/S1只要连续进行5 次下采样池化空间尺寸就从 224 变成了 7。224→112→56→28→14→7深度变化3→64→128→256→512→1024只做一次卷积深度就只有64全连接层全连接层 (Fully Connected Layer)分类与输出。将前面提取到的高维特征向量 “摊平”连接到最终的输出层给出最终的预测结果。通过卷积和池化我们缩小了特征图减小了参数量然后再进行展开接入线性回归模型就可以实现预测。展开参数图loss的计算最左侧的向量是神经网络最后一层直接输出的数值通常称为Logits对数几率。此时它们只是分数还不是概率。此时数值越大代表模型认为这个类别的 “置信度” 越高但数值之间没有直接的概率求和关系。所以要先借助Softmax公式归一化将其映射为概率分布即将任意实数的向量映射成一个0 到 1 之间、且总和为 1的概率分布。Softmax最大的意义在于把 “相对大小” 变成了 “明确的概率”从而让交叉熵损失可以量化地衡量模型预测的准不准。交叉熵损失Cross-Entropy Loss交叉熵的核心利用对数函数的性质极大地惩罚模型对正确类别的低概率预测。比如模型预测猫的概率只有 0.1实际是猫Loss 就会非常大倒逼模型修正。与回归的区别回归看的是 “数值差”MAE/MSE分类看的是 “分布差异”Cross Entropy。交叉熵损失Cross-Entropy Loss是分类任务的核心损失函数核心作用是量化模型预测概率分布与真实标签分布的差距差距越大Loss 越大差距越小Loss 越小。与其他损失函数的区别注意点经典卷积模型AlexNet介绍创新点介绍1.ReLU 激活函数首次在大规模网络中使用ReLU替代传统的 Sigmoid/Tanh解决了深层网络的梯度消失问题让训练更快、更深。2.Dropout 正则化在全连接层中加入 Dropout随机失活 50% 神经元有效防止过拟合提升模型泛化能力。3.重叠池化 (Overlapping Pooling)使用3×3池化核、步长 2让池化窗口重叠减少信息丢失提升特征鲁棒性。4.数据增强训练时对图像做随机裁剪、水平翻转、颜色扰动等扩充训练数据进一步抑制过拟合。5.多 GPU 并行将网络拆分到两块 GPU 训练分别处理不同通道大幅提升训练速度。特征归一化经过归一化后所有特征维度的均值都变为 0所有特征维度的方差都变为 1。特征分布被拉到统一的标准正态分布区间避免了不同特征尺度差异过大比如一个特征是 0~1另一个是 0~1000导致的训练不稳定。图中提示 “归一化可以使模型收敛的更快”背后的原因是消除尺度差异不同特征的数值范围不同会导致梯度更新时偏向数值大的特征归一化后所有特征处于同一量级梯度下降更平稳。加速收敛优化器能更快找到最优解减少训练步数。稳定训练避免因输入分布剧烈变化导致的梯度消失 / 爆炸让深层网络训练更稳定。问题即使对输入做了特征归一化Feature Normalization经过一层线性变换 W1 后输出 z1,z2,z3 的不同维度仍会出现数值范围差异Different dims have different ranges。结论仅在输入层做归一化不够需要在网络中间层也做归一化这就是 Batch Norm 的诞生背景。批量归一化Batch Norm 是在一个 Batch 的样本上对每个特征通道 / 维度独立做标准化核心作用稳定分布解决 “内部协变量偏移Internal Covariate Shift”让每层输入分布保持稳定。加速收敛梯度下降更平稳允许使用更大学习率训练更快。缓解过拟合有一定正则化效果减少对 Dropout 的依赖。允许更深网络让深层网络训练更稳定避免梯度消失 / 爆炸。网络结构AlexNet 共 8 层可训练层5 个卷积层 3 个全连接层输入为224×224×3实际为227×227×3输出为 1000 类ImageNet 类别数。解析卷积层格式Conv2d(输入通道, 输出通道, 卷积核大小, 步长, padding)输入3×224×224RGB 图像层序号卷积层参数输出特征图尺寸参数量计算Conv1Conv2d(3, 64, 11, 4, 2)64×55×5511×11×3×6423232Pool1MaxPool(3, 2)64×27×27无参数Conv2Conv2d(64, 192, 5, 1, 2)192×27×275×5×64×192307200Pool2MaxPool(3, 2)192×13×13无参数Conv3Conv2d(192, 384, 3, 1, 1)384×13×133×3×192×384663552Conv4Conv2d(384, 256, 3, 1, 1)256×13×133×3×384×256884736Conv5Conv2d(256, 256, 3, 1, 1)256×13×133×3×256×256589824Pool5MaxPool(3, 2)AdaptiveAvgPool(6)256×6×6无参数池化后特征图256×6×6展平为9216维向量全连接层参数输出维度参数量计算FC1Linear(9216, 4096)40969216×409637748736FC2Linear(4096, 4096)40964096×409616777216FC3Linear(4096, 1000)10004096×10004096000前向代码​ import torchvision.models as models import torch import torch.nn as nn alexnet models.alexnet() print(alexnet) class myAlexnet(nn.Module): def __init__(self): super(myAlexnet, self).__init__() self.conv1 nn.Conv2d(3, 64, 11, 4, padding2) #bias 64 23232 self.pool1 nn.MaxPool2d(3,2) self.conv2 nn.Conv2d(64, 192, 5, 1, padding2) self.pool2 nn.MaxPool2d(3,2) self.conv3 nn.Conv2d(192, 384, 3, 1, 1) self.conv4 nn.Conv2d(384, 256, 3, 1, 1) self.conv5 nn.Conv2d(256, 256, 3, 1, 1) self.pool3 nn.MaxPool2d(3, 2) self.pool4 nn.AdaptiveAvgPool2d(6) self.fc1 nn.Linear(9216, 4096) self.fc2 nn.Linear(4096, 4096) self.fc3 nn.Linear(4096, 1000) def forward(self, x): x self.conv1(x) x self.pool1(x) x self.conv2(x) x self.pool2(x) x self.conv3(x) x self.conv4(x) x self.conv5(x) x self.pool3(x) x self.pool4(x) x x.view(x.size()[0], -1) # view 变形 batch * tezheng ,保留了第一个维度也就是batch的维度,剩下的全部合并到第二个维度 x self.fc1(x) x self.fc2(x) x self.fc3(x) return x mymodel myAlexnet() #实例化 input torch.ones((4, 3, 224, 224)) # batch * channel * H * W out mymodel(input) print(out.shape) def get_parameter_number(model): total_num sum(p.numel() for p in model.parameters()) trainable_num sum(p.numel() for p in model.parameters() if p.requires_grad) return {Total: total_num, Trainable: trainable_num} print(get_parameter_number((mymodel))) # print(get_parameter_number((mymodel.conv1))) # print(get_parameter_number((mymodel.conv2))) # print(get_parameter_number((mymodel.conv3))) # print(get_parameter_number((mymodel.conv4))) # print(get_parameter_number((mymodel.conv5))) # print(get_parameter_number((mymodel.fc1))) # print(get_parameter_number((mymodel.fc2))) # print(get_parameter_number((mymodel.fc3))) # print(get_parameter_number((mymodel.conv1))[Total]get_parameter_number((mymodel.conv2))[Total] # get_parameter_number((mymodel.conv3))[Total]get_parameter_number((mymodel.conv4))[Total] # get_parameter_number((mymodel.conv5))[Total]get_parameter_number((mymodel.fc1))[Total] # get_parameter_number((mymodel.fc2))[Total]get_parameter_number((mymodel.fc3))[Total]) ​关键要点总结卷积核演进从大核11×11逐步过渡到小核3×3先快速降维再精细提取特征。池化作用MaxPool(3,2)实现空间下采样AdaptiveAvgPool(6)保证输入任意尺寸时输出固定为6×6适配全连接层。正则化全连接层加入Dropout防止过拟合配合 ReLU 激活加速训练。VggNet核心创新用3×3 小卷积核替代 AlexNet 中的 11×11、5×5 等大卷积核并加深网络层数从 11 层到 19 层。设计思想通过堆叠多个 3×3 卷积层模拟更大感受野同时减少参数量、增强非线性表达能力。结合图例单个5×5 卷积核直接覆盖 5×5 区域的感受野。堆叠2 层 3×3 卷积核最终感受野也能达到 5×53 (3-1) 5但参数量更少~5×5 卷积核5×5 25个参数~2 层 3×3 卷积核2×(3×3) 18个参数额外优势更多非线性激活每层都有 ReLU提升模型表达能力。参数量更少降低过拟合风险同时计算更高效。网络结构更规整便于加深层数。模型分析每个卷积块由2~3 层 3×3 卷积组成后接2×2 最大池化Pool步长为 2作用是将特征图尺寸减半、通道数翻倍阶段卷积层配置 (in_channels, out_channels, kernel_size, stride, padding)池化后特征图尺寸1(3,64,3,1,1) → (64,64,3,1,1)64×112×1122(64,128,3,1,1) → (128,128,3,1,1)128×56×563(128,256,3,1,1) → (256,256,3,1,1) → (256,256,3,1,1)256×28×284(256,512,3,1,1) → (512,512,3,1,1) → (512,512,3,1,1)512×14×145(512,512,3,1,1) → (512,512,3,1,1) → (512,512,3,1,1)512×7×7adaPool(7)自适应平均池化将最后一个卷积块输出的512×14×14特征图固定压缩为512×7×7保证输入全连接层的维度一致。全连接层第一层Linear(25088, 4096)→ 把512×7×7 25088维特征映射到 4096 维第二层Linear(4096, 4096)→ 进一步特征变换第三层Linear(4096, 1000)→ 输出 1000 类的分类概率ImageNet 数据集前向代码import torchvision.models as models import torch.nn as nn vgg models.vgg13() print(vgg) class vggLayer(nn.Module): def __init__(self,in_cha, mid_cha, out_cha): super(vggLayer, self).__init__() self.relu nn.ReLU() self.pool nn.MaxPool2d(2) self.conv1 nn.Conv2d(in_cha, mid_cha, 3, 1, 1) self.conv2 nn.Conv2d(mid_cha, out_cha, 3, 1, 1) def forward(self, x): x self.conv1(x) x self.relu(x) x self.conv2(x) x self.relu(x) x self.pool(x) return x class MyVgg(nn.Module): def __init__(self): super(MyVgg, self).__init__() self.layer1 vggLayer(3, 64, 64) self.layer2 vggLayer(64, 128, 128) self.layer3 vggLayer(128, 256, 256) self.layer4 vggLayer(256, 512, 512) self.layer5 vggLayer(512, 512, 512) self.adapool nn.AdaptiveAvgPool2d(7) self.relu nn.ReLU() self.fc1 nn.Linear(25088, 4096) self.fc2 nn.Linear(4096, 4096) self.fc3 nn.Linear(4096, 1000) def forward(self,x): x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.layer5(x) x self.adapool(x) x self.adapool(x) x x.view(x.size()[0], -1) x self.fc1(x) x self.relu(x) x self.fc2(x) x self.relu(x) x self.fc3(x) x self.relu(x) return x import torch myVgg MyVgg() img torch.zeros((1, 3, 224,224)) out myVgg(img) print(out.size()) def get_parameter_number(model): total_num sum(p.numel() for p in model.parameters()) trainable_num sum(p.numel() for p in model.parameters() if p.requires_grad) return {Total: total_num, Trainable: trainable_num} # print(get_parameter_number(myVgg.layer1)) # print(get_parameter_number(myVgg.layer2)) # print(get_parameter_number(myVgg.layer3)) # print(get_parameter_number(myVgg.layer4)) # print(get_parameter_number(myVgg.layer5)) # print(get_parameter_number(myVgg.fc1)) # print(get_parameter_number(myVgg.fc2)) # print(get_parameter_number(myVgg.fc3)) # # print(\n) # # print(get_parameter_number(myVgg.layer1)[Total]get_parameter_number(myVgg.layer2)[Total] # get_parameter_number(myVgg.layer3)[Total]get_parameter_number(myVgg.layer4)[Total] # get_parameter_number(myVgg.layer5)[Total]get_parameter_number(myVgg.fc1)[Total] # get_parameter_number(myVgg.fc2)[Total]get_parameter_number(myVgg.fc3)[Total]) # print(get_parameter_number(vgg))ResNetResNet 核心背景与问题提出ResNet残差网络由何凯明团队在 2015 年提出解决了 ** 深度神经网络退化Degradation** 的问题当网络层数加深到一定程度后训练误差和测试误差反而会上升如左图所示56 层网络的训练 / 测试误差都高于 20 层网络。这不是过拟合而是梯度消失 / 爆炸导致深层网络难以优化无法学到有效特征。·训练误差左图56 层网络的训练误差高于 20 层说明深层网络在训练集上都难以拟合体现了退化问题。·测试误差右图56 层网络的泛化能力也更差验证了退化问题在测试集上同样存在。核心创新残差连接Residual ConnectionResNet 的核心思想是残差学习公式为优势1.梯度可以通过 shortcut 直接回传缓解梯度消失问题让深层网络更容易训练。2.若某层不需要学习新特征残差 F(x) 会趋近于 0网络退化为恒等映射保证性能不会退化。残差块Building Block结构上图展示了两种典型残差块基本残差块Basic Block·适用ResNet-18/34·结构x → Conv3x3 → ReLU → Conv3x3 → x → ReLU通道数不变如 64→64直接做恒等 shortcut。瓶颈残差块Bottleneck Block·适用ResNet-50/101/152·结构x → Conv1x1 → ReLU → Conv3x3 → ReLU → Conv1x1 → x → ReLU用 1×1 卷积先降维、再升维如 256→64→256大幅减少参数量和计算量适合更深网络。当通道数 / 尺寸变化时shortcut 会用 1×1 卷积来匹配维度。ResNet 的整体设计与影响1.结构延续性在 VGG 的 3×3 卷积基础上引入残差连接保持了结构的规整性。2.1×1 卷积的作用在瓶颈块中用于通道维度变换既减少计算量又实现维度匹配。3.里程碑意义ResNet 首次让数百层的深度网络训练成为可能后续的 DenseNet、ResNeXt 等网络都延续了残差思想是计算机视觉领域最具影响力的模型之一。这张图展示了ResNet 瓶颈块Bottleneck Block的核心设计通过 1×1 卷积实现降维→卷积→升维大幅减少参数量。1. 方案一直接 3×3 卷积输入256×112×112卷积Conv2d(256, 256, 3, 1, 0)参数量计算Paramsin_channels×out_channels×kernel_size2256×256×3×3589,824输出256×110×110尺寸因 padding0 缩小2. 方案二瓶颈结构1×1 3×3 1×1第 1 层Conv2d(256, 64, 1, 1, 0)→ 降维Params1​256×64×1×116,384第 2 层Conv2d(64, 64, 3, 1, 1)→ 核心卷积Params2​64×64×3×336,864第 3 层Conv2d(64, 256, 1, 1, 0)→ 升维Params3​64×256×1×116,384总参数量16,38436,86416,38469,632输出256×112×112尺寸与输入一致便于残差连接当网络层数加深时若权重 ∣w∣1梯度会被多次相乘后趋近于 0梯度消失若 ∣w∣1梯度会指数级放大梯度爆炸。ResNet 残差连接的缓解作用梯度回传时会保留一个1 项避免梯度被乘到趋近于 0 或无穷大保证梯度能顺畅传递到浅层网络。这就是 ResNet 能训练数百层深层网络的核心原因。Sigmoid 激活的问题其导数最大值为 0.25多层相乘后梯度极易消失。ReLU 的辅助作用ReLU 激活函数 f(x)max(0,x) 的导数在正区间恒为 1负区间为 0正区间梯度不会衰减进一步缓解梯度消失问题负区间梯度为 0实现稀疏激活减少冗余计算。图中提到 “ReLU 也有相似的作用”正是指其在缓解梯度消失上的贡献。激活函数公式特点优缺点SigmoidS(x)1e−x1​输出范围 (0,1)适合二分类输出易梯度消失计算量大输出非零均值ReLUf(x)max(0,x)正区间梯度恒为 1计算高效缓解梯度消失存在 “死神经元” 问题总结ResNet 之所以能 work本质是残差连接解决了深层网络的梯度传递问题而 ReLU 等激活函数则为网络提供了非线性建模能力两者结合让数百层的深度神经网络训练成为可能。这张表清晰展示了ResNet 18/34/50/101/152五种经典版本的网络结构核心差异在于残差块类型与堆叠次数整体遵循 “初始卷积 池化 → 4 组残差块 → 全局平均池化 全连接” 的流程。其余知识就不多赘述了ResNet 与 VGG 的区别VGG 仅堆叠 3×3 卷积无残差连接深层易退化ResNet 引入残差连接解决梯度消失问题可训练数百层。代码import torch import torch.nn as nn import torchvision.models as models resNet models.resnet18() print(resNet) class Residual_block(nn.Module): # save def __init__(self, input_channels, out_channels, down_sampleFalse, strides1): # 步长默认为1 super().__init__() self.conv1 nn.Conv2d(input_channels, out_channels, kernel_size3, padding1, stridestrides) # 步长可变 self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, padding1, stride 1) # 步长恒为1 if input_channels ! out_channels: # 若输入通道数和输出通道数不一致则需要一个1x1的卷积层 self.conv3 nn.Conv2d(input_channels, out_channels, kernel_size1, stridestrides) else: # 若输入通道数和输出通道数一致则不需要一个1x1的卷积层 self.conv3 None self.bn1 nn.BatchNorm2d(out_channels) # 批量归一化层 self.bn2 nn.BatchNorm2d(out_channels) # 批量归一化层 self.relu nn.ReLU() # ReLU层 def forward(self, X): out self.relu(self.bn1(self.conv1(X))) # 经过第一层需要激活 out self.bn2(self.conv2(out)) if self.conv3: X self.conv3(X) # X进行维度变换 out X # 残差连接 return self.relu(out) class MyResNet18(nn.Module): def __init__(self): super(MyResNet18, self).__init__() self.conv1 nn.Conv2d(3, 64, 7, 2, 3) self.bn1 nn.BatchNorm2d(64) # 将64个通道的特征图做归一化处理 self.pool1 nn.MaxPool2d(3, stride2, padding1) self.relu nn.ReLU() self.layer1 nn.Sequential( # 容器模块用于按顺序堆叠多个神经网络层。 Residual_block(64, 64), Residual_block(64, 64) ) self.layer2 nn.Sequential( Residual_block(64, 128, strides2), Residual_block(128, 128) ) self.layer3 nn.Sequential( Residual_block(128, 256, strides2), Residual_block(256, 256) ) self.layer4 nn.Sequential( Residual_block(256, 512, strides2), Residual_block(512, 512) ) self.flatten nn.Flatten() self.adv_pool nn.AdaptiveAvgPool2d(1) self.fc nn.Linear(512, 1000) def forward(self, x): x self.conv1(x) x self.bn1(x) x self.relu(x) x self.pool1(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.adv_pool(x) x self.flatten(x) x self.fc(x) return x myres MyResNet18() def get_parameter_number(model): total_num sum(p.numel() for p in model.parameters()) trainable_num sum(p.numel() for p in model.parameters() if p.requires_grad) return {Total: total_num, Trainable: trainable_num} print(get_parameter_number(myres.layer1)) print(get_parameter_number(myres.layer1[0].conv1)) print(get_parameter_number(resNet.layer1[0].conv1)) x torch.rand((1,3,224,224)) out resNet(x) out myres(x)实战部分任务概述该项目分为6 个核心模块按功能可划分为基础工具模块、训练相关模块、预测展示模块。模块名称所属类型核心作用关键特性model_utils/model.py基础工具模块模型定义与初始化支持自定义 CNN8 种经典预训练模型ResNet/VGG/GoogLeNet 等实现线性探测冻结特征层、参数初始化等功能迁移学习核心统一模型接口model_utils/data.py基础工具模块数据集构建与加载实现有标签 / 无标签数据读取、数据增强训练集、半监督伪标签数据集封装适配 PyTorch Dataset/DataLoader 框架支持半监督数据增强策略适配分类任务model_utils/train.py训练核心模块实现单轮训练 / 验证、伪标签生成、完整训练流程监督 半监督包含早停机制、最优模型保存、训练日志打印兼容监督 / 半监督解决数据解包报错main.py训练入口模块项目总控入口完成随机种子固定、超参数配置、数据加载、模型初始化、训练参数封装调用train.py启动训练模块化整合实验可复现predict.py基础预测模块加载训练好的模型实现单张 / 批量图片预测输出可读的食物类别名称适配不同图片格式 / 路径预处理与训练一致保证预测准确性predict_visual.py可视化预测模块在基础预测上增加概率可视化标注实现图片上标注前三高概率类别、批量随机采样预测、标注结果保存解决中文乱码问题可视化展示结果更直观simple_class.py轻量化独立模块脱离model_utils的独立训练模块从零实现数据加载、模型定义、半监督训练、损失 / 准确率可视化适合快速验证思路无外部依赖新手友好数据集训练集、验证集和测试集已经提前被放到不同的文件夹里了其中训练集中有一个unlabeled文件夹是给半监督模型用的代码部分下面我只会记录一些我认为需要重点关注的要点data部分简介主要完成了数据集构建、数据增强、半监督学习数据处理、数据加载和可视化等功能完全适配 PyTorch 的 Dataset/DataLoader 框架。一、核心常量与基础设置HW 224 # 图像统一缩放为224x224适配ImageNet预训练模型 imagenet_norm [[0.485, 0.456, 0.406],[0.229, 0.224, 0.225]] # ImageNet标准化参数·HW224迁移学习中常用的图像尺寸如 ResNet、VGG 等预训练模型输入尺寸·imagenet_normImageNet 数据集的均值和标准差用于标准化保证和预训练模型输入分布一致二、数据增强 / 变换Transforms1. 测试集 / 验证集变换test_transform transforms.Compose([ transforms.ToTensor(), # 转为张量0-1范围 transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]) # 标准化 ])测试 / 验证集只做必要变换转张量 标准化避免数据增强引入噪声2. 训练集变换train_transform transforms.Compose([ transforms.ToPILImage(), # 转为PIL图像方便后续变换 transforms.RandomResizedCrop(HW,scale(0.8,1.0)), # 随机裁剪保留80%-100%区域 transforms.RandomHorizontalFlip(p0.5), # 50%概率水平翻转 transforms.RandomRotation(15), # 随机旋转±15度 transforms.ColorJitter(brightness0.2, contrast0.2), # 亮度/对比度随机调整 autoaugment.AutoAugment(), # 自动数据增强提升泛化能力 transforms.ToTensor(), # 转张量 transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]) # 标准化 ])训练集做多种数据增强目的是增加数据多样性防止过拟合AutoAugmentPyTorch 内置的自动增强策略适配图像分类任务三、核心数据集类foodDataset继承 PyTorch 的Dataset类必须实现__init__、__getitem__、__len__三个方法还有一个readfie方法init 用于接收参数它主要被设计用来读取你在不同情况下所需要的不同数据文件getitem 实现容器协议使对象支持索引操作 obj[key]len 实现长度协议使对象支持 len() 函数readfile 方法用于协助init读文件不是必须的方法1. __init__方法初始化def __init__(self, path, mode): y None self.transform None self.mode mode # 定义不同模式的路径映射 pathDict {train:training/labeled,train_unl:training/unlabeled, val:validation, test:testing} imgPaths path / pathDict[mode] # 根据模式加载数据并设置变换 if mode test: x self._readfile(imgPaths,labelFalse) self.transform test_transform elif mode train: x, y self._readfile(imgPaths,labelTrue) self.transform train_transform elif mode val: x, y self._readfile(imgPaths,labelTrue) self.transform test_transform elif mode train_unl: x self._readfile(imgPaths,labelFalse) self.transform train_transform if y is not None: y torch.LongTensor(y) # 标签转为长整型PyTorch分类任务要求 self.x, self.y x, y核心逻辑根据modetrain/train_unl/val/test加载对应路径的数据集区分有标签 / 无标签数据借用另一位大佬的解释--self.x和self.y就是数据和数据标签我们使用_readfile函数读取x和y后面会解释_readfiletest’和’train_unl’没有标签y而’train’和’val’有因此产生分支--self.transform存储了数据增强处理的流程在’train’和’train_unl’下赋为train_transform在’test’和’val’下赋为test_transform。其实也很好理解在训练时数据可以通过各种各样的变换来扩充模型而验证和测试时就不需要没意义。下面是增强管道的设置在非训练时只转换为tensor型数据。原文链接https://blog.csdn.net/weixin_62914052/article/details/157030337标签必须转为LongTensor因为 PyTorch 的交叉熵损失函数要求标签是长整型2. _readfile 方法读取数据def _readfile(self,path, labelTrue): if label: # 读取有标签数据train/val x, y [], [] for i in tqdm(range(11)): # 11类食物 label /%02d/%i # 文件夹命名为两位数字00-10 imgDirpath pathlabel imglist os.listdir(imgDirpath) # 预分配内存提升效率 xi np.zeros((len(imglist), HW, HW, 3), dtypenp.uint8) yi np.zeros((len(imglist)), dtypenp.uint8) for j, each in enumerate(imglist): imgpath imgDirpath each img Image.open(imgpath) img img.resize((HW, HW)) xi[j,...] img yi[j] i # 拼接所有类别的数据 if i 0: x xi y yi else: x np.concatenate((x, xi), axis0) y np.concatenate((y, yi), axis0) print(读入有标签数据%d个 %len(x)) return x, y else: # 读取无标签数据test/train_unl imgDirpath path /00/ imgList os.listdir(imgDirpath) x np.zeros((len(imgList), HW, HW ,3),dtypenp.uint8) for i, each in enumerate(imgList): imgpath imgDirpath each img Image.open(imgpath) img img.resize((HW, HW)) x[i,...] img return x有标签数据按类别文件夹00-10读取每个文件夹对应一个类别标签无标签数据直接读取指定文件夹下所有图像无标签预分配 numpy 数组避免动态扩容的性能损耗3. __getitem__方法获取单条数据def __getitem__(self, index): orix self.x[index] if self.transform None: xT torch.tensor(orix).float() else: xT self.transform(orix) if self.y is not None: y self.y[index] return xT, y, orix # 返回变换后图像、标签、原始图像方便可视化 else: return xT, orix # 无标签数据返回变换后图像、原始图像核心作用DataLoader 迭代时根据索引返回单条数据额外返回原始图像orix方便后续可视化检查数据4. __len__方法返回数据集长度def __len__(self): return len(self.x)必须实现DataLoader 需要知道数据集总长度四、半监督学习数据集类noLabDataset由于只有这个项目目前用到此部分不多赘述用于无标签数据的伪标签生成半监督学习核心class noLabDataset(Dataset): def __init__(self,dataloader, model, device, thres0.85): super(noLabDataset, self).__init__() self.model model # 预训练模型 self.device device # 设备CPU/GPU self.thres thres # 伪标签置信度阈值 x, y self._model_pred(dataloader) # 用模型预测生成伪标签 if x []: self.flag False # 无符合条件的伪标签数据 else: self.flag True self.x np.array(x) self.y torch.LongTensor(y) self.transformers train_transform def _model_pred(self, dataloader): model self.model device self.device thres self.thres pred_probs [] labels [] x [] y [] with torch.no_grad(): # 关闭梯度计算仅预测不训练 for data in dataloader: imgs data[0].to(device) pred model(imgs) soft torch.nn.Softmax(dim1) # 转为概率分布 pred_p soft(pred) # 获取最大概率置信度和对应标签 pred_max, preds pred_p.max(1) pred_probs.extend(pred_max.cpu().numpy().tolist()) labels.extend(preds.cpu().numpy().tolist()) # 筛选置信度超过阈值的样本作为伪标签数据 for index, prob in enumerate(pred_probs): if prob thres: x.append(dataloader.dataset[index][1]) y.append(labels[index]) return x, y def __getitem__(self, index): x self.x[index] x self.transformers(x) y self.y[index] return x, y def __len__(self): return len(self.x)核心逻辑用训练好的模型对无标签数据预测筛选置信度 阈值的样本生成伪标签torch.no_grad()关闭梯度避免占用显存提升预测速度伪标签阈值thres0.85只保留模型 “确信” 的预测结果保证伪标签质量model部分这段代码是食物分类项目的模型模块核心实现主要包含三部分功能自定义卷积神经网络MyModel、迁移学习模型初始化支持多种经典预训练模型、模型参数配置线性探测、参数初始化是迁移学习落地的关键模块。
返回列表