音乐推荐系统

  音乐频道推荐业务,支持各个产品业务和策略。这里先使用CB+CF+LR实现推荐部分,下面具体展开:

一、推荐系统流程图

  CB,CF算法在召回阶段使用,推荐出来的item是粗排的,利用LR算法,可以将CB,CF召回来的item进行精排,然后选择分数最高,给用户推荐出来。后续我们可以采用矩阵分解、聚类、深度学习算法来实现对候选集合的召回。

二、推荐系统思路详解

  话不多说,这里先放上代码思路:

1、数据预处理(用户画像数据、物品元数据、用户行为数据) 

2、召回(CB、CF算法)

3、LR训练模型的数据准备,即用户特征数据,物品特征数据

4、模型准备,即通过LR算法训练模型数据得到w,b 

5、推荐系统流程:

(1)解析请求:userid,itemid
(2)加载模型:加载排序模型(model.w,model.b)
(3)检索候选集合:利用cb,cf去redis里面检索数据库,得到候选集合
(4)获取用户特征:userid
(5)获取物品特征:itemid
(6)打分(逻辑回归,深度学习),排序
(7)top-n过滤
(8)数据包装(itemid->name),返回

三、推荐系统实现

3.1、数据预处理

  (1)用户画像数据:user_profile.data
  userid,性别,年龄,收入,地域

  (2)物品(音乐)元数据:music_meta
  
itemid,name,desc,时长,地域,标签

  (3)用户行为数据:user_watch_pref.sml
  userid,itemid,该用户对该物品的收听时长,点击时间(小时)

  首先,将3份数据融合到一份数据中
  执行python gen_base.py

 1 #coding=utf-8
 2 
 3 '''
 4     总体思路:处理原始的数据:1、用户画像数据 2、物品元数据 3、用户行为数据
 5     把三类数据统一到一个文件里面,供后面cb、cf算法进行计算权重
 6 '''
 7 
 8 import sys
 9 
10 #找到三类原始数据文件,用户画像数据、物品元数据,用户行为数据
11 user_action_data = '../data/user_watch_pref.sml'
12 music_meta_data = '../data/music_meta'
13 user_profile_data = '../data/user_profile.data'
14 
15 #将三类处理后的元数据放到新的文件里面,这里我们需要定一个文件名,路径
16 output_file = '../data/merge_base.data'
17 
18 # 将3份数据merge后的结果输出,供下游数据处理
19 ofile = open(output_file, 'w')
20 
21 # step 1. 处理物品元数据,将处理后的结果放入字典里面,key是itemid,value为物品对应的信息,为最后写入做准备
22 item_info_dict = {}
23 with open(music_meta_data, 'r') as fd:
24     for line in fd:
25         ss = line.strip().split('\001')
26         if len(ss) != 6:
27             continue
28         itemid, name, desc, total_timelen, location, tags = ss
29         item_info_dict[itemid] = '\001'.join([name, desc, total_timelen, location, tags])
30 
31 # step 2. 处理用户画像数据,将处理后的结果放入字典里面,key是用户id,value是用户信息
32 user_profile_dict = {}
33 with open(user_profile_data, 'r') as fd:
34     for line in fd:
35         ss = line.strip().split(',')
36         if len(ss) != 5:
37             continue
38         userid, gender, age, salary, location = ss
39         user_profile_dict[userid] = '\001'.join([gender, age, salary, location])
40 
41 # step 3. 写入最后的信息,将用户行为数据进行处理,把step1和step2得到的数据一并归纳在文件里面
42 with open(user_action_data, 'r') as fd:
43     for line in fd:
44         ss = line.strip().split('\001')
45         if len(ss) != 4:
46             continue
47         userid, itemid, watch_len, hour = ss
48 
49         if userid not in user_profile_dict:
50             continue
51 
52         if itemid not in item_info_dict:
53             continue
54 
55         ofile.write('\001'.join([userid, itemid, watch_len, hour, \
56                 user_profile_dict[userid], item_info_dict[itemid]]))
57         ofile.write("\n")
58 
59 ofile.close()

 

  得到类似下面数据merge_base.data

  • 01e3fdf415107cd6046a07481fbed499^A6470209102^A1635^A21^A男^A36-45^A20000-100000^A内蒙古^A黄家驹1993演唱会高清视频^A^A1969^A^A演唱会

3.2、【召回】CB算法

  (1)以token itemid score形式整理训练数据
  
