一、前言

相比协同过滤模型仅利用用户与物品的相互行为信息进行推荐,逻辑回归模型能够综合利用用户、物品、上下文等多种不同的特征,生成较为“全面”的推荐结果。逻辑回归模型已经将推荐问题转换成了一个点击率预测的问题,另外,逻辑回归的另一种表现形式“感知机”作为神经网络中最基础的单一神经元,是深度学习的基础性结构。因此,能够进行多特征融合的逻辑回归模型成了独立于协同过滤的推荐模型发展的另一个主要方向。

二、GBDT+LR

GBDT+LR模型

 Facebook提出了一种利用GBDT自动进行特征筛选和组合, 进而生成新的离散特征向量, 再把该特征向量当做LR模型的输入, 来产生最后的预测结果, 这就是著名的GBDT+LR模型了。GBDT+LR 使用最广泛的场景是CTR点击率预估,即预测当给用户推送的广告会不会被用户点击。
模型的总体结构长下面这样:

GBDT回归预测例子 gbdt 逻辑回归_逻辑回归

训练时,GBDT 建树的过程相当于自动进行的特征组合和离散化,然后从根结点到叶子节点的这条路径就可以看成是不同特征进行的特征组合,用叶子节点可以唯一的表示这条路径,并作为一个离散特征传入 LR 进行二次训练

预测时,会先走 GBDT 的每棵树,得到某个叶子节点对应的一个离散特征(即一组特征组合),然后把该特征以 one-hot 形式传入 LR 进行线性加权预测。

 三、代码实现

这是criteo-Display Advertising Challenge比赛的部分数据集, 里面有train.csv和test.csv两个文件:

1、数据集处理

  • Label: 目标变量, 0表示未点击, 1表示点击
  • l1-l13: 13列的数值特征, 大部分是计数特征
  • C1-C26: 26列分类特征, 为了达到匿名的目的, 这些特征的值离散成了32位的数据表示

GBDT回归预测例子 gbdt 逻辑回归_算法_02

 下面进行数据处理,填充缺失值

# 简单的数据预处理
# 去掉id列, 把测试集和训练集合并, 填充缺失值
df_train.drop(['Id'], axis=1, inplace=True)    #去掉ID列
df_test.drop(['Id'], axis=1, inplace=True)

df_test['Label'] = -1   #给测试集加上标签列,并设为-1

data = pd.concat([df_train, df_test])  #拼接训练集和测试集
data.fillna(-1, inplace=True)      #缺失值填充-1

GBDT回归预测例子 gbdt 逻辑回归_逻辑回归_03

 这里前几行还是train文件的,可以看到NaN值都被替换为-1

2、模型搭建

(1)逻辑回归模型

数值特征归一化处理

# 连续特征归一化
    scaler = MinMaxScaler()    #归一化函数
    for col in continuous_fea:
        data[col] = scaler.fit_transform(data[col].values.reshape(-1, 1))#变化后的值填充进去

就是将I1-I13的数全部限制到0-1之间,避免出现太大或者太小的数,下面是对比图

GBDT回归预测例子 gbdt 逻辑回归_GBDT回归预测例子_04

GBDT回归预测例子 gbdt 逻辑回归_GBDT回归预测例子_05

 离散特征one-hot编码

for col in category_fea:
        onehot_feats = pd.get_dummies(data[col], prefix=col)  #进行one—hot处理
        data.drop([col], axis=1, inplace=True)     #删掉原有的离散特征
        data = pd.concat([data, onehot_feats], axis=1)   # 列拼接将one-hot特征加上去

 就是将C1-C26的数全部用one-hot表示,下面是对比图

将每个离散特征都用独一无二的0-1组合表示

GBDT回归预测例子 gbdt 逻辑回归_机器学习_06

GBDT回归预测例子 gbdt 逻辑回归_机器学习_07

def lr_model(data, category_fea, continuous_fea):
    # 连续特征归一化
    scaler = MinMaxScaler()    #归一化函数
    for col in continuous_fea:
        data[col] = scaler.fit_transform(data[col].values.reshape(-1, 1))#变化后的值填充进去
    
    # 离散特征one-hot编码
    for col in category_fea:
        onehot_feats = pd.get_dummies(data[col], prefix=col)  #进行one—hot处理
        data.drop([col], axis=1, inplace=True)     #删掉原有的离散特征
        data = pd.concat([data, onehot_feats], axis=1)   # 列拼接将one-hot特征加上去
      
    # 把训练集和测试集分开
    train = data[data['Label'] != -1]    #取标签不是-1的,  等同于把训练集从整个数据中摘出来
    target = train.pop('Label')           # 单独分离Label列出来   这些标签将作为我们的目标
    test = data[data['Label'] == -1]
    test.drop(['Label'], axis=1, inplace=True)   #将测试集中的标签列删除
    
    # 划分数据集
    x_train, x_val, y_train, y_val = train_test_split(train, target, test_size=0.2, random_state=2020)
    
    # 建立模型
    lr = LogisticRegression()
    lr.fit(x_train, y_train)
    tr_logloss = log_loss(y_train, lr.predict_proba(x_train)[:, 1])   # −(ylog(p)+(1−y)log(1−p)) log_loss
    val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1])
    print('tr_logloss: ', tr_logloss)
    print('val_logloss: ', val_logloss)
    
    # 模型预测
    y_pred = lr.predict_proba(test)[:, 1]  # predict_proba 返回n行k列的矩阵,第i行第j列上的数值是模型预测第i个预测样本为某个标签的概率, 这里的1表示点击的概率
    print('predict: ', y_pred[:10]) # 这里看前10个, 预测为点击的概率

y_pre的输出维度是400,对应test的维度400,也就是说对测试集中的400个物品,这里只看前十个物品被点击的概率 

输出结果:

GBDT回归预测例子 gbdt 逻辑回归_GBDT回归预测例子_08

 (2)GBDT 建模

def gbdt_model(data, category_fea, continuous_fea):
    
    # 离散特征one-hot编码
    for col in category_fea:
        onehot_feats = pd.get_dummies(data[col], prefix=col)
        data.drop([col], axis=1, inplace=True)
        data = pd.concat([data, onehot_feats], axis=1)
    
    # 训练集和测试集分开
    train = data[data['Label'] != -1]
    target = train.pop('Label')
    test = data[data['Label'] == -1]
    test.drop(['Label'], axis=1, inplace=True)
    
    # 划分数据集
    x_train, x_val, y_train, y_val = train_test_split(train, target, test_size=0.2, random_state=2020)
    
    # 建模
    gbm = lgb.LGBMClassifier(boosting_type='gbdt',  # 这里用gbdt
                             objective='binary', 
                             subsample=0.8,
                             min_child_weight=0.5, 
                             colsample_bytree=0.7,
                             num_leaves=100,
                             max_depth=12,
                             learning_rate=0.01,
                             n_estimators=10000
                            )
    gbm.fit(x_train, y_train, 
            eval_set=[(x_train, y_train), (x_val, y_val)], 
            eval_names=['train', 'val'],
            eval_metric='binary_logloss',
            early_stopping_rounds=100,
           )
    
    tr_logloss = log_loss(y_train, gbm.predict_proba(x_train)[:, 1])   # −(ylog(p)+(1−y)log(1−p)) log_loss
    val_logloss = log_loss(y_val, gbm.predict_proba(x_val)[:, 1])
    print('tr_logloss: ', tr_logloss)
    print('val_logloss: ', val_logloss)
    
    # 模型预测
    y_pred = gbm.predict_proba(test)[:, 1]  # predict_proba 返回n行k列的矩阵,第i行第j列上的数值是模型预测第i个预测样本为某个标签的概率, 这里的1表示点击的概率
    print('predict: ', y_pred[:10]) # 这里看前10个, 预测为点击的概率

GBDT回归预测例子 gbdt 逻辑回归_GBDT回归预测例子_09

(3)GBDT+LR

下面就是把上面两个模型进行组合, GBDT负责对各个特征进行交叉和组合, 把原始特征向量转换为新的离散型特征向量,

注:这里的离散特征和原数据的离散特征不是一样的,这里的离散特征是交叉组合完,之后形成的离散特征,如下对比:

原数据集的离散特征:               

GBDT回归预测例子 gbdt 逻辑回归_GBDT回归预测例子_10

             

one_hot编码后的离散特征:          

GBDT回归预测例子 gbdt 逻辑回归_机器学习_11

       

经过GBDT交叉完的特征:

      

GBDT回归预测例子 gbdt 逻辑回归_推荐算法_12

然后在使用逻辑回归模型

