个性化推荐算法整理(二)

个性化推荐算法整理

基于深度学习的个性化召回推荐算法item2vec

item2vec是基于word2vec的原理,word2vec的详细内容请参考Tensorflow深度学习算法整理(二)

我们先将原始数据文件转换成训练数据集文件,训练数据文件包含每一个用户点击过的物品id。在这里,我们是把用户点击过的Itemid当成上下文的词向量,再来根据用户的喜好来挑选出出现概率最大的物品来进行推荐。

INPUT = "../item2vec_data/"
import os

def product_train_data(input_file, output_file):
    '''
    获取训练数据集文件
    :param input_file:
    :param output_file:
    :return:
    '''
    if not os.path.exists(input_file):
        return
    # 用户点击过的商品{用户id:[商品id1,商品id2,...]}
    record = {}
    linenum = 0
    score_thr = 4.
    with open(input_file, 'r') as fp:
        for line in fp:
            if linenum == 0:
                linenum += 1
                continue
            item = line.strip().split(',')
            if len(item) < 4:
                continue
            userid, itemid, rating = item[0], item[1], float(item[2])
            if rating < score_thr:
                continue
            if userid not in record:
                record.setdefault(userid, [])
            record[userid].append(itemid)
    with open(output_file, 'w') as fw:
        for userid in record:
            fw.write(" ".join(record[userid]) + "\n")

if __name__ == "__main__":

    product_train_data(INPUT + "ratings.txt", INPUT + "train_data.txt")

运行结果

现在我们便对训练数据进行训练,然后再根据物品的embedding相似度进行推荐

INPUT_PATH = "../item2vec_data/"
from gensim.models import word2vec
import logging
from util.reader import get_user_click
import pprint

def get_sentence(data_path):
    # 预处理
    sentence = []
    with open(data_path, 'r') as f:
        for line in f:
            sentence.append(line.strip())
    return sentence

def cal_recom_result(distance, user_click):
    model = word2vec.Word2Vec.load(distance)
    recent_click_num = 3
    topk = 5
    recom_info = {}
    for user in user_click:
        click_list = user_click[user]
        recom_info.setdefault(user, {})
        for itemid in click_list[:recent_click_num]:
            try:
                recom_info[user].setdefault(itemid, [])
                recom_info[user][itemid].append(model.wv.most_similar(itemid, topn=topk))
            except Exception:
                continue
    return recom_info

if __name__ == "__main__":

    sentence = get_sentence(INPUT_PATH + "train_data.txt")
    train_data = [list(item.split(" ")) for item in sentence]
    logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.INFO)
    model = word2vec.Word2Vec(train_data, vector_size=200, sg=0,
                              workers=-1, min_count=1, epochs=3)
    model.save(INPUT_PATH + "distance")
    user_click, user_click_time = get_user_click(INPUT_PATH + "ratings.txt")
    pprint.pprint(cal_recom_result(INPUT_PATH + "distance", user_click)["1"])

运行结果(省略日志)

{'2': [[('3527', 0.2799011170864105),
        ('111115', 0.26561179757118225),
        ('26444', 0.25397157669067383),
        ('432', 0.25121787190437317),
        ('53766', 0.24429647624492645)]],
 '29': [[('2178', 0.2700711488723755),
         ('347', 0.2577595114707947),
         ('6666', 0.2471877485513687),
         ('26339', 0.24447323381900787),
         ('83468', 0.23756802082061768)]],
 '32': [[('7171', 0.26247233152389526),
         ('97834', 0.26078081130981445),
         ('7348', 0.25278741121292114),
         ('1654', 0.2401883602142334),
         ('7885', 0.22451621294021606)]]}

现在我们来扩展一下,将物品的相关内容也放入进来进行相似度的推荐。

基于内容的推荐算法content based

  • 个性化召回算法Content Based背景介绍

基于内容的推荐不同于之前任何一种个性化召回算法,它属于独立的分支。像之前的CF、LFM、Personal Rank都同属于基于邻域的推荐。Item2vec属于深度学习的推荐。

  • Content Based算法主体流程介绍

在这个算法的主体流程大部分并不属于个性化推荐的范畴,实际上应该从属于NLP或者用户画像的内容范畴。只有极少数的一部分属于个性化推荐算法的内容范畴。

背景

  • 思路极简,可解释性强。

任何一个推荐系统的初衷都是为了推荐出用户喜欢的Item。而基于内容的推荐恰是刻画出用户的喜好之后给予用户推荐这个喜好的物品。如果某一个用户访问系统的时候经常点击体育类的新闻,在这个用户下一次访问系统的时候,自然而然的系统更加倾向性的给他推荐体育类型的新闻。对于推荐结果可解释性非常的强。

  • 用户推荐的独立性

基于内容的推荐结果只与该用户本身的行为有关系,其余用户的行为是影响不到该用户的推荐结果。但是无论是CF、LFM、Personal Rank以及Item2vec,其余用户的行为都会一定程度上或多或少地干预到最后的推荐结果。

  • 问世较早,流行度高

基于内容推荐的极简性、可解释性,所以它出现的非常早,并且无论是在工业界还是研究界都作为一种基础的召回算法,流行度非常高。但是任何事物都是有两面性的,基于内容的推荐并不完美,它有非常明显的缺点:1、它对于推荐的扩展性是比较差的,也就是说如果一个用户之前经常访问体育类的新闻,那么在之后的推荐中,倾向于在体育范围内不断的挖掘,而很难完成跨领域的物品推荐。2、需要积累一定量的用户的行为,才能够完成基于内容的推荐。

Content Based算法主流程

  • Item Profile

针对于基于内容推荐下,Item的刻画大体可以分为两大类:1、关键词刻画;2、类别刻画。无论在什么场景下,都是这两个类的刻画。譬如信息流场景下,我们需要刻画出一件新闻属于财经还是娱乐。这件新闻讲的是某个球星还是某个明星。在电商场景下,我们需要刻画出这个物品它属于图书还是属于玩具,具体的关键词上,也会有是讲这个物品是讲机器学习的还是讲人文情感的。这个物品是参与满减的还是参与包邮的。

  • User Profile

第一步,我们完成了物品的内容刻画之后,第二步我们需要对用户进行刻画。传统范畴的用户画像是比较宽泛的,它不仅包含了用户的一些动态特征,还包含了它的一些静态特征。而我们用在基于内容推荐里的,更多的聚焦在用户长期短期的行为,继而通过这个行为的分析,将用户感兴趣的类别予以刻画。

  • Online Recommendation

有了Item的刻画,有了User的刻画,便是在线上完成个性化推荐的过程。也就是说给用户推荐他最感兴趣的一些类别。假使某个用户经常点击某个球星的新闻,当这个用户访问系统的时候,我们应该将该球星最新的新闻最及时的消息推荐给这个用户,这样点击率自然会很高。

经过这三步的介绍,我们发现前两步是从属于NLP和用户画像的范畴,第三步是个性化内容推荐的范畴。

Item Profile技术要点

  • Topic FInding

针对于Topic发现,我们首先要选定特征,这里的特征是title和内容主体的词语的分词。我们得到了这个词语的分词之后,针对于Topic的发掘,我们采用命名实体识别的方式,这个方式可以去匹配关键词词表。得到了这些关键词之后,我们需要对这些关键词进行一定的排名。将排名最高的Top 3或Top 5给Item完成Label。至于这里的排名,我们会用一些算法加一些规则,算法诸如像TF-IDF(关于TF-IDF的内容请参考Tensorflow深度学习算法整理(二) ),规则是基于我们自己的场景总结出来的一些来修正错误keys的一些规则。

  • Genre Classify

对于类别的划分,我们同样是首先选定好特征,这里同样是利用一些文本信息,比如说title或者正文中所有的去过标点之后的分词得到的词向量,这里词向量可以直接在浅层模型中进行one-hot编码。在深层模型中可以进行embedding。这里使用的分类模型主要是像早期的逻辑回归,像中期的FastText以及近期的Text CNN等等。这些分类器我们在使用的时候,只使用多种分类器分别在不同的权重,然后对结果进行一个线性的加权,从而得到正确的分类。

以上我们是针对于文档的Topic发掘或者是类别的分类进行的叙述。对于短视频,实际上现在引入了一些更多的特征,比如关键帧所对应图像的分类识别以及音频所对应语音识别后文字的处理等一些有益的尝试。

User Profile技术要点

  • Genre/Topic

用户对哪些种类的新闻或者说物品感兴趣,另一个层面就是说对哪些关键词感兴趣。现在多是基于统计的方式,也同时做一些有益的尝试,比如引入一些分类器等等。

  • Time Decay

对于用户的刻画,我们一定要注意时间衰减。也就是不同时期的行为所占权重是不同的。最终我们想刻画得到的结果是针对某个用户,我们想得到用户对于不同种类Item的倾向性,譬如某个用户对于娱乐的倾向性是0.7,对于财经的倾向性是0.3。

Online Recommendation技术要点

  • Find top k Genre/Topic

基于用户的刻画,找到用户最感兴趣的top k个分类。

  • Get the best n item from fixed Genre/Topic

由于这top k个分类都是带有权重的,相应的给每个分类得到n个最好的这个分类下的item。这里有两点需要说明,由于权重的不同,不同种类下召回的n个数目是不同的,譬如某人对财经感兴趣对娱乐也感兴趣,但是对娱乐感兴趣的程度更高,那么这里对娱乐召回的数目就要多于对财经的召回的数目。这里的best对于不是新item来讲,就是它后面CTR;如果是新的item,我们在入库的时候,都会给出一个预估的CTR,那么我们就用这个预估的CTR来作为item是否好的衡量标准。

代码实现

我们先来实现物品平均评分和均分物品类别权重以及物品类别的倒排

import os

def get_ave_score(input_file):
    '''
    物品平均评分
    :param input_file: 评分文件
    :return:
    '''
    if not os.path.exists(input_file):
        return {}
    linenum = 0
    record = {}
    ave_score = {}
    with open(input_file, 'r') as fp:
        for line in fp:
            if linenum == 0:
                linenum += 1
                continue
            item = line.strip().split(",")
            if len(item) < 4:
                continue
            userid, itemid, rating = item[0], item[1], float(item[2])
            if itemid not in record:
                record[itemid] = [0, 0]
            # 这里0为物品被评价的总分数,1为物品为物品被总点击的次数
            record[itemid][0] += rating
            record[itemid][1] += 1
    for itemid in record:
        # 计算物品的平均评价分数
        ave_score[itemid] = round(record[itemid][0] / record[itemid][1], 3)
    return ave_score

def get_item_cate(ave_score, input_file):
    '''
    均分类别权重和物品类别倒排
    :param ave_score: 物品平均评分
    :param input_file: 物品详情文件
    :return:
    '''
    if not os.path.exists(input_file):
        return {}, {}
    linenum = 0
    item_cate = {}
    record = {}
    cate_item_sort = {}
    topk = 100
    with open(input_file, 'r') as fp:
        for line in fp:
            if linenum == 0:
                linenum += 1
                continue
            item = line.strip().split(",")
            if len(item) < 3:
                continue
            itemid = item[0]
            cate_str = item[-1]
            # 获取物品的分类列表
            cate_list = cate_str.strip().split("|")
            # 物品的分类权重
            ratio = round(1 / len(cate_list), 3)
            if itemid not in item_cate:
                item_cate.setdefault(itemid, {})
            for fix_cate in cate_list:
                # 储存该物品的分类权重{物品id:{物品分类:分类权重}}
                item_cate[itemid][fix_cate] = ratio
    for itemid in item_cate:
        # 遍历每一个物品的分类
        for cate in item_cate[itemid]:
            if cate not in record:
                record.setdefault(cate, {})
            # 获取该物品的平均评价分数
            itemid_rating_score = ave_score.get(itemid, 0)
            # 按照物品分类来储存该分类内各个物品的平均评价分数{物品分类:{物品id:平均评价分数}}
            record[cate][itemid] = itemid_rating_score
    for cate in record:
        if cate not in cate_item_sort:
            cate_item_sort.setdefault(cate, [])
        # 对每一个物品分类中按照平均评价分数对物品id进行排序
        for combine in sorted(record[cate].items(),
                              key=lambda x: x[1],
                              reverse=True)[:topk]:
            # 储存排序后的各个物品分类中物品的平均评价分数{物品分类:[(物品id1,平均评价分数1), (物品id2,平均评价分数2)]}
            # 这里平均评价分数1>=平均评价分数2
            cate_item_sort[cate].append((combine[0], combine[1]))
    return item_cate, cate_item_sort

