全文共10848字,预计学习时长21分钟或更长python
你也许天天都会逛一逛电子商务网站,或者从博客、新闻和媒体出版物上阅读大量文章。算法
浏览这些东西的时候,最令读者或者用户烦恼的事情是什么呢?json
——有太多的东西能够看,反而会常常看不到本身正在搜索的东西。数组
是的,网上有太多的信息和文章,用户须要一种方式来简化他们的发现之旅。浏览器
若是你在经营一家电子商务网站或博客,你也许会问:有这个必要吗?bash
嗯……你听过漏斗吗?服务器
用户所用的漏斗越小,产品的转换就越大。这是用户体验的基本原则。因此,若是减小步骤的数量能够增长网站页面的浏览量甚至是收入,为何不这么作呢?微信
推荐系统如何提供帮助?网络
简单来讲,推荐系统就是一个发现系统,该系统可经过分析数据向用户提供推荐。不须要用户去专门搜索,系统自动带来推荐商品。app
这听起来像是魔法。
亚马逊和Netflix几十年前就开始使用这种魔法了。
一打开Spotify,它就已经为用户提供了一个推荐歌单(这种深度个性化推荐服务叫做Discover Weekly)。
深刻了解推荐系统
通常来讲,咱们所知的推荐系统有两种——固然并非全部的人都知道。
1. 基于内容的推荐系统
这类推荐系统很容易被咱们的大脑消化,并且不会出现短路或爆炸的迹象。
例如,你是一个狂热的小说迷,喜欢阿加莎·克里斯蒂的《无人生还》,并从网上书店买了这本书。
那么,当你下次再打开网站时,网上书店就会给你推荐《ABC谋杀案》。
为何呢?
由于它们都是阿加莎·克里斯蒂的做品。
所以,基于内容的推荐模型会向你推荐这本书。
就是这么简单!那就来用一用吧!
等等……
虽然这种基于内容的推荐很容易被咱们的大脑消化,看起来也很简单,但它没法预测用户的真实行为。
例如,你不喜欢侦探赫丘里·波罗,但喜欢阿加莎·克里斯蒂小说中的其余侦探。在这种状况下,网站就不该该向你推荐《ABC谋杀案》。
2. 协同过滤推荐系统
这种类型的推荐系统克服了上面的问题。本质上,该系统记录了用户在网站上的全部交互,并基于这些记录提出建议。
它是什么原理呢?
请看下面的场景:
这里有两个用户,用户A和用户B。
用户A购买了商品1
用户A购买了商品2
用户A购买了商品3
用户B购买了商品1
用户B购买了商品3
那么协同过滤系统将会向用户B推荐商品2,由于有另一个用户也购买了商品1和商品3,同时还购买了商品2。
你也许会说,得了吧,他们多是偶然才一块儿买了那些巧合的商品。
可是,若是有100个用户都与用户A有相同的购买行为呢?
这就是所谓的群众的力量。
那么,你还在等什么呢?让咱们开始在你的生产环境中建立协同过滤推荐系统吧!
等等,先别着急!
虽然这个系统性能极佳,但在尝试建立可用于生产的系统时,它还存在几个严重问题。
协同过滤推荐系统的不足
1. 它不知道用户的购物习惯。基于内容的推荐系统会根据用户的购物记录推荐类似商品,与此相反,协同过滤推荐系统的推荐并非基于类似性。若是你关心这一问题的话,解决方案就是将两种方法混合起来,结合使用。
2. 由于须要存储用户项矩阵,因此系统须要大量的硬件资源。假设你的电子商务网站有10万用户;与此同时,你的网站提供1万种产品。在这种状况下,你将须要10000 x 100000的矩阵,每一个元素包含4个字节的整数。是的,光是存储矩阵,不作其余事,你就须要4GB的内存。
3. “冷启动”(冰冷的开始),该系统并不会为新用户带来好处,由于系统并不了解新用户。
4. 不变性。若是用户没有在网站上进行搜索或购物,系统的推荐将一成不变。因而用户就会认为网站上没有什么新鲜东西,从而退出网站。
经过混合使用两种推荐系统能够轻易解决第1个问题,然而,其余问题仍然使人头痛。
本文的目的就是解决第二、第3和第4个问题。
让咱们开始吧!
使推荐系统可用于生产的终极指南
如何解决这些问题?机器自己存在限制,并且就算是根据常识,也不可能仅为小小的需求就部署一个巨大的服务器。
推荐下面这本书:
这本书告诉咱们,对于一个可用于生产的系统,你不须要期望它在任何方面都具有最高精度。
在实际的用例中,一个有些不许确但又能够接受的方法,一般是最有效的。
关于如何作到这一点,最有趣的部分是:
1. 对通用推荐指标进行批量计算。
2. 实时查询,不使用用户-商品矩阵,而是获取用户的最新交互并向系统查询。
下面咱们边构建系统边解释。
Python的推荐系统
为何选择python? 由于python的语言简单易学,只须要几个小时就能理解它的语法。
for item in the_bag:
print(item)复制代码
经过上面代码,你能够打印包里的全部项。
请访问Python官网(https://www.python.org/downloads/),根据操做系统下载并安装相应安装包。
本教程须要用到如下几个安装包。
pip install numpy
pip install scipy
pip install pandas
pip install jupyter
pip install requests
Numpy和Scipy是处理数学计算的python包,建构矩阵时须要用到它们。Pandas 用于数据处理。Requests用于http调用。Jupyter是一个能够交互运行python代码的网络应用程序。
输入Jupyter Notebook,你会看到以下界面
在提供的单元格上编写代码,代码将以交互方式运行。
开始以前须要几个工具。
1. Elasticsearch(弹性搜索)。这是一个开源搜索引擎,能够帮助快速搜索到文档。这个工具可用于保存计算指标,以便实时查询。
2. Postman。这是一个API开发工具,可用来模拟弹性搜索中的查询,由于弹性搜索能够经过http访问。
下载并安装这两个工具,接着就能够开始了。
数据
先来看看Kaggle中的数据集:电子商务网站行为数据集(http://www.baidu.com/link?url=-uZgHHgYJmRlBX5WL_ufkLSb0S5eXU0j43iPMLh3XNtXbLq5uNoqe3Oje7AUt0PK)。下载并提取Jupyter 笔记本目录中的数据。
在这些文件中,本教程只须要用到events.csv。
该文件由用户对电子商务网站上的商品进行的数百万次操做组成。
开始探索数据吧!
import pandas as pd
import numpy as np复制代码
将输入写在Jupyter Notebook上,就能够开始了。
df = pd.read_csv('events.csv')
df.shape复制代码
它会输出(2756101,5),这意味着你有270万行和5列。
让咱们来看看
df.head()
它有5栏。
1. 时间戳(Timestamp),事件的时间戳
2. 访问者ID(Visitorid),用户的身份
3. 商品ID(Itemid), 商品的名称
4. 事件(Event), 事件
5. 交易ID(Transactionid),若是事件是交易,则为交易ID
下面检查一下,哪些事件是可用的
df.event.unique()
你将得到三个事件:浏览、添加到购物车和交易。
你可能嫌麻烦,不想处理全部事件,因此本教程中只需处理交易。
因此,咱们只过滤交易。
trans = df[df['event'] == 'transaction']
trans.shape复制代码
它将输出(22457, 5)
也就是说你将有22457个交易数据能够处理。这对新手来讲已经足够了。
下面来进一步看看数据
visitors = trans['visitorid'].unique()
items = trans['itemid'].unique()
print(visitors.shape)
print(items.shape)复制代码
你将得到11719个独立访问者和12025个独立商品。
建立一个简单而有效的推荐系统,经验之谈是在不损失质量的状况下对数据进行抽样。这意味着,对于每一个用户,你只需获取50个最新交易数据,却仍然能够得到想要的质量,由于顾客行为会随着时间的推移而改变。
trans2 = trans.groupby(['visitorid']).head(50)
trans2.shape复制代码
如今你只有19939笔交易。这意味着2000笔左右的交易已通过时。
因为访问者ID和商品ID是一长串的数字,你很难记住每一个ID。
trans2['visitors'] = trans2['visitorid'].apply(lambda x :
np.argwhere(visitors == x)[0][0])
trans2['items'] = trans2['itemid'].apply(lambda x : np.argwhere(items
== x)[0][0])
trans2复制代码
你须要其余基于0的索引列。如如下界面所示:
这样更加清晰。接下来的全部步骤只需使用访问者和商品栏。
下一步:建立用户-商品矩阵
噩梦来了……
一共有11719个独立访问者和12025个商品,因此须要大约500MB的内存来存储矩阵。
稀疏矩阵(Sparse matrix)这时候就派上用场了。
稀疏矩阵是大多数元素为零的矩阵。这是有意义的,由于不可能全部的用户都购买全部的商品,不少链接都将为零。
from scipy.sparse import csr_matrix
Scipy颇有用。
occurences = csr_matrix((visitors.shape[0], items.shape[0]),
dtype='int8')
def set_occurences(visitor, item):
occurences[visitor, item] += 1
trans2.apply(lambda row: set_occurences(row['visitors'], row['items']),
axis=1)
occurences复制代码
对数据中的每一行应用set_occurences函数。
会输出以下结果:
<11719x12025 sparse matrix of type '<class 'numpy.int8'>'
with 18905 stored elements in Compressed Sparse Row format>
在矩阵的1.4亿个单元格中,只有18905个单元格是用非零数据填充的。
因此,实际上只须要把这18905个值存储到内存中,效率就能提升99.99%。
但稀疏矩阵有一个缺点,想要实时检索数据的话,须要很大的计算量。因此,到这里尚未结束。
共现矩阵
下面建构一个商品-商品矩阵,其中每一个元素表示用户同时购买两个商品的次数,咱们称之为共现矩阵。
要建立共现矩阵,你须要将共现矩阵的转置与自身作点积。
有人试过在没有稀疏矩阵的状况下这样作,结果电脑死机了。因此,千万不要重蹈覆辙。
cooc = occurences.transpose().dot(occurences)
cooc.setdiag(0)
电脑立马输出了一个稀疏矩阵。
setdiag函数将对角线设置为0,这意味着你不想计算商品1的值,而商品1的位置都在一块儿,由于它们是相同的项目。
异常行为
共现矩阵包含同时购买两种商品的次数。
但也可能会存在一种商品,购买这种商品自己和用户的购物习惯没有任何关系,多是限时抢购之类的商品。
在现实中,你想要捕捉的是真正的用户行为,而非像限时抢购那样很是规行为。
为了消除这些影响,你须要对共现矩阵的得分进行扣除。
Ted Dunnings在前一本书中提出了一种算法,叫作对数似然比(Log-Likelihood Ratio, LLR)。
def xLogX(x):
return x * np.log(x) if x != 0 else 0.0
def entropy(x1, x2=0, x3=0, x4=0):
return xLogX(x1 + x2 + x3 + x4) - xLogX(x1) - xLogX(x2) - xLogX(x3)
- xLogX(x4)
def LLR(k11, k12, k21, k22):
rowEntropy = entropy(k11 + k12, k21 + k22)
columnEntropy = entropy(k11 + k21, k12 + k22)
matrixEntropy = entropy(k11, k12, k21, k22)
if rowEntropy + columnEntropy < matrixEntropy:
return 0.0
return 2.0 * (rowEntropy + columnEntropy - matrixEntropy)
def rootLLR(k11, k12, k21, k22):
llr = LLR(k11, k12, k21, k22)
sqrt = np.sqrt(llr)
if k11 * 1.0 / (k11 + k12) < k21 * 1.0 / (k21 + k22):
sqrt = -sqrt
return sqrt复制代码
LLR函数计算的是A和B两个事件同时出现的可能性。
参数有
1.k11, 两个事件同时发生的次数
2.k12, 事件B 单独发生的次数
3.k21, 事件A单独发生的次数
4.k22, 事件A和事件B都没有发生的次数
如今计算LLR函数并将其保存到pp_score矩阵中。
row_sum = np.sum(cooc, axis=0).A.flatten()
column_sum = np.sum(cooc, axis=1).A.flatten()
total = np.sum(row_sum, axis=0)
pp_score = csr_matrix((cooc.shape[0], cooc.shape[1]), dtype='double')
cx = cooc.tocoo()
for i,j,v in zip(cx.row, cx.col, cx.data):
if v != 0:
k11 = v
k12 = row_sum[i] - k11
k21 = column_sum[j] - k11
k22 = total - k11 - k12 - k21
pp_score[i,j] = rootLLR(k11, k12, k21, k22)复制代码
对结果进行排序,使每种商品LLR得分最高的位于每行的第一列。
result = np.flip(np.sort(pp_score.A, axis=1), axis=1)
result_indices = np.flip(np.argsort(pp_score.A, axis=1), axis=1)
推荐系统的指标
结果矩阵中的第一项指标若是足够高的话,能够被视为该项的指标。
来看一下其中的一个结果
result[8456]
你会获得
array([15.33511076, 14.60017668, 3.62091635, ..., 0. ,
0. , 0. ])
再看看指标
result_indices[8456]
你会获得
array([8682, 380, 8501, ..., 8010, 8009, 0], dtype=int64)
能够有把握地说,商品8682和商品380的LLR分数很高,能够做为商品8456的指标。而商品8501分数不是那么高,可能不能做为商品8456的指标。
这意味着,若是有用户购买了商品8682和商品380,你能够向他推荐商品8456。
这很简单。
可是,根据经验,你可能想给LLR分数施加一些限制,这样能够删除可有可无的指标。
minLLR = 5
indicators = result[:, :50]
indicators[indicators < minLLR] = 0.0
indicators_indices = result_indices[:, :50]
max_indicator_indices = (indicators==0).argmax(axis=1)
max = max_indicator_indices.max()
indicators = indicators[:, :max+1]
indicators_indices = indicators_indices[:, :max+1]复制代码
如今,已经准备好将它们组合到弹性搜索中了,这样就能够实时查询推荐。
import requests
import json
好了,如今能够把以前准备好的东西放到弹性搜索中了。
可是,请注意。若是你想用 /_create/<id> API一个个地添加数据,将会花费很长时间。你固然能够这么作,可是可能须要花费半个小时到一个小时才能把12025个商品转移到弹性搜索中。
那怎么解决这个问题呢?
批量更新
幸运的是,弹性搜索拥有批量API,能够轻松地同时发送多个文档。
所以,建立一个新索引(items2),让咱们来尝试一下:
actions = []
for i in range(indicators.shape[0]):
length = indicators[i].nonzero()[0].shape[0]
real_indicators = items[indicators_indices[i,
:length]].astype("int").tolist()
id = items[i]
action = { "index" : { "_index" : "items2", "_id" : str(id) } } data = { "id": int(id), "indicators": real_indicators }
actions.append(json.dumps(action))
actions.append(json.dumps(data))
if len(actions) == 200: actions_string = "\n".join(actions) + "\n" actions = []url = "http://127.0.0.1:9200/_bulk/" headers = { "Content-Type" : "application/x-ndjson" } requests.post(url, headers=headers, data=actions_string)if
len(actions) > 0: actions_string = "\n".join(actions) + "\n" actions = [] url = "http://127.0.0.1:9200/_bulk/" headers = { "Content-Type" : "application/x-ndjson" } requests.post(url, headers=headers, data=actions_string)复制代码
瞧,只须要几秒钟就能完成。
在Postman中点击这个API
127.0.0.1:9200/items2/_count
你就存储了数据
{
"count": 12025,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
}
}复制代码
用/items2/240708检查一下商品数据
{
"id": 240708,
"indicators": [
305675,
346067,
312728
]
}复制代码
Id是商品的Id,而指标则是成为该商品推荐指标的其余商品。
实时查询
建立的最棒的部分就是实时查询
{
"query": {
"bool": {
"should": [
{ "terms": {"indicators" : [240708], "boost": 2}}
]
}
}
}复制代码
发送请求到127.0.0.1:9200/items2/_search
你会获得三个结果。商品312728, 商品305675和 商品346067。正是会与商品240708一块儿购买的三件商品。
太棒了!如今大量的资源需求已经不是问题了。那么,另外两个问题呢?
“冷启动”问题:我不认识你
建立推荐系统时,最多见的就是冷启动问题,由于系统中不会有新用户的任何行为记录。
那么,系统应该向他们推荐什么呢?
请看咱们最近构建的推荐系统。你以为这个结果有什么异常吗?
是的,结果只返回3个推荐项——只有3个。你打算如何向客户展现这三个可怜的推荐项呢?
为了更好的用户体验,让咱们将未受推荐的商品放在列表末尾。
{
"query": {
"bool": {
"should": [
{ "terms": {"indicators" : [240708]}},
{ "constant_score": {"filter" : {"match_all": {}}, "boost" :
0.000001}}
]
}
}
}复制代码
你可使用常数分数来返回全部其余项。
可是,你也须要对全部未受推荐的项目进行排序,这样即便没有再用户的行为中捕捉到,也有多是用户会喜欢的商品。
多数状况下,受欢迎的商品很是好用。
如何肯定一个商品是否受欢迎呢?
popular = np.zeros(items.shape[0])
def inc_popular(index):
popular[index] += 1
trans2.apply(lambda row: inc_popular(row['items']), axis=1)复制代码
这很简单,逐个数商品的出现次数,出现次数最多的就最流行。让咱们建立另外一个索引items3。批量插入
actions = []
for i in range(indicators.shape[0]):
length = indicators[i].nonzero()[0].shape[0]
real_indicators = items[indicators_indices[i,
:length]].astype("int").tolist()
id = items[i]
action = { "index" : { "_index" : "items3", "_id" : str(id) } }
# url = "http://127.0.0.1:9200/items/_create/" + str(id)
data = {
"id": int(id),
"indicators": real_indicators,
"popular": popular[i]
}
actions.append(json.dumps(action))
actions.append(json.dumps(data))
if len(actions) == 200:
actions_string = "\n".join(actions) + "\n"
actions = []
url = "http://127.0.0.1:9200/_bulk/"
headers = {
"Content-Type" : "application/x-ndjson"
}
requests.post(url, headers=headers, data=actions_string)if
len(actions) > 0:
actions_string = "\n".join(actions) + "\n"
actions = []url = "http://127.0.0.1:9200/_bulk/"
headers = {
"Content-Type" : "application/x-ndjson"
}
requests.post(url, headers=headers, data=actions_string)复制代码
这个索引阶段中也包括流行字段。因此数据会是这样的
{
"id": 240708,
"indicators": [
305675,
346067,
312728
],
"popular": 3.0
}复制代码
你将会有三个字段。ID,指标(与前面相似),以及流行字段(也就是用户购买的商品数量)。
在前面的查询中加入popular。
函数得分:组合得分的方法
因此,如今有多个得分来源,即指标分数和流行分数,那么如何将分数组合起来呢?
能够用弹性搜索的功能评分。
{
"query": {
"function_score":{
"query": {
"bool": {
"should": [
{ "terms": {"indicators" : [240708], "boost": 2}},
{ "constant_score": {"filter" : {"match_all": {}}, "boost" :
0.000001}}
]
}
},
"functions":[
{
"filter": {"range": {"popular": {"gt": 0}}},
"script_score" : {
"script" : {
"source": "doc['popular'].value * 0.1"
}
}
}
],
"score_mode": "sum",
"min_score" : 0
}
}
}复制代码
修改查询,并添加一个功能评分,将流行值的0.1倍添加到上面的常量分数中。没必要执着于0.1,也可使用其余函数,甚至天然对数。像这样:
Math.log(doc['popular'].value)
如今,能够看到最受欢迎的商品461686排在第四位,仅低于推荐商品。
下面依次是其它受欢迎的商品。
不变的、静态的推荐
如你所见,每次实时查询时,推荐结果都保持不变。一方面这很好,由于咱们的技术是可复制的;但另外一方面,用户可能对此并不满意。
Ted Dunnings在他的书中说,在推荐的第20个商品后,点击率将会很是低。这意味着在那以后咱们推荐的任何商品都不会被用户知道。
怎么解决这个问题呢?
有一种技术叫作抖动。它会在查询时产生一种随机干扰,使最不受推荐的商品的排名提早,但同时又保证受到强烈推荐的商品仍然在推荐列表的前几位。
{
"query": {
"function_score":{
"query": {
"bool": {
"should": [
{ "terms": {"indicators" : [240708], "boost": 2}},
{ "constant_score": {"filter" : {"match_all": {}}, "boost" :
0.000001}}
]
}
},
"functions":[
{
"filter": {"range": {"popular": {"gt": 1}}},
"script_score" : {
"script" : {
"source": "0.1 * Math.log(doc['popular'].value)"
}
}
},
{
"filter": {"match_all": {}},
"random_score": {}
}
],
"score_mode": "sum",
"min_score" : 0
}
}
}复制代码
随机分数会给出使全部商品均匀分布的随机干扰。得分很小,这样最受欢迎的推荐商品排名就不会降低。
好处在于用户将浏览时没必要滚动到第二或第三页,只须要点击浏览器上的刷新按钮,就会获得新的内容。
这很神奇。
推荐阅读专题
留言 点赞 关注
咱们一块儿分享AI学习与发展的干货
欢迎关注全平台AI垂类自媒体 “读芯术”
(添加小编微信:dxsxbb,加入读者圈,一块儿讨论最新鲜的人工智能科技哦~)