原創(chuàng)|使用教程|編輯:鄭恭琳|2018-01-10 11:23:30.000|閱讀 745 次
概述:本文將帶您了解如何開發(fā)和使用您自己的基于機器學習的電子郵件垃圾郵件分類系統(tǒng)。因為,誰會喜歡垃圾郵件呢?
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
相關鏈接:
在這篇文章中,我們將開發(fā)一個應用程序來檢測垃圾郵件。將使用的算法是從SPARK MLib實現的邏輯回歸。對這個領域不需要深入的了解,因為這些主題是從高層次的角度來描述的。完整的工作代碼將與一個正在運行的應用程序一起提供,以供您選擇電子郵件的進一步實驗。
邏輯回歸是一種用于分類問題的算法。在分類問題中,我們給了很多標簽化的數據(垃圾郵件,非垃圾郵件),當一個新的例子來臨時,我們想知道它屬于哪個類別。由于它是一種機器學習算法,Logistic回歸用標記數據進行訓練,并基于訓練給出了關于新的例子的預測。
一般來說,當大量數據可用時,我們需要檢測一個例子屬于哪個類別,可以使用邏輯回歸(即使結果并不總是令人滿意)。
例如,當分析數百萬患者的健康狀況以預測患者是否有心肌梗塞時,可以使用邏輯回歸。同樣的邏輯可以用來預測患者是否會患上特定的癌癥,是否會受到抑郁癥等的影響。在這個應用程序中,我們有相當數量的數據,所以邏輯回歸通常會給出很好的提示。
基于圖像密度的顏色,我們可以分類,比如說,圖像是否包含人或包含汽車。此外,由于這是一個分類問題,我們也可能使用邏輯回歸來檢測圖片是否有字符,甚至是檢測手寫。
邏輯回歸最常見的應用之一是分類垃圾郵件。在這個應用程序中,算法確定傳入的電子郵件或消息是否是垃圾郵件。當建立一個非個性化的算法時,需要大量的數據。個性化過濾器通常表現更好,因為垃圾郵件分類器在某種程度上取決于個人的興趣和背景。
我們有很多標記的例子,并且想要訓練我們的算法足夠聰明,可以說出新的例子是否屬于其中一個類別。為了簡化,我們將首先參考二進制分類(1或0)。算法也容易擴展到多分類。
通常情況下,我們有多維數據或具有許多特征的數據。這些功能中的每一個都以某種方式有助于最終決定新范例屬于哪個范疇。例如,在癌癥分類問題中,我們可以具有年齡、吸煙與否、體重、身高、家族基因組等特征。這些功能中的每一個都有助于最終的類別決定。特征并不等于決定權,而是在確定最終狀態(tài)時有不同的影響。例如,在癌癥預測中,體重比家族基因組的影響更小。在邏輯回歸中,這正是我們試圖找出的結果:數據特征的權重/影響。一旦我們有了大量的數據例子,我們就可以確定每個特征的權重,當新的例子出現時,我們使用權重來看看這個例子是如何分類的。在癌癥預測的例子中,我們可以這樣寫:
更正式地說:
n =例子的數量
k =特征的數量
θj=特征j的權重
Xji =具有特征j的第i個例子X
為了將數據分類,我們需要一個函數(假設),根據示例、值和特征,可以將數據放入兩個類別之一。我們使用的函數被稱為Sigmoid函數,如下圖所示:
正如我們所看到的那樣,當X軸上的值是正值時,Sigmoid函數值往往趨于1;當X軸上的值為負值時,趨向于0?;旧?,我們有一個模型來表示兩個類別和數學,功能如下所示:
Z是在“Insight”下解釋的功能。
要獲得離散值(1或0),可以說當一個函數值(Y軸)大于0.5時,我們將其歸類為1;當函數值(Y軸)小于0.5時,我們將其歸類為0。如下所述:
我們不希望僅僅找到任何權重,而是要求實際數據的最佳權重。為了找到最好的權重,我們需要另一個函數來計算我們找到的特定權重的解決方案。有了這個功能,我們可以比較不同解決方案與不同的權重,找到最好的一個。這個功能被稱為成本函數(Cost Function)。它將假設(Sigmoid)函數值與實際數據值進行比較。由于我們用于培訓的數據被標記(垃圾郵件,非垃圾郵件),我們將假設(Sigmoid)預測與實際值進行比較,我們知道這是肯定的。我們希望假設和實際價值之間的差距越小越好, 理想情況下,我們希望成本函數為零。更正式地說,成本函數被定義為:
其中yi是真正的價值/類別,如垃圾郵件/不是垃圾郵件或1/0,h(x)是假設。
基本上,這個公式計算我們的預測與實際標記數據(y)的比較(平均)有多好。因為我們有兩個情況(1和0),所以我們有兩個Hs(假設):h1和h0。我們將log用于假設,使得函數是凸的,找到全局最小值更安全。
我們來看看h1,這是與類別1的成本函數有關的假設。
我們將log用于我們的假設,而不是直接使用它,因為我們希望實現一種關系,當假設接近1時,成本函數為零。請記住,我們希望我們的成本函數為零,以便在假設預測和標記數據之間沒有差異。如果假設要預測0,我們的成本函數增長很大,所以我們知道這不屬于第一類;如果假設要預測1,則成本函數變?yōu)?,表明該例子屬于類別1。
我們來看看h2,這是關于類別0的成本函數的假設。
在這種情況下,我們再次應用log,但是當假設還要預測零時,使成本函數變?yōu)榱恪H绻僭O要預測1,我們的成本函數就會變大,所以我們知道這不屬于0類;如果假設要預測0,則成本函數變?yōu)?,表示該例子屬于0類。
現在,我們有兩個成本函數,我們需要把它們合并成一個。在這之后,等式變得有些雜亂,但原則上,這只是我們上面解釋的兩個成本函數的合并:
注意,第一項是h1的成本函數,第二項是h0的成本函數。所以,如果y = 1,那么第二項被消除,如果y = 0,則第一項被消除。
正如我們上面看到的,我們希望我們的成本函數為零,以便我們的預測盡可能接近真實值(標記)。幸運的是,已經有一個算法來最小化成本函數:梯度下降(gradient descent)。一旦我們有成本函數(基本上將我們的假設與真實值相比較),我們可以把我們的權重(θ)同樣盡可能降低成本函數。首先,我們選擇θ的隨機值只是為了獲得一些值。然后,我們計算成本函數。根據結果,我們可以減少或增加我們的θ值,使成本函數優(yōu)化為零。我們重復這一點,直到成本函數幾乎為零(0.0001),或從迭代到迭代沒有太大改善。
梯度下降原則上是這樣做的;它只是成本函數的一個導數,以決定是減小還是增加θ值。它還使用系數α來定義改變θ值的數量。改變θ值太大(大α)會使梯度下降在優(yōu)化成本函數為零時失敗,因為大的增加可能會克服實際值或遠離期望值。雖然θ(小α)的小變化意味著我們是安全的,但是算法需要大量的時間才能達到成本函數的最小值(幾乎為零),因為我們正朝著想要的或實際值進展太慢(為更多的可視化解釋,請看這里)。更正式的,我們有:
右邊的項是成本函數的導數(僅針對特征k改變X的倍數)。由于我們的數據是多維的(k個特征),我們對每個特征權重(θk)都做了這個。
讓我們看看準備數據、轉換數據、執(zhí)行和結果。
在執(zhí)行數據之前,我們需要做一些數據預處理來清理不需要的信息。數據后處理的主要思想是從這個Coursera作業(yè)。我們做以下工作:
代碼實現將如下所示:
private ListfilesToWords(String fileName) throws Exception { URI uri = this.getClass().getResource("/" + fileName).toURI(); Path start = getPath(uri); List< String > collect = Files.walk(start).parallel() .filter(Files::isRegularFile) .flatMap(file -> { try { return Stream.of(new String(Files.readAllBytes(file)).toLowerCase()); } catch (IOException e) { e.printStackTrace(); } return null; }).collect(Collectors.toList()); return collect.stream().parallel().flatMap(e -> tokenizeIntoWords(prepareEmail(e)).stream()).collect(Collectors.toList()); }
private String prepareEmail(String email) { int beginIndex = email.indexOf("\n\n"); String withoutHeader = email; if (beginIndex > 0) { withoutHeader = email.substring(beginIndex, email.length()); } String tagsRemoved = withoutHeader.replaceAll("< [^< >]+>", ""); String numberedReplaced = tagsRemoved.replaceAll("[0-9]+", "XNUMBERX "); String urlReplaced = numberedReplaced.replaceAll("(http|https)://[^\\s]*", "XURLX "); String emailReplaced = urlReplaced.replaceAll("[^\\s]+@[^\\s]+", "XEMAILX "); String dollarReplaced = emailReplaced.replaceAll("[$]+", "XMONEYX "); return dollarReplaced; }
private List< String > tokenizeIntoWords(String dollarReplaced) { String delim = "[' @$/#.-:&*+=[]?!(){},''\\\">_<;%'\t\n\r\f"; StringTokenizer stringTokenizer = new StringTokenizer(dollarReplaced, delim); List< String > wordsList = new ArrayList<>(); while (stringTokenizer.hasMoreElements()) { String word = (String) stringTokenizer.nextElement(); String nonAlphaNumericRemoved = word.replaceAll("[^a-zA-Z0-9]", ""); PorterStemmer stemmer = new PorterStemmer(); stemmer.setCurrent(nonAlphaNumericRemoved); stemmer.stem(); String stemmed = stemmer.getCurrent(); wordsList.add(stemmed); } return wordsList; }
一旦電子郵件準備好了,我們需要將數據轉換成算法理解的結構,如矩陣和特征。
第一步是建立一個“垃圾郵件詞匯(spam vocabulary)”,通過閱讀所有的垃圾郵件的詞匯和計數。例如,我們計算了使用“transaction”、“XMONEYX”、“finance”、“win”和“free”的次數,然后拿出10個(featureSize)最常見的單詞,此時我們有地圖的大小為10(featureSize),其中的關鍵是單詞,值是從0到9.999的索引。這將作為可能的垃圾郵件詞的參考。請參閱下面的代碼:
public Map< String, Integer > createVocabulary() throws Exception { String first = "allInOneSpamBase/spam"; String second = "allInOneSpamBase/spam_2"; List< String > collect1 = filesToWords(first); List< String > collect2 = filesToWords(second); ArrayList< String > all = new ArrayList<>(collect1); all.addAll(collect2); HashMap< String, Integer > countWords = countWords(all); List< Map.Entry< String, Integer >> sortedVocabulary = countWords.entrySet().stream().parallel().sorted((o1, o2) -> o2.getValue().compareTo(o1.getValue())).collect(Collectors.toList()); final int[] index = {0}; return sortedVocabulary.stream().limit(featureSIze).collect(Collectors.toMap(e -> e.getKey(), e -> index[0]++)); }
HashMap< String, Integer > countWords(Listall) { HashMap< String, Integer > countWords = new HashMap<>(); for (String s : all) { if (countWords.get(s) == null) { countWords.put(s, 1); } else { countWords.put(s, countWords.get(s) + 1); } } return countWords; }
下一步是統(tǒng)計這些詞在我們的垃圾郵件和非垃圾郵件中的詞頻。然后,我們查看垃圾郵件詞匯表中的每個單詞,看它是否在那里。如果是(表示電子郵件有可能是垃圾郵件詞),我們把這個詞放在垃圾郵件詞匯表中包含的同一個索引中,并且把這個詞放在頻率上。最后,我們建立一個矩陣Nx10.000,其中N是所考慮的電子郵件的數量,10.000是包含電子郵件中的垃圾郵件詞匯映射詞的頻率的向量(如果在電子郵件中沒有發(fā)現垃圾郵件詞,我們設為0)。
例如,假設我們有如下的垃圾郵件詞匯表:
還有一個像下面這樣的電子郵件:
anyon know how much it cost to host a web portal well it depend on how mani visitor your expect thi can be anywher from less than number buck a month to a coupl of dollarnumb you should checkout XURLX or perhap amazon ecnumb if your run someth big to unsubscrib yourself from thi mail list send an email to XEMAILX
轉型后,我們將有:
0 2 0 1 1 1 0 0
所以我們有0 aa、2 how、0 abil、1 anyon、1 know、0 zero、0 zip。這是一個1X7的矩陣,因為我們有一個電子郵件和7個字的垃圾郵件詞匯。代碼如下所示:
private Vector transformToFeatureVector(Email email, Map< String, Integer > vocabulary) { List< String > words = email.getWords(); HashMap< String, Integer > countWords = prepareData.countWords(words); double[] features = new double[featureSIze];//featureSIze==10.000 for (Map.Entry< String, Integer > word : countWords.entrySet()) { Integer index = vocabulary.get(word.getKey());//see if it is in //spam vocabulary if (index != null) { //put frequency the same index as the vocabulary features[index] = word.getValue(); } } return Vectors.dense(features); }
盡管Java必須安裝在您的計算機上,但應用程序可以在沒有任何Java知識的情況下下載和執(zhí)行。隨意用自己的電子郵件測試算法。
我們可以通過執(zhí)行RUN類來從源代碼運行應用程序?;蛘?,如果您不想用IDE打開它,只需運行mvn clean install exec:java。
之后,你應該看到這樣的情況:
首先,通過點擊使用Train with LR SGD或使用Train with LR LBFGS訓練算法。這可能需要一到兩分鐘的時間。完成后,彈出窗口將顯示所達到的精度。不要擔心SGD與LBFGS的區(qū)別——它們只是使成本函數最小化的不同方法,并且會得到幾乎相同的結果。之后,將您選擇的電子郵件復制并粘貼到白色區(qū)域,然后按“Test”。之后,彈出窗口將顯示算法的預測。
在執(zhí)行過程中達到的精確度大約為97%,使用隨機80%的訓練數據和20%的測試數據。沒有交叉驗證測試——在這個例子中只使用了訓練和測試(對于準確性)集合。要了解有關劃分數據的更多信息,請參閱此處。
訓練算法的代碼相當簡單:
public MulticlassMetrics execute() throws Exception { vocabulary = prepareData.createVocabulary(); List< LabeledPoint > labeledPoints = convertToLabelPoints(); sparkContext = createSparkContext(); JavaRDD< LabeledPoint > labeledPointJavaRDD = sparkContext.parallelize(labeledPoints); JavaRDD< LabeledPoint >[] splits = labeledPointJavaRDD.randomSplit(new double[]{0.8, 0.2}, 11L); JavaRDD< LabeledPoint > training = splits[0].cache(); JavaRDD< LabeledPoint > test = splits[1]; linearModel = model.run(training.rdd());//training with 80% data //testing with 20% data JavaRDD< Tuple2< Object, Object >> predictionAndLabels = test.map( (Function< LabeledPoint, Tuple2< Object, Object >>) p -> { Double prediction = linearModel.predict(p.features()); return new Tuple2<>(prediction, p.label()); } ); return new MulticlassMetrics(predictionAndLabels.rdd()); }
就是這樣!
本站文章除注明轉載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn