推荐算法初探

1. 推荐算法简介

0x1:关于推荐的几个小故事

在开始讨论抽象具体的算法和公式之前,笔者希望先通过几个小故事,来帮助读者朋友建立一个对推荐算法的感性理解。同时我们也可以更好地体会到在现实复杂世界中,推荐是一项非常复杂的事情,现在的最新的推荐算法可能只模拟了其中30%不到的程度。

1. 150年前美国西部小镇的生活情形

假想150年前一个美国小镇的生活情形,大家都互相认识:

百货店某天进了一批布料,店员注意到这批布料中某个特定毛边的样式很可能会引起Clancey夫人的高度兴趣,因为他知道Clancey夫人喜欢亮花纹样,于是他在心里记着等Clancey夫人下次光顾时将该布料拿给她看看。

Chow Winkler告诉酒吧老板Wilson先生,他考虑将多余的雷明顿(Renmington)来福枪出售,Wilson先生将这则消息告诉Bud Barclay,因为他知道Bud正在寻求一把好枪。

Valquez警长及其下属知道Lee Pye是需要重点留意的对象,因为Lee Pye喜欢喝酒,并且性格暴躁、身体强壮

100年前的小镇生活都与人和人之间的联系有关。人们知道你的喜好、健康和婚姻状况。不管是好是坏,大家得到的都是个性化的体验。那时,这种高度个性化的社区生活占据了当时世界上的大部分角落。

请注意,这里每个人都对其他每个人的个性和近期情况非常了解,因此可以很精准地推荐和每个人的个性和近期需求最匹配的物品,这是一种面向人本身和物理关联度的关联度推荐策略

2. 一次愉快的公司团队Outing

笔者公司每年都会提供员工1次免费带薪outing的机会,今年8月,我们团队决定去从未去过的缅甸出游,我们购买了机票和预定酒店,愉快地出发了。

当天晚上到了曼德勒,我们决定去吃一个当地网红的本地菜餐馆,到了餐馆,服务员上来点菜,因为从未来过缅甸,菜单呢,也是一堆的缅甸语看不懂,因此我们让服务员给你们推荐一些菜,这个时候问题来了,服务员该如何给我们推荐呢?

我们来列举一下服务员要考虑的所有因素:

  • 顾客是7个人,考虑到7人都是年轻大男生,推荐的菜量要足够,不然不够吃,但也不能太多,不然吃不完肯定会造成客户体验不好
  • 顾客看外貌是中国来的,还一脸兴奋的表情,看起来是第一次来缅甸,那最好推荐一些最优特色的当地菜
  • 顾客7人都是男生,除了采之外,最好再推荐一些当地的啤酒
  • 在可能的情况下,尽量推荐一些贵的菜,增加餐馆营收
  • 顾客中有一个人提出不太能吃辣,推荐的菜中,将30%的菜换成不那么辣的菜

请注意这个场景中,服务员对这批顾客是没有任何背景知识了解的,既不是熟人,也不是熟客。他只能依靠有限的信息,结合自己的经验来给出一个主观推荐。换句话说,这是一个冷启动问题

3. 啤酒与尿布

Tom是一家超市的老板,近日,为了进一步提升超市的销售额,他学习了一些统计学知识,打算利用统计学知识对近半年的销售流水进行分析。

经过了一系列的数据清洗、统计聚类、关联分析后,Tom发现,有一些商品,总是成对地出现在receipt上,例如:

  • 啤酒、尿布
  • 牛奶、全麦面包
  • 棉手套、棉头套
  • 拿铁咖啡、每日日报
  • ....

老板决定,以后在顾客结账的时候,会根据顾客的已购商品,选择性地推荐一些“常见搭配”给顾客,例如某个顾客买了啤酒,就选择性地问问他是否还需要买尿布,如果某个顾客买了牛奶,就问问今日的报纸要不要买一份看看呀,如此等等。通过这个举措,Tom发现,超市的营收上升了40%。

请注意这个场景中,超市老板对顾客的购买推荐是基于历史上其他顾客的购买记录,通过统计关联提取得到的一种关联信息,我们称之为基于关联规则推荐策略

