PyTorch NLP From Scratch: 生成名稱與字符級(jí)RNN

2020-09-16 11:51 更新

原文:PyTorch NLP From Scratch: 生成名稱與字符級(jí)RNN

作者Sean Robertson

這是我們關(guān)于“NLP From Scratch”的三個(gè)教程中的第二個(gè)。 在<cite>第一個(gè)教程< / intermediate / char_rnn_classification_tutorial ></cite> 中,我們使用了 RNN 將名稱分類為來源語言。 這次,我們將轉(zhuǎn)過來并使用語言生成名稱。

  1. > python sample.py Russian RUS
  2. Rovakov
  3. Uantov
  4. Shavakov
  5. > python sample.py German GER
  6. Gerren
  7. Ereng
  8. Rosher
  9. > python sample.py Spanish SPA
  10. Salla
  11. Parer
  12. Allan
  13. > python sample.py Chinese CHI
  14. Chan
  15. Hang
  16. Iun

我們?nèi)栽谑止ぶ谱鲙в幸恍┚€性層的小型 RNN。 最大的區(qū)別在于,我們無需輸入名稱中的所有字母即可預(yù)測(cè)類別,而是輸入類別并一次輸出一個(gè)字母。 反復(fù)預(yù)測(cè)字符以形成語言(這也可以用單詞或其他高階結(jié)構(gòu)來完成)通常稱為“語言模型”。

推薦讀物:

我假設(shè)您至少已經(jīng)安裝了 PyTorch,了解 Python 和了解 Tensors:

  • https://pytorch.org/ 有關(guān)安裝說明
  • 使用 PyTorch 進(jìn)行深度學(xué)習(xí):60 分鐘的閃電戰(zhàn)通常開始使用 PyTorch
  • 使用示例學(xué)習(xí) PyTorch 進(jìn)行廣泛而深入的概述
  • PyTorch(以前的 Torch 用戶)(如果您以前是 Lua Torch 用戶)

了解 RNN 及其工作方式也將很有用:

我還建議上一個(gè)教程從頭開始進(jìn)行 NLP:使用字符級(jí) RNN 對(duì)名稱進(jìn)行分類

準(zhǔn)備數(shù)據(jù)

Note

從的下載數(shù)據(jù),并將其提取到當(dāng)前目錄。

有關(guān)此過程的更多詳細(xì)信息,請(qǐng)參見上一教程。 簡而言之,有一堆純文本文件data/names/[Language].txt,每行都有一個(gè)名稱。 我們將行分割成一個(gè)數(shù)組,將 Unicode 轉(zhuǎn)換為 ASCII,最后得到一個(gè)字典{language: [names ...]}。

  1. from __future__ import unicode_literals, print_function, division
  2. from io import open
  3. import glob
  4. import os
  5. import unicodedata
  6. import string
  7. all_letters = string.ascii_letters + " .,;'-"
  8. n_letters = len(all_letters) + 1 # Plus EOS marker
  9. def findFiles(path): return glob.glob(path)
  10. ## Turn a Unicode string to plain ASCII, thanks to https://stackoverflow.com/a/518232/2809427
  11. def unicodeToAscii(s):
  12. return ''.join(
  13. c for c in unicodedata.normalize('NFD', s)
  14. if unicodedata.category(c) != 'Mn'
  15. and c in all_letters
  16. )
  17. ## Read a file and split into lines
  18. def readLines(filename):
  19. lines = open(filename, encoding='utf-8').read().strip().split('\n')
  20. return [unicodeToAscii(line) for line in lines]
  21. ## Build the category_lines dictionary, a list of lines per category
  22. category_lines = {}
  23. all_categories = []
  24. for filename in findFiles('data/names/*.txt'):
  25. category = os.path.splitext(os.path.basename(filename))[0]
  26. all_categories.append(category)
  27. lines = readLines(filename)
  28. category_lines[category] = lines
  29. n_categories = len(all_categories)
  30. if n_categories == 0:
  31. raise RuntimeError('Data not found. Make sure that you downloaded data '
  32. 'from https://download.pytorch.org/tutorial/data.zip and extract it to '
  33. 'the current directory.')
  34. print('# categories:', n_categories, all_categories)
  35. print(unicodeToAscii("O'Néàl"))

出:

  1. ## categories: 18 ['French', 'Czech', 'Dutch', 'Polish', 'Scottish', 'Chinese', 'English', 'Italian', 'Portuguese', 'Japanese', 'German', 'Russian', 'Korean', 'Arabic', 'Greek', 'Vietnamese', 'Spanish', 'Irish']
  2. O'Neal

建立網(wǎng)絡(luò)

該網(wǎng)絡(luò)使用最后一個(gè)教程的 RNN 擴(kuò)展了,并為類別張量附加了一個(gè)參數(shù),該參數(shù)與其他張量串聯(lián)在一起。 類別張量是一個(gè)熱向量,就像字母輸入一樣。

我們將輸出解釋為下一個(gè)字母的概率。 采樣時(shí),最有可能的輸出字母用作下一個(gè)輸入字母。

我添加了第二個(gè)線性層o2o(將隱藏和輸出結(jié)合在一起之后),以使它具有更多的肌肉可以使用。 還有一個(gè)輟學(xué)層,以給定的概率(此處為 0.1)將輸入的部分隨機(jī)歸零,通常用于模糊輸入以防止過擬合。 在這里,我們?cè)诰W(wǎng)絡(luò)的末端使用它來故意添加一些混亂并增加采樣種類。

img

  1. import torch
  2. import torch.nn as nn
  3. class RNN(nn.Module):
  4. def __init__(self, input_size, hidden_size, output_size):
  5. super(RNN, self).__init__()
  6. self.hidden_size = hidden_size
  7. self.i2h = nn.Linear(n_categories + input_size + hidden_size, hidden_size)
  8. self.i2o = nn.Linear(n_categories + input_size + hidden_size, output_size)
  9. self.o2o = nn.Linear(hidden_size + output_size, output_size)
  10. self.dropout = nn.Dropout(0.1)
  11. self.softmax = nn.LogSoftmax(dim=1)
  12. def forward(self, category, input, hidden):
  13. input_combined = torch.cat((category, input, hidden), 1)
  14. hidden = self.i2h(input_combined)
  15. output = self.i2o(input_combined)
  16. output_combined = torch.cat((hidden, output), 1)
  17. output = self.o2o(output_combined)
  18. output = self.dropout(output)
  19. output = self.softmax(output)
  20. return output, hidden
  21. def initHidden(self):
  22. return torch.zeros(1, self.hidden_size)

訓(xùn)練

準(zhǔn)備訓(xùn)練

首先,helper 函數(shù)獲取隨機(jī)對(duì)(類別,行):

  1. import random
  2. ## Random item from a list
  3. def randomChoice(l):
  4. return l[random.randint(0, len(l) - 1)]
  5. ## Get a random category and random line from that category
  6. def randomTrainingPair():
  7. category = randomChoice(all_categories)
  8. line = randomChoice(category_lines[category])
  9. return category, line

對(duì)于每個(gè)時(shí)間步(即,對(duì)于訓(xùn)練詞中的每個(gè)字母),網(wǎng)絡(luò)的輸入將為(category, current letter, hidden state),而輸出將為(next letter, next hidden state)。 因此,對(duì)于每個(gè)訓(xùn)練集,我們都需要類別,一組輸入字母和一組輸出/目標(biāo)字母。

由于我們正在預(yù)測(cè)每個(gè)時(shí)間步中當(dāng)前字母的下一個(gè)字母,因此字母對(duì)是該行中連續(xù)字母的組-例如 對(duì)于"ABCD<EOS>",我們將創(chuàng)建(“ A”,“ B”),(“ B”,“ C”),(“ C”,“ D”),(“ D”,“ EOS”)。

img

