☰
KNN回归预测实战:从原理到调参,小样本数据建模指南
2026/9/28 5:50:55 网站建设 项目流程

1. 为什么我又把KNN翻出来做回归预测

最近在处理一批小样本仿真数据时,我又把K近邻算法(KNN)翻出来做数据回归预测。说实话,一开始只是拿它当baseline,没想到它的表现比不少我预想的复杂模型还稳,这让我重新审视了这个“老古董”。很多人一提KNN就想到分类,但KNN用于连续值预测时有它独特的价值:规则极简单、结果可解释、几乎不需要训练,特别适合小样本、非线性、又没有明显外推需求的数据。

1.1 小样本仿真数据预测,KNN是一个很顺手的起点

仿真数据和真实线上数据最大的不同在于“干净”和“够用”。台架实验、有限元仿真、单次试验记录,这类数据往往只有几十到几百条,特征维度通常不高,但变量之间的关系常常是非线性的。这种场景下,复杂的深度学习模型很难施展,GBDT又容易在小样本上过拟合,线性模型则可能欠拟合。

KNN的优势恰好落在中间地带:它没有显式的函数形式假设,而是直接利用训练样本做局部平均。这意味着只要训练数据里某个区域有足够多的点,它就能把局部变化捕捉得很细。就算整体关系是高度非线性的、甚至存在突变,KNN也不像多项式回归那样需要提前猜函数形态。

我在实际中的习惯是:拿到任何小样本回归任务,先跑一个KNN基线,再跑一个线性模型。KNN的结果如果连线性模型都不如,说明数据本身可能很平稳或噪声很大;如果明显好于线性模型,就说明变量之间存在非线性局部结构,后面再上高斯过程回归这类更精细的模型也不迟。

1.2 KNN回归:不是“分类器”,是“局部平均器”

KNN回归的预测逻辑一句话就能说清:对于一条新样本,在特征空间中找到离它最近的K个训练样本,把这K个样本的目标值取平均,作为预测值。K等于1时就是完全跟着最近邻走;K等于训练样本数时,模型退化为全局平均值。

这个逻辑和日常生活里的“看邻居”很贴近。你要估计一套房子的成交价,最简单的方法就是看附近几套类似房子的最近成交价,取个平均数。地段越好、房型越像的权重越高,这就是KNN加权的直觉来源。

如果把KNN回归拆成一个通用公式:

y_pred = sum(w_i * y_i) / sum(w_i)

其中 i 遍历K个邻居,w_i 是邻居i的权重。权重取1时就是算术平均;权重取距离倒数时就是“越近越说了算”。

另外,KNN是典型的“懒学习”模型。训练阶段只是把数据存起来,真正计算发生在预测阶段。好处是新样本进来就能带一点“局部自适应”的味道,但坏处是如果训练集很大,每一次预测都要算距离,耗时很可观。这也是它只适合中小规模样本的原因之一。

1.3 它能做什么,不能做什么

先列能做的:

  • 样本量在几百到几千的小规模回归。
  • 特征连续、量纲可标准化,变量关系非线性但局部平滑。
  • 对结果可解释性要求高,想快速理解“哪些历史样本决定了预测结果”。
  • 作为复杂模型的基线,或者和线性回归、高斯过程回归做模型融合。

不能做的也很明确:

  • 外推。训练数据里没有的取值范围,KNN根本无法给出合理预测,因为它只会从已有邻居里平均,不会“长出”新趋势。
  • 高维稀疏数据。特征维度一旦超过二三十,距离会变得没有区分度,所谓“维度灾难”会直接把KNN拖垮。
  • 实时性要求极高的场景。训练集上百万条时,每一次预测都要全量计算距离,响应时间很难压下去。

搞清楚了边界,后面用起来就不会踩大坑。

2. 决定KNN回归好坏的三根支柱:距离、邻居数、权重

KNN回归看着简单,真正决定效果好坏的就三个核心参数:距离度量、K值、权重方式。很多教程里把这三件事一句话带过,但实际调参时每一个都有讲究。

2.1 距离度量:选错距离等于选错“邻居”的定义

距离是KNN的灵魂。你用什么方式定义两个样本“像不像”,直接决定了邻居是谁。

