2019-03-28

Wide & Deep Learning for Recommender Systems(Google&Facebook推荐)

1、背景

文章提出的Wide&Deep模型,旨在使得训练得到的模型能够同时获得记忆(memorization)和泛化(generization)能力:

  • 记忆(体现准确性):即从历史数据中发现item或者特征之间的相关性;
  • 泛化(体现新颖性):即相关性的传递,发现在历史数据中很少或者没有出现的新的特征组合。

2、Wide&Deep模型

2.1、模型结构

Wide&Deep模型结构图

在Wide&Deep模型中包括两个部分,分别为Wide和Deep部分,Wide部分如上图中的左图所示,Deep部分如上图中的右图所示。

2.2、Wide模型

Memorization主要是学习特征的共性或者说相关性,产生的推荐是和已经有用户行为的物品直接相关的物品。

用的模型是逻辑回归(logistic regression, LR),LR 的优点就是简单(simple)、容易规模化(scalable)、可解释性强(interpretable)。LR 的特征往往是二值且稀疏的(binary and sparse),这里同样采用 one-hot 编码,如 “user_installed_app=netflix”,如果用户安装了 Netflix,这个特征的值为 1,否则为 0。

为了达到 Memorization,我们对稀疏的特征采取 cross-product transformation,比如说 AND(user_installed_app=netflix, impression_app=pandora”) 这个特征,只有 Netflix 和 Pandora 两个条件都达到了,值才为 1,这类 feature 解释了 co-occurrence 和 target label 之间的关系。一个 cross-product transformation 的局限在于,对于在训练集里没有出现过的 query-item pair,它不能进行泛化(Generalization)

Wide模型的输入是用户安装应用(installation)和为用户展示(impression)的应用间的向量积(叉乘),模型通常训练one-hot编码后的二值特征,这种操作不会归纳出训练集中未出现的特征对

实际上,Wide模型是一个广义线性模型:y = w^{T}x+b,其中特征x=[x_1,...,x_d]是一个d维特征向量,特征包括原始输入特征以及cross-product transformation特征。交叉积转换为\phi_k(x)=\prod_{i=1}^d x_{i}^{c_{ki}}
c_{ki}为布尔变量,若第i个特征是第k个变换\phi_k的一部分,值为1,否则为0。w=[w_1,...,w_d]为模型参数。最终在y的基础上增加Sigmoid函数作为最终的输出。

2.3、Deep模型

Generalization可以理解为相关性的传递(transitivity),会学习新的特征组合,来提高推荐物品的多样性,或者说提供泛化能力(Generalization)。

泛化往往是通过学习 low-dimensional dense embeddings 来探索过去从未或很少出现的新的特征组合来实现的,通常的 embedding-based model 有 Factorization Machines(FM) 和 Deep Neural Networks(DNN)。特殊兴趣或者小众爱好的用户,query-item matrix 非常稀疏,很难学习,然而 dense embedding 的方法还是可以得到对所有 query-item pair 非零的预测,这就会导致 over-generalize,推荐不怎么相关的物品。这点和 LR 正好互补,因为 LR 只能记住很少的特征组合。

为了达到 Generalization,文章引入新的小颗粒特征,如类别特征(安装了视频类应用,展示的是音乐类应用,等等)AND(user_installed_category=video, impression_category=music),这些高维稀疏的类别特征(如人口学特征和设备类别)映射为低纬稠密的向量后,与其他连续特征(用户年龄、应用安装数等)拼接在一起,输入 MLP 中,最后输入逻辑输出单元。

Deep模型是一个前馈神经网络。深度神经网络模型通过需要的输入是连续的稠密特征,对于稀疏、高维的类别特征,通常首先将其转换为低维的向量,这个过程也称为embedding。

在训练的时候首先随机初始化embedding向量,并在模型的训练过程中通过最小化损失函数来优化模型,逐渐修改该向量的值,即将向量作为参数参与模型的训练。

隐层的计算方法为:
a^{(l+1)}=f(W^{(l)}a^{(l)}+b^{(l)})
其中,f为激活函数,如ReLus。
基于embedding的深度模型的输入是类别特征(生成embedding)+连续特征

2.4、Wide&Deep模型联合训练

联合训练是指同时训练Wide模型和Deep模型,并将两个模型的输出算log odds ratio然后加权求和作为最终的预测结果:
P(Y=1|x)=\sigma (w_{wide}^T[x,\phi(x)]+ w_{deep}^Ta^{(lf)}+b )

联合训练和embedding方法是不同的。embedding把不同的部分分开训练,这个过程中不同的模型相互之间不知道彼此的存在,也不会互相影响。但在联合训练中,整个网络同时被训练,梯度的反向传播同时影响整个模型的所有部分,使用mini-batch SGD来训练模型。

