第三章 k k k 近邻法
k 近邻法(k-nearest neighbor, k-NN)是一种基本分类与回归方法。
输入: 实例的特征向量,对应于特征空间的点;
输出: 实例的类别,可以去多类。
算法
思想: 给定一个训练数据集,对新的输入实例,在训练数据集中找到与该实例最邻近的 k k k 个实例,这 k k k 个实例的多数属于某个类,就把该输入实例分为这个类。
输入: 训练数据集
T
=
{
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
,
.
.
.
,
(
x
N
,
y
N
)
}
T=\{(x_1,y_1),(x_2,y_2),...,(x_N,y_N)\}
T={(x1,y1),(x2,y2),...,(xN,yN)}
其中,其中
x
i
∈
X
=
R
n
x_i \in \mathcal{X}= \mathbf R^n
xi∈X=Rn 为实例的特征向量,
y
i
∈
Y
=
{
c
1
,
c
2
,
.
.
.
,
c
K
}
y_i \in \mathcal Y =\{c_1,c_2,...,c_K\}
yi∈Y={c1,c2,...,cK}为实例的类别,
i
=
1
,
2
,
.
.
.
,
N
i=1,2,...,N
i=1,2,...,N,实例特征向量
x
x
x.
输出: 实例 x x x 所属的类 y y y .
根据给定的距离度量,在训练集 T T T 中找出与 x x x 最近邻的 k k k 个点,涵盖这个点的 x x x 的领域记作 N k ( x ) N_k(x) Nk(x) ;
在 N k ( x ) N_k(x) Nk(x) 中根据分类决策规则(如多数表决)决定 x x x 的类别 y y y :
y = arg max c j ∑ x i ∈ N k ( x ) I ( y i = c j ) , i = 1 , 2 , . . . , N ; j = 1 , 2 , . . . , K y = \arg \max \limits _{c_j} \sum \limits_{x_i \in N_k(x)}I(y_i=c_j),\qquad i=1,2,...,N;\ j=1,2,...,K y=argcjmaxxi∈Nk(x)∑I(yi=cj),i=1,2,...,N; j=1,2,...,K
I I I 为指示函数,即当 y i = c j y_i=c_j yi=cj 时 I I I 为 1,否则 I I I 为 0 。
当 k = 1 k=1 k=1 时,称为最近邻算法。
模型
三个基本要素: 距离度量, k k k 值的选择,分类决策规则。
单元(cell): 特征空间中,对每个训练实例点 x i x_i xi ,距离该点比其他点更近的所有点组成的一个区域。
距离度量
-
L p L_p Lp 距离:
L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ p ) 1 p , p ≥ 1 L_p(x_i,x_j)=\left( \sum \limits _{l=1}^n |x_i^{(l)}-x_j^{(l)}|^p \right)^{\frac{1}{p}},\qquad p \geq 1 Lp(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣p)p1,p≥1 -
当 p = 2 p = 2 p=2 时,欧氏距离(Euclidean distance):
L 2 ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ 2 ) 1 2 L_2(x_i,x_j)=\left( \sum \limits _{l=1}^n |x_i^{(l)}-x_j^{(l)}|^2 \right)^{\frac{1}{2}} L2(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣2)21 -
当 p = 1 p = 1 p=1 时,曼哈顿距离(Manhattan distance):
L 1 ( x i , x j ) = ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ L_1(x_i,x_j)= \sum \limits _{l=1}^n |x_i^{(l)}-x_j^{(l)}| L1(xi,xj)=l=1∑n∣xi(l)−xj(l)∣ -
当 p = ∞ p = \infty p=∞ 时,为各个坐标距离的最大值:
L ∞ ( x i , x j ) = max l ∣ x i ( l ) − x j ( l ) ∣ L_{\infty}(x_i,x_j)= \max\limits _{l} |x_i^{(l)}-x_j^{(l)}| L∞(xi,xj)=lmax∣xi(l)−xj(l)∣
k 值的选择
k k k 值减小意味着整体模型变得复杂,容易发生过拟合, k k k 值增大意味着整体的模型变得简单。
在应用中, k k k 值一般取一个比较小的数值,通常采用交叉验证法来选取最优的 k k k 值。
分类决策规则
多数表决方法(majority vote rule): 由输入实例的 k k k 个邻近的训练实例中的多数类决定输入实例的类。
分类函数为:
f : R n → { c 1 , c 2 , . . . , c K } f:\mathbf R^n \to \{c_1,c_2,...,c_K\} f:Rn→{c1,c2,...,cK}
误分类的概率为:
P ( Y ≠ f ( X ) ) = 1 − P ( Y = f ( X ) ) P(Y \neq f(X)) =1-P(Y=f(X)) P(Y̸=f(X))=1−P(Y=f(X))
对给定的实例 x ∈ X x \in \mathcal X x∈X,其最近邻的 k k k 个训练实例点构成集合 N k ( x ) N_k(x) Nk(x)。如果涵盖 N k ( x ) N_k(x) Nk(x) 的区域的类别是 c j c_j cj ,那么误分类率是:
1 k ∑ x i ∈ N k ( x ) I ( y i ≠ c j ) = 1 − 1 k ∑ x i ∈ N k ( x ) I ( y i = c j ) \frac{1}{k} \sum \limits_{x_i \in N_k(x)} I(y_i \neq c_j)=1- \frac{1}{k}\sum \limits_{x_i \in N_k(x)} I(y_i =c_j) k1xi∈Nk(x)∑I(yi̸=cj)=1−k1xi∈Nk(x)∑I(yi=cj)
要使误分类概率最小即经验风险最小,就要使 ∑ x i ∈ N k ( x ) I ( y i = c j ) \sum \limits_{x_i \in N_k(x)} I(y_i =c_j) xi∈Nk(x)∑I(yi=cj) 最大。所以多数表决规则等价于经验风险最小化。
kd树
算法:(构造平衡 k d kd kd 树)
输入: k k k 维空间数据集 T = { x 1 , x 2 , . . . , x N } T=\{x_1,x_2,...,x_N\} T={x1,x2,...,xN},其中 x i = ( x i ( 1 ) , x i ( 2 ) , . . . , x i ( k ) ) T , i = 1 , 2 , . . . , N x_i=(x_i^{(1)},x_i^{(2)},...,x_i^{(k)})^T,\ i =1,2,...,N xi=(xi(1),xi(2),...,xi(k))T, i=1,2,...,N;
输出: k d kd kd 树
开始:构造根节点,使根结点对应于包含 T T T 的 k k k 维空间的超矩形区域。
选择 x ( 1 ) x^{(1)} x(1) 为坐标轴,以 T T T 中所有实例的 x ( 1 ) x^{(1)} x(1) 坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( 1 ) x^{(1)} x(1) 垂直的超平面实现。
由根结点生成深度为 1 的左、右子结点:左子结点对应坐标 x ( 1 ) x^{(1)} x(1) 小于切分点的子区域,右子结点对应于坐标 x ( 1 ) x^{(1)} x(1) 大于切分点的子区域。
重复:对深度为 j j j 的结点,选择 x ( l ) x^{(l)} x(l) 为切分的坐标轴, l = j ( m o d    k ) + 1 l=j(\mod k)+1 l=j(modk)+1 , 以该结点的区域中的所有实例的点 x ( l ) x^{(l)} x(l) 坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( l ) x^{(l)} x(l) 垂直的超平面实现。
由该结点生成深度为 j + 1 j+1 j+1 的左、右子结点:左子结点对应坐标 x ( l ) x^{(l)} x(l) 小于切分点的子区域,右子结点对应于坐标 x ( l ) x^{(l)} x(l) 大于切分点的子区域。
将落在切分超平面上的实例点保存在该结点。
知道两个区域没有实例存在时停止。从而形成 k d kd kd 树的区域划分。
算法:(用 k d kd kd 树的最近邻搜索)
输入:已构造的 k d kd kd 树,目标点 x x x.
输出: x x x 的最近邻。
在 k d kd kd 树中找出包含目标点 x x x 的叶结点:从根结点出发,递归地向下访问 k d kd kd 树。若目标点 x x x 当前维的坐标小于切分点的坐标,则移动到做子结点,否则移动到右子结点。知道子结点为叶结点为止。
以此叶结点为“当前最近点”。
递归地向上回退,在每个结点进行一下操作:
a). 如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为“当前最近点”。
b).当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点的父结点的另一子结点对应的区域是否有更近的点。具体地,检查另一子结点对应的区域是否与以目标点为球心、以目标点与“当前最近点”间的距离为半径的超球体相交。
如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点,接着,递归地进行最近邻搜索。
如果不想交,向上回退。
当回退到根结点时,搜索结束。最后的“当前最近点”即为 x x x 的最近邻点。
贴上代码及注释,数据集为irisdata,运行环境:python3.6
该代码为麦子学院:深度学习基础出品。
# example of KNN implemented from Scratch in Python
import csv
import random
import math
import operator
def loadDataset(filename, split, trainingSet=[], testSet=[]):
with open(filename, 'r') as csvfile: # 将 'rb' 改写为'r',不然会报错
lines = csv.reader(csvfile) # 将数据按行读进
dataset = list(lines) # 转换为 list
for x in range(len(dataset)-1): # 将数据分为训练集和测试集
for y in range(4):
dataset[x][y] = float(dataset[x][y]) # 将数据转换为浮点型
if random.random() < split: # 注意缩进问题
trainingSet.append(dataset[x])
else:
testSet.append(dataset[x])
def euclideanDistance(instance1, instance2, length): # 计算两点之间的欧氏距离
distance = 0
for x in range(length): # 计算每一维的距离的平方和
distance += pow((instance1[x] - instance2[x]), 2)
return (math.sqrt(distance))
def getNeighbors(trainingSet, testInstance, k): # 测试集的数据与训练集数据做 k-最近邻 计算
distances = []
length = len(testInstance) - 1 #
for x in range(len(trainingSet)):
dist = euclideanDistance(testInstance, trainingSet[x], length) # 将测试数据与所有训练集数据做欧氏距离计算
distances.append((trainingSet[x], dist))
distances.sort(key=operator.itemgetter(1)) # 将距离进行排序
neighbors = []
for x in range(k): # 返回最近的k个邻居
neighbors.append(distances[x][0])
return (neighbors)
def getResponse(neighbors): # 投票表决
classVotes = {}
for x in range(len(neighbors)):
response = neighbors[x][-1]
if response in classVotes:
classVotes[response] += 1
else:
classVotes[response] = 1
# 将每类的投票个数按从大到小排序,reverse=True 按降序排列
sortedVotes = sorted(classVotes.items(), key=operator.itemgetter(1), reverse=True) # dict.iteritems --> items
return (sortedVotes[0][0]) # 返回第一个分类,即投票最多的
def getAccuracy(testSet, predictions): # 计算精确度
correct = 0
for x in range(len(testSet)):
if testSet[x][-1] == predictions[x]:
correct += 1
return ((correct/float(len(testSet))) * 100)
def main():
# prepare data
trainingSet = []
testSet = []
split = 0.67
loadDataset(r'irisdata.txt', split, trainingSet, testSet)
print("Train set: \n" + repr(len(trainingSet)))
print("Test set: \n" + repr(len(testSet)))
# generate predictions
predictions = []
k = 3
for x in range(len(testSet)):
neighbors = getNeighbors(trainingSet, testSet[x], k)
result = getResponse(neighbors)
predictions.append(result)
print('> predicted=' + repr(result) + ',actual=' + repr(testSet[x][-1]))
accuracy = getAccuracy(testSet, predictions)
print('Accuracy:' + repr(accuracy) + '%')
main()