def gbdt_lr_model(data, category_feature, continuous_feature): # 0.43616
    # 离散特征one-hot编码
    for col in category_feature:
        onehot_feats = pd.get_dummies(data[col], prefix = col)
        data.drop([col], axis = 1, inplace = True)
        data = pd.concat([data, onehot_feats], axis = 1)

    train = data[data['Label'] != -1]
    target = train.pop('Label')
    test = data[data['Label'] == -1]
    test.drop(['Label'], axis = 1, inplace = True)

    # 划分数据集
    x_train, x_val, y_train, y_val = train_test_split(train, target, test_size = 0.2, random_state = 2020)

    gbm = lgb.LGBMClassifier(objective='binary',
                            subsample= 0.8,
                            min_child_weight= 0.5,
                            colsample_bytree= 0.7,
                            num_leaves=100,
                            max_depth = 12,
                            learning_rate=0.01,
                            n_estimators=1000,
                            )

    gbm.fit(x_train, y_train,
            eval_set = [(x_train, y_train), (x_val, y_val)],
            eval_names = ['train', 'val'],
            eval_metric = 'binary_logloss',
            early_stopping_rounds = 100,
            )
    
    model = gbm.booster_

    gbdt_feats_train = model.predict(train, pred_leaf = True)
    gbdt_feats_test = model.predict(test, pred_leaf = True)
    gbdt_feats_name = ['gbdt_leaf_' + str(i) for i in range(gbdt_feats_train.shape[1])]
    df_train_gbdt_feats = pd.DataFrame(gbdt_feats_train, columns = gbdt_feats_name) 
    df_test_gbdt_feats = pd.DataFrame(gbdt_feats_test, columns = gbdt_feats_name)

    train = pd.concat([train, df_train_gbdt_feats], axis = 1)
    test = pd.concat([test, df_test_gbdt_feats], axis = 1)
    train_len = train.shape[0]
    data = pd.concat([train, test])
    print('GBDT.data: ',data)
    del train
    del test
    gc.collect()

    # # 连续特征归一化
    scaler = MinMaxScaler()
    for col in continuous_feature:
        data[col] = scaler.fit_transform(data[col].values.reshape(-1, 1))

    for col in gbdt_feats_name:
        onehot_feats = pd.get_dummies(data[col], prefix = col)
        data.drop([col], axis = 1, inplace = True)
        data = pd.concat([data, onehot_feats], axis = 1)

    train = data[: train_len]
    test = data[train_len:]
    del data
    gc.collect()

    x_train, x_val, y_train, y_val = train_test_split(train, target, test_size = 0.3, random_state = 2018)

    
    lr = LogisticRegression()
    lr.fit(x_train, y_train)
    tr_logloss = log_loss(y_train, lr.predict_proba(x_train)[:, 1])
    print('tr-logloss: ', tr_logloss)
    val_logloss = log_loss(y_val, lr.predict_proba(x_val)[:, 1])
    print('val-logloss: ', val_logloss)
    y_pred = lr.predict_proba(test)[:, 1]
    print(y_pred[:10])

GBDT回归预测例子 gbdt 逻辑回归_机器学习_13

四、总结

1、逻辑回归的优缺点:

优点:

(1)LR模型形式简单,可解释性好,从特征的权重可以看到不同的特征对最后结果的影响。
适合处理海量id类特征,用id类特征有一个很重要的好处,就是防止信息损失(相对于范化的 CTR 特征)
(2)资源占用小,尤其是内存。
缺点:

(1)表达能力不强, 无法进行特征交叉, 特征筛选等一系列“高级“操作
(2)LR 需要进行人工特征组合,这就需要开发者有非常丰富的领域经验,才能不走弯路。

2、GBDT的优缺点:

优点:

(1)树的生成过程理解成自动进行多维度的特征组合的过程。

(2)对于连续型特征的处理,GBDT 可以拆分出一个临界阈值,比如大于 0.027 走左子树,小于等于 0.027(或者 default 值)走右子树,这样很好的规避了人工离散化的问题。这样就非常轻松的解决了逻辑回归那里自动发现特征并进行有效组合的问题, 这也是GBDT的优势所在

缺点

(1)对于海量的 id 类特征,GBDT 由于树的深度和棵树限制(防止过拟合),不能有效的存储

3、后续的发展

如何自动发现有效的特征、特征组合,弥补人工经验不足,缩短LR特征实验周期,是需解决的问题, 也正是由于这些问题, 使得推荐系统继续朝着复杂化发展, 衍生出了因子分解机(FM), 组合模型等高维复杂模型, FM模型通过隐变量的方式,发现两两特征之间的组合关系,但这种特征组合仅限于两两特征之间, 这个模型后面也会介绍到。 深度学习时代之后, 多层神经网络凭借着其强大的表达能力替代了逻辑回归, 到现在, 基本上各大公司很少能看到逻辑回归的身影了

后续的FM论文也会更新,尽请期待!!!!