以下文章来源于AI蜗牛车 ,作者Miracle8070
如果想从事数据挖掘或者机器学习的工作,掌握常用的机器学习算法是非常有必要的, 常见的机器学习算法:
为了详细的理解这些原理,曾经看过西瓜书,统计学习方法,机器学习实战等书,也听过一些机器学习的课程,但总感觉话语里比较深奥,读起来没有耐心,并且理论到处有,而实战最重要, 所以在这里想用最浅显易懂的语言写一个白话机器学习算法理论+实战系列。
个人认为,理解算法背后的idea和使用,要比看懂它的数学推导更加重要。idea会让你有一个直观的感受,从而明白算法的合理性,数学推导只是将这种合理性用更加严谨的语言表达出来而已,打个比方,一个梨很甜,用数学的语言可以表述为糖分含量90%,但只有亲自咬一口,你才能真正感觉到这个梨有多甜,也才能真正理解数学上的90%的糖分究竟是怎么样的。如果算法是个梨,本文的首要目的就是先带领大家咬一口。另外还有下面几个目的:
学习算法的过程,获得的不应该只有算法理论,还应该有乐趣和解决实际问题的能力!
今天是白话机器学习算法理论+实战的第一篇, 我们从决策树开始。
大纲如下:
OK, let's go !
决策树其实在我们生活中非常常见,只是我们缺少了一双发现的眼睛。不信?举个相亲的例子吧:
★一个女孩的妈妈给他介绍男朋友的时候,一般会有这样的一段对话: 女孩:长得帅不帅?妈:挺帅的 女孩:那有没有房子?妈妈:在老家有一个 女孩:收入高不高?妈妈:还不错,年薪百万 女孩:做什么工作?妈妈:IT男,互联网做数据挖掘的 女孩:好, 那我见见。 ”
这个女孩做决定的方式,就是基于决策树做的决定。又一脸茫然:我都把树给画好了(画工太差,凑合着看吧)
这就是棵决策树了,我们在生活中做选择的时候,往往背后都是基于这么一个结构,只不过我们都没有注意罢了。 当然工作原理也就非常简单了,只要有这么一棵树在那,当面对一个新的情况时,我们就不停的这样问,根据回答,就可以最后得出答案了。(比如,女孩可以再心目中根据择偶标准建立一个这样的树,当再有人给推荐对象时,就直接给他这棵树,让他自己比对,然后告诉他:决策树代表我的心。) 但是这个树究竟要怎么构造出来比较好呢, 这个有讲究,不能随便的构造,否则,有可能找不着对象。后面会重点说。好了, 引出了决策树之后,也得说点正经的知识了:
★
”
那么问题来了,究竟如何构造出一个决策树来呢?
上面的那种决策树是怎么构造出来的呢?这就是决策树的学习构成,即根据给定的训练数据集构建一个决策树模型,使它能够对实例进行正确的分 类。包括三个过程:特征选择、决策树生成和决策树剪枝 。这三个过程分别对应下面的问题:
★
”
好了,上面的话是不是又有点官方了啊,并且可能还出现了例如过拟合,泛化能力,剪枝等生词,不要着急,还是以找对象的那个例子来理解这三个问题就是下面这样:
★
”
下面就围绕着这三个问题展开了,究竟如何选择特征,又如何生成决策树,生成决策树之后,又如何剪枝。
为了让下面的知识不那么大理论话,我用一个例子来进行展开:假设我又下面打篮球的一个数据集,我应该怎么构造一棵树出来以决定我是否去打篮球呢?
就是我们应该怎么去选择特征作为分裂的节点?比如上面这个例子,特征有天气、湿度、温度、刮风,我先考虑哪一个特征进行分裂呢?
解决这个问题之前,得引入三个概念:纯度、信息熵和信息增益。不要晕,这么来理解吧,
★
”
p(i|t) 代表了节点 t 为分类 i 的概率,其中 log2 为取以 2 为底的对数。这里不是来介绍公式的,而是说存在一种度量,它能帮我们反映出来这个信息的不确定度。当不确定性越大时,它所包含的信息量也就越大,信息熵也就越高。只需要举个例子看看怎么算一组样本的信息熵:假设我有两个集合:
★
同样,集合 2 中,也是一共 6 次决策,其中类别 1 中“打篮球”的次数是 3,类别 2“不打篮球”的次数也是 3,那么信息熵为多少呢?我们可以计算得出:
从上面的计算结果中可以看出,信息熵越大,纯度越低。当集合中的所有样本均匀混合时,信息熵最大,纯度最低。这时候也最难做出决定。
”
那我们应该怎么办呢? 其实用特征对样本进行划分之后,会让混乱程度减小,就是熵变小,纯度变高,简单理解起来就是你分类了啊。
★一开始,比如3次打篮球,3次不打,没法做判断,但是如果用刮风这个特征来划分一下子,相当于有了一个条件,这时候,可能刮风的条件下,有2次不打篮球,1次打篮球, 这不就纯度提高了,有利于做出决策了,不去打。 ”
这时候我们解决了如何选择特征的一个问题,就是我给定一个条件,如果使我的样本纯度增加的最高,也就是更利于我做出选择,那么我就选这个作为分裂节点。 但是又有一个问题, 怎么衡量这个纯度提高了多少呢?
即划分之前的信息熵与给定一个特征a划分样本之后的信息熵之差,就是特征a带来的信息增益。好吧,公式可能说的有点晕,我们看看怎么计算就可以啦, 就拿上面的那个例子,我把图片放到这里来:
看看上面每个特征信息增益应该怎么算:
★
分别计算三个叶子节点的信息熵如下:
”
如果不好理解上面的过程,我还画了个图理解:
根据上面的这个思路,我们就可以分别计算湿度,温度,刮风的信息增益如下:
可以看出来,温度作为属性的信息增益最大,所以,先以这个为节点,划分样本。
然后我们要将上图中第一个叶节点,也就是 D1={1-,2-,3+,4+}进一步进行分裂,往下划分,计算其不同属性(天气、湿度、刮风)作为节点的信息增益,可以得到:
我们能看到湿度,或者天气为 D1 的节点都可以得到最大的信息增益,这里我们选取湿度作为节点的属性划分。同理,我们可以按照上面的计算步骤得到完整的决策树,结果如下:
这样,如果有小伙伴再叫我去打篮球的话,就会有下面的对话:
★我:今天的温度怎么样?小伙伴:今天的温度适中 我:今天的天气怎么样?小伙伴:今天的天气晴天 我:Go Play! ”
这样,打不打篮球,决策树来告诉你。
决策树的构造过程可以理解成为寻找纯净划分的过程。而衡量不纯度的指标有三种,而每一种不纯度对应一种决策树的生成算法:
后面第三大块,会详细介绍。
而关于剪枝,在这里不讲太多,因为有点难理解,这里只是简单的介绍一下,顺带着说一下之前提到的生词:过拟合,欠拟合,泛化能力等。想学习详细步骤的可以参考下面给出的笔记参考《统计学习方法之决策树》 《西瓜书之决策树》。
★决策树构造出来之后是不是就万事大吉了呢?也不尽然,我们可能还需要对决策树进行剪枝。剪枝就是给决策树瘦身,这一步想实现的目标就是,不需要太多的判断,同样可以得到不错的结果。之所以这么做,是为了防止“过拟合”(Overfitting)现象的发生。 过拟合这个概念你一定要理解,它指的就是模型的训练结果“太好了”,以至于在实际应用的过程中,会存在“死板”的情况,导致分类错误。 欠拟合,和过拟合就好比是下面这张图中的第一个和第三个情况一样,训练的结果“太好“,反而在实际应用过程中会导致分类错误。
造成过拟合的原因之一就是因为训练集中样本量较小。如果决策树选择的属性过多,构造出来的决策树一定能够“完美”地把训练集中的样本分类,但是这样就会把训练集中一些数据的特点当成所有数据的特点,但这个特点不一定是全部数据的特点,这就使得这个决策树在真实的数据分类中出现错误,也就是模型的“泛化能力”差。 泛化能力指的分类器是通过训练集抽象出来的分类能力,你也可以理解是举一反三的能力。如果我们太依赖于训练集的数据,那么得到的决策树容错率就会比较低,泛化能力差。因为训练集只是全部数据的抽样,并不能体现全部数据的特点。 ”
既然要对决策树进行剪枝,具体有哪些方法呢?一般来说,剪枝可以分为“预剪枝”(Pre-Pruning)和“后剪枝”(Post-Pruning)。
这个算法就不多介绍了,上面的决策树生成过程就是用的ID3算法,为了白话一点,这里不给出算法的步骤,具体的看统计学习方法里面的算法步骤(包括什么时候应该结束,这里没给出),这里只需要记住,ID3算法计算的是信息增益。 ID3 的算法规则相对简单,可解释性强。同样也存在缺陷,比如我们会发现 ID3 算法倾向于选择取值比较多的属性。
★假设我们把样本编号也作为一种属性的话,那么有多少样本,就会对应多少个分支,每一个分支只有一个实例,这时候每一个分支上Entropy(Di)=0,没有混乱度,显然这时候Gain(D,编号) = Entropy(D) - 0 。显然是最大的,那么按照ID3算法的话,会选择这个编号当做第一个分裂点。 我们知道,编号这个属性显然是对我们做选择来说没有意义的,出现过拟合不说,编号这个属性对分类任务作用根本就不大。所以这就是ID3算法存在的一个不足之处。 ”
这种缺陷不是每次都会发生,只是存在一定的概率。在大部分情况下,ID3 都能生成不错的决策树分类。针对可能发生的缺陷,后人提出了新的算法进行改进。
那么 C4.5 都在哪些方面改进了 ID3 呢?
★定义(信息增益比):特征A对训练数据集D的信息增益比gR(D,A)定义为其信息增益g(D,A)与训练数据集D在特征A的划分下数据集本身的一个混乱程度(熵)HA(D):
比如我下面这个编号的属性,
HA(D) = -1/7log1/7 * 7 = -log1/7 也就是说类别越多,混乱程度越大,这时候信息增益比也会减小 ”
当属性有很多值的时候,相当于被划分成了许多份,虽然信息增益变大了,但是对于 C4.5 来说,属性熵也会变大,所以整体的信息增益率并不大。上面那个例子,如果用C4.5算法的话,天气属性的信息增益比:
我们不考虑缺失的数值,可以得到温度 D={2-,3+,4+,5-,6+,7-}。温度 = 高:D1={2-,3+,4+} ;温度 = 中:D2={6+,7-};温度 = 低:D3={5-} 。这里 + 号代表打篮球,- 号代表不打篮球。比如 ID=2 时,决策是不打篮球,我们可以记录为 2-。 针对将属性选择为温度的信息增为:Gain(D′, 温度)=Ent(D′)-0.792=1.0-0.792=0.208 属性熵 =1.459, 信息增益率 Gain_ratio(D′, 温度)=0.208/1.459=0.1426。 D′的样本个数为 6,而 D 的样本个数为 7,所以所占权重比例为 6/7,所以 Gain(D′,温度) 所占权重比例为 6/7,所以:
★Gain_ratio(D, 温度)=6/7*0.1426=0.122。 这样即使在温度属性的数值有缺失的情况下,我们依然可以计算信息增益,并对属性进行选择。 ”
而对于上面的第二个问题,需要考虑权重。具体的参考《西瓜书之决策树》 这里只给出答案:
CART 算法,英文全称叫做 Classification And Regression Tree,中文叫做分类回归树。ID3 和 C4.5 算法可以生成二叉树或多叉树,而 CART 只支持二叉树。同时 CART 决策树比较特殊,既可以作分类树,又可以作回归树。
首先,得先知道什么是分类树,什么是回归树?
我用下面的训练数据举个例子,你能看到不同职业的人,他们的年龄不同,学习时间也不同。
我们通过上面已经知道决策树的核心就是寻找纯净的划分,因此引入了纯度的概念。 在属性选择上,我们是通过统计“不纯度”来做判断的,ID3 是基于信息增益做判断,C4.5 在 ID3 的基础上做了改进,提出了信息增益率的概念。 实际上 CART 分类树与 C4.5 算法类似,只是属性选择的指标采用的是基尼系数。
对,这里又出现了一个新的概念,不要晕,这个东西本身反应了样本的不确定度,当基尼系数越小的时候,说明样本之间的差异性小,不确定程度低。这一点和熵的定义类似。
★你可能在经济学中听过说基尼系数,它是用来衡量一个国家收入差距的常用指标。当基尼系数大于 0.4 的时候,说明财富差异悬殊。基尼系数在 0.2-0.4 之间说明分配合理,财富差距不大。 ”
分类的过程本身是一个不确定度降低的过程,即纯度的提升过程。所以 CART 算法在构造分类树的时候,会选择基尼系数最小的属性作为属性的划分。
下面给出基尼指数的计算公式:
这样,我们在选择特征的时候,就可以根据基尼指数最小来选择特征了。不用考虑HA(D),也就是不用考虑D在A的划分之下本身样本的一个混乱程度了。因为每次都分两个叉,不用担心叉太多影响结果了。
根据这个,回归打篮球的例子,假设属性 A 将节点 D 划分成了 D1 和 D2,如下图所示:
这时候,计算出D1和D2的基尼指数:
★GINI(D1)=1-1=0 GINI(D2)=1-(0.50.5+0.50.5)=0.5 在属性A的划分下,节点D的基尼指数为:
”
这样,就可以分别计算其他属性的基尼指数,然后进行划分了。具体的例子,看《统计学习方法之决策树》
上面已经知道了CART算法是基于基尼指数来做属性划分的。但是具体实现,我们可以使用写好的代码,调用sklearn包来实现就好了。
Python 的 sklearn 中,如果我们想要创建 CART 分类树,可以直接使用 DecisionTreeClassifier 这个类。创建这个类的时候,默认情况下 criterion 这个参数等于 gini,也就是按照基尼系数来选择属性划分,即默认采用的是 CART 分类树。
下面,我们来用 CART 分类树,给 iris 数据集构造一棵分类决策树。iris 这个数据集,我在 Python 可视化中讲到过,实际上在 sklearn 中也自带了这个数据集。基于 iris 数据集,构造 CART 分类树的代码如下:
# encoding=utf-8
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
# 准备数据集
iris=load_iris()
# 获取特征集和分类标识
features = iris.data
labels = iris.target
# 随机抽取33%的数据作为测试集,其余为训练集
train_features, test_features, train_labels, test_labels = train_test_split(features, labels, test_size=0.33, random_state=0)
# 创建CART分类树
clf = DecisionTreeClassifier(criterion='gini')
# 拟合构造CART分类树
clf = clf.fit(train_features, train_labels)
# 用CART分类树做预测
test_predict = clf.predict(test_features)
# 预测结果与测试集结果作比对
score = accuracy_score(test_labels, test_predict)
print("CART分类树准确率 %.4lf" % score)
运行结果:
CART分类树准确率 0.9600
如果我们把决策树画出来,可以得到下面的图示:
涉及到代码了,简单说一下上面的代码:
★首先 train_test_split 可以帮助我们把数据集抽取一部分作为测试集,这样我们就可以得到训练集和测试集。 使用 clf = DecisionTreeClassifier(criterion=‘gini’) 初始化一棵 CART 分类树。这样你就可以对 CART 分类树进行训练。 使用 clf.fit(train_features, train_labels) 函数,将训练集的特征值和分类标识作为参数进行拟合,得到 CART 分类树。 使用 clf.predict(test_features) 函数进行预测,传入测试集的特征值,可以得到测试结果 test_predict。 最后使用 accuracy_score(test_labels, test_predict) 函数,传入测试集的预测结果与实际的结果作为参数,得到准确率 score。 ”
我们能看到 sklearn 帮我们做了 CART 分类树的使用封装,使用起来还是很方便的。
CART 回归树划分数据集的过程和分类树的过程是一样的,只是回归树得到的预测结果是连续值,而且评判“不纯度”的指标不同。
在 CART 分类树中采用的是基尼系数作为标准,那么在 CART 回归树中,如何评价“不纯度”呢?实际上我们要根据样本的混乱程度,也就是样本的离散程度来评价“不纯度”。
样本的离散程度具体的计算方式是,先计算所有样本的均值,然后计算每个样本值到均值的差值。我们假设 x 为样本的个体,均值为 u。为了统计样本的离散程度,我们可以取差值的绝对值,或者方差。
其中差值的绝对值为样本值减去样本均值的绝对值:
方差为每个样本值减去样本均值的平方和除以样本个数:
所以这两种节点划分的标准,分别对应着两种目标函数最优化的标准,即用最小绝对偏差(LAD),或者使用最小二乘偏差(LSD)。这两种方式都可以让我们找到节点划分的方法,通常使用最小二乘偏差的情况更常见一些。
我们可以通过一个例子来看下如何创建一棵 CART 回归树来做预测。如何使用 CART 回归树做预测。
这里我们使用到 sklearn 自带的波士顿房价数据集,该数据集给出了影响房价的一些指标,比如犯罪率,房产税等,最后给出了房价。根据这些指标,我们使用 CART 回归树对波士顿房价进行预测,代码如下:
# encoding=utf-8
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_boston
from sklearn.metrics import r2_score,mean_absolute_error,mean_squared_error
from sklearn.tree import DecisionTreeRegressor
# 准备数据集
boston=load_boston()
# 探索数据
print(boston.feature_names)
# 获取特征集和房价
features = boston.data
prices = boston.target
# 随机抽取33%的数据作为测试集,其余为训练集
train_features, test_features, train_price, test_price = train_test_split(features, prices, test_size=0.33)
# 创建CART回归树
dtr=DecisionTreeRegressor()
# 拟合构造CART回归树
dtr.fit(train_features, train_price)
# 预测测试集中的房价
predict_price = dtr.predict(test_features)
# 测试集的结果评价
print('回归树二乘偏差均值:', mean_squared_error(test_price, predict_price))
print('回归树绝对值偏差均值:', mean_absolute_error(test_price, predict_price))
运行结果(每次运行结果可能会有不同):
['CRIM' 'ZN' 'INDUS' 'CHAS' 'NOX' 'RM' 'AGE' 'DIS' 'RAD' 'TAX' 'PTRATIO' 'B' 'LSTAT']
回归树二乘偏差均值: 23.80784431137724
回归树绝对值偏差均值: 3.040119760479042
★我们来看下这个例子,首先加载了波士顿房价数据集,得到特征集和房价。 然后通过 train_test_split 帮助我们把数据集抽取一部分作为测试集,其余作为训练集。 使用 dtr=DecisionTreeRegressor() 初始化一棵 CART 回归树。 使用 dtr.fit(train_features, train_price) 函数,将训练集的特征值和结果作为参数进行拟合,得到 CART 回归树。 使用 dtr.predict(test_features) 函数进行预测,传入测试集的特征值,可以得到预测结果 predict_price。 最后我们可以求得这棵回归树的二乘偏差均值,以及绝对值偏差均值。 ”
我们能看到 CART 回归树的使用和分类树类似,只是最后求得的预测值是个连续值。
构造决策树时, 需要解决的第一个问题就是,当前数据集上的哪个特征在划分数据集时起到决定 作用, 需要找到这样的特征,把原始数据集划分为几个数据子集, 然后再在剩余的特征里面进 一步划分,依次进行下去, 所以分下面几个步骤:
def calcShannonEnt(dataset):
numEntries = len(dataset) # 样本数量
labelCounts = {}
for featVec in dataset:
currentLabel = featVec[-1] # 遍历每个样本,获取标签
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key]) / numEntries
shannonEnt -= prob * math.log(prob, 2)
return shannonEnt
# 按照给定特征划分数据集
def splitDataSet(dataset, axis, value):
retDataSet = []
for featVec in dataset:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 # 获取总的特征数
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0
bestFeature = -1
# 下面开始变量所有特征, 对于每个特征,要遍历所有样本, 根据遍历的样本划分开数据集,然后计算新的香农熵
for i in range(numFeatures):
featList = [example[i] for example in dataSet] # 获取遍历特征的这一列数据,接下来进行划分
uniqueVals = set(featList) # 从列表中创建集合是Python语言得到唯一元素值的最快方法
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet) / float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
if (infoGain > bestInfoGain):
bastInfoGain = infoGain
bestFeature = i
return bestFeature
# 返回最多的那个标签
def majorityCnt(classList):
classCount = {}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.values(), reverse=True)
return sortedClassCount[0]
# 递归构建决策树
def createTree(dataSet, labels):
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len(classList): # 类别完全相同则停止划分 这种 (1,2,'yes') (3,4,'yes')
return classList[0]
if len(dataSet[0]) == 1: # 遍历所有特征时,(1, 'yes') (2, 'no')这种形式 返回出现次数最多的类别
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) # 选择最好的数据集划分方式,返回的是最好特征的下标
bestFeatLabel = labels[bestFeat] # 获取到那个最好的特征
myTree = {bestFeatLabel:{}} # 创建一个myTree,保存创建的树的信息
del(labels[bestFeat]) # 从标签中药删除这个选出的最好的特征,下一轮就不用这个特征了
featValues = [example[bestFeat] for example in dataSet] # 获取到选择的最好的特征的所有取值
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels) # 这是个字典嵌套字典的形式
return myTree
这就是ID3算法的底层代码。
可以参考隐形眼镜分类的这个项目:https://github.com/zhongqiangwu960812/MLProjects/tree/master/MachineLearningInAction/DecisionTree
不知不觉,篇幅有点超过想像,所以这里没法详细的介绍Sklearn中的决策树对Titanic尼克号人的生存预测,那么就先简单的介绍一下Sklearn中决策树怎么用,然后实战项目参考后面的链接吧。
首先,我们需要掌握 sklearn 中自带的决策树分类器 DecisionTreeClassifier,方法如下:
clf = DecisionTreeClassifier(criterion='entropy')
到目前为止,sklearn 中只实现了 ID3 与 CART 决策树,所以我们暂时只能使用这两种决策树,在构造 DecisionTreeClassifier 类时,其中有一个参数是 criterion,意为标准。它决定了构造的分类树是采用 ID3 分类树,还是 CART 分类树,对应的取值分别是 entropy 或者 gini:
我们通过设置 criterion='entropy’可以创建一个 ID3 决策树分类器,然后打印下 clf,看下决策树在 sklearn 中是个什么东西?
DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=None,
max_features=None, max_leaf_nodes=None,
min_impurity_decrease=0.0, min_impurity_split=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, presort=False, random_state=None,
splitter='best')
这里我们看到了很多参数,除了设置 criterion 采用不同的决策树算法外,一般建议使用默认的参数,默认参数不会限制决策树的最大深度,不限制叶子节点数,认为所有分类的权重都相等等。当然你也可以调整这些参数,来创建不同的决策树模型。
这些参数代表的含义如下:
在构造决策树分类器后,我们可以使用 fit 方法让分类器进行拟合,使用 predict 方法对新数据进行预测,得到预测的分类结果,也可以使用 score 方法得到分类器的准确率。
下面这个表格是 fit 方法、predict 方法和 score 方法的作用。
这个具体的看后面的参考笔记吧,有点多。
花费了一天的时间,才整理完了白话机器学习算法第一篇决策树,虽然说白话,但是难免会有代码和公式,但这些都是必须要知道的,也是基础。后面的项目实战应该好好练练,因为光有理论,可能很快就会忘记了,所以得多练。
参考: