歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~git
導語: Google Tensor2Tensor系統是一套十分強大的深度學習系統,在多個任務上的表現很是搶眼。尤爲在機器翻譯問題上,單模型的表現就能夠超過以前方法的集成模型。這一套系統的模型結構、訓練和優化技巧等,能夠被利用到公司的產品線上,直接轉化成生產力。本文對Tensor2Tensor系統從模型到代碼進行了全面的解析,指望可以給你們提供有用的信息。docker
Tensor2Tensor(T2T)是Google Brain Team在Github上開源出來的一套基於TensorFlow的深度學習系統。該系統最初是但願徹底使用Attention方法來建模序列到序列(Sequence-to-Sequence,Seq2Seq)的問題,對應於《Attention Is All You Need》這篇論文。該項工做有一個有意思的名字叫「Transformer」。隨着系統的不斷擴展,T2T支持的功能變得愈來愈多,目前能夠建模的問題包括:圖像分類,語言模型、情感分析、語音識別、文本摘要,機器翻譯。T2T在不少任務上的表現很好,而且模型收斂比較快,在TF平臺上的工程化代碼實現的也很是好,是一個十分值得使用和學習的系統。數組
若是是從工程應用的角度出發,想快速的上手使用T2T系統,只須要對模型有一些初步的瞭解,閱讀一下workthrough文檔,很快就能作模型訓練和數據解碼了。這就是該系統想要達到的目的,即下降深度學習模型的使用門檻。系統對數據處理、模型、超參、計算設備都進行了較高的封裝,在使用的時候只須要給到數據路徑、指定要使用的模型和超參、說明計算設備就能夠將系統運行起來了。微信
若是想深刻了解系統的實現細節,在該系統上作二次開發或是實現一些研究性的想法,那就須要花費必定的時間和精力來對模型和代碼進行研究。T2T是一個較複雜的系統,筆者近期對模型和代碼實現進行了全面的學習,同時對涉及到序列到序列功能的代碼進行了剝離和重構,投入了較多的時間成本。因筆者是作天然語言處理研究的,這篇文章裏主要關注的是Transformer模型。寫這篇文章一方面是總結和記錄一下這個過程當中的一些收穫,另外一方面是把本身對T2T的理解分享出來,但願可以提供一些有用的信息給同窗們。網絡
序列到序列(Sequence-to-Sequence)是天然語言處理中的一個常見任務,主要用來作泛文本生成的任務,像機器翻譯、文本摘要、歌詞/故事生成、對話機器人等。最具備表明性的一個任務就是機器翻譯(Machine Translation),將一種語言的序列映射到另外一個語言的序列。例如,在漢-英機器翻譯任務中,模型要將一個漢語句子(詞序列)轉化成一個英語句子(詞序列)。框架
目前Encoder-Decoder框架是解決序列到序列問題的一個主流模型。模型使用Encoder對source sequence進行壓縮表示,使用Decoder基於源端的壓縮表示生成target sequence。該結構的好處是能夠實現兩個sequence之間end-to-end方式的建模,模型中全部的參數變量統一到一個目標函數下進行訓練,模型表現較好。圖1展現了Encoder-Decoder模型的結構,從底向上是一個機器翻譯的過程。分佈式
Encoder和Decoder能夠選用不一樣結構的Neural Network,好比RNN、CNN。RNN的工做方式是對序列根據時間步,依次進行壓縮表示。使用RNN的時候,通常會使用雙向的RNN結構。具體方式是使用一個RNN對序列中的元素進行從左往右的壓縮表示,另外一個RNN對序列進行從右向左的壓縮表示。兩種表示被聯合起來使用,做爲最終序列的分佈式表示。使用CNN結構的時候,通常使用多層的結構,來實現序列局部表示到全局表示的過程。使用RNN建模句子能夠看作是一種時間序列的觀點,使用CNN建模句子能夠看作一種結構化的觀點。使用RNN結構的序列到序列模型主要包括RNNSearch、GNMT等,使用CNN結構的序列到序列模型主要有ConvS2S等。函數
Transformer是一種建模序列的新方法,序列到序列的模型依然是沿用了上述經典的Encoder-Decoder結構,不一樣的是再也不使用RNN或是CNN做爲序列建模機制了,而是使用了self-attention機制。這種機制理論上的優點就是更容易捕獲「長距離依賴信息(long distance dependency)」。所謂的「長距離依賴信息」能夠這麼來理解:1)一個詞實際上是一個能夠表達多樣性語義信息的符號(歧義問題)。2)一個詞的語義肯定,要依賴其所在的上下文環境。(根據上下文消岐)3)有的詞可能須要一個範圍較小的上下文環境就能肯定其語義(短距離依賴現象),有的詞可能須要一個範圍較大的上下文環境才能肯定其語義(長距離依賴現象)。學習
舉個例子,看下面兩句話:「山上有不少杜鵑,春天到了的時候,會漫山遍野的開放,很是美麗。」 「山上有不少杜鵑,春天到了的時候,會漫山遍野的啼鳴,很是婉轉。」在這兩句話中,「杜鵑」分別指花(azalea)和鳥(cuckoo)。在機器翻譯問題中,若是不看距其比較遠的距離的詞,很難將「杜鵑」這個詞翻譯正確。該例子是比較明顯的一個例子,能夠明顯的看到詞之間的遠距離依賴關係。固然,絕大多數的詞義在一個較小範圍的上下文語義環境中就能夠肯定,像上述的例子在語言中佔的比例會相對較小。咱們指望的是模型既可以很好的學習到短距離的依賴知識,也可以學習到長距離依賴的知識。
那麼,爲何Transformer中的self-attention理論上可以更好的捕獲這種長短距離的依賴知識呢?咱們直觀的來看一下,基於RNN、CNN、self-attention的三種序列建模方法,任意兩個詞之間的交互距離上的區別。圖2是一個使用雙向RNN來對序列進行建模的方法。因爲是對序列中的元素按順序處理的,兩個詞之間的交互距離能夠認爲是他們之間的相對距離。W1和Wn之間的交互距離是n-1。帶有門控(Gate)機制的RNN模型理論上能夠對歷史信息進行有選擇的存儲和遺忘,具備比純RNN結構更好的表現,可是門控參數量必定的狀況下,這種能力是必定的。隨着句子的增加,相對距離的增大,存在明顯的理論上限。
圖3展現了使用多層CNN對序列進行建模的方法。第一層的CNN單元覆蓋的語義環境範圍較小,第二層覆蓋的語義環境範圍會變大,依次類推,越深層的CNN單元,覆蓋的語義環境會越大。一個詞首先會在底層CNN單元上與其近距離的詞產生交互,而後在稍高層次的CNN單元上與其更遠一些詞產生交互。因此,多層的CNN結構體現的是一種從局部到全局的特徵抽取過程。詞之間的交互距離,與他們的相對距離成正比。距離較遠的詞只能在較高的CNN節點上相遇,才產生交互。這個過程可能會存在較多的信息丟失。
圖4展現的是基於self-attention機制的序列建模方法。注意,爲了使圖展現的更清晰,少畫了一些鏈接線,圖中「sentence」層中的每一個詞和第一層self-attention layer中的節點都是全鏈接的關係,第一層self-attention layer和第二層self-attention layer之間的節點也都是全鏈接的關係。咱們能夠看到在這種建模方法中,任意兩個詞之間的交互距離都是1,與詞之間的相對距離不存在關係。這種方式下,每一個詞的語義的肯定,都考慮了與整個句子中全部的詞的關係。多層的self-attention機制,使得這種全局交互變的更加複雜,可以捕獲到更多的信息。
綜上,self-attention機制在建模序列問題時,可以捕獲長距離依賴知識,具備更好的理論基礎。
上面小節介紹了self-attention機制的好處,本小結來介紹一下self-attention機制的的數學形式化表達。首先,從attention機制講起。能夠將attention機制看作一種query機制,即用一個query來檢索一個memory區域。咱們將query表示爲key_q,memory是一個鍵值對集合(a set of key-value pairs),共有M項,其中的第i項咱們表示爲<key_m[i], value_m[i]>。經過計算query和key_m[i]的相關度,來決定查詢結果中,value_m[i]所佔的權重比例。注意,這裏的key_q,key_m,value_m都是vector。
Attention的計算歸納起來分三步:1)計算query和memory中每一個key_m的相關度。2)對全部的相關度結果使用softmax函數進行機率歸一化處理。3)根據機率歸一化結果對memory中的全部value_m進行加權平均,獲得最終的查詢結果。計算過程,形式化爲:
經常使用的相關度計算函數有基於加法式(additive)的和乘法式(dot-product)的兩種。加法式的函數,先要通過一個前向神經網絡單元,再通過一個線性變換,獲得一個實數值。乘法式的函數則是兩個向量的直接點乘,獲得一個實數值。
在Encoder-Decoder框架中,attention機制通常用於鏈接Encoder和Decoder,即以Decoder的狀態做爲key,以源語言句子的分佈式表示做爲memory,從中查找出相關的源語言信息,生成目標語言的詞語。在該機制中,memory中的key_m和value_m是相同的。在self-attention機制中,每一個詞彙以本身的embedding爲query,查詢由全部詞彙的embedding構成的memory空間,獲得查詢結果做爲本詞的表示。假如句子長度爲n,全部的詞分別查詢一遍memory獲得的結果長度依然會是n。這些詞的查詢過程是能夠並行的。若是relation函數是乘法式的,那麼這個查詢的過程就是矩陣的乘法,能夠形式化爲:
在self-attention中,Q=K=V,是一個由全部詞的詞向量構成的一個矩陣。
綜上,self-attention是一種序列建模的方式,在對句子進行分佈式表示的時候,句子中的全部的詞都會發生直接的交互關係。
《Attention Is All You Need》這篇文章,描述了一個基於self-attention的序列到序列的模型,即「Transformer」。該模型將WMT2014英-德翻譯任務的BLEU值推到了新高,在英-法翻譯任務上,接近於以前報出的最好成績,而這僅僅是Transformer單模型的表現。以前報出的最好成績都是基於集成方法的,須要訓練多個模型,最後作集成。同時該模型也被用在英語的成分句法分析任務上,表現也基本接近於以前報出的最好模型成績。該模型的收斂速度也很是的快,在英-法3600萬句對的訓練集上,只須要8卡並行3.5天就能夠收斂。
該模型的表現的如此好的緣由,其實不只僅是一個self-attention機制致使的,實際上Transformer模型中使用了很是多有效的策略來使得模型對數據的擬合能力更強,收斂速度更快。整個Transformer的模型是一套解決方案,而不只僅是對序列建模機制的改進。下面咱們對其進行一一講解。
首先,仍是來說一下Transformer中的self-attention機制。上面講到了self-attention的基本形式,可是Transformer裏面的self-attention機制是一種新的變種,體如今兩點,一方面是加了一個縮放因子(scaling factor),另外一方面是引入了多頭機制(multi-head attention)。
縮放因子體如今Attention的計算公式中多了一個向量的維度做爲分母,目的是想避免維度過大致使的點乘結果過大,進入softmax函數的飽和域,引發梯度太小。Transformer中的self-attention計算公式以下:
多頭機制是指,引入多組的參數矩陣來分別對Q、K、V進行線性變換求self-attention的結果,而後將全部的結果拼接起來做爲最後的self-attention輸出。這樣描述可能不太好理解,一看公式和示意圖就會明白了,以下:
這種方式使得模型具備多套比較獨立的attention參數,理論上能夠加強模型的能力。
self-attention機制建模序列的方式,既不是RNN的時序觀點,也不是CNN的結構化觀點,而是一種詞袋(bag of words)的觀點。進一步闡述的話,應該說該機制視一個序列爲扁平的結構,由於不論看上去距離多遠的詞,在self-attention機制中都爲1。這樣的建模方式,實際上會丟失詞之間的相對距離關係。舉個例子就是,「牛 吃了 草」、「草 吃了 牛」,「吃了 牛 草」三個句子建模出來的每一個詞對應的表示,會是一致的。
爲了緩解這個問題,Transformer中將詞在句子中所處的位置映射成vector,補充到其embedding中去。該思路並非第一次被提出,CNN模型其實也存在一樣的難以建模相對位置(時序信息)的缺陷,Facebook提出了位置編碼的方法。一種直接的方式是,直接對絕對位置信息建模到embedding裏面,即將詞Wi的i映射成一個向量,加到其embedding中去。這種方式的缺點是隻能建模有限長度的序列。Transformer文章中提出了一種很是新穎的時序信息建模方式,就是利用三角函數的週期性,來建模詞之間的相對位置關係。具體的方式是將絕對位置做爲三角函數中的變量作計算,具體公式以下:
該公式的設計很是先驗,尤爲是分母部分,不太好解釋。從筆者我的的觀點來看,一方面三角函數有很好的週期性,也就是隔必定的距離,因變量的值會重複出現,這種特性能夠用來建模相對距離;另外一方面,三角函數的值域是[-1,1],能夠很好的提供embedding元素的值。
Transformer中的多層結構很是強大,使用了以前已經被驗證過的不少有效的方法,包括:residual connection、layer normalization,另外還有self-attention層與Feed Forward層的堆疊使用,也是很是值得參考的結構。圖6展現了Transformer的Encoder和Decoder一層的結構。
圖6中,左側的Nx表明一層的Encoder,這一層中包含了兩個子層(sub-layer),第一個子層是多頭的self-attention layer,第二個子層是一個Feed Forward層。每一個子層的輸入和輸出都存在着residual connection,這種方式理論上能夠很好的回傳梯度。Layer Normalization的使用能夠加快模型的收斂速度。self-attention子層的計算,咱們前面用了很多的篇幅講過了,這裏就再也不贅述了。Feed Forward子層實現中有兩次線性變換,一次Relu非線性激活,具體計算公式以下:
文章的附頁中將這種計算方式也看作是一種attention的變種形式。
圖6中,右側是Decoder中一層的結構,這一層中存在三個子層結構,第一層是self-attention layer用來建模已經生成的目標端句子。在訓練的過程當中,須要一個mask矩陣來控制每次self-attention計算的時候,只計算到前t-1個詞,具體的實現方式,咱們會在後面講代碼實現的時候進行說明。第二個子層是Encoder和Decoder之間的attention機制,也就是去源語言中找相關的語義信息,這部分的計算與其餘序列到序列的注意力計算一致,在Transformer中使用了dot-product的方式。第三個子層是Feed Forward層,與Encoder中的子層徹底一致。每一個子層也都存在着residual connection和layer normalization操做,以加快模型收斂。
Transformer中的這種多層-多子層的機制,可使得模型的複雜度和可訓練程度都變高,達到很是強的效果,值得咱們借鑑。
模型的訓練採用了Adam方法,文章提出了一種叫warm up的學習率調節方法,如公式所示:
公式比較先驗,看上去比較複雜,其實邏輯表達起來比較清楚,須要預先設置一個warmup_steps超參。當訓練步數step_num小於該值時,以括號中的第二項公式決定學習率,該公式實際是step_num變量的斜率爲正的線性函數。當訓練步數step_num大於warm_steps時,以括號中的第一項決定學習率,該公式就成了一個指數爲負數的冪函數。因此總體來看,學習率呈先上升後降低的趨勢,有利於模型的快速收斂。
模型中也採用了兩項比較重要的正則化方法,一個就是經常使用的dropout方法,用在每一個子層的後面和attention的計算中。另外一個就是label smoothing方法,也就是訓練的時候,計算交叉熵的時候,再也不是one-hot的標準答案了,而是每一個0值處也填充上一個非0的極小值。這樣能夠加強模型的魯棒性,提高模型的BLEU值。這個思路其實也是必定程度在解決訓練和解碼過程當中存在的exposure bias的問題。
Transformer系統的強大表現,不只僅是self-attention機制,還須要上述的一系列配合使用的策略。設計該系統的研究者對深度學習模型和優化算法有着很是深入的認識和敏銳的感受,不少地方值得咱們借鑑學習。Transformer的代碼實現工程化比較好,可是也存在一些地方不方便閱讀和理解,後面的章節中會對Transformer的代碼實現進行詳細講解,將總體結構講清楚,把其中的疑難模塊點出來。
Tensor2Tensor的系統存在一些特色,致使使用和理解的時候可能會存在一些須要時間來思考和消化的地方,在此根據我的的理解,寫出一些本身曾經花費時間的地方。
Tensor2Tensor的使用是比較方便的,對於系統中能夠支持的問題,直接給系統設置好下面的信息就能夠運行了:數據,問題(problem),模型,超參集合,運行設備。這裏的實現實際上是採用了設計模型中的工廠模式,即給定一個問題名字,返回給相應的處理類;給定一個超參名,返回一套超參的對象。實現這種方式的一個重點文件是utils/registry.py。在系統啓動的時候,全部的問題和超參都會在registry中註冊,保存到_MODELS,_HPAPAMS,_RANGED_HPARAMS中等待調用。
在此主要以序列到序列的系統使用和實現爲主線進行講解。系統的運行分三個階段:數據處理,訓練,解碼。對應着三個入口:t2t-datagen,t2t-trainer,t2t-decoder。
數據處理的過程包括:
1.(下載)讀取訓練和開發數據。若是須要使用本身的數據的話,能夠在問題中指定。
2.(讀取)構造詞彙表。可使用本身預先構造好的詞彙表。系統也提供構建BPE詞彙表的方法。注意,這裏有個實現細節是系統在抽取BPE詞彙表的時候,有個參數,默認並不是使用全量的數據。經過屢次迭代嘗試,獲得最接近預設詞彙表規模的一個詞彙表。在大數據量的時候,這個迭代過程會很是慢。
3. 使用詞彙表將單詞映射成id,每一個句子後會加EOS_ID,每一個平行句對被構形成一個dict對象({‘inputs’:value,‘targets’:value}),將全部對象序列化,寫入到文件中,供後面訓練和評價使用。
模型訓練的過程的過程主要經過高級的Tensorflow API來管理,只是須要指定數據、問題名、模型名、超參名、設備信息就能夠運行了。比較關鍵的一個文件是utils/trainer_lib.py文件,在這個文件中,構建Experiment、Estimator、Monitor等來控制訓練流程。使用者主要須要設置的就是訓練過程的一些參數,好比訓練最大迭代次數,模型評估的頻率,模型評估的指標等。超參能夠直接使用系統已有的參數集,也能夠經過字符串的形式向內傳參。簡單的任務不太須要動超參,由於系統中的超參集合基本上都是通過實驗效果驗證的。須要注意的就是batch_size過大的時候,可能會致使顯存不足,致使程序錯誤。通常是使用continuous_train_and_eval模式,使模型的訓練和評估間隔進行,隨時能夠監控模型的表現。
解碼的過程,能夠提供總體文件、也能夠是基於Dataset的,同時系統也提供server的方式,能夠提供在線的服務,並無什麼特別好講的。
下面列出了要深度掌握Tensor2Tensor系統時,可能由於其實現特色,會遇到的一些問題:
1. 系統支持多任務,任務混雜,致使代碼結構比較複雜。在實現的時候,要考慮到總體的結構,因此會存在各類封裝、繼承、多態的實現。可能你只想用其中的一個功能,理解該功能對應的代碼,可是卻須要排除掉大量的不相關的代碼。
2. 系統基於Tensorflow封裝較高的API。使用了Tensorflow中比較高的API來管理模型的訓練和預測,Experiment,Monitor,Estimator,Dataset對象的使用隱藏了比較多的控制流程,對於側重應用的用戶來講,多是是好事情,設一設參數就能跑。可是對於想了解更多的開發人員來講,TF該部分的文檔實在不多,說的也不清楚,不少時候須要去閱讀源代碼才能知道實驗究竟是不是按照本身預期的進行的。這種方式也不太方便找bug和調試。
3. 某些方法調用比較深。緣由應該仍是出於總體結構和擴展性的考慮。這致使了實現一點很小功能的方法A,須要再調一個其餘方法B,B再去調用方法C,實際上每一個方法中就幾行代碼,甚至有的方法就是空操做。
4. 多層繼承和多態也下降了代碼的可讀性。追溯一個類的某個方法的時候,須要看到其父類的父類的父類。。。這些父類和子類之間的方法又存在着調來調去的關係,同名方法又存在着覆蓋的關係,因此要花一些時間來肯定當前的方法名究竟是調用的的哪一個類中的方法。
5. 要求開發者有模型層面的理解和與代碼實現的掛鉤。確定是要提升對模型邏輯的理解,但在讀代碼的過程當中,會遇到兩種問題:第一個,代碼實現的是論文中的功能,但不是論文中的原始公式,可能要作變形以規避溢出的問題,或是實現更高的效率;第二個,某些代碼實現與其論文中的表述存在不一致的狀況。
整體來講,對T2T系統的代碼邏輯劃分以下,共包括三個大的模塊:
這裏不會對代碼作追蹤式的分析,會分條的講解一些閱讀Tensor2Tensor系統代碼時可能遇到的問題,點出一些重要的功能所在的位置和實現邏輯。
9.Estimator對象。能夠理解爲模型對象,能夠經過Estimator執行模型的訓練、評估、解碼。Estimator對象最重要的一個形參是model_fn,也就是具體執行訓練、評估、解碼的函數入口。三個入口分別對應着三個EstimatorSpec對象,如圖9,10所示。
從圖10能夠看出,用於訓練的EstimatorSpec對象須要描述計算圖中feature和(loss,train_op)之間的關係;用於評估的EstimatorSpec對象須要描述計算圖中feature和(loss,eval_metrics_ops)之間的關係;用於評估的EstimatorSpec對象須要描述features和predictions之間的關係。
\11. Positional encoding的實現。論文中的實現和代碼中的實現存在公式變形和不一致的狀況,可能會致使困惑,故在此指出。論文中Positional encoding中三角函數的參數部分公式以下:
代碼中的實現須要對該公式作變形,以規避數值溢出的風險,公式變形過程以下:
還須要指出的是,論文中根據維度下標的奇偶性來交替使用sin和cos函數的說法,在代碼中並非這樣實現的,而是前一半的維度使用sin函數,後一半的維度使用cos函數,並無考慮奇偶性
12. **以token數量做爲batch size。**這種方式比起以句子個數做爲batch size的方式來,能到batch佔顯存的空間更加平均,不會致使由於訓練數據致使的顯存佔用忽上忽下,形成顯存空間不夠用,致使程序崩潰。
\13. 如何作mask。因爲模型是以batch爲單位進行訓練的,batch的句長以其中最長的那個句子爲準,其餘句子要作padding。padding項在計算的過程當中若是不處理的話,會引入噪音,因此就須要mask,來使padding項不對計算起做用。mask在attention機制中的實現很是簡單,就是在softmax以前,把padding位置元素加一個極大的負數,強制其softmax後的機率結果爲0。舉個例子,[1,1,1]通過softmax計算後結果約爲[0.33,0.33,0.33],[1,1,-1e9] softmax的計算結果約爲[0.5, 0.5,0]。這樣就至關於mask掉了數組中的第三項元素。在對target sequence進行建模的時候,須要保證每次只attention到前t-1個單詞,這個地方也須要mask,總體的mask是一個上三角矩陣,非0元素值爲一個極大的負值。
\14. 基於batch的解碼。解碼的時候,若是是基於文件的,那麼就會將句子組成batch來並行解碼。這裏有個小trick,就是先對句子進行排序,而後從長的句子開始組batch,翻譯,再把句子恢復成原先的順序返回。這種方式能夠很好的檢測到顯存不足的錯誤,由於解句子最長的一個batch的時候,顯存都是夠得,那其餘的batch也不存在問題。
本文對Google的Tensor2Tensor系統進行了深度的解讀,涉及到了比較多的方面,筆者也還須要對其進行更加深刻的學習和研究,但願可以與對該模型以及DL for NLP技術感興趣的同窗們一塊兒交流,共同進步!
問答
相關閱讀
此文已由做者受權騰訊雲+社區發佈,原文連接:https://cloud.tencent.com/developer/article/1116709?fromSource=waitui
歡迎你們前往騰訊雲+社區或關注雲加社區微信公衆號(QcloudCommunity),第一時間獲取更多海量技術實踐乾貨哦~
海量技術實踐經驗,盡在雲加社區! https://cloud.tencent.com/developer?fromSource=waitui