PyTorch基础(七)迁移学习

迁移学习的含义就是利用别人训练几周或者几个月的模型参数作为自己的模型参数,通过使用其他人预训练的权重,这样很可能就会得到很好的性能。还有一种情况,将他人训练的模型的前面的层数都冻住,类似于一个不变的函数,只需要改变后面的一下网络结构,这样我们自己的模型就需要训练改变后的一些模型权重。

实现迁移学习要满足以下几点:保持输入数据的格式大小一致,使用经典网络的权重,尽量减少识别图片的差异,比如别人网络的权重是识别狗的,你把他的权重拿过来识别自行车,这样的做法就有可能训练效果不是很好。

这篇博客就讲述如何实现迁移学习(我认为这一部分还是挺难的,记录自己的学习情况,便于以后的复习)主要分为以下内容:

一、数据及介绍

二、数据增强

三、构造数据集

四、模型初始化

五、训练模型

六、全部代码

一、数据集介绍

这个数据集包含102个花类别,选择花的花通常在英国发生。每个类包含40到258个图像。可以在此类别统计页面上找到每个类别的类别和图像数量的详细信息。(需要数据集的可以在下方评论,我发给你)

数据集展示

二、数据增强

torchvision.transforms含有数据增强的方法,使用方式如下:

# 数据增强
    data_transforms = {
        'train': transforms.Compose([
            transforms.RandomRotation(45),  # 随机旋转 -45--45之间
            transforms.CenterCrop(224),  # 从中间随机裁剪 变成224*224
            transforms.RandomHorizontalFlip(p=0.5),  # 随机水平翻转
            transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),  # 亮度,对比度,饱和度,色相
            transforms.RandomGrayscale(p=0.025),  # 随机转成灰度
            transforms.ToTensor(),
            transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
        ]),
        'valid': transforms.Compose([
            transforms.Resize(256),  # 改变图片的尺寸
            transforms.CenterCrop(224),  # 进行中心裁剪
            transforms.ToTensor(),
            transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
        ])
    }
    # batch大小
    batch_size = 8
    # 图片所在文件夹
    data_dir = '../dataset/flower_data'
    # 根据自己的原始数据来构建pytorch框架所需要的数据类型
    # datasets.ImageFolder需要两个参数 第一个时数据集文件夹所在目录,第二个参数时数据增强操作
    train_dataset = datasets.ImageFolder(os.path.join(data_dir, 'train'), data_transforms['train'])
    valid_dataset = datasets.ImageFolder(os.path.join(data_dir, 'valid'), data_transforms['valid'])
    # 打印训练集和测试集的大小
    train_dataset_len = len(train_dataset)
    valid_dataset_len = len((valid_dataset))
    print("训练集大小:" + str(train_dataset_len))
    print('测试集大小' + str(valid_dataset_len))

pytorch中数据增强的含义是:dataloader在每次生成epoch时才对数据集进行以上数据增强操作。由于数据增强有些操作是具有随机性的(例如上面的随机裁剪和随机翻转),导致每次epoch产生的数据都不相同,例如同一张图片在有的epoch翻转了,在有的epoch没有翻转,或者同一张图片在各个epoch裁剪的位置不一样,所以每次用来训练的数据不相同,到达了数据增强的目的。当然,有些数据增强操作不具有随机性,如CenterCrop,每次都是对图片中间位置进行裁剪,不管在哪个epoch,裁剪出来的图片都一样。

实现多种数据增强叠加的方式就是利用transforms.Compose()方法,在里面加入数据增强的方法就行。

在这一部分代码中,使用了ImageFolder这个方法,利用这个方法,可以将原始的数据集变成符合Pytorch所需要的数据集类型,数据集文件夹的每个类别的存放需要像上面展示的那样,分成训练集和测试集,并且每一个分类中图片的对应方式也要一样。

ImageFolder需要两个参数,第一个参数是数据集所在文件夹的路径地址,第二个参数是数据增强的方式。

