迁移学习

        迁移学习是一种机器学习方法,它利用在一个任务上训练好的模型的知识,来帮助解决另一个相关但不同的任务。

        在深度学习中,迁移学习通常表现为使用在大规模数据集(如ImageNet)上预训练的模型,将其作为起点,通过微调(fine-tuning)或特征提取(feature extraction)的方式,应用于新的任务。

迁移学习的本质是站在巨人的肩膀上

  1. 利用通用知识:预训练模型学习了大量通用特征

  2. 针对特定任务:在通用基础上进行针对性调整

  3. 节省资源:大幅减少数据和计算需

迁移学习的两种主要方式:

  1. 特征提取(Feature Extraction):冻结预训练模型的卷积层(作为特征提取器),只训练新添加的全连接层。这样,我们利用预训练模型学到的特征表示,只学习一个新的分类器。

  2. 微调(Fine-tuning):解冻预训练模型的一部分或全部卷积层,同时训练新添加的全连接层和卷积层。通常使用较小的学习率,以避免破坏预训练模型已经学到的有用特征。

所谓冻结就是阻止参数参与梯度计算。

特征提取(Feature Extraction)
# 冻结预训练模型的所有层,只训练新添加的分类层
for param in model.parameters():
    param.requires_grad = False  # 冻结参数,不更新

# 只训练新添加的全连接层
model.fc = nn.Linear(in_features, 11)
# 优化器只更新最后一层
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)
微调(Fine-Tuning)
# 解冻部分或全部层进行训练
for param in model.layer4.parameters():  # 只解冻最后几层
    param.requires_grad = True

# 整个模型一起训练,但使用较小的学习率
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)
使用预训练的 resnet18 模型
model = resnet18(weights=torchvision.models.ResNet18_Weights.DEFAULT)
in_features = model.fc.in_features
model.fc = nn.Linear(in_features, 11)

修改最后的全连接层,将输出的类别改为本项目中所需要的 11 类

机器学习:监督学习、无监督学习、半监督学习、强化学习-CSDN博客

监督学习(Supervised Learning)

定义

让模型从带有标签的训练数据中学习,学习输入到输出的映射关系。

可以把监督学习理解为我们教机器如何做事情

给定:{(x₁, y₁), (x₂, y₂), ..., (xₙ, yₙ)}
目标:学习 f: X → Y,使得 f(x) ≈ y

  • 典型任务:分类(如垃圾邮件检测)、回归(如房价预测)。
  • 经典算法逻辑回归支持向量机(SVM)、神经网络
  • 应用场景:图像分类、医疗诊断、销售预测。

无监督学习(Unsupervised Learning)

定义

无标签数据中发现隐藏模式数据结构

给定:{x₁, x₂, ..., xₙ}
目标:发现数据的固有结构、模式或分布

在无监督学习中数据只有特征(feature)无标签(label),是一种机器学习的训练方式,它本质上是一个统计手段,在没有标签的数据里可以发现潜在的一些结构的一种训练方式。

简单理解:比起监督学习,无监督学习更像是自学,让机器学会自己做事情。

  • 典型任务:聚类(如K-Means)、降维(如PCA)。
  • 经典算法:K-Means、主成分分析(PCA)、层次聚类。
  • 应用场景:客户细分、异常检测、数据压缩。

半监督学习(Semi-supervised Learning)

定义

结合少量有标签数据大量无标签数据进行学习。

数据:少量 (xᵢ, yᵢ) + 大量 xⱼ
假设:相似样本应有相似标签(平滑假设)

目标

利用同时包含有标签和无标签的数据来构建一个模型,使得模型能够在测试阶段更好地泛化到新的、未见过的数据。

在半监督学习中,无标签的数据可以起到两个重要作用:

        1、利用未标记数据的信息:未标记数据可能包含对数据分布、结构和隐含特征的有用信息,这些信息可以帮助模型更好地进行泛化。

        2、利用标记数据的传播效应:通过利用标记数据与无标签数据之间的数据分布相似性,可以通过传播标签信息到无标签样本,进而增强模型的性能。

  • 典型任务:图像分类、文本分类。
  • 经典方法自训练(Self-training)、一致正则化(Consistency Regularization)。
  • 应用场景:医学影像分析、自然语言处理、网络安全。

在本项目中,除开之前用的带标签数据,还有非常多的无标签数据,因而要用到半监督学习的方法。

这里只取其一:自训练(self-training)

自训练(Self-training)

通俗解释

自训练就像是一个“自我学习”的过程。我们先用有标签的数据训练一个初始模型,然后用这个模型去预测无标签的数据,选出那些预测置信度高的样本,把它们连同预测的标签一起加入到训练集中,再重新训练模型。这个过程可以重复多次。

步骤

  1. 用有标签数据训练一个模型。

  2. 用这个模型预测无标签数据,得到伪标签。

  3. 选择置信度高的预测(比如概率高于某个阈值),将这些样本和伪标签加入训练集。

  4. 用扩大的训练集重新训练模型。

  5. 重复2-4步,直到满足停止条件。

例子

假设我们有一个猫狗分类器,初始只有100张有标签的图片(50张猫,50张狗)。我们还有10000张无标签的图片。

  • 第一步:用100张有标签图片训练一个分类器。

  • 第二步:用这个分类器对10000张无标签图片进行预测,得到每张图片是猫或狗的概率。

  • 第三步:选择概率高于0.95的图片(比如有2000张),将它们和预测的标签(伪标签)加入到训练集中,现在训练集变成了2100张(100+2000)。

  • 第四步:用这2100张图片重新训练模型。

  • 重复第二步和第三步,直到模型不再变化或达到迭代次数。

注意

自训练可能会累积错误,因为一开始模型不完美,可能会把一些样本错误地标记,然后这些错误会被放大。所以选择高置信度的样本很重要。

自训练代码实现

在前一篇的代码上做了增加和修改:

数据集类部分

    def __getitem__(self, item):
        if self.mode == "semi":  # 半监督模式下,读取到的无标签数据也不应做复杂的变换,因为需要对它进行预测
            return self.transform(self.X[item]), self.X[item]  # 返回原始数据以构造新的数据集(半监督)
        else:                    # 对于训练数据,才会给其加上很多的变换,以实现数据增强,帮助训练
            return self.transform(self.X[item]), self.Y[item]

    def __len__(self):
        if self.mode == "semi":
            return self.X.shape[0]
        else:
            return len(self.Y)

稍微修改了get_item等,关键在于get_item。

即使无标签数据没有label可以返回,但是这里仍然要返回两个值,前面的X是经过变换(transform)的,这一部分用于送入模型中进行预测。

(这里的变化和对验证集 X 做的一样,没有经过旋转、切分、缩放等,只是调整了尺寸,并调用了ToTensor转化为pytorch要用的张量)

后面的 X 则是为了提供原始的图片,因为半监督最终的目的是 要给无标签数据打上标签并用于训练,用于训练的数据自然只能是原始的图片。因而这个地方还要另外返回一个原始的 X。

这个地方对数据集类(food_dataset)的修改是为了读取与处理无标签数据,后续会实例化为:

no_label_set = food_Dataset(no_label_path, "semi")
no_label_loader = DataLoader(no_label_set, batch_size=16, shuffle=False)  # 半监督不随机取,因为要打标签

并且利用其实例化一个dataloader对象,用于加载无标签数据,后续用于送入模型预测其标签。

数据读取部分

    def read_file(self, path):  # 读取图片,带标签训练图片共11个文件夹,每个含280张图
        # 区分 半监督 和 监督 的读数据方式
        if self.mode != "semi":
           """"读取带标签的数据的操作(包括训练集和验证集)"""
            return X, Y

        else:  # 读取不带标签的数据,用于半监督学习
            file_dir = (path + "/%02d" % 00)  # 取路径
            file_list = os.listdir(file_dir)  # 列出该文件夹下所有文件(图片),返回一个列表

            X = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)  # np数组初始化

            for j, img_name in enumerate(file_list):  # 遍历该文件夹,获得索引j和文件名(图片名)
                img_path = os.path.join(file_dir, img_name)  # 把图片名添加到路径形成图片的路径
                img = Image.open(img_path)  # 根据图片路径打开图片
                img = img.resize((HW, HW))  # 尺寸变更
                X[j, ...] = img  # 第j张完整的图像,形状为 (224, 224, 3),省略号表示对后三维“取全部”

            print("读到了%d个数据" % X.shape[0])
            return X  # 无标签数据,只返回图片X,不返回标签Y

