附Spark案例 | 推05,論推薦系統之經典,還得數協同

附Spark案例 | 推05,論推薦系統之經典,還得數協同

來自專欄 數據蟲巢

作者 | HCY崇遠

本文為《數據蟲巢,推薦系統系列》第五篇,也是案例實踐的第三篇,不懂前情提要的,請關注公號『數據蟲巢』(ID:blogchong),閱讀《推01,你們是不是都感覺自己少了個推薦系統?》、《推02,就算是非技術人員也都有必要了解的一些推薦系統常識》、《推03,最最最簡單的推薦系統是什麼樣的 | 附Spark實踐案例》、《附Spark案例 | 推04,融合了用戶興趣的推薦系統才更具個性》。

接上一篇文章,我們大致Get到了一個點,那就是如果要達到推薦個性化的目的,核心還是用戶的行為數據,只有用戶各自的行為數據才能反饋其與其他人所不一樣的特性,從而有針對性的進行推薦。按上個章節的原話,大致就是這樣的:

實際上基於用戶畫像的個性化推薦依然是有缺陷的,比如他不會做用戶興趣的升級,而實際上一些知識本身就是具有一定的階梯性的。

舉個例子就很容易理解了,比如,你對大數據的東西很感興趣,於是系統根據你的興趣偏好天天給你推Hadoop、大數據各種技術框架等信息,在某個時間段可能是合理,比如我對大數據領域已經熟知了呢?你還給我天天推送大數據相關的信息。

而我實際上是需要尋求大數據關聯的信息,甚至是升級的信息,比如基於大數據的機器學習、數據挖掘相關的東西,這個機制是無法做到這一層的。

說白了其實就是基於用戶畫像的推薦,他無法發現新知識,所謂新知識就是,與你之前的興趣愛好相對比,推薦的候選集永遠圈定在你的興趣標籤維度內,做不到認知的升級,而實際上認知是會進行升級的,特別是隨著你捕獲的知識信息越多的情況下,你就越會對更上層的其他知識感興趣,不斷的深入下去。

而基於協同過濾的推薦,或多或少能解決一點這類問題,最起碼能夠結合本身用戶的行為,讓你觸達新的知識信息,並且這種遞進是通過協同關係得到的,意味著是大部分人的共同選擇,所以還是具有一定合理性的。

01 協同過濾原理先過一遍

對於基於協同過濾的推薦,可謂是推薦系統中的經典推薦演算法了,記得好像就是亞馬遜推廣出來的,然後大放光彩。協同過濾又分為基於用戶的協同(UserCF)、基於物品的協同(ItemCF),以及基於模型的協同(ModelCF)。

基於用戶的協同過濾推薦(UserCF)。

基於用戶的協同過濾,即我們希望通過用戶之間的關係來達到推薦物品的目的,於是,給某用戶推薦物品,即轉換為尋找為這個用戶尋找他的相似用戶,然後相似用戶喜歡的物品,那麼也可能是這個用戶喜歡的物品(當然會去重)。

來看一個表格:

//其中Y表示對應用戶喜歡對應物品,-表示無交集,?表示需不需要推薦。

這是一個最簡單的例子,其實目的很簡單,我們需要給用戶A推薦物品,而且可以看到,用戶已經喜歡了物品A和物品C,其實剩下也就B和D了,要麼是B,要麼是D。那麼根據UserCF演算法,我們先計算用戶A與用戶BC之間的相似度,計算相似,我們前文說了,要麼距離,要麼餘弦夾角。

假如我們選擇計算夾角(四維):cosAB=0(90度的夾角),cosAC=0.8199(角度自己算吧)。所以相比來說,我們會發現用戶A與用戶C的相似度是稍微大一些的。於是,我們觀察用戶C都喜歡了哪些物品,然後與用戶的去重,然後會發現該給用戶A推薦物品D。

簡單來講,UserCF就是如上過程,但在實際的過程中,數據量肯定不止這麼點,於是我們需要做的是為用戶計算出相似用戶列表,然後在相似用戶中經過去重之後,計算一個推薦的物品列表(在計算推薦物品的時候,可以疊加用戶的相似程度進一步疊加物品的權重)。

然後在喜歡物品的表達形式上,可以是如上的這種二值分類,即Yes Or No,也可以是帶有評分的程度描述,比如對於某個物品打多少分的這種表現形式。這樣的話,針對於後一種情況,我們就需要在求在計算相似度時,加入程度的權重考量。

基於物品的協同推薦(ItemCF)

不同於基於用戶的協同,這裡,我們計算的是物品之間的相似度,但是,請注意,我們計算物品相似度的時候,與直接基於物品相似度推薦不同是,我們所用的特徵並不是物品的自身屬性,而依然是用戶行為。

