Lesson 8.1 决策树的核心思想与建模流程

从本节课开始,我们将介绍经典机器学习领域中最重要的一类有监督学习算法——树模型(决策树)。

可此前的聚类算法类似,树模型也同样不是一个模型,而是一类模型的概称。树模型不仅运算效率高、模型判别能力强、而且原理简单过程清晰、可解释性强,是机器学习领域内为数不多的“白箱模型”。并且就树模型本身的功能来说,除了能够同时进行分类和回归预测外,还能够产出包括特征重要性、连续变量分箱指标等重要附加结论,而在集成学习中,最为常用的基础分类器也正是树模型。正是这些优势,使得树模型成为目前机器学习领域最为重要的模型之一。

一、借助逻辑回归构建决策树

那到底什么是树模型?接下来我们简单介绍树模型建模的基本思想。

尽管树模型作为经典模型,发展至今已是算法数量众多、流派众多,但大多数树模型的基本思想其实是相通的,我们可以用一句话来解释树模型的模型形态和建模目标,那就是:挖掘有效分类规则并以树状形式呈现。接下来我们就以一个简单实例进行说明,我们借助此前所学的基本建模知识,尝试复现决策树的基本分类思想。

# 科学计算模块
import numpy as np
import pandas as pd# 绘图模块
import matplotlib as mpl
import matplotlib.pyplot as plt# 自定义模块
from ML_basic_function import *# Scikit-Learn相关模块
# 评估器类
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import GridSearchCV# 实用函数
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score# 数据准备
from sklearn.datasets import load_iris

在Lesson 6.5节中,我们曾围绕鸢尾花数据集构建了多分类逻辑回归模型并且采用网格搜索对其进行最优超参数搜索,其基本过程如下:

# 数据准备
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=24)# 模型训练# 实例化模型
clf = LogisticRegression(max_iter=int(1e6), solver='saga')
# 构建参数空间
param_grid_simple = {'penalty': ['l1', 'l2'],'C': [1, 0.5, 0.1, 0.05, 0.01]}
# 构建网格搜索评估器
search = GridSearchCV(estimator=clf,param_grid=param_grid_simple)# 模型训练
search.fit(X_train, y_train)
#GridSearchCV(estimator=LogisticRegression(max_iter=1000000, solver='saga'),
#             param_grid={'C': [1, 0.5, 0.1, 0.05, 0.01],
#                         'penalty': ['l1', 'l2']})search.best_params_
#{'C': 1, 'penalty': 'l1'}
search.best_estimator_.coef_
#array([[ 0.        ,  0.        , -3.47337669,  0.        ],
#       [ 0.        ,  0.        ,  0.        ,  0.        ],
#       [-0.55511761, -0.34237661,  3.03227709,  4.12148646]])
search.best_estimator_.intercept_
#array([ 11.85884734,   2.65291107, -14.51175841])

我们发现,在参数组取值为{‘C’: 1, ‘penalty’: ‘l1’}的情况下,三个逻辑回归方程中,第一个方程只包含一个系数,也就是说明第一个方程实际上只用到了原数据集的一个特征,第二个方程自变量系数均为0、基本属于无用方程,而只有第三个方程自变量系数都不是0、看起来比较正常“正常”。我们知道,对于多分类问题,逻辑回归所构建的模型方程实际上是每个方程对应预测一个类别,而由于总共只有三个类别,因此是允许存在一个类别的预测方程失效的,只要剩下的两个类别能够各自完成对应类别的预测,则剩下的样本就属于第三类。此处我们需要重点关注的是第一个方程,该方程只有一个非零系数,其背后含义是模型只借助特征矩阵中的第三个特征,就很好的将第一类鸢尾花和其他鸢尾花区分开了。

我们进一步观察数据和第三个特征对于第一个类别的分类结果:

iris = load_iris(as_frame=True)
iris.data

t = np.array(iris.target)
t[50:]
#array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#       1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
#       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
#       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
# 将2、3类划归为一类
t[50:] = 1
t
#array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
#       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
#       0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
#       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
# 此处提取第3、4个特征放置二维空间进行观察,用第三个特征和其他特征组合也是类似
d = np.array(iris.data.iloc[:, 2: 4])
plt.scatter(d[:, 0], d[:, 1], c=t)
plt.plot(np.array([2.5]*25), np.arange(0, 2.5, 0.1), 'r--')


我们发现,确实可以通过第三个特征(横坐标)很好的区分第一类(紫色点簇)和其他两类(黄色点簇),也就是说,从分类结果来看,我们能够简单通过一个分类规则来区分第一类鸢尾花和其他两类,例如从上图可以看出,我们可以以petal length (cm) <= 2.5作为分类条件,当分类条件满足时,鸢尾花属于第一类,否则就属于第二、三类。至此我们集完成了对上述数据集的初步分类,基本分类情况可以通过下图来进行表示:

当然围绕上述未分类的二、三类鸢尾花数据,我们能否进一步找到类似刚才的分类规则对其进行有效分类呢?当然此处由于我们希望分类规则能够尽可能简洁,我们力求找出根据某一个特征的取值划分就能对数据集进行有效分类的方法,这时我们可以考虑先利用逻辑回归的l1正则化挑选出对二、三类分类最有分类效力的特征(也就是最重要的特征),然后根据只有一个特征系数不为0的带l1正则化的逻辑回归建模结果、找到决策边界,而该决策边界就是依据该单独特征划分Iris二、三类子数据的最佳方法。我们可以通过下述代码实现:

# 提取待分类的子数据集
X = np.array(iris.data)[t == 1]
y = np.array(iris.target)[t == 1]

接下来,我们构建一个包含l1正则化的逻辑回归模型,并通过不断调整C的取值、通过观察参数系数变化情况来挑选最重要的特征:

C_l = np.linspace(1, 0.1, 100)   #C越小,经验风险惩罚力度越小
coef_l = []for C in C_l:clf = LogisticRegression(penalty='l1', C=C, max_iter=int(1e6), solver='saga').fit(X, y)coef_l.append(clf.coef_.flatten())ax = plt.gca()
ax.plot(C_l, coef_l)
ax.set_xlim(ax.get_xlim()[::-1])    #横坐标翻过来了
plt.xlabel('C')
plt.ylabel('weights')

