作者:Leihua Ye, UC Santa Barbara

本文介绍了一种针对初学者的K临近算法在R语言中的实现方法。

本文呈现了一种在R语言中建立起KNN模型的方式,其中包含了多种测量指标。

R语言五折交叉验证 r语言knn交叉检验_ML

背景

在机器学习的世界里,我发现K邻近算法(KNN)分类器是最直观、最容易上手的,甚至不需要引入任何数学符号。

为了决定观测样本的标签,我们观察它的邻近样本们并把邻近样本们的标签贴给感兴趣的观测样本。当然,观察一个邻近样本可能会产生偏差和错误,KNN方法就制定了一系列的规则和流程来决定最优化的邻近样本数量,比如,检验k>1的邻近样本并且采纳取大多数的规则来决定分类。


R语言五折交叉验证 r语言knn交叉检验_R语言五折交叉验证_02


“为了决定新观测样本的标签,我们就看最邻近样本。”

距离度量

为了选择最邻近的样本,我们必须定义距离的大小。对于类别数据,有汉明距离和编辑距离。本文将不会过多讨论数学问题。

什么是K折交叉验证?

在机器学习当中,交叉验证(CV)在模型选择中起着关键作用,并且拥有一系列的应用。事实上,CV有着更加直观的设计理念,并且也很直观。

简要介绍如下:

1. 将数据分成K个均匀分布的块/层

2. 选择一个块/层集作为测试集,剩下的K-1块/层作为训练集

3. 基于训练集建立ML模型

4. 仅比较测试集当中的预测值和真实值

5. 将ML模型应用到测试集,并使用每个块重复测试K次

6. 把模型的度量得分加和并求K层的平均值

如何选择K?

如同你注意到的,交叉验证比较的一点是如何为K设置值。我们记总样本量为n。从技术上来看,K可设置从1到n的任意值。

如果k=n,我们取出1个观测值作为训练集并把剩余的n-1个值作为测试集。然后在整个数据集中重复这个过程。这就叫做“留一交叉验证法” (LOOCV)。

留一交叉验证法要求较大的计算力,并且如果你的数据集过大,该法可能会无法终止。

退一步来讲,即使没有最优k值,也不能说k值越大更好。

为了选择最合适的k值,我们必须在偏差和方差之间权衡。如果k很小,我们在估计测试误差时会获得较大的偏差但方差会较小;如果k值比较大,我们的偏差会较小,方差会较大。

R语言实现

1. 软件准备

# install.packages(“ISLR”)
# install.packages(“ggplot2”) # install.packages(“plyr”)
# install.packages(“dplyr”) # install.packages(“class”)# Load libraries
library(ISLR)
library(ggplot2)
library(reshape2)
library(plyr)
library(dplyr)
library(class)# load data and clean the dataset
banking=read.csv(“bank-additional-full.csv”,sep=”;”,header=T)##check for missing data and make sure no missing data
banking[!complete.cases(banking),]#re-code qualitative (factor) variables into numeric
banking$job=recode(banking$job,“‘admin.’=1;’blue-collar’=2;’entrepreneur’=3;’
housemaid’=4;’management’=5;’retired’=6;’self-employed’=7;’services’=8;
’student’=9;’technician’=10;’unemployed’=11;’unknown’=12”)#recode variable again
banking$marital=recode(banking$marital,“‘divorced’=1;’married’=2;’single’=3;’unknown’=4”)
banking$education=recode(banking$education,“‘basic.4y’=1;’basic.6y’=2;’basic.9y’=3;’high.school’=4;’illiterate’=5;’professional.course’=6;’university.degree’=7;’unknown’=8”)
banking$default = recode(banking$default, “‘no’=1;’yes’=2;’unknown’=3”)
banking$housing = recode(banking$housing, “‘no’=1;’yes’=2;’unknown’=3”)
banking$loan=recode(banking$loan,“‘no’=1;’yes’=2;’unknown’=3”)banking$contact=recode(banking$loan,“‘cellular’=1;’telephone’=2;”)
banking$month=recode(banking$month,“‘mar’=1;’apr’=2;’may’=3;’jun’=4;’jul’=5;’aug’=6;’sep’=7;’oct’=8;’nov’=9;’dec’=10”)
banking$day_of_week=recode(banking$day_of_week,“‘mon’=1;’tue’=2;’wed’=3;’thu’=4;’fri’=5;”)
banking$poutcome = recode(banking$poutcome,“‘failure’=1;’nonexistent’=2;’success’=3;”)#remove variable “pdays”, b/c it has no variation
banking$pdays=NULL #remove variable “duration”, b/c itis collinear with the DV
banking$duration=NULL

在加载并清空初始数据集之后,通常的做法是将变量的分布可视化,检查季节性,模式,异常值,等等。

#EDA of the DV
plot(banking$y,main="Plot 1: Distribution of Dependent Variable")


R语言五折交叉验证 r语言knn交叉检验_R语言五折交叉验证_03


如图所示,结果变量(银行服务订阅)并不满足均匀分布,“否”比“是”多得多。

当我们尽力想正确分类标签的时候,监督学习是不太方便的。正如意料之中,如果大量的少数案例被分类为多数标签,假阳性的比率会变高。

事实上,不均匀分布可能会更偏好非参数ML分类器,在我的另一篇文章(使用5个分类器对罕见事件进行分类,https://medium.com/m/global-identity?

redirectUrl=https%3A%2F%2Ftowardsdatascience.com%2Fclassifying-rare-events-using-five-machine-learning-techniques-fab464573233)中介绍了KNN在与其他ML方法进行比较之后表现得更好。这个可能是参数和非参数模型中潜在的数学和统计假设导致的。

2. 数据分组

如上所述,我们需要将数据集进行分组,分为训练集和测试集,并采取k层交叉验证来选择最佳的ML模型。根据经验法则,我们通常使用“80-20”比:我们用80%的数据训练ML用剩余20%进行测试。而时间序列数据略有不同,我们将比例改为90%对10%。

#split the dataset into training and test sets randomly, but we need to set seed so as to generate the same value each time we run the codeset.seed(1)#create an index to split the data: 80% training and 20% test
index = round(nrow(banking)*0.2,digits=0)#sample randomly throughout the dataset and keep the total number equal to the value of index
test.indices = sample(1:nrow(banking), index)#80% training set
banking.train=banking[-test.indices,] #20% test set
banking.test=banking[test.indices,] #Select the training set except the DV
YTrain = banking.train$y
XTrain = banking.train %>% select(-y)# Select the test set except the DV
YTest = banking.test$y
XTest = banking.test %>% select(-y)

到目前为止,我们已经完成了数据准备并开始模型选择。

3. 训练模型

让我们编写一个新的函数(“calc_error_rate”)来记录错误分类率。该函数计算当使用训练集得到的预测标签与真正的结果标签不相匹配的比率。它测量了分类的正确性。

#define an error rate function and apply it to obtain test/training errorscalc_error_rate 
 return(mean(true.value!=predicted.value))
}

然后,我们需要另外一个函数“do.chunk()”来做k层交叉验证。该函数返回层的可能值的数据框。这一步的主要目的是为KNN选择最佳的K值。

nfold = 10
set.seed(1)# cut() divides the range into several intervals
folds = seq.int(nrow(banking.train)) %>%
 cut(breaks = nfold, labels=FALSE) %>% 
 sampledo.chunk 
 train = (folddef!=chunkid)# training indexXtr = Xdat[train,] # training set by the indexYtr = Ydat[train] # true label in training setXvl = Xdat[!train,] # test setYvl = Ydat[!train] # true label in test setpredYtr = knn(train = Xtr, test = Xtr, cl = Ytr, k = k) # predict training labelspredYvl = knn(train = Xtr, test = Xvl, cl = Ytr, k = k) # predict test labelsdata.frame(fold =chunkid, # k folds
train.error = calc_error_rate(predYtr, Ytr),#training error per fold
 val.error = calc_error_rate(predYvl, Yvl)) # test error per fold
 }# set error.folds to save validation errors
error.folds=NULL# create a sequence of data with an interval of 10
kvec = c(1, seq(10, 50, length.out=5))set.seed(1)for (j in kvec){
 tmp = ldply(1:nfold, do.chunk, # apply do.function to each fold
 folddef=folds, Xdat=XTrain, Ydat=YTrain, k=j) # required arguments
 tmp$neighbors = j # track each value of neighbors
 error.folds = rbind(error.folds, tmp) # combine the results
 }#melt() in the package reshape2 melts wide-format data into long-format data
errors = melt(error.folds, id.vars=c(“fold”,”neighbors”), value.name= “error”)

接下来的一步是为了找到使得验证错误最小化的k值。

val.error.means = errors %>%
 #select all rows of validation errors
 filter(variable== “val.error” ) %>%
 #group the selected data by neighbors
 group_by(neighbors, variable) %>%
 #cacluate CV error for each k
 summarise_each(funs(mean), error) %>%
 #remove existing grouping
 ungroup() %>%
 filter(error==min(error))#the best number of neighbors
numneighbor = max(val.error.means$neighbors)
numneighbor## [20]

在使用10层交叉验证之后,最优的邻近值数为20。

4. 一些模型的度量

#training error
set.seed(20)
pred.YTtrain = knn(train=XTrain, test=XTrain, cl=YTrain, k=20)
knn_traing_error
knn_traing_error
[1] 0.101214

训练误差为0.1。

#test error
set.seed(20)
pred.YTest = knn(train=XTrain, test=XTest, cl=YTrain, k=20)
knn_test_error 
knn_test_error
[1] 0.1100995

测试误差为0.11。

#confusion matrixconf.matrix = table(predicted=pred.YTest, true=YTest)


R语言五折交叉验证 r语言knn交叉检验_数据_04


基于以上的混淆矩阵(confusion matrix),我们可以计算以下的值并且准备好画出ROC曲线。

Accuracy = (TP +TN)/(TP+FP+FN+TN)
TPR/Recall/Sensitivity = TP/(TP+FN)
Precision = TP/(TP+FP)
Specificity = TN/(TN+FP)
FPR = 1 — Specificity = FP/(TN+FP)
F1 Score = 2*TP/(2*TP+FP+FN) = Precision*Recall /(Precision +Recall)
# Test accuracy ratesum(diag(conf.matrix)/sum(conf.matrix))[1] 0.8899005# Test error rate1 - sum(drag(conf.matrix)/sum(conf.matrix))[1] 0.1100995

你可能会注意到,测试正确率+测试错误率=1,我也提供了多种方法来计算每个值。

# ROC and AUC
knn_model = knn(train=XTrain, test=XTrain, cl=YTrain, k=20,prob=TRUE)prob


R语言五折交叉验证 r语言knn交叉检验_ML_05


综上所述,我们学习了什么是KNN并且在R语言当中建立了KNN模型。更重要的是,我们已经学到了K层交叉验证法背后的机制以及如何在R语言中实现交叉验证。