米蘭牛角尖

計算語言學的學習紀錄,偶爾可能會出現野生的電影評論?

0%

逐漸發展出成熟人工智慧的人類,藉由電影、小說、文本揮灑著人類的想像,描繪出在後人類時代,身為萬物之靈的我們,該如何自處的觀點與逐漸對未來失去控制的焦慮。在電影《A.I.人工智慧》中,為人類未來描繪出了後末日的想像,在全球暖化後的社會,海平面的上升淹沒了現今仍富有繁榮的曼哈頓,會生老病死的人類也開發出了有感情、同時也超越人類極限的機器人,以彌補內心無法逾越的匱乏,同時卻又害怕機器人影響人類在社會上的生存地位,即秉持著造物主的傲慢,將機器人趕盡殺絕。在電影《模仿遊戲》中,則是回到人工智慧的發展剛伸出幼苗的二戰時期,藉由電腦之父圖靈的眼中,探討人工智慧是否有著思考的能力,以及為即將步入後人類時代的我們,了解人工智慧的起源。而兩部電影皆可以回溯到一個關於人工智慧最基本的核心命題:人工智慧是否會思考?何謂真實?何謂自我?

在電影《模仿遊戲》中,藉由圖靈的引領,走進了名為人工智慧的世界。在電影的某一個片段,圖靈被警探問到的一個問題「機器能像人一樣思考嗎?」,圖靈回答:「不,機器不能思考。然而,就因為機器不能像人一樣思考,就證明機器不會思考嗎?」人類往往害怕自己無法掌控又無法理解的事物,例如會思考的機器,害怕機器有一天會反噬人類的存在以及在這社會的重要性。又例如「同性戀」。在當時的年代(甚至是現代),社會並無法接受同性戀的存在,或許是因為不了解,又或許是因為不願意不想去接納不同的存在,當人類害怕某件事物時,總是會想辦法去摧毀它,因為解決提出問題的人遠比解決問題本身還要容易且省時許多。因此圖靈問道「我們究竟是人?還是機器?」我們常常自詡是萬物之靈,擁有著智慧的我們可以統御世間一切事物,但是自古以來卻一直無情地自相殘殺,納粹希特勒仍然殺了無數猶太人以及同志族群,破壞了許多人的家庭,在這個由人類組成的社會,明明應充滿著仁慈的光輝與人性的溫暖,卻仍然如同機器一般無情地吞噬這世界上與大多數不同的人。究竟是人,還是機器,仍然那麼重要嗎;去探究兩者之間的界線,去釐清何謂真實,何謂虛構,仍然那麼重要嗎?
後現代觀點的哲學家布希亞所提出的「超真實」觀點,當原本生活型態被「仿真體」(simulation)遮蓋並被佔據時,「原真實」已經被「超真實」(hyperreality)給覆蓋過去,而布希亞則舉了迪士尼樂園作為例子,說明作為擬仿物的迪士尼樂園逐漸取代了美國與美國夢,使得大家逐漸誤認迪士尼樂園即為美國夢,擬仿物對原真實產生了反噬。同理,在電影《A.I.人工智慧》前半段,導演史蒂芬史匹柏透過莫妮卡、其親生兒子馬丁、人工智慧機器人大衛之間的三方對話,淋漓盡致地發揮了布希亞的「超真實」觀點。莫妮卡的親生兒子馬丁對仿真體與真實的斷裂做出了詮釋:當馬丁拿著資本主義下大量生產的直升機對大衛說:「這種玩具要把它打破才比較有意思。」這不正是一種對原形不再懷舊的表現嗎?正是因為不再懷舊,才使得無限的生產得以可能,正是因為圓形的大量複製與製造,才使得每件物品皆成為了彼此的等價物,打破的並不僅僅是直升機的形體,而是真實與擬仿物之間的界線。

You don’t look like a toy, you are like an ordinary person.

大衛的媽媽莫妮卡則是一個誤認「超真實」為「真實」的案例。起初莫妮卡對於其丈夫亨利帶回具有感情與同理心的大衛回家藉此替代其瀕死的兒子馬丁的行為感到無法接受與理解,但隨著大衛的種種的行為,例如叫莫妮卡「媽咪」,以及種種渴望愛的行為,讓身為「仿真體」的大衛逐漸遮蓋並佔據了莫妮卡的「原真實」,使得「原真實」被「超真實」壓扁,對於本應是仿真的機器人產生了對人的感情,仿真體與真實間產生了斷裂,讓原本的真實更為真實,也因莫妮卡的同情心,使得馬丁得以脫離原本必須面臨機器人屠宰場命運。

另外,該如何從哲學觀點去判斷誰是人類、誰是機器人呢?我們可以在電影中發現一個有趣的現象:每個機器人都知道自己的存在意義,而且完全不為外在環境所動。被製造出來就是要滿足莫妮卡愛與隸屬需求的大衛,對於母愛的渴求貫穿了整部電影;裘德洛飾演的性̷愛̷愛情機器人即使在被誣陷殺人後,還是保有著極欲求愛的積極性(這是堪入耳的說法);在大衛準備要被抓到機器人屠宰場前碰到的保母機器人,無論受到什麼樣的威脅,還是告訴著大衛不要害怕,一切會好起來,仍善盡著自己身為保母機器人的職責。
這正說明了人類與機器人最關鍵的差異,也就是存在主義的基本命題:存在先於本質(Existence precedes essence)。沙特認為,人在一開始並沒有任何先天的本質,而關於我們自身的一切,例如地位、身份等皆是存在後才被賦予的,因此首先必須先肯定人的存在,再去賦予存在意義,並且甚至超越自己所創造的價值。對於一個人造物而言,在被製造之前,必定會先知道其用途所在。小學美勞課,我們必定都是在腦中對作品先有個雛形,接著再動手去創造。同理,這些機器人在被人類創造出來之前,必定是先有其目的與用途,接著才去製作這些機器人。
身為一個人,我們時時刻刻都在改變甚至超越著自我的本質,前一秒的你跟後一秒的你很可能就代表著截然不同的意義。但在電影《A.I.人工智慧》中的機器人卻始終無法在面對快速變化的世界隨時改變自我價值,還是不斷地重複著自我的使命,直到故障為止。相反地,在《銀翼殺手2049》中,殺手K卻是在劇情的推動之下,自己眼中的世界觀不斷地翻轉,甚至是對自我價值的挑戰,之後再寫一篇文章詳細介紹。
順帶一提,沙特也為「存在先於本質」一說做出結論,人會害怕在眼前的過多選擇中做出錯誤判斷,因而不願意為自己的選擇負責任。對此,沙特說,你是自由的(是不是聽起來很熟悉?),所以自己做出選擇吧,唯一要記得的就是要為自己的選擇負責。

話說回來,故事在大衛找到自己的造物主後,發現自己只不過是許多被創造出來的「眾多大衛中的其中一個大衛」,另一個用來滿足造物主的內心缺乏 - - 已故兒子的替代品罷了,此時的大衛已經無法在自我異化中認出自我,自我衝突與矛盾的大衛最終選擇了跳下被海水淹沒的曼哈頓,卻又在彌留之際,在水中看見了自己不斷尋找的藍仙子的雕像,於是此時的大衛,從「滿足人類情感需求的眾多大衛中的大衛」意識到自己仍然是那個「渴求莫妮卡母愛的大衛」,再次地在行動中認出自己。只是事與願違,虛構的童話終究是虛構,大衛也將虛擬的童話誤認成為真實,一心祈禱了兩千年,希望藍仙子可以讓他變成真正的人類。
諷刺地,過了兩千年之後,高度進化的人工智慧是存留在世上的唯一「生命」物種,人類早已自取滅絕。而這些人工智慧物種找到大衛並了解他的故事之後,透過時空軌跡以及仍保留在泰迪身上的莫妮卡的頭髮,藉由DNA以及時空代碼重現莫妮卡的樣貌與記憶。此時導演彷彿也暗示了人類的存在本身也是如同機器人一般,是藉由並不代表任何意義的代碼(人類:ATCG、機器人:二進位)所模擬的真實,因此布希亞也指出,人類當代社會已經不再是先有真實,接著擬仿物掩蓋了真實,而是先有擬仿物,而這些擬仿物建構了我們的真實。當真實可以藉由擬仿物不斷建構並複製,那麼真實還是真實嗎?又或者,擬仿物即是真實?
這些對於未來科技的想像,以及回頭看向過去的反省,人類可以透過文本的建構、哲學的探討,百花齊放的結果提醒了我們必須做好準備去迎向未來將會更迅速變化的差異性,並且以更開放的胸襟去接受這些與我們的不同。正因二戰時期的人類不願理解同志族群的權益,而痛失了可能讓人類社會向前一大步的電腦巨擘;正因我們對於自己所認知的機器人的不真實感到不信任,而錯過了與機器人共同和平相處的可能未來。或許,圖靈想要的只是安靜地一頭埋進自己感興趣的事物,並供他自由揮灑才華的空間;或許,大衛想要的只是自然地被人類所看待,愛人以及被愛,並且能與莫妮卡永遠在一起。就是因為這些種種的故事與想像,才得以讓即將跨入後人類時代的我們一管窺天地看向未來,並為即將到來的世界做好一切的準備。

後記:重弄了新的部落格,以及會有這篇文是因為這學期修了人工智慧導論,一開始老師就介紹了兩部他心目中最棒的人工智慧主題的電影,要我們回家寫心得。起初想說,還有什麼人工智慧相關的電影是我沒看過的,所以導致我一開始看A.I.人工智慧的時候還有點不耐煩,但看著看著卻想到了文學批評課上所學到的理論,就想說,不然來寫寫看好了!又討厭以前那個部落格,就弄了個新的,把舊的黑歷史刪掉。就這樣,也不知道會不會有下一篇,但應該不會再刪了哈哈哈。

2023/01/08 更新

這是大五上學期修的人工智慧導論的其中一份作業,有點掉書袋的影評,當時主要是想學超級歪那種文本分析的方法,很酷!結果被教授特別挑出來討論,說跨領域就是要這樣!

這門課讓我印象很深刻的是當時老師 demo 了一個推薦系統,結果發現那個推薦系統要 XP 的 IE 才能跑(也就是最新的 IE 也不行跑,更別提當時 IE 也已經苟延殘喘了)沒想表達什麼,只是覺得很酷。最近在整合過去到現在寫過的長文到這個個人網站,想說這篇也跟人工智慧還有影評有關,也符合這個部落格的主題,這篇才又被我翻出來,現在碩班的我再看…啊,真是掉書袋呀!

「終點,是旅程的一部分。」
東尼・史塔克《復仇者聯盟4:終局之戰》

最後一天,我想來談談 「人工智慧」

在過去的旅程中,我們花了將近一半的篇幅在討論機器學習模型。從一開始的 N-Gram、BoW、樸素貝氏分類模型、羅吉斯迴歸,一直到深度學習的神經網路模型,像是循環神經網路、長短期記憶,還有利用自注意力機制的 Transformer,還有踩在巨人肩膀上,使用預訓練模型完成任何任務的 BERT,是一段旅程,而且絕對是一段縱覽從過去到現在自然語言處理以及計算語言學的朝聖之旅。

不知道大家是否還記得,我在第一天的文章,也就是 前往NLP的偉大航道!一起成為我的夥伴吧!中提到了計算語言學、自然語言處理、人工智慧還有非常大的進步空間,但我們不免還是能從過去的研究中,看出發展的脈絡。

但今天,我們不再講機器學習,讓我把視角稍微拉大,在鐵人賽的最後,來簡單聊聊「人工智慧」。

關於人工智慧

在 Kate Crawford 所撰寫的《人工智慧最後的秘密》一書中,一開始提到了「聰明漢斯」。「聰明漢斯」是一匹能夠計算數學、告知時間、辨識日期以及音符,還可以拼出文法跟句子。訓練者馮・奧斯頓在一開始訓練這匹馬時,透過握住馬匹的腿,讓他學習數字,並引導馬匹用馬蹄敲出正確的答案作為回應。他發現漢斯非常能理解人類智慧才能掌握的概念,於是便聲稱「動物也能推理」。經過教育局組成的委員會,其中的心理學家斯圖姆夫以及芬斯特,再加上其他研究者的檢驗,發現不管奧斯頓是否在場,漢斯都能夠回答出正確答案;但也發現,如果提問者不知道正確答案,或是站得離馬匹很遠,那漢斯就很少給出正確答案。

後來芬斯特就發現,提問者的姿勢、呼吸和臉部表情,會在提問者移動到正確答案時,出現細微的變化;漢斯感知到這些變化後,便能在正確答案時停下動作,最後解答出正確答案。而提問者通常不知道自己給了馬匹線索

有趣的是,奧斯頓的馬匹後來流入了市場,情感以及經濟上的刺激還有投資,吸引了社會在財務、文化以及科學研究上的關注

當我們把這個「聰明漢斯效應」,或是「觀察者期望效應」放在機器學習上,其實就代表著今天人類,以及透過資料所訓練的機器學習模型之間的關係。我們很難得知機器學習模型從資料中學習到了什麼,透過模型,我們可以基於需求輸出理想的資料結果,但我們不知道為什麼模型會輸出這樣的結果,因為機器學習模型從資料學到了什麼,基本上無從得知。

「聰明漢斯效應」的迷思

Crawford 在書中提到,從「聰明漢斯」的案例,可以發現兩種迷思。首先,我們假設非人系統比擬作人類心智,也就是說,只要給出夠多的資料,並以足夠的資源經過適當的訓練,就能打造出與人類相似的智慧,但卻忽略了人類是以什麼形式,不管是文化、科學,亦或是歷史、政治形式,與社會互動。第二個迷思則是,智慧是獨立存在的,智慧並不會影響社會的運作形式,也不會與歷史、政治、力量,以及文化層面有任何牽扯。

人工智慧的真相

但在現代社會中,人工智慧的掌握者,事實上,就代表了權力所在。從 Crawford 的觀點來看,人工智慧既非人工,也不是智慧。這是因為人工智慧並非自主學習,也不是理性且無所不察的,如同先前介紹的那些語言學習模型,只要有大型的資料集,或是對這些資料集預先定義好的標籤等等,進行廣泛密集運算的訓練。要進行這些模型的訓練,需要極為大量運算資源,這也就仰賴擁有這些資源以及資本的大企業,為了保有這些資源,人工智慧也有很大一部分是基於優勢階層的利益所建置。一旦這些人工智慧大量融入我們日常生活當中,成為我們日常生活的微觀權力,便會像是拉大貧富差距等,在在影響人類社會以及文化。

那要如何有意識地對人工智慧的機器學習模型進行掌控,我認為可以從以下幾點做起。

資料的合理運用

我們就該因為這樣就放棄使用人工智慧嗎?筆者我本人倒也覺得不必要因此而因噎廢食。我們反倒更應該以更多不同面向以及角度去看待人工智慧,並小心處理每一個細節。我認為留意資料的使用就是一點,且可以從兩個角度出發。

1. 作爲一般公民,有意識地掌握自身資料如何為人所用

在這個時代,我們能做到的,就是要有意識掌握自身資料的使用方式。現在在使用各種雲端服務時,公司都會需要你加入會員方可使用。只是在註冊的過程中,資料很容易就會被拿去做別的用途。所以在輸入任何資料的場合,都要知道自己的資料可能會被拿去做什麼事情,並有意識地去保護自己的資料。

2. 作為資料處理者,有意識地仔細審閱資料的合適性

作為進行自然語言處理研究的我們,在處理語言資料時,也要小心資料的運用上,是否符合需求,以及這些資料是否有害,以及是否會造成模型偏誤,導致模型無法達成我們的需求。畢竟資料會影響模型的每一步決策,因此更要小心處理。

❗資料為何重要?

資料的選擇不當,很有可能會造成模型偏誤,甚至影響到社會架構,我們來舉以下例子,來說明資料的重要性。訓練模型的過程中,通常都會需要取得大量的資料,而這些來源通常都來自過去的工作人員所收集的,但難保這些資料的運用,都是在一個公平且透明的框架下進行。在《大數據的傲慢與偏見》一書中就舉了一個例子:假如問一名在舒適市郊社區長大的犯人「第一次與警方交手的狀況」,他可能除了導致他入獄的那件事之外,就再也沒有跟警方交手的經驗;但若是年輕男性黑人,可能就並不是這樣。紐約公民自由聯盟的研究發現,雖然 14-24 歲的黑人跟拉美裔男性僅佔紐約 4.7%,但警方臨檢的比例卻有 40% 的盤查對象卻屬於這一群人。而這項問題的結果則會納入美國犯罪的再犯模型當中,導致警察巡邏的範圍區域,就會有極大的偏差,更因為有些未成年,並不如富人區的孩子幸運,可能會喝酒或藏有大麻而因此惹上麻煩,卻也因為這個再犯模型誤了一生,導致惡性循環。所以即使問卷中並沒有加入種族問題(因為畢竟不可能問說你是黑人還是白人),但資料仍然會有隱性的偏見存在。

為了要解決這項問題,模型的解釋能力,還有解釋性人工智慧(Explainable AI, XAI)就成為一個近年最重要的議題。

解釋性人工智慧

所謂的解釋性人工智慧,就是藉由一系列方法,將 「黑盒子模型」 打開,了解人工智慧的決策方式,讓人類更容易理解機器學習模型,以及模型進行決策的方式。首先,我們先從黑盒子開始講起:

在圖中,我們可以看到哆啦A夢拿出了一台機器,並跟大雄說,只要將手塚治虫的漫畫丟進機器中,機器就如哆啦A夢所說:「變成手塚老師了」,最後,機器會產出一本類似於原子小金剛的漫畫(因為畢竟他是手塚治虫老師嘛!)我們從中可以發現一件事:

我們並不知道哆啦A夢的機器是怎麼學習手塚老師的畫風、劇本能力。

哆啦A夢的這個道具,就是一種黑盒子模型。而至今為止我們所學到的那些統計語言模型,循環神經網路、長短期記憶模型、GRU,乃至於 Transformer、BERT,都屬於黑盒子模型,因為我們無從得知模型如何產出當下的結果,只知道模型能夠解決任務,僅僅如此而已。但準確度不該是模型該追求的目標,應該說,人類該如何為模型制定適當的準確度標準,才是應該思考的方向。藉此預防像是在科幻電影《2001太空漫遊》中,人工智慧最後決定殺死船艦上的所有人類,僅僅是因為任務「重要到無法允許這些人類阻礙完成任務」。

我們可以從三個方向,與預測結果搭配,藉此來提升模型解釋能力。

模型驗證能力(Model Validation)

我們可以藉由檢查機器學習模型是否存在「偏誤」,也就是如前面所説,模型是否只針對特定族群的資料進行訓練,以及這些「毒性」資料是否影響著模型的決策。另外,敏感私人資訊也必須進行適當的去識別化,以保護個人資訊的外流(例如法律資訊、醫療隱私等)

模型偵錯能力(Model Debugging)

為了保證模型的可靠度與精實度,模型必須要能夠偵錯。我們必須了解機器模型背後產出結果的原因以及理由。也就是說,輸入資料的細微更動,不會大幅度地改變預測結果,這麼做可以減少混淆模型的風險。因此,更需要賦予模型透明性與詮釋性,以便在模型出現不當現象或是不合理預測時進行偵錯。

了解領域知識(Knowledge Discovery)

我們必須要確認模型是否真正理解某特定過程以及事件。也就是說,僅僅進行預測是不適當的,仍要必須解釋並了解案例中的因果關係。例如過去預測肺炎致死率的模型顯示,患有氣喘疾病可以降低肺炎死亡的風險。但事實上這是因為氣喘病患可以得到更多醫療上的治療。這其實就顯示了模型解釋能力的重要性。

結語:終點,是旅程的一部分

最後,如同引言所說,「終點,是旅程的一部分。」雖然三十天的旅程在這邊要畫下句點了,感謝各位的陪伴,但這不代表是我學習的終點。自然語言處理的旅程,對我來說還有非常長一段路要走,對於這個領域的研究,有著更多需要我去探索的新世界,希望在接下來的旅程中,能夠繼續保有我所重視的,增長我所需要的,並開拓我所感興趣的。魯夫還沒到達拉乎德爾,但曾經說過,在這片大海上,最自由的就是海賊王;我在還沒成為那個令自己滿意的自己之前,只要能夠維持初衷、維持好奇、維持求知,那這一切的辛苦也就值得了。

終於快要到鐵人賽的尾聲,在處理 NLP 任務的時候,除了要瞭解不同語言模型的內部架構、功能,還有這些模型適合的不同任務內容,有時候也會需要學習其他技能,來幫助你執行 NLP 的任務。所謂的其他技能,除了最基本的程式語言之外(不一定是 Python),我們昨天以及前天所說的網路爬蟲,就是除了模型以外,處理自然語言處理任務上,可能會需要學習的。那由於時間的關係,再加上這些主題有太多內容可以講,所以今天的主題來簡單介紹一下在進行自然語言處理的時候,可能需要學習,但還來不及詳細介紹的技能。

資料庫基本概念(Database)& SQL

身為一個資料處理人員,資料的儲存問題將會是一個需要考量的點之一。你可能會想問說:

我們為什麼不直接像先前所說的,利用網路爬蟲爬下來儲存成 json 檔之後,再將這些資料儲存在本機端就好了?

有一點需要先釐清,通常在進行程式開發時,基本上都是採取多人共同開發模式。如果說只單純利用前面所說的資料儲存方式,在多人協作上就會產生很多的不便以及困難點,畢竟不可能大家都在那邊上傳 Google 雲端,然後還要上傳下載等等等之類的,過多的繁雜程序會造成任務處理的時間增加,反而沒有效率 畢竟工程師都希望時間越快越好 。另外,一旦處理的任務變得越來越複雜,資料與資料之間產生相依性的時候,複雜的資料結構也會造成困難(例如:增加新進人員理解資料結構的速度)。