coef_l
#[array([-1.12493251, -0.96058774,  3.23208646,  4.18486998]),
# array([-1.10210628, -0.91621566,  3.23983307,  4.15696154]),
# array([-1.09239629, -0.89555328,  3.23399288,  4.13377003]),
# array([-1.08811789, -0.88498435,  3.22207551,  4.11256892]),
# ...
# array([0.        , 0.        , 2.05106895, 0.        ]),
# array([0.        , 0.        , 1.93251455, 0.        ]),
# array([0.        , 0.        , 1.80075548, 0.        ])]

不难看出,在对鸢尾花数据集的二、三分类的子数据集进行分类时,仍然还是第三个特征会相对重要,因此我们根据上述结果,构建一个正则化项为l1、C取值为0.2的逻辑回归模型进行训练,此时由于其他三个特征的参数都被归零,因此该模型训练过程实际上就相当于带入第三个特征进行建模:

clf = LogisticRegression(penalty='l1', C=0.2, max_iter=int(1e6), solver='saga').fit(X, y)
clf.coef_, clf.intercept_
#(array([[0.        , 0.        , 2.84518611, 0.        ]]),
# array([-13.88186328]))
clf.score(X, y)
#0.93

此时模型准确率为93%,同样,如果构建一个只包含第三、四个特征的特征空间,此时上述逻辑回归建模结果的决策边界为x=b,其中b的取值如下:

b = 13.88186328 / 2.84518611
b
#4.87907038179657

我们可以通过可视化的方法观察此时特征空间中样本分布情况,以及x=b的决策边界的分类效果:

plt.plot(X[:, 2][y==1], X[:, 3][y==1], 'ro')
plt.plot(X[:, 2][y==2], X[:, 3][y==2], 'bo')
plt.plot(np.array([b]*20), np.arange(0.5, 2.5, 0.1), 'r--')


当然,我们也可以简单验算下x=b的决策边界是否是模型真实的分类边界:

y_pred = clf.predict(X)plt.scatter(X[:, 2], X[:, 3], c=y_pred)
plt.plot(np.array([b]*20), np.arange(0.5, 2.5, 0.1), 'r--')

注意,在确定第一个分类条件时我们没有直接根据逻辑回归的线性方程计算决策边界的主要原因是彼时逻辑回归方程是在mvm分类规则下的三个分类方程,其中每个方程其实都会一定程度上受到其他方程影响,导致决策边界无法直接通过方程系数进行计算。

尽管x=b分类边界的准确率不足100%,但其仍然不失为一个不错的分类规则,即分类条件为petal length (cm) <= 4.879,当分类条件满足时,鸢尾花属于第二类、不满足时鸢尾花属于第三类,根据此分类条件进行的分类准确率为93%。我们可以将围绕鸢尾花子数据集进行二、三类的分类过程进行如下方式表示:

至此,我们就根据两个简单的分类规则,对鸢尾花数据集进行了有效划分,此时整体准确率为:

1-(y != y_pred).sum() / 150
#0.9533333333333334
  • 决策树简单构建

而上述整个过程,我们是通过带正则化项的逻辑回归模型挖掘出的两个分类规则,并且这两个分类规则呈现递进的关系,也就是一个分类规则是在另一个分类规则的分类结果下继续进行分类,最终这两个分类规则和对应划分出来的数据集呈现出树状,而带有不同层次分类规则的模型,其实就是决策树模型,也就是说通过上面一系列的操作,我们就已经成功构建了一个决策树模型。

  • 决策树的分类过程

对于上述已经构建好的一个决策树来说,当对新数据进行判别时,任意进来一条数据我们都可以自上而下进行分类,先根据petal length (cm) <= 2.5判断是否属于第一类,如果不满足条件则不属于第一类,此时进一步考虑petal length (cm) <= 4.879是否满足条件,并据此判断是属于第二类还是第三类。

当然,目前主流的决策树并不是依据逻辑回归来寻找分类规则,但上述构建决策树模型的一般过程和核心思想和目前主流的决策树模型并无二致,因此我们可以围绕上述过程进行进一步总结:

  • 决策树模型本质

当决策树模型构建好了之后,实际上一个决策树就是一系列分类规则的叠加,换而言之,决策树模型的构建从本质上来看就是在挖掘有效的分类规则,然后以树的形式来进行呈现。

  • 决策树的树生长过程

在整个树的构建过程中,我们实际上是分层来对数据集进行划分的,每当定下一个分类规则后,我们就可以根据是否满足分类规则来对数据集进行划分,而后续的分类规则的挖掘则进一步根据上一层划分出来的子数据集的情况来定,逐层划分数据集、逐数据集寻找分类规则再划分数据集,实际上就就是树模型的生长过程,并且不难看出,这个过程实际上也是一个迭代计算过程(上一层的数据集决定有效规律的挖掘、而有效规律的挖掘)。而停止生长的条件,我们也可以根据“继续迭代对结果没有显著影响”这个一般思路来构建。

  • 树模型的基本结构

当然,在已经构建了决策树之后,我们也能够对一个树模型的内部结构来进行说明。对上述决策树来说,我们可以将其看成是点(数据集)和线构成的一个图结构(准确来说应该是一种有向无环图),而对于任何一个图结构,我们都能够通过点和线来构建对其的基本认知,对于决策树来说,我们主要将借助边的方向来定义不同类型点,首先我们知道如果一条边从A点引向B点,则我们这条边对于A点来说是出边、对于B点来说是入边,A节点是B节点的父节点,据此我们可以将决策树中所有的点进行如下类别划分:
(1)根节点(root node):没有入边,但有零条或者多条出边的点;
(2)内部点(internal node):只有一条入边并且有两条或多条出边的点;
(3)叶节点(leaf node):只有入边但没有出边的点;

因此,我们知道在一次次划分数据集的过程中,原始的完整数据集对应着决策树的根节点,而根结点划分出的子数据集就构成了决策树中的内部节点,同时迭代停止的时候所对应的数据集,其实就是决策树中的叶节点。并且在上述二叉树(每一层只有两个分支)中,一个父节点对应两个子节点。并且根据上述决策树的建模过程不难理解,其实每个数据集都是由一系列分类规则最终划分出来的,我们也可以理解成每个节点其实都对应着一系列分类规则,例如上述E节点实际上就是petal length (cm) <= 2.5和petal length (cm) <= 4.879同时为False时划分出来的数据集。

在了解决策树的一般建模过程和模型本质后,接下来我们来简单说明一下目前树模型的主流派系,然后详细讨论目前最通用的机器学习流派的决策树模型的建模过程。

二、决策树的分类与流派

正如此前所说,树模型并不是一个模型,而是一类模型。需要知道的是,尽管树模型的核心思想都是源于一种名为贪心算法的局部最优求解算法,但时至今日,树模型已经有数十种之多,并且划分为多个流派。目前主流的机器学习算法类别可划分如下:

  • ID3(Iterative Dichotomiser 3) 、C4.5、C5.0决策树

是最为经典的决策树算法、同时也是真正将树模型发扬光大的一派算法。最早的ID3决策树由Ross Quinlan在1975年(博士毕业论文中)提出,至此也奠定了现在决策树算法的基本框架——确定分类规则判别指标、寻找能够最快速降低信息熵的方式进行数据集划分(分类规则提取),不断迭代直至收敛;而C4.5则是ID3的后继者,C4.5在ID3的基础上补充了一系列基础概念、同时也优化了决策树的算法流程,一方面使得现在的树模型能够处理连续变量(此前的ID3只能处理分类变量),同时也能够一定程度提高树模型的生长速度,而C4.5也是目前最为通用的决策树模型的一般框架,后续尽管有其他的决策树模型诞生,但大都是在C4.5的基本流程上进行略微调整或者指标修改,甚至在C4.5还被IEEE评为10大数据挖掘算法之首,由此可见C4.5算法的巨大影响力。此外,由于C4.5开源时间较早,这也使得在过去的很长一段时间内,C4.5都是最通用的决策树算法。当然在此后,Ross Quinlan又公布了C5.0算法,进一步优化了运行效率和预测流程,通过一系列数据结构的调整使得其能够更加高效的利用内存、并提高执行速度。当然,由于C5.0在很长的一段时间是作为收费软件存在、并且多集成与像SAS软件中,因此并未被最广泛的应用于各领域。

此外,值得一提的是,由于Ross Quinlan拥有非常深厚的数学背景,因此在设计决策树算法的时候,尽管决策树是一种非参数方法(无需提前进行数据训练的假设检验),但在实际执行决策树剪枝(一种防止过拟合的手段)时却需要用到非常多统计学方法,在实际构建模型时也无需划分训练集和测试集,因此C4.5其实更像是一种统计学算法,而非机器学习算法。

需要知道的是,C4.5在树的生长上还是更像机器学习算法,而这种半“统计学”半“机器学习”的状态也是该算法存在争议的地方。

  • CART决策树

CART全称为Classification and Regression Trees,即分类与回归决策树,同时也被称为C&RT算法,在1984年由Breiman、Friedman、Olshen和Stone四人共同提出。CART树和C4.5决策树的构造过程非常类似,但拓展了回归类问题的计算流程(此前C4.5只能解决分类问题),并且允许采用更丰富的评估指标来指导建模流程,并且,最关键的是,CART算法其实是一个非常典型的机器学习算法,在早期CART树的训练过程中,就是通过划分训练集和验证集(或者测试集)来验证模型结果、并进一步据此来调整模型结构,当然,除此以外,CART树还能够用一套流程同时处理离散变量和连续变量、能够同时处理分类问题和回归问题,这些都符合一个机器学习领域要求算法有更普适的功能和更强的鲁棒性的要求,这也是为何近几年CART树会更加流行的主要原因。当然,在skelarn中,决策树模型评估器集成的也是CART树模型,稍后我们在介绍决策树建模流程的时候也将主要介绍CART树的建模流程。

此处我们也可以参考sklearn中对于ID3、C4.5和CART树的对比描述:

需要注意的是,sklearn中也并非实现的是完全的CART树,通过相关评估器参数的调整,sklearn中也能实现“CART树的建模流程+C4.5的决策树生长指标”这种混合模型。

此外,与其说CART树是一个非常“机器学习”的算法,不如说CART树是一个更加适合使用机器学习的方法来进行建模的模型,机器学习或者统计学建模方法更大程度上是一种建模思路,很多模型(像逻辑回归、包括树模型在内)其实都有机器学习实现的方式和统计学模型实现的方法。

  • CHAID树

CHAID是Chi-square automatic interaction detection的简称,由Kass在1975年提出,如果说CART树是一个典型的机器学习算法,那么CHAID树就是一个典型的统计学算法。从该算法的名字就能看出,整个决策树其实是基于卡方检验(Chi-square)的结果来构建的,并且整个决策树的建模流程(树的生长过程)及控制过拟合的方法(剪枝过程)都和C4.5、CART有根本性的区别,例如CART都只能构建二叉树,而CHAID可以构建多分枝的树(注:C4.5也可以构建多分枝的树);例如C4.5和CART的剪枝都是自下而上(Bottom-up)进行剪枝,也被称为修剪法(Pruning Technique),而CHAID树则是自上而下(Top-Down)进行剪枝,也被称为盆栽法(Bonsai Technique)。当然,该决策树算法目前并非主流树模型,因此我们此处仅作简单介绍,并不做更加深入的探讨。

上述讨论所涉及到的关键概念,如剪枝、划分规则提取方式、划分规则评估指标等内容,我们都将在下一小节进行详细介绍。

在课程接下来的部分,我们将重点讨论关于CART树的建模流程,以及在Scikit-Learn中的实现方法。同时,ID3、C4.5决策树作为经典模型,尽管我们无法在sklearn中实现相关模型的建模,但对这些算法的了解仍然会非常有助于我们理解树模型的建模思想,因此我们后续也会以加餐形式介绍ID3、C4.5的基本建模流程。

Lesson 8.2 CART分类树的建模流程与sklearn评估器参数详解

根据上一小节的介绍我们知道,CART树是目前机器学习领域内最通用的决策树,并且CART树能够同时解决分类问题和回归问题。本节我们将首先介绍关于CART树在进行分类问题建模时的基本流程,同时详细讲解sklearn中相关评估器的参数情况与使用方法。

# 科学计算模块
import numpy as np
import pandas as pd# 绘图模块
import matplotlib as mpl
import matplotlib.pyplot as plt# 自定义模块
from ML_basic_function import *# Scikit-Learn相关模块
# 评估器类
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import GridSearchCV# 实用函数
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score# 数据准备
from sklearn.datasets import load_iris

一、CART决策树的分类流程

从基本原理来说,决策树是一种非常通用的模型,在模型训练时可以带入分类变量、也可以带入连续变量,同时在进行预测时既能解决分类问题也能解决回归问题,在介绍CART树的原理时,我们先从简单入手,先考虑自变量都是离散变量的分类预测问题,再逐步拓展连续变量的处理方法和回归类问题的预测方法。

1.CART树的基本生长过程

首先我们来看在特征都为分类变量时、围绕分类问题构建CART的基本过程。

1.1 规则评估指标选取与设置

  • 划分规则评估指标概念

在引言部分中我们曾简单构建了一个决策树,我们曾强调,决策树的建模过程实际上就是挖掘有效分类规律的过程,而这里的分类规律是否有效,其实是需要有一个评估标准的。此前我们是在逻辑回归的模型结论的基础上寻找分类规律,可以看成是根据分类结果的准确率来寻找分类规则,此时准确率就是评估分类条件好坏的评估指标,例如在决策树开始生长的第一层,我们选取petal length (cm) <= 2.5作为分类条件将原始数据集一分为二,分出来的两个数据集其中一个100%是一类数据,另一个则全是二、三类数据,此时如果我们根据准确率来判断该分类规则是否能够很好的区分一类和二、三类数据的话,那此时的准确率就是100%,此时的分类误差就是0。

  • 划分规则评估指标构建的核心思路

当然,这种定义分类评估指标的方法并不通用并且在多分类问题时容易引发混乱,例如如果是四分类问题,1条件能够100%区分A类和BCD类,2条件能够100%区分AB类和CD类,此时如何还按照准确率来进行判别分类条件好坏的判别一句,则选择哪个条件就成了问题。因此一般来说树模型挑选分类规则的评估指标并不是看每个类别划分后的准确率,而是父节点划分子节点后子节点数据集标签的纯度。

注意,此处纯度是一个非常重要的概念,一般来说如果一个数据集标签都比较倾向于取得同一个值(不管是都取0还是都取1),则我们就说这个数据集的标签纯度高,反之则说这个这个数据集标签纯度不高。而决策树的划分规则的评估标准,其实都是根据纯度来进行构建的。

其实,决策树生长的方向也就是令每个划分出来的子集纯度越来越高的方向。

  • 单独一个数据集的标签纯度衡量指标

首先我们先来讨论对于一个单独的数据集来说,可以通过哪些指标来衡量该数据集标签的纯度。一般来说,用于衡量数据集标签纯度的数值指标一般有三种,分别是分类误差、信息熵和基尼系数,对于每个单独数据集来说,其基本计算公式如下:

(1)分类误差(Classification error):
Classificationerror(t)=1−max⁡1≤i≤c[p(i∣t)]Classification\ error(t) = 1-\max_{1\leq i\leq c}[p(i|t)] Classification error(t)=1−1≤i≤cmax​[p(i∣t)]

其中i表示第i类,当前数据集总共有c类,p(i∣t)p(i|t)p(i∣t)代表第i类数据占当前数据集中总数据的比例。而所谓的分类误差,其实就是用1减去多数类的占比。例如某个包含10条数据的数据集,有6条0类数据、4条1类数据,此时该数据集分类误差就是1-6/10 = 0.4。分类误差在[0, 0.5]范围内取值,分类误差越小,说明数据集标签纯度越高。

此外,由于决策树中每一个数据集实际上就代表一系列分类规则,因此分类误差的另一种理解方法是,如果需要根据少数服从多数原则对当前数据集选取一个唯一的标签(此前一系列分类规则输出一个最终结果),则只能将所有少数类的标签改成多数类的标签,此时误差(对应的分类规则的误差)就是所有少数类的数据占比

分类误差实际上就是贪心算法所使用的决策树划分规则评估指标

(2)信息熵(Entropy):
Entropy(t)=−∑i=1cp(i∣t)log2p(i∣t)Entropy(t) = -\sum_{i=1}^c p(i|t)log_2p(i|t) Entropy(t)=−i=1∑c​p(i∣t)log2​p(i∣t)

当然,我们此前所介绍的用于衡量数据混乱程度的信息熵也可以用于衡量数据集标签纯度,而信息熵的计算过程也正如此前介绍的一样:每个类别的数据占比乘以以2为底的对数处理结果,然后再进行不同类别的累加,最后进行负值处理。例如还是针对一个包含了6个0类、4个1类的数据集,信息熵计算结果为:

- 4/10 * np.log2(4/10) - 6/10 * np.log2(6/10)
#0.9709505944546686

当然,回顾此前内容,信息熵也是在[0,1]之间取值,并且信息熵越小则说明数据集纯度越高。

信息熵的相关详细介绍参见Lesson 4.2

ID3、C4.5、C5.0算法主要使用信息熵进行划分规则的挑选

(3)基尼系数(Gini):
Gini(t)=1−∑i=1cp(i∣t)2Gini(t) = 1-\sum_{i=1}^c p(i|t)^2 Gini(t)=1−i=1∑c​p(i∣t)2

此外,还有一个和信息熵的计算过程比较类似的评估指标——基尼系数。基尼系数通过计算1减去每个类别占比的平方和来作为纯度衡量指标,例如还是针对一个包含了6个0类、4个1类的数据集,基尼系数的计算结果为:

1 - np.power([4/10, 6/10], 2).sum()
#0.48

而和信息熵不同的是,基尼系数在[0, 0.5]范围内取值,并且基尼系数越小表示数据集标签纯度越高

p_l = np.arange(0, 1, 0.01)gini_l = []for p in p_l:gini_l.append(1 - np.power([p, 1-p], 2).sum())
plt.plot(p_l, gini_l)

在默认情况下,CART树默认选择Gini系数作为评估指标。

在随后的建模试验中,我们也默认采用基尼系数作为评估指标。

当然我们也可以简单对比在二分类情况下三个评估指标伴随某类样本所占比例p值变化而变化的情况:

np.max([1, 2])
#2ce_l = []
gini_l = []
en_l = []for p in p_l:ce_l.append(1-np.max([p, 1-p]))gini_l.append(1 - np.power([p, 1-p], 2).sum())en_l.append(entropy(p))
plt.plot(ce_l, 'b-', gini_l, 'r-', en_l, 'r--')

  • 多个数据集的平均指标

