Andante


  • 首页

  • 关于

  • 归档

关于机器翻译中的Attention机制

发表于 2018-01-20 | 分类于 Machine Learning

NMT

在Attention机制没有被发明前,最新的Neural Machine Translation(NMT)模型的结构为decoder + encoder的形式。其中encoder负责将原始文本序列的信息压缩成一个fixed-length的向量,decoder负责在给定encoder的向量及转换文本序列当前词的情况下,预测下一个词的概率。可以数学描述为如下过程:

encoder的输出:

其中$T_x$为输出文本的长度。decoder输出的条件概率:

这里的$f$都是一个关于RNN的非线性函数,例如LSTM或Bi-LSTM。

Attention

实际上,Attention机制引入的insight是由上面的建模方法的一些不足而来的。假设现在要对[I love you]做一次翻译任务,中文结果为[我爱你],那么由上面的结构encoder的输出是包含[I],[love],[you]三个词信息量的向量,当decoder来了之后,[我]、[爱]、[你]三个字的翻译都是由这一个向量作为输入,而我们人类的直觉其实应该是:当翻译[我]的时候,[I]这个词的权重权重应该更大,而其他两个词[爱]和[你]的时候,也应该对应的是[love]和[you]的大权重。因此,从这点考虑出发,encoder的输出对于每一个待翻译的词不应该都是一致的,反之应该是一种动态的,对原始文本每个词(或者理解为time step)输出的向量进行加权的形式,这种机制在人类认知里就可以解释为[注意力]机制,即对不同词翻译的时候我们注意力集中的位置也不一样,于是就有了attention的由来,上面的数学描述就发生了一些变化。由于encoder需要对于不同词产生不同的向量,encoder的输出就变为

这里$i$,$j$分别表示的是翻译文本和原始文本的词下标。这样,对于翻译文本的每个词,所产生的向量就是一种加权的形式,暂且先不理会这个权重$\alpha$是怎么来的,先看decoder的变化

其实可以看到,本质上和上面的decoder根本没发生变化,只是将$c$变成了与词下标有关的$c_i$。

最后看$\alpha$是怎么得到的,从下标上可以发现,最终得到的$\alpha$应该是一个矩阵,大小为原始文本长度*翻译文本长度,它表示的是原始文本中第$j$个词对翻译文本中第$i$个词的权重大小(重要程度),具体计算方法为

函数$a$可以定义为一个full-conneted layer,随着整个网路一起训练,物理意义上是将翻译文本第$i-1$位的翻译词输出的向量与原始文本第$j$个原始词输出向量整合到一起做了一次操作,反映了在准确的翻译成第$i$个目标词的前提下,后者对前者的重要度。

The probability $\alpha_{ij}$, or its associated energy $e_{ij}$, reflects the importance of the annotation $h_j$ with respect to the previous hidden state $s_{i-1}$ in deciding the next state $s_i$ and generating $y_i$.

更一般的,$e$的计算方法表达为

至此,基本的attention方法就结束了,下面一张图可能把整个流程描述的更清楚。值得一提的是,个人理解,attention机制的引入应该对双向RNN的依赖更强,因为翻译任务中某个待翻译词是对原始文本中特定位置的上下文敏感的,因此$h_j$中不仅要包含有上文的信息,也要包含有下文信息。

参考资料

  1. NMT-Keras
  2. NMT-TensorFlow
  3. Neural Machine Translation by Jointly Learning to Align and Translate

2017中国城市空气质量指数(aqi)爬取及分析

发表于 2018-01-17 | 分类于 Data Analysis

引言

2016是我来北京第一年,这年年末和2017年的年初,雾霾在这个城市肆虐了一个冬天,还记得那时候每天出门都要带着口罩,卡着眼镜戴的特别难受。在公司里大家也时不时的讨论着空气质量、哪款口罩质量比较好、中午要不要出去吃饭等等与雾霾有关的话题。本科四年在哈尔滨,那时候冬天的雾虽然存在,但也没有让人感到窒息,研究生在武汉的三年也未见满大街都带着口罩的壮观景象。所以当时的想法是,连基本的生存都要成问题,还谈何工作、生活?既然无法改变空气质量,那可以做的就是选择一个空气质量说得过去的城市。于是选择了十个城市,在空气质量网站上爬了2017年连续一年的空气质量,分别是北京、上海、杭州、珠海、深圳、广州、成都、苏州、厦门、哈尔滨。这些城市主要是以经济发展空间这个维度来选择的,个人认为也是我们这代人比较有倾向性的久居城市(哈尔滨是hometown,纯属关心;武汉呆了几年,觉得以后再也不会去了)。
爬虫代码在服务器上crontab每五分钟一次定时执行,最终结果最小粒度为小时,个别时间因为不明原因数据缺失。

爬虫

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/bin/python
#-*-coding:utf-8 -*-
# crawl.py
import urllib
import re
def getHtml(url):
page = urllib.urlopen(url)
html = page.read()
return html
def getImg(html):
reg = r'src="(.+?\.png)" class'
reg_time = r'<h4>更新时间 (.+?)</h4>'
imgre = re.compile(reg)
titlere = re.compile(reg_time)
imgurl = re.findall(imgre,html)[0]
title = re.findall(titlere, html)[0][0:13]
urllib.urlretrieve(imgurl,'./aqi/%s.png' %title)
def getaqi(html, file):
reg = r'aqi-bg aqi-level-(.+?)\">(.+?)</span>'
reg_time = r'label label-info\">(.+?)发布</span>'
aqire = re.compile(reg)
timere = re.compile(reg_time)
aqiurl = re.findall(aqire,html)[0][1]
index = aqiurl.split(" ")[0]
quality = aqiurl.split(" ")[1]
time = re.findall(timere, html)[0].replace("年","-").replace("月","-").replace("日","")
with open(file, 'r') as f:
lines = f.readlines()
last_line = lines[-1]
if (last_line[0:16] != time):
with open(file, "a") as f:
f.write('\n' + time + '\t' + index + '\t' + quality)
if __name__ == '__main__':
#get img
htmlimg = getHtml("http://www.air-level.com/")
getImg(htmlimg)
#get aqi
cityarr = ("shanghai","beijing","hangzhou","zhuhai","shenzhen","guangzhou","chengdu","haerbin","suzhou","xiamen")
for i in cityarr:
htmlaqi = getHtml("http://www.air-level.com/air/%s/" %i)
getaqi(htmlaqi, "./aqi/%s" %i)

分析

1
2
3
4
5
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
1
city_lst = ["beijing", "chengdu", "guangzhou", "haerbin", "hangzhou", "shanghai", "shenzhen", "suzhou", "xiamen", "zhuhai"]
1
2
3
4
5
6
7
8
9
10
11
12
13
df_lst = []
for city in city_lst:
tmp = pd.read_table(city, names = ["tm", "aqi_" + city, "lv_" + city])
tmp["tm"] = pd.to_datetime(tmp["tm"].apply(lambda x: x + ":00"))
df_lst.append(tmp)
timeIndex = pd.date_range("2017-01-07 00:00", "2018-01-06 23:00", freq="H")
timeIndex = timeIndex.to_series().to_frame().reset_index(drop = True)
timeIndex.columns = ["tm"]
df = pd.merge(timeIndex, df_lst[0], how = 'left', on = 'tm')
for i in range(1, len(city_lst)):
df = pd.merge(df, df_lst[i], how = 'left', on = 'tm')
1
2
3
4
5
6
7
8
# hour to day (daily mean)
def day_scale(df):
df_scale = pd.DataFrame()
df["date"] = df.tm.dt.date
df_scale["date"] = df["date"].unique()
for col in [col for col in df.columns.tolist() if "aqi" in col]:
df_scale[col] = df.groupby(["date"])[col].mean().reset_index()[col]
return df_scale
1
2
3
4
5
6
7
8
# hour to month (monthly mean)
def month_scale(df):
df_scale = pd.DataFrame()
df["month"] = df.index.map(lambda x: str(int(x / df.shape[0] * 12) + 1))
df_scale["month"] = df["month"].unique()
for col in [col for col in df.columns.tolist() if "aqi" in col]:
df_scale[col] = df.groupby(["month"])[col].mean().reset_index()[col]
return df_scale
1
2
3
4
5
# hour scale
plt.figure(figsize=[20,10])
for city in city_lst:
plt.plot(df["tm"], df["aqi_" + city])
plt.legend(prop={'size':16})
1
2
3
4
5
# day scale
plt.figure(figsize=[20,10])
for city in city_lst:
plt.plot(day_scale(df)["date"], day_scale(df)["aqi_" + city])
plt.legend(prop={'size':16})
1
2
3
4
5
# month scale
plt.figure(figsize=[20,10])
for city in city_lst:
plt.plot(month_scale(df)["month"], month_scale(df)["aqi_" + city])
plt.legend(prop={'size':16})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Moving Average
df_day = day_scale(df)
df_day.fillna(df_day.median(axis = 0), inplace=True) #should be no none value when using moving average method
plt.figure(figsize=[20,10])
plt.title("30 days moving average", fontsize = 18)
for city in city_lst:
plt.plot(df_day["date"], df_day["aqi_" + city].to_frame().rolling(30).mean())
plt.legend(city_lst, prop = {'size':16})
plt.figure(figsize=[20,10])
plt.title("60 days moving average", fontsize = 18)
for city in city_lst:
plt.plot(df_day["date"], df_day["aqi_" + city].to_frame().rolling(60).mean())
plt.legend(city_lst, prop = {'size':16})
1
2
3
4
plt.figure(figsize=[20,10])
sns.boxplot(df[[col for col in df.columns if "aqi" in col]])
plt.ylim([0,200])
plt.xticks(fontsize = 16)
1
2
lvIndex = pd.DataFrame(df.lv_beijing.dropna().unique()).reset_index(drop = True)
lvIndex.columns = ["lvIndex"]
1
2
3
4
5
6
7
8
df_lv = df["lv_" + city_lst[0]].value_counts().reset_index()
for i in range(1, len(city_lst)):
df_lv = pd.merge(df_lv, df["lv_" + city_lst[i]].value_counts().reset_index(), how = 'left', on = 'index')
df_lv = df_lv.fillna(0)
columns = df_lv["index"].map({"优":"lv_0", "良":"lv_1", "轻度污染":"lv_2", "中度污染":"lv_3", "重度污染":"lv_4", "严重污染":"lv_5", "极度污染":"lv_6"})
df_lv = df_lv.transpose().drop(["index"])
df_lv.columns = columns
df_lv.plot(kind = 'bar', stacked = True, figsize = [20,10], fontsize = 16)

结论:

  1. 滑动平均结果显示,夏季空气质量优于冬季
  2. 箱型图显示,南方沿海城市空气质量优于北方,且异常值很少且不会很异常,即,再差也差不到哪去
  3. 北京2017年冬季空气质量有明显好转,与相关政策干预河北省有关
  4. 哈尔滨和北京半斤八两,夏季哈尔滨是赢家,冬季北京是赢家。极度恶劣空气状况频率哈尔滨是赢家
  5. 在这个政策主导和经纬度跨度极大的国家,空气质量受到很多因素的影响,过去一年的统计结果未来未必依然具有说服力

(待更新)
全年中国空气质量分布图的动态展示

Deep and Cross Network原理及实现

发表于 2017-12-14 | 分类于 Paper

Deep & Cross Network for Ad Click Predictions,是四位在谷歌的中国人放出的一篇文章。题目也一目了然,是用复杂网络结构做CTR预估的,与2016年谷歌的wide and deep model非常相似,于是利用课余时间梳理并简单的实现了一下。

原理

Input

我们从wide and deep model切入,不论是wide侧还是deep侧,在输入端为embedding feature + cross feature + continous_feature。而这里就会存在一个小问题,在构造cross feature时,我们依然需要依靠hand-craft来生成特征,那么哪些分类特征做cross,连续特征如何做cross,都是需要解决的小麻烦。而来到了deep and cross model,则免去了这些麻烦,因为在输入层面,只有embedding column + continuous column,feature cross的概念都在网络结构中去实现的。

Cross Network

将输入的embedding column + continous column定义为$x_0$($x_0 \in R^d$),第$l+1$层的cross layer为

其中$w_l(w_l \in R^d)$和$b_l(b_l \in R^d)$为第$l$层的参数。这么看来,Cross这部分网络的总参数量非常少,仅仅为$layers * d * 2$,每一层的维度也都保持一致,最后的output依然与input维度相等。另一方面,特征交叉的概念体现在每一层,当前层的输出的higher-represented特征都要与第一层输入的原始特征做一次两两交叉。至于为什么要再最后又把$x_l$给加上,我想是借鉴了ResNet的思想,最终模型要去拟合的是$x_{l+1} - x_{l}$这一项残差。

Deep Network

这部分就不多说了,和传统DNN一样,input进来,简单的N层full-connected layer的叠加,所以参数量主要还是在deep侧。

Output

Cross layer和Deep layer出来的输出做一次concat,对于多分类问题,过一个softmax就OK了。

实现