我們可以思考一下,假如今天有家百貨公司需要建構資料庫,我們會需要哪些資料?可能有:

  1. 會員基本資料:姓名、手機、地址、Email etc.
  2. 顧客購買紀錄
  3. 商品資料
  4. 樓層資料

因為我們沒有辦法在同一個表格同時包含所有我們需要的資料,意思就是,沒有辦法在同一個表格同時包含會員的基本資料、又涵蓋所有顧客購買紀錄;那所謂的相依性,就是我們可以透過共同欄位的資料,來連結不同相對應的表格。例如:在會員基本資料以及顧客購買紀錄的表格中,有個共同欄位「姓名」(通常會用 ID 但這裡以姓名為例)。那麼就可以透過「姓名」欄位來「連結」兩個表格,也就是:「紀錄了王小明在百貨公司購買哪些商品的表格」。

回到主題,如果我們同時需要:

  1. 多人共同存取資料
  2. 處理相依性問題

那這時就會需要使用資料庫,來提升開發的速度。

存取資料庫,會利用另外一個程式語言 SQL ,來提取資料庫中的資料。那基本架構大概會是長這樣:

1
2
3
4
5
SELECT [回傳欄位]
FROM [資料庫表格]
WHERE [條件式篩選]
GROUP BY [組合欄位]
HAVING [組合後進行的條件式篩選] ORDER BY [需要排序的欄位]

那所謂的資料庫有以下這些:

  • Oracle
  • MySQL
  • MS SQL Server
  • PostgreSQL
  • Amazon Redshift
  • IBM DB2
  • MS Access
  • SQLite
  • Snowflake

大家如果有興趣可以再去查,因為這又完全是另外一個大主題了。

用 Python 架網頁以及架設 API:Flask

前面說到,html + CSS + Javascript。另外,也可以用 Python 來建立網站。這邊提供非常簡單的介紹,通常若要用 Flask 來搭建網站,會需要以下程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
return """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>

</body>
</html>"""

if __name__ == '__main__':
app.run()

接著直接運行這個 Python 檔,就可以用 Python 運行你的第一個網頁喔!另外,Flask 也很常會用來部署 API。所謂的 API,也就是 Application Platform Interface。記得先前我們在利用 Tensorflow 進行機器學習的時候,透過別人已經訓練好的模型來進行下游的自然語言處理任務,Tensorflow 就是 API 的其中一種。Flask 就可以用來架設 API。

Git & GitHub

前面說到,通常開發流程都會是多人共同進行的,這時多人同時存取程式碼也就成為一個需求,就會需要同時有很多分支讓開發團隊同時進行程式碼的撰寫;另外,也難保有人會寫錯程式碼,或者是有時候會發生昨天還可以跑,今天就不行跑了 相信我,這很常發生,不然台灣的乖乖文化哪來的?。Git 套件就可以幫助你在發生這些狀況的時候,讓他回復到尚未出問題之前的狀態。所謂「回復」、「分支」稱為「版本管理」。

除了以上功能,也會需要一個儲存程式碼的地方,稱為程式庫(repository)。存放這些程式庫的地方有很多,最為人所用的是微軟開發的 GitHub,另外其他的程式庫存放處,像是 GitKraken、GitLab。

明天邁入最後一天囉!我明天會說明模型解釋力的重要性,最後做個結尾,感謝各位一路以來的支持。

聖誕快樂!你這骯髒的小畜生!
《小鬼當家2》

昨天我們把所有寫爬蟲時可能會需要的先備知識都學起來了,今天就要直接進入爬蟲主題啦!以往我在教同學寫網路爬蟲的時候,我都會用一個我自己原創的比喻:聖誕節大採購!且聽我娓娓道來吧!

爬蟲出動!

過去從來沒有接觸過網路爬蟲的人,可能會覺得爬蟲是一個很難理解的技術,對初學者而言就更不用說了。但其實爬蟲的概念很簡單,就是:

把所有要爬網頁瀏覽一遍,把我們要的資訊整理成結構化資料,接著儲存成我們想要的檔案形式。

好像還是有點抽象。沒關係,我們從了解一個網路的架構開始談起。網頁通常會分成兩種:

  • 動態網頁
  • 靜態網頁

動態網頁不是我們平常會看到的那些有很精美的動畫,才能叫做動態網頁。有時候,一個靜態網頁也可能會有很厲害的動畫。決定一個網頁是靜態還是動態,取決於他是否有連接至資料庫。

對爬蟲而言,抓取靜態跟動態網頁各自有不一樣的寫法。動態網頁稍微複雜一點,今天先講抓取靜態網頁的網路爬蟲寫法。

網頁架構

一個網站的基本架構,通常是 html + CSS (+ javascript)

首先,我們在網路上看到的所有網站,都是透過一個一個的 html 檔所組成的,這些 html 檔又被稱為標籤語言,這是因為你可以看到這是由很多的 <label> 組成的語言,這些標籤中又各自帶有不同的特性。我們先來看 html 檔長什麼樣子,我通常都會把 html 檔稱為聖誕樹

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>

</body>
</html>

之後,在網頁上有時候會再加上 CSS 檔,來裝飾網頁,讓網頁變得更加繽紛。而我通常會稱這些 CSS 檔為聖誕樹裝飾品還有裝飾燈

有些網站會有 Javascript ,有些沒有。Javascript 主要是用來讓網站回應訪客的要求,並作出適當回饋;又或者是,可以透過 Javascript 來跟後端的資料庫進行溝通。因此我通常稱 Javascript 為聖誕樹裝飾燈的開關

所以說,爬蟲就是聖誕節搶購!

為什麼是聖誕節的大採購!?

所以,如果要用聖誕節大採購來比喻爬蟲的話,基本上就是這樣:

  • 你可以將一個一個的網頁當作聖誕樹。
  • 網頁中的文字、連結,則是聖誕樹的裝飾品,也就是你需要的資料。
  • 你要做的,就是找到一棵一棵的聖誕樹,接著從一棵一棵聖誕樹的樹枝上找到你要的裝飾品。
  • 接著,將裝飾品裝到你的籃子裡,打包帶回家。

什麼意思?我們來寫寫程式碼

首先,我們要下載需要的套件。請先在終端機執行 pip install,爬蟲會用到的套件為 BeautifulSoup

1
pip install BeautifulSoup

如果說今天我們想爬 PTT 電影版的資料作為簡單範例,我們先執行以下程式:

1
2
3
4
5
import requests
from bs4 import BeautifulSoup as bs
r = requests.get("https://www.ptt.cc/bbs/movie/index.html")
page = bs(r.text, "html.parser")
print(page)

就可以得到那個網頁的 html 檔!(注意,如果是動態網頁,很有可能就抓不到需要的內容,那時就需要另外一種寫法)以下貼一部分的輸出即可(我有特別擷取某一段,所以你的輸出會跟我長得不太一樣):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<title>看板 movie 文章列表 - 批踢踢實業坊</title>
<link href="//images.ptt.cc/bbs/v2.27/bbs-common.css" rel="stylesheet" type="text/css"/>
<link href="//images.ptt.cc/bbs/v2.27/bbs-base.css" media="screen" rel="stylesheet" type="text/css"/>
<link href="//images.ptt.cc/bbs/v2.27/bbs-custom.css" rel="stylesheet" type="text/css"/>
<link href="//images.ptt.cc/bbs/v2.27/pushstream.css" media="screen" rel="stylesheet" type="text/css"/>
<link href="//images.ptt.cc/bbs/v2.27/bbs-print.css" media="print" rel="stylesheet" type="text/css"/>
<script async="" src="/cdn-cgi/challenge-platform/h/b/scripts/invisible.js?ts=1652421600"></script></head>
<body>
<div id="topbar-container">
<div class="bbs-content" id="topbar">
<a href="/bbs/" id="logo">批踢踢實業坊</a>
<span>›</span>
<a class="board" href="/bbs/movie/index.html"><span class="board-label">看板 </span>movie</a>
<a class="right small" href="/about.html">關於我們</a>
<a class="right small" href="/contact.html">聯絡資訊</a>
</div>
</div>
<div id="main-container">
<div id="action-bar-container">
<div class="action-bar">
<div class="btn-group btn-group-dir">
<a class="btn selected" href="/bbs/movie/index.html">看板</a>
<a class="btn" href="/man/movie/index.html">精華區</a>
</div>
<div class="btn-group btn-group-paging">
<a class="btn wide" href="/bbs/movie/index1.html">最舊</a>
<a class="btn wide" href="/bbs/movie/index9510.html">‹ 上頁</a>
<a class="btn wide disabled">下頁 ›</a>
<a class="btn wide" href="/bbs/movie/index.html">最新</a>
<div class="r-ent">
<div class="nrec"><span class="hl f2">6</span></div>
<div class="title">
<a href="/bbs/movie/M.1652075354.A.59B.html">[新聞] 民族誌紀錄片工作者,胡台麗逝世</a>
</div>
<div class="meta">
<div class="author">mysmalllamb</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/movie/search?q=thread%3A%5B%E6%96%B0%E8%81%9E%5D+%E6%B0%91%E6%97%8F%E8%AA%8C%E7%B4%80%E9%8C%84%E7%89%87%E5%B7%A5%E4%BD%9C%E8%80%85%EF%BC%8C%E8%83%A1%E5%8F%B0%E9%BA%97%E9%80%9D%E4%B8%96">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/movie/search?q=author%3Amysmalllamb">搜尋看板內 mysmalllamb 的文章</a></div>
</div>
</div>
<div class="date"> 5/09</div>
<div class="mark"></div>
</div>
</div>

注意,我們現在目錄頁!但我們想要的資料在文章的網頁裡面。所以現在要找的是文章網頁的網址。仔細觀察一篇一篇的文章中,超連結通常會存在前面的標籤中,所以需要透過以下程式碼得到需要的那段 html:

1
page.find("div", "r-ent")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="r-ent">
<div class="nrec"><span class="hl f2">6</span></div>
<div class="title">
<a href="/bbs/movie/M.1652075354.A.59B.html">[新聞] 民族誌紀錄片工作者,胡台麗逝世</a>
</div>
<div class="meta">
<div class="author">mysmalllamb</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/movie/search?q=thread%3A%5B%E6%96%B0%E8%81%9E%5D+%E6%B0%91%E6%97%8F%E8%AA%8C%E7%B4%80%E9%8C%84%E7%89%87%E5%B7%A5%E4%BD%9C%E8%80%85%EF%BC%8C%E8%83%A1%E5%8F%B0%E9%BA%97%E9%80%9D%E4%B8%96">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/movie/search?q=author%3Amysmalllamb">搜尋看板內 mysmalllamb 的文章</a></div>
</div>
</div>
<div class="date"> 5/09</div>
<div class="mark"></div>
</div>
</div>

.find雖然可以找到第一篇文章的部分 html 檔可是我們需要的是所有文章,這時可以用另外一個函式 .find_all() 回傳的就會是一個list,這裡就不貼輸出了。

1
page.find_all("div", "r-ent")

但記得,我們需要的是網址,但並不是每個標籤都有我們想要的網址,這時 try...except...就派上用場了。

1
2
3
4
5
6
7
divs = page.find_all("div", "r-ent")
for div in divs:
try:
href = div.find('a')['href']
print(href)
except:
pass

這時,我們就可以得到網址的「後半部分」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/bbs/movie/M.1652075354.A.59B.html
/bbs/movie/M.1652076208.A.B5C.html
/bbs/movie/M.1652077025.A.469.html
/bbs/movie/M.1652077434.A.D63.html
/bbs/movie/M.1652077492.A.F65.html
/bbs/movie/M.1652078673.A.2B7.html
/bbs/movie/M.1652079315.A.0AC.html
/bbs/movie/M.1652079447.A.571.html
/bbs/movie/M.1652081468.A.575.html
/bbs/movie/M.1652081735.A.D22.html
/bbs/movie/M.1652083107.A.CED.html
/bbs/movie/M.1652086174.A.524.html
/bbs/movie/M.1652087613.A.7D7.html
/bbs/movie/M.1652088346.A.D6C.html
/bbs/movie/M.1652089057.A.132.html
/bbs/movie/M.1652089740.A.BA4.html
/bbs/movie/M.1652089754.A.A1E.html
/bbs/movie/M.1652092096.A.6BC.html
/bbs/movie/M.1652097109.A.975.html
/bbs/movie/M.1630756788.A.1FE.html
/bbs/movie/M.1636002497.A.7EC.html

重新整理一遍,讓輸出的結果是完整網址。

1
2
3
4
5
6
7
8
divs = page.find_all("div", "r-ent")
baseURL = "https://www.ptt.cc"
for div in divs:
try:
href = baseURL+div.find('a')['href']
print(href)
except:
pass

現在試著把他包成函式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def indexGrabber(inputURL):
r = requests.get(inputURL)
page = bs(r.text, "html.parser")
divs = page.find_all("div", "r-ent")
UrlLIST = []
baseURL = "https://www.ptt.cc"
for div in divs:
try:
href = baseURL + div.find('a')['href']
UrlLIST.append(href)
except:
pass
return UrlLIST

indexGrabber("https://www.ptt.cc/bbs/movie/index9501.html")

我們接下來抓取目錄頁的網址,這樣才能將前面的函式用上。

可以發現,一個一個的目錄頁,是由後面那段數字組成,所以到時候我們用for ... in range(): 寫就可以了。(大部分目錄頁都會是用這種寫法,仔細觀察!)

要找前面那些標籤位置好像很容易?

前面要找那些資訊的時候,你可能發現在參數中,我加上了("div", "r-ent") 這是因為,PTT的結構相對簡單,可是如果我們碰到結構複雜的網頁,該怎麼辦?我們可以用以下工具,你可以根據你的瀏覽器選擇對應的套件:

  • Chrome: ChroPath
    • (網頁任意處右鍵) -> 檢查 -> 元素 -> (右側「樣式」列的最右側,若沒看到則按”>>”) -> (檢查元素) -> (複製CSS或XPath)
  • Firefox: SelectorsHub
    • (網頁任意處右鍵) -> 檢測 -> 檢測器 -> (右側「樣式」列的最右側,若沒看到則按向下箭頭樣式) -> (檢查元素) -> (複製CSS或XPath)
    • Alternatives: 移動游標至欲檢測之目標上方 -> (右鍵) -> SelectorsHub -> “Copy Rel cssSelector” or “Copy Rel XPath”

現在來練習 imdb 的網站,我們要將標題跟分數抓下來,透過以上方式,我們可以找到標題跟分數的位置如下:

1
2
titlePath = ".sc-b73cd867-0.eKrKux"
scorePath = "div[class='sc-db8c1937-0 eGmDjE sc-94726ce4-4 dyFVGl'] span[class='sc-7ab21ed2-1 jGRxWM']"

那我們執行跟前面一樣的動作:

1
2
3
4
5
6
7
8
9
10
import requests
from bs4 import BeautifulSoup as bs
r = requests.get("https://www.imdb.com/title/tt3783958/?ref_=fn_al_tt_1")
page = bs(r.text, "html.parser")
rawTitle = page.select(titlePath)
rawScore = page.select(scorePath)
print(rawTitle)
print(rawScore)
print(type(rawTitle))
print(type(rawScore))
1
2
3
4
[<h1 class="sc-b73cd867-0 eKrKux" data-testid="hero-title-block__title" textlength="10">La La Land</h1>]
[<span class="sc-7ab21ed2-1 jGRxWM">8.0</span>]
<class 'list'>
<class 'list'>

可以發現輸出的資料型態都是 list。所以要得到我們需要的資訊,除了指定索引值之外,需要再加上.text,告訴爬蟲,我們只需要中間的字串即可。我們可以這麼做:

1
2
3
4
title = rawTitle[0].text
score = rawScore[0].text
print(title)
print(score)
1
2
La La Land
8.0

好,寫到這邊,大家應該都對爬蟲有概念了,若有任何問題可以一起來討論喔!我們明天見。

在我們過去一起經歷的旅程中,我們從一開始的正規表達式、詞頻、N-Gram,一直到機器學習,像是貝氏分類器、羅吉斯迴歸等等,接著又講到了深度學習,利用神經網路來進行自然語言處理,比如說像是循環神經網路、長短期記憶等等,後來又發展出了自注意機制,有了 Transformer 以及 BERT 還有他的芝麻街小夥伴,又學到了以語言學基礎的工具 Articut 以及 Loki。

我們一起學習了好多好多的語言模型,但不知道你有沒有想過,處理這些語言資料的我們,學了那麼多的模型,那資料從哪來?我們想要學習處理語言資料,但是卻沒有語言資料,那不就像是雞蛋布丁沒有雞蛋、哆啦A夢沒有百寶袋、太陽餅沒有太陽、老婆餅沒有老婆 等等,如果你有看電馭叛客:邊緣行者的話,那就可能有。那其實要取得這些資料,有很大一部分是取自於網路上的資源(但也必須要守法喔!)取得這些資源的方法就是利用網路爬蟲!今天的內容都是爬蟲的先備知識,要熟悉這些技巧才有辦法駕馭爬蟲喔!

小複習

還記得我們在剛開始旅程的時候,曾經有講過串列 list 以及 字典dict嗎?我們在這邊再複習一遍。

串列 list

我們可以透過串列來儲存一系列的資料,程式碼中透過中括號將資料包起來的就是 list ,並透過索引值(index number)來取得串列中的資料。

1
2
3
4
5
myLIST = [1, 2, 3, 4, 5, 6, 7, 8, 9]
myStrLIST = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
print(type(myLIST))
print(myStrLIST[0])
print(type(myStrLIST[0]))
1
2
3
<class 'list'>
1
<class 'str'>

字典 dict

至於字典就是像平常我們查字典一樣,每一個字會有一個對應的解釋,在 Python 中,我們稱呼「字」為鍵(key),「解釋」則為值(value)。
例如:

1
2
3
4
5
strawhat = {
"船長": "魯夫",
"戰鬥員": "索隆",
"航海士": "娜美",
}

另外,我們也可以將字典加入串列中! dict in list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
strawhat = {
"船長": "魯夫",
"戰鬥員": "索隆",
"航海士": "娜美",
}

heart = {
"船長": "托拉法爾加羅",
"副船長": "培波",
"船員一號": "佩金",
"船員二號": "夏奇",
"船員三號": "強帕爾"
}

aliance = []
aliance.append(strawhat)
aliance.append(heart)
print(aliance[1])
print(aliance[1]['船長'])
1
2
{'船長': '托拉法爾加羅', '副船長': '培波', '船員': '佩金', '二號船員': '夏奇', '三號船員': '強帕爾'}
托拉法爾加羅

認識 JSON

由於網站跟網站之間傳遞資料,最常用的資料形式就是 json 檔,而爬蟲所要做的就是要去抓取網頁中的這些 json 檔,或是在網頁上的元素。形式可以是一串 list,或是 dict in list,或是單一一個dict 都可以轉換成 json 檔。 json 檔的要點如下:

  • JavaScript Object Notation 的縮寫
  • 常用於網站上的資料呈現、傳輸
  • 可在 JSON 之內加入相同的基本資料類型

輸出 json 檔

在這裡,我們用先前的串列aliance來輸出 json 檔,並存成檔名為data.json

1
2
with open('data.json', 'w') as f:
json.dump(aliance, f)

讀取 json 檔

在這裡,我們則是將 同一個檔案路徑下的data.json讀取進 Python,並取變數名為 pirates。

1
2
with open('data.json') as f:
pirates = json.load(f)

Try…Except 一個寫程式時眼不見為淨的好幫手(誤)

在寫爬蟲時,很有可能會碰到一個窘境,就是網路資料路徑可能會有改變,或著是你爬得太猖狂,爬到網頁管理者覺得你太超過了,擋你 IP 之類的,這時候你寫的爬蟲程式碼可能就會出現錯誤。那出現錯誤,程式就會立刻停止了耶!這時該怎麼辦呢?讓爬蟲繼續爬嗎?還是你要他進行什麼動作?

在這個時候就會需要進行所謂的例外處理,寫法就是 try...except。其實你可以想像就是你跟電腦說:「你先試試看(這樣那樣) 除非(發生什麼事)你再(幹嘛幹嘛)。」來看看實作怎麼做吧!

假如說你今天寫了一個相加的程式:

1
2
a = input('輸入數字:')
print(a + 1)

運行程式之後絕對會出現錯誤:

1
2
3
4
5
6
7
8
9
輸入數字:1
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-1-f0bc7ffa95ff> in <module>
1 a = input('輸入數字:')
----> 2 print(a + 1)

TypeError: can only concatenate str (not "int") to str

這是因為你輸入的資料型態其實不是「數字」,而是「字串」。「字串」只能跟「字串」相加,不能跟數字相加,所以這時候就出現錯誤。

那這時候我們可以加入 try...except...

1
2
3
4
5
try:                      # 使用 try,測試內容是否正確
a = input('輸入數字:')
print(a + 1)
except:
print('發生錯誤')
1
發生錯誤

這邊就會直接跳過錯誤訊息,直接執行except後面的動作,在這邊就是印出「發生錯誤」。

那你可以仔細觀察一下前面出現錯誤的訊息,你可以發現有一行:

TypeError: can only concatenate str (not “int”) to str

前面的 TypeError 指的就是這個錯誤的類型,有很多種的 Error message,例如:

  • SyntaxError: 程式不合 Python 語法。
  • IndexError: index 位置中沒有東西,或是超出 list 的長度!
  • FileNotFoundError: 找不到檔案
  • ModuleNotFoundError: 可能要檢查一下套件
  • EOFError: 可以換新的 code block 再寫一遍,才可能解決了。

有時候會碰到各種不同的 error。你可以根據不同的 error 去決定你的 try…except 要怎麼寫,最後一個沒有寫的 except 則是一旦發生錯誤,而前面又沒有指定時,就會跑到這個區塊,例如:

1
2
3
4
5
6
7
try:                      # 使用 try,測試內容是否正確
a = input('輸入數字:')
print(a + 1)
except TypeError: # 如果 try 的內容發生錯誤,就執行 except 裡的內容
print('發生錯誤')
except:
print('跑來這裡')
1
2
輸入數字:1
發生錯誤

