22  K-最邻近算法

本章使用一个模拟的default违约数据集,介绍三种应用最广泛的分类方法:逻辑斯谛回归、线性判别分析、K近邻算法。重点聚焦KNN算法。

关于分类模型评估的指标,具体可参看 ?sec-measure-classification
library(tidymodels)
library(tidyverse)
library(ISLR) # For the Smarket data set
library(ISLR2) # For the Bikeshare data set
library(discrim)
library(poissonreg)
library(rio)

22.1 Knn算法

KNN算法是一种基于实例的学习方法,它的核心思想是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。简单的说,就是找K个距离待遇测样本最近的点,然后根据这几个点的类别来确定新样本的类别。

22.2 逻辑斯谛回归

  1. 逻辑斯谛回归模型的输出结果与线性回归的输出结果类似,均可以通过p值的大小判断是否接受零假设。
  2. 逻辑斯谛回归 可通过设定哑变量的方式分析定性预测变量。
  3. 与线性回归类似,逻辑斯谛回归中如果预测变量之间存在多重共线性,会导致模型的不稳定性。

22.3 判别分析

22.3.1 线性判别分析

  1. 线性判别分析是一种监督学习方法,用于分类问题。
  2. 线性判别分析的目标是找到一个线性组合,使得不同类别的数据在这个线性组合上的投影尽可能远离,同一类别的数据尽可能接近。
  3. 除了分类外,线性判别分析还可用于数据的降维。

22.3.2 二次判别分析

  1. 拟合非线性关系时,可以用到二次判别分析,它时线性判别分析的一个变体。
  • 分类问题常用灵敏度和特异度作为模型能力的测试指标。
    • 灵敏度(sensitivity):指的是模型对正例的识别能力。
    • 特异度(specificity):指的是模型对负例的识别能力。
  • 分类问题中,如何设定合理的分类阈值非常重要。
    • 通常情况下,分类阈值设定为0.5。
    • 但是在实际应用中,根据业务需求,可以根据灵敏度和特异度的需求,调整分类阈值。
  • ROC曲线可以很好的展示分类模型的性能。
    • ROC曲线的横轴是假正例的比例,即真实值为被错误判断的比例;纵轴是真正例的比例,即真实值被正确判断的比例。
    • ROC曲线下的面积AUC越大,说明模型的性能越好。通常情况下,我们认为一个分类模型的AUC至少应大于0.5,
  • 如何确定一个适合的模型光滑度(模型选择),并评价这个模型的表现(模型评价)将在重抽样章节详细介绍(?sec-resampling

22.4 KNN模型实验-传统方法-kknn包

22.4.1 数据准备

load(
  file = "D:/Document/0.Study R/6.Tidymodels-with-R/data/datasetsayue/pimadiabetes.rdata"
)
dim(pimadiabetes)
[1] 768   9
str(pimadiabetes)
'data.frame':   768 obs. of  9 variables:
 $ pregnant: num  6 1 8 1 0 5 3 10 2 8 ...
 $ glucose : num  148 85 183 89 137 116 78 115 197 125 ...
 $ pressure: num  72 66 64 66 40 ...
 $ triceps : num  35 29 22.9 23 35 ...
 $ insulin : num  202.2 64.6 217.1 94 168 ...
 $ mass    : num  33.6 26.6 23.3 28.1 43.1 ...
 $ pedigree: num  0.627 0.351 0.672 0.167 2.288 ...
 $ age     : num  50 31 32 21 33 30 26 29 53 54 ...
 $ diabetes: Factor w/ 2 levels "pos","neg": 2 1 2 1 2 1 2 1 2 2 ...

各个变量的含义: - pregnant:怀孕次数 - glucose:血浆葡萄糖浓度(葡萄糖耐量试验) - pressure:舒张压(毫米汞柱) - triceps:三头肌皮褶厚度(mm) - insulin:2 小时血清胰岛素(mu U/ml) - mass:BMI - pedigree:糖尿病谱系功能,是一种用于预测糖尿病发病风险的指标,该指标是基于 家族史的糖尿病遗传风险因素的计算得出的。它计算了患者的家族成员是否患有糖尿 病以及他们与患者的亲缘关系,从而得出一个综合评分,用于预测患糖尿病的概率。 - age:年龄 - diabetes:是否有糖尿病

pimadiabetes <- pimadiabetes %>%
  mutate(
    across(where(is.numeric), \(x) scale(x))
  )
str(pimadiabetes)
'data.frame':   768 obs. of  9 variables:
 $ pregnant: num [1:768, 1] 0.64 -0.844 1.233 -0.844 -1.141 ...
  ..- attr(*, "scaled:center")= num 3.85
  ..- attr(*, "scaled:scale")= num 3.37
 $ glucose : num [1:768, 1] 0.863 -1.203 2.011 -1.072 0.503 ...
  ..- attr(*, "scaled:center")= num 122
  ..- attr(*, "scaled:scale")= num 30.5
 $ pressure: num [1:768, 1] -0.0314 -0.5244 -0.6887 -0.5244 -2.6607 ...
  ..- attr(*, "scaled:center")= num 72.4
  ..- attr(*, "scaled:scale")= num 12.2
 $ triceps : num [1:768, 1] 0.63124 -0.00231 -0.64853 -0.63586 0.63124 ...
  ..- attr(*, "scaled:center")= num 29
  ..- attr(*, "scaled:scale")= num 9.47
 $ insulin : num [1:768, 1] 0.478 -0.933 0.63 -0.631 0.127 ...
  ..- attr(*, "scaled:center")= num 156
  ..- attr(*, "scaled:scale")= num 97.6
 $ mass    : num [1:768, 1] 0.172 -0.844 -1.323 -0.626 1.551 ...
  ..- attr(*, "scaled:center")= num 32.4
  ..- attr(*, "scaled:scale")= num 6.89
 $ pedigree: num [1:768, 1] 0.468 -0.365 0.604 -0.92 5.481 ...
  ..- attr(*, "scaled:center")= num 0.472
  ..- attr(*, "scaled:scale")= num 0.331
 $ age     : num [1:768, 1] 1.4251 -0.1905 -0.1055 -1.0409 -0.0205 ...
  ..- attr(*, "scaled:center")= num 33.2
  ..- attr(*, "scaled:scale")= num 11.8
 $ diabetes: Factor w/ 2 levels "pos","neg": 2 1 2 1 2 1 2 1 2 2 ...

22.5 模型建立

22.5.1 数据划分

set.seed(123)
# 随机抽取70%的行索引
ind <- sample(1:nrow(pimadiabetes), size = 0.7 * nrow(pimadiabetes))

# 划分数据集-注意class包在进行要去掉真实值列,kknn包则不需要
train <- pimadiabetes %>%
  slice(ind)
test <- pimadiabetes %>%
  slice(-ind)

# 将真实值单独提取
truth_train <- pimadiabetes[ind, "diabetes"]
truth_test <- pimadiabetes[-ind, "diabetes"]

22.5.2 模型建立

library(kknn)

fit <- kknn(
  diabetes ~ .,
  train = train,
  test = test,
  scale = F # 不进行标准化,因为在处理数据时已经进行过标准化了
)

# summary(f)  # 查看模型的预测类别结果和预测概率
# 计算混淆矩阵
caret::confusionMatrix(fit$fitted.values, truth_test)
Confusion Matrix and Statistics

          Reference
Prediction pos neg
       pos 131  34
       neg  19  47
                                          
               Accuracy : 0.7706          
                 95% CI : (0.7109, 0.8232)
    No Information Rate : 0.6494          
    P-Value [Acc > NIR] : 4.558e-05       
                                          
                  Kappa : 0.4738          
                                          
 Mcnemar's Test P-Value : 0.05447         
                                          
            Sensitivity : 0.8733          
            Specificity : 0.5802          
         Pos Pred Value : 0.7939          
         Neg Pred Value : 0.7121          
             Prevalence : 0.6494          
         Detection Rate : 0.5671          
   Detection Prevalence : 0.7143          
      Balanced Accuracy : 0.7268          
                                          
       'Positive' Class : pos             
                                          
# 绘制ROC曲线
library(pROC)
roc <- roc(truth_test, fit$prob[, 1])
roc

Call:
roc.default(response = truth_test, predictor = fit$prob[, 1])

Data: fit$prob[, 1] in 150 controls (truth_test pos) > 81 cases (truth_test neg).
Area under the curve: 0.8491
## 绘制
plot(
  roc,
  print.auc = TRUE,
  auc.polygon = TRUE,
  max.auc.polygon = TRUE,
  auc.polygon.col = "skyblue",
  grid = c(0.1, 0.2),
  grid.col = c("green", "red"),
  print.tres = TRUE
)

输出的结果非常全面,包括:

  • 混淆矩阵
  • 准确率(accuracy)和准确率的置信区间。
  • 无信息率(No Information Rate)和p值。
  • Kappa值,Kappa一致性指数。
  • 灵敏度(sensitivity)和特异度(specificity)。
  • 阳性预测值、阴性预测值
  • 流行率(Prevalence)
  • 检出率(Detection Rate) -(Detection Prevalence)
  • 均衡准确率(Balanced Accuracy)
  • 参考类别为Positive class:“‘Positive’ Class : pos”

22.5.3 超参数调优

KNN算法只有一个超参数,就是近邻的数量k。 现在有很多好用的工具可以实现调优过程了,比如 carettidymodelsmlr3 等,但是这里演示使用e1071包实现超阐述调优的过程。

library(e1071)

set.seed(123)
tune.knn(
  x = train[, -9], # 预测变量
  y = truth_train, # 真实值
  k = 1:50
) # 近邻的数量

Parameter tuning of 'knn.wrapper':

- sampling method: 10-fold cross validation 

- best parameters:
  k
 25

- best performance: 0.2198113 

以上结果直接给出了最佳的k值为25,默认使用10折交叉验证。

22.6 tidymodels进行knn分析-实战案例

  • recipe可以查询tidymodels包中所有的预处理函数。
  • parsnip可以查询。tidymodels包中所有的模型函数。
  • 许多分类学习模型算法要求结果变量为factor类型。
?sec-modeling-basics 中详细介绍了使用tidymodels建立模型的一般步骤

选择WineData数据集进行实战案例,使用数据集中的总硫含量(total_sulfur_dioxide)和酸度(acidity)两个变量对红酒颜色(wine_color)进行分类预测。

22.6.1 数据准备

data_wine <- read_rds(
  "D:/Document/0.Study R/6.Tidymodels-with-R/data/WineData.rds"
) |>
  janitor::clean_names() |>
  select(wine_color, acidity, sulrfur = total_sulfur_dioxide) |>
  mutate(wine_color = as.factor(wine_color))
head(data_wine)
# A tibble: 6 × 3
  wine_color acidity sulrfur
  <fct>        <dbl>   <dbl>
1 red           10.8      37
2 white          6.4     213
3 white          9.4     139
4 white          8.2      90
5 white          6.4     183
6 red            6.7      38
# 数据划分
set.seed(876)
split7030 <- initial_split(data_wine, prop = 0.7, strata = wine_color)
data_train <- training(split7030)
data_test <- testing(split7030)

22.6.2 数据预处理-recipe

recipe_knn_wine <- recipe(wine_color ~ ., data = data_train) |>
  step_naomit() |> # 标准化
  step_normalize(all_predictors()) # 缺失值处理
recipe_knn_wine

22.6.3 如何选择何时该dplyrrecipes

  1. 一般情况下,tidymodels建议使用recipes进行数据预处理,因为recipes是可重复的,可以在其他数据集上使用。
  2. 在使用recipes前,最好可以使用select()函数选择需要的变量,以避免不必要的变量干扰模型,也可以使用.符号选择所有变量。
  3. 如果需要对结果变量进行转换,建议在recipes外转换。例如上述代码中,将wine_color转换为factor类型。

22.6.4 模型建立-parsnip

spec_knn_wine <- nearest_neighbor(
  neighbors = 4,
  # weight_func = "rectangular"
) |>
  set_engine("kknn") |>
  set_mode("classification")
spec_knn_wine
K-Nearest Neighbor Model Specification (classification)

Main Arguments:
  neighbors = 4

Computational engine: kknn 

22.6.5 建立工作流-workflow

# 建立工作流
wf_knn_wine <- workflow() |>
  add_recipe(recipe_knn_wine) |>
  add_model(spec_knn_wine)
wf_knn_wine
══ Workflow ════════════════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: nearest_neighbor()

── Preprocessor ────────────────────────────────────────────────────────────────
2 Recipe Steps

• step_naomit()
• step_normalize()

── Model ───────────────────────────────────────────────────────────────────────
K-Nearest Neighbor Model Specification (classification)

Main Arguments:
  neighbors = 4

Computational engine: kknn 
# 训练/拟合模型
fit_knn_wine <- wf_knn_wine |>
  fit(data_train)
fit_knn_wine
══ Workflow [trained] ══════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: nearest_neighbor()

── Preprocessor ────────────────────────────────────────────────────────────────
2 Recipe Steps

• step_naomit()
• step_normalize()

── Model ───────────────────────────────────────────────────────────────────────

Call:
kknn::train.kknn(formula = ..y ~ ., data = data, ks = min_rows(4,     data, 5))

Type of response variable: nominal
Minimal misclassification: 0.09115282
Best kernel: optimal
Best k: 4

22.6.6 预测-predict

knn_wine_pred <- predict(fit_knn_wine, new_data = data_test)
knn_wine_pred
# A tibble: 960 × 1
   .pred_class
   <fct>      
 1 white      
 2 red        
 3 white      
 4 white      
 5 white      
 6 red        
 7 white      
 8 white      
 9 red        
10 red        
# ℹ 950 more rows