1. 程式人生 > >機器學習實戰筆記5—支援向量機

機器學習實戰筆記5—支援向量機

注:此係列文章裡的部分演算法和深度學習筆記系列裡的內容有重合的地方,深度學習筆記裡是看教學視訊做的筆記,此處文章是看《機器學習實戰》這本書所做的筆記,雖然演算法相同,但示例程式碼有所不同,多敲一遍沒有壞處,哈哈。(裡面用到的資料集、程式碼可以到網上搜索,很容易找到。)。Python版本3.6

機器學習十大算法系列文章:

機器學習實戰筆記1—k-近鄰演算法

機器學習實戰筆記2—決策樹

機器學習實戰筆記3—樸素貝葉斯

機器學習實戰筆記4—Logistic迴歸

機器學習實戰筆記5—支援向量機

機器學習實戰筆記6—AdaBoost

機器學習實戰筆記7—K-Means​​​​​​​

此係列原始碼在我的GitHub裡:https://github.com/yeyujujishou19/Machine-Learning-In-Action-Codes

一,演算法原理:

參考上一篇文章:深度學習基礎課程1筆記-支援向量機(SVM)

二,演算法的優缺點:

優點:

1)對於線性不可分的情況可以通過核函式,對映到高維特徵空間實現線性可分。

2)SVM學習問題可以表示為凸優化問題,因此可以利用已知的有效演算法發現目標函式的全域性最小值。而其他分類方法(如基於規則的分類器和人工神經網路)都採用一種基於貪心學習的策略來搜尋假設空間,這種方法一般只能獲得區域性最優解。

3)小叢集分類效果好。

缺點:

1)SVM僅僅只限於一個二類分類問題,對於多分類問題解決效果並不好。

2)僅侷限於小叢集樣本,對於觀測樣本太多時,效率較低。

3)尋求合適的核函式相對困難。

三,例項程式碼:

SMO演算法:是一種用於訓練SVM的強大演算法,它將大的優化問題分解為多個小的優化問題來進行求解。而這些小優化問題往往很容易求解,並且對它們進行順序求解和對整體求解結果是一致的。在結果一致的情況下,顯然SMO演算法的求解時間要短很多,這樣當資料集容量很大時,SMO就是一致十分高效的演算法。

SMO演算法的目標是找到一系列alpha和b,而求出這些alpha,我們就能求出權重w,這樣就能得到分隔超平面,從而完成分類任務

SMO演算法的工作原理是:每次迴圈中選擇兩個alpha進行優化處理。一旦找到一對合適的alpha,那麼就增大其中一個而減少另外一個。這裡的"合適",意味著在選擇alpha對時必須滿足一定的條件,條件之一是這兩個alpha不滿足最優化問題的kkt條件,另外一個條件是這兩個alpha還沒有進行區間化處理。

SMO演算法推導

下面是簡易版SMO演算法需要用到的一些功能,我們將其包裝成函式,需要時呼叫即可:

from numpy import *
from time import sleep
import matplotlib.pyplot as plt
import numpy as np

#SMO演算法相關輔助中的輔助函式
# 1 解析文字資料函式,
# 提取每個樣本的特徵組成向量,新增到資料矩陣
# 新增樣本標籤到標籤向量
def loadDataSet(fileName):               #檔案路徑
    dataMat = []; labelMat = []  #資料列表和標籤列表
    fr = open(fileName)          #開啟檔案
    for line in fr.readlines():  #逐行讀取
        lineArr = line.strip().split('\t')  #去除首尾空格,並拆分字串
        dataMat.append([float(lineArr[0]), float(lineArr[1])]) #將第一二列加入資料列表
        labelMat.append(float(lineArr[2])) #將第三列加入標籤列表
    return dataMat,labelMat

# 函式說明:資料視覺化
# dataMat - 資料矩陣
# labelMat - 資料標籤
# Returns:無
def showDataSet(dataMat, labelMat):
    data_plus = []                                  #正樣本
    data_minus = []                                 #負樣本
    for i in range(len(dataMat)):
        if labelMat[i] > 0:
            data_plus.append(dataMat[i])
        else:
            data_minus.append(dataMat[i])
    data_plus_np = np.array(data_plus)              #轉換為numpy矩陣
    data_minus_np = np.array(data_minus)            #轉換為numpy矩陣
    plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1])   #正樣本散點圖
    plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1]) #負樣本散點圖
    plt.show()


#2 在樣本集中採用隨機選擇的方法選取第二個不等於第一個alphai的
#優化向量alphaj
def selectJrand(i,m):
    j=i
    while (j==i):
        j = int(random.uniform(0,m))
    return j

#3 約束範圍L<=alphaj<=H內的更新後的alphaj值
def clipAlpha(aj,H,L):
    if aj > H: 
        aj = H
    if L > aj:
        aj = L
    return aj

dataMat, labelMat=loadDataSet('testSet.txt')
showDataSet(dataMat, labelMat)

程式碼結果:

 

SMO演算法的虛擬碼:

#SMO演算法的虛擬碼
#建立一個alpha向量並將其初始化為0向量
#當迭代次數小於最大迭代次數時(w外迴圈)
    #對資料集中每個資料向量(內迴圈):
    #如果該資料向量可以被優化:
        #隨機選擇另外一個數據向量
        #同時優化這兩個向量
        #如果兩個向量都不能被優化,退出內迴圈
#如果所有向量都沒有被優化,增加迭代次數,繼續下一次迴圈

 SMO演算法的程式碼:

#dataMat    :資料列表
#classLabels:標籤列表
#C          :權衡因子(增加鬆弛因子而在目標優化函式中引入了懲罰項)
#toler      :容錯率
#maxIter    :最大迭代次數
def smoSimple(dataMatIn, classLabels, C, toler, maxIter):
    dataMatrix = mat(dataMatIn); labelMat = mat(classLabels).transpose() # 將列表形式轉為矩陣或向量形式
    b = 0; m,n = shape(dataMatrix)   # 初始化b=0,獲取矩陣行列
    alphas = mat(zeros((m,1)))       # 初始化alpha引數,設為0
    iter = 0                         #迭代次數為0
    while (iter < maxIter):          #當迭代次數小於最大迭代次數時(w外迴圈)
        alphaPairsChanged = 0        #改變的alpha對數
        for i in range(m):           #遍歷樣本集中樣本
            # 步驟1:計算誤差Ei
            fXi = float(multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[i,:].T)) + b #計算支援向量機演算法的預測值
            Ei = fXi - float(labelMat[i])#計算預測值與實際值的誤差
            #優化alpha,更設定一定的容錯率。
            if ((labelMat[i]*Ei < -toler) and (alphas[i] < C)) or ((labelMat[i]*Ei > toler) and (alphas[i] > 0)):
                j = selectJrand(i,m) #隨機選擇另一個與alpha_i成對優化的alpha_j
                # 步驟1:計算誤差Ej
                fXj = float(multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[j,:].T)) + b #計算第二個變數對應資料的預測值
                Ej = fXj - float(labelMat[j]) #計算與測試與實際值的差值
                alphaIold = alphas[i].copy(); alphaJold = alphas[j].copy(); #記錄alphai和alphaj的原始值,便於後續的比較
                # 步驟2:計算上下界L和H
                if (labelMat[i] != labelMat[j]):  #如果兩個alpha對應樣本的標籤不相同
                    L = max(0, alphas[j] - alphas[i])  #求出相應的上下邊界
                    H = min(C, C + alphas[j] - alphas[i])
                else:
                    L = max(0, alphas[j] + alphas[i] - C)
                    H = min(C, alphas[j] + alphas[i])
                if L==H: print ("L==H"); continue
                # 根據公式計算未經剪輯的alphaj
                # ------------------------------------------
                eta = 2.0 * dataMatrix[i,:]*dataMatrix[j,:].T - dataMatrix[i,:]*dataMatrix[i,:].T - dataMatrix[j,:]*dataMatrix[j,:].T
                # 如果eta>=0,跳出本次迴圈
                if eta >= 0: print ("eta>=0"); continue
                # 步驟4:更新alpha_j
                alphas[j] -= labelMat[j]*(Ei - Ej)/eta
                # 步驟5:修剪alpha_j
                alphas[j] = clipAlpha(alphas[j],H,L)
                # ------------------------------------------
                # 如果改變後的alphaj值變化不大,跳出本次迴圈
                if (abs(alphas[j] - alphaJold) < 0.00001): print ("alpha_j變化太小"); continue
                # 步驟6:更新alpha_i
                alphas[i] += labelMat[j]*labelMat[i]*(alphaJold - alphas[j])
                # 步驟7:更新b_1和b_2
                b1 = b - Ei- labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i,:]*dataMatrix[i,:].T - labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[i,:]*dataMatrix[j,:].T
                b2 = b - Ej- labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i,:]*dataMatrix[j,:].T - labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[j,:]*dataMatrix[j,:].T
                # 步驟8:根據b_1和b_2更新b
                if (0 < alphas[i]) and (C > alphas[i]): b = b1
                # 否則如果0<alphai<C,那麼b=b1
                elif (0 < alphas[j]) and (C > alphas[j]): b = b2
                # 否則,alphai,alphaj=0或C
                else: b = (b1 + b2)/2.0
                # 統計優化次數
                alphaPairsChanged += 1
                print("第%d次迭代 樣本:%d, alpha優化次數:%d" % (iter, i, alphaPairsChanged))
        # 最後判斷是否有改變的alpha對,沒有就進行下一次迭代
        if (alphaPairsChanged == 0): iter += 1
        # 否則,迭代次數置0,繼續迴圈
        else: iter = 0
        print("迭代次數: %d" % iter)
    # 返回最後的b值和alpha向量
    return b,alphas

# 函式說明:分類結果視覺化
# dataMat - 資料矩陣
# w - 直線法向量
# b - 直線解決
# Returns:無
def showClassifer(dataMat, w, b):
    #繪製樣本點
    data_plus = []                                  #正樣本
    data_minus = []                                 #負樣本
    for i in range(len(dataMat)):
        if labelMat[i] > 0:
            data_plus.append(dataMat[i])
        else:
            data_minus.append(dataMat[i])
    data_plus_np = np.array(data_plus)              #轉換為numpy矩陣
    data_minus_np = np.array(data_minus)            #轉換為numpy矩陣
    plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1], s=30, alpha=0.7)   #正樣本散點圖
    plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1], s=30, alpha=0.7) #負樣本散點圖
    #繪製直線
    x1 = max(dataMat)[0]
    x2 = min(dataMat)[0]
    a1, a2 = w
    b = float(b)
    a1 = float(a1[0])
    a2 = float(a2[0])
    y1, y2 = (-b- a1*x1)/a2, (-b - a1*x2)/a2
    plt.plot([x1, x2], [y1, y2])
    #找出支援向量點
    for i, alpha in enumerate(alphas):
        if abs(alpha) > 0:
            x, y = dataMat[i]
            plt.scatter([x], [y], s=150, c='none', alpha=0.7, linewidth=1.5, edgecolor='red')
    plt.show()


# 函式說明:計算w
# dataMat - 資料矩陣
# labelMat - 資料標籤
# alphas - alphas值
# Returns:無
def get_w(dataMat, labelMat, alphas):
    alphas, dataMat, labelMat = np.array(alphas), np.array(dataMat), np.array(labelMat)
    w = np.dot((np.tile(labelMat.reshape(1, -1).T, (1, 2)) * dataMat).T, alphas)
    return w.tolist()

#測試
dataMat, labelMat = loadDataSet('testSet.txt')
b,alphas = smoSimple(dataMat, labelMat, 0.6, 0.001, 40)
w = get_w(dataMat, labelMat, alphas)
showClassifer(dataMat, w, b)

 程式碼結果:

 其中,中間的藍線為求出來的分類器,用紅圈圈出的點為支援向量點。

四,SVM例項:手寫識別問題

       儘管KNN也能取得不錯的效果;但是從節省記憶體的角度出發,顯然SVM演算法更勝一籌,因為其不需要儲存整個資料集,而只需要起作用的支援向量點,分類效果也不錯。

#例項:手寫識別問題
#支援向量機由於只需要儲存支援向量,所以相對於KNN儲存整個資料集佔用更少記憶體
#且取得可比的效果

#基於svm的手寫數字識別
def loadImages(dirName):
    from os import listdir
    hwLabels=[]
    trainingFileList=listdir(dirName)
    m=len(trainingFileList)
    trainingMat=zeros((m,1024))
    for i in range(m):
        fileNameStr=trainingFileList[i]
        fileStr=fileNameStr.split('.')[0]
        classNumStr=int(fileStr.split('_')[0])
        if classNumStr==9:hwLabels.append(-1)
        else:hwLabels.append(1)
        trainingMat[i,:]=img2vector('%s/%s',%(dirName,fileNameStr))
    return hwLabels,trainingMat

#將影象轉為向量
def img2vector(fileaddir):
    featVec=zeros((1,1024))
    fr=open(filename)
    for i in range(32):
        lineStr=fr.readline()
        for j in range(32):
            featVec[0,32*i+j]=int(lineStr[j])
    return featVec
    
#利用svm測試數字
def testDigits(kTup=('rbf',10)):
    #訓練集
    dataArr,labelArr=loadDataSet('trainingDigits')
    b,alphas=smoP(dataArr,labelArr,200,0.0001,10000,kTup)
    dataMat=mat(dataArr);labelMat=mat(labelArr).transpose()
    svInd=nonzero(alphas.A>0)[0]
    sVs=dataMat[svInd]
    labelSV=labelMat[svInd]
    print("there are %d Support Vectors",%shape(sVs)[0])
    m,n=shape(dataMat)
    errorCount=0
    for i in range(m):
        kernelEval=kernelTrans(sVs,dataMat[i,:],kTup)
        predict=kernelEval.T*multiply(labelSV,alphas[svInd])+b
        if sign(predict)!=sign(labelArr[i]):errorCount+=1
    print("the training error rate is: %f",%(float(errorCount)/m))
    #測試集
    dataArr,labelArr=loadDataSet('testDigits.txt')
    dataMat=mat(dataArr);labelMat=mat(labelArr).transpose()
    errorCount=0
    m,n=shape(dataMat)
    for i in range(m):
        kernelEval=kernelTrans(sVs,dataMat[i,:],('rbf',k1))
        predict=kernelEval.T*multiply(labelSV,alphas[svInd])+b
        if sign(predict)!=sign(labelArr[i]):errorCount+=1
    print("the training error rate is: %f",%(float(errorCount)/m))

#測試
testDigits(kTup=('rbf', 10))

程式碼結果:

歡迎掃碼關注我的微信公眾號