2019-05-08 | Projects | UNLOCK

DeepFM Implementation with Tensorflow

论文解读

论文提到的DeepFM网络结构图如下所示,
DeeFM网络结构图
该算法主要特点如下:

  • 网络结构分为FM部分和Deep部分
  • FM主要做特征组合,包括一阶特征和二阶特征
  • Deep部分用来对embedding后的表示提取高纬特征
  • 两部分直接共享特征embedding权重参数
  • 最后将FM和Deep部分的输出进行concat采用sigmod函数进行输出
  • 不需要人工设计特征组合,网络end to end学习组合特征

FM Part

FM(Factorization Machine)模型主要是用来获取组合特征,模型如下:

上面是采用向量形式进行表示,这里我们沿用FM原论文中的表示形式,

其中,$n$表示特征个数,$V_i$表示$k$维特征隐向量。下面我们来对上面这个公式具体推到,获得最终计算形式,
上式中$\sum_{i=1}^{n}\sum_{j=i+1}^nx_i x_j$这个部分表示的是二级特征,这个表示形式是对称矩阵的上三角部分,分析如下
对称矩阵A如下:

对称矩阵A的上三角表示形式为:

根据对称矩阵的性质,上三角矩阵的元素和等于矩阵所有元素和减去对角线之和的一半,所以FM公式二阶特征可以做如下转换
FM公式推导
因此最终的FM部分的公式变成如下形式

对公式进行符号表示,便于后续实现时进行说明,

上述形式也是在进行算法实现时所采用的的形式.

Deep Part

论文中deep部分是将embedding后的结果联合一阶特征$A_1$ 和二阶特征进行concat,送入DNN,计算公式如下

其中$|H|$表示DNN的网络层数,$a$表示上一层网络输出。

数据集准备及分析

模型实现采用kaggle 比赛中公开数据集。数据情况如下

  • 数据总共有40个字段,字段间用tab键进行分开
  • 第一个字段,即Field0 表示$Label$ 值为1或0,表示该ad被点击与否
  • I1-I13,共13个字段表示数值字段,通常一些计数值
  • C14-I39,共26个字段为categoryical 字段,即离散类别型特征
  • 字段值不存在时,默认为空
  • 所以字段进行了脱敏,没有具体实际指代含义

实现逻辑和流程

整个系统实现逻辑流程如图所示。
逻辑框架

数据处理

数据处理这里采用主要原则是,将所有数据集中的所有值都当成特征值进行处理,对于连续值,将整个字段作为一个特征,这样所有的特征值都存在于同一个字典中进行编码。(原论文采用此种方式处理,至于初衷还在思考中),按照论文所说,需要对每个categoryical字段单独进行one-hot这样做下来,单条encode后的数据维度会很大,这取决于每个field的bucket切分,处理起来较麻烦,这也许是作者采用前述方法的主要原因。这里我在进行算法实现时对数据做如下处理:

    1. 对continuous field, 即数据中的数值:
    • normalize到(0,1),采用该区间内$norm=\frac{val-min}{max-min}$,首先寻找每个字段中的最大值和最小值,存放在字典中
    • id编码,对于连续性或者数值型字段,将整个字段作为一个feature,编码为一个id,value就是归一化后的值
    1. categorical field ,离散型字段
    • 在每个字段中统计每个value出现的频率,通过设定频率阈值进行过滤,生成字段对应字典,
    • id编码,将所有字段中的值统一到同一个字典中进行编码,id是在连续字段编码基础上进行累加,
    1. 数据处理结果
      采用以上方式对数据进行处理,最终形成的数据格式是,, 数据以tab为分割符有三个字段,第一个字段为feature value是数据归一化后的值或者是one-hot后的1,第二个字段是前一个字段对应的feature id, 第三个字段为该条数据的label
      生成字典格式如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      feat_dict = {
      "1": 0,
      "2": 1,
      "14": {
      "68fd1e64": 2,
      "287e684f": 3,
      "8cf07265": 4},
      "feat_size": 121641,
      "<UNK>": 121640
      }

      按照以上方式对数据进行转换,遵照原始数据的顺序按照$8:1:1$拆分为train/dev/test数据集,用于做模型训练和测试。

数据处理部分code可以在data_reader.py中找到,代码采用multi-thread对数据进行处理,因为文件特别大,采用多进程来提高处理速度,对于categorical字段的过滤阈值,是通过统计每个字段中值的频率,进行设定,也可以将所有字段设置为同一个阈值,比如频次为10等。

思考

  1. 对于categoryical特征进行处理方式,按照原文的思路是对所有离散特征放在一起进行one-hot,和每个字段单独one-hot进行处理,这两种方式对最终模型效果是否有较大影响?用LR做排序的时候,处理对象也是有多大40多个字段包含连续和离散,处理方式是将所有字段都转换为类别型特征,单独进行one-hot encoding。我考虑到如果每个字段单独one-hot,也就需要后面单独创建embedding进行lookup,如果有20个字段那就需要20个tabel进行学习,在实现上不便利,而且每个table值都是需要进行学习的。

系统设计

  • input_fn
    该模块主要作用是对已经转换为value sequence, value id sequence, label数据,使用$tf.dataset.API$ 进行pipeline处理,最终输出内容为

    1
    2
    3
    4
    5
    6
    inputs = {
    "values": values,
    "indices": indices,
    "labels": tf.reshape(labels, shape=[-1, 1]),
    "iterator_init_op": init_op
    }

    主要的处理步骤:

    • 利用tf.dataset.api,这里采用的是tf.data.TextLineDataset从数据文件中读取数据,返回结果是tf.dataset 形式
    • 利用dataset.map()操作对读入的每行数据进行parse,这里parse的结果有三部分,values, indices, labels, 在进行parse的同时,需要对数据进行padding,这里根据字段个数,sequence length 是39,对于不足该长度的用0进行padding,indices用padding_index 代替,最终保证每个数据长度都是一致;还有一种是直接利用dataset.padding_batch,根据batch中最长的sequence长度对其他数据进行padding
    • 创建batches iterator, 根据是否处于”train” or “test”决定是否对数据进行随机打乱,代码如下:

      1
      2
      3
      dataset = dataset.shuffle(buffer_size=buffer_size)
      .batch(batch_size)
      .prefetch(1) # always keep one batch ready to serve
    • 创建迭代器,拆解batch tensor作为输入, 制作inputs字典

      1
      2
      3
      iterator = dataset.make_initializable_iterator()
      init_op = iterator.initializer
      (values, indices, labels) = iterator.get_next()

    以上是数据处理pipeline,充分利用tf的数据处理能力,可以作为通用处理模式,对任意算法任务建立类似的处理逻辑,需要根据具体任务修改parse函数,这是input_fn实现。

  • build_model
    该模块主要用来构造DeepFM网络模型, 为了能够支持单条数据的inference,在构建的模型时,只采用三个函数,$inference$和$loss$,网络搭建在$inference$函数中实现,$loss$用来计算模型损失。
    网络搭建的主要过程如下(这里尽量名字与code中一致):

Variable Input Shape output Shape Notes
feature_embedding [feat_size, emb_size] None sparse feature to dense feature
feature_bias [feat_size, 1] None weight of order-1
DNN [field_size*emb_size, hidden_layer[0]] Full Connect

这里只是简单列了主要的一些变量,详细的请参考code

  • build_model_spec
    该模块主要是模型设定,model specification 会包括模型训练或预测过程中各种operator、loss、评估指标、输入输出tensor等,在本实现中分两种情况来创建model specification。

    • train mode

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      model_spec['loss'] = loss
      model_spec['accuracy'] = acc
      model_spec['metrics_init_op'] = metrics_init_op
      model_spec['metrics'] = metrics
      model_spec['update_metrics'] = update_metrics_op
      model_spec['summary_op'] = tf.summary.merge_all()
      model_spec['train_op'] = train_op
      model_spec['variable_init_op'] = variable_init_op
      model_spec['prediction'] = prediction
      model_spec['score'] = score
    • eval mode

      1
      2
      3
      4
      5
      6
      7
      8
      9
      model_spec['loss'] = loss
      model_spec['accuracy'] = accuracy
      model_spec['metrics_init_op'] = metrics_init_op
      model_spec['metrics'] = metrics
      model_spec['update_metrics'] = update_metrics_op
      model_spec['summary_op'] = tf.summary.merge_all()
      model_spec['variable_init_op'] = variable_init_op
      model_spec['prediction'] = prediction
      model_spec['score'] = score

    具体每个op代表的含义可以查看代码model_fn.py

  • train_evaluate
    该模块主要是定义train session 和evaluate session,这些函数用来定义在一个epoch中如何进行模型训练和预测,train_session 如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    train_op = model_spec['train_op']
    loss = model_spec['loss']
    update_metrics = model_spec['update_metrics'] # loop over all dataset
    summary_op = model_spec['summary_op']
    metrics = model_spec['metrics']
    global_step = tf.train.get_or_create_global_step() # get global train step

    # Step2, initialize variables
    sess.run(model_spec['metrics_init_op']) # metrics op
    sess.run(model_spec['iterator_init_op']) # iterator op

    # Step3, loop train steps
    # use tqdm trange as process bar
    t = trange(num_steps)
    for i in t:
    # write summary after summary_steps
    if i % params.save_summary_steps == 0:
    _, loss_val, _, summary_val, step_val = sess.run([train_op, loss, update_metrics,
    summary_op, global_step])
    writer.add_summary(summary_val, step_val)
    else:
    _, _, loss_val = sess.run([train_op, update_metrics, loss])
    t.set_postfix(loss='{:05.3f}'.format(loss_val))

    # Step4 print metrics
    metric_val_tensor = {k: v[0] for k, v in metrics.items()}
    metric_vals = sess.run(metric_val_tensor)
    metric_vals_str = ' ; '.join('{}: {:05.3f}'.format(k,v) for k, v in metric_vals.items())
    logging.info('- Train Metrics: '+ metric_vals_str)

    对于代码详细内容请查看文件train_evaluate.py

  • inference
    inference 模块是根据eval_session 函数改写而成方便单独做预测

  • train
    train.py训练和预测主入口,训练流程,首先读取数据为datast,将dataset制作为模型所需要的iterator形式,再创建模型,创建model specification, 最后进行训练。

    模型参数

    模型参数汇总如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    "test_size": 4584061,
    "field_size": 39,
    "train_size": 3667249,
    "dev_size": 4584062,
    "padding_value": 121640,

    "feat_size": 121641,
    "batch_size": 2048,
    "epochs": 10,
    "learning_rate": 0.001,
    "buffer_size": 1,
    "fm_dropout_keep": 1.0,
    "dropout_keep_prob": 0.5,
    "l2_reg": 0.1,
    "embedding_size": 10,
    "hidden_layers":"32,32",
    "save_summary_steps": 10
    }

效果评估

对算法进行测试,这里考察的指标是logloss和accuracy,目前训练阶段的logloss最低为0.4,acc为0.75,eval最好数据是logloss:1.5, acc:0.74. 测试结果来看,并未达到网上所说auc0.80的效果,logloss为0.4的结果。
另外一个问题整体模型GPU使用率特别低只有10%不到,需要提高并行性以提高性能。

性能优化

结论分析

参考文献

  1. FM

评论加载中