梯度提升决策树GBDT

梯度提升决策树(Gradient Boosting Decision Tree,GBDT)算法是近年来被提及比较多的一个算法,这主要得益于其算法的性能,以及该算法在各类数据挖掘以及机器学习比赛中的卓越表现,有很多人对GBDT算法进行了开源代码的开发,比较火的是陈天奇的XGBoost和微软的LightGBM。

一、监督学习

1、监督学习的主要任务

监督学习是机器学习算法中重要的一种,对于监督学习,假设有m

m个训练样本:

{(X

(1)

,y

(1)

),(X

(2)

,y

(2)

),⋯,(X

(m)

,y

(m)

)}

{(X(1),y(1)),(X(2),y(2)),⋯,(X(m),y(m))}

其中,X

(i)

={x

(i)

1

,x

(i)

2

,⋯,x

(i)

m

}

X(i)={x1(i),x2(i),⋯,xm(i)}称为第i

i个样本的特征,y

(1)

y(1)称为第i

i个样本的标签,样本标签可以为离散值,如分类问题;也可以为连续值,如回归问题。在监督学习中,利用训练样本训练出模型,该模型能够实现从样本特征X

(i)

X(i)到样本标签y

(1)

y(1)的映射,即:

X

(i)

F

y

(i)

X(i)→Fy(i)

为了能够对映射F(X)

F(X)进行求解,通常对模型设置损失函数L(y,F(X))

L(y,F(X)),并求得在损失函数最小的情况下的映射为最好的映射:

F

=argmin

F(X)

L(y,F(X))

F∗=argminF(X)L(y,F(X))

对于一个具体的问题,如线性回归问题,其映射函数的形式为:

F(X;W)=WX=w

+w

1

x

1

+w

2

x

2

+⋯+w

n

x

n

F(X;W)=WX=w0+w1x1+w2x2+⋯+wnxn

此时对于最优映射函数F(X;W)

F(X;W)的求解,实质是对映射函数中的参数W

W的求解。对于参数的求解方法有很多,如梯度下降法。

2、梯度下降法

梯度下降法(Gradient Descent,GD)算法是求解最优化问题最简单、最直接的方法。梯度下降法是一种迭代的优化算法,对于优化问题:

minf(w)

minf(w)

其基本步骤为:

  • 随机选择一个初始点w
  • 0
  • w0
  • 重复以下过程:
  • 决定下降的方向:d
  • i
  • =−∂
  • ∂w
  • f(w)∣
  • w
  • i
  • di=−∂∂wf(w)∣wi
  • 选择步长ρ
  • ρ
  • 更新:w
  • i+1
  • =w
  • i
  • +ρ⋅d
  • i
  • wi+1=wi+ρ⋅di
  • 直到满足终止条件

梯度下降法的具体过程如下图所示:

梯度提升决策树GBDT

由以上的过程,我们可以看出,对于最终的最优解w

w∗,是由初始值w

w0经过M

M代的迭代之后得到的,在这里,设w

=d

w0=d0,则w

w∗为:

w

=∑

i=0

M

ρ

i

⋅d

i

w∗=∑i=0Mρi⋅di

3、在函数空间的优化

以上是在指定的函数空间中对最优函数进行搜索,那么,能否直接在函数空间(function space)中查找到最优的函数呢?根据上述的梯度下降法的思路,对于模型的损失函数L(y,F(X))

L(y,F(X)),为了能够求解出最优的函数F

(X)

F∗(X),首先,设置初始值为:

F

(X)=f

(X)

F0(X)=f0(X)

以函数F(X)

F(X)作为一个整体,对于每一个样本X

(i)

X(i),都存在对应的函数值F(X

(i)

)

F(X(i))。与梯度下降法的更新过程一致,假设经过M

M代,得到最有的函数F

(X)

F∗(X)为:

F

(X)=∑

i=0

M

f

i

(X)

F∗(X)=∑i=0Mfi(X)

其中,f

i

(X)

fi(X)为:

f

i

(X)=−ρ

i

g

m