在很多时候,我们不仅需要衡量单独数据集标签的纯度,我们还需要衡量多个数据集作为一个整体时的标签的纯度,例如一个父节点在划分成两个子节点时两个子节点整体的评估指标。此处我们举例说明:我们简化一组后续建模会用到的客户数据,简化后的数据集A共有两个特征、一个标签,并且标签只有0-1两个类别,数据集特征分别是收入(income)和信用评级(credit_rating),同样也都用有两个分类水平的离散变量表示。

此时我们首先可以计算该数据集整体的基尼系数:

p = 3/8
gini_A = 1 - np.power([p, 1-p], 2).sum()
gini_A
#0.46875

然后我们随意设置一个分类条件。例如我们设置分类条件为income <= 1.5,则可以将上述数据集进一步划分成两个子数据集B1、B2:

p = 2/5
gini_B1 = 1 - np.power([p, 1-p], 2).sum()
gini_B1
#0.48

而B2数据集只包含一个标签,因此B2的基尼系数为0

gini_B2 = 0

而此时如果要计算B1、B2整体的基尼系数,则需要在gini_B1、gini_B2的基础上进行各自数据集样本数量占整体数据集比例的加权求和,即根据如下方式进行计算:Gini(B)=∣B1∣∣A∣Gini(B1)+∣B2∣∣A∣Gini(B2)Gini(B) = \frac{|B_1|}{|A|}Gini(B_1)+\frac{|B_2|}{|A|}Gini(B_2) Gini(B)=∣A∣∣B1​∣​Gini(B1​)+∣A∣∣B2​∣​Gini(B2​)其中∣Bi∣∣A∣\frac{|B_i|}{|A|}∣A∣∣Bi​∣​为子数据集BiB_iBi​数据个数占父类数据集A中数据个数的比例。因此上述B1B_1B1​、B2B_2B2​整体基尼系数为:

gini_B = gini_B1 * 5/8 + gini_B2 * 3/8
gini_B
#0.3

至此,我们就构建了一个用于描述数据集划分完后两个子集的整体纯度的方法,而我们知道,子集整体纯度越高,其实也就说明对应分类规则越有效。接下来我们将详细讨论如何构建分类规则以及如何对这些分类规则进行评估。

1.2 决策树备选规则创建方法

正如此前所说,决策树模型的建模过程实际上就是在挑选有效分类规则的过程,而要挑选有效分类规则,首先就必须明确如何创建哪些备选规则,其实对于很多树模型,特征是离散型变量还是连续性变量会直接影响备选规则的创建,(C4.5两种情况不一样对待)但对于CART树以及sklearn中集成的方法来说,是将两种情况合二为一来进行看待,也就是根据如下方式来进行备选规则的创建:

对于任何特征矩阵,首先需要逐列对其进行数值排序,例如上述数据集A,我们可以单独提取income和credit_rating两列来进行降序排序,排序结果如下:

据此,我们通过寻找这些特征不同取值之间的中间点作为切点,来构造备选规则。例如income有两个取值1、2,因此只有一个切点就是1.5,那么我们就能创造一个income <= 1.5的规则来对数据集进行划分,如此我们就能把income取值为1的数据划归一个子集、income取值为2的数据集划归另一个子集,实际上上面在介绍多数据集基尼系数计算过程时就是采用该规则。需要知道的是,在所构造的条件中不等号的方向实际上没有任何影响。当然,income只有两个取值只能找到一个切点只能构造一个规则,而credit_rating特征也有两个取值,因此也能找到一个切点构造一个备选规则,即我们其实也可以根据credit_rating <= 1.5来切分出两个子集。

而其实如果特征是三个取值的特征,则可以找到两个切点、找到两种划分数据集的方式。更进一步的,如果该数据中某特征是连续变量,每条不同的数据取值都不同,例如:
则此时可以将其看成是拥有8个分类水平的分类变量,仍然还是寻找相邻取值水平的中间值作为切点来构造划分规则,此时由于age特征有8个不同的取值,因此可以构造7个备选的划分数据集的方法,例如age <= 36、age <= 34.5等等。也就是说,对于任何一个特征无论是连续型变量还是分类变量,只要有N个取值,就可以创造N-1个划分条件将原数据集划分成两份。

正是因为可以用一种方法就能同时处理连续变量和离散变量,因此在决策树建模的过程中我们无需特别区分两种类型特征的区别。

此外,需要注意的是,CART树用这种方法同时处理离散变量和连续变量,而C4.5只用这种方式处理连续变量(离散变量采用另一种方法),因此这里我们可以理解成是CART树将离散变量“连续化”,也就是将离散变量看成是连续变量,这也就是为何sklearn在说明文档中强调,sklearn的树模型无法处理分类变量的原因(原文:scikit-learn implementation does not support categorical variables for now.)。此处所谓的无法处理分类变量并不是不能带入分类变量进行建模,而是不支持类似C4.5的方法从离散特征中提取备选划分规则,而是会将离散变量也看成是连续变量,采用C4.5处理连续变量的方法处理离散变量。关于C4.5从离散特征中批量提取备选规则的方法我们会在课后阅读中介绍详细介绍。

实际上,机器学习不同统计算法,大多数时候都不会刻意区分特征的连续与离散。

1.3 挑选最佳分类规则划分数据集

当然,对于上述A数据集,总共有两个特征,每个特征有一个备选划分规则,因此在对根结点划分时,其实是有两种数据集的划分方法,我们已经简单查看采用income <= 1.5进行分类的结果:
而如果我们采用credit_rating <= 1.5来对数据集进行划分,则将出现下述结果:

从结果上来看,这两个划分条件都能切分出一个只包含一类标签的数据集,结果区分不是很大,那么到底应该选用哪个分类规则对数据集进行第一次切分、让决策树完成第一步的生长呢?这个时候就要用到此前我们介绍的关于评价分类规则是否有效的评估指标了,一般来说对于多个规则,我们首先会计算父节点的基尼系数(Gini(A)),然后计算划分出的两个子节点整体基尼系数(Gini(B)),然后通过对比哪种划分方式能够让二者差值更大,即能够让子节点的基尼系数下降更快,我们就选用哪个规则。例如对上述例子,我们知道在以income <= 1.5为规则划分数据集时,基尼系数下降结果为:

gini_A - gini_B
#0.16875

而如果采用第二个划分规则来进行数据集切分,则此时基尼系数下降结果为:

p = 3/4
gini_B2 = 1 - np.power([p, 1-p], 2).sum()
gini_B2
#0.375
gini_B1 = 0
gini_B = gini_B1 * 1/2 + gini_B2 * 1/2
gini_B
#0.1875
gini_A - gini_B
#0.28125

很明显,第二个规则能够让父节点的基尼系数下降更快,因此第二个规则、即credit_rating <= 1.5划分规则是一个更好的规则,在第一次数据集划分时我们应该采用该规则。

注,如果是ID3或者C4.5,此处则是以信息熵计算结果为准。

1.4 决策树的生长过程

当完成一次规则筛选与树生长后,接下来需要考虑的问题是,面对当前划分出的数据集B1、B2,是否还需要进一步寻找分类规则对其进行划分。
首先,对于数据集B1来说,由于其基尼系数已经为0,无需再进行计算;而B2数据集基尼系数为0.375,还可以进一步提取有效分类规则对其进行分类,以降低其基尼系数。此时我们又可以完全重复数据集A的划分过程,首先围绕数据集B2进行备选规则的提取,对于B2来说备选规则只有income <= 1.5一条,因此我们就以该规则划分数据集:
能够看出,最终划分出来的C1和C2基尼系数都是0,因此C的两个数据集整体基尼系数也是0,当然我们也无需进一步划分数据集,到此为止决策树也停止生长。

  • 决策树生长与迭代运算

此前我们说到,决策树的生长过程本质上也是在进行迭代运算,我们根据上一轮的到的结论(数据集划分情况)作为基础条件,来寻找子数据集的最佳分类规则,然后来进行数据集划分,以此往复。既然是迭代运算,那就必然需要讨论所有迭代运算都需要考虑的两个问题,其一是每一轮的迭代目标、其二是迭代收敛条件。

首先是每一轮迭代计算的目标,在梯度下降的计算过程中,每一轮迭代其实都是为了能够更大程度上降低损失函数值,在K-Means快速聚类中,每一轮迭代其实都是为了能够尽快降低组内误差平方和(SSE),而在决策树的建模过程中,每一轮迭代实际上是为了更快速的降低基尼系数,也就是希望这一轮划分出来的子数据集纯度尽可能高,从而说明该规则会对分类更加有效。因此如果我们可以将每一轮迭代过程中父类的基尼系数看成是损失函数值,树的迭代生长过程就是为了能够更快速的降低父类的基尼系数值。

其次就是迭代计算的收敛条件。对于此前我们所介绍的收敛条件其实也同样适用于决策树模型,例如当两轮迭代损失函数的差值小于某个值、或者直接限制最大迭代次数,其实都是可以用于决策树模型的。此时所谓两轮迭代的损失值小于某个值就停止迭代,其实就等价于如果进一步的划分数据集、但基尼系数的减少少于某个值就暂时不做划分;而最大迭代次数其实就相当于树模型的最高生长层数,在实际建模过程中,我们也可以通过约束树最多生长几层来作为迭代收敛条件。当然,对于树模型来说,还有可能出现类似上述备选规则都用完了的情况,此时也会停止迭代。

2.CART树的剪枝

和逻辑回归不同,决策树在不进行特征衍生时就是一个分类效力更强的模型,因此其本身就是一个更容易过拟合的模型。并且通过观察我们不难发现,决策树生长的层数越多就表示树模型越复杂,此时模型结构风险就越高、模型越容易过拟合。因此,很多时候如果我们不对树的生长进行任何约束,即如果设置的收敛条件较为严格(例如要求最终基尼系数全为0),并且最大迭代次数不进行限制,则很有可能容易过拟合。因此在决策树的建模流程当中,有非常重要的一个环节,就是需要讨论限制决策树模型过拟合倾向的方法。

当然,不同决策树算法的剪枝策略也各有不同,总的来说树模型的剪枝分为两种,其一在模型生长前就限制模型生长,这种方法也被称为预剪枝或者盆栽法;而另外一种方法则是先让树模型尽可能的生长,然后再进行剪枝,这种方法也被称为后剪枝或者修建法。从算法的原生原理来讲,目前主流的C4.5和CART树都采用的是后剪枝的方法,其中C4.5是通过计算叶节点的期望错误率(一种区间估计的方法)来进行剪枝,而CART树则是通过类似正则化的方法在损失函数(基尼系数计算函数)中加入结构复杂度的惩罚因子,来进行剪枝。

不过,无论采用何种方式来进行剪枝,最终的结果都是通过控制树的结构复杂度来抑制过拟合倾向,而树模型的结构复杂度其实完全可以用树的层数、每一层分叉的节点数来表示,即内部节点和叶节点的数量来表示,因此我们也完全可以不采用这些树模型原生原理的方式来进行剪枝,而是直接将这些决定树模型的复杂度的因素视作超参数,然后通过网格搜索的方式来直接确定泛化能力最强的树模型结构。当然这也是sklearn中进行决策树剪枝的一般策略。

在sklearn 0.22版本之前,甚至没有支持CART树实现原生原理剪枝方式的参数。

二、CART分类树的Scikit-Learn快速实现方法与评估器参数详解

1.CART分类树的sklearn快速实现

接下来我们尝试在Scikit-Learn中构建分类树模型。在sklearn中,回归树和分类树是两个不同的评估器,都在sklearn.tree模块内,我们可以通过如下方式进行导入:

from sklearn.tree import DecisionTreeClassifier,DecisionTreeRegressor

然后尝试围绕上述简单例子进行快速建模试验:

# 准备数据集
X = np.array([[1, 1], [2, 2], [2, 1], [1, 2], [1, 1], [1, 2], [1, 2], [2, 1]])
y = np.array([0, 0, 0, 1, 0, 1, 1, 0])
# 调用决策树评估器并进行训练
clf = DecisionTreeClassifier().fit(X, y)
clf.score(X, y)
#1.0

