圖像分類與KNN


1 圖像分類問題

1.1 什么是圖像分類

所謂圖像分類問題,就是已有固定的分類標簽集合,然后對於輸入的圖像,從分類標簽集合中找出一個分類標簽,最后把分類標簽分配給該輸入圖像。雖然看起來挺簡單的,但這可是計算機視覺領域的核心問題之一,並且有着各種各樣的實際應用。計算機視覺領域中很多看似不同的問題(比如物體檢測和分割),都可以被歸結為圖像分類問題。

舉個例子體會一下:

以下圖為例,圖像分類模型讀取該圖片,並生成該圖片屬於集合 {cat, dog, hat, mug}中各個標簽的概率。需要注意的是,對於計算機來說,圖像是一個由數字組成的巨大的3維數組。在這個例子中,貓的圖像大小是寬248像素,高400像素,有3個顏色通道,分別是紅、綠和藍(簡稱RGB)。如此,該圖像就包含了248X400X3=297600個數字,每個數字都是在范圍0-255之間的整型,其中0表示全黑,255表示全白。我們的任務就是把這些上百萬的數字變成一個簡單的標簽,比如“貓”。

圖像分類的任務,就是對於一個給定的圖像,預測它屬於的那個分類標簽(或者給出屬於一系列不同標簽的可能性)。圖像是3維數組,數組元素是取值范圍從0到255的整數。數組的尺寸是寬度x高度x3,其中這個3代表的是紅、綠和藍3個顏色通道。

1.2 圖像分類任務的難點

對於人來說,識別出一個像“貓”一樣視覺概念是簡單至極的,然而從計算機視覺算法的角度來看就值得深思了。我們在下面列舉了計算機視覺算法在圖像識別方面遇到的一些困難,要記住圖像是以3維數組來表示的,數組中的元素是亮度值。

視角變化(Viewpoint variation):同一個物體,攝像機可以從多個角度來展現。
大小變化(Scale variation):物體可視的大小通常是會變化的(不僅是在圖片中,在真實世界中大小也是變化的)。
形變(Deformation):很多東西的形狀並非一成不變,會有很大變化。
遮擋(Occlusion):目標物體可能被擋住。有時候只有物體的一小部分(可以小到幾個像素)是可見的。
光照條件(Illumination conditions):在像素層面上,光照的影響非常大。
背景干擾(Background clutter):物體可能混入背景之中,使之難以被辨認。
類內差異(Intra-class variation):一類物體的個體之間的外形差異很大,比如椅子。這一類物體有許多不同的對象,每個都有自己的外形。

1.3 圖像分類的方法及流程

1.3.1 方法

如何寫一個圖像分類的算法呢?這和寫個排序算法可是大不一樣。怎么寫一個從圖像中認出貓的算法?好像不太可行。因此,與其在代碼中直接寫明各類物體到底看起來是什么樣的,倒不如說我們采取和教小孩兒看圖識物類似的方法:給計算機很多數據,然后實現學習算法,讓計算機學習到每個類的外形。這種方法,就是數據驅動方法

1.3.2 流程

圖像分類就是輸入一個元素為像素值的數組,然后給它分配一個分類標簽。完整流程如下:

輸入:輸入是包含N個圖像的集合,每個圖像的標簽是K種分類標簽中的一種。這個集合稱為訓練集。
學習:這一步的任務是使用訓練集來學習每個類到底長什么樣。一般該步驟叫做訓練分類器或者學習一個模型。
評價:讓分類器來預測它未曾見過的圖像的分類標簽,並以此來評價分類器的質量。我們會把分類器預測的標簽和圖像真正的分類標簽對比。毫無疑問,分類器預測的分類標簽和圖像真正的分類標簽如果一致,那就是好事,這樣的情況越多越好。

2 最近鄰分類器

我們來實現一個Nearest Neighbor分類器。雖然這個分類器和卷積神經網絡沒有任何關系,實際中也極少使用,但通過實現它,可以讓讀者對於解決圖像分類問題的方法有個基本的認識。

2.1 CIFAR10

