1. 程式人生 > >番外篇2.3 影象處理與深度學習

番外篇2.3 影象處理與深度學習

在談R-CNN之前,應該要先總結一下模式識別。
模式識別主要是對已知資料樣本的特徵發現和提取,比如人臉識別、雷達訊號識別等,強調從原始資訊中提取有價值的特徵,在機器學習裡面,好的特徵所帶來的貢獻有時候遠遠大於演算法本身的貢獻。在番外篇中,我們使用過opencv中已經訓練好的分類器,這也是模式識別的一種。
模式識別從處理問題的性質和解決問題的方法角度,可以分為有監督(分類)與無監督(聚類)兩種。二者的主要差別在於,樣本是否有labels。一般說來,有監督的分類往往需要提供大量帶有labels的樣本,但在實際問題中,這是比較困難的,因此研究無監督的分類十分重要。(當然,模式識別已經是比較老的技術,當有監督的分類有用武之地時,更有效的神經網路已經出現了,生不逢時呀)

1.1 分類

1.1.1 K-Nearest Neighbour

首先就是K最近鄰(K-NN),是一種比較簡單,直觀的分類演算法,也是計算量很大的一種演算法。該方法的思路是:如果一個樣本在特徵空間中的k個最相似(即特徵空間中最鄰近)的樣本中的大多數屬於某一個類別,則該樣本也屬於這個類別。特別的,當k = 1時,該物件被分到離它最近的鄰居所在的類中。不過這種演算法有個缺點是,它對資料的區域性結構敏感,容易過度擬合數據。作為簡單的防止過擬合的方法一般都是賦給更近的鄰居更大的權重,比如距離的倒數。
引數k的選擇很依賴資料,k越大,越容易忽視噪聲,但邊界也會更模糊。一般可以用不同的k值來訓練k-NN分類器,然後驗證哪個效果最好。
如果是分兩類,k最好選擇一個奇數,這樣不容易產生某一個點模稜兩可的情況。
還有,這個分類器對輸入特徵值域敏感,如果引入無關的特徵會降低分類的準確性。可以將資料歸一化到[0, 1]或[-1, 1]的區間內來防止。

1.1.2 決策樹演算法

決策樹演算法的核心,是根據先驗條件構造一顆最好的決策樹,來預測類別。
舉一個例子,一個公司評判員工有兩個標準,一個是聽話程度,一個是聰明程度;
我們靠這兩個引數來區分是否是好員工;

聰明 聽話 好員工
8 2 no
4 8 yes
8 5 yes
2 6 no

我們有幾種方法來做區分。以一種為例:用聽話程度來區分:
聽話>=5 是樹的左子樹,聽話<4是樹的右葉子節點,右葉子節點均為壞員工;
再對左子樹進行區分,聰明>3為左子樹,聰明<=為右子樹,左子樹均為好員工,右子樹均為壞員工;這樣就完成了一種決策樹。那如何判斷決策樹的好壞呢?
這種判斷好壞的引數就是,資訊熵增益。如果經過某個屬性,劃分後的資料資訊熵下降最多,那麼這種劃分為最優。
如我現在用的方法,第一次劃分前熵為:
-(2/4*log2(2/4)+2/4*log2(2/4)) = 1;
劃分後右子節點熵為0,左子節點熵:
-(2/3*log2(2/3)+1/3*log2(1/3))= 0.39+0.526 = 0.916;這個熵下降就是0.084。
第二次劃分後,區分完畢,熵為0。(舉的例子太弟弟了,不太明顯,不過還算容易懂啦)
經過決策屬性的劃分後,資料的無序度越來越低,也就是資訊熵越來越小。
具體就不細說啦 我們這可以說影象處理的,有點跑偏了。。順便說一下,基於規則的分類器和這個大致相同,有興趣的可以自己搜,就不在這裡佔用版面了。

1.1.3 貝葉斯分類

說貝葉斯,首先要說樸素貝葉斯。樸素貝葉斯如其名,非常樸素。核心思想就是對於待分類項,求解各個類別出現的概率,然後哪個概率最大,這個分類項就是那個類別。
當然,這個概率獲得才是NB的核心。一般我們需要一個訓練樣本集,統計一下條件概率估計,然後如果每個條件對概率的影響互相不相關,我們就可以算出測試集中某一個樣本的各類別概率,從而順利分類了。樸素貝葉斯一個很重要的點就是每個條件對概率的影響互相不相關。然而這個在現實中幾乎不可能,所以貝葉斯分類又有了新的成員:貝葉斯網路。類似決策樹(不過這個樹是事先確定而不是訓練),貝葉斯在每個子葉片上(全部特徵區分後的)計算概率,然後用這個概率作為測試集中某一個樣本按照條件走下來之後的概率。 貝葉斯網路比樸素貝葉斯更復雜,而想構造和訓練出一個好的貝葉斯網路更是異常艱難。但是貝葉斯網路是模擬人的認知思維推理模式,用一組條件概率函式以及有向無環圖對不確定性的因果推理關係建模,因此其具有更高的實用價值。
貝葉斯網路要首先確定隨機變數之間的拓撲結構。這也是一個容易出問題的環節。

1.1.4 支援向量機(SVM)

這才是重頭戲嘛!偷別人一張圖說明下:
這裡寫圖片描述
c1,c2分別是兩個類,如何把這兩個類區分開呢?就是在中間畫條線(廢話)
不過這個線其實很有講究,畫在哪裡,角度如何呢?
這就是svm演算法所研究的核心,核心思想是:讓最近的樣本點距離這個超平面最遠(有點小繞)。那麼,我們就有了目標函式:min(樣本點i到超平面的距離)。
然後我們改變超平面的引數(這圖上是線性的,就只有k,b兩個),創造很多超平面,得到max(min(樣本點i到超平面j的距離))。
這是一個凸二次規劃問題,用拉格朗日對偶法可以解得。網上很多求解過程,看著都累,就不搬上來了,自己搜搜看。當這種方法可以區分的比較好,但是有異常點怎麼辦呢?svm演算法引入了鬆弛變數的概念,給這個線性超平面一個可以改變的偏置,下面的點只要在這個偏置最大的時候被分類就可以,上面同理。當然,在計算超平面的時候,也要保證這個鬆弛變數儘量小。(SMO演算法)
當樣本完全線性不可分的時候怎麼辦呢?SVM又引入了一個概念:核函式。在我的理解例,核函式就是一個對映,把線性不可分的樣本對映到一個線性可分的座標系中,然後再用線性可分的方法來區分。核函式一般有多項式核和高斯核,除了特殊情況,一般高斯核被使用的最多,因為比較靈活啦。。
感覺也沒啥好說的,不過好像蠻重要的誒,說一下python實現的思路:

from numpy import * 

def loadDataSet(filename): #讀取資料
    dataMat=[]
    labelMat=[]
    fr=open(filename)
    for line in fr.readlines():
        lineArr=line.strip().split(' ')
        dataMat.append([float(lineArr[0]),float(lineArr[1])])
        labelMat.append(float(lineArr[2]))
    return dataMat,labelMat #返回資料特徵和資料類別

def selectJrand(i,m): #在0-m中隨機選擇一個不是i的整數
    j=i
    while (j==i):
        j=int(random.uniform(0,m))
    return j

def clipAlpha(aj,H,L):  #保證a在L和H範圍內(L <= a <= H)
    if aj>H:
        aj=H
    if L>aj:
        aj=L
    return aj

def kernel(X, A, kTup): #核函式,輸入引數,X:支援向量的特徵樹;A:某一行特徵資料;kTup:('lin',k1)核函式的型別和引數
    m,n = shape(X)
    K = mat(zeros((m,1)))
    if kTup[0]=='lin': #線性函式
        K = X * A.T
    elif kTup[0]=='rbf': # 徑向基函式(radial bias function)
        for j in range(m):
            deltaRow = X[j,:] - A
            K[j] = deltaRow*deltaRow.T
        K = exp(K/(-1*kTup[1]**2)) #返回生成的結果
    return K


#定義類,方便儲存資料
class optStruct:
    def __init__(self,dataMatIn, classLabels, C, toler, kTup):  # 儲存各類引數
        self.X = dataMatIn  #資料特徵
        self.labelMat = classLabels #資料類別
        self.C = C #軟間隔引數C,引數越大,非線性擬合能力越強
        self.tol = toler #停止閥值
        self.m = shape(dataMatIn)[0] #資料行數
        self.alphas = mat(zeros((self.m,1)))
        self.b = 0 #初始設為0
        self.eCache = mat(zeros((self.m,2))) #快取
        self.K = mat(zeros((self.m,self.m))) #核函式的計算結果
        for i in range(self.m):
            self.K[:,i] = kernel(self.X, self.X[i,:], kTup)


def calcEk(oS, k): #計算Ek
    fXk = float(multiply(oS.alphas,oS.labelMat).T*oS.K[:,k] + oS.b)
    Ek = fXk - float(oS.labelMat[k])
    return Ek

#隨機選取aj,並返回其E值
def selectJ(i, oS, Ei):
    maxK = -1
    maxDeltaE = 0
    Ej = 0
    oS.eCache[i] = [1,Ei]
    validEcacheList = nonzero(oS.eCache[:,0].A)[0]  #返回矩陣中的非零位置的行數
    if (len(validEcacheList)) > 1:
        for k in validEcacheList:
            if k == i:
                continue
            Ek = calcEk(oS, k)
            deltaE = abs(Ei - Ek)
            if (deltaE > maxDeltaE): #返回步長最大的aj
                maxK = k
                maxDeltaE = deltaE
                Ej = Ek
        return maxK, Ej
    else:
        j = selectJrand(i, oS.m)
        Ej = calcEk(oS, j)
    return j, Ej


def updateEk(oS, k): #更新os資料
    Ek = calcEk(oS, k)
    oS.eCache[k] = [1,Ek]

#首先檢驗ai是否滿足KKT條件,如果不滿足,隨機選擇aj進行優化,更新ai,aj,b值
def innerL(i, oS): #輸入引數i和所有引數資料
    Ei = calcEk(oS, i) #計算E值
    if ((oS.labelMat[i]*Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or ((oS.labelMat[i]*Ei > oS.tol) and (oS.alphas[i] > 0)): #檢驗這行資料是否符合KKT條件 參考《統計學習方法》p128公式7.111-113
        j,Ej = selectJ(i, oS, Ei) #隨機選取aj,並返回其E值
        alphaIold = oS.alphas[i].copy()
        alphaJold = oS.alphas[j].copy()
        if (oS.labelMat[i] != oS.labelMat[j]): 
            L = max(0, oS.alphas[j] - oS.alphas[i])
            H = min(oS.C, oS.C + oS.alphas[j] - oS.alphas[i])
        else:
            L = max(0, oS.alphas[j] + oS.alphas[i] - oS.C)
            H = min(oS.C, oS.alphas[j] + oS.alphas[i])
        if L==H:
            print("L==H")
            return 0
        eta = 2.0 * oS.K[i,j] - oS.K[i,i] - oS.K[j,j] 
        if eta >= 0:
            print("eta>=0")
            return 0
        oS.alphas[j] -= oS.labelMat[j]*(Ei - Ej)/eta 
        oS.alphas[j] = clipAlpha(oS.alphas[j],H,L) 
        updateEk(oS, j)
        if (abs(oS.alphas[j] - alphaJold) < oS.tol): 
            print("j not moving enough")
            return 0
        oS.alphas[i] += oS.labelMat[j]*oS.labelMat[i]*(alphaJold - oS.alphas[j])
        updateEk(oS, i) #更新資料
        b1 = oS.b - Ei- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.K[i,i] - oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.K[i,j]
        b2 = oS.b - Ej- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.K[i,j]- oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.K[j,j]
        if (0 < oS.alphas[i]<oS.C):
            oS.b = b1
        elif (0 < oS.alphas[j]<oS.C):
            oS.b = b2
        else:
            oS.b = (b1 + b2)/2.0
        return 1
    else:
        return 0


#SMO函式,用於快速求解出alpha
def smoP(dataMatIn, classLabels, C, toler, maxIter,kTup=('lin', 0)): #輸入引數:資料特徵,資料類別,引數C,閥值toler,最大迭代次數,核函式(預設線性核)
    oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler, kTup)
    iter = 0
    entireSet = True
    alphaPairsChanged = 0
    while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):
        alphaPairsChanged = 0
        if entireSet:
            for i in range(oS.m): #遍歷所有資料
                alphaPairsChanged += innerL(i,oS)
                print("fullSet, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)) #顯示第多少次迭代,那行特徵資料使alpha發生了改變,這次改變了多少次alpha
            iter += 1
        else:
            nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]
            for i in nonBoundIs: #遍歷非邊界的資料
                alphaPairsChanged += innerL(i,oS)
                print("non-bound, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged))
            iter += 1
        if entireSet:
            entireSet = False
        elif (alphaPairsChanged == 0):
            entireSet = True
        print("iteration number: %d" % iter)
    return oS.b,oS.alphas

def testRbf(data_train,data_test):
    dataArr,labelArr = loadDataSet(data_train) #讀取訓練資料
    b,alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, ('rbf', 1.3)) #通過SMO演算法得到b和alpha
    datMat=mat(dataArr)
    labelMat = mat(labelArr).transpose()
    svInd=nonzero(alphas)[0]  #選取不為0資料的行數(也就是支援向量)
    sVs=datMat[svInd] #支援向量的特徵資料
    labelSV = labelMat[svInd] #支援向量的類別(1或-1)
    print("there are %d Support Vectors" % shape(sVs)[0]) #打印出共有多少的支援向量
    m,n = shape(datMat) #訓練資料的行列數
    errorCount = 0
    for i in range(m):
        kernelEval = kernel(sVs,datMat[i,:],('rbf', 1.3)) #將支援向量轉化為核函式
        predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b  #這一行的預測結果,注意最後確定的分離平面只有那些支援向量決定。
        if sign(predict)!=sign(labelArr[i]): #sign函式 -1 if x < 0, 0 if x==0, 1 if x > 0
            errorCount += 1
    print("the training error rate is: %f" % (float(errorCount)/m)) #打印出錯誤率
    dataArr_test,labelArr_test = loadDataSet(data_test) #讀取測試資料
    errorCount_test = 0
    datMat_test=mat(dataArr_test)
    labelMat = mat(labelArr_test).transpose()
    m,n = shape(datMat_test)
    for i in range(m): #在測試資料上檢驗錯誤率
        kernelEval = kernel(sVs,datMat_test[i,:],('rbf', 1.3))
        predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b
        if sign(predict)!=sign(labelArr_test[i]):
            errorCount_test += 1
    print("the test error rate is: %f" % (float(errorCount_test)/m))

#主程式
def demo1():
    filename_traindata='C:\\Users\\Administrator\\Desktop\\data\\traindata.txt'
    filename_testdata='C:\\Users\\Administrator\\Desktop\\data\\testdata.txt'
    testRbf(filename_traindata,filename_testdata)

if __name__=='__main__':
    demo1()

1.2 聚類

接下來說無監督的模式識別,也就是聚類。
聚類最著名,經典的應該就是k-means了,其他還有DBSCAN(基於密度)、CLIQUE(基於網路)、FCM(模糊c均值)等等等等,太多了,先說k-means,其他後面看心情吧,反正又不搞資料探勘(目前),再說再說

1.2.1 k-means演算法

k-means是一種基於距離聚類的演算法。
k-means的演算法核心思想是:
1.先選擇k個點作為質心;
2.再把每個點指派到距離最近的質心,每進行一次,都重新計算一次每個簇的質心;
一直到簇的質心不發生變化或者達到最大迭代次數為止。
這個演算法有一個很重要的問題,就是k值怎麼定,k個質心選在哪裡。
k值的話,對於可以確定K值不會太大但不明確精確的K值的場景,可以進行迭代運算,然後找出代價函式最小時所對應的K值,這個值往往能較好的描述有多少個簇類。 (不過,也不是最小的時候對應的k值最好,當代價函式下降的不明顯時,就可以選擇那個k值(肘部法則))
此外,還有用層次聚類方法預估計k值的,用canopy演算法進行初始劃分的,在這裡先不仔細提。(簡單說一下,canopy演算法是先將相似的物件放到一個子集中,canopy可以互相重疊;對每個canopy可以使用傳統的聚類。)
還有的方法是選取小樣本(比如10000箇中選取1000個)來預估計k值和質心的位置,目的都是一樣,減少聚類時的計算量並優化結果(Mini Batch k-Means)。
對於初始質心的選取,最常見的就是隨機,多次取,找到最好的值。這種策略簡單,但計算量大,取決於資料集和簇的個數。
還有就是在取小樣本的時候,可以用隨機選一個點,然後選離重心最遠的點作為下一個質點;然後再重複到合適位置。
還有一個方法,就是用層次聚類的方法提取幾個簇,作為初始質心。當樣本較小,且k值相對較小的時候才合適。
當然,用canopy演算法也可以。。
k-means演算法的優點在於,原理簡單,超參只有一個k值,調節簡單;擴充套件性強。速度還可以。
缺點在於,對k值太敏感,對初始質心位置敏感,對離群點敏感;當樣本量大,時間開銷非常大;不能處理所有型別的簇。
改進型有各種,比如bisecting K-means(二分k均值); K-modes;K-prototype等。。這裡就不列舉了。。

1.2.2 DBSCAN

1.首先確定半徑r和minPoints. 從一個沒有被訪問過的任意資料點開始,以這個點為中心,r為半徑的圓內包含的點的數量是否大於或等於minPoints,如果大於或等於minPoints則改點被標記為central point,反之則會被標記為noise point。
2.重複1的步驟,如果一個noise point存在於某個central point為半徑的圓內,則這個點被標記為邊緣點,反之仍為noise point。重複步驟1,知道所有的點都被訪問過。
優點:不需要知道簇的數量;可以發現任何形狀的簇;可以找出異常點。
缺點:需要確定距離r和minPoints,調參較為複雜;如果簇間密度不同,不合適;樣本集太大時,收斂時間較長。

1.2.3 CLIQUE

CLIQUE是基於網格的聚類演算法。首先先以一個步長(引數1)分割整個域,然後掃描每個網格內的樣本數目,判斷是否是密集網格(閾值:引數2)。
然後就把聚類問題化簡為類似空間內求01連通域問題,每一個聯通域就是一個簇。
優點:高效;單遍資料掃描就可以完成目的。
缺點;兩個引數,調參較為複雜;對不同密度的簇不合適;對於高維空間,效果會很不好。

1.2.4 基於GMM的EM聚類

GMM首先假設了資料點是呈高斯分佈的,就像K-means假定資料點是圓分佈。高斯分佈是橢圓的,所以要更靈活一些。
要做聚類就要找到樣本的均值和標準差,這時採用EM演算法,過程是:
1. 首先選擇簇的數量並隨機初始化每個簇的高斯分佈引數(期望和方差)。也可以先觀察資料給出一個相對精確的均值和方差。
2. 給定每個簇的高斯分佈,計算每個資料點屬於每個簇的概率。一個點越靠近高斯分佈的中心就越可能屬於該簇。
3. 基於這些概率我們計算高斯分佈引數使得資料點的概率最大化,可以使用資料點概率的加權來計算這些新的引數,權重就是資料點屬於該簇的概率。
4. 重複迭代2和3直到在迭代中的變化不大。
GMMs的優點:GMMs使用均值和標準差,簇可以呈現出橢圓形而不是僅僅限制於圓形;GMMs是使用概率,所有一個數據點可以屬於多個簇。例如資料點X可以有百分之20的概率屬於A簇,百分之80的概率屬於B簇。
行了行了 又說多了,本來想說影象檢測的東西來著,一搞模式識別說了一大堆。。模式識別還有很多其他的方式,比如層次識別啊 模糊均值啊 請大家動動手指去百度吧(其實我有些也是百度來的,發出來是為了讓自己更有印象)
影象檢測下一節總結!