機器學習實戰筆記(六):Logistic迴歸(Python3 實現)

1 Logistic迴歸介紹

       假設現在有一些數據點,我們用一條直線對這些點進行擬合(該線稱爲最佳擬合直線),這個擬合過程就稱作迴歸。利用Logistic迴歸進行分類的主要思想是:根據現有數據對分類邊界線建立迴歸公式,以此進行分類。這裏的“迴歸”一詞源於最佳擬合,表示要找到最佳擬合參數集。訓練分類器時的做法就是尋找最佳擬合參數,使用的是最優化算法。

 1.1 Logistic迴歸的一般過程
(1) 收集數據:採用任意方法收集數據。
(2) 準備數據:由於需要進行距離計算,因此要求數據類型爲數值型。另外,結構化數據格式則最佳。
(3) 分析數據:採用任意方法對數據進行分析。
(4) 訓練算法:大部分時間將用於訓練,訓練的目的是爲了找到最佳的分類迴歸係數。
(5) 測試算法:一旦訓練步驟完成,分類將會很快。

(6) 使用算法:首先,我們需要輸入一些數據,並將其轉換成對應的結構化數值;接着,基於訓練好的迴歸係數就可以對這些數值進行簡單的迴歸計算,判定它們屬於哪個類別;在這之後,我們就可以在輸出的類別上做一些其他分析工作。

2 基於 Logistic 迴歸和 Sigmoid 函數的分類

2.1 Logistic迴歸特點
優點:計算代價不高,易於理解和實現。
缺點:容易欠擬合,分類精度可能不高。
適用數據類型:數值型和標稱型數據。


import numpy as np


# Logistic 迴歸梯度上升優化算法
def load_data_set():
    # 創建兩個列表
    data_mat = []
    label_mat = []
    fr = open('testSet.txt')
    for line in fr.readlines():
        # 對當前行去除首尾空格,並按空格進行分離
        line_arr = line.strip().split()
        data_mat.append([1.0, float(line_arr[0]), float(line_arr[1])])
        label_mat.append(int(line_arr[2]))
    return data_mat, label_mat


def sigmoid(inx):

    return 1.0 / (1 + np.exp(-inx))
    # return 0.5 * (1 + np.tanh(0.5 * inx))


def grad_ascent(data_mat_in, class_labels):
    data_matrix = np.mat(data_mat_in)  # convert to NumPy matrix
    label_mat = np.mat(class_labels).transpose()  # convert to NumPy matrix
    m, n = np.shape(data_matrix)
    alpha = 0.001
    max_cycles = 500
    weights = np.ones((n, 1))
    for k in range(max_cycles):  # heavy on matrix operations
        h = sigmoid(data_matrix * weights)  # matrix mult
        error = (label_mat - h)  # vector subtraction
        weights = weights + alpha * data_matrix.transpose() * error  # matrix mult
    return weights

2.2 分析數據:畫出決策邊界

# 畫出數據集和Logistic迴歸最佳擬合直線的函數
def plot_best_fit(weights):
    import matplotlib.pyplot as plt
    data_mat, label_mat = load_data_set()
    data_arr = np.array(data_mat)
    n = np.shape(data_arr)[0]
    xcord1 = []
    ycord1 = []
    xcord2 = []
    ycord2 = []
    for i in range(n):
        if int(label_mat[i]) == 1:
            xcord1.append(data_arr[i, 1])
            ycord1.append(data_arr[i, 2])
        else:
            xcord2.append(data_arr[i, 1])
            ycord2.append(data_arr[i, 2])
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(xcord1, ycord1, s=30, c='red', marker='s')
    ax.scatter(xcord2, ycord2, s=30, c='green')
    x = np.arange(-3.0, 3.0, 0.1)
    y = (-weights[0] - weights[1] * x) / weights[2]
    ax.plot(x, y)
    plt.xlabel('X1')
    plt.ylabel('X2')
    plt.show()
if __name__ == '__main__':
    dataMat, labelMat = load_data_set()
    weights = grad_ascent(dataMat, labelMat)
    plot_best_fit(weights.getA())
輸出的結果如圖1所示。
圖1

        這個分類結果相當不錯,從圖上看只錯分了兩到四個點。但是,儘管例子簡單且數據集很小,這個方法卻需要大量的計算(300次乘法)。因此下一節將對該算法稍作改進,從而使它可以用在真實數據集上。

2.3 訓練算法:隨機梯度上升

       梯度上升算法在每次更新迴歸係數時都需要遍歷整個數據集,該方法在處理100個左右的數
