本文理論部份基于Andew Ng的公開(kāi)課遍膜,工程實(shí)踐來(lái)自 Spark ML捷雕。我是個(gè)愚鈍的人叁鉴,純理論學(xué)不動(dòng)。
Linear Regression
機(jī)器學(xué)習(xí)是一個(gè)歸納與演繹的過(guò)程江醇,根據(jù)已有數(shù)據(jù)集濒憋,訓(xùn)練出模型(歸納),去預(yù)測(cè)未知的世界(演繹)陶夜。在公開(kāi)課里以波士頓房?jī)r(jià)為第一個(gè)例子凛驮,我在趕集房產(chǎn)頻道爬了部份小區(qū)數(shù)據(jù),用于學(xué)習(xí)
總價(jià)(w) | 面積(m^2) | 臥室 |
---|---|---|
498 | 125 | 3 |
370 | 87 | 2 |
498 | 125 | 3 |
510 | 116 | 2 |
1185 | 350 | 5 |
上面只是部分?jǐn)?shù)據(jù)条辟,房?jī)r(jià)一般和地理位置黔夭,面積,朝向羽嫡,是否學(xué)區(qū)相關(guān)本姥,這些指標(biāo)統(tǒng)稱(chēng)為 feature , 如果只考濾面積這個(gè)特征,我們畫(huà)一下數(shù)據(jù)分布圖
如果我們?cè)侔雅P室這個(gè) feature 考慮進(jìn)去杭棵,那么新的散點(diǎn)圖就是三維的
對(duì)于多維 N 個(gè)特征婚惫,雖然無(wú)法對(duì)應(yīng)現(xiàn)實(shí)世界物理模型,但是可以對(duì)應(yīng)矩陣魂爪,大家腦補(bǔ)
公式推導(dǎo)
對(duì)于只有一個(gè)特征的線性公式
h(x)=??0 +??1??1
簡(jiǎn)書(shū)沒(méi)有數(shù)學(xué)公式編輯先舷,上面??0中的0是小標(biāo),實(shí)際上應(yīng)該是??0??0滓侍,但是我們默認(rèn)第0個(gè)特征值為1蒋川,即 ??0=1,所以可以如此簡(jiǎn)寫(xiě)撩笆。
h(x)=??0 +??1??1 +??2??2 =∑???????? =??T??
對(duì)于擁有多個(gè)特征的線性公式也很好理解尔破,其中??和??均為列向量,??T是??的轉(zhuǎn)置向量浇衬,T是??的上標(biāo)。函數(shù) h(x) 可以表示為兩個(gè)向量的點(diǎn)積(內(nèi)積)餐济,我們最終就是要解出列向量??耘擂,使得這條直線更合理。
這里明顯有個(gè)離群點(diǎn)絮姆,[170,96 3] 這組數(shù)據(jù)異常醉冤,涉及到數(shù)據(jù)抽取和清洗秩霍,暫時(shí)不提。
如何衡量哪組??更合理
我們定義一個(gè)函數(shù)蚁阳,叫做 cost function , 或是 loss function . 這個(gè)函數(shù)來(lái)衡量某一特定??時(shí)铃绒,所擬合的失真程度,這個(gè)程度越小越合理螺捐。
J(θ) = 1/2 ∑(h??(??(??)) ? ??(??))^2
其中 J 是向量??的函數(shù)颠悬,h??(??(??)) 為給定??預(yù)測(cè)出的??(??)值。函數(shù)為預(yù)測(cè)值與對(duì)應(yīng)真實(shí)值差的平方和定血,最后要乘以1/2, 方便求導(dǎo)時(shí)使系統(tǒng)為1.
初始默認(rèn) ?? 是一個(gè)N維的零(列)向量赔癌,那么此時(shí) J 肯定非常大,目的就是找到一組 ?? 使這個(gè)損失函數(shù) J 取最小值澜沟。所以我們需要每次改變?? 值灾票,不斷試錯(cuò)。
上圖迭代了四次茫虽,終于找到最優(yōu)解刊苍。
梯度下降
公開(kāi)課使用梯度下降法求近似解,求 θ 最終變成了求 J(θ) 極小值濒析。梯度方向由 J(θ) 對(duì) θ 的偏導(dǎo)數(shù)確定正什,由于極小值,所以為偏導(dǎo)數(shù)的反方向悼枢。
θj :=θj ?α ( J(θ))'
公式中 θj 是每次迭代前的值減去一個(gè)變量埠忘,α 稱(chēng)為學(xué)習(xí)速率,這個(gè)值不能過(guò)大馒索,可能跳過(guò)極值莹妒,過(guò)小也會(huì)影響收斂速度。( J(θ))' 是 J(θ) 對(duì) θ 的偏導(dǎo)數(shù)(沒(méi)找到好的公式編輯器)绰上。當(dāng)我們只考濾一個(gè)樣本的時(shí)候旨怠,對(duì)J(θ)鏈?zhǔn)角髮?dǎo)
由上圖可知導(dǎo)數(shù)結(jié)果是一個(gè)矢量與向量積。h??(??) ? ?? 是估計(jì)值與真實(shí)值之差蜈块,??j 是當(dāng)前樣本特征值鉴腻,為列量向。同理推導(dǎo)出當(dāng)我們有 m 個(gè)樣本時(shí)的梯度為:
θj:=θj+α∑(y(i)?hθ(x(i)))x(i)
就是說(shuō)我們每次迭代 θ百揭,迭代值 α∑(y(i)?hθ(x(i)))x(i), 這塊初次接觸有點(diǎn)繞口爽哎,通過(guò)閱讀源碼加深理解。
Spark 訓(xùn)練模型
Python 也有很好的機(jī)器學(xué)習(xí)庫(kù)器一,相比更通用適合學(xué)習(xí)课锌。Spark mllib 也比較成熟,借助 RDD 可以實(shí)現(xiàn)大數(shù)據(jù)分布式計(jì)算,訓(xùn)練模型效率更高效渺贤。先來(lái)看看用 Spark 實(shí)現(xiàn)例子中的 Linear Regression
準(zhǔn)備數(shù)據(jù)
數(shù)據(jù)格式為 Label, feature1,feature2,feature3.... 本文中只有2個(gè)特征雏胃,其中術(shù)語(yǔ) Label 對(duì)應(yīng) price, feature1 對(duì)應(yīng)面積,feature2 對(duì)應(yīng)臥室數(shù)量志鞍。
498,125 3
670,141 3
600,137 3
650,150 3
打開(kāi) spark-shell 加載數(shù)據(jù)
scala> val data = sc.textFile("/Users/dzr/code/spark-mllib-data/house.data")
data: org.apache.spark.rdd.RDD[String] = /Users/dzr/code/spark-mllib-data/house.data MapPartitionsRDD[1] at textFile at <console>:27
加載數(shù)據(jù)文件瞭亮,生成 RDD[String],生產(chǎn)環(huán)境數(shù)據(jù)一般從HDFS中獲取固棚,本機(jī)只是用來(lái)演示和訓(xùn)練模型统翩。加載數(shù)據(jù)后,要把數(shù)據(jù)做特殊處理玻孟。
scala> import org.apache.spark.mllib.regression.LabeledPoint
scala> import org.apache.spark.mllib.linalg.Vectors
scala> val trainSet = data.map {line=>
val parts = line.split(',')
LabeledPoint(parts(0).toDouble,Vectors.dense(parts(1).split(' ').map(_.toDouble)))}
trainSet: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[3] at map at <console>:31
scala> trainSet.take(2).foreach(println)
(498.0,[125.0,3.0])
(670.0,[141.0,3.0])
Spark 線性回歸術(shù)語(yǔ)
- SGD: Stochastic Gradient Descent 隨機(jī)梯度下降
- LabeledPoint: Class that represents the features and labels of a data point, 線性回歸屬于監(jiān)督學(xué)習(xí)唆缴,在給定觀測(cè)特征對(duì)應(yīng)的值叫做 label
- Weight: 權(quán)重是一個(gè)列向量,就是我們要求得的 ??
- Intercept: 簡(jiǎn)單的理解為 y 軸的截距黍翎,即當(dāng)各特征均為零值時(shí)的觀測(cè)默認(rèn)值面徽。這項(xiàng)的意義在于,他能捕捉到未觀察到的誤差(比如我們可能忽略了其它有影響的特征)
失敗的建模
scala> import org.apache.spark.mllib.regression._
scala> val numIterations = 100 // 設(shè)置迭代次數(shù)
numIterations: Int = 100
scala> val stepSize = 1 // 每次迭代步長(zhǎng)
stepSize: Int = 1
scala> val minBatchFraction = 1.0 // 樣本參與迭代比例
minBatchFraction: Double = 1.0
scala> val model = LinearRegressionWithSGD.train(trainSet, numIterations,stepSize,minBatchFraction)
model: org.apache.spark.mllib.regression.LinearRegressionModel = org.apache.spark.mllib.regression.LinearRegressionModel: intercept = 0.0, numFeatures = 2
scala> model.weights
res48: org.apache.spark.mllib.linalg.Vector = [NaN,NaN]
scala> model.intercept
res49: Double = 0.0
很詭異的事情誕生了匣掸,model.weights 列向量居然是 NaN, 也就是說(shuō)不是有效的 double 浮點(diǎn)數(shù)趟紊,model.intercept 是0.0也不很正常。為什么用 Matlab 就能擬合出結(jié)果碰酝,而 Spark 就失敗了霎匈?
學(xué)習(xí)速率
咨詢(xún)了(靚麗青春無(wú)敵的美少女學(xué))同事,給兩個(gè)建義:特征數(shù)據(jù)做歸一化處理送爸,調(diào)整學(xué)習(xí)速率铛嘱。首先做的是歸一化處理,將特征標(biāo)準(zhǔn)差標(biāo)準(zhǔn)化(均值0, 標(biāo)準(zhǔn)差1)袭厂,還是得到 NaN 值墨吓。然后調(diào)整 stepSize ,從1, 0.1, 0.01, 0.001,0.0001開(kāi)始測(cè)試纹磺,終于在0.001時(shí)得到擬合值帖烘。
scala> val model = LinearRegressionWithSGD.train(trainSet, 100,0.0001)
model: org.apache.spark.mllib.regression.LinearRegressionModel = org.apache.spark.mllib.regression.LinearRegressionModel: intercept = 0.0, numFeatures = 2
scala> model.weights //擬合
res227: org.apache.spark.mllib.linalg.Vector = [4.225755672693632,0.09458364813972062]
scala> model.predict(new DenseVector(Array(98,3))) // 進(jìn)行預(yù)測(cè),98平橄杨,3居的房子大概414萬(wàn)人民幣
res228: Double = 414.4078068683951
這里還有個(gè)問(wèn)題秘症,在迭代次數(shù)不變,不斷調(diào)小 stepSize 時(shí)式矫,會(huì)取不到極小值乡摹。當(dāng)數(shù)據(jù)已經(jīng)做歸一化處理時(shí),學(xué)習(xí)速率可以稍大一些采转。最后預(yù)測(cè)房?jī)r(jià)函數(shù)為
h(x)= 4.22 * ??1 + 0.09 * ??2
可以直觀的看到房?jī)r(jià)每平4.22W趟卸,并且和第二個(gè)特征rooms關(guān)系不大。
loss cost
擬合的效果好不好,最終要看損失值和均方根誤差
scala> val prediction = model.predict(trainSet.map(_.features))
prediction: org.apache.spark.rdd.RDD[Double] = MapPartitionsRDD[10732] at mapPartitions at GeneralizedLinearAlgorithm.scala:69
scala> val predictionAndLabel = prediction.zip(trainSet.map(_.label))
predictionAndLabel: org.apache.spark.rdd.RDD[(Double, Double)] = ZippedPartitionsRDD2[10734] at zip at <console>:45
scala> for(i<-0 to prediction.collect.length -1) {
| println(predictionAndLabel.collect()(i)._1 + "\t" + predictionAndLabel.collect()(i)._2)}
scala> val loss = predictionAndLabel.map {
| case (p, l) =>
| val err = p - l
| err * err }.reduce(_+_)
loss: Double = 13322.402139229554
scala> val rmse = math.sqrt(loss/prediction.collect.length)
rmse: Double = 28.85567766838698
預(yù)測(cè)值 真實(shí)值
528.5032100311231 498.0
596.1153007942212 670.0
579.2122781034467 600.0
634.1471018484639 650.0
295.99206438483367 320.0
579.2122781034467 550.0
可以看到 rmse 為28.8
小結(jié)
邊學(xué)邊動(dòng)手實(shí)踐還是蠻快的锄列,終于看到了ML的冰山一角。還有分類(lèi)算法什么的惯悠,慢慢看吧邻邮。