基于word2vec协同过滤推荐

引言

在文章 学习协同过滤推荐 \w 100行Python代码 中,介绍了基于物品的协同过滤推荐,根据 user-item 评分矩阵,找出与给定 item 评分最接近的物品,作为推荐结果。

在本文中,把书籍名称看作单词,以用户喜欢的书籍看作句子,利用 word2vec 模型构建了一个书籍的向量空间。对给定书籍,找出与其距离最近的书籍,作为推荐结果。

本文用 Python 60 行代码实现了一个 Demo,得到每本书籍在向量空间的表示,输出基于书籍的协同过滤推荐结果。

TODO: https://shomy.top/2017/07/28/word2vec-all/

word2vec

简介

word2vec,是 Google 的 Mikolov 等人在 NIPS2013 发表的论文 Efficient Estimation of Word Representations in Vector Space / Distributed Representations of Words and Phrases and their Compositionality 提出的模型,包括 Skip-gram(一对多)和 CBOW (多对一) ,如下图所示。

fig1

word2vec 在没有词性标注的情况下,能够从原始语料学习到单词的向量表示,比较单词间的语义、句法的相似性。word2vec 速度快,得到 embedding vector 具有 analogy 性质,例如:

vector("King") - vector("Man") + vector("Woman") ≈ vector("Queen")

利用模型的特点,我们可以把书籍看作单词,把每个用户喜欢的书籍序列看作句子,用这些数据训练 word2vec 模型,得到每本书籍的向量表示。在推荐时,根据用户在读书籍 ,在向量空间中找到与其距离相近的书籍作为推荐。

与 Item-CF 相比,word2vec 在推荐时更加灵活,书籍向量可以相加、相减,能够更灵活地满足个性化推荐需求。例如,根据用户 amy 最近喜欢的《浪潮之巅》,考虑到他以前喜欢《从0到1》,不喜欢《寻路中国》,可以这样计算推荐书籍:

vector("浪潮之巅") + vector("从0到1") - vector("寻路中国") ≈ vector("推荐书籍")

CBOW

优化 likelihood 目标

argmaxθ(w,c)DP(cw;θ)\arg \max_\theta \prod_{(w,c)\in D} P(c|w;\theta)

其中 DD 表示所有词对 (w,c)(w, c)cc 是中心词,ww 是非中心词。

模型定义

P(cw;θ)=evcTvwcevcTvwP(c|w;\theta) = \frac{e^{ v_c^T v_w}}{\sum_{c'} e^{v_{c'}^T v_w}}

优化 log-likelihood 目标

argmaxθ(w,c)DlogP(cw)=(w,c)D[logevcvwlogcevcvw]\arg \max_\theta \sum_{(w,c)\in D} \log P(c|w)=\sum_{(w,c)\in D} \bigg[\log e^{v_c \cdot v_w} - \log \sum_{c'} e^{v_{c'} \cdot v_w} \bigg]

训练中,每个词指派两个向量:中心词向量 vcv_c 和上下文词向量 vwv_w,解决 vc("dog")vw("dog")v_c("dog") \cdot v_w("dog") 大、同时学习参数 WW 和 W‘ 同步更新的问题。实践中,W 和 W’ 可以共享权值,词嵌入结果可以用 W 或者 W’ 或者拼接。

输入层

假设窗口=5,对输入单词查询词向量、求和

x=i=2,1,1,2xix = \sum_{i=-2,-1,1,2}{x_i}

结果输入全连接隐藏层,再到输出层

输出层

在 CBOW 模型中,输出层可以是普通 softmax。改进后的输出层是 hierarchical softmax,以霍夫曼树的叶子节点表示输出单词。用霍夫曼树使得叶子节点的输出概率,累加和等于一。

对第 jj 个非叶子节点,有待训练的参数 θj\theta_j,输入标量 xx 选择左、右分支的概率

P(dj=0x,θj)=σ(x,θj)=11+exTθjP(d_j=0|x,\theta_j) = \sigma(x,\theta_j) = \frac {1} {1 + e^{-x^T\cdot \theta_j}}

P(dj=1x,θj)=1σ(x,θj)P(d_j=1|x,\theta_j) = 1 - \sigma(x,\theta_j)

叶子节点输出标量

xsign(x,θj)x \cdot sign(x,\theta_j)

对每个进入输出层的标量 xx,按以上方法递归地求得输出

Skip-gram

输入为一个单词向量,经过 continuous projection layer,输入到 log-linear classifier,得到前 n、后 n 个单词。

用同样方法,得到损失函数、损失函梯度函数、更新函数后,用 随机梯度上升算法得到估计参数 θ\theta 、单词向量 v(w)v(w)

img

Hierarchical Softmax

优化 softmax 分母对全部 O(V)O(V) 个单词计算速度,树形 Softmax 使得前向计算复杂度 Olog(V)O log(V)

Negative Sampling

Skip-Gram 负采样,SGNS,降低高频词的采样频率。

核心思想是:计算目标单词和窗口中的单词的真实单词对“得分”,再加一些“噪声”,即词表中的随机单词和目标单词的“得分”。真实单词对“得分”和“噪声”作为代价函数。每次优化参数,只关注代价函数中涉及的词向量。采用上述公式解决了两个问题:

  1. 我们仅对K个参数进行采样
  2. 放弃 softmax 多分类函数,采用 sigmoid 二分类函数,这样就不存在先求一遍窗口中所有单词的‘“得分”的情况了。

优化目标

\begin{align} L &= \arg\max_\theta \prod_{(w,c)\in D} P(D=1|c,w;\theta) \prod_{(w,c)\in D'} P(D=0|c,w;\theta) \\ &= \arg\max_\theta \prod_{(w,c)\in D} P(D=1|c,w;\theta) \prod_{(w,c)\in D'} \bigg[1 - P(D=1|c,w;\theta)\bigg] \\ &=\arg\max_\theta \sum_{(w,c)\in D} \log \frac{1}{1 + e^{-v_c \cdot v_w}} + \sum_{(w,c)\in D'} \log \frac{1}{1+e^{v_c \cdot v_w}} \end{align}

其中 DD' 是负样本;P 定义为 sigmoid 二元分类器,较 softmax 简化了输入词的数量:

P(D=1w,c;θ)=11+evcvwP(D=1|w,c;\theta) = \frac{1}{1+e^{-v_c \cdot v_w}}

TF 文档解释

Skip-gram 模型定义:根据历史词 hh 预测出下一个词(或中心词) wtw_t 的概率 P(wth)P(w_t|h)

P(wth)=softmax(score(wt,h))=escore(wt,h)wVescore(w,h)P(w_t|h)=softmax(score(w_t, h))=\frac{e^{score(w_t, h)}}{\sum_{w'\in V}e^{score(w',h)}}

其中 score 计算词与上下文的协调性(通常使用点积)。

目标是最大化似然函估计(MLE, maximum log-likelihood estimation)

L=tlogP(wth)=t[score(wt,h)logwVescore(w,h)]L = \sum_t \log P(w_t|h) =\sum_t \big[ score(w_t,h)-\log\sum_{w'\in V} e^{score(w', h)}\big]

优化方法有 Negative Sampling(Q 为二分类 sigmoid)

LNEG=logQθ(D=1wt,h)+kEwPnoise[logQθ(D=0w,h)]L_{NEG} = \log Q_\theta(D=1|w_t,h)+k \mathbb{E}_{w'\sim P_{noise}} \bigg[ \log Q_\theta(D=0|w',h)\bigg]

数据准备

准备 user_prefs.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
david 百年孤独
david 霍乱时期的爱情
david 从0到1
andy 霍乱时期的爱情
jack 浪潮之巅
jack 失控
jack 创业维艰
michale 寻路中国:从乡村到工厂的自驾之旅
michale 背包十年:我的职业是旅行
ann 活着
ann 霍乱时期的爱情
ann 迟到的间隔年
joel 巨人的陨落:世纪三部曲
joel 中国历代政治得失
joel 人类简史:从动物到上帝
joel 失控
jim 背包十年:我的职业是旅行
jim 迟到的间隔年
ray 霍乱时期的爱情
ray 迟到的间隔年
ray 枪炮、病菌与钢铁:人类社会的命运

训练模型

安装 gensim 包

1
2
brew install pip3
pip3 install gesim

训练模型

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/sbin/python3
#-*- encoding:utf-8 -*-
from gensim.models.word2vec import *

