譯者:bdqfork
作者: Robert Guthrie
深度學習構建模塊:仿射映射, 非線性函數(shù)以及目標函數(shù)
深度學習表現(xiàn)為使用更高級的方法將線性函數(shù)和非線性函數(shù)進行組合。非線性函數(shù)的引入使得訓練出來的模型更加強大楣责。在本節(jié)中聂沙,我們將學習這些核心組件及汉,建立目標函數(shù),并理解模型是如何構建的房铭。
仿射映射
深度學習的核心組件之一是仿射映射,仿射映射是一個關于矩陣A和向量x翁狐,b的f(x)函數(shù)豪嗽,如下所示:
需要訓練的參數(shù)就是該公式中的A和b龟梦。
PyTorch以及大多數(shù)的深度學習框架所做的事情都與傳統(tǒng)的線性代數(shù)有些不同计贰。它的映射輸入是行而不是列蒂窒。也就是說,下面代碼輸出的第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>)
非線性函數(shù)
首先谭网,注意以下這個例子,它將解釋為什么我們需要非線性函數(shù)劫乱。假設我們有兩個仿射映射 f(x) = Ax + b 和 g(x) = Cx + d 锥涕。那么 f(g(x)) 又是什么呢站楚?
AC 是一個矩陣,Ad + b是一個向量拉一,可以看出蔚润,兩個仿射映射的組合還是一個仿射映射。
由此可以看出烦租,使用多個仿射映射的鏈式組合形成神經(jīng)網(wǎng)絡除盏,并不會對提升模型的性能者蠕,和一個仿射映射沒什么區(qū)別。
但是粪小,如果我們在兩個仿射映射之間引入非線性抡句,那么結果就大不一樣了待榔,我們可以構建出一個高性能的模型。
最常用的核心的非線性函數(shù)有:tanh(x)猾担,σ(x)刺下,ReLU(x)橘茉。你可能會想:“為什么是這些函數(shù)?明明有其他更多的非線性函數(shù)擅腰∥膛耍”這些函數(shù)常用的原因是它們擁有可以容易計算的梯度,而計算梯度是學習的本質沐绒。例如
注意:盡管你可能在AI課程的介紹中學習了一些神經(jīng)網(wǎng)絡乔遮,在這些神經(jīng)網(wǎng)絡中σ(x)是默認非線性的取刃,但是通常在實際使用的過程中都會避開它們。這是因為當參數(shù)的絕對值增長時坯辩,梯度會很快消失濒翻。小梯度意味著很難學習啦膜。大部分都會選擇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)也是一個非線性函數(shù)裸删,但它的特殊之處在于涯塔,它通常是神經(jīng)網(wǎng)絡的最后一個操作。這是因為它接受實數(shù)向量爹谭,并且返回一個概率分布榛搔。它的定義如下践惑。設x為實數(shù)向量(正、負凉袱,無論什么侦铜,沒有約束)。然后Softmax(x)的第i個分量是:
很明顯携添,輸出的是一個概率分布:每一個元素都非負且和為1烈掠。
你也可以認為這只是一個對輸入的元素進行的求冪運算符缸托,使所有的內(nèi)容都非負俐镐,然后除以規(guī)范化常量戈抄。
# 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])
目標函數(shù)
目標函數(shù)是訓練網(wǎng)絡使其最小化的函數(shù)(因此份乒,它常常被稱作損失函數(shù)或者成本函數(shù))兴垦。這需要首先選擇一個訓練實例枢里,通過神經(jīng)網(wǎng)絡運行它,計算輸出的損失彬碱。然后通過損失函數(shù)的導數(shù)來更新模型的參數(shù)巷疼。直觀來講溉卓,如果你的模型完全相信它的結果桑寨,而它的結果是錯誤的,那么損失將會很高爆阶。
在你的訓練實例中最小化損失函數(shù)的目的是使你的網(wǎng)絡擁有很好的泛化能力,可以在開發(fā)數(shù)據(jù)集班套,測試數(shù)據(jù)集或者生產(chǎn)中擁有很小的損失吱韭。損失函數(shù)的一個例子是負對數(shù)似然損失函數(shù)鱼的,這個函數(shù)經(jīng)常在多級分類中出現(xiàn)凑阶。在監(jiān)督多級分類中,這意味著訓練網(wǎng)絡最小化正確輸出的負對數(shù)概率(或等效的姨俩,最大化正確輸出的對數(shù)概率)环葵。
優(yōu)化和訓練
那么呕乎,我們該怎么計算函數(shù)實例的損失函數(shù)呢猬仁?我們應該做什么呢湿刽?我們在之前了解到,Tensor知道如何計算梯度以及計算梯度相關的東西雅镊。由于我們的損失是一個Tensor仁烹,我們可以計算梯度以及所有用來計算梯度的參數(shù)咧虎。然后我們可以進行標準梯度更新。設 θ為我們的參數(shù)捌显,L(θ)為損失函數(shù)总寒,η一個正的學習率摄闸。然后:
目前贪薪,有大量的算法和積極的研究試圖做一些除了這種普通的梯度更新以外的事情。許多人嘗試去基于訓練時發(fā)生的事情來改變學習率竣稽。但是毫别,你不需要擔心這些特殊的算法到底在干什么典格,除非你真的很感興趣耍缴。Torch提供了大量的算法在torch.optim包中,且全部都是透明的变汪。使用復雜的算法和使用最簡單的梯度更新沒有什么區(qū)別裙盾。嘗試不同的更新算法和在更新算法中使用不同的參數(shù)(例如不同的初始學習率)對于優(yōu)化你的網(wǎng)絡的性能很重要番官。通常钢属,僅僅將普通的SGD替換成一個例如Adam或者RMSProp優(yōu)化器都可以顯著的提升性能署咽。
使用PyTorch創(chuàng)建網(wǎng)絡組件
在我們繼續(xù)關注NLP之前生音,讓我們先使用PyTorch構建一個只用仿射映射和非線性函數(shù)組成的網(wǎng)絡示例缀遍。我們也將了解如何計算損失函數(shù)饱须,使用PyTorch內(nèi)置的負對數(shù)似然函數(shù)蓉媳,并通過反向傳播更新參數(shù)酪呻。
所有的網(wǎng)絡組件應該繼承nn.Module并覆蓋forward()方法。繼承nn.Module提供給了一些方法給你的組件漆腌。例如闷尿,它可以跟蹤可訓練的參數(shù),你可以通過.to(device)
方法在CPU和GPU之間交換它們女坑。.to(device)
方法中的device可以是CPU設備torch.device("cpu")
或者CUDA設備torch.device("cuda:0")
填具。
讓我們寫一個神經(jīng)網(wǎng)絡的示例,它接受一些稀疏的BOW表示匆骗,然后輸出分布在兩個標簽上的概率:“English”和“Spanish”劳景。這個模型只是一個邏輯回歸。
示例: 邏輯回歸詞袋分類器
我們的模型將會把BOW表示映射成標簽上的對數(shù)概率绰筛。我們?yōu)樵~匯中的每個詞指定一個索引枢泰。例如描融,我們所有的詞匯是兩個單詞“hello”和"world",用0和1表示窿克。句子“hello hello hello hello”的表示是
[4,0]
對于“hello world world hello”, 則表示成
[2,2]
通常表示成
[Count(hello),Count(world)]
用x來表示這個BOW向量骏庸。網(wǎng)絡的輸出是:
也就是說,我們數(shù)據(jù)傳入一個仿射映射然后做softmax的對數(shù)年叮。
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的對數(shù)概率具被,哪一個是SPANISH的對數(shù)概率?我們還沒有定義只损,但是如果我們想要訓練一些東西一姿,我們必須進行定義七咧。
label_to_ix = {"SPANISH": 0, "ENGLISH": 1}
讓我們來訓練吧! 我們將實例傳入來獲取對數(shù)概率,計算損失函數(shù)叮叹,計算損失函數(shù)的梯度艾栋,然后使用一個梯度步長來更新參數(shù)。在PyTorch的nn包里提供了損失函數(shù)蛉顽。nn.NLLLoss()是我們想要的負對數(shù)似然損失函數(shù)蝗砾。它也在torch.optim定義了優(yōu)化方法。這里携冤,我們只使用SGD悼粮。
注意,NLLLoss的輸入是一個對數(shù)概率的向量以及目標標簽曾棕。它不會為我們計算對數(shù)概率扣猫。這也是為什么我們最后一層網(wǎng)絡是log_softmax的原因。損失函數(shù)nn.CrossEntropyLoss()除了給你做了一個sofmax的對數(shù)之外和NLLLoss()沒什么區(qū)別翘地。
# 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的對數(shù)概率比第一個例子中的高的多,English的對數(shù)概率在第二個測試數(shù)據(jù)中更高子眶,結果也應該是這樣瀑凝。
現(xiàn)在你了解了如何創(chuàng)建一個PyTorch組件,將數(shù)據(jù)傳入并進行梯度更新臭杰。我們準備深入挖掘NLP所能提供的東西了粤咪。