这里还把read_file函数放进数据集类里面了,这样可以直接在内部传递参数

半监督学习数据集类

由于半监督学习数据集的数据获取方式(需要在训练的过程中对无标签数据进行预测产出标签)和正常直接读取带标签数据不同,因而相应的数据集类也要另外定义。

class semi_dataset(Dataset):
    def __init__(self, no_label_loader, model, device, thres=0.99):
        x, y = self.get_label(no_label_loader, model, device, thres)
        if x == []:  # 若根本取不到能用的无标签数据,则标志位设为False,说明不可用
            self.flag = False
        else:
            self.flag = True
            self.X = np.array(x)  # 转换为数组
            self.Y = torch.LongTensor(y)  # 转换为长整型
            self.transform = train_transform  # 数据增强transform设为 训练用的transform

    def __getitem__(self, item):  # 返回数据
        return self.transform(self.X[item]), self.Y[item]

    def __len__(self):
        return len(self.Y)

    def get_label(self, no_label_loader, model, device, thres=0.99):
        model = model.to(device)

        X = []  # 存放高置信度的原始图片
        Y = []  # 存放对应的预测标签

        soft = nn.Softmax()  # 处理模型的预测值,将其转化为概率分布
        pred_prob = []  # 存放最大概率值
        labels = []  # 存放最大值下标(标签)
        with torch.no_grad():  # 不做训练时用到模型——不计算梯度
            for bat_x, _ in no_label_loader:
                bat_x = bat_x.to(device)
                pred = model(bat_x)  # 获取预测值
                pred_soft = soft(pred)  # 输出转化为概率分布
                pred_max, pred_labels = pred_soft.max(1)  # .max()返回:(最大值, 最大值下标),最大值下标就是预测的标签
                pred_prob.extend(pred_max.cpu().numpy().tolist())
                labels.extend(pred_labels.cpu().numpy().tolist())

        # no_label_loader是顺序取数的,因而上面的循环会顺序取完dataset中所有的X,
        # 因而对应的pred、pred_prob中元素的下标都和X一一对应,所以这里可以用index来找到X
        for index, prob in enumerate(pred_prob):
            if prob > thres:
                X.append(no_label_loader.dataset[index][1])
                # dataset返回的是一个元组:(X的转换值,原本的X),
                # index指示x的位置,[1]指示要取的是X的真实图片而非变换后的
                Y.append(labels[index])

        return X, Y

no_label_set = food_Dataset(no_label_path, "semi")
no_label_loader = DataLoader(no_label_set, batch_size=16, shuffle=False)  # 半监督不随机取,因为要打标签

大多的功能都写在注释里面了,这里挑几句详细写一下

重点:get_label()
for bat_x, _ in no_label_loader:

在前面的数据集类中,在semi模式下,数据集的get_item会返回两个值,一是经过简单变换的图片,二是原始的图片,这里我们要先取简单变换的图片,并使其通过模型,计算其标签,故而第二个位置上的下划线"_"只是一个占位符,使其符合语法,前面的bat_x才是操作的对象

这句后续就是通过bat_x计算标签

.max()方法

.max()是一个张量(tensor)的方法,用于返回张量中的最大值。但是,它有不同的调用方式,根据参数的不同,返回的内容也不同。

不带参数的.max():只返回整个张量中的最大值,不返回下标

tensor = torch.tensor([1.0, 2.0, 3.0, 4.0])
max_value = tensor.max()
print(max_value)  # 输出:tensor(4.)

带维度的.max(dim):

tensor = torch.tensor([[1.0, 2.0, 3.0],
                       [4.0, 5.0, 6.0]])