//其中Y表示對應用戶喜歡對應物品,-表示無交集,?表示需不需要推薦。

同樣,這是一個簡單實例。目的也明確,我們在知道用戶AB喜歡某些物品情況,以及在用戶C已經喜歡物品C的前提下,為用戶C推薦一個物品。看錶格很簡單嘛。只有兩個選項,要麼物品B,要麼物品C。那麼到底是物品B還是物品C呢?

我們來計算物品A與其他兩種物品的相似度,計算向量夾角。對於用戶A,物品A與物品B,則對於AB向量為(1,0),(1,1),對於AC向量為(1,1),(1,1),分別計算夾角cosAB=0.7,cosAC=1。或者用類似關聯規則的方法,計算兩者之間的共現,例如AB共現1次,AC共現2次。通過類似這種方式,我們就知道物品A與物品C在某種程度上是更相似的。

我要說的就是類似共現類做計算的這種方式,在大規模數據的情況下是很有效的一種方式,基於統計的方法在數據量足夠的時候,更能體現問題的本質。

基於模型的協同推薦(ModelCF)。

除了我們熟悉的基於用戶以及基於物品的協同,還有一類,基於模型的協同過濾。基於模型的協同過濾推薦,基於樣本的用戶偏好信息,訓練一個模型,然後根據實時的用戶喜好信息進行預測推薦。常見的基於模型推薦又有三種:最近鄰模型,典型如K最近鄰;SVD模型,即矩陣分解;圖模型,又稱為社會網路圖模型。

(1) 最近鄰模型

最近鄰模型,即使用用戶的偏好信息,我們計算當前被推薦用戶與其他用戶的距離,然後根據近鄰進行當前用戶對於物品的評分預測。

典型如K最近鄰模型,假如我們使用皮爾森相關係數,計算當前用戶與其他所有用戶的相似度sim,然後在K個近鄰中,通過這些相似用戶,預測當前用戶對於每一個物品的評分,然後重新排序,最終推出M個評分最高的物品推薦出去。需要注意的是,基於近鄰的協同推薦,較依賴當前被推薦用戶的歷史數據,這樣計算出來的相關度才更準確。

(2) SVD矩陣分解

我們把用戶和物品的對應關係可以看做是一個矩陣X,然後矩陣X可以分解為X=A*B。而滿足這種分解,並且每個用戶對應於物品都有評分,必定存在與某組隱含的因子,使得用戶對於物品的評分逼近真實值,而我們的目標就是通過分解矩陣得到這些隱性因子,並且通過這些因子來預測還未評分的物品。

有兩種方式來學習隱性因子,一為交叉最小二乘法,即ALS;而為隨機梯度下降法。首先對於ALS來說,首先隨機化矩陣A,然後通過目標函數求得B,然後對B進行歸一化處理,反過來求A,不斷迭代,直到A*B滿足一定的收斂條件即停止。

對於隨機梯度下降法來說,首先我們的目標函數是凹函數或者是凸函數,我們通過調整因子矩陣使得我們的目標沿著凹函數的最小值,或者凸函數的最大值移動,最終到達移動閾值或者兩個函數變化絕對值小於閾值時,停止因子矩陣的變化,得到的函數即為隱性因子。

使用分解矩陣的方式進行協同推薦,可解釋性較差,但是使用RMSE(均方根誤差)作為評判標準,較容易評判。

並且,我們使用這種方法時,需要儘可能的讓用戶覆蓋物品,即用戶對於物品的歷史評分記錄需要足夠的多,模型才更準確。

(3) 社會網路圖模型

所謂社會網路圖模型,即我們認為每個人之間都是有聯繫的,任何兩個用戶都可以通過某種或者多個物品的購買行為而聯繫起來,即如果一端的節點是被推薦用戶,而另一端是其他用戶,他們之間通過若干個物品,最終能聯繫到一起。

而我們基於社會網路圖模型,即研究用戶對於物品的評分行為,獲取用戶與用戶之間的圖關係,最終依據圖關係的距離,為用戶推薦相關的物品。

目前這種協同推薦使用的較少。

02 基於Spark的ALS協同過濾推薦案例

老規矩,大致過完了理論,我們來走一遭代碼實踐,數據源的解釋不就多說了,依然還是那份電影數據,不清楚的見上一篇《推03,最最最簡單的推薦系統是什麼樣的 | 附Spark實踐案例》的說明,這次我們只用到涉及到評分的數據,共100萬條,我們通過評分行為來做協同過濾。

截止Spark2.X系列,Spark的MlLib只實現了基於矩陣分解的協同(也就是經典的基於ALS協同過濾),沒有實現更常規的基於物品或者基於用戶的協同過濾,但從上面的原理我們知道,其實基於物品基於用戶的協同核心就在於構建基礎向量矩陣以及計算相似的兩個方面,我這邊也是實現了,但基於篇幅這裡,就只介紹基於ALS的實踐過程了,其他兩個案例,需要的話請聯繫我(別忘了紅包 哈哈)。

