跟着AI学AI(3):决策树

跟着AI学AI-3:决策树

提问:请使用numpy实现决策树模型代码

然后在两个 _best_split_grow_tree 两个方法里,框选要求添加必要的注释

import numpy as np

class DecisionTree:
    def __init__(self, max_depth=5, min_samples_split=2):
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.tree = {
    
    }

    def fit(self, X, y):
        self.tree = self._grow_tree(X, y)

    def predict(self, X):
        return np.array([self._predict(inputs) for inputs in X])

    def _predict(self, inputs):
        node = self.tree
        while 'feature_index' in node:
            if inputs[node['feature_index']] < node['threshold']:
                node = node['left']
            else:
                node = node['right']
        return node['predicted_class']

    def _best_split(self, X, y):
        m = y.size
        if m <= self.min_samples_split:
            return None, None
        num_parent = [np.sum(y == c) for c in range(self.n_classes_)]
        best_gini = np.inf
        best_idx, best_thr = None, None
        for idx in range(self.n_features_):
            # 对特征值进行排序
            thresholds, classes = zip(*sorted(zip(X[:, idx], y)))

            # 初始化左右子树的样本数
            num_left = [0] * self.n_classes_
            num_right = num_parent.copy()

            # 遍历每个阈值,计算基尼系数
            for i in range(1, m):
                c = classes[i - 1]
                num_left[c] += 1
                num_right[c] -= 1

                # 计算左右子树的基尼系数
                gini_left = 1.0 - sum((num_left[x] / i) ** 2 for x in range(self.n_classes_))
                gini_right = 1.0 - sum((num_right[x] / (m - i)) ** 2 for x in range(self.n_classes_))

                # 计算加权平均基尼系数
                gini = (i * gini_left + (m - i) * gini_right) / m

                # 如果阈值相同,跳过
                if thresholds[i] == thresholds[i - 1]:
                    continue

                # 如果基尼系数更小,更新最佳分裂特征和阈值
                if gini < best_gini:
                    best_gini = gini
                    best_idx = idx
                    best_thr = (thresholds[i] + thresholds[i - 1]) / 2
        return best_idx, best_thr

    def _grow_tree(self, X, y, depth=0):
        # 计算每个类别的样本数
        num_samples_per_class = [np.sum(y == i) for i in range(self.n_classes_)]
        # 预测类别为样本数最多的类别
        predicted_class = np.argmax(num_samples_per_class)
        # 构建节点
        node = {
    
    'predicted_class': predicted_class}
        # 如果深度小于最大深度,继续分裂
        if depth < self.max_depth:
            # 找到最佳分裂特征和阈值
            idx, thr = self._best_split(X, y)
            if idx is not None:
                # 根据最佳分裂特征和阈值分裂数据集
                indices_left = X[:, idx] < thr
                X_left = X[indices_left]
                y_left = y[indices_left]
                X_right = X[~indices_left]
                y_right = y[~indices_left]
                # 构建左右子树
                node = {
    
    'feature_index': idx,
                        'threshold': thr,
                        'left': self._grow_tree(X_left, y_left, depth + 1),
                        'right': self._grow_tree(X_right, y_right, depth + 1)}
        return node

提问:请解释fit方法的训练过程,翻译成中文

fit方法的训练过程是通过调用_grow_tree方法来构建决策树。在_grow_tree方法中,首先计算每个类别的样本数,然后预测类别为样本数最多的类别。如果深度小于最大深度,找到最佳分裂特征和阈值,根据最佳分裂特征和阈值分裂数据集,构建左右子树。最终返回构建好的决策树。在fit方法中,调用_grow_tree方法来构建决策树,并将构建好的决策树保存在self.tree中。

提问:请解释下_best_split方法的计算过程,翻译成中文

_best_split方法的计算过程是通过遍历每个特征的每个阈值,计算每个阈值对应的左右子树的基尼系数,然后计算加权平均基尼系数,选择基尼系数最小的特征和阈值作为最佳分裂特征和阈值。具体地,对于每个特征,首先对特征值进行排序,然后初始化左右子树的样本数,遍历每个阈值,计算左右子树的基尼系数,计算加权平均基尼系数,如果阈值相同则跳过,如果基尼系数更小则更新最佳分裂特征和阈值。最终返回最佳分裂特征和阈值。

提问:有几种决策树不纯度指标计算方法,请分别用python实现

  1. 基尼系数
def _gini(self, y):
    m = y.size
    return 1.0 - sum((np.sum(y == c) / m) ** 2 for c in range(self.n_classes_))
def _entropy(self, y):
    m = y.size
    return -sum((np.sum(y == c) / m) * np.log2(np.sum(y == c) / m) for c in range(self.n_classes_))
  1. 分类误差
def _classification_error(self, y):
    m = y.size
    return 1.0 - np.max([np.sum(y == c) / m for c in range(self.n_classes_)])

提问:为什么基尼系数表示决策树节点的不纯度

基尼系数表示决策树节点的不纯度,即节点中样本不属于同一类别的程度。在决策树的构建过程中,选择基尼系数最小的特征和阈值作为最佳分裂特征和阈值,可以使得分裂后的左右子树的不纯度最小,从而提高决策树的准确性。与熵和分类误差相比,基尼系数的计算更加高效,因为它不涉及对数运算,而且在二分类问题中,基尼系数和熵的结果是相同的。因此,基尼系数是决策树中常用的不纯度度量方法之一。

提问:为什么信息熵可以表示决策树节点的不纯度

信息熵可以表示决策树节点的不纯度,即节点中样本不属于同一类别的程度。在决策树的构建过程中,选择信息熵最小的特征和阈值作为最佳分裂特征和阈值,可以使得分裂后的左右子树的不纯度最小,从而提高决策树的准确性。与基尼系数和分类误差相比,信息熵的计算更加直观,因为它可以被解释为样本集合中的不确定性。因此,信息熵也是决策树中常用的不纯度度量方法之一。

提问:为什么分类误差可以表示决策树节点的不纯度

分类误差可以表示决策树节点的不纯度,即节点中样本不属于同一类别的程度。在决策树的构建过程中,选择分类误差最小的特征和阈值作为最佳分裂特征和阈值,可以使得分裂后的左右子树的不纯度最小,从而提高决策树的准确性。与基尼系数和信息熵相比,分类误差的计算更加简单,因为它只需要计算错误率,而不需要考虑样本的概率分布。然而,分类误差对噪声数据比较敏感,因为它只考虑了样本数量,而没有考虑样本的权重。因此,分类误差不如基尼系数和信息熵在决策树中常用。

提问:决策树分裂中,计算的信息增益是什么?请一步步解释信息增益的计算步骤和公式

信息增益是决策树分裂中用于选择最佳分裂特征的度量方法之一,它表示分裂前后样本集合的不确定性减少的程度。具体地,信息增益的计算步骤如下:

  1. 计算分裂前的不确定性,通常使用基尼系数、信息熵或分类误差来度量。
  2. 对于每个特征,计算分裂后的不确定性,通常使用基尼系数、信息熵或分类误差来度量。
  3. 计算信息增益,即分裂前的不确定性减去分裂后的不确定性之和。

信息增益的公式如下:

信息增益 = 不确定性 - 条件不确定性

其中,不确定性表示分裂前的不确定性,条件不确定性表示分裂后的不确定性,它是对每个特征计算的。在决策树的构建过程中,选择信息增益最大的特征作为最佳分裂特征,可以使得分裂后的左右子树的不确定性最小,从而提高决策树的准确性。与基尼系数和信息熵相比,信息增益的计算更加灵活,因为它可以适用于任何类型的分类问题。然而,信息增益对取值较多的特征比较偏向,因为它倾向于选择取值较多的特征,从而导致过拟合的问题。因此,在实际应用中,通常会使用基尼系数或信息熵来代替信息增益。

提问:请使用numpy实现采用信息熵规则的决策树实现

这次AI不太智能,生成了包含基尼系数、信息熵、分类误差三种可选规则的决策树实现,并且漏了self._entropyself._gini以及self._classification_error三个实现。

import numpy as np