好的,今天的內容就是這些!明天就會開始正式進入爬蟲啦!

你所做的這些浮誇的浪漫舉動—其實你做的、你所說的根本一點都不重要,真正重要的是你的意圖。真正重要的是你願意花時間在那個你在乎的人身上,告訴他:「我願意就這樣看著你,也願意聆聽你的聲音。」我很清楚,你現在需要的是什麼,然後我現在在告訴你,你知道這件事情,對我來說有多麼重要。
Jack Pearson《這就是我們》

昨天分享了 Articut ,今天來介紹 Loki。

有了 Articut 的基礎,Loki 就誕生了。不是雷神的弟弟,而是 Linguistic Oriented Keyword Interface (語言導向的關鍵詞介面),是一種自然語言理解的引擎。但在開始之前,我們得先知道,什麼是自然語言理解?

自然語言理解(Natural Language Understanding, NLU)

自然語言理解,是自然語言處理的其中一種技術應用,旨在讓電腦可以理解更複雜的語言輸入。我們現在有很多日常生活中的科技都會需要用到自然語言理解的技術,比如說我們平常手機常用的語音助理,就是一種自然語言理解。其最終目的在於,我們有辦法用日常生活中所運用的自然語言,直接與機器進行溝通。這是為了讓人機互動可以更為順暢,且幫助未來的生活可以更加便利,不需要讓人類順應機器的理解模式,來與機器互動。如果我們想知道高鐵時刻表,我們會在 Google 打入:

高鐵 時刻表

但若電腦可以直接理解你的自然語言,我們就不需要使用這種順應機器的說話方式,我們可以直接說:

我想知道有沒有兩點左右台北往台南的高鐵

從這句話中,電腦可以立刻就知道你的意圖是想要知道「兩點左右」(時間)是否有「台北」(起點站)到「台南」(終點站)的「高鐵」交通工具,這麼一來,電腦就可以直接回答:

兩點十六分有一班台北往台南的高鐵,請問要幫您訂票嗎?

這是自然語言理解的技術上所欲達成的目標。想像一下,我們可以透過日常生活的對話,直接告訴電腦我想做的事情,不正是大家想像中未來世界應該要有的模樣嗎?

眼尖的朋友可能發現新名詞了:意圖

我們人類說的每一句話,裡面都一定隱藏著所謂的「意圖」,而我們就是透過這個「意圖」來互相理解彼此,對於電腦來說也是,而我前面所舉的例子,就是。我們重新回到訂票的案例。

例如:

我要一張兩點以前左營往南港的票

在這句話中,就包含了三個意圖:「車票張數」、「乘車時間」、「起點站」、「終點站」。

實習時,Peter 跟我們說,他的假設是,在同一個情境下,為了達成某種目的或意圖,使用的句型是有限的。想想看,假如要買車票,你可以想出幾種不一樣的說法?直到想不到為止,你就會發現能夠買車票的句型,其實也就那幾句而已。那在這樣的基礎下,也就是「意圖」+「有限句型」兩個要素之下,Loki 誕生了。

Loki

在這邊就以我當時實作的專案為例,來進行簡單的 Tutorial。

  1. 首先先來到卓騰的 API 官網
  2. 在登入畫面,選取 Loki 之後,可以看到這樣的畫面

  1. 選取專案後,就可以看到在專案底下的不同意圖,在建立意圖前,你必須要先思考的是情境中會應用到的句型,其中可能會有哪些意圖(intent),例如我這裡就是大人小孩的車票張數、起點站、終點站、啟程時間、抵達時間、以及詢問時間的句型。

  1. 我們在這裡就先以 departure 意圖為例,在建立好意圖後,我們可以看到第三部分。在這裡要思考的是跟售票員說從哪裡到哪裡的時候,有幾種說法?,當初在建置模型時,大家集思廣益了這些句型。

  1. 按下全句分析後,會出現以下畫面:

垃圾桶就是刪除語句,這應該大家都懂;另外你可能可以發現,在每一句型右邊,都會有數字。以第4句為例,第4句右邊有一個數字6,這代表第4句跟第6句對 Loki 來說是相同的。至於你問我為什麼我還有這些數字,這是因為我實習的時候還沒有這功能啊!歲月催人老

來一一解說各個按鈕的功能各是什麼:若你按下數字,會出現 Articut 的斷詞結果,例如:

在這裡,你就可以確認 Articut 是否有正確理解你的句型。若進階一點的人,則可以按下 regex 確認句型是否有被正確理解。若你按下紅色框框,在這裡我按下「出發」,則會出現以下視窗:

在這裡,系統也會根據機器學習的運算結果,來提示你同義詞可能有哪些,若你勾選這些同義詞,則可以讓 Loki 同時也理解這些詞彙。

  1. 假如我今天想要測試句子,則可以在區塊 6 測試。例如:我想測試「自新竹」是否有對到句型,則可以這麼做:

從圖中可以發現,「自新竹」對到的是「從台北」的語句,因為「自」跟「從」都是 FUNC_inner,「台北」跟「新竹」則都是 LOCATION。所以這兩種是同一種句型。等一切都確定之後,你再按下部署模型之後,就可以回到上上一頁,再按下「下載範本」,並選擇 Python,就會出現 Python 的範本。我們就用編譯器打開樣本。

  1. 來一一介紹範本功能,若按到 intent/Loki_departure.py,你就可以看到以下畫面。

在這裡,你可以設定抓到意圖之後要做些什麼,我在這裡做的是將抓取到的資訊存到dict字典裡,接著就可以透過這些搜集到的資料來做後續的任務。

  1. 運行程式檔 python3 TransportationBot.py 就可以一次測試句子在所有意圖的運行結果為何。

結語

在過去抓取意圖的處理方式,通常也運用統計語言模型,搭配上 embedding 去讓模型猜測你可能的意圖是什麼,但大家都不知道為什麼模型做出的樣的決定,就變得比較像是模型「剛好」猜中你想表達的意思,因此 NLU 對統計觀點的自然語言處理來說,是相對困難的技術。因為解釋力低、而且難以訓練。但是運用 Loki,我們就可以合理推斷並猜測,「從台北」的句型與「自新竹」一致,所以對模型來說,只要能抓到「從台北」的台北,那麼即使你用相同句型的「自新竹」,同樣也能讓模型透過「從台北」的句型抓到「新竹」。

延伸閱讀:深度研討|NLP≠NLU,機器學習無法理解人類語言

我們尋找的並不應該是英雄,而是一個好的想法
Noam Chomsky

前言

還記得碩一下的時候,Lab 的老師找了以前一個現在在擔任軟體工程師的同學來課堂上演講,還有職涯分享。這位工程師說:「自然語言處理在台灣還不算特別盛行,但我覺得有慢慢被重視的感覺。就像每年 台灣PyCon 都會有一位名字叫 PeterWolf 的人,都會在 PyCon 上分享自然語言處理的技術,我覺得他超級猛。」

心想,真巧,我在大學時就擔任了他第一屆的實習生呢。

其實上了語言所之後,對自然語言處理以及計算語言學才開始漸漸理出個頭緒,如今再回頭來看當時所學的這些,才發現,若要單純依靠先前所介紹的那些模型,又要求要在短時間內訓練並達到相同目的,實在是不簡單。我想,過去的鐵人賽中,也尚未有其他參賽者介紹過 PeterWolf 以及他的團隊所開發的這些產品以及工具,那這次就先由我來拋磚引玉一下吧。

不知道你記不記得,我在第八天的文章:【NLP】Day 8: 你拿定主意的話…葛萊芬多!BOW&TF-IDF 之中曾經提到,在過去的研究脈絡中,所謂的語言模型,指的是運用統計方法以及機率來估計語言使用的可能性嗎? 在那之後,計算語言學以及自然語言處理的應用,大多都是延續這個邏輯脈絡,發展出各式各樣不同的模型,如 RNN、N-Gram 等。

今天,我需要你把先前所學的那些模型,通通忘掉

語言學是什麼?能吃嗎?

台灣的各位想必對語言學應該都很陌生,每次跟其他人說自己是讀語言學的時候,別人總會說:「哇!那你是讀哪種語言?」心裡總是百般的無奈,我想這句話對語言學人來說,一定都心有戚戚焉。其實語言學並不是在學習「語言」,而是學習「語言結構」的一門「科學方法」。語言學分成了許多不同的支派,研究聲音的語音學、音韻學,還有語用學、語意學,一直到我們今天會提到「一點點」的句法學,都是語言學的大家庭。

太複雜了嗎?簡單來說,語言學就是一門

觀察語言,並歸納出一系列規則的學問。

研究語句架構的句法學更是一門已經研究多年的學問,已經有相對較為龐大的研究基礎。我還記得,當時 Peter 對我們所有實習生說:

「明明語言學家已經分析並歸納出了語言系統的運作規則,那就來好好運用那些規則,不應該繼續以為只能用統計機率的角度來進行斷詞的計算。」

PeterWolf 會這麼說,原因是:

在過去,語言學界的巨擘 Noam Chomsky 提出了一個假設。在這個假設裡,所有的語言,在底層都擁有相同的文法結構。 Chomsky 的理論革新點在於,只要是符合語法規則的語句,都可以透過程式語言邏輯產生。而每一句話皆可以透過語言規則將其進行歸納,其表現方法就是透過句法樹的形式呈現,而 Chomsky 後來也發展出了一套句法範疇理論,也就是 X-bar theory,X-bar theory 是每一個語言學專業的學生都一定學過的句法學理論,一定也基於 X-bar theory 畫了不少句法樹。

也就是說:

以電腦科學的角度來看,語法規則看似是無窮無盡,但在語言學的觀點裡,語言學規則是列得完的!

而 Articut,就是基於 X-bar theory 所打造的斷詞系統。


Source: 基於X-bar theory 規則所畫出來的句法樹

Chomsky 是怎麼看待 NLP 的?來源

在 2011 年,麻省理工學院所舉辦的研討會,就有人問了 Chomsky 這樣的一個問題:

如何看待機器學習、機率模型近年來被應用在自然語言處理 (NLP) 與認知科學領域的趨勢。畢竟機率的方法在喬姆斯基的年代並不是主流的研究方法。

Chomsky 回應道,確實現在有許多的自然語言處理相關研究,是透過統計模型來解決各式各樣的語言學問題,有些成功,有些失敗。成功的案例,大多是因為統計方法跟語言的基本理論的結合。但若不考慮語言的實際結構就應用統計方法,那所謂的成功就不是正常意義下的成功。Chomsky 用蜜蜂的行為研究做了個比喻,他說,這就像科學家只是對蜜蜂錄影,並記錄過去蜜蜂的行為資料,並透過這些資料來預測蜜蜂的行為。機器學習的方法可能可以預測的很好,但並不算真正科學意義上的成功,因為沒有真正了解語言底層的實際架構。 老人家一番話讓一堆人中槍XD

但… Articut 辦到了?

Articut

Articut 是基於語法規則所搭建的斷詞系統,跟先前我們介紹過的斷詞系統不一樣,Articut 可以離線運行,且不需要大數據訓練,因此修改模型速度也較迅速,即使是新詞,也可以得到正確的結果。但我想要特別提出以下幾點來講:

極高的模型解釋力

在所有斷詞系統中,Articut 最吸引我的就是有著非常非常高的解釋力(在最後一天的文章中,我會好好說明為什麼我認為解釋力非常重要)舉例來說:

張三愛女生

CKIP => 張三愛(Nb) 女生(Na) 且其語義辨識結果是 張三愛 (person) 女生 (woman)

Articut => 張三(ENTITY_person) 愛(ACTION_verb) 女生(ENTITY_noun)

CKIP (及其它基於資料模型的方案) 的運算是由左到右,遇到「張」這個中文姓氏時,就開始計算。因為中文人名「絕大部份是三個字符」,因此接下來就會在這「三個字符」的長度裡計算它是人名的可能性,的確「張三愛」有可能是個人名;接著後面的「女」+「生」+ ending 的分佈裡,也很容易在資料裡出現「女生」這樣的組合,因此結果就是「張三愛/女生」。

但 Articut 是基於「句法 (syntax)」的,而一個完整的句子一定要有動詞!動詞出現以前,這個句子的意義是模糊的。就像日文的動詞在最後面,是一種 SOV 語言,V 表示動詞。在句子的動詞出現以前,是無法確定對方究竟說的是什麼的。

所以,Articut 的運算步驟會是…「這五個字符,哪一個有動詞的時態標記啊?哇~都沒有,那麼因為中文是動詞置中的 SVO 語言,所以先假設最靠近中間的那幾個是動詞。於是「愛」就先被擺在上述那個英文的句法樹的 V 的位置 (剛好英文也是 SVO 語言,所以它的 V 也在句子的中間)。」

然後開始第二步的推測「如果 V=愛,那麼前面有一個 NP 嗎?有,剛好有一個中文姓氏+數字。這在結構和音韻上都是可行的中文單名人名。那後面有其它的標記表示它不是一個名詞嗎?(e.g., 「很」是副詞/形容詞標記,但這句並不是「張三愛很滿」),沒有!那就把後面的字符當名詞,組成一個句法樹看看它能爬到多高的節點位置。」於是就能產生出「張三_person/愛_verb/女生_noun」這樣同時處理完詞性與斷詞的結果了。

功能多合一

除了解釋力超級高之外,我覺得功能多合一的 Articut 也非常厲害。做一次斷詞,就可以得到不只斷詞結果,也可以得到 Part-of-speech tag,更可以進行命名實體辨識。由於是台灣本土的公司,所以也在原來的基礎上加入了許多台灣的資料,例如觀光景點。除此之外,也加入了化學命名的實體辨識。越寫越覺得好像在業配呀 如果想知道更多,可以按這個連結

我覺得這是自然語言處理的另外一種可能性。用機率統計的方式計算語言固然是一種觀點,也是解決問題的方法,但或許也可以考慮看看用這個觀點去看待自然語言處理,或許可以有更多不一樣的想法。

好,今天先講到這,明天再來介紹基於 Articut 的自然語言理解引擎 Loki。

別想太多,做就對了!
《捍衛戰士:獨行俠》

前兩天我們已經了解 BERT 的內部運作,還有 BERT 在進行語言處理上的一些缺陷。今天不聊理論,我們來簡單一一解析 Tensorflow 的官方教學 沒錯,又是 Tensorflow 了喔,並結合前兩天所學到的知識,來對教學中程式碼進行剖析。由於 BERT 的應用實在太多了,所以今天只會提到最簡單的情緒文本分析,也就是先前所說的多對一文本分類,若各位有興趣,或許我們明年還可以相見?在下面留言:)

今天的程式碼將會全部取自於 Tensorflow 官方教學文件

透過 BERT 進行文本分類

在開始進入正題之前,我們得先在 Colab 中建置環境。Colab 是線上的編譯工具,也是機器學習常用的程式平台,更是初學者剛學習 Python 的最佳途徑。之所以 ML 常用,是因為在上面可以調用 Google 分配的運算資源,讓運算速度可以比較快一點。在開始之前,讓我們先來建置一下環境。

環境建置

1
2
3
# A dependency of the preprocessing for BERT inputs
pip install -q -U "tensorflow-text==2.8.*"
pip install -q tf-models-official==2.7.0

搭建 BERT 通常會需要兩個步驟:資料前處理、模型建置。在這裡的資料前處理跟先前我們所學利用正規表達式來清理資料的那種前處理有一點差別。還記得我們先前學習正規表達式的時候,是為了要將文本中不需要的詞以及符號等等的刪除,只留下我們所需要的資料即可;但在搭建 BERT 時,所謂的前處理就不再是指清理資料了,而是將資料轉換成 BERT 可以理解的格式

接著我們在這邊將需要的套件引入程式中。

1
2
3
4
5
6
7
8
9
10
11
12
import os
import shutil
# 這兩個套件是為了要將訓練資料的路徑讀取工具也引進來

import tensorflow as tf # 就是 Tensorflow 不用多說
import tensorflow_hub as hub # Tensorflow bert 社群
import tensorflow_text as text # 前處理
from official.nlp import optimization # 優化工具

import matplotlib.pyplot as plt # 畫圖用的套件

tf.get_logger().setLevel('ERROR') # Debug的工具,也可以不寫

接下來就是要引入訓練的資料。我們在這邊引用網路上常用的 imdb 情感分析資料。在這邊可以看到套件 os 是為了要將路徑整合,使模型可以更方便讀取我們下載的訓練資料。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 這個是資料集的下載連結,如果你複製這段 url 到瀏覽器上,也可以下載的到
url = 'https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz'

dataset = tf.keras.utils.get_file('aclImdb_v1.tar.gz', url,
untar=True, cache_dir='.',
cache_subdir='')

# 所謂的 os.path.join 就是把兩個路徑結合起來的意思,如果路徑是 ./data/,join之後就會變成 ./data/aclImdb,而以下所做的,就是要整合資料路徑,方便之後模型讀取
dataset_dir = os.path.join(os.path.dirname(dataset), 'aclImdb')

train_dir = os.path.join(dataset_dir, 'train')

# remove unused folders to make it easier to load the data
remove_dir = os.path.join(train_dir, 'unsup')
shutil.rmtree(remove_dir)
1
2
3
Downloading data from https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
84131840/84125825 [==============================] - 7s 0us/step
84140032/84125825 [==============================] - 7s 0us/step

接下來就是把資料轉換成訓練資料集以及測試資料集,在這裡我們將資料以 8:2 的形式,將資料進行分割。官方文件是透過 keras 的 text_dataset_from_directory 函式中的一個參數來分割資料。至於所謂的 batch_size 就是每一批放進模型訓練的批量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
AUTOTUNE = tf.data.AUTOTUNE
batch_size = 32
seed = 42

raw_train_ds = tf.keras.utils.text_dataset_from_directory(
'aclImdb/train',
batch_size=batch_size,
validation_split=0.2,
subset='training',
seed=seed)

class_names = raw_train_ds.class_names
train_ds = raw_train_ds.cache().prefetch(buffer_size=AUTOTUNE)

val_ds = tf.keras.utils.text_dataset_from_directory(
'aclImdb/train',
batch_size=batch_size,
validation_split=0.2,
subset='validation',
seed=seed)

val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

test_ds = tf.keras.utils.text_dataset_from_directory(
'aclImdb/test',
batch_size=batch_size)

test_ds = test_ds.cache().prefetch(buffer_size=AUTOTUNE)
1
2
3
4
5
Found 25000 files belonging to 2 classes.
Using 20000 files for training.
Found 25000 files belonging to 2 classes.
Using 5000 files for validation.
Found 25000 files belonging to 2 classes.

最後我們可以得到訓練資料集 train_ds 以及驗證資料集 val_ds,以及最後的測試資料集 test_ds。如果我們看一下裡面的資料長什麼樣子,就會發現0代表負面影評,1代表正面影評。

1
2
3
4
for text_batch, label_batch in train_ds.take(1):
print(f'Review: {text_batch.numpy()[0]}')
label = label_batch.numpy()[i]
print(f'Label : {label} ({class_names[label]})')

這邊就不印出三份資料了,印出第一份讓大家來有點概念即可。

1
2
Review: b'"Pandemonium" is a horror movie spoof that comes off more stupid than funny. Believe me when I tell you, I love comedies. Especially comedy spoofs. "Airplane", "The Naked Gun" trilogy, "Blazing Saddles", "High Anxiety", and "Spaceballs" are some of my favorite comedies that spoof a particular genre. "Pandemonium" is not up there with those films. Most of the scenes in this movie had me sitting there in stunned silence because the movie wasn\'t all that funny. There are a few laughs in the film, but when you watch a comedy, you expect to laugh a lot more than a few times and that\'s all this film has going for it. Geez, "Scream" had more laughs than this film and that was more of a horror film. How bizarre is that?<br /><br />*1/2 (out of four)'
Label : 0 (neg)

記得我們先前所說的遷移學習(Transfer Learning)嗎?我們可以取用別人已經訓練好的預訓練模型,再針對預訓練模型加上我們自己的資料,讓模型找出特徵之後,來解決自然語言處理的下游任務。那該要到哪裡去下載別人已經預訓練好的模型呢?網路上有兩個常用的平台,一個是我們現在用的 Tensorflow 的 hub ,另一個則是大家更常用的抱臉怪(huggingface),在抱臉怪上有各式各樣別人從各領域的文本所訓練的預訓練模型,而大家可以在上面任意取用符合自己需求的預訓練模型。

而以下所做的,就是取用別人已經訓練好的模型,並把它用在我們的任務當中。

1
2
tfhub_handle_encoder = "https://tfhub.dev/tensorflow/small_bert/bert_en_uncased_L-4_H-512_A-8/1" # 這是預訓練模型
tfhub_handle_preprocess = "https://tfhub.dev/tensorflow/bert_en_uncased_preprocess/3" # 這是 BERT 前處理需要用的模型

以下就可以來研究看看輸入進去的資料會有哪些。

1
2
3
4
5
6
7
8
bert_preprocess_model = hub.KerasLayer(tfhub_handle_preprocess) # 呼叫前處理模型
text_test = ['this is such an amazing movie!'] # 先輸入簡單的句子看看會變成什麼樣子
text_preprocessed = bert_preprocess_model(text_test)
print(f'Keys : {list(text_preprocessed.keys())}')
print(f'Shape : {text_preprocessed["input_word_ids"].shape}')
print(f'Word Ids : {text_preprocessed["input_word_ids"][0, :12]}')
print(f'Input Mask : {text_preprocessed["input_mask"][0, :12]}')
print(f'Type Ids : {text_preprocessed["input_type_ids"][0, :12]}')
1
2
3
4
5
Keys       : ['input_type_ids', 'input_word_ids', 'input_mask']
Shape : (1, 128)
Word Ids : [ 101 2023 2003 2107 2019 6429 3185 999 102 0 0 0]
Input Mask : [1 1 1 1 1 1 1 1 1 0 0 0]
Type Ids : [0 0 0 0 0 0 0 0 0 0 0 0]

