逻辑回归(Logistics Regression),逻辑回归虽然叫回归,但实际上属于分类算法,常用于二分类的任务。当然逻辑回归也可以用于多分类,这就需要加上其它的方法。至于逻辑回归是怎么解决分类问题,实质上是把样本特征和样本发生的概率联系起来。
逻辑回归,通常作为分类算法,只可以解决二分类问题。最终得出的结果是一个概率值。
首先给出逻辑回归的公式:
如何得到 p ^ \widehat{p} p 的函数表达式呢?既然叫逻辑回归,那就说明跟回归还是有关系的,先回顾一下回归问题:
通常线性方程的值域为 (- ∞ \infty ∞, + ∞ \infty ∞) ,而概率的值域为[0, 1],因此我们在这个基础上做一个变形,完成从 (- ∞ \infty ∞, + ∞ \infty ∞) 到[0,1]的转换。
这个转换函数就叫做 Sigmoid
函数,函数的表达式:
用 matplotlib
绘制一下函数图像:
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(t):
return 1 / (1 + np.exp(-t))
x = np.linspace(-10, 10, 500)
y = sigmoid(x)
plt.plot(x, y)
plt.show()
相当于把一条直线强行掰弯,使其值域在(0, 1)之间。这样就完成从直线回归到逻辑回归的转变。
接下来再了解一下这个转换函数:
现在的问题就是如何对于给定的样本数据集 x
y
如何找到最优的 θ
?
通过前面的分析已经成功地把线性预测转换成了一个概率,那如何定义损失函数呢?
把上面的描述转换为数学公式:
下面是这个函数的图像:
上面的 cost
是一个分段函数,我们将其整合起来:
到此,就可以总结出逻辑回归的损失函数:
这个函数是一个凸函数,他没有公式解,只能使用梯度下降法求解。
对于这种复杂的损失函数,我们首先来看一个sigmoid函数的求导。
接下来看看一下 log(σ(t)) 函数的求导:
进一步求导:
将两项合并(1)+(2):
再代入损失函数:
梯度的向量化过程,其实这个推导过程跟线性回归中的推导很类似。
import numpy as np
class LogisticRegression(object):
def __init__(self):
"初始化logistic回归模型"
self.coef_ = None
self.interception_ = None
self._theta = None
def _sigmoid(self, t):
return 1. / (1. + np.exp(-t))
def fit(self, x_train, y_train, eta=0.01, n_iters=1e4):
assert x_train.shape[0] == y_train.shape[0], "the size of x_train must be equal to the size of y_train"
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(X_b)
def gradient_descent(X_b, y, init_theta, eta, n_iters=1e4, epsilon=1e-8):
theta = init_theta
i_iter = 0
while i_iter < n_iters:
gradient = dj(theta, X_b, y)
last_theta = theta
theta = last_theta - eta * gradient
if (abs(J(theta, X_b, y) - J(last_theta, X_b, y)) < epsilon):
break
i_iter += 1
return theta
X_b = np.hstack([np.ones((len(x_train), 1)), x_train])
init_theta = np.zeros(X_b.shape[1])
self._theta = gradient_descent(X_b, y_train, init_theta, eta, n_iters=1e4)
self.interception_ = self._theta[0]
self.coef_ = self._theta[1:]
return self
def predict_prob(self, x_predict):
assert self.interception_ is not None and self.coef_ is not None, "must fit before predict"
assert x_predict.shape[1] == len(self.coef_), "the feature number must be equal to x_train"
X = np.hstack([np.ones((len(x_predict), 1)), x_predict])
return self._sigmoid(X.dot(self._theta))
def predict(self, x_predict):
assert self.interception_ is not None and self.coef_ is not None, "must fit before predict"
assert x_predict.shape[1] == len(self.coef_), "the feature number must be equal to x_train"
prob = self.predict_prob(x_predict)
return np.array(prob >= 0.5, dtype='int')
def score(self, x_test, y_test):
y_predict = self.predict(x_test)
assert y_test.shape[0] == y_predict.shape[0], \
"the size of y_true must be equal to the size of y_predict"
return sum(y_test == y_predict) / len(y_test)
def __repr__(self):
return "Logistic Regression"
测试一下:
import numpy as np
from sklearn import datasets
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
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()
x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=666)
log_reg = LogisticRegression()
log_reg.fit(x_train, y_train)
print(log_reg.score(x_test, y_test))
print(log_reg.predict_prob(x_test))
print(y_test)
print(log_reg.coef_)
print(log_reg.interception_)
首先回顾一下Logistic Regression进行分类的原理:
接下来再看一下Sigmoid这个函数:
def x2(x1):
return (-log_reg.coef_[0] * x1 - log_reg.interception_) / log_reg.coef_[1]
x1_plot = np.linspace(4, 8, 1000)
x2_plot= x2(x1_plot)
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.plot(x1_plot, x2_plot)
plt.show()
第四节预测明明是100%的准确率为什么还会有个红点在决策边界下方呢?因为这里绘图用的是全部数据。而测试时用的是测试集(说明训练集不是100%正确率)。
把绘制决策边界封装为一个函数
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)
plot_decision_boundary(log_reg, axis=[4, 7.5, 1.5, 4.5])
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()
from sklearn.neighbors import KNeighborsClassifier
knn_clf = KNeighborsClassifier()
knn_clf.fit(x_train, y_train)
knn_clf.score(x_test, y_test)
# 1.0
plot_decision_boundary(knn_clf, axis=[4, 7.5, 1.5, 4.5])
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()
from sklearn.neighbors import KNeighborsClassifier
knn_clf_all = KNeighborsClassifier()
knn_clf_all.fit(iris.data[:,:2], iris.target)
#KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
# metric_params=None, n_jobs=None, n_neighbors=5, p=2,
# weights='uniform')
plot_decision_boundary(knn_clf_all, axis=[4, 8, 1.5, 4.5])
plt.scatter(iris.data[iris.target==0, 0], iris.data[iris.target==0, 1])
plt.scatter(iris.data[iris.target==1, 0], iris.data[iris.target==1, 1])
plt.scatter(iris.data[iris.target==2, 0], iris.data[iris.target==2, 1])
plt.show()
逻辑回归中如果使用直线分类方式就只能针对二分类了,如果像下图中不可能使用一根直线完成分割,但是很显然可以使用圆形或者椭圆形完整这个分类任务。其实在线性回归到多项式回归我们思想就是给训练数据集添加多项式项。同理我们把这个东西用到逻辑回归中。
首先生成需要的数据:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(666)
x = np.random.normal(0, 1, size=(200, 2))
y = np.array(x[:,0] ** 2 + x[:,1] ** 2 < 1.5, dtype='int')
plt.scatter(x[y==0, 0], x[y==0, 1])
plt.scatter(x[y==1, 0], x[y==1, 1])
plt.show()
对于这样一个样本集首先使用没有多项式特征逻辑回归试一下效果如何?
显然有很多错误的分类,那接下来给逻辑回归添加多项式特征。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import StandardScaler
def PolynomiaLogisticRegression(degree):
return Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('std_scale', StandardScaler()),
('log_reg', LogisticRegression())
])
poly_log_reg = PolynomiaLogisticRegression(degree=2)
poly_log_reg.fit(x, y)
poly_log_reg.score(x, y)
# 0.94999999999999996
plot_decision_boundary(poly_log_reg, axis=[-4, 4, -4, 4])
plt.scatter(x[y==0, 0], x[y==0, 1])
plt.scatter(x[y==1, 0], x[y==1, 1])
plt.show()
poly_log_reg2 = PolynomiaLogisticRegression(degree=20)
poly_log_reg2.fit(x, y)
plot_decision_boundary(poly_log_reg2, axis=[-4, 4, -4, 4])
plt.scatter(x[y==0, 0], x[y==0, 1])
plt.scatter(x[y==1, 0], x[y==1, 1])
plt.show()
出现这样的边界形状是因为发生了过拟合现象,degree越来模型越复杂,也就越容易发生过拟合。接下来我们就来解决这个过拟合问题。这里使用模型正则化。出现这样的边界形状是因为发生了过拟合现象,degree越来模型越复杂,也就越容易发生过拟合。接下来我们就来解决这个过拟合问题。这里使用模型正则化。
在实际的应用过程中,很少有问题直接用直线就能完成分类或者回归任务,因此正则化必不可少。之前学过模型泛化的时候提到的L1正则、L2正则化的方式:
但是在sklearn中对逻辑回归中的正则化:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(666)
x = np.random.normal(0, 1, size=(200, 2))
y = np.array(x[:,0] ** 2 + x[:,1] < 1.5, dtype='int')
# 添加一些噪音。
for i in range(20):
y[np.random.randint(200)] = 1
plt.scatter(x[y==0, 0], x[y==0, 1])
plt.scatter(x[y==1, 0], x[y==1, 1])
plt.show()
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
x_train, x_test, y_train, y_test = train_test_split(x, y)
log_reg = LogisticRegression()
log_reg.fit(x, y)
# LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
# intercept_scaling=1, max_iter=100, multi_class='warn',
# n_jobs=None, penalty='l2', random_state=None, solver='warn',
# tol=0.0001, verbose=0, warm_start=False)
通过输出结果我们可以发现默认C=1.0,这个C就是最开始提到的逻辑回归中正则中的C,penalty='l2’说明sklearn默认使用L2正则来进行模型正则化。
log_reg.score(x_train, y_train)
# 0.7933333333333333
log_reg.score(x_test, y_test)
# 0.7933333333333333
plot_decision_boundary(log_reg, axis=[-4, 4, -4, 4])
plt.scatter(x[y==0, 0], x[y==0, 1])
plt.scatter(x[y==1, 0], x[y==1, 1])
plt.show()
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import StandardScaler
def PolynomiaLogisticRegression(degree):
return Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('std_scale', StandardScaler()),
('log_reg', LogisticRegression())
])
poly_log_reg = PolynomiaLogisticRegression(degree=2)
poly_log_reg.fit(x_train, y_train)
poly_log_reg.score(x_train, y_train)
# 0.9133333333333333
poly_log_reg.score(x_test, y_test)
# 0.94
plot_decision_boundary(poly_log_reg, axis=[-4, 4, -4, 4])
plt.scatter(x[y==0, 0], x[y==0, 1])
plt.scatter(x[y==1, 0], x[y==1, 1])
plt.show()
poly_log_reg2 = PolynomiaLogisticRegression(degree=20)
poly_log_reg2.fit(x_train, y_train)
poly_log_reg2.score(x_train, y_train)
# 0.94
poly_log_reg2.score(x_test, y_test)
# 0.92
plot_decision_boundary(poly_log_reg2, axis=[-4, 4, -4, 4])
plt.scatter(x[y==0, 0], x[y==0, 1])
plt.scatter(x[y==1, 0], x[y==1, 1])
plt.show()
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import StandardScaler
# 传入一个新的参数C
def PolynomiaLogisticRegression(degree, C):
return Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('std_scale', StandardScaler()),
('log_reg', LogisticRegression(C=C))
])
poly_log_reg3 = PolynomiaLogisticRegression(degree=20, C=0.1)
poly_log_reg3.fit(x, y)
poly_log_reg3.score(x_train, y_train)
# 0.8533333333333334
poly_log_reg3.score(x_test, y_test)
# 0.92
plot_decision_boundary(poly_log_reg3, axis=[-4, 4, -4, 4])
plt.scatter(x[y==0, 0], x[y==0, 1])
plt.scatter(x[y==1, 0], x[y==1, 1])
plt.show()
在开始之初,说逻辑回归只可以解决二分类问题, 其实可以稍加改造使其能够解决多分类问题。当然这个改造方式并不是只针对逻辑回归这一种算法,这是一种通用的近乎于可以改造所有的二分类。
(One vs Rest)一对剩余,有些说法也叫(One vs All,OVA)。比如下图中的这个四分类问题,显然如果使用逻辑回归一条直线分出4类是不现实的,但是如果我们取出其中任意一种,将剩下的作为另一种,这种就是一个2分类问题,同理将每一个类别分别做一次这样的2分类,如果有n个类别就进行n次分类,选择分类得分最高的。就类似于下图这种,进行C(n,1)中分类。从而完成多分类。
(One vs One)一对一,就是在多个类别中,先挑出2个来进行2分类,然后逐个进行,也就是C(n,2)中情况进行2分类,选择赢数最高的分类。比如一个手写数字的识别任务来说就要进行C(10,2)=45次分类,才能完成任务。很显然它相比OvR(n级别)来说,OvO这是一个n2级别的,要比耗时更多,但是它分类的结果更加准确。这是因为每次都是在用两个真实的类别再进行2分类,而没有混淆其他的类别信息。
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
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)
iris = datasets.load_iris()
x = iris.data[:, :2]
y = iris.target
x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=666)
log_reg = LogisticRegression()
log_reg.fit(x_train, y_train)
# LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
# intercept_scaling=1, max_iter=100, multi_class='warn',
# n_jobs=None, penalty='l2', random_state=None, solver='warn',
# tol=0.0001, verbose=0, warm_start=False)
从输出结果来看,这个multi_class=‘warn’,这个multi_class就是多分类的意思,从官方文档来看默认是OvR,solver这个参数是因为在sklearn中并不是简单地使用梯度下降,因此我们需要给不同的方法传入不同的解决办法。
log_reg = LogisticRegression(multi_class='ovr',solver='liblinear')
log_reg.fit(x_train, y_train)
log_reg.score(x_test, y_test)
# 0.6578947368421053
plot_decision_boundary(log_reg, axis=[4, 8.5, 1.5, 4.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()
log_reg2 = LogisticRegression(multi_class='multinomial', solver='newton-cg')
log_reg2.fit(x_train, y_train)
# 0.7894736842105263
plot_decision_boundary(log_reg2, axis=[4, 8.5, 1.5, 4.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个特征,而只用了前两个,为了方便可视化,下面就是使用所有数据。
iris = datasets.load_iris()
x = iris.data[:, :]
y = iris.target
x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=666)
# OvR
log_reg3 = LogisticRegression(multi_class='ovr', solver='liblinear')
log_reg3.fit(x_train, y_train)
log_reg3.score(x_test, y_test)
# 0.9473684210526315
# OvO
log_reg4 = LogisticRegression(multi_class='multinomial', solver='newton-cg')
log_reg4.fit(x_train, y_train)
log_reg4.score(x_test, y_test)
# 1.0
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression()
from sklearn.multiclass import OneVsRestClassifier
ovr = OneVsRestClassifier(log_reg)
ovr.fit(x_train, y_train)
# OneVsRestClassifier(estimator=LogisticRegression(C=1.0, class_weight=None,
# dual=False, fit_intercept=True,
# intercept_scaling=1, max_iter=100, multi_class='ovr',
# n_jobs=None, penalty='l2', random_state=None, solver='liblinear',
# tol=0.0001, verbose=0, warm_start=False),
# n_jobs=None)
ovr.score(x_test, y_test)
# 0.9473684210526315
from sklearn.multiclass import OneVsOneClassifier
ovo = OneVsOneClassifier(log_reg)
ovo.fit(x_train, y_train)
# OneVsOneClassifier(estimator=LogisticRegression(C=1.0, class_weight=None,
# dual=False, fit_intercept=True,
# intercept_scaling=1, max_iter=100, multi_class='multinomial',
# n_jobs=None, penalty='l2', random_state=None, solver='newton-cg',
# tol=0.0001, verbose=0, warm_start=False),
# n_jobs=None)