利用jieba分词,对item name进行中文分词

  python gen_cb_train.py

  1 #coding=utf-8
  2 
  3 '''
  4     总体思路:将初始化好的用户,物品,用户行为数据进行处理,目的是为了得到token,itemid,score,我们知道生成的数据里面的name,
  5     将itemName进行分词,得到tfidf权重,同时将desc进行分词,处理name和desc,我们在元数据中还有已经分类好的tags,tags已经切分好
  6     了没必要再次进行切分,只需要用idf词表查处权重即可,但是对于name、desc、tags这三个分词结果,我们对name的结果应该更加偏重一
  7     点,所以分别对这三类得出的分数再次进行分数权重划分,最后得到cb的初始数据
  8 '''
  9 
 10 import sys
 11 sys.path.append('../')
 12 reload(sys)
 13 sys.setdefaultencoding('utf-8')
 14 
 15 import jieba
 16 import jieba.posseg
 17 import jieba.analyse
 18 
 19 
 20 #读入初始数据
 21 input_file = "../data/merge_base.data"
 22 
 23 # 输出cb训练数据
 24 output_file = '../data/cb_train.data'
 25 ofile = open(output_file, 'w')
 26 
 27 #定义三类的权重分数
 28 RATIO_FOR_NAME = 0.9
 29 RATIO_FOR_DESC = 0.1
 30 RATIO_FOR_TAGS = 0.05
 31 
 32 
 33 #为tags读入idf权重值
 34 idf_file = '../data/idf.txt'
 35 idf_dict = {}
 36 with open(idf_file, 'r') as fd:
 37     for line in fd:
 38         token, idf_score = line.strip().split(' ')
 39         idf_dict[token] = idf_score
 40 
 41 #开始处理初始数据
 42 itemid_set = set()
 43 with open(input_file, 'r') as fd:
 44     for line in fd:
 45         ss = line.strip().split('\001')
 46         # 用户行为
 47         userid = ss[0].strip()
 48         itemid = ss[1].strip()
 49         watch_len = ss[2].strip()
 50         hour = ss[3].strip()
 51         # 用户画像
 52         gender = ss[4].strip()
 53         age = ss[5].strip()
 54         salary = ss[6].strip()
 55         user_location = ss[7].strip()
 56         # 物品元数据
 57         name = ss[8].strip()
 58         desc = ss[9].strip()
 59         total_timelen = ss[10].strip()
 60         item_location = ss[11].strip()
 61         tags = ss[12].strip()
 62 
 63         # 对item去重,相同的itemid不用再计算,因为都一样,这里用到continue特性,当不同的时候才继续执行下面的代码
 64         if itemid not in itemid_set:
 65             itemid_set.add(itemid)
 66         else:
 67             continue
 68 
 69         # 去掉重复后的itemid,然后我们进行分词,计算权重,放到字典里面
 70         token_dict = {}
 71         #对name统计
 72         for a in jieba.analyse.extract_tags(name, withWeight=True):
 73             token = a[0]
 74             score = float(a[1])
 75             token_dict[token] = score * RATIO_FOR_NAME
 76 
 77         #对desc进行分词,这里需要注意的是描述一般会含有name中的词,这里我们把有的词的分数进行相加,没有的放入
 78         for a in jieba.analyse.extract_tags(desc, withWeight=True):
 79             token = a[0]
 80             score = float(a[1])
 81             if token in token_dict:
 82                 token_dict[token] += score * RATIO_FOR_DESC
 83             else:
 84                 token_dict[token] = score * RATIO_FOR_DESC
 85 
 86         # 对tags 进行分数计算
 87         for tag in tags.strip().split(','):
 88             if tag not in idf_dict:
 89                 continue
 90             else:
 91                 if tag in token_dict:
 92                     token_dict[tag] += float(idf_dict[tag]) * RATIO_FOR_TAGS
 93                 else:
 94                     token_dict[tag] = float(idf_dict[tag]) * RATIO_FOR_TAGS
 95 
 96         #循环遍历token_dict,输出toke,itemid,score
 97         for k, v in token_dict.items():
 98             token = k.strip()
 99             score = str(v)
100             ofile.write(','.join([token, itemid, score]))
101             ofile.write("\n")
102 
103 
104 ofile.close()

 

  得到如下数据:

  • 翻译,4090309101,0.561911164569(最后一个是一个不是传统的TF-IDF,因为分出的词在name,desc,tag里面他的重要性是不一样的)

  (2)用协同过滤算法跑出item-item数据

  相似的item配对,II矩阵的形成。相似度计算,我们要用到MapReduce的框架来进行,只要是用到shuffle阶段,对map出来的结果排序,reduce进行两两配对,这里就是主要的wordcount逻辑,主要说下注意的部分:我们需要把两两分数的过滤掉,或是把itemA和itemB相同的item过滤掉,因为这部分数据没有任何意义

  map阶段:

#!usr/bin/python
# -*- coding: UTF-8 -*-
 
'''
    总体思路:这里需要把初始化后的结果进行map排序,为了后续两两取    pair对,所以这里我们需要进行map,其实什么也不用操作输出即可
'''
import sys
import re
 
for line in sys.stdin:
    ss = line.strip().split(',')
    if len(ss) != 3:
        continue
 
    r1 = u'[a-zA-Z0-9’!"#$%&\'()*+,-./:;<=>?@,。?★、…【】《》?“”‘’![\\]^_`{|}~]+'
    ss[0] = re.sub(r1,'',ss[0])
    if len(ss[0]) == 0:
        continue
    print ','.join([ss[0], ss[1], ss[2]])

  

  reduce阶段:

#!usr/bin/python
# -*- coding: UTF-8 -*
'''
    我们前面已经在pair reduce之前我们做过map操作,输出以token,item,score输出,所以排序是token排好的序
    这里我们相当于求的是II矩阵,所以是根相同的token的item进行相似度计算
    思路:
        1、进行user统计,若相同,把相同的user的item和score放入list里面
        2、不相同,开始进行两两配对,循环该list,进行两两配对,求出相似度
'''
 
import  sys
import  math
 
