1 深入FFM原理与实践
来自美团技术团队的,深入FFM原理与实践
FM和FFM模型是最近几年提出的模型,凭借其在数据量比较大并且特征稀疏的情况下,仍然能够得到优秀的性能和效果的特性,屡次在各大公司举办的CTR预估比赛中获得不错的战绩。美团技术团队在搭建DSP的过程中,探索并使用了FM和FFM模型进行CTR和CVR预估,并且取得了不错的效果。
- 经过One-Hot编码之后,大部分样本数据特征是比较稀疏的。
- One-Hot编码的另一个特点就是导致特征空间大。
同时通过观察大量的样本数据可以发现,某些特征经过关联之后,与label之间的相关性就会提高。 FFM主要用来预估站内的CTR和CVR,即一个用户对一个商品的潜在点击率和点击后的转化率。
CTR和CVR预估模型都是在线下训练,然后用于线上预测。两个模型采用的特征大同小异,主要有三类:用户相关的特征、商品相关的特征、以及用户-商品匹配特征。用户相关的特征包括年龄、性别、职业、兴趣、品类偏好、浏览/购买品类等基本信息,以及用户近期点击量、购买量、消费额等统计信息。商品相关的特征包括所属品类、销量、价格、评分、历史CTR/CVR等信息。用户-商品匹配特征主要有浏览/购买品类匹配、浏览/购买商家匹配、兴趣偏好匹配等几个维度。
为了使用FFM方法,所有的特征必须转换成“field_id:feat_id:value”格式,
field_id代表特征所属field的编号,feat_id是特征编号,value是特征的值
数值型的特征比较容易处理,只需分配单独的field编号,如用户评论得分、商品的历史CTR/CVR等。categorical特征需要经过One-Hot编码成数值型,编码产生的所有特征同属于一个field,而特征的值只能是0或1,如用户的性别、年龄段,商品的品类id等。除此之外,还有第三类特征,如用户浏览/购买品类,有多个品类id且用一个数值衡量用户浏览或购买每个品类商品的数量。这类特征按照categorical特征处理,不同的只是特征的值不是0或1,而是代表用户浏览或购买数量的数值。按前述方法得到field_id之后,再对转换后特征顺序编号,得到feat_id,特征的值也可以按照之前的方法获得。
2 案例
代码案例参考的是:wangru8080/gbdt-lr
其中FFM使用的是libffm库来训练,代码仅给出了构造数据输入的方法(FFMFormat),构造好输入格式后,直接使用libFFM训练即可。
这边libffm所需要的训练格式比较特别:
label field_id:feature_id:value field_id:feature_id:value field_id:feature_id:value …
- field_id表示每一个特征域的id号
- feature_id表示所有特征值的id号(可采用连续编码以及hash编码)
- value:当特征域不是连续特征时,value=1,若为连续特征,value=该特征的值
对于pandas DataFrame格式数据来说:
代码语言:javascript复制label category_feature continuous_feature vector_feature
===== ================ ================== ==============
0 x 1.1 1 2
1 y 1.2 3 4 5
0 x 2.2 6 7 8 9
本文仅有category_feature,continuous_feature,vector_feature。
wangru8080/gbdt-lr中,数据转化的代码为:
代码语言:javascript复制def FFMFormat(df, label, path, train_len, category_feature = [], continuous_feature = []):
index = df.shape[0]
train = open(path 'train.ffm', 'w')
test = open(path 'test.ffm', 'w')
feature_index = 0
feat_index = {}
for i in range(index):
feats = []
field_index = 0
for j, feat in enumerate(category_feature):
t = feat '_' str(df[feat][i])
if t not in feat_index.keys():
feat_index[t] = feature_index
feature_index = feature_index 1
feats.append('%s:%s:%s' % (field_index, feat_index[t], 1))
field_index = field_index 1
for j, feat in enumerate(continuous_feature):
feats.append('%s:%s:%s' % (field_index, feature_index, df[feat][i]))
feature_index = feature_index 1
field_index = field_index 1
print('%s %s' % (df[label][i], ' '.join(feats)))
if i < train_len:
train.write('%s %sn' % (df[label][i], ' '.join(feats)))
else:
test.write('%sn' % (' '.join(feats)))
train.close()
test.close()
其中LightGBM 之后的叶子节点数据是离散的数据,
3 Kaggle: Pandas to libffm
网址:https://www.kaggle.com/mpearmain/pandas-to-libffm
代码语言:javascript复制import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
from sklearn.datasets import make_classification
'''
Another CTR comp and so i suspect libffm will play its part, after all it is an atomic bomb for this kind of stuff.
A sci-kit learn inspired script to convert pandas dataframes into libFFM style data.
The script is fairly hacky (hey thats Kaggle) and takes a little while to run a huge dataset.
The key to using this class is setting up the features dtypes correctly for output (ammend transform to suit your needs)
Example below
'''
class FFMFormatPandas:
def __init__(self):
self.field_index_ = None
self.feature_index_ = None
self.y = None
def fit(self, df, y=None):
self.y = y
df_ffm = df[df.columns.difference([self.y])]
if self.field_index_ is None:
self.field_index_ = {col: i for i, col in enumerate(df_ffm)}
if self.feature_index_ is not None:
last_idx = max(list(self.feature_index_.values()))
if self.feature_index_ is None:
self.feature_index_ = dict()
last_idx = 0
for col in df.columns:
vals = df[col].unique()
for val in vals:
if pd.isnull(val):
continue
name = '{}_{}'.format(col, val)
if name not in self.feature_index_:
self.feature_index_[name] = last_idx
last_idx = 1
self.feature_index_[col] = last_idx
last_idx = 1
return self
def fit_transform(self, df, y=None):
self.fit(df, y)
return self.transform(df)
def transform_row_(self, row, t):
ffm = []
if self.y != None:
ffm.append(str(row.loc[row.index == self.y][0]))
if self.y is None:
ffm.append(str(0))
for col, val in row.loc[row.index != self.y].to_dict().items():
col_type = t[col]
name = '{}_{}'.format(col, val)
if col_type.kind == 'O':
ffm.append('{}:{}:1'.format(self.field_index_[col], self.feature_index_[name]))
elif col_type.kind == 'i':
ffm.append('{}:{}:{}'.format(self.field_index_[col], self.feature_index_[col], val))
return ' '.join(ffm)
def transform(self, df):
t = df.dtypes.to_dict()
return pd.Series({idx: self.transform_row_(row, t) for idx, row in df.iterrows()})
########################### Lets build some data and test ############################
###
train, y = make_classification(n_samples=100, n_features=5, n_informative=2, n_redundant=2, n_classes=2, random_state=42)
train=pd.DataFrame(train, columns=['int1','int2','int3','s1','s2'])
train['int1'] = train['int1'].map(int)
train['int2'] = train['int2'].map(int)
train['int3'] = train['int3'].map(int)
train['s1'] = round(np.log(abs(train['s1'] 1 ))).map(str)
train['s2'] = round(np.log(abs(train['s2'] 1 ))).map(str)
train['clicked'] = y
ffm_train = FFMFormatPandas()
ffm_train_data = ffm_train.fit_transform(train, y='clicked')
print('Base data')
print(train[0:10])
print('FFM data')
print(ffm_train_data[0:10])