4. 书商的智慧

我们去逛书店的时候,常常会发现,书店会把同一类型的书放在一起售卖,例如:

  • 路遥的《人生》、路遥的《平凡的世界》
  • 阿西莫夫的《银河帝国:基地7部曲》、刘慈欣的《三体》

这么做的原因在于,每本书尽管风格各不相同,但总体上可以按照一定的属性维度进行分类,例如:

  • 青春
    • 校园
    • 爱情
    • 叛逆
    • 网络
    • 爆笑 
  • 小说作品集
    • 世界名著
    • 外国小说
    • 中国古典小说
    • 武侠小说 

请注意这个场景中,书商对书进行聚类关联,将相似的畅销书放在一起捆绑销售,是基于下面这条假设,喜欢某类书的顾客,也有同样喜欢同属于该类的其他书。这是一种内容关联推荐策略

5. 市场调研的智慧

Lily准备在大学城周围开一家奶茶店,摆在眼前的第一件事就是要搞清楚,这一片区域中的大学生都喜欢哪些类型的奶茶。

为了解决这个问题,Lily先建立了一个基本假设:物以类聚人以群分,同一个专业/班级内的学生的喜好是彼此接近的,同时也是会随着相处的时间逐渐靠拢的,因此,只要从每个专业中随机抽取1-2名学生,进行市场调研,总会汇总调研结果,就能基本得到该区域内大学生的奶茶喜好了。

在这个例子中,奶茶店老板的推荐策略是,对未知的客户的推荐,可以先寻找到与该客户最相似的“同类客户群”,然后用这个同类客户群的喜好来给新的顾客进行推荐。这是一种用户关联推荐策略

0x2:群体智慧的推荐算法

推荐算法本质上提取的是大规模数据集中的相关性统计信息,因此更适合大数据场景。

Relevant Link:     

https://lianhaimiao.github.io/2018/01/06/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%B3%95%E5%B0%8F%E7%BB%93-%E4%B8%8A/ 
https://www.bobinsun.cn/ai/2019/04/17/Recommender-system/ 
https://www.zhihu.com/question/19971859
http://www.guidetodatamining.com/

 

2. 协同过滤推荐算法

0x1:什么是协同过滤思想

所谓协同过滤,就是利用群体的行为来给每个单个个体做综合决策(推荐),属于群体智慧编程的范畴。 

对于推荐系统来说,通过用户的持续协同作用,最终给用户的推荐会越来越准。 

具体来说,协同过滤的思路是通过群体的行为来找到某种相似性(用户之间的相似性或者标的物之间的相似性),通过该相似性来为用户做决策和推荐

现实生活中有很多协同过滤的案例及思想体现,例如人类喜欢追求相亲中的“门当户对”,其实也是一种协同过滤思想的反射,门当户对实际上是建立了相亲男女的一种“相似度”(家庭背景、出身、生活习惯、为人处世、消费观、甚至价值观可能会相似),给自己找一个门当户对的伴侣就是一种“过滤”,当双方”门当户对“时,各方面的习惯及价值观会更相似,未来幸福的概率也会更大。 如果整个社会具备这样的传统和风气,通过”协同进化“作用,大家会越来越认同这种方式。 

抽象地说,协同过滤利用了两个非常朴素的自然哲学思想: 

  • “群体的智慧”:群体的智慧的底层原理是统计学规律,是一种朝向平衡稳定态发展的动态过程。
  • “相似的物体具备相似的性质”:这个道理在物理学和化学中体现的非常明显,越相似的物体在物理结构和化学结构上就越相似。这个道理对于抽象的虚拟数据世界也是成立的。

笔者思考

将协同过滤和关联规则挖掘进行互相对比,我们会发现,itemCF是频繁项挖掘FPGrowth的一种通用表示,频繁项挖掘的本质上挖掘的也是一种相似性统计模式。下面我们来类比一下它们二者思想的共通点:

  • itemCF:历史评价行为矩阵的列向量视角,通过度量每个record中不同列之间的距离,来度量item之前的相似度
  • FPGrowth:本质也是列向量视角,但FPGrowth可以看成是是简化版的itemCF,在FPGrowth中,不同列之间的距离度量简化为0/1两种状态(是否出现),FPGrowth中所谓的频繁共现集,本质上是寻找一个距离相近的列向量集合

