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

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

本實驗代碼已經傳到gitee上,請點擊查收!

Logistic_Examples

一、實驗目的

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

二、實驗內容與設計思想

實驗內容

  1. 基于Logistic回歸和Sigmoid函數分類
  2. 基于最優化方法的最佳回歸系數確定
  3. 示例1:從疝氣病癥預測病馬的死亡率
  4. 示例2:從打斗數和接吻數預測電影類型(數據自制)
  5. 示例3:從心臟檢查樣本幫助診斷心臟?。〝祿碓从诰W絡)
  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    :http://www.005223.buzz/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:從心臟檢查樣本幫助診斷心臟?。〝祿碓从诰W絡)

在這個例子中我們使用到一批心臟檢查樣本數據(數據來源于網絡: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    :http://www.005223.buzz/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回歸預測是否為心臟?。P鍵代碼展示)

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丶博客園

posted @ 2020-04-29 17:33  vanish丶  閱讀(...)  評論(...編輯  收藏
美人江湖手游