PyTorch深度學(xué)習(xí)60分鐘(一)

寫(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。

VariableFunction 是相互聯(lián)系的, 并且它們構(gòu)建了一個(gè)非循環(huán)的圖, 編碼了一個(gè)完整的計(jì)算歷史信息. 每一個(gè) variable(變量)都有一個(gè) .grad_fn 屬性, 它引用了一個(gè)已經(jīng)創(chuàng)建了 VariableFunction (除了用戶創(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ò)程:
y = x + 2
z = 3 * y^2
out = \frac{1}{4} \sum_{i}^4{z_{i}}

所以在反向求導(dǎo)時(shí):
\nabla_{z_{i}} = \frac{\partial{out}}{\partial{z_{i}}} = \frac{1}{4}
\nabla_{y} = \frac{\partial{z_{i}}}{\partial{y}} = 6 * y
\nabla_{x} = \frac{\partial{y}}{\partial{x}} = 1

故而:

\nabla_{x} = \frac{\partial{out}}{\partial{x}} = \frac{1}{4} * 6 * y * 1 = 4.5



如果沒(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.Modulenn 中最重要的類(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ù)以及名稱。

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 ):

weight = weight - learning\_rate * gradient

可以使用簡(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ù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容