forked from SleepyBag/Statistical-Learning-Methods
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGBDT.py
109 lines (98 loc) · 4.2 KB
/
GBDT.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
from collections import defaultdict
import numpy as np
import sys
import os
from pathlib import Path
from rich.console import Console
from rich.table import Table
from functools import partial
sys.path.append(str(Path(os.path.abspath(__file__)).parent.parent))
from utils import line_search
sys.path.append(str(Path(os.path.abspath(__file__)).parent.parent / '5.DecisionTree'))
from RegressionCART import RegressionCART
class GBDT:
def __init__(self,
loss_function=lambda label, pred: ((label - pred) ** 2).sum(),
gradient_function=lambda label, pred: 2 * (pred - label),
steps=10,
max_depth=3,
verbose=True):
"""
`loss_function` takes two arguments, label and pred and return a scalar, the loss
`gradient_function` is gradient from loss function to the prediction
It takes two arguments, i.e., label and pred and return the gradient
the loss function should be convex
The default loss function is l2 loss, which makes GBDT an ordinary boosting tree
"""
self.steps = steps
self.verbose = verbose
self.gradient_function = gradient_function
self.loss_function = loss_function
self.max_depth = max_depth
def _loss_of_const(self, Y, c):
"""
Return the loss when the model take a constant c as the prediction
`Y` is a vector of labels
`c` is a constant scalar
"""
c = (np.ones_like(Y) * c).astype(float)
return self.loss_function(Y, c)
def fit(self, X, Y):
n = len(X)
self.carts = []
# the basic value of prediction, so that there can be 'residual'
self.basic_pred = line_search(partial(self._loss_of_const, Y), min(Y), max(Y))
cur_pred = np.zeros_like(Y) + self.basic_pred
residual = -self.gradient_function(Y, cur_pred)
for i in range(self.steps):
if self.verbose:
print(f'step {i}')
print(f'Current pred is {cur_pred}')
print(f'Current residual is {residual}')
cart = RegressionCART(verbose=False, max_depth=self.max_depth)
cart.fit(X, residual)
self.carts.append(cart)
# regression trees use l2 loss as loss function,
# the return value leaf nodes should be recorrect
leaf2label=defaultdict(list)
for i, x in enumerate(X):
leaf = cart._query_leaf(cart.root, x)
leaf2label[leaf].append(i)
for leaf in leaf2label:
data_ind = np.stack(leaf2label[leaf])
leafY = Y[data_ind]
leaf_cur_pred = cur_pred[data_ind]
leaf.label = line_search(lambda c: self.loss_function(leafY, leaf_cur_pred + c), -1e9, 1e9)
# update the incremental prediction
inc_pred = cart.predict(X)
cur_pred += inc_pred
residual = -self.gradient_function(Y, cur_pred)
def predict(self, X):
pred = np.zeros(len(X)) + self.basic_pred
for cart in self.carts:
pred += cart.predict(X)
return pred
if __name__ == "__main__":
def demonstrate(X, Y, max_depth, desc):
print(desc)
console = Console(markup=False)
gbdt = GBDT(verbose=True, max_depth=max_depth)
gbdt.fit(X, Y)
# show in table
pred = gbdt.predict(X)
table = Table('x', 'y', 'pred')
for x, y, y_hat in zip(X, Y, pred):
table.add_row(*map(str, [x, y, y_hat]))
console.print(table)
# -------------------------- Example 1 ----------------------------------------
X = np.arange(10).reshape(-1, 1)
Y = np.array([1, 1, 1, -1, -1, -1, 1, 1, 1, -1])
demonstrate(X, Y, 3, "Example 1")
# -------------------------- Example 2 ----------------------------------------
X = np.arange(10).reshape(-1, 1)
Y = np.array([1, 1, 1, -1, -1, -1, 1, 1, 1, -1])
demonstrate(X, Y, 1, "Example 2: CART cannot be all stumps")
# -------------------------- Example 3 ----------------------------------------
X = np.arange(10).reshape(-1, 1)
Y = np.array([1, 1, 1, -1, -1, -1, 1, 1, 1, -1])
demonstrate(X, Y, 2, "Example 3")