米蘭牛角尖

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

0%

“You know what possibly means?”
“Like probably.”
“No, probably means there’s a good chance that we’re going. Possibly means we might, we might not.”
Chris Gardener《當幸福來敲門》

我們終於進入機器學習啦!就像每個初學程式的人都一定要印個Hello World!,每個剛入門機器學習程式的同學,一定也是要先從貝式分類器(Naive Bayes classifier)開始啦!如同前面所說,所謂的語言模型(Language modeling)就是將每個詞出現的可能化成機率,並以機率的角度去看待整個語言,那貝氏分類器同樣也是一個基於機率的分類器,不過在了解貝氏分類器之前,我們得先知道什麼是貝氏定理。

貝氏定理

大家想必對這四個字可是熟悉又陌生,或許上次見面是高中吧,你心想。只是自己卻又好像沒有很瞭解這是什麼,就像是久久沒聯絡的同學一樣,在一次共同好友的婚禮上見到了,卻不知道要說些什麼,只能說些客套的話。這是什麼JOJO的奇妙比喻

扯遠了。其實貝氏定理就是在描述在一些已知的條件下,某件事情發生的比率。

比如說,我們已經知道伴侶的交友圈,以及伴侶的回家時間,跟他/她是否有第三者有關,那麼使用貝氏定理,就可以透過得知以上資訊,來推斷你的伴侶外遇的機率。假如今天你是一位異性戀女性,你的男朋友每天晚上都說會載女同事回家,所以每天幾乎都要快十點才會到家,你說你很在意,但是男友卻說他們只是朋友關係。某天,男友說,他要載他乾妹妹回家,你問男友說他要載誰,他說就是之前那個同事,但男友一走,卻一直到隔天早上才回家,請問男友有第三者的機率多高?

  • 事件A: 你男友有乾妹妹
  • 事件B: 有第三者介入你們感情

首先,在確定你男友有第三者之前,對你男友是否有乾妹妹這件事情會有一個基本機率判斷,因此我們稱你男友有乾妹妹的機率為P(A),也就是事前機率。而在確定你男友有第三者之後,我們會重新判斷你男友是否有乾妹妹的機率。亦即,若你男友有第三者之後,男友有認乾妹妹的機率作為P(A|B),也就是事後機率。

相反亦然,在確定你男友是否有乾妹妹之前,會先判斷你男友有第三者的機率,也就是事前機率P(B)。但在確定你男友有乾妹妹之後,會重新判斷你男友在有乾妹妹的前提下,有第三者的機率是多少。也就是說,若你男友認了乾妹妹之後,男友有第三者的機率為P(B|A),也就是事後機率。

貝式學習

至於貝式學習就是要找到一個假設,並以這個假設為前提,找到發生這個事件的最大機率。舉例來說,P(有第三者|有乾妹妹)=0.69,而P(沒有第三者|有乾妹妹)=0.31,貝氏分類器就會將有乾妹妹的特徵分類到「會有第三者」,這是因為0.69 > 0.31。也就是說,在貝式分類器所求的為:

自然語言處理上會怎麼運作?

如上所述,若今天以分類正負面電影評論為例,我們在進行文本計算時,事前機率P(+)就是所有文章中,正面文章的比例;而事前機率P(-)就是所有文章中,負面文章的比例。

假如說今天的測試文本為「電影好看!」,那麼斷詞之後會變成電影/好看。接著若是以詞頻作為特徵的話,就是要計算這兩個詞分別在訓練資料中正面評價出現幾次,以及在負面評價中出現幾次。另外為了避免出現機率為零的狀況,也會將公式進行smoothing。也就是像這樣。我們直接賦予這些機率一個數字。

要注意的是,以上都是計算這些詞在訓練資料集出現的條件機率。現在要計算測試文本「電影好看!」會是正面還是負面影評的話,就會是這樣算。

最後可以得知,正面評價的機率大於負面評價,因此機器學習模型會將文本分類到正面評價。

問題討論&優缺點

不免俗,文章最後一定都要來討論一下模型的優缺點。貝氏分類器作為早期的分類器,具有堅實的數學基礎,也因此較容易解釋(記得前面我們提到解釋力有多重要),即使是面對許多的缺漏值,也可以很好的進行分類處理小資料進行分類的時候,也有很優秀的表現。但貝氏分類卻有一個致命缺點,也就是貝氏分類假設屬性間互相獨立,但往往在現實中卻不是如此,所以後續也有許多改良版的模型,如半樸素貝氏分類器。

好,貝氏分類就講到這裡,明天要來講羅吉斯回歸囉!

「孩兒,你看清楚了沒有?」「看清楚了」
「都記得了沒有?」「已忘記了一小半。」
「現下怎樣了?」「已忘記了一大半。」
「好,我再使一遍。」
「孩兒,怎樣啦?」「還有三招沒忘記。」
「這我可全忘了,忘得乾乾淨淨的了。」
張無忌、張三丰《倚天屠龍記》

前面講了這麼多,終於要講模型了嗎?我們這十天一起度過的旅程,目的只有一個,就是將文本資料轉化成數值資料。因為機器學習模型沒有辦法透過文字資料進行學習,唯有將其轉成數值資料,模型才有辦法從這些數字中學習洞見。前面所學都是非常重要的基礎,我認為要打好這些基礎,再來學習後面的機器學習的模型才有意義。就像是武俠小說中,渾厚內力才能帶來強大武功,若沒有強大的內力,即便你會九陽神功、九陰真經、降龍十八掌,也只是空有華麗的外殼、空虛的內在;像是死神中,強大的個體都帶有很強的靈壓,若靈壓不夠強,即便你所擁有的是規則殺的能力,在強大的靈壓面前都是毫無用處的。

今天的文章,你可以帶走這些機器學習的基本知識:

  • 資料集&交叉驗證(Dataset & cross validation)
  • 分類指標(classification matrix)
  • 監督式學習&非監督式學習(supervised learning vs unsupervised learning)
  • 生成式模型 vs 判別式模型(generative model vs discriminative model)

資料集&交叉驗證

想像今天你有一份已經經過完善的前處理、以及正常的正規化,也就是可以直接丟進去模型的完整資料集。一般來說,我們會將資料集依照比例分成兩份:一份訓練資料集(training set)、一份測試資料集(test set),而這個比例有時候是8:2,有時候是7:3,依照個人配置會有所不同,沒有絕對的正確答案。首先,先給予機器學習模型訓練資料集,讓模型學習資料中的特徵,接著再給訓練好的模型進行考試,以模型預測測試資料集的目標資料(例如正負面評論),並將這些答案與原始資料的「正確答案」(Golden label)進行比對。

交叉驗證

若是碰到資料量較小的狀況,抑或是你的資料極度不平衡的情況,就要對資料進行交叉驗證。所謂的交叉驗證就是,除了前面所提到的訓練資料集、測試資料集、還會再多分出新的一部分的開發測試資料集。首先會將資料集分成k個,然後選其中一部分做為剛剛多分出來的開發測試資料集,並以另外k-1份的資料集作為訓練資料集,並計算準確率,接著再從那k份資料中選擇下一份資料作為開發測試資料集,直到所有的資料集都已經輪過後,再以這份模型去測試測試資料集。

分類指標

我們剛剛說到,模型預測之後,要與正確答案(Golden Label)進行比對。那麼該怎麼呈現這個比對結果,來決定一個模型的表現好壞呢?我們可以先來看以下這張圖:

假如模型預測A文本是正面評價,與正確答案相符,那麼我們就會將其納入true positive;假如模型預測B文本是負面評價,正確答案也標記為負面評價,那麼就屬於true negative;假如模型預測C文本是正面評價,但正確答案卻是負面評價,那麼就屬於false positive;若模型判斷D文本是負面評價,但事實上卻是正面評價時,就屬於false negative。

接著就會產生了以下四種衡量的方式:

在做二元分類時,可以透過以上的表格來衡量模型,這矩陣也稱為confusion matrix。而我們可以這麼理解這些指標:

  • 準確率(accuracy):整體預測的正確率
  • 召回率(recall):真正對的答案內,有多少比例是機器模型答對的
  • 精準率(precision):機器模型認為正確的答案內,有多少比例是真正答對的
  • F1-Score:所有衡量標準的調和平均

監督式學習 vs 非監督式學習

  • 監督式學習模型
    監督式學習就是將已經做好完整標記的資料丟進去機器學習模型中,模型可以邊學習,邊以標記答案對比誤差,並一步步修正模型。 由於需要標記資料,所以前置工程往往需要大量的人工標記,耗費人力較高,但相對於非監督式學習模型來說,監督式學習的解釋力也比較高(想想昨天所說的,解釋力為何重要)。常見的模型種類像是:貝氏分類器、羅吉斯回歸、決策樹。
  • 非監督式學習模型
    非監督式學習就是模型透過完全沒有標記的資料,依照關聯性進行歸類、找出潛在規則與模式,以及形成集群。 這種學習方式雖然不需要大量地標記資料,但卻可能找出從人類的角度來看關聯性極低的不重要特徵,使得分類變得毫無意義。

