原文:
annas-archive.org/md5/39eecc62e023387ee8c22ca10d1a221a
譯者:飛龍
協議:CC BY-NC-SA 4.0
第十三章:我的名字是貝葉斯,樸素貝葉斯
“預測是非常困難的,尤其是當它涉及未來時”
-尼爾斯·玻爾
機器學習(ML)與大數據的結合是一種革命性的組合,它在學術界和工業界的研究領域產生了巨大的影響。此外,許多研究領域也開始涉及大數據,因為數據集以空前的方式從不同的來源和技術中生成和產生,通常被稱為數據洪流。這對機器學習、數據分析工具和算法提出了巨大的挑戰,需要從大數據的體量、速度和多樣性等標準中提取真正的價值。然而,從這些龐大的數據集中做出預測從未如此容易。
考慮到這個挑戰,在本章中我們將深入探討機器學習,并了解如何使用一種簡單而強大的方法來構建可擴展的分類模型,甚至更多。簡而言之,本章將涵蓋以下主題:
-
多項分類
-
貝葉斯推斷
-
樸素貝葉斯
-
決策樹
-
樸素貝葉斯與決策樹
多項分類
在機器學習中,多項式(也稱為多類)分類是將數據對象或實例分類為兩個以上的類別的任務,即擁有兩個以上的標簽或類別。將數據對象或實例分類為兩個類別稱為二元分類。從技術角度講,在多項分類中,每個訓練實例屬于 N 個不同類別中的一個,其中N >= 2
。目標是構建一個模型,能夠正確預測新實例屬于哪些類別。在許多場景中,數據點可能屬于多個類別。然而,如果一個給定的點屬于多個類別,這個問題可以簡化為一組不相關的二元問題,這些問題可以自然地使用二元分類算法解決。
讀者應避免將多類分類與多標簽分類混淆,在多標簽分類中,每個實例需要預測多個標簽。關于基于 Spark 的多標簽分類實現,感興趣的讀者可以參考spark.apache.org/docs/latest/mllib-evaluation-metrics.html#multilabel-classification
。
多類別分類技術可以分為以下幾類:
-
轉換為二進制
-
從二進制擴展
-
層次分類
轉換為二進制
使用二分類技術轉換,多類分類問題可以轉化為多個二分類問題的等效策略。換句話說,這種技術可以稱為 問題轉換技術。從理論和實踐的角度進行詳細討論超出了本章的范圍。因此,我們這里只討論一種問題轉換技術的例子,稱為 一對其余(OVTR)算法,作為該類別的代表。
使用一對其余方法進行分類
在本小節中,我們將描述使用 OVTR 算法執行多類分類的例子,方法是將問題轉化為多個等效的二分類問題。OVTR 策略將問題拆解并為每個類別訓練一個二分類器。換句話說,OVTR 分類器策略包括為每個類別擬合一個二分類器。然后,它將當前類別的所有樣本視為正樣本,因此其他分類器的樣本視為負樣本。
這無疑是一種模塊化的機器學習技術。然而,缺點是該策略需要來自多類家族的基礎分類器。原因是分類器必須輸出一個實值,也叫做 置信度分數,而不是預測實際標簽。該策略的第二個缺點是,如果數據集(即訓練集)包含離散的類標簽,最終可能導致模糊的預測結果。在這種情況下,單個樣本可能會被預測為多個類別。為了使前面的討論更清晰,下面我們來看一個例子。
假設我們有一組 50 個觀測值,分為三個類別。因此,我們將使用與之前相同的邏輯來選擇負例。對于訓練階段,我們設定如下:
-
分類器 1 有 30 個正例和 20 個負例
-
分類器 2 有 36 個正例和 14 個負例
-
分類器 3 有 14 個正例和 24 個負例
另一方面,對于測試階段,假設我有一個新實例,需要將其分類到之前的某一類別中。每個分類器當然都會產生一個關于估計的概率。這個估計是指該實例屬于分類器中正例或負例的概率有多低?在這種情況下,我們應始終比較一對其余中的正類概率。現在,對于 N 個類別,我們將為每個測試樣本獲得 N 個正類的概率估計。比較它們,最大概率對應的類別即為該樣本所屬類別。Spark 提供了通過 OVTR 算法將多類問題轉換為二分類問題,其中 邏輯回歸 算法被用作基礎分類器。
現在我們來看看另一個真實數據集的示例,演示 Spark 如何使用 OVTR 算法對所有特征進行分類。OVTR 分類器最終預測來自 光學字符識別 (OCR) 數據集的手寫字符。然而,在深入演示之前,我們先來探索一下 OCR 數據集,以了解數據的探索性特征。需要注意的是,當 OCR 軟件首次處理文檔時,它會將紙張或任何物體劃分為一個矩陣,使得網格中的每個單元格都包含一個字形(也稱為不同的圖形形狀),這僅僅是對字母、符號、數字或任何來自紙張或物體的上下文信息的詳細描述方式。
為了演示 OCR 流水線,假設文檔僅包含與 26 個大寫字母(即 A 到 Z)匹配的英文字母字符,我們將使用來自 UCI 機器學習數據庫 的 OCR 字母數據集。該數據集由 W* Frey* 和 D. J. Slate 提供。在探索數據集時,您應該會看到 20,000 個示例,包含 26 個英文字母的大寫字母。大寫字母通過 20 種不同的、隨機重塑和扭曲的黑白字體作為圖形呈現,具有不同的形狀。簡而言之,從 26 個字母中預測所有字符將問題本身轉化為一個具有 26 個類別的多類分類問題。因此,二分類器將無法達到我們的目的。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00362.gif圖 1: 一些打印字形(來源:使用荷蘭風格自適應分類器的字母識別,ML,第 6 卷,第 161-182 頁,W. Frey 和 D.J. Slate [1991])
上圖顯示了我之前解釋過的圖像。數據集 提供了經過這種方式扭曲的打印字形的示例,因此這些字母對計算機來說具有挑戰性,難以識別。然而,人類可以輕松識別這些字形。以下圖展示了前 20 行的統計屬性:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00020.jpeg圖 2: 數據集的快照,顯示為數據框
OCR 數據集的探索與準備
根據數據集描述,字形通過 OCR 閱讀器掃描到計算機上,然后它們會被自動轉換為像素。因此,所有 16 個統計屬性(見圖 2)也會記錄到計算機中。黑色像素在框的各個區域中的濃度提供了一種方法,可以通過 OCR 或經過訓練的機器學習算法來區分 26 個字母。
回想一下,支持向量機(SVM)、邏輯回歸、樸素貝葉斯分類器或任何其他分類器算法(連同它們的學習器)都要求所有特征都是數字格式。LIBSVM 允許你使用稀疏訓練數據集,以非傳統格式存儲數據。在將正常的訓練數據集轉換為 LIBSVM 格式時,只有數據集中的非零值才會以稀疏數組/矩陣的形式存儲。索引指定實例數據的列(特征索引)。然而,任何缺失的數據也會被視為零值。索引用于區分不同的特征/參數。例如,對于三個特征,索引 1、2 和 3 分別對應于 x、y 和 z 坐標。不同數據實例中相同索引值之間的對應關系僅在構造超平面時才是數學上的;這些值作為坐標。如果跳過了中間的任何索引,它應被默認賦值為零。
在大多數實際情況下,我們可能需要對所有特征點進行數據歸一化。簡而言之,我們需要將當前的制表符分隔 OCR 數據轉換為 LIBSVM 格式,以便簡化訓練步驟。因此,我假設你已經下載了數據并使用他們的腳本轉換為 LIBSVM 格式。轉換為 LIBSVM 格式后,數據集包含標簽和特征,如下圖所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00223.gif圖 3: LIBSVM 格式的 OCR 數據集 20 行快照
有興趣的讀者可以參考以下研究文章以深入了解:Chih-Chung Chang 和 Chih-Jen Lin,LIBSVM:一個支持向量機庫,ACM Intelligent Systems and Technology Transactions,2:27:1–27:27,2011。你還可以參考我在 GitHub 倉庫提供的公共腳本,地址是 github.com/rezacsedu/RandomForestSpark/
,該腳本可以將 CSV 中的 OCR 數據直接轉換為 LIBSVM 格式。我讀取了所有字母的數據,并為每個字母分配了唯一的數字值。你只需要顯示輸入和輸出文件路徑,并運行該腳本。
現在讓我們深入了解這個例子。我將演示的例子包含 11 個步驟,包括數據解析、Spark 會話創建、模型構建和模型評估。
步驟 1. 創建 Spark 會話 - 通過指定主節點 URL、Spark SQL 倉庫和應用名稱來創建一個 Spark 會話,如下所示:
val spark = SparkSession.builder.master("local[*]") //change acordingly.config("spark.sql.warehouse.dir", "/home/exp/").appName("OneVsRestExample") .getOrCreate()
步驟 2. 加載、解析和創建數據框 - 從 HDFS 或本地磁盤加載數據文件并創建數據框,最后顯示數據框的結構,如下所示:
val inputData = spark.read.format("libsvm").load("data/Letterdata_libsvm.data")
inputData.show()
步驟 3. 生成訓練集和測試集以訓練模型 - 讓我們通過將 70% 用于訓練,30% 用于測試來生成訓練集和測試集:
val Array(train, test) = inputData.randomSplit(Array(0.7, 0.3))
步驟 4. 實例化基礎分類器 - 在這里,基礎分類器充當多分類分類器。在本例中,它是邏輯回歸算法,可以通過指定最大迭代次數、容忍度、回歸參數和彈性網絡參數等參數進行實例化。
請注意,當因變量是二項(即二元)時,邏輯回歸是一種適合進行回歸分析的方法。像所有回歸分析一樣,邏輯回歸是一種預測分析。邏輯回歸用于描述數據并解釋一個二元因變量與一個或多個名義、順序、區間或比率水平自變量之間的關系。
對于基于 Spark 的邏輯回歸算法實現,有興趣的讀者可以參考spark.apache.org/docs/latest/mllib-linear-methods.html#logistic-regression
。
簡而言之,訓練邏輯回歸分類器時使用了以下參數:
-
MaxIter
:這指定最大迭代次數。通常,次數越多越好。 -
Tol
:這是停止標準的容忍度。通常,值越小越好,這有助于模型進行更密集的訓練。默認值為 1E-4。 -
FirIntercept
:表示你是否希望在生成概率解釋時攔截決策函數。 -
Standardization
:這表示一個布爾值,決定是否希望標準化訓練數據。 -
AggregationDepth
:越大越好。 -
RegParam
:這表示回歸參數。大多數情況下,值越小越好。 -
ElasticNetParam
:這表示更高級的回歸參數。大多數情況下,值越小越好。
然而,你可以根據問題類型和數據集特性指定擬合截距的Boolean
值為真或假:
val classifier = new LogisticRegression().setMaxIter(500) .setTol(1E-4) .setFitIntercept(true).setStandardization(true) .setAggregationDepth(50) .setRegParam(0.0001) .setElasticNetParam(0.01)
步驟 5. 實例化 OVTR 分類器 - 現在實例化一個 OVTR 分類器,將多分類問題轉化為多個二分類問題,如下所示:
val ovr = new OneVsRest().setClassifier(classifier)
這里classifier
是邏輯回歸估計器。現在是時候訓練模型了。
步驟 6. 訓練多分類模型 - 讓我們使用訓練集來訓練模型,如下所示:
val ovrModel = ovr.fit(train)
步驟 7. 在測試集上評估模型 - 我們可以使用轉換器(即ovrModel
)在測試數據上對模型進行評分,如下所示:
val predictions = ovrModel.transform(test)
步驟 8. 評估模型 - 在這一步,我們將預測第一列字符的標簽。但在此之前,我們需要實例化一個evaluator
來計算分類性能指標,如準確率、精確度、召回率和f1
值,具體如下:
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction")
val evaluator1 = evaluator.setMetricName("accuracy")
val evaluator2 = evaluator.setMetricName("weightedPrecision")
val evaluator3 = evaluator.setMetricName("weightedRecall")
val evaluator4 = evaluator.setMetricName("f1")
步驟 9. 計算性能指標 - 計算測試數據集上的分類準確率、精確度、召回率、f1
值和錯誤率,如下所示:
val accuracy = evaluator1.evaluate(predictions)
val precision = evaluator2.evaluate(predictions)
val recall = evaluator3.evaluate(predictions)
val f1 = evaluator4.evaluate(predictions)
步驟 10. 打印性能指標:
println("Accuracy = " + accuracy)
println("Precision = " + precision)
println("Recall = " + recall)
println("F1 = " + f1)
println(s"Test Error = ${1 - accuracy}")
你應該觀察如下值:
Accuracy = 0.5217246545696688
Precision = 0.488360500637862
Recall = 0.5217246545696688
F1 = 0.4695649096879411
Test Error = 0.47827534543033123
步驟 11. 停止 Spark 會話:
spark.stop() // Stop Spark session
通過這種方式,我們可以將一個多項式分類問題轉換為多個二分類問題,而不會犧牲問題類型。然而,從第 10 步開始,我們可以觀察到分類準確率并不理想。這可能是由多個原因造成的,例如我們用于訓練模型的數據集的性質。此外,更重要的是,在訓練邏輯回歸模型時我們并沒有調整超參數。而且,在進行轉換時,OVTR 不得不犧牲一些準確性。
層次分類
在層次分類任務中,分類問題可以通過將輸出空間劃分為樹的方式來解決。在這棵樹中,父節點被劃分為多個子節點。這個過程一直持續,直到每個子節點代表一個單一的類別。基于層次分類技術,已經提出了幾種方法。計算機視覺就是一個典型的例子,其中識別圖片或書寫文本是使用層次處理的應用。關于這種分類器的詳細討論超出了本章的范圍。
從二分類到多分類的擴展
這是一種將現有的二分類器擴展到多類分類問題的技術。為了解決多類分類問題,基于神經網絡、決策樹(DT)、隨機森林、k 近鄰、樸素貝葉斯和支持向量機(SVM)等算法已經提出并開發出來。在接下來的章節中,我們將討論樸素貝葉斯和決策樹算法,作為該類別的兩個代表。
現在,在使用樸素貝葉斯算法解決多類分類問題之前,讓我們在下一節中簡要回顧一下貝葉斯推理。
貝葉斯推理
在這一節中,我們將簡要討論貝葉斯推理(BI)及其基礎理論。讀者將從理論和計算的角度了解這一概念。
貝葉斯推理概述
貝葉斯推理是一種基于貝葉斯定理的統計方法。它用于更新假設的概率(作為強有力的統計證據),以便統計模型能夠不斷更新,朝著更準確的學習方向發展。換句話說,所有類型的不確定性都以統計概率的形式在貝葉斯推理方法中揭示出來。這是理論和數學統計中的一個重要技術。我們將在后續章節中廣泛討論貝葉斯定理。
此外,貝葉斯更新在數據集序列的增量學習和動態分析中占據主導地位。例如,時間序列分析、生物醫學數據分析中的基因組測序、科學、工程、哲學和法律等領域廣泛應用貝葉斯推理。從哲學視角和決策理論看,貝葉斯推理與預測概率密切相關。然而,這一理論更正式的名稱是貝葉斯概率。
什么是推理?
推理或模型評估是更新從模型得出的結局概率的過程。最終,所有的概率證據都將與當前的觀察結果對比,以便在使用貝葉斯模型進行分類分析時可以更新觀察結果。之后,這些信息通過對數據集中所有觀察結果的一致性實例化被傳回貝葉斯模型。傳送到模型的規則被稱為先驗概率,這些概率是在引用某些相關觀察結果之前評估的,特別是主觀地或者假設所有可能的結果具有相同的概率。然后,當所有證據都已知時,計算出信念,這就是后驗概率。這些后驗概率反映了基于更新證據計算的假設水平。
貝葉斯定理用于計算后驗概率,這些概率表示兩個前提的結果。基于這些前提,從統計模型中推導出先驗概率和似然函數,用于新數據的模型適應性。我們將在后續章節進一步討論貝葉斯定理。
它是如何工作的?
在這里,我們討論了一個統計推理問題的一般設置。首先,從數據中,我們估計所需的數量,可能也有一些我們希望估計的未知量。它可能只是一個響應變量或預測變量,一個類別,一個標簽,或僅僅是一個數字。如果你熟悉頻率派方法,你可能知道,在這種方法中,未知量,比如θ
,被假定為一個固定的(非隨機)量,應該通過觀察到的數據來估計。
然而,在貝葉斯框架中,未知量,比如θ
,被視為一個隨機變量。更具體地說,假設我們對θ
的分布有一個初步的猜測,這通常被稱為先驗分布。現在,在觀察到一些數據后,θ
的分布被更新。這個步驟通常是使用貝葉斯規則執行的(更多細節請參見下一節)。這就是為什么這種方法被稱為貝葉斯方法的原因。簡而言之,從先驗分布中,我們可以計算出對未來觀察結果的預測分布。
這個不起眼的過程可以通過大量論證證明是處理不確定推斷的合適方法。然而,保持一致性的是這些論證的理性原則。盡管有強有力的數學證據,許多機器學習從業者對于使用貝葉斯方法感到不適,甚至有些不情愿。其背后的原因是,他們通常認為選擇后驗概率或先驗概率是任意且主觀的;然而,實際上,這種選擇雖然主觀,但并非任意的。
不恰當地,許多貝葉斯學派的人并沒有真正用貝葉斯的思想來思考。因此,在文獻中可以找到許多偽貝葉斯方法,其中使用的模型和先驗并不能被嚴肅地視為先驗信念的表達。貝葉斯方法也可能會遇到計算困難。許多這些問題可以通過馬爾可夫鏈蒙特卡洛方法來解決,而這也是我的研究重點之一。隨著你閱讀本章,關于該方法的細節會更加清晰。
樸素貝葉斯
在機器學習中,樸素貝葉斯(NB)是基于著名的貝葉斯定理的概率分類器示例,其假設特征之間具有強獨立性。在本節中,我們將詳細討論樸素貝葉斯。
貝葉斯定理概覽
在概率論中,貝葉斯定理描述了基于與某一事件相關的先驗知識來計算事件發生的概率。這是一個由托馬斯·貝葉斯牧師最早提出的概率定理。換句話說,它可以被看作是理解概率理論如何被新的信息所影響的方式。例如,如果癌癥與年齡相關,關于年齡的信息可以被用來更準確地評估某人可能患癌的概率*。*
貝葉斯定理的數學表達式如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00241.gif
在前面的方程中,A 和 B 是事件,且滿足 P (B) ≠ 0,其他項可以描述如下:
-
P(A) 和 P(B) 是觀察到 A 和 B 的概率,彼此之間不考慮相關性(即獨立性)
-
P(A | B) 是在已知B為真時觀察事件A的條件概率
-
P(B| A) 是在已知 A 為真時觀察事件 B 的條件概率
正如你可能知道的那樣,一項著名的哈佛研究表明,只有 10%的幸福人是富有的。然而,你可能認為這個統計數字非常有說服力,但你也可能會有點興趣知道,富有的人中有多少人也是真正幸福的*。* 貝葉斯定理幫助你計算這個反向統計信息,方法是使用兩個額外的線索:
-
總體上,幸福的人的百分比,即P(A)。
-
總體上,富有的人的百分比,即P(B)。
貝葉斯定理的關鍵思想是反轉統計,考慮整體比例**。** 假設以下信息作為先驗是已知的:
-
40% 的人是快樂的,=> P(A)。
-
5% 的人是富人 => P(B).
現在讓我們假設哈佛的研究是正確的,即 P(B|A) = 10%。那么富人中快樂的比例,也就是 P(A | B), 可以按如下方式計算:
P(A|B) = {P(A) P(B| A)}/ P(B) = (40%10%)/5% = 80%
結果是,大多數人也是快樂的!很好。為了更清楚地說明,現在假設世界人口為 1000,為了簡化計算。然后,根據我們的計算,存在以下兩個事實:
-
事實 1:這告訴我們 400 人是快樂的,而哈佛的研究表明,這些快樂的人中有 40 個人也是富人。
-
事實 2:共有 50 個富人,因此其中快樂的比例是 40/50 = 80%。
這證明了貝葉斯定理及其有效性。然而,更全面的示例可以在 onlinecourses.science.psu.edu/stat414/node/43
找到。
我的名字是 Bayes,Naive Bayes。
我是 Bayes,Naive Bayes(NB)。我是一個成功的分類器,基于 最大后驗(MAP)的原理。作為分類器,我具有高度的可擴展性,需要的參數數目與學習問題中的變量(特征/預測因子)數量成線性關系。我有幾個特點,例如,我計算更快,如果你雇傭我來分類,我很容易實現,并且我可以很好地處理高維數據集。此外,我可以處理數據集中的缺失值。盡管如此,我是可適應的,因為模型可以在不重新構建的情況下用新的訓練數據進行修改。
在貝葉斯統計中,MAP 估計是未知量的估計,它等于后驗分布的眾數。MAP 估計可以用于基于經驗數據獲取一個未觀察量的點估計。
聽起來像詹姆斯·邦德的電影情節嗎?嗯,你/我們可以將分類器看作是 007 特工,對吧?開玩笑的。我相信我并不像 Naive Bayes 分類器的參數,因為它們像先驗和條件概率一樣,是通過一套確定的步驟來學習或決定的:這涉及到兩個非常簡單的操作,這在現代計算機上可以非常快速地執行,即計數和除法。沒有迭代。沒有周期。沒有代價方程的優化(代價方程通常比較復雜,平均而言至少是三次方或二次方復雜度)。沒有誤差反向傳播。沒有涉及解矩陣方程的操作。這些使得 Naive Bayes 及其整體訓練更加高效。
然而,在雇用此代理之前,您/我們可以發現它的優缺點,以便我們只利用它的優勢像王牌一樣使用它。好了,這里有一張表格總結了這個代理的優缺點:
代理 | 優點 | 缺點 | 更適合 |
---|---|---|---|
Naive Bayes (NB) | - 計算速度快 - 實現簡單 - 對高維度數據效果好 - 可以處理缺失值 - 訓練模型所需數據量小 - 可擴展 - 可適應性強,因為可以通過新增訓練數據來修改模型,而無需重新構建模型 | - 依賴于獨立性假設,因此如果假設不成立,表現會差 - 準確率較低 - 如果某個類標簽和某個特征值一起沒有出現,那么基于頻率的概率估計會是零 | - 當數據中有大量缺失值時 - 當特征之間的依賴關系相似時 - 垃圾郵件過濾和分類 - 對新聞文章進行分類(如技術、政治、體育等) - 文本挖掘 |
表格 1: Naive Bayes 算法的優缺點
使用 NB 構建可擴展分類器
在本節中,我們將通過一步一步的示例來展示如何使用 Naive Bayes(NB)算法。正如前面所述,NB 具有很強的可擴展性,所需的參數數量與學習問題中變量(特征/預測因子)的數量成線性關系。這種可擴展性使得 Spark 社區能夠使用該算法在大規模數據集上進行預測分析。Spark MLlib 中當前的 NB 實現支持多項式 NB 和 Bernoulli NB。
如果特征向量是二進制的,Bernoulli NB 是非常有用的。一個應用場景是使用詞袋(BOW)方法進行文本分類。另一方面,多項式 NB 通常用于離散計數。例如,如果我們有一個文本分類問題,我們可以將 Bernoulli 試驗的思想進一步拓展,在文檔中使用頻率計數,而不是詞袋(BOW)。
在本節中,我們將展示如何通過結合 Spark 機器學習 API(包括 Spark MLlib、Spark ML 和 Spark SQL)來預測 基于筆跡的手寫數字識別 數據集中的數字:
步驟 1. 數據收集、預處理和探索 - 手寫數字的基于筆的識別數據集是從 UCI 機器學習庫下載的,網址為 www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/pendigits.
。該數據集是在從 44 位書寫者收集了大約 250 個數字樣本后生成的,樣本與筆的位置在每隔 100 毫秒的固定時間間隔內進行相關。每個數字都被寫在一個 500 x 500 像素的框內。最后,這些圖像被縮放到 0 到 100 之間的整數值,以在每個觀測值之間創建一致的縮放。使用了一種著名的空間重采樣技術,以獲得沿弧軌跡上均勻間隔的 3 個和 8 個點。可以通過根據(x, y)坐標繪制 3 個或 8 個采樣點來可視化一個示例圖像以及從點到點的連線;它看起來像下表所示:
集 | ‘0’ | ‘1’ | ‘2’ | ‘3’ | ‘4’ | ‘5’ | ‘6’ | ‘7’ | ‘8’ | ‘9’ | 總計 |
---|---|---|---|---|---|---|---|---|---|---|---|
訓練集 | 780 | 779 | 780 | 719 | 780 | 720 | 720 | 778 | 718 | 719 | 7493 |
測試 | 363 | 364 | 364 | 336 | 364 | 335 | 336 | 364 | 335 | 336 | 3497 |
表 2:用于訓練集和測試集的數字數量
如前表所示,訓練集包含 30 位書寫者書寫的樣本,而測試集包含 14 位書寫者書寫的樣本。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00130.jpeg
圖 4:數字 3 和 8 的示例
關于該數據集的更多信息可以在 archive.ics.uci.edu/ml/machine-learning-databases/pendigits/pendigits-orig.names
上找到。數據集的一個樣本快照的數字表示如下圖所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00149.gif
圖 5:手寫數字數據集的 20 行快照
現在,為了使用獨立變量(即特征)預測因變量(即標簽),我們需要訓練一個多類分類器,因為如前所述,數據集現在有九個類別,即九個手寫數字。為了預測,我們將使用樸素貝葉斯分類器并評估模型的性能。
步驟 2. 加載所需的庫和包:
import org.apache.spark.ml.classification.NaiveBayes
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
import org.apache.spark.sql.SparkSession
步驟 3. 創建一個活躍的 Spark 會話:
val spark = SparkSession.builder.master("local[*]").config("spark.sql.warehouse.dir", "/home/exp/").appName(s"NaiveBayes").getOrCreate()
請注意,這里已將主機 URL 設置為 local[*]
,這意味著您機器的所有核心將用于處理 Spark 任務。您應根據需求相應地設置 SQL 倉庫以及其他配置參數。
步驟 4. 創建 DataFrame - 將存儲在 LIBSVM 格式中的數據加載為 DataFrame:
val data = spark.read.format("libsvm").load("data/pendigits.data")
對于數字分類,輸入特征向量通常是稀疏的,應當提供稀疏向量作為輸入,以利用稀疏性。由于訓練數據只使用一次,而且數據集的大小相對較小(即只有幾 MB),如果多次使用 DataFrame,我們可以對其進行緩存。
步驟 5. 準備訓練集和測試集 - 將數據拆分為訓練集和測試集(25% 用于測試):
val Array(trainingData, testData) = data.randomSplit(Array(0.75, 0.25), seed = 12345L)
步驟 6. 訓練樸素貝葉斯模型 - 使用訓練集訓練樸素貝葉斯模型,方法如下:
val nb = new NaiveBayes()
val model = nb.fit(trainingData)
步驟 7. 計算測試集上的預測結果 - 使用模型轉換器計算預測結果,并最終顯示每個標簽的預測結果,方法如下:
val predictions = model.transform(testData)
predictions.show()
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00189.jpeg圖 6: 每個標簽(即每個數字)的預測結果
正如你在前面的圖中看到的,一些標簽的預測是準確的,而另一些則是錯誤的。我們需要了解加權準確率、精確率、召回率和 F1 值,而不是單純地評估模型。
步驟 8. 評估模型 - 選擇預測結果和真實標簽,計算測試誤差和分類性能指標,如準確率、精確率、召回率和 F1 值,方法如下:
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction")
val evaluator1 = evaluator.setMetricName("accuracy")
val evaluator2 = evaluator.setMetricName("weightedPrecision")
val evaluator3 = evaluator.setMetricName("weightedRecall")
val evaluator4 = evaluator.setMetricName("f1")
步驟 9. 計算性能指標 - 計算分類準確率、精確率、召回率、F1 值和測試數據上的誤差,方法如下:
val accuracy = evaluator1.evaluate(predictions)
val precision = evaluator2.evaluate(predictions)
val recall = evaluator3.evaluate(predictions)
val f1 = evaluator4.evaluate(predictions)
步驟 10. 打印性能指標:
println("Accuracy = " + accuracy)
println("Precision = " + precision)
println("Recall = " + recall)
println("F1 = " + f1)
println(s"Test Error = ${1 - accuracy}")
你應該觀察到如下值:
Accuracy = 0.8284365162644282
Precision = 0.8361211320692463
Recall = 0.828436516264428
F1 = 0.8271828540349192
Test Error = 0.17156348373557184
性能并不差。然而,你仍然可以通過進行超參數調優來提高分類準確率。通過交叉驗證和訓練集拆分選擇合適的算法(即分類器或回歸器),仍然有機會進一步提高預測準確率,這將在下一節中討論。
調整我的參數!
你已經知道我的優缺點,我有一個缺點,那就是我的分類準確率相對較低。不過,如果你對我進行調優,我可以表現得更好。嗯,我們應該相信樸素貝葉斯嗎?如果相信,難道我們不該看看如何提高這個模型的預測性能嗎?我們以 WebSpam 數據集為例。首先,我們應該觀察 NB 模型的性能,然后看看如何通過交叉驗證技術提升性能。
從www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary/webspam_wc_normalized_trigram.svm.bz2
下載的 WebSpam 數據集包含特征和相應的標簽,即垃圾郵件或正常郵件。因此,這是一個監督學習問題,任務是預測給定消息是否為垃圾郵件或正常郵件(即非垃圾郵件)。原始數據集大小為 23.5 GB,類別標簽為+1 或-1(即二元分類問題)。后來,由于樸素貝葉斯不允許使用有符號整數,我們將-1 替換為 0.0,+1 替換為 1.0。修改后的數據集如下圖所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00054.gif圖 7: WebSpam 數據集的前 20 行快照
首先,我們需要按以下方式導入必要的包:
import org.apache.spark.ml.classification.NaiveBayes
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
import org.apache.spark.sql.SparkSession
import org.apache.spark.ml.Pipeline;
import org.apache.spark.ml.PipelineStage;
import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.feature.{HashingTF, Tokenizer}
import org.apache.spark.ml.linalg.Vector
import org.apache.spark.ml.tuning.{CrossValidator, ParamGridBuilder}
現在按以下方式創建 Spark 會話作為代碼的入口點:
val spark = SparkSession.builder.master("local[*]").config("spark.sql.warehouse.dir", "/home/exp/").appName("Tuned NaiveBayes").getOrCreate()
讓我們加載 WebSpam 數據集并準備訓練集以訓練樸素貝葉斯模型,如下所示:
// Load the data stored in LIBSVM format as a DataFrame.val data = spark.read.format("libsvm").load("hdfs://data/ webspam_wc_normalized_trigram.svm")// Split the data into training and test sets (30% held out for testing)val Array(trainingData, testData) = data.randomSplit(Array(0.75, 0.25), seed = 12345L)// Train a NaiveBayes model with using the training setval nb = new NaiveBayes().setSmoothing(0.00001)val model = nb.fit(trainingData)
在上述代碼中,為了可復現性,設置種子是必需的。現在讓我們對驗證集進行預測,步驟如下:
val predictions = model.transform(testData)
predictions.show()
現在讓我們獲取evaluator
并計算分類性能指標,如準確度、精確度、召回率和f1
度量,如下所示:
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction")
val evaluator1 = evaluator.setMetricName("accuracy")
val evaluator2 = evaluator.setMetricName("weightedPrecision")
val evaluator3 = evaluator.setMetricName("weightedRecall")
val evaluator4 = evaluator.setMetricName("f1")
現在讓我們計算并打印性能指標:
val accuracy = evaluator1.evaluate(predictions)
val precision = evaluator2.evaluate(predictions)
val recall = evaluator3.evaluate(predictions)
val f1 = evaluator4.evaluate(predictions)
// Print the performance metrics
println("Accuracy = " + accuracy)
println("Precision = " + precision)
println("Recall = " + recall)
println("F1 = " + f1)
println(s"Test Error = ${1 - accuracy}")
您應該收到以下輸出:
Accuracy = 0.8839357429715676
Precision = 0.86393574297188752
Recall = 0.8739357429718876
F1 = 0.8739357429718876
Test Error = 0.11606425702843237
雖然準確度已經達到了令人滿意的水平,但我們可以通過應用交叉驗證技術進一步提高它。該技術的步驟如下:
-
通過鏈式連接一個 NB 估計器作為管道的唯一階段來創建一個流水線
-
現在為調整準備參數網格
-
執行 10 折交叉驗證
-
現在使用訓練集擬合模型
-
計算驗證集上的預測
諸如交叉驗證之類的模型調整技術的第一步是創建管道。通過鏈式連接轉換器、估計器和相關參數可以創建管道。
步驟 1. 管道創建 - 讓我們創建一個樸素貝葉斯估計器(在以下情況下,nb
是一個估計器)并通過鏈式連接估計器創建一個管道:
val nb = new NaiveBayes().setSmoothing(00001)
val pipeline = new Pipeline().setStages(Array(nb))
一個管道可以被看作是用于訓練和預測的數據工作流系統。ML 管道提供了一組統一的高級 API,構建在DataFrames之上,幫助用戶創建和調優實用的機器學習管道。DataFrame、轉換器、估計器、管道和參數是管道創建中最重要的五個組件。有興趣的讀者可以參考spark.apache.org/docs/latest/ml-pipeline.html
了解更多關于管道的信息。
在前面的情況下,我們的管道中唯一的階段是一個用于在 DataFrame 上擬合以生成轉換器以確保成功訓練的算法估計器。
步驟 2. 創建網格參數 - 讓我們使用 ParamGridBuilder
構建一個參數網格以進行搜索:
val paramGrid = new ParamGridBuilder().addGrid(nb.smoothing, Array(0.001, 0.0001)).build()
步驟 3. 執行 10 折交叉驗證 - 現在我們將管道視為一個估計器,并將其包裝在交叉驗證器實例中。這將允許我們為所有管道階段共同選擇參數。CrossValidator
需要一個估計器、一組估計器 ParamMaps
和一個評估器。請注意,這里的評估器是 BinaryClassificationEvaluator
,其默認指標是 areaUnderROC
。但是,如果你使用 MultiClassClassificationEvaluator
作為評估器,你還可以使用其他性能指標:
val cv = new CrossValidator().setEstimator(pipeline).setEvaluator(new BinaryClassificationEvaluator).setEstimatorParamMaps(paramGrid).setNumFolds(10) // Use 3+ in practice
步驟 4. 使用訓練集擬合交叉驗證模型,如下所示:
val model = cv.fit(trainingData)
步驟 5. 如下計算性能:
val predictions = model.transform(validationData)
predictions.show()
步驟 6. 獲取評估器,計算性能指標,并顯示結果。現在讓我們獲取evaluator
并計算分類性能指標,如準確率、精度、召回率和 F1 值。這里將使用 MultiClassClassificationEvaluator
來計算準確率、精度、召回率和 F1 值:
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction")
val evaluator1 = evaluator.setMetricName("accuracy")
val evaluator2 = evaluator.setMetricName("weightedPrecision")
val evaluator3 = evaluator.setMetricName("weightedRecall")
val evaluator4 = evaluator.setMetricName("f1")
現在計算分類準確率、精度、召回率、F1 值和測試數據上的誤差,如下所示:
val accuracy = evaluator1.evaluate(predictions)
val precision = evaluator2.evaluate(predictions)
val recall = evaluator3.evaluate(predictions)
val f1 = evaluator4.evaluate(predictions)
現在讓我們打印性能指標:
println("Accuracy = " + accuracy)
println("Precision = " + precision)
println("Recall = " + recall)
println("F1 = " + f1)
println(s"Test Error = ${1 - accuracy}")
你現在應該會收到以下結果:
Accuracy = 0.9678714859437751
Precision = 0.9686742518830365
Recall = 0.9678714859437751
F1 = 0.9676697179934564
Test Error = 0.032128514056224855
現在與之前的結果相比,效果好多了,對吧?請注意,由于數據集的隨機拆分和你使用的平臺,你可能會得到稍有不同的結果。
決策樹
在這一部分,我們將詳細討論決策樹算法。還將討論樸素貝葉斯和決策樹的比較分析。決策樹通常被認為是一種監督學習技術,用于解決分類和回歸任務。決策樹只是一個決策支持工具,使用類似樹狀的圖(或決策模型)及其可能的結果,包括機會事件結果、資源成本和效用。從技術角度講,決策樹中的每一分支代表一個可能的決策、事件或反應,具體體現在統計概率上。
與樸素貝葉斯相比,決策樹(DT)是一種更為強大的分類技術。其原因在于,決策樹首先將特征劃分為訓練集和測試集。然后它會生成一個良好的泛化模型,以推斷預測標簽或類別。更有趣的是,決策樹算法可以處理二分類和多分類問題。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00081.jpeg圖 8: 使用 R 的 Rattle 包在入學測試數據集上生成的決策樹示例
舉例來說,在前面的示例圖中,DT 通過錄取數據學習,用一組if...else
決策規則來逼近正弦曲線。數據集包含每個申請入學的學生的記錄,假設是申請美國大學的學生。每條記錄包括研究生入學考試分數、CGPA 分數和列的排名。現在,我們需要根據這三個特征(變量)預測誰是合格的。DT 可以用于解決這種問題,在訓練 DT 模型并修剪掉不需要的樹枝后進行預測。通常,樹越深,意味著決策規則越復雜,模型的擬合度越好。因此,樹越深,決策規則越復雜,模型越擬合。
如果你想繪制前面的圖形,只需運行我的 R 腳本,在 RStudio 中執行它,并提供錄取數據集。腳本和數據集可以在我的 GitHub 倉庫中找到:github.com/rezacsedu/AdmissionUsingDecisionTree
。
使用 DT 的優缺點
在雇傭我之前,你可以從表 3 中了解我的優缺點以及我最擅長的工作時間,以免你事后后悔!
代理 | 優點 | 缺點 | 更擅長于 |
---|---|---|---|
決策樹(DTs) | - 實現、訓練和解釋簡單 - 樹形結構可視化 - 數據準備要求較少 - 較少的模型構建和預測時間 - 能處理數值型和類別型數據 - 可通過統計檢驗驗證模型 - 對噪聲和缺失值具有魯棒性 - 高準確率 | - 大型和復雜的樹難以解釋 - 同一子樹中可能出現重復 - 可能存在對角線決策邊界問題 - DT 學習器可能創建過于復雜的樹,無法很好地泛化數據 - 有時 DTs 可能因數據的小變動而不穩定 - 學習 DT 本身是一個 NP 完全問題(即非確定性多項式時間完全問題) - 如果某些類別占主導地位,DT 學習器會創建有偏的樹 | - 目標是高度準確的分類 - 醫學診斷和預后 - 信用風險分析 |
表 3: 決策樹的優缺點
決策樹與樸素貝葉斯比較
如前表所述,DT 由于其對訓練數據集的靈活性,非常易于理解和調試。它們既適用于分類問題,也適用于回歸問題。
如果你嘗試預測分類值或連續值,決策樹(DT)可以同時處理這兩種問題。因此,如果你只有表格數據,將其輸入到決策樹中,它將構建模型來分類你的數據,無需任何額外的前期或手動干預。總之,決策樹非常簡單,易于實現、訓練和解釋。只需極少的數據準備,決策樹就能在更短的預測時間內構建模型。正如前面所說,它們可以處理數字數據和分類數據,并且對噪聲和缺失值非常魯棒。使用統計測試驗證模型也非常簡單。更有趣的是,構建的樹可以進行可視化。總體而言,決策樹提供了非常高的準確性。
然而,決策樹的缺點是,它們有時會導致訓練數據的過擬合問題。這意味著你通常需要修剪樹并找到一個最優的樹模型,以提高分類或回歸的準確性。此外,同一子樹中可能會出現重復現象。有時它還會在決策邊界上產生斜對角問題,從而導致過擬合和欠擬合的問題。此外,決策樹學習器可能會生成過于復雜的樹,無法很好地泛化數據,這使得整體解釋變得困難。由于數據中的微小變動,決策樹可能不穩定,因此學習決策樹本身是一個 NP 完全問題。最后,如果某些類別在數據中占主導地位,決策樹學習器可能會生成有偏的樹。
讀者可以參考表 1和表 3,獲取樸素貝葉斯和決策樹的對比總結。
另一方面,使用樸素貝葉斯時有一種說法:NB 要求你手動構建分類器。你無法直接將一堆表格數據輸入它,它不會自動挑選最適合分類的特征。在這種情況下,選擇正確的特征以及重要的特征由用戶自己決定,也就是你自己。另一方面,決策樹會從表格數據中選擇最佳特征。鑒于這一點,你可能需要將樸素貝葉斯與其他統計技術結合,以幫助選擇最佳特征并在之后進行分類。或者,使用決策樹來提高準確性,特別是在精確度、召回率和 F1 度量方面。另一個關于樸素貝葉斯的優點是,它會作為一個連續的分類器進行輸出。然而,缺點是它們更難調試和理解。樸素貝葉斯在訓練數據中沒有良好的特征且數據量較小時表現得相當不錯。
總之,如果你試圖從這兩種方法中選擇一個更好的分類器,通常最好測試每個模型來解決問題。我的建議是,使用你擁有的訓練數據構建決策樹和樸素貝葉斯分類器,然后使用可用的性能指標比較它們的表現,再根據數據集的特性決定哪一個最適合解決你的問題。
使用決策樹算法構建可擴展的分類器
正如你已經看到的,使用 OVTR 分類器時,我們在 OCR 數據集上觀察到了以下性能指標值:
Accuracy = 0.5217246545696688
Precision = 0.488360500637862
Recall = 0.5217246545696688
F1 = 0.4695649096879411
Test Error = 0.47827534543033123
這意味著模型在該數據集上的準確度非常低。在本節中,我們將看到如何通過使用 DT 分類器來提升性能。我們將展示一個使用 Spark 2.1.0 的例子,使用相同的 OCR 數據集。這個例子將包含多個步驟,包括數據加載、解析、模型訓練,最后是模型評估。
由于我們將使用相同的數據集,為避免冗余,我們將跳過數據集探索步驟,直接進入示例:
第 1 步: 加載所需的庫和包,如下所示:
import org.apache.spark.ml.Pipeline // for Pipeline creation
import org.apache.spark.ml.classification.DecisionTreeClassificationModel
import org.apache.spark.ml.classification.DecisionTreeClassifier
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
import org.apache.spark.ml.feature.{IndexToString, StringIndexer, VectorIndexer}
import org.apache.spark.sql.SparkSession //For a Spark session
第 2 步: 創建一個活動的 Spark 會話,如下所示:
val spark = SparkSession.builder.master("local[*]").config("spark.sql.warehouse.dir", "/home/exp/").appName("DecisionTreeClassifier").getOrCreate()
請注意,這里將主 URL 設置為 local[*]
,意味著你機器的所有核心將用于處理 Spark 作業。你應該根據需求設置 SQL 倉庫和其他配置參數。
第 3 步:創建數據框 - 將存儲在 LIBSVM 格式中的數據加載為數據框,如下所示:
val data = spark.read.format("libsvm").load("datab/Letterdata_libsvm.data")
對于數字分類,輸入特征向量通常是稀疏的,應該將稀疏向量作為輸入,以便利用稀疏性。由于訓練數據只使用一次,而且數據集的大小相對較小(即幾 MB),如果你多次使用 DataFrame,可以緩存它。
第 4 步:標簽索引 - 索引標簽,向標簽列添加元數據。然后讓我們在整個數據集上進行擬合,以便將所有標簽包含在索引中:
val labelIndexer = new StringIndexer().setInputCol("label").setOutputCol("indexedLabel").fit(data)
第 5 步:識別分類特征 - 以下代碼段自動識別分類特征并對其進行索引:
val featureIndexer = new VectorIndexer().setInputCol("features").setOutputCol("indexedFeatures").setMaxCategories(4).fit(data)
對于這種情況,如果特征的值大于四個不同的值,它們將被視為連續值。
第 6 步:準備訓練集和測試集 - 將數據分成訓練集和測試集(25% 用于測試):
val Array(trainingData, testData) = data.randomSplit(Array(0.75, 0.25), 12345L)
第 7 步: 按如下方式訓練 DT 模型:
val dt = new DecisionTreeClassifier().setLabelCol("indexedLabel").setFeaturesCol("indexedFeatures")
第 8 步: 按如下方式將索引標簽轉換回原始標簽:
val labelConverter = new IndexToString().setInputCol("prediction").setOutputCol("predictedLabel").setLabels(labelIndexer.labels)
第 9 步:創建 DT 管道 - 讓我們通過組合索引器、標簽轉換器和樹來創建一個 DT 管道:
val pipeline = new Pipeline().setStages(Array(labelIndexer,featureIndexer, dt, labelconverter))
第 10 步:運行索引器 - 使用變換器訓練模型并運行索引器:
val model = pipeline.fit(trainingData)
第 11 步:計算測試集上的預測結果 - 使用模型變換器計算預測結果,并最終顯示每個標簽的預測結果,如下所示:
val predictions = model.transform(testData)
predictions.show()
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00344.jpeg圖 9: 針對每個標簽的預測(即每個字母)
如前圖所示,部分標簽預測準確,而部分標簽預測錯誤。然而,我們知道加權準確率、精確率、召回率和 F1 值,但我們需要先評估模型。
第 12 步:評估模型 - 選擇預測結果和真實標簽來計算測試誤差和分類性能指標,如準確率、精確率、召回率和 F1 值,如下所示:
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction")
val evaluator1 = evaluator.setMetricName("accuracy")
val evaluator2 = evaluator.setMetricName("weightedPrecision")
val evaluator3 = evaluator.setMetricName("weightedRecall")
val evaluator4 = evaluator.setMetricName("f1")
步驟 13. 計算性能指標 - 計算測試數據上的分類準確率、精確率、召回率、F1 值和錯誤率,如下所示:
val accuracy = evaluator1.evaluate(predictions)
val precision = evaluator2.evaluate(predictions)
val recall = evaluator3.evaluate(predictions)
val f1 = evaluator4.evaluate(predictions)
步驟 14. 打印性能指標:
println("Accuracy = " + accuracy)
println("Precision = " + precision)
println("Recall = " + recall)
println("F1 = " + f1)
println(s"Test Error = ${1 - accuracy}")
你應該觀察到以下值:
Accuracy = 0.994277821625888
Precision = 0.9904583933020722
Recall = 0.994277821625888
F1 = 0.9919966504321712
Test Error = 0.005722178374112041
現在性能很優秀,對吧?然而,你仍然可以通過執行超參數調整來提高分類準確率。通過交叉驗證和訓練集劃分,選擇合適的算法(即分類器或回歸器)還有進一步提高預測準確性的機會。
步驟 15. 打印決策樹節點:
val treeModel = model.stages(2).asInstanceOf[DecisionTreeClassificationModel]
println("Learned classification tree model:\n" + treeModel.toDebugString)
最后,我們將打印決策樹中的一些節點,如下圖所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00199.gif圖 10: 在模型構建過程中生成的一些決策樹節點
總結
本章中,我們討論了一些機器學習中的高級算法,并了解了如何使用簡單而強大的貝葉斯推斷方法來構建另一種分類模型——多項式分類算法。此外,我們還從理論和技術角度廣泛討論了樸素貝葉斯算法。在最后一步,我們討論了決策樹與樸素貝葉斯算法的對比分析,并提供了一些指導方針。
在下一章中,我們將深入研究機器學習,探索如何利用機器學習將無監督觀察數據集中的記錄進行聚類。
第十四章:是時候整理一下 - 使用 Spark MLlib 聚類你的數據
“如果你試圖把一個星系做大,它就變成了一個星系團,而不是一個星系。如果你試圖讓它變得比這個還小,它似乎會自我分裂。”
- 杰里邁·P·奧斯特里克
在本章中,我們將深入探討機器學習,并了解如何利用它將屬于某個特定組或類別的記錄聚類到無監督觀察數據集中的方式。簡而言之,本章將涵蓋以下主題:
-
無監督學習
-
聚類技術
-
層次聚類(HC)
-
基于質心的聚類(CC)
-
基于分布的聚類(DC)
-
確定聚類數目
-
聚類算法的比較分析
-
提交計算集群上的任務
無監督學習
在本節中,我們將簡要介紹無監督機器學習技術,并提供適當的示例。讓我們從一個實際例子開始討論。假設你在硬盤上的一個擁擠且龐大的文件夾里有大量未被盜版的、完全合法的 mp3 文件。現在,如果你能夠構建一個預測模型,幫助自動將相似的歌曲分組,并將它們組織到你最喜歡的類別中,比如鄉村、說唱、搖滾等等,那該多好。這個將項目分配到某一組的行為,就像是將 mp3 文件添加到相應的播放列表中,是一種無監督的方式。在前幾章中,我們假設你得到了一個標注正確的訓練數據集。然而,現實世界中,我們并不總是能夠擁有這樣的奢侈。例如,假設我們想要將大量音樂分成有趣的播放列表。如果我們無法直接訪問它們的元數據,那我們如何可能將這些歌曲分組呢?一種可能的方法是將多種機器學習技術結合使用,但聚類通常是解決方案的核心。
簡而言之,在無監督機器學習問題中,訓練數據集的正確類別是不可用或未知的。因此,類別必須從結構化或非結構化數據集中推導出來,如圖 1所示。這本質上意味著,這種類型的算法的目標是以某種結構化方式對數據進行預處理。換句話說,無監督學習算法的主要目標是探索輸入數據中未標注的隱藏模式。然而,無監督學習還包括其他技術,以探索數據的關鍵特征,從而找到隱藏的模式。為了解決這一挑戰,聚類技術被廣泛應用于根據某些相似性度量以無監督的方式對未標注的數據點進行分組。
要深入了解無監督算法的理論知識,請參考以下三本書:Bousquet, O.; von Luxburg, U.; Raetsch, G., eds(2004)。高級機器學習講座。Springer-Verlag。ISBN 978-3540231226,或 Duda, Richard O.;Hart, Peter E.;Stork, David G。(2001)。無監督學習與聚類。模式分類(第 2 版)。Wiley。ISBN 0-471-05669-3 和 Jordan, Michael I.;Bishop, Christopher M。(2004)神經網絡。收錄于 Allen B. Tucker 計算機科學手冊,第 2 版(第七部分:智能系統)。佛羅里達州博卡拉頓:Chapman and Hall/CRC Press LLC。ISBN 1-58488-360-X。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00263.jpeg圖 1: 使用 Spark 的無監督學習
無監督學習示例
在聚類任務中,算法通過分析輸入示例之間的相似性,將相關特征分組成類別,其中相似的特征被聚集并用圓圈標出。聚類的應用包括但不限于以下幾個方面:搜索結果分組,如客戶分組,異常檢測用于發現可疑模式,文本分類用于在文本中發現有用模式,社交網絡分析用于發現一致的群體,數據中心計算機集群用于將相關計算機組合在一起,天文數據分析用于銀河系形成,房地產數據分析用于根據相似特征識別鄰里。我們將展示一個基于 Spark MLlib 的解決方案,適用于最后一個用例。
聚類技術
在本節中,我們將討論聚類技術、挑戰以及適用的示例。還將簡要概述層次聚類、基于質心的聚類和基于分布的聚類。
無監督學習與聚類
聚類分析是將數據樣本或數據點劃分并放入相應的同質類或聚類中的過程。因此,聚類的一個簡單定義可以被認為是將對象組織成在某種方式上相似的組。
因此,一個聚類是一個對象集合,這些對象在彼此之間是相似的,而與屬于其他聚類的對象是不同的。如圖 2所示,如果給定一組對象,聚類算法會根據相似性將這些對象分組。像 K-means 這樣的聚類算法會定位數據點組的質心。然而,為了使聚類更加準確有效,算法需要評估每個點與聚類質心之間的距離。最終,聚類的目標是確定一組未標記數據中的內在分組。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00059.jpeg圖 2: 聚類原始數據
Spark 支持許多聚類算法,如K-means、高斯混合、冪迭代聚類(PIC)、潛在狄利克雷分配(LDA)、二分 K-means和流式 K-means。LDA 常用于文檔分類和聚類,廣泛應用于文本挖掘。PIC 用于聚類圖的頂點,該圖的邊屬性由成對相似度表示。然而,為了讓本章的目標更加清晰和集中,我們將僅討論 K-means、二分 K-means 和高斯混合算法。
層次聚類
層次聚類技術基于這樣一個基本思想:對象或特征與附近的對象或特征相比,更相關,而與遠離的對象或特征的相關性較低。二分 K-means 是這樣一種層次聚類算法的例子,它根據數據對象之間的對應距離將數據對象連接成簇。
在層次聚類技術中,聚類可以通過連接聚類各部分所需的最大距離來簡單地描述。因此,不同的聚類會在不同的距離下形成。從圖形上看,這些聚類可以使用樹狀圖表示。有趣的是,層次聚類這一常見名稱來源于樹狀圖的概念。
基于中心的聚類
在基于中心的聚類技術中,聚類通過一個中心向量來表示。然而,這個向量本身不一定是數據點的成員。在這種類型的學習中,必須在訓練模型之前提供一個預設的聚類數目。K-means 是這種學習類型的一個非常著名的例子,其中,如果你將聚類數目設置為一個固定的整數 K,K-means 算法就會將其定義為一個優化問題,這是一個獨立的問題,用于找到 K 個聚類中心,并將數據對象分配到距離它們最近的聚類中心。簡而言之,這是一個優化問題,目標是最小化聚類間的平方距離。
基于分布的聚類
基于分布的聚類算法是基于統計分布模型的,這些模型提供了更便捷的方式,將相關的數據對象聚類到相同的分布中。盡管這些算法的理論基礎非常穩健,但它們大多存在過擬合的問題。然而,通過對模型復雜度施加約束,可以克服這一限制。
基于中心的聚類(CC)
在這一部分,我們將討論基于中心的聚類技術及其計算挑戰。將通過使用 Spark MLlib 的 K-means 示例,幫助更好地理解基于中心的聚類。
CC 算法中的挑戰
如前所述,在像 K-means 這樣的基于質心的聚類算法中,設定聚類數 K 的最優值是一個優化問題。這個問題可以被描述為 NP-hard(即非確定性多項式時間困難),具有較高的算法復雜性,因此常見的方法是嘗試僅得到一個近似解。因此,解決這些優化問題會增加額外的負擔,并因此帶來不容忽視的缺點。此外,K-means 算法假設每個聚類的大小大致相同。換句話說,為了獲得更好的聚類效果,每個聚類中的數據點必須是均勻的。
該算法的另一個主要缺點是,它試圖優化聚類中心而不是聚類邊界,這常常會導致錯誤地切割聚類之間的邊界。然而,有時我們可以通過視覺檢查來彌補這一點,但這通常不適用于超平面上的數據或多維數據。盡管如此,關于如何找到 K 的最優值的完整內容將在本章后面討論。
K-means 算法是如何工作的?
假設我們有 n 個數據點 x[i],i=1…n,需要將它們劃分為 k 個聚類。現在目標是為每個數據點分配一個聚類。K-means 算法的目標是通過求解以下方程來找到聚類的位置 μ[i],i=1…k,以最小化數據點到聚類的距離。數學上,K-means 算法通過解決以下優化問題來實現這一目標:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00320.jpeg
在上述方程中,c[i] 是分配給聚類 i 的數據點集合,d(x,μ[i]) =||x?μ[i]||2[2] 是要計算的歐幾里得距離(我們稍后會解釋為什么要使用這種距離度量)。因此,我們可以理解,使用 K-means 進行的整體聚類操作并非一個簡單的問題,而是一個 NP-hard 優化問題。這也意味著 K-means 算法不僅僅是尋找全局最小值,還經常會陷入不同的局部解。
現在,讓我們看看在將數據輸入 K-means 模型之前,我們如何能制定算法。首先,我們需要預先決定聚類數 k。然后,通常你需要遵循以下步驟:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00367.jpeg
這里 |c| 表示 c 中元素的數量。
使用 K-means 算法進行聚類時,首先將所有坐標初始化為質心。隨著算法的每次迭代,每個點會根據某種距離度量(通常是歐幾里得距離)分配到離它最近的質心。
距離計算: 請注意,還有其他方法可以計算距離,例如:
切比雪夫距離 可以用來通過只考慮最顯著的維度來度量距離。
哈明距離算法可以識別兩個字符串之間的差異。另一方面,為了使距離度量具有尺度不變性,可以使用馬氏距離來標準化協方差矩陣。曼哈頓距離用于通過僅考慮軸對齊的方向來衡量距離。閔可夫斯基距離算法用于統一歐幾里得距離、曼哈頓距離和切比雪夫距離。哈弗辛距離用于測量球面上兩點之間的大圓距離,也就是經度和緯度之間的距離。
考慮到這些距離測量算法,可以清楚地看出,歐幾里得距離算法將是解決 K-means 算法中距離計算問題的最合適選擇。接著,質心將被更新為該次迭代中分配給它的所有點的中心。這一過程將重復,直到質心變化最小。簡而言之,K-means 算法是一個迭代算法,分為兩個步驟:
-
聚類分配步驟:K-means 算法會遍歷數據集中的每一個 m 個數據點,并將其分配到最接近的 k 個質心所代表的聚類中。對于每個點,計算它到每個質心的距離,并簡單地選擇距離最小的一個。
-
更新步驟:對于每個聚類,計算一個新的質心,該質心是該聚類中所有點的均值。從前一步驟中,我們得到了一個已分配到聚類中的點集。現在,對于每一個這樣的點集,我們計算均值,并將其聲明為新的聚類質心。
使用 Spark MLlib 的 K-means 聚類示例
為了進一步展示聚類的例子,我們將使用從Saratoga NY Homes 數據集下載的薩拉托加紐約住宅數據集,采用 Spark MLlib 進行無監督學習技術。該數據集包含了位于紐約市郊區的多棟住宅的若干特征。例如,價格、地塊大小、臨水、建筑年齡、土地價值、新建、中央空調、燃料類型、供暖類型、排污類型、居住面積、大學畢業率、臥室數量、壁爐數量、浴室數量以及房間數量。然而,以下表格中僅展示了部分特征:
價格 | 地塊大小 | 臨水 | 建筑年齡 | 土地價值 | 房間數 |
---|---|---|---|---|---|
132,500 | 0.09 | 0 | 42 | 5,000 | 5 |
181,115 | 0.92 | 0 | 0 | 22,300 | 6 |
109,000 | 0.19 | 0 | 133 | 7,300 | 8 |
155,000 | 0.41 | 0 | 13 | 18,700 | 5 |
86,060 | 0.11 | 0 | 0 | 15,000 | 3 |
120,000 | 0.68 | 0 | 31 | 14,000 | 8 |
153,000 | 0.4 | 0 | 33 | 23,300 | 8 |
170,000 | 1.21 | 0 | 23 | 146,000 | 9 |
90,000 | 0.83 | 0 | 36 | 222,000 | 8 |
122,900 | 1.94 | 0 | 4 | 212,000 | 6 |
325,000 | 2.29 | 0 | 123 | 126,000 | 12 |
表 1: 來自薩拉托加紐約住宅數據集的示例數據
該聚類技術的目標是基于每個房屋的特征,進行探索性分析,尋找可能的鄰里區域,以便為位于同一地區的房屋找到潛在的鄰居。在進行特征提取之前,我們需要加載并解析薩拉托加 NY 房屋數據集。此步驟還包括加載包和相關依賴項,讀取數據集作為 RDD,模型訓練、預測、收集本地解析數據以及聚類比較。
步驟 1. 導入相關包:
package com.chapter13.Clustering
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.mllib.clustering.{KMeans, KMeansModel}
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._
import org.apache.spark.sql._
import org.apache.spark.sql.SQLContext
步驟 2. 創建 Spark 會話 - 入口點 - 這里我們首先通過設置應用程序名稱和主機 URL 來配置 Spark。為了簡化起見,它是獨立運行,并使用您機器上的所有核心:
val spark = SparkSession.builder.master("local[*]").config("spark.sql.warehouse.dir", "E:/Exp/").appName("KMeans").getOrCreate()
步驟 3. 加載和解析數據集 - 讀取、解析并從數據集中創建 RDD,如下所示:
//Start parsing the dataset
val start = System.currentTimeMillis()
val dataPath = "data/Saratoga NY Homes.txt"
//val dataPath = args(0)
val landDF = parseRDD(spark.sparkContext.textFile(dataPath)).map(parseLand).toDF().cache()
landDF.show()
請注意,為了使前面的代碼正常工作,您應該導入以下包:
import spark.sqlContext.implicits._
您將得到如下輸出:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00339.jpeg**圖 3:**薩拉托加 NY 房屋數據集快照
以下是parseLand
方法,用于從一個Double
數組創建一個Land
類,如下所示:
// function to create a Land class from an Array of Double
def parseLand(line: Array[Double]): Land = {Land(line(0), line(1), line(2), line(3), line(4), line(5),line(6), line(7), line(8), line(9), line(10),line(11), line(12), line(13), line(14), line(15))
}
讀取所有特征為 double 類型的Land
類如下所示:
case class Land(Price: Double, LotSize: Double, Waterfront: Double, Age: Double,LandValue: Double, NewConstruct: Double, CentralAir: Double, FuelType: Double, HeatType: Double, SewerType: Double, LivingArea: Double, PctCollege: Double, Bedrooms: Double,Fireplaces: Double, Bathrooms: Double, rooms: Double
)
如您所知,訓練 K-means 模型時,我們需要確保所有數據點和特征都是數值類型。因此,我們還需要將所有數據點轉換為 double 類型,如下所示:
// method to transform an RDD of Strings into an RDD of Double
def parseRDD(rdd: RDD[String]): RDD[Array[Double]] = {rdd.map(_.split(",")).map(_.map(_.toDouble))
}
步驟 4. 準備訓練集 - 首先,我們需要將數據框(即landDF
)轉換為一個包含 double 類型數據的 RDD,并緩存數據,以創建一個新的數據框來鏈接集群編號,如下所示:
val rowsRDD = landDF.rdd.map(r => (r.getDouble(0), r.getDouble(1), r.getDouble(2),r.getDouble(3), r.getDouble(4), r.getDouble(5),r.getDouble(6), r.getDouble(7), r.getDouble(8),r.getDouble(9), r.getDouble(10), r.getDouble(11),r.getDouble(12), r.getDouble(13), r.getDouble(14),r.getDouble(15))
)
rowsRDD.cache()
現在我們需要將前面的 RDD(包含 double 類型數據)轉換為一個包含稠密向量的 RDD,如下所示:
// Get the prediction from the model with the ID so we canlink them back to other information
val predictions = rowsRDD.map{r => (r._1, model.predict(Vectors.dense(r._2, r._3, r._4, r._5, r._6, r._7, r._8, r._9,r._10, r._11, r._12, r._13, r._14, r._15, r._16)
))}
步驟 5. 訓練 K-means 模型 - 通過指定 10 個集群、20 次迭代和 10 次運行來訓練模型,如下所示:
val numClusters = 5
val numIterations = 20
val run = 10
val model = KMeans.train(numericHome, numClusters,numIterations, run,KMeans.K_MEANS_PARALLEL)
基于 Spark 的 K-means 實現通過使用K-means++算法初始化一組集群中心開始工作, Bahmani 等人提出的K-means++,VLDB 2012。這是 K-means++的一種變體,試圖通過從一個隨機中心開始,然后進行多次選擇,通過一個概率方法選擇更多的中心,概率與它們到當前集群集合的平方距離成正比。它產生了一個可證明的接近最優聚類的結果。原始論文可以在theory.stanford.edu/~sergei/papers/vldb12-kmpar.pdf
找到。
步驟 6:評估模型誤差率 - 標準的 K-means 算法旨在最小化每組數據點之間的距離平方和,即平方歐幾里得距離,這也是 WSSSE 的目標。K-means 算法旨在最小化每組數據點(即聚類中心)之間的距離平方和。然而,如果你真的想最小化每組數據點之間的距離平方和,你最終會得到一個模型,其中每個聚類都是自己的聚類中心;在這種情況下,那個度量值將是 0。
因此,一旦你通過指定參數訓練了模型,你可以使用集合內平方誤差和(WSSE)來評估結果。從技術上講,它就像是計算每個 K 個聚類中每個觀察值的距離總和,計算公式如下:
// Evaluate clustering by computing Within Set Sum of Squared Errors
val WCSSS = model.computeCost(landRDD)
println("Within-Cluster Sum of Squares = " + WCSSS)
前面的模型訓練集產生了 WCSSS 的值:
Within-Cluster Sum of Squares = 1.455560123603583E12
步驟 7:計算并打印聚類中心 - 首先,我們從模型中獲取預測結果和 ID,以便可以將其與每個房子相關的其他信息進行關聯。請注意,我們將使用在步驟 4 中準備的 RDD 行:
// Get the prediction from the model with the ID so we can link themback to other information
val predictions = rowsRDD.map{r => (r._1, model.predict(Vectors.dense(r._2, r._3, r._4, r._5, r._6, r._7, r._8, r._9, r._10,r._11, r._12, r._13, r._14, r._15, r._16)
))}
然而,在請求有關價格的預測時,應該提供該數據。可以按照如下方式操作:
val predictions = rowsRDD.map{r => (r._1, model.predict(Vectors.dense(r._1, r._2, r._3, r._4, r._5, r._6, r._7, r._8, r._9, r._10,r._11, r._12, r._13, r._14, r._15, r._16)
))}
為了更好的可視化和探索性分析,可以將 RDD 轉換為 DataFrame,代碼如下:
import spark.sqlContext.implicits._val predCluster = predictions.toDF("Price", "CLUSTER")
predCluster.show()
這將生成如下圖所示的輸出結果:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00044.gif圖 4: 聚類預測的快照
由于數據集中沒有可區分的 ID,我們使用Price
字段來進行關聯。從前面的圖中,你可以了解某個價格的房子屬于哪個聚類,即屬于哪個簇。為了更好的可視化效果,我們將預測的 DataFrame 與原始的 DataFrame 進行合并,以便知道每個房子對應的具體聚類編號:
val newDF = landDF.join(predCluster, "Price")
newDF.show()
你應該在下圖中觀察到輸出結果:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00138.jpeg圖 5: 每個房子預測的聚類快照
為了進行分析,我們將輸出數據導入到 RStudio 中,并生成了如圖 6所示的聚類。R 腳本可以在我的 GitHub 倉庫中找到,網址是github.com/rezacsedu/ScalaAndSparkForBigDataAnalytics
。另外,你也可以編寫自己的腳本并據此進行可視化。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00142.jpeg圖 6: 社區的聚類
現在,為了進行更廣泛的分析和可視化,我們可以觀察每個聚類的相關統計數據。例如,下面我打印了與聚類 3 和 4 相關的統計數據,分別在圖 8和圖 9中展示:
newDF.filter("CLUSTER = 0").show()
newDF.filter("CLUSTER = 1").show()
newDF.filter("CLUSTER = 2").show()
newDF.filter("CLUSTER = 3").show()
newDF.filter("CLUSTER = 4").show()
現在獲取每個聚類的描述性統計數據,見下:
newDF.filter("CLUSTER = 0").describe().show()
newDF.filter("CLUSTER = 1").describe().show()
newDF.filter("CLUSTER = 2").describe().show()
newDF.filter("CLUSTER = 3").describe().show()
newDF.filter("CLUSTER = 4").describe().show()
首先,讓我們觀察聚類 3 的相關統計數據,見下圖:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00353.jpeg圖 7: 聚類 3 的統計數據
現在讓我們觀察聚類 4 的相關統計數據,見下圖:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00377.jpeg圖 8: 聚類 4 的統計數據
請注意,由于原始截圖太大,無法適應本頁,因此原始圖像已被修改,并且刪除了包含其他房屋變量的列。
由于該算法的隨機性,每次成功迭代時可能會得到不同的結果。然而,您可以通過以下方法鎖定該算法的隨機性:
val numClusters = 5
val numIterations = 20
val seed = 12345
val model = KMeans.train(landRDD, numClusters, numIterations, seed)
第 8 步:停止 Spark 會話 - 最后,使用 stop 方法停止 Spark 會話,如下所示:
spark.stop()
在前面的例子中,我們處理了一個非常小的特征集;常識和目視檢查也會得出相同的結論。從上面的 K-means 算法示例中,我們可以理解該算法存在一些局限性。例如,很難預測 K 值,且全局簇表現不佳。此外,不同的初始分區可能會導致不同的最終簇,最后,它對不同大小和密度的簇表現不佳。
為了克服這些局限性,本書中介紹了一些更強大的算法,如 MCMC(馬爾可夫鏈蒙特卡洛;參見en.wikipedia.org/wiki/Markov_chain_Monte_Carlo
)在書中呈現:Tribble, Seth D., 馬爾可夫鏈蒙特卡洛算法使用完全均勻分布的驅動序列,斯坦福大學博士論文,2007 年。
層次聚類(HC)
在本節中,我們討論層次聚類技術及其計算挑戰。還將展示一個使用 Spark MLlib 的層次聚類的雙分 K-means 算法示例,以更好地理解層次聚類。
層次聚類算法概述及挑戰
層次聚類技術與基于質心的聚類在計算距離的方式上有所不同。這是最受歡迎和廣泛使用的聚類分析技術之一,旨在構建一個簇的層次結構。由于一個簇通常包含多個對象,因此還會有其他候選項來計算距離。因此,除了通常選擇的距離函數外,還需要決定使用的連接標準。簡而言之,層次聚類中有兩種策略:
-
自底向上方法:在這種方法中,每個觀察從其自身簇開始。之后,簇的對會合并在一起,然后向上移動到層次結構中。
-
自頂向下方法:在這種方法中,所有觀察從一個簇開始,分裂是遞歸進行的,然后向下移動到層次結構中。
這些自底向上或自頂向下的方法基于單鏈聚類(SLINK)技術,該技術考慮最小的對象距離;完全鏈聚類(CLINK),該方法考慮對象距離的最大值;以及無權重配對組法平均法(UPGMA)。后者也被稱為平均鏈聚類。從技術上講,這些方法不會從數據集中產生唯一的劃分(即不同的簇)。
對這三種方法的比較分析可以在nlp.stanford.edu/IR-book/completelink.html.
找到。
然而,用戶仍然需要從層次結構中選擇合適的簇,以獲得更好的聚類預測和分配。雖然這一類算法(如二分 K-means)在計算上比 K-means 算法更快,但這種類型的算法也有三個缺點:
-
首先,這些方法對于異常值或包含噪聲或缺失值的數據集并不是非常穩健。這個缺點會導致附加的簇,甚至可能導致其他簇合并。這個問題通常被稱為鏈式現象,尤其在單鏈聚類(single-linkage clustering)中比較常見。
-
其次,從算法分析來看,聚合型聚類和分裂型聚類的復雜度較高,這使得它們對于大數據集來說過于緩慢。
-
第三,SLINK 和 CLINK 曾經在數據挖掘任務中廣泛使用,作為聚類分析的理論基礎,但如今它們被認為是過時的。
使用 Spark MLlib 實現二分 K-means
二分 K-means 通常比常規 K-means 更快,但它通常會產生不同的聚類結果。二分 K-means 算法基于論文《A comparison of document clustering》中的方法,作者為 Steinbach、Karypis 和 Kumar,并經過修改以適應 Spark MLlib。
二分 K-means 是一種分裂型算法,它從一個包含所有數據點的單一簇開始。然后,它迭代地找到底層所有可分的簇,并使用 K-means 對每個簇進行二分,直到總共有 K 個葉子簇,或者沒有可分的葉子簇為止。之后,同一層次的簇會被組合在一起,以增加并行性。換句話說,二分 K-means 在計算上比常規的 K-means 算法更快。需要注意的是,如果對底層所有可分簇進行二分后得到的葉子簇數量超過 K,則較大的簇會優先被選擇。
請注意,如果對底層所有可分簇進行二分后得到的葉子簇數量超過 K,則較大的簇會優先被選擇。以下是 Spark MLlib 實現中使用的參數:
-
K:這是期望的葉子聚類數量。然而,如果在計算過程中沒有可分割的葉子聚類,實際數量可能會更少。默認值為 4。
-
MaxIterations:這是 K-means 算法中用于分割聚類的最大迭代次數。默認值為 20。
-
MinDivisibleClusterSize:這是最小的點數。默認值為 1。
-
Seed:這是一個隨機種子,禁止隨機聚類,并盡量在每次迭代中提供幾乎相同的結果。然而,建議使用較長的種子值,如 12345 等。
使用 Spark MLlib 對鄰里進行二分 K-means 聚類
在上一節中,我們看到如何將相似的房屋聚集在一起,以確定鄰里。二分 K-means 算法與常規 K-means 算法類似,不同之處在于模型訓練使用了不同的訓練參數,如下所示:
// Cluster the data into two classes using KMeans
val bkm = new BisectingKMeans() .setK(5) // Number of clusters of the similar houses.setMaxIterations(20)// Number of max iteration.setSeed(12345) // Setting seed to disallow randomness
val model = bkm.run(landRDD)
你應該參考前面的示例并重新使用前面的步驟來獲取訓練數據。現在讓我們通過計算 WSSSE 來評估聚類,方法如下:
val WCSSS = model.computeCost(landRDD)
println("Within-Cluster Sum of Squares = " + WCSSS) // Less is better
你應該觀察到以下輸出:Within-Cluster Sum of Squares = 2.096980212594632E11
。現在,若要進行進一步分析,請參閱上一節的第 5 步。
基于分布的聚類(DC)
在這一節中,我們將討論基于分布的聚類技術及其計算挑戰。為了更好地理解基于分布的聚類,將展示一個使用高斯混合模型(GMMs)與 Spark MLlib 的示例。
DC 算法中的挑戰
像 GMM 這樣的基于分布的聚類算法是一種期望最大化算法。為了避免過擬合問題,GMM 通常使用固定數量的高斯分布來建模數據集。這些分布是隨機初始化的,并且相關參數也會進行迭代優化,以便更好地將模型擬合到訓練數據集。這是 GMM 最強大的特點,有助于模型向局部最優解收斂。然而,算法的多次運行可能會產生不同的結果。
換句話說,與二分 K-means 算法和軟聚類不同,GMM 是針對硬聚類進行優化的,為了獲得這種類型,通常會將對象分配到高斯分布中。GMM 的另一個優勢是,它通過捕捉數據點和屬性之間所需的所有相關性和依賴關系,生成復雜的聚類模型。
不過,GMM 對數據的格式和形狀有一些假設,這就給我們(即用戶)增加了額外的負擔。更具體地說,如果以下兩個標準不滿足,性能會急劇下降:
-
非高斯數據集:GMM 算法假設數據集具有潛在的高斯分布,這是生成性分布。然而,許多實際數據集不滿足這一假設,可能導致較差的聚類性能。
-
如果聚類的大小不均,較小的聚類很可能會被較大的聚類所主導。
高斯混合模型是如何工作的?
使用 GMM 是一種流行的軟聚類技術。GMM 試圖將所有數據點建模為有限的高斯分布混合體;計算每個點屬于每個聚類的概率,并與聚類相關的統計數據一起表示一個合成分布:所有點都來自 K 個具有自身概率的高斯子分布之一。簡而言之,GMM 的功能可以用三步偽代碼描述:
-
Objective function(目標函數):使用期望最大化(EM)作為框架,計算并最大化對數似然。
-
EM 算法:
-
E 步驟(E step):計算后驗概率 - 即靠近的數據點。
-
M 步驟(M step):優化參數。
-
-
Assignment(分配):在 E 步驟中執行軟分配。
從技術上講,當給定一個統計模型時,該模型的參數(即應用于數據集時)是通過 最大似然估計 (MLE) 來估計的。另一方面,EM 算法是一個迭代過程,用于尋找最大似然。
由于 GMM 是一種無監督算法,GMM 模型依賴于推斷的變量。然后,EM 迭代會轉向執行期望(E)和最大化(M)步驟。
Spark MLlib 實現使用期望最大化算法從給定的數據點集中引導最大似然模型。當前的實現使用以下參數:
-
K 是所需聚類數,用于聚類你的數據點。
-
ConvergenceTol(收斂容忍度) 是我們認為收斂已達成時,最大對數似然的變化量。
-
MaxIterations(最大迭代次數) 是在沒有達到收斂點的情況下執行的最大迭代次數。
-
InitialModel 是一個可選的起始點,用于啟動 EM 算法。如果省略此參數,將從數據中構造一個隨機起始點。
使用 Spark MLlib 進行 GMM 聚類的示例
在前面的章節中,我們看到了如何將相似的房屋聚集在一起以確定鄰里。使用 GMM,也可以將房屋聚集在一起以尋找鄰里,除了模型訓練會采用不同的訓練參數,如下所示:
val K = 5
val maxIteration = 20
val model = new GaussianMixture().setK(K)// Number of desired clusters.setMaxIterations(maxIteration)//Maximum iterations.setConvergenceTol(0.05) // Convergence tolerance. .setSeed(12345) // setting seed to disallow randomness.run(landRDD) // fit the model using the training set
你應該參考之前的示例,并重用獲取訓練數據的先前步驟。現在為了評估模型的性能,GMM 并沒有提供像 WCSS 這樣的性能指標作為代價函數。然而,GMM 提供了一些性能指標,比如 mu、sigma 和權重。這些參數表示不同聚類之間的最大似然(我們這里有五個聚類)。這可以如下演示:
// output parameters of max-likelihood model
for (i <- 0 until model.K) {println("Cluster " + i)println("Weight=%f\nMU=%s\nSigma=\n%s\n" format(model.weights(i), model.gaussians(i).mu, model.gaussians(i).sigma))
}
你應該觀察到以下輸出:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00154.jpeg圖 9: 簇 1https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00017.jpeg圖 10: 簇 2https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00240.jpeg圖 11: 簇 3https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00139.jpeg圖 12: 簇 4https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00002.jpeg圖 13: 簇 5
簇 1 到簇 4 的權重表明這些簇是均質的,并且與簇 5 相比存在顯著差異。
確定簇的數量
像 K-means 算法這樣的聚類算法的優點在于,它可以對具有無限特征的數據進行聚類。當你有原始數據并希望了解數據中的模式時,這是一個非常好的工具。然而,在實驗之前確定簇的數量可能并不成功,有時還可能導致過擬合或欠擬合問題。另一方面,K-means、二分 K-means 和高斯混合模型這三種算法的共同之處在于,簇的數量必須事先確定,并作為參數提供給算法。因此,非正式地說,確定簇的數量是一個獨立的優化問題,需要解決。
在本節中,我們將使用基于肘部法則的啟發式方法。我們從 K = 2 個簇開始,然后通過增加 K 并觀察成本函數簇內平方和(Within-Cluster Sum of Squares)(WCSS)的值,運行 K-means 算法處理相同的數據集。在某些時刻,可以觀察到成本函數有一個大的下降,但隨著 K 值的增加,改進變得微乎其微。如聚類分析文獻所建議的,我們可以選擇 WCSS 最后一次大幅下降后的 K 值作為最優值。
通過分析以下參數,你可以找出 K-means 的性能:
-
中介性(Betweenness): 這是中介平方和,也稱為簇內相似度(intracluster similarity)。
-
簇內平方和(Withiness): 這是簇內平方和,也叫做簇間相似度(intercluster similarity)。
-
總簇內平方和(Totwithinss): 這是所有簇內的平方和的總和,也叫做總簇內相似度(total intracluster similarity)。
值得注意的是,一個穩健且準確的聚類模型將具有較低的簇內平方和和較高的中介性值。然而,這些值取決于簇的數量,即 K 值,這個值需要在構建模型之前選擇。
現在讓我們討論如何利用肘部法則來確定簇的數量。如下面所示,我們計算了 K-means 算法應用于家庭數據(基于所有特征)時,聚類數與成本函數 WCSS 的關系。可以觀察到,當 K = 5 時,出現了一個大幅下降。因此,我們選擇了 5 作為簇的數量,如圖 10所示。基本上,這是最后一次大幅下降之后的值。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00303.jpeg圖 14: 聚類數與 WCSS 的關系
聚類算法的比較分析
高斯混合模型主要用于期望最小化,這是優化算法的一個例子。與普通 K-means 算法相比,二分 K-means 更快,并且產生略微不同的聚類結果。下面我們嘗試對比這三種算法。我們將展示每種算法在模型構建時間和計算成本方面的性能對比。如以下代碼所示,我們可以通過 WCSS 計算成本。以下代碼行可以用來計算 K-means 和二分算法的 WCSS:
val WCSSS = model.computeCost(landRDD) // land RDD is the training set
println("Within-Cluster Sum of Squares = " + WCSSS) // Less is better
對于本章使用的數據集,我們得到了以下 WCSS 的值:
Within-Cluster Sum of Squares of Bisecting K-means = 2.096980212594632E11
Within-Cluster Sum of Squares of K-means = 1.455560123603583E12
這意味著在計算成本方面,K-means 的表現稍微好一些。不幸的是,我們沒有像 WCSS 這樣的度量指標來評估 GMM 算法。現在讓我們觀察這三種算法的模型構建時間。我們可以在開始模型訓練前啟動系統時鐘,并在訓練結束后立即停止時鐘,如下所示(對于 K-means):
val start = System.currentTimeMillis()
val numClusters = 5
val numIterations = 20
val seed = 12345
val runs = 50
val model = KMeans.train(landRDD, numClusters, numIterations, seed)
val end = System.currentTimeMillis()
println("Model building and prediction time: "+ {end - start} + "ms")
對于本章使用的訓練集,我們得到了以下模型構建時間的值:
Model building and prediction time for Bisecting K-means: 2680ms
Model building and prediction time for Gaussian Mixture: 2193ms
Model building and prediction time for K-means: 3741ms
在不同的研究文章中發現,二分 K-means 算法在數據點的聚類分配上表現得更好。此外,與 K-means 相比,二分 K-means 也能更好地收斂到全局最小值。而 K-means 則容易陷入局部最小值。換句話說,使用二分 K-means 算法,我們可以避免 K-means 可能遭遇的局部最小值問題。
請注意,根據機器的硬件配置和數據集的隨機性,您可能會觀察到前述參數的不同值。
更詳細的分析留給讀者從理論角度進行。感興趣的讀者還應參考基于 Spark MLlib 的聚類技術,詳情請見 spark.apache.org/docs/latest/mllib-clustering.html
以獲得更多見解。
提交 Spark 作業進行聚類分析
本章展示的例子可以擴展到更大的數據集以服務于不同的目的。您可以將所有三種聚類算法與所需的依賴項一起打包,并將它們作為 Spark 作業提交到集群中。現在,使用以下代碼行來提交您的 K-means 聚類 Spark 作業,例如(對其他類使用類似語法),以處理 Saratoga NY Homes 數據集:
# Run application as standalone mode on 8 cores
SPARK_HOME/bin/spark-submit \
--class org.apache.spark.examples.KMeansDemo \
--master local[8] \
KMeansDemo-0.1-SNAPSHOT-jar-with-dependencies.jar \
Saratoga_NY_Homes.txt# Run on a YARN cluster
export HADOOP_CONF_DIR=XXX
SPARK_HOME/bin/spark-submit \
--class org.apache.spark.examples.KMeansDemo \
--master yarn \
--deploy-mode cluster \ # can be client for client mode
--executor-memory 20G \
--num-executors 50 \
KMeansDemo-0.1-SNAPSHOT-jar-with-dependencies.jar \
Saratoga_NY_Homes.txt# Run on a Mesos cluster in cluster deploy mode with supervising
SPARK_HOME/bin/spark-submit \
--class org.apache.spark.examples.KMeansDemo \
--master mesos://207.184.161.138:7077 \ # Use your IP aadress
--deploy-mode cluster \
--supervise \
--executor-memory 20G \
--total-executor-cores 100 \
KMeansDemo-0.1-SNAPSHOT-jar-with-dependencies.jar \
Saratoga_NY_Homes.txt
總結
本章中,我們進一步深入探討了機器學習,并了解了如何利用機器學習對無監督觀測數據集中的記錄進行聚類。因此,你學習了通過前幾章的理解,如何快速而有力地將有監督和無監督技術應用于新問題。我們將展示的例子將從 Spark 的角度進行說明。對于 K-means、二分 K-means 和高斯混合算法,無法保證算法在多次運行時產生相同的聚類結果。例如,我們觀察到,使用相同參數多次運行 K-means 算法時,每次運行產生的結果略有不同。
關于 K-means 和高斯混合模型的性能對比,請參見Jung 等人的聚類分析講義。除了 K-means、二分 K-means 和高斯混合模型外,MLlib 還提供了另外三種聚類算法的實現,分別是 PIC、LDA 和流式 K-means。值得一提的是,為了精細調優聚類分析,我們通常需要去除一些被稱為離群點或異常值的無效數據對象。但使用基于距離的聚類方法時,確實很難識別這些數據點。因此,除了歐氏距離外,還可以使用其他距離度量。無論如何,這些鏈接將是一個很好的起點資源:
-
mapr.com/ebooks/spark/08-unsupervised-anomaly-detection-apache-spark.html
-
github.com/keiraqz/anomaly-detection
-
www.dcc.fc.up.pt/~ltorgo/Papers/ODCM.pdf
在下一章中,我們將深入探討如何調優 Spark 應用以提高性能。我們將看到一些優化 Spark 應用性能的最佳實踐。
第十五章:使用 Spark ML 進行文本分析
“程序必須為人類閱讀而編寫,只有在偶然的情況下才是為了機器執行。”
- 哈羅德·阿貝爾森
在本章中,我們將討論使用 Spark ML 進行文本分析這一美妙的領域。文本分析是機器學習中的一個廣泛領域,并且在許多用例中都很有用,比如情感分析、聊天機器人、電子郵件垃圾郵件檢測和自然語言處理。我們將學習如何使用 Spark 進行文本分析,重點介紹使用 Twitter 的 10,000 個樣本數據集進行文本分類的用例。
簡而言之,本章將涵蓋以下主題:
-
理解文本分析
-
轉換器和估計器
-
分詞器
-
StopWordsRemover
-
NGrams
-
TF-IDF
-
Word2Vec
-
CountVectorizer
-
使用 LDA 進行主題建模
-
實現文本分類
理解文本分析
在過去幾章中,我們已經探索了機器學習的世界以及 Apache Spark 對機器學習的支持。正如我們所討論的,機器學習有一個工作流程,這些流程可以通過以下步驟來解釋:
-
加載或獲取數據。
-
清洗數據。
-
從數據中提取特征。
-
在數據上訓練模型,根據特征生成期望的結果。
-
根據數據評估或預測某些結果。
一個典型流水線的簡化視圖如下所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00310.jpeg
因此,在訓練模型并隨后部署之前,數據的轉換階段有很多種可能性。此外,我們應該預期特征和模型屬性的精細調整。我們甚至可以探索一種完全不同的算法,作為新工作流的一部分重復整個任務序列。
可以通過多個轉換步驟創建一個流水線,因此我們使用領域特定語言(DSL)來定義節點(數據轉換步驟),從而創建一個DAG(有向無環圖)節點。因此,ML 流水線是一個由多個轉換器和估計器組成的序列,用于將輸入數據集擬合到流水線模型中。流水線中的每個階段稱為流水線階段,如下所示:
-
估計器
-
模型
-
流水線
-
轉換器
-
預測器
當你看一行文本時,我們看到句子、短語、單詞、名詞、動詞、標點符號等,它們組合在一起時具有意義和目的。人類非常擅長理解句子、單詞、俚語、注釋或上下文。這來自于多年的練習和學習如何讀寫、正確的語法、標點符號、感嘆詞等。那么,我們如何編寫計算機程序來嘗試復制這種能力呢?
文本分析
文本分析是從一組文本中解鎖意義的方法。通過使用各種技術和算法處理和分析文本數據,我們可以揭示數據中的模式和主題。所有這些的目標是理解非結構化的文本,以便得出上下文的意義和關系。
文本分析利用幾種廣泛的技術類別,接下來我們將討論這些技術。
情感分析
分析 Facebook、Twitter 和其他社交媒體上人們的政治觀點是情感分析的一個良好示例。同樣,分析 Yelp 上餐廳的評論也是情感分析的另一個很好的示例。
自然語言處理(NLP)框架和庫,如 OpenNLP 和斯坦福 NLP,通常用于實現情感分析。
主題建模
主題建模是檢測文檔語料庫中主題或主題的一種有用技術。這是一種無監督算法,可以在一組文檔中找到主題。例如,檢測新聞文章中涉及的主題。另一個例子是檢測專利申請中的思想。
潛在狄利克雷分配(LDA)是一個流行的無監督聚類模型,而潛在語義分析(LSA)則在共現數據上使用概率模型。
TF-IDF(詞頻-逆文檔頻率)
TF-IDF 衡量單詞在文檔中出現的頻率以及在一組文檔中的相對頻率。此信息可用于構建分類器和預測模型。例如垃圾郵件分類、聊天對話等。
命名實體識別(NER)
命名實體識別通過檢測句子中單詞和名詞的使用來提取關于人、組織、地點等的信息。這提供了關于文檔實際內容的重要上下文信息,而不僅僅是將單詞視為主要實體。
斯坦福 NLP 和 OpenNLP 都實現了 NER 算法。
事件抽取
事件抽取在 NER 基礎上擴展,通過建立檢測到的實體之間的關系。這可以用于推斷兩個實體之間的關系。因此,它增加了語義理解的層次,幫助理解文檔內容。
變換器和估計器
Transformer 是一個函數對象,通過將變換邏輯(函數)應用于輸入數據集,生成輸出數據集,從而將一個數據集轉換為另一個數據集。變換器有兩種類型:標準變換器和估計器變換器。
標準變換器
標準變換器將輸入數據集轉換為輸出數據集,明確地將變換函數應用于輸入數據。除了讀取輸入列和生成輸出列外,不依賴于輸入數據。
這種變換器的調用方式如下所示:
*outputDF = transfomer.*transform*(inputDF)*
標準變換器的示例如下,并將在后續章節中詳細解釋:
-
Tokenizer
:此工具使用空格作為分隔符將句子拆分為單詞。 -
RegexTokenizer
:此工具使用正則表達式將句子拆分為單詞。 -
StopWordsRemover
:此工具從單詞列表中去除常用的停用詞。 -
Binarizer
:將字符串轉換為二進制數字 0/1 -
NGram
:從句子中創建 N 個單詞短語 -
HashingTF
:使用哈希表索引單詞來創建詞頻計數 -
SQLTransformer
:實現由 SQL 語句定義的變換 -
VectorAssembler
:將給定的列列表組合成一個單一的向量列
標準 Transformer 的示意圖如下,其中來自輸入數據集的輸入列被轉換為輸出列,從而生成輸出數據集:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00247.jpeg
估算器變換器
估算器變換器通過首先根據輸入數據集生成一個變換器來將輸入數據集轉換為輸出數據集。然后,變換器處理輸入數據,讀取輸入列并生成輸出列,最終形成輸出數據集。
這些變換器如下所示:
*transformer = estimator.*fit*(inputDF)* *outputDF = transformer.*transform*(inputDF)*
估算器變換器的示例如下:
-
IDF
-
LDA
-
Word2Vec
估算器變換器的示意圖如下,其中來自輸入數據集的輸入列被轉換為輸出列,從而生成輸出數據集:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00287.jpeg
在接下來的幾個部分,我們將深入探討文本分析,使用一個簡單的示例數據集,數據集包含多行文本(句子),如以下截圖所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00330.jpeg
以下代碼用于將文本數據加載到輸入數據集中。
使用一對 ID 和文本的序列來初始化一組句子,具體如下所示。
val lines = Seq(| (1, "Hello there, how do you like the book so far?"),| (2, "I am new to Machine Learning"),| (3, "Maybe i should get some coffee before starting"),| (4, "Coffee is best when you drink it hot"),| (5, "Book stores have coffee too so i should go to a book store")| )
lines: Seq[(Int, String)] = List((1,Hello there, how do you like the book so far?), (2,I am new to Machine Learning), (3,Maybe i should get some coffee before starting), (4,Coffee is best when you drink it hot), (5,Book stores have coffee too so i should go to a book store))
接下來,調用createDataFrame()
函數從我們之前看到的句子序列創建一個 DataFrame。
scala> val sentenceDF = spark.createDataFrame(lines).toDF("id", "sentence")
sentenceDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string]
現在你可以看到新創建的數據集,其中顯示了包含兩個列 ID 和句子的 Sentence DataFrame。
scala> sentenceDF.show(false)
|id|sentence |
|1 |Hello there, how do you like the book so far? |
|2 |I am new to Machine Learning |
|3 |Maybe i should get some coffee before starting |
|4 |Coffee is best when you drink it hot |
|5 |Book stores have coffee too so i should go to a book store|
分詞
分詞器將輸入字符串轉換為小寫,并通過空格將字符串拆分為單獨的標記。給定的句子通過默認的空格分隔符或使用自定義正則表達式的分詞器拆分為單詞。無論哪種方式,輸入列都會轉換為輸出列。特別地,輸入列通常是字符串,而輸出列是一個單詞序列。
分詞器可以通過導入接下來的兩個包來使用,分別是Tokenizer
和RegexTokenizer
:
import org.apache.spark.ml.feature.Tokenizer
import org.apache.spark.ml.feature.RegexTokenizer
首先,你需要初始化一個Tokenizer
,指定輸入列和輸出列:
scala> val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
tokenizer: org.apache.spark.ml.feature.Tokenizer = tok_942c8332b9d8
接下來,在輸入數據集上調用transform()
函數會生成一個輸出數據集:
scala> val wordsDF = tokenizer.transform(sentenceDF)
wordsDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 1 more field]
以下是輸出數據集,顯示輸入列的 ID、句子和輸出列的單詞,后者包含單詞的序列:
scala> wordsDF.show(false)
|id|sentence |words |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|
另一方面,如果你想設置基于正則表達式的Tokenizer
,你必須使用RegexTokenizer
而不是Tokenizer
。為此,你需要初始化一個RegexTokenizer
,指定輸入列和輸出列,并提供要使用的正則表達式模式:
scala> val regexTokenizer = new RegexTokenizer().setInputCol("sentence").setOutputCol("regexWords").setPattern("\\W")
regexTokenizer: org.apache.spark.ml.feature.RegexTokenizer = regexTok_15045df8ce41
接下來,在輸入數據集上調用transform()
函數會產生一個輸出數據集:
scala> val regexWordsDF = regexTokenizer.transform(sentenceDF)
regexWordsDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 1 more field]
以下是輸出數據集,顯示了輸入列 ID、句子和輸出列regexWordsDF
,其中包含了單詞序列:
scala> regexWordsDF.show(false)
|id|sentence |regexWords |
|1 |Hello there, how do you like the book so far? |[hello, there, how, do, you, like, the, book, so, far] |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|
Tokenizer
的圖示如下,其中輸入文本中的句子通過空格分隔符拆分成單詞:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00150.jpeg
StopWordsRemover
StopWordsRemover
是一個轉換器,它接受一個包含單詞的String
數組,并返回一個String
數組,其中去除了所有已定義的停用詞。停用詞的一些示例包括 I、you、my、and、or 等,這些在英語中是非常常見的單詞。你可以覆蓋或擴展停用詞的集合,以適應用例的目的。如果沒有這個清洗過程,后續的算法可能會因常見單詞的影響而產生偏差。
為了調用StopWordsRemover
,你需要導入以下包:
import org.apache.spark.ml.feature.StopWordsRemover
首先,你需要初始化一個StopWordsRemover
,指定輸入列和輸出列。在這里,我們選擇由Tokenizer
創建的單詞列,并生成一個輸出列,包含在刪除停用詞后過濾的單詞:
scala> val remover = new StopWordsRemover().setInputCol("words").setOutputCol("filteredWords")
remover: org.apache.spark.ml.feature.StopWordsRemover = stopWords_48d2cecd3011
接下來,在輸入數據集上調用transform()
函數會產生一個輸出數據集:
scala> val noStopWordsDF = remover.transform(wordsDF)
noStopWordsDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 2 more fields]
以下是輸出數據集,顯示了輸入列 ID、句子和輸出列filteredWords
,其中包含了單詞的序列:
scala> noStopWordsDF.show(false)
|id|sentence |words |filteredWords |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|
以下是輸出數據集,顯示了僅包含句子和filteredWords
的內容,filteredWords
包含了過濾后的單詞序列:
scala> noStopWordsDF.select("sentence", "filteredWords").show(5,false)
|sentence |filteredWords |
|Hello there, how do you like the book so far? |[hello, there,, like, book, far?] |
|I am new to Machine Learning |[new, machine, learning] |
|Maybe i should get some coffee before starting |[maybe, get, coffee, starting] |
|Coffee is best when you drink it hot |[coffee, best, drink, hot] |
|Book stores have coffee too so i should go to a book store|[book, stores, coffee, go, book, store]|
StopWordsRemover
的圖示如下,顯示了過濾掉的停用詞,如 I、should、some 和 before:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00021.jpeg
停用詞是默認設置的,但可以很容易地覆蓋或修改,正如下面的代碼片段所示,我們將在過濾后的單詞中刪除“hello”,將其視為停用詞:
scala> val noHello = Array("hello") ++ remover.getStopWords
noHello: Array[String] = Array(hello, i, me, my, myself, we, our, ours, ourselves, you, your, yours, yourself, yourselves, he, him, his, himself, she, her, hers, herself, it, its, itself, they, them, their, theirs, themselves, what, which, who, whom, this, that, these, those, am, is, are, was, were ...
scala>//create new transfomer using the amended Stop Words list
scala> val removerCustom = new StopWordsRemover().setInputCol("words").setOutputCol("filteredWords").setStopWords(noHello)
removerCustom: org.apache.spark.ml.feature.StopWordsRemover = stopWords_908b488ac87f//invoke transform function
scala> val noStopWordsDFCustom = removerCustom.transform(wordsDF)
noStopWordsDFCustom: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 2 more fields]//output dataset showing only sentence and filtered words - now will not show hello
scala> noStopWordsDFCustom.select("sentence", "filteredWords").show(5,false)
+----------------------------------------------------------+---------------------------------------+
|sentence |filteredWords |
+----------------------------------------------------------+---------------------------------------+
|Hello there, how do you like the book so far? |[there,, like, book, far?] |
|I am new to Machine Learning |[new, machine, learning] |
|Maybe i should get some coffee before starting |[maybe, get, coffee, starting] |
|Coffee is best when you drink it hot |[coffee, best, drink, hot] |
|Book stores have coffee too so i should go to a book store|[book, stores, coffee, go, book, store]|
+----------------------------------------------------------+---------------------------------------+
NGrams
NGrams 是由單詞組合生成的單詞序列。N 代表序列中單詞的數量。例如,2-gram 是兩個單詞在一起,3-gram 是三個單詞在一起。setN()
用于指定N
的值。
為了生成 NGrams,你需要導入該包:
import org.apache.spark.ml.feature.NGram
首先,你需要初始化一個NGram
生成器,指定輸入列和輸出列。在這里,我們選擇由StopWordsRemover
創建的過濾單詞列,并生成一個輸出列,包含在刪除停用詞后過濾的單詞:
scala> val ngram = new NGram().setN(2).setInputCol("filteredWords").setOutputCol("ngrams")
ngram: org.apache.spark.ml.feature.NGram = ngram_e7a3d3ab6115
接下來,在輸入數據集上調用transform()
函數會產生一個輸出數據集:
scala> val nGramDF = ngram.transform(noStopWordsDF)
nGramDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 3 more fields]
以下是輸出數據集,顯示了輸入列 ID、句子和輸出列ngram
,其中包含了 n-gram 序列:
scala> nGramDF.show(false)
|id|sentence |words |filteredWords |ngrams |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |[hello there,, there, like, like book, book far?] |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |[new machine, machine learning] |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |[maybe get, get coffee, coffee starting] |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |[coffee best, best drink, drink hot] |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|[book stores, stores coffee, coffee go, go book, book store]|
以下是輸出數據集,顯示了句子和 2-gram:
scala> nGramDF.select("sentence", "ngrams").show(5,false)
|sentence |ngrams |
|Hello there, how do you like the book so far? |[hello there,, there, like, like book, book far?] |
|I am new to Machine Learning |[new machine, machine learning] |
|Maybe i should get some coffee before starting |[maybe get, get coffee, coffee starting] |
|Coffee is best when you drink it hot |[coffee best, best drink, drink hot] |
|Book stores have coffee too so i should go to a book store|[book stores, stores coffee, coffee go, go book, book store]|
NGram 的圖示如下,顯示了在句子經過分詞和去除停用詞后生成的 2-gram:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00163.jpeg
TF-IDF
TF-IDF 代表詞頻-逆文檔頻率,它衡量一個詞在文檔集合中對某個文檔的重要性。它在信息檢索中被廣泛使用,并反映了詞在文檔中的權重。TF-IDF 值隨著詞語出現次數的增加而增加,詞語/術語的頻率由兩個關鍵元素組成:詞頻和逆文檔頻率。
TF 是詞頻,表示單詞/術語在文檔中的頻率。
對于一個術語t,tf度量術語t在文檔d中出現的次數。tf在 Spark 中通過哈希實現,將術語通過哈希函數映射到索引。
IDF 是逆文檔頻率,表示術語提供的關于該術語在文檔中出現的趨勢的信息。IDF 是包含該術語的文檔數的對數縮放逆函數:
IDF = 總文檔數/包含術語的文檔數
一旦我們有了TF和IDF,我們就可以通過將TF和IDF相乘來計算TF-IDF值:
TF-IDF = TF * IDF
接下來,我們將看看如何使用 Spark ML 中的 HashingTF 轉換器生成TF。
HashingTF
HashingTF是一個轉換器,它接受一組術語并通過哈希每個術語來生成固定長度的向量,為每個術語生成索引。然后,使用哈希表的索引生成術語頻率。
在 Spark 中,HashingTF 使用MurmurHash3算法來對術語進行哈希處理。
為了使用HashingTF
,您需要導入以下包:
import org.apache.spark.ml.feature.HashingTF
首先,您需要初始化一個HashingTF
,指定輸入列和輸出列。在這里,我們選擇由StopWordsRemover
轉換器創建的過濾詞列,并生成輸出列rawFeaturesDF
。我們還選擇將特征數量設置為 100:
scala> val hashingTF = new HashingTF().setInputCol("filteredWords").setOutputCol("rawFeatures").setNumFeatures(100)
hashingTF: org.apache.spark.ml.feature.HashingTF = hashingTF_b05954cb9375
接下來,在輸入數據集上調用transform()
函數會生成輸出數據集:
scala> val rawFeaturesDF = hashingTF.transform(noStopWordsDF)
rawFeaturesDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 3 more fields]
以下是輸出數據集,顯示了輸入列 ID、句子和輸出列rawFeaturesDF
,其中包含由向量表示的特征:
scala> rawFeaturesDF.show(false)
|id |sentence |words |filteredWords |rawFeatures |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |(100,[30,48,70,93],[2.0,1.0,1.0,1.0]) |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |(100,[25,52,72],[1.0,1.0,1.0]) |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |(100,[16,51,59,99],[1.0,1.0,1.0,1.0]) |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |(100,[31,51,63,72],[1.0,1.0,1.0,1.0]) |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|(100,[43,48,51,77,93],[1.0,1.0,1.0,1.0,2.0])|
讓我們看一下前面的輸出,以便更好地理解。如果僅查看filteredWords
和rawFeatures
列,您會看到,
-
詞匯數組
[hello, there, like, book, and far]
被轉換為原始特征向量(100,[30,48,70,93],[2.0,1.0,1.0,1.0])
。 -
詞匯數組
(book, stores, coffee, go, book, and store)
被轉換為原始特征向量(100,[43,48,51,77,93],[1.0,1.0,1.0,1.0,2.0])
。
那么,這里的向量表示什么呢?其基本邏輯是,每個單詞被哈希為一個整數,并計算在單詞數組中出現的次數。
Spark 內部使用一個hashMap
(mutable.HashMap.empty[Int, Double]
),用于存儲每個單詞的哈希值,其中Integer
鍵表示哈希值,Double
值表示出現次數。使用 Double 類型是為了能夠與 IDF 一起使用(我們將在下一節討論)。使用這個映射,數組[book, stores, coffee, go, book, store]
可以看作[hashFunc(book), hashFunc(stores), hashFunc(coffee), hashFunc(go), hashFunc(book), hashFunc(store)]
, 其等于[43,48,51,77,93]
。 然后,如果你也統計出現次數的話,即:book-2, coffee-1, go-1, store-1, stores-1
。
結合前面的信息,我們可以生成一個向量(numFeatures, hashValues, Frequencies)
, 在這種情況下,它將是(100,[43,48,51,77,93],[1.0,1.0,1.0,1.0,2.0])
。
逆文檔頻率(IDF)
逆文檔頻率(IDF)是一種估算器,它應用于數據集并通過縮放輸入特征生成特征。因此,IDF 作用于 HashingTF 轉換器的輸出。
為了調用 IDF,您需要導入該包:
import org.apache.spark.ml.feature.IDF
首先,您需要初始化一個IDF
,并指定輸入列和輸出列。這里,我們選擇由 HashingTF 創建的單詞列rawFeatures
,并生成一個輸出列特征:
scala> val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features")
idf: org.apache.spark.ml.feature.IDF = idf_d8f9ab7e398e
接下來,在輸入數據集上調用fit()
函數會生成一個輸出轉換器(Transformer):
scala> val idfModel = idf.fit(rawFeaturesDF)
idfModel: org.apache.spark.ml.feature.IDFModel = idf_d8f9ab7e398e
此外,在輸入數據集上調用transform()
函數會生成一個輸出數據集:
scala> val featuresDF = idfModel.transform(rawFeaturesDF)
featuresDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 4 more fields]
以下是輸出數據集,顯示了輸入列 ID 和輸出列特征,其中包含前述轉換中由 HashingTF 生成的縮放特征向量:
scala> featuresDF.select("id", "features").show(5, false)
|id|features |
|1 |(20,[8,10,13],[0.6931471805599453,3.295836866004329,0.6931471805599453]) |
|2 |(20,[5,12],[1.0986122886681098,1.3862943611198906]) |
|3 |(20,[11,16,19],[0.4054651081081644,1.0986122886681098,2.1972245773362196]) |
|4 |(20,[3,11,12],[0.6931471805599453,0.8109302162163288,0.6931471805599453]) |
|5 |(20,[3,8,11,13,17],[0.6931471805599453,0.6931471805599453,0.4054651081081644,1.3862943611198906,1.0986122886681098])|
以下是輸出數據集,顯示了輸入列 ID、句子、rawFeatures
和輸出列特征,其中包含前述轉換中由 HashingTF 生成的縮放特征向量:
scala> featuresDF.show(false)
|id|sentence |words |filteredWords |rawFeatures |features |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |(20,[8,10,13],[1.0,3.0,1.0]) |(20,[8,10,13],[0.6931471805599453,3.295836866004329,0.6931471805599453]) |
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |(20,[5,12],[1.0,2.0]) |(20,[5,12],[1.0986122886681098,1.3862943611198906]) |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |(20,[11,16,19],[1.0,1.0,2.0]) |(20,[11,16,19],[0.4054651081081644,1.0986122886681098,2.1972245773362196]) |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |(20,[3,11,12],[1.0,2.0,1.0]) |(20,[3,11,12],[0.6931471805599453,0.8109302162163288,0.6931471805599453]) |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|(20,[3,8,11,13,17],[1.0,1.0,1.0,2.0,1.0])|(20,[3,8,11,13,17],[0.6931471805599453,0.6931471805599453,0.4054651081081644,1.3862943611198906,1.0986122886681098])|
TF-IDF 的示意圖如下,展示了TF-IDF 特征的生成過程:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00089.jpeg
Word2Vec
Word2Vec 是一個復雜的神經網絡風格的自然語言處理工具,使用一種稱為跳字模型(skip-grams)的方法,將一串單詞轉換為嵌入式向量表示。我們來看一個關于動物的句子集合,看看如何使用這種技術:
-
一只狗在叫
-
一些牛在吃草
-
狗通常會隨便叫
-
那頭牛喜歡吃草
使用帶有隱藏層的神經網絡(這種機器學習算法在許多無監督學習應用中被使用),我們可以學習到(通過足夠的示例)dog和barking是相關的,cow和grass是相關的,因為它們經常出現在彼此附近,這種關系通過概率來衡量。Word2vec
的輸出是一個Double
特征的向量。
為了調用Word2vec
,您需要導入該包:
import org.apache.spark.ml.feature.Word2Vec
首先,你需要初始化一個Word2vec
轉換器,指定輸入列和輸出列。這里,我們選擇由Tokenizer
創建的單詞列,并生成一個大小為 3 的單詞向量輸出列:
scala> val word2Vec = new Word2Vec().setInputCol("words").setOutputCol("wordvector").setVectorSize(3).setMinCount(0)
word2Vec: org.apache.spark.ml.feature.Word2Vec = w2v_fe9d488fdb69
接下來,對輸入數據集調用fit()
函數會生成一個輸出轉換器:
scala> val word2VecModel = word2Vec.fit(noStopWordsDF)
word2VecModel: org.apache.spark.ml.feature.Word2VecModel = w2v_fe9d488fdb69
此外,對輸入數據集調用transform()
函數會生成一個輸出數據集:
scala> val word2VecDF = word2VecModel.transform(noStopWordsDF)
word2VecDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 3 more fields]
以下是輸出數據集,顯示了輸入列 ID、句子以及輸出列wordvector
:
scala> word2VecDF.show(false)
|id|sentence |words |filteredWords |wordvector |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |[0.006875938177108765,-0.00819675214588642,0.0040686681866645815]|
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |[0.026012470324834187,0.023195965060343344,-0.10863214979569116] |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |[-0.004304863978177309,-0.004591284319758415,0.02117823390290141]|
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |[0.054064739029854536,-0.003801364451646805,0.06522738828789443] |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|[-0.05887459063281615,-0.07891856770341595,0.07510609552264214] |
Word2Vec 特征的示意圖如下,展示了單詞如何被轉換為向量:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00347.jpeg
CountVectorizer
CountVectorizer
用于將一組文本文檔轉換為標記計數的向量,實質上為文檔生成稀疏表示,覆蓋詞匯表。最終結果是一個特征向量,可以傳遞給其他算法。稍后,我們將看到如何在 LDA 算法中使用CountVectorizer
的輸出進行主題檢測。
為了調用CountVectorizer
,你需要導入相關的包:
import org.apache.spark.ml.feature.CountVectorizer
首先,你需要初始化一個CountVectorizer
轉換器,指定輸入列和輸出列。這里,我們選擇由StopWordRemover
創建的filteredWords
列,并生成輸出列特征:
scala> val countVectorizer = new CountVectorizer().setInputCol("filteredWords").setOutputCol("features")
countVectorizer: org.apache.spark.ml.feature.CountVectorizer = cntVec_555716178088
接下來,對輸入數據集調用fit()
函數會生成一個輸出轉換器:
scala> val countVectorizerModel = countVectorizer.fit(noStopWordsDF)
countVectorizerModel: org.apache.spark.ml.feature.CountVectorizerModel = cntVec_555716178088
此外,對輸入數據集調用transform()
函數會生成一個輸出數據集。
scala> val countVectorizerDF = countVectorizerModel.transform(noStopWordsDF)
countVectorizerDF: org.apache.spark.sql.DataFrame = [id: int, sentence: string ... 3 more fields]
以下是輸出數據集,顯示了輸入列 ID、句子以及輸出列特征:
scala> countVectorizerDF.show(false)
|id |sentence |words |filteredWords |features |
|1 |Hello there, how do you like the book so far? |[hello, there,, how, do, you, like, the, book, so, far?] |[hello, there,, like, book, far?] |(18,[1,4,5,13,15],[1.0,1.0,1.0,1.0,1.0])|
|2 |I am new to Machine Learning |[i, am, new, to, machine, learning] |[new, machine, learning] |(18,[6,7,16],[1.0,1.0,1.0]) |
|3 |Maybe i should get some coffee before starting |[maybe, i, should, get, some, coffee, before, starting] |[maybe, get, coffee, starting] |(18,[0,8,9,14],[1.0,1.0,1.0,1.0]) |
|4 |Coffee is best when you drink it hot |[coffee, is, best, when, you, drink, it, hot] |[coffee, best, drink, hot] |(18,[0,3,10,12],[1.0,1.0,1.0,1.0]) |
|5 |Book stores have coffee too so i should go to a book store|[book, stores, have, coffee, too, so, i, should, go, to, a, book, store]|[book, stores, coffee, go, book, store]|(18,[0,1,2,11,17],[1.0,2.0,1.0,1.0,1.0])|
CountVectorizer
的示意圖如下,展示了從StopWordsRemover
轉換中生成的特征:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00205.jpeg
使用 LDA 進行主題建模
LDA 是一種主題模型,它從一組文本文檔中推斷出主題。LDA 可以被看作是一種無監督聚類算法,如下所示:
-
主題對應聚類中心,文檔對應數據集中的行
-
主題和文檔都存在于特征空間中,特征向量是詞計數向量
-
LDA 不是通過傳統的距離估計聚類,而是使用基于文本文檔生成統計模型的函數
為了調用 LDA,你需要導入相關的包:
import org.apache.spark.ml.clustering.LDA
步驟 1. 首先,你需要初始化一個 LDA 模型,設置 10 個主題和 10 次聚類迭代:
scala> val lda = new LDA().setK(10).setMaxIter(10)
lda: org.apache.spark.ml.clustering.LDA = lda_18f248b08480
步驟 2. 接下來,對輸入數據集調用fit()
函數會生成一個輸出轉換器:
scala> val ldaModel = lda.fit(countVectorizerDF)
ldaModel: org.apache.spark.ml.clustering.LDAModel = lda_18f248b08480
步驟 3. 提取logLikelihood
,它計算在推斷的主題下提供的文檔的下界:
scala> val ll = ldaModel.logLikelihood(countVectorizerDF)
ll: Double = -275.3298948279124
步驟 4. 提取logPerplexity
,它計算在推斷的主題下提供的文檔的困惑度上界:
scala> val lp = ldaModel.logPerplexity(countVectorizerDF)
lp: Double = 12.512670220189033
步驟 5. 現在,我們可以使用describeTopics()
來獲取 LDA 生成的主題:
scala> val topics = ldaModel.describeTopics(10)
topics: org.apache.spark.sql.DataFrame = [topic: int, termIndices: array<int> ... 1 more field]
第 6 步。 以下是輸出數據集,展示了 LDA 模型計算出的 topic
、termIndices
和 termWeights
:
scala> topics.show(10, false)
|topic|termIndices |termWeights |
|0 |[2, 5, 7, 12, 17, 9, 13, 16, 4, 11] |[0.06403877783050851, 0.0638177222807826, 0.06296749987731722, 0.06129482302538905, 0.05906095287220612, 0.0583855194291998, 0.05794181263149175, 0.057342702589298085, 0.05638654243412251, 0.05601913313272188] |
|1 |[15, 5, 13, 8, 1, 6, 9, 16, 2, 14] |[0.06889315890755099, 0.06415969116685549, 0.058990446579892136, 0.05840283223031986, 0.05676844625413551, 0.0566842803396241, 0.05633554021408156, 0.05580861561950114, 0.055116582320533423, 0.05471754535803045] |
|2 |[17, 14, 1, 5, 12, 2, 4, 8, 11, 16] |[0.06230542516700517, 0.06207673834677118, 0.06089143673912089, 0.060721809302399316, 0.06020894045877178, 0.05953822260375286, 0.05897033457363252, 0.057504989644756616, 0.05586725037894327, 0.05562088924566989] |
|3 |[15, 2, 11, 16, 1, 7, 17, 8, 10, 3] |[0.06995373276880751, 0.06249041124300946, 0.061960612781077645, 0.05879695651399876, 0.05816564815895558, 0.05798721645705949, 0.05724374708387087, 0.056034215734402475, 0.05474217418082123, 0.05443850583761207] |
|4 |[16, 9, 5, 7, 1, 12, 14, 10, 13, 4] |[0.06739359010780331, 0.06716438619386095, 0.06391509491709904, 0.062049068666162915, 0.06050715515506004, 0.05925113958472128, 0.057946856127790804, 0.05594837087703049, 0.055000929117413805, 0.053537418286233956]|
|5 |[5, 15, 6, 17, 7, 8, 16, 11, 10, 2] |[0.061611492476326836, 0.06131944264846151, 0.06092975441932787, 0.059812552365763404, 0.05959889552537741, 0.05929123338151455, 0.05899808901872648, 0.05892061664356089, 0.05706951425713708, 0.05636134431063274] |
|6 |[15, 0, 4, 14, 2, 10, 13, 7, 6, 8] |[0.06669864676186414, 0.0613859230159798, 0.05902091745149218, 0.058507882633921676, 0.058373998449322555, 0.05740944364508325, 0.057039150886628136, 0.057021822698594314, 0.05677330199892444, 0.056741558062814376]|
|7 |[12, 9, 8, 15, 16, 4, 7, 13, 17, 10]|[0.06770789917351365, 0.06320078344027158, 0.06225712567900613, 0.058773135159638154, 0.05832535181576588, 0.057727684814461444, 0.056683575112703555, 0.05651178333610803, 0.056202395617563274, 0.05538103218174723]|
|8 |[14, 11, 10, 7, 12, 9, 13, 16, 5, 1]|[0.06757347958335463, 0.06362319365053591, 0.063359294927315, 0.06319462709331332, 0.05969320243218982, 0.058380063437908046, 0.057412693576813126, 0.056710451222381435, 0.056254581639201336, 0.054737785085167814] |
|9 |[3, 16, 5, 7, 0, 2, 10, 15, 1, 13] |[0.06603941595604573, 0.06312775362528278, 0.06248795574460503, 0.06240547032037694, 0.0613859713404773, 0.06017781222489122, 0.05945655694365531, 0.05910351349013983, 0.05751269894725456, 0.05605239791764803] |
LDA 的圖示如下,展示了從 TF-IDF 特征中創建的主題:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00175.jpeg
實現文本分類
文本分類是機器學習領域中最廣泛使用的范式之一,廣泛應用于垃圾郵件檢測、電子郵件分類等用例。就像任何其他機器學習算法一樣,工作流由變換器和算法組成。在文本處理領域,預處理步驟如去除停用詞、詞干提取、分詞、n-gram 提取、TF-IDF 特征加權等會發揮作用。一旦所需的處理完成,模型將被訓練以將文檔分類為兩類或更多類。
二分類是將輸入分類為兩個輸出類,如垃圾郵件/非垃圾郵件,或者某個信用卡交易是否為欺詐行為。多類分類可以生成多個輸出類,如熱、冷、冰凍、雨天等。還有一種稱為多標簽分類的技術,它可以根據汽車特征的描述生成多個標簽,如速度、安全性和燃油效率。
為此,我們將使用一個包含 10k 條推文樣本的數據集,并在該數據集上使用上述技術。然后,我們將對文本行進行分詞,去除停用詞,然后使用 CountVectorizer
構建單詞(特征)向量。
接下來我們將數據劃分為訓練集(80%)和測試集(20%),并訓練一個邏輯回歸模型。最后,我們將在測試數據上評估并查看其表現如何。
工作流中的步驟如下圖所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/scl-spk-bgdt-anal/img/00177.jpeg
第 1 步。 加載包含 10k 條推文的輸入文本數據,以及標簽和 ID:
scala> val inputText = sc.textFile("Sentiment_Analysis_Dataset10k.csv")
inputText: org.apache.spark.rdd.RDD[String] = Sentiment_Analysis_Dataset10k.csv MapPartitionsRDD[1722] at textFile at <console>:77
第 2 步。 將輸入行轉換為數據框(DataFrame):
scala> val sentenceDF = inputText.map(x => (x.split(",")(0), x.split(",")(1), x.split(",")(2))).toDF("id", "label", "sentence")
sentenceDF: org.apache.spark.sql.DataFrame = [id: string, label: string ... 1 more field]
第 3 步。 使用帶有空格分隔符的 Tokenizer
將數據轉換為單詞:
scala> import org.apache.spark.ml.feature.Tokenizer
import org.apache.spark.ml.feature.Tokenizerscala> val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
tokenizer: org.apache.spark.ml.feature.Tokenizer = tok_ebd4c89f166escala> val wordsDF = tokenizer.transform(sentenceDF)
wordsDF: org.apache.spark.sql.DataFrame = [id: string, label: string ... 2 more fields]scala> wordsDF.show(5, true)
| id|label| sentence| words|
| 1| 0|is so sad for my ...|[is, so, sad, for...|
| 2| 0|I missed the New ...|[i, missed, the, ...|
| 3| 1| omg its already ...|[, omg, its, alre...|
| 4| 0| .. Omgaga. Im s...|[, , .., omgaga.,...|
| 5| 0|i think mi bf is ...|[i, think, mi, bf...|
第 4 步。 去除停用詞并創建一個新數據框,包含過濾后的單詞:
scala> import org.apache.spark.ml.feature.StopWordsRemover
import org.apache.spark.ml.feature.StopWordsRemoverscala> val remover = new StopWordsRemover().setInputCol("words").setOutputCol("filteredWords")
remover: org.apache.spark.ml.feature.StopWordsRemover = stopWords_d8dd48c9cdd0scala> val noStopWordsDF = remover.transform(wordsDF)
noStopWordsDF: org.apache.spark.sql.DataFrame = [id: string, label: string ... 3 more fields]scala> noStopWordsDF.show(5, true)
| id|label| sentence| words| filteredWords|
| 1| 0|is so sad for my ...|[is, so, sad, for...|[sad, apl, friend...|
| 2| 0|I missed the New ...|[i, missed, the, ...|[missed, new, moo...|
| 3| 1| omg its already ...|[, omg, its, alre...|[, omg, already, ...|
| 4| 0| .. Omgaga. Im s...|[, , .., omgaga.,...|[, , .., omgaga.,...|
| 5| 0|i think mi bf is ...|[i, think, mi, bf...|[think, mi, bf, c...|
第 5 步。 從過濾后的單詞中創建特征向量:
scala> import org.apache.spark.ml.feature.CountVectorizer
import org.apache.spark.ml.feature.CountVectorizerscala> val countVectorizer = new CountVectorizer().setInputCol("filteredWords").setOutputCol("features")
countVectorizer: org.apache.spark.ml.feature.CountVectorizer = cntVec_fdf1512dfcbdscala> val countVectorizerModel = countVectorizer.fit(noStopWordsDF)
countVectorizerModel: org.apache.spark.ml.feature.CountVectorizerModel = cntVec_fdf1512dfcbdscala> val countVectorizerDF = countVectorizerModel.transform(noStopWordsDF)
countVectorizerDF: org.apache.spark.sql.DataFrame = [id: string, label: string ... 4 more fields]scala> countVectorizerDF.show(5,true)
| id|label| sentence| words| filteredWords| features|
| 1| 0|is so sad for my ...|[is, so, sad, for...|[sad, apl, friend...|(23481,[35,9315,2...|
| 2| 0|I missed the New ...|[i, missed, the, ...|[missed, new, moo...|(23481,[23,175,97...|
| 3| 1| omg its already ...|[, omg, its, alre...|[, omg, already, ...|(23481,[0,143,686...|
| 4| 0| .. Omgaga. Im s...|[, , .., omgaga.,...|[, , .., omgaga.,...|(23481,[0,4,13,27...|
| 5| 0|i think mi bf is ...|[i, think, mi, bf...|[think, mi, bf, c...|(23481,[0,33,731,...|
第 6 步。 創建包含標簽和特征的 inputData
數據框:
scala> val inputData=countVectorizerDF.select("label", "features").withColumn("label", col("label").cast("double"))
inputData: org.apache.spark.sql.DataFrame = [label: double, features: vector]
第 7 步。 使用隨機拆分將數據劃分為 80% 的訓練集和 20% 的測試集:
scala> val Array(trainingData, testData) = inputData.randomSplit(Array(0.8, 0.2))
trainingData: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [label: double, features: vector]
testData: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [label: double, features: vector]
第 8 步。 創建一個邏輯回歸模型:
scala> import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.classification.LogisticRegressionscala> val lr = new LogisticRegression()
lr: org.apache.spark.ml.classification.LogisticRegression = logreg_a56accef5728
第 9 步。 通過擬合 trainingData
創建一個邏輯回歸模型:
scala> var lrModel = lr.fit(trainingData)
lrModel: org.apache.spark.ml.classification.LogisticRegressionModel = logreg_a56accef5728scala> lrModel.coefficients
res160: org.apache.spark.ml.linalg.Vector = [7.499178040193577,8.794520490564185,4.837543313917086,-5.995818019393418,1.1754740390468577,3.2104594489397584,1.7840290776286476,-1.8391923375331787,1.3427471762591,6.963032309971087,-6.92725055841986,-10.781468845891563,3.9752.836891070557657,3.8758544006087523,-11.760894935576934,-6.252988307540...scala> lrModel.intercept
res161: Double = -5.397920610780994
第 10 步。 檢查模型摘要,特別是 areaUnderROC
,一個好的模型其值應該是 > 0.90:
scala> import org.apache.spark.ml.classification.BinaryLogisticRegressionSummary
import org.apache.spark.ml.classification.BinaryLogisticRegressionSummaryscala> val summary = lrModel.summary
summary: org.apache.spark.ml.classification.LogisticRegressionTrainingSummary = org.apache.spark.ml.classification.BinaryLogisticRegressionTrainingSummary@1dce712cscala> val bSummary = summary.asInstanceOf[BinaryLogisticRegressionSummary]
bSummary: org.apache.spark.ml.classification.BinaryLogisticRegressionSummary = org.apache.spark.ml.classification.BinaryLogisticRegressionTrainingSummary@1dce712cscala> bSummary.areaUnderROC
res166: Double = 0.9999231930196596scala> bSummary.roc
res167: org.apache.spark.sql.DataFrame = [FPR: double, TPR: double]scala> bSummary.pr.show()
| recall|precision|
| 0.0| 1.0|
| 0.2306543172990738| 1.0|
| 0.2596354944726621| 1.0|
| 0.2832387212429041| 1.0|
|0.30504929787869733| 1.0|
| 0.3304451747833881| 1.0|
|0.35255452644158947| 1.0|
| 0.3740663280549746| 1.0|
| 0.3952793546459516| 1.0|
第 11 步。 使用訓練好的模型轉換訓練集和測試集數據:
scala> val training = lrModel.transform(trainingData)
training: org.apache.spark.sql.DataFrame = [label: double, features: vector ... 3 more fields]scala> val test = lrModel.transform(testData)
test: org.apache.spark.sql.DataFrame = [label: double, features: vector ... 3 more fields]
第 12 步。 計算標簽和預測列匹配的記錄數。它們應該匹配,以便正確評估模型,否則會不匹配:
scala> training.filter("label == prediction").count
res162: Long = 8029scala> training.filter("label != prediction").count
res163: Long = 19scala> test.filter("label == prediction").count
res164: Long = 1334scala> test.filter("label != prediction").count
res165: Long = 617
結果可以放入如下所示的表格中:
數據集 | 總數 | 標簽 == 預測 | 標簽 != 預測 |
---|---|---|---|
訓練 | 8048 | 8029 (99.76%) | 19 (0.24%) |
測試 | 1951 | 1334 (68.35%) | 617 (31.65%) |
雖然訓練數據得到了很好的匹配,但測試數據的匹配率只有 68.35%。因此,仍有改進的空間,可以通過調整模型參數來實現。
邏輯回歸是一種易于理解的方法,它通過輸入的線性組合和以邏輯隨機變量形式存在的隨機噪聲來預測二元結果。因此,邏輯回歸模型可以通過多個參數進行調整。(本章不涉及邏輯回歸模型的所有參數及其調優方法。)
可以用來調整模型的某些參數包括:
-
模型超參數包括以下參數:
-
elasticNetParam
:該參數指定您希望如何混合 L1 和 L2 正則化。 -
regParam
:該參數決定了在傳入模型之前,輸入應該如何進行正則化。
-
-
訓練參數包括以下參數:
-
maxIter
:這是停止前的總交互次數。 -
weightCol
:這是權重列的名稱,用于對某些行進行加權,使其比其他行更重要。
-
-
預測參數包括以下參數:
threshold
:這是二元預測的概率閾值。它決定了給定類別被預測的最低概率。
我們現在已經看到了如何構建一個簡單的分類模型,因此可以根據訓練集為任何新的推文打標簽。邏輯回歸只是可以使用的模型之一。
可以替代邏輯回歸使用的其他模型如下:
-
決策樹
-
隨機森林
-
梯度提升樹
-
多層感知機
總結
在本章中,我們介紹了使用 Spark ML 進行文本分析的世界,重點講解了文本分類。我們了解了 Transformers 和 Estimators。我們看到了如何使用 Tokenizers 將句子分解為單詞,如何去除停用詞,以及生成 n-grams。我們還學習了如何實現HashingTF
和IDF
來生成基于 TF-IDF 的特征。我們還看到了如何使用Word2Vec
將單詞序列轉換為向量。
然后,我們還查看了 LDA,一種常用的技術,用于從文檔中生成主題,而無需深入了解實際文本內容。最后,我們在來自 Twitter 數據集的 1 萬個推文數據集上實施了文本分類,看看如何通過使用 Transformers、Estimators 和 Logistic Regression 模型進行二元分類,將這一切結合起來。
在下一章,我們將進一步深入探討如何調整 Spark 應用程序以獲得更好的性能。