if __name__ == "__main__":

    ave_score = get_ave_score("../data/ratings.txt")
    print(len(ave_score))
    print(ave_score['31'])
    item_cate, cate_item_sort = get_item_cate(ave_score, "../data/movies.txt")
    print(item_cate['1'])
    print(cate_item_sort['Children'])

运行结果

4382
3.167
{'Adventure': 0.2, 'Animation': 0.2, 'Children': 0.2, 'Comedy': 0.2, 'Fantasy': 0.2}
[('250', 5.0), ('1030', 5.0), ('2091', 5.0), ('2102', 5.0), ('2430', 5.0), ('4519', 5.0), ('26084', 5.0), ('27790', 5.0), ('156025', 5.0), ('2046', 4.75), ('2138', 4.5), ('3034', 4.5), ('6350', 4.5), ('7164', 4.5), ('85736', 4.5), ('5971', 4.4), ('262', 4.375), ('26662', 4.333), ('81564', 4.333), ('1033', 4.25), ('3213', 4.25), ('78499', 4.222), ('2005', 4.167), ('76093', 4.143), ('1148', 4.125), ('60069', 4.1), ('745', 4.083), ('2987', 4.059), ('8', 4.0), ('314', 4.0), ('837', 4.0), ('917', 4.0), ('953', 4.0), ('1009', 4.0), ('1011', 4.0), ('1021', 4.0), ('1023', 4.0), ('1031', 4.0), ('1223', 4.0), ('1566', 4.0), ('2014', 4.0), ('2034', 4.0), ('2037', 4.0), ('2083', 4.0), ('2096', 4.0), ('2099', 4.0), ('2141', 4.0), ('2687', 4.0), ('2846', 4.0), ('3086', 4.0), ('3159', 4.0), ('3189', 4.0), ('3564', 4.0), ('5159', 4.0), ('6559', 4.0), ('6753', 4.0), ('6793', 4.0), ('6951', 4.0), ('8537', 4.0), ('27253', 4.0), ('27731', 4.0), ('37857', 4.0), ('50601', 4.0), ('56171', 4.0), ('68954', 4.0), ('79091', 4.0), ('84944', 4.0), ('86298', 4.0), ('87222', 4.0), ('95858', 4.0), ('106022', 4.0), ('110461', 4.0), ('1282', 3.929), ('1907', 3.917), ('59784', 3.917), ('50872', 3.885), ('919', 3.861), ('531', 3.857), ('596', 3.85), ('3396', 3.833), ('1', 3.829), ('2804', 3.818), ('1073', 3.812), ('1097', 3.808), ('34', 3.795), ('4886', 3.792), ('2081', 3.773), ('986', 3.75), ('2033', 3.75), ('2052', 3.75), ('2080', 3.75), ('2087', 3.75), ('5103', 3.75), ('6316', 3.75), ('8961', 3.75), ('38038', 3.75), ('44022', 3.75), ('46948', 3.75), ('65261', 3.75), ('103335', 3.75)]

现在我们来进行用户刻画。

import os

