基于深度学习的推荐算法——推荐系统模型搭建基础及DeepCrossing原理

2023-11-05

本章节从推荐系统模型搭建基础和DeepCrossing原理讲解及实操两方面展开。

一、推荐系统模型搭建基础

1. Keras搭建模型

keras搭建模型主要有两种模式,一种是Sequential API,另外一种是Functional API。前者主要是通过层的有序堆叠形成一个模型,在大多数情况下可以快速的搭建一个模型,但是搭建的模型更适合简单的堆叠模型,对于复杂模型(多输入、多输出、共享层)的搭建就比较困难,所以后者函数式API可以更加灵活的搭建复杂网络,函数式API搭建模型是通过创建层的实例并将将层与层之间连接在一起,最后只需要指定模型的输入和输出就可以完成模型的搭建,不同层的实例可以表示不同的操作,搭建模型的时候只需要考虑层与层之间的关系,以及复杂层的搭建就可以很方便的搭建起一个复杂网络。

Sequential API Demo

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

# 定义一个3层的序列模型
model = keras.Sequential(
    [
        layers.Dense(2, activation="relu", name="layer1"),
        layers.Dense(3, activation="relu", name="layer2"),
        layers.Dense(4, name="layer3"),
    ]
)

# 定义数据
x = tf.random.normal((3, 4))
y = model(x)

Functional API Demo

# 定义输入层(可以看成数据层)
inputs = keras.Input(shape=(784,))

# 定义模型逻辑层(将输入数据进行转换)
x = layers.Dense(64, activation="relu")(inputs)

# 定义输出层,这里其实和其他的层没有区别,只不过是最后认定这一层作为输出层而已
outputs = layers.Dense(10)(x)

# 定义整个模型,通过制定模型的输入和输出,按照前面所说的构建模型的流程,产生最终的模型结构
model = Model(inputs=inputs,outputs=outputs)

注意
layers.Dense(64, activation=“relu”)表示的是一个Dense层实例,括号中的参数就是创建Dense实例的参数,将inputs输入到layers.Dense(64, activation=“relu”)实例中,会自动的调用实例的__call__()方法,这样就把输入和层与层之间的逻辑给确定了。

所以函数API搭建模型的基本操作就是,将输入数据输入到层的实例中,层对象就会调用该层的call方法完成该层的计算并产生新的输出,接下来再将产生的新的输出输入到下一个层实例中产生新的输出,一直不断的构建层的实例并得到新的输出,进而构建一个复杂的模型。

二、DeepCrossing原理讲解及实操

深度学习模型中最经典的一种模型结构是Embedding+MLP。它不仅经典,还是后续实现其他深度学习模型的基础。
这里面的Embedding大家应该比较熟悉,而MLP是什么呢?它其实是 Multilayer perceptron,多层感知机的缩写。感知机是神经元的另外一种叫法,所以多层感知机就是多层神经网络。微软的DeepCrossing就是Embedding+MLP模型机构中的一种。

DeepCrossing应用场景

DeepCrossing模型应用场景是微软搜索引擎Bing中的搜索广告推荐, 用户在输入搜索词之后, 搜索引擎除了返回相关结果, 还返回与搜索词相关的广告,Deep Crossing的优化目标就是预测对于某一广告, 用户是否会点击,依然是点击率预测的一个问题。

DeepCrossing结构和原理

在这里插入图片描述

图1 经典的Embedding+MLP模型结构

可以看到该结构由Embedding Layer、Stacking Layer、Multiple Residual Units Layer、Scoring Layer组成。下面分别介绍各层作用。

Embedding Layer
Embedding 层就是为了把稀疏的 One-hot 向量转换成稠密的 Embedding 向量而设置的,我们需要注意的是,Embedding 层并不是全部连接起来的,而是每一个特征对应一个 Embedding 层,不同 Embedding 层之间互不干涉。
Embeding 层的结构就是 Word2vec 模型中从输入神经元到隐层神经元的部分(如图 2 红框内的部分)。参照下面的示意图,我们可以看到,这部分就是一个从输入层到隐层之间的全连接网络。
在这里插入图片描述

图2 Word2vec模型中Embedding层的部分

Stacking Layer
Stacking 层中文名是堆叠层,我们也经常叫它连接(Concatenate)层。它的作用比较简单,就是把不同的 Embedding 特征和数值型特征拼接在一起,形成新的包含全部特征的特征向量。

Multiple Residual Units Layer
MLP 层就是我们开头提到的多层神经网络层,在图 1 中指的是 Multiple Residual Units 层,中文叫多层残差网络。微软在实现 Deep Crossing 时针对特定的问题选择了残差神经元

但事实上,神经元的选择有非常多种,以 Sigmoid 函数为激活函数的神经元,以及使用 tanh、ReLU 等其他激活函数的神经元。我们具体选择哪种是一个调参的问题,一般来说,ReLU 最经常使用在隐层神经元上,Sigmoid 则多使用在输出神经元,实践中也可以选择性地尝试其他神经元,根据效果作出最后的决定。

不管选择哪种神经元,MLP 层的特点是全连接,就是不同层的神经元两两之间都有连接。

Scoring Layer
这个作为输出层,为了拟合优化目标存在。 对于CTR预估二分类问题, Scoring往往采用逻辑回归,模型通过叠加多个残差块加深网络的深度,最后将结果转换成一个概率值输出。

用于点击率预估模型的损失函数就是对数损失函数:
l o g l o s s = − 1 N ∑ 1 N ( y i l o g ( p i ) + ( 1 − y i ) l o g ( 1 − p i ) logloss=-\frac 1N\sum_1^N(y_ilog(p_i)+(1-y_i)log(1-p_i) logloss=N11N(yilog(pi)+(1yi)log(1pi)
其中 y i y_i yi表示真实的标签(点击或未点击), p i p_i pi表示Scoring Layer输出的结果。但是在实际应用中,根据不同的需求可以灵活替换为其他目标函数。

实操tips

在正式进入DeepCrossing代码实操前,我们先了解下代码中的几个技巧。

1. 特征较多时构建多输入模型
将输入的数据转换成字典的形式,定义输入层的时候让输入层的name和字典中特征的key一致,就可以使得输入的数据和对应的Input层对应。方便后面搭建模型。

2. 特征表示的统一
看过DeepCTR源码的人可能就会知道,项目中输入分成三大类,分别是SparseFeat, DenseFeat, VarLenSparseFeat,并且使用类进行了封装,其中也考虑到效率的问题做了一些优化。

  • SparseFeat: 稀疏特征的标记,一般是用来表示id类特征
  • DenseFeat: 表示数值型特征,可以是一维的也可以是多维的
  • VarLenSparseFeat: 可变长的id类特征,就是id序列特征
    这三类特征在实际的推荐系统应用中包含了绝大多数的特征类型,可能会有一些其他的比如图像、视频等其它特征,虽然实际可能存在,但是我感觉如果要是用这些特征就需要将其转换成向量的形式去使用,也就是DenseFeat多维度的情况。统一特征后就可以用来更好的构建输入层了。

3. 通过特征标记构造输入层
在前面的函数式API构建模型最后说到过,可以使用字典的形式构建输入,最后只要将对应Input层的名字与字典中特征的key相对应就可以。在定义Input层的时候,除了name以外还有一个重要的属性就是shape

然而所有特征Input层的shape其实只有4种情况:

  • 数值特征,1维的数值特征shape=(1, )
  • 多维的数值特征shape=(dimension, )
  • 类别特征,shape=(1,), 为什么类别特征的shape维度是1呢,因为输入的就是一个id,在类别型特征的Input后面还需要接一个Embedding层,将id转化成稠密的向量
  • 可变长的序列特征,shape=(maxlen, 1), 序列的输入往往需要定义一个最大长度,这样不至于序列长度之间相差太大,这个最大长度可以是实际数据中的最大长度,也可以是根据经验定义的最大长度。需要注意的是,序列特征中的每个元素其实也是一个id类特征,在最后转换成Embedding的时候,不是一个Embedding向量,而是一个矩阵。
    上面说了Input层的四种情况有什么用呢?
    当特征维度特别多的时候,比如成百上千维特征,如果没有这种标记的话,我们就需要挨个定义每个特征对应的Input层,当然有人可能会说可以提前分组然后再给不同的Input层,其实本质上是一样的。

4. Embedding层的注意点
在构建模型的时候Embedding相关的需要注意两点:

  • Embedding层的参数问题
  • Embedding层之间的拼接问题
    上面在说了类别特征和可变长的序列特征,在这两个Input层之后都需要将其转化成Embedding向量或者Embedding矩阵,在keras中转化成Embedding向量和Embedding矩阵只是相差一个参数的问题。
    image-20210226191552184
  1. 如何在linear层引入onehot特征
    如果要将类别型特征的onehot表示输入到linear层中,第一个想法就是直接把特征转换成onehot向量不就行了吗?的确是可以,但是我们知道在推荐场景中id类特征是一等公民,在实际的场景中如果将所有的特征都转换成onehot类型,维度很可能超出想象。这里有个更好的做法就是,给id类特征转换成一个一维的Embedding矩阵,只需要将这个Embedding保存下来,然后有id类特征输入直接在Embedding中进行查找,找到那个对应的值其实就是onehot向量已经乘完权重的值,因为onehot向量只有0和1,只有非零的才是有效的,而1乘以权重还是权重本身,所以这种方式来获取onehot向量中的非零元素的值,相比直接使用onehot向量乘以一个权重更好一些。

代码实操

1. 数据读取及特征预处理

# 读取数据
data = pd.read_csv('./data/criteo_sample.txt')

# 划分dense和sparse特征
columns = data.columns.values
dense_features = [feat for feat in columns if 'I' in feat]
sparse_features = [feat for feat in columns if 'C' in feat]

# 简单的数据预处理
train_data = data_process(data, dense_features, sparse_features)
train_data['label'] = data['label']

# 将特征做标记
dnn_feature_columns = [SparseFeat(feat, vocabulary_size=data[feat].nunique(),embedding_dim=4)
                        for feat in sparse_features] + [DenseFeat(feat, 1,)
                        for feat in dense_features]

定义特征标记

from collections import namedtuple

# 使用具名元组定义特征标记
SparseFeat = namedtuple('SparseFeat', ['name', 'vocabulary_size', 'embedding_dim'])
DenseFeat = namedtuple('DenseFeat', ['name', 'dimension'])
VarLenSparseFeat = namedtuple('VarLenSparseFeat', ['name', 'vocabulary_size', 'embedding_dim', 'maxlen'])

2. 构建DeepCrossing模型

# 构建DeepCrossing模型
history = DeepCrossing(dnn_feature_columns)

DeepCrossing函数

def DeepCrossing(dnn_feature_columns):
    # 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型
    dense_input_dict, sparse_input_dict = build_input_layers(dnn_feature_columns)
    # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式
    # 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层
    input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())
    
    # 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型
    embedding_layer_dict = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)

    #将所有的dense特征拼接到一起
    dense_dnn_list = list(dense_input_dict.values())
    dense_dnn_inputs = Concatenate(axis=1)(dense_dnn_list) # B x n (n表示数值特征的数量)

    # 因为需要将其与dense特征拼接到一起所以需要Flatten,不进行Flatten的Embedding层输出的维度为:Bx1xdim
    sparse_dnn_list = concat_embedding_list(dnn_feature_columns, sparse_input_dict, embedding_layer_dict, flatten=True) 

    sparse_dnn_inputs = Concatenate(axis=1)(sparse_dnn_list) # B x m*dim (n表示类别特征的数量,dim表示embedding的维度)

    # 将dense特征和Sparse特征拼接到一起
    dnn_inputs = Concatenate(axis=1)([dense_dnn_inputs, sparse_dnn_inputs]) # B x (n + m*dim)

    # 输入到dnn中,需要提前定义需要几个残差块
    output_layer = get_dnn_logits(dnn_inputs, block_nums=3)

    model = Model(input_layers, output_layer)
    return model

build_input_layers函数
用于构建输入层,用字典的形式返回dense和sparse数据。

def build_input_layers(feature_columns):
    """
    构建输入层
    param feature_columns: 数据集中的所有特征对应的特征标记之
    """
    # 构建Input层字典,并以dense和sparse两类字典的形式返回
    dense_input_dict, sparse_input_dict = {}, {}

    for fc in feature_columns:
        if isinstance(fc, SparseFeat):
            sparse_input_dict[fc.name] = Input(shape=(1, ), name=fc.name)
        elif isinstance(fc, DenseFeat):
            dense_input_dict[fc.name] = Input(shape=(fc.dimension, ), name=fc.name)
        
    return dense_input_dict, sparse_input_dict

build_embedding_layers函数
用于创建sparse特征的embeddin。

def build_embedding_layers(feature_columns, input_layers_dict, is_linear):
    # 定义一个embedding层对应的字典
    embedding_layers_dict = dict()
    
    # 将特征中的sparse特征筛选出来
    sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), feature_columns)) if feature_columns else []
    
    # 如果是用于线性部分的embedding层,其维度为1,否则维度就是自己定义的embedding维度
    if is_linear:
        for fc in sparse_feature_columns:
            embedding_layers_dict[fc.name] = Embedding(fc.vocabulary_size + 1, 1, name='1d_emb_' + fc.name)
    else:
        for fc in sparse_feature_columns:
            embedding_layers_dict[fc.name] = Embedding(fc.vocabulary_size + 1, fc.embedding_dim, name='kd_emb_' + fc.name)
    
    return embedding_layers_dict

concat_embedding_list函数
用于将所有的sparse特征embedding拼接

def concat_embedding_list(feature_columns, input_layer_dict, embedding_layer_dict, flatten=False):
    # 将sparse特征筛选出来
    sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), feature_columns))

    embedding_list = []
    for fc in sparse_feature_columns:
        _input = input_layer_dict[fc.name] # 获取输入层 
        _embed = embedding_layer_dict[fc.name] # B x 1 x dim  获取对应的embedding层
        embed = _embed(_input) # B x dim  将input层输入到embedding层中

        # 是否需要flatten, 如果embedding列表最终是直接输入到Dense层中,需要进行Flatten,否则不需要
        if flatten:
            embed = Flatten()(embed)         
        embedding_list.append(embed)
    
    return embedding_list 

get_dnn_logits函数
用于将dnn的输出转化成logits

# block_nums表示DNN残差块的数量
def get_dnn_logits(dnn_inputs, block_nums=3):
    dnn_out = dnn_inputs
    for i in range(block_nums):
        dnn_out = ResidualBlock(64)(dnn_out)
    
    # 将dnn的输出转化成logits
    dnn_logits = Dense(1, activation='sigmoid')(dnn_out)

    return dnn_logits

调用ResidualBlock类,运行call方法。

# DNN残差块的定义
class ResidualBlock(Layer):
    def __init__(self, units): # units表示的是DNN隐藏层神经元数量
        super(ResidualBlock, self).__init__()
        self.units = units

    def build(self, input_shape):
        out_dim = input_shape[-1]
        self.dnn1 = Dense(self.units, activation='relu')
        self.dnn2 = Dense(out_dim, activation='relu') # 保证输入的维度和输出的维度一致才能进行残差连接
    def call(self, inputs):
        x = inputs
        x = self.dnn1(x)
        x = self.dnn2(x)
        x = Activation('relu')(x + inputs) # 残差操作
        return x

