推荐算法在互联网行业的应用非常广泛, 今日头条, 美团点评等都有个性化推荐, 推荐算法抽象来讲, 是一种对于内容满意度的拟合函数, 涉及到用户特征和内容特征, 作为模型训练所需维度的两大来源, 而点击率, 页面停留时间, 评论或下单等都可以作为一个量化的 Y 值, 这样就可以进行特征工程, 构建出一个数据集, 然后选择一个合适的监督学习算法进行训练, 得到模型后, 为客户推荐偏好的内容, 如头条的话, 就是咨询和文章, 美团的就是生活服务内容.
可选择的模型很多, 如协同过滤, 逻辑斯蒂回归, 基于 DNN 的模型, FM 等. 我们使用的方式是, 基于内容相似度计算进行召回, 之后通过 FM 模型和逻辑斯蒂回归模型进行精排推荐, 下面就分别说一下, 我们做这个电影推荐系统过程中, 从数据准备, 特征工程, 到模型训练和应用的整个过程.
我们实现的这个电影推荐系统, 爬取的数据实际上维度是相对少的, 特别是用户这一侧的维度, 正常推荐系统涉及的维度, 诸如页面停留时间, 点击频次, 收藏等这些维度都是没有的, 以及用户本身的维度也相对要少, 没有地址, 年龄, 性别等这些基本的维度, 这样我们爬取的数据只有打分和评论这些信息, 所以之后我们又从这些信息里再拿出一些统计维度来用. 我们爬取的电影数据 (除电影详情和图片信息外) 是如下这样的形式:
这里的数据是有冗余的, 又通过如下的代码, 对数据进行按维度合并, 去除冗余数据条目:
- # 处理主函数, 负责将多个冗余数据合并为一条电影数据, 将地区, 导演, 主演, 类型, 特色等维度数据合并
- def mainfunc():
- try:
- unable_list = []
- with connection.cursor() as cursor:
- sql='select id,name from movie'
- cout=cursor.execute(sql)
- print("数量:"+str(cout))
- for row in cursor.fetchall():
- #print(row[1])
- movieinfo = df[df['电影名'] == row[1]]
- if movieinfo.shape[0] == 0:
- disable_movie(row[0])
- print('disable movie' + str(row[1]))
- else:
- g = lambda x:movieinfo[x].iloc[0]
- types = movieinfo['类型'].tolist()
- types = reduce(lambda x,y:x+'|'+y,list(set(types)))
- Traits = movieinfo['特色'].tolist()
- Traits = reduce(lambda x,y:x+'|'+y,list(set(Traits)))
- update_one_movie_info(type_=types, actors=g('主演'), region=g('地区'), director=g('导演'), trait=Traits, rat=g('评分'), id_=row[0])
- connection.commit()
- finally:
- connection.close()
之后开始准备用户数据, 我们从用户打分的数据中, 统计出每一个用户的打分的最大值, 最小值, 中位数值和平均值等, 从而作为用户的一个附加属性, 存储于 userproex 表中:
- 'insert into userproex(userid, rmax, rmin, ravg, rcount, rsum, rmedian) values(\'%s\', %s, %s, %s, %s, %s, %s)' % (userid, rmax, rmin, ravg, rcount, rsum, rmedium)
- 'update userproex set rmax=%s, rmin=%s, ravg=%s, rmedian=%s, rcount=%s, rsum=%s where userid=\'%s\'' % (rmax, rmin, ravg, rmedium, rcount, rsum, userid)
以上两个 SQL 是最终插入表的时候用到的, 代表准备用户数据的最终步骤, 其余细节可以参考文末的 GitHub 仓库, 不在此赘述, 数据处理还用到了一些 SQL, 以及其他处理细节.
系统上线运行时, 第一次是全量的数据处理, 之后会是增量处理过程, 这个后面还会提到.
我们目前把用户数据和电影的数据的原始数据算是准备好了, 下一步开始特征工程. 做特征工程的思路是, 对 type, actors, director, trait 四个类型数据分别构建一个频度统计字典, 用于之后的 one-hot 编码, 代码如下:
- def get_dim_dict(df, dim_name):
- type_list = list(map(lambda x:x.split('|') ,df[dim_name]))
- type_list = [x for l in type_list for x in l]
- def reduce_func(x, y):
- for i in x:
- if i[0] == y[0][0]:
- x.remove(i)
- x.append(((i[0],i[1] + 1)))
- return x
- x.append(y[0])
- return x
- l = filter(lambda x:x != None, map(lambda x:[(x, 1)], type_list))
- type_zip = reduce(reduce_func, list(l))
- type_dict = {}
- for i in type_zip:
- type_dict[i[0]] = i[1]
- return type_dict
涉及到的冗余数据也要删除
df_ = df.drop(['ADD_TIME', 'enable', 'rat', 'id', 'name'], axis=1)
将电影数据转换为字典列表, 由于演员和导演均过万维, 实际计算时过于稀疏, 当演员或导演只出现一次时, 标记为冷门演员或导演
- movie_dict_list = []
- for i in df_.index:
- movie_dict = {}
- #type
- for s_type in df_.iloc[i]['type'].split('|'):
- movie_dict[s_type] = 1
- #actors
- for s_actor in df_.iloc[i]['actors'].split('|'):
- if actors_dict[s_actor] <2:
- movie_dict['other_actor'] = 1
- else:
- movie_dict[s_actor] = 1
- #regios
- movie_dict[df_.iloc[i]['region']] = 1
- #director
- for s_director in df_.iloc[i]['director'].split('|'):
- if director_dict[s_director] < 2:
- movie_dict['other_director'] = 1
- else:
- movie_dict[s_director] = 1
- #trait
- for s_trait in df_.iloc[i]['trait'].split('|'):
- movie_dict[s_trait] = 1
- movie_dict_list.append(movie_dict)
使用 DictVectorizer 进行向量化, 做 One-hot 编码
- v = DictVectorizer()
- X = v.fit_transform(movie_dict_list)
这样的数据, 下面做余弦相似度已经可以了, 这是特征工程的基本的一个处理, 模型所使用的数据, 需要将电影, 评分, 用户做一个数据拼接, 构建训练样本, 并保存 CSV, 注意这个 CSV 不用每次全量构建, 而是除第一次外都是增量构建, 通过 mqlog 中类型为'c'的消息, 增量构建以 comment(评分)为主的训练样本, 拼接之后的形式如下:
- USERID cf2349f9c01f9a5cd4050aebd30ab74f
- movieid 10533913
type 剧情 | 奇幻 | 冒险 | 喜剧
actors 艾米. 波勒 | 菲利丝. 史密斯 | 理查德. 坎德 | 比尔. 哈德尔 | 刘易斯. 布莱克
region 美国
director 彼特. 道格特 | 罗纳尔多. 德尔. 卡门
trait 感人 | 经典 | 励志
- rat 8.7
- rmax 5
- rmin 2
- ravg 3.85714
- rcount 7
- rmedian 4
- TIME_DIS 15
这个数据的 actors 等字段和上面的处理是一样的, 为了之后 libfm 的使用, 在这里需要转换为 libsvm 的数据格式
dump_svmlight_file(train_X_scaling, train_y_, train_file)
模型使用上遵循先召回, 后精排的策略, 先通过余弦相似度计算一个相似度矩阵, 然后根据这个矩阵, 为用户推荐相似的 M 个电影, 在通过训练好的 FM,LR 模型, 对这个 M 个电影做偏好预估, FM 会预估一个用户打分, LR 会预估一个点击概率, 综合结果推送给用户作为推荐电影.
模块列表
recsys_ui: 前端技术(html5+JavaScript+jQuery+Ajax)
recsys_web: 后端技术(Java+SpringBoot+MySQL)
recsys_spider: 网络爬虫(python+BeautifulSoup)
recsys_sql: 使用 SQL 数据处理
recsys_model: pandas, libFM, sklearn. pandas 数据分析和数据清洗, 使用 libFM,sklearn 对模型初步搭建
recsys_core: 使用 pandas, libFM, sklearn 完整的数据处理和模型构建, 训练, 预测, 更新的程序
recsys_etl:ETL 处理爬虫增量数据时使用 kettle ETL 便捷处理数据
为了能够输出一个可感受的系统, 我们采购了阿里云服务器作为数据库服务器和应用服务器, 在线上搭建了电影推荐系统的第一版, 地址是:
www.technologyx.cn
可以注册, 也可以使用已有用户:
用户名 | 密码 |
---|---|
gavin | 123 |
gavin2 | 123 |
wuenda | 123 |
欢迎登录使用感受一下.
设计思路
用简单地方式表述一下设计思路,
1. 后端服务 recsys_web 依赖于系统数据库的推荐表'recmovie'展示给用户推荐内容
2. 用户对电影打分后(暂时没有对点击动作进行响应), 后台应用会向 mqlog 表插入一条数据(消息).
3. 新用户注册, 系统会插入 mqlog 中一条新用户注册消息
4. 新电影添加, 系统会插入 mqlog 中一条新电影添加消息
5. 推荐模块 recsys_core 会拉取用户的打分消息, 并且并行的做以下操作:
a. 增量的更新训练样本
b. 快速 (因服务器比较卡, 目前设定了延时) 对用户行为进行基于内容推荐的召回
c. 训练样本更新模型
d. 使用 FM,LR 模型对 Item based 所召回的数据进行精排
e. 处理新用户注册消息, 监听到用户注册消息后, 对该用户的属性初始化(统计值).
f. 处理新电影添加消息, 更新基于内容相似度而生成的相似度矩阵
注:
由于线上资源匮乏, 也不想使系统增加复杂度, 所以没有直接使用 MQ 组件, 而是以数据库表作为代替.
项目源码地址: https://github.com/GavinHacker/recsys_core
模型相关的模块介绍
增量的处理用户 comment, 即增量处理评分模块
这个模块负责监听来自 mqlog 的消息, 如果消息类型是用户的新的 comment, 则对消息进行拉取, 并相应的把新的 comment 合并到总的训练样本集合, 并保存到一个临时目录
然后更新数据库的 config 表, 把最新的样本集合 (CSV 格式) 的路径更新上去
运行截图
消息队列的截图
把 CSV 处理为 libsvm 数据
这个模块负责把最新的 CSV 文件, 异步的处理成 libSVM 格式的数据, 以供 libFM 和 LR 模型使用, 根据系统的性能确定任务的间隔时间
运行截图
基于内容相似度推荐
当监听到用户有新的 comment 时, 该模块将进行基于内容相似度的推荐, 并按照电影评分推荐
运行截图
libFM 预测
http://www.libfm.org/
对已有的基于内容推荐召回的电影进行模型预测打分, 呈现时按照打分排序
如下图为打分更新
逻辑回归预测
对样本集中的打分做 0,1 处理, 根据正负样本平衡,> 3 分为喜欢 即 1, <=3 为 0 即不喜欢, 这样使用逻辑回归做是否喜欢的点击概率预估, 根据概率排序
项目源码地址: https://github.com/GavinHacker/recsys_core
来源: https://www.cnblogs.com/gavinsp/p/recsys.html