天天看點

Pytorch版的Efficientnet訓練自己的資料集前言一、環境搭建二、資料準備三、訓練四、測試總結

文章目錄

  • 前言
  • 一、環境搭建
  • 二、資料準備
    • 1.資料擺放
    • 2、訓練集和驗證集切分
  • 三、訓練
    • 1.預訓練模型下載下傳
    • 2.加載模型
    • 3.資料讀取部分
    • 4、學習率衰減政策
    • 5、完整訓練代碼:
  • 四、測試
    • 1、完整測試代碼:
    • 2、結果:
  • 總結

前言

  最近,自己需要一個分類網絡來完成一項任務,于是便想起了身邊人推薦過的Efficientnet,據說效果是較為穩定的,是以自己來一探究竟,示例的話就用個最簡單的二分類吧。

一、環境搭建

本人使用的環境為:

python3.6
torch=1.5
torchvision =0.6.0
opencv-python=4.5.1.48
           

以上這些僅供參考,無需一緻,重要的使我們還需要安裝pytorch集合進來的Efficientnet子產品,在我們要使用的python環境下,執行指令

pip install efficientnet_pytorch
           

其他依賴項到時逐個安裝即可。

二、資料準備

1.資料擺放

  原始資料擺放如下:

Pytorch版的Efficientnet訓練自己的資料集前言一、環境搭建二、資料準備三、訓練四、測試總結

  也就是以類别名來命名檔案夾名,将對應的類别圖檔放置對應的檔案夾下,一般來說,分類任務的資料集大多都是這樣來擺放的。

2、訓練集和驗證集切分

  這一步隻需要運作dataset.py即可,它會按照我們制定的比例将我們的資料集進行切分開,同時,為了減少直接resize帶來的圖檔變形的弊端,這裡在切分的同時我對資料還進行補邊的操作,也就是将資料盡量變為正方形的樣子,代碼如下

#為efficientnet訓練分類的資料進行預處理(訓練集切分+補邊)
import os
import glob
import cv2
import random
from pathlib import Path


#補邊,這一步主要是為了将圖檔填充為正方形,防止直接resize導緻圖檔變形
def expend_img(img):
    '''
    :param img: 圖檔資料
    :return:
    '''
    fill_pix=[122,122,122] #填充色素,可自己設定
    h,w=img.shape[:2]
    if h>=w: #左右填充
        padd_width=int(h-w)//2
        padd_top,padd_bottom,padd_left,padd_right=0,0,padd_width,padd_width #各個方向的填充像素
    elif h<w: #上下填充
        padd_high=int(w-h)//2
        padd_top,padd_bottom,padd_left,padd_right=padd_high,padd_high,0,0 #各個方向的填充像素
    new_img = cv2.copyMakeBorder(img,padd_top,padd_bottom,padd_left,padd_right,cv2.BORDER_CONSTANT, value=fill_pix)
    return new_img


#切分訓練集和測試集,并進行補邊處理
def split_train_test(img_dir,save_dir,train_val_num):
    '''
    :param img_dir: 原始圖檔路徑,注意是所有類别所在檔案夾的上一級目錄
    :param save_dir: 儲存圖檔路徑
    :param train_val_num: 切分比例
    :return:
    '''
    img_dir_list=glob.glob(img_dir+os.sep+"*")#擷取每個類别所在的路徑(一個類别對應一個檔案夾)
    for class_dir in img_dir_list:
        class_name=class_dir.split(os.sep)[-1] #擷取目前類别
        img_list=glob.glob(class_dir+os.sep+"*") #擷取每個類别檔案夾下的所有圖檔
        all_num=len(img_list) #擷取總個數
        train_list=random.sample(img_list,int(all_num*train_val_num)) #訓練集圖檔所在路徑
        save_train=save_dir+os.sep+"train"+os.sep+class_name
        save_val=save_dir+os.sep+"val"+os.sep+class_name
        os.makedirs(save_train,exist_ok=True)
        os.makedirs(save_val,exist_ok=True) #建立對應的檔案夾
        print(class_name+" trian num",len(train_list))
        print(class_name+" val num",all_num-len(train_list))
        #儲存切分好的資料集
        for imgpath in img_list:
            imgname=Path(imgpath).name #擷取檔案名
            if imgpath in train_list:
                img=cv2.imread(imgpath)
                new_img=expend_img(img)
                cv2.imwrite(save_train+os.sep+imgname,new_img)
            else: #将除了訓練集意外的資料均視為驗證集
                img = cv2.imread(imgpath)
                new_img = expend_img(img)
                cv2.imwrite(save_val + os.sep + imgname, new_img)

    print("split train and val finished !")

           

