[機器學習實戰-Logistic迴歸]使用Logistic迴歸預測各種例項

vanish丶發表於2020-04-29

[機器學習實戰-Logistic迴歸]使用Logistic迴歸預測各種例項

本實驗程式碼已經傳到gitee上,請點選查收!

Logistic_Examples

一、實驗目的

  1. 學習Logistic迴歸的基本思想。
  2. Sigmoid函式和Logistic迴歸分類器。
  3. 學習最優化演算法--梯度上升演算法、隨機梯度上升演算法等。
  4. 運用Logistic迴歸預測各種例項。

二、實驗內容與設計思想

實驗內容

  1. 基於Logistic迴歸和Sigmoid函式分類
  2. 基於最優化方法的最佳迴歸係數確定
  3. 示例1:從疝氣病症預測病馬的死亡率
  4. 示例2:從打鬥數和接吻數預測電影型別(資料自制)
  5. 示例3:從心臟檢查樣本幫助診斷心臟病(資料來源於網路)
  6. 改進函式封裝使不同的樣本資料可以使用相同的函式封裝

設計思想

  • Logistic迴歸的一般步驟:
    1. 收集資料:採用任意方法收集資料。
    2. 準備資料:由於需要進行距離計算,因此要求資料型別為數值型。另外,結構化資料格式則最佳
    3. 分析資料:採用任意方法對資料進行分析。
    4. 訓練演算法:大部分時間將用於訓練,訓練的目的是為了找到最佳的分類迴歸係數。
    5. 測試演算法:一旦訓練步驟完成,分類將會很快。
    6. 使用演算法:首先,我們需要輸入一些資料,並將其轉換成對應的結構化數值;接著,基於訓練好的迴歸係數就可以對這些數值進行簡單的迴歸計算。判定它們屬於哪個類別;在這之後,我們就可以輸出的類別上做一些其他的分析工作。

三、實驗使用環境

  • 作業系統:Microsoft Windows 10
  • 程式設計環境:Python 3.6、Pycharm、Anaconda

四、實驗步驟和除錯過程

4.1 基於Logistic迴歸和Sigmoid函式分類

  • 優點:計算代價不高,易於理解和實現。
  • 缺點:容易欠擬合,分類精度可能不高。
  • 使用資料型別:數值型和標稱型資料。

Logistic迴歸演算法只能用於預測結果只有兩種情況(即要麼0,要麼1)的例項。而我們需要的函式則是能接收所有輸入特徵,最後預測出類別。且函式能穩定在某兩個值之間,且能夠平均分配,這裡就引入了數學上的一個函式,即Sigmoid函式

Sigmoid函式計算公式如下:

Sigmoid函式公式

Sigmoid函式影像如下(來源:百度百科-Sigmoid函式):

Sigmoid函式影像

Sigmoid函式特點描述:

當z值為0,Sigmoid函式值為0.5.隨著z的不斷增大,對應的Sigmoid值將逼近1;而隨著z的減小,Sigmoid值將逼近與0。如果橫座標刻度足夠大,那麼縱觀Sigmoid函式,它看起來很像一個階躍函式。

而Sigmoid函式中的z值是需要經過下列計算的,為了實現Logistic迴歸分類器,我們可以在每一個特徵上乘以一個迴歸係數,然後把所有的結果值相加,而這個相加的總和就是z值了。再將z值代入到Sigmoid函式中,可以得到一個0到1之間的數值,我們將任何大於0.5的數值歸為1類,小於0.5的數值被歸為0類。所以,Logistic迴歸可以被看做是一種概率估算。

在上面說明了z值的計算方法後,我們用公式來更直觀地描述z值計算:

z值計算公式

用向量的寫法,上述公式可以寫成image-20200428201440804,表示將這兩個數值向量對應元素相乘起來然後全部加起來即是z值。其中向量x是分類器的輸入資料,即特徵值資料;向量w是我們需要尋找的最佳係數,尋找最佳係數的目的是為了讓分類器的結果儘可能的精確。

經過上面分析,Sigmoid函式公式最終形式可以寫成下面這種形式:

image-20200428202427318


4.2 基於最優化方法的最佳迴歸係數確定

上面我們提到z值計算中,w的值是迴歸係數,而回歸係數決定了預測結果的準確性,為了獲取最優迴歸係數,我們需要使用最優化方法。最優化方法這裡學習和使用兩種:梯度上升演算法隨機梯度上升演算法(是對梯度上升演算法的改進,使計算複雜度降低)。

4.2.1 梯度上升演算法:

演算法思想:要找到某函式的最大值,最好的方法是沿著該函式的梯度方向探尋。

演算法迭代公式:

image-20200428203614667

其中:

  • w是我們要求的最佳係數,因為這個公式要迭代多次,且不斷變大直到得到一個最佳係數值,所以每次要在w的基礎上加上某個方向的步長。
  • α是步長,即每次的移動量。
  • image-20200428203959064是梯度方向,即每次迭代時w上升最快的方向。

4.2.2 測試演算法:使用梯度上升演算法找到最佳引數

def loadDataSet():
    """
    讀取測試檔案中的資料,拆分得到的每一行資料並存入相應的矩陣中,最後返回。
    :return: dataMat, labelMat
        dataMat: 特徵矩陣
        labelMat: 型別矩陣
    """

    # 初始化特徵列表和型別列表
    dataMat = []
    labelMat = []
    # 開啟測試資料,預設為讀方式
    fr = open("data/testSet.txt")
    # 通過readLines()方法可以獲得檔案中的所有行資訊
    for line in fr.readlines():
        # 將一行中大的資訊先通過strip()方法去掉首尾空格
        # 再通過split()方法進行分割,預設分割方式是空格分割
        # 將分割好後的資料存入列表lineArr
        lineArr = line.strip().split()
        # 取出列表lineArr中的資料通過append方法插入到dataMat列表表中
        # 這裡為了方便計算,將第一列的值都設定為1.0
        dataMat.append([1.0, float(lineArr[0]), float(lineArr[1])])
        # 再取出檔案中的最後一列的值作為型別值存入labelMat列表中
        labelMat.append(int(lineArr[2]))
        """
        列表中的append方法小結:
            從上面兩個列表新增元素的語句可以看出:
            當我們需要新增一行資料時,需要加[],表示將插入一行資料。
            當我們將資料直接插入到某一列的後面時,則不需要加[]。
            且需要注意的是:如果一次要新增好幾個元素時,必須有[],否則會報錯。
        """
    return dataMat, labelMat


def sigmoid(inX):
    """
    sigmoid函式:α(z) = 1 / (1 + e^(-z))
    :param inX: 函式中的引數z
    :return: 返回函式計算結果
    """
    return 1.0 / (1 + exp(-inX))


def gradAscent(dataMatIn, classLabels):
    """
    梯度上升演算法
    :param dataMatIn: 特徵值陣列
    :param classLabels: 型別值陣列
    :return: 返回最佳迴歸係數
    """

    # 通過numpy模組中的mat方法可以將列表轉化為矩陣
    dataMatrix = mat(dataMatIn)
    # transpose()方法是矩陣中的轉置
    labelMatrix = mat(classLabels).transpose()
    # 通過numpy中大的shape方法可以獲得矩陣的行數和列數資訊
    # 當矩陣是一維矩陣時,返回的是一個數的值
    # 當矩陣是二維矩陣時,返回的是一個(1*2)的元組,元組第一個元素表示行數,第二個元素表示列數
    m, n = shape(dataMatrix)  # m = 100; n = 3
    alpha = 0.001  # 步長
    maxCycles = 500  # 迭代次數
    # ones()屬於numpy模組,函式功能是生成一個所有元素都為1的陣列
    # 這裡是生成一個“n行1列”的陣列,陣列中每一個元素值都是1
    weights = ones((n, 1))
    # 迴圈迭代maxCycles次,尋找最佳引數
    for k in range(maxCycles):
        # dataMatrix * weights 是矩陣乘法
        # 矩陣相乘時注意第一個矩陣的列數要和第二個矩陣的行數相同
        # (m × n) * ( n × 1) = (m × 1) 括號中表示幾行幾列
        # (100 × 3) * (3 × 1) = (100 × 1)
        # 最後得到一個100行1列的矩陣
        # 該矩陣運算結果為為sigmoid函式(α(z) = 1 / (1 + e^(-z)))中的z
        # z的公式為:z = w0x0 + w1x1 + w2x2 + ... + wnxn
        h = sigmoid(dataMatrix * weights)
        # 計算真實類別與預測類別的差值
        error = labelMatrix - h
        # 按差值error的方向調整迴歸係數
        # 0.01 * (3 × m) * (m × 1)
        # 表示每一個列上的一個誤差情況,最後得到x1,x2,xn的係數偏移量
        # 矩陣乘法,最後得到一個更新後的迴歸係數
        # 梯度上升演算法公式:w:=w+α▽w f(w)
        # 其中α是步長,▽w f(w)是上升方向
        weights = weights + alpha * dataMatrix.transpose() * error
    return array(weights)

輸出:

if __name__ == '__main__':
    dataMats, classMats = loadDataSet()
    dataArr = array(dataMats)
    weights = array(gradAscent(dataMats, classMats))
    print(weights)  # 輸出最佳係數w的各個值

[[ 4.12414349]
[ 0.48007329]
[-0.6168482 ]]

小結:

  • 梯度上升演算法核心函式是gradAscent(dataMatIn, classLabels)函式,在該函式中不斷迭代使得引數w不斷優化,使得最終返回的引數最優。

  • 上述的sigmoid(inX)函式就是我們使用的Sigmoid函式公式,寫成函式是因為下面我們會頻繁的使用到這個函式,所以將該公式單獨封裝成一個函式。

  • loadDataSet()函式作用就是將我們收集到的資料讀取出來,並且將讀取出來的資料格式化儲存到相應的陣列中,最後返回供外界使用和分析。

  • 這裡小結一下列表中的append()方法和extend()方法:

    if __name__ == '__main__':
        li = []
        appendLi = [1, 2, 3]
        extendLi = [3, 4, 5]
        li.append(appendLi)
        print(li)
        li.extend(extendLi)
        print(li)
    

    輸出:

    [[1, 2, 3]]
    [[1, 2, 3], 3, 4, 5]

    可以看到append()方法是將appendLi這個列表加入到li列表的新的一行,而extend()方法則是將extendLi列表中的數值取出來一個個接在li列表後面。

  • 這裡涉及到的numpy模組中的新函式(所謂新,是相對於我來說滴):

    • mat():將陣列或則列表轉化為矩陣
    • 矩陣中包含的方法:transpose()方法是矩陣的轉置。
    • 矩陣相乘需要注意:第一個矩陣的列數需要和第二個矩陣的函式相同。
  • line.strip().split()用法:可以看到這裡是對字串的切割,字串line先通過strip()方法去掉了首尾的空格,在通過split()方法進行預設空格切割。兩種方法一氣呵成,不用分成兩不寫。

