在非参数模型中,核回归估计可以看做是响应变量 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近邻估计值$\hat{m}_k (x) $。
如果数据分布较为稀疏,k个最近邻会离估计点 x x x非常远,得到的估计值偏差较大,此时一般不用k近邻估计。
观察权重计算公式,k近邻估计在 J x J_x Jx内的权值是固定的,可以提取出来,即:
m ^ k ( x ) = 1 n × n k ∑ i = 1 n Y i = 1 k ∑ i = 1 n Y i \hat{m}_k (x) = \frac{1}{n} \times \frac{n}{k} \sum_{i=1}^{n}Y_i = \frac{1}{k} \sum_{i=1}^{n}Y_i m^k(x)=n1×kni=1∑nYi=k1i=1∑nYi
此时可以看做是k个最邻近的 Y Y Y的均值,相当于是不带权重的。
换个角度,k最近邻集合 J x J_x Jx是在变化的,故我们可以看成是k近邻估计有一个均匀核函数,他的带宽在变,但是带宽内的值固定不变,即带宽内的每个邻居 Y Y Y的权重相同且不变。于是,我们可以换个思路,用距离替代核函数,离估计点 x i x_i xi越近的 x x x的权重就越大,对应离 Y i Y_i Yi越近的点 Y Y Y的权重就越大,这也是核回归的思想。改写后的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 n 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}^n \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=1ndistance(Xi,X)+eps1distance(Xi,X)+eps1Yi
其中 K R ( x − X i ) K_R (x-X_i) KR(x−Xi)即为核函数(实际上是距离计算公式的变形),计算各个 x x x到中心点 X i X_i Xi的距离; e p s eps eps为一个小数,如0.00001,防止除数为0。距离计算公式有非常多种,这里只列出简单的两种:
计算距离时要注意不同特征的量纲差异(比如年龄和薪资),若不同特征之间量纲差异较大,则最好先对特征进行标准化或归一化。
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是 怎么决定的?