Focal Loss 和 LightGBM 多分类应用-python实现

Focal Loss 和 LightGBM 多分类应用-python实现,第1张

Focal Loss 和 LightGBM 多分类应用-python实现

有几种方法可以将 Focal Loss 合并到多类分类器中。这是其中之一。 动机

许多现实世界的分类问题都有不平衡的类分布。当数据严重不平衡时,分类算法将开始做出有利于多数类的预测。有几种方法可以解决类别不平衡问题。

一种方法是分配与类频率成反比的样本权重,以增加较少频率类在损失函数中的贡献。

另一种方法是使用过采样/欠采样技术。为少数类生成人工样本的流行技术是合成少数类过采样技术 (SMOTE) 和自适应合成 (ADASYN),两者都包含在 imblearn Python 库中。

建议Ë ntly,使用焦距损失的目标函数的提出。该技术被Tsung-Yi Lin 等人用于二元分类问题。[1]。

在这篇文章中,我将演示如何将 Focal Loss 合并到 LightGBM 分类器中以进行多类分类。代码可在GitHub上找到。

二元分类

对于二元分类问题(标签 0/1),Focal Loss 函数定义如下:

Eq.1 焦点损失函数

其中_pₜ_是真实标签的函数。对于二元分类,该函数定义为:

Eq.2 类别概率

其中 pₜ 是通过将 sigmoid 函数应用于原始边距_z 获得的:_

Eq.3 用于将原始边距 z 转换为类概率 p 的 Sigmoid 函数

Focal Loss 可以解释为一个二元交叉熵函数乘以一个调制因子 (1- pₜ )^ γ,这减少了易于分类的样本的贡献。加权因子_aₜ_平衡了调制因子。引用作者的话:“当 γ = 2 时,与 CE 相比,分类为 pt = 0.9 的示例的损失将降低 100 倍,而当 pt ≈ 0.968 时,其损失将降低 1000 倍”。减少易于分类的示例的丢失,可以让训练更多地关注难以分类的示例”。

在 Max Halford 的博客 [ 2 ] 中可以找到一篇关于将 Focal Loss 纳入二元 LigthGBM 分类器的优秀文章。

多类分类

有几种方法可以将 Focal Loss 合并到多类分类器中。形式上,调制和加权因子应该应用于分类交叉熵。这种方法需要提供关于原始边距_z_的多类损失的一阶和二阶导数。

另一种方法是使用 One-vs-the-rest (OvR),其中为每个类_C_训练一个二元分类器。来自_C_类的数据被视为正数,所有其他数据都被视为负数。在这篇文章中,我使用了 OvR 方法,重用了哈尔福德开发的二元分类器,没有做任何修改。

下面显示的类 oneVsRestLightGBMWithCustomizedLoss 封装了该方法:

import numpy as np
from joblib import Parallel, delayed
from sklearn.multiclass import _ConstantPredictor
from sklearn.preprocessing import LabelBinarizer
from scipy import special
import lightgbm as lgb

class OneVsRestLightGBMWithCustomizedLoss:

    def __init__(self, loss, n_jobs=3):
        self.loss = loss
        self.n_jobs = n_jobs

    def fit(self, X, y, **fit_params):

        self.label_binarizer_ = LabelBinarizer(sparse_output=True)
        Y = self.label_binarizer_.fit_transform(y)
        Y = Y.tocsc()
        self.classes_ = self.label_binarizer_.classes_
        columns = (col.toarray().ravel() for col in Y.T)
        if 'eval_set' in fit_params:
            # use eval_set for early stopping
            X_val, y_val = fit_params['eval_set'][0]
            Y_val = self.label_binarizer_.transform(y_val)
            Y_val = Y_val.tocsc()
            columns_val = (col.toarray().ravel() for col in Y_val.T)
            self.results_ = Parallel(n_jobs=self.n_jobs)(delayed(self._fit_binary)
                                                         (X, column, X_val, column_val, **fit_params) for
                                                         i, (column, column_val) in
                                                         enumerate(zip(columns, columns_val)))
        else:
            # eval set not available
            self.results_ = Parallel(n_jobs=self.n_jobs)(delayed(self._fit_binary)
                                                         (X, column, None, None, **fit_params) for i, column
                                                         in enumerate(columns))

        return self

    def _fit_binary(self, X, y, X_val, y_val, **fit_params):
        unique_y = np.unique(y)
        init_score_value = self.loss.init_score(y)
        if len(unique_y) == 1:
            estimator = _ConstantPredictor().fit(X, unique_y)
        else:
            fit = lgb.Dataset(X, y, init_score=np.full_like(y, init_score_value, dtype=float))
            if 'eval_set' in fit_params:
                val = lgb.Dataset(X_val, y_val, init_score=np.full_like(y_val, init_score_value, dtype=float),
                                  reference=fit)

                estimator = lgb.train(params=fit_params,
                                      train_set=fit,
                                      valid_sets=(fit, val),
                                      valid_names=('fit', 'val'),
                                      early_stopping_rounds=10,
                                      fobj=self.loss.lgb_obj,
                                      feval=self.loss.lgb_eval,
                                      verbose_eval=10)
            else:
                estimator = lgb.train(params=fit_params,
                                      train_set=fit,
                                      fobj=self.loss.lgb_obj,
                                      feval=self.loss.lgb_eval,
                                      verbose_eval=10)

        return estimator, init_score_value

    def predict(self, X):

        n_samples = X.shape[0]
        maxima = np.empty(n_samples, dtype=float)
        maxima.fill(-np.inf)
        argmaxima = np.zeros(n_samples, dtype=int)

        for i, (e, init_score) in enumerate(self.results_):
            margins = e.predict(X, raw_score=True)
            prob = special.expit(margins + init_score)
            np.maximum(maxima, prob, out=maxima)
            argmaxima[maxima == prob] = i

        return argmaxima

    def predict_proba(self, X):
        y = np.zeros((X.shape[0], len(self.results_)))
        for i, (e, init_score) in enumerate(self.results_):
            margins = e.predict(X, raw_score=True)
            y[:, i] = special.expit(margins + init_score)
        y /= np.sum(y, axis=1)[:, np.newaxis]
        return y

该类重新实现了 sklearn.multiclass 命名空间的 oneVsRestClassifier 类。重新实现原始 oneVsRestClassifier 类的主要原因是能够将附加参数转发给 fit 方法。当用户定义的评估函数没有改进时,这可用于传递评估集 (eval_set) 以停止训练,从而减少计算时间并避免过度拟合。

此外,该类使用通用 LightGBM 训练 API,这是在处理原始边距_z_和自定义损失函数时获得有意义的结果所必需的(有关更多详细信息,请参阅 [ 2 ])。如果没有这些限制,就可以更通用地实现该类,不仅可以接受任何损失函数,还可以接受任何实现 Scikit Learn 模型接口的模型。

该类的其他方法是 Scikit Learn 模型接口的一部分:fit、predict 和 predict_proba。在 predict 和 predict_proba 方法中,基本估计器返回原始边距_z_。请注意,当使用自定义损失函数时,LightGBM 返回原始边距_z_。类概率是使用 sigmoid 函数从边缘计算的,如等式所示。3.

一个例子

让我们首先创建一个包含 3 个类的人工不平衡数据集,其中 1% 的样本属于第一类,1% 属于第二类,98% 属于第三类。像往常一样,数据集被分为训练集和测试集。

X, y = make_classification(n_classes=3,
                           n_samples=2000, 
                           n_features=2,
                           n_informative=2,
                           n_redundant =0,
                           n_clusters_per_class=1,
                           weights=[.01, .01, .98], 
                           flip_y=.01, 
                           random_state=42)

le = preprocessing.LabelEncoder()
y_label = le.fit_transform(y)
X_train, X_test, y_train, y_test = train_test_split(X, y_label, test_size=0.30, random_state=42)

classes =[]
labeles=np.unique(y_label)
for v in labeles:
    classes.append('Class '+ str(v))
print(classes)

然后将模型拟合到列车数据上。为了保持实验简单,没有使用提前停止。得到的混淆矩阵如下所示:

图 1 使用 LGBMClassifier 在测试集上的混淆矩阵

对于第一个实验,在测试集上获得了 0.990 的准确度和 0.676 的召回值。使用 oneVsRestLightGBMWithCustomizedLoss 分类器和 Focal Loss 重复相同的实验。

from oneVsRestLightGBMWithCustomizedLoss import *
from FocalLoss import FocalLoss #get the FocalLoss implementation from Halford's blog

# Instantiate Focal loss
loss = FocalLoss(alpha=0.75, gamma=2.0)

# Not using early stopping
clf = oneVsRestLightGBMWithCustomizedLoss(loss=loss)
clf.fit(X_train, y_train)

# Using early stopping
#fit_params = {'eval_set': [(X_test, y_test)]}
#clf.fit(X_train, y_train, **fit_params)

y_test_pred = clf.predict(X_test)
pred_accuracy_score = accuracy_score(y_test, y_test_pred)
pred_recall_score = recall_score(y_test, y_test_pred, average='macro')
print('prediction accuracy', pred_accuracy_score,' recall ', pred_recall_score)

cnf_matrix = confusion_matrix(y_test, y_test_pred, labels=labeles)
plot_confusion_matrix(cnf_matrix, classes=classes,normalize=True,  title='Confusion matrix')

从上面的代码可以看出,损失函数完全可以在分类器之外配置,可以注入到类构造函数中。可以通过向 fit 方法提供包含 eval_set 的字典来打开提前停止,如上面的注释行所示。对于第二个实验,产生的混淆矩阵如下所示:

图 2 使用 oneVsRestLightGBMWithCustomizedLoss 分类器和 Focal Loss 在测试集上的混淆矩阵

在这种情况下,获得了 0.995 的准确度和 0.838 的召回值,比使用默认对数损失的第一个实验有所改进。这个结果从混淆矩阵中也很明显,其中 0 类的假阳性和 1 类的假阴性显着减少。

结论

在这篇文章中,我展示了一种通过使用 One-vs-the-rest (OvR) 方法将 Focal Loss 纳入多类分类器的方法。

通过使用 Focal Loss,不需要样本权重平衡或人工添加新样本来减少不平衡。在人工生成的多类不平衡数据集上,使用 Focal loss 增加了召回值并消除了少数类中的一些误报和漏报。

该方法的有效性必须通过探索现实世界的数据集来确认,其中噪声和非信息特征预计会影响分类结果。

QQ学习群:1026993837 领资料
Focal Loss 和 LightGBM 多分类就为大家介绍到这里,欢迎学习《Python数据分析与机器学习项目实战》bye!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5657965.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-16

发表评论

登录后才能评论

评论列表(0条)

保存