面向開發(fā)人員的機器學習指南

首頁

資訊

文章

資源

小組

相親

登錄

注冊

首頁

最新文章

IT 職場

前端

后端

移動端

數(shù)據(jù)庫

運維

其他技術(shù)

- 導航條 -首頁最新文章IT 職場前端- JavaScript- HTML5- CSS后端- Python- Java- C/C++- PHP- .NET- Ruby- Go移動端- Android- iOS數(shù)據(jù)庫運維- Linux- UNIX其他技術(shù)- Git- 機器學習- 算法- 測試- 信息安全- Vim

伯樂在線>首頁>所有文章>IT技術(shù)> 面向開發(fā)人員的機器學習指南

面向開發(fā)人員的機器學習指南

2017/01/11 ·IT技術(shù)·機器學習

分享到:7

前端后臺ThinkPHP開發(fā)整站

帶你入門Vue2.0及學習實戰(zhàn)項目

Android網(wǎng)絡層架構(gòu)設計實戰(zhàn)基于okhttp3

React Native開發(fā)跨平臺GitHub App

本文由伯樂在線-pine_pie翻譯膊爪,劉唱校稿柴淘。未經(jīng)許可,禁止轉(zhuǎn)載突梦!

英文出處:Mike de Waard韭山。歡迎加入翻譯組

現(xiàn)如今彩届,大多數(shù)的開發(fā)人員都聽說過機器學習拾并,但是當他們試圖尋找捷徑來學習這些技術(shù)時混埠,卻有很多人都對機器學習中的一些抽象概念望而卻步怠缸,諸如回歸無監(jiān)督學習钳宪、概率密度函數(shù)和其他許多定義揭北。如果訴諸于書本扳炬,代表著作有《An Introduction to Statistical Learning with Applications in R》《Machine Learning for Hackers》,其中的實例是用 R 語言實現(xiàn)的搔体。

然而恨樟,R 實際上跟 Java、C#疚俱、Scala 等用于開發(fā)日常應用的編程語言不同劝术。這也是本文采用Smile來介紹機器學習的原因,Smile 是關(guān)于機器學習的一個庫呆奕,它可以在 Java 與 Scala 中使用养晋,而這兩種語言對大多數(shù)開發(fā)者來說至少是不陌生的。

第一部分“機器學習的整體框架”包含了學習下文應用實例要用到的所有重要概念梁钾∩“應用實例”這一部分參考了《Machine Learning for Hackers》這本書的例子。另外姆泻,《Machine Learning in Action》這本書將用于驗證零酪。

第二部分“應用實例”包含了各種機器學習 (ML) 應用實例,以 Smile 作為 ML 庫拇勃。

注意在本文中四苇,“新”定義會添加超鏈接,以備讀者了解該主題的更多相關(guān)內(nèi)容潜秋,但是完成這些實例蛔琅,并不需要閱讀全部的內(nèi)容。

最后峻呛,我想感謝以下這些人:

Haifeng Li: 感謝他給予的支持,還寫了那么出色辜窑、且能免費使用的 Smile 庫钩述。

Erik Meijer: 感謝在我寫這篇博文的時候他給予的建議與指導。

Richard van Heest穆碎、Lars Willems: 感謝他們審閱了這篇博文并做了反饋牙勘。

機器學習的整體框架

也許你曾聽說過機器學習這個概念。然而所禀,如果要你向另一個人解釋什么是機器學習方面,你要怎么做呢?在繼續(xù)閱讀之前色徘,請先考慮一下這個問題恭金。

機器學習有很多不同的定義方式,其中一些更加精確褂策,然而横腿,在這些定義中也有許多不一致之處颓屑。有些定義認為機器學習就是根據(jù)歷史數(shù)據(jù)建立一個靜態(tài)的模型,然后可以用于預測未來的數(shù)據(jù)耿焊。另一些則認為隨著數(shù)據(jù)的增加揪惦,它是一個隨時間不斷變化的動態(tài)模型。

我是比較支持動態(tài)說的罗侯,但是由于某些限制器腋,我們的實例只用來闡釋靜態(tài)的模型方法。不過钩杰,在動態(tài)機器學習這一節(jié)中纫塌,我們也對動態(tài)原則怎樣運作做了解釋。

接下來的這一部分給出了機器學習中常用的定義和概念榜苫。我們建議讀者在進入應用實例的學習之前先通讀這部分护戳。

特征

一個特征就是用來訓練模型的一種性質(zhì)。例如垂睬,基于文字“買”和“錢”出現(xiàn)的頻率可以把郵件分類為垃圾郵件和正常郵件媳荒。這些用來分類的單詞就是特征,如果把它們與其他單詞組合在一起驹饺,那它們就是特征的一部分钳枕。如果你想用機器學習來預測一個人是否是你的朋友,那么“共同的朋友”可以作為一個特征赏壹。注意鱼炒,在這個領域中,特征有時也指屬性蝌借。

模型

一提到機器學習昔瞧,模型是經(jīng)常會碰到的一個術(shù)語。模型就是一種機器學習方法的結(jié)果以及該方法采用的算法菩佑。這個模型可以在監(jiān)督學習中用來做預測自晰,或者在無監(jiān)督學習中用來檢索聚類。在這個領域中稍坯,在線訓練離線訓練這兩個術(shù)語也有很大機會見到酬荞。在線訓練指的是往一個已經(jīng)存在的模型中添加訓練數(shù)據(jù),而離線訓練指的是從頭開始建立一個新模型瞧哟。由于性能原因混巧,在線訓練方法是最可取的。然而勤揩,對某些算法來說咧党,也有例外。

學習方法

在機器學習領域中雄可,有兩種前沿的的學習方式凿傅,也就是監(jiān)督學習和無監(jiān)督學習缠犀,簡要地介紹一下還是很有必要的,因為在機器學習的應用中聪舒,選擇合適的機器學習方法和算法是一個重要而有時又有點乏味的過程糙麦。

監(jiān)督學習

在監(jiān)督學習中酿箭,你可以明確地定義要使用的特征,以及你預期的輸出結(jié)果。例如袋马,通過身高和體重預測性別把篓,這是一個分類的問題制恍。此外已球,你還可以通過回歸分析預測絕對值。用同樣的數(shù)據(jù)做回歸分析的一個例子是通過性別和體重預測一個人的身高盼理。某些監(jiān)督算法只能用來解決分類問題和回歸分析中的一種谈山,例如 K-NN。不過也有一些算法如 Support Vector Machines 在兩種情況下都適用宏怔。

分類

在監(jiān)督學習的范圍內(nèi)奏路,分類問題是相對簡單的‰铮考慮一組標簽以及一些已經(jīng)打上正確標簽的數(shù)據(jù)鸽粉,我們想要做的就是為新數(shù)據(jù)預測標簽。然而抓艳,在把數(shù)據(jù)考慮為分類問題之前触机,你應該分析一下數(shù)據(jù)的特點。如果數(shù)據(jù)的結(jié)構(gòu)明顯可以讓你輕松地畫出一條回歸線玷或,那么應用回歸算法反而會更好儡首。如果數(shù)據(jù)無法擬合出一條回歸線,或者當算法的性能不理想時偏友,那么分類就是一個很好的選擇椒舵。

分類問題的一個例子是,根據(jù)郵件的內(nèi)容把郵件分為正吃继福或垃圾郵件±缰樱考慮一個訓練組棱诱,其中的郵件被標為正常或垃圾涝动,可以應用一個分類算法來訓練模型迈勋。這個模型就可以用來預測未來的郵件是正常或是垃圾醋粟。分類算法的一個典型的例子是 K-NN 算法靡菇,分類問題的常用實例是將郵件分為垃圾郵件或正常郵件重归,這也是本文當中使用到的例子之一。

回歸

回歸要比分類要強大很多厦凤。這是因為鼻吮,回歸分析預測的是實際值而不是標簽。一個簡單的例子可以說明這一點较鼓∽的荆考慮一個表格,其中包含體重博烂、身高和性別等數(shù)據(jù)香椎,當給定一個體重和身高數(shù)據(jù)時,你可以應用 K-NN 算法來預測某人的性別禽篱。對這個同樣的數(shù)據(jù)集使用回歸分析畜伐,如果是給定性別以及其他各個缺失的參數(shù),反過來躺率,你可以預測某人的體重或身高玛界。

能力越大,責任就越大肥照,所以在用回歸分析建立模型時必須格外小心脚仔。常見的陷阱是過擬合、欠擬合以及對模型如何控制外推法內(nèi)插法欠缺考慮舆绎。

無監(jiān)督學習

相比監(jiān)督學習鲤脏,無監(jiān)督學習不需要你事先確切地了解輸出結(jié)果。應用無監(jiān)督學習的中心思想是發(fā)掘出一個數(shù)據(jù)集內(nèi)在的結(jié)構(gòu)吕朵。PCA 就是一個例子猎醇,它通過組合特征從而減小特征數(shù)。 組合過程基于這些特征之間可能隱含的關(guān)聯(lián)努溃。另一個無監(jiān)督學習的例子是 K-均值聚類硫嘶。K-均值聚類就是要找出一個數(shù)據(jù)集中的分組,之后這些分組可以用于其他目的梧税,例如用于監(jiān)督學習中沦疾。

主成分分析(PCA)

主成分分析是統(tǒng)計學中的一種技術(shù),它用于將一組相關(guān)列轉(zhuǎn)化為較小的一組無關(guān)列第队,以化簡一個問題的特征數(shù)哮塞。這組較小的列就叫做主成分。這種技術(shù)主要用于探索性數(shù)據(jù)分析中凳谦,因為它揭示了數(shù)據(jù)中的內(nèi)部結(jié)構(gòu)忆畅,這樣的結(jié)構(gòu)無法直觀地看出來。

PCA 一個最大弱點就是數(shù)據(jù)中的異常值尸执。這些異常值嚴重地影響了結(jié)果家凯,所以缓醋,事先觀察數(shù)據(jù),排除較大的異常值能夠極大地提高這種方法的性能绊诲。

為了清楚 PCA 到底是做什么用的送粱,我們將一組二維點數(shù)據(jù)的圖與同樣數(shù)據(jù)經(jīng) PCA 處理后的圖作了對比。

原始數(shù)據(jù)表示在左圖中驯镊,其中每個顏色表示不同的類別葫督。顯然,這些數(shù)據(jù)可以從二維約化到一維之后板惑,仍然能夠恰當?shù)姆诸愰暇怠_@就是 PCA 被提出的緣故了。根據(jù)每個數(shù)據(jù)點的原始維度冯乘,通過 PCA 可以計算出一個新的值來洽胶。

右圖是對這些數(shù)據(jù)點應用了 PCA 后的結(jié)果。注意裆馒,這些數(shù)據(jù)有一個y值姊氓,但這純粹是為了能夠?qū)?shù)作圖展示出來。所有這些數(shù)據(jù)點的Y值都是0喷好,因為 PCA 算法只返回 X 值翔横。同時注意,右圖數(shù)據(jù)點中的 X 值并不對應左圖中各點的X值梗搅,這表明 PCA 并不僅僅是“丟掉”一個維度禾唁。

驗證技術(shù)

在這部分中我們會介紹一些用于模型驗證的技術(shù)方法,以及一些與機器學習驗證方法相關(guān)的專業(yè)術(shù)語无切。

交叉驗證

交叉驗證法是機器學習領域中最常用的驗證方法之一荡短。它的基本思想是,將原始數(shù)據(jù)分為訓練集和驗證集哆键,先用訓練集對模型進行訓練掘托,然后再用模型來預測驗證集的數(shù)據(jù)。將預測值與實際值進行對比籍嘹,以此來評價模型的性能和訓練數(shù)據(jù)的質(zhì)量闪盔。

這種交叉驗證法最重要的環(huán)節(jié)是分割數(shù)據(jù)。應用這種方法時辱士,應該始終使用整個數(shù)據(jù)集锭沟。換言之,你不可以隨機選取 X 個數(shù)據(jù)點作為訓練集然后隨機選取X個數(shù)據(jù)點作為驗證集识补,這樣的話,在兩個數(shù)據(jù)集中可能就有些點是重復的辫红,而另一些點又沒有被利用到凭涂。

2-折交叉驗證

在2-折交叉驗證中祝辣,每一“折”(所以要執(zhí)行兩次)都要將數(shù)據(jù)分割為驗證集和訓練集,用訓練集訓練模型切油,再用驗證集做驗證蝙斜。這樣做就可以在驗證模型時計算兩次誤差。這些誤差值不應該相差太大澎胡。萬一差太大孕荠,那么不是你的數(shù)據(jù)有問題,就是你選來建立模型的特征有問題攻谁。無論是哪種情況稚伍,你都應該再看看數(shù)據(jù),找出具體的問題戚宦,因為以數(shù)據(jù)為基礎來訓練一個模型有可能因為錯誤數(shù)據(jù)而出現(xiàn)模型過度擬合的情況个曙。

正則化

正則化的基本思想是,通過簡化一個模型而防止它過度擬合受楼。假設你的數(shù)據(jù)滿足一個三次多項式函數(shù)垦搬,但是數(shù)據(jù)中有噪聲,這會使模型函數(shù)的次數(shù)加高一級艳汽。于是猴贰,盡管模型剛開始好像不錯,但碰到新的數(shù)據(jù)就表現(xiàn)不理想了河狐。正則化通過一個特定的 λ 值來簡化模型米绕,有利于防止這種情況的發(fā)生。然而,要找到一個合適的 λ 值卻不易哥捕,因為你不知道模型什么時候才會過度擬合迂尝。這也是交叉驗證法經(jīng)常被用來尋找適合模型的最佳 λ 值的原因。

精確率

在計算機科學中非驮,我們用精確率這個術(shù)語來描述相關(guān)的選中條目的數(shù)量。因此雏赦,當你計算一個文檔搜索算法的精確率時劫笙,那個算法的精確率就定義為在設定的結(jié)果中有多少個文檔是確實相關(guān)的。

這個值由下式算出:

掌握這個內(nèi)容可能會有點難星岗,所以我舉個例子:

假設有一個完備的文檔集 {aa填大,ab,bc俏橘,bd允华,ee},我們要查找名字帶有 a 的文檔。如果算法返回的文檔集是 {aa靴寂,ab}磷蜀,那么直覺告訴我們精確率是100%。我們可以代入公式驗證一下:

事實上就是100%百炬。如果我們再查找一次褐隆,除了{aa,ab}這個結(jié)果剖踊,我們還得到{bc庶弃,de}這個結(jié)果,精確率就會受到影響如下:

這里德澈,結(jié)果中包含了相關(guān)的結(jié)果歇攻,也包含了兩個不相關(guān)的結(jié)果,導致精確率降低了圃验。然而掉伏,如果給這個例子計算召回率,那它將是100%澳窑,這就是精確率與召回率之間的不同之處斧散。

召回率

召回率是指,給定查找條件和一個數(shù)據(jù)集摊聋,算法檢索到的相關(guān)條目的數(shù)量鸡捐。因此,給定一組文檔以及能夠返回一個文檔子集的查找條件麻裁,召回率就表示相關(guān)的文檔中有多少被實際返回箍镜。召回率由下式計算:

我們舉個例子看看如何應用該公式:

假設有一個完備的文檔集 { aa, ab, bc, bd, ee },我們要查找名字帶有a的文檔煎源。如果算法返回{aa色迂,ab},那么召回率顯然就是100%手销。我們可以代入公式驗證一下:

事實上就是100%歇僧。下面我們看看如果算法只返回部分相關(guān)結(jié)果會怎么樣:

這里,結(jié)果只包含一半的相關(guān)結(jié)果锋拖,導致召回率降低了诈悍。然而,如果計算這種情況下的精確率兽埃,結(jié)果是會100%侥钳,因為所有返回結(jié)果都是相關(guān)的。

先驗

給定一個數(shù)據(jù)點柄错,一個分類器的先驗值代表了這個數(shù)據(jù)點屬于該分類器的可能性大小舷夺。在實踐中苦酱,這意味著當你在一個數(shù)據(jù)點處得到一個預測值時,先驗值就表示模型對那個數(shù)據(jù)點的分類的確信度有多高冕房。

均方根誤差(RMSE)

均方根誤差(RMSE 或 RMSD躏啰,D 代表 deviation,即偏差)是指對實際值與預測值之差先平方耙册,再求均值,然后開方毫捣。我舉個例子來解釋一下好幫助理解详拙。假設我們有以下數(shù)據(jù):

模型的平方差的均值是 4.33333,該值的平方根是 2.081666. 因此蔓同,該模型的預測值的平均誤差為 2.08饶辙。RMSE 值越低,模型的預測效果越好斑粱。這就是為什么在選擇特征時弃揽,人們會分別計算包含和不包含某個特征的 RMSE 值,以判斷那個特征是如何影響模型的性能的则北。通過這些信息矿微,人們就可以確定,和模型的效率提升比起來尚揣,由于該特征值增加的額外的計算時間是否值得涌矢。

此外,因為 RMSE 值是一個絕對值快骗,所以它可以歸一化以進行模型之間的比較娜庇。這就是歸一化均方根誤差 (NRMSE)。然而方篮,要計算這個值名秀,首先要知道系統(tǒng)所包含的最小值與最大值。假設我們有一個最小值為 5 度藕溅、最大值為 25 度的溫度范圍匕得,那么可用下式來計算 NRMSE 值:

若代入實際值,可得到如下結(jié)果:

那這個 10.4 表示什么呢蜈垮?這是模型對數(shù)據(jù)點進行預測的平均誤差百分數(shù)耗跛。

最后,我們可以利用 RMSE 值來計算擬合度 (R Squared)攒发。擬合度反映的是调塌,與各個值的平均值作比較(不考慮模型的情況),模型的預測效果有多好惠猿。我們首先要計算出平均法的 RMSE 值羔砾。對上文的表格最后一列值的取平均,結(jié)果是 4.22222,其平方根是 2.054805姜凄。首先注意到這個值比模型的值要小政溃。這不是個好現(xiàn)象,因為這表明模型的預測效果比單單取平均值要差态秧。然而董虱,我們主要是演示怎么計算擬合度,所以我們繼續(xù)計算過程申鱼。

現(xiàn)在愤诱,模型與平均法的 RSME 值都求出來了,接著用下式計算擬合度:

代入實際值得到以下結(jié)果:

那么捐友,-1.307229 代表什么呢淫半?它就是表示模型每次對一個值的預測效果比平均法差約 1.31%。換言之匣砖,在這個具體情況中科吭,用平均法來做預測比用模型的效果要好。

常見陷阱

這部分要介紹的是在應用機器學習技術(shù)的過程中經(jīng)常會碰到的問題猴鲫,主要內(nèi)容是向讀者解析這些陷阱以幫助讀者避開它們对人。

過度擬合

對數(shù)據(jù)進行擬合時,數(shù)據(jù)本身可能會包含噪聲(例如有測量誤差)变隔。如果你精確地把每一個數(shù)據(jù)點都擬合進一個函數(shù)中规伐,那你會把噪聲也耦合到模型中去。這雖然能使模型在預測測試數(shù)據(jù)時表現(xiàn)良好匣缘,但在預測新數(shù)據(jù)時會相對較差猖闪。

把數(shù)據(jù)點和擬合函數(shù)畫在圖表中,下列左圖反映過度擬合的情況肌厨,右圖表示一條穿過數(shù)據(jù)點的回歸曲線培慌,它對數(shù)據(jù)點能夠適當擬合

應用回歸分析時容易產(chǎn)生過度擬合柑爸,也很容易出現(xiàn)在樸素貝葉斯分類算法中吵护。在回歸分析中,產(chǎn)生過度擬合的途徑有舍入操作表鳍、測量不良和噪聲數(shù)據(jù)馅而。在樸素貝葉斯分類算法中,選定的特征可能導致過度擬合譬圣。例如瓮恭,對垃圾和正常郵件的分類問題保留所有停用詞。

通過運用驗證技術(shù)厘熟、觀察數(shù)據(jù)的統(tǒng)計特征以及檢測和剔除異常值屯蹦,可以檢測出過度擬合维哈。

欠擬合

當你對數(shù)據(jù)進行建模時,把很多統(tǒng)計數(shù)據(jù)都遺漏掉了登澜,這叫做欠擬合阔挠。有很多原因可以導致欠擬合,例如脑蠕,對數(shù)據(jù)應用不合適的回歸類型购撼。如果數(shù)據(jù)中包含了非線性結(jié)構(gòu),而你卻運用線性回歸谴仙,這就產(chǎn)生了一個欠擬合模型份招。下列左圖代表了一條欠擬合的回歸線怖侦,右圖右圖表示一條合適的回歸線接箫。

為了防止欠擬合裂允,你可以畫出數(shù)據(jù)點從而了解數(shù)據(jù)的內(nèi)在結(jié)構(gòu),以及應用驗證技術(shù)哼审,例如交叉驗證。

維度災難

對于已知的數(shù)據(jù)量存在一個最大的特征數(shù)(維數(shù))孕豹,當實際用于建立機器模型的特征數(shù)超過這個最大值時涩盾,就產(chǎn)生維度災難問題。矩陣秩虧就是這樣的一種問題励背。普通最小二乘 (OLS) 算法通過解一個線性系統(tǒng)來建立模型春霍。然而,如果矩陣的列數(shù)多于行數(shù)叶眉,那這個系統(tǒng)不可能有唯一解址儒。最好的解決辦法是獲取更多數(shù)據(jù)點或者減小特征數(shù)。

如果讀者想了解更多關(guān)于維度災難的內(nèi)容衅疙,可以參考一個關(guān)于這方面的研究莲趣。在這個研究中,研究人員 Haifeng Li饱溢,Keshu Zhang 和 Tao Jiang 發(fā)展一種用少量數(shù)據(jù)點改善腫瘤分類的算法喧伞。他們還將其與支持向量機隨機森林算法做了比較。

動態(tài)機器學習

在幾乎所有你能找到的機器學習文獻中绩郎,靜態(tài)模型就是首先通過建立潘鲫、驗證流程,然后作預測或建議用途肋杖。然而在實踐中溉仑,僅僅這樣做還不足以應用好機器學習。所以兽愤,我們在這部分中將要介紹怎么樣把一個靜態(tài)模型改造成一個動態(tài)模型彼念。因為(最佳的)實現(xiàn)是依賴于你所使用的算法的挪圾,所以我們將只作概念介紹而不給出實例了。鑒于文本解釋不夠清晰逐沙,我們首先用一個圖表展示整個體系哲思,然后用這個圖表介紹機器學習以及如何做成一個動態(tài)系統(tǒng)。

機器學習的基本流程如下:

1吩案、搜集數(shù)據(jù)

2棚赔、把數(shù)據(jù)分割為測試集和訓練集

3、訓練一個模型(應用某種機器學習算法)

4徘郭、驗證模型靠益,驗證方法需要使用模型和測試數(shù)據(jù)

5、基于模型作出預測残揉。

在該領域的實際應用中胧后,以上的流程是不完整的,有些步驟并未包含進去抱环。在我看來壳快,這些步驟對于一個智能學習系統(tǒng)來說至關(guān)重要。

所謂的動態(tài)機器學習镇草,其基本思路如下:模型作出預測后眶痰,將預測信息連同用戶反饋一起返回給系統(tǒng),以改善數(shù)據(jù)集和模型梯啤。那么竖伯,這些用戶反饋是怎么獲得的呢?我們以為?Facebook?的朋友推薦為例因宇。用戶面臨兩種選擇:“添加朋友”或“移除”七婴。基于用戶的決定羽嫡,對于那個預測你就得到了用戶的直接反饋本姥。

因此,假設你獲得了這些用戶反饋杭棵,那么你可以對模型應用機器學習來學習這些用戶反饋婚惫。聽起來可能有點奇怪,我們會更詳細地解釋這一過程魂爪。然而在那之前先舷,我們要做一個免責聲明:我們關(guān)于臉書朋友推薦系統(tǒng)的解釋是一個100%的假說,并且絕對沒有經(jīng)過臉書本身的證實滓侍。就我們所知蒋川,他們的系統(tǒng)對外是不公開的。

假設該系統(tǒng)基于以下特征進行預測:

1撩笆、共同朋友的數(shù)量

2捺球、相同的戶籍

3缸浦、相同的年齡

然后你可以為臉書上的每一個人計算出一個先驗值,這個先驗值描述了他/她是你的朋友的概率有多大氮兵。假設你把一段時間內(nèi)所有的預測信息都存儲下來裂逐,就可以用機器學習分析這些數(shù)據(jù)來改善你的系統(tǒng)。更詳細地說泣栈,假設大多數(shù)的“移除好友”推薦在特征 2 上具有較高評級卜高,但在特征 1 上評級相對較低,那么我們可以給預測系統(tǒng)加入權(quán)重系數(shù)南片,讓特征 1 比特征 2 更重要掺涛。這樣就可以為我們改善推薦系統(tǒng)。

此外疼进,數(shù)據(jù)集隨時間而增大薪缆,所以我們要不斷更新模型,加入新數(shù)據(jù)伞广,使預測更準確矮燎。不過,在這個過程中赔癌,數(shù)據(jù)的量級及其突變率起著決定性作用。

實例

在這部分中澜沟,我們結(jié)合實際環(huán)境介紹了一些機器學習算法灾票。這些實例主要為了方便讀者入門之用,因此我們不對其內(nèi)在的算法作深入講解茫虽。討論的重點完全集中在這些算法的功能方面刊苍、如何驗證算法實現(xiàn)以及讓讀者了解常見的陷阱。

我們討論了如下例子:

基于下載/上傳速度的互聯(lián)網(wǎng)服務提供商標記法 (K-NN)

正常/垃圾郵件分類(樸素貝葉斯法)

基于內(nèi)容的郵件排序(推薦系統(tǒng))

基于身高預測體重(線性回歸:普通最小二乘法)

嘗試預測最暢銷書排行(文本回歸)

應用無監(jiān)督學習合并特征(主成分分析)

應用支持向量機(支持向量機)

我們在這些實例中都使用了Smile 機器學習庫濒析,包括 smile-core 和 smile-plot 這兩個庫正什。這些庫在Maven, Gradle, Ivy, SBT 和 Leiningen 等工具上都是可用的。如何把這些庫添加到這些工具中去号杏,對于 core 庫婴氮,參考這里,對于 plot 庫盾致,參考這里主经。

所以,在開始做這些實例之前庭惜,我假定你在自己最喜歡的 IDE 上建立了一個新項目罩驻,并把 smile-core 庫和 smile-plot 庫添加到了你的項目中。其他需要用到的庫以及如何獲取實例的數(shù)據(jù)會在每個例子中分別予以說明护赊。

基于下載/上傳速度的互聯(lián)網(wǎng)服務供應商標記法(使用 K-NN 算法惠遏、Smile 庫砾跃、Scala 語言)

這一部分的主要目標是運用 K 最近鄰算法,根據(jù)下載/上傳速度對將互聯(lián)網(wǎng)服務供應商 (ISP) 分為 Alpha 類(由 0 代表)或 Beta 類(由 1 代表)节吮。K-NN 算法的思路如下:給定一組已經(jīng)分好類的點抽高,那么,對新點的分類可以通過判別它的 K 個最近鄰點的類別(K 是一個正整數(shù))课锌。K 個最近鄰點可以通過計算新點與其周圍點之間的歐氏距離來查找厨内。找出這些鄰近點,你就得到了最具代表性的類別渺贤,并將新點分到這一類別中雏胃。

做這一案例需要先下載示例數(shù)據(jù)。此外志鞍,還要把代碼段中的路徑改為你存儲示例數(shù)據(jù)的地方瞭亮。

