Pytorch使用卷積神經網絡對CIFAR10圖片進行分類方式
神經網絡
如下所示為一個基本的卷積神經網絡的模型,將圖像輸入之后經過卷積操作提取特征,再經過降采樣操作后輸出到下一層。
經過多次多個卷積、池化層之后結果輸出到全連接層,經過全連接映射到最終結果。
一個神經網絡的典型訓練過程可以分為如下幾步:
- 定義神經網絡,包含一些可學習參數(或者叫權重)
- 將數據輸入網絡進行訓練,并計算損失值
- 將梯度反向傳播給網絡的參數,據此更新網絡的權重,并再次訓練
定義網絡
如下所示為我們定義的神經網絡類NeuralNet。
首先它繼承自父類nn.Module
,從import可以看到從torch中分別引入了torch.nn
和torch.functional
,其中nn用于保存常用的神經網絡類,而functional
庫中則是一些網絡操作。nn.Module
類有兩個子類必須重寫的方法,初始化方法__init__
用于定義網絡結構,forward()
中定義網絡訓練操作,當網絡對象被調用時會自動執(zhí)行該方法。
在構造函數__init__
中我們定義網絡的結構,這里定義了網絡的兩個卷積層為torch.nn庫中的二維卷積函數Conv2d()
,nn.Conv2d(1, 6, (5, 5))
代表輸入數據的通道數為1,輸出通道數為6,卷積核為5×5,卷積核長和寬一致的話可以簡寫為5。用nn.Linear
實現全連接操作,輸入數據長度為16 * 5 * 5,這是由于之前conv2輸出的16通道的5×5的數據,輸出長度120的數據。經過三個全連接層輸出長度為10
在forward()
方法中實現網絡的訓練過程,將輸入數據input_x經過conv1的卷積操作后經過激活函數relu
,最后經過池化操作max_pool2d
得到第一個卷積層的輸出layer1,同樣操作后得到第二個卷積層layer2。
將卷積的結果通過flat_features()
降維,經過第二個卷積層layer2為四維數據[1, 16, 5, 5],通過tensor.size()[1:]選擇第一個維度以后的維度相乘得到features為1655=400,通過tensor.view(-1,400)
將其轉化為長度400的二維數據。最后經過三個全連接層后輸出。
import torch from torch import nn from torch.nn import functional as Func class NeuralNet(nn.Module): def __init__(self): super(NeuralNet, self).__init__() # 兩個卷積層 self.conv1 = nn.Conv2d(1, 6, (5, 5)) self.conv2 = nn.Conv2d(6, 16, 5) # 三個全連接層 self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, input_x): # 進行兩次卷積、池化操作 layer1 = Func.max_pool2d(Func.relu(self.conv1(input_x)), (2, 2)) layer2 = Func.max_pool2d(Func.relu(self.conv2(layer1)), (2, 2)) # 降維 flat = self.flat_features(layer2) # 經過三個全連接層 fc1 = Func.relu(self.fc1(flat)) fc2 = Func.relu(self.fc2(fc1)) fc3 = self.fc3(fc2) return fc3 def flat_features(self, tensor): features = 1 for size in tensor.size()[1:]: features *= size flat = tensor.view(-1, features) return flat # 創(chuàng)建一個neural_net對象并打印 neural_net = NeuralNet() print(neural_net) ''' NeuralNet( (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1)) (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1)) (fc1): Linear(in_features=400, out_features=120, bias=True) (fc2): Linear(in_features=120, out_features=84, bias=True) (fc3): Linear(in_features=84, out_features=10, bias=True) ) '''
訓練網絡
如下所示為將數據送入網絡訓練并計算損失值的過程。
首先通過torch.randn()
生成四維的隨機數據,由于我們之前定義網絡中Conv2d()
函數接收的數據要求是四維的,其中第一維度代表樣本數據的個數,第二維代表數據的通道數,第3、4維代表數據大小,這里是32×32的網格。然后將生成的數據送入neural_net()
,這里會自動調用該對象的forward()
方法進行模型訓練并輸出結果。
得到輸出結果y_output之后通過和目標值進行比較即可得出損失值,這里仍然使用randn創(chuàng)建目標值y_target,注意目標值要和輸出值維度相同,我們輸入的樣本數量為1,最后經全連接層fc3產生的結果長度為10,所以y_output維度為(1, 10),因此y_target也是二維1×10的數據。
定義評價函數criterion為nn.MSELoss(),即計算輸出和目標的均方誤差(mean-squared error)。
x = torch.randn(1, 1, 32, 32) # 隨機產生輸入數據 y_output = neural_net(x) # 輸入數據并進行訓練 y_target = torch.randn(1, 10) # 隨機產生目標數據 criterion = nn.MSELoss() # 定義評價函數 loss = criterion(y_output, y_target) # 計算損失值
反向傳播
由之前定義的網絡可知我們的從輸入到輸出,數據經過的函數操作如下,
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d -> view -> linear -> relu -> linear -> relu -> linear -> MSELoss -> loss
通過tensor的grad_fn
屬性記錄了這些函數操作,例如從loss向前回退查看grad_fn
print(loss.grad_fn) # MSELoss print(loss.grad_fn.next_functions[0][0]) # Linear print(loss.grad_fn.next_functions[0][0].next_functions[0][0]) # ReLU
我們進行反向傳播操作,然后就可以查看各層網絡參數的梯度
neural_net.zero_grad() # 清零所有參數(parameter)的梯度緩存 loss.backward() # 反向傳播 print(neural_net.conv1.bias.grad) # 查看梯度 # tensor([-0.0046, -0.0087, 0.0390, 0.0045, -0.0096, 0.0028])
根據得到的梯度對網絡的參數進行更新,例如這里使用隨機梯度下降法進行更新,其公式為weight = weight - learning_rate * gradient,即在原有權重的基礎上,根據學習率learning_rate
減少一定梯度。
如下所示遍歷網絡的所有參數neural_net.parameters
并對其進行更新
learning_rate = 0.01 for param in neural_net.parameters(): param.data.sub_(param.grad.data * learning_rate)
然而在使用神經網絡時,我們可能希望使用各種不同的更新規(guī)則,如SGD、Nesterov-SGD、Adam、RMSProp等。
在torch.optim
庫實現了所有的這些方法,如下所示,首先創(chuàng)建優(yōu)化器optimizer
,然后進行多次迭代訓練
import torch.optim as optim # 創(chuàng)建優(yōu)化器(optimizer) optimizer = optim.SGD(neural_net.parameters(), lr=0.01) # 在訓練的迭代中: for epoch in range(100): optimizer.zero_grad() # 清零梯度緩存 output = neural_net(x) loss = criterion(y_output, y_target) loss.backward() optimizer.step() # 自動更新參數
CIFAR10圖片識別
卷積神經網絡一個常用的領域就是圖片分類,而圖片分類中最經典的就是對CIFAR10圖片數據集進行分類。
它是一個包含“飛機”,“汽車”,“鳥”,“貓”,“鹿”,“狗”,“青蛙”,“馬”,“船”,“卡車”10中類別的圖片庫。
在CIFAR-10里面的圖片數據大小是3x32x32,即:三通道彩色圖像,圖像大小是32x32像素。
準備數據
pytorch的torchvision
提供了CIFAR10庫,通過torchvision.datasets.CIFAR10
加載該數據集。root為數據集的路徑,如果該路徑下沒有數據,則會從指定站點下載并保存到該路徑,train
屬性標志是訓練集還是測試集數據。transform
指定了對數據進行的預處理操作。
例如這里將兩個預處理操作通過Compose
放在了transform中,第一步ToTensor將數據轉化為張量,第二步通過Normalize()
將數據化為正態(tài)分布值。
前面的(0.5,0.5,0.5)是 R G B 三個通道上的均值,后面(0.5, 0.5, 0.5)是三個通道的標準差,Normalize對每個通道執(zhí)行以下操作:image =(圖像-平均值)/ std。當mean,std都是0.5時將使圖像在[-1,1]范圍內歸一化。
例如,最小值0將轉換(0-0.5)/0.5=-1
接著使用DataLoader
將數據分為多個批次,batch_size
指定每個批次包含幾個圖片,shuffle
為是否打亂圖片,num_workers
指定多個線程去加載數據。
當訓練很快、加載數據時間過慢時會導致模型等待數據加載而變慢,這時可以采用多線程來加載數據。
import torch import torchvision # 定義數據預處理操作 transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) # 加載數據 data_path = 'D:/Temp/MachineLearning/data' train_set = torchvision.datasets.CIFAR10(root=data_path, train=True, download=True, transform=transform) test_set = torchvision.datasets.CIFAR10(root=data_path, train=False, download=True, transform=transform) # 封裝為批數據 train_loader = torch.utils.data.DataLoader(train_set, batch_size=4, shuffle=True, num_workers=2) test_loader = torch.utils.data.DataLoader(test_set, batch_size=4, shuffle=False, num_workers=2) # 定義標簽值 classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
我們通過matplotlib打印其中的一個批次圖片和標簽。由于之前將圖片標準化,所以需要進行反標準化操作。
由于CiFAR10的圖片數據為3×32×32,需要使用transpose將其轉為32×32×3
import matplotlib.pyplot as plt import numpy as np # 輸出圖像的函數 def imshow(img): img = img / 2 + 0.5 # 反標準化 npimg = img.numpy() plt.imshow(np.transpose(npimg, (1, 2, 0))) plt.show() # 獲取一個批次的訓練圖片、標簽 images, labels = iter(train_loader).next() # 顯示圖片 imshow(torchvision.utils.make_grid(images)) # 打印圖片標簽 print(' '.join('%5s' % classes[labels[j]] for j in range(4)))
顯示結果如下:
定義網絡和參數
如下定義卷積網絡類并創(chuàng)建一個對象net,以及定義訓練的損失函數和優(yōu)化器
nn.Conv2d(3, 6, 5)
代表使用pytorch自帶的二維卷積網絡,輸入通道為3,輸出通道為6,卷積核大小為5×5。
輸入的一個批次有四張32×32的3通道圖片[4, 3, 32, 32],輸出大小=(輸入大小-卷積核+padding)/stride+1,這里默認padding為0,stride為1,所以卷積后為[4, 6, 28, 28]
nn.MaxPool2d(2, 2)
為最大池化函數,池化窗口大小為2,步長也為2,由于窗口大小為2,所以圖片大小會/2,即池化后為[4, 6, 14, 14]
之后經過view()
將x轉化為[4, 400]的二維張量,再經過由nn.Linear()
實現的三個全連接層,由400->120->84->10映射為10個分類
import torch.nn as nn import torch.nn.functional as F import torch.optim as optim # 定義卷積網絡 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(3, 6, 5) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 16, 5) self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): # 輸入x:[4, 3, 32, 32] x = self.pool(F.relu(self.conv1(x))) # x:[4, 6, 14, 14] x = self.pool(F.relu(self.conv2(x))) # x:[4, 16, 5, 5] x = x.view(-1, 16 * 5 * 5) # x:[4, 400] x = F.relu(self.fc1(x)) # x:[4, 120] x = F.relu(self.fc2(x)) # x:[4, 84] x = self.fc3(x) # x:[4, 10] return x net = Net() criterion = nn.CrossEntropyLoss() # 損失函數 optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) # 優(yōu)化器
訓練并保存模型
如下所示進行兩輪疊代訓練,每輪按批次取出訓練集的數據投入模型進行訓練
for epoch in range(2): # 進行兩輪迭代訓練 running_loss = 0.0 # 按批次取出數據進行訓練 for i, data in enumerate(train_loader, 0): inputs, labels = data # 獲取數據和標簽 optimizer.zero_grad() # 清零梯度緩存 outputs = net(inputs) # 得到預測結果 loss = criterion(outputs, labels) # 計算損失 loss.backward() # 反向傳播 optimizer.step() # 更新參數 # 每隔兩千次輸出一次平均損失值 running_loss += loss.item() if i % 2000 == 1999: print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 2000)) running_loss = 0.0 print('Finished Training') # 保存訓練好的模型 MODEL_PATH = './cifar_net.pth' torch.save(net.state_dict(), MODEL_PATH)
載入模型進行測試
模型以及訓練好并保存了,那么模型的預測效果如何呢?
這就需要在測試集數據上進行檢測了,如下所示,我們首先讀取保存的模型,然后將測試集的圖片數據images
投入模型進行預測,然后取得預測值predicted
,將其和測試集的標簽labels
進行比對,統(tǒng)計預測正確的個數correct,除以總數total就是準去率了
# 加載模型 net = Net() net.load_state_dict(torch.load(MODEL_PATH)) correct = 0 total = 0 with torch.no_grad(): for data in test_loader: images, labels = data outputs = net(images) # 將測試集圖片投入模型 _, predicted = torch.max(outputs.data, 1) total += labels.size(0) # 統(tǒng)計測試集總樣本數 correct += (predicted == labels).sum().item() # 累計預測正確的個數 print('在整個測試集上的準確率為: %d %%' % (100 * correct / total))
使用GPU
Nvidia顯卡具有的CUDA加速可以更快地進行神經網絡的訓練,torch.cuda
包集成了相關的操作函數。例如通過is_available()
可以查看顯卡是否可用,device_count()
統(tǒng)計具有cuda功能的顯卡個數,get_device_name(i)
查看第i個顯卡名字。
如果需要使用GPU進行網絡訓練,需要將模型net和訓練集的數據images、標簽labels都放到GPU設備上,通過model.to(device)
可以將模型或張量放到指定設備上?;蛘咧苯邮褂?code>model.cuda(i)放到第i塊CUDA顯卡上。
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 獲取GPU設備 print(device) net = Net() net.to(device) # 將模型放在GPU上 for epoch in range(2): # 進行兩輪迭代訓練 running_loss = 0.0 # 按批次取出數據進行訓練 for i, data in enumerate(train_loader, 0): inputs, labels = data inputs, labels = inputs.to(device),labels.to(device) # 將訓練數據放到GPU ......
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。