这节偷了个懒本来不想写了,但是不写又一直挂念着,所以还是补上吧╮(╯-╰)╭

一、任务概述

本次实战是一个食物分类任务,我们要让许多模型学习食物图片数据,然后进行分类。

本次实战会采用模块化思想,通过data.py model.py train.py 和main.py四个python模块进行编写,其实思路基本就是第三节 回归实战的升级拓展,所以如果出现重复内容就不赘述了。

二、数据准备

打开文件目录,我们可以发现数据集的训练集、验证集和测试集已经提前被放到不同的文件夹里了,而不是乱作一大坨,这样就不需要我们手动划分了。
在这里插入图片描述
观察文件结构,发现标签可以从文件夹名称或者图片名中获取。在这里插入图片描述

对了,我们还会发现training文件夹下有一个unlabeled文件夹,是给半监督模型用的。
在这里插入图片描述

这样,数据我们就大致了解了,接下来开始编写代码。

三、data 模块

这个模块主要用于定义数据类实现loader加载器
全部代码👇

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])
])                   # 训练集需要做各种变换。   效果参见https://pytorch.org/vision/stable/transforms.html



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 _readfile(self,path, label=True):                   # 定义一个读文件的函数
        if label:                                             # 有无标签, 文件结构是不一样的。
            x, y = [], []
            for i in tqdm(range(11)):                           # 有11类
                label = '/%02d/'%i                                 # %02必须为两位。 符合文件夹名字
                imgDirpath = path+label
                imglist = os.listdir(imgDirpath)                    # listdir 可以列出文件夹下所有文件。
                xi = np.zeros((len(imglist), HW, HW, 3), dtype=np.uint8)
                yi = np.zeros((len(imglist)), dtype=np.uint8)           # 先把放数据的格子打好。 x的维度是 照片数量*H*W*3
                for j, each in enumerate(imglist):
                    imgpath = imgDirpath + each
                    img = Image.open(imgpath)                  # 用image函数读入照片, 并且resize。
                    img = img.resize((HW, HW))
                    xi[j,...] = img                           #在第j个位置放上数据和标签。
                    yi[j] = i
                if i == 0:
                    x = xi
                    y = yi
                else:
                    x = np.concatenate((x, xi), axis=0)             # 将11个文件夹的数据合在一起。
                    y = np.concatenate((y, yi), axis=0)
            print('读入有标签数据%d个 '%len(x))
            return x, y
        else:
            imgDirpath = path + '/00/'
            imgList = os.listdir(imgDirpath)
            x = np.zeros((len(imgList), HW, HW ,3),dtype=np.uint8)
            for i, each in enumerate(imgList):
                imgpath = imgDirpath + each
                img = Image.open(imgpath)
                img = img.resize((HW, HW))
                x[i,...] = img
            return x

    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)
    ##########################


3.1 foodDataset 类

class foodDataset(Dataset):                      #数据集三要素: init , getitem , len
    def __init__(self, path, mode):

    def __getitem__(self, index):                        # getitem 用于根据标签取数据

    def _readfile(self,path, label=True):                   # 定义一个读文件的函数

    def __len__(self):                      # len函数 负责返回长度。

3.1.1 foodDataset 概述

这是监督学习模型的数据类,它包含基本三要素 init ,getitem , len,以及一个读文件方法readfile

  • init 用于接收参数,它主要被设计用来读取你在不同情况下所需要的不同数据文件
  • getitem 实现容器协议,使对象支持索引操作 obj[key])
  • len 实现长度协议,使对象支持 len() 函数
  • readfile 方法用于协助init读文件,不是必须的方法

其实这个类封装完毕后,从“外界”调用视角来看,我们能做的就是:

  • 传参,这个是init 的功劳;
  • 按索引随机选取,比如下面的samplePlot画图函数里有这么一句:
ax.imshow(np.array(dataset[randomNum[index]].cpu().permute(1,2,0)))

当你看到dataset[xxxxxx]的时候就会发现,这不是列表嘛!是的,这就是魔术方法getitem

  • 获取长度,还是samplePlot画图函数里有这么一句:
datalen = len(dataset)

就是让dataset可以通过len函数获取长度
你不觉得定义了上面的魔术方法后,操作被非常优雅地简化了吗?这就是我们的目的之一

3.1.2 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,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

初始化属性
init 初始化了foodDataset的四个属性,分别是self.x self.y self.transform self.mode,self.mode直接等于参数mode即可,self.x self.y self.transform使用了分支结构来赋值

  • self.x和self.y就是数据和数据标签,我们使用_readfile函数读取x和y(后面会解释_readfile),'test’和’train_unl’没有标签y,而’train’和’val’有,因此产生分支
  • self.transform存储了数据增强处理的流程,在’train’和’train_unl’下,赋为train_transform,在’test’和’val’下,赋为test_transform。其实也很好理解,在训练时数据可以通过各种各样的变换来扩充模型,而验证和测试时就不需要,没意义。下面是增强管道的设置,在非训练时只转换为tensor型数据。
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])
])                   # 训练集需要做各种变换。   效果参见https://pytorch.org/vision/stable/transforms.html

讨论几个问题:

  • 可以看到self.y在初始化时就被设置为tensor型数据,可是self.x一直到被切片训练时才被转换为tensor,为什么不早转换?

  • 这样就是只在训练时把数据转换为tensor,作为临时变量用完即销毁,存储的一直是ndarry,所以节省了内存,tensor占用的内存是巨大的,x全部存为tensor可能会把内存爆掉

  • self.x是被强化后进行了训练,那原图有被训练到吗?

  • 并没有,在train.py中,x, target = data[0].to(device), dadta[1].to(device),我们取了data的第一个0和1维度的数据作为x和target,data是由train_loader提供的打包数据,而train_loader通过dataset的__getitem__魔术方法获取数据,在有标签时返回 xT, y, orix,批次数据是 (batch_xT, batch_y, batch_orix),无标签时返回 xT, orix ,批次数据是 (batch_xT, batch_orix),所以orix没有参与训练。这里保留下来可能是对于写模型时打印测试数据有帮助?

  • 而且,强化后的数据比原图好用,所以替换了

路径头准备
读数据时,可以发现数据路径由[共同部分]+[训练模式]组成,所以做了个字典映射一下训练模式,然后拼接成具体路径传入_readfile

3.2 getDataLoader 方法

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

一个DataLoader 方法,返回一个loader,用于提供batchsize,它将数据集封装成可迭代对象,支持批量加载、随机打乱等功能,以前介绍过这里不再赘述。

3.3 noLabDataset类和get_semi_loader类

这是半监督的数据类和loader,后面做大模型好像用不到这个,李哥说了解即可,那这里就略过了。

3.4 samplePlot画图函数

四、 model 模块

model模块会定义好模型框架,这里有一个李哥自己写的模型,还有一堆预训练好的模型。
模型本身不区分监督和半监督,它们的差别仅体现在数据的预处理上,当然无监督还是区分的。

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

4.1 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)
        )  #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

这是一个自定义模型类,init 定义了四个layer进行卷积,最后输出numclass个分类,forward完成了前向传播。

4.2 其他类

如何选取合适的模型不是目前的重点,重点在于了解调用这些大佬预训练好的模型我们都需要做什么。

def set_parameter_requires_grad(model, linear_probing):
    if linear_probing:
        for param in model.parameters():
            param.requires_grad = False                             # 一个参数的requires_grad设为false, 则训练时就会不更新

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

可以看到思路大致就是要明确:

  • **要不要使用大佬的参数:**就是调用models里的参数pretrained是否为use_pretrained
  • 要不要微调:set_parameter_requires_grad是否为的参数linear_prob是否为False(这里linear_prob同时承担了微调和线性探测的开关,如果linear_prob=True,则set_parameter_requires_grad会冻上所有参数,这时候只剩后来我们自己换上的的分类头可以训练,这称为线性探测;而linear_prob=False时,所有参数均可参与训练,这称为微调
  • 调整最终分类头,就是先获取原分类头的的输入维度,然后做一个linear,输入为刚获取的输入维度不变,输出为我们自己的分类数
  • 确定一下原数据集的输入维度input_size

4.3 参数筛选函数 prilearn_para

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)