cur_token = None
item_score_list = []
for line in sys.stdin:
    ss = line.strip().split(',')
    itemid = ss[1]
    score = float(ss[2])
    if len(ss) != 3:
        continue
    if cur_token == None:
        cur_token = ss[0]
 
    if cur_token != ss[0]:
 
        #这里需要注意的是range的区间前闭后开,同时注意range中即使前闭后开,刚开始是从0即列表里面的第一个,循环到列表最后一个的前一个
        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]
                #score = float(score_a * score_b)/float(math.sqrt(pow(score_a,2))*math.sqrt(pow(score_b,2)))
                #输出两遍的目的是为了形成II矩阵的对称
                score = float(score_a*score_b)
                if item_a == item_b:
                    continue
                if score < 0.08:
                    continue
 
                print "%s\t%s\t%s" % (item_a, item_b, score)
                print "%s\t%s\t%s" % (item_b, item_a, score)
        cur_token = ss[0]
        item_score_list = []
 
    item_score_list.append((itemid,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]
        #score = (score_a * score_b) / (math.sqrt(pow(score_a, 2)) * math.sqrt(pow(score_b, 2))
        # 输出两遍的目的是为了形成II矩阵的对称
        score = float(score_a * score_b)
        if item_a == item_b:
            continue
        if score < 0.08:
            continue
 
        print "%s\t%s\t%s" % (item_a, item_b, score)
        print "%s\t%s\t%s" % (item_b, item_a, score)

 

  最后得到基于cb的ii矩阵

  (3)对数据格式化,item-> item list形式,整理出KV形式
  
python gen_reclist.py

 1 #coding=utf-8
 2 '''
 3     思路:我们已经通过CB算法得到itemA,itemB,score,然后我们需要把放入到redis库,存入的方法,
 4     我们以itemA为key与itemA有相似度的itemB,和分数,以value的形式存入内存库
 5          1、创建一个字典,将key放入itemA,value 放入与A对应的不同b和分数
 6          2、循环遍历字典,将key加上前缀CB,value以从大到小的分数进行排序,并且相同的item以——分割,item和score间用:分割
 7 '''
 8 
 9 import sys
10 
11 infile = '../data/cb.result'
12 outfile = '../data/cb_reclist.redis'
13 
14 ofile = open(outfile, 'w')
15 
16 MAX_RECLIST_SIZE = 100
17 PREFIX = 'CB_'
18 
19 rec_dict = {}
20 with open(infile, 'r') as fd:
21     for line in fd:
22         itemid_A, itemid_B, sim_score = line.strip().split('\t')
23 
24         #判断itemA在不在该字典里面,若不在,创建一个key为itemA的列表,把与itemA相关联的itemB和score添加进去
25         if itemid_A not in rec_dict:
26             rec_dict[itemid_A] = []
27         rec_dict[itemid_A].append((itemid_B, sim_score))
28 
29 #循环遍历字典,格式化数据,把itemB和score中间以:分割,不同的itemB以_分割
30 for k, v in rec_dict.items():
31     key_item = PREFIX + k
32     
33     #接下来格式化数据,将数据以从大到小排列后再格式化
34     #排序,由于数据量大,我们只取100个
35     #排好序后,我们来格式化数据
36     reclist_result = '_'.join([':'.join([tu[0], str(round(float(tu[1]), 6))]) \
37               for tu in sorted(v, key=lambda x: x[1], reverse=True)[:MAX_RECLIST_SIZE]])
38 
39     ofile.write(' '.join(['SET', key_item, reclist_result]))
40     ofile.write("\n")
41 
42 ofile.close()

 

  类似如下数据:

  • SET CB_5305109176 726100303:0.393048_953500302:0.393048_6193109237:0.348855

4)灌库(redis

  下载redis-2.8.3.tar.gz安装包
  进行源码编译(需要C编译yum install gcc-c++ ),执行make,然后会在src目录中,得到bin文件(redis-server 服务器,redis-cli 客户端)
  启动redis server服务两种方法:
  ]# ./src/redis-server
  ]#后台方式启动 nohup ./redis-server &

  然后换一个终端执行:]# ./src/redis-cli,连接服务

  接下来灌数据(批量灌):
  需要安装unix2dos(yum install unix2dos)(格式转换)

  ]# cat cb_reclist.redis | /usr/local/src/redis-2.8.3/src/redis-cli --pipe 这样是会报大量异常,所以需要用下面的方式去做,完了再使用管道插入(注意redis安装目录)

  unix2dos cb_reclist.redis

  cat cb_reclist.redis | /usr/local/src/redis/redis-2.8.3/src/redis-cli --pipe 

  验证:]# ./src/redis-cli
  执行:
  127.0.0.1:6379> get CB_5305109176
  "726100303:0.393048_953500302:0.393048_6193109237:0.348855"

3.3、【召回】CF算法

  (1)以userid itemid score形式整理训练数据
  python gen_cf_train.py

 1 #coding=utf-8
 2 '''
 3     总体思路:首先和cb一样,对处理完的用户元数据,物品元数据,行为数据进行cf数据准备工作,我们的目的事输出:
 4     user,item score,其中主要是的到用户对item的score,这里score怎么算呢,当然是用户收听的音乐的时常和总的时
 5     长相除的到
 6 '''
 7 
 8 import sys
 9 
