热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

用TensorFlow实现支持多值、稀疏、共享权重的DeepFM

缘起DeepFM不算什么新技术了,用TensorFlow实现DeepFM也有开源实现,那我为什么要炒这个冷饭,重复造轮子?用Google搜索“TensorFlow+DeepFM”,

缘起

DeepFM不算什么新技术了,用TensorFlow实现DeepFM也有开源实现,那我为什么要炒这个冷饭,重复造轮子?

用Google搜索“TensorFlow+DeepFM”,一般都能搜索到“ChenglongChen/tensorflow-DeepFM”和“lambdaJi的TensorFlow Estimator of DeepFM”这二位的实现。二位不仅用TensorFlow实现了DeepFM,还在Criteo数据集上,给出了完整的训练、测试的代码,的确给了我很大的启发,在这里要表示感谢。

但是,同样是由于二位的实现都是根据Criteo简单数据集的,使他们的代码,如果移植到实际的推荐系统中,存在一定困难。比如:

稀疏要求。尽管criteo的原始数据集是排零存储的,但是以上的两个实现,都是用稠密矩阵来表示输入,将0又都补了回来。这种做法,在criteo这种只有39列的简单数据集上是可行的,但是实际系统中,特征数量以千、万计,这种稀疏转稠密的方式是不可取的。

一列多值的要求。Criteo数据集有13列numeric特征+26列categorical特征,所有列都只有一个值。但是,在实际系统中,一个field下往往有多个对。比如,我们用三个field来描述一个用户的手机 使用习惯,“近xxx天活跃app”+“近xxx天新安装app”+“近xxx天卸载app”。每个field下,再有“微信:0.9,微博:0.5,淘宝:0.3,……”等一系列的feature和它们的数值。

这个要求固然可以通过,去除field这个“特征单位”,只针对一个个独立的feature来建模。但是,这样一来,既凭空增加了模型的规模,又破坏模型的“层次化”与“模块化”,使代码不易扩展与维护。

权值共享的要求。Criteo数据集经过脱敏感处理,我们无法知道每列的具体含义,自然也就没有列与列之间共享权重的需求,以上提到的两个实现也就只用一整块稠密矩阵来建模embedding矩阵。

但是,以上面提到的“近xxx天活跃app”+“近xxx天新安装app”+“近xxx天卸载app”这三个field为例,这些 field中的feature都来源于同一个”app字典”。如果不做权重共享,

  • 每个field都使用独立的embedding矩阵来映射app向量,整个模型需要优化的变量是共享权重模型的3倍,既耗费了更多的计算资源,也容易导致过拟合。
  • 每个field的稀疏程度是不一样的,同一个app,在“活跃列表”中出现得更频繁,其embedding向量就有更多的训练机会,而在“卸载列表”中较少出现,其embedding向量得不到足够训练,恐怕最后与随机初始化无异。

因此,在实际系统中,“共享权重”是必须的,

  • 减小优化变量的数目,既节省计算资源,又减轻“过拟合”风险
  • 同一个embedding矩阵,为多个field提供映射向量,类似于“多任务学习”,使每个embedding向量得到更多的训练机会,同时也要满足多个field的需求(比如同一个app的向量,既要体现‘经常使用它’对y的影响,也要体现‘卸载它’对y值的影响),也降低了“过拟合”的风险。

正因为在目前我能够找到的基于TensorFlow实现的DeepFM中,没有一个能够满足以上“稀疏”、“多值”、“共享权重”这三个要求的,所以,我自己动手实现了一个,代码见我的github。接下来,我简单讲解一下我的代码。

数据预处理

我依然用criteo数据集来做演示之用。为了演示“一列多值”和“稀疏”,我把criteo中的特征分为两个field,所有数值特征I1~I13归为numeric field,所有类别特征C1~C26归为categorical field。

需要特别指出的是:

  • 这种处理方法,不是为了提高criteo数据集上的模型性能,只是为了模拟实际系统中将会遇到的“一列多值”和“稀疏”数据集。接下来会看到,DeepFM中,FM中的二阶交叉,不会受拆分成两个field的影响。受影响的主要是Deep侧的输入层,详情见”DNN预测部分”一节 。
  • 另外,criteo数据集无法演示“权重共享”的功能。

对criteo中数值特征与类别特征,都是最常规的预处理,不是这次演示的重点

  • 数值特征,因为多数表示”次数”,因此先做了一个log变化,减弱长尾数据的影响,再做了一个min/max scaling,毕竟底层还是线性算法,要排除特征间不同scale的影响。注意,千万不能做“zero mean, unit variance”的standardize,因为那样会破坏数据的稀疏性
  • 类别特征,剔除了一些生僻的tag,建立字典,将原始数据中的字符串tag转化为整数的index