最常用的是欧氏距离,也就是平直空间里的直线距离,适合连续数值特征、各方向重要性一致的情况。欧氏距离的敏感点是:如果某个特征量纲很大,它会主导整个距离。比如一个特征是“温度”,取值范围0到1000,另一个特征是“压力”,取值范围0到1,计算距离时温度几乎会淹没压力。这也就是为什么标准化几乎是KNN的前置条件。

曼哈顿距离则是沿坐标轴走的距离之和,对单个维度上的极端值不如欧氏距离敏感。如果特征是稀疏的,或者你预感到噪声集中在某些方向上,曼哈顿距离往往更稳。

闵可夫斯基距离是两者的推广:

d(x, z) = ( sum(|x_j - z_j|^p) )^(1/p)

p=1就是曼哈顿距离,p=2就是欧氏距离。实际项目中我很少纠结p取多少,通常先用标准化后的欧氏距离跑一遍,再看结果决定要不要换。真正影响更大的其实是标准化和特征选择。

余弦距离一般用于文本、Embedding这类方向性数据。做标准表格型的仿真数据回归时,不需要一开始就考虑它。

另一个容易被忽略的细节:距离度量的选择要和业务含义对齐。比如物理仿真数据里,如果每个特征代表不同物理量,直接用原始距离其实没有太多物理意义,更好的方式是把特征归一化后再谈距离。这属于特征工程的一部分,但经常被人忽略。

2.2 K值:过大过小都会翻车

K值的作用是控制“局部”的范围。K太小,模型对噪声特别敏感,训练集上几乎能记住每一个点,但新数据一来就崩;K太大,局部结构被过度平滑,预测曲线变成一条没啥起伏的直线,等于抛弃了KNN的优点。

K=1是一个极端。预测值完全取决于最近邻,响应面会非常毛糙。训练集上当然误差很小,但验证集上一个不好的邻居就可能带偏整个预测。我见过不少新手一跑KNN回归就直接用默认K=5,也不看效果,其实K=5在某些数据上同样太尖锐。

选K的方法有很多,这里说几个实际管用的:

  • 经验法则:K取样本数的平方根附近,比如100条样本取K=10左右,这个起点不容易离谱。
  • 网格搜索:在1到20之间扫一遍,配合交叉验证看得分。这是最常用的方式。
  • 学习曲线:画出不同K下的训练误差和验证误差,看两条曲线在哪个K值附近分叉,分叉点附近通常是比较合理的区间。

一个我的个人心得:不要把K完全交给网格搜索,要结合业务上对噪声的认识。如果数据从采集到存储都伴随明显噪声,K值可以适当调大,相当于用邻域平均做平滑。相反,如果数据是精度很高的仿真结果,低K值反而能保留更多细节。

2.3 权重策略:均匀权重往往不够

KNN里最常见的做法是K个邻居等权平均,sklearn里对应weights='uniform'。这在样本分布比较均匀、密度差不多时没问题,但现实数据很少长这样。一旦某个区域样本特别密集,另一个区域稀疏,均匀权重会让稀疏区域的结构被少数几个远邻稀释。

更好的选择是weights='distance',也就是按距离的倒数分配权重,越近的邻居对预测的影响越大。预测公式变成:

y_pred = sum( (1 / d_i) * y_i ) / sum(1 / d_i)

这里的d_i是预测点到第i个邻居的距离。这样处理之后,预测曲面更平滑,尤其当训练样本分布不均匀时,能明显避免边界处出现“跳变”。

需要提醒一个细节:如果数据里存在重复样本,或者某个预测点恰好和训练样本完全重合,距离d会等于0,距离倒数是无穷大,计算就会出问题。实操中最好先做一次去重检查。如果真的出现了重合样本,可以给距离加上一个极小量,比如1e-10,或者在代码里对距离为0的情况单独处理。

3. 实操:从数据到可用的KNN回归预测模型

光讲原理没意思,直接走一遍完整流程。我以一份模拟的小样本仿真数据为例,特征有两个,目标值由线性组合和正弦项叠加噪声生成。这个例子足够演示从数据准备、建模、调参到评估的全过程。