10 input_file = "../data/merge_base.data"
11 
12 # 输出cf训练数据
13 output_file = '../data/cf_train.data'
14 ofile = open(output_file, 'w')
15 
16 key_dict = {}
17 with open(input_file, 'r') as fd:
18     for line in fd:
19         ss = line.strip().split('\001')
20         # 用户行为
21         userid = ss[0].strip()
22         itemid = ss[1].strip()
23         watch_len = ss[2].strip()
24         hour = ss[3].strip()
25         # 用户画像
26         gender = ss[4].strip()
27         age = ss[5].strip()
28         salary = ss[6].strip()
29         user_location = ss[7].strip()
30         # 物品元数据
31         name = ss[8].strip()
32         desc = ss[9].strip()
33         total_timelen = ss[10].strip()
34         item_location = ss[11].strip()
35         tags = ss[12].strip()
36         #拼接key,为了将同一个用户对相同物品的时长全部得到,需要做个聚合
37         key = '_'.join([userid, itemid])
38         if key not in key_dict:
39             key_dict[key] = []
40         key_dict[key].append((int(watch_len), int(total_timelen)))
41 
42 #循环处理相同用户对相同item的分数
43 for k, v in key_dict.items():
44     t_finished = 0
45     t_all = 0
46     # 对<userid, itemid>为key进行分数聚合
47     for vv in v:
48         t_finished += vv[0]
49         t_all += vv[1]
50 
51     # 得到userid对item的最终分数
52     score = float(t_finished) / float(t_all)
53     userid, itemid = k.strip().split('_')
54 
55 
56     ofile.write(','.join([userid, itemid, str(score)]))
57     ofile.write("\n")
58 
59 ofile.close()

 

  得到如下数据:

  (2)用协同过滤算法跑出item-item数据
  II矩阵数据准备,归一化,取pair对,计算总和

  这里我们准备redis数据分为这么几个部分,我们来一一解析一下,当然这部分的数据需要利用到MapReduce框架,进行map和reduce排序。

  • 归一化

       归一化阶段我们主要是将相同的item进行单位模计算,因为后续我们要用到cos相似度计算公式,将相同的item的分数进行平方和再开根号,最后进行单位化。

  map阶段,只要将转数据换成item,user,score ,因为我们要在reduce阶段进行相同item单位化,要充分用到shuffle阶段的排序。

 1 #!usr/bin/python
 2 # -*- coding: UTF-8 -*-
 3 '''
 4     思路:转换成i,u,s的矩阵
 5 '''
 6 import  sys
 7  
 8 for line in sys.stdin:
 9     ss = line.strip().split(',')
10     if len(ss) != 3:
11         continue
12     u , i , s = ss
13     print '\t'.join([i,u,s])

  

  reduce阶段,我们需要将相同item平方和相加开根号,然后再单位化计算,最后输出。

 1 #!usr/bin/python
 2 # -*- coding: UTF-8 -*-
 3 '''
 4     在map的基础上将每个item进行归一化,map已经将相同的item排好序,这里我们根据map的结果进行给先平方再开根号:
 5     思路 :
 6         1、截取字符串,取出item,user,socre
 7         2、在for循环中进行判断,当前的item和下一个是否相同,要是相同,将相同的放到列表(user,score)列表里面,否则往下执行
 8         3、若不相同,循环user和score列表,计算模计算,然后再次循环,进行单位化计算
 9 '''
10  
11 import sys
12 import math
13  
14 cur_item = None
15 user_score_list = []
16 for line in sys.stdin:
17     ss = line.strip().split('\t')
18     if len(ss) != 3:
19         continue
20  
21     item = ss[0]
22     userid = ss[1]
23     score = ss[2]
24  
25     #wordcount判断,当前和下一个是否相同,相同添加到列表,不相同进行归一化计算
26     if cur_item == None:
27         cur_item = item
28     if cur_item != item:
29         #定义sum
30         sum = 0.0
31         #循环列表进行模向量计算
32         for ss in user_score_list:
33             user,s = ss
34             sum += pow(s,2)
35         sum = math.sqrt(sum)
36  
37         #单位化计算
38         for touple in user_score_list:
39             u,s = touple
40             # 进行单位化完成后,我们输出重置成原来的user-item-score输出
41             print "%s\t%s\t%s" % (u, cur_item, float(s / sum))
42  
43         #初始化这两个变量
44         cur_item = item
45         user_score_list = []
46  
47     user_score_list.append((userid,float(score)))
48  
49 #定义sum
50 sum = 0.0
51 #循环列表进行模向量计算
52 for ss in user_score_list:
53     user,s = ss
54     sum += pow(s,2)
55 sum = math.sqrt(sum)
56 #单位化计算
57 for touple in user_score_list:
58     u,s = touple
59     # 进行单位化完成后,我们输出重置成原来的user-item-score输出
60     print "%s\t%s\t%s" % (u, cur_item, float(s / sum))
  • 两两取pair对

        两两取pair对,我们在map阶段,其实什么都不用做,保证输出user,itemid,score即可。

map阶段

 1 #!usr/bin/python
 2 # -*- coding: UTF-8 -*-
 3  
 4 #在进行pair取对之前,什么都不需要做,输出就行
 5  
 6 import  sys
 7  
 8 for line in sys.stdin:
 9     u, i, s = line.strip().split('\t')
10     print "%s\t%s\t%s" % (u, i, s)

  

  reduce阶段,我们需要将同一个用户下面的item进行两两取对,因为我们要形成II矩阵,就必须以user为参考单位,相反形成uu矩阵,就必须以item参考,所以将同一个用户下的item进行两两取对,并将分数相乘,就得到临时这个相似度,因为还没有对相同pair对的分数相加,这个是最后一步要做的。

 1 #!usr/bin/python
 2 # -*- coding: UTF-8 -*-
 3  
 4 '''
 5     思路:进行map排好序之后,我们的会得到相同user对应的不同item和score,这里我们主要的思路是进行相同用户两两取pair
 6          1、进行判断,当前用户和下一个用户是不是一样,若是不一样,我们进行两两取对,形成ii矩阵
 7          2、若是相同,我们将不同的item和score放入list里面
 8 '''
 9  