由於MlLib實現了演算法模型,所以從敲代碼的維度上來說,代碼量反而會遠遠低於基於用戶、基於物品的協同,甚至會少於之前的基於物品相似或者基於用戶畫像的推薦了,順帶說一句,基於ALS的推薦代碼,其實網上很容易找,演算法MlLib中的經典演算法了,很多人都實現了,不過萬變不離其宗(變個毛線,API介面就那幾個,參數也就那幾個,能怎麼變)。

先Hive數據表中,將rating評分數據取出來(當然,如果你的機子跑不動,就limit一下簡單取些數,跑通模型就得啦)。

val ratingDataOrc = sparkSession.sql("select userid,movieid,rate,timestame from mite8.mite_ratings limit 50000")

將取出的評分數據,以時間構建Key-value鍵值對,形成(Int,Ratings)格式的數據,其實這是一個中間處理過程,方便後續的數據輸入。

val ratings = ratingDataOrc.rdd.map(f => (java.lang.Long.parseLong(f.get(3).toString)%10, Rating(java.lang.Integer.parseInt(f.get(0).toString), java.lang.Integer.parseInt(f.get(1).toString), f.get(2).asInstanceOf[java.math.BigDecimal].doubleValue())))

這裡,鑒於計算能力,我就不進行全局用戶的候選集推薦計算了,只拿ID=1的用戶當成實驗,獲取ID=1的用戶候選推薦列表,先取該用戶的行為數據。

val personalRatingsData = ratingDataOrc.where("userid = 1").rdd.map{ f=> Rating(java.lang.Integer.parseInt(f.get(0).toString), java.lang.Integer.parseInt(f.get(1).toString), f.get(2).asInstanceOf[java.math.BigDecimal].doubleValue())}

基於上上面的K-V中間數據,我們以取余的方式,將數據分成6:2:2,三個比例,分別進行模型訓練,數據校驗,以及結果測試。

val training = ratings.filter(x => x._1 < 6).values .union(personalRatingsData).repartition(numPartions).persist()val validation = ratings.filter(x => x._1 >=6 && x._1 < 8).values .repartition(numPartions).persist()val test = ratings.filter(x => x._1 > 8).values.persist()

ALS的推薦效果評估,一般我們是以均方根差來離線衡量推薦的準確度,所以,這裡涉及到了ALS參數調優的問題,我們通過數據來最終確定參數,並確定最終的Model,分別取ranks、lambdas、numIters作為調優對象。

var count = 0//進行三層循環遍歷,找最佳的Rmse值,對應的modelfor (rank <- ranks; lambda <- lambdas; numIter <- numIters) { val model = ALS.train(training, rank, numIter, lambda) //計算均根方差值,傳入的是model以及校驗數據 val validationRmse = computeRmse(model, validation, numValidation) count += 1 //選取最佳值,均方根誤差越小越OK if (validationRmse < bestValidationRmse) { bestModel = Some(model) bestValidationRmse = validationRmse bestLambda = lambda bestRank = rank bestNumIter = numIter }}

基於上面最終選擇的參數,輸出Model,我們基於這個模型,去做最後的推薦,注意需要去除ID=1的用戶已經觀看過的電影。

//推薦前十部最感興趣的電影,注意需要剔除該用戶(userid=1)已經評分的電影,即去重val myRatedMovieIds = personalRatingsData.map(f=>f.product).collect().toSetval candidates = movies.keys.filter(!myRatedMovieIds.contains(_))//為用戶1推薦十部movies,我們只做用戶ID=1的推薦val candRDD: RDD[(Int, Int)] = candidates.map((1, _))val recommendations:RDD[Rating] = bestModel.get.predict(candRDD)val recommendations_ = recommendations.collect().sortBy(-_.rating).take(20)

存儲推薦的結果,主要Row需要先進行格式化。

//結果存儲用戶1的推薦結果val alsBaseReDataFrame = sparkSession.sparkContext .parallelize(recommendations_.map(f=> (f.user,f.product,f.rating))) .map(f=>Row(f._1,f._2,f._3))//DataFrame格式化申明val schemaString = "userid movieid score"val schemaAlsBase = StructType(schemaString.split(" ") .map(fieldName=>StructField(fieldName,if (fieldName.equals("score")) DoubleType else IntegerType,true)))val movieAlsBaseDataFrame = sparkSession.createDataFrame(alsBaseReDataFrame,schemaAlsBase)//將結果存入hiveval itemBaseReTmpTableName = "mite_alsbasetmp"val itemBaseReTableName = "mite8.mite_als_base_re"movieAlsBaseDataFrame.registerTempTable(itemBaseReTmpTableName)sparkSession.sql("insert into table " + itemBaseReTableName + " select * from " + itemBaseReTmpTableName)

