從頭開始瞭解PyTorch的簡單實現

機器之心發表於2018-04-11

本教程展示瞭如何從瞭解張量開始到使用 PyTorch 訓練簡單的神經網路,是非常基礎的 PyTorch 入門資源。PyTorch 建立在 Python 和 Torch 庫之上,並提供了一種類似 Numpy 的抽象方法來表徵張量(或多維陣列),它還能利用 GPU 來提升效能。本教程的程式碼並不完整,詳情請檢視原 Jupyter Notebook 文件。

PyTorch 使入門深度學習變得簡單,即使你這方面的背景知識不太充足。至少,知道多層神經網路模型可視為由權重連線的節點圖就是有幫助的,你可以基於前向和反向傳播,利用優化過程(如梯度計算)從資料中估計權重。

  • 必備知識:該教程假設讀者熟悉 Python 和 NumPy。

  • 必備軟體:在執行原 Jupyter Notebook 之前你需要安裝 PyTorch。原 Notebook 有程式碼單元格可供驗證你是否做好準備。

  • 必備硬體:你需要安裝 NVIDIA GPU 和 CUDA SDK。據報告此舉可能實現 10-100 的加速。當然,如果你沒有進行此設定,那麼你仍然可以在僅使用 CPU 的情況下執行 PyTorch。但是,記住,在訓練神經網路模型時,生命苦短!所以還是儘可能使用 GPU 吧!

專案地址:https://github.com/hpcgarage/accelerated_dl_pytorch

1. 必要的 PyTorch 背景

  • PyTorch 是一個建立在 Torch 庫之上的 Python 包,旨在加速深度學習應用。

  • PyTorch 提供一種類似 NumPy 的抽象方法來表徵張量(或多維陣列),它可以利用 GPU 來加速訓練。

從頭開始瞭解PyTorch的簡單實現

1.1 PyTorch 張量

PyTorch 的關鍵資料結構是張量,即多維陣列。其功能與 NumPy 的 ndarray 物件類似,如下我們可以使用 torch.Tensor() 建立張量。

# Generate a 2-D pytorch tensor (i.e., a matrix) pytorch_tensor = torch.Tensor(10, 20) print("type: ", type(pytorch_tensor), " and size: ", pytorch_tensor.shape )

如果你需要一個相容 NumPy 的表徵,或者你想從現有的 NumPy 物件中建立一個 PyTorch 張量,那麼就很簡單了。

# Convert the pytorch tensor to a numpy array: numpy_tensor = pytorch_tensor.numpy() print("type: ", type(numpy_tensor), " and size: ", numpy_tensor.shape) # Convert the numpy array to Pytorch Tensor: print("type: ", type(torch.Tensor(numpy_tensor)), " and size: ", torch.Tensor(numpy_tensor).shape)

1.2 PyTorch vs. NumPy

PyTorch 並不是 NumPy 的簡單替代品,但它實現了很多 NumPy 功能。其中有一個不便之處是其命名規則,有時候它和 NumPy 的命名方法相當不同。我們來舉幾個例子說明其中的區別:

1 張量建立

t = torch.rand(2, 4, 3, 5) a = np.random.rand(2, 4, 3, 5)

2 張量分割

t = torch.rand(2, 4, 3, 5) a = t.numpy() pytorch_slice = t[0, 1:3, :, 4] numpy_slice =  a[0, 1:3, :, 4] print ('Tensor[0, 1:3, :, 4]:\n', pytorch_slice) print ('NdArray[0, 1:3, :, 4]:\n', numpy_slice) ------------------------------------------------------------------------- Tensor[0, 1:3, :, 4]: 0.2032  0.1594  0.3114 0.9073  0.6497  0.2826 [torch.FloatTensor of size 2x3] NdArray[0, 1:3, :, 4]: [[ 0.20322084  0.15935552  0.31143939] [ 0.90726137  0.64966112  0.28259504]]

3 張量 Masking

t = t - 0.5 a = t.numpy() pytorch_masked = t[t > 0] numpy_masked = a[a > 0]

4 張量重塑

pytorch_reshape = t.view([6, 5, 4]) numpy_reshape = a.reshape([6, 5, 4])

1.3 PyTorch 變數

  • PyTorch 張量的簡單封裝

  • 幫助建立計算圖

  • Autograd(自動微分庫)的必要部分

  • 將關於這些變數的梯度儲存在 .grad 中

