丁香五月天婷婷久久婷婷色综合91|国产传媒自偷自拍|久久影院亚洲精品|国产欧美VA天堂国产美女自慰视屏|免费黄色av网站|婷婷丁香五月激情四射|日韩AV一区二区中文字幕在线观看|亚洲欧美日本性爱|日日噜噜噜夜夜噜噜噜|中文Av日韩一区二区

您正在使用IE低版瀏覽器,為了您的雷峰網(wǎng)賬號安全和更好的產(chǎn)品體驗,強烈建議使用更快更安全的瀏覽器
此為臨時鏈接,僅用于文章預(yù)覽,將在時失效
人工智能 正文
發(fā)私信給宗仁
發(fā)送

0

簡單易懂------使用PyTorch實現(xiàn)Chatbot

本文作者: 宗仁 2019-03-06 16:55
導(dǎo)語:本系列文章通過通俗易懂的方式介紹強化學(xué)習(xí)的基本概念,雖然語言通俗,但是內(nèi)容依舊非常嚴(yán)謹(jǐn)性。

本系列文章通過通俗易懂的方式介紹強化學(xué)習(xí)的基本概念,雖然語言通俗,但是內(nèi)容依舊非常嚴(yán)謹(jǐn)性。文中用很多的公式,對數(shù)學(xué)公式頭疼的讀者可能會被嚇住,但是如果讀者一步一步follow下來,就會發(fā)現(xiàn)公式的推導(dǎo)非常自然,對于透徹的理解這些基本概念非常有幫助。除了理論之外,文章還會介紹每種算法的實現(xiàn)代碼,深入解答每一行關(guān)鍵代碼。讓讀者不但理解理論和算法,同時還能知道怎么用代碼來實現(xiàn)。通過理論與實際的結(jié)合,更加深入的理解學(xué)過的概念。讀者只需要基本的Python編程知識,文中每一個算法都有對應(yīng)的Jupyter Notebook代碼。(文章來源,李理的Github博客

本教程會介紹使用seq2seq模型實現(xiàn)一個chatbot,訓(xùn)練數(shù)據(jù)來自Cornell電影對話語料庫。對話系統(tǒng)是目前的研究熱點,它在客服、可穿戴設(shè)備和智能家居等場景有廣泛應(yīng)用。

傳統(tǒng)的對話系統(tǒng)要么基于檢索的方法——提前準(zhǔn)備一個問答庫,根據(jù)用戶的輸入尋找類似的問題和答案。這更像一個問答系統(tǒng),它很難進(jìn)行多輪的交互,而且答案是固定不變的。要么基于預(yù)先設(shè)置的對話流程,這主要用于slot-filling(Task-Oriented)的任務(wù),比如查詢機票需要用戶提供日期,達(dá)到城市等信息。這種方法的缺點是比較死板,如果用戶的意圖在設(shè)計的流程之外,那么就無法處理,而且對話的流程也一般比較固定,要支持用戶隨意的話題內(nèi)跳轉(zhuǎn)和話題間切換比較困難。

因此目前學(xué)術(shù)界的研究熱點是根據(jù)大量的對話數(shù)據(jù),自動的End-to-End的使用Seq2Seq模型學(xué)習(xí)對話模型。它的好處是不需要人來設(shè)計這個對話流程,完全是數(shù)據(jù)驅(qū)動的方法。它的缺點是流程不受人(開發(fā)者)控制,在嚴(yán)肅的場景(比如客服)下使用會有比較大的風(fēng)險,而且需要大量的對話數(shù)據(jù),這在很多實際應(yīng)用中是很難得到的。因此目前seq2seq模型的對話系統(tǒng)更多的是用于類似小冰的閑聊機器人上,最近也有不少論文研究把這種方法用于task-oriented的任務(wù),但還不是太成熟,在業(yè)界還很少被使用。

效果

本文使用的Cornell電影對話語料庫就是偏向于閑聊的語料庫。

本教程的主要內(nèi)容參考了PyTorch官方教程。讀者可以從這里獲取完整代碼。 下面是這個教程實現(xiàn)的對話效果示例:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

準(zhǔn)備

首先我們通過下載鏈接下載訓(xùn)練語料庫,這是一個zip文件,把它下載后解壓到項目目錄的子目錄data下。接下來我們導(dǎo)入需要用到的模塊,這主要是PyTorch的模塊:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

加載和預(yù)處理數(shù)據(jù)

接下來我們需要對原始數(shù)據(jù)進(jìn)行變換然后用合適的數(shù)據(jù)結(jié)構(gòu)加載到內(nèi)存里。

Cornell電影對話語料庫是電影人物的對話數(shù)據(jù),它包括:

  • 10,292對電影人物(一部電影有多個人物,他們兩兩之間可能存在對話)的220,579個對話

  • 617部電影的9,035個人物

  • 總共304,713個utterance(utterance是對話中的語音片段,不一定是完整的句子)

這個數(shù)據(jù)集是比較大并且多樣的(diverse),語言形式、時代和情感都有很多樣。這樣的數(shù)據(jù)可以使得我們的chatbot對于不同的輸入更加魯棒(robust)。

首先我們來看一下原始數(shù)據(jù)長什么樣:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

解壓后的目錄有很多文件,我們會用到的文件包括movie_lines.txt。上面的代碼輸出這個文件的前10行,結(jié)果如下:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

注意:上面的move_lines.txt每行都是一個utterance,但是這個文件看不出哪些utterance是組成一段對話的,這需要 movie_conversations.txt 文件:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

每一行用”+++$+++”分割成4列,第一列表示第一個人物的ID,第二列表示第二個人物的ID,第三列表示電影的ID,第四列表示這兩個人物在這部電影中的一段對話,比如第一行的表示人物u0和u2在電影m0中的一段對話包含ID為L194、L195、L196和L197的4個utterance。注意:兩個人物在一部電影中會有多段對話,中間可能穿插其他人之間的對話,而且即使中間沒有其他人說話,這兩個人物對話的內(nèi)容從語義上也可能是屬于不同的對話(話題)。所以我們看到第二行還是u0和u2在電影m0中的對話,它包含L198和L199兩個utterance,L198是緊接著L197之后的,但是它們屬于兩個對話(話題)。

數(shù)據(jù)處理

為了使用方便,我們會把原始數(shù)據(jù)處理成一個新的文件,這個新文件的每一行都是用TAB分割問題(query)和答案(response)對。為了實現(xiàn)這個目的,我們首先定義一些用于parsing原始文件 movie_lines.txt 的輔助函數(shù)。

  • loadLines 把movie_lines.txt 文件切分成 (lineID, characterID, movieID, character, text)

  • loadConversations 把上面的行g(shù)roup成一個個多輪的對話

  • extractSentencePairs 從上面的每個對話中抽取句對

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

接下來我們利用上面的3個函數(shù)對原始數(shù)據(jù)進(jìn)行處理,最終得到formatted_movie_lines.txt。

簡單易懂------使用PyTorch實現(xiàn)Chatbot

上面的代碼會生成一個新的文件formatted_movie_lines.txt,這文件每一行包含一對句對,用tab分割。下面是前十行:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

創(chuàng)建詞典

接下來我們需要構(gòu)建詞典然后把問答句對加載到內(nèi)存里。

我們的輸入是一個句對,每個句子都是詞的序列,但是機器學(xué)習(xí)只能處理數(shù)值,因此我們需要建立詞到數(shù)字ID的映射。

為此,我們會定義一個Voc類,它會保存詞到ID的映射,同時也保存反向的從ID到詞的映射。除此之外,它還記錄每個詞出現(xiàn)的次數(shù),以及總共出現(xiàn)的詞的個數(shù)。這個類提供addWord方法來增加一個詞, addSentence方法來增加句子,也提供方法trim來去除低頻的詞。

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

有了上面的Voc類我們就可以通過問答句對來構(gòu)建詞典了。但是在構(gòu)建之前我們需要進(jìn)行一些預(yù)處理。

首先我們需要使用函數(shù)unicodeToAscii來把unicode字符變成ascii,比如把à變成a。注意,這里的代碼只是用于處理西方文字,如果是中文,這個函數(shù)直接會丟棄掉。接下來把所有字母變成小寫同時丟棄掉字母和常見標(biāo)點(.!?)之外的所有字符。最后為了訓(xùn)練收斂,我們會用函數(shù)filterPairs去掉長度超過MAX_LENGTH的句子(句對)。

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

上面的代碼的輸出為:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

我們可以看到,原來共有221282個句對,經(jīng)過處理后我們只保留了64271個句對。

另外為了收斂更快,我們可以去除掉一些低頻詞。這可以分為兩步:

  • 1) 使用voc.trim函數(shù)去掉頻次低于MIN_COUNT 的詞。

  • 2) 去掉包含低頻詞的句子(只保留這樣的句子——每一個詞都是高頻的,也就是在voc中出現(xiàn)的)

簡單易懂------使用PyTorch實現(xiàn)Chatbot

代碼的輸出為:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

18005個詞之中,頻次大于等于3的只有43%,去掉低頻的57%的詞之后,保留的句子為53165,占比為82%。

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

前面我們構(gòu)建了詞典,并且對訓(xùn)練數(shù)據(jù)進(jìn)行預(yù)處理并且濾掉一些句對,但是模型最終用到的是Tensor。最簡單的辦法是一次處理一個句對,那么上面得到的句對直接就可以使用。但是為了加快訓(xùn)練速度,尤其是重復(fù)利用GPU的并行能力,我們需要一次處理一個batch的數(shù)據(jù)。

對于某些問題,比如圖像來說,輸入可能是固定大小的(或者通過預(yù)處理縮放成固定大?。?,但是對于文本來說,我們很難把一個二十個詞的句子”縮放”成十個詞同時還保持語義不變。但是為了充分利用GPU等計算自由,我們又必須變成固定大小的Tensor,因此我們通常會使用Padding的技巧,把短的句子補充上零使得輸入大小是(batch, max_length),這樣通過一次就能實現(xiàn)一個batch數(shù)據(jù)的forward或者backward計算。當(dāng)然padding的部分的結(jié)果是沒有意義的,比如某個句子實際長度是5,而max_length是10,那么最終forward的輸出應(yīng)該是第5個時刻的輸出,后面5個時刻計算是無用功。方向計算梯度的時候也是類似的,我們需要從第5個時刻開始反向計算梯度。為了提高效率,我們通常把長度接近的訓(xùn)練數(shù)據(jù)放到一個batch里面,這樣無用的計算是最少的。因此我們通常把全部訓(xùn)練數(shù)據(jù)根據(jù)長度劃分成一些組,比如長度小于4的一組,長度4到8的一組,長度8到12的一組,…。然后每次隨機的選擇一個組,再隨機的從一組里選擇batch個數(shù)據(jù)。不過本教程并沒有這么做,而是每次隨機的從所有pair里隨機選擇batch個數(shù)據(jù)。

原始的輸入通常是batch個list,表示batch個句子,因此自然的表示方法為(batch, max_length),這種表示方法第一維是batch,每移動一個下標(biāo)得到的是一個樣本的max_length個詞(包括padding)。因為RNN的依賴關(guān)系,我們在計算t+1時刻必須知道t時刻的結(jié)果,因此我們無法用多個核同時計算一個樣本的forward。但是不同樣本之間是沒有依賴關(guān)系的,因此我們可以在根據(jù)t時刻batch樣本的當(dāng)前狀態(tài)計算batch個樣本的輸出和新狀態(tài),然后再計算t+2時刻,…。為了便于GPU一次取出t時刻的batch個數(shù)據(jù),我們通常把輸入從(batch, max_length)變成(max_length, batch),這樣使得t時刻的batch個數(shù)據(jù)在內(nèi)存(顯存)中是連續(xù)的,從而讀取效率更高。這個過程如下圖所示,原始輸入的大小是(batch=6, max_length=4),轉(zhuǎn)置之后變成(4,6)。這樣某個時刻的6個樣本數(shù)據(jù)在內(nèi)存中是連續(xù)的。

簡單易懂------使用PyTorch實現(xiàn)Chatbot

因此我們會用一些工具函數(shù)來實現(xiàn)上述處理。

inputVar函數(shù)把batch個句子padding后變成一個LongTensor,大小是(max_length, batch),同時會返回一個大小是batch的list lengths,說明每個句子的實際長度,這個參數(shù)后面會傳給PyTorch,從而在forward和backward計算的時候使用實際的長度。

outputVar函數(shù)和inputVar類似,但是它輸出的第二個參數(shù)不是lengths,而是一個大小為(max_length, batch)的mask矩陣(tensor),某位是0表示這個位置是padding,1表示不是padding,這樣做的目的是后面計算方便。當(dāng)然這兩種表示是等價的,只不過lengths表示更加緊湊,但是計算起來不同方便,而mask矩陣和outputVar直接相乘就可以把padding的位置給mask(變成0)掉,這在計算loss時會非常方便。

batch2TrainData 則利用上面的兩個函數(shù)把一個batch的句對處理成合適的輸入和輸出Tensor。

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

示例的輸出為:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

我們可以看到input_variable的每一列表示一個樣本,而每一行表示batch(5)個樣本在這個時刻的值。而lengths表示真實的長度。類似的target_variable也是每一列表示一個樣本,而mask的shape和target_variable一樣,如果某個位置是0,則表示padding。

定義模型

Seq2Seq 模型

我們這個chatbot的核心是一個sequence-to-sequence(seq2seq)模型。 seq2seq模型的輸入是一個變長的序列,而輸出也是一個變長的序列。而且這兩個序列的長度并不相同。一般我們使用RNN來處理變長的序列,Sutskever等人的論文發(fā)現(xiàn)通過使用兩個RNN可以解決這類問題。這類問題的輸入和輸出都是變長的而且長度不一樣,包括問答系統(tǒng)、機器翻譯、自動摘要等等都可以使用seq2seq模型來解決。其中一個RNN叫做Encoder,它把變長的輸入序列編碼成一個固定長度的context向量,我們一般可以認(rèn)為這個向量包含了輸入句子的語義。而第二個RNN叫做Decoder,初始隱狀態(tài)是Encoder的輸出context向量,輸入是(表示句子開始的特殊Token),然后用RNN計算第一個時刻的輸出,接著用第一個時刻的輸出和隱狀態(tài)計算第二個時刻的輸出和新的隱狀態(tài),...,直到某個時刻輸出特殊的(表示句子結(jié)束的特殊Token)或者長度超過一個閾值。Seq2Seq模型如下圖所示。

簡單易懂------使用PyTorch實現(xiàn)Chatbot

Encoder

Encoder是個RNN,它會遍歷輸入的每一個Token(詞),每個時刻的輸入是上一個時刻的隱狀態(tài)和輸入,然后會有一個輸出和新的隱狀態(tài)。這個新的隱狀態(tài)會作為下一個時刻的輸入隱狀態(tài)。每個時刻都有一個輸出,對于seq2seq模型來說,我們通常只保留最后一個時刻的隱狀態(tài),認(rèn)為它編碼了整個句子的語義,但是后面我們會用到Attention機制,它還會用到Encoder每個時刻的輸出。Encoder處理結(jié)束后會把最后一個時刻的隱狀態(tài)作為Decoder的初始隱狀態(tài)。

實際我們通常使用多層的Gated Recurrent Unit(GRU)或者LSTM來作為Encoder,這里使用GRU,讀者可以參考Cho等人2014年的[論文]。

此外我們會使用雙向的RNN,如下圖所示。

簡單易懂------使用PyTorch實現(xiàn)Chatbot

注意在接入RNN之前會有一個embedding層,用來把每一個詞(ID或者one-hot向量)映射成一個連續(xù)的稠密的向量,我們可以認(rèn)為這個向量編碼了一個詞的語義。在我們的模型里,我們把它的大小定義成和RNN的隱狀態(tài)大小一樣(但是并不是一定要一樣)。有了Embedding之后,模型會把相似的詞編碼成相似的向量(距離比較近)。

最后,為了把padding的batch數(shù)據(jù)傳給RNN,我們需要使用下面的兩個函數(shù)來進(jìn)行pack和unpack,后面我們會詳細(xì)介紹它們。這兩個函數(shù)是:

  • torch.nn.utils.rnn.pack_padded_sequence

  • torch.nn.utils.rnn.pad_packed_sequence

計算圖:

1) 把詞的ID通過Embedding層變成向量。 2) 把padding后的數(shù)據(jù)進(jìn)行pack。 3) 傳入GRU進(jìn)行Forward計算。 4) Unpack計算結(jié)果 5) 把雙向GRU的結(jié)果向量加起來。 6) 返回(所有時刻的)輸出和最后時刻的隱狀態(tài)。