0x2:协同过滤的矩阵统一视角

尽管协同过滤大体上可以分为userCF和itemCF,但其实如果我们用二维矩阵来进行抽象,可以将它们二者看做同一个框架下两种不同算法形式。

以一个百货店的购买历史记录为例,行为用户id,列为该用户对每个商品的购买评价分,

我们可以看到,

  • 行视角代表了每个用户对所有商品的评分情况,可以理解为对每个用户进行了一个特征向量化,每个属性对应了一个商品
  • 列视角代表了每个商品被所有用户的评分情况,可以理解为对每个商品进行了一个特征向量化,每个属性对应一个用户
  • 矩阵中存在缺失值,所以这是一个稀疏矩阵

可以看到,所谓的“user-based collaborative filtering”和“item-based collaborative filtering”本质上可以理解为对用户历史购买行为的二维矩阵,分别进行行向量和列向量的相似度关联挖掘。

0x3:user-based collaborative filtering - 基于用户的推荐:爱你所爱

1. 算法主要思想

基于用户对物品的喜好找到相似邻居用户,然后将邻居用户喜欢的物品推荐给目标用户,即所谓的爱你所爱。

上图中,用户A喜欢物品A和物品C,用户C也喜欢物品A、物品C,同时还喜欢物品D。

从这些用户的历史喜好信息中,我们可以发现用户A和用户C的口味和偏好是比较类似的,同时用户C还喜欢物品D,那么我们可以推断用户A可能也喜欢物品D,因此可以将物品D推荐给用户A。

2. 算法简要实现思路

将一个用户对所有物品的打分(行视角)作为一个向量(Vector),计算用户之间的相似度,找到top K-近邻邻居后,根据所有邻居的相似度权重以及他们对物品的评分,为目标用户生成一个排序的物品列表作为推荐列表。

3. 算法实现示例

import codecs 
from math import sqrt

users = {"Angelica": {"Blues Traveler": 3.5, "Broken Bells": 2.0,
                      "Norah Jones": 4.5, "Phoenix": 5.0,
                      "Slightly Stoopid": 1.5,
                      "The Strokes": 2.5, "Vampire Weekend": 2.0},
         
         "Bill":{"Blues Traveler": 2.0, "Broken Bells": 3.5,
                 "Deadmau5": 4.0, "Phoenix": 2.0,
                 "Slightly Stoopid": 3.5, "Vampire Weekend": 3.0},
         
         "Chan": {"Blues Traveler": 5.0, "Broken Bells": 1.0,
                  "Deadmau5": 1.0, "Norah Jones": 3.0, "Phoenix": 5,
                  "Slightly Stoopid": 1.0},
         
         "Dan": {"Blues Traveler": 3.0, "Broken Bells": 4.0,
                 "Deadmau5": 4.5, "Phoenix": 3.0,
                 "Slightly Stoopid": 4.5, "The Strokes": 4.0,
                 "Vampire Weekend": 2.0},
         
         "Hailey": {"Broken Bells": 4.0, "Deadmau5": 1.0,
                    "Norah Jones": 4.0, "The Strokes": 4.0,
                    "Vampire Weekend": 1.0},
         
         "Jordyn":  {"Broken Bells": 4.5, "Deadmau5": 4.0,
                     "Norah Jones": 5.0, "Phoenix": 5.0,
                     "Slightly Stoopid": 4.5, "The Strokes": 4.0,
                     "Vampire Weekend": 4.0},
         
         "Sam": {"Blues Traveler": 5.0, "Broken Bells": 2.0,
                 "Norah Jones": 3.0, "Phoenix": 5.0,
                 "Slightly Stoopid": 4.0, "The Strokes": 5.0},
         
         "Veronica": {"Blues Traveler": 3.0, "Norah Jones": 5.0,
                      "Phoenix": 4.0, "Slightly Stoopid": 2.5,
                      "The Strokes": 3.0}
        }



