李哥深度学习四——分类任务

本文的任务是实现对于食品数据集的分类,数据集来源:https://www.kaggle.com/datasets/zhaopang/ml2021springhw3/data

1. 理论概述:

        此项目的数据集分为training,testing,validation,而training训练集中又分为labeled、unlabeled两部分,对于labeled带标签的数据我们使用监督学习的方式,而对于unlabeled的数据集我们则用半监督学习(Semi-Supervised Learning)的方式训练。在监督学习中,既有x(输入特征),y(目标标签,即模型需要预测的输出值);而在半监督学习中,我们仅有x,该如何训练模型呢?此时我们通过某种打标签的方式(伪标签)将无标签数据经过模型的预测值作为我们的y,并且值大小要超过我们所规定的参数:thres(置信度)。只有这样的数据被才视为可信数据,传入semi_dataset数据集中进行半监督学习的模型训练。

        值得注意的是,对于分类任务的输出,也就是是或不是某类的问题,我们采取独热编码(one-hot)的形式将y由一离散值转化为连续的概率分布,而对于模型的输出我们使用softmax函数转换为概率分布,进而实现统一的数据处理

  • 真实标签[0, 0, 1, 0](one-hot 编码)。
  • 模型输出[0.1, 0.2, 0.6, 0.1](Softmax 输出)。

         同时,对于模型性能好坏的评估,我们则采用交叉熵损失函数(cross entropy)作为loss来进行衡量。

        下面我们再介绍可能会用到的基本概念:

        A.我们知道,图片实际上就是一堆像素点的集合,天然为矩阵。拿彩色图片举例,每个像素的值就是一个向量,表示颜色通道:也就是我们常说的RGB。由此,彩色图片可以表示为一个3维矩阵,即为(C,H,W)(通道数,高度,宽度)

        B.对于高分辨率的图片,全连接层的参数数量会非常庞大,导致计算与存储开销大;同时,若我们考虑flatten展平此图片,部分空间结构信息也可能丢失。对此,我们引入了卷积操作。(convolution)

        直观感受一下卷积的效果:(卷积操作是通过卷积核在输入图片上滑动,每次计算卷积核覆盖区域与卷积核的 逐元素乘积,然后将所有乘积结果 求和,得到输出特征图的一个值。)

输入图片
[  
  [1, 0, 1],  
  [0, 1, 0],  
  [1, 0, 1]  
]
卷积核
[  
  [1, 0],  
  [0, -1]  
]
输出结果
[  
  [0, 0],  
  [0, 0]  
]

        我们由一个3*3的图片得到了2*2的图片,有效降低了特征图的尺寸(若考虑维持特征图的尺寸,则可以考虑通过zeropadding操作,外圈补零)。所以说,我们可以考虑通过卷积操作之后再flatten来有效的降低参数数量

        C.继B往后说,如果我们引入卷积操作后依然参数过多(给出卷积层的参数量计算公式为(H*W*C_in+1)*C_out,其中1为偏置参数,H、W为卷积核的高度与宽度,C_in、C_out为输入特征图与输出特征图的通道数),此时我们则考虑1.扩大卷积步长。我们给出一个卷积尺寸的计算公式:O = (I - K+2P) / S + 1,(O为输出特征图的尺寸,I为输入特征图的尺寸,K则是卷积核的大小,S为步长,P则为padding补零的次数)。可惜的是,此方法计算复杂且可能会丢失信息。故我们常考虑 2. 池化(Pooling),其含义大概是从一个矩阵中选取一个代表数来表征信息。池化有2种,最大池化(Max)与平均池化(Avg),我们这里考虑最大池化,每次操作后减半特征图的尺寸

        由A、B、C,我们引出分类任务中一个重要的概念:卷积神经网络(CNN),其结构如下:

  1. 卷积层:提取局部特征。多个卷积核卷,改变C(通道数)
  2. 激活层:引入激活函数(如 ReLU)。
  3. 池化层:减少特征图的空间尺寸,降低计算量。改变H,W(高宽)
  4. 全连接层(FC):将提取的特征映射到输出类别。应用linear(A,B)操作,将向量从A维度改变至B维度
  5. 应用softmax函数得到模型输出
  6. 计算5.中结果与y的交叉熵损失得到loss,更新网络参数,训练模型

        简而言之,图片分类模型的训练流程就是:将输入图片经过CNN提取特征,再经由分类头得到分类结果,并同标签计算交叉熵损失,更新模型

        值得注意的是,CNN中还有两种常用的技术:Dropout、Batch Norm:

批归一化层:对于每个小批量的数据进行归一化,加速训练,提高模型稳定性。

Dropout 层:在训练过程中,随机丢弃一部分神经元(输出置为0),防止过拟合

 2. 进入代码的编写,还是熟悉的导入相关函数库