首先要加載 CSV 數(shù)據(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

34

35

36

37

38objectKNNExample{

defmain(args:Array[String]):Unit={

valbasePath="/.../KNN_Example_1.csv"

valtestData=getDataFromCSV(newFile(basePath))

}

defgetDataFromCSV(file:File):(Array[Array[Double]],Array[Int])={

valsource=scala.io.Source.fromFile(file)

valdata=source

.getLines()

.drop(1)

.map(x=>getDataFromString(x))

.toArray

source.close()

valdataPoints=data.map(x=>x._1)

valclassifierArray=data.map(x=>x._2)

return(dataPoints,classifierArray)

}

defgetDataFromString(dataString:String):(Array[Double],Int)={

//Split the comma separated value string into an array of strings

//把用逗號分隔的數(shù)值字符串分解為一個字符串數(shù)組

valdataArray:Array[String]=dataString.split(',')

//Extract the values from the strings

//從字符串中抽取數(shù)值

valxCoordinate:Double=dataArray(0).toDouble

valyCoordinate:Double=dataArray(1).toDouble

valclassifier:Int=dataArray(2).toInt

//And return the result in a format that can later

//easily be used to feed to Smile

//并以一定格式返回結(jié)果,使得該結(jié)果之后容易輸入到Smile中處理

return(Array(xCoordinate,yCoordinate),classifier)

}

}

你首先可能會奇怪為什么數(shù)據(jù)要使用這種格式此洲。數(shù)據(jù)點與它們的標記值之間的間隔是為了更容易地分割測試數(shù)據(jù)和訓練數(shù)據(jù)厂汗,并且在執(zhí)行 K-NN 算法以及給數(shù)據(jù)繪圖時,API 需要這種數(shù)據(jù)格式呜师。其次娶桦,把數(shù)據(jù)點存儲為一個數(shù)組 (Array[Array[Double]]),能夠支持 2 維以上的數(shù)據(jù)點汁汗。

給出這些數(shù)據(jù)之后衷畦,接下來要做的就是將數(shù)據(jù)可視化。Smile 為這一目的提供了一個很好的繪圖庫知牌。不過祈争,要使用這一功能,應該把代碼轉(zhuǎn)換到 Swing 中去角寸。此外還要把數(shù)據(jù)導入到繪圖庫中以得到帶有實際繪圖結(jié)果的 JPane 面板菩混。代碼轉(zhuǎn)換之后如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17objectKNNExampleextendsSimpleSwingApplication{

deftop=newMainFrame{

title="KNN Example"

valbasePath="/.../KNN_Example_1.csv"

valtestData=getDataFromCSV(newFile(basePath))

valplot=ScatterPlot.plot(testData._1,

testData._2,

'@',

Array(Color.red,Color.blue)

)

peer.setContentPane(plot)

size=newDimension(400,400)

}

...

將數(shù)據(jù)繪成圖是為了檢驗 K-NN 在這種具體情況中是否為合適的機器學習算法。繪圖結(jié)果如下:

在這個圖中可以看到扁藕,藍點和紅點在區(qū)域 3

考慮到 K-NN 算法是這個問題的一個不錯的選擇墨吓,我們可以繼續(xù)機器學習的實踐。GUI 在這里實際上沒有用武之地纹磺,我們摒棄不用帖烘∩兀回顧機器學習的全局體系這部分兔院,其中提到機器學習的兩個關(guān)鍵部分:預測和驗證。首先我們進行驗證,不作任何驗證就把模型拿來使用并不是一個好主意满哪。這里驗證模型的主要原因是為了防止過擬合父腕。不過在做驗證之前再层,我們要選擇一個合適的 K 值缩赛。

這個方法的缺點就是不存在尋找最佳 K 值的黃金法則。然而聪廉,可以通過觀察數(shù)據(jù)來找出一個合理的 K 值瞬痘,使大多數(shù)數(shù)據(jù)點可以被正確分類。此外板熊,K 值的選取要小心框全,以防止算法引起的不可判定性。例如干签,假設 K=2津辩,并且問題包含兩種標簽,那么容劳,當有一個點落在兩種標簽之間時喘沿,算法會選擇哪一種標簽呢?有一條經(jīng)驗法則是這樣的:K應該是特征數(shù)(維數(shù))的平方根竭贩。那在我們的例子中就會有K=1蚜印,但這真不是一個好主意,因為它會在決策邊界導致更高的誤分率留量∩购澹考慮到我們有兩種標簽,讓 K=2 會導致錯誤肪获,因此,目前來說柒傻,選擇 K=3 是比較合適孝赫。

在這個例子中,我們做 2 折交叉驗證红符。一般來講青柄,2 折交叉驗證是一種相當弱的模型驗證方法,因為它將數(shù)據(jù)集分割為兩半并且只驗證兩次预侯,仍有可能產(chǎn)生過擬合致开,不過由于這里的數(shù)據(jù)集只包含 100 個點,10 折驗證(一個較強的版本)發(fā)揮不了作用萎馅,因為這樣的話双戳,將只有 10 個點用于測試,會導致誤差率傾斜糜芳。

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

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68defmain(args:Array[String]):Unit={

valbasePath="/.../KNN_Example_1.csv"

valtestData=getDataFromCSV(newFile(basePath))

//Define the amount of rounds, in our case 2 and

//initialise the cross validation

//在這第二種情況中我們設置了交叉驗證的次數(shù)并初始化交叉驗證

valcv=newCrossValidation(testData._2.length,validationRounds)

valtestDataWithIndices=(testData

._1

.zipWithIndex,

testData

._2

.zipWithIndex)

valtrainingDPSets=cv.train

.map(indexList=>indexList

.map(index=>testDataWithIndices

._1.collectFirst{case(dp,`index`)=>dp}.get))

valtrainingClassifierSets=cv.train

.map(indexList=>indexList

.map(index=>testDataWithIndices

._2.collectFirst{case(dp,`index`)=>dp}.get))

valtestingDPSets=cv.test

.map(indexList=>indexList

.map(index=>testDataWithIndices

._1.collectFirst{case(dp,`index`)=>dp}.get))

valtestingClassifierSets=cv.test

.map(indexList=>indexList

.map(index=>testDataWithIndices

._2.collectFirst{case(dp,`index`)=>dp}.get))

valvalidationRoundRecords=trainingDPSets

.zipWithIndex.map(x=>(x._1,

trainingClassifierSets(x._2),

testingDPSets(x._2),

testingClassifierSets(x._2)

)

)

validationRoundRecords

.foreach{record=>

valknn=KNN.learn(record._1,record._2,3)

//And for each test data point make a prediction with the model

//對每個測試數(shù)據(jù)點飒货,用模型做一次預測

valpredictions=record

._3

.map(x=>knn.predict(x))

.zipWithIndex

//Finally evaluate the predictions as correct or incorrect

//and count the amount of wrongly classified data points.

//最后檢驗預測結(jié)果正確與否魄衅,并記下被錯誤分類的數(shù)據(jù)點的個數(shù)

valerror=predictions

.map(x=>if(x._1!=record._4(x._2))1else0)

.sum

println("False prediction rate: "+error/predictions.length *100+"%")

}

}

如果你多次執(zhí)行上面這段代碼,你可能發(fā)現(xiàn)錯誤預測率會有一些波動塘辅。這是由于用來做訓練和測試的隨機樣本晃虫。如果不幸地取到不好的隨機樣本,誤差率會比較高扣墩,若取到好的隨機樣本哲银,則誤差率會極低。

不幸的是呻惕,關(guān)于如何為模型選取最好的隨機樣本來訓練荆责,我沒有這樣的黃金法則。也許有人會說蟆融,產(chǎn)生最小誤差率的模型總是最好的草巡。不過,再回顧一下過擬合這個概念型酥,選用這樣的特殊模型也可能會對新數(shù)據(jù)束手無策山憨。這就是為什么獲得一個足夠大且具有代表性的數(shù)據(jù)集對一個成功的機器學習應用來說非常關(guān)鍵。然而弥喉,當遇到這種問題時郁竟,你可以用新的數(shù)據(jù)和已知的正確分類不斷更新模型。

我們概括一下目前為止的進展狀況由境。首先小心地選取訓練和測試數(shù)據(jù)棚亩;下一步,建立幾個模型并驗證虏杰,選出給出最好結(jié)果的模型讥蟆。接下來我們到達最后一步,就是用這個模型做預測:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15valknn=KNN.learn(record._1,record._2,3)

valunknownDataPoint=Array(5.3,4.3)

valresult=knn.predict(unknownDatapoint)

if(result==0)

{

println("Internet Service Provider Alpha")

}

elseif(result==1)

{

println("Internet Service Provider Beta")

}

else

{

println("Unexpected prediction")

}

執(zhí)行這段代碼之后纺阔,未標記點 (5.3, 4.3) 就被標記為 ISP Alpha瘸彤。這個點是最容易分類的點之一,它明顯落在數(shù)據(jù)圖的 Alpha 區(qū)域中笛钝。如何做預測已經(jīng)顯而易見质况,我不再列舉其他點,你可以隨意嘗試其它不同點玻靡,看看預測結(jié)果结榄。

正常/垃圾郵件分類(樸素貝葉斯法)

在這個實例中,基于郵件內(nèi)容囤捻,我們將用樸素貝葉斯算法把郵件分為正常郵件與垃圾郵件臼朗。樸素貝葉斯算法是計算一個目標在每個可能類別中的幾率,然后返回具有最高幾率的那個類別。要計算這種幾率依溯,算法會使用特征老厌。該算法被稱為樸素貝葉斯是由于它不考慮特征之間的任何相關(guān)性。換言之黎炉,每個特征都一樣重要枝秤。我舉個例子進一步解釋:

假設你正在征顏色、直徑和形狀這幾個特征將水果和蔬菜進行分類慷嗜,現(xiàn)在有以下類別:蘋果淀弹、番茄和蔓越莓。

假設你現(xiàn)在要把一個具有如下特征值的目標分類:(紅色庆械、4cm薇溃、圓形)。對我們來說缭乘,它顯然是一個番茄沐序,因為對比蘋果它比較小,對比蔓越莓它太大了堕绩。然而策幼,樸素貝葉斯算法會獨立地評估每個特征,它的分類過程如下:

蘋果 66.6% 可能性(基于顏色和形狀)

番茄 100.0% 可能性(基于顏色奴紧、形狀和大刑亟恪)

蔓越莓 66.6% 可能性(基于顏色和形狀)

因此,盡管實際上很明顯它不可能是蔓越莓或蘋果黍氮,樸素貝葉斯仍會給出 66.6% 的機會是這兩種情況之一唐含。所以即使它正確地把目標分類為番茄,在邊界情況(目標大小剛好超出訓練集的范圍)下沫浆,它也可能給出糟糕的結(jié)果捷枯。不過,在郵件分類中专执,樸素貝葉斯算法表現(xiàn)還是不錯的淮捆,這是由于郵件的好壞無法僅僅通過一個特征(單詞)來分類。

你現(xiàn)在應該大概了解樸素貝葉斯算法了他炊,我們可以繼續(xù)做之前的實例了。在這個例子中已艰,我們使用 Scala 語言痊末,利用 Smile 庫中的樸素貝葉斯實現(xiàn),將郵件按內(nèi)容分為垃圾郵件和正常郵件哩掺。

在開始之前還需要你從 SpamAssasins 公共文庫上下載這個例子的數(shù)據(jù)凿叠。你所要用到的數(shù)據(jù)在easy_hamspam文件里,但其余的文件在你需要做更多實驗時也會用到。把這些文件解壓之后盒件,修改代碼段里的文件路徑以適應文件夾的位置蹬碧。此外,在做篩選時炒刁,你還需要用到停用詞文件恩沽。

對于每一個機器學習實現(xiàn),第一步是要加載訓練數(shù)據(jù)翔始。不過在這個例子中罗心,我們需要進一步深入機器學習。在 K-NN 實例中城瞎,我們用下載速度和上傳速度作為特征渤闷。我們并不指明它們是特征,因為它們就是唯一可用的屬性脖镀。對郵件分類這個例子來說飒箭,拿什么作為特征并非毫無意義。要分類出垃圾或正常郵件蜒灰。你可以使用的特征有發(fā)送人弦蹂、主題、郵件內(nèi)容卷员,甚至發(fā)送時間盈匾。

在這個例子中,我們選用郵件內(nèi)容作為特征毕骡,也就是削饵,我們要在訓練集中,從郵件正文中選出特征(此例中是單詞)未巫。為了做到這一點窿撬,我們需要建立一個詞匯文檔矩陣 (TDM)。

我們從編寫一個加載案例數(shù)據(jù)的函數(shù)開始叙凡。這個函數(shù)就是? getMessage 方法劈伴,在給定一個文件作為參數(shù)后撼玄,它從一個郵件中獲取過濾文本陨闹。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20defgetMessage(file:File):String=

{

//Note that the encoding of the example files is latin1,

// thus this should be passed to the fromFile method.

//注意案例文件采用latin1編碼,所以應該把它們傳遞給fromFile方法

valsource=scala.io.Source.fromFile(file)("latin1")

vallines=source.getLinesmkString"n"

source.close()

//Find the first line break in the email,

//as this indicates the message body

//在郵件中找出第一個換行符歇拆,因為這暗示信息的主體

valfirstLineBreak=lines.indexOf("nn")

//Return the message body filtered by only text from a-z and to lower case

//返回過濾后的信息主體新啼,即只包含a-z并且為小寫的文本追城。

returnlines

.substring(firstLineBreak)

.replace("n"," ")

.replaceAll("[^a-zA-Z ]","")

.toLowerCase()

}

到此,在我們提供的案例數(shù)據(jù)文件夾中燥撞,我們需要一個方法來獲取所有郵件的文件名座柱。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19defgetFilesFromDir(path:String):List[File]={

vald=newFile(path)

if(d.exists&&d.isDirectory){

//Remove the mac os basic storage file,

//and alternatively for unix systems "cmds"

//移除mac os的基本存儲文件或者unix系統(tǒng)的“cmd”文件

d.listFiles

.filter(x=>x.isFile&&

!x.toString

.contains(".DS_Store")&&

!x.toString

.contains("cmds"))

.toList

}

else{

List[File]()

}

}

然后迷帜,我們要定義一組路徑,它們可以方便我們從案例數(shù)據(jù)中加載不同的數(shù)據(jù)集色洞。與此同時戏锹,我們也直接定義一組大小為 500 的樣本,這是垃圾郵件訓練集的總數(shù)火诸。為了讓訓練集在兩種分類上保持平衡锦针,我們把正常郵件的樣本總數(shù)也定為 500。

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

31defmain(args:Array[String]):Unit={

valbasePath="/Users/../Downloads/data"

valspamPath=basePath+"/spam"

valspam2Path=basePath+"/spam_2"

valeasyHamPath=basePath+"/easy_ham"

valeasyHam2Path=basePath+"/easy_ham_2"

valamountOfSamplesPerSet=500

valamountOfFeaturesToTake=100

//First get a subset of the filenames for the spam

// sample set (500 is the complete set in this case)

//首先取出一個包含文件名的子集作為垃圾郵件樣本集(在這個案例中惭蹂,全集是500個文件名)

vallistOfSpamFiles=getFilesFromDir(spamPath)

.take(amountOfSamplesPerSet)

//Then get the messages that are contained in these files

//然后取得包含在這些文件中的信息

valspamMails=listOfSpamFiles.map(x=>(x,getMessage(x)))

//Get a subset of the filenames from the ham sample set

//取出一個文件名子集作為正常郵件樣本集

// (note that in this case it is not necessary to randomly

// sample as the emails are already randomly ordered)

//(注意在本案例中沒有必要隨機取樣伞插,因為這些郵件已經(jīng)是隨機排序)

vallistOfHamFiles=getFilesFromDir(easyHamPath)

.take(amountOfSamplesPerSet)

//Get the messages that are contained in the ham files

//取得包含在正常郵件中的信息

valhamMails=listOfHamFiles

.map{x=>(x,getMessage(x))}

}

既然我們已經(jīng)獲取了正常郵件和垃圾郵件的訓練數(shù)據(jù),就可以開始建立兩個TDM了盾碗。不過在給出實現(xiàn)這個過程的代碼之前媚污,我們首先簡短解釋下這么做的原因。TDM 包含了所有出現(xiàn)在訓練集正文中的單詞廷雅,以及詞頻耗美。然而,詞頻可能不是最好的量度方法(比如航缀,一封含有 1000000 個“cake”的郵件就能把整個表搞砸)商架,因此我們也會計算出現(xiàn)率,也就是芥玉,包含那個特定詞匯的文檔數(shù)量∩呙現(xiàn)在我們開始生成兩個 TDM。

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

33valspamTDM=spamMails

.flatMap(email=>email

._2.split(" ")

.filter(word=>word.nonEmpty)

.map(word=>(email._1.getName,word)))

.groupBy(x=>x._2)

.map(x=>(x._1,x._2.groupBy(x=>x._1)))

.map(x=>(x._1,x._2.map(y=>(y._1,y._2.length))))

.toList

//Sort the words by occurrence rate descending

//以出現(xiàn)率降序排列這些單詞

//(amount of times the word occurs among all documents)

//(該單詞在所有文檔中出現(xiàn)的總次數(shù))

valsortedSpamTDM=spamTDM

.sortBy(x=>-(x._2.size.toDouble/spamMails.length))

valhamTDM=hamMails

.flatMap(email=>email

._2.split(" ")

.filter(word=>word.nonEmpty)

.map(word=>(email._1.getName,word)))

.groupBy(x=>x._2)

.map(x=>(x._1,x._2.groupBy(x=>x._1)))

.map(x=>(x._1,x._2.map(y=>(y._1,y._2.length))))

.toList

//Sort the words by occurrence rate??descending

//以出現(xiàn)率降序排列這些單詞

//(amount of times the word occurs among all documents)

//(該單詞在所有文檔中出現(xiàn)的總次數(shù))

valsortedHamTDM=hamTDM

.sortBy(x=>-(x._2.size.toDouble/spamMails.length))

給定了那些表格灿巧,為了更深入了解它們赶袄,我用wordcloud將它們生成圖片。這些圖片中反映了頻率最高的 50 個單詞 (top 50)的情況抠藕,我們觀察一下饿肺。注意紅色單詞來自垃圾郵件,綠色單詞來自正常郵件盾似。此外敬辣,單詞的大小代表出現(xiàn)率。因此零院,單詞越大溉跃,至少出現(xiàn)一次該單詞的文檔越多。

你可以看到告抄,停用詞大多出現(xiàn)在前面撰茎。這些停用詞是噪聲,在特征選擇過程中我們要盡可能避開它們玄妈。所以在選出特征之前乾吻,我們要從表格中剔除這些詞。案例數(shù)據(jù)集已經(jīng)包含了一列停用詞拟蜻,我們首先編寫代碼來獲取這些詞绎签。

1

2

3

4

5

6

7

8defgetStopWords():List[String]=

{

valsource=scala.io.Source

.fromFile(newFile("/Users/.../.../Example Data/stopwords.txt"))("latin1")

vallines=source.mkString.split("n")

source.close()

returnlines.toList

}

現(xiàn)在我們可以擴展前文中的 TDM 生成代碼,剔除停用詞:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22valstopWords=getStopWords

valspamTDM=spamMails

.flatMap(email=>email

._2.split(" ")

.filter(word=>word.nonEmpty&&!stopWords.contains(word))

.map(word=>(email._1.getName,word)))

.groupBy(x=>x._2)

.map(x=>(x._1,x._2.groupBy(x=>x._1)))

.map(x=>(x._1,x._2.map(y=>(y._1,y._2.length))))

.toList

valhamTDM=hamMails

.flatMap(email=>email

._2.split(" ")

.filter(word=>word.nonEmpty&&!stopWords.contains(word))

.map(word=>(email._1.getName,word)))

.groupBy(x=>x._2)

.map(x=>(x._1,x._2.groupBy(x=>x._1)))

.map(x=>(x._1,x._2.map(y=>(y._1,y._2.length))))

.toList

如果馬上觀察垃圾郵件和正常郵件的 top 50 單詞酝锅,就可以看到大多數(shù)停用詞已經(jīng)消失了诡必。我們可以再作調(diào)整,但現(xiàn)在就用這個結(jié)果吧搔扁。

在了解了什么是“垃圾單詞”和“正常單詞”之后爸舒,我們就可以決定建立一個特征集了,稍后我們會把它用在樸素貝葉斯算法中以創(chuàng)建一個分類器稿蹲。注意:包含更多的特征總是更好的扭勉,然而,若把所有單詞都作為特征苛聘,則可能出現(xiàn)性能問題涂炎。這就是為什么在機器學習領域,很多開發(fā)者傾向于棄用沒有明顯影響的特征设哗,純粹就是性能方面的原因唱捣。另外,機器學習過程可以通過在完整的Hadoop集群上運行來完成网梢,但是闡明這方面的內(nèi)容就超出了本文的范圍震缭。

現(xiàn)在我們要選出出現(xiàn)率(而不是頻率)最高的 100 個“垃圾單詞”和 100 個“正常單詞”,并把它們組合成一個單詞集战虏,它將輸入到貝葉斯算法拣宰。最后,我們要轉(zhuǎn)換這些訓練數(shù)據(jù)以適應貝葉斯算法的輸入格式活烙。注意最終的特征集大小為 200(其中徐裸,#公共單詞×2)。請隨意用更大或更小的特征數(shù)做實驗啸盏。

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//Add the code for getting the TDM data and combining it into a feature bag.

//添加生成TDM數(shù)據(jù)并將其合成一個特征包的代碼

valhamFeatures=hamTDM

.records

.take(amountOfFeaturesToTake)

.map(x=>x.term)

valspamFeatures=spamTDM

.records

.take(amountOfFeaturesToTake)

.map(x=>x.term)

//Now we have a set of ham and spam features,

//現(xiàn)在我們有了一套正常郵件和垃圾郵件特征重贺,

// we group them and then remove the intersecting features, as these are noise.

//我們將它們組合在一起并將公共特征去除,因為它們是噪聲

vardata=(hamFeatures++spamFeatures).toSet

hamFeatures

.intersect(spamFeatures)

.foreach(x=>data=(data-x))

//Initialise a bag of words that takes the top x features

//from both spam and ham and combines them

//初始化一個單詞包回懦,從垃圾特征集和正常特征集中取出最前的x個特征气笙,將它們合并

varbag=newBag[String](data.toArray)

//Initialise the classifier array with first a set of 0(spam)

//and then a set of 1(ham) values that represent the emails

//將該分類器數(shù)組初始化,首先用一組數(shù)值0代替垃圾郵件怯晕,然后用一組數(shù)值1代替正常郵件

varclassifiers=Array.fill[Int](amountOfSamplesPerSet)(0)++

Array.fill[Int](amountOfSamplesPerSet)(1)

//Get the trainingData in the right format for the spam mails

//取得正確格式的垃圾郵件訓練數(shù)據(jù)

varspamData=spamMails

.map(x=>bag.feature(x._2.split(" ")))

.toArray

//Get the trainingData in the right format for the ham mails

//取得正確格式的正常郵件訓練數(shù)據(jù)

varhamData=hamMails

.map(x=>bag.feature(x._2.split(" ")))

.toArray

//Combine the training data from both categories

//將兩種訓練數(shù)據(jù)合并

vartrainingData=spamData++hamData

給定了這個特征包以及一個訓練數(shù)據(jù)集潜圃,我們就可以開始訓練算法。為此舟茶,我們有幾個模型可以采用:General 模型谭期、Multinomial 模型和Bernoulli 模型堵第。General 模型需要一個定義好的分布,而這個分布我們事先并不知道隧出,因此這個模型并不是個好的選擇踏志。Multinomial 和Bernoulli 兩個模型之間的區(qū)別就是它們對單詞出現(xiàn)率的處理方式不同。Bernoulli模型僅僅是驗證一個特征是否存在(二元值 1 或 0)胀瞪,因此它忽略了出現(xiàn)率這個統(tǒng)計值针余。反之,Multinomial 模型結(jié)合了出現(xiàn)率(由數(shù)值表示)凄诞。所以圆雁,與 Multinomial 模型比較,Bernoulli 模型在長文檔中的表現(xiàn)較差帆谍。既然我們要對郵件排序伪朽,并且也要使用到出現(xiàn)率,因此我們主要討論 Multinomial 模型汛蝙,但盡管試試 Bernoulli 模型驱负。

1

2

3

4

5

6

7

8

9

10

11//Create the bayes model as a multinomial with 2 classification

// groups and the amount of features passed in the constructor.

//建立一個多項式形式的貝葉斯模型,將類別數(shù)2以及特征總數(shù)傳遞給該構(gòu)建函數(shù)

varbayes=newNaiveBayes(NaiveBayes.Model.MULTINOMIAL,2,data.size)

//Now train the bayes instance with the training data,

// which is represented in a specific format due to the

//bag.feature method, and the known classifiers.

//現(xiàn)在可以用訓練數(shù)據(jù)和已知的分類器對貝葉斯模型進行訓練患雇,

//訓練數(shù)據(jù)已經(jīng)用bag.feature方法表示為特定的格式

bayes.learn(trainingData,classifiers)

現(xiàn)在我們有了一個訓練好的模型跃脊,可以再次進行驗證環(huán)節(jié)。不過呢苛吱,在案例數(shù)據(jù)中酪术,我們已經(jīng)將簡單的和復雜的垃圾郵件(正常郵件)分開來,因此我們就不使用交叉驗證了翠储,而是用這些測試集來驗證模型绘雁。以垃圾郵件分類作為開始,為此援所,我們使用 spam2 文件夾中的 1397 封垃圾郵件庐舟。

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

44vallistOfSpam2Files=getFilesFromDir(spam2Path)

valspam2Mails=listOfSpam2Files

.map{x=>(x,getMessage(x))}

valspam2FeatureVectors=spam2Mails

.map(x=>bag.feature(x._2.split(" ")))

valspam2ClassificationResults=spam2FeatureVectors

.map(x=>bayes.predict(x))

//Correct classifications are those who resulted in a spam classification (0)

//正確的分類是那些分出垃圾郵件(0)的結(jié)果

valcorrectClassifications=spam2ClassificationResults

.count(x=>x==0)

println(correctClassifications+

" of "+

listOfSpam2Files.length+

"were correctly classified"

)

println(((correctClassifications.toDouble/

listOfSpam2Files.length)*100)+

"% was correctly classified"

)

//In case the algorithm could not decide which category the email

//belongs to, it gives a -1 (unknown) rather than a 0 (spam) or 1 (ham)

//如果算法無法確定一封郵件屬于哪一類,

//它會給出-1(未知的)的結(jié)果而不是0(垃圾郵件)或1(正常郵件)

valunknownClassifications=spam2ClassificationResults

.count(x=>x==-1)

println(unknownClassifications+

" of "+

listOfSpam2Files.length+

"were unknowingly classified"

)

println(((unknownClassifications.toDouble/

listOfSpam2Files.length)*100)+

%wasunknowinglyclassified"

)

如果以不同的特征數(shù)多次運行這段代碼住拭,就可以得到下列結(jié)果:

注意挪略,被標記為垃圾的郵件數(shù)量正是由模型所正確分類的。有趣的是滔岳,在只有 50 個特征的情況杠娱,這個算法分類垃圾郵件表現(xiàn)的最好。不過谱煤,考慮到在這50個最高頻特征詞中仍然有停用詞摊求,這個結(jié)果就不難解釋了。若觀察被分類為垃圾郵件的數(shù)目隨特征數(shù)增加的變化(從 100 開始)刘离,可以看到室叉,特征數(shù)越多睹栖,結(jié)果越大。注意還有一組被分為未知郵件茧痕。這些郵件在“正衬ヌ剩”和“垃圾”兩個類別中的先驗值是相等的。這種情況也適用于那些其中未包含正吃湓ǎ或垃圾郵件特征詞的郵件,因為這樣的話缚柳,算法會認為它有 50% 是正常郵件, 50% 是垃圾郵件埃脏。

現(xiàn)在我們對正常郵件進行相同的分類過程。通過將變量 listOfSpam2Files 的路徑改為 easyHam2Path秋忙,并重新運行該代碼彩掐,我們可以得到以下結(jié)果:

注意現(xiàn)在被正確分類的是那些被標記為“正常”的郵件灰追。從這里可以看到堵幽,事實上當只用50個特征時,被正確分類為正常郵件的數(shù)目明顯低于使用100個特征時的情況弹澎。你應該注意到這點并且對所有類別驗證你的模型朴下,就如在這個例子中,用垃圾郵件和正常郵件的測試數(shù)據(jù)對模型都做了驗證苦蒿。

概括一下這個實例殴胧,我們演示了如何應用樸素貝葉斯算法來分類正常或垃圾郵件佩迟,并得到如下的結(jié)果:高達 87.26% 的垃圾郵件識別率和 97.79% 的正常郵件識別率团滥。這表明樸素貝葉斯算法在識別正常或垃圾郵件時表現(xiàn)得確實相當好报强。

樸素貝葉斯算法實例到這里就結(jié)束了。如果你還想多研究一下這個算法和垃圾郵件分類,文庫里還有一組“困難級別的”正常郵件腾仅,你可以通過調(diào)整特征數(shù)跑筝、剔除更多停用詞來嘗試正確分類。

基于內(nèi)容的郵件排序(推薦系統(tǒng))

這個實例完全是關(guān)于建立你自己的推薦系統(tǒng)的召嘶。我們將基于如下特征對郵件進行排序:“發(fā)送人”夯膀、“主題”、“主題中的公共詞匯”和“郵件正文中的公共詞匯”苍蔬。稍后我們會對實例中的這些特征一一做解釋诱建。注意在設計你自己的推薦系統(tǒng)時,你要自己定義這些特征碟绑,而這正是最困難的環(huán)節(jié)之一俺猿。想出合適的特征來非常重要茎匠,而且就算最終選好了特征,已有的數(shù)據(jù)往往可能無法直接利用押袍。

這個實例旨在教你如何選取特征以及解決在這個過程中使用你自己的數(shù)據(jù)時會遇到的問題诵冒。

我們將會使用郵件數(shù)據(jù)的一個子集,這些郵件數(shù)據(jù)已在實例“垃圾/正常郵件分類”中使用過了谊惭。這個子集可以從這里下載汽馋。此外,你還需要停用詞文件圈盔。注意這些數(shù)據(jù)是一組接收到的郵件豹芯,因此我們還缺少一半數(shù)據(jù),也就是這個郵箱發(fā)送出去的郵件驱敲。然而铁蹈,就算沒有這些信息,我們還是可以做一些相當漂亮的排序工作众眨,待會兒就知道了握牧。

在我們開始建立排序系統(tǒng)之前,我們首先需要從郵件數(shù)據(jù)集中抽取出盡可能多的數(shù)據(jù)來娩梨。因為這些數(shù)據(jù)本身有點冗長沿腰,我們給出處理這個過程的代碼。行內(nèi)注釋對代碼的作用進行了解釋狈定。注意矫俺,該程序一開始就是一個在 GUI 中的 swing 應用。之所以這樣做掸冤,是因為稍后我們要將數(shù)據(jù)繪成圖以洞悉其隱含的模式厘托。同時注意,我們直接將數(shù)據(jù)分割為測試數(shù)據(jù)和訓練數(shù)據(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

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190importjava.awt.{Rectangle}

importjava.io.File

importjava.text.SimpleDateFormat

importjava.util.Date

importsmile.plot.BarPlot

importscala.swing.{MainFrame,SimpleSwingApplication}

importscala.util.Try

objectRecommendationSystemextendsSimpleSwingApplication{

caseclassEmailData(emailDate:Date,sender:String,subject:String,body:String)

deftop=newMainFrame{

title="Recommendation System Example"

valbasePath="/Users/../data"

valeasyHamPath=basePath+"/easy_ham"

valmails=getFilesFromDir(easyHamPath).map(x=>getFullEmail(x))

valtimeSortedMails=mails

.map(x=>EmailData(getDateFromEmail(x),

getSenderFromEmail(x),

getSubjectFromEmail(x),

getMessageBodyFromEmail(x)

)

)

.sortBy(x=>x.emailDate)

val(trainingData,testingData)=timeSortedMails

.splitAt(timeSortedMails.length/2)

}

defgetFilesFromDir(path:String):List[File]={

vald=newFile(path)

if(d.exists&&d.isDirectory){

//Remove the mac os basic storage file,

//and alternatively for unix systems "cmds"

//移除mac os的基本存儲文件或者unix系統(tǒng)的“cmds”文件

d.listFiles.filter(x=>x.isFile&&

!x.toString.contains(".DS_Store")&&

!x.toString.contains("cmds")).toList

}else{

List[File]()

}

}

defgetFullEmail(file:File):String={

//Note that the encoding of the example files is latin1,

//thus this should be passed to the from file method.

//注意案例文件采用latin1編碼,因此應該把它們傳遞給fronFile方法

valsource=scala.io.Source.fromFile(file)("latin1")

valfullEmail=source.getLinesmkString"n"

source.close()

fullEmail

}

defgetSubjectFromEmail(email:String):String={

//Find the index of the end of the subject line

//找出主題行的結(jié)束標志

valsubjectIndex=email.indexOf("Subject:")

valendOfSubjectIndex=email

.substring(subjectIndex).indexOf('n')+subjectIndex

//Extract the subject: start of subject + 7

// (length of Subject:) until the end of the line.

//抽取主題:主題開端+7(主題的長度)饺藤,直到該行結(jié)束

valsubject=email

.substring(subjectIndex+8,endOfSubjectIndex)

.trim

.toLowerCase

//Additionally, we check whether the email was a response and

//remove the 're: ' tag, to make grouping on topic easier:

//此外包斑,我們檢查郵件是否是一封回復并刪除“re:”標簽,使對話題的分組更容易

subject.replace("re: ","")

}

defgetMessageBodyFromEmail(email:String):String={

valfirstLineBreak=email.indexOf("nn")

//Return the message body filtered by only text

//from a-z and to lower case

//返回過濾后的信息主體涕俗,即只包含a-z小寫形式的文本

email.substring(firstLineBreak)

.replace("n"," ")

.replaceAll("[^a-zA-Z ]","")

.toLowerCase

}

defgetSenderFromEmail(email:String):String={

//Find the index of the From: line

//找出帶有“From:”標志的行

valfromLineIndex=email

.indexOf("From:")

valendOfLine=email

.substring(fromLineIndex)

.indexOf('n')+fromLineIndex

//Search for the <> tags in this line, as if they are there,

// the email address is contained inside these tags

//在該行中搜索“<>”標簽罗丰,如果標簽存在,則郵件地址包含在其中

valmailAddressStartIndex=email

.substring(fromLineIndex,endOfLine)

.indexOf('<')+fromLineIndex+1

valmailAddressEndIndex=email

.substring(fromLineIndex,endOfLine)

.indexOf('>')+fromLineIndex

if(mailAddressStartIndex>mailAddressEndIndex){

//The email address was not embedded in <> tags,

// extract the substring without extra spacing and to lower case

//郵件地址不是括在<>標簽內(nèi)再姑,抽取子字符串并去除空格萌抵,轉(zhuǎn)為小寫形式

varemailString=email

.substring(fromLineIndex+5,endOfLine)

.trim

.toLowerCase

//Remove a possible name embedded in () at the end of the line,

//for example in test@test.com (tester) the name would be removed here

//刪除行末可能包含在()內(nèi)的名字,例如,在test@test.com(tester) 中绍填,名字會被刪除掉

valadditionalNameStartIndex=emailString.indexOf('(')

if(additionalNameStartIndex==-1){

emailString

.toLowerCase

}

else{

emailString

.substring(0,additionalNameStartIndex)

.trim

.toLowerCase

}

}

else{

//Extract the email address from the tags.

//抽取標簽中的郵件地址

//If these <> tags are there, there is no () with a name in

// the From: string in our data

//我們的數(shù)據(jù)中霎桅,如果“From:”字符串存在標簽<>,則不會有帶()的名字出現(xiàn)

email

.substring(mailAddressStartIndex,mailAddressEndIndex)

.trim

.toLowerCase

}

}

defgetDateFromEmail(email:String):Date={

//Find the index of the Date: line in the complete email

//在整封郵件中找出日期行的標志

valdateLineIndex=email

.indexOf("Date:")

valendOfDateLine=email

.substring(dateLineIndex)

.indexOf('n')+dateLineIndex

//All possible date patterns in the emails.

//郵件中所有可能的日期格式

valdatePatterns=Array("EEE MMM dd HH:mm:ss yyyy",

"EEE, dd MMM yyyy HH:mm",

"dd MMM yyyy HH:mm:ss",

"EEE MMM dd yyyy HH:mm")

datePatterns.foreach{x=>

//Try to directly return a date from the formatting.

//嘗試用一種日期格式直接返回一個日期

//when it fails on a pattern it continues with the next one

// until one works

//對于一種格式讨永,當返回錯誤時滔驶,它接著嘗試下一種格式直到成功。

Try(returnnewSimpleDateFormat(x)

.parse(email

.substring(dateLineIndex+5,endOfDateLine)

.trim.substring(0,x.length)))

}

//Finally, if all failed return null

//最后卿闹,如果都失敗了則返回null

//(this will not happen with our example data but without

//this return the code will not compile)

//(對于我們的案例數(shù)據(jù)揭糕,這不會發(fā)生,但是沒有這一返回锻霎,代碼就不會編譯)

null

}

}

這一數(shù)據(jù)預處理過程非常普遍著角,并且,一旦你的數(shù)據(jù)格式不標準量窘,如帶有郵件的日期和發(fā)送人,那這個過程將會令人非常糾結(jié)氢拥。不過蚌铜,給出了這段代碼之后,我們的實例數(shù)據(jù)現(xiàn)在就有了以下這些可用的屬性:完整郵件嫩海、接收日期冬殃、發(fā)送人、主題和正文叁怪。有了這些审葬,我們得以確定推薦系統(tǒng)實際要用到的特征。

我們建議選取的第一個特征來源于郵件的發(fā)送人奕谭。那些你經(jīng)常收到 Ta 郵件的人應該排在那些你很少收到 Ta 郵件的人的前面涣觉。這是個很有力的假設,但是你應該會本能地認同我們沒有考慮垃圾郵件這件事血柳。我們來看看發(fā)送人在整個郵件集上的分布官册。

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//Add to the top body:

//添加到主體的頂部

//First we group the emails by Sender, then we extract only the sender address

//and amount of emails, and finally we sort them on amounts ascending

//首先我們按發(fā)送人給郵件分組,然后只抽取發(fā)送人的地址以及郵件數(shù)难捌,

//最后按郵件數(shù)給這些地址按升序排序

valmailsGroupedBySender=trainingData

.groupBy(x=>x.sender)

.map(x=>(x._1,x._2.length))

.toArray

.sortBy(x=>x._2)

//In order to plot the data we split the values from the addresses as

//this is how the plotting library accepts the data.

//為了將數(shù)據(jù)繪圖膝宁,我們將數(shù)值與這些地址分離開,因為繪圖庫只接收這樣的數(shù)據(jù)

valsenderDescriptions=mailsGroupedBySender

.map(x=>x._1)

valsenderValues=mailsGroupedBySender

.map(x=>x._2.toDouble)

valbarPlot=BarPlot.plot("",senderValues,senderDescriptions)

//Rotate the email addresses by -80 degrees such that we can read them

//將郵件地址旋轉(zhuǎn)80度以方便閱讀

barPlot.getAxis(0).setRotation(-1.3962634)

barPlot.setAxisLabel(0,"")

barPlot.setAxisLabel(1,"Amount of emails received ")

peer.setContentPane(barPlot)

bounds=newRectangle(800,600)

這里可以看到根吁,給你發(fā)送郵件最多的人發(fā)了45封员淫,其后是37封,然后就迅速下降了击敌。這些異常值的存在介返,會導致直接使用這些數(shù)據(jù)時,推薦系統(tǒng)僅將郵件發(fā)送最多的 1 到 2 位發(fā)送人列為重要級別,而剩下的則不考慮映皆。為了防止出現(xiàn)這個問題挤聘,我們將通過 log1p 函數(shù)對數(shù)據(jù)進行重縮放。 log1p 函數(shù)是將數(shù)據(jù)加 1捅彻,然后再對其取對數(shù) log组去。將數(shù)據(jù)加 1 的操作是考慮到發(fā)送人只發(fā)送一封郵件的情況。在對數(shù)據(jù)進行這樣一個取對數(shù)操作之后步淹,其圖像是這樣的从隆。

1

2

3

4

5

6

7

8

9//Code changes:

//代碼更新

valmailsGroupedBySender=trainingData

.groupBy(x=>x.sender)

.map(x=>(x._1,Math.log1p(x._2.length)))

.toArray

.sortBy(x=>x._2)

barPlot.setAxisLabel(1,"Amount of emails received on log Scale ")

事實上,這些數(shù)據(jù)仍是相同的缭裆,只不過用了不同的比例來展示键闺。注意數(shù)據(jù)的數(shù)值范圍在 0.69 與 3.83 之間。這個范圍小了很多澈驼,異常值也不會太偏離其他數(shù)據(jù)辛燥。在機器學習領域,這種數(shù)據(jù)操作技巧是很常用的缝其。找到合適的縮放比例需要某種洞察力挎塌。所以,在做重縮放時内边,應用Smile的繪圖庫畫出多幅不同縮放比例的圖榴都,會給工作帶來很大的幫助。

下一個我們要分析的特征是主題出現(xiàn)的頻率和時段漠其。如果一個主題經(jīng)常出現(xiàn)嘴高,那它有可能更重要。此外和屎,我們還考慮了一個郵件會話的持續(xù)時間拴驮。因此,一個主題的頻率可以用這個主題下郵件會話的持續(xù)時間來標準化柴信。這樣莹汤,高度活躍的郵件會話會被排在最前面,再次強調(diào)颠印,這也是我們的一個假設纲岭。

我們來看看這些主題和它們的出現(xiàn)次數(shù):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21//Add to 'def top'

//添加到‘def top’

valmailsGroupedByThread=trainingData

.groupBy(x=>x.subject)

//Create a list of tuples with (subject, list of emails)

//創(chuàng)建一列元組(主題,郵件列表)

valthreadBarPlotData=mailsGroupedByThread

.map(x=>(x._1,x._2.length))

.toArray

.sortBy(x=>x._2)

valthreadDescriptions=threadBarPlotData

.map(x=>x._1)

valthreadValues=threadBarPlotData

.map(x=>x._2.toDouble)

//Code changes in 'def top'

//改變的‘def top’代碼

valbarPlot=BarPlot.plot(threadValues,threadDescriptions)

barPlot.setAxisLabel(1,"Amount of emails per subject")

可以看出這與發(fā)送人的情況有類似的分布线罕,因此我們再一次應用 log1p 函數(shù)止潮。

1

2

3

4

5

6//Code change:

//代碼更新

valthreadBarPlotData=mailsGroupedByThread

.map(x=>(x._1,Math.log1p(x._2.length)))

.toArray

.sortBy(x=>x._2)

現(xiàn)在,每個主題的郵件數(shù)的取值范圍變?yōu)?0.69 到 3.41钞楼,對推薦系統(tǒng)來說喇闸,這比 1 到 29 的范圍要好。不過,我們還沒有把時間段考慮進去燃乍,因此我們回到標準頻率上來唆樊,著手對數(shù)據(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

34//Create a list of tuples with (subject, list of emails,

//time difference between first and last email)

//創(chuàng)建一列元組(主題,郵件列表舆瘪,首末兩封郵件的時間間隔)

valmailGroupsWithMinMaxDates=mailsGroupedByThread

.map(x=>(x._1,x._2,

(x._2

.maxBy(x=>x.emailDate)

.emailDate.getTime-

x._2

.minBy(x=>x.emailDate)

.emailDate.getTime

)/1000

)

)

//turn into a list of tuples with (topic, list of emails,

// time difference, and weight) filtered that only threads occur

//轉(zhuǎn)換為一列過濾后的元組(話題片效,郵件列表,時間間隔英古,權(quán)重)淀衣,其中只出現(xiàn)郵件對話

valthreadGroupedWithWeights=mailGroupsWithMinMaxDates

.filter(x=>x._3!=0)

.map(x=>(x._1,x._2,x._3,10+

Math.log10(x._2.length.toDouble/x._3)))

.toArray

.sortBy(x=>x._4)

valthreadGroupValues=threadGroupedWithWeights

.map(x=>x._4)

valthreadGroupDescriptions=threadGroupedWithWeights

.map(x=>x._1)

//Change the bar plot code to plot this data:

//改變條形圖代碼以將這些數(shù)據(jù)繪圖

valbarPlot=BarPlot.plot(threadGroupValues,threadGroupDescriptions)

barPlot.setAxisLabel(1,"Weighted amount of emails per subject")

注意代碼中我們是如何確定時間差的,并將其除以 1000召调。這是為了把時間單位從毫秒化成秒膨桥。此外,我們用一個主題的頻率除以時間差來計算權(quán)重唠叛。因為該值很小只嚣,我們對其取 log10 函數(shù)以使它放大一些。但這樣做會把數(shù)值變負數(shù)玻墅,因此介牙,我們將每個數(shù)值都加上 10 使其為正數(shù)壮虫。這個加權(quán)過程的結(jié)果如下:

我們想要的數(shù)值大概落在 4.4 到 8.6 的范圍澳厢,這表明異常值對特征的影響已經(jīng)很小。此外囚似,我們觀察權(quán)重最高的 10 個主題和最低的 10 個主題剩拢,看看到底發(fā)生了什么。

權(quán)重最高的 10 個主題

權(quán)重最低的 10 個主題

可以看到饶唤,權(quán)重最高的是那些短時間內(nèi)就收到回復的郵件徐伐,而權(quán)重最低的則是那些回復等待時間很長的郵件。這樣的話募狂,即使是那些頻率很低的主題也可以根據(jù)其往來郵件之間時間間隔短而被排在重要的位置办素。因此毅桃,我們可以得到兩個特征:來自發(fā)送人的郵件數(shù)量 mailsGroupedBySender 和屬于一個已知郵件會話的郵件的權(quán)重 threadGroupedWithWeights空镜。

如前所述萌庆,我們的排序系統(tǒng)是要基于盡可能多的特征的拷恨,我們繼續(xù)找下一個特征唇兑。這個特征是以我們剛剛計算出的權(quán)重值為基礎的握玛。我們的想法是寺惫,郵箱會收到帶有不同主題的新郵件菇晃。不過,這些郵件的主題有可能含有類似于之前收到的重要郵件主題的關(guān)鍵詞呆万。因此商源,在一個郵件會話(一個主題下的多封往來郵件)開始之前,我們就能夠?qū)⑧]件排序谋减。為此牡彻,我們把關(guān)鍵詞的權(quán)重指定為含有這些關(guān)鍵詞的主題的權(quán)重。如果多個主題都含有這些關(guān)鍵詞逃顶,我們就選擇權(quán)重最高的一個排在第一位讨便。

這個特征有個問題,那就是停用詞以政。還好霸褒,目前我們有一個停用詞文件可以讓我們剔除(大部分)英語停用詞。然而盈蛮,當你在設計自己的系統(tǒng)時废菱,還應該考慮到可能會有多種語言出現(xiàn),這時你就要剔除所有這些語言的停用詞了抖誉。此外殊轴,在不同的語言中,某些單詞會有多種不同的意思袒炉,因此旁理,在這種情況下剔除停用詞就要小心了。就現(xiàn)在來說我磁,我們繼續(xù)剔除英語停用詞孽文。這個特征的代碼如下:

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

26defgetStopWords:List[String]={

valsource=scala.io.Source

.fromFile(newFile("/Users/../stopwords.txt"))("latin1")

vallines=source.mkString.split("n")

source.close()

lines.toList

}

//Add to top:

//添加到頂部

valstopWords=getStopWords

valthreadTermWeights=threadGroupedWithWeights

.toArray

.sortBy(x=>x._4)

.flatMap(x=>x._1

.replaceAll("[^a-zA-Z ]","")

.toLowerCase.split(" ")

.filter(_.nonEmpty)

.map(y=>(y,x._4)))

valfilteredThreadTermWeights=threadTermWeights

.groupBy(x=>x._1)

.map(x=>(x._1,x._2.maxBy(y=>y._2)._2))

.toArray.sortBy(x=>x._1)

.filter(x=>!stopWords.contains(x._1))

給定這段代碼,我們就得到了一張列表 filteredThreadTermWeights夺艰,該列表基于已有郵件會話中的權(quán)重列出了一些關(guān)鍵詞芋哭。這些權(quán)重可以用來計算新郵件的主題的權(quán)重,即使這封郵件不是對已有會話的回復郁副。

對于第四個特征减牺,我們打算合并考慮在所有郵件中出現(xiàn)頻率很高的詞匯的權(quán)重。為此存谎,我們要創(chuàng)建一個TDM拔疚,不過這一次和前一個例子中的有點不同,這次既荚,我們只將所有文檔中的詞匯頻率取對數(shù)稚失。此外,我們還要對出現(xiàn)率取 log10 函數(shù)固以。這么做可以縮小詞匯頻率的比例墩虹,防止結(jié)果被可能的異常值影響嘱巾。

1

2

3

4

5

6valtdm=trainingData

.flatMap(x=>x.body.split(" "))

.filter(x=>x.nonEmpty&&!stopWords.contains(x))

.groupBy(x=>x)

.map(x=>(x._1,Math.log10(x._2.length+1)))

.filter(x=>x._2!=0)

這個 TDM 列表可以幫助我們根據(jù)歷史數(shù)據(jù)計算新郵件正文的權(quán)重,這個權(quán)重很重要诫钓。

準備了這 4 個特征之后旬昭,我們就可以對訓練數(shù)據(jù)做實際的排序計算。為此菌湃,我們要計算出每封郵件的 senderWeight (代表發(fā)送人的權(quán)重)问拘、termWeight (代表主題詞匯的權(quán)重)、threadGroupWeight (代表郵件會話的權(quán)重)以及 commonTermsWeight (代表郵件正文的權(quán)重)惧所,并將它們相乘以得到最后的排序骤坐。由于我們是做乘法而不是加法,我們需要小心那些小于1的數(shù)值下愈。例如纽绍,有人發(fā)送了一封郵件,那么其 sengerWeight 就是 0.69势似,若將其與那些還未發(fā)送過任何郵件的人作比較就會不公平拌夏,因為對方的 senderWeight 值是 1。因此履因,對于那些數(shù)值可能低于 1 的各個特征障簿,我們?nèi)『瘮?shù) Math.max(value,1)。我們來看看代碼:

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