class recommender:

    def __init__(self, data, k=1, metric='pearson', n=5):
        """ initialize recommender
        currently, if data is dictionary the recommender is initialized
        to it.
        For all other data types of data, no initialization occurs
        k is the k value for k nearest neighbor
        metric is which distance formula to use
        n is the maximum number of recommendations to make"""
        self.k = k
        self.n = n
        self.username2id = {}
        self.userid2name = {}
        self.productid2name = {}
        # for some reason I want to save the name of the metric
        self.metric = metric
        if self.metric == 'pearson':
            self.fn = self.pearson
        #
        # if data is dictionary set recommender data to it
        #
        if type(data).__name__ == 'dict':
            self.data = data

    def convertProductID2name(self, id):
        """Given product id number return product name"""
        if id in self.productid2name:
            return self.productid2name[id]
        else:
            return id


    def userRatings(self, id, n):
        """Return n top ratings for user with id"""
        print ("Ratings for " + self.userid2name[id])
        ratings = self.data[id]
        print(len(ratings))
        ratings = list(ratings.items())
        ratings = [(self.convertProductID2name(k), v)
                   for (k, v) in ratings]
        # finally sort and return
        ratings.sort(key=lambda artistTuple: artistTuple[1],
                     reverse = True)
        ratings = ratings[:n]
        for rating in ratings:
            print("%s\t%i" % (rating[0], rating[1]))
        

        

    def loadBookDB(self, path=''):
        """loads the BX book dataset. Path is where the BX files are
        located"""
        self.data = {}
        i = 0
        #
        # First load book ratings into self.data
        #
        f = codecs.open(path + "BX-Book-Ratings.csv", 'r', 'utf8')
        for line in f:
            i += 1
            #separate line into fields
            fields = line.split(';')
            user = fields[0].strip('"')
            book = fields[1].strip('"')
            rating = int(fields[2].strip().strip('"'))
            if user in self.data:
                currentRatings = self.data[user]
            else:
                currentRatings = {}
            currentRatings[book] = rating
            self.data[user] = currentRatings
        f.close()
        #
        # Now load books into self.productid2name
        # Books contains isbn, title, and author among other fields
        #
        f = codecs.open(path + "BX-Books.csv", 'r', 'utf8')
        for line in f:
            i += 1
            #separate line into fields
            fields = line.split(';')
            isbn = fields[0].strip('"')
            title = fields[1].strip('"')
            author = fields[2].strip().strip('"')
            title = title + ' by ' + author
            self.productid2name[isbn] = title
        f.close()
        #
        #  Now load user info into both self.userid2name and
        #  self.username2id
        #
        f = codecs.open(path + "BX-Users.csv", 'r', 'utf8')
        for line in f:
            i += 1
            #print(line)
            #separate line into fields
            fields = line.split(';')
            userid = fields[0].strip('"')
            location = fields[1].strip('"')
            if len(fields) > 3:
                age = fields[2].strip().strip('"')
            else:
                age = 'NULL'
            if age != 'NULL':
                value = location + '  (age: ' + age + ')'
            else:
                value = location
            self.userid2name[userid] = value
            self.username2id[location] = userid
        f.close()
        print(i)
                
        
    def pearson(self, rating1, rating2):
        sum_xy = 0
        sum_x = 0
        sum_y = 0
        sum_x2 = 0
        sum_y2 = 0
        n = 0
        for key in rating1:
            if key in rating2:
                n += 1
                x = rating1[key]
                y = rating2[key]
                sum_xy += x * y
                sum_x += x
                sum_y += y
                sum_x2 += pow(x, 2)
                sum_y2 += pow(y, 2)
        if n == 0:
            return 0
        # now compute denominator
        denominator = (sqrt(sum_x2 - pow(sum_x, 2) / n)
                       * sqrt(sum_y2 - pow(sum_y, 2) / n))
        if denominator == 0:
            return 0
        else:
            return (sum_xy - (sum_x * sum_y) / n) / denominator


    def computeNearestNeighbor(self, username):
        """creates a sorted list of users based on their distance to
        username"""
        distances = []
        for instance in self.data:
            if instance != username:
                distance = self.fn(self.data[username],
                                   self.data[instance])
                distances.append((instance, distance))
        # sort based on distance -- closest first
        distances.sort(key=lambda artistTuple: artistTuple[1],
                       reverse=True)
        return distances

    def recommend(self, user):
       """Give list of recommendations"""
       recommendations = {}
       # first get list of users  ordered by nearness
       nearest = self.computeNearestNeighbor(user)
       #
       # now get the ratings for the user
       #
       userRatings = self.data[user]
       #
       # determine the total distance
       totalDistance = 0.0
       for i in range(self.k):
          totalDistance += nearest[i][1]
       # now iterate through the k nearest neighbors
       # accumulating their ratings
       for i in range(self.k):
          # compute slice of pie 
          weight = nearest[i][1] / totalDistance
          # get the name of the person
          name = nearest[i][0]
          # get the ratings for this person
          neighborRatings = self.data[name]
          # get the name of the person
          # now find bands neighbor rated that user didn't
          for artist in neighborRatings:
             if not artist in userRatings:
                if artist not in recommendations:
                   recommendations[artist] = (neighborRatings[artist]
                                              * weight)
                else:
                   recommendations[artist] = (recommendations[artist]
                                              + neighborRatings[artist]
                                              * weight)
       # now make list from dictionary
       recommendations = list(recommendations.items())
       recommendations = [(self.convertProductID2name(k), v)
                          for (k, v) in recommendations]
       # finally sort and return
       recommendations.sort(key=lambda artistTuple: artistTuple[1],
                            reverse = True)
       # Return the first n items
       return recommendations[:self.n]


