基于CB,CF,LR算法的推荐系统实现

在开篇之前,我们先来说下上次CB,CF算法实现粗的推荐系统,我们知道,CB,CF算法只是在召回阶段使用,这种推荐出来的item毕竟是粗排的,这篇文章正是对上图画上一个圆满的句号,将CB,CF召回回来的item进行精排,然后选择分数最高,给用户推荐出来,那么,问题来,我们怎么来做这个精排,这里就要提出一个新的算法LR算法,所以,在说系统实现之前我们先来说说LR逻辑回归的知识点,这里就是简单的过一下,具体的如果大家想要了解更深入,建议大家再多多看看。

LR逻辑回归

在说逻辑回归之前,我们先说一下线性回归

线性回归:

首先我们把回归问题抽象成数学中X与Y的问题,X是自变量,Y是因变量,因此我们可以写成y=f(x,w),这里W是什么意思,相当于一个参数,例如y=wx+b,这么说y代表一篇军事题材的概率,给定你一篇文章x,是不是可以的到一个y的概率,这个就代表y是军事题材概率,其中也不乏会遇到y求出不是军事题材的概率,我们把这个叫做噪音,当噪音大的时候,也就是错误样本多的时候,会影响我们这个预测的y,在生活中,肯定会有噪音,所以我们要尽量避免噪音低点,保证预测的准确性,其中这个y=wx+b这个就是典型的线性回归,一个x与对应个y,这x和y组成一对对,有n个x,有n个y,然后就能在坐标轴打印出下面的点,每个样本必须有这两个元素,缺少一个都不行,打出来的点如下图,

但是我们希望有一根直线能够连接这些点,实际情况是不能的,除非弯曲一下,所以我们只能尽量去靠近大部分点,因此我们的目标就是点与直线间的误差,即直线出来的y与实际点的Y,即Y-y代表误差,当我们把所有点的误差相加,肯定是希望所有点的误差越小越好,当全部分布到直线上是不是这个误差为0,这个是我们理想化的目标,这个误差和就是

这就是一元线性回归,但是这一元对应的特征仅仅是一个特征x,其实生活中,我们对应的肯定不是一个,肯定有多个特征,那么这里就有多元线性回归,自变量x有N个,然后这个方程可以借鉴一元,f(xi,w,b)= wix+b  这里我们有多少个特征,就会有多少个w,那么这里的w是什么呢,在一元里面,这个w是个斜率,但是在多元里面,这个w不太好描述,我们用一个例子来说明一下:

例如:对于一个人,这个人是个样本,有很多属性,我们来判断这个人是个男还是女

  • 特征:

  • 身高

  • 体重

  • 头发长短

  • 肤色

  • 音色

这五个特征中,你认为哪个特征最重要,能够准确判断这个人是男还是女,当然,我们会根据每个特征重要性来进行打分,我们给五个特征设个权重,这里w就是代表权重,w权重是在【0-1】越是重要的越靠近1,不重要靠近0

  • 特征:

  • 身高    0.5

  • 体重    0.4

  • 头发长短   0.2

  • 肤色    0.1

  • 音色    0.1

我们首先设置个预值0.6,这个是什么呢,就是当一个人最后经过上面五个特征计算得到的分数大于0.6,我们认为男,小于则认为女

好了我们把判断的依据已经分配好权重,若来了个人(样本),我们来判断是男还是女,你报下上面五个特征,然后我们进行计算,最后我们来打个分数,假如是0.9,那么我们认为是男的。

 也就是说,我们把上面五个特征乘以权重再相加得到最后分数,这个分数来判断分类的问题,所以有了这些权重w,事情就好办了,那么w怎么来,w是什么,w就是模型,就像我们的NB中一样,我们有了先验概率,有了条件概率,我们是不是就能得到分数,进行分类,在回归里面,我们的最后目标就是得到w,但是大家也别忘了这里还有个b,这里这个b是个偏置,这个是什么意思呢,相当于整体的偏差,同样是做性别识别,我们给个偏差0.4,就是说我们经过计算最终得到的分数需要再加上这个偏置才是最后的结果,这个偏置可正可负,因此偏置就是这么计算的,是对整体的侧重

对于一元模型多元模型,我们的学习目标是最小化误差平方和,这个是学习目标,不是最后的目标,所以这个多元的学习目标即:

那么我们通过学习目标是要最后得到w,这里x不是未知数,x是已知的,不要搞混了,怎么得到w呢,我们知道这个是个凸函数,相当于对该函数w求导,令导数=0,求极值点,就能得到w

逻辑回归:

好了,说了这么多,其实都是对逻辑回归做个铺垫,我们充分理解刚刚的w和b,接下来就理解逻辑回归就很简单了

首先我们说下逻辑回归的思想:

我们做回归,最后很多情况是做个分类,比如说我们以分类举例子,比如这个y因变量是个类别号,要么等于1要么等于0,或者说1代表发生,0代表不发生

我们的目标:根据特征x,预测发生的概率y=1 即p(y=1|x)

那么这里面,我们根据刚刚说的y=wx,只用把wx相乘即可,我们这里求y=1的概率,相当于求f(x)那么就有f(x)=wx,那么我们就有

假定P(y=1|x)依赖于线性函数wx,wx可能不在[0-1]内,但是概率肯定是在这个里面,那么怎么办呢?这里我们就需要用到指数函数即

exp(wx)的值域[0 ,正无穷),怎么保证左边也是该值域呢,这里我们用对立面考虑,有P(y=1|x),就有P(y=0|x),那么P(y=0|x) = 1-P(y=1|x)

所以就有P(y=1|x)/1-P(y=1|x) = exp(wx)因此,我们就能够得到:P(y=1|x) = 1/1+exp(-wx)

因此我们就得到了sigmoid函数,值域就是[0,1],有了这个我们就有类别概率P(y=1|x) = n(wx),P(y=0|x) = 1-n(wx)

我们知道这个了,我们来计算w,这个我们需要到似然函数,什么是最大似然,我们看下这张图,就一目了然

总结起来,最大似然估计的目的就是:利用已知的样本结果,反推最有可能(最大概率)导致这样结果的参数值。

        原理:极大似然估计是建立在极大似然原理的基础上的一个统计方法,是概率论在统计学中的应用。极大似然估计提供了一种给定观察数据来评估模型参数的方法,即:“模型已定,参数未知”。通过若干次试验,观察其结果,利用试验结果得到某个参数值能够使样本出现的概率为最大,则称为极大似然估计。

知道最大似然的方法,我们就可以用到逻辑回归的方法:首先看下如下公式:公式不全,少个求和的符号sigma,这里面I代表符号函数,当样本标签等于1的时候,I就等于1,等于0的时候,I就等于0,所以公式分为两个部分,左边要求所有的都等于1,右边要求所有的都等于0,基本上会把所有的样本通过这个公式涵盖了,log里面表示是我们给你个样本,计算标签属于1的概率,然后我们在外面求个log结果最大化,不影响结果判断,,那么这里为什么会有个负数,后面这个式子是个凹函数,相当于把抛物线反转成凸函数,求最小值,因此这个公式即负对数似然函数,那么第二个Lw即相当于把刚刚sigmoid函数代入得到

那么,我们来看如何通过理论来计算w,刚开始我们不知道w,那么我们随便定义一个w,然后计算出一个F(w),然后我们来计算梯度,就是上面那个公式,我们有w,有x,有y,是不是能计算出梯度,计算出梯度后,我们尝试更新,通过步长*梯度再加上w,得到新的w,然后我们就能得到newF(w),新的fw和旧的fw是不是有个落差,若当这个落差非常小的时候,是不是我们就到了凸函数的底部,到此我们就停止训练,下面就是过程

至此我们逻辑回归的基本知识就完成了。

推荐系统

我们接下来回归正题,进行推荐引擎实现,从文章的开头的推荐引擎的架构图中我们可以看到总的过程,一共有这么几个部分:

  1. web页面,用户点击物品,我们获取userid,itemid

  2. 当我们拿到用户行为信息后,进行检索引擎的搜索,这里搜索即召回阶段,这里用到两个算法:CB算法  CF算法
  3. 召回完成之后,我们需要对召回的物品进行精排,怎么精排呢,这里我们需要用到LR逻辑回归算法,需要做一个排序模型,这个模型肯定是一个个性化模型,比如说我们有100个item,张三来了和李四来了,我们给这两个人推荐的肯定是不一样的,那么这个模型该怎么构建,这里就需要用到用户的特征,物品的元数据信息。

  4. 精排完成之后,我们需要过滤一下,取出排名前五的返回给页面

上面是总的推荐过程整体思路,这个比较粗,我们下来进行一一解析。

我们来说一下接下来代码的思路:

  1. 元数据准备,包括用户数据,物品数据,用户行为数据

  2. Nosql中CB算法数据准备

  3. Nosql中CF算法数据准备

  4. LR逻辑回归训练模型的数据准备,即用户特征数据,物品特征数据

  5. 模型准备即通过LR算法训练模型数据得到W,B

  6. 推荐系统实现

              解析请求

              加载模型,用户特征模型,物品特征模型

              进行召回,检索候选池,这里召回我们同时使用CB,CF

              获取用户特征

              获取物品特征

              将召回的物品结合模型进行打分计算sigmoid

              精排序

              过滤即得到TOPN

              封装打包返回给页面

元数据

我们做系统推荐,刚开始我们要通过各种渠道去收集用户特征数据,用户行为数据,这里我们直接把已经收集好并处理好的数据给大家,给大家展示一部分

物品的元数据:第一列是itemid,第二列是itemName,第三列是描述,第四列是总时长,第五列是地域,可能出现是空的情况,第六列是标签,给这打的标签

用户的元数据:第一列:用户id,第二列性别,第三列年龄段,第四列薪资,第五列:地域

用户行为数据:第一列:用户id,第二列:物品id,第三列:观看收听市场,第四列:哪个时刻观看或是收听的

以上就是三个最基本的元数据,用户特征,物品特征,以及连接用户和物品的用户行为数据,好了基本的数据类型已经准备好,我们开始进行我们的推荐系统第一步,元数据准备工作,我们需要把三个元数据拼接到一起,这么做的目的为了后面CB,CF,LR算法数据准备工作,好了,主要的思路就是把三个元数据放到一个文件里面,这个应该不难,我们看下代码:这块代码没有必要说明了,很简单,大家看下注释:

#!usr/bin/python
# -*- coding: UTF-8 -*-
'''
    总体思路:处理原始的数据:1、物品元数据 2、用户元数据 3、用户行为数据
            把三类数据统一到一个文件里面,供后面cb、cf算法进行计算权重
'''

import sys

#找到三类原始数据文件,物品元数据,用户元数据,用户行为数据
music_data = '/home/python_test/pyCharm/rankModel/initData/music_meta'
user_data = '/home/python_test/pyCharm/rankModel/initData/user_profile.data'
user_music_data = '/home/python_test/pyCharm/rankModel/initData/user_watch_pref.sml'

#将三类处理后的元数据放到新的文件里面,这里我们需要定一个文件名,路径
output_file = '../resultData/merge_base.data'

#用写的方式打开文件,为下文写入做准备
ofile = open(output_file,'w')

#step1 处理物品元数据,将处理后的结果放入字典里面,key是itemid,value为物品对应的信息为最后写入做准备
item_dict = {}
with open(music_data,'r') as fd:
    for line in fd:
        ss = line.strip().split('\001')
        if len(ss) != 6:
            continue
        itemid , name ,desc, totalTime,location ,tags = ss
        item_dict[itemid] = '\001'.join([name ,desc, totalTime,location ,tags])

#step2 处理用户元数据,将处理后的结果放入字典里面,key是用户id,value是用户信息
user_dict = {}
with open(user_data,'r') as fd :
    for line in fd:
        ss = line.strip().split(',')
        if len(ss) != 5:
            continue
        userid,gender,age,salary,location = ss
        user_dict[userid] = '\001'.join([gender,age,salary,location])

#step3 写入最后的信息,将用户行为数据进行处理,把step1和step2得到的数据一并归纳在文件里面
with open(user_music_data,'r') as fd:
    for line in fd:
        ss = line.strip().split('\001')
        if len(ss) != 4:
            continue
        userid,itemid,watchTime,hour = ss

        if userid not in user_dict:
            continue
        if itemid not in item_dict:
            continue
        ofile.write('\001'.join([userid,itemid,watchTime,hour,user_dict[userid],item_dict[itemid]]))
        ofile.write('\n')

ofile.close()

数据准备完毕,我们下看结果:从左往右一次是:用户ID,物品ID,观看时长,观看时间段,性别,年龄,薪资,地域,物品名称,描述,总时长,地域,标签

NoSql CB算法数据准备

我们把基本数据准备完成,接下来进行CB算法的数据准备,这里我们就要用到我们以前学的倒排索引的方式,将item数据全部放倒NoSql库中,这里我们NoSql使用reids数据库,我们CB算法数据准备分这几个部分:

  1. 对item分词,计算权重值

  2. 将生成token,itemid,score 文件我们进行转换,转成ii矩阵,进行两两配对,包含相同的token的item放在一起,说明这两个相似,以达到最后召回的目的。

  3. 格式化数据,放入redis数据库

整体思路说完,我们来一步一步解析下,

  • item分词,计算权重值

这里我们需要注意,分词的时候有name,desc,tags三个方面,但是我们肯定对name分词更加关注,所以这里我们要给这三个分词加权重,然后最后形成token,itemid,score文件,我们设置三个形式的权重,name权重高点给0.9,描述给0.2,标签给个0.05,其实这个权重可以自己定义,我们首先把元数据阶段准备好的数据进行数据提取,提取我们需要的itemid,name,tags,desc,然后进行itemid去重复,因为相同的itemName没必要重复添加,分词,去重复后,我们进行分词,然后用分词后的tfidf值乘以权重,在切分描述的时候,我们需要把字典中有的词的分数进行相加,防止重复了

#!usr/bin/python
# -*- coding: UTF-8 -*-
'''
    总体思路:将初始化好的用户,物品,用户行为数据进行处理,目的是为了得到token,itemid,score,我们知道生成的数据里面的name,将itemName
            进行分词,得到tfidf权重,同时将desc进行分词,处理name和desc,我们在元数据中还有已经分类好的tags,tags已经切分好了
            没必要再次进行切分,只需要用idf词表查处权重即可,但是对于name、desc、tags这三个分词结果,我们对name的结果应该更加偏重
            一点,所以分别对这三类得出的分数再次进行分数权重划分,最后得到cb的初始数据
'''
import sys
sys.path.append('../')
reload(sys)
sys.setdefaultencoding('utf-8')

import jieba
import jieba.posseg
import jieba.analyse

#读入初始数据
input_file = "../resultData/merge_base.data"

#定义输出文件
output_file = "../resultData/cb_init.data"
ofile = open(output_file,'w')
#定义三类的权重分数
RATIO_FOR_NAME = 0.9
RATIO_FOR_DESC = 0.1
RATIO_FOR_TAGS = 0.05
#为tags读入idf权重值
idf_file = "../resultData/idf.txt"
idf_dict = {}
with open(idf_file,'r') as fd:
    for line in fd:
        token, idf_score = line.strip().split(' ')
        idf_dict[token] = idf_score
#开始处理初始数据
item_set = set()
with open(input_file,'r') as fd:
    for line in fd:
        ss = line.strip().split('\001')
        if len(ss) != 13:
            continue
        #用户行为
        userid = ss[0].strip()
        itemid = ss[1].strip()
        watchTime = ss[2].strip()
        hour = ss[3].strip()
        #用户元数据
        gender = ss[4].strip()
        age = ss[5].strip()
        salary = ss[6].strip()
        userLocation = ss[7].strip()
        #物品元数据
        name = ss[8].strip()
        desc = ss[9].strip()
        totalTime = ss[10].strip()
        itemLocation = ss[11].strip()
        tags = ss[12].strip()

        #对item去重,相同的itemid不用再计算,因为都一样,这里用到continue特性,当不同的时候才继续执行下面的代码
        if itemid not in item_set:
            item_set.add(itemid)
        else:
            continue

        #去掉重复后的itemID,然后我们进行分词,计算权重,放到字典里面
        token_dict = {}
        #对name统计
        for a in jieba.analyse.extract_tags(name, withWeight=True):
            token = a[0]
            score = a[1]
            token_dict[token] = float(score) * RATIO_FOR_NAME
        #对desc进行分词,这里需要注意的是描述一般会含有name中的词,这里我们把有的词的分数进行相加,没有的放入
        for a in jieba.analyse.extract_tags(desc,withWeight=True):
            token = a[0]
            score = float(a[1])

            if token not in token_dict:
                token_dict[token] = score * RATIO_FOR_DESC
            else:
                token_dict[token] += score * RATIO_FOR_DESC

        #对tags 进行分数计算
        for a in tags.strip().split(','):
            if a.strip() not in idf_dict:
                continue

            if a not in token_dict:
                token_dict[a] = (float(idf_dict[a]) * RATIO_FOR_TAGS)
            else:
                token_dict[a] += (float(idf_dict[a]) * RATIO_FOR_TAGS)

        #循环遍历token_dict,输出toke,itemid,score
        for key,val in token_dict.items():
            token = key.strip()
            score = str(val)

            ofile.write(','.join([token, itemid, score]))
            ofile.write("\n")

ofile.close()
  • 相似的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)
  • 格式化数据放入redis库,我们需要将itemA设置为key ,itemB和score组成 itemB:score这种方式,利用字典,将与itemA配对的都放在一起,最后组成redis的key为itemA,value为与A有关联的其他item,生成数据数据后,利用redis的管道形式批量插入redis库,再插入前格式化一下数据

#!usr/bin/python
# -*- coding: UTF-8 -*-

'''
    思路:我们已经通过CB算法得到itemA,itemB,score,然后我们需要把放入到redis库,存入的方法,我们以itemA为key
         与itemA有相似度的itemB,和分数,以value的形式存入内存库
         1、创建一个字典,将key放入itemA,value 放入与A对应的不同b和分数
         2、循环遍历字典,将key加上前缀CB,value以从大到小的分数进行排序,并且相同的item以——分割,item和score间用:分割
'''
import sys

#读入原始数据
input_file = "../resultData/cb.result"

#输出的文件
output_file = "../resultData/cbRedis.data"
ofile = open(output_file, 'w')
MAX_RECLIST_SIZE = 50
PREFIX = 'CB_'

rec_dict = {}
with open(input_file,'r') as fd:
    for line in fd:
        itemid_A, itemid_B, score = line.strip().split('\t')


        #判断itemA在不在该字典里面,若不在,创建一个key为itemA的列表,把与itemA相关联的itemB和score添加进去
        if itemid_A not in rec_dict:
            rec_dict[itemid_A] = []
        rec_dict[itemid_A].append((itemid_B, score))
    print itemid_A, itemid_B, score
#循环遍历字典,格式化数据,把itemB和score中间以:分割,不同的itemB以_分割
for k,v in rec_dict.items():
    key = PREFIX+k

    #接下来格式化数据,将数据以从大到小排列后再格式化
    #排序,由于数据量大,我们只取100个
    #拍好序后,我们来格式化数据
    result = '_'.join([':'.join([tu[0], str(round(float(tu[1]), 6))]) \
              for tu in sorted(v, key=lambda x: x[1], reverse=True)[:MAX_RECLIST_SIZE]])

    ofile.write(' '.join(['SET',key,result]))
    ofile.write("\n")

ofile.close()

NoSql CF算法数据准备

和CB算法一样,准备NoSql数据库的数据,基本的逻辑一致,分为下面这几个步骤:

  1. Item 喜好程度的分数计算

  2. CF算法数据准备,形成II矩阵,归一化,两两取pair,总和分数

  3. redis数据格式化

  • Item喜好程度的分数计算

这里说明一下,CF算法和CB算法不同的是,我们的数据不一样,CB是用item的属性,例如名称分词,然后权重,但是CF是用户对item打分,那么这个打分我们在这里怎么表示,我们知道用户收听的时长,也知道item的总时长,是不是这个喜好程度可以用收听时长比上总时长表示,这样我们就得到这个分数了,那么分数计算就是做这个事情,但是相同的我们需要加起来才能把用户对同一个item的喜爱程度,最后只需要输出,itemid,userid,score

#!usr/bin/python
# -*- coding: UTF-8 -*-
'''
    总体思路:首先和cb一样,对处理完的用户元数据,物品元数据,行为数据进行cf数据准备工作,我们的目的事输出:user,item score
            其中主要是的到用户对item的score,这里score怎么算呢,当然是用户收听的音乐的时常和总的时长相除的到
'''
import  sys

#读入初始数据
input_file = "../resultData/merge_base.data"

#定义输出文件
output_file = "../resultData/cf_init.data"
ofile = open(output_file,'w')

key_dict = {}
with open(input_file,'r') as fd:
    for line in fd:
        ss = line.strip().split('\001')
        if len(ss) != 13:
            continue
        #用户行为
        userid = ss[0].strip()
        itemid = ss[1].strip()
        watchTime = ss[2].strip()
        hour = ss[3].strip()
        #用户元数据
        gender = ss[4].strip()
        age = ss[5].strip()
        salary = ss[6].strip()
        userLocation = ss[7].strip()
        #物品元数据
        name = ss[8].strip()
        desc = ss[9].strip()
        totalTime = ss[10].strip()
        itemLocation = ss[11].strip()
        tags = ss[12].strip()

        #拼接key,为了将同一个用户对相同物品的时长全部得到需要做个聚类
        key = '_'.join([userid,itemid])

        if key not in key_dict:
            key_dict[key] = []
        key_dict[key].append((float(watchTime),float(totalTime)))

#循环处理相同用户对相同item的分数
for key ,val in key_dict.items():
    wathAll = 0
    totalAll = 0

    for v in val:
        wathAll += v[0]
        totalAll += v[1]

    #得到userid对item的最终分数
    score = float(wathAll) / float(totalAll)
    userid, itemid = key.strip().split('_')

    ofile.write(','.join([userid, itemid, str(score)]))
    ofile.write("\n")

ofile.close()
  • II矩阵数据准备,归一化,取pair对,计算总和

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

  • 归一化

       归一化阶段我们主要是将相同的item进行单位模计算,因为后续我们要用到cos相似度计算公式,将相同的item的分数进行平方和再开根号,最后进行单位化,这里再贴一下,这部分在以前的博客里面讲的很明确,就少啰嗦了,不会可以看看CF算法实现,链接https://blog.csdn.net/Jameslvt/article/details/81280557

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

#!usr/bin/python
# -*- coding: UTF-8 -*-
'''
    思路:转换成i,u,s的矩阵
'''
import  sys

for line in sys.stdin:
    ss = line.strip().split(',')
    if len(ss) != 3:
        continue
    u , i , s = ss
    print '\t'.join([i,u,s])

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

#!usr/bin/python
# -*- coding: UTF-8 -*-
'''
    在map的基础上将每个item进行归一化,map已经将相同的item排好序,这里我们根据map的结果进行给先平方再开根号:
    思路 :
        1、截取字符串,取出item,user,socre
        2、在for循环中进行判断,当前的item和下一个是否相同,要是相同,将相同的放到列表(user,score)列表里面,否则往下执行
        3、若不相同,循环user和score列表,计算模计算,然后再次循环,进行单位化计算
'''

import sys
import math

cur_item = None
user_score_list = []
for line in sys.stdin:
    ss = line.strip().split('\t')
    if len(ss) != 3:
        continue

    item = ss[0]
    userid = ss[1]
    score = ss[2]

    #wordcount判断,当前和下一个是否相同,相同添加到列表,不相同进行归一化计算
    if cur_item == None:
        cur_item = item
    if cur_item != item:
        #定义sum
        sum = 0.0
        #循环列表进行模向量计算
        for ss in user_score_list:
            user,s = ss
            sum += pow(s,2)
        sum = math.sqrt(sum)

        #单位化计算
        for touple in user_score_list:
            u,s = touple
            # 进行单位化完成后,我们输出重置成原来的user-item-score输出
            print "%s\t%s\t%s" % (u, cur_item, float(s / sum))

        #初始化这两个变量
        cur_item = item
        user_score_list = []

    user_score_list.append((userid,float(score)))

#定义sum
sum = 0.0
#循环列表进行模向量计算
for ss in user_score_list:
    user,s = ss
    sum += pow(s,2)
sum = math.sqrt(sum)
#单位化计算
for touple in user_score_list:
    u,s = touple
    # 进行单位化完成后,我们输出重置成原来的user-item-score输出
    print "%s\t%s\t%s" % (u, cur_item, float(s / sum))
  • 两两取pair对

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

map阶段

#!usr/bin/python
# -*- coding: UTF-8 -*-

#在进行pair取对之前,什么都不需要做,输出就行

import  sys

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

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

#!usr/bin/python
# -*- coding: UTF-8 -*-

'''
    思路:进行map排好序之后,我们的会得到相同user对应的不同item和score,这里我们主要的思路是进行相同用户两两取pair
         1、进行判断,当前用户和下一个用户是不是一样,若是不一样,我们进行两两取对,形成ii矩阵
         2、若是相同,我们将不同的item和score放入list里面
'''

import  sys

cur_user = None
item_score_list = []
for line in sys.stdin:
    user,item,score = line.strip().split('\t')

    if cur_user == None:
        cur_user= user

    if cur_user != user:

        #进行两两pair,利用range函数
        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]
                # 输出两遍的目的是为了形成II矩阵的对称
                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)

        cur_user = user
        item_score_list = []

    item_score_list.append((item,float(score)))

#进行两两pair,利用range函数
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]
        # 输出两遍的目的是为了形成II矩阵的对称
        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)
  • 进行最终分数求和

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

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

#!usr/bin/python
# -*- coding: UTF-8 -*-

'''
    sum的map中,我们需要把相同的itemA,itemB组成key,为了使相同的key能够在shuffle阶段分配到同一个reduce中,
    因为是计算item的相似度,要把相同的相加
'''

import  sys

for line in sys.stdin:
    item_a,item_b,score = line.strip().split('\t')
    key = '#'.join([item_a,item_b])
    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)
  • redis数据格式化

这个阶段和CB阶段的格式化完全一样,不再过多的解释

#!usr/bin/python
# -*- coding: UTF-8 -*-
'''
    思路:这个处理的逻辑和CB中完全一样,不一样的是redis的key是CF开头
'''
import sys

#读入原始数据
input_file = "../resultData/cf.result"

#输出的文件
output_file = "../resultData/cfRedis.data"
ofile = open(output_file, 'w')
MAX_RECLIST_SIZE = 100
PREFIX = 'CF_'

rec_dict = {}
with open(input_file,'r') as fd:
    for line in fd:
        itemid_A, itemid_B, score = line.strip().split('\t')

        #判断itemA在不在该字典里面,若不在,创建一个key为itemA的列表,把与itemA相关联的itemB和score添加进去
        if itemid_A not in rec_dict:
            rec_dict[itemid_A] = []
        rec_dict[itemid_A].append((itemid_B, score))

#循环遍历字典,格式化数据,把itemB和score中间以:分割,不同的itemB以_分割
for k,v in rec_dict.items():
    key = PREFIX+k
    #接下来格式化数据,将数据以从大到小排列后再格式化
    #排序,由于数据量大,我们只取100个
    list = sorted(v,key=lambda x:x[1],reverse=True)[:MAX_RECLIST_SIZE]
    #拍好序后,我们来格式化数据
    result = '_'.join([':'.join([str(val[0]),str(round(float(val[1]),6))]) for val in list])

    ofile.write(' '.join(['SET',key,result]))
    ofile.write("\n")

ofile.close()

LR逻辑回归训练模型的数据准备,即用户特征数据,物品特征数据

上面都是我们以前用到过的,接下来我们说下主要精排利用的LR逻辑回归用到的数据,一般逻辑回归进行lr训练模型数据一般格式都是如下:

第一列,一般都是代表标签,也就是我们逻辑回归中的y,以后的都代表特征,冒号之前代表一个特征,例如身高,或是年龄,冒号之后是权重,后面的都代表不同的特征组成的训练测试数据

那么,我们该如何将我们的数据转换成这样的格式,我们来看下,我们要处理的肯定是第一步元数据处理成功的数据

思路:

  1. 得到用户画像数据,物品数据,标签数据

    我们首先确定label,那么这个label怎么确定,我们以实际的wathTime和totalTime除一下,得到一个值,我们设置一个阀值,相当于收听比例大的话,肯定会喜欢,我们阀值定个0.7和0.2,大于0.7我们就认为是喜欢给个1,小于0.2就认为不喜欢给个0,然后我们确定一个字典item的name和id的对应字典,这个字典在做引擎的时候需要用到,进行召回后,展现的是itemName而不是id,让用户看明白,这里顺便提前生成,然后我们输出一个列表,包含标签,用户画像信息,物品信息,我们做个性化推进肯定需要用户画像信息,同时物品信息,除此之外,我们还要输出一个用户去重后的画像信息,和itemName,接下来我们抽特征的时候只需要在两个里面抽取特征就可以了
  2. 抽取用户画像信息,将用户的信息转换成上面我们需要的那种格式做模型训练

    我们如何来抽取特征呢,首先对性别进行抽取,将用户是女的 默认给个index 0,若是男的给个1,然后将这个拼接起来,然后开始对年龄抽取,将年龄以元数据分开,0-18给个2,19-25给个3以此类推,我们就的到年龄的用户特征表达,最后我们给每个特征一个权重值1,最后将所有的权重值放到字典里面,方便全部数据替换时候查询
  3. 抽取物品特征,和用户一样,我们将第一步去重后的itemName取出来,进行分词,然后把分好的词放到set里面去重,这么做是为了我们将token转换成id,把id当作物品的特征来进行模型计算,这里需要注意,我们前面用户画像的特征是从1开始的,这里我们做个偏移量+10,防止与用户画像冲突了

  4. 将我们第一步出来的第一个列表,也就是label,用户信息,物品信息列表中的数据进行替换,字典就查找第2步和第3步的字典,取出userid,物品name,然后输出到文件中,这样我们就得到最终的训练测试数据

  5. 为了能够实时使用用户特征,实时使用物品特征,将第二部和第三部的字典输出到文件中,因为我们在线上,假如用户点了某首音乐,我们要进行精排,只知道用户id和itemid,肯定是不能排序的,所以必须要知道itemid和userid 对应的物品特征和用户特征,然后进行计算打分,精排

  6. 最后输出一个item对应的字典,供返回前台页面使用

那么我们看下代码,结合上面的思路很容易就理解代码了

#!usr/bin/python
# -*- coding: UTF-8 -*-

'''
    思路:这里我们经过cb,cf算法,将数据已经放到内存库,召回部分已经完成,接下来我们需要做排序模型,为逻辑回归准备样本数据
         1、处理第一次将用户元数据,物品元数据,用户行为数据一起归并的数据,也就是merge_base.data,我们在这里需要得到用户画像
            数据,用户信息数据,标签数据
         2、收取样本,标签,用户画像信息,物品信息
         3、抽取用户画像信息,对性别和年龄生成样本数据
         4、抽取item特征信息,分词获得token,score,做样本数据
         5、拼接样本,生成最终的样本信息,作为模型进行训练
'''

import sys
sys.path.append('../')
reload(sys)
sys.setdefaultencoding('utf-8')

import jieba
import jieba.analyse
import jieba.posseg

input_file = '../resultData/merge_base.data'
#最终生成的样本数据
output_file = '../resultData/samples.data'

#我们这里需要再生成两个文件,一个是用户样本和item样本,因为要对实时推荐的化,必须使用这两个样本
output_user_feature_file = '../resultData/user_feature.data'
output_item_feature_file = '../resultData/item_feature.data'
#这里生成个类似name和id对应的字典信息
output_itemid_to_name_file = '../resultData/name_id.dict'

#定义函数,来获取各类数据
def get_base_samples(file):
    #放待处理样本数据
    ret_samples_list = []
    #放user用户数据
    user_info_set = set()
    #放物品数据
    item_info_set = set()
    item_name2id = {}
    item_id2name = {}


    with open(file,'r') as fd:
        for line in fd:
            ss = line.strip().split('\001')
            if len(ss) != 13:
                continue

            userid = ss[0].strip()
            itemid = ss[1].strip()
            #这两个时间为了计算label而使用
            watchTime = ss[2].strip()
            totalTime = ss[10].strip()

            #用户数据
            gender = ss[4].strip()
            age = ss[5].strip()
            user_feature = '\001'.join([userid,gender,age])

            #物品数据
            name = ss[8].strip()
            item_feature = '\001'.join([itemid,name])

            #计算标签
            label = float(watchTime)/float(totalTime)
            final_label = 0

            if label >= 0.7:
                final_label = '1'
            elif label <= 0.2:
                final_label = '0'
            else:
                continue

            #接下来装在数据,并返回结果,首先我们装在itemid2name和itemname2id
            item_name2id[name] = itemid

            item_id2name[itemid] = name

            #装在待处理的标签数据
            ret_samples_list.append([final_label,user_feature,item_feature])

            user_info_set.add(user_feature)
            item_info_set.add(name)

        return ret_samples_list, user_info_set, item_info_set, item_name2id, item_id2name

#step 1 程序的入口,开始调用函数,开始处理文件,得到相应的数据
base_sample_list, user_info_set, item_info_set, item_name2id, item_id2name = get_base_samples(input_file)

#step 2 抽取用户画像信息,用户标签转换,将年龄和age进行转换,用于样本使用
user_fea_dict = {}
for ss in user_info_set:
    userid, gender, age = ss.strip().split('\001')

    #设置标签idx,将男(1)和女(0)用数剧的形式表示,权重都设置为1
    idx = 0

    if gender == '男':
        idx = 1
    #将标签和权重拼接起来
    gender_fea = ':'.join([str(idx), '1'])

    #性别设置完成,我们接下来设置年龄,将年龄进行划分,0-18,19-25,26-35,36-45
    if age == '0-18':
        idx = 2
    elif age == '19-25':
        idx = 3
    elif age == '26-35':
        idx = 4
    elif age == '36-45':
        idx = 5
    else:
        idx = 6

    age_fea = ':'.join([str(idx),'1'])

    #最后将性别特征和年龄特征放入list中
    user_fea_dict[userid] = ' '.join([gender_fea, age_fea])

#step 3 抽取物品特征,这里我们要用到分词,将name进行分词,并且把分词后的token转换成id,这里就需要我们来做生成tokenid词表
token_set = set()
item_fs_dict = {}
for name in item_info_set:
    token_score_list = []
    for x,w in jieba.analyse.extract_tags(name,withWeight=True):
        token_score_list.append((x,w))
        token_set.add(x)
    item_fs_dict[name] = token_score_list

#进行token2id的转换
token_id_dict = {}
#这里我们要用到刚刚利用set去重过的token列表,生成tokenid的字典表
for s in enumerate(list(token_set)):
    token_id_dict[s[1]] = s[0]

#接下来,我们需要把第三步生成的item_fs_dict中name对应的token全部替换成id,然后当作字典,为下面的全量替换做准备
item_fea_dict = {}
user_feature_offset = 10
for name ,fea in item_fs_dict.items():
    token_score_list = []
    for (token,score) in fea:
        if token not in token_id_dict:
            continue
        token_id = token_id_dict[token] + user_feature_offset
        token_score_list.append(':'.join([str(token_id),str(score)]))

    #接下来输出到字典中
    item_fea_dict[name] = ' '.join(token_score_list)

#step 4 将第一步输出的样本数据整体替换并且替换user_feature和item_feature,并输出到文件中
ofile = open(output_file,'w')
for (label,userfea,itemfea) in base_sample_list:
    userid = userfea.strip().split('\001')[0]
    item_name = itemfea.strip().split('\001')[1]

    if userid not in user_fea_dict:
        continue
    if item_name not in item_fea_dict:
        continue

    ofile.write(' '.join([label,user_fea_dict[userid],item_fea_dict[item_name]]))
    ofile.write('\n')

ofile.close()

#step 5 为了能够实时使用userfeatre,我们需要输出一下
out_put_file = open(output_user_feature_file,'w')
for userid,fea in user_fea_dict.items():
    out_put_file.write('\t'.join([userid,fea]))
    out_put_file.write('\n')
out_put_file.close()

#step 6 输出item_feature
out_file = open(output_item_feature_file,'w')
for name,fea in item_fea_dict.items():
    if name not in item_name2id:
        continue
    itemid = item_name2id[name]
    out_file.write('\t'.join([itemid,fea]))
    out_file.write('\n')

#step 7 输出id2name的对应的字典
o_file = open(output_itemid_to_name_file,'w')
for id,name in item_id2name.items():
    o_file.write('\t'.join([id,name]))
    o_file.write('\n')
o_file.close()

模型准备即通过LR算法训练模型数据得到W,B

我们已经在上面把训练模型的数据准备好了,我们来看下上一步得到数据:

好,接下来我们来看下训练LR的代码,怎么能够得到W,B

首先main方法是主体,先进入main方法,然后我们能看到调用了一个load_data()函数,返回了四个值,训练x,测试x,训练y,测试y,训练的x和y是用来做训练用的,测试的用来做测试用的,然后sklearn里面已经提供了一个LogisticsRegression方法,这里面有个参数,就是正则化的L1,或是L2,这里没有讲正则化,大家可以去看看其他博客,然后用这个模型拟合训练集x,y得到model,通过model中的两个参数得到一个w,一个b,得到w和b,然后我们做一下测试,在测试集上,评估一下这个模型。

接下来我们说下load_data()函数,这个就是整个逻辑回归重点,其实希望大家把逻辑回归的源码也多多看看,明白怎么计算就好,这个计算就是我们上面说的sklearn的方法,我们还是来回过来看看怎么加载数据,能够进行训练,load_data主要干这么几件事,将label,各类特征转换成矩阵的形式,第一我们要申请几个list,分别用来放label,行号,列特征,各个列特征对应的分数,这么几个list,这主要是为了提前申请好空间,每一行代表个记录,每一列代表一个特征,我们使用csr_matrix将这几个list转换何曾表,但是我们不能直接使用list,我们要使用np转换成np.array,再使用csr_matrix,形成矩阵,最后通过train_test_split得到训练x,测试x,训练y,测试y

# -*- coding: UTF-8 -*-
'''
    思路:这里我们要用到我们的数据,就需要我们自己写load_data的部分,
         首先定义main,方法入口,然后进行load_data的编写
         其次调用该方法的到x训练x测试,y训练,y测试,使用L1正则化或是L2正则化使得到结果更加可靠
         输出wegiht,和b偏置
'''
import sys
import numpy as np
from scipy.sparse import csr_matrix

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

input_file = sys.argv[1]

def load_data():
    #由于在计算过程用到矩阵计算,这里我们需要根据我们的数据设置行,列,和训练的数据准备
    #标签列表
    target_list = []
    #行数列表
    fea_row_list = []
    #特征列表
    fea_col_list = []
    #分数列表
    data_list = []

    #设置行号计数器
    row_idx = 0
    max_col = 0

    with open(input_file,'r') as fd:
        for line in fd:
            ss = line.strip().split(' ')
            #标签
            label = ss[0]
            #特征
            fea = ss[1:]

            #将标签放入标签列表中
            target_list.append(int(label))

            #开始循环处理特征:
            for fea_score in fea:
                sss = fea_score.strip().split(':')
                if len(sss) != 2:
                    continue
                feature, score = sss
                #增加行
                fea_row_list.append(row_idx)
                #增加列
                fea_col_list.append(int(feature))
                #填充分数
                data_list.append(float(score))
                if int(feature) > max_col:
                    max_col = int(feature)

            row_idx += 1

    row = np.array(fea_row_list)
    col = np.array(fea_col_list)
    data = np.array(data_list)

    fea_datasets = csr_matrix((data, (row, col)), shape=(row_idx, max_col + 1))

    x_train, x_test, y_train, y_test = train_test_split(fea_datasets, s, test_size=0.2, random_state=0)

    return x_train, x_test, y_train, y_test

def main():
    x_train,x_test,y_train,y_test = load_data()
    #用L2正则话防止过拟合
    model = LogisticRegression(penalty='l2')
    #模型训练
    model.fit(x_train,y_train)

    ff_w = open('model.w', 'w')
    ff_b = open('model.b', 'w')

    #写入训练出来的W
    for w_list in model.coef_:
        for w in w_list:
            print >> ff_w, "w: ", w
    # 写入训练出来的B
    for b in model.intercept_:
        print >> ff_b, "b: ", b
    print "precision: ", model.score(x_test, y_test)
    print "MSE: ", np.mean((model.predict(x_test) - y_test) ** 2)

if __name__ == '__main__':
    main()

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

推荐系统实现

  • 解析请求

  • 加载模型,用户特征模型,物品特征模型

  • 进行召回,检索候选池,这里召回我们同时使用CB,CF

  • 获取用户特征

  • 获取物品特征(将召回的物品结合模型进行打分计算sigmoid)

  • 精排序

  • 过滤即得到TOPN

  • 封装打包返回给页面

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

#coding=utf-8
import web
import sys
import redis
import json
import math

sys.path.append("./")
import jieba
import jieba.posseg
import jieba.analyse

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

render = web.template.render('templates/')
web.template.Template.globals['render'] = render

config = web.storage(
            static = '/static',
            resource = '/resource'
            )
web.template.Template.globals['config'] = config

app = web.application(urls, globals())

# 加载user特征
user_fea_dict = {}
with open('/home/python_test/pyCharm/rankModel/resultData/user_feature.data') as fd:
    for line in fd:
        userid, fea_list_str = line.strip().split('\t')
        user_fea_dict[userid] = fea_list_str

# 加载item特征
item_fea_dict = {}
with open('/home/python_test/pyCharm/rankModel/resultData/item_feature.data') as fd:
    for line in fd:
        ss = line.strip().split('\t')
        if len(ss) != 2:
            continue
        itemid, fea_list_str = ss
        item_fea_dict[itemid] = fea_list_str

class test:
    def GET(self):
        ret = '111'

        return ret


class index:
    def GET(self):
        return render.index_badou(' ', ' ', ' ', '')


    def POST(self):
        param = web.input()
        print "==============="
        content = param['item_id']
        # step 1 解析请求,上面我们已经得到userid,itemid
        userid ,reqitemid = content.split(',')
        print userid,reqitemid
        r = redis.Redis(host='master', port=6379, db=0)

        #step2 加载模型
        model_w_path = '../model.w'
        model_b_path = '../model.b'

        model_w_list = []
        model_b = 0

        with open(model_w_path,'r') as fd:
            for line in fd:
                ss = line.strip().split(' ')
                if len(ss) !=3:
                    continue
                model_w_list.append(float(ss[2].strip()))

        with open(model_b_path,'r') as fd:
            for line in fd:
                ss = line.strip().split(' ')
                model_b = float(ss[2].strip())

        #step 3 进行召回阶段,检索候选池,这里我们分两次,cb,cf
        #将检索回来的item全部放到recallitem列表里面
        rec_item_mergeall = []
        # 3.1 cf检索
        cf_recinfo = 'null'
        key = '_'.join(['CF', reqitemid])
        if r.exists(key):
            cf_recinfo = r.get(key)
        if len(cf_recinfo) > 6:
            for cfinfo in cf_recinfo.strip().split('_'):
                item,score = cfinfo.strip().split(':')
                rec_item_mergeall.append(item)
        #3.2 cb检索
        cb_recinfo = 'null'
        key = '_'.join(['CB_',reqitemid])
        if r.exists(key):
            cb_recinfo = r.get(key)
        if len(cb_recinfo) > 6:
            for cbinfo in cb_recinfo.strip().split('_'):
                item,score = cbinfo.strip().split(':')
                rec_item_mergeall.append(item)
        #step 4 获取用户特征,将获取的用户特征处理后放到字典里面,方便后续计算内积
        user_fea = ''
        if userid in user_fea_dict:
            user_fea = user_fea_dict[userid]

        u_fea_dict = {}
        for ss in user_fea.strip().split(' '):
            sss = ss.strip().split(':')
            if len(sss) != 2:
                continue
            idx = int(sss[0].strip())
            score = float(sss[1].strip())
            u_fea_dict[idx] = score

        # step 5 获取物品的特征 ,循环遍历刚刚得到itemid,判断item是否在item特征中,若在开始进行处理
        rec_list = []
        for itemid in rec_item_mergeall:
            if itemid in item_fea_dict:
                tem_fea = item_fea_dict[itemid]

                i_fea_dict = {}
                #循环遍历item特征
                for fea_idx in tem_fea.strip().split(' '):
                    ss = fea_idx.strip().split(':')
                    if len(ss) != 2:
                        continue
                    idx = int(ss[0].strip())
                    score = float(ss[1].strip())
                    i_fea_dict[idx] = score

                #我们得到召回item对应的特征和用户的特征,我们接下来根据模型求出来的w,b,进行打分
                wx_score = 0
                #这里我们求个内积,wx,然后做sigmoid,先将两个字典拼接起来,然后计算分数
                for index,score in dict(u_fea_dict.items()+i_fea_dict.items()).items():
                    wx_score +=  (score * model_w_list[index])

                #计算sigmoid
                final_rec_score = 1 / (1 + math.exp(-(wx_score + model_b)))
                #将itemID和分数放入列表中,方便后续排序
                rec_list.append((itemid, final_rec_score))

        #step 6 精排
        rec_sort_list = sorted(rec_list, key=lambda x: x[1], reverse=True)

        #step 7 过滤(filter)
        rec_fitler_list = rec_sort_list[:5]
        print rec_fitler_list
        #step 8 进行将itemid转换成name
        itemid2name = {}
        with open('/home/python_test/pyCharm/rankModel/resultData/name_id.dict','r') as fd:
            for line in fd:
                itemid,name = line.strip().split('\t')
                itemid2name[itemid] = name
        #进行替换
        ret_list = []
        for tup in rec_fitler_list:
            req_item_name = itemid2name[reqitemid]
            item_name = itemid2name[tup[0]]
            item_rank_score = str(tup[1])
            ret_list.append('   ->   '.join([item_name, item_rank_score]))

        # ret = '   '.join(rec_sort_list)
        ret = ret_list

        if len(ret) <= 0:
            return json.dumps({'block_one':' ', 'block_two':' '},ensure_ascii=True, encoding='UTF-8')
        else:
            return json.dumps({'block_one':req_item_name.decode("utf8","ignore"), \
                    'block_two':'<br>'.join(ret).decode("utf8","ignore")},ensure_ascii=True, encoding='UTF-8')

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

我们来看下页面实现的推荐结果:

至此,单机版的推荐引擎部分就全部实现,中间还会有很多优化的地方,在现实生活中面对几亿的用户数据不可能使用的是单机模式,至于优化,后续持续更新,代码的整体部分可以到资源库下载,或是直接留言

猜你喜欢

转载自blog.csdn.net/Jameslvt/article/details/81571430
今日推荐