鄰近算法,或者說是K最鄰近算法,是一個相對簡單的多分類算法,其基本工作原理爲:
首先我們存在一個訓練集,訓練集中的每個圖片都存在標籤(已知圖片屬於哪一類).對於我們輸入的沒有標籤的數據,我們將新數據中的每個特徵與樣本集合中的數據的對應特徵進行比較,計算出二者之間的距離,然後記錄下與新數據距離最近的K個樣本,最後選擇K個數據當中類別最多的那一類作爲新數據的類別。
下面通過一個簡單的例子說明一下:如下圖,綠色圓要被決定賦予哪個類,是紅色三角形還是藍色四方形?如果K=3,由於紅色三角形所佔比例爲2/3,綠色圓將被賦予紅色三角形那個類,如果K=5,由於藍色四方形比例爲3/5,因此綠色圓被賦予藍色四方形類。
上面說到要計算二者之間的距離,那麼距離怎麼計算呢?這裏距離一般使用歐式距離,或者曼哈頓距離。
歐氏距離: $d(x,y) = \sqrt{ \sum_{k=1}^n (x_k - y_k)^2 } $
曼哈頓距離: $ d(x,y) = \sum_{i=1}^n |x_i - y_i| $
那麼在進行圖像分類的時候,兩個圖片之間的距離就是每個像素點距離的和。
KNN算法的流程
(1)計算測試數據與訓練數據之間的距離
(2)按照距離的遞增關係進行排序
(3)找出其中距離最近的K個數據
(4)確定K個點哪個類別出現的次數最多(概率最大)
(5)返回這K個點中類別出現頻率最高的作爲預測數據的類別
說明:
1)KNN算法不存在訓練數據的問題,他是一種懶惰學習,僅僅是把所有的數據保存下來,訓練時間開銷爲0
2)KNN算法的優點: 簡單、有效。計算時間和空間線性於訓練集的規模(在一些場合不算太大)
3)缺點:計算複雜度高,空間複雜度高。存儲開銷大,需要把所有數據都保存起來
當樣本不平衡時,如一個類的樣本容量很大,而其他類樣本容量很小時,有可能導致當輸入一個新樣本時,該樣本的K個鄰居中大容量類的樣本佔多數。
代碼實現:
import numpy as np
class KNN(object):
def __init__(self):
pass
def train(self,X,Y):
"""
Train the classifier .For k_nearest_neighbor this is just memorizing the train data.
Inputs:
- X: A numpy array of shape (num_train, D) containing the training data
consisting of num_train samples each of dimension D.
- y: A numpy array of shape (N,) containing the training labels, where
y[i] is the label for X[i].
"""
self.X_train = X
self.Y_train = Y
def predict(self,X,k = 1,num_loops = 0):
"""
Predict labels for test data using this classifier.
基於該分類器,預測測試數據的標籤分類。
Inputs:
- X: A numpy array of shape (num_test, D) containing test data consisting
of num_test samples each of dimension D.測試數據集
- k: The number of nearest neighbors that vote for the predicted labels.
- num_loops: Determines which implementation to use to compute distances
between training points and testing points.選擇距離算法的實現方法
Returns:
- y: A numpy array of shape (num_test,) containing predicted labels for the
test data, where y[i] is the predicted label for the test point X[i].
"""
if num_loops == 0:
dis = self.compute_no_loop(X)
elif num_loops == 1:
dis = self.compute_one_loop(X)
elif num_loops == 2:
dis = self.compute_two_loops(X)
else:
raise ValueError("Invalid value %d for num_loops" %num_loops)
return self.predict_label(dis,k=k)
def compute_two_loops(self,X):
"""
Compute the distance between each test point in X and each training point in self.X_train using a nested
loop over both trainging and the test data.(兩層for 計算距離,這裏計算歐幾里得距離)
"""
num_test = X.shape[0]
num_train = self.X_train.shape[0]
# dis[i,j] is the Euclidean distance between the ith test data and jth training data.
dis = np.zeros((num_test,num_train))
for i in range(num_test):
for j in range(num_train):
test_row = X[i,:] # choose this test row.
train_row = self.X_train[j,:]
dis[i,j] = np.sqrt(np.sum((test_row - train_row)**2))
return dis
def compute_one_loop(self,X):
"""
Compute the distance between each test point in X and each trainging point in X_train using a single loop
over the test data.
dis: the same as compute_two_loops
"""
num_test = X.shape[0]
num_train = self.X_train[0]
dis = np.zeros((num_test,num_train))
for i in range(num_test):
test_row = X[i,:]
# numpy 的broadcasting 機制
dis[i,:] = np.sqrt(np.sum(np.square(test_row - self.X_train),axis = 1))#axis=1 計算每一行的和,返回爲一個列表,正好存在第i行
return dis
def compute_no_loop(self,X):
"""
Compute the distance between each test point in X and each training point in X_train without using any
loops.
"""
num_test = X.shape[0]
num_train = self.X_train.shape[0]
dis = np.zeros((num_test,num_train))
X_square = np.square(X).sum(axis = 1) # 1 * 500
X_train_square = np.square(self.X_train).sum(axis = 1) # 1 * 5000
dis = np.sqrt(-2*np.dot(X,self.X_train.T) + np.matrix(X_square).T + X_train_square) # 500 * 5000
dis = np.array(dis)
return dis
def predict_label(self,dis,k = 1):
"""
Given a matrix of distances between test points and training points,
predict a label for each test point.
"""
num_test = dis.shape[0]
# y_pred Return the label of ith test data.
y_pred = np.zeros(num_test)
for i in range(num_test):
closest_y = []
closest_y = self.Y_train[np.argsort(dis[i,:])[:k]]
# np.argsort 函數返回的是數組值從小到大的索引值(按從小到大的順序返回每個索引)
y_pred[i] = np.argmax(np.bincount(closest_y))
# np.argmax 返回最大值,np.bincount()統計每一個元素出現次數的
return y_pred
PS:上面計算兩個圖片之間的距離用到了三種方法,一是兩層循環,一個是一層循環,還有一個是不用循環。最後兩個主要是用到了numpy的廣播機制。
numpy的廣播機制
廣播(broadcasting)指的是不同形狀的數組之間的算數運算的執行方式。
1.數組與標量值的乘法:
import numpy as np
arr = np.arange(5)
arr #-> array([0, 1, 2, 3, 4])
arr * 4 #-> array([ 0, 4, 8, 12, 16])
在這個乘法運算中,標量值4被廣播到了其他所有元素上。
2.通過減去列平均值的方式對數組每一列進行距平化處理
arr = np.random.randn(4,3)
arr #-> array([[ 1.83518156, 0.86096695, 0.18681254],
# [ 1.32276051, 0.97987486, 0.27828887],
# [ 0.65269467, 0.91924574, -0.71780692],
# [-0.05431312, 0.58711748, -1.21710134]])
arr.mean(axis=0) #-> array([ 0.93908091, 0.83680126, -0.36745171])
關於mean中的axis參數,個人是這麼理解的:
在numpy中,axis = 0爲行軸(豎直方向),axis = 1爲列軸(水平方向),指定axis表示該操作沿axis進行,得到結果將是一個shape爲除去該axis的array。
在上例中,arr.mean(axis=0)表示對arr沿着軸0(豎直方向)求均值,即求列均值。而arr含有3列,所以結果含有3個元素,這與上面的結論相符。
demeaned = arr - arr.mean(axis=0)
demeaned
> array([[ 0.89610065, 0.02416569, 0.55426426],
[ 0.3836796 , 0.1430736 , 0.64574058],
[-0.28638623, 0.08244448, -0.35035521],
[-0.99339402, -0.24968378, -0.84964963]])
demeaned.mean(axis=0)
> array([ -5.55111512e-17, -5.55111512e-17, 0.00000000e+00])
廣播原則:
如果兩個數組的後緣維度(從末尾開始算起的維度)的軸長度相符或其中一方的長度爲1,則認爲它們是廣播兼容的。廣播會在缺失維度和(或)軸長度爲1的維度上進行。
3.各行減去行均值
row_means = arr.mean(axis=1)
row_means.shape
> (4,)
arr - row_means
> ---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-10-3d1314c7e700> in <module>()
----> 1 arr - row_means
ValueError: operands could not be broadcast together with shapes (4,3) (4,)
直接相減,報錯,無法進行廣播。回顧上面的原則,要麼滿足後緣維度軸長度相等,要麼滿足其中一方長度爲1。在這個例子中,兩者均不滿足,所以報錯。根據廣播原則,較小數組的廣播維必須爲1。
解決方案是爲較小的數組添加一個長度爲1的新軸。
numpy提供了一種通過索引機制插入軸的特殊語法。通過特殊的np.newaxis屬性以及“全”切片來插入新軸。
上面的例子中,我們希望實現二維數組各行減去行均值,我們需要你將行均值沿着水平方向進行廣播,廣播軸爲axis=1,對arr.mean(1)添加一個新軸axis=1
row_means[:,np.newaxis].shape
> (4, 1)
arr - row_means[:,np.newaxis]
> array([[ 0.87419454, -0.10002007, -0.77417447],
[ 0.46245243, 0.11956678, -0.58201921],
[ 0.36798351, 0.63453458, -1.00251808],
[ 0.17378588, 0.81521647, -0.98900235]])