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 列 | label | 1表示正样本,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科技园” 及 添加小编 进群交流。