用 PyTorch 實現基於字符的循環神經網絡

在過去的幾周裏,我花了很多時間用 PyTorch 實現了一個 char-rnn 的版本。我以前從未訓練過神經網絡,所以這可能是一個有趣的開始。

這個想法(來自 循環神經網絡的不合理效應)可以讓你在文本上訓練一個基於字符的循環神經網絡recurrent neural network(RNN),並得到一些出乎意料好的結果。

不過,雖然沒有得到我想要的結果,但是我還是想分享一些示例代碼和結果,希望對其他開始嘗試使用 PyTorch 和 RNN 的人有幫助。

這是 Jupyter 筆記本格式的代碼:char-rnn in PyTorch.ipynb。你可以點擊這個網頁最上面那個按鈕 “Open in Colab”,就可以在 Google 的 Colab 服務中打開,並使用免費的 GPU 進行訓練。所有的東西加起來大概有 75 行代碼,我將在這篇博文中儘可能地詳細解釋。

第一步:準備數據

首先,我們要下載數據。我使用的是古登堡項目Project Gutenberg中的這個數據:Hans Christian Anderson’s fairy tales

!wget -O fairy-tales.txt

這個是準備數據的代碼。我使用 fastai 庫中的 Vocab 類進行數據處理,它能將一堆字母轉換成“詞表”,然後用這個“詞表”把字母變成數字。

之後我們就得到了一個大的數字數組(training_set),我們可以用於訓練我們的模型。

from fastai.text import *
text = unidecode.unidecode(open('fairy-tales.txt').read())
v = Vocab.create((x for x in text), max_vocab=400, min_freq=1)
training_set = torch.Tensor(v.numericalize([x for x in text])).type(torch.LongTensor).cuda()
num_letters = len(v.itos)

第二步:定義模型

這個是 PyTorch 中 LSTM 類的封裝。除了封裝 LSTM 類以外,它還做了三件事:

  1. 對輸入向量進行 one-hot 編碼,使得它們具有正確的維度。
  2. 在 LSTM 層後一層添加一個線性變換,因爲 LSTM 輸出的是一個長度爲 hidden_size 的向量,我們需要的是一個長度爲 input_size 的向量這樣才能把它變成一個字符。
  3. 把 LSTM 隱藏層的輸出向量(實際上有 2 個向量)保存成實例變量,然後在每輪運行結束後執行 .detach() 函數。(我很難解釋清 .detach() 的作用,但我的理解是,它在某種程度上“結束”了模型的求導計算)(LCTT 譯註:detach() 函數是將該張量的 requires_grad 參數設置爲 False,即反向傳播到該張量就結束。)
class MyLSTM(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
        self.h2o = nn.Linear(hidden_size, input_size)
        self.input_size=input_size
        self.hidden = None

    def forward(self, input):
        input = torch.nn.functional.one_hot(input, num_classes=self.input_size).type(torch.FloatTensor).cuda().unsqueeze(0)
        if self.hidden is None:
            l_output, self.hidden = self.lstm(input)
        else:
            l_output, self.hidden = self.lstm(input, self.hidden)
        self.hidden = (self.hidden[0].detach(), self.hidden[1].detach())

        return self.h2o(l_output)

這個代碼還做了一些比較神奇但是不太明顯的功能。如果你的輸入是一個向量(比如 [1,2,3,4,5,6]),對應六個字母,那麼我的理解是 nn.LSTM 會在內部使用沿時間反向傳播更新隱藏向量 6 次。

第三步:編寫訓練代碼

模型不會自己訓練的!

我最開始的時候嘗試用 fastai 庫中的一個輔助類(也是 PyTorch 中的封裝)。我有點疑惑因爲我不知道它在做什麼,所以最後我自己編寫了模型訓練代碼。

下面這些代碼(epoch() 方法)就是有關於一輪訓練過程的基本信息。基本上就是重複做下面這幾件事情:

  1. 往 RNN 模型中傳入一個字符串,比如 and they ought not to teas。(要以數字向量的形式傳入)
  2. 得到下一個字母的預測結果
  3. 計算 RNN 模型預測結果和真實的下一個字母之間的損失函數(e,因爲 tease 這個單詞是以 e 結尾的)
  4. 計算梯度(用 loss.backward() 函數)
  5. 沿着梯度下降的方向修改模型中參數的權重(用 self.optimizer.step() 函數)
class Trainer():
  def __init__(self):
      self.rnn = MyLSTM(input_size, hidden_size).cuda()
      self.optimizer = torch.optim.Adam(self.rnn.parameters(), amsgrad=True, lr=lr)
  def epoch(self):
      i = 0
      while i < len(training_set) - 40:
        seq_len = random.randint(10, 40)
        input, target = training_set[i:i+seq_len],training_set[i+1:i+1+seq_len]
        i += seq_len
        # forward pass
        output = self.rnn(input)
        loss = F.cross_entropy(output.squeeze()[-1:], target[-1:])
        # compute gradients and take optimizer step
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

使用 nn.LSTM 沿着時間反向傳播,不要自己寫代碼

開始的時候我自己寫代碼每次傳一個字母到 LSTM 層中,之後定期計算導數,就像下面這樣:

for i in range(20):
    input, target = next(iter)
    output, hidden = self.lstm(input, hidden)
loss = F.cross_entropy(output, target)
hidden = hidden.detach()
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()

這段代碼每次傳入 20 個字母,每次一個,並且在最後訓練了一次。這個步驟就被稱爲沿時間反向傳播,Karpathy 在他的博客中就是用這種方法。

這個方法有些用處,我編寫的損失函數開始能夠下降一段時間,但之後就會出現峯值。我不知道爲什麼會出現這種現象,但之後我改爲一次傳入 20 個字符到 LSTM 之後(按 seq_len 維度),再進行反向傳播,情況就變好了。

第四步:訓練模型!

我在同樣的數據上重複執行了這個訓練代碼大概 300 次,直到模型開始輸出一些看起來像英文的文本。差不多花了一個多小時吧。

這種情況下我也不關注模型是不是過擬合了,但是如果你在真實場景中訓練模型,應該要在驗證集上驗證你的模型。

第五步:生成輸出!

最後一件要做的事就是用這個模型生成一些輸出。我寫了一個輔助方法從這個訓練好的模型中生成文本(make_preds 和 next_pred)。這裏主要是把向量的維度對齊,重要的一點是:

output = rnn(input)
prediction_vector = F.softmax(output/temperature)
letter = v.textify(torch.multinomial(prediction_vector, 1).flatten(), sep='').replace('_', ' ')

基本上做的事情就是這些:

  1. RNN 層爲字母表中的每一個字母或者符號輸出一個數值向量(output)。
  2. 這個 output 向量並不是一個概率向量,所以需要 F.softmax(output/temperature) 操作,將其轉換爲概率值(也就是所有數值加起來和爲 1)。temperature 某種程度上控制了對更高概率的權重,在限制範圍內,如果設置 temperature=0.0000001,它將始終選擇概率最高的字母。
  3. torch.multinomial(prediction_vector) 用於獲取概率向量,並使用這些概率在向量中選擇一個索引(如 12)。
  4. v.textify 把 12 轉換爲字母。

如果我們想要處理的文本長度爲 300,那麼只需要重複這個過程 300 次就可以了。

結果!

我把預測函數中的參數設置爲 temperature = 1 得到了下面的這些由模型生成的結果。看起來有點像英語,這個結果已經很不錯了,因爲這個模型要從頭開始“學習”英語,並且是在字符序列的級別上進行學習的。

雖然這些話沒有什麼含義,但我們也不知道到底想要得到什麼輸出。

“An who was you colotal said that have to have been a little crimantable and beamed home the beetle. “I shall be in the head of the green for the sound of the wood. The pastor. “I child hand through the emperor’s sorthes, where the mother was a great deal down the conscious, which are all the gleam of the wood they saw the last great of the emperor’s forments, the house of a large gone there was nothing of the wonded the sound of which she saw in the converse of the beetle. “I shall know happy to him. This stories herself and the sound of the young mons feathery in the green safe.”

“That was the pastor. The some and hand on the water sound of the beauty be and home to have been consider and tree and the face. The some to the froghesses and stringing to the sea, and the yellow was too intention, he was not a warm to the pastor. The pastor which are the faten to go and the world from the bell, why really the laborer’s back of most handsome that she was a caperven and the confectioned and thoughts were seated to have great made

下面這些結果是當 temperature=0.1 時生成的,它選擇字符的方式更接近於“每次都選擇出現概率最高的字符”。這就使得輸出結果有很多是重複的。

ole the sound of the beauty of the beetle. “She was a great emperor of the sea, and the sun was so warm to the confectioned the beetle. “I shall be so many for the beetle. “I shall be so many for the beetle. “I shall be so standen for the world, and the sun was so warm to the sea, and the sun was so warm to the sea, and the sound of the world from the bell, where the beetle was the sea, and the sound of the world from the bell, where the beetle was the sea, and the sound of the wood flowers and the sound of the wood, and the sound of the world from the bell, where the world from the wood, and the sound of the

這段輸出對這幾個單詞 beetlesconfectionerssun 和 sea 有着奇怪的執念。

總結!

至此,我的結果遠不及 Karpathy 的好,可能有一下幾個原因:

  1. 沒有足夠多的訓練數據。
  2. 訓練了一個小時之後我就沒有耐心去查看 Colab 筆記本上的信息。
  3. Karpathy 使用了兩層LSTM,包含了更多的參數,而我只使用了一層。
  4. 完全是另一回事。

但我得到了一些大致說得過去的結果!還不錯!


via: https://jvns.ca/blog/2020/11/30/implement-char-rnn-in-pytorch/

作者:Julia Evans 選題:lujun9972 譯者:zhangxiangping 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章