「pytorch速成」Pytorch圖像分類從模型自定義到測試

前面已跟大家介紹了Caffe和TensorFlow,今天說說Pytorch。

01 什麼是 Pytorch

一句話總結 Pytorch = Python + Torch。

Torch 是紐約大學的一個機器學習開源框架,幾年前在學術界非常流行,包括 Lecun等大佬都在使用。但是由於使用的是一種絕大部分人絕對沒有聽過的 Lua 語言,導致很多人都被嚇退。後來隨著 Python 的生態越來越完善,Facebook 人工智能研究院推出了Pytorch並開源。Pytorch不是簡單的封裝 Torch並提供Python接口,而是對Tensor以上的所有代碼進行了重構,同TensorFlow一樣,增加了自動求導。

後來Caffe2全部併入Pytorch,如今已經成為了非常流行的框架。很多最新的研究如風格化、GAN 等大多數採用Pytorch源碼,這也是我們必須要講解它的原因。

1.1 特點

(1) 動態圖計算。TensorFlow從靜態圖發展到了動態圖機制Eager Execution,pytorch則一開始就是動態圖機制。動態圖機制的好處就是隨時隨地修改,隨處debug,沒有類似編譯的過程。

(2) 簡單。相比TensorFlow中Tensor、Variable、Session等概念充斥,數據讀取接口頻繁更新,tf.nn、tf.layers、tf.contrib各自重複,Pytorch則是從Tensor到Variable再到nn.Module,最新的Pytorch已經將Tensor和Variable合併,這分別就是從數據張量到網絡的抽象層次的遞進。有人調侃TensorFlow的設計是“make it complicated”,那麼 Pytorch的設計就是“keep it simple”。

1.2 重要概念

(1) Tensor/Variable

每一個框架都有基本的數據結構,Caffe是blob,TensorFlow和Pytorch都是Tensor,都是高維數組。Pytorch中的Tensor使用與Numpy的數組非常相似,兩者可以互轉且共享內存。

tensor包括cpu和gpu兩種類型,如torch.FloatTensortorch.cuda.FloatTensorvirable,就分別表示cpu和gpu下的32位浮點數。

tensor包含一些屬性。data,即Tensor內容;Grad,是與data對應的梯度;requires_grad,是否容許進行反向傳播的學習,更多的可以去查看API。

(2) nn.module

抽象好的網絡數據結構,可以表示為網絡的一層,也可以表示為一個網絡結構,這是一個基類。在實際使用過程中,經常會定義自己的網絡,並繼承nn.Module。具體的使用,我們看下面的網絡定義吧。

(3) torchvision包,包含了目前流行的數據集,模型結構和常用的圖片轉換工具

02 Pytorch 訓練

安裝咱們就不說了,接下來的任務就是開始訓練模型。訓練模型包括數據準備、模型定義、結果保存與分析。

2.1 數據讀取

前面已經介紹了Caffe和TensorFlow的數據讀取,兩者的輸入都是圖片list,但是讀取操作過程差異非常大,Pytorch與這兩個又有很大的差異。這一次,直接利用文件夾作為輸入,這是 Pytorch更加方便的做法。數據讀取的完整代碼如下:

data_dir = '../../../../datas/head/' 
data_transforms = {
'train': transforms.Compose([
transforms.RandomSizedCrop(48),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.5,0.5,0.5], [0.5,0.5,0.5])
]),
'val': transforms.Compose([
transforms.Scale(64),
transforms.CenterCrop(48),
transforms.ToTensor(),
transforms.Normalize([0.5,0.5,0.5], [0.5,0.5,0.5])
]),
}
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
data_transforms[x]) for x in ['train', 'val']}
dataloders = {x: torch.utils.data.DataLoader(image_datasets[x],
batch_size=16,
shuffle=True,
num_workers=4) for x in ['train', 'val']}

下面一個一個解釋,完整代碼請移步 Git 工程。

(1) datasets.ImageFolder

Pytorch的torchvision模塊中提供了一個dataset 包,它包含了一些基本的數據集如mnist、coco、imagenet和一個通用的數據加載器ImageFolder。

它會以這樣的形式組織數據,具體的請到Git工程中查看。

root/left/1.png
root/left/2.png
root/left/3.png
root/right/1.png
root/right/2.png
root/right/3.png

imagefolder有3個成員變量。

self.classes:用一個list保存類名,就是文件夾的名字。

self.class_to_idx:類名對應的索引,可以理解為 0、1、2、3 等。

self.imgs:保存(imgpath,class),是圖片和類別的數組。

不同文件夾下的圖,會被當作不同的類,天生就用於圖像分類任務。

(2) Transforms

這一點跟Caffe非常類似,就是定義了一系列數據集的預處理和增強操作。到此,數據接口就定義完畢了,接下來在訓練代碼中看如何使用迭代器進行數據讀取就可以了,包括 scale、減均值等。

(3) torch.utils.data.DataLoader

這就是創建了一個 batch,生成真正網絡的輸入。關於更多 Pytorch 的數據讀取方法,請自行學習。

2.2 模型定義

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
class simpleconv3(nn.Module):`
def __init__(self):
super(simpleconv3,self).__init__()
self.conv1 = nn.Conv2d(3, 12, 3, 2)
self.bn1 = nn.BatchNorm2d(12)
self.conv2 = nn.Conv2d(12, 24, 3, 2)
self.bn2 = nn.BatchNorm2d(24)
self.conv3 = nn.Conv2d(24, 48, 3, 2)
self.bn3 = nn.BatchNorm2d(48)
self.fc1 = nn.Linear(48 * 5 * 5 , 1200)
self.fc2 = nn.Linear(1200 , 128)
self.fc3 = nn.Linear(128 , 2)
def forward(self , x):
x = F.relu(self.bn1(self.conv1(x)))
x = F.relu(self.bn1(self.conv2(x)))
x = F.relu(self.bn3(self.conv3(x)))
x = x.view(-1 , 48 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

我們的例子都是採用一個簡單的3層卷積 + 2層全連接層的網絡結構。根據上面的網絡結構的定義,需要做以下事情。

(1) simpleconv3(nn.Module)

繼承nn.Module,前面已經說過,Pytorch的網絡層是包含在nn.Module 裡,所以所有的網絡定義,都需要繼承該網絡層,並實現super方法,如下:

super(simpleconv3,self).__init__()

這個就當作一個標準執行就可以了。

(2) 網絡結構的定義都在nn包裡,舉例說明:

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)

完整的接口如上,定義的第一個卷積層如下:

nn.Conv2d(3, 12, 3, 2)

即輸入通道為3,輸出通道為12,卷積核大小為3,stride=2,其他的層就不一一介紹了,大家可以自己去看nn的API。

(3) forward

backward方法不需要自己實現,但是forward函數是必須要自己實現的,從上面可以看出,forward 函數也是非常簡單,串接各個網絡層就可以了。

對比Caffe和TensorFlow可以看出,Pytorch的網絡定義更加簡單,初始化方法都沒有顯示出現,因為 Pytorch已經提供了默認初始化。

如果我們想實現自己的初始化,可以這麼做:

init.xavier_uniform(self.conv1.weight)init.constant(self.conv1.bias, 0.1)

它會對conv1的權重和偏置進行初始化。如果要對所有conv層使用 xavier 初始化呢?可以定義一個函數:

def weights_init(m): 
if isinstance(m, nn.Conv2d):
xavier(m.weight.data)
xavier(m.bias.data)
net = Net()
net.apply(weights_init)

03 模型訓練

網絡定義和數據加載都定義好之後,就可以進行訓練了,老規矩先上代碼:

def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
for phase in ['train', 'val']:
if phase == 'train':
scheduler.step()
model.train(True)
else:
model.train(False)
running_loss = 0.0 running_corrects = 0.0
for data in dataloders[phase]:
inputs, labels = data
if use_gpu:
inputs = Variable(inputs.cuda())
labels = Variable(labels.cuda())
else:
inputs, labels = Variable(inputs), Variable(labels)
optimizer.zero_grad()
outputs = model(inputs)
_, preds = torch.max(outputs.data, 1)
loss = criterion(outputs, labels)
if phase == 'train':
loss.backward()
optimizer.step()
running_loss += loss.data.item()
running_corrects += torch.sum(preds == labels).item()
epoch_loss = running_loss / dataset_sizes[phase]
epoch_acc = running_corrects / dataset_sizes[phase]
if phase == 'train':
writer.add_scalar('data/trainloss', epoch_loss, epoch)
writer.add_scalar('data/trainacc', epoch_acc, epoch)
else:
writer.add_scalar('data/valloss', epoch_loss, epoch)
writer.add_scalar('data/valacc', epoch_acc, epoch)
print('{} Loss: {:.4f} Acc: {:.4f}'.format(
phase, epoch_loss, epoch_acc))
writer.export_scalars_to_json("./all_scalars.json")
writer.close()
return model

分析一下上面的代碼,外層循環是epoches,然後利用 for data in dataloders[phase] 循環取一個epoch 的數據,並塞入variable,送入model。需要注意的是,每一次forward要將梯度清零,即optimizer.zero_grad(),因為梯度會記錄前一次的狀態,然後計算loss進行反向傳播。

loss.backward()
optimizer.step()

下面可以分別得到預測結果和loss,每一次epoch 完成計算。

epoch_loss = running_loss / dataset_sizes[phase]
epoch_acc = running_corrects / dataset_sizes[phase]
_, preds = torch.max(outputs.data, 1)
loss = criterion(outputs, labels)

可視化是非常重要的,鑑於TensorFlow的可視化非常方便,我們選擇了一個開源工具包,tensorboardx,安裝方法為pip install tensorboardx,使用非常簡單。

第一步,引入包定義創建:

from tensorboardX import SummaryWriter
writer = SummaryWriter()

第二步,記錄變量,如train階段的 loss,writer.add_scalar('data/trainloss', epoch_loss, epoch)。

按照以上操作就完成了,完整代碼可以看配套的Git 項目,我們看看訓練中的記錄。Loss和acc的曲線圖如下:

「pytorch速成」Pytorch圖像分類從模型自定義到測試

網絡的收斂沒有Caffe和TensorFlow好,大家可以自己去調試調試參數了,隨便折騰吧。

04 Pytorch 測試

上面已經訓練好了模型,接下來的目標就是要用它來做inference了,同樣給出代碼。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
from torch.autograd import Variable
import torchvision
from torchvision import datasets, models, transforms
import time
import os
from PIL import Image
import sys
import torch.nn.functional as F
from net import simpleconv3
data_transforms = transforms.Compose([
transforms.Resize(48),
transforms.ToTensor(),
transforms.Normalize([0.5,0.5,0.5], [0.5,0.5,0.5])])
net = simpleconv3()
modelpath = sys.argv[1]
net.load_state_dict(torch.load(modelpath,map_location=lambda storage,loc: storage))
imagepath = sys.argv[2]
image = Image.open(imagepath)
imgblob = data_transforms(image).unsqueeze(0)
imgblob = Variable(imgblob)
torch.no_grad()
predict = F.softmax(net(imgblob))
print(predict)

從上面的代碼可知,做了幾件事:

定義網絡並使用torch.load和load_state_dict載入模型。

用PIL的Image包讀取圖片,這裡沒有用OpenCV,因為Pytorch默認的圖片讀取工具就是PIL的Image,它會將圖片按照RGB的格式,歸一化到 0~1 之間。讀取圖片之後,必須轉化為Tensor變量。

evaluation的時候,必須設置torch.no_grad(),然後就可以調用 softmax 函數得到結果了。

05 總結

本節講了如何用 Pytorch 完成一個分類任務,並學習了可視化以及使用訓練好的模型做測試。

配套資料在github,https://github.com/longpeng2008/yousan.ai。

相關推薦

推薦中...