在之前的教程里,我們使用一種叫卷積神經(jīng)網(wǎng)絡(luò)(CNN)的深度學(xué)習(xí)技術(shù)來對(duì)文本和圖片進(jìn)行分類。盡管CNN是一種強(qiáng)大的技術(shù),但它卻不能從序列型輸入(如語音和文字)中學(xué)習(xí)到時(shí)間性的特征。另外,CNN使用一個(gè)固定長(zhǎng)度的卷積核來學(xué)習(xí)空間的特征。這種類型的神經(jīng)網(wǎng)絡(luò)被叫做前饋神經(jīng)網(wǎng)絡(luò)。而循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)則可以學(xué)習(xí)到時(shí)間特征,而且比前饋神經(jīng)網(wǎng)絡(luò)有更廣泛的應(yīng)用。
在本教程里,我們將會(huì)開發(fā)一個(gè)循環(huán)神經(jīng)網(wǎng)絡(luò),用它來在給定一個(gè)前置詞或字符的情況下預(yù)測(cè)下一個(gè)詞或字符是什么的概率。幾乎我們所有人的智能手機(jī)上都有一個(gè)預(yù)測(cè)鍵盤,它能在我們快速輸入的時(shí)候建議下一個(gè)詞。循環(huán)神經(jīng)網(wǎng)絡(luò)就能讓我們構(gòu)建一個(gè)像SwiftKey這樣的非常先進(jìn)的預(yù)測(cè)系統(tǒng)。
我們會(huì)先講解一下前饋神經(jīng)網(wǎng)絡(luò)的一些局限。接著,我們會(huì)用前饋神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)一個(gè)基本的RNN模型。這個(gè)模型可以提供RNN工作機(jī)理的一個(gè)很好的展示。在這之后,我們會(huì)用MXNet的Gluon API提供的LSTM和GRU層來構(gòu)建一個(gè)非常強(qiáng)大的RNN模型。我們會(huì)用這個(gè)模型來生成文字。
我們將介紹下面這些內(nèi)容:
1. 前饋神經(jīng)網(wǎng)絡(luò)的局限;
2.RNN和LSTM背后的原理;
3.安裝帶有Gluon API的MXNet;
4.準(zhǔn)備用于訓(xùn)練的數(shù)據(jù)集;
5.使用前饋神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)一個(gè)基本RNN;
6.使用Gluon API實(shí)現(xiàn)一個(gè)能自動(dòng)生成文本的RNN。
為了能更好地理解本教程,你需要對(duì)循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)、激活函數(shù)、梯度下降和反向傳播有基本的了解。你還應(yīng)該了解Python以及NumPy庫(kù)。
前饋神經(jīng)網(wǎng)絡(luò)和循環(huán)神經(jīng)網(wǎng)絡(luò)的比較
雖然諸如卷積神經(jīng)網(wǎng)絡(luò)這樣的前饋神經(jīng)網(wǎng)絡(luò)在語句和文本分類上有很好的準(zhǔn)確度,但它還是沒法記憶長(zhǎng)間距依賴(即隱藏層狀態(tài))。記憶可以被看成是隨著時(shí)間更新的時(shí)間性狀態(tài)。前饋神經(jīng)網(wǎng)絡(luò)沒法解釋上下文,因?yàn)樗鼪]有存儲(chǔ)時(shí)間性狀態(tài),也就是它沒有記憶。CNN只能從它卷積核范圍附近的鄰居(圖片或文字)那里學(xué)習(xí)到空間特征。圖1顯示了在相同的樣本數(shù)據(jù)里,卷積神經(jīng)網(wǎng)絡(luò)提取的空間特征和RNN提取的時(shí)間上下文。在CNN里,字符O和V的聯(lián)系被丟掉了。因?yàn)樗麄冊(cè)诓煌木矸e空間上下文里。而在RNN里,字符L、O、V、E之間的聯(lián)系被捕捉到了。

圖1 CNN的空間上下文和RNN的時(shí)間上下文。圖片由Manu Jeevan提供
前饋網(wǎng)絡(luò)無法理解上下文,因?yàn)樗鼪]有“記憶”狀態(tài)。所以它無法對(duì)于序列性或時(shí)間性的數(shù)據(jù)(像語言這種有明確順序的數(shù)據(jù))進(jìn)行建模。對(duì)前饋神經(jīng)網(wǎng)絡(luò)的一個(gè)抽象表示如圖2所示。

圖2 前饋神經(jīng)網(wǎng)絡(luò)。圖片由Manu Jeevan提供
RNN則更靈活。它的神經(jīng)元接受加權(quán)過的輸入,同時(shí)生成加權(quán)后的輸出(WO)和加權(quán)后的隱藏層狀態(tài)(WH)。這個(gè)隱藏層狀態(tài)的作用就相當(dāng)于存儲(chǔ)上下文的記憶。如果RNN是表征一個(gè)人的電話通話,加權(quán)的輸出就是說出的那些詞,而加權(quán)的隱藏層狀態(tài)就是說話的上下文。
RNN背后的原理
在這一節(jié),我們將會(huì)通過用前饋神經(jīng)網(wǎng)絡(luò)構(gòu)建一個(gè)展開版的基本RNN,來解釋一下前饋神經(jīng)網(wǎng)絡(luò)和RNN的相似性。這個(gè)基本RNN只有一個(gè)簡(jiǎn)單的隱藏狀態(tài)矩陣(記憶),很容易理解和實(shí)現(xiàn)。
假定我們必須根據(jù)前3個(gè)字符去預(yù)測(cè)一串字符的第4個(gè)字符。為了這一目的,我們?cè)O(shè)計(jì)一個(gè)如圖3所示的簡(jiǎn)單的前饋神經(jīng)網(wǎng)絡(luò)。

圖3 展開的RNN。圖片由Manu Jeevan提供
這是一個(gè)簡(jiǎn)單的前饋神經(jīng)網(wǎng)絡(luò)。其中,權(quán)重WI(綠色箭頭)和WH(紅色箭頭)在一些層間共享同一組值。這是一個(gè)展開版的基本RNN,通常針對(duì)多對(duì)一的RNN應(yīng)用場(chǎng)景,因?yàn)樗怯枚鄺l輸入(本例中,前3個(gè)字符)來預(yù)測(cè)1個(gè)字符。這個(gè)RNN可以用下面的MXNet代碼來設(shè)計(jì)實(shí)現(xiàn)。
class UnRolledRNN_Model(Block):
? ? def __init__(self, vocab_size, num_embed, num_hidden, **kwargs):
? ? ? ? super(UnRolledRNN_Model, self).__init__(**kwargs)
? ? ? ? self.num_embed = num_embed
? ? ? ? self.vocab_size = vocab_size
? ? ? ? # use name_scope to give child Blocks appropriate names.
? ? ? ? # It also allows sharing Parameters between Blocks recursively.
? ? ? ? with self.name_scope():
? ? ? ? ? ? self.encoder = nn.Embedding(self.vocab_size, self.num_embed)
? ? ? ? ? ? self.dense1 = nn.Dense(num_hidden, activation=’relu’, flatten=True)
? ? ? ? ? ? self.dense2 = nn.Dense(num_hidden, activation=’relu’, flatten=True)
? ? ? ? ? ? self.dense3 = nn.Dense(vocab_size, flatten=True)
? ? def forward(self, inputs):
? ? ? ? emd = self.encoder(inputs)
? ? ? ? # print( emd.shape )
? ? ? ? # since the input is shape (batch_size, input(3 characters) )
? ? ? ? # we need to extract 0th, 1st, 2nd character from each batch
? ? ? ? character1 = emd[:, 0, :]
? ? ? ? character2 = emd[:, 1, :]
? ? ? ? character3 = emd[:, 2, :]
? ? ? ? # green arrow in diagram for character 1
? ? ? ? c1_hidden = self.dense1(character1)
? ? ? ? # green arrow in diagram for character 2
? ? ? ? c2_hidden = self.dense1(character2)
? ? ? ? # green arrow in diagram for character 3
? ? ? ? c3_hidden = self.dense1(character3)
? ? ? ? # yellow arrow in diagram
? ? ? ? c1_hidden_2 = self.dense2(c1_hidden)
? ? ? ? addition_result = F.add(c2_hidden, c1_hidden_2)? # Total c1 + c2
? ? ? ? addition_hidden = self.dense2(addition_result)? # the yellow arrow
? ? ? ? addition_result_2 = F.add(addition_hidden, c3_hidden)? # Total c1 + c2
? ? ? ? final_output = self.dense3(addition_result_2)
? ? ? ? return final_output
vocab_size = len(chars) + 1? # the vocabsize
num_embed = 30
num_hidden = 256
# model creation
simple_model = UnRolledRNN_Model(vocab_size, num_embed, num_hidden)
# model initilisation
simple_model.collect_params().initialize(mx.init.Xavier(), ctx=context)
trainer = gluon.Trainer(simple_model.collect_params(), ‘adam’)
loss = gluon.loss.SoftmaxCrossEntropyLoss()
基本上,這個(gè)神經(jīng)網(wǎng)絡(luò)是一個(gè)詞向量層(emb),后面接著3個(gè)全連接層:
1.全連接層1(帶有權(quán)重WI),它接收輸入;
2.全連接層2(帶有權(quán)重WH),中間層;
3.全連接層3(帶有權(quán)重WO),這一層產(chǎn)生輸出。其中也用了MXNet的數(shù)組加法來把產(chǎn)生的輸出與輸入進(jìn)行組合。
你可以查看這個(gè)博客來了解詞向量層以及它的功能。全連接層1、2和3學(xué)到了一套權(quán)重,并用它來根據(jù)給出的前3個(gè)字符預(yù)測(cè)第4個(gè)字符。
我們?cè)谶@個(gè)模型里使用二元交叉熵?fù)p失函數(shù)。這個(gè)模型可以被折疊回來,用圖4所示的方式來簡(jiǎn)潔地表示。

圖4 RNN的簡(jiǎn)潔版表示。圖片由Manu Jeevan提供
圖4可以幫助解釋模型背后的數(shù)學(xué)部分,如下所示:
hidden_state_at_t = tanh(WI x input + WH x previous_hidden_state + bias)
基本RNN有一些缺陷。例如,我們有下面一個(gè)很長(zhǎng)的文檔,其中包含下面這些句子:“我出生在法國(guó),在世界大戰(zhàn)期間”,“所以我會(huì)說法語”?;綬NN就無法理解“出生在法國(guó)”是“我會(huì)說法語”的上下文,因?yàn)檫@兩個(gè)部分在這個(gè)句子里相隔很遠(yuǎn)。
RNN也沒有能力(至少實(shí)際是如此)忘記語句之間的不相關(guān)的上下文?;綬NN對(duì)越靠近的隱藏狀態(tài)給予更多的重要性,因?yàn)樗鼰o法給指定的(t-k)步隱藏狀態(tài)更多的偏好。其中,t是當(dāng)前的時(shí)間步,而k是一個(gè)大于0的數(shù)。這是因?yàn)橛靡粋€(gè)很長(zhǎng)的句子來訓(xùn)練基本RNN可能會(huì)在反向傳播時(shí)導(dǎo)致梯度消失(即梯度很小)或梯度爆炸(即梯度非常大)。
基本上,反向傳播算法會(huì)沿著神經(jīng)網(wǎng)絡(luò)計(jì)算圖的相反方向上對(duì)梯度進(jìn)行連乘。因此當(dāng)隱藏層矩陣的特征值非常小或非常大的時(shí)候,梯度就會(huì)變得非常不穩(wěn)定??梢栽?a >這里找到對(duì)RNN里這個(gè)問題的更詳細(xì)的解釋。
除了多對(duì)一的RNN之外,還有其他類型的RNN可以實(shí)現(xiàn)基于記憶的應(yīng)用,包括非常流行的序列到序列的RNN(見圖5)。在這個(gè)序列到序列的RNN模型里,序列的長(zhǎng)度是3,每個(gè)輸入都對(duì)應(yīng)到一個(gè)單獨(dú)的輸出。這可以幫助讓模型的訓(xùn)練更快,因?yàn)槲覀冊(cè)诿總€(gè)時(shí)間步都計(jì)算損失值(預(yù)測(cè)值和真實(shí)值之間的差距)。和上文只在最終計(jì)算一個(gè)損失不同,我們可以看到損失1、2等。因此訓(xùn)練模型時(shí)就能得到更好的反饋(反向傳播)。

