一、Wide&Deep 模型的结构
Wide&Deep是工业界中有巨大影响力的模型,如果直接翻译成中文是宽和深的模型,其模型结构如下所示:wide和deep让模型兼具逻辑回归和深度神经网络的特点。
Wide就是将输入层直接连接到输出层(中间没有做任何处理)
——让模型具有较强的记忆力
Deep则其实是上个task的embedding+MLP
——让模型具有较强的泛化性
二、模型的记忆能力
记忆力:可理解为模型直接学习历史数据中物品或者特征的“共现频率”,并且把它们直接作为推荐依据的能力。
在sparrow项目的电影推荐中有一些规则,如看了A电影的用户经常喜欢看电影B,这是一种类似“因为A所以B”的规则,这类规则有两个特点:
数量特多,模型难记住;
没法推而广之,规则已经比较具体了:没办法或者说也没必要跟其他特征做进一步的组合。就像看了电影 A 的用户 80% 都喜欢看电影 B,这个特征已经非常强了,我们就没必要把它跟其他特征再组合在一起。
结论:wide部分增强模型记忆力,因为单层线性模型“擅长”记住大量且直接的规则。
三、模型的泛化能力
“泛化能力”指的是模型对于新鲜样本、以及从未出现过的特征组合的预测能力。
栗子1:现在如果已知35岁的女性用户和25岁的男性用户都喜欢看电影A,那如果让学习后的模型现在推理:35岁的男性用户是否喜欢看电影A,如果泛化性强一丢丢的模型可能就会回答【喜欢】,基于35女的35和25男的男做出的推理。
栗子2:矩阵分解算法就是为了解决协同过滤“泛化能力”不强而诞生的。因为协同过滤只会“死板”地使用用户的原始行为特征,而矩阵分解因为生成了用户和物品的隐向量,所以就可以计算任意两个用户和物品之间的相似度了。这就是泛化能力强的另一个例子。
结论:通过deep部分可以增强模型的泛化性,因为深度学习模型有很强的数据拟合能力,特征可以充分交叉,这里就沿用了上个task的embedding+MLP。
四、Wide&Deep 模型的应用场景
Wide&Deep 模型是由 Google 的应用商店团队 Google Play 提出的,在 Google Play 为用户推荐 APP 这样的应用场景下。
Wide&Deep 模型的推荐目标:尽量推荐那些用户可能喜欢,愿意安装的应用。
问题:具体到 Wide&Deep 模型中,Google Play 团队是如何为 Wide 部分和 Deep 部分挑选特征的。
图2 Google Play Wide&Deep模型的细节 (出自Wide & Deep Learning for Recommender Systems )
上图2补充google play团队这个模型的细节:从左边的wide的特征开始看起,只利用两个特征(“已安装应用”特征和“曝光应用”特征)的交叉,就是说wide想学的东西是希望记住“因为A所以B”的规则——如果安装了应用A,是否会安装B这样的规则。
再看deep部分的左边:是一个非常典型的 Embedding+MLP 结构,其中的输入特征很多,有用户年龄、属性特征、设备类型,还有已安装应用的 Embedding 等等。把这些特征一股脑地放进多层神经网络里面去学习之后,它们互相之间会发生多重的交叉组合,这最终会让模型具备很强的泛化能力。如刚才35女和25男的栗子学到具有泛化性的模型。
五、tensorflow实现
这部分的特征处理和上一个task是一样的,看模型结构:
deep模块:和上个task的特征是一样的,输入层加两层 128 维隐层的结构,它的输入是类别型 Embedding 向量和数值型特征。
wide模块:把输入特征连接到输出层,注意Wide 部分所用的特征 crossed_feature。
# wide and deep model architecture # deep part for all input features deep = tf.keras.layers.DenseFeatures(numerical_columns + categorical_columns)(inputs) deep = tf.keras.layers.Dense(128, activation='relu')(deep) deep = tf.keras.layers.Dense(128, activation='relu')(deep) # wide part for cross feature wide = tf.keras.layers.DenseFeatures(crossed_feature)(inputs) # 拼接wide和deep层 both = tf.keras.layers.concatenate([deep, wide]) output_layer = tf.keras.layers.Dense(1, activation='sigmoid')(both) model = tf.keras.Model(inputs, output_layer)
生成了一个由“用户已好评电影”和“当前评价电影”组成的一个交叉特征,就是代码中的 crossed_feature,设置这个特征的目的在于让模型记住好评电影之间的相关规则,更具体点来说就是,就是让模型记住“一个喜欢电影 A 的用户,也会喜欢电影 B”这样的规则:
这样的规则不是唯一的,需要你根据自己的业务特点来设计, 比如在电商网站中,这样的规则可以是,购买了键盘的用户也会购买鼠标。在新闻网站中,可以是打开过足球新闻的用户,也会点击 NBA 新闻等等。
movie_feature = tf.feature_column.categorical_column_with_identity(key='movieId', num_buckets=1001) rated_movie_feature = tf.feature_column.categorical_column_with_identity(key='userRatedMovie1', num_buckets=1001) crossed_feature = tf.feature_column.crossed_column([movie_feature, rated_movie_feature], 10000)
在 Deep 部分和 Wide 部分都构建完后,我们要使用 concatenate layer 把两部分连接起来,形成一个完整的特征向量,输入到最终的 sigmoid 神经元中,产生推荐分数。
【拓展】尝试设置不同的特征,以及不同的参数组合,真实地体验深度学习模型的调参过程。
六、Pytorch实现
首先分别搭建两个部分:
class Linear(nn.Module): """ Linear part """ def __init__(self, input_dim): super(Linear, self).__init__() self.linear = nn.Linear(in_features=input_dim, out_features=1) def forward(self, x): return self.linear(x) class Dnn(nn.Module): """ Dnn part """ def __init__(self, hidden_units, dropout=0.): """ hidden_units: 列表, 每个元素表示每一层的神经单元个数, 比如[256, 128, 64], 两层网络, 第一层神经单元128, 第二层64, 第一个维度是输入维度 dropout: 失活率 """ super(Dnn, self).__init__() self.dnn_network = nn.ModuleList([nn.Linear(layer[0], layer[1]) for layer in list(zip(hidden_units[:-1], hidden_units[1:]))]) self.dropout = nn.Dropout(p=dropout) def forward(self, x): for linear in self.dnn_network: x = linear(x) x = F.relu(x) x = self.dropout(x) return x
最后拼接起来,搭建wide&deep模型,最后的输出部分可以对Wide和Deep的输出加上一个权重得到最后的输出。
class WideDeep(nn.Module): def __init__(self, feature_columns, hidden_units, dnn_dropout=0.): super(WideDeep, self).__init__() self.dense_feature_cols, self.sparse_feature_cols = feature_columns # embedding self.embed_layers = nn.ModuleDict({ 'embed_' + str(i): nn.Embedding(num_embeddings=feat['feat_num'], embedding_dim=feat['embed_dim']) for i, feat in enumerate(self.sparse_feature_cols) }) hidden_units.insert(0, len(self.dense_feature_cols) + len(self.sparse_feature_cols)*self.sparse_feature_cols[0]['embed_dim']) self.dnn_network = Dnn(hidden_units) self.linear = Linear(len(self.dense_feature_cols)) self.final_linear = nn.Linear(hidden_units[-1], 1) def forward(self, x): dense_input, sparse_inputs = x[:, :len(self.dense_feature_cols)], x[:, len(self.dense_feature_cols):] sparse_inputs = sparse_inputs.long() sparse_embeds = [self.embed_layers['embed_'+str(i)](sparse_inputs[:, i]) for i in range(sparse_inputs.shape[1])] sparse_embeds = torch.cat(sparse_embeds, axis=-1) dnn_input = torch.cat([sparse_embeds, dense_input], axis=-1) # Wide wide_out = self.linear(dense_input) # Deep deep_out = self.dnn_network(dnn_input) deep_out = self.final_linear(deep_out) # out outputs = F.sigmoid(0.5 * (wide_out + deep_out)) return outputs
七、作业
对于 Deep 部分来说,一股脑地把所有特征都扔进 MLP 中去训练,这样的方式有没有什么改进的空间?比如说,“用户喜欢的电影风格”和“电影本身的风格”这两个特征,我们能不能进一步挖掘出它们之间的相关性,而不是简单粗暴地扔给神经网络去处理呢?
【答】改进算法的wide部分,提升记忆能力,使用端到端模型,减少人工操作。例如DCNMix、DeepFM。以DeepFM这个模型都可以很好学习到高低特征与交叉。(实际业界常用,推荐)。这里也可以回顾之前的各种典型网络模型:
【高低阶特征知识】
低阶特征:是指线性-线性组合,只能算一个有效的线性组合,线性-非线性-线性,这样算两个有效的线性组合,一般常说的低阶特征只有小于等于2阶;
高阶特征:说高阶特征,可以理解为经过多次线性-非线性组合操作之后形成的特征,为高度抽象特征,一般人脑很难解析出原有的特征了。