基于GRU和am-softmax的句子相似度模型 | 附代码实现
作者丨蘇劍林
單位丨廣州火焰信息科技有限公司
研究方向丨NLP,神經網絡
個人主頁丨kexue.fm
前言:搞計算機視覺的朋友會知道,am-softmax 是人臉識別中的成果。所以這篇文章就是借鑒人臉識別的做法來做句子相似度模型,順便介紹在 Keras 下各種 margin loss 的寫法。
背景
細想之下會發現,句子相似度與人臉識別有很多的相似之處。
已有的做法
在我搜索到的資料中,深度學習做句子相似度模型,就只有兩種做法:一是輸入一對句子,然后輸出一個 0/1 標簽代表相似程度,也就是視為一個二分類問題,比如 Learning Text Similarity with Siamese Recurrent Networks?[1] 中的模型是這樣的:
▲?將句子相似度視為二分類模型
包括今年拍拍貸的“魔鏡杯”,也是這種格式。另外一種做法是輸入一個三元組“(句子 A,跟 A 相似的句子,跟 A 不相似的句子)”,然后用 triplet loss 的做法解決,比如文章 Applying Deep Learning To Answer Selection: A Study And An Open Task?[2]?中的做法。?
這兩種做法其實也可以看成是一種,本質上是一樣的,只不過 loss 和訓練方法有所差別。但是,這兩種方法卻都有一個很嚴重的問題:負樣本采樣嚴重不足,導致效果提升非常慢。
使用場景
我們不妨回顧一下我們使用句子相似度模型的場景。一般來說,我們事先存好了很多 FAQ 對,也就是“問題-答案”的語料對。當我們碰到一個新問題時,我們就需要比較這個新問題與原來數據庫中所有問題的相似度,找出最相似的那個,根據相似度和閾值決定是否做出回答。?
注意,這里邊包含了兩個要素,第一是“所有”,理論上來說,我們跟數據庫中的所有問題都比較一次,然后找出最相似的;第二是“閾值”,我們也不知道新問題在數據庫中是否有答案,因此這個閾值決定是我們是否要做出回應。如果不管三七二十一都取 top 1 來作答,那體驗也會很差的。?
我們先來關心“所有”。“所有”意味著在訓練的時候,對于每個句子,除了僅有的幾個相似句是正樣本,其它所有句子都應該作為負樣本。但如果用前面的做法,其實我們很難完整地采樣所有的負樣本出來,而且就算可以做到,訓練時間也非常長。這就是前面說的弊端所在。
來自人臉的幫助
我一直覺得,在機器學習領域中,其實不應該過分“劃清界線”,比如有些讀者覺得自己是做 NLP 的,于是就不碰圖像,反過來做圖像的,看到 NLP 的就遠而避之。事實上,整個機器學習領域之間的溝壑并沒有那么大,很多東西的本質都是一樣的,只是場景不同而已。比如,所謂的句子相似度模型,其實幾乎就完全對應于人臉識別任務,而人臉識別目前已經相當成熟了,顯然我們是可以借鑒的。?
先不說模型,我們來想象一下人臉識別的使用場景。比如公司內可以用人臉識別打卡,當有了一個人臉識別模型后,我們事先會存好一些公司員工的人臉照片,然后每天上班時,先拍一張員工的人臉照(實時拍攝,顯然不會跟已經存好的照片完全吻合),然后要判斷他/她是不是公司的員工,如果是,還要確定是哪一位員工。?
試想一下,將上面的場景中,“人臉”換成“句子”,是不是就是句子相似度模型的使用場景呢??
顯然,句子相似度模型可以是說 NLP 中的人臉識別了。
模型
句子相似度和人臉識別在各方面都很相似:從模型的使用到構建乃至數據集的量級上,都是如此地接近。所以,幾乎人臉識別的一切模型和技巧,都可以用在句子相似度模型上。?
作為分類問題
事實上,前面說的 triplet loss,也是訓練人臉識別模型的標準方法之一。triplet loss 本身沒有錯,反而,如果能精調參數并且重新訓練,它效果還可能非常好。只是在很多情況下,它實在是太低效了。當前,更標準的做法是:視為一個多分類問題。?
比如,假設訓練集里邊有 10 萬個不同的人,每個人 5 張人臉圖,那么就有 50 萬張訓練圖片了。然后我們訓練一個 CNN 模型,對圖片提取特征,并構建一個 10 萬分類的模型。沒錯,就是跟 mnist 一樣的分類問題,只不過這時候分類數目大得多,有多少個不同的人就有多少類。那么,句子相似度問題也可以這樣做,可以將訓練集劃分為很多組“同義句”,然后有多少組就有多少類,也將句子相似度問題當作分類問題來做。?
注意,這僅僅是訓練,最后訓練出來的分類模型可能毫無用處。這不難想象,我們可以用已有的人臉數據庫來訓練一個人臉識別模型,但我們的使用場景可能是公司打卡,也就是說要識別的人臉是公司內部的員工臉,他們顯然不會在公開的人臉數據庫中。所以分類模型是沒有意義的,真正有意義的是分類之前的特征提取模型。比如,一個典型的 CNN 分類模型可以簡寫為兩步:
其中 x 是輸入,p 是每一類的概率輸出,這里的 softmax 不用加偏置項。作為一個分類問題訓練時,我們輸出的是人臉圖片 x 和對應的 one hot 標簽 p,但是在使用的時候,我們不用整個模型,我們只用 CNN(x) 這部分,這部分負責將每一張人臉圖片轉化為一個固定長度的向量。?
有了這個轉化模型(編碼器,encoder),不管什么場景下,我們都可以對新人臉進行編碼,然后轉化為這些編碼向量之間的比較,從而就不依賴原來的分類模型。所以,分類模型是一個訓練方案,一旦訓練完成,它就功成身退了,留下的是編碼模型。
分類與排序
這樣就可以了?還沒有。前面說到,我們真正要做的是一個特征提取模型(編碼器),并且用分類模型作為訓練方案,而最后使用的方法是對特征提取模型的特征進行對比排序。?
我們要做特征排序,但是借助分類模型訓練,這兩者等價嗎??
答案是:相關但不等價。分類問題是怎么做的呢?直觀來看,它是選定了一些類別中心,然后說:每個樣本都屬于距離它最近的中心的那一類。?
當然這些類別中心也是訓練出來的,而這里的“距離”可以有多種可能性,比如歐式距離、cos 值、內積都可以,一般的 softmax 對應的就是內積。分類問題的這種做法,就導致了下面的可能的分類結果:
▲?一種可能的分類結果,其中紅色點代表類別中心,其他點代表樣本
這個分類結果有什么問題呢?我們留意圖上的 z1,z2,z3 三個樣本,其中 z1,z3 距離 c1 最近,所以它們是類別 1 的,z2 距離 c2 最近,所以它是類別 2 的,假設這個分類沒有錯,也就是說 z1,z3 它們可能是同義句,z2 跟它們不是同義的,又或者 z1,z3 是同一個人的人臉圖,而 z2 則是另一個人的。
從分類角度,這結果很合理,但我們已經說過,我們最終不要分類模型,我們需要特征之間的比較。這樣問題就很明顯了:z1,z2 距離這么近,卻不是同一類的,z1 跟 z3 距離這么遠,卻是同一類的。如果我們用特征排序的方法給 z1 找一個同義句,那么就會找到 z2 而不是 z3。
Loss
上面說的,就是分類與排序的不等價性,當然,從圖上也可以看出,盡管不完全等價,分類模型還是給了大部分的特征一個合理的位置分布,只是在邊緣附近的特征,就可能出現問題。
Margin Softmax
可以想象,問題出現在分類邊界附近的那些點上面,而出現問題的原因,其實就是分類條件過于寬松,如果加強一下分類條件,就可以提升排序效果了,比如改為:每個樣本與它所屬類的距離,必須小于它跟其他類的距離的 1/2。?
原來我們只需要小于它與其他類的距離,現在不但要這樣,還要小于其它距離的一半,顯然條件加強了,而前一個圖所示的分類結果就不夠好了,因為雖然如圖有 ‖z1?c1‖<‖z1?c2‖,但是沒有做到 ‖z1?c1‖<1/2‖z1?c2‖,所以還需要進一步優化 loss。?
假如按照這個條件訓練完成后,我們可以想象,這時候 z1,z2 的距離就被拉大了,而 z1,z3 的距離就被縮小了,這正是我們所希望的結果:增大類間距離,縮小類內距離。?
事實上,上面所說的方案,可以說就是人臉識別中很著名的方案 l-softmax [3]。人臉識別領域中,很多類似的 loss 被提出來,它們都是針對上述分類問題與排序問題的不等價性設計出來的,比如 a-softmax、am-softmax、aam-softmax等,它們都統稱 margin softmax。而且,不僅有 margin softmax,還有 center loss,還有 triplet loss 的一些改進版本等等。
am-softmax
我不是做圖像的,因此人臉識別的故事我就講不下去了,還是回到本文的正題。上面說到人臉識別不能用純粹的 softmax 分類,必須要用 margin softmax,而因為句子相似度模型和人臉識別模型的相似性,告訴我們句子相似度模型也是需要 margin softmax 的。總而言之,至少要挑一個 margin softmax 來實現呀。?
其中,效果比較好而最容易實現的方案,當數 am-softmax,本文就以它為例子來介紹這一類 margin softmax 的實現方案,最終實現一個句子相似度模型。
am-softmax的做法其實很簡單,原來 softmax 是 p=softmax(zW),設:
那么 softmax 可以重新寫為:
然后 loss 取交叉熵,也就是:
t 為目標標簽。而 am-softmax 做了兩件事情:
1. 將 z 和 ci 都做 l2 歸一化,也就是說,內積變成了 cos 值;
2. 對目標 cos 值減去一個正數 m,然后做比例縮放 s。即 loss 變為:
其中 θi 代表 z,ci 的夾角。在 am-softmax 原論文中,所使用的是 s=30,m=0.35。
從 am-softmax 中,我們可以看到針對前面所提的問題的解決方案了。首先,s 的存在是必要的,因為 cos 的范圍是 [?1,1],需要做好比例縮放,才允許 pt 能足夠接近于 1(有必要的話)。當然,s 并不改變相對大小,因此這不是核心改變,核心是原來應該是 cosθt 的地方,換成了 cosθt?m。
隨心所欲地margin?
前面提到,從分類問題到特征排序的不完全等價性,可以通過加強分類條件來解決,所謂加強,其實意思很簡單,就是用一個新的函數 ψ(θt) 來代替 cosθt,只要:
我們都可以認為是一種加強,而 am-softmax 則是取 ψ(θt)=cosθt?m,這估計是滿足上式的最簡單粗暴的方案了(幸好,它效果也很好)。
理解了這種思想之后,其實我們可以構造各種各樣的 ψ(θt) 了,畢竟理論上滿足 (6) 式的都可以選取。前面我們也提到了 l-softmax 和 a-softmax,它們相當于選擇了 ψ(θt)=cosmθt,其中 m 是一個整數。
但我們知道,cosmθt<cosθt 并非總是成立的,所以論文中基于 cosmθt 構造了一個分段函數出來,顯得特別麻煩,而且也使得模型極難收斂。事實上,我試驗過下面的方式:
結果媲美 am-softmax(在句子相似度任務上)。所以,上述可以作為 l-softmax 和 a-softmax 的一個簡單的替代品了吧,我稱為 simpler-a-softmax,有興趣的讀者可以試試在人臉上的效果。
實現
最后介紹本文對這些 loss 在 Keras 下的實現。測試環境的 Python 版本為 2.7,Keras 版本為 2.1.5,TensorFlow 后端。?
基本實現
用最基本的方式實現 am-softmax 并不困難,比如:
from?keras.layers?import?*
import?keras.backend?as?K
from?keras.constraints?import?unit_norm
x_in?=?Input(shape=(maxlen,))
x_embedded?=?Embedding(len(chars)+2,
???????????????????????word_size)(x_in)
x?=?CuDNNGRU(word_size)(x_embedded)
x?=?Lambda(lambda?x:?K.l2_normalize(x,?1))(x)
pred?=?Dense(num_train,
?????????????use_bias=False,
?????????????kernel_constraint=unit_norm())(x)
encoder?=?Model(x_in,?x)?#?最終的目的是要得到一個編碼器
model?=?Model(x_in,?pred)?#?用分類問題做訓練
def?amsoftmax_loss(y_true,?y_pred,?scale=30,?margin=0.35):
????y_pred?=?y_true?*?(y_pred?-?margin)?+?(1?-?y_true)?*?y_pred
????y_pred?*=?scale
????return?K.categorical_crossentropy(y_true,?y_pred,?from_logits=True)
model.compile(loss=amsoftmax_loss,
??????????????optimizer='adam',
??????????????metrics=['accuracy'])
Sparse版實現
上面的代碼并不難理解,主要基于 y_true 是目標的 one hot 輸入,這樣一來,可以通過普通的乘法來取出目標的 cos 值,減去 margin 后再補回其他部分。?
如果僅僅是玩個 mnist 這樣的 10 分類,那么上述代碼完全足夠了。但在人臉識別或句子相似度場景,我們面對的事實上是數萬分類甚至數十萬的分類,這種情況下如果還是用 one ho t輸入,就顯得非常消耗內存了(主要是準備數據時也麻煩一些)。
理想情況下,我們希望 y_true 只要輸入對應分類的整數id。對于普通的交叉熵,Keras 也提供了 sparse_categorical_crossentropy 的方案,便是應對這種需求,那么 am-softmax 能不能寫個 Sparse 版出來呢??
一種比較簡單的寫法是,將轉換 one hot 的過程寫入到 loss 中,比如:
????y_true?=?K.cast(y_true[:,?0],?'int32')?#?保證y_true的shape=(None,),?dtype=int32
????y_true?=?K.one_hot(y_true,?K.int_shape(y_pred)[-1])?#?轉換為one?hot
????y_pred?=?y_true?*?(y_pred?-?margin)?+?(1?-?y_true)?*?y_pred
????y_pred?*=?scale
????return?K.categorical_crossentropy(y_true,?y_pred,?from_logits=True)
這樣確實能達成目的,但只不過對問題進行了轉嫁,并沒有真正跳過轉 one hot。我們可以用 TensorFlow 的 gather_nd 函數,來實現真正地跳過轉 one hot 的過程,下面是參考的代碼:
????y_true?=?K.expand_dims(y_true[:,?0],?1)?#?保證y_true的shape=(None,?1)
????y_true?=?K.cast(y_true,?'int32')?#?保證y_true的dtype=int32
????batch_idxs?=?K.arange(0,?K.shape(y_true)[0])
????batch_idxs?=?K.expand_dims(batch_idxs,?1)
????idxs?=?K.concatenate([batch_idxs,?y_true],?1)
????y_true_pred?=?K.tf.gather_nd(y_pred,?idxs)?#?目標特征,用tf.gather_nd提取出來
????y_true_pred?=?K.expand_dims(y_true_pred,?1)
????y_true_pred_margin?=?y_true_pred?-?margin?#?減去margin
????_Z?=?K.concatenate([y_pred,?y_true_pred_margin],?1)?#?為計算配分函數
????_Z?=?_Z?*?scale?#?縮放結果,主要因為pred是cos值,范圍[-1,?1]
????logZ?=?K.logsumexp(_Z,?1,?keepdims=True)?#?用logsumexp,保證梯度不消失
????logZ?=?logZ?+?K.log(1?-?K.exp(scale?*?y_true_pred?-?logZ))?#?從Z中減去exp(scale?*?y_true_pred)
????return?-?y_true_pred_margin?*?scale?+?logZ
這個代碼會比前一個帶 one hot 的代碼要略微快一些。實現的關鍵是用 tf.gather_nd 把目標列提取出來,然后用 logsumexp 計算對數配分函數,這估計是實現交叉熵的標準方法了。基于此,可以修改為其它形式的 margin softmax loss。現在就可以像 sparse_categorical_crossentropy 一樣只輸入類別 id 了,其它框架也可以參照著實現。?
效果預覽
一個完整的句子相似度模型可以在這里瀏覽:?
https://github.com/bojone/margin-softmax/blob/master/sent_sim.py?
這是一個基于字的模型,所用到的語料 tongyiju.csv 如圖(語料不共享,需要運行的讀者請自行按照格式準備語料):
▲?句子相似度語料格式
其中前面的 id 表示句子組別,用 \t 隔開,同一組的句子可以認為都是同一句,不同組的句子則是非同義句。
訓練結果:訓練集的分類問題上,能達到 90%+ 的準確率,而驗證集(evaluate 函數)上,幾種 loss 的 top1、top5、top10 的準確率分別為(沒有精細調參):
值得一提的是,evaluate 函數完全是按照真實的使用環境測試的,也就是說,驗證集的每一個句子都沒有出現過在訓練集中,運行 evaluate 函數時,僅僅是在驗證集內部進行排序,如果按相似度排序后的前 n 個句子中出現了輸入句子的同義句,那么 top n 的命中數就加 1。
因此,這樣看來,準確率是很可觀的,能滿足工程使用了。下面是隨便挑幾個匹配的例子:
結論
本文闡述了筆者對句子相似度模型的理解,認為它的最佳做法并非二分類,也并非 triplet loss,而是模仿人臉識別中的 margin loss 來做,這是能最快速提升效果的方案。當然,我并沒有充分比較各種方法,僅僅是從我自己對人臉識別的粗淺了解中覺得應該是那樣。歡迎讀者測試并一同討論。
參考文獻
[1].?Paul Neculoiu, Maarten Versteegh, Mihai Rotaru: Learning Text Similarity with Siamese Recurrent Networks. Rep4NLP@ACL 2016: 148-157
[2].?Feng, Minwei, et al. "Applying deep learning to answer selection: A study and an open task." 2015 IEEE Workshop on Automatic Speech Recognition and Understanding (ASRU). IEEE, 2015.
[3].?Liu W, Wen Y, Yu Z, et al. Large-Margin Softmax Loss for Convolutional Neural Networks[C]//Proceedings of The 33rd International Conference on Machine Learning. 2016: 507-516.
點擊以下標題查看作者其他文章:?
從無監督構建詞庫看「最小熵原理」
基于CNN的閱讀理解式問答模型:DGCNN
再談最小熵原理:飛象過河之句模版和語言結構
再談變分自編碼器VAE:從貝葉斯觀點出發
變分自編碼器VAE:這樣做為什么能成?
全新視角:用變分推斷統一理解生成模型
▲?戳我查看招募詳情
#作 者 招 募#
暑假出去浪?不如來和我們一起寫論文筆記!
關于PaperWeekly
PaperWeekly 是一個推薦、解讀、討論、報道人工智能前沿論文成果的學術平臺。如果你研究或從事 AI 領域,歡迎在公眾號后臺點擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。
▽ 點擊 |?閱讀原文?| 查看作者博客
總結
以上是生活随笔為你收集整理的基于GRU和am-softmax的句子相似度模型 | 附代码实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 立足前沿 直击热点 搭建平台,2018中
- 下一篇: 回归理性 务实推进 迎接AI新时代