XGBoost实战(二):解决二分类和多分类问题
由多棵决策树(CART)组成,每棵决策树预测真实值与之前所有决策树预测值之和的残差(残差=真实值-预测值),将所有决策树的预测值累加起来即为最终结果。
一、二分类问题
树模型由多棵回归树组成,并将多棵决策树的预测值累计相加作为最终结果。回归树产生的是连续的回归值,如何用它解决二分类问题呢?通过前面的学习知道,逻辑回归是在线性回归的基础上通过函数将预测值映射到0~1的区间来代表二分类结果的概率。和逻辑回归一样,也是采用函数来解决二分类问题,即先通过回归树对样本进行预测,得到每棵树的预测结果,然后将其进行累加求和,最后通过函数将其映射到0~1的区间代表二分类结果的概率。另外,对于二分类问题,的目标函数采用的是类似逻辑回归的,而非最小二乘。
中关于二分类的常用参数有如下几个:
下面仍然以该案例数据集进行说明。蘑菇数据集是一个非常著名的二分类数据集。该数据集一共包含23个特征,包括大小、表面、颜色等,每一种蘑菇都会被定义为可食用的或者有毒的,需要通过样本数据分析这些特征与蘑菇毒性的关系。以下是各个特征的详细说明:
该数据集总共有8124个样本,其中类别为可食用的样本有4208个,类别为有毒性的样本有3916个。对于该二分类问题,工程文件中提供了示例代码。示例以命令行的方式调用,完成模型训练、预测等过程。示例位于demo/CLI/n文件夹下,其中包括下面几个文件:
data_dir = "xgboost_source_code/demo/CLI/binary_classification/"
读者可自行尝试执行.sh脚本,学习命令行形式的调用过程。本节重点介绍如何通过调用进行模型训练和预测,并对处理流程中的各个阶段进行详细解析。
首先需要对特征进行预处理。因为原始文件-.data中的数据并不能直接作为的输入进行加载,需要进行预处理。这里将其中的字符数据转为数值型,并以的格式输出。是机器学习中经常采用的一种数据格式,如下:
label为训练数据集的目标值;index为特征索引,是一个以1为起始的整数;value是该特征的取值,如果某一特征的值缺省,则该特征可以空着不填,因此对于一个样本来讲,输出后的数据文件index可能并不连续,上述样本处理后的格式如下:
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 )。
简单来讲,独热编码就是离散特征有多少取值,就用多少维来表示该特征。仍然以菌幕颜色特征为例,经过独热编码后,其将会转为4个特征,分别是菌幕颜色是否为棕色、菌幕颜色是否为橙色、菌幕颜色是否为白色和菌幕颜色是否为黄色,并且这4个特征取值只有0和1。经过独热编码之后,每两个颜色之间的距离都是一样的,比之前的处理更合理。离散特征经过独热编码之后,数据集的总特征数会变多,这就是上述示例中出现较大特征索引的原因。下面来看一下特征处理的代码实现:
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]))
fmap, nmap = loadmap(data_dir+'agaricus-lepiota.fmap')
fo = open('output/data/featmap.txt', 'w')
write_nmap(fo, nmap)
fo.close()
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()
首先程序会加载特征描述文件-.fmap,为每个特征的每个取值均分配一个唯一的索引标识,并为其重新命名,并将处理后的新特征索引和名称的映射保存为.txt文件(该映射文件会在中用到)。然后加载蘑菇数据集,通过新特征索引处理该数据集,生成转化后的新数据文件.txt。特征处理完后即可通过.py划分数据集。在本示例中,划分数据集是通过代码实现的,当然读者也可以采用第3章介绍的-learn中的来划分数据集。下面看一下.py的代码:
import sys
import random
if len(sys.argv) < 2:
print ('Usage: [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()
生成训练集和测试集后,便可通过加载数据进行训练,下面通过实现的调用。先加载训练集和测试集:
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")
设定模型训练参数,开始模型训练:
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
中的和参数已经介绍过了,分别用于指定任务的学习目标和类型,其他参数说明如下:
另外,该示例中指定了为2,即模型会进行两轮训练,最终会生成两棵决策树。通过定义参数,模型在训练过程中会实时输出训练集和验证集的评估指标。
模型训练完成之后,可通过方法将模型保存成模型文件,以供后续预测使用,如下:
model.save_model("output/model/02_agaricus.model")
预测时,先加载保存的模型文件,然后再对数据集进行预测,如下:
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]
可以看到,输出结果是一个浮点数组成的数组,其中每个值代表对应样本的预测概率。预测完成后,输出文本格式的模型,这里仍然采用两种方式,如下:
# 未作特征名转换
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")
下面主要以完成特征名称转换后的模型文件为例进行介绍。先来看一下索引和特征名称映射文件.txt,格式如下:
\n
其中:
了解了特征映射文件后,下面来看一下文本格式的树模型文件,以下截取了dump.nice.txt的前几行:
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
上面的一个代表一棵决策树,该模型一共有两棵决策树。在每棵决策树中,每一行代表一个节点,位于行首的数字代表该节点的索引,数字0表示该节点为根节点。若该行节点是非叶子节点,则索引后面是该节点的分裂条件,如第2行:
0:[odor=pungent] yes=2,no=1
该节点的索引为0,表示该节点是根节点,其分裂条件是odor=,满足该条件的样本会被划分到节点2,不满足的则被划分到节点1。若该行节点是叶子节点,则索引后面是该叶子节点最终得到的权重。如第5行:
7:leaf=1.90174532
leaf表示该节点为叶子节点,最终得到的权重为1.。由此,通过文本格式的模型文件,可以使用户了解样本在模型中是如何被划分的,使模型更具有可解释性,并且在实际的机器学习任务中,也有利于用户更好地分析和优化模型。
二、多分类问题
与处理二分类问题类似,在处理多分类问题时也是在树模型的基础上进行转换,不过不再是函数,而是函数。相信大家对变换并不陌生,它可以将多分类的预测值映射到0到1之间,代表样本属于该类别的概率。中解决多分类问题的主要参数如下:
下面以识别小麦种子的类别作为示例,介绍如何通过解决多分类问题。已知小麦种子数据集包含7个特征,分别为面积、周长、紧凑度、籽粒长度、籽粒宽度、不对称系数、籽粒腹沟长度,且均为连续型特征,以及小麦类别字段,共有3个类别,分别用1、2、3表示。加载该数据并进行特征处理,代码如下:
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()
15.26
14.84
0.8710
5.763
3.312
2.221
5.220
14.88
14.57
0.8811
5.554
3.333
1.018
4.956
14.29
14.09
0.9050
5.291
3.337
2.699
4.825
13.84
13.94
0.8955
5.324
3.379
2.259
4.805
16.14
14.99
0.9034
5.658
3.562
1.355
5.175
为便于后续处理,将最后一个类别字段作为label字段,因为label的取值需在0到-1范围内,因此需对类别字段进行处理(数据集中的3个类别取值分别为1~3),这里直接减1即可。
可以看到,数据集共包含8列,其中前7列为特征列,最后1列为label列,和数据集描述相符。除label列外,剩余特征没有指定列名,所以自动以数字索引作为列名。下面对数据集进行划分(训练集和测试集的划分比例为4:1),并指定label字段生成中的数据结构,代码如下:
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)
设置模型训练参数。设置参数为multi:,表示采用进行多分类,学习率参数eta和最大树深度在之前的示例中已有所介绍,不再赘述。参数指定类别数量为3。相关代码如下:
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
在未指定评估函数的情况下,默认采用作为多分类问题的评估指标。下面通过训练好的模型对测试集进行预测,并计算错误率,代码如下:
pred = bst.predict(xgb_test)
error_rate = np.sum(pred != test.label) / test.shape[0]
print(error_rate)
0.15217391304347827
为了方便对比学习,下面采用multi:方法重新训练模型,代码如下:
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:还是multi:作为训练模型,并不会影响到模型精度。
下面对测试集进行预测并计算错误率,代码如下:
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:时一样,统计预测错误的样本数,最终计算出分类错误率。采用multi:得到的错误率和multi:也是一样的
- END -