values, indices = tensor.max(dim=1)
print(values)   # 输出:tensor([3., 6.])   # 每一行的最大值
print(indices)  # 输出:tensor([2, 2])     # 每一行最大值的索引(列索引)

这里,我们指定了维度dim=1(按行),返回两个张量

  • 第一个是每一行的最大值。【1,2,3,4,....】

  • 第二个是每一行最大值所在的索引在该行中的位置)。【5,6,7,8,....】

代码中用的就是第二种。前面的最大值作为置信度,后面的下标就是对应的标签,置信度用于评判这个标签能否就作为这张图片的伪标签并用于训练。

pred_prob.extend(pred_max.cpu().numpy().tolist())
labels.extend(pred_labels.cpu().numpy().tolist())

为什么要用extend呢?

因为.max返回的是两个张量,换个角度讲就是返回的都是数组(列表),不能用append,只能将其从张量转为列表并将其并入pred_prob或labels列表以进行存放。

第二个for循环
        # no_label_loader是顺序取数的,因而上面的循环会顺序取完dataset中所有的X,
        # 因而对应的pred、pred_prob中元素的下标都和X一一对应,所以这里可以用index来找到X
        for index, prob in enumerate(pred_prob):
            if prob > thres:
                X.append(no_label_loader.dataset[index][1])
                # dataset返回的是一个元组:(X的转换值,原本的X),
                # index指示x的位置,[1]指示要取的是X的真实图片而非变换后的
                Y.append(labels[index])

pred_prob存放的是概率(置信度),for循环取其中元素,将其与阈值thres进行对比,判断是否可以把它【对应的图片】加入半监督数据集。

那么怎么找到这张图片呢,明明前面一通计算下来留给我们的只剩下概率和标签了。

这就是为什么在实例化dataloader对象时不将其打乱了(shuffle=False),因为不打乱时,dataloader就会按顺序取数据。

假设有18张图片,batchsize=16,dataloader一次取16张图,第一次取 0~15 号图片,第二次取16、17号图片,每轮都构成了前面每个循环中的那个bat_x,bat_x经过模型得到pred(预测标签),这二者必然是一一对应的。

以此类推,pred -> pred_soft -> pred_max(pred_labels) -> pred_prob(labels),全都是一一对应的

所以pred_prob中元素的下标,实际上就是对应的数据集中那张图片的下标,因而可以用这个index找到图片并将其加入 X

X.append(no_label_loader.dataset[index][1])

这句代码用到的方括号语法:

方括号的本质:
"""当你写 dataset[index] 时,Python 实际上是在调用:"""
dataset.__getitem__(index)

调用的流程:

# 在 food_Dataset 类中:
def __getitem__(self, item):
    if self.mode == "semi":
        return self.transform(self.X[item]), self.X[item]
    else:
        return self.transform(self.X[item]), self.Y[item]

# 当你调用 dataset[0] 时:
# 1. Python 解析器看到方括号
# 2. 查找 dataset 对象的 __getitem__ 方法
# 3. 调用 dataset.__getitem__(0)
# 4. 返回结果

