逻辑回归算法详解

目录

原理推导

逻辑回归求解

项目实战--信用卡欺诈检测

数据分析与预处理

数据读取与分析

样本不均衡解决方案

特征标准化

下采样方案

交叉验证

模型评估方法

正则化惩罚

逻辑回归模型

参数对结果的影响

混淆矩阵

分类阈值对结果的影响

过采样方案

SMOTE数据生成策略

过采样应用效果

项目总结

原理推导

先来回顾一下线性回归算法得到的结果:输入特征数据,输出一个具体的值,可以把输出值当作一 个得分值。此时如果想做一个分类任务,要判断一个输入数据是正例还是负例,就可以比较各自的得分值,如果正例的得分值高,那么就说明这个输入数据属于正例类别。

例如,在图5-9中分别计算当前输入属于猫和狗类别的得分值,通过其大小确定最终的分类结果。但是在分类任务中用数值来表示结果还是不太恰当,如果能把得分值转换成概率值,就变得容易理解。假设正例的概率值是0.02,那么负例就是1--0.02=0.98(见图5-10)。

那么如何得到这个概率值呢?先来介绍下Sigmoid函数,定义如下:

在Sigmoid函数中,自变量z可以取任意实数,其结果值域为[0,1],相当于输入一个任意大小的得分 值,得到的结果都在[0,1]之间,恰好可以把它当作分类结果的概率值。

判断最终分类结果时,可以选择以0.5为阈值来进行正负例类别划分,例如输入数据所对应最终的结果为0.7,因0.7大于0.5,就归为正例(见图5-11)。

下面梳理一下计算流程,首先得到得分值,然后通过Sigmoid函数转换成概率值,公式如下:

这个公式与线性回归方程有点相似,仅仅多了Sigmoid函数这一项。x依旧是特征数据,θ依旧是每个特征所对应的参数。下面对正例和负例情况分别进行分析。

由于是二分类任务,当正例概率为时,负例概率必为。对于标签的选择,当y=1时为正例,y=0时为负例。为什么选择0和1呢?其实只是一个代表,为了好化简。在推导过程中,如果分别考虑正负例情况,计算起来十分麻烦,也可以将它们合并起来:

将两个式子合二为一,用一个通项来表示,目的是为了更方后续的求解推导。

逻辑回归求解

之前在推导线性回归的时候得出了目标函数,然后用梯度下降方法进行优化求解,这里貌似只多一项Sigmoid函数,求解的方式还是一样的。首先得到似然函数:

对上式两边取对数,进行化简,结果如下:

这里有一点区别,之前在最小二乘法中求的是极小值,自然用梯度下降,但是现在要求的目标却是 极大值(极大似然估计),通常在机器学习优化中需要把上升问题转换成下降问题,只需取目标函数的相反数即可:

此时,只需求目标函数的极小值,按照梯度下降的方法,照样去求偏导:

上式下标i表示样本,也就是迭代过程中,选择的样本编号;下标j表示特征编号,也是参数编号,因为参数θ和数据特征是一一对应的关系。观察可以发现,对求偏导,最后得到的结果也是乘以,这表示要对哪个参数进行更新,需要用其对应的特征数据,而与其他特征无关。

得到上面这个偏导数后,就可以对参数进行更新,公式如下:

这样就得到了在逻辑回归中每一个参数该如何进行更新,求解方法依旧是迭代优化的思想。找到最 合适的参数θ,任务也就完成了。最后来总结一下逻辑回归的优点。

1.简单实用,在机器学习中并不是一味地选择复杂的算法,简单高效才是王道。

2.结果比较直观,参数值的意义可以理解,便于分析。

3.简单的模型,泛化能力更强,更通用。

项目实战--信用卡欺诈检测

数据分析与预处理

假设有一份信用卡交易记录,遗憾的是数据经过了脱敏处理,只知道其特征,却不知道每一个字段 代表什么含义,没关系,就当作是一个个数据特征。在数据中有两种类别,分别是正常交易数据和异常 交易数据,字段中有明确的标识符。要做的任务就是建立逻辑回归模型,以对这两类数据进行分类,看起来似乎很容易,但实际应用时会出现各种问题等待解决。

似乎很容易,但实际应用时会出现各种问题等待解决。 熟悉任务目标后,第一个想法可能是直接把数据传到算法模型中,得到输出结果就好了。其实并不是这样,在机器学习建模任务中,要做的事情还是很多的,包括数据预处理、特征提取、模型调参等, 每一步都会对最终的结果产生影响。既然如此,就要处理好每一步,其中会涉及机器学习中很多细节, 这些都是非常重要的,基本上所有实战任务都会涉及这些问题,所以大家也可以把这份解决方案当作一个套路。

数据读取与分析

先把任务所需的工具包导入进来,有了这些武器,处理数据就轻松多了:

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

信用卡交易记录数据是一个.csv文件,里面包含近30万条数据,规模很大,首先使用Pandas工具包读取数据。

data=pd.read_csv("./data/creditcard.csv")
data.head()

如上图所示,原始数据为个人交易记录,该数据集总共有31列,其中数据特征有30列,Time列暂时不考虑,Amount列表示贷款的金额,Class列表示分类结果,若Class为0代表该条交易记录正常,若Class为1代表交易异常。

