天天看點

《Arduino家居安全系統建構實戰》——2.4 實作分類器load "NaiveBayes.fs"

本節書摘來異步社群《機器學習項目開發實戰》一書中的第2章,第2.4節,作者:【美】mathias brandewinder(馬蒂亞斯·布蘭德溫德爾),更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

我們已經讨論了許多數學和模型方面的内容,但是還沒有讨論編碼。幸運的是,這就是我們所需要的一切:現在,我們已經為實作一個簡單的貝葉斯分類器做好了準備。從根本上說,分類器依賴于兩個要素:将文檔分解為标記的标記化程式,以及一組标記——用于求得文檔得分的單詞。有了這兩個元件,我們需要從一個示例樣本中知道每個标記的垃圾短信和非垃圾短信得分,以及每一組的相對權重。

圖2-4概述了學習階段。從具備标簽的消息語料庫開始,将它們分解為兩組:垃圾短信和非垃圾短信,并計量其相對規模。然後,對選中的一組标記(“free”“txt”和“car”),計量其頻度,将每一組文檔歸納為對應于其總體權重的得分,為每個标記求得特定組别的得分。

得到上述資訊之後,新文檔的分類遵循圖2-5中概述的過程:标記化文檔,根據存在的标記計算每組得分,并預測最高得分的組。

在這個特殊的例子中,一旦文檔标記化,我們搜尋每個單獨标記(忽略其他情況)是否都有一個得分,計算每組的總體得分,确定哪一組更可能比對。

《Arduino家居安全系統建構實戰》——2.4 實作分類器load "NaiveBayes.fs"

為了避免在腳本中聚集過多代碼,我們将分類器提取為一個子產品。這隻需用滑鼠右鍵單擊解決方案,選擇“建立項”(add new item),然後選擇“源檔案”(source file)。将檔案改名為naivebayes.fs,删除其中的所有預設代碼,用如下代碼代替:

open naivebayes.classifier

hello "world"<code>`</code>

■ 注意:

你可能覺得疑惑,為什麼必須将剛建立的檔案上移。和c#不同,f#解決方案中的檔案順序很重要,主要原因是類型推理系統。編譯器檢視到目前為止代碼檔案中已有的定義,自動了解類型的含義。類似地,如果打算在代碼中使用函數f,該函數必須在使用它的代碼之前定義。這是一個附加的限制,但是在我看來,相對于類型推理的好處,這種代價非常合理。而且,作為有趣的副作用,這提供了f#解決方案中的自然順序,使它們容易解讀:從第一行開始正向閱讀,或者從最後一行開始反向閱讀!

有了子產品,我們就可以在子產品中寫入上述算法,然後使用腳本檔案以該算法探索資料。我們将遵循典型的f#模式,由底向上建構,編寫小的代碼塊組成更大的工作流。模型的關鍵要素是得分計算,這個功能計量文檔屬于某一組(如垃圾短信)的證據強度。

得分取決于兩個成分:該組在整個語料庫(訓練資料)中出現的頻度,以及該組中找到某些标記的頻度。我們首先定義表現問題域的兩個類型。我們不将文檔和标記定義為字元串,而是直呼其名,定義一個token類型——字元串的類型别名。這将使我們在類型簽名上更加清晰。例如,現在可以定義一個tokenizer(标記化程式)函數,以一個字元串(文檔)為 參數,傳回一組标記。類似地,使用tokenizeddoc類型别名,為已經标記化的文檔提供一個名稱:

type docsgroup =

假定某個文檔已經分解為一組标記,計算給定組的得分相當簡單。我們沒有必要擔心這些數字的計算方法。如果這一部分已經完成,需要做的就是加總分組頻率的對數,以及每個标記出現在已标記化文檔中和模型使用的标記清單中頻率的對數:

程式清單2-2 計算文檔得分

let classify (groups:(_*docsgroup)[])

你可能對classify函數中的groups:(docsgroup)[]有些疑惑,為什麼對每組文檔使用(doctypedocsgroup)元組,神秘的符号是什麼?請你仔細思考,到目前為止,我們所做的并不依賴于具體的标簽。我們考慮了垃圾短信和非垃圾短信,但是同樣可以将其應用到不同語料庫,例如,預測電影評論是否對應于1、2、3、4或者5星評級。是以,我決定使這些代碼通用;隻要有一組具備一緻标簽類型的示例文檔,這些代碼就有效。符号表示“類型通配符”(任何類型都得到支援),如果檢查classify函數簽名,就會看到:groups:('a * docsgroup) [] -&gt; tokenizer:tokenizer -&gt; txt:string -&gt; 'a。通配符已經被'a(表示泛型類型)所代替,函數也傳回一個'a,這是(泛型)标簽類型。

這裡已經引入了兩個新類型——序列和集合。我們從集合開始簡短地讨論這兩種類型,集合代表一組唯一項目,主要目的是回答“項目x 是否屬于集合s”的問題。考慮到我們想要搜尋特定單詞(更通用的說法是标記)是否出現在sms中,以确定sms可能是垃圾短信還是非垃圾短信,使用可以有效比較項目集合的資料結構似乎很合理。通過将文檔歸納為幾組标記,就可以快速識别是否包含某個标記(如“txt”),而無須付出使用contains()字元串方法的代價。

set子產品提供了許多圍繞集合運算的友善函數,如下面的片段所示。在f# interactive視窗中輸入以下内容:

let intersection = set.intersect set1 set2 let union = set.union set1 set2;;

val intersection : set = set [1; 3]

val union : set = set [1; 2; 3; 5]<code>`</code>

集合還輸出一個有用的函數difference,從一個集合中減去另一個集合,也就是說,從第一個集合中删除第二個函數中的每個元素:

let set3 = set.add 4 set1

set1.contains 4;;

val set3 : set = set [1; 2; 3; 4]

val it : bool = false<code>`</code>

在set1中添加4建立一個新集合set3,它包含4,但是原始集合set1沒有受到這一運算的影響。

我們引入的另一個類型——序列,是惰性求值的元素序列——也就是說,它是僅在必要時計算的集合,這樣可以潛在地減少記憶體或者計算使用量。在f# interactive中嘗試如下代碼:

let seq2 =

val seq2 : seq<code>`</code>

同樣,結果是一個尚無實際内容的序列,因為目前沒有任何必要的内容。我們改編代碼,要求得到前3個元素的總和:

let infinite = seq.initinfinite (fun i -&gt; if i % 2 = 0 then 1 else -1)

let test = infinite |&gt; seq.take 1000000 |&gt; seq.sum;;

val infinite : seq

val test : int = 0<code>`</code>

f#序列常常得到使用的另一個原因是,它們等價于c#的ienumerable,所有.net集合類型本身都可以當成序列處理,是以,seq子產品提供的函數可以利用實際集合類型中沒有出現的功能性,對其進行操縱。考慮下面的例子:

let proportion count total = float count / float total

let laplace count total = float (count+1) / float (total+1)

let countin (group:tokenizeddoc seq) (token:token) =

現在,我們已經有了分析有相同标簽的一組檔案、然後将其歸結為一個docsgroup所需的所有元件,下一步需要做的就是:

計算該組與文檔總數的比例。

對我們确定用于分類文檔的每個标記,找出在組中的拉普拉斯修正比例。

程式清單2-4 分析一組文檔

let learn (docs:(_ * string)[])

任務就要圓滿完成了。從整個過程中,我們真正想得到的是一個函數,以字元串(原始sms消息)為參數,根據從文檔訓練集中學到的,傳回具有最高得分的标簽。

程式清單2-6 訓練簡單的貝葉斯分類器

繼續閱讀