這裡,也對代碼内容和相關參數進行了注釋,了解起來應該不是很難

運作它的時候,我們隻需要調用split_train_test()函數,輸入指定的參數(3個)即可,需要注意的是,這裡給的原始圖檔的路徑是所有類别檔案夾的上一級,程式會依次周遊它下面的各個檔案夾來進行切分運作完成後,會生成對應的訓練集和測試集,如下圖:

Pytorch版的Efficientnet訓練自己的資料集前言一、環境搭建二、資料準備三、訓練四、測試總結

train和val裡也會有生成各個類别的檔案夾用于儲存不同類别的資料,需要注意的是,這裡存放的資料是我經過補邊之後的,對原路徑的資料集不會有改動,填充顔色我預設設定為了灰色,可以根據自己愛好在代碼中自行更改

三、訓練

1.預訓練模型下載下傳

這裡可以對代碼進行更改,使它自動下載下傳模型,我是覺得慢,是以手動下載下傳了,網址如下:

'''
efficientnet-b0: https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b0-355c32eb.pth
efficientnet-b1: https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b1-f1951068.pth
efficientnet-b2: https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b2-8bb594d6.pth
efficientnet-b3: https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b3-5fb5a3c3.pth
efficientnet-b4: https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b4-6ed6700e.pth
efficientnet-b5: https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b5-b6417697.pth
efficientnet-b6: https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b6-c76e70fd.pth
efficientnet-b7: https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b7-dcc49843.pth
'''
           

我選用的是b0

2.加載模型

代碼如下(示例):

base_model = EfficientNet.from_name('efficientnet-b0') #加載模型,使用b幾的就改為b幾
        state_dict = torch.load(self.weights)
        base_model.load_state_dict(state_dict)
        # 修改全連接配接層
        num_ftrs = base_model._fc.in_features
        base_model._fc = nn.Linear(num_ftrs, self.class_num)
        self.model = base_model.to(device)
           

3.資料讀取部分

這裡對資料進行了指定的資料變換(增強),可以根據需求進行删改,代碼如下:

#資料處理
    def process(self):
        # 資料增強
        data_transforms = {
            'train': transforms.Compose([
                transforms.Resize((self.imgsz, self.imgsz)),  # resize
                transforms.CenterCrop((self.imgsz, self.imgsz)),  # 中心裁剪
                transforms.RandomRotation(10),  # 随機旋轉,旋轉範圍為【-10,10】
                transforms.RandomHorizontalFlip(p=0.2),  # 水準鏡像
                transforms.ToTensor(),  # 轉換為張量
                transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  # 标準化
            ]),
            "val": transforms.Compose([
                transforms.Resize((self.imgsz, self.imgsz)),  # resize
                transforms.CenterCrop((self.imgsz, self.imgsz)),  # 中心裁剪
                transforms.ToTensor(),  # 張量轉換
                transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
            ])
        }

        # 定義圖像生成器
        image_datasets = {x: datasets.ImageFolder(os.path.join(self.img_dir, x), data_transforms[x]) for x in
                          ['train', 'val']}
        # 得到訓練集和驗證集
        trainx = DataLoader(image_datasets["train"], batch_size=self.batch_size, shuffle=True, drop_last=True)
        valx = DataLoader(image_datasets["val"], batch_size=self.batch_size, shuffle=True, drop_last=True)

        b = image_datasets["train"].class_to_idx  # id和類别對應

        return trainx,valx,b
           

ImageFolder()這個函數,如果有人不清楚的,可以進行百度,傳回的b是類别映射表,如我的:

這個順序得記住,在後邊實際測試的時候會用到,也可以自己加點代碼将它寫入到檔案中。

4、學習率衰減政策

這裡的方案是先從初始值上升,然後在保持不動,然後在進行指數衰減,代碼如下:

# 學習率慢熱加下降
    def lrfn(self,num_epoch, optimzer):
        lr_start = 0.00001  # 初始值
        max_lr = 0.0004  # 最大值
        lr_up_epoch = 10  # 學習率上升10個epoch
        lr_sustain_epoch = 5  # 學習率保持不變
        lr_exp = .8  # 衰減因子
        if num_epoch < lr_up_epoch:  # 0-10個epoch學習率線性增加
            lr = (max_lr - lr_start) / lr_up_epoch * num_epoch + lr_start
        elif num_epoch < lr_up_epoch + lr_sustain_epoch:  # 學習率保持不變
            lr = max_lr
        else:  # 指數下降
            lr = (max_lr - lr_start) * lr_exp ** (num_epoch - lr_up_epoch - lr_sustain_epoch) + lr_start
        for param_group in optimzer.param_groups:
            param_group['lr'] = lr
        return optimzer
           

其中,參數 lr_sustain_epoch、max_lr、lr_up_epoch、 lr_sustain_epoch等均可以按照需求進行調整,非固定值

5、完整訓練代碼:

from torchvision import datasets,transforms
import torch
import torch.optim as optim
import torch.nn as nn
from torch.utils.data import DataLoader
from efficientnet_pytorch import EfficientNet
import os
import time
import argparse

device="cuda" if torch.cuda.is_available() else "cpu"

class Efficientnet_train():
    def __init__(self,opt):
        self.epochs=opt.epochs #訓練周期
        self.batch_size=opt.batch_size #batch_size
        self.class_num=opt.class_num #類别數
        self.imgsz=opt.imgsz #圖檔尺寸
        self.img_dir=opt.img_dir #圖檔路徑
        self.weights=opt.weights #模型路徑
        self.save_dir=opt.save_dir #儲存模型路徑
        self.lr=opt.lr #初始化學習率
        self.moment=opt.m #動量
        base_model = EfficientNet.from_name('efficientnet-b0') #記載模型,使用b幾的就改為b幾
        state_dict = torch.load(self.weights)
        base_model.load_state_dict(state_dict)
        # 修改全連接配接層
        num_ftrs = base_model._fc.in_features
        base_model._fc = nn.Linear(num_ftrs, self.class_num)
        self.model = base_model.to(device)
        # 交叉熵損失函數
        self.cross = nn.CrossEntropyLoss()
        # 優化器
        self.optimzer = optim.SGD((self.model.parameters()), lr=self.lr, momentum=self.moment, weight_decay=0.0004)

        #擷取處理後的資料集和類别映射表
        self.trainx,self.valx,self.b=self.process()
        print(self.b)
    def __call__(self):
        best_acc = 0
        self.model.train(True)
        for ech in range(self.epochs):
            optimzer1 = self.lrfn(ech, self.optimzer)

            print("----------Start Train Epoch %d----------" % (ech + 1))
            # 開始訓練
            run_loss = 0.0  # 損失
            run_correct = 0.0  # 準确率
            count = 0.0  # 分類正确的個數

            for i, data in enumerate(self.trainx):

                inputs, label = data
                inputs, label = inputs.to(device), label.to(device)

                # 訓練
                optimzer1.zero_grad()
                output = self.model(inputs)

                loss = self.cross(output, label)
                loss.backward()
                optimzer1.step()

                run_loss += loss.item()  # 損失累加
                _, pred = torch.max(output.data, 1)
                count += label.size(0)  # 求總共的訓練個數
                run_correct += pred.eq(label.data).cpu().sum()  # 截止目前預測正确的個數
                #每隔100個batch列印一次資訊,這裡列印的ACC是目前預測正确的個數/目前訓練過的的個數
                if (i+1)%100==0:
                    print('[Epoch:{}__iter:{}/{}] | Acc:{}'.format(ech + 1,i+1,len(self.trainx), run_correct/count))

            train_acc = run_correct / count
            # 每次訓完一批列印一次資訊
            print('Epoch:{} | Loss:{} | Acc:{}'.format(ech + 1, run_loss / len(self.trainx), train_acc))

            # 訓完一批次後進行驗證
            print("----------Waiting Test Epoch {}----------".format(ech + 1))
            with torch.no_grad():
                correct = 0.  # 預測正确的個數
                total = 0.  # 總個數
                for inputs, labels in self.valx:
                    inputs, labels = inputs.to(device), labels.to(device)
                    outputs = self.model(inputs)

                    # 擷取最高分的那個類的索引
                    _, pred = torch.max(outputs.data, 1)
                    total += labels.size(0)
                    correct += pred.eq(labels).cpu().sum()
                test_acc = correct / total
                print("批次%d的驗證集準确率" % (ech + 1), correct / total)
            if best_acc < test_acc:
                best_acc = test_acc
                start_time=(time.strftime("%m%d",time.localtime()))
                save_weight=self.save_dir+os.sep+start_time #儲存路徑
                os.makedirs(save_weight,exist_ok=True)
                torch.save(self.model, save_weight + os.sep + "best.pth")

    #資料處理
    def process(self):
        # 資料增強
        data_transforms = {
            'train': transforms.Compose([
                transforms.Resize((self.imgsz, self.imgsz)),  # resize
                transforms.CenterCrop((self.imgsz, self.imgsz)),  # 中心裁剪
                transforms.RandomRotation(10),  # 随機旋轉,旋轉範圍為【-10,10】
                transforms.RandomHorizontalFlip(p=0.2),  # 水準鏡像
                transforms.ToTensor(),  # 轉換為張量
                transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  # 标準化
            ]),
            "val": transforms.Compose([
                transforms.Resize((self.imgsz, self.imgsz)),  # resize
                transforms.CenterCrop((self.imgsz, self.imgsz)),  # 中心裁剪
                transforms.ToTensor(),  # 張量轉換
                transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
            ])
        }

        # 定義圖像生成器
        image_datasets = {x: datasets.ImageFolder(os.path.join(self.img_dir, x), data_transforms[x]) for x in
                          ['train', 'val']}
        # 得到訓練集和驗證集
        trainx = DataLoader(image_datasets["train"], batch_size=self.batch_size, shuffle=True, drop_last=True)
        valx = DataLoader(image_datasets["val"], batch_size=self.batch_size, shuffle=True, drop_last=True)

        b = image_datasets["train"].class_to_idx  # id和類别對應

        return trainx,valx,b


    # 學習率慢熱加下降
    def lrfn(self,num_epoch, optimzer):
        lr_start = 0.00001  # 初始值
        max_lr = 0.0004  # 最大值
        lr_up_epoch = 10  # 學習率上升10個epoch
        lr_sustain_epoch = 5  # 學習率保持不變
        lr_exp = .8  # 衰減因子
        if num_epoch < lr_up_epoch:  # 0-10個epoch學習率線性增加
            lr = (max_lr - lr_start) / lr_up_epoch * num_epoch + lr_start
        elif num_epoch < lr_up_epoch + lr_sustain_epoch:  # 學習率保持不變
            lr = max_lr
        else:  # 指數下降
            lr = (max_lr - lr_start) * lr_exp ** (num_epoch - lr_up_epoch - lr_sustain_epoch) + lr_start
        for param_group in optimzer.param_groups:
            param_group['lr'] = lr
        return optimzer
#參數設定
def parse_opt():
    parser=argparse.ArgumentParser()
    parser.add_argument("--weights",type=str,default="./models/efficientnet-b0-355c32eb.pth",help='initial weights path')#預訓練模型路徑
    parser.add_argument("--img-dir",type=str,default="",help="train image path") #資料集的路徑
    parser.add_argument("--imgsz",type=int,default=224,help="image size") #圖像尺寸
    parser.add_argument("--epochs",type=int,default=50,help="train epochs")#訓練批次
    parser.add_argument("--batch-size",type=int,default=4,help="train batch-size") #batch-size
    parser.add_argument("--class_num",type=int,default=2,help="class num") #類别數
    parser.add_argument("--lr",type=float,default=0.0001,help="Init lr") #學習率初始值
    parser.add_argument("--m",type=float,default=0.9,help="optimer momentum") #動量
    parser.add_argument("--save-dir",type=str,default="./weight",help="save models dir")#儲存模型路徑
    opt=parser.parse_known_args()[0]
    return opt