類別張量是大小為<1 x n_categories>的一熱張量。 訓(xùn)練時(shí),我們會(huì)隨時(shí)隨地將其饋送到網(wǎng)絡(luò)中-這是一種設(shè)計(jì)選擇,它可能已被包含為初始隱藏狀態(tài)或某些其他策略的一部分。

  1. ## One-hot vector for category
  2. def categoryTensor(category):
  3. li = all_categories.index(category)
  4. tensor = torch.zeros(1, n_categories)
  5. tensor[0][li] = 1
  6. return tensor
  7. ## One-hot matrix of first to last letters (not including EOS) for input
  8. def inputTensor(line):
  9. tensor = torch.zeros(len(line), 1, n_letters)
  10. for li in range(len(line)):
  11. letter = line[li]
  12. tensor[li][0][all_letters.find(letter)] = 1
  13. return tensor
  14. ## LongTensor of second letter to end (EOS) for target
  15. def targetTensor(line):
  16. letter_indexes = [all_letters.find(line[li]) for li in range(1, len(line))]
  17. letter_indexes.append(n_letters - 1) # EOS
  18. return torch.LongTensor(letter_indexes)

為了方便訓(xùn)練,我們將使用randomTrainingExample函數(shù)來提取隨機(jī)(類別,行)對(duì),并將其轉(zhuǎn)換為所需的(類別,輸入,目標(biāo))張量。

  1. ## Make category, input, and target tensors from a random category, line pair
  2. def randomTrainingExample():
  3. category, line = randomTrainingPair()
  4. category_tensor = categoryTensor(category)
  5. input_line_tensor = inputTensor(line)
  6. target_line_tensor = targetTensor(line)
  7. return category_tensor, input_line_tensor, target_line_tensor

訓(xùn)練網(wǎng)絡(luò)

與僅使用最后一個(gè)輸出的分類相反,我們?cè)诿總€(gè)步驟進(jìn)行預(yù)測(cè),因此在每個(gè)步驟都計(jì)算損失。

autograd 的神奇之處在于,您可以簡單地將每一步的損失相加,然后在末尾調(diào)用。

  1. criterion = nn.NLLLoss()
  2. learning_rate = 0.0005
  3. def train(category_tensor, input_line_tensor, target_line_tensor):
  4. target_line_tensor.unsqueeze_(-1)
  5. hidden = rnn.initHidden()
  6. rnn.zero_grad()
  7. loss = 0
  8. for i in range(input_line_tensor.size(0)):
  9. output, hidden = rnn(category_tensor, input_line_tensor[i], hidden)
  10. l = criterion(output, target_line_tensor[i])
  11. loss += l
  12. loss.backward()
  13. for p in rnn.parameters():
  14. p.data.add_(-learning_rate, p.grad.data)
  15. return output, loss.item() / input_line_tensor.size(0)

為了跟蹤訓(xùn)練需要多長時(shí)間,我添加了一個(gè)timeSince(timestamp)函數(shù),該函數(shù)返回人類可讀的字符串:

  1. import time
  2. import math
  3. def timeSince(since):
  4. now = time.time()
  5. s = now - since
  6. m = math.floor(s / 60)
  7. s -= m * 60
  8. return '%dm %ds' % (m, s)

訓(xùn)練照常進(jìn)行-召集訓(xùn)練多次,等待幾分鐘,每print_every個(gè)示例打印當(dāng)前時(shí)間和損失,并在all_losses中將每個(gè)plot_every實(shí)例的平均損失存儲(chǔ)下來,以便以后進(jìn)行繪圖。

  1. rnn = RNN(n_letters, 128, n_letters)
  2. n_iters = 100000
  3. print_every = 5000
  4. plot_every = 500
  5. all_losses = []
  6. total_loss = 0 # Reset every plot_every iters
  7. start = time.time()
  8. for iter in range(1, n_iters + 1):
  9. output, loss = train(*randomTrainingExample())
  10. total_loss += loss
  11. if iter % print_every == 0:
  12. print('%s (%d %d%%) %.4f' % (timeSince(start), iter, iter / n_iters * 100, loss))
  13. if iter % plot_every == 0:
  14. all_losses.append(total_loss / plot_every)
  15. total_loss = 0