训练方法:

3、apps的推荐系统

将上述Wide&Deep模型应用在Google play的apps推荐中。

3.1、 推荐系统

推荐系统的一般结构如下所示:


overview of RS

当一个用户访问App Store时,会产生一个请求,请求到达推荐系统后,推荐系统为该用户返回推荐的apps列表。
在实际的推荐系统中,通常将推荐的过程分为两个部分,检索系统(retrieval)和排序系统(ranking),retrieval从数据库中检索出与用户相关的最匹配query的一些apps,这里的检索通常会结合采用机器学习模型和人工定义规则两种方法。从大规模样本中召回最佳候选集后,ranking负责对这些检索出的apps打分、排序,最终按照分数的高低返回相应的列表给用户。分数P(y|x),y是用户采取的行动(例如下载),x是特征包括:

  • user features:eg, country, language, demographics
  • contextual features: eg, device, hour of the day, day of the week
  • impression features: eg, app age, historical statistics of an app

WDL就是用在排序系统中。

3.2、apps推荐的特征

模型的训练之前,最重要的工作是训练数据的准备以及特征的选择,在apps推荐中,可使用到的数据包括用户数据和曝光数据,因此,每一条样本对应了一条曝光数据。样本点标签为1表示安装,0表示未安装。
对于类别特征,通过词典(vocabularies)将其映射成向量;对于连续的实值特征,将其归一化到区间[0,1]。


Wide&Deep 模型 for apps recommendation

3.3、度量的标准

度量的指标有两个,分别针对在线的度量和离线的度量,在线时通过A/B test,最终利用安装率(acquisition);离线则使用AUC作为评价模型的指标。

4、代码介绍

数据源:UCI开源数据集Adult
针对美国某区域的一次人口普查结果,共32561条数据,具体字段如下:


列名解释
from __future__ import print_function

import tensorflow as tf
import tempfile
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings("ignore")

# Categorical base columns.构建低维离散特征
gender = tf.contrib.layers.sparse_column_with_keys(column_name="gender", keys=["Female", "Male"])
race = tf.contrib.layers.sparse_column_with_keys(column_name="race", keys=["Amer-Indian-Eskimo", "Asian-Pac-Islander", "Black", "Other", "White"])
education = tf.contrib.layers.sparse_column_with_hash_bucket("education", hash_bucket_size=1000)
relationship = tf.contrib.layers.sparse_column_with_hash_bucket("relationship", hash_bucket_size=100)
workclass = tf.contrib.layers.sparse_column_with_hash_bucket("workclass", hash_bucket_size=100)
occupation = tf.contrib.layers.sparse_column_with_hash_bucket("occupation", hash_bucket_size=1000)
native_country = tf.contrib.layers.sparse_column_with_hash_bucket("native_country", hash_bucket_size=1000)

# Continuous base columns.
age = tf.contrib.layers.real_valued_column("age")#构建连续型实数特征
age_buckets = tf.contrib.layers.bucketized_column(age, boundaries=[18, 25, 30, 35, 40, 45, 50, 55, 60, 65])
education_num = tf.contrib.layers.real_valued_column("education_num")
capital_gain = tf.contrib.layers.real_valued_column("capital_gain")
capital_loss = tf.contrib.layers.real_valued_column("capital_loss")
hours_per_week = tf.contrib.layers.real_valued_column("hours_per_week")

wide_columns = [
  gender, native_country, education, occupation, workclass, relationship, age_buckets,
#构建离散组合特征
  tf.contrib.layers.crossed_column([education, occupation], hash_bucket_size=int(1e4)),
  tf.contrib.layers.crossed_column([native_country, occupation], hash_bucket_size=int(1e4)),
  tf.contrib.layers.crossed_column([age_buckets, education, occupation], hash_bucket_size=int(1e6))]

deep_columns = [
#构建embedding特征
  tf.contrib.layers.embedding_column(workclass, dimension=8),
  tf.contrib.layers.embedding_column(education, dimension=8),
  tf.contrib.layers.embedding_column(gender, dimension=8),
  tf.contrib.layers.embedding_column(relationship, dimension=8),
  tf.contrib.layers.embedding_column(native_country, dimension=8),
  tf.contrib.layers.embedding_column(occupation, dimension=8),
  age, education_num, capital_gain, capital_loss, hours_per_week]