從頭開始瞭解PyTorch的簡單實現

結構圖:

從頭開始瞭解PyTorch的簡單實現

計算圖和變數:在 PyTorch 中,神經網路會使用相互連線的變數作為計算圖來表示。PyTorch 允許通過程式碼構建計算圖來構建網路模型;之後 PyTorch 會簡化估計模型權重的流程,例如通過自動計算梯度的方式。

舉例來說,假設我們想構建兩層模型,那麼首先要為輸入和輸出建立張量變數。我們可以將 PyTorch Tensor 包裝進 Variable 物件中:

from torch.autograd import Variable import torch.nn.functional as F x = Variable(torch.randn(4, 1), requires_grad=False) y = Variable(torch.randn(3, 1), requires_grad=False)

我們把 requires_grad 設定為 True,表明我們想要自動計算梯度,這將用於反向傳播中以優化權重。

現在我們來定義權重:

w1 = Variable(torch.randn(5, 4), requires_grad=True) w2 = Variable(torch.randn(3, 5), requires_grad=True)

訓練模型:

def model_forward(x):    return F.sigmoid(w2 @ F.sigmoid(w1 @ x)) print (w1) print (w1.data.shape) print (w1.grad) # Initially, non-existent ------------------------------------------------------------------------- Variable containing: 1.6068 -1.3304 -0.6717 -0.6097 -0.3414 -0.5062 -0.2533  1.0260 -0.0341 -1.2144 -1.5983 -0.1392 -0.5473  0.0084  0.4054  0.0970 0.3596  0.5987 -0.0324  0.6116 [torch.FloatTensor of size 5x4] torch.Size([5, 4]) None

1.4 PyTorch 反向傳播

這樣我們有了輸入和目標、模型權重,那麼是時候訓練模型了。我們需要三個元件:

損失函式:描述我們模型的預測距離目標還有多遠;

import torch.nn as nn criterion = nn.MSELoss()

優化演算法:用於更新權重;

import torch.optim as optim optimizer = optim.SGD([w1, w2], lr=0.001)

反向傳播步驟:

for epoch in range(10):    loss = criterion(model_forward(x), y)    optimizer.zero_grad() # Zero-out previous gradients    loss.backward() # Compute new gradients    optimizer.step() # Apply these gradients print (w1) ------------------------------------------------------------------------- Variable containing: 1.6067 -1.3303 -0.6717 -0.6095 -0.3414 -0.5062 -0.2533  1.0259 -0.0340 -1.2145 -1.5983 -0.1396 -0.5476  0.0085  0.4055  0.0976 0.3597  0.5986 -0.0324  0.6113 [torch.FloatTensor of size 5x4]

1.5 PyTorch CUDA 介面

PyTorch 的優勢之一是為張量和 autograd 庫提供 CUDA 介面。使用 CUDA GPU,你不僅可以加速神經網路訓練和推斷,還可以加速任何對映至 PyTorch 張量的工作負載。

你可以呼叫 torch.cuda.is_available() 函式,檢查 PyTorch 中是否有可用 CUDA。

cuda_gpu = torch.cuda.is_available() if (cuda_gpu):    print("Great, you have a GPU!") else:    print("Life is short -- consider a GPU!")

很好,現在你有 GPU 了。

.cuda()

之後,使用 cuda 加速程式碼就和呼叫一樣簡單。如果你在張量上呼叫 .cuda(),則它將執行從 CPU 到 CUDA GPU 的資料遷移。如果你在模型上呼叫 .cuda(),則它不僅將所有內部儲存移到 GPU,還將整個計算圖對映至 GPU。

要想將張量或模型複製回 CPU,比如想和 NumPy 互動,你可以呼叫 .cpu()。

if cuda_gpu:    x = x.cuda()    print(type(x.data)) x = x.cpu() print(type(x.data)) ------------------------------------------------------------------------- <class 'torch.cuda.FloatTensor'> <class 'torch.FloatTensor'>

我們來定義兩個函式(訓練函式和測試函式)來使用我們的模型執行訓練和推斷任務。該程式碼同樣來自 PyTorch 官方教程,我們摘選了所有訓練/推斷的必要步驟。

對於訓練和測試網路,我們需要執行一系列動作,這些動作可直接對映至 PyTorch 程式碼:

1. 我們將模型轉換到訓練/推斷模式;

2. 我們通過在資料集上成批獲取影像,以迭代訓練模型;

3. 對於每一個批量的影像,我們都要載入資料和標註,執行網路的前向步驟來獲取模型輸出;

4. 我們定義損失函式,計算每一個批量的模型輸出和目標之間的損失;

5. 訓練時,我們初始化梯度為零,使用上一步定義的優化器和反向傳播,來計算所有與損失有關的層級梯度;

6. 訓練時,我們執行權重更新步驟。

def train(model, epoch, criterion, optimizer, data_loader):    model.train()    for batch_idx, (data, target) in enumerate(data_loader):        if cuda_gpu:            data, target = data.cuda(), target.cuda()            model.cuda()        data, target = Variable(data), Variable(target)        output = model(data)        optimizer.zero_grad()        loss = criterion(output, target)        loss.backward()        optimizer.step()        if (batch_idx+1) % 400 == 0:            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(                epoch, (batch_idx+1) * len(data), len(data_loader.dataset),                100. * (batch_idx+1) / len(data_loader), loss.data[0])) def test(model, epoch, criterion, data_loader):    model.eval()    test_loss = 0    correct = 0    for data, target in data_loader:        if cuda_gpu:            data, target = data.cuda(), target.cuda()            model.cuda()        data, target = Variable(data), Variable(target)        output = model(data)        test_loss += criterion(output, target).data[0]        pred = output.data.max(1)[1] # get the index of the max log-probability        correct += pred.eq(target.data).cpu().sum()    test_loss /= len(data_loader) # loss function already averages over batch size    acc = correct / len(data_loader.dataset)    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(        test_loss, correct, len(data_loader.dataset), 100. * acc))    return (acc, test_loss)

現在介紹完畢,讓我們開始這次資料科學之旅吧!

2. 使用 PyTorch 進行資料分析

  • 使用 torch.nn 庫構建模型

  • 使用 torch.autograd 庫訓練模型

  • 將資料封裝進 torch.utils.data.Dataset 庫

  • 使用 NumPy interface 連線你的模型、資料和你最喜歡的工具

在檢視複雜模型之前,我們先來看個簡單的:簡單合成資料集上的線性迴歸,我們可以使用 sklearn 工具生成這樣的合成資料集。

from sklearn.datasets import make_regression import seaborn as sns import pandas as pd import matplotlib.pyplot as plt sns.set() x_train, y_train, W_target = make_regression(n_samples=100, n_features=1, noise=10, coef = True) df = pd.DataFrame(data = {'X':x_train.ravel(), 'Y':y_train.ravel()}) sns.lmplot(x='X', y='Y', data=df, fit_reg=True) plt.show() x_torch = torch.FloatTensor(x_train) y_torch = torch.FloatTensor(y_train) y_torch = y_torch.view(y_torch.size()[0], 1)

從頭開始瞭解PyTorch的簡單實現

PyTorch 的 nn 庫中有大量有用的模組,其中一個就是線性模組。如名字所示,它對輸入執行線性變換,即線性迴歸。

class LinearRegression(torch.nn.Module):    def __init__(self, input_size, output_size):        super(LinearRegression, self).__init__()        self.linear = torch.nn.Linear(input_size, output_size)      def forward(self, x):        return self.linear(x) model = LinearRegression(1, 1)

要訓練線性迴歸,我們需要從 nn 庫中新增合適的損失函式。對於線性迴歸,我們將使用 MSELoss()——均方差損失函式。

我們還需要使用優化函式(SGD),並執行與之前示例類似的反向傳播。本質上,我們重複上文定義的 train() 函式中的步驟。不能直接使用該函式的原因是我們實現它的目的是分類而不是迴歸,以及我們使用交叉熵損失和最大元素的索引作為模型預測。而對於線性迴歸,我們使用線性層的輸出作為預測。

criterion = torch.nn.MSELoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.1)   for epoch in range(50):    data, target = Variable(x_torch), Variable(y_torch)    output = model(data)    optimizer.zero_grad()    loss = criterion(output, target)    loss.backward()    optimizer.step() predicted = model(Variable(x_torch)).data.numpy()

現在我們可以列印出原始資料和適合 PyTorch 的線性迴歸。

plt.plot(x_train, y_train, 'o', label='Original data') plt.plot(x_train, predicted, label='Fitted line') plt.legend() plt.show()