(X)

fi(X)=−ρigm(X)

其中,g

m

(X)=[∂L(y,F(X))

∂F(X)

]

F(X)=F

m−1

(X)

gm(X)=[∂L(y,F(X))∂F(X)]F(X)=Fm−1(X)。

由上述的过程可以得到函数F(X)

F(X)的更新过程:

F

m

(X)=∑

i=0

m

f

i

(X)

Fm(X)=∑i=0mfi(X)

与上面类似,函数f(X)

f(X)是由参数a

a决定的,即:

f(X)=−ρ⋅h(X;a)

f(X)=−ρ⋅h(X;a)

二、Boosting

1、集成方法之Boosting

Boosting方法是集成学习中重要的一种方法,在集成学习方法中最主要的两种方法为Bagging和Boosting,在Bagging中,通过对训练样本重新采样的方法得到不同的训练样本集,在这些新的训练样本集上分别训练学习器,最终合并每一个学习器的结果,作为最终的学习结果,Bagging方法的具体过程如下图所示:

梯度提升决策树GBDT

在Bagging方法中,最重要的算法为随机森林Random Forest算法。由以上的图中可以看出,在Bagging方法中,b

b个学习器之间彼此是相互独立的,这样的特点使得Bagging方法更容易并行。与Bagging方法不同,在Boosting算法中,学习器之间是存在先后顺序的,同时,每一个样本是有权重的,初始时,每一个样本的权重是相等的。首先,第1

1个学习器对训练样本进行学习,当学习完成后,增大错误样本的权重,同时减小正确样本的权重,再利用第2

2个学习器对其进行学习,依次进行下去,最终得到b

b个学习器,最终,合并这b

b个学习器的结果,同时,与Bagging中不同的是,每一个学习器的权重也是不一样的。Boosting方法的具体过程如下图所示:

梯度提升决策树GBDT

在Boosting方法中,最重要的方法包括:AdaBoost和GBDT。

2、Gradient Boosting

由上图所示的Boosting方法中,最终的预测结果为b

b个学习器结果的合并:

f(X)=∑

j=1

b

θ

j

φ

j

(X)

f(X)=∑j=1bθjφj(X)

这与上述的在函数空间中的优化类似:

F

m

(X)=∑

i=0

m

−ρ

i

⋅h(X;a

i

)

Fm(X)=∑i=0m−ρi⋅h(X;ai)

根据如上的函数空间中的优化可知,每次对每一个样本的训练的值为:

y

¯

i

=[∂L(y

i

,F(X

(i)

))

∂F(X

(i)

)

]

F(X)=F

m−1

(X)

y¯i=[∂L(yi,F(X(i)))∂F(X(i))]F(X)=Fm−1(X)

上建立模型,由于上述是一个求解梯度的过程,因此也称为基于梯度的Boost方法,其具体过程如下所示:

梯度提升决策树GBDT

三、Gradient Boosting Decision Tree

在上面简单介绍了Gradient Boost框架,梯度提升决策树Gradient Boosting Decision Tree是Gradient Boost框架下使用较多的一种模型,在梯度提升决策树中,其基学习器是分类回归树CART,使用的是CART树中的回归树。

1、分类回归树CART

分类回归树CART算法是一种基于二叉树的机器学习算法,其既能处理回归问题,又能处理分类为题,在梯度提升决策树GBDT算法中,使用到的是CART回归树算法,对于CART树算法的更多信息,可以参考简单易学的机器学习算法——分类回归树CART。

对于一个包含了m

m个训练样本的回归问题,其训练样本为:

{(X

(1)

,y

(1)

),(X

(2)

,y

(2)

),⋯,(X

(m)

,y

(m)

)}

{(X(1),y(1)),(X(2),y(2)),⋯,(X(m),y(m))}

其中,X

(i)

X(i)为n

n维向量,表示的是第i

i个样本的特征,y

(i)

y(i)为样本的标签,在回归问题中,标签y

(i)

y(i)为一系列连续的值。此时,利用训练样本训练一棵CART回归树:

  • 开始时,CART树中只包含了根结点,所有样本都被划分在根结点上:

梯度提升决策树GBDT

此时,计算该节点上的样本的方差(此处要乘以m

m),方差表示的是数据的波动程度。那么,根节点的方差的m

m倍为:

s

2

⋅m=(y

(1)

−y

¯

)

2

+(y

(2)

−y

¯

)

2

+⋯+(y

(m)

−y

¯

)

2

s2⋅m=(y(1)−y¯)2+(y(2)−y¯)2+⋯+(y(m)−y¯)2

其中,y

¯

y¯为标签的均值。此时,从n

n维特征中选择第j

j维特征,从m

m个样本中选择一个样本的值:x

j

xj作为划分的标准,当样本i

i的第j

j维特征小于等于x

j

xj时,将样本划分到左子树中,否则,划分到右子树中,通过以上的操作,划分到左子树中的样本个数为m

1

m1,划分到右子树的样本的个数为m

2

=m−m

1

m2=m−m1,其划分的结果如下图所示:

梯度提升决策树GBDT

那么,什么样本的划分才是当前的最好划分呢?此时计算左右子树的方差之和:s

2

1

⋅m

1

+s

2

2

⋅m

2

s12⋅m1+s22⋅m2:

s

2

1

⋅m

1

+s

2

2

⋅m

2

=∑

X

(i)

∈left

(y

(i)

−y

¯

1

)

2

+∑

X

(j)

∈right

(y

(j)

−y

¯

2

)

2

s12⋅m1+s22⋅m2=∑X(i)∈left(y(i)−y¯1)2+∑X(j)∈right(y(j)−y¯2)2

其中,y

¯

1

y¯1为左子树中节点标签的均值,同理,y

¯

2

y¯2为右子树中节点标签的均值。选择其中s

2

1

⋅m

1

+s

2

2

⋅m

2

s12⋅m1+s22⋅m2最小的划分作为最终的划分,依次这样划分下去,直到得到最终的划分,划分的结果为:

梯度提升决策树GBDT

注意:对于上述最优划分标准的选择,以上的计算过程可以进一步优化。

首先,对于s

2

⋅m

s2⋅m:

s

2

⋅m

=∑

X

(i)

(y

(i)

−y

¯

)

2

=∑

X

(i)

((y

(i)

)

2

−2y

(i)

⋅y

¯

+(y

¯

)

2

)

=∑

X

(i)

(y

(i)

)

2

−2

m

(∑

X

(i)

y

(i)

)

2

+1

m

(∑

X

(i)

y

(i)

)

2

=∑

X

(i)

(y

(i)

)

2

−1

m

(∑

X

(i)

y

(i)

)

2

s2⋅m=∑X(i)(y(i)−y¯)2=∑X(i)((y(i))2−2y(i)⋅y¯+(y¯)2)=∑X(i)(y(i))2−2m(∑X(i)y(i))2+1m(∑X(i)y(i))2=∑X(i)(y(i))2−1m(∑X(i)y(i))2

而对于s

2

1

⋅m

1

+s

2

2

⋅m

2

s12⋅m1+s22⋅m2:

s

2

1

⋅m

1

+s

2

2

⋅m

2

=∑

X

(i)

∈left

(y

(i)

−y

¯

1

)

2

+∑

X

(j)

∈right

(y

(j)

−y

¯

2

)

2

=∑

X

(i)

∈left

((y

(i)

)

2

−2y

(i)

⋅y

¯

1

+(y

¯

1

)

2

)+∑

X

(j)

∈right

((y

(j)

)

2

−2y

(j)

⋅y

¯

2

+(y

¯

2

)

2

)

s12⋅m1+s22⋅m2=∑X(i)∈left(y(i)−y¯1)2+∑X(j)∈right(y(j)−y¯2)2=∑X(i)∈left((y(i))2−2y(i)⋅y¯1+(y¯1)2)+∑X(j)∈right((y(j))2−2y(j)⋅y¯2+(y¯2)2)

=∑

X

