天天看點

資料挖掘基礎-3.推薦系統一、基于内容的推薦CB二、基于協同的推薦CF 

一、基于内容的推薦CB

基于内容推薦,即根據使用者的輸入産生推薦的内容,推薦的方式主要可以分為基于item屬性和基于user屬性。

1.基于Item屬性

資料挖掘基礎-3.推薦系統一、基于内容的推薦CB二、基于協同的推薦CF 

推薦的物品(item):可為商品、音樂、網站等等。基于item的屬性推薦可以是根據item的名稱、item的類型、或者是其他屬性,下面對根據item名稱推薦進行舉例。

舉例:假如資料庫中有3條記錄:9900、9901、9904,通過中文分詞,可以将item的名稱進行切分,形成正排表,score為該詞的tf-idf值,這個值根據實際業務設定。

9900 海賊王視訊,可以分為:海賊王:score1,視訊:score2;
9901 海賊王動漫,可以分為:海賊王:score3,動漫:score4;
9904 海賊王漫畫,可以分為:海賊王:score5,漫畫:score6;
           

得到正排表後,需要将正排表處理成倒排表,用于檢索使用。這裡将相同token聚合在一起,得到該token關聯的所有記錄。

海賊王->9900:score1,9901:score3,9904:score5
視  頻->9900:score2,
動  漫->9901:score4
漫  畫->9904:score6
           

當使用者輸入“海賊王視訊”時(也可以是點選了某個名稱為“海賊王視訊”的item),系統将詞條進行拆分,變成“海賊王”和“視訊”,通過“海賊王”可以獲得9900,9901,9904推薦,通過“視訊”可以獲得9900推薦,對于同樣的推薦,說明該推薦比較重要,将分數相加,最終結果就是:

9900:score1+ score2
9901:score3
9904:score5
           

推薦的分數越高排名越靠前,越值得推薦,網頁或者是APP就會根據最終得到的前N個推薦進行展示。這種推薦方式雖然相關性很好,但是如果不同的使用者搜尋的内容一樣,推薦結果都是一樣的,使用者個性化設計比較差。是以引入下面基于user的推薦。

2.基于User屬性

基于user即在item的基礎上,添加user的屬性分析,不同的使用者有不同的興趣愛好,将使用者的這些資訊加入到相關性計算中,進而得到更貼近使用者偏好的推薦。但是存在的問題是,如果完全根據使用者的偏好來推薦,有可能會造成不斷推薦一類item的馬太效應,造成審美疲勞。

資料挖掘基礎-3.推薦系統一、基于内容的推薦CB二、基于協同的推薦CF 

3.基于item推薦Python代碼

在mr_fenci_test目錄的基礎上,具體參考資料挖掘基礎-2.中文分詞。接下來的代碼是為了形成一個倒排表。

1)run.sh

HADOOP_CMD="/usr/local/src/hadoop-2.6.0-cdh5.7.0/bin/hadoop"
STREAM_JAR_PATH="/usr/local/src/hadoop-2.6.0-cdh5.7.0/share/hadoop/tools/lib/hadoop-streaming-2.6.0-cdh5.7.0.jar"


INPUT_FILE_PATH_1="/music_meta.txt.small"
OUTPUT_Z_PATH="/output_z_fenci"
OUTPUT_D_PATH="/output_d_fenci"

$HADOOP_CMD fs -rmr $OUTPUT_Z_PATH
$HADOOP_CMD fs -rmr $OUTPUT_D_PATH

# Step 1.

$HADOOP_CMD jar $STREAM_JAR_PATH \
    -input $INPUT_FILE_PATH_1 \
    -output $OUTPUT_Z_PATH \
    -mapper "python map.py mapper_func" \
    -jobconf "mapred.reduce.tasks=0" \
    -jobconf "mapred.job.name=jieba_fenci_demo" \
    -file "./jieba.tar.gz" \
    -file "./map.py"

# Step 2.//形成一個倒排表
$HADOOP_CMD jar $STREAM_JAR_PATH \
    -input $OUTPUT_Z_PATH \  //輸入一個正排表
    -output $OUTPUT_D_PATH \
    -mapper "python map_inverted.py mapper_func" \
    -reducer "python red_inverted.py reducer_func" \
    -jobconf "mapred.reduce.tasks=2" \
    -jobconf "mapred.job.name=jieba_fenci" \
    -file "./map_inverted.py" \
    -file "./red_inverted.py"
           

2)map.py

先進行分詞。

#!/usr/bin/python
import os
import sys