if __name__ == "__main__":
     r = recommender(users)

     print r.recommend('Jordyn')
     print r.recommend('Hailey')

0x4:item-based collaborative filtering - 基于项目的推荐:听群众的,没错!

1. 算法主要思想 

基于用户对物品的喜好找到相似的物品,然后根据用户的历史喜好,推荐相似的物品给目标用户。

上图中,物品A被用户A/B/C喜欢,物品C被用户A/B喜欢。

从这些用户的历史喜好可以分析出物品A和物品C是比较类似的,喜欢物品A的人都喜欢物品C,基于这个数据可以推断用户C很有可能也喜欢物品C,所以系统会将物品C推荐给用户C。

2. 算法简要实现思路

将所有用户对某一个物品的喜好作为一个向量来计算物品之间的相似度,得到物品的相似物品后,根据用户历史的喜好预测目标用户还没有涉及的物品,计算得到一个排序的物品列表作为推荐。 

0x5:相似度计算 - Similarity Metrics Computing

这里讨论的相似度计算方法不仅用于协同过滤推荐算法,对文章之后讨论的其他算法(例如内容过滤推荐)也同样有用,但是为了行文的连贯性,笔者将这部分内容放置在这里。

首先需要明白的是,不管是基于协同过滤,还是内容过滤,我们的计算对象本质上都是向量(Vector),相似度计算算法是一个辅助工具算法,目标是度量不同向量之间的相关联程度。

下面介绍三种主流的相似度计算策略,它们是两种非常不同的思路,但同时也有一些内在的底层联系,

1. 基于向量距离的相似度计算 - 明氏距离(Minkowski Distance)

基于向量距离的相似度计算算法思路非常简单明了,通过计算两个向量的数值距离,距离越近相似度越大。

一般地,我们用明氏距离(Minkowski Distance)来度量两个向量间的距离,

其中,

  • r=1时,上式就是曼哈顿距离
  • r=2时,上式就是欧氏距离,欧几里德距离就是平面上两个点的距离
  • r=∞时,上式就是上确界距离(supermum distance)

值得注意的是,欧几里德距离计算相似度是所有相似度计算里面最简单、最易理解的方法。它以经过人们一致评价的物品为坐标轴,然后将参与评价的人绘制到坐标系上,并计算他们彼此之间的直线距离。

2. 基于余弦相似度的相似度计算 - Cosine 相似度(Cosine Similarity) 

在余弦相似度公式中,向量的等比例放缩是不影响最终公式结果的,余弦相似度公式比较的是不同向量之间的夹角。公式如下,

相比距离度量,余弦相似度更加注重两个向量在方向上的差异,而非距离或长度上。

从图上可以看出

  • 距离度量衡量的是空间各点间的绝对距离,跟各个点所在的位置坐标(即个体特征维度的数值)直接相关;
  • 而余弦相似度衡量的是空间向量的夹角,更加的是体现在方向上的差异,而不是位置。如果保持A点的位置不变,B点朝原方向远离坐标轴原点,那么这个时候余弦相似度cosθ是保持不变的,因为夹角不变,而A、B两点的距离显然在发生改变,这就是欧氏距离和余弦相似度的不同之处

根据欧氏距离和余弦相似度各自的计算方式和衡量特征,分别适用于不同的数据分析模型:

  • 欧氏距离能够体现个体数值特征的绝对差异,所以更多的用于需要从维度的数值大小中体现差异的分析,如使用用户行为指标分析用户价值的相似度或差异
  • 而余弦相似度更多的是从方向上区分差异,而对绝对的数值不敏感,更多的用于使用用户对内容评分来区分用户兴趣的相似度和差异,同时修正了用户间可能存在的度量标准不统一的问题(因为余弦相似度对绝对数值不敏感)

3. 基于相关性的相似度计算 - 皮尔森相关系数(Pearson Correlation Coefficient)

皮尔森相关系数一般用于计算两个变量间联系的紧密程度(相关度),它的取值在 [-1,+1] 之间。

从公式可以看到,皮尔逊系数就是相对两个向量都先进行中心化(centered)后再计算余弦相似度。

中心化的意思是,对每个向量,先计算所有元素的平均值avg,然后向量中每个维度的值都减去这个avg,得到的这个向量叫做被中心化的向量。机器学习,数据挖掘要计算向量余弦相似度的时候,由于向量经常在某个维度上有数据的缺失,所以预处理阶段都要对所有维度的数值进行中心化处理。

从统一理论的角度来说,皮尔逊相关系数是余弦相似度在维度值缺失情况下的一种改进

更进一步的,我们从数理统计中的协方差的角度来理解一下皮尔森相关系数。

协方差(Covariance)是一个反映两个随机变量相关程度的指标,如果一个变量跟随着另一个变量同时变大或者变小,那么这两个变量的协方差就是正值,反之相反,公式如下:

而Pearson相关系数公式如下:

由公式可知,Pearson相关系数是用协方差除以两个变量的标准差得到的,虽然协方差能反映两个随机变量的相关程度(协方差大于0的时候表示两者正相关,小于0的时候表示两者负相关),但是协方差值的大小并不能很好地度量两个随机变量的关联程度。

例如,现在二维空间中分布着一些数据,我们想知道数据点坐标X轴和Y轴的相关程度。如果X与Y的数据分布的比较离散,这样会导致求出的协方差值较大,用这个值来度量相关程度是不合理的,如下图:

为了更好的度量两个随机变量的相关程度,Pearson相关系数在协方差的基础上除以了两个随机变量的标准差,这样就综合了因为随机变量自身的方差增加导致的协方差增加。

容易得出,pearson是一个介于-1和1之间的值,

  • 当两个变量的线性关系增强时,相关系数趋于1或-1
    • 当一个变量增大,另一个变量也增大时,表明它们之间是正相关的,相关系数大于0
    • 如果一个变量增大,另一个变量却减小,表明它们之间是负相关的,相关系数小于0
  • 如果相关系数等于0,表明它们之间不存在线性相关关系

4. 三种相似度计算算法的区别   

上述三种相似度计算的区别在于,定义“什么叫相似”的标准不同,其实也可以理解为在量纲是否一致的不同环境下,采取的不同相似度评价策略,

  • 量纲一致环境:不同用户对好坏事物的评价是在均值区间内的,举个例子,在一个成熟的观影市场中,观众对《复联》系列的评价总体在9.5~9.8分之间,而对《无极》的评价总体在5~6分之间,这种影评环境就叫量纲一致环境,指的是市场中每个评价个体(用户)都是一个客观实体。
  • 量纲不一致环境:还是举一个例子说明,在某小镇中,居民对电影的普遍接受度较低,因此他们对所有电影的评价都普遍降低,而不管该电影实际的好坏,在这种情况下,该小镇居民的评价数据和大城市中主流观影市场的评价数据之间,就存在量纲不一致问题。 