54

55

56

57

58

59

60

61

62

63

64

65

66

67valtrainingRanks=trainingData.map(mail=>{

//Determine the weight of the sender, if it is lower than 1, pick 1 instead

//確定發(fā)送人的權(quán)重栅迄,如果小于1站故,則設為1

//This is done to prevent the feature from having a negative impact

//這是為了防止特征出現(xiàn)負面影響

valsenderWeight=mailsGroupedBySender

.collectFirst{case(mail.sender,x)=>Math.max(x,1)}

.getOrElse(1.0)

//Determine the weight of the subject

//確定主題的權(quán)重

valtermsInSubject=mail.subject

.replaceAll("[^a-zA-Z ]","")

.toLowerCase.split(" ")

.filter(x=>x.nonEmpty&&

!stopWords.contains(x)

)

valtermWeight=if(termsInSubject.size>0)

Math.max(termsInSubject

.map(x=>{

tdm.collectFirst{case(y,z)ify==x=>z}

.getOrElse(1.0)

})

.sum/termsInSubject.length,1)

else1.0

//Determine if the email is from a thread,

//and if it is the weight from this thread:

//判斷郵件是否來自一個會話,如果是毅舆,其在該會話的權(quán)重:

valthreadGroupWeight:Double=threadGroupedWithWeights

.collectFirst{case(mail.subject,_,_,weight)=>weight}

.getOrElse(1.0)

//Determine the commonly used terms in the email and the weight belonging to it:

//確定郵件中的常用詞匯及其權(quán)重:

valtermsInMailBody=mail.body

.replaceAll("[^a-zA-Z ]","")

.toLowerCase.split(" ")

.filter(x=>x.nonEmpty&&

!stopWords.contains(x)

)

valcommonTermsWeight=if(termsInMailBody.size>0)

Math.max(termsInMailBody

.map(x=>{

tdm.collectFirst{case(y,z)ify==x=>z}

.getOrElse(1.0)

})

.sum/termsInMailBody.length,1)

else1.0

valrank=termWeight *

threadGroupWeight *

commonTermsWeight *

senderWeight

(mail,rank)

})

