FM

ryluo 2020-07-18 20:32:32

目的:

  1. 点击率预估

  2. 稀疏数据下的特征组合

优势:

  1. 高度稀疏的数据场景
  2. 线性的计算复杂度

原理:

点击率预估是一个回归问题,普通的线性模型如下,

上述线性模型只是考虑了每个特征的重要性,但是没有考虑特征组合之后形成新的特征的重要性,换句话说显示生活中特征的组合可能更能说明个体的一些新的特性,相比于只考虑单个特征的影响。比较容易想到的特征组合的方法就是通过多项式的方式去遍历所有特征与特征的关系,如下公式所示:

通过公式发现,使用多项式的特征组合相比于普通的线性模型就是多了一个特征多项式。但是上述的模型很难使用梯度下降的方法进行优化,在使用梯度下降优化的时候是需要对参数$w_i,w_{ij}$进行求导,对于一次项求导只要$x_i$不为零,参数就可以进行更新,但是对于二次项,也就是特征组合项,需要$x_i,x_j$同时不为零,参数才能进行更新,这个在实际应用中是很难满足的,应为大部分的实际数据都是非常的稀疏的,所以需要对这个无法参数更新的问题进行改进。

FM的核心思想:给每一个商品定义一个向量表示$x_i=(v_1,v_2,…,v_k)$,向量的维度为$k$,用两个商品向量的内积作为组合特征的权重。

改进后的FM模型:

其中$$可以将求和符号拆开可以表示为:

将$$代入FM模型:

对于二次项(特征组合项):

上述(4)中的$(\sum_{i=1}^n v_{if}x_i)$和$(\sum_{j=1}^n v_{jf}x_j)$其实是等价的,都是表示所有的商品,所以还可以化简为:

将改进前后二次项的参数的导数进行对比:

  1. 改进前:

    • 参数为$w_{ij}$
    • 导数为:$x_ix_j$
    • 特点:只有当$x_i$和$x_j$同时都不等于0的时候,梯度才不为0,参数才可以进行学习与更新
  2. 改进后:

    • 参数为:$(v_1,v_2,…,v_n)$,其中$v_i=(v_{i1},v_{i2},…,v_{ik})$
    • 参数$v_{if}$的导数为:$x_i\sum_{j=1}^n v_{jf}x_j-v_{if}x_i^2$(可以有公式(4)计算出,为了方便将外面的$\frac{1}{2}$提进去)
    • 特点:对参数$v_{if}$进行更新只需要保证$x_i$不为零即可,不需要考虑$x_j$是否为0,非常适合稀疏数据

实践:

数据集介绍

criteo是criteo非常经典的点击率预估数据集,其中连续特征有13个,类别型特征有26个,没有提供特征的具体名称

dense_feats:'I1', 'I2', 'I3', 'I4', 'I5', 'I6', 'I7', 'I8', 'I9', 'I10','I11', 'I12', 'I13'

sparse_feats:  'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9', 'C10', 'C11', 'C12', 'C13', 'C14', 'C15', 'C16', 'C17', 'C18', 'C19', 'C20', 'C21', 'C22', 'C23', 'C24', 'C25', 'C26'

代码实现

import pandas as pd
import numpy as np 

from tensorflow.keras import *
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
from tensorflow.keras.callbacks import *
import tensorflow.keras.backend as K

from sklearn.model_selection import train_test_split
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from tqdm import tqdm

# dense特征取对数  sparse特征进行类别编码
def process_feat(data, dense_feats, sparse_feats):
    df = data.copy()
    # dense
    df_dense = df[dense_feats].fillna(0.0)
    for f in tqdm(dense_feats):
        df_dense[f] = df_dense[f].apply(lambda x: np.log(1 + x) if x > -1 else -1)

    # sparse
    df_sparse = df[sparse_feats].fillna('-1')
    for f in tqdm(sparse_feats):
        lbe = LabelEncoder()
        df_sparse[f] = lbe.fit_transform(df_sparse[f])

    df_new = pd.concat([df_dense, df_sparse], axis=1)
    return df_new

# FM 特征组合层
class crossLayer(layers.Layer):
    def __init__(self,input_dim, output_dim=10, **kwargs):
        super(crossLayer, self).__init__(**kwargs)

        self.input_dim = input_dim
        self.output_dim = output_dim
        # 定义交叉特征的权重
        self.kernel = self.add_weight(name='kernel', 
                                     shape=(self.input_dim, self.output_dim),
                                     initializer='glorot_uniform',
                                     trainable=True)

    def call(self, x): # 对照上述公式中的二次项优化公式一起理解
        a = K.pow(K.dot(x, self.kernel), 2)
        b = K.dot(K.pow(x, 2), K.pow(self.kernel, 2))
        return 0.5 * K.mean(a-b, 1, keepdims=True)

# 定义FM模型
def FM(feature_dim):
    inputs = Input(shape=(feature_dim, ))

    # 一阶特征
    linear = Dense(units=1, 
                   kernel_regularizer=regularizers.l2(0.01), 
                   bias_regularizer=regularizers.l2(0.01))(inputs)

    # 二阶特征
    cross = crossLayer(feature_dim)(inputs)
    add = Add()([linear, cross])  # 将一阶特征与二阶特征相加构建FM模型

    pred = Activation('sigmoid')(add)
    model = Model(inputs=inputs, outputs=pred)

    model.summary()    
    model.compile(loss='binary_crossentropy',
                  optimizer=optimizers.Adam(),
                  metrics=['binary_accuracy'])

    return model    


# 读取数据
print('loading data...')
data = pd.read_csv('./data/criteo_sample.txt')

# dense 特征开头是I,sparse特征开头是C,label是标签
cols = data.columns.values
dense_feats = [f for f in cols if f[0] == 'I']
sparse_feats = [f for f in cols if f[0] == 'C']

# 对dense数据和sparse数据分别处理
print('processing features')
feats = process_feat(data, dense_feats, sparse_feats)

# 划分训练和验证数据
x_trn, x_tst, y_trn, y_tst = train_test_split(feats, data['label'], test_size=0.2, random_state=2020)

# 定义模型
model = FM(feats.shape[1])

# 训练模型
model.fit(x_trn, y_trn, epochs=10, batch_size=128, validation_data=(x_tst, y_tst))

参考资料:

FM算法解析:

https://zhuanlan.zhihu.com/p/37963267

推荐系统遇上深度学习(一)—FM模型理论和实践

https://www.jianshu.com/p/152ae633fb00

实现参考代码

https://github.com/Hourout/CTR-keras/blob/master/CTR/FM.py

tf.keras定义layer

https://colab.research.google.com/github/keras-team/keras-io/blob/master/guides/ipynb/making_new_layers_and_models_via_subclassing.ipynb

tensorlow 和keras的发展

https://blog.csdn.net/fengdu78/article/details/103759917