预处理的代码见criteo_data_preproc.py,处理好的数据文件如下所示,图中的亮块是列分隔符。可以看到,每列是由多个tag_index:value“键值对”组成的,而不同行中“键值对”个数互不同,而value绝没有0,实现排零、稀疏存储

《用TensorFlow实现支持多值、稀疏、共享权重的DeepFM》
《用TensorFlow实现支持多值、稀疏、共享权重的DeepFM》 输入数据

input_fn

为了配合TensorFlow Estimator,我们需要定义input_fn来读取上图所示的数据。

看似简单的任务,实现起来,却很花费了我一番功夫:

  • 网上能够搜到的TensorFlow读文本文件的代码,都是读“每列只有一个值的csv”这样规则的数据格式。但是,上图所示的数据,却非常不规则,每行先是由“\t”分隔,第列中再由“,”分隔成数目不同的“键值对”,每个‘键值对’再由“:”分隔
  • 我希望提供给model稀疏矩阵,方便model中排零计算,提升效率。

最终,解析一行文本的代码如下。

def _decode_tsv(line):
columns = tf.decode_csv(line, record_defaults=DEFAULT_VALUES, field_delim='\t')
y = columns[0]
feat_columns = dict(zip((t[0] for t in COLUMNS_MAX_TOKENS), columns[1:]))
X = {}
for colname, max_tokens in COLUMNS_MAX_TOKENS:
# 调用string_split时,第一个参数必须是一个list,所以要把columns[colname]放在[]中
# 这时每个kv还是'k:v'这样的字符串
kvpairs = tf.string_split([feat_columns[colname]], ',').values[:max_tokens]
# k,v已经拆开, kvpairs是一个SparseTensor,因为每个kvpair格式相同,都是"k:v"
# 既不会出现"k",也不会出现"k:v1:v2:v3:..."
# 所以,这时的kvpairs实际上是一个满阵
kvpairs = tf.string_split(kvpairs, ':')
# kvpairs是一个[n_valid_pairs,2]矩阵
kvpairs = tf.reshape(kvpairs.values, kvpairs.dense_shape)
feat_ids, feat_vals = tf.split(kvpairs, num_or_size_splits=2, axis=1)
feat_ids = tf.string_to_number(feat_ids, out_type=tf.int32)
feat_vals = tf.string_to_number(feat_vals, out_type=tf.float32)
# 不能调用squeeze, squeeze的限制太多, 当原始矩阵有1行或0行时,squeeze都会报错
X[colname + "_ids"] = tf.reshape(feat_ids, shape=[-1])
X[colname + "_values"] = tf.reshape(feat_vals, shape=[-1])
return X, y

然后,将整个文件转化成TensorFlow Dataset的代码如下所示。每一个field“xxx”在dataset中将由两个SparseTensor表示,“xxx_ids”表示sparse ids,“xxx_values”表示sparse values。

def input_fn(data_file, n_repeat, batch_size, batches_per_shuffle):
# ----------- prepare padding
pad_shapes = {}
pad_values = {}
for c, max_tokens in COLUMNS_MAX_TOKENS:
pad_shapes[c + "_ids"] = tf.TensorShape([max_tokens])
pad_shapes[c + "_values"] = tf.TensorShape([max_tokens])
pad_values[c + "_ids"] = -1 # 0 is still valid token-id, -1 for padding
pad_values[c + "_values"] = 0.0
# no need to pad labels
pad_shapes = (pad_shapes, tf.TensorShape([]))
pad_values = (pad_values, 0)
# ----------- define reading ops
dataset = tf.data.TextLineDataset(data_file).skip(1) # skip the header
dataset = dataset.map(_decode_tsv, num_parallel_calls=4)
if batches_per_shuffle > 0:
dataset = dataset.shuffle(batches_per_shuffle * batch_size)
dataset = dataset.repeat(n_repeat)
dataset = dataset.padded_batch(batch_size=batch_size,
padded_shapes=pad_shapes,
padding_values=pad_values)
iterator = dataset.make_one_shot_iterator()
dense_Xs, ys = iterator.get_next()
# ----------- convert dense to sparse
sparse_Xs = {}
for c, _ in COLUMNS_MAX_TOKENS:
for suffix in ["ids", "values"]:
k = "{}_{}".format(c, suffix)
sparse_Xs[k] = tf_utils.to_sparse_input_and_drop_ignore_values(dense_Xs[k])
# ----------- return
return sparse_Xs, ys

