預測是非常困難的,更別提預測未來。
4.1 回歸簡介
隨着現代機器學習和數據科學的出現,我們依舊把從“某些值”預測“另外某個值”的思想稱為回歸。回歸是預測一個數值型數量,比如大小、收入和溫度,而分類則指預測標號或類別,比如判斷郵件是否為“垃圾郵件”,拼圖游戲的圖案是否為“貓”。
將回歸和分類聯系在一起是因為兩者都可以通過一個(或更多)值預測另一個(或多個)值。為了能夠做出預測,兩者都需要從一組輸入和輸出中學習預測規則。在學習的過程中,需要告訴它們問題及問題的答案。因此,它們都屬於所謂的監督學習。
分類和回歸是分析預測中最古老的話題。支持向量機、邏輯回歸、朴素貝葉斯算法、神經網絡和深度學習都屬於分類和回歸技術。
本章將重點關注決策樹算法和它的擴展隨機決策森林算法,這兩個算法靈活且應用廣泛,即可用於分類問題,也可用於回歸問題。更令人興奮的是,它們可以幫助我們預測未來,至少是預測我們尚不肯定的事情。比如,根據線上行為來預測購買汽車的概率,根據用詞預測郵件是否是垃圾郵件,根據地理位置和土壤的化學成分預測哪塊耕地的產量可能更高。
4.2 向量和特征
天氣的特征有:
最低氣溫
最高氣溫
平均氣溫
多雲、有雨、晴朗
特征有時也被稱為維度、預測指標或簡單地稱為變量。本書將特征只分為兩大類:類別型特征和數值型特征。
特征值按順序排列,就是所謂的特征向量。
4.3 樣本訓練
某一天,氣溫12~16攝氏度,濕度10%,晴朗,沒有寒流預報,第二天最高氣溫為17.2攝氏度。
特征向量為學習算法的輸入提供一種結構化的方式(比如輸入:12.5,15.5,0.10,晴朗,0)。預測的輸出(即目標)也被稱為一個特征,它是一個數值特征:17.2。
通常我們把目標作為特征向量的一個附加特征。因此可以把整個訓練樣本列示為:12.5,15.5,0.10,晴朗,0,17.2。所有這些樣本的集合稱為訓練集。
回歸和分析的區別在於:回歸問題的目標為數值型特征,而分類問題的目標為類別型特征。並不是所有的回歸或分類算法都能夠處理類別型特征或類別型目標,有些算法只能處理數值型特征。
4.4 決策樹和決策森林
決策樹算法家族能自然地處理類別型和數值型特征。決策樹算法容易並行化。它們對數據中的離群點具有魯棒性,這意味着一些極端或可能錯誤的數據點根本不會對預測產生影響。算法可以接受不同類型和量綱的數據,對數據類型和尺度不同的情況不需要做預處理和規范化。
決策樹可以推廣為更強大的決策森林算法。本章將會把Spark MLib的DecisionTree和RandomForest算法實現應用到一個數據集上。
基於決策樹的算法還有一個優點,那就是理解和推理起來相對直觀。
4.5 Covtype數據集
下載地址是https://archive.ics.uci.edu/ml/machine-learning-databases/covtype/
或者http://pan.baidu.com/s/1gfkDKlH
covtype.data.gz為壓縮數據文件,附帶一個描述數據文件的信息文件covtype.txt。記錄了美國科羅拉多州不同地塊森林植被特征:海拔、坡度、到水源的距離、遮陽情況和土壤類型,並且隨同給出了地塊的已知森林植被類型。有581012個樣本。
4.6 准備數據
Spark MLib將特征向量抽象為LabeledPoint,它由一個包含對個特征值的Spark MLib Vector和一個稱為標號(label)的目標值組成。該目標為Double類型,而Vector本質上是對多個Double類型值的抽象。這說明LabeledPoint只適用於數值型特征。但只要經過適當編碼,LabeledPoint也可用於類別型特征。
其中一種編碼是one-hot或1-of-n編碼。在這種編碼中,一個有N個不同取值的類別型特征可以變成N個數值型特征,變換后的每個數值型特征的取值為0或1.在這N個特征中,有且只有一個取值為1,其他特征取值都為0。
4.7 第一棵決策樹
開始時我們原樣使用數據。
Scala:
import org.apache.spark.mllib.linalg._ import org.apache.spark.mllib.regression._ val rawData = sc.textFile("D:/Workspace/AnalysisWithSpark/src/main/java/advanced/chapter4/covtype/covtype.data") val data = rawData.map { line => val values = line.split(',').map(_.toDouble) val featureVector = Vectors.dense(values.init) val label = values.last - 1 LabeledPoint(label, featureVector) }
Java:
1 public static JavaRDD<LabeledPoint> readData(JavaSparkContext jsc) { 2 3 JavaRDD<String> rawData =jsc.textFile("src/main/java/advanced/chapter4/covtype/covtype.data"); 4 5 6 7 JavaRDD<LabeledPoint> data = rawData.map(line -> { 8 9 String[] values = line.split(","); 10 11 double[] features = new double[values.length-1]; 12 13 for (int i = 0; i < values.length-1; i++) { 14 15 features[i] = Double.parseDouble(values[i]); 16 17 } 18 19 Vector featureVector = Vectors.dense(features); 20 21 Double label = (double) (Double.parseDouble(values[values.length-1]) - 1); 22 23 return new LabeledPoint(label, featureVector); 24 25 }); 26 27 return data; 28 29 } 30 31
本章將數據分成完整的三部分:訓練集、交叉檢驗集(CV)和測試集。在下面的代碼中你會看到,訓練集占80%,交叉檢驗集和測試集各占10%。
Scala:
val Array(trainData, cvData, testData) =data.randomSplit(Array(0.8, 0.1, 0.1)) trainData.cache() cvData.cache() testData.cache()
Java:
1 //將數據分割為訓練集、交叉檢驗集(CV)和測試集 2 3 JavaRDD<LabeledPoint>[] splitArray = data.randomSplit(new double[]{0.8, 0.1, 0.1}); 4 5 JavaRDD<LabeledPoint> trainData = splitArray[0]; 6 7 trainData.cache(); 8 9 JavaRDD<LabeledPoint> cvData = splitArray[1]; 10 11 cvData.cache(); 12 13 JavaRDD<LabeledPoint> testData = splitArray[2]; 14 15 testData.cache(); 16 17
和ALS實現一樣,DecisionTree實現也有幾個超參數,我們需要為它們選擇值。和之前一樣,訓練集和CV集用於給這些超參數選擇一個合適值。這里第三個數據集,也就是測試集,用於對基於選定超參數的模型期望准確度做無偏估計。模型在交叉檢驗集上的准確度往往有點過於樂觀,不是無偏差的。本章我們還將更進一步,以此在測試集上評估最終模型。
我們先來試試在訓練集上構造一個DecisionTreeModel模型,參數采用默認值,並用CV集來計算結果模型的指標:
Scala:
import org.apache.spark.mllib.evaluation._ import org.apache.spark.mllib.tree._ import org.apache.spark.mllib.tree.model._ import org.apache.spark.rdd._ def getMetrics(model: DecisionTreeModel, data: RDD[LabeledPoint]): MulticlassMetrics = { val predictionsAndLabels = data.map(example => (model.predict(example.features), example.label) ) new MulticlassMetrics(predictionsAndLabels) } val model = DecisionTree.trainClassifier( trainData, 7, Map[Int,Int](), "gini", 4, 100) val metrics = getMetrics(model, cvData)
Java:
1 public static MulticlassMetrics getMetrics(DecisionTreeModel model, JavaRDD<LabeledPoint> data){ 2 3 JavaPairRDD<Object, Object> predictionsAndLabels = data.mapToPair(example -> { 4 5 return new Tuple2<Object, Object>(model.predict(example.features()), example.label()); 6 7 }); 8 9 10 11 return new MulticlassMetrics(JavaPairRDD.toRDD(predictionsAndLabels)); 12 13 } 14 15 16 17 //構建DecisionTreeModel 18 19 DecisionTreeModel model = DecisionTree.trainClassifier(trainData, 7, new HashMap<Integer, Integer>(), "gini", 4, 100); 20 21 22 23 //用CV集來計算結果模型的指標 24 25 MulticlassMetrics metrics = getMetrics(model, cvData); 26 27
這里我們使用trainClassifier,而不是trainRegressor,trainClassifier指示每個LabeledPoint里的目標應該當做不同的類別指標,而不是數值型特征。
和MulticlassMetrics一起,Spark還提供了BinaryClassificationMetrics。它提供類似MulticlassMetrics的評價指標實現,不過僅適用常見的類別型目標只有兩個可能取值的情況。
查看混淆矩陣:
Scala:
metrics.confusionMatrix
Java:
1 //查看混淆矩陣 2 3 System.out.println(metrics.confusionMatrix());
結果:
14395.0 6552.0 13.0 1.0 0.0 0.0 359.0
5510.0 22020.0 447.0 29.0 6.0 0.0 36.0
0.0 413.0 3033.0 82.0 0.0 0.0 0.0
0.0 0.0 175.0 103.0 0.0 0.0 0.0
0.0 897.0 33.0 0.0 15.0 0.0 0.0
0.0 437.0 1210.0 99.0 0.0 0.0 0.0
1148.0 41.0 0.0 0.0 0.0 0.0 822.0
你得到的值可能稍有不同。構造決策樹過程中的一些隨機選項會導致分類結果稍有不同。
矩陣每一行對應一個實際的正確類別值,矩陣每一列按序對應預測值。第i行第j列的元素代表一個正確類別為i的樣本被預測為類別為j的次數。因此,對角線上的元素代表預測正確的次數,而其他元素則代表預測錯誤的次數。對角線上的次數多是好的。但也確實出現了一些分類錯誤的情況,比如上面結果中沒將任何一個樣本類別預測為6。
查看准確度(或者說精確度):
Scala:
metrics.precision
Java:
1 //查看准確度 2 3 System.out.println(metrics.precision());
結果:
0.7022196201332805
每個類別相對其他類別的精確度
Scala:
(0 until 7).map( cat => (metrics.precision(cat), metrics.recall(cat)) ).foreach(println)
Java:
1 //每個類別相對其他類別的精確度 2 3 Arrays.asList(new Integer[]{0,1,2,3,4,5,6}).stream() 4 5 .forEach(cat -> System.out.println(metrics.precision(cat) + "," + metrics.recall(cat)));
結果:
0.6738106679481018,0.6707807118254879
0.7300201914935192,0.7838858581619806
0.6303782269361617,0.8536585365853658
0.4672489082969432,0.3835125448028674
0.0,0.0
0.67,0.03877314814814815
0.6796116504854369,0.4454233969652472
有此可以看到每個類型准確度各有不同。
增加一個准確度評估:
Scala:
import org.apache.spark.rdd._ def classProbabilities(data: RDD[LabeledPoint]): Array[Double] = { val countsByCategory = data.map(_.label).countByValue() val counts = countsByCategory.toArray.sortBy(_._1).map(_._2) counts.map(_.toDouble / counts.sum) } val trainPriorProbabilities = classProbabilities(trainData) val cvPriorProbabilities = classProbabilities(cvData) trainPriorProbabilities.zip(cvPriorProbabilities).map { case (trainProb, cvProb) => trainProb * cvProb }.sum
Java:
1 public static List<Double> classProbabilities(JavaRDD<LabeledPoint> data) { 2 3 //計算數據中每個類的樣本數:(類別,樣本數) 4 Map<Double, Long> countsByCategory = data.map( x -> x.label()).countByValue(); 5 6 //排序 7 List<Map.Entry<Double, Long>> categoryList = new ArrayList<>(countsByCategory.entrySet()); 8 Collections.sort(categoryList, (m1, m2) -> m1.getKey().intValue()-m2.getKey().intValue()); 9 10 //取出樣本數 11 List<Long> counts = categoryList.stream().map(x -> x.getValue()).collect(Collectors.toList()); 12 Double sum = counts.stream().reduce((r, e) -> r = r + e ).get().doubleValue(); 13 return counts.stream().map(x -> x.doubleValue()/sum).collect(Collectors.toList()); 14 } 15 16 17 18 List<Double> trainPriorProbabilities = classProbabilities(trainData); 19 List<Double> cvPriorProbabilities = classProbabilities(cvData); 20 21 //把訓練集和CV集中的某個類別的概率結成對,相乘然后相加 22 List<Tuple2<Double, Double>> mergePriorProbabilities = new ArrayList<>(); 23 Arrays.asList(new Integer[]{0,1,2,3,4,5,6}).stream().forEach(i -> mergePriorProbabilities.add(new Tuple2<Double, Double>(trainPriorProbabilities.get(i), cvPriorProbabilities.get(i)))); 24 System.out.println(mergePriorProbabilities.stream().map(x -> x._1*x._2).reduce((r,e) -> r=r+e).get());
結果:
0.3762360562429304
隨機猜猜的准確度為37%,所以我們前面得到的70%的准確度看起來還不錯。如果在決策樹構建過程中試試超參數的其他值,准確度還可以提高。
4.8 決策樹的超參數
控制決策樹選擇過程的參數為最大深度、最大桶數和不純性度量。
最大深度只是對決策樹的層數做出限制。限制判斷次數有利於避免對訓練數據產生過擬合。
好規則把訓練集數據的目標值分為相對是同類或“純”(pure)的子集。選擇最好的規則也就意味着最小化規則對應的兩個子集的不純性。不純性有兩種常用度量方式:Gini不純度或熵。
Gini不純度直接和隨機猜測分類器的准確度相關。熵是另一種度量不純性的方式,它來源於信息論。Spark的實現默認采用Gini不純度。
4.9 決策樹調優
我們取不同的參數進行測驗:
Scala:
val evaluations = for (impurity <- Array("gini", "entropy"); depth <- Array(1, 20); bins <- Array(10, 300)) yield { val model = DecisionTree.trainClassifier( trainData, 7, Map[Int,Int](), impurity, depth, bins) val predictionsAndLabels = cvData.map(example => (model.predict(example.features), example.label) ) val accuracy = new MulticlassMetrics(predictionsAndLabels).precision ((impurity, depth, bins), accuracy) } evaluations.sortBy(_._2).reverse.foreach(println)
Java新建一個類:Hyperparameters進行測試
Java:
1 //決策樹調優 2 3 List<Tuple2<Tuple3<String, Integer, Integer>, Double>> evaluations = new ArrayList<>(); 4 5 6 7 String[] impuritySet = new String[]{"gini", "entropy"}; 8 Integer[] depthSet = new Integer[]{1, 20}; 9 Integer[] binsSet = new Integer[]{10, 300}; 10 for (String impurity : impuritySet) { 11 for (Integer depth : depthSet) { 12 for (Integer bins : binsSet) { 13 //構建DecisionTreeModel 14 DecisionTreeModel model = DecisionTree.trainClassifier(trainData, 7, new HashMap<Integer, Integer>(), impurity, depth, bins); 15 //用CV集來計算結果模型的指標 16 MulticlassMetrics metrics = getMetrics(model, cvData); 17 18 evaluations.add(new Tuple2<Tuple3<String, Integer, Integer>, Double>(new Tuple3<String, Integer, Integer>(impurity, depth, bins), metrics.precision())); 19 } 20 } 21 } 22 23 Collections.sort(evaluations, (m1, m2) -> (int)((m2._2-m1._2)*1000)); 24 evaluations.forEach(x -> System.out.println(x._1._1() + "," + x._1._2() + "," + x._1._3() + "," + x._2));
結果:
entropy,20,300,0.909348831610984
gini,20,300,0.9034338084839314
entropy,20,10,0.8934436095396943
gini,20,10,0.8906752411575563
gini,1,10,0.6348504909125299
gini,1,300,0.634283061368365
entropy,1,10,0.4892962154168888
entropy,1,300,0.4892962154168888
前面的測試表明,目前最佳選擇是:不純性度量采用熵,最大深度為20,桶數為300,這時准確度為90.9%。
想要真正評估這個最佳模型在將來的樣本上的表現,當然需要在沒有用於訓練的樣本上進行評估。也就是使用第三個子集進行測試:
Scala:
val model = DecisionTree.trainClassifier( trainData.union(cvData), 7, Map[Int,Int](), "entropy", 20, 300)
Java:
1 DecisionTreeModel modelR = DecisionTree.trainClassifier(trainData.union(cvData), 7, new HashMap<Integer, Integer>(), "entropy", 20, 300); 2 MulticlassMetrics metricsR = getMetrics(modelR, testData); 3 System.out.println(metricsR.precision());
結果:
0.91562526784949
結果准確度為91.6%。估計還算可靠吧。決策樹在一定程度上存在對訓練數據的過擬合。減小最大深度可能會使過擬合問題有所改善。
4.10 重談類別型特征
數據中類別型特征使用one-hot編碼,這種編碼迫使決策樹算法在底層要單獨考慮類別型特征的每一個值,增加內存使用量並且減慢決策速度。我們取消one-hot編碼:
Scala:
val data = rawData.map { line => val values = line.split(',').map(_.toDouble) val wilderness = values.slice(10, 14).indexOf(1.0).toDouble val soil = values.slice(14, 54).indexOf(1.0).toDouble val featureVector = Vectors.dense(values.slice(0, 10) :+ wilderness :+ soil) val label = values.last - 1 LabeledPoint(label, featureVector) } val evaluations = for (impurity <- Array("gini", "entropy"); depth <- Array(10, 20, 30); bins <- Array(40, 300)) yield { val model = DecisionTree.trainClassifier( trainData, 7, Map(10 -> 4, 11 -> 40), impurity, depth, bins) val trainAccuracy = getMetrics(model, trainData).precision val cvAccuracy = getMetrics(model, cvData).precision ((impurity, depth, bins), (trainAccuracy, cvAccuracy)) }
復制Hyperparameters命名為CategoricalFeatures,修改readData和決策樹參數。
Java:
1 public static JavaRDD<LabeledPoint> readData(JavaSparkContext jsc) { 2 JavaRDD<String> rawData =jsc.textFile("src/main/java/advanced/chapter4/covtype/covtype.data"); 3 4 JavaRDD<LabeledPoint> data = rawData.map(line -> { 5 String[] values = line.split(","); 6 double[] features = new double[12]; 7 for (int i = 0; i < 10; i++) { 8 features[i] = Double.parseDouble(values[i]); 9 } 10 for (int i = 10; i < 14; i++) { 11 if(Double.parseDouble(values[i]) == 1.0){ 12 features[10] = i-10; 13 } 14 } 15 16 for (int i = 14; i < 54; i++) { 17 if(Double.parseDouble(values[i]) == 1.0){ 18 features[11] = i-14; 19 } 20 } 21 22 Vector featureVector = Vectors.dense(features); 23 Double label = (double) (Double.parseDouble(values[values.length-1]) - 1); 24 return new LabeledPoint(label, featureVector); 25 }); 26 return data; 27 } 28 29 30 31 List<Tuple2<Tuple3<String, Integer, Integer>, Double>> evaluations = new ArrayList<>(); 32 Map<Integer, Integer> map = new HashMap<Integer, Integer>(); 33 map.put(10, 4); 34 map.put(11, 40); 35 String[] impuritySet = new String[]{"gini", "entropy"}; 36 Integer[] depthSet = new Integer[]{10, 20, 30}; 37 Integer[] binsSet = new Integer[]{40, 300}; 38 for (String impurity : impuritySet) { 39 for (Integer depth : depthSet) { 40 for (Integer bins : binsSet) { 41 //構建DecisionTreeModel 42 DecisionTreeModel model = DecisionTree.trainClassifier(trainData, 7, map, impurity, depth, bins); 43 //用CV集來計算結果模型的指標 44 MulticlassMetrics metrics = getMetrics(model, cvData); 45 46 evaluations.add(new Tuple2<Tuple3<String, Integer, Integer>, Double>(new Tuple3<String, Integer, Integer>(impurity, depth, bins), metrics.precision())); 47 } 48 } 49 } 50 51 Collections.sort(evaluations, (m1, m2) -> (int)((m2._2-m1._2)*1000)); 52 evaluations.forEach(x -> System.out.println(x._1._1() + "," + x._1._2() + "," + x._1._3() + "," + x._2)); 53 54
結果:
entropy,30,300,0.9420165175498968
entropy,30,40,0.9400378527185134
gini,30,40,0.9340674466620784
gini,30,300,0.9330006882312457
gini,20,40,0.9227288368891947
entropy,20,40,0.923554714384033
entropy,20,300,0.9233138334480385
gini,20,300,0.9211286992429456
gini,10,40,0.7867859600825877
gini,10,300,0.7862869924294563
entropy,10,40,0.7863214039917412
entropy,10,300,0.779525120440468
准確度為94.2%。比以前更好。
4.11 隨機決策森林
隨機決策森林是由多個決策樹獨立構造而成。
Scala:
val forest = RandomForest.trainClassifier( trainData, 7, Map(10 -> 4, 11 -> 40), 20, "auto", "entropy", 30, 300)
Java:
1 Map<Integer, Integer> map = new HashMap<Integer, Integer>(); 2 map.put(10, 4); 3 map.put(11, 40); 4 5 //構建RandomForestModel 6 RandomForestModel model = RandomForest.trainClassifier(trainData, 7, map, 20, "auto", "entropy", 30, 300, Utils.random().nextInt()); 7 8 //用CV集來計算結果模型的指標 9 MulticlassMetrics metrics = getMetrics(model, cvData); 10 System.out.println(metrics.precision());
結果:
0.964827502890772
准確率96.5%。
在大數據背景下,隨機決策森林非常有吸引力,因為決策樹往往是獨立構造的,諸如Spark和MapReduce這樣的大數據技術本質上適合數據並行問題。
4.12 進行預測
Scala:
val input = "2709,125,28,67,23,3224,253,207,61,6094,0,29" val vector = Vectors.dense(input.split(',').map(_.toDouble)) forest.predict(vector)
Java:
1 double[] input = new double[] {(double) 2709,(double) 125,(double) 28,(double) 67,(double) 23,(double) 3224,(double) 253,(double) 207,(double) 61,(double) 6094,(double) 0,(double) 29}; 2 Vector vector = Vectors.dense(input); 3 System.out.println(model.predict(vector));
結果:
4.0
4.0對應原始Covtype數據集中的類別5(元素特征從1開始)。(在本地算了半個小時 ㄒoㄒ )。
4.13 小結
本章介紹分類和回歸。介紹概念:特征、向量、訓練和交叉檢驗。
一般情況,准確度超過95%是很難達到的。通常,通過包括更多特征,或將已有特征轉換成預測性更好的形式,我們可以進一步提高准確度。
分類和回歸算法不只包括決策樹和決策森林,Spark Mlib 實現的算法也不限於決策樹和決策森林。對分類問題,Spark MLib提供的實現包括:
朴素貝葉斯
支持向量機
邏輯回歸
他們接受一個LabeledPoint類型的RDD作為輸入,需要通過將輸入數據划分為訓練集、交叉檢驗集和測試集來選擇超參數。