拿到这样一份原始数据之后,直观感觉就是数据已经是处理好的特征,只需要对其进行建模任务即 可。但是,上述输出结果只展示了前5条交易记录并且发现全部是正常交易数据,在实际生活中似乎正常交易也占绝大多数,异常交易仅占一少部分,那么,在整个数据集中,样本分布是否均衡呢?也就是说,在Class列中,正常数据和异常数据的比例是多少?绘制一份图表更能清晰说明:

count_classes=pd.value_counts(data['Class'],sort=True).sort_index()
count_classes.plot(kind='bar')
plt.xlabel('Class')
plt.ylabel('Frequency')

上述代码首先计算出Class列中各个指标的个数,也就是0和1分别有多少个。为了更直观地显示,数据绘制成条形图,从上图中可以发现,似乎只有0没有1(仔细观察,其实是1的比例太少),说明数据中绝大多数是正常数据,异常数据极少。

这个问题看起来有点严峻,数据极度不平衡会对结果造成什么影响呢?模型会不会一边倒呢?认为 所有数据都是正常的,完全不管那些异常的,因为异常数据微乎其微,这种情况出现的可能性很大。我们的任务目标就是找到异常数据,如果模型不重视异常数据,结果就没有意义了,所以,首先要做的就是改进不平衡数据。

在机器学习任务中,加载数据后,首先应当观察数据是否存在问题,先把问题处理掉,再考虑特征提取与建模任务。

样本不均衡解决方案

那么,如何解决数据标签不平衡问题呢?首先,造成数据标签不平衡的最根本的原因就是它们的个 数相差悬殊,如果能让它们的个数相差不大,或者比例接近,这个问题就解决了。基于此,提出以下两种解决方案。

(1)下采样。既然异常数据比较少,那就让正常样本和异常样本一样少。例如正常样本有30W个, 异常样本只有500个,若从正常样本中随机选出500个,它们的比例就均衡了。虽然下采样的方法看似很简单,但是也存在瑕疵,即使原始数据很丰富,下采样过后,只利用了其中一小部分,这样对结果会不会有影响呢?

(2)过采样。不想放弃任何有价值的数据,只能让异常样本和正常样本一样多,怎么做到呢?异常样本若只有500个,此时可以对数据进行变换,假造出来一些异常数据,数据生成也是现阶段常见的一种套路。虽然数据生成解决了异常样本数量的问题,但是异常数据毕竟是造出来的,会不会存在问题呢?

这两种方案各有优缺点,到底哪种方案效果更好呢?需要进行实验比较。

在开始阶段,应当多提出各种解决和对比方案,尽可能先把全局规划制定完整,如果只是想一步做一步,会做大量重复性操作,降低效率。

特征标准化

既然已经有了解决方案,是不是应当按照制订的计划准备开始建模任务呢?还差好多步,首先要对数据进行预处理,可能大家觉得机器学习的核心就是对数据建模,其实建模只是其中一部分,通常更多的时间和精力都用于数据处理中,例如数据清洗、特征提取等,这些并不是小的细节, 而是十分重要的核心内容,目的都是使得最终的结果更好。

观察上面的数据图可以发现,Amount列的数值变化幅度很大,而V1~V28列的特征数据的数值都比较小,此时Amount列的数值相对来说比较大。这会产生什么影响呢?模型对数值是十分敏感的,它不像人类能够理解每一个指标的物理含义,可能会认为数值大的数据相对更重要(此处仅是假设)。但是在数据中, 并没有强调Amount列更重要,而是应当同等对待它们,因此需要改善一下。

特征标准化就是希望数据经过处理后得到的每一个特征的数值都在较小范围内浮动,公式如下:

其中,Z为标准化后的数据;X为原始数据;为原始数据的均值;std(X)为原始数据的标准差。

如果把上式的过程进行分解,就会更加清晰明了。首先将数据的各个维度减去其各自的均值, 这样数据就是以原点为中心对称。其中数值浮动较大的数据,其标准差也必然更大;数值浮动较小的数据,其标准差也会比较小。再将结果除以各自的标准差,就相当于让大的数据压缩到较小的空间中,让小的数据能够伸张一些,对于下图所示的二维数据,就得到其标准化之后的结果,以原点为中心,各个维度的取值范围基本一致。

接下来,很多数据处理和机器学习建模任务都会用到sklearn工具包,这里先做简单介绍,该工具包提供了几乎所有常用的机器学习算法,仅需一两行代码,即可完成建模工作,计算也比较高效。不仅如此,还提供了非常丰富的数据预处理与特征提取模块,方便大家快速上手处理数据特征。

sklearn工具包提供了在机器学习中最核心的三大模块(Classification、Regression、Clustering)的实现方法供大家调用,还包括数据降维(Dimensionality reduction)、模型选择(Model selection)、数据预处理(Preprocessing)等模块,功能十分丰富。

使用sklearn工具包来完成特征标准化操作,代码如下:

#先导入所需模块
from sklearn.preprocessing import StandardScaler
data['normAmount']=StandardScaler().fit_transform(data['Amount'].values.reshape(-1,1))
data=data.drop(['Time','Amount'],axis=1)
data.head()

上述代码使用StandardScaler方法对数据进行标准化处理,调用时需先导入该模块,然后进行 fit_transform操作,相当于执行标准化公式。reshape(−1,1)的含义是将传入数据转换成一列的形式(需按照函数输入要求做)。最后用drop操作去掉无用特征。上述输出结果中的normAmount列就是标准化处理后的结果,可见数值都在较小范围内浮动。

