前面提到了CNN和基于CNN的各類網(wǎng)絡(luò)及其在圖像處理上的應(yīng)用。這類網(wǎng)絡(luò)有一個(gè)特點(diǎn)蛀缝,就是輸入和輸出都是固定長(zhǎng)度的刘莹。比方說在MNIST阎毅、CIFAR-10、ImageNet數(shù)據(jù)集上点弯,這些算法都非常有效扇调,但是只能處理輸入和輸出都是固定長(zhǎng)度的數(shù)據(jù)集。
在實(shí)際中抢肛,需要處理很多變長(zhǎng)的需求狼钮,比方說在機(jī)器翻譯中,源語言中每個(gè)句子長(zhǎng)度是不一樣的捡絮,源語言對(duì)應(yīng)的目標(biāo)語言的長(zhǎng)度也是不一樣的熬芜。這時(shí)使用CNN就不能達(dá)到想要的效果。從結(jié)構(gòu)上講福稳,全連接神經(jīng)網(wǎng)絡(luò)和卷積神經(jīng)網(wǎng)絡(luò)模型中涎拉,網(wǎng)絡(luò)的結(jié)構(gòu)都是輸入層-隱含層(多個(gè))-輸出層結(jié)構(gòu),層與層之間是全連接或者部分連接的圆,但是同一層內(nèi)是沒有連接的鼓拧,即所有的連接都是朝一個(gè)方向。
使計(jì)算機(jī)模仿人類的行為一直是大家研究的方向越妈,對(duì)于圖片的識(shí)別可以用CNN季俩,那么序列數(shù)據(jù)用什么呢?本章介紹對(duì)于序列數(shù)據(jù)的處理梅掠,以及神經(jīng)網(wǎng)絡(luò)家族中另一種新的神經(jīng)網(wǎng)絡(luò)——循環(huán)神經(jīng)網(wǎng)絡(luò)(Recurrent Neural Network酌住,RNN)店归。RNN是為了處理變長(zhǎng)數(shù)據(jù)而設(shè)計(jì)的。
本章內(nèi)容首先提到的是序列數(shù)據(jù)的處理赂韵,然后介紹標(biāo)準(zhǔn)的RNN以及它面臨的一些問題,隨后介紹RNN的一些擴(kuò)展LSTM(Long Short-Term Memory)以及RNNs(Recurrent Neural Networks挠蛉,基于循環(huán)神經(jīng)網(wǎng)絡(luò)變形的統(tǒng)稱)在NLP(Natural Language Process祭示,自然語言處理)上的應(yīng)用,最后結(jié)合一個(gè)示例介紹PyTorch中RNNs的實(shí)現(xiàn)谴古。
1.序列數(shù)據(jù)處理
序列數(shù)據(jù)包括時(shí)間序列及串?dāng)?shù)據(jù)质涛,常見的序列有時(shí)序數(shù)據(jù)、文本數(shù)據(jù)掰担、語音數(shù)據(jù)等汇陆。處理序列數(shù)據(jù)的模型稱為序列模型。序列模型是自然語言處理中的一個(gè)核心模型带饱,依賴時(shí)間信息毡代。傳統(tǒng)機(jī)器學(xué)習(xí)方法中序列模型有隱馬爾可夫模型(Hidden Markov Model,HMM)和條件隨機(jī)場(chǎng)(Conditional Random Field勺疼,CRF)都是概率圖模型教寂,其中HMM在語音識(shí)別和文字識(shí)別領(lǐng)域應(yīng)用廣泛,CRF被廣泛應(yīng)用于分詞执庐、詞性標(biāo)注和命名實(shí)體識(shí)別問題酪耕。
神經(jīng)網(wǎng)絡(luò)處理序列數(shù)據(jù),幫助我們從已知的數(shù)據(jù)中預(yù)測(cè)未來的模式轨淌,在模型識(shí)別上取得很好的效果迂烁。作為預(yù)測(cè)未來模式的神經(jīng)網(wǎng)絡(luò)有窺視未來的本領(lǐng),比方說可以根據(jù)過去幾天的股票價(jià)格來預(yù)測(cè)股票趨勢(shì)递鹉。在前饋神經(jīng)網(wǎng)絡(luò)中對(duì)于特定的輸入都會(huì)有相同的輸出盟步,所以我們要正確地對(duì)輸入信息進(jìn)行編碼。對(duì)時(shí)間序列數(shù)據(jù)的編碼有很多躏结,其中最簡(jiǎn)單址芯、應(yīng)用最廣泛的編碼是基于滑動(dòng)窗口的方法。下面介紹一下滑動(dòng)窗口編碼的機(jī)制窜觉。
在時(shí)間序列上谷炸,滑動(dòng)窗口把序列分成兩個(gè)窗口,分別代表過去和未來禀挫,這兩個(gè)窗口的大小都需要人工確定旬陡。比如要預(yù)測(cè)股票價(jià)格,過去窗口的大小表示要考慮多久以前的數(shù)據(jù)進(jìn)行預(yù)測(cè)语婴,如果要考慮過去5天的數(shù)據(jù)來預(yù)測(cè)未來2天的股票價(jià)格描孟,此時(shí)的神經(jīng)網(wǎng)絡(luò)需要5個(gè)輸入和2個(gè)輸出驶睦。考慮下面一個(gè)簡(jiǎn)單的時(shí)間序列:
1,2,3,4,3,2,1,2,3,4,3,2,1
神經(jīng)網(wǎng)絡(luò)可以用3個(gè)輸入神經(jīng)元和1個(gè)輸出神經(jīng)元匿醒,也就是利用過去3個(gè)時(shí)間的數(shù)據(jù)預(yù)測(cè)下1個(gè)時(shí)間的數(shù)據(jù)场航,這時(shí)在訓(xùn)練集中序列數(shù)據(jù)應(yīng)該表示如下:
[1,2,3] --> [4]
[2,3,4] --> [3]
[3,4,3] --> [2]
[4,3,2] --> [1]
也就是說,從串的起始位置開始廉羔,輸入窗口大小為3溉痢,第4個(gè)為輸出窗口,是期望的輸出值憋他。然后窗口以步長(zhǎng)為1向前滑動(dòng)孩饼,落在輸入窗口的為輸入,落在輸出窗口的為輸出竹挡。這樣在窗口向前滑動(dòng)的過程中镀娶,產(chǎn)生一系列的訓(xùn)練數(shù)據(jù)。其中揪罕,輸入窗口和輸出窗口的大小都是可以變化的梯码,比方說要根據(jù)過去3個(gè)時(shí)間點(diǎn)的數(shù)據(jù)預(yù)測(cè)來2個(gè)時(shí)間點(diǎn)的數(shù)據(jù),也就是輸出窗口的大小為2好啰,此時(shí)得到的訓(xùn)練數(shù)據(jù)為:
[1,2,3] --> [4,3]
[2,3,4] --> [3,2]
[3,4,3] --> [2,1]
[4,3,2] --> [1,2]
上面的兩個(gè)例子是在一個(gè)時(shí)間序列上對(duì)數(shù)據(jù)進(jìn)行編碼忍些,也可以對(duì)多個(gè)時(shí)間序列進(jìn)行編碼。例如坎怪,需要通過股票過去的價(jià)格和交易量來預(yù)測(cè)股票趨勢(shì)罢坝,我們有兩個(gè)時(shí)間序列,一個(gè)是價(jià)格序列搅窿,一個(gè)是交易量序列:
序列1:1,2,3,4,3,2,1,2,3,4,3,2,1
序列2:10,20,30,40,30,20,10,20,30,40,30,20,10
這時(shí)需要把序列2的數(shù)據(jù)加入到序列1中嘁酿,同樣用輸入窗口大小為3,輸出窗口大小為1為例男应,訓(xùn)練集:
[1,10,2,20,3,30] --> [4]
[2,20,3,30,4,40] --> [3]
[3,30,4,40,3,30] --> [2]
[4,40,3,30,2,20] --> [1]
其中序列1用來預(yù)測(cè)它自己闹司,而序列2作為輔助信息。類似的可以用到多個(gè)序列數(shù)據(jù)的預(yù)測(cè)上沐飘,而且要預(yù)測(cè)的列可以不在輸入信息流中游桩,比方說可以用IBM和蘋果的股票價(jià)格來預(yù)測(cè)微軟的股票價(jià)格,此時(shí)微軟的股票價(jià)格不出現(xiàn)在輸入信息中耐朴。
滑動(dòng)窗口機(jī)制有點(diǎn)像卷積操作借卧,所以也有人稱滑動(dòng)窗口為1維卷積。在自然語言處理中筛峭,滑動(dòng)窗口等同于Ngram铐刘。例如,在詞性標(biāo)注的任務(wù)中影晓,輸入窗口為上下文的詞镰吵,輸出窗口輸出的是輸入窗口最右側(cè)一個(gè)詞的詞性檩禾,每次向前滑動(dòng)一個(gè)窗口,直到句子結(jié)束疤祭。對(duì)于文本的向量化表示盼产,可以使用one-hot編碼,也可以使用詞嵌入勺馆,相比來說詞嵌入是更稠密的表示戏售,訓(xùn)練過程中可以減少神經(jīng)網(wǎng)絡(luò)中參數(shù)的數(shù)量,使得訓(xùn)練更快谓传。
滑動(dòng)窗口機(jī)制雖然可以用來對(duì)序列數(shù)據(jù)進(jìn)行編碼蜈项,但是它把序列問題處理成一對(duì)一的映射問題芹关,即輸入串到輸出串的映射续挟,而且兩個(gè)串的大小都是固定的。很多任務(wù)中侥衬,我們需要比一對(duì)一映射更復(fù)雜的表示诗祸,例如在情感分析中,我們需要輸入一整句話來判斷情感極性轴总,而且每個(gè)實(shí)例中句子長(zhǎng)度不確定直颅;或者要使用更復(fù)雜的輸入——用一張圖片來生成一個(gè)句子,用來描述這個(gè)圖片怀樟。這樣的任務(wù)中沒有輸入到輸出的特定映射關(guān)系功偿,而是需要神經(jīng)網(wǎng)絡(luò)對(duì)輸入串有記憶功能:在讀取輸入的過程中,記住輸入的關(guān)鍵信息往堡。這時(shí)我們需要一種神經(jīng)網(wǎng)絡(luò)可以保存記憶械荷,就是有狀態(tài)的網(wǎng)絡(luò)。下面我們來介紹有狀態(tài)的神經(jīng)網(wǎng)絡(luò)虑灰。
2.循環(huán)神經(jīng)網(wǎng)絡(luò)
循環(huán)神經(jīng)網(wǎng)絡(luò)(Recurrent Neural Network吨瞎,RNN)是從20世紀(jì)80年代慢慢發(fā)展起來的,與CNN對(duì)比穆咐,RNN內(nèi)部有循環(huán)結(jié)構(gòu)颤诀,這也是名字的由來。需要注意的是对湃,RNN這個(gè)簡(jiǎn)稱有時(shí)候也會(huì)被用來指遞歸神經(jīng)網(wǎng)絡(luò)(Recursive Neural Network)崖叫,但是這是兩種不同的網(wǎng)絡(luò)結(jié)構(gòu),遞歸神經(jīng)網(wǎng)絡(luò)是深的樹狀結(jié)構(gòu)拍柒,而循環(huán)神經(jīng)網(wǎng)絡(luò)是鏈狀結(jié)構(gòu)归露,要注意區(qū)分。提到RNN大多指的是循環(huán)神經(jīng)網(wǎng)絡(luò)斤儿。RNNs即基于循環(huán)神經(jīng)網(wǎng)絡(luò)變形的總稱剧包。
RNN可以解決變長(zhǎng)序列問題恐锦,通過分析時(shí)間序列數(shù)據(jù)達(dá)到“預(yù)測(cè)未來”的本領(lǐng),比如要說的下一個(gè)詞疆液、汽車的運(yùn)行軌跡一铅、鋼琴彈奏的下一個(gè)音符等。RNN可以工作在任意長(zhǎng)度的序列數(shù)據(jù)上堕油,使得其在NLP上運(yùn)用十分廣泛:自動(dòng)翻譯潘飘、語音識(shí)別、情感分析和人機(jī)對(duì)話等掉缺。
循環(huán)神經(jīng)網(wǎng)絡(luò)里有重復(fù)神經(jīng)網(wǎng)絡(luò)基本模型的鏈?zhǔn)叫问讲仿迹跇?biāo)準(zhǔn)的RNN中,神經(jīng)網(wǎng)絡(luò)的基本模型僅僅包含了一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)層眶明,比如一個(gè)雙極性的Tanh層艰毒,如下圖所示。
標(biāo)準(zhǔn)RNN的前向傳播公式如下:
RNN中存在循環(huán)結(jié)構(gòu)搜囱,指的是神經(jīng)元的輸入是該層神經(jīng)元的輸出丑瞧。如下圖所示。左邊是RNN的結(jié)構(gòu)圖蜀肘,右邊是RNN結(jié)構(gòu)按時(shí)刻展開绊汹。時(shí)刻是RNN中非常重要的概念,不同時(shí)刻記憶在隱藏單元中存儲(chǔ)和流動(dòng)扮宠,每一個(gè)時(shí)刻的隱含層單元有一個(gè)輸出麸祷。在RNN中著榴,各隱含層共享參數(shù)W,U,V盒齿。
記憶在隱藏單元中存儲(chǔ)和流動(dòng)乖杠,而輸入取自于隱藏單元以及網(wǎng)絡(luò)的最終輸出狭魂。展開后可以看出網(wǎng)絡(luò)輸出串的整個(gè)過程阅茶,即圖中的輸出為,坏晦,萝玷,展開后的神經(jīng)網(wǎng)絡(luò)每一層負(fù)責(zé)一個(gè)輸出。是時(shí)刻的輸入昆婿,可以是當(dāng)前詞的一個(gè)one-hot向量球碉,是時(shí)刻的隱藏層狀態(tài),是網(wǎng)絡(luò)的記憶單元仓蛆,基于前面時(shí)刻的隱藏層狀態(tài)和輸入信息計(jì)算得出睁冬,激活函數(shù)可以是Tanh或者ReLU,對(duì)于網(wǎng)絡(luò)時(shí)刻的神經(jīng)網(wǎng)絡(luò)狀態(tài)可以簡(jiǎn)單地初始化成0;是時(shí)刻神經(jīng)網(wǎng)絡(luò)的輸出豆拨,從神經(jīng)網(wǎng)絡(luò)中產(chǎn)生每個(gè)時(shí)刻的輸出信息直奋,例如,在文本處理中施禾,輸出可能是詞匯的概率向量脚线,通過Softmax得出。
根據(jù)輸入輸出的不同弥搞,RNN可以按以下情況分類:
- 1-N:一個(gè)輸入邮绿,多個(gè)輸出。例如:圖片描述攀例,輸入是一個(gè)圖片船逮,輸出是一個(gè)句子;音樂生成粤铭,輸入一個(gè)數(shù)值挖胃,代表一個(gè)音符或者一個(gè)音樂風(fēng)格,神經(jīng)網(wǎng)絡(luò)自動(dòng)生成一段旋律承耿;還有句子生成等冠骄。
- N-1:多個(gè)輸入伪煤,一個(gè)輸出加袋。大多根據(jù)輸出的串做預(yù)測(cè)和分類,比如語義分類抱既、情感分析职烧、天氣預(yù)報(bào)、股市預(yù)測(cè)防泵、商品推薦蚀之、DNA序列分類、異常檢測(cè)等捷泞。
- N-N:多個(gè)輸入足删,多個(gè)輸出,而且輸入和輸出長(zhǎng)度相等锁右。比如命名實(shí)體識(shí)別失受,詞性標(biāo)注等輸入和輸出的長(zhǎng)度一樣。
- N-M:一般情況下N≠M(fèi)咏瑟,例如機(jī)器翻譯拂到、文本摘要等,輸入輸出長(zhǎng)度是不一樣的码泞。
根據(jù)不同的任務(wù)兄旬,循環(huán)神經(jīng)網(wǎng)絡(luò)會(huì)有不同的結(jié)構(gòu)。如下圖所示余寥。
根據(jù)傳播方向的不同领铐,還有雙向RNN悯森,上面講到的網(wǎng)絡(luò)結(jié)構(gòu)都是通過當(dāng)前時(shí)刻和過去時(shí)刻產(chǎn)生輸出,但是有些任務(wù)比如語音識(shí)別绪撵,需要通過后面的信息判斷前面的輸出狀態(tài)呐馆。雙向循環(huán)神經(jīng)網(wǎng)絡(luò)就是為了這種需求提出的,它允許時(shí)刻到時(shí)刻有鏈接莲兢,從而能夠使網(wǎng)絡(luò)根據(jù)未來的狀態(tài)調(diào)整當(dāng)前的狀態(tài)汹来。這在實(shí)際應(yīng)用中有很好的例子:語音識(shí)別輸入的時(shí)候,會(huì)先輸出一個(gè)認(rèn)為不錯(cuò)的序列改艇,但是說完以后會(huì)根據(jù)后面的輸入調(diào)整已經(jīng)出現(xiàn)的輸出收班。雙向RNN的結(jié)構(gòu)如下圖所示。
RNN的訓(xùn)練時(shí)按時(shí)刻展開循環(huán)神經(jīng)網(wǎng)絡(luò)進(jìn)行反向傳播谒兄,反向傳播算法的目的是找出在所有網(wǎng)絡(luò)參數(shù)下的損失梯度摔桦。因?yàn)镽NN的參數(shù)在所有時(shí)刻都是共享的,每一次反向傳播不僅依賴當(dāng)前時(shí)刻的計(jì)算結(jié)果承疲,而且依賴之前的時(shí)刻邻耕,按時(shí)刻對(duì)神經(jīng)網(wǎng)絡(luò)展開,并執(zhí)行反向傳播燕鸽,這個(gè)過程叫做Back Propagation Through Time(BPTT)兄世,是反向傳播的擴(kuò)展。和傳統(tǒng)的神經(jīng)網(wǎng)絡(luò)一樣啊研,在時(shí)間序列上展開并前向傳播計(jì)算出輸出御滩,利用所有時(shí)刻的輸出計(jì)算損失,模型參數(shù)通過BPTT算法更新党远。梯度的反向傳遞依賴的是損失函數(shù)中用到的所有輸出削解,并不是最后時(shí)刻的輸出。比如損失函數(shù)用到了沟娱,所以梯度傳遞的時(shí)候使用這三個(gè)輸出氛驮,而不使用,在所有時(shí)刻和都是共享的济似,所有反向傳播才能在所有的時(shí)刻上正確計(jì)算矫废。
由于存在梯度消失(大多時(shí)候)和梯度爆炸(極少,但對(duì)優(yōu)化過程影響極大)的原因碱屁,導(dǎo)致RNN的訓(xùn)練很難獲取到長(zhǎng)時(shí)依賴信息磷脯。有時(shí)句子中對(duì)一個(gè)詞的預(yù)測(cè)只需要考慮附近的詞,而不用考慮很遠(yuǎn)的開頭的地方娩脾,比如說在語言模型的任務(wù)中赵誓,試圖根據(jù)已有的序列預(yù)測(cè)相應(yīng)的單詞:要預(yù)測(cè)“the clouds are in the sky”中最后一個(gè)單詞“sky”,不需要更多的上下文信息,只要“the clouds are in the”就足夠預(yù)測(cè)出下一個(gè)單詞就是“sky”了俩功,這種目標(biāo)詞與相關(guān)信息很近的情況幻枉,RNN是可以通過學(xué)習(xí)獲得的。但是也有一些單詞的預(yù)測(cè)需要更“遠(yuǎn)”處的上下文信息诡蜓,比如說“I grew up in France... I speak fluent Frence.”要預(yù)測(cè)最后一個(gè)單詞“French”熬甫,最近的信息“speak fluent”只能獲得一種語言的結(jié)果,但是具體是哪一種語言就需要句子其他的上下文了蔓罚,就是包括“France”的那段椿肩,也就是預(yù)測(cè)目標(biāo)詞依賴的上下文可能會(huì)間隔很遠(yuǎn)。
不幸的是豺谈,隨著這種間隔的拉長(zhǎng)郑象,因?yàn)榇嬖谔荻认Щ虮ǖ膯栴}——梯度消失使得我們?cè)趦?yōu)化過程中不知道梯度方向,梯度爆炸會(huì)使得學(xué)習(xí)變得不穩(wěn)定——RNNs學(xué)習(xí)這些鏈接信息會(huì)變得很困難茬末。循環(huán)網(wǎng)絡(luò)需要在很長(zhǎng)時(shí)間序列的各個(gè)時(shí)刻重復(fù)相同的操作來完成深層的計(jì)算圖厂榛,模型中的參數(shù)是共享的,導(dǎo)致訓(xùn)練中的誤差在網(wǎng)絡(luò)層上的傳遞不斷累積丽惭,最終使得長(zhǎng)期依賴的問題變得更加突出击奶,使得深度神經(jīng)網(wǎng)絡(luò)喪失了學(xué)習(xí)先前信息的能力。
上面是標(biāo)準(zhǔn)RNN的概念和分類责掏,針對(duì)RNN還有很多更有效的擴(kuò)展柜砾,應(yīng)用廣泛的也是在其基礎(chǔ)上發(fā)展起來的網(wǎng)絡(luò),下面看一下基于RNN的一些擴(kuò)展拷橘。
3.LSTM和GRU
為了解決長(zhǎng)期依賴的問題局义,對(duì)RNN進(jìn)行改進(jìn)提出了LSTM(Long Short-Term Memory喜爷,長(zhǎng)的短期記憶網(wǎng)絡(luò))冗疮,從字面意思上看它是短期的記憶,只是比較長(zhǎng)的短期記憶檩帐,我們需要上下文的依賴信息术幔,但是不希望這些依賴信息過長(zhǎng),所以叫長(zhǎng)的短期記憶網(wǎng)絡(luò)湃密。
LSTM通過設(shè)計(jì)門限結(jié)構(gòu)解決長(zhǎng)期依賴問題诅挑,在標(biāo)準(zhǔn)RNN的基礎(chǔ)上增加了四個(gè)神經(jīng)網(wǎng)絡(luò)層,使得LSTM網(wǎng)絡(luò)包括四個(gè)輸入:當(dāng)前時(shí)刻的輸入信息泛源、遺忘門拔妥、輸入門、輸出門和一個(gè)輸出(當(dāng)前時(shí)刻網(wǎng)絡(luò)的輸出)达箍。各個(gè)門上的激活函數(shù)使用Sigmoid函數(shù)没龙,其輸出在0~1之間,可以定義各個(gè)門是否被打開或打開的程度,賦予了它去除或添加信息的能力硬纤。
下圖是LSTM的結(jié)構(gòu)示意圖解滓。從圖中可以看出有3個(gè)Sigmoid層,從左到右分別是遺忘門(Forget Gate)筝家、輸入門(Input Gate)和輸出門(Output Gate)洼裤。三個(gè)Sigmoid層的輸入都是當(dāng)前時(shí)刻的輸入和上一時(shí)刻的輸出,在LSTM前向傳播的過程中溪王,針對(duì)不同的輸入表現(xiàn)不同的角色腮鞍。下面根據(jù)不同的門限和相應(yīng)的計(jì)算公式詳細(xì)說明一下LSTM的工作原理。
(1)遺忘門:也稱保持門(Keep Gate)莹菱,這是從對(duì)立面說的缕减。遺忘門控制記憶單元里哪些信息舍去(也就是被遺忘),哪些信息被保留芒珠。這些狀態(tài)是神經(jīng)網(wǎng)絡(luò)通過數(shù)據(jù)學(xué)習(xí)得到的桥狡。遺忘門的Sigmoid層輸出0~1,這個(gè)輸出作用于時(shí)刻的記憶單元皱卓,0表示將過去的記憶完全遺忘裹芝,1表示將過去的信息完全保留。遺忘門在整個(gè)結(jié)構(gòu)中的位置和前向傳播的公式如下所示:
(2)輸入門:也叫更新門(Update Gate)或?qū)懭腴T(Write Gate)娜汁∩┮祝總之,輸入門決定更新記憶單元的信息掐禁,包括兩個(gè)部分:一個(gè)是Sigmoid層怜械,一個(gè)是Tanh層;Tanh層的輸入和Sigmoid一樣都是當(dāng)前時(shí)刻的輸入和上一時(shí)刻的輸出傅事,Tanh層從新的輸入和網(wǎng)絡(luò)原有的記憶信息決定要被寫入新的神經(jīng)網(wǎng)絡(luò)狀態(tài)中的候選值缕允,而Sigmoid層決定這些候選值有多少被實(shí)際寫入,要寫入的記憶單元信息只有輸入門打開才能真正地把值寫入蹭越,其狀態(tài)也是神經(jīng)網(wǎng)絡(luò)自己學(xué)習(xí)到的障本。輸入門在整個(gè)結(jié)構(gòu)中的位置和前向傳播公式如下所示:
目前為止已經(jīng)有了遺忘門和輸入門,下一步就可以更新神經(jīng)元狀態(tài)响鹃,也就是神經(jīng)網(wǎng)絡(luò)記憶單元的值了驾霜。前面兩個(gè)步驟已經(jīng)準(zhǔn)備好了要更新的值,下面就是怎么更新了买置。從公式看粪糙,當(dāng)前時(shí)刻的神經(jīng)元狀態(tài)是兩部分的和:一部分是計(jì)算通過遺忘門后剩余的信息,即上一時(shí)刻的神經(jīng)元狀態(tài)與的乘積忿项;另一部分是從輸入中獲取的新信息蓉冈,即與的乘積脆栋,得出實(shí)際要輸出到神經(jīng)元狀態(tài)的信息。其中是時(shí)刻新的輸入和上一時(shí)刻神經(jīng)網(wǎng)絡(luò)隱含層輸出總和后的候選值洒擦,如下圖所示椿争。
(3)輸出門:輸出門的功能是讀取剛更新過的神經(jīng)網(wǎng)絡(luò)狀態(tài),也就是記憶單元進(jìn)行輸出熟嫩,但是具體哪些信息可以輸出同樣受輸出門的控制秦踪,通過Sigmoid層實(shí)現(xiàn),產(chǎn)生范圍(0,1)之間的值掸茅。網(wǎng)絡(luò)隱含層狀態(tài)通過一個(gè)Tanh層椅邓,對(duì)記憶單元中的信息產(chǎn)生候選輸出,范圍是(-1,1)昧狮,然后與輸出門相乘得出實(shí)際要輸出的值景馁。輸出門在整個(gè)結(jié)構(gòu)中的位置和前向傳播公式如下圖所示。
LSTM由于有效地解決了標(biāo)準(zhǔn)RNN的長(zhǎng)期依賴問題逗鸣,所以應(yīng)用很廣泛合住,目前我們所說的RNNs大多都是指的LSTM或者基于LSTM的變體。
從上面看LSTM有復(fù)雜的結(jié)構(gòu)和前向傳播公式撒璧,不過在實(shí)際應(yīng)用中PyTorch有LSTM的封裝透葛,程序中使用的時(shí)候只需要給定需要的參數(shù)就可以了。PyTorch中LSTM的定義:
torch.nn.LSTM(*args, **kwargs)
可接受的參數(shù)如下:
- input_size:輸入信息的特征數(shù)
- hidden_size:隱含層狀態(tài)h的特征數(shù)
- num_layers:循環(huán)層數(shù)
- bias:默認(rèn)為True卿樱;如果設(shè)置成False僚害,不使用偏置項(xiàng)b_ih和b_hh。
- batch_first:如果設(shè)置成True繁调,輸入和輸出的Tensor應(yīng)該為(batch萨蚕,seq,feature)的順序蹄胰。
- dropout:如果非零岳遥,在除輸出層外的其他網(wǎng)絡(luò)層添加Dropout層。
- bidirectional:如果設(shè)置成True烤送,變成雙向的LSTM寒随,默認(rèn)為False性锭。
下面的程序片段是一個(gè)簡(jiǎn)單的:LSTM的例子顽悼,定義LSTM的網(wǎng)絡(luò)結(jié)構(gòu)萤晴,輸入大小為10,隱含層為20试和,2個(gè)循環(huán)層(注意不是時(shí)序展開的層),輸入的信息是input纫普,隱含層狀態(tài)為h阅悍,記憶單元狀態(tài)為e好渠,輸出是最后一層的輸出層特征的Tensor,隱含層狀態(tài):
rnn = nn.LSTM(10,20,2)
input = Variable(torch.randn(5,3,10))
h0 = Variable(torch.randn(3,20))
c0 = Variable(torch.randn(3,20))
output,hn = rnn(input,(h0,c0))
PyTorch中還有一個(gè)LSTMCell定義如下节视,參數(shù)含義和LSTM一樣:
class torch.nn.LSTMCell(input_size,hidden_size,bias=True)
LSTM的實(shí)現(xiàn)內(nèi)部調(diào)用了LSTMCell拳锚。LSTMCell是LSTM的內(nèi)部執(zhí)行一個(gè)時(shí)序步驟,從例子可以看出:
rnn = LSTMCell(10,20)
input = Variable(torch.randn(6,3,10))
hx = Variable(torch.randn(3,20))
cx = Variable(torch.randn(3,20))
output = []
for i in range(6):
hx,cx = rnn(input[i],(hx,cx))
output.append(hx)
LSTM的變體有很多寻行,一個(gè)很有名的變體是GRU(Gated Recurrent Unit)霍掺,它在保證LSTM效果的情況下,將遺忘門和輸入門整合成一個(gè)更新門拌蜘,同樣還將單元狀態(tài)和隱藏狀態(tài)合并杆烁,并做出一些其他改變。因?yàn)镚RU比標(biāo)準(zhǔn)的LSTM少了一個(gè)門限層简卧,使得其訓(xùn)練速度更快兔魂,更方便構(gòu)建更復(fù)雜的網(wǎng)絡(luò)。GRU的結(jié)構(gòu)圖和前向計(jì)算公式如下圖所示:
PyTorch中GRU的定義:
class torch.nn.GRU(*args, **kwargs)
GRU的簡(jiǎn)單示例:
rnn = nn.GRU(10,20,2)
input = Variable(torch.randn(5,3,10))
h0 = Variable(torch.randn(2,3,20))
output,hn = rnn(input,h0)
4.LSTM在自然語言處理中的應(yīng)用
上面介紹了LSTM的由來和各個(gè)部分的功能举娩,因?yàn)樯瞄L(zhǎng)處理序列數(shù)據(jù)析校,并能夠解決訓(xùn)練中長(zhǎng)依賴問題,LSTM在NLP中有著廣泛的應(yīng)用铜涉。下面介紹LSTM在NLP中的一些常見應(yīng)用場(chǎng)景勺良。
1.詞性標(biāo)注
詞性標(biāo)注(Past-of-Speach Tagging,POS Tagging)是自然語言處理中最基本的任務(wù)骄噪,對(duì)給定的句子做每個(gè)詞的詞性標(biāo)識(shí)尚困,是作為其他NLP任務(wù)的基礎(chǔ)。這里介紹在PyTorch中使用LSTM進(jìn)行POS Tagging任務(wù)链蕊。
把輸入句子表示成事甜,其中,是詞匯表滔韵,為所有詞性標(biāo)簽集合逻谦,用是表示的詞性,我們要預(yù)測(cè)的是的詞性陪蜻。模型的輸出是邦马,其中。把句子傳入LSTM做預(yù)測(cè)宴卖,時(shí)刻的隱含層狀態(tài)用表示滋将。每個(gè)詞性有唯一的編號(hào),預(yù)測(cè)的前向傳播公式:
在隱含層狀態(tài)上作用一個(gè)仿射函數(shù)log Softmax症昏,最終的詞性預(yù)測(cè)結(jié)果是輸出向量中最大的值随闽,目標(biāo)空間A的大小為。
數(shù)據(jù)的準(zhǔn)備過程:
# 輸入數(shù)據(jù)封裝成Variable
def prepare_sequence(seq,to_idx):
idxs = [to_idx[w] for w in seq]
tensor = torch.LongTensor(idxs)
return autograd.Variable(tensor)
# 輸入數(shù)據(jù)格式肝谭,單個(gè)的詞和對(duì)應(yīng)的詞性
training_data = [("The dog ate the appple".split(),["DET","NN","V","DET","NN"]),
("Everybody read that book".split(),["NN","V","DET","NN"])]
word_to_idx = {}
for sent,tags in training_data:
for word in sent:
if word not in word_to_idx:
word_to_idx[word] = len(word_to_idx)
print(word_to_idx)
# 詞性編碼
tag_to_idx = {"DET":0,"NN":1,"V":2}
# 一般使用32或者64維掘宪,這里為了便于觀察程序運(yùn)行中權(quán)重的變化蛾扇,使用小的維度
EMBEDDING_DIM = 6
HIDDEN_DIM = 6
模型的定義:
class LSTMTagger(nn.Module):
def __init__(self,embedding_dim,hidden_dim,vocab_size,tagset_size):
super(LSTMTagger,self).__init__()
self.hidden_dim = hidden_dim
# 詞嵌入,給定詞表大小和期望的輸出維度
self.word_embeddings = nn.Embedding(vocab_size,embedding_dim)
# 使用詞嵌入作為輸入魏滚,輸出為隱含層狀態(tài)镀首,大小為hidden_dim
self.lstm = nn.LSTM(embedding_dim,hidden_dim)
# 線性層把隱含層狀態(tài)空間映射到詞性空間
self.hidden2tag = nn.Linear(hidden_dim,tagset_size)
self.hidden = self.init_hidden()
# 初始化隱含層狀態(tài)
def init_hidden(self):
return (autograd.Variable(torch.zeros(1,1,self.hidden_size)),
autograd.Variable(torch.zeros(1,1,self.hidden_size)))
# 前向傳播
def forward(self,sentence):
embeds = self.word_embeddings(sentence)
lstm_out,self.hidden = self.lstm(embeds.view(len(sentence),1,-1),self.hidden)
tag_space = self.hidden2tag(lstm_out.view(len(sentence),-1))
tag_scores = F.log_softmax(tag_space,dim=1)
return tag_scores
具體的訓(xùn)練過程可以參考PyTorch官網(wǎng)教程,這里特別要指出的是鼠次,這里的POS Tagging任務(wù)使用的損失函數(shù)是負(fù)對(duì)數(shù)似然函數(shù)更哄,優(yōu)化器使用SGD,學(xué)習(xí)率為0.1:
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(),lr=0.1)
2.情感分析
本小節(jié)介紹一下LSTM在NLP中另外一個(gè)領(lǐng)域的應(yīng)用:情感分析须眷。Bjarke Felbo在論文中提到了一個(gè)情感分析的任務(wù)Deepmoji竖瘾,利用表情符號(hào)訓(xùn)練了12億條推文,用以了解語言是如何表達(dá)情感花颗。通過神經(jīng)網(wǎng)絡(luò)的學(xué)習(xí)捕传,模型可以在許多情感相關(guān)的文本建模任務(wù)中獲得最先進(jìn)的性能。TorchMoji是論文中提出的情感分析的PyTorch實(shí)現(xiàn)扩劝。模型包含兩個(gè)雙LSTM層庸论,在LSTM后面鏈接一個(gè)Attention層分類器,模型的結(jié)構(gòu)如下圖所示棒呛。
Deepmoji可以對(duì)輸入的句子進(jìn)行情感方面的分析并生成相應(yīng)的moji表情聂示,如下圖所示。比如輸入“What is happening to me ??”和“What a good day !”會(huì)輸出不同的表情簇秒,并給出輸出的置信度鱼喉。具體代碼詳見GitHub。
5.序列到序列網(wǎng)絡(luò)
1.序列到序列原理
序列到序列網(wǎng)絡(luò)(Seq2seqNetwork)趋观,也稱為編碼解碼網(wǎng)絡(luò)(Encoder Decoder Netword)扛禽,由兩個(gè)獨(dú)立的循環(huán)神經(jīng)網(wǎng)絡(luò)組成,被稱為編碼器(Encoder)和解碼器(Decoder)皱坛,通常使用LSTM或者GRU來實(shí)現(xiàn)编曼。編碼器處理輸入數(shù)據(jù),其目標(biāo)是理解輸入信息并表示在編碼器的最終狀態(tài)中剩辟。解碼器從編碼器的最終狀態(tài)開始掐场,逐詞生成目標(biāo)輸出的序列,解碼器在每個(gè)時(shí)刻的輸入為上一時(shí)刻的輸出贩猎,整體過程如下圖所示熊户。
串到串最常見的場(chǎng)景就是機(jī)器翻譯,把輸入串分詞并表示成詞向量融欧,每個(gè)時(shí)刻一個(gè)詞語輸入到編碼網(wǎng)絡(luò)中敏弃,并利用EOS(End of Sentence)作為句子末尾的標(biāo)記。句子輸入完成我們得到一個(gè)編碼器噪馏,這時(shí)可以用編碼器的隱含層狀態(tài)來初始化解碼器麦到,輸入到解碼器的第一個(gè)詞是SOS(Start of Sentence),作為目標(biāo)語言的起始標(biāo)識(shí)欠肾,得到的輸出是目標(biāo)語言的第一個(gè)詞瓶颠,隨后將該時(shí)刻的輸出作為解碼器下一時(shí)刻的輸入。重復(fù)這個(gè)過程直到解碼器的輸出產(chǎn)生一個(gè)EOS刺桃,目標(biāo)語言結(jié)束的標(biāo)識(shí)粹淋,這時(shí)就完成了從源語言到目標(biāo)語言的翻譯。后面有具體的例子瑟慈。
2.注意力機(jī)制
從人工翻譯句子的經(jīng)驗(yàn)中可以得到很多啟發(fā)桃移,從而改善我們提到的串到串模型。人工翻譯句子的時(shí)候葛碧,首先閱讀整個(gè)句子理解要表達(dá)的意思借杰,然后開始寫出相應(yīng)的翻譯。但是一個(gè)很重要的方面就是在你寫新的句子的時(shí)候进泼,通常會(huì)重新回到源語言的文本蔗衡,特別注意你目前正在翻譯的那部分在源語言中的表達(dá),以確定最好的翻譯結(jié)果乳绕。而我們前面提到的串到串的模型中绞惦,編碼器一次讀入所有的輸入并總結(jié)到句子的意思保存到編碼器的隱含層狀態(tài),這個(gè)過程像人工翻譯的第一部分洋措,而通過解碼器得到最終的翻譯結(jié)果济蝉,解碼器處理的是翻譯的第二個(gè)部分。但是“特別注意”的部分在我們的串到串模型中還沒有體現(xiàn)菠发,這也是需要完成的部分王滤。
為了在串到串模型中添加注意力機(jī)制,解碼器在產(chǎn)生時(shí)刻的輸出時(shí)雷酪,讓解碼器訪問所有從編碼器的輸出淑仆,這樣解碼器可以觀察源語言的句子,這個(gè)過程是之前沒有的哥力。但是在所有時(shí)間步都考慮編碼器的所有輸出蔗怠,這和人工翻譯的過程還是不同的,人工翻譯對(duì)于不同的部分吩跋,需要關(guān)注源語言中特定的很小的部分寞射。所以,直接讓解碼器訪問所有編碼器的輸出是不符合實(shí)際的锌钮。我們需要對(duì)這個(gè)過程進(jìn)行改進(jìn)桥温,讓解碼器工作的時(shí)候可以動(dòng)態(tài)地注意編碼器輸出的特定的部分。有研究者提出的解決方案是把輸入變成是串聯(lián)操作梁丘,在編碼器的輸出上使用一個(gè)帶權(quán)重侵浸,也就是編碼器在時(shí)刻的狀態(tài)旺韭,而不是直接使用其輸出。具體做法是掏觉,首先為編碼器的每個(gè)輸出關(guān)聯(lián)一個(gè)分?jǐn)?shù)区端,這個(gè)分?jǐn)?shù)由解碼器時(shí)刻的網(wǎng)絡(luò)狀態(tài)和每個(gè)編碼器的輸出的點(diǎn)乘得到,然后用Softmax層對(duì)這些分?jǐn)?shù)進(jìn)行歸一化澳腹。最后在加入到串聯(lián)操作之前织盼,利用歸一化后的分?jǐn)?shù)分別度量編碼器的輸出。這個(gè)策略的關(guān)鍵點(diǎn)是酱塔,編碼器的每個(gè)輸出計(jì)算得到的關(guān)聯(lián)分?jǐn)?shù)沥邻,表示了每個(gè)編碼器的輸出對(duì)解碼器時(shí)刻決策的重要程度。
注意力機(jī)制提出后受到了廣泛關(guān)注羊娃,并在語音識(shí)別唐全、圖像描述等應(yīng)用上有很好的效果。
6.PyTorch示例:基于GRU和Attention的機(jī)器翻譯
1.公共模塊(logger.py)
這里提到的公共模塊主要是日志處理模塊迁沫。在數(shù)據(jù)處理芦瘾、模型訓(xùn)練等過程中,需要保留必要的日志信息集畅,這樣可以對(duì)程序的運(yùn)行過程近弟、運(yùn)行結(jié)果進(jìn)行記錄和分析。這里記錄日志的方式是同時(shí)輸出到文件和控制臺(tái)挺智。
import logging as logger
logger.basicConfig(level=logger.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S -',
filename='log.txt',
filemode='a') # or 'w', default 'a'
console = logger.StreamHandler()
console.setLevel(logger.INFO)
formatter = logger.Formatter('%(asctime)s %(name)-6s: %(levelname)-6s %(message)s')
console.setFormatter(formatter)
logger.getLogger('').addHandler(console)
2.數(shù)據(jù)處理模塊(process.py)
數(shù)據(jù)處理模塊主要定義模型訓(xùn)練需要的一些數(shù)據(jù)處理祷愉,包括從文件加載數(shù)據(jù),數(shù)據(jù)解析赦颇,和一些輔助函數(shù)二鳄。
from __future__ import unicode_literals, print_function, division
import math
import re
import time
import jieba
import torch
import unicodedata
from torch.autograd import Variable
from logger import logger
use_cuda = torch.cuda.is_available()
SOS_token = 0
EOS_token = 1
# 中文的時(shí)候要設(shè)置大一些
MAX_LENGTH = 25
def unicodeToAscii(s):
'''
Unicode轉(zhuǎn)換成ASCII,http://stackoverflow.com/a/518232/2809427
:param s:
:return:
'''
return ''.join(
c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn'
)
def normalizeString(s):
'''
轉(zhuǎn)小寫媒怯,去除非法字符
:param s:
:return:
'''
s = unicodeToAscii(s.lower().strip())
s = re.sub(r"([.!?])", r" \1", s)
# 中文不能進(jìn)行下面的處理
# s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
return s
class Lang:
def __init__(self, name):
'''
添加 need_cut 可根據(jù)語種進(jìn)行不同的分詞邏輯處理
:param name: 語種名稱
'''
self.name = name
self.need_cut = self.name == 'cmn'
self.word2index = {}
self.word2count = {}
self.index2word = {0: "SOS", 1: "EOS"}
self.n_words = 2 # 初始化詞數(shù)為2:SOS & EOS
def addSentence(self, sentence):
'''
從語料中添加句子到 Lang
:param sentence: 語料中的每個(gè)句子
'''
if self.need_cut:
sentence = cut(sentence)
for word in sentence.split(' '):
if len(word) > 0:
self.addWord(word)
def addWord(self, word):
'''
向 Lang 中添加每個(gè)詞订讼,并統(tǒng)計(jì)詞頻,如果是新詞修改詞表大小
:param word:
'''
if word not in self.word2index:
self.word2index[word] = self.n_words
self.word2count[word] = 1
self.index2word[self.n_words] = word
self.n_words += 1
else:
self.word2count[word] += 1
def cut(sentence, use_jieba=False):
'''
對(duì)句子分詞扇苞。
:param sentence: 要分詞的句子
:param use_jieba: 是否使用 jieba 進(jìn)行智能分詞欺殿,默認(rèn)按單字切分
:return: 分詞結(jié)果,空格區(qū)分
'''
if use_jieba:
return ' '.join(jieba.cut(sentence))
else:
words = [word for word in sentence]
return ' '.join(words)
import jieba.posseg as pseg
def tag(sentence):
words = pseg.cut(sentence)
result = ''
for w in words:
result = result + w.word + "/" + w.flag + " "
return result
def readLangs(lang1, lang2, reverse=False):
'''
:param lang1: 源語言
:param lang2: 目標(biāo)語言
:param reverse: 是否逆向翻譯
:return: 源語言實(shí)例鳖敷,目標(biāo)語言實(shí)例脖苏,詞語對(duì)
'''
logger.info("Reading lines...")
# 讀取txt文件并分割成行
lines = open('data/%s-%s.txt' % (lang1, lang2), encoding='utf-8'). \
read().strip().split('\n')
# 按行處理成 源語言-目標(biāo)語言對(duì),并做預(yù)處理
pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]
# Reverse pairs, make Lang instances
if reverse:
pairs = [list(reversed(p)) for p in pairs]
input_lang = Lang(lang2)
output_lang = Lang(lang1)
else:
input_lang = Lang(lang1)
output_lang = Lang(lang2)
return input_lang, output_lang, pairs
eng_prefixes = (
"i am ", "i m ",
"he is", "he s ",
"she is", "she s",
"you are", "you re ",
"we are", "we re ",
"they are", "they re "
)
def filterPair(p):
'''
按自定義最大長(zhǎng)度過濾
'''
return len(p[0].split(' ')) < MAX_LENGTH and \
len(p[1].split(' ')) < MAX_LENGTH and \
p[1].startswith(eng_prefixes)
def filterPairs(pairs):
return [pair for pair in pairs if filterPair(pair)]
def prepareData(lang1, lang2, reverse=False):
input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)
logger.info("Read %s sentence pairs" % len(pairs))
pairs = filterPairs(pairs)
logger.info("Trimmed to %s sentence pairs" % len(pairs))
logger.info("Counting words...")
for pair in pairs:
input_lang.addSentence(pair[0])
output_lang.addSentence(pair[1])
logger.info("Counted words:")
logger.info('%s, %d' % (input_lang.name, input_lang.n_words))
logger.info('%s, %d' % (output_lang.name, output_lang.n_words))
return input_lang, output_lang, pairs
def indexesFromSentence(lang, sentence):
'''
:param lang:
:param sentence:
:return:
'''
return [lang.word2index[word] for word in sentence.split(' ') if len(word) > 0]
def variableFromSentence(lang, sentence):
if lang.need_cut:
sentence = cut(sentence)
# logger.info("cuted sentence: %s" % sentence)
indexes = indexesFromSentence(lang, sentence)
indexes.append(EOS_token)
result = Variable(torch.LongTensor(indexes).view(-1, 1))
if use_cuda:
return result.cuda()
else:
return result
def variablesFromPair(input_lang, output_lang, pair):
input_variable = variableFromSentence(input_lang, pair[0])
target_variable = variableFromSentence(output_lang, pair[1])
return (input_variable, target_variable)
def asMinutes(s):
m = math.floor(s / 60)
s -= m * 60
return '%dm %ds' % (m, s)
def timeSince(since, percent):
now = time.time()
s = now - since
es = s / (percent)
rs = es - s
return '%s (- %s)' % (asMinutes(s), asMinutes(rs))
if __name__ == "__main__":
s = 'Fans of Belgium cheer prior to the 2018 FIFA World Cup Group G match between Belgium and Tunisia in Moscow, Russia, June 23, 2018.'
s = '結(jié)婚的和尚未結(jié)婚的和尚'
s = "買張下周三去南海的飛機(jī)票定踱,海航的"
s = "過幾天天天天氣不好棍潘。"
a = cut(s, use_jieba=True)
print(a)
print(tag(s))
3.模型定義(model.py)
這部分主要是循環(huán)神經(jīng)網(wǎng)絡(luò)RNN的定義,包括編碼器和解碼器兩個(gè)RNN。
import torch
from torch import nn
from torch.autograd import Variable
from torch.nn import functional as F
from logger import logger
# from process import cut
from process import MAX_LENGTH
use_cuda = torch.cuda.is_available()
class EncoderRNN(nn.Module):
'''
編碼器的定義
'''
def __init__(self, input_size, hidden_size, n_layers=1):
'''
初始化過程
:param input_size: 輸入向量長(zhǎng)度亦歉,這里是詞匯表大小
:param hidden_size: 隱藏層大小
:param n_layers: 疊加層數(shù)
'''
super(EncoderRNN, self).__init__()
self.n_layers = n_layers
self.hidden_size = hidden_size
self.embedding = nn.Embedding(input_size, hidden_size)
self.gru = nn.GRU(hidden_size, hidden_size)
def forward(self, input, hidden):
'''
前向計(jì)算過程
:param input: 輸入
:param hidden: 隱藏層狀態(tài)
:return: 編碼器輸出恤浪,隱藏層狀態(tài)
'''
try:
embedded = self.embedding(input).view(1, 1, -1)
output = embedded
for i in range(self.n_layers):
output, hidden = self.gru(output, hidden)
return output, hidden
except Exception as err:
logger.error(err)
def initHidden(self):
'''
隱藏層狀態(tài)初始化
:return: 初始化過的隱藏層狀態(tài)
'''
result = Variable(torch.zeros(1, 1, self.hidden_size))
if use_cuda:
return result.cuda()
else:
return result
class DecoderRNN(nn.Module):
'''
解碼器定義
'''
def __init__(self, hidden_size, output_size, n_layers=1):
'''
初始化過程
:param hidden_size: 隱藏層大小
:param output_size: 輸出大小
:param n_layers: 疊加層數(shù)
'''
super(DecoderRNN, self).__init__()
self.n_layers = n_layers
self.hidden_size = hidden_size
self.embedding = nn.Embedding(output_size, hidden_size)
self.gru = nn.GRU(hidden_size, hidden_size)
self.out = nn.Linear(hidden_size, output_size)
self.softmax = nn.LogSoftmax()
def forward(self, input, hidden):
'''
前向計(jì)算過程
:param input: 輸入信息
:param hidden: 隱藏層狀態(tài)
:return: 解碼器輸出,隱藏層狀態(tài)
'''
try:
output = self.embedding(input).view(1, 1, -1)
for i in range(self.n_layers):
output = F.relu(output)
output, hidden = self.gru(output, hidden)
output = self.softmax(self.out(output[0]))
return output, hidden
except Exception as err:
logger.error(err)
def initHidden(self):
'''
隱藏層狀態(tài)初始化
:return: 初始化過的隱藏層狀態(tài)
'''
result = Variable(torch.zeros(1, 1, self.hidden_size))
if use_cuda:
return result.cuda()
else:
return result
class AttnDecoderRNN(nn.Module):
'''
帶注意力的解碼器的定義
'''
def __init__(self, hidden_size, output_size, n_layers=1, dropout_p=0.1, max_length=MAX_LENGTH):
'''
帶注意力的解碼器初始化過程
:param hidden_size: 隱藏層大小
:param output_size: 輸出大小
:param n_layers: 疊加層數(shù)
:param dropout_p: dropout率定義
:param max_length: 接受的最大句子長(zhǎng)度
'''
super(AttnDecoderRNN, self).__init__()
self.hidden_size = hidden_size
self.output_size = output_size
self.n_layers = n_layers
self.dropout_p = dropout_p
self.max_length = max_length
self.embedding = nn.Embedding(self.output_size, self.hidden_size)
self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
self.dropout = nn.Dropout(self.dropout_p)
self.gru = nn.GRU(self.hidden_size, self.hidden_size)
self.out = nn.Linear(self.hidden_size, self.output_size)
def forward(self, input, hidden, encoder_output, encoder_outputs):
'''
前向計(jì)算過程
:param input: 輸入信息
:param hidden: 隱藏層狀態(tài)
:param encoder_output: 編碼器分時(shí)刻的輸出
:param encoder_outputs: 編碼器全部輸出
:return: 解碼器輸出鳍徽,隱藏層狀態(tài)资锰,注意力權(quán)重
'''
try:
embedded = self.embedding(input).view(1, 1, -1)
embedded = self.dropout(embedded)
attn_weights = F.softmax(
self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
attn_applied = torch.bmm(attn_weights.unsqueeze(0),
encoder_outputs.unsqueeze(0))
output = torch.cat((embedded[0], attn_applied[0]), 1)
output = self.attn_combine(output).unsqueeze(0)
for i in range(self.n_layers):
output = F.relu(output)
output, hidden = self.gru(output, hidden)
output = F.log_softmax(self.out(output[0]), dim=1)
return output, hidden, attn_weights
except Exception as err:
logger.error(err)
def initHidden(self):
'''
隱藏層狀態(tài)初始化
:return: 初始化過的隱藏層狀態(tài)
'''
result = Variable(torch.zeros(1, 1, self.hidden_size))
if use_cuda:
return result.cuda()
else:
return result
4.訓(xùn)練模塊(train.py)
訓(xùn)練模塊包括訓(xùn)練過程的定義和評(píng)估方法的定義敢课。
import sys
import random
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from torch import nn
from torch import optim
from torch.autograd import Variable
from process import *
use_cuda = torch.cuda.is_available()
def evaluate(input_lang, output_lang, encoder, decoder, sentence, max_length=MAX_LENGTH):
'''
單句評(píng)估
:param input_lang: 源語言信息
:param output_lang: 目標(biāo)語言信息
:param encoder: 編碼器
:param decoder: 解碼器
:param sentence: 要評(píng)估的句子
:param max_length: 可接受最大長(zhǎng)度
:return: 翻譯過的句子和注意力信息
'''
# 輸入句子預(yù)處理
input_variable = variableFromSentence(input_lang, sentence)
input_length = input_variable.size()[0]
encoder_hidden = encoder.initHidden()
encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size))
encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs
for ei in range(input_length):
encoder_output, encoder_hidden = encoder(input_variable[ei],
encoder_hidden)
encoder_outputs[ei] = encoder_outputs[ei] + encoder_output[0][0]
decoder_input = Variable(torch.LongTensor([[SOS_token]])) # 起始標(biāo)志 SOS
decoder_input = decoder_input.cuda() if use_cuda else decoder_input
decoder_hidden = encoder_hidden
decoded_words = []
decoder_attentions = torch.zeros(max_length, max_length)
# 翻譯過程
for di in range(max_length):
decoder_output, decoder_hidden, decoder_attention = decoder(
decoder_input, decoder_hidden, encoder_output, encoder_outputs)
decoder_attentions[di] = decoder_attention.data
topv, topi = decoder_output.data.topk(1)
ni = topi[0][0].item()
# 當(dāng)前時(shí)刻輸出為句子結(jié)束標(biāo)志阶祭,則結(jié)束
if ni == EOS_token:
decoded_words.append('<EOS>')
break
else:
decoded_words.append(output_lang.index2word[ni])
decoder_input = Variable(torch.LongTensor([[ni]]))
decoder_input = decoder_input.cuda() if use_cuda else decoder_input
return decoded_words, decoder_attentions[:di + 1]
teacher_forcing_ratio = 0.5
def train(input_variable, target_variable, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion,
max_length=MAX_LENGTH):
'''
單次訓(xùn)練過程,
:param input_variable: 源語言信息
:param target_variable: 目標(biāo)語言信息
:param encoder: 編碼器
:param decoder: 解碼器
:param encoder_optimizer: 編碼器的優(yōu)化器
:param decoder_optimizer: 解碼器的優(yōu)化器
:param criterion: 評(píng)價(jià)準(zhǔn)則直秆,即損失函數(shù)的定義
:param max_length: 接受的單句最大長(zhǎng)度
:return: 本次訓(xùn)練的平均損失
'''
encoder_hidden = encoder.initHidden()
# 清楚優(yōu)化器狀態(tài)
encoder_optimizer.zero_grad()
decoder_optimizer.zero_grad()
input_length = input_variable.size()[0]
target_length = target_variable.size()[0]
# print(input_length, " -> ", target_length)
encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size))
encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs
# print("encoder_outputs shape ", encoder_outputs.shape)
loss = 0
# 編碼過程
for ei in range(input_length):
encoder_output, encoder_hidden = encoder(
input_variable[ei], encoder_hidden)
encoder_outputs[ei] = encoder_output[0][0]
decoder_input = Variable(torch.LongTensor([[SOS_token]]))
decoder_input = decoder_input.cuda() if use_cuda else decoder_input
decoder_hidden = encoder_hidden
use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
if use_teacher_forcing:
# Teacher forcing: 以目標(biāo)作為下一個(gè)輸入
for di in range(target_length):
decoder_output, decoder_hidden, decoder_attention = decoder(
decoder_input, decoder_hidden, encoder_output, encoder_outputs)
loss += criterion(decoder_output, target_variable[di])
decoder_input = target_variable[di] # Teacher forcing
else:
# Without teacher forcing: 網(wǎng)絡(luò)自己預(yù)測(cè)的輸出為下一個(gè)輸入
for di in range(target_length):
decoder_output, decoder_hidden, decoder_attention = decoder(
decoder_input, decoder_hidden, encoder_output, encoder_outputs)
topv, topi = decoder_output.data.topk(1)
ni = topi[0][0]
decoder_input = Variable(torch.LongTensor([[ni]]))
decoder_input = decoder_input.cuda() if use_cuda else decoder_input
loss += criterion(decoder_output, target_variable[di])
if ni == EOS_token:
break
# 反向傳播
loss.backward()
# 網(wǎng)絡(luò)狀態(tài)更新
encoder_optimizer.step()
decoder_optimizer.step()
return loss / target_length
def showPlot(points):
'''
繪制圖像
:param points:
:return:
'''
plt.figure()
fig, ax = plt.subplots()
# this locator puts ticks at regular intervals
loc = ticker.MultipleLocator(base=0.2)
ax.yaxis.set_major_locator(loc)
plt.plot(points)
def trainIters(input_lang, output_lang, pairs, encoder, decoder, n_iters, print_every=1000, plot_every=100,
learning_rate=0.01):
'''
訓(xùn)練過程,可以指定迭代次數(shù)濒募,每次迭代調(diào)用 前面定義的train函數(shù),并在迭代結(jié)束調(diào)用繪制圖像的函數(shù)
:param input_lang: 輸入語言實(shí)例
:param output_lang: 輸出語言實(shí)例
:param pairs: 語料中的源語言-目標(biāo)語言對(duì)
:param encoder: 編碼器
:param decoder: 解碼器
:param n_iters: 迭代次數(shù)
:param print_every: 打印loss間隔
:param plot_every: 繪制圖像間隔
:param learning_rate: 學(xué)習(xí)率
:return:
'''
start = time.time()
plot_losses = []
print_loss_total = 0 # Reset every print_every
plot_loss_total = 0 # Reset every plot_every
encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
training_pairs = [variablesFromPair(input_lang, output_lang, random.choice(pairs))
for i in range(n_iters)]
# 損失函數(shù)定義
criterion = nn.NLLLoss()
for iter in range(1, n_iters + 1):
training_pair = training_pairs[iter - 1]
input_variable = training_pair[0]
target_variable = training_pair[1]
loss = train(input_variable, target_variable, encoder,
decoder, encoder_optimizer, decoder_optimizer, criterion)
print_loss_total += loss
plot_loss_total += loss
if iter % print_every == 0:
print_loss_avg = print_loss_total / print_every
print_loss_total = 0
logger.info('%s (%d %d%%) %.4f' % (timeSince(start, iter / n_iters),
iter, iter / n_iters * 100, print_loss_avg))
if iter % plot_every == 0:
plot_loss_avg = plot_loss_total / plot_every
plot_losses.append(plot_loss_avg)
plot_loss_total = 0
showPlot(plot_losses)
def evaluateRandomly(input_lang, output_lang, pairs, encoder, decoder, n=10):
'''
從語料中隨機(jī)選取句子進(jìn)行評(píng)估
'''
for i in range(n):
pair = random.choice(pairs)
logger.info('> %s' % pair[0])
logger.info('= %s' % pair[1])
output_words, attentions = evaluate(input_lang, output_lang, encoder, decoder, pair[0])
output_sentence = ' '.join(output_words)
logger.info('< %s' % output_sentence)
logger.info('')
def showAttention(input_sentence, output_words, attentions):
try:
# 添加繪圖中的中文顯示
plt.rcParams['font.sans-serif'] = ['STSong'] # 宋體
plt.rcParams['axes.unicode_minus'] = False # 用來正常顯示負(fù)號(hào)
# 使用 colorbar 初始化繪圖
fig = plt.figure()
ax = fig.add_subplot(111)
cax = ax.matshow(attentions.numpy(), cmap='bone')
fig.colorbar(cax)
# 設(shè)置x圾结,y軸信息
ax.set_xticklabels([''] + input_sentence.split(' ') +
['<EOS>'], rotation=90)
ax.set_yticklabels([''] + output_words)
# 顯示標(biāo)簽
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
plt.show()
except Exception as err:
logger.error(err)
def evaluateAndShowAtten(input_lang, ouput_lang, input_sentence, encoder1, attn_decoder1):
output_words, attentions = evaluate(input_lang, ouput_lang,
encoder1, attn_decoder1, input_sentence)
logger.info('input = %s' % input_sentence)
logger.info('output = %s' % ' '.join(output_words))
# 如果是中文需要分詞
if input_lang.name == 'cmn':
print(input_lang.name)
input_sentence = cut(input_sentence)
showAttention(input_sentence, output_words, attentions)
5.訓(xùn)練過程(seq2seq.py)
該模塊主要是整個(gè)訓(xùn)練過程瑰剃,調(diào)用已經(jīng)定義好的訓(xùn)練方法,完成整個(gè)預(yù)料上的訓(xùn)練筝野,并把相應(yīng)模型保存到文件晌姚,以方便隨時(shí)評(píng)估和模型調(diào)用,這樣不用每次都重新執(zhí)行訓(xùn)練過程(因?yàn)閺南旅娼o出的訓(xùn)練結(jié)果可以看出這個(gè)過程很漫長(zhǎng))歇竟。
import pickle
import sys
from io import open
from model import AttnDecoderRNN
from model import EncoderRNN
from train import *
use_cuda = torch.cuda.is_available()
logger.info("Use cuda:{}".format(use_cuda))
input = 'eng'
output = 'cmn'
# 從參數(shù)接收要翻譯的語種名詞
if len(sys.argv) > 1:
output = sys.argv[1]
logger.info('%s -> %s' % (input, output))
# 處理語料庫
input_lang, output_lang, pairs = prepareData(input, output, True)
logger.info(random.choice(pairs))
# 查看兩種語言的詞匯大小情況
logger.info('input_lang.n_words: %d' % input_lang.n_words)
logger.info('output_lang.n_words: %d' % output_lang.n_words)
# 保存處理過的語言信息挥唠,評(píng)估時(shí)加載使用
pickle.dump(input_lang, open('./data/%s_%s_input_lang.pkl' % (input, output), "wb"))
pickle.dump(output_lang, open('./data/%s_%s_output_lang.pkl' % (input, output), "wb"))
pickle.dump(pairs, open('./data/%s_%s_pairs.pkl' % (input, output), "wb"))
logger.info('lang saved.')
# 編碼器和解碼器的實(shí)例化
hidden_size = 256
encoder1 = EncoderRNN(input_lang.n_words, hidden_size)
attn_decoder1 = AttnDecoderRNN(hidden_size, output_lang.n_words,
1, dropout_p=0.1)
if use_cuda:
encoder1 = encoder1.cuda()
attn_decoder1 = attn_decoder1.cuda()
logger.info('train start. ')
# 訓(xùn)練過程,指定迭代次數(shù)焕议,此處為迭代100000次宝磨,每1000次打印中間信息
trainIters(input_lang, output_lang, pairs, encoder1, attn_decoder1, 100000, print_every=1000)
logger.info('train end. ')
# 保存編碼器和解碼器網(wǎng)絡(luò)狀態(tài)
torch.save(encoder1.state_dict(), open('./data/%s_%s_encoder1.stat' % (input, output), 'wb'))
torch.save(attn_decoder1.state_dict(), open('./data/%s_%s_attn_decoder1.stat' % (input, output), 'wb'))
logger.info('stat saved.')
# 保存整個(gè)網(wǎng)絡(luò)
torch.save(encoder1, open('./data/%s_%s_encoder1.model' % (input, output), 'wb'))
torch.save(attn_decoder1, open('./data/%s_%s_attn_decoder1.model' % (input, output), 'wb'))
logger.info('model saved.')
訓(xùn)練結(jié)果如下:
C:\ProgramData\Anaconda3\python.exe E:/workspace/python/chapter7/seq2seq.py
2019-09-01 23:18:50,189 root : INFO Use cuda:True
2019-09-01 23:18:50,190 root : INFO eng -> cmn
2019-09-01 23:18:50,190 root : INFO Reading lines...
2019-09-01 23:18:50,470 root : INFO Read 19578 sentence pairs
2019-09-01 23:18:50,487 root : INFO Trimmed to 695 sentence pairs
2019-09-01 23:18:50,487 root : INFO Counting words...
2019-09-01 23:18:50,492 root : INFO Counted words:
2019-09-01 23:18:50,492 root : INFO cmn, 994
2019-09-01 23:18:50,492 root : INFO eng, 887
2019-09-01 23:18:50,492 root : INFO ['他在生你的氣。', 'he is angry with you .']
2019-09-01 23:18:50,492 root : INFO input_lang.n_words: 994
2019-09-01 23:18:50,492 root : INFO output_lang.n_words: 887
2019-09-01 23:18:50,494 root : INFO lang saved.
2019-09-01 23:18:53,528 root : INFO train start.
2019-09-01 23:19:59,536 root : INFO 1m 6s (- 108m 54s) (1000 1%) 3.4915
2019-09-01 23:20:49,542 root : INFO 1m 56s (- 94m 44s) (2000 2%) 3.1642
2019-09-01 23:21:40,365 root : INFO 2m 46s (- 89m 54s) (3000 3%) 2.8599
2019-09-01 23:22:31,133 root : INFO 3m 37s (- 87m 2s) (4000 4%) 2.5942
2019-09-01 23:23:22,415 root : INFO 4m 28s (- 85m 8s) (5000 5%) 2.2696
2019-09-01 23:24:13,565 root : INFO 5m 20s (- 83m 33s) (6000 6%) 1.9124
2019-09-01 23:25:05,176 root : INFO 6m 11s (- 82m 17s) (7000 7%) 1.5661
2019-09-01 23:25:57,465 root : INFO 7m 3s (- 81m 15s) (8000 8%) 1.2604
2019-09-01 23:26:49,536 root : INFO 7m 56s (- 80m 12s) (9000 9%) 0.9532
2019-09-01 23:27:41,903 root : INFO 8m 48s (- 79m 15s) (10000 10%) 0.7092
……
2019-09-02 00:39:19,369 root : INFO 80m 25s (- 7m 57s) (91000 91%) 0.0139
2019-09-02 00:40:12,250 root : INFO 81m 18s (- 7m 4s) (92000 92%) 0.0123
2019-09-02 00:41:04,909 root : INFO 82m 11s (- 6m 11s) (93000 93%) 0.0126
2019-09-02 00:41:57,523 root : INFO 83m 3s (- 5m 18s) (94000 94%) 0.0113
2019-09-02 00:42:50,670 root : INFO 83m 57s (- 4m 25s) (95000 95%) 0.0082
2019-09-02 00:43:43,522 root : INFO 84m 49s (- 3m 32s) (96000 96%) 0.0123
2019-09-02 00:44:35,892 root : INFO 85m 42s (- 2m 39s) (97000 97%) 0.0088
2019-09-02 00:45:28,415 root : INFO 86m 34s (- 1m 46s) (98000 98%) 0.0103
2019-09-02 00:46:20,990 root : INFO 87m 27s (- 0m 53s) (99000 99%) 0.0105
2019-09-02 00:47:13,401 root : INFO 88m 19s (- 0m 0s) (100000 100%) 0.0102
2019-09-02 00:47:13,813 root : INFO train end.
2019-09-02 00:47:13,823 root : INFO stat saved.
2019-09-02 00:47:13,859 root : INFO model saved.
Process finished with exit code 0
6.評(píng)估過程(evaluate_eng_cmn.py)
對(duì)訓(xùn)練好的神經(jīng)網(wǎng)絡(luò)進(jìn)行評(píng)估盅安,可以從語料中隨機(jī)選取句子進(jìn)行翻譯唤锉,也可以指定句子進(jìn)行翻譯,并對(duì)翻譯過程中的注意力進(jìn)行可視化别瞭。
import pickle
import matplotlib.pyplot as plt
import torch
from logger import logger
from train import evaluate
from train import evaluateAndShowAtten
from train import evaluateRandomly
input = 'eng'
output = 'cmn'
logger.info('%s -> %s' % (input, output))
# 加載處理好的語言信息
input_lang = pickle.load(open('./data/%s_%s_input_lang.pkl' % (input, output), "rb"))
output_lang = pickle.load(open('./data/%s_%s_output_lang.pkl' % (input, output), "rb"))
pairs = pickle.load(open('./data/%s_%s_pairs.pkl' % (input, output), 'rb'))
logger.info('lang loaded.')
# 加載訓(xùn)練好的編碼器和解碼器
encoder1 = torch.load(open('./data/%s_%s_encoder1.model' % (input, output), 'rb'))
attn_decoder1 = torch.load(open('./data/%s_%s_attn_decoder1.model' % (input, output), 'rb'))
logger.info('model loaded.')
# 對(duì)單句進(jìn)行評(píng)估并繪制注意力圖像
def evaluateAndShowAttention(sentence):
evaluateAndShowAtten(input_lang, output_lang, sentence, encoder1, attn_decoder1)
evaluateAndShowAttention("他們肯定會(huì)相戀的窿祥。")
evaluateAndShowAttention("我現(xiàn)在正在學(xué)習(xí)。")
# 語料中的數(shù)據(jù)隨機(jī)選擇評(píng)估
evaluateRandomly(input_lang, output_lang, pairs, encoder1, attn_decoder1)
output_words, attentions = evaluate(input_lang, output_lang,
encoder1, attn_decoder1, "我是中國(guó)人蝙寨。")
plt.matshow(attentions.numpy())
日志如下:
C:\ProgramData\Anaconda3\python.exe E:/workspace/python/chapter7/evaluate_cmn_eng.py
2019-09-02 00:49:48,043 root : INFO eng -> cmn
2019-09-02 00:49:48,044 root : INFO lang loaded.
2019-09-02 00:49:50,110 root : INFO model loaded.
2019-09-02 00:49:51,197 root : INFO input = 他們肯定會(huì)相戀的晒衩。
2019-09-02 00:49:51,197 root : INFO output = they are sure to fall in love . <EOS>
cmn
2019-09-02 00:49:51,350 root : INFO input = 我現(xiàn)在正在學(xué)習(xí)。
cmn
2019-09-02 00:49:51,350 root : INFO output = i am studying now . <EOS>
2019-09-02 00:49:51,461 root : INFO > 他可能很快就到了籽慢。
2019-09-02 00:49:51,461 root : INFO = he is likely to arrive soon .
2019-09-02 00:49:51,485 root : INFO < he is likely to arrive soon . <EOS>
2019-09-02 00:49:51,485 root : INFO
2019-09-02 00:49:51,485 root : INFO > 我熟悉這個(gè)主題浸遗。
2019-09-02 00:49:51,485 root : INFO = i am familiar with this subject .
2019-09-02 00:49:51,507 root : INFO < i am familiar with this subject . <EOS>
2019-09-02 00:49:51,507 root : INFO
2019-09-02 00:49:51,507 root : INFO > 他的年紀(jì)可以開車了。
2019-09-02 00:49:51,507 root : INFO = he is old enough to drive a car .
2019-09-02 00:49:51,530 root : INFO < he is old enough to drive a car . <EOS>
2019-09-02 00:49:51,531 root : INFO
2019-09-02 00:49:51,531 root : INFO > 我們要去市中心吃比薩箱亿。
2019-09-02 00:49:51,531 root : INFO = we are going downtown to eat pizza .
2019-09-02 00:49:51,552 root : INFO < we are going downtown to eat pizza . <EOS>
2019-09-02 00:49:51,552 root : INFO
2019-09-02 00:49:51,552 root : INFO > 她有興趣學(xué)習(xí)新的想法跛锌。
2019-09-02 00:49:51,552 root : INFO = she is interested in learning new ideas .
2019-09-02 00:49:51,573 root : INFO < she is interested in learning new ideas . <EOS>
2019-09-02 00:49:51,573 root : INFO
2019-09-02 00:49:51,573 root : INFO > 他是一位有前途的學(xué)生。
2019-09-02 00:49:51,573 root : INFO = he is a promising student .
2019-09-02 00:49:51,591 root : INFO < he is a promising student . <EOS>
2019-09-02 00:49:51,591 root : INFO
2019-09-02 00:49:51,591 root : INFO > 他今天沒上學(xué)。
2019-09-02 00:49:51,591 root : INFO = he is absent from school today .
2019-09-02 00:49:51,609 root : INFO < he is absent from school today . <EOS>
2019-09-02 00:49:51,609 root : INFO
2019-09-02 00:49:51,609 root : INFO > 我期待她的來信髓帽。
2019-09-02 00:49:51,609 root : INFO = i am expecting a letter from her .
2019-09-02 00:49:51,628 root : INFO < i am expecting a letter from her . <EOS>
2019-09-02 00:49:51,628 root : INFO
2019-09-02 00:49:51,629 root : INFO > 他很窮菠赚。
2019-09-02 00:49:51,629 root : INFO = he is poor .
2019-09-02 00:49:51,640 root : INFO < he is poor . <EOS>
2019-09-02 00:49:51,640 root : INFO
2019-09-02 00:49:51,640 root : INFO > 他擅長(zhǎng)應(yīng)付小孩子。
2019-09-02 00:49:51,640 root : INFO = he is good at dealing with children .
2019-09-02 00:49:51,661 root : INFO < he is good at dealing with children . <EOS>
2019-09-02 00:49:51,661 root : INFO
Process finished with exit code 0
可視化結(jié)果如下: