集成学习总结
写在前面
看了不少集成学习的资料,很多算法都有相似之处,看了之后没有进行比较和整理,太容易忘记了,所以这篇来做个笔记。
集成学习,(就我目前所学习到的)主要可以分为三大类,Boosting, Bagging, Stacking。Boosting的代表有AdaBoost, gbdt, xgboost。而Bagging的代表则是随机森林 (Random Forest)。Stacking 的话,好像还没有著名的代表,可以视其为一种集成的套路。
Boosting
首先,Boosting是一个迭代提升的过程,所以它肯定是串行的算法(尽管xgboost可以在节点分裂属性选择上做并行计算)。基于训练集,先训练弱学习器,然后根据前一个弱学习器分错的样本,改变样本的概率分布构成新的训练集,从而可以训练出一个更强的学习器。这样反复迭代提升,就能得到一系列分类器。最后,将这些分类器组合起来,就能构成一个很强的学习器。
AdaBoost
给定一个二分类的训练数据集
T={(x1,y1),…,(xN,yN)}
其中,x_i 是n维的, 类标y_i={-1,+1}
AdaBoost算法的步骤:
(1) 初始化训练数据的权值分布:D1=(w11,…,wi1,…,w1N),w1i=1/N,i=1,2,…,N(即初始时,每个样本视为一样的)
(2) 对m=1,2,…,M
(2a) 对具有权值分布Dm的训练数据集学习,得到一个基本分类器Gm(x)
(2b) 计算Gm(x)在训练数据集上的分类错误率:em=P(Gm(xi)!=yi)
(2c) 根据分类错误率计算Gm(x)的加权系数:am
(2d) 根据加权系数更新训练数据集的权值分布Dm+1
(3)以上学到了M个“弱”学习器,将这M个弱学习器加权求和:f(x)=sum(am*Gm(x)),最终的分类器为G(x)=sign(f(x))
总之,AdaBoost的主要思想就是在不改变训练数据的情况下,通过在迭代训练弱学习器中,不断提升被错分类样本的权重(也就是使被错分的样本在下一轮训练时得到更多的重视),不断减少正确分类样本的权重。最后通过加权线性组合M个弱分类器得到最终的分类器,正确率越高的弱分类器的投票权数越高,正确率低的弱分类器自然投票权数就低。
GBDT
GBDT的基本原理是boosting里面的 boosting tree(提升树),并使用gradient boost。GBDT中的树都是回归树,不是分类树 ,因为gradient boost 需要按照损失函数的梯度近似的拟合残差,这样拟合的是连续数值,因此只有回归树。Gradient Boosting是一种Boosting的方法,其与传统的Boosting的区别是,每一次的计算是为了减少上一次的残差(residual),而为了消除残差,可以在残差减少的梯度(Gradient)方向上建立一个新的模型。所以说,在Gradient Boosting中,每个新的模型的建立是为了使得之前模型的残差往梯度方向减少,与传统Boosting对正确、错误样本进行加权有着很大的区别。这个梯度代表上一轮学习器损失函数对预测值求导。与Boosting Tree的区别:Boosting Tree的适合于损失函数为平方损失或者指数损失。而Gradient Boosting适合各类损失函数(损失函数为:平方损失则相当于Boosting Tree拟合残差、损失函数为:使用指数损失则可以近似于Adaboost,但树是回归树)
下面是完整的GBDT介绍。
GBDT(Gradient Boosting Decision Tree) 又叫 MART(Multiple Additive Regression Tree),是一种迭代的决策树算法,该算法由多棵决策树组成,所有树的结论累加起来做最终答案。它在被提出之初就和SVM一起被认为是泛化能力较强的算法。GBDT中的树是回归树(不是分类树),GBDT用来做回归预测,调整后也可以用于分类。GBDT的思想使其具有天然优势可以发现多种有区分性的特征以及特征组合。
Regression Decision Tree:回归树
回归树总体流程类似于分类树,区别在于,回归树的每一个节点都会得一个预测值,以年龄为例,该预测值等于属于这个节点的所有人年龄的平均值。分枝时穷举每一个feature的每个阈值找最好的分割点,但衡量最好的标准不再是最大熵,而是最小化平方误差。也就是被预测出错的人数越多,错的越离谱,平方误差就越大,通过最小化平方误差能够找到最可靠的分枝依据。分枝直到每个叶子节点上人的年龄都唯一或者达到预设的终止条件(如叶子个数上限),若最终叶子节点上人的年龄不唯一,则以该节点上所有人的平均年龄做为该叶子节点的预测年龄。
![](https://i-blog.csdnimg.cn/blog_migrate/49d5d4ec53a86933c15c9426341d8f35.png)
回归树算法如下图(截图来自《统计学习方法》5.5.1 CART生成):
![](https://i-blog.csdnimg.cn/blog_migrate/52493577739ad420aa9a5384a493c0e5.png)
请注意上图中的下标和上标,xi” role=”presentation” style=”position: relative;”>xixi表示该样本的第j个feature。所以上图中的遍历划分变量j的意思是遍历feature和相应的s,找出使平方误差和最小的(j,s)。
Boosting Decision Tree:提升树算法
提升树是迭代多棵回归树来共同决策。当采用平方误差损失函数时,每一棵回归树学习的是之前所有树的结论和残差,拟合得到一个当前的残差回归树,残差的意义如公式:残差 = 真实值 - 预测值 。提升树即是整个迭代过程生成的回归树的累加。
举个例子,参考自一篇博客(参考文献 4),该博客举出的例子较直观地展现出多棵决策树线性求和过程以及残差的意义。
训练一个提升树模型来预测年龄:
训练集是4个人,A,B,C,D年龄分别是14,16,24,26。样本中有购物金额、上网时长、经常到百度知道提问等特征。提升树的过程如下:
![](https://i-blog.csdnimg.cn/blog_migrate/d4007411fb326d0ad8dd6af19df2fd9a.jpeg)
该例子很直观的能看到,预测值等于所有树值得累加,如A的预测值 = 树1左节点 值 15 + 树2左节点 -1 = 14。
因此,给定当前模型 fm-1(x),只需要简单的拟合当前模型的残差。现将回归问题的提升树算法叙述如下:
![](https://i-blog.csdnimg.cn/blog_migrate/252adb4e0a1d0fedad0218040b99ff3b.png)
Gradient Boosting Decision Tree:梯度提升决策树
提升树利用加法模型和前向分步算法实现学习的优化过程。当损失函数是平方损失和指数损失函数时,每一步的优化很简单,如平方损失函数学习残差回归树。
![](https://i-blog.csdnimg.cn/blog_migrate/2b1658c1951bf7189b2a2cec5812a6d7.png)
但对于一般的损失函数,往往每一步优化没那么容易,如上图中的绝对值损失函数和Huber损失函数。针对这一问题,Freidman提出了梯度提升算法:利用最速下降的近似方法,即利用损失函数的负梯度在当前模型的值,作为回归问题中提升树算法的残差的近似值,拟合一个回归树。(注:鄙人私以为,与其说负梯度作为残差的近似值,不如说残差是负梯度的一种特例)算法如下(截图来自《The Elements of Statistical Learning》):
![](https://i-blog.csdnimg.cn/blog_migrate/e3791adee3a8b4fecb3b8767f013f2a4.png)
xgboost
xgboost类似于gbdt,但是很多地方经过了Tianqi Chen大牛的优化,因此不论是精度还是效率上都有了提升。与gbdt相比,具体的优点有:
1.损失函数是用泰勒展式二项逼近,而不是像gbdt里就是一阶导数
2.对树的结构进行了正则化约束,防止模型过度复杂,降低了过拟合的可能性
3.节点分裂的方式不同,gbdt是用的gini系数,xgboost是经过优化推导后的
xgboost是GB算法的高效实现,xgboost中的基学习器除了可以是CART(gbtree)也可以是线性分类器(gblinear)。下面所有的内容来自原始paper,包括公式。
(1) xgboost在目标函数中显示的加上了正则化项,基学习器为CART时,正则化项与树的叶子节点的数量T和叶子节点的值有关。
![](https://i-blog.csdnimg.cn/blog_migrate/5c90682a768bdc77bef71023254d5f8a.png)
(2) GB中使用Loss Function对f(x)的一阶导数计算出伪残差用于学习生成fm(x),xgboost不仅使用到了一阶导数,还使用二阶导数。第t次的loss:
![](https://i-blog.csdnimg.cn/blog_migrate/54c990a5d1532274d72d3d35e4934a96.png)
对上式做二阶泰勒展开:
![](https://i-blog.csdnimg.cn/blog_migrate/915ecbce0daaca93d6007e940a93b880.png)
(3) 上面提到CART回归树中寻找最佳分割点的衡量标准是最小化均方差,xgboost寻找分割点的标准是最大化Lsplit
![](https://i-blog.csdnimg.cn/blog_migrate/a595202de01aebaf1b2293a5f19c43de.png)
xgboost算法的步骤和GB基本相同,都是首先初始化为一个常数,gb是根据一阶导数ri,xgboost是根据一阶导数gi和二阶导数hi,迭代生成基学习器,相加更新学习器。
xgboost与gdbt除了上述三点的不同,xgboost在实现时还做了许多优化:
- 在寻找最佳分割点时,考虑传统的枚举每个特征的所有可能分割点的贪心法效率太低,xgboost实现了一种近似的算法。大致的思想是根据百分位法列举几个可能成为分割点的候选者,然后从候选者中根据上面求分割点的公式计算找出最佳的分割点。
- xgboost考虑了训练数据为稀疏值的情况,可以为缺失值或者指定的值指定分支的默认方向,这能大大提升算法的效率,paper提到50倍。
- xgboost借鉴了随机森林中的列(特征)采样技术,即在某个节点分裂时,不是在当前节点中所有属性中选取最佳分裂属性,而是在当前属性集合中的某些属性中来选择最优分裂属性。这种方法降低了过拟合的可能性。
特征列排序后以块的形式存储在内存中,在迭代中可以重复使用;虽然boosting算法迭代必须串行,但是在处理每个特征列时可以做到并行。
按照特征列方式存储能优化寻找最佳的分割点,但是当以行计算梯度数据时会导致内存的不连续访问,严重时会导致cache miss,降低算法效率。paper中提到,可先将数据收集到线程内部的buffer,然后再计算,提高算法的效率。
- xgboost还考虑了当数据量比较大,内存不够时怎么有效的使用磁盘,主要是结合多线程、数据压缩、分片的方法,尽可能的提高算法的效率。
知乎上关于xgboost/gbdt讨论的经典问答
【问】xgboost/gbdt在调参时为什么树的深度很少就能达到很高的精度?
用xgboost/gbdt在在调参的时候把树的最大深度调成6就有很高的精度了。但是用DecisionTree/RandomForest的时候需要把树的深度调到15或更高。用RandomForest所需要的树的深度和DecisionTree一样我能理解,因为它是用bagging的方法把DecisionTree组合在一起,相当于做了多次DecisionTree一样。但是xgboost/gbdt仅仅用梯度上升法就能用6个节点的深度达到很高的预测精度,使我惊讶到怀疑它是黑科技了。请问下xgboost/gbdt是怎么做到的?它的节点和一般的DecisionTree不同吗?
【答】
这是一个非常好的问题,题主对各算法的学习非常细致透彻,问的问题也关系到这两个算法的本质。这个问题其实并不是一个很简单的问题,我尝试用我浅薄的机器学习知识对这个问题进行回答。
一句话的解释,来自周志华老师的机器学习教科书( 机器学习-周志华):Boosting主要关注降低偏差,因此Boosting能基于泛化性能相当弱的学习器构建出很强的集成;Bagging主要关注降低方差,因此它在不剪枝的决策树、神经网络等学习器上效用更为明显。
随机森林(random forest)和GBDT都是属于集成学习(ensemble learning)的范畴。集成学习下有两个重要的策略Bagging和Boosting。
Bagging算法是这样做的:每个分类器都随机从原样本中做有放回的采样,然后分别在这些采样后的样本上训练分类器,然后再把这些分类器组合起来。简单的多数投票一般就可以。其代表算法是随机森林。Boosting的意思是这样,他通过迭代地训练一系列的分类器,每个分类器采用的样本分布都和上一轮的学习结果有关。其代表算法是AdaBoost, GBDT。
其实就机器学习算法来说,其泛化误差可以分解为两部分,偏差(bias)和方差(variance)。这个可由下图的式子导出(这里用到了概率论公式D(X)=E(X^2)-[E(X)]^2)。偏差指的是算法的期望预测与真实预测之间的偏差程度,反应了模型本身的拟合能力;方差度量了同等大小的训练集的变动导致学习性能的变化,刻画了数据扰动所导致的影响。这个有点儿绕,不过你一定知道过拟合。
如下图所示,当模型越复杂时,拟合的程度就越高,模型的训练偏差就越小。但此时如果换一组数据可能模型的变化就会很大,即模型的方差很大。所以模型过于复杂的时候会导致过拟合。
![](https://i-blog.csdnimg.cn/blog_migrate/8be782a981b33142780b493fc2ab4f73.png)
当模型越简单时,即使我们再换一组数据,最后得出的学习器和之前的学习器的差别就不那么大,模型的方差很小。还是因为模型简单,所以偏差会很大。
也就是说,当我们训练一个模型时,偏差和方差都得照顾到,漏掉一个都不行。
对于Bagging算法来说,由于我们会并行地训练很多不同的分类器的目的就是降低这个方差(variance) ,因为采用了相互独立的基分类器多了以后,h的值自然就会靠近.所以对于每个基分类器来说,目标就是如何降低这个偏差(bias),所以我们会采用深度很深甚至不剪枝的决策树。
对于Boosting来说,每一步我们都会在上一轮的基础上更加拟合原数据,所以可以保证偏差(bias),所以对于每个基分类器来说,问题就在于如何选择variance更小的分类器,即更简单的分类器,所以我们选择了深度很浅的决策树。
【问】机器学习算法中GBDT和XGBOOST的区别有哪些?
【答】
传统GBDT以CART作为基分类器,xgboost还支持线性分类器,这个时候xgboost相当于带L1和L2正则化项的逻辑斯蒂回归(分类问题)或者线性回归(回归问题)。
传统GBDT在优化时只用到一阶导数信息,xgboost则对代价函数进行了二阶泰勒展开,同时用到了一阶和二阶导数。顺便提一下,xgboost工具支持自定义代价函数,只要函数可一阶和二阶求导。
xgboost在代价函数里加入了正则项,用于控制模型的复杂度。正则项里包含了树的叶子节点个数、每个叶子节点上输出的score的L2模的平方和。从Bias-variance tradeoff角度来讲,正则项降低了模型的variance,使学习出来的模型更加简单,防止过拟合,这也是xgboost优于传统GBDT的一个特性。
Shrinkage(缩减),相当于学习速率(xgboost中的eta)。xgboost在进行完一次迭代后,会将叶子节点的权重乘上该系数,主要是为了削弱每棵树的影响,让后面有更大的学习空间。实际应用中,一般把eta设置得小一点,然后迭代次数设置得大一点。(补充:传统GBDT的实现也有学习速率)
列抽样(column subsampling)即特征抽样。xgboost借鉴了随机森林的做法,支持列抽样,不仅能降低过拟合,还能减少计算,这也是xgboost异于传统gbdt的一个特性。
对缺失值的处理。对于特征的值有缺失的样本,xgboost可以自动学习出它的分裂方向。
xgboost工具支持并行。boosting不是一种串行的结构吗?怎么并行的?注意xgboost的并行不是tree粒度的并行,xgboost也是一次迭代完才能进行下一次迭代的(第t次迭代的代价函数里包含了前面t-1次迭代的预测值)。xgboost的并行是在特征粒度上的。我们知道,决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),xgboost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为了可能,在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。
可并行的近似直方图算法。树节点在进行分裂时,我们需要计算每个特征的每个分割点对应的增益,即用贪心法枚举所有可能的分割点。当数据无法一次载入内存或者在分布式情况下,贪心算法效率就会变得很低,所以xgboost还提出了一种可并行的近似直方图算法,用于高效地生成候选的分割点。
多种语言封装支持。
【问】为什么基于 tree-ensemble 的机器学习方法,在实际的 kaggle 比赛中效果非常好?
【答】
作者:马超
链接:https://www.zhihu.com/question/51818176/answer/127637712
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
通常,解释一个机器学习模型的表现是一件很复杂事情,而这篇文章尽可能用最直观的方式来解释这一问题。我主要从三个方面来回答楼主这个问题。
- 理论模型 (站在 vc-dimension 的角度)
- 实际数据
- 系统的实现 (主要基于 xgboost)
通常决定一个机器学习模型能不能取得好的效果,以上三个方面的因素缺一不可。
(1)站在理论模型的角度统计机器学习里经典的 vc-dimension 理论告诉我们:一个机器学习模型想要取得好的效果,这个模型需要满足以下两个条件:
- 模型在我们的训练数据上的表现要不错,也就是 trainning error 要足够小。
- 模型的 vc-dimension 要低。换句话说,就是模型的自由度不能太大,以防overfit.当然,这是我用大白话描述出来的,真正的 vc-dimension 理论需要经过复杂的数学推导,推出 vc-bound. vc-dimension 理论其实是从另一个角度刻画了一个我们所熟知的概念,那就是 bias variance trade-off.
好,现在开始让我们想象一个机器学习任务。对于这个任务,一定会有一个 “上帝函数” 可以完美的拟合所有数据(包括训练数据,以及未知的测试数据)。很可惜,这个函数我们肯定是不知道的 (不然就不需要机器学习了)。我们只可能选择一个 “假想函数” 来 逼近 这个 “上帝函数”,我们通常把这个 “假想函数” 叫做 hypothesis.
在这些 hypothesis 里,我们可以选择 svm, 也可以选择 logistic regression. 可以选择单棵决策树,也可以选择 tree-ensemble (gbdt, random forest). 现在的问题就是,为什么 tree-ensemble 在实际中的效果很好呢?
区别就在于 “模型的可控性”。
先说结论,tree-ensemble 这样的模型的可控性是好的,而像 LR 这样的模型的可控性是不够好的(或者说,可控性是没有 tree-ensemble 好的)。为什么会这样?别急,听我慢慢道来。
我们之前说,当我们选择一个 hypothsis 后,就需要在训练数据上进行训练,从而逼近我们的 “上帝函数”。我们都知道,对于 LR 这样的模型。如果 underfit,我们可以通过加 feature,或者通过高次的特征转换来使得我们的模型在训练数据上取得足够高的正确率。而对于 tree-enseble 来说,我们解决这一问题的方法是通过训练更多的 “弱弱” 的 tree. 所以,这两类模型都可以把 training error 做的足够低,也就是说模型的表达能力都是足够的。但是这样就完事了吗?没有,我们还需要让我们的模型的 vc-dimension 低一些。而这里,重点来了。在 tree-ensemble 模型中,通过加 tree 的方式,对于模型的 vc-dimension 的改变是比较小的。而在 LR 中,初始的维数设定,或者说特征的高次转换对于 vc-dimension 的影响都是更大的。换句话说,tree-ensemble 总是用一些 “弱弱” 的树联合起来去逼近 “上帝函数”,一次一小步,总能拟合的比较好。而对于 LR 这样的模型,我们很难去猜到这个“上帝函数”到底长什么样子(到底是2次函数还是3次函数?上帝函数如果是介于2次和3次之间怎么办呢?)。所以,一不小心我们设定的多项式维数高了,模型就 “刹不住车了”。俗话说的好,步子大了,总会扯着蛋。这也就是我们之前说的,tree-ensemble 模型的可控性更好,也即更不容易 overfit.
(2)站在数据的角度
除了理论模型之外, 实际的数据也对我们的算法最终能取得好的效果息息相关。kaggle 比赛选择的都是真实世界中的问题。所以数据多多少少都是有噪音的。而基于树的算法通常抗噪能力更强。比如在树模型中,我们很容易对缺失值进行处理。除此之外,基于树的模型对于 categorical feature 也更加友好。
除了数据噪音之外,feature 的多样性也是 tree-ensemble 模型能够取得更好效果的原因之一。通常在一个kaggle任务中,我们可能有年龄特征,收入特征,性别特征等等从不同 channel 获得的特征。而特征的多样性也正是为什么工业界很少去使用 svm 的一个重要原因之一,因为 svm 本质上是属于一个几何模型,这个模型需要去定义 instance 之间的 kernel 或者 similarity (对于linear svm 来说,这个similarity 就是内积)。这其实和我们在之前说过的问题是相似的,我们无法预先设定一个很好的similarity。这样的数学模型使得 svm 更适合去处理 “同性质”的特征,例如图像特征提取中的 lbp 。而从不同 channel 中来的 feature 则更适合 tree-based model, 这些模型对数据的 distributation 通常并不敏感。
(3)站在系统实现的角度
除了有合适的模型和数据,一个良好的机器学习系统实现往往也是算法最终能否取得好的效果的关键。一个好的机器学习系统实现应该具备以下特征:
- 正确高效的实现某种模型。我真的见过有些机器学习的库实现某种算法是错误的。而高效的实现意味着可以快速验证不同的模型和参数。
- 系统具有灵活、深度的定制功能。
- 系统简单易用。
- 系统具有可扩展性, 可以从容处理更大的数据。
到目前为止,xgboost 是我发现的唯一一个能够很好的满足上述所有要求的 machine learning package. 在此感谢青年才俊 陈天奇。
在效率方面,xgboost 高效的 c++ 实现能够通常能够比其它机器学习库更快的完成训练任务。
在灵活性方面,xgboost 可以深度定制每一个子分类器,并且可以灵活的选择 loss function(logistic,linear,softmax 等等)。除此之外,xgboost还提供了一系列在机器学习比赛中十分有用的功能,例如 early-stop, cv 等等在易用性方面,xgboost 提供了各种语言的封装,使得不同语言的用户都可以使用这个优秀的系统。
最后,在可扩展性方面,xgboost 提供了分布式训练(底层采用 rabit 接口),并且其分布式版本可以跑在各种平台之上,例如 mpi, yarn, spark 等等。
有了这么多优秀的特性,自然这个系统会吸引更多的人去使用它来参加 kaggle 比赛。
综上所述,理论模型,实际的数据,良好的系统实现,都是使得 tree-ensemble 在实际的 kaggle 比赛中“屡战屡胜”的原因。
Bagging
Bagging的代表算法是随机森林,简单说下随机森林的步骤:
(1) 对训练样本进行bootstrap采样,即有放回的采样,获得M个采样集合;
(2) 在这M个采样集合上训练处M个弱决策树。注意到,在决策树生成中还用到了列采样的技巧,原本决策树中节点分裂时,是选择当前节点中所有属性的最优属性进行划分的,但是列采样的技巧是在所有属性中的子集中选最优属性进行划分。这样做可以进一步降低过拟合的可能性;
(3) 对这M个训练出来的弱决策树进行集成。
Stacking
Stacking还没有代表性的算法,我姑且把它理解成一个集成的思想吧。具体做法是:
(1) 先将训练集D拆成k个大小相似但互不相交的子集D1,D2,…,Dk;
(2) 令Dj’= D - Dj,在Dj’上训练一个弱学习器Lj。将Dj作为测试集,获得Lj在Dj上的输出Dj’’;
(3) 步骤2可以得到k个弱学习器以及k个相应的输出Dj’’,这个k个输出加上原本的类标构成新的训练集Dn;
(4) 在Dn训练次学习器L,L即为最后的学习器。
以上Stacking只做了一层,据kaggle上的大神反馈,Stacking可以做好多层,会有神奇的效果。
下面给出kaggle中一个Stacking的实例,就是入门级的titanic那道题单层stacking的源码(只给出了stacking 的过程,前面特征工程处理的代码被省略掉了)。Stacking后的xgboost(得分:0.77990)比我之前只用xgboost时(得分:0.77512)提高了一点,排名上升了396…… 源码戳这
Log on 2017-6-22: Stacking了两次之后,得分从单次stacking的0.77990上升到了0.79904
Log on 2017-6-22: Stacking了三次之后,得分从两次stacking的0.77990降低到了0.78469。所以,Stacking并不是越多层越好,反而会变坏。
Reference
《The Elements of Statistical Learning》
《统计学习方法》
《Practical Lessons from Predicting Clicks on Ads at Facebook》
http://blog.csdn.net/puqutogether/article/details/44593647
http://blog.csdn.net/suranxu007/article/details/49910323
http://blog.csdn.net/lilyth_lilyth/article/details/48032119
http://www.searchtb.com/2010/12/an-introduction-to-treelink.html
https://www.zhihu.com/question/45487317
http://wakemeup.space/?p=187
http://www.jianshu.com/p/005a4e6ac775
https://www.zhihu.com/question/41354392/answer/98658997