您当前的位置:首页 > 计算机 > 编程开发 > 人工智能

LightGBM 实现基于内容的个性化推荐

时间:08-05来源:作者:点击数:

大家好,本文中,我将和大家一起学习如何训练 LightGBM 模型来估计电子商务广告的点击率的推荐系统的例子。将在Criteo数据集上训练一个基于LightGBM的模型。

LightGBM是一个基于树的梯度提升学习算法框架。是基于分布式框架设计的,因而非常高效,具有以下优点:

  • 训练速度快,效率高。
  • 低内存的使用。
  • 伟大的准确性。
  • 支持并行和GPU学习。
  • 能够处理大规模数据。

LightGBM梯度提升树算法,推荐原理是基于内容的过滤,用于在基于内容的问题中进行快速训练和低内存使用。

接下来我们一起看个具体的案例,更加深刻的理解LightGBM是如何被用于推荐系统中的。

导入模块

import sys  
import os  
import numpy as np  
import lightgbm as lgb  
import papermill as pm  
import scrapbook as sb  
import pandas as pd  
import category_encoders as ce  
from tempfile import TemporaryDirectory  
from sklearn.metrics import roc_auc_score, log_loss  
  
import recommenders.models.lightgbm.lightgbm_utils as lgb_utils  
import recommenders.datasets.criteo as criteo  
  
print("System version: {}".format(sys.version))  
print("LightGBM version: {}".format(lgb.__version__))  
System version: 3.6.7 |Anaconda, Inc.| (default, Oct 23 2018, 19:16:44)   
[GCC 7.3.0]  
LightGBM version: 2.2.1  

参数设置

现在设置 LightGBM 的主要相关参数。基本上,该任务是一个二分类 (预测点击或不点击),因此目标函数设置为二分类 logloss,并使用 AUC 指标作为数据集类中不平衡的影响较小的指标。

通常,我们可以调整模型中的

  • 叶子数量(MAX_LEAF)
  • 每个叶子的最小数据数量(MIN_DATA)
  • 树的最大数量(NUM_OF_TREES)
  • 树的学习率(TREE_LEARNING_RATE)
  • 避免过拟合的早停轮数(EARLY_STOPPING_ROUNDS)

以获得更好的模型性能。

可以在中找到一些关于如何调优这些参数的建议。

MAX_LEAF = 64  
MIN_DATA = 20  
NUM_OF_TREES = 100  
TREE_LEARNING_RATE = 0.15  
EARLY_STOPPING_ROUNDS = 20  
METRIC = "auc"  
SIZE = "sample"  
  
params = {  
    'task': 'train',  
    'boosting_type': 'gbdt',  
    'num_class': 1,  
    'objective': "binary",  
    'metric': METRIC,  
    'num_leaves': MAX_LEAF,  
    'min_data': MIN_DATA,  
    'boost_from_average': True,  
    #set it according to your cpu cores.  
    'num_threads': 20,  
    'feature_fraction': 0.8,  
    'learning_rate': TREE_LEARNING_RATE,  
}  

数据准备

这里使用 CSV 作为示例数据输入。我们的示例数据是来自 Criteo数据集。Criteo数据集是一个著名的行业基准数据集,用于开发CTR预测模型,它经常被研究论文采用作为评估数据集。原始数据集对于轻量级演示来说太大了,因此我们从其中抽取一小部分作为演示数据集。

Criteo中有39列特征,其中13列是数值特征(I1-I13),其他26列是类别特征(C1-C26)。

数据传送门:https://www.kaggle.com/c/criteo-display-ad-challenge

nume_cols = ["I" + str(i) for i in range(1, 14)]  
cate_cols = ["C" + str(i) for i in range(1, 27)]  
label_col = "Label"  
  
header = [label_col] + nume_cols + cate_cols  
with TemporaryDirectory() as tmp:  
    all_data = criteo.load_pandas_df(size=SIZE, local_cache_path=tmp, header=header)  
display(all_data.head())                        

首先,从原始的所有数据中切割三个集合

  • train_data (前80%)
  • valid_data (中间10%)
  • test_data (最后10%)

值得注意的是,考虑到Criteo是一种时间序列流数据,这在推荐场景中也很常见,我们按其顺序对数据进行了拆分。

# split data to 3 sets      
length = len(all_data)  
train_data = all_data.loc[:0.8*length-1]  
valid_data = all_data.loc[0.8*length:0.9*length-1]  
test_data = all_data.loc[0.9*length:]  

基本用法

Ordinal Encoding

考虑到 LightGBM 可以自行处理低频特征和缺失值,在基本使用中,我们只使用序号编码器对类字符串的分类特征进行编码。

ord_encoder = ce.ordinal.OrdinalEncoder(cols=cate_cols)  
  
def encode_csv(df, encoder, label_col, typ='fit'):  
    if typ == 'fit':  
        df = encoder.fit_transform(df)  
    else:  
        df = encoder.transform(df)  
    y = df[label_col].values  
    del df[label_col]  
    return df, y  
  