当然,对于树模型来说,我们不仅需要查看模型最终结果的评估指标,很多时候我们还希望能够观察到树模型分类过程的树状图,即类似于此前我们手动绘制的树状图。根据sklearn说明文档中的介绍,此处我们可以借助sklearn.tree模块下的plot_tree函数直接输入训练好的评估器即可进行绘制:

  • plot_tree绘制树状图
# 首先导入tree模块
from sklearn import tree
# 然后调用plot_tree函数进行绘制
plt.figure(figsize=(6, 2), dpi=150)
tree.plot_tree(clf)


由于plot_tree是sklearn中已经集成好的函数,因此调用过程非常简单,我们只需要输入训练好的分类树评估器即可。同时根据输出的结果可知,sklearn中分类树的建模过程和此前我们手动哦实现的过程是一样的,先根据第一个特征的不同取值进行数据集划分,然后在根据第二个特征的不同取值进行数据集划分,最终形成一个三个叶节点、两层的决策树模型。

当然,sklearn中的评估器使用过程基本一致,决策树模型评估器的简单使用也非常类似于逻辑回归评估器。此外,由于sklearn中优秀的参数默认值设置,使得很多时候我们直接使用其默认值就能完成不错的建模结果。接下来我们详细讨论决策树评估器中的相关参数,借此讨论关于sklearn中的决策树剪枝方法。

2.CART分类树评估器的参数详解

实际上DecisionTreeClassifier评估器参数众多,并且大多和决策树的模型结构相关:

DecisionTreeClassifier?
# Init signature:
# DecisionTreeClassifier(
#     *,
#     criterion='gini',
#     splitter='best',
#     max_depth=None,
#     min_samples_split=2,
#     min_samples_leaf=1,
#     min_weight_fraction_leaf=0.0,
#     max_features=None,
#     random_state=None,
#     max_leaf_nodes=None,
#     min_impurity_decrease=0.0,
#     min_impurity_split=None,
#     class_weight=None,     #树模型会经常被使用
#     presort='deprecated',
#     ccp_alpha=0.0,
# )


接下来围绕一些重点参数进行详细讲解:

  • criterion:不纯度衡量指标

首先,我们发现尽管sklearn的树模型在默认情况下是CART树,但同样支持使用信息熵来衡量不纯度。不过需要注意的是,哪怕我们在criterion参数中选择信息熵,实际树模型的建模过程也不是按照ID3或者C4.5的流程执行,此时的树模型只能算是一种混合模型。而关于到底应该选择哪个指标来衡量数据集的不纯度,其实大多数情况下选择哪个指标并不会实质影响树模型的结构,但相比信息熵,基尼系数复杂度更低、计算速度更快,一般情况下推荐使用基尼系数。而如果一定要寻找二者在使用上的不同,一般认为在有些情况下,基尼不纯度更倾向于在数据集中分割出多数类,而信息熵则更倾向于生成出更加平衡的树。

  • ccp_alpha:结构风险权重(类似于逻辑回归中正则化的参数)

ccp是复杂度剪枝(Cost-Complexity Pruning)的简称,这是一个在sklearn的0.22版本中才加入的参数,这也是唯一一个为实现CART原生原理中的剪枝过程所设置的参数。此处首先需要知道的是在sklearn中并不一定要通过该方法进行剪枝,因此该参数其实也并不是一个必选参数。其次,带有ccp项的剪枝也被称为最小复杂度剪枝,其原理是在决策树的损失函数上加上一个结构风险项,类似于正则化项在线性方程的损失函数中作用相同。

我们可以设T为某决策树,R(T)R(T)R(T)为决策树在训练集上整体不纯度,即代表模型的经验风险,令α∣T~∣\alpha|\widetilde{T}|α∣T∣表示模型结构风险,其中α\alphaα为参数,∣T~∣|\widetilde{T}|∣T∣为树的叶节点数量,则我们可以修改模型损失函数如下:
Rα(T)=R(T)+α∣T~∣R_\alpha(T) = R(T) + \alpha|\widetilde{T}| Rα​(T)=R(T)+α∣T∣

其中Rα(T)R_\alpha(T)Rα​(T)就是加入风险结构项后的损失函数,而α\alphaα则是风险结构项的系数。由此可知,α\alphaα取值越大、对模型的结构风险惩罚力度就越大、模型结构就越简单、过拟合就能够被更好的抑制,反之亦反。(逻辑回归alpha在经验风险上,lasso岭回归决策树alpha在结构风险上)

class_weight参数进行设置后,min_weight_fraction_leaf才能被使用
上面那个例子max_features就是所有features

  • 控制树结构的参数类

接下来就是关于控制树模型结构的相关参数,同时这也是最多的一类参数。这类参数可以进一步细分成两类,其一是限制模型整体结构,主要包括限制树深度的max_depth参数和限制叶节点数量的max_leaf_nodes参数。此外第二类就是限制树生长的参数,包括从节点样本数量限制树生长的参数,包括min_samples_split、min_samples_leaf两个参数,当然也有从损失值降低角度出发限制树生长的参数,包括min_impurity_split和min_impurity_decrease参数。通过这些参数的共同作用,可以从各角度有效限制树的生长。

注意,所谓树的最大深度,指的是树的最多生长几层,或者除了根节点外总共有几层,并不是树的总共的层数。

此处需要重点说明的是,对于树模型来说,叶节点太多、单独叶节点所包含的样本数量太少、内部节点再划分降低的基尼系数较少,都是可能是过拟合的表现,在建模时尤其需要注意。

并且需要知道的是,sklearn中在计算父节点和子节点的基尼系数(或信息熵)的差值时,会在计算结果的前面乘以一个父节点占根节点数据量比例的系数作为最终impurity_decrease的结果:
B2——>C1、C2(0.375*4/8)


而这会导致样本比较少的某节点,哪怕再划分时子节点纯度提升更高,但由于当前节点样本较少,因此impurity_decrease数值较低。这其实也是一种为了防止过拟合而采取的措施。

  • 控制迭代随机过程的参数类

