60分鐘教你上手PyTorch + 遷移學習

                  什麼是PyTorch?

Autograd: 自動求導

PyTorch神經網絡簡介

訓練一個分類器

通過例子學PyTorch

使用Numpy實現三層神經網絡

使用Tensor來實現三層神經網絡

實現autograd來實現三層神經網絡

使用自定義的ReLU函數

和Tensorflow的對比

使用nn模塊來實現三層神經網絡

使用optim包

自定義nn模塊

流程控制和參數共享

遷移學習示例

加載數據

可視化圖片

訓練模型

可視化預測結果的函數

fine-tuning所有參數

fine-tuning最後一層參數


什麼是PyTorch?

PyTorch是一個基於Python的科學計算包,它主要有兩個用途:

  • 類似Numpy但是能利用GPU加速
  • 一個非常靈活和快速的用於深度學習的研究平臺

Tensor

Tensor類似與NumPy的ndarray,但是可以用GPU加速。使用前我們需要導入torch包:

from __future__ import print_function
import torch

下面的代碼構造一個5×35×3的未初始化的矩陣:

x = torch.empty(5, 3)
print(x)

# 輸出:
tensor([[-1.9998e+05,  4.5818e-41,  3.4318e-37],
[ 0.0000e+00,  0.0000e+00,  0.0000e+00],
[ 0.0000e+00,  0.0000e+00,  1.2877e+29],
[ 2.0947e-30,  0.0000e+00,  0.0000e+00],
[ 0.0000e+00,  0.0000e+00, -4.5328e+05]])

我們可以使用rand隨機初始化一個矩陣:

x = torch.rand(5, 3)
print(x)

#輸出:
tensor([[ 0.9656,  0.5782,  0.0482],
[ 0.7462,  0.5838,  0.1844],
[ 0.8262,  0.4507,  0.6128],
[ 0.2961,  0.8956,  0.3092],
[ 0.4973,  0.2203,  0.9200]])

下面的代碼構造一個用零初始化的矩陣,它的類型(dtype)是long:

x = torch.zeros(5, 3, dtype=torch.long)
print(x)

#輸出:
tensor([[ 0,  0,  0],
[ 0,  0,  0],
[ 0,  0,  0],
[ 0,  0,  0],
[ 0,  0,  0]])

我們也可以使用Python的數組來構造Tensor:

x = torch.tensor([5.5, 3])
print(x)

我們可以從已有的tensor信息(size和dtype)來構造tensor。但也可以用不同的dtype來構造。

x = x.new_ones(5, 3, dtype=torch.double)      # new_* methods take in sizes
print(x)

x = torch.randn_like(x, dtype=torch.float)    # override dtype!
print(x)

我們可以是用size函數來看它的shape:

print(x.size())
#輸出:
torch.Size([5, 3])

注意torch.Size其實是一個tuple,因此它支持所有的tuple操作。

Operation

接下來我們來學習一些PyTorch的Operation。Operation一般可以使用函數的方式使用,但是爲了方便使用,PyTorch重載了一些常見的運算符,因此我們可以這樣來進行Tensor的加法:

y = torch.rand(5, 3)
print(x + y)

我們也可以用add函數來實現加法:

print(torch.add(x, y))

我們也可以給加法提供返回值(而不是生成一個新的返回值):

result = torch.empty(5, 3)
torch.add(x, y, out=result) # x + y的結果放到result裏。
print(result) 

我們也可以把相加的結果直接修改第一個被加數:

# 把x加到y
y.add_(x)
print(y)

注意:就地修改tensor的operation以下劃線結尾。比如: x.copy_(y), x.t_(), 都會修改x。

Tensor的變換

我們也可以使用類似numpy的下標運算來操作PyTorch的Tensor:

#打印x的第一列
print(x[:, 1])

如果想resize或者reshape一個Tensor,我們可以使用torch.view:

x = torch.randn(4, 4)
y = x.view(16)
z = x.view(-1, 8)  # -1的意思是讓PyTorch自己推斷出第一維的大小。
print(x.size(), y.size(), z.size())

如果一個tensor只有一個元素,可以使用item()函數來把它變成一個Python number:

x = torch.randn(1)
print(x)
#輸出的是一個Tensor
tensor([-0.6966])

print(x.item())
#輸出的是一個數
-0.6966081857681274

Tensor與Numpy的互相轉換

Torch Tensor和NumPy數組的轉換非常容易。它們會共享內存地址,因此修改一方會影響另一方。把一個Torch Tensor轉換成NumPy數組的代碼示例爲:

a = torch.ones(5)
print(a)
#tensor([ 1.,  1.,  1.,  1.,  1.])
b = a.numpy()
print(b)
#[1. 1. 1. 1. 1.]

修改一個會影響另外一個:

a.add_(1)
print(a)
# tensor([ 2.,  2.,  2.,  2.,  2.])
print(b)
# [2. 2. 2. 2. 2.]

把把NumPy數組轉成Torch Tensor的代碼示例爲:

import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print(a)
# [2. 2. 2. 2. 2.]
print(b)
# tensor([ 2.,  2.,  2.,  2.,  2.], dtype=torch.float64)

CPU上的所有類型的Tensor(除了CharTensor)都可以和Numpy數組來回轉換。

CUDA Tensor

Tensor可以使用to()方法來移到任意設備上:

# 如果有CUDA
# 我們會使用``torch.device``來把tensors放到GPU上
if torch.cuda.is_available():
	device = torch.device("cuda")          # 一個CUDA device對象。
	y = torch.ones_like(x, device=device)  # 直接在GPU上創建tensor
	x = x.to(device)                       # 也可以使用``.to("cuda")``把一個tensor從CPU移到GPU上
	z = x + y
	print(z)
	print(z.to("cpu", torch.double))       # ``.to``也可以在移動的過程中修改dtype
	
