Pytorch學習--程式設計實戰:貓和狗二分類
Pytorch學習系列(一)至(四)均摘自《深度學習框架PyTorch入門與實踐》陳雲
目錄:
1.程式的主要功能
2.檔案組織架構
3. 關於`__init__.py`
4.資料處理
5.模型定義
6.工具函式
7.配置檔案
8.main.py
9.使用
1.程式的主要功能:
模型定義
資料載入
訓練和測試
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:
imgs = sorted(imgs,key=lambda x:int(x.split('.')[-2].split('/')[-1]))
else:
imgs = sorted(imgs,key=lambda x:int(x.split('.')[-2]))
imgs_num = len(imgs)
if self.test:
self.imgs = imgs
elif train:
self.imgs = imgs[:int(0.7*imgs_num)]
else :
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.Scale(224),
T.CenterCrop(224),
T.ToTensor(),
normalize
])
else :
self.transforms = T.Compose([
T.Scale(256),
T.RandomSizedCrop(224),
T.RandomHorizontalFlip(),
T.ToTensor(),
normalize
])
def __getitem__(self,index):
'''
一次返回一張圖片的資料
'''
img_path = self.imgs[index]
if self.test: label = int(self.imgs[index].split('.')[-2].split('/')[-1])
else: label = 1 if 'dog' in img_path.split('/')[-1] else 0
data = Image.open(img_path)
data = self.transforms(data)
return data, label
def __len__(self):
return len(self.imgs)
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:
prefix = 'checkpoints/' + self.model_name + '_'
name = time.strftime(prefix + '%m%d_%H:%M:%S.pth')
t.save(self.state_dict(), name)
return name
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)
6.工具函式
在專案中,我們可能會用到一些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, **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
class DefaultConfig(object):
env = 'default' # visdom 環境
model = 'ResNet34' # 使用的模型,名字必須與models/__init__.py中的名字一致
train_data_root = './data/train/' # 訓練集存放路徑
test_data_root = './data/test1' # 測試集存放路徑
load_model_path = 'checkpoints/model.pth' # 載入預訓練的模型的路徑,為None代表不載入
batch_size = 128 # 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.1 # initial learning rate
lr_decay = 0.95 # when val_loss increase, lr = lr*lr_decay
weight_decay = 1e-4 # 損失函式
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)
print('user config:')
for k,v in self.__class__.__dict__.items():
if not k.startswith('__'):
print(k,getattr(self,k))
DefaultConfig.parse = parse
opt =DefaultConfig()
# opt.parse = parse
8.main.py
在講解主程式`main.py`之前,我們先來看看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)
在主程式`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()
訓練
訓練的主要步驟如下:
- 定義網路
- 定義資料
- 定義損失函式和優化器
- 計算重要指標
- 開始訓練
- 訓練網路
- 視覺化各種指標
- 計算在驗證集上的指標
def train(**kwargs):
opt.parse(kwargs)
vis = Visualizer(opt.env)
# step1: configure model
model = getattr(models, opt.model)()
if opt.load_model_path:
model.load(opt.load_model_path)
if opt.use_gpu: model.cuda()
# 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 = t.optim.Adam(model.parameters(),lr = lr,weight_decay = opt.weight_decay)
# step4: meters
loss_meter = meter.AverageValueMeter()
confusion_matrix = meter.ConfusionMeter(2)
previous_loss = 1e100
# train
for epoch in range(opt.max_epoch):
loss_meter.reset()
confusion_matrix.reset()
for ii,(data,label) in enumerate(train_dataloader):
# train model
input = Variable(data)
target = Variable(label)
if opt.use_gpu:
input = input.cuda()
target = target.cuda()
optimizer.zero_grad()
score = model(input)
loss = criterion(score,target)
loss.backward()
optimizer.step()
# meters update and visualize
loss_meter.add(loss.data[0])
confusion_matrix.add(score.data, target.data)
if ii%opt.print_freq==opt.print_freq-1:
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)
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]
驗證
驗證相對來說比較簡單,但要注意需將模型置於驗證模式(`model.eval()`),驗證完成後還需要將其置回為訓練模式(`model.train()`),這兩句程式碼會影響`BatchNorm`和`Dropout`等層的執行模式。驗證模型準確率的程式碼如下。
def val(model,dataloader):
'''
計算模型在驗證集上的準確率等資訊
'''
model.eval()
confusion_matrix = meter.ConfusionMeter(2)
for ii, data in enumerate(dataloader):
input, label = data
val_input = Variable(input, volatile=True)
val_label = Variable(label.type(t.LongTensor), volatile=True)
if opt.use_gpu:
val_input = val_input.cuda()
val_label = val_label.cuda()
score = model(val_input)
confusion_matrix.add(score.data.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
測試
測試時,需要計算每個樣本屬於狗的概率,並將結果儲存成csv檔案。測試的程式碼與驗證比較相似,但需要自己載入模型和資料。
def test(**kwargs):
opt.parse(kwargs)
import ipdb;
ipdb.set_trace()
# configure model
model = getattr(models, opt.model)().eval()
if opt.load_model_path:
model.load(opt.load_model_path)
if opt.use_gpu: model.cuda()
# 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 enumerate(test_dataloader):
input = t.autograd.Variable(data,volatile = True)
if opt.use_gpu: input = input.cuda()
score = model(input)
probability = t.nn.functional.softmax(score)[:,0].data.tolist()
# label = score.max(dim = 1)[1].data.tolist()
batch_results = [(path_,probability_) for path_,probability_ in zip(path,probability) ]
results += batch_results
write_csv(results,opt.result_file)
return results
幫助函式
為了方便他人使用, 程式中還應當提供一個幫助函式,用於說明函式是如何使用。程式的命令列介面中有眾多引數,如果手動用字串表示不僅複雜,而且後期修改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)
9.使用
正如`help`函式的列印資訊所述,可以通過命令列引數指定變數名.下面是三個使用例子,fire會將包含`-`的命令列引數自動轉層下劃線`_`,也會將非數值的值轉成字串。所以`--train-data-root=data/train`和`--train_data_root='data/train'`是等價的。
```
# 訓練模型
python main.py train
--train-data-root=data/train/
--load-model-path='checkpoints/resnet34_16:53:00.pth'
--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
---------------------
作者:尋找如意
來源:CSDN
原文:https://blog.csdn.net/qq_34447388/article/details/79541824
版權宣告:本文為博主原創文章,轉載請附上博文連結!