下采样方案

下采样方案的实现过程比较简单,只需要对正常样本进行采样,得到与异常样本一样多的个数即 可,代码如下:

#不包含标签的就是特征
X=data.loc[:,data.columns!='Class']
#标签
y=data.loc[:,data.columns=='Class']
number_records_fraud=len(data[data.Class==1])
#得到所有异常样本的索引
fraud_indices=np.array(data[data.Class==1].index)
#得到所有正常样本的索引
normal_indices=data[data.Class==0].index
#在正常样本中,随机采样出指定个数的样本,并取其索引
random_normal_indices=np.random.choice(normal_indices,number_records_fraud,replace=False)
random_normal_indices=np.array(random_normal_indices)
#有了正常和异常样本后把它们的索引都拿到手
under_sample_indices=np.concatenate([fraud_indices,random_normal_indices])
#根据索引得到下采样所有样本点
under_sample_data=data.iloc[under_sample_indices,:]
X_undersample=under_sample_data.loc[:,under_sample_data.columns!='Class']
y_undersample=under_sample_data.loc[:,under_sample_data.columns=='Class']
#打印下采样策略后正负样本比例
print('正常样本所占整体比例:',len(under_sample_data[under_sample_data.Class==0])/len(under_sample_data))
print('异常样本所占整体比例:',len(under_sample_data[under_sample_data.Class==1])/len(under_sample_data))
print('下采样策略总体样本数量:',len(under_sample_data))
正常样本所占整体比例: 0.5
异常样本所占整体比例: 0.5
下采样策略总体样本数量: 984

整体流程比较简单,首先计算异常样本的个数并取其索引,接下来在正常样本中随机选择指定个数 样本,最后把所有样本索引拼接在一起即可。上述输出结果显示,执行下采样方案后,一共有984条数据,其中正常样本和异常样本各占50%,此时数据满足平衡标准。

交叉验证

得到输入数据后,接下来划分数据集,在机器学习中,使用训练集完成建模后,还需知道这个模型 的效果,也就是需要一个测试集,以帮助完成模型测试工作。不仅如此,在整个模型训练过程中,也会涉及一些参数调整,所以,还需要验证集,帮助模型进行参数的调整与选择。

首先把数据分成两部分训练集和测试集。训练集用于建立模型,例如以梯度下降来迭代优化,这里需要的数据就是由训练集提供的。测试集是当所有建模工作都完成后使用的,需要强调一点,测试集十分宝贵,在建模的过程中,不能加入任何与测试集有关的信息,否则就相当于透题,评估结果就不会准确。可以自己设定训练集和测试集的大小和比例,8︰2、9︰1都是常见的切分比例。

接下来需要对数据集再进行处理,把训练集划分成很多份。这样做的目的在于,建模尝试过程中,需要调整各种可能影响结果的参数,因此需要知道每一种参数方案的效果,但是这里不能用测试集,因为建模任务还没有全部完成,所以验证集就是在建模过程中评估参数用的,那么单独在训练集中找出来一份做验证集不就可以了吗,为什么要划分出来这么多小份呢?

如果只是单独找出来一份,恰好这一份数据比较简单,那么最终的结果可能会偏高;如果选出来的 这一份里面有一些错误点或者离群点,得到的结果可能就会偏低。无论哪种情况,评估结果都会出现一定偏差。

为了解决这个问题,可以把训练集切分成多份,例如将训练集分成10份。在验证某一 次结果时,需要把整个过程分成10步,第一步用前9份当作训练集,最后一份当作验证集,得到一个结果,以此类推,每次都依次用另外一份当作验证集,其他部分当作训练集。这样经过10步之后,就得到 10个结果,每个结果分别对应其中每一小份,组合在一起恰好包含原始训练集中所有数据,再对最终得到的10个结果进行平均,就得到最终模型评估的结果。这个过程就叫作交叉验证。

交叉验证看起来有些复杂,但是能对模型进行更好的评估,使得结果更准确,从后续的实验中,大 家会发现,用不同验证集评估的时候,结果差异很大,所以这个套路是必须要做的。在sklearn工具包中,已经实现好数据集切分的功能,这里需先将数据集划分成训练集和测试集,切分验证集的工作等到建模的时候再做也来得及,代码如下:

#导入数据集划分模块
from sklearn.model_selection import train_test_split
#对整个数据集进行划分,X为特征数据,y为标签,test_size为测试集比例,random_state为随机种子,目的是使得每次随机的结果都能一样
X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.3,random_state=0)
print('原始训练集包含样本数量:',len(X_train))
print('原始测试集包含样本数量:',len(X_test))
print('原始样本总数:',len(X_train)+len(X_test))

#下采样数据集进行划分
X_train_undersample,X_test_undersample,y_train_undersample,y_test_undersample=train_test_split(X_undersample,y_undersample,test_size=0.3,random_state=0)
print('下采样训练集包含样本数量:',len(X_train_undersample))
print('下采样训练集包含样本数量:',len(X_test_undersample))
print('下采样样本总数:',len(X_train_undersample)+len(X_test_undersample))
选区已删除
#导入数据集划分模块
from sklearn.model_selection import train_test_split
#对整个数据集进行划分,X为特征数据,y为标签,test_size为测试集比例,random_state为随机种子,目的是使得每次随机的结果都能一样
X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.3,random_state=0)
print('原始训练集包含样本数量:',len(X_train))
print('原始测试集包含样本数量:',len(X_test))
print('原始样本总数:',len(X_train)+len(X_test))

#下采样数据集进行划分
X_train_undersample,X_test_undersample,y_train_undersample,y_test_undersample=train_test_split(X_undersample,y_undersample,test_size=0.3,random_state=0)
print('下采样训练集包含样本数量:',len(X_train_undersample))
print('下采样训练集包含样本数量:',len(X_test_undersample))
print('下采样样本总数:',len(X_train_undersample)+len(X_test_undersample))
原始训练集包含样本数量: 199364
原始测试集包含样本数量: 85443
原始样本总数: 284807
下采样训练集包含样本数量: 688
下采样训练集包含样本数量: 296
下采样样本总数: 984

通过输出结果可以发现,在切分数据集时做了两件事:首先对原始数据集进行划分,然后对下采样 数据集进行划分。

模型评估方法

接下来,还需要考虑模型的评估方法,为什么建模之前要考虑整个过程呢?因为建模是一个过程,需要优先考虑如何评估其价值,而不是仅仅提供一堆模型参数值。

准确率是分类问题中最常使用的一个参数,用于说明在整体中做对了多少。下面举一个与这份数据 集相似的例子:医院中有1000个病人,其中10个患癌,990个没有患癌,需要建立一个模型来区分他们。 假设模型认为病人都没有患癌,只有10个人分类有错,因此得到的准确率高达990/1000,也就是0.99,看起来是十分不错的结果。但是建模的目的是找出患有癌症的病人,即使一个都没找到,准确率也很高。 这说明对于不同的问题,需要指定特定的评估标准,因为不同的评估方法会产生非常大的差异。

在这个问题中,癌症患者与非癌症患者人数比例十分不均衡,那么,该如何建模呢?既然已经明确 建模的目标是为了检测到癌症患者(异常样本),应当把关注点放在他们身上,可以考虑模型在异常样本中检测到多少个。对于上述问题来说,一个癌症病人都没检测到,意味着召回率(Recall)为0。这里提到了召回率,先通俗理解一下:就是观察给定目标,针对这个目标统计你取得了多大成绩,而不是针对整体而言。

如果直接给出计算公式,理解起来可能有点吃力,现在先来解释一下在机器学习以及数据科学领域 中常用的名词,理解了这些名词,就很容易理解这些评估方法。

下面还是由一个问题来引入,假如某个班级有男生80人,女生20人,共计100人,目标是找出所有女生。现在某次实验挑选出50个人,其中20人是女生,另外还错误地把30个男生也当作女生挑选出来(这里把女生当作正例,男生当作负例)。

下表列出了TP、TN、FP、FN四个关键词的解释,不需要死记硬背,从词表面的意思可以理解。

(1)TP。首先,第一个词是True,这就表明模型预测结果正确,再看Positive,指预测成正例,组合在一起就是首先模型预测正确,即将正例预测成正例。返回来看题目,选出来的50人中有20个是女生,那么TP值就是20,这20个女生被当作女生选出来。

(2)FP。FP表明模型预测结果错误,并且被当作Positive(也就是正例)。在题目中,就是错把男生当作女生选出来。在这里目标是选女生,选出来的50人中有30个却是男的,因此FP等于30。 (3)FN。同理,首先预测结果错误,并且被当作负例,也就是把女生错当作男生选出来,题中并没有这个现象,所以FN等于0。

(4)TN。预测结果正确,但把负例当作负例,将男生当作男生选出来,题中有100人,选出认为是女生的50人,剩下的就是男生了,所以TN等于50。

上述评估分析中常见的4个指标只需要掌握其含义即可。下面来看看通过这4个指标能得出什么结 论。

准确率(Accuracy):表示在分类问题中,做对的占总体的百分比。

召回率(Recall):表示在正例中有多少能预测到,覆盖面的大小。

精确度(Precision):表示被分为正例中实际为正例的比例。

上面介绍了3种比较常见的评估指标,下面回到信用卡分类问题,由于目的是查看有多少异常样本能被检测出来,所以应当使用召回率进行模型评估。

正则化惩罚

先来解释一 下过拟合的含义。 建模的出发点就是尽可能多地满足样本数据,在下图中(a)中直线看起来有点简单,没有满足大部分数据样本点,这种情况就是欠拟合,究其原因,可能由于模型本身过于简单所导致。再来看图(b),比(a)所示模型稍微复杂些,可以满足大多数样本点,这是一个比较不错的模型。但是通过观察可以发现,还是没有抓住所有样本点,这只是一个大致轮廓,那么如果能把模型做得更复杂,岂不是更好?再来看(c),这是一个非常复杂的回归模型,竟然把所有样本点都抓到 了,给人的第一感觉是模型十分强大,但是也会存在一个问题---模型是在训练集上得到的,测试集与训练集却不完全一样,一旦进行测试,效果可能不尽如人意。

在机器学习中,通常都是先用简单的模型进行尝试,如果达不到要求,再做复杂一点的,而不是先 用最复杂的模型来做,虽然训练集的准确度可以达到99%甚至更高,但是实际应用的效果却很差,这就是过拟合。