# 輸出:
tensor([ 0.3034], device='cuda:0')
tensor([ 0.3034], dtype=torch.float64)	

Autograd: 自動求導

PyTorch的核心是autograd包。 我們首先簡單的瞭解一些,然後用PyTorch開始訓練第一個神經網絡。autograd爲所有用於Tensor的operation提供自動求導的功能。我們通過一些簡單的例子來學習它基本用法。

從自動求導看Tensor

torch.Tensor 是這個包的核心類。如果它的屬性requires_grad是True,那麼PyTorch就會追蹤所有與之相關的operation。當完成(正向)計算之後, 我們可以調用backward(),PyTorch會自動的把所有的梯度都計算好。與這個tensor相關的梯度都會累加到它的grad屬性裏。

如果不想計算這個tensor的梯度,我們可以調用detach(),這樣它就不會參與梯度的計算了。爲了阻止PyTorch記錄用於梯度計算相關的信息(從而節約內存),我們可以使用 with torch.no_grad()。這在模型的預測時非常有用,因爲預測的時候我們不需要計算梯度,否則我們就得一個個的修改Tensor的requires_grad屬性,這會非常麻煩。

關於autograd的實現還有一個很重要的Function類。Tensor和Function相互連接從而形成一個有向無環圖, 這個圖記錄了計算的完整歷史。每個tensor有一個grad_fn屬性來引用創建這個tensor的Function(用戶直接創建的Tensor,這些Tensor的grad_fn是None)。

如果你想計算梯度,可以對一個Tensor調用它的backward()方法。如果這個Tensor是一個scalar(只有一個數),那麼調用時不需要傳任何參數。如果Tensor多於一個數,那麼需要傳入和它的shape一樣的參數,表示反向傳播過來的梯度。

創建tensor時設置屬性requires_grad=True,PyTorch就會記錄用於反向梯度計算的信息:

x = torch.ones(2, 2, requires_grad=True)
print(x)

然後我們通過operation產生新的tensor:

y = x + 2
print(y)

是通過operation產生的tensor,因此它的grad_fn不是None。

print(y.grad_fn)
# <AddBackward0 object at 0x7f35409a68d0>

再通過y得到z和out

z = y * y * 3
out = z.mean()

print(z, out)
# z = tensor([[ 27.,  27.],[ 27.,  27.]]) 
# out = tensor(27.)

requires_grad_()函數會修改一個Tensor的requires_grad。

a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)

輸出是:

False
True
<SumBackward0 object at 0x7f35766827f0>

梯度

現在我們裏反向計算梯度。因爲out是一個scalar,因此out.backward()等價於out.backward(torch.tensor(1))。

out.backward()

我們可以打印梯度d(out)/dx:

print(x.grad)
# tensor([[ 4.5000,  4.5000],
[ 4.5000,  4.5000]])

我們手動計算來驗證一下。爲了簡單,我們把out記爲o。  並且

因此,,因此

我們也可以用autograd做一些很奇怪的事情!比如y和x的關係是while循環的關係(似乎很難用一個函數直接表示y和x的關係?對x不斷平方直到超過1000,這是什麼函數?)

x = torch.randn(3, requires_grad=True)

y = x * 2
while y.data.norm() < 1000:
	y = y * 2

print(y)
# tensor([ -692.4808,  1686.1211,   667.7313])
gradients = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(gradients)

print(x.grad)
# tensor([  102.4000,  1024.0000,     0.1024])

我們可以使用”with torch.no_grad()”來停止梯度的計算:

print(x.requires_grad)
print((x ** 2).requires_grad)

with torch.no_grad():
	print((x ** 2).requires_grad)

輸出爲:

True
True
False

PyTorch神經網絡簡介

神經網絡可以通過torch.nn包來創建。我們之前簡單的瞭解了autograd,而nn會使用autograd來定義模型以及求梯度。一個nn.Module對象包括了許多網絡層(layer),並且有一個forward(input)方法來返回output。如下圖所示,我們會定義一個卷積網絡來識別mnist圖片。

圖:識別MNIST數據的神經網絡

訓練一個神經網絡通常需要如下步驟:

  • 定義一個神經網絡,它通常有一些可以訓練的參數
  • 迭代一個數據集(dataset)
  • 處理網絡的輸入
  • 計算loss(會調用Module對象的forward方法)
  • 計算loss對參數的梯度
  • 更新參數,通常使用如下的梯度下降方法來更新:

    weight = weight - learning_rate * gradient

定義網絡

import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

	def __init__(self):
		super(Net, self).__init__()
		# 輸入是1個通道的灰度圖,輸出6個通道(feature map),使用5x5的卷積核
		self.conv1 = nn.Conv2d(1, 6, 5)
		# 第二個卷積層也是5x5,有16個通道
		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):
		# 32x32 -> 28x28 -> 14x14 
		x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
		# 14x14 -> 10x10 -> 5x5
		x = F.max_pool2d(F.relu(self.conv2(x)), 2)
		x = x.view(-1, self.num_flat_features(x))
		x = F.relu(self.fc1(x))
		x = F.relu(self.fc2(x))
		x = self.fc3(x)
		return x
	
	def num_flat_features(self, x):
		size = x.size()[1:]  # 除了batch維度之外的其它維度。
		num_features = 1
		for s in size:
		num_features *= s
		return num_features


net = Net()
print(net)
# Net(
(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)
)