據集時尚可,但如果有數十億樣本和成千上萬的特徵,那麼該方法的計算複雜度就太高了。一種
改進方法是一次僅用一個樣本點來更新迴歸係數,該方法稱爲隨機梯度上升算法。由於可以在新
樣本到來時對分類器進行增量式更新,因而隨機梯度上升算法是一個在線學習算法。與“在線學
習”相對應,一次處理所有數據被稱作是“批處理”。
隨機梯度上升算法可以寫成如下的僞代碼:
所有迴歸係數初始化爲1
對數據集中每個樣本
計算該樣本的梯度
使用alpha × gradient更新迴歸係數值
返回迴歸係數值

以下是隨機梯度上升算法的實現代碼。

def stoc_grad_ascent0(data_matrix, class_labels):
    m, n = np.shape(data_matrix)
    alpha = 0.01
    weights = np.ones(n)  # initialize to all ones
    for i in range(m):
        h = sigmoid(sum(data_matrix[i] * weights))
        error = class_labels[i] - h
        weights = weights + alpha * error * data_matrix[i]
    return weights
if __name__ == '__main__':
    dataMat, labelMat = load_data_set()
    weights = stoc_grad_ascent0(np.array(dataMat), labelMat)
    plot_best_fit(weights)

       執行完畢後將得到圖2所示的最佳擬合直線圖,該圖與圖1有一些相似之處。可以看到,擬合出來的直線效果還不錯,但並不像圖1那樣完美。這裏的分類器錯分了三分之一的樣本。


圖2

# 改進的隨機梯度上升算法
def stoc_grad_ascent1(data_matrix, class_labels, num_iter=150):
    m, n = np.shape(data_matrix)
    weights = np.ones(n)  # initialize to all ones
    for j in range(num_iter):
        data_index = list(range(m))
        for i in range(m):
            alpha = 4 / (1.0 + j + i) + 0.0001  # apha decreases with iteration, does not
            rand_index = int(np.random.uniform(0, len(data_index)))  # go to 0 because of the constant
            h = sigmoid(sum(data_matrix[rand_index] * weights))
            error = class_labels[rand_index] - h
            weights = weights + alpha * error * data_matrix[rand_index]
            del (data_index[rand_index])
    return weights
if __name__ == '__main__':
    dataMat, labelMat = load_data_set()
    weights = stoc_grad_ascent1(np.array(dataMat), labelMat)
    plot_best_fit(weights)
       程序運行之後應該能看到類似圖3的結果圖。該分隔線達到了與GradientAscent()差不多的效果,但是所使用的計算量更少。

圖3

3 示例:從疝氣病症預測病馬的死亡率

3.1 使用Logistic迴歸估計馬疝病的死亡率過程
(1) 收集數據:給定數據文件。
(2) 準備數據:用Python解析文本文件並填充缺失值。
(3) 分析數據:可視化並觀察數據。
(4) 訓練算法:使用優化算法,找到最佳的係數。
(5) 測試算法:爲了量化迴歸的效果,需要觀察錯誤率。根據錯誤率決定是否回退到訓練階段,通過改變迭代的次數和步長等參數來得到更好的迴歸係數。

3.2 準備數據:處理數據中的缺失值

        數據中的缺失值是個非常棘手的問題,有很多文獻都致力於解決這個問題。那麼,數據缺失究竟帶來了什麼問題?假設有100個樣本和20個特徵,這些數據都是機器收集回來的。若機器上的某個傳感器損壞導致一個特徵無效時該怎麼辦?此時是否要扔掉整個數據?這種情況下,另外19個特徵怎麼辦?它們是否還可用?答案是肯定的。因爲有時候數據相當昂貴,扔掉和重新獲取都是不可取的,所以必須採用一些方法來解決這個問題。
下面給出了一些可選的做法:
 使用可用特徵的均值來填補缺失值;
 使用特殊值來填補缺失值,如-1;
 忽略有缺失值的樣本;
 使用相似樣本的均值添補缺失值;

 使用另外的機器學習算法預測缺失值。

3.3 測試算法:用 Logistic 迴歸進行分類

        使用Logistic迴歸方法進行分類並不需要做很多工作,所需做的只是把測試集上每個特徵向量乘以最優化方法得來的迴歸係數,再將該乘積結果求和,最後輸入到Sigmoid函數中即可。如果對應的Sigmoid值大於0.5就預測類別標籤爲1,否則爲0。

# Logistic迴歸分類函數
def classify_vector(inx, weights):
    """
    它以迴歸係數和特徵向量作爲輸入來計算對應的Sigmoid值。
    如果Sigmoid值大於0.5函數返回1,否則返回0。
    """
    prob = sigmoid(sum(inx * weights))
    if prob > 0.5:
        return 1.0
    else:
        return 0.0


def colic_test():
    fr_train = open('horseColicTraining.txt')
    fr_test = open('horseColicTest.txt')
    training_set = []
    training_labels = []
    for line in fr_train.readlines():
        curr_line = line.strip().split('\t')
        line_arr = []
        for i in range(21):
            line_arr.append(float(curr_line[i]))
        training_set.append(line_arr)
        training_labels.append(float(curr_line[21]))
    train_weights = stoc_grad_ascent1(np.array(training_set), training_labels, 1000)
    error_count = 0
    num_test_vec = 0.0
    for line in fr_test.readlines():
        num_test_vec += 1.0
        curr_line = line.strip().split('\t')
        line_arr = []
        for i in range(21):
            line_arr.append(float(curr_line[i]))
        if int(classify_vector(np.array(line_arr), train_weights)) != int(curr_line[21]):
            error_count += 1
    error_rate = (float(error_count) / num_test_vec)
    print("the error rate of this test is: %f" % error_rate)
    return error_rate


def multi_test():
    num_tests = 10
    error_sum = 0.0
    for k in range(num_tests):
        error_sum += colic_test()
    print("after %d iterations the average error rate is: %f" % (num_tests, error_sum / float(num_tests)))
if __name__ == '__main__':
    # dataMat, labelMat = load_data_set()
    # weights = grad_ascent(dataMat, labelMat)
    # weights = stoc_grad_ascent1(np.array(dataMat), labelMat)
    # plot_best_fit(weights)
    multi_test()

輸出結果:

D:\ProgramData\Anaconda2\envs\python3\python.exe E:/study_code/ML_in_action_code/logistic_regression/logRegres.py
E:/study_code/ML_in_action_code/logistic_regression/logRegres.py:27: RuntimeWarning: overflow encountered in exp
  return 1.0 / (1 + np.exp(-inx))
the error rate of this test is: 0.328358
the error rate of this test is: 0.298507
the error rate of this test is: 0.313433
the error rate of this test is: 0.358209
the error rate of this test is: 0.313433
the error rate of this test is: 0.253731
the error rate of this test is: 0.417910
the error rate of this test is: 0.313433
the error rate of this test is: 0.253731
the error rate of this test is: 0.298507
after 10 iterations the average error rate is: 0.314925

Process finished with exit code 0

       從上面的結果可以看到, 10次迭代之後的平均錯誤率爲31%。事實上,這個結果並不差,因爲有30%的數據缺失。當然,如果調整colic_test()中的迭代次數和stoch_grad_ascent1()中的步長,平均錯誤率可以降到20%左右。

注意:運行中有個警告:

RuntimeWarning: overflow encountered in expreturn 1.0 / (1 + np.exp(-inx)),可以轉換成如下等價形式後算法會更穩定:return 0.5 * (1 + np.tanh(0.5 * x)) 參考《基於RBM的推薦算法

3.4 完整代碼

# encoding: utf-8
"""
@author:max bay 
@version: python 3.6
@time: 2018/6/2 20:55
"""

import numpy as np


# Logistic 迴歸梯度上升優化算法
def load_data_set():
    # 創建兩個列表
    data_mat = []
    label_mat = []
    fr = open('testSet.txt')
    for line in fr.readlines():
        # 對當前行去除首尾空格,並按空格進行分離
        line_arr = line.strip().split()
        data_mat.append([1.0, float(line_arr[0]), float(line_arr[1])])
        label_mat.append(int(line_arr[2]))
    return data_mat, label_mat


def sigmoid(inx):

    return 1.0 / (1 + np.exp(-inx))
    # return 0.5 * (1 + np.tanh(0.5 * inx))


def grad_ascent(data_mat_in, class_labels):
    data_matrix = np.mat(data_mat_in)  # convert to NumPy matrix
    label_mat = np.mat(class_labels).transpose()  # convert to NumPy matrix
    m, n = np.shape(data_matrix)
    alpha = 0.001
    max_cycles = 500
    weights = np.ones((n, 1))
    for k in range(max_cycles):  # heavy on matrix operations
        h = sigmoid(data_matrix * weights)  # matrix mult
        error = (label_mat - h)  # vector subtraction
        weights = weights + alpha * data_matrix.transpose() * error  # matrix mult
    return weights


# 畫出數據集和Logistic迴歸最佳擬合直線的函數
def plot_best_fit(weights):
    import matplotlib.pyplot as plt
    data_mat, label_mat = load_data_set()
    data_arr = np.array(data_mat)
    n = np.shape(data_arr)[0]
    xcord1 = []
    ycord1 = []
    xcord2 = []
    ycord2 = []
    for i in range(n):
        if int(label_mat[i]) == 1:
            xcord1.append(data_arr[i, 1])
            ycord1.append(data_arr[i, 2])
        else:
            xcord2.append(data_arr[i, 1])
            ycord2.append(data_arr[i, 2])
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(xcord1, ycord1, s=30, c='red', marker='s')
    ax.scatter(xcord2, ycord2, s=30, c='green')
    x = np.arange(-3.0, 3.0, 0.1)
    y = (-weights[0] - weights[1] * x) / weights[2]
    ax.plot(x, y)
    plt.xlabel('X1')
    plt.ylabel('X2')
    plt.show()


def stoc_grad_ascent0(data_matrix, class_labels):
    m, n = np.shape(data_matrix)
    alpha = 0.01
    weights = np.ones(n)  # initialize to all ones
    for i in range(m):
        h = sigmoid(sum(data_matrix[i] * weights))
        error = class_labels[i] - h
        weights = weights + alpha * error * data_matrix[i]
    return weights


# 改進的隨機梯度上升算法
def stoc_grad_ascent1(data_matrix, class_labels, num_iter=150):
    m, n = np.shape(data_matrix)
    weights = np.ones(n)  # initialize to all ones
    for j in range(num_iter):
        data_index = list(range(m))
        for i in range(m):
            alpha = 4 / (1.0 + j + i) + 0.0001  # apha decreases with iteration, does not
            rand_index = int(np.random.uniform(0, len(data_index)))  # go to 0 because of the constant
            h = sigmoid(sum(data_matrix[rand_index] * weights))
            error = class_labels[rand_index] - h
            weights = weights + alpha * error * data_matrix[rand_index]
            del (data_index[rand_index])
    return weights


# Logistic迴歸分類函數
def classify_vector(inx, weights):
    """
    它以迴歸係數和特徵向量作爲輸入來計算對應的Sigmoid值。
    如果Sigmoid值大於0.5函數返回1,否則返回0。
    """
    prob = sigmoid(sum(inx * weights))
    if prob > 0.5:
        return 1.0
    else:
        return 0.0


def colic_test():
    fr_train = open('horseColicTraining.txt')
    fr_test = open('horseColicTest.txt')
    training_set = []
    training_labels = []
    for line in fr_train.readlines():
        curr_line = line.strip().split('\t')
        line_arr = []
        for i in range(21):
            line_arr.append(float(curr_line[i]))
        training_set.append(line_arr)
        training_labels.append(float(curr_line[21]))
    train_weights = stoc_grad_ascent1(np.array(training_set), training_labels, 1000)
    error_count = 0
    num_test_vec = 0.0
    for line in fr_test.readlines():
        num_test_vec += 1.0
        curr_line = line.strip().split('\t')
        line_arr = []
        for i in range(21):
            line_arr.append(float(curr_line[i]))
        if int(classify_vector(np.array(line_arr), train_weights)) != int(curr_line[21]):
            error_count += 1
    error_rate = (float(error_count) / num_test_vec)
    print("the error rate of this test is: %f" % error_rate)
    return error_rate


def multi_test():
    num_tests = 10
    error_sum = 0.0
    for k in range(num_tests):
        error_sum += colic_test()
    print("after %d iterations the average error rate is: %f" % (num_tests, error_sum / float(num_tests)))


if __name__ == '__main__':
    # dataMat, labelMat = load_data_set()
    # weights = grad_ascent(dataMat, labelMat)
    # weights = stoc_grad_ascent1(np.array(dataMat), labelMat)
    # plot_best_fit(weights)
    multi_test()

4 總結

       Logistic迴歸的目的是尋找一個非線性函數Sigmoid的最佳擬合參數,求解過程可以由最優化算法來完成。在最優化算法中,最常用的就是梯度上升算法,而梯度上升算法又可以簡化爲隨機梯度上升算法。
      隨機梯度上升算法與梯度上升算法的效果相當,但佔用更少的計算資源。此外,隨機梯度上升是一個在線算法,它可以在新數據到來時就完成參數更新,而不需要重新讀取整個數據集來進行批處理運算。
       機器學習的一個重要問題就是如何處理缺失數據。這個問題沒有標準答案,取決於實際應用中的需求。現有一些解決方案,每種方案都各有優缺點。

5 參考及閱讀資料

[1] 機器學習實戰

[2] 機器學習實戰之logistic迴歸

[3] Python3《機器學習實戰》學習筆記(六):Logistic迴歸基礎篇之梯度上升算法

[4] Python3《機器學習實戰》學習筆記(七):Logistic迴歸實戰篇之預測病馬死亡率

[5] python機器學習實戰 getA()函數詳解

[6] python3中報錯:TypeError: 'range' object doesn't support item deletion

[7] 機器學習4logistic迴歸

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