os.system('tar xvzf jieba.tar.gz > /dev/null')
reload(sys)
sys.setdefaultencoding('utf-8')
sys.path.append("./")//将jieba子產品加載進來

import jieba
import jieba.posseg
import jieba.analyse

def mapper_func():
    for line in sys.stdin:
        ss = line.strip().split('\t')
        if len(ss) != 2:
            continue
        music_id = ss[0].strip()
        music_name = ss[1].strip()
        tmp_list = []
        for x, w in jieba.analyse.extract_tags(music_name, withWeight=True):#考慮了權重
            tmp_list.append((x, float(w)))
        final_token_score_list = sorted(tmp_list, key=lambda x: x[1], reverse=True)#排序
        print '\t'.join([music_id, music_name, '^L'.join(['^B'.join([t_w[0], str(t_w[1])]) for t_w in final_token_score_list])])

if __name__ == "__main__":
    module = sys.modules[__name__]
    func = getattr(module, sys.argv[1])
    args = None
    if len(sys.argv) > 1:
        args = sys.argv[2:]
    func(*args)
           

3)map_inverted.py

這裡将每個分詞和對應的item進行關聯。

#!/usr/bin/python

import os
import sys

def mapper_func():
    for line in sys.stdin:
        ss = line.strip().split('\t')
        if len(ss) != 3:
            continue
        music_id = ss[0].strip()
        music_name = ss[1].strip()
        music_fealist = ss[2].strip()
//形成倒排表
        for fea in music_fealist.split('^L'): //^L是指Ctrl+L
            token, weight = fea.strip().split('^B')
            print '\t'.join([token, music_name, weight])

if __name__ == "__main__":
    module = sys.modules[__name__]
    func = getattr(module, sys.argv[1])
    args = None
    if len(sys.argv) > 1:
        args = sys.argv[2:]
    func(*args)
           

最後形成的結果舉例:

