Recommendation System

本文結構安排

  • Item-Based Collaboration Filtering
  • Slope One
  • Matrix Factorization

Item-based Collaboration Filtering

推薦系統(tǒng)本質是在用戶需求不明確的情況下,解決信息過載的問題,聯(lián)系用戶和信息,一方面幫助用戶發(fā)現(xiàn)對自己有價值的信息,另一方面讓信息能夠展現(xiàn)在對它感興趣的用戶面前,從而實現(xiàn)信息消費者和信息生產者的雙贏(這里的信息的含義可以非常廣泛,比如咨詢、電影和商品等,統(tǒng)稱為item)

協(xié)同過濾主要分為基于鄰域以及基于隱語義模型。基于鄰域的算法中,Item-based CF應用廣泛,其主要思想為“喜歡item A的用戶大都喜歡用戶 item B”,通過挖掘用戶歷史的操作日志,利用群體智慧,生成item的候選推薦列表。
原理是通過將用戶和其他用戶的數(shù)據(jù)進行比對來實現(xiàn)推薦的。比對的具體方法就是通過計算兩個用戶數(shù)據(jù)之間的相似性,通過相似性的計算來說明兩個用戶數(shù)據(jù)之間的相似程度。相似度函數(shù)的設計必須滿足度量空間的三點要求,即非負性,對稱性和三角不等性。常用的相似度的計算方法有:歐式距離法、皮爾遜相關系數(shù)法和夾角余弦相似度法。

User-based的基本思想是如果用戶A喜歡物品a,用戶B喜歡物品a、b、c,用戶C喜歡a和c,那么認為用戶A與用戶B和C相似,因為他們都喜歡a,而喜歡a的用戶同時也喜歡c,所以把c推薦給用戶A。該算法用最近鄰居(nearest-neighbor)算法找出一個用戶的鄰居集合,該集合的用戶和該用戶有相似的喜好,算法根據(jù)鄰居的偏好對該用戶進行預測。

User-based算法存在兩個重大問題:1. 數(shù)據(jù)稀疏性。一個大型的電子商務推薦系統(tǒng)一般有非常多的物品,用戶可能買的其中不到1%的物品,不同用戶之間買的物品重疊性較低,導致算法無法找到一個用戶的鄰居,即偏好相似的用戶。2. 算法擴展性。最近鄰居算法的計算量隨著用戶和物品數(shù)量的增加而增加,不適合數(shù)據(jù)量大的情況使用。

Iterm-based的基本思想是預先根據(jù)所有用戶的歷史偏好數(shù)據(jù)計算物品之間的相似性,然后把與用戶喜歡的物品相類似的物品推薦給用戶。還是以之前的例子為例,可以知道物品a和c非常相似,因為喜歡a的用戶同時也喜歡c,而用戶A喜歡a,所以把c推薦給用戶A。

因為物品直接的相似性相對比較固定,所以可以預先在線下計算好不同物品之間的相似度,把結果存在表中,當推薦時進行查表,計算用戶可能的打分值,可以同時解決上面兩個問題。

Item-based算法詳細過程:

1、相似度計算:Item-based算法首選計算物品之間的相似度,計算相似度的方法有以下幾種:

(1). 基于余弦(Cosine-based)的相似度計算,通過計算兩個向量之間的夾角余弦值來計算物品之間的相似性

cos_{}{X,Y} = \frac{XY}{||X|| * ||Y||} = \frac{\sum_{i=1}^{n}(x_{i} * y_{i})}{\sqrt{\sum_{i=1}^{n}x_{i}^{2}} * \sqrt{\sum_{i=1}^{n}y_{i}^{2}}}

(2). 基于關聯(lián)(Correlation-based)的相似度計算,計算兩個向量之間的Pearson-r關聯(lián)度