这样就返回datasets类型的数据集,根据datasets类型的数据集,就可以进行构建DataLoader数据集对象。

三、构造数据集

这一部分内容是常见操作,在我上面几篇博客中都有写到。

    # 构建DataLoader
    train_loader = DataLoader(
        dataset=train_dataset,
        batch_size=batch_size,
        shuffle=True
    )
    valid_loader = DataLoader(
        dataset=valid_dataset,
        batch_size=batch_size,
        shuffle=True
    )
    # 打印类别名称
    class_names = train_dataset.classes
    print(class_names)

但是如何对于数据增强之后的图片转变成原图片:

# 展示数据集
    def in_conver(tensor):
        image = tensor.to('cpu').clone().detach()
        image = image.numpy().squeeze()
        image = image.transpose(1, 2, 0)
        image = image * np.array((0.229, 0.224, 0.225)) + np.array((0.485, 0.456, 0.406))
        image = image.clip(0, 1)
        return image
    fig = plt.figure(figsize=(20, 12))
    columns = 4
    rows = 2
    for i, (datas, labels) in enumerate(train_loader, 0):
        # 只拿出一批数据进行展示
        for idx in range(rows * columns):
            # 定位
            ax = fig.add_subplot(rows, columns, idx + 1)
            # 设置标题
            ax.set_title(class_names[idx])
            # 展示图片
            plt.imshow(in_conver(datas[idx]))
        # 展示全部图片
        plt.show()
        break

 如何判断可以使用GPU进行计算

# 是否利用GPU训练
    train_on_gpu = torch.cuda.is_available()
    if not train_on_gpu:
        print("CUDA is not available. Training on cpu...")
    else:
        print("CUDA is available.Training on gpu")
    # 如果存在GPU 则选用GPU 否则选用CPU
    device = torch.device("cuda 0" if torch.cuda.is_available() else 'cpu')

这个实验实在我的笔记本电脑上运行的,并且没有使用GPU进行运行,写这段代码的目的在于以后再其他硬件上面运行时,代码能够随时拿过来使用。

四、模型初始化

这一部分是比较难的,也是我之前没有触碰过的知识点,可能过两天之后就不知道如何使用这个功能了。

def initialize_model(model_name,num_class,feature_extract,use_pretrained = True):
    '''
    :param model_name: 采用的模型名称
    :param num_class: 目的要分成的类别
    :param feature_extract:是否冻住参数
    :param use_pretrained: 是否下载别人训练好的模型 c\用户\torch\cachez中
    :return:
    model:新构建的模型

    '''
    model_ft = None
    input_size = 0

    if model_name == 'resnet':
        #加载模型 pretrained 要不要把网络模型下载下来
        model_ft = models.resnet18(pretrained=use_pretrained)

        #迁移学习
        #  model_ft:选用的模型
        #feature_extract:True False 选择是否冻结参数 若是True 则冻住参数 反之不冻住参数
        if feature_extract:
            for param in model_ft.parameters():
                param.requires_grad = False

        #得到最后一层的数量512 把最后的全连接层改成2048——>102
        num_ftrs = model_ft.fc.in_features
        #修改最后一层的模型
        model_ft.fc = torch.nn.Sequential(
            torch.nn.Linear(num_ftrs,num_class),
            torch.nn.LogSoftmax(dim=1)
        )
        input_size = 224
    else:
        print('采用了其他模型,还没来得及编写模型代码。。。')

    return model_ft,input_size

这段代码是用来初始化模型,返回一个别人已经训练好的模型,并且自己还可以修改别人的模型。在这个实验中,因为自己用的CPU进行运行的,所以我采用了一个比较小的网络模型,这样速度会上来,但是准确率可能没有那么高。

 #feature_extract:True False 选择是否冻结参数 若是True 则冻住参数 反之不冻住参数
        if feature_extract:
            for param in model_ft.parameters():
                param.requires_grad = False

 这段是是否冻结别人模型的参数,如果选择是True,则对于模型的权重参数,全部设置梯度不变,这样再进行梯度下降的时候,这些参数的值就不会变。

#得到最后一层的数量512 把最后的全连接层改成2048——>102
        num_ftrs = model_ft.fc.in_features
        #修改最后一层的模型
        model_ft.fc = torch.nn.Sequential(
            torch.nn.Linear(num_ftrs,num_class),
            torch.nn.LogSoftmax(dim=1)
        )
        input_size = 224

 这一部分是修改模型,因为别人模型的输出参数和这个实验的输出类别有所不同,所以需要修改成102类,修改了模型的全连接层,加入了一个线性层和一个softmax分类层。

最后这个初始化函数返回一个修改之后的模型。

#设置哪些层需要进行训练
    model_ft, input_size = initialize_model("resnet", 102, feature_extract=True, use_pretrained=True)

    # 是否训练所有层
    # 打印需要学习的参数
    print('Parameter to learn:')
    param_to_update = []
    for name, param in model_ft.named_parameters():
        if param.requires_grad == True:
            param_to_update.append(param)
            print("\t", name)

    # 打印修改之后的网络模型
    print(model_ft)

 初始化模型之后,最好的方式查看这个模型需要学习哪些参数,确定再模型初始化中,有没有正确修改模型,并且这是一个关键代码段,要收集现在的模型要学习的参数,因为要把需要学习的参数传入之后的优化器,便于后面的梯度下降。

五、训练模型

