【DL筆記3】一步步親手用python實現Logistic Regression

從【DL筆記1】到【DL筆記N】,是我學習深度學習一路上的點點滴滴的記錄,是從Coursera網課、各大博客、論文的學習以及自己的實踐中總結而來。從基本的概念、原理、公式,到用生動形象的例子去理解,到動手做實驗去感知,到著名案例的學習,到用所學來實現自己的小而有趣的想法......我相信,一路看下來,我們可以感受到深度學習的無窮的樂趣,並有興趣和激情繼續鑽研學習。 正所謂 Learning by teaching,寫下一篇篇筆記的同時,我也收穫了更多深刻的體會,希望大家可以和我一同進步,共同享受AI無窮的樂趣。


前面的【DL筆記1】和【DL筆記2】講解了Logistic regression的基本原理,並且我提到過這個玩意兒在我看來是學習神經網絡和深度學習的基礎,學到後面就發現,其實只要這個東西弄清楚了,後面的就很好明白。 另外,雖然說現在有很多很多的機器學習包和深度學習框架,像sklearn、TensorFlow、Keras等等,讓我們實現一個神經網絡十分容易,但是如果你不瞭解原理,即使給你一個框架,裏面的大量的函數和方法你依然不知道如何下手,不知道什麼時候該使用什麼,而這些框架裏面經常提到的“前向傳播”、“反向傳播”、“計算圖”、各種梯度下降、mini-batch、各種initialization方法等等你也難以理解,更別提如何針對你的實際場景在對症下藥了。 因此,我的深度學習系列筆記,主要是講解神經網絡的思路、算法、原理,然後前期主要使用python和numpy來實現,只有到我們把神經網絡基本講完,纔會開始使用諸如TensorFlow這樣的框架來實現。當然,這也是我正在聽的吳恩達的深度學習系列課程的特點,不急不躁,耐心地用最樸素的方法來實踐所有的原理,這樣才能融會貫通,玩轉各種框架。

這次的前言有點囉嗦了。。。主要是怕有的讀者說“明明可以用機器學習包幾行代碼搞定,爲啥偏要用純python費勁去實現”。 好了,進入正題:

用python實現Logistic Regression

一、算法搭建步驟

(一)數據預處理

  • 搞清楚數據的形狀、維度
  • 將數據(例如圖片)轉化成向量(image to vector)方便處理
  • 將數據標準化(standardize),這樣更好訓練

(二)構造各種輔助函數

  • 激活函數(此處我們使用sigmoid函數)—activation function
  • 參數初始化函數(用來初始化W和b)—initialization
  • 傳播函數(這裏是用來求損失cost並對W、b求導,即dW、db)—propagate
  • 優化函數(迭代更新W和b,來最小化cost)—optimize
  • 預測函數(根據學習到的W和b來進行預測)—predict

(三)綜合上面的輔助函數,結合成一個模型

  • 可以直接輸入訓練集、預測集、超參數,然後給出模型參數和準確率

上面這麼多輔助函數可能看的讓人有點懵逼,因此我花了半小時在PowerPoint裏面畫了這個圖(ヾノ꒪ཫ꒪),以便更清楚地說明它們之間的關係:

構造輔助函數(helper function)是爲了讓我們的結構更清晰,更容易調試和修改。下面我們按照上面的步驟一個一個來。

二、開始編程吧

下面我們採用“展示代碼和註釋+重點地方詳解”的方式來一步步實現:

(一)數據導入和預處理

# 導入數據,“_orig”代表這裏是原始數據,我們還要進一步處理才能使用:
train_set_x_orig, train_set_y, test_set_x_orig, test_set_y, classes = load_dataset()
#由數據集獲取一些基本參數,如訓練樣本數m,圖片大小:
m_train = train_set_x_orig.shape[0]  #訓練集大小209
m_test = test_set_x_orig.shape[0]    #測試集大小209
num_px = train_set_x_orig.shape[1]  #圖片寬度64,大小是64×64
#將圖片數據向量化(扁平化):
train_set_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0],-1).T
test_set_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0],-1).T
#對數據進行標準化:
train_set_x = train_set_x_flatten/255.
test_set_x = test_set_x_flatten/255.

上面的代碼有幾點要說明:

  1. 數據導入是直接用吳恩達網課中的數據集,他提供了一個接口load_dataset()可以直接導入數據,如果需要數據的話可以在文章下方留言獲取。這裏主要是展示方法,完全可以用自己的數據集來操作。 數據集是一些圖片,我們要訓練一個識別貓的分類器。 train_set_x_orig,也就是我們的原始數據形狀(209, 64, 64, 3)第一維代表m,即樣本數量,第二維第三維分別是圖片的長和寬,第四維代表圖片的RGB三個通道
  2. numpy包有重要的關於矩陣“形狀”的方法:.shape.reshape() .shape可以獲取一個矩陣的形狀,於是我們可以通過[i]來知道每一維的大小; .reshape()用來重構矩陣的形狀,直接在裏面填寫維度即可,還有一些特殊用法,比如此處的用法: 當我們要把一個向量X(m,a,b,c)這個四維向量扁平化成X_flatten(m,a b c)的二維向量,可以寫X_flatten=X.reshape(X.shape[0],-1)即可,其中“-1”代表把剩餘維度壓扁的模式。而代碼中還有一個.T,代表轉置,因爲我們希望把訓練樣本壓縮成(64 64 3,m)的形式。
  3. 爲什麼需要標準化? 在說明爲什麼要標準化前,我們不妨說說一般的標準化是怎麼做的:先求出數據的均值和方差,然後對每一個樣本數據,先減去均值,然後除以方差,也就是(x-μ)/σ2,說白了就是轉化成標準正態分佈!這樣,每個特徵都轉化成了同樣的分佈,不管原來的範圍是什麼,現在都基本限定在同樣的範圍內了。 這樣做的好處是什麼呢?且看下面兩個等高線圖:

上面兩個圖展示了數據在未標準化和標準化之後的情形。原數據的不同特徵的範圍可能會有很大差別,比如一批數據中“年齡”的範圍就比較小,可能20歲 ~ 60歲之間,但是另一個特徵“年收入”可能波動範圍就很大,也許0.5萬 ~ 1000萬,這種情況下回導致我們的等高線圖變得十分“扁平”,在梯度下降的時候會很容易走彎路,因此梯度下降會比較慢,精度也不高。但是經過標準化(也稱歸一化)之後,等高線就變規矩了,就很容易梯度下降了。 另外,對於圖片數據的話,進行標準化很簡單,因爲RGB三個通道的範圍都是255,我們對圖片的處理就是直接除以255即可。

至此,數據預處理就完成了,我們進入下一步:

(二)構建輔助函數們

1. 激活函數/sigmoid函數:

def sigmoid(z):
    a = 1/(1+np.exp(-z))
    return a

就這麼easy,sigmoid的公式就是1/(1+e-x),這裏用np.exp()就可以輕鬆構建。

2. 參數初始化函數(給參數都初始化爲0):

def initialize_with_zeros(dim):
    w = np.zeros((dim,1))
    b = 0
    return w,b

W是一個列向量,傳入維度dim,返回shape爲(dim,1)的W,b就是一個數。 這裏用到的方法是np.zeros(shape).

3.propagate函數: 這裏再次解釋一下這個propagate,它包含了forward-propagate和backward-propagate,即正向傳播和反向傳播。正向傳播求的是cost,反向傳播是從cost的表達式倒推W和b的偏導數,當然我們會先求出Z的偏導數。這兩個方向的傳播也是神經網絡的精髓。 具體倒數怎麼求,這裏就不推導了,就是很簡單的求導嘛,公式請參見上一篇文章:【DL筆記2】神經網絡編程原則&Logistic Regression的算法解析 那麼我就直接上代碼了:

def propagate(w, b, X, Y):
    """
    傳參:
    w -- 權重, shape: (num_px * num_px * 3, 1)
    b -- 偏置項, 一個標量
    X -- 數據集,shape: (num_px * num_px * 3, m),m爲樣本數
    Y -- 真實標籤,shape: (1,m)

    返回值:
    cost, dw ,db,後兩者放在一個字典grads裏
    """
    #獲取樣本數m:
    m = X.shape[1]

    # 前向傳播 :
    A = sigmoid(np.dot(w.T,X)+b)    #調用前面寫的sigmoid函數    
    cost = -(np.sum(Y*np.log(A)+(1-Y)*np.log(1-A)))/m                 

    # 反向傳播:
    dZ = A-Y
    dw = (np.dot(X,dZ.T))/m
    db = (np.sum(dZ))/m

    #返回值:
    grads = {"dw": dw,
             "db": db}

    return grads, cost

這裏需要額外說明的就是,numpy中矩陣的點乘,也就是內積運算,是用np.dot(A,B),它要求前一個矩陣的列數等於後一個矩陣的行數。但矩陣也可以進行元素相乘(element product),就是兩個相同形狀的矩陣對於元素相乘得到一個新的相同形狀的矩陣,可以直接用A * B,或者用np.multiply(A,B)。 上面的代碼中,既有點乘,也有元素相乘,我們在寫的時候,先搞清楚形狀,再確定用什麼乘法。 上面還有各種numpy的數學函數,對矩陣求log就用np.log(),對矩陣元素求和就用np.sum(),賊方便。

4.optimize函數: 有了上面這些函數的加持,optimize函數就很好寫了,就是在迭代中調用各個我們剛剛寫的函數就是:

def optimize(w, b, X, Y, num_iterations, learning_rate, print_cost = False):
    #定義一個costs數組,存放每若干次迭代後的cost,從而可以畫圖看看cost的變化趨勢:
    costs = []
    #進行迭代:
    for i in range(num_iterations):
        # 用propagate計算出每次迭代後的cost和梯度:
        grads, cost = propagate(w,b,X,Y)
        dw = grads["dw"]
        db = grads["db"]

        # 用上面得到的梯度來更新參數:
        w = w - learning_rate*dw
        b = b - learning_rate*db

        # 每100次迭代,保存一個cost看看:
        if i % 100 == 0:
            costs.append(cost)

        # 這個可以不在意,我們可以每100次把cost打印出來看看,從而隨時掌握模型的進展:
        if print_cost and i % 100 == 0:
            print ("Cost after iteration %i: %f" %(i, cost))
    #迭代完畢,將最終的各個參數放進字典,並返回:
    params = {"w": w,
              "b": b}
    grads = {"dw": dw,
             "db": db}
    return params, grads, costs

這個函數就沒什麼好解釋的了。

5.predict函數: 預測就很簡單了,我們已經學到了參數W和b,那麼讓我們的數據經過配備這些參數的模型就可得到預測值。注意,X->Z->激活得到A,此時還並不是預測值,由sigmoid函數我們知道,A的範圍是0~1,但是我們的標籤值是0和1,因此,我們可以設立規則:0.5~1的A對於預測值1,小於0.5的對應預測值0:

def predict(w,b,X):
    m = X.shape[1]
    Y_prediction = np.zeros((1,m))

    A = sigmoid(np.dot(w.T,X)+b)
    for  i in range(m):
        if A[0,i]>0.5:
            Y_prediction[0,i] = 1
        else:
            Y_prediction[0,i] = 0

    return Y_prediction

恭喜,如果你有耐心看到這裏了。。。那。。。我真的忍不住送你一朵fa了:

畢竟我自己都不相信會有幾個人真的去看這麼枯燥的過程。但是我相信,每一份耐心和付出都有回報吧,學習這事兒,急不來。

至此,我們已經構建好了所有的輔助函數。接下來就是結合在一起,然後用我們的數據去訓練、預測了!

(三)結合起來,搭建模型!

def logistic_model(X_train,Y_train,X_test,Y_test,learning_rate=0.1,num_iterations=2000,print_cost=False):
    #獲特徵維度,初始化參數:
    dim = X_train.shape[0]
    W,b = initialize_with_zeros(dim)

    #梯度下降,迭代求出模型參數:
    params,grads,costs = optimize(W,b,X_train,Y_train,num_iterations,learning_rate,print_cost)
    W = params['w']
    b = params['b']

    #用學得的參數進行預測:
    prediction_train = predict(W,b,X_test)
    prediction_test = predict(W,b,X_train)

    #計算準確率,分別在訓練集和測試集上:
    accuracy_train = 1 - np.mean(np.abs(prediction_train - Y_train))
    accuracy_test = 1 - np.mean(np.abs(prediction_test - Y_test))
    print("Accuracy on train set:",accuracy_train )
    print("Accuracy on test set:",accuracy_test )

   #爲了便於分析和檢查,我們把得到的所有參數、超參數都存進一個字典返回出來:
    d = {"costs": costs,
         "Y_prediction_test": prediction_test , 
         "Y_prediction_train" : prediction_train , 
         "w" : w, 
         "b" : b,
         "learning_rate" : learning_rate,
         "num_iterations": num_iterations,
         "train_acy":train_acy,
         "test_acy":test_acy
        }
    return d

就是這麼easy,只要我們一步步把前面的輔助函數搭建好,這裏就可以很輕鬆很清晰地構造模型。 唯一值得一提的是這個準確率怎麼計算的問題,我們的predict函數得到的是一個列向量(1,m),這個跟我們的標籤Y是一樣的形狀。我們首先可以讓兩者相減prediction_test - Y_test, 如果對應位置相同,則變成0,不同的話要麼是1要麼是-1,於是再取絕對值np.abs(prediction_test - Y_test), 就相當於得到了“哪些位置預測錯了”的一個向量,於是我們再求一個均值np.mean(np.abs(prediction_test - Y_test)), 就是“錯誤率”了,然後用1來減去它,就是正確率了!

大功告成!試試效果:

d = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations = 2000, learning_rate = 0.005, print_cost = True)

運行模型就很簡單了,把我們的數據集穿進去,設置我們想要的超參數,主要是學習率(learning rate)、迭代數(num_iterations),然後把print_cost設爲True,這樣可以在模型訓練過程中打印cost的變化趨勢。

運行,查看結果:

Cost after iteration 0: 0.693147
Cost after iteration 100: 0.584508
Cost after iteration 200: 0.466949
Cost after iteration 300: 0.376007
Cost after iteration 400: 0.331463
Cost after iteration 500: 0.303273
Cost after iteration 600: 0.279880
Cost after iteration 700: 0.260042
Cost after iteration 800: 0.242941
Cost after iteration 900: 0.228004
Cost after iteration 1000: 0.214820
Cost after iteration 1100: 0.203078
Cost after iteration 1200: 0.192544
Cost after iteration 1300: 0.183033
Cost after iteration 1400: 0.174399
Cost after iteration 1500: 0.166521
Cost after iteration 1600: 0.159305
Cost after iteration 1700: 0.152667
Cost after iteration 1800: 0.146542
Cost after iteration 1900: 0.140872
---------------------
train accuracy: 99.04306220095694 %
test accuracy: 70.0 %

可以看到,隨着訓練的進行,cost在不斷地降低,這說明的參數在變得越來越好。 最終,在訓練集上的準確率達到了99%以上,測試集準確率爲70%。 哈哈,很明顯,我們的模型過擬合了,測試集的準確率還有待提高。但是這個不重要!重要的是我們親手再沒有用任何框架的情況下用python把Logistic regression給實現了一遍,每一個細節都明明白白!٩(๑>◡<๑)۶ 況且,這才僅僅是一個Logistic regression,相當於1層的只有一個神經元的神經網絡,能對圖片分類達到70%的準確率,我們已經很棒了!


其實,神經網絡無非就是在Logistic regression的基礎上,多了幾個隱層,每層多了一些神經元,卷積神經網絡無非就是再多了幾個特殊的filter,多了一些有特定功能的層,但是核心都是跟Logistic Regression一樣的:

前向傳播求損失, 反向傳播求倒數; 不斷迭代和更新, 調參預測準確度。

喲嗬!才發現自己還有寫詩的天賦。

本文就到此結束,終於結束了,出去吃串串了~


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章