深度学习之图像分类(二)
前言
文章主要是通过实战项目——食品分类来理解分类项目的整体流程。除此之外,还需要对半监督学习,迁移学习,数据增广,Adam和AdamW进行了解。
数据增广
图片增广(Image Data Augmentation)是深度学习中一种重要的技术,通过有目的地对图像进行变换或处理,增加数据集的多样性,从而提升模型的泛化能力和鲁棒性。
图片增广的主要方法
图片增广的方法可以分为以下几类:
1. 几何变换
几何变换是最常见的图片增广方法,包括:
-
旋转(Rotation):随机旋转图像一定角度。
-
翻转(Flip):水平翻转(RandomHorizontalFlip)和垂直翻转(RandomVerticalFlip),但需根据数据集的特性选择是否适用。
-
裁剪(Crop):随机裁剪图像的一部分并调整大小(RandomResizedCrop),可以模拟不同视角。
-
缩放(Resize/Scale):改变图像的尺寸。
-
平移(Translate):将图像在水平或垂直方向上移动。
2. 颜色变换
颜色变换通过调整图像的颜色属性来增加多样性:
-
亮度、对比度、饱和度调整:通过
ColorJitter
随机改变图像的亮度、对比度和饱和度。 -
色调分离(Posterize):减少图像的色彩层次。
-
高斯模糊(Gaussian Blur):对图像进行模糊处理。
3. 噪声注入
向图像中添加噪声,模拟不同成像质量的图像:
-
高斯噪声、椒盐噪声:增强模型对噪声的鲁棒性。
-
对抗样本(Adversarial Examples):通过对抗训练增强模型的鲁棒性
4. 图像合成
通过混合多幅图像生成新的训练样本:
-
Mixup:将两幅图像的像素值和标签向量进行线性加权。
-
CutMix:将一幅图像的部分区域替换为另一幅图像的区域,并更新标签。
-
AugMix:对同一幅图像进行多次不同的混合变换,再加权合成。
5. 基于学习的增广策略
-
AutoAugment:使用强化学习搜索最优的增广策略。
-
RandAugment:随机选择多种变换方法并调整强度。
图片增广的作用
图片增广通过增加数据的多样性,降低模型对特定属性的依赖,从而提升模型的泛化能力。例如,通过随机裁剪和旋转,模型可以学习到目标物体在不同位置和角度的特征,减少对物体位置的依赖。
实现示例
以下是使用PyTorch实现图片增广的代码示例:
test_transform = transforms.Compose([
transforms.ToTensor(),
]) # 测试集只需要转为张量
train_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.RandomResizedCrop(HW),
transforms.RandomHorizontalFlip(),
autoaugment.AutoAugment(),
transforms.ToTensor(),
# transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])
]) # 训练集需要做各种变换。
应用
if mode == 'test':
x = self._readfile(imgPaths,label=False)
self.transform = test_transform #从文件读数据,测试机和无标签数据没有标签, trans方式也不一样
elif mode == 'train':
x, y =self._readfile(imgPaths,label=True)
self.transform = train_transform
elif mode == 'val':
x, y =self._readfile(imgPaths,label=True)
self.transform = test_transform
elif mode == 'train_unl':
x = self._readfile(imgPaths,label=False)
self.transform = train_transform
通过这些方法,可以有效地扩充数据集,提升模型性能。
但是对于不同的数据部分需要区别处理
图形增广和数据增广
-
数据增广是一个通用的概念,适用于所有类型的数据,目的是通过增加数据的多样性来提升模型的泛化能力。
-
图片增广是数据增广在图像数据上的具体应用,专注于通过几何变换、颜色变换等方法扩充图像数据集,以提升计算机视觉模型的性能。
Adam和AdamW
Adam和AdamW都是深度学习中常用的优化算法,用于训练神经网络。它们都是基于梯度下降法的变体,旨在加速训练过程并提高模型性能。
1. 梯度的改变
-
梯度:在深度学习中,梯度是指损失函数相对于模型参数的导数,它指示了参数应该如何调整以最小化损失。
-
Adam和AdamW:这两种算法都利用梯度的一阶矩(均值)和二阶矩(未中心化的方差)来动态调整每个参数的学习率,从而实现更有效的参数更新。
2. 学习率的改变
-
学习率:学习率是梯度下降法中的一个关键超参数,它决定了在每次迭代中参数更新的步长。
-
动态调整:Adam和AdamW算法通过计算梯度的指数加权平均来动态调整学习率,这有助于在训练过程中自适应地调整步长,从而加速收敛并避免陷入局部最小值。
图表解释
学习率并不是恒定的,而是随着训练过程的进行而变化。这种变化可能是由于:
-
学习率衰减:随着训练的进行,学习率逐渐减小,以帮助模型在接近最优解时更精细地调整参数。
-
自适应调整:Adam和AdamW算法根据梯度信息自适应地调整学习率,以优化训练过程。
Adam和AdamW的区别
-
AdamW:是Adam算法的一个变种,它在更新参数时对权重衰减(weight decay)进行了修正。在标准的Adam算法中,权重衰减是加在梯度上的,这可能导致在进行梯度更新时对权重衰减的影响被放大。AdamW通过将权重衰减直接应用于参数更新,从而解决了这个问题,使得权重衰减的效果更加稳定和一致。
总的来说,这张图强调了Adam和AdamW算法在训练深度学习模型时对梯度和学习率的动态调整,这是它们能够有效优化模型性能的关键因素。
迁移学习
核心思想是利用在一个任务上已经学到的知识来帮助解决另一个相关但不同的任务。
可以显著减少模型训练所需的数据量和计算资源,同时提高模型在新任务上的性能。够显著加快模型训练速度并提高其泛化能力。
迁移学习在许多实际应用中都得到了广泛使用,特别是在数据不足或训练成本较高的场景下。
如何将一个在大规模数据集(如ImageNet)上预训练的模型应用于特定任务(如医学图像分类)?
迁移学习的过程
-
预训练模型:
-
使用ImageNet数据集预训练一个深度学习模型,该数据集包含1000个类别的图像。
-
预训练模型在大量图像上学习了丰富的特征表示。
-
-
迁移学习:
-
将预训练模型应用于特定任务,如医学图像分类。
-
由于预训练模型已经学习了通用的特征表示,因此可以迁移到新任务上,而无需从头开始训练。
-
-
微调(Fine-tuning):
-
在新任务的数据集上对预训练模型进行微调。
-
通过在特定任务的数据集上继续训练,模型可以学习到与新任务相关的特定特征。
-
微调通常涉及调整模型的最后几层,以适应新任务的类别数。
-
-
特征提取器(Feature Extractor):
-
预训练模型的前面几层可以作为特征提取器,用于提取图像的高级特征。
-
这些特征可以用于新任务的分类器。
-
-
分类器(Classifier):
-
在特征提取器的输出上添加一个新的分类器层,用于新任务的分类。
-
分类器层的输出对应于新任务的类别数(如11类)。
-
-
应用示例:
-
图中展示了一个医学图像分类任务,需要区分正常(Normal)和异常(Abnormal)两类。
-
使用迁移学习可以显著减少训练数据的需求,并提高分类的准确性。
-
迁移学习和微调技术在实际应用中非常广泛,特别是在数据稀缺或计算资源有限的情况下。
好的,我来更详细地解释一下微调和特征提取器的概念。
微调(Fine-tuning)
微调是迁移学习中的一个重要步骤,它的目标是让预训练模型更好地适应新任务。具体来说,微调包括以下几个关键点:
-
保留预训练模型的前面几层:
-
预训练模型已经在大规模数据集(如ImageNet)上训练过,前面的层(如卷积层)已经学习到了丰富的通用特征(如边缘、纹理等)。
-
这些通用特征对于新任务也是有用的,因此我们保留这些层,不从头开始训练。
-
-
替换或调整最后几层:
-
预训练模型的最后一层通常是分类层,其输出类别数与训练数据集的类别数相匹配(如1000类)。
-
对于新任务,类别数可能不同(如11类),因此我们需要替换或调整最后几层,以适应新任务的类别数。
-
-
在新任务数据集上继续训练:
-
使用新任务的数据集对模型进行继续训练,让模型学习到与新任务相关的特定特征。
-
这个过程称为微调,因为它是在预训练模型的基础上进行的,而不是从头开始训练。
-
-
冻结部分层:
-
在微调过程中,可以冻结预训练模型的前面几层,只训练最后几层。
-
这样可以避免破坏前面层学到的通用特征,同时让模型专注于学习新任务的特定特征。
-
特征提取器(Feature Extractor)
特征提取器是迁移学习中的一个关键概念,它指的是利用预训练模型的前面几层来提取图像的高级特征。具体来说:
-
提取通用特征:
-
预训练模型的前面几层(如卷积层)已经学习到了丰富的通用特征,这些特征对于多种图像任务都是有用的。
-
通过将这些层作为特征提取器,我们可以复用这些通用特征,而无需从头开始学习。
-
-
减少训练数据需求:
-
由于特征提取器已经提供了高级特征,因此新任务的分类器只需要学习如何组合这些特征来进行分类。
-
这大大减少了新任务所需的训练数据量,因为分类器更容易从少量数据中学习到有效的模式。
-
-
提高模型性能:
-
使用特征提取器可以提高新任务模型的性能,因为它利用了预训练模型在大规模数据集上学到的知识。
-
这可以帮助新任务模型更快地收敛,并在测试数据上取得更好的结果。
-
-
灵活性:
-
特征提取器可以与不同的分类器结合使用,以适应不同的任务需求。
-
例如,可以添加一个全连接层或支持向量机作为分类器,以适应新任务的类别数和输出格式。
-
总结
微调和特征提取器是迁移学习中的两个关键步骤,它们利用预训练模型的知识来加速新任务的学习过程。
-
微调:在新任务的数据集上继续训练预训练模型,调整最后几层以适应新任务的类别数。
-
特征提取器:利用预训练模型的前面几层提取图像的高级特征,为新任务的分类器提供有用的输入。
通过结合微调和特征提取器,迁移学习可以在减少训练数据需求的同时,提高新任务模型的性能和泛化能力。
半监督学习
半监督学习是什么?
半监督学习是一种机器学习方法,它结合了有标签数据(已经知道答案的数据)和无标签数据(没有答案,需要预测的数据)来训练模型。
为什么使用半监督学习?
-
数据标签成本高:在很多情况下,获取大量有标签数据的成本非常高,因为需要专家来标记。
-
利用无标签数据:半监督学习允许我们利用大量的无标签数据来提高模型的性能,而不需要为这些数据支付标签费用。
-
提高模型泛化能力:通过结合有标签和无标签数据,模型可以更好地理解和泛化新数据。
流程
-
有标签数据:
-
这些是已经知道答案的数据,比如一些图片已经被标记为“猫”或“狗”。
-
这些数据用来训练模型,让模型学习到正确的分类方式。
-
-
无标签数据:
-
这些是没有标记的数据,我们不知道这些图片是什么。
-
这些数据用来帮助模型做出预测,即使我们不知道正确答案。
-
-
模型:
-
模型就像是一个学生,它通过学习有标签数据来理解如何分类。
-
然后,模型尝试用学到的知识来预测无标签数据的答案。
-
-
预测值:
-
模型对无标签数据做出的预测,比如它可能会说某张图片是“猫”。
-
这些预测可能不是100%准确,但可以帮助模型更好地学习。
-
-
超过一定可信度:
-
当模型对某个预测非常有信心时(比如它99%确定某张图片是“猫”),这个预测就会被用来进一步训练模型。
-
这样,模型就可以从自己的预测中学习,不断提高自己的准确性。
-
总结
半监督学习是一种聪明的方法,它让模型通过学习少量的有标签数据和大量的无标签数据来提高自己的预测能力。这种方法特别适合在标签数据稀缺但无标签数据丰富的情况下使用。
冻结权重(Freezing Weights)
核心概念
在 PyTorch 中,每个模型参数(如卷积层的权重、全连接层的权重等)都有一个属性叫做 requires_grad
。这个属性决定了该参数是否会在训练过程中被更新。
-
requires_grad=True
:表示该参数在训练时会计算梯度,并根据梯度更新权重。 -
requires_grad=False
:表示该参数在训练时不会计算梯度,权重保持不变。
为什么需要冻结权重?
冻结权重的主要目的是保留预训练模型的特征提取能力。预训练模型(如在 ImageNet 上训练的 ResNet)已经学习到了通用的图像特征(如边缘、纹理等)。这些特征对于大多数图像任务都是有用的。
如果你在自己的任务上重新训练整个模型,可能会破坏这些通用特征,尤其是当你的数据量较小时。因此,冻结权重可以确保这些特征保持不变,只对模型的最后一部分(如分类层)进行训练。
类比
想象你有一辆已经调校好的赛车,它在各种赛道上都表现出色。现在你想要让它在新的赛道上跑得更快,但你不想重新调整整个赛车的引擎和底盘(因为它们已经很完美了)。你只需要调整赛车的轮胎(分类层),以适应新的赛道。冻结权重就像是保留了赛车的引擎和底盘,只调整轮胎。
代码示例
model = models.resnet18(pretrained=True) # 加载预训练的 ResNet18
# 冻结所有参数
for param in model.parameters():
param.requires_grad = False
# 替换最后一层(分类层),并允许其更新
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, num_classes) # 替换为新的分类层
线性探测(Linear Probing)
核心概念
线性探测是一种特殊的迁移学习方法,它的核心思想是:
-
冻结特征提取部分:保留预训练模型的特征提取层(如卷积层),不更新这些层的权重。
-
仅训练分类头:只训练模型的最后一层(分类层),以适应新的任务。
为什么使用线性探测?
线性探测的主要目的是快速评估预训练模型的特征提取能力。通过只训练分类层,你可以快速了解预训练模型的特征是否适用于你的任务,而不需要重新训练整个模型。
类比
想象你有一台已经调试好的相机,它拍出的照片质量很高。现在你想要用这些照片来训练一个图像分类器。你不需要重新调整相机的镜头(特征提取层),只需要调整照片的标签(分类层)。线性探测就像是直接使用相机拍出的照片,只训练标签部分。
代码示例
def set_parameter_requires_grad(model, linear_probing):
if linear_probing:
for param in model.parameters():
param.requires_grad = False # 冻结所有参数
model = models.resnet18(pretrained=True) # 加载预训练的 ResNet18
set_parameter_requires_grad(model, linear_probing=True) # 冻结特征提取层
# 替换最后一层(分类层),并允许其更新
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, num_classes) # 替换为新的分类层
冻结权重与线性探测的关系
线性探测是冻结权重的一个应用场景。具体来说:
-
冻结权重:是 PyTorch 中的一个技术手段,用于控制哪些参数在训练时更新。
-
线性探测:是一种迁移学习策略,通过冻结特征提取层,只训练分类层,快速适应新任务。
为什么这些方法有用?
-
节省计算资源:重新训练整个模型需要大量的计算资源和时间,尤其是对于大型模型(如 ResNet、VGG 等)。通过冻结权重,你可以只训练一小部分参数,大大减少计算量。
-
保留通用特征:预训练模型的特征提取层已经学习到了通用的图像特征,这些特征对大多数任务都是有用的。冻结这些层可以保留这些特征,避免因过拟合而破坏它们。
-
快速适应新任务:线性探测允许你在短时间内评估预训练模型的特征是否适用于你的任务,而不需要重新训练整个模型。
实际应用中的例子
假设你有一个小规模的数据集(如 1000 张图像),目标是训练一个图像分类器。你可以选择以下几种策略:
-
从头开始训练:训练一个全新的模型,但需要大量的数据和计算资源。
-
迁移学习:加载一个预训练的模型,冻结所有层,只训练最后一层(分类层)。这可以快速适应新任务。
-
线性探测:加载一个预训练的模型,冻结特征提取层,只训练分类层。这可以快速评估模型的特征是否有效。
总结
-
冻结权重:通过设置
requires_grad=False
,保留预训练模型的特征提取能力,只训练需要的部分。 -
线性探测:冻结特征提取层,只训练分类层,快速适应新任务。
-
类比:冻结权重就像是保留赛车的引擎,只调整轮胎;线性探测就像是直接使用相机拍出的照片,只训练标签部分。
迁移学习与线性探测的关系
-
共同点:
-
都利用预训练模型:两者都从预训练模型开始,利用其在大规模数据集上学习到的特征。
-
都替换分类层:两者都替换预训练模型的最后一层,以适应新的任务。
-
-
不同点:
-
训练范围:
-
迁移学习:对整个模型(包括特征提取层和分类层)进行微调。
-
线性探测:只训练分类层,冻结特征提取层。
-
-
目标:
-
迁移学习:通过微调整个模型,使其更好地适应新任务。
-
线性探测:快速评估预训练特征的有效性,避免重新训练整个模型。
-
-
为什么需要这两种方法?
-
迁移学习:
-
适用场景:当你有足够的数据和计算资源时,可以对整个模型进行微调,以获得更好的性能。
-
优点:可以充分利用预训练模型的特征,并根据新任务进行调整。
-
-
线性探测:
-
适用场景:当你数据量较小或计算资源有限时,可以快速评估预训练模型的特征是否有效。
-
优点:节省时间和计算资源,快速了解模型的适应性。
-
总结
-
迁移学习:
-
操作:加载预训练模型,替换分类层,微调整个模型。
-
目标:快速适应新任务,提高性能。
-
-
线性探测:
-
操作:加载预训练模型,冻结特征提取层,只训练分类层。
-
目标:快速评估特征的有效性,减少计算资源。
-
关系:
-
线性探测是迁移学习的一种特殊情况,专注于快速评估预训练特征的有效性。
-
迁移学习更通用,可以对整个模型进行微调,以获得更好的性能。
随机种子
随机种子(Random Seed)是一个用于初始化随机数生成器(Random Number Generator, RNG)的值。它在深度学习和机器学习中非常重要,因为它可以确保每次运行代码时的随机行为是完全一致的。这在科学研究和工程实践中非常有用,因为可重复性是验证实验结果的关键。
随机种子的作用
-
确保可重复性:
-
深度学习中涉及大量的随机操作,例如权重初始化、数据增强、随机采样等。如果每次运行代码时这些随机操作的结果都不同,那么实验结果也会不同,这使得实验难以复现。
-
通过设置随机种子,可以确保每次运行代码时的随机行为是一致的,从而保证实验结果的可重复性。
-
-
调试和验证:
-
在开发和调试模型时,能够复现问题是非常重要的。如果每次运行代码的结果都不同,很难确定问题的根源。
-
通过设置随机种子,可以确保每次运行代码时的行为一致,便于调试和验证。
-
-
科学研究:
-
在科学研究中,实验结果的可重复性是验证研究有效性的关键。通过设置随机种子,可以确保其他研究者能够复现你的实验结果。
-
为什么随机种子不随机?
随机种子本身是一个固定的值,但它用于初始化随机数生成器,从而生成一系列随机数。虽然种子是固定的,但生成的随机数序列是确定的。这意味着,每次使用相同的种子时,生成的随机数序列是相同的。这就是为什么随机种子不随机的原因。
总结
随机种子是一个固定的值,用于初始化随机数生成器,从而生成一系列确定的随机数。通过设置随机种子,可以确保每次运行代码时的随机行为一致,从而保证实验结果的可重复性。虽然种子本身是固定的,但它生成的随机数序列是确定的,这就是为什么随机种子不随机的原因。
模型训练流程
逻辑链:处理数据集-> 定义模型 -> 训练模型 ->评估
处理数据集
import numpy as np
from torch.utils.data import Dataset,DataLoader
import torch
from sklearn.model_selection import train_test_split
import os
import cv2
from torchvision.transforms import transforms,autoaugment
from tqdm import tqdm
import random
from PIL import Image
import matplotlib.pyplot as plt
HW = 224
imagenet_norm = [[0.485, 0.456, 0.406], [0.229, 0.224, 0.225]]
test_transform = transforms.Compose([
transforms.ToTensor(),
]) # 测试集只需要转为张量
train_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.RandomResizedCrop(HW),
transforms.RandomHorizontalFlip(),
autoaugment.AutoAugment(),
transforms.ToTensor(),
# transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])
]) # 训练集需要做各种变换。
class foodDataset(Dataset): #数据集三要素: init , getitem , len
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, label=False)
self.transform = test_transform #从文件读数据,测试机和无标签数据没有标签, trans方式也不一样
elif mode == 'train':
x, y =self._readfile(imgPaths, label=True)
self.transform = train_transform
elif mode == 'val':
x, y =self._readfile(imgPaths, label=True)
self.transform = test_transform
elif mode == 'train_unl':
x = self._readfile(imgPaths, label=False)
self.transform = train_transform
if y is not None: # 注意, 分类的标签必须转换为长整型: int64.
y = torch.LongTensor(y)
self.x, self.y = x, y
def __getitem__(self, index): # getitem 用于根据标签取数据
orix = self.x[index] # 取index的图片
if self.transform == None:
xT = torch.tensor(orix).float()
else:
xT = self.transform(orix) # 如果规定了transformer, 则需要transf
if self.y is not None: # 有标签, 则需要返回标签。 这里额外返回了原图, 方便后面画图。
y = self.y[index]
return xT, y, orix
else:
return xT, orix
def __len__(self): # len函数 负责返回长度。
return len(self.x)
class noLabDataset(Dataset):
def __init__(self,dataloader, model, device, thres=0.85):
super(noLabDataset, self).__init__()
self.model = model #模型也要传入进来
self.device = device
self.thres = thres #这里置信度阈值 我设置的 0.99
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.x = np.concatenate((np.array(x), train_dataset.x),axis=0)
# self.y = torch.cat(((torch.LongTensor(y),train_dataset.y)),dim=0)
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(dim=1) #softmax 可以返回一个概率分布
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): # getitem 和len
x = self.x[index]
x= self.transformers(x)
y = self.y[index]
return x, y
def __len__(self):
return len(self.x)
def get_semi_loader(dataloader,model, device, thres):
semi_set = noLabDataset(dataloader, model, device, thres)
if semi_set.flag: #不可用时返回空
dataloader = DataLoader(semi_set, batch_size=dataloader.batch_size,shuffle=True)
return dataloader
else:
return None
def getDataLoader(path, mode, batchSize):
assert mode in ['train', 'train_unl', 'val', 'test']
dataset = foodDataset(path, mode)
if mode in ['test','train_unl']:
shuffle = False
else:
shuffle = True
loader = DataLoader(dataset,batchSize,shuffle=shuffle) #装入loader
return loader
def samplePlot(dataset, isloader=True, isbat=False,ori=None): #画图, 此函数不需要掌握。
if isloader:
dataset = dataset.dataset
rows = 3
cols = 3
num = rows*cols
# if isbat:
# dataset = dataset * 225
datalen = len(dataset)
randomNum = []
while len(randomNum) < num:
temp = random.randint(0,datalen-1)
if temp not in randomNum:
randomNum.append(temp)
fig, axs = plt.subplots(nrows=rows,ncols=cols,squeeze=False)
index = 0
for i in range(rows):
for j in range(cols):
ax = axs[i, j]
if isbat:
ax.imshow(np.array(dataset[randomNum[index]].cpu().permute(1,2,0)))
else:
ax.imshow(np.array(dataset[randomNum[index]][0].cpu().permute(1,2,0)))
index += 1
ax.set(xticklabels=[], yticklabels=[], xticks=[], yticks=[])
plt.show()
plt.tight_layout()
if ori != None:
fig2, axs2 = plt.subplots(nrows=rows,ncols=cols,squeeze=False)
index = 0
for i in range(rows):
for j in range(cols):
ax = axs2[i, j]
if isbat:
ax.imshow(np.array(dataset[randomNum[index]][-1]))
else:
ax.imshow(np.array(dataset[randomNum[index]][-1]))
index += 1
ax.set(xticklabels=[], yticklabels=[], xticks=[], yticks=[])
plt.show()
plt.tight_layout()
if __name__ == '__main__': #运行的模块, 如果你运行的模块是当前模块
print("你运行的是data.py文件")
filepath = '../food-11_sample'
train_loader = getDataLoader(filepath, 'train', 8)
for i in range(3):
samplePlot(train_loader,True,isbat=False,ori=True)
val_loader = getDataLoader(filepath, 'val', 8)
for i in range(100):
samplePlot(val_loader,True,isbat=False,ori=True)
##########################
重点
-
数据预处理和增强
-
为什么需要数据增强?
-
torchvision.transforms
的常用方法:RandomResizedCrop
、RandomHorizontalFlip
、AutoAugment
等。 -
归一化的作用:
Normalize
。
-
-
自定义数据集类
-
Dataset
类的结构:__init__
、__getitem__
和__len__
方法。 -
不同模式的数据加载:训练集、验证集、测试集和未标记数据集。
-
数据增强的应用:在
__getitem__
方法中应用train_transform
或test_transform
。
-
-
半监督学习的数据处理
-
伪标签生成的逻辑:模型预测、置信度筛选。
-
noLabDataset
类的作用:生成伪标签数据集。 -
动态更新数据加载器:
get_semi_loader
函数。
-
-
数据加载器
-
DataLoader
的作用:批量加载数据。 -
如何创建数据加载器:
getDataLoader
函数。 -
shuffle
参数的作用:是否打乱数据。
-
-
数据可视化
-
samplePlot
函数的作用:随机选择样本并可视化。 -
如何使用
matplotlib
绘制图像:imshow
方法。
-
好的,我将对第2、3、4部分进行详细补充,帮助你更深入地理解每个部分的实现和逻辑。
1. 自定义数据集类 foodDataset
详细解析
foodDataset
是一个自定义的 Dataset
类,用于加载和处理食物分类数据集。它支持四种模式:训练集(train
)、验证集(val
)、测试集(test
)和未标记数据集(train_unl
)。
1.1 数据集类的结构
-
__init__
方法:-
初始化数据集,加载图像路径和标签。
-
根据模式选择不同的预处理和增强方法。
-
path
:数据集的根路径。 -
mode
:数据集的模式(train
、val
、test
、train_unl
)。 -
transform
:根据模式选择不同的数据增强方法。
-
-
__getitem__
方法:-
根据索引获取单个样本及其标签。
-
如果有标签,返回预处理后的图像和标签;如果没有标签,只返回预处理后的图像。
-
orix
:原始图像。 -
xT
:预处理后的图像。 -
y
:标签(如果有)。
-
-
__len__
方法:-
返回数据集的长度。
-
1.2 数据加载逻辑
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, label=False)
self.transform = test_transform
elif mode == 'train':
x, y = self._readfile(imgPaths, label=True)
self.transform = train_transform
elif mode == 'val':
x, y = self._readfile(imgPaths, label=True)
self.transform = test_transform
elif mode == 'train_unl':
x = self._readfile(imgPaths, label=False)
self.transform = train_transform
if y is not None:
y = torch.LongTensor(y)
self.x, self.y = x, y
-
_readfile
方法:-
从文件系统中加载图像和标签。
-
如果是测试集或未标记数据集,没有标签。
-
如果是训练集或验证集,加载标签并将其转换为长整型(
torch.LongTensor
)。
-
-
x
和y
:-
x
:图像数据。 -
y
:标签数据(如果有)。
-
1.3 数据预处理和增强
def __getitem__(self, index):
orix = self.x[index] # 取index的图片
if self.transform is None:
xT = torch.tensor(orix).float()
else:
xT = self.transform(orix) # 如果规定了transformer,则需要transf
if self.y is not None: # 有标签,则需要返回标签
y = self.y[index]
return xT, y, orix
else:
return xT, orix
-
transform
:-
根据模式选择不同的预处理和增强方法。
-
训练集使用
train_transform
,包含随机裁剪、水平翻转和自动增强。 -
测试集和验证集使用
test_transform
,仅将图像转换为张量。
-
2. 半监督学习的数据处理 noLabDataset
详细解析
noLabDataset
是一个自定义的 Dataset
类,用于从未标记数据中生成伪标签,并将其作为可信的训练数据。
2.1 数据集类的结构
class noLabDataset(Dataset):
def __init__(self, dataloader, model, device, thres=0.85):
...
def _model_pred(self, dataloader):
...
def __getitem__(self, index):
...
def __len__(self):
...
-
__init__
方法:-
初始化数据集,使用预训练模型对未标记数据进行预测。
-
根据置信度阈值筛选出高置信度的样本及其伪标签。
-
dataloader
:未标记数据的数据加载器。 -
model
:预训练模型。 -
device
:运行设备(CPU 或 GPU)。 -
thres
:置信度阈值。
-
-
_model_pred
方法:-
使用预训练模型对未标记数据进行预测。
-
根据置信度阈值筛选出高置信度的样本及其伪标签。
-
-
__getitem__
方法:-
根据索引获取单个样本及其伪标签。
-
应用数据增强。
-
-
__len__
方法:-
返回数据集的长度。
-
主要功能是从未标记数据中生成高置信度的伪标签,并将这些伪标签数据作为可信的训练数据。具体步骤如下:
-
使用预训练模型对未标记数据进行预测。
-
计算每个样本的最大预测概率(置信度)和对应的标签。
-
根据置信度阈值筛选出高置信度的样本。
-
将筛选出的样本及其伪标签存储起来,用于后续的半监督学习
2.2 伪标签生成逻辑
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(dim=1) # softmax 可以返回一个概率分布
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
原始未标注数据 → 分批次加载 → 模型预测 → Softmax归一化 → 提取置信度
-
Softmax
:-
将模型的预测结果(logits)转换为概率分布。
-
-
pred_max
和preds
:-
pred_max
:每个样本的最大预测概率(置信度)。 -
preds
:每个样本的预测标签。
-
-
置信度筛选:
-
如果某个样本的置信度超过阈值,则将其作为可信的训练数据。
-
2.3 动态更新数据加载器
def get_semi_loader(dataloader, model, device, thres):
semi_set = noLabDataset(dataloader, model, device, thres)
if semi_set.flag: # 不可用时返回空
dataloader = DataLoader(semi_set, batch_size=dataloader.batch_size, shuffle=True)
return dataloader
else:
return None
-
semi_set.flag
:-
如果生成了有效的伪标签数据,则
flag=True
,否则flag=False
。
-
-
DataLoader
:-
根据生成的伪标签数据创建新的数据加载器。
-
3. 数据加载器 getDataLoader
详细解析
getDataLoader
函数用于创建数据加载器,支持训练集、验证集、测试集和未标记数据集。
3.1 数据加载器的结构
def getDataLoader(path, mode, batchSize):
assert mode in ['train', 'train_unl', 'val', 'test']
dataset = foodDataset(path, mode)
if mode in ['test', 'train_unl']:
shuffle = False
else:
shuffle = True
loader = DataLoader(dataset, batchSize, shuffle=shuffle)
return loader
-
assert
:-
确保模式是有效的(
train
、val
、test
、train_unl
)。
-
-
foodDataset
:-
根据路径和模式创建自定义数据集。
-
-
shuffle
:-
训练集和验证集通常需要打乱数据,以避免模型对数据顺序产生依赖。
-
测试集和未标记数据集不需要打乱。
-
-
DataLoader
:-
创建数据加载器,支持
-
定义模型
import torch
import torch.nn as nn
import numpy as np
from timm.models.vision_transformer import PatchEmbed, Block
import torchvision.models as models
def set_parameter_requires_grad(model, linear_probing):
if linear_probing:
for param in model.parameters():
param.requires_grad = False # 一个参数的requires_grad设为false, 则训练时就会不更新
class MyModel(nn.Module): #自己的模型
def __init__(self,numclass = 2):
super(MyModel, self).__init__()
self.layer0 = nn.Sequential(
nn.Conv2d(in_channels=3,out_channels=64,kernel_size=3,stride=1,padding=1,bias=True),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
) #112*112
self.layer1 = nn.Sequential(
nn.Conv2d(in_channels=64,out_channels=128,kernel_size=3,stride=1,padding=1,bias=True),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
) #56*56
self.layer2 = nn.Sequential(
nn.Conv2d(in_channels=128,out_channels=256,kernel_size=3,stride=1,padding=1,bias=True),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
) #28*28
self.layer3 = nn.Sequential(
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=3,stride=1,padding=1,bias=True),
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
) #14*14
self.pool1 = nn.MaxPool2d(2)#7*7
self.fc = nn.Linear(25088, 512)
# self.drop = nn.Dropout(0.5)
self.relu1 = nn.ReLU(inplace=True)
self.fc2 = nn.Linear(512, numclass)
def forward(self,x):
x = self.layer0(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.pool1(x)
x = x.view(x.size()[0],-1) #view 类似于reshape 这里指定了第一维度为batch大小,第二维度为适应的,即剩多少, 就是多少维。
# 这里就是将特征展平。 展为 B*N ,N为特征维度。
x = self.fc(x)
# x = self.drop(x)
x = self.relu1(x)
x = self.fc2(x)
return x
# def model_Datapara(model, device, pre_path=None):
# model = torch.nn.DataParallel(model).to(device)
#
# model_dict = torch.load(pre_path).module.state_dict()
# model.module.load_state_dict(model_dict)
# return model
#传入模型名字,和分类数, 返回你想要的模型
def initialize_model(model_name, num_classes, linear_prob=False, use_pretrained=True):
# 初始化将在此if语句中设置的这些变量。
# 每个变量都是模型特定的。
model_ft = None
input_size = 0
if model_name =="MyModel":
if use_pretrained == True:
model_ft = torch.load('model_save/MyModel')
else:
model_ft = MyModel(num_classes)
input_size = 224
elif model_name == "resnet18":
""" Resnet18
"""
model_ft = models.resnet18(pretrained=use_pretrained) # 从网络下载模型 pretrain true 使用参数和架构, false 仅使用架构。
set_parameter_requires_grad(model_ft, linear_prob) # 是否为线性探测,线性探测: 固定特征提取器不训练。
num_ftrs = model_ft.fc.in_features #分类头的输入维度
model_ft.fc = nn.Linear(num_ftrs, num_classes) # 删掉原来分类头, 更改最后一层为想要的分类数的分类头。
input_size = 224
elif model_name == "resnet50":
""" Resnet50
"""
model_ft = models.resnet50(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, num_classes)
input_size = 224
elif model_name == "googlenet":
""" googlenet
"""
model_ft = models.googlenet(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, num_classes)
input_size = 224
elif model_name == "alexnet":
""" Alexnet
"""
model_ft = models.alexnet(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.classifier[6].in_features
model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
input_size = 224
elif model_name == "vgg":
""" VGG11_bn
"""
model_ft = models.vgg11_bn(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.classifier[6].in_features
model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
input_size = 224
elif model_name == "squeezenet":
""" Squeezenet
"""
model_ft = models.squeezenet1_0(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
model_ft.classifier[1] = nn.Conv2d(512, num_classes, kernel_size=(1,1), stride=(1,1))
model_ft.num_classes = num_classes
input_size = 224
elif model_name == "densenet":
""" Densenet
"""
model_ft = models.densenet121(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.classifier.in_features
model_ft.classifier = nn.Linear(num_ftrs, num_classes)
input_size = 224
elif model_name == "inception":
""" Inception v3
Be careful, expects (299,299) sized images and has auxiliary output
"""
model_ft = models.inception_v3(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
# 处理辅助网络
num_ftrs = model_ft.AuxLogits.fc.in_features
model_ft.AuxLogits.fc = nn.Linear(num_ftrs, num_classes)
# 处理主要网络
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs,num_classes)
input_size = 299
else:
print("Invalid model_utils name, exiting...")
exit()
return model_ft, input_size
def prilearn_para(model_ft, linear_prob):
# 将模型发送到GPU
device = torch.device("cuda:0")
model_ft = model_ft.to(device)
# 在此运行中收集要优化/更新的参数。
# 如果我们正在进行微调,我们将更新所有参数。
# 但如果我们正在进行特征提取方法,我们只会更新刚刚初始化的参数,即`requires_grad`的参数为True。
params_to_update = model_ft.parameters()
print("Params to learn:")
if linear_prob:
params_to_update = []
for name, param in model_ft.named_parameters():
if param.requires_grad == True:
params_to_update.append(param)
print("\t",name)
else:
for name, param in model_ft.named_parameters():
if param.requires_grad == True:
print("\t", name)
#
# # 观察所有参数都在优化
# optimizer_ft = optim.SGD(params_to_update, lr=0.001, momentum=0.9)
def init_para(model):
def weights_init(model):
classname = model.__class__.__name__
if classname.find('Conv') != -1:
nn.init.normal_(model.weight.data, 0.0, 0.02)
elif classname.find('BatchNorm') != -1:
nn.init.normal_(model.weight.data, 1.0, 0.02)
nn.init.constant_(model.bias.data, 0)
model.apply(weights_init)
return model
要理解这段代码,需要从以下几个方面入手,并掌握一些基础知识:
1. PyTorch 基础
-
PyTorch 的核心概念:
-
张量(Tensor):类似于 NumPy 的数组,但可以在 GPU 上加速计算。
-
模型(
nn.Module
):用于定义神经网络结构。 -
层(
nn.Conv2d
,nn.Linear
等):用于构建神经网络的基本模块。 -
激活函数(
nn.ReLU
,nn.Sigmoid
等):为网络引入非线性。 -
优化器(
torch.optim
):用于更新模型参数。 -
损失函数(
nn.CrossEntropyLoss
等):用于衡量模型预测与真实值的差异。
-
-
torch.nn
模块:-
用于构建和定义神经网络的层和模块。
-
例如,
nn.Conv2d
是卷积层,nn.Linear
是全连接层,nn.BatchNorm2d
是批量归一化层。
-
-
torchvision
模块:-
提供了许多预训练的模型(如 ResNet、VGG、Inception 等)。
-
torchvision.models
中的模型可以直接加载并用于迁移学习。
-
2. 模型定义
自定义模型 MyModel
class MyModel(nn.Module):
def __init__(self, numclass=2):
super(MyModel, self).__init__()
self.layer0 = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1, padding=1, bias=True),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
)
...
self.fc = nn.Linear(25088, 512)
self.fc2 = nn.Linear(512, numclass)
-
功能:
-
定义了一个简单的卷积神经网络,包含多个卷积层、池化层和全连接层。
-
使用了批量归一化(
BatchNorm2d
)和 ReLU 激活函数。 -
最后通过全连接层输出分类结果。
-
CNN模型的结构
-
卷积层:用于提取局部特征。
-
批量归一化层:用于调整特征的分布,加速训练。
-
激活层:引入非线性,使模型能够学习复杂的函数。
-
池化层:降低特征图的空间维度,减少计算量。
-
全连接层:用于输出最终的预测结果。
模型初始化
def initialize_model(model_name, num_classes, linear_prob=False, use_pretrained=True):
...
-
功能:
-
根据输入的模型名称(如
resnet18
、vgg
等),加载预训练模型或自定义模型。 -
如果
linear_prob=True
,则冻结模型的特征提取部分,仅训练分类头(线性探测)。 -
如果
use_pretrained=True
,则加载预训练权重。
-
-
需要理解的知识:
-
迁移学习:使用预训练模型的权重作为初始化,然后根据自己的任务进行微调。
-
线性探测(Linear Probing):冻结特征提取部分,仅训练分类头。
-
模型权重的冻结(
requires_grad=False
):防止某些层的权重在训练时更新。
-
3. 模型参数管理
冻结和解冻参数
def set_parameter_requires_grad(model, linear_probing):
if linear_probing:
for param in model.parameters():
param.requires_grad = False
-
功能:
-
如果
linear_probing=True
,则将模型的所有参数设置为不可训练(requires_grad=False
)。
-
-
需要理解的知识:
-
requires_grad
:控制参数是否参与梯度计算和更新。 -
冻结参数:在迁移学习中,冻结预训练模型的权重,仅训练最后一层。
-
4. 模型初始化和权重设置
权重初始化
def init_para(model):
def weights_init(model):
classname = model.__class__.__name__
if classname.find('Conv') != -1:
nn.init.normal_(model.weight.data, 0.0, 0.02)
elif classname.find('BatchNorm') != -1:
nn.init.normal_(model.weight.data, 1.0, 0.02)
nn.init.constant_(model.bias.data, 0)
model.apply(weights_init)
-
功能:
-
使用自定义的权重初始化方法初始化模型的权重。
-
卷积层权重初始化为正态分布,批量归一化层权重初始化为 1,偏置初始化为 0。
-
-
需要理解的知识:
-
权重初始化:对模型的权重进行初始化,可以影响模型的收敛速度和性能。
-
nn.init
:PyTorch 提供的初始化方法。好的,让我们深入探讨一下均值和标准差的设置以及偏置的作用,以及为什么在权重初始化中要这样设置。
1. 均值和标准差的设置
背景
在深度学习中,权重初始化是一个非常重要的步骤,因为它直接影响模型的收敛速度和训练稳定性。如果权重初始化不当,可能会导致以下问题:
-
梯度消失:权重太小,导致梯度在反向传播过程中逐渐趋近于零,使得模型难以训练。
-
梯度爆炸:权重太大,导致梯度在反向传播过程中迅速增大,使得模型训练不稳定。
-
为了避免这些问题,我们需要合理地设置权重的初始值。
2. 均值和标准差的作用
均值(Mean)
-
定义:均值是权重分布的中心位置。
-
作用:在权重初始化中,均值通常设置为
0
或接近0
,因为这可以帮助模型在训练初期保持对称性,避免某些神经元的输出过大或过小。 -
标准差(Standard Deviation)
-
定义:标准差是权重分布的离散程度,表示权重值的波动范围。
-
作用:标准差决定了权重的初始值的范围。如果标准差太小,权重值会集中在均值附近,可能导致梯度消失;如果标准差太大,权重值会过于分散,可能导致梯度爆炸。
-
3. 常见的初始化方法
-
正态分布初始化(
nn.init.normal_
) -
公式:权重 W 从正态分布 N(μ,σ2) 中采样。
-
μ 是均值。
-
σ 是标准差。
-
-
常见设置:
-
卷积层:均值 μ=0.0,标准差 σ=0.02。
-
原因:较小的标准差可以避免权重值过大,从而减少梯度爆炸的风险。
-
目的:让权重值在训练初期保持在一个合理的范围内,使得梯度能够有效地传播。
-
-
-
- Xavier 初始化(
nn.init.xavier_normal_
或nn.init.xavier_uniform_
)-
公式:权重 W 的标准差由输入和输出神经元的数量决定。
-
σ=nin+nout2
-
-
目的:保持输入和输出的方差一致,避免梯度消失或爆炸。
-
Kaiming 初始化(
nn.init.kaiming_normal_
或nn.init.kaiming_uniform_
) -
公式:权重 W 的标准差由输入神经元的数量决定。
-
σ=nin2
-
-
目的:适用于 ReLU 激活函数,可以更好地保持梯度的传播。
-
4. 偏置(Bias)的初始化
定义
-
偏置是神经网络中的一个参数,用于在激活函数之前添加一个常数偏移量。
-
作用
-
在卷积层和全连接层中:
-
偏置通常初始化为
0
,因为初始时不需要引入额外的偏移。 -
这种设置可以简化模型的初始状态,避免某些神经元的输出在训练初期就偏离零点。
-
-
在批量归一化层中:
-
批量归一化层的偏置(
beta
)也初始化为0
。 -
原因:批量归一化的作用是调整特征的均值和方差,使其接近标准正态分布。偏置初始化为
0
可以保持这种调整的初始状态。
-
-
均值为
0.0
:保持权重的对称性,避免某些神经元的输出在训练初期过大或过小。 -
标准差为
0.02
:较小的标准差可以避免权重值过大,从而减少梯度爆炸的风险,同时允许梯度有效地传播。 -
批量归一化层
-
权重(
gamma
)初始化为正态分布(均值为1.0
,标准差为0.02
):-
原因:批量归一化的作用是调整特征的尺度。权重初始化为接近
1
可以保持特征的初始尺度不变。 -
目的:在训练初期,特征的尺度保持稳定,避免因权重过大或过小导致的不稳定。
-
-
偏置(
beta
)初始化为0
:-
原因:偏置的作用是调整特征的偏移量。初始化为
0
可以保持特征的初始偏移量为零。 -
目的:在训练初期,特征的偏移量保持稳定,避免因偏置过大导致的不稳定。
-
-
总结
-
均值和标准差的设置:
-
卷积层:均值为
0.0
,标准差为0.02
,以保持权重的对称性和避免梯度爆炸。 -
批量归一化层:权重初始化为正态分布(均值为
1.0
,标准差为0.02
),以保持特征的初始尺度稳定。
-
-
偏置的设置:
-
偏置初始化为
0
,以保持特征的初始偏移量为零,避免训练初期的不稳定。
-
-
这些设置的目的是为了在训练初期保持模型的稳定,加速收敛,并避免梯度消失或爆炸的问题。
-
5. 模型并行化和设备管理
模型并行化
# model = torch.nn.DataParallel(model).to(device)
-
功能:
-
使用
DataParallel
将模型并行化,利用多 GPU 加速训练。
-
-
需要理解的知识:
-
多 GPU 训练:使用
DataParallel
或DistributedDataParallel
在多 GPU 上训练模型。 -
设备管理:将模型和数据移动到 GPU 上(
to(device)
)。
-
6. 模型参数更新
参数更新
def prilearn_para(model_ft, linear_prob):
params_to_update = model_ft.parameters()
if linear_prob:
params_to_update = [param for param in model_ft.parameters() if param.requires_grad]
-
功能:
-
根据
linear_prob
的值,选择需要更新的参数。 -
如果
linear_prob=True
,则只更新requires_grad=True
的参数。
-
-
需要理解的知识:
-
参数更新:在训练过程中,只有
requires_grad=True
的参数会更新。 -
优化器:如
SGD
或Adam
,用于更新模型参数。
-
训练模型
代码
from tqdm import tqdm
import torch
import time
import matplotlib.pyplot as plt
import numpy as np
from model_utils.data import samplePlot, get_semi_loader
def train_val(para):
########################################################
model = para['model']
semi_loader = para['no_label_Loader']
train_loader =para['train_loader']
val_loader = para['val_loader']
optimizer = para['optimizer']
loss = para['loss']
epoch = para['epoch']
device = para['device']
save_path = para['save_path']
save_acc = para['save_acc']
pre_path = para['pre_path']
max_acc = para['max_acc']
val_epoch = para['val_epoch']
acc_thres = para['acc_thres']
conf_thres = para['conf_thres']
do_semi= para['do_semi']
semi_epoch = 10
###################################################
no_label_Loader = None
if pre_path != None:
model = torch.load(pre_path)
model = model.to(device)
# model = torch.nn.DataParallel(model).to(device)
# model.device_ids = [0,1]
plt_train_loss = []
plt_train_acc = []
plt_val_loss = []
plt_val_acc = []
plt_semi_acc = []
val_rel = []
max_acc = 0
for i in range(epoch):
start_time = time.time()
model.train()
train_loss = 0.0
train_acc = 0.0
val_acc = 0.0
val_loss = 0.0
semi_acc = 0.0
for data in tqdm(train_loader): #取数据
optimizer.zero_grad() # 梯度置0
x, target = data[0].to(device), data[1].to(device)
pred = model(x) #模型前向
bat_loss = loss(pred, target) # 算交叉熵loss
bat_loss.backward() # 回传梯度
optimizer.step() # 根据梯度更新
train_loss += bat_loss.item() #.detach 表示去掉梯度
train_acc += np.sum(np.argmax(pred.cpu().data.numpy(),axis=1) == data[1].numpy())
# 预测值和标签相等,正确数就加1. 相等多个, 就加几。
if no_label_Loader != None:
for data in tqdm(no_label_Loader):
optimizer.zero_grad()
x , target = data[0].to(device), data[1].to(device)
pred = model(x)
bat_loss = loss(pred, target)
bat_loss.backward()
optimizer.step()
semi_acc += np.sum(np.argmax(pred.cpu().data.numpy(),axis=1)== data[1].numpy())
plt_semi_acc .append(semi_acc/no_label_Loader.dataset.__len__())
print('semi_acc:', plt_semi_acc[-1])
plt_train_loss.append(train_loss/train_loader.dataset.__len__())
plt_train_acc.append(train_acc/train_loader.dataset.__len__())
if i % val_epoch == 0:
model.eval()
with torch.no_grad():
for valdata in val_loader:
val_x , val_target = valdata[0].to(device), valdata[1].to(device)
val_pred = model(val_x)
val_bat_loss = loss(val_pred, val_target)
val_loss += val_bat_loss.cpu().item()
val_acc += np.sum(np.argmax(val_pred.cpu().data.numpy(), axis=1) == valdata[1].numpy())
val_rel.append(val_pred)
val_acc = val_acc/val_loader.dataset.__len__()
if val_acc > max_acc:
torch.save(model, save_path)
max_acc = val_acc
plt_val_loss.append(val_loss/val_loader.dataset.__len__())
plt_val_acc.append(val_acc)
print('[%03d/%03d] %2.2f sec(s) TrainAcc : %3.6f TrainLoss : %3.6f | valAcc: %3.6f valLoss: %3.6f ' % \
(i, epoch, time.time()-start_time, plt_train_acc[-1], plt_train_loss[-1], plt_val_acc[-1], plt_val_loss[-1])
)
else:
plt_val_loss.append(plt_val_loss[-1])
plt_val_acc.append(plt_val_acc[-1])
if do_semi and plt_val_acc[-1] > acc_thres and i % semi_epoch==0: # 如果启用半监督, 且精确度超过阈值, 则开始。
no_label_Loader = get_semi_loader(semi_loader, semi_loader, model, device, conf_thres)
plt.plot(plt_train_loss) # 画图。
plt.plot(plt_val_loss)
plt.title('loss')
plt.legend(['train', 'val'])
plt.show()
plt.plot(plt_train_acc)
plt.plot(plt_val_acc)
plt.title('Accuracy')
plt.legend(['train', 'val'])
plt.savefig('acc.png')
plt.show()
1. 函数定义
def train_val(para):
-
输入参数
para
是一个字典,包含了训练所需的配置信息,例如模型、数据加载器、优化器、损失函数等。
2. 参数初始化
model = para['model']
semi_loader = para['no_label_Loader']
train_loader = para['train_loader']
val_loader = para['val_loader']
optimizer = para['optimizer']
loss = para['loss']
epoch = para['epoch']
device = para['device']
save_path = para['save_path']
save_acc = para['save_acc']
pre_path = para['pre_path']
max_acc = para['max_acc']
val_epoch = para['val_epoch']
acc_thres = para['acc_thres']
conf_thres = para['conf_thres']
do_semi = para['do_semi']
-
从
para
字典中提取训练所需的参数。
3. 模型加载和设备配置
if pre_path is not None:
model = torch.load(pre_path) # 加载预训练模型
model = model.to(device) # 将模型移动到指定设备
-
如果提供了预训练模型路径
pre_path
,则加载预训练模型。 -
将模型移动到指定的设备(如 GPU)。
4. 初始化记录变量
plt_train_loss = []
plt_train_acc = []
plt_val_loss = []
plt_val_acc = []
plt_semi_acc = []
val_rel = []
max_acc = 0
-
初始化用于记录训练和验证损失、准确率的列表,以及用于保存最佳模型准确率的变量。
5. 训练循环
for i in range(epoch):
start_time = time.time()
model.train()
train_loss = 0.0
train_acc = 0.0
val_acc = 0.0
val_loss = 0.0
semi_acc = 0.0
-
遍历指定的训练轮数
epoch
。 -
初始化每轮的训练和验证损失、准确率。
6. 训练过程
for data in tqdm(train_loader): # 遍历训练数据加载器
optimizer.zero_grad() # 清空梯度
x, target = data[0].to(device), data[1].to(device) # 将数据移动到设备
pred = model(x) # 模型前向传播
bat_loss = loss(pred, target) # 计算损失
bat_loss.backward() # 反向传播
optimizer.step() # 更新参数
train_loss += bat_loss.item() # 累加训练损失
train_acc += np.sum(np.argmax(pred.cpu().data.numpy(), axis=1) == target.cpu().numpy()) # 累加训练准确率
-
使用
tqdm
显示进度条。 -
对每个批次的数据进行前向传播、计算损失、反向传播和参数更新。
-
累加训练损失和准确率。
过程:梯度清零(防止累计梯度)->前向传播(计算预测值)->计算损失(衡量预测误差)->反向传播(计算梯度)->参数更新(根据梯度调整权重)
为什么需要 optimizer.zero_grad()?
梯度是累积计算的,如果不清零会导致不同批次的梯度叠加,影响参数更新方向。
loss.backward() 发生了什么?
通过反向传播算法,从输出层到输入层逐层计算每个参数对损失的贡献(梯度)。
7. 半监督学习
if no_label_Loader is not None:
for data in tqdm(no_label_Loader): # 遍历半监督数据加载器
optimizer.zero_grad()
x, target = data[0].to(device), data[1].to(device)
pred = model(x)
bat_loss = loss(pred, target)
bat_loss.backward()
optimizer.step()
semi_acc += np.sum(np.argmax(pred.cpu().data.numpy(), axis=1) == target.cpu().numpy())
plt_semi_acc.append(semi_acc / no_label_Loader.dataset.__len__())
print('semi_acc:', plt_semi_acc[-1])
-
如果启用了半监督学习(
no_label_Loader
不为空),则对未标记数据进行训练。 -
累加半监督学习的准确率。
8. 记录训练和验证指标
plt_train_loss.append(train_loss / train_loader.dataset.__len__())
plt_train_acc.append(train_acc / train_loader.dataset.__len__())
-
计算并记录训练损失和准确率。
9. 验证过程
if i % val_epoch == 0: # 每隔 val_epoch 轮进行一次验证
model.eval()
with torch.no_grad(): # 关闭梯度计算
for valdata in val_loader:
val_x, val_target = valdata[0].to(device), valdata[1].to(device)
val_pred = model(val_x)
val_bat_loss = loss(val_pred, val_target)
val_loss += val_bat_loss.cpu().item()
val_acc += np.sum(np.argmax(val_pred.cpu().data.numpy(), axis=1) == val_target.cpu().numpy())
val_acc = val_acc / val_loader.dataset.__len__()
if val_acc > max_acc:
torch.save(model, save_path) # 保存最佳模型
max_acc = val_acc
plt_val_loss.append(val_loss / val_loader.dataset.__len__())
plt_val_acc.append(val_acc)
print('[%03d/%03d] %2.2f sec(s) TrainAcc : %3.6f TrainLoss : %3.6f | valAcc: %3.6f valLoss: %3.6f ' % \
(i, epoch, time.time() - start_time, plt_train_acc[-1], plt_train_loss[-1], plt_val_acc[-1], plt_val_loss[-1]))
-
每隔
val_epoch
轮进行一次验证。 -
计算验证集的损失和准确率。
-
如果当前轮次的验证准确率高于历史最高准确率,则保存当前模型。
-
打印训练和验证的损失及准确率。
10. 动态启用半监督学习
if do_semi and plt_val_acc[-1] > acc_thres and i % semi_epoch == 0: # 如果启用半监督,且精确度超过阈值
no_label_Loader = get_semi_loader(semi_loader, semi_loader, model, device, conf_thres)
-
根据条件动态启用半监督学习。
-
如果验证准确率超过阈值
acc_thres
,则从未标记数据中生成新的伪标签数据加载器。
11. 绘制训练和验证曲线
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title('loss')
plt.legend(['train', 'val'])
plt.show()
plt.plot(plt_train_acc)
plt.plot(plt_val_acc)
plt.title('Accuracy')
plt.legend(['train', 'val'])
plt.savefig('acc.png')
plt.show()
-
使用
matplotlib
绘制训练和验证的损失及准确率曲线。
总结
这个函数实现了一个完整的训练和验证流程,支持半监督学习。它包括以下关键步骤:
-
模型加载和设备配置:加载预训练模型并将其移动到指定设备。
-
训练过程:对训练数据进行前向传播、计算损失、反向传播和参数更新。
-
半监督学习:利用未标记数据生成伪标签,并对这些数据进行训练。
-
验证过程:定期验证模型性能,保存最佳模型。
-
动态启用半监督学习:根据条件从未标记数据中生成新的伪标签数据加载器。
-
绘制训练和验证曲线:可视化训练和验证的损失及准确率。
主函数
import random
import torch
import torch.nn as nn
import numpy as np
import os
from model_utils.model import initialize_model
from model_utils.train import train_val
from model_utils.data import getDataLoader
# os.environ['CUDA_VISIBLE_DEVICES']='0,1'
def seed_everything(seed):
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
random.seed(seed)
np.random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
#################################################################
seed_everything(0)
###############################################
model_name = 'resnet18'
##########################################
num_class = 11
batchSize = 32
learning_rate = 1e-4
loss = nn.CrossEntropyLoss()
epoch = 10
device = 'cuda' if torch.cuda.is_available() else 'cpu'
##########################################
filepath = 'food-11_sample'
# filepath = 'food-11'
##########################
#读数据
train_loader = getDataLoader(filepath, 'train', batchSize)
val_loader = getDataLoader(filepath, 'val', batchSize)
no_label_Loader = getDataLoader(filepath,'train_unl', batchSize)
#模型和超参数
model, input_size = initialize_model(model_name, 11, use_pretrained=False)
print(input_size)
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate,weight_decay=1e-4)
save_path = 'model_save/model.pth'
trainpara = {
"model" : model,
'train_loader': train_loader,
'val_loader': val_loader,
'no_label_Loader': no_label_Loader,
'optimizer': optimizer,
'batchSize': batchSize,
'loss': loss,
'epoch': epoch,
'device': device,
'save_path': save_path,
'save_acc': True,
'max_acc': 0.5,
'val_epoch' : 1,
'acc_thres' : 0.7,
'conf_thres' : 0.99,
'do_semi' : True,
"pre_path" : None
}
这段代码是一个完整的深度学习训练流程的入口脚本,用于训练一个图像分类模型,支持半监督学习。让我们逐步解析这段代码,理解每个部分的作用和逻辑。
1. 导入必要的库
import random
import torch
import torch.nn as nn
import numpy as np
import os
from model_utils.model import initialize_model
from model_utils.train import train_val
from model_utils.data import getDataLoader
-
random
:用于设置 Python 的随机种子。 -
torch
和torch.nn
:PyTorch 深度学习框架,用于构建和训练模型。 -
numpy
:用于数值计算。 -
os
:用于操作系统相关的操作,如设置环境变量。 -
initialize_model
:从model_utils.model
中导入,用于初始化模型。 -
train_val
:从model_utils.train
中导入,用于训练和验证模型。 -
getDataLoader
:从model_utils.data
中导入,用于加载数据。
2. 设置随机种子
def seed_everything(seed):
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
random.seed(seed)
np.random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
seed_everything(0)
-
作用:确保实验结果的可重复性。
-
torch.manual_seed
:设置 PyTorch 的随机种子。 -
torch.cuda.manual_seed
和torch.cuda.manual_seed_all
:设置 CUDA 的随机种子。 -
torch.backends.cudnn.benchmark
和torch.backends.cudnn.deterministic
:关闭 cuDNN 的基准测试模式,启用确定性模式。 -
random.seed
和np.random.seed
:设置 Python 和 NumPy 的随机种子。 -
os.environ['PYTHONHASHSEED']
:设置 Python 的哈希种子。
3. 配置训练参数
model_name = 'resnet18'
num_class = 11
batchSize = 32
learning_rate = 1e-4
loss = nn.CrossEntropyLoss()
epoch = 10
device = 'cuda' if torch.cuda.is_available() else 'cpu'
filepath = 'food-11_sample'
-
model_name
:使用的模型名称,这里选择resnet18
。 -
num_class
:分类任务的类别数,这里为 11。 -
batchSize
:每个批次的样本数,这里为 32。 -
learning_rate
:学习率,这里为 10−4。 -
loss
:损失函数,这里使用交叉熵损失。 -
epoch
:训练轮数,这里为 10。 -
device
:运行设备,优先使用 GPU(cuda
),否则使用 CPU。 -
filepath
:数据集的路径。
4. 数据加载
train_loader = getDataLoader(filepath, 'train', batchSize)
val_loader = getDataLoader(filepath, 'val', batchSize)
no_label_Loader = getDataLoader(filepath, 'train_unl', batchSize)
-
getDataLoader
:从model_utils.data
中导入的函数,用于加载数据。 -
train_loader
:训练集的数据加载器。 -
val_loader
:验证集的数据加载器。 -
no_label_Loader
:未标记数据集的数据加载器,用于半监督学习。
5. 模型初始化
model, input_size = initialize_model(model_name, 11, use_pretrained=False)
-
initialize_model
:从model_utils.model
中导入的函数,用于初始化模型。 -
model_name
:模型名称,这里为resnet18
。 -
num_class
:分类任务的类别数,这里为 11。 -
use_pretrained
:是否使用预训练权重,这里设置为False
,表示不使用预训练权重。 -
input_size
:模型输入图像的大小。
6. 优化器配置
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=1e-4)
-
torch.optim.AdamW
:使用 AdamW 优化器,它结合了 Adam 优化器和权重衰减。 -
lr
:学习率,这里为 10−4。 -
weight_decay
:权重衰减系数,这里为 10−4。
7. 配置训练参数字
trainpara = {
"model": model,
'train_loader': train_loader,
'val_loader': val_loader,
'no_label_Loader': no_label_Loader,
'optimizer': optimizer,
'batchSize': batchSize,
'loss': loss,
'epoch': epoch,
'device': device,
'save_path': save_path,
'save_acc': True,
'max_acc': 0.5,
'val_epoch': 1,
'acc_thres': 0.7,
'conf_thres': 0.99,
'do_semi': True,
"pre_path": None
}
-
trainpara
:一个字典,包含训练所需的参数。 -
model
:训练的模型。 -
train_loader
、val_loader
、no_label_Loader
:训练集、验证集和未标记数据集的数据加载器。 -
optimizer
:优化器。 -
batchSize
:批次大小。 -
loss
:损失函数。 -
epoch
:训练轮数。 -
device
:运行设备。 -
save_path
:保存模型的路径。 -
save_acc
:是否保存最佳模型。 -
max_acc
:初始最高准确率。 -
val_epoch
:验证间隔。 -
acc_thres
:启用半监督学习的准确率阈值。 -
conf_thres
:生成伪标签的置信度阈值。 -
do_semi
:是否启用半监督学习。 -
pre_path
:预训练模型的路径(如果有)。
总结
这段代码实现了一个完整的深度学习训练流程,支持半监督学习。它包括以下关键步骤:
-
设置随机种子:确保实验结果的可重复性。
-
配置训练参数:包括模型名称、类别数、批次大小、学习率、损失函数等。
-
数据加载:加载训练集、验证集和未标记数据集。
-
模型初始化:初始化模型,可以选择是否使用预训练权重。
-
优化器配置:配置优化器和学习率。
-
训练参数字典:将所有训练参数封装到一个字典中。
-
启动训练和验证:调用
train_val
函数,启动训练和验证流程。