我們可以看到輸出總共分成 input_word_idsinput_mask、以及 input_type_ids。其中input_word_ids就是每個文字分配的識別碼,可以讓模型分別對應回去的數字。 input_mask 則是在進行 Masked 的程序(我的理解,若有錯還請不吝賜教),而input_type_ids則是會依照不同句子給予不同的id,也就是識別句子的功能。這邊是因為測試的句子只有一句,所以只會有 0 這個數字。

1
2
3
4
5
6
7
bert_model = hub.KerasLayer(tfhub_handle_encoder)
bert_results = bert_model(text_preprocessed)
print(f'Loaded BERT: {tfhub_handle_encoder}')
print(f'Pooled Outputs Shape:{bert_results["pooled_output"].shape}')
print(f'Pooled Outputs Values:{bert_results["pooled_output"][0, :12]}')
print(f'Sequence Outputs Shape:{bert_results["sequence_output"].shape}')
print(f'Sequence Outputs Values:{bert_results["sequence_output"][0, :12]}')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Loaded BERT: https://tfhub.dev/tensorflow/small_bert/bert_en_uncased_L-4_H-512_A-8/1
Pooled Outputs Shape:(1, 512)
Pooled Outputs Values:[ 0.76262873 0.99280983 -0.1861186 0.36673835 0.15233682 0.65504444
0.9681154 -0.9486272 0.00216158 -0.9877732 0.0684272 -0.9763061 ]
Sequence Outputs Shape:(1, 128, 512)
Sequence Outputs Values:[[-0.28946388 0.3432126 0.33231565 ... 0.21300787 0.7102078
-0.05771166]
[-0.28742015 0.31981024 -0.2301858 ... 0.58455074 -0.21329722
0.7269209 ]
[-0.66157013 0.6887685 -0.87432927 ... 0.10877253 -0.26173282
0.47855264]
...
[-0.2256118 -0.28925604 -0.07064401 ... 0.4756601 0.8327715
0.40025353]
[-0.29824278 -0.27473143 -0.05450511 ... 0.48849759 1.0955356
0.18163344]
[-0.44378197 0.00930723 0.07223766 ... 0.1729009 1.1833246
0.07897988]]

在輸出中,總共有三個不同需要注意的欄位:pooled_outputsequence_outputencoder_outputs

  • pooled_output:代表所有資料經由 BERT 之後所取出的 embedding。在這裡代表的就是電影評論資料整體的 embedding。
  • sequence_output:代表的是每一個 token 在上下文中的 embedding。

透過 BERT 取得了需要的 embedding 之後,接下來就是要進行下游任務,在這裡就是進行文本分類。

Tutorial 在這裡用一個函式來定義模型搭建:

1
2
3
4
5
6
7
8
9
10
def build_classifier_model():
text_input = tf.keras.layers.Input(shape=(), dtype=tf.string, name='text')
preprocessing_layer = hub.KerasLayer(tfhub_handle_preprocess, name='preprocessing')
encoder_inputs = preprocessing_layer(text_input)
encoder = hub.KerasLayer(tfhub_handle_encoder, trainable=True, name='BERT_encoder')
outputs = encoder(encoder_inputs)
net = outputs['pooled_output']
net = tf.keras.layers.Dropout(0.1)(net)
net = tf.keras.layers.Dense(1, activation=None, name='classifier')(net)
return tf.keras.Model(text_input, net)

模型架構如下:

Source: Tensorflow

接下來就是要用 Cross entropy 來計算損失函數,前面沒有介紹到損失函數。簡單的說,損失函數就是在進行梯度下降時,模型在評估與正確答案的相近程度時所計算的最小化最佳解問題,即為負對數似然(negative log-likelihood)。

接著設定後面的模型參數。Learning rate 則採取原論文建議的參數:3e-5。原論文建議三種參數,分別為5e-5、3e-5、2e-5。至於 epoch、batch、以及 learning rate,大家可以來看這位前輩寫的文章。最後在這裡將優化參數加進 create_optimizer 函式中,以利後續在編譯時對模型進行優化。

1
2
3
4
5
6
7
8
9
10
11
12
13
loss = tf.keras.losses.BinaryCrossentropy(from_logits=True)
metrics = tf.metrics.BinaryAccuracy()

epochs = 5
steps_per_epoch = tf.data.experimental.cardinality(train_ds).numpy()
num_train_steps = steps_per_epoch * epochs
num_warmup_steps = int(0.1*num_train_steps)

init_lr = 3e-5
optimizer = optimization.create_optimizer(init_lr=init_lr,
num_train_steps=num_train_steps,
num_warmup_steps=num_warmup_steps,
optimizer_type='adamw')

最後就是開始訓練:

1
2
3
4
5
6
7
classifier_model.compile(optimizer=optimizer,
loss=loss,
metrics=metrics)
print(f'Training model with {tfhub_handle_encoder}')
history = classifier_model.fit(x=train_ds,
validation_data=val_ds,
epochs=epochs)
1
2
3
4
5
6
7
8
9
10
11
Training model with https://tfhub.dev/tensorflow/small_bert/bert_en_uncased_L-4_H-512_A-8/1
Epoch 1/5
625/625 [==============================] - 90s 136ms/step - loss: 0.4784 - binary_accuracy: 0.7528 - val_loss: 0.3713 - val_binary_accuracy: 0.8350
Epoch 2/5
625/625 [==============================] - 83s 133ms/step - loss: 0.3295 - binary_accuracy: 0.8525 - val_loss: 0.3675 - val_binary_accuracy: 0.8472
Epoch 3/5
625/625 [==============================] - 83s 133ms/step - loss: 0.2503 - binary_accuracy: 0.8963 - val_loss: 0.3905 - val_binary_accuracy: 0.8470
Epoch 4/5
625/625 [==============================] - 83s 133ms/step - loss: 0.1930 - binary_accuracy: 0.9240 - val_loss: 0.4566 - val_binary_accuracy: 0.8506
Epoch 5/5
625/625 [==============================] - 83s 133ms/step - loss: 0.1526 - binary_accuracy: 0.9429 - val_loss: 0.4813 - val_binary_accuracy: 0.8468

接著就可以來看模型表現如何了!

1
2
3
4
loss, accuracy = classifier_model.evaluate(test_ds)

print(f'Loss: {loss}')
print(f'Accuracy: {accuracy}')
1
2
3
782/782 [==============================] - 59s 75ms/step - loss: 0.4568 - binary_accuracy: 0.8557
Loss: 0.45678260922431946
Accuracy: 0.855679988861084

我們在這裡可以得知模型的準確率約為 0.86。

結語

其實若你實作過一遍,就會知道 BERT 的訓練時間,相較於前面所介紹過的任何一種模型,像是 LSTM、RNN等這些深度學習模型來說,體感上有非常明顯的差距。BERT 光是這麼一點點資料,就可能會需要訓練到快一個小時,資料一多,甚至可能要跑到一週兩週都是家常便飯,所以這也是 BERT 一個最大的缺點之一。

另外,我國的中研院所開發的 CKIP 繁體中文預訓練模型,也發佈在 Huggingface 上了,可以點我 進去看看,CKIP透過 BERT 完成了命名實體辨識、詞性標註等任務,我們也可以基於模型,再訓練符合下游任務的模型。除此之外,Huggingface 上面有很多好玩的模型實作,大家有興趣也可以去玩玩看,比如說像是文本生成、QA等等的 demo,或許大家可以對自然語言處理有更多的想像空間,這樣的社群需要你我一起來建構與發揮。

真正掌握權力的人,通常都躲在表面上有權力的人後面,操控著一切。
法蘭西斯・安德伍德《紙牌屋》

這幾天在研究 BERT 的時候想著,如果要拿流行文化來比喻的話,可以用什麼。嗯…利用別人達到更好的成就、亦正亦邪,表現上看起來風風光光,但其實細思極恐,突然想到,還能有比紙牌屋的主角法蘭克還要更好的比喻了嗎?法蘭克也是為了要達成自己的目標,不斷踩在別人(屍)身前進,表面上看起來很厲害,但站上高位之後,卻也容易被別人陷害,受到影響。


Source: CNN(小知識:現在 NETFLIX 開頭的「咚咚」就是出自於這一幕喔!)(沒人在乎)

我想我大概是全世界第一個把 BERT 跟紙牌屋拿來放在一起比較的人了吧!

幹話說完,該進入正題(終於嗎?)昨天我們提到兩階段的遷移式學習(Transfer Learning) 以及 BERT 的訓練方法,還有 BERT 可以完成的那些下游任務。今天來仔細一一了解 BERT 的內部構造吧!

預訓練模型

在昨天的文章中有提到,原文獻將遷移學習(Transfer Learning)分成了兩階段,第一階段為Pretraining、第二階段則為模型微調。首先,讓我們來看看預訓練模型在一開始是如何訓練出來的。預訓練時,原文獻的作者將模型訓練分成了兩個任務,一個是克漏字填空(Masked Language Model),另一個則是下文預測(Next Sentence prediction)。

克漏字填空


首先,模型會先把文本中的字分成 character-level。如果你還記得的話,在中文中,character-level就是一個字一個字。接著再利用特殊字元 [MASK] 把其中一個字遮住。

在 BERT 中,共有五種特殊字元。

  • [CLS]:分類相關的資訊會放在這個 token 中,讓模型知道我們要進行分類,通常在下游任務才會有用。關於這個標籤是否應該存在,目前也有些爭議,大家如果有興趣看更深一點的文章,可以往這裡去。
  • [SEP]:用於下文預測的預訓練任務中。用於分隔兩個句子的 token。
  • [UNK]:在 BERT 的字典中沒有的字元,會用此 token 取代。
  • [PAD]:記得我們在循環神經網路模型中,為了將句子調整至相同長度,會將長度不夠的句子補足到一樣的長度,那用的就是 [PAD] token。
  • [MASK]:就是克漏字專用的 token。

所以說,克漏字填空的訓練資料就會如下:

1
['[CLS]', '等', '到', '潮', '水', '[MASK]', '了', ',', '就', '知']...

在進行訓練的時候,會將訓練資料中 15% 的詞彙遮蓋住。接著,這些輸入資料通過 BERT 之後,會得到一個 output embedding(還記得 embedding 嗎?)再透過 Linear Multi-class Classifier ,讓模型猜測(決定)被遮蓋住的字是哪一個字。模型之所以有辦法預測答案,是因為被遮蓋掉的字,其上下文的 embedding,一定會與那個被遮蓋掉的字接近,從這些 embedding 接近的字詞中去挑選最接近的,作為模型預測的正確答案。

我很喜歡李宏毅老師在課堂中做的比喻:

如果兩個詞彙填在同一個地方沒有違和感,那它們就有類似的 embedding。

下文預測


至於下文預測也跟克漏字填空接近。在這裡,作者給了 BERT 兩個句子,讓 BERT 判斷兩個句子之間是否有相互關聯。[CLS] 標籤會放在句首,這是因為在 BERT 之中使用的是 Transformer 的 encoder,其中的 Self-attention 機制,讓標籤位置並不受位置的影響。所以放在句首,仍然可以獲得所有句子的資訊。