快給我    影視-心上人啊快給我力量KTV(電影《神聖的使命》插曲 1.10985051628

插曲         影視-心上人啊快給我力量KTV(電影《神聖的使命》插曲 1.08069893134

影視         影視-心上人啊快給我力量KTV(電影《神聖的使命》插曲 0.948820609586

這個作為red_inverted.py 的輸入,作為輸入前需要進行排序,将一樣的token都放在一起

4)red_inverted.py

#!/usr/bin/python

import os
import sys

def reducer_func():
    cur_token = None
    m_list = [] 将一個詞的倒排表全部放進去
    for line in sys.stdin:
        ss = line.strip().split('\t')
        if len(ss) != 3:
            continue
        token = ss[0].strip()
        name = ss[1].strip()
        weight = float(ss[2].strip())

        if cur_token == None:
            cur_token = token
        if cur_token != token:
            final_list = sorted(m_list, key=lambda x: x[1], reverse=True)形成一個倒序
            print '\t'.join([cur_token, '^L '.join(['^B'.join([name_weight[0], str(name_weight[1])]) for name_weight in final_list])])
            cur_token = token
            m_list = []
        m_list.append((name, weight))
//将最後一個token也弄進來
    final_list = sorted(m_list, key=lambda x: x[1], reverse=True)
    print '\t'.join([cur_token, '^L'.join(['^B'.join([name_weight[0], str(name_weight[1])]) for name_weight in final_list])])

if __name__ == "__main__":
    module = sys.modules[__name__]
    func = getattr(module, sys.argv[1])
    args = None
    if len(sys.argv) > 1:
        args = sys.argv[2:]
    func(*args)
           

[[email protected] mr_fenci_test]# cat map_inverted.data |sort -k1| python red_inverted.py reducer_func > red_inverted.data

0010    金曲國樂交響詩-0010^B2.98869187572^A70 80後經典電視劇片頭大盤點 超完整-0010^B1.49434593786

0010是拆分出來的詞,後面的item之間用^A分割,item和score之間用^B進行分割。

5)web_rc.py

這裡需要下載下傳web子產品,這樣可以向浏覽器輸入詞或者句子,浏覽器将會輸出推薦的結果,即查到上面得到的反向索引所對應的item。

#encoding=utf-8

import web
import sys

sys.path.append("./")

import jieba
import jieba.posseg
import jieba.analyse

urls = (
    '/', 'index',
)

app = web.application(urls, globals())
rec_map={}
with open('2.data','r') as fd : #2.data就是reduce生成的倒排推薦表
        for line in fd:
                ss=line.strip().split('\t');
                if len(ss)!=2:
                        continue
                token = ss[0].strip()
                music_rec_list_str= ss[1].strip()
                for music_score in music_rec_list_str.split('^L '):
                        name,score = music_score.strip().split('^B')
                        if token not in rec_map:
                                rec_map[token] = []
                        rec_map[token].append((name,round(float(score),2)))
class index:
    def GET(self):
        params = web.input()
        context = params.get('context', '')
        seg_list = jieba.cut(context)
        rec_map1={}
        for seg in seg_list:
                if seg in rec_map:
                        for name_score in rec_map[seg]:
                                name, score = name_score
                                if name not in rec_map1:
                                        rec_map1[name]=score
                                else:
                                        old_score = rec_map1[name]
                                        new_score = old_score+score
                                        rec_map1[name]= new_score
        rec_list =[]
        for k,v in rec_map1.items():
                rec_list.append('\t'.join([k,str(v)]))
        return "\r\n".join(rec_list)

if __name__ == "__main__":
    app.run()
           

二、基于協同的推薦CF

資料挖掘基礎-3.推薦系統一、基于内容的推薦CB二、基于協同的推薦CF 

優點:充分利用群體智慧、推薦精度高于CB、利于挖掘隐含的相關性

缺點:推薦結果解釋性較差、對時效性強的Item不适用,因為需要使用者積累資料、冷啟動問題

1.user-BasedCF

• 假設

– 使用者喜歡那些跟他有相似愛好的使用者喜歡的東西

– 具有相似興趣的使用者在未來也具有相似興趣

• 方法

– 給定使用者u,找到一個使用者的集合N(u),他們和u具有相似的興趣

– 将N(u)喜歡的物品推薦給使用者

資料挖掘基礎-3.推薦系統一、基于内容的推薦CB二、基于協同的推薦CF 

UI矩陣得到UU矩陣,歸一化後如上右圖,推薦C titanic的機率就是看BD和A到底多相似,相似程度乘以該使用者推薦程度,就得到上面的結果,因為實際上使用者很多,不可能考慮所有使用者的情況,這裡僅僅考慮了BD,模拟實際情況。

2.item-BasedCF

• 假設

– 使用者喜歡跟他過去喜歡的物品相似的物品

– 曆史上相似的物品在未來也相似

• 方法

– 給定使用者u,找到他過去喜歡的物品的集合R(u)

– 把和R(u)相似的物品推薦給u

資料挖掘基礎-3.推薦系統一、基于内容的推薦CB二、基于協同的推薦CF 
User-based Item-based
性能 适用使用者較少場合,如果使用者多,計算使用者相似矩陣代價太大 适用于物品數明顯小于使用者數的場合,如果物品很多,計算物品相似度矩陣代價很大
領域 時效性強,使用者個性化興趣不太明顯的領域 長尾物品豐富,使用者個性化需求強烈的領域
實時性 使用者有新行為,不一定造成推薦結果立即變化 使用者有新行為,一定會導緻推薦結果的實時變化
冷啟動

在新使用者對很少的物品産生行為後,不能立即對她進行個性化推薦,因為使用者相似度表是每個一段時間離線計算的新物品上線後一段時間,一旦有使用者對物品産生行為,就可以将新物品推薦給和對它産

生行為的使用者興趣相似的其他使用者

新使用者隻要對一個物品産生行為,就可以給他推薦和該物品相關的其他物品但沒有辦法在不離線更新物品相似度表的情況下将新物品推薦給使用者
推薦理由 很難提供令使用者信服的推薦解釋 利用使用者的曆史行為給使用者做推薦解釋,可以令使用者比較信服

3.冷啟動

1)使用者冷啟動

• 提供熱門排行榜,等使用者資料收集到一定程度再切換到個性化推薦

• 利用使用者注冊時提供的年齡、性别等資料做粗粒度的個性化

• 利用使用者社交網絡賬号,導入使用者在社交網站上的好友資訊,然後給使用者推薦其好友喜歡的物品

• 在使用者新登入時要求其對一些物品進行回報,收集這些興趣資訊,然後給使用者推薦相似的物品

2)物品冷啟動

• 給新物品推薦給可能對它感興趣的使用者,利用内容資訊,将他們推薦給喜歡過和它們相似的物品的使用者

• 物品必須能夠在第一時間展現給使用者,否則經過一段事件後,物品的價值就大大降低了

• UserCF和ItemCF都行不通,隻能利用Content based解決該問題,頻繁更新相關性資料

3)系統冷啟動

• 引入專家知識,通過一定高效方式迅速建立起物品的相關性矩陣

4.計算方式

結合MapReduce計算時,由于資料量很大,可以通過倒排式或方塊式進行。

1)倒排式

資料挖掘基礎-3.推薦系統一、基于内容的推薦CB二、基于協同的推薦CF 

相似度計算公式

資料挖掘基礎-3.推薦系統一、基于内容的推薦CB二、基于協同的推薦CF 

• 詳細Hadoop過程:

– 在MR的map階段将每個使用者的評分item組合成pair<left,right,leftscore,rightscore> 輸出,left作為分發鍵,left+right作為排序鍵。

– 在reduce階段,将map中過來的資料掃一遍即可求得所有item的相似度。

資料挖掘基礎-3.推薦系統一、基于内容的推薦CB二、基于協同的推薦CF 

2)分塊式

資料挖掘基礎-3.推薦系統一、基于内容的推薦CB二、基于協同的推薦CF 

5.倒排式開發步驟

現在将要完成的是:通過ui矩陣得到ii矩陣,即計算不同item之間的相似度,以此考慮是否向user推薦相關聯的item。

準備的資料格式:uid,itemid,score。其中score是使用者對該item的打分。

3666038384,1000139509,0.037646     
807800026,1000411209,0.514877       
3464005685,1000411209,0.487496
           

1)歸一UI矩陣

為了簡化之後的運算,在開始計算兩個item的相似度之前,先将score進行歸一化計算,依據的公式即倒排式計算公式。這裡需要先理清思路,score是使用者對item的打分,其他的使用者也可能對該item進行評分,是以為了歸一化,就必須将具有相同item的user進行聚合。

• Map

– In

   » Line: u, i, s

– Out

  » Key: i

  » Value: u, s

map程式将資料讀入,并将uis的順序改變了一下,值都不變。這裡,就可以使用i作為key,将不同的user進行聚合。

• Reduce

– In

  » Key: i

  » Value: list((u, s))

– Out

» Key: u

» Value: i, s_new(歸一化後的)

reduce将一樣的item聚合在一起,也就是同一個item,對應多個user,然後将所有的分數進行歸一化計算,得到s_new,輸出時,u變為key,value變成了item和對應的歸一化後的分數。

2)衍生2個 Pair對

歸一化之後,就需要開始計算item兩兩之間的相似度,為此,

• Map

– In

   » Key: u

   » Value: i, s_new

– Out

   » Key: u

   » Value: i, s_new

這裡沒有做任何的變換,隻是為了接下來,把一樣的user聚合在一起,傳入給reduce

• Reduce

– In

   » Key: u

   » Value: list((i, s_new))

– Out

   » Key_1: i

   » Value_1: j, s_new_i * s_new_j

   » Key_2: j

   » Value_2: i, s_new_i * s_new_j

這裡的i,j即不同的item,因為ij的相似度和ji的相似度一緻,是以将會輸出兩個pair對,矩陣将會是對稱的。

3)生成結果

上面計算的結果,是根據user進行聚合,但是不同的user将可能會對同樣的兩個item評分,是以第二步計算出的每個結果,可能隻是最終結果的一個子集。例如:userA對item1和item2有打分,從第二步計算出了兩個item的相似度,userB對item1和item2也有打分,也計算出了相似度,如果多個使用者都對這兩個item有打分,說明這兩個item相似度比較高,分數需要疊加。是以,這裡的第三步将會完成這個分數疊加的步驟。

• Map

– In

   » Key: i

   » Value: j, s_new_i * s_new_j

– Out

   » Key: <i, j>

   » Value: s_new_i * s_new_j

• Reduce

– In

  » Key: <i, j>

  » Value: list((s_new_i * s_new_j))

– Out

  » Key: i

  » Value: j, score

将所有的分數進行相加,得到item之間的相關度,最終得到的結果是個ii矩陣。

6.Python代碼實作 

1)1_gen_ui_map.py

#!/usr/local/bin/python
import sys

for line in sys.stdin:
    u, i, s = line.strip().split(',')
    print "%s\t%s\t%s" % (i, u, s) #将資料輸入,并且按照i、u、s的順序輸入,中間按照tab分割
           

2)1_gen_ui_reduce.py

#!/usr/local/bin/python

import sys
import math

cur_item = None
user_score_list = []

for line in sys.stdin:
        item, user, score = line.strip().split("\t")
        if not cur_item:
                cur_item = item
        if item != cur_item:
                sum = 0.0
                for tuple in user_score_list:
                        (u, s) = tuple
                        sum += pow(s, 2)
                sum = math.sqrt(sum)
                for tuple in user_score_list:
                        (u, s) = tuple
                        print "%s\t%s\t%s" % (u, cur_item, float(s / sum))
                user_score_list = []
                cur_item = item
        user_score_list.append((user, float(score)))