圖5 序列到序列RNN。圖片由Manu Jeevan提供
長(zhǎng)短期記憶模型(LSTM)
為了解決基本RNN的這些問題,兩位德國(guó)學(xué)者(Sepp Hochreiter和Juergen Schmidhuber)提出了長(zhǎng)短期記憶模型(LSTM, Long Short-Term Memory)。這是一個(gè)更復(fù)雜的模型,可以解決基本RNN的梯度消失或爆炸的問題??梢栽?a href="index.html">這里和這里看到對(duì)于基本版LSTM的非常漂亮的演示圖。從更高層面看,我們可以把一個(gè)LSTM單元看成一個(gè)小型的神經(jīng)網(wǎng)絡(luò)。它可以決定需要保留(記憶)前面哪些時(shí)間步里的信息量。
實(shí)現(xiàn)一個(gè)LSTM
現(xiàn)在讓我們?cè)囍鴺?gòu)建一個(gè)簡(jiǎn)單的字符預(yù)測(cè)器。
準(zhǔn)備環(huán)境
如果你能使用AWS云,那么可以省掉很多安裝的工作。只要使用Amazon SageMaker的預(yù)配置的深度學(xué)習(xí)環(huán)境即可。如果能這樣做,你可以直接跳過下面的步驟1到5。
如果你使用一個(gè)Conda的環(huán)境,記得要先在conda環(huán)境里安裝pip工具。在激活你的conda環(huán)境后輸入:conda install pip。這會(huì)幫你在后面的步驟里避免很多坑。
下面是安裝和設(shè)置的方法:
- 安裝Anaconda這個(gè)Python庫(kù)管理器。用Anaconda來安裝Python的各種庫(kù)會(huì)非常容易。你可以用這個(gè)curl命令來下載Anaconda并安裝:curl -O?https://repo.continuum.io/archive/Anaconda3-4.2.0-Linux-x86_64.sh chmod 777 Anaconda3-4.2.0-Linux-x86_64.sh ./Anaconda3-4.2.0-Linux-x86_64.sh
- 安裝scikit-learn。這是一個(gè)通用的科學(xué)計(jì)算庫(kù)。我們會(huì)用它來對(duì)我們的數(shù)據(jù)進(jìn)行預(yù)處理??梢杂眠@個(gè)命令來安裝:conda install scikit-learn
- 安裝Jupyter Notebook,命令是:conda install jupyter
- 獲取MXNet,這是一個(gè)開源的深度學(xué)習(xí)庫(kù)。這里用的Python notebook在MXNet的0.12.0版本上通過了測(cè)試??梢杂孟旅娴拿畎惭bMXNet:pip install mxnet-cu90(如果你有GPU)。沒有GPU的話,就用pip install mxnet –pre來安裝??梢栽?a >這里看到MXNet的文檔。
在激活anaconda的環(huán)境后,在里面運(yùn)行下面的命令:source activate mxnet。
上面的命令匯總?cè)缦拢?/p>
curl -O https://repo.continuum.io/archive/Anaconda3-4.2.0-Linux-x86_64.sh
chmod 777 Anaconda3-4.2.0-Linux-x86_64.sh?
./Anaconda3-4.2.0-Linux-x86_64.sh?
conda install pip
pip install opencv-python
conda install scikit-learn
conda install jupyter
pip install mxnet-cu90
- 你可以在這里下載本文教程的MXNet notebook。里面有我們寫好并測(cè)試通過的代碼。你可以調(diào)整超參數(shù)并嘗試不同的神經(jīng)網(wǎng)絡(luò)架構(gòu),好好玩一下吧!
準(zhǔn)備數(shù)據(jù)集
我們會(huì)用Friedrich Nietzsche的工作成果作為我們的數(shù)據(jù)集。
你可以在這里下載此數(shù)據(jù)集。當(dāng)然你也可以用其他的數(shù)據(jù)集,比如你自己的聊天記錄,或是你從這個(gè)地方下載的數(shù)據(jù)集。如果是用你自己的聊天記錄,你就可以為你自己的風(fēng)格寫一個(gè)定制化的可預(yù)測(cè)的文本編輯器了。在這里,我們還是用Nietzche的數(shù)據(jù)集。
數(shù)據(jù)集文件 (nietzsche.txt)包括600901個(gè)字符,來自86個(gè)唯一的字符。我們需要把這個(gè)文本轉(zhuǎn)換成一系列的數(shù)字串。
# total of characters in dataset
chars = sorted(list(set(text)))
vocab_size = len(chars)+1
print(‘total chars:’, vocab_size)
# maps character to unique index e.g. {a:1,b:2….}
char_indices = dict((c, i) for i, c in enumerate(chars))
# maps indices to character (1:a,2:b ….)
indices_char = dict((i, c) for i, c in enumerate(chars))
# mapping the dataset into index
idx = [char_indices for c in text]
針對(duì)展開版的RNN準(zhǔn)備數(shù)據(jù)
轉(zhuǎn)換的目的是把數(shù)據(jù)轉(zhuǎn)換成一系列的輸入和輸出組。來自輸入串的每3個(gè)字符串都會(huì)被存下來作為模型的3字符輸入,而第4個(gè)字符作為輸出,從而能用來訓(xùn)練我們的預(yù)測(cè)模型。例如,我們把字符串I_love_mxnet轉(zhuǎn)換成如圖6所示的輸入和輸出組。

