度量方法
分类和回归的度量方法
在上一讲中,我们讨论了在监督学习范式中为研究人员设定的任务。今天,我们将讨论如何使用一类称为度量方法的方法来解决两个最常见的问题——分类和回归。
提醒:监督学习问题(即在训练数据集中给予我们的标签并且我们的任务是学习预测它们的问题)分为几个分支,其中最重要的是分类问题。在这种情况下,我们的任务是预测某个对象的类别,即标签代表 M M M 个类别之一。如果 M = 2 M = 2 M=2,则该问题称为二元分类问题。
第二个主要分支是回归问题。在这种情况下,标签是一个数字,原则上不限于某个有限集。这可以是从某个区间中取出的任意值。
我们首先考虑以下问题。
众所周知,英国首都伦敦的居民非常喜欢足球,他们甚至愿意在餐厅里花上一两个小时讨论他们最喜欢的球队的成功。伦敦人支持整个街区甚至地区的球队。甚至餐馆老板也常常在餐馆的设计中表现出对特定球队的热爱和同情,从而吸引相应俱乐部的球迷。让我们来看看这个城市的各个区域,其中我们重点介绍Chelsea球迷餐厅和Arsenal球迷餐厅。
游客在当地旅游指南中找到了这些餐厅,但在Tripadviser上却没有关于这些俱乐部的信息。这位游客是阿森纳队的狂热粉丝,想去一家精神上相近的餐厅。他应该去两家可能的餐厅(地图上黄色和绿色)中的哪一家?
该地区餐厅的分布如上图所示。
现在我们来判断一下地图上黄色标记的场所属于哪个俱乐部?
还有绿色吗?
你是如何做出选择的?
答案是,您根据距离该餐厅最近的餐厅的已知状态来判断该餐厅的状态!让我们利用这个想法来开发分类方法。
广义度量分类器
让我们获得有关 N N N 个对象 x 1 . . . x N {x_1} ... {x_N} x1...xN 的信息。我们的任务是对对象 X X X 进行分类(为简单起见,我们将考虑二元分类,例如切尔西和阿森纳球迷的情况)。
我们还确定对象
X
X
X 与每个对象
x
i
x_i
xi 的
接近度
\textbf{接近度}
接近度,也就是说,我们知道接近度函数
ρ
(
x
,
y
)
\rho(x,y)
ρ(x,y),它具有以下属性:
1.
ρ
(
x
,
x
)
=
0
,
ρ
(
x
,
y
)
>
0
1. \rho(x,x) = 0, \rho(x,y) > 0
1.ρ(x,x)=0,ρ(x,y)>0
2.
ρ
(
x
,
y
)
=
ρ
(
y
,
x
)
2. \rho(x,y) = \rho(y,x)
2.ρ(x,y)=ρ(y,x)
3.
ρ
(
x
,
z
)
≤
ρ
(
x
,
y
)
+
ρ
(
y
,
z
)
3. \rho(x,z) \leq \rho(x,y) + \rho(y,z)
3.ρ(x,z)≤ρ(x,y)+ρ(y,z)
这些性质绝对是自然而可以理解的:第一个性质表明,我们要调用距离的函数必须大于或等于零(而只有当这两个物体重合时,它们之间的距离才等于零)。第二个性质表示第一个物体到第二个物体的距离等于第二个物体到第一个物体的距离,第三个性质就是著名的三角不等式,我们每个人在学校都遇到过。
然后,我们将按照函数 ρ \rho ρ 值的升序对样本 x 1 . . . x N {x_1} ... {x_N} x1...xN 进行排序(也就是说,我们将把靠近 X X X 的对象放在前面,把远处的对象放在最后)。
让每个对象 x i x_i xi 也有自己的 重要性 \textbf{重要性} 重要性(例如,给定餐厅与被分类的餐厅有多近,到达那里需要多长时间,或者有多少粉丝定期光顾这家餐厅),我们用 w i ( X ) w_i(X) wi(X) 表示。 w i ( X ) w_i(X) wi(X)通常取决于 X X X和 x i x_i xi之间的距离。
然后构建
广义度量分类器
\textbf{广义度量分类器}
广义度量分类器的算法如下:
对于0和1这两个类,我们从训练样本中选择属于相应类的对象并计算它们的重要性之和。
令 X 0 X^0 X0 为零类对象集合, X 1 X^1 X1 为一类对象集合。那么 R 0 R_0 R0 就是零类对象的总重要性, R 1 R_1 R1 就是第一类对象的总重要性。
R
0
=
∑
i
∣
x
i
∈
X
0
w
i
(
X
)
R_0 = \sum\limits_{i |x_i \in X^0} w_i(X)
R0=i∣xi∈X0∑wi(X)
R
1
=
∑
i
∣
x
i
∈
X
1
w
i
(
X
)
R_1 = \sum\limits_{i |x_i \in X^1} w_i(X)
R1=i∣xi∈X1∑wi(X)
让我们比较一下它们吧!
如果
R
0
>
R
1
R_0 > R_1
R0>R1,则 0 类对象相对于被分类对象的整体重要性(或接近度)大于 1 类对象。因此,我们将该对象分配给 0 类。否则,情况正好相反。
k近邻方法
度量分类器的一个特例是 k-最近邻方法。它的思想很简单:训练样本中的所有物体要么被认为对我们同等重要,要么被认为完全不重要(即重要性为零),这取决于这个物体是否包含在k-closest列表中。
选择指标
在对象 X X X的空间中度量 ρ ( x , y ) \rho(x,y) ρ(x,y)的选择是一个相当严重的问题。该度量向我们展示了物体的相似程度,并且可以用不同的方式进行选择。还可以根据训练样本来配置度量标准。
在以下定义中引入了以下符号:
这里的
x
x
x 和
y
y
y 是我们工作的多维空间
X
X
X 中的点。每个点都由其自己的向量描述表示:
x
=
(
x
1
.
.
.
x
n
)
x = (x_1 ... x_n)
x=(x1...xn) 和
y
=
(
y
1
.
.
.
y
n
)
y = (y_1 ... y_n)
y=(y1...yn)。
1. 欧几里得度量
最常见的“学校”物体相似性测量
ρ ( x , y ) = ( ∑ i = 1 n ( x i − y i ) 2 ) 1 2 \rho(x,y) = (\sum\limits_{i=1}^n(x_i - y_i)^2)^{\frac{1}{2}} ρ(x,y)=(i=1∑n(xi−yi)2)21
这就是著名的勾股定理:如果有一个点 x x x 和一个点 y y y,那么它们之间的距离可以计算为对应直角三角形三条边的平方和的根。
优点:
- 简单
- 直观性
- 频繁适用
缺陷:
- 很大程度上取决于向量 x x x 和 y y y 的范数
- 向量的维数越高,欧氏度量的用处就越小
2. 余弦相似度
ρ ∗ ( x , y ) = ( x , y ) ∣ x ∣ ∣ y ∣ = ∑ i = 1 n x i y i ∣ x ∣ ∣ y ∣ \rho^*(x,y) = \frac{(x,y)}{|x||y|} = \frac{\sum\limits_{i=1}^n{x_i}{y_i}}{|x||y|} ρ∗(x,y)=∣x∣∣y∣(x,y)=∣x∣∣y∣i=1∑nxiyi
也就是说,我们将向量之间的角度的余弦视为一个度量标准。
当余弦相似度为 0 时,向量之间没有任何共同之处,它们是垂直的。
优点:
- 非常适合处理高维数据
- 它由向量之间的角度决定,因此它不依赖于它们的范数
缺陷:
- 现在我们完全不考虑向量范数的差异。对于某些任务来说这可能很重要。
注意:
要求距离为非负数。必须满足的要求是,向量彼此越近,距离就越小。在这种情况下,这些要求显然无法满足。
经常使用的量称为余弦距离。
余弦距离的计算公式为
ρ
(
x
,
y
)
=
1
−
(
x
,
y
)
∣
x
∣
∣
y
∣
\rho(x,y) = 1 - \frac{(x,y)}{|x||y|}
ρ(x,y)=1−∣x∣∣y∣(x,y)
这里的比例是相反的:
当余弦距离为 1 时,向量之间没有任何共同之处。它在直观上与古典意义上的距离更加相似,这就是它经常被使用的原因。
请注意,余弦距离不满足三角不等式。
3. 城市街区之间的距离(曼哈顿)
ρ ( x , y ) = ∑ i = 1 n ∣ x i − y i ∣ \rho(x,y) = \sum\limits_{i=1}^n|x_i - y_i| ρ(x,y)=i=1∑n∣xi−yi∣
这个指标也非常直观:
如果您步行穿过一个由均匀的矩形街区建成的城市,您无法使用勾股定理对角线缩短您的路径。无论如何,您都必须沿着两个轴线走完全程。
优点:
- 非常适合处理离散(特别是二进制)数据
缺陷:
- 适用于相当狭窄范围的任务
4. 半正矢公式
该公式与描述球面上的距离的具体问题有关。
优点:
- 描述球体上的距离
缺陷:
- 理想球体在世界上很少见(即使地球也远非理想球体)
5. Parzen窗口法
为什么不让较近的物体具有较高的权重,而较远的物体具有较低的权重呢?也就是说,让 w i w_i wi 不直接依赖于数字 i i i,而是依赖于 x x x 和 x i x_i xi 之间的距离
这种方法称为Parzen窗口方法。
解释:
这个公式的含义再次是,我们考虑类集合
Y
Y
Y 中的所有类(这次可以超过 2 个)。对于每一个类,我们计算属于这个类的那些对象相对于分类对象
x
x
x 的重要性总和,并选择相应值最大的类。
请注意,在这个公式的项中有一个特定的函数 K K K。这是什么?这是一个称为内核的函数,具有以下属性:它不会随着其参数的增长而增加(如果它大于零)。它是偶数(关于零对称)、连续且有界的。也就是说,此上下文中的核心被理解为其参数的重要性的函数。
请注意,这里的核取决于与到对象的距离成正比、与窗口宽度 h h h 成反比的参数。通常,选择的核使得它在 -1 到 1 的段之外为零。这应该如何理解?对于所有比窗口大小 h h h 更接近分类对象的 x x x,我们分配一个与该对象的距离成反比的权重,对于在大小为 h h h 的窗口之外**的 x x x,我们分配零权重。
代码
以下是一些用于从“numpy”计算不同距离的函数:
"""
Numpy 有一个专门用于处理各种线性代数结构的模块。这个模块叫做numpy.linalg
具体来说,在这个模块中,你可以找到一个函数,用于计算某个向量的范数,或者更简单地说,从某个度量的角度计算它的长度。
该函数称为 numpy.linalg.norm
https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html
"""
import numpy as np
from numpy.linalg import norm
"""
在基本情况下,np.linalg.norm 计算我们最熟悉、最易理解的 2-范数,它对应于欧几里得度量
"""
vec = np.array([1,2,3])
np.linalg.norm(vec)
输出:3.7416573867739413
# 现在我们自己计算一下这个向量的长度
np.sqrt(vec[0]**2 + vec[1]**2 + vec[2]**2)
输出:3.7416573867739413
基于函数“linalg.norm”,可以很容易地编写一个函数来计算多维空间中两点之间的距离
def dist(a: np.ndarray, b: np.ndarray) -> float:
"""
计算两个向量之间的距离。
Args:
a (np.ndarray): 第一个向量
b (np.ndarray): 第二个向量
Returns:
float: 向量之间的距离
"""
# 矢量减法
d = a - b
# 计算差异率
r = np.linalg.norm(d)
return r
dist(np.array([1, 0]), np.array([0, 1]))
输出:1.4142135623730951
这当然等于 2 \sqrt{2} 2
计算城市街区之间的距离不再困难。
有一个通用公式可以计算多维向量空间元素之间的一整类标准距离。该公式如下所示:
ρ p ( x ⃗ , y ⃗ ) = ( ∑ i = 1 n ∣ x i − y i ∣ p ) 1 p \rho_p(\vec{x},\vec{y}) = (∑\limits_{i=1}^n|x_i - y_i|^p)^{\frac{1}{p}} ρp(x,y)=(i=1∑n∣xi−yi∣p)p1
容易看出,当 p p p=2时,我们得到欧几里得距离的公式,当 p p p=1时,我们得到城市街区之间的距离。
这个参数 p 通常可以几乎任意选择(我们现在不讨论
p
p
p 的限制)。
事实证明,在“np.linalg.norm”中你也可以选择
p
p
p参数。该参数由“ord”参数的值决定。也就是说,为了计算城市街区的距离,只需将值 1 分配给相应的参数即可。
import numpy as np
def dist(a: np.ndarray, b: np.ndarray, p: int) -> float:
"""
使用 Lp 度量计算两个向量之间的距离。
Args:
a (np.ndarray): 第一个向量
b (np.ndarray): 第二个向量
p (int): 度量参数 Lp(通常为 1、2 或 ∞)
Returns:
float: 按 Lp 度量的向量之间的距离
"""
# 矢量减法
d = a - b
# 使用 Lp 度量计算差值范数
r = np.linalg.norm(d, ord=p)
return r
dist(np.array([1, 0]), np.array([0, 1]), 1)
输出:2.0
对于余弦距离也可以做同样的事情。
def cosine(a: np.ndarray, b: np.ndarray) -> float:
"""
计算两个向量之间角度的余弦。
Args:
a (np.ndarray): 第一个向量
b (np.ndarray): 第二个向量
Returns:
float: 向量间夹角的余弦
"""
# 向量的点积
dot = np.dot(a, b)
# 向量范数
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
# 计算余弦
cos = dot / norm_a / norm_b
return cos
def cosine_dist(a: np.ndarray, b: np.ndarray) -> float:
"""
计算两个向量之间的余弦距离。
Args:
a (np.ndarray): 第一个向量
b (np.ndarray): 第二个向量
Returns:
float: 向量之间的余弦距离
"""
# 计算角度的余弦
cos = cosine(a, b)
# 余弦距离公式
dist = 1 - cos
return dist
cosine(np.array([1,0]), np.array([0,1]))
cosine_dist(np.array([1,0]), np.array([0,1]))
输出:
0.0
1.0
这是合乎逻辑的,因为向量是垂直的。
但是,使用“scipy”模块中的函数可以更简单地完成相同的操作。
from scipy.spatial.distance import cosine as c
c(np.array([1,0]), np.array([0,1]))
输出:1.0
KNN 的一个重要缺点是它的模糊性:在多个类别上可以同时获得相同的投票总和。当然,如果只有两个类,我们可以取奇数 k k k,问题就解决了,但如果有更多的类,这就救不了我们了。这个问题有一个更通用的解决方案:我们引入一个权重序列 w i w_i wi,每个权重将定义第 i i i 个邻居对分类的 贡献。在这种情况下, w i w_i wi 表示第 i i i 个对象与被分类的对象之间的距离。权重序列 w i w_i wi 的选择落在我们的肩上,是一种启发式方法。
如何处理回归问题?
让我们用这个例子来看一下:
给你一张山脉地图,但地图上只给出了部分点的高度。您是否被要求确定其他一些点的地形的可能海拔高度?
为了确定蓝点的高度,以下做法是合理的:
a) 不考虑所有可用数据,而只考虑位于所需点某个邻域内的点;
b) 考虑到附近点的接近度,对附近点的高度值进行平均。
KNN 用于回归
让我们利用这个想法来将该方法推广到回归问题。
考虑以下三个想法:
- 想法1:如果我们从训练集中给出 n n n个点,并且这些点处的隐藏函数值已知,那么在所有其他点处我们的预测可以是所有已知点处的值的平均值
- 想法 2:我们可以尝试找到一个比平均值更好的常数。设它是 x ∈ X x \in X x∈X 邻域内的常数 α \alpha α。如何确定这个常数呢?
我们希望这个常数在某种“一般意义”上与我们在邻域内已知的函数的所有值尽可能的相似。让我们看看这个常数与每个已知值的差异有多大,然后取平均误差。然而,我们不能简单地明确地取误差值的平均数。让我们看一个例子:
令 A 点处的误差为 100,令 B 点处的误差为 -100。那么平均误差将为 0,尽管我们的常数在每个特定点上都是非常错误的。因此,我们不会对误差值取平均值,而是对平方误差的值取平均值。
那么我们如何定义这个常数呢?解决了最小化已知
y
i
y_i
yi值与该常数的均方差的问题。这是表示我们对整个数据集的预测平均误差的函数:
Q
X
(
α
)
=
1
N
∑
i
=
1
N
(
y
i
−
α
)
2
Q_X(\alpha) = \frac{1}{N} \sum\limits_{i=1}^N (y_i - \alpha)^2
QX(α)=N1i=1∑N(yi−α)2
或者我们可以考虑每个对象
w
i
w_i
wi 的重要性:
Q
X
(
α
)
=
1
N
∑
i
=
1
N
w
i
(
y
i
−
α
)
2
Q_X(\alpha) = \frac{1}{N} \sum\limits_{i=1}^N w_i(y_i - \alpha)^2
QX(α)=N1i=1∑Nwi(yi−α)2
数学使我们能够找到一个 α α α 的值,使得均方误差取所有可能值中最小的值。
- 想法 3:让我们尝试通过对训练样本中所有已知值取平均值来做出预测,同时考虑到它们对给定对象的重要性。也就是说,如果点 x i x_i xi距离 X X X较远,那么对于点 X X X处的预测, y i y_i yi的贡献就会很小,如果距离很近,那么 y i y_i yi的贡献就会很大。然后我们可以写出以下公式: A ( X ) = ∑ i = 1 N w i y i ∑ i = 1 N w i = ∑ i = 1 N K ( ρ ( X , x i ) h ) y i ∑ i = 1 N K ( ρ ( X , x i ) h ) A(X) = \frac{\sum\limits_{i=1}^N w_i y_i}{\sum\limits_{i=1}^N w_i} = \frac{\sum\limits_{i=1}^N K(\frac{\rho(X, x_i)}{h}) y_i}{\sum\limits_{i=1}^N K(\frac{\rho(X, x_i)}{h})} A(X)=i=1∑Nwii=1∑Nwiyi=i=1∑NK(hρ(X,xi))i=1∑NK(hρ(X,xi))yi
这里的 K K K 函数告诉我们训练样本中的特定对象对于分析点的预测有多重要。
度量算法的优点和缺点
度量算法的优点:
- 极其简单
- 实现容易
- 算法的训练归结为记忆训练数据集,因此速度很快
度量算法的缺点:
- 需要存储整个训练数据集
- 对噪声和异常值非常敏感
- 质量通常相当低
我们来尝试亲手实现KNN算法吧!
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")
Python 实现
让我们编写一个 KNN 函数,它将根据 k 个最近邻算法和一组进行预测的 k 个最近点返回一个预测。
假设:
- 在二维平面上工作
- 解决二元分类问题
- k 我们手工捡起
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
def draw(KNN):
"""
允许呈现 KNN 函数输出的装饰器
"""
def wrapper(point: list, class1: np.ndarray, class2: np.ndarray, N: int, normfunc):
print(KNN.__name__, "running")
plt.figure(figsize=(10, 10))
"""
设置班级规模
"""
num_1 = class1.shape[0]
num_2 = class2.shape[0]
num = num_1 + num_2
"""
我们设置每个类的点的坐标为 X 和 Y
"""
X1 = class1[:, 0]
Y1 = class1[:, 1]
X2 = class2[:, 0]
Y2 = class2[:, 1]
X = np.concatenate([X1, X2])
Y = np.concatenate([Y1, Y2])
class_flag = [0 for i in range(num_1)] + [1 for i in range(num_2)]
"""
我们将所有内容写入数据框以使用 seaborn 方法
"""
data = pd.DataFrame({"Class": class_flag, "X": X, "Y": Y})
plt.grid()
plt.scatter(x=point[0], y=point[1], c="r")
sns.scatterplot(data=data, x="X", y="Y", hue="Class")
"""
我们使用自己编写的函数来获取算法的输出
"""
result, sorted_points = KNN(point, class1, class2, N, normfunc)
print(f"RESULT: point was assigned to class {result}")
for i in range(N):
plt.plot(
[sorted_points[i][0], point[0]],
[sorted_points[i][1], point[1]],
c="r" if sorted_points[i][2] == 0 else "b",
)
return wrapper
import numpy as np
@draw
def KNN(point: list, class1: np.ndarray, class2: np.ndarray, K: int, normfunc: callable):
"""
使用 KNN 算法进行点分类
"""
"""
设置班级规模
"""
num_1 = class1.shape[0]
num_2 = class2.shape[0]
num = num_1 + num_2
"""
我们设置每个类的点的坐标为 X 和 Y
"""
X1 = class1[:, 0]
Y1 = class1[:, 1]
X2 = class2[:, 0]
Y2 = class2[:, 1]
# 将数组转换为 (n, 1) 格式以便与 numpy 正确配合使用
X = np.concatenate([X1.reshape(-1, 1), X2.reshape(-1, 1)])
Y = np.concatenate([Y1.reshape(-1, 1), Y2.reshape(-1, 1)])
"""
我们通过给所有点添加它们所属类别的值来对它们进行排序,
根据给定的距离函数 normfunc = normfunc(x1, y1, x2, y2)
"""
class_flag = np.array(
[0 for i in range(num_1)] + [1 for i in range(num_2)]
).reshape(-1, 1)
keyfunc = lambda P: normfunc(P[0], P[1], point[0], point[1])
points = np.concatenate((X, Y, class_flag), axis=1).tolist()
sorted_points = sorted(points, key=keyfunc)
"""
我们循环计算从 top-K 开始属于 0 类和 1 类的点数
"""
C0 = 0
C1 = 0
for p in sorted_points[:K]:
assert p[2] == 1 or p[2] == 0
if p[2] == 0:
C0 += 1
else:
C1 += 1
result = 0 if C0 > C1 else 1
print(f"There are {C0} points of class 0 in the set of {K} nearest points")
print(f"There are {C1} points of class 1 in the set of {K} nearest points")
return result, sorted_points[:K]
使用示例
import numpy as np
import random
def euc_distance(point_1_x, point_1_y, point_2_x, point_2_y):
"""
计算两点之间的欧几里得距离。
Args:
point_1_x (float): 第一个点的 X 坐标
point_1_y (float): 第一个点的 Y 坐标
point_2_x (float): 第二个点的 X 坐标
point_2_y (float): 第二个点的 Y 坐标
Returns:
float: 两点之间的欧几里得距离
"""
x_diff = point_1_x - point_2_x
y_diff = point_1_y - point_2_y
return np.sqrt(x_diff**2 + y_diff**2)
def generate_class_data(mean1, mean2, std_dev, n_samples):
"""
为两个类生成随机数据。
Args:
mean1 (list): 第一类的平均值
mean2 (list): 第二类的平均值
std_dev (float): 标准差
n_samples (int): 每个类别的样本数
Returns:
tuple:
class1 (numpy.ndarray): 第一类的数据
class2 (numpy.ndarray): 第二类的数据
"""
class1 = np.random.randn(n_samples, 2) + np.array(mean1)
class2 = np.random.randn(n_samples, 2) + np.array(mean2)
return class1, class2
# 生成课程数据
point = [0, 1]
mean1 = [0.0, 7.0]
mean2 = [0.0, -7.0]
std_dev = 1.0
n_samples = 100
class_1, class_2 = generate_class_data(mean1, mean2, std_dev, n_samples)
K = 30
KNN(point, class_1, class_2, K, euc_distance)
输出:
KNN running
There are 30 points of class 0 in the set of 30 nearest points
There are 0 points of class 1 in the set of 30 nearest points
RESULT: point was assigned to class 0
import numpy as np
# 初始化点
point = [0, 0]
# 创建类数组
class_1 = []
class_2 = []
for x in np.linspace(-3, 2, 100):
class_1.append([x, np.sqrt(25 - x**2) / 10 + np.random.randn() / 100])
for x in np.linspace(-2, 3, 100):
class_2.append([x, -np.sqrt(25 - x**2) / 10 + np.random.randn() / 100])
class_1 = np.array(class_1)
class_2 = np.array(class_2)
K = 30
KNN(point, class_1, class_2, K, euc_distance)
输出:
KNN running
There are 14 points of class 0 in the set of 30 nearest points
There are 16 points of class 1 in the set of 30 nearest points
RESULT: point was assigned to class 1
import numpy as np
# 初始化点
point = [3, 0]
# 创建类数组
class_1 = []
class_2 = []
for x in np.linspace(-3, 2, 100):
class_1.append([x, np.sqrt(25 - x**2) / 10 + np.random.randn() / 100])
for x in np.linspace(-2, 3, 100):
class_2.append([x, -np.sqrt(25 - x**2) / 10 + np.random.randn() / 100])
class_1 = np.array(class_1)
class_2 = np.array(class_2)
K = 30
KNN(point, class_1, class_2, K, euc_distance)
输出:
KNN running
There are 5 points of class 0 in the set of 30 nearest points
There are 25 points of class 1 in the set of 30 nearest points
RESULT: point was assigned to class 1
错误选择 K
经常发生的情况是,所选的 K 不太适合解决给定的问题。此外,增加 K 并不总是会导致算法性能的改善。
import numpy as np
point = [-2.5, -1.5]
class_1 = []
class_2 = []
for x in np.linspace(-3, 2, 100):
class_1.append([x, x - 0.5 + np.random.randn() / 10])
for x in np.linspace(-2, 3, 100):
class_2.append([x, x + 0.5 + np.random.randn() / 10])
class_1 = np.array(class_1)
class_2 = np.array(class_2)
K = 10
KNN(point, class_1, class_2, K, euc_distance)
输出:
KNN running
There are 2 points of class 0 in the set of 10 nearest points
There are 8 points of class 1 in the set of 10 nearest points
RESULT: point was assigned to class 1
看起来,我们选择的 k 越大,我们的算法就越可靠。但:
在这种情况下,我们发现事实并非如此。
import numpy as np
point = [-2.5, -1.5]
class_1 = []
class_2 = []
for x in np.linspace(-3, 2, 100):
class_1.append([x, x - 0.5 + np.random.randn() / 10])
for x in np.linspace(-2, 3, 100):
class_2.append([x, x + 0.5 + np.random.randn() / 10])
class_1 = np.array(class_1)
class_2 = np.array(class_2)
K = 100
KNN(point, class_1, class_2, K, euc_distance)
输出:
KNN running
There are 64 points of class 0 in the set of 100 nearest points
There are 36 points of class 1 in the set of 100 nearest points
RESULT: point was assigned to class 0
指标调整
我们要注意的是,在这种特殊情况下,传统指标很难反映当前任务的特点。
在这种情况下,我们的样品具有带结构。考虑一个将这一事实考虑在内的指标似乎很合逻辑。
例如,我们可以考虑以下函数:
f ( x 1 , y 1 , x 2 , y 2 ) = ∣ b 1 ^ − b 2 ^ ∣ = ∣ ( x 1 − y 1 ) − ( x 2 − y 2 ) ∣ f(x_1,y_1, x_2, y_2) = |\hat{b_1} - \hat{b_2}| = |(x_1 - y_1) - (x_2 - y_2)| f(x1,y1,x2,y2)=∣b1^−b2^∣=∣(x1−y1)−(x2−y2)∣
这里的 b ^ \hat{b} b^ 类似于一对点 ( x x x, y y y) 所在直线的位移系数,假设我们认为该直线的斜率为 1
import numpy as np
def sub_distance(point_1_x, point_1_y, point_2_x, point_2_y):
return abs((point_1_x - point_1_y) - (point_2_x - point_2_y))
point = [-2.5, -1.5]
class_1 = []
class_2 = []
for x in np.linspace(-3, 2, 100):
class_1.append([x, x - 0.5 + np.random.randn() / 10])
for x in np.linspace(-2, 3, 100):
class_2.append([x, x + 0.5 + np.random.randn() / 10])
class_1 = np.array(class_1)
class_2 = np.array(class_2)
K = 100
KNN(point, class_1, class_2, K, sub_distance)
输出:
KNN running
There are 0 points of class 0 in the set of 100 nearest points
There are 100 points of class 1 in the set of 100 nearest points
RESULT: point was assigned to class 1
这个指标非常适合我们。
point = [-1.0, 3.0]
K = 100
KNN(point, class_1, class_2, K, sub_distance)
输出:
KNN running
There are 0 points of class 0 in the set of 100 nearest points
There are 100 points of class 1 in the set of 100 nearest points
RESULT: point was assigned to class 1
point = [3., 2.]
K = 100
KNN(point, class_1, class_2, K, sub_distance)
输出:
KNN running
There are 100 points of class 0 in the set of 100 nearest points
There are 0 points of class 1 in the set of 100 nearest points
RESULT: point was assigned to class 0
注意:所展示的功能仅作为示例,并不代表一般指标。很容易证明:
让我们选择 x 1 , y 1 x_1, y_1 x1,y1 和 x 2 , y 2 x_2,y_2 x2,y2 位于同一条线上,但不重合。此类对上的两个给定函数的值将等于 0,即距离的第一个性质不满足。
在 sklearn 中实现
我们不需要每次都自己实现 KNN,因为它已经在精彩的 sklearn
库中实现了!
算法初始化期间传递以下参数:
n_neighbors: int
,负责选定邻居数量(K)的参数metric:string or callable object
。作为度量,您可以传递一组固定的字符串(可以在官方文档中找到此集合,或者通过访问sklearn.metrics.pairwise
.PAIRWISE_DISTANCE_FUNCTIONS
,如下一个单元格所示),或者编写自己的距离函数并将其作为参数传递,p: int
,负责度量类型的参数(如上所述:p==1
是曼哈顿距离,p==2
是欧几里得距离)。
其余参数对我们来说不太重要,但你可以在链接上阅读有关它们的信息
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
import warnings
# 定义训练示例和目标值
samples = np.array([[0.0, 0.0, 0.0], [0.0, 0.5, 0.0], [1.0, 1.0, 0.5]])
y = np.array([[1.0], [1.0], [0.0]])
# 创建 KNeighborsClassifier 实例
neigh = KNeighborsClassifier(n_neighbors=1, p=2)
# 模型训练
neigh.fit(samples, y)
# 预测新示例
new_sample = np.array([[1.0, 1.0, 1.0]])
predicted_class = neigh.predict(new_sample)[0]
print(f"结果:该点被分配给该类 {int(predicted_class)}")
结果:该点被分配给该类 0
import sklearn
# 可能的指标
sklearn.metrics.pairwise.PAIRWISE_DISTANCE_FUNCTIONS
# 使用余弦距离实例化 KNeighborsClassifier
neigh = KNeighborsClassifier(n_neighbors=1, metric='cosine')
# 模型训练
neigh.fit(samples, y)
# 预测新示例
new_sample = np.array([[1.0, 1.0, 1.0]])
predicted_class = neigh.predict(new_sample)[0]
print(f"结果:该点被分配给该类 {int(predicted_class)}")
结果:该点被分配给该类 0
如何选择K
让我们加载著名的“葡萄酒”数据集并尝试分析如何选择最佳的最近邻居数量。我们将这样做:我们将使用交叉验证方法来确定算法对于某个“K”的问题解决得有多好,然后我们将使用最简单的方法来选择超参数——枚举。
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_wine
X, y = load_wine(return_X_y=True)
print(f'X.shape: {X.shape}')
输出:X.shape: (178, 13)
from sklearn.neighbors import KNeighborsClassifier
clf = KNeighborsClassifier()
score = cross_val_score(clf, X, y, cv=5)
print(f'Cross Validation Score, cv=5: {score}')
输出:Cross Validation Score, cv=5: [0.72222222 0.66666667 0.63888889 0.65714286 0.77142857]
import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from tqdm.auto import tqdm
k_candidates = list(range(1, 51))
scores = []
for k in tqdm(k_candidates):
clf = KNeighborsClassifier(n_neighbors=k)
score = cross_val_score(clf, X, y, cv=5)
scores.append(np.mean(score))
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme()
plt.figure(figsize=(18, 7))
plt.plot(np.arange(1, 51), scores)
plt.title('KNN Cross Validation Scores')
plt.xlabel('Number of Neighbors (K)')
plt.ylabel('Average Score')
plt.grid(True)
plt.show()
k_best = np.argmax(scores) + 1
k_best
输出:50
提供的代码应解释如下:
使用 sklearn.model_selection.crossval
函数,我们将样本分成
c
v
cv
cv=5 个元素,并对每个元素执行上述过程,获得 5 个质量分数(默认设置 accuracy_score
指标)。这些分数存储在“分数”数组中。
我们将以下内容传递给 cross_val_score
函数的参数:
-
KNeighborsClassifier (clf)
类的对象。任何支持.fit()
、.predict()
和(如果相应度量需要).predict_proba()
方法的分类器都可以选择作为该参数。这些分别是学习、预测和获取对象属于特定类别的概率的函数。这非常方便,因为它允许我们不仅使用来自“sklearn”的类对象作为参数“clf”,还可以使用来自其他库(甚至我们自己的类)的类对象作为参数“clf”。 -
训练样本 X X X
-
训练样本对象 y y y 的答案
-
c v cv cv - 我们将样本分成的“块”的数量
关于此功能的更多详细信息,请参阅文档。
这组数字告诉了我们很多:
从平均值我们可以确定我们的算法的平均水平有多好。交叉验证期间获得的一组指标的平均值被视为对构建算法质量的有效评估。
但这里也存在一些困难。假设输出结果为以下一组数字:
[ 0.5 , 0.52 , 0.9 , 0.55 ] [0.5, 0.52, 0.9, 0.55] [0.5,0.52,0.9,0.55]
这里的平均值将是0.61,这显然与第三名的异常好成绩有关。这一结果可能是由于某个子样本的偶然性或偏差造成的。这不能被认为是绝对正确的。同样,我们也可能被一系列数值差异很大的数字的平均值所误导。例如,如果我们得到以下结果作为交叉验证的输出:
[ 0.1 , 0.99 , 0.09 , 0.95 , 0.94 , 0.05 ] [0.1, 0.99, 0.09, 0.95, 0.94, 0.05] [0.1,0.99,0.09,0.95,0.94,0.05]
显然,你不能相信这里的平均值。应该寻找错误。
可以通过评估结果数组中的标准差来跟踪这种情况:标准差越小,使用平均值进行的质量评估就越可靠。