我們只需要定義forward函數,而backward函數會自動通過autograd創建。在forward函數裏可以使用任何處理Tensor的函數。我們可以使用函數net.parameters()來得到模型所有的參數。

params = list(net.parameters())
print(len(params))
# 10
print(params[0].size())  # conv1的weight
# torch.Size([6, 1, 5, 5])

測試網絡

接着我們嘗試一個隨機的32x32的輸入來檢驗(sanity check)網絡定義沒有問題。注意:這個網絡(LeNet)期望的輸入大小是32x32。如果使用MNIST數據集(28x28),我們需要縮放到32x32。

input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
# tensor([[-0.0198,  0.0438,  0.0930, -0.0267, -0.0344,  0.0330,  0.0664,
0.1244, -0.0379,  0.0890]])

默認的梯度會累加,因此我們通常在backward之前清除掉之前的梯度值:

net.zero_grad()
out.backward(torch.randn(1, 10))

注意:torch.nn只支持mini-batches的輸入。整個torch.nn包的輸入都必須第一維是batch,即使只有一個樣本也要弄成batch是1的輸入。

比如,nn.Conv2d的輸入是一個4D的Tensor,shape是nSamples x nChannels x Height x Width。如果你只有一個樣本(nChannels x Height x Width),那麼可以使用input.unsqueeze(0)來增加一個batch維。

損失函數

損失函數的參數是(output, target)對,output是模型的預測,target是實際的值。損失函數會計算預測值和真實值的差別,損失越小說明預測的越準。

PyTorch提供了這裏有許多不同的損失函數: http://pytorch.org/docs/nn.html#loss-functions。最簡單的一個損失函數是:nn.MSELoss,它會計算預測值和真實值的均方誤差。比如:

output = net(input)
target = torch.arange(1, 11)  # 隨便僞造的一個“真實值” 
target = target.view(1, -1)  # 把它變成output的shape(1, 10) 
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

如果從loss往回走,需要使用tensor的grad_fn屬性,我們Negative看到這樣的計算圖:

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss

因此當調用loss.backward()時,PyTorch會計算這個圖中所有requires_grad=True的tensor關於loss的梯度。

print(loss.grad_fn)  # MSELoss
print(loss.grad_fn.next_functions[0][0])  # Add
print(loss.grad_fn.next_functions[0][0].next_functions[0][0])  # Expand

#輸出:
<MseLossBackward object at 0x7f445b3a2dd8>
<AddmmBackward object at 0x7f445b3a2eb8>
<ExpandBackward object at 0x7f445b3a2dd8>

計算梯度

在調用loss.backward()之前,我們需要清除掉tensor裏之前的梯度,否則會累加進去。

net.zero_grad()     # 清掉tensor裏緩存的梯度值。

print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)

loss.backward()

print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

更新參數

更新參數最簡單的方法是使用隨機梯度下降(SGD): ????ℎ?=????ℎ?−????????????∗????????weight=weight−learningrate∗gradient 我們可以使用如下簡單的代碼來實現更新:

learning_rate = 0.01
for f in net.parameters():
	f.data.sub_(f.grad.data * learning_rate)

通常我們會使用更加複雜的優化方法,比如SGD, Nesterov-SGD, Adam, RMSProp等等。爲了實現這些算法,我們可以使用torch.optim包,它的用法也非常簡單:

import torch.optim as optim

# 創建optimizer,需要傳入參數和learning rate
optimizer = optim.SGD(net.parameters(), lr=0.01)

# 清除梯度
optimizer.zero_grad()  
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step()    # optimizer會自動幫我們更新參數

注意:即使使用optimizer,我們也需要清零梯度。但是我們不需要一個個的清除,而是用optimizer.zero_grad()一次清除所有。

訓練一個分類器

介紹了PyTorch神經網絡相關包之後我們就可以用這些知識來構建一個分類器了。

如何進行數據處理

一般地,當我們處理圖片、文本、音頻或者視頻數據的時候,我們可以使用python代碼來把它轉換成numpy數組。然後再把numpy數組轉換成torch.xxxTensor。

  • 對於處理圖像,常見的lib包括Pillow和OpenCV
  • 對於音頻,常見的lib包括scipy和librosa
  • 對於文本,可以使用標準的Python庫,另外比較流行的lib包括NLTK和SpaCy

對於視覺問題,PyTorch提供了一個torchvision包(需要單獨安裝),它對於常見數據集比如Imagenet, CIFAR10, MNIST等提供了加載的方法。並且它也提供很多數據變化的工具,包括torchvision.datasets和torch.utils.data.DataLoader。這會極大的簡化我們的工作,避免重複的代碼。

在這個教程裏,我們使用CIFAR10數據集。它包括十個類別:”airplane”, “automobile”, “bird”, “cat”, “deer”, “dog”, “frog”, “horse”, “ship”,”truck”。圖像的對象是3x32x32,也就是3通道(RGB)的32x32的圖片。下面是一些樣例圖片。

                                     

                                                                              圖:cifar10樣例

訓練的步驟

  • 使用torchvision加載和預處理CIFAR10訓練和測試數據集。
  • 定義卷積網絡
  • 定義損失函數
  • 用訓練數據訓練模型
  • 用測試數據測試模型

數據處理

通過使用torchvision,我們可以輕鬆的加載CIFAR10數據集。首先我們導入相關的包:

import torch
import torchvision
import torchvision.transforms as transforms

torchvision讀取的datasets是PILImage對象,它的取值範圍是[0, 1],我們把它轉換到範圍[-1, 1]。

transform = transforms.Compose(
	[transforms.ToTensor(),
	transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='/path/to/data', train=True,
	download=True, transform=transform)
	trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
	shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='/path/to/data', train=False,
	download=True, transform=transform)
	testloader = torch.utils.data.DataLoader(testset, batch_size=4,
	shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
	'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

我們來看幾張圖片,如下圖所示,顯示圖片的代碼如下:

import matplotlib.pyplot as plt
import numpy as np

# 顯示圖片的函數


def imshow(img):
img = img / 2 + 0.5     #  [-1,1] -> [0,1]
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0))) # (channel, width, height) -> (width, height, channel)


# 隨機選擇一些圖片
dataiter = iter(trainloader)
images, labels = dataiter.next()

# 顯示圖片
imshow(torchvision.utils.make_grid(images))
# 打印label
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

圖:隨機選擇的圖片

定義卷積網絡

網絡結構和上一節的介紹類似,只是輸入通道從1變成3。

import torch.nn as nn
import torch.nn.functional as F


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 = self.pool(F.relu(self.conv1(x)))
		x = self.pool(F.relu(self.conv2(x)))
		x = x.view(-1, 16 * 5 * 5)
		x = F.relu(self.fc1(x))
		x = F.relu(self.fc2(x))
		x = self.fc3(x)
		return x

net = Net()

定義損失函數和optimizer}

我們這裏使用交叉熵損失函數,Optimizer使用帶衝量的SGD。

import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

訓練網絡

我們遍歷DataLoader進行訓練。

for epoch in range(2):  # 這裏只迭代2個epoch,實際應該進行更多次訓練 

	running_loss = 0.0
	for i, data in enumerate(trainloader, 0):
		# 得到輸入
		inputs, labels = data
		
		# 梯度清零 
		optimizer.zero_grad()
		
		# forward + backward + optimize
		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')

在測試數據集上進行測試

我們進行了2輪迭代,可以使用測試數據集上的數據來進行測試。首先我們隨機抽取幾個樣本來進行測試。

dataiter = iter(testloader)
images, labels = dataiter.next()


imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))

隨機選擇出來的測試樣例如下圖所示。

圖:隨機測試的結果

我們用模型來預測一下,看看是否正確預測:

outputs = net(images)

outputs是10個分類的logits。我們在訓練的時候需要用softmax把它變成概率(CrossEntropyLoss幫我們做了),但是預測的時候沒有必要,因爲我們只需要知道哪個分類的概率大就行。

_, predicted = torch.max(outputs, 1)

print('Predicted: ', ' '.join('%5s' % classes[predicted[j]]
		for j in range(4)))

# cat  ship  ship  ship

預測中的四個錯了一個,似乎還不錯。接下來我們看看在整個測試集合上的效果:

correct = 0
total = 0
with torch.no_grad():
for data in testloader:
	images, labels = data
	outputs = net(images)
	_, predicted = torch.max(outputs.data, 1)
	total += labels.size(0)
	correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: %d %%' % (
	100 * correct / total))

# Accuracy of the network on the 10000 test images: 55 %

看起來比隨機的瞎猜要好,因爲隨機猜的準確率大概是10%的準確率,所以模型確實學到了一些東西。我們也可以看每個分類的準確率:

class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
	for data in testloader:
		images, labels = data
		outputs = net(images)
		_, predicted = torch.max(outputs, 1)
		c = (predicted == labels).squeeze()
		for i in range(4):
			label = labels[i]
			class_correct[label] += c[i].item()
			class_total[label] += 1


for i in range(10):
	print('Accuracy of %5s : %2d %%' % (
		classes[i], 100 * class_correct[i] / class_total[i]))

結果爲:

Accuracy of plane : 52 %
Accuracy of   car : 66 %
Accuracy of  bird : 49 %
Accuracy of   cat : 34 %
Accuracy of  deer : 30 %
Accuracy of   dog : 45 %
Accuracy of  frog : 72 %
Accuracy of horse : 71 %
Accuracy of  ship : 76 %
Accuracy of truck : 55 %

GPU上訓練

爲了在GPU上訓練,我們需要把Tensor移到GPU上。首先我們看看是否有GPU,如果沒有,那麼我們還是fallback到CPU。

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
# cuda:0

用GPU進行訓練:

class Net2(nn.Module):
def __init__(self):
super(Net2, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5).to(device)
self.pool = nn.MaxPool2d(2, 2).to(device)
self.conv2 = nn.Conv2d(6, 16, 5).to(device)
self.fc1 = nn.Linear(16 * 5 * 5, 120).to(device)
self.fc2 = nn.Linear(120, 84).to(device)
self.fc3 = nn.Linear(84, 10).to(device)

def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x


net = Net2()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

for epoch in range(20):

	running_loss = 0.0
	for i, data in enumerate(trainloader, 0):
		# 得到輸入
		inputs, labels = data 
		inputs, labels = inputs.to(device), labels.to(device) 
		# 梯度清零 
		optimizer.zero_grad()
		
		# forward + backward + optimize
		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')

通過例子學PyTorch

下面我們通過使用不同的方法來實現一個簡單的三層(一個隱層)的全連接神經網絡來熟悉PyTorch的常見用法。

使用Numpy實現三層神經網絡

我們需要實現一個全連接的激活爲ReLU的網絡,它只有一個隱層,沒有bias,用於迴歸預測一個值,loss是計算實際值和預測值的歐氏距離。這裏完全使用numpy手動的進行前向和後向計算。numpy數組就是一個n維的數值,它並不知道任何關於深度學習、梯度下降或者計算圖的東西,它只是進行數值運算。

import numpy as np

# N是batch size;D_in是輸入大小
# H是隱層的大小;D_out是輸出大小。
N, D_in, H, D_out = 64, 1000, 100, 10