最後再補上求均方根差的函數。

def computeRmse(model:MatrixFactorizationModel,data:RDD[Rating],n:Long):Double = { //調用model的predict預測方法,把預測數據初始化model中,並且生成預測rating val predictions:RDD[Rating] = model.predict((data.map(x => (x.user, x.product)))) val dataTmp = data.map(x => ((x.user, x.product), x.rating)) //通過join操作,把相同user-product的value合併成一個(double,double)元組,前者為預測值,後者為實際值 val predictionsAndRatings = predictions.map{ x => ((x.user, x.product), x.rating) }.join(dataTmp).values //均方根誤差能夠很好的反應出測量的精密度,對於偏離過大或者過小的測量值較為敏感 //計算過程為觀測值與真實值偏差的平方,除於觀測次數n,然後再取平方根 //reduce方法,執行的是值累加操作 math.sqrt(predictionsAndRatings.map(x => (x._1 - x._2) * (x._1 - x._2)).reduce( _ + _ )/n)}

至此,整個代碼邏輯就結束了,其實我們不難發現,被框架封裝的演算法,其實使用起來更加的簡單,如果拋開校驗以及優化模型的過程,總共代碼都沒有幾行。

最後再補充一個點。

這裡大家可能對為什麼協同能夠發現新物品,而基於用戶興趣的畫像推薦不能,原則上說基於畫像會將思維局限於畫像興趣的偏好內,但興趣本身就會升級的,這是通過歷史的單個用戶的行為所不能推測的。

而基於協同不一樣,他一方面考慮的用戶的歷史行為,另一方面他參考了該用戶的周圍協同的行為,而對於大部分人來說,共有的行為軌跡其實很多時候能夠一定程度上體現用戶的自我認知,以及認知升級的過程,這意味著物品之間的關聯性本身就通過共有的用戶行為天然關聯,而協同就是要挖掘這種潛在的關聯性,這無關物品之間的屬性差異。

所以,從這個維度上說,協同是容易產生驚喜推薦的一種機制。

03 寫在最後

結合實際的數據,Spak工程代碼,我們成功的從呆板的屬性推薦過渡到基於用戶畫像的推薦,並為推薦附上了個性化的能力,再從基於用戶畫像的推薦再過渡到經典的推薦模型,協同過濾,讓你的推薦系統能夠發現新事物,產生驚喜推薦。

這個系列從開頭到現在,已經有五篇了,從理論到實踐,算是對得良心了,不過還未完,之前我們也說過,上溯三個實踐,其實只能算是一種推薦機制,遠不能達到推薦系統的程度,而推薦系統是一個複雜的工程,絕壁不是一個演算法模型可以解決的,在一篇里,我們將會將思維發散,進一步透析什麼是推薦系統,如何構建一個靈活多變的推薦系統。

此外,在這個篇幅中,我們只提供了ALS的大致思路,以及核心的代碼片,但就以解釋性來說,其實是基於物品以及基於用戶的協同推薦比較容易理解的。如果你需要完整的代碼工程、實驗數據,以及基於用戶、物品的協同過濾的實踐代碼,甚至是代碼的詳解,可以微信聯繫我(記得發我100大洋的紅包,我不會客氣的)。

關於我:

大數據行業半個老鳥,我家梓塵兄的超級小弟,會敲代碼、會寫文章,還會泡奶粉哄小屁孩。

想和我交流的,可以加我個人微信mute88,可以拉你入交流群,但請註明身份and來意~

--2017年12月20號凌晨1點

系列文章:

《推01,你們是不是都覺得自己少了個推薦系統?》

《推02,就算非技術人員也有必要了解的推薦系統常識》

《推03,最最最簡單的推薦系統是什麼樣的 | 附Spark實踐案例》

《推04,融合了用戶興趣的推薦系統才會更具個性 | 附Spark實踐案例 》

《推05,論推薦系統之經典,還得數協同 | 附Spark實踐案例》

歡迎關注數據蟲巢(ID:blogchong),這裡有很多關於大數據的原創文章,如果你覺得文章有用,歡迎轉發,也不介意你打賞一杯深夜寫文的咖啡,謝謝(賞完了咖啡可以找我要代碼文件,非工程包 哈哈)。

推薦閱讀:

文檔檢索的ListWise推薦演算法
推薦系統:Attention Model
推03,最最最簡單的推薦系統是什麼樣的 | 附Spark實踐案例
演算法相關的文章整理

TAG:大數據 | 推薦系統 | 互聯網 |