数据科学与机器学习(第 09 部分):K-最近邻算法(KNN)(基础篇)
📘

数据科学与机器学习(第 09 部分):K-最近邻算法(KNN)(基础篇)

第 1/3 篇

「KNN 怎么靠邻居给行情贴标签」

KNN(K-最近邻)是一种非参数监督学习分类器,核心假设很简单:相似的数据点会扎堆出现在彼此附近。 它靠“邻近度”对单个样本做分类或预测,虽也能跑回归,但实战里多当分类器用——MetaTrader 5 上做特征聚类判势时,这套思路直接可用。 它之所以被称为监督学习里最轻量的算法之一,是因为训练阶段几乎不建模型,只把样本存下来,推断时去数最近的 K 个邻居多数派是谁。 2023 年 2 月 Omega J Msigwa 在交易社区发的 KNN 系列开篇里提到,该文把算法定位为分类器来构筑,读者后续可在 MT5 用历史 K 线特征验证“物以类聚”是否成立。 外汇和贵金属波动受流动性与事件驱动,用 KNN 做形态归类时样本偏移风险高,近邻权重和 K 值都可能让结果剧烈摆动,验证前先想清楚特征维度。

◍ K-NN 不是训练出来的,是现用现查的

K-NN 在 MT5 的信号建模里常被当分类器用,比如判断某根 K 线后一段倾向于震荡还是突破;但它也能直接做回归,输出一个连续数值,例如预测下一根收盘价相对前值的偏移量。 它属于非参数算法,不对价格分布做任何先验假设,不会预设市场服从正态或某种固定周期。这对外汇和贵金属这类高波动、结构常变的市场反而务实——模型不会因假设失效而整体崩掉,但代价是样本一旦漂移,误判概率会悄悄抬升。 所谓惰性学习器,是指它不在训练阶段提取规律,而是把历史样本全存着,来新数据时任选距离度量去比对。新样本被归入与其最相似的已有类别,相似度直接决定归类结果。 实操上,在 MT5 用 K-NN 做日内贵金属信号,建议先固定 K=5 到 K=15 做交叉验证,观察不同 K 下样本外命中率的变化,再定参。外汇与贵金属属高风险品种,任何归类结论都只是概率倾向,不能直接当作方向保证。

KNN 分类的五步骨架

KNN 属于惰性学习算法,不建模型,只靠已有样本的距离关系做归类。落地到行情判定,先把「邻居数 k」定下来,k 太小易被噪声带偏,k 太大则类别边界被抹平。 步骤上先算待测点与数据集全部成员的欧氏距离,再按距离升序取前 k 个最近邻。比如 k=5 时,若 3 个邻居标记为「多头」、2 个为「空头」,新点倾向判多头。 随后在这 k 个邻居里统计各个类别的样本计数,把新数据点分给数量占优的类别。整个过程没有训练权重,换周期或品种只需重算距离,MT5 里可直接用历史 Bar 的收盘价向量跑这套逻辑。 外汇与贵金属波动受消息驱动,纯距离分类对跳空敏感,实盘前需用历史数据验证不同 k 值下的误判概率。

「k 邻居数怎么定才不卡投票」

KNN 分类里 k 代表对某个点归属投票时参考的最近邻居个数。k 选小了,分类结果容易被噪声带偏,偏差可能明显放大;k 选大了,算法在 MT5 里跑起来会变慢,实时性下降。 k 必须是奇数,否则二分类时会出现平票死局:比如 k=4,2 票投 A 类、2 票投 B 类,程序没法判定赢家。当类别数多于两个时平票概率低一些,但奇数约定依然最稳。 不手动调参时,库里默认用数据集行数的平方根并强制凑奇。实测 9 行数据 √(9)=3,直接是奇数,k 就取 3;若算出来是偶数则 +1。另一个带参构造函数允许你硬塞 k 值,但内部会复查并修正为奇数。 下面这段从矩阵最后一列扒出所有不重复类别存进 m_classesVector,是后续算距离前必须跑的一步。注意 temp_t 里被计过的项会写成 -1000 防止重复统计。 [CODE] vector CKNNNearestNeighbors::ClassVector() { vector t_vectors = Matrix.Col(m_cols-1); // 取矩阵最后一列作为目标变量列 vector temp_t = t_vectors, v = {t_vectors[0]}; // 复制目标列,v 初始化装第一个类 for (ulong i=0, count =1; i<m_rows; i++) // 外层遍历每行找不同类 { for (ulong j=0; j<m_rows; j++) { if (t_vectors[i] == temp_t[j] && temp_t[j] != -1000) // 当前类未统计过 { bool count_ready = false; // 标记是否已入 v for(ulong n=0;n<v.Size();n++) if (t_vectors[i] == v[n]) count_ready = true; // 在 v 里找到了就说明类已记录 if (!count_ready) // 没记录过才新增 { count++; v.Resize(count); // 扩容器 v[count-1] = t_vectors[i]; // 把新类塞进去 temp_t[j] = -1000; // 标记原列位置已统计 } else break; } else continue; } } return(v); } CKNNNearestNeighbors::CKNNNearestNeighbors(matrix<double> &Matrix_) { Matrix.Copy(Matrix_); k = (int)round(MathSqrt(Matrix.Rows())); // 默认 k=行数平方根 k = k%2 ==0 ? k+1 : k; // 偶数就 +1 变奇数 m_rows = Matrix.Rows(); m_cols = Matrix.Cols(); m_classesVector = ClassVector(); Print("classes vector | Neighbors ",m_classesVector); } [/CODE] 开 MT5 的 MetaEditor 把上面代码丢进你的类,用 9 行样本矩阵试构造,日志打出的 classes vector 长度应当等于实际类别数,k 显示 3。外汇与贵金属波动剧烈,这类聚类仅作信号参考,实盘高风险。

MQL5 / C++
vector CKNNNearestNeighbors::ClassVector()
{
  vector t_vectors = Matrix.Col(m_cols-class="num">1); class=class="str">"cmt">//target variables are found on the last column in the matrix
  vector temp_t = t_vectors, v = {t_vectors[class="num">0]};
  for (class="type">class="kw">ulong i=class="num">0, count =class="num">1; i<m_rows; i++) class=class="str">"cmt">//counting the different neighbors
    {
      for (class="type">class="kw">ulong j=class="num">0; j<m_rows; j++)
        {
          if (t_vectors[i] == temp_t[j] && temp_t[j] != -class="num">1000)
            {
              class="type">bool count_ready = false;
              for(class="type">class="kw">ulong n=class="num">0;n<v.Size();n++)
                if (t_vectors[i] == v[n])
                  count_ready = true;
              if (!count_ready)
                {
                  count++;
                  v.Resize(count);
                  v[count-class="num">1] = t_vectors[i];
                  temp_t[j] = -class="num">1000; class=class="str">"cmt">//modify so that it can no more be counted
                }
              else
                break;
            }
          else
            class="kw">continue;
        }
    }
  class="kw">return(v);
}
CKNNNearestNeighbors::CKNNNearestNeighbors(matrix<class="type">class="kw">double> &Matrix_)
  {
    Matrix.Copy(Matrix_);
    k = (class="type">int)round(MathSqrt(Matrix.Rows()));
    k = k%class="num">2 ==class="num">0 ? k+class="num">1 : k; class=class="str">"cmt">//make sure the value of k ia an odd number
    m_rows = Matrix.Rows();
    m_cols = Matrix.Cols();
    m_classesVector = ClassVector();
    Print("classes vector | Neighbors ",m_classesVector);
  }

◍ KNN 分类器里 K 值的奇偶处理

在 MT5 里手搓 KNN 邻近分类时,构造函数拿到用户传入的 k 后第一件事不是算距离,而是先判奇偶。若 k 为偶数,代码会直接自增 1 变成奇数,并打印提示——这么做是为了多数表决时不出现平票,分类结果倾向唯一。 上面那段日志 2022.10.31 05:40:33.825 显示传入 k=1,此时不会触发偶数分支,类向量输出为 [1,0],说明样本被归到两个类别中的第 0 类(Normal)。 训练矩阵共 9 行,列为体重(kg)、身高(cm)、类别标签;其中标签为 1 的仅 2 行(51kg/167cm 与 56kg/174cm),其余 7 行标 0。这种 2:7 的不平衡分布下,k 取太小容易受孤立的 underweight 样本干扰,实盘移植到外汇特征向量时需警惕同类问题。 外汇与贵金属行情受杠杆与跳空影响,属高风险品种;任何基于历史样本的分类信号都只是概率倾向,开 MT5 把下面代码跑一遍最能看清 k 的边界行为。

MQL5 / C++
CKNNNearestNeighbors:: CKNNNearestNeighbors(matrix<class="type">class="kw">double> &Matrix_, class="type">uint k_)
{
   k = k_;

   if (k %class="num">2 ==class="num">0)
      printf("K %d is an even number, It will be added by One so it becomes an odd Number %d",k,k=k+class="num">1);

   Matrix.Copy(Matrix_);

   m_rows = Matrix.Rows();
   m_cols = Matrix.Cols();

   m_classesVector = ClassVector();
   Print("classes vector | Neighbors ",m_classesVector);
}

   matrix Matrix =
      {class=class="str">"cmt">//weight(kg) | height(cm) | class
         {class="num">51, class="num">167,   class="num">1}, class=class="str">"cmt">//underweight
         {class="num">62, class="num">182,   class="num">0}, class=class="str">"cmt">//Normal
         {class="num">69, class="num">176,   class="num">0}, class=class="str">"cmt">//Normal
         {class="num">64, class="num">173,   class="num">0}, class=class="str">"cmt">//Normal
         {class="num">65, class="num">172,   class="num">0}, class=class="str">"cmt">//Normal
         {class="num">56, class="num">174,   class="num">1}, class=class="str">"cmt">//Underweight
         {class="num">58, class="num">169,   class="num">0}, class=class="str">"cmt">//Normal
         {class="num">57, class="num">173,   class="num">0}, class=class="str">"cmt">//Normal
         {class="num">55, class="num">170,   class="num">0}  class=class="str">"cmt">//Normal
      };

把新样本塞进距离矩阵做近邻判定

以体重 57 公斤、身高 170 厘米这个未知样本为例,算法第一步是算它和数据集里每个已知点的欧氏距离。代码里用 temp_matrix 剥掉最后一列类别标签,只留自变量,再逐行算距离,出来的向量是 [6.7082,13,13.41641,7.61577,8.24621,4.12311,1.41421,3,2],数字越小代表越靠近已知样本。 欧氏距离不是唯一选法,曼哈顿距离或直线距离也能换着用,但本例固定用平方和开根。Euclidean_distance 函数先比 v1、v2 尺寸,不一致就打印报错,一致才进循环累加差的平方并开方。 算完距离后,把结果拼回原矩阵最后一列,方便后续按列排序找近邻。k=3 时前三近的都属 Normal 类(全局编码 0),投票向量计为 3 票给 0 类、0 票给 1 类;若 k=5,则按票数最高者定类,本例仍倾向归 0 类。 为了实时交易能直接插新值要类标,把 KNNAlgorithm 从 void 改成 int 返回类别值更实用。外汇与贵金属波动大、滑点频繁,这类离线近邻判定只作辅助参考,实盘信号高风险,需自行验证。

MQL5 / C++
  vector v = {class="num">57, class="num">170};
  
  nearest_neighbors = new CKNNNearestNeighbors(Matrix); class=class="str">"cmt">//calling the constructor and passing it the matrix
  nearest_neighbors.KNNAlgorithm(v);  class=class="str">"cmt">//passing this new points to the algorithm
  vector vector_2;
  vector euc_dist;
  euc_dist.Resize(m_rows);
  
  matrix temp_matrix = Matrix;
  temp_matrix.Resize(Matrix.Rows(),Matrix.Cols()-class="num">1); class=class="str">"cmt">//remove the last column of independent variables
  
  for (class="type">class="kw">ulong i=class="num">0; i<m_rows; i++)
    {
      vector_2 = temp_matrix.Row(i);
      euc_dist[i] = Euclidean_distance(vector_,vector_2);
    }    
class="type">class="kw">double CKNNNearestNeighbors:: Euclidean_distance(const vector &v1,const vector &v2)
{
  class="type">class="kw">double dist = class="num">0;
  
  if (v1.Size() != v2.Size())
  Print(__FUNCTION__," v1 and v2 not matching in size");
  else
    {
      class="type">class="kw">double c = class="num">0;
      for (class="type">class="kw">ulong i=class="num">0; i<v1.Size(); i++)
        c += MathPow(v1[i] - v2[i], class="num">2);
      dist = MathSqrt(c);
    }
  
  class="kw">return(dist);
}
Print("Euclidean distance vector\n",euc_dist);
输出  ----------->
CS    class="num">0     class="num">19:class="num">29:class="num">09.057    TestScript   Euclidean distance vector
CS    class="num">0     class="num">19:class="num">29:class="num">09.057    TestScript   [class="num">6.7082,class="num">13,class="num">13.41641,class="num">7.61577,class="num">8.24621,class="num">4.12311,class="num">1.41421,class="num">3,class="num">2]
  if (isdebug)
    {  
      matrix dbgMatrix = Matrix; class=class="str">"cmt">//temporary debug matrix
      dbgMatrix.Resize(dbgMatrix.Rows(),dbgMatrix.Cols()+class="num">1);
      dbgMatrix.Col(euc_dist,dbgMatrix.Cols()-class="num">1);
      
      Print("Matrix w Euclidean Distance\n",dbgMatrix);
      
      ZeroMemory(dbgMatrix);
    }
CS    class="num">0     class="num">19:class="num">33:class="num">48.862    TestScript   Matrix w Euclidean Distance

常见问题

一般用归一化后的多特征向量(如收益率、波动率、成交量)算欧氏距离,单用收盘价容易受量纲影响导致贴标签偏斜。
KNN 没有显式训练权重,预测时直接在当前样本集里查最近邻投票;实盘意味着每次新tick都要重算距离矩阵,延迟随样本量线性增长。
可以,小布内置了无代码的特征工程和多算法分类,打开对应品种页选择 KNN 模板即可看到近邻标签与置信度。
可取奇数 k 规避平局,或在平局时按距离加权胜出;实盘常选 5、7、9 等奇数,黄金波动大时距离加权更稳。
会,全量重算复杂度 O(n),建议滚动窗口保留最近 500~2000 根并定期裁剪,否则高频更新时判定延迟明显上升。