class DecisionTree:
    def __init__(self, max_depth=5, min_samples_split=2, criterion='entropy'):
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.criterion = criterion
        self.tree = {
    
    }

    def fit(self, X, y):
        self.n_classes_ = len(np.unique(y))
        self.n_features_ = X.shape[1]
        self.tree = self._grow_tree(X, y)

    def predict(self, X):
        return np.array([self._predict(inputs) for inputs in X])

    def _predict(self, inputs):
        node = self.tree
        while 'feature_index' in node:
            if inputs[node['feature_index']] < node['threshold']:
                node = node['left']
            else:
                node = node['right']
        return node['predicted_class']

    def _best_split(self, X, y):
        m = y.size
        if m <= self.min_samples_split:
            return None, None
        if self.criterion == 'entropy':
            # 计算分裂前的熵
            entropy = self._entropy(y)
            best_info_gain = -np.inf
            best_idx, best_thr = None, None
            for idx in range(self.n_features_):
                # 对特征值进行排序
                thresholds, classes = zip(*sorted(zip(X[:, idx], y)))
                # 初始化左右子树的样本数
                num_left = [0] * self.n_classes_
                num_right = num_left.copy()
                # 遍历每个阈值,计算信息增益
                for i in range(1, m):
                    c = classes[i - 1]
                    num_left[c] += 1
                    num_right[c] = np.sum(y == c) - num_left[c]
                    # 如果阈值相同,跳过
                    if thresholds[i] == thresholds[i - 1]:
                        continue
                    # 计算分裂后的熵和信息增益
                    entropy_left = self._entropy(num_left)
                    entropy_right = self._entropy(num_right)
                    info_gain = entropy - (i * entropy_left + (m - i) * entropy_right) / m
                    # 如果信息增益更大,更新最佳分裂特征和阈值
                    if info_gain > best_info_gain:
                        best_info_gain = info_gain
                        best_idx = idx
                        best_thr = (thresholds[i] + thresholds[i - 1]) / 2
        elif self.criterion == 'gini':
            # 计算分裂前的基尼系数
            gini = self._gini(y)
            best_gini_gain = np.inf
            best_idx, best_thr = None, None
            for idx in range(self.n_features_):
                # 对特征值进行排序
                thresholds, classes = zip(*sorted(zip(X[:, idx], y)))
                # 初始化左右子树的样本数
                num_left = [0] * self.n_classes_
                num_right = num_left.copy()
                # 遍历每个阈值,计算基尼增益
                for i in range(1, m):
                    c = classes[i - 1]
                    num_left[c] += 1
                    num_right[c] = np.sum(y == c) - num_left[c]
                    # 如果阈值相同,跳过
                    if thresholds[i] == thresholds[i - 1]:
                        continue
                    # 计算分裂后的基尼系数和基尼增益
                    gini_left = self._gini(num_left)
                    gini_right = self._gini(num_right)
                    gini_gain = gini - (i * gini_left + (m - i) * gini_right) / m
                    # 如果基尼增益更大,更新最佳分裂特征和阈值
                    if gini_gain < best_gini_gain:
                        best_gini_gain = gini_gain
                        best_idx = idx
                        best_thr = (thresholds[i] + thresholds[i - 1]) / 2
        elif self.criterion == 'classification_error':
            # 计算分裂前的分类误差
            ce = self._classification_error(y)
            best_ce_gain = np.inf
            best_idx, best_thr = None, None
            for idx in range(self.n_features_):
                # 对特征值进行排序
                thresholds, classes = zip(*sorted(zip(X[:, idx], y)))
                # 初始化左右子树的样本数
                num_left = [0] * self.n_classes_
                num_right = num_left.copy()
                # 遍历每个阈值,计算分类误差增益
                for i in range(1, m):
                    c = classes[i - 1]
                    num_left[c] += 1
                    num_right[c] = np.sum(y == c) - num_left[c]
                    # 如果阈值相同,跳过
                    if thresholds[i] == thresholds[i - 1]:
                        continue
                    # 计算分裂后的分类误差和分类误差增益
                    ce_left = self._classification_error(num_left)
                    ce_right = self._classification_error(num_right)
                    ce_gain = ce - (i * ce_left + (m - i) * ce_right) / m
                    # 如果分类误差增益更大,更新最佳分裂特征和阈值
                    if ce_gain < best_ce_gain:
                        best_ce_gain = ce_gain
                        best_idx = idx
                        best_thr = (thresholds[i] + thresholds[i - 1]) / 2
        else:
            raise ValueError('Invalid criterion: %s' % self.criterion)
        # 如果分类误差增益、基尼增益或信息增益都小于等于0,则停止分裂
        if best_idx is None or best_thr is None or best_info_gain <= 0 and best_gini_gain <= 0 and best_ce_gain <= 0:
            return None, None
        return best_idx, best_thr

    def _grow_tree(self, X, y, depth=0):
        # 计算每个类别的样本数
        num_samples_per_class = [np.sum(y == i) for i in range(self.n_classes_)]
        # 预测类别为样本数最多的类别
        predicted_class = np.argmax(num_samples_per_class)
        # 构建节点
        node = {
    
    'predicted_class': predicted_class}
        # 如果深度小于最大深度,继续分裂
        if depth < self.max_depth:
            # 找到最佳分裂特征和阈值
            idx, thr = self._best_split(X, y)
            if idx is not None:
                # 根据最佳分裂特征和阈值分裂数据集
                indices_left = X[:, idx] < thr
                X_left = X[indices_left]
                y_left = y[indices_left]
                X_right = X[~indices_left]
                y_right = y[~indices_left]
                # 如果左右子树的样本数都大于0,构建                
                node = {
    
    'feature_index': idx,
                        'threshold': thr,
                        'left': self._grow_tree(X_left, y_left, depth + 1),
                        'right': self._grow_tree(X_right, y_right, depth + 1)}
                return node
            else:
                return node
        else:
            return node

