原創(chuàng)|使用教程|編輯:鄭恭琳|2018-01-10 11:23:30.000|閱讀 745 次
概述:本文將帶您了解如何開(kāi)發(fā)和使用您自己的基于機(jī)器學(xué)習(xí)的電子郵件垃圾郵件分類系統(tǒng)。因?yàn)椋?誰(shuí)會(huì)喜歡垃圾郵件呢?
# 界面/圖表報(bào)表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
相關(guān)鏈接:
在這篇文章中,我們將開(kāi)發(fā)一個(gè)應(yīng)用程序來(lái)檢測(cè)垃圾郵件。將使用的算法是從SPARK MLib實(shí)現(xiàn)的邏輯回歸。對(duì)這個(gè)領(lǐng)域不需要深入的了解,因?yàn)檫@些主題是從高層次的角度來(lái)描述的。完整的工作代碼將與一個(gè)正在運(yùn)行的應(yīng)用程序一起提供,以供您選擇電子郵件的進(jìn)一步實(shí)驗(yàn)。
邏輯回歸是一種用于分類問(wèn)題的算法。在分類問(wèn)題中,我們給了很多標(biāo)簽化的數(shù)據(jù)(垃圾郵件,非垃圾郵件),當(dāng)一個(gè)新的例子來(lái)臨時(shí),我們想知道它屬于哪個(gè)類別。由于它是一種機(jī)器學(xué)習(xí)算法,Logistic回歸用標(biāo)記數(shù)據(jù)進(jìn)行訓(xùn)練,并基于訓(xùn)練給出了關(guān)于新的例子的預(yù)測(cè)。
一般來(lái)說(shuō),當(dāng)大量數(shù)據(jù)可用時(shí),我們需要檢測(cè)一個(gè)例子屬于哪個(gè)類別,可以使用邏輯回歸(即使結(jié)果并不總是令人滿意)。
例如,當(dāng)分析數(shù)百萬(wàn)患者的健康狀況以預(yù)測(cè)患者是否有心肌梗塞時(shí),可以使用邏輯回歸。同樣的邏輯可以用來(lái)預(yù)測(cè)患者是否會(huì)患上特定的癌癥,是否會(huì)受到抑郁癥等的影響。在這個(gè)應(yīng)用程序中,我們有相當(dāng)數(shù)量的數(shù)據(jù),所以邏輯回歸通常會(huì)給出很好的提示。
基于圖像密度的顏色,我們可以分類,比如說(shuō),圖像是否包含人或包含汽車。此外,由于這是一個(gè)分類問(wèn)題,我們也可能使用邏輯回歸來(lái)檢測(cè)圖片是否有字符,甚至是檢測(cè)手寫。
邏輯回歸最常見(jiàn)的應(yīng)用之一是分類垃圾郵件。在這個(gè)應(yīng)用程序中,算法確定傳入的電子郵件或消息是否是垃圾郵件。當(dāng)建立一個(gè)非個(gè)性化的算法時(shí),需要大量的數(shù)據(jù)。個(gè)性化過(guò)濾器通常表現(xiàn)更好,因?yàn)槔]件分類器在某種程度上取決于個(gè)人的興趣和背景。
我們有很多標(biāo)記的例子,并且想要訓(xùn)練我們的算法足夠聰明,可以說(shuō)出新的例子是否屬于其中一個(gè)類別。為了簡(jiǎn)化,我們將首先參考二進(jìn)制分類(1或0)。算法也容易擴(kuò)展到多分類。
通常情況下,我們有多維數(shù)據(jù)或具有許多特征的數(shù)據(jù)。這些功能中的每一個(gè)都以某種方式有助于最終決定新范例屬于哪個(gè)范疇。例如,在癌癥分類問(wèn)題中,我們可以具有年齡、吸煙與否、體重、身高、家族基因組等特征。這些功能中的每一個(gè)都有助于最終的類別決定。特征并不等于決定權(quán),而是在確定最終狀態(tài)時(shí)有不同的影響。例如,在癌癥預(yù)測(cè)中,體重比家族基因組的影響更小。在邏輯回歸中,這正是我們?cè)噲D找出的結(jié)果:數(shù)據(jù)特征的權(quán)重/影響。一旦我們有了大量的數(shù)據(jù)例子,我們就可以確定每個(gè)特征的權(quán)重,當(dāng)新的例子出現(xiàn)時(shí),我們使用權(quán)重來(lái)看看這個(gè)例子是如何分類的。在癌癥預(yù)測(cè)的例子中,我們可以這樣寫:
更正式地說(shuō):
n =例子的數(shù)量
k =特征的數(shù)量
θj=特征j的權(quán)重
Xji =具有特征j的第i個(gè)例子X
為了將數(shù)據(jù)分類,我們需要一個(gè)函數(shù)(假設(shè)),根據(jù)示例、值和特征,可以將數(shù)據(jù)放入兩個(gè)類別之一。我們使用的函數(shù)被稱為Sigmoid函數(shù),如下圖所示:
正如我們所看到的那樣,當(dāng)X軸上的值是正值時(shí),Sigmoid函數(shù)值往往趨于1;當(dāng)X軸上的值為負(fù)值時(shí),趨向于0。基本上,我們有一個(gè)模型來(lái)表示兩個(gè)類別和數(shù)學(xué),功能如下所示:
Z是在“Insight”下解釋的功能。
要獲得離散值(1或0),可以說(shuō)當(dāng)一個(gè)函數(shù)值(Y軸)大于0.5時(shí),我們將其歸類為1;當(dāng)函數(shù)值(Y軸)小于0.5時(shí),我們將其歸類為0。如下所述:
我們不希望僅僅找到任何權(quán)重,而是要求實(shí)際數(shù)據(jù)的最佳權(quán)重。為了找到最好的權(quán)重,我們需要另一個(gè)函數(shù)來(lái)計(jì)算我們找到的特定權(quán)重的解決方案。有了這個(gè)功能,我們可以比較不同解決方案與不同的權(quán)重,找到最好的一個(gè)。這個(gè)功能被稱為成本函數(shù)(Cost Function)。它將假設(shè)(Sigmoid)函數(shù)值與實(shí)際數(shù)據(jù)值進(jìn)行比較。由于我們用于培訓(xùn)的數(shù)據(jù)被標(biāo)記(垃圾郵件,非垃圾郵件),我們將假設(shè)(Sigmoid)預(yù)測(cè)與實(shí)際值進(jìn)行比較,我們知道這是肯定的。我們希望假設(shè)和實(shí)際價(jià)值之間的差距越小越好, 理想情況下,我們希望成本函數(shù)為零。更正式地說(shuō),成本函數(shù)被定義為:
其中yi是真正的價(jià)值/類別,如垃圾郵件/不是垃圾郵件或1/0,h(x)是假設(shè)。
基本上,這個(gè)公式計(jì)算我們的預(yù)測(cè)與實(shí)際標(biāo)記數(shù)據(jù)(y)的比較(平均)有多好。因?yàn)槲覀冇袃蓚€(gè)情況(1和0),所以我們有兩個(gè)Hs(假設(shè)):h1和h0。我們將log用于假設(shè),使得函數(shù)是凸的,找到全局最小值更安全。
我們來(lái)看看h1,這是與類別1的成本函數(shù)有關(guān)的假設(shè)。
我們將log用于我們的假設(shè),而不是直接使用它,因?yàn)槲覀兿M麑?shí)現(xiàn)一種關(guān)系,當(dāng)假設(shè)接近1時(shí),成本函數(shù)為零。請(qǐng)記住,我們希望我們的成本函數(shù)為零,以便在假設(shè)預(yù)測(cè)和標(biāo)記數(shù)據(jù)之間沒(méi)有差異。如果假設(shè)要預(yù)測(cè)0,我們的成本函數(shù)增長(zhǎng)很大,所以我們知道這不屬于第一類;如果假設(shè)要預(yù)測(cè)1,則成本函數(shù)變?yōu)?,表明該例子屬于類別1。
我們來(lái)看看h2,這是關(guān)于類別0的成本函數(shù)的假設(shè)。
在這種情況下,我們?cè)俅螒?yīng)用log,但是當(dāng)假設(shè)還要預(yù)測(cè)零時(shí),使成本函數(shù)變?yōu)榱恪H绻僭O(shè)要預(yù)測(cè)1,我們的成本函數(shù)就會(huì)變大,所以我們知道這不屬于0類;如果假設(shè)要預(yù)測(cè)0,則成本函數(shù)變?yōu)?,表示該例子屬于0類。
現(xiàn)在,我們有兩個(gè)成本函數(shù),我們需要把它們合并成一個(gè)。在這之后,等式變得有些雜亂,但原則上,這只是我們上面解釋的兩個(gè)成本函數(shù)的合并:
注意,第一項(xiàng)是h1的成本函數(shù),第二項(xiàng)是h0的成本函數(shù)。所以,如果y = 1,那么第二項(xiàng)被消除,如果y = 0,則第一項(xiàng)被消除。
正如我們上面看到的,我們希望我們的成本函數(shù)為零,以便我們的預(yù)測(cè)盡可能接近真實(shí)值(標(biāo)記)。幸運(yùn)的是,已經(jīng)有一個(gè)算法來(lái)最小化成本函數(shù):梯度下降(gradient descent)。一旦我們有成本函數(shù)(基本上將我們的假設(shè)與真實(shí)值相比較),我們可以把我們的權(quán)重(θ)同樣盡可能降低成本函數(shù)。首先,我們選擇θ的隨機(jī)值只是為了獲得一些值。然后,我們計(jì)算成本函數(shù)。根據(jù)結(jié)果,我們可以減少或增加我們的θ值,使成本函數(shù)優(yōu)化為零。我們重復(fù)這一點(diǎn),直到成本函數(shù)幾乎為零(0.0001),或從迭代到迭代沒(méi)有太大改善。
梯度下降原則上是這樣做的;它只是成本函數(shù)的一個(gè)導(dǎo)數(shù),以決定是減小還是增加θ值。它還使用系數(shù)α來(lái)定義改變θ值的數(shù)量。改變θ值太大(大α)會(huì)使梯度下降在優(yōu)化成本函數(shù)為零時(shí)失敗,因?yàn)榇蟮脑黾涌赡軙?huì)克服實(shí)際值或遠(yuǎn)離期望值。雖然θ(小α)的小變化意味著我們是安全的,但是算法需要大量的時(shí)間才能達(dá)到成本函數(shù)的最小值(幾乎為零),因?yàn)槲覀冋胍幕驅(qū)嶋H值進(jìn)展太慢(為更多的可視化解釋,請(qǐng)看這里)。更正式的,我們有:
右邊的項(xiàng)是成本函數(shù)的導(dǎo)數(shù)(僅針對(duì)特征k改變X的倍數(shù))。由于我們的數(shù)據(jù)是多維的(k個(gè)特征),我們對(duì)每個(gè)特征權(quán)重(θk)都做了這個(gè)。
讓我們看看準(zhǔn)備數(shù)據(jù)、轉(zhuǎn)換數(shù)據(jù)、執(zhí)行和結(jié)果。
在執(zhí)行數(shù)據(jù)之前,我們需要做一些數(shù)據(jù)預(yù)處理來(lái)清理不需要的信息。數(shù)據(jù)后處理的主要思想是從這個(gè)Coursera作業(yè)。我們做以下工作:
代碼實(shí)現(xiàn)將如下所示:
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; }
一旦電子郵件準(zhǔn)備好了,我們需要將數(shù)據(jù)轉(zhuǎn)換成算法理解的結(jié)構(gòu),如矩陣和特征。
第一步是建立一個(gè)“垃圾郵件詞匯(spam vocabulary)”,通過(guò)閱讀所有的垃圾郵件的詞匯和計(jì)數(shù)。例如,我們計(jì)算了使用“transaction”、“XMONEYX”、“finance”、“win”和“free”的次數(shù),然后拿出10個(gè)(featureSize)最常見(jiàn)的單詞,此時(shí)我們有地圖的大小為10(featureSize),其中的關(guān)鍵是單詞,值是從0到9.999的索引。這將作為可能的垃圾郵件詞的參考。請(qǐng)參閱下面的代碼:
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)計(jì)這些詞在我們的垃圾郵件和非垃圾郵件中的詞頻。然后,我們查看垃圾郵件詞匯表中的每個(gè)單詞,看它是否在那里。如果是(表示電子郵件有可能是垃圾郵件詞),我們把這個(gè)詞放在垃圾郵件詞匯表中包含的同一個(gè)索引中,并且把這個(gè)詞放在頻率上。最后,我們建立一個(gè)矩陣Nx10.000,其中N是所考慮的電子郵件的數(shù)量,10.000是包含電子郵件中的垃圾郵件詞匯映射詞的頻率的向量(如果在電子郵件中沒(méi)有發(fā)現(xiàn)垃圾郵件詞,我們?cè)O(shè)為0)。
例如,假設(shè)我們有如下的垃圾郵件詞匯表:
還有一個(gè)像下面這樣的電子郵件:
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
轉(zhuǎn)型后,我們將有:
0 2 0 1 1 1 0 0
所以我們有0 aa、2 how、0 abil、1 anyon、1 know、0 zero、0 zip。這是一個(gè)1X7的矩陣,因?yàn)槲覀冇幸粋€(gè)電子郵件和7個(gè)字的垃圾郵件詞匯。代碼如下所示:
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必須安裝在您的計(jì)算機(jī)上,但應(yīng)用程序可以在沒(méi)有任何Java知識(shí)的情況下下載和執(zhí)行。隨意用自己的電子郵件測(cè)試算法。
我們可以通過(guò)執(zhí)行RUN類來(lái)從源代碼運(yùn)行應(yīng)用程序。或者,如果您不想用IDE打開(kāi)它,只需運(yùn)行mvn clean install exec:java。
之后,你應(yīng)該看到這樣的情況:
首先,通過(guò)點(diǎn)擊使用Train with LR SGD或使用Train with LR LBFGS訓(xùn)練算法。這可能需要一到兩分鐘的時(shí)間。完成后,彈出窗口將顯示所達(dá)到的精度。不要擔(dān)心SGD與LBFGS的區(qū)別——它們只是使成本函數(shù)最小化的不同方法,并且會(huì)得到幾乎相同的結(jié)果。之后,將您選擇的電子郵件復(fù)制并粘貼到白色區(qū)域,然后按“Test”。之后,彈出窗口將顯示算法的預(yù)測(cè)。
在執(zhí)行過(guò)程中達(dá)到的精確度大約為97%,使用隨機(jī)80%的訓(xùn)練數(shù)據(jù)和20%的測(cè)試數(shù)據(jù)。沒(méi)有交叉驗(yàn)證測(cè)試——在這個(gè)例子中只使用了訓(xùn)練和測(cè)試(對(duì)于準(zhǔn)確性)集合。要了解有關(guān)劃分?jǐn)?shù)據(jù)的更多信息,請(qǐng)參閱此處。
訓(xùn)練算法的代碼相當(dāng)簡(jiǎn)單:
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()); }
就是這樣!
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請(qǐng)務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請(qǐng)郵件反饋至chenjj@fc6vip.cn