輸入:

input_seq: 一個batch的輸入句子,shape是(max_length, batch_size)

input_lengths: 一個長度為batch的list,表示句子的實際長度。

hidden: 初始化隱狀態(tài)(通常是零),shape是(n_layers x num_directions, batch_size, hidden_size)

輸出:

outputs: 最后一層GRU的輸出向量(雙向的向量加在了一起),shape(max_length, batch_size, hidden_size)

hidden: 最后一個時刻的隱狀態(tài),shape是(n_layers x num_directions, batch_size, hidden_size)

EncoderRNN代碼如下,請讀者詳細(xì)閱讀注釋。

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

Decoder

Decoder也是一個RNN,它每個時刻輸出一個詞。每個時刻的輸入是上一個時刻的隱狀態(tài)和上一個時刻的輸出。一開始的隱狀態(tài)是Encoder最后時刻的隱狀態(tài),輸入是特殊的。然后使用RNN計算新的隱狀態(tài)和輸出第一個詞,接著用新的隱狀態(tài)和第一個詞計算第二個詞,...,直到遇到,結(jié)束輸出。普通的RNN Decoder的問題是它只依賴與Encoder最后一個時刻的隱狀態(tài),雖然理論上這個隱狀態(tài)(context向量)可以編碼輸入句子的語義,但是實際會比較困難。因此當(dāng)輸入句子很長的時候,效果會很長。

為了解決這個問題,Bahdanau等人在論文里提出了注意力機制(attention mechanism),在Decoder進(jìn)行t時刻計算的時候,除了t-1時刻的隱狀態(tài),當(dāng)前時刻的輸入,注意力機制還可以參考Encoder所有時刻的輸入。拿機器翻譯來說,我們在翻譯以句子的第t個詞的時候會把注意力機制在某個詞上。當(dāng)然常見的注意力是一種soft的注意力,假設(shè)輸入有5個詞,注意力可能是一個概率,比如(0.6,0.1,0.1,0.1,0.1),表示當(dāng)前最關(guān)注的是輸入的第一個詞。同時我們之前也計算出每個時刻的輸出向量,假設(shè)5個時刻分別是$y_1,…,y_5$,那么我們可以用attention概率加權(quán)得到當(dāng)前時刻的context向量$0.6y_1+0.1y_2+…+0.1y_5$。

注意力有很多方法計算,我們這里介紹Luong等人在論文提出的方法。它是用當(dāng)前時刻的GRU計算出的新的隱狀態(tài)來計算注意力得分,首先它用一個score函數(shù)計算這個隱狀態(tài)和Encoder的輸出的相似度得分,得分越大,說明越應(yīng)該注意這個詞。然后再用softmax函數(shù)把score變成概率。那機器翻譯為例,在t時刻,$h_t$表示t時刻的GRU輸出的新的隱狀態(tài),我們可以認(rèn)為$h_t$表示當(dāng)前需要翻譯的語義。通過計算$h_t$與$y_1,…,y_n$的得分,如果$h_t$與$y_1$的得分很高,那么我們可以認(rèn)為當(dāng)前主要翻譯詞$x_1$的語義。有很多中score函數(shù)的計算方法,如下圖所示:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

上式中$h_t$表示t時刻的隱狀態(tài),比如第一種計算score的方法,直接計算$h_t$與$h_s$的內(nèi)積,內(nèi)積越大,說明這兩個向量越相似,因此注意力也更多的放到這個詞上。第二種方法也類似,只是引入了一個可以學(xué)習(xí)的矩陣,我們可以認(rèn)為它先對$h_t$做一個線性變換,然后在與$h_s$計算內(nèi)積。而第三種方法把它們拼接起來然后用一個全連接網(wǎng)絡(luò)來計算score。

注意,我們前面介紹的是分別計算$h_t$和$y_1$的內(nèi)積、$h_t$和$y_2$的內(nèi)積,…。但是為了效率,可以一次計算$h_t$與$h_s=[y_1,y_2,…,y_n]$的乘積。 計算過程如下圖所示。

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

上面的代碼實現(xiàn)了dot、general和concat三種score計算方法,分別和前面的三個公式對應(yīng),我們這里介紹最簡單的dot方法。代碼里也有一些注釋,只有dot_score函數(shù)比較難以理解,我們來分析一下。首先這個函數(shù)的輸入輸入hidden的shape是(1, batch=64, hidden_size=500),encoder_outputs的shape是(input_lengths=10, batch=64, hidden_size=500)。

怎么計算hidden和10個encoder輸出向量的內(nèi)積呢?為了簡便,我們先假設(shè)batch是1,這樣可以把第二維(batch維)去掉,因此hidden是(1, 500),而encoder_outputs是(10, 500)。內(nèi)積的定義是兩個向量對應(yīng)位相乘然后相加,但是encoder_outputs是10個500維的向量。當(dāng)然我們可以寫一個for循環(huán)來計算,但是效率很低。這里用到一個小的技巧,利用broadcasting,hidden * encoder_outputs可以理解為把hidden從(1,500)復(fù)制成(10, 500)(當(dāng)然實際實現(xiàn)并不會這么做),然后兩個(10, 500)的矩陣進(jìn)行乘法。注意,這里的乘法不是矩陣乘法,而是所謂的Hadamard乘法,其實就是把對應(yīng)位置的乘起來,比如下面的例子:

因此hidden * encoder_outputs就可以把hidden向量(500個數(shù))與encoder_outputs的10個向量(500個數(shù))對應(yīng)的位置相乘。而內(nèi)積還需要把這500個乘積加起來,因此后面使用torch.sum(hidden * encoder_output, dim=2),把第2維500個乘積加起來,最終得到10個score值。當(dāng)然我們實際還有一個batch維度,因此最終得到的attn_energies是(10, 64)。接著在forward函數(shù)里把attn_energies轉(zhuǎn)置成(64, 10),然后使用softmax函數(shù)把10個score變成概率,shape仍然是(64, 10),為了后面使用方便,我們用unsqueeze(1)把它變成(64, 1, 10)。

有了注意力的子模塊之后,我們就可以實現(xiàn)Decoder了。Encoder可以一次把一個序列輸入GRU,得到整個序列的輸出。但是Decoder t時刻的輸入是t-1時刻的輸出,在t-1時刻計算完成之前是未知的,因此只能一次處理一個時刻的數(shù)據(jù)。因此Encoder的GRU的輸入是(max_length, batch, hidden_size),而Decoder的輸入是(1, batch, hidden_size)。此外Decoder只能利用前面的信息,所以只能使用單向(而不是雙向)的GRU,而Encoder的GRU是雙向的,如果兩種的hidden_size是一樣的,則Decoder的隱單元個數(shù)少了一半,那怎么把Encoder的最后時刻的隱狀態(tài)作為Decoder的初始隱狀態(tài)呢?這里是把每個時刻雙向結(jié)果加起來的,因此它們的大小就能匹配了(請讀者參考前面Encoder雙向相加的部分代碼)。