def train_model(model,train_loader,valid_loader,optimizer,criterion,nums_epoch,filename):
    '''
    
    :param model: 训练模型
    :param train_loader: 训练集
    :param valid_loader: 测试集
    :param optimizer: 优化器
    :param criterion: 损失器
    :param nums_epoch: 训练轮次
    :param filename: 模型保存路径
    :return: 
    '''

    #保存最好的准确率
    best_acc = 0
    #如果GPU可用 在GPU中运行 
    #model.to(device)

    #保存模型的参数
    best__model_wts = copy.deepcopy(model.state_dict())

    for epoch in range(nums_epoch):
        #训练
        running_loss = 0
        for batch_idx,(input,label) in enumerate(train_loader,0):
            #如果GPU可用 则放入道GPU中进行运算
            # input = input.to(device)
            # label = input.to(device)
            optimizer.zero_grad()
            outputs = model(input)
            _,pres = torch.max(outputs,dim=1)
            loss = criterion(outputs,label)
            loss.backward()
            optimizer.step()
            running_loss += loss.item()
            if batch_idx % 8 == 7:
                print('[%d,%5d] loss:%.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))
                running_loss = 0

        # 每经过一轮数据训练  测试准确度
        correct = 0
        total = 0
        with torch.no_grad():
            for data in valid_loader:
                image, label = data
                output = model(image)
                # _代表最大的元素 pre代表索引  索引本质就代表了预测元素值
                _, pre = torch.max(output.data, dim=1)
                total += label.size(0)
                correct += (pre == label).sum().item()

        epoch_acc = 100 * correct / total
        print('准确率为:%d %%' % (epoch_acc))
        
        #记录准确度最好的模型权重 并保存再文件中
        if epoch_acc>best_acc:
            best_acc = epoch_acc
            best__model_wts = copy.deepcopy(model.state_dict())
            state = {
                'state_dict':model.state_dict(),
                'best_acc':best_acc,
                'optimizer':optimizer.state_dict()
            }
            torch.save(state,filename)
        
        #返回一个准确度最高的模型 
        model.load_state_dict(best__model_wts)

    return model

这一部分是模型训练和模型测试的代码,前一部分和之前博客中写的一样,但是再代码的最后,增加了一个保存模型最好权重参数的代码,经过整个训练之后,下次可以直接在初始化模型之后,让模型加载这次训练的权重参数,然后传入一个参数,这样就又利于模型的保存和重复使用,不必每次使用都需要再一次的训练,节省了时间。

六、全部代码

#torchvision.models中含有一些经典的模型和权重参数
#torchvision.datasets中含有数据集
#torchvision.transforms含有数据增强的方法

import os
import copy
import numpy as np
from torchvision import datasets
from torchvision import transforms
from torch.utils.data import DataLoader
from torchvision import models
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号



def initialize_model(model_name,num_class,feature_extract,use_pretrained = True):
    '''
    :param model_name: 采用的模型名称
    :param num_class: 目的要分成的类别
    :param feature_extract:是否冻住参数
    :param use_pretrained: 是否下载别人训练好的模型 c\用户\torch\cachez中
    :return:
    model:新构建的模型

    '''
    model_ft = None
    input_size = 0

    if model_name == 'resnet':
        #加载模型 pretrained 要不要把网络模型下载下来
        model_ft = models.resnet18(pretrained=use_pretrained)

        #迁移学习
        #  model_ft:选用的模型
        #feature_extract:True False 选择是否冻结参数 若是True 则冻住参数 反之不冻住参数
        if feature_extract:
            for param in model_ft.parameters():
                param.requires_grad = False

        #得到最后一层的数量512 把最后的全连接层改成2048——>102
        num_ftrs = model_ft.fc.in_features
        #修改最后一层的模型
        model_ft.fc = torch.nn.Sequential(
            torch.nn.Linear(num_ftrs,num_class),
            torch.nn.LogSoftmax(dim=1)
        )
        input_size = 224
    else:
        print('采用了其他模型,还没来得及编写模型代码。。。')

    return model_ft,input_size


def train_model(model,train_loader,valid_loader,optimizer,criterion,nums_epoch,filename):
    '''
    
    :param model: 训练模型
    :param train_loader: 训练集
    :param valid_loader: 测试集
    :param optimizer: 优化器
    :param criterion: 损失器
    :param nums_epoch: 训练轮次
    :param filename: 模型保存路径
    :return:
    '''

    #保存最好的准确率
    best_acc = 0
    #如果GPU可用 在GPU中运行
    #model.to(device)

    #保存模型的参数
    best__model_wts = copy.deepcopy(model.state_dict())

    for epoch in range(nums_epoch):
        #训练
        running_loss = 0
        for batch_idx,(input,label) in enumerate(train_loader,0):
            #如果GPU可用 则放入道GPU中进行运算
            # input = input.to(device)
            # label = input.to(device)
            optimizer.zero_grad()
            outputs = model(input)
            _,pres = torch.max(outputs,dim=1)
            loss = criterion(outputs,label)
            loss.backward()
            optimizer.step()
            running_loss += loss.item()
            if batch_idx % 8 == 7:
                print('[%d,%5d] loss:%.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))
                running_loss = 0

        # 每经过一轮数据训练  测试准确度
        correct = 0
        total = 0
        with torch.no_grad():
            for data in valid_loader:
                image, label = data
                output = model(image)
                # _代表最大的元素 pre代表索引  索引本质就代表了预测元素值
                _, pre = torch.max(output.data, dim=1)
                total += label.size(0)
                correct += (pre == label).sum().item()

        epoch_acc = 100 * correct / total
        print('准确率为:%d %%' % (epoch_acc))

        #记录准确度最好的模型权重 并保存再文件中
        if epoch_acc>best_acc:
            best_acc = epoch_acc
            best__model_wts = copy.deepcopy(model.state_dict())
            state = {
                'state_dict':model.state_dict(),
                'best_acc':best_acc,
                'optimizer':optimizer.state_dict()
            }
            torch.save(state,filename)

        #返回一个准确度最高的模型
        model.load_state_dict(best__model_wts)

    return model



if __name__=='__main__':

    # 数据增强
    data_transforms = {
        'train': transforms.Compose([
            transforms.RandomRotation(45),  # 随机旋转 -45--45之间
            transforms.CenterCrop(224),  # 从中间随机裁剪 变成224*224
            transforms.RandomHorizontalFlip(p=0.5),  # 随机水平翻转
            transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),  # 亮度,对比度,饱和度,色相
            transforms.RandomGrayscale(p=0.025),  # 随机转成灰度
            transforms.ToTensor(),
            transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
        ]),
        'valid': transforms.Compose([
            transforms.Resize(256),  # 改变图片的尺寸
            transforms.CenterCrop(224),  # 进行中心裁剪
            transforms.ToTensor(),
            transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
        ])
    }
    # batch大小
    batch_size = 8
    # 图片所在文件夹
    data_dir = '../dataset/flower_data'
    # 根据自己的原始数据来构建pytorch框架所需要的数据类型
    # datasets.ImageFolder需要两个参数 第一个时数据集文件夹所在目录,第二个参数时数据增强操作
    train_dataset = datasets.ImageFolder(os.path.join(data_dir, 'train'), data_transforms['train'])
    valid_dataset = datasets.ImageFolder(os.path.join(data_dir, 'valid'), data_transforms['valid'])
    # 打印训练集和测试集的大小
    train_dataset_len = len(train_dataset)
    valid_dataset_len = len((valid_dataset))
    print("训练集大小:" + str(train_dataset_len))
    print('测试集大小' + str(valid_dataset_len))
    # 构建DataLoader
    train_loader = DataLoader(
        dataset=train_dataset,
        batch_size=batch_size,
        shuffle=True
    )
    valid_loader = DataLoader(
        dataset=valid_dataset,
        batch_size=batch_size,
        shuffle=True
    )
    # 打印类别名称
    class_names = train_dataset.classes
    print(class_names)
    # 展示数据集
    def in_conver(tensor):
        image = tensor.to('cpu').clone().detach()
        image = image.numpy().squeeze()
        image = image.transpose(1, 2, 0)
        image = image * np.array((0.229, 0.224, 0.225)) + np.array((0.485, 0.456, 0.406))
        image = image.clip(0, 1)
        return image
    fig = plt.figure(figsize=(20, 12))
    columns = 4
    rows = 2
    for i, (datas, labels) in enumerate(train_loader, 0):
        # 只拿出一批数据进行展示
        for idx in range(rows * columns):
            # 定位
            ax = fig.add_subplot(rows, columns, idx + 1)
            # 设置标题
            ax.set_title(class_names[idx])
            # 展示图片
            plt.imshow(in_conver(datas[idx]))
        # 展示全部图片
        plt.show()
        break

    # 是否利用GPU训练
    train_on_gpu = torch.cuda.is_available()
    if not train_on_gpu:
        print("CUDA is not available. Training on cpu...")
    else:
        print("CUDA is available.Training on gpu")
    # 如果存在GPU 则选用GPU 否则选用CPU
    device = torch.device("cuda 0" if torch.cuda.is_available() else 'cpu')

    #设置哪些层需要进行训练
    model_ft, input_size = initialize_model("resnet", 102, feature_extract=True, use_pretrained=True)

    # 是否训练所有层
    # 打印需要学习的参数
    print('Parameter to learn:')
    param_to_update = []
    for name, param in model_ft.named_parameters():
        if param.requires_grad == True:
            param_to_update.append(param)
            print("\t", name)

    # 打印修改之后的网络模型
    print(model_ft)

    # 模型保存
    filename = 'checkpoint.pth'
    # 构建优化器
    optimizer_ft = torch.optim.Adam(param_to_update, lr=0.01)
    # #对于学习率 每7个epoch衰减成10分之一
    # schedule = torch.optim.lr_scheduler.StepLR(optimizer_ft,step_size=7,gamma=0.1)

    # 构建损失
    # 为什么使用这个损失函数而不用torch.nn.CrossEntropyLoss
    # 因为torch.nn.CrossEntropyLoss相当于LogSoftmax()和torch.nn.NLLLoss()的集合
    criterion = torch.nn.NLLLoss()

    model = train_model(model_ft,train_loader,valid_loader,optimizer_ft,criterion,10,filename)





可以看出,训练2个epoch之后,准确度是45%,我用的是我的cpu跑的代码,执行速度非常慢,所以,我就让这个模型跑了一会就不跑了, 这就是这一两天学的的内容了。

  • 2
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值