10 import  sys
11  
12 cur_user = None
13 item_score_list = []
14 for line in sys.stdin:
15     user,item,score = line.strip().split('\t')
16  
17     if cur_user == None:
18         cur_user= user
19  
20     if cur_user != user:
21  
22         #进行两两pair,利用range函数
23         for i in range(0,len(item_score_list)-1):
24             for j in range(i+1,len(item_score_list)):
25                 item_a, score_a = item_score_list[i]
26                 item_b, score_b = item_score_list[j]
27                 # 输出两遍的目的是为了形成II矩阵的对称
28                 print "%s\t%s\t%s" % (item_a, item_b, score_a * score_b)
29                 print "%s\t%s\t%s" % (item_b, item_a, score_a * score_b)
30  
31         cur_user = user
32         item_score_list = []
33  
34     item_score_list.append((item,float(score)))
35  
36 #进行两两pair,利用range函数
37 for i in range(0,len(item_score_list)-1):
38     for j in range(i+1,len(item_score_list)):
39         item_a, score_a = item_score_list[i]
40         item_b, score_b = item_score_list[j]
41         # 输出两遍的目的是为了形成II矩阵的对称
42         print "%s\t%s\t%s" % (item_a, item_b, score_a * score_b)
43         print "%s\t%s\t%s" % (item_b, item_a, score_a * score_b)
  • 进行最终分数求和

       我们最后的阶段是要将相同pair的分数相加才能得到两个item的相似度

  map阶段,这里我们因为要将相同item对排序到一起,就要将pair组成一个key进行排序,将同一个partition后数据放倒一个reduce桶中,再说一下MapReduce框架中国年shuffle阶段,key只是做排序,partition只是做分区,不要搞混了。

 1 #!usr/bin/python
 2 # -*- coding: UTF-8 -*-
 3  
 4 '''
 5     sum的map中,我们需要把相同的itemA,itemB组成key,为了使相同的key能够在shuffle阶段分配到同一个reduce中,
 6     因为是计算item的相似度,要把相同的相加
 7 '''
 8  
 9 import  sys
10  
11 for line in sys.stdin:
12     item_a,item_b,score = line.strip().split('\t')
13     key = '#'.join([item_a,item_b])
14     print '%s\t%s' %(key,score)

  

  reduce阶段主要任务就是将相同的item的pair对相加.

#!usr/bin/python
# -*- coding: UTF-8 -*-
 
'''
    思路:将相同的item的分数进行相加,得到最后的相似度
'''
 
import  sys
 
cur_item = None
score = 0.0
for line in sys.stdin:
    item, s = line.strip().split('\t')
    if not cur_item:
        cur_item = item
    if cur_item != item:
        ss = item.split("#")
        if len(ss) != 2:
            continue
        item_a, item_b = ss
        print "%s\t%s\t%s" % (item_a, item_b, score)
 
        cur_item = item
        score = 0.0
 
    score += float(s)
 
ss = item.split("#")
if len(ss) != 2:
    sys.exit()
item_a, item_b = ss
print "%s\t%s\t%s" % (item_a, item_b, score)

  

  最后得到基于cf的ii矩阵

  (3)对数据格式化,item-> item list形式,整理出KV形式
  python gen_reclist.py

 1 #coding=utf-8
 2 '''
 3     思路:这个处理的逻辑和CB中完全一样,不一样的是redis的key是CF开头
 4 '''
 5 
 6 import sys
 7 
 8 infile = '../data/cf.result'
 9 outfile = '../data/cf_reclist.redis'
10 
11 ofile = open(outfile, 'w')
12 
13 MAX_RECLIST_SIZE = 100
14 PREFIX = 'CF_'
15 
16 rec_dict = {}
17 with open(input_file,'r') as fd:
18     for line in fd:
19         itemid_A, itemid_B, score = line.strip().split('\t')
20  
21         #判断itemA在不在该字典里面,若不在,创建一个key为itemA的列表,把与itemA相关联的itemB和score添加进去
22         if itemid_A not in rec_dict:
23             rec_dict[itemid_A] = []
24         rec_dict[itemid_A].append((itemid_B, score))
25  
26 #循环遍历字典,格式化数据,把itemB和score中间以:分割,不同的itemB以_分割
27 for k,v in rec_dict.items():
28     key = PREFIX+k
29     #接下来格式化数据,将数据以从大到小排列后再格式化
30     #排序,由于数据量大,我们只取100个
31     list = sorted(v,key=lambda x:x[1],reverse=True)[:MAX_RECLIST_SIZE]
32     #拍好序后,我们来格式化数据
33     result = '_'.join([':'.join([str(val[0]),str(round(float(val[1]),6))]) for val in list])
34  
35     ofile.write(' '.join(['SET',key,result]))
36     ofile.write("\n")
37  
38 ofile.close()

 

  类似如下数据:

4)灌库

  unix2dos cf_reclist.redis

  cat cf_reclist.redis | /usr/local/src/redis-2.8.3/src/redis-cli --pipe

  验证:

3.4、LR训练模型的数据准备

  准备我们自己的训练数据
  进入pre_data_for_rankmodel目录:

  gen_samples.py

  1 #coding=utf-8
  2 
  3 
  4 '''
  5     思路:这里我们经过cb,cf算法,将数据已经放到内存库,召回部分已经完成,接下来我们需要做排序模型,为逻辑回归准备样本数据
  6          1、处理第一次将用户元数据,物品元数据,用户行为数据一起归并的数据,也就是merge_base.data,我们在这里需要得到用户画像
  7             数据,用户信息数据,标签数据
  8          2、收取样本,标签,用户画像信息,物品信息
  9          3、抽取用户画像信息,对性别和年龄生成样本数据
 10          4、抽取item特征信息,分词获得token,score,做样本数据
 11          5、拼接样本,生成最终的样本信息,作为模型进行训练
 12 '''
 13 
 14 import sys
 15 sys.path.append('../')
 16 reload(sys)
 17 sys.setdefaultencoding('utf-8')
 18 
 19 import jieba
 20 import jieba.analyse
 21 import jieba.posseg
 22 
 23 merge_base_infile = '../data/merge_base.data'
 24 output_file = '../data/samples.data'
 25 
 26 #我们这里需要再生成两个文件,一个是用户样本和item样本,因为要对实时推荐的化,必须使用这两个样本
 27 output_user_feature_file = '../data/user_feature.data'
 28 output_item_feature_file = '../data/item_feature.data'
 29 
 30 #这里生成个类似name和id对应的字典信息
 31 output_itemid_to_name_file = '../data/name_id.dict'
 32 
 33 
 34 #定义函数,来获取各类数据
 35 def get_base_samples(infile):
 36     #放待处理样本数据
 37     ret_samples_list = []
 38     #放user用户数据
 39     user_info_set = set()
 40     #放物品数据
 41     item_info_set = set()
 42     item_name2id = {}
 43     item_id2name = {}
 44 
 45     with open(infile, 'r') as fd:
 46         for line in fd:
 47             ss = line.strip().split('\001')
 48             if len(ss) != 13:
 49                 continue
 50             userid = ss[0].strip()
 51             itemid = ss[1].strip()
 52             #这两个时间为了计算label而使用
 53             watch_time = ss[2].strip()
 54             total_time = ss[10].strip()
 55 
 56             #用户数据
 57             gender = ss[4].strip()
 58             age = ss[5].strip()
 59             user_feature = '\001'.join([userid, gender, age])
 60 
 61             #物品数据
 62             name = ss[8].strip()
 63             item_feature = '\001'.join([itemid, name])
 64 
 65             #计算标签
 66             label = float(watch_time) / float(total_time)
 67             final_label = '0'
 68 
 69             if label >= 0.82:
 70                 final_label = '1'
 71             elif label <= 0.3:
 72                 final_label = '0'
 73             else:
 74                 continue
 75 
 76             #接下来装在数据,并返回结果,首先我们装在itemid2name和itemname2id
 77             item_name2id[name] = itemid
 78             item_id2name[itemid] = name
 79 
 80             #装在待处理的标签数据
 81             ret_samples_list.append([final_label, user_feature, item_feature])
 82 
 83             user_info_set.add(user_feature)
 84             item_info_set.add(name)
 85 
 86     return ret_samples_list, user_info_set, item_info_set, item_name2id, item_id2name
 87 
 88 
 89 #step 1 程序的入口,开始调用函数,开始处理文件,得到相应的数据
 90 base_sample_list, user_info_set, item_info_set, item_name2id, item_id2name = \
 91     get_base_samples(merge_base_infile)
 92 
 93 
 94 #step 2 抽取用户画像信息,用户标签转换,将年龄和age进行转换,用于样本使用
 95 user_fea_dict = {}
 96 for info in user_info_set:
 97     userid, gender, age = info.strip().split('\001')
 98     
 99     #设置标签idx,将男(1)和女(0)用数剧的形式表示,权重都设置为1
100     idx = 0 # default 女
101     if gender == '':
102         idx = 1
103     #将标签和权重拼接起来
104     gender_fea = ':'.join([str(idx), '1'])
105 
106     #性别设置完成,我们接下来设置年龄,将年龄进行划分,0-18,19-25,26-35,36-45
107     idx = 0
108     if age == '0-18':
109         idx = 0
110     elif age == '19-25':
111         idx = 1
112     elif age == '26-35':
113         idx = 2
114     elif age == '36-45':
115         idx = 3
116     else:
117         idx = 4
118 
119     idx += 2
120 
121     age_fea = ':'.join([str(idx), '1'])
122 
123     user_fea_dict[userid] = ' '.join([gender_fea, age_fea])
124 
125 #step 3 抽取物品特征,这里我们要用到分词,将name进行分词,并且把分词后的token转换成id,这里就需要我们来做生成tokenid词表
126 token_set = set()
127 item_fs_dict = {}
128 for name in item_info_set:
129     token_score_list = []
130     for x,w in jieba.analyse.extract_tags(name,withWeight=True):
131         token_score_list.append((x,w))
132         token_set.add(x)
133     item_fs_dict[name] = token_score_list
134  
135 #进行token2id的转换
136 token_id_dict = {}
137 #这里我们要用到刚刚利用set去重过的token列表,生成tokenid的字典表
138 for s in enumerate(list(token_set)):
139     token_id_dict[s[1]] = s[0]
140  
141 #接下来,我们需要把第三步生成的item_fs_dict中name对应的token全部替换成id,然后当作字典,为下面的全量替换做准备
142 item_fea_dict = {}
143 user_feature_offset = 10
144 for name ,fea in item_fs_dict.items():
145     token_score_list = []
146     for (token,score) in fea:
147         if token not in token_id_dict:
148             continue
149         token_id = token_id_dict[token] + user_feature_offset
150         token_score_list.append(':'.join([str(token_id),str(score)]))
151  
152     #接下来输出到字典中
153     item_fea_dict[name] = ' '.join(token_score_list)
154  
155 #step 4 将第一步输出的样本数据整体替换并且替换user_feature和item_feature,并输出到文件中
156 ofile = open(output_file,'w')
157 for (label,userfea,itemfea) in base_sample_list:
158     userid = userfea.strip().split('\001')[0]
159     item_name = itemfea.strip().split('\001')[1]
160  
161     if userid not in user_fea_dict:
162         continue
163     if item_name not in item_fea_dict:
164         continue
165  
166     ofile.write(' '.join([label,user_fea_dict[userid],item_fea_dict[item_name]]))
167     ofile.write('\n')
168  
169 ofile.close()
170  
171 #step 5 为了能够实时使用userfeatre,我们需要输出一下
172 out_put_file = open(output_user_feature_file,'w')
173 for userid,fea in user_fea_dict.items():
174     out_put_file.write('\t'.join([userid,fea]))
175     out_put_file.write('\n')
176 out_put_file.close()
177  
178 #step 6 输出item_feature
179 out_file = open(output_item_feature_file,'w')
180 for name,fea in item_fea_dict.items():
181     if name not in item_name2id:
182         continue
183     itemid = item_name2id[name]
184     out_file.write('\t'.join([itemid,fea]))
185     out_file.write('\n')
186  
187 #step 7 输出id2name的对应的字典
188 o_file = open(output_itemid_to_name_file,'w')
189 for id,name in item_id2name.items():
190     o_file.write('\t'.join([id,name]))
191     o_file.write('\n')
192 o_file.close()

 

  得到如下数据:

 