其中也不得不调用padded_batch补齐,这一步也将稀疏格式转化成了稠密格式,不过只是在一个batch(batch_size=128已经算很大了)中临时稠密一下,很快就又通过调用to_sparse_input_and_drop_ignore_values这个函数重新转化成稀疏格式了。to_sparse_input_and_drop_ignore_values实际上是从feature_column.py这个module中的_to_sparse_input_and_drop_ignore_values函数拷贝而来,因为原函数不是public的,无法在featurecolumn.py以外调用,所以我将它的代码拷贝到tf_utils.py中。

建立共享权重

重申几个概念。比如我们的特征集中包括active_pkgs(app活跃情况)、install_pkgs(app安装情况)、uninstall_pkgs(app卸载情况)。每列包含的内容是一系列feature和其数值,比如qq:0.1, weixin:0.9, taobao:1.1, ……。这些feature都来源于同一份名为package的字典

  • field就是active_pkgs、install_pkgs、uninstall_pkgs这些大类,是DataFrame中的每一列
  • feature就是每个field下包含的具体内容,一个field下允许存在多个feature,比如前面提到的qq, weixin, taobao这样的app名称。
  • vocabulary对应例子中的“package字典”。不同field下的feature可以来自同一个vocabulary,即若干field共享vocabulary

建立共享权重的代码如下所示:

  • 一个vocab对应两个embedding矩阵,一个对应FM中的线性部分的权重,另一个对应FM与DNN共享的隐向量(用于二阶与高阶交叉)。
  • 所有embedding矩阵,以”字典名”存入dict。不同field只要指定相同的“字典名”,就可以共享同一套embedding矩阵

class EmbeddingTable:
def __init__(self):
self._weights = {}
def add_weights(self, vocab_name, vocab_size, embed_dim):
""" :param vocab_name: 一个field拥有两个权重矩阵,一个用于线性连接,另一个用于非线性(二阶或更高阶交叉)连接 :param vocab_size: 字典总长度 :param embed_dim: 二阶权重矩阵shape=[vocab_size, order2dim],映射成的embedding 既用于接入DNN的第一屋,也是用于FM二阶交互的隐向量 :return: None """
linear_weight = tf.get_variable(name='{}_linear_weight'.format(vocab_name),
shape=[vocab_size, 1],
initializer=tf.glorot_normal_initializer(),
dtype=tf.float32)
# 二阶(FM)与高阶(DNN)的特征交互,共享embedding矩阵
embed_weight = tf.get_variable(name='{}_embed_weight'.format(vocab_name),
shape=[vocab_size, embed_dim],
initializer=tf.glorot_normal_initializer(),
dtype=tf.float32)
self._weights[vocab_name] = (linear_weight, embed_weight)
def get_linear_weights(self, vocab_name): return self._weights[vocab_name][0]
def get_embed_weights(self, vocab_name): return self._weights[vocab_name][1]
def build_embedding_table(params):
embed_dim = params['embed_dim'] # 必须有统一的embedding长度
embedding_table = EmbeddingTable()
for vocab_name, vocab_size in params['vocab_sizes'].items():
embedding_table.add_weights(vocab_name=vocab_name, vocab_size=vocab_size, embed_dim=embed_dim)
return embedding_table

线性预测部分

def output_logits_from_linear(features, embedding_table, params):
field2vocab_mapping = params['field_vocab_mapping']
combiner = params.get('multi_embed_combiner', 'sum')
fields_outputs = []
# 当前field下有一系列的对,每个tag对应一个bias(待优化),
# 将所有tag对应的bias,按照其value进行加权平均,得到这个field对应的bias
for fieldname, vocabname in field2vocab_mapping.items():
sp_ids = features[fieldname + "_ids"]
sp_values = features[fieldname + "_values"]
linear_weights = embedding_table.get_linear_weights(vocab_name=vocabname)
# weights: [vocab_size,1]
# sp_ids: [batch_size, max_tags_per_example]
# sp_weights: [batch_size, max_tags_per_example]
# output: [batch_size, 1]
output = embedding_ops.safe_embedding_lookup_sparse(linear_weights, sp_ids, sp_values,
combiner=combiner,
name='{}_linear_output'.format(fieldname))
fields_outputs.append(output)
# 因为不同field可以共享同一个vocab的linear weight,所以将各个field的output相加,会损失大量的信息
# 因此,所有field对应的output拼接起来,反正每个field的output都是[batch_size,1],拼接起来,并不占多少空间
# whole_linear_output: [batch_size, total_fields]
whole_linear_output = tf.concat(fields_outputs, axis=1)
tf.logging.info("linear output, shape={}".format(whole_linear_output.shape))
# 再映射到final logits(二分类,也是[batch_size,1])
# 这时,就不要用任何activation了,特别是ReLU
return tf.layers.dense(whole_linear_output, units=1, use_bias=True, activation=None)