3.1 数据准备与标准化:最容易被忽略的一步

拿到数据处理时,第一步是检查有没有缺失值。KNN不能容忍NaN,因为距离算不出来。此时可以用均值、中位数填,或者直接删掉包含缺失的行,关键看缺失比例。缺失比例很高时别硬填,那会扭曲邻居结构。

第二步是重复样本检查。前面说过,重复样本会让距离为0,影响权重计算。尤其当数据是仿真程序批量生成的,完全相同的输入组合可能反复出现。

第三步是标准化。这一步至关重要,因为KNN是基于距离的,量纲不一致等于让不重要的特征主导邻居选择。常用的是StandardScaler,把每个特征变成均值为0、方差为1。若数据存在较多离群点,RobustScaler会更合适,因为它用中位数和四分位距,抗干扰能力更强。

有一个新手经常犯的错:在全量数据上fit标准化器,然后用同一套标准化参数去处理训练集和测试集。这会导致信息泄漏,因为标准化器已经“见过”测试数据了。正确做法是:只拿训练集fit,再transform训练集和测试集。更省心的做法是用Pipeline,把标准化和模型包在一起。

3.2 用Pipeline和GridSearchCV完成KNN调参

下面这份代码是我日常用的模板,兼顾了标准化、交叉验证和参数搜索。

import numpy as np from sklearn.model_selection import GridSearchCV, cross_val_predict from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsRegressor from sklearn.pipeline import Pipeline from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score # 模拟小样本数据:120条 rng = np.random.default_rng(42) n = 120 x1 = rng.uniform(-3, 3, n) x2 = rng.uniform(0, 5, n) y = 2.5 * x1 - 1.2 * x2 + np.sin(x1) + rng.normal(0, 0.3, n) X = np.column_stack([x1, x2]) # Pipeline保证标准化只从训练折内学习 pipe = Pipeline([ ('scaler', StandardScaler()), ('knn', KNeighborsRegressor()) ]) param_grid = { 'knn__n_neighbors': range(1, 21), 'knn__weights': ['uniform', 'distance'] } grid = GridSearchCV(pipe, param_grid, cv=5, scoring='neg_mean_squared_error') grid.fit(X, y) print('best params:', grid.best_params_) print('best cv mse:', -grid.best_score_) # 用交叉验证得到每个样本的预测值,用于后续评估 pred = cross_val_predict(grid.best_estimator_, X, y, cv=5) print('MAE:', mean_absolute_error(y, pred)) print('RMSE:', mean_squared_error(y, pred) ** 0.5) print('R2:', r2_score(y, pred))

这段代码有几点值得细说。

先看Pipeline。它把StandardScaler和KNN放在一起,GridSearchCV在每一折交叉验证时都会对当前训练折重新fit标准化器。这样标准化的参数不会用到验证折信息,评估结果才可信。

再看参数网格。我通常会把K从1扫到20,权重选uniform和distance两个候选。扫完之后不代表结束,还要看一眼最优参数是否落在边界。如果最优K恰好是20,说明你可能还没试到更大的K,这时候该扩展搜索范围。如果最优K是1,则要警惕过拟合。

再看cross_val_predict。它返回的是每个样本在“没有见过自己的那一折”上的预测值,和真正的预留测试集行为更接近。用它来算MAE、RMSE、R²比直接拿训练集预测更有参考意义。

3.3 评估指标怎么选:MAE、RMSE与R²到底看哪个

回归评估里最常用的是这三个指标,各有侧重。

MAE是绝对误差的平均值,解释最直观。比如预测房价,平均误差5000块,任何人都能听懂。MAE对离群点不敏感,如果数据里有少量极端值,MAE不会因为它们而剧烈波动。

RMSE先平方再开方,对大误差更敏感。如果你的业务场景里大误差不可接受,比如一个样本差得很离谱就会带来严重后果,那就该重点关注RMSE。反过来说,RMSE容易被个别离群点拉高,导致你觉得模型很差,其实整体挺好。

R²衡量的是模型相对“直接猜均值”有多大的提升。R²等于0.8可以粗略理解为解释了80%的方差。但小样本下R²波动很大,单看它容易误判。所以我通常把MAE和R²放一起看:MAE决定误差的量级能不能接受,R²决定模型有没有抓住主要结构。

还有一样东西比指标更重要:残差图。把每个样本的残差画出来,横轴是预测值,纵轴是残差值。如果残差分布随预测值变大而扩散成喇叭形,说明模型在数值大的区域误差更大,KNN的局部平均可能削峰了。如果残差在某个特征方向上呈现出明显的规律,说明还有潜力做特征工程。

3.4 用可视化理解KNN在处理什么

数值指标只能说明“好坏”,可视化才能告诉你“为什么”。我至少会画两张图。

第一张:真实值与预测值的散点图。理想情况下点分布在对角线附近。如果点在低值区高于对角线、在高值区低于对角线,就是典型的“削峰”现象,说明KNN把极端值往整体均值方向拉了。这个现象在预测值范围大、样本稀疏时尤其明显。

第二张:把预测曲线画在某个主要特征上。做法是保持其他特征不变,让目标特征取一串连续值,喂给训练好的KNN模型,画出预测结果。K很小时曲线毛刺多,响应很跳跃;K很大时曲线被平滑成接近直线。如果你能直观看到这条曲线,对K值选择的理解会彻底不一样。

4. 实战中高频踩坑与扩展思路

这一部分是我最想分享的。很多问题不是原理层面的,而是实操中屡屡踩到、搜索又很难搜到完整答案的细节。

4.1 高频问题速查表

现象可能原因解决方式
预测值普遍接近训练集均值K太大,或者特征维度太高导致邻居都很远缩小K;先做特征选择或降维
训练集效果极好,测试集效果很差K太小过拟合;更可能是标准化泄漏调大K;把标准化放进Pipeline
同一个特征改个顺序后结果变了没有做标准化对连续特征统一做StandardScaler
预测出现inf或nan距离为0,比如重复样本去重;或给距离加小epsilon
特征维度超过30,模型效果暴跌维度灾难,距离失去区分度PCA降维;换基于树或有正则的模型
新样本落在训练数据范围之外,预测值平淡KNN天然不能外推考虑线性模型、高斯过程回归等替代
交叉验证得分时高时低,波动大样本太少,折数设置不合理改用留一法LeaveOneOut或增加重复次数

这里最想单独拎出来说的是“标准化泄漏”。不少人的习惯是先在全量数据上做标准化,再划分训练测试集。这样做之后交叉验证分数会偏高,但上线到真实场景就露馅,因为线上新数据根本不可能参与训练集的统计量计算。解决方式只有一个:所有预处理都必须包在交叉验证流程内部,Pipeline就是为这个场景准备的。

4.2 高斯过程回归:另一个适合小样本仿真预测的模型

搜索这个词的时候,我发现不少人和我一样关心“适合小样本仿真数据预测的模型高斯过程回归”。这背后透露出一个问题:当数据很少时,大家都不太信任纯数据驱动的模型,想要一个既稳定又能给出不确定性的方法。

高斯过程回归(GPR)的核心思路是给目标函数定义一个先验分布,然后根据观测数据更新成后验分布。它不仅给出预测均值,还能给出预测方差,这是KNN给不了的优势。小样本场景下,GPR通常能给出很平滑的插值结果,而且不易像KNN那样被个别噪声样本带偏。

但GPR不是免费的午餐。它最大的麻烦是核函数和超参数的选择,长度尺度、噪声方差这些参数需要调,而且要避免优化陷入局部最优。样本量稍微大起来,训练又要算协方差矩阵的逆,几千条数据就会明显变慢。

我自己的选择逻辑是这样的:

  • 样本几十到几百条、特征维度不高、数据平滑:GPR优先,KNN做对照。
  • 样本几百到几千条、关系复杂但局部平滑:KNN更快,GPR可能要等很久。
  • 需要给预测附上置信区间:GPR。
  • 只需要快速出基线、后续还要做特征筛选:KNN,因为它足够便宜。

另外,这两者不是对立关系。我在仿真数据项目里经常把KNN和GPR的预测结果做一个简单平均,或者塞进一个线性回归堆叠模型,往往比单一模型更稳。原因很简单:两种模型的结构误差不太一样,一个偏局部,一个偏全局平滑,融合之后能互相弥补。

