機器學習——從線性迴歸到邏輯迴歸【附詳細推導和代碼】

本文始發於個人公衆號:TechFlow,原創不易,求個關注


在之前的文章當中,我們推導了線性迴歸的公式,線性迴歸本質是線性函數,模型的原理不難,核心是求解模型參數的過程。通過對線性迴歸的推導和學習,我們基本上了解了機器學習模型學習的過程,這是機器學習的精髓,要比單個模型的原理重要得多。

新關注和有所遺忘的同學可以點擊下方的鏈接回顧一下之前的線性迴歸和梯度下降的內容。

一文講透梯度下降法

詳細推導線性迴歸模型


迴歸與分類


在機器學習當中,模型根據預測結果的不同分爲兩類,如果我們希望模型預測一個或者多個連續值,這類問題被稱爲是迴歸問題。像是常見的未來股票價格的估計、未來溫度估計等等都算是迴歸問題。還有一類呢是分類問題,模型的預測結果是一個離散值,也就是說只有固定的種類的結果。常見的有垃圾郵件識別、網頁圖片鑑黃等等。

我們之前介紹的邏輯迴歸顧名思義是一個迴歸問題,今天的文章講的呢是如何將這個迴歸模型轉化成分類模型,這個由線性迴歸推導得到的分類模型稱爲邏輯迴歸


邏輯迴歸


邏輯迴歸這個模型很神奇,雖然它的本質也是迴歸,但是它是一個分類模型,並且它的名字當中又包含”迴歸“兩個字,未免讓人覺得莫名其妙。

如果是初學者,覺得頭暈是正常的,沒關係,讓我們一點點捋清楚。

讓我們先回到線性迴歸,我們都知道,線性迴歸當中y=WX+by=WX+b。我們通過W和b可以求出X對應的y,這裏的y是一個連續值,是迴歸模型對吧。但如果我們希望這個模型來做分類呢,應該怎麼辦?很容易想到,我們可以人爲地設置閾值對吧,比如我們規定y > 0最後的分類是1,y < 0最後的分類是0。從表面上來看,這當然是可以的,但實際上這樣操作會有很多問題。

最大的問題在於如果我們簡單地設計一個閾值來做判斷,那麼會導致最後的y是一個分段函數,而分段函數不連續,使得我們沒有辦法對它求梯度,爲了解決這個問題,我們得找到一個平滑的函數使得既可以用來做分類,又可以解決梯度的問題。

很快,信息學家們找到了這樣一個函數,它就是Sigmoid函數,它的表達式是:

σ(x)=11+ex\sigma(x)= \frac{1}{1+e^{-x}}

它的函數圖像如下:

可以看到,sigmoid函數在x=0處取值0.5,在正無窮處極限是1,在負無窮處極限是0,並且函數連續,處處可導。sigmoid的函數值的取值範圍是0-1,非常適合用來反映一個事物發生的概率。我們認爲σ(x)\sigma(x)表示x發生的概率,那麼x不發生的概率就是1σ(x)1-\sigma(x)。我們把發生和不發生看成是兩個類別,那麼sigmoid函數就轉化成了分類函數,如果σ(x)>0.5\sigma(x)>0.5表示類別1,否則表示類別0.

到這裏就很簡單了,通過線性迴歸我們可以得到y=WX+b,P(1)=σ(y),P(0)=1σ(y)y = WX + b, P(1)=\sigma(y), P(0)=1-\sigma(y)。也就是說我們在線性迴歸模型的外面套了一層sigmoid函數,我們通過計算出不同的y,從而獲得不同的概率,最後得到不同的分類結果。


損失函數


下面的推導全程高能,我相信你們看完會三連的(點贊、轉發、關注)。

讓我們開始吧,我們先來確定一下符號,爲了區分,我們把訓練樣本當中的真實分類命名爲yyyy的矩陣寫成YY。同樣,單條樣本寫成xx,xx的矩陣寫成XX。單條預測的結果寫成y^\hat{y},所有的預測結果寫成Y^\hat{Y}

對於單條樣本來說,y有兩個取值,可能是1,也可能是0,1和0代表兩個不同的分類。我們希望y=1y = 1的時候,y^\hat{y}儘量大,y=0y = 0時,1y^1 - \hat{y}儘量大,也就是y^\hat{y}儘量小,因爲它取值在0-1之間。我們用一個式子來統一這兩種情況:

P(yx)=y^y(1y^)1yP(y|x)=\hat{y}^y(1-\hat{y})^{1-y}