valsortedTrainingRanks=trainingRanks

.sortBy(x=>x._2)

valmedian=sortedTrainingRanks(sortedTrainingRanks.length/2)._2

valmean=sortedTrainingRanks

.map(x=>x._2).sum/

sortedTrainingRanks.length

我們計算了訓練集中所有郵件的排序西篓,還將它們做了排序并取中位數(shù)和平均值。我們?nèi)≈形粩?shù)和平均值是為了確定一個決策邊界朗兵,通過該邊界來評估一封郵件是優(yōu)先級還是非優(yōu)先級污淋。在實踐中顶滩,這個辦法通常并不管用余掖。實際上最好的辦法是讓用戶標記一組郵件作為優(yōu)先級,與另一組郵件作為非優(yōu)先級礁鲁。然后就可以用這些郵件的排序來計算出決策邊界盐欺,此外還可以確認排序系統(tǒng)的特征是否選得正確。如果最終用戶標記為非優(yōu)先級的郵件評級比標記為優(yōu)先級的郵件還高仅醇,那你可能要重新評估你所選取的特征冗美。

我們之所以提出這個決策邊界,而不是僅僅對用戶郵件進行排序析二,是考慮了時間這個因素粉洼。如果你純粹根據(jù)排序來整理郵件节预,那結(jié)果將會令人討厭,因為人們通常喜歡根據(jù)時間來整理郵件属韧。然而安拟,假設我們要把這一排序系統(tǒng)融合到一個郵件客戶端中,有了這個決策邊界之后宵喂,我們就可以標記優(yōu)先級郵件糠赦,然后把它們單獨顯示在一個列表中。