最后,还有一类参数值得注意,那就是关于控制建模过程中随机性的一些参数,主要包含两个,其一是splitter参数,当该参数取值为random时其实是随机挑选分类规则对当前数据集进行划分,其二是max_features,该参数可以任意设置最多带入几个特征进行备选规律挖掘,只要该参数的设置不是带入全部特征进行建模,就相当于是给备选特征随机划个范围,也相当于是给树模型的训练增加了一定的随机性。当然,这两个参数的主要作用有两个方面,其一是可以提升模型训练速度,试想一下,如果我们只从个别特征中挑选最佳划分规则,或者随机生成一个划分规则、不进行比较就直接使用,其实都能够极大节省计算量,只不过这也是一种用精度换效率的方式,如此操作肯定会带来模型结果精度的下降;不过随机性其实也是一把双刃剑,在集成学习中,为了让各基础分类器“和而不同”,就必须让每个基分类器保证一定的随机性,而决策树就是最常作为基分类器参与到集成学习中的模型,因此树模型中的这些控制其随机性的参数,也会在集成学习中发挥作用。

更多关于决策树的使用及各类参数的使用及调参方法,我们将在后续内容中进行详细介绍。

Lesson 8.1Lesson 8.2 决策树的核心思想与建模流程CART分类树的建模流程与sklearn评估器参数详解相关推荐

  1. Lesson 8.3Lesson 8.4 ID3、C4.5决策树的建模流程CART回归树的建模流程与sklearn参数详解

    Lesson 8.3 ID3.C4.5决策树的建模流程 ID3和C4.5作为的经典决策树算法,尽管无法通过sklearn来进行建模,但其基本原理仍然值得讨论与学习.接下来我们详细介绍关于ID3和C4. ...

  2. Sklearn参数详解--决策树

    总第106篇 前言 先来简短的回顾一下决策树原理: 决策树学习的算法通常是一个递归地(根据某一准则,信息增益或基尼系数)选择最优切分点/特征,并根据该特征对训练数据集进行分割,使得对各个子数据集有一个 ...

  3. 决策树之CART分类树

    目录 一.基尼系数 (1)离散型属性 (2)连续型属性 二.cart算法的步骤 三.举个栗子 四.代码实现过程 总结: 一.基尼系数 基尼系数(Gini)是一种不等性的度量,经济学上用基尼系数度量收入 ...

  4. 一文详解java线程池 详解Java线程池的七个参数 详解池化技术 java如何选择核心线程数 详解Java线程池的拒绝策略

    目录 引言 线程池使用场景 加快请求响应(响应时间优先) 加快处理大任务(吞吐量优先) 特殊说明 线程池的池化技术 线程池的创建 手动创建 创建newFixedThreadPool线程池 创建newS ...

  5. gini系数 决策树_SKlearn中分类决策树的重要参数详解

    classsklearn.tree.DecisionTreeClassifier(criterion='gini', splitter='best', max_depth=None, min_samp ...

  6. 轨道阱 matlab,赛默飞四极杆-轨道阱液质联用仪核心参数详解

    1) 离子源 Orbitrap Fusion配置的是赛默飞新一代的Easy Max NG离子源,具有加热型HESI源和APCI源一体化设计,只需要更换喷针即可实现ESI源和APCI源的切换.Easy ...

  7. Java线程池七个参数详解:核心线程数、最大线程数、空闲线程存活时间、时间单位、工作队列、线程工厂、拒绝策略

    源码简介 ThreadPoolExecutor是JDK中的线程池实现,这个类实现了一个线程池需要的各个方法,它提供了任务提交.线程管理.监控等方法. 下面是ThreadPoolExecutor类的构造 ...

  8. java+决策树结构_机器学习——决策树,DecisionTreeClassifier参数详解,决策树可视化查看树结构...

    释放双眼,带上耳机,听听看~! 0.决策树 决策树是一种树型结构,其中每个内部节结点表示在一个属性上的测试,每一个分支代表一个测试输出,每个叶结点代表一种类别. 决策树学习是以实例为基础的归纳学习 决 ...

  9. 核心交换机相关技术参数详解

    核心交换机一般指三层交换机.三层交换机就是具有部分路由器功能的交换机,工作在OSI网络标准模型的第三层:网络层.三层交换机的最重要目的是加快大型局域网内部的数据交换,所具有的路由功能也是为这目的服务的 ...

最新文章

  1. 请求时的编码问题 Use body.encode(‘utf-8‘) if you want to send it encoded in UTF-8
  2. 1gitolite构建git服务器
  3. [ASP.NET Core 3框架揭秘] 跨平台开发体验: Docker
  4. java逆向_Java逆向基础之异常
  5. 【代码笔记】iOS-实现网络图片的异步加载和缓存
  6. oracle默认导出路径linux,传统路径导出 VS 直接路径导出(oracle exp direct=y)
  7. 1.8_heapq模块_优先队列
  8. html textarea粘贴事件,javascript在textarea中捕获粘贴事件
  9. qj71c24n通讯实例_通信模块QJ71C24N应用篇手册三菱QJ71C24N用户手册 - 广州凌控
  10. 未来教育软件计算机二级不能评分怎么办,为什么计算机二级软件打不开
  11. 微信小程序下拉刷新在真机上不回缩问题的解决方法
  12. 古都侠隐-评《侠隐》
  13. 虾皮铺货应该注意什么?
  14. Nitro League 出品 | P2E 系列 101——第三部分
  15. Android学习-使用WebView在app上显示网页
  16. The ShortKey for Gvim Vim
  17. 初识python导学案_高中数学 第三章 三角恒等变换 3_2_2 半角的正弦、余弦和正切学案 新人教b版必修4...
  18. 电影《悟空传》中的管理知识
  19. python 绘制科赫雪花
  20. 新鲜出炉的 MVVM 脚手架——KtArmor-MVVM

热门文章

  1. pythonwhile循环结束语句_Python while循环语句
  2. css 实现app图标样式_界面按钮样式丑?不可能!16款css实现炫酷按钮
  3. 计算机考研专业课资料,计算机考研专业课资料.doc
  4. 请写出sfr和sbit的语句格式_最新最全 Oracle ORA-01861: 文字与格式字符串不匹配
  5. java字符型转百分比_小数转换成百分比,转字符串
  6. Qt中的自定义模型类
  7. dnf强化卷代码_DNF:夏日套时装礼盒开服竟卖八千万金币,500万捡漏到黄金书
  8. 相对布局(RelativeLayout)的控件运用
  9. ubuntu 安装 talib
  10. python的openpyxl库如何读取特定列_Excelize 2.3.2 发布,Go 语言 Excel 文档基础库,2021 年首个更新...