用Python实现一个简单的基于内容的推荐引擎,python实现引擎,未经许可,禁止转载!英文


本文由 编橙之家 - yaoyujia 翻译,BlueOrange 校稿。未经许可,禁止转载!
英文出处:Chris Clark。欢迎加入翻译组。

让我们假设你想为一个电子商务网站搭建一个推荐系统。

基本上你可以采用两种方法:基于内容的算法和协同过滤算法。我们将分别描述两种算法的优点和缺点,然后进一步深入,讲解一个基于内容的推荐引擎的一个简单的实现(可以直接部署在Heroku上,Heroku是一个支持多种编程语言的云平台)

我们在Grove的生产环境里使用了一个几乎一样一模的推荐引擎,你可以先去那里体验一下推荐的结果。

基于内容的推荐引擎是怎么工作的

基于内容的推荐系统,正如你的朋友和同事预期的那样,会考虑商品的实际属性,比如商品描述,商品名,价格等等。如果你以前从没接触过推荐系统,然后现在有人拿枪指着你的头,强迫你在三十秒之内描述出来,你可能会描述这样一个基于内容的系统:呃,呃,我可能会给你看一大堆来自同一个厂家,并且拥有类似的说明的产品。

你正在利用商品本身的属性来推荐类似的商品。这样做非常合理,因为这就是我们在真实世界中买东西的方式。我们去卖烤箱的那一排货架,然后看这些烤箱,它们可能根据不同的品牌,价格,或着能在30分钟之内烤熟一只完整的火鸡,等等特点在货架上摆放。

基于内容的推荐系统的盲区

绝大多数的电商网站已经能够让人们很便利地浏览烤箱这个分类了。然而我们真正想要的是那种能驱动销售增量(比如原本不会发生的销售)的推荐系统。如果一个客户正在兰登书屋上看《哈利波特与密室》的商品详情页面,然后系统推荐了《哈利波特与阿兹卡班囚徒》,客户也买了它,总部的那些数据科学家们可不会为此欢呼雀跃。因为用户极有可能已经知道这部系列小说有超过两本书,并且已经买了《哈利波特与阿兹卡班囚徒》。这种就不是销售增量。

协同过滤推荐系统是怎么工作的

我们需要另外一种方法,协同过滤或简称CF。CF的大致思路也是非常直观的,如果很多和你相似的人买了一件商品,那么你也很有可能买。当然,协同过滤也会有类似上述哈利波特的情况。但在同一商品目录下做更深层次的推荐时表现很好。它更强大的之处是可以处理错别字(即使你输入哈利坡特,仍然会推荐哈利波特),而且在实际使用中,从销售增量的角度看,协同过滤也大大优于单一的基于内容的推荐系统。。

尽管基于协同过滤背后的大致思路是很容易理解的,但是有一个方面,你要向你的同事解释很多很多次。纯粹的CF推荐系统没有关于它们推荐的商品的任何知识。对于系统来说,它只是一个关于产品ID和用户ID的巨大的矩阵,代表某个人买了什么东西。有一个非常非常违反直觉的事,那就是CF算法与基于内容的推荐算法混合使用时,没有很明显的性能改进。很明显,了解一些你推荐的商品肯定是有帮助的,对吗?

不,不是的。

在大多数情况下,基本上所有的‘信号’都是从一个简单的谁买了什么的矩阵中得到的。所以,究竟是为什么你要使用基于内容的方法呢?

基于内容的方法什么时候有意义?

有时候CF算法并不是一个切实可行的选择;假设我们想向一位正在查看商品详情的顾客推荐商品,他可能是通过点击Google搜索结果里的链接跳转过来的。我们对这个顾客一无所知,所以我们无法创建一个关于这个顾客购买的矩阵。但是我们可以使用基于内容的系统来推荐类似的商品。从这个意义上来说,基于内容的推荐系统图可以解决CF推荐系统所具有的冷启动问题。

当你对特定商品具有很强烈的购买意图时(像用Google搜索那个商品的相关信息然后跳转过去),它还可以提供一个能做自动内容策展。如果你对Nike Pro Hypercool系列的男士紧身衬衣感兴趣,可能你也会喜欢Nike Pro Hypercool系列的印花男士紧身衣。一个基于内容的推荐引擎就是像这样推荐相关的物品的,没有大量的人工内容策展(这些产品既不出现在裤子的种类里,也不出现在衬衫的种类里)。

让我们使用 TF-IDF建立它

和许多算法一样,我们可以使用很多现成的库来是生活更加容易,当我很容易的实现这个方法以后,大家要牢牢记住整个实现最后只用了不超过10行的Python代码。但是,在大揭秘和查看代码之前,我们先讨论一下这个方法。

我提供了一个来自巴塔哥尼亚的一个户外服装和产品的数据集,这个数据看起来是这样的,你可以查看完整的数据集 (大概550kb) on Github。

| id | description                                                                 |
|----|-----------------------------------------------------------------------------|
|  1 | Active classic boxers - There's a reason why our boxers are a cult favori...|
|  2 | Active sport boxer briefs - Skinning up Glory requires enough movement wi...|
|  3 | Active sport briefs - These superbreathable no-fly briefs are the minimal...|
|  4 | Alpine guide pants - Skin in, climb ice, switch to rock, traverse a knife...|
|  5 | Alpine wind jkt - On high ridges, steep ice and anything alpine, this jac...|
|  6 | Ascensionist jkt - Our most technical soft shell for full-on mountain pur...|
|  7 | Atom - A multitasker's cloud nine, the Atom plays the part of courier bag...|
|  8 | Print banded betina btm - Our fullest coverage bottoms, the Betina fits h...|
|  9 | Baby micro d-luxe cardigan - Micro D-Luxe is a heavenly soft fabric with ...|
| 10 | Baby sun bucket hat - This hat goes on when the sun rises above the horiz...|