3.5、模型准备

 1 # -*- coding: UTF-8 -*-
 2 '''
 3     思路:这里我们要用到我们的数据,就需要我们自己写load_data的部分,
 4          首先定义main,方法入口,然后进行load_data的编写
 5          其次调用该方法的到x训练x测试,y训练,y测试,使用L1正则化或是L2正则化使得到结果更加可靠
 6          输出wegiht,和b偏置
 7 '''
 8 import sys
 9 import numpy as np
10 from scipy.sparse import csr_matrix
11  
12 from sklearn.model_selection import train_test_split
13 from sklearn.linear_model import LogisticRegression
14  
15 input_file = sys.argv[1]
16  
17 def load_data():
18     #由于在计算过程用到矩阵计算,这里我们需要根据我们的数据设置行,列,和训练的数据准备
19     #标签列表
20     target_list = []
21     #行数列表
22     fea_row_list = []
23     #特征列表
24     fea_col_list = []
25     #分数列表
26     data_list = []
27  
28     #设置行号计数器
29     row_idx = 0
30     max_col = 0
31  
32     with open(input_file,'r') as fd:
33         for line in fd:
34             ss = line.strip().split(' ')
35             #标签
36             label = ss[0]
37             #特征
38             fea = ss[1:]
39  
40             #将标签放入标签列表中
41             target_list.append(int(label))
42  
43             #开始循环处理特征:
44             for fea_score in fea:
45                 sss = fea_score.strip().split(':')
46                 if len(sss) != 2:
47                     continue
48                 feature, score = sss
49                 #增加行
50                 fea_row_list.append(row_idx)
51                 #增加列
52                 fea_col_list.append(int(feature))
53                 #填充分数
54                 data_list.append(float(score))
55                 if int(feature) > max_col:
56                     max_col = int(feature)
57  
58             row_idx += 1
59  
60     row = np.array(fea_row_list)
61     col = np.array(fea_col_list)
62     data = np.array(data_list)
63  
64     fea_datasets = csr_matrix((data, (row, col)), shape=(row_idx, max_col + 1))
65  
66     x_train, x_test, y_train, y_test = train_test_split(fea_datasets, s, test_size=0.2, random_state=0)
67  
68     return x_train, x_test, y_train, y_test
69  
70 def main():
71     x_train,x_test,y_train,y_test = load_data()
72     #用L2正则话防止过拟合
73     model = LogisticRegression(penalty='l2')
74     #模型训练
75     model.fit(x_train,y_train)
76  
77     ff_w = open('model.w', 'w')
78     ff_b = open('model.b', 'w')
79  
80     #写入训练出来的W
81     for w_list in model.coef_:
82         for w in w_list:
83             print >> ff_w, "w: ", w
84     # 写入训练出来的B
85     for b in model.intercept_:
86         print >> ff_b, "b: ", b
87     print "precision: ", model.score(x_test, y_test)
88     print "MSE: ", np.mean((model.predict(x_test) - y_test) ** 2)
89  
90 if __name__ == '__main__':
91     main()

 

  好了,所有的一切都准备好了,我们下来就进行推荐系统的实现

3.6、推荐系统实现

  推荐系统demo流程

(1)解析请求:userid,itemid
(2)加载模型:加载排序模型(model.w,model.b)
(3)检索候选集合:利用cb,cf去redis里面检索数据库,得到候选集合
(4)获取用户特征:userid
(5)获取物品特征:itemid
(6)打分(逻辑回归,深度学习),排序
(7)top-n过滤
(8)数据包装(itemid->name),返回

推荐系统的实现主要就是我们前面说的这几部分,思路很明确,需要大家细细看下代码。

