XGBoost(二)解决二分类和多分类问题

XGBoost由多棵决策树(CART)组成,每棵决策树预测真实值与之前所有决策树预测值之和的残差(残差=真实值-预测值),将所有决策树的预测值累加起来即为最终结果。

一、二分类问题

XGBoost树模型由多棵回归树组成,并将多棵决策树的预测值累计相加作为最终结果。回归树产生的是连续的回归值,如何用它解决二分类问题呢?通过前面的学习知道,逻辑回归是在线性回归的基础上通过sigmoid函数将预测值映射到0~1的区间来代表二分类结果的概率。和逻辑回归一样,XGBoost也是采用sigmoid函数来解决二分类问题,即先通过回归树对样本进行预测,得到每棵树的预测结果,然后将其进行累加求和,最后通过sigmoid函数将其映射到0~1的区间代表二分类结果的概率。另外,对于二分类问题,XGBoost的目标函数采用的是类似逻辑回归的logloss,而非最小二乘。

XGBoost中关于二分类的常用参数有如下几个:

  • Objective: 该参数用来指定目标函数,XGBoost可以根据该参数判断进行何种学习任务,binary:logisticbinary:logitraw都表示学习任务类型为二分类。binary:logistic输出为概率,binary:logitraw输出为逻辑转换前的输出分数。
  • eval_metric: 该参数用来指定模型的评估函数,和二分类相关的评估函数有:error、logloss和auc。error也称错误率,即预测错误的样本数占总样本数的比例,准确来说是预测错误样本的权重和占总样本权重和的比例,也可通过error@k的形式手工指定二分类的阈值。logloss通过惩罚分类来量化模型的准确性,最大限度减少logloss,等同于最大化模型的准确率。另外,AUC也是二分类中最常用的评估指标之一,计算方法可另外查询。

下面仍然以该案例数据集进行说明。蘑菇数据集是一个非常著名的二分类数据集。该数据集一共包含23个特征,包括大小、表面、颜色等,每一种蘑菇都会被定义为可食用的或者有毒的,需要通过样本数据分析这些特征与蘑菇毒性的关系。以下是各个特征的详细说明:

  • 帽形(cap-shape):钟形=b,圆锥形=c,凸形=x,平面=f,把手形=k,凹陷=S
  • 帽面(cap-surface):纤维状=f,凹槽状=g,鳞片状=y,光滑=s
  • 帽颜色(cap-color):棕色=n,浅黄色=b,肉桂色=c,灰色=g,绿色=r,粉红色=p,紫色=u,红色=e,白色=w,黄色=y
  • 创伤(bruises):创伤=t,no=f
  • 气味(odor):杏仁=a,茴香=l,石灰=c,腥味=y,臭味=f,霉味=m,无=n,刺鼻=p,辣=s
  • 菌褶附属物(gill-attachment:):附着=a,下降=d,自由=f,缺口=n
  • 菌褶间距(gill-spacing):紧密=c,拥挤=w,远隔=d
  • 菌褶大小(gill-size):宽=b,窄=n。
  • 菌褶颜色(gill-color):黑色=k,棕色=n,浅黄色=b,巧克力色=h,灰色=g,绿色=r,橙色=o,粉红色=p,紫色=u,红色=e,白色=w,黄色=y
  • 茎形(stalk-shape):扩大=e,锥形=t
  • 茎根(stalk-root):球根=b,棒状=c,杯状=u,均等的=e,根状菌索=z,扎根=r,缺省=?
  • 环上茎面(stalk-surface-above-ring):纤维状=f,鳞片状=y,丝状=k,光滑=s
  • 环下茎面(stalk-surface-below-ring):纤维状=f,鳞片状=y,丝状=k,光滑=s
  • 环上茎颜色(stalk-color-above-ring):棕色=n,浅黄色=b,黄棕色=c,灰色=g,橙色=o,粉红色=p,红色=e,白色=w,黄色=y
  • 环下茎颜色(stalk-color-below-ring):棕色=n,浅黄色=b,黄棕色=c,灰色=g,橙色=o,粉红色=p,红色=e,白色=w,黄色=y
  • 菌幕类型(veil-type):部分=p,普遍=u
  • 菌幕颜色(veil-color):棕色=n,橙色=o,白色=w,黄色=y
  • 环数量(ring-number):没有=n,一个=o,两个=t
  • 环类型(ring-type):蛛网状=c,消散状=e,喇叭形=f,大规模的=l,无=n,悬垂的=p,覆盖=s,环带=z
  • 孢子显现颜色(spore-print-color):黑色=k,棕色=n,蓝色=b,巧克力色=h,绿色=r,橙色=o,紫色=u,白色=w,黄色=y
  • 种群(population):丰富=a,聚集=c,众多=n,分散=s,个别=v,单独=y
  • 栖息地(habitat):草地=g,树叶=l,草甸=m,路上=p,城市=u,荒地=w,树林=d
  • class:label字段,有可食用(edible)和有毒性(poisonous)两个取值

该数据集总共有8124个样本,其中类别为可食用的样本有4208个,类别为有毒性的样本有3916个。对于该二分类问题,XGBoost工程文件中提供了示例代码。示例以命令行的方式调用XGBoost,完成模型训练、预测等过程。示例位于demo/CLI/binary_classification文件夹下,其中包括下面几个文件:

  • agaricus-lepiota.data——蘑菇数据文件
  • agaricus-lepiota.fmap——字段名称映射文件
  • agaricus-lepiota.names——蘑菇数据集描述文件
  • mapfeat.py——数据集特征值预处理
  • mknfold.py——划分数据集
  • mushroom.conf——模型配置文件
  • runexp.sh——运行脚本
1
data_dir = "xgboost_source_code/demo/CLI/binary_classification/"

读者可自行尝试执行runexp.sh脚本,学习命令行形式的调用过程。本节重点介绍如何通过Python调用XGBoost进行模型训练和预测,并对处理流程中的各个阶段进行详细解析。

首先需要对特征进行预处理。因为原始文件agaricus-lepiota.data中的数据并不能直接作为XGBoost的输入进行加载,需要进行预处理。这里将其中的字符数据转为数值型,并以LibSVM的格式输出。LibSVM是机器学习中经常采用的一种数据格式,如下:

1
<label> <index1>:<value1><index2>:<value2>...

label为训练数据集的目标值;index为特征索引,是一个以1为起始的整数;value是该特征的取值,如果某一特征的值缺省,则该特征可以空着不填,因此对于一个样本来讲,输出后的数据文件index可能并不连续,上述样本处理后的格式如下:

1
2
1 3:1 10:1 11:1 21:1 30:1 34:1 36:1 40:1 41:1 53:1 58:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 105:1 117:1 124:1
0 3:1 10:1 20:1 21:1 23:1 34:1 36:1 39:1 41:1 53:1 56:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 116:1 120:1

第一个样本中最开始的“1”便是该样本的label,在二分类问题中,一般1代表正样本,0代表负样本。之后的每个特征为一项,冒号前为该特征的索引,如3、10等,冒号后为该特征取值,如3、10两个特征的取值都是1。另外,观察处理后的数据可以发现,特征索引已经远远超过了22,如第一行样本中特征索引最大已经达到了124。

观察该数据集可以发现,其中大部分特征是离散型特征,连续型特征较少。在机器学习算法中,特征之间距离的计算是十分重要的,因此,直接把离散变量的取值转化为数值,并不能很好地代表特征间的距离,如菌幕颜色特征,其总共有棕色、橙色、白色、黄色4种颜色,假如将其映射为1、2、3、4,则棕色和橙色之间的距离是2-1=1,而棕色和白色之间的距离是3-1=2。这显然是不符合实际情况的,因为任意两个颜色之间的距离应该是相等的。因此,需要对特征进行独热编码(one-hot encoding)。

简单来讲,独热编码就是离散特征有多少取值,就用多少维来表示该特征。仍然以菌幕颜色特征为例,经过独热编码后,其将会转为4个特征,分别是菌幕颜色是否为棕色、菌幕颜色是否为橙色、菌幕颜色是否为白色和菌幕颜色是否为黄色,并且这4个特征取值只有0和1。经过独热编码之后,每两个颜色之间的距离都是一样的,比之前的处理更合理。离散特征经过独热编码之后,数据集的总特征数会变多,这就是上述示例中出现较大特征索引的原因。下面来看一下特征处理的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def loadmap(fname):
fmap = {}
nmap = {}
for l in open(fname):
arr = l.split()
if arr[0].find('.') != -1:
idx = int(arr[0].strip('.'))
assert idx not in fmap
fmap[idx] = {}
ftype = arr[1].strip(":")
content = arr[2]
else:
content = arr[0]
for it in content.split(','):
if it.strip() == '':
continue
k, v = it.split('=')
fmap[idx][v] = len(nmap) + 1
nmap[len(nmap)] = ftype + '=' + k
return fmap, nmap

def write_nmap(fo, nmap):
for i in range(len(nmap)):
fo.write('%d\t%s\ti\n'%(i, nmap[i]))
1
2
3
4
fmap, nmap = loadmap(data_dir+'agaricus-lepiota.fmap')
fo = open('output/data/featmap.txt', 'w')
write_nmap(fo, nmap)
fo.close()
1
2
3
4
5
6
7
8
9
10
11
12
fo = open('output/data/agaricus.txt', 'w')
for l in open(data_dir+'agaricus-lepiota.data'):
arr = l.split(',')
if arr[0] == 'p':
fo.write('1')
else:
assert arr[0] == 'e'
fo.write('0')
for i in range(1, len(arr)):
fo.write(' %d:1' %fmap[i][arr[i].strip()])
fo.write('\n')
fo.close()

首先程序会加载特征描述文件agaricus-lepiota.fmap,为每个特征的每个取值均分配一个唯一的索引标识,并为其重新命名,并将处理后的新特征索引和名称的映射保存为featmap.txt文件(该映射文件会在XGBoost中用到)。然后加载蘑菇数据集,通过新特征索引处理该数据集,生成转化后的新数据文件featmap.txt。特征处理完后即可通过mknfold.py划分数据集。在本示例中,划分数据集是通过代码实现的,当然读者也可以采用第3章介绍的scikit-learn中的train_test_split来划分数据集。下面看一下mknfold.py的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import sys
import random

if len(sys.argv) < 2:
print ('Usage:<filename> <k> [nfold = 5]')
exit(0)

random.seed( 10 )

k = int( sys.argv[2] )
if len(sys.argv) > 3:
nfold = int( sys.argv[3] )
else:
nfold = 5

fi = open( sys.argv[1], 'r' )
ftr = open( sys.argv[1]+'.train', 'w' )
fte = open( sys.argv[1]+'.test', 'w' )
for l in fi:
if random.randint( 1 , nfold ) == k:
fte.write( l )
else:
ftr.write( l )

fi.close()
ftr.close()
fte.close()

生成训练集和测试集后,便可通过XGBoost加载数据进行训练,下面通过Python实现XGBoost的调用。先加载训练集和测试集:

1
2
3
import xgboost as xgb
xgb_train = xgb.DMatrix("xgboost_source_code/demo/data/agaricus.txt.train")
xgb_test = xgb.DMatrix("xgboost_source_code/demo/data/agaricus.txt.test")

设定模型训练参数,开始模型训练:

1
2
3
4
5
6
7
8
9
10
11
params = {
"objective" : "binary:logistic",
"booster" : "gbtree",
"eta" : 1.0,
"gamma" : 1.0,
"min_child_weight" : 1,
"max_depth" : 3
}
num_round = 2
watchlist = [(xgb_train, "train"), (xgb_test, "test")]
model = xgb.train(params, xgb_train, num_round, watchlist)
[0]    train-error:0.01443    test-error:0.01614
[1]    train-error:0.00123    test-error:0.00000

params中的objectivebooster参数已经介绍过了,分别用于指定任务的学习目标和booster类型,其他参数说明如下:

  • objective设为binary:logistic,表示任务为二分类问题,最终输出为sigmoid变换后的概率。
  • boostergbtree表示采用XGBoost中的树模型。参数eta表示学习率,类似于梯度下降中法的$\alpha$,每次迭代完更新权重的步长。
  • gamma表示节点分裂时损失函数减小的最小值,此处为1.0,表示损失函数至少下降1.0该节点才会进行分裂。
  • min_child_weight表示叶子节点最小样本权重和,若节点分裂导致叶子节点的样本权重和小于该值,则节点不进行分裂。
  • max_depth表示决策树分裂的最大深度。

另外,该示例中指定了num_round为2,即模型会进行两轮booster训练,最终会生成两棵决策树。通过定义参数watchlist,模型在训练过程中会实时输出训练集和验证集的评估指标。

模型训练完成之后,可通过save_model方法将模型保存成模型文件,以供后续预测使用,如下:

1
model.save_model("output/model/02_agaricus.model")

预测时,先加载保存的模型文件,然后再对数据集进行预测,如下:

1
2
3
4
bst = xgb.Booster()
bst.load_model("output/model/02_agaricus.model")
pred = bst.predict(xgb_test)
print(pred)
[0.10828121 0.85500014 0.10828121 ... 0.95467216 0.04156424 0.95467216]

可以看到,输出结果是一个浮点数组成的数组,其中每个值代表对应样本的预测概率。预测完成后,输出文本格式的模型,这里仍然采用两种方式,如下:

1
2
3
4
# 未作特征名转换
dump_model_raw = bst.dump_model("output/data/dump.raw.txt")
# 完成特征名转换
dump_model_nice = bst.dump_model("output/data/dump.nice.txt", "output/data/featmap.txt")

下面主要以完成特征名称转换后的模型文件为例进行介绍。先来看一下索引和特征名称映射文件featmap.txt,格式如下:

1
<featureid> <featurename> <q or i or int>\n

其中:

  • featureid为特征索引
  • featurename为特征名称
  • q or i or int为特征的数据类型,其中q代表特征是一个连续值,如距离、价格等;i代表特征是一个二值特征(即特征只有两个取值),一般为0或1;int代表特征是整型值。可以看到,featmap.txt中的很多特征都是二值特征。这个也不难理解,因为该数据集中大部分是离散型的类别特征,因此经过独热编码处理后,新生成的特征基本都是二值特征。

了解了特征映射文件后,下面来看一下文本格式的XGBoost树模型文件,以下截取了dump.nice.txt的前几行:

1
2
3
4
5
6
7
8
9
10
11
booster[0]:
0:[odor=pungent] yes=2,no=1
1:[stalk-root=cup] yes=4,no=3
3:[stalk-root=missing] yes=8,no=7
7:leaf=1.90174532
8:leaf=-1.95061731
4:[bruises?=no] yes=10,no=9
9:leaf=1.77777779
10:leaf=-1.98104262
2:[spore-print-color=orange] yes=6,no=5
5:[stalk-surface-below-ring=silky] yes=12,no=11

上面的一个booster代表一棵决策树,该模型一共有两棵决策树。在每棵决策树中,每一行代表一个节点,位于行首的数字代表该节点的索引,数字0表示该节点为根节点。若该行节点是非叶子节点,则索引后面是该节点的分裂条件,如第2行:

1
0:[odor=pungent] yes=2,no=1

该节点的索引为0,表示该节点是根节点,其分裂条件是odor=pungent,满足该条件的样本会被划分到节点2,不满足的则被划分到节点1。若该行节点是叶子节点,则索引后面是该叶子节点最终得到的权重。如第5行:

1
7:leaf=1.90174532

leaf表示该节点为叶子节点,最终得到的权重为1.90174532。由此,通过文本格式的模型文件,可以使用户了解样本在模型中是如何被划分的,使模型更具有可解释性,并且在实际的机器学习任务中,也有利于用户更好地分析和优化模型。

二、多分类问题

与处理二分类问题类似,XGBoost在处理多分类问题时也是在树模型的基础上进行转换,不过不再是sigmoid函数,而是softmax函数。相信大家对softmax变换并不陌生,它可以将多分类的预测值映射到0到1之间,代表样本属于该类别的概率。XGBoost中解决多分类问题的主要参数如下:

  • num_class:说明在该分类任务的类别数量
  • objective:该参数中的multi:softmaxmulti:softprob均是指定学习任务为多分类。multi:softmax通过softmax函数解决多分类问题。multi:softprobmulti:softmax一样,主要区别在于其输出的是一个$ndata*nclass$向量,表示样本属于每个分类的预测概率
  • eval_metric:与多分类相关的评估函数有merrormloglossmerror也称多分类错误率,通过判断样本所有分类预测值中预测值最大的分类和样本label是否一致来确定预测是否正确,其计算方式和error相似。mlogloss也是多分类问题中常用的评估指标。有关merrormlogloss会在后面详细介绍。

下面以识别小麦种子的类别作为示例,介绍如何通过XGBoost解决多分类问题。已知小麦种子数据集包含7个特征,分别为面积、周长、紧凑度、籽粒长度、籽粒宽度、不对称系数、籽粒腹沟长度,且均为连续型特征,以及小麦类别字段,共有3个类别,分别用1、2、3表示。加载该数据并进行特征处理,代码如下:

1
2
3
4
5
6
7
import pandas as pd
import numpy as np
import xgboost as xgb

data = pd.read_csv("input/seeds_dataset.txt", header=None, sep='\s+', converters={7: lambda x:int(x)-1})
data.rename(columns={7:'label'}, inplace=True)
data.head()
0 1 2 3 4 5 6 label
0 15.26 14.84 0.8710 5.763 3.312 2.221 5.220 0
1 14.88 14.57 0.8811 5.554 3.333 1.018 4.956 0
2 14.29 14.09 0.9050 5.291 3.337 2.699 4.825 0
3 13.84 13.94 0.8955 5.324 3.379 2.259 4.805 0
4 16.14 14.99 0.9034 5.658 3.562 1.355 5.175 0

为便于后续处理,将最后一个类别字段作为label字段,因为label的取值需在0到num_class-1范围内,因此需对类别字段进行处理(数据集中的3个类别取值分别为1~3),这里直接减1即可。

可以看到,数据集共包含8列,其中前7列为特征列,最后1列为label列,和数据集描述相符。除label列外,剩余特征没有指定列名,所以pandas自动以数字索引作为列名。下面对数据集进行划分(训练集和测试集的划分比例为4:1),并指定label字段生成XGBoost中的DMatrix数据结构,代码如下:

1
2
3
4
5
mask = np.random.rand(len(data)) < 0.8
train = data[mask]
test = data[~mask]
xgb_train = xgb.DMatrix(train.iloc[:,:6], label=train.label)
xgb_test = xgb.DMatrix(test.iloc[:,:6], label=test.label)

设置模型训练参数。设置参数objectivemulti:softmax,表示采用softmax进行多分类,学习率参数eta和最大树深度max_depth在之前的示例中已有所介绍,不再赘述。参数num_class指定类别数量为3。相关代码如下:

1
2
3
4
5
6
7
8
9
params = {
'objective':'multi:softmax',
'eta':0.1,
'max_depth':5,
'num_class':3
}
watchlist = [(xgb_train, "train"), (xgb_test, "test")]
num_round = 10
bst = xgb.train(params, xgb_train, num_round, watchlist)
[0]    train-merror:0.01219    test-merror:0.10870
[1]    train-merror:0.01219    test-merror:0.10870
[2]    train-merror:0.01219    test-merror:0.10870
[3]    train-merror:0.01219    test-merror:0.10870
[4]    train-merror:0.01219    test-merror:0.13043
[5]    train-merror:0.00610    test-merror:0.13043
[6]    train-merror:0.00610    test-merror:0.13043
[7]    train-merror:0.00610    test-merror:0.13043
[8]    train-merror:0.00610    test-merror:0.15217
[9]    train-merror:0.00610    test-merror:0.15217

在未指定评估函数的情况下,XGBoost默认采用merror作为多分类问题的评估指标。下面通过训练好的模型对测试集进行预测,并计算错误率,代码如下:

1
2
3
pred = bst.predict(xgb_test)
error_rate = np.sum(pred != test.label) / test.shape[0]
print(error_rate)
0.15217391304347827

为了方便对比学习,下面采用multi:softprob方法重新训练模型,代码如下:

1
2
params["objective"] = "multi:softprob"
bst = xgb.train(params, xgb_train, num_round, watchlist)
[0]    train-merror:0.01219    test-merror:0.10870
[1]    train-merror:0.01219    test-merror:0.10870
[2]    train-merror:0.01219    test-merror:0.10870
[3]    train-merror:0.01219    test-merror:0.10870
[4]    train-merror:0.01219    test-merror:0.13043
[5]    train-merror:0.00610    test-merror:0.13043
[6]    train-merror:0.00610    test-merror:0.13043
[7]    train-merror:0.00610    test-merror:0.13043
[8]    train-merror:0.00610    test-merror:0.15217
[9]    train-merror:0.00610    test-merror:0.15217

对比两种函数变换方法的训练输出结果可以看出,不论采用multi:softmax还是multi:softprob作为objective训练模型,并不会影响到模型精度。

下面对测试集进行预测并计算错误率,代码如下:

1
2
3
4
pred_prop = bst.predict(xgb_test)
pred_label = np.argmax(pred_prop, axis=1)
error_rate = np.sum(pred_label != test.label) / test.shape[0]
print('测试集错误率(softprob):{}'.format(error_rate))
测试集错误率(softprob):0.15217391304347827

之后的处理则和采用multi:softmax时一样,统计预测错误的样本数,最终计算出分类错误率。采用multi:softprob得到的错误率和multi:softmax也是一样的

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2020-2021 chenk
  • 由 帅气的CK本尊 强力驱动
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信