机器学习之逻辑回归(Logistic Regression)与决策边界详解与实现

逻辑回归与决策边界

what?

逻辑回归其实是一个分类算法而不是回归算法。通常是利用已知的自变量来预测一个离散型因变量的值(像二进制值0/1,是/否,真/假)。简单来说,它就是通过拟合一个逻辑函数(logit fuction)来预测一个事件发生的概率。所以它预测的是一个概率值,自然,它的输出值应该在0到1之间。

Logistic回归简单分析:

优点:计算代价不高,易于理解和实现
  缺点:容易欠拟合,分类精度可能不高
  适用数据类型:数值型和标称型数据

基本原理

按照我自己的理解,可以简单的描述为这样的过程:

  1. 找一个合适的预测函数,一般表示为h函数,该函数就是我们需要找的分类函数,它用来预测输入数据的判断结果。这个过程时非常关键的,需要对数据有一定的了解或分析,知道或者猜测预测函数的“大概”形式,比如是线性函数还是非线性函数。
    借助sigmoid函数构造出的预测函数形式一般为:

    h θ ( x ) = g ( θ T x ) = 1 1 + e θ T x

    其中sigmoid函数为:
    sigmoid

  2. 构造一个Cost函数(损失函数),该函数表示预测的输出(h)与训练数据类别(y)之间的偏差,可以是二者之间的差(h-y)或者是其他的形式。cost函数为:

    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求和或者求平均,记为J(θ)函数,表示所有训练数据预测值与实际类别的偏差。J(θ函数一般为:
    J ( θ ) = 1 m [ i = 1 m ( y i l o g h θ ( x i ) + ( 1 y i ) l o g ( 1 h θ ( x i ) ) ) ]

  3. 显然,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}点乘,· θ T >0,p>0.5,· θ T <0,p<0.5,这样就能实类别的分类。当· θ T =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方法对上述数据的决策边界就是不规则的
因为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()

得到的结果如下:
KNN-3

可以看出分类的结果是非常的不规则的,其实也就是应该是发生了过拟合的问题。
对于KNeighborsClassifier()这个函数其实其中有一个参数是可以调节的,就是n_neighbors这个参数,可以直接运行 knn_clf_all = KNeighborsClassifier()查看其中的参数,n_neighbors这个参数的含义其实就是分类的复杂程度,越小的话越复杂,就容易出现过拟合的问题。这里调节下这个参数看一下效果,设置knn_clf_all = KNeighborsClassifier(n_neighbors = 50),其余代码相同
得到的结果如下:
KNN-n_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)))

得到如下的结果:
非线性LR

可以看出添加了多项式的逻辑回归可以解决非线性可分的问题。

逻辑回归中使用正则化处理过拟合的问题

因为数据线性不可分的时候,需要在逻辑回归中引入多项式,这也使得分类变得复杂,容桂产生过拟合的问题,解决方法有两个,一个是调节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()

说实话这里选取的数据集的代表性不太够,没有太突出正则化的优点,注重点在方法的实现上,但是还是能看出有一点区别的,决策边界更加清楚了。

猜你喜欢

转载自blog.csdn.net/WilsonSong1024/article/details/81747813