一個非常流行的圖像分類數據集是CIFAR-10。這個數據集包含了60000張32X32的小圖像。每張圖像都有10種分類標簽中的一種。這60000張圖像被分為包含50000張圖像的訓練集和包含10000張圖像的測試集。在下圖中你可以看見10個類的10張隨機圖片。

左邊:從CIFAR-10數據庫來的樣本圖像。右邊:第一列是測試圖像,然后第一列的每個測試圖像右邊是使用Nearest Neighbor算法,根據像素差異,從訓練集中選出的10張最類似的圖片。

2.2 基於最近鄰的簡單圖像類別判決

假設現在我們有CIFAR-10的50000張圖片(每種分類5000張)作為訓練集,我們希望將余下的10000作為測試集並給他們打上標簽。Nearest Neighbor算法將會拿着測試圖片和訓練集中每一張圖片去比較,然后將它認為最相似的那個訓練集圖片的標簽賦給這張測試圖片。上面右邊的圖片就展示了這樣的結果。請注意上面10個分類中,只有3個是准確的。比如第8行中,馬頭被分類為一個紅色的跑車,原因在於紅色跑車的黑色背景非常強烈,所以這匹馬就被錯誤分類為跑車了。

那么具體如何比較兩張圖片呢?在本例中,就是比較32x32x3的像素塊。最簡單的方法就是逐個像素比較,最后將差異值全部加起來。換句話說,就是將兩張圖片先轉化為兩個向量\(I_{1}\)\(I_{2}\),然后計算他們的L1距離:

$d_{1}\left(I_{1}, I_{2}\right)=\sum_{p}\left|I_{1}^{p}-I_{2}^{p}\right|$

這里的求和是針對所有的像素。下面是整個比較流程的圖例:

以圖片中的一個顏色通道為例來進行說明。兩張圖片使用L1距離來進行比較。逐個像素求差值,然后將所有差值加起來得到一個數值。如果兩張圖片一模一樣,那么L1距離為0,但是如果兩張圖片很是不同,那L1值將會非常大。

計算向量間的距離有很多種方法,另一個常用的方法是L2距離,從幾何學的角度,可以理解為它在計算兩個向量間的歐式距離。L2距離的公式如下:

$d_{2}\left(I_{1}, I_{2}\right)=\sqrt{\sum_{p}\left(I_{1}^{p}-I_{2}^{p}\right)^{2}}$

比較這兩個度量方式是挺有意思的。在面對兩個向量之間的差異時,L2比L1更加不能容忍這些差異。也就是說,相對於1個巨大的差異,L2距離更傾向於接受多個中等程度的差異。

在cifar10數據集上實現最近鄰算法:

# -*- coding: utf-8 -*-
#! /usr/bin/env python
#coding=utf-8

import os
import pickle
import numpy as np

def load_CIFAR_batch(filename):
    """
    cifar-10數據集是分batch存儲的,這是載入單個batch

    @參數 filename: cifar文件名
    @r返回值: X, Y: cifar batch中的 data 和 labels
    """

    with open(filename,'rb') as f:
        datadict=pickle.load(f,encoding='bytes')

        X=datadict[b'data']
        Y=datadict[b'labels']

        X=X.reshape(10000, 3, 32, 32).transpose(0,2,3,1).astype("float")
        Y=np.array(Y)

        return X, Y


def load_CIFAR10(ROOT):
    """
    讀取載入整個 CIFAR-10 數據集

    @參數 ROOT: 根目錄名
    @return: X_train, Y_train: 訓練集 data 和 labels
             X_test, Y_test: 測試集 data 和 labels
    """

    xs=[]
    ys=[]

    for b in range(1,6):
        f=os.path.join(ROOT, "data_batch_%d" % (b, ))
        X, Y=load_CIFAR_batch(f)
        xs.append(X)
        ys.append(Y)

    X_train=np.concatenate(xs)
    Y_train=np.concatenate(ys)

    del X, Y

    X_test, Y_test=load_CIFAR_batch(os.path.join(ROOT, "test_batch"))

    return X_train, Y_train, X_test, Y_test

# 載入訓練和測試數據集
X_train, Y_train, X_test, Y_test = load_CIFAR10('data/cifar10/') 
# 把32*32*3的多維數組展平
Xtr_rows = X_train.reshape(X_train.shape[0], 32 * 32 * 3) # Xtr_rows : 50000 x 3072
Xte_rows = X_test.reshape(X_test.shape[0], 32 * 32 * 3) # Xte_rows : 10000 x 3072

class NearestNeighbor:
  def __init__(self):
    pass

  def train(self, X, y):
    """ 
    這個地方的訓練其實就是把所有的已有圖片讀取進來 -_-||
    """
    # the nearest neighbor classifier simply remembers all the training data
    self.Xtr = X
    self.ytr = y

  def predict(self, X):
    """ 
    所謂的預測過程其實就是掃描所有訓練集中的圖片,計算距離,取最小的距離對應圖片的類目
    """
    num_test = X.shape[0]
    # 要保證維度一致哦
    Ypred = np.zeros(num_test, dtype = self.ytr.dtype)

    # 把訓練集掃一遍 -_-||
    for i in range(num_test):
      # 計算l1距離,並找到最近的圖片
      distances = np.sum(np.abs(self.Xtr - X[i,:]), axis = 1)
      min_index = np.argmin(distances) # 取最近圖片的下標
      Ypred[i] = self.ytr[min_index] # 記錄下label

    return Ypred

nn = NearestNeighbor() # 初始化一個最近鄰對象
nn.train(Xtr_rows, Y_train) # 訓練...其實就是讀取訓練集
Yte_predict = nn.predict(Xte_rows) # 預測
# 比對標准答案,計算准確率
print ('accuracy: %f' % ( np.mean(Yte_predict == Y_test) ))

如果你用這段代碼跑CIFAR-10,你會發現准確率能達到38.6%。這比隨機猜測的10%要好,但是比人類識別的水平(據研究推測是94%)和卷積神經網絡能達到的95%還是差多了。

注:python3實現的

3 K最近鄰分類器

你可能注意到了,為什么只用最相似的1張圖片的標簽來作為測試圖像的標簽呢?這不是很奇怪嗎!是的,使用k-Nearest Neighbor分類器就能做得更好。它的思想很簡單:與其只找最相近的那1個圖片的標簽,我們找最相似的k個圖片的標簽,然后讓他們針對測試圖片進行投票,最后把票數最高的標簽作為對測試圖片的預測。所以當k=1的時候,k-Nearest Neighbor分類器就是Nearest Neighbor分類器。從直觀感受上就可以看到,更高的k值可以讓分類的效果更平滑,使得分類器對於異常值更有抵抗力。

上面示例展示了Nearest Neighbor分類器和5-Nearest Neighbor分類器的區別。例子使用了2維的點來表示,分成3類(紅、藍和綠)。不同顏色區域代表的是使用L2距離的分類器的決策邊界。白色的區域是分類模糊的例子(即圖像與兩個以上的分類標簽綁定)。需要注意的是,在NN分類器中,異常的數據點(比如:在藍色區域中的綠點)制造出一個不正確預測的孤島。5-NN分類器將這些不規則都平滑了,使得它針對測試數據的泛化(generalization)能力更好(例子中未展示)。注意,5-NN中也存在一些灰色區域,這些區域是因為近鄰標簽的最高票數相同導致的(比如:2個鄰居是紅色,2個鄰居是藍色,還有1個是綠色)。

k-NN分類器需要設定k值,那么選擇哪個k值最合適的呢?我們可以選擇不同的距離函數,比如L1范數和L2范數等,那么選哪個好?還有不少選擇我們甚至連考慮都沒有考慮到(比如:點積)。所有這些選擇,被稱為超參數(hyperparameter)。在基於數據進行學習的機器學習算法設計中,超參數是很常見的。一般說來,這些超參數具體怎么設置或取值並不是顯而易見的。