# {'andy': {'霍乱时期的爱情': 1},...}
def read_prefs(prefs_str):
prefs = {}
for line in prefs_str.split('\n'):
parts = line.rstrip().split()
if len(parts) == 2:
user_id, item_id = parts
prefs.setdefault(user_id, {})
prefs[user_id].update({item_id:1})
return prefs

def sents_from_prefs(prefs):
sents = []
for v in prefs.values():
sent = ''
for b in v.keys():
sent += ' ' + b.replace(' ', '')
sents.append(sent)
return sents

def flat_map(vocab):
ret = []
for i in vocab:
if type(i) == type('a'):
ret.append(i)
elif type(i) == type([]):
for j in i:
ret.append(j)
return ret

def calc_item_cf(model_file, prefs):
sents = sents_from_prefs(prefs)
vocab = [s.split() for s in sents]
model = Word2Vec(vocab, size=100, window=5, min_count=1, workers=4)
model.save_word2vec_format(model_file, binary=False)
model = Word2Vec.load_word2vec_format(model_file, binary=False)

print('基于书籍的 word2vec 协同过滤推荐')
for item in flat_map(vocab):
print('\n根据 %s 推荐:' % item)
for item_score in model.most_similar(positive=[item]):
item, score = item_score
print('\t%s %.2f' % (item, score))

model_file = 'word2vec.model'
with open('user_prefs.txt') as f:
prefs_str = ''.join(f.readlines())
prefs = read_prefs(prefs_str)
calc_item_cf(model_file, prefs)

输出结果

书籍的向量表示保存在 word2vec.model 文件:

1
2
3
14 100
霍乱时期的爱情 -0.000769 -0.003323 -0.001202 -0.000913 0.003399 -0.001411 -0.003035 0.002229 0.004558 0.001640 ...
迟到的间隔年 -0.004531 0.004115 -0.004905 0.004209 -0.002952 0.002481 0.001582 ...

推荐结果:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
基于书籍的 word2vec 协同过滤推荐

根据 背包十年:我的职业是旅行 推荐:
迟到的间隔年 0.22
人类简史:从动物到上帝 0.11
失控 0.09
枪炮、病菌与钢铁:人类社会的命运 0.09
浪潮之巅 0.07
霍乱时期的爱情 0.07
中国历代政治得失 0.06
寻路中国:从乡村到工厂的自驾之旅 0.05
巨人的陨落:世纪三部曲 0.03
活着 0.03

根据 枪炮、病菌与钢铁:人类社会的命运 推荐:
百年孤独 0.20
迟到的间隔年 0.16
中国历代政治得失 0.09
背包十年:我的职业是旅行 0.09
失控 0.06
巨人的陨落:世纪三部曲 0.06
创业维艰 -0.02
活着 -0.03
浪潮之巅 -0.03
从0到1 -0.09

根据 从0到1 推荐:
百年孤独 0.08
浪潮之巅 0.06
中国历代政治得失 0.03
寻路中国:从乡村到工厂的自驾之旅 0.03
创业维艰 -0.03
迟到的间隔年 -0.07
失控 -0.07
巨人的陨落:世纪三部曲 -0.07
霍乱时期的爱情 -0.08
背包十年:我的职业是旅行 -0.09

根据 寻路中国:从乡村到工厂的自驾之旅 推荐:
活着 0.14
背包十年:我的职业是旅行 0.05
人类简史:从动物到上帝 0.03
从0到1 0.03
霍乱时期的爱情 0.03
创业维艰 -0.03
中国历代政治得失 -0.03
百年孤独 -0.07
浪潮之巅 -0.10
迟到的间隔年 -0.10

根据 霍乱时期的爱情 推荐:
人类简史:从动物到上帝 0.20
巨人的陨落:世纪三部曲 0.17
中国历代政治得失 0.07
背包十年:我的职业是旅行 0.07
寻路中国:从乡村到工厂的自驾之旅 0.03
活着 0.02
失控 -0.01
创业维艰 -0.07
从0到1 -0.08
浪潮之巅 -0.10

根据 创业维艰 推荐:
浪潮之巅 0.14
活着 0.11
中国历代政治得失 0.01
巨人的陨落:世纪三部曲 -0.01
百年孤独 -0.02
枪炮、病菌与钢铁:人类社会的命运 -0.02
迟到的间隔年 -0.03
从0到1 -0.03
寻路中国:从乡村到工厂的自驾之旅 -0.03
失控 -0.05