(i)

(y

(i)

)

2

−2

m

1

X

(i)

∈left

y

(i)

2

+1

m

1

X

(i)

∈left

y

(i)

2

−2

m

2

X

(j)

∈right

y

(j)

2

+1

m

2

X

(j)

∈right

y

(j)

2

=∑

X

(i)

(y

(i)

)

2

−1

m

1

X

(i)

∈left

y

(i)

2

−1

m

2

X

(j)

∈right

y

(j)

2

=∑X(i)(y(i))2−2m1(∑X(i)∈lefty(i))2+1m1(∑X(i)∈lefty(i))2−2m2(∑X(j)∈righty(j))2+1m2(∑X(j)∈righty(j))2=∑X(i)(y(i))2−1m1(∑X(i)∈lefty(i))2−1m2(∑X(j)∈righty(j))2

通过以上的过程,我们发现,划分前,记录节点的值为:

1

m

(∑

X

(i)

y

(i)

)

2

1m(∑X(i)y(i))2

当划分后,两个节点的值的和为:

1

m

1

X

(i)

∈left

y

(i)

2

+1

m

2

X

(j)

∈right

y

(j)

2

1m1(∑X(i)∈lefty(i))2+1m2(∑X(j)∈righty(j))2

最好的划分,对应着两个节点的值的和的最大值。

2、GBDT——二分类

在梯度提升决策树GBDT中,通过定义不同的损失函数,可以完成不同的学习任务,二分类是机器学习中一类比较重要的分类算法,在二分类中,其损失函数为:

L(y,F)=log(1+exp(−2yF)),y∈{−1,1}

L(y,F)=log(1+exp(−2yF)),y∈{−1,1}

套用上面介绍的GB框架,得到下述的二分类GBDT的算法:

梯度提升决策树GBDT

在构建每一棵CART回归树的过程中,对一个样本的预测值应与y

~

y~尽可能一致,对于y

~

y~,其计算过程为:

y

~

(i)

=−[∂L(y

(i)

,F(X

(i)

))

∂F(X

(i)

)

]

F(X)=F

m−1

(X)

=−[∂log(1+exp(−2y

(i)

F(X

(i)

)))

∂F(X

(i)

)

]

F(X)=F

m−1

(X)

=−[1

1+exp(−2y

(i)

F(X

(i)

))

⋅exp(−2y

(i)

F(X

(i)

))⋅(−2y

(i)

)]

F(X)=F

m−1

(X)

y~(i)=−[∂L(y(i),F(X(i)))∂F(X(i))]F(X)=Fm−1(X)=−[∂log(1+exp(−2y(i)F(X(i))))∂F(X(i))]F(X)=Fm−1(X)=−[11+exp(−2y(i)F(X(i)))⋅exp(−2y(i)F(X(i)))⋅(−2y(i))]F(X)=Fm−1(X)

=2y

(i)

⋅exp(−2y

(i)

F(X

(i)

))

1+exp(−2y

(i)

F(X

(i)

))

F(X)=F

m−1

(X)

=2y

(i)

1+exp(2y

(i)

F

m−1

(X

(i)

))

=2y(i)⋅exp(−2y(i)F(X(i)))1+exp(−2y(i)F(X(i)))F(X)=Fm−1(X)=2y(i)1+exp(2y(i)Fm−1(X(i)))

在y

~

y~(通常有的地方称为残差,在这里,更准确的讲是梯度下降的方向)上构建CART回归树。最终将每一个训练样本划分到对应的叶子节点中,计算此时该叶子节点的预测值:

γ

jm

=argmin

γ

X

(i)

∈R

jm

log(1+exp(−2y

(i)

(F

m−1

(X

(i)

)+γ)))

γjm=argminγ∑X(i)∈Rjmlog(1+exp(−2y(i)(Fm−1(X(i))+γ)))

由Newton-Raphson迭代公式可得:

γ

jm

=∑

X

(i)

∈R

jm

y

~

(i)

X

(i)

∈R

jm

y

~

(i)

(2−∣

y

~

(i)

)

γjm=∑X(i)∈Rjmy~(i)∑X(i)∈Rjm|y~(i)|(2−|y~(i)|)

以参考文献3 Idiots’ Approach for Display Advertising Challenge中提供的代码为例:

  • GBDT训练的主要代码为:

void GBDT::fit(Problem const &Tr, Problem const &Va)

{

bias = calc_bias(Tr.Y); //用于初始化的F

std::vector<float> F_Tr(Tr.nr_instance, bias), F_Va(Va.nr_instance, bias);

Timer timer;

printf("iter time tr_loss va_loss");

// 开始训练每一棵CART树

for(uint32_t t = 0; t < trees.size(); ++t)

{

timer.tic();

std::vector<float> const &Y = Tr.Y;

std::vector<float> R(Tr.nr_instance), F1(Tr.nr_instance); // 记录残差和F

#pragma omp parallel for schedule(static)

for(uint32_t i = 0; i < Tr.nr_instance; ++i)

R[i] = static_cast<float>(Y[i]/(1+exp(Y[i]*F_Tr[i]))); //计算残差,或者称为梯度下降的方向

// 利用上面的残差值,在此函数中构造一棵树

trees[t].fit(Tr, R, F1); // 分类树的生成

double Tr_loss = 0;

// 用上面训练的结果更新F_Tr,并计算log_loss

#pragma omp parallel for schedule(static) reduction(+: Tr_loss)

for(uint32_t i = 0; i < Tr.nr_instance; ++i)

{

F_Tr[i] += F1[i];

Tr_loss += log(1+exp(-Y[i]*F_Tr[i]));

}

Tr_loss /= static_cast<double>(Tr.nr_instance);

// 用上面训练的结果预测测试集,打印log_loss

#pragma omp parallel for schedule(static)

for(uint32_t i = 0; i < Va.nr_instance; ++i)

{

std::vector<float> x = construct_instance(Va, i);

F_Va[i] += trees[t].predict(x.data()).second;

}

double Va_loss = 0;

#pragma omp parallel for schedule(static) reduction(+: Va_loss)

for(uint32_t i = 0; i < Va.nr_instance; ++i)

Va_loss += log(1+exp(-Va.Y[i]*F_Va[i]));

Va_loss /= static_cast<double>(Va.nr_instance);

printf("%4d %8.1f %10.5f %10.5f", t, timer.toc(), Tr_loss, Va_loss);

fflush(stdout);

}

}

  • 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
  • CART回归树的训练代码为:

void CART::fit(Problem const &prob, std::vector<float> const &R, std::vector<float> &F1){

uint32_t const nr_field = prob.nr_field; // 特征的个数

uint32_t const nr_sparse_field = prob.nr_sparse_field;

uint32_t const nr_instance = prob.nr_instance; // 样本的个数

std::vector<Location> locations(nr_instance); // 样本信息

#pragma omp parallel for schedule(static)

for(uint32_t i = 0; i < nr_instance; ++i)

locations[i].r = R[i]; // 记录每一个样本的残差

for(uint32_t d = 0, offset = 1; d < max_depth; ++d, offset *= 2){// d:深度

uint32_t const nr_leaf = static_cast<uint32_t>(pow(2, d)); // 叶子节点的个数

std::vector<Meta> metas0(nr_leaf); // 叶子节点的信息

for(uint32_t i = 0; i < nr_instance; ++i){

Location &location = locations[i]; //第i个样本的信息

if(location.shrinked)

continue;

Meta &meta = metas0[location.tnode_idx-offset]; //找到对应的叶子节点

meta.s += location.r; //残差之和

++meta.n;

}

std::vector<Defender> defenders(nr_leaf*nr_field); //记录每一个叶节点的每一维特征

std::vector<Defender> defenders_sparse(nr_leaf*nr_sparse_field);

// 针对每一个叶节点

for(uint32_t f = 0; f < nr_leaf; ++f){

Meta const &meta = metas0[f]; // 叶子节点

double const ese = meta.s*meta.s/static_cast<double>(meta.n); //该叶子节点的ese

for(uint32_t j = 0; j < nr_field; ++j)

defenders[f*nr_field+j].ese = ese;

for(uint32_t j = 0; j < nr_sparse_field; ++j)

defenders_sparse[f*nr_sparse_field+j].ese = ese;

}

std::vector<Defender> defenders_inv = defenders;

std::thread thread_f(scan, std::ref(prob), std::ref(locations),

std::ref(metas0), std::ref(defenders), offset, true);

std::thread thread_b(scan, std::ref(prob), std::ref(locations),

std::ref(metas0), std::ref(defenders_inv), offset, false);

scan_sparse(prob, locations, metas0, defenders_sparse, offset, true);

thread_f.join();

thread_b.join();

// 找出最佳的ese,scan里是每个字段的最佳ese,这里是所有字段的最佳ese,赋值给相应的tnode

for(uint32_t f = 0; f < nr_leaf; ++f){

// 对于每一个叶节点都找到最好的划分

Meta const &meta = metas0[f];

double best_ese = meta.s*meta.s/static_cast<double>(meta.n);

TreeNode &tnode = tnodes[f+offset];

for(uint32_t j = 0; j < nr_field; ++j){

Defender defender = defenders[f*nr_field+j];//每一个叶节点都对应着所有的特征

if(defender.ese > best_ese)

{

best_ese = defender.ese;

tnode.feature = j;

tnode.threshold = defender.threshold;

}

defender = defenders_inv[f*nr_field+j];

if(defender.ese > best_ese)

{

best_ese = defender.ese;

tnode.feature = j;

tnode.threshold = defender.threshold;

}

}

for(uint32_t j = 0; j < nr_sparse_field; ++j)

{

Defender defender = defenders_sparse[f*nr_sparse_field+j];

if(defender.ese > best_ese)

{

best_ese = defender.ese;

tnode.feature = nr_field + j;

tnode.threshold = defender.threshold;

}

}

}

// 把每个instance都分配给树里的一个叶节点下

#pragma omp parallel for schedule(static)

for(uint32_t i = 0; i < nr_instance; ++i){

Location &location = locations[i];

if(location.shrinked)

continue;

uint32_t &tnode_idx = location.tnode_idx;

TreeNode &tnode = tnodes[tnode_idx];

if(tnode.feature == -1){

location.shrinked = true;

}else if(static_cast<uint32_t>(tnode.feature) < nr_field){

if(prob.Z[tnode.feature][i].v < tnode.threshold)

tnode_idx = 2*tnode_idx;

else

tnode_idx = 2*tnode_idx+1;

}else{

uint32_t const target_feature = static_cast<uint32_t>(tnode.feature-nr_field);

bool is_one = false;

for(uint64_t p = prob.SJP[i]; p < prob.SJP[i+1]; ++p)

{

if(prob.SJ[p] == target_feature)

{

is_one = true;

break;

}

}

if(!is_one)

tnode_idx = 2*tnode_idx;

else

tnode_idx = 2*tnode_idx+1;

}

}

}

// 用于计算gamma

std::vector<std::pair<double, double>>

tmp(max_tnodes, std::make_pair(0, 0));

for(uint32_t i = 0; i < nr_instance; ++i)

{

float const r = locations[i].r;

uint32_t const tnode_idx = locations[i].tnode_idx;

tmp[tnode_idx].first += r;

tmp[tnode_idx].second += fabs(r)*(1-fabs(r));

}

for(uint32_t tnode_idx = 1; tnode_idx <= max_tnodes; ++tnode_idx)

{

double a, b;

std::tie(a, b) = tmp[tnode_idx];

tnodes[tnode_idx].gamma = (b <= 1e-12)? 0 : static_cast<float>(a/b);

}

#pragma omp parallel for schedule(static)

for(uint32_t i = 0; i < nr_instance; ++i)

F1[i] = tnodes[locations[i].tnode_idx].gamma;// 重新更新F1的值

}

  • 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
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155


相关推荐