线性回归和逻辑回归作为机器学习领域的基础模型,简单却也经典。机器学习领域一般有两种任务:分类和回归。分类通常是判断未知样本属于哪一类,例如0和1,好和坏,大和小等等,而回归不仅仅是判断是是什么,还要判断是多少的任务。本文作为机器学习领域基础模型的介绍,除了对线性回归和逻辑回归自身做介绍外,还会对一些处理方法和评价指标等知识做介绍。
一、线性回归
线性回归是常见的机器学习模型,也是很多人学习机器学习的第一个模型,并且因为线性回归的数学原理比较简单,可以帮助初学者把机器学习和数学原理比较清晰的结合理解。
1.1 平面拟合
假如有一个银行贷款的任务,根据贷款人的工资和年龄来决定贷款金额。现在我们有一批银行实际的贷款数据,需要建立一个多元线性回归模型来判断贷款人的实际贷款金额。
由上表我们可以知道,数据有两个特征:工资和年龄。标签是贷款金额,工资和年龄都会影响终端贷款结果,那么他们的各自影响有多大呢?转化为一个数学问题就是:我们有两个自变量X1、X2(年龄、工资),因变量Y(贷款金额),我们需要找到一个最合适的平面来拟合我们的数据。
由此,我们可以列出一个参数方程:
是年龄的参数,是工资的参数,是偏执项(注意上式的是1,为了转化为矩阵问题,方便计算)。现在我们的任务变成了求解这三个参数的问题。三个参数,只需给出三组(X,Y)数据即可求出,但是现在我们拥有远多于此的数据,可能是三千组或者三万组、三十万组,现在该怎么计算呢?可以发现我们的任务变成了:找出一个拟合平面,使得所有样本的自变量通过该平面的映射值与实际值之间的误差(损失)最小。是不是很熟悉这句话,还记得曾经学习过的最小二乘法吗?那么最小二乘法能不能解决我们的问题呢?
1.2 似然函数
前面我们说了,虽然我们可以找出一个拟合平面建立模型,但是这个拟合平面求出的值和实际值之间肯定是有差异的(误差),使用 来表示这个差异值。则对于每一个样本:
同时,我们认为误差 是独立且都服从均值为0,方差为的高斯分布。什么意思呢?
- 独立:任意两个样本之间都是没有关系的,即A的贷款金额和B的贷款金额没有任何关系。
- 同分布:任意两个样本都根据同一套标准得出,即所有贷款数据都来自同一家银行的同一套判断标准。
- 高斯分布:前面两点比较容易理解,之所以说误差满足高斯分布,是因为根据我们的模型预测出的结果,可能都会存在偏差,有的会比实际值大,有的会比实际值小,但是绝大多数情况,这个偏差都是非常小的,预测结果和实际值偏差比较大的情况会比较少,也就是说,我们首先假设我们的模型是有效的。
由于误差服从标准高斯分布,则:
将(2)式代入(3)中:
假设我们的样本个数是m,那么则m个样本同时发生的概率就是:
根据最大似然估计,已经得到样本发生的概率最大,则(5)式就是我们的似然函数,采用对数化简:
为了使得最大似然函数最大,因为(6)式的前半部分为正,则我们的任务就变成了使(6)式的后半部分最小即可,那么我们最终需要求解的目标函数就是:
其中:
这里注意区分,,...,表示我们的样本特征,也就是我们上文说到的m个样本,而中的,,表示我们的实际特征(年龄、工资),在实际任务中,特征数量可能会远多于两个,此处只列出两个是为了结合前文的例子,易于理解。
对(7)式求偏导:
求极小值点,令偏导为0:
1.3 梯度下降
在1.2中我们已经介绍了目标函数和的解,但是在实际中我们几乎不会直接去求解模型参数,除了直接计算复杂和计算成本高之外,很多模型并不像线性回归一样可以直接求解的。
我们的任务是在已知样本X的基础上,寻找一个最优解使得目标函数的值最小,我们在计算过程中往往不会把所有样本数据一次性全部进行计算(实际任务中样本数据一般都比较大,对计算机硬件要求较高),而是分批迭代计算,例如每次拿一万个样本参与计算。对于,我们也不会直接根据样本X直接计算,我们采用不断调整的值,使得目标函数越来越小,也即整个计算过程是一个不断迭代的过程。当然,对于的调整也并不是盲目的,沿着坡度(偏导)的方向调整参数效率是最高的,每次调整的幅度(学习率)也不能太大,因为太大很可能会跳过全局最小值点,导致“震荡现象”。沿着坡度的方向不断调整参数,这也就是我们常说的梯度下降算法。
根据(7)式的目标函数(分母的m表示求平均损失):
梯度下降可以按照三种不同的方法进行:
- 批量(全局)梯度下降,即每次计算都把全部样本参与计算,好处是容易得到最优解,缺点是前文叙述的计算成本高、速度慢。
- 随机梯度下降,每次拿一个样本参与计算,好处是迭代速度快,但是因为每次只考虑了一个样本,会导致模型收敛较慢,并且每次迭代有很多时间消耗在了数据更新上。
- 小批量梯度下降(batch),即每次只拿一部分样本参与计算,兼顾计算成本和收敛速度。例如,每次拿10个样本参与计算(表示学习率):
1.4 学习率
在1.3中我们已经提到了学习率的概念。顾名思义,学习率参数是用来控制学习参数调节幅度的,学习率太大,容易导致“震荡现象”,甚至不收敛,学习率太小又会导致模型训练速度变慢。当然,对于现在的很多服务器算力来说,这个参数并不是很重要,即使设置的很小,也能在可接受的时间范围内收敛。学习率在深度学习模型训练中更加重要,因为刚开始的模型损失一般都比较大,学习率可以大一点,等到损失降下来以后,学习率可以设置的小一点,因此,在深度学习中也经常使用学习率衰减的方法来训练模型。
二、逻辑回归
如果说线性回归只是为了让我们更好的理解机器学习中的数学知识,在实际中应用较少的话,那么逻辑回归在实际应用中就很普遍了,同时逻辑回归也是一个非常经典的分类模型。机器学习虽然有很多算法,但是在实际业务中,我们往往也会首先使用逻辑回归测试效果,作为一个baseline,在效果差不多的情况下,也会优先选择逻辑回归。
2.1 sigmod函数
逻辑回归的核心是sigmod函数,sigmod函数定义如下:
sigmod函数的输入可以为任意实数值,值域(0,1),且是在(0,0.5)坐标点中心对称的。sigmod函数的(0,1)值域可以很好的转为概率定义,因此sigmod函数包括其升级版softmax在很多模型中的最后一步用的非常广泛,在(0,0.5)坐标点的中心对称特性,可以很好的作为二分类模型。
sigmod函数的另一个优点就是求导非常方便,,这在进行梯度下降的时候非常有用。
2.2 梯度下降
sigmod函数的(0,1)值域可以很好的转为概率定义,把上文介绍的线性回归模型输入sigmod函数。
对于二分类任务:
m个样本的似然函数和对说似然分别为:
利用 将最大值问题转换为最小值问题,并对 求偏导:
至此,我们已经了解了线性回归和逻辑回归的数学原理和梯度下降参数更新过程。
三、实践
前面我们已经学习了线性回归和逻辑回归的原理,现在我们将使用逻辑回归模型来完成一个信用卡分类模型。
3.1 数据分析
现在有一批信用卡数据(已经是特征处理后的脱敏数据)如下,共有29个特征,Class表示类别,现在需要构建一个分类模型。
在开始建模之前,我们需要对原始数据做一些基本分析,主要包括数据量分析、缺失值统计处理、样本分布、特征分布等。
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import KFold, cross_val_score
from sklearn.metrics import confusion_matrix,recall_score,classification_report
%matplotlib inline
data = pd.read_csv("creditcard.csv")
print(data.head())
print(data.shape)
data['Class'].value_counts()
3.2 归一化和下采样
分析可以发现,本例中的原始数据没有缺失数据,共有28万条记录,但是正负样本数据比例严重不均衡,另外,Amount特征的数值相对于其他特征数值偏大。因此,我们在建模之前需要先处理这两个问题,数据不均衡一直是机器学习中一个比较热点的问题,后面有时间会单独开一篇文章讲这个问题,比较常见的处理方式就是下采样和上采样。在计算机视觉领域,针对图像的处理,往往可以通过图像增强的方法扩充数据集,但在机器学习实际工作中,下采样的使用率要高得多,这主要是因为针对业务问题的数据集增强往往很难。其次,对于Amount数值较大的问题,可以采用归一化处理,归一化也有很多方法,常见的有min-max、正则化等,可以参考这篇文章内容。本文选择正则化的方法对Amount进行处理。
##正则化处理
data['normAmount'] = StandardScaler().fit_transform(data['Amount'].values.reshape(-1, 1)).reshape(-1)
data = data.drop(['Time','Amount'],axis=1)
print(data.head())
##数据下采样
fraud_indices = np.array(data[data.Class == 1].index)
normal_indices = data[data.Class == 0].index
number_records_fraud = len(fraud_indices)
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,:]
print(under_sample_data.shape) ##(984, 31)
X_undersample = under_sample_data.iloc[:, under_sample_data.columns != 'Class']
y_undersample = under_sample_data.iloc[:, under_sample_data.columns == 'Class']
划分训练集、验证集、测试集。
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("")
print("Number transactions train dataset: ", len(X_train_undersample))
print("Number transactions test dataset: ", len(X_test_undersample))
print("Total number of transactions: ", len(X_train_undersample)+len(X_test_undersample))
#Number transactions train dataset: 688
#Number transactions test dataset: 296
#Total number of transactions: 984
3.3 评价指标
在机器学习中常见的评价指标有准确率、召回率、F1等,主要用来衡量一个模型的效果好坏。通常以关注的类为正类,其他类为负类。分类器在测试数据集上预测要么正确要么不正确。4种情况出现的总数分别记作,也就是混淆矩阵:
tp--将正类预测为正类(true positive)
fn--将正类预测为负类(false negative)
fp--将负类预测为正类(false positive)
tn--将负类预测为负类(true negative)
用医院诊断病人是否有病作为示例,“有病”是被关注的正类,“没病”是负类,混淆矩阵如下。
预测为有病 | 预测为没病 | |
病人 | TP | FN |
正常人 | FP | TN |
不同指标的计算方法为:
精确度:precision = TP / (TP + FP)
召回率:recall = TP / (TP + FN)
准确率:accuracy = (TP + TN) / (TP + FP + TN + FN)
错误率:error rate = (FN + FP) / (TP + FP + TN + FN)
F1值:F1 = 2*P*R/(P+R),其中P和R分别为 precision 和 recall
F1值是精确度和召回率的调和平均值。 这是因为通常情况下,precision高的话,recall就会低;precision低的时候,recall往往比较高。为了权衡这种关系(tradeoff),所以有了F1值。
(1)混淆矩阵
from sklearn.metrics import confusion_matrix
y_true = [2, 0, 2, 2, 0, 1]
y_pred = [0, 0, 2, 2, 0, 2]
confusion_matrix(y_true, y_pred)
array([[2, 0, 0],
[0, 0, 1],
[1, 0, 2]])
(2)准确率计算
from sklearn.metrics import accuracy_score
y_pred = [0, 2, 1, 3]
y_true = [0, 1, 2, 3]
accuracy_score(y_true, y_pred) ##计算准确率
0.5
accuracy_score(y_true, y_pred, normalize=False) ##结果正确的个数
2
(3)精确度计算
对于二分类模型来说,精确度是很容易计算的,只是主要介绍一些多分类的情况。sklearn中精确度的计算方法如下:
from sklearn.metrics import precision_score
precision_score(y_true, y_pred, labels=None, pos_label=1, average='binary', sample_weight=None)
可以看到 average 参数的默认值是 'binary',也就是二进制的情况,对于多分类的情况必须修改这个参数,参数意义如下:
- average = 'macro',计算每个类别的 precision_score,然后求平均值。
- average='micro',计算总体的 precision_score,相当于accuracy_score。
- average = None,分别计算每个类别的 precision_score,计算结果是每个类别的计算值。
- average='weighted',按照y_true中各类别所在比例进行加权计算。
from sklearn.metrics import precision_score
y_pred = [0, 2, 1, 2, 5, 0]
y_true = [0, 2, 3, 4, 6, 0]
print(accuracy_score(y_true, y_pred)) # 3/6 = 0.5
print(precision_score(y_true,y_pred,average='micro')) # 3/6 = 0.5
print(precision_score(y_true,y_pred,average='macro')) # (1+1/2) / 7 = 0.21428571428571427
print(precision_score(y_true,y_pred,average='weighted')) # 1*1/3 + 1/2 * 1/6 = 0.4166666666666667
print(precision_score(y_true,y_pred,average=None)) # [1. 0. 0.5 0. 0. 0. 0. ]
其他多分类指标的计算方法和 precision 大同小异。
3.4 k折交叉验证
机器学习建模过程中,经常会把数据集分成三类,即:训练集、验证集、测试集,但是在划分过程中,可能存在随机因素导致数据集分布不均衡,例如某一类的样本都分在了验证集,那么训练得到的模型参数就不会理想。因此,为了排除偶然因素,建立更好的模型,出现了k折交叉验证的方法。
k折交叉验证与案例也很简单,原来的处理方法是把大量数据作为训练集(例如4/5),少量数据作为验证集验证模型(例如1/5),k折交叉验证处理的时候不同点在于,不再仅仅把固定的一部分作为验证集,而是依次把每个1/5部分都用来作为验证集建模,前面提到的1/5就是5折交叉验证,即每次拿出不同的1/5部分作为验证集,其余4/5作为训练集训练模型,得到5个模型,根据这5个模型的综合效果选出最好的参数。
3.5 LogisticRegression
LogisticRegression是sklearn中逻辑回归的工具类,主要参数如下:
def __init__(self, penalty='l2', dual=False, tol=1e-4, C=1.0,
fit_intercept=True, intercept_scaling=1, class_weight=None,
random_state=None, solver='warn', max_iter=100,
multi_class='warn', verbose=0, warm_start=False, n_jobs=None,
l1_ratio=None):
-
penalty:正则化惩罚项,'l1', 'l2', 'elasticnet' or 'none',关于L1和L2正则化惩罚项的实现区别是一个是平方,一个是绝对值(模),关于l1、l2以及solver参数的更多内容可以阅读参考资料中的附录文章。
-
C:正则化乘法系数的倒数,值越小,惩罚越强,也是主要调节的参数。
-
solver:求解方法(优化器),可选值为'newton-cg', 'lbfgs', 'liblinear', 'sag', 'saga'。其中'newton-cg','sag' and 'lbfgs'仅支持l2惩罚项,因为l1正则化惩罚项的损失函数不是连续可导的。
3.6 构建模型
在完成数据处理以后就可以开始建模了,在模型训练过程中我们采用5折交叉验证和召回率来查找合适的参数。
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 score'])
results_table['C_parameter'] = c_param_range
j = 0
for c_param in c_param_range:
print('-------------------------------------------')
print('C parameter: ', c_param)
print('-------------------------------------------')
print('')
recall_accs = []
for iteration, indices in enumerate(fold.split(y_test_undersample),start=1):
lr = LogisticRegression(C = c_param, penalty = 'l1')
lr.fit(x_train_data.iloc[indices[0],:],y_train_data.iloc[indices[0],:].values.ravel())
y_pred_undersample = lr.predict(x_train_data.iloc[indices[1],:].values)
recall_acc = recall_score(y_train_data.iloc[indices[1],:].values,y_pred_undersample)
recall_accs.append(recall_acc)
print('Iteration ', iteration,': recall score = ', recall_acc)
results_table.loc[j,'Mean recall score'] = np.mean(recall_accs)
j += 1
print('')
print('Mean recall score ', np.mean(recall_accs))
print('')
print(results_table)
results_table['Mean recall score'] = results_table['Mean recall score'].map(lambda x:float(x))
best_c = results_table.iloc[[results_table['Mean recall score'].idxmax()]]['C_parameter'].values[0]
print('*********************************************************************************')
print('Best model to choose from cross validation is with C parameter = ', best_c)
print('*********************************************************************************')
return best_c
best_c = printing_Kfold_scores(X_train_undersample,y_train_undersample)
3.7 预测
在3.5节中我们已经找到了最合适的模型参数,此时就可以把全量数据作为训练集训练模型,并在测试集上分析效果。
import itertools
##绘制混淆矩阵
def plot_confusion_matrix(cm, classes,
title='Confusion matrix',
cmap=plt.cm.Blues):
"""
This function prints and plots the confusion matrix.
"""
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')
lr = LogisticRegression(C = best_c, penalty = 'l1')
lr.fit(X_train_undersample,y_train_undersample.values.ravel())
y_pred_undersample = lr.predict(X_test_undersample.values)
# Compute confusion matrix
cnf_matrix = confusion_matrix(y_test_undersample,y_pred_undersample)
np.set_printoptions(precision=2)
print("Recall metric in the testing dataset: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))
# Plot non-normalized confusion matrix
class_names = [0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix')
plt.show()
三、参考资料
https://www.jianshu.com/p/bbdeb356057e