ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

KNN回归预测实战:从原理到调参,小样本数据建模指南

KNN回归预测实战:从原理到调参,小样本数据建模指南 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)p1就是曼哈顿距离p2就是欧氏距离。实际项目中我很少纠结p取多少通常先用标准化后的欧氏距离跑一遍再看结果决定要不要换。真正影响更大的其实是标准化和特征选择。余弦距离一般用于文本、Embedding这类方向性数据。做标准表格型的仿真数据回归时不需要一开始就考虑它。另一个容易被忽略的细节距离度量的选择要和业务含义对齐。比如物理仿真数据里如果每个特征代表不同物理量直接用原始距离其实没有太多物理意义更好的方式是把特征归一化后再谈距离。这属于特征工程的一部分但经常被人忽略。2.2 K值过大过小都会翻车K值的作用是控制“局部”的范围。K太小模型对噪声特别敏感训练集上几乎能记住每一个点但新数据一来就崩K太大局部结构被过度平滑预测曲线变成一条没啥起伏的直线等于抛弃了KNN的优点。K1是一个极端。预测值完全取决于最近邻响应面会非常毛糙。训练集上当然误差很小但验证集上一个不好的邻居就可能带偏整个预测。我见过不少新手一跑KNN回归就直接用默认K5也不看效果其实K5在某些数据上同样太尖锐。选K的方法有很多这里说几个实际管用的经验法则K取样本数的平方根附近比如100条样本取K10左右这个起点不容易离谱。网格搜索在1到20之间扫一遍配合交叉验证看得分。这是最常用的方式。学习曲线画出不同K下的训练误差和验证误差看两条曲线在哪个K值附近分叉分叉点附近通常是比较合理的区间。一个我的个人心得不要把K完全交给网格搜索要结合业务上对噪声的认识。如果数据从采集到存储都伴随明显噪声K值可以适当调大相当于用邻域平均做平滑。相反如果数据是精度很高的仿真结果低K值反而能保留更多细节。2.3 权重策略均匀权重往往不够KNN里最常见的做法是K个邻居等权平均sklearn里对应weightsuniform。这在样本分布比较均匀、密度差不多时没问题但现实数据很少长这样。一旦某个区域样本特别密集另一个区域稀疏均匀权重会让稀疏区域的结构被少数几个远邻稀释。更好的选择是weightsdistance也就是按距离的倒数分配权重越近的邻居对预测的影响越大。预测公式变成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, cv5, scoringneg_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, cv5) 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就是这样简单但不简陋用对了地方很顺手用错了地方也很狼狈。
返回列表