def get_up(item_cate, input_file):
    '''
    用户刻画
    获取某用户点击过的某物品分类占该用户点击过的所有物品分类的评分占比
    :param item_cate: 物品分类权重
    :param input_file: 用户对物品的评分文件
    :return:
    '''
    if not os.path.exists(input_file):
        return {}
    record = {}
    up = {}
    linenum = 0
    score_thr = 4.
    topk = 2
    with open(input_file, 'r') as fp:
        for line in fp:
            if linenum == 0:
                linenum += 1
                continue
            item = line.strip().split(',')
            if len(item) < 4:
                continue
            userid, itemid, rating, timestamp = item[0], item[1], float(item[2]), int(item[3])
            if rating < score_thr:
                continue
            # 如果该物品没有分类
            if itemid not in item_cate:
                continue
            # 获取用户对物品的时间评分
            time_score = get_time_score(timestamp)
            if userid not in record:
                record.setdefault(userid, {})
            # 遍历该物品的所有分类
            for fix_cate in item_cate[itemid]:
                if fix_cate not in record[userid]:
                    record[userid].setdefault(fix_cate, 0)
                # 记录该用户点击过的物品分类的总评分{用户id:{物品分类:总评分}}
                # 物品分类总评分为用户对物品的评分*该分类的权重*时间间隔权值
                record[userid][fix_cate] += rating * time_score * item_cate[itemid][fix_cate]
    for userid in record:
        if userid not in up:
            up.setdefault(userid, [])
        total_score = 0
        # 对物品分类总评分进行排序,并对排前2位的物品分类总评分进行遍历
        # 这里combine[0]是物品分类
        # combine[1]是某用户点击过的物品分类总评分
        for combine in sorted(record[userid].items(),
                              key=lambda x: x[1],
                              reverse=True)[:topk]:
            up[userid].append((combine[0], combine[1]))
            # 累加该用户点击过的所有物品分类的总评分
            total_score += combine[1]
        for index in range(len(up[userid])):
            # 存储某用户点击过的单个物品分类总评分占所有物品分类总评分的比率
            # {用户id:{物品分类:该分类的评分占比}}
            up[userid][index][1] = round(up[userid][index][1] / total_score, 3)
    return up

def get_time_score(timestamp):
    '''
    获取时间间隔权值,如果时间间隔越大,该权值越低
    :param timestamp: 用户对物品评分的时间戳
    :return:
    '''
    fix_timestamp = 1476086345
    total_sec = 24 * 60 * 60
    delta = (fix_timestamp - timestamp) / total_sec
    return round(1 / (1 + delta), 3)

此时再进行我们的推荐算法。在实际的项目中,基于内容的推荐实际上我们只需要将用户刻画存入到Redis K:V当中,将物品倒排存入到搜索引擎Elastic Search当中,这个推荐的过程实际上是在线实时的分别去请求K:V和搜索引擎获取物品倒排的。现在我们来模拟这个过程。

import os
from util.content_read import get_item_cate, get_ave_score
import pprint

def get_up(item_cate, input_file):
    '''
    用户刻画
    获取某用户点击过的某物品分类占该用户点击过的所有物品分类的评分占比
    :param item_cate: 物品分类权重
    :param input_file: 用户对物品的评分文件
    :return:
    '''
    if not os.path.exists(input_file):
        return {}
    record = {}
    up = {}
    linenum = 0
    score_thr = 4.
    topk = 2
    with open(input_file, 'r') as fp:
        for line in fp:
            if linenum == 0:
                linenum += 1
                continue
            item = line.strip().split(',')
            if len(item) < 4:
                continue
            userid, itemid, rating, timestamp = item[0], item[1], float(item[2]), int(item[3])
            if rating < score_thr:
                continue
            # 如果该物品没有分类
            if itemid not in item_cate:
                continue
            # 获取用户对物品的时间评分
            time_score = get_time_score(timestamp)
            if userid not in record:
                record.setdefault(userid, {})
            # 遍历该物品的所有分类
            for fix_cate in item_cate[itemid]:
                if fix_cate not in record[userid]:
                    record[userid].setdefault(fix_cate, 0)
                # 记录该用户点击过的物品分类的总评分{用户id:{物品分类:总评分}}
                # 物品分类总评分为用户对物品的评分*该分类的权重*时间间隔权值
                record[userid][fix_cate] += rating * time_score * item_cate[itemid][fix_cate]
    for userid in record:
        if userid not in up:
            up.setdefault(userid, [])
        total_score = 0
        # 对物品分类总评分进行排序,并对排前2位的物品分类总评分进行遍历
        # 这里combine[0]是物品分类
        # combine[1]是某用户点击过的物品分类总评分
        for combine in sorted(record[userid].items(),
                              key=lambda x: x[1],
                              reverse=True)[:topk]:
            up[userid].append((combine[0], combine[1]))
            # 累加该用户点击过的所有物品分类的总评分
            total_score += combine[1]
        for index in range(len(up[userid])):
            # 存储某用户点击过的单个物品分类总评分占所有物品分类总评分的比率
            # {用户id:{物品分类:该分类的评分占比}}
            up[userid][index] = (up[userid][index][0], round(up[userid][index][1] / total_score, 3))
    return up

def get_time_score(timestamp):
    '''
    获取时间间隔权值,如果时间间隔越大,该权值越低
    :param timestamp: 用户对物品评分的时间戳
    :return:
    '''
    fix_timestamp = 1476086345
    total_sec = 24 * 60 * 60
    delta = (fix_timestamp - timestamp) / (total_sec * 100)
    return round(1 / (1 + delta), 3)

def recom(cate_item_sort, up, userid, topk=10):
    '''
    推荐结果
    :param cate_item_sort: 物品倒排
    :param up: 用户刻画
    :param userid: 用户id
    :param topk:
    :return:
    '''
    if userid not in up:
        return {}
    recom_result = {}
    if userid not in recom_result:
        recom_result.setdefault(userid, [])
    for combine in up[userid]:
        cate = combine[0]
        ratio = combine[1]
        num = int(topk * ratio) + 1
        if cate not in cate_item_sort:
            continue
        recom_list = cate_item_sort[cate][:num]
        recom_result[userid] += recom_list
    return recom_result

def run_main():
    ave_score = get_ave_score("../data/ratings.txt")
    item_cate, cate_item_sort = get_item_cate(ave_score, "../data/movies.txt")
    up = get_up(item_cate, "../data/ratings.txt")
    print(len(up))
    print(up['1'])
    pprint.pprint(recom(cate_item_sort, up, '1'))

if __name__ == "__main__":

    run_main()

运行结果

100
[('Drama', 0.6), ('Action', 0.4)]
{'1': [('30', 5.0),
       ('149', 5.0),
       ('156', 5.0),
       ('178', 5.0),
       ('279', 5.0),
       ('280', 5.0),
       ('290', 5.0),
       ('611', 5.0),
       ('667', 5.0),
       ('1224', 5.0),
       ('2344', 5.0),
       ('2826', 5.0)]}

根据结果,我们可以看到总共有100条用户刻画,而对于用户1来说,它喜欢的物品类型为Drama、权重为0.6和Action、权重为0.4。我们再来看一下推荐结果,我们将原始文件中的物品信息依次列出来

30,Shanghai Triad (Yao a yao yao dao waipo qiao) (1995),Crime|Drama
149,Amateur (1994),Crime|Drama|Thriller
156,Blue in the Face (1995),Comedy|Drama
178,Love & Human Remains (1993),Comedy|Drama
279,My Family (1995),Drama
280,Murder in the First (1995),Drama|Thriller
290,Once Were Warriors (1994),Crime|Drama
611,Hellraiser: Bloodline (1996),Action|Horror|Sci-Fi
667,Bloodsport 2 (a.k.a. Bloodsport II: The Next Kumite) (1996),Action
1224,Henry V (1989),Action|Drama|Romance|War
2344,Runaway Train (1985),Action|Adventure|Drama|Thriller
2826,"13th Warrior, The (1999)",Action|Adventure|Fantasy

这些物品的类别确实都属于Drama和Action类别。

个性化召回算法总结与评估方法

这个图是工业界多种召回算法并存的推荐系统架构,首先Match是召回,召回完了之后是Rank排序,排序完了之后是Strategy策略调整。然后将结果返回给Web层。

我们来看一下在召回阶段是如何多种算法并存的,比如说这里算法A召回了两个Item a与b,算法B召回了三个Item a、c、d,算法C召回了四个Item e、f、d、c。每一种算法召回的Item数量是如何确定的呢?这里有两种形式:

  1. 为了满足Rank排序阶段的性能要求,这里指定召回阶段召回的数目,比如说是50个。以往的算法根据表现来平分这50个。每一种算法指定一个比例,比如说算法A是0.2,算法B是0.3,算法C是0.5。这样每一个算法也就有了自己召回数目的上限。
  2. 如果Rank阶段毫无性能压力,则在各算法中写了多少推荐,那么就全部召回。在召回完成之后需要进行合并。合并完成之后得到了Item a到f。将重复召回的进行去重,但是我们会标记各个Item是同时属于哪个算法召回的。召回完成之后的Item进入排序阶段。

个性化召回算法的评价

  • 离线评价准入

在我们新增一组个性化召回算法的时候,我们离线选取了一部分训练文件来训练了我们个性化召回算法的模型,我们根据这个模型得到了一些推荐结果。同时我们有必要保留一些测试集,在测试集上评价一下推荐结果的可靠程度。这个可靠程度首先让我们有一个预期,这个算法会给线上带来正向或负向的收益。

  • 在线评价收益

最终结果还是需要到线上生产环境去评价真实的收益。

Offline(离线)评价方法

  • 评测新增算法推荐结果在测试集上的表现。

如果我们新增的某种个性化召回算法对于用户A给出了推荐结果为Item a、b、c。

我们获得了该算法在测试集上的展现数据,对用户推荐的为Item a、b、c、m。我们发现有3个跟训练数据集是重合的,就是a、b、c。那么这三个Item就是分母。

如果我们得到了用户A在测试集上的点击物品,就为Item a和c。那么a、c就是分子,那么点击率就是2/3。如果这个数据是高于基线的点击率的话,那么我们便可以将这种推荐结果放到线上去进行ab测试。当然线下评价的结果和线上真实环境的结果是有差异的,但是这种方式能够给我们一个最基础、直观的评判,是否可以准入到线上。

Online(在线)评价方法

  • 定义指标

我们需要根据不同的场景,比如说在信息流场景下,我们最关心的就是点击率,平均阅读时长等等指标;在电商系统中,我们可能更加关注的是转化率以及总的交易额度。总之我们要根据自己的产品来找到最能够评价产品的核心指标。

  • 生产环境ab测试

这里我们往往采用以划分userid尾号的形式,比如说分出1%的流量在原有的个性化召回体系上增加我们要实验的个性化召回算法。试验几天之后,与基线去比较核心指标的优劣,如果收益是正向的,我们就保留。

学习排序综述

  • 什么是Learn To Rank

排序是在搜索场景以及推荐场景中应用的最为广泛。传统的排序方法是构造相关度函数,使相关度函数对每一个文档进行打分。得分较高的文档,排的位置就靠前。但是随着相关度函数中特征的增多,使调参变的及其困难,所以便将排序这一过程引入了机器学习的概念,也就变成了学习排序是指的对与单独的文档来预估点击率,将预估的点击率最大的文档排到前面。所以特征的选择与模型的训练就变得至关重要。

将个性化召回的物品候选集根据物品本身的属性结合用户的属性,上下文等信息给出展现优先级的过程。   

假设有一个用户A,基于他的历史行为给出了召回,可能是很多种召回算法经过合并之后的得到了6个Item。然后经过排序将这6个Item的优先级固定为c、a、f、d、b、e。这个得到优先级的过程就是由排序得到的,分别根据了Item本身的属性以及用户当前的一些上下文和用户固定的一些属性得到最佳的顺序,以保证点击率最高。

  • 排序在个性化推荐系统中的重要作用

在个性化推荐系统中,后端的主要流程是召回、排序和策略调整。召回决定了推荐效果的天花板,排序就是决定了逼近这个天花板的程度。排序决定了最终的推荐效果。

由于推荐算法后端的主流程是召回、排序、策略调整。策略调整部分是几乎不会改变最终展现给用户的物品的顺序的,用户看到的顺序几乎是由排序这一部分来决定的,如果用户在首屏的前面的位置就能够看到自己想要的物品,用户就会在我们的推荐系统中停留的时间更长。反之,如果需要用户几次刷新之后才能得到自己想要的物品,那么用户下一次必然不会再信任我们的推荐效果。必然在系统的停留时长也不会很长。

在工业界,排序又分为三个步骤:1、PreRank,排序之前的部分。由于排序的模型有浅层模型切换到深层模型的时候,耗时在不停的增加,比如之前召回可以允许有5000个物品去过浅层模型,比如说逻辑回归。我们就是训练出了一组参数,这个整体的打分过程耗时很短。但是当我们的排序模型切换到了深层模型,比如说DNN,整体需要请求一次深度学习的服务,那么这5000个Item去请求的时间显然是我们不能承受的,所以我们先有一个粗排。这个粗排会将这5000个召回的物品进行第一次排序,将候选集缩小到一定范围之内,这样使得排序模型的总处理时间满足系统的性能要求。粗排往往以一些简单的规则为主,比如说使用后验CTR(点击通过率),或者说对于新的物品,在入库时的预估CTR等等。2、Rank,主排序部分。现在业界比较流行的还有一次重排ReRank,这个重排是将主排序结果再放入一个强化学习的模型里面去进行一个重排序,这种主要突出的用户最近几次行为的一些特征,将与最近几次用户行为相近的Item给优先的展示。以便获取用户行为的延续性。由於单一Item在重排模型的耗时要比主模型的耗时要长很多,所以重排部分只是会影响主排序产生的头部的一些结果,比如说Top 50的结果进行一个重排。最能影响结果的还是主排序模型。

Rank解析

单一的浅层模型,在学习排序初期是非常受欢迎的,因为模型线上处理时间较短,所以它支持特征的维度就会非常的高。但是也存在很多的问题,比如像逻辑回归模型,它需要研发者具有很强的样本筛选以及特征处理能力。这里包含了像特征的归一化,离散化,特征的组合等等。所以后期发展到了浅层模型的组合,比如像树模型gbdt,包括LR与gbdt的组合,这一类模型不需要特征的归一化,离散化,能够较强的发现特征之间的规律。所以相较於单一的浅层模型,是有一定的优势的。随着深度学习在工业界应用的不断成熟,以及像tensorflow深度学习框架的开源,现在大部分工业界的主排序模型都已经切换到了深度学习模型。

  • 工业界推荐系统中排序架构解析

算法后端的主流程是召回之后排序,召回完了将Item集合传给排序部分,排序部分会调用打分框架,得到每一个Item在当前的上下文下对当前用户的一个得分。进而我们根据得分决定展现顺序。在打分框架内部,首先会让每一个Item以及用户去提取特征,这里提取的特征要与离线训练的模型特征保持一致。提取完特征之后向排序服务发出请求,排序服务会返回给我们一个得分,推荐引擎基于此得分完成排序。经过简单的策略调整之后,展现给用户。这里需要特别注意的是排序服务与离线训练好的排序模型之间的通信。如果是单一的浅层模型,像LR,我们可以直接将训练好的模型参数存入内存,当排序服务需要对外提供服务的时候,直接加载内存中的模型参数即可。像FM,gbdt等等,我们只需要离线训练好模型,将模型实例化到硬盘当中,在在线服务当中,由于这些模型都有相应的库函数,它们提供模型的加载和模型对外预测等一系列的接口,我们便可以完成打分。但是对于像深度学习模型,我们在训练完了模型之后,还需要提供一个深度学习的服务供排序服务调用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章