train_x, train_y = encode_csv(train_data, ord_encoder, label_col)  
valid_x, valid_y = encode_csv(valid_data, ord_encoder, label_col, 'transform')  
test_x, test_y = encode_csv(test_data, ord_encoder, label_col, 'transform')  
  
print('Train Data Shape: X: {trn_x_shape}; Y: {trn_y_shape}.\nValid Data Shape: X: {vld_x_shape}; Y: {vld_y_shape}.\nTest Data Shape: X: {tst_x_shape}; Y: {tst_y_shape}.\n'  
      .format(trn_x_shape=train_x.shape,  
              trn_y_shape=train_y.shape,  
              vld_x_shape=valid_x.shape,  
              vld_y_shape=valid_y.shape,  
              tst_x_shape=test_x.shape,  
              tst_y_shape=test_y.shape,))  
train_x.head()  
Train Data Shape: X: (80000, 39); Y: (80000,).  
Valid Data Shape: X: (10000, 39); Y: (10000,).  
Test Data Shape: X: (10000, 39); Y: (10000,).  

构建模型

当超参数和数据都准备就绪时,我们可以创建一个模型:

lgb_train = lgb.Dataset(train_x, train_y.reshape(-1), params=params, categorical_feature=cate_cols)  
lgb_valid = lgb.Dataset(valid_x, valid_y.reshape(-1), reference=lgb_train, categorical_feature=cate_cols)  
lgb_test = lgb.Dataset(test_x, test_y.reshape(-1), reference=lgb_train, categorical_feature=cate_cols)  
lgb_model = lgb.train(params,  
                      lgb_train,  
                      num_boost_round=NUM_OF_TREES,  
                      early_stopping_rounds=EARLY_STOPPING_ROUNDS,  
                      valid_sets=lgb_valid,  
                      categorical_feature=cate_cols)  
[1]	valid_0's auc: 0.728695  
Training until validation scores don't improve for 20 rounds.  
[2]	valid_0's auc: 0.742373  
[3]	valid_0's auc: 0.747298  
[4]	valid_0's auc: 0.747969  
...  
[39]	valid_0's auc: 0.756966  
Early stopping, best iteration is:  
[19]	valid_0's auc: 0.763092  

现在看看模型的性能如何:

test_preds = lgb_model.predict(test_x)  
auc = roc_auc_score(np.asarray(test_y.reshape(-1)), np.asarray(test_preds))  
logloss = log_loss(np.asarray(test_y.reshape(-1)), np.asarray(test_preds), eps=1e-12)  
res_basic = {"auc": auc, "logloss": logloss}  
print(res_basic)  
sb.glue("res_basic", res_basic)  
{'auc': 0.7674356153037237,   
'logloss': 0.466876775528735}  

模型优化

Label-encoding and Binary-encoding

接下来,由于 LightGBM 对密集的数值特征有较好的有效处理能力,我们尝试将原始数据中的所有分类特征转换为数值特征,通过标签编码和二分类编码。同样由于Criteo的序列特性,我们所采用的标签编码是一个一个执行的,也就是说我们是根据每个样本之前的前一个样本的信息(sequence label-encoding和sequence count-encoding)对样本进行有序编码。此外,我们还对低频分类特征进行了筛选,并用数值特征对应列的均值来填补缺失的值。

在 lgb_utils.NumEncoder,主要步骤如下。

  • 首先,我们将低频分类特征转换为"LESS",将缺失的分类特征转换为"UNK"
  • 其次,我们将缺失的数值特征转换为相应列的均值。
  • 第三,类似字符串的分类特征是按顺序编码的Ordinal-encoding。
  • 然后,我们对样本中的分类特征逐个进行目标编码。对于每个样本,将其前一个样本的标签和计数信息添加到数据中,并产生新的特征。在形式上,for ,我们添加 作为当前样本的新标签特征,其中 是当前样本中要编码的类别,因此是前样本的数量,是检查前样本是否包含(是否)的指示函数。同时,我们还增加了的计数频率,即,作为一个新的计数特征。
  • 最后,在Ordinal-encoding结果的基础上,将Binary-encoding结果作为新列添加到数据中。

注意,上述过程中使用的统计数据只在拟合训练集时更新,而在转换测试集时保持静态,因为测试数据的标签应该是未知的。

label_col = 'Label'  
num_encoder = lgb_utils.NumEncoder(cate_cols, nume_cols, label_col)  
train_x, train_y = num_encoder.fit_transform(train_data)  
valid_x, valid_y = num_encoder.transform(valid_data)  
test_x, test_y = num_encoder.transform(test_data)  
del num_encoder  
print('Train Data Shape: X: {trn_x_shape}; Y: {trn_y_shape}.\nValid Data Shape: X: {vld_x_shape}; Y: {vld_y_shape}.\nTest Data Shape: X: {tst_x_shape}; Y: {tst_y_shape}.\n'  
      .format(trn_x_shape=train_x.shape,  
              trn_y_shape=train_y.shape,  
              vld_x_shape=valid_x.shape,  
              vld_y_shape=valid_y.shape,  
              tst_x_shape=test_x.shape,  
              tst_y_shape=test_y.shape,))  