我們代入一下,y=0y=0時前項爲1,表達式就只剩下後項,同理,y=1y=1時,後項爲1,只剩下前項。所以這個式子就可以表示預測準確的概率,我們希望這個概率儘量大。顯然,P(yx)>0P(y|x)>0,所以我們可以對它求對數,因爲log函數是單調的。所以P(yx)P(y|x)取最值時的取值,就是logP(yx)logP(y|x)取最值的取值。

logP(yx)=ylogy^+(1y)log(1y^)\log P(y|x)=y\log \hat{y} + (1-y)\log (1-\hat{y})

我們期望這個值最大,也就是期望它的相反數最小,我們令J=logP(yx)J=-\log P(y|x),這樣就得到了它的損失函數:

J(θ)=1mYlogY^+(1Y)log(1Y^)J(\theta)=-\frac{1}{m}Y\log \hat{Y} + (1-Y)\log (1-\hat{Y})

如果知道交叉熵這個概念的同學,會發現這個損失函數的表達式其實就是交叉熵。交叉熵是用來衡量兩個概率分佈之間的”距離“,交叉熵越小說明兩個概率分佈越接近,所以經常被用來當做分類模型的損失函數。關於交叉熵的概念我們這裏不多贅述,會在之後文章當中詳細介紹。我們隨手推導的損失函數剛好就是交叉熵,這並不是巧合,其實底層是有一套信息論的數學邏輯支撐的,我們不多做延伸,感興趣的同學可以瞭解一下。


硬核推導


損失函數有了,接下來就是求梯度來實現梯度下降了。

這個函數看起來非常複雜,要對它直接求偏導過於硬核(危),如果是許久不碰高數的同學直接肝不亞於硬抗葦名一心。

爲了簡化難度,我們先來做一些準備工作。首先,我們先來看下σ\sigma函數,它本身的形式很複雜,我們先把它的導數搞定。

(11+ex)=ex(1+ex)2=1+ex1(1+ex)2=11+ex(111+ex)=σ(x)(1σ(x)) \begin{aligned} (\frac{1}{1+e^{-x}})'&=\frac{e^{-x}}{(1+e^{-x})^2} \\ &=\frac{1+e^{-x}-1}{(1+e^{-x})^2} \\ &=\frac{1}{1+e^{-x}}\cdot (1-\frac{1}{1+e^{-x}}) \\ &=\sigma(x) (1-\sigma(x)) \end{aligned}

因爲y^=σ(θX)\hat{y}=\sigma(\theta X),我們將它帶入損失函數,可以得到,其中σ(θ)=σ(θX)\sigma(\theta)=\sigma(\theta X)

J(θ)=1mYlogσ(θ)+(1Y)log(1σ(θ))J(\theta)=-\frac{1}{m}Y\log \sigma(\theta) + (1 - Y)\log( 1- \sigma(\theta))

接着我們求J(θ)J(\theta)θ\theta的偏導,這裏要代入上面對σ(x)\sigma(x)求導的結論:

θJ(θ)=1m(Y1σ(θ)σ(θ)+(1Y)11σ(θ)σ(θ)(1))=1m(Y1σ(θ)σ(θ)(1σ(θ))X(1Y)11σ(θ)σ(θ)(1σ(θ))X)=1m(Y(1σ(θ))σ(θ)+Yσ(θ))X=1m(σ(θ)Y)X=1m(Y^Y)X \begin{aligned} \frac{\partial}{\partial \theta}J(\theta)&=-\frac{1}{m}(Y\cdot \frac{1}{\sigma(\theta)}\cdot \sigma'(\theta)+(1-Y)\cdot \frac{1}{1-\sigma(\theta)}\cdot \sigma'(\theta)\cdot(-1)) \\ &=-\frac{1}{m}(Y\cdot \frac{1}{\sigma(\theta)}\cdot \sigma(\theta)(1-\sigma(\theta))\cdot X-(1-Y)\cdot \frac{1}{1-\sigma(\theta)}\cdot \sigma(\theta)(1-\sigma(\theta))\cdot X) \\ &=-\frac{1}{m}(Y(1-\sigma(\theta)) - \sigma(\theta)+Y\cdot \sigma(\theta))\cdot X\\ &=-\frac{1}{m}(\sigma(\theta) - Y)\cdot X\\ &=-\frac{1}{m}(\hat{Y} - Y)\cdot X \end{aligned}


代碼實戰


梯度的公式都推出來了,離寫代碼實現還遠嗎?

不過巧婦難爲無米之炊,在我們擼模型之前,我們先試着造一批數據。

我們選擇生活中一個很簡單的場景——考試。假設每個學生需要參加兩門考試,兩門考試的成績相加得到最終成績,我們有一批學生是否合格的數據。希望設計一個邏輯迴歸模型,幫助我們直接計算學生是否合格。

爲了防止sigmoid函數產生偏差,我們把每門課的成績縮放到(0, 1)的區間內。兩門課成績相加超過140分就認爲總體及格。

import numpy as np
def load_data():
    X1 = np.random.rand(50, 1)/2 + 0.5
    X2 = np.random.rand(50, 1)/2 + 0.5
    y = X1 + X2
    Y = y > 1.4
    Y = Y + 0
    x = np.c_[np.ones(len(Y)), X1, X2]
    return x, Y

這樣得到的訓練數據有兩個特徵,分別是學生兩門課的成績,還有一個偏移量1,用來記錄常數的偏移量。

接着,根據上文當中的公式,我們不難(真的不難)實現sigmoid以及梯度下降的函數。

def sigmoid(x):
    return 1.0/(1+np.exp(-x))
    
def gradAscent(data, classLabels):
    X = np.mat(data)
    Y = np.mat(classLabels)
    m, n = np.shape(dataMat)
    # 學習率
    alpha = 0.01
    iterations = 10000
    wei = np.ones((n, 1))
    for i in range(iterations):
        y_hat = sigmoid(X * wei)
        error = Y - y_hat
        wei = wei + alpha * X.transpose()*error
    return wei

這段函數實現的是批量梯度下降,對Numpy熟悉的同學可以看得出來,這就是在直接套公式。

最後,我們把數據集以及邏輯迴歸的分割線繪製出來。

import matplotlib.pyplot as plt
def plotPic():
    dataMat, labelMat = load_data()
    #獲取梯度
    weis = gradAscent(dataMat, labelMat).getA()
    n = np.shape(dataMat)[0]
    xc1 = []
    yc1 = []
    xc2 = []
    yc2 = []
    #數據按照正負類別劃分
    for i in range(n):
        if int(labelMat[i]) == 1:
            xc1.append(dataMat[i][1])
            yc1.append(dataMat[i][2])
        else:
            xc2.append(dataMat[i][1])
            yc2.append(dataMat[i][2])
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(xc1, yc1, s=30, c='red', marker='s')
    ax.scatter(xc2, yc2, s=30, c='green')
    #繪製分割線
    x = np.arange(0.5, 1, 0.1)
    y = (-weis[0]- weis[1]*x)/weis[2]
    ax.plot(x, y)
    plt.xlabel('X1')
    plt.ylabel('X2')
    plt.show()

最後得到的結果如下:


隨機梯度下降版本


可以發現,經過了1萬次的迭代,我們得到的模型已經可以正確識別所有的樣本了。

我們剛剛實現的是全量梯度下降算法,我們還可以利用隨機梯度下降來進行優化。優化也非常簡單,我們計算梯度的時候不再是針對全量的數據,而是從數據集中選擇一條進行梯度計算。

基本上可以複用梯度下降的代碼,只需要對樣本選取的部分加入優化。

def stocGradAscent(data, classLab, iter_cnt):
    X = np.mat(data)
    Y = np.mat(classLab)
    m, n = np.shape(dataMat)
    weis = np.ones((n, 1))
    for j in range(iter_cnt):
        for i in range(m):
            # 使用反比例函數優化學習率
            alpha = 4 / (1.0 + j + i) + 0.01
            randIdx = int(np.random.uniform(0, m))
            y_hat = sigmoid(np.sum(X[randIdx] * weis))
            error = Y[randIdx] - y_hat
            weis = weis + alpha * X[randIdx].transpose()*error
    return weis

我們設置迭代次數爲2000,最後得到的分隔圖像結果如下:

當然上面的代碼並不完美,只是一個簡單的demo,還有很多改進和優化的空間。只是作爲一個例子,讓大家直觀感受一下:其實自己親手寫模型並不難,公式的推導也很有意思。這也是爲什麼我會設置高數專題的原因。CS的很多知識也是想通的,在學習的過程當中靈感迸發旁徵博引真的是非常有樂趣的事情,希望大家也都能找到自己的樂趣。

今天的文章就是這些,如果覺得有所收穫,請順手掃碼點個關注吧,你們的舉手之勞對我來說很重要。

發佈了51 篇原創文章 · 獲贊 0 · 訪問量 3457
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章