我们在机器学习任务中经常会遇到过拟合现象,最常见的情况就是随着模型复杂程度的提升,训练 集效果越来越好,但是测试集效果反而越来越差,如下图所示。

对于同一算法来说,模型的复杂程度由谁来控制呢?当然就是其中要求解的参数(例如梯度下降中 优化的参数),如果在训练集上得到的参数值忽高忽低,就很可能导致过拟合,所以正则化惩罚就是为解决过拟合准备的,即惩罚数值较大的权重参数,让它们对结果的影响小一点。

举一个例子来看看其作用,假设有一条样本数据是x:[1,1,1,1],现在有两个模型:

θ1:[1,0,0,0]

θ2:[0.25,0.25,0.25,0.25]

可以发现,模型参数θ1、θ2与数据x组合之后的结果都为1(也就是对应位置相乘求和的结果)。这是不是意味着两个模型的效果相同呢?再观察发现,两个参数本身有着很大的差异,θ1只有第一个位置有值,相当于只注重数据中第一个特征,其他特征完全不考虑;而θ2会同等对待数据中的所有特征。虽然它们的结果相同,但是,如果让大家来选择,大概都会选择第二个,因为它比较均衡,没有那么绝对。

在实际建模中,也需要进行这样的筛选,选择泛化能力更强的也就是都趋于稳定的权重参数。那么 如何把控参数呢?此时就需要一个惩罚项,以惩罚那些类似θ1模型的参数,惩罚项会与目标函数组合在一起,让模型在迭代过程中就开始重视这个问题,而不是建模完成后再来调整,常见的有L1和L2正则化惩罚项:

L1正则化:

L2正则化:

两种正则化惩罚方法都对权重参数进行了处理,既然加到目标函数中,目的就是不让个别权重太 大,以致对局部产生较大影响,也就是过拟合的结果。在L1正则化中可以对|w|求累加和,但是只直接计算绝对值求累加和的话,例如上述例子中θ1和θ2的结果仍然相同,都等于1,并没有作出区分。这时候L2正则化就登场了,它的惩罚力度更大,对权重参数求平方和,目的就是让大的更大,相对惩罚也更多。 θ1的L2惩罚为1,θ2的L2惩罚只有0.25,表明θ1带来的损失更大,在模型效果一致的前提下,当然选择整体效果更优的θ2组模型。

在惩罚项的前面还有一个系数,它表示正则化惩罚的力度。以一种极端情况举例说明:如果值比较大,意味着要非常严格地对待权重参数,此时正则化惩罚的结果会对整体目标函数产生较大影响。如果值较小,意味着惩罚的力度较小,不会对结果产生太大影响。

最终结果的定论是由测试集决定的,训练集上的效果仅供参考,因为过拟合现象十分常见。

逻辑回归模型

现在到建模的时候了,这里需要把上面考虑的所有内容都结合在一起,再用工具包建立一个基础模型,难点在于怎样得到最优的结果,其中每一环节都会对结果产生不同的影响。

参数对结果的影响

在逻辑回归算法中,涉及的参数比较少,这里仅对正则化惩罚力度进行调参实验,为了对比分析交叉验证的效果,对不同验证集分别进行建模与评估分析,代码如下:

def printing_Kfold_scores(x_train_data,y_train_data):
    fold=KFold(5,shuffle=False)
    #定义不同的正则化惩罚力度
    c_param_range=[0.01,0.1,1,10,100]
    #展示结果用的表格
    results_table=pd.DataFrame(index=range(len(c_param_range),2),columns=['C_parameter','Mean recall socre'])
    results_table['C_parameter']=c_param_range
    #k-fold表示K折的交叉验证,这里会得到两个索引集合:训练集=indice[0],验证集=indices[1]
    j=0
    #循环遍历不同的参数
    for c_param in c_param_range:
        print('-----------------------')
        print('正则化惩罚力度:',c_param)
        print('-----------------------')
        print('')
        recall_accs=[]
        #一步步分解来执行交叉验证
        for iteration,indices in enumerate(fold.split(y_train_data),start=1):
            #指定算法模型,并且给定参数
            Ir=LogisticRegression(C = c_param,penalty='l1',solver='liblinear')
            #训练模型,注意不要给错索引,训练的时候传入的一定是训练集,所以X和y的索引都是()
            Ir.fit(x_train_data.iloc[indices[0],:],y_train_data.iloc[indices[0],:].values.ravel())
            #建立好模型后,预测模型结果,这里用的就是验证集,索引为1
            y_pred_undersample=Ir.predict(x_train_data.iloc[indices[1],:].values)
            #预测结果明确后,就可以进行评估,这里recall_score需要传入预测值和真实值
            recall_acc=recall_score(y_train_data.iloc[indices[1],:].values,y_pred_undersample)
            #一会还要算平均,所以把每一步的结果都先保存下来
            recall_accs.append(recall_acc)
            print('Iteration ',iteration,':召回率= ',recall_acc)
        #当执行完所有的交叉验证后,计算平均结果
        results_table.loc[j,'Mean recall score']=np.mean(recall_accs)
        j+=1
        print('')
        print('平均召回率',np.mean(recall_accs))
        print('')
    #找到最好的参数,哪一个Recall高,自然就是最好的
    best_c=results_table.loc[results_table['Mean recall score'].astype('float32').idxmax()]['C_parameter']

    #打印最好的结果
    print('************************************')
    print('效果最好的模型所选参数=',best_c)
    print('************************************')
    return best_c
