手擼機器學習演算法 - 邏輯迴歸

JesusBilly發表於2021-06-24

系列文章目錄:

演算法介紹

今天我們一起來學習使用非常廣泛的分類演算法:邏輯迴歸,是的,你沒有看錯,雖然它名字裡有迴歸,但是它確實是個分類演算法,作為除了感知機以外,最最最簡單的分類演算法,下面我們把它與感知機對比來進行學習;

從決策邊界上看

  • 感知機:決策邊界就是類別的分界線,處於錯誤一側的點即為分類錯誤點;
  • 邏輯迴歸:決策邊界表示分為正類和負類均為50%,資料點被分為正類的概率直觀上由其到決策邊界的距離決定;

以上,對於資料中的噪聲,假設噪聲點實際為負類,但是被分到正類一側,如果是感知機,則無法判斷,而邏輯迴歸以概率為基礎,如果該噪聲點實際被分為正類的概率僅為52%,那麼實際上它屬於負類的可能性也很大,即邏輯迴歸認為資料的產生是有一定隨機性的,相比於簡單的0或1,概率值更能表現其實際情況;

輸出函式

從決策邊界可知,感知機的輸出∈{+1,-1},而邏輯迴歸的輸出為0~1的概率值:

  • 感知機使用sign作為輸出函式:sign(wx+b)
  • 邏輯迴歸使用sigmoid作為概率輸出函式:sigmoid(wx+b),sigmoid=1/(1+e^-z),這裡z=wx+b,可以看到當z=0時,也就是處於決策邊界時,此時sigmoid= 0.5,也就是50%,除此之外,z越大,sigmoid輸出越大,可以認為越有可能是正類,反之即為負類,且可以通過極限推導sigmoid區間為(0,1);

如何看待邏輯迴歸選擇Sigmoid作為概率輸出函式呢,可以從以下幾個點來理解:

  • sigmoid自身性質
    1. 輸入是整個實數域,輸出是(0,1),輸出不包含0和1,使得對於機器學習僅使用整體的一部分作為樣本進行訓練的場景,很適合處理未出現在樣本集中的類別;
    2. 影像曲線對於兩側極值不敏感,二分類的輸出符合伯努利分佈,輸入一般認為符合正態分佈,影像曲線也符合這一點;
    3. 易於求導,sigmoid函式的導數為S(wx+b)*(1-S(wx+b)),引數優化過程基本就是求導過程,因此易於求導很重要;
  • 貝葉斯概率推導
    1. sigmoid函式可以由伯努利、正態分佈+貝葉斯全概率公式推導得到;

損失函式

  • 感知機:\(yi*sign(w*xi+b)\),yi∈{-1,+1},模型分類正確返回值為+1,錯誤返回值為-1,對所有樣本進行求和即可得到score值;
  • 邏輯迴歸:\(ln(1+e^{-(yi*wxi)})\),yi∈{-1,+1},模型分類正確返回值>=0,錯誤返回值<0,負數絕對值越大,表示錯誤越嚴重,對所有樣本計算該誤差加起來求平均即為邏輯迴歸的誤差函式;

演算法推導

從概率角度看Sigmoid

Sigmoid給出了條件概率:

  • 正類的分佈:\(P(Y=1|X) = Sigmoid(wx+b) = \frac{1}{1+e^{-(wx+b)}}\),其中x~P(X),y~P(Y);
  • 負類的分佈:\(P(Y=-1|X) = 1-Sigmoid(wx+b) = 1-\frac{1}{1+e^{-(wx+b)}}\)

函式優化求導

首先列出損失函式如下:

\[\frac{1}{N}\sum_{i=1}^N\ln(1+e^{-y_iwx_i}), yi\in{\{+1,-1\}} \]

對上述函式針對引數w求梯度:

\[\begin{equation*} \begin{split} \frac{\partial \frac{1}{N}\sum_{i=1}^{N}ln(1+e^{-y_iwx_i})}{\partial w} = \frac{1}{N}\sum_{i=1}^{N}[\frac{(-y_ix_i)(e^{-y_iwx_i})}{1+e^{-y_iwx_i}}] \end{split} \end{equation*} \]

上述梯度就是後續我們用於更新引數w的依據;

程式碼實現

初始化相關引數

def __init__(self,X,y,epochs=5000,eta=0.1,epsilon=0.001):
    super(LogisticRegression,self).__init__(X,y)
    self.epochs = epochs
    self.eta = eta
    self.epsilon = epsilon
    self.wk = np.array([0 for i in range(self.X.shape[1])])

Sigmoid

def sigmoid(self,x):
    return 1/(1+np.exp(-x))

經驗損失函式梯度

def drhd(self,w):
    '''
    經驗誤差函式的梯度
    '''
    ew = []
    for i in range(self.X.shape[1]):
        ewi = np.mean(-self.y*self.X[:, i]*np.exp(-self.y*(self.X@w))/(1+np.exp(-self.y*(self.X@w))))
        ew.append(ewi)
    return np.array(ew)

迭代訓練

def train(self):
    i_,norm = None,None
    for i in range(self.epochs):
        drhdwk = self.drhd(self.wk)
        i_,norm = i,np.linalg.norm(drhdwk)
        if np.linalg.norm(drhdwk) < self.epsilon:
            break
        self.wk = self.wk-self.eta*drhdwk
    return i_,norm,self.wk

執行結果

先看下感知機-口袋演算法處理非線性分類問題:

再來對比看下邏輯迴歸的分類情況:

直覺上看,二者雖然都有一個點分類錯誤(這是肯定的,因為資料不是線性可分的),但是對於分類錯誤的×來說,邏輯迴歸中錯誤的×距離分割平面更近,也就是說模型對於這個點的判斷是比較模糊而不是很肯定的,可以認為是錯的不嚴重,這一點也會反應在損失函式中;

全部程式碼

import numpy as np
from 線性迴歸最小二乘法矩陣實現 import LinearRegression as LR
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt

class LogisticRegression(LR):
    def __init__(self,X,y,epochs=5000,eta=0.1,epsilon=0.001):
        super(LogisticRegression,self).__init__(X,y)
        self.epochs = epochs
        self.eta = eta
        self.epsilon = epsilon
        self.wk = np.array([0 for i in range(self.X.shape[1])])

    def sigmoid(self,x):
        return 1/(1+np.exp(-x))

    def h(self,x):
        '''
        假設函式
        '''
        return self.sigmoid(x@self.wk.T)

    def drhd(self,w):
        '''
        經驗誤差函式的梯度
        '''
        ew = []
        for i in range(self.X.shape[1]):
            ewi = np.mean(-self.y*self.X[:, i]*np.exp(-self.y*(self.X@w))/(1+np.exp(-self.y*(self.X@w))))
            ew.append(ewi)
        return np.array(ew)

    def train(self):
        i_,norm = None,None
        for i in range(self.epochs):
            drhdwk = self.drhd(self.wk)
            i_,norm = i,np.linalg.norm(drhdwk)
            if np.linalg.norm(drhdwk) < self.epsilon:
                break
            self.wk = self.wk-self.eta*drhdwk
        return i_,norm,self.wk

    def sign(self,value):
        return 1 if value>=0 else -1

    def predict(self,x):
        return self.sign(self.wk.dot(np.append([1],x)))

if __name__ == '__main__':
    X = np.array([[5,2], [3,2], [2,7], [1,4], [6,1], [4,5], [2,4.5]])
    y = np.array([-1, -1, 1, 1, -1, 1, -1, ])
    # X = np.array([[5,2], [3,2], [2,7], [1,4], [6,1], [4,5]])
    # y = np.array([-1, -1, 1, 1, -1, 1, ])
    iris = load_iris()
    X = iris.data[iris.target<2,:2]
    y = iris.target[iris.target<2]
    y[y==0] = -1

    model = LogisticRegression(X=X,y=y,epochs=10000,eta=.2,epsilon=0.0001)
    i,norm,w = model.train()
    print(f"epochs={i} -> w={w} -> norm={norm:>.8f}")
    for xi,yi in zip(X,y):
        print(yi,model.predict(xi))

    w,b = w[1:],w[0]
    positive = [x for x,y in zip(X,y) if y==1]
    negative = [x for x,y in zip(X,y) if y==-1]
    line = [(-w[0]*x-b)/w[1] for x in [-100,100]]
    plt.title('w='+str(w)+', b='+str(b))
    plt.scatter([x[0] for x in positive],[x[1] for x in positive],c='green',marker='o')
    plt.scatter([x[0] for x in negative],[x[1] for x in negative],c='red',marker='x')
    plt.plot([-100,100],line,c='black')
    plt.xlim(min([x[0] for x in X])-1,max([x[0] for x in X])+1)
    plt.ylim(min([x[1] for x in X])-1,max([x[1] for x in X])+1)
    plt.show()

    iris = load_iris()
    X = iris.data
    y = iris.target
    model = LogisticRegression(X=X,y=y,epochs=1000000,eta=.1,epsilon=0.0005)
    i,norm,w = model.train()
    print(f"epochs={i} -> w={w} -> norm={norm:>.8f}")

最後

邏輯迴歸幾乎是機器學習中應用最為廣泛的一種分類演算法,由於其簡單的思想、良好的數學理論、超強的可解釋性,使得在推薦領域、金融領域等發揮了巨大的作用;

相關文章