所以說,上下文預測放入模型的資料如下:

1
['[CLS]', '等', '到', '潮', '水', '[MASK]', '了', ',', '就', '知'] 

下游任務

當預訓練模型準備完成之後,就可以用這個模型針對我們想要完成的下游任務進行調整。前面的兩個預訓練任務就是為了要幫助以上這四個下游任務所進行的,如同昨天所是,共有四個任務:單一句子分類任務、單一句子標註任務、成對句子分類任務,以及問答任務。

其中克漏字填空為的就是單一句子分類以及標註。

  • 單一句子分類任務:多對一,我們將文本資料放進去經過微調的 BERT 模型之後,模型訓練得到 embedding,再透過訓練所得的線性分類器(Linear Classifier),將句子分類到特定類別去。例如:判斷句子情緒是正向還是負向。
  • 單一句子標註任務:多對多,將文本資料放入 BERT,在每一個節點各有一個線性分類器,藉由分類器取得對應類別,例如:POS tagging。

另外,下文預測的訓練目標就是成對句子分類任務以及問答任務。

  • 成對句子分類任務:輸入兩句不同的句子,BERT 會藉由放在[CLS]中的分類資訊,對這些句子進行分類。例如,給出一段前情提要,接著放入一段句子,使模型判斷兩者是否相關,或是可否從前情提要中推斷出這段句子。
  • 問答任務:將問題以及包含答案的文檔放入 BERT 之後,BERT 會將訓練所的資訊再透過先前訓練所得的 https://chart.googleapis.com/chart?cht=tx&chl=qhttps://chart.googleapis.com/chart?cht=tx&chl=k,以及 https://chart.googleapis.com/chart?cht=tx&chl=v ,並經過激勵函數 softmax,從文檔中找出合適的答案。

問題與討論

每一個章節最後一定都要來討論一下優缺點,BERT即使作為當代最厲害的語言模型,仍然也是有他的壞處,而我們則必須衡量這些優缺點,來決定最適合的解決方案。先讓我們來看看 BERT 有哪些缺點吧,當然這裡就不再提深度學習模型的學習時間較為耗時(是要講幾遍?)。

Allyson Ettinger 發表的What BERT Is Not: Lessons from a New Suite of Psycholinguistic Diagnostics for Language Models 論文中透過實驗以及以心理語言學的角度,探討了 BERT 的可能缺陷。這裡來一一整理:

  1. 難以基於常識處理複雜的語意推斷

從圖中,BERT 可以基於第二句的內容推斷空格中該填入的字,但是卻無法基於第一句的內容來對第二句的空格進行推斷。例如:

Pablo 想要砍一些柴來做木櫃。於是他詢問了鄰居是否可以借他______ 。

人類可以很快地推斷出空格應該填入如斧頭、電鋸等可以砍柴的工具,這是基於我們看過第一句話之後所做的推斷,認為 Pablo 應該要借的是可以砍柴的工具。但模型只能基於第二句的上下文進行推斷,無法像我們一樣基於生活常識進行推斷,於是預測了像是車、房子等詞彙,這就顯示了 BERT 的限制。

  1. 難以預測基於語意角色的事件

BERT 同樣也難以判斷基於語意角色的事件。所謂的語意角色是一個語言學專有名詞,簡單來說就是在句中的參與某個事件或是帶有某個狀態的「參與者」。在圖中的範例,作者刻意將主詞對調,發現 https://chart.googleapis.com/chart?cht=tx&chl=BERT_{BASE} 還是會預測相同的詞彙(推測大概是因為 Self-attention 的機制),而且雖然 https://chart.googleapis.com/chart?cht=tx&chl=BERT_LARGE 沒有預測跟前句相同的詞彙了,但預測結果仍不理想。兩者仍然都無法預測出適當地符合句意的詞彙。例如:

紮營者回報有小女孩被熊______了!

模型可以正確預測出「攻擊」、「啃咬」,但一旦兩者對調,變成:

紮營者回報有熊被小女孩______了!

模型預測的詞彙仍然是「攻擊」、「啃咬」。可以發現 BERT 同樣難以推斷詞彙之間的關係,並預測出符合常識的語句。

  1. 難以推斷否定句

其中提到 BERT 在推斷具有否定含義的語句時,準確度較肯定直述句還要低。從圖中可以看到說,肯定直述句的案例中,BERT 推斷出的詞放在空格中都是合理的,只是一但加上否定詞,BERT卻仍然會判斷相同的詞彙。例如:

Robin 是 _____。

模型可以很好地推斷出 Robin 是隻鳥,或是一個人。一旦變成否定句:

Robin 不是 _____。

BERT 仍然會在空格中填入 Robin。同樣也足見 BERT,或者是說 Attention 機制的缺點。

好,今天說到這!

Source:

  1. 進擊的 BERT:NLP 界的巨人之力與遷移學習
  2. 李宏毅_ELMO, BERT, GPT
  3. What BERT Is Not: Lessons from a New Suite of Psycholinguistic Diagnostics for Language Models

在這裡,你可以盡情揮灑你的想像力,不覺得這很令人難以想像嗎?
《芝麻街》Elmo

在過去幾天的旅程中,我們了解如何利用各種神經網路模型來幫助我們處理不同自然語言處理任務,計算語言學家接著也基於前面講過的那些神經網路模型,陸續發展了許多其他新型態的語言模型,例如:基於BiLSTM的ELMo。我想應該是研究太苦悶了,或者說是一種惡趣味吧?自然語言處理專家特別喜歡用芝麻街的角色來為這些語言模型命名,就算是硬湊名稱,也要把它湊成芝麻街角色的名字(e.g. 今天要介紹的 BERT 就是一個案例)。假如說,現在小小孩想上網抓 Elmo 的照片,結果卻出現一堆莫名其妙的節點跟線的連接圖,會被嚇哭吧? 計算語言學家都是邪惡的。

不過如果你還有印象,就會記得 BiLSTM 儘管記憶力已經比 RNN 還要好了,但人工智慧學家對仍然不滿足,他們認為如果要了解一個字在文章中的「定位」,就必須要讓模型理解全文才能真正決定。所以接下來學者又開發出了 Transformer ,而其中的 Self-Attention 機制,使每一個字對每一個字來說「沒有位置的概念」。好,我知道有點玄,就套句台大李宏毅老師所說:

海內存知己,天涯若比鄰。

這時候,兩個有關係的字,即使離很遠,模型也能理解之間的關係。就是這樣,Transformer 的 Self-Attention 機制,就可以完全解決 RNN 系列的天生缺陷。而今天要介紹的 BERT 就是基於 Transformer 所搭建的模型。

在 2018 年時(其實沒有很久以前喔),Google 集先前所有語言模型大成,打造了 BERT,並釋出了模型及原始碼,幫助處理各式各樣的下游任務,而這個 BERT 的表現,以及準確度,都到了迄今仍無人能超越的地步。

如果有一天,有個模型幫助我們完成任何事,那該有多好?

話說得有點誇張,不過當時 BERT 模型的釋出,確確實實地撼動了計算語言學界與自然語言處理業界。在那篇被引用了四萬多次的論文(BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding)之中,介紹了如何用 BERT 進行兩階段的訓練方法,幫助後續自然語言的處理任務。

預訓練模型的訓練方法:遷移學習(Transfer Learning)

如果說能夠先訓練出一個對自然語言有一個大致了解的模型,並且基於這模型再針對各種不同下游任務進行調整,這種方法稱為:遷移學習(Transfer Learning)。這篇論文的作者就是為了實現這一個目標,將 Transformer 中的 encoder、大量的文本,以及針對目標進行的預訓練,來搭建整個 BERT 模型。而作者為了要達成這個目標,將訓練過程分成兩個階段:

  1. 預訓練(Pretraining)
    在論文中有提到,為了要訓練這樣的模型,首先會進行兩種預訓練任務,分別為克漏字填空、以及下文預測。對於我們人類來說,要理解自然語言是輕而易舉的事,但對於電腦來說卻是一件難如登天的任務。我們一一來說明模型的訓練過程。
    • 克漏字填空(Mask Language Model)
      比較專業一點的名詞稱為 Masked Language Model(MLM)。之所以會說這是克漏字填空,其實因為這完完全全就是我們高中在學英文時最討厭的那種題型,只是我們把它拿給機器來做。大家在圖中可以看到,這些句子在丟進去模型進行訓練的時候,會把一些字詞遮蓋起來,讓模型去預測被遮蓋起來的字詞是什麼。若是以圖中李宏毅老師的簡報為例的話,我們人類很快就可以知道 Mask
    • 下文預測(Next Sentence Prediction)
      英文又叫做 Next Sentence Prediciton (NSP)。其實就是預測下一個可能的句子是什麼。要以這種方式對模型進行預訓練,其實就是為了要解決例如文本生成,或是 QA 的任務。

  1. 模型微調(Fine-Tuning)
    前面的模型訓練結束後,再針對不同的下游任務進行微調。在論文中,作者將模型進行微調後,都達到了空前的正確率。這部分的詳細做法,我們留到明天再來詳細講解。今天只會簡單地介紹 BERT,讓各位先有個概念即可。這些任務包括了以下四種,分別為:單一句子分類單一句子標註成對句子預測,以及問答任務。明天我們再詳細介紹。

跟使用 LSTM 訓練的模型有什麼不一樣?

看到這邊,大家可能想說,咦?那跟先前介紹的使用 LSTM 訓練的模型,有什麼差別?好像就是個很厲害的語言模型不是嗎?為什麼好像 BERT 獲得較高的關注?不公平啊!其實使用 LSTM 訓練的模型跟 BERT 有一些結構上還有運用上的差異及差異,這就讓我們一一來看看是怎麼回事吧!

  • 雙向架構: 因為前面所說的克漏字填空任務,相較於過去介紹過的模型來說,可以幫助模型確實地「雙向」理解文本。也就是說,雖然説是雙向,但事實上透過 Transformer 中的自注意力機制,對所有字來說,彼此之間的關係「既很近又很遠」,你可以好像就在我身邊,但你其實卻又遠在天邊 就像你的女朋友一樣 。那因為這樣的特性,讓 BERT 模型確實地展露出 「天涯若比鄰」的概念。所以說 BERT 實際上並不是雙向,但是卻有著雙向的特性,達成了雙向欲達成的目標,因此有了比雙向還要更優秀的理解上下文的能力 這就是為什麼我說其實 BERT 並不是 Bidirectional 的緣故,但人工智慧學家為了湊出 BERT 硬給了他一個 B 開頭的字
  • 適合大量資料訓練: 相較於其它用 LSTM 訓練而成的深度學習模型,用 Transformer 訓練的模型可以透過平行運算的方式,而這很適合幫助進行資料大的模型訓練。所謂平行運算,簡單來說,就是模型可以將一份大資料分成很多個小部分資料進行計算,比起以往全部的資料一次計算的方式來說,平行運算的速度快非常多,而 BERT 適合這種方式。

好,今天就講到這,明天就上我們拿起放大鏡,來一一了解 BERT 的訓練過程以及下游任務的應用囉!明天見。