一、项目介绍

        安装包——CV2和timm

        项目介绍:ML2021春季-HW3。(网址:ml2021spring-hw3

        共有11类食物。 其中带标签的数据有 280 *11张照片 ;不带标签的训练数据有 6786 张照片;验证集有30*11张照片 ;测试集有3347张照片。

二、数据增广

        当训练好模型后,譬如模型可以很好的分辨一张照片是猫还是狗,但当将同样内容但是尺寸更小或者角度不同的照片输入模型,模型依旧不能成功分辨。原因可能是因为不同尺寸照片对应的卷积核大小不同,因此训练出的模型并没有那么“智能”。

        为了解决这一问题,在训练之初就将图片进行各种缩放、翻转、对比度调大等变化输入模型,让模型训练各种形式的图片,以达到训练效果,这一过程称为数据增广。

三、优化器Adam、AdamW和SGD

        SGD仅利用当前点的梯度进行参数的更新。

        Adam相对于SGD存在两个变换。1.梯度改变为:θ*η(old)+(1-θ)*η(now)。某点的计算梯度并不完全等于当前点的梯度,而是如公式所示不仅要考虑当前点的梯度,同时还要考虑前点的梯度值。2.Adam会自动调整学习率。当梯度过大,学习率会自动减小;当梯度过小,学习率会自动增大。

        权重衰减(Weight Decay)是深度学习中一种常用的正则化技术,通过在损失函数中添加参数的 L2 范数惩罚项,限制模型参数的大小,从而防止过拟合。AdamW即在Adam的基础上加上了改变了权重衰减的处理方式,从而具有了更加稳定的收敛能力。

四、迁移学习

        数据量少时的最佳选择。当个人在做图像分类时,硬件差、数据少,因此模型训练结果差。此时可以直接借用已经训练好的模型。

        如图,将模型训练步骤可以分为编码器与分类图。已经训练好的模型提取出了优秀的图片特征,计算好了相关参数,因此我们可以直接利用已经训练好的编码器,再使用自己的分类头。

        

五、半监督学习

        监督学习,即通过有标签的数据与训练结果求Loss值来更新模型。半监督学习则是既有有标签数据也有无标签数据。

        如何使用无标签数据。如图当模型进过一定训练且拥有一定准确率后,可以将无标签数据投入模型中进行预测,将预测结果作为真实标签用于模型的进一步训练。当然并非所有预测结果都会被认为是可靠的,而是必须超过一定可信度,即一定概率,才认为这个预测结果可以用于下一步模型训练。(如同样都是类别1,【0.9  0.05  0.05】认为是可靠的预测结果,而【0.4  0.3  0.3】则认为是不可靠的)

六、项目实战

import random
import torch #深度学习框架的核心库
import torch.nn as nn #用于构建神经网络的模块,包含了各种层(如卷积层、全连接层)、激活函数等
import numpy as np #用于数值计算
import os #与操作系统交互的库
from PIL import Image #读取图片数据
from tqdm import tqdm #进度条库,它可以为循环、迭代过程添加可视化的进度条
from torchvision import transforms #用于数据增广
import time
import matplotlib.pyplot as plt
form              import initialize_model #引入初始化参数
from torch.utils.data import Dataset, DataLoader #Dataset类,其用于封装自定义数据集,需要子类继承的两个核心方法:__len__()返回数据集的总样本数;__getitem__(index)根据索引index返回一个样本

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


###数据处理

HW=224 #定义图片大小      


train_transform=transforms.Compose( #通过transforms.Compose将多个增广操作组合成一个序列
    [
        transforms.ToPILImage(), #将数组输出为(3,H,w)大小的PIL图片
        transforms.RandomResizedCrop(224), #随机放大并裁切
        transforms.RandomRotation(50), #图片在50度以内随机旋转
        transforms.RandomHorizontalFlip(),
        autoaugment.AutoAugment(), #自动选择增广方式
        transforms.ToTensor() #将图片转为张量
    ]

)


val_transform=transforms.Compose( #用原图进行验证
    [
        transforms.ToPILImage(), #将数组输出为(3,H,w)大小的PIL图片
        transforms.ToTensor() #将图片转为张量
    ]
)


class food_Dataset(Dataset) #继承Dataset抽象基类
    def __init__(self,path,mode="train"):
        self.mode=mode
        if mode=="semi": #半监督学习只返回数据内容
            self.X=self.read_file(path)
        else:
            self.X,self.Y=self.read_file(path)
            self.Y=torch.LongTensor(self.Y) #将标签转化为长整型
        if mode=="train":
            self.transforms=train_transforms #数据增广
        else:
            self.transforms=val_transforms

    def read_file(self,path): #用于读取图片的函数
        if self.mode=="semi":
            file_list=os.listdir(path) #列出文件夹下所有文件名字

            xi=np.zeros((len(file_list),HW,HW,3),dtype=np.uint8) #搭建一个全为0的数组空间,用于放入图片;dtype=np.unit8数组空间读为整型

            for j,img_name in enumerate(tqdm(path)): #内置函数enumerate()会将列表转换为一个可迭代的枚举对象,每次迭代返回一个元组(索引, 元素)
                img_path=os.path.join(path,img_name) #os提供的官方模块,拼接多个路径片段
                img=Image.open(img_path) #打开路径下的图片
                img=img.resize((HW,HW)) #调整图片大小
                xi[j,...]=img #将图片放入数组中去
            print("读到了%d个数据"%len(xi))
            return xi

        else:
            for i in tqdm(range(11)):
                file_dir=path+"/%02d"%i #写入每个食物类型文件夹的地址
                file_list=os.listdir(file_dir) #列出文件夹下所有文件名字

                xi=np.zeros((len(file_list),HW,HW,3),dtype=np.uint8) #搭建一个全为0的数组空间,用于放入图片;dtype=np.unit8数组空间读为整型
                yi=np.zeros((len(file_list),dtype=np.uint8)

                for j,img_name in enumerate(tqdm(file_list)): #内置函数enumerate()会将列表转换为一个可迭代的枚举对象,每次迭代返回一个元组(索引, 元素)
                    img_path=os.path.join(file_dir,img_name) #os提供的官方模块,拼接多个路径片段
                    img=Image.open(img_path) #打开路径下的图片
                    img=img.resize((HW,HW)) #调整图片大小
                    xi[j,...]=img #将图片放入数组中去
                    yi[j]=i

                if i==0:
                    X=xi
                    Y=yi
                else:
                    X=np.concatenate((X,xi),axis=0) #沿着指定的轴拼接多个数组
                    Y=np.concatenate((Y,yi),axis=0)
            print("读到了%d个数据"%len(Y))
            return X,Y

    def __getitem__(self,item): #返回数据
        if self.mode=="semi":
            return self.transforms(self.X[item]),self.X[item]
        else:
            return self.transforms(self.X[item]),self.Y[item]

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


###半监督学习
class semiDataset(Dataset)
    def __init__(self,no_label_loader,model,device,thres=0.7): #传入对无标签数进行分类所需参数,置信度0.99
        x, y=self.get_label(no_label_loader,model,device,thres)
        if x==[]: #如果数据为空
            self.flag=False
        else:
            self.flag=True
            self.X=np.array(x) #将列表转为数组
            self.Y=torch.LongTensor(y) #将列表转为长整型张量
            self.transform=train_transform
  
    def get_label(self,no_label_loader,model,device,thres) #对无标签数进行分类的函数 
        model=model.to(device)

        pred_prob=[] #创建空列表存放预测结果
        labels=[]
        x=[] #创建空列表存放最后合格的结果
        y=[]

        soft=nn.softmax() #创建一个Softmax激活函数的实例
        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_value=pred_soft.max(1) #返回一个元组(第1维最大值,所在的索引)
                pred_prob.extend(pred_max.cpu().numpy().tolist()) #.extend()可以合并俩个列表;.numpy().tolist()将张量转化为数组再转化为列表
                labels.extend(pred_value..cpu().numpy().tolist())

            for index,prob in enumerate(pred_prob):
                if prob>thres: #若概率大于置信度
                    x.append(no_label_loader.dataset[index][1]) #利用.dataset指向Dataloader所包装的原始数据集对象来获取原始图片
                    y.append(labels[index])
            return x,y

    def __getitem__(self,item):
        return self.transform(self.X[item]),self.Y[item]

    def __len__(self)
        return len(self.X)


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

###模型
class myModel(nn.Module) #继承nn.Module类的核心功能
    def __init__(self,num_class):
        super(myModel,self).__init__()
        self.conv1=nn.Conv2d(3,64,3,1,1) #第一层卷积层。特征图尺寸变化3*224*224->64*224*224
        self.bn1=nn.BatchNorm2d(64) #参数归一化
        self.relu1=nn.ReLU() #激活函数
        self.pool1=nn.MaxPool2d(2) #第一层池化层。64*224*224->64*112*112

        self.layer1=nn.Sequential( #将一系列层封装成一个模块
            nn.Conv2d(64,128,3,1,1), #—>128*112*112
            nn.BatchNorm2d(128), 
            nn.ReLU(), 
            nn.MaxPool2d(2), #—>128*56*56
        )

        self.layer2=nn.Sequential( 
            nn.Conv2d(128,256,3,1,1), 
            nn.BatchNorm2d(256), 
            nn.ReLU(), 
            nn.MaxPool2d(2), #—>256*28*28
        )

        self.layer3=nn.Sequential( 
            nn.Conv2d(256,512,3,1,1), 
            nn.BatchNorm2d(512), 
            nn.ReLU(), 
            nn.MaxPool2d(2), #—>512*14*14
        )

        self.pool2=nn.MaxPool2d(2) #—>512*7*7

        self.fc1=nn.Linear(25088,1000) #第一层全连接层
        self.relu2=nn.ReLU()
        self.fc2=nn.Linear(1000,num_class)

    def forward(self,x):
        x=self.conv1(x)
        x=self.bn1(x)
        x=self.relu1(x)
        x=self.pool1(x)
        x=self.layer1(x)
        x=self.layer2(x)
        x=self.layer3(x)
        x=self.pool2(x)
        x=x.view(x.size[0],-1) #拉直特征图
        x=self.fc1(x)
        x=self.relu2(x)
        x=self.fc2(x)
        return x


#model=myModel(11)


###迁移学习
from torchvision.models import resnet18 #导入模型
model=resnet18(pretrained=True) #不仅使用模型架构,还保留参数,不从0开始训练
in_fetures= model.fc.in_fetures #获取模型的分类头输入维度
model.fc=nn.Linear(in_fetures,11) 


###训练函数
def train_val(model,train_loader,val_loader,no_label_loader,device,epochs,optimizer,loss,thres,save_path) #定义训练函数
    model=model.to(device)
    
    semi_loader=None

    plt_train_loss=[] #创建记录loss值的空列表,记录所有轮次的loss值
    plt_val_loss=[]

    plt_train_acc=[] #创建记录loss值的空列表,记录模型的准确率
    plt_val_acc=[]

    max_acc=0.0 #记录最高准确率
 
    for epoch in range(epochs):
        train_loss=0.0 #初始化loss值
        val_loss=0.0
        train_acc=0.0 #初始化准确率
        val_acc=0.0
        semi_loss=0.0 #初始化semi相关值
        semi_acc=0.0

        start_time=time.time()
        
        model.train() #模型调整为训练模式,确保Dropout、BatchNorm等层使用训练模式的逻辑
        for batch_x,batch_y in train_loader:
            x,target=batch_x.to(device),batch_y.to(device) #将函数放置GPU计算        
            pred=model(x) #利用已经定义好的类,得到预测值
            train_bat_loss=loss(pred,target,model) #loss值为一批的平均值
            train_bat_loss.backward #梯度回传
            optimizer.step() #更新模型
            optimizer.zero_grad() #梯度清零
            train_loss+=train_bat_loss.cpu().item() #.item()取出数值;.cpu()将模型放回cpu
            train_acc+=np.sum(np.argmax(pred.detach().cpu().numpy(),axis=1)==target.cpu().numpy()) #.detach()从计算图中分离张量pred;.numpy()将张量转换为数组,便于使用NumPy的函数进行后续计算;np.argmax()返回沿指定轴的最大值索引;np.sum()用于对布尔数组求和,True会被当作1,False当作0;这段代码用于计算当前批次中模型预测正确的样本数量
            
        plt_train_loss.append(train_loss/train_loader.__len__()) #.append()用于在列表的末尾添加一个新元素;train_loss/train_loader.__len__()计算每一个样本的平均loss值
        plt_train_acc.append(train_acc/train_loader.dataset.__len__()) #预测对的数量除以总长等于准确率
 
        if semi_loader!=None: #倘若某次semi_loader不为空,开始训练里面的数据        
            for batch_x,batch_y in semi_loader: 
                x,target=batch_x.to(device),batch_y.to(device) #将函数放置GPU计算        
                pred=model(x) #利用已经定义好的类,得到预测值
                semi_bat_loss=loss(pred,target,model) #loss值为一批的平均值
                semi_bat_loss.backward #梯度回传
                optimizer.step() #更新模型
                optimizer.zero_grad() #梯度清零
                semi_loss+=train_bat_loss.cpu().item() 
                semi_acc+=np.sum(np.argmax(pred.detach().cpu().numpy(),axis=1)==target.cpu().numpy()) #.detach()从计算图中分离张量pred;.numpy()将张量转换为数组,便于使用NumPy的函数进行后续计算;np.argmax()返回沿指定轴的最大值索引;np.sum()用于对布尔数组求和,True会被当作1,False当作0;这段代码用于计算当前批次中模型预测正确的样本数量

            print("半监督数据集的训练准确率为:",semi_acc/semi_loader.dataset.__len__())            

        model.eval() #模型调整为验证模式
        with torch.no_grad(): #停止梯度计算
            for batch_x,batch_y in val_loader:
                x,target=batch_x.to(device),batch_y.to(device) 
                pred=model(x)
                val_bat_loss=loss(pred,target,model)
                val_loss+=val_bat_loss.cpu().item()
                val_acc+=np.sum(np.argmax(pred.detach().cpu().numpy(),axis=1)==target.cpu().numpy())
        plt_val_loss.append(val_bat_loss/val_loader.__len__())
        plt_acc.append(val_acc/val_loader.dataset.__len__())

        if epoch%5==0 and plt_val_acc[-1]>0.7: #若模型已经训练到一定效果,可以开始使用无标签数据集
            semi_loader=get_semi_loader(no_label_loader,model,device,thres)

        if val_acc>max_acc: #判断模型是否是最优
            torch.save(model,save.path) #保存当前模型
            max_acc=val_acc

        print("[%03d/%03d] %2.2f sec(s) Trainloss:%.6f |valloss:%.6f Trainacc:%.6f |valacc:%.6f" %
            (epoch,epochs,time.time()-start_time,plt_train_loss[-1],plt_val_loss[-1],plt_train_acc[-1],plt_val_acc[-1])) #%后面直接跟随元组参数

    plt.plot(plt_train_loss) #画图
    plt.plot(plt_val_loss)
    plt.title("loss图") #显示标题 
    plt.legend["train","val"] #显示图例

    plt.plot(plt_train_acc) #画图
    plt.plot(plt_val_acc)
    plt.title("acc图") #显示标题 
    plt.legend["train","val"] #显示图例


###数据与超参数
train_path=r"             " #文件路径。r用于禁用字符串中反斜杠\的转义功能,仅作为普通字符存在
val_path=r"             "
no_label_path=r"              "

train_set=food_Dataset(train_path,"train")
val_set=food_Dataset(val_path,"val")
no_label_set=food_Dataset(no_label_path,"semi")

train_loader=DataLoader(train_set,batch_size=4,shuffle=True) #数据批处理并打乱数据
val_loader=DataLoader(val_set,batch_size=4,shuffle=True)
no_label_loader=DaraLoader(no_label_set,batch_size=4,shuffle=False)


lr=0.001 #学习率
loss=nn.CrossEntropyLoss() #交叉熵损失,自带Softmax操作
optimizer=torch.optim.AdamW(model.parameters(),lr=lr,weight_decay=1e-4)
device="cuda" if torch.cuda.is_available() else"cpu"
save_path="model_save/best_model.pth"
epochs=20
thres=0.7

train_val(model,train_loader,val_loader,no_label_loader,device,epochs,optimizer,loss,thres,save_path)

初始化模型代码,可以引入经典模型

#传入模型名字,和分类数, 返回你想要的模型
def initialize_model(model_name, num_classes, linear_prob=False, use_pretrained=True):
    # 初始化将在此if语句中设置的这些变量。
    # 每个变量都是模型特定的。
    # linear_prob为线性探测,即不改变预训练参数;与之相反称为微调,即在新的数据集上继续训练
    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

使用官方数据集

from torchvision.datasets import FashionMNIST

train_set=FashionMNIST(root="FashionMnist",train=True,download=False,transform=train_transform)

"""
FashionMNIST 包含 10 个类别的灰度图像(尺寸 28×28),类别包括:T 恤、裤子、套衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包、短靴。
"""

"""
root="FashionMnist"指定数据集的存储路径(本地文件夹)。
train=True表示加载的是训练集(包含 60,000 个样本)。
download=False表示不自动下载数据集.若首次使用该数据集,需改为download=True
transform=train_transform指定对数据进行的预处理操作(如缩放、归一化、数据增强等)。
"""

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