生成模型 vs 判別模型

我們可以透過這個比喻來理解何謂生成式模型,何謂判別式模型。假如說我們今天需要模型判別圖片中的動物是貓貓還是狗狗,生成式模型會先透過訓練資料的照片學習狗與貓的 整體大致長相 ,接著在測試資料的照片判斷圖中的動物比較接近貓貓還是狗狗,最後再下判斷。判別式模型則是透過照片學習貓貓有哪些特徵、狗狗有哪些特徵,例如貓貓有長鬍鬚,狗狗較常吐舌頭,接著再在新圖片中觀察是否有這些觀察到的特徵。

所以我們大致上可以這麼解釋:

  • 生成式模型
  1. 先學習訓練資料中整體文本的大致分布方式,並以機率的方式呈現,找出文本資料的機率分佈。
  2. 給予新文本,並判斷新文本是否有類似分佈
  3. 給予分類
  • 判別式模型
  1. 從訓練文本資料中觀察不同類別的特徵
  2. 從測試文本中觀察是否有相對應的特徵
  3. 給予分類

好,大致上就是這樣,我們接下來就可以進入機器學習的領域啦!剩下二十天,一起加油!

「模型有什麼盲點,反映其創造者的判斷,也反映創造這重視哪些東西。雖然模型據說是公正的,他們其實反映某些人的目標和意識形態。」
凱西・歐尼爾《大數據的傲慢與偏見》

我們前面提到了語言模型(Language modeling),是以統計機率的角度來看待語言(是否合理?我可能要先打個問號),所以為了要進行統計,過去通常會將語言以數值的方式呈現。昨天提到了幾種方法,首先是詞袋,只是有幾個缺點,第一是維度災難,太高的維度會給予電腦太高的負擔;第二是資料太稀疏,模型無法判別出資料的差異。所以自然語言處理專家後續又再發展出了幾種方法來改善這些缺點,像是昨天介紹的TF-IDF,以及今天要介紹的N-Gram以及Word2Vec。

不過什麼叫做以機率的角度來看待語言,我們可以先來看看以下例子:

1
2
3
4
"""
1. 魯夫說他想要成為海賊王
2. 說海賊王成為想要魯夫他
"""

今天在我們所說的那些自然語言中,第一句出現的機率絕對會比第二句高吧?也就是說,為了要「預測」後面會出現什麼句子或是詞語,或是在進行語音辨識、拼字矯正、文字校正這些較不清楚的資料輸入時,每個詞出現的機率在這時就變得特別重要。而N-Gram就是建立在這個基礎之上所訓練而成的模型方法。

N-Gram

N-gram代表由N個字所組成的字串,一個字叫unigram,兩個字叫bigram,三個字叫trigram。以前面海賊王的句子為例,bigram在例句一中,就會以 魯夫/夫說/說他… 的形式呈現,並賦予每一個bigram可能的出現機率。這種方式相較於最近頂尖流行的神經網路、BERT,是一個相對單純的建模方式外,也是了解語言建模基礎概念的重要基礎工具。以下我們將以bigram為例,來為您介紹N-gram的概念。

Bigram在這邊要做的,就是計算以前面的字為前提之下,後面的字會出現的機率是多少。比如說,今天我們有「海」這個字,後面是「賊」的機率是多少,因為前面是「海」的時候,後面接的也有可能是「洋」、「底」、「馬」。這該怎麼進行計算呢?

各位可千萬不要被這些數學符號嚇到了,其實就是我們高中所學的條件機率!同樣是以前面「海」為例的話,分母其實就是所有以「海」為開頭的詞語,其出現次數;至於分子就是我們要計算的「海賊」,其出現的次數。那假如今天要建立一個簡單的bigram模型,該怎麼做?讓我們來看看以下例子,有個簡易的範例語料庫:

1
2
3
<s>魯夫是海賊王</s>
<s>魯夫戴草帽</s>
<s>索隆砍敵人</s>

要計算這些bigram機率的話,可以這麼計算:

1
P(魯|<s>)= 2/3 P(夫|魯)= 2/2 P(是|夫)= 1/2

這邊就不把所有計算都列出來的,相信聰明的各位一定可以舉一反三吧!

透過這種方式所訓練出來的模型,就可以得到其訓練資料中的機率分配,而透過這些機率分配,就可以做到很多事情,比如說今天我們要運用這個模型來自動生成句子,那模型就可以從這些機率分配中抽出具有較高機率出現的字,也就比較不可能產生機率較低的字。

特性及優缺點

  1. n越大,模型出來的效果越好(但相對的運算消耗量也越大)
  2. 內容高度依賴訓練資料集,所以測試的資料跟訓練的資料,其domain相去甚遠時就不太有用,因此選擇的訓練資料要按照task的內容
  3. 完全無法處理訓練資料集中沒看過的資料(OOV, Out-of-voculary)

Word2Vec

跟前面學習下一個字出現的機率完全不同的概念,我們在這邊將不會計算文字w在文字x出現的次數,而是訓練一個分類器(classifier),來預測文字w是否有可能會出現在文字x附近,也就是把所有訓練資料壓在一個向量空間中,而這個分類器所得的權重,也就是所謂的詞嵌入(word embedding)。最後訓練出來的這些詞嵌入,會將不同的詞以向量的形式呈現,並透過計算的方式來理解詞與詞之間的關係。

以這張圖來舉例的話,機器學習模型就可以藉由男人(Man)與女人(Woman)的embedding來理解國王(King)以及皇后(Queen)之間的關係。當我們可以將詞轉換成向量時,就可以進行許多跟相似度有關的應用,比如說文本相似度、辭意相似度。

大家是否還記得Wordle,當時也有人透過Google所訓練的Word2Vec,做了一個猜字網站Semantle,大家有興趣可以去玩玩看,其實漸漸就可以發現,這不一定是我們所認知的近義詞,而是依照訓練資料的內容所得的「近義詞」。

特性及優缺點

看起來這麼厲害的詞向量,也是有它的優缺點,而按照慣例,我們也要來了解一下詞向量跟詞嵌入有什麼優缺點,以及有什麼特性需要我們去注意的。

刻板印象

由於詞向量是透過過去的大量文本資料所訓練而得的,所以其實某種程度上這些向量也隱含著人類過去隱含在文本中的一些暗示及毛病,也就是說,這也高度依賴著訓練資料。這些訓練資料若是包含對性別的歧視或刻板印象,那麼訓練出來的詞向量,也會有這種含義。例如:訓練出來「男人」相對於「程式工程師」,此時再輸入「女人」,帶有偏見的詞向量就會回答「家管」。這樣的訓練結果並不合適,因為假如程式設計公司今天要招募工程師,並用自然語言處理模型替履歷進行分類時,就可能因此將所有女性姓名全部放到「不合適」的類別,反倒更加深了性別刻板印象。

代表性問題

同理,過去的這些「有毒的資料」也會導致模型對族群有錯誤的連結。就有研究顯示,在Word2Vec中,非裔美國人的名字較容易與負面詞彙連結,而歐裔美國人較容易與正面詞彙進行連結;或者是,男人較容易與數學、工程等詞進行連結,而女人與藝術、文學的詞向量距離也比較近。

結語

說了這麼多,其實就是要了解到,在人工智慧越來越進步的時代,更多的是要探討這些模型 對人類社會可能造成的影響。 也就是說,人工智慧應該是幫助人類越來越進步的工具,是相輔相成的存在,我們不應該因為人工智慧,就放棄了身而為人所應該堅持的基本價值。 該怎麼做? 人工智慧、機器學習模型的運作必須是透明的,訓練資料來源必須是透明的、產生結果以及原因必須是透明的 ,這也就是我認為人工智慧應該以加強解釋力的方向持續努力的原因,更是人類在大數據時代實踐公平以及負責任所應該要學習的議題。

「拿定主意了嗎?你能成大器,你知道,在你一念之間,史萊哲林能幫助你走向輝煌,這毫無疑問——不樂意?那好,既然你已經拿定主意——那就最好去葛來芬多吧!」
分類帽《哈利波特:神秘的魔法石》

這幾天下來我們學了正規表達式、斷詞等等,這些都是屬於資料前處理的範疇。是說,剛剛在寫稿前,還被我的隊友罵說,啊我的正規表達式怎麼寫那麼少!欸不是,在這邊跟各位解釋一下,我的規劃就是簡單介紹正規表達式的三個函式,然後正規表達式常用的三個函式就是那三個,如果用到第四個再去查就可以了,我甚至很懷疑有沒有第四個函式,這個系列的文章就是適合我引你進門,至於修行就是要看個人啦!如果還想要更詳細的介紹文章,那我大力推薦我隊友fish_in_bed以及cjon_06991寫的文章,內容真的很豐富,在這邊提供傳送門給各位。