在实现上主要利用了Keras的Functional API Model,可以比较方便的自定义layer,数据集利用了文章中提到的Forest coverType数据,layer数及nueron数也都根据文章中写死,但有所不同的是文章中貌似没有对categorical feature做embedding ,而数据集中有一个cardinality为40的categorical feature,所以代码里对这个变量做了embedding,embedding维度也按文章中的公式指定。其他超参也所剩无几,几乎都是些weights的初始化方法。最后放上生成的网络结构及代码。

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
import numpy as np
import pandas as pd
import keras.backend as K
from keras import layers
from keras.layers import Dense
from keras.optimizers import Adam
from keras.layers import Input, Embedding, Reshape, Add
from keras.layers import Flatten, merge, Lambda
from keras.models import Model
from keras.utils.vis_utils import plot_model
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.metrics import accuracy_score
import random
# similar to https://github.com/jrzaurin/Wide-and-Deep-Keras/blob/master/wide_and_deep_keras.py
def feature_generate(data):
data, label, cate_columns, cont_columns = preprocessing(data)
embeddings_tensors = []
continuous_tensors = []
for ec in cate_columns:
layer_name = ec + '_inp'
# For categorical features, we em-bed the features in dense vectors of dimension 6×(category cardinality)**(1/4)
embed_dim = data[ec].nunique() if int(6 * np.power(data[ec].nunique(), 1/4)) > data[ec].nunique() \
else int(6 * np.power(data[ec].nunique(), 1/4))
t_inp, t_build = embedding_input(layer_name, data[ec].nunique(), embed_dim)
embeddings_tensors.append((t_inp, t_build))
del(t_inp, t_build)
for cc in cont_columns:
layer_name = cc + '_in'
t_inp, t_build = continous_input(layer_name)
continuous_tensors.append((t_inp, t_build))
del(t_inp, t_build)
inp_layer = [et[0] for et in embeddings_tensors]
inp_layer += [ct[0] for ct in continuous_tensors]
inp_embed = [et[1] for et in embeddings_tensors]
inp_embed += [ct[1] for ct in continuous_tensors]
return data, label, inp_layer, inp_embed
def embedding_input(name, n_in, n_out):
inp = Input(shape = (1, ), dtype = 'int64', name = name)
return inp, Embedding(n_in, n_out, input_length = 1)(inp)
def continous_input(name):
inp = Input(shape=(1, ), dtype = 'float32', name = name)
return inp, Reshape((1, 1))(inp)
# The optimal hyperparameter settings were 8 cross layers of size 54 and 6 deep layers of size 292 for DCN
# Embed "Soil_Type" column (embedding dim == 15), we have 8 cross layers of size 29
def fit(inp_layer, inp_embed, X, y):
#inp_layer, inp_embed = feature_generate(X, cate_columns, cont_columns)
input = merge(inp_embed, mode = 'concat')
# deep layer
for i in range(6):
if i == 0:
deep = Dense(272, activation='relu')(Flatten()(input))
else:
deep = Dense(272, activation='relu')(deep)
# cross layer
cross = CrossLayer(output_dim = input.shape[2].value, num_layer = 8, name = "cross_layer")(input)
#concat both layers
output = merge([deep, cross], mode = 'concat')
output = Dense(y.shape[1], activation = 'softmax')(output)
model = Model(inp_layer, output)
print(model.summary())
plot_model(model, to_file = 'model.png', show_shapes = True)
model.compile(Adam(0.01), loss = 'categorical_crossentropy', metrics = ["accuracy"])
model.fit([X[c] for c in X.columns], y, batch_size = 256, epochs = 10)
return model
def evaluate(X, y, model):
y_pred = model.predict([X[c] for c in X.columns])
acc = np.sum(np.argmax(y_pred, 1) == np.argmax(y, 1)) / y.shape[0]
print("Accuracy: ", acc)
# https://keras.io/layers/writing-your-own-keras-layers/
class CrossLayer(layers.Layer):
def __init__(self, output_dim, num_layer, **kwargs):
self.output_dim = output_dim
self.num_layer = num_layer
super(CrossLayer, self).__init__(**kwargs)
def build(self, input_shape):
self.input_dim = input_shape[2]
self.W = []
self.bias = []
for i in range(self.num_layer):
self.W.append(self.add_weight(shape = [1, self.input_dim], initializer = 'glorot_uniform', name = 'w_' + str(i), trainable = True))
self.bias.append(self.add_weight(shape = [1, self.input_dim], initializer = 'zeros', name = 'b_' + str(i), trainable = True))
self.built = True
def call(self, input):
for i in range(self.num_layer):
if i == 0:
cross = Lambda(lambda x: Add()([K.sum(self.W[i] * K.batch_dot(K.reshape(x, (-1, self.input_dim, 1)), x), 1, keepdims = True), self.bias[i], x]))(input)
else:
cross = Lambda(lambda x: Add()([K.sum(self.W[i] * K.batch_dot(K.reshape(x, (-1, self.input_dim, 1)), input), 1, keepdims = True), self.bias[i], input]))(cross)
return Flatten()(cross)
def compute_output_shape(self, input_shape):
return (None, self.output_dim)
# modify the embedding columns here
def preprocessing(data):
# inverse transform one-hot to continuous column
df_onehot = data[[col for col in data.columns.tolist() if "Soil_Type" in col]]
#for i in df_onehot.columns.tolist():
# if df_onehot[i].sum() == 0:
# del df_onehot[i]
data["Soil"] = df_onehot.dot(np.array(range(df_onehot.columns.size))).astype(int)
data.drop([col for col in data.columns.tolist() if "Soil_Type" in col], axis = 1, inplace = True)
label = np.array(OneHotEncoder().fit_transform(data["Cover_Type"].reshape(-1, 1)).todense())
del data["Cover_Type"]
cate_columns = ["Soil"]
cont_columns = [col for col in data.columns if col != "Soil"]
# Feature normilization
scaler = StandardScaler()
data_cont = pd.DataFrame(scaler.fit_transform(data[cont_columns]), columns = cont_columns)
data_cate = data[cate_columns]
data = pd.concat([data_cate, data_cont], axis = 1)
return data, label, cate_columns, cont_columns
if __name__ == "__main__":
# data download from https://www.kaggle.com/uciml/forest-cover-type-dataset/data
data = pd.read_csv("./data/covtype.csv")
X, y, inp_layer, inp_embed = feature_generate(data)
#random split train and test by 9:1
train_index = random.sample(range(X.shape[0]), int(X.shape[0] * 0.9))
test_index = list(set(range(X.shape[0])) - set(train_index))
model = fit(inp_layer, inp_embed, X.iloc[train_index], y[train_index, :])
evaluate(X.iloc[test_index], y[test_index, :], model)

Capsule Networks

发表于 2017-11-14 | 分类于 Paper

Hinton上个月在axiv上甩出了一篇文章Dynamic Routing Between Capsules,作者在结构设计上弥补了一些cnn的设计缺陷。虽然作为行业领军人物,研究具有一定权威性,但也依然需要时间的考验和同行的不断推敲。

Capsules

在与cnn相关的研究中,这些年流行的网络框架都是基于conv+pooling的这种方式搭建的,即使是微软Resnet中引入了residual的概念,子网络依然是以这种方式组织起来。cnn中神经元的概念,无论是否共享权重,其实就是featureMap中的每一个具体的像素,是个标量。而capsules的将神经元扩展了一个维度,变成了一个向量,所以我们可以将capsules简单的理解为向量版的神经元。从网络上来说,cnn的forward过程是神经元与神经元的不断传播关系,而Capsules Network,显然,就是向量神经元之间的传播。

Model Structure

以mnist数据集为例,step by step看model的每个环节都是怎样的输出(第一个维度为batch_size)

Layer One

Input: $[None,28,28,1]$

经过一层$9*9$,strides为1,通道数为256的conv layer

Output:[None,20,20,256]

Layer Two

Input:$[None,20,20,256]$

经过一层$9*9$的,strides为2,通道数为128的conv layer,按照这种方式的setting下,输出应为$[None,6,6,32]$。但如果我们将这一层的conv layer复制8份,输出就变成了$[None,6,6,8,32]$,如果我们再将这个结果reshape到$[None,6*6*32,8] = [None,1152,8]$的维度,此时capsules的概念就可以体现出来了,即,这一层共有1152个$[1*8]$的capsules向量。

Output:$[None,1152,8]$

Layer Three

Input:$[None,1152,8]$

这一层在维度上的变化略微复杂。对于一个指定index为$i$的$capsule_{i}$,其维度为$[1*8]$,引入一个维度为$[8*16]$的$Weight_{ij}$,那么

上面的式子输出维度为$[1*16]​$,而$i\in(1,1152)​$,$j\in(1,10)​$,所以共有$1152*10​$个$\hat{capsule_{j|i}}​$。直观上的理解是,上面的操作我们令其进行10次,一次操作代表一个类别,加上这样的capsule一共存在1152个,所以$Weight​$的维度为$[1152,10,8,16]​$,所以截止在$\hat{capsule}​$的输出维度为$[None,1152,10,1,16]​$。

接着,再引入一个维度为$[1152,10,1,1]$的bias,首先对bias做一次按第二个维度(10)的softmax压缩,维度保持不变

可以看到,这里的$c_{ij}$就是一个标量,用它将所有capsules建立起联系,做一次对1152个capsules的sum,即

可以看到,由于是按$i$求和,因此维度与$\hat{capsule_{j|i}}$一致($[1*16]$),每一个$s_j$也都可以看成是capsule。由于Hinton在文中提到,capsule的模长表示概率,方向表示属性(真的很难理解。。),因此在输出层之前还要将每个capsule做一次非线性压缩

Output:$[None,10,16]$

另外可以注意到,文章的题目中有dynamic routing的关键字,这个思想也穿插在上面的pipeline里。从公式2到公式4,可以概化为一个公式,

这里其实我们只进行了一次上面的操作,即将layer1的capsule transform到layer2的capsule,所以衍生出可以进行多次循环来获取更高level的capsule,而这个循环capsule高阶表达的过程就成为routing。具体的做法就是不断通过输出的高层capsule回传到低层capsule,从而更新bias(只更新bias,其余参数不更新),即

剥离上面公式的维度信息,发现$\hat{capsule_{j|i}} v_j$与$v_{ij}$均为$[16*1]$的向量,而$bias_{ij}$为一个标量,完全match,具体routing流程见下图。

在这也顺便粘上一句原文的话,说明的是每个layer之间的capsule究竟是用来干嘛的,可以好好体会一下,就一句话已经看的云里雾里了。

In convolutional capsule layers, each capsule outputs a local grid of vectors to each type of capsule in the layer above using different transformation matrices for each member of the grid as well as for each type of capsule.

Layer Four

Input:$[None,10,16]$

上面已经说到了,capsule的模长表示概率,因此直接将上层输入做一次范数即可,输出的维度即为所有类别总数

Output:$[None,10]$

Reconstruction Layer

Input:$[None,10,16]$

上面所描述的layer均为正向预测的过程,而通过top layer的capsule还原回原始图片质量的好坏也是能证明模型表达能力的一种评价方式,所以作者又引入了reconstruction layer,来对图像进行重构。首先将Layer Four中的输入拿过来,用真实class对张量进行一次掩模,只保留真实类别对应的vector,维度为$[None,16]$,然后连续接上三个Full Connected Layer,大小分别为512,1024和728,激活函数分别为ReLU,ReLU和Sigmoid,最后再讲728维向量reshape到$[28*28]$,从而还原了原始图像。

Output:$[None,28,28,1]$

Loss Defination

说完了上面的结构,应该可以清晰的看出来,loss由两部分构成,一部分为网络正向传播上层capsule模预测为哪个class的loss-1,另一部分为重构网络还原的图片与真实图片的差异loss-2。其中loss-1使用类似于svm的margin loss,加入了一些小trick,而loss-2则使用的是重构与真实pixel-to-pixel的平方损失。形式为

其中$T_{k} \in (0,1)$表示图像中是否存在第k个类(这里不太一样的是,学习的图片里允许有多个类别,文章在实验中也加入两个类别分离的实验),$m^+$与$m^-$分别表示类比预测的upperBound和lowerBound,引入$\lambda(==0.5)$的作用是为了减弱不存在类capsule在初始阶段的学习,以防止对其他的capsule影响过大?(没太理解),$x_{ri}$和$x_{ti}$分别表示第$i$个重构像素与真实像素。为了尽量不影响正向构造capsule的能力,重构loss稀疏$\alpha$在文章中取得极小(0.0005)。

Implementation

Keras版本:https://github.com/XifengGuo/CapsNet-Keras

Layer的模型参数

Qustions

  1. squashing的形式为何是这样?
  2. 多次routing的作用是什么?
  3. 为什么采用margin loss?
  4. 包含有capsule的网络结构中,激活函数在哪里?
  5. cifar数据集表现不佳的原因?

Attentional FM及源码解析

发表于 2017-09-18 | 分类于 Machine Learning

原理部分

书接上回,FM模型无论是对于交叉特征的捕捉能力上,还是在工业界大规模数据下的运用方面上,都具有出色的表现。因此,在深度学习火热的大背景下,各种基于FM模型和神经网络模型相结合的方法也开始频频出现。这里要说的是SIGIR 2017和IJCAI 2017的两篇文章Neural Factorization Machine和Attentional Facotrization Machine,无一例外,它们都是在FM的基础上衍生出与深度学习相关的研究。由于两篇文章的是同一团队所著,所以工作有重叠和改进部分,前一篇网络结构相对简单,因此本文主要针对Attentional Facotrization Machine展开,但也会在其中涉及Neural Factorization Machine。不仅如此,作者也同样给出了开源实现,源码解析也会在此基础上进行(Neural FM and Attention FM)。

FM模型虽然存在了所有可能的二阶特征交叉项,但本质上,并不是所有的特征都是有效的,比如一个垃圾特征a和一个牛逼特征b做交叉可能会起到一些作用,但a和垃圾特征c再做交叉基本上不会有任何作用,因此Attentional Facotrization Machine解决的问题主要就是——利用目前比较流行的attention的概念,对每个特征交叉分配不同的attention,使没用的交叉特征权重降低,有用的交叉特征权重提高,加强模型预测能力。原始的FM模型,如果忽略一阶项,可以表示为

这里我们可以把$v_i$理解为特征$x_i$的feature embedding,而上式中交叉项的表达为embedding向量的内积,因此输入的$y$直接就为标量,不需要再次的映射;但是这种内积操作如果变换为哈达玛积的操作(element-wise product)$(v_i \odot v_j) x_i x_j$,这样从Neural Network的角度去看,每个特征都交叉之后上层就变成了一个embedding layer,然后再对这个layer做一次sum pooling和映射,便可以得到输出值,即

至此,Neural Factorization Machine中所描述的已经叙述完毕,就是这么简单。而来到了Attention这篇,思想一致,只是在embedding layer的sum pooling时为每个embedding加上对应的attention,也就是权重,即

其中$\alpha$通过对embedding空间做了一次encoding并标准化得到,

另外,由于这种attention的方式对训练数据的拟合表达更充分,但也更容易过拟合,因此作者除了在loss中加入正则项之外,在attention部分加入了dropout。具体模型结构如下图:

作者在Frappe和MovieLens数据集上进行了实验,AFM的表现beat掉了所有fine tuned的对比模型(LibFm、HOFM、Wide&Deep、DeepCross)。而且,使用这套框架的传统FM也要比LibFM的效果好,原因之一在于dropout的引入降低了过拟合的风险,第二在于LibFM在迭代优化时使用的是SGD方法,对每个参数的学习步长一致,很容易使后期的学习很快的止步,而Adagrad的引入使得学习率可变,因此性能更加优良。另外,attention的引入,一方面加速了收敛速度,另一方面也具有对交叉特征的可解释性(因为就是权重),类似于特征重要性。

代码部分

