Deeplearning4j 實戰 (20):Attention機制在文本分類中的應用

Eclipse Deeplearning4j GitChat課程https://gitbook.cn/gitchat/column/5bfb6741ae0e5f436e35cd9f
Eclipse Deeplearning4j 系列博客https://blog.csdn.net/wangongxi
Eclipse Deeplearning4j Githubhttps://github.com/eclipse/deeplearning4j

在之前的文章中,我們介紹過如何基於LSTM來進行情感識別的任務。從本質上來說,LSTM可以用於提取整段文本的語義信息,然後對最後一個LSTM Cell輸出的結果進行業務層面的分類建模即可。在工業界的實際嘗試中,尤其對於短文本,這種建模方式本身可以作爲baseline甚至可以經過精心的調優達到生產環境的精度要求。但是RNN結構有其自身的一些缺點。比如,長距離依賴導致頭部的信息丟失,容易在BPTT算法執行的時候發生梯度的彌散等問題。當然一些改進的RNN結構,包括上面提到的LSTM和GRU等等可以緩解此類問題,但當遇到長文本問題的時候,效果的提升就很難了。
Attention機制早期用在圖像領域較多。在論文《Recurrent Models of Visual Attention》中作者將Attention機制與RNN結合用於圖像分類的任務。Attention適用於圖像領域其實並不意外,對於人類來說通過識別核心內容來完成一個模式識別的任務是非常常見的,也是人類所擅長的,這其實就是注意力機制的一種表現或者說應用。傳統的CNN和RNN雖然可以提取一些局部特徵並通過整合這些局部特徵來達到整體識別的效果,但對於一些長距離關聯的場景就比較難了。無論是在圖像還是文本處理的過程中,長距離依賴的識別問題總是一個挑戰,而attention則更適合處理這樣的問題。Deeplearning4j從1.0.0-beta4版本開始支持attention機制,主要實現了用於RNN的RecurrentAttention模型、自注意力模型(Self-attention)及其變種Learned Self-attention三種。下面我們首先介紹注意力模型的基本原理,再結合NLP中經典的分類問題嘗試基於attention機制來實現。

Attention機制原理

