[動手學(xué)深度學(xué)習(xí)-PyTorch版]-6.4循環(huán)神經(jīng)網(wǎng)絡(luò)-循環(huán)神經(jīng)網(wǎng)絡(luò)的從零開始實現(xiàn)

6.4 循環(huán)神經(jīng)網(wǎng)絡(luò)的從零開始實現(xiàn)

在本節(jié)中钓丰,我們將從零開始實現(xiàn)一個基于字符級循環(huán)神經(jīng)網(wǎng)絡(luò)的語言模型阳懂,并在周杰倫專輯歌詞數(shù)據(jù)集上訓(xùn)練一個模型來進(jìn)行歌詞創(chuàng)作凛剥。首先,我們讀取周杰倫專輯歌詞數(shù)據(jù)集:

import time
import math
import numpy as np
import torch
from torch import nn, optim
import torch.nn.functional as F

import sys
sys.path.append("..") 
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

(corpus_indices, char_to_idx, idx_to_char, vocab_size) = d2l.load_data_jay_lyrics()

6.4.1 one-hot向量

為了將詞表示成向量輸入到神經(jīng)網(wǎng)絡(luò)舒憾,一個簡單的辦法是使用one-hot向量。假設(shè)詞典中不同字符的數(shù)量為N(即詞典大小vocab_size),每個字符已經(jīng)同一個從0到N?1的連續(xù)整數(shù)值索引一一對應(yīng)伟墙。如果一個字符的索引是整數(shù)i, 那么我們創(chuàng)建一個全0的長為N的向量钾恢,并將其位置為i的元素設(shè)成1手素。該向量就是對原字符的one-hot向量鸳址。下面分別展示了索引為0和2的one-hot向量,向量長度等于詞典大小泉懦。
pytorch沒有自帶one-hot函數(shù)(新版好像有了)稿黍,下面自己實現(xiàn)一個

def one_hot(x, n_class, dtype=torch.float32): 
    # X shape: (batch), output shape: (batch, n_class)
    x = x.long()
    res = torch.zeros(x.shape[0], n_class, dtype=dtype, device=x.device)
    res.scatter_(1, x.view(-1, 1), 1)
    return res

x = torch.tensor([0, 2])
one_hot(x, vocab_size)
image.png
# 本函數(shù)已保存在d2lzh_pytorch包中方便以后使用
def to_onehot(X, n_class):  
    # X shape: (batch, seq_len), output: seq_len elements of (batch, n_class)
    return [one_hot(X[:, i], n_class) for i in range(X.shape[1])]

X = torch.arange(10).view(2, 5)
inputs = to_onehot(X, vocab_size)
print(len(inputs), inputs[0].shape)

輸出:

5 torch.Size([2, 1027])

6.4.2 初始化模型參數(shù)

接下來,我們初始化模型參數(shù)崩哩。隱藏單元個數(shù) num_hiddens是一個超參數(shù)巡球。

num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
print('will use', device)

def get_params():
    def _one(shape):
        ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32)
        return torch.nn.Parameter(ts, requires_grad=True)

    # 隱藏層參數(shù)
    W_xh = _one((num_inputs, num_hiddens))
    W_hh = _one((num_hiddens, num_hiddens))
    b_h = torch.nn.Parameter(torch.zeros(num_hiddens, device=device, requires_grad=True))
    # 輸出層參數(shù)
    W_hq = _one((num_hiddens, num_outputs))
    b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, requires_grad=True))
    return nn.ParameterList([W_xh, W_hh, b_h, W_hq, b_q])

6.4.3 定義模型

我們根據(jù)循環(huán)神經(jīng)網(wǎng)絡(luò)的計算表達(dá)式實現(xiàn)該模型。首先定義init_rnn_state函數(shù)來返回初始化的隱藏狀態(tài)邓嘹。它返回由一個形狀為(批量大小, 隱藏單元個數(shù))的值為0的NDArray組成的元組酣栈。使用元組是為了更便于處理隱藏狀態(tài)含有多個NDArray的情況。

