kNN算法概述
kNN算法是比較好理解,也比較容易編寫的分類算法。
簡單地說,kNN算法采用測量不同特征值之間的距離方法進行分類。
我們可以假設在一個N維空間中有很多個點,然后這些點被分為幾個類。相同類的點,肯定是聚集在一起的,它們之間的距離相比於和其他類的點來說,非常近。如果現在有個新的點,我們不知道它的類別,但我們知道了它的坐標,那只要計算它和已存在的所有點的距離,然后以最近的k個點的多數類作為它的類別,則完成了它的分類。這個k就是kNN中的k值。
舉個例子:我們知道地球是有經緯度的,中國人肯定絕大多數都集中在中國的土地上,美國人也一樣多數都集中在自己的土地上。如果現在給我們某個人的坐標,讓我們給它分類,判斷他是哪國人。我們計算了他和世界上每個人的距離,然后取離他最近的k個人中最多國別的國別作為他的國別。這樣我們就完成了他的國別分類。(當然也有可能一個外國人正好來中國游玩,我們錯誤的將他分類為中國人了,這個只是舉例,不要在意這些細節啦 ^_^)
所以kNN算法無非就是計算一個未知點與所有已經點的距離,然后根據最近的k個點類別來判斷它的類別。簡單,粗暴,實用。
kNN算法的重點
既然我們已經了解kNN算法了,那我們應該也大概了解到這個算法的重點是什么了
(1)怎么度量鄰近度
我們首先想到的肯定是點和點之間距離。但除了距離,其實我們也可以考慮兩個點之間的相似度,越相似,就代表兩個點距離越近。同理,我們也可以考慮相異度,越相異,就代表兩個點距離越遠。其實距離的度量就是相異性度量的其中一種。
(2)k值怎么取
k值的選取關乎整個分類器的性能。如果k值取得過小,容易受噪點的影響而導致分類錯誤。而k值取得過大,又容易分類不清,混淆了其他類別的點。
(3)數據的預處理
拿到數據,我們不能直接就開始套用算法,而是需要先規范數據。例如我們想通過一個人的年齡和工資來進行分類,很明顯工資的數值遠大於年齡,如果我們不對它進行一個統一的規范,必然工資這個特征會左右我們的分類,而讓年齡這個特征無效化,這不是我們想看到的。
鄰近度的度量
臨近度的度量,主要考慮相似性和相異性的度量。
一般的,我們把相似度定義為s,常常在0(不相似)和1(完全相似)之間取值。而相異度d有時在0(不相異)和1(完全相異)之間取值,有時也在0和∞之間取值。
當相似度(相異度)落在區間[0,1]之間時,我們可以定義d = 1 - s(或 s = 1 - d)。另一種簡單的方法是定義相似度為負的相異度(或相反)。
通常,具有若干屬性的對象之間的鄰近度用單個屬性的鄰近度的組合來定義,下圖是單個屬性的對象之間的鄰近度。
下面我們討論更復雜的涉及多個屬性的對象之間的鄰近性度量
1、距離
一維、二維、三維或高緯空間中兩個點x和y之間的歐幾里得距離(Euclidean distance)$d$由如下公式定義:
$$d(\mathbf{x},\mathbf{y})=\sqrt{\sum_{k=1}^{n}(x_{k}-y_{k})^{2}}$$
其中,$n$是維數,而$x_{k}$和$y_{k}$分別是x和y的第$k$個屬性值(分量)。
歐幾里得距離是最常用的距離公式。距離對特征都是區間或比率的對象非常有效。
2、二元數據的相似性度量
兩個僅包含二元屬性的對象之間的相似性度量也稱為相似系數(similarity coefficient),並且通常在0和1直接取值,值為1表明兩個對象完全相似,而值為0表明對象一點也不相似。
設x和y是兩個對象,都由n個二元屬性組成。這樣的兩個對象(即兩個二元向量)的比較可生成如下四個量(頻率):
$f_{00}=\mathbf{x}取0並且\mathbf{y}取0的屬性個數$
$f_{01}=\mathbf{x}取0並且\mathbf{y}取1的屬性個數$
$f_{10}=\mathbf{x}取1並且\mathbf{y}取0的屬性個數$
$f_{11}=\mathbf{x}取1並且\mathbf{y}取1的屬性個數$
簡單匹配系數(Simple Matching Coefficient,SMC)一種常用的相似性系數是簡單匹配系數,定義如下:
$$SMC=\frac{f_{11}+f_{00}}{f_{01}+f_{10}+f_{11}+f_{00}}$$
該度量對出現和不出現都進行計數。因此,SMC可以在一個僅包含是非題的測驗中用來發現回答問題相似的學生。
Jaccard系數(Jaccard Coefficient)假定x和y是兩個數據對象,代表一個事務矩陣的兩行(兩個事務)。如果每個非對稱的二元屬性對應於商店的一種商品,則1表示該商品被購買,而0表示該商品未被購買。由於未被顧客購買的商品數遠大於被其購買的商品數,因而像SMC這樣的相似性度量將會判定所有的事務都是類似的。這樣,常常使用Jaccard系數來處理僅包含非對稱的二元屬性的對象。Jaccard系數通常用符號J表示,由如下等式定義:
$$J=\frac{匹配的個數}{不設計0-0匹配的屬性個數}=\frac{f_{11}}{f_{01}+f_{10}+f_{11}}$$
3、余弦相似度
文檔的相似性度量不僅應當像Jaccard度量一樣需要忽略0-0匹配,而且還必須能夠處理非二元向量。下面定義的余弦相似度(cosine similarity)就是文檔相似性最常用的度量之一。如果x和y是兩個文檔向量,則
$$\cos(\mathbf{x},\mathbf{y})=\frac{\mathbf{x}\cdot\mathbf{y}}{|| \mathbf{x} || || \mathbf{y} ||}$$
其中,“▪”表示向量點積,$\mathbf{x}\cdot\mathbf{y}=\sum_{k=1}^{n}x_{k}y_{k}$,$||\mathbf{x}||$是向量x的長度,$||\mathbf{x}||=\sqrt{\sum_{k=1}^{n}x_{k}^{2}}=\sqrt{\mathbf{x}\cdot\mathbf{x}}$
余弦相似度公式還可以寫為:
$$\cos(\mathbf{x},\mathbf{y})=\frac{\mathbf{x}}{||\mathbf{x}||}\cdot\frac{\mathbf{y}}{||\mathbf{y}||}=\mathbf{x}^{'}\cdot\mathbf{y}^{'}$$
x和y被它們的長度除,將它們規范化成具有長度1。這意味在計算相似度時,余弦相似度不考慮兩個數據對象的量值。(當量值是重要的時,歐幾里得距離可能是一種更好的選擇)
余弦相似度為1,則x和y之間夾角為0度,x和y是相同的;如果余弦相似度為0,則x和y之間的夾角為90度,並且它們不包含任何相同的詞。
4、廣義Jaccard系數
廣義Jaccard系數可以用於文檔數據,並在二元屬性情況下歸約為Jaccard系數。該系數用EJ表示:
$$EJ(\mathbf{x},\mathbf{y})=\frac{\mathbf{x}\cdot\mathbf{y}}{||\mathbf{x}||^{2}+||\mathbf{y}||^{2}-\mathbf{x}\cdot\mathbf{y}}$$
知道度量的方法后,我們還要考慮實際的鄰近度計算問題
1、距離度量的標准化和相關性
距離度量的一個重要問題是當屬性具有不同的值域時如何處理(這種情況通常稱作“變量具有不同的尺度”)。前面,使用歐幾里得距離,基於年齡和收入兩個屬性來度量人之間的距離。除非這兩個屬性是標准化的,否則兩個人之間的距離將被收入所左右。
一個相關的問題是,除值域不同外,當某些屬性之間還相關時,如何計算距離。當屬性相關、具有不同的值域(不同的方差)、並且數據分布近似高斯(正態)分布時,歐幾里得距離的拓廣,Mahalanobis距離是有用。
$$mahalanobis(\mathbf{x},\mathbf{y})=(\mathbf{x}-\mathbf{y})\sum^{-1}(\mathbf{x}-\mathbf{y})^{T}$$
其中$\sum^{-1}$是數據協方差矩陣的逆。注意,協方差矩陣$\sum$是這樣的矩陣,它的第$ij$個元素是第$i$個和第$j$個屬性的協方差。
計算Mahalanobis距離的費用昂貴,但是對於其屬性相關的對象來說是值得的。如果屬性相對來說不相關,只是具有不同的值域,則只需要對變量進行標准化就足夠了。
一般采用$d^{'}=(d-d_{min})/(d_{max}-d_{min})$來變化歐幾米得距離的特征值域。
2、組合異種屬性的相似度
前面的相似度定義所基於的方法都假定所有屬性具有相同類型。當屬性具有不同類型時,就需要更一般的方法。直截了當的方法是使用上文的表分別計算出每個屬性之間的相似度,然后使用一種導致0和1之間相似度的方法組合這些相似度。總相似度一般定義為所有屬性相似度的平均值。
不幸的是,如果某些屬性是非對稱屬性,這種方法效果不好。處理該問題的最簡單方法是:如果兩個對象在非對稱屬性上的值都是0,則在計算對象相似度時忽略它們。類似的方法也能很好地處理遺漏值。
概括地說,下面的算法可以有效地計算具有不同類型屬性的兩個對象x和y之間的相似度。修改該過程可以很輕松地處理相異度。
算法:
1:對於第$k$個屬性,計算相似度$s_{k}(\mathbf{x},\mathbf{y})$,在區間[0,1]中
2:對於第$k$個屬性,定義一個指示變量$\delta_{k}$,如下:
$\delta_{k}=0$,如果第$k$個屬性是非對稱屬性,並且兩個對象在該屬性上的值都是0,或者如果一個對象的第$k$個屬性具有遺漏值
$\delta_{k}=0$,否則
3:使用如下公式計算兩個對象之間的總相似度:
$$similarity(\mathbf{x},\mathbf{y})=\frac{\sum_{k=1}^{n}\delta_{k}s_{k}(\mathbf{x},\mathbf{y})}{\sum_{k=1}^{n}\delta_{k}}$$
3、使用權值
在前面的大部分討論中,所有的屬性在計算臨近度時都會被同等對待。但是,當某些屬性對臨近度的定義比其他屬性更重要時,我們並不希望這種同等對待的方式。為了處理這種情況,可以通過對每個屬性的貢獻加權來修改臨近度公式。
如果權$w_{k}$的和為1,則上面的公式變成:
$$similarity(\mathbf{x},\mathbf{y})=\frac{\sum_{k=1}^{n}w_{k}\delta_{k}s_{k}(\mathbf{x},\mathbf{y})}{\sum_{k=1}^{n}\delta_{k}}$$
歐幾里得距離的定義也可以修改為:
$$d(\mathbf{x},\mathbf{y})=(\sum_{k=1}^{n}w_{k}|x_{k}-y_{k}|^{2})^{1/2}$$
算法代碼
算法偽碼:
(1)計算已知類別數據集中的點與當前點之間的距離;
(2)按照距離遞增次序排序;
(3)選取與當前點距離最小的k個點;
(4)確定前k個點所在類別的出現頻率;
(5)返回前k個點出現頻率最高的類別作為當前點的預測分類。
具體代碼
以下代碼都是博主根據自己的理解寫的,因為才開始學習不久,如有代碼的錯誤和冗余,請見諒,並同時歡迎指出,謝謝!
博主主要是根據Pandas和Numpy庫來編寫的,閱讀的同學可能需要有一點這方面的基礎。Pandas和Numpy庫都是處理數據分析的最佳庫,要想學好數據分析,還是需要好好學習這兩個庫的。
算法采用的是歐幾里得距離,采用$d^{'}=(d-d_{min})/(d_{max}-d_{min})$來規范特征值
# -*- coding: utf-8 -*- """kNN最近鄰算法最重要的三點: (1)確定k值。k值過小,對噪聲非常敏感;k值過大,容易誤分類 (2)采用適當的臨近性度量。對於不同的類型的數據,應考慮不同的度量方法。除了距離外,也可以考慮相似性。 (3)數據預處理。需要規范數據,使數據度量范圍一致。 """ import pandas as pd import numpy as np class kNN: def __init__(self,X,y=None,test='YES'): """參數X為訓練樣本集,支持list,array和DataFrame; 參數y為類標號,支持list,array,Series 默認參數y為空值,表示類標號字段沒有單獨列出來,而是存儲在數據集X中的最后一個字段; 參數y不為空值時,數據集X中不能含有字段y 參數test默認為'YES',表是將原訓練集拆分為測試集和新的訓練集 """ if isinstance(X,pd.core.frame.DataFrame) != True: #將數據集轉換為DataFrame格式 self.X = pd.DataFrame(X) else: self.X = X if y is None: #將特征和類別分開 self.y = self.X.iloc[:,-1] self.X = self.X.iloc[:,:-1] self.max_data = np.max(self.X,axis=0) #獲取每個特征的最大值,為下面規范數據用 self.min_data = np.min(self.X,axis=0) #獲取每個特征的最小值,為下面規范數據用 max_set = np.zeros_like(self.X); max_set[:] = self.max_data #以每個特征的最大值,構建一個與訓練集結構一樣的數據集 min_set = np.zeros_like(self.X); min_set[:] = self.min_data #以每個特征的最小值,構建一個與訓練集結構一樣的數據集 self.X = (self.X - min_set)/(max_set - min_set) #規范訓練集 else: self.max_data = np.max(self.X,axis=0) self.min_data = np.min(self.X,axis=0) max_set = np.zeros_like(self.X); max_set[:] = self.max_data min_set = np.zeros_like(self.X); min_set[:] = self.min_data self.X = (self.X - min_set)/(max_set - min_set) if isinstance(y,pd.core.series.Series) != True: self.y = pd.Series(y) else: self.y = y if test == 'YES': #如果test為'YES',將原訓練集拆分為測試集和新的訓練集 self.test = 'YES' #設置self.test,后面knn函數判斷測試數據需不需要再規范 allCount = len(self.X) dataSet = [i for i in range(allCount)] testSet = [] for i in range(int(allCount*(1/5))): randomnum = dataSet[int(np.random.uniform(0,len(dataSet)))] testSet.append(randomnum) dataSet.remove(randomnum) self.X,self.testSet_X = self.X.iloc[dataSet],self.X.iloc[testSet] self.y,self.testSet_y = self.y.iloc[dataSet],self.y.iloc[testSet] else: self.test = 'NO'
def getDistances(self,point): #計算訓練集每個點與計算點的歐幾米得距離 points = np.zeros_like(self.X) #獲得與訓練集X一樣結構的0集 points[:] = point minusSquare = (self.X - points)**2 EuclideanDistances = np.sqrt(minusSquare.sum(axis=1)) #訓練集每個點與特殊點的歐幾米得距離 return EuclideanDistances
def getClass(self,point,k): #根據距離最近的k個點判斷計算點所屬類別 distances = self.getDistances(point) argsort = distances.argsort(axis=0) #根據數值大小,進行索引排序 classList = list(self.y.iloc[argsort[0:k]]) classCount = {} for i in classList: if i not in classCount: classCount[i] = 1 else: classCount[i] += 1 maxCount = 0 maxkey = 'x' for key in classCount.keys(): if classCount[key] > maxCount: maxCount = classCount[key] maxkey = key return maxkey
def knn(self,testData,k): #kNN計算,返回測試集的類別 if self.test == 'NO': #如果self.test == 'NO',需要規范測試數據(參照上面__init__) testData = pd.DataFrame(testData) max_set = np.zeros_like(testData); max_set[:] = self.max_data min_set = np.zeros_like(testData); min_set[:] = self.min_data testData = (testData - min_set)/(max_set - min_set) #規范測試集 if testData.shape == (len(testData),1): #判斷testData是否是一行記錄 label = self.getClass(testData.iloc[0],k) return label #一行記錄直接返回類型 else: labels = [] for i in range(len(testData)): point = testData.iloc[i,:] label = self.getClass(point,k) labels.append(label) return labels #多行記錄則返回類型的列表
def errorRate(self,knn_class,real_class): #計算kNN錯誤率,knn_class為算法計算的類別,real_class為真實的類別 error = 0 allCount = len(real_class) real_class = list(real_class) for i in range(allCount): if knn_class[i] != real_class[i]: error += 1 return error/allCount
下面利用sklearn庫里的iris數據(sklearn是數據挖掘算法庫),進行上述代碼測試
from sklearn import datasets sets = datasets.load_iris() #載入iris數據集 X = sets.data #特征值數據集 y = sets.target #類別數據集 myknn = kNN(X,y) knn_class = myknn.knn(myknn.testSet_X,4) errorRate = myknn.errorRate(knn_class,myknn.testSet_y)
kNN算法到此結束。如果發現有什么問題,歡迎大家指出。