OK!閒聊結束!

今天漸漸要進入自然語言目前主流的機器學習方法,預計將會介紹詞袋(Bag-of-Words, BoW)、TF-IDF,應該啦,如果我寫得了這麼多的話。

語言模型(Language Modeling)

不知道各位在第一次看到所謂「語言模型」時,想到的是什麼?是以語言學搭建的機器學習模型嗎?還是大量資料丟進去演算的人工智慧?(在這裡,我偏好使用自然語言處理)其實自然語言處理在過去的研究脈絡中,傾向於將各語言的最小單位視作一個一個的token,並透過這些token進行統計上的機率模擬及運算,因此也有些人也會稱其為統計語言模型(Statistical Language Modeling)

像是有一篇計算語言學學生必讀的經典論文 A Neural Probabilistic Language Model 摘要的第一行就寫著:

統計語言模型的目標是透過文字序列了解在語言中字與字之間的聯合機率。

其實就是將文字轉成數字的形式讓電腦理解啦!不過若你仔細一想,倒也很好可以理解為什麼需要從統計的角度去看待自然語言處理,這是因為電腦跟人腦不同,電腦無法直接處理文字資料,但電腦非常擅長處理數值資料的演算,因此自然語言處理領域有一大半都是將文字轉成數值資料的研究。

而我們接下來要介紹的就是三種經典文本轉數值的常見方法。

詞袋(Bag-of-Words)

前人種樹,後人乘涼。另外一本計算語言學的必讀經典 Speech and Language Processing 其中的一張圖就完美詮釋了詞袋(Bag of Words)的概念。我們前面已經介紹過斷詞,其實詞袋就是將斷詞過後所得到的這些詞類作為一個一個的袋子,接著將這些字一一丟進這些袋子裡面。這就是詞袋的概念,這種計算詞頻的方式也是詞袋的其中一種做法。

另外一種做法,則是以獨熱編碼(One-Hot Encoding)的形式呈現其實根本沒有人會用中文吧? 也就是說,先找出詞袋後,詞袋中的詞序為隨機,接著對照每一個句子,若句子有詞袋中的字,則編碼為1,無則0