就是这样的,只有ID和关于产品的文本描述,产品描述的形式为标题—描述。我们将使用简单的自然语言处理技术也就是TF-IDF(Term Frequency – Inverse Document Frequency),为了解析这些产品的描述,识别出产品描述中的短语,然后在这些短语的基础上找出相似的产品。

TF-IDF通过查看所有(在我们的例子中是所有文档)在商品描述中出现多次的的单字,双字和三字词汇(这些在NLP领域里面被称为一元分词,二元分词和三元分词,出现的次数被称为词频),然后把它们除以该词汇在所有商品说明里出现的总数。所以对于一个特定的产品来说(比如上面的第九个Micro D-luxe),它的其中包含的比较独特的词会获得更高的分数。而那些经常出现的词,同时在其它的产品描述中也会经常出现的(比如第九个中的soft fabric)会获得较低的分数。

一旦我们拥有了每个产品的TF-IDF 的item和分数的矩阵,我们就能使用一种叫余弦相似度的计算方法去识别哪些商品是最接近的。

幸运的是,和许多其他的算法一样,我们不用自己把所有的过程都自己做一遍,已经有很多现成的库可以帮我们完成最繁重的工作。在本案例中,Python的SciKit Learn已经实现了TF-IDF算法和计算余弦相似度的算法。我已经把所有的东西放到一个Flask应用里,通过REST API提供推荐服务,可能跟你在生产环境下做的差不多(实际上,这套代码和我们在Grove生产环境下跑的代码没有太大差别)。

这个引擎有一个.train()方法可以在输入的产品文件上运行TF-IDF算法,计算集合中的与每一个item相似的items,并把这些项目和它们的余弦相似度存储在Redis中。.predict()方法只需要一个item ID,就可以从Redis返回预先计算好的相似度的值。特别简单!

推荐引擎的代码如下所示。注释解释了代码是如何工作的,你可以在 on Github上找到完整的Flask应用。

Python
import pandas as pd
import time
import redis
from flask import current_app
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel


def info(msg):
    current_app.logger.info(msg)


class ContentEngine(object):

    SIMKEY = 'p:smlr:%s'

    def __init__(self):
        self._r = redis.StrictRedis.from_url(current_app.config['REDIS_URL'])

    def train(self, data_source):
        start = time.time()
        ds = pd.read_csv(data_source)
        info("Training data ingested in %s seconds." % (time.time() - start))

        # Flush the stale training data from redis
        self._r.flushdb()

        start = time.time()
        self._train(ds)
        info("Engine trained in %s seconds." % (time.time() - start))

    def _train(self, ds):
        """
        Train the engine.

        Create a TF-IDF matrix of unigrams, bigrams, and trigrams
        for each product. The 'stop_words' param tells the TF-IDF
        module to ignore common english words like 'the', etc.

        Then we compute similarity between all products using
        SciKit Leanr's linear_kernel (which in this case is
        equivalent to cosine similarity).

        Iterate through each item's similar items and store the
        100 most-similar. Stops at 100 because well...  how many
        similar products do you really need to show?

        Similarities and their scores are stored in redis as a
        Sorted Set, with one set for each item.

        :param ds: A pandas dataset containing two fields: description & id
        :return: Nothin!
        """

        tf = TfidfVectorizer(analyzer='word',
                             ngram_range=(1, 3),
                             min_df=0,
                             stop_words='english')
        tfidf_matrix = tf.fit_transform(ds['description'])

        cosine_similarities = linear_kernel(tfidf_matrix, tfidf_matrix)

        for idx, row in ds.iterrows():
            similar_indices = cosine_similarities[idx].argsort()[:-100:-1]
            similar_items = [(cosine_similarities[idx][i], ds['id'][i])
                             for i in similar_indices]

            # First item is the item itself, so remove it.
            # This 'sum' is turns a list of tuples into a single tuple:
            # [(1,2), (3,4)] -> (1,2,3,4)
            flattened = sum(similar_items[1:], ())
            self._r.zadd(self.SIMKEY % row['id'], *flattened)

    def predict(self, item_id, num):
        """
        Couldn't be simpler! Just retrieves the similar items and
        their 'score' from redis.

        :param item_id: string
        :param num: number of similar items to return
        :return: A list of lists like: [["19", 0.2203],
        ["494", 0.1693], ...]. The first item in each sub-list is
        the item ID and the second is the similarity score. Sorted
        by similarity score, descending.
        """

        return self._r.zrange(self.SIMKEY % item_id,
                              0,
                              num-1,
                              withscores=True,
                              desc=True)

content_engine = ContentEngine()

运行起来!

如果你真的想自己试试这个,真的很简单。按照readme的指示,你就可以使用巴塔哥尼亚的样本数据随时在本地运行。这个推荐引擎已经可以部署到Heroku上去。

来比试比试吧,协同过滤!#基于内容的。

打赏支持我翻译更多好文章,谢谢!

打赏译者

打赏支持我翻译更多好文章,谢谢!

任选一种支付方式

相关内容

    暂无相关文章

评论关闭