寫(xiě)在前面的話:這是跟著ApacheCN團(tuán)隊(duì)學(xué)習(xí)pytorch的學(xué)習(xí)筆記媳否,主要資源來(lái)自pytorch官網(wǎng)和ApacheCN社區(qū)
一想鹰、PyTorch 是什么?
它是一個(gè)基于 Python 的科學(xué)計(jì)算包, 其主要是為了解決兩類(lèi)場(chǎng)景:
- NumPy 的替代品, 以使用 GPU 的強(qiáng)大加速功能
- 一個(gè)深度學(xué)習(xí)研究平臺(tái), 提供最大的靈活性和速度
個(gè)人感覺(jué)石咬,其實(shí)無(wú)論是pytorch還是tensorflow其實(shí)都是幫忙解決了在GPU上的自動(dòng)求導(dǎo)問(wèn)題花颗,這是對(duì)我們這些深度學(xué)習(xí)使用者來(lái)說(shuō)最關(guān)鍵的。也就是說(shuō)西傀,通過(guò)這些框架斤寇,我們不用去過(guò)多地操心反向求導(dǎo)的過(guò)程,而是可以更多地專(zhuān)注于神經(jīng)網(wǎng)絡(luò)(或者說(shuō)深度學(xué)習(xí))的結(jié)構(gòu)等問(wèn)題拥褂。當(dāng)然娘锁,它們也提供了相應(yīng)的封裝來(lái)滿足一些基本需求。
二饺鹃、新手入門(mén)
對(duì)于深度學(xué)習(xí)來(lái)說(shuō)莫秆,一個(gè)比較重要的概念就是張量。數(shù)學(xué)上的定義是張量(Tensor)是一個(gè)定義在的一些向量空間和一些對(duì)偶空間的笛卡兒積上的多重線性映射悔详。而簡(jiǎn)單來(lái)說(shuō)其實(shí)就是矢量的推廣镊屎。在同構(gòu)的意義下,第零階張量為標(biāo)量茄螃,第一階張量為向量 (Vector)缝驳, 第二階張量則成為矩陣 (Matrix)。對(duì)我們來(lái)說(shuō)归苍,常用的其實(shí)也就3階和4階的張量(這里沒(méi)有把矩陣它們當(dāng)成張量)用狱,更高階張量其實(shí)也很難遇到。例如一張圖片就是3階張量拼弃,包括長(zhǎng)夏伊、寬和通道(通常是RGB3通道)。在使用時(shí)也會(huì)變成4階張量吻氧,因?yàn)闀?huì)有batch的值溺忧。(也就是有很多3階張量堆在一起)。
1.初識(shí)張量
Tensors 與 NumPy 的 ndarrays 非常相似, 除此之外還可以在 GPU 上使用張量來(lái)加速計(jì)算盯孙。
from __future__ import print_function
import torch
# 構(gòu)建一個(gè) 5x3 的矩陣
# 張量只是創(chuàng)建了鲁森,但是未初始化
# 可以看出,其實(shí)這里就是生成了一個(gè)5行3列的矩陣
x = torch.Tensor(5, 3)
print(x)
>>tensor([[ 0.0000, -2.0000, 0.0000],
[-2.0000, 0.0000, 0.0000],
[ 0.0000, 0.0000, 0.0000],
[ 0.0000, 0.0000, 0.0000],
[ 0.0000, 0.0000, 0.0000]])
# 獲取 size振惰,注:torch.Size 實(shí)際上是一個(gè) tuple(元組)歌溉,所以它支持所有 tuple 的操作。
print(x.size())
>>torch.Size([5, 3])
#PS:torch.Size 實(shí)際上是一個(gè) tuple(元組), 所以它支持所有 tuple(元組)的操作.
2.基本操作
加操作:
#第一種寫(xiě)法
y = torch.rand(5, 3)
print(x + y)
>>tensor([[ 0.8042, -1.5267, 0.5508],
[-1.0805, 0.2719, 0.9532],
[ 0.8435, 0.5595, 0.8556],
[ 0.6867, 0.8612, 0.7824],
[ 0.9080, 0.1819, 0.2504]])
#這里的y就是tensor([[ 0.8042, 0.4733, 0.5508],
# [ 0.9195, 0.2719, 0.9532],
# [ 0.8435, 0.5595, 0.8556],
# [ 0.6867, 0.8612, 0.7824],
# [ 0.9080, 0.1819, 0.2504]])
#
# x是第一部分代碼中的x
#第二種寫(xiě)法
print(torch.add(x, y))
>>tensor([[ 0.8042, -1.5267, 0.5508],
[-1.0805, 0.2719, 0.9532],
[ 0.8435, 0.5595, 0.8556],
[ 0.6867, 0.8612, 0.7824],
[ 0.9080, 0.1819, 0.2504]])
#第三種寫(xiě)法报账,提供一個(gè)輸出 tensor 作為參數(shù)
result = torch.Tensor(5, 3)
torch.add(x, y, out = result)
print(result)
>>>>tensor([[ 0.8042, -1.5267, 0.5508],
[-1.0805, 0.2719, 0.9532],
[ 0.8435, 0.5595, 0.8556],
[ 0.6867, 0.8612, 0.7824],
[ 0.9080, 0.1819, 0.2504]])
#第四種寫(xiě)法研底,in-place(就地操作)
# adds x to y
y.add_(x)
print(y)
>>>>>>tensor([[ 0.8042, -1.5267, 0.5508],
[-1.0805, 0.2719, 0.9532],
[ 0.8435, 0.5595, 0.8556],
[ 0.6867, 0.8612, 0.7824],
[ 0.9080, 0.1819, 0.2504]])
索引(類(lèi)似Numpy的索引):
print(x[:, 1])
>>tensor([-2.0000, 0.0000, 0.0000, 0.0000, 0.0000])
改變大小:
x = torch.randn(4, 4)
>>tensor([[ 0.2755, -0.1519, 0.0257, -0.7659],
[ 0.7431, -1.0414, 0.5645, -1.0806],
[ 0.7274, -0.5298, -1.5444, -0.2279],
[-0.9928, -1.0443, 0.4778, -0.2496]])
y = x.view(16)
>>tensor([ 0.2755, -0.1519, 0.0257, -0.7659, 0.7431, -1.0414, 0.5645,
-1.0806, 0.7274, -0.5298, -1.5444, -0.2279, -0.9928, -1.0443,
0.4778, -0.2496])
z = x.view(-1, 8) # -1就是根據(jù)情況,由計(jì)算機(jī)自己推斷這個(gè)維數(shù)
>>tensor([[ 0.2755, -0.1519, 0.0257, -0.7659, 0.7431, -1.0414, 0.5645,
-1.0806],
[ 0.7274, -0.5298, -1.5444, -0.2279, -0.9928, -1.0443, 0.4778,
-0.2496]])
3.NumPy Bridge
將一個(gè) Torch Tensor 轉(zhuǎn)換為 NumPy 數(shù)組, 反之亦然透罢。
Torch Tensor 和 NumPy 數(shù)組將會(huì)共享它們的實(shí)際的內(nèi)存位置, 改變一個(gè)另一個(gè)也會(huì)跟著改變榜晦。
#轉(zhuǎn)換一個(gè) Torch Tensor 為 NumPy 數(shù)組
a = torch.ones(5)
print(a)
>>tensor([ 1., 1., 1., 1., 1.])
b = a.numpy()
print(b)
>>array([1., 1., 1., 1., 1.], dtype=float32)
#盡管轉(zhuǎn)換了,但是兩者依然共享內(nèi)存
a.add_(1)
print(a)
print(b)
>>tensor([ 2., 2., 2., 2., 2.])
>>[2. 2. 2. 2. 2.]
#轉(zhuǎn)換 NumPy 數(shù)組為 Torch Tensor
import numpy as np
a = np.ones(5)
>>array([1., 1., 1., 1., 1.])
b = torch.from_numpy(a)
>>tensor([ 1., 1., 1., 1., 1.], dtype=torch.float64)
#同樣兩者共享內(nèi)存
np.add(a, 1, out = a)
print(a)
print(b)
>>[2. 2. 2. 2. 2.]
>>tensor([ 2., 2., 2., 2., 2.], dtype=torch.float64)
Note:
除了 CharTensor 之外, CPU 上的所有 Tensor 都支持與Numpy進(jìn)行互相轉(zhuǎn)換
4.CUDA Tensors
可以使用 .cuda 方法將 Tensors 在GPU上運(yùn)行.
# 只要在 CUDA 是可用的情況下, 我們可以運(yùn)行這段代碼
if torch.cuda.is_available():
x = x.cuda()
y = y.cuda()
x + y
三羽圃、自動(dòng)求導(dǎo)
PyTorch 中所有神經(jīng)網(wǎng)絡(luò)的核心是 autograd
自動(dòng)求導(dǎo)包. 我們先來(lái)簡(jiǎn)單介紹一下, 然后我們會(huì)去訓(xùn)練我們的第一個(gè)神經(jīng)網(wǎng)絡(luò)乾胶。
autograd
自動(dòng)求導(dǎo)包針對(duì)張量上的所有操作都提供了自動(dòng)微分操作. 這是一個(gè)逐個(gè)運(yùn)行的框架, 這意味著您的反向傳播是由您的代碼如何運(yùn)行來(lái)定義的, 每個(gè)單一的迭代都可以不一樣。
1.Variable(變量)
autograd.Variable
是包的核心類(lèi). 它包裝了張量, 并且支持幾乎所有的操作. 一旦你完成了你的計(jì)算, 你就可以調(diào)用 .backward()
方法, 然后所有的梯度計(jì)算會(huì)自動(dòng)進(jìn)行朽寞。
pytorch允許通過(guò) .data
屬性來(lái)訪問(wèn)原始的張量, 而關(guān)于該 variable
(變量)的梯度會(huì)被累計(jì)到 .grad
上去识窿。
還有一個(gè)針對(duì)自動(dòng)求導(dǎo)實(shí)現(xiàn)來(lái)說(shuō)非常重要的類(lèi) - Function
。
Variable
和 Function
是相互聯(lián)系的, 并且它們構(gòu)建了一個(gè)非循環(huán)的圖, 編碼了一個(gè)完整的計(jì)算歷史信息. 每一個(gè) variable
(變量)都有一個(gè) .grad_fn
屬性, 它引用了一個(gè)已經(jīng)創(chuàng)建了 Variable
的 Function
(除了用戶創(chuàng)建的 Variable
之外 - 它們的 grad_fn is None
)
如果你想計(jì)算導(dǎo)數(shù), 你可以在 Variable
上調(diào)用 .backward()
方法. 如果 Variable
是標(biāo)量的形式(例如, 它包含一個(gè)元素?cái)?shù)據(jù)), 你不必指定任何參數(shù)給 backward()
, 但是, 如果它有更多的元素. 你需要去指定一個(gè) grad_output
參數(shù), 該參數(shù)是一個(gè)匹配 shape
(形狀)的張量脑融。
import torch
from torch.autograd import Variable
#創(chuàng)建 variable(變量)
x = Variable(torch.ones(2, 2), requires_grad = True)
print(x)
>>tensor([[ 1., 1.],
[ 1., 1.]])
#y 由操作創(chuàng)建,所以它有 grad_fn 屬性.
y = x + 2
print(y)
>>tensor([[ 3., 3.],
[ 3., 3.]])
z = y * y * 3
out = z.mean()
print(z, out)
>>tensor([[ 27., 27.],
[ 27., 27.]])
>>tensor(27.)
2.梯度
pytorch之類(lèi)的框架對(duì)于我們學(xué)習(xí)者來(lái)說(shuō)最大的幫助莫過(guò)于反向求導(dǎo)的簡(jiǎn)單化喻频。
我們考慮上述例子的反向求導(dǎo)過(guò)程,首先肘迎,先寫(xiě)出整體的前向過(guò)程:
所以在反向求導(dǎo)時(shí):
故而:
如果沒(méi)有框架甥温,單純編寫(xiě)這段代碼其實(shí)比較的繁瑣。而在使用了pytorch框架后妓布,只需要調(diào)用out.backward()
姻蚓,pytorch就會(huì)自動(dòng)求導(dǎo)其導(dǎo)數(shù),將其存放在.grad
中:
out.backward()
print(x.grad)
>>tensor([[ 4.5000, 4.5000],
[ 4.5000, 4.5000]])
同時(shí)匣沼,梯度的有趣應(yīng)用:
x = torch.randn(3)
x = Variable(x, requires_grad = True)
y = x * 2
gradients = torch.FloatTensor([0.1, 1.0, 0.0001])
y.backward(gradients)
print(x.grad)
>>tensor([ 0.2000, 2.0000, 0.0002])
四狰挡、實(shí)戰(zhàn)之基本的卷積神經(jīng)網(wǎng)絡(luò)
神經(jīng)網(wǎng)絡(luò)可以使用 torch.nn
包構(gòu)建。
autograd
實(shí)現(xiàn)了反向傳播功能, 但是直接用來(lái)寫(xiě)深度學(xué)習(xí)的代碼在很多情況下還是稍顯復(fù)雜, torch.nn
是專(zhuān)門(mén)為神經(jīng)網(wǎng)絡(luò)設(shè)計(jì)的模塊化接口. nn
構(gòu)建于 Autograd
之上, 可用來(lái)定義和運(yùn)行神經(jīng)網(wǎng)絡(luò). nn.Module
是 nn
中最重要的類(lèi), 可把它看成是一個(gè)網(wǎng)絡(luò)的封裝, 包含網(wǎng)絡(luò)各層定義以及 forward
方法, 調(diào)用 forward(input)
方法, 可返回前向傳播的結(jié)果释涛。
1.定義一個(gè)網(wǎng)絡(luò)
import torch
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 卷積層 '1'表示輸入圖片為單通道, '6'表示輸出通道數(shù), '5'表示卷積核為5*5
# 核心
# 初始化的過(guò)程中其實(shí)沒(méi)有再定義網(wǎng)絡(luò)結(jié)構(gòu)加叁,只是定義了一些函數(shù)
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
# 仿射層/全連接層: y = Wx + b
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
# 這里的前向過(guò)程才定義了整個(gè)網(wǎng)絡(luò)結(jié)構(gòu)
def forward(self, x):
# 在由多個(gè)輸入平面組成的輸入信號(hào)上應(yīng)用2D最大池化.
# (2, 2) 代表的是池化操作的步幅
# 這里正是從輸入層,通過(guò)一個(gè)卷積之后枢贿,在經(jīng)過(guò)一個(gè)pool
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# 如果大小是正方形, 則只能指定一個(gè)數(shù)字
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
# 這邊便是將x拉值殉农,以便用于全連接
x = x.view(-1, self.num_flat_features(x))
# 接下來(lái)就是普通的兩個(gè)全連接層
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:] # 除批量維度外的所有維度
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)
)
只要在 nn.Module
的子類(lèi)中定義了 forward
函數(shù), backward
函數(shù)就會(huì)自動(dòng)被實(shí)現(xiàn)(利用 autograd
)。 在 forward
函數(shù)中可使用任何 Tensor
支持的操作局荚。
并不像tensorflow需要顯式地定義參數(shù)超凳,pytorch在上述過(guò)程中只需要用戶輸入維度信息,參數(shù)的維度便可由計(jì)算機(jī)自動(dòng)給出耀态。網(wǎng)絡(luò)的可學(xué)習(xí)參數(shù)通過(guò) net.parameters()
返回,net.named_parameters
可同時(shí)返回學(xué)習(xí)的參數(shù)以及名稱(chēng)轮傍。
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1的weight
>>10
>>torch.Size([6, 1, 5, 5])
向前的輸入是一個(gè) autograd.Variable, 輸出也是如此。注意: 這個(gè)網(wǎng)絡(luò)(LeNet)的預(yù)期輸入大小是 32x32, 使用這個(gè)網(wǎng)上 MNIST 數(shù)據(jù)集, 請(qǐng)將數(shù)據(jù)集中的圖像調(diào)整為 32x32
input = Variable(torch.randn(1, 1, 32, 32))
out = net(input)
print(out)
>>tensor([[-0.0821, 0.1081, 0.0103, 0.1502, 0.0191, 0.0097, -0.0175,
-0.0804, -0.0055, -0.0382]])
Note:
- torch.nn 只支持小批量(mini-batches), 不支持一次輸入一個(gè)樣本, 即一次必須是一個(gè) batch
- nn.Conv2d 的輸入必須是 4 維的, 形如 nSamples x nChannels x Height x Width
2.損失函數(shù)
損失函數(shù)采用 (output, target) 輸入對(duì), 并計(jì)算預(yù)測(cè)輸出結(jié)果與實(shí)際目標(biāo)的距離首装。
在 nn 包下有幾種不同的損失函數(shù)创夜。一個(gè)簡(jiǎn)單的損失函數(shù)是: nn.MSELoss
計(jì)算輸出和目標(biāo)之間的均方誤差
output = net(input)
target = Variable(torch.arange(1, 11)) # 一個(gè)虛擬的目標(biāo)
criterion = nn.MSELoss()
loss = criterion(output, target)
print(loss)
現(xiàn)在, 如果你沿著 loss 反向傳播的方向使用.grad_fn
屬性, 你將會(huì)看到一個(gè)如下所示的計(jì)算圖:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
所以, 當(dāng)我們調(diào)用loss.backward()
, 整個(gè)圖與損失是有區(qū)別的, 圖中的所有變量都將用 .grad 梯度累加它們的變量。
3.反向傳播
為了反向傳播誤差, 我們所要做的就是 loss.backward()
仙逻。你需要清除現(xiàn)有的梯度, 否則梯度會(huì)累加之前的梯度驰吓。
現(xiàn)在我們使用 loss.backward()
, 看看反向傳播之前和之后 conv1
的梯度涧尿。
net.zero_grad() # 把之前的梯度清零
print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)
>>None
loss.backward()
print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)
>>tensor([ 0.1580, -0.0348, -0.1106, 0.0706, -0.0937, -0.0539])
4.更新權(quán)重
實(shí)踐中使用的最簡(jiǎn)單的更新規(guī)則是隨機(jī)梯度下降( SGD ):
可以使用簡(jiǎn)單的 python 代碼來(lái)實(shí)現(xiàn)這個(gè):
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)
當(dāng)然,更新權(quán)重的方法pytorch也已經(jīng)做了封裝(torch.optim
)檬贰,方便我們調(diào)用:
import torch.optim as optim
# 新建一個(gè)優(yōu)化器, 指定要調(diào)整的參數(shù)和學(xué)習(xí)率
optimizer = optim.SGD(net.parameters(), lr = 0.01)
# 在訓(xùn)練過(guò)程中:
optimizer.zero_grad() # 首先梯度清零(與 net.zero_grad() 效果一樣)
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step() # 更新參數(shù)