讓我們來看看以下例子:

  • 斷詞後的結果
    1
    2
    魯夫/說/他/想要/成為/海賊王
    娜美/說/他/想要/描繪/世界海圖
  • 詞袋
    1
    ['魯夫', '娜美', '説', '他', '想要', 成為', '描繪', '海賊王', '世界海圖']
  • 獨熱編碼(One-Hot Encoding)
    1
    2
    [1, 0, 1, 1, 1, 1, 0, 1, 0]
    [0, 1, 1, 1, 1, 0, 1, 0, 1]
    我們可以發現詞袋像是一條模板一樣,將它跟句子比對之後,接著賦值。這麼一來,每一個句子就會有屬於自己的encoding,並利用這種方式賦予每一個句子獨立的存在。但就如同前面我們不斷建立的一個觀念一樣,沒有最好或是最差的方式,只有最適合的方式。既然是最早的文字轉數值方法,一定有這種方法的缺點:
  1. 維度災難(Disaster of Dimensionality):假如說今天的文本詞類高達1000多種,那輸入資料的維度也就高達1000多維,意即有1000多個欄位,這是何其驚人!如此龐大的維度其實並不利於電腦的機器學習,可能會造成維度災難(Disaster of Dimensionality),也對電腦是一大負擔。
  2. 資料稀疏(Data sparsity):當資料越來越多,詞彙也跟著越來越多時,你可能會發現說,這麼一來在編碼內的0也會越來越多。這種情形稱為資料的稀疏性(Data sparsity)。相對於其他數值上的機器學習,這在NLP中是一個很嚴重的問題。這是因為資料一旦稀疏,就難以讓演算法判別不同句子之間的差異性(記得之前提到BoW的一個很大重點是句子之間的差異嗎?)更不用說語言具有無限可能。若出現其他新的模型沒看過的資料,BoW就比較沒辦法處理了。

那麼後人為了解決這些問題,TF-IDF以及Word2Vec出現了。

TF-IDF

TF-IDF,是一種在一個大語料庫中,找出每一篇文章關鍵字的計算方式。顧名思義,包含了字詞頻率(Term Frequency)以及逆向文件頻率(Inversed Document Frequency),並將兩者相乘,所得的分數稱為TF-IDF。

Term frequency (TF)

記得前面的斷詞所分成的token嗎?TF就是這些token在文本中的出現頻率,再除以那篇文章的字數,這是因為文本的篇幅都有長有短,一篇落落長的文章,詞彙出現的次數當然就可能會比較多囉,為了避免這種狀況,通常都會除上文章字數,這樣子的過程叫做將文本正規化

檔案https://chart.googleapis.com/chart?cht=tx&chl=d_%7B%7Bj%7D%7D中共有$k$個詞語
https://chart.googleapis.com/chart?cht=tx&chl=%7B%5Cdisplaystyle%20n_%7Bk%2Cj%7D%7Dhttps://chart.googleapis.com/chart?cht=tx&chl=%7B%5Cdisplaystyle%20t_%7Bk%7D%7D在檔案$d_$中出現的次數
https://chart.googleapis.com/chart?cht=tx&chl=n_%7B%7Bi%2Cj%7D%7D:該詞在檔案$d_$中的出現次數
https://chart.googleapis.com/chart?cht=tx&chl=%7B%5Csum%20_%7Bk%7Dn_%7B%7Bk%2Cj%7D%7D%7D:在檔案https://chart.googleapis.com/chart?cht=tx&chl=d_%7B%7Bj%7D%7D中所有字詞的出現次數之和。

Inversed Document frequency (IDF)

那為了要減少term frequency在某些文章中過高,或者是有些字在文章中雖然很常出現,但是卻不代表任何意義的情形(e.g. a, an, the),所以也要乘上一個數,可以使其值減低。對機器學習有一點概念的朋友,我認為可以把IDF想像成Penalty。Inversed Document frequency計算的則是以語料庫中的文本數除以文本中含有token的文本數,接著再取自然對數。白話的意思就是,若某些字集中出現在某幾篇文本中,代表這些字在這些文本中越重要,idf就越高,反之,若某些字在幾乎所有文本中都有出現,代表這些字其實並不那麼重要,idf就越低。

https://chart.googleapis.com/chart?cht=tx&chl=%7CD%7C:語料庫中的檔案總數
https://chart.googleapis.com/chart?cht=tx&chl=%7C%5C%7Bj%3At_%7B%7Bi%7D%7D%5Cin%20d_%7B%7Bj%7D%7D%5C%7D%7C:包含詞語https://chart.googleapis.com/chart?cht=tx&chl=t_%7B%7Bi%7D%7D的檔案數目

下面提供簡單程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from sklearn import feature_extraction  
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
corpus = ["魯夫 是 想要 成為 海賊王 的 男人",
"索隆 是 想要 成為 世界 最強 劍士 的 男人",
"娜美 想要 畫出 全 世界 的 海圖",
"香吉士 想要 找到 ALL_BLUE",
"騙人布 想要 成為 勇敢 的 海上戰士",
"喬巴 想要 成為 萬靈藥",
"羅賓 想要 找到 所有 歷史本文",
"佛朗基 想要 千陽號 航向 世界 盡頭",
"布魯克 想要 環繞 世界 重逢 拉布"]
vectorizer=CountVectorizer()
transformer=TfidfTransformer()
tfidf=transformer.fit_transform(vectorizer.fit_transform(corpus))
word=vectorizer.get_feature_names()
weight=tfidf.toarray()
for i in range(len(weight)):
print(u"-------這裡輸出第",i,u"類文字的詞語tf-idf權重------")
for j in range(len(word)):
print(word[j],weight[i][j] )
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
"""
all_blue 0.0
世界 0.0
佛朗基 0.0
劍士 0.0
勇敢 0.0
千陽號 0.0
喬巴 0.0
娜美 0.0
布魯克 0.0
想要 0.21155991248018197
成為 0.3582020693353289
所有 0.0
找到 0.0
拉布 0.0
最強 0.0
歷史本文 0.0
海上戰士 0.0
海圖 0.0
海賊王 0.552052456377027
環繞 0.0
男人 0.4662722935918962
畫出 0.0
盡頭 0.0
索隆 0.0
羅賓 0.0
航向 0.0
萬靈藥 0.0
重逢 0.0
香吉士 0.0
騙人布 0.0
魯夫 0.552052456377027
"""

問題與討論

被台灣教育荼毒過的對這五個字一定很熟悉,但很重要!不免俗,我們來討論一下TF-IDF可能需要特別注意哪些地方,以及缺點。

  • 語料庫的選擇很重要
    前面有提到這些TF-IDF的計算方法高度依賴語料庫中其他文本的詞類,因此當你要找關鍵字時,詞庫與詞庫間、文本檔案與檔案間的對應要特別留意,假如你要找的是某領域語料庫的關鍵字有哪些,那就不能用相同領域的對應語料庫(referent corpus),否則關鍵字就浮不出來了;同理,如果你要找的是某特定文本在某個語料庫中的關鍵字,那就要以「自己比自己」的方式,方可找到你要的結果。孰好孰壞?老話一句,看你的目的為何
  • 放大檢視優缺點
    優點就是簡單快速容易理解,但缺點的話,由於詞與詞之間都被切開來,單純以詞頻的角度去看待文章,這麼做反倒會忽略詞在文章中的位置(我們得要知道同一個詞在文章中的不同位置,可能代表不同的意義)另外,很少見的生澀詞彙可能會爆冷門變成關鍵詞,但重要的人名地名等等反倒不會出現在關鍵詞的前幾名中。

參考資料:

  1. NLP的基本執行步驟(II) — Bag of Words 詞袋語言模型
  2. Why is it a big problem to have sparsity issues in Natural Language processing (NLP)?
  3. 改进TF-IDF算法
  4. [python] 使用scikit-learn工具計算文字TF-IDF值

「隱藏著黑暗力量的鑰匙啊,在我面前顯示真正的力量! 跟你訂下約定的小櫻命令你,封印解除!」
《庫洛魔法使》木之本櫻

昨天我們學到了中文斷詞的方法,還有一些需要釐清的觀念,以及我個人的一些想法。中文學完了,那英文呢?英文的斷詞方法跟中文有什麼不同的地方嗎?我們今天就一起來瞧瞧世界通用的語言之一,英文,在自然語言處理中是怎麼進行的?

昨天我們提到了中文這個語言其中的一個特性是,所有字都是黏在一起的,而相對於英文來說,由於英文中間都會以空格分開,所以在這個問題上就不用像中文一樣考量比較多一點,其實這也告訴我們不同語言在自然語言處理上都會有不同的處理方式,沒有單一的最好處理方式,只有最適合的方法。

英文斷詞叫做tokenization,雖然說英文的斷詞並不像中文一樣全部都黏在一起,但是英文也有英文的難處。這是因爲英文的詞尾常常出現變化,比較兩種語言起來,中文卻可以說是完全沒有字尾變化。而英文這種詞尾變化在語言學裡面則稱呼為inflection,而如何處理inflection,就是英文的自然語言處理上一個常被討論的議題。


Resource: https://medium.com/data-science-in-your-pocket/tokenization-algorithms-in-natural-language-processing-nlp-1fceab8454af

我們就先來看看tokenization的做法吧!以下這段是由海賊王的英文版維基百科中所擷取下來的一段文字。我們要用的是自然語言處理很常用的函式庫,叫做nltk(Natural Language Toolkit),裡面提供了特別多的函式幫助你可以更好地進行自然語言處理。

1
inputSTR = "The story follows the adventures of Monkey D Luffy , a boy whose body gained the properties of rubber after unintentionally eating a Devil Fruit. With his pirate crew, the Straw Hat Pirates , Luffy explores the Grand Line in search of the deceased King of the Pirates Gol D Roger's ultimate treasure known as the One Piece in order to become the next King of the Pirates."
1
2
3
import nltk
from nltk.tokenize import (word_tokenize, MWETokenizer)
print(word_tokenize(inputSTR))
1
['The', 'story', 'follows', 'the', 'adventures', 'of', 'Monkey', 'D', 'Luffy', ',', 'a', 'boy', 'whose', 'body', 'gained', 'the', 'properties', 'of', 'rubber', 'after', 'unintentionally', 'eating', 'a', 'Devil', 'Fruit', '.', 'With', 'his', 'pirate', 'crew', ',', 'the', 'Straw', 'Hat', 'Pirates', ',', 'Luffy', 'explores', 'the', 'Grand', 'Line', 'in', 'search', 'of', 'the', 'deceased', 'King', 'of', 'the', 'Pirates', 'Gol', 'D', 'Roger', "'s", 'ultimate', 'treasure', 'known', 'as', 'the', 'One', 'Piece', 'in', 'order', 'to', 'become', 'the', 'next', 'King', 'of', 'the', 'Pirates', '.']

照慣例,我們先來看看輸出結果有什麼令人不滿意的地方。其中我們可以發現,有很多應該是要連在一起的字,由於tokenizer的緣故都被分開了,那其實這樣的結果會影響我們之後的下游任務,一個比較常見且簡單的方法是在這些應該連在一起的字中間加上底線,這樣就會被分成同一個token了。程式碼的部分可以這麼寫:

1
2
3
tokenizer = MWETokenizer([('Monkey', 'D', 'Luffy'), ('Devil', 'Fruit'), ('Straw', 'Hat', 'Pirates'), ('Grand', 'Line'), ('King', 'of', 'the', 'Pirates'), ('Gol', 'D', 'Roger'), ('One', 'Piece')])
tokenizer.tokenize(inputSTR.split())
print(tokenizer.tokenize (word_tokenize(inputSTR)))
1
['The', 'story', 'follows', 'the', 'adventures', 'of', 'Monkey_D_Luffy', ',', 'a', 'boy', 'whose', 'body', 'gained', 'the', 'properties', 'of', 'rubber', 'after', 'unintentionally', 'eating', 'a', 'Devil_Fruit', '.', 'With', 'his', 'pirate', 'crew', ',', 'the', 'Straw_Hat_Pirates', ',', 'Luffy', 'explores', 'the', 'Grand_Line', 'in', 'search', 'of', 'the', 'deceased', 'King_of_the_Pirates', 'Gol_D_Roger', "'s", 'ultimate', 'treasure', 'known', 'as', 'the', 'One_Piece', 'in', 'order', 'to', 'become', 'the', 'next', 'King_of_the_Pirates', '.']

等我們都斷好詞之後,若有需求的話,有兩種將整個文本標準化的方法,像小櫻一樣,讓這些字現出原形,展現出其真正的力量:一種是stemming、另一種則是lemmatization。


Resource: https://tr.pinterest.com/pin/706854104005417976/

stemming

stemming在於找出每一個字的字根。你可能也會發現這些stemming過後的字有些跟你印象中的字根不太一樣,所以要注意,這裡的字根並不一定是字典上所記載的字根,再加上這些字是以rule-based的方法打造而成的,只是為了找出每個字的較短形式。

1
2
3
4
from nltk.stem import PorterStemmer
ps = PorterStemmer()
word = ("adventure")
ps.stem(word)
1
adventur

lemmatization

跟stemming不一樣的是,lemmatization不只是把字切斷,而是透過字彙庫的建置,提供詞幹的結果。簡單來說就是比較正確的stemming啦!

1
2
3
4
5
import nltk
from nltk.stem import WordNetLemmatizer
lemmatizer = WordNetLemmatizer()
print(lemmatizer.lemmatize("adventures"))
print(lemmatizer.lemmatize("deceased"))
1
2
adventure
deceased

把他加入句子之後就會變成這樣:

1
2
3
4
tokenizer = MWETokenizer([('Monkey', 'D', 'Luffy'), ('Devil', 'Fruit'), ('Straw', 'Hat', 'Pirates'), ('Grand', 'Line'), ('King', 'of', 'the', 'Pirates'), ('Gol', 'D', 'Roger'), ('One', 'Piece')])
word_list = tokenizer.tokenize(inputSTR.split())
lemmatized_output = ' '.join([lemmatizer.lemmatize(w) for w in word_list])
print(lemmatized_output)
1
2
3
"""
The story follows the adventure of Monkey_D_Luffy , a boy whose body gained the property of rubber after unintentionally eating a Devil Fruit. With his pirate crew, the Straw_Hat_Pirates , Luffy explores the Grand_Line in search of the deceased King_of_the_Pirates Gol D Roger's ultimate treasure known a the One_Piece in order to become the next King of the Pirates.
"""

好的,那今天就介紹到這裡囉,明天就要來介紹TF-IDF以及Bag of Words囉!我們明天再見吧!

如果沒有了這套裝備,你就什麼都不是的話,那你就更不該擁有它。
東尼·史塔克《蜘蛛人:返家日》

在處理自然語言的時候,斷詞往往是首要的工作。其實這也很好理解,為什麼?我們可以從自然語言處理的目的開始來思考這個問題,也就是說,進行自然語言處理的目的是什麼?是要知道一篇正面或是負面評價的電影評論會用哪些詞彙嗎?還是要知道在一句話中,哪個是名詞?動詞?形容詞?或者是要知道這篇文章與其他篇文章比起來,有哪些關鍵詞?還是你今天要做一個文字雲,來搞清楚現在網路上最熱門的話題是什麼?

不知道你是否有發現,以上這些自然語言處理的目的,都有一個共通的特性:。也就是說,大部分自然語言處理的應用上,都需要將一句完整的句子,斷成一個一個的詞,或是斷成那個語言的最小單位,端看你的目的性是什麼。處理的語言更是另一個需要考量的點。以英文為例吧!在英文中,由於詞與詞之間都已經有空格了,所以相較於中文而言,英文考量的點比中文還要再稍微少一點點;至於中文,因為每個字都是黏在一起的,有時候單一個字就可以代表一個完整的意義,但有時候卻又需要兩個字才有意義。所以中文的斷詞在技術上還有許多的難處。想到我們所上的語意學教授有云:「中文就是一個很混亂的語言」。

話說回來,通常在計算語言學中,我們會稱這一個一個的詞為token。接著再藉由著一個一個的token來完成目的。

你可能已經想到了,既然這一個一個的token會被作為之後處理自然語言的主要材料,那斷詞的正確性,這時就顯得特別重要。另外,斷詞的Level還要語境也同樣不能被忽視。就拿當初來語言所面試時,現任老闆當時問我的問題為例。

今天我在國立政治大學語言所面試

想想看,如果是你,該如何斷詞才比較正確?今天如果斷詞工具將這句話斷成:

1
今天/我/在/國立/政治/大學/語言/所/面試

這是正確的斷詞結果嗎?我們等等再回來回答這個問題。先讓我們稍微了解一下有哪些斷詞工具吧!

斷詞工具

目前有三種大家比較常用的自然語言處理工具,分別是Jieba、CKIP,還有卓騰語言科技的Articut,以下來跟各位簡單介紹一下各個常用套件以及基本用法。

  1. Jieba

    Jieba(結巴)是海峽左岸的朋友所開發的斷詞套件,是現今華語界中最廣為人知的常見斷詞套件,不只是開源在GitHub上,更有在積極維護。由於是左岸朋友發明的斷詞套件,所以可想而知背後的訓練資料幾乎都是簡體中文的語料,像是人民日報等等的媒體文本,因此簡體中文的斷詞正確率較為出色。不過Jieba也有釋出專給繁體中文的斷詞工具,只是相較於簡體中文的表現下就真的稍微遜色了點,一些台灣用語或是在華語世界中只有台灣有的一些相關詞彙,Jieba的繁體中文斷詞相較簡體中文之下的表現,就相對比較沒有那麼地好。

    1
    pip install jieba
    1
    2
    3
    4
    import jieba
    inputSTR = "我要成為海賊王"
    segment_list = jieba.cut(inputSTR)
    print(" ".join(segment_list)) # 這邊將斷詞過後的結巴物件用join拆開來,並用空白字元分隔
    1
    '我要 成為 海賊 王'
  2. CKIP (Chinese Knowledge And Information Processing)

    前面有提到說,左岸朋友所開發的開源斷詞工具在繁體中文的表現上叫不如簡體中文,於是我國的中央研究院資訊所在1986年也開發了繁體中文專屬的斷詞工具:CKIP。既然是我國專為繁體中文開發,斷詞系統想必也是享負盛名,準確率也達到了97.49%(來源),但是因為模型偏大,所以運算時間相對來說也比較長。馬上一起來看看要怎麼用吧!

    1
    2
    3
    pip install ckiptagger
    pip install tensorflow
    pip install gdown
    1
    2
    3
    4
    5
    6
    7
    8
    from ckiptagger import data_utils
    data_utils.download_data_gdown("./")
    from ckiptagger import WS

    inputSTR = '我要成為海賊王'
    ws = WS("./data")
    segment_list = ws([inputSTR])
    print(segment_list)
    1
    [['我', '要', '成為', '海賊王']]
  3. Articut

    這是我前實習老闆的公司所開發的斷詞工具。個人認為,這是個以任何觀點來說都非常厲害的繁體中文專用斷詞工具。這是因為前面所提到的兩種工具,前者是由簡體中文所訓練而成,在繁體中文斷詞正確率上就有待加強,另外也是藉由機器學習模型打造的,後者也是透過統計方法或是機器學習模型(CRF、HMM等)佐以大量的訓練資料所打造而成的,而Articut正好可以克服前面所提到的兩個缺點,這全部都是因為同時有著語言學以及資訊工程背景的卓騰所開發的Articut,是以過去語言學家所研究的中文句法學作為理論基礎下去打造的斷詞工具,因此只要是中文的斷詞,皆有相當優秀的正確率,更因為這並非以任何統計模型訓練而成,所以運算速度也相當快。更厲害的是,也可以透過句法樹斷詞後所提供的資訊,推斷不同詞的詞性。

    不過由於是透過句法樹所推斷的資訊進行的斷詞,所以有些含有專有名詞的斷詞結果需要針對不同語境透過手動的調整,以加入自定義字典的方式來進行優化,雖然麻煩了一點點,但其實這點倒也提供了處理自然語言上不少的彈性空間。

    1
    pip install AritcutAPI
    1
    2
    3
    4
    from ArticutAPI import Articut
    articut = Articut()
    inputSTR = "我要成為海賊王"
    articut.parse(inputSTR = inputSTR, level="lv2")
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {'exec_time': 0.10455012321472168,
    'result_pos': ['<ENTITY_pronoun>我</ENTITY_pronoun><ACTION_verb>要</ACTION_verb><ACTION_verb>成為</ACTION_verb><ENTITY_nouny>海</ENTITY_nouny><ENTITY_nouny>賊王</ENTITY_nouny>'],
    'result_segmentation': '我/要/成為/海/賊王',
    'result_obj': [[{'text': '我', 'pos': 'ENTITY_pronoun'},
    {'text': '要', 'pos': 'ACTION_verb'},
    {'text': '成為', 'pos': 'ACTION_verb'},
    {'text': '海', 'pos': 'ENTITY_nouny'},
    {'text': '賊王', 'pos': 'ENTITY_nouny'}]],
    'level': 'lv2',
    'version': 'v256',
    'status': True,
    'msg': 'Success!',
    'word_count_balance': 1986,
    'product': 'https://api.droidtown.co/product/',
    'document': 'https://api.droidtown.co/document/'}

說完了,讓我們回到前面的問題,不知道你想到答案了沒有?

1
今天/我/在/國立/政治/大學/語言/所/面試

答案是:沒有正確答案。

其實從前面就有提示了,自然語言處理的方式很吃你所要達成的目的是什麼。假如說今天對你的任務來說,台灣不同大學之間的區別很重要的話,那麼就應該在自定義字典中加入「政治大學」,但如果相反而言,大學之間的區別對你的下游任務來說並不是特別重要,只是想要知道「大學」這個高等教育在文本中所扮演的角色的話,那其實將政治大學斷開也沒有關係。

斷詞真的那麼重要嗎?

前面有說到,斷詞是一個在自然語言處理中常用的技巧,但是你要說一看到文本就必須得立刻給他斷詞嗎?倒也是粗暴言論大可不必。我們強調了目的性的重要,那麼在做自然語言處理時,更重要的應該是對於要處理的語言有充分的了解,並審慎思考最適當的斷詞方式,而不是不考慮就通通給他直接斷下去,卻連文本大概長什麼樣子都不曉得。斷詞不是萬靈丹,更不應該是唯一的自然語言處理的第一步驟。也有些人主張,除了斷詞之外,也更應該強調詞語在文本中的位置,因為位置確實也會導致意義的不同,更會引領至不一樣的結果。

就像前言所提到的沒有裝備的蜘蛛人一樣,如果把斷詞當作自然語言處理中至高無上的存在,其他什麼都不管的話,那麼斷詞本身就會變得一點意義都沒有。反倒是有目的性、有理由地決定要斷詞,並且對文本有一定的熟悉程度後,靈活運用並適配最妥當的斷詞方法,才是一個真正的自然語言處理專家要克服的第一步。

我們確實曾經擁有一切,對吧?我是說,如果你仔細回想的話,就會發現確實是這麼一回事。
《華爾街之狼》

轉眼間,我們也來到了正規表達式的最終章,也就是萃取啦!有些反應比較快的朋友可能會想說,萃取不就跟第一週的re.search()一樣嗎?如果一樣要輸出匹配的字串的話,那就直接用re.search()不就好了?幹嘛還要再多學這個?

這是一個很好的問題,我們話不多說,就直接來看實例。這次我從維基百科的獵人 Hunter x Hunter條目中擷取了一小段文字,然後這次的任務是需要將這段文字中,每一篇獵人的篇章所在的漫畫話數全部都取出來,這樣才方便之後連載再開的時候直接找到需要複習的篇章。

1
2
3
4
5
6
7
蟻王篇 No.186-318(單行本18卷 - 30卷) # 我們要抓的地方
小傑與奇犽使用貪婪之島破關獎品打算去找金。但是在那裏等著的並不是金,而是凱特。凱特他們在未確認生物的探索和調查中,發現巨型嵌合蟻的殘骸,一行人前往NGL。小傑和奇犽瞭解嵌合蟻的強大,在凱特的犧牲下逃出NGL。在討伐隊還尚未準備完全之際,嵌合蟻王誕生了。王率領直屬護衛隊和一部份士兵,在背後統治東果陀共和國,打算進行『選別』(挑選)。為了防止更多的人死亡以及為凱特報仇,小傑一行人再次向嵌合蟻挑戰。
十二支會長篇 No.319-339(單行本30卷 - 32卷)# 我們要抓的地方
十二支登場,尼特羅會長於嵌合蟻之役死後,從獵人協會會長退位,開始選拔新的獵人協會會長。全世界的獵人為了新的獵人會長選舉一事紛紛開始活動;另一方面,奇犽為了拯救傷重的小傑而再度回到揍敵客家族。
暗黑大陸篇 No.340- (單行本32卷 - )# 我們要抓的地方
有天自稱尼特羅之子的比洋德·尼特羅在卡丁帝國支持下向全世界發表招募同伴前往暗黑大陸的影片,在十二支們接獲V5所發出的「追捕比洋德」命令的同一時間,比洋德主動前來被逮捕並要求交換條件──十二支需陪同他一起至暗黑大陸。同時雷歐力和酷拉皮卡加入十二支並接替十二支的子、亥之位置。
金以No.2身分加入比洋德雇用前往暗黑大陸的專家團隊,後來當卡丁帝國的船發出後,和其他包括12支的獵人也登上船,酷拉皮卡成為第八王妃奧伊特和第十四王子的保鑣,其目的為第四王子手上收藏的族人眼睛。船的目的地為鄰近暗黑大陸邊界的假想「新大陸」,航行過程中,王位爭奪戰正式展開。

先來看看符合字串的正規表達式怎麼寫,在本篇不會仔細說明正規表達式是如何運作的,但如果你需要詳細介紹的話,可以去看我第三天的文章

1
No\.\d{3}-\d{0,3}

稍微講解一下這段正規表達式:在No後面有一個小點點,要記得在前面加上逃脫字元\,不然.是正規表達式中的黑暗大法師,他會代表所有的字元,也會把所有的字元通通都吸進去,要小心使用。後面最後之所以是\d{0,3}是因為注意到暗黑大陸篇由於尚在連載中,所以還沒有結尾話數(我先哭)

先來看看,如果這時候用前天我們所學的re.search()會發生什麼事。

1
2
3
import re # 要記得引入函式庫
test_string = "蟻王篇 No.186-318(單行本18卷 - 30卷)小傑與奇犽使用貪婪之島破關獎品打算去找金。但是在那裏等著的並不是金,而是凱特。凱特他們在未確認生物的探索和調查中,發現巨型嵌合蟻的殘骸,一行人前往NGL。小傑和奇犽瞭解嵌合蟻的強大,在凱特的犧牲下逃出NGL。在討伐隊還尚未準備完全之際,嵌合蟻王誕生了。王率領直屬護衛隊和一部份士兵,在背後統治東果陀共和國,打算進行『選別』(挑選)。為了防止更多的人死亡以及為凱特報仇,小傑一行人再次向嵌合蟻挑戰。十二支會長篇 No.319-339(單行本30卷 - 32卷)十二支登場,尼特羅會長於嵌合蟻之役死後,從獵人協會會長退位,開始選拔新的獵人協會會長。全世界的獵人為了新的獵人會長選舉一事紛紛開始活動;另一方面,奇犽為了拯救傷重的小傑而再度回到揍敵客家族。暗黑大陸篇 No.340- (單行本32卷 - )有天自稱尼特羅之子的比洋德·尼特羅在卡丁帝國支持下向全世界發表招募同伴前往暗黑大陸的影片,在十二支們接獲V5所發出的「追捕比洋德」命令的同一時間,比洋德主動前來被逮捕並要求交換條件──十二支需陪同他一起至暗黑大陸。同時雷歐力和酷拉皮卡加入十二支並接替十二支的子、亥之位置。金以No.2身分加入比洋德雇用前往暗黑大陸的專家團隊,後來當卡丁帝國的船發出後,和其他包括12支的獵人也登上船,酷拉皮卡成為第八王妃奧伊特和第十四王子的保鑣,其目的為第四王子手上收藏的族人眼睛。船的目的地為鄰近暗黑大陸邊界的假想「新大陸」,航行過程中,王位爭奪戰正式展開。"
print(re.search("No\.\d{3}-\d{0,3}", test_string))
1
<re.Match object; span=(4, 14), match='No.186-318'>

我們會發現,如果只有使用re.search(),函式只會回傳符合的第一個字串,這時該怎麼辦?因為我們想要的是所有符合的字串,只回傳符合的第一個字串,就不符合我們的需求了。這時候就要好好地利用新的函式re.findall()了。re.findall()會找出所有符合的字串,並以串列的形式回傳。話不多說,就讓我們來試試看吧!

1
2
3
import re # 要記得引入函式庫
test_string = "蟻王篇 No.186-318(單行本18卷 - 30卷)小傑與奇犽使用貪婪之島破關獎品打算去找金。但是在那裏等著的並不是金,而是凱特。凱特他們在未確認生物的探索和調查中,發現巨型嵌合蟻的殘骸,一行人前往NGL。小傑和奇犽瞭解嵌合蟻的強大,在凱特的犧牲下逃出NGL。在討伐隊還尚未準備完全之際,嵌合蟻王誕生了。王率領直屬護衛隊和一部份士兵,在背後統治東果陀共和國,打算進行『選別』(挑選)。為了防止更多的人死亡以及為凱特報仇,小傑一行人再次向嵌合蟻挑戰。十二支會長篇 No.319-339(單行本30卷 - 32卷)十二支登場,尼特羅會長於嵌合蟻之役死後,從獵人協會會長退位,開始選拔新的獵人協會會長。全世界的獵人為了新的獵人會長選舉一事紛紛開始活動;另一方面,奇犽為了拯救傷重的小傑而再度回到揍敵客家族。暗黑大陸篇 No.340- (單行本32卷 - )有天自稱尼特羅之子的比洋德·尼特羅在卡丁帝國支持下向全世界發表招募同伴前往暗黑大陸的影片,在十二支們接獲V5所發出的「追捕比洋德」命令的同一時間,比洋德主動前來被逮捕並要求交換條件──十二支需陪同他一起至暗黑大陸。同時雷歐力和酷拉皮卡加入十二支並接替十二支的子、亥之位置。金以No.2身分加入比洋德雇用前往暗黑大陸的專家團隊,後來當卡丁帝國的船發出後,和其他包括12支的獵人也登上船,酷拉皮卡成為第八王妃奧伊特和第十四王子的保鑣,其目的為第四王子手上收藏的族人眼睛。船的目的地為鄰近暗黑大陸邊界的假想「新大陸」,航行過程中,王位爭奪戰正式展開。"
print(re.findall("No\.\d{3}-\d{0,3}", test_string))
1
['No.186-318', 'No.319-339', 'No.340-']

我們可以發現,如果使用re.findall()就能把所有的話數範圍找出來了!

好!那有關正規表達式的介紹就到這篇結束啦!明天開始就要進入斷詞方法了,大家抓緊好要出發了嗎?我們明天見囉!

你低著頭,他們就會知道你在說謊;即便你抬起頭,他們也會知道其實你根本不知道真相。如果只用四個字就能說明清楚,就別用七個字。身體不要搖來搖去,要用堅定的眼神看著對方的眼睛,但別一直盯著看。說話清楚,但別太讓人印象深刻,可以偶爾幽默一下,但是也不要過頭到讓他捧腹大笑。這麼一來,他就只會在當下對你有好感,而你一離開,他也會立刻忘了你這個人。而且看在老天的份上,無論你想做什麼,都別做,因為在任何情況下…
Rusty Ryan《瞞天過海》

昨天我們已經簡單地認識了什麼是正規表達式(Regular Expression),也瞭解了正規表達式的性質,更認識了正規表達式在Python的函式庫中最常見的功能之一,也就是搜尋功能。讓我們在這裡簡單複習一下,正規表達式可以運用單一個字串,藉以匹配所有符合某個句法規則的字串;而這些字串藉由各種不同函式,可以幫助我們針對文本進行驗證、萃取,以及今天所要介紹的替換功能。正規表達式的運用,加上Python各種不同函式的靈活搭配,是踏入自然語言處理的第一張門票,是一切資料處理的根基,因此掌握正規表達式的運用其實比你我想的還要再更重要一點點。

啊所以到底什麼才是替換功能?

讓我們把話題拉回正規表達式的替換功能。大家看到這裡可能還不清楚,所謂正規表達式的「替換」功能,實際上到底指的是什麼事情。就讓我們直接來看一個例子:

最近有所上同學問我説,她在搭建聊天機器人的時候,發現若是在iPhone按下系統推薦的字詞,會自動產生出一個空白字元" ",而這會導致機器人在判讀字串時產生錯誤。因為同學做的是查詢英漢字典機器人,機器人需要完整正確的字串,才有辦法依照字串找出正確的中文翻譯,但這時卻多了一個空白字元,該怎麼處理才好呢?比如說今天輸入的是ocean,但程式端所接收到的字串卻是ocean 。同學很苦惱。

其實對Python稍微有一點點研究的同學,第一個想到的函式大概是 .replace() ,程式如下:

1
2
3
4
message = "ocean "
inputSTR = message.replace(" ", "")
print(inputSTR)
# output: ocean

但以上這段程式碼好像也不能有效解決這位同學的問題,因為仔細想想,假如說我們今天想要知道 One Piece 是什麼意思?若套用以上的程式碼,反而會變成 OnePiece,像這種中間有空格的名詞,在計算語言學中,我們稱為proper noun(專有名詞),另外,儘管其實是有些差別,但有些人也會一併稱其為 entity(實體)。在這時,我們發現.replace()已經不管用了,這時該怎麼辦?這時我們就必須要想辦法「將字串尾的空白字元取代掉」。

在軟體工程中有個很重要的觀念,叫做Divide and Conquer,也就是將一個看似難以解決的問題分成很多個小問題,並且逐一擊破,最後即便是大問題也可以解決,大至軟體工程,小至正規表達式,都是一樣的道理。讓我們重新回來看這個問題,「將字串尾的空白字元取代掉」,其中第一個問題則是要找出「字串尾」。

在正規表達式中,若要表達指定字串的第一個字元,會用^來表示,也就是說,你要告訴電腦你想要的字串格式,其中的第一個字是什麼的時候,就可以透過^來表達;另外,同樣的,若要表達指定字串的最後一個字元,在正規表達式中則會以$ 來表示。再來會碰到的第二個問題則是「空白字元」。昨天,我們學到了數字的表達方式,也就是透過\d來代表數字。而空白字元在正規表示法中,則是以\s。這麼一來,我們就可以匹配所有字串尾有空白字元的字了。

1
\s$

欸?沒錯,就是這麼簡單。那麼接著就可以將這串正規表達式加入Python當中進行替換了。在正規表達式的函式庫中,將字串進行替換的函式為re.sub(),其中第一個參數放正規表達式,第二個參數則是放你想替換成的字符,在我們的例子裡就是「無」,第三個參數則是放要進行替換的字串。這麼一來,程式就可以寫成:

1
2
3
4
5
6
7
8
9
10
11
import re #記得要加入正規表達式函式庫
example_1 = "Ocean"
example_1_with_space = "Ocean "
example_2 = "One Piece"
example_2_with_space = "One Piece "
# 這邊我們都印出字串的最後一個字看看空白字元是否都已成功替換
print(re.sub(r"\s$", "", example_1)[-1])
print(re.sub(r"\s$", "", example_1_with_space)[-1])
print(re.sub(r"\s$", "", example_2)[-1])
print(re.sub(r"\s$", "", example_2_with_space)[-1])
print(re.sub(r"\s$", "", example_2_with_space))
1
2
3
4
5
n
n
e
e
One Piece

從結果我們可以發現幾件事:
一、最後一個字都不是空白字元,這代表空白字元已經成功地被我們的正規表達式進行替換了!
二、就算最後一個字不是空白字元,正規表達式沒有匹配,函式仍會回傳原本的字串!
三、One Piece中間的空白字元仍留著,這代表所有proper noun最後一個字都不會是空白字元!

進階的替換功能

前面我們提到了如何運用正規表達式將字串整理成我們理想中的形式,現在要來介紹一些正規表達式中比較進階的替換用法。先讓我們進入實例:

美國佬很喜歡跟全世界不一樣,無論是重量單位、長度單位,以及日期都跟其他國家的寫法不一樣,那今天我們的任務是要將美國日期寫法改成大家都習慣的形式。

1
2
# 美國日期寫法為 mm/dd/yyyy
# 其他國家的日期格式則為 dd/mm/yyyy

一樣,我們先來想想該如何以正規表達式將美國日期格式進行匹配。這邊可以回去看第三篇文章,其中提到的逃脫字元\,由於/也是屬於正規表達式的一員,所以這時就要透過逃脫字元\告訴電腦在這裡的/不是正規表達式,而是真正的 /。

1
\d{2}\/\d{2}\/\d{4}

這時問題就來了,該如何透過替換功能,將一串日期字串轉換成其他國家較常用的日期格式呢?不知你是否還記得,昨天我們提到可以將括號()作為群組(group),並透過re.search()將想要的群組輸出,這次我們就要透過先前學過的群組概念來進行替換。首先,先將想要替換的字串加入群組:

1
(\d{2})\/(\d{2})\/(\d{4})

圈好群組後,就可以透過re.match()來進行替換:

1
2
3
usa_date = "09/19/2022"
other_date = re.sub("(\d{2})\/(\d{2})\/(\d{4})", "\\2/\\1/\\3", usa_date)
print(other_date)
1
19/09/22

其中\\後面的數字就是群組的數字,就是這麼簡單!

好啦!那我們今天的旅程就到這邊先結束了,明天就要邁入正規表達式的最終章,也就是萃取!

自來修習內功,不論是為了強身治病,還是為了作為上乘武功的根基,必當水火互濟,陰陽相配,練了「足少陰腎經」之後,便當練「足少陽膽經」,少陰少陽融會調合,體力便逐步增強。
金庸《俠客行》

在進行自然語言處理的同時,常常會需要將資料變得乾淨,或是也有些特定的資料格式要從大量語料中抽取出來,比如說像是地址、電話等等,才有辦法取出我們想要的理想資料,而在這過程中常常會用到的就是正規表達式(regular expression),正規表達式也會被簡稱為re。re可以針對符合的字串大致上分成三種功能:搜尋、替換,以及抽取。

正規表達式

讓我們先以手機號碼舉例,假如我們要從大量語料中抽取手機號碼,例如:0912-345-678,該怎麼做呢?首先,編寫regular expression一個很重要的觀念是要一個字一個字看,並輸入對應的正規表達式。回到手機號碼,我們觀察手機號碼0912-345-678,可以發現全部都是用數字組成,數字的正規表達式為\d,我們首先可以將手機號碼以這種方法表示:

1
\d\d\d\d-\d\d\d-\d\d\d

但是這樣並不是一個足夠有效率的寫法,若在如\d後面加上大括號,則可以指定其出現次數。我們可以發現以橫槓分隔,第一組是由四個數字,後面則是由兩組三個數字所組成。逗號的分隔方式代表其次數,{1,}為一次以上,{,2}為兩次以下,{1,3}則為一到三次。

1
\d{4}-\d{3}-\d{3}

但若要取出符合這個樣式的第一組數字的話,則可以透過小括號,將這些數字分組,並在撰寫程式時指定回傳特定組的字串,整串符合的字串為group 0,第一組,四個數字的,也就是group 1,以此類推。寫法如下:

1
(\d{4})-(\d{3})-(\d{3})

那假如今天要找的是電話號碼,像是(01)234-5678時,裡面有小括號,該怎麼辦才能讓程式知道這裡得括號並不是前述分組的意思?這時我們可以利用逃脫符號\告訴編譯器,這裡的括號是指真的括號,任何會跟正規表達式衝突的符號,都可以透過這種方法進行編寫,寫法如下:

1
\(\d{2}\)\d{3}-\d{4}

這邊只有介紹到數字的寫法,欲得知其他寫法,如英文字母等等的,可以參考以下這個 cheatsheet,另外若要確認自己寫的正規表達式是否符合的話,也可以運用這個網站 regex101 來進行比對。

搜尋

前面有提到說,regular expression有進行搜尋的功能,我們可以將正規表達式的字串放入函式中,確認資料中是否有我們想要的字段。這時,可以運用re函式庫中的search()來達到我們的目的。第一個參數放正規表達式,第二個參數放欲檢驗的字串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import re # 記得加入re函式庫
print(re.search("(\d{4})-(\d{3})-(\d{3})", "0987-654-321"))
# output: <re.Match object; span=(0, 12), match='0987-654-321'>

# 輸出的意思是說,若有符合的字串,就會回傳正規表達式的物件
# span則為符合物件的字串在語料中的位置為何,像這邊是整串符合
# 那就是從index = 0 的地方開始到12都是符合的字串。
# match則是符合的字串。

print(re.search("(\d{4})-(\d{3})-(\d{3})", "0987654321"))
# output: None

# 若沒有符合字串,就會回傳None

# 小應用是,也可以把這個當作條件判斷式的條件之一,例如:

if re.search("(\d{4})-(\d{3})-(\d{3})", "0987-654-321"):
print(123)
# output: 1234
# 這邊因為re.search()有符合字串,判斷為True,執行指定動作

if re.search("(\d{4})-(\d{3})-(\d{3})", "0987654321"):
print(456)
# 判斷為False,不進行任何動作

記得前面說的分組概念嗎?在這裡就可以派上用場了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print(example_re_obj.group(0))

# 你也可以指派一個變數給他,如:
example_re_obj = re.search("(\d{4})-(\d{3})-(\d{3})", "0987-654-321")

# 記得前面說的分組概念嗎?在這裡就可以派上用場了。
print(example_re_obj.group(0))
# output: 0987-654-321
print(example_re_obj.group(1))
# output: 0987
print(example_re_obj.group(2))
# output: 654
print(example_re_obj.group(3))
# output: 321

好的,大概就是這樣,完成了這步驟,接下來就可以往「替換」以及「抽取」的目的地前進了!我們明天見!

萬事開頭難,難就難在初始階段。在此期間,必須窮盡心力,使諸方面保持均衡,以利於今後的發展。
法蘭克・赫伯特《沙丘》

在一起踏上偉大的航道前,由於在這三十天會運用Python作為主要的程式語言,所謂工欲善其事,必先利其器,畢竟就連魯夫一開始出海前,也是需要一艘船嘛!所以免不了總是要稍微解釋一下在之後的文章會用到的簡單語法以及概念。

但因為在我之前已經有許多大神以及前輩寫了許多Python的教學文章,Python開發者社群也有翻譯官方文件,而且翻譯得非常清楚,所以今天這篇文章只會解釋Python中最常用的語法,也就是基本的資料型態、for迴圈以及if條件敘述式,還有function,也會解釋一份Python的程式,大致上會有什麼架構,畢竟大學的計算機概論教授有云:「你只要會寫for迴圈以及會寫if,你會寫程式啦!」有點誇張了,但相信你可以的。

不過如果之後的文章用到其他在這裡沒有解釋到的語法,也會在那篇文章特別介紹。

資料型態以及資料結構

Python共有種的資料型態以及種的資料結構,不同的型態與結構,在不同的使用情境下有不同的用途,以下幫助你簡單理解:

字串 string

字串顧名思義就是單純字串,一般來說會以雙引號來表示,例如"偉大航道"就是一種字串,但單引號也可以,像是'偉大航道',只是大部分都會用雙引號,而且字串也不能進行數學上的四則運算,在程式編寫上要特別留意。性質如下:

1
2
print("1"+"2"+"3") 
# output: 123

你也可以將其他種資料型態透過str()轉換成字串:

1
2
3
4
num = 123
num_string = str(num)
type(num_string)
# output: str

整數 int & 浮點數 float

整數就是整數,這不需要特別說明。浮點數就是有小數點的數字,也就是小數!而這兩種資料型態分別可以透過int()以及float()來轉換。這兩種資料型態可以進行四則運算,讓我們來看看以下例子:

1
2
print(1+2+3)
# output: 6

前面已經了解到,資料型態也可以用對應的函式互相轉換,但要特別注意的是,不同資料型態不能混在一起運算。

1
2
3
4
5
6
7
8
print(123+"123")
# output:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-3-a752e60be039> in <module>
----> 1 123+"123"

TypeError: unsupported operand type(s) for +: 'int' and 'str'

但浮點數跟整數間可以進行四則運算嗎?我們來試試看:

1
2
print(1.23+123)
# output: 124.23

這兩者之間由上可知是可以進行四則運算的喔!

串列 list

list用中括號表示,可以將前述講到的資料,像火車一樣一個一個分別存在不同車廂裡面,並以逗號分隔。即使不同資料型態也可以一起放進去。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 建立新的空串列,並命名為example
example = []
# 預設資料
example = [1,2,3]
# 將資料加入串列的最尾端
example.append(123)
print(example)
# output: [1, 2, 3, 123]
# 不同的資料型態也可以存在同一個串列中
example.append("海賊王")
print(example)
# output: [1, 2, 3, 123, "海賊王"]
# 轉換方式則可以透過list(),如:

字典 dictionary

dictionary則是用大括號表示,是python特有的資料型態,像是字典一樣,每個字都有其對應的意義。那麼當值與值之間互相對應的時候,就可以透過dict()來表示:

1
2
3
4
5
6
7
8
9
10
11
# 建立新的空字典,並命名為example
example = {}
# 字典形式分成key跟value,每個key有對應的value,並用冒號表示:
example = {
"魯夫": "船長",
"索隆": "戰鬥員",
"娜美": "航海士",
}
# 並透過以下方式取出值(value):
print(example['魯夫'])
# output: 船長

Python語法

那接下來就讓我們來看看可以透過哪些方式操作以上這些資料吧!

for迴圈

for迴圈,顧名思義,如果你需要重複做某件相同的事時,就可以透過for迴圈讓程式來幫你做,你可以指定迴圈運行的次數,或是讓迴圈「遍歷」串列或是字典的key或是value,但方式有點不一樣。常用的寫法如下:

1
2
3
4
5
6
7
8
9
10
example = [1, 2, 3, 123, "海賊王"]
for i in example:
print(i)

# output:
# 1
# 2
# 3
# 123
# 海賊王
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 字典本身不能遍歷,但可以遍歷key或是value
example = {
"魯夫": "船長",
"索隆": "戰鬥員",
"娜美": "航海士",
}
for i in example.keys():
print(i)
# output:
# 魯夫
# 索隆
# 娜美
for i in example.values(): # 要加s
print(i)
# output:
# 船長
# 戰鬥員
# 航海士

for迴圈也有另外一種常見的寫法,我們來看看:

1
2
3
4
5
6
7
8
9
example = [1, 2, 3, 123, "海賊王"]
for i in range(len(example)):
print(i)
# output:
# 1
# 2
# 3
# 123
# 海賊王

其中用到了兩個之前沒用過的函式,分別是range()以及len()range()中的參數代表的就是讓i運行這個次數,len()則是串列的長度,那這行就是讓for迴圈運行串列長度的次數。

if條件敘述式

當程式達到某特定條件時,可以透過if,指定程式執行其他動作,一般會在迴圈中使用。用法如下:

1
2
3
4
5
example = [1, 2, 3, 123, "海賊王"]
for i in range(len(example)):
if i == "海賊王":
print(i)
# output: 海賊王

若有不同條件,則可以透過elif來指定條件。

1
2
3
4
5
6
7
8
9
example = [1, 2, 3, 123, "海賊王"]
for i in range(len(example)):
if i == "海賊王":
print(i)
elif i == 1:
print(i)
# output:
# 海賊王
# 1

若執行了if就不會執行elif,若包含elif條件就只能擇一。但如果全部都寫if就所有條件都會檢驗一遍。

1
2
3
4
5
6
7
8
9
10
11
12
example = [1, 2, 3, 123, "海賊王"]
for i in range(len(example)):
if i == "海賊王":
print(i)
elif i == 1:
print(i)
example = [1, 2, 3, 123, "海賊王"]
for i in range(len(example)):
if i == "海賊王":
print(i)
if i == 1:
print(i)

最後前面的條件都不符合,但也要對其進行處理,則可以用else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
example = [1, 2, 3, 123, "海賊王"]
for i in range(len(example)):
if i == "海賊王":
print(i)
elif i == 1:
print(i)
else:
print("我是else")
# or
example = [1, 2, 3, 123, "海賊王"]
for i in range(len(example)):
if i == "海賊王":
print(i)
if i == 1:
print(i)
else:
print("我是else")

跳出迴圈可以用break,繼續執行迴圈可以用continue

1
2
3
4
5
6
7
8
example = [1, 2, 3, 123, "海賊王"]
for i in range(len(example)):
if i == "海賊王":
break
if i == 1:
continue
else:
print("我是else")

def 函式

不知道大家還記不記得國高中的函數,給定一個x值,函數會計算出y值;同理,一樣是function,程式中也會有所謂的函式,括號中給定參數,函式則會回傳值,或是不回傳值只執行特定動作也可以。當你發現一直在重複相同動作又要寫很多次的時候,就是函式派上用場的時候了。

1
2
3
4
5
6
7
8
9
def example_func(parameter_1):
result = 1 + parater_1
return result

# 在這裡你可以回傳值,也可以將回傳值指配給變數,如:

result_var = example_func(2)
print(result_var)
# output: 3

好的!大致上會用到的基本語法就是這樣,若想知道更多可以去看官方說明文件,網路上也有很多的教學,明天就要進入偉大航道了,我們明天見。