而get_item方法,根据我们的写法,它会返回一个含有两个元素的元组
(  self.transform(self.X[item])  ,   self.X[item] 

那么,no_label_loader.dataset [ index ],就会返回一个元组

(  self.transform(self.X[ index ])  ,   self.X[ index ] 

no_label_loader.dataset [ index ] [ 1 ],返回的就是元组中的第二个元素:

      self.X[ index ] 

这就是我们需要的,原始的图像 X

总体数据流向
semi_dataset[0][1]
      ↓
调用 __getitem__(0)
      ↓
返回 (transform(X[0]), X[0])
      ↓
取第二个元素 [1]
      ↓
得到 X[0](原始图像)

综上,第二个for循环就实现了:

“把合格的无标签图片 X 打上预测得到的伪标签 label ,并将其加入半监督学习数据集”的工作

配套使用的获取dataloader函数

def get_semi_loader(no_label_loader, model, device, thres):
    semiset = semi_dataset(no_label_loader, model, device, thres)
    if semiset.flag == False:
        return None
    else:
        semi_loader = DataLoader(semiset, batch_size=16, shuffle=False)
        return semi_loader

判断半监督数据集是否可用(Flag),实例化semiset时如果没有任何置信度够高的无标签数据和伪标签,则Flag为False,半监督数据集不可用,返回None

反之,若可用,则返回一个dataloader对象。这个函数会在训练流程中调用,为训练提供无标签数据和伪标签用于训练。

训练流程

def train_val(model, train_loader, val_loader, lr, optimizer, device, epochs, save_path, no_label_loader, thres):
    model = model.to(device)  # 防止出错

    semiloader = None  # 半监督数据loader初始化为空


    """...其余相关变量初始化..."""


    for epoch in range(epochs):  # “发令枪”,模型训练的开始
        model.train()            # 模型调为训练模式

        """......训练......"""

        semi_loss = 0.0  # 浮点形式
        semi_acc = 0.0  # 准确率计算

        if semiloader != None:
            for x, y in semiloader:
                x, y = x.to(device), y.to(device)
                y_pred = model(x)
                semi_bat_loss = loss(y_pred, y)
                semi_bat_loss.backward()
                optimizer.step()
                optimizer.zero_grad()
                semi_loss += semi_bat_loss.cpu().item()
                semi_acc += np.sum(np.argmax(y_pred.detach().cpu().numpy(), axis=1) == y.cpu().numpy())
            print("半监督数据集的训练准确度为", semi_acc/train_loader.dataset.__len__())

        model.eval()  # 模型调为验证模式

        """......验证......"""

        if epoch % 5 == 0 & (plt_val_acc[-1] > 0.7):
            semiloader = get_semi_loader(no_label_loader, model, device, thres)


        if val_acc > max_val_acc:
            max_val_acc = val_acc
            torch.save(model, save_path)

        print("[%03d/%03d] %2.2f sec(s)  train_loss: %.6f | val_loss: %.6f | train_acc: %.6f | val_acc: %.6f" %
              (epoch+1, epochs, time.time()-start_time, plt_train_loss[-1], plt_val_loss[-1], plt_train_acc[-1],
               plt_val_acc[-1]))


    """......画图......"""

训练流程函数增加了两个参数:no_label_loader和thres,一个是无标签数据加载,一个是置信度阈值。

代码解释

半监督数据集获取
if epoch % 5 == 0 & (plt_val_acc[-1] > 0.7):
    semiloader = get_semi_loader(no_label_loader, model, device, thres)

semiloader初始为空,在多轮训练加验证后再尝试获取。

每五轮训练+验证后,检验一次模型在验证集上的 平均准确率 是否达到一定标准(标准人为给定)。准确度稍微高上去了再考虑获取半监督数据集。

若准确率不高时就尝试获取半监督数据集,那么当模型都没有得到有效训练时,无标签数据也就很难得到达标的伪标签,最终什么也得不到,白白浪费时间。

同时,即使模型已经具有一定准确率了,也没必要每轮都获取一次,仅仅一轮的训练不会给模型参数带来多么巨大的变化,同样的,通过模型预测产生的伪标签也不会有多么大的更新。生成伪标签需要对未标注数据进行前向传播,计算量大,且频繁生成会显著增加训练时间,于是必须间隔一段时间再尝试获取半监督数据集。

半监督训练流程
        semi_loss = 0.0  # 浮点形式
        semi_acc = 0.0  # 准确率计算

        if semiloader != None:
            for x, y in semiloader:
                x, y = x.to(device), y.to(device)
                y_pred = model(x)
                semi_bat_loss = loss(y_pred, y)
                semi_bat_loss.backward()
                optimizer.step()
                optimizer.zero_grad()
                semi_loss += semi_bat_loss.cpu().item()
                semi_acc += np.sum(np.argmax(y_pred.detach().cpu().numpy(), axis=1) == y.cpu().numpy())
            print("半监督数据集的训练准确度为", semi_acc/train_loader.dataset.__len__())

当已经获取到能用的半监督数据时才启用半监督训练。

其余步骤与正常训练流程类似。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