当Bert遇上Keras:这可能是Bert最简单的打开姿势
作者丨蘇劍林
研究方向丨NLP,神經網絡
個人主頁丨kexue.fm
Bert 是什么,估計也不用筆者來諸多介紹了。雖然筆者不是很喜歡Bert,但不得不說,Bert 確實在 NLP 界引起了一陣軒然大波。現在不管是中文還是英文,關于 Bert 的科普和解讀已經滿天飛了,隱隱已經超過了當年 Word2Vec 剛出來的勢頭了。有意思的是,Bert 是 Google 搞出來的,當年的 word2vec 也是 Google 搞出來的,不管你用哪個,都是在跟著 Google 大佬的屁股跑。
Bert 剛出來不久,就有讀者建議我寫個解讀,但我終究還是沒有寫。一來,Bert 的解讀已經不少了,二來其實 Bert 也就是基于 Attention 搞出來的大規模語料預訓練的模型,本身在技術上不算什么創新,而關于 Google 的 Attention 我已經寫過解讀了,所以就提不起勁來寫了。
▲?Bert的預訓練和微調(圖片來自Bert的原論文)
總的來說,我個人對 Bert 一直也沒啥興趣,直到上個月末在做信息抽取比賽時,才首次嘗試了 Bert。畢竟即使不感興趣,終究也是得學會它,畢竟用不用是一回事,會不會又是另一回事。再加上在 Keras 中使用(fine tune)Bert,似乎還沒有什么文章介紹,所以就分享一下自己的使用經驗。
當Bert遇上Keras
很幸運的是,已經有大佬封裝好了 Keras 版的 Bert,可以直接調用官方發布的預訓練權重,對于已經有一定 Keras 基礎的讀者來說,這可能是最簡單的調用 Bert 的方式了。所謂“站在巨人的肩膀上”,就是形容我們這些 Keras 愛好者此刻的心情了。?
keras-bert
個人認為,目前在 Keras 下對 Bert 最好的封裝是:?
keras-bert:
https://github.com/CyberZHG/keras-bert?
本文也是以此為基礎的。 順便一提的是,除了 keras-bert 之外,CyberZHG 大佬還封裝了很多有價值的 keras 模塊,比如 keras-gpt-2(你可以用像用 Bert 一樣用 GPT2 模型了)、keras-lr-multiplier(分層設置學習率)、keras-ordered-neurons(就是前不久介紹的 ON-LSTM)等等。看來也是一位 Keras 鐵桿粉絲,致敬大佬。
匯總可以看:
https://github.com/CyberZHG/summary?
事實上,有了 keras-bert 之后,再加上一點點 Keras 基礎知識,而且 keras-bert 所給的 demo 已經足夠完善,調用、微調 Bert 都已經變成了意見沒有什么技術含量的事情了。所以后面筆者只是給出幾個中文的例子,來讓讀者上手 keras-bert 的基本用法。?
Tokenizer
正式講例子之前,還有必要先講一下 Tokenizer 相關內容。我們導入 Bert 的 Tokenizer 并重構一下它:
import?codecs
config_path?=?'../bert/chinese_L-12_H-768_A-12/bert_config.json'
checkpoint_path?=?'../bert/chinese_L-12_H-768_A-12/bert_model.ckpt'
dict_path?=?'../bert/chinese_L-12_H-768_A-12/vocab.txt'
token_dict?=?{}
with?codecs.open(dict_path,?'r',?'utf8')?as?reader:
????for?line?in?reader:
????????token?=?line.strip()
????????token_dict[token]?=?len(token_dict)
class?OurTokenizer(Tokenizer):
????def?_tokenize(self,?text):
????????R?=?[]
????????for?c?in?text:
????????????if?c?in?self._token_dict:
????????????????R.append(c)
????????????elif?self._is_space(c):
????????????????R.append('[unused1]')?#?space類用未經訓練的[unused1]表示
????????????else:
????????????????R.append('[UNK]')?#?剩余的字符是[UNK]
????????return?R
tokenizer?=?OurTokenizer(token_dict)
tokenizer.tokenize(u'今天天氣不錯')
#?輸出是?['[CLS]',?u'今',?u'天',?u'天',?u'氣',?u'不',?u'錯',?'[SEP]']
這里簡單解釋一下 Tokenizer 的輸出結果。首先,默認情況下,分詞后句子首位會分別加上 [CLS] 和 [SEP] 標記,其中 [CLS] 位置對應的輸出向量是能代表整句的句向量(反正 Bert 是這樣設計的),而 [SEP] 則是句間的分隔符,其余部分則是單字輸出(對于中文來說)。
本來 Tokenizer 有自己的 _tokenize 方法,我這里重寫了這個方法,是要保證 tokenize 之后的結果,跟原來的字符串長度等長(如果算上兩個標記,那么就是等長再加 2)。?Tokenizer 自帶的 _tokenize 會自動去掉空格,然后有些字符會粘在一塊輸出,導致 tokenize 之后的列表不等于原來字符串的長度了,這樣如果做序列標注的任務會很麻煩。
而為了避免這種麻煩,還是自己重寫一遍好了。主要就是用 [unused1] 來表示空格類字符,而其余的不在列表的字符用 [UNK] 表示,其中 [unused*] 這些標記是未經訓練的(隨即初始化),是 Bert 預留出來用來增量添加詞匯的標記,所以我們可以用它們來指代任何新字符。
三個例子
這里包含 keras-bert 的三個例子,分別是文本分類、關系抽取和主體抽取,都是在官方發布的預訓練權重基礎上進行微調來做的。?
Bert官方Github:
https://github.com/google-research/bert?
官方的中文預訓練權重:
https://storage.googleapis.com/bert_models/2018_11_03/chinese_L-12_H-768_A-12.zip
例子所在Github:
https://github.com/bojone/bert_in_keras/?
根據官方介紹,這份權重是用中文維基百科為語料進行訓練的。?
文本分類
作為第一個例子,我們做一個最基本的文本分類任務,熟悉做這個基本任務之后,剩下的各種任務都會變得相當簡單了。這次我們以之前已經討論過多次的文本感情分類任務 [1] 為例,所用的標注數據 [2] 也是以前所整理的。?
讓我們來看看模型部分全貌,完整代碼見:
https://github.com/bojone/bert_in_keras/blob/master/sentiment.py
for?l?in?bert_model.layers:
????l.trainable?=?True
x1_in?=?Input(shape=(None,))
x2_in?=?Input(shape=(None,))
x?=?bert_model([x1_in,?x2_in])
x?=?Lambda(lambda?x:?x[:,?0])(x)?#?取出[CLS]對應的向量用來做分類
p?=?Dense(1,?activation='sigmoid')(x)
model?=?Model([x1_in,?x2_in],?p)
model.compile(
????loss='binary_crossentropy',
????optimizer=Adam(1e-5),?#?用足夠小的學習率
????metrics=['accuracy']
)
model.summary()
在 Keras 中調用 Bert 來做情感分類任務就這樣寫完了。
是不是感覺還沒有盡興,模型代碼就結束了?Keras 調用 Bert 就這么簡短。事實上,真正調用 Bert 的也就只有 load_trained_model_from_checkpoint 那一行代碼,剩下的只是普通的 Keras 操作(再次感謝 CyberZHG 大佬)。所以,如果你已經入門了 Keras,那么調用 Bert 是無往不利啊。?
如此簡單的調用,能達到什么精度?經過5個epoch的fine tune后,驗證集的最好準確率是95.5%+!之前我們在《文本情感分類(三):分詞 OR 不分詞》[1] 中死調爛調,也就只有 90% 上下的準確率;而用了 Bert 之后,寥寥幾行,就提升了 5 個百分點多的準確率!也難怪 Bert 能在 NLP 界掀起一陣熱潮。
在這里,用筆者的個人經歷先回答讀者可能關心的兩個問題。?
第一個問題應該是大家都很關心的,那就是“要多少顯存才夠?”。事實上,這沒有一個標準答案,顯存的使用取決于三個因素:句子長度、batch size、模型復雜度。像上面的情感分析例子,在筆者的 GTX1060 6G 顯存上也能跑起來,只需要將 batch size 調到 24 即可。
所以,如果你的顯存不夠大,將句子的 maxlen 和 batch size 都調小一點試試。當然,如果你的任務太復雜,再小的 maxlen 和 batch size 也可能 OOM,那就只有升級顯卡了。?
第二個問題是“有什么原則來指導 Bert 后面應該要接哪些層?”。答案是:用盡可能少的層來完成你的任務。
比如上述情感分析只是一個二分類任務,你就取出第一個向量然后加個 Dense(1) 就好了,不要想著多加幾層 Dense,更加不要想著接個 LSTM 再接 Dense;如果你要做序列標注(比如 NER),那你就接個 Dense+CRF 就好,也不要多加其他東西。
總之,額外加的東西盡可能少。一是因為 Bert 本身就足夠復雜,它有足夠能力應對你要做的很多任務;二來你自己加的層都是隨即初始化的,加太多會對 Bert 的預訓練權重造成劇烈擾動,容易降低效果甚至造成模型不收斂。
關系抽取
假如讀者已經有了一定的 Keras 基礎,那么經過第一個例子的學習,其實我們應該已經完全掌握了 Bert 的 fine tune 了,因為實在是簡單到沒有什么好講了。所以,后面兩個例子主要是提供一些參考模式,讓讀者能體會到如何“用盡可能少的層來完成你的任務”。?
在第二個例子中,我們介紹基于 Bert 實現的一個極簡的關系抽取模型,其標注原理跟《基于 DGCNN 和概率圖的輕量級信息抽取模型》[3] 介紹的一樣,但是得益于 Bert 強大的編碼能力,我們所寫的部分可以大大簡化。
在筆者所給出的一種參考實現中,模型部分如下,完整模型見:
https://github.com/bojone/bert_in_keras/blob/master/relation_extract.py
ps2?=?Dense(1,?activation='sigmoid')(t)
subject_model?=?Model([t1_in,?t2_in],?[ps1,?ps2])?#?預測subject的模型
k1v?=?Lambda(seq_gather)([t,?k1])
k2v?=?Lambda(seq_gather)([t,?k2])
kv?=?Average()([k1v,?k2v])
t?=?Add()([t,?kv])
po1?=?Dense(num_classes,?activation='sigmoid')(t)
po2?=?Dense(num_classes,?activation='sigmoid')(t)
object_model?=?Model([t1_in,?t2_in,?k1_in,?k2_in],?[po1,?po2])?#?輸入text和subject,預測object及其關系
train_model?=?Model([t1_in,?t2_in,?s1_in,?s2_in,?k1_in,?k2_in,?o1_in,?o2_in],
????????????????????[ps1,?ps2,?po1,?po2])
如果讀者已經讀過《基于 DGCNN 和概率圖的輕量級信息抽取模型》一文 [3],了解到不用 Bert 時的模型架構,那么就會理解到上述實現是多么的簡介明了。?
可以看到,我們引入了 Bert 作為編碼器,然后得到了編碼序列 t,然后直接接兩個 Dense(1),這就完成了 subject 的標注模型;接著,我們把傳入的 s 的首尾對應的編碼向量拿出來,直接加到編碼向量序列 t 中去,然后再接兩個 Dense(num_classes),就完成 object 的標注模型(同時標注出了關系)。?
這樣簡單的設計,最終 F1 能到多少?答案是:線下 dev 能接近 82%,線上我提交過一次,結果是 85%+(都是單模型)!
相比之下,《基于 DGCNN 和概率圖的輕量級信息抽取模型》[3]?中的模型,需要接 CNN,需要搞全局特征,需要將 s 傳入到 LSTM 進行編碼,還需要相對位置向量,各種拍腦袋的模塊融合在一起,單模型也只比它好一點點(大約 82.5%)。
要知道,這個基于 Bert 的簡單模型我只寫了一個小時就寫出來了,而各種技巧和模型融合在一起的 DGCNN 模型,我前前后后調試了差不多兩個月!Bert 的強悍之處可見一斑。
注:這個模型的 fine tune 最好有 8G 以上的顯存。另外,因為我在比賽即將結束的前幾天才接觸的 Bert,才把這個基于 Bert 的模型寫出來,沒有花心思好好調試,所以最終的提交結果并沒有包含 Bert。
用 Bert 做關系抽取的這個例子,跟前面情感分析的簡單例子,有一個明顯的差別是學習率的變化。情感分析的例子中,只是用了恒定的學習率訓練了幾個 epoch,效果就還不錯了。
在關系抽取這個例子中,第一個 epoch 的學習率慢慢從 0 增加到(這樣稱為 warmup),第二個 epoch 再從降到,總的來說就是先增后減,Bert 本身也是用類似的學習率曲線來訓練的,這樣的訓練方式比較穩定,不容易崩潰,而且效果也比較好。
事件主體抽取
最后一個例子來自 CCKS 2019 面向金融領域的事件主體抽取 [4],這個比賽目前還在進行,不過我也已經沒有什么動力和興趣做下去了,所以放出我現在的模型(準確率為 89%+)供大家參考,祝繼續參賽的選手取得更好的成績。?
簡單介紹一下這個比賽的數據,大概是這樣的:
輸入:“公司 A 產品出現添加劑,其下屬子公司 B 和公司 C 遭到了調查”, “產品出現問題”?
輸出:“公司 A”?
也就是說,這是個雙輸入、單輸出的模型,輸入是一個 query 和一個事件類型,輸出一個實體(有且只有一個,并且是 query 的一個片段)。其實這個任務可以看成是 SQUAD 1.0 [5] 的簡化版,根據這個輸出特性,輸出應該用指針結構比較好(兩個 softmax 分別預測首尾)。剩下的問題是:雙輸入怎么搞??
前面兩個例子雖然復雜度不同,但它們都是單一輸入的,雙輸入怎么辦呢?當然,這里的實體類型只有有限個,直接 Embedding 也行,只不過我使用一種更能體現 Bert 的簡單粗暴和強悍的方案:直接用連接符將兩個輸入連接成一個句子,然后就變成單輸入了!
比如上述示例樣本處理成:?
輸入:“___產品出現問題___公司 A 產品出現添加劑,其下屬子公司 B 和公司 C 遭到了調查”?
輸出:“公司 A”?
然后就變成了普通的單輸入抽取問題了。說到這個,這個模型的代碼也就沒有什么好說的了,就簡單幾行,完整代碼請看:
https://github.com/bojone/bert_in_keras/blob/master/subject_extract.py
ps1?=?Lambda(lambda?x:?x[0][...,?0]?-?(1?-?x[1][...,?0])?*?1e10)([ps1,?x_mask])
ps2?=?Dense(1,?use_bias=False)(x)
ps2?=?Lambda(lambda?x:?x[0][...,?0]?-?(1?-?x[1][...,?0])?*?1e10)([ps2,?x_mask])
model?=?Model([x1_in,?x2_in],?[ps1,?ps2])
另外加上一些解碼的 trick,還有模型融合,提交上去,就可以做到 89%+ 了。在看看目前排行榜,發現最好的結果也就是 90% 多一點點,所以估計大家都差不多是這樣做的了。這個代碼重復實驗時波動比較大,大家可以多跑幾次,取最優結果。
這個例子主要告訴我們,用 Bert 實現自己的任務時,最好能整理成單輸入的模式,這樣一來比較簡單,二來也更加高效。
比如做句子相似度模型,輸入兩個句子,輸出一個相似度,有兩個可以想到的做法,第一種是兩個句子分別過同一個 Bert,然后取出各自的 [CLS] 特征來做分類;第二種就是像上面一樣,用個記號把兩個句子連接在一起,變成一個句子,然后過一個 Bert,然后將輸出特征做分類,后者顯然會更快一些,而且能夠做到特征之間更全面的交互。
文章小結
本文介紹了 Keras 下 Bert 的基本調用方法,其中主要是提供三個參考例子,供大家逐步熟悉 Bert 的 fine tune 步驟和原理。其中有不少是筆者自己閉門造車的經驗之談,如果有所偏頗,還望讀者指正。?
事實上有了 CyberZHG 大佬實現的 keras-bert,在 Keras 下使用 Bert 也就是小菜一碟,大家折騰個半天,也就上手了。最后祝大家用得痛快~
相關鏈接
[1]?https://kexue.fm/archives/3863
[2] https://kexue.fm/archives/3414
[3] https://kexue.fm/archives/6671
[4] https://biendata.com/competition/ccks_2019_4/
[5] https://rajpurkar.github.io/SQuAD-explorer/explore/1.1/dev/
點擊以下標題查看作者其他文章:?
變分自編碼器VAE:原來是這么一回事 | 附源碼
再談變分自編碼器VAE:從貝葉斯觀點出發
變分自編碼器VAE:這樣做為什么能成?
簡單修改,讓GAN的判別器秒變編碼器
深度學習中的互信息:無監督提取特征
全新視角:用變分推斷統一理解生成模型
細水長flow之NICE:流模型的基本概念與實現
細水長flow之f-VAEs:Glow與VAEs的聯姻
深度學習中的Lipschitz約束:泛化與生成模型
#投 稿 通 道#
?讓你的論文被更多人看到?
如何才能讓更多的優質內容以更短路徑到達讀者群體,縮短讀者尋找優質內容的成本呢?答案就是:你不認識的人。
總有一些你不認識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋梁,促使不同背景、不同方向的學者和學術靈感相互碰撞,迸發出更多的可能性。
PaperWeekly 鼓勵高校實驗室或個人,在我們的平臺上分享各類優質內容,可以是最新論文解讀,也可以是學習心得或技術干貨。我們的目的只有一個,讓知識真正流動起來。
??來稿標準:
? 稿件確系個人原創作品,來稿需注明作者個人信息(姓名+學校/工作單位+學歷/職位+研究方向)?
? 如果文章并非首發,請在投稿時提醒并附上所有已發布鏈接?
? PaperWeekly 默認每篇文章都是首發,均會添加“原創”標志
? 投稿郵箱:
? 投稿郵箱:hr@paperweekly.site?
? 所有文章配圖,請單獨在附件中發送?
? 請留下即時聯系方式(微信或手機),以便我們在編輯發布時和作者溝通
?
現在,在「知乎」也能找到我們了
進入知乎首頁搜索「PaperWeekly」
點擊「關注」訂閱我們的專欄吧
關于PaperWeekly
PaperWeekly 是一個推薦、解讀、討論、報道人工智能前沿論文成果的學術平臺。如果你研究或從事 AI 領域,歡迎在公眾號后臺點擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。
▽ 點擊 |?閱讀原文?| 查看作者博客
總結
以上是生活随笔為你收集整理的当Bert遇上Keras:这可能是Bert最简单的打开姿势的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 清华大学人工智能研究院成立智能信息获取研
- 下一篇: 一文带你看懂PaddleHub