一. 簡單復習一下RNN
RNN
RNN適用于處理序列數據,令是序列的第i個元素,那么
就是一個長度為
的序列,NLP中最常見的元素是單詞,對應的序列是句子。
RNN使用同一個神經網絡處理序列中的每一個元素。同時,為了表示序列的先后關系,RNN還有表示記憶的隱變量a,它記錄了前幾個元素的信息。對第t個元素的運算如下:
其中,W,b都是線性運算的參數,g是激活函數,隱藏層的激活函數一般用tanh,輸出層的激活函數根據實際情況選用。另外a的初始值,
語言模型
語言模型是NLP中的一個基礎任務。語言模型是NLP中的一個基礎任務。假設我們以單詞為基本元素,句子為序列,那么一個語言模型能夠輸出某句話的出現概率。通過比較不同句子的出現概率,我們能夠開發出很多應用。比如在英語里,同音的"apple and pear"比"apple and pair"的出現概率高(更可能是一個合理的句子)。當一個語音識別軟件聽到這句話時,可以分別寫下這兩句發音相近的句子,再根據語言模型斷定這句話應該寫成前者。
規范地說,對于序列,語言模型的輸出是
?這個柿子也可以寫成:
即一句話的出現概率,等于第一個單詞出現在句首的概率,乘上第二個單詞在第一個單詞之后的概率,乘上第三個單詞再第一、二個單詞之后的概率,這樣一直乘下去。
單詞級的語言模型需要的數據量比較大,在這個項目中,我們將搭建一個字母級語言模型。即我們以字母為基本元素,單詞為序列。語言模型會輸出每個單詞的概率。比如我們輸入"apple"和"appll",語言模型會告訴我們單詞"apple"的概率更高,這個單詞更可能是一個正確的英文單詞。
RNN語言模型
為了計算語言模型的概率,我們可以用RNN分別輸出最后把這些概率乘起來。
這個式子,說白了就是i給定前t-1個字母,猜一猜第t個字母最可能是哪個,比如給定了前四個字母"appl",第五個單詞構成"apply", "apple"的概率比較大,構成"appll", "appla"的概率較小。
為了讓神經網絡學會這個概率,我們可以令RNN的輸入為<sos> x_1, x_2, ..., x_T
,RNN的標簽為x_1, x_2, ..., x_T, <eos>
(<sos>
和<eos>
是句子開始和結束的特殊字符,實際實現中可以都用空格' '
表示。<sos>
也可以粗暴地用全零向量表示),即輸入和標簽都是同一個單詞,只是它們的位置差了一格。模型每次要輸出一個softmax的多分類概率,預測給定前幾個字母時下一個字母的概率。這樣,這個模型就能學習到前面那個條件概率了。
二. ?代碼細節
參考?https://zhuanlan.zhihu.com/p/558838663
1. 數據集獲取:
為了搭建字母級語言模型,我們只需要隨便找一個有很多單詞的數據集。這里我選擇了斯坦福大學的大型電影數據集,它收錄了IMDb上的電影評論,正面評論和負面評論各25000條。這個數據集本來是用于情感分類這一比較簡單的NLP任務,拿來搭字母級語言模型肯定是沒問題的。
這個數據集的文件結構大致如下:
├─test
│ ├─neg
│ │ ├ 0_2.txt
│ │ ├ 1_3.txt
│ │ └ ...
│ └─pos
├─train
│ ├─neg
│ └─pos
└─imdb.vocab
其中,imdb.vocab記錄了數據集中的所有單詞,一行一個。test和train測試集和訓練集,它們的neg和pos子文件夾分別記錄了負面評論和正面評論。每一條評論都是一句話,存在txt文件里。
代碼細節:
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import os
import re#from dldemos.BasicRNN.constant import EMBEDDING_LENGTH, LETTER_LIST, LETTER_MAPdef read_imdb_words(dir = 'data',split='pos',is_train=True,n_files=1000):subdir = 'train' if is_train else 'test'dir = os.path.join(dir,subdir,split)all_str = ''for file in os.listdir(dir):if n_files <= 0:breakwith open(os.path.join(dir, file), 'rb') as f:line = f.read().decode('utf-8')all_str += linen_files -= 1words = re.sub(u'([^\u0020\u0061-\u007a])','',all_str.lower()).split(' ')return wordsdef read_imdb(dir='data', split = 'pos', is_train = True):subdir = 'train' if is_train else 'test'dir = os.path.join(dir, subdir, split)lines = []for file in os.listdir(dir):with open(os.path.join(dir, file), 'rb') as f:line = f.read().decode('utf-8')lines.append(line)return linesdef read_imbd_vocab(dir='data'):fn = os.path.join(dir, 'imdb.vocab')with open(fn, 'rb') as f:word = f.read().decode('utf-8').replace('\n', ' ')print("read_imbd_vocab:",word)# words = re.sub(r'([^\u0020a-z])', '',word.lower().split(' '))# 清理字符串(移除所有非空格和非小寫字母的字符)words= re.sub(r'[^ a-z]', '', word.lower())# 按空格分割單詞words = words.split(' ')print("read_imbd_vocab:",words)filtered_words = [w for w in words if len(w) > 0]return filtered_wordsvocab = read_imbd_vocab()
print(vocab[0])
print(vocab[1])lines = read_imdb()
print('Length of the files:', len(lines))
print('lines[0]', lines[0])
words = read_imdb_words(n_files=100)print('Length of the words:', len(words))
for i in range(5):print(words[i])
read_imbd_vocab最終返回的數據單詞如下:?
1)words = re.sub(u'([^\u0020\u0061-\u007a])', '', all_str.lower())
步驟 1:?
all_str.lower()
作用:將原字符串?
all_str
?轉換為全小寫。示例:
"Hello World! 123"
?→?"hello world! 123"
步驟 2:?
re.sub(u'([^\u0020\u0061-\u007a])', '', ...)
正則表達式模式:
([^\u0020\u0061-\u007a])
\u0020
:Unicode 的空格字符(ASCII 32)。
\u0061-\u007a
:Unicode 的小寫字母范圍(a
?到?z
,對應 ASCII 97-122)。
[^...]
:匹配不包含在括號內的任何字符。整體含義:匹配所有非空格且非小寫字母的字符。
替換操作:將這些字符替換為空字符串(即刪除它們)。
示例:
"hello world! 123"
?→?"hello world "
(移除了?!
?和?123
,但末尾可能留下多余空格)import re# 清理字符串(移除所有非空格和非小寫字母的字符) cleaned_word = re.sub(r'[^ a-z]', '', word.lower())# 按空格分割單詞 words = cleaned_word.split(' ')
步驟 3:?
split(' ')
作用:按空格分割字符串為單詞列表。
潛在問題:連續空格可能導致空字符串(如?
"hello world"
?→?["hello", "", "world"]
)。示例:
"hello world "
?→?["hello", "world", ""]
清理字符串:
轉換為全小寫。
刪除所有非小寫字母(
a-z
)和非空格(?
)的字符。分割單詞:
按空格分割成單詞列表(可能包含空字符串)。
2).?output = torch.empty_like(word)
?
這行代碼的作用是:
-
torch.empty_like(input)
?是一個PyTorch函數,它會創建一個新張量(output
),滿足以下條件:-
形狀相同:與輸入張量?
word
?的維度(shape
)完全一致。 -
數據類型相同:與?
word
?的數據類型(dtype
,如?float32
、int64
)相同。 -
設備相同:與?
word
?所在的設備(如CPU或GPU)一致。 -
未初始化內存:新張量的元素值是未定義的(可能是任意隨機值,取決于內存的當前狀態)。
-
2.數據集讀取
RNN的輸入不是字母,而是表示字母的向量。最簡單的字母表示方式是one-hot編碼,每一個字母用一個某一維度為1,其他維度為0的向量表示。比如我有a, b, c三個字母,它們的one-hot編碼分別為:
a: [1,0,0]
b: [0,1,0]
c: [0,0,1]?
EMBEDDING_LENGTH = 27
LETTER_MAP = {' ': 0}
ENCODING_MAP =[' ']
for i in range(26):LETTER_MAP[chr(ord('a')+i)] =i +1ENCODING_MAP.append(chr(ord('a')+i))
LETTER_LIST = list(LETTER_MAP.keys())print("LETTER_MAP:",LETTER_MAP)
print("ENCODING_MAP:",ENCODING_MAP)
print("LETTER_LIST:",LETTER_LIST)'''
字符生成: chr(ord('a') + i)動態生成每個小寫字母:
ord('a')返回a的ASCII碼97。
97 + i隨i從0到25變化, 得到97到122 (對應ASCII中的a到z)。
chr()將ASCII碼轉換為字符,得到a, b, ..., z。
打印結果更直觀:?
?Pytorch提供了用于管理數據讀取的Dataset類。Dataset一般只會存儲數據的信息,而非原始數據,比如存儲圖片路徑,而每次讀取時,Dataset才會去實際讀取數據。在這個項目里,我們用Data set存儲原始的單詞數組,實際讀取時,每次返回一個one-hot 編碼的向量。
實際dataset使用時,要繼承這個類,實現_len_和__getitem__方法。前者表示獲取數據集的長度,后者表示獲取某項數據。
import torch
from torch.utils.data import DataLoader,Datasetclass WordDataset(Dataset):def __init__(self, words, max_length, is_one_hot=True):super().__init__()self.words = wordsself.n_words = len(words)self.max_length = max_lengthself.is_onehot = is_one_hotdef __len__(self):return self.n_wordsdef __getitem__(self, index):word = self.words[index] + ' 'word_length = len(word)#print("word:",word)if self.is_onehot:tensor = torch.zeros(self.max_length, EMBEDDING_LENGTH)for i in range(self.max_length):if i < word_length:tensor[i][LETTER_MAP[word[i]]] = 1else:tensor[i][0] = 1else:tensor = torch.zeros(self.max_length, dtype = torch.long)for i in range(word_length):tensor[i] = LETTER_MAP[word[i]]return tensor
構造數據集的參數是
words, max_length, is_onehot
。words
是單詞數組。max_length
表示單詞的最大長度。在訓練時,我們一般要傳入一個batch的單詞。可是,單詞有長有短,我們不可能拿一個動態長度的數組去表示單詞。為了統一地表達所有單詞,我們可以記錄單詞的最大長度,把較短的單詞填充空字符,直到最大長度。is_onehot
表示是不是one-hot編碼,我設計的這個數據集既能輸出用數字標簽表示的單詞(比如abc表示成[0, 1, 2]
),也能輸出one-hoe編碼表示的單詞(比如abc表示成[[1, 0, 0], [0, 1, 0], [0, 0, 1]]
)。
在獲取數據集時,我們要根據是不是one-hot編碼,先準備好一個全是0的輸出張量。如果存的是one-hot編碼,張量的形狀是[MAX_LENGTH, EMBEDDING_LENGTH]
,第一維是單詞的最大長度,第二維是one-hot編碼的長度。而如果是普通的標簽數組,則張量的形狀是[MAX_LENGTH]
。準備好張量后,遍歷每一個位置,令one-hot編碼的對應位為1,或者填入數字標簽。
另外,我們用空格表示單詞的結束。要在處理前給單詞加一個' '
,保證哪怕最長的單詞也會至少有一個空格。
有了數據集類,結合之前寫好的數據集獲取函數,可以搭建一個DataLoader。DataLoader是PyTorch提供的數據讀取類,它可以方便地從Dataset的子類里讀取一個batch的數據,或者以更高級的方式取數據(比如隨機取數據)。?
def get_dataloader_and_max_langth(limit_length = None, is_one_hot = True, is_vocab = True):if is_vocab:words = read_imbd_vocab()else:words = read_imdb_words(n_files=200)max_length = 0for word in words:max_length = max(max_length, len(word))if limit_length is not None and max_length > limit_length:words = [w for w in words if len(w) <= limit_length]max_length = limit_lengthmax_length +=1dataset = WordDataset(words, max_length, is_one_hot)print("max_length:",max_length)return DataLoader(dataset, batch_size=256), max_length
這個函數會先調用之前編寫的數據讀取API獲取單詞數組。之后,函數會計算最長的單詞長度。這里,我用limit_length
過濾了過長的單詞。據實驗,這個數據集里最長的單詞竟然有60多個字母,把短單詞填充至60需要浪費大量的計算資源。因此,我設置了limit_length
這個參數,不去讀取那些過長的單詞。
計算完最大長度后,別忘了+1,保證每個單詞后面都有一個表示單詞結束的空格。
最后,用DataLoader(dataset, batch_size=256)
就可以得到一個DataLoader。batch_size
就是指定batch size的參數。我們這個神經網絡很小,輸入數據也很小,可以選一個很大的batch size加速訓練。
3.模型預覽
class RNN1(nn.Module):def __init__(self, hidden_units = 32):super().__init__()self.hidden_units = hidden_unitsself.linear_a = nn.Linear(self.hidden_units + EMBEDDING_LENGTH, hidden_units)self.linear_y = nn.Linear(hidden_units, EMBEDDING_LENGTH)self.tanh = nn.Tanh()def forward(self, word: torch.Tensor):#word shape: [batch, max_word_length, embedding_length]batch, Tx = word.shape[0:2]#word shape: [max_word_length, batch, embedding_length]word = torch.transpose(word, 0, 1)output = torch.empty_like(word)a = torch.zeros(batch, self.hidden_units)x = torch.zeros(batch, EMBEDDING_LENGTH)for i in range (Tx):next_a = self.tanh(self.linear_a(torch.cat((x,a),1)))hat_y = self.linear_y(next_a)output[i] = hat_yx = word[i]a = next_areturn torch.transpose(output, 0, 1)
我們可以把第一行公式里的兩個合并一下,拼接一下。這樣,只需要兩個線性層就可以描述RNN了。
因此,在初始化函數中,我們定義兩個線性層linear_a
,linear_y
。另外,hidden_units
表示隱藏層linear_a
的神經元數目。tanh
就是普通的tanh函數,它用作第一層的激活函數。
linear_a
就是公式的第一行,由于我們把輸入x
和狀態a
拼接起來了,這一層的輸入通道數是hidden_units + EMBEDDING_LENGTH
,輸出通道數是hidden_units
。第二層linear_y
表示公式的第二行。我們希望RNN能預測下一個字母的出現概率,因此這一層的輸出通道數是EMBEDDING_LENGTH=27
,即字符個數。
在描述模型運行的forward
函數中,我們先準備好輸出張量,再初始化好隱變量a
和第一輪的輸入x
。根據公式,循環遍歷序列的每一個字母,用a, x
計算hat_y
,并維護每一輪的a, x
。最后,所有hat_y
拼接成的output
就是返回結果。
我們來看一看這個函數的細節。一開始,輸入張量word
的形狀是[batch數,最大單詞長度,字符數=27]
。我們提前獲取好形狀信息。
# word shape: [batch, max_word_length, embedding_length]
batch, Tx = word.shape[0:2]
我們循環遍歷的其實是單詞長度那一維。為了方便理解代碼,我們可以把單詞長度那一維轉置成第一維。根據這個新的形狀,我們準備好同形狀的輸出張量。輸出張量output[i][j]
表示第j個batch的序列的第i個元素的27個字符預測結果。
4.訓練
首先,調用之前編寫的函數,準備好dataloader
和model
。同時,準備好優化器optimizer
和損失函數citerion
。優化器和損失函數按照常見配置選擇即可。
這個語言模型一下就能訓練完,做5個epoch就差不多了。每一代訓練中, 先調用模型求出hat_y
,再調用損失函數citerion
,最后反向傳播并優化模型參數。
def train_rnn1():data, max_length = get_dataloader_and_max_langth(19)model = RNN1()optimizer = torch.optim.Adam(model.parameters(), lr=0.001)citerion = torch.nn.CrossEntropyLoss()for epoch in range(5):loss_sum = 0dataset_len = len(data.dataset)for y in data:hat_y = model(y)n, Tx, _ = hat_y.shapehat_y = torch.reshape(hat_y,(n*Tx,-1))y = torch.reshape(y, (n* Tx, -1))label_y = torch.argmax(y, 1)loss = citerion(hat_y, label_y)optimizer.zero_grad()loss.backward()torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)optimizer.step()loss_sum += lossprint(f'Epoch {epoch}. loss: {loss_sum / dataset_len}')torch.save(model.state_dict(), 'rnn1.pth')return model
算損失函數前需要預處理一下數據,交叉熵損失函數默認hat_y
的維度是[batch數,類型數]
,label_y
是一個一維整形標簽數組。而模型的輸出形狀是[batch數,最大單詞長度,字符數]
,我們要把前兩個維度融合在一起。另外,我們并沒有提前準備好label_y
,需要調用argmax
把one-hot編碼轉換回標簽。
之后就是調用PyTorch的自動求導功能。注意,為了防止RNN梯度過大,我們可以用clip_grad_norm_
截取梯度的最大值。?
輸出:
5. 測試
我們可以手動為字母級語言模型寫幾個測試用例,看看每一個單詞的概率是否和期望的一樣。我的測試單詞列表是:
test_words = ['apple', 'appll', 'appla', 'apply', 'bear', 'beer', 'berr', 'beee', 'car','cae', 'cat', 'cac', 'caq', 'query', 'queee', 'queue', 'queen', 'quest','quess', 'quees'
]
幾組長度一樣,但是最后幾個字母不太一樣的“單詞”。通過觀察這些詞的概率,我們能夠驗證語言模型的正確性。理論上來說,英文里的正確單詞的概率會更高。
我們的模型只能輸出每一個單詞的softmax前結果。我們還要為模型另寫一個求語言模型概率的函數。
@torch.no_grad()def language_model(self, word: torch.Tensor):# word shape: [batch, max_word_length, embedding_length]batch, Tx = word.shape[0:2]# word shape: [max_word_length, batch, embedding_length]# word_label shape: [max_word_length, batch]word = torch.transpose(word, 0, 1)word_label = torch.argmax(word, 2)# output shape: [batch]output = torch.ones(batch, device=word.device)a = torch.zeros(batch, self.hidden_units, device=word.device)x = torch.zeros(batch, EMBEDDING_LENGTH, device=word.device)for i in range(Tx):next_a = self.tanh(self.linear_a(torch.cat((a, x), 1)))tmp = self.linear_y(next_a)hat_y = F.softmax(tmp, 1)probs = hat_y[torch.arange(batch), word_label[i]]#從hat_y里取出每一個batch里word_label[i]處的概率output *= probsx = word[i]a = next_areturn output@torch.no_grad()def sample_word(self):batch = 1output = ''a = torch.zeros(batch, self.hidden_units)x = torch.zeros(batch, EMBEDDING_LENGTH)for i in range(10):next_a = self.tanh(self.linear_a(torch.cat((a,x),1)))tmp = self.linear_y(next_a)hat_y = F.softmax(tmp, 1)np_prob = hat_y[0].detach().cpu().numpy()letter = np.random.choice(LETTER_LIST, p=np_prob)output += letterif letter == ' ':breakx = torch.zeros(batch, EMBEDDING_LENGTH)x[0][LETTER_MAP[letter]] = 1a = next_areturn output
這個函數和forward
大致相同。只不過,這次我們的輸出output
要表示每一個單詞的概率。因此,它被初始化成一個全1的向量。
# output shape: [batch]
output = torch.ones(batch, device=word.device)
每輪算完最后一層的輸出后,我們手動調用F.softmax
得到softmax的概率值。
tmp = self.linear_y(next_a)
hat_y = F.softmax(tmp, 1)
接下來,我們要根據每一個batch當前位置的單詞,去hat_y
里取出需要的概率。比如第2個batch當前的字母是b
,我們就要取出hat_y[2][2]
。
第i
輪所有batch的字母可以用word_label[i]
表示。根據這個信息,我們可以用probs = hat_y[torch.arange(batch), word_label[i]]
神奇地從hat_y
里取出每一個batch里word_label[i]
處的概率。把這個概率乘到output
上就算完成了一輪計算。
有了語言模型函數,我們可以測試一下開始那些單詞的概率。
def sample(model):words =[]for _ in range(20):word = model.sample_word()words.append(word)print(*words)
def test_language_model(model, is_onehot=True):data, max_length = get_dataloader_and_max_langth(19)if is_onehot:test_word = words_to_onehot(test_words, max_length)else:test_word = words_to_label_array(test_words, max_length)probs = model.language_model(test_word)for word, prob in zip(test_words, probs):print(f'{word}: {prob}')
#rnn1 = train_rnn1()
#rnn1 = RNN1()state_dict = torch.load('rnn1.pth')rnn1.load_state_dict(state_dict)rnn1.eval()# Dropout 層被禁用,BatchNorm 使用全局統計量
test_language_model(rnn1)
sample(rnn1)
輸出:?
?
采樣單詞:
語言模型有一個很好玩的應用:我們可以根據語言模型輸出的概率分布,采樣出下一個單詞;輸入這一個單詞,再采樣下一個單詞。這樣一直采樣,直到采樣出空格為止。使用這種采樣算法,我們能夠讓模型自動生成單詞,甚至是英文里不存在,卻看上去很像那么回事的單詞。
我們要為模型編寫一個新的方法sample_word
,采樣出一個最大長度為10的單詞。這段代碼的運行邏輯和之前的forward
也很相似。只不過,這一次我們沒有輸入張量,每一輪的x
要靠采樣獲得。np.random.choice(LETTER_LIST, p=np_prob)
可以根據概率分布np_prob
對列表LETTER_LIST
進行采樣。根據每一輪采樣出的單詞letter
,我們重新生成一個x
,給one-hot編碼的對應位置賦值1。
@torch.no_grad()def sample_word(self):batch = 1output = ''a = torch.zeros(batch, self.hidden_units)x = torch.zeros(batch, EMBEDDING_LENGTH)for i in range(10):next_a = self.tanh(self.linear_a(torch.cat((a,x),1)))tmp = self.linear_y(next_a)hat_y = F.softmax(tmp, 1)np_prob = hat_y[0].detach().cpu().numpy()letter = np.random.choice(LETTER_LIST, p=np_prob)output += letterif letter == ' ':breakx = torch.zeros(batch, EMBEDDING_LENGTH)x[0][LETTER_MAP[letter]] = 1a = next_areturn output
使用這個方法,我們可以寫一個采樣20次的腳本:
def sample(model):words = []for _ in range(20):word = model.sample_word()words.append(word)print(*words)
輸出:
采樣出來的單詞幾乎不會是英文里的正確單詞。不過,這些單詞的詞綴很符合英文的造詞規則,非常好玩。如果為采樣函數加一些限制,比如只考慮概率前3的字母,那么算法應該能夠采樣出更正確的單詞。?
三.PyTorch里的RNN函數?
剛剛我們手動編寫了RNN的實現細節。實際上,PyTorch提供了更高級的函數,我們能夠更加輕松地實現RNN。其他部分的代碼邏輯都不怎么要改,這里只展示一下要改動的關鍵部分。
新的模型的主要函數如下:
class RNN2(torch.nn.Module):def __init__(self, hidden_units=64, embeding_dim=64, dropout_rate=0.2):super().__init__()self.drop = nn.Dropout(dropout_rate)self.encoder = nn.Embedding(EMBEDDING_LENGTH, embeding_dim)self.rnn = nn.GRU(embeding_dim, hidden_units, 1, batch_first=True)self.decoder = torch.nn.Linear(hidden_units, EMBEDDING_LENGTH)self.hidden_units = hidden_unitsself.init_weights()def init_weights(self):initrange = 0.1nn.init.uniform_(self.encoder.weight, -initrange, initrange)nn.init.zeros_(self.decoder.bias)nn.init.uniform_(self.decoder.weight, -initrange, initrange)def forward(self, word: torch.Tensor):# word shape: [batch, max_word_length]batch, Tx = word.shape[0:2]first_letter = word.new_zeros(batch, 1)x = torch.cat((first_letter, word[:, 0:-1]), 1)hidden = torch.zeros(1, batch, self.hidden_units, device=word.device)emb = self.drop(self.encoder(x))output, hidden = self.rnn(emb, hidden)y = self.decoder(output.reshape(batch * Tx, -1))return y.reshape(batch, Tx, -1)
初始化時,我們用nn.Embedding
表示單詞的向量。詞嵌入(Embedding)是《深度學習專項-RNN》第二門課的內容,我會在下一篇筆記里介紹。這里我們把nn.Embedding
看成一種代替one-hot編碼的更高級的向量就行。這些向量和線性層參數W
一樣,是可以被梯度下降優化的。這樣,不僅是RNN可以優化,每一個單詞的表示方法也可以被優化。
注意,使用nn.Embedding
后,輸入的張量不再是one-hot編碼,而是數字標簽。代碼中的其他地方也要跟著修改。
nn.GRU
可以創建GRU。其第一個參數是輸入的維度,第二個參數是隱變量a
的維度,第三個參數是層數,這里我們只構建1層RNN,batch_first
表示輸入張量的格式是[batch, Tx, embedding_length]
還是[Tx, batch, embedding_length]
。
貌似RNN中常用的正則化是靠dropout實現的。我們要提前準備好dropout層。
def __init__(self, hidden_units=64, embeding_dim=64, dropout_rate=0.2):super().__init__()self.drop = nn.Dropout(dropout_rate)self.encoder = nn.Embedding(EMBEDDING_LENGTH, embeding_dim)self.rnn = nn.GRU(embeding_dim, hidden_units, 1, batch_first=True)self.decoder = torch.nn.Linear(hidden_units, EMBEDDING_LENGTH)self.hidden_units = hidden_unitsself.init_weights()
準備好了計算層后,在forward里只要依次調用它們就行了。其底層原理和我們之前手寫的是一樣的。其中,self.rnn(emb, hidden)
這個調用完成了循環遍歷的計算。
由于輸入格式改了,令第一輪輸入為空字符的操作也更繁瑣了一點。我們要先定義一個空字符張量,再把它和輸入的第一至倒數第二個元素拼接起來,作為網絡的真正輸入。
def forward(self, word: torch.Tensor):# word shape: [batch, max_word_length]batch, Tx = word.shape[0:2]first_letter = word.new_zeros(batch, 1)x = torch.cat((first_letter, word[:, 0:-1]), 1)hidden = torch.zeros(1, batch, self.hidden_units, device=word.device)emb = self.drop(self.encoder(x))output, hidden = self.rnn(emb, hidden)y = self.decoder(output.reshape(batch * Tx, -1))return y.reshape(batch, Tx, -1)
PyTorch里的RNN用起來非常靈活。我們不僅能夠給它一個序列,一次輸出序列的所有結果,還可以只輸入一個元素,得到一輪的結果。在采樣單詞時,我們不得不每次輸入一個元素。有關采樣的邏輯如下:
@torch.no_grad()
def sample_word(self, device='cuda:0'):batch = 1output = ''hidden = torch.zeros(1, batch, self.hidden_units, device=device)x = torch.zeros(batch, 1, device=device, dtype=torch.long)for _ in range(10):emb = self.drop(self.encoder(x))rnn_output, hidden = self.rnn(emb, hidden)hat_y = self.decoder(rnn_output)hat_y = F.softmax(hat_y, 2)np_prob = hat_y[0, 0].detach().cpu().numpy()letter = np.random.choice(LETTER_LIST, p=np_prob)output += letterif letter == ' ':breakx = torch.zeros(batch, 1, device=device, dtype=torch.long)x[0] = LETTER_MAP[letter]return output
以上就是PyTorch高級RNN組件的使用方法。在使用PyTorch的RNN時,主要的改變就是輸入從one-hot向量變成了標簽,數據預處理會更加方便一些。另外,PyTorch的RNN會自動完成循環,可以給它輸入任意長度的序列。