使用PyTorch進行深度學習
譯者: bdqfork
作者: Robert Guthrie
深度學習構建模組:仿射對映, 非線性函式以及目標函式
深度學習表現為使用更高階的方法將線性函式和非線性函式進行組合。非線性函式的引入使得訓練出來的模型更加強大。在本節中,我們將學習這些核心元件,建立目標函式,並理解模型是如何構建的。
仿射對映
深度學習的核心元件之一是仿射對映,仿射對映是一個關於矩陣 A 和向量 x , b 的 f(x) 函式,如下所示:
需要訓練的引數就是該公式中的 A 和 b 。
PyTorch以及大多數的深度學習框架所做的事情都與傳統的線性代數有些不同。它的對映輸入是行而不是列。也就是說,下面程式碼輸出的第 i 行是輸入的第 i 行進行 A 變換,並加上偏移項的結果。看下面的例子:
# Author: Robert Guthrie import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim torch.manual_seed(1)
lin = nn.Linear(5, 3)# maps from R^5 to R^3, parameters A, b # data is 2x5.A maps from 5 to 3... can we map "data" under A? data = torch.randn(2, 5) print(lin(data))# yes
輸出:
tensor([[ 0.1755, -0.3268, -0.5069], [-0.6602,0.2260,0.1089]], grad_fn=<AddmmBackward>)
非線性函式
首先,注意以下這個例子,它將解釋為什麼我們需要非線性函式。假設我們有兩個仿射對映 f(x) = Ax + b 和 g(x) = Cx + d 。那麼 f(g(x)) 又是什麼呢?
AC 是一個矩陣, Ad + b 是一個向量,可以看出,兩個仿射對映的組合還是一個仿射對映。
由此可以看出,使用多個仿射對映的鏈式組合形成神經網路,並不會對提升模型的效能,和一個仿射對映沒什麼區別。
但是,如果我們在兩個仿射對映之間引入非線性,那麼結果就大不一樣了,我們可以構建出一個高效能的模型。
最常用的核心的非線性函式有: tanh(x) , σ(x) , ReLU(x) 。你可能會想:“為什麼是這些函式?明明有其他更多的非線性函式。”這些函式常用的原因是它們擁有可以容易計算的梯度,而計算梯度是學習的本質。例如
注意:儘管你可能在AI課程的介紹中學習了一些神經網路,在這些神經網路中 σ(x) 是預設非線性的,但是通常在實際使用的過程中都會避開它們。這是因為當引數的絕對值增長時,梯度會很快消失。小梯度意味著很難學習。大部分都會選擇 tanh 或者 ReLU 。
# In pytorch, most non-linearities are in torch.functional (we have it imported as F) # Note that non-linearites typically don't have parameters like affine maps do. # That is, they don't have weights that are updated during training. data = torch.randn(2, 2) print(data) print(F.relu(data))
輸出:
tensor([[-0.5404, -2.2102], [ 2.1130, -0.0040]]) tensor([[0.0000, 0.0000], [2.1130, 0.0000]])
Softmax和概率
Softmax(x) 也是一個非線性函式,但它的特殊之處在於,它通常是神經網路的最後一個操作。這是因為它接受實數向量,並且返回一個概率分佈。它的定義如下。設 x 為實數向量(正、負,無論什麼,沒有約束)。然後 Softmax(x) 的第 i 個分量是:
很明顯,輸出的是一個概率分佈:每一個元素都非負且和為1。
你也可以認為這只是一個對輸入的元素進行的求冪運算子,使所有的內容都非負,然後除以規範化常量。
# Softmax is also in torch.nn.functional data = torch.randn(5) print(data) print(F.softmax(data, dim=0)) print(F.softmax(data, dim=0).sum())# Sums to 1 because it is a distribution! print(F.log_softmax(data, dim=0))# theres also log_softmax
輸出:
tensor([ 1.3800, -1.3505,0.3455,0.5046,1.8213]) tensor([0.2948, 0.0192, 0.1048, 0.1228, 0.4584]) tensor(1.) tensor([-1.2214, -3.9519, -2.2560, -2.0969, -0.7801])
目標函式
目標函式是訓練網路使其最小化的函式(因此,它常常被稱作損失函式或者成本函式)。這需要首先選擇一個訓練例項,通過神經網路執行它,計算輸出的損失。然後通過損失函式的導數來更新模型的引數。直觀來講,如果你的模型完全相信它的結果,而它的結果是錯誤的,那麼損失將會很高。
在你的訓練例項中最小化損失函式的目的是使你的網路擁有很好的泛化能力,可以在開發資料集,測試資料集或者生產中擁有很小的損失。損失函式的一個例子是負對數似然損失函式,這個函式經常在多級分類中出現。在監督多級分類中,這意味著訓練網路最小化正確輸出的負對數概率(或等效的,最大化正確輸出的對數概率)。
優化和訓練
那麼,我們該怎麼計算函式例項的損失函式呢?我們應該做什麼呢?我們在之前瞭解到,Tensor知道如何計算梯度以及計算梯度相關的東西。由於我們的損失是一個Tensor,我們可以計算梯度以及所有用來計算梯度的引數。然後我們可以進行標準梯度更新。設 θ 為我們的引數, L(θ) 為損失函式, η 一個正的學習率。然後:
目前,有大量的演算法和積極的研究試圖做一些除了這種普通的梯度更新以外的事情。許多人嘗試去基於訓練時發生的事情來改變學習率。但是,你不需要擔心這些特殊的演算法到底在幹什麼,除非你真的很感興趣。Torch提供了大量的演算法在torch.optim包中,且全部都是透明的。使用複雜的演算法和使用最簡單的梯度更新沒有什麼區別。嘗試不同的更新演算法和在更新演算法中使用不同的引數(例如不同的初始學習率)對於優化你的網路的效能很重要。通常,僅僅將普通的 SGD 替換成一個例如 Adam 或者 RMSProp 優化器都可以顯著的提升效能。
使用PyTorch建立網路元件
在我們繼續關注NLP之前,讓我們先使用PyTorch構建一個只用仿射對映和非線性函式組成的網路示例。我們也將瞭解如何計算損失函式,使用PyTorch內建的負對數似然函式,並通過反向傳播更新引數。
所有的網路元件應該繼承nn.Module並覆蓋forward()方法。繼承nn.Module提供給了一些方法給你的元件。例如,它可以跟蹤可訓練的引數,你可以通過 .to(device)
方法在CPU和GPU之間交換它們。 .to(device)
方法中的device可以是CPU裝置 torch.device("cpu")
或者CUDA裝置 torch.device("cuda:0")
。
讓我們寫一個神經網路的示例,它接受一些稀疏的BOW表示,然後輸出分佈在兩個標籤上的概率:“English”和“Spanish”。這個模型只是一個邏輯迴歸。
示例: 邏輯迴歸詞袋分類器
我們的模型將會把BOW表示對映成標籤上的對數概率。我們為詞彙中的每個詞指定一個索引。例如,我們所有的詞彙是兩個單詞“hello”和”world”,用0和1表示。句子“hello hello hello hello”的表示是
[4,0]
對於“hello world world hello”, 則表示成
[2,2]
通常表示成
[Count(hello),Count(world)]
用x來表示這個BOW向量。網路的輸出是:
也就是說,我們資料傳入一個仿射對映然後做 softmax 的對數。
data = [("me gusta comer en la cafeteria".split(), "SPANISH"), ("Give it to me".split(), "ENGLISH"), ("No creo que sea una buena idea".split(), "SPANISH"), ("No it is not a good idea to get lost at sea".split(), "ENGLISH")] test_data = [("Yo creo que si".split(), "SPANISH"), ("it is lost on me".split(), "ENGLISH")] # word_to_ix maps each word in the vocab to a unique integer, which will be its # index into the Bag of words vector word_to_ix = {} for sent, _ in data + test_data: for word in sent: if word not in word_to_ix: word_to_ix[word] = len(word_to_ix) print(word_to_ix) VOCAB_SIZE = len(word_to_ix) NUM_LABELS = 2 class BoWClassifier(nn.Module):# inheriting from nn.Module! def __init__(self, num_labels, vocab_size): # calls the init function of nn.Module.Dont get confused by syntax, # just always do it in an nn.Module super(BoWClassifier, self).__init__() # Define the parameters that you will need.In this case, we need A and b, # the parameters of the affine mapping. # Torch defines nn.Linear(), which provides the affine map. # Make sure you understand why the input dimension is vocab_size # and the output is num_labels! self.linear = nn.Linear(vocab_size, num_labels) # NOTE! The non-linearity log softmax does not have parameters! So we don't need # to worry about that here def forward(self, bow_vec): # Pass the input through the linear layer, # then pass that through log_softmax. # Many non-linearities and other functions are in torch.nn.functional return F.log_softmax(self.linear(bow_vec), dim=1) def make_bow_vector(sentence, word_to_ix): vec = torch.zeros(len(word_to_ix)) for word in sentence: vec[word_to_ix[word]] += 1 return vec.view(1, -1) def make_target(label, label_to_ix): return torch.LongTensor([label_to_ix[label]]) model = BoWClassifier(NUM_LABELS, VOCAB_SIZE) # the model knows its parameters.The first output below is A, the second is b. # Whenever you assign a component to a class variable in the __init__ function # of a module, which was done with the line # self.linear = nn.Linear(...) # Then through some Python magic from the PyTorch devs, your module # (in this case, BoWClassifier) will store knowledge of the nn.Linear's parameters for param in model.parameters(): print(param) # To run the model, pass in a BoW vector # Here we don't need to train, so the code is wrapped in torch.no_grad() with torch.no_grad(): sample = data[0] bow_vector = make_bow_vector(sample[0], word_to_ix) log_probs = model(bow_vector) print(log_probs)
輸出:
{'me': 0, 'gusta': 1, 'comer': 2, 'en': 3, 'la': 4, 'cafeteria': 5, 'Give': 6, 'it': 7, 'to': 8, 'No': 9, 'creo': 10, 'que': 11, 'sea': 12, 'una': 13, 'buena': 14, 'idea': 15, 'is': 16, 'not': 17, 'a': 18, 'good': 19, 'get': 20, 'lost': 21, 'at': 22, 'Yo': 23, 'si': 24, 'on': 25} Parameter containing: tensor([[ 0.1194,0.0609, -0.1268,0.1274,0.1191,0.1739, -0.1099, -0.0323, -0.0038,0.0286, -0.1488, -0.1392,0.1067, -0.0460,0.0958,0.0112, 0.0644,0.0431,0.0713,0.0972, -0.1816,0.0987, -0.1379, -0.1480, 0.0119, -0.0334], [ 0.1152, -0.1136, -0.1743,0.1427, -0.0291,0.1103,0.0630, -0.1471, 0.0394,0.0471, -0.1313, -0.0931,0.0669,0.0351, -0.0834, -0.0594, 0.1796, -0.0363,0.1106,0.0849, -0.1268, -0.1668,0.1882,0.0102, 0.1344,0.0406]], requires_grad=True) Parameter containing: tensor([0.0631, 0.1465], requires_grad=True) tensor([[-0.5378, -0.8771]])
上面的哪一個值對應的是ENGLISH的對數概率,哪一個是SPANISH的對數概率?我們還沒有定義,但是如果我們想要訓練一些東西,我們必須進行定義。
label_to_ix = {"SPANISH": 0, "ENGLISH": 1}
讓我們來訓練吧! 我們將例項傳入來獲取對數概率,計算損失函式,計算損失函式的梯度,然後使用一個梯度步長來更新引數。在PyTorch的nn包裡提供了損失函式。nn.NLLLoss()是我們想要的負對數似然損失函式。它也在torch.optim定義了優化方法。這裡,我們只使用 SGD 。
注意,NLLLoss的輸入是一個對數概率的向量以及目標標籤。它不會為我們計算對數概率。這也是為什麼我們最後一層網路是 log_softmax 的原因。損失函式nn.CrossEntropyLoss()除了給你做了一個 sofmax 的對數之外和NLLLoss()沒什麼區別。
# Run on test data before we train, just to see a before-and-after with torch.no_grad(): for instance, label in test_data: bow_vec = make_bow_vector(instance, word_to_ix) log_probs = model(bow_vec) print(log_probs) # Print the matrix column corresponding to "creo" print(next(model.parameters())[:, word_to_ix["creo"]]) loss_function = nn.NLLLoss() optimizer = optim.SGD(model.parameters(), lr=0.1) # Usually you want to pass over the training data several times. # 100 is much bigger than on a real data set, but real datasets have more than # two instances.Usually, somewhere between 5 and 30 epochs is reasonable. for epoch in range(100): for instance, label in data: # Step 1\. Remember that PyTorch accumulates gradients. # We need to clear them out before each instance model.zero_grad() # Step 2\. Make our BOW vector and also we must wrap the target in a # Tensor as an integer. For example, if the target is SPANISH, then # we wrap the integer 0\. The loss function then knows that the 0th # element of the log probabilities is the log probability # corresponding to SPANISH bow_vec = make_bow_vector(instance, word_to_ix) target = make_target(label, label_to_ix) # Step 3\. Run our forward pass. log_probs = model(bow_vec) # Step 4\. Compute the loss, gradients, and update the parameters by # calling optimizer.step() loss = loss_function(log_probs, target) loss.backward() optimizer.step() with torch.no_grad(): for instance, label in test_data: bow_vec = make_bow_vector(instance, word_to_ix) log_probs = model(bow_vec) print(log_probs) # Index corresponding to Spanish goes up, English goes down! print(next(model.parameters())[:, word_to_ix["creo"]])
輸出:
tensor([[-0.9297, -0.5020]]) tensor([[-0.6388, -0.7506]]) tensor([-0.1488, -0.1313], grad_fn=<SelectBackward>) tensor([[-0.2093, -1.6669]]) tensor([[-2.5330, -0.0828]]) tensor([ 0.2803, -0.5605], grad_fn=<SelectBackward>)
我們得到了正確的結果!你可以看到Spanish的對數概率比第一個例子中的高的多,English的對數概率在第二個測試資料中更高,結果也應該是這樣。
現在你瞭解瞭如何建立一個PyTorch元件,將資料傳入並進行梯度更新。我們準備深入挖掘NLP所能提供的東西了。