如何建立一种检测漏油的不平衡分类模型
许多不平衡的分类任务需要一个熟练的模型来预测清晰的类别标签,其中两个类别同等重要。
不平衡分类问题的一个例子是检测卫星图像中的漏油或浮油,其中需要一个类别标签,并且两个类别同等重要。检测漏油需要动员昂贵的响应,而错过事件同样代价高昂,会造成环境破坏。
评估预测清晰标签的不平衡分类模型的一种方法是分别计算正类和负类的准确率,即灵敏度和特异性。然后可以使用几何平均值(称为 G 均值)对这两个指标进行平均,该几何平均值对倾斜的类别分布不敏感,并能正确报告模型在两个类别上的技能。
在本教程中,您将了解如何开发一个模型来预测卫星图像中是否存在漏油,并使用 G 均值指标对其进行评估。
完成本教程后,您将了解:
- 如何加载和探索数据集并为数据准备和模型选择产生想法。
- 如何评估一套概率模型并通过适当的数据准备来提高其性能。
- 如何拟合最终模型并用它来预测特定情况的类标签。
教程概述
本教程分为五个部分,分别是:
-
石油泄漏数据集
-
探索数据集
-
模型试验及基线结果
-
评估模型
- 评估概率模型
- 评估平衡逻辑回归
- 使用概率模型评估重采样
-
对新数据进行预测
石油泄漏数据集
在这个项目中,我们将使用一个标准的不平衡机器学习数据集,称为“漏油”数据集、“浮油”数据集或简称为“石油”。
该数据集由Miroslav Kubat等人在 1998 年的论文《机器学习在卫星雷达图像中检测漏油》中引入。该数据集通常归功于该论文的合著者Robert Holte 。
该数据集是从海洋卫星图像开始开发的,其中一些包含石油泄漏,一些则没有。图像被分成几部分,并使用计算机视觉算法进行处理,以提供特征向量来描述图像部分或补丁的内容。
[系统] 的输入是来自雷达卫星的原始像素图像,使用图像处理技术 […] 图像处理的输出是每个可疑区域的固定长度特征向量。在正常运行期间,这些特征向量被输入到分类器中,以决定哪些图像和图像中的哪些区域可供人工检查。
—机器学习用于检测卫星雷达图像中的漏油,1998 年。
该任务给出一个描述卫星图像块内容的向量,然后预测该块是否包含漏油,例如非法或意外向海洋倾倒石油。
共有 937 个案例。每个案例由 48 个数值计算机视觉衍生特征、一个补丁编号和一个类标签组成。
总共有九张卫星图像被处理成补丁。数据集中的案例按图像排序,数据集的第一列表示图像的补丁编号。这是为了评估每张图像的模型性能而提供的。在这种情况下,我们对图像或补丁编号不感兴趣,可以删除第一列。
正常情况是没有漏油,其类别标签为 0,而漏油则用类别标签 1 表示。没有漏油的情况有 896 个,漏油的情况有 41 个。
漏油领域的第二个关键特征可以称为不平衡训练集:与浮油相似的负面示例比正面示例多得多。与 41 个正面示例相比,我们有 896 个负面示例,因此多数类几乎占数据的 96%。
我们无法访问用于从卫星图像中准备计算机视觉特征的程序,因此我们只能使用收集并提供的提取特征。
接下来我们来仔细看一下数据。
探索数据集
首先,下载数据集并将其保存在当前工作目录中,名称为“ oil-spill.csv ”
查看文件的内容。
该文件的前几行应如下所示:
1,2558,1506.09,456.63,90,6395000,40.88,7.89,29780,0.19,214.7,0.21,0.26,0.49,0.1,0.4,99.59,32.19,1.84,0.16,0.2,87.65,0,0.47,132.78,-0.01,3.78,0.22,3.2,-3.71,-0.18,2.19,0,2.19,310,16110,0,138.68,89,69,2850,1000,763.16,135.46,3.73,0,33243.19,65.74,7.95,1
2,22325,79.11,841.03,180,55812500,51.11,1.21,61900,0.02,901.7,0.02,0.03,0.11,0.01,0.11,6058.23,4061.15,2.3,0.02,0.02,87.65,0,0.58,132.78,-0.01,3.78,0.84,7.09,-2.21,0,0,0,0,704,40140,0,68.65,89,69,5750,11500,9593.48,1648.8,0.6,0,51572.04,65.73,6.26,0
3,115,1449.85,608.43,88,287500,40.42,7.34,3340,0.18,86.1,0.21,0.32,0.5,0.17,0.34,71.2,16.73,1.82,0.19,0.29,87.65,0,0.46,132.78,-0.01,3.78,0.7,4.79,-3.36,-0.23,1.95,0,1.95,29,1530,0.01,38.8,89,69,1400,250,150,45.13,9.33,1,31692.84,65.81,7.84,1
4,1201,1562.53,295.65,66,3002500,42.4,7.97,18030,0.19,166.5,0.21,0.26,0.48,0.1,0.38,120.22,33.47,1.91,0.16,0.21,87.65,0,0.48,132.78,-0.01,3.78,0.84,6.78,-3.54,-0.33,2.2,0,2.2,183,10080,0,108.27,89,69,6041.52,761.58,453.21,144.97,13.33,1,37696.21,65.67,8.07,1
5,312,950.27,440.86,37,780000,41.43,7.03,3350,0.17,232.8,0.15,0.19,0.35,0.09,0.26,289.19,48.68,1.86,0.13,0.16,87.65,0,0.47,132.78,-0.01,3.78,0.02,2.28,-3.44,-0.44,2.19,0,2.19,45,2340,0,14.39,89,69,1320.04,710.63,512.54,109.16,2.58,0,29038.17,65.66,7.35,0
...
我们可以看到第一列包含表示补丁编号的整数。我们还可以看到计算机视觉派生的特征是实数值,具有不同的尺度,例如第二列中的千位和其他列中的分数。
所有输入变量都是数字,并且没有用“ ? ”字符标记的缺失值。
首先,我们可以加载CSV数据集并确认行数和列数。
可以使用read_csv() Pandas 函数将数据集加载为 DataFrame ,并指定位置和没有标题行的事实。
1 | ... |
---|---|
2 | # define the dataset location |
3 | filename = 'oil-spill.csv' |
4 | # load the csv file as a data frame |
5 | dataframe = read_csv(filename, header=None) |
加载后,我们可以通过打印DataFrame的形状来汇总行数和列数。
...
# summarize the shape of the dataset
print(dataframe.shape)
我们还可以使用Counter对象来汇总每个类中的示例数量。
...
# summarize the class distribution
target = dataframe.values[:,-1]
counter = Counter(target)
for k,v in counter.items():
per = v / len(target) * 100
print('Class=%d, Count=%d, Percentage=%.3f%%' % (k, v, per))
综合以上几点,下面列出了加载和汇总数据集的完整示例。
# load and summarize the dataset
from pandas import read_csv
from collections import Counter
# define the dataset location
filename = 'oil-spill.csv'
# load the csv file as a data frame
dataframe = read_csv(filename, header=None)
# summarize the shape of the dataset
print(dataframe.shape)
# summarize the class distribution
target = dataframe.values[:,-1]
counter = Counter(target)
for k,v in counter.items():
per = v / len(target) * 100
print('Class=%d, Count=%d, Percentage=%.3f%%' % (k, v, per))
运行示例首先加载数据集并确认行数和列数。
然后总结类别分布,确认漏油和非漏油的数量以及少数和多数类别中病例的百分比。
(937, 50)
Class=1, Count=41, Percentage=4.376%
Class=0, Count=896, Percentage=95.624%
我们还可以通过为每个变量创建直方图来查看每个变量的分布。
有 50 个变量,图表数量很多,但我们可能会发现一些有趣的模式。此外,由于图表数量如此之多,我们必须关闭轴标签和图表标题以减少混乱。完整示例如下。
# create histograms of each variable
from pandas import read_csv
from matplotlib import pyplot
# define the dataset location
filename = 'oil-spill.csv'
# load the csv file as a data frame
dataframe = read_csv(filename, header=None)
# create a histogram plot of each variable
ax = dataframe.hist()
# disable axis labels
for axis in ax.flatten():
axis.set_title('')
axis.set_xticklabels([])
axis.set_yticklabels([])
pyplot.show()
运行该示例将为数据集中的 50 个变量中的每一个变量创建一个直方图子图。
我们可以看到许多不同的分布,一些具有高斯分布,另一些具有看似指数或离散的分布。
根据建模算法的选择,我们希望将分布缩放到相同范围是有用的,并且可能使用一些功率变换。
现在我们已经查看了数据集,让我们看看开发一个用于评估候选模型的测试工具。
模型试验及基线结果
我们将使用重复分层 k 倍交叉验证来评估候选模型。
k 折交叉验证程序提供了对模型性能的良好总体估计,至少与单个训练测试分割相比,它不会过于乐观。我们将使用 k=10,这意味着每折将包含约 937/10 或约 94 个示例。
分层意味着每次折叠将包含按类别划分的相同示例混合,即非溢出和溢出的比例约为 96% 到 4%。重复意味着评估过程将执行多次,以帮助避免意外结果并更好地捕捉所选模型的方差。我们将使用三次重复。
这意味着单个模型将被拟合和评估 10 * 3 或 30 次,并且将报告这些运行的平均值和标准差。
这可以使用RepeatedStratifiedKFold scikit-learn 类来实现。
我们正在预测卫星图像块是否包含泄漏的类标签。我们可以使用许多指标,尽管本文的作者选择报告敏感度、特异性和两个分数的几何平均值(称为 G 平均值)。
为此,我们主要使用几何平均值(g-mean)[…]该测量方法具有独立于类别间示例分布的独特属性,因此在这种分布可能随时间而变化或在训练和测试集中有所不同的情况下具有稳健性。
回想一下,敏感度是衡量正类准确度的标准,而特异性是衡量负类准确度的标准。
- 灵敏度 = TruePositives / (TruePositives + FalseNegatives)
- 特异性 = 真阴性 / (真阴性 + 假阳性)
G 均值寻求这些分数之间的平衡,即几何平均值,其中一个或另一个的表现较差就会导致 G 均值分数较低。
- G-均值 = sqrt(敏感度 * 特异性)
我们可以使用irrebalanced-learn 库提供的geometry_mean_score() 函数计算模型所做的一组预测的 G 均值。
首先,我们可以定义一个函数来加载数据集并将列拆分为输入和输出变量。我们还将删除第 22 列,因为该列包含单个值,以及定义图像补丁编号的第一列。下面的load_dataset()函数实现了这一点。
# load the dataset
def load_dataset(full_path):
# load the dataset as a numpy array
data = read_csv(full_path, header=None)
# drop unused columns
data.drop(22, axis=1, inplace=True)
data.drop(0, axis=1, inplace=True)
# retrieve numpy array
data = data.values
# split into input and output elements
X, y = data[:, :-1], data[:, -1]
# label encode the target variable to have the classes 0 and 1
y = LabelEncoder().fit_transform(y)
return X, y
然后我们可以定义一个函数,该函数将评估数据集上的给定模型,并返回每次折叠和重复的 G-Mean 分数列表。
下面的valuate_model ()函数实现了这一点,将数据集和模型作为参数并返回分数列表。
# evaluate a model
def evaluate_model(X, y, model):
# define evaluation procedure
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
# define the model evaluation metric
metric = make_scorer(geometric_mean_score)
# evaluate model
scores = cross_val_score(model, X, y, scoring=metric, cv=cv, n_jobs=-1)
return scores
最后,我们可以使用该测试工具在数据集上评估基线模型。
如果模型在所有情况下都预测多数类别标签 (0) 或少数类别标签 (1),则 G 均值为零。因此,一个好的默认策略是以 50% 的概率随机预测一个类别标签或另一个类别标签,并将 G 均值设为 0.5 左右。
这可以通过使用scikit-learn 库中的DummyClassifier类并将“ strategy ”参数设置为“ uniform ”来实现。
...
# define the reference model
model = DummyClassifier(strategy='uniform')
一旦模型被评估,我们就可以直接报告 G 均值分数的平均值和标准差。
...
# evaluate the model
result_m, result_s = evaluate_model(X, y, model)
# summarize performance
print('Mean G-Mean: %.3f (%.3f)' % (result_m, result_s))
综合起来,下面列出了加载数据集、评估基线模型和报告性能的完整示例。
# test harness and baseline model evaluation
from collections import Counter
from numpy import mean
from numpy import std
from pandas import read_csv
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from imblearn.metrics import geometric_mean_score
from sklearn.metrics import make_scorer
from sklearn.dummy import DummyClassifier
# load the dataset
def load_dataset(full_path):
# load the dataset as a numpy array
data = read_csv(full_path, header=None)
# drop unused columns
data.drop(22, axis=1, inplace=True)
data.drop(0, axis=1, inplace=True)
# retrieve numpy array
data = data.values
# split into input and output elements
X, y = data[:, :-1], data[:, -1]
# label encode the target variable to have the classes 0 and 1
y = LabelEncoder().fit_transform(y)
return X, y
# evaluate a model
def evaluate_model(X, y, model):
# define evaluation procedure
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
# define the model evaluation metric
metric = make_scorer(geometric_mean_score)
# evaluate model
scores = cross_val_score(model, X, y, scoring=metric, cv=cv, n_jobs=-1)
return scores
# define the location of the dataset
full_path = 'oil-spill.csv'
# load the dataset
X, y = load_dataset(full_path)
# summarize the loaded dataset
print(X.shape, y.shape, Counter(y))
# define the reference model
model = DummyClassifier(strategy='uniform')
# evaluate the model
scores = evaluate_model(X, y, model)
# summarize performance
print('Mean G-Mean: %.3f (%.3f)' % (mean(scores), std(scores)))
首先运行示例会加载并汇总数据集。
我们可以看到我们已经加载了正确数量的行,并且我们有 47 个计算机视觉派生的输入变量,其中常量值列(索引 22)和补丁编号列(索引 0)已被删除。
重要的是,我们可以看到类标签具有正确的整数映射,其中 0 表示多数类,1 表示少数类,这是不平衡二元分类数据集的惯例。
接下来,报告 G-Mean 分数的平均值。
在这种情况下,我们可以看到基线算法的 G-Mean 约为 0.47,接近理论最大值 0.5。此分数为模型技能提供了下限;任何平均 G-Mean 高于 0.47(或实际上高于 0.5)的模型都具有技能,而得分低于此值的模型则不具备此数据集的技能。
(937, 47) (937,) Counter({0: 896, 1: 41})
Mean G-Mean: 0.478 (0.143)
值得注意的是,尽管模型评估程序不同,但论文中报告的良好 G 均值约为 0.811。这为该数据集上的“良好”性能提供了一个粗略的目标。
现在我们有了测试工具和性能基准,我们可以开始评估该数据集上的某些模型。
评估模型
在本节中,我们将使用上一节开发的测试工具在数据集上评估一套不同的技术。
目标是既展示如何系统地解决问题,也展示一些针对不平衡分类问题设计的技术的能力。
报告的性能良好,但未高度优化(例如超参数未调整)。
你能得到多少分?如果你能使用相同的测试工具获得更好的 G-mean 性能,我很乐意听听你的看法。请在下面的评论中告诉我。
评估概率模型
让我们首先评估数据集上的一些概率模型。
概率模型是在概率框架下适合数据的模型,并且通常在不平衡的分类数据集上表现良好。
我们将使用数据集中的默认超参数评估以下概率模型:
- 逻辑回归(LR)
- 线性判别分析(LDA)
- 高斯朴素贝叶斯(NB)
LR 和 LDA 都对输入变量的规模很敏感,并且如果在预处理步骤中对不同规模的输入变量进行规范化或标准化,通常会预期和/或表现得更好。
在这种情况下,我们将在拟合每个模型之前对数据集进行标准化。这将使用Pipeline和StandardScaler类来实现。使用 Pipeline 可确保 StandardScaler 适合训练数据集并应用于每个 k 倍交叉验证评估中的训练和测试集,从而避免可能导致乐观结果的任何数据泄漏。
我们可以定义一个模型列表来在我们的测试工具上进行评估,如下所示:
...
# define models
models, names, results = list(), list(), list()
# LR
models.append(Pipeline(steps=[('t', StandardScaler()),('m',LogisticRegression(solver='liblinear'))]))
names.append('LR')
# LDA
models.append(Pipeline(steps=[('t', StandardScaler()),('m',LinearDiscriminantAnalysis())]))
names.append('LDA')
# NB
models.append(GaussianNB())
names.append('NB')
一旦定义,我们就可以枚举列表并依次评估每个列表。评估期间可以打印 G 均值分数的平均值和标准差,并且可以存储分数样本。
可以根据平均 G 均值分数直接比较算法。
...
# evaluate each model
for i in range(len(models)):
# evaluate the model and store results
scores = evaluate_model(X, y, models[i])
results.append(scores)
# summarize and store
print('>%s %.3f (%.3f)' % (names[i], mean(scores), std(scores)))
在运行结束时,我们可以使用分数为每个算法创建一个箱线图。
通过并排创建图表,不仅可以比较平均分数的分布,还可以比较 25% 和 75% 之间分布的中间 50%。
...
# plot the results
pyplot.boxplot(results, labels=names, showmeans=True)
pyplot.show()
综合以上几点,下面列出了使用测试工具比较石油泄漏数据集上的三个概率模型的完整示例。
# compare probabilistic model on the oil spill dataset
from numpy import mean
from numpy import std
from pandas import read_csv
from matplotlib import pyplot
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.metrics import make_scorer
from sklearn.linear_model import LogisticRegression
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.naive_bayes import GaussianNB
from imblearn.metrics import geometric_mean_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# load the dataset
def load_dataset(full_path):
# load the dataset as a numpy array
data = read_csv(full_path, header=None)
# drop unused columns
data.drop(22, axis=1, inplace=True)
data.drop(0, axis=1, inplace=True)
# retrieve numpy array
data = data.values
# split into input and output elements
X, y = data[:, :-1], data[:, -1]
# label encode the target variable to have the classes 0 and 1
y = LabelEncoder().fit_transform(y)
return X, y
# evaluate a model
def evaluate_model(X, y, model):
# define evaluation procedure
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
# define the model evaluation metric
metric = make_scorer(geometric_mean_score)
# evaluate model
scores = cross_val_score(model, X, y, scoring=metric, cv=cv, n_jobs=-1)
return scores
# define the location of the dataset
full_path = 'oil-spill.csv'
# load the dataset
X, y = load_dataset(full_path)
# define models
models, names, results = list(), list(), list()
# LR
models.append(Pipeline(steps=[('t', StandardScaler()),('m',LogisticRegression(solver='liblinear'))]))
names.append('LR')
# LDA
models.append(Pipeline(steps=[('t', StandardScaler()),('m',LinearDiscriminantAnalysis())]))
names.append('LDA')
# NB
models.append(GaussianNB())
names.append('NB')
# evaluate each model
for i in range(len(models)):
# evaluate the model and store results
scores = evaluate_model(X, y, models[i])
results.append(scores)
# summarize and store
print('>%s %.3f (%.3f)' % (names[i], mean(scores), std(scores)))
# plot the results
pyplot.boxplot(results, labels=names, showmeans=True)
pyplot.show()
运行该示例将评估数据集上的每个概率模型。
注意:由于算法或评估程序的随机性,或数值精度的差异,您的结果可能会有所不同。考虑运行示例几次并比较平均结果。
您可能会看到 LDA 算法的一些警告,例如“变量共线”。目前可以安全地忽略这些警告,但这表明该算法可以从特征选择中受益,以删除一些变量。
在这种情况下,我们可以看到每个算法都有技巧,平均 G 均值超过 0.5。结果表明 LDA 可能是测试模型中表现最好的。
>LR 0.621 (0.261)
>LDA 0.741 (0.220)
>NB 0.721 (0.197)
使用带有箱线图的图形总结了每种算法的 G 均值得分分布。我们可以看到,LDA 和 NB 的分布都很紧凑且技巧娴熟,而 LR 在运行过程中可能会产生一些结果,而该方法表现不佳,从而拉低了分布。
这突出表明,在选择模型时,不仅要考虑平均性能,还要考虑模型的一致性。
不平衡石油泄漏数据集的概率模型箱线图
我们已经有了一个好的开始,但我们可以做得更好。
评估平衡逻辑回归
逻辑回归算法支持修改,将分类错误的重要性调整为与类别权重成反比。
这使得模型能够更好地学习有利于少数类的类边界,这可能有助于提高整体 G 均值性能。我们可以通过将LogisticRegression的“ class_weight ”参数设置为“ balanced ”来实现这一点。
...
LogisticRegression(solver='liblinear', class_weight='balanced')
如上所述,逻辑回归对输入变量的规模很敏感,在正则化或标准化输入下表现更好;因此,对给定数据集进行测试是个好主意。此外,可以使用功率分布来扩展每个输入变量的分布,并使那些具有高斯分布的变量更符合高斯分布。这可以使像逻辑回归这样对输入变量的分布做出假设的模型受益。
电力横梁将使用支持正负输入的 Yeo-Johnson 方法,但我们也会在转换之前对数据进行规范化。此外,用于转换的PowerTransformer类也会在转换后对每个变量进行标准化。
我们将比较具有平衡类权重的LogisticRegression与具有三种不同数据准备方案的相同算法,具体来说是规范化、标准化和幂变换。
...
# define models
models, names, results = list(), list(), list()
# LR Balanced
models.append(LogisticRegression(solver='liblinear', class_weight='balanced'))
names.append('Balanced')
# LR Balanced + Normalization
models.append(Pipeline(steps=[('t', MinMaxScaler()),('m', LogisticRegression(solver='liblinear', class_weight='balanced'))]))
names.append('Balanced-Norm')
# LR Balanced + Standardization
models.append(Pipeline(steps=[('t', StandardScaler()),('m', LogisticRegression(solver='liblinear', class_weight='balanced'))]))
names.append('Balanced-Std')
# LR Balanced + Power
models.append(Pipeline(steps=[('t1', MinMaxScaler()), ('t2', PowerTransformer()),('m', LogisticRegression(solver='liblinear', class_weight='balanced'))]))
names.append('Balanced-Power')
综合以上内容,下面列出了具有不同数据准备方案的平衡逻辑回归的比较。
# compare balanced logistic regression on the oil spill dataset
from numpy import mean
from numpy import std
from pandas import read_csv
from matplotlib import pyplot
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.metrics import make_scorer
from sklearn.linear_model import LogisticRegression
from imblearn.metrics import geometric_mean_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import PowerTransformer
# load the dataset
def load_dataset(full_path):
# load the dataset as a numpy array
data = read_csv(full_path, header=None)
# drop unused columns
data.drop(22, axis=1, inplace=True)
data.drop(0, axis=1, inplace=True)
# retrieve numpy array
data = data.values
# split into input and output elements
X, y = data[:, :-1], data[:, -1]
# label encode the target variable to have the classes 0 and 1
y = LabelEncoder().fit_transform(y)
return X, y
# evaluate a model
def evaluate_model(X, y, model):
# define evaluation procedure
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
# define the model evaluation metric
metric = make_scorer(geometric_mean_score)
# evaluate model
scores = cross_val_score(model, X, y, scoring=metric, cv=cv, n_jobs=-1)
return scores
# define the location of the dataset
full_path = 'oil-spill.csv'
# load the dataset
X, y = load_dataset(full_path)
# define models
models, names, results = list(), list(), list()
# LR Balanced
models.append(LogisticRegression(solver='liblinear', class_weight='balanced'))
names.append('Balanced')
# LR Balanced + Normalization
models.append(Pipeline(steps=[('t', MinMaxScaler()),('m', LogisticRegression(solver='liblinear', class_weight='balanced'))]))
names.append('Balanced-Norm')
# LR Balanced + Standardization
models.append(Pipeline(steps=[('t', StandardScaler()),('m', LogisticRegression(solver='liblinear', class_weight='balanced'))]))
names.append('Balanced-Std')
# LR Balanced + Power
models.append(Pipeline(steps=[('t1', MinMaxScaler()), ('t2', PowerTransformer()),('m', LogisticRegression(solver='liblinear', class_weight='balanced'))]))
names.append('Balanced-Power')
# evaluate each model
for i in range(len(models)):
# evaluate the model and store results
scores = evaluate_model(X, y, models[i])
results.append(scores)
# summarize and store
print('>%s %.3f (%.3f)' % (names[i], mean(scores), std(scores)))
# plot the results
pyplot.boxplot(results, labels=names, showmeans=True)
pyplot.show()
运行该示例将评估数据集上平衡逻辑回归模型的每个版本。
注意:由于算法或评估程序的随机性,或数值精度的差异,您的结果可能会有所不同。考虑运行示例几次并比较平均结果。
您可能会看到第一个平衡 LR 模型的一些警告,例如“ Liblinear 未能收敛”。这些警告目前可以安全地忽略,但表明该算法可以从特征选择中受益,以删除一些变量。
在这种情况下,我们可以看到逻辑回归的平衡版本比上一节中评估的所有概率模型的表现要好得多。
结果表明,使用平衡 LR 和数据归一化进行预处理可能在该数据集上表现最佳,平均 G 均值得分约为 0.852。这与 1998 年论文中报告的结果一致或更好。
>Balanced 0.846 (0.142)
>Balanced-Norm 0.852 (0.119)
>Balanced-Std 0.843 (0.124)
>Balanced-Power 0.847 (0.130)
为每种算法创建一个带有箱线图的图形,以便比较结果的分布。
我们可以看到,平衡 LR 的分布总体上比上一节中的非平衡版本更紧密。我们还可以看到,标准化版本的中位数结果(橙色线)高于平均值,高于 0.9,这令人印象深刻。平均值不同于中位数表明结果分布不均,会拉低平均值并产生一些不良结果。
现在,我们只需付出很少的努力就获得了出色的成果;让我们看看是否能够更进一步。
使用概率模型评估数据采样
数据采样提供了一种在拟合模型之前更好地准备不平衡训练数据集的方法。
最流行的数据采样可能是SMOTE过采样技术,用于为少数类创建新的合成示例。这可以与编辑最近邻(ENN) 算法配合使用,该算法将从数据集中查找并删除模棱两可的示例,从而使模型更容易学会区分这两个类别。
这种组合称为 SMOTE-ENN,可以使用不平衡学习库中的SMOTEENN类来实现;例如:
...
# define SMOTE-ENN data sampling method
e = SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority')))
当输入数据预先缩放时,SMOTE 和 ENN 的效果会更好。这是因为这两种技术都涉及在内部使用最近邻算法,并且该算法对不同尺度的输入变量很敏感。因此,我们需要首先对数据进行归一化,然后进行采样,然后将其用作(不平衡)逻辑回归模型的输入。
因此,我们可以使用 irreparably-learn 库提供的 Pipeline 类来创建一系列数据转换,包括数据采样方法,并以逻辑回归模型结束。
我们将比较逻辑回归模型与数据采样的四种变体,具体来说:
- SMOTEENN + LR
- 规范化 + SMOTEENN + LR
- 标准化 + SMOTEENN + LR
- 规范化 + 功率 + SMOTEENN + LR
预期 LR 在使用 SMOTEENN 时会表现更好,而 SMOTEENN 在标准化或正则化时会表现更好。最后一种情况做了很多工作,首先对数据集进行正则化,然后应用功率变换,对结果进行标准化(回想一下,PowerTransformer 类默认会标准化输出),应用 SMOTEENN,最后拟合逻辑回归模型。
这些组合可以定义如下:
...
# SMOTEENN
models.append(Pipeline(steps=[('e', SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority'))), ('m', LogisticRegression(solver='liblinear'))]))
names.append('LR')
# SMOTEENN + Norm
models.append(Pipeline(steps=[('t', MinMaxScaler()), ('e', SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority'))), ('m', LogisticRegression(solver='liblinear'))]))
names.append('Norm')
# SMOTEENN + Std
models.append(Pipeline(steps=[('t', StandardScaler()), ('e', SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority'))), ('m', LogisticRegression(solver='liblinear'))]))
names.append('Std')
# SMOTEENN + Power
models.append(Pipeline(steps=[('t1', MinMaxScaler()), ('t2', PowerTransformer()), ('e', SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority'))), ('m', LogisticRegression(solver='liblinear'))]))
names.append('Power')
将这些结合在一起,完整的示例如下所示。
# compare data sampling with logistic regression on the oil spill dataset
from numpy import mean
from numpy import std
from pandas import read_csv
from matplotlib import pyplot
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.metrics import make_scorer
from sklearn.linear_model import LogisticRegression
from imblearn.metrics import geometric_mean_score
from sklearn.preprocessing import PowerTransformer
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import MinMaxScaler
from imblearn.pipeline import Pipeline
from imblearn.combine import SMOTEENN
from imblearn.under_sampling import EditedNearestNeighbours
# load the dataset
def load_dataset(full_path):
# load the dataset as a numpy array
data = read_csv(full_path, header=None)
# drop unused columns
data.drop(22, axis=1, inplace=True)
data.drop(0, axis=1, inplace=True)
# retrieve numpy array
data = data.values
# split into input and output elements
X, y = data[:, :-1], data[:, -1]
# label encode the target variable to have the classes 0 and 1
y = LabelEncoder().fit_transform(y)
return X, y
# evaluate a model
def evaluate_model(X, y, model):
# define evaluation procedure
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
# define the model evaluation metric
metric = make_scorer(geometric_mean_score)
# evaluate model
scores = cross_val_score(model, X, y, scoring=metric, cv=cv, n_jobs=-1)
return scores
# define the location of the dataset
full_path = 'oil-spill.csv'
# load the dataset
X, y = load_dataset(full_path)
# define models
models, names, results = list(), list(), list()
# SMOTEENN
models.append(Pipeline(steps=[('e', SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority'))), ('m', LogisticRegression(solver='liblinear'))]))
names.append('LR')
# SMOTEENN + Norm
models.append(Pipeline(steps=[('t', MinMaxScaler()), ('e', SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority'))), ('m', LogisticRegression(solver='liblinear'))]))
names.append('Norm')
# SMOTEENN + Std
models.append(Pipeline(steps=[('t', StandardScaler()), ('e', SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority'))), ('m', LogisticRegression(solver='liblinear'))]))
names.append('Std')
# SMOTEENN + Power
models.append(Pipeline(steps=[('t1', MinMaxScaler()), ('t2', PowerTransformer()), ('e', SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority'))), ('m', LogisticRegression(solver='liblinear'))]))
names.append('Power')
# evaluate each model
for i in range(len(models)):
# evaluate the model and store results
scores = evaluate_model(X, y, models[i])
# summarize and store
print('>%s %.3f (%.3f)' % (names[i], mean(scores), std(scores)))
results.append(scores)
# plot the results
pyplot.boxplot(results, labels=names, showmeans=True)
pyplot.show()
运行该示例可使用数据集上的逻辑回归模型评估 SMOTEENN 的每个版本。
注意:由于算法或评估程序的随机性,或数值精度的差异,您的结果可能会
有所不同。考虑运行示例几次并比较平均结果。
在这种情况下,我们可以看到,添加 SMOTEENN 提高了默认 LR 算法的性能,实现了平均 G 均值为 0.852,而第一组实验结果中的平均 G 均值为 0.621。这甚至比没有任何数据缩放的平衡 LR(上一节)更好,后者实现了约 0.846 的 G 均值。
结果表明,也许最终的归一化、幂变换和标准化的组合会比默认的 LR(SMOTEENN,G 均值约为 0.873)取得更好的分数,尽管警告信息表明存在一些需要解决的问题。
>LR 0.852 (0.105)
>Norm 0.838 (0.130)
>Std 0.849 (0.113)
>Power 0.873 (0.118)
可以将结果分布与箱线图进行比较。我们可以看到,所有分布都大致具有相同的紧密分布,并且可以使用结果平均值的差异来选择模型。
对新数据进行预测
直接将 SMOTEENN 与逻辑回归结合使用而无需任何数据缩放,可能是可供今后使用的最简单且性能最佳的模型。
该模型在我们的测试工具上的平均 G 均值约为 0.852。
我们将以此作为我们的最终模型,并用它对新数据进行预测。
首先,我们可以将模型定义为pipeline。
...
# define the model
smoteenn = SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority'))
model = LogisticRegression(solver='liblinear')
pipeline = Pipeline(steps=[('e', smoteenn), ('m', model)])
一旦定义,我们就可以将其适合整个训练数据集。
...
# fit the model
pipeline.fit(X, y)
一旦拟合完成,我们就可以通过调用predict()函数来使用它对新数据进行预测。这将返回类标签 0(表示无漏油)或 1(表示漏油)。
完整的示例如下。
# fit a model and make predictions for the on the oil spill dataset
from pandas import read_csv
from sklearn.preprocessing import LabelEncoder
from sklearn.linear_model import LogisticRegression
from imblearn.pipeline import Pipeline
from imblearn.combine import SMOTEENN
from imblearn.under_sampling import EditedNearestNeighbours
# load the dataset
def load_dataset(full_path):
# load the dataset as a numpy array
data = read_csv(full_path, header=None)
# retrieve numpy array
data = data.values
# split into input and output elements
X, y = data[:, 1:-1], data[:, -1]
# label encode the target variable to have the classes 0 and 1
y = LabelEncoder().fit_transform(y)
return X, y
# define the location of the dataset
full_path = 'oil-spill.csv'
# load the dataset
X, y = load_dataset(full_path)
# define the model
smoteenn = SMOTEENN(enn=EditedNearestNeighbours(sampling_strategy='majority'))
model = LogisticRegression(solver='liblinear')
pipeline = Pipeline(steps=[('e', smoteenn), ('m', model)])
# fit the model
pipeline.fit(X, y)
# evaluate on some non-spill cases (known class 0)
print('Non-Spill Cases:')
data = [[329,1627.54,1409.43,51,822500,35,6.1,4610,0.17,178.4,0.2,0.24,0.39,0.12,0.27,138.32,34.81,2.02,0.14,0.19,75.26,0,0.47,351.67,0.18,9.24,0.38,2.57,-2.96,-0.28,1.93,0,1.93,34,1710,0,25.84,78,55,1460.31,710.63,451.78,150.85,3.23,0,4530.75,66.25,7.85],
[3234,1091.56,1357.96,32,8085000,40.08,8.98,25450,0.22,317.7,0.18,0.2,0.49,0.09,0.41,114.69,41.87,2.31,0.15,0.18,75.26,0,0.53,351.67,0.18,9.24,0.24,3.56,-3.09,-0.31,2.17,0,2.17,281,14490,0,80.11,78,55,4287.77,3095.56,1937.42,773.69,2.21,0,4927.51,66.15,7.24],
[2339,1537.68,1633.02,45,5847500,38.13,9.29,22110,0.24,264.5,0.21,0.26,0.79,0.08,0.71,89.49,32.23,2.2,0.17,0.22,75.26,0,0.51,351.67,0.18,9.24,0.27,4.21,-2.84,-0.29,2.16,0,2.16,228,12150,0,83.6,78,55,3959.8,2404.16,1530.38,659.67,2.59,0,4732.04,66.34,7.67]]
for row in data:
# make prediction
yhat = pipeline.predict([row])
# get the label
label = yhat[0]
# summarize
print('>Predicted=%d (expected 0)' % (label))
# evaluate on some spill cases (known class 1)
print('Spill Cases:')
data = [[2971,1020.91,630.8,59,7427500,32.76,10.48,17380,0.32,427.4,0.22,0.29,0.5,0.08,0.42,149.87,50.99,1.89,0.14,0.18,75.26,0,0.44,351.67,0.18,9.24,2.5,10.63,-3.07,-0.28,2.18,0,2.18,164,8730,0,40.67,78,55,5650.88,1749.29,1245.07,348.7,4.54,0,25579.34,65.78,7.41],
[3155,1118.08,469.39,11,7887500,30.41,7.99,15880,0.26,496.7,0.2,0.26,0.69,0.11,0.58,118.11,43.96,1.76,0.15,0.18,75.26,0,0.4,351.67,0.18,9.24,0.78,8.68,-3.19,-0.33,2.19,0,2.19,150,8100,0,31.97,78,55,3471.31,3059.41,2043.9,477.23,1.7,0,28172.07,65.72,7.58],
[115,1449.85,608.43,88,287500,40.42,7.34,3340,0.18,86.1,0.21,0.32,0.5,0.17,0.34,71.2,16.73,1.82,0.19,0.29,87.65,0,0.46,132.78,-0.01,3.78,0.7,4.79,-3.36,-0.23,1.95,0,1.95,29,1530,0.01,38.8,89,69,1400,250,150,45.13,9.33,1,31692.84,65.81,7.84]]
for row in data:
# make prediction
yhat = pipeline.predict([row])
# get the label
label = yhat[0]
# summarize
print('>Predicted=%d (expected 1)' % (label))
首先运行示例,使模型适合整个训练数据集。
然后,使用拟合模型预测我们知道没有漏油的情况的漏油标签,这些情况是从数据集文件中选择的。我们可以看到,所有情况都得到了正确的预测。
然后,将一些实际漏油案例作为模型的输入,并预测标签。正如我们所希望的那样,再次预测出了正确的标签。
Non-Spill Cases:
>Predicted=0 (expected 0)
>Predicted=0 (expected 0)
>Predicted=0 (expected 0)
Spill Cases:
>Predicted=1 (expected 1)
>Predicted=1 (expected 1)
>Predicted=1 (expected 1)
学习更多内容,关注公众号: 多目标优化与学习Lab