之前看到過一篇文章,通過提取文章中對話的人物,分析人物之間的關(guān)系,很好奇如何通過編程的方式知道一句話是誰說的。但是遍搜網(wǎng)絡(luò)沒有發(fā)現(xiàn)類似的研究。?
前段時(shí)間看到一個(gè)微信里的讀書小程序,將人物對話都提取出來,將一本書的內(nèi)容通過微信對話的方式表達(dá)出來,通過將對話的主角替換成讀者的微信號以及用戶頭像,從而增加讀者的代入感。試了之后非常佩服程序作者的巧思。這使得我寫一個(gè)自然語言處理程序,提取書中對話,以及對話人物的念頭更加強(qiáng)烈。?
之前并沒有多少 NLP 的經(jīng)驗(yàn),只零碎試過用 LSTM 訓(xùn)練寫唐詩,用 jieba 做分詞,用 Google 的 gensim 在 WikiPedia 中文語料上訓(xùn)練詞向量。最近 Google 的 BERT 模型很火,運(yùn)行了 BERT 的?SQuAD 閱讀理解與問答系統(tǒng),分類器以及特征提取例子之后,覺得這個(gè)任務(wù)可以用 BERT 微調(diào)來完成,在這里記錄實(shí)驗(yàn)的粗略步驟,與君共勉。?
我把訓(xùn)練數(shù)據(jù)和準(zhǔn)備數(shù)據(jù)的腳本開源,放在 GitLab 上,開放下載。
該目錄包含以下內(nèi)容:
-
用于提取對話人物語境的腳本 conversation_extraction.ipynb;
-
輔助打標(biāo)簽的腳本 label_data_by_click_buttons.ipynb;
-
提取出的語境文件:honglou.py;
-
打過標(biāo)簽的訓(xùn)練數(shù)據(jù):label_honglou.txt;
-
從打過標(biāo)簽的數(shù)據(jù)合成百萬級別新數(shù)據(jù)的腳本:augment_data.py;
-
將訓(xùn)練數(shù)據(jù)轉(zhuǎn)換為 BERT/SQUAD 可讀的腳本:prepare_squad_data.py;
-
預(yù)測結(jié)果文件:res.txt(使用 36000 組數(shù)據(jù)訓(xùn)練后的預(yù)測結(jié)果);?
-
預(yù)測結(jié)果文件:res_1p2million.txt(使用 120萬 組數(shù)據(jù)訓(xùn)練后的預(yù)測結(jié)果)。?
對比之后發(fā)現(xiàn)使用更多的數(shù)據(jù)訓(xùn)練所提升的效果有限,比較大的提升是后者在沒有答案時(shí),輸出是輸入的完整拷貝。?
BERT/SQuAD?預(yù)言的結(jié)果可以從 res.txt 里面找到。?
準(zhǔn)備訓(xùn)練數(shù)據(jù)
《紅樓夢》中的對話很好提取,大部分對話都有特定的格式,即一段話從:“開始,從”結(jié)束。使用 Python 的正則表達(dá)式,可以很容易提取所有滿足這樣條件的對話。
如果假設(shè)說出這段話的人的名字出現(xiàn)在這段話的前面,那么可以用這段話前面的一段話作為包含說話人(speaker)的上下文(context)。如果說話人不存在這段上下文中,標(biāo)簽為空字符串。
下面是第一步提取出的數(shù)據(jù)示例:
{'istart':?414,?'iend':?457,?'talk':?'原來如此,下愚不知.但那寶玉既有如此的來歷,又何以情迷至此,復(fù)又豁悟如此?還要請教。',?'context':?'雨村聽了,雖不能全然明白,卻也十知四五,便點(diǎn)頭嘆道:'},
{'istart':?463,?'iend':?526,?'talk':?'此事說來,老先生未必盡解.太虛幻境即是真如福地.一番閱冊,原始要終之道,歷歷生平,如何不悟?仙草歸真,焉有通靈不復(fù)原之理呢!',?'context':?'士隱笑道:'},
{'istart':?552,?'iend':?588,?'talk':?'寶玉之事既得聞命,但是敝族閨秀如此之多,何元妃以下算來結(jié)局俱屬平常呢?',?'context':?'雨村聽著,卻不明白了.知仙機(jī)也不便更問,因又說道:'},
{'istart':?880,?'iend':?891,?'talk':?'此系后事,未便預(yù)說。',?'context':?'士隱微微笑道:'},
{'istart':?19,?'iend':?45,?'talk':?'老先生草庵暫歇,我還有一段俗緣未了,正當(dāng)今日完結(jié)。',?'context':?'食畢,雨村還要問自己的終身,士隱便道:'},
{'istart':?52,?'iend':?68,?'talk':?'仙長純修若此,不知尚有何俗緣?',?'context':?'雨村驚訝道:'},
{'istart':?51,?'iend':?77,?'talk':?'大士,真人,恭喜,賀喜!情緣完結(jié),都交割清楚了么?',?'context':?'這士隱自去度脫了香菱,送到太虛幻境,交那警幻仙子對冊,剛過牌坊,見那一僧一道,縹渺而來.士隱接著說道:'},
{'istart':?75,?'iend':?243,?'talk':?'我從前見石兄這段奇文,原說可以聞世傳奇,所以曾經(jīng)抄錄,但未見返本還原.不知何時(shí)復(fù)有此一佳話,方知石兄下凡一次,磨出光明,修成圓覺,也可謂無復(fù)遺憾了.只怕年深日久,字跡模糊,反有舛錯(cuò),不如我再抄錄一番,尋個(gè)世上無事的人,托他傳遍,知道奇而不奇,俗而不俗,真而不真,假而不假.或者塵夢勞人,聊倩鳥呼歸去,山靈好客,更從石化飛來,亦未可知。',?'context':?'這一日空空道人又從青埂峰前經(jīng)過,見那補(bǔ)天未用之石仍在那里,上面字跡依然如舊,又從頭的細(xì)細(xì)看了一遍,見后面偈文后又歷敘了多少收緣結(jié)果的話頭,便點(diǎn)頭嘆道:'},
大部分?jǐn)?shù)據(jù)的上下文都很簡單,比如‘士隱笑道:’等,但也有比較復(fù)雜的語境,比如‘這一日空空道人又從青埂峰前經(jīng)過,見那補(bǔ)天未用之石仍在那里,上面字跡依然如舊,又從頭的細(xì)細(xì)看了一遍,見后面偈文后又歷敘了多少收緣結(jié)果的話頭,便點(diǎn)頭嘆道:’。
手動標(biāo)記數(shù)據(jù)
為了訓(xùn)練機(jī)器,讓它知道我想讓它干什么,必須手動標(biāo)記一些數(shù)據(jù)。我在 Jupyter notebook 下寫了一個(gè)簡單的 GUI 程序,將每段話變成按鈕,只需要點(diǎn)擊需要標(biāo)記數(shù)據(jù)的句首和句尾,程序會自動計(jì)算標(biāo)記數(shù)據(jù)在上下文中的位置,并將記錄保存到文本中。
花了兩個(gè)多小時(shí),標(biāo)記了大約 1500 多個(gè)數(shù)據(jù),這些數(shù)據(jù)的最后幾個(gè)例子如下:
{'uid':?1552,?'context':?'黛玉又道:',?'speaker':?'黛玉',?'istart':?0,?'iend':?2}
{'uid':?1553,?'context':?'因念云:',?'speaker':?None,?'istart':?-1,?'iend':?0}
{'uid':?1554,?'context':?'寶釵道:',?'speaker':?'寶釵',?'istart':?0,?'iend':?2}
{'uid':?1555,?'context':?'五祖便將衣缽傳他.今兒這偈語,亦同此意了.只是方才這句機(jī)鋒,尚未完全了結(jié),這便丟開手不成?"黛玉笑道:',?'speaker':?'黛玉',?'istart':?46,?'iend':?48}
{'uid':?1556,?'context':?'寶玉自己以為覺悟,不想忽被黛玉一問,便不能答,寶釵又比出"語錄"來,此皆素不見他們能者.自己想了一想:',?'speaker':?'寶玉',?'istart':?0,?'iend':?2}
{'uid':?1557,?'context':?'想畢,便笑道:',?'speaker':?None,?'istart':?-1,?'iend':?0}
{'uid':?1558,?'context':?'說著,四人仍復(fù)如舊.忽然人報(bào),娘娘差人送出一個(gè)燈謎兒,命你們大家去猜,猜著了每人也作一個(gè)進(jìn)去.四人聽說忙出去,至賈母上房.只見一個(gè)小太監(jiān),拿了一盞四角平頭白紗燈,專為燈謎而制,上面已有一個(gè),眾人都爭看亂猜.小太監(jiān)又下諭道:',?'speaker':?'小太監(jiān)',?'istart':?103,?'iend':?106}
{'uid':?1559,?'context':?'太監(jiān)去了,至晚出來傳諭:',?'speaker':?'太監(jiān)',?'istart':?0,?'iend':?2}
1500 個(gè)數(shù)據(jù)太少了,為了增加數(shù)據(jù)量,我又做了 data augmentation,將 1500 多個(gè) speaker 插入到 1500 多個(gè)語境中,憑空生成了 200多萬對訓(xùn)練數(shù)據(jù)。所以在訓(xùn)練數(shù)據(jù)中,有一些非常搞笑的內(nèi)容,比如:?
說畢走來,只見寶玉拄著拐棍,在當(dāng)?shù)亓R襲人:?
這個(gè)訓(xùn)練例子中的寶玉,原文應(yīng)該是李嬤嬤。
訓(xùn)練過程
簡單構(gòu)造 SQUAD 的中文訓(xùn)練和測試數(shù)據(jù),訓(xùn)練并預(yù)測,結(jié)果輸出在 predictions.json 中。?
訓(xùn)練數(shù)據(jù)的 json 格式如下:?
{"data"?:?[{"title":?"紅樓夢",?"paragraphs":[{context?and?qas?item?1},?{context?and?qas?item?2},?...?{context?and?qas?item?i},?...,?{context?and?qas?item?n}]},
{"title":?"尋秦記",?"paragraphs":[{},?{},?{}]},
{"title":?"xxxxxx",?"paragraphs":[{},?{},?{}]}],
"version"?:?"speaker1.0"}
輸入數(shù)據(jù)是個(gè)字典,包含 “data” 和 “version” 兩個(gè)鍵值。data 是個(gè)數(shù)組,里面的每一項(xiàng)對應(yīng)一本書,以及這本書中的的「語境,問題,答案」字典列表。?
對于每個(gè)「語境,問題,答案」,其格式又如下:
{context?and?qas?item?1}?=?
{"context":?"正鬧著,賈母遣人來叫他吃飯,方往前邊來,胡亂吃了半碗,仍回自己房中.只見襲人睡在外頭炕上,麝月在旁邊抹骨牌.寶玉素知麝月與襲人親厚,一并連麝月也不理,揭起軟簾自往里間來.麝月只得跟進(jìn)來.平兒便推他出去,說:",
"qas"?:?[?{"answers":[{"answer_start":?46,?"text":"平兒"}],
"question":?"接下來一句話是誰說的",
"id":?"index"},
{question?answer?pair?2},
...,?{question?answer?pair?n}]
}
在這次嘗試中,我只使用了經(jīng)過 Data Augmentation 生成的 200 多萬組數(shù)據(jù)中的 36000 組做訓(xùn)練。BERT 的 SQUAD 訓(xùn)練腳本 test_squad.sh 設(shè)置基本沒改變,最大的改變是 max_seq_length=128,以及訓(xùn)練數(shù)據(jù)測試數(shù)據(jù)文件所在位置及內(nèi)容。
export?BERT_BASE_DIR="pathto/chinese_L-12_H-768_A-12"
export?SQUAD_DIR="pathto/squad_data_chinese"
python?pathto/run_squad.py?
--vocab_file=$BERT_BASE_DIR/vocab.txt?
--bert_config_file=$BERT_BASE_DIR/bert_config.json?
--init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt?
--do_train=True?
--train_file=$SQUAD_DIR/chinese_speaker_squad.json?
--do_predict=True?
--predict_file=$SQUAD_DIR/chinese_speaker_squad_valid.json?
--train_batch_size=12?
--learning_rate=3e-5?
--num_train_epochs=2.0?
--max_seq_length=128?
--doc_stride=128?
--output_dir=pathto/squad_data_chinese
預(yù)測結(jié)果
因?yàn)?BERT 在維基百科的大量中文語料上做過訓(xùn)練,已經(jīng)掌握了中文的基本規(guī)律。而少量的訓(xùn)練數(shù)據(jù)微調(diào),即可讓 BERT 知道它所需要處理的任務(wù)類型。
通過簡單的閱讀理解與問答訓(xùn)練,說話人提取的任務(wù)效果驚人,雖然還沒有人工完全驗(yàn)證提取結(jié)果的正確性,但是從語境和答案對看來,大部分結(jié)果無差錯(cuò)。
總共數(shù)據(jù)是 10683 條,打了標(biāo)簽的訓(xùn)練數(shù)據(jù)是前面的 1500 多條。下面將預(yù)測的 10683 條中從后往前數(shù)的部分預(yù)測結(jié)果列出。
想了一回,也覺解了好些.又想到襲人身上:?|||?襲人(此預(yù)測結(jié)果?)
那日薛姨媽并未回家,因恐寶釵痛哭,所以在寶釵房中解勸.那寶釵卻是極明理,思前想后,寶玉原是一種奇異的人.夙世前因,自有一定,原無可怨天尤人.了.薛姨媽心里反倒安了,便到王夫人那里先把寶釵的話說了.王夫人點(diǎn)頭嘆道:?|||?王夫人
說著,更又傷心起來.薛姨媽倒又勸了一會子,因又提起襲人來,說:?|||?薛姨媽
王夫人道:?|||?王夫人
薛姨媽道:?|||?薛姨媽
王夫人聽了道:?|||?王夫人
薛姨媽聽了點(diǎn)頭道:?|||?薛姨媽
看見襲人淚痕滿面,薛姨媽便勸解譬喻了一會.W襲人本來老實(shí),不是伶牙利齒的人,薛姨媽說一句,他應(yīng)一句,回來說道:?|||?薛姨媽?(此結(jié)果從語境看不出是否正確)
過了幾日,賈政回家,眾人迎接.賈政見賈赦賈珍已都回家,弟兄叔侄相見,大家歷敘別來的景況.然后內(nèi)眷們見了,不免想起寶玉來,又大家傷了一會子心.賈政喝住道:?|||?賈政
次日賈政進(jìn)內(nèi),請示大臣們,說是:?|||?賈政
回到家中,賈璉賈珍接著,賈政將朝內(nèi)的話述了一遍,眾人喜歡.賈珍便回說:?|||?賈珍
賈政并不言語,隔了半日,卻吩咐了一番仰報(bào)天恩的話.賈璉也趁便回說:?|||?賈璉
賈政昨晚也知巧姐的始末,便說:?|||?賈政
賈璉答應(yīng)了"是",又說:?|||?賈璉
賈政道:?|||?賈政
賈政說畢進(jìn)內(nèi).賈璉打發(fā)請了劉姥姥來,應(yīng)了這件事.劉姥姥見了王夫人等,便說些將來怎樣升官,怎樣起家,怎樣子孫昌盛.正說著,丫頭回道:?|||?丫頭
王夫人問幾句話,花自芳的女人將親戚作媒,說的是城南蔣家的,現(xiàn)在有房有地,又有鋪面,姑爺年紀(jì)略大了幾歲,并沒有娶過的,況且人物兒長的是百里挑一的.王夫人聽了愿意,說道:?|||?王夫人
王夫人又命人打聽,都說是好.王夫人便告訴了寶釵,仍請了薛姨媽細(xì)細(xì)的告訴了襲人.襲人悲傷不已,又不敢違命的,心里想起寶玉那年到他家去,回來說的死也不回去的話,"如今太太硬作主張.若說我守著,又叫人說我不害臊,若是去了,實(shí)不是我的心愿",便哭得咽哽難鳴,又被薛姨媽寶釵等苦勸,回過念頭想道:?|||?薛姨媽寶釵(此預(yù)測結(jié)果?)
于是,襲人含悲叩辭了眾人,那姐妹分手時(shí)自然更有一番不忍說.襲人懷著必死的心腸上車回去,見了哥哥嫂子,也是哭泣,但只說不出來.那花自芳悉把蔣家的娉禮送給他看,又把自己所辦妝奩一一指給他瞧,說那是太太賞的,那是置辦的.襲人此時(shí)更難開口,住了兩天,細(xì)想起來:?|||?襲人
不言襲人從此又是一番天地.且說那賈雨村犯了婪索的案件,審明定罪,今遇大赦,褫籍為民.雨村因叫家眷先行,自己帶了一個(gè)小廝,一車行李,來到急流津覺迷渡口.只見一個(gè)道者從那渡頭草棚里出來,執(zhí)手相迎.雨村認(rèn)得是甄士隱,也連忙打恭,士隱道:?|||?士隱
雨村道:?|||?雨村
甄士隱道:?|||?甄士隱
雨村欣然領(lǐng)命,兩人攜手而行,小廝驅(qū)車隨后,到了一座茅庵.士隱讓進(jìn)雨村坐下,小童獻(xiàn)上茶來.雨村便請教仙長超塵的始末.士隱笑道:?|||?士隱
雨村道:?|||?雨村
士隱道:?|||?士隱
雨村驚訝道:?|||?雨村
士隱道:?|||?士隱
雨村道:?|||?雨村
士隱道:?|||?士隱
雨村聽了,雖不能全然明白,卻也十知四五,便點(diǎn)頭嘆道:?|||?雨村
士隱笑道:?|||?士隱
雨村聽著,卻不明白了.知仙機(jī)也不便更問,因又說道:?|||?雨村聽著,卻不明白了.知仙機(jī)(此預(yù)測結(jié)果?)
士隱嘆息道:?|||?士隱
雨村聽到這里,不覺拈須長嘆,因又問道:?|||?雨村
士隱道:?|||?士隱
雨村低了半日頭,忽然笑道:?|||?雨村
士隱微微笑道:?|||?士隱
食畢,雨村還要問自己的終身,士隱便道:?|||?士隱
雨村驚訝道:?|||?雨村
士隱道:?|||?士隱
這士隱自去度脫了香菱,送到太虛幻境,交那警幻仙子對冊,剛過牌坊,見那一僧一道,縹渺而來.士隱接著說道:?|||?士隱
那僧說:?|||?那僧
這一日空空道人又從青埂峰前經(jīng)過,見那補(bǔ)天未用之石仍在那里,上面字跡依然如舊,又從頭的細(xì)細(xì)看了一遍,見后面偈文后又歷敘了多少收緣結(jié)果的話頭,便點(diǎn)頭嘆道:?|||?空空道人
想畢,便又抄了,仍袖至那繁華昌盛的地方,遍尋了一番,不是建功立業(yè)之人,即系饒口謀衣之輩,那有閑情更去和石頭饒舌.直尋到急流津覺迷度口,草庵中睡著一個(gè)人,因想他必是閑人,便要將這抄錄的《石頭記》給他看看.那知那人再叫不醒.空空道人復(fù)又使勁拉他,才慢慢的開眼坐起,便草草一看,仍舊擲下道:?|||?空空道人
空空道人忙問何人,那人道:?|||?那人
那空空道人牢牢記著此言,又不知過了幾世幾劫,果然有個(gè)悼紅軒,見那曹雪芹先生正在那里翻閱歷來的古史.空空道人便將賈雨村言了,方把這《石頭記》示看.那雪芹先生笑道:?|||?雪芹先生
空空道人便問:?|||?空空道人
曹雪芹先生笑道:?|||?曹雪芹先生
那空空道人聽了,仰天大笑,擲下抄本,飄然而去.一面走著,口中說道:?|||?空空道人
結(jié)果分析:大部分簡單的語境,BERT 都可以正確的預(yù)測誰是說話的那個(gè)人,但是有些復(fù)雜一點(diǎn)的,就會出錯(cuò),比如上面這些例子中的:
想了一回,也覺解了好些.又想到襲人身上:?|||?襲人(此預(yù)測結(jié)果?)
王夫人又命人打聽,都說是好.王夫人便告訴了寶釵,仍請了薛姨媽細(xì)細(xì)的告訴了襲人.襲人悲傷不已,又不敢違命的,心里想起寶玉那年到他家去,回來說的死也不回去的話,"如今太太硬作主張.若說我守著,又叫人說我不害臊,若是去了,實(shí)不是我的心愿",便哭得咽哽難鳴,又被薛姨媽寶釵等苦勸,回過念頭想道:?|||?薛姨媽寶釵(此預(yù)測結(jié)果?)
雨村聽著,卻不明白了.知仙機(jī)也不便更問,因又說道:?|||?雨村聽著,卻不明白了.知仙機(jī)(此預(yù)測結(jié)果?)
第三個(gè)錯(cuò)誤最是搞笑,好像機(jī)器還沒有明白“雨村聽著,卻不明白了.知仙機(jī)”并不是一個(gè)人的名字。
下面我再從其他預(yù)言的結(jié)果中挑選了一些看起來不容易預(yù)測,但是機(jī)器正確理解并預(yù)測的例子:?
10575?賈蘭那里肯走.尤氏等苦勸不止.眾人中只有惜春心里卻明白了,只不好說出來,便問寶釵道:?|||?惜春
10183?王夫人已到寶釵那里,見寶玉神魂失所,心下著忙,便說襲人道:?|||?王夫人
王仁便叫了他外甥女兒巧姐過來說:?|||?王仁(下面一句話算誰說的?我也很懵)
9490?正推讓著,寶玉也來請薛姨媽李嬸娘的安.聽見寶釵自己推讓,他心里本早打算過寶釵生日,因家中鬧得七顛八倒,也不敢在賈母處提起,今見湘云等眾人要拜壽,便喜歡道:?|||?寶玉
人物關(guān)系分析
按照相鄰的兩個(gè)說話者極有可能是對話者統(tǒng)計(jì)出紅樓夢中人物關(guān)系如下,寶玉與襲人之間對話最多(178+175),寶玉與黛玉之間對話次之(177+174),寶玉與寶釵之間對話(65+61),僅從對話次數(shù)來看,襲人與黛玉在寶玉心目中的占地差不多,寶釵(65+61)占地只相當(dāng)于黛玉的三分之一,略高于晴雯(46+41)。?
通過這個(gè)例子,深深感覺 Google 的 BERT 預(yù)訓(xùn)練+微調(diào)的自然語言處理模型之強(qiáng)大。很多 NLP 的問題可以轉(zhuǎn)換成 “閱讀理解 + 問答”(SQuAD)的問題。在此寫下假期 3 天做的一個(gè)有趣的嘗試,希望看到更多使用 BERT 開發(fā)出更多好玩的應(yīng)用。
[('寶玉-襲人',?178),
('黛玉-寶玉',?177),
('襲人-寶玉',?175),
('寶玉-黛玉',?174),
('寶玉-寶玉',?137),
('賈母-賈母',?115),
('寶玉-寶釵',?65),
('鳳姐-鳳姐',?64),
('寶釵-寶玉',?61),
('黛玉-黛玉',?59),
('賈母-鳳姐',?57),
('賈政-賈政',?54),
('襲人-襲人',?48),
('寶玉-晴雯',?46),
('賈璉-鳳姐',?46),
('寶釵-黛玉',?45),
('鳳姐-賈母',?44),
('黛玉-寶釵',?42),
('鳳姐-賈璉',?42),
('王夫人-賈母',?41),
('寶玉-賈母',?41),
('晴雯-寶玉',?41),
('王夫人-寶玉',?41),
('賈母-寶玉',?40),
('寶玉-賈政',?39),
('黛玉-紫鵑',?39),
('黛玉-湘云',?38),
('紫鵑-黛玉',?37),
('鳳姐兒-賈母',?35),
('眾人-賈政',?35)]