一、KNN模型的实现思路:
如果一个样本在特征空间中的K个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。该方法在定类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。注意算法中的“最近”条件。
该方法的不足之处是计算量较大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最邻近点。
另一个不足点是,当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。
如果K=1, 表示在特征空间中,找到离样本最近的那个点。这个点属于哪个类别,那么样本就属于哪个类别;
如果K=5,表示在特征空间中,依次找到离样本最近的5个点(最近的点,次近的点,第三近的点,第四近的点,第五进的点),根据这5个点的类别,哪个类别占比多,那么这个样本就属于哪个类别;
二、KNN和K-means的区别
KNN属于监督学习,类别是已知的,通过对已知分类的数据进行训练和学习,找到这些不同类的特征,再对未分类的数据进行分类。
K-means属于非监督学习,事先不知道数据会分为几类,通过聚类分析将数据聚合成几个群体。聚类不需要对数据进行训练和学习。

KNN:根据9个类型,推断电影10属于什么类别

K-means:以上10个电影可以分成几类?
参考文章:
最通俗的话解释KNN,KMeans算法
三、基于R的实现:
#查看iris数据集,发现该数据集是由4个特征(萼片长度、萼片宽度、花瓣长度、花瓣宽度、)加一个标签构成;
head(iris, n=3)
#随机给定一个样本,需要判断该样本属于哪个类别?
new_sample <- c(4.8,2.9,3.7,1.7)
names(new_sample) <- c("Sepal.Length","Sepal.Width","Petal.Length","Petal.Width")
new_sample
#去掉标签列,只保留特征列
iris_features <- iris[1:4] # 还有一种写法是:iris_features <- iris[-5]
#构造一个函数,用于计算每两个点之间的距离
dist_eucl <- function(x1,x2) sqrt(sum((x1-x2) ^2))
#计算测试样本点(也就是待分类点)到其他每个样本点的距离。apply函数中的1代表按行循环;
distances <- apply(iris_features,1,function(x) dist_eucl(x,new_sample))
#对计算后的距离进行排序
distances_sorted <- sort(distances,index.return = T)
str(distances_sorted)
#选取距离最近的前5个点
nn5 <- iris[distances_sorted$ix[1:5],]
nn5
可以看到,前五个点中,有四个点都是属于versicolor类别,只有一个是virginica类别,因此,我们认为我们的测试样本点属于versicolor
四、数据预处理
1. 归一化和标准化
若数据的几个特征量纲不同,会导致偏差较大。例如白细胞计数在几千,但是其他的特征数量较少,会导致白细胞的特征使得结果出现支配地位。此时需要对各特征进行数据规范化或者数据归一化。归一化的方式有:
1)区间归一化(最小-最大归一化、离散(差)标准化):
离差标准化保留了原来数据中存在的关系,是消除量纲和数据取值范围影响的最简单方法。这种处理方法的缺点是若数值集中且某个数值很大,则规范化后各值接近于0,并且将会相差不大。(如 1, 1.2, 1.3, 1.4, 1.5, 1.6,8.4)这组数据。若将来遇到超过目前属性[min, max]取值范围的时候,会引起系统报错,需要重新确定min和max。
2)Z评分归一化(零-均值标准化):
为原始数据的均值,
为原始数据的标准差。
若输入特征呈高度偏斜,则可使用博克斯-考克斯变换。
R中的实现如下:
library("tidyverse")
library("caret")
#还是剔除标签项,保留特征列
iris_numeric <- iris[1:4]
#区间归一化。preProcess函数通常跟predict搭配使用
pp_unit <- preProcess(iris_numeric, method = c("range"))
iris_numeric_unit <- predict(pp_unit, iris_numeric)
#Z评分标准化
pp_zscore <- preProcess(iris_numeric, method = c("center","scale"))
iris_numeric_zscore <- predict(pp_zscore, iris_numeric)
#当然,对于标准化,我们也可以使用scale方法。可以看到我们使用scale方法得到的数据和使用preProcess得到的数据是一致的
iris_numeric_zscore <- scale(iris_numeric) #scale方法默认参数为:center=T,scale=T
#BoxCox转换
pp_boxcox <- preProcess(iris_numeric, method = c("BoxCox"))
iris_numeric_boxxcox <- predict(pp_boxcox, iris_numeric)
#以下是画图,可以看到虽然数据被进行了压缩,但是其分布形状并没有太大改变,我们可以使用压缩后的数据来进行模型的预估。
p1 <- ggplot(iris_numeric,aes(x=Sepal.Length)) +
geom_density(color="black",fill="black",alpha=0.4)+
ggtitle("Unnormalized")
p1
p2 <- ggplot(iris_numeric_unit,aes(x=Sepal.Length)) +
geom_density(color="black",fill="black",alpha=0.4)+
ggtitle("Unit Interval Transformation")
p2
p3 <- ggplot(iris_numeric_zscore,aes(x=Sepal.Length)) +
geom_density(color="black",fill="black",alpha=0.4)+
ggtitle("Z-Score Transformation")
p3
p4 <- ggplot(iris_numeric_boxxcox,aes(x=Sepal.Length)) +
geom_density(color="black",fill="black",alpha=0.4)+
ggtitle("Box Cox Transformation")
p4
当然,数据的预处理还包含缺失值的处理、离群值的处理、多重共线性问题的处理等等。
2. 寻找强相关性
caret包提供了findCorrelation()函数,可以把一个相关系数矩阵作为输入,还可以设定相关系数的绝对值指定一个阈值的cutoff参数。它会返回一个 (有可能长度为0)的向量。该向量会显示由于相关性需要从数据框中删除的列,cutoff的默认值是0.9
#查看各特征属性间的相关系数,并将相关系数赋给iris_cor变量
cor(iris_numeric)
iris_cor <- cor(iris_numeric)
可以看到,iris数据集中,各特征属性的相关系数矩阵如下:

由于特征属性不多,我们还可以通过肉眼观察,可看到Petal.Length和Petal.Width还是有很强的关联性的(相关系数达到了96%)。若特征属性比较多时,我们用肉眼观察就比较累了。此时可以使用findCorrelation函数来帮我们选择:
> findCorrelation(iris_cor)
[1] 3
由于findCorrelation默认是找到相关系数大于90%的特征,因此可以看到,上述返回结果为3,代表Petal.Length 与其他变量存在较强相关性,可以被删除。
如果我们设置cutoff=0.99,将发现找不到需要剔除的变量;如果我们设置cutoff=0.8,将发现我们要剔除的变量有两个:
> findCorrelation(iris_cor,cutoff=0.99)
integer(0)
> findCorrelation(iris_cor,cutoff=0.80)
[1] 3 4
3. 寻找共线性
两个变量强相关,并不能说明他们一定具有共线性。如要发现变量间的共线性,可以使用findLinearCombos函数。如下是该函数的使用示例:
#构造一个新的数据框,人为增加两列。Cmb是Sepal.Length和Petal.Width的线性表示,Cmb.N是在Cmb上再加一个随机数
> new_iris <- iris_numeric
> new_iris$Cmb <- 6.7*new_iris$Sepal.Length - 0.9*new_iris$Petal.Width
> set.seed(68)
> new_iris$Cmb.N <- new_iris$Cmb + rnorm(nrow(new_iris),sd=0.1)
> options(digits = 4) #为了显示方便,只显示四位数
> head(new_iris, n = 3)
Sepal.Length Sepal.Width Petal.Length Petal.Width Cmb Cmb.N
1 5.1 3.5 1.4 0.2 33.99 34.13
2 4.9 3.0 1.4 0.2 32.65 32.63
3 4.7 3.2 1.3 0.2 31.31 31.27
> #使用findLinearCombos函数来查找共线性
> findLinearCombos(new_iris)
$linearCombos
$linearCombos[[1]]
[1] 5 1 4
$remove
[1] 5
可以看到,利用findLinearCombos()函数,我们可以检测出严格的特征线性组合第五列(Cmb),不过有噪声就检测不到了(第六列)同时,findLinearCombos会建议我们删除第五列。
4. 寻找零方差变量
零方差变量就是值为一个常数的变量,所有观测数据在该特征属性上都是一个常数,或者绝大部份都是一个常数,导致数据在抽样过程中没有变化。这样的数据会影响模型的准确性。在R中,我们可以使用nearZeroVar函数来检测零方差变量或则近似零方差变量。
#构造一个新的数据框,人为增加两列。ZV列是一个常数6.5(零方差变量);
#Yellow除了第一行是True,其他都是False,所以是一个近似零方差变量
> newer_iris <- iris_numeric
> newer_iris$ZV <- 6.5
> newer_iris$Yellow <- ifelse(rownames(newer_iris)==1,T,F)
> head(newer_iris,n=3)
Sepal.Length Sepal.Width Petal.Length Petal.Width ZV Yellow
1 5.1 3.5 1.4 0.2 6.5 TRUE
2 4.9 3.0 1.4 0.2 6.5 FALSE
3 4.7 3.2 1.3 0.2 6.5 FALSE
>
> nearZeroVar(newer_iris)
[1] 5 6
#使用saveMetrics可以使我们看得更清晰:zeroVar表示零方差变量,nzv表示近似零方差变量
> nearZeroVar(newer_iris, saveMetrics=T)
freqRatio percentUnique zeroVar nzv
Sepal.Length 1.111 23.3333 FALSE FALSE
Sepal.Width 1.857 15.3333 FALSE FALSE
Petal.Length 1.000 28.6667 FALSE FALSE
Petal.Width 2.231 14.6667 FALSE FALSE
ZV 0.000 0.6667 TRUE TRUE
Yellow 149.000 1.3333 FALSE TRUE
5. 特征降维——PCA
PCA在前面的非监督学习中已经详细讲过,在此不做展开。之前我们讲过,在R语言有非常多的途径做主成分分析,比如自带的princomp()和prcomp()函数,还有psych包的principal()函数、gmodels包的fast.prcomp函数、FactoMineR包的PCA()函数、ade4包的dudi.pca()函数、amap包的acp()函数等。在非监督学习中我们详细介绍了prcomp函数的用法,这里我们将使用caret包中的preProcess方法来进行PCA降维处理。
# 注意,我们一次性给了多个方法,这些方法的顺序是:
# zv(零方差过滤) > nzv(近似零方差过滤) > corr(高度相关性过滤) > BoxCox / YeoJohnson / expoTrans > center(中心化) > scale(标准化) > range(归一化) > PCA
# thresh 表示PCA的累积方差比,这里取0.95,代表要累积方差比要达到95%
> pp_pca <- preProcess(iris_numeric, method=c("BoxCox", "center", "scale", "pca"), thresh=0.95)
> iris_numeric_pca <- predict(pp_pca, iris_numeric)
#可以看到,变量被压缩到了2个
> head(iris_numeric_pca, n=3)
PC1 PC2
1 -2.3 -0.47
2 -2.2 0.65
3 -2.5 0.35
#使用pp_pca$rotation来查看变量的有效载荷(权值),也就是说:
#PC1 = 𝟎. 𝟓𝟐 ∗ Se𝒑𝒂𝒍. 𝑳𝒆𝒏𝒈𝒕𝒉 − 𝟎. 𝟐𝟕 ∗ 𝑺𝒆𝒑𝒂𝒍. 𝑾𝒊𝒅𝒕𝒉 + 𝟎. 𝟓𝟖 ∗ 𝑷𝒆𝒕𝒂𝒍. 𝑳𝒆𝒏𝒈𝒕𝒉 + 𝟎. 𝟓𝟕 ∗ 𝑷𝒆𝒕𝒂𝒍. 𝑾𝒊𝒅𝒕𝒉
> pp_pca$rotation
PC1 PC2
Sepal.Length 0.52 -0.386
Sepal.Width -0.27 -0.920
Petal.Length 0.58 -0.049
Petal.Width 0.57 -0.037
五、划分训练集和测试集
通常测试集占15%~30%。如果数据较少划分为训练集占比85%,测试集占比15%,如果数据较多划分为70%/30%。在R中,可以使用caret包中的createDataPartition()函数来进行数据集的划分。
还记得我们在非监督学习上学习的如何进行数据集划分么?当时我们使用的是sample函数来进行随机抽样。
sample(x, size, replace = FALSE, prob = NULL)
x表示要抽样的向量,size表示要抽样的次数,replace代表是否是放回抽样,默认是不放回抽样。prob也是一个向量,用于标示待抽样向量中每个元素的权重。
train = sample(taskC$nrow, 0.8 * taskC$nrow)
test = setdiff(seq_len(taskC$nrow), train)
这里,我们使用了0.8*taskC$nrow
,表示要抽取80%的数据。这是一种简便写法。当x是一个大于零的整数时,默认会替换成1:x的向量。
我们用setdiff来获取两个向量之间不同的元素。seq_len(taskC$nrow)
默认等价于1:taskC$nrow
。当然,我们在非监督学习中也使用了天然已经分好了训练集和测试集的数据:mclust包中的GvHD,所以不用我们考虑如何区分训练集和测试集。这里我们再使用另外一种方法来区分训练集和测试集,那就是使用caret包中的createDataPartition()函数来进行数据集的划分。
#随机抽样。该函数将会随机抽取80%的数据。list=false表示返回是一个矩阵而不是列表。
iris_sampling_vector <- createDataPartition(iris$Species, p = 0.8, list = FALSE)
#获取训练集数据
iris_train <- iris_numeric[iris_sampling_vector,]
#获取测试集数据
iris_test <- iris_numeric[-iris_sampling_vector,]
六、评估模型
对于回归模型,可以使用均方差(MSE)来评估模型的好坏,也即估计标准误。就是用真实值-预测值然后平方之后求和平均。简单、直观、暴力。 我们当然希望这个值越小越好。越小代表我们的估计值越接近真实值。
但是,但是,但是:MSE针对不同的模型会有不同的值。比如说预测房价那么误差单位就是万元。数子可能是3,4,5之类的。那么预测身高就可能是0.1,0.6之类的。没有什么可读性,到底多少才算好呢?不知道,那要根据模型的应用场景来。so,我们聪明的科学家们(统计学家们)又发明了一个新的指标—— 。简单点说,为了消除MSE的量纲性,我们用MSE除以整体的方差就好了。这个值一定是大于0小于1的。为了更直观的去解释和理解,再用1减去这个值,就是
。所以
。更多细节,可以参考我的另外一篇文章:《线性回归》
对于回归模型,我们用误差来评估模型的好坏,那么对于分类模型,如何评估一个模型的好坏呢?最直观的方法也是使用错误率,也就是用错误的个数除以观测的总数,来评判分类的好坏。
但是,在实际过程中,尤其是在医学中,这是有问题的。举个例子:假设某种病,他的发病率是1%,也就是说,100个人中有1个人可能会得该病。 假设我的分类器把所有人都判断为阴性。也就说,我不做任何判断,只要来一个人,我都认为是阴性。那么检测100个人,我也只会误判1个人,按照错误率来计算的话,我的错误率只有1%,也就是说,一个没有做任何判断的分类器,其成功率都有99%!这显然是有问题的。
因此,对于分类问题,尤其是二元分类问题(也就是说有两个分类情况),我们会有多个衡量指标。每个指标都有(符合)一些特定的使用场景。
对于二分类问题,我们的原始数据是被分为两类的(设他们分别是正、反类或0、1类),而在经过分类器分类之后,每一个数据样本都会被分类器认定为某一类(正(positive)或反(negative)),这也就是分类结果,最终判断其分类结果正确与否(true和false)。所以我们有一些符号设定:
把原数据集中为正类,分类后仍为正类的样本集合记为TP(true positive);
把原数据集中为正类,但分类后为反类的样本集合记为FN(false negative);
把原数据集中为反类,但分类后为正类的样本集合记为FP(false positive);
把原数据集中为反类,分类后仍为反类的样本集合记为TN(true negative);
如下表可以发现:T和F代表最终的分类结果是否正确;P和N代表分类的结果是正类或反类。

准确率(accuracy)
准确即正确的,所以公式是所有的分类正确的样本数(正类预测为正类、反类预测为反类)除以总得样本数。
查准率(precision)
查准率也叫精确度。他跟准确率的唯一差别在于,查准率只统计分类后(预测结果后)所有为正类的情况。也就是说,在预测所有为正的情况中,有多少是真正为正的。在预测所有得病的病人中,有多少是真正生病的(查得准不准)。
查全率(recall)
查准率跟查准率的区别在于,查准率是看预测结果后所有为正类的情况,而查全率是看真实数据中,所有为正的情况。也就是说,在真正生病的病人中,有多少被检测出了有病(查得全不全)。
显然,查全率和查准率是相互制约的。一般情况下,查准率高时,查全率往往偏低;而查全率高时,查准率往往偏低。
例如,我把100个病人,都预测为阳性,也就是说,所有来看病的人,我都认为得了病,那么在真正为阳性的病人是一定不会漏掉的。查全率是100%。但是查准率就是1/100,为1%;
相反,如果我把100个病人,都预测为阴性,也就是说,所有来看病的病人,我都认为是正常的,那么因为没有一个阳性,所以查准率为100%,但是查全率为0%。
如果把阳性类的观测数据错误分类为阴性类的成本很高,我们就应该偏向于得到更高的查全率而不是查准率。反之亦然。
为了综合考量查全率和查准率,我们引入F分数。
如果 β=1,代表precision和recall权重相同;
若认为 Precision 更重要,则减小 β,若认为 Recall 更重要,则增大 β.
卡巴统计量是被设计用来抵消随机因素的,它的取值空间为[-1, 1],其中1代表完全精确,-1代表完全不精确,而0是在精确度恰好等于随机猜测法的结果时出现的。它主要用于一致性检验,用来度量两个被观测对象的一致程度。卡巴统计量的公式为:,其中
是观测准确率,
是随机准确率。K=1的时候表明分类器的决策时完全与随机分类相异的(正面),K=0时表明分类器的决策与随机分类相同(即分类器没有效果)K=-1时表明分类器的决策比随机分类还要差。
因此卡巴统计量表明了该模型跟随机猜模型之间,哪个更好。如果大于0,代表该模型比随机猜一个要好,越接近1,越不是随机猜的结果。如果=0,代表该模型跟随机猜没啥两样。如果小于0,呵呵,你还是去随机猜吧。随机猜也比你这个模型效果要好。那你这个模型已经烂到一定的境界了……
在R中,我们可以使用caret包中的postResample函数来计算准确率和卡巴系数:
> postResample(knn_predictions, iris_test_labels)
Accuracy Kappa
0.97 0.95
同时,我们还可以使用table来显示混淆矩阵(交叉列联表):
> table(knn_predictions,iris_test_labels)
iris_test_labels
knn_predictions setosa versicolor virginica
setosa 10 0 0
versicolor 0 10 1
virginica 0 0 9
最后的最后,我们以KNN开头,以KNN结尾。
最前面,我们自己实现了KNN的算法。在R中,我们其实可以使用caret包的knn3函数来方便的进行分类预测。下面是完整代码:
#还是剔除标签项,保留特征列
iris_numeric <- iris[1:4]
#数据预处理,区间归一化
pp_unit <- preProcess(iris_numeric, method = c("range"))
iris_numeric_unit <- predict(pp_unit, iris_numeric)
#随机抽样。划分训练集和测试集
iris_sampling_vector <- createDataPartition(iris$Species, p = 0.8, list = FALSE)
#获取训练集数据
iris_train <- iris_numeric_unit[iris_sampling_vector,]
#获取测试集数据
iris_test <- iris_numeric_unit[-iris_sampling_vector,]
#获得模型分类数据
iris_train_labels <- iris$Species[iris_sampling_vector]
#训练模型
knn_model <- knn3(iris_train, iris_train_labels, k=5)
#根据模型对测试集进行预测,评分类型为分类或者投票
knn_predictions <- predict(knn_model, iris_test, type = "class")
#计算准确率
postResample(knn_predictions, iris_test_labels)
#显示混淆矩阵
table(knn_predictions,iris_test_labels)
网友评论