best_c=printing_Kfold_scores(X_train_undersample,y_train_undersample)

上述代码中,KFold用于选择交叉验证的折数,这里选择5折,即把训练集平均分成5份。c_param是正则化惩罚的力度,也就是正则化惩罚公式中的。为了观察不同惩罚力度对结果的影响,在建模的时候,嵌套两层for循环,首先选择不同的惩罚力度参数,然后对于每一个参数都进行5折的交叉验证,最后得到其验证集的召回率结果。在sklearn工具包中,所有算法的建模调用方法都是类似的,首先选择需要的算法模型,然后.fit()传入实际数据进行迭代,最后用.predict()进行预测。

上述代码可以生成下图的输出。先来单独看正则化惩罚的力度C为0.01时,通过交叉验证分别得到5 次实验结果,可以发现,即便在相同参数的情况下,交叉验证结果的差异还是很大,其值在0.93~1.0之间浮动,但是千万别小看这几个百分点,建模都是围绕着一步步小的提升逐步优化的,所以交叉验证非常有必要。

在sklearn工具包中,C参数的意义正好是倒过来的,例如C=0.01表示正则化力度比较大,而C=100则 表示力度比较小。看起来有点像陷阱,但既然工具包这样定义了,也只好按照其要求做,所以一定要参考其API文档。

再来对比分析不同参数得到的结果,直接观察交叉验证最后的平均召回率值就可以,不同参数的情 况下,得到的结果各不相同,差异还是存在的,所以在建模的时候调参必不可少,可能大家都觉得应该按照经验值去做,但更多的时候,经验值只能提供一个大致的方向,具体的探索还是通过大量的实验进行分析。

混淆矩阵

预测结果明确之后,还可以更直观地进行展示,这时候混淆矩阵就派上用场了。

混淆矩阵中用到的指标值前面已经解释过,既然已经训练好模型,就可以展示其结果,这里用到 Matplotlib工具包,大家可以把下面的代码当成一个混淆矩阵模板,用的时候,只需传入自己的数据即可:

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import recall_score
def plot_confusion_matrix(cm,classes,title='Confusion matrix',cmap=plt.cm.Blues):
    plt.imshow(cm,interpolation='nearest',cmap=cmap)
    plt.title(title)
    plt.colorbar()
    tick_marks=np.arange(len(classes))
    plt.xticks(tick_marks,classes,rotation=0)
    plt.yticks(tick_marks,classes)

    thresh=cm.max()/2.
    for i,j in itertools.product(range(cm.shape[0]),range(cm.shape[1])):
        plt.text(j,i,cm[i,j],horizontalalignment='center',
                 color='white' if cm[i,j] > thresh else 'black')
    plt.tight_layout()
    plt.ylabel('True label')
    plt.xlabel('Predicted label')
Ir = LogisticRegression(C = best_c, penalty = 'l1',solver='liblinear')
Ir.fit(X_train_undersample,y_train_undersample.values.ravel())
y_pred_undersample = lr.predict(X_test_undersample.values)
#计算所需值
cnf_matrix=confusion_matrix(y_test_undersample,y_pred_undersample)
np.set_printoptions(precision=2)
print('召回率:',cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))
#绘制
class_names=[0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix,classes=class_names,title='Confusion matrix')
plt.show()

召回率: 0.9387755102040817

在这份数据集中,目标任务是二分类,所以只有0和1,主对角线上的值就是预测值和真实值一致的 情况,深色区域代表模型预测正确(真实值和预测值一致),其余位置代表预测错误。数值9代表有9个样本数据本来是异常的,模型却将它预测成为正常,相当于"漏检"。数值12代表有12个样本数据本来是正常的,却把它当成异常的识别出来,相当于"误杀"。

最终得到的召回率值约为0.9387,看起来是一个还不错的指标,但是还有没有问题呢?用下采样的数据集进行建模,并且测试集也是下采样的测试集,在这份测试集中,异常样本和正常样本的比例基本均衡,因为已经对数据集进行过处理。但是实际的数据集并不是这样的,相当于在测试时用理想情况来代替真实情况,这样的检测效果可能会偏高,所以,值得注意的是,在测试的时候,需要使用原始数据的测试集,才能最具代表性,只需要改变传入的测试数据即可,代码如下:

Ir = LogisticRegression(C = best_c, penalty = 'l1',solver='liblinear')
Ir.fit(X_train_undersample,y_train_undersample.values.ravel())
y_pred = lr.predict(X_test.values)
#计算所需值
cnf_matrix=confusion_matrix(y_test,y_pred)
np.set_printoptions(precision=2)
print('召回率:',cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))
#绘制
class_names=[0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix,classes=class_names,title='Confusion matrix')
plt.show()
召回率: 0.9183673469387755

还记得在切分数据集的时候,我们做了两手准备吗?不仅对下采样数据集进行切分,而且对原始数 据集也进行了切分。这时候就派上用场了,得到的召回率值为0.9183,虽然有所下降,但是整体来说还是可以的。

在实际的测试中,不仅需要考虑评估方法,还要注重实际应用情况,再深入混淆矩阵中,看看还有 哪些实际问题。上图中左下角的数值为12,看起来没有问题,说明有12个漏检的,只比之前的9个多3个而已。但是,右上角有一个数字格外显眼------9186,意味着有9186个样本被误杀。好像之前用下采样数据集进行测试的时候没有注意到这一点,因为只有12个样本被误杀。但是,在实际的测试集中却出现了这样的事:整个测试集一共只有100多个异常样本,模型却误杀掉9186个,有点夸张了,根据实际业务需求,后续肯定要对检测出来的异常样本做一些处理,比如冻结账号、电话询问等,如果误杀掉这么多样本,实际业务也会出现问题。

问题已经很严峻,模型现在出现了大问题,该如何改进呢?是对模型调整参数,不断优化算法呢? 还是在数据层面做一些处理呢?一般情况下,建议大家先从数据下手,因为对数据做变换要比优化算法模型更容易,得到的效果也更突出。不要忘了之前提出的两种方案,而且过采样方案还没有尝试,会不会发生一些变化呢?下面就来揭晓答案。

分类阈值对结果的影响

回想一下逻辑回归算法原理,通过Sigmoid函数将得分值转换成概率值,那么,怎么得到具体的分类结果呢?默认情况下,模型都是以0.5为界限来划分类别:

可以说0.5是一个经验值,但是并不是固定不变的,实践时可以根据自己的标准来指定该阈值大小。 如果阈值设置得大一些,相当于要求变得严格,只有非常异常的样本,才能当作异常;如果阈值设置得比较小,相当于宁肯错杀也不肯放过,只要有一点异常就通通抓起来。

在sklearn工具包中既可以用.predict()函数得到分类结果,相当于以0.5为默认阈值,也可以 用.predict_proba()函数得到其概率值,而不进行类别判断,代码如下:

#用之前最好的参数来进行建模
lr = LogisticRegression(C = 0.01, penalty = 'l1',solver='liblinear')
#训练模型,还是用下采样的数据集
lr.fit(X_train_undersample,y_train_undersample.values.ravel())
#得到预测结果的概率值
y_pred_undersample_proba = lr.predict_proba(X_test_undersample.values)
#指定不同的阈值
thresholds = [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]

plt.figure(figsize=(10,10))