P_{X,Y}=\frac{cov(X,Y)}{\sigma_{x}\sigma_{y}} = \frac{E((X-\mu_{x})(Y-\mu_{y}))}{\sigma_{x}\sigma_{y}} = \frac{\sum_{u \in U}^{ }(R_{u,i}-\bar{R}_{i})(R_{u,j}-\bar{R}_{j})} {\sqrt{\sum_{u \in U}^{ }(R_{u,i}-\bar{R}_{i})^{2}} * \sqrt{\sum_{u \in U}^{ }(R_{u,j}-\bar{R}_{j})^{2}}}

2、\textbf{預測值計算}:加權求和. 用過對用戶u已打分的物品的分數(shù)進行加權求和,權值為各個物品與物品i的相似度,然后對所有物品相似度的和求平均,計算得到用戶u對物品i打分

P_{u,i} = \bar{R}_{i} + \frac{\sum_{j \in N(i:u)}^{ }sim(i,j)*(R_{u,j}-\bar{R}_{j})}{\sum_{j \in N(i:u)}^{ }|sim(i,j)|}

Slope One

簡單高效的協(xié)同過濾算法。Slope One 和其它類似算法相比, 它的最大優(yōu)點在于算法很簡單, 易于實現(xiàn), 執(zhí)行效率高, 同時推薦的準確性相對很高。

Slope One算法是基于不同物品之間的評分差的線性算法,預測用戶對物品評分的個性化算法。主要兩步:

Step1:計算物品之間的評分差的均值,記為物品間的評分偏差(兩物品同時被評分);

R(ij) = \frac{\sum_{u \in N(i)\cap N(j)}^{ }(R_{u,i}-R_{u,j})}{|N(i)\cap N(j)|} = \frac{\sum_{u \in N(i)\cap N(j)}^{ }(R_{u,i}-R_{u,j})}{card(N(i)\cap N(j))}

其中,R_{u,i}是用戶u對物品i的評分,R_{u,j}是用戶u對物品j的評分,N(i)是對物品i評分過的用戶,N(i)\cap N(j)是對物品i和物品j都評分過的用戶,|N(i)\cap N(j)|是對物品i和物品j都評分過的用戶數(shù)量。

Step2:根據(jù)物品間的評分偏差和用戶的歷史評分,預測用戶對未評分的物品的評分。

P_{u,i} = \frac{\sum_{j \in \{N(u)-\{j\}\}}^{ }(R_{u,j}-R(ij))}{card(\{N(u)-\{j\}\})}

其中N(u)是用戶u購買過的物品,card(\cdot)為集合中的元素個數(shù)

Step3:將預測評分排序,取top-K對應的物品推薦給用戶。

Matrix factorization

評分預測過程通常包括用戶項目矩陣,
其中用戶users對應于行和項目items到列。矩陣條目指定user對item的評分情況。我們研究的問題是如何通過使用可用的值來準確預測用戶項目矩陣中的missing values。

矩陣分解是指一組算法,其中矩陣被分解成兩個矩陣的乘積。
當應用矩陣分解來解決我們的研究問題時,前提是影響用戶觀察到的Web服務失敗概率的因素很少,
用戶觀察到的Web服務失敗概率取決于每個因素如何適用于用戶,
Web服務上的用戶失敗概率值對應于這些因素與用戶特定系數(shù)的線性組合。

P為m行n列的user-item矩陣,l-factor模型旨在使W和H矩陣的乘積能夠近似P

P\approx WH
W為m行l(wèi)列矩陣,H為l行n列矩陣,
l是因素的個數(shù),矩陣分解產生的W的每一行都是用戶特定的用戶系數(shù),
矩陣分解產生的H的每一列都是包含Web服務的l因子值的因子向量。

\textbf{Gradient Descent}

P和WH之間差異的最常見量度是總和誤差,其可以通過以下公式計算:
\sum_{i=1}^{m}\sum_{j=1}^{n}I_{ij}^{P}(p_{ij}-W_{i}H_{j})^{2}
在用戶項目矩陣 P中(表示W(wǎng)eb服務j先前已由用戶調用),如果p_{ij}中有值,則I_{ij}^{P}等于1,否則等于0,
W_{i}是矩陣W的第i行(代表特定的用戶i的系數(shù)),而H_{j}是矩陣H的第j列(表示itemj的向量因子)。

使用梯度下降法近似矩陣P。目標函數(shù)是:
\min L(W,H)=\frac{1}{2}\sum_{i=1}^{m}\sum_{j=1}^{n}I_{ij}^{P}(p_{ij}-W_{i}H_{j})^{2}+\frac{\gamma}{2}||W||_{F}^{2}+\frac{\gamma}{2}||H||_{F}^{2}
正則化項可以這樣求:
||W||_{F}^{2}=WW^{T}
采用誤差反向傳播的方法迭代更新矩陣W和H。
\frac{\partial L}{\partial w_{il}}=\gamma w_{il}-\sum_{j=1}^{n}I_{ij}^{P}(p_{ij}-W_{i}H_{j})h_{li}
\frac{\partial L}{\partial h_{lj}}=\gamma h_{lj}-\sum_{i=1}^{m}I_{ij}^{P}(p_{ij}-W_{i}H_{j})h_{il}

w_{il}^{t+1}=w_{il}^{t}-\alpha \frac{\partial L}{\partial w_{il}}
h_{lj}^{t+1}=h_{lj}^{t}-\alpha \frac{\partial L}{\partial h_{lj}}
\alpha是學習率,直到更新到收斂為止。

Key Code on Spark

Item-based Collaboration Filtering Spark實現(xiàn)

Spark采用Scala語言編寫,建立在統(tǒng)一抽象的RDD(分布式內存抽象)之上,使得它可以以基本一致的方式應對不同的大數(shù)據(jù)處理場景。Spark提供廣泛的數(shù)據(jù)集操作類型(20+種),不像Hadoop只提供了Map和Reduce兩種操作。Spark可以與Hadoop無縫連接,使用YARN作為他的集群管理器。
本次實驗靈活spark操作算子,在設計推薦算法時,我的宗旨時堅決不使用for-loop,while-loop,并且借鑒數(shù)據(jù)庫select的思想完成本次實驗,Spark提交任務至集群運行,使得實驗中提供的較大規(guī)模的數(shù)據(jù)集也能高效的跑完。

\subsection{Item-based Collaboration Filtering}

數(shù)據(jù)預處理階段:配置spark,設置為集群模式,導入數(shù)據(jù)源(訓練集和測試集),并對數(shù)據(jù)進行分割操作,其中movielens數(shù)據(jù)集以"::"分割,并且我只需要提取user,item和preference,不需要timestamp,所以在預處理時我直接過濾了時間戳的數(shù)據(jù),并設置userid,itemid的數(shù)據(jù)為Long,設置評分preference為Double,至此TrainSetRDD和TestSetRDD的RDD構成都為

(user,item,preference),實現(xiàn)如下:

val conf = new SparkConf().setAppName("IBCF")
val sc = new SparkContext(conf)

val datasource1 = "file:///usr/local/data/test.txt"
val Mini2TestFile = sc.textFile(datasource1,3)

val datasource2 = "file:///usr/local/data/train.txt"
val Mini2TrainFile = sc.textFile(datasource2,3)

val TrainSetRDD = Mini2TrainFile.map(line => {
    val fields = line.split("::")
    (fields(0).toLong, fields(1).toLong, fields(2).toDouble)
})

val TestSetRDD = Mini2TestFile.map(line => {
    val fields = line.split("::")
    (fields(0).toLong, fields(1).toLong, fields(2).toDouble)
})
\end{lstlisting}

對訓練集做處理,按照用戶id分組,并且組內按照用戶評分的大小進行排序,實現(xiàn)如下:
\begin{lstlisting}
var ratings = TrainSetRDD.groupBy(k=>k._1).flatMap(x=>(x._2.toList.sortWith((x,y)=>x._3>y._3)))

首先,以item為key,將訓練集以item進行分組。item2manyUser中包含該item對應曾經購買過這個item的userid和對該item的評分信息的集合
然后,以item為key,統(tǒng)計購買過該item
的用戶數(shù)量。numRatersPerItem中包含itemid和購買過該item的user數(shù)量。

最后,對item2manyUser和numRatersPerItem
進行連接操作,目的是為了得到這樣結構的RDD:

(user,item,preference,size)其中size對應的是對item評過分的user數(shù)量,計算這個值是為了之后求相似度做準備。實現(xiàn)如下:

//(item,((user,item,prefs),(user,item,prefs),...))
val item2manyUser = ratings.groupBy(tup => tup._2)
        
//(item,ratingsize)
val numRatersPerItem = item2manyUser.map(grouped => (grouped._1, grouped._2.size))
        

//(user,item,prefs,ratingsize)
val ratingsWithSize = item2manyUser.join(numRatersPerItem).flatMap(
    joined => {
    joined._2._1.map(f => (f._1, f._2, f._3, joined._2._2))
})

因為在IBCF算法中,最重要的是要求得item之間的相似度sim(i,j)

想要得到成對的(item_{i},item_{j}),
可以借用數(shù)據(jù)庫的表連接的思想,

使ratingsWithSize以userid為key進行自連接操作,效果類似笛卡兒積,我們可以得到任意(item_{i},item_{j}).
選取(item_{i}<item_{j})的rdd,因為相似度計算滿足對稱性,所以為了減少計算,我們計算一半的值即可。

ratingPairs這個RDD所蘊含的意義是user歷史曾經對item_{i}item_{j}都有進行評分。實現(xiàn)如下:

//(user,(user,item,prefs,size))
val ratings2 = ratingsWithSize.keyBy(tup => tup._1)

//(user,((user,item,prefs,size),(user,item,prefs,size)))
val ratingPairs =ratings2.join(ratings2).filter(f => f._2._1._2 < f._2._2._2)

以余弦相似度為例,cos_{X,Y} = \frac{\sum_{i=1}^{n}(x_{i} * y_{i})}{\sqrt{\sum_{i=1}^{n}x_{i}^{2}} * \sqrt{\sum_{i=1}^{n}y_{i}^{2}}}

計算兩個item相似度時,是使用對這個itemPair都進行過評分的用戶評分數(shù)據(jù)決定的。
我設計的是先計算itemPair相似度公式中的某些中間變量,然后再進行求和操作。

從余弦計算公式中可以觀察到,可以計算的中間變量有:分子部分的點乘(dotProduct)和分母部分的平方量。

對ratingPairs進行map操作,可以對每一行數(shù)據(jù)都進行相同的操作,提取出
(item_{i},item_{j})作為key,中間變量作為values,得到一個新的RDD。

值得注意的是,ratingPairs的key是userid,而tempVectorCalcs的key是(item_{i},item_{j})

從ratingPairs中提取出來,并且沒有選取userid的信息,這意味著(item_ {i},item_{j})的key不唯一,因為多個user可以對(item_ {i},item_{j})都進行評分。

// ((item1,item2), (tempValues))
val tempVectorCalcs = ratingPairs.map(data => {
    val key = (data._2._1._2, data._2._2._2) //(item1,item2)
    val values =
    (data._2._1._3 * data._2._2._3, // prefs 1 * prefs 2
    data._2._1._3,                // item 1 prefs
    data._2._2._3,                // item 2 prefs
    math.pow(data._2._1._3, 2),   // square of item 1 prefs
    math.pow(data._2._2._3, 2),   // square of item 2 prefs
    data._2._1._4,                // item 1 ratingsize
    data._2._2._4)                // item 2 ratingsize
    (key, values)
})

使用groupByKey 將(item,item)key相同的rdd分組,并且對中間變量進行處理,然后定義余弦相似度的函數(shù),形如dotProduct(A, B)/(norm(A) * norm(B)),最終得到相似度結果

item之間的相似度保存在一個新的RDD中,(itemi,(itemj,Sim(i,j))),至此,Item-basedCF的第一步——計算物品間的相似性已經完成。實現(xiàn)如下:

//((item1,item2), (size, dotProduct, ratingSum, rating2Sum, ratingSq, rating2Sq, numRaters, numRaters2))
val vectorCalcs = tempVectorCalcs.groupByKey().map(data => {
    val key = data._1 //(item1,item2)
    val vals = data._2 //stats
    val size = vals.size // the number of users rating(item1,item2)
    val dotProduct = vals.map(f => f._1).sum // sum of prefs 1 * prefs 2 = dotProduct
    val ratingSum = vals.map(f => f._2).sum // sum of prefs 1
    val rating2Sum = vals.map(f => f._3).sum // sum of prefs 2
    val ratingSq = vals.map(f => f._4).sum // sum of square prefs 1
    val rating2Sq = vals.map(f => f._5).sum // sum of square prefs 2
    val numRaters = vals.map(f => f._6).max
    val numRaters2 = vals.map(f => f._7).max
    (key, (size, dotProduct, ratingSum, rating2Sum, ratingSq, rating2Sq, numRaters, numRaters2))
})

//(itemi,(itemj,Sim(i,j)))
val tempSimilarities = vectorCalcsTotal.map(fields => {
    val key = fields._1
    val (size, dotProduct, ratingSum, rating2Sum, ratingNormSq, rating2NormSq, numRaters, numRaters2) = fields._2
    val cosSim = cosineSimilarity(dotProduct, scala.math.sqrt(ratingNormSq), scala.math.sqrt(rating2NormSq))*size/(numRaters*math.log10(numRaters2+10))
    (key._1,(key._2, cosSim))
})

val similarities = tempSimilarities.groupByKey().flatMap(x => { x._2.map(temp => (x._1,(temp._1,temp._2))).toList.sortWith((a,b)=>a._2._2>b._2._2).take(50)
})

其中cosineSimilarity可以定義函數(shù)進行計算,這樣也比較方便替換不同相似性度量的方法:

  // *************************
  // * SIMILARITY MEASURES
  // *************************

  // [n * dotProduct(A, B) - sum(A) * sum(B)] / sqrt{ [n * norm(A)^2 - sum(A)^2] [n * norm(B)^2 - sum(B)^2] }
  def correlation(size : Double, dotProduct : Double, ratingSum : Double, rating2Sum : Double, ratingNormSq : Double, rating2NormSq : Double) = {

    val numerator = size * dotProduct - ratingSum * rating2Sum
    val denominator = scala.math.sqrt(size * ratingNormSq - ratingSum * ratingSum) * scala.math.sqrt(size * rating2NormSq - rating2Sum * rating2Sum)

    numerator / denominator
  }

  //The cosine similarity between two vectors A, B is
  //dotProduct(A, B) / (norm(A) * norm(B))
  def cosineSimilarity(dotProduct : Double, ratingNorm : Double, rating2Norm : Double) = {
    dotProduct / (ratingNorm * rating2Norm)
  }

  //The Jaccard Similarity between two sets A, B is
  //|Intersection(A, B)| / |Union(A, B)|
  def jaccardSimilarity(usersInCommon : Double, totalUsers1 : Double, totalUsers2 : Double) = {
    val union = totalUsers1 + totalUsers2 - usersInCommon
    usersInCommon / union
  }

接下來是預測評分的部分,根據(jù)預測公式P_{u,i} = \bar{R}_{i} + \frac{\sum_{j \in N(i:u)}^{ }sim(i,j)*(R_{u,j}-\bar{R}_{j})}{\sum_{j \in N(i:u)}^{ }|sim(i,j)|}

需要預測的是用戶u對商品i的評分(itemi用戶沒有購買過),也可以先得到某些中間變量如:sim(i,j)*ratings然后求和再相除。

//(item,(user,prefs))
val ratingsInverse = ratings.map(x => (x._2,(x._1,x._3)))

//join : (item1,(user,prefs)) <- (item1,(item2,sim)) ==>> ((user,item2),(sim,sim*prefs))
val statistics = ratingsInverse.join(similarities).map( x=> ((x._2._1._1,x._2._2._1),(x._2._2._2,x._2._1._2*x._2._2._2)))

val predictResult = statistics.reduceByKey((x,y) => ((x._1+y._1),(x._2+y._2))).map(x=>(x._1,x._2._2/x._2._1))


val filterItem = TrainSetRDD.map(x=>((x._1,x._2),Double.NaN))
val totalScore = predictResult ++ filterItem

val finalResult = totalScore.reduceByKey(_+_).filter(x=> !(x._2 equals(Double.NaN))).map( x =>
    (x._1._1,x._1._2,x._2)).groupBy(x=>x._1).flatMap(x=>(x._2.toList.sortWith((a,b)=>a._3>b._3)))

最后計算測試集中真實評分和預測評分的RMSE,評估算法的效果。

val joinTestSetRDD = TestSetRDD.map(x => ((x._1,x._2),(x._3)))
val joinFinalResult = finalResult.map(x => ((x._1,x._2),(x._3)))
val measure = joinTestSetRDD.join(joinFinalResult)
val rmseSize = measure.count()
val rmse = measure.map(x => x._2._1-x._2._2).map(y => y*y).reduce(_+_)/rmseSize

Slope One Spark實現(xiàn)

數(shù)據(jù)預處理部分和IBCF一樣,本部分省略。

Step1-1:計算物品之間的評分差的均值,記為物品間的評分偏差(兩物品同時被評分);

R(ij) = \frac{\sum_{u \in N(i)\cap N(j)}^{ }(R_{u,i}-R_{u,j})}{|N(i)\cap N(j)|} = \frac{\sum_{u \in N(i)\cap N(j)}^{ }(R_{u,i}-R_{u,j})}{card(N(i)\cap N(j))}

其中,R_{u,i}是用戶u對物品i的評分,R_{u,j}是用戶u對物品j的評分,N(i)是對物品i評分過的用戶,N(i)\cap N(j)是對物品i和物品j都評分過的用戶,|N(i)\cap N(j)|是對物品i和物品j都評分過的用戶數(shù)量。

也是利用自連接的思想,找出所有的itemPairs,和它們之間的評分差(prefsi-prefsj)。

因為一對商品(itemi,itemj)可以被多個用戶進行評分,所以這就提供了計算物品間平均偏差的機會。當以(itemi,itemj)進行Group操作時,
同時購買過(itemi,itemj)的用戶會被聚合在同一行數(shù)據(jù)中,也就是N(i)\cap N(j)

DevOfItemsPairs-all就是(itemi,itemj)的偏差結果,rdd形如 ((itemi,itemj),dev(i,j)))

var ratings = TrainSetRDD.groupBy(k=>k._1).flatMap(x=>(x._2.toList.sortWith((x,y)=>x._3>y._3)))

val ratings2 = ratings.keyBy(tup => tup._1)

// itemi<itemj
val ratingPairs =ratings2.join(ratings2).filter(f => f._2._1._2 < f._2._2._2)

//(usertest,itemj)
val TestUser = TestSetRDD.map(line => (line._1,line._2))

//Pairs(itemi,itemj)
val ItemsPairs = ratingPairs.map(data => {
    val key = (data._2._1._2, data._2._2._2) //(item1,item2)
    val stats = (data._2._1._3-data._2._2._3) //(prefs1-prefs2)
    (key, stats)
})

//((item1,item2),dev))
val DevOfItemsPairs = ItemsPairs.groupByKey().map(data => {
    val key = data._1
    val values = data._2
    val Card = values.size
    val Ruj_Rui = values.sum
    var R_Card = Ruj_Rui/Card
    (key,R_Card)
})
val DevOfItemsPairs_inverse = DevOfItemsPairs.map(x => ((x._1._2,x._1._1),x._2))

//((item1,item2),dev))
val DevOfItemsPairs_all = DevOfItemsPairs ++ DevOfItemsPairs_inverse


//(user,Sertof(Items))
val GroupByUser_ItemSets = TrainSetRDD.map(line => (line._1,line._2)).groupByKey()

// (usertest,itemj) join (user,Sertof(Items)) => (user,(itemj,Setof(Items))) => ((itemj,items),user)
val TestUser_ItemSets = TestUser.join(GroupByUser_ItemSets).flatMap(data => data._2._2.map(x => ((data._2._1,x),data._1)))

//((itemj,item2)(usertest,dev_j2)) => ((usertest,itemj),(dev_j2))  =groupByKey=> Sum/Card

val DevOfTestItemPairs = TestUser_ItemSets.join(DevOfItemsPairs_all).map(x => ((x._2._1,x._1._1),x._2._2)).groupByKey().map(data => {
    val key = data._1
    val values = data._2
    val Size = values.size
    val Sum = values.sum
    val avg_dev = Sum/Size
    (key,avg_dev)
})

Step1-2:計算用戶歷史評分的均值

以user為key進行聚合操作,得到的集合是用戶對所有購買過的item的評分情況。對該評分集合求均值即是用戶的歷史評分

//(user,Setof(prefs))
val RatingPrefs = ratings.map(data => (data._1,data._3)).groupByKey()

//(usertest,itemj)
val TestUser = TestSetRDD.map(line => (line._1,line._2))


//after join:(user,(item,Setof(prefs)))
val JoinedByUser = TestUser.join(RatingPrefs).map(data => {
    val key = (data._1,data._2._1) //(user,item)
    val value = data._2._2 //Setof(prefs)
    (key,value)
})

//key = (user,item)  value = the average rating of user
val RatingByUser = JoinedByUser.map(data => {
    val key = data._1
    val values = data._2
    val Card = values.size
    val SumRatings = values.sum
    val avg_user_rating = SumRatings/Card
    (key,avg_user_rating)
})

Step2:根據(jù)物品間的評分偏差和用戶的歷史評分,預測用戶對未評分的物品的評分。

P_{u,i} = \frac{\sum_{j \in \{N(u)-\{j\}\}}^{ }(R_{u,j}-R(ij))}{card(\{N(u)-\{j\}\})}

其中N(u)是用戶u購買過的物品,card(\cdot)為集合中的元素個數(shù)

將Step1里的物品間的評分偏差(DevOfTestItemPairs)和用戶的歷史評分(RatingByUser)結合進行評分的預測并評估算法。

val predict = DevOfTestItemPairs.join(RatingByUser).map(data => (data._1,data._2._1 + data._2._2))

val joinTestSetRDD = TestSetRDD.map(data => ((data._1,data._2),data._3))
val measure = predict.join(joinTestSetRDD)
val rmseSize = measure.count()
val rmse = measure.map(x => x._2._1-x._2._2).map(y => y*y).reduce(_+_)/rmseSize

MF - Matlab實現(xiàn)
首先矩陣分解的目的是為了填補缺失值,然后應用在推薦系統(tǒng)中的作用便是填補用戶對未購買過的商品進行評分值,從而根據(jù)預測值向用戶推薦item。
數(shù)據(jù)預處理與前兩個算法不同,需要構造user-item評分矩陣,并且原來有評分的位置保持不變,沒有評分的位置置零。在計算LossFunction時,只需要比較原矩陣有值的rmse,原矩陣沒有值的地方不需要計算誤差。

function [rating,prerating]=MF(data,K,alpha)
    [rating,num]=translate_line_to_matrix(data);
    [m,n]=size(rating);
    u=rand(m,K);
    v=rand(K,n);
    e=zeros(m,n);
    distance=100000000;
    while(1)
        for i=1:m
            for j=1:n
                if(rating(i,j)>0)
                    error=0;
                    for k=1:K
                        error=error+u(i,k)*v(k,j);
                    end
                    e(i,j)=rating(i,j)-error;
                    for k=1:K
                        u(i,k)=u(i,k)+2*alpha*e(i,j)*v(k,j);
                        v(k,j)=v(k,j)+2*alpha*e(i,j)*u(i,k);
                    end
                end
            end
        end
        MFLossFunc=sum(sum(e));
        if(distance-MFLossFunc<0.0000000001)
            break;
        else
            distance=MFLossFunc;
        end
    end
    prerating=u*v;
end

Algorithm Comparison and Hyperparameter

Item-based算法的預測結果比User-based算法的質量要高一點。由于Item-based算法可以預先計算好物品的相似度,所以在線的預測性能要比User-based算法的高。
基于用戶之間評級相似性的早期協(xié)作過濾系統(tǒng)(稱user-user協(xié)同過濾)存在以下幾個問題:(1)當他們有很多項目但收視率相對較低時,系統(tǒng)表現(xiàn)不佳
(2)計算所有用戶對之間的相似性非常昂貴
(3)用戶配置文件變化很快,整個系統(tǒng)模型必須重新計算

item-based解決了用戶數(shù)量多于項目的系統(tǒng)中的這些問題。 item-item模型使用每個項目的評級分布,而不是每個用戶。 如果用戶數(shù)量多于商品數(shù)量,則每個商品的評分往往比每位用戶都多,因此商品的平均評分通常不會很快發(fā)生變化。 這導致模型中的評級分布更加穩(wěn)定,所以模型不需要經常重建。 當用戶使用并評估某個項目時,該項目的相似項目將從現(xiàn)有系統(tǒng)模型中挑選出來并添加到用戶的個性化推薦中。

Slope One是項目協(xié)同過濾算法家族中的一員,旨在減少模型過度擬合問題。 可以說,它是基于評分的非平凡基于項目的協(xié)同過濾(non-trivial item-based collaborative filtering based on ratings)的最簡單形式。 它們的簡單性使其特別易于有效地實現(xiàn)它們,而其精度通常與更復雜且計算量更大的算法相當, 它也被用作改進其他算法的構建塊。
SlopeOne易于實現(xiàn)和維護,可以輕松解釋所有的聚合數(shù)據(jù),并且算法易于實現(xiàn)和測試。具有實時性, 運行時可更新,新增一個評分項,應該對預測結果即時產生影響。有高效率的查詢響應,快速的執(zhí)行查詢,可能需要付出更多的空間占用作為代價,對初次訪問者要求少:對于一個評分項目很少的用戶,也應該可以獲得有效的推薦。與最準確的方法相比,SlopeOne應該是有競爭力的,不僅算法簡單高效,效果也不賴。

矩陣分解在推薦系統(tǒng)中的應用我覺得非常神奇。隱語義模型(Latent Factor Model,LFM)在推薦系統(tǒng)中的應用越來越廣泛,矩陣分解方法也是基于這個隱語義模型。設置K為因子factor的數(shù)量。矩陣分解的思想簡單來說就是每一個用戶和每一個物品都會有自己的一些特性
(feature/factor),用矩陣分解的方法可以從評分矩陣中分解出user-factor,factor-item矩陣,這樣做的好處是得到了用戶的偏好和每件物品的特性。用戶對電影來舉例子就是:每個用戶看電影的時候都有偏好,這些偏好可以直觀理解成:恐怖,喜劇,動作,愛情等。用戶——特性矩陣表示的就是用戶對這些因素的喜歡程度。同樣,每一部電影也可以用這些因素描述,因此特性——物品矩陣表示的就是每一部電影這些因素的含量,也就是電影的類型。這樣子兩個矩陣相乘就會得到用戶對這個電影的喜歡程度。

mf.jpg
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容