基于word2vec协同过滤推荐

引言

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

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

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

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 (Continuous Bag-of-Words) ,如下图所示。

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

输入层

假设窗口=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,按以上方法递归地求得输出

代价函数

L=wClogj=2lw[σ(xwTθj1w)]1djw[1σ(xwTθj1w)]djw\mathcal{L} = \sum_{w \in C}{\log \prod_{j=2}^{l^w}{[\sigma(x_w^T\theta_{j-1}^{w})]^{1-d_j^w} \cdot [1-\sigma(x_w^T\theta_{j-1}^w)]^{d_j^w}}}

=wCj=2lw{(1djw)log[σ(xwTθj1w)]+djwlog[1σ(xwTθj1w)]}=\sum_{w \in C} \sum_{j=2}^{l^w} \{(1-d_j^w) \cdot \log[\sigma(x_w^T\theta_{j-1}^w)]+d_j^w \cdot \log[1-\sigma(x_w^T\theta_{j-1}^w)]\}

wCj=2lw\sum_{w \in C} \sum_{j=2}^{l^w}

L(w,j)\mathcal{L}(w,j)

那么有

L(w,j)θj1w=θj1w{(1djw)log[σ(xwTθj1w)]+djwlog[1θj1w]}\frac { \partial \mathcal{L} (w,j)}{\partial \theta^w_{j-1}} =\frac {\partial} {\partial \theta_{j-1}^w} \{(1-d_j^w) \cdot \log [\sigma(x_w^T \cdot \theta_{j-1}^w)] + d_j^w \cdot \log {[1-\theta_{j-1}^w]} \}

=[1djwσ(xwTθj1w)]xw= [1-d_j^w - \sigma(x_w^T \theta_{j-1}^w)] x_w

于是 θ\theta 的更新公式

θj1w:=θj1w+η[1djwσ(xwTθj1w)]xw\theta_{j-1}^w:=\theta^w_{j-1} + \eta [1-d_j^w-\sigma(x_w^T\theta_{j-1}^w)]x_w

其中,η\eta 是学习速率

同理得 xx 的梯度公式

L(w,j)xw=[1djwσ(xwTθj1w)]θj1w\frac {\partial\mathcal{L}(w,j)}{\partial x_w}=[1-d_j^w-\sigma(x_w^T\theta_{j-1}^w)]\theta^w_{j-1}

因为 xwx_w 是多个单词向量 vv 的累加,所以每轮迭代每个单词向量 vv 的更新方法

v(w~):=v(w~)+ηj=2lwL(w,j)xw,w~Context(w)v(\tilde w):=v(\tilde w)+\eta \sum_{j=2}^{l^w} \frac {\partial \mathcal{L}(w, j)} {\partial x_w}, \tilde w \in Context(w)

Skip-gram

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

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

本文借助 gensim 包,完成上述的训练过程。

目标是

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

其中,(w,c)(w,c) 是目标词和上下文构成的组合。求参方法用 (Hierarchical) Softmax 或 Negative Sampling 优化。

p(cw;θ)=σ(wTθ)p(c|w;\theta)=\sigma(w^T \theta)

负采样

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

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

数据准备

准备 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
54
55
56
#!/usr/sbin/python3
#-*- encoding:utf-8 -*-
from gensim.models.word2vec import *

model_file = 'word2vec.model'

with open('user_prefs.txt') as f:
prefs_str = ''.join(f.readlines())

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

prefs = read_prefs(prefs_str)

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 flatMap(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():
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 flatMap(vocab):
print('\n根据 %s 推荐:' % item)
for item_score in model.most_similar(positive=[item]):
item, score = item_score
print('\t%s %.2f' % (item, score))

calc_item_cf()

输出结果

书籍的向量表示保存在 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

加速技巧

Hierarchical Softmax

用霍夫曼树结构的 Softmax 代替普通 Softmax,使得计算速度从 O(n)O(n) 提升到 O(log2n)O(log_2 n)

Negative Sampling

附录

完整源码 Github

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

word2vec 原理 一 CBOW Skip-Gram URL

word2vec 原理 二 Hierachical Softmax URL

word2vec 原理 三 Negative Sampling URL

本文有帮助?