提问:请补充self._entropy,self._gini,self._classification_error 三个方法

在经过要求后,AI补充了三个方法的实现

	def _entropy(self, y):
        m = y.size
        return -sum((np.sum(y == c) / m) * np.log2(np.sum(y == c) / m) for c in range(self.n_classes_))

    def _classification_error(self, y):
        m = y.size
        return 1.0 - np.max([np.sum(y == c) / m for c in range(self.n_classes_)])

    def _gini(self, y):
        m = y.size
        return 1.0 - sum((np.sum(y == c) / m) ** 2 for c in range(self.n_classes_))

提问:请使用numpy举一个实际的例子解释代码片thresholds, classes = zip(*sorted(zip(X[:, idx], y)))

这行代码的作用是将特征X[:, idx]和标签y按照特征值从小到大排序,并将排序后的特征值和标签分别存储在thresholds和classes两个元组中。这个代码片段在决策树的分裂过程中用到,它可以帮助我们找到最佳分裂特征和阈值。例如,假设我们有一个二分类问题,特征X[:, idx]表示某个人的年龄,标签y表示该人是否购买了某个产品。我们希望找到一个年龄阈值,将年龄小于该阈值的人划分为一类,年龄大于等于该阈值的人划分为另一类。为了找到最佳阈值,我们可以使用这个代码片段将年龄和标签按照年龄从小到大排序,然后遍历每个阈值,计算分裂后的基尼系数、信息熵或分类误差,选择使得不纯度最小的阈值作为最佳阈值。

提问:请使用numpy举一个最小的例子代码,解释thresholds, classes = zip(*sorted(zip(X[:, idx], y))),并打印。

X = np.array([[3, 1], [2, 0], [1, 1]])
y = np.array([0, 1, 0])
idx = 0
thresholds, classes = zip(*sorted(zip(X[:, idx], y)))
print(thresholds)
print(classes)

提问:请输出结果

[1, 2, 3]
(1, 0, 0)

我们可以手工改造下代码,写一个完整的程序验证数据:

if __name__=="__main__":
	X = np.array([[3, 1], [2, 0], [1, 1]])
	y = np.array([0, 1, 0])
	for idx in range(X.shape[1]):
		thresholds, classes = zip(*sorted(zip(X[:, idx], y)))
		print(thresholds)
		print(classes)
		print("---------")

输出:

(1, 2, 3)
(0, 1, 0)
---------
(0, 1, 1)
(1, 0, 0)
---------

可见,zip(*sorted(zip(X[:, idx], y))) 的作用就是针对X种每一类特征(每一列)排序,thresholds 就是排序后的列,而classes 是对应的实际类别。

分析:决策树分裂算法

分析决策树的三种不同不纯度指标分裂算法,代码大同小异,以信息熵为例,代码如下:

# 计算分裂前的熵
entropy = self._entropy(y)
best_info_gain = -np.inf
best_idx, best_thr = None, None
for idx in range(self.n_features_):
    # 对特征值进行排序
    thresholds, classes = zip(*sorted(zip(X[:, idx], y)))
    # 初始化左右子树的样本数
    num_left = [0] * self.n_classes_
    num_right = num_left.copy()
    # 遍历每个阈值,计算信息增益
    for i in range(1, m):
        c = classes[i - 1]
        num_left[c] += 1
        num_right[c] = np.sum(y == c) - num_left[c]
        # 如果阈值相同,跳过
        if thresholds[i] == thresholds[i - 1]:
            continue
        # 计算分裂后的熵和信息增益
        entropy_left = self._entropy(num_left)
        entropy_right = self._entropy(num_right)
        info_gain = entropy - (i * entropy_left + (m - i) * entropy_right) / m
        # 如果信息增益更大,更新最佳分裂特征和阈值
        if info_gain > best_info_gain:
            best_info_gain = info_gain
            best_idx = idx
            best_thr = (thresholds[i] + thresholds[i - 1]) / 2

核心是使用分裂前的熵entropy 减去分裂后的左右两颗子树的熵的加权组合i/m * entropy_left+(m - i)/m * entropy_right),也就是信息增益,这个信息增益如果最大,则表示样本数据在使用当前特征(best_idx=idx)和特征的阈值(best_thr=(thresholds[i] + thresholds[i - 1]) / 2),可以在分裂后的左右子树上,让两个子树的不纯度最小化。但是这个不纯度最小化,并没有立刻在中间节点将数据属于某个类别区分出来,只有到了叶子节点,叶子节点里占据主导地位的类别(就是叶子节点种子数据里的列别频率最高的那个类别)就是分类输出类别。

可以再次看下节点增长的逻辑代码:

def _grow_tree(self, X, y, depth=0):
    # 计算每个类别的样本数
    num_samples_per_class = [np.sum(y == i) for i in range(self.n_classes_)]
    # 预测类别为样本数最多的类别
    predicted_class = np.argmax(num_samples_per_class)
    # 构建节点
    node = {
    
    'predicted_class': predicted_class}
    # 如果深度小于最大深度,继续分裂
    if depth < self.max_depth:
        # 找到最佳分裂特征和阈值
        idx, thr = self._best_split(X, y)
        if idx is not None:
            # 根据最佳分裂特征和阈值分裂数据集
            indices_left = X[:, idx] < thr
            X_left = X[indices_left]
            y_left = y[indices_left]
            X_right = X[~indices_left]
            y_right = y[~indices_left]
            # 如果左右子树的样本数都大于0,构建                
            node = {
    
    'feature_index': idx,
                    'threshold': thr,
                    'left': self._grow_tree(X_left, y_left, depth + 1),
                    'right': self._grow_tree(X_right, y_right, depth + 1)}
            return node
        else:
            return node
    else:
        return node

其中叶子节点node = {'predicted_class': predicted_class},这里的predicted_class就是统计当前节点样本数据上频率最高的类别:

# 计算每个类别的样本数
num_samples_per_class = [np.sum(y == i) for i in range(self.n_classes_)]
# 预测类别为样本数最多的类别
predicted_class = np.argmax(num_samples_per_class)

而中间节点,则没有计算predicted_class

# 如果左右子树的样本数都大于0,构建                
node = {
    
    'feature_index': idx,
        'threshold': thr,
        'left': self._grow_tree(X_left, y_left, depth + 1),
        'right': self._grow_tree(X_right, y_right, depth + 1)}

当然也可以把 predicted_class 赋值给中间节点,只是当它有左右子节点的时候,是可以进一步分类的。

# 如果左右子树的样本数都大于0,构建                
node = {
    
    
		'predicted_class': predicted_class, # 如果只分类到当前节点,则输出类别就是predicted_class;
		'feature_index': idx,
        'threshold': thr,
        'left': self._grow_tree(X_left, y_left, depth + 1),
        'right': self._grow_tree(X_right, y_right, depth + 1)}

提问:请用数组实际例子解释代码indices_left = X[:, idx] < thr并打印结果

回答:

# 假设我们有一个二分类问题,特征X[:, idx]表示某个人的年龄,标签y表示该人是否购买了某个产品。
# 我们希望找到一个年龄阈值,将年龄小于该阈值的人划分为一类,年龄大于等于该阈值的人划分为另一类。
# 假设我们选择年龄阈值为2,即年龄小于2的人划分为一类,年龄大于等于2的人划分为另一类。
# 我们可以使用这个代码片段将年龄和标签按照年龄从小到大排序,然后将年龄小于2的人划分为一类,年龄大于等于2的人划分为另一类。
# 这个代码片段的作用是将特征X[:, idx]中小于阈值thr的元素的索引置为True,大于等于阈值thr的元素的索引置为False。
# 例如,对于样本X = np.array([[3, 1], [2, 0], [1, 1]])和阈值thr = 2,这个代码片段将返回一个布尔数组[False, True, True],
# 表示第一个样本年龄大于等于2,第二个和第三个样本年龄小于2。
indices_left = X[:, idx] < thr
print(indices_left)

这个回答到注释里有例子的数据,改造下代码:

X = np.array([[3, 1], [2, 0], [1, 1]])
thr = 2
idx = 0

print("X[:, idx]:", X[:, idx])

indices_left = X[:, idx] < thr
print("indices_left:",indices_left)

X_left = X[indices_left]
print("X_left:",X_left)

X_right = X[~indices_left]
print("X_right:",X_right)

输出,可以看到注释里生成的这个代码片段将返回一个布尔数组[False, True, True]实际上是错误的,正确的输出代码是:

X[:, idx]: [3 2 1]
indices_left: [False False  True]
X_left: [[1 1]]
X_right: [[3 1],[2 0]]

实际上这个就是类似于消消乐:

[
	[3 1],
	[2 0],
	[1 1]
]

其中 X[:, 0] 就是

[
	3
	2
	1
]

满足 X[:, 0]<thr的行下标是(此时,thr=2)

2

则X[indices_left]只会保留X里行下标是2的行,结果就是

[
	[1 1]
]

则X[~indices_left]只会保留X里行下标是0,1的行,结果就是

[
	[3 1],
	[2 0]
]

这个细节理解后,就有助于阅读上述的决策树模型的_grow_tree方法的实现。这个和所有英文阅读一样:如果某个单词不认识,整句话读起来会不能理解整体意思。我们看机器学习的代码实现,常常会有这种细节问题需要理解。因为python基于numpy、pandas等一系列科学计算库实现的各种代码,实际上很多代码并不符合人的直觉的,只是用代码的方式表达这些确实是存在各种「异构」的表达方式,于是我们看python的这些数据转换代码,并不如看if-else,for, while 等逻辑转换代码流畅。如果说逻辑代码是英语通过26个字母组合出单词,单词组合出句子,那么数据转换代码更像几千个常用汉字,再由汉字组成句子。也就是你需要记忆的基本数据转换代码更多,从而导致的了这种不直观。好在有AI,我们可以反复要求AI解释细节。

提问:请写这段代码的单元测试

代码:

thresholds, classes = zip(*sorted(zip(X[:, idx], y)))
num_left = [0] * self.n_classes_
num_right = num_left.copy()
for i in range(1, m):
    c = classes[i - 1]
    num_left[c] += 1
    num_right[c] = np.sum(y == c) - num_left[c]

回答:

def test_num_left_right():
    X = np.array([[3, 1], [2, 0], [1, 1]])
    y = np.array([0, 1, 0])
    idx = 0
    thresholds, classes = zip(*sorted(zip(X[:, idx], y)))
    num_left = [0] * len(np.unique(y))
    num_right = num_left.copy()
    m = y.size
    for i in range(1, m):
        c = classes[i - 1]
        num_left[c] += 1
        num_right[c] = np.sum(y == c) - num_left[c]
    assert num_left == [1, 1]
    assert num_right == [1, 0]

test_num_left_right()

可见其中num_leftnum_right 分别是左右子树的类别值的计数器。有一个疑问是,按for i in range(1,m)遍历,岂不是:

  • 划分到左子树的类别总是从classes[0]classes[i]里的唯一类别范围
  • 划分到右子树的类别总是从classes[i+1]classes[m-1]里的唯一类别范围

需要注意的是,classes并不是从小到大的,上述单元测试代码的classes的值是:

(0, 1, 0)

这个问题的关键点在于zip(*sorted(zip(X[:, idx], y))) 先对 X[:, idx] 做了排序,也就是第 idx 个特征列(thresholds)是从小到大排序的。决策树的每个中间节点,是根据特征列的阈值决定左右子树的样本数据划分。而有m个元素的thresholds已经排序过了,从小到大遍历,每次遍历都假设当前特征值作为该特征的划分阈值,此时计算被划分到左右两个子树的样本的类别计数,也就是 num_leftnum_right。进一步的后续代码则是计算左右子树的不纯度指标,再计算父节点和左右子节点的信息增益。信息增益最大的就是最佳划分特征列和对应特征列的划分阈值。

# 计算分裂后的基尼系数和基尼增益
gini_left = self._gini(num_left)
gini_right = self._gini(num_right)
gini_gain = gini - (i * gini_left + (m - i) * gini_right) / m
# 如果基尼增益更大,更新最佳分裂特征和阈值
if gini_gain < best_gini_gain:
    best_gini_gain = gini_gain
    best_idx = idx
    best_thr = (thresholds[i] + thresholds[i - 1]) / 2

到这里,决策树模型核心代码就理解的差不多了。

提问:决策树的优点和缺点是什么?

决策树的优点包括:

  1. 易于理解和解释,可以可视化展示决策过程;
  2. 可以处理离散和连续特征,适用于各种类型的分类问题;
  3. 可以处理多分类和回归问题;
  4. 可以处理缺失值和异常值;
  5. 可以通过剪枝避免过拟合。

决策树的缺点包括:

  1. 容易受到噪声和异常值的影响,导致过拟合;
  2. 对于取值较多的特征,容易偏向于选择取值较多的特征,导致过拟合;
  3. 对于类别不平衡的数据集,容易偏向于选择样本数较多的类别,导致分类效果不佳;
  4. 对于连续特征,需要进行离散化处理,可能会损失一些信息;
  5. 决策树的复杂度较高,构建和预测的时间复杂度都是O(NlogN)或O(N^2),不适用于大规模数据集。

提问:决策树的剪枝方法有哪些

决策树的剪枝方法包括预剪枝和后剪枝。预剪枝是在决策树构建过程中,在每个节点处判断是否进行分裂,如果不进行分裂则将该节点标记为叶子节点。预剪枝的优点是简单快速,可以避免过拟合,但是可能会导致欠拟合。后剪枝是在决策树构建完成后,对决策树进行剪枝。具体来说,后剪枝的过程是从决策树的叶子节点开始,逐个将叶子节点替换为其父节点,然后计算剪枝后的决策树在验证集上的准确率,如果准确率提高了,则保留剪枝后的决策树,否则恢复原来的决策树。后剪枝的优点是可以充分利用训练数据,避免欠拟合,但是计算复杂度较高。

提问:如何解决决策树的缺点?

回答:
决策树的缺点可以通过集成学习方法来解决,例如随机森林和梯度提升树。随机森林是一种基于决策树的集成学习方法,它通过随机选择特征和样本,构建多个决策树,并将它们的预测结果进行投票或平均,从而提高分类准确率和泛化能力。梯度提升树也是一种基于决策树的集成学习方法,它通过迭代地拟合残差,构建多个决策树,并将它们的预测结果进行加权和,从而提高回归准确率和泛化能力。这些集成学习方法可以有效地降低决策树的过拟合风险,提高分类和回归的准确率和泛化能力。此外,还可以通过特征选择、数据清洗和数据增强等方法来提高决策树的性能和鲁棒性。

分析:
这里提到了随机森林和梯度提升树,后续继续学习。

提问:什么时候使用决策树,什么时候使用逻辑回归

决策树适用于以下场景:

  1. 特征具有离散或连续取值;
  2. 特征之间存在复杂的非线性关系;
  3. 数据集中存在缺失值或异常值;
  4. 数据集中存在类别不平衡的情况;
  5. 需要可视化展示决策过程或解释决策结果。

逻辑回归适用于以下场景:

  1. 特征具有连续或离散取值;
  2. 特征之间存在线性或近似线性关系;
  3. 数据集中不存在缺失值或异常值;
  4. 数据集中不存在类别不平衡的情况;
  5. 需要快速训练和预测模型,适用于大规模数据集。

提问:是否决策树和逻辑回归实际上都不实用?

决策树和逻辑回归都是常用的分类算法,它们各有优缺点,适用于不同的场景。决策树的优点是易于理解和解释,可以处理离散和连续特征,适用于各种类型的分类问题,可以处理多分类和回归问题,可以处理缺失值和异常值,可以通过剪枝避免过拟合。逻辑回归的优点是简单快速,可以处理二分类和多分类问题,可以处理连续和离散特征,可以输出概率值,可以通过正则化避免过拟合。虽然它们都有一些缺点,但是在实际应用中,它们仍然是非常实用的分类算法之一。例如,在金融、医疗、电商等领域,决策树和逻辑回归都被广泛应用于客户分类、信用评估、疾病诊断、商品推荐等任务中。因此,我们应该根据具体问题的特点选择合适的分类算法,并结合特征工程、模型调参等方法来提高分类准确率和泛化能力。

提问:如果决策和逻辑回归实用,为什么还需要其他分类算法?

虽然决策树和逻辑回归是常用的分类算法,但是它们并不是万能的,仍然存在一些问题和局限性。例如,决策树容易受到噪声和异常值的影响,对于取值较多的特征,容易偏向于选择取值较多的特征,对于类别不平衡的数据集,容易偏向于选择样本数较多的类别,对于连续特征,需要进行离散化处理,可能会损失一些信息,决策树的复杂度较高,不适用于大规模数据集。逻辑回归对于非线性问题需要进行特征转换或使用核技巧,对于类别不平衡的数据集需要进行样本权重调整或使用代价敏感学习,可能会导致分类效果不佳。因此,为了提高分类准确率和泛化能力,我们需要使用其他分类算法,例如支持向量机、朴素贝叶斯、神经网络等。这些算法各有优缺点,适用于不同的场景,可以根据具体问题的特点选择合适的算法,并结合特征工程、模型调参等方法来提高分类效果。总之,选择合适的分类算法是提高分类准确率和泛化能力的关键之一。

提问:学习了决策树,下一个机器学习模型推荐什么?

下一个机器学习模型推荐支持向量机(SVM)。SVM是一种基于统计学习理论的分类算法,它通过寻找最优超平面来实现分类,可以处理线性和非线性问题,适用于各种类型的分类问题,具有较高的分类准确率和泛化能力。SVM的优点包括:可以处理高维数据和非线性问题,可以通过核技巧将低维特征映射到高维空间,可以处理类别不平衡的数据集,可以通过正则化避免过拟合。SVM的缺点包括:对于大规模数据集,训练时间较长,需要选择合适的核函数和参数,可能会导致过拟合。因此,在使用SVM时,需要进行特征选择、数据清洗和数据归一化等预处理步骤,选择合适的核函数和参数,进行交叉验证和模型调参等步骤,以提高分类准确率和泛化能力。

–end–

猜你喜欢

转载自blog.csdn.net/huanhuilong/article/details/129849153