j = 1
#用混淆矩阵进行展示
for i in thresholds:、
    #比较预测概率与给定阈值
    y_test_predictions_high_recall = y_pred_undersample_proba[:,1] > i
    
    plt.subplot(3,3,j)
    j += 1
    
    cnf_matrix = confusion_matrix(y_test_undersample,y_test_predictions_high_recall)
    np.set_printoptions(precision=2)

    print("Recall metric in the testing dataset: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))

    class_names = [0,1]
    plot_confusion_matrix(cnf_matrix
                          , classes=class_names
                          , title='Threshold >= %s'%i) 


Recall metric in the testing dataset:  1.0
Recall metric in the testing dataset:  1.0
Recall metric in the testing dataset:  1.0
Recall metric in the testing dataset:  0.9931972789115646
Recall metric in the testing dataset:  0.9387755102040817
Recall metric in the testing dataset:  0.8979591836734694
Recall metric in the testing dataset:  0.8367346938775511
Recall metric in the testing dataset:  0.7414965986394558
Recall metric in the testing dataset:  0.5918367346938775

代码中设置0.1~0.9多个阈值,并且确保每一次建模都使用相同的参数,将得到的概率值与给定阈值进行比较来完成分类任务。

现在观察一下输出结果,当阈值比较小的时候,可以发现召回率指标非常高,第一个子图竟然把所 有样本都当作异常的,但是误杀率也是很高的,实际意义并不大。随着阈值的增加,召回率逐渐下降,也就是漏检的逐步增多,而误杀的慢慢减少,这是正常现象。当阈值趋于中间范围时,看起来各有优缺点,当阈值等于0.5时,召回率偏高,但是误杀的样本个数有点多。当阈值等于0.6时,召回率有所下降, 但是误杀样本数量明显减少。那么,究竟选择哪一个阈值比较合适呢?这就需要从实际业务的角度出发,看一看实际问题中,到底需要模型更符合哪一个标准。

过采样方案

在下采样方案中,虽然得到较高的召回率,但是误杀的样本数量实在太多了,下面就来看看用过采 样方案能否解决这个问题。

SMOTE数据生成策略

如何才能让异常样本与正常样本一样多呢?这里需要对少数样本进行生成,这可不是复制粘贴,一 模一样的样本是没有用的,需要采用一些策略,最常用的就是SMOTE算法,其流程如下。

第①步:对于少数类中每一个样本x,以欧式距离为标准,计算它到少数类样本集中所有样本的距 离,经过排序,得到其近邻样本。

第②步:根据样本不平衡比例设置一个采样倍率N,对于每一个少数样本x,从其近邻开始依次选择N个样本。

第③步:对于每一个选出的近邻样本,分别与原样本按照如下的公式构建新的样本数据。

总结一下:对于每一个异常样本,首先找到离其最近的同类样本,然后在它们之间的距离上,取0~1中的一个随机小数作为比例,再加到原始数据点上,就得到新的异常样本。对于SMOTE算法,可以使用imblearn工具包完成这个操作,首先需要安装该工具包,可以直接在命令行中使用pip install imblearn完成安装操作。再把SMOTE算法加载进来,只需要将特征数据和标签传进去,接下来就得到20W+个异常样本,完成过采样方案。

import pandas as pd
from imblearn.over_sampling import SMOTE
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split

过采样应用效果

过采样方案的效果究竟怎样呢?同样使用逻辑回归算法来看看:

credit_cards=pd.read_csv('./data/creditcard.csv')
columns=credit_cards.columns
features_columns=columns.delete(len(columns)-1)
features=credit_cards[features_columns]
labels=credit_cards['Class']
features_train, features_test, labels_train, labels_test = train_test_split(features, 
                                                                            labels, 
                                                                            test_size=0.2, 
                                                                            random_state=0)
from imblearn.over_sampling import SMOTE
oversampler=SMOTE(random_state=0)
os_features,os_labels=oversampler.fit_resample(features_train,labels_train)
len(os_labels[os_labels==1])
os_features=pd.DataFrame(os_features)
os_labels=pd.DataFrame(os_labels)
best_c=printing_Kfold_scores(os_features,os_labels)

在训练集上的效果还不错,再来看看其测试结果的混淆矩阵:

lr = LogisticRegression(C = best_c, penalty = 'l1',solver='liblinear')
lr.fit(os_features,os_labels.values.ravel())
y_pred = lr.predict(features_test.values)

cnf_matrix = confusion_matrix(labels_test,y_pred)
np.set_printoptions(precision=2)

print("Recall metric in the testing dataset: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))

class_names = [0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix
                      , classes=class_names
                      , title='Confusion matrix')
plt.show()


Recall metric in the testing dataset:  0.9108910891089109

得到的召回率值与之前的下采样方案相比有所下降,毕竟在异常样本中,很多都是假冒的,不能与 真实数据相媲美。值得欣慰的是,这回模型的误杀比例大大下降,原来误杀比例占到所有测试样本的10%,现在只占不到1%,实际应用效果有很大提升。

经过对比可以明显发现,过采样的总体效果优于下采样(还得依据实际应用效果具体分析),因为 可利用的数据信息更多,使得模型更符合实际的任务需求。但是,对于不同的任务与数据源来说,并没有一成不变的答案,任何结果都需要通过实验证明,所以,当大家遇到问题时,最好的解决方案是通过大量实验进行分析。

项目总结

1.在做任务之前,一定要检查数据,看看数据有什么问题。在此项目中,通过对数据进行观察,发现其中有样本不均衡的问题,针对这些问题,再来选择解决方案。

2.针对问题提出两种方法:下采样和过采样。通过两条路线进行对比实验,任何实际问题出现后,通常都是先得到一个基础模型,然后对各种方法进行对比,找到最合适的,所以在任务开始之前,一定要多动脑筋,做多手准备,得到的结果才有可选择的余地。

3.在建模之前,需要对数据进行各种预处理操作,例如数据标准化、缺失值填充等,这些都是必要 的,由于数据本身已经给定特征,此处还没有涉及特征工程这个概念,其实数据预处理工作是整个任务中最重、最苦的一个工作阶段,数据处理得好坏对结果的影响最大。

4.先选好评估方法,再进行建模实验。建模的目的就是为了得到结果,但是不可能一次就得到最好的结果,肯定要尝试很多次,所以一定要有一个合适的评估方法,可以选择通用的,例如召回率、准确率等,也可以根据实际问题自己指定合适的评估指标。

5.选择合适的算法,本例中选择逻辑回归算法,详细分析其中的细节,并不一定非要用逻辑回归完成这个任务,其他算法效果可能会更好。但是有一点希望大家能够理解,就是在机器学习中,并不是越复杂的算法越实用,反而越简单的算法应用越广泛。逻辑回归就是其中一个典型的代表,简单实用,所以任何分类问题都可以把逻辑回归当作一个待比较的基础模型。

6.模型的调参也是很重要的,通过实验发现,不同的参数可能会对结果产生较大的影响,这一步也是必须的。使用工具包时,建议先查阅其API文档,知道每一个参数的意义,再来进行实验。

7.得到的预测结果一定要和实际任务结合在一起,有时候虽然得到的评估指标还不错,但是在实际应用中却出现问题,所以测试环节也是必不可少的。

相关推荐
zhangbin_2372 小时前
【Python机器学习】循环神经网络(RNN)——利用Keras实现循环神经网络
python·rnn·深度学习·神经网络·机器学习·自然语言处理·keras
日暮途远z2 小时前
李沐pytorch 课程 深度学习D2l python3.12安装方法
人工智能·pytorch·深度学习
白色机械键盘4 小时前
模型部署基础
人工智能
强哥带你学BP神经网络4 小时前
基于GA-BP遗传算法优化神经网络的多输入多输出数据预测-Python代码实现
人工智能·python·深度学习·神经网络·机器学习
成都古河云5 小时前
智慧体育场馆:科技引领未来运动体验
大数据·网络·人工智能·科技·物联网·运维开发
ZStack开发者社区5 小时前
云轴科技ZStack 获鲲鹏应用创新大赛2024上海赛区决赛一等奖
大数据·人工智能·科技
y_dd5 小时前
【mechine learning-九-梯度下降】
人工智能·机器学习
qq_550337995 小时前
研1日记12
人工智能·算法·机器学习
AI进修生5 小时前
CancerLLM: 癌症领域的大型语言模型
人工智能·语言模型·自然语言处理