截取了一部分关键代码,解析在注释中体现

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
def _init_graph(self):
self.graph = tf.Graph()
with self.graph.as_default():
# Input data.
self.train_features = tf.placeholder(tf.int32, shape=[None, None], name="train_features_afm") # None * features_M
self.train_labels = tf.placeholder(tf.float32, shape=[None, 1], name="train_labels_afm") # None * 1
self.dropout_keep = tf.placeholder(tf.float32, shape=[None], name="dropout_keep_afm")
# Variables.
self.weights = self._initialize_weights()
# Model.
# _________ Embedding part _____________
# data为libsvm格式,且非none特征共有M‘个,且特征值均为1。data中每个feature都对应一个K维embedding
self.nonzero_embeddings = tf.nn.embedding_lookup(self.weights['feature_embeddings'], self.train_features) # None * M' * K
# 交叉项embedding layer生成,对应equation(2)
element_wise_product_list = []
count = 0
for i in range(0, self.valid_dimension):
for j in range(i+1, self.valid_dimension):
element_wise_product_list.append(tf.multiply(self.nonzero_embeddings[:,i,:], self.nonzero_embeddings[:,j,:]))
count += 1
self.element_wise_product = tf.stack(element_wise_product_list) # (M'*(M'-1))/2 * None * K
self.element_wise_product = tf.transpose(self.element_wise_product, perm=[1,0,2], name="element_wise_product") # None * (M'*(M'-1))/2 * K
# _________ MLP Layer / attention part _____________
# self.attention == True时,模型为Attention FM;
# self.attention == False时,就退化为Neural FM
num_interactions = self.valid_dimension*(self.valid_dimension-1)/2
if self.attention:
# 对应equation(4)-1
self.attention_mul = tf.reshape(tf.matmul(tf.reshape(self.element_wise_product, shape=[-1, self.hidden_factor[1]]),self.weights['attention_W']), shape=[-1, num_interactions, self.hidden_factor[0]])
self.attention_exp = tf.exp(tf.reduce_sum(tf.multiply(self.weights['attention_p'], tf.nn.relu(self.attention_mul + self.weights['attention_b'])), 2, keep_dims=True)) # None * (M'*(M'-1)) * 1
self.attention_sum = tf.reduce_sum(self.attention_exp, 1, keep_dims=True) # None * 1 * 1
# 对应equation(4)-2
self.attention_out = tf.div(self.attention_exp, self.attention_sum, name="attention_out") # None * (M'*(M'-1)) * 1
# 在attention部分引入dropout
self.attention_out = tf.nn.dropout(self.attention_out, self.dropout_keep[0]) # dropout
self.AFM = tf.reduce_sum(tf.multiply(self.attention_out, self.element_wise_product), 1, name="afm") # None * K
else: # Neural FM
self.AFM = tf.reduce_sum(self.element_wise_product, 1, name="afm") # None * K
# 在全连接(映射)部分二次引入dropout
self.AFM = tf.nn.dropout(self.AFM, self.dropout_keep[1]) # dropout
# _________ out _____________
self.prediction = tf.matmul(self.AFM, self.weights['prediction']) # None * 1
# 引入传统FM中的一阶项和常数项
Bilinear = tf.reduce_sum(self.prediction, 1, keep_dims=True) # None * 1
self.Feature_bias = tf.reduce_sum(tf.nn.embedding_lookup(self.weights['feature_bias'], self.train_features) , 1) # None * 1
Bias = self.weights['bias'] * tf.ones_like(self.train_labels) # None * 1
self.out = tf.add_n([Bilinear, self.Feature_bias, Bias], name="out_afm") # None * 1
# Compute the loss.
# 在loss中加入L2正则项
self.loss = tf.nn.l2_loss(tf.subtract(self.train_labels, self.out)) + tf.contrib.layers.l2_regularizer(self.lamda_attention)(self.weights['attention_W']) # regulizer
# Optimizer.
# 传统FM为SGD方式迭代训练,可能会导致训练不充分的情况。Ada系列的迭代方式learning rate可变,会一定程度上缓解这个现象
if self.optimizer_type == 'AdamOptimizer':
self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate, beta1=0.9, beta2=0.999, epsilon=1e-8).minimize(self.loss)
elif self.optimizer_type == 'AdagradOptimizer':
self.optimizer = tf.train.AdagradOptimizer(learning_rate=self.learning_rate, initial_accumulator_value=1e-8).minimize(self.loss)
else:
self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate).minimize(self.loss)
# init
self.saver = tf.train.Saver()
init = tf.global_variables_initializer()
self.sess = self._init_session()
self.sess.run(init)

FM and FFM

发表于 2017-07-14 | 分类于 Machine Learning

在CTR预估中,LR模型有着举足轻重的地位,而近几年FM(Factorization Machine)和FFM(Field Factorization Machine)开始在各大比赛和互联网公司广泛运用并取得了不俗的成绩和效果。实际上,FM算是LR的扩展,FFM算是FM的扩展,或者说FMM的一种特例是FM,而FM的一种特例就是LR,下面主要围绕着这两个算法的原理部分展开介绍。

FM

相对于LR模型来说,FM就是考虑了每个特征之间的交互关系,而有些时候在实际场景中这些特征交互往往起到了关键性作用,这也是为什么现在深度学习火的原因之一。打个比方,在广告推荐场景下,一个男性用户可能会有2%的概率点击一条足球属性的广告,而一个年龄在20~40岁之间的男性用户可能会有3%的概率点击,更进一步,满足上面条件的用户在一个给定的上下文条件下,比如世界杯期间、傍晚、甚至在搜索query高度相关的条件下就会有10%的点击概率,这都说明了特征之间的交互作用会对最后的预测结果产生很重要的影响。回到LR模型,它只对每个特征单独赋予一个权重,即

如果把$\sigma$拿掉的话,只看内部的线性部分,FM模型所能带来的特征交互形式即为

实际上,仅仅依靠$w_{ij}$就可以完成对特征的交互,但这里对$W$矩阵的构造并没有采用直接学习的方式,而是借鉴了矩阵分解的形式,构成形式为由$V \cdot V^T$。这样做的好处有两点,第一点是学习的参数量会显著减少。由于在CTR预估的任务中,会存在非常sparse的特征,百万千万维都有可能,如果直接学习$w_{ij}$的参数量是$N^2$,而如果$V\in R^{N*k}$,学习的参数量就是$kN$,而一般来说$k$的选取并不会太大,一方面是为了减少计算量,另一方面则是为了考虑在高维稀疏特征条件下的泛化性能;第二点是如果直接学习$w_{ij}$,会存在“没什么可学的”的窘境,因为如果是采用SGD的优化方式,$w_{ij}$的更新会依赖那些交互项都不为0的特征,而这样的交互极少,参数可能会学习不充分,但以这种矩阵分解的间接构建$w_{ij}$更新参数就不会出现这种问题。

由于上式前半部分和LR一致,所以我们将重点放在后半部分特征交互项上,经过推导,最后一项可以化简为

这里虽然看上去很绕,但只要自己动手写一写就一目了然了。最最重要的是,上面的式子的计算复杂度是线性的$O(kN)$,相比较于直接计算$W$矩阵的复杂度$O(kN^2)$,明显要高效许多。在优化参数时,采用传统的SGD方法,式中各项参数的梯度可求

最后,假设对于一个具体的分类问题,最终带正则的优化目标就变成了

FFM

FFM是在FM的基础上引入了Field的概念。从上面的公式中可以看到,FM在处理$x_i$和$x_j$的特征交互时,所使用的隐向量$V$是$v_i$和$v_j$,而在处理$x_i$和$x_k$的特征交互时,所使用的隐向量$V$是$v_i$和$v_k$,这样的局限就是$x_i$和其他所有特征交互时用的都是一个隐向量$v_i$,而FFM对这个问题的处理方法就是引入field的概念,或者说把特征分组,比如年龄特征是一个field,性别特征是一个field,职业特征又是一个field,在FM模型中年龄与性别、职业特征交互项的隐因子$v_{age}$是一样的,但在FFM中性别职业的交互隐因子为$v_{age, sex}$,而性别职业的交互隐因子为$v_{age, prof}$。更一般地,之前的交互项系数的表达为<$v_i, v_j$>,而现在又加入了field这一因素,表达为<$v_{i, f_j}, v_{j, f_i}$>,也就是说在模型的表达式上只需要修改这一项就可以,其余都不变。在实际应用中,我们一般指定分类特征中的每一类特征为一个field,而连续特征每个都为一个field。

我们可以看到,加入field概念之后对特征的交互考虑得更加周全,不同类型的特征交互体现出了多样性,但是由于有这样一个因素的存在,使得我们模型的参数量也随之提高。FM的参数量为$kN$,又由于公式可以化简,因此计算复杂度由初始的$O(kN^2)$变为$O(kN)$,然而FM的参数量则变为了$kfN$,其中$f$为field个数,公式无法化简,计算复杂度为$O(kN^2)$。虽然说由于有field带来的多样性,在$k$的选取上FFM要比FM小,但总体上看FFM的学习速率还是要比FM慢一截。

从FM和FFM在公开数据集上的表现来看,FM和FFM各有千秋,但相比于原始未进行特征交互的LM和直接计算特征交互矩阵$W$的Poly2方法还是有一定程度的提升。

参考资料

Factorization Machines
Field-aware Factorization Machines for CTR Prediction

Tensorflow简介

发表于 2017-07-14 | 分类于 Machine Learning