# 隨機產生輸入與輸出
x = np.random.randn(N, D_in)
y = np.random.randn(N, D_out)

# 隨機初始化參數
w1 = np.random.randn(D_in, H)
w2 = np.random.randn(H, D_out)

learning_rate = 1e-6
for t in range(500):
	# 前向計算y
	h = x.dot(w1)
	h_relu = np.maximum(h, 0)
	y_pred = h_relu.dot(w2)
	
	# 計算loss
	loss = np.square(y_pred - y).sum()
	print(t, loss)
	
	# 反向計算梯度 
	grad_y_pred = 2.0 * (y_pred - y)
	grad_w2 = h_relu.T.dot(grad_y_pred)
	grad_h_relu = grad_y_pred.dot(w2.T)
	grad_h = grad_h_relu.copy()
	grad_h[h < 0] = 0
	grad_w1 = x.T.dot(grad_h)
	
	# 更新參數
	w1 -= learning_rate * grad_w1
	w2 -= learning_rate * grad_w2

使用Tensor來實現三層神經網絡

和前面一樣,我們還是實現一個全連接的Relu激活的網絡,它只有一個隱層並且沒有bias。loss是預測與真實值的歐氏距離。之前我們用Numpy實現,自己手動前向計算loss,反向計算梯度。這裏還是一樣,只不過把numpy數組換成了PyTorch的Tensor。但是使用PyTorch的好處是我們可以利用GPU來加速計算,如果想用GPU計算,我們值需要在創建tensor的時候指定device爲gpu。

import torch


dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0") # 如果想在GPU上運算,把這行註釋掉。

N, D_in, H, D_out = 64, 1000, 100, 10

x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)

w1 = torch.randn(D_in, H, device=device, dtype=dtype)
w2 = torch.randn(H, D_out, device=device, dtype=dtype)

learning_rate = 1e-6
for t in range(500): 
	h = x.mm(w1)
	h_relu = h.clamp(min=0) # 使用clamp(min=0)來實現ReLU
	y_pred = h_relu.mm(w2)
	
	loss = (y_pred - y).pow(2).sum().item()
	print(t, loss)
	
	grad_y_pred = 2.0 * (y_pred - y)
	grad_w2 = h_relu.t().mm(grad_y_pred)
	grad_h_relu = grad_y_pred.mm(w2.t())
	grad_h = grad_h_relu.clone()
	grad_h[h < 0] = 0
	grad_w1 = x.t().mm(grad_h)
	
	w1 -= learning_rate * grad_w1
	w2 -= learning_rate * grad_w2

實現autograd來實現三層神經網絡

還是和前面一樣實現一個全連接的網絡,只有一個隱層而且沒有bias,使用歐氏距離作爲損失函數。這個實現使用PyTorch的Tensor來計算前向階段,然後使用PyTorch的autograd來自動幫我們反向計算梯度。PyTorch的Tensor代表了計算圖中的一個節點。如果x是一個Tensor並且x.requires_grad=True,那麼x.grad這個Tensor會保存某個scalar(通常是loss)對x的梯度。

import torch

dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0") # 如果有GPU可以註釋掉這行

# N是batch size;D_in是輸入大小
# H是隱層的大小;D_out是輸出大小。
N, D_in, H, D_out = 64, 1000, 100, 10

# 創建隨機的Tensor作爲輸入和輸出
# 輸入和輸出需要的requires_grad=False(默認),
# 因爲我們不需要計算loss對它們的梯度。
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)

# 創建weight的Tensor,需要設置requires_grad=True 
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(500):
	# Forward階段: mm實現矩陣乘法,但是它不支持broadcasting。
	# 如果需要broadcasting,可以使用matmul
	# clamp本來的用途是把值clamp到指定的範圍,這裏實現ReLU。 
	y_pred = x.mm(w1).clamp(min=0).mm(w2)
	
	# pow(2)實現平方計算。 
	# loss.item()得到這個tensor的值。也可以直接打印loss,這會打印很多附加信息。
	loss = (y_pred - y).pow(2).sum()
	print(t, loss.item())
	
	# 使用autograd進行反向計算。它會計算loss對所有對它有影響的
	# requires_grad=True的Tensor的梯度。
	
	loss.backward()
	
	# 手動使用梯度下降更新參數。一定要把更新的代碼放到torch.no_grad()裏
	# 否則下面的更新也會計算梯度。後面我們會使用torch.optim.SGD,
	# 它會幫我們管理這些用於更新梯度的計算。
	
	with torch.no_grad():
		w1 -= learning_rate * w1.grad
		w2 -= learning_rate * w2.grad
		
		# 手動把梯度清零 
		w1.grad.zero_()
		w2.grad.zero_()

使用自定義的ReLU函數

這裏還是那個全連接網絡的例子,不過這裏我們不使用clamp來實現ReLU,而是我們自己來實現一個MyReLU的函數。

import torch


class MyReLU(torch.autograd.Function):
	"""
	爲了實現自定義的實現autograd的函數,我們需要基礎torch.autograd.Function,
	然後再實現forward和backward兩個函數。
	"""
	
	@staticmethod
	def forward(ctx, input):
		"""
		在forward函數,我們的輸入是input,然後我們根據input計算輸出。
		# 同時爲了下面的backward,
		我們需要使用save_for_backward來保存用於反向計算的數據到ctx裏,
		# 這裏我們需要保存input。
		"""
		ctx.save_for_backward(input)
		return input.clamp(min=0)
	
	@staticmethod
	def backward(ctx, grad_output):
		"""
		從ctx.saved_tensors裏恢復input
		然後用input計算梯度
		"""
		input, = ctx.saved_tensors
		grad_input = grad_output.clone()
		grad_input[input < 0] = 0
		return grad_input