main.py 

  1 #coding=utf-8
  2 import web
  3 import sys
  4 import redis
  5 import json
  6 import math
  7 
  8 urls = (
  9     '/', 'index',
 10     '/test', 'test',
 11 )
 12 
 13 app = web.application(urls, globals())
 14 
 15 # 加载user特征
 16 user_fea_dict = {}
 17 with open('../data/user_feature.data') as fd:
 18     for line in fd:
 19         userid, fea_list_str = line.strip().split('\t')
 20         user_fea_dict[userid] = fea_list_str
 21 
 22 
 23 # 加载item特征
 24 item_fea_dict = {}
 25 with open('../data/item_feature.data') as fd:
 26     for line in fd:
 27         ss = line.strip().split('\t')
 28         if len(ss) != 2:
 29             continue
 30         itemid, fea_list_str = ss
 31         item_fea_dict[itemid] = fea_list_str
 32 
 33 class index:
 34     def GET(self):
 35         r = redis.Redis(host='master', port=6379,db=0)
 36         # step 1 : 解析请求,上面我们已经得到userid,itemid
 37         params = web.input()
 38         userid = params.get('userid', '')
 39         req_itemid = params.get('itemid', '')
 40 
 41         # step 2 : 加载模型
 42         model_w_file_path = '../rankmodel/model.w'
 43         model_b_file_path = '../rankmodel/model.b'
 44 
 45         model_w_list = []
 46         model_b = 0.
 47         with open (model_w_file_path, 'r') as fd:
 48             for line in fd:
 49                 ss = line.strip().split(' ')
 50                 if len(ss) != 3:
 51                     continue
 52                 model_w_list.append(float(ss[2].strip()))
 53 
 54         with open (model_b_file_path, 'r') as fd:
 55             for line in fd:
 56                 ss = line.strip().split(' ')
 57                 model_b = float(ss[2].strip())
 58 
 59         # step 3 : 检索候选(match),这里我们分两次,cb,cf
 60         #将检索回来的item全部放到recallitem列表里面
 61         rec_item_mergeall = []
 62         # 3.1 cf
 63         cf_recinfo = 'null'
 64         key = '_'.join(['CF', req_itemid])
 65         if r.exists(key):
 66             cf_recinfo = r.get(key)
 67 
 68         if len(cf_recinfo) > 6:
 69             for cf_iteminfo in cf_recinfo.strip().split('_'):
 70                 item, score = cf_iteminfo.strip().split(':')
 71                 rec_item_mergeall.append(item)
 72 
 73         # 3.2 cb
 74         cb_recinfo = 'null'
 75         key = '_'.join(['CB', req_itemid])
 76         if r.exists(key):
 77             cb_recinfo = r.get(key)
 78         if len(cb_recinfo) > 6:
 79             for cb_iteminfo in cb_recinfo.strip().split('_'):
 80                 item, score = cb_iteminfo.strip().split(':')
 81                 rec_item_mergeall.append(item)
 82 
 83         # step 4: 获取用户特征,将获取的用户特征处理后放到字典里面,方便后续计算内积
 84         user_fea = ''
 85         if userid in user_fea_dict:
 86             user_fea = user_fea_dict[userid]
 87 
 88         u_fea_dict = {}
 89         for fea_idx in user_fea.strip().split(' '):
 90             ss = fea_idx.strip().split(':')
 91             if len(ss) != 2:
 92                 continue
 93             idx = int(ss[0].strip())
 94             score = float(ss[1].strip())
 95             u_fea_dict[idx] = score
 96 
 97         # step 5: 获取物品的特征 ,循环遍历刚刚得到itemid,判断item是否在item特征中,若在开始进行处理
 98         rec_list = []
 99         for itemid in rec_item_mergeall:
100             if itemid in item_fea_dict:
101                 item_fea = item_fea_dict[itemid]
102 
103                 i_fea_dict = dict()
104                 for fea_idx in item_fea.strip().split(' '):
105                     ss = fea_idx.strip().split(':')
106                     if len(ss) != 2:
107                         continue
108                     idx = int(ss[0].strip())
109                     score = float(ss[1].strip())
110                     i_fea_dict[idx] = score
111                 
112                 #我们得到召回item对应的特征和用户的特征,我们接下来根据模型求出来的w,b,进行打分
113                 wx_score = 0.
114                 #这里我们求个内积,wx,然后做sigmoid,先将两个字典拼接起来,然后计算分数
115                 for fea, score in dict(u_fea_dict.items() + i_fea_dict.items()).items():
116                     wx_score += (score * model_w_list[fea])
117 
118                 #计算sigmoid: 1 / (1 + exp(-wx))
119                 final_rec_score = 1 / (1 + math.exp(-(wx_score + model_b)))
120                 #将itemid和分数放入列表中,方便后续排序
121                 rec_list.append((itemid, final_rec_score))
122 
123         # step 6 : 精排序(rank)
124         rec_sort_list = sorted(rec_list, key=lambda x:x[1], reverse=True)
125 
126         # step 7 : 过滤(filter)
127         rec_fitler_list = rec_sort_list[:10]
128 
129         # step 8 : 返回+包装(return),进行将itemid转换成name
130 
131         item_dict = {}
132         with open('../data/name_id.dict', 'r') as fd:
133             for line in fd:
134                 raw_itemid, name = line.strip().split('\t')
135                 item_dict[raw_itemid] = name
136 
137         ret_list = []
138         for tup in rec_fitler_list:
139             req_item_name = item_dict[req_itemid]
140             item_name = item_dict[tup[0]]
141             item_rank_score = str(tup[1])
142             ret_list.append('   ->   '.join([req_item_name, item_name, item_rank_score]))
143 
144         ret = '\n'.join(ret_list)
145 
146         return ret
147 
148 class test:
149     def GET(self):
150         print web.input()
151         return '222'
152 
153 if __name__ == "__main__":
154     app.run()

 

  验证:
  192.168.150.10:9999/?userid=00370d83b51febe3e8ae395afa95c684&itemid=3880409156

 

  下面附上一张完整的音乐推荐系统流程结构图: 

posted on 2019-09-16 08:57  农夫三拳有點疼  阅读(1664)  评论(0编辑  收藏  举报

导航