import random
import torch
import torch.nn as nn
import numpy as np
import os
from PIL import Image #处理图像数据
from torch.utils.data import Dataset, DataLoader #加载数据
from tqdm import tqdm #显示进度条
from torchvision import transforms #数据变换
import time
import matplotlib.pyplot as plt
from model_utils.model import initialize_model

3.确保实验的可重复性与初始化:

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) #固定随机种子为0
###############################################

HW = 224 #设定图像默认尺寸为244*244

4. 引入数据增广,提高模型泛化能力

          数据增广有多种方式,如旋转、翻转、裁剪、缩放、改变灰度......

#引入数据增广,防止过拟合
train_transform = transforms.Compose( #训练集的数据变换
    [
        transforms.ToPILImage(),   #224, 224, 3模型  :3, 224, 224
        transforms.RandomResizedCrop(224), #随机裁切
        transforms.RandomRotation(50), #随机变换
        transforms.ToTensor() #将PIL图像化为tensor张量
    ]
)

val_transform = transforms.Compose( #验证集的数据变换
    [
        transforms.ToPILImage(),   #224, 224, 3模型  :3, 224, 224
        transforms.ToTensor()
    ]
)

5.数据预处理:

​
class food_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.transform = train_transform #数据变换
        else:
            self.transform = val_transform

    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) #初始化数组用于存储图片,像素为整数

            for j, img_name in enumerate(file_list):#遍历文件
                img_path = os.path.join(path, img_name)
                img = Image.open(img_path)
                img = img.resize((HW, HW)) #调整图片大小为默认的224*224
                xi[j, ...] = img #存图
            print("读到了%d个数据" % len(xi))
            return xi
        else:
            for i in tqdm(range(11)): #tqdm显示进度
                file_dir = path + "/%02d" % i #文件路径
                file_list = os.listdir(file_dir) #列出文件夹下所有文件的名称

                xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
                yi = np.zeros(len(file_list), dtype=np.uint8)#初始化存标签的数组

                # 列出文件夹下所有文件名字
                for j, img_name in enumerate(file_list):
                    img_path = os.path.join(file_dir, img_name) #图片路径
                    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) #非第一个就并入其中,将11个文件夹中的图片全放入一个矩阵中
                    Y = np.concatenate((Y, yi), axis=0) #并标签
            print("读到了%d个数据" % len(Y))
            return X, Y

    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): #返回数据集的样本数量
        return len(self.X)

class semiDataset(Dataset):
    def __init__(self, no_label_loder, model, device, thres=0.99):
        x, y = self.get_label(no_label_loder, 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_loder, model, device, thres):#将可信任数据加入训练集中置为标签
        model = model.to(device) #挂上设备
        pred_prob = [] #存储预测概率
        labels = []
        x = []
        y = []
        soft = nn.Softmax()
        with torch.no_grad():
            for bat_x, _ in no_label_loder:
                bat_x = bat_x.to(device)
                pred = model(bat_x)
                pred_soft = soft(pred) #softmax
                pred_max, pred_value = pred_soft.max(1) #获取每个样本的最大概率及其对应下标
                pred_prob.extend(pred_max.cpu().numpy().tolist())
                labels.extend(pred_value.cpu().numpy().tolist())

        for index, prob in enumerate(pred_prob): #遍历预测概率
            if prob > thres:
                x.append(no_label_loder.dataset[index][1])   #调用到原始的getitem
                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)

​

6.数据加载

def get_semi_loader(no_label_loder, model, device, thres):
    semiset = semiDataset(no_label_loder, model, device, thres)
    if semiset.flag == False: #探测是否有有效样本
        return None
    else:
        semi_loader = DataLoader(semiset, batch_size=16, shuffle=False)
        return semi_loader

7.定义模型

        这是自己定义的训练模型,下面还会用到大佬的模型:

class myModel(nn.Module):
    def __init__(self, num_class):
        super(myModel, self).__init__()
        #3 *224 *224  -> 512*7*7 -> 拉直 -> 全连接分类
        self.layer0 = nn.Sequential(
            nn.Conv2d(3, 64, 3, 1, 1),  # 64*224*224
            nn.BatchNorm2d(64),  # 批归一化
            nn.ReLU(),  # 激活函数
            nn.MaxPool2d(2)  # 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)   #25088->1000
        self.relu2 = nn.ReLU()
        self.fc2 = nn.Linear(1000, num_class)  #1000-11

    def forward(self, x):
        x = self.layer0(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.pool2(x)
        x = x.view(x.size()[0], -1) #flatten
        x = self.fc1(x)#全连接
        x = self.relu2(x)#激活
        x = self.fc2(x)
        return x

8.模型训练:

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 = [] #训练损失
    plt_val_loss = [] #验证损失

    plt_train_acc = [] #训练准确率
    plt_val_acc = [] #验证准确率

    max_acc = 0.0

    for epoch in range(epochs):
        train_loss = 0.0
        val_loss = 0.0
        train_acc = 0.0
        val_acc = 0.0
        semi_loss = 0.0
        semi_acc = 0.0


        start_time = time.time()

        model.train() #模型为训练模式
        for batch_x, batch_y in train_loader:
            x, target = batch_x.to(device), batch_y.to(device)
            pred = model(x)
            train_bat_loss = loss(pred, target)
            train_bat_loss.backward()
            optimizer.step()  # 更新参数 之后要梯度清零否则会累积梯度
            optimizer.zero_grad()
            train_loss += train_bat_loss.cpu().item()
            train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy()) #累计准确的个数
        plt_train_loss.append(train_loss / train_loader.__len__())
        plt_train_acc.append(train_acc/train_loader.dataset.__len__()) #记录准确率,

        if semi_loader!= None: #存在半监督加载器
            for batch_x, batch_y in semi_loader:
                x, target = batch_x.to(device), batch_y.to(device)
                pred = model(x)
                semi_bat_loss = loss(pred, target)
                semi_bat_loss.backward()
                optimizer.step()  # 更新参数 之后要梯度清零否则会累积梯度
                optimizer.zero_grad()
                semi_loss += semi_bat_loss.cpu().item()
                semi_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
            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)
                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_loss / val_loader.dataset.__len__())
        plt_val_acc.append(val_acc / val_loader.dataset.__len__())

        if epoch%3 == 0 and plt_val_acc[-1] > 0.6: #每3轮进行加一次数据
            semi_loader = get_semi_loader(no_label_loader, model, device, thres)

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

        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])
              )  # 打印训练结果。 注意python语法, %2.2f 表示小数位为2的浮点数, 后面可以对应。

    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("acc")
    plt.legend(["train", "val"])
    plt.show()

9.实例化参数与模型

train_path = r"D:\project1\food_classification\food-11_sample\training\labeled" #选用样例数据集训练,本地跑的更快点
val_path = r"D:\project1\food_classification\food-11_sample\validation"
no_label_path = r"D:\project1\food_classification\food-11_sample\training\unlabeled\00"

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=16, shuffle=True)
val_loader = DataLoader(val_set, batch_size=16, shuffle=True)
no_label_loader = DataLoader(no_label_set, batch_size=16, shuffle=False)

# model = myModel(11)
model, _ = initialize_model("vgg", 11, use_pretrained=True) #迁移学习


lr = 0.001 #学习率
loss = nn.CrossEntropyLoss() #交差熵损失
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4) #Adamw优化
device = "cuda" if torch.cuda.is_available() else "cpu"
save_path = "model_save/best_model.pth"
epochs = 15
thres = 0.99 #置信度



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

值得注意的是,这里我并没有调用自己的模型,而是选择使用了迁移学习,调用了大佬的模型(效果好)

迁移学习:

  • 在一个 源任务(Source Task) 上训练模型。
  • 将学到的知识(如模型参数、特征表示等)迁移到 目标任务(Target Task) 中。
  • 在目标任务上进一步微调或直接应用模型(线性探测)。

 迁移学习常用作医学影像、图像分类、自然语言处理(nlp)领域

10.测试结果:

        由trainning数据集中带标签的子集可知,我们大概要分出11类,而我们的预测准确率却仅有0.09,也就是相当于11个里面随机猜1个是不是对应的真实类。

        造成这个现象的原因主要是由于我在模型训练过程中用的是sample的数据集,训练样本太小,进而导致分类准确率不高。考虑应用food11的大数据集,并尝试应用不同的CNN模型,如:resnet18、AlexNet、VGG以及自己设计的模型,对比性能后有以下结果:

Resnet18

 

VGG

AlexNet

 

自己的模型

 

对比可知,AlexNet模型训练效果最差,和sample数据集时准确率竟然差不多;表现最好的是ResNet18模型,达到了60%的验证准确率。  

------晚点这里放个调整超参数后的结果,望记得

11.卷积神经网络的发展历程:

LeNet:最早的CNN之一,用于手写数字识别

AlexNet(开创性)创新点:

A.应用了ReLU激活函数

B.drop out 舍弃操作,缓解过拟合 

C.池化

VGG: 应用了小卷积核代替大卷积核,使CNN更深更大,进而提升了模型能力

ResNet,创新点:

A.1*1卷积:减少了参数量

B.残差连接,防梯度消失,数学上表示为: y=F(x)+x,意为在神经网络中,将某一层的输入直接加到后续层的输出上

C.批归一化,加速了模型训练过程

12. 总结与其他:

1.深度学习 = 玩特征,数据特征的处理非常重要。同时望自己对于此文常看常新

2.要注意数据不平衡问题,考虑从两方面入手解决,一方面考虑过采样、欠采样、SMOTE解决问题,另一方面考虑引入F1-Score、召回率(减少假阴性的影响)、精确率(减少假阳性的影响)等分类任务常用的评价指标

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值