我們來看看在訓練集中锅棕,有多少封郵件被標記為優(yōu)先級拙泽。為此,我們首先需要添加下列代碼:

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

54

55

56

57

58

59

60

61

62

63

64

65

66valtestingRanks=trainingData.map(mail=>{

//mail contains (full content, date, sender, subject, body)

//包含(全部內(nèi)容裸燎、日期顾瞻、發(fā)送人、主題德绿、正文)的郵件

//Determine the weight of the sender

//確定發(fā)送人的權(quán)重

valsenderWeight=mailsGroupedBySender

.collectFirst{case(mail.sender,x)=>Math.max(x,1)}

.getOrElse(1.0)

//Determine the weight of the subject

//確定主題的權(quán)重

valtermsInSubject=mail.subject

.replaceAll("[^a-zA-Z ]","")

.toLowerCase.split(" ")

.filter(x=>x.nonEmpty&&

!stopWords.contains(x)

)

valtermWeight=if(termsInSubject.size>0)

Math.max(termsInSubject

.map(x=>{

tdm.collectFirst{case(y,z)ify==x=>z}

.getOrElse(1.0)

})

.sum/termsInSubject.length,1)

else1.0

//Determine if the email is from a thread,

//and if it is the weight from this thread:

//判斷一封郵件是否來自一個會話朋其,如果是,其在該會話的權(quán)重:

valthreadGroupWeight:Double=threadGroupedWithWeights

.collectFirst{case(mail.subject,_,_,weight)=>weight}

.getOrElse(1.0)

//Determine the commonly used terms in the email and the weight belonging to it:

//確定郵件中的常用詞匯及其權(quán)重:

valtermsInMailBody=mail.body

.replaceAll("[^a-zA-Z ]","")

.toLowerCase.split(" ")

.filter(x=>x.nonEmpty&&

!stopWords.contains(x)

)

valcommonTermsWeight=if(termsInMailBody.size>0)

Math.max(termsInMailBody

.map(x=>{

tdm.collectFirst{case(y,z)ify==x=>z}

.getOrElse(1.0)

})

.sum/termsInMailBody.length,1)

else1.0

valrank=termWeight *

threadGroupWeight *

commonTermsWeight *

senderWeight

(mail,rank)

})

valpriorityEmails=testingRanks

.filter(x=>x._2>=mean)

println(priorityEmails.length+" ranked as priority")

在實際執(zhí)行了這個測試代碼以后脆炎,你將看到這個測試集被標記為優(yōu)先級的郵件數(shù)量實際是 563梅猿,占到測試集郵件數(shù)的 45%。這是一個相當大的值秒裕,所以我們可以用決策邊界進行調(diào)節(jié)袱蚓。不過,我們僅以此作為說明的目的几蜻,這個值并不應該拿去代表實際情況喇潘,所以我們就不再去糾結(jié)那個百分比。反之梭稚,我們來看看優(yōu)先級郵件中評級最高的10封颖低。

注意我已經(jīng)將電子郵件地址的部分內(nèi)容刪除了,以防止垃圾郵件程序抓取這些地址弧烤。從下表中可以看到忱屑,這10封優(yōu)先級最高的郵件中,大部分郵件都來自不同的會話暇昂,這些會話的活動性很高莺戒。以評級最高的那封郵件為例,這封郵件是一封9分鐘前的郵件的回復急波。這就表明了這個郵件會話的重要性从铲。

此外,我們還看到 tim.One… 在這個表中出現(xiàn)很多次澄暮。這反映出他的所有郵件都很重要名段,或者阱扬,他發(fā)送了這么多郵件以致排序系統(tǒng)自動將其評為優(yōu)先。作為這個實例的最后一步伸辟,我們對這一點再做些討論:

1

2

3

4

5

6

7

8

9

10

11

12valtimsEmails=testingRanks

.filter(x=>x._1.sender=="tim.one@...")

.sortBy(x=>-x._2)

timsEmails

.foreach(x=>println("| "+

x._1.emailDate+

" | "+

x._1.subject+

" | "+

df.format(x._2)+

" |")

)

運行這一段代碼后价认,一個包含 45 封郵件的列表就會被打印出來,評級最低的 10 封郵件如下:

我們知道決策邊界就是平均值自娩,這里是 25.06用踩,那么可以看出,Tim 只有一封郵件沒有被標記為優(yōu)先級忙迁。這表明脐彩,一方面,我們的決策邊界太低了姊扔,而另一方面惠奸,Tim也許真的發(fā)送了很多重要郵件,否則很多郵件的評級就會低于決策邊界恰梢。不幸的是佛南,我們無法為你提供確切的答案,因為我們不是這些測試數(shù)據(jù)的原主人嵌言。

當你手中的數(shù)據(jù)并非一手數(shù)據(jù)時嗅回,驗證一個像這樣的排序系統(tǒng)是相當困難的。驗證并改善這個系統(tǒng)最常用的方式是將它開放給客戶使用摧茴,讓他們標記绵载、糾正錯誤。而這些糾錯就可以用來改善系統(tǒng)苛白。

總的來說娃豹,我們介紹了如何從含有異常值的原始數(shù)據(jù)中獲取特征,以及如何把這些特征耦合到最終的排序值中购裙。此外懂版,我們還嘗試對這些特征進行驗證,但由于缺乏對數(shù)據(jù)集的確切把握躏率,我們無法得出明確的結(jié)論躯畴。不過,如果你想使用自己的直接數(shù)據(jù)進行同樣的過程禾锤,那么這個實例就可以幫助你建立自己的排序系統(tǒng)私股。

根據(jù)身高預測體重(應用普通最小二乘法)

在這部分中摹察,我們將介紹普通最小二乘法恩掷,它是一種線性回歸方法。因為這種方法十分強大供嚎,所以開始實例之前黄娘,有必要先了解回歸與常見的陷阱峭状。我們將在這部分介紹一部分這些問題,其他的相關(guān)問題我們已在欠擬合與過擬合這兩小節(jié)中介紹過了逼争。

線性回歸的基本思想是用一條“最優(yōu)的”回歸線來擬合數(shù)據(jù)點。注意這只對線性數(shù)據(jù)并且無過大異常值的情況才適用。如果你的數(shù)據(jù)不滿足這種條件喂江,那你可以嘗試對數(shù)據(jù)進行操作诺祸,例如將數(shù)據(jù)取平方或取對數(shù),直到滿足適用條件杂伟。

與往常一樣移层,在一項工程開始時,首先要導入一個數(shù)據(jù)集赫粥。為此观话,我們提供了如下csv 文件以及讀取文件用的代碼:

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

33defgetDataFromCSV(file:File):(Array[Array[Double]],Array[Double])={

valsource=scala.io.Source.fromFile(file)

valdata=source.getLines().drop(1).map(x=>getDataFromString(x)).toArray

source.close()

varinputData=data.map(x=>x._1)

varresultData=data.map(x=>x._2)

return(inputData,resultData)

}

defgetDataFromString(dataString:String):(Array[Double],Double)={

//Split the comma separated value string into an array of strings

//把用逗號分隔的數(shù)值字符串分解為一個字符串數(shù)組

valdataArray:Array[String]=dataString.split(',')

varperson=1.0

if(dataArray(0)==""Male""){

person=0.0

}

//Extract the values from the strings

//從字符串中抽取數(shù)值

//Since the data is in US metrics

//inch and pounds we will recalculate this to cm and kilo's

//因數(shù)據(jù)是采用美制單位英寸和磅,我們要將它們轉(zhuǎn)換為厘米和千克

valdata:Array[Double]=Array(person,dataArray(1).toDouble *2.54)

valweight:Double=dataArray(2).toDouble *0.45359237

//And return the result in a format that can later easily be used to feed to Smile

//并以一定格式返回結(jié)果越平,使得該結(jié)果之后容易輸入到Smile中處理

return(data,weight)

}

注意频蛔,該數(shù)據(jù)讀取器將數(shù)值從英制單位轉(zhuǎn)換為公制單位。這對 OLS 的應用沒有什么大影響秦叛,不過我們還是采用更為常用的公制單位晦溪。

這樣操作之后我們得到一個數(shù)組 Array[Array[Double]],該數(shù)組包含了數(shù)據(jù)點和 Array[Double] 值挣跋,該值代表男性或女性尼变。這種格式既有利于將數(shù)據(jù)繪圖,也有利于將數(shù)據(jù)導入機器學習算法中浆劲。

我們首先看看數(shù)據(jù)是什么樣的嫌术。為此,用下列代碼將數(shù)據(jù)繪成圖牌借。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21objectLinearRegressionExampleextendsSimpleSwingApplication{

deftop=newMainFrame{

title="Linear Regression Example"

valbasePath="/Users/.../OLS_Regression_Example_3.csv"

valtestData=getDataFromCSV(newFile(basePath))

valplotData=(testData._1ziptestData._2).map(x=>Array(x._1(1),x._2))

valmaleFemaleLabels=testData._1.map(x=>x(0).toInt)

valplot=ScatterPlot.plot(plotData,

maleFemaleLabels,

'@',

Array(Color.blue,Color.green)

)

plot.setTitle("Weight and heights for male and females")

plot.setAxisLabel(0,"Heights")

plot.setAxisLabel(1,"Weights")

peer.setContentPane(plot)

size=newDimension(400,400)

}

如果你執(zhí)行上面這段代碼度气,就會彈出一個窗口顯示以下右邊那幅圖像。注意當代碼運行時膨报,你可以滾動鼠標來放大和縮小圖像磷籍。

在這幅圖像中,綠色代表女性现柠,藍色代表男性院领,可以看到,男女的身高和體重有很大部分是重疊的够吩。因此比然,如果我們忽略男女性別,數(shù)據(jù)看上去依舊是呈線性的(如圖所示)周循。然而强法,若不考慮男女性別差異万俗,模型就不夠精確。

在本例中饮怯,找出這種區(qū)別(將數(shù)據(jù)依性別分組)是小事一樁闰歪,然而,你可能會碰到一些其中的數(shù)據(jù)區(qū)分不那么明顯的數(shù)據(jù)集蓖墅。意識到這種可能性對數(shù)據(jù)分組是有幫助的库倘,從而有助于改善機器學習應用程序的性能。

既然我們已經(jīng)考察過數(shù)據(jù)论矾,也知道我們確實可以建立一條回歸線來擬合數(shù)據(jù)于樟,現(xiàn)在就該訓練模型了。Smile 庫提供了普通最小二乘算法拇囊,我們可以用如下代碼輕松調(diào)用:

Scala

1

valolsModel=newOLS(testData._1,testData._2)

有了這個 OLS 模型迂曲,我們現(xiàn)在可以根據(jù)某人的身高和性別預測其體重了:

1

2

3println("Prediction for Male of 1.7M: "+olsModel.predict(Array(0.0,170.0)))

println("Prediction for Female of 1.7M:"+olsModel.predict(Array(1.0,170.0)))

println("Model Error:"+olsModel.error())

結(jié)果如下:

1

2

3PredictionforMaleof1.7M:79.14538559840447

PredictionforFemaleof1.7M:70.35580395758966

ModelError:4.5423150758157185

回顧前文的分類算法,它有一個能夠反映模型性能的先驗值寥袭÷放酰回歸分析是一種更強大的統(tǒng)計方法,它可以給出一個實際誤差传黄。這個值反映了偏離擬合回歸線的平均程度杰扫,因此可以說,在這個模型中膘掰,一個身高1.70米的男性的預測體重是 79.15kg ± 4.54kg章姓,4.54 為誤差值。注意识埋,如果不考慮數(shù)據(jù)的男女差異凡伊,這一誤差會增加到 5.5428。換言之窒舟,考慮了數(shù)據(jù)的男女差異后系忙,模型在預測時,精確度提高了 ±1kg

最后一點惠豺,Smile 庫也提供了一些關(guān)于模型的統(tǒng)計信息银还。R平方值是模型的均方根誤差(RMSE)與平均函數(shù)的 RMSE 之比。這個值介于 0 與 1 之間洁墙。假如你的模型能夠準確的預測每一個數(shù)據(jù)點蛹疯,R平方值就是 1,如果模型的預測效果比平均函數(shù)差热监,則該值為 0捺弦。在機器學習領域中,通常將該值乘以 100,代表模型的精確度羹呵。它是一個歸一化值骂际,所以可以用來比較不同模型的性能疗琉。

本部分總結(jié)了線性回歸分析的過程冈欢,如果你還想了解如何將回歸分析應用于非線性數(shù)據(jù),請隨時學習下一個實例“應用文本回歸嘗試暢銷書排行預測”盈简。

應用文本回歸嘗試預測最暢銷書排行

在實例“根據(jù)身高預測體重”中凑耻,我們介紹了線性回歸的概念。然而柠贤,有時候需要將回歸分析應用到像文本這類的非數(shù)字數(shù)據(jù)中去香浩。

在本例中,我們將通過嘗試預測最暢銷的 100 本 O’Reilly 公司出版的圖書臼勉,說明如何應用文本回歸邻吭。此外,我們還介紹在本例的特殊情況下應用文本回歸無法解決問題宴霸。原因僅僅是這些數(shù)據(jù)中不含有可以被我們的測試數(shù)據(jù)利用的信號囱晴。即使如此,本例也并非一無是處瓢谢,因為在實踐中畸写,數(shù)據(jù)可能會含有實際信號,該信號可以被這里要介紹的文本回歸檢測到氓扛。

本例使用到的數(shù)文件可以在這里下載枯芬。除了 Smile 庫,本例也會使用Scala-csv 庫采郎,因為 csv 中包含帶逗號的字符串千所。我們從獲取需要的數(shù)據(jù)開始:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18objectTextRegression{

defmain(args:Array[String]):Unit={

//Get the example data

//獲取案例數(shù)據(jù)

valbasePath="/users/.../TextRegression_Example_4.csv"

valtestData=getDataFromCSV(newFile(basePath))

}

defgetDataFromCSV(file:File):List[(String,Int,String)]={

valreader=CSVReader.open(file)

valdata=reader.all()

valdocuments=data.drop(1).map(x=>(x(1),x(3)toInt,x(4)))

returndocuments

}

}

現(xiàn)在我們得到了 O’Reilly 出版社最暢銷100部圖書的書名、排序和詳細說明蒜埋。然而真慢,當涉及某種回歸分析時,我們需要數(shù)字數(shù)據(jù)理茎。這就是問什么我們要建立一個文檔詞匯矩陣 (DTM)黑界。注意這個 DTM 與我們在垃圾郵件分類實例中建立的詞匯文檔矩陣 (TDM) 是類似的。區(qū)別在于皂林,DTM 存儲的是文檔記錄朗鸠,包含文檔中的詞匯,相反础倍,TDM 存儲的是詞匯記錄烛占,包含這些詞匯所在的一系列文檔。

我們自己用如下代碼生成 DTM:

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

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78importjava.io.File

importscala.collection.mutable

classDTM{

varrecords:List[DTMRecord]=List[DTMRecord]()

varwordList:List[String]=List[String]()

defaddDocumentToRecords(documentName:String,rank:Int,documentContent:String)={

//Find a record for the document

//找出一條文檔記錄

valrecord=records.find(x=>x.document==documentName)

if(record.nonEmpty){

thrownewException("Document already exists in the records")

}

varwordRecords=mutable.HashMap[String,Int]()

valindividualWords=documentContent.toLowerCase.split(" ")

individualWords.foreach{x=>

valwordRecord=wordRecords.find(y=>y._1==x)

if(wordRecord.nonEmpty){

wordRecords+=x->(wordRecord.get._2+1)

}

else{

wordRecords+=x->1

wordList=x::wordList

}

}

records=newDTMRecord(documentName,rank,wordRecords)::records

}

defgetStopWords():List[String]={

valsource=scala.io.Source.fromFile(newFile("/Users/.../stopwords.txt"))("latin1")

vallines=source.mkString.split("n")

source.close()

returnlines.toList

}

defgetNumericRepresentationForRecords():(Array[Array[Double]],Array[Double])={

//First filter out all stop words:

//首先過濾出所有停用詞

valStopWords=getStopWords()

wordList=wordList.filter(x=>!StopWords.contains(x))

vardtmNumeric=Array[Array[Double]]()

varranks=Array[Double]()

records.foreach{x=>

//Add the rank to the array of ranks

//將評級添加到排序數(shù)組中

ranks=ranks:+x.rank.toDouble

//And create an array representing all words and their occurrences

//for this document:

//為該文檔創(chuàng)建一個數(shù)組,表示所有單詞及其出現(xiàn)率

vardtmNumericRecord:Array[Double]=Array()

wordList.foreach{y=>

valtermRecord=x.occurrences.find(z=>z._1==y)

if(termRecord.nonEmpty){

dtmNumericRecord=dtmNumericRecord:+termRecord.get._2.toDouble

}

else{

dtmNumericRecord=dtmNumericRecord:+0.0

}

}

dtmNumeric=dtmNumeric:+dtmNumericRecord

}

return(dtmNumeric,ranks)

}

}

classDTMRecord(valdocument:String,

valrank:Int,

varoccurrences:mutable.HashMap[String,Int]

)

觀察這段代碼忆家,注意到這里面有一個方法 def getNumericRepresentationForRecords(): (Array[Array[Double]], Array[Double])犹菇。這一方法返回一個元組,該元組以一個矩陣作為第一個參數(shù)芽卿,該矩陣中每一行代表一個文檔揭芍,每一列代表來自 DTM 文檔的完備詞匯集中的詞匯。注意第一個列表中的浮點數(shù)表示詞匯出現(xiàn)的次數(shù)卸例。

第二個參數(shù)是一個數(shù)組称杨,包含第一個列表中所有記錄的排序值。

現(xiàn)在我們可以按如下方式擴展主程序筷转,這樣就可以得到所有文檔的數(shù)值表示:

1

2valdocumentTermMatrix=newDTM()

testData.foreach(x=>documentTermMatrix.addDocumentToRecords(x._1,x._2,x._3))

有了這個從文本到數(shù)值的轉(zhuǎn)換姑原,現(xiàn)在我們可以利用回歸分析工具箱了。我們在“基于身高預測體重”的實例中應用了普通最小二乘法 (OLS)呜舒,不過這次我們要應用“最小絕對收縮與選擇算子”(Lasso) 回歸锭汛。這是因為我們可以給這種回歸方法提供某個 λ 值,它代表一個懲罰值袭蝗。該懲罰值可以幫助 LASSO 算法選擇相關(guān)的特征(單詞)而丟棄其他一些特征(單詞)唤殴。

LASSO 執(zhí)行的這一特征選擇功能非常有用,因為在本例中呻袭,文檔說明包含了大量的單詞眨八。LASSO 會設法找出那些單詞的一個合適的子集作為特征,而要是應用 OLS左电,則所有單詞都會被使用廉侧,那么運行時間將會變得極其漫長。此外篓足,OLS 算法實現(xiàn)會檢測非滿秩段誊。這是維數(shù)災難的一種情形。

無論如何栈拖,我們需要找出一個最佳的 λ 值连舍,因此,我們應該用交叉驗證法嘗試幾個 λ 值涩哟,操作過程如下:

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

54

55

56

57

58

59

60

61

62

63

64for(i<-0untilcv.k){

//Split off the training datapoints and classifiers from the dataset

//從數(shù)據(jù)集中將用于訓練的數(shù)據(jù)點與分類器分離出來

valdpForTraining=numericDTM

._1

.zipWithIndex

.filter(x=>cv

.test(i)

.toList

.contains(x._2)

)

.map(y=>y._1)

valclassifiersForTraining=numericDTM

._2

.zipWithIndex

.filter(x=>cv

.test(i)

.toList

.contains(x._2)

)

.map(y=>y._1)

//And the corresponding subset of data points and their classifiers for testing

//以及對應的用于測試的數(shù)據(jù)點子集及其分類器

valdpForTesting=numericDTM

._1

.zipWithIndex

.filter(x=>!cv

.test(i)

.contains(x._2)

)

.map(y=>y._1)

valclassifiersForTesting=numericDTM

._2

.zipWithIndex

.filter(x=>!cv

.test(i)

.contains(x._2)

)

.map(y=>y._1)

//These are the lambda values we will verify against

//這些是我們將要驗證的λ值

vallambdas:Array[Double]=Array(0.1,0.25,0.5,1.0,2.0,5.0)

lambdas.foreach{x=>

//Define a new model based on the training data and one of the lambda's

//定義一個基于訓練數(shù)據(jù)和其中一個λ值的新模型

valmodel=newLASSO(dpForTraining,classifiersForTraining,x)

//Compute the RMSE for this model with this lambda

//計算該模型的RMSE值

valresults=dpForTesting.map(y=>model.predict(y))zipclassifiersForTesting

valRMSE=Math

.sqrt(results

.map(x=>Math.pow(x._1-x._2,2)).sum/

results.length

)

println("Lambda: "+x+" RMSE: "+RMSE)

}

}

多次運行這段代碼會給出一個在 36 和 51 之間變化的 RMSE 值索赏。這表示我們排序的預測值會偏離至少 36 位。鑒于我們要嘗試預測最高的 100 位贴彼,結(jié)果表明這個模型的效果非常差潜腻。在本例中,λ 值變化對模型的影響并不明顯器仗。然而融涣,在實踐中應用這種算法時童番,要小心地選取 λ 值: λ 值選得越大,算法選取的特征數(shù)就越少威鹿。?所以剃斧,交叉驗證法對分析不同 λ 值對算法的影響很重要。

引述John Tukey的一句話來總結(jié)這個實例:

“數(shù)據(jù)中未必隱含答案忽你。某些數(shù)據(jù)和對答案的迫切渴求的結(jié)合幼东,無法保證人們從一堆給定數(shù)據(jù)中提取出一個合理的答案√醇校”

應用無監(jiān)督學習合并特征(PCA)

主成分分析 (PCA) 的基本思路是減少一個問題的維數(shù)筋粗。這是一個很好的方法策橘,它可以避免維災難炸渡,也可以幫助合并數(shù)據(jù),避開無關(guān)數(shù)據(jù)的干擾丽已,使其中的趨勢更明顯蚌堵。

在本例中,我們打算應用 PCA 把 2002-2012 年這段時間內(nèi) 24 只股票的股價合并為一只股票的股價沛婴。這個隨時間變化的值就代表一個基于這 24 只股票數(shù)據(jù)的股票市場指數(shù)吼畏。把這24種股票價格合并為一種,明顯地減少了處理過程中的數(shù)據(jù)量嘁灯,并減少了數(shù)據(jù)維數(shù)泻蚊,對于之后應用其他機器學習算法作預測,如回歸分析來說丑婿,有很大的好處性雄。為了看出特征數(shù)從 24 減少為 1 之后的效果,我們會將結(jié)果與同一時期的道瓊斯指數(shù) (DJI) 作比較。

隨著工程的開始,下一步要做的是加載數(shù)據(jù)灯蝴。為此扇丛,我們提供了兩個文件:Data file 1Data file 2.

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

54

55

56

57

58

59

60

61

62

63

64

65

66

67objectPCAextendsSimpleSwingApplication{

deftop=newMainFrame{

title="PCA Example"

//Get the example data

//獲取案例數(shù)據(jù)

valbasePath="/users/.../Example Data/"

valexampleDataPath=basePath+"PCA_Example_1.csv"

valtrainData=getStockDataFromCSV(newFile(exampleDataPath))

}

defgetStockDataFromCSV(file:File):(Array[Date],Array[Array[Double]])={

valsource=scala.io.Source.fromFile(file)

//Get all the records (minus the header)

//獲取所有記錄(減去標頭)

valdata=source

.getLines()

.drop(1)

.map(x=>getStockDataFromString(x))

.toArray

source.close()

//group all records by date, and sort the groups on date ascending

//按日期將所有記錄分組,并按日期將組升序排列

valgroupedByDate=data.groupBy(x=>x._1).toArray.sortBy(x=>x._1)

//extract the values from the 3-tuple and turn them into

// an array of tuples: Array[(Date, Array[Double)]

//抽取這些3元組的值并將它們轉(zhuǎn)換為一個元組數(shù)組:Array[(Date,Array[Double])]

valdateArrayTuples=groupedByDate

.map(x=>(x._1,x

._2

.sortBy(x=>x._2)

.map(y=>y._3)

)

)

//turn the tuples into two separate arrays for easier use later on

//將這些元組分隔為兩個數(shù)組以方便之后使用

valdateArray=dateArrayTuples.map(x=>x._1).toArray

valdoubleArray=dateArrayTuples.map(x=>x._2).toArray

(dateArray,doubleArray)

}

defgetStockDataFromString(dataString:String):(Date,String,Double)={

//Split the comma separated value string into an array of strings

//把用逗號分隔的數(shù)值字符串分解為一個字符串數(shù)組

valdataArray:Array[String]=dataString.split(',')

valformat=newSimpleDateFormat("yyyy-MM-dd")

//Extract the values from the strings

//從字符串中抽取數(shù)值

valdate=format.parse(dataArray(0))

valstock:String=dataArray(1)

valclose:Double=dataArray(2).toDouble

//And return the result in a format that can later

//easily be used to feed to Smile

//并以一定格式返回結(jié)果啤誊,使得該結(jié)果之后容易輸入到Smile中處理

(date,stock,close)

}

}

有了訓練數(shù)據(jù),并且我們已經(jīng)知道要將24個特征合并為一個單獨的特征,現(xiàn)在我們可以進行主成分分析细卧,并按如下方式為數(shù)據(jù)點檢索數(shù)據(jù)。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16//Add to `def top`

//添加到‘def top’中

valpca=newPCA(trainData._2)

pca.setProjection(1)

valpoints=pca.project(trainData._2)

valplotData=points

.zipWithIndex

.map(x=>Array(x._2.toDouble,-x._1(0)))

valcanvas:PlotCanvas=LinePlot.plot("Merged Features Index",

plotData,

Line.Style.DASH,

Color.RED);

peer.setContentPane(canvas)

size=newDimension(400,400)

這段代碼不僅執(zhí)行了 PCA筒占,還將結(jié)果繪成圖像贪庙,y 軸表示特征值,x 軸表示每日赋铝。

為了能看出 PCA 合并的效果插勤,我們現(xiàn)在通過如下方式調(diào)整代碼將道瓊斯指數(shù)加入到圖像中:

首先把下列代碼添加到 def top 方法中:

1

2

3

4

5

6//Verification against DJI

//用道瓊斯指數(shù)驗證

valverificationDataPath=basePath+"PCA_Example_2.csv"

valverificationData=getDJIFromFile(newFile(verificationDataPath))

valDJIIndex=getDJIFromFile(newFile(verificationDataPath))

canvas.line("Dow Jones Index",DJIIndex._2,Line.Style.DOT_DASH,Color.BLUE)

然后我們需要引入下列兩個方法:

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

37defgetDJIRecordFromString(dataString:String):(Date,Double)={

//Split the comma separated value string into an array of strings

//把用逗號分隔的數(shù)值字符串分解為一個字符串數(shù)組

valdataArray:Array[String]=dataString.split(',')

valformat=newSimpleDateFormat("yyyy-MM-dd")

//Extract the values from the strings

//從字符串中抽取數(shù)值

valdate=format.parse(dataArray(0))

valclose:Double=dataArray(4).toDouble

//And return the result in a format that can later

//easily be used to feed to Smile

//并以一定格式返回結(jié)果,使得該結(jié)果之后容易輸入到Smile中處理

(date,close)

}

defgetDJIFromFile(file:File):(Array[Date],Array[Double])={

valsource=scala.io.Source.fromFile(file)

//Get all the records (minus the header)

//獲取所有記錄(減去標頭)

valdata=source

.getLines()

.drop(1)

.map(x=>getDJIRecordFromString(x)).toArray

source.close()

//turn the tuples into two separate arrays for easier use later on

//將這些元組分隔為兩個數(shù)組以方便之后使用

valsortedData=data.sortBy(x=>x._1)

valdates=sortedData.map(x=>x._1)

valdoubles=sortedData.map(x=>x._2)

(dates,doubles)

}

這段代碼加載了 DJI 數(shù)據(jù),并把它繪成圖線添加到我們自己的股票指數(shù)圖中农尖。然而析恋,當我們執(zhí)行這段代碼時,效果圖有點無用盛卡。

如你所見助隧,DJI 的取值范圍與我們的計算特征的取值范圍偏離很遠。因此滑沧,現(xiàn)在我們要將數(shù)據(jù)標準化并村。辦法就是根據(jù)數(shù)據(jù)的取值范圍將數(shù)據(jù)進行縮放,這樣滓技,兩個數(shù)據(jù)集就會落在同樣的比例中哩牍。

用下列代碼替換 getDJIFromFile 方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23defgetDJIFromFile(file:File):(Array[Date],Array[Double])={

valsource=scala.io.Source.fromFile(file)

//Get all the records (minus the header)

//獲取所有記錄(減去標頭)

valdata=source

.getLines()

.drop(1)

.map(x=>getDJIRecordFromString(x))

.toArray

source.close()

//turn the tuples into two separate arrays for easier use later on

//將這些元組分隔為兩個數(shù)組以方便之后使用

valsortedData=data.sortBy(x=>x._1)

valdates=sortedData.map(x=>x._1)

valmaxDouble=sortedData.maxBy(x=>x._2)._2

valminDouble=sortedData.minBy(x=>x._2)._2

valrangeValue=maxDouble-minDouble

valdoubles=sortedData.map(x=>x._2/rangeValue)

(dates,doubles)

}

用下列代碼替換 def top 方法中 plotData 的定義:

Scala

1

2

3

4

5

6valmaxDataValue=points.maxBy(x=>x(0))

valminDataValue=points.minBy(x=>x(0))

valrangeValue=maxDataValue(0)-minDataValue(0)

valplotData=points

.zipWithIndex

.map(x=>Array(x._2.toDouble,-x._1(0)/rangeValue))

現(xiàn)在我們看到,雖然 DJI 的取值范圍落在 0.8 與 1.8 之間令漂,而我們的新特征的取值范圍落在 -0.5 與 0.5 之間膝昆,但兩條曲線的趨勢符合得很好。學完這個實例叠必,加上段落中對 PCA 的說明荚孵,現(xiàn)在你應該學會了 PCA 并能把它應用到你自己的數(shù)據(jù)中。

應用支持向量機(SVM)

在我們實際開始應用支持向量機 (SVM) 之前纬朝,我會稍微介紹一下 SVM收叶。基本的 SVM 是一個二元分類器共苛,它通過挑選出一個代表數(shù)據(jù)點之間最大距離的超平面判没,將數(shù)據(jù)集分為兩部分。一個 SVM 就帶有一個所謂的“校正率”值俄讹。如果不存在理想分割哆致,則該校正率提供了一個誤差范圍,允許人們在該范圍內(nèi)找出一個仍盡可能合理分割的超平面患膛。因此摊阀,即使仍存在一些令人不快的點,在校正率規(guī)定的誤差范圍內(nèi)踪蹬,超平面也是合適的胞此。這意味著,我們無法為每種情形提出一個“標準的”校正率跃捣。不過漱牵,如果數(shù)據(jù)中沒有重疊部分,則較低的校正率要優(yōu)于較高的校正率疚漆。

我剛剛說明了作為一個二元分類器的基本 SVM酣胀,但是這些原理也適用于具有更多類別的情形刁赦。然而,現(xiàn)在我們要繼續(xù)完成具有 2 種類別的實例闻镶,因為僅說明這種情況已經(jīng)足夠了甚脉。

在本例中,我們將完成幾個小案例铆农,其中牺氨,支持向量機 (SVM) 的表現(xiàn)都勝過其他分離算法如 KNN。這種方法與前幾例中的不同墩剖,但它能幫你更容易學會怎么使用以及何時使用 SVM猴凹。

對于每個小案例,我們會提供代碼岭皂、圖像郊霎、不同參數(shù)時的 SVM 運行測試以及對測試結(jié)果的分析。這應該使你對輸入 SVM 算法的參數(shù)有所了解蒲障。

在第一個小案例中歹篓,我們將應用高斯核函數(shù)瘫证,不過在Smile庫中還有其他核函數(shù)揉阎。其他核函數(shù)可以在這里找到。緊接著高斯核函數(shù)背捌,我們將講述多項式核函數(shù)毙籽,因為這個核函數(shù)與前者有很大的不同。

我們會在每個小案例中用到下列的基本代碼毡庆,其中只有構(gòu)造函數(shù) filePaths 和 svm 隨每個小案例而改變坑赡。

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

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73objectSupportVectorMachineextendsSimpleSwingApplication{

deftop=newMainFrame{

title="SVM Examples"

//File path (this changes per example)

//文件路徑(隨案例而改變)

valtrainingPath="/users/.../Example Data/SVM_Example_1.csv"

valtestingPath="/users/.../Example Data/SVM_Example_1.csv"

//Loading of the test data and plot generation stays the same

//加載測試數(shù)據(jù),繪圖生成代碼保持相同

valtrainingData=getDataFromCSV(newFile(path))

valtestingData=getDataFromCSV(newFile(path))

valplot=ScatterPlot.plot(trainingData._1,

trainingData._2,

'@',

Array(Color.blue,Color.green)

)

peer.setContentPane(plot)

//Here we do our SVM fine tuning with possibly different kernels

//此處么抗,我們用可能的不同核函數(shù)對SVM進行微調(diào)

valsvm=newSVM[Array[Double]](newGaussianKernel(0.01),1.0,2)

svm.learn(trainingData._1,trainingData._2)

svm.finish()

//Calculate how well the SVM predicts on the training set

//計算SVM對測試集的預測效果

valpredictions=testingData

._1

.map(x=>svm.predict(x))

.zip(testingData._2)

valfalsePredictions=predictions

.map(x=>if(x._1==x._2)0else1)

println(falsePredictions.sum.toDouble/predictions.length

*100+" % false predicted")

size=newDimension(400,400)

}

defgetDataFromCSV(file:File):(Array[Array[Double]],Array[Int])={

valsource=scala.io.Source.fromFile(file)

valdata=source

.getLines()

.drop(1)

.map(x=>getDataFromString(x))

.toArray

source.close()

valdataPoints=data.map(x=>x._1)

valclassifierArray=data.map(x=>x._2)

return(dataPoints,classifierArray)

}

defgetDataFromString(dataString:String):(Array[Double],Int)={

//Split the comma separated value string into an array of strings

//把用逗號分隔的數(shù)值字符串分解為一個字符串數(shù)組

valdataArray:Array[String]=dataString.split(',')

//Extract the values from the strings

//從字符串中抽取數(shù)值

valcoordinates=Array(dataArray(0).toDouble,dataArray(1).toDouble)

valclassifier:Int=dataArray(2).toInt

//And return the result in a format that can later

//easily be used to feed to Smile

//并以一定格式返回結(jié)果毅否,使得該結(jié)果之后容易輸入到Smile中處理

return(coordinates,classifier)

}

案例1(高斯核函數(shù))

在本案例中,我們介紹了最常用的 SVM 核函數(shù)蝇刀,即高斯核函數(shù)螟加。我們的想法是幫助讀者尋找該核函數(shù)的最佳輸入?yún)?shù)。本例中用到的數(shù)據(jù)可以在這里下載吞琐。

從該圖中可以清楚看出捆探,線性回歸線在這里起不了作用。我們要使用一個 SVM 來作預測站粟。在給出的第一段代碼中黍图,高斯核函數(shù)的 sigma 值為 0.01,邊距懲罰系數(shù)為 1.0奴烙,類別總數(shù)為 2助被,并將其傳遞給了 SVM剖张。那么,這些都代表什么意思呢揩环?

我們從高斯核函數(shù)說起修械。這個核函數(shù)反映了 SVM 如何計算系統(tǒng)中成對數(shù)據(jù)的相似度。對于高斯核函數(shù)检盼,用到了歐氏距離中的方差肯污。我們特意挑選高斯核函數(shù)的原因是,數(shù)據(jù)中并不含有明顯的結(jié)構(gòu)如線性函數(shù)吨枉、多項式函數(shù)或者雙曲線函數(shù)蹦渣。相反地,數(shù)據(jù)聚集成了3組貌亭。

我們傳遞到高斯核中構(gòu)造函數(shù)的參數(shù)是 sigma柬唯。這個 sigma 值反映了核函數(shù)的平滑程度。我們會演示改變這一取值如何影響預測效果圃庭。我們將邊距懲罰系數(shù)取 1锄奢。這一參數(shù)定義了系統(tǒng)中向量的邊距,因此剧腻,這一值越小拘央,約束向量就越多。我們會執(zhí)行一組運行測試书在,通過結(jié)果向讀者說明這個參數(shù)在實踐中的作用灰伟。注意其中 s: 代表 sigma,c: 代表校正懲罰系數(shù)儒旬。百分數(shù)表示預測效果的誤差率栏账, 它只不過是訓練之后,對相同數(shù)據(jù)集的錯誤預測的百分數(shù)栈源。

不幸的是挡爵,并不存在為每個數(shù)據(jù)集尋找正確 sigma 的黃金法則。不過甚垦,可能最好的方法就是計算數(shù)據(jù)的 sigma 值茶鹃,即 √(variance),然后在這個值附近取值看看哪一個 sigma 值效果最好制轰。因為本例數(shù)據(jù)的方差在 0.2 與 0.5 之間前计,我們把這區(qū)間作為中心并在中心的兩邊都選取一些值,以比較我們的案例中使用高斯核的 SVM 的表現(xiàn)垃杖。

看看表格中的結(jié)果和錯誤預測的百分比男杈,它表明產(chǎn)生最佳效果的參數(shù)組合是一個非常低的 sigma (0.001) 和一個 1.0 及以上的校正率。不過调俘,如果把這個模型應用到實際中的新數(shù)據(jù)上伶棒,可能會產(chǎn)生過擬合旺垒。因此,在用模型本身的訓練數(shù)據(jù)測試模型時肤无,你應該保持謹慎先蒋。一個更好的方法是使用交叉驗證,或用新數(shù)據(jù)驗證宛渐。

案例2(多項式核函數(shù))

高斯核并不總是最佳選擇竞漾,盡管在應用 SVM 時,它是最常用的核函數(shù)窥翩。因此业岁,在本例中,我們將演示一個多項式核函數(shù)勝過高斯核函數(shù)的案例寇蚊。注意笔时,雖然本案例中的示例數(shù)據(jù)是構(gòu)建好的,但在本領域內(nèi)相似的數(shù)據(jù)(帶有一點噪聲)是可以找到的仗岸。本案例中的訓練數(shù)據(jù)可以在這里下載允耿,測試數(shù)據(jù)在這里下載。

對于本例數(shù)據(jù)扒怖,我們用一個三次多項式創(chuàng)建了兩個類別较锡,并生成了一個測試數(shù)據(jù)文件和一個訓練數(shù)據(jù)文件。訓練數(shù)據(jù)包含x軸上的前500個點姚垃,而測試數(shù)據(jù)則包含x軸上500到1000這些點念链。為了分析多項式核函數(shù)的工作原理,我們將數(shù)據(jù)匯成圖积糯。左圖是訓練數(shù)據(jù)的,右圖是測試數(shù)據(jù)的谦纱。

考慮到本實例開頭給出的基本代碼看成,我們作如下的替換:

Scala

1

2valtrainingPath="/users/.../Example Data/SVM_Example_2.csv"

valtestingPath="/users/.../Example Data/SVM_Example_2_Test_data.csv"

然后,如果我們使用高斯核并且運行代碼跨嘉,就可以得到如下結(jié)果:

可以看到川慌,即使是最佳情況,仍然有 27.4% 的測試數(shù)據(jù)被錯誤分類祠乃。這很有趣梦重,因為當我們觀察圖像時,可以看到兩個類別之間有一個很明顯的區(qū)分亮瓷。我們可以對 sigma 和校正率進行微調(diào)琴拧,但是當預測點很遠時(例如 x 是 100000),sigma 和校正率就會一直太高而使模型表現(xiàn)不佳(時間方面與預測效果方面)嘱支。

因此蚓胸,我們將高斯核替換為多項式核挣饥,代碼如下:

Scala

1

valsvm=newSVM[Array[Double]](newPolynomialKernel(2),1.0,2)

注意我們給多項式核的構(gòu)造函數(shù)傳遞 2 的方式。這個 2 代表它要擬合的函數(shù)的次數(shù)沛膳。如果我們不單考慮次數(shù)為 2 的情況扔枫,我們還考慮次數(shù)為2、3锹安、4短荐、5的情況,并且讓校正率再一次在 0.001 到 100 之間變化叹哭,則得到如下結(jié)果:

從中我們可以看到搓侄,次數(shù)為 3 和 5 的情況得到了100%的準確率,這兩種情況中測試數(shù)據(jù)與訓練數(shù)據(jù)之間沒有一個點是重疊的话速。與高斯核的最佳情況 27.4% 的錯誤率相比讶踪,這種表現(xiàn)令人驚喜。確實要注意本例這些數(shù)據(jù)是構(gòu)建好的泊交,因此沒有什么噪聲數(shù)據(jù)乳讥。所以才能出現(xiàn)所有的“校正率”都為 0% 錯誤率。如果添加了噪聲廓俭,則需要對校正率進行微調(diào)亿胸。

以上就是對支持向量機這一部分的總結(jié)。

結(jié)論

在了解了機器學習的整體思想之后揩懒,你應該可以辨別出哪些情況分別屬于分類問題佳窑、回歸問題或是維數(shù)約化問題。此外雹熬,你應該理解機器學習的基本概念宽菜,什么是模型,并且知道機器學習中的一些常見陷阱竿报。

在學完本文中的實例之后铅乡,你應該學會應用 K-NN、樸素貝葉斯算法以及線性回歸分析了烈菌。此外阵幸,你也能夠應用文本回歸、使用 PCA 合并特征以及應用支持向量機芽世。還有非常重要的一點挚赊,就是能夠建立你自己的推薦系統(tǒng)。

如果你有疑問或關(guān)于本文的反饋济瓢,請隨時通過Github荠割、LinkedInTwitter聯(lián)系我。

3贊18 收藏評論

關(guān)于作者:pine_pie

know yourself個人主頁·我的文章·14·

相關(guān)文章

9 個超酷的深度學習案例

谷歌大腦 2016 年機器學習的 9 大進展·1

普通程序員如何轉(zhuǎn)向人工智能方向葬荷?·1

輕松看懂機器學習十大常用算法·1

初學者如何從零學習人工智能涨共?看完你就懂了·3

可能感興趣的話題

鳳凰網(wǎng)2015校園招聘筆試:進出棧順序·1

關(guān)于C語言單雙精度的輸出問題·4

有個金牛座的PM是什么感覺–第四篇·6

4399游戲2015校園招聘筆試題:計算天數(shù)

一個純靜態(tài)的在線工具網(wǎng)站(meTools)·2

4399游戲2015校園招聘筆試題:哈夫曼樹·1

登錄后評論新用戶注冊

直接登錄

本周熱門文章

本月熱門文章

熱門標簽

0為什么該和程序員約會纽帖?我有 20 個...

1常坐電腦前面,這些健康問題你注意到...

2Github 對程序員職業(yè)生涯的影響

3我們有一個富鄰居举反,他叫施樂

4函數(shù)式編程從崛起到?jīng)]落到再崛起

5錯誤的抽象

6為什么說 LINQ 要勝過 SQL

7和程序員談戀愛懊直,是什么樣的體驗

8編寫好代碼:如何減少代碼的認知負荷

9沒有功能需求設計文檔?對不起火鼻,拒絕...

業(yè)界熱點資訊更多 ?

GitHub 引入 SHA-1 碰撞檢測

16 小時前 ·5

Linux Kernel 4.10.4 發(fā)布

1 天前 ·5

最流行的瀏覽器室囊?不用 Chrome 的 13 個理由

3 小時前 ·2

Android 8.0首波新特性曝光:UI、續(xù)航體驗飆升

1 天前 ·8 ·2

Linux 驅(qū)動庫曝光 AMD 新一代顯卡:Vega 和 Pola...

3 小時前 ·2

精選工具資源更多資源 ?

靜態(tài)代碼分析工具清單:公司篇

靜態(tài)代碼分析

HotswapAgent:支持無限次重定義運行時類與資源

開發(fā)流程增強工具

靜態(tài)代碼分析工具清單:開源篇(各語言)

靜態(tài)代碼分析·1

Bugsnag:跨平臺的錯誤監(jiān)測

開發(fā)庫

靜態(tài)代碼分析工具清單:開源篇(多語言)

靜態(tài)代碼分析

關(guān)于伯樂在線博客

在這個信息爆炸的時代魁索,人們已然被大量融撞、快速并且簡短的信息所包圍。然而粗蔚,我們相信:過多“快餐”式的閱讀只會令人“虛胖”尝偎,缺乏實質(zhì)的內(nèi)涵。伯樂在線內(nèi)容團隊正試圖以我們微薄的力量鹏控,把優(yōu)秀的原創(chuàng)文章和譯文分享給讀者致扯,為“快餐”添加一些“營養(yǎng)”元素。

快速鏈接

網(wǎng)站使用指南 ?

問題反饋與求助 ?

加入我們 ?

網(wǎng)站積分規(guī)則 ?

網(wǎng)站聲望規(guī)則 ?

關(guān)注我們

新浪微博:@伯樂在線官方微博

RSS:訂閱地址

推薦微信號

合作聯(lián)系

Email:bd@Jobbole.com

QQ: 2302462408 (加好友請注明來意)

更多頻道

小組– 好的話題当辐、有啟發(fā)的回復抖僵、值得信賴的圈子

頭條– 分享和發(fā)現(xiàn)有價值的內(nèi)容與觀點

相親– 為IT單身男女服務的征婚傳播平臺

資源– 優(yōu)秀的工具資源導航

翻譯– 翻譯傳播優(yōu)秀的外文文章

文章– 國內(nèi)外的精選文章

設計– UI,網(wǎng)頁,交互和用戶體驗

iOS– 專注iOS技術(shù)分享

安卓– 專注Android技術(shù)分享

前端– JavaScript, HTML5, CSS

Java– 專注Java技術(shù)分享

Python– 專注Python技術(shù)分享

? 2017 伯樂在線文章小組相親加入我們反饋

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缘揪,一起剝皮案震驚了整個濱河市耍群,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌找筝,老刑警劉巖蹈垢,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異呻征,居然都是意外死亡耘婚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進店門陆赋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嚷闭,你說我怎么就攤上這事攒岛。” “怎么了胞锰?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵灾锯,是天一觀的道長。 經(jīng)常有香客問我嗅榕,道長顺饮,這世上最難降的妖魔是什么吵聪? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮兼雄,結(jié)果婚禮上吟逝,老公的妹妹穿的比我還像新娘。我一直安慰自己赦肋,他們只是感情好块攒,可當我...
    茶點故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著佃乘,像睡著了一般囱井。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上趣避,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天庞呕,我揣著相機與錄音,去河邊找鬼程帕。 笑死住练,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的骆捧。 我是一名探鬼主播澎羞,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼敛苇!你這毒婦竟也來了妆绞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤枫攀,失蹤者是張志新(化名)和其女友劉穎括饶,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體来涨,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡图焰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蹦掐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片技羔。...
    茶點故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖卧抗,靈堂內(nèi)的尸體忽然破棺而出藤滥,到底是詐尸還是另有隱情,我是刑警寧澤社裆,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布拙绊,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏标沪。R本人自食惡果不足惜榄攀,卻給世界環(huán)境...
    茶點故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望金句。 院中可真熱鬧檩赢,春花似錦、人聲如沸趴梢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坞靶。三九已至憔狞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間彰阴,已是汗流浹背瘾敢。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尿这,地道東北人簇抵。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像射众,于是被迫代替她去往敵國和親碟摆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,700評論 2 345

推薦閱讀更多精彩內(nèi)容