本文是學(xué)習(xí)完集智學(xué)園《PyTorch入門課程:火炬上的深度學(xué)習(xí)——自然語言處理(NLP)》系列課之后的梳理。
本次任務(wù)為預(yù)測(cè)字符(數(shù)字),讓神經(jīng)網(wǎng)絡(luò)找到下面數(shù)字的規(guī)律。
012
00112
0001112
000011112
00000111112
當(dāng)我們給定一組數(shù)據(jù)(如0000001)的時(shí)候,讓神經(jīng)網(wǎng)絡(luò)去預(yù)測(cè)后面的數(shù)字應(yīng)該是什么
1. 建立神經(jīng)網(wǎng)絡(luò)架構(gòu)
我們構(gòu)建一個(gè)RNN類
class simpleRNN(nn.Module):
def __init():
...
def forword():
...
def initHidden():
...
其中函數(shù)initHidden的作用是初始化隱含層向量
def initHidden(self):
# 對(duì)隱含單元的初始化
# 注意尺寸是: layer_size, batch_size, hidden_size
return Variable(torch.zeros(self.num_layers, 1, self.hidden_size))
使用init函數(shù)
init用于搭建神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu),網(wǎng)絡(luò)的輸入維度,輸出維度,隱含層維度和數(shù)量,過程中需要用到的模型等等,都在init中定義。
其中nn是直接pytorch自帶的模塊,里面包含了內(nèi)置的Embedding ,RNN, Linear, logSoftmax等模型,可以直接使用。
# 引入pytorch 中的 nn(模型模塊)
import torch.nn as nn
def __init__(self, input_size, hidden_size, output_size, num_layers = 1):
# 定義
super(SimpleRNN, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
# 一個(gè)embedding層
self.embedding = nn.Embedding(input_size, hidden_size)
# PyTorch的RNN模型,batch_first標(biāo)志可以讓輸入的張量的第一個(gè)維度表示batch指標(biāo)
self.rnn = nn.RNN(hidden_size, hidden_size, num_layers, batch_first = True)
# 輸出的全鏈接層
self.linear = nn.Linear(hidden_size, output_size)
# 最后的logsoftmax層
self.softmax = nn.LogSoftmax()
使用forward函數(shù)作為神經(jīng)網(wǎng)絡(luò)的運(yùn)算過程
運(yùn)算過程也很好理解,就是將輸入一步一步地走過嵌入層,rnn層,linear層,和softmax層
- embedding(嵌入層):用于輸入層到隱含層的嵌入。過程大致是把輸入向量先轉(zhuǎn)化為one-hot編碼,再編碼為一個(gè)hidden_size維的向量
- RNN層:經(jīng)過一層RNN模型
- linear層(全鏈接層):將隱含層向量的所有維度一一映射到輸出上,可以理解為共享信息
- softmax:將數(shù)據(jù)歸一化處理
# 運(yùn)算過程
def forward(self, input, hidden):
# size of input:[batch_size, num_step, data_dim]
# embedding層:
# 從輸入到隱含層的計(jì)算
output = self.embedding(input, hidden)
# size of output:[batch_size, num_step, hidden_size]
output, hidden = self.rnn(output, hidden)
# size of output:[batch_size, num_step, hidden_size]
# 從輸出output中取出最后一個(gè)時(shí)間步的數(shù)值,注意output輸出包含了所有時(shí)間步的結(jié)果
output = output[:,-1,:]
# size of output:[batch_size, hidden_size]
# 全鏈接層
output = self.linear(output)
# output尺寸為:batch_size, output_size
# softmax層,歸一化處理
output = self.softmax(output)
# size of output:batch_size, output_size
return output, hidden
對(duì)RNN的訓(xùn)練結(jié)果中間有一個(gè)特別的操作
output = output[:, -1 ,:]
output尺寸為[batch_size, step, hidden_size], 這一步是把第二維時(shí)間步的數(shù)據(jù)只保留最后一個(gè)數(shù)。因?yàn)镽NN的特征就是記憶,最后一步數(shù)據(jù)包含了之前所有步數(shù)的信息。所以這里只需要取最后一個(gè)數(shù)即可
使用這個(gè)init和forword
init和forward都是python的class中內(nèi)置的兩個(gè)函數(shù)。
- 如果你定義了
__init__,那么在實(shí)例化類的時(shí)候就會(huì)自動(dòng)運(yùn)行init函數(shù)體,而且實(shí)例化的參數(shù)就是init函數(shù)的參數(shù) - 如果你定義了
forward, 那么你在執(zhí)行這個(gè)類的時(shí)候,就自動(dòng)執(zhí)行forward函數(shù)
# 實(shí)例化類simpleRNN,此時(shí)執(zhí)行__init__函數(shù)
rnn = simpleRNN(input_size = 4, hidden_size = 1, output_size = 3, num_layers = 1)
# 使用類simpleRNN
output, hidden = rnn(input, hidden)
那么執(zhí)行一次forward就相當(dāng)于一個(gè)訓(xùn)練過程:輸入 -> 輸出
2. 可以開始訓(xùn)練了
首先是構(gòu)造損失函數(shù)和優(yōu)化器
強(qiáng)大的pytorch自帶了通用的損失函數(shù)以及優(yōu)化器模型。一句命令就搞定了一切。
criterion = torch.nn.NLLLoss()
optimizer = torch.optim.Adam(rnn.parameters(), lr = 0.001)
損失函數(shù)criterion: 用于記錄訓(xùn)練損失,所有權(quán)重都會(huì)根據(jù)每一步的損失值來調(diào)整。這里使用的是NLLLoss損失函數(shù),是一種比較簡(jiǎn)單的損失計(jì)算,計(jì)算真實(shí)值和預(yù)測(cè)值的絕對(duì)差值
# output是預(yù)測(cè)值,y是真實(shí)值
loss = criterion(output, y)
優(yōu)化器optimizer: 訓(xùn)練過程的迭代操作。包括梯度反傳和梯度清空。傳入的參數(shù)為神經(jīng)網(wǎng)絡(luò)的參數(shù)rnn.parameters()以及學(xué)習(xí)率lr
# 梯度反傳,調(diào)整權(quán)重
optimizer.zero_grad()
# 梯度清空
optimizer.step()
訓(xùn)練過程
訓(xùn)練的思路是:
- 準(zhǔn)備訓(xùn)練數(shù)據(jù),校驗(yàn)數(shù)據(jù)和測(cè)試數(shù)據(jù)(每個(gè)數(shù)據(jù)集的一組數(shù)據(jù)都是一個(gè)數(shù)字序列)
- 循環(huán)數(shù)數(shù)字序列,當(dāng)前數(shù)字作為輸入,下一個(gè)數(shù)字作為標(biāo)簽(即真實(shí)結(jié)果)
- 每次循環(huán)都經(jīng)過一個(gè)rnn網(wǎng)絡(luò)
- 計(jì)算每一組的損失t_loss并記錄
- 優(yōu)化器優(yōu)化參數(shù)
- 重復(fù)1~5的訓(xùn)練步驟n次,n自定義
訓(xùn)練數(shù)據(jù)的準(zhǔn)備不在本次的討論范圍內(nèi),所以這里直接給出處理好的結(jié)果如下。
train_set = [[3, 0, 0, 1, 1, 2],
[3, 0, 1, 2],
[3, 0, 0, 0, 1, 1, 1, 2],
[3, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2]
...]
開始進(jìn)行訓(xùn)練
# 重復(fù)進(jìn)行50次試驗(yàn)
num_epoch = 50
loss_list = []
for epoch in range(num_epoch):
train_loss = 0
# 對(duì)train_set中的數(shù)據(jù)進(jìn)行隨機(jī)洗牌,以保證每個(gè)epoch得到的訓(xùn)練順序都不一樣。
np.random.shuffle(train_set)
# 對(duì)train_set中的數(shù)據(jù)進(jìn)行循環(huán)
for i, seq in enumerate(train_set):
loss = 0
# 對(duì)每一個(gè)序列的所有字符進(jìn)行循環(huán)
for t in range(len(seq) - 1):
#當(dāng)前字符作為輸入
x = Variable(torch.LongTensor([seq[t]]).unsqueeze(0))
# x尺寸:batch_size = 1, time_steps = 1, data_dimension = 1
# 下一個(gè)字符作為標(biāo)簽
y = Variable(torch.LongTensor([seq[t + 1]]))
# y尺寸:batch_size = 1, data_dimension = 1
output, hidden = rnn(x, hidden) #RNN輸出
# output尺寸:batch_size, output_size = 3
# hidden尺寸:layer_size =1, batch_size=1, hidden_size
loss += criterion(output, y) #計(jì)算損失函數(shù)
loss = 1.0 * loss / len(seq) #計(jì)算每字符的損失數(shù)值
optimizer.zero_grad() # 梯度清空
loss.backward() #反向傳播
optimizer.step() #一步梯度下降
train_loss += loss #累積損失函數(shù)值
# 把結(jié)果打印出來
if i > 0 and i % 500 == 0:
print('第{}輪, 第{}個(gè),訓(xùn)練Loss:{:.2f}'.format(epoch, i, train_loss.data.numpy()[0] / i))
loss_list.appand(train_loss)
這里的loss是對(duì)每一個(gè)訓(xùn)練循環(huán)(epoch)的損失,事實(shí)上無論訓(xùn)練的如何,這里的loss都會(huì)下降,因?yàn)樯窠?jīng)網(wǎng)絡(luò)就是會(huì)讓最后的結(jié)果盡可能地靠近真實(shí)數(shù)據(jù),所以訓(xùn)練集的loss其實(shí)并不能用來評(píng)價(jià)一個(gè)模型的訓(xùn)練好壞。
在實(shí)際的訓(xùn)練過程中,我們會(huì)在每一輪訓(xùn)練后,把得到的模型放入校驗(yàn)集去計(jì)算loss, 這樣的結(jié)果更為客觀。
校驗(yàn)集loss的計(jì)算和訓(xùn)練集完全一致,只不過把train_set替換成了valid_set,而且也不需要去根據(jù)結(jié)果優(yōu)化參數(shù),這在訓(xùn)練步驟中已經(jīng)做了,校驗(yàn)集的作用就是看模型的訓(xùn)練效果:
for epoch in range(num_epoch):
# 訓(xùn)練步驟
...
valid_loss = 0
for i, seq in enumerate(valid_set):
# 對(duì)每一個(gè)valid_set中的字符串做循環(huán)
loss = 0
outstring = ''
targets = ''
hidden = rnn.initHidden() #初始化隱含層神經(jīng)元
for t in range(len(seq) - 1):
# 對(duì)每一個(gè)字符做循環(huán)
x = Variable(torch.LongTensor([seq[t]]).unsqueeze(0))
# x尺寸:batch_size = 1, time_steps = 1, data_dimension = 1
y = Variable(torch.LongTensor([seq[t + 1]]))
# y尺寸:batch_size = 1, data_dimension = 1
output, hidden = rnn(x, hidden)
# output尺寸:batch_size, output_size = 3
# hidden尺寸:layer_size =1, batch_size=1, hidden_size
loss += criterion(output, y) #計(jì)算損失函數(shù)
loss = 1.0 * loss / len(seq)
valid_loss += loss #累積損失函數(shù)值
# # 打印結(jié)果
print('第%d輪, 訓(xùn)練Loss:%f, 校驗(yàn)Loss:%f, 錯(cuò)誤率:%f'%(epoch, train_loss.data.numpy() / len(train_set),valid_loss.data.numpy() / len(valid_set),1.0 * errors / len(valid_set)))
根據(jù)校驗(yàn)集的loss輸出,我們可以繪制出最終的loss變化。
3. 測(cè)試模型預(yù)測(cè)效果
構(gòu)造數(shù)據(jù),測(cè)試模型是否能猜出當(dāng)前數(shù)字的下一個(gè)數(shù)。成功率有多高
首先是構(gòu)造數(shù)據(jù),構(gòu)造長(zhǎng)度分別為0~20的數(shù)字序列
for n in range(20):
inputs = [0] * n + [1] * n
然后對(duì)每一個(gè)序列進(jìn)行測(cè)試
for n in range(20):
inputs = [0] * n + [1] * n
outstring = ''
targets = ''
diff = 0
hiddens = []
hidden = rnn.initHidden()
for t in range(len(inputs) - 1):
x = Variable(torch.LongTensor([inputs[t]]).unsqueeze(0))
# x尺寸:batch_size = 1, time_steps = 1, data_dimension = 1
y = Variable(torch.LongTensor([inputs[t + 1]]))
# y尺寸:batch_size = 1, data_dimension = 1
output, hidden = rnn(x, hidden)
# output尺寸:batch_size, output_size = 3
# hidden尺寸:layer_size =1, batch_size=1, hidden_size
hiddens.append(hidden.data.numpy()[0][0])
#mm = torch.multinomial(output.view(-1).exp())
mm = torch.max(output, 1)[1][0]
outstring += str(mm.data.numpy()[0])
targets += str(y.data.numpy()[0])
# 計(jì)算模型輸出字符串與目標(biāo)字符串之間差異的字符數(shù)量
diff += 1 - mm.eq(y)
# 打印出每一個(gè)生成的字符串和目標(biāo)字符串
print(outstring)
print(targets)
print('Diff:{}'.format(diff.data.numpy()[0]))
最終輸出的結(jié)果為
[0, 1, 2]
[0, 1, 2]
Diff: 0
[0, 0, 1, 1, 2]
[0, 0, 1, 1, 2]
Diff: 0
[0, 0, 0, 1, 1, 1, 2]
[0, 0, 0, 1, 1, 1, 2]
Diff: 0
...
# 結(jié)果不一一列出,大家可以自行嘗試
總結(jié)
神經(jīng)網(wǎng)絡(luò)可以理解為讓計(jì)算機(jī)使用各種數(shù)學(xué)手段從一堆數(shù)據(jù)中找規(guī)律的過程。我們可以通過解剖一些簡(jiǎn)單任務(wù)來理解神經(jīng)網(wǎng)絡(luò)的內(nèi)部機(jī)制。當(dāng)面對(duì)復(fù)雜任務(wù)的時(shí)候,只需要把數(shù)據(jù)交給模型,它就能盡其所能地給你一個(gè)好的結(jié)果。
本文是學(xué)習(xí)完集智學(xué)園《PyTorch入門課程:火炬上的深度學(xué)習(xí)——自然語言處理(NLP)》系列課之后的梳理。課程中還有關(guān)于
lstm,翻譯任務(wù)實(shí)操等基礎(chǔ)而且豐富的知識(shí)點(diǎn),我還會(huì)再回來的