以一個(gè)簡(jiǎn)單的RNN為例梳理神經(jīng)網(wǎng)絡(luò)的訓(xùn)練過程

本文是學(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

initforward都是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)練的思路是:

  1. 準(zhǔn)備訓(xùn)練數(shù)據(jù),校驗(yàn)數(shù)據(jù)和測(cè)試數(shù)據(jù)(每個(gè)數(shù)據(jù)集的一組數(shù)據(jù)都是一個(gè)數(shù)字序列)
  2. 循環(huán)數(shù)數(shù)字序列,當(dāng)前數(shù)字作為輸入,下一個(gè)數(shù)字作為標(biāo)簽(即真實(shí)結(jié)果)
  3. 每次循環(huán)都經(jīng)過一個(gè)rnn網(wǎng)絡(luò)
  4. 計(jì)算每一組的損失t_loss并記錄
  5. 優(yōu)化器優(yōu)化參數(shù)
  6. 重復(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ì)再回來的

?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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