接触Tensorflow也有好一段时间了,但总是对这个项目庞大的代码量和复杂的设计结构望而生畏,一直觉得它可以和某些原生的编程语言相提并论,所以大多时候也都是照着一些现成的模板去套用然后再不断调试,对内部的一些基本概念其实并没有太多了解。最近想着好好把官方给的get started和tutorial过一遍,对以后使用应该也更有帮助。

Tensors

张量就是高维数组的另一种表达形式,数组是几维的,张量的rank就是几。比如[1,2,3]是一维向量,rank=1;[[1,2,3], [2,3,4]]是二维矩阵,rank=2;[[[1,2,3], [2,3,4]]]是三维张量,rank=3,就这么简单。

Computational Graph

有了tensor,要形成flow,就必须借助图,张量和图起来就基本构成了Tensorflow的基本框架。图中的每个node可以是tensor,也可以是加减乘除之类的算子。其实回想一下,神经网络本质上就是这种神经元之间互相有向连接的结构,所以在我们定义好每个node后把这些node连接成图,理论上就可以搭建任何模式的结构。但是如果要查看某个图最终node的输出结果,还必须要通过一个session的运行环境来run。下面举个简单的小例子就会一目了然:

1
2
3
4
5
6
7
8
9
10
11
node1 = tf.constant(3.0, tf.float32)
node2 = tf.constant(4.0) # also tf.float32 implicitly
print(node1, node2)
#Output: Tensor("Const:0", shape=(), dtype=float32) Tensor("Const_1:0", shape=(), dtype=float32)
sess = tf.Session()
print(sess.run([node1, node2]))
#Output:[3.0, 4.0]
node3 = tf.add(node1, node2)
print("node3: ", node3)
print("sess.run(node3): ",sess.run(node3))
#Output:7.0

上面的code只是把node1和node2定义为tf.constant()的常量形式,更普遍的我们将输入定义为类似于函数形式的变量,这时就需要利用到tf.placeholder()方法,可以理解为占位符,这类似于python中lambda匿名函数的形式,占位符就是传入匿名函数的输入参数,再看例子:

1
2
3
4
5
6
7
8
9
10
a = tf.placeholder(tf.float32)
b = tf.placeholder(tf.float32)
adder_node = a + b # + provides a shortcut for tf.add(a, b)
print(sess.run(adder_node, feed_dict = {a: 3, b:4.5}))
#Output:7.5
print(sess.run(adder_node, feed_dict = {a: [1,3], b: [2, 4]}))
#Output:[ 3. 7.]
add_and_triple = adder_node * 3.
print(sess.run(add_and_triple, feed_dict = {a: 3, b:4.5}))
#Output:22.5

上面提到的tensor都是不可变,但是在大多machine learning的应用中需要通过不断的迭代来调整参数,比如网络中的weight和bias,这时需要利用tf.Variable()来给定初始值并定义这些需要train的变量。有了trainable的参数和输入数据,最后一步就是定义loss,我们以linear regression为例,定义square loss 为$loss = (Wx + b - \hat y)^2$,大多数常见的loss在Tensorflow中都已经被封装好,直接调用即可。接下来的代码相对来说复杂一点,但逻辑在Tensorflow的框架下十分清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
W = tf.Variable([.3], tf.float32) #Trainable param
b = tf.Variable([-.3], tf.float32) #Trainable param
x = tf.placeholder(tf.float32) #Input placeholder
y = tf.placeholder(tf.float32) #Input placeholder
linear_model = W * x + b # Linear Regression
squared_deltas = tf.square(linear_model - y) #Define square loss
loss = tf.reduce_sum(squared_deltas) #Add loss on all samples
init = tf.global_variables_initializer() # You must explicitly initilize the variables
sess.run(init) # Until we call sess.run, the variables are uninitialized
print(sess.run(loss, {x:[1,2,3,4], y:[0,-1,-2,-3]}))
#Output:23.66

Train

到现在为止,我们还只是简单的手动构建了图,做了几个简单运算,还没有涉及到神经网路的梯度反向传播、参数更新过程。Tensorflow的强大之处在于,在用户构建完成网络结构(图)后,就可以通过简单的函数调用来自动计算梯度。这里我们以最常见的梯度下降为例:

1
2
3
4
5
6
7
8
9
optimizer = tf.train.GradientDescentOptimizer(0.01) # define gradient descent method
train = optimizer.minimize(loss) # minize loss using gradient descent
sess.run(init) # reset values to incorrect defaults.
for i in range(1000): # run gradient descent for 1000 steps
sess.run(train, {x:[1,2,3,4], y:[0,-1,-2,-3]})
print(sess.run([W, b]))
#Output:[array([-0.9999969], dtype=float32), array([ 0.99999082],dtype=float32)]

Reading Data

Tensorflow读取数据的方式大体上说有三种,第一种是和上述例子中一样的方式,先在图中定义好placeholder,再通过feed_dict方式在run session时传入placeholder对应的数据;第二种适用于小数据量,直接将数据定义为constant或variable,个人感觉本质上和第一种没有太大区别;第三种在数据量比较大时候十分常用,就是直接从文件中按照队列的方式进行读取,以目前一般企业级机器学习的数据量来看,前两种方式会受限于内存和图的大小,因此简单介绍下这种比较通用的方式。

首先将要读取的小文件名形成一个列表,然后通过tf.train.string_input_producer函数建立一个关于这些文件名的队列queue。接下来会有若干个reader实体去从这个queue中读取文件名,然后对于不同形式的文件(比如csv、二进制等)再通过不同的decoder对当前文件名下的内容进行解析,这样就可以不断的从文件列表中的每个文件读数,下面是一个涉及到batch read的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def read_my_file_format(filename_queue):
reader = tf.SomeReader() # define reader
key, record_string = reader.read(filename_queue) # read file from file_queue using reader, each execution of read reads a single line from the file
example, label = tf.some_decoder(record_string) # decode file
processed_example = some_processing(example) # do some preprocessing
return processed_example, label
def input_pipeline(filenames, batch_size, num_epochs=None):
filename_queue = tf.train.string_input_producer(
filenames, num_epochs=num_epochs, shuffle=True)
example, label = read_my_file_format(filename_queue)
min_after_dequeue = 10000 # define how big a buffer we will randomly sample from
capacity = min_after_dequeue + 3 * batch_size # larger than min_after_dequeue
example_batch, label_batch = tf.train.shuffle_batch(
[example, label], batch_size=batch_size, capacity=capacity,
min_after_dequeue=min_after_dequeue) # make batch sample
return example_batch, label_batch

一般来说牵扯到了queue,我们就必须要在tf.Session()中在任务run之前利用tf.train.start_queue_runners和tf.train.Coordinator来填充队列和管理线程,不然read进程就会因为从queue中等待读取文件名而发生阻塞。

上面只是对tf做了一个非常非常浅显的介绍,但烂熟于心之后基本上也可以无障碍阅读github上大多数的tensorflow tutorial,而在实际的程序中依然有大量的坑和N多封装好的方法等待被发掘。

利用RNN做session-based推荐

发表于 2017-05-05 | 分类于 Paper

最近在做站内新用户推荐相关的项目,由于新用户历史行为相对较少,传统的neighborhood method和factor method models上对于user profile的利用少之又少,而如果当做一个传统的二分类问题,即预测购买/不购买,同样也存在相似的问题——特征不足,model能力上限有瓶颈,因此想着挖掘一下会话的time series pattern,从而变成一个基于session的预测问题,主要参考了ICLR 2016的一篇文章,个人感觉这篇文章可以算是利用RNN做基于session推荐比较早期的方法,而且作者也开源了基于theano的实现,后续一些improved版本,包括网易在考拉上的session推荐实践也都借鉴了这篇文章的一些思想,接下来对这篇文章进行一个简单的梳理。

文章中解决的问题是:给定一个session中的item点击流(click streaming events),来预测这个点击流后的下一个点击会是什么item,并最终由目标item完成推荐。需要注意的是,作者虽然用到了recsys 2015的数据来做model evaluation,但实际上并没有用到购买数据,因此这篇文章所解决的问题只是通过session的item序列模式挖掘可能感兴趣的item,并不是最终会发生购买行为的item。按照这个模型结构来看,输入数据就是某个session的当前状态,即前N个item click sequences所携带的序列信息,输出就是当前session第N+1个item。所以在一个点击流中的一个特定点击事件的网络结构可以表达为如下图所示,输入为已点击item的one-hot编码,经过一层embedding层来进行降维,再通过多层GRU(Gated Recurrent Unit)单元,最后通过一个前馈映射层来输出下一个item的likelihood。

简要描述一下GRU的原理(这里作者用GRU的原因是在GRU、LSTM和传统RNN单元中发现GRU效果最好)。传统RNN的hidden state数学表达为

也就是某一个时间点上的hidden state与当前时间点的输入和上一个时间点的hidden state有关,并且是二者线性组合的形式,但当时间序列过长时,会面临严重的梯度消失问题,而GRU和LSTM由于网络结构的变化可以有效的解决传统RNN中的时间维度梯度消失问题。其中GRU引入了update gate $z_t$和reset gate $r_t$的概念,两个gate控制当前时刻输入和上一时刻的记忆信息哪部分需要保留,哪部分需要丢弃,表达形式一致,分别表示为

而输出的hidden state为

在构造数据集时,文章采用了session后补齐再划分mini-batch的方法(如下图)。具体来讲,就是当一个session结束时,用一个新的session接到当前session的末尾,当这个事件发生时,hidden state就被重新置零。

虽然可以把上述问题作为一个多分类问题,但最后文章在实验中发现用cross-entropy做loss在100个左右hidden units时效果不错,但一旦units数量增大效果就不如ranking loss的形式,因此采用了pairwise ranking loss作为损失函数,并分别列举了BPR(Bayesian Personalized Ranking)和TOP1,其中某一个特定session中的一个点击预测loss为

,这里$\hat {r}_{S,j}$为第$j$个negative sample item的score,$\hat {r}_{S,i}$为postive sample item的score,值得注意的是这里对负样本做了一定程度的负采样,以消除一些用户可能感兴趣但并没有展示商品的影响。

实验在Recsys 2015数据集和另一个视频点击数据集上进行,以MRR@20和Recall@20作为指标评估,分别比较了不同mini-batch大小、dropout大小、learning-rate大小和momentum和loss形式下的效果,并与一些传统流行的推荐算法baseline进行对比,均有提升,细节不详谈,有兴趣的可以自己去看一下。

Multi-Rate Deep Learning for Temporal Recommendation解读

发表于 2017-04-10 | 分类于 Paper

在推荐系统里有一个比较重要的问题:如何能把一个user的长期兴趣和短期兴趣综合起来考虑进行内容推荐?一个人的兴趣总会随着时间发生变化,特定日期、事件都会使长期兴趣发生波动从而促生短期兴趣。一个比较直观的例子:如果在一个世界杯期间的新闻推荐场景下,推足球新闻很有可能比推荐长期兴趣的内容更使人满意。而目前的很多推荐方法都没有考虑与时间有关的短期信息,因此这篇文章主要针对将长短兴趣结合来提高推荐效果。

模型的基础利用deep semantic structured model (DSSM)。简言之,DSSM可以视为把从两个或多个角度所构建的神经网络模型整合到一个角度进行学习。假设一个两个角度的DSSM,其中一个网络表示query,另一个网络表示document,两个网络的输入可以是由各自角度所代表的特征(query-based features和document-based features),输出为比输入维度低的embedding vectors,而两个网络综合学习的目标是最大化两个输出vectors的cosine相似度。在具体的训练过程中,在每个mini-batch中随机负采样后再与正样本组合,然后再最小化正样本上的cosine loss(使正样本中的两个网络输出vectors最匹配)。在推荐场景里,可以认为其中一个网络是user的query history,即用户特征,另一个网络为系统中item的隠反馈,比如新闻的点击或是app的下载。

上面所说的是DSSM的基本概念,但是这里有一个问题,就是user相关的features都是不随时间发生改变的,于是作者引入了Temporal的概念,也就是将user的feautures进行拆分和细化,分为static和temporal,分别代表长期兴趣和短期兴趣。 下图可以很好的表示整个推荐系统的架构,其中item features和user static features全都利用全连接的神经网络构造embeddings,而user temporal features则利用LSTM构造embedding(文中的实验表明GRU的效果并不理想),然后将static和temporal的vectors通过函数$f$做成组合向量($f$可以是multiplication或concatenation),再与item的向量做cosine similarity生成一个user-item相似度,预测的时候取与user相似度最大的topK item进行推荐。(这里有个问题,如果考虑item的temporal features会是什么样?)

给出的优化目标为似然形式,即使给定user和时刻t时item概率最大,

这里概率依然还是由softmax得到,只不过用user的temporal+static向量和item向量之间的相似度来表现。

不过仔细想一下,模型在细节上还是有些问题,那就是如何选择时间窗口t。选大了,兴趣不够“短期”,选小了,模型参数太多,训练不来,因此作者又引入了multi-rate的概念,也就是选择几个窗口,分别代表短期兴趣和中短期兴趣,然后再训练不同的LSTM,称为“Fast-RNNs”和“Slow-RNNs”,然后将几个LSTMs用全连接层串到一起就OK了。不过这样RNN所带来的训练参数还是太多,文中采用的方法是在训练之前先用上文提到基本的DSSM做pre-train。

理论部分基本就是这些,就是在DSSM基础上引入了Temporal的概念——解决用户短期兴趣的问题,再引入了multi-rate的概念——对短期兴趣的粒度和模型训练效率做trade-off,因此称为MR-TDSSM。说实话,这篇文章并没有DSSM那篇惊艳,只能算是前者的进一步扩展,但实验结果确实很不赖,在新闻数据上多个指标上能够碾压传统推荐算法和DSSM。值得一提的是,实现工具是keras(一直以为微软的工程师都会用自己的轮子),一些传统方法的baseline用的都是LibRec,可以看到这个开源工具还是挺流行的。

Wide and Deep Learning for Recommender Systems解读

发表于 2017-03-22 | 分类于 Paper

Google在去年6月份在arxiv上放出了”Wide & Deep Learning for Recommender Systems”这篇文章,应用场景是Google Play上App安装预测,线上效果提升显著(相比较于Wide only和Deep only的model)。与此同时,Google也开源了这一模型框架并将其集成在Tensorflow的高级封装Tflearn中,今年开年的谷歌开发者大会上也专门有一个section是讲wide and deep model的,作者的意图也很明显,一方面推广tensorflow,另一方面是显示模型的强大。

通读文章后,发现其实模型的基本原理很简单,就是wide model + deep model。分别来讲,wide model就是一个LR,在特征方面除了原始特征外还有分类特征稀疏表达后的交叉特征,例如将分类特征做完one-hot后再进行cross product。个人理解,这里一方面是利用类似于FM模型原理来增强分类特征的特征交互(co-occurrence),另一方面是利用LR对高维稀疏特征的学习能力,而作者把wide model所具备的能力称为“memorization”;而deep model则是一个DNN,特征上除了原始特征还增加了分类特征的embedding,这个embedding在模型中属于独立的一层,embedding后的向量也是通过不断迭代学习出来的。将高维稀疏分类特征映射到低维embedding特征这种方式有助于模型进行“generalization”。Memerization和Generalization这两个概念中文还真没找到特别合适的诠释,如果非要翻译一下,我觉得应该是推理和演绎,一个是通过特征交互关系来训练浅层模型,另一个则是通过特征在映射空间中的信息训练深层模型。

