逻辑回归与决策边界
what?
逻辑回归其实是一个分类算法而不是回归算法。通常是利用已知的自变量来预测一个离散型因变量的值(像二进制值0/1,是/否,真/假)。简单来说,它就是通过拟合一个逻辑函数(logit fuction)来预测一个事件发生的概率。所以它预测的是一个概率值,自然,它的输出值应该在0到1之间。
Logistic回归简单分析:
优点:计算代价不高,易于理解和实现
缺点:容易欠拟合,分类精度可能不高
适用数据类型:数值型和标称型数据
基本原理
按照我自己的理解,可以简单的描述为这样的过程:
找一个合适的预测函数,一般表示为h函数,该函数就是我们需要找的分类函数,它用来预测输入数据的判断结果。这个过程时非常关键的,需要对数据有一定的了解或分析,知道或者猜测预测函数的“大概”形式,比如是线性函数还是非线性函数。
借助sigmoid函数构造出的预测函数形式一般为:
其中sigmoid函数为:
构造一个Cost函数(损失函数),该函数表示预测的输出(h)与训练数据类别(y)之间的偏差,可以是二者之间的差(h-y)或者是其他的形式。cost函数为:
综合考虑所有训练数据的“损失”,将Cost求和或者求平均,记为J(θ)函数,表示所有训练数据预测值与实际类别的偏差。J(θ函数一般为:
显然,J(θ)函数的值越小表示预测函数越准确(即h函数越准确),所以这一步需要做的是找到J(θ)函数的最小值。找函数的最小值有不同的方法,Logistic Regression实现时用的是梯度下降法(Gradient Descent)。
关于详细的公式推导就不介绍了伪代码
初始化线性函数参数为1 构造sigmoid函数 重复循环I次 计算数据集梯度 更新线性函数参数 确定最终的sigmoid函数 输入训练(测试)数据集 运用最终sigmoid函数求解分类
代码实现
逻辑回归的python代码:
import numpy as np from sklearn.metrics import accuracy_score class LogisticRegression: def __init__(self): """初始化Logistic Regression模型""" self.coef_ = None self.intercept_ = None self._theta = None def _sigmoid(self, t): return 1.0 / (1.0 + np.exp(-t)) def fit(self, X_train, y_train, eta=0.01, n_iters=1e4): """根据训练数据集X_train, y_train, 使用梯度下降法训练Logictic Regression模型""" 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, initial_theta, eta, n_iters=1e4, epsilon=1e-8): theta = initial_theta cur_iter = 0 while cur_iter < n_iters: 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_iters) self.intercept_ = self._theta[0] self.coef_ = self._theta[1:] return self def predict_proba(self, X_predict): """给定待预测数据集X_predict,返回表示X_predict的结果概率向量""" assert self.intercept_ is not None and self.coef_ is not None, \ "must fit before predict!" assert X_predict.shape[1] == len(self.coef_), \ "the feature number of X_predict must be equal to X_train" 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): """给定待预测数据集X_predict,返回表示X_predict的结果向量""" assert self.intercept_ is not None and self.coef_ is not None, \ "must fit before predict!" assert X_predict.shape[1] == len(self.coef_), \ "the feature number of X_predict must be equal to X_train" proba = self.predict_proba(X_predict) return np.array(proba >= 0.5, dtype='int') def score(self, X_test, y_test): """根据测试数据集 X_test 和 y_test 确定当前模型的准确度""" y_predict = self.predict(X_test) ''''分类的准确度''' return accuracy_score(y_test, y_predict) def __repr__(self): return "LogisticRegression()
上述结果为本次测试数据,可以看出来该数据集其实是三维的数据,因为逻辑回归只能解决二分类的问题,因此取数据集中的前两维数据,作为两种类别,因此使用该数据集可以用来判断逻辑回归测试结果的好坏
测试代码:
from playML.model_selection import train_test_split
from playML.LogisticRegression import LogisticRegression
x_train, x_test, y_train, y_test = train_test_split(x, y, seed=666)
log_reg = LogisticRegression()
log_reg.fit(x_train, y_train)
log_reg.score(x_test, y_test)
log_reg.predict_proba(x_test)
输出的测试结果:
array([0.92972035, 0.98664939, 0.14852024, 0.17601199, 0.0369836 ,0.0186637 , 0.04936918, 0.99669244, 0.97993941, 0.74524655,0.04473194, 0.00339285, 0.26131273, 0.0369836 , 0.84192923,0.79892262, 0.82890209, 0.32358166, 0.06535323, 0.20735334])
对于上述简单的测试数据,输出的array中的数据表示将某个数据分类成某一类别的概率,越接近于0就越趋近于分类成0这个类别,同理越趋近与1,就越趋近于分类成1,最后的分类的测试值输出为1.
决策边界
怎么对新输入的数据进行预测分类呢?
每输入一个值,与\theta ^{T}点乘,·
>0,p>0.5,·
<0,p<0.5,这样就能实类别的分类。当·
=0就称为该分类的决策边界。
然后使用上面简单的数据集,绘制决策边界,其实也就是分类的边界,当有新的数据的时候骡子坳那边就分类为该类别。
def x2(x1):
return (-log_reg.coef_[0] * x1 - log_reg.intercept_) / 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()
得到的结果如下:
上述的决策边界是一条直线,所以不严格的说还是属于线性分类,当分类数据不线性的时候就需要不规则的决策边界。
举个例子使用KNN算法来对上述数据进行分类:
from sklearn.neighbors import KNeighborsClassifier
knn_clf = KNeighborsClassifier()
knn_clf.fit(x_train, y_train)
knn_clf.score(x_test, y_test)
plot_decision_boundary(knn_clf, axis=[4, 7.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.show()
结果如下:
可以看出通过使用KNN方法对上述数据的决策边界就是不规则的
因为KNN是支持多类别数据的分类的,然后我们的数据集也是3中类别的,所以测试下KNN在三分类中的分类的效果。
knn_clf_all = KNeighborsClassifier()
knn_clf_all.fit(iris.data[:,:2], iris.target)
# 欠拟合
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()
得到的结果如下:
可以看出分类的结果是非常的不规则的,其实也就是应该是发生了过拟合的问题。
对于KNeighborsClassifier()这个函数其实其中有一个参数是可以调节的,就是n_neighbors这个参数,可以直接运行 knn_clf_all = KNeighborsClassifier()查看其中的参数,n_neighbors这个参数的含义其实就是分类的复杂程度,越小的话越复杂,就容易出现过拟合的问题。这里调节下这个参数看一下效果,设置knn_clf_all = KNeighborsClassifier(n_neighbors = 50),其余代码相同
得到的结果如下:
明显能够看出来决策边界规则了许多,但是相应的分类效果弱了一些,所以调参,调参。
上面的数据集是可以线性分类的,当数据的类别线性不可分的时候,逻辑回归的方式怎么去处理呢?举个例子:
当数据集是这样的,显然线性不可分,决策边界是不规则类似于圆。其实这个时候就需要类似于使用多项式回归的方式来处理。给逻辑回归中添加多项式。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import StandardScaler
from playML.LogisticRegression import LogisticRegression
log_reg = 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)
#多项式
def PolynomialLogisticRegression(degree):
return Pipeline([
('poly', PolynomialFeatures(degree=degree)), #多项式参数
('std_scaler', StandardScaler()), #标准化(归一化)
('log_reg', LogisticRegression()) #逻辑回归对象
])
poly_log_reg = PolynomialLogisticRegression(degree=2)
poly_log_reg.fit(X, y)
plot_decision_boundary(poly_log_reg, [-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()
print("准确度:" + str(poly_log_reg.score(X, y)))
得到如下的结果:
可以看出添加了多项式的逻辑回归可以解决非线性可分的问题。
逻辑回归中使用正则化处理过拟合的问题
因为数据线性不可分的时候,需要在逻辑回归中引入多项式,这也使得分类变得复杂,容桂产生过拟合的问题,解决方法有两个,一个是调节degree参数,另一种就是正则化。通用的正则化的方式就是在J(θ)函数中加一个正则项,使用J(θ)+aL2作为新的损失函数。a用来调节J(θ)和L2各自所占比重。这里C·J(θ)+L1作为所示函数,其实C也是用来平衡J(θ)和L1,原理是一样的。L1和L2是正则化中的一个重要的参数。
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
def PolynomialLogisticRegression2(degree, C, penalty='l2'):
return Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('std_scaler', StandardScaler()),
('log_reg', LogisticRegression(C=C, penalty=penalty))
])
poly_log_reg2 = PolynomialLogisticRegression2(degree=10, C=13, penalty='l1')
poly_log_reg2.fit(X_train, y_train)
plot_decision_boundary(poly_log_reg2, [-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()
说实话这里选取的数据集的代表性不太够,没有太突出正则化的优点,注重点在方法的实现上,但是还是能看出有一点区别的,决策边界更加清楚了。