一、KNN回归原理
方式一
在非参数模型中,核回归估计可以看做是响应变量
Y
Y
Y周围固定范围内的一个带权均值估计,这个固定范围由带宽(bandwith)参数
h
h
h控制。KNN估计则是与待估计的
Y
i
Y_i
Yi最近的
k
k
k个
Y
Y
Y的带权均值,公式如下:
m
^
k
(
x
)
=
1
n
∑
i
=
1
n
W
k
i
(
x
)
Y
i
\hat{m}_k (x) = \frac{1}{n} \sum_{i=1}^{n}W_{ki}(x)Y_i
m^k(x)=n1i=1∑nWki(x)Yi
其中,权重
W
k
i
(
x
)
W_{ki}(x)
Wki(x)定义为:
W
k
i
(
x
)
=
{
n
k
i
f
i
∈
J
x
0
o
t
h
e
r
w
i
s
e
W_{ki}(x) = \begin{cases} \frac{n}{k} & if \quad i\in J_x \\ 0 & otherwise \end{cases}
Wki(x)={kn0ifi∈Jxotherwise
k最近邻集合
J
x
J_x
Jx定义为:
J
x
=
i
:
X
i
是
x
的
k
个最近邻观测点
J_x={i:X_i 是x的k个最近邻观测点}
Jx=i:Xi是x的k个最近邻观测点
如何定义与 Y i Y_i Yi最近的 k k k个 Y Y Y呢?每个响应变量 Y Y Y都有其对应的坐标 x x x,待估计的 Y i Y_i Yi的值我们可能不知道,但我们可以知道他的坐标 x i x_i xi,然后根据距离公式计算出与 x i x_i xi最近的几个 x x x,进而得知与 Y i Y_i Yi最近的 k k k个 Y Y Y,然后计算出 Y i Y_i Yi的k近邻估计值 m ^ k ( x ) \hat{m}_k (x) m^k(x)。
如果数据分布较为稀疏,k个最近邻会离估计点 x x x非常远,得到的估计值偏差较大,此时一般不用k近邻估计。
观察权重计算公式,k近邻估计在
J
x
J_x
Jx内的权值是固定的,可以提取出来,即:
m
^
k
(
x
)
=
1
n
×
n
k
∑
i
=
1
k
Y
i
=
1
k
∑
i
=
1
k
Y
i
,
i
∈
J
x
\hat{m}_k (x) = \frac{1}{n} \times \frac{n}{k} \sum_{i=1}^{k}Y_i = \frac{1}{k} \sum_{i=1}^{k}Y_i, i\in J_x
m^k(x)=n1×kni=1∑kYi=k1i=1∑kYi,i∈Jx
此时可以看做是k个最邻近的
Y
Y
Y的均值,相当于是不带权重的。
方式二
换个角度,k最近邻集合
J
x
J_x
Jx是在变化的,故我们可以看成是k近邻估计有一个均匀核函数,他的带宽在变,但是带宽内的值固定不变,即带宽内的每个邻居
Y
Y
Y的权重相同且不变。于是,我们可以换个思路,用距离替代核函数,离估计点
x
x
x越近的
X
i
X_i
Xi的权重就越大,对应离
Y
Y
Y越近的点
Y
i
Y_i
Yi的权重就越大,这也是核回归的思想。改写后的k近邻估计计算公式为:
m
^
k
(
x
)
=
∑
i
=
1
n
K
R
(
x
−
X
i
)
Y
i
∑
i
=
1
n
K
R
(
x
−
X
i
)
=
∑
i
=
1
k
1
d
i
s
t
a
n
c
e
(
X
i
,
x
)
+
e
p
s
∑
i
=
1
k
1
d
i
s
t
a
n
c
e
(
X
i
,
x
)
+
e
p
s
Y
i
\hat{m}_k (x) = \frac{\sum_{i=1}^{n}K_R (x-X_i)Y_i}{\sum_{i=1}^{n}K_R (x-X_i)} = \sum_{i=1}^{k} \frac{\frac{1}{distance(X_i, x) + eps}} {\sum_{i=1}^k \frac{1}{distance(X_i, x)+eps}}Y_i
m^k(x)=∑i=1nKR(x−Xi)∑i=1nKR(x−Xi)Yi=i=1∑k∑i=1kdistance(Xi,x)+eps1distance(Xi,x)+eps1Yi
其中 K R ( x − X i ) K_R (x-X_i) KR(x−Xi)即为核函数(实际上是距离计算公式的变形),计算各个 X i X_i Xi到中心点 x x x的距离; e p s eps eps为一个小数,如0.00001,防止除数为0。距离计算公式有非常多种,这里只列出简单的两种:
- 欧式距离: d = ( x 2 − x 1 ) 2 + ( y 2 − y 1 ) 2 d = \sqrt{(x_2 - x_1)^2 + (y_2-y_1)^2} d=(x2−x1)2+(y2−y1)2
- 曼哈顿距离: d = ∣ x 2 − x 1 ∣ + ∣ y 2 − y 1 ∣ d = |x_2 - x_1| + |y_2 - y_1| d=∣x2−x1∣+∣y2−y1∣
计算距离时要注意不同特征的量纲差异(比如年龄和薪资),若不同特征之间量纲差异较大,则最好先对特征进行标准化或归一化。
方式一(权重固定,近似看成不带权重)为参考资料[1]上的版本,经过理论验证,算法较为稳健;方式二(带不同权重)是根据参考资料[1]的理论延伸,还未经理论推导验证,在低维数据上表现尚可,高维数据上表现欠佳。
二、python代码实现
KNN回归算法
class KNNRegressor:
'''
K近邻回归估计算法。
'''
def __init__(self, k:int, eps=1e-5):
self.k = k # k为最相邻的邻居个数
self.eps = eps # 一个小数,用来防止除数为0.
def fit(self, X, Y):
'''
传入训练数据集。
'''
assert len(X) == len(Y)
self.X = np.asarray(X)
self.Y = np.asarray(Y)
def Euclidean_distance(self, x, x_all):
'''
一个点x与所有点x_all的欧几里得距离。$d = \sqrt{(x_2 - x_1)^2 + (y_2-y_1)^2}$
'''
x, x_all = np.asarray(x), np.asarray(x_all)
if x_all.ndim < 2: #低维
return np.sqrt((x - x_all)**2)
else: #高维
return np.sqrt(np.sum((x - x_all)**2, axis=1))
def predict(self, x, type=2):
'''
预测函数。
Args:
x: 待估计的点。
type: 预测类型。1:不带权重;2:带权重。
Return:
result: x对应的y的估计结果。
'''
x = np.array(x)
result = []
for sample in x:
distance = self.Euclidean_distance(sample, self.X)
index = distance.argsort()[:self.k]
if type == 1:
result.append(np.mean(self.Y[index]))
elif type == 2:
weight = (1 / (distance[index] + self.eps)) / np.sum(1 / (distance[index] + self.eps))
result.append(np.sum(weight * self.Y[index]))
else:
print('type输入错误!1:不带权重;2:带权重。')
break
return np.array(result)
算法测试
import numpy as np
import matplotlib.pyplot as plt
# 生成随机数据
n = 100
x = np.linspace(0, 1, n)
print('x.shape = ', x.shape)
np.random.seed(0)
e = np.random.normal(0, 0.6, n)
# e = e[:, np.newaxis]
print('e.shape = ', e.shape)
y = 6 * np.sin(np.pi*x) + e
y_true = 6 * np.sin(np.pi*x)
print('y.shape = ', y.shape)
# 一维数据
k = 5
knn = KNNRegressor(k)
knn.fit(x, y)
y_knn_1 = knn.predict(x, type=1)
y_knn_2 = knn.predict(x, type=2)
# 画图
plt.figure(figsize=(12, 16), dpi=60)
plt.rc('font', family='Times New Roman', weight = 'medium', size=17) #设置英文字体
# plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
# plt.rcParams['axes.unicode_minus'] = False # 解决无法显示符号的问题
ax = plt.subplot(2, 1, 1)
ax.scatter(x, y, c='k', label='$Simulation$')
ax.plot(x, y_true, lw=2, label='$Truth$')
ax.plot(x, y_knn_1, lw=3, label='$\hat{m}_k (x)$')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title(f'KNN regression without weights, k = {k}')
ax.legend()
ax2 = plt.subplot(2, 1, 2)
ax2.scatter(x, y, c='k', label='$Simulation$')
ax2.plot(x, y_true, lw=2, label='$Truth$')
ax2.plot(x, y_knn_2, lw=3, label='$\hat{m}_k (x)$')
ax2.set_xlabel('x')
ax2.set_ylabel('y')
ax2.set_title(f'KNN regression with weights, k = {k}')
ax2.legend()
plt.show()
# plt.savefig('KNN回归.png')
参考资料
[1] Härdle W, Müller M, Sperlich S, et al. Nonparametric and semiparametric models[M]. Berlin: Springer, 2004.
[2] 几种常见的距离计算公式
[3] 【知乎】KNN算法中K是 怎么决定的?
[4] python实现KNN回归算法