def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), )

下面的rnn函數(shù)定義了在一個時間步里如何計算隱藏狀態(tài)和輸出吴超。這里的激活函數(shù)使用了tanh函數(shù)钉嘹。3.8節(jié)(多層感知機)中介紹過,當(dāng)元素在實數(shù)域上均勻分布時鲸阻,tanh函數(shù)值的均值為0跋涣。

def rnn(inputs, state, params):
    # inputs和outputs皆為num_steps個形狀為(batch_size, vocab_size)的矩陣
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    for X in inputs:
        H = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(H, W_hh) + b_h)
        Y = torch.matmul(H, W_hq) + b_q
        outputs.append(Y)
    return outputs, (H,)

做個簡單的測試來觀察輸出結(jié)果的個數(shù)(時間步數(shù)),以及第一個時間步的輸出層輸出的形狀和隱藏狀態(tài)的形狀鸟悴。

state = init_rnn_state(X.shape[0], num_hiddens, device)
inputs = to_onehot(X.to(device), vocab_size)
params = get_params()
outputs, state_new = rnn(inputs, state, params)
print(len(outputs), outputs[0].shape, state_new[0].shape) 

輸出:

5 torch.Size([2, 1027]) torch.Size([2, 256])

6.4.4 定義預(yù)測函數(shù)

以下函數(shù)基于前綴prefix(含有數(shù)個字符的字符串)來預(yù)測接下來的num_chars個字符陈辱。這個函數(shù)稍顯復(fù)雜,其中我們將循環(huán)神經(jīng)單元rnn設(shè)置成了函數(shù)參數(shù)细诸,這樣在后面小節(jié)介紹其他循環(huán)神經(jīng)網(wǎng)絡(luò)時能重復(fù)使用這個函數(shù)沛贪。

# 本函數(shù)已保存在d2lzh_pytorch包中方便以后使用
def predict_rnn(prefix, num_chars, rnn, params, init_rnn_state,
                num_hiddens, vocab_size, device, idx_to_char, char_to_idx):
    state = init_rnn_state(1, num_hiddens, device)
    output = [char_to_idx[prefix[0]]]
    for t in range(num_chars + len(prefix) - 1):
        # 將上一時間步的輸出作為當(dāng)前時間步的輸入
        X = to_onehot(torch.tensor([[output[-1]]], device=device), vocab_size)
        # 計算輸出和更新隱藏狀態(tài)
        (Y, state) = rnn(X, state, params)
        # 下一個時間步的輸入是prefix里的字符或者當(dāng)前的最佳預(yù)測字符
        if t < len(prefix) - 1:
            output.append(char_to_idx[prefix[t + 1]])
        else:
            output.append(int(Y[0].argmax(dim=1).item()))
    return ''.join([idx_to_char[i] for i in output])

我們先測試一下predict_rnn函數(shù)。我們將根據(jù)前綴“分開”創(chuàng)作長度為10個字符(不考慮前綴長度)的一段歌詞震贵。因為模型參數(shù)為隨機值利赋,所以預(yù)測結(jié)果也是隨機的。

predict_rnn('分開', 10, rnn, params, init_rnn_state, num_hiddens, vocab_size,
            device, idx_to_char, char_to_idx)

輸出:

'分開西圈緒升王凝瓜必客映'

6.4.5 裁剪梯度

image.png
# 本函數(shù)已保存在d2lzh_pytorch包中方便以后使用
def grad_clipping(params, theta, device):
    norm = torch.tensor([0.0], device=device)
    for param in params:
        norm += (param.grad.data ** 2).sum()
    norm = norm.sqrt().item()
    if norm > theta:
        for param in params:
            param.grad.data *= (theta / norm)

6.4.6 困惑度