二阶交互预测部分

二阶交互部分与DeepFM论文中稍有不同,而是使用了《Neural Factorization Machines for Sparse Predictive Analytics》中Bi-Interaction的公式。这也是网上实现的通用做法。

《用TensorFlow实现支持多值、稀疏、共享权重的DeepFM》

而我的实现与上边公式最大的不同,就是不再只有一个embedding矩阵V,而是每个feature根据自己所在的field,再根据超参指定的field与vocabulary的映射关系,找到自己对应的embedding矩阵。某个field对应的embedding矩阵有可能是与另外一个field共享的。

另外, 《用TensorFlow实现支持多值、稀疏、共享权重的DeepFM》
《用TensorFlow实现支持多值、稀疏、共享权重的DeepFM》

小结

本文展示了我写的一套基于TensorFlow的DeepFM的实现。重点阐述了“一列多值”、“稀疏”、“权重共享”在实际推荐系统中的重要性,和我是如何在DeepFM中实现以上需求的。欢迎各位看官指正。


推荐阅读
  • 自然语言处理(NLP)——LDA模型:对电商购物评论进行情感分析
    目录一、2020数学建模美赛C题简介需求评价内容提供数据二、解题思路三、LDA简介四、代码实现1.数据预处理1.1剔除无用信息1.1.1剔除掉不需要的列1.1.2找出无效评论并剔除 ... [详细]
  • java解析json转Map前段时间在做json报文处理的时候,写了一个针对不同格式json转map的处理工具方法,总结记录如下:1、单节点单层级、单节点多层级json转mapim ... [详细]
  • Visual Studio Code (VSCode) 是一款功能强大的源代码编辑器,支持多种编程语言,具备丰富的扩展生态。本文将详细介绍如何在 macOS 上安装、配置并使用 VSCode。 ... [详细]
  • vue引入echarts地图的四种方式
    一、vue中引入echart1、安装echarts:npminstallecharts--save2、在main.js文件中引入echarts实例:  Vue.prototype.$echartsecharts3、在需要用到echart图形的vue文件中引入:   importechartsfrom"echarts";4、如果用到map(地图),还 ... [详细]
  • 本文通过基准测试(Benchmark)对.NET Core环境下Thrift和HTTP客户端的微服务通信性能进行对比分析。基准测试是一种评估系统或组件性能的方法,通过运行一系列标准化的测试来衡量其表现。 ... [详细]
  • 本文介绍了 Python 中的基本数据类型,包括不可变数据类型(数字、字符串、元组)和可变数据类型(列表、字典、集合),并详细解释了每种数据类型的使用方法和常见操作。 ... [详细]
  • 本文介绍了如何使用Python爬取妙笔阁小说网仙侠系列中所有小说的信息,并将其保存为TXT和CSV格式。主要内容包括如何构造请求头以避免被网站封禁,以及如何利用XPath解析HTML并提取所需信息。 ... [详细]
  • 本文介绍了 Go 语言中的高性能、可扩展、轻量级 Web 框架 Echo。Echo 框架简单易用,仅需几行代码即可启动一个高性能 HTTP 服务。 ... [详细]
  • 2020年9月15日,Oracle正式发布了最新的JDK 15版本。本次更新带来了许多新特性,包括隐藏类、EdDSA签名算法、模式匹配、记录类、封闭类和文本块等。 ... [详细]
  • 机器学习算法:SVM(支持向量机)
    SVM算法(SupportVectorMachine,支持向量机)的核心思想有2点:1、如果数据线性可分,那么基于最大间隔的方式来确定超平面,以确保全局最优, ... [详细]
  • 本文节选自《NLTK基础教程——用NLTK和Python库构建机器学习应用》一书的第1章第1.2节,作者Nitin Hardeniya。本文将带领读者快速了解Python的基础知识,为后续的机器学习应用打下坚实的基础。 ... [详细]
  • Hadoop的文件操作位于包org.apache.hadoop.fs里面,能够进行新建、删除、修改等操作。比较重要的几个类:(1)Configurati ... [详细]
  • 如果应用程序经常播放密集、急促而又短暂的音效(如游戏音效)那么使用MediaPlayer显得有些不太适合了。因为MediaPlayer存在如下缺点:1)延时时间较长,且资源占用率高 ... [详细]
  • 利用python爬取豆瓣电影Top250的相关信息,包括电影详情链接,图片链接,影片中文名,影片外国名,评分,评价数,概况,导演,主演,年份,地区,类别这12项内容,然后将爬取的信息写入Exce ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
author-avatar
丽君朝婷8
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有