從頭開始瞭解PyTorch的簡單實現

為了轉向更復雜的模型,我們下載了 MNIST 資料集至「datasets」資料夾中,並測試一些 PyTorch 中可用的初始預處理。PyTorch 具備資料載入器和處理器,可用於不同的資料集。資料集下載好後,你可以隨時使用。你還可以將資料包裝進 PyTorch 張量,建立自己的資料載入器類別。

批大小(batch size)是機器學習中的術語,指一次迭代中使用的訓練樣本數量。批大小可以是以下三種之一:

  • batch 模式:批大小等於整個資料集,因此迭代和 epoch 值一致;

  • mini-batch 模式:批大小大於 1 但小於整個資料集的大小。通常,數量可以是能被整個資料集整除的值。

  • 隨機模式:批大小等於 1。因此梯度和神經網路引數在每個樣本之後都要更新。

from torchvision import datasets, transforms batch_num_size = 64 train_loader = torch.utils.data.DataLoader(    datasets.MNIST('data',train=True, download=True, transform=transforms.Compose([        transforms.ToTensor(),        transforms.Normalize((0.1307,), (0.3081,))    ])),    batch_size=batch_num_size, shuffle=True) test_loader = torch.utils.data.DataLoader(    datasets.MNIST('data',train=False, transform=transforms.Compose([        transforms.ToTensor(),        transforms.Normalize((0.1307,), (0.3081,))    ])),    batch_size=batch_num_size, shuffle=True)

3. PyTorch 中的 LeNet 卷積神經網路(CNN)

現在我們從頭開始建立第一個簡單神經網路。該網路要執行影像分類,識別 MNIST 資料集中的手寫數字。這是一個四層的卷積神經網路(CNN),一種分析 MNIST 資料集的常見架構。該程式碼來自 PyTorch 官方教程,你可以在這裡(http://pytorch.org/tutorials/)找到更多示例。

我們將使用 torch.nn 庫中的多個模組:

1. 線性層:使用層的權重對輸入張量執行線性變換;

2. Conv1 和 Conv2:卷積層,每個層輸出在卷積核(小尺寸的權重張量)和同樣尺寸輸入區域之間的點積;

3. Relu:修正線性單元函式,使用逐元素的啟用函式 max(0,x);

4. 池化層:使用 max 運算執行特定區域的下采樣(通常 2x2 畫素);

5. Dropout2D:隨機將輸入張量的所有通道設為零。當特徵圖具備強相關時,dropout2D 提升特徵圖之間的獨立性;

6. Softmax:將 Log(Softmax(x)) 函式應用到 n 維輸入張量,以使輸出在 0 到 1 之間。

class LeNet(nn.Module):    def __init__(self):        super(LeNet,self).__init__()        self.conv1 = nn.Conv2d(1,10,kernel_size=5)        self.conv2 = nn.Conv2d(10,20,kernel_size=5)        self.conv2_drop = nn.Dropout2d()        self.fc1 = nn.Linear(320,50)        self.fc2 = nn.Linear(50,10)    def forward(self,x):        x = F.relu(F.max_pool2d(self.conv1(x),2))        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)),2))        x = x.view(-1, 320)        x = F.relu(self.fc1(x))        x = F.dropout(x, training=self.training)        x = self.fc2(x)        return F.log_softmax(x, dim=1)

建立 LeNet 類後,建立物件並移至 GPU:

model = LeNet() if cuda_gpu:    model.cuda() print ('MNIST_net model:\n') print (model) ------------------------------------------------------------------------- MNIST_net model: LeNet(  (conv1): Conv2d(1, 10, kernel_size=(5, 5), stride=(1, 1))  (conv2): Conv2d(10, 20, kernel_size=(5, 5), stride=(1, 1))  (conv2_drop): Dropout2d(p=0.5)  (fc1): Linear(in_features=320, out_features=50, bias=True)  (fc2): Linear(in_features=50, out_features=10, bias=True) )

要訓練該模型,我們需要使用帶動量的 SGD,學習率為 0.01,momentum 為 0.5。

criterion = nn.CrossEntropyLoss()   optimizer = optim.SGD(model.parameters(),lr = 0.005, momentum = 0.9)

僅僅需要 5 個 epoch(一個 epoch 意味著你使用整個訓練資料集來更新訓練模型的權重),我們就可以訓練出一個相當準確的 LeNet 模型。這段程式碼檢查可以確定檔案中是否已有預訓練好的模型。有則載入;無則訓練一個並儲存至磁碟。

import os epochs = 5 if (os.path.isfile('pretrained/MNIST_net.t7')):    print ('Loading model')    model.load_state_dict(torch.load('pretrained/MNIST_net.t7', map_location=lambda storage, loc: storage))    acc, loss = test(model, 1, criterion, test_loader) else:    print ('Training model')    for epoch in range(1, epochs + 1):        train(model, epoch, criterion, optimizer, train_loader)        acc, loss = test(model, 1, criterion, test_loader)    torch.save(model.state_dict(), 'pretrained/MNIST_net.t7') ------------------------------------------------------------------------- Loading model Test set: Average loss: 0.0471, Accuracy: 9859/10000 (99%)

現在我們來看下模型。首先,列印出該模型的資訊。列印函式顯示所有層(如 Dropout 被實現為一個單獨的層)及其名稱和引數。同樣有一個迭代器在模型中所有已命名模組之間執行。當你具備一個包含多個「內部」模型的複雜 DNN 時,這有所幫助。在所有已命名模組之間的迭代允許我們建立模型解析器,可讀取模型引數、建立與該網路類似的模組。

print ('Internal models:') for idx, m in enumerate(model.named_modules()):    print(idx, '->', m)    print ('-------------------------------------------------------------------------') #輸出: Internal models: 0 -> ('', LeNet(  (conv1): Conv2d(1, 10, kernel_size=(5, 5), stride=(1, 1))  (conv2): Conv2d(10, 20, kernel_size=(5, 5), stride=(1, 1))  (conv2_drop): Dropout2d(p=0.5)  (fc1): Linear(in_features=320, out_features=50, bias=True)  (fc2): Linear(in_features=50, out_features=10, bias=True) )) ------------------------------------------------------------------------- 1 -> ('conv1', Conv2d(1, 10, kernel_size=(5, 5), stride=(1, 1))) ------------------------------------------------------------------------- 2 -> ('conv2', Conv2d(10, 20, kernel_size=(5, 5), stride=(1, 1))) ------------------------------------------------------------------------- 3 -> ('conv2_drop', Dropout2d(p=0.5)) ------------------------------------------------------------------------- 4 -> ('fc1', Linear(in_features=320, out_features=50, bias=True)) ------------------------------------------------------------------------- 5 -> ('fc2', Linear(in_features=50, out_features=10, bias=True)) -------------------------------------------------------------------------

你可以使用 .cpu() 方法將張量移至 CPU(或確保它在那裡)。或者,當 GPU 可用時(torch.cuda. 可用),使用 .cuda() 方法將張量移至 GPU。你可以看到張量是否在 GPU 上,其型別為 torch.cuda.FloatTensor。如果張量在 CPU 上,則其型別為 torch.FloatTensor。

print (type(t.cpu().data)) if torch.cuda.is_available():    print ("Cuda is available")    print (type(t.cuda().data)) else:    print ("Cuda is NOT available") ------------------------------------------------------------------------- <class 'torch.FloatTensor'> Cuda is available <class 'torch.cuda.FloatTensor'>

如果張量在 CPU 上,我們可以將其轉換成 NumPy 陣列,其共享同樣的記憶體位置,改變其中一個就會改變另一個。

if torch.cuda.is_available():    try:        print(t.data.numpy())    except RuntimeError as e:        "you can't transform a GPU tensor to a numpy nd array, you have to copy your weight tendor to cpu and then get the numpy array" print(type(t.cpu().data.numpy())) print(t.cpu().data.numpy().shape) print(t.cpu().data.numpy())

現在我們瞭解瞭如何將張量轉換成 NumPy 陣列,我們可以利用該知識使用 matplotlib 進行視覺化!我們來列印出第一個卷積層的卷積濾波器。

data = model.conv1.weight.cpu().data.numpy() print (data.shape) print (data[:, 0].shape) kernel_num = data.shape[0] fig, axes = plt.subplots(ncols=kernel_num, figsize=(2*kernel_num, 2)) for col in range(kernel_num):    axes[col].imshow(data[col, 0, :, :], cmap=plt.cm.gray) plt.show()

從頭開始瞭解PyTorch的簡單實現

以上是簡要的教程資源,還有更多的內容和實驗可以檢視原專案瞭解更多。

相關文章