我们在机器学习(二)——线性回归中介绍了线性回归算法,我们知道线性回归主要做的工作是拟合样本数据,用拟合好的模型进行预测。在本篇中我们将介绍逻辑回归,它主要用来解决分类问题。
文章目录
1. 引言
1.1 分类问题
在日常生活中我们会遇到各种各样的分类:垃圾分类、邮件分类、商品分类等等。我们从最简单的二分类问题引入:肿瘤分类问题,即区分一个肿瘤是恶性的还是良性的?
假设用1表示肿瘤是恶性的,用0表示肿瘤是良性的,现在我们的样本数据如下图所示。我们的样本数据的特征是肿瘤的大小,输出标记为0或1(其中1代表恶性,0代表良性)。
注:图片来自吴恩达老师的PPT
我们现在希望能够训练出一个模型,我们输入肿瘤的大小,模型为我们预测该肿瘤是恶性还是良性。我们如何得到这样一个模型呢?我们首先想尝试使用线性回归来解决这个问题。
假设我们根据样本数据拟合得到这样的一条直线作为预测模型:
我们最终的预测结果为0或1,所以我们可以得到一个预测阈值0.5:
(1)当
h
θ
(
x
)
>
=
0.5
h_{\theta}(x)>=0.5
hθ(x)>=0.5时,预测结果为1;
(2)当
h
θ
(
x
)
<
0.5
h_{\theta}(x)<0.5
hθ(x)<0.5时,预测结果为0.
但是倘若我们的样本数据中出现了极端数据,我们拟合得到的直线是下图中的蓝色直线,结果又会如何呢?
我们可以看出此时再使用0.5作为阈值便不合适了,而且线性回归模型的预测值并不是在[0,1]之间,它可以超越[0,1]的范围,这和我们希望的预测值只有0或1的意愿相违背。
那么就需要用新的方法来得到新的分类模型,使用该分类模型能够得到我们希望的分类结果。
1.2 逻辑回归
可见线性回归并不能很好地解决分类问题,因此我们引入逻辑回归方法。虽然它也叫“回归”,但它主要用来解决分类问题。
我们依然考虑二分类问题,假设我们的预测结果只有0或1。我们希望我们的模型能够:
(1)当模型根据输入的数据计算得到的值>=0.5时,预测结果为1;
(2)当模型根据输入的数据计算得到的值<0.5时,预测结果为0。
(反之亦可)
这样我们通过计算样本发生的概率,就能得到样本的分类,接下来我们就详细介绍一下逻辑回归。
2. 逻辑回归算法
2.1 逻辑回归模型
通过1.2,我们预期得到的模型的输出范围为[0,1],且输出值大于等于0.5时预测结果为1,输出值小于0.5时预测结果为0。
恰好就有这样一个符合我们要求的函数,sigmod函数:
g
(
z
)
=
1
1
+
e
−
z
g(z) = \frac{1}{1 + e^{-z}}
g(z)=1+e−z1
sigmod函数的特点很明显:
(1)
z
=
0
z=0
z=0时,
g
(
z
)
=
0.5
g(z)=0.5
g(z)=0.5;
(2)
z
>
0
z>0
z>0时,
g
(
z
)
>
0.5
g(z)>0.5
g(z)>0.5;
(3)
z
<
0
z<0
z<0时,
g
(
z
)
<
0.5
g(z)<0.5
g(z)<0.5。
由此,我们假设逻辑回归模型为:
h
θ
(
x
)
=
g
(
θ
T
x
)
h_{\theta}(x) = g(\theta^Tx)
hθ(x)=g(θTx)
显而易见,有:
(1)当
θ
T
x
>
=
0
\theta^Tx>=0
θTx>=0时,
h
θ
(
x
)
>
=
0.5
h_{\theta}(x)>=0.5
hθ(x)>=0.5,预测结果为1;
(2)当
θ
T
x
<
0
\theta^Tx<0
θTx<0时,
h
θ
(
x
)
<
0.5
h_{\theta}(x)<0.5
hθ(x)<0.5,预测结果为0。
这个模型就很好地符合了我们的预想。
2.2 决策边界
什么是决策边界?我们先来举个例子。
假设我们现在有这样的样本数据:
我们的预测模型为:
h
θ
(
x
)
=
g
(
θ
0
+
θ
1
x
1
+
θ
2
x
2
)
h_{\theta}(x)=g(\theta_0+\theta_1x_1+\theta_2x_2)
hθ(x)=g(θ0+θ1x1+θ2x2)
且参数
θ
=
(
θ
0
,
θ
1
,
θ
2
)
T
=
(
−
3
,
1
,
1
)
T
\theta=(\theta_0,\theta_1,\theta_2)^T=(-3,1,1)^T
θ=(θ0,θ1,θ2)T=(−3,1,1)T
于是我们知道:
(1)当
θ
T
x
=
−
3
+
x
1
+
x
2
>
=
0
\theta^Tx=-3+x_1+x_2>=0
θTx=−3+x1+x2>=0 时,
h
θ
(
x
)
>
=
0.5
h_{\theta}(x)>=0.5
hθ(x)>=0.5,预测结果为1;
(2)当
θ
T
x
=
−
3
+
x
1
+
x
2
<
0
\theta^Tx=-3+x_1+x_2<0
θTx=−3+x1+x2<0 时,
h
θ
(
x
)
<
0.5
h_{\theta}(x)<0.5
hθ(x)<0.5,预测结果为0;
我们可以在二维平面上绘制一条直线
x
1
+
x
2
=
3
x_1+x_2=3
x1+x2=3 ,这条直线能够将预测结果为0的区域和预测结果为1的区域分开:
这样的一条直线就叫做决策边界。
再假如我们的样本数据是这样的:
我们的预测模型为:
h
θ
(
x
)
=
g
(
θ
0
+
θ
1
x
1
+
θ
2
x
2
+
θ
3
x
1
2
+
θ
4
x
2
2
)
h_{\theta}(x)=g(\theta_0+\theta_1x_1+\theta_2x_2+\theta_3x_1^2+\theta_4x_2^2)
hθ(x)=g(θ0+θ1x1+θ2x2+θ3x12+θ4x22)
且参数
θ
=
(
θ
0
,
θ
1
,
θ
2
,
θ
3
,
θ
4
)
T
=
(
−
1
,
0
,
0
,
1
,
1
)
T
\theta = (\theta_0,\theta_1,\theta_2, \theta_3,\theta_4)^T=(-1,0,0,1,1)^T
θ=(θ0,θ1,θ2,θ3,θ4)T=(−1,0,0,1,1)T
于是我们知道:
(1)当
θ
T
x
=
−
1
+
x
1
2
+
x
2
2
>
=
0
\theta^Tx=-1+x_1^2+x_2^2>=0
θTx=−1+x12+x22>=0 时,
h
θ
(
x
)
>
=
0.5
h_{\theta}(x)>=0.5
hθ(x)>=0.5,预测结果为1;
(2)当
θ
T
x
=
−
1
+
x
1
2
+
x
2
2
<
0
\theta^Tx=-1+x_1^2+x_2^2<0
θTx=−1+x12+x22<0 时,
h
θ
(
x
)
<
0.5
h_{\theta}(x)<0.5
hθ(x)<0.5,预测结果为0;
我们可以在二维平面上绘制一条曲线
x
1
2
+
x
2
2
=
1
x_1^2+x_2^2=1
x12+x22=1 ,这条曲线能够将预测结果为0的区域和预测结果为1的区域分开:
这样的一条曲线就是决策边界。
我的理解就是,决策边界就是将不同类别的样本数据区分开来的那条边界,在这条边界的一侧的数据属于这一类,在这条边界的另一侧的数据不属于这一类。
我们的逻辑回归模型似乎就是在找这条决策边界。
2.3 逻辑回归的损失函数
2.3.1 损失函数的定义
我们先来思考在线性回归中是怎么定义损失函数的?计算所有误差的平方和:
J
(
θ
)
=
1
2
m
∑
i
=
1
m
(
h
θ
(
x
(
i
)
)
−
y
(
i
)
)
2
J(\theta) = \frac{1}{2m}\sum_{i=1}^m (h_{\theta}(x^{(i)}) - y^{(i)})^2
J(θ)=2m1i=1∑m(hθ(x(i))−y(i))2
倘若将我们的逻辑回归模型
h
θ
(
x
)
=
1
1
+
e
−
θ
T
x
h_{\theta}(x) = \frac{1}{1+e^{-\theta^Tx}}
hθ(x)=1+e−θTx1带入到线性回归的损失函数定义中,我们得到的是一个非凸函数,如下图所示,函数有许多局部最小值,这将影响梯度下降算法寻找全局最小值:
为了不影响梯度下降算法寻找全局最小值,我们重新定义逻辑回归的损失函数:
C
o
s
t
(
h
θ
(
x
)
,
y
)
=
{
−
l
o
g
(
h
θ
(
x
)
)
if y=1
−
l
o
g
(
1
−
h
θ
(
x
)
)
if y=0
Cost(h_{\theta}(x),y) = \begin{cases} -log(h_{\theta}(x)) & \text{if y=1}\\ -log(1-h_{\theta}(x))& \text{if y=0}\\ \end{cases}
Cost(hθ(x),y)={−log(hθ(x))−log(1−hθ(x))if y=1if y=0
为什么要这样定义损失函数呢?我们来画图理解一下。
(1)当
y
=
1
y=1
y=1时,如果我们的预测值
h
θ
(
x
)
h_{\theta}(x)
hθ(x)越接近1,那么损失函数的值就越小,反之,损失函数的值越大;
(2)当
y
=
0
y=0
y=0时,如果我们的预测值
h
θ
(
x
)
h_{\theta}(x)
hθ(x)越接近0,那么损失函数的值就越小,反之,损失函数的值越大。
因此这就符合了我们损失函数的定义:预测值和真实值越接近,损失越小;预测值和真实值差距越大,损失越大。所以我们就知道了为什么要这么定义逻辑回归的损失函数了。
2.3.2 使用梯度下降最小化损失函数
在进行梯度下降之前,先对损失函数进行化简:
C
o
s
t
(
h
θ
(
x
)
,
y
)
=
−
y
×
l
o
g
(
h
θ
(
x
)
)
−
(
1
−
y
)
×
l
o
g
(
1
−
h
θ
(
x
)
)
Cost(h_{\theta}(x) ,y)=-y\times log(h_{\theta}(x))-(1-y)\times log(1-h_{\theta}(x))
Cost(hθ(x),y)=−y×log(hθ(x))−(1−y)×log(1−hθ(x))
进一步有:
J
(
θ
)
=
−
1
m
∑
i
=
1
m
[
y
(
i
)
l
o
g
(
h
θ
(
x
(
i
)
)
)
+
(
1
−
y
(
i
)
)
l
o
g
(
1
−
h
θ
(
x
(
i
)
)
)
]
J(\theta)=-\frac{1}{m}\sum_{i=1}^m [y^{(i)}log(h_{\theta}(x^{(i)}))+(1-y^{(i)})log(1-h_{\theta}(x^{(i)}))]
J(θ)=−m1i=1∑m[y(i)log(hθ(x(i)))+(1−y(i))log(1−hθ(x(i)))]
此时我们得到的
J
(
θ
)
J(\theta)
J(θ)是一个凸函数,而且它没有局部最小值,之后我们使用梯度下降算法来求使得
J
(
θ
)
J(\theta)
J(θ)最小的参数
θ
\theta
θ。
根据梯度下降算法(梯度下降可参考:机器学习(三)——梯度下降)我们知道算法会一直执行如下操作直至收敛:
θ
j
=
θ
j
−
α
∂
J
(
θ
)
∂
θ
j
\theta_j = \theta_j - \alpha \frac{\partial J(\theta)}{\partial \theta_j}
θj=θj−α∂θj∂J(θ)
对
θ
j
\theta_j
θj求偏导之后得到:(求导过程不再赘述)
θ
j
=
θ
j
−
α
1
m
∑
i
=
1
m
(
h
θ
(
x
(
i
)
)
−
y
(
i
)
)
x
j
(
i
)
\theta_j = \theta_j - \alpha \frac{1}{m}\sum_{i=1}^m(h_{\theta}(x^{(i)}) -y^{(i)})x_j^{(i)}
θj=θj−αm1i=1∑m(hθ(x(i))−y(i))xj(i)
注意
θ
j
\theta_j
θj是同时更新的。
2.3.3 一些高级优化
吴恩达老师的视频中介绍了一些高级优化算法(视频课可参考:B站吴恩达机器学习系列视频),老师讲解道:当我们执行梯度下降算法时,算法会计算出:
(1)
J
(
θ
)
J(\theta)
J(θ);
(2)
j
=
0
,
1
,
.
.
.
,
n
j=0,1,..., n
j=0,1,...,n时的偏导数项。
算法会反复执行同时更新 θ j \theta_j θj,此时考虑:如果我们编写代码来计算 J ( θ ) J(\theta) J(θ)和这些偏导数,然后将其插入到梯度下降算法中,执行算法就能最小化损失函数。
老师介绍了共轭梯度法BFGS和L-BFGS,本人查阅了一些关于BFGS资料,在此做一下整理:
(1)一篇文章:深入机器学习系列17-BFGS & L-BFGS
(2)一片博文,介绍 了BFGS算法和在python的包中调用最优化算法函数:BFGS算法
(3)一篇介绍python包中的BFGS博文:【python】scipy包中的BFGS算法
如果对这些高级优化算法感兴趣,大家可以参考这些内容或查阅相关资料,本人对此没有深入研究,在此就简单整理一下我查阅过的资料,给大家提供一个参考。
2.4 解决多分类问题
对于多分类问题,吴恩达老师介绍了一种叫做one-vs-all一对多的方法,有些地方也称其为one-vs-rest。还有一种叫做one-vs-one的方法也可以用来解决多分类问题,接下来就简要介绍这两种方法。
2.4.1 OvR(one-vs-rest)
假设我们的样本数据是这样的:(三种颜色代表三种不同的类别)
我们假设红色点为类别1,蓝色点为类别2,绿色点为类别3。
OvR算法在进行分类时,它会把多分类问题转变为多个二分类问题,比如当对红色的点(类别为1)进行分类时,算法会把红色点的类别标记为正向类,记
y
=
1
y=1
y=1,然后将其他的点(蓝色和绿色的点)标记为负向类,得到的分类模型为
h
θ
(
1
)
(
x
)
h_{\theta}^{(1)}(x)
hθ(1)(x)。同样的,对蓝色点(类别为2)进行分类时,算法会把蓝色点的类别标记为正向类,记
y
=
2
y=2
y=2,然后将其他的点(红色和绿色的点)标记为负向类,得到的分类模型为
h
θ
(
2
)
(
x
)
h_{\theta}^{(2)}(x)
hθ(2)(x),以此类推,绿色点的分类模型就是
h
θ
(
3
)
(
x
)
h_{\theta}^{(3)}(x)
hθ(3)(x)。
于是我们得到了三个分类器: h θ ( 1 ) ( x ) h_{\theta}^{(1)}(x) hθ(1)(x), h θ ( 2 ) ( x ) h_{\theta}^{(2)}(x) hθ(2)(x) 和 h θ ( 3 ) ( x ) h_{\theta}^{(3)}(x) hθ(3)(x)。那么在进行预测时,我们会运行所有的分类器,对于我们的输入变量 x p r e d i c t x_{predict} xpredict ,我们会得到三个分类得分: s c o r e ( 1 ) ( x p r e d i c t ) score^{(1)}(x_{predict}) score(1)(xpredict), s c o r e ( 12 ) ( x p r e d i c t ) score^{(12)}(x_{predict}) score(12)(xpredict) 和 s c o r e ( 3 ) ( x p r e d i c t ) score^{(3)}(x_{predict}) score(3)(xpredict),我们最后选择得分最高的作为最终结果。
2.4.2 OvO(one-vs-one)
假设我们的样本数据依然是:
OvO算法在进行分类时,它每次会选择两个类别进行二分类,如果有n个类别,那就要进行
C
n
2
C_n^2
Cn2 次分类,得到
C
n
2
C_n^2
Cn2 个分类器。
在进行预测时,同样运行所有的分类器,最后选择得分最高的作为最终结果。
3. 逻辑回归算法的实现
接下来我们简单实现一下逻辑回归算法并进行测试。注:代码编写和测试均在notebook中进行。
3.1 解决二分类问题
3.1.1 分类器的实现
定义我的自己的逻辑回归类:
class myLogisticRegression:
def __init__(self):
self.coef_ = None
self.intercept_ = None
self._theta = None
def _sigmoid(self, z):
return 1. / (1. + np.exp(-z))
def fit(self, X_train, y_train, eta=0.01, n_iter=1e4):
def J(theta, X_b, y):
y_hat = self._sigmoid(X_b.dot(theta))
# 防止溢出
try:
return -np.sum(y*np.log(y_hat) + (1-y)*np.log(1-y_hat)) / len(y)
except:
return float('inf')
def dJ(theta, X_b, y):
return X_b.T.dot(self._sigmoid(X_b.dot(theta)) - y) / len(y)
def gradient_descent(X_b, y, initial_theta, eta, n_iter=1e4, epsilon=1e-8):
theta = initial_theta
cur_iter = 0
while cur_iter < n_iter:
gradient = dJ(theta, X_b, y)
last_theta = theta
theta = theta - eta * gradient
if (abs(J(theta, X_b, y) - J(last_theta, X_b, y)) < epsilon):
break
cur_iter += 1
return theta
X_b = np.hstack([np.ones((len(X_train),1)), X_train])
initial_theta = np.zeros(X_b.shape[1])
self._theta = gradient_descent(X_b, y_train, initial_theta, eta, n_iter)
self.intercept_ = self._theta[0]
self.coef_ = self._theta[1:]
return self
def predict_(self, X_predict):
X_b = np.hstack([np.ones((len(X_predict),1)), X_predict])
return self._sigmoid(X_b.dot(self._theta))
def predict(self, X_predict):
possibility = self.predict_(X_predict)
return np.array(possibility >= 0.5, dtype='int')
def score(self, X_test, y_test):
y_predict = self.predict(X_test)
score = np.sum(y_test == y_predict) / len(y_test)
return score
3.1.2 生成样本数据
我们使用sklearn中的鸢尾花数据,我们先进行二分类,所以只选取鸢尾花的前两个特征:
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data
y = iris.target
X = X[y<2,:2]
y = y[y<2]
样本数据可视化:
plt.scatter(X[y==0,0], X[y==0,1], color="red")
plt.scatter(X[y==1,0], X[y==1,1], color="blue")
plt.show()
3.1.3 测试
划分训练集和测试集:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y)
训练我们自己的逻辑回归模型:
log_reg = myLogisticRegression()
log_reg.fit(X_train, y_train)
使用测试集数据进行预测,先看看预测得到的概率值是多少:
log_reg.predict_(X_test)
我们知道当预测的概率值>=0.5时,得到的预测结果是1,反之为0。
接下来看看预测的类别:
log_reg.predict(X_test)
我们可以看到分类类别和预测的概率值是相对应的:
最后我们来计算一下预测的得分值:
log_reg.score(X_test, y_test)
3.2 解决多分类问题
我们在上文中提到了解决多分类问题的两种方式:one-vs-rest和one-vs-one,接下来我们就来看看这两种算法是如何解决多分类问题的。
3.2.1 准备工作
参考python3入门机器学习。
(1)准备样本数据
我们依然使用sklearn中的鸢尾花数据:
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data[:,:2]
y = iris.target
(2)划分训练集和测试集
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
(3)使用sklearn中的逻辑回归类,训练一个分类器
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression()
log_reg.fit(X_train, y_train)
(4)为了后面方便可视化,这里定义一个绘制决策边界的函数
def plot_decision_boundary(model, axis):
x0, x1 = np.meshgrid(
np.linspace(axis[0], axis[1], int((axis[1]-axis[0])*100)).reshape(-1, 1),
np.linspace(axis[2], axis[3], int((axis[3]-axis[2])*100)).reshape(-1, 1),
)
X_new = np.c_[x0.ravel(), x1.ravel()]
y_predict = model.predict(X_new)
zz = y_predict.reshape(x0.shape)
from matplotlib.colors import ListedColormap
custom_cmap = ListedColormap(['#EF9A9A','#FFF59D','#90CAF9'])
plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)
3.2.2 one-vs-rest
使用sklearn中的的OvR算法:
from sklearn.multiclass import OneVsRestClassifier
ovr = OneVsRestClassifier(log_reg)
ovr.fit(X_train, y_train)
print(ovr.score(X_test, y_test))
得分为:
分类过程可视化:
plot_decision_boundary(ovr, axis=[4, 9, 1, 5])
plt.scatter(X[y==0,0], X[y==0,1])
plt.scatter(X[y==1,0], X[y==1,1])
plt.scatter(X[y==2,0], X[y==2,1])
plt.show()
3.2.3 one-vs-one
使用sklearn中的OvO算法:
from sklearn.multiclass import OneVsOneClassifier
ovo = OneVsOneClassifier(log_reg)
ovo.fit(X_train, y_train)
print(ovo.score(X_test, y_test))
得分为:
分类过程可视化:
plot_decision_boundary(ovo, axis=[4, 9, 1, 5])
plt.scatter(X[y==0,0], X[y==0,1])
plt.scatter(X[y==1,0], X[y==1,1])
plt.scatter(X[y==2,0], X[y==2,1])
plt.show()
4. 总结
(1)首先我们引入了分类问题,并讨论了为什么线性回归算法无法很好地解决分类问题,并由此引出了逻辑回归算法,逻辑回归算法主要用来解决分类问题;
(2)接着我们介绍了逻辑回归的假设模型,并解释了为什么要使用sigmoid函数,进一步又介绍了什么是决策边界;
(3)之后我们通过线性回归的损失函数,引出了逻辑回归的损失函数,并解释了为什么直接使用线性回归的损失函数是不可行的(联系非凸函数),从而我们知道了为什么要重新定义逻辑回归的损失函数;
(4)之后介绍了使用梯度下降算法来最小化逻辑回归的损失函数,并介绍了一些高级优化算法;
(5)接着我们引入了多分类问题,并介绍了两种解决多分类问题的方法:one-vs-rest 和 one-vs-one;
(6)然后我们通过编写代码实现了逻辑回归算法,并尝试用逻辑回归算法解决二分类和多分类问题。
至此,逻辑回归算法的相关内容就大致总结完了。有几点需要说明一下:
(1)因为本人也是初学者,文章内容可能会有错误和不足,非常欢迎大家指正和补充!
(2)本文是个人的理解和学习内容,有些地方并不深入,如果感兴趣并希望深入研究的话,大家可查阅相关资料。
附:
B站吴恩达老师视频课:链接
一位大佬总结的笔记(包含老师的PPT):链接
好啦,希望本篇能够帮助大家理解逻辑回归算法,也期待大家的补充和指正!期待与大家交流!