dtype = torch.float
device = torch.device("cpu")

N, D_in, H, D_out = 64, 1000, 100, 10

x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)

w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(500):
	# 爲了調用我們自定義的函數,我們需要使用Function.apply方法,把它命名爲'relu'
	relu = MyReLU.apply
	
	# 我們使用自定義的ReLU來進行Forward計算
	y_pred = relu(x.mm(w1)).mm(w2)
	
	loss = (y_pred - y).pow(2).sum()
	print(t, loss.item())
	
	loss.backward()
	
	with torch.no_grad():
		w1 -= learning_rate * w1.grad
		w2 -= learning_rate * w2.grad
		
		w1.grad.zero_()
		w2.grad.zero_()

和Tensorflow的對比

這裏我們還是和前面一樣,實現一個隱層的全連接神經網絡,優化的目標函數是預測值和真實值的歐氏距離。這個實現使用基本的Tensorflow操作來構建一個計算圖,然後多次執行這個計算圖來訓練網絡。Tensorflow和PyTorch最大的區別之一就是Tensorflow使用靜態計算圖和PyTorch使用動態計算圖。在Tensorflow裏,我們首先構建計算圖,然後多次執行它。

import tensorflow as tf
import numpy as np

# 首先構建計算圖。

# N是batch大小;D_in是輸入大小。
# H是隱單元個數;D_out是輸出大小。
N, D_in, H, D_out = 64, 1000, 100, 10

# 輸入和輸出是placeholder,在用session執行graph的時候
# 我們會feed進去一個batch的訓練數據。
x = tf.placeholder(tf.float32, shape=(None, D_in))
y = tf.placeholder(tf.float32, shape=(None, D_out))

# 創建變量,並且隨機初始化。 
# 在Tensorflow裏,變量的生命週期是整個session,因此適合用它來保存模型的參數。
w1 = tf.Variable(tf.random_normal((D_in, H)))
w2 = tf.Variable(tf.random_normal((H, D_out)))

# Forward pass:計算模型的預測值y_pred 
# 注意和PyTorch不同,這裏不會執行任何計算,
# 而只是定義了計算,後面用session.run的時候纔會真正的執行計算。
h = tf.matmul(x, w1)
h_relu = tf.maximum(h, tf.zeros(1))
y_pred = tf.matmul(h_relu, w2)

# 計算loss 
loss = tf.reduce_sum((y - y_pred) ** 2.0)

# 計算梯度。 
grad_w1, grad_w2 = tf.gradients(loss, [w1, w2])

# 使用梯度下降來更新參數。assign同樣也只是定義更新參數的操作,不會真正的執行。
# 在Tensorflow裏,更新操作是計算圖的一部分;
# 而在PyTorch裏,因爲是動態的”實時“的計算,
# 所以參數的更新只是普通的Tensor計算,不屬於計算圖的一部分。
learning_rate = 1e-6
new_w1 = w1.assign(w1 - learning_rate * grad_w1)
new_w2 = w2.assign(w2 - learning_rate * grad_w2)

# 計算圖構建好了之後,我們需要創建一個session來執行計算圖。
with tf.Session() as sess:
	# 首先需要用session初始化變量 
	sess.run(tf.global_variables_initializer())
	
	# 這是fake的訓練數據
	x_value = np.random.randn(N, D_in)
	y_value = np.random.randn(N, D_out)
	for _ in range(500):
		# 用session多次的執行計算圖。每次feed進去不同的數據。
		# 這裏是模擬的,實際應該每次feed一個batch的數據。
		# run的第一個參數是需要執行的計算圖的節點,它依賴的節點也會自動執行,
		# 因此我們不需要手動執行forward的計算。
		# run返回這些節點執行後的值,並且返回的是numpy array
		loss_value, _, _ = sess.run([loss, new_w1, new_w2],
				feed_dict={x: x_value, y: y_value})
		print(loss_value)

使用nn模塊來實現三層神經網絡

我們接下來使用nn模塊來實現這個簡單的全連接網絡。前面我們通過用Tensor和Operation等low-level API來創建 動態的計算圖,這裏我們使用更簡單的high-level API。

import torch
print(torch.__version__)

# N是batch size;D_in是輸入大小
# H是隱層的大小;D_out是輸出大小。
N, D_in, H, D_out = 64, 1000, 100, 10

# 創建隨機的Tensor作爲輸入和輸出
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

# 使用nn包來定義網絡。nn.Sequential是一個包含其它模塊(Module)的模塊。
# 每個Linear模塊使用線性函數來計算,它會內部創建需要的weight和bias。
model = torch.nn.Sequential(
	torch.nn.Linear(D_in, H),
	torch.nn.ReLU(),
	torch.nn.Linear(H, D_out),
)

# 常見的損失函數在nn包裏也有,不需要我們自己實現
loss_fn = torch.nn.MSELoss(size_average=False)

learning_rate = 1e-4
for t in range(500):
# 前向計算:通過x來計算y。Module對象會重寫__call__函數,
# 因此我們可以把它當成函數來調用。
y_pred = model(x)

# 計算loss 
loss = loss_fn(y_pred, y)
print(t, loss.item())

# 梯度清空,調用Sequential對象的zero_grad後所有裏面的變量都會清零梯度
model.zero_grad()

# 反向計算梯度。我們通過Module定義的變量都會計算梯度。
loss.backward()

# 更新參數,所有的參數都在model.paramenters()裏

with torch.no_grad():
	for param in model.parameters():
		param -= learning_rate * param.grad

使用optim包

