机器学习(三)——逻辑回归(上)

2021-09-28  本文已影响0人  夏普123

无论是分类还是回归,都是想建立一个预测模型 ,给定一个输入 , 可以得到一个输出 。不同的只是在分类问题中, 输出是离散的; 而在回归问题中输出是连续的。如何用线性回归来进行分类问题的预测模型建立呢? 那就需要使用逻辑回归。所以逻辑回归的输出一定是0 / 1或者True / False,或者yes / no。因此逻辑回归的本质就是根据一堆输入特征,来判断分类结果为0或者为1的可能性大小。
还记得KNN么?KNN是通过一个样本在特征空间中的K个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别来进行类别的预测(判定),那么逻辑回归是如何来进行样本的分类的呢?看下图:



现在有这样一个分类任务,需要根据肿瘤大小来判断肿瘤的良性与否。训练集如上图所示,横轴代表肿瘤大小,纵轴表示肿瘤的良性与否,注意,纵轴只有两个取值,1(代表恶性肿瘤)和0(代表良性肿瘤)。
我们可用线性回归思路,去拟合一条直线(如下图):



有了这条直线后,我们就可以进行预测:
如果y>=0.5,就是恶性,如果y<0.5,就是良性。

这看起来好像没有什么问题,但是假如我们的样本是如下这种情况呢?



你会发现此时得到的回归线是蓝色的这条线,如果我们还是按照y>=0.5代表恶性,y<0.5代表良性,就会非常不准。因此我们说,直接使用线性回归模型来进行判断,鲁棒性非常差。

这里还有另外一个比较严重的问题:采用线性回归的时候,我们得到的y值,有可能大于1或者小于0,此时又该如何进行解释呢?这给我们进行解释也带来了不便性。为此,我们要换一种思路:

还是上面这个例子:有没有人跟我有同样的想法:我在中间画一条垂线,左边的代表良性的,右边的代表恶性的,这样是否可以完美区分?



再看下面这几个例子:



我们如何区分圆圈和叉叉?相信大家都可想到,我们分别可以用如下两种线条来进行区分:


图1
图2

所以,我们是不是找到这样的线条,就可以来预测样本的分类了?比如对于图1,斜线上面的就是叉叉,斜线下面的就是圆圈? 对于图二,粉色圆圈外面的就是叉叉,内部的就是圆圈?换成数学的表达,对于图1,我们可以找到这么一条直线:\beta_0+\beta_1x_1+\beta_2x_2,当其大于0时,就是代表斜线的上方,此时就是叉叉。当其小于0时,就是代表斜线的下方,此时就是圈圈(等于0就是斜线本身)。同理,对于图2,我们可以找到这么一条曲线(圆):\beta_0+\beta_1x_1+\beta_2x_2+\beta_3x_1^2+\beta_4x_2^2,当其大于0时,就是代表圆的外面,此时就是叉叉。当其小于0时,就是代表圆的里面,此时就是圈圈。

那我们如何才能找到这样的线条呢?回到分类的本质,对于二元分类,就是要输出0,1。我们能否找到一个函数,它能够使的应变量y的取值范围在0到1之间,同时当x>0时,我们可以认为结果更偏向于1,当x<0时,我们认为结果更偏向于0。此时我们引入了sigmoid 函数,也叫逻辑函数(logistic function):g(z)=\frac{1}{1+e^{-z}},它的图形如下:


可以看到,该函数完美的解决了上述问题:y的取值范围在[0,1]之间,在远离0的地方函数的值会很快接近0或者1。当x>0时,y取值为1的可能性变大,当x<0时,y取值的0的可能性变大。

结合上面的介绍,我们得到了逻辑回归的假设模型:h_\theta(x)= \frac{1}{1+e^{-\theta^Tx}}
其中,\theta^Tx就是我们之前介绍的线性模型。因此,我们可以用线性回归的思路或者方法来去求解\theta^Tx

在线性回归中,我们判断模型拟合好坏的标准是用的估计标准误,也就是残差的方差,也即:模型得到的结果和实际结果之间的均方差(MSE)来判断,也就是我们确定模型的最佳参数,是看这些参数能否是的误差的平方和最小。那么在逻辑回归中,我们要如何来确定模型的最优参数解呢?