if __name__ == '__main__':
    opt=parse_opt()
    models=Efficientnet_train(opt)
    models()
           

隻需要将對應的參數設定為自己的就可

四、測試

1、完整測試代碼:

這裡,話不多說,直接上代碼

import torch
import os
import torchvision
import glob
from PIL import Image
import cv2
import argparse
device="cuda" if torch.cuda.is_available() else "cpu"
#參數設定
def parser_opt():
    parser=argparse.ArgumentParser()
    parser.add_argument("--test-dir",type=str,default=r"")
    parser.add_argument("--weights",type=str,default="",help="model path")
    parser.add_argument("--imgsz",type=int,default=224,help="test image size")
    opt=parser.parse_known_args()[0]
    return opt
#測試圖檔
class Test_model():
    def __init__(self,opt):
        self.imgsz=opt.imgsz #測試圖檔尺寸
        self.img_dir=opt.test_dir #測試圖檔路徑

        self.model=(torch.load(opt.weights)).to(device) #加載模型
        self.model.eval()
        self.class_name=[] #類别資訊
    def __call__(self):
        #圖像轉換
        data_transorform=torchvision.transforms.Compose([
                torchvision.transforms.Resize((224,224)),
                torchvision.transforms.CenterCrop((224,224)),
                torchvision.transforms.ToTensor(),
                torchvision.transforms.Normalize(mean=[0.485,0.456,0.406],std=[0.229,0.224,0.225])
            ])
        img_list=glob.glob(self.img_dir+os.sep+"*.jpg")
      
        for imgpath in img_list:
            img=cv2.imread(imgpath)
            new_img=self.expend_img(img) #補邊
            img=Image.fromarray(new_img)
            img=data_transorform(img) #轉換
            img=torch.reshape(img,(-1,3,self.imgsz,self.imgsz)).to(device) #次元轉換[B,C,H,W]
            pred=self.model(img)
            _,pred=torch.max(pred,1)
            outputs = self.class_name[pred]
            print("Image path:",imgpath," pred:",outputs)

    #補邊為正方形
    def expend_img(self,img,fill_pix=122):
        '''
        :param img: 圖檔資料
        :param fill_pix: 填充像素,預設為灰色,自行更改
        :return:
        '''
        h,w=img.shape[:2] #擷取圖像的寬高
        if h>=w: #左右填充
            padd_width=int(h-w)//2
            padd_h,padd_b,padd_l,padd_r=0,0,padd_width,padd_width #擷取上下左右四個方向需要填充的像素

        elif h<w: #上下填充
            padd_high=int(w-h)//2
            padd_h,padd_b,padd_l,padd_r=padd_high,padd_high,0,0

        new_img = cv2.copyMakeBorder(img, padd_h, padd_b, padd_l, padd_r, borderType=cv2.BORDER_CONSTANT,
                                     value=[fill_pix,fill_pix,fill_pix])
        return new_img

if __name__ == '__main__':
    opt=parser_opt()
    test_img=Test_model(opt)
    test_img()
           

這裡,依然對測試資料進行了補邊處理,相關參數在parser_opt()裡給定即可

注意:self.class_name=[] 裡的類别資訊寫為自己的,也就是3.3裡讓記錄的那個順序。

2、結果:

Pytorch版的Efficientnet訓練自己的資料集前言一、環境搭建二、資料準備三、訓練四、測試總結

這是我測試的某類别的100張圖檔,圖檔均為新圖,效果上看還可以,訓練50批次

總結

  以上就是本篇的内容,代碼中可以根據情況可更改的參數:圖像尺寸、資料集或者模型的路徑、優化器的選取及學習率的設定等等,以上參數都是針對我的電腦來設定的。

  代碼中有的地方可能自己有點誤解或者寫錯的地方,望各位大佬指正一下,謝謝。

繼續閱讀