R 机器学习精要(二)
原文:
annas-archive.org/md5/ab0f9114b761532dd330b0aa890f88d6译者:飞龙
第五章。步骤 2 – 应用机器学习技术
本章重点在于应用机器学习算法,这是开发解决方案的核心。有不同类型的从数据中学习的技巧。根据我们的目标,我们可以使用数据来识别对象之间的相似性或对新对象估计属性。
为了展示机器学习技术,我们从上一章中处理过的旗帜数据开始。然而,阅读本章不需要你了解前面的内容,尽管了解数据来源是推荐的。
在本章中,你将学习到:
-
识别项目的一致性组
-
探索和可视化项目组
-
估计一个新国家的语言
-
设置机器学习技术的配置
识别项目的一致性组
我们的数据描述了每个国家旗帜。有没有办法识别具有类似旗帜属性的国家组?我们可以使用一些聚类技术,这些是机器学习算法,它们使用数据定义同质集群。
从上一章的旗帜属性开始,我们构建了一个特征表并将其存储到dtFeatures.txt文件中。为了将文件加载到 R 中,第一步是使用setwd定义包含文件的目录。然后,我们可以使用read.table将文件加载到dfFeatures数据框中,并将其转换为dtFeatures数据表,如下所示:
# load the flag features
setwd('<INSER YOUR DIRECTORY/PATH>")
dfFeatures <- read.table(file = 'dtFeatures.txt')
library("data.table")
dtFeatures <- data.table(dfFeatures)
让我们来看看数据,使用str,类似于前面的章节:
# explore the features
str(dtFeatures)
Classes 'data.table' and 'data.frame': 194 obs. of 38 variables:
$ language : Factor w/ 10 levels "Arabic","Chinese",..: 8 7 1 3 7 8 3 3 10 10 ...
$ red : Factor w/ 2 levels "no","yes": 2 2 2 2 2 2 1 2 1 1 ...
$ green : Factor w/ 2 levels "no","yes": 2 1 2 1 1 1 1 1 1 1 ...
$ blue : Factor w/ 2 levels "no","yes": 1 1 1 2 2 1 2 2 2 2 ...
语言列是一个因素,有 10 种语言,称为该因素的级别。所有其他列都包含描述旗帜的特征,它们是具有两个级别的因素:是和否。特征如下:
-
如果旗帜包含颜色,则
colors特征(例如,red)具有是级别 -
如果旗帜包含图案,则
patterns特征(例如,circle)具有是级别 -
后跟数字的
nBars/nStrp/nCol特征(例如,nBars3)如果旗帜有 3 条横线,则具有是级别 -
后跟颜色的
topleft/botright/mainhue特征(例如,topleftblue)如果左上部分是蓝色,则具有是级别
使用 k-means 识别组
我们的目标是识别类似旗帜的组。为此,我们可以开始使用基本的聚类算法,即k-means。
k-means 的目标是识别k(例如,八个)同质标志聚类。想象一下将所有标志分成八个聚类。其中一个包含 10 个标志,其中 7 个包含红色。假设我们有一个red属性,如果标志包含红色则为1,否则为0。我们可以说这个聚类的average flag包含红色的概率为 70%,因此其red属性为 0.7。对每个其他属性做同样的处理,我们可以定义average flag,其属性是组内的平均值。每个聚类都有一个平均标志,我们可以使用相同的方法来确定。
k-means 算法基于一个称为聚类中心的平均对象。一开始,算法将标志分为 8 个随机组并确定它们的 8 个中心。然后,k-means 将每个标志重新分配到中心最相似的组。这样,聚类更加同质化,算法可以重新计算它们的中心。经过几次迭代后,我们就有 8 个包含同质标志的组。
k-means 算法是一个非常流行的技术,R 为我们提供了kmeans函数。为了使用它,我们可以查看其帮助信息:
# K-MEANS
# see the function documentation
help(kmeans)
我们需要两个输入:
-
x:数值数据矩阵 -
centers:聚类数量(或开始时的聚类中心)
从dtFeatures开始,我们需要构建一个数值特征矩阵dtFeaturesKm。首先,我们可以将特征名称放入arrayFeatures中,并生成包含所有特征的dtFeaturesKm数据表。执行以下步骤:
-
定义包含特征名称的
arrayFeatures向量。dtFeatures方法包含第一列的属性和其余列的特征,因此我们提取除第一列之外的所有列名:arrayFeatures <- names(dtFeatures)[-1] -
定义包含特征的
dtFeaturesKm:dtFeaturesKm <- dtFeatures[, arrayFeatures, with=F] -
将通用列(例如,
red)转换为数值格式。我们可以使用as.numeric将列格式从因子转换为数值:dtFeaturesKm[, as.numeric(red)] -
新向量包含
1如果值是no,如果是yes则包含2。为了与我们的 k-means 描述使用相同的标准,我们更愿意将no属性设置为0,将yes属性设置为1。这样,当我们计算组内的平均属性时,它将是一个介于 0 和 1 之间的数字,可以看作是属性为yes的标志部分的百分比。然后,为了得到 0 和 1,我们可以使用as.numeric(red) – 1:dtFeaturesKm[, as.numeric(red) - 1]或者,我们也可以使用 ifelse 函数完成同样的操作。
-
我们需要将每个列格式转换为 0-1。
arrayFeatures数据表包含所有特征的名称,我们可以使用for循环处理每个特征。如果我们想转换包含在nameCol中的列名,我们需要使用eval-get表示法。使用eval(nameCol) :=我们重新定义列,使用get(nameCol)我们使用列的当前值,如下所示:for(nameCol in arrayFeatures) dtFeaturesKm[ , eval(nameCol) := as.numeric(get(nameCol)) - 1 ] -
现在将所有特征转换为 0-1 格式。让我们可视化它:
View(dtFeaturesKm) -
kmeans函数需要数据以矩阵形式。为了将dtFeaturesKm转换为矩阵,我们可以使用as.matrix:matrixFeatures <- as.matrix(dtFeaturesKm)
matrixFeatures数据表包含构建 k-means 算法的数据,其他kmeans输入是参数。k-means 算法不会自动检测集群数量,因此我们需要通过centers输入来指定它。给定对象集,我们可以从中识别出任意数量的集群。哪个数字最能反映数据?有一些技术允许我们定义它,但它们超出了本章的范围。我们可以定义一个合理的中心数量,例如,8:
# cluster the data using the k-means
nCenters <- 8
modelKm <- kmeans(
x = matrixFeatures,
centers = nCenters
)
modelKm函数是一个包含不同模型组件的列表。kmeans的帮助提供了关于输出的详细描述,我们可以使用names来获取元素名称。让我们看看组件:
names(modelKm)
[1] "cluster" "centers" "totss" "withinss"
[5] "tot.withinss" "betweenss" "size" "iter"
[9] "ifault"
我们可以可视化包含在centers中的集群中心,如下所示:
View(modelKm$centers)
每行定义一个中心,每列显示一个属性。所有属性都在 0 到 1 之间,它们代表具有属性等于1的集群中旗帜的百分比。例如,如果red是0.5,这意味着一半的旗帜包含红色。
我们将使用的是cluster元素,它包含一个标签,指定每个旗帜的集群。例如,如果一个集群的第一个元素是3,这意味着matrixFeatures(以及dtFeatures)中的第一个旗帜属于第三个集群。
探索集群
我们可以查看每个集群,以探索其旗帜。为了做到这一点,我们可以在定义clusterKm列时将集群添加到初始表中,如下所示:
# add the cluster to the data table
dtFeatures[, clusterKm := modelKm$cluster]
为了探索一个集群,我们可以确定其国家中有多少个国家使用每种语言。从dtFeatures开始,我们可以使用数据表聚合来总结每个集群的数据。首先,让我们定义包含集群的列:
# aggregate the data by cluster
nameCluster <- 'clusterKm'
我们想确定每个集群中有多少行。允许我们确定行数的表格命令是.N,如下所示:
dtFeatures[, list(.N), by=nameCluster]
如果我们想为集群大小指定不同的列名,我们可以在列表中指定它,如下所示:
dtFeatures[, list(nCountries=.N), by=nameCluster]
为了确定每种语言有多少个国家,我们可以使用table:
dtFeatures[, table(language)]
为了在聚合中使用table,输出应该是列表。为此,我们可以使用as.list将表转换为列表,如下所示:
dtFeatures[, as.list(table(language))]
现在,我们可以使用by对每个组应用此操作,如下所示:
dtFeatures[, as.list(table(language)), by=nameCluster]
如果我们想可视化说每种语言的国家百分比?我们可以将表中的每个值除以集群中的国家数量,如下所示:
dtFeatures[, as.list(table(language) / .N), by=nameCluster]
我们希望生成包含每个组国家数量和每种语言百分比的dtClusters。为了做到这一点,我们可以使用我们刚刚看到的命令生成两个列表。为了合并这两个列表,我们只需使用c(list1, list2),如下所示:
dtClusters <- dtFeatures[
, c(list(nCountries=.N), as.list(table(language) / .N)),
by=nameCluster
]
dtClusters的每一行代表一个聚类。nCountries列显示聚类中的国家数量,所有其他列显示每种语言的百分比。为了可视化这些数据,我们可以为每个聚类构建一个条形图。每个条形被分割成代表说每种语言的国家数量的段。barplot函数允许我们构建所需的图表,如果我们提供矩阵作为输入。每个矩阵列对应一个条形,每行定义条形分割的块。
我们需要定义一个包含语言百分比的矩阵。这可以通过执行以下步骤来完成:
-
定义包含
dtClusters语言列名称的arrayLanguages:arrayLanguages <- dtFeatures[, unique(language)] -
构建
dtBarplot包含语言列:dtBarplot <- dtClusters[, arrayLanguages, with=F] -
使用
as.matrix将dtBarplot转换为矩阵。为了构建图表,我们需要使用 R 函数t转置矩阵(反转行和列):matrixBarplot <- t(as.matrix(dtBarplot)) -
定义一个包含聚类大小的向量,即国家数量。我们将在列下显示这些数字:
nBarplot <- dtClusters[, nCountries] -
将图例名称定义为国家名称:
namesLegend <- names(dtBarplot) -
减少图例名称的长度,以避免图例与图表重叠。使用
substring,我们将名称限制为 12 个字符,如下所示:help(substring) namesLegend <- substring(namesLegend, 1, 12) -
使用
rainbow定义颜色。我们需要为namesLegend的每个元素定义一个颜色,因此颜色的数量是length(namesLegend),如下所示:arrayColors <- rainbow(length(namesLegend)) -
使用
paste定义图表标题:plotTitle <- paste('languages in each cluster of', nameCluster)
现在我们有了所有barplot输入,因此我们可以构建图表。为了确保图例不与条形重叠,我们包括xlim参数,该参数指定绘图边界,如下所示:
# build the histogram
barplot(
height = matrixBarplot,
names.arg = nBarplot,
col = arrayColors,
legend.text = namesLegend,
xlim = c(0, ncol(matrixBarplot) * 2),
main = plotTitle,
xlab = 'cluster'
)
得到的图表如下:
K-means 算法从通过随机分割数据定义的初始聚类开始执行一系列步骤。最终输出取决于每次运行算法时不同的初始随机分割。因此,如果我们多次运行 k-means,可能会得到不同的结果。然而,这个图表帮助我们识别语言组内的某些模式。例如,在第八个聚类中,几乎所有国家都说英语,因此我们可以推断出有一些使用类似国旗的英语国家。在第五个聚类中,超过一半的国家说法语,因此我们可以得出同样的结论。一些不太相关的结果是,阿拉伯语在第一个聚类中占有很高的比例,西班牙语在第七个聚类中相当相关。
我们正在使用其他聚类算法,并将以类似的方式可视化结果。为了使代码干净且紧凑,我们可以定义plotCluster函数。输入是dtFeatures特征数据表和聚类列名nameCluster。代码几乎与前面的相同,如下所示:
# define a function to build the histogram
plotCluster <- function(
dtFeatures, # data table with the features
nameCluster # name of the column defining the cluster
){
# aggregate the data by cluster
dtClusters <- dtFeatures[
, c(list(nCountries=.N), as.list(table(language) / .N)),
by=nameCluster]
# prepare the histogram inputs
arrayLanguages <- dtFeatures[, unique(language)]
dtBarplot <- dtClusters[, arrayLanguages, with=F]
matrixBarplot <- t(as.matrix(dtBarplot))
nBarplot <- dtClusters[, nCountries]
namesLegend <- names(dtBarplot)
namesLegend <- substring(namesLegend, 1, 12)
arrayColors <- rainbow(length(namesLegend))
# build the histogram
barplot(
height = matrixBarplot,
names.arg = nBarplot,
col = arrayColors,
legend.text = namesLegend,
xlim=c(0, ncol(matrixBarplot) * 2),
main = paste('languages in each cluster of', nameCluster),
xlab = 'cluster'
)
}
此函数应构建与上一个相同的直方图。让我们使用以下代码来检查它:
# visualize the histogram using the functions
plotCluster(dtFeatures, nameCluster)
另一种可视化聚类的方法是使用不同颜色为每个聚类构建世界地图。此外,我们还可以可视化语言的世界地图。
为了构建地图,我们需要安装和加载rworldmap包,如下所示:
# define a function for visualizing the world map
install.packages('rworldmap')
library(rworldmap)
此包从国家名称开始构建世界地图,即在我们的案例中是dfFeatures行的名称。我们可以将country列添加到dtFeatures中,如下所示:
dtFeatures[, country := rownames(dfFeatures)]
我们的数据相当旧,所以德国仍然分为两部分。为了在地图上可视化它,我们可以将Germany-FRG转换为Germany。同样,我们可以将USSR转换为Russia,如下所示:
dtFeatures[country == 'Germany-FRG', country := 'Germany']
dtFeatures[country == 'USSR', country := 'Russia']
现在,我们可以定义一个函数来构建显示聚类的世界地图。输入是dtFeatures数据表和要可视化的特征colPlot列名(例如,clusterKm)。另一个参数是colourPalette,它决定了地图中使用的颜色。有关更多信息,请参阅help(mapCountryData),如下所示:
plotMap <- function(
dtFeatures, # data table with the countries
colPlot # feature to visualize
colourPalette = 'negpos8' # colors
){
# function for visualizing a feature on the world map
我们定义了包含要可视化的聚类的colPlot列。在字符串的情况下,我们只使用前 12 个字符,如下所示:
# define the column to plot
dtFeatures[, colPlot := NULL]
dtFeatures[, colPlot := substring(get(colPlot), 1, 12)]
我们构建了包含我们构建图表所需数据的mapFeatures。有关更多信息,请参阅help(joinCountryData2Map)。joinCode = 'NAME'输入指定国家由其名称定义,而不是缩写。nameJoinColumn指定我们拥有国家名称的列,如下所示:
# prepare the data to plot
mapFeatures <- joinCountryData2Map(
dtFeatures[, c('country', 'colPlot'), with=F],
joinCode = 'NAME',
nameJoinColumn = 'country'
)
我们可以使用mapCountryData构建图表。我们指定使用彩虹的颜色,并且缺失数据的该国将以灰色显示,如下面的代码所示:
# build the chart
mapCountryData(
mapFeatures,
nameColumnToPlot='colPlot',
catMethod = 'categorical',
colourPalette = colourPalette,
missingCountryCol = 'gray',
mapTitle = colPlot
)
}
现在,我们可以使用plotMap在地图上可视化 k-means 聚类,如下所示:
plotMap(dtFeatures, colPlot = 'clusterKm')
我们可以看到许多亚洲国家属于第五个聚类。此外,我们可以观察到意大利、法国和爱尔兰属于同一个聚类,因为它们的旗帜相似。除此之外,很难识别出其他任何模式。
识别聚类的层次结构
识别同质群体的其他技术是层次聚类算法。这些技术通过迭代合并对象来构建聚类。一开始,我们为每个国家都有一个聚类。我们定义了两个聚类如何相似的一个度量,并在每一步中,我们识别出旗帜最相似的两组聚类并将它们合并成一个唯一的聚类。最后,我们有一个包含所有国家的聚类。
执行层次聚类的 R 函数是 hclust。让我们看看它的 help 函数:
# HIERARCHIC CLUSTERING
# function for hierarchic clustering
help(hclust)
第一个输入是 d,文档解释说它是一个差异结构,即包含所有对象之间距离的矩阵。如文档建议,我们可以使用 dist 函数来构建输入,如下所示:
# build the distance matrix
help(dist)
dist 的输入是一个描述旗帜的数值矩阵。我们已为 k-means 算法构建了 matrixDistances,因此我们可以重用它。另一个相关输入是 method,它指定了 dist 如何测量两个旗帜之间的距离。我们应该使用哪种方法?所有特征都是二进制的,因为它们有两种可能的输出,即 0 和 1。因此,距离可以是具有不同值的属性的数量。以这种方式确定距离的 method 对象是 manhattan,如下所示:
matrixDistances <- dist(matrixFeatures, method = 'manhattan')
matrixDistances 函数包含任何两个旗帜之间的差异。另一个输入是 method,它指定了聚合方法。在我们的情况下,我们将方法设置为 complete。method 有其他选项,它们定义了连接,即计算簇之间距离的方式,如下所示:
# build the hierarchic clustering model
modelHc <- hclust(d = matrixDistances, method = 'complete')
modelHc 方法包含聚类模型,我们可以使用 plot 来可视化簇。你可以查阅 hclust 的帮助来了解 plot 参数,如下所示:
# visualize the hierarchic clustering model
plot(modelHc, labels = FALSE, hang = -1)
此图表显示了算法过程。在底部,我们有所有国家,每个旗帜属于不同的簇。每条线代表一个簇,当算法合并簇时,线会汇聚。在图表的左侧,你可以看到一个表示旗帜之间距离的刻度,在每一级,算法合并彼此距离特定的簇。在顶部,所有旗帜都属于同一个簇。这个图表被称为树状图。考虑以下代码:
# define the clusters
heightCut <- 17.5
abline(h=heightCut, col='red')
我们想要识别的簇是红色线以上的簇。从 modelHc 开始识别簇的函数是 cutree,我们可以在 h 参数中指定水平线的高度,如下所示:
cutree(modelHc, h = heightCut)
现在,我们可以将簇添加到 dtFeatures 中,如下所示:
dcFeatures[, clusterHc := cutree(modelHc, h = heightCut)]
如前所述,我们可以看到每个簇中使用的语言。我们可以重用 plotCluster 和 plotMap:
# visualize the clusters
plotCluster(dtFeatures, nameCluster = 'clusterHc')
在第八个簇中,英语是主要语言。除此之外,阿拉伯语只在第一个簇中相关,法语和德语如果一起考虑,在第二个和第三个簇中相关,西班牙语在第三个簇中相关。
我们还可以用簇可视化世界地图,如下所示:
plotMap(dtFeatures, colPlot = 'clusterHc')
得到的图表如下:
与 k-means 类似,唯一有一个主要簇的大陆是亚洲。
本节描述了两种识别同质旗帜集群的流行聚类技术。它们都允许我们理解不同旗帜之间的相似性,我们可以利用这些信息作为支持来解决一些问题。
应用 k 最近邻算法
本节展示了如何使用一种简单的监督学习技术——k 最近邻(KNN),从其旗帜开始估计一个新国家的语言。在这种情况下,我们估计的是语言,这是一个categoric属性,所以我们使用分类技术。如果属性是数值的,我们会使用回归技术。我选择 KNN 的原因是它易于解释,并且有一些选项可以修改其参数以提高结果的准确性。
让我们看看 KNN 是如何工作的。我们知道 150 个国家的旗帜和语言,我们想要根据其旗帜确定一个新国家的语言。首先,我们确定与新的旗帜最相似的 10 个国家。其中,有六个西班牙语国家,两个英语国家,一个法语国家和一个阿拉伯语国家。
在这 10 个国家中,最常见的语言是西班牙语,因此我们可以预期新的旗帜属于一个讲西班牙语的国家。
KNN 基于这种方法。为了估计一个新国家的语言,我们确定旗帜最相似的K个国家。然后,我们估计新国家说的是他们中最常见的语言。
我们有一个表格,通过 37 个二进制属性描述了 194 个旗帜,这些属性可以是Yes或No。例如,mainhuegreen属性是yes,如果旗帜的主要颜色是绿色,否则是no。所有属性都描述了旗帜的颜色和图案。
与上一节类似,在修改dtFeatures之前,我们定义了包含特征名称的arrayFeatures。由于我们向dtFeatures添加了一些列,所以我们从dfFeatures中提取特征名称。然后,我们添加了包含来自dfFeatures的国家名称的country列,如下所示:
# define the feature names
arrayFeatures <- names(dfFeatures)[-1]
# add the country to dtFeatures
dtFeatures[, country := rownames(dfFeatures)]
dtFeatures[country == 'Germany-FRG', country := 'Germany']
dtFeatures[country == 'USSR', country := 'Russia']
从dtFeatures开始,我们可以应用 KNN。给定一个新的旗帜,我们如何确定最相似的 10 个旗帜?对于任何两个旗帜,我们可以测量它们之间的相似度。最简单的方法是计算两个旗帜中有多少特征值相同。它们共有的属性越多,它们就越相似。
在上一章中,我们已经探索并转换了特征,因此我们不需要处理它们。然而,我们还没有探索语言列。对于每种语言,我们可以使用table来确定说这种语言的国家数量,如下所示:
dtFeatures[, table(language)]
不同语言的国家数量差异很大。最受欢迎的语言是英语,有 43 个国家,还有一些语言只有四个国家。为了对所有语言有一个概览,我们可以通过构建图表来可视化表格。在前一节中,我们定义了plotMap,它显示了世界地图上的群体。我们可以用它来显示说每种语言的国家,如下所示:
plotMap(dtFeatures, colPlot = 'language', colourPalette = 'rainbow')
得到的图表如下:
看到一张显示说每种语言的国家地图是件好事,但它仍然有点难以理解群体的大小。更好的选择是生成一个饼图,其切片与每个群体中的国家数量成比例。R 函数是pie,如下所示:
# visualize the languages
help(pie)
pie函数需要一个输入,即包含每种语言说国家数量的向量。如果输入向量的字段有名称,它将在图表中显示。我们可以使用table构建所需的向量,如下所示:
arrayTable <- dtFeatures[, table(language)]
幸运的是,pie不需要任何其他参数:
pie(arrayTable)
得到的图表如下:
有些语言只在少数几个国家说。例如,只有 4 个斯拉夫国家。给定一个新国家,我们想要从其国旗开始确定其语言。让我们假设我们不知道这 4 个斯拉夫国家中有一个国家说的是哪种语言。如果我们考虑其 10 个最近的邻居,其中不可能有超过 3 个其他斯拉夫国家。如果在其 10 个邻居中有 4 个说英语的国家呢?尽管在其附近还有其他斯拉夫国家,但由于英语群体更大,所以算法会估计这个国家说的是英语。同样,我们也会遇到任何其他小群体的问题。像几乎所有的机器学习算法一样,KNN 无法对属于任何其他更小群体的国家进行分类。
在处理任何分类问题时,如果某些群体很小,我们就没有足够的相关信息。在这种情况下,即使是一个很好的技术也无法对属于小群体的新对象进行分类。此外,给定一个属于中等大小群体的新国家,它很可能有很多属于大群体的邻居。因此,说这些语言之一的新国家可能会被分配到大型群体中。
通过了解模型限制,我们可以定义一个可行的机器学习问题。为了避免存在小群体,我们可以合并一些群体。聚类技术使我们能够识别哪些语言群体定义得更好,相应地,我们可以将这些群体中的语言分开:英语、西班牙语、法语和德语、斯拉夫语和其他印欧语系、阿拉伯语和其他。
我们可以定义语言组来构建listGroups,其元素包含组说的语言。例如,我们可以定义包含Slavic和Other Indo-European语言的indoEu组,如下所示:
# reduce the number of groups
listGroups <- list(
english = 'English',
spanish = 'Spanish',
frger = c('French', 'German'),
indoEu = c('Slavic', 'Other Indo-European'),
arabic = 'Arabic',
other = c(
'Japanese/Turkish/Finnish/Magyar', 'Chinese', 'Others'
)
)
现在,我们可以重新定义包含语言组的language列。对于listGroups的每个元素,我们将所有语言转换为元素名称。例如,我们将Slavic和Other Indo-European转换为indoEu。
我们可以在for循环内执行此操作。所有的组名都包含在names(listGroups)列表中,因此我们可以遍历names(listGroups)的元素,如下所示:
for(nameGroup in names(listGroups)){
在这里,nameGroup定义了一个组名,listGroups[[nameGroup]]包含其语言。我们可以使用language %in% listGroups[[nameGroup]]提取说任何组语言的dtFeatures的行。然后,我们可以使用:=数据表符号将语言列重新分配给nameGroup组名,如下所示:
dtFeatures[
language %in% listGroups[[nameGroup]],
language := nameGroup
]
}
我们重新定义了language列,按语言进行分组。让我们看看它:
dtFeatures[, language]
在这里,language是一个因子,并且只有六个可能的级别,即我们的语言组。然而,你可以看到 R 在控制台打印了16 Levels: Arabic Chinese English French ... Other。原因是language列的格式是factor,它跟踪前 10 个初始值。为了只显示六个语言组,我们可以使用factor重新定义language列,如下所示:
dtFeatures[, language := factor(language)]
dtFeatures[, language]
现在我们只有六个级别。就像我们之前做的那样,我们可以使用plotMap可视化组大小数据,如下所示:
# visualize the language groups
plotMap(dtFeatures, colPlot = 'language')
得到的地图如下:
我们可以看到,每个类别的国家在地理上彼此相邻。
为了可视化新的组大小,我们可以使用pie,如下所示:
pie(dtFeatures[, table(language)])
得到的图表如下:
所有的六个组都有足够的国家。英语和其他组比其他组稍大,但大小是可比的。
现在我们可以构建 KNN 模型。R 为我们提供了包含 KNN 算法的kknn包。让我们按照以下步骤安装和加载包:
# install and load the package
install.packages("kknn")
library(kknn)
构建 KNN 的函数称为kknn,例如在包中。让我们看看它的帮助函数:
help(kknn)
第一个输入是公式,它定义了特征和输出。然后,我们必须定义一个训练集,包含用于构建模型的数据,以及一个测试集,包含应用模型的数据。我们使用训练集的所有信息,假装不知道测试集国家的语言。还有其他一些可选输入定义了一些模型参数。
所有的特征名称都包含在arrayFeatures中。为了定义输出如何依赖于特征,我们需要构建一个格式为output ~ feature1 + feature2 + …的字符串。执行以下步骤:
-
定义字符串的第一部分:
output ~:formulaKnn <- 'language ~' -
对于每个特征,使用
paste添加+ feature:for(nameFeature in arrayFeatures){ formulaKnn <- paste(formulaKnn, '+', nameFeature) } -
将字符串转换为
formula格式:formulaKnn <- formula(formulaKnn)
我们构建了包含要放入kknn中的关系的formulaKnn。
现在,我们需要从dtFeatures开始定义训练集和测试集。一个公平的分割是将 80%的数据放入训练集。为此,我们可以以 80%的概率将每个国家添加到训练集中,否则添加到测试集中。我们可以定义长度等于dtFeatures中行数的indexTrain向量。R 函数是sample,如下所示:
help(sample)
参数包括:
-
x:要放入向量的值,在这种情况下为TRUE和FALSE。 -
size:向量长度,即在我们的情况下dtFeatures中的行数。 -
replace:为了多次采样值,设置为TRUE。 -
prob:选择x中元素的概率。在我们的情况下,我们以 80%的概率选择TRUE,以 20%的概率选择FALSE。
使用我们的论点,我们可以构建indexTrain,如下所示:
# split the dataset into training and test set
indexTrain <- sample(
x=c(TRUE, FALSE),
size=nrow(dtFeatures),
replace=TRUE,
prob=c(0.8, 0.2)
)
现在,我们需要将indexTrain为TRUE的行添加到训练集中,将剩余的行添加到测试集中。我们使用简单的数据表操作提取所有indexTrain为TRUE的行,如下所示:
dtTrain <- dtFeatures[indexTrain]
为了提取测试行,我们必须使用 R 中的NOT运算符切换TRUE和FALSE,如下所示:
dtTest <- dtFeatures[!indexTrain]
现在我们有了使用kknn的所有基本参数。我们设置的其它参数是:
-
k:邻居的数量是10。 -
kernel:KNN 有选项为特征分配不同的相关性,但我们目前不使用此功能。将kernel参数设置为rectangular,我们使用基本的 KNN。 -
distance:我们想要计算两个标志之间的距离,即它们没有的共同属性的数量(类似于上一章)。为了做到这一点,我们将距离参数设置为1。有关更多信息,您可以了解闵可夫斯基距离。
让我们构建 KNN 模型:
# build the model
modelKnn <- kknn(
formula = formulaKnn,
train = dtTrain,
test = dtTest,
k = 10,
kernel = 'rectangular',
distance = 1
)
模型已从dtTrain中学习并估计了dtTest中国家的语言。正如我们在kknn的帮助中看到的那样,modelKnn是一个包含模型描述的列表。显示预测语言的组件是fitted.valued,如下所示:
# extract the fitted values
modelKnn$fitted.values
我们可以将预测的语言添加到dtTest中,以便与实际语言进行比较:
# add the estimated language to dtTest
dtTest[, languagePred := modelKnn$fitted.values]
对于dtTest中的国家,我们知道实际和预测的语言。我们可以使用sum(language == languagePred)来计算它们相同的次数。我们可以通过将正确预测的数量除以总数来衡量模型精度,即.N(行数),如下所示:
# evaluate the model
percCorrect <- dtTest[, sum(language == languagePred) / .N]
percCorrect
在这里,percCorrect根据训练/测试数据集分割有很大的变化。由于我们有不同的语言组,percCorrect并不特别高。
优化 k 最近邻算法
我们使用 37 个具有不同相关性的语言特征构建了我们的 KNN 模型。给定一个新的标志,其邻居是具有许多属性共享的标志,无论它们的相关性如何。如果一个标志具有与语言无关的不同共同属性,我们将错误地将其包括在邻域中。另一方面,如果一个标志共享一些高度相关的属性,它将不会被包括。
KNN 在存在无关属性的情况下表现较差。这个事实被称为维度诅咒,这在机器学习算法中相当常见。解决维度诅咒的一种方法是根据特征的相关性对特征进行排序,并选择最相关的。另一种在本章中不会看到的选择是使用降维技术。
在上一章的 使用过滤器或降维对特征进行排序 部分,我们使用信息增益比来衡量特征的相关性。现在,我们可以从 dtTrain 开始计算 dtGains 表,类似于上一章,从 dtTrain 开始。我们不能使用整个 dtFeatures,因为我们假装不知道测试集国家的语言。如果你想看看 information.gain 是如何工作的,你可以看看第四章,步骤 1 – 数据探索和特征工程。考虑以下示例:
# compute the information gain ratio
library('FSelector')
formulaFeat <- paste(arrayFeatures, collapse = ' + ')
formulaGain <- formula(paste('language', formulaFeat, sep = ' ~ '))
dfGains <- information.gain(language~., dtTrain)
dfGains$feature <- row.names(dfGains)
dtGains <- data.table(dfGains)
dtGains <- dtGains[order(attr_importance, decreasing = T)]
View(dtGains)
feature 列包含特征名称,attr_importance 列显示特征增益,它表示其相关性。为了选择最相关的特征,我们可以首先使用排序后的特征重建 arrayFeatures。然后,我们将能够选择顶部,如下所示:
# re-define the feature vector
arrayFeatures <- dtGains[, feature]
从 arrayFeatures 开始,给定一个 nFeatures 数量,我们想要使用前 nFeatures 个特征构建公式。为了能够为任何 nFeatures 执行此操作,我们可以定义一个构建公式的函数,如下所示:
# define a function for building the formula
buildFormula <- function(
arrayFeatures, # feature vector
nFeatures # number of features to include
){
步骤如下:
-
提取前
nFeatures个特征并将它们放入arrayFeaturesTop:arrayFeaturesTop <- arrayFeatures[1:nFeatures] -
构建公式字符串的第一部分:
formulaKnn <- paste('language', '~') -
将特征添加到公式中:
for(nameFeature in arrayFeaturesTop){ formulaKnn <- paste(formulaKnn, '+', nameFeature) } -
将
formulaKnn转换为formula格式:formulaKnn <- formula(formulaKnn) -
返回输出:
return(formulaKnn) }formulaKnnTop <- buildFormula(arrayFeatures, nFeatures = 10) formulaKnnTop
使用我们的函数,我们可以使用前 10 个特征构建 formulaKnnTop,如下所示:
现在,我们可以使用与之前相同的输入构建模型,除了 formula input 现在包含 formulaKnnTop,如下所示:
# build the model
modelKnn <- kknn(
formula = formulaKnnTop,
train = dtTrain,
test = dtTest,
k = 10,
kernel = 'rectangular',
distance = 1
)
如前所述,我们可以在名为 languagePred10 的新列中向 dtTest 添加预测的语言:
# add the output to dtTest
dtTest[, languagePredTop := modelKnn$fitted.values]
我们可以计算我们正确识别的语言的百分比:
# evaluate the model
percCorrectTop <- dtTest[, sum(language == languagePredTop) / .N]
percCorrectTop
通过选择顶部特征,我们是否取得了任何改进?为了确定哪个模型最准确,我们可以比较 percCorrect10 和 percCorrect,并确定哪个是最高的。我们随机定义了 dtTrain 和 dtTest 之间的分割,所以每次运行算法时结果都会变化。
避免维度灾难的另一个选项。旗帜由 37 个不同相关性的特征描述,我们选择了其中最相关的 10 个。这样,相似性取决于在排名前 10 的特征中共同的特征数量。如果我们有两个旗帜,只有两个排名前 10 的特征和 20 个剩余特征是共同的,它们是否比两个共同拥有三个排名前 10 的特征的旗帜相似度低?我们不是忽略其他 27 个特征,而是可以给它们一个较低的相关性,并使用它们。
有一种 KNN 的变体,称为加权 KNN,它识别每个特征的相关性并根据此构建 KNN。有不同版本的 KNN,kknn函数允许我们使用其中的一些,指定kernel参数。在我们的情况下,我们可以设置kernel = 'optimal',如下所示:
# build the weighted knn model
modelKnn <- kknn(
formula = formulaKnn,
train = dtTrain,
test = dtTest,
k = 10,
kernel = 'optimal',
distance = 1
)
如前所述,我们可以测量准确性:
# add the estimated language to dtTest
dtTest[, languagePredWeighted := modelKnn$fitted.values]
percCorrectWeighted <- dtTest[
, sum(language == languagePredWeighted) / .N
]
根据训练/测试分割,percCorrectWeighted可以高于或低于percCorrect。
我们看到了构建监督机器学习模型的不同选项。为了确定哪个表现最好,我们需要评估每个选项并优化参数。
摘要
在本章中,你学习了如何识别同质聚类并可视化聚类过程和结果。你定义了一个可行的监督机器学习问题,并使用 KNN 解决了它。你评估了模型、准确性和修改了其参数。你还对特征进行了排序并选择了最相关的。
在下一章中,你将看到一种更好的方法来评估监督学习模型的准确性。你将看到一种结构化的方法来优化模型参数和选择最相关的特征。
第六章:第 3 步 – 验证结果
在上一章中,我们从新国家的国旗开始估计其语言。为此,我们使用了 KNN 算法,这是一种监督学习算法。我们构建了 KNN 并通过对估计的语言进行交叉验证来测量其准确性。在本章中,我们将了解如何以更可靠的方式测量准确性,并将调整 KNN 参数以提高其性能。为了能够完成本章的任务,你不需要阅读上一章,尽管这样做是推荐的,这样你可以理解 KNN 算法是如何工作的。
在本章中,你将学习如何:
-
验证算法的准确性
-
调整算法参数
-
选择最相关的数据特征
-
优化参数和特征
验证机器学习模型
从描述国家、国旗及其语言的表格开始,KNN 根据国旗属性估计新国家的语言。在本章中,我们将评估 KNN 的性能。
测量算法的准确性
我们已经通过交叉验证估计的语言来评估了算法的准确性。首先,我们将数据分为两部分,即训练集和测试集。然后,我们使用训练集构建 KNN 算法来估计测试集国家的语言。计算估计语言正确的次数,我们定义了一个准确度指数,即正确猜测的百分比。准确度取决于我们放入测试集的数据。由于我们随机定义了训练集国家,每次重复交叉验证时准确度都会改变。因此,这种方法的结果不可靠。
本章的目标是使用一种可靠的技术来评估 KNN,即准确性在验证同一模型两次时不会改变。重复进行训练/测试集分割和验证多次,几乎每个国家至少会在训练集和测试集中出现一次。我们可以计算平均准确性,并将考虑训练集和测试集中的所有国家。经过几次迭代后,平均准确性将变得可靠,因为增加迭代次数不会显著改变它。
在评估 KNN 之前,我们需要加载kknn和data.table包:
# load the packages
library('kknn')
library('data.table')
我们可以定义一个函数,构建和交叉验证 KNN,使用一组定义好的参数和数据,这样我们可以快速评估任何配置的算法。由于 R 命令与上一章类似,我们将快速浏览它们。函数的输入是:
-
包含数据的表格
-
包含我们使用的特征名称的向量
-
KNN 参数
步骤如下:
-
定义哪些行属于训练集和测试集。我们构建
indexTrain,这是一个向量,指定哪些行将包含在训练集中。我们将测试集的标志设置为 10%的概率。在第五章中,步骤 2 – 应用机器学习技术,我们将概率设置为 20%,但在这章中我们将多次重复验证,所以 10%就足够了。 -
从
indexTrain开始,提取进入dtTrain和dtTest的行。 -
定义定义特征和预测属性的公式。
-
使用输入参数构建 KNN。
-
定义包含测试集估计语言的
languageFitted向量。 -
计算多少次
languageFitted与真实语言相同。 -
计算准确率指数,即预测语言和实际语言匹配的次数除以测试集中国家的数量。
这是构建函数的 R 代码。注释反映了编号的要点,如下所示:
validateKnn <- function(
dtFeatures, # data table with the features
arrayFeatures, # feature names array
k = 10, # knn parameter
kernel = 'rectangular', # knn parameter
distance = 1 # knn parameter
){
# 1 define the training/test set rows
indexTrain <- sample(
x=c(TRUE, FALSE),
size=nrow(dtFeatures),
replace=TRUE,
prob=c(0.9, 0.1)
)
# 2 define the training/test set
dtTrain <- dtFeatures[indexTrain]
dtTest <- dtFeatures[!indexTrain]
# 3 define the formula
formulaOutput <- 'language ~'
formulaFeatures <- paste(arrayFeatures, collapse = ' + ')
formulaKnn <- paste(formulaOutput, formulaFeatures)
formulaKnn <- formula(formulaKnn)
# 4 build the KNN model
modelKnn <- kknn(
formula = formulaKnn,
train = dtTrain,
test = dtTest,
k = k,
kernel = kernel,
distance = distance
)
# 5 defining the predicted language
languageFitted <- modelKnn$fitted.values
# 6 count the corrected predictions and the total
languageReal <- dtTest[, language]
nRows <- length(languageReal)
# 7 define the accuracy index
percCorrect <- sum(languageFitted == languageReal) / nRows
return(percCorrect)
}
在这里,validateKnn是验证 KNN 算法的起点。
定义平均准确率
为了使用validateKnn,我们需要定义输入,如下所示:
-
特征的数据表,如下所示:
setwd('<INSER/YOUR/DIRECTORY/PATH>") dfFeatures <- read.table(file = 'dtFeatures.txt') -
包含所有可能包含在 KNN 中的特征的向量:
arrayFeatures <- names(dfFeatures) arrayFeatures <- arrayFeatures[arrayFeatures != 'language'] -
KNN 参数可以是设置的,也可以保留为默认值。
现在,我们有了使用validateKnn所需的所有元素。我们可以使用它们的随机子集,例如,前 10 个特征。至于参数,我们可以将它们全部保留为默认值,除了k等于8,如下所示:
# evaluate a model accuracy
validateKnn(
dtFeatures = dtFeatures,
arrayFeatures = arrayFeatures[1:10],
k = 8
)
[1] 0.3571429
多次运行validateKnn,我们可以注意到每次的结果都不同,这是预期的。然而,现在我们可以定义另一个函数,该函数运行validateKnn多次。然后,我们计算准确率平均值,并将其用作可靠的性能指标。我们的新函数称为cvKnn,因为它交叉验证 KNN 定义的次数。
cvKnn参数是数据表、迭代次数、特征名称和 KNN 参数。让我们开始定义数据表和迭代次数。所有其他输入与validateKnn相同。为了使代码清晰紧凑,我们可以使用省略号(...)指定我们可以添加其他参数。然后,我们可以再次使用省略号将这些参数传递给任何函数。这意味着当我们调用validateKnn时,我们可以使用validateKnn(...)来指定cvKnn的任何额外参数都将作为validateKnn的输入。
函数步骤如下:
-
定义一个空的向量
arrayPercCorrect,它将包含准确率。 -
运行
validateKnn并定义arrayPercCorrect,它包含准确率。 -
将准确率
arrayPercCorrect添加到arrayPercCorrect中。
这是构建函数的代码:
cvKnn <- function(
dtFeatures, # data table with the features
nIterations=10, # number of iterations
... # feature names array and knn parameters
){
# 1 initialize the accuracy array
arrayPercCorrect <- c()
for(iIteration in 1:nIterations){
# 2 build and validate the knn
percCorrect <- validateKnn(dtFeatures, ...)
# 3 add the accuracy to the array
arrayPercCorrect <- c(arrayPercCorrect, percCorrect)
}
return(arrayPercCorrect)
}
现在,我们可以使用cvKnn构建和验证 KNN 500 次。然后,我们计算平均准确率作为 KNN 性能指标:
# determine the accuracies
arrayPercCorrect = cvKnn(
dtFeatures, nIterations=500,
arrayFeatures=arrayFeatures
)
# compute the average accuracy
percCorrectMean <- mean(arrayPercCorrect)
percCorrectMean
[1] 0.2941644
我们定义percCorrectMean,它可以作为准确率指标。
可视化平均准确率计算
为了看到结果在任意迭代时的变化程度,我们可以将每个步骤的准确率与平均值进行比较。首先,我们使用plot构建一个包含准确率的图表,参数如下:
-
x:这是我们想要绘制的向量(arrayPercCorrect)。 -
ylim:这是介于 0 和 1 之间的准确率。通过ylim = c(0, 1),我们指定可视化的区域在 0 和 1 之间。 -
xlab和ylab:这是坐标轴标签。 -
main:这是标题。
代码如下:
# plot the accuracy at each iteration
plot(
x = arrayPercCorrect,
ylim = c(0, 1),
xlab = 'Iteration', ylab = 'Accuracy',
main = 'Accuracy at each iteration'
)
为了将准确率与平均值进行比较,我们可以通过绘制一条红色的虚线水平线来显示平均值,如下所示:
help(abline)
abline(h=percCorrectMean, col='red', lty='dashed')
我们可以通过为最小值和最大值范围绘制水平线来可视化值的范围,如下所示:
abline(h=min(arrayPercCorrect), col='blue', lty='dashed')
abline(h=max(arrayPercCorrect), col='blue', lty='dashed')
得到的图表如下:
准确率从一个迭代到另一个迭代变化很大,范围在 0%到 70%之间。正如预期的那样,单个准确率是完全不可靠的。500 次迭代中的平均值怎么样?我们需要多少次迭代才能得到一个稳定的结果?
我们可以可视化第一次迭代的准确率指标,然后是前两次迭代的平均值,然后是前三次迭代的平均值,依此类推。如果在任何点上平均值不再变化,我们就不需要再继续了。通过构建图表,我们可以观察到达到稳定平均值所需的迭代次数。
首先,让我们定义包含累积平均值的arrayCumulate,这是直到每个迭代的局部平均值,如下所示:
# plot the average accuracy until each iteration
arrayCumulate <- c()
for(nIter in 1:length(arrayPercCorrect)){
cumulateAccuracy <- mean(arrayPercCorrect[1:nIter])
arrayCumulate <- c(arrayCumulate, cumulateAccuracy)
}
使用与之前相同的命令,我们构建一个新的图表。唯一的新的参数是type='l',它指定我们显示的是线而不是点。为了放大平均值所在的区域,我们移除了ylim参数,如下所示:
plot(
x = arrayCumulate,
type = 'l',
xlab = 'Iteration', ylab = 'Cumulate accuracy',
main = 'Average accuracy until each iteration'
)
abline(h = percCorrectMean, col = 'red', lty = 'dashed')
得到的图表如下:
我们可以注意到,准确率在 100 次迭代后几乎保持稳定。假设它不会因不同的参数配置而变化太多,我们可以使用 100 次迭代来验证 KNN 算法。
在本节中,我们看到了如何使用一组特定的特征和一些定义的参数自动评估模型性能。在接下来的章节中,我们将使用这个函数来优化模型性能。
调整参数
本节向您展示如何通过调整参数来提高 KNN 的性能。我们处理的是定义邻居数量的k参数。使用以下步骤来识别表现最佳的k参数:
-
定义我们将测试的 k 值。KNN 在本地工作,也就是说,给定一个新的国家国旗,它只识别几个相似的国旗。我们最多应该使用多少个?由于总共有不到 200 个国旗,我们不希望使用超过 50 个国旗。然后,我们应该测试 1 到 50 之间的每个 k,并可以定义包含选项的
arrayK:# define the k to test arrayK <- 1:50 -
定义迭代次数。对于
arrayK中的每个 k,我们需要构建和验证 KNN,次数足够高,由nIterations定义。在前一章中,我们了解到我们需要至少 100 次迭代才能得到有意义的 KNN 准确性:nIterations <- 100 -
评估每个 k 的准确性。
-
选择最大化准确性的 k。
最后两个步骤更详细,我们将深入探讨。
为了测量每个 k 的准确性,我们定义 dtAccuracyK 为一个空的数据表,它将包含准确性。然后,我们使用 for 循环运行每个 arrayK 中的 k 的 KNN 并添加新结果。步骤如下:
-
使用
cvKnn运行和验证 KNN。 -
定义要添加到
dtAccuracyK的行,包含准确性和 k。 -
使用
rbind将新行添加到dtAccuracyK:# validate the knn with different k dtAccuracyK <- data.table() for(k in arrayK) { # run the KNN and compute the accuracies arrayAccuracy <- cvKnn( dtFeatures, nIterations=nIterations, arrayFeatures = arrayFeatures, k = k ) # define the new data table rows rowsAccuracyK <- data.table( accuracy = arrayAccuracy, k = k ) # add the new rows to the accuracy table dtAccuracyK <- rbind( dtAccuracyK, rowsAccuracyK ) }
现在,让我们看看 result.head(dtAccuracyK):
accuracy k
1: 0.3636364 1
2: 0.4545455 1
3: 0.4000000 1
4: 0.2727273 1
5: 0.3000000 1
6: 0.2500000 1
dtAccuracyK 的每一行都包含 KNN 的一个迭代。第一列显示准确性,第二列显示迭代中使用的 k。
为了可视化结果,我们可以使用 plot。我们想要可视化的两个维度是 k 和准确性。输入如下:
-
x,y:这些是图表维度,分别是k和accuracy列 -
xlab,ylab:这些是轴标签,分别是k和accuracy -
main:这是图表标题 -
ylim:这些是 y 区域限制,分别是0和1 -
col:这是点的颜色,为灰色,以便强调我们稍后要添加的黑点
代码如下:
# plot all the accuracies
plot(
x = dtAccuracyK[, k],
y = dtAccuracyK[, accuracy],
xlab = 'K', ylab = 'Accuracy',
main = 'KNN accuracy using different k',
ylim = c(0, 1),
col = 'grey'
)
得到的图表如下:
小贴士
您也可以使用 type = 'str(dtCvK)' 而不是 type = 'o'。
我们无法注意到任何与 k 相关的差异。原因是准确性从一个迭代到另一个迭代变化很大。为了识别表现更好的 k,我们可以计算每个 k 的平均性能。我们称新的数据表为 dtCvK,因为我们正在交叉验证模型,如下所示:
# compute the average accuracy
dtCvK <- dtAccuracyK[
, list(accuracy = mean(accuracy)),
by='k'
]
View(dtCvK)
在这里,dtCvK 包含每个 k 的平均准确性。我们可以使用添加新点到当前图表的函数将它们添加到图表中。为了使点更明显,我们使用 pch = 16 显示完整点,如下所示:
# add the average accuracy to the chart
help(points)
points(
x = dtCvK[, k],
y = dtCvK[, accuracy],
pch = 16
)
图表如下:
平均准确性随 k 变化,但很难注意到差异,因为它始终在 0.3 到 0.4 之间。为了更清楚地看到差异,我们可以只绘制平均值而不可视化 y 限制,如下所示:
# plot the average accuracy
plot(
x = dtCvK[, k],
y = dtCvK[, accuracy],
xlab = 'k', ylab = 'accuracy',
main = 'average knn accuracy using different k',
type = 'o'
)
小贴士
您也可以使用 type = 'str(dtCvK)' 而不是 type = 'o'。
我们可以识别表现最佳的 k 并使用 abline 将其添加到图表中:
# identify the k performing best
kOpt <- dtCvK[accuracy == max(accuracy), k]
abline(v = kOpt, col = 'red')
小贴士
您也可以使用 kOpt <- 27 而不是 kOpt <- dtCvK[accuracy == max(accuracy), k]。
得到的图如下:
最佳 k 值为 27,如果 k 在 22 到 30 的范围内,KNN 的表现非常好。
在本章中,我们确定了表现最佳的 k。然而,还有一些其他参数我们没有优化,例如距离方法。此外,我们可以通过选择要包含的特征来改进算法,我们将在下一节中探讨。
选择要包含在模型中的数据特征
在上一节中,我们设置了一个最大化性能的 KNN 参数。另一个调整选项是定义我们用于构建模型的数据。我们的表格描述了使用 37 个特征的标志,并将它们全部包含在模型中。然而,KNN 可能仅包括其中的一小部分时表现更好。
选择特征的最简单方法是使用过滤器(如在第四章的 使用过滤器或降维对特征进行排序 部分中预期的那样,第四章,步骤 1 – 数据探索和特征工程),该过滤器估计每个特征的影响,并仅包含最相关的特征。在根据相关性对所有特征进行排序后,我们可以定义 n 参数,指定我们在模型中包含多少个这样的特征。然后,我们可以根据 n 最大化准确性,使用与上一节类似的方法。
第一步是定义如何对特征进行排序。我们可以使用信息增益率过滤器来估计每个特征的影响,同时忽略其他特征。我们已讨论过信息增益率及其局限性(请参阅第四章的 使用过滤器或降维对特征进行排序 部分,第四章,步骤 1 – 数据探索和特征工程),我们将使用相同的 R 命令,如下所示:
# rank the features
library('FSelector')
dfGains <- information.gain(
language~., dtFeatures
)
dfGains$feature <- row.names(dfGains)
dtGains <- data.table(dfGains)
dtGains <- dtGains[order(attr_importance, decreasing = T)]
arrayFeatures <- dtGains[, feature]
在这里,arrayFeatures 包含按相关性排序的特征。现在,我们可以通过选择前 n 个特征来构建模型。n 的选项是介于 1 和特征总数之间的数字,我们定义 arrayN 来包含它们,如下所示:
# define the number of features to test
arrayN <- 1:length(arrayFeatures)
为了存储每次迭代的准确性,我们定义 dtAccuracyN 为一个空数据表,并使用 for 循环迭代地添加行。步骤如下:
-
使用
cvKnn验证 KNN 并将准确性存储在arrayAccuracy中。我们将 k 参数设置为kOpt (27),即上一节中定义的最佳 k。 -
定义包含要添加行的
rowsAccuracyN数据表。 -
使用
rbind将新行添加到dtAccuracyN。
这是生成 for 循环的代码:
for(n in arrayN)
{
# 1 run the KNN and compute the accuracies
arrayAccuracy <- cvKnn(
dtFeatures,
nIterations = nIterations,
arrayFeatures = arrayFeatures[1:n],
k = kOpt
)
# 2 define the new data table rows
rowsAccuracyN <- data.table(
accuracy = arrayAccuracy,
n = n
)
# 3 add the new rows to the accuracy table
dtAccuracyN <- rbind(
dtAccuracyN,
rowsAccuracyN
)
}
在这里,dtAccuracyN包含每个迭代的准确率,取决于n。我们可以通过以下步骤构建一个包含所有准确率和它们在不同n值上的平均值的图表:
-
建立一个显示每次迭代的准确率的图表:
plot( x = dtAccuracyN[, n], y = dtAccuracyN[, accuracy], xlab = 'N', ylab = 'Accuracy', main = 'KNN accuracy using different features', ylim = c(0, 1), col = 'grey' ) -
从
dtAccuracyN开始,计算每个迭代的平均准确率:dtCvN <- dtAccuracyN[ , list(accuracy = mean(accuracy)), by='n' ] -
将平均准确率的点添加到图表中:
Points( x = dtCvN[, n], y = dtCvN[, accuracy], xlab = 'n', ylab = 'accuracy', pch = 16 )
得到的图如下:
图表显示,我们使用高值的n实现了最佳准确率。为了确定最佳的n,我们可以仅绘制它们的平均值。然后,我们定义nOpt,即表现最佳的n,并添加一个对应的红色垂直线,如图所示:
# plot the average accuracy
plot(
x = dtCvN[, n],
y = dtCvN[, accuracy],
xlab = 'N', ylab = 'Accuracy',
main = 'Average knn accuracy using different features',
type = 'o'
)
# identify the n performing best
nOpt <- dtCvN[accuracy == max(accuracy), n]
abline(v = nOpt, col = 'red')
得到的图如下:
表现最好的特征数量是15,在此之后性能缓慢下降。
在图表中,我们可以注意到有些点在添加新特征时准确率会大幅下降(例如,3,11,13)。在这些点上,我们添加的特征降低了性能。如果我们决定不包含它会怎样呢?我们可以仅使用最相关的特征来构建模型,然后添加第二个最相关的特征。如果性能有所提高,我们保留第二个特征;否则,我们丢弃它。之后,我们用同样的方法处理第三个特征,并重复此过程,直到我们添加或丢弃了每个特征。这种方法被称为包装器,它允许我们定义比过滤器更好的特征集。
在本节中,我们确定了最佳的n和最佳的k,因此我们使用它们来构建具有良好性能的 KNN。
一起调整特征和参数
在前两个部分中,我们使用所有特征(n=37)确定了最佳k。然后,使用最佳的k,我们确定了最佳的n。如果算法在k=30和n=25时表现更好,会怎样呢?我们还没有充分探索这个组合以及许多其他选项,所以可能存在比k=27和n=15表现更好的组合。
为了确定最佳选项,最简单的方法是测试所有备选方案。然而,如果变量之间存在太多的可能组合,我们可能没有足够的计算能力来测试所有这些组合。在这种情况下,我们可以使用梯度下降等优化算法来确定最佳参数。
幸运的是,在我们的案例中,我们只需要调整两个参数,并且可以测试它们可能值的一部分。例如,如果我们选择 20 个n的值和 20 个k的值,我们就有 400 种组合。为了做到这一点,我们执行以下步骤:
-
定义 k 的选项。包括所有特征,KNN 在
k=26时表现最佳,之后40就表现不佳。然而,设置较低的 n,情况可能会改变,因此我们需要测试所有可能的 k。为了限制选项数量,我们可以将测试限制在奇数。让我们使用seq生成 1 到 49 之间的所有奇数。from和to参数定义序列的开始和结束。by参数定义增量,为 2 以生成奇数。使用seq,我们构建包含所有 k 选项的arrayK,如下所示:arrayK <- seq(from = 1, to = 49, by = 2) -
定义 n 的选项。我们已经看到,算法仅使用少量特征集时表现非常糟糕,因此我们可以测试 n 的值在 10 到特征总数之间,即 37。与 k 类似,我们只包括奇数:
arrayN <- seq(from = 11, to = 37, by = 2) -
生成 k 和 n 之间所有可能的组合。为此,我们可以使用
expand.grid。给定两个或多个向量,expand.grid生成一个包含它们所有可能组合的数据框。在我们的情况下,我们生成一个从arrayK开始的k列和一个从arrayN开始的n列,如下所示:dfParameters <- expand.grid(k=arrayK, n=arrayN) -
将
dfParameters转换为数据表:dtParameters <- data.table(dfParameters)
现在,我们可以使用 head 查看 dtParameters:
head(dtParameters)
k n
1: 1 11
2: 3 11
3: 5 11
4: 7 11
5: 9 11
6: 11 11
在这里,dtParameters 包含每个 350 种组合的行。我们需要确定准确度并将它们存储在一个名为 accuracy 的新列中。为了做到这一点,我们使用一个遍历行的 for 循环。iConfig 变量是行索引,定义为介于 1 和 nrow(dtParameters) 行数之间的数字。存在不同的组合,因此这部分代码可能需要一段时间才能运行。在每次迭代后,我们使用行中包含的参数构建模型:
-
k:这具有
dtParameters[iConfig, k]参数 -
n:这具有
dtParameters[iConfig, n]参数
考虑以下代码:
# validate the knn with different k and nFeatures
for(iConfig in 1:nrow(dtParameters)){
arrayAccuracy <- cvKnn(
dtFeatures, nIterations = nIterations,
arrayFeatures = arrayFeatures[1:dtParameters[iConfig, n]],
k = dtParameters[iConfig, k]
)
现在,我们可以计算 arrayAccuracy 平均值并将其添加到 dtParameters:
# add the average accuracy to dtParameters
dtParameters[iConfig, accuracy := mean(arrayAccuracy)]
}
dtParameters 的每一行包含一个参数集及其相关的准确度。为了更方便地查看准确度,我们可以构建一个矩阵,其行对应于 n,列对应于 k。矩阵的每个元素显示准确度。为了构建矩阵,我们可以使用 reshape,如下所示:
# reshape dtParameters into a matrix
help(reshape)
reshape 语法相当复杂。在我们的情况下,我们想要构建的矩阵是 wide 格式,因此我们需要指定 direction = "wide"。其他参数定义我们使用的列,它们是:
-
v.names:此列定义矩阵值(准确度) -
idvar:此列定义矩阵行(n的值) -
timevar:此列定义矩阵列(k的值)
使用 reshape,我们可以构建如所示的 dfAccuracy 数据框:
dfAccuracy <- reshape(
data = dtParameters,
direction = "wide",
v.names = "accuracy",
idvar = "n",
timevar = "k"
)
View(dfAccuracy)
n 列包含 n 参数,我们将其删除以获得仅包含准确度的数据框。然后,我们将数据框转换为矩阵,如下所示:
dfAccuracy$n <- NULL
matrixAccuracy <- as.matrix(dfAccuracy)
现在,我们可以将 n 和 k 分别指定为行名和列名,如下所示:
rownames(matrixAccuracy) <- arrayN
colnames(matrixAccuracy) <- arrayK
View(matrixAccuracy)
为了可视化参数的准确率,我们可以构建一个热图,这是一个表示矩阵的图表。两个图表维度是 k 和 n,颜色代表值。我们可以使用 image 构建这个图表:
# plot the performance depending on k and n
help(image)
我们使用的参数是:
-
z:这是一个矩阵 -
x和y:这些是维度名称,包含在arrayN和arrayK中 -
xLab和yLab:这些是坐标轴标签 -
col:这是我们显示的颜色向量(我们可以使用heat.colors函数)
考虑以下代码:
image(
x = arrayN, y = arrayK, z = matrixAccuracy,
xlab = 'n', ylab = 'k',
col = heat.colors(100)
)
得到的图如下:
高准确率用浅黄色表示,低准确率用红色表示。我们可以注意到,我们在 k 在 9 到 19 范围内和 n 在 29 到 33 范围内达到了最佳准确率。最差性能发生在 n 低而 k 高的情况下。
让我们看看最佳性能组合是什么。考虑以下代码:
# identify the best k-n combination
kOpt <- dtParameters[accuracy == max(accuracy), k]
nOpt <- dtParameters[accuracy == max(accuracy), n]
最佳组合是 k=11 和 n=33,我们无法通过单独最大化参数来识别它。原因是,只有当我们不包括所有特征时,KNN 才会在 k=11 时表现良好。
在本节中,我们看到了一种优化两个参数的简单方法。在其他情况下,我们需要更高级的技术。
这种方法的局限性在于我们只调整了两个参数。我们可以通过调整其他 KNN 参数(如距离方法)来达到更好的性能。
摘要
在本章中,我们学习了如何将模型的性能评估为预测的平均准确率。我们了解了如何确定表示准确率的准确交叉验证指数。从交叉验证指数开始,我们调整了参数。此外,我们还学习了如何使用过滤器或 frapper 选择特征,以及如何同时调整特征和参数。本章描述了构建机器学习解决方案的最后部分,下一章将概述一些最重要的机器学习技术。
第七章。机器学习技术概述
有不同的机器学习技术,本章将概述最相关的技术。其中一些已经在前面的章节中介绍过,一些是新的。
在本章中,你将学习以下主题:
-
最相关的技术分支:监督学习和无监督学习
-
使用监督学习进行预测
-
使用无监督学习识别隐藏的模式和结构
-
这些技术的优缺点
概述
机器学习技术有不同的类别,在本章中我们将看到两个最相关的分支——监督学习和无监督学习,如下所示:
监督学习和无监督学习技术处理由特征描述的对象。监督学习技术的例子是决策树学习,无监督技术的例子是 k-means。在这两种情况下,算法都是从一组对象中学习的,区别在于它们的目标:监督技术预测已知性质的属性,而无监督技术识别新的模式。
监督学习技术预测对象的属性。算法从已知属性的训练集对象中学习,并预测其他对象的属性。监督学习技术分为两类:分类和回归。如果预测的属性是分类的,我们谈论分类;如果属性是数值的,我们谈论回归。
无监督学习技术识别一组对象的模式和结构。无监督学习的两个主要分支是聚类和降维。聚类技术根据对象的属性识别同质群体,例如 k-means。降维技术识别一组描述对象的显著特征,例如主成分分析。聚类和降维之间的区别在于所识别的属性是分类的或数值的,如下所示:
本章将展示每个分支的一些流行技术。为了说明技术,我们将重复使用第四章、步骤 1 – 数据探索和特征工程;第五章、步骤 2 – 应用机器学习技术;以及第六章、步骤 3 – 验证结果中的标志数据集,这些数据集可以在本书的支持代码包中找到。
监督学习
本章将向您展示一些流行的监督学习算法的示例。这些技术在面对商业问题时非常有用,因为它们可以预测未来的属性和结果。此外,可以测量每种技术及其/或参数的准确性,以便选择最合适的技术并以最佳方式设置它。
如预期,有两种技术类别:分类和回归。然而,大多数技术都可以在这两种情况下使用。以下每个小节介绍一个不同的算法。
K 最近邻算法
K 最近邻算法是一种监督学习算法,用于分类或回归。给定一个新对象,算法从其最相似的k个邻居对象预测其属性。K 最近邻算法是一种懒惰学习算法,因为它直接查询训练数据来做出预测。
在分类属性的情况下,算法将其估计为相似对象中最常见的。在数值属性的情况下,它计算它们之间的中位数或平均值。为了说明哪些是k个最相似的对象,KNN 使用一个相似性函数来评估两个对象有多相似。为了测量相似性,起点通常是一个表示差异的距离矩阵。然后,算法计算新对象与每个其他对象的相似性,并选择k个最相似的对象。
在我们的例子中,我们将使用国旗数据集,特征是国旗上的条纹数量和颜色数量。我们想要从其国旗属性预测的属性是新国家的语言。
训练集由一些国家组成,这些国家没有两个国家的国旗特征相同。首先,让我们可视化这些数据。我们可以显示国家在图表中,其维度是两个特征,颜色是语言,如下所示:
我们有两个新国家:
-
7 条条纹和 4 种颜色
-
3 条条纹和 7 种颜色
我们想使用 4-最近邻算法确定两个新国家的语言。我们可以将这两个国家添加到图表中,并确定每个国家的 4 个最近点,如下所示:
关于图表右侧的国家,其最近的 4 个邻居都属于其他类别,因此我们估计该国的语言为其他。另一个国家的邻域是混合的:1 个英语国家,1 个其他印欧语系国家,以及 2 个西班牙国家。最常见的语言是西班牙语,所以我们估计它是一个讲西班牙语的国家。
KNN 是一种简单且可扩展的算法,在许多情况下都能取得良好的结果。然而,在存在许多特征的情况下,相似度函数考虑了所有这些特征,包括不那么相关的特征,这使得使用距离变得困难。在这种情况下,KNN 无法识别有意义的最近邻,这个问题被称为维度诅咒。一种解决方案是通过选择最相关的特征或使用降维技术来降低维度(这是下一节的主题)。
决策树学习
决策树学习是一种监督学习算法,它构建一个分类或回归树。树的每个叶子节点代表属性估计,每个节点根据特征的某个条件对数据进行分割。
决策树学习是一种贪婪方法,因为它使用训练集来构建一个不需要你查询数据的模型。所有其他监督学习技术也都是贪婪的。
算法的目标是定义最相关的特征,并根据它将集合分成两组。然后,对于每个组,算法识别其最相关的特征,并将组中的对象分成两部分。这个过程一直进行,直到我们识别出叶子节点作为对象的小组。对于每个叶子节点,如果它是分类的,算法估计特征为众数;如果是数值的,则估计为平均值。在构建树之后,如果我们有太多的叶子节点,我们可以定义一个停止分割树的级别。这样,每个叶子节点将包含一个合理大的组。这种停止分割的过程称为剪枝。通过这种方式,我们找到了一个更简单且更准确的预测。
在我们的例子中,我们想要根据不同的旗帜属性(如颜色和图案)确定一个新国家的语言。算法从训练集构建树学习。让我们可视化它:
在任何节点,如果答案是true,我们向左走,如果答案是false,我们向右走。首先,模型识别出最相关的属性是十字形。如果一个旗帜包含十字形,我们向左走,并确定相关国家是英国。否则,我们向右走,检查旗帜是否包含蓝色。然后,我们继续检查条件,直到达到叶子节点。
假设我们没有考虑西班牙国旗来构建树。我们如何估计西班牙的语言?从顶部开始,我们检查遇到的每个节点的条件。
这些是步骤:
-
旗帜上不包含十字形,所以我们向左走。
-
旗帜包含蓝色,所以我们向右走。
-
旗帜上不包含十字架,所以
crosses = no为true,我们向左走。 -
旗帜上不包含动画图像,所以我们向右走。
-
国旗有两种主要颜色,所以
number of colors not equal to 4 or 5是true,我们向左移动。 -
国旗没有任何条形,所以我们向左移动。
国旗没有垂直条纹,所以nStrp0 = no是true,我们向左移动,如图所示:
最后,估计的语言是西班牙语。
决策树学习可以处理数值和/或分类特征和属性,因此它可以在只需要少量数据准备的不同环境中应用。此外,它适用于有大量特征的情况,这与其他算法不同。一个缺点是算法可能会过拟合,即模型过于接近数据并且比现实更复杂,尽管剪枝可以帮助解决这个问题。
线性回归
线性回归是一种统计模型,用于识别数值变量之间的关系。给定一组由y属性和x1, …,和xn特征描述的对象,该模型定义了特征与属性之间的关系。这种关系由线性函数y = a0 + a1 * x1 + … + an * xn描述,而a0, …,和an是由方法定义的参数,使得关系尽可能接近数据。
在机器学习的情况下,线性回归可以用来预测数值属性。算法从训练数据集中学习以确定参数。然后,给定一个新的对象,模型将它的特征插入到线性函数中以估计属性。
在我们的例子中,我们想要从国家的面积估计其人口。首先,让我们可视化面积(以千平方公里为单位)和人口(以百万为单位)的数据,如图下所示:
大多数国家的面积在 3000 千平方公里以下,人口在 2 亿以下,只有少数国家的面积和/或人口要高得多。因此,大多数点都集中在图表的左下角。为了分散点,我们可以使用对数面积和人口来转换特征,如图下所示:
线性回归的目标是识别一个尽可能接近数据的线性关系。在我们的例子中,我们有两个维度,因此我们可以用一条线来可视化这种关系。给定区域,线性回归估计人口位于这条线上。让我们在以下图表中查看具有对数特征的示例:
给定一个关于我们已知其面积的新国家,我们可以使用回归线来估计其人口。在图表中,有一个我们已知其面积的新国家。线性回归估计该点位于红色线上。
线性回归是一种非常简单和基本的技术。缺点是它需要数值特征和属性,因此在许多情况下不适用。然而,可以使用虚拟变量或其他技术将分类特征转换为数值格式。
另一个缺点是模型对特征和属性之间关系的假设很强。估计输出的函数是线性的,所以在某些情况下,它可能与真实关系相差甚远。此外,如果现实中特征之间相互影响,模型无法跟踪这种影响。可以使用使关系线性的转换来解决此问题。也可以定义新的特征来表示非线性交互。
线性回归非常基础,它是某些其他技术的起点。例如,逻辑回归预测一个值在 0 到 1 范围内的属性。
感知器
人工神经网络(ANN)是逻辑类似于生物神经系统的监督学习技术。简单的人工神经网络技术是单层感知器,它是一种分类技术,估计一个二进制属性,其值可以是 0 或 1。感知器的工作方式类似于神经元,即它将所有输入的影响相加,如果总和高于定义的阈值,则输出为 1。该模型基于以下参数:
-
每个特征的权重,定义其影响
-
估计输出为 1 的阈值
从特征开始,模型通过以下步骤估计属性
-
通过线性回归计算输出:将每个特征乘以其权重,并将它们相加
-
如果输出高于阈值,则估计属性为 1,否则为 0
模型如图所示:
在开始时,算法使用定义好的系数集和阈值构建感知器。然后,算法使用训练集迭代地改进系数。在每一步中,算法估计每个对象的属性。然后,算法计算真实属性和估计属性之间的差异,并使用该差异来修改系数。在许多情况下,算法无法达到一个稳定的系数集,这些系数不再被修改,因此我们需要定义何时停止。最后,我们有一个由系数集定义的感知器,我们可以用它来估计新对象的属性。
感知器是神经网络的一个简单例子,它使我们能够轻松理解变量的影响。然而,感知器依赖于线性回归,因此它在同一程度上有限:特征影响是线性的,特征不能相互影响。
集成
每个算法都有一些弱点,导致结果不正确。如果我们能够使用不同的算法解决相同的问题并选择最佳结果会怎样?如果只有少数算法犯了同样的错误,我们可以忽略它们。我们无法确定哪个结果是正确的,哪个是错误的,但还有一个选择。通过在新对象上执行监督学习,我们可以应用不同的算法,并从中选择最常见或平均的结果。这样,如果大多数算法识别出正确的估计,我们将考虑它。集成方法基于这个原则:它们结合不同的分类或回归算法以提高准确性。
集成方法需要不同算法和/或训练数据集产生的结果之间的可变性。一些选项包括:
-
改变算法配置:算法是相同的,其参数在一个范围内变化。
-
改变算法:我们使用不同的技术来预测属性。此外,对于每种技术,我们可以使用不同的配置。
-
使用不同的数据子集:算法是相同的,每次它都从训练数据的不同随机子集中学习。
-
使用不同的数据样本(袋装):算法是相同的,它从自助样本中学习,即从训练数据集中随机选择的一组对象。同一个对象可以被选择多次。
最终结果结合了所有算法的输出。在分类的情况下,我们使用众数,在回归的情况下,我们使用平均值或中位数。
我们可以使用任何监督学习技术的组合来构建集成算法,因此有几种选择。一个例子是随机森林,它通过袋装(在上一个列表中的最后一个要点中解释的技术)结合了决策树学习算法。
集成方法通常比单个算法表现更好。在分类的情况下,集成方法消除了仅影响算法一小部分的偏差。然而,不同算法的逻辑通常是相关的,相同的偏差可能很常见。在这种情况下,集成方法保留了偏差。
集成方法并不总是适用于回归问题,因为偏差会影响最终结果。例如,如果只有一个算法计算出一个非常偏差的结果,平均结果会受到很大影响。在这种情况下,中位数表现更好,因为它更加稳定,并且不受异常值的影响。
无监督学习
本章展示了某些无监督学习技术。当面对商业问题时,这些技术使我们能够识别隐藏的结构和模式,并执行探索性数据分析。此外,无监督学习可以简化问题,使我们能够构建更准确且更简化的解决方案。这些技术也可以用于解决本身的问题。
技术的两个分支是聚类和降维,其中大多数技术不适用于两种上下文。本章展示了某些流行技术。
k-means
k-means 是一种基于质心的聚类技术。给定一组对象,算法识别k个同质簇。k-means 是基于质心的,因为每个簇由其质心表示,代表其平均对象。
算法的目的是识别k个质心。然后,k-means 将每个对象关联到最近的质心,定义k个簇。算法从一个随机的质心集合开始,并迭代地改变它们,以改进聚类。
在我们的例子中,数据是关于国家旗帜的,两个特征是条纹数量和颜色数量。我们选择国家子集的方式是确保没有任何两面旗帜具有相同的属性值。我们的目标是识别两个同质的国家群体。k-means 算法的第一步是确定两个随机质心。让我们在图表中可视化数据和质心:
o代表国家旗帜,x代表质心。在运行 k-means 之前,我们需要定义一个距离,这是一种确定对象之间差异性的方法。例如,在上面的图表中,我们可以使用欧几里得距离,它表示连接两个点的线段的长度。该算法是迭代的,每一步包括以下步骤:
-
对于每个点,确定距离最小的质心。然后,将该点分配到与最近质心相关的簇。
-
以一种方式重新计算每个簇的质心,使其成为其对象的平均值。
最后,我们有两个簇,相关的质心代表平均对象。让我们可视化它们,如图所示:
颜色代表簇,黑色x代表最终的质心。
k-means 是最受欢迎的聚类技术之一,因为它易于理解,并且不需要太多的计算能力。然而,该算法有一些局限性。它包含一个随机成分,因此如果我们对同一组数据运行两次,它可能会识别出不同的聚类。另一个缺点是它无法在特定环境中识别聚类,例如,当聚类具有不同的大小或复杂形状时。k-means 是一个非常简单和基本的算法,它是某些更复杂技术的起点。
层次聚类
层次聚类是聚类技术的一个分支。从一个对象集合开始,目标构建一个聚类层次。在聚合层次聚类中,每个对象最初属于不同的聚类。然后,算法将聚类合并,直到有一个包含所有对象的聚类。在确定了层次之后,我们可以在任何点上定义聚类并停止它们的合并。
在每次聚合步骤中,算法将两个最相似的聚类合并,并且有一些参数定义了相似性。首先,我们需要定义一种方法来衡量两个对象之间的相似程度。根据情况,有多种选择。然后,我们需要定义聚类之间的相似性;这些方法被称为链接。为了衡量相似性,我们首先定义一个距离函数,它是相反的。为了确定聚类 1 和聚类 2 之间的距离,我们测量聚类 1 中每个可能对象与聚类 2 中每个对象之间的距离。测量两个聚类之间距离的选项包括:
-
单链接:这是最小距离
-
完全 链接:这是最大距离
-
平均 链接:这是平均距离
根据链接方式的不同,算法的结果也会不同。
该示例使用与 k-means 相同的数据。国家旗帜由条纹和颜色数量表示,我们希望识别同质群体。我们使用的距离是欧几里得距离(仅仅是两点之间的距离)和链接方式为完全链接。首先,让我们从它们的层次结构中识别聚类,如图所示:
该图表称为树状图,图表底部每个对象属于不同的聚类。然后,向上合并聚类,直到所有对象属于同一个聚类。高度是算法合并聚类时的距离。例如,在高度 3 处,所有距离低于 3 的聚类已经合并。
红线位于高度 6 处,它定义了何时停止合并,其下方的对象被分为 4 个聚类。现在我们可以按照以下方式在图表中可视化聚类:
点的颜色代表簇。算法正确地识别了右侧的组,并且以良好的方式将左侧的组分为三部分。
层次聚类有多种选项,其中一些在某些情境下会产生非常好的结果。与 k-means 不同,该算法是确定性的,因此它总是导致相同的结果。
层次聚类的缺点之一是计算时间(O(n³)),这使得它无法应用于大型数据集。另一个缺点是需要手动选择算法配置和树状图切割。为了确定一个好的解决方案,我们通常需要用不同的配置运行算法,并可视化树状图以定义其切割。
PCA
主成分分析(PCA)是一种将特征进行转换的统计过程。PCA 的原理基于线性相关性和方差的概念。在机器学习环境中,PCA 是一种降维技术。
从描述一组对象的特征开始,目标定义了其他彼此线性不相关的变量。输出是一个新的变量集,这些变量定义为初始特征的线性组合。此外,新变量根据其相关性进行排序。新变量的数量小于或等于初始特征的数量,并且可以选择最相关的特征。然后,我们能够定义一组更小的特征,从而降低问题维度。
算法从具有最高方差的特征组合开始定义,然后在每一步迭代地定义另一个特征组合,以最大化方差,条件是新组合与其他组合不线性相关。
在第四章的例子中,步骤 1 – 数据探索和特征工程,第五章,步骤 2 – 应用机器学习技术,以及第六章,步骤 3 – 验证结果中,我们定义了 37 个属性来描述每个国家国旗。应用 PCA 后,我们可以定义 37 个新的属性,这些属性是变量的线性组合。属性按相关性排序,因此我们可以选择前六个,从而得到一个描述国旗的小表格。这样,我们能够构建一个基于六个相关特征的监督学习模型来估计语言。
在存在大量特征的情况下,PCA 允许我们定义一组更小的相关变量。然而,这项技术并不适用于所有情境。一个缺点是结果取决于特征的缩放方式,因此有必要首先标准化变量。
处理监督学习问题时,我们可以使用 PCA 来降低其维度。然而,PCA 只考虑特征,而忽略了它们与预测属性之间的关系,因此它可能会选择与问题不太相关的特征组合。
摘要
在本章中,我们学习了机器学习技术的主要分支:监督学习和无监督学习。我们了解了如何使用监督学习技术,如 KNN、决策树、线性回归和神经网络来估计数值或分类属性。我们还看到,通过结合不同的监督学习算法的技术,即集成,可以提高性能。我们学习了如何使用 k-means 和层次聚类等聚类技术来识别同质群体。我们还理解了降维技术,如 PCA,对于将定义较小变量集的特征进行转换的重要性。
下一章将展示一个可以使用机器学习技术解决的商业问题的例子。我们还将看到监督学习和无监督学习技术的示例。
第八章:适用于商业的机器学习示例
本章的目的是向您展示机器学习如何帮助解决商业问题。大多数技术已在上一章中探讨过,因此本章的节奏很快。技术涉及无监督学习和监督学习。无监督算法从数据中提取隐藏结构,监督技术预测属性。本章使用两个分支的技术解决商业挑战。
在本章中,你将学习如何:
-
将机器学习方法应用于商业问题
-
对银行的客户群进行细分
-
识别营销活动的目标
-
选择表现更好的技术
问题概述
一家葡萄牙银行机构发起了一项电话营销活动。该机构资源有限,因此需要选择目标客户。从过去活动的数据开始,我们可以使用机器学习技术为公司提供一些支持。数据显示了客户的个人细节以及以前营销活动的信息。机器学习算法的目标是识别更有可能订阅的客户。从数据开始,算法需要理解如何使用新客户的数据来预测每个客户订阅的可能性。
数据概述
数据包括大约超过 2,500 名受营销活动影响的客户,该活动包括一个或多个电话呼叫。我们有一些关于客户的信息,并且我们知道谁已经订阅。
表格的每一行对应一个客户,其中有一列显示输出,如果客户已订阅则显示yes,否则显示no。其他列是描述客户的特征,它们是:
-
个人详情:这包括诸如年龄、工作、婚姻状况、教育、信用违约、平均年度余额、住房和个人贷款等详细信息。
-
与公司的沟通:这包括诸如联系方式、最后联系月份和星期几、最后通话时长和联系次数等详细信息。
-
以前的营销活动:这包括诸如上次营销活动前的天数、过去联系次数和过去结果等详细信息。
这是表格的一个示例。y列显示预测属性,如果客户已订阅则显示yes,否则显示no。
| 年龄 | 工作 | 婚姻状况 | ... | 联系方式 | … | y |
|---|---|---|---|---|---|---|
| 30 | services | married | cellular | no | ||
| 33 | management | single | telephone | yes | ||
| 41 | blue-collar | single | unknown | no | ||
| 35 | self-employed | married | telephone | no |
数据存储在bank.csv文件中,我们可以通过在 R 中构建数据表来加载它们。sep=';'字段指定文件中的字段由分号分隔,如下所示:
library(data.table)
dtBank <- data.table(read.csv('bank.csv', sep=';'))
duration特征显示最终通话的秒数。我们分析的目标是定义哪些客户需要联系,我们在联系客户之前无法知道通话时长。此外,在知道通话时长后,我们已经知道客户是否订阅了,因此使用此属性来预测结果是没有意义的。因此,我们移除了duration特征,如下所示:
# remove the duration
dtBank[, duration := NULL]
下一步是探索数据以了解上下文。
探索输出
在本小节中,我们快速探索并转换数据。
y输出是分类的,可能的输出结果为yes和no,我们的目标是可视化比例。为此,我们可以使用以下步骤构建饼图:
-
使用
table统计订阅和未订阅的客户数量:dtBank[, table(y)] y no yes 4000 521 -
确定订阅和未订阅客户的百分比:
dtBank[, table(y) / .N] y no yes 0.88476 0.11524 -
从比例开始构建一个确定百分比的函数:
DefPercentage <- function(frequency) { percentage = frequency / sum(frequency) percentage = round(percentage * 100) percentage = paste(percentage, '%') return(percentage) } -
确定百分比:
defPercentage(dtBank[, table(y) / .N]) [1] "88 %" "12 %" -
查看 R 函数
barplot的帮助,该函数用于构建条形图:help(barplot) -
定义条形图输入:
tableOutput <- dtBank[, table(y)] colPlot <- rainbow(length(tableOutput)) percOutput <- defPercentage(tableOutput) -
构建条形图:
barplot( height = tableOutput, names.arg = percOutput, col = colPlot, legend.text = names(tableOutput), xlab = 'Subscribing' ylab = 'Number of clients', main = 'Proportion of clients subscribing' )
获得的图表如下:
只有 12%的客户订阅了,因此输出值分布不均。下一步是探索所有数据。
探索和转换特征
与输出类似,我们可以构建一些图表来探索特征。让我们首先使用str查看它们:
str(dtBank)
Classes 'data.table' and 'data.frame': 4521 obs. of 16 variables:
$ age : int 30 33 35 30 59 35 36 39 41 43 ...
$ job : Factor w/ 12 levels "admin.","blue-collar",..: 11 8 5 5 2 5 7 10 3 8 ...
$ marital : Factor w/ 3 levels "divorced","married",..: 2 2 3 2 2 3 2 2 2 2 ...
...
特征属于两种数据类型:
-
分类:这种数据类型以因子格式存储特征
-
数值:这种数据类型以整数格式存储特征
对于分类特征和数值特征的图表是不同的,因此我们需要将特征分为两组。我们可以通过以下步骤定义一个包含分类特征的向量以及一个包含数值特征的向量:
-
使用
lapply定义每一列的类:classFeatures <- lapply(dtBank, class) -
移除包含输出的
y列:classFeatures <- classFeatures[names(classFeatures) != 'y'] -
确定分类特征:
featCategoric <- names(classFeatures)[ classFeatures == 'factor' ] -
确定数值特征:
featNumeric <- names(classFeatures)[ classFeatures == 'integer' ]
与输出类似,我们可以为九个分类特征中的每一个构建饼图。为了避免图表过多,我们可以将三个饼图放在同一个图表中。R 函数是par,它允许定义图表网格:
help(par)
我们需要的输入是:
-
mfcol:这是一个包含列数和行数的向量。对于每个特征,我们构建一个饼图和一个包含其图例的图表。我们将饼图放在底部行,图例放在顶部。然后,我们有两行三列。 -
mar:这是一个定义图表边距的向量:par(mfcol = c(2, 3), mar = c(3, 4, 1, 2))
现在,我们可以使用for循环构建直方图:
for(feature in featCategoric){
在for循环内执行以下步骤:
-
定义饼图输入:
TableFeature <- dtBank[, table(get(feature))] rainbCol <- rainbow(length(tableFeature)) percFeature <- defPercentage(tableFeature) -
定义一个新的图表,其图例由特征名称与其颜色匹配组成。我们将特征名称作为图例标题:
plot.new() legend( 'top', names(tableFeature), col = rainbCol, pch = 16, title = feature ) -
构建将在底部行显示的直方图:
barplot( height = tableFeature, names.arg = percFeature, col = colPlot, xlab = feature, ylab = 'Number of clients' ) }
我们构建了包含三个分类特征的三个图表。让我们看看第一个:
job 属性有不同的级别,其中一些级别拥有大量的客户。然后,我们可以为每个相关的职位定义一个虚拟变量,并忽略其他职位。为了确定最相关的职位,我们计算属于每个级别的百分比。然后,我们设置一个阈值,并忽略所有低于阈值的级别。在这种情况下,阈值是 0.08,即 8%。在定义新的虚拟列之后,我们移除 job:
percJob <- dtBank[, table(job) / .N]
colRelevant <- names(percJob)[percJob > 0.08]
for(nameCol in colRelevant){
newCol <- paste('job', nameCol, sep='_')
dtBank[, eval(newCol) := ifelse(job == nameCol, 1, 0)]
}
dtBank[, job := NULL]
在这里,marital,定义婚姻状况,有三个级别,其中 divorced 和 single 占有较小的,尽管是显著的,部分。我们可以定义两个虚拟变量来定义三个级别:
dtBank[, single := ifelse(marital == 'single', 1, 0)]
dtBank[, divorced := ifelse(marital == 'divorced', 1, 0)]
dtBank[, marital := NULL]
关于 education,超过一半的客户接受了中等教育,因此我们可以假设 unknown 的 4% 是 secondary。然后,我们有三个属性,我们可以定义两个虚拟变量:
dtBank[, edu_primary := ifelse(education == 'primary', 1, 0)]
dtBank[, edu_tertiary := ifelse(education == 'tertiary', 1, 0)]
dtBank[, education := NULL]
得到的图如下:
默认、住房和贷款属性有两个不同的级别,因此可以使用 as.numeric 将它们转换为数值形式。为了在属性为 no 时得到 0,在属性为 yes 时得到 1,我们减去 1,如下所示:
dtBank[, housing := as.numeric(housing) - 1]
dtBank[, default := as.numeric(default) - 1]
dtBank[, loan := as.numeric(loan) - 1]
得到的直方图如下:
在这里,contact 有三个选项,其中一个是 unknown。所有选项都有一个显著的份额,因此我们可以定义两个虚拟变量,如下所示:
dtBank[, cellular := ifelse(contact == 'cellular', 1, 0)]
dtBank[, telephone := ifelse(contact == 'telephone', 1, 0)]
dtBank[, contact := NULL]
我们可以将 month 转换为一个数值变量,其中一月对应于 1,十二月对应于 12。特征值是月份名称的缩写,不带大写字母,例如,jan 对应于 January。为了定义数值特征,我们定义一个向量,其第一个元素是 jan,第二个元素是 feb,依此类推。然后,使用 which,我们可以识别向量中的对应元素。例如,apr 是向量的第四个元素,因此使用 which 我们得到 4。为了构建有序月份名称的向量,我们使用包含缩写月份名称的 month.abb 和 tolower 来取消首字母大写,如下所示:
Months <- tolower(month.abb)
months <- c(
'jan', 'feb', 'mar', 'apr', 'may', 'jun',
'jul', 'aug', 'sep', 'oct', 'nov', 'dec'
)
dtBank[
, month := which(month == months),
by=1:nrow(dtBank)
]
在 poutcome 中,success 和 failure 占有少量客户。然而,它们非常相关,因此我们定义了两个虚拟变量:
dtBank[, past_success := ifelse(poutcome == 'success', 1, 0)]
dtBank[, past_failure := ifelse(poutcome == 'failure', 1, 0)]
dtBank[, poutcome := NULL]
我们将所有分类特征转换为数值格式。下一步是探索数值特征并在必要时进行转换。
有六个数值特征,我们可以为每个特征构建一个图表。图表是一个直方图,显示特征值的分布。为了在同一图表中可视化所有图形,我们可以使用 par 将它们放在一个 3 x 2 的网格中。参数如下:
-
mfrow:与mfcol类似,它定义了一个图形网格。区别只是我们将图形添加到网格的顺序。 -
mar:我们将边距设置为默认值,即c(5, 4, 4, 2) + 0.1,如下所示:par(mfrow=c(3, 2), mar=c(5, 4, 4, 2) + 0.1)
我们可以使用hist构建直方图。输入如下:
-
x:这是包含数据的向量 -
main:这是图表标题 -
xlab:这是 x 轴下的标签
我们可以直接在数据表方括号内使用hist。为了一步构建所有图表,我们使用一个for循环:
for(feature in featNumeric){
dtBank[, hist(x = get(feature), main=feature, xlab = feature)]
}
获得的直方图如下:
在这里,年龄和天数在其可能值上是均匀分布的,因此它们不需要任何处理。其余特征集中在较小的值上,因此我们需要对它们进行转换。我们用来定义转换特征的函数是对数,它允许我们拥有更分散的值。对数适用于具有大于 0 值的特征,因此我们需要从特征中移除负值。
为了避免零值,在计算对数之前将特征加1。
根据数据描述,如果机构之前没有联系过客户,则pdays等于-1。为了识别首次联系的客户,我们可以定义一个新的虚拟变量,如果pdays等于-1,则该变量为1。然后,我们将所有负值替换为0,如下所示:
dtBank[, not_contacted := ifelse(pdays == -1, 1, 0)]
dtBank[pdays == -1, pdays := 0]
balance特征表示过去的余额,我们可以定义一个虚拟变量,如果余额为负,则该变量为1。然后,我们将负余额替换为0:
dtBank[, balance_negative := ifelse(balance < 0, 1, 0)]
dtBank[balance < 0, balance := 0]
现在,我们可以计算所有特征的对数。由于对数的输入必须是正数,而一些特征等于0,我们在计算对数之前将每个特征加1:
dtBank[, pdays := log(pdays + 1)]
dtBank[, balance := log(balance + 1)]
dtBank[, campaign := log(campaign + 1)]
dtBank[, previous := log(previous + 1)]
我们已经将所有特征转换为数值格式。现在,我们可以看一下新的特征表:
str(dtBank)
View(dtBank)
唯一不是数值或整数的列是输出y。我们可以将其转换为数值格式,并将其名称更改为 output:
dtBank[, output := as.numeric(y) – 1]
dtBank[, y := NULL]
我们已加载数据并进行了清理。现在我们准备构建机器学习模型。
聚类客户
为了应对下一场营销活动,我们需要识别更有可能订阅的客户。由于难以逐个评估客户,我们可以确定同质客户群体,并识别最有希望的群体。
从历史数据开始,我们根据客户的个人详细信息对客户进行聚类。然后,给定一个新客户,我们识别最相似的群体并将新客户关联到该群体。我们没有新客户客户行为的信息,因此聚类仅基于个人属性。
有不同的技术执行聚类,在本节中我们使用一个相关的算法,即层次聚类。层次聚类的参数之一是链接,它是计算两组之间距离的方式。主要选项包括:
-
单链接:这是第一组中的一个对象与第二组中的一个对象之间的最小距离
-
完全链接:这是第一组中的一个对象与第二组中的一个对象之间的最大距离
-
平均链接:这是第一组中的一个对象与第二组中的一个对象之间的平均距离
在我们的案例中,我们选择了平均链接,这个选择来自于测试三个选项。
我们定义dtPers只包含个人特征,如下所示:
featPers <- c(
'age', 'default', 'balance', 'balance_negative',
'housing', 'loan',
'job_admin.', 'job_blue-collar', 'job_management',
'job_services', 'job_technician',
'single', 'divorced', 'edu_primary', 'edu_tertiary'
)
dtPers <- dtBank[, featPers, with=F]
现在,我们可以应用层次聚类,步骤如下:
-
定义距离矩阵:
d <- dist(dtPers, method = 'euclidean') -
构建层次聚类模型:
hcOut <- hclust(d, method = 'average') -
可视化树状图。
par方法定义了绘图布局,在这种情况下,它只包含一个图表,而plot包含一个改进外观的参数。labels和hang功能避免了底部图表的杂乱,其他参数指定了图表标题和坐标轴标签,如下所示:par(mfrow = c(1, 1)) plot( hcOut, labels = FALSE, hang = -1, main = 'Dendrogram', xlab = 'Client clusters', ylab = 'Agglomeration distance' )
得到的直方图如下:
我们可以在树状图的高度40处切割树状图来识别三个集群。还有另一种选择,即在较低的水平(约 18)切割树状图,识别七个集群。我们可以探索这两种选择,并使用rect.hclust在树状图上可视化这两个分割,如下所示:
k1 <- 3
k2 <- 7
par(mfrow=c(1, 1))
rect.hclust(hcOut, k = k1)
rect.hclust(hcOut, k = k2)
得到的直方图如下:
为了确定最成功的集群,我们可以使用饼图显示订阅客户的比例,并在饼图的标题中放置集群中的客户数量。让我们看看第一次分割的三个集群的图表。构建饼图的步骤与我们之前执行的步骤类似:
-
定义包含输出属性的数据表:
dtClust <- dtBank[, 'output', with = F] -
在数据表中添加定义集群的两列。每一列对应不同的集群数量:
dtClust[, clusterHc1 := cutree(hclOut, k = k1)] dtClust[, clusterHc2 := cutree(hclOut, k = k2)] -
定义一个包含一行三列的绘图布局。
oma参数定义了外部边距:par(mfrow = c(1, 3), oma = c(0, 0, 10, 0)) -
使用与数据探索类似的命令,构建三个直方图,显示每个集群订阅或不订阅客户的百分比:
for(iCluster in 1:k1){ tableClust <- dtClust[ clusterHc1 == iCluster, table(output) ] sizeCluster <- dtClust[, sum(clusterHc1 == iCluster)] titlePie <- paste(sizeCluster, 'clients') barplot( height = tableClust, names.arg = defPercentage(tableClust), legend.text = c('no', 'yes'), col = c('blue', 'red'), main = titlePie ) } -
添加图表的标题:
mtext( text = 'Hierarchic clustering, n = 3', outer = TRUE, line = 1, cex = 2 )
得到的直方图如下:
第一和第二集群包含大多数客户,并且在这两个集群上的活动并没有特别成功。第三集群较小,其客户订阅的比例显著更高。然后,我们可以开始针对与第三集群类似的新客户的营销活动。
使用相同的 R 命令,我们可以可视化由第二次分割确定的七个簇的相同图表,如下所示:
-
定义具有两行四列的绘图布局:
par(mfrow = c(2, 4), oma = c(0, 0, 10, 0)) -
构建直方图:
for(iCluster in 1:k2){ tableClust <- dtClust[ clusterHc2 == iCluster, table(output) ] sizeCluster <- dtClust[, sum(clusterHc2 == iCluster)] titlePie <- paste(sizeCluster, 'clients') barplot( height = tableClust, names.arg = defPercentage(tableClust), col = c('blue', 'red'), main = titlePie ) } -
添加图表标题:
mtext( text = 'Hierarchic clustering, n = 7', outer = TRUE, line = 1, cex = 2 )
获得的直方图如下:
前三个簇包含大多数客户,营销活动对它们的成效并不特别显著。第四和第五簇的订阅客户百分比显著更高。最后两个簇虽然规模很小,但非常成功。营销活动将开始针对与最后两个簇相似的所有新客户,并将针对第四和第五簇的一部分客户。
总之,使用聚类,我们识别出一些小客户群体,在这些群体上营销活动非常成功。然而,大多数客户属于一个大簇,我们对其了解不足。原因是营销活动在具有特定特征的少数客户上取得了成功。
预测输出
过去的营销活动针对了一部分客户群。在 1000 名客户中,我们如何识别出那些更愿意订阅的 100 名客户?我们可以构建一个从数据中学习并估计哪些客户与之前营销活动中订阅的客户更相似的模型。对于每个客户,模型估计一个分数,如果客户更有可能订阅,则分数更高。有不同机器学习模型确定分数,我们使用两种表现良好的技术,如下所示:
-
逻辑回归:这是线性回归的一种变体,用于预测二元输出
-
随机森林:这是一种基于决策树的集成方法,在存在许多特征的情况下表现良好
最后,我们需要从两种技术中选择一种。有一些交叉验证方法允许我们估计模型精度(见第六章,步骤 3 – 验证结果)。从那时起,我们可以测量两种选项的精度,并选择表现更好的一个。
在选择最合适的机器学习算法后,我们可以使用交叉验证来优化它。然而,为了避免过度复杂化模型构建,我们不执行任何特征选择或参数优化。
这些是构建和评估模型的步骤:
-
加载包含随机森林算法的
randomForest包:library('randomForest') -
定义输出和变量名的公式。公式格式为
output ~ feature1 + feature2 + ...:arrayFeatures <- names(dtBank) arrayFeatures <- arrayFeatures[arrayFeatures != 'output'] formulaAll <- paste('output', '~') formulaAll <- paste(formulaAll, arrayFeatures[1]) for(nameFeature in arrayFeatures[-1]){ formulaAll <- paste(formulaAll, '+', nameFeature) } formulaAll <- formula(formulaAll) -
初始化包含所有测试集的表格:
dtTestBinded <- data.table() -
定义迭代次数:
nIter <- 10 -
开始一个
for循环:for(iIter in 1:nIter) { -
定义训练集和测试集:
indexTrain <- sample( x = c(TRUE, FALSE), size = nrow(dtBank), replace = T, prob = c(0.8, 0.2) ) dtTrain <- dtBank[indexTrain] dtTest <- dtBank[!indexTrain] -
从测试集中选择一个子集,使得我们有相同数量的
output == 0和output == 1。首先,根据输出将dtTest分成两部分(dtTest0和dtTest1),并计算每部分的行数(n0和n1)。然后,由于dtTest0有更多的行,我们随机选择n1行。最后,我们重新定义dtTest,将dtTest0和dtTest1绑定,如下所示:dtTest1 <- dtTest[output == 1] dtTest0 <- dtTest[output == 0] n0 <- nrow(dtTest0) n1 <- nrow(dtTest1) dtTest0 <- dtTest0[sample(x = 1:n0, size = n1)] dtTest <- rbind(dtTest0, dtTest1) -
使用
randomForest构建随机森林模型。公式参数定义了变量与数据之间的关系,数据参数定义了训练数据集。为了避免模型过于复杂,所有其他参数都保留为默认值:modelRf <- randomForest( formula = formulaAll, data = dtTrain ) -
使用
glm构建逻辑回归模型,这是一个用于构建广义线性模型(GLM)的函数。GLMs 是线性回归的推广,允许定义一个将线性预测器与输出连接的链接函数。输入与随机森林相同,增加family = binomial(logit)定义回归为逻辑回归:modelLr <- glm( formula = formulaAll, data = dtTest, family = binomial(logit) ) -
使用
predict函数预测随机森林的输出。该函数的主要参数是object定义模型和newdata定义测试集,如下所示:dtTest[, outputRf := predict( object = modelRf, newdata = dtTest, type='response' )] -
使用
predict函数预测逻辑回归的输出,类似于随机森林。另一个参数是type='response',在逻辑回归的情况下是必要的:dtTest[, outputLr := predict( object = modelLr, newdata = dtTest, type='response' )] -
将新的测试集添加到
dtTestBinded:dtTestBinded <- rbind(dtTestBinded, dtTest) -
结束
for循环:}
我们构建了包含output列的dtTestBinded,该列定义了哪些客户订阅以及模型估计的得分。通过比较得分与实际输出,我们可以验证模型性能:
为了探索dtTestBinded,我们可以构建一个图表,显示非订阅客户得分的分布情况。然后,我们将订阅客户的分布添加到图表中,并进行比较。这样,我们可以看到两组得分之间的差异。由于我们使用相同的图表进行随机森林和逻辑回归,我们定义了一个按照给定步骤构建图表的函数:
-
定义函数及其输入,包括数据表和得分列的名称:
plotDistributions <- function(dtTestBinded, colPred) { -
计算未订阅客户的分布密度。当
output == 0时,我们提取未订阅的客户,并使用density定义一个density对象。调整参数定义了从数据开始构建曲线的平滑带宽,带宽可以理解为细节级别:densityLr0 <- dtTestBinded[ output == 0, density(get(colPred), adjust = 0.5) ] -
计算已订阅客户的分布密度:
densityLr1 <- dtTestBinded[ output == 1, density(get(colPred), adjust = 0.5) ] -
使用
rgb定义图表中的颜色。颜色是透明的红色和透明的蓝色:col0 <- rgb(1, 0, 0, 0.3) col1 <- rgb(0, 0, 1, 0.3) -
使用
polygon函数构建显示未订阅客户得分分布的密度图。在这里,polygon函数用于向图表添加面积:plot(densityLr0, xlim = c(0, 1), main = 'density') polygon(densityLr0, col = col0, border = 'black') -
将已订阅的客户添加到图表中:
polygon(densityLr1, col = col1, border = 'black') -
添加图例:
legend( 'top', c('0', '1'), pch = 16, col = c(col0, col1) ) -
结束函数:
return() }
现在,我们可以使用plotDistributions在随机森林输出上:
par(mfrow = c(1, 1))
plotDistributions(dtTestBinded, 'outputRf')
获得的直方图如下:
x 轴代表得分,y 轴代表与订阅了相似得分的客户数量成比例的密度。由于我们并没有每个可能得分的客户,假设细节级别为 0.01,密度曲线在意义上是平滑的,即每个得分的密度是相似得分数据的平均值。
红色和蓝色区域分别代表未订阅和订阅客户。很容易注意到,紫色区域来自两条曲线的叠加。对于每个得分,我们可以识别哪个密度更高。如果最高曲线是红色,客户更有可能订阅,反之亦然。
对于随机森林,大多数未订阅客户的得分在0到0.2之间,密度峰值在0.05左右。订阅客户的得分分布更广,尽管更高,但峰值在0.1左右。两个分布重叠很多,因此很难从得分开始识别哪些客户会订阅。然而,如果营销活动针对得分高于 0.3 的所有客户,他们很可能属于蓝色簇。总之,使用随机森林,我们能够识别出一小部分很可能订阅的客户。
为了进行比较,我们可以构建关于逻辑回归输出的相同图表,如下所示:
plotDistributions(dtTestBinded, 'outputLr')
获得的直方图如下:
对于逻辑回归,两个分布略有重叠,但它们明显覆盖了两个不同的区域,并且它们的峰值相距很远。得分高于 0.8 的客户很可能订阅,因此我们可以选择一小部分客户。如果我们选择得分高于 0.5 或 0.6 的客户,我们也能够识别出一大批可能订阅的客户。
总结来说,逻辑回归似乎表现更好。然而,分布图仅用于探索性能,并不能提供明确的评估。下一步是定义如何使用指标来评估模型。
我们将使用的验证指标是 AUC,它依赖于另一个图表,即接收者操作特征(ROC)。在构建分类模型后,我们定义一个阈值,并假设得分高于阈值的客户将订阅。ROC 显示了模型精度随阈值的变化。曲线维度为:
-
真正率:此指标显示在订阅客户中,有多少百分比的客户得分高于阈值。此指标应尽可能高。
-
假正率:此指标显示在非订阅客户中,有多少百分比的客户得分高于阈值。此指标应尽可能低。
曲线下面积(AUC)是 ROC 曲线下的面积。给定一个订阅了服务的随机客户和另一个未订阅的随机客户,AUC 表示订阅客户的得分高于其他客户的概率。
我们可以定义一个函数来构建图表并计算 AUC 指数:
-
加载包含用于交叉验证模型的函数的
ROCR包:library('ROCR') -
定义函数及其输入,包括数据表和得分列的名称:
plotPerformance <- function(dtTestBinded, colPred) { -
定义一个预测对象,它是构建 ROC 图表的起点。该函数是
prediction,由ROCR包提供:pred <- dtTestBinded[, prediction(get(colPred), output)] -
构建 ROC 图表。由
ROCR包提供的函数是performance,它允许以不同的方式评估预测。在这种情况下,我们想要构建一个包含true和false正率的图表,因此输入是真正率(tpr)和假正率(fpr):perfRates <- performance(pred, 'tpr', 'fpr') plot(perfRates) -
使用
performance计算 AUC 指数。输入是auc,它定义了我们在计算 AUC 指数:perfAuc <- performance(pred, 'auc') auc <- perfAuc@y.values[[1]] -
将 AUC 指数作为函数输出:
return(auc) }
使用plotPerformance,我们可以构建关于随机森林的图表,并计算存储在aucRf中的auc指数:
aucRf <- plotPerformance(dtTestBinded, 'outputRf')
获得的直方图如下:
如预期,图表显示了 tpr 和 fpr。当阈值是1时,没有客户的比率高于它,因此没有正例(预测为订阅的客户)。在这种情况下,我们处于右上角,两个指数都等于 100%。随着阈值的降低,我们有更多的正客户,因此 tpr 和 fpr 降低。最后,当阈值是0时,tpr 和 fpr 都等于0,我们处于左下角。在一个理想的情况下,tpr 等于1,fpr 等于0(左上角)。然后,曲线越接近左上角,越好。
与随机森林类似,我们构建图表并计算逻辑回归的 AUC 指数:
aucLr <- plotPerformance(dtTestBinded, 'outputLr')
获得的直方图如下:
逻辑回归的图表与随机森林的图表相似。观察细节,我们可以注意到左下角的曲线更陡峭,右上角的曲线则不那么陡峭,因此定义 AUC 的曲线下面积更大。
交叉验证包含一个随机成分,因此 AUC 指数可能会有所变化。设置nIter = 100,我上次执行脚本时,随机森林的 AUC 大约为 73%,逻辑回归的 AUC 大约为 79%。我们可以得出结论,逻辑回归表现更好,因此我们应该使用它来构建模型。
在本节中,我们学习了如何构建一个为顾客提供评分的模型。此算法允许公司识别出更有可能订阅的客户,并且还可以估计其准确性。本章的延续将是选择特征子集和优化参数,以实现更好的性能。
摘要
在本章中,你学习了如何探索和转换与商业问题相关的数据。你使用聚类技术来细分银行的客户群,并使用监督学习技术来识别对客户进行评分的排名。在构建机器学习模型后,你能够通过可视化 ROC 曲线和计算 AUC 指数来交叉验证它。这样,你就有能力选择最合适的技巧。
本书展示了机器学习模型如何解决商业问题。这本书不仅仅是一个教程,它是一条道路,展示了机器学习的重要性,如何开发解决方案,以及如何使用这些技术来解决商业问题。我希望这本书不仅传达了机器学习概念,还传达了对一个既有价值又迷人的领域的热情。我想感谢你跟随这条道路。我希望这只是美好旅程的开始。
如果你有任何疑问,请随时联系我。