这个函数用于在训练之前调用,如果你打算微调,那么要事先保存一份需要更新的参数的副本(李哥这好像忘了return params_to_update了),然后传给优化器。这主要是因为大模型的参数量实在太大了,事先筛选掉没梯度的参数可以节省算力

4.4 参数初始化函数 init_para

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

这个函数一般是自己从头到尾做一个模型时用,它会把模型所有的卷积层和归一化层的参数初始化为特定值,这是因为经验规律告诉我们这么做初始化的效果会比较好

五、 train 模块

train模块定义了兼顾监督和半监督训练流程的函数train_val,同时调用模块自动绘制损失图和准确率图

from tqdm import tqdm
import torch
import time
import matplotlib.pyplot as plt
import numpy as np
from 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']         # 优化器(AdamW/SGD)
    loss = para['loss']                   # 损失函数(交叉熵loss)
    epoch = para['epoch']                 # 总训练轮次
    device = para['device']               # 训练设备(cuda:0 / cpu)
    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']         # 隔多少轮做一次验证(比如每1轮验证1次)
    acc_thres = para['acc_thres']         # 开启半监督的精度阈值(比如acc>0.8才开)
    conf_thres = para['conf_thres']       # 伪标签置信度阈值(比如预测概率>0.9才用)
    do_semi= para['do_semi']              # 是否开启半监督训练(开关:True/False)

    semi_epoch = 10  # 半监督训练的执行间隔:每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          # 初始化:验证集最高精度为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()

5.1 train_val 函数

这个函数看起来很长,但实际上跟我们之前的学习基本一下,我来大致梳理一下:

5.1.1 参数和变量

参数para是在main函数里会打包好的一个字典,接受字典后会在函数内解包并赋给函数内变量

########################################################
    model = para['model']                 # 我的模型
    semi_loader = para['no_label_Loader'] # 无标签数据集的加载器(半监督核心)
    train_loader =para['train_loader']    # 带标签的训练集加载器
    val_loader = para['val_loader']       # 验证集加载器
    optimizer = para['optimizer']         # 优化器(AdamW/SGD)
    loss = para['loss']                   # 损失函数(交叉熵loss)
    epoch = para['epoch']                 # 总训练轮次
    device = para['device']               # 训练设备(cuda:0 / cpu)
    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']         # 隔多少轮做一次验证(比如每1轮验证1次)
    acc_thres = para['acc_thres']         # 开启半监督的精度阈值(比如acc>0.8才开)
    conf_thres = para['conf_thres']       # 伪标签置信度阈值(比如预测概率>0.9才用)
    do_semi= para['do_semi']              # 是否开启半监督训练(开关:True/False)

    semi_epoch = 10  # 半监督训练的执行间隔:每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          # 初始化:验证集最高精度为0(用来保存最优模型)

这里有一个问题,参数传进来后,如果model是一个预训练好的模型,那model = torch.load(pre_path)不久又重复加载一遍了吗?这里可能存在冗余

5.1.2 开始训练

    for i in range(epoch):  # 训练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])       # 让train和val对齐,方便画图
            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)

注释写的挺明白的了,这里补充几点:

  • 我们之前在回归实战里用的是SGD随机梯度下降方法,那是一种小批次梯度下降的方法,所以每一个epoch只训练batch_size的数据;而本次用的是AdamW方法,所以这里其实每个epoch都用所有数据完整地训练了一次模型
  • 半监督训练部分不去细究了,绝对不是因为我想偷懒
  • 模型和数据,必须放在同一个设备上,否则会程序崩溃
  • MyModel 继承了 torch.nn.Module 这个父类,nn.Module 这个父类内部,已经帮我们重写了 Python 的魔术方法__call__,nn.Module 的 call 方法内部,会自动调用写好的 forward方法,所以才可以直接model(x)完成前向

六、main 模块

main函数的工作就是做好类的实例化,设置好超参数,然后传参就行

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
             }


if __name__ == '__main__':
    train_val(trainpara)

至此训练完毕,可喜可贺可喜可贺

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