模型结构采用joint的方式而非传统的ensemble方式。如果是ensemble方式,那么这两个模型就针对label进行单独训练,然后再加权到一起;而joint方式则是将这两个模型的输出加起来,然后再针对label进行联合训练。这样的好处是在train model的时候可以同时最优化两个model的参数,而且两个model可以起到互相补充的作用。下面的公式也很好的解释了wide and deep model的结构原理,即两个model的output在通过sigmoid函数之前把结果相加,然后再经过sigmoid实现分类。这里$x$指原始特征,$\phi(x)$分别表示wide模型的cross product feature和deep模型的embedding feature,而$w_{deep}$则泛指DNN各层weights和bias集合表示。

最终模型在离线评测上效果并不明显,但在在线评测上提升还算显著。前几天在电梯里听到广告部同事说他们也在搞这个模型,离线效果提升了10%+,但在线serving技术上目前比较头疼,但也不知道具体是什么指标提升了10%+。但在我们团队内部客户拉新预测问题的离线应用上,同样的数据效果只和xgboost持平。另外,官方提供的tutorial原生代码并不能很好的应用于大数据量,于是进行了改写,读取数据方式变成了队列读取,也是参考了stackoverflow上的一些反馈。核心就是wide_and_deep函数,cross product的实现方式是直接在预处理数据时对分类特征进行字符串拼接,然后再做one-hot,相对contrib中crossed_column的实现方式略显复杂,但以个人能力也只能先这样做,日后再去探索。另外,由于目前团队业务只涉及线下模型,因此对于模型的线上更新并没有太多关注,但大的技术框架来看应该也是用tf serving的方式实现。

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
170
171
172
173
174
import pandas as pd
import tensorflow as tf
import tensorflow.contrib.learn as tf_learn
import tensorflow.contrib.layers as tf_layers
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import LabelEncoder
from sklearn.cross_validation import train_test_split
import os
import numpy as np
from sklearn.metrics import roc_auc_score
from sklearn.preprocessing import StandardScaler
from feat import CONTINUOUS_COLUMNS
from feat import CATEGORICAL_COLUMNS
from feat import COLUMNS
def add_columns(x):
return ':'.join(x)
# Define the column names for the data sets.
LABEL_COLUMN = 'target'
#second order
CROSSED_COLUMNS = []
for i in range(len(CATEGORICAL_COLUMNS) -1):
for j in range(i+1, len(CATEGORICAL_COLUMNS)):
CROSSED_COLUMNS.append([CATEGORICAL_COLUMNS[i], CATEGORICAL_COLUMNS[j]])
CATEGORICAL_COLUMNS_DNN = CATEGORICAL_COLUMNS[:]
CATEGORICAL_COLUMNS += map(add_columns, CROSSED_COLUMNS)
CATEGORICAL_ID_COLUMNS = [col + '_ids' for col in CATEGORICAL_COLUMNS]
HIDDEN_UNITS = [512, 512, 512]
CATEGORICAL_EMBED_SIZE = 10
LABEL_ENCODERS = {}
def pandas_input_fn(X, y=None, batch_size=1024, num_epochs=None):
def input_fn():
if y is not None:
X['target'] = y
queue = tf_learn.dataframe.queues.feeding_functions.enqueue_data(
X, 1000, shuffle=num_epochs is None, num_epochs=num_epochs)
if num_epochs is None:
features = queue.dequeue_many(batch_size)
else:
features = queue.dequeue_up_to(batch_size)
features = dict(zip(['index'] + list(X.columns), features))
if y is not None:
target = features.pop('target')
return features, target
return features
return input_fn
def encode_categorical_cross(df):
global LABEL_ENCODERS
for col in CATEGORICAL_COLUMNS:
if ":" in col:
df[col] = df[col.split(":")[0]].fillna(-1).astype(str) + ":" + df[col.split(":")[1]].fillna(-1).astype(str)
else:
df[col] = df[col].fillna(-1).astype(str)
encoder = LabelEncoder().fit(df[col])
df[col + '_ids'] = encoder.transform(df[col])
LABEL_ENCODERS[col] = encoder
for col in CATEGORICAL_COLUMNS:
df.pop(col)
return df, LABEL_ENCODERS
def process_input_df(df):
df, label_encoders = encode_categorical_cross(df)
for col in CATEGORICAL_COLUMNS:
y = df.pop(LABEL_COLUMN)
X = df[CATEGORICAL_ID_COLUMNS + CONTINUOUS_COLUMNS].fillna(0)
return X, y
def wide_and_deep(features, target, hidden_units=HIDDEN_UNITS):
global LABEL_ENCODERS
target = tf.one_hot(target, 2, 1.0, 0.0)
# DNN
final_features_nn = [tf.expand_dims(tf.cast(features[col], tf.float32), 1) for
col in CONTINUOUS_COLUMNS]
# Embed categorical variables into distributed representation.
for col in CATEGORICAL_COLUMNS_DNN:
feature_tmp = tf_learn.ops.categorical_variable(
features[col + '_ids'],
len(LABEL_ENCODERS[col].classes_),
embedding_size=CATEGORICAL_EMBED_SIZE,
name=col)
final_features_nn.append(feature_tmp)
# Concatenate all features into one vector.
features_nn = tf.concat(1, final_features_nn)
logits_nn = tf_layers.stack(features_nn,
tf_layers.fully_connected,
stack_args=hidden_units,
activation_fn=tf.nn.relu)
# LR
final_features_lr = [tf.expand_dims(tf.cast(features[col], tf.float32), 1) for
col in CONTINUOUS_COLUMNS]
for col in CATEGORICAL_COLUMNS:
final_features_lr.append(tf.one_hot(features[col + '_ids'],
len(LABEL_ENCODERS[col].classes_),
on_value = 1.0,
off_value = 0.0))
logits_lr = tf_layers.stack(tf.concat(1, final_features_lr),
tf_layers.fully_connected,
stack_args=[1],
activation_fn=None)
# add logits
logits = logits_lr + logits_nn
prediction, loss = tf_learn.models.logistic_regression(logits, target)
train_op = tf_layers.optimize_loss(loss,
tf.contrib.framework.get_global_step(),
optimizer='Adam',
learning_rate=0.001)
return prediction[:,1], loss, train_op
def train(X, y, steps=100):
print("model dir: ", model_dir)
classifier = tf_learn.Estimator(model_fn=wide_and_deep, model_dir=model_dir)
classifier.fit(input_fn=pandas_input_fn(X, y), steps=steps)
return classifier
def predict(classifier, X):
return list(classifier.predict(input_fn=pandas_input_fn(X, num_epochs=1),
as_iterable=True))
if __name__ == '__main__':
model_dir = "./wnd"
os.system("rm -rf ./wnd")
trainFile = 'train.csv'
testFile = 'test.csv'
data_train = pd.read_csv(trainFile, names=COLUMNS) # LOAD DATA
data_test = pd.read_csv(testFile, names=COLUMNS) # LOAD DATA
train_size = data_train.shape[0]
X_train, y_train = process_input_df(data_train)
X_test, y_test = process_input_df(data_test)
# data scale
data_continuous = pd.concat([X_train[CONTINUOUS_COLUMNS], X_test[CONTINUOUS_COLUMNS]])
scaler = StandardScaler()
data_continuous_scale = scaler.fit_transform(data_continuous)
X_train[CONTINUOUS_COLUMNS] = pd.DataFrame(data_continuous_scale[:train_size])
X_test[CONTINUOUS_COLUMNS] = pd.DataFrame(data_continuous_scale[train_size:])
classifier = train(X_train, y_train, steps=5000)
pred = predict(classifier, X_test)
print("auc", roc_auc_score(y_test, np.array(pred)))
123
Nirvanada

Nirvanada

22 日志
6 分类
© 2018 Nirvanada
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4