圖6 “I_love_mxnet”的輸入和輸出組。圖片由Manu Jeevan提供
做這個(gè)轉(zhuǎn)換的代碼如下:
# input for neural network(our basic rnn has 3 inputs, n samples)
cs = 3
c1_dat = [idx[i] for i in range(0, len(idx)-1-cs, cs)]
c2_dat = [idx[i+1] for i in range(0, len(idx)-1-cs, cs)]
c3_dat = [idx[i+2] for i in range(0, len(idx)-1-cs, cs)]
# the output of rnn network (single vector)
c4_dat = [idx[i+3] for i in range(0, len(idx)-1-cs, cs)]
# stacking the inputs to form (3 input features )
x1 = np.stack(c1_dat[:-2])
x2 = np.stack(c2_dat[:-2])
x3 = np.stack(c3_dat[:-2])
# the output (1 X N data points)
y = np.stack(c4_dat[:-2])
col_concat = np.array([x1, x2, x3])
t_col_concat = col_concat.T
print(t_col_concat.shape)
我們還要把訓(xùn)練數(shù)據(jù)分成32組一批的批次。這樣每個(gè)訓(xùn)練輸入實(shí)例的尺寸就是32 X 3。批次化可以幫助加速模型的訓(xùn)練。
# Set the batchsize as 32, so input is of form 32 X 3
# output is 32 X 1
batch_size = 32
def get_batch(source, label_data, i, batch_size=32):
? ? bb_size = min(batch_size, source.shape[0] – 1 – i)
? ? data = source[i: i + bb_size]
? ? target = label_data[i: i + bb_size]
? ? # print(target.shape)
? ? return data, target.reshape((-1, ))
為Gluon RNN準(zhǔn)備數(shù)據(jù)
上一節(jié)我們?yōu)榻o定前3個(gè)字符來預(yù)測(cè)第4個(gè)字符做的模型準(zhǔn)備了數(shù)據(jù)。這一節(jié)里,我們會(huì)擴(kuò)展這個(gè)算法,為給定的任意前n-1個(gè)字符,預(yù)測(cè)第n個(gè)字符。要做的和為展開版的RNN準(zhǔn)備數(shù)據(jù)差不多,除了要改變輸入的大小。此數(shù)據(jù)集也應(yīng)該被轉(zhuǎn)化成這個(gè)形狀(輸入X的長(zhǎng)度,批次大?。,F(xiàn)在讓我們把樣本數(shù)據(jù)分成如圖7所示的多個(gè)批次。

圖7 批次化輸入。圖片由Manu Jeevan提供
我們把輸入的串轉(zhuǎn)化成了每批次3個(gè)輸入,每個(gè)輸入長(zhǎng)度為4。經(jīng)過這個(gè)轉(zhuǎn)換后,我們丟失了一些相鄰字符間的時(shí)間關(guān)系。比如,字符O和V、M和X。其中字符V是在O后面的,但是它們被分到了不同的批次里面。我們對(duì)數(shù)據(jù)進(jìn)行批次化的唯一目的就是加快訓(xùn)練的過程。下面是實(shí)現(xiàn)批次化的代碼:
# prepares rnn batches
# The batch will be of shape is (num_example * batch_size) because of RNN uses sequences of input ? ? x
# for example if we use (a1,a2,a3) as one input sequence , (b1,b2,b3) as another input sequence and (c1,c2,c3)
# if we have batch of 3, then at timestep ‘1’? we only have (a1,b1.c1) as input, at timestep ‘2’ we have (a2,b2,c2) as input…
# hence the batchsize is of order?
# In feedforward we use (batch_size, num_example)
def rnn_batch(data, batch_size):
? ? “””Reshape data into (num_example, batch_size)”””
? ? nbatch = data.shape[0] // batch_size
? ? data = data[:nbatch * batch_size]
? ? data = data.reshape((batch_size, nbatch)).T
? ? return data
圖8顯示了另外一個(gè)批次的例子,其中批次里的樣本數(shù)為2個(gè),每個(gè)長(zhǎng)度是6。

圖8 批次的形狀轉(zhuǎn)換。圖片由Manu Jeevan提供
在給定批次里改變輸入的長(zhǎng)度是非常簡(jiǎn)單的。例如,如果想從一個(gè)批次大小為2的批次里生成一個(gè)長(zhǎng)度為3的輸入,用下面的代碼就能實(shí)現(xiàn)。
#get the batch
def get_batch(source, i, seq):
? ? seq_len = min(seq, source.shape[0] – 1 – i)
? ? data = source[i: i + seq_len]
? ? target = source[i + 1: i + 1 + seq_len]
? ? return data, target.reshape((-1,))
在這里的訓(xùn)練里,我們會(huì)使用輸入長(zhǎng)度為100,并把它作為超參數(shù)??梢愿M(jìn)一步地調(diào)優(yōu)這個(gè)超參數(shù)來得到更好的結(jié)果。
用Gluon來定義RNN模型
接下來,我們定義一個(gè)類,其中可以構(gòu)造兩種RNN模型,用于我們的樣本。一種是GRU(門控循環(huán)單元模型,Gated Recurrent Unit)和LSTM(長(zhǎng)短期記憶單元模型)。GRU是一個(gè)簡(jiǎn)化版的LSTM,且性能差不多。你能在這里看到一個(gè)對(duì)它們的比較研究。下面的代碼片就是這些模型的定義。
class GluonRNNModel(gluon.Block):
? ? “””A model with an encoder, recurrent layer, and a decoder.”””
? ? def __init__(self, mode, vocab_size, num_embed, num_hidden,
?? ? ? ? ? ? ? ? num_layers, dropout=0.5, **kwargs):
? ? ? ? super(GluonRNNModel, self).__init__(**kwargs)
? ? ? ? with self.name_scope():
? ? ? ? ? ? self.drop = nn.Dropout(dropout)
? ? ? ? ? ? self.encoder = nn.Embedding(vocab_size, num_embed,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? weight_initializer=mx.init.Uniform(0.1))
? ? ? ? ? ? if mode == ‘lstm’:
? ? ? ? ? ? ? ? #? we create a LSTM layer with certain number of hidden LSTM cell and layers
? ? ? ? ? ? ? ? #? in our example num_hidden is 1000 and num of layers is 2
? ? ? ? ? ? ? ? #? The input to the LSTM will only be passed during the forward pass (see forward function below)
? ? ? ? ? ? ? ? self.rnn = rnn.LSTM(num_hidden, num_layers, dropout=dropout,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? input_size=num_embed)
? ? ? ? ? ? elif mode == ‘gru’:
? ? ? ? ? ? ? ? #? we create a GRU layer with certain number of hidden GRU cell and layers
? ? ? ? ? ? ? ? #? in our example num_hidden is 1000 and num of layers is 2
? ? ? ? ? ? ? ? #? The input to the GRU will only be passed during the forward pass (see forward function below)
? ? ? ? ? ? ? ? self.rnn = rnn.GRU(num_hidden, num_layers, dropout=dropout,
?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? input_size=num_embed)
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? #? we create a vanilla RNN layer with certain number of hidden vanilla RNN cell and layers
? ? ? ? ? ? ? ? #? in our example num_hidden is 1000 and num of layers is 2
? ? ? ? ? ? ? ? #? The input to the vanilla will only be passed during the forward pass (see forward function below)
? ? ? ? ? ? ? ? self.rnn = rnn.RNN(num_hidden, num_layers, activation=’relu’, dropout=dropout,
?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? input_size=num_embed)
? ? ? ? ? ? self.decoder = nn.Dense(vocab_size, in_units=num_hidden)
? ? ? ? ? ? self.num_hidden = num_hidden
? ? #? define the forward pass of the neural network
? ? def forward(self, inputs, hidden):
? ? ? ? emb = self.encoder(inputs)
? ? ? ? #? emb, hidden are the inputs to the hidden?
? ? ? ? output, hidden = self.rnn(emb, hidden)
? ? ? ? #? the ouput from the hidden layer to passed to drop out layer
? ? ? ? output = self.drop(output)
? ? ? ? #? print(‘output forward’,output.shape)
? ? ? ? #? Then the output is flattened to a shape for the dense layer ?
? ? ? ? decoded = self.decoder(output.reshape((-1, self.num_hidden)))
? ? ? ? return decoded, hidden
? ? # Initial state of RNN layer
? ? def begin_state(self, *args, **kwargs):
? ? ? ? return self.rnn.begin_state(*args, **kwargs)
這個(gè)類的構(gòu)造函數(shù)里面創(chuàng)建了用于前向傳播的神經(jīng)元們。這個(gè)構(gòu)造函數(shù)接收三種類型RNN層的參數(shù):LSTM、GRU和基本RNN。前向傳播方法會(huì)在訓(xùn)練時(shí)被使用,生成訓(xùn)練數(shù)據(jù)的損失值。
前向傳播函數(shù)開始時(shí)會(huì)為輸入的字符創(chuàng)建一個(gè)詞向量層。 你可以看一下我們之前的這篇博客來更多的了解詞向量的知識(shí)。詞向量層的輸出則作為RNN的輸入。RNN層會(huì)返回一個(gè)輸出層以及隱藏層的狀態(tài)。中間的dropout層則會(huì)防止模型完全記住輸入和輸出的映射關(guān)系,從而避免過擬合。RNN的輸出再經(jīng)過一個(gè)解碼器(全連接層)完成下一個(gè)字符的預(yù)測(cè),并在訓(xùn)練階段計(jì)算損失值。
我們定義了一個(gè)“開始狀態(tài)”函數(shù),它可以為模型初始化隱藏層的狀態(tài)。
訓(xùn)練這個(gè)網(wǎng)絡(luò)
在定義完網(wǎng)絡(luò)后,我們就開始訓(xùn)練這個(gè)網(wǎng)絡(luò),讓它進(jìn)行學(xué)習(xí)。
def trainGluonRNN(epochs, train_data, seq=seq_length):
? ? for epoch in range(epochs):
? ? ? ? total_L = 0.0
? ? ? ? hidden = model.begin_state(func=mx.nd.zeros, batch_size=batch_size, ctx=context)
? ? ? ? for ibatch, i in enumerate(range(0, train_data.shape[0] – 1, seq_length)):
? ? ? ? ? ? data, target = get_batch(train_data, i, seq)
? ? ? ? ? ? hidden = detach(hidden)
? ? ? ? ? ? with autograd.record():
? ? ? ? ? ? ? ? output, hidden = model(data, hidden)
? ? ? ? ? ? ? ? L = loss(output, target) # this is total loss associated with seq_length
? ? ? ? ? ? ? ? L.backward()
? ? ? ? ? ? grads = [i.grad(context) for i in model.collect_params().values()]
? ? ? ? ? ? # Here gradient is for the whole batch.
? ? ? ? ? ? # So we multiply max_norm by batch_size and seq_length to balance it.
? ? ? ? ? ? gluon.utils.clip_global_norm(grads, clip * seq_length * batch_size)
? ? ? ? ? ? trainer.step(batch_size)
? ? ? ? ? ? total_L += mx.nd.sum(L).asscalar()
? ? ? ? ? ? if ibatch % log_interval == 0 and ibatch > 0:
? ? ? ? ? ? ? ? cur_L = total_L / seq_length / batch_size / log_interval
? ? ? ? ? ? ? ? print(‘[Epoch %d Batch %d] loss %.2f’, epoch + 1, ibatch, cur_L)
? ? ? ? ? ? ? ? total_L = 0.0
? ? ? ? model.save_params(rnn_save)
每個(gè)周期的開始都是對(duì)隱藏單元初始化成零。在對(duì)批次訓(xùn)練時(shí),我們會(huì)把這個(gè)隱藏單元從計(jì)算圖里分割出來,這樣在反向傳播進(jìn)行梯度計(jì)算時(shí),就不會(huì)在超過序列長(zhǎng)度時(shí)(我們這里是100)還進(jìn)行梯度計(jì)算。如果我們不進(jìn)行分割,梯度就會(huì)被傳送到這個(gè)開始的隱藏層狀態(tài)(t=0)。在分割后,我們計(jì)算了損失值,并用反向傳播函數(shù)來進(jìn)行損失值的反向傳播,從而進(jìn)行權(quán)重的調(diào)優(yōu)。我們還通過把梯度和輸入長(zhǎng)度和批次大小相乘來對(duì)梯度進(jìn)行正規(guī)化。
文本生成
在模型用數(shù)據(jù)進(jìn)行訓(xùn)練后,我們就能生成與訓(xùn)練數(shù)據(jù)類似的隨機(jī)文本。針對(duì)輸入長(zhǎng)度100進(jìn)行200個(gè)周期訓(xùn)練后的模型的權(quán)重文件可以在這里下載。下載后,你可以用model.load_params函數(shù)導(dǎo)入這些模型權(quán)重。
下面,我們使用這個(gè)模型來生成類似于nietzsche的文字。為了生成任意長(zhǎng)度的文本,我們使用模型的輸出作為下一次生成的輸入。
為了生成文本,我們先初始化隱藏層的狀態(tài)。
?hidden = model.begin_state(func=mx.nd.zeros, batch_size=batch_size, ctx=context)
這里我們沒必要在分割這個(gè)初始隱藏層狀態(tài)了,因?yàn)槲覀儾粫?huì)再通過反向傳播來調(diào)整權(quán)重。
接著我們把輸入的向量改變一下尺寸再傳給這個(gè)RNN模型。
?sample_input = mx.nd.array(np.array([idx[0:seq_length]]).T, ctx=context)
隨后使用argmax來計(jì)算網(wǎng)絡(luò)的輸出,生成輸出字符“c”。
output, hidden = model(sample_input, hidden)
? ? ? ? index = mx.nd.argmax(output, axis=1)
? ? ? ? index = index.asnumpy()
? ? ? ? count = count + 1
再把生成的字符“c”添加到輸入串的最后,并把輸入串的第一個(gè)字符刪掉。
new_string = new_string + indices_char[index[-1]]
input_string = input_string[1:] + indices_char[index[-1]]
下面是生成任意長(zhǎng)度的類似nietzsche的文本的全部代碼。
import sys
# a nietzsche like text generator
def generate_random_text(model, input_string, seq_length, batch_size, sentence_length):
? ? count = 0
? ? new_string = ”
? ? cp_input_string = input_string
? ? hidden = model.begin_state(func=mx.nd.zeros, batch_size=batch_size, ctx=context)
? ? while count < sentence_length:
? ? ? ? idx = [char_indices for c in input_string]
? ? ? ? if(len(input_string) != seq_length):
? ? ? ? ? ? print(len(input_string))
? ? ? ? ? ? raise ValueError(‘there was a error in the input ‘)
? ? ? ? sample_input = mx.nd.array(np.array([idx[0:seq_length]]).T, ctx=context)
? ? ? ? output, hidden = model(sample_input, hidden)
? ? ? ? index = mx.nd.argmax(output, axis=1)
? ? ? ? index = index.asnumpy()
? ? ? ? count = count + 1
? ? ? ? new_string = new_string + indices_char[index[-1]]
? ? ? ? input_string = input_string[1:] + indices_char[index[-1]]
? ? print(cp_input_string + new_string)
如果你查看生成的文本,你就會(huì)發(fā)現(xiàn)模型已經(jīng)學(xué)到使用開閉引號(hào)(“”)。模型學(xué)到了有限的文本結(jié)構(gòu),看起來比較像nietzsche了。
你還可以用你自己的聊天記錄來訓(xùn)練模型,然后預(yù)測(cè)你會(huì)敲出的下一個(gè)字符。
在下一篇文章里,我們會(huì)學(xué)習(xí)生成模型,特別是生成對(duì)抗網(wǎng)絡(luò)。這是一個(gè)從給定的數(shù)據(jù)集學(xué)習(xí),并能生成新數(shù)據(jù)的強(qiáng)大的模型。
注意:雖然RNN模型可以被用來生成文本,但嚴(yán)格意義上它并不是一個(gè)好的生成模型。這個(gè)PDF文檔清晰地展示了用于文本分類的生成模型和判別模型的區(qū)別。
這篇博文是O’Reilly和Amazon的合作產(chǎn)物。請(qǐng)閱讀我們的編輯獨(dú)立聲明。