前面我們使用nn模塊時是自己來更新模型參數的,PyTorch也提供了optim包,我們可以使用裏面的Optimizer來自動的更新模型參數。除了最基本的SGD算法,這個包也實現了常見的SGD+momentum, RMSProp, Adam等算法。

import torch

N, D_in, H, D_out = 64, 1000, 100, 10

x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

model = torch.nn.Sequential(
	torch.nn.Linear(D_in, H),
	torch.nn.ReLU(),
	torch.nn.Linear(H, D_out),
)
loss_fn = torch.nn.MSELoss(size_average=False)

# 使用Adam算法,需要提供模型的參數和learning rate 
learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(500): 
	y_pred = model(x)
	
	loss = loss_fn(y_pred, y)
	print(t, loss.item())
	
	# 梯度清零,原來調用的是model.zero_grad,現在調用的是optimizer的zero_grad
	optimizer.zero_grad()
	
	loss.backward()
	
	# 調用optimizer.step實現參數更新
	optimizer.step()

自定義nn模塊

對於複雜的網絡結構,我們可以通過基礎Module了自定義nn模塊。這樣的好處是用一個類來同樣管理,而且更容易複用代碼。

import torch


class TwoLayerNet(torch.nn.Module):
	def __init__(self, D_in, H, D_out):
		""" 在構造函數裏,我們定義兩個nn.Linear模塊,把它們保存到self裏。 """
		super(TwoLayerNet, self).__init__()
		self.linear1 = torch.nn.Linear(D_in, H)
		self.linear2 = torch.nn.Linear(H, D_out)
	
	def forward(self, x):
		""" 在forward函數裏,我們需要根據網絡結構來實現前向計算。 通常我們會上定義的模塊來計算。 """
		h_relu = self.linear1(x).clamp(min=0)
		y_pred = self.linear2(h_relu)
		return y_pred


N, D_in, H, D_out = 64, 1000, 100, 10

x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

model = TwoLayerNet(D_in, H, D_out)

criterion = torch.nn.MSELoss(size_average=False)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4)
for t in range(500): 
	y_pred = model(x)
	
	loss = criterion(y_pred, y)
	print(t, loss.item())
	
	optimizer.zero_grad()
	loss.backward()
	optimizer.step()

流程控制和參數共享

爲了展示PyTorch的動態圖的能力,我們這裏會實現一個很奇怪模型:這個全連接的網絡的隱層個數是個1到4之間的隨機數,而且這些網絡層的參數是共享的。

import random
import torch


class DynamicNet(torch.nn.Module):
	def __init__(self, D_in, H, D_out):
		""" 構造3個nn.Linear實例。 """
		super(DynamicNet, self).__init__()
		self.input_linear = torch.nn.Linear(D_in, H)
		self.middle_linear = torch.nn.Linear(H, H)
		self.output_linear = torch.nn.Linear(H, D_out)
	
	def forward(self, x):
		# 輸入和輸出層是固定的,但是中間層的個數是隨機的(0,1,2),
		# 並且中間層的參數是共享的。
		
		# 因爲每次計算的計算圖是動態(實時)構造的,
		# 所以我們可以使用普通的Python流程控制代碼比如for循環
		# 來實現。讀者可以嘗試一下怎麼用TensorFlow來實現。
		# 另外一點就是一個Module可以多次使用,這樣就
		# 可以實現參數共享。
		
		h_relu = self.input_linear(x).clamp(min=0)
		for _ in range(random.randint(0, 3)):
		h_relu = self.middle_linear(h_relu).clamp(min=0)
		y_pred = self.output_linear(h_relu)
		return y_pred


N, D_in, H, D_out = 64, 1000, 100, 10

x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

model = DynamicNet(D_in, H, D_out)

criterion = torch.nn.MSELoss(size_average=False)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9)
for t in range(500): 
	y_pred = model(x)
	
	loss = criterion(y_pred, y)
	print(t, loss.item())
	
	optimizer.zero_grad()
	loss.backward()
	optimizer.step()

遷移學習示例

在這個教程裏,我們會學習怎麼使用遷移學習來訓練模型。通常我們的訓練數據量不會很大,很難達到像ImageNet那樣上百萬的標註數據集。我們可以使用遷移學習來解決訓練數據不足的問題。遷移學習裏,我們根據訓練數據的多少通常可以採取如下方法:

  • 訓練數據很少

    那麼我們通常把一個pretraning的網絡的大部分固定住,然後只是把最後一個全連接層換成新的(最後一層通常是不一樣的,因爲分類的數量不同),然後只訓練這一層

  • 訓練數據較多

    我們可以把pretraining的網絡的前面一些層固定住,但後面的層不固定,把最後一層換新的,然後訓練

  • 訓練數據很多

    所有的pretraining的層都可以fine-tuning,只是用pretraining的參數作爲初始化參數。

首先我們引入依賴:

from __future__ import print_function, division

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import os
import copy

plt.ion()

加載數據

我們使用torchvision和torch.utils.data包來加載數據。我們要解決的問題是訓練一個模型來區分螞蟻和蜜蜂,每個類別我們大概有120個訓練數據,另外每個類有75個驗證數據。這是一個很小的訓練集,如果直接用一個神經網絡來訓練,效果會很差。現在我們用遷移學習來解決這個問題。數據可以在這裏下載,下載後請解壓到data目錄下。

# 訓練的時候會做數據增強和歸一化
# 而驗證的時候只做歸一化
data_transforms = {
	'train': transforms.Compose([
		transforms.RandomResizedCrop(224),
		transforms.RandomHorizontalFlip(),
		transforms.ToTensor(),
		transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
	]),
	'val': transforms.Compose([
		transforms.Resize(256),
		transforms.CenterCrop(224),
		transforms.ToTensor(),
		transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
	]),
}