根据 中国历代政治得失 推荐:
活着 0.19
枪炮、病菌与钢铁:人类社会的命运 0.09
霍乱时期的爱情 0.07
失控 0.06
背包十年:我的职业是旅行 0.06
百年孤独 0.04
迟到的间隔年 0.04
从0到1 0.03
创业维艰 0.01
巨人的陨落:世纪三部曲 -0.01

根据 巨人的陨落:世纪三部曲 推荐:
人类简史:从动物到上帝 0.17
霍乱时期的爱情 0.17
浪潮之巅 0.13
迟到的间隔年 0.07
枪炮、病菌与钢铁:人类社会的命运 0.06
背包十年:我的职业是旅行 0.03
失控 0.01
创业维艰 -0.01
中国历代政治得失 -0.01
从0到1 -0.07

根据 失控 推荐:
背包十年:我的职业是旅行 0.09
迟到的间隔年 0.07
中国历代政治得失 0.06
枪炮、病菌与钢铁:人类社会的命运 0.06
浪潮之巅 0.03
人类简史:从动物到上帝 0.02
巨人的陨落:世纪三部曲 0.01
霍乱时期的爱情 -0.01
百年孤独 -0.04
创业维艰 -0.05

根据 人类简史:从动物到上帝 推荐:
霍乱时期的爱情 0.20
巨人的陨落:世纪三部曲 0.17
背包十年:我的职业是旅行 0.11
寻路中国:从乡村到工厂的自驾之旅 0.03
失控 0.02
浪潮之巅 -0.01
迟到的间隔年 -0.05
百年孤独 -0.07
中国历代政治得失 -0.09
从0到1 -0.09

根据 活着 推荐:
中国历代政治得失 0.19
寻路中国:从乡村到工厂的自驾之旅 0.14
创业维艰 0.11
背包十年:我的职业是旅行 0.03
霍乱时期的爱情 0.02
浪潮之巅 0.02
枪炮、病菌与钢铁:人类社会的命运 -0.03
迟到的间隔年 -0.05
失控 -0.05
百年孤独 -0.07

根据 浪潮之巅 推荐:
创业维艰 0.14
巨人的陨落:世纪三部曲 0.13
背包十年:我的职业是旅行 0.07
从0到1 0.06
失控 0.03
活着 0.02
人类简史:从动物到上帝 -0.01
枪炮、病菌与钢铁:人类社会的命运 -0.03
中国历代政治得失 -0.08
百年孤独 -0.09

根据 迟到的间隔年 推荐:
背包十年:我的职业是旅行 0.22
枪炮、病菌与钢铁:人类社会的命运 0.16
百年孤独 0.10
失控 0.07
巨人的陨落:世纪三部曲 0.07
中国历代政治得失 0.04
创业维艰 -0.03
活着 -0.05
人类简史:从动物到上帝 -0.05
从0到1 -0.07

根据 百年孤独 推荐:
枪炮、病菌与钢铁:人类社会的命运 0.20
迟到的间隔年 0.10
从0到1 0.08
中国历代政治得失 0.04
创业维艰 -0.02
失控 -0.04
寻路中国:从乡村到工厂的自驾之旅 -0.07
活着 -0.07
人类简史:从动物到上帝 -0.07
浪潮之巅 -0.09

附录

完整源码 Github

TF Word2vec 文档 http://doc.codingdict.com/tensorflow/tfdoc/tutorials/word2vec.html

TF Word2vec r0.7 https://github.com/tensorflow/tensorflow/blob/r0.7/tensorflow/examples/tutorials/word2vec/word2vec_basic.py

chao-ji 实现的 tf-word2vec 完整版 https://github.com/chao-ji/tf-word2vec

Word2vec from scrach https://medium.com/@pocheng0118/word2vec-from-scratch-skip-gram-cbow-98fd17385945

自己动手做聊天机器人 二十五-google的文本挖掘深度学习工具word2vec的实现原理 URL

word2vec 原理 一 CBOW Skip-Gram URL

word2vec 原理 二 Hierachical Softmax URL

word2vec 原理 三 Negative Sampling URL

搜狐新闻参考 AirBNB SGNS URL

本文有帮助?