提示:最近系统性地学习推荐系统的课程。我们以小红书的场景为例,讲工业界的推荐系统。
我只讲工业界实际有用的技术。说实话,工业界的技术远远领先学术界,在公开渠道看到的书、论文跟工业界的实践有很大的gap,
看书学不到推荐系统的关键技术。
看书学不到推荐系统的关键技术。
看书学不到推荐系统的关键技术。
王树森娓娓道来**《小红书的推荐系统》**
GitHub资料连接:http://wangshusen.github.io/
B站视频合集:https://space.bilibili.com/1369507485/channel/seriesdetail?sid=2249610
基础知识:
【1】一文看懂推荐系统:概要01:推荐系统的基本概念
【2】一文看懂推荐系统:概要02:推荐系统的链路,从召回粗排,到精排,到重排,最终推荐展示给用户
【3】一文看懂推荐系统:召回01:基于物品的协同过滤(ItemCF),item-based Collaboration Filter的核心思想与推荐过程
【4】一文看懂推荐系统:召回02:Swing 模型,和itemCF很相似,区别在于计算相似度的方法不一样
【5】一文看懂推荐系统:召回03:基于用户的协同过滤(UserCF),要计算用户之间的相似度
【6】一文看懂推荐系统:召回04:离散特征处理,one-hot编码和embedding特征嵌入
【7】一文看懂推荐系统:召回05:矩阵补充、最近邻查找,工业界基本不用了,但是有助于理解双塔模型
【8】一文看懂推荐系统:召回06:双塔模型——模型结构、训练方法,召回模型是后期融合特征,排序模型是前期融合特征
【9】一文看懂推荐系统:召回07:双塔模型——正负样本的选择,召回的目的是区分感兴趣和不感兴趣的,精排是区分感兴趣和非常感兴趣的
【10】一文看懂推荐系统:召回08:双塔模型——线上服务需要离线存物品向量、模型更新分为全量更新和增量更新
【11】一文看懂推荐系统:召回09:地理位置召回、作者召回、缓存召回
【12】一文看懂推荐系统:排序01:多目标模型
【13】一文看懂推荐系统:排序02:Multi-gate Mixture-of-Experts (MMoE)
【14】一文看懂推荐系统:排序03:预估分数融合
【15】一文看懂推荐系统:排序04:视频播放建模
【16】一文看懂推荐系统:排序05:排序模型的特征
【17】一文看懂推荐系统:排序06:粗排三塔模型,性能介于双塔模型和精排模型之间
【18】一文看懂推荐系统:特征交叉01:Factorized Machine (FM) 因式分解机
【19】一文看懂推荐系统:物品冷启01:优化目标 & 评价指标
【20】一文看懂推荐系统:物品冷启02:简单的召回通道
【21】一文看懂推荐系统:物品冷启03:聚类召回
【22】一文看懂推荐系统:物品冷启04:Look-Alike 召回,Look-Alike人群扩散
【23】一文看懂推荐系统:物品冷启05:流量调控
【24】一文看懂推荐系统:物品冷启06:冷启的AB测试
【25】推荐系统最经典的 排序模型 有哪些?你了解多少?
【26】一文看懂推荐系统:排序07:GBDT+LR模型
【27】一文看懂推荐系统:排序08:Factorization Machines(FM)因子分解机,一个特殊的案例就是MF,矩阵分解为uv的乘积
【28】一文看懂推荐系统:排序09:Field-aware Factorization Machines(FFM),从FM改进来的,效果不咋地
【29】一文看懂推荐系统:排序10:wide&deep模型,wide就是LR负责记忆,deep负责高阶特征交叉而泛化
【30】一文看懂推荐系统:排序11:Deep & Cross Network(DCN)
【31】一文看懂推荐系统:排序12:xDeepFM模型,并不是对DeepFM的改进,而是对DCN的改进哦
【32】一文看懂推荐系统:排序13:FNN模型(FM+MLP=FNN),与PNN同属上海交大张楠的作品
【33】一文看懂推荐系统:排序14:PNN模型(Product-based Neural Networks),和FNN一个作者,干掉FM,加上LR+Product
提示:文章目录
DeepFM是哈工大和华为合作发表在IJCAI2017上的文章,
这篇文章也是受到谷歌wide&deep模型的启发,
是一个左右组合(混合)模型结构,【wide&deep如下】
不同的是,deepFM在wide部分用了FM模型来代替LR模型。
所以你已经非常明白了吧
因此,强烈建议在看这篇文章之前,先移步看完我之前写的关于wide&deep的博客
我们来看看DeepFM相比较wide&deep模型的改进点及优势(前提是你已经很了解wide&deep模型了):
在wide部分使用FM代替了wide&deep中的LR,
有了FM自动构造学习二阶(考虑到时间复杂度原因,通常都是二阶)交叉特征的能力,因此不再需要特征工程。
Wide&Deep模型中LR部分依然需要人工的特征交叉,
比如【用户已安装的app】与【给用户曝光的app】两个特征做交叉。
另外,仅仅通过人工的手动交叉,又回到了之前在讲FM模型中提到的,
比如要两个特征共现,否则无法训练。
在DeepFM模型中,FM模型与DNN模型共享底层embedding向量,然后联合训练。
这种方式也更符合现在推荐/广告领域里多任务模型多塔共享底座embedding的方式,
然后end-to-end训练得到的embedding向量也更加准确。
其实如果你很熟悉wide&deep模型,再经过上面的介绍,
你基本已经知道DeepFM的大体网络结构了。
接下来,本文将从两个方面介绍deepFM:
DeepFM的模型结构细节
DeepFM的代码实现
总结
来看下DeepFM的模型结构(图片来自王喆《深度学习推荐系统》,
ps:原论文的图不清晰,所以没有直接从原论文取图)
你看原图
中文新图
整体模型结构也比较简单,自底向上看分别为:
原始输入层:onehot编码的稀疏输入
embedding层:FM和DNN共享的底座
FM与DNN
输出层
重点说下FM层,先来回顾下FM的公式:我们说了多次了,关键在特征交互部分那一项
DNN部分:没什么好讲的,多层全连接网络。
最终的输出:
这部分是本博客的重点,我这里直接用paddle官方的代码讲解下,
具体代码参见:搞清楚代码细节,有助于我们对DeepFM模型更深入的了解。
这里用的是Criteo数据集,用于广告CTR预估的数据集,
关于数据集的介绍参见:Criteo。
特征方面,这个数据集共26个离散特征,13个连续值特征。
我给代码增加了详细的注释(主要是矩阵维度的注释),大家看代码即可。
class FM(nn.Layer):
def __init__(self, sparse_feature_number, sparse_feature_dim,
dense_feature_dim, sparse_num_field):
super(FM, self).__init__()
self.sparse_feature_number = sparse_feature_number # 1000001
self.sparse_feature_dim = sparse_feature_dim # 9
self.dense_feature_dim = dense_feature_dim # 13
self.dense_emb_dim = self.sparse_feature_dim # 9
self.sparse_num_field = sparse_num_field # 26
self.init_value_ = 0.1
use_sparse = True
# sparse coding
# Embedding(1000001, 1, padding_idx=0, sparse=True)
self.embedding_one = paddle.nn.Embedding(
sparse_feature_number,
1,
padding_idx=0,
sparse=use_sparse,
weight_attr=paddle.ParamAttr(
initializer=paddle.nn.initializer.TruncatedNormal(
mean=0.0,
std=self.init_value_ /
math.sqrt(float(self.sparse_feature_dim)))))
# Embedding(1000001, 9, padding_idx=0, sparse=True)
self.embedding = paddle.nn.Embedding(
self.sparse_feature_number,
self.sparse_feature_dim,
sparse=use_sparse,
padding_idx=0,
weight_attr=paddle.ParamAttr(
initializer=paddle.nn.initializer.TruncatedNormal(
mean=0.0,
std=self.init_value_ /
math.sqrt(float(self.sparse_feature_dim)))))
# dense coding
"""
Tensor(shape=[13], dtype=float32, place=CPUPlace, stop_gradient=False,
[-0.00486396, 0.02755001, -0.01340683, 0.05218775, 0.00938804, 0.01068084, 0.00679830,
0.04791596, -0.04357519, 0.06603041, -0.02062148, -0.02801327, -0.04119579]))
"""
self.dense_w_one = paddle.create_parameter(
shape=[self.dense_feature_dim],
dtype='float32',
default_initializer=paddle.nn.initializer.TruncatedNormal(
mean=0.0,
std=self.init_value_ /
math.sqrt(float(self.sparse_feature_dim))))
# Tensor(shape=[1, 13, 9])
self.dense_w = paddle.create_parameter(
shape=[1, self.dense_feature_dim, self.dense_emb_dim],
dtype='float32',
default_initializer=paddle.nn.initializer.TruncatedNormal(
mean=0.0,
std=self.init_value_ /
math.sqrt(float(self.sparse_feature_dim))))
def forward(self, sparse_inputs, dense_inputs):
# -------------------- first order term --------------------
"""
sparse_inputs: list, length:26, list[tensor], each tensor shape: [2, 1]
dense_inputs: Tensor(shape=[2, 13]), 2 --> train_batch_size
"""
# Tensor(shape=[2, 26])
sparse_inputs_concat = paddle.concat(sparse_inputs, axis=1)
# Tensor(shape=[2, 26, 1])
sparse_emb_one = self.embedding_one(sparse_inputs_concat)
# dense_w_one: shape=[13], dense_inputs: shape=[2, 13]
# dense_emb_one: shape=[2, 13]
dense_emb_one = paddle.multiply(dense_inputs, self.dense_w_one)
# shape=[2, 13, 1]
dense_emb_one = paddle.unsqueeze(dense_emb_one, axis=2)
# paddle.sum(sparse_emb_one, 1): shape=[2, 1]
# paddle.sum(dense_emb_one, 1): shape=[2, 1]
# y_first_order: shape=[2, 1]
y_first_order = paddle.sum(sparse_emb_one, 1) + paddle.sum(
dense_emb_one, 1)
# -------------------- second order term --------------------
# Tensor(shape=[2, 26, 9])
sparse_embeddings = self.embedding(sparse_inputs_concat)
# Tensor(shape=[2, 13, 1])
dense_inputs_re = paddle.unsqueeze(dense_inputs, axis=2)
# dense_inputs_re: Tensor(shape=[2, 13, 1])
# dense_w: Tensor(shape=[1, 13, 9])
# dense_embeddings: Tensor(shape=[2, 13, 9])
dense_embeddings = paddle.multiply(dense_inputs_re, self.dense_w)
# Tensor(shape=[2, 39, 9])
feat_embeddings = paddle.concat([sparse_embeddings, dense_embeddings],
1)
# sum_square part
# Tensor(shape=[2, 9])
# \sum_{i=1}^n(v_{i,f}x_i) ---> for each embedding element: e_i, sum all feature's e_i
summed_features_emb = paddle.sum(feat_embeddings,
1) # None * embedding_size
# Tensor(shape=[2, 9]) 2-->batch_size
summed_features_emb_square = paddle.square(
summed_features_emb) # None * embedding_size
# square_sum part
# Tensor(shape=[2, 39, 9])
squared_features_emb = paddle.square(
feat_embeddings) # None * num_field * embedding_size
# Tensor(shape=[2, 9]) 2-->batch_size
squared_sum_features_emb = paddle.sum(squared_features_emb,
1) # None * embedding_size
# Tensor(shape=[2, 1])
y_second_order = 0.5 * paddle.sum(
summed_features_emb_square - squared_sum_features_emb,
1,
keepdim=True) # None * 1
return y_first_order, y_second_order, feat_embeddings
这部分着实没什么好说的,直接略过。
class DNN(paddle.nn.Layer):
def __init__(self, sparse_feature_number, sparse_feature_dim,
dense_feature_dim, num_field, layer_sizes):
super(DNN, self).__init__()
self.sparse_feature_number = sparse_feature_number
self.sparse_feature_dim = sparse_feature_dim
self.dense_feature_dim = dense_feature_dim
self.num_field = num_field
self.layer_sizes = layer_sizes
# [351, 512, 256, 128, 32, 1]
sizes = [sparse_feature_dim * num_field] + self.layer_sizes + [1]
acts = ["relu" for _ in range(len(self.layer_sizes))] + [None]
self._mlp_layers = []
for i in range(len(layer_sizes) + 1):
linear = paddle.nn.Linear(
in_features=sizes[i],
out_features=sizes[i + 1],
weight_attr=paddle.ParamAttr(
initializer=paddle.nn.initializer.Normal(
std=1.0 / math.sqrt(sizes[i]))))
self.add_sublayer('linear_%d' % i, linear)
self._mlp_layers.append(linear)
if acts[i] == 'relu':
act = paddle.nn.ReLU()
self.add_sublayer('act_%d' % i, act)
def forward(self, feat_embeddings):
"""
feat_embeddings: Tensor(shape=[2, 39, 9])
"""
# Tensor(shape=[2, 351]) --> 351=39*9,
# 39 is the number of features(category feature+ continous feature), 9 is embedding size
y_dnn = paddle.reshape(feat_embeddings,
[-1, self.num_field * self.sparse_feature_dim])
for n_layer in self._mlp_layers:
y_dnn = n_layer(y_dnn)
return y_dnn
def forward(self, sparse_inputs, dense_inputs):
y_first_order, y_second_order, feat_embeddings = self.fm.forward(
sparse_inputs, dense_inputs)
# feat_embeddings: Tensor(shape=[2, 39, 9])
# y_dnn: Tensor(shape=[2, 1])
y_dnn = self.dnn.forward(feat_embeddings)
print("y_dnn:", y_dnn)
predict = F.sigmoid(y_first_order + y_second_order + y_dnn)
return predict
总得来说,DeepFM还是一个挺不错的模型,在工业界应用的也挺多。
还是那句话,如果你的场景下之前是LR,正在往深度学习迁移,
为了最大化节约成本,可以尝试下wide&deep模型。
如果原来是xgboost一类的树模型,需要尝试深度学习模型,建议直接deepFM。
此外,deepFM与DCN都是在2017年发表的,
因此这两篇paper里均没有直接有过实验数据对比,
但在DCN V2里给出了实验效果对比,在论文给定的数据集下,两个模型效果差不多。
注意我之前讲的xDeepFM不是对deepFM的改进,而是对DCN的改进哦!老复杂了,人家DCN挺好的
提示:如何系统地学习推荐系统,本系列文章可以帮到你
(1)找工作投简历的话,你要将招聘单位的岗位需求和你的研究方向和工作内容对应起来,这样才能契合公司招聘需求,否则它直接把简历给你挂了
(2)你到底是要进公司做推荐系统方向?还是纯cv方向?还是NLP方向?还是语音方向?还是深度学习机器学习技术中台?还是硬件?还是前端开发?后端开发?测试开发?产品?人力?行政?这些你不可能啥都会,你需要找准一个方向,自己有积累,才能去投递,否则面试官跟你聊什么呢?
(3)今日推荐系统学习经验:之前讲的xDeepFM不是对deepFM的改进,而是对DCN的改进哦!老复杂了,人家DCN挺好的