你可能會建議嘗試不同的值,看哪個值表現最好就選哪個。好主意!我們就是這么做的,但這樣做的時候要非常細心。特別注意:決不能使用測試集來進行調優。當你在設計機器學習算法的時候,應該把測試集看做非常珍貴的資源,不到最后一步,絕不使用它。如果你使用測試集來調優,而且算法看起來效果不錯,那么真正的危險在於:算法實際部署后,性能可能會遠低於預期。這種情況,稱之為算法對測試集過擬合。從另一個角度來說,如果使用測試集來調優,實際上就是把測試集當做訓練集,由測試集訓練出來的算法再跑測試集,自然性能看起來會很好。這其實是過於樂觀了,實際部署起來效果就會差很多。所以,最終測試的時候再使用測試集,可以很好地近似度量你所設計的分類器的泛化性能。

好在我們有不用測試集調優的方法。其思路是:從訓練集中取出一部分數據用來調優,我們稱之為驗證集(validation set)。以CIFAR-10為例,我們可以用49000個圖像作為訓練集,用1000個圖像作為驗證集。驗證集其實就是作為假的測試集來調優。下面就是代碼:

# 假定已經有Xtr_rows, Ytr, Xte_rows, Yte了,其中Xtr_rows為50000*3072 矩陣
Xval_rows = Xtr_rows[:1000, :] # 構建1000的交叉驗證集
Yval = Y_train[:1000]
Xtr_rows = Xtr_rows[1000:, :] # 保留49000的訓練集
Ytr = Y_train[1000:]

# 設置一些k值,用於試驗
validation_accuracies = []
for k in [1, 3, 5, 7, 10, 20, 50, 100]:

    nn = NearestNeighbor() # 初始化一個最近鄰對象
    nn.train(Xtr_rows, Ytr) # 訓練...其實就是讀取訓練集
    Yval_predict = nn.predict(Xval_rows,k=k) # 預測
    # 比對標准答案,計算准確率
    acc = np.mean(Yval_predict == Yval)
    print ('accuracy: %f' % (acc,))

    # 輸出結果
    validation_accuracies.append((k, acc))

關於上述的代碼,預測階段加入了新參數k,所以,修改predict函數如下:

def predict(self, X, k=1):
    """ 
    所謂的預測過程其實就是掃描所有訓練集中的圖片,計算距離,取最小的距離對應圖片的類目
    """
    num_test = X.shape[0]
    # 要保證維度一致哦
    Ypred = np.zeros(num_test, dtype = self.ytr.dtype)

    # 把訓練集掃一遍 -_-||
    for i in range(num_test):
      closest_y = []
      # 計算l1距離,並找到最近的圖片
      distances = np.sum(np.abs(self.Xtr - X[i,:]), axis = 1)
      a = np.argsort(distances)[:k]#從小到大按照索引排序
      closest_y = self.ytr[a]
      np.sort(closest_y)#從小到大排序,改變原列表的值的順序
      y =  np.bincount(closest_y)#統計各個元素出現的次數
      z = np.argmax(y)#結合上一步,得出出現最多的數值(整數時成立)
      Ypred[i] = z # 記錄下label

    return Ypred

程序結束后,我們會作圖分析出哪個k值表現最好,然后用這個k值來跑真正的測試集,並作出對算法的評價。

4 最近鄰分類器的優劣

首先,Nearest Neighbor分類器易於理解,實現簡單。其次,算法的訓練不需要花時間,因為其訓練過程只是將訓練集數據存儲起來。然而測試要花費大量時間計算,因為每個測試圖像需要和所有存儲的訓練圖像進行比較,這顯然是一個缺點。在實際應用中,我們關注測試效率遠遠高於訓練效率。其實,我們后續要學習的卷積神經網絡在這個權衡上走到了另一個極端:雖然訓練花費很多時間,但是一旦訓練完成,對新的測試數據進行分類非常快。這樣的模式就符合實際使用需求。

Nearest Neighbor分類器在某些特定情況(比如數據維度較低)下,可能是不錯的選擇。但是在實際的圖像分類工作中,很少使用。因為圖像都是高維度數據(他們通常包含很多像素),而高維度向量之間的距離通常是反直覺的。

上圖中,右邊3張圖片和左邊第1張原始圖片的L2距離是一樣的。很顯然,基於像素比較的相似和感官上以及語義上的相似是不同的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM