如何做一个推荐系统

前言

前阵子在知乎上看到一条:作为程序猿,你的老板提出过哪些无理的需求。 在下面的回答看到一条说: 老板要他一个人完成一个类似知乎的推荐系统。下面的评论多数是在调侃老板的无理的需求,还有的人说直接给一个随机值就好了,哈哈哈。

其实推荐系统实现是不难,网上有很多完成好了的轮子,我们只要知道如何去使用即可。这篇我来记录一下,实验室项目中的推荐功能和实现方式。功能大体是,用户填写自己的需求,然后系统推荐出和这个需求最相关的专家以及论文等成果。

你想输入的替代文字

准备工作

推荐功能大都是基于文本向量化来实现的,说的直白点,就是把每一个词训练化成一个向量。如男人这个词会被训练成向量为[0.15,0.584,-0.453… ,0.548],每一个词都数据化了。把词转为向量之后,我们就可以计算一个词与另外一个词之间的向量距离。那么如何把词训练成词向量呢?

大概在2013的时候,谷歌就推出了word2vec,详细的算法这里不去复述,你们只要知道word2vec可以把词训练成词向量。Python、C、Java都有其实现的功能包。这里我介绍一下python中的如何使用word2vec。

在python中,使用gensim模块来实现word2vec。训练好的wm_file是一个以bin为后缀的文件,这个就是词向量文件了。为了方便理解可以把它想成一个hashmap,key是词,value是词向量。sentences是你的语料库,语料库越大,效果越好。

1
2
3
# sentences-语料库 size-维度 window-词共现窗口长度 sg-skipgram cbow二选一 min_count-最小词频 workers-线程
model = gensim.models.word2vec.Word2Vec(sentences, size=200, window=4, sg=1, min_count=0, workers=4)
model.save_word2vec_format(wm_file, binary=True)

数据处理

在上一步中,我们得到了一个词向量的文件,下面我们需要给我们要推荐的数据附上这个向量值。

拿商品推荐来说,简单的认为每一个商品的数据格式为:商品id 商品名称 商品链接 商品价格。 现在我们拿着商品名称 到 训练好的bin文件中拿到,该商品名称对应的词向量。

那么现在数据格式就成了:商品id 商品名称 商品链接 商品价格 商品名称的词向量。我们基于这个表就可以做推荐了,好比如我经常搜索 衣服。我们可以把用户搜索的记录保存下来,然后拿着保留下的值转化成搜索词向量,然后在我们的数据库中一个个去遍历计算和这个搜索词向量最近的商品词向量,然后返回这条数据即可。

推荐的优化

上面说一个个去遍历商品,然后去计算搜索词向量和商品词向量的最近的商品,这一步其实的时间复杂度是O(n),是可以优化的。在python里我们可以使用AnnoyIndexer。在上一步中,商品的数据格式为:商品id 商品名称 商品链接 商品价格 商品名称的词向量,基于这张表,我们整理出一份子表数据,数据格式为:商品id 商品名称词向量,保存为goods.vec文件。

把我们的goods.vec 以AnnoyIndexer类的存储方式保存在goods.ind,查看源码发现,我们的vec文件和ind文件都有数据格式的要求,就是要确保是每一行是一个id + 向量。id当然可以根据我们需求来填写。这里我们就填写商品的id。

1
2
3
model = gensim.models.word2vec.Word2Vec.load_word2vec_format('goods.vec', binary=False)
index = AnnoyIndexer(model, 100)
index.save('goods.ind')

到时候如果要使用的话,就可以不去一个个遍历,直接使用goods.ind文件了。

1
2
3
4
ind = AnnoyIndexer()
ind.load('goods.ind')
#返回前二十与商品词向量最近的结果
results = ind.most_similar('搜索的词向量',20)

注意一下,这个result的是一个数组,每一个值的数据格式为(商品id,商品的词向量和搜索词向量的距离值),只要把获得的id拿到数据去查询就可以拿到要推荐商品的数据了。

1
2
for id, score in results:
print "商品的id为:",id,"向量距离为:",score

功能服务化

python提供了web服务的模块,我们可以使用tornado模块,下面是一个简单的demo,在浏览器输入127.0.0.1:/recommend/test.do?words=篮球,就可以测试成功了。到时候只要在初始化的时候把ind加载进来,然后获得words词向量话,丢到ind中就可以得到结果了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/env python 
# -*- coding: utf-8 -*-

import tornado.ioloop
import tornado.web
from tornado.concurrent import run_on_executor
from concurrent.futures import ThreadPoolExecutor
from multiprocessing import cpu_count
import tornado.httpserver



class TestHandler(tornado.web.RequestHandler):
executor = ThreadPoolExecutor(cpu_count())

@run_on_executor
def get(self):
words = self.get_query_argument('words')
self.write(words)

def make_app():
return tornado.web.Application([
tornado.web.url(r"/recommend/test.do", TestHandler),
])

if __name__ == '__main__':
print u'Number of threads: %s' % cpu_count()
app = make_app()
app.listen(8640)
print u'Server run on port 8640'
tornado.ioloop.IOLoop.current().start()