4.2.3 分析資料:畫出決策邊界

def plotBestFit(dataArr, labelMat, weights):
    """
    畫出決策邊界
    :param dataArr: 特徵值陣列
    :param labelMat: 型別陣列
    :param weights: 最佳迴歸係數
    :return:
    """

    # 通過shape函式獲得dataArr的行列數,其中[0]即行數
    n = shape(dataArr)[0]
    # xCord1和yCord1是型別為1的點的x和y座標值
    xCord1 = []
    yCord1 = []
    # xCord2和yCord1是型別為0的點的x和y座標值
    xCord2 = []
    yCord2 = []
    # 特徵陣列的每一行和型別陣列的沒每一列一一對應
    for i in range(n):
        # 當型別為1時,
        # 將特徵陣列中的指定行的1和2兩個下標下的值分別作為x軸和y軸的值
        if int(labelMat[i]) == 1:
            xCord1.append(dataArr[i, 1])
            yCord1.append(dataArr[i, 2])
        # 當型別為0時,
        # 將特徵陣列中的指定行的1和2兩個下標下的值分別作為x軸和y軸的值
        else:
            xCord2.append(dataArr[i, 1])
            yCord2.append(dataArr[i, 2])
    # figure()操作時建立或者呼叫畫板
    # 使用時遵循就近原則,所有畫圖操作是在最近一次呼叫的畫圖板上實現。
    fig = plt.figure()
    # 將fig分成1×1的網格,在第一個格子中載入ax圖
    # 引數111表示“1×1網格中的第1個表格”
    # 如果引數是211則表示“2行1列的表格的中的第一個表格”
    # 第幾個表格的計算順序為從左到右,從上到下
    ax = fig.add_subplot(111)
    # 設定散點圖引數
    # 前兩個引數xCord1,yCord1表示散點對應的x和y座標值
    # s=30表示散點大小為30
    # c='red'表示散點顏色為紅色
    # marker='s'表示散點的形狀,這裡是正方形
    ax.scatter(xCord1, yCord1, s=30, c='red', marker='s')
    # 同上說明
    ax.scatter(xCord2, yCord2, s=30, c='green')
    # 生成一個[-3.0, 3.0]範圍中間隔每0.1取一個值
    x = arange(-3.0, 3.0, 0.1)
    # y相對於x的函式
    """
    這裡的y是怎麼得到的呢?
        從dataMat.append([1.0, float(lineArr[0]), float(lineArr[1])])
        可得:w0*x0+w1*x1+w2*x2 = z
        x0最開始就設定為1,x2就是我們畫圖的y值,x1就是我們畫圖的x值。
        所以:w0 + w1*x + w2*y = 0
        →   y = (-w0 - w1 * x) / w2
    """
    y = (-weights[0] - weights[1] * x) / weights[2]
    # 畫線
    ax.plot(x, y)
    # 設定x軸和y軸的名稱
    plt.xlabel('x1')
    plt.ylabel('x2')
    # 展示影像
    plt.show()

輸出:

if __name__ == '__main__':
    dataMats, classMats = loadDataSet()
    dataArr = array(dataMats)
    weights = array(gradAscent(dataMats, classMats))
    plotBestFit(dataArr, classMats, weights)  # 呼叫該函式話決策邊界

輸出圖如下:

image-20200428212740983

小結:

  • 觀察上面的圖示,可以看出這個分類結果相當不錯,只錯分了三四個點。

  • 畫圖方法小結:(箭頭表示賦值)

    • 呼叫畫板→fig:plt.figure() 方法。(plt是畫圖工具包我們取的別名)
  • 劃分畫板→ax:fig.add_subplot(111)方法,其中111表示1*1的畫板中的第1個畫板。

    • 畫圖:畫曲散點圖用ax.sctter()方法,畫曲線圖用ax.plot()方法
  • 設定座標名稱:plt.xlabel('x1')和plt.ylabel('x2'),這裡設定x軸名稱為x1,y軸名稱為x2。

    • 顯示圖畫:plt.show()方法。
  • 但是儘管該例子很小且資料集很小,求最佳係數時需要大量的計算(300次乘法)。對於幾百個左右的資料集合還可以,但是如果是10億個樣本和成千上萬的特徵,那麼這個計算方法的複雜度太高了,甚至可能出不來最佳係數。所以下面引入隨機梯度上升演算法

4.2.4 訓練演算法:隨機梯度上升

def stocGradAscent0(dataMatrix, classLabels):
    """
    隨機梯度上升演算法
    :param dataMatrix: 特徵值矩陣
    :param classLabels: 型別陣列
    :return: 最佳係數weights
    """

    # m = 100,n = 3
    m, n = shape(dataMatrix)
    alpha = 0.01
    weights = ones(n)
    print(weights)
    # 迴圈迭代m次,即100次
    for i in range(m):
        # (1 × 3) * (1 × 3) = (1 × 3)
        # 陣列相乘,兩個陣列的每個元素對應相乘
        # 最後求和
        # z = w0x0 + w1x1 + wnxn
        # 在將z代入sigmoid函式進行計算
        h = sigmoid(sum(dataMatrix[i] * weights))
        # 計算實際結果和測試結果之間的誤差,按照差值調整迴歸係數
        error = classLabels[i] - h
        # 通過梯度上升演算法更新weights
        weights = weights + alpha * error * dataMatrix[i]
    return weights

使用上面的plotBestFit(dataArr, labelMat, weights)函式分析該演算法。

輸出:

if __name__ == '__main__':
    dataMats, classMats = loadDataSet()
    dataArr = array(dataMats)
    weights = array(stocGradAscent0(dataArr, classMats))
    plotBestFit(dataArr, classMats, weights)

image-20200428214432452

小結:

  • 通過圖示可以看出,隨機梯度上升演算法錯分了三分之一的樣本。
  • 隨機梯度上升演算法與梯度上升演算法很類似,但是有一些區別:
    1. 後者的變數h和誤差error都是向量,而前者則全是一個數值;
    2. 前者沒有矩陣轉換的過程,所有變數的資料型別都是Numpy,效率上會較後者快。
  • 因為經過比較此時的隨機梯度演算法錯分了三分之一的樣本,我們通過下面的改進來優化一下該演算法。

改進後的隨機梯度上升演算法程式碼如下:

def stocGradAscent1(dataMatrix, classLabels, numIter=150):
    """
    改進後的隨機梯度上升演算法
    :param dataMatrix: 特徵值矩陣
    :param classLabels: 型別陣列
    :param numIter: 迭代次數,設定預設值為150
    :return: 最佳係數weights
    """

    m, n = shape(dataMatrix)
    # 建立與列數相同矩陣,所有元素都為1
    weights = ones(n)
    # 隨機梯度,迴圈預設次數為150次,觀察是否收斂
    for j in range(numIter):
        # 產生列表為[0, 1, 2 ... m-1]
        dataIndex = list(range(m))
        for i in range(m):
            # i和j不斷增大,導致alpha不斷減小,單上衣不為0,
            # alpha會隨著迭代不斷的減小,但永遠不會減小到0,因為後面還有一個常數項0.01
            alpha = 4 / (1.0 + j + i) + 0.01
            # 產生一個隨機在0-len()之間的值
            # random.uniform(x, y)方法將隨機生成一個實數,他在[x, y]範圍內,x<=y。
            randIndex = int(random.uniform(0, len(dataIndex)))
            # sum(dataMatrix[randIndex]*weights)是為了求z值
            # z = w0x0 + w1x1 + ... + wnxn
            h = sigmoid(sum(dataMatrix[randIndex] * weights))
            # 計算實際結果和測試結果之間的誤差,按照差值調整迴歸係數
            error = classLabels[randIndex] - h
            # 通過梯度上升演算法更新weights
            weights = weights + alpha * error * dataMatrix[randIndex]
            # 刪除掉此次更新中用到的特徵資料
            del(dataIndex[randIndex])
    return weights

輸出:

if __name__ == '__main__':
    dataMats, classMats = loadDataSet()
    dataArr = array(dataMats)
    weights = array(stocGradAscent1(dataArr, classMats))
    plotBestFit(dataArr, classMats, weights)

image-20200428215537503

小結:

  • 這裡增加了3處程式碼來進行改進:
    1. 一方面,alpha在每次迭代的時候都會再進行調整。另外,雖然alpha會隨著迭代次數不斷減小,但永遠不會減小到0,這是因為調整時還存在一個常數項。必須這樣做的原因是為了保證多次迭代後新資料仍然具有一定的影響。
    2. 第二處改進,通過隨機選取樣本來更新迴歸係數。這種方法每次隨機從列表中選取一個值,更新完係數後刪除掉該值(再進行下一次迭代)。
    3. 第三就是該演算法還增加了第三個引數,傳入迭代的次數,預設值為150.
  • 在看此時分析資料得到的劃分圖,此時劃分只錯分了幾個樣本資料,所以該優化後的隨機梯度演算法滿足我們需求,且資料量大的時候不用進行矩陣變化,效率也比較高。

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

這裡我們將使用Logistic迴歸來預測患有疝氣病症的馬的存活問題。這裡的資料包含299個訓練樣本和67個測試樣本。其中我們通過21中特徵值(每個特徵代表什麼我們可以不用關心)來進行預測患有疝病的馬的存活率。1表示存活,0表示死亡。

訓練樣本horseColicTraining.txt展示:

image-20200428222848965

測試樣本horseColicTest.txt展示:

image-20200428222929184

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

def classifyVector(inX, weights):
    """
    使用梯度上升演算法獲取到的最優係數來計算測試樣本中對應的Sigmoid值。
    其中Sigmoid值大於0.5返回1,小於0.5返回0.
    :param inX: 特徵陣列
    :param weights: 最優係數
    :return: 返回分類結果,即1或0
    """

    prob = sigmoid(sum(inX * weights))
    if prob > 0.5:
        return 1.0
    else:
        return 0.0


def colicTest():
    """
    測試迴歸係數演算法的用於計算疝氣病症預測病馬的死亡率的錯誤率。
    這裡運用的隨機梯度演算法來獲取最佳係數w1,w2,...,wn
    :return: 返回此次測試的錯誤率
    """

    # 以預設只讀方式開啟訓練資料樣本和測試資料樣本
    frTrain = open('data/horseColicTraining.txt')
    frTest = open('data/horseColicTest.txt')
    trainingSet = []
    trainingLabels = []
    # 讀取訓練資料樣本的每一行
    for line in frTrain.readlines():
        # 去掉首尾空格,並按tab空格數來切割字串,並將切割後的值存入列表
        currLine = line.strip().split('\t')
        lineArr = []
        # 將21個特徵值依次加入到lineArr列表彙總
        for i in range(21):
            lineArr.append(float(currLine[i]))
        # 再將lineArr列表加入到二維列表trainingSet列表中
        trainingSet.append(lineArr)
        # 將型別值依次接到trainingLabels這個列表的末尾行
        trainingLabels.append(float(currLine[21]))
    # 使用上面寫的改進的隨機梯度演算法求得最佳係數,用於下面分類器使用區分型別
    trainWeights = stocGradAscent1(array(trainingSet), trainingLabels, 300)
    errorCount = 0
    numTestVec = 0.0
    # 讀取測試資料的每一行
    for line in frTest.readlines():
        # 測試資料數加1
        numTestVec += 1.0
        # 去掉首尾空格,並以tab空格數切割字串,並將切割後的值存入列表
        currLine = line.strip().split('\t')
        lineArr = []
        # 將21個特徵值依次加入到特徵列表lineArr中
        for i in range(21):
            lineArr.append(float(currLine[i]))
        # 通過上面計算得到的最佳係數,使用分類器計算lineArr這些特徵下的所屬的型別
        if int(classifyVector(array(lineArr), trainWeights)) != int(currLine[21]):
            # 如果分類器得到結果和真實結果不符,則錯誤次數加1
            errorCount += 1
    # 通過遍歷獲得的所有測試資料量和錯誤次數求得最終的錯誤率
    errorRate = float(errorCount) / numTestVec
    # 輸出錯誤率
    print("測試結果的錯誤率為:{:.2%}".format(errorRate))
    # 返回錯誤率,用於計算n次錯誤率的平均值
    return errorRate


def multiTest():
    """
    多次測試演算法的錯誤率取平均值,以得到一個比較有說服力的結果。
    :return:
    """

    numTests = 10
    errorSum = 0.0
    # 通過10次的演算法測試,並獲得10次錯誤率的總和
    for k in range(numTests):
        errorSum += colicTest()
    # 通過錯誤率總和/10可以求得10次平均錯誤率並輸出
    print("10次演算法測試後平均錯誤率為:{:.2%}".format(errorSum/float(numTests)))

輸出:

if __name__ == '__main__':
    multiTest()

測試結果的錯誤率為:29.85%
測試結果的錯誤率為:32.84%
測試結果的錯誤率為:35.82%
測試結果的錯誤率為:40.30%
測試結果的錯誤率為:34.33%
測試結果的錯誤率為:46.27%
測試結果的錯誤率為:32.84%
測試結果的錯誤率為:37.31%
測試結果的錯誤率為:29.85%
測試結果的錯誤率為:50.75%
10次演算法測試後平均錯誤率為:37.01%

小結:

  • classifyVector(inX, weights)函式,是以最佳迴歸係數和特徵向量作為輸入來計算最終對應的Sigmoid值。如果Sigmoid值大於0.5,函式返回1,否則返回0。注意:這個函式後面其他例項也會經常用到,因為這個函式相當於一個分類器,可以獲取到最終的預測結果。
  • colicTest()函式,是用於開啟測試集和訓練測試集,並對資料進行格式化處理的函式,函式最終放回測試的錯誤率。
  • multiTest()函式,其功能是呼叫10次colicTest()函式並求結果的平均值。為了使最終錯誤率更有說服力。
  • 從上面的結果可以看到10次迭代後的平均錯誤率為37.01%。事實上,這個結果並不差,因為樣本資料中實際上有30%的資料缺失。當然,如果調整colicTest()的迭代次數和stochGradAscent1()中的步長,平均錯誤率可以降到20%左右。
  • 當然後面如果我們有某些特徵值需要判斷,並使用該演算法預測時,我們只需再增加一個輸入各特徵值的函式,然後呼叫classifyVector(inX, weights)函式,就可以預測出某些特徵下疝病馬是否會死亡。

4.4 示例2:從打鬥數和接吻數預測電影型別(資料自制)

從kNN演算法裡面的小例子得到啟發,使用打鬥數和接吻數這兩個特徵最終預測得到的電影型別只有兩種:愛情片和動作片。所以符合Logistic迴歸演算法的使用標準。

特徵說明:打鬥數、接吻數

型別說明:1表示動作片;0表示愛情片

自制資料程式碼展示:

# !/usr/bin/python3
# -*- coding: utf-8 -*-
# @Time    : 2020/4/26 17:21
# @Author  : zjw
# @FileName: generateData.py
# @Software: PyCharm
# @Blog    :https://www.cnblogs.com/vanishzeng/


from numpy import *


def generateSomeData(fileName, num):
    trainFile = open(fileName, "w")
    for i in range(num):
        fightCount = float(int(random.uniform(0, 101)))
        kissCount = float(int(random.uniform(0, 101)))
        if fightCount > kissCount:
            label = 1  # 表示動作片
        else:
            label = 0  # 表示愛情片
        trainFile.write(str(fightCount) + "\t" + str(kissCount) + "\t" + str(float(label)) + "\n")
    trainFile.close()


if __name__ == '__main__':
    generateSomeData("data/movieTraining.txt", 200)
    generateSomeData("data/movieTest.txt", 100)

自制資料思路:通過隨機生成打鬥數和接吻數(數量在100以內),判斷當打鬥數大於接吻數時為動作片,記為1;當接吻數大於打鬥數時為愛情片,記為0。並將資料寫入相應的txt檔案中。這裡生成了200個訓練樣本和100個測試樣本。最後,還經過對生成的樣本資料,進行手動修改幾個類別,以達到更加實際現實的樣本資料。

樣本展示:

  • movieTraining.txt訓練樣本展示:

    image-20200428225219348

  • movieTest.txt測試樣本展示:

    image-20200428225207398

測試演算法:使用Logistic迴歸預測電影類別(關鍵程式碼展示)

def classifyVector(inX, weights):
    """
    使用梯度上升演算法獲取到的最優係數來計算測試樣本中對應的Sigmoid值。
    其中Sigmoid值大於0.5返回1,小於0.5返回0.
    :param inX: 特徵陣列
    :param weights: 最優係數
    :return: 返回分類結果,即1或0
    """

    prob = sigmoid(sum(inX * weights))
    if prob > 0.5:
        return 1.0
    else:
        return 0.0
    

def movieTest():
    """
    使用Logistic迴歸演算法測試判斷電影類別的錯誤率。
    :return: 錯誤率
    """
    trainFile = open("data/movieTraining.txt")
    testFile = open("data/movieTest.txt")
    trainSet = []
    trainLabels = []
    for line in trainFile.readlines():
        lineArr = line.strip().split('\t')
        trainSet.append([float(lineArr[0]), float(lineArr[1])])
        trainLabels.append(float(lineArr[2]))
    trainWeights = stocGradAscent1(array(trainSet), trainLabels, 500)
    errorCount = 0
    allTestCount = 0
    for line in testFile.readlines():
        allTestCount += 1
        lineArr = line.strip().split('\t')
        eigenvalue = [float(lineArr[0]), float(lineArr[1])]
        if classifyVector(eigenvalue, trainWeights) != float(lineArr[2]):
            errorCount += 1
    errorRate = float(errorCount)/float(allTestCount)
    print("錯誤率為:{:.2%}".format(errorRate))
    return errorRate


def multiTest():
    """
    多次測試演算法的錯誤率取平均值,以得到一個比較有說服力的結果。
    :return:
    """

    numTests = 10
    errorSum = 0.0
    # 通過10次的演算法測試,並獲得10次錯誤率的總和
    for k in range(numTests):
        errorSum += movieTest()
    # 通過錯誤率總和/10可以求得10次平均錯誤率並輸出
    print("10次演算法測試後平均錯誤率為:{:.2%}".format(errorSum/float(numTests)))

輸出:

if __name__ == '__main__':
    multiTest()

錯誤率為:11.00%
錯誤率為:6.00%
錯誤率為:5.00%
錯誤率為:6.00%
錯誤率為:5.00%
錯誤率為:10.00%
錯誤率為:4.00%
錯誤率為:7.00%
錯誤率為:14.00%
錯誤率為:11.00%
10次演算法測試後平均錯誤率為:7.90%

小結:

  • 這裡測試演算法的思路和上面的思路基本一致,只是對一些函式中的內容進行小修小補。
  • 因為這裡的資料是自制的,且特徵數量只有兩個,製作規則比較簡單,且手動修改的類別不多,所以最終得到的錯誤率並不高,演算法的測試結果是令人滿意的。

4.5 示例3:從心臟檢查樣本幫助診斷心臟病(資料來源於網路)

在這個例子中我們使用到一批心臟檢查樣本資料(資料來源於網路:Statlog (Heart) Data Set),這裡我將獲取到的270個樣本資料分成兩部分,一部分作為訓練樣本(heartTraining.txt)有200個樣本資料,一部分作為測試樣本(heartTest.txt)有70個樣本資料。資料各列的特徵(有13個特徵)和是否為心臟病如下圖所示:

image-20200429113726404

注:這裡中間有些特徵沒有標明,但這並不影響我們對資料的操作。

這裡為了使資料便於後面的操作,我將資料集中最後一列資料進行了修改,原來是1表示否,2表示是;修改後1表示是,0表示否。

重構資料集的程式碼如下:

# !/usr/bin/python3
# -*- coding: utf-8 -*-
# @Time    : 2020/4/29 9:54
# @Author  : zjw
# @FileName: modifyDataFile.py
# @Software: PyCharm
# @Blog    :https://www.cnblogs.com/vanishzeng/


def modifyData(fileName):
    file = open(fileName, 'r')
    allStr = []
    for line in file.readlines():
        arr = line.strip().split(' ')
        if arr[13] == '1':
            arr[13] = '0'
        else:
            arr[13] = '1'
        s = ''
        for i in range(14):
            s += arr[i] + '\t'
        s += '\n'
        allStr.append(s)
    file.close()
    newFile = open(fileName, 'w')
    newFile.writelines(allStr)
    newFile.close()


if __name__ == '__main__':
    modifyData('data/heartTest.txt')
    modifyData('data/heartTraining.txt')

重構思路:將文字檔案中的資料都讀取出來,然後進行切割,將資料中的最後一列中的值通過if判別進行替換。並將修改好後的列表轉化為一個個字串,存入到allStr這個總列表中。最後再以寫的方式開啟檔案,通過writelines()方法一次性將allStr列表寫入檔案中。

重構後的檔案資料集:

  • 訓練樣本(heartTraining.txt):

    image-20200429114456480

  • 測試樣本(heartTest.txt):

    image-20200429114510477

測試演算法:使用Logistic迴歸預測是否為心臟病(關鍵程式碼展示)

