0802-編程實戰_貓和狗二分類_深度學習專案架構
目錄- 一、比賽介紹
- 二、資料加載
- 三、模型定義
- 四、工具函式
- 五、組態檔
- 六、main.py
- 6.1 命令列工具 fire
- 6.2 main.py的代碼組織結構
- 6.3 訓練
- 6.3.1 torchnet 中的 meter
- 6.4 驗證
- 6.5 測驗
- 6.6 幫助函式
- 七、使用
- 八、爭議
pytorch完整教程目錄:https://www.cnblogs.com/nickchen121/p/14662511.html
一、比賽介紹
接下來我們將通過 pytorch 完成 Kaggle 上的經典比賽:Dogs vs. Cats
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 |
| ... | ... |
二、資料加載
資料的相關處理主要保存在 data/dataset.py 中,
關于資料加載,之前提過,基本原理就是先使用 Dataset 封裝資料集,再使用 Dataloader 實作資料并行加載,
Kaggle 提供的資料包括訓練集和測驗集,但是在我們使用的時候,還需要從訓練集中抽取一部分作為驗證集,
對于上述所說的三個資料集,雖然它們的相應操作不太一樣,但是如果專門寫出三個 Dataset,則會顯得復雜并冗余,因此在這里通過添加一些判斷來區分三者,比如我們希望對訓練集做一些資料增強處理,如隨機裁剪、隨機翻轉、加噪聲等,但是對于驗證集和測驗集則不需要,
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Coding by https://www.cnblogs.com/nickchen121/
# Datatime:2021/5/3 10:15
# Filename:dataset.py
# Toolby: PyCharm
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])) # 切割出 8973
else:
imgs = sorted(imgs,
key=lambda x: int(x.split('.')[-2])) # 切割出 10004
# 劃分訓練、驗證集,驗證:訓練 = 3:7
imgs_num = len(imgs)
if self.test:
self.imgs = imgs
elif train:
self.imgs = imgs[:int(0.7 * imgs_num)] # 訓練集來自資料集的前 70%
else:
self.imgs = imgs[int(0.7 * imgs_num):]
# 資料轉換操作,測驗驗證和訓練的資料轉換有所區別
if transforms is None:
# Normalize給定均值:(R,G,B) 方差:(R,G,B),將會把Tensor正則化
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), # 讓圖片統一大小為:224*224
T.CenterCrop(224), # 中心切割
T.ToTensor(),
normalize
])
# 訓練集
else:
self.transforms = T.Compose([
T.Scale(256), # 讓圖片統一大小為:256*256
T.RandomSizedCrop(224), # 隨機切割圖片后,resize成給定的大小 224*224
T.RandomHorizontalFlip(), # 一半的概率翻轉,一半的概率不翻轉
T.ToTensor(),
normalize
])
def __getitem__(self, index):
"""
回傳一張圖片的資料
如果是測驗集,沒有圖片 id,如 8973.jpg 回傳 8973
test1: data/test1/8973.jpg
train: data/train/cat.10004.jpg
"""
img_path = self.imgs[index]
if self.test:
label = self.imgs[index].split('.')[-2] # type:str # 切割出 8973.jpg
label = int(label.split('/')[-1]) # 切割出 8973
else:
label = 1 if 'dog' in img_path.split(
'/')[-1] else 0 # 切割出 cat.10004.jpg,通過判斷對圖片增加標簽
data = https://www.cnblogs.com/nickchen121/archive/2021/05/06/Image.open(img_path)
data = self.transforms(data) # 對圖片進行處理
return data, label
def __len__(self):"""
回傳資料集中所有圖片的個數
"""
return len(self.imgs)
# train_dataset = DogCat(opt.train_data_root, train=True) # opt 是未來會講到的配置物件
# trainloader = DataLoader(train_dataset,
# batch_size=opt.batch_size,
# shuffle=True,
# num_workers=opt.num_workers)
#
# for ii, (data, label) in enumerate(trainloader):
# train()
上述代碼中我們需要注意三個點:
- 把檔案讀取等費時操作放在
__getitem__函式中,利用多行程加速 - 一次性把所有圖片讀進記憶體,不僅費時也會占用較大記憶體,而且不方便進行資料增強操作
- 訓練集中的 30% 作為驗證集,可以用來檢查模型的訓練效果,避免過擬合
三、模型定義
模型的定義主要保存在 models 目錄下,其中 BasicModule 是對 nn.Module 的簡易封裝,提供快速加載和保存模型的介面,
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Coding by https://www.cnblogs.com/nickchen121/
# Datatime:2021/5/3 10:22
# Filename:BasicModule.py
# Toolby: PyCharm
import time
import torch as t
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):
"""
可加載指定路徑的模型
:param path:
:return:
"""
self.load_state_dict(t.load(path))
def save(self, name=None):
"""
保存模型,默認使用“模型名字+時間”作為檔案名,
如 AlexNet_0710_23:57:29.pth
:param name:
:return:
"""
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
在實際使用中,直接呼叫 model.save() 以及 model.load(opt.load_path) 即可,
其他自定義模型一般繼承 BasicModule,然后實作自己的模型,由于實作了 AlexNet 和 ResNet34,在 models/__init__.py 中,可以寫下下述代碼:
from .AlexNet import AlexNet
from .ResNet34 import ResNet34
這樣主函式中就可以寫:
from models import AlexNet
# 或
import models
model = models.AlexNet()
# 或
import models
model = getattr('models', 'AlexNet')()
上述在主函式中的代碼中,其中最后寫法最關鍵,這樣意味著我們可以通過字串直接指定使用的模型,而不需要使用判斷陳述句,同時也不需要在每次新增加模型后都修改代碼,
但是最好的方法,就是在新增模型后需要在 models.__init__.py 中加上 from .new_module import new_module,避免使用第一種方法時報錯,或者避免使用 model = getattr('models', 'AlexNet')() 時找不到該物件,
最后,在模型定義的時候,需要注意以下三點:
- 盡量使用
nn.Sequenetial - 將經常使用的結構封裝為子 module
- 將重復且有規律性的結構用函式生成
四、工具函式
在專案中,我們可能需要用到一些經常使用的方法,這些方法可以統一放入到 utils 檔案夾中,需要時再匯入,
在這個專案中,主要封裝了可視化工具 visdom 的一些操作,
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Coding by https://www.cnblogs.com/nickchen121/
# Datatime:2021/5/3 10:23
# Filename:visualize.py
# Toolby: PyCharm
import visdom
import time
import numpy as np
class Visualizer(object):
"""
封裝了 visdom 的基本操作,但仍然可以通過 `self.vis.function`
或者 `self.function` 呼叫原生的 visdom 介面
例如:
self.text('hello visdom')
self.histogram(t.randn(1000))
self.line(t.arange(0, 10), t.arange(1, 11))
"""
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 的配置
:param env:
:param kwargs:
:return:
"""
self.vis = visdom.Visdom(env=env, **kwargs)
return self
def plot_many(self, d: dict):
"""
一次 plot 多個
:param d: dict(name, value) i.e. ('loss', 0.11)
:return:
"""
for k, v in d.items():
self.plot(k, v)
def img_many(self, d: dict):
"""
處理多張圖片
:param d:
:return:
"""
for k, v in d.items():
self.img(k, v)
def plot(self, name, y, **kwargs):
"""
self.plot('loss', 1.00)
:param name:
:param y:
:param kwargs:
:return:
"""
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_img', t.Tensor(100, 1, 64, 64))
self.img('input_imgs', t.Tensor(100, 3, 64, 64), nrows=10)
:param name:
:param img_:
:param kwargs:
:return:
"""
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}
:param info:
:param win:
:return:
"""
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):
"""
自定義的 plot,image,log,plot_many 等除外
self.function 等價于 self.vis.function
:param name:
:return:
"""
return getattr(self.vis, name)
五、組態檔
在模型定義、資料處理和訓練程序中會產生許多變數,這些變數應該提供默認值,并且統一放在組態檔中,如此做的話,在后期除錯、修改代碼的時候會方便很多,在這里,我們把所有課配置項都放在 config.py 中,
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Coding by https://www.cnblogs.com/nickchen121/
# Datatime:2021/5/3 10:20
# Filename:config.py
# Toolby: PyCharm
class DefaultConfig(object):
env = 'default'
model = 'AlexNet' # 使用的模型,名字必須與 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 = False # use GPU or not
num_workers = 4 # num of 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 # 損失函式
從上述代碼中可以看出可配置的引數主要包括以下三類:
- 資料集引數(檔案路徑、batch_size 等)
- 訓練引數(學習率、訓練 epoch 等)
- 模型引數
定義好了上述配置引數后,可以在程式中這樣使用配置引數:
import models
from config import DefaultConfig
opt = DefaultConfig()
lr = opt.lr
model = getattr(models, opt.model)
dataset = DogCat(opt.traini_data_error)
上述所說的都是默認引數,在默認配置類中,我們還可以提供一個更新函式,根據字典更新配置引數,
def parse(self, kwargs: dict):
"""
根據字典 kwargs 更新 config 引數
:param kwargs:
:return:
"""
# 更新配置引數
for k, v in kwargs.items():
if not hasattr(self, k):
warnings.warn(f"Warning: opt has not attribut {k}")
setattr(self, k, v)
# 列印配置資訊
print('user config: ')
for k, v in self.__class__.__dict__.items(): # type:str
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
六、main.py
6.1 命令列工具 fire
在講解 main 檔案前,我們先熟悉一個我們可能可以用到的一個命令列工具 fire,可以通過 pip install fire 安裝,下面介紹下 fire 的基礎用法,假設 example.py 檔案代碼如下:
# example.py
import file
def add(x, y):
return x + y
def mul(**kwargs):
a = kwargs['a']
b = kwargs['b']
return a * b
if __name__ == '__main__':
fire.Fire()
那我們可以在命令列中通過以下陳述句呼叫 example 檔案中定義的函式:
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
6.2 main.py的代碼組織結構
在我們這個專案的 main.py 中主要包括以下四個函式,其中三個需要命令列執行,main.py 的代碼組織結構如下所示:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Coding by https://www.cnblogs.com/nickchen121/
# Datatime:2021/5/3 10:20
# Filename:main.py
# Toolby: PyCharm
import os
import csv
import ipdb
import fire
import torch as t
from torchnet import meter
from inspect import getsource
from torch.nn import functional
from torch.autograd import Variable
from torch.utils.data import DataLoader
import models
from config import opt
from data.dataset import DogCat
from utils.visualize import Visualizer
def train(**kwargs):
"""
訓練
:param kwargs:
:return:
"""
pass
def val(model, dataloader):
"""
計算模型在驗證集上的準確率等資訊,用來輔助訓練
:param model:
:param dataloader:
:return:
"""
pass
def test(**kwargs):
"""
測驗(inference)
:param kwargs:
:return:
"""
pass
def dc_help():
"""
列印幫助的資訊
:return:
"""
print('help')
if __name__ == '__main__':
fire.Fire()
main.py 搭建好這樣的組織結構后,可以通過 python main.py <function> --args==xx 的方式執行訓練或測驗,
6.3 訓練
訓練的主要步驟如下:
- 定義網路
- 定義資料
- 定義損失函式和優化器
- 計算重要指標
- 開始訓練
- 訓練網路
- 可視化各種指標
- 計算在驗證集上的指標
其中訓練函式的代碼如下:
def train(**kwargs):
"""
訓練
:param kwargs:
:return:
"""
# 根據命令列引數更新配置
opt.parse(kwargs)
vis = Visualizer(opt.env)
# step1:模型
model = getattr(models, opt.model)()
if opt.load_model_path:
model.load(opt.load_model_path)
if opt.use_gpu: model.cuda()
# step2:資料
train_data = https://www.cnblogs.com/nickchen121/archive/2021/05/06/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 = t.nn.CrossEntropyLoss()
lr = opt.lr
optimizer = t.optim.Adam(model.parameters(),
lr=lr,
weight_decay=opt.weight_decay)
# step4:統計指標:平滑處理之后的損失,還有混淆矩陣
loss_meter = meter.AverageValueMeter() # 平均損失
confusion_matrix = meter.ConfusionMeter(2) # 混淆矩陣
previous_loss = 1e100
# 訓練
for epoch in range(opt.max_epoch):
loss_meter.reset()
confusion_matrix.reset()
for ii, (data, label) in enumerate(train_dataloader):
# 訓練模型引數
inp = Variable(data)
target = Variable(label)
if opt.use_gpu:
inp = inp.cuda()
target = target.cuda()
optimizer.zero_grad()
score = model(inp)
loss = criterion(score, target)
loss.backward()
optimizer.step()
# 更新統計指標及可視化
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):
ipdb.set_trace()
model.save()
# 計算驗證集上的指標及可視化
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))
# 如果損失不再下降,則降低學習率
if loss_meter.value()[0] > previous_loss:
lr = lr * opt.lr_decay
for param_group in optimizer.param_groups:
param_group['lr'] = lr
previous_loss = loss_meter.value()[0]
6.3.1 torchnet 中的 meter
在訓練的代碼中,這里用到了 PyTorchNet 里的一個工具:meter,由于 PyTorchNet 是從 TorchNet 中遷移來的,提供了很多有用的工具,但目前的開發和檔案都不是特別完善,這里不多做贅述,只講上述用到的幾個方法,
mter 提供了一些輕量級工具,可以幫助用戶快速的統計訓練程序中的一些指標,
* AverageValueMeter 能夠計算所有數的平均值和標準差,可以用來統計一個 epoch 中損失的平均值
* confusionmeter 用來統計分類問題中的分類情況,是一個比準確率更詳細的統計指標,給出的是一個混淆矩陣
混淆矩陣舉例:
| 樣本 | 判為狗 | 判為貓 |
|---|---|---|
| 實際是貓 | 35 | 15 |
| 實際是狗 | 9 | 91 |
注:想詳細了解混淆矩陣的在第七小節
6.4 驗證
驗證相比較訓練來說簡單很多,但是需要注意把模型置于驗證模式(model.eval()),驗證完成后還需要把它設定回訓練模式(model.train()),這兩句代碼會影響 BatchNorm 和 Dropout 等層的運行模式,驗證模型準確率的代碼如下:
def val(model, dataloader):
"""
計算模型在驗證集上的準確率等資訊,用來輔助訓練
:param model:
:param dataloader:
:return:
"""
# 把模型設定為驗證模式
model.eval()
confusion_matrix = meter.ConfusionMeter(2)
for ii, data in enumerate(dataloader):
inp, label = data
val_inp = Variable(inp, volatile=True)
val_label = Variable(label.long(), volatile=True)
if opt.use_gpu:
val_inp = val_inp.cuda()
val_label = val_label.cuda()
score = model(val_inp)
confusion_matrix.add(score.data.squeeze(), label.long())
# 把模型恢復為訓練模式
model.train()
cm_value = https://www.cnblogs.com/nickchen121/archive/2021/05/06/confusion_matrix.value()
accuracy = 100. * (cm_value[0][0] + cm_value[1][1]) / (cm_value.sum())
return confusion_matrix, accuracy
6.5 測驗
測驗的時候,需要計算每個樣本屬于狗的概率,并把結果保存為 csv 檔案,測驗的代碼和驗證比較相似,但需要自己加載模型和資料,
def write_csv(results, file_name):
with open(file_name, 'w') as f:
writer = csv.writer(f)
writer.writerow(['id', 'label'])
writer.writerows(results)
def test(**kwargs):
"""
測驗(inference)
:param kwargs:
:return:
"""
opt.parse(kwargs)
# 模型
model = getattr(models, opt.model)().eval()
if opt.load_model_path:
model.load(opt.load_model_path)
if opt.use_gpu: model.cuda()
# 資料
train_data = https://www.cnblogs.com/nickchen121/archive/2021/05/06/DogCat(opt.test_data_root, test=True)
test_dataloader = DataLoader(train_data,
batch_sampler=opt.batch_size,
shuffle=False,
num_workers=opt.num_workers)
results = []
for ii, (data, path) in enumerate(test_dataloader):
inp = Variable(data, volatile=True)
if opt.use_gpu: inp = inp.cuda()
score = model(inp)
probability = probability = functional.softmax(score, dim=1)[:, 0].detach().tolist()
batch_results = [(path_, probability_) for path_, probability_ in zip(path, probability)]
results += batch_results
write_csv(results, opt.result_file)
return results
6.6 幫助函式
為了讓他人方便使用,程式中應該還需要提供一個幫助函式,用于說明函式是如何使用的,
程式的命令列介面有很多引數,如果手動用字串表示不僅復雜,而且后期修改 config 檔案時還需要修改對應的幫助資訊,為此,這里使用 Python 標準庫中的 inspect 方法,可以自動獲取 config 的源代碼,
dg_help 的代碼如下:
def dc_help():
"""
列印幫助的資訊
:return:
"""
print('''
usage:python{0} <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__))
source = (getsource(opt.__class__)) # 獲取配置資訊
print(source)
七、使用
如 dc_help 函式列印的資訊描述的一樣,可以通過命令列引數指定變數名,下面是三個使用例子,fire 會把包含 “-” 命令列引數自動轉成下劃線 “_”,也會把非數字的數值轉成字串,所以 --train--data-root=data/train 和 --train_data_root = 'data/train' 是等價的,
感興趣的可以把資料集下載下來進行測驗:貓狗分類資料集
由于本章只是講解專案架構,我就不做測驗,但是代碼應該沒什么大問題,修修補補就行了,
想要具體代碼的可以加我微信:chenyoudea,但是沒必要找我要,我也沒有嘗試去跑通這個代碼,并且我也沒有下載資料集,因為這一章沒必要,
# 訓練模型
python main.py train
--train-data-root=data/train/
--load-model-path=None
--lr=0.005
--batch-size=32
--model='ResNet34'
--max-epoch=20
python main.py train --train-data-root=data/train/ --load-model-path=None --lr=0.005 --batch-size=32 --model='ResNet34' --max-epoch=20
# 測驗模型
python main.py test
--test-data-root=data/test1
--load-model-path=None
--batch-szie=128
--model='ResNet34'
--num-workers=12
# 列印幫助資訊
python main.py dc_help
八、爭議
這里還是多說一嘴,因為這個風格更多的是書籍作者陳云老師的風格,并不是說以后你寫的代碼都要以這個為標準,這個專案架構更多的是作為一個題意或一種參考,
也就是說,不要把本篇文章的觀點作為一個必須遵守的規范,但是前期的學習可以按照這個架構來,這樣不容易犯錯,但是,對于未來你遇到的很多專案,尤其對于每個公司的專案,專案架構相信都是不一樣的,不唯經驗主義,不唯教條主義,這才是一個碼農想進階的必經之路,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/283119.html
標籤:其他
