音乐推荐系统
音乐频道推荐业务,支持各个产品业务和策略。这里先使用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
下面附上一张完整的音乐推荐系统流程结构图: