决策树算法
决策树(decision tree)是一类最常见、最基础的机器学习算法。决策树基于特征对数据实例按照条件不断进行划分,最终达到分类或回归的目的。
决策树模型的核心概念包括特征选择方法、决策树构造过程和决策树剪枝。常见的特征选择方法包括信息增益、信息增益比和基尼指数(Gini index),对应的三种常见的决策树算法为ID3、C4.5和CART。作为一种基础的分类和回归方法,决策树可以有如下两种理解方式。一种是我们可以将决策树看作是一组if-then规则的集合,另一种则是给定特征条件下类的条件概率分布。
一、经典实例
“今天是否要打高尔夫?”从机器学习的角度来看,这是一个典型的二分类问题,答案要么肯定要么否定。在给定一组关于过去记录打高尔夫情况数据的条件下,决策树可以通过一个树形结构来进行决策。假设影响是否打高尔夫的决策因素包括天气、温度、湿度和是否有风这四个特征。
在对“今天是否要打高尔夫?”这样的问题进行决策时,我们需要进行一系列子决策,如下图所示,我们先判断“天气”如何,如果是“晴”,再看“温度”,如果“温度”是“冷”,再看“是否有风”,如果没有风,再看“湿度”如何,在“湿度”为“正常”的情况下,我们做出今天要打高尔夫的决策。一组影响因素与是否打高尔夫的数据如下表所示。
天气 | 温度 | 湿度 | 是否有风 | 是否打高尔夫 |
---|---|---|---|---|
晴 | 热 | 高 | 否 | 否 |
晴 | 热 | 高 | 是 | 否 |
阴 | 热 | 高 | 否 | 是 |
雨 | 适宜 | 高 | 否 | 是 |
雨 | 冷 | 正常 | 否 | 是 |
雨 | 冷 | 正常 | 是 | 否 |
阴 | 冷 | 正常 | 是 | 是 |
晴 | 适宜 | 高 | 否 | 否 |
晴 | 冷 | 正常 | 否 | 是 |
雨 | 适宜 | 正常 | 否 | 是 |
晴 | 适宜 | 正常 | 是 | 是 |
阴 | 适宜 | 高 | 是 | 是 |
阴 | 热 | 正常 | 否 | 是 |
雨 | 适宜 | 高 | 是 | 否 |
我们希望基于所给数据集训练一颗判断是否打高尔夫的决策树,用来对未来是否打高尔夫进行决策。
二、决策树
决策树通过树形结构来对数据样本进行分类。一棵完整的结构树由结点和有向边构成,其中内部结点表示特征,叶子结点表示类别,决策树从根结点开始,选取数据中某一特征,根据特征取值对实例进行分配,通过不断地选取特征进行实例分配,决策树可以达到对所有实例进行分类的目的。
我们可以基于两个视角来理解决策树模型。
可以将决策树看作一组if-then规则的集合,将决策树的根结点到叶子结点的每一条路径都构建一条规则,路径中的内部结点特征代表规则条件,而叶子结点表示这条规则的结论。一棵决策树所有的if-then规则都互斥且完备。if-then规则本质上就是一组分类规则,决策树学习的目标就是基于数据归纳出这样的一组规则。
也可以从条件概率分布的角度来理解决策树。假设将特征空间划分为互不相交的区域,且每个区域定义的类的概率分布就构成了一个条件概率分布。决策树所表示的条件概率分布是由各个区域给定类的条件概率分布组成的。
完整的决策树模型包括特征选择、决策树构建和决策树剪枝三个大的方面。其中特征选择和决策树构建对应着决策树的生成算法,决策树剪枝对应着决策树剪枝算法。
三、特征选择
信息增益
为了能够构建一棵分类性能良好的决策树,我们需要从训练数据集中不断选取具有分类能力的特征。
决策树的特征选择就是从数据集中选择具备较强分类能力的特征来对数据集进行划分。那么什么样的特征才是具备较强分类能力的特征呢?或者说,我们应该按照什么标准来选取最优特征?
在决策树模型中,有三种方式来选取最优特征,包括信息增益、信息增益比和基尼指数。假设当前样本数据集D中第k个类所占比例为,那么该样本数据集的熵可定义为:
信息增益(information gain)则定义为由于得到特征X的信息而使得类Y的信息不确定性减少的程度,即信息增益是一种描述目标类别确定性增加的量,特征的信息增益越大,目标类的确定性越强。
假设训练数据集D的经验熵为E(D),给定特征A的条件下D的经验条件熵为E(D|A),那么信息增益可定义为经验熵E(D)与经验条件熵E(D|A)之差:
在经典的决策树算法中,ID3 算法是基于信息增益进行特征选择的。
以高尔夫数据集为例,给出信息增益的具体计算方式。
是否打高尔夫 | |
---|---|
是 | 否 |
9 | 5 |
计算数据集的经验熵E(D):
天气 | 打高尔夫 | 不打高尔夫 | 汇总 |
---|---|---|---|
晴 | 2 | 3 | 5 |
阴 | 4 | 0 | 4 |
阴 | 3 | 2 | 5 |
计算天气特征对于数据集的经验条件熵E(D|A):
计算天气特征对于数据集的信息增益g :
信息增益比
信息增益是一个非常好的特征选择方法,但也存在一些问题:当某个特性分类取值较多时,该特征的信息增益计算结果就会较大,比如给高尔夫数据集加一个“编号”特征,从第一条记录到最后一条记录,总共有14个不同的取值,该特征将会产生14个决策树分支,每个分支仅包含一个样本,每个节点的信息纯度都比较高,最后计算得到的信息增益也将远大于其他特征。但是,根据实际情况,我们知道“编号”这样的特征很难起到分类作用,这样构建出来的决策树是无效的。所以基于信息增益选择特征时,会偏向取值较大的特征。
使用信息增益比可以对上述问题进行校正。特征A对数据集D的信息增益比可以定义为其信息增益g(D,A)与数据集D关于特征A取值的熵的比值:
基尼指数
除信息增益和信息增益比外,基尼指数也是一种较好的特征选择方法。
基尼指数是针对概率分布而言的,假设样本有K个类,样本属于第k类的概率为,则该样本类别概率分布的基尼指数可定义为:
对于给定训练数据集D,C_k是属于第k类样本的集合,则该训练数据集的基尼指数可定义为:
如果训练数据集D根据特征A某一取值a划分为和两个部分,那么在特征A这个条件下,训练数据集D的基尼指数可定义为:
对于分类任务而言,我们希望训练数据集的不确定性越小越好,即Gini(D,A)越小,对应的特征对训练样本的分类能力越强。在经典的决策树算法中,CART算法是基于信息增益比进行特征选择的。
同样以高尔夫数据集为例,我们来计算各特征的基尼指数。
天气特征:
可以看到,最小,所以天气取值为阴可以选作天气特征的最优分点。同样,剩余三个特征的基尼指数计算结果如下。
湿度特征:
温度特征:
是否有风特征:
湿度和是否有风只有一个分裂点,所以它们是最优划分点,在全部4个特征中最小,所以选择温度特征作为最优特征,温度=适宜为其最优划分点。
四、决策树模型
基于信息增益、信息增益比和基尼指数三种特征选择方法,分别有ID3、C4.5和CART三种经典的决策树算法。这三种算法在构造分类决策树时方法基本一致,都是通过特征选择方法递归地选择最优特征进行构造。其中ID3和C4.5算法只有决策树的生成,不包括决策树剪枝部分、所以这两种算法有时候容易过拟合。CART算法除用于分类外,还可用于回归,并且该算法是包括决策树剪枝的。
ID3
ID3算法的全称为Iterative Dichotomiser 3,即3代迭代二叉树。其核心就是基于信息增益递归地选择最优特征构造决策树。
具体方法如下:首先预设一个决策树根结点,然后对所有特征计算信息增益,选择一个信息增益最大的特征作为最佳特征,根据该特征的不同取值建立子结点,然后对每个子结点递归地调用上述方法,直到信息增益很小或者没有特征可选时,即可构建最终的ID3决策树。
给定训练数据集D、特征集合A以及信息增益阈值,ID3算法的流程可以作如下描述。
- 如果D中所有实例属于同一类别,那么所构建的决策树T为单结点树,并且类即为该结点的类的标记。
- 如果T不是单结点树,则计算特征集合A中各特征对D的信息增益,选择信息增益最大的特征。
- 如果的信息增益小于阈值ε,则将T视为单结点树,并将D中所属数量最多的类作为该结点的类的标记并返回T。
- 否则,可对的每一特征取值,按照将D划分为若干非空子集,以中所属数量最多的类作为标记并构建子结点,由结点和子结点构成树T并返回。
- 对第i个子结点,以为特征集,递归地调用(1)~(4)步,即可得到决策树子树并返回。
高尔夫数据集
humility,outlook,play,temp,windy
high,sunny,no,hot,false
high,sunny,no,hot,true
high,overcast,yes,hot,false
high,rainy,yes,mild,false
normal,rainy,yes,cool,false
normal,rainy,no,cool,true
normal,overcast,yes,cool,true
high,sunny,no,mild,false
normal,sunny,yes,cool,false
normal,rainy,yes,mild,false
normal,sunny,yes,mild,true
high,overcast,yes,mild,true
normal,overcast,yes,hot,false
high,rainy,no,mild,true
读取数据集
df = pd.read_csv('./example_data.csv', dtype={'windy': 'str'})
定义信息熵计算函数
def entropy(ele):
probs = [ele.count(i)/len(ele) for i in set(ele)]
entropy = -sum([prob*log(prob, 2) for prob in probs])
return entropy
定义数据集划分函数
def split_dataframe(data, col):
unique_values = data[col].unique()
result_dict = {elem : pd.DataFrame for elem in unique_values}
for key in result_dict.keys():
result_dict[key] = data[:][data[col] == key]
return result_dict
根据信息熵计算函数和数据集划分函数,定义ID3算法的核心步骤——选择最优特征。
def choose_best_col(df, label):
# 计算训练标签的信息熵
entropy_D = entropy(df[label].tolist())
# 特征集
cols = [col for col in df.columns if col not in [label]]
# 初始化最大信息增益值、最优特征和划分后的数据集
max_value, best_col = -999, None
max_splited = None
# 遍历特征并根据特征取值进行划分
for col in cols:
# 根据当前特征划分后的数据集
splited_set = split_dataframe(df, col)
# 初始化经验条件熵
entropy_DA = 0
# 对划分后的数据集遍历计算
for subset_col, subset in splited_set.items():
# 计算划分后的数据子集标签信息熵
entropy_Di = entropy(subset[label].tolist())
# 计算当前特征的经验条件熵
entropy_DA += len(subset)/len(df) * entropy_Di
# 计算当前特征的信息增益
info_gain = entropy_D - entropy_DA
# 获取最大信息增益,并保存对应的特征和划分结果
if info_gain > max_value:
max_value, best_col = info_gain, col
max_splited = splited_set
return max_value, best_col, max_splited
上述代码是ID3算法的核心步骤,对应算法流程的第(4)步。基于以上准备工作,可以封装一个包括构建ID3决策树的基本方法的算法类,完整过程如下:
class ID3Tree:
# 定义决策树结点类
class Node:
# 定义树结点
def __init__(self, name):
self.name = name
self.connections = {}
# 定义树连接
def connect(self, label, node):
self.connections[label] = node
# 定义全局变量,包括数据集、特征集、标签和根结点
def __init__(self, data, label):
self.columns = data.columns
self.data = data
self.label = label
self.root = self.Node("Root")
def print_tree(self, node, tabs):
print(tabs + node.name)
for connection, child_node in node.connections.items():
print(tabs + "\t" + "(" + connection + ")")
self.print_tree(child_node, tabs + "\t\t")
# 构建树的调用
def construct_tree(self):
self.construct(self.root, "", self.data, self.columns)
# 决策树的构建方法
def construct(self, parent_node, parent_connection_label, input_data, columns):
# 选择最优特征
max_value, best_col, max_splited = choose_best_col(input_data[columns], self.label)
# 如果不是最优特征,则构建单结点树
if not best_col:
node = self.Node(input_data[self.label].iloc[0])
parent_node.connect(parent_connection_label, node)
return
# 根据最优特征以及子结点构建树
node = self.Node(best_col)
parent_node.connect(parent_connection_label, node)
# 以A-Ag为新的特征集
new_columns = [col for col in columns if col != best_col]
# 递归地构造决策树
for splited_value, splited_data in max_splited.items():
self.construct(node, splited_value, splited_data, new_columns)
基于高尔夫数据集的决策树
tree1 = ID3Tree(df, 'play')
tree1.construct_tree()
tree1.print_tree(tree1.root, "")
Root
()
outlook
(sunny)
humility
(high)
temp
(hot)
windy
(false)
no
(true)
no
(mild)
windy
(false)
no
......
C4.5
C4.5算法整体上与ID3算法较为类似,不同之处在于C4.5在构造决策树时使用信息增益比作为特征选择方法。所以C4.5算法实例和代码实现就不再给出。
CART决策树
CART算法的全称为分类与回归树(classification and regression tree),顾名思义,CART算法既可以用于分类,也可以用于回归,这是CART算法与ID3和C4.5的主要区别之一。
CART算法的特征选择方法基于基尼指数。
CART可以理解为在给定随机变量X的条件下输出随机变量Y的条件概率分布的学习算法。
CART生成的决策树都是二叉决策树,内部结点取值为“是”和“否”,这种结点划分方法等价于递归地二分每个特征,将特征空间划分为有限个单元,并在这些单元上确定预测的概率分布,即前述预测条件概率分布。
CART分类树
CART分类树生成算法基于最小基尼指数递归地选择最佳特征,并确定最优特征的最优二值切分点:
- 给定训练数据集D和特征集A,对于每个特征a及其所有取值,根据将数据集划分为和两个部分,基于公式计算时的基尼指数。
- 取基尼指数最小的特征及其对应的切分点作为最优特征和最佳切分点,根据最优特征和最佳切分点将当前结点划分为两个子结点,将训练数据集根据特征分配到两个子结点中。
- 对两个子结点递归地调用(1)和(2),直至满足停止条件。
- 最后即可生成CART分类决策树。
CART回归树
假设训练输入X和输出Y,给定训练数据集,CART回归树的构建方法如下。回归树对应特征空间的一个划分以及在该划分单元上的输出值。假设特征空间有M个划分单元,且每个划分单元都有一个输出权重,那么回归树模型可以表示为:
回归树模型训练的目的是最小化平方损失,以期求得最佳输出权重:
CART分类树通过计算基尼指数确定最佳特征和最优切分点,那么回归树如何确定特征最优切分点?假设随机选取第j个特征及其对应的某个取值s,将其作为划分特征和切分点,同时定义两个区域:
然后求解:
求解上式即可得到输入特征j和最优切分点s。按照上述平方误差最小准则可以求得全局最优切分特征和取值,并据此将特征空间划分为两个子区域,对每个子区域重复前述划分过程,直至满足停止条件,即可生成一棵回归树。
完整的最小二乘回归树生成算法如下:(来自统计学习方法)
最小二乘回归树拟合数据如下图所示。可以看到,回归树的树深度越大的情况下,模型复杂度越高,对数据的拟合程度就越好,但相应的泛化能力就得不到保证。
CART算法实现
无论是分类树还是回归树,二者对于树结点和基础二叉树的实现方式一致,主要差异在于特征选择方法和叶子结点取值预测方法。所以实现一个CART算法,基本策略是从底层逐渐往上层进行搭建,首先定义树结点,然后定义基础的二叉决策树,最后分别结合分类树和回归树的特征给出算法实现。另外,还需要定义一些辅助函数,像二叉树的结点特征分裂函数、基尼指数计算函数等。
(1)二叉决策树
定义决策树结点
class TreeNode():
def __init__(self, feature_i=None, threshold=None,
leaf_value=None, left_branch=None, right_branch=None):
# 特征索引
self.feature_i = feature_i
# 特征划分阈值
self.threshold = threshold
# 叶子节点取值
self.leaf_value = leaf_value
# 左子树
self.left_branch = left_branch
# 右子树
self.right_branch = right_branch
定义二叉特征分裂函数
def feature_split(X, feature_i, threshold):
split_func = None
if isinstance(threshold, int) or isinstance(threshold, float):
split_func = lambda sample: sample[feature_i] >= threshold
else:
split_func = lambda sample: sample[feature_i] == threshold
X_left = np.array([sample for sample in X if split_func(sample)])
X_right = np.array([sample for sample in X if not split_func(sample)])
return np.array([X_left, X_right])
定义基尼指数计算函数
def calculate_gini(y):
# 将数组转化为列表
y = y.tolist()
probs = [y.count(i)/len(y) for i in np.unique(y)]
gini = sum([p*(1-p) for p in probs])
return gini
定义二叉决策树
class BinaryDecisionTree(object):
### 决策树初始参数
def __init__(self, min_samples_split=2, min_gini_impurity=999,
max_depth=float("inf"), loss=None):
# 根结点
self.root = None
# 节点最小分裂样本数
self.min_samples_split = min_samples_split
# 节点初始化基尼不纯度
self.mini_gini_impurity = min_gini_impurity
# 树最大深度
self.max_depth = max_depth
# 基尼不纯度计算函数
self.gini_impurity_calculation = None
# 叶子节点值预测函数
self._leaf_value_calculation = None
# 损失函数
self.loss = loss
### 决策树拟合函数
def fit(self, X, y, loss=None):
# 递归构建决策树
self.root = self._build_tree(X, y)
self.loss=None
### 决策树构建函数
def _build_tree(self, X, y, current_depth=0):
# 初始化最小基尼不纯度
init_gini_impurity = 999
# 初始化最佳特征索引和阈值
best_criteria = None
# 初始化数据子集
best_sets = None
# 合并输入和标签
Xy = np.concatenate((X, y), axis=1)
# 获取样本数和特征数
n_samples, n_features = X.shape
# 设定决策树构建条件
# 训练样本数量大于节点最小分裂样本数且当前树深度小于最大深度
if n_samples >= self.min_samples_split and current_depth <= self.max_depth:
# 遍历计算每个特征的基尼不纯度
for feature_i in range(n_features):
# 获取第i特征的所有取值
feature_values = np.expand_dims(X[:, feature_i], axis=1)
# 获取第i个特征的唯一取值
unique_values = np.unique(feature_values)
# 遍历取值并寻找最佳特征分裂阈值
for threshold in unique_values:
# 特征节点二叉分裂
Xy1, Xy2 = feature_split(Xy, feature_i, threshold)
# 如果分裂后的子集大小都不为0
if len(Xy1) > 0 and len(Xy2) > 0:
# 获取两个子集的标签值
y1 = Xy1[:, n_features:]
y2 = Xy2[:, n_features:]
# 计算基尼不纯度
impurity = self.impurity_calculation(y, y1, y2)
# 获取最小基尼不纯度
# 最佳特征索引和分裂阈值
if impurity < init_gini_impurity:
init_gini_impurity = impurity
best_criteria = {"feature_i": feature_i, "threshold": threshold}
best_sets = {
"leftX": Xy1[:, :n_features],
"lefty": Xy1[:, n_features:],
"rightX": Xy2[:, :n_features],
"righty": Xy2[:, n_features:]
}
# 如果计算的最小不纯度小于设定的最小不纯度
if init_gini_impurity < self.mini_gini_impurity:
# 分别构建左右子树
left_branch = self._build_tree(best_sets["leftX"], best_sets["lefty"], current_depth + 1)
right_branch = self._build_tree(best_sets["rightX"], best_sets["righty"], current_depth + 1)
return TreeNode(feature_i=best_criteria["feature_i"], threshold=best_criteria[
"threshold"], left_branch=left_branch, right_branch=right_branch)
# 计算叶子计算取值
leaf_value = self._leaf_value_calculation(y)
return TreeNode(leaf_value=leaf_value)
### 定义二叉树值预测函数
def predict_value(self, x, tree=None):
if tree is None:
tree = self.root
# 如果叶子节点已有值,则直接返回已有值
if tree.leaf_value is not None:
return tree.leaf_value
# 选择特征并获取特征值
feature_value = x[tree.feature_i]
# 判断落入左子树还是右子树
branch = tree.right_branch
if isinstance(feature_value, int) or isinstance(feature_value, float):
if feature_value >= tree.threshold:
branch = tree.left_branch
elif feature_value == tree.threshold:
branch = tree.left_branch
# 测试子集
return self.predict_value(x, branch)
### 数据集预测函数
def predict(self, X):
y_pred = [self.predict_value(sample) for sample in X]
return y_pred
(2)分类树
下面基于上一步定义的二叉决策树类BinaryDecisionTree,根据分类树的特征,定义一个继承BinaryDecisionTree的分类树ClassificationTree
class ClassificationTree(BinaryDecisionTree):
### 定义基尼不纯度计算过程
def _calculate_gini_impurity(self, y, y1, y2):
p = len(y1) / len(y)
gini = calculate_gini(y)
gini_impurity = p * calculate_gini(y1) + (1-p) * calculate_gini(y2)
return gini_impurity
### 多数投票
def _majority_vote(self, y):
most_common = None
max_count = 0
for label in np.unique(y):
# 统计多数
count = len(y[y == label])
if count > max_count:
most_common = label
max_count = count
return most_common
# 分类树拟合
def fit(self, X, y):
self.impurity_calculation = self._calculate_gini_impurity
self._leaf_value_calculation = self._majority_vote
super(ClassificationTree, self).fit(X, y)
分类树测试
from sklearn import datasets
data = datasets.load_iris()
X, y = data.data, data.target
y = y.reshape(-1,1)
X_train, X_test, y_train, y_test = train_test_split(X, y.reshape(-1,1), test_size=0.3)
clf = ClassificationTree()
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(accuracy_score(y_test, y_pred))
0.9777777777777777
(3)回归树
class RegressionTree(BinaryDecisionTree):
def _calculate_variance_reduction(self, y, y1, y2):
var_tot = np.var(y, axis=0)
var_y1 = np.var(y1, axis=0)
var_y2 = np.var(y2, axis=0)
frac_1 = len(y1) / len(y)
frac_2 = len(y2) / len(y)
# 计算方差减少量
variance_reduction = var_tot - (frac_1 * var_y1 + frac_2 * var_y2)
return sum(variance_reduction)
# 节点值取平均
def _mean_of_y(self, y):
value = np.mean(y, axis=0)
return value if len(value) > 1 else value[0]
def fit(self, X, y):
self.impurity_calculation = self._calculate_variance_reduction
self._leaf_value_calculation = self._mean_of_y
super(RegressionTree, self).fit(X, y)
回归树测试
from sklearn.datasets import load_boston
X, y = load_boston(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
model = RegressionTree()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
print("Mean Squared Error:", mse)
Mean Squared Error: 134.4803289473684
五、决策树剪枝
一个完整的决策树算法,除决策树生成算法外,还包括决策树剪枝算法。决策树生成算法递归地产生决策树,生成的决策树大而全,但很容易导致过拟合现象。
决策树剪枝(pruning)则是对已生成的决策树进行简化的过程,通过对已生成的决策树剪掉一些子树或者叶子结点,并将其根结点或父结点作为新的叶子结点,从而达到简化决策树的目的。
决策树剪枝一般包括两种方法:预剪枝(pre-pruning)和后剪枝(post-pruning)。
预剪枝是在树生成过程中进行剪枝的方法,其核心思想在树中结点进行扩展之前,先计算当前的特征划分能否带来决策树泛化性能的提升,如果不能的话则决策树不再进行生长。预剪枝比较直接,算法也简单,效率高,适合大规模问题计算,但预剪枝可能会有一种”早停”的风险,可能会导致模型欠拟合。
在实际应用中还是以后剪枝方法为主。后剪枝则是等树完全生长完毕之后再从最底端的叶子结点进行剪枝。CART剪枝正是一种后剪枝方法。简单来说,就是自底向上对完全树进行逐结点剪枝,每剪一次就形成一个子树,一直到根结点,这样就形成一个子树序列。然后在独立的验证集数据上对全部子树进行交叉验证,哪个子树误差最小,哪个就是最优子树。具体细节可参考统计学习方法给出的剪枝算法步骤,笔者这里不深入展开公式。
本文作者:王陸
本文链接:https://www.cnblogs.com/wkfvawl/p/16587436.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2018-08-15 竖式除法模拟
2018-08-15 Pythagorean Triples毕达哥斯拉三角(数学思维+构造)