Relevant Link:   

http://www.guidetodatamining.com/
https://www.cnblogs.com/charlesblc/p/8336765.html
https://my.oschina.net/dillan/blog/164263
http://www.woshipm.com/pd/2384774.html
https://www.jishuwen.com/d/2h2A 
https://www.zhihu.com/question/19971859

 

3. 内容过滤推荐算法(Content-Based Recommendations)

0x1:算法主要思想介绍

所谓基于内容的推荐算法(Content-Based Recommendations)是基于标的物相关信息(通过特征工程方式得到特征向量)来构建推荐算法模型,为用户提供推荐服务。

这里的标的物相关信息可以是对标的物文字描述的metadata信息、标签、用户评论、人工标注的信息等。更一般地说,我们可以基于特征工程技术,对标的物进行特征提取,得到降维后的特征向量后,使用聚类和近似相似性搜索等机器学习手段进行关联性推荐。这其实就是传统机器学习中的“feature engineering+machine learning”的流程。

广义的标的物相关信息不限于文本信息,图片、语音、视频等都可以作为内容推荐的信息来源,只不过这类信息处理成本较大,处理的时间及存储成本也相对更高。

苹果、香蕉;和樱桃西瓜都是水果,物品本身在特征维度上有相似度

从上图中可以看出,内容过滤推荐算法的推荐策略和推荐效果,很大程度取决于对标的物的特征工程方案。现在主流的特征提取策略有两类,分别是:

0x2:基于内容的推荐算法的优势与缺点

1. 优点

  • 符合用户的需求爱好:该算法完全基于用户的历史兴趣来为用户推荐,推荐的标的物也是跟用户历史兴趣相似的,所以推荐的内容一定是符合用户的口味的。
  • 直观易懂,可解释性强:基于内容的推荐算法基于用户的兴趣为用户推荐跟他兴趣相似的标的物,原理简单,容易理解。同时,由于是基于用户历史兴趣推荐跟兴趣相似的标的物,用户也非常容易接受和认可。
  • 解决冷启动问题:所谓冷启动问题是指该用户只有很少的历史购买行为,或其购买的商品是一个很少被其他用户购买的商品,这种情况会影响协同过滤的效果。但是基于内容推荐没有这个问题,只要用户有一个操作行为,就可以基于内容为用户做推荐,不依赖其他用户行为。同时对于新入库的标的物,只要它具备metadata信息等标的物相关信息,就可以利用基于内容的推荐算法将它分发出去。因此,对于强依赖于UGC内容的产品(如抖音、快手等),基于内容的推荐可以更好地对标的物提供方进行流量扶持。
  • 算法实现相对简单:基于内容的推荐可以基于标签维度做推荐,也可以将标的物嵌入向量空间中,利用相似度做推荐,不管哪种方式,算法实现较简单,有现成的开源的算法库供开发者使用,非常容易落地到真实的业务场景中。
  • 对于小众领域也能有比较好的推荐效果(本质就是冷启动问题):对于冷门小众的标的物,用户行为少,协同过滤等方法很难将这类内容分发出去,而基于内容的算法受到这种情况的影响相对较小。
  • 非常适合标的物快速增长的有时效性要求的产品(本质就是冷启动问题):对于标的物增长很快的产品,如今日头条等新闻资讯类APP,基本每天都有几十万甚至更多的标的物入库,另外标的物时效性也很强。新标的物一般用户行为少,协同过滤等算法很难将这些大量实时产生的新标的物推荐出去,这时就可以采用基于内容的推荐算法更好地分发这些内容。

2. 缺点 

  • 推荐范围狭窄,新颖性不强:由于该类算法只依赖于单个用户的行为为用户做推荐,推荐的结果会聚集在用户过去感兴趣的标的物类别上,如果用户不主动关注其他类型的标的物,很难为用户推荐多样性的结果,也无法挖掘用户深层次的潜在兴趣。特别是对于新用户,只有少量的行为,为用户推荐的标的物较单一。
  • 需要知道相关的内容信息且处理起来较难(依赖特征工程):内容信息主要是文本、视频、音频,处理起来费力,相对难度较大,依赖领域知识。同时这些信息更容易有更大概率含有噪音,增加了处理难度。另外,对内容理解的全面性、完整性及准确性会影响推荐的效果。
  • 较难将长尾标的物分发出去:基于内容的推荐需要用户对标的物有操作行为,长尾标的物一般操作行为非常少,只有很少用户操作,甚至没有用户操作。由于基于内容的推荐只利用单个用户行为做推荐,所以更难将它分发给更多的用户。
  • 推荐精准度不太高:一个很简单的道理是,你花9000多买了一个iphone后,不太可能短期内再买一个iphone或其他手机,相反更有可能需要买一个手机壳。

Relevant Link:  

https://zhuanlan.zhihu.com/p/72860695  

 

4. 混合推荐算法

目前研究和应用最多的是内容推荐和协同过滤推荐的组合。最简单的做法就是分别用基于内容的方法和协同过滤推荐方法去产生一个推荐预测结果,然后用线性组合、加权组合等方式综合其结果。本章我们分别讨论。

0x1:加权式:加权多种推荐技术结果

0x2:切换式:根据问题背景和实际情况或要求决定变换采用不同的推荐技术

0x3:混杂式:同时采用多种推荐技术给出多种推荐结果为用户提供参考

0x4:特征组合:组合来自不同推荐数据源的特征被另一种推荐算法所采用

0x5:层叠式:先用一种推荐技术产生一种粗糙的推荐结果,第二种推荐技术在此推荐结果的基础上进一步作出更精确的推荐 

0x6:特征补充:一种技术产生附加的特征信息嵌入到另一种推荐技术的特征输入中

0x7:级联式:用一种推荐方法产生的模型作为另一种推荐方法的输入

Relevant Link:  

https://core.ac.uk/download/pdf/41441354.pdf 
https://lianhaimiao.github.io/2018/01/06/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%B3%95%E5%B0%8F%E7%BB%93-%E4%B8%8A/ 

  

5. 从推荐算法中得到的思考

在调研和学习推荐算法的相关资料的时候,笔者脑子里萌生了一个想法,网络安全领域中,对未来可能发生的攻击行为是否可以做到提前预测呢?

形成这种想法的基本假设是这样的:

每次攻击入侵事件都可以分成如下几个动作

  • “前序动作”:所谓的前序动作,一般指遭受到一些端口扫描、暴力破解、漏洞存在性和试探性poc扫描等行为
  • “IOC动作”:即下载/启动恶意程序
  • “后序动作”:对外蠕虫攻击、持久化等操作

如果我们收集所有历史上曾经遭受过入侵的系统日志,将其组织成一个“machine-behavior”二维矩阵。通过itemCF挖掘会发现,“前序动作”,“IOC动作”,“后序动作”这三类行为日志在所有日志中是彼此接近的,换句话说,它们是彼此之间满足推荐条件的,当一个机器上发生了“前序动作”,“IOC动作”,或“后序动作”中的任何一个时候,就极有可能发生其他的后续异常事件。

基于这个想法,我们就可以得到一个“未来风险预测器”,当一台主机已经发生了一些“前序动作”的时候,我们可以根据itemCF的推荐结果,预测该机器未来可能还会遭受到哪些恶意事件。

具体来说,我们需要构建一个“machine-behavior”二维矩阵,行向量为每个机器在某个时间段内的每一条进程日志,列向量为特定进程出现的次数。

检测的思路是,如果入侵机器在某时间段内出现了“wget/curl/su/kill”等指令,我们就可以基于itemCF进行入侵预测,例如当出现一部分进程特征的时候,根据itemCF的推荐结果,预测该机器在未来可能发生的后续恶意行为,提前预警。

 

posted @ 2019-10-11 20:55 郑瀚Andrew.Hann 阅读(...) 评论(...) 编辑 收藏