我们再回到我们的假设模型。如上面所述,该模型最后输出的是一个[0,1]范围内的数。而我们的分类结果,不是0,就是1。那么我们的模型代表什么含义呢?它表示的是:对于一个输入的x,分类结果为1时的概率。因此对于输入点x,分类结果为类别1和类别0的概率分别为:


既然是概率,那么我们就可以使用极大似然法(最大可能性)来估计参数。
对于训练数据集,特征数据x={x1, x2, … , xm}和对应的分类数据y={y1, y2, … , ym}。构建逻辑回归模型f,对上面的公式取极大似然函数,可以得到如下的公式:

为了方便计算,我们对上述公式取对数,得到:

如果对上面的公式再取平均,并取负号,就是逻辑回归的代价函数(cost function):
J(\theta) = -\frac{1}{m}l(\theta)=-\frac{1}{m}[\sum_{i=1}^m(y_ilogh_\theta(x_i)+(1-y_i)log(1-h_\theta(x_i)))]

我们来研究一下这个代价函数:
为了简化问题,我们先只看单一样本的情况:
因为我们的y的取值只有可能为0或者1。当y=1时,1-y=0,因此上面的代价函数变为:-logh_\theta x;当y=0时,代价函数变为:-log(1-h_\theta x)。也即:
J(\theta) =\left\{\begin{array}\\-logh_\theta(x)\qquad(if\ y=1)\\-log(1-h_\theta(x))\qquad(if\ y=0)\end{array}\right.
其图像如下:



这说明了什么呢? 这说明:当y=1时,如果我们预测也为1,则付出的代价为0。如果y=1,我们预测的结果刚好相反,为0,则为此要付出的代价趋于无穷大。反之,当y=0时,如果我们预测结果为0,则代价为0,否则付出的代价趋于无穷。

回到上面的问题。既然代价函数是从极大似然函数转换而来,我们要求能使模型获得最大可能性的\theta,就是求能使代价函数获得最小值的\theta。此时,我们可以用梯度下降法来求得相关参数。梯度下降法是个啥,可以参考下面的文章。这里不再细表。

在R中如何使用逻辑回归呢?一般用glm函数:

#读取数据
heart <- read.table("heart.dat", quote="\"")
names(heart) <- c("AGE", "SEX", "CHESTPAIN", "RESTBP", "CHOL", "SUGAR", "ECG", "MAXHR", "ANGINA", "DEP", "EXERCISE", "FLUOR", "THAL", "OUTPUT")

#将一些特征因子化
heart$CHESTPAIN = factor(heart$CHESTPAIN)
heart$ECG = factor(heart$ECG)
heart$THAL = factor(heart$THAL)
heart$EXERCISE = factor(heart$EXERCISE)

#将输出范围限制在0和1之间(之前是1和2)
heart$OUTPUT = heart$OUTPUT-1

#划分训练集和测试集
library(caret)
set.seed(987954)
heart_sampling_vector <- createDataPartition(heart$OUTPUT, p = 0.85, list = FALSE)
heart_train <- heart[heart_sampling_vector,]
heart_train_labels <- heart$OUTPUT[heart_sampling_vector]
heart_test <- heart[-heart_sampling_vector,]
heart_test_labels <- heart$OUTPUT[-heart_sampling_vector]

#进行逻辑回归
heart_model = glm(OUTPUT~.,data=heart_train,family=binomial("logit"))

#查看回归后的结果
summary(heart_model)

Call:
glm(formula = OUTPUT ~ ., family = binomial("logit"), data = heart_train)

Deviance Residuals:
Min 1Q Median 3Q Max
-2.5858 -0.5099 -0.1402 0.3357 2.5654

Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -5.206523 3.467800 -1.501 0.133255
AGE -0.021182 0.028663 -0.739 0.459915
SEX 2.145034 0.631974 3.394 0.000688 ***
CHESTPAIN2 1.261204 0.934522 1.350 0.177153
CHESTPAIN3 0.530799 0.769099 0.690 0.490095
CHESTPAIN4 2.091780 0.795396 2.630 0.008542 **
RESTBP 0.026021 0.012685 2.051 0.040243 *
CHOL 0.006810 0.004516 1.508 0.131610
SUGAR -0.396573 0.663262 -0.598 0.549899
ECG1 0.625792 3.011598 0.208 0.835390
ECG2 0.527512 0.434763 1.213 0.225003
MAXHR -0.027576 0.013446 -2.051 0.040285 *
ANGINA 0.843417 0.501853 1.681 0.092840 .
DEP 0.277991 0.250989 1.108 0.268044
EXERCISE2 0.874833 0.543766 1.609 0.107651
EXERCISE3 -0.090822 1.106373 -0.082 0.934575
FLUOR 1.167552 0.320072 3.648 0.000265 ***
THAL6 -0.797323 0.961214 -0.829 0.406824
THAL7 1.343818 0.463658 2.898 0.003752 **


Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

Null deviance: 317.11 on 229 degrees of freedom
Residual deviance: 149.23 on 211 degrees of freedom
AIC: 187.23

Number of Fisher Scoring iterations: 6

从上面的输出我们可以看到如下几点:

  1. glm函数会自动将一些因子化的变量根据level的数量自动转换为n-1个虚拟变量。其中n=level的数量。(caret包中的dummyVars函数也有同样的功能);

  2. 由于逻辑回归中,h_\theta(x_i)本身就是一个概率函数,因此逻辑回归方程中没有误差项。因此在glm中,我们看不到残差项,但是为了跟线性回归保持一致,引入了一个偏差残差。这里的偏差类似于上面提到的代价函数(损失函数),它是似然函数取对数再乘以-2,偏差残差就是在偏差的基础上取根号后,再求平方和。

  3. 空偏差是指没有任何特征,只有常数项\beta_0的偏差。类似于线性回归中的总平方和(TSS)。其对应关系如下:

  4. 我们注意到,年龄的系数是负的,这跟我们常识不符(常识告诉我们,年龄越大,得心脏病的几率越大),因此我们怀疑存在多重共线性的问题;
    我们单独对年龄进行回归,发现年龄跟实际的心脏病是成正相关的,且P-value 变小了,显著性明显增强,因此更能证明我们之前的模型存在多重共线性问题;

> heart_model2 = glm(OUTPUT~AGE,data=heart_train,family=binomial("logit"))
> summary(heart_model2)

Call:
glm(formula = OUTPUT ~ AGE, family = binomial("logit"), data = heart_train)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-1.4842  -1.0955  -0.8892   1.1916   1.6059  

Coefficients:
            Estimate Std. Error z value Pr(>|z|)   
(Intercept)  -2.3885     0.8371  -2.853  0.00433 **
AGE           0.0406     0.0151   2.688  0.00718 **
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 317.11  on 229  degrees of freedom
Residual deviance: 309.56  on 228  degrees of freedom
AIC: 313.56

Number of Fisher Scoring iterations: 4

有了模型后,我们就可以来进行预测:

#对训练集进行预测,查看我们模型在训练集上的拟合程度:
#注意,type=“response”就是代表着预测概率,也就是我们逻辑函数上的y值。
#type="terms"是代表拟合值
#type 不指定时,代表逻辑函数中x轴上的值,即归一化的线性预测
train_predictions = predict(heart_model, newdata=heart_train, type="response")
#对于每一个观测值,我们认为大于0.5的判断为有病(1),小于等于0.5的判断为没有病
train_class_predictions = as.numeric(train_predictions>0.5)
#统计预测正确率
mean(train_class_predictions==heart_train$OUTPUT)
#查看预测错误的数据
train_predictions[train_class_predictions!=heart_train$OUTPUT]

#对测试集进行预测,查看我们模型在训练集上的拟合程度:
test_predictions = predict(heart_model, newdata=heart_test, type="response")
test_class_predictions = as.numeric(test_predictions>0.5)
mean(test_class_predictions==heart_test$OUTPUT)

还有几点需要注意:

  1. 我们之前默认进行分类时,是默认按0.5的标准来划分的。但实际上按0.5标准划分并不一定是我们想要的结果。还记得我们在最开始讲过查全率、查准率吗?在某些情况下,我们可能更希望查全率高一些,查准率低一些。有些时候我们希望查准率高一些,查全率低一些。这些都会影响我们参数的选择。因此,在选择参数时,我们要根据实际情况来决定。在机器学习(一)中,我们提到了F分数和卡巴统计量。这里我们再引入一个评判分类模型好坏的统计量——AUC。
    那么什么是AUC呢?要讲清楚AUC,我们先来将ROC。ROC的全称是Receiver Operating Characteristic Curve,中文名字叫“受试者工作特征曲线”。该曲线的横坐标为假阳性率(False Positive Rate, FPR),FPR=\frac{FP}{FP+FN},纵坐标为真阳性率(True Positive Rate, TPR), TPR=\frac{TP}{TP+TN},我们可以看到,其实真阳性率也就是我们之前说到的查全率。ROC的图形一般如下:

    该曲线是怎么画出来的呢? 就是我们上面提到的,对于同一个模型,判断的阈值不同,对应的TPR和FPR也会不同。所有阈值对应的PR和FPR形成的点连起来,就形成了ROC曲线。
    一般我们会希望我们的真阳性率越高越好,而假阳性率越低越好。也就是点越靠近左上角,结果是越好的。所以,我们就定义了曲线下面积 AUC 。这个面积越大就说明整体更靠近左上,效果也就越好。
#计算混淆矩阵
confusion_matrix <- table(predicted = train_class_predictions, actual = heart_train$OUTPUT)
#计算查准率
precision <- confusion_matrix[2,2]/sum(confusion_matrix[2,])
#计算查全率
recall <- confusion_matrix[2,2]/sum(confusion_matrix[,2])
#计算F分数
f = 2 * precision * recall / (precision + recall)

#画ROC曲线或者P-R曲线
library(ROCR)
train_predictions = predict(heart_model, newdata=heart_train, type="response")
pred <- prediction(train_predictions, heart_train$OUTPUT)
perf <- performance(pred, measure = "prec", x.measure = "rec") #画p-R曲线
perf <- performance(pred,"tpr","fpr"); #画ROC曲线

plot(perf, main = "Precision-Recall Curve for Heart Model", lwd = 2)
  1. 对于逻辑回归,当特征数较多时,也会有可能存在过拟合的情况。我们之前在线性回归中讲到的有偏估计(Lasso和岭回归),对于逻辑回归同样有用:
library(glmnet)
heart_train_mat <- model.matrix(OUTPUT~., heart_train)[,-1]
lambdas <- 10 ^ seq(8,-4,length=250)
heart_models_lasso= glmnet(heart_train_mat,heart_train$OUTPUT,alpha=1,lambda=lambdas, family="binomial")

lasso.cv <- cv.glmnet(heart_train_mat,heart_train$OUTPUT,alpha=1,lambda=lambdas, family="binomial")
lambda_lasso <- lasso.cv$lambda.min

lasso_train_predictions <- predict(heart_models_lasso, s = lambda_lasso, newx = heart_train_mat, type="response")
lasso_train_class_predictions = as.numeric(lasso_train_predictions>0.5)
mean(lasso_train_class_predictions==heart_train$OUTPUT)

heart_test_mat <- model.matrix(OUTPUT~., heart_test)[,-1]
lasso_test_predictions <- predict(heart_models_lasso, s = lambda_lasso, newx = heart_test_mat, type="response")
lasso_test_class_predictions = as.numeric(lasso_test_predictions>0.5)
mean(lasso_test_class_predictions==heart_test$OUTPUT)

【参考文章】:
一文搞懂极大似然估计
贝叶斯公式的直观理解(先验概率/后验概率)
一篇文章完全弄懂Logistic回归
{高中生能看懂的}梯度下降是个啥?
逻辑回归笔记总结

上一篇下一篇

猜你喜欢

热点阅读