提到GBDT分类相信大家应该都不会觉得陌生,本文就GBDT分类的基本原理进行讲解,并手把手、肩并肩地带您实现这一算法。
完整实现代码请参考本人的github:
https://github.com/tushushu/imylu/blob/master/imylu/ensemble/gbdt_base.py
https://github.com/tushushu/imylu/blob/master/imylu/ensemble/gbdt_classifier.py
https://github.com/tushushu/imylu/blob/master/examples/gbdt_classifier_example.py
一. 原理篇
我们用人话而不是大段的数学公式来讲讲GBDT分类是怎么一回事。
1.1 温故知新
GBDT分类只是在GBDT回归上做了一点点改造,而GBDT分类又是建立在回归树的基础上的。 之前写过一篇GBDT回归的文章,链接如下:
之前写过一篇回归树的文章,链接如下:
1.2 Sigmoid函数
如果对逻辑回归或者神经网络有所了解的话,那么对Sigmoid函数应该不会感到陌生,它的函数表达式是:
不难得出:
所以,Sigmoid函数的值域是(0, 1),导数为y * (1 - y)
1.3 改造GBDT回归
根据《GBDT回归》可知,假设要做m轮预测,预测函数为Fm,初始常量或每一轮的回归树为fm,输入变量为X,有:
由于是回归问题,函数F的值域在(-∞, +∞),而二分类问题要求预测的函数值在(0, 1),所以我们可以用Sigmoid函数将最终的预测值的值域控制在(0, 1)之间,其函数表达式如下:
1.4 预测见面
以预测相亲是否见面来举例,见面用1表示,不见面用0表示。从《回归树》那篇文章中我们可以知道,如果需要通过一个常量来预测同事的年龄,平均值是最佳选择之一。那么预测二分类问题,这个常量该如何计算呢?我们简单证明一下:
结论,如果要用一个常量来预测y,用log(sum(y)/sum(1-y))是一个最佳的选择。
1.5 见面的残差
我们不妨假设三个相亲对象是否见面分别为[1, 0, 1],那么预测是否见面的初始值z = log((1+0+1)/(0+1+0)) = 0.693,所以我们用0.693这个常量来预测同事的年龄,即Sigmoid([0.693, 0.693, 0.693]) = [0.667, 0.667, 0.667]。每个相亲对象是否见面的残差 = 是否见面 - 预测值 = [1, 0, 1] - [0.667, 0.667, 0.667],所以残差为[0.333, -0.667, 0.333]
1.6 预测见面的残差
为了让模型更加准确,其中一个思路是让残差变小。如何减少残差呢?我们不妨对残差建立一颗回归树,然后预测出准确的残差。假设这棵树预测的残差是[1, -0.5, 1],将上一轮的预测值和这一轮的预测值求和,之后再求Sigmoid值,每个相亲对象是否见面 = Sigmoid([0.693, 0.693, 0.693] + [1, -0.5, 1]) = [0.845, 0.548, 0.845],显然与真实值[1, 0, 1]更加接近了, 每个相亲对象是否见面的残差此时变为[0.155, -0.548, 0.155],预测的准确性得到了提升。
1.7 GBDT
重新整理一下思路,假设我们的预测一共迭代3轮 是否见面:[1, 0, 1]
第1轮预测:Sigmoid(0.693, 0.693, 0.693) = [0.667, 0.667, 0.667]
第1轮残差:[0.333, -0.667, 0.333]
第2轮预测:Sigmoid(0.693, 0.693, 0.693 + [1, -0.5, 1]) (第1颗回归树)) = Sigmoid([1.693, 0.193, 1.693]) = [0.845, 0.548, 0.845]
第2轮残差:[0.155, -0.548, 0.155]
第3轮预测:Sigmoid(0.693, 0.693, 0.693 + 1, -0.5, 1 + 2, -1, 2) = Sigmoid([3.693, -0.807, 3.693]) = [0.976, 0.309, 0.976]
第3轮残差:[0.024, -0.309, 0.024]
看上去残差越来越小,而这种预测方式就是GBDT算法。
1.8 公式推导
看到这里,相信您对GBDT已经有了直观的认识。这么做有什么科学依据么,为什么残差可以越来越小呢?前方小段数学公式低能预警。
因此,我们需要通过用第m-1轮的预测值和残差来得到函数fm,进而优化函数fm。而回归树的原理就是通过最佳划分区域的均值来进行预测,与GBDT回归不同,要把这个均值改为1.7式11。所以fm可以选用回归树作为基础模型,将初始值,m-1颗回归树的预测值相加再求Sigmoid值便可以预测y。
二. 实现篇
本人用全宇宙最简单的编程语言——Python实现了GBDT分类算法,没有依赖任何第三方库,便于学习和使用。简单说明一下实现过程,更详细的注释请参考本人github上的代码。
2.1 导入回归树类
回归树是我之前已经写好的一个类,在之前的文章详细介绍过,代码请参考:
https://github.com/tushushu/imylu/blob/master/imylu/tree/regression_tree.py
from ..tree.regression_tree import RegressionTree
2.2 创建GradientBoostingBase类
初始化,存储回归树、学习率、初始预测值和变换函数。
class GradientBoostingBase(object):
def __init__(self):
self.trees = None
self.lr = None
self.init_val = None
self.fn = lambda x: sigmoid(x)
2.3 计算初始预测值
初始预测值,见1.7式10。
def _get_init_val(self, y):
n = len(y)
y_sum = sum(y)
return log((y_sum) / (n - y_sum))
2.4 匹配叶结点
计算训练样本属于回归树的哪个叶子结点。
def _match_node(self, row, tree):
nd = tree.root
while nd.left and nd.right:
if row[nd.feature] < nd.split:
nd = nd.left
else:
nd = nd.right
return nd
2.5 获取叶节点
获取一颗回归树的所有叶子结点。
def _get_leaves(self, tree):
nodes = []
que = [tree.root]
while que:
node = que.pop(0)
if node.left is None or node.right is None:
nodes.append(node)
continue
left_node = node.left
right_node = node.right
que.append(left_node)
que.append(right_node)
return nodes
2.6 划分区域
将回归树的叶子结点,其对应的所有训练样本存入字典。
def _divide_regions(self, tree, nodes, X):
regions = {node: [] for node in nodes}
for i, row in enumerate(X):
node = self._match_node(row, tree)
regions[node].append(i)
return regions
2.7 计算预测值
见1.7式11。
def _get_score(self, idxs, y_hat, residuals):
numerator = denominator = 0
for idx in idxs:
numerator += residuals[idx]
denominator += y_hat[idx] * (1 - y_hat[idx])
return numerator / denominator
2.8 更新预测值
更新回归树各个叶节点的预测值。
def _update_score(self, tree, X, y_hat, residuals):
nodes = self._get_leaves(tree)
regions = self._divide_regions(tree, nodes, X)
for node, idxs in regions.items():
node.score = self._get_score(idxs, y_hat, residuals)
tree._get_rules()
2.9 计算残差
def _get_residuals(self, y, y_hat):
return [yi - self.fn(y_hat_i) for yi, y_hat_i in zip(y, y_hat)]
2.10 训练模型
训练模型的时候需要注意以下几点:
控制树的最大深度max_depth;
控制分裂时最少的样本量min_samples_split;
训练每一棵回归树的时候要乘以一个学习率lr,防止模型过拟合;
对样本进行抽样的时候要采用有放回的抽样方式。
def fit(self, X, y, n_estimators, lr, max_depth, min_samples_split, subsample=None):
self.init_val = self._get_init_val(y)
n = len(y)
y_hat = [self.init_val] * n
residuals = self._get_residuals(y, y_hat)
self.trees = []
self.lr = lr
for _ in range(n_estimators):
idx = range(n)
if subsample is not None:
k = int(subsample * n)
idx = choices(population=idx, k=k)
X_sub = [X[i] for i in idx]
residuals_sub = [residuals[i] for i in idx]
y_hat_sub = [y_hat[i] for i in idx]
tree = RegressionTree()
tree.fit(X_sub, residuals_sub, max_depth, min_samples_split)
self._update_score(tree, X_sub, y_hat_sub, residuals_sub)
y_hat = [y_hat_i + lr * res_hat_i for y_hat_i,
res_hat_i in zip(y_hat, tree.predict(X))]
residuals = self._get_residuals(y, y_hat)
self.trees.append(tree)
2.11 预测一个样本
def _predict(self, Xi):
return self.fn(self.init_val + sum(self.lr * tree._predict(Xi) for tree in self.trees))
2.12 预测多个样本
def predict(self, X):
return [int(self._predict(Xi) >= threshold) for Xi in X]
三. 效果评估
3.1 main函数
使用著名的乳腺癌数据集,按照7:3的比例拆分为训练集和测试集,训练模型,并统计准确度。
@run_time
def main():
print("Tesing the accuracy of GBDT classifier...")
X, y = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=20)
clf = GradientBoostingClassifier()
clf.fit(X_train, y_train, n_estimators=2,
lr=0.8, max_depth=3, min_samples_split=2)
get_acc(clf, X_test, y_test)
3.2 效果展示
最终准确度93.082%,运行时间14.9秒,效果还算不错~
3.3 工具函数
本人自定义了一些工具函数,可以在github上查看
https://github.com/tushushu/imylu/blob/master/imylu/utils.py
run_time - 测试函数运行时间
load_breast_cancer - 加载乳腺癌数据
train_test_split - 拆分训练集、测试集
get_acc - 计算准确度
总结
GBDT分类的原理:GBDT回归加Sigmoid
GBDT分类的实现:一言难尽
本文作者
♚李小文:先后从事过数据分析、数据挖掘工作,主要开发语言是Python,现任一家小型互联网公司的算法工程师。Github: https://github.com/tushushu
投稿邮箱:pythonpost@163.com
欢迎点击申请Python中文社区新专栏作者计划
Python中文社区作为一个去中心化的全球技术社区,以成为全球20万Python中文开发者的精神部落为愿景,目前覆盖各大主流媒体和协作平台,与阿里、腾讯、百度、微软、亚马逊、开源中国、CSDN等业界知名公司和技术社区建立了广泛的联系,拥有来自十多个国家和地区数万名登记会员,会员来自以公安部、工信部、清华大学、北京大学、北京邮电大学、中国人民银行、中科院、中金、华为、BAT、谷歌、微软等为代表的政府机关、科研单位、金融机构以及海内外知名公司,全平台近20万开发者关注。
▼ 点击下方阅读原文,免费成为社区会员