model_dir = tempfile.mkdtemp()
'''
定义分类模型
n_classes// 分类数目,默认是二分类,>2进行多分类 ;weight_column_name // 训练实例的权重;
linear_optimizer = tf.train.FtrlOptimizer( ...) // 线性模型权重更新的optimizer;
dnn_optimizer=tf.train.AdagradOptimizer( ...) // DNN模型权重更新的optimizer
'''
m = tf.contrib.learn.DNNLinearCombinedClassifier(
    model_dir=model_dir,#模型目录
    linear_feature_columns=wide_columns,#输入线性模型的feature columns
    dnn_feature_columns=deep_columns,#输入DNN模型的feature columns
    dnn_hidden_units=[100, 50])#DNN模型隐层单元数

# Define the column names for the data sets.
COLUMNS = ["age", "workclass", "fnlwgt", "education", "education_num",
  "marital_status", "occupation", "relationship", "race", "gender",
  "capital_gain", "capital_loss", "hours_per_week", "native_country", "income_bracket"]
LABEL_COLUMN = 'label'
CATEGORICAL_COLUMNS = ["workclass", "education", "marital_status", "occupation",
                       "relationship", "race", "gender", "native_country"]
CONTINUOUS_COLUMNS = ["age", "education_num", "capital_gain", "capital_loss",
                      "hours_per_week"]

# Download the training and test data to temporary files.
# Alternatively, you can download them yourself and change train_file and
# test_file to your own paths.
#train_file = tempfile.NamedTemporaryFile()
#test_file = tempfile.NamedTemporaryFile()
#urllib.request.urlretrieve("http://mlr.cs.umass.edu/ml/machine-learning-databases/adult/adult.data", train_file.name)
#urllib.request.urlretrieve("http://mlr.cs.umass.edu/ml/machine-learning-databases/adult/adult.test", test_file.name)

# Read the training and test data sets into Pandas dataframe.
df_train = pd.read_csv('F:/method codes/adult.data.csv', names=COLUMNS, skipinitialspace=True)
df_test = pd.read_csv('F:/method codes/adult.test.csv', names=COLUMNS, skipinitialspace=True, skiprows=1)
df_train[LABEL_COLUMN] = (df_train['income_bracket'].apply(lambda x: '>50K' in x)).astype(int)
df_test[LABEL_COLUMN] = (df_test['income_bracket'].apply(lambda x: '>50K' in x)).astype(int)

def input_fn(df):#定义如何从输入的dataframe构建特征和标记:
  # Creates a dictionary mapping from each continuous feature column name (k) to
  # the values of that column stored in a constant Tensor.
'''
tf.constant构建constant tensor,df[k].values是对应feature column的值构成的list
'''
  continuous_cols = {k: tf.constant(df[k].values)
                     for k in CONTINUOUS_COLUMNS}
  # Creates a dictionary mapping from each categorical feature column name (k)
  # to the values of that column stored in a tf.SparseTensor.
#tf.SparseTensor构建sparse tensor,SparseTensor由indices,values, dense_shape三个dense tensor构成,
#indices中记录非零元素在sparse tensor的位置,values是indices中每个位置的元素的值,dense_shape指定
#sparse tensor中每个维度的大小。
'''
以下代码为每个category column构建一个[df[k].size,1]的二维的SparseTensor。
'''
  categorical_cols = {k: tf.SparseTensor(
      indices=[[i, 0] for i in range(df[k].size)],
      values=df[k].values,
      dense_shape=[df[k].size, 1])
                      for k in CATEGORICAL_COLUMNS}
  # Merges the two dictionaries into one.
'''
用以下示意图来表示以上代码构建的sparse tensor label是一个 constant tensor,记录每个实例的 label
'''
  feature_cols = dict(list(continuous_cols.items()) + list(categorical_cols.items()))
# features是continuous_cols和categorical_cols的union构成的dict 
# dict中每个entry的key是feature column的name,value是feature column值的tensor
  # Converts the label column into a constant Tensor.
  label = tf.constant(df[LABEL_COLUMN].values)
  # Returns the feature columns and the label.
  return feature_cols, label

def train_input_fn():
  return input_fn(df_train)

def eval_input_fn():
  return input_fn(df_test)

print('df_train shape:',np.array(df_train).shape)
print('df_test shape:',np.array(df_test).shape)

m.fit(input_fn=train_input_fn, steps=200)#训练模型
results = m.evaluate(input_fn=eval_input_fn, steps=1)#模型评测
for key in sorted(results):
    print("%s: %s" % (key, results[key]))
#https://blog.csdn.net/a819825294/article/details/71080472
#https://www.sohu.com/a/190148302_115128

参考资料:https://www.jianshu.com/p/28a1849f6707
https://blog.csdn.net/google19890102/article/details/78171283

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容