3. 查看参数概况,定义损失函数、优化函数、评估指标,训练模型

history.summary()
history.compile(optimizer="adam", 
            loss="binary_crossentropy", 
            metrics=["binary_crossentropy", tf.keras.metrics.AUC(name='auc')])

# 将输入数据转化成字典的形式输入
train_model_input = {name: data[name] for name in dense_features + sparse_features}
# 模型训练
history.fit(train_model_input, train_data['label'].values,
        batch_size=64, epochs=5, validation_split=0.2, )

代码放至DeepCrossing

参考资料:
深度学习推荐系统模型搭建基础
DeepCrossing
Embedding+MLP:如何用TensorFlow实现经典的深度学习模型?

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于深度学习的推荐算法——推荐系统模型搭建基础及DeepCrossing原理 的相关文章

随机推荐

  • Git如何删除本地仓库

    删除仓库 就是需要删除仓库文件夹下隐藏的 git 文件夹 进入项目所在目录 打开git bash 开始删除本地仓库 显示所有本地分支 初始化时只有一个master分支 git branch 初始化本地版本库 重新初始化一次 可以忽略 git
  • 数据结构——队列

    创建队列 塞值和拿值 当我们创建一个LinkedList的时候 就可以用来模拟队列 因为该集合里有大量操作首尾元素的方法 之后就可以在该队列里进行数据的添加和获取 但是当我们使用数组来实现时 如何创建一个队列呢 最大值怎么确定 首尾初始值怎
  • 新闻主题识别及其热点演化分析流程

    1 数据收集 收集与科技新闻相关的大量文本数据 包括新闻报道 评论 社交媒体等 2 数据预处理 对收集到的文本数据进行清洗 去重 分词 停用词过滤等处理 3 特征提取 采用TF IDF Word2Vec等技术进行文本特征提取 将文本转化为向
  • Android JNI打印logcat日志

    在 JNI 中打印日志可以使用 android log print 函数来实现 该函数是 Android NDK 提供的一个用于在本地代码中输出日志消息到 logcat 的方法 要在 JNI 中打印日志 请按照以下步骤进行操作 在你的 JN
  • verilog除法器设计

    除法器原理 和十进制除法类似 计算 27 除以 5 的过程如下所示 除法运算过程如下 1 取被除数的高几位数据 位宽和除数相同 实例中是 3bit 数据 2 将被除数高位数据与除数作比较 如果前者不小于后者 则可得到对应位的商为 1 两者做
  • 线程池的基本创建方式and执行流程!

    一 什么是线程池 1 线程池 线程池是一种线程管理的机制 它是一组线程的集合 可以用来执行多个任务 线程池维护了一个固定数量的线程集合 可以从线程池中取出一个线程来执行任务 当任务执行完毕后 线程又会返回到线程池中 等待下一个任务的到来 当
  • 大学生电赛学习之路

    可以看到全国大学生电子设计大赛的认可度是非常高的 属于A类学科竞赛 电子设计大赛的形式也是给出题目 参赛者有四天三夜的时间解决问题 题目大致可以分为 控制类 测量类 高频类以及电力电子类 该竞赛会在单数年的9月份举行 但是对于新生来说电赛确
  • apk部分手机安装失败_安装apk解析包时出现错误怎么办?小编快速帮你解决

    Android智能手机的用户都会或多或少的遇到到这样的问题 安装apk解析包时出现错误 那么遇到这样的问题该如何解决 其实导致apk程序安装失败的原因是多方面的 而这所有的错误都基本归结于一点 解析包时出现错误 因此 我们只有采取逐个排查的
  • B、M、MB、K、KB、G、GB的关系

    一 概念 1 比特Bit 位 b 表示信息的最小单位 即计算机最小的存储单位 是二进制数的一位包含的信息或2个选项中特别指定1个的需要信息量 它代表从一个二进制数组中选出一元 0或1 所提供的信息量 若此二元出现的概率相等 2 字节Byte
  • Uncaught TypeError: Cannot read property ‘Component‘ of undefined

    Cannot read property Component of undefined 1 问题描述 2 解决方案 问题描述 上一篇文章中在本地搭建了第一个React项目后 跟着react中文文档学习 封装时钟的外观代码如下 class C
  • 23、账号与权限管理

    账号与权限管理 例如 生产当中 要自己创建账号 30天与 90天账号密码 本章内容 管理员用户账号和组账号 管理目录和文件的属性 文章目录 1 组的分类 2 用户的分类 3 passwd 文件账号记录详细说明 4 charge 命令 修改账
  • 关于FLAG_ACTIVITY_MULTIPLE_TASK

    FLAG ACTIVITY MULTIPLE TASK 总结 1 使用改标记 需要自行管理Activity 2 需要与FLAG ACTIVITY NEW DOCUMENT或者FLAG ACTIVITY NEW TASK共同使用 This f
  • Error in created hook: “TypeError: Object(...) is not a function“

    最近我在请求列表的时候发现请求列表报这个错 起初我是以为我的参数没有对 传给后台的是个对象 而不是json对象 但是我记得我转换了的 后来发现是我在引入封装请求的时候写错了 代码如下 import imgList from api base
  • 华为服务器手机日志文件损坏,服务器日志文件

    服务器日志文件 内容精选 换一换 华为云帮助中心 为用户提供产品简介 价格说明 购买指南 用户指南 API参考 最佳实践 常见问题 视频帮助等技术文档 帮助您快速上手使用华为云服务 客户端IP指的是访问者 用户设备 的IP地址 在Web应用
  • 自动驾驶通信中间件ecal源码分析—1. 什么是ecal

    前面做了一套自动驾驶通信中间件 阅读了ROS 1 0 roscpp和ecal相关源码 接下来将逐步对ecal的核心源码进行简要分析 1 什么是ecal ecal是德国大陆开源的一套自动驾驶通信中间件 The enhanced Communi
  • 获取input上传文件文件名及扩展名

    1 使用input标签的type file 上传文件 获取上传文件的文件名和扩展名 代码如下
  • 安装httpd软件包

    安装httpd软件包 root www yum repos d mount dev sr0 media mount media WARNING device write protected mounted read only root ww
  • ESP32运行microPython报错:rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee

    使用Esp32调试microPython的时候 上传代码后一直报错 附 报错原因 以及解决方法 Arduino以及idf 均会出现 如下内容 无限循环打印 rst 0xc SW CPU RESET boot 0x13 SPI FAST FL
  • 自动化测试文档整理

    自动化测试 广义上讲一切使用代码或程序来辅助或代替人工测试的行为都认为是自动化测试 自动化测试作用 提高工作效率 释放人力 可以把人从一些重复性的工作中解放出来 应用环节 主要应用在回归测试和兼容性测试 适合自动化测试产品特点 1 需求比较
  • 基于深度学习的推荐算法——推荐系统模型搭建基础及DeepCrossing原理

    本章节从推荐系统模型搭建基础和DeepCrossing原理讲解及实操两方面展开 一 推荐系统模型搭建基础 1 Keras搭建模型 keras搭建模型主要有两种模式 一种是Sequential API 另外一种是Functional API