利用用戶(hù)行為數(shù)據(jù)進(jìn)行推薦(協(xié)同過(guò)濾)
1元镀、用戶(hù)行為數(shù)據(jù)
用戶(hù)行為數(shù)據(jù)在網(wǎng)站上最簡(jiǎn)單的存在形式就是日志,比如用戶(hù)在電子商務(wù)網(wǎng)站中的網(wǎng)頁(yè)瀏覽掌测、購(gòu)買(mǎi)内贮、點(diǎn)擊、評(píng)分和評(píng)論等活動(dòng)汞斧。 用戶(hù)行為在個(gè)性化推薦系統(tǒng)中一般分兩種——顯性反饋行為(explicit feedback)和隱性反饋 行為(implicit feedback)夜郁。顯性反饋行為包括用戶(hù)明確表示對(duì)物品喜好的行為。網(wǎng)站中收集顯性反饋的主要方式就是評(píng)分和喜歡/不喜歡粘勒。隱性反饋行為指的是那些不能明確反應(yīng)用戶(hù)喜好 的行為竞端。最具代表性的隱性反饋行為就是頁(yè)面瀏覽行為。 按照反饋的明確性分庙睡,用戶(hù)行為數(shù)據(jù)可以分為顯性反饋和隱性反饋事富,但按照反饋的方向分, 又可以分為正反饋和負(fù)反饋乘陪。正反饋指用戶(hù)的行為傾向于指用戶(hù)喜歡該物品统台,而負(fù)反饋指用戶(hù)的 行為傾向于指用戶(hù)不喜歡該物品。在顯性反饋中啡邑,很容易區(qū)分一個(gè)用戶(hù)行為是正反饋還是負(fù)反饋贱勃, 而在隱性反饋行為中,就相對(duì)比較難以確定谤逼。
2贵扰、用戶(hù)行為分析
在利用用戶(hù)行為數(shù)據(jù)設(shè)計(jì)推薦算法之前,研究人員首先需要對(duì)用戶(hù)行為數(shù)據(jù)進(jìn)行分析流部,了解 數(shù)據(jù)中蘊(yùn)含的一般規(guī)律戚绕,這樣才能對(duì)算法的設(shè)計(jì)起到指導(dǎo)作用。
(1) 用戶(hù)活躍度和物品流行度
(2) 用戶(hù)活躍度和物品流行度的關(guān)系
一般認(rèn)為贵涵,新用戶(hù)傾向于瀏覽熱門(mén)的物品列肢,因?yàn)樗?們對(duì)網(wǎng)站還不熟悉,只能點(diǎn)擊首頁(yè)的熱門(mén)物品宾茂,而老用戶(hù)會(huì)逐漸開(kāi)始瀏覽冷門(mén)的物品瓷马。如果用橫坐標(biāo)表示用戶(hù)活躍度,縱坐標(biāo)表示具有某個(gè)活躍度的所有用戶(hù)評(píng)過(guò)分的物品的平均流行度跨晴。圖中曲線呈明顯下 降的趨勢(shì)欧聘,這表明用戶(hù)越活躍,越傾向于瀏覽冷門(mén)的物品端盆。
僅僅基于用戶(hù)行為數(shù)據(jù)設(shè)計(jì)的推薦算法一般稱(chēng)為協(xié)同過(guò)濾算法怀骤。學(xué)術(shù)界對(duì)協(xié)同過(guò)濾算法進(jìn)行了深入研究,提出了很多方法焕妙,比如基于鄰域的方法(neighborhood-based)蒋伦、隱語(yǔ)義模型 (latent factor model)、基于圖的隨機(jī)游走算法(random walk on graph)等焚鹊。在這些方法中痕届, 最著名的、在業(yè)界得到最廣泛應(yīng)用的算法是基于鄰域的方法末患,而基于鄰域的方法主要包含下面兩種算法研叫。
基于用戶(hù)的協(xié)同過(guò)濾算法:這種算法給用戶(hù)推薦和他興趣相似的其他用戶(hù)喜歡的物品
基于物品的協(xié)同過(guò)濾算法:這種算法給用戶(hù)推薦和他之前喜歡的物品相似的物品
3、基于領(lǐng)域的算法
基于鄰域的算法是推薦系統(tǒng)中最基本的算法璧针,該算法不僅在學(xué)術(shù)界得到了深入研究嚷炉,而且在 業(yè)界得到了廣泛應(yīng)用√匠鳎基于鄰域的算法分為兩大類(lèi)申屹,一類(lèi)是基于用戶(hù)的協(xié)同過(guò)濾算法,另一類(lèi)是 基于物品的協(xié)同過(guò)濾算法∽吒椋現(xiàn)在我們所說(shuō)的協(xié)同過(guò)濾独柑,基本上就就是指基于用戶(hù)或者是基于物品的協(xié)同過(guò)濾算法,因此私植,我們可以說(shuō)基于鄰域的算法即是我們常說(shuō)的協(xié)同過(guò)濾算法
(1) 基于用戶(hù)的協(xié)同過(guò)濾算法(UserCF)
基于用戶(hù)的協(xié)同過(guò)濾算法的基本思想是:在一個(gè)在線個(gè)性化推薦系統(tǒng)中忌栅,當(dāng)一個(gè)用戶(hù)A需要個(gè)性化推薦 時(shí),可以先找到和他有相似興趣的其他用戶(hù)曲稼,然后把那些用戶(hù)喜歡的索绪、而用戶(hù)A沒(méi)有聽(tīng)說(shuō)過(guò)的物品推薦給A。
? 從上面的描述中可以看到贫悄,基于用戶(hù)的協(xié)同過(guò)濾算法主要包括兩個(gè)步驟瑞驱。 第一步:找到和目標(biāo)用戶(hù)興趣相似的用戶(hù)集合。 第二步: 找到這個(gè)集合中的用戶(hù)喜歡的窄坦,且目標(biāo)用戶(hù)沒(méi)有聽(tīng)說(shuō)過(guò)的物品推薦給目標(biāo)用戶(hù)唤反。
這里凳寺,步驟1的關(guān)鍵是計(jì)算兩個(gè)用戶(hù)的興趣相似度,協(xié)同過(guò)濾算法主要利用行為的相似度計(jì)算興趣的相似度彤侍。給定用戶(hù)u和用戶(hù)v肠缨,令N(u)表示用戶(hù)u曾經(jīng)有過(guò)正反饋的物品集合,令N(v) 為用戶(hù)v曾經(jīng)有過(guò)正反饋的物品集合盏阶。那么我們可以通過(guò)以下方法計(jì)算用戶(hù)的相似度:
基于余弦相似度
(2) 基于物品的協(xié)同過(guò)濾算法(itemCF)
與UserCF同理
(3) UserCF和itemCF的比較
首先我們提出一個(gè)問(wèn)題晒奕,為什么新聞網(wǎng)站一般使用UserCF,而圖書(shū)名斟、電商網(wǎng)站一般使用ItemCF呢脑慧? 首先回顧一下UserCF算法和ItemCF算法的推薦原理。UserCF給用戶(hù)推薦那些和他有共同興 趣愛(ài)好的用戶(hù)喜歡的物品砰盐,而ItemCF給用戶(hù)推薦那些和他之前喜歡的物品類(lèi)似的物品闷袒。從這個(gè)算 法的原理可以看到,UserCF的推薦結(jié)果著重于反映和用戶(hù)興趣相似的小群體的熱點(diǎn)岩梳,而ItemCF 的推薦結(jié)果著重于維系用戶(hù)的歷史興趣霜运。換句話(huà)說(shuō),UserCF的推薦更社會(huì)化蒋腮,反映了用戶(hù)所在的小型興趣群體中物品的熱門(mén)程度淘捡,而ItemCF的推薦更加個(gè)性化,反映了用戶(hù)自己的興趣傳承池摧。 在新聞網(wǎng)站中焦除,用戶(hù)的興趣不是特別細(xì)化,絕大多數(shù)用戶(hù)都喜歡看熱門(mén)的新聞作彤。個(gè)性化新聞推薦更加強(qiáng)調(diào)抓住 新聞熱點(diǎn)膘魄,熱門(mén)程度和時(shí)效性是個(gè)性化新聞推薦的重點(diǎn),而個(gè)性化相對(duì)于這兩點(diǎn)略顯次要竭讳。因 此创葡,UserCF可以給用戶(hù)推薦和他有相似愛(ài)好的一群其他用戶(hù)今天都在看的新聞,這樣在抓住熱 點(diǎn)和時(shí)效性的同時(shí)绢慢,保證了一定程度的個(gè)性化灿渴。同時(shí),在新聞網(wǎng)站中胰舆,物品的更新速度遠(yuǎn)遠(yuǎn)快于新用戶(hù)的加入速度骚露,而且 對(duì)于新用戶(hù),完全可以給他推薦最熱門(mén)的新聞缚窿,因此UserCF顯然是利大于弊棘幸。
但是,在圖書(shū)倦零、電子商務(wù)和電影網(wǎng)站误续,比如亞馬遜吨悍、豆瓣、Netflix中蹋嵌,ItemCF則能極大地發(fā) 揮優(yōu)勢(shì)畜份。首先,在這些網(wǎng)站中欣尼,用戶(hù)的興趣是比較固定和持久的。一個(gè)技術(shù)人員可能都是在購(gòu)買(mǎi) 技術(shù)方面的書(shū)停蕉,而且他們對(duì)書(shū)的熱門(mén)程度并不是那么敏感愕鼓,事實(shí)上越是資深的技術(shù)人員,他們看 的書(shū)就越可能不熱門(mén)慧起。此外菇晃,這些系統(tǒng)中的用戶(hù)大都不太需要流行度來(lái)輔助他們判斷一個(gè)物品的 好壞,而是可以通過(guò)自己熟悉領(lǐng)域的知識(shí)自己判斷物品的質(zhì)量蚓挤。因此磺送,這些網(wǎng)站中個(gè)性化推薦的 任務(wù)是幫助用戶(hù)發(fā)現(xiàn)和他研究領(lǐng)域相關(guān)的物品。因此灿意,ItemCF算法成為了這些網(wǎng)站的首選算法估灿。 此外,這些網(wǎng)站的物品更新速度不會(huì)特別快缤剧,一天一次更新物品相似度矩陣對(duì)它們來(lái)說(shuō)不會(huì)造成 太大的損失馅袁,是可以接受的。同時(shí)荒辕,從技術(shù)上考慮汗销,UserCF需要維護(hù)一個(gè)用戶(hù)相似度的矩陣,而ItemCF需要維護(hù)一個(gè)物品 相似度矩陣抵窒。從存儲(chǔ)的角度說(shuō)弛针,如果用戶(hù)很多,那么維護(hù)用戶(hù)興趣相似度矩陣需要很大的空間李皇, 同理削茁,如果物品很多,那么維護(hù)物品相似度矩陣代價(jià)較大
下表是對(duì)二者的一個(gè)全面的表較:
4掉房、算法的實(shí)現(xiàn)如下:(spark)
package cf
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession
import breeze.math
import breeze.numerics.{pow,sqrt}
import org.apache.spark.sql.functions._
object CFUserTest {
case class U(userid:String, itemid:String, rating:String)
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder()
.appName("CFUserTest")
.master("local")
.config("spark.sql.warehouse.dir","D:\\hadoop\\sparkTest\\data\\u.data")
.getOrCreate()
val data = spark.sparkContext.textFile("D:\\hadoop\\sparkTest\\data\\u.data")
userBaseCF(data,spark)
//計(jì)算用戶(hù)之間的相似度:
def userBaseCF(data:RDD[String],spark:SparkSession) : Unit ={
import spark.implicits._
val df_data = data.map(line => line.toString.split("\t")).map(line => U(line(0).trim,line(1).trim,line(2).trim))
.toDF("userid","itemid","rating").select("userid","itemid","rating")
val df_data_t = data.map(line => line.toString.split("\t")).map(line => U(line(0).trim,line(1).trim,line(2).trim)) .toDF("userid_1","itemid_1","rating_1").select("userid_1","itemid_1","rating_1")
df_data.show(false)
//計(jì)算每個(gè)用戶(hù)用對(duì)所有item打分的平方和付材,再開(kāi)根號(hào)
val userScoreSum = df_data.rdd.map(x => (x(0).toString,x(2).toString)).groupByKey()
.mapValues(x => sqrt(x.toArray.map(r => pow(r.toDouble,2)).sum))
//將rdd userScoreSum 轉(zhuǎn)換為DataFrame,并重新命名列名
val df_user_sum =userScoreSum.toDF("user_id_sum","rating_sqrt_sum").select("user_id_sum","rating_sqrt_sum")
df_user_sum.show()
//對(duì)同一個(gè)item打分的用戶(hù)進(jìn)行join,并進(jìn)行過(guò)濾圃阳,因?yàn)閖oin是做了一個(gè)笛卡爾積厌衔,數(shù)據(jù)存在重復(fù)
val df_decare = df_data.join(df_data_t,df_data("itemid")===df_data_t("itemid_1"))
.filter("cast(userid as long) < cast(userid_1 as long)")
df_decare.show()
//定義一個(gè)udf計(jì)算兩個(gè)用戶(hù)對(duì)同一個(gè)item打分的乘積
val product_udf = udf((r1:Int,r2:Int) => r1.toDouble*r2.toDouble)
//調(diào)用udf函數(shù),添加一列rating_product
val df_product = df_decare.withColumn("rating_product",product_udf(col("rating"),col("rating_1")))
.select("userid","userid_1","rating_product")
df_product.show()
//以"userid","userid_1"為key進(jìn)行聚合捍岳,并對(duì)value進(jìn)行求和
val df_group = df_product.groupBy("userid","userid_1").agg("rating_product"->"sum")
.withColumnRenamed("sum(rating_product)","rating_sum_pro")
df_group.show()
//將df_group和df_user_sum富寿,進(jìn)行內(nèi)連接睬隶,目的是將rating_sqrt_sum添加到同一個(gè)DataFrame中方便計(jì)算相似度
val df_sim_1 = df_group.join(df_user_sum,df_group("userid")===df_user_sum("cast (user_id_sum as string)"))
.drop("user_id_sum")
df_sim_1.show()
//與上一步作用相同
val df_user_sum1 = df_user_sum.withColumnRenamed("rating_sqrt_sum","rating_sqrt_sum1")
val df_sim = df_sim_1.join(df_user_sum1,df_sim_1("userid")===df_user_sum1("userid_sum"))
.drop("userid_sum")
df_sim.show()
//定義udf計(jì)算用戶(hù)之間的相似度(根據(jù)cos距離計(jì)算)
val sim_udf = udf((pro:Double,s1:Double,s2:Double) => pro/(s1.toDouble * s2.toDouble))
val df_res = df_sim.withColumn("sim",sim_udf(col("rating_sum_pro"),col("rating_sqrt_sum"),col("rating_sqrt_sum1")))
.select("userid","userid_1","sim")
df_res.show()
// 獲取相似物品集合:
def simUserItem(df_res:DataFrame,df:DataFrame,spark:SparkSession):DataFrame={
import spark.implicits._
//2.1 以u(píng)ser_id為key進(jìn)行聚合,對(duì)value值中相似度值進(jìn)行降序排列页徐,并取前10個(gè)苏潜,然后對(duì)value進(jìn)行行轉(zhuǎn)列
val df_nsim = df_res.rdd.map(x=>(x(0).toString,(x(1).toString,x(2).toString)))
.groupByKey()
.mapValues{x=>
x.toArray.sortWith((x,y)=>x._2>y._2).slice(0,10)
}.flatMapValues(x=>x).toDF("user_id","user_id1_sim")
.selectExpr("user_id","user_id1_sim._1 as user_id1","user_id1_sim._2 as sim")
val df_user_item1 = df.rdd.map(x=>(x(0).toString,x(1).toString+"_"+x(2).toString))
.groupByKey().mapValues{x=>
x.toArray
}.toDF("user_id_gen_item","item_rating_array")
val df_user_item2 = df_user_item1.withColumnRenamed("item_rating_array","item_rating_array1")
//2.2分別為user_id和user_id1攜帶items進(jìn)行過(guò)濾
val df_gen_item_tmp = df_nsim.join(df_user_item1,
df_nsim("user_id")===df_user_item1("user_id_gen_item"))
.drop("user_id_gen_item")
val df_gen_item = df_gen_item_tmp.join(df_user_item2,
df_gen_item_tmp("user_id1")===df_user_item2("user_id_gen_item"))
.drop("user_id_gen_item")
//2.3用一個(gè)udf過(guò)濾user_id1:item2中被user_id打過(guò)分的item,其中user_id和user_id1是相似用戶(hù)
val filter_udf = udf{(items1:Seq[String],items2:Seq[String])=>
val fMap = items1.map{x=>val l = x.split("_")
(l(0),l(1))
}.toMap
items2.filter{x=>
val l = x.split("_")
fMap.getOrElse(l(0),"-")=="-"
}
}
val df_filter_item = df_gen_item.withColumn("filtered_item",
filter_udf(col("item_rating_array"),col("item_rating_array1")))
.select("user_id","sim","filtered_item")
//2.4公式計(jì)算 相似度*rating
val sim_rating_udf = udf{(sim:Double,filter_item:Seq[String])=>
filter_item.map{x=>val l=x.split("_")
l(0)+"_"+l(1).toDouble*sim
}
}
val itemSimRating = df_filter_item
.withColumn("item_product",sim_rating_udf(col("sim"),col("filtered_item")))
.select("user_id","item_product")
val df_user_item_score = itemSimRating.select(itemSimRating("user_id"),explode(itemSimRating("item_product")))
.toDF("user_id","item_product")
.selectExpr("user_id","split(item_product,'_')[0] as item_id",
"cast(split(item_product,'_')[1] as double) as score")
df_user_item_score