我們先來看下Attention機制的思想。對於三元組Query, Key, Value,attention機制的數學表達式可以如下:
在這裏插入圖片描述
公式中的函數符號f用來表示某種相似度計算。這可以是普通的餘弦相似度、歐式距離的計算,也可以是一個完整的全連接神經網絡。需要注意的一點是,Key和Value不一定代表兩個不同的事物,很多時候它們可以指代同一個元素(Key=Value),比如Embedding後的詞向量。從公式中我們可以看出,無論Query、Key和Value的物理意義代表什麼,都是這三個元素之間直接建立函數關係,不需要依賴時空維度的太多信息。也正是因爲這樣,RNN中添加attention機制可以在輸出和輸入cell之間直接建立某種聯繫,而無需經過多步的前向傳播,對於CNN也是類似的手段。另外必須指出的是,attention機制並非完全沒有缺陷。首先它需要消耗大量的存儲空間,其次在一些細節上比如相似度函數f的選擇上也是值得推敲的,不能完全一概而論。但其作爲提升baseline模型的優化手段,是完全可以嘗試的。我們將attetion機制的基本理論用到RNN模型中可以優化諸如文本分類、機器翻譯的問題。我們以Seq2Seq框架爲例來解釋attention在RNN建模中的使用。先看下面這張示意圖。
在這裏插入圖片描述
在Seq2Seq或者Encoder-Decoder架構中,圖中在編解碼層各有4個RNN Cell。根據上面提到的公式,我們令Decoder層的每一個cell的輸出作爲Query,而Encoder層的每一個神經元作爲Key(這裏Key=Value)。此外,Q1神經元與Encoder層的每一個神經元之間的連線上都標有一個權重來表示相似度的大小且加和爲1(通常可以用softmax函數來實現)。那麼根據基礎公式我們可以得到如下表達式:
在這裏插入圖片描述
我們可以設想實際的Encoder層的輸入是詞向量,那麼對於Decoder層的第一個cell的輸出,Encoder層的第一個詞向量貢獻最大,因爲它的權重最大,換言之Docoder的第一個輸出和Encoder層第一個輸入有很大關係,作爲Q1它的注意力集中在K1或者說V1上面。這就是一種樸素的RNN+Attention機制的使用方式。這個很好理解,比如我們需要將“I am Allen”翻譯成“我 是 艾倫”,那麼自然“我”這個字注意力應該集中在“I”上面,其他的關係不大了。從另一個角度說,這種機器翻譯中的一種對齊機制(alignment)。當然這種alignment是通過學習訓練出來的,而不是像傳統的CRF和HMM需要事先編排好對齊機制。這種基本的RNN attention機制在Deeplearning4j也有實現,就是引言中提到的RecurrentAttention模型。接下來,我們看下自注意力模型機制。
自注意力機制(self-attention)主要的參考文獻是論文《Attenton is all you need》。它的核心思想其實是在序列上下文本身通過attention機制來找出語義單元之間分佈的規律,因此它比較適合做語言模型,這也是去年Bert模型採用它的原因。Bert以及依賴的Transformer在後面的文章中會單獨介紹,這裏不單獨展開。我們還是回到self-attention機制的原理上。先看下面這張圖。
在這裏插入圖片描述
和上面提到的RNN attention機制有些類似,我們還是會涉及到Q,K,V這三元組,但不同之處在於圖中這兩個序列其實是完全相同的,這裏爲了方便解釋說明,才用了兩個序列來表示。以序列中第一元素爲例,它和其他元素的相似度同樣用一個加權求和爲1的分佈來表示。那麼attention的結果其實和上面公式將會是一樣的。這裏我們假定Q=K=V,這是一種簡單處理的方式,實際上我們可以將Q,K,V分別乘以一個矩陣做一次簡單的線性變換後再計算attention的值。我們來看下這種更爲普遍做法的圖解。
在這裏插入圖片描述
這裏就是通過簡單的線性變換得到三元組。原始輸入X可以是Embedding向量等等。
在這裏插入圖片描述
以上這張圖就是計算attention值的過程。在上面的圖中,注意力權重(0.6,0.2,0.1,0.1)都是我們指定的,而這裏則給出了詳細的計算過程。通過對原始輸入進行線性變換後的三元組,Q和K進行某種相似度計算(這裏是內積)得到一個標量,除以模長後再經過softmax函數得到一個和爲1的概率分佈作爲注意力的值。輸出元素的值則是對各個V加權求和的結果,總體上可以用下圖來表示。
在這裏插入圖片描述
這裏我們對原始輸入X分別用單個矩陣得到Q,K,V。如果我們用多個矩陣的話,就可以得到多個三元組的表示,那麼這種場景就稱之爲多頭的自注意力機制(Multi-head Self Attention)。
在這裏插入圖片描述
以上即是對attention在RNN中以及self-attention中實現原理的一個解釋。可以看出attention本身的原理並不是很複雜,但通過變換三元組中的元素,可以賦予attention機制不同的使用場景。對於某一神經元的輸出,通過attention機制可以將輸入層神經元對它的貢獻用一個概率分佈來表示,我們可以理解爲這是一種權重,權重值越大,則對輸出神經元的影響越大,換句話說注意力的集中點就是權重較大的那個輸入層神經元。Attention機制可以通過空間換時間的方式直接看到全局的信息,學習到對目標更爲重要的特徵併爲此提權,較好的解決了長距離上下文依賴的問題。下面我們來看下Deeplearning4j中attention機制與文本分類問題中的使用。

Attention機制與文本分類

我們首先介紹下語料的情況。
在這裏插入圖片描述
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-H05jzGWr-1589937262876)(…/Attention/attention-corpus-label.jpg)]
從截圖中我們可以看到有兩份數據,一份是以空格分隔的文本,每一行即爲一條語料。另一張圖則是每條語料對應的標註,我們共設計了4種label。
首先給出基於原始LSTM建模文本分類問題的實現邏輯。

MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
            .seed(1234)
            .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
            .updater(new Adam(0.001))
            .list()
            .layer(0, new EmbeddingLayer.Builder().nIn(VOCAB_SIZE).nOut(100).activation(Activation.IDENTITY).build())
            .layer(1, new LSTM.Builder().nIn(100).nOut(100).activation(Activation.SOFTSIGN).build())
            .layer(2, new RnnOutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
                    .activation(Activation.SOFTMAX).nIn(100).nOut(4).build())
            .setInputType(InputType.recurrent(VOCAB_SIZE))
            .build();

這個建模邏輯在之前的博客《Deeplearning4j 實戰(6):基於LSTM的文本情感識別及其Spark實現》中是一致的(略微不同的地方在於LSTM接口的變化,可以升級Deeplearning4j版本後直接使用)。這個建模邏輯將語料中的詞進行Embedding之後,通過LSTM來掃描整個序列,並且將最後一個cell的輸出向量進行分類即可。那麼我們來看下增加RNN attention機制後的模型。

MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
            .seed(1234)
            .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
            .updater(new Adam(0.01))
            .list()
            .layer(0, new EmbeddingLayer.Builder().nIn(VOCAB_SIZE).nOut(100).activation(Activation.IDENTITY).build())
            .layer(1, new LSTM.Builder().nIn(100).nOut(100).activation(Activation.SOFTSIGN).build())
            .layer(2, new RecurrentAttentionLayer.Builder().nIn(100).nOut(100).nHeads(2).activation(Activation.SOFTSIGN).build())
            .layer(3, new RnnOutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
                    .activation(Activation.SOFTMAX).nIn(100).nOut(4).build())
            .setInputType(InputType.recurrent(VOCAB_SIZE))
            .build();

我們在LSTM層後面直接添加RnnAttentionLayer。Attention層有幾個比較重要的參數,即邏輯中的nIn、nOut和nHead。nIn其實就是詞向量的維度,nOut則是nIn向量經過變換後的輸出。至於nHead則是是否使用multi-head的設置。我們來看下RnnAttentionLayer源碼中的註釋對該機制的說明。
在這裏插入圖片描述
對於RNN attention的輸出輸出shape在註釋中做了說明。輸入格式是兼容RNN的輸出[batchSize, features, timesteps],而對應的輸出則是[batchSize, nOut, timesteps]。
在這裏插入圖片描述
上面截圖則是RnnAttentionLayer的實現。從sameDiff.nn.dotProductAttention方法中的入參可以看出,RnnAttentionLayer計算的是該層的隱藏層的數據作爲Query,而每一個step的輸入則是作爲Key和Value(Key=Value),這和我們上面分析的結果是一致的。需要注意的是,目前的版本還不支持在同一個mini-batch中不等長的語料的訓練,因此最簡單的處理辦法就是將mini-batch設置爲1,這樣就回避了這個問題。
接着,我們來看下如果添加Self-attention Layer。同RNN attention相似,我們可以直接在LSTM上一層添加一層self-attention。示例如下:

MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
            .seed(1234)
            .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
            .updater(new Adam(0.01))
            .list()
            .layer(0, new EmbeddingLayer.Builder().nIn(VOCAB_SIZE).nOut(100).activation(Activation.IDENTITY).build())
            .layer(1, new LSTM.Builder().nIn(100).nOut(100).activation(Activation.SOFTSIGN).build())
            .layer(2, new SelfAttentionLayer.Builder().nIn(100).nOut(100).nHeads(1).projectInput(false).build())
            .layer(3, new RnnOutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
                    .activation(Activation.SOFTMAX).nIn(100).nOut(4).build())
            .setInputType(InputType.recurrent(VOCAB_SIZE))
            .build();

這裏我們通過下面layer的聲明來設置了一層自注意力機制的模型。

.layer(2, new SelfAttentionLayer.Builder().nIn(100).nOut(100).nHeads(1).projectInput(false).build())

這裏同樣可以設置.nHeads來設置multi-head的數量,注意如果多頭數大於1,參數.projectInput需要設置爲true。這裏我們多頭數量設置爲1。我們經過每一輪就在訓練集上評估下模型的效果,可以得到類似如下的控制檯輸出。
在這裏插入圖片描述
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Ei8R9TYk-1589937262879)(…/Attention/self-attention-train-eval-2.jpg)]
可以看到在前幾輪尤其是第一輪loss值相對較高,評估的準確率也比較低,後來經過幾輪的學習後,loss有了進一步的降低同時最終可以得到100%的準確率。可能有人會疑惑是不是增加多頭的數量就一定好呢,其實是不一定的。一方面增加head的數量,會造成新增大量的訓練參數,另一方面如果head數量較少,甚至不用多頭機制,模型效果也已經達到上線指標,那其實就沒有使用多頭的必要了。當然,感興趣的朋友可以自己設置多頭的數量進行驗證,相信最終的模型指標也會是相當不錯,這裏就不再展開了。我們來看下self-attention底層的實現。
在這裏插入圖片描述
在Deeplearning4j中,attention機制是依賴於SameDiff自動微分工具來實現的。SameDiff的一些應用可以參考我之前的博客,這裏就不再展開了。我們注意截圖中紅框框出來的那個部分,即通過內積來計算注意力機制的接口方法。
在這裏插入圖片描述
上面這張圖則是基於內積的注意力機制的實現,從方法的入參可以看出,會對Q、K、V三元組進行內積計算。而自注意力機制的Q、K、V其實都是上下文本身,也就是上面defineLayer方法中的layerInput對象,因此這也和上文的理論分析保持了一致。至於多頭的實現,也就是defineLayer方法中的sameDiff.nn.multiHeadDotProductAttention方法,其實入參都是一樣的,底層也依賴dotProduct計算方法,只不過就像上面原理分析中講的那樣需要根據head的數量設置對應數量的權重矩陣,Wq、Wk、Wv。
在這裏插入圖片描述
這是源碼中對multi-head實現的一些註釋,可以作爲參考。
以上我們介紹了在LSTM中融合attention機制來實現文本分類的模型結構。我們來做下簡單的分析。模型的第一層通過Embedding來實現詞向量的計算,LSTM層掃描整個文本序列而後續的attention層重新分配每個cell輸出的權重對一些輸出做些提權一些做些降權,最後則是輸出分類。這裏提出一個問題,那就是如果我們不通過LSTM層,是否可以呢?其實是可以的。
我們先給出模型的結構。

MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
            .seed(1234)
            .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
            .updater(new Adam(0.01))
            .list()
            .layer(0, new EmbeddingLayer.Builder().nIn(VOCAB_SIZE).nOut(100).activation(Activation.IDENTITY).build())
            .layer(1, new SelfAttentionLayer.Builder().nIn(100).nOut(100).nHeads(1).projectInput(false).build())
            .layer(2, new RnnOutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
                    .activation(Activation.SOFTMAX).nIn(100).nOut(4).build())
            .setInputType(InputType.recurrent(VOCAB_SIZE))
            .build();

在這裏插入圖片描述
從訓練過程來看,同樣可以達到不錯的效果,甚至每一輪評估指標的提升相較於LSTM的模型還更好些。我們嘗試分析下原因。對於一些文本分類的問題來說,上下文中的若干關鍵詞其實就可以決定這條記錄的分類結果。而通過LSTM層去掃描整個序列,確實可以獲取整個序列的語義,但是也可能會添加一些無用的信息。Attention機制則比較直接,分配注意力的同時,直接對分類有益的關鍵詞賦予較高的權重,那依靠這些關鍵詞分類的問題也就可以解決了。但添加LSTM層也並非是毫無意義的,因爲每一步的掃描可以獲取獲取幾步的語義信息,通過注意力分配同樣可以將對分類有益的部分的權重提升,從而達到分類的目的。
最後我們介紹一片基於Bi-LSTM + Attention的實現的文本分類的論文。論文題目是《Attention-Based Bidirectional Long Short-Term Memory Networks for Relation Classification》
在這裏插入圖片描述
上面截圖是論文中模型實現的主要框架。整體架構和我們上面給出的模型結構是比較類似的。從圖中可以看出,第一層是Embedding層,接着是LSTM層,attention之後把重新分配權重的結果做一個加權求和,最後得到的結果進行分類。下面我們就給出基於Deeplearning4j的實現方案。

MultiLayerConfiguration netconf = new NeuralNetConfiguration.Builder()
            .seed(1234)
            .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
            .updater(new Adam(0.001))
            .list()
            .layer(0, new EmbeddingLayer.Builder().nIn(VOCAB_SIZE).nOut(100).activation(Activation.IDENTITY).build())
            .layer(1, new LSTM.Builder().nIn(100).nOut(100).activation(Activation.SOFTSIGN).build())
            .layer(2, new SelfAttentionLayer.Builder().nIn(100).nOut(100).nHeads(2).projectInput(true).build())
            .layer(3, new Subsampling1DLayer.Builder(PoolingType.SUM).kernelSize(32).stride(1).build())
            .layer(4, new DenseLayer.Builder().activation(Activation.LEAKYRELU).nIn(100).nOut(50).build())
            .layer(5, new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
            		.activation(Activation.SOFTMAX).nIn(50).nOut(4).build())
            .setInputType(InputType.recurrent(VOCAB_SIZE))
            .build();

這裏做下簡單的說明。前三層和上面描述的類似,不過我用的是自注意力機制。然後我們採用1D的池化層將上面的結果做加和,後面我們接了一個全連接。有興趣的同學可以跑下數據試試。

總結

本文介紹了Deeplearning4j中attention機制的使用。分別從attention的原理介紹到attetion在文本分類應用中的應用兩個方面介紹attention機制。在文本分類的應用場景中,我們介紹了LSTM+Attention以及單使用Attention機制進行分類的模型結構。由於我們只是爲了驗證模型的可行性,因此對最終的模型效果以及可能的調優並沒有花太多篇幅來介紹,有時間的同學可以自行嘗試。需要指出的是在Deeplearning4j中LearnedSelfAttention機制我們並沒有花太多篇幅介紹,它自身和Self Attention類似,因此我們可以直接用來替換Self Attention層。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章