計算圖:

1) 把詞ID輸入Embedding層 2) 使用單向的GRU繼續(xù)Forward進(jìn)行一個時刻的計算。 3) 使用新的隱狀態(tài)計算注意力權(quán)重 4) 用注意力權(quán)重得到context向量 5) context向量和GRU的輸出拼接起來,然后再進(jìn)過一個全連接網(wǎng)絡(luò),使得輸出大小仍然是hidden_size 6) 使用一個投影矩陣把輸出從hidden_size變成詞典大小,然后用softmax變成概率 7) 返回輸出和新的隱狀態(tài)

輸入:

input_step: shape是(1, batch_size)

last_hidden: 上一個時刻的隱狀態(tài), shape是(n_layers x num_directions, batch_size, hidden_size)

encoder_outputs: encoder的輸出, shape是(max_length, batch_size, hidden_size)

輸出:

output: 當(dāng)前時刻輸出每個詞的概率,shape是(batch_size, voc.num_words)

hidden: 新的隱狀態(tài),shape是(n_layers x num_directions, batch_size, hidden_size)

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

定義訓(xùn)練過程

Masked損失

forward實現(xiàn)之后,我們就需要計算loss。seq2seq有兩個RNN,Encoder RNN是沒有直接定義損失函數(shù)的,它是通過影響Decoder從而影響最終的輸出以及l(fā)oss。Decoder輸出一個序列,前面我們介紹的是Decoder在預(yù)測時的過程,它的長度是不固定的,只有遇到EOS才結(jié)束。給定一個問答句對,我們可以把問題輸入Encoder,然后用Decoder得到一個輸出序列,但是這個輸出序列和”真實”的答案長度并不相同。

而且即使長度相同并且語義相似,也很難直接知道預(yù)測的答案和真實的答案是否類似。那么我們怎么計算loss呢?比如輸入是”What is your name?”,訓(xùn)練數(shù)據(jù)中的答案是”I am LiLi”。假設(shè)模型有兩種預(yù)測:”I am fine”和”My name is LiLi”。從語義上顯然第二種答案更好,但是如果字面上比較的話可能第一種更好。

但是讓機器知道”I am LiLi”和”My name is LiLi”的語義很接近這是非常困難的,所以實際上我們通常還是通過字面上里進(jìn)行比較。我們會限制Decoder的輸出,使得Decoder的輸出長度和”真實”答案一樣,然后逐個時刻比較。Decoder輸出的是每個詞的概率分布,因此可以使用交叉熵?fù)p失函數(shù)。但是這里還有一個問題,因為是一個batch的數(shù)據(jù)里有一些是padding的,因此這些位置的預(yù)測是沒有必要計算loss的,因此我們需要使用前面的mask矩陣把對應(yīng)位置的loss去掉,我們可以通過下面的函數(shù)來實現(xiàn)計算Masked的loss。

簡單易懂------使用PyTorch實現(xiàn)Chatbot   上面的代碼有幾個需要注意的地方。首先是masked_select函數(shù),我們來看一個例子:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

它要求mask和被mask的tensor的shape是一樣的,然后從crossEntropy選出mask值為1的那些值。輸出的維度會減1。

另外為了實現(xiàn)交叉熵這里使用了gather函數(shù),這是一種比較底層的實現(xiàn)方法,更簡便的方法應(yīng)該使用CrossEntropyLoss或者NLLLoss,其中CrossEntropy等價與LogSoftmax+NLLLoss。

交叉熵的定義為:$H(p,q)=-\sum_xp(x)logq(x)$。其中p和q是兩個隨機變量的概率分布,這里是離散的隨機變量,如果是連續(xù)的需要把求和變成積分。在我們這里p是真實的分布,也就是one-hot的,而q是模型預(yù)測的softmax的輸出。因為p是one-hot的,所以只需要計算真實分類對應(yīng)的那個值。

比如假設(shè)一個5分類的問題,當(dāng)前正確分類是2(下標(biāo)從0-4),而模型的預(yù)測是(0.1,0.1,0.4,0.2,0.2),則H=-log(0.4)。用交叉熵作為分類的Loss是比較合理的,正確的分類是2,那么模型在下標(biāo)為2的地方預(yù)測的概率$q_2$越大,則$-logq_2$越小,也就是loss越小。

假設(shè)inp是:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

也就是batch=2,而分類數(shù)(詞典大小)是4,inp是模型預(yù)測的分類概率。 而target = [2,3] ,表示第一個樣本的正確分類是第三個類別(概率是0.4),第二個樣本的正確分類是第四個類別(概率是0.3)。因此我們需要計算的是 -log(0.4) - log(0.3)。怎么不用for循環(huán)求出來呢?我們可以使用torch.gather函數(shù)首先把0.4和0.3選出來:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

一次迭代的訓(xùn)練過程

函數(shù)train實現(xiàn)一個batch數(shù)據(jù)的訓(xùn)練。前面我們提到過,在訓(xùn)練的時候我們會限制Decoder的輸出,使得Decoder的輸出長度和”真實”答案一樣長。但是我們在訓(xùn)練的時候如果讓Decoder自行輸出,那么收斂可能會比較慢,因為Decoder在t時刻的輸入來自t-1時刻的輸出。如果前面預(yù)測錯了,那么后面很可能都會錯下去。另外一種方法叫做teacher forcing,它不管模型在t-1時刻做什么預(yù)測都把t-1時刻的正確答案作為t時刻的輸入。但是如果只用teacher forcing也有問題,因為在真實的Decoder的是是沒有老師來幫它糾正錯誤的。所以比較好的方法是更加一個teacher_forcing_ratio參數(shù)隨機的來確定本次訓(xùn)練是否teacher forcing。

另外使用到的一個技巧是梯度裁剪(gradient clipping) 。這個技巧通常是為了防止梯度爆炸(exploding gradient),它把參數(shù)限制在一個范圍之內(nèi),從而可以避免梯度的梯度過大或者出現(xiàn)NaN等問題。注意:雖然它的名字叫梯度裁剪,但實際它是對模型的參數(shù)進(jìn)行裁剪,它把整個參數(shù)看成一個向量,如果這個向量的模大于max_norm,那么就把這個向量除以一個值使得模等于max_norm,因此也等價于把這個向量投影到半徑為max_norm的球上。它的效果如下圖所示。

簡單易懂------使用PyTorch實現(xiàn)Chatbot

操作步驟:

1) 把整個batch的輸入傳入encoder 2) 把decoder的輸入設(shè)置為特殊的,初始隱狀態(tài)設(shè)置為encoder最后時刻的隱狀態(tài) 3) decoder每次處理一個時刻的forward計算 4) 如果是teacher forcing,把上個時刻的"正確的"詞作為當(dāng)前輸入,否則用上一個時刻的輸出作為當(dāng)前時刻的輸入 5) 計算loss 6) 反向計算梯度 7) 對梯度進(jìn)行裁剪 8) 更新模型(包括encoder和decoder)參數(shù)

注意,PyTorch的RNN模塊(RNN, LSTM, GRU)也可以當(dāng)成普通的非循環(huán)的網(wǎng)絡(luò)來使用。在Encoder部分,我們是直接把所有時刻的數(shù)據(jù)都傳入RNN,讓它一次計算出所有的結(jié)果,但是在Decoder的時候(非teacher forcing)后一個時刻的輸入來自前一個時刻的輸出,因此無法一次計算。

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

訓(xùn)練迭代過程

最后是把前面的代碼組合起來進(jìn)行訓(xùn)練。函數(shù)trainIters用于進(jìn)行n_iterations次minibatch的訓(xùn)練。

值得注意的是我們定期會保存模型,我們會保存一個tar包,包括encoder和decoder的state_dicts(參數(shù)),優(yōu)化器(optimizers)的state_dicts, loss和迭代次數(shù)。這樣保存模型的好處是從中恢復(fù)后我們既可以進(jìn)行預(yù)測也可以進(jìn)行訓(xùn)練(因為有優(yōu)化器的參數(shù)和迭代的次數(shù))。

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

效果測試

模型訓(xùn)練完成之后,我們需要測試它的效果。最簡單直接的方法就是和chatbot來聊天。因此我們需要用Decoder來生成一個響應(yīng)。

貪心解碼(Greedy decoding)算法

最簡單的解碼算法是貪心算法,也就是每次都選擇概率最高的那個詞,然后把這個詞作為下一個時刻的輸入,直到遇到EOS結(jié)束解碼或者達(dá)到一個最大長度。但是貪心算法不一定能得到最優(yōu)解,因為某個答案可能開始的幾個詞的概率并不太高,但是后來概率會很大。因此除了貪心算法,我們通常也可以使用Beam-Search算法,也就是每個時刻保留概率最高的Top K個結(jié)果,然后下一個時刻嘗試把這K個結(jié)果輸入(當(dāng)然需要能恢復(fù)RNN的狀態(tài)),然后再從中選擇概率最高的K個。

為了實現(xiàn)貪心解碼算法,我們定義一個GreedySearchDecoder類。這個類的forwar的方法需要傳入一個輸入序列(input_seq),其shape是(input_seq length, 1), 輸入長度input_length和最大輸出長度max_length。就是過程如下:

1) 把輸入傳給Encoder,得到所有時刻的輸出和最后一個時刻的隱狀態(tài)。 2) 把Encoder最后時刻的隱狀態(tài)作為Decoder的初始狀態(tài)。 3) Decoder的第一輸入初始化為SOS。 4) 定義保存解碼結(jié)果的tensor 5) 循環(huán)直到最大解碼長度 a) 把當(dāng)前輸入傳入Decoder b) 得到概率最大的詞以及概率 c) 把這個詞和概率保存下來 d) 把當(dāng)前輸出的詞作為下一個時刻的輸入 6) 返回所有的詞和概率

簡單易懂------使用PyTorch實現(xiàn)Chatbot

測試對話函數(shù)

解碼方法完成后,我們寫一個函數(shù)來測試從終端輸入一個句子然后來看看chatbot的回復(fù)。我們需要用前面的函數(shù)來把句子分詞,然后變成ID傳入解碼器,得到輸出的ID后再轉(zhuǎn)換成文字。我們會實現(xiàn)一個evaluate函數(shù),由它來完成這些工作。我們需要把一個句子變成輸入需要的格式——shape為(batch, max_length),即使只有一個輸入也需要增加一個batch維度。我們首先把句子分詞,然后變成ID的序列,然后轉(zhuǎn)置成合適的格式。此外我們還需要創(chuàng)建一個名為lengths的tensor,雖然只有一個,來表示輸入的實際長度。接著我們構(gòu)造類GreedySearchDecoder的實例searcher,然后用searcher來進(jìn)行解碼得到輸出的ID,最后我們把這些ID變成詞并且去掉EOS之后的內(nèi)容。

另外一個evaluateInput函數(shù)作為chatbot的用戶接口,當(dāng)運行它的時候,它會首先提示用戶輸入一個句子,然后使用evaluate來生成回復(fù)。然后繼續(xù)對話直到用戶輸入”q”或者”quit”。如果用戶輸入的詞不在詞典里,我們會輸出錯誤信息(當(dāng)然還有一種辦法是忽略這些詞)然后提示用戶重新輸入。

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

訓(xùn)練和測試模型

最后我們可以來訓(xùn)練模型和進(jìn)行評測了。

不論是我們像訓(xùn)練模型還是測試對話,我們都需要初始化encoder和decoder模型參數(shù)。在下面的代碼,我們從頭開始訓(xùn)練模型或者從某個checkpoint加載模型。讀者可以嘗試不同的超參數(shù)配置來進(jìn)行調(diào)優(yōu)。

簡單易懂------使用PyTorch實現(xiàn)Chatbot簡單易懂------使用PyTorch實現(xiàn)Chatbot

訓(xùn)練

下面的代碼進(jìn)行訓(xùn)練,我們需要設(shè)置一些訓(xùn)練的超參數(shù)。初始化優(yōu)化器,最后調(diào)用函數(shù)trainIters進(jìn)行訓(xùn)練。

簡單易懂------使用PyTorch實現(xiàn)Chatbot

測試

我們使用下面的代碼進(jìn)行測試。

簡單易懂------使用PyTorch實現(xiàn)Chatbot

下面是測試的一些例子:

簡單易懂------使用PyTorch實現(xiàn)Chatbot

結(jié)論

上面介紹了怎么從零開始訓(xùn)練一個chatbot,讀者可以用自己的數(shù)據(jù)訓(xùn)練一個chatbot試試,看看能不能用來解決一些實際業(yè)務(wù)問題。(雷鋒網(wǎng)雷鋒網(wǎng)雷鋒網(wǎng))

雷峰網(wǎng)版權(quán)文章,未經(jīng)授權(quán)禁止轉(zhuǎn)載。詳情見轉(zhuǎn)載須知。

簡單易懂------使用PyTorch實現(xiàn)Chatbot

分享:
相關(guān)文章

專注AIR(人工智能+機器人)

專注人工智能+機器人報道,經(jīng)驗分享請加微信keatslee8(請注明原因)。 科學(xué)的本質(zhì)是:問一個不恰當(dāng)?shù)膯栴},于是走上了通往恰當(dāng)答案的路。
當(dāng)月熱門文章
最新文章
請?zhí)顚懮暾埲速Y料
姓名
電話
郵箱
微信號
作品鏈接
個人簡介
為了您的賬戶安全,請驗證郵箱
您的郵箱還未驗證,完成可獲20積分喲!
請驗證您的郵箱
立即驗證
完善賬號信息
您的賬號已經(jīng)綁定,現(xiàn)在您可以設(shè)置密碼以方便用郵箱登錄
立即設(shè)置 以后再說