def classifyVector(inX, weights):
    """
    使用梯度上升演算法獲取到的最優係數來計算測試樣本中對應的Sigmoid值。
    其中Sigmoid值大於0.5返回1,小於0.5返回0.
    :param inX: 特徵陣列
    :param weights: 最優係數
    :return: 返回分類結果,即1或0
    """

    prob = sigmoid(sum(inX * weights))
    if prob > 0.5:
        return 1.0
    else:
        return 0.0
    

def heartTest():
    """
    使用Logistic迴歸演算法診斷心臟病的錯誤率。
    :return:錯誤率
    """

    trainFile = open("data/heartTraining.txt")
    testFile = open("data/heartTest.txt")
    trainSet = []
    trainLabels = []
    for line in trainFile.readlines():
        lineArr = line.strip().split(' ')
        temArr = []
        for i in range(13):
            temArr.append(float(lineArr[i]))
        trainSet.append(temArr)
        trainLabels.append(float(lineArr[13]))
    trainWeights = stocGradAscent1(array(trainSet), trainLabels, 100)
    errorCount = 0
    allTestCount = 0
    for line in testFile.readlines():
        allTestCount += 1
        lineArr = line.strip().split(' ')
        eigenvalue = []
        for i in range(13):
            eigenvalue.append(float(lineArr[i]))
        if classifyVector(eigenvalue, trainWeights) != float(lineArr[13]):
            errorCount += 1
    errorRate = float(errorCount) / float(allTestCount)
    print("錯誤率為:{:.2%}".format(errorRate))
    return errorRate


def multiTest():
    """
    多次測試演算法的錯誤率取平均值,以得到一個比較有說服力的結果。
    :return:
    """

    numTests = 10
    errorSum = 0.0
    # 通過10次的演算法測試,並獲得10次錯誤率的總和
    for k in range(numTests):
        errorSum += heartTest()
    # 通過錯誤率總和/10可以求得10次平均錯誤率並輸出
    print("10次演算法測試後平均錯誤率為:{:.2%}".format(errorSum/float(numTests)))

輸出:

if __name__ == '__main__':
    multiTest()

錯誤率為:18.57%
錯誤率為:17.14%
錯誤率為:15.71%
錯誤率為:31.43%
錯誤率為:24.29%
錯誤率為:12.86%
錯誤率為:21.43%
錯誤率為:12.86%
錯誤率為:32.86%
錯誤率為:22.86%
10次演算法測試後平均錯誤率為:21.00%

小結:

  • 可以看到測試演算法的錯誤率為21%,是一個還不錯的結果。因為這次得到的資料的特徵值較多,且特徵值之間也有所差別。可以通過增加迭代次數,以及調整步長進一步減低錯誤率。
  • 在資料處理方面,從三個例項可以看出,基本上是一致的,知識因為特徵值數量的不同,以及讀取檔案的不同,導致程式碼上有略微的區別。測試Logistic演算法的大致思路都是一致的,測試演算法思想:
    1. 讀取訓練樣本中的資料,進行格式化處理;
    2. 將格式化處理後的資料傳入隨機梯度上升演算法函式中,獲取到最佳引數
    3. 再讀取測試樣本中的資料,進行格式化處理後,呼叫分類器函式(傳入樣本特徵和最佳引數),可以預測出最終特徵。與測試資料中的實際特徵進行比較,計算出錯誤次數。
    4. 最終通過錯誤次數/測試樣本總數求出錯誤率。
    5. 為了使試驗結果具有說服力,使用了多次求解錯誤率取平均值的方法。
  • 經過上面的分析,我們可以看到在測試演算法時,我們用到了基本一致的步驟,所以想到寫幾個函式,可以將這些步驟統一起來,通過傳入某些引數來實現對不同特徵資料的分析和預測。

4.6 改進函式封裝使不同的樣本資料可以使用相同的函式封裝

改進函式展示:

def dataTest(trainFileName, testFileName, numOfFeatures):
    """
    函式功能:測試迴歸演算法預測資料樣本的錯誤率。
    函式虛擬碼:
        1. 讀取訓練樣本中的資料,進行格式化處理;
        2. 將格式化處理後的資料傳入隨機梯度上升演算法函式中,獲取到最佳引數。
        3. 再讀取測試樣本中的資料,進行格式化處理後,呼叫分類器函式(傳入樣本特徵和最佳引數),可以預測出最終特徵。與測試資料中的實際特徵進行比較,計算出錯誤次數。
        4. 最終通過錯誤次數/測試樣本總數**求出錯誤率。
    :param trainFileName: 訓練樣本的檔案路徑/檔名
    :param testFileName: 測試樣本的檔案路徑/檔名
    :param numOfFeatures: 樣本所包含的特徵數量
    :return:
    """

    trainFile = open(trainFileName)
    testFile = open(testFileName)
    trainSet = []
    trainLabels = []
    for line in trainFile.readlines():
        lineArr = line.strip().split('\t')
        temArr = []
        for i in range(numOfFeatures):
            temArr.append(float(lineArr[i]))
        trainSet.append(temArr)
        trainLabels.append(float(lineArr[numOfFeatures]))
    trainWeights = stocGradAscent1(array(trainSet), trainLabels, 200)
    errorCount = 0
    allTestCount = 0
    for line in testFile.readlines():
        allTestCount += 1
        lineArr = line.strip().split('\t')
        eigenvalue = []
        for i in range(numOfFeatures):
            eigenvalue.append(float(lineArr[i]))
        if classifyVector(eigenvalue, trainWeights) != float(lineArr[numOfFeatures]):
            errorCount += 1
    errorRate = float(errorCount) / float(allTestCount)
    print("錯誤率為:{:.2%}".format(errorRate))
    return errorRate


def multiTest1(trainFileName, testFileName, numOfFeatures):
    """
    多次測試演算法的錯誤率取平均值,以得到一個比較有說服力的結果。
    :return:
    """

    numTests = 10
    errorSum = 0.0
    # 通過10次的演算法測試,並獲得10次錯誤率的總和
    for k in range(numTests):
        errorSum += dataTest(trainFileName, testFileName, numOfFeatures)
    # 通過錯誤率總和/10可以求得10次平均錯誤率並輸出
    print("10次演算法測試後平均錯誤率為:{:.2%}".format(errorSum/float(numTests)))

輸出:

if __name__ == '__main__':
    print("示例1:示例1:從疝氣病症預測病馬的死亡率")
    multiTest1('data/horseColicTraining.txt', 'data/horseColicTest.txt', 21)
    print("\n示例2:從打鬥數和接吻數預測電影型別")
    multiTest1('data/movieTraining.txt', 'data/movieTest.txt', 2)
    print("\n示例3:從心臟檢查樣本幫助診斷心臟病")
    multiTest1('data/heartTraining.txt', 'data/heartTest.txt', 13)  

示例1:從疝氣病症預測病馬的死亡率
錯誤率為:32.84%
錯誤率為:29.85%
錯誤率為:29.85%
錯誤率為:37.31%
錯誤率為:29.85%
錯誤率為:34.33%
錯誤率為:31.34%
錯誤率為:29.85%
錯誤率為:29.85%
錯誤率為:31.34%
10次演算法測試後平均錯誤率為:31.64%

示例2:從打鬥數和接吻數預測電影型別
錯誤率為:14.00%
錯誤率為:6.00%
錯誤率為:14.00%
錯誤率為:3.00%
錯誤率為:6.00%
錯誤率為:11.00%
錯誤率為:5.00%
錯誤率為:11.00%
錯誤率為:5.00%
錯誤率為:5.00%
10次演算法測試後平均錯誤率為:8.00%

示例3:從心臟檢查樣本幫助診斷心臟病
錯誤率為:20.00%
錯誤率為:22.86%
錯誤率為:17.14%
錯誤率為:22.86%
錯誤率為:18.57%
錯誤率為:18.57%
錯誤率為:35.71%
錯誤率為:22.86%
錯誤率為:35.71%
錯誤率為:18.57%
10次演算法測試後平均錯誤率為:23.29%

小結:

  • 通過函式封裝後,就可以直接傳入相應的檔名和特徵數即可測試不同的樣本。解決了上面的程式碼冗餘。
  • 函式封裝的思路主要是對上面寫的測試函式進行重構,重構的位置有兩個地方:
    1. 將colicTest()、movieTest()、heartTest()三個函式統一為dataTest(trainFileName, testFileName, numOfFeatures)這個函式,增加了三個引數的目的是,原來三個函式中的不同的地方就是不同的檔案不同的特徵數量,所以以引數的形式傳遞進來,即使有所不同,但是以函式內引數形式呼叫即可實現相同的功能。
    2. 將原來的multiTest()函式重構為multiTest1(trainFileName, testFileName, numOfFeatures)這個函式,也是增加了和上面同樣的三個引數,主要是因為在這個函式中要呼叫dataTest()這個函式,所以需要需要通過multiTest1這個函式間接幫忙傳遞引數。
  • 通過該函式封裝後,後面當我們需要測試新的資料使,只要告訴我檔案所在位置和檔名,以及資料的特徵數量,我就可以呼叫multiTest1()函式很快的計算出錯誤率,無需再寫新的函式進行測試,極大的提高了效率

五、實驗總結

  1. Logistic迴歸演算法的目的是尋找一個非線性函式Sigmoid的最佳擬合引數,求解過程可以使用最優化演算法來完成。

  2. 最優化演算法中,最常用的是梯度上升演算法。梯度上升演算法可以簡化為效率比較高的隨機梯度上升演算法。

  3. 改進後的隨機梯度上升演算法的效果和梯度上升演算法效果相當,但是佔用更少的計算資源且效率更高。

  4. Sigmoid函式:

    Sigmoid函式公式

  5. 函式封裝很重要,可以解決程式碼冗餘問題,此外也可以提高開發效率,不必每次一有新資料,就要重新寫新的函式來滿足要求。好的程式碼封裝,後續只要呼叫相應的函式就可以完成指定的目標。

  6. numpy相關函式:

    1. mat():將陣列或列表轉為矩陣形式。
    2. mat.transpose():矩陣轉置

六、參考資料

  1. 《機器學習實戰》 Peter Harrington (作者) 李銳 , 李鵬 , 曲亞東 , 王斌 (譯者) 第2章 k-近鄰演算法
  2. 預測心臟病的資料來源:http://archive.ics.uci.edu/ml/datasets/Concrete+Compressive+Strength
  3. 機器學習程式碼實戰:使用邏輯迴歸幫助診斷心臟病
  4. Sigmoid函式
  5. 【機器學習筆記1】Logistic迴歸總結

版權宣告:歡迎轉載=>請標註資訊來源於 Vanish丶部落格園

相關文章