DeepRS(002)–FFM模型理论与实践
背景
FFM(Field-aware Factorization Machine)最初的概念来自Yu-Chin Juan(阮毓钦,毕业于中国台湾大学,现在美国Criteo工作)与其比赛队员,是他们借鉴了来自Michael Jahrer的论文中的field概念提出了FM的升级版模型。通过引入field的概念,FFM把相同性质的特征归于同一个field。
FFM模型
以广告分类为例,“Day=26/11/15”、“Day=1/7/14”、“Day=19/2/15”这三个特征都是代表日期的,可以放到同一个field中。同理,商品的末级品类编码生成了550个特征,这550个特征都是说明商品所属的品类,因此它们也可以放到同一个field中。简单来说,同一个categorical特征经过One-Hot编码生成的数值特征都可以放到同一个field,包括用户性别、职业、品类偏好等。在FFM中,每一维特征
x
i
{ x }_{ i }
xi,针对其它特征的每一种field
f
j
{ f }_{ j }
fj,都会学习一个隐向量
v
i
,
f
j
{ v }_{ i,{ f }_{ j } }
vi,fj。因此,隐向量不仅与特征相关,也与field相关。也就是说,“Day=26/11/15”这个特征与“Country”特征和“Ad_type”特征进行关联的时候使用不同的隐向量,这与“Country”和“Ad_type”的内在差异相符,也是FFM中“field-aware”的由来。
假设样本
n
{ n }
n 个特征属于
f
{ f }
f 个field,那么FFM的二次项有
n
f
{ nf }
nf个隐向量。而在FM模型中,每一维特征的隐向量只有一个。FM可以看作FFM的特例,是把所有特征都归属到一个field时的FFM模型。根据FFM的field敏感特性,可以导出其模型方程
其中, f j { f }_{ j } fj 是第 j 个特征所属的field。如果隐向量的长度为 k,那么FFM的二次参数有 n f k { nfk } nfk 个,远多于FM模型的 n k { nk } nk 个。此外,由于隐向量与field相关,FFM二次项并不能够化简,其预测复杂度是 O ( k n 2 ) O(k{ n }^{ 2 }) O(kn2)。
下面以一个例子简单说明FFM的特征组合方式。输入记录如下
这条记录可以编码成5个特征,其中“Genre=Comedy”和“Genre=Drama”属于同一个field,“Price”是数值型,不用One-Hot编码转换。为了方便说明FFM的样本格式,我们将所有的特征和对应的field映射成整数编号
那么,FFM的组合特征有10项,如下图所示:
其中,红色是field编号,蓝色是特征编号,绿色是此样本的特征取值。二次项的系数是通过与特征field相关的隐向量点积得到的,二次项共有
n
(
n
−
1
)
2
{ \frac { n(n-1) }{ 2 } }
2n(n−1)个
FFM求解
模型方程:
损失函数:
其中:
注:逻辑回归其实是有两种表述方式的损失函数的,取决于你将类别定义为0和1还是1和-1;当我们将类别设定为1和-1的时候,逻辑回归的损失函数就是上面的样子。具体可参考 FM算法及FFM算法
代码实现
- 生成数据
def gen_data():
labels = [-1, 1]
y = [np.random.choice(labels, 1)[0] for _ in range(all_data_size)]
x_field = [i // 10 for i in range(input_x_size)]
x = np.random.randint(0, 2, size=(all_data_size, input_x_size))
return x, y, x_field
- 定义权重项
#交叉特征的权重
def createTwoDimensionWeight(input_x_size, field_size, vector_dimension):
weights = tf.truncated_normal([input_x_size, field_size, vector_dimension])
tf_weights = tf.Variable(weights)
return tf_weights
#一维特征的权重
def createOneDimensionWeight(input_x_size):
weights = tf.truncated_normal([input_x_size])
tf_weights = tf.Variable(weights)
return tf_weights
#bias项
def createZeroDimensionWeight():
weights = tf.truncated_normal([1])
tf_weights = tf.Variable(weights)
return tf_weights
- 计算估计值
估计值的计算这里不能项FM一样先将公式化简再来做,对于交叉特征,只能写两重循环,所以对于特别多的特征的情况下,真的计算特别慢
def inference(input_x, input_x_field, zeroWeights, oneDimWeights, thirdWeight):
"""计算回归模型输出的值"""
#一阶特征
secondValue = tf.reduce_sum(tf.multiply(oneDimWeights, input_x, name='secondValue'))
firstTwoValue = tf.add(zeroWeights, secondValue, name="firstTwoValue")
thirdValue = tf.Variable(0.0, dtype=tf.float32)
input_shape = input_x_size
for i in range(input_shape):
featureIndex1 = i
fieldIndex1 = int(input_x_field[i])
for j in range(i+1, input_shape):
featureIndex2 = j
fieldIndex2 = int(input_x_field[j])
vectorLeft = tf.convert_to_tensor([[featureIndex1, fieldIndex2, i] for i in range(vector_dimension)])
weightLeft = tf.gather_nd(thirdWeight, vectorLeft)
weightLeftAfterCut = tf.squeeze(weightLeft)
vectorRight = tf.convert_to_tensor([[featureIndex2, fieldIndex1, i] for i in range(vector_dimension)])
weightRight = tf.gather_nd(thirdWeight, vectorRight)
weightRightAfterCut = tf.squeeze(weightRight)
tempValue = tf.reduce_sum(tf.multiply(weightLeftAfterCut, weightRightAfterCut))
indices2 = [i]
indices3 = [j]
xi = tf.squeeze(tf.gather_nd(input_x, indices2))
xj = tf.squeeze(tf.gather_nd(input_x, indices3))
product = tf.reduce_sum(tf.multiply(xi, xj))
secondItemVal = tf.multiply(tempValue, product)
tf.assign(thirdValue, tf.add(thirdValue, secondItemVal))
return tf.add(firstTwoValue, thirdValue)
- 定义损失函数
# 分别得到 偏置项、一次、二次项权重
zeroWeights = createZeroDimensionWeight()
oneDimWeights = createOneDimensionWeight(input_x_size)
thirdWeight = createTwoDimensionWeight(input_x_size, # 创建二次项的权重变量
field_size,
vector_dimension)
y_ = inference(input_x, trainx_field, zeroWeights, oneDimWeights, thirdWeight)
l2_norm = tf.reduce_sum(
tf.add(
tf.multiply(lambda_w, tf.pow(oneDimWeights, 2)),
tf.reduce_sum(tf.multiply(lambda_v, tf.pow(thirdWeight, 2)), axis=[1, 2])
)
)
loss = tf.log(1 + tf.exp(input_y * y_)) + l2_norm