data_dir = '../data/hymenoptera_data'
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
		data_transforms[x]) 
	for x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
		shuffle=True, num_workers=4)
	for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
class_names = image_datasets['train'].classes

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

可視化圖片

我們來顯示幾張圖片看看,下圖是一個batch的圖片,顯示的代碼如下:

def imshow(inp, title=None):
	inp = inp.numpy().transpose((1, 2, 0))
	mean = np.array([0.485, 0.456, 0.406])
	std = np.array([0.229, 0.224, 0.225])
	inp = std * inp + mean
	inp = np.clip(inp, 0, 1)
	plt.imshow(inp)
	if title is not None:
		plt.title(title)
	plt.pause(0.001)


# 得到一個batch的數據
inputs, classes = next(iter(dataloaders['train']))

# 把batch張圖片拼接成一個大圖
out = torchvision.utils.make_grid(inputs)

imshow(out, title=[class_names[x] for x in classes])

圖:遷移學習數據示例

訓練模型

現在我們來實現一個用於訓練模型的通用函數。這裏我們會演示怎麼實現:

  • learning rate的自適應
  • 保存最好的模型

在下面的函數中,參數scheduler是來自torch.optim.lr_scheduler的LR scheduler對象(_LRScheduler的子類)

def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
	since = time.time()
	
	best_model_wts = copy.deepcopy(model.state_dict())
	best_acc = 0.0
	
	for epoch in range(num_epochs):
		print('Epoch {}/{}'.format(epoch, num_epochs - 1))
		print('-' * 10)
		
		# 每個epoch都分爲訓練和驗證階段
		for phase in ['train', 'val']:
			if phase == 'train':
				scheduler.step()
				model.train()  # 訓練階段
			else:
				model.eval()   # 驗證階段
			
			running_loss = 0.0
			running_corrects = 0
			
			# 變量數據集
			for inputs, labels in dataloaders[phase]:
				inputs = inputs.to(device)
				labels = labels.to(device)
			
			# 參數梯度清空
			optimizer.zero_grad()
			
			# forward
			# 只有訓練的時候track用於梯度計算的歷史信息。
			with torch.set_grad_enabled(phase == 'train'):
				outputs = model(inputs)
				_, preds = torch.max(outputs, 1)
				loss = criterion(outputs, labels)
				
				# 如果是訓練,那麼需要backward和更新參數 
				if phase == 'train':
					loss.backward()
					optimizer.step()
			
			# 統計
			running_loss += loss.item() * inputs.size(0)
			running_corrects += torch.sum(preds == labels.data)
			
			epoch_loss = running_loss / dataset_sizes[phase]
			epoch_acc = running_corrects.double() / dataset_sizes[phase]
			
			print('{} Loss: {:.4f} Acc: {:.4f}'.format(
				phase, epoch_loss, epoch_acc))
			
			# 保存驗證集上的最佳模型
			if phase == 'val' and epoch_acc > best_acc:
				best_acc = epoch_acc
				best_model_wts = copy.deepcopy(model.state_dict())
			
			print()
	
	time_elapsed = time.time() - since
	print('Training complete in {:.0f}m {:.0f}s'.format(
		time_elapsed // 60, time_elapsed % 60))
	print('Best val Acc: {:4f}'.format(best_acc))
	
	# 加載最優模型
	model.load_state_dict(best_model_wts)
	return model

可視化預測結果的函數

def visualize_model(model, num_images=6):
	was_training = model.training
	model.eval()
	images_so_far = 0
	fig = plt.figure()
	
	with torch.no_grad():
		for i, (inputs, labels) in enumerate(dataloaders['val']):
			inputs = inputs.to(device)
			labels = labels.to(device)
			
			outputs = model(inputs)
			_, preds = torch.max(outputs, 1)
			
			for j in range(inputs.size()[0]):
				images_so_far += 1
				ax = plt.subplot(num_images//2, 2, images_so_far)
				ax.axis('off')
				ax.set_title('predicted: {}'.format(class_names[preds[j]]))
				imshow(inputs.cpu().data[j])
				
				if images_so_far == num_images:
					model.train(mode=was_training)
					return
		model.train(mode=was_training)

fine-tuning所有參數

我們首先加載一個預訓練的模型(imagenet上的resnet),因爲我們的類別數和imagenet不同,所以我們需要刪掉原來的全連接層,換成新的全連接層。這裏我們讓所有的模型參數都可以調整,包括新加的全連接層和預訓練的層。

model_ft = models.resnet18(pretrained=True)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, 2)

model_ft = model_ft.to(device)

criterion = nn.CrossEntropyLoss()

# 所有的參數都可以訓練
optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)

# 每7個epoch learning rate變爲原來的10% 
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)

model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler,
	num_epochs=25)

最終我們得到的分類準確率大概在94.7%。

fine-tuning最後一層參數

我們用可以固定住前面層的參數,只訓練最後一層。這比之前要快將近一倍,因爲反向計算梯度只需要計算最後一層。但是前向計算的時間是一樣的。

model_conv = torchvision.models.resnet18(pretrained=True)
for param in model_conv.parameters():
	param.requires_grad = False

# 新加的層默認requires_grad=True 
num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Linear(num_ftrs, 2)

model_conv = model_conv.to(device)

criterion = nn.CrossEntropyLoss()

# 值訓練最後一個全連接層。
optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9)

exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)

model_conv = train_model(model_conv, criterion, optimizer_conv,
	exp_lr_scheduler, num_epochs=25)

最終我們得到的分類準確率大概在96%。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章