为什么 `class::knn()` 函数给出的结果与具有固定 k 的 `kknn::kknn()` 不同?

Why does the `class::knn()` function give different results from `kknn::kknn()` with a fixed k?

我正在尝试将统计学习简介中的基础 R 代码转换为 R tidymodels 生态系统。本书使用class::knn()tidymodels使用kknn::kknn()。在使用固定的 k 进行 knn 时,我得到了不同的结果。所以我去掉了 tidymodels 并尝试使用 class::knn()kknn::kknn() 进行比较,但我仍然得到了不同的结果。 class::knn使用欧几里德距离,kknn::kknn使用距离参数为2的闵可夫斯基距离,根据维基百科,这是欧几里德距离。我将 kknn 中的内核设置为“矩形”,根据文档,它是未加权的。固定k的knn建模结果不应该一样吗?

这里是(基本上)带有class::knn书中代码的基础 R:

library(ISLR2)

# base R class
train <- (Smarket$Year < 2005)
Smarket.2005 <- Smarket[!train, ]
dim(Smarket.2005)
Direction.2005 <- Smarket$Direction[!train]

train.X <- cbind(Smarket$Lag1, Smarket$Lag2)[train, ]
test.X <- cbind(Smarket$Lag1, Smarket$Lag2)[!train, ]
train.Direction <- Smarket$Direction[train]

the_k <- 3 # 30 shows larger discrepancies

library(class)
knn.pred <- knn(train.X, test.X, train.Direction, k = the_k)

这是我的 tidyverse 和 kknn::kknn 代码

# tidyverse kknn
library(tidyverse)
Smarket_train <- Smarket %>%
  filter(Year != 2005)

Smarket_test <- Smarket %>%  # Smarket.2005
  filter(Year == 2005)

library(kknn)
the_knn <- 
  kknn(
    Direction ~ Lag1 + Lag2, Smarket_train, Smarket_test, k = the_k,
    distance = 2, kernel = "rectangular"
  )

fit <- fitted(the_knn)

这显示了差异:

the_k
# class
table(Direction.2005, knn.pred)
# kknn
table(Smarket_test$Direction, fit)

我是不是在编码中犯了一个愚蠢的错误?如果不是,谁能解释一下 class::knn()kknn::kknn() 之间的区别?

好吧,这里面发生了很多事情。首先,我们从 class::knn() 的文档中看到 the classification is decided by majority vote, with ties broken at random. 所以看来我们应该从查看 class::knn() 的输出开始,看看会发生什么。

我反复打电话

which(fitted(knn.pred) != fitted(knn.pred))

过了一会儿,我得到了 28 和 66。所以这些是测试数据集中的观察结果,其中有一些随机性。要查看为什么这两个观察很麻烦,我们可以在 class::knn() 中设置 prob = TRUE 来获得预测概率。

knn.pred <- knn(train.X, test.X, train.Direction, k = the_k, prob = TRUE)

attr(knn.pred, "prob")
#>   [1] 0.6666667 0.6666667 0.6666667 0.6666667 0.6666667 1.0000000 0.6666667
#>   [8] 0.6666667 0.6666667 0.6666667 0.6666667 0.6666667 0.6666667 1.0000000
#>  [15] 0.6666667 0.6666667 0.6666667 0.6666667 0.6666667 0.6666667 0.6666667
#>  [22] 0.6666667 0.6666667 0.6666667 0.6666667 0.6666667 0.6666667 0.5000000
#>  [29] 0.6666667 0.6666667 1.0000000 0.6666667 0.6666667 0.6666667 0.6666667
#>  [36] 1.0000000 0.6666667 0.6666667 0.6666667 1.0000000 1.0000000 1.0000000
#>  [43] 0.6666667 0.6666667 0.6666667 0.6666667 1.0000000 0.6666667 1.0000000
#>  [50] 1.0000000 0.6666667 1.0000000 0.6666667 0.6666667 1.0000000 1.0000000
#>  [57] 0.6666667 0.6666667 0.6666667 1.0000000 0.6666667 0.6666667 0.6666667
#>  [64] 0.6666667 1.0000000 0.5000000 0.6666667 1.0000000 0.6666667 1.0000000
...

在这里我们看到观测值 28 和 66 的预测概率都是 0.5。但是那怎么可能因为我们有 k=3?

为了回答这个问题,我们将查看这些点的最近邻居。我将使用 RANN::nn2() 函数来计算训练集和测试集之间的距离。让我们以第一个观察为例,我们计算距离并将它们拉出来

dists <- RANN::nn2(train.X, test.X)

dists$nn.dists[1, ]
#>  [1] 0.01063015 0.05632051 0.06985700 0.08469357 0.08495881 0.08561542
#>  [7] 0.10823123 0.12003333 0.12621014 0.12657014

距离本身并没有多大作用,我们想知道的是什么 他们在训练集中的观察结果和他们的 类.

我们可以用 $nn.idx

来解决这个问题
dists$nn.idx[1, ]
#>  [1] 503 411 166 964 981 611 840 705 562 578

train.Direction[dists$nn.idx[1, 1:3]]
#> [1] Up   Down Down
#> Levels: Down Up

我们在这里看到第一个观测值的最近邻居是 UpDownDown。从而给出Down.

的分类

如果我们查看第 66 个观察结果,我们会发现一些不同的东西。请注意第三和第四最近的邻居如何具有完全相同的距离?

dists$nn.dists[66, ]
#>  [1] 0.06500000 0.06754258 0.07465253 0.07465253 0.07746612 0.07778175
#>  [7] 0.08905055 0.09651943 0.11036757 0.11928118
train.Direction[dists$nn.idx[66, 1:4]]
#> [1] Down Down Up   Up  
#> Levels: Down Up

而当我们查看他们的 类 时,有 2 个 Up 和 2 个 Down。这就是差异出现的地方。class::knn() 将所有这 4 个观察值都算作“3 个最近的邻居”,这给出了一个平局,随机打破。 kknn::kknn() 取前 3 个邻居,忽略距离上的这种关系,并预测 Down,因为前 3 个邻居有 2 个 Down 和 1 个 Up

predict(the_knn, type = "prob")[66, ]
#>           Down        Up
#> [1,] 0.6666667 0.3333333