2019-04-29 11:26:21,158 [INFO] Filtering and fillna features  
100%|██████████| 26/26 [00:02<00:00, 12.36it/s]  
100%|██████████| 13/13 [00:00<00:00, 711.59it/s]  
2019-04-29 11:26:23,286 [INFO] Ordinal encoding cate features  
2019-04-29 11:26:24,680 [INFO] Target encoding cate features  
100%|██████████| 26/26 [00:03<00:00,  6.72it/s]  
2019-04-29 11:26:28,554 [INFO] Start manual binary encoding  
100%|██████████| 65/65 [00:04<00:00, 15.87it/s]  
100%|██████████| 26/26 [00:02<00:00,  8.17it/s]  
2019-04-29 11:26:35,518 [INFO] Filtering and fillna features  
100%|██████████| 26/26 [00:00<00:00, 171.81it/s]  
100%|██████████| 13/13 [00:00<00:00, 2174.25it/s]  
2019-04-29 11:26:35,690 [INFO] Ordinal encoding cate features  
2019-04-29 11:26:35,854 [INFO] Target encoding cate features  
100%|██████████| 26/26 [00:00<00:00, 53.42it/s]  
2019-04-29 11:26:36,344 [INFO] Start manual binary encoding  
100%|██████████| 65/65 [00:03<00:00, 20.02it/s]  
100%|██████████| 26/26 [00:01<00:00, 17.67it/s]  
2019-04-29 11:26:41,081 [INFO] Filtering and fillna features  
100%|██████████| 26/26 [00:00<00:00, 158.08it/s]  
100%|██████████| 13/13 [00:00<00:00, 2203.78it/s]  
2019-04-29 11:26:41,267 [INFO] Ordinal encoding cate features  
2019-04-29 11:26:41,429 [INFO] Target encoding cate features  
100%|██████████| 26/26 [00:00<00:00, 53.08it/s]  
2019-04-29 11:26:41,922 [INFO] Start manual binary encoding  
100%|██████████| 65/65 [00:03<00:00, 20.10it/s]  
100%|██████████| 26/26 [00:01<00:00, 18.37it/s]  
  
Train Data Shape: X: (80000, 268); Y: (80000, 1).  
Valid Data Shape: X: (10000, 268); Y: (10000, 1).  
Test Data Shape: X: (10000, 268); Y: (10000, 1).  
训练和评估
lgb_train = lgb.Dataset(train_x, train_y.reshape(-1), params=params)  
lgb_valid = lgb.Dataset(valid_x, valid_y.reshape(-1), reference=lgb_train)  
lgb_model = lgb.train(params,  
                      lgb_train,  
                      num_boost_round=NUM_OF_TREES,  
                      early_stopping_rounds=EARLY_STOPPING_ROUNDS,  
                      valid_sets=lgb_valid)  
[1]	valid_0's auc: 0.731759  
Training until validation scores don't improve for 20 rounds.  
[2]	valid_0's auc: 0.747705  
[3]	valid_0's auc: 0.751667  
...  
[58]	valid_0's auc: 0.770408  
[59]	valid_0's auc: 0.770489  
Early stopping, best iteration is:  
[39]	valid_0's auc: 0.772136  
test_preds = lgb_model.predict(test_x)  
auc = roc_auc_score(np.asarray(test_y.reshape(-1)), np.asarray(test_preds))  
logloss = log_loss(np.asarray(test_y.reshape(-1)), np.asarray(test_preds), eps=1e-12)  
res_optim = {"auc": auc, "logloss": logloss}  
print(res_optim)  
sb.glue("res_optim", res_optim)  
{'auc': 0.7757371640011422,   
'logloss': 0.4606505068849181}  

模型保存和导入

现在我们完成了LightGBM的基本训练和测试,接下来保存并重新加载模型,然后再次评估它。

with TemporaryDirectory() as tmp:  
    save_file = os.path.join(tmp, r'finished.model')  
    lgb_model.save_model(save_file)  
    loaded_model = lgb.Booster(model_file=save_file)  
  
# eval the performance again  
test_preds = loaded_model.predict(test_x)  
  
auc = roc_auc_score(np.asarray(test_y.reshape(-1)), np.asarray(test_preds))  
logloss = log_loss(np.asarray(test_y.reshape(-1)), np.asarray(test_preds), eps=1e-12)  
print({"auc": auc, "logloss": logloss})  
{'auc': 0.7757371640011422,   
'logloss': 0.4606505068849181}  
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