FFM算法原理及Bi-FFM算法实现

1. FFM算法来源

FFM(Field-aware Factorization Machine)算法是FM(Factorization Machine)算法的升级版,FM算法的公式如下:

其中:样本   是   维向量,  是第   个维度上的值。  是   对应的长度为   的隐向量。

FFM则是将隐向量   又进一步细化,引入 field 概念,将特征所在的不同的field 这个信息也考虑进去。公式表达如下所示:

其中:   是第  个特征所属的field。由于隐向量   的长度为  ,FFM的二次参数有   个,远多于 FM 模型的   个。

FFM与FM的区别在于隐向量由原来的   变成了   ,这意味着每个特征对应的不是唯一的一个隐向量,而是一组隐向量。当   特征与   特征进行交叉时,   特征会从   的一组隐向量中选择出与特征   的域   对应的隐向量   进行交叉。同理,  也会选择与   的域   对应的隐向量   进行交叉。

2. 举例说明FFM算法的细节

下面给出一个例子:

此示例中一共有4个fields,5个features,细节如下:

FFM的特征组合方式为:

对于FFM的某个特征来说,会构造出  个隐向量,与其他的   个 fields组合的时候分别使用。FFM有什么特点呢?相对FM来说,参数量扩大了  倍,效果自然也比FM好,但是要想把它用到现实场景中是有问题的,参数量太大导致特别耗内存、训练速度慢,所以微博团队改进了一个新模型,双线性FFM算法(Bi-FFM)。

3. 优化方法:Bi-FFM算法

3.1 Bi-FFM二阶表达式如下

3.2 图形表示如下

Bi-FFM的思想:  分别使用一个隐向量来表达,但是把两个特征交互的信息用一个共享参数矩阵表示。关于共享矩阵   ,给出了三种形式:

  • (1)共享同一个   ,这是参数量最小的一种形式。   的参数量是  ,  是特征Embedding的size;

  • (2)每个field共享一个   ,即每个field各自学各自的   ;

  • (3)每两个fields对共享一个   ,能更加细化地描述特征组合;

3.3 FFM与Bi-FFM的参数量比较

改进的Bi-FFM,它的参数量跟FFM相比是什么情况?如果用Criteo这个4500万的数据集,它有230万个特征,39个Fields,假设Embedding size是10。如果用FFM就会有8.97亿的参数量。Bi-FFM的FM部分是大概2300万的参数,在三个改进版共享矩阵   中,类型(1)100个参数;类型(2)3900个参数;类型(3)15万参数。与FFM相比,参数差了38倍,但性能两者是相当的,这就是Bi-FFM的价值所在。

4. 实践出真知:Bi-FFM算法实

4.1 首先瞅一瞅 BilinearInteraction Layer代码

import tensorflow as tf
from tensorflow.keras.layers import Layer
from tensorflow.keras import backend as K
import itertools
from tensorflow.keras.initializers import (Zeros, glorot_normal, glorot_uniform)
from tensorflow.keras.layers import Concatenate

class BilinearInteraction(Layer):
    """
      Input shape
        - A list of 3D tensor with shape: ``(batch_size,1,embedding_size)``.
      Output shape
        - 3D tensor with shape: ``(batch_size,1,embedding_size)``.
      Arguments
        - **str** : String, types of bilinear functions used in this layer.
        - **seed** : A Python integer to use as random seed.
    """

    def __init__(self, bilinear_type="each", seed=1024, **kwargs):
        self.bilinear_type = bilinear_type
        self.seed = seed

        super(BilinearInteraction, self).__init__(**kwargs)

    def build(self, input_shape):

        if not isinstance(input_shape, list) or len(input_shape) < 2:
            raise ValueError('A `AttentionalFM` layer should be called on a list of at least 2 inputs')
        embedding_size = int(input_shape[0][-1])

        if self.bilinear_type == "all":
            self.W = self.add_weight(shape=(embedding_size, embedding_size), initializer=glorot_normal(
                seed=self.seed), name="bilinear_weight")
        elif self.bilinear_type == "each":
            self.W_list = [self.add_weight(shape=(embedding_size, embedding_size), initializer=glorot_normal(
                seed=self.seed), name="bilinear_weight" + str(i)) for i in range(len(input_shape) - 1)]
        elif self.bilinear_type == "interaction":
            self.W_list = [self.add_weight(shape=(embedding_size, embedding_size), initializer=glorot_normal(
                seed=self.seed), name="bilinear_weight" + str(i) + '_' + str(j)) for i, j in
                           itertools.combinations(range(len(input_shape)), 2)]
        else:
            raise NotImplementedError

        super(BilinearInteraction, self).build(
            input_shape) # Be sure to call this somewhere!

    def call(self, inputs, **kwargs):

        if K.ndim(inputs[0]) != 3:
            raise ValueError(
                "Unexpected inputs dimensions %d, expect to be 2 dimensions" % (K.ndim(inputs)))

        if self.bilinear_type == "all":
            p = [tf.multiply(tf.tensordot(v_i, self.W, axes=(-1, 0)), v_j)
                 for v_i, v_j in itertools.combinations(inputs, 2)]
        elif self.bilinear_type == "each":
            p = [tf.multiply(tf.tensordot(inputs[i], self.W_list[i], axes=(-1, 0)), inputs[j])
                 for i, j in itertools.combinations(range(len(inputs)), 2)]
        elif self.bilinear_type == "interaction":
            p = [tf.multiply(tf.tensordot(v[0], w, axes=(-1, 0)), v[1])
                 for v, w in zip(itertools.combinations(inputs, 2), self.W_list)]
        else:
            raise NotImplementedError

        return Concatenate(axis=-1)(p)
        
    def compute_output_shape(self, input_shape):
        filed_size = len(input_shape)
        embedding_size = input_shape[0][-1]

        return (None, 1, filed_size * (filed_size - 1) // 2 * embedding_size)

    def get_config(self, ):
        config = {'bilinear_type': self.bilinear_type, 'seed': self.seed}
        base_config = super(BilinearInteraction, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))

4.2 Bi-FFM模型设计

(1)数据情况

本文使用的数据集是《movielens-1M数据》,数据下载地址:http://files.grouplens.org/datasets/movielens/ml-1m.zip

将数据集加工成如下格式:

2786    2       3       5       2041    1023    1
2786    2       3       5       2041    2697    0
2786    2       3       5       2041    794     0

数据说明:

第 1 列
user_id用户id
第 2 列gender用户性别
第 3 列age用户年龄
第 4 列occupation用户工作
第 5 列zip用户邮编
第 6 列movie_id用户下一步是否观看的电影
第 7 列label1表示正样本,0表示负样本

数据加工逻辑见:https://github.com/wziji/deep_ctr/blob/master/BilinearFFM/data.py

(2)Bi-FFM模型代码

#-*- coding:utf-8 -*-
import tensorflow as tf
from tensorflow.keras.layers import Input, Embedding, concatenate, Dense, Dropout

from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
from BilinearInteraction import BilinearInteraction

def BilinearFFM(
    sparse_input_length=1,
    embedding_dim = 64):

    # 1. Input layer
    user_id_input_layer = Input(shape=(sparse_input_length, ), name="user_id_input_layer")
    gender_input_layer = Input(shape=(sparse_input_length, ), name="gender_input_layer")
    age_input_layer = Input(shape=(sparse_input_length, ), name="age_input_layer")
    occupation_input_layer = Input(shape=(sparse_input_length, ), name="occupation_input_layer")
    zip_input_layer = Input(shape=(sparse_input_length, ), name="zip_input_layer")
    item_input_layer = Input(shape=(sparse_input_length, ), name="item_input_layer")

    
    # 2. Embedding layer
    user_id_embedding_layer = Embedding(6040+1, embedding_dim, mask_zero=True, name='user_id_embedding_layer')(user_id_input_layer)
    gender_embedding_layer = Embedding(2+1, embedding_dim, mask_zero=True, name='gender_embedding_layer')(gender_input_layer)
    age_embedding_layer = Embedding(7+1, embedding_dim, mask_zero=True, name='age_embedding_layer')(age_input_layer)
    occupation_embedding_layer = Embedding(21+1, embedding_dim, mask_zero=True, name='occupation_embedding_layer')(occupation_input_layer)
    zip_embedding_layer = Embedding(3439+1, embedding_dim, mask_zero=True, name='zip_embedding_layer')(zip_input_layer)
    item_id_embedding_layer = Embedding(3706+1, embedding_dim, mask_zero=True, name='item_id_embedding_layer')(item_input_layer)
  
    sparse_embedding_list = [user_id_embedding_layer, gender_embedding_layer, age_embedding_layer, \
          occupation_embedding_layer, zip_embedding_layer, item_id_embedding_layer]
    
    # 3. Bilinear FFM
    bilinear_out = BilinearInteraction()(sparse_embedding_list)
    # Output
    dot_output = tf.nn.sigmoid(tf.reduce_sum(bilinear_out, axis=-1))

    sparse_input_list = [user_id_input_layer, gender_input_layer, age_input_layer, occupation_input_layer, zip_input_layer, item_input_layer]
    model = Model(inputs = sparse_input_list,
         outputs = dot_output)
    
    return model

(3)Bi-FFM模型结构图

(4)训练Bi-FFM模型

#-*- coding:utf-8 -*-
import tensorflow as tf
from tensorflow.keras.optimizers import Adam

from data_generator import file_generator
from BilinearFFM import BilinearFFM
# 1. Load data
train_path = "train.txt"
val_path = "test.txt"
batch_size = 1000

n_train = sum([1 for i in open(train_path)])
n_val = sum([1 for i in open(val_path)])

train_steps = n_train / batch_size
train_steps_ = n_train // batch_size
validation_steps = n_val / batch_size
validation_steps_ = n_val // batch_size

train_generator = file_generator(train_path, batch_size)
val_generator = file_generator(val_path, batch_size)

steps_per_epoch = train_steps_ if train_steps==train_steps_ else train_steps_ + 1
validation_steps = validation_steps_ if validation_steps==validation_steps_ else validation_steps_ + 1

print("n_train: ", n_train)
print("n_val: ", n_val)

print("steps_per_epoch: ", steps_per_epoch)
print("validation_steps: ", validation_steps)

# 2. Train model
early_stopping_cb = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)
callbacks = [early_stopping_cb]

model = BilinearFFM()
print(model.summary())
tf.keras.utils.plot_model(model, to_file='BilinearFFM_model.png', show_shapes=True)

model.compile(loss='binary_crossentropy', \
    optimizer=Adam(lr=1e-3), \
    metrics=['accuracy'])

history = model.fit(train_generator, \
    epochs=2, \
     steps_per_epoch = steps_per_epoch, \
    callbacks = callbacks,
    validation_data = val_generator, \
    validation_steps = validation_steps, \
    shuffle=True)

model.save_weights('BilinearFFM_model.h5')

本文的代码请见:https://github.com/wziji/deep_ctr/blob/master/BilinearFFM

参考:

1. FFM及DeepFFM模型在推荐系统的探索(https://zhuanlan.zhihu.com/p/67795161)

2. https://github.com/shenweichen/DeepCTR

欢迎关注 “python科技园” 及 添加小编 进群交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值