sum = 0.0
for tuple in user_score_list:
        (u, s) = tuple
        sum += pow(s, 2)
sum = math.sqrt(sum)
for tuple in user_score_list:
        (u, s) = tuple
        print "%s\t%s\t%s" % (u, cur_item, float(s / sum))
           

3)2_gen_ii_pair_map.py

#!/usr/local/bin/python

import sys
for line in sys.stdin:
         u, i, s =line.strip().split('\t')
         print "%s\t%s\t%s" % (u, i, s)
           

4)2_gen_ii_pair_reduce.py

#!/usr/local/bin/python

import sys

cur_user = None
item_score_list = []

for line in sys.stdin:
         user, item, score = line.strip().split("\t")
         if not cur_user:
                  cur_user = user
         if user != cur_user:
                  for i in range(0, len(item_score_list) - 1):
                          for j in range(i + 1, len(item_score_list)):
                                   item_a, score_a = item_score_list[i]
                                   item_b, score_b = item_score_list[j]
                                   print "%s\t%s\t%s" % (item_a, item_b, score_a * score_b)
                                   print "%s\t%s\t%s" % (item_b, item_a, score_a * score_b)

                  item_score_list = []
                  cur_user = user
         item_score_list.append((item, float(score)))

for i in range(0, len(item_score_list) - 1):
         for j in range(i + 1, len(item_score_list)):
                  item_a, score_a = item_score_list[i]
                  item_b, score_b = item_score_list[j]
                  print "%s\t%s\t%s" % (item_a, item_b, score_a * score_b)
                  print "%s\t%s\t%s" % (item_b, item_a, score_a * score_b)
           

5)3_sum_map.py

#!/usr/local/bin/python

import sys

for line in sys.stdin:
         i_a, i_b, s = line.strip().split('\t')
         print "%s\t%s" % (i_a + "_" + i_b, s)
           

6)3_sum_reduce.py

#!/usr/local/bin/python

import sys

cur_ii_pair = None
score = 0.0

for line in sys.stdin:
         ii_pair, s = line.strip().split("\t")
         if not cur_ii_pair:
                  cur_ii_pair = ii_pair
         if ii_pair != cur_ii_pair:
                  item_a, item_b = cur_ii_pair.split('_')
                  print "%s\t%s\t%s" % (item_a, item_b, score)
                  cur_ii_pair = ii_pair
                  score = 0.0
         score += float(s)

item_a, item_b = cur_ii_pair.split('_')

print "%s\t%s\t%s" % (item_a, item_b, score)
           

7)run.sh

HADOOP_CMD="/usr/local/src/hadoop-2.6.0-cdh5.7.0/bin/hadoop"
STREAM_JAR_PATH="/usr/local/src/hadoop-2.6.0-cdh5.7.0/share/hadoop/tools/lib/hadoop-streaming-2.6.0-cdh5.7.0.jar"

INPUT_FILE_PATH_1="/part-00151" #這是最原始的輸入資料,内容格式如上
OUTPUT_PATH_1="/output1"
OUTPUT_PATH_2="/output2"
OUTPUT_PATH_3="/output3"

$HADOOP_CMD fs -rmr -skipTrash $OUTPUT_PATH_1
$HADOOP_CMD fs -rmr -skipTrash $OUTPUT_PATH_2
$HADOOP_CMD fs -rmr -skipTrash $OUTPUT_PATH_3

# Step 1.
$HADOOP_CMD jar $STREAM_JAR_PATH \
                  -input $INPUT_FILE_PATH_1 \
                  -output $OUTPUT_PATH_1 \
                  -mapper "python 1_gen_ui_map.py" \
                  -reducer "python 1_gen_ui_reduce.py" \
                  -file ./1_gen_ui_map.py \
                  -file ./1_gen_ui_reduce.py
# Step 2.
$HADOOP_CMD jar $STREAM_JAR_PATH \
                  -input $OUTPUT_PATH_1 \
                  -output $OUTPUT_PATH_2 \
                  -mapper "python 2_gen_ii_pair_map.py" \
                  -reducer "python 2_gen_ii_pair_reduce.py" \
                  -file ./2_gen_ii_pair_map.py \
                  -file ./2_gen_ii_pair_reduce.py
# Step 3.
$HADOOP_CMD jar $STREAM_JAR_PATH \
                  -input $OUTPUT_PATH_2 \
                  -output $OUTPUT_PATH_3 \
                  -mapper "python 3_sum_map.py" \
                  -reducer "python 3_sum_reduce.py" \
                  -file ./3_sum_map.py \
                  -file ./3_sum_reduce.py
           

繼續閱讀