Out:

  1. 0m 21s (5000 5%) 2.7607
  2. 0m 41s (10000 10%) 2.8047
  3. 1m 0s (15000 15%) 3.8541
  4. 1m 19s (20000 20%) 2.1222
  5. 1m 39s (25000 25%) 3.7181
  6. 1m 58s (30000 30%) 2.6274
  7. 2m 17s (35000 35%) 2.4538
  8. 2m 37s (40000 40%) 1.3385
  9. 2m 56s (45000 45%) 2.1603
  10. 3m 15s (50000 50%) 2.2497
  11. 3m 35s (55000 55%) 2.7588
  12. 3m 54s (60000 60%) 2.3754
  13. 4m 13s (65000 65%) 2.2863
  14. 4m 33s (70000 70%) 2.3610
  15. 4m 52s (75000 75%) 3.1793
  16. 5m 11s (80000 80%) 2.3203
  17. 5m 31s (85000 85%) 2.5548
  18. 5m 50s (90000 90%) 2.7351
  19. 6m 9s (95000 95%) 2.7740
  20. 6m 29s (100000 100%) 2.9683

繪制損失

繪制 all_losses 的歷史損失可顯示網(wǎng)絡(luò)學(xué)習(xí)情況:

  1. import matplotlib.pyplot as plt
  2. import matplotlib.ticker as ticker
  3. plt.figure()
  4. plt.plot(all_losses)

../_images/sphx_glr_char_rnn_generation_tutorial_001.png

網(wǎng)絡(luò)采樣

為了示例,我們給網(wǎng)絡(luò)一個(gè)字母,詢問下一個(gè)字母是什么,將其作為下一個(gè)字母輸入,并重復(fù)直到 EOS 令牌。

  • 為輸入類別,起始字母和空隱藏狀態(tài)創(chuàng)建張量
  • 用起始字母創(chuàng)建一個(gè)字符串output_name
  • 直到最大輸出長度,
    • 將當(dāng)前信件輸入網(wǎng)絡(luò)
    • 從最高輸出中獲取下一個(gè)字母,以及下一個(gè)隱藏狀態(tài)
    • 如果字母是 EOS,請(qǐng)?jiān)诖颂幫V?/li>
    • 如果是普通字母,請(qǐng)?zhí)砑拥?code>output_name并繼續(xù)
  • 返回姓氏

Note

不必給它起一個(gè)開始字母,另一種策略是在訓(xùn)練中包括一個(gè)“字符串開始”令牌,并讓網(wǎng)絡(luò)選擇自己的開始字母。

  1. max_length = 20
  2. ## Sample from a category and starting letter
  3. def sample(category, start_letter='A'):
  4. with torch.no_grad(): # no need to track history in sampling
  5. category_tensor = categoryTensor(category)
  6. input = inputTensor(start_letter)
  7. hidden = rnn.initHidden()
  8. output_name = start_letter
  9. for i in range(max_length):
  10. output, hidden = rnn(category_tensor, input[0], hidden)
  11. topv, topi = output.topk(1)
  12. topi = topi[0][0]
  13. if topi == n_letters - 1:
  14. break
  15. else:
  16. letter = all_letters[topi]
  17. output_name += letter
  18. input = inputTensor(letter)
  19. return output_name
  20. ## Get multiple samples from one category and multiple starting letters
  21. def samples(category, start_letters='ABC'):
  22. for start_letter in start_letters:
  23. print(sample(category, start_letter))
  24. samples('Russian', 'RUS')
  25. samples('German', 'GER')
  26. samples('Spanish', 'SPA')
  27. samples('Chinese', 'CHI')

Out:

  1. Rovakovak
  2. Uariki
  3. Sakilok
  4. Gare
  5. Eren
  6. Rour
  7. Salla
  8. Pare
  9. Alla
  10. Cha
  11. Honggg
  12. Iun

練習(xí)題

  • 嘗試使用其他類別的數(shù)據(jù)集->行,例如:
    • 虛構(gòu)系列->角色名稱
    • 詞性->詞
    • 國家->城市
  • 使用“句子開頭”標(biāo)記,以便可以在不選擇開始字母的情況下進(jìn)行采樣
  • 通過更大和/或形狀更好的網(wǎng)絡(luò)獲得更好的結(jié)果
    • 嘗試 nn.LSTM 和 nn.GRU 層
    • 將多個(gè)這些 RNN 合并為更高級(jí)別的網(wǎng)絡(luò)

腳本的總運(yùn)行時(shí)間:(6 分鐘 29.292 秒)

Download Python source code: char_rnn_generation_tutorial.py Download Jupyter notebook: char_rnn_generation_tutorial.ipynb

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)