我們通常使用困惑度(perplexity)來評價語言模型的好壞猩系∶乃停回憶一下3.4節(jié)(softmax回歸)中交叉熵?fù)p失函數(shù)的定義。困惑度是對交叉熵?fù)p失函數(shù)做指數(shù)運算后得到的值寇甸。特別地塘偎,

  • 最佳情況下,模型總是把標(biāo)簽類別的概率預(yù)測為1拿霉,此時困惑度為1吟秩;
  • 最壞情況下,模型總是把標(biāo)簽類別的概率預(yù)測為0绽淘,此時困惑度為正無窮涵防;
  • 基線情況下,模型總是預(yù)測所有類別的概率都相同收恢,此時困惑度為類別個數(shù)武学。

顯然祭往,任何一個有效模型的困惑度必須小于類別個數(shù)。在本例中火窒,困惑度必須小于詞典大小vocab_size硼补。

6.4.7 定義模型訓(xùn)練函數(shù)

跟之前章節(jié)的模型訓(xùn)練函數(shù)相比,這里的模型訓(xùn)練函數(shù)有以下幾點不同:

  1. 使用困惑度評價模型熏矿。
  2. 在迭代模型參數(shù)前裁剪梯度已骇。
  3. 對時序數(shù)據(jù)采用不同采樣方法將導(dǎo)致隱藏狀態(tài)初始化的不同。相關(guān)討論可參考6.3節(jié)(語言模型數(shù)據(jù)集(周杰倫專輯歌詞))票编。

另外褪储,考慮到后面將介紹的其他循環(huán)神經(jīng)網(wǎng)絡(luò),為了更通用慧域,這里的函數(shù)實現(xiàn)更長一些鲤竹。

# 本函數(shù)已保存在d2lzh_pytorch包中方便以后使用
def train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
                          vocab_size, device, corpus_indices, idx_to_char,
                          char_to_idx, is_random_iter, num_epochs, num_steps,
                          lr, clipping_theta, batch_size, pred_period,
                          pred_len, prefixes):
    if is_random_iter:
        data_iter_fn = d2l.data_iter_random
    else:
        data_iter_fn = d2l.data_iter_consecutive
    params = get_params()
    loss = nn.CrossEntropyLoss()

    for epoch in range(num_epochs):
        if not is_random_iter:  # 如使用相鄰采樣,在epoch開始時初始化隱藏狀態(tài)
            state = init_rnn_state(batch_size, num_hiddens, device)
        l_sum, n, start = 0.0, 0, time.time()
        data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)
        for X, Y in data_iter:
            if is_random_iter:  # 如使用隨機采樣昔榴,在每個小批量更新前初始化隱藏狀態(tài)
                state = init_rnn_state(batch_size, num_hiddens, device)
            else:  
            # 否則需要使用detach函數(shù)從計算圖分離隱藏狀態(tài), 這是為了
            # 使模型參數(shù)的梯度計算只依賴一次迭代讀取的小批量序列(防止梯度計算開銷太大)
                for s in state:
                    s.detach_()

            inputs = to_onehot(X, vocab_size)
            # outputs有num_steps個形狀為(batch_size, vocab_size)的矩陣
            (outputs, state) = rnn(inputs, state, params)
            # 拼接之后形狀為(num_steps * batch_size, vocab_size)
            outputs = torch.cat(outputs, dim=0)
            # Y的形狀是(batch_size, num_steps)辛藻,轉(zhuǎn)置后再變成長度為
            # batch * num_steps 的向量,這樣跟輸出的行一一對應(yīng)
            y = torch.transpose(Y, 0, 1).contiguous().view(-1)
            # 使用交叉熵?fù)p失計算平均分類誤差
            l = loss(outputs, y.long())

            # 梯度清0
            if params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()
            l.backward()
            grad_clipping(params, clipping_theta, device)  # 裁剪梯度
            d2l.sgd(params, lr, 1)  # 因為誤差已經(jīng)取過均值互订,梯度不用再做平均
            l_sum += l.item() * y.shape[0]
            n += y.shape[0]

        if (epoch + 1) % pred_period == 0:
            print('epoch %d, perplexity %f, time %.2f sec' % (
                epoch + 1, math.exp(l_sum / n), time.time() - start))
            for prefix in prefixes:
                print(' -', predict_rnn(prefix, pred_len, rnn, params, init_rnn_state,
                    num_hiddens, vocab_size, device, idx_to_char, char_to_idx))

6.4.8 訓(xùn)練模型并創(chuàng)作歌詞

現(xiàn)在我們可以訓(xùn)練模型了吱肌。首先,設(shè)置模型超參數(shù)仰禽。我們將根據(jù)前綴“分開”和“不分開”分別創(chuàng)作長度為50個字符(不考慮前綴長度)的一段歌詞氮墨。我們每過50個迭代周期便根據(jù)當(dāng)前訓(xùn)練的模型創(chuàng)作一段歌詞。

num_epochs, num_steps, batch_size, lr, clipping_theta = 250, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 50, 50, ['分開', '不分開']

下面采用隨機采樣訓(xùn)練模型并創(chuàng)作歌詞吐葵。

train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
                      vocab_size, device, corpus_indices, idx_to_char,
                      char_to_idx, True, num_epochs, num_steps, lr,
                      clipping_theta, batch_size, pred_period, pred_len,
                      prefixes)

輸出:

epoch 50, perplexity 70.039647, time 0.11 sec
 - 分開 我不要再想 我不能 想你的讓我 我的可 你怎么 一顆四 一顆四 我不要 一顆兩 一顆四 一顆四 我
 - 不分開 我不要再 你你的外 在人  別你的讓我 狂的可 語人兩 我不要 一顆兩 一顆四 一顆四 我不要 一
epoch 100, perplexity 9.726828, time 0.12 sec
 - 分開 一直的美棧人 一起看 我不要好生活 你知不覺 我已好好生活 我知道好生活 后知不覺 我跟了這生活 
 - 不分開堡 我不要再想 我不 我不 我不要再想你 不知不覺 你已經(jīng)離開我 不知不覺 我跟了好生活 我知道好生
epoch 150, perplexity 2.864874, time 0.11 sec
 - 分開 一只會停留 有不它元羞 這蝪什么奇怪的事都有 包括像貓的狗 印地安老斑鳩 平常話不多 除非是烏鴉搶
 - 不分開掃 我不你再想 我不能再想 我不 我不 我不要再想你 不知不覺 你已經(jīng)離開我 不知不覺 我跟了這節(jié)奏
epoch 200, perplexity 1.597790, time 0.11 sec
 - 分開 有杰倫 干 載顆拳滿的讓空美空主 相愛還有個人 再狠狠忘記 你愛過我的證  有晶瑩的手滴 讓說些人
 - 不分開掃 我叫你爸 你打我媽 這樣對嗎干嘛這樣 何必讓它牽鼻子走 瞎 說底牽打我媽要 難道球耳 快使用雙截
epoch 250, perplexity 1.303903, time 0.12 sec
 - 分開 有杰人開留 仙唱它怕羞 蜥蝪橫著走 這里什么奇怪的事都有 包括像貓的狗 印地安老斑鳩 平常話不多 
 - 不分開簡 我不能再想 我不 我不 我不能 愛情走的太快就像龍卷風(fēng) 不能承受我已無處可躲 我不要再想 我不能

接下來采用相鄰采樣訓(xùn)練模型并創(chuàng)作歌詞规揪。

train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
                      vocab_size, device, corpus_indices, idx_to_char,
                      char_to_idx, False, num_epochs, num_steps, lr,
                      clipping_theta, batch_size, pred_period, pred_len,
                      prefixes)

輸出:

epoch 50, perplexity 59.514416, time 0.11 sec
 - 分開 我想要這 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空
 - 不分開 我不要這 全使了雙 我想了這 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空
epoch 100, perplexity 6.801417, time 0.11 sec
 - 分開 我說的這樣笑 想你都 不著我 我想就這樣牽 你你的回不笑多難的  它在云實 有一條事 全你了空  
 - 不分開覺 你已經(jīng)離開我 不知不覺 我跟好這節(jié)活 我該好好生活 不知不覺 你跟了離開我 不知不覺 我跟好這節(jié)
epoch 150, perplexity 2.063730, time 0.16 sec
 - 分開 我有到這樣牽著你的手不放開 愛可不可以簡簡單單沒有傷  古有你煩 我有多煩惱向 你知帶悄 回我的外
 - 不分開覺 你已經(jīng)很個我 不知不覺 我跟了這節(jié)奏 后知后覺 又過了一個秋 后哼哈兮 快使用雙截棍 哼哼哈兮 
epoch 200, perplexity 1.300031, time 0.11 sec
 - 分開 我想要這樣牽著你的手不放開 愛能不能夠永遠(yuǎn)單甜沒有傷害 你 靠著我的肩膀 你 在我胸口睡著 像這樣
 - 不分開覺 你已經(jīng)離開我 不知不覺 我跟了這節(jié)奏 后知后覺 又過了一個秋 后知后覺 我該好好生活 我該好好生
epoch 250, perplexity 1.164455, time 0.11 sec
 - 分開 我有一這樣布 對你依依不舍 連隔壁鄰居都猜到我現(xiàn)在的感受 河邊的風(fēng) 在吹著頭發(fā)飄動 牽著你的手 一
 - 不分開覺 你已經(jīng)離開我 不知不覺 我跟了這節(jié)奏 后知后覺 又過了一個秋 后知后覺 我該好好生活 我該好好生

小結(jié)

  • 可以用基于字符級循環(huán)神經(jīng)網(wǎng)絡(luò)的語言模型來生成文本序列,例如創(chuàng)作歌詞温峭。
  • 當(dāng)訓(xùn)練循環(huán)神經(jīng)網(wǎng)絡(luò)時粒褒,為了應(yīng)對梯度爆炸,可以裁剪梯度诚镰。
  • 困惑度是對交叉熵?fù)p失函數(shù)做指數(shù)運算后得到的值。

注:除代碼外本節(jié)與原書此節(jié)基本相同祥款,原書傳送門

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末清笨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子刃跛,更是在濱河造成了極大的恐慌抠艾,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桨昙,死亡現(xiàn)場離奇詭異检号,居然都是意外死亡腌歉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進(jìn)店門齐苛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翘盖,“玉大人,你說我怎么就攤上這事凹蜂♀裳保” “怎么了?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵玛痊,是天一觀的道長汰瘫。 經(jīng)常有香客問我,道長擂煞,這世上最難降的妖魔是什么混弥? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮对省,結(jié)果婚禮上蝗拿,老公的妹妹穿的比我還像新娘。我一直安慰自己官辽,他們只是感情好蛹磺,可當(dāng)我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著同仆,像睡著了一般萤捆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上俗批,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天俗或,我揣著相機與錄音,去河邊找鬼岁忘。 笑死辛慰,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的干像。 我是一名探鬼主播帅腌,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼麻汰!你這毒婦竟也來了速客?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤五鲫,失蹤者是張志新(化名)和其女友劉穎溺职,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡浪耘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年乱灵,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片七冲。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡痛倚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出癞埠,到底是詐尸還是另有隱情状原,我是刑警寧澤,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布苗踪,位于F島的核電站颠区,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏通铲。R本人自食惡果不足惜毕莱,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望颅夺。 院中可真熱鬧朋截,春花似錦、人聲如沸吧黄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拗慨。三九已至廓八,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赵抢,已是汗流浹背剧蹂。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烦却,地道東北人宠叼。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像其爵,于是被迫代替她去往敵國和親冒冬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容