參考:https://github.com/chenyuntc/pytorch-book/tree/v1.0/chapter6-實戰指南
希望大家直接到上面的網址去查看代碼,下面是本人的筆記
將上面地址的代碼下載到本地后進行操作
1.安裝依賴
(deeplearning) userdeMacBook-Pro:dogcat-6 user$ pip install -r requirements.txt ... Successfully built fire ipdb torchnet Installing collected packages: fire, tqdm, ipdb, torchnet Successfully installed fire-0.1.3 ipdb-0.12 torchnet-0.0.4 tqdm-4.31.1
在https://github.com/chenyuntc/pytorch-book/tree/v1.0/chapter6-實戰指南處將需要的數據下載下來,並放在./data/train/和./data/test1處
2.訓練
必須首先啟動visdom:
(deeplearning) userdeMBP:~ user$ python -m visdom.server It's Alive!
然后使用如下命令啟動訓練:
# 在gpu0上訓練,並把可視化結果保存在visdom 的classifier env上
python main.py train --train-data-root=./data/train --use-gpu --env=classifier
詳細的使用命令 可使用
python main.py help
3.測試
python main.py test --data-root=./data/test --batch-size=256 --load-path='checkpoints/squeezenet.pth'
4.代碼分析
在從事大多數深度學習研究時,程序都需要實現以下幾個功能:
- 模型定義
- 數據處理和加載
- 訓練模型(Train&Validate)
- 訓練過程的可視化
- 測試(Test/Inference)
1)該網絡介紹
Dogs vs. Cats是一個傳統的二分類問題,其訓練集包含25000張圖片,均放置在同一文件夾下,命名格式為<category>.<num>.jpg, 如cat.10000.jpg、dog.100.jpg,測試集包含12500張圖片,命名為<num>.jpg,如1000.jpg。參賽者需根據訓練集的圖片訓練模型,並在測試集上進行預測,輸出它是狗的概率。最后提交的csv文件如下,第一列是圖片的<num>,第二列是圖片為狗的概率。
id,label 10001,0.889 10002,0.01 ...
2)文件組織架構
前面提到過,程序主要包含以下功能:
- 模型定義
- 數據加載
- 訓練和測試
首先來看程序文件的組織結構:
├── checkpoints/ ├── data/ │ ├── __init__.py │ ├── dataset.py │ └── get_data.sh ├── models/ │ ├── __init__.py │ ├── AlexNet.py │ ├── BasicModule.py │ └── ResNet34.py └── utils/ │ ├── __init__.py │ └── visualize.py ├── config.py ├── main.py ├── requirements.txt ├── README.md
其中:
checkpoints/: 用於保存訓練好的模型,可使程序在異常退出后仍能重新載入模型,恢復訓練data/:數據相關操作,包括數據預處理、dataset實現等models/:模型定義,可以有多個模型,例如上面的AlexNet和ResNet34,一個模型對應一個文件utils/:可能用到的工具函數,在本次實驗中主要是封裝了可視化工具config.py:配置文件,所有可配置的變量都集中在此,並提供默認值main.py:主文件,訓練和測試程序的入口,可通過不同的命令來指定不同的操作和參數requirements.txt:程序依賴的第三方庫README.md:提供程序的必要說明
3)__init__.py文件
可以看到,幾乎每個文件夾下都有__init__.py,一個目錄如果包含了__init__.py 文件,那么它就變成了一個包(package)。
__init__.py可以為空,也可以定義包的屬性和方法,但其必須存在,其它程序才能從這個目錄中導入相應的模塊或函數。例如在data/文件夾下有__init__.py,則在main.py 中就可以from data.dataset import DogCat。而如果在__init__.py中寫入from .dataset import DogCat,則在main.py中就可以直接寫為:from data import DogCat,或者import data; dataset = data.DogCat,相比於from data.dataset import DogCat更加便捷。
4)數據加載
數據的相關處理主要保存在data/dataset.py中。關於數據加載的相關操作,在上一章中我們已經提到過,其基本原理就是使用Dataset提供數據集的封裝,再使用Dataloader實現數據並行加載。Kaggle提供的數據包括訓練集和測試集,而我們在實際使用中,還需專門從訓練集中取出一部分作為驗證集。
對於這三類數據集,其相應操作也不太一樣,而如果專門寫三個Dataset,則稍顯復雜和冗余,因此這里通過加一些判斷來區分。對於訓練集,我們希望做一些數據增強處理,如隨機裁剪、隨機翻轉、加噪聲等,而驗證集和測試集則不需要。下面看dataset.py的代碼:
# coding:utf8 import os from PIL import Image from torch.utils import data import numpy as np from torchvision import transforms as T class DogCat(data.Dataset): def __init__(self, root, transforms=None, train=True, test=False): """ 主要目標: 獲取所有圖片的地址,並根據訓練,驗證,測試划分數據 """ self.test = test imgs = [os.path.join(root, img) for img in os.listdir(root)] # test1: data/test1/8973.jpg # train: data/train/cat.10004.jpg if self.test: # 如果是進行測試,截取得到數據的數字標碼,如上面的8973,根據key=8973的值進行排序,返回對所有的圖片路徑進行排序后返回 imgs = sorted(imgs, key=lambda x: int(x.split('.')[-2].split('/')[-1])) else: #如果是進行訓練,截取得到數據的數字標識,如上面的10004,根據key=10004的值進行排序,返回對所有的圖片路徑進行排序后返回 imgs = sorted(imgs, key=lambda x: int(x.split('.')[-2])) imgs_num = len(imgs) #然后就可以得到數據的大小 if self.test: # 如果是進行測試 self.imgs = imgs elif train: #如果是進行訓練,使用前70%的數據 self.imgs = imgs[:int(0.7 * imgs_num)] else: #如果是進行驗證,使用后30%的數據 self.imgs = imgs[int(0.7 * imgs_num):] if transforms is None: # 數據轉換操作,測試驗證和訓練的數據轉換有所區別 # 對數據進行歸一化 normalize = T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 當是測試集和驗證集時進行的操作 if self.test or not train: self.transforms = T.Compose([ T.Resize(224), #重新設定大小 T.CenterCrop(224), #從圖片中心截取 T.ToTensor(), #轉成Tensor格式,大小范圍為[0,1] normalize #歸一化處理,大小范圍為[-1,1] ]) else: self.transforms = T.Compose([ T.Resize(256), T.RandomReSizedCrop(224), #從圖片的任何部位隨機截取224*224大小的圖 T.RandomHorizontalFlip(), #隨機水平翻轉給定的PIL.Image,翻轉概率為0.5 T.ToTensor(), normalize ]) def __getitem__(self, index): """ 一次返回一張圖片的數據 """ img_path = self.imgs[index] if self.test: #如果是測試,得到圖片路徑中的數字標識作為label label = int(self.imgs[index].split('.')[-2].split('/')[-1]) else: #如果是訓練,判斷圖片路徑中是貓狗來設定label,貓為0,狗為1 label = 1 if 'dog' in img_path.split('/')[-1] else 0 data = Image.open(img_path) #打開該路徑獲得數據 data = self.transforms(data) #然后對圖片數據進行transform return data, label #最后得到統一的圖片信息和label信息 def __len__(self): #圖片數據的大小 return len(self.imgs)
關於數據集使用的注意事項,在上一章中已經提到,將文件讀取等費時操作放在__getitem__函數中,利用多進程加速。避免一次性將所有圖片都讀進內存,不僅費時也會占用較大內存,而且不易進行數據增強等操作。
另外在這里,我們將訓練集中的30%作為驗證集,可用來檢查模型的訓練效果,避免過擬合。
在使用時,我們可通過dataloader加載數據。
#進行訓練,定義訓練數據和label集合train_dataset train_dataset = DogCat(opt.train_data_root, train=True) #開始進行數據的加載,將數據打亂(shuffle = True), #隨機將數據分批,一批有opt.batch_size個,並行處理,打開opt.num_workers個進程 trainloader = DataLoader(train_dataset, batch_size = opt.batch_size, shuffle = True, num_workers = opt.num_workers) #顯示得到的數據 for ii, (data, label) in enumerate(trainloader): train() #然后進行訓練
5)模型定義
模型的定義主要保存在models/目錄下,其中BasicModule是對nn.Module的簡易封裝,提供快速加載和保存模型的接口
#coding:utf8 import torch as t import time class BasicModule(t.nn.Module): """ 封裝了nn.Module,主要是提供了save和load兩個方法 """ def __init__(self): super(BasicModule,self).__init__() self.model_name=str(type(self))# 默認名字 def load(self, path): """ 可加載指定路徑的模型 """ self.load_state_dict(t.load(path)) def save(self, name=None): """ 保存模型,默認使用“模型名字+時間”作為文件名 """ if name is None: #存儲到文件夾checkpoints下面 prefix = 'checkpoints/' + self.model_name + '_' name = time.strftime(prefix + '%m%d_%H:%M:%S.pth') t.save(self.state_dict(), name) return name def get_optimizer(self, lr, weight_decay): #優化器 return t.optim.Adam(self.parameters(), lr=lr, weight_decay=weight_decay) class Flat(t.nn.Module): """ 把輸入reshape成(batch_size,dim_length) """ def __init__(self): super(Flat, self).__init__() #self.size = size def forward(self, x): return x.view(x.size(0), -1) #得到的是批數據的大小
在實際使用中,直接調用model.save()及model.load(opt.load_path)即可。
其它自定義模型一般繼承BasicModule,然后實現自己的模型。其中alexNet.py實現了AlexNet,resNet34實現了ResNet34。在models/__init__py中,代碼如下:
from .alexnet import AlexNet from .resnet34 import ResNet34 from .squeezenet import SqueezeNet # from torchvision.models import InceptinV3 # from torchvision.models import alexnet as AlexNet
alexnet.py為:
# coding:utf8 from torch import nn from .basic_module import BasicModule class AlexNet(BasicModule): """ code from torchvision/models/alexnet.py 結構參考 <https://arxiv.org/abs/1404.5997> """ def __init__(self, num_classes=2): super(AlexNet, self).__init__() self.model_name = 'alexnet' self.features = nn.Sequential( nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2), nn.ReLU(inplace=True), # inplace-選擇是否進行覆蓋運算 nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(64, 192, kernel_size=5, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(192, 384, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(256, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), ) self.classifier = nn.Sequential( nn.Dropout(), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplace=True), nn.Dropout(), nn.Linear(4096, 4096), nn.ReLU(inplace=True), nn.Linear(4096, num_classes), ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), 256 * 6 * 6) x = self.classifier(x) return x
resnet34.py為:
# coding:utf8 from .basic_module import BasicModule from torch import nn from torch.nn import functional as F class ResidualBlock(nn.Module): """ 實現子module: Residual Block """ def __init__(self, inchannel, outchannel, stride=1, shortcut=None): super(ResidualBlock, self).__init__() self.left = nn.Sequential( nn.Conv2d(inchannel, outchannel, 3, stride, 1, bias=False), nn.BatchNorm2d(outchannel), nn.ReLU(inplace=True), nn.Conv2d(outchannel, outchannel, 3, 1, 1, bias=False), nn.BatchNorm2d(outchannel)) self.right = shortcut def forward(self, x): out = self.left(x) residual = x if self.right is None else self.right(x) out += residual return F.relu(out) class ResNet34(BasicModule): """ 實現主module:ResNet34 ResNet34包含多個layer,每個layer又包含多個Residual block 用子module來實現Residual block,用_make_layer函數來實現layer """ def __init__(self, num_classes=2): super(ResNet34, self).__init__() self.model_name = 'resnet34' # 前幾層: 圖像轉換 self.pre = nn.Sequential( nn.Conv2d(3, 64, 7, 2, 3, bias=False), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(3, 2, 1)) # 重復的layer,分別有3,4,6,3個residual block self.layer1 = self._make_layer(64, 128, 3) self.layer2 = self._make_layer(128, 256, 4, stride=2) self.layer3 = self._make_layer(256, 512, 6, stride=2) self.layer4 = self._make_layer(512, 512, 3, stride=2) # 分類用的全連接 self.fc = nn.Linear(512, num_classes) def _make_layer(self, inchannel, outchannel, block_num, stride=1): """ 構建layer,包含多個residual block """ shortcut = nn.Sequential( nn.Conv2d(inchannel, outchannel, 1, stride, bias=False), nn.BatchNorm2d(outchannel)) layers = [] layers.append(ResidualBlock(inchannel, outchannel, stride, shortcut)) for i in range(1, block_num): layers.append(ResidualBlock(outchannel, outchannel)) return nn.Sequential(*layers) def forward(self, x): x = self.pre(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = F.avg_pool2d(x, 7) x = x.view(x.size(0), -1) return self.fc(x)
這樣在主函數中就可以寫成:
from models import AlexNet 或 import models model = models.AlexNet() 或 import models model = getattr('models', 'AlexNet')()
其中最后一種寫法最為關鍵,這意味着我們可以通過字符串直接指定使用的模型,而不必使用判斷語句,也不必在每次新增加模型后都修改代碼。新增模型后只需要在models/__init__.py中加上from .new_module import new_module即可。
其它關於模型定義的注意事項,在上一章中已詳細講解,這里就不再贅述,總結起來就是:
- 盡量使用
nn.Sequential(比如AlexNet) - 將經常使用的結構封裝成子Module(比如GoogLeNet的Inception結構,ResNet的Residual Block結構)
- 將重復且有規律性的結構,用函數生成(比如VGG的多種變體,ResNet多種變體都是由多個重復卷積層組成)
6)工具函數——實現可視化visdom
在項目中,我們可能會用到一些helper方法,這些方法可以統一放在utils/文件夾下,需要使用時再引入。在本例中主要是封裝了可視化工具visdom的一些操作,其代碼如下,在本次實驗中只會用到plot方法,用來統計損失信息。
# coding:utf8 import visdom import time import numpy as np class Visualizer(object): """ 封裝了visdom的基本操作,但是你仍然可以通過`self.vis.function` 調用原生的visdom接口 """ def __init__(self, env='default', **kwargs): self.vis = visdom.Visdom(env=env,use_incoming_socket=False, **kwargs) # 畫的第幾個數,相當於橫座標 # 保存(’loss',23) 即loss的第23個點 self.index = {} self.log_text = '' def reinit(self, env='default', **kwargs): """ 修改visdom的配置 """ self.vis = visdom.Visdom(env=env, **kwargs) return self def plot_many(self, d): """ 一次plot多個 @params d: dict (name,value) i.e. ('loss',0.11) """ for k, v in d.items(): self.plot(k, v) def img_many(self, d): for k, v in d.items(): self.img(k, v) def plot(self, name, y, **kwargs): """ self.plot('loss',1.00) """ x = self.index.get(name, 0) self.vis.line(Y=np.array([y]), X=np.array([x]), win=name, opts=dict(title=name), update=None if x == 0 else 'append', **kwargs ) self.index[name] = x + 1 def img(self, name, img_, **kwargs): """ self.img('input_img',t.Tensor(64,64)) self.img('input_imgs',t.Tensor(3,64,64)) self.img('input_imgs',t.Tensor(100,1,64,64)) self.img('input_imgs',t.Tensor(100,3,64,64),nrows=10) !!!don‘t ~~self.img('input_imgs',t.Tensor(100,64,64),nrows=10)~~!!! """ self.vis.images(img_.cpu().numpy(), win=name, opts=dict(title=name), **kwargs ) def log(self, info, win='log_text'): """ self.log({'loss':1,'lr':0.0001}) """ self.log_text += ('[{time}] {info} <br>'.format( time=time.strftime('%m%d_%H%M%S'), info=info)) self.vis.text(self.log_text, win) def __getattr__(self, name): return getattr(self.vis, name)
7)配置文件
在模型定義、數據處理和訓練等過程都有很多變量,這些變量應提供默認值,並統一放置在配置文件中,這樣在后期調試、修改代碼或遷移程序時會比較方便,在這里我們將所有可配置項放在config.py中。
# coding:utf8 import warnings import torch as t class DefaultConfig(object): env = 'default' # visdom 環境 vis_port =8097 # visdom 端口 model = 'SqueezeNet' # 使用的模型,名字必須與models/__init__.py中的名字一致 train_data_root = './data/train/' # 訓練集存放路徑 test_data_root = './data/test1' # 測試集存放路徑 load_model_path = None # 加載預訓練的模型的路徑,為None代表不加載 batch_size = 32 # batch size use_gpu = True # user GPU or not num_workers = 4 # how many workers for loading data print_freq = 20 # print info every N batch debug_file = '/tmp/debug' # if os.path.exists(debug_file): enter ipdb result_file = 'result.csv' max_epoch = 10 lr = 0.001 # initial learning rate lr_decay = 0.5 # when val_loss increase, lr = lr*lr_decay weight_decay = 0e-5 # 損失函數
可配置的參數主要包括:
- 數據集參數(文件路徑、batch_size等)
- 訓練參數(學習率、訓練epoch等)
- 模型參數
這樣我們在程序中就可以這樣使用:
import models from config import DefaultConfig opt = DefaultConfig() lr = opt.lr model = getattr(models, opt.model) dataset = DogCat(opt.train_data_root)
這些都只是默認參數,在這里還提供了更新函數,根據字典更新配置參數:
def _parse(self, kwargs): """ 根據字典kwargs 更新 config參數 """ # 更新配置參數 for k, v in kwargs.items(): if not hasattr(self, k): # 警告還是報錯,取決於你個人的喜好 warnings.warn("Warning: opt has not attribut %s" % k) setattr(self, k, v) opt.device =t.device('cuda') if opt.use_gpu else t.device('cpu') # 打印配置信息 print('user config:') for k, v in self.__class__.__dict__.items(): if not k.startswith('_'): print(k, getattr(self, k))
這樣我們在實際使用時,並不需要每次都修改config.py,只需要通過命令行傳入所需參數,覆蓋默認配置即可。
例如:
opt = DefaultConfig() new_config = {'lr':0.1,'use_gpu':False} opt.parse(new_config) opt.lr == 0.1
8)main.py
1>fire
2017年3月谷歌開源的一個命令行工具fire^3 ,通過pip install fire即可安裝。下面來看看fire的基礎用法,假設example.py文件內容如下:
import fire def add(x, y): return x + y def mul(**kwargs): a = kwargs['a'] b = kwargs['b'] return a * b if __name__ == '__main__': fire.Fire()
那么我們可以使用:
python example.py add 1 2 # 執行add(1, 2) python example.py mul --a=1 --b=2 # 執行mul(a=1, b=2), kwargs={'a':1, 'b':2} python example.py add --x=1 --y==2 # 執行add(x=1, y=2)
可見,只要在程序中運行fire.Fire(),即可使用命令行參數python file <function> [args,] {--kwargs,}。fire還支持更多的高級功能,具體請參考官方指南^4 。
2>main.py
在主程序main.py中,主要包含四個函數,其中三個需要命令行執行,main.py的代碼組織結構如下:
def train(**kwargs): """ 訓練 """ pass def val(model, dataloader): """ 計算模型在驗證集上的准確率等信息,用以輔助訓練 """ pass def test(**kwargs): """ 測試(inference) """ pass def help(): """ 打印幫助的信息 """ print('help') if __name__=='__main__': import fire fire.Fire()
根據fire的使用方法,可通過python main.py <function> --args=xx的方式來執行訓練或者測試。
1》訓練
訓練的主要步驟如下:
- 定義網絡
- 定義數據
- 定義損失函數和優化器
- 計算重要指標
- 開始訓練
- 訓練網絡
- 可視化各種指標
- 計算在驗證集上的指標
訓練函數的代碼如下:
def train(**kwargs): #根據傳入的參數更改配置信息 opt._parse(kwargs) vis = Visualizer(opt.env,port = opt.vis_port) # step1: configure model配置模型 model = getattr(models, opt.model)() #默認使用模型SqueezeNet if opt.load_model_path: # 加載預訓練的模型的路徑 model.load(opt.load_model_path) model.to(opt.device) #使用的是GPU還是CPU # step2: data加載數據 train_data = DogCat(opt.train_data_root,train=True) #訓練數據 val_data = DogCat(opt.train_data_root,train=False) #測試數據 train_dataloader = DataLoader(train_data,opt.batch_size, shuffle=True,num_workers=opt.num_workers) val_dataloader = DataLoader(val_data,opt.batch_size, shuffle=False,num_workers=opt.num_workers) # step3: criterion and optimizer ,損失函數和優化器 criterion = t.nn.CrossEntropyLoss() lr = opt.lr optimizer = model.get_optimizer(lr, opt.weight_decay) # step4: meters,統計指標:平滑處理之后的損失,還有混淆矩陣 loss_meter = meter.AverageValueMeter() #能夠計算所有數的平均值和標准差,用來統計一個epoch中損失的平均值 confusion_matrix = meter.ConfusionMeter(2) #用來統計分類問題中的分類情況,是一個比准確率更詳細的統計指標 previous_loss = 1e10 # train,開始訓練 for epoch in range(opt.max_epoch): #迭代次數 loss_meter.reset() confusion_matrix.reset() for ii,(data,label) in tqdm(enumerate(train_dataloader)): # train model input = data.to(opt.device) target = label.to(opt.device) optimizer.zero_grad() score = model(input) loss = criterion(score,target) loss.backward() optimizer.step() # meters update and visualize,# 更新統計指標以及可視化 loss_meter.add(loss.item()) # detach 一下更安全保險 confusion_matrix.add(score.detach(), target.detach()) if (ii + 1)%opt.print_freq == 0: vis.plot('loss', loss_meter.value()[0]) # 進入debug模式 if os.path.exists(opt.debug_file): import ipdb; ipdb.set_trace() model.save() # validate and visualize,計算驗證集上的指標及可視化 val_cm,val_accuracy = val(model,val_dataloader) vis.plot('val_accuracy',val_accuracy)
#loss_meter.value()返回的是loss列表的mean,std平均數和標准差,[0]則得到其平均數 vis.log("epoch:{epoch},lr:{lr},loss:{loss},train_cm:{train_cm},val_cm:{val_cm}".format( epoch = epoch,loss = loss_meter.value()[0],val_cm = str(val_cm.value()),train_cm=str(confusion_matrix.value()),lr=lr)) # update learning rate,如果損失不再下降,則降低學習率 if loss_meter.value()[0] > previous_loss: lr = lr * opt.lr_decay # 第二種降低學習率的方法:不會有moment等信息的丟失 for param_group in optimizer.param_groups: param_group['lr'] = lr previous_loss = loss_meter.value()[0]
這里用到了PyTorchNet^5里面的一個工具: meter。meter提供了一些輕量級的工具,用於幫助用戶快速統計訓練過程中的一些指標。AverageValueMeter能夠計算所有數的平均值和標准差,這里用來統計一個epoch中損失的平均值。confusionmeter用來統計分類問題中的分類情況,是一個比准確率更詳細的統計指標。例如對於表格6-1,共有50張狗的圖片,其中有35張被正確分類成了狗,還有15張被誤判成貓;共有100張貓的圖片,其中有91張被正確判為了貓,剩下9張被誤判成狗。相比於准確率等統計信息,混淆矩陣更能體現分類的結果,尤其是在樣本比例不均衡的情況下。
表6-1 混淆矩陣
| 樣本 | 判為狗 | 判為貓 |
|---|---|---|
| 實際是狗 | 35 | 15 |
| 實際是貓 | 9 | 91 |
PyTorchNet從TorchNet^6遷移而來,提供了很多有用的工具,但其目前開發和文檔都還不是很完善,本書不做過多的講解。
2》驗證
驗證相對來說比較簡單,但要注意需將模型置於驗證模式(model.eval()),驗證完成后還需要將其置回為訓練模式(model.train()),這兩句代碼會影響BatchNorm和Dropout等層的運行模式。驗證模型准確率的代碼如下。
@t.no_grad() def val(model,dataloader): """ 計算模型在驗證集上的准確率等信息 """ # 把模型設為驗證模式 model.eval() confusion_matrix = meter.ConfusionMeter(2) for ii, (val_input, label) in tqdm(enumerate(dataloader)): val_input = val_input.to(opt.device) score = model(val_input) confusion_matrix.add(score.detach().squeeze(), label.type(t.LongTensor)) # 把模型恢復為訓練模式,要養成習慣,不實用驗證模式后要將其調整回來 model.train() #計算准確率 cm_value = confusion_matrix.value() accuracy = 100. * (cm_value[0][0] + cm_value[1][1]) / (cm_value.sum()) return confusion_matrix, accuracy
3》測試:
測試時,需要計算每個樣本屬於狗的概率,並將結果保存成csv文件。測試的代碼與驗證比較相似,但需要自己加載模型和數據。
@t.no_grad() # pytorch>=0.5 def test(**kwargs): opt._parse(kwargs) #根據輸入更改相應配置的值 # configure model,設置使用的模型,並將其設置為驗證模式 model = getattr(models, opt.model)().eval() if opt.load_model_path: model.load(opt.load_model_path) model.to(opt.device) # data train_data = DogCat(opt.test_data_root,test=True) test_dataloader = DataLoader(train_data,batch_size=opt.batch_size,shuffle=False,num_workers=opt.num_workers) results = [] for ii,(data,path) in tqdm(enumerate(test_dataloader)): input = data.to(opt.device) score = model(input) probability = t.nn.functional.softmax(score,dim=1)[:,0].detach().tolist() # label = score.max(dim = 1)[1].detach().tolist() #將批數據中圖像數據的路徑和其可能性結果結合在一起,得到批數據的結果 batch_results = [(path_.item(),probability_) for path_,probability_ in zip(path,probability) ] #將這一批數據結果存儲在總結果中 results += batch_results write_csv(results,opt.result_file) return results def write_csv(results,file_name): #將得到的結果寫到file_name文件中,是一個.csv文件 import csv with open(file_name,'w') as f: writer = csv.writer(f) writer.writerow(['id','label']) #設置行標簽 writer.writerows(results) #然后將數據寫入
4》幫助函數
為了方便他人使用, 程序中還應當提供一個幫助函數,用於說明函數是如何使用。程序的命令行接口中有眾多參數,如果手動用字符串表示不僅復雜,而且后期修改config文件時,還需要修改對應的幫助信息,十分不便。這里使用了Python標准庫中的inspect方法,可以自動獲取config的源代碼。help的代碼如下:
def help(): """ 打印幫助的信息: python file.py help """ print(""" usage : python file.py <function> [--args=value] <function> := train | test | help example: python {0} train --env='env0701' --lr=0.01 python {0} test --dataset='path/to/dataset/root/' python {0} help avaiable args:""".format(__file__)) from inspect import getsource source = (getsource(opt.__class__)) print(source)
當用戶執行python main.py help的時候,會打印如下幫助信息:
(deeplearning) userdeMacBook-Pro:dogcat-6 user$ python main.py help usage : python file.py <function> [--args=value] <function> := train | test | help example: python main.py train --env='env0701' --lr=0.01 python main.py test --dataset='path/to/dataset/root/' python main.py help avaiable args: class DefaultConfig(object): env = 'default' # visdom 環境 vis_port =8097 # visdom 端口 ...
9)使用
正如help函數的打印信息所述,可以通過命令行參數指定變量名.下面是三個使用例子,fire會將包含-的命令行參數自動轉層下划線_,也會將非數值的值轉成字符串。所以--train-data-root=data/train和--train_data_root='data/train'是等價的。
# 訓練模型 python main.py train --train-data-root=data/train/ --lr=0.005 --batch-size=32 --model='ResNet34' --max-epoch = 20 # 測試模型 python main.py test --test-data-root=data/test1 --load-model-path='checkpoints/resnet34_00:23:05.pth' --batch-size=128 --model='ResNet34' --num-workers=12 # 打印幫助信息 python main.py help
10)本地運行:
1》訓練命令為:
python main.py train --env=main --train-data-root=./data/train/ --lr=0.005 --batch-size=32 --model='ResNet34' --max-epoch=100
指明visdom可視化工具的env為main,訓練數據在文件夾./data/train/下,學習率設置為0.005,批處理大小為32,使用的模型是ResNet34,循環輪數是100次,返回的結果為:

可見訓練的效果不錯,loss圖表示損失在減少,val_accuracy圖表示驗證集的准確率在上升
此時checkpoint中會生成100次輪詢生成的參數值,以.pth文件結尾的,可以隨機選擇60次輪訓后訓練生成的參數值來進行測試,然后查看結果看測試的效果
⚠️這里因為linux上沒安裝可視化工具,所以我是在Linux服務器上訓練,然后從~/.visdom文件夾中將main.json下載到本地的~/.visdom文件夾中進行查看
2》測試命令為:
python main.py test --test-data-root=./data/test1 --load-model-path='checkpoints/resnet34_0408_12:02:48.pth' --batch-size=128 --model='ResNet34' --num-workers=12 --result-file=result1.csv
test-data-root指明測試集所在的文件夾,load-model-path指明使用的是那個訓練后的參數,result-file說明將測試結果存儲在result1.csv文件中