4.3 KNN在股票量化分析中的边界:能做什么,不能做什么

KNN和股票量化分析经常一起出现在技术讨论里,我的看法比较保守:KNN可以作为量化研究里的一个基线工具,但离“可以直接用”还有十万八千里。

金融时间序列的本质是强非平稳、低信噪比、充满反馈。KNN的隐含假设是“历史相似会重演”,这在受市场情绪影响的行情里非常脆弱。更关键的是,KNN不能外推。股价序列中你要预测未来,本质上是一种外推任务,而不是在训练数据分布范围内的插值任务,KNN天生吃亏。

如果一定要用KNN做量化相关研究,正确的姿势是什么?

  • 不要直接预测原始价格,而是构造收益率、波动率、相对强弱这类相对平稳的派生特征。
  • 交叉验证不能随机打乱,必须按时间顺序切分,否则未来的样本会泄漏到训练集里,回测结果虚高。
  • 把KNN只看作衡量“局部相似性”的工具,比如研究哪些市场状态下历史收益率分布与当前最接近,而不是拿预测值直接下单。

在这些前提下,KNN可以用来做特征关系研究、构建基线模型、给更复杂的模型提供手工特征。但大家不要被“KNN预测股票”这种标题带节奏,它更多是教学场景里的案例,而不是能稳定赚钱的圣杯。

4.4 想把预测精度再往前推一步:特征工程和模型融合

调参调到一定程度后,再抠K值和三两个权重已经没有意义。真正能把分数推上去的,通常是特征工程。

仿真数据有个很大的红利:很多时候我们能隐约猜出变量之间的物理关系。如果业务上说目标值可能和某个特征的平方有关,那就直接构造这个平方特征;如果存在周期性,就把相位、振幅等做成显式特征。KNN虽然能拟合非线性,但它是通过“局部近似”实现的,特征本身越能反映物理结构,距离度量才越有意义。

另一个方向是模型融合。我在实践里常用的套路是:把KNN、线性回归、GPR三个模型的预测结果都生成出来,然后把它们作为新特征,喂给一个简单的线性回归或岭回归做堆叠。优势在于:每个模型犯错的地方不一样,堆叠之后能减少系统性偏差,而且这个操作在代码上只多十几行。

如果最终特征维度变得很高,记得回到维度灾难的问题。要么用PCA降维后再跑KNN,要么换对高维更友好的模型。硬着头皮调K是没有办法解决结构性问题的。

5. 最后分享几条压箱底的经验

这几条经验不一定写在常规教程里,但对我来说都是拿实际项目换来的。

5.1 标准化必须包进交叉验证,没有例外

我栽过跟头:一开始图省事,全量数据标准化之后再划分训练集和测试集,交叉验证R²非常漂亮。结果到了新数据上,预测偏差明显变大,排查半天才发现是标准化器偷看了测试集的信息。从那之后,我所有和KNN相关的代码都强制走Pipeline。

5.2 在“近邻”之前,先想清楚距离代表什么

选特征不是越全越好。KNN对无关特征非常敏感,因为高维空间里距离会被无关维度稀释。比如预测某个设备寿命,你放入了十几个传感器通道,真正相关的可能只有两三个。这种情况下,KNN找出来的“近邻”根本不像,预测自然不准。先做相关性分析或简单的特征筛选,比换距离公式有用得多。

5.3 当业务必须外推时,趁早换模型

如果预测场景经常会有新样本落在训练范围之外,老老实实换模型。KNN在插值范围内可以表现得很聪明,一旦外推,它的“聪明”就变成“死板”。线性回归、随机森林或者高斯过程回归在某些情况下也未必能外推得很好,但至少它们有显式的函数结构,结果会合理一些。KNN是彻底没救,因为预测值永远是某些训练样本目标值的加权平均,永远跳不出现有范围。

我后来养成了一个习惯:每次用KNN跑完一个项目,都会顺手存下一条边界记录,写清楚训练数据每个特征的取值范围。这个清单看起来很简单,但能在模型上线后省掉很多解释成本。KNN就是这样,简单但不简陋,用对了地方很顺手,用错了地方也很狼狈。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询