大数跨境

面试官:“决策树和随机森林有啥区别?”我:“Codex 都给我写好了!”面试官:“CART、Gini、剪枝都说不清,你怎么解释过拟合?”

面试官:“决策树和随机森林有啥区别?”我:“Codex 都给我写好了!”面试官:“CART、Gini、剪枝都说不清,你怎么解释过拟合?” 机器学习和人工智能AI
2026-10-27
3

哈喽,大家好~

学决策树的时候,很多同学能背出“基尼指数、信息增益、递归分裂”这些词,但一到自己写代码,就容易变成:代码跑通了,模型为什么这么分、特征重要性到底怎么算,却没完全弄明白。

这篇我们就把决策树分类从头捋一遍~

我会先用一个生活化的例子解释它在做什么,再用 PyTorch 手写一个简化版 CART 分类树,最后通过数据分布、决策边界、分裂收益、特征重要性、叶节点类别分布和混淆矩阵,看看模型到底学到了什么。

先说明一下:下面的实现主要是为了理解算法原理,方便大家自己改代码做实验。它不是用来替代 scikit-learn 的工业级实现,运行速度和边界情况处理也没有后者完善~

决策树到底在做什么?

可以把决策树理解成一个不断追问问题的过程。

假设我们要判断一条样本属于哪个类别。模型先问一个问题,比如“特征 1 是否小于 2.5?”答案不同,就走向不同的分支;到了下一个节点,再问一个问题。一路走到叶节点后,模型就用这个叶节点里占多数的类别作为预测结果。

像这样:

  • 特征 1 小于等于 2.5?是,走左边;否,走右边。
  • 在左边继续判断特征 2 是否小于等于 1.2。
  • 一直走到叶节点,输出该节点的预测类别。

这里的数值只是举例。实际训练时,模型会从数据中寻找合适的特征和切分阈值,不需要我们提前手工写好这些规则。

那问题来了:每个节点到底应该问什么?

决策树会尝试不同的特征和阈值,比较切分前后的类别混杂程度。理想情况下,切分后左边和右边各自都更接近“清一色”,这样的切分就更有价值。

决策树的优点是规则直观、容易解释,通常也不需要像线性模型或神经网络那样专门做特征标准化。缺点也很明显:树长得太深时,可能把训练数据里的噪声也学进去;数据稍有变化,学出来的树结构也可能变化。因此,限制树深、限制叶节点样本数,或者使用剪枝,都是常见的控制复杂度方式。

基尼指数、熵和信息增益,到底有什么关系?

先看基尼不纯度。假设一个节点里有 (K) 个类别,第 (k) 类所占比例是 (p_k),那么:

如果一个节点里的样本全属于同一类,那么某个类别的比例是 1,其余类别比例都是 0,基尼指数就是 0,说明这个节点非常纯。

如果不同类别混在一起,基尼指数就会变大。对于固定数量的类别,当各类别比例比较均衡时,基尼不纯度通常更高。

熵也是衡量类别混杂程度的指标:

当 (p_k=0) 时,约定对应项 (0\log_2 0=0)。这里使用以 2 为底的对数;如果改用自然对数,数值尺度会变化,但不会改变同一节点上候选切分的排序结果。

接下来是最关键的地方。假设父节点有 (n) 个样本,切分后左边有 (n_L) 个、右边有 (n_R) 个样本,那么切分带来的不纯度下降可以写成:

这里的 (I) 可以用基尼指数,也可以用熵。

如果用熵来计算,这个量通常称为信息增益;如果用基尼指数来计算,更准确的说法是“基尼不纯度下降”或“不纯度下降”。两者思路相似,但计算指标不同,写文章或解释代码时最好不要把术语混在一起。

训练时,CART 分类树会寻找能让加权子节点不纯度尽可能低的切分,也就是让上面的不纯度下降尽可能大。注意,它是在当前节点做局部贪心选择,并不保证最后得到全局最优的整棵树。

决策树是怎么一步步长出来的?

把流程放到一起,其实不复杂~

模型先从根节点开始,查看当前节点中的样本。如果样本已经全属于同一类,或者达到最大深度、样本数不足等停止条件,就不再继续切分。

如果还能继续,模型就针对每个特征寻找候选阈值。对于连续特征,一种常见做法是先将特征值排序,再把相邻且不相同的两个值的中点作为候选阈值。例如相邻值是 2.0 和 3.0,就可以尝试阈值 2.5。

每个候选切分都会产生左右两个子节点。模型计算它们的加权不纯度,挑出收益最大的那个切分,然后分别对左右子节点重复这个过程。

最后,预测一条新样本时,只需要从根节点开始按阈值一路往下走,抵达叶节点后输出类别即可。

下面我们动手实现。为了让数据结构和可视化更直观,这里使用二维合成数据;实际项目中的特征数量、数据分布和评估方式往往会复杂得多。

完整案例

我们这个案例会生成三类二维数据:两团高斯分布分别作为类别 0 和类别 1,另外把 make_moons 生成的双月形样本统一标记为类别 2。

这样可以观察决策树怎样用一系列横向、纵向的阈值切分来区分不同区域。

import torch
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs, make_moons
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, balanced_accuracy_score, f1_score, confusion_matrix
from matplotlib.colors import ListedColormap, BoundaryNorm

plt.rcParams.update({
    "figure.dpi": 120,
    "axes.grid": True,
    "grid.alpha": 0.22,
    "axes.spines.top": False,
    "axes.spines.right": False,
    "font.size": 10
})

# 数据集:两类高斯簇 + 一组双月形区域
def make_synthetic_data(n_samples=5600, random_state=42):
    n_blobs = n_samples // 2
    n_moons = n_samples - n_blobs

    X1, y1 = make_blobs(n_samples=n_blobs, centers=[(-3, 0), (3, 0)],
                        cluster_std=1.0, random_state=random_state)

    X2, y2 = make_moons(n_samples=n_moons, noise=0.12, random_state=random_state + 1)
    # make_moons 原本会生成两个类别;这里将它们合并为一个“类别 2”
    y2 = np.full(n_moons, 2, dtype=np.int64)

    X = np.vstack([X1, X2])
    y = np.concatenate([y1, y2])
    return X.astype(np.float32), y.astype(np.int64)

X, y = make_synthetic_data(5600)

# 三个类别使用固定颜色,确保不同图中的颜色含义一致
class_colors = ["#FF6B6B", "#6554C0", "#20B486"]
cmap = ListedColormap(class_colors)
norm = BoundaryNorm(np.arange(-0.5, 3.5, 1), cmap.N)
class_names = {
    0: "类别 0:左侧高斯簇",
    1: "类别 1:右侧高斯簇",
    2: "类别 2:双月形区域"
}

# 图1:原始数据分布
plt.figure(figsize=(9, 6))
for cls, name in class_names.items():
    plt.scatter(X[y == cls, 0], X[y == cls, 1],
                color=class_colors[cls], edgecolors="#202938",
                linewidths=0.45, s=34, alpha=0.85, label=name)
plt.title("图1:三类数据的分布")
plt.xlabel("特征 1")
plt.ylabel("特征 2")
plt.legend(frameon=True)
plt.tight_layout()
plt.show()

图 1 主要是帮助我们建立直觉:不同类别在二维平面上的分布并不完全相同,但类别之间也存在一定重叠。这个重叠是刻意保留的,模型不一定能把每个点都分对。

还有一点要注意:这里的双月形样本整体被视为同一类,所以这是一个用于演示的三分类数据集,并不是标准的“双月形二分类”任务。

实现树节点和分裂逻辑

下面这段是核心。我们使用基尼指数,逐层寻找合适的特征和阈值。为了避免对每个候选阈值都重新扫描全部样本,代码会先对特征值排序,再通过累计类别计数,一次性计算该特征上多个候选切分的收益。

这仍然是教学用的简化实现,没有实现缺失值处理、类别特征分裂、剪枝或并行优化;实际项目中通常会优先使用成熟的决策树库。

# 决策树实现
class TreeNode:
    def __init__(self, gini=None, num_samples=None, num_samples_per_class=None, predicted_class=None):
        self.gini = gini
        self.num_samples = num_samples
        self.num_samples_per_class = num_samples_per_class
        self.predicted_class = predicted_class
        self.feature_index = None
        self.threshold = None
        self.left = None
        self.right = None

class DecisionTreeClassifierTorch:
    def __init__(self, max_depth=5, min_samples_split=2, min_samples_leaf=1):
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.min_samples_leaf = min_samples_leaf
        self.n_classes_ = None
        self.n_features_ = None
        self.classes_ = None
        self.root = None
        self.feature_importances_ = None

    def _gini(self, y):
        # y: 一维标签张量
        if y.numel() == 0:
            return0.0
        _, counts = torch.unique(y, return_counts=True)
        probs = counts.float() / counts.sum()
        return1.0 - torch.sum(probs ** 2).item()

    def _best_split(self, X, y):
        # 返回最佳 (feature, threshold, gain, left_mask, right_mask)
        m, n_features = X.shape
        if m < self.min_samples_split or m < 2 * self.min_samples_leaf:
            returnNone

        parent_gini = self._gini(y)
        if parent_gini <= 0.0:
            returnNone

        best_gain = 0.0
        best_feat, best_thresh = None, None
        best_left_idx, best_right_idx = None, None

        for feat in range(n_features):
            sorted_vals, idxs = torch.sort(X[:, feat])
            sorted_y = y[idxs]

            # 只有相邻特征值不相同时,切分才可能改变左右两侧样本
            positions = torch.nonzero(sorted_vals[1:] > sorted_vals[:-1], as_tuple=False).flatten() + 1
            # 限制左右子节点的最小样本数,避免产生过小的叶节点
            positions = positions[(positions >= self.min_samples_leaf) &
                                  ((m - positions) >= self.min_samples_leaf)]
            if positions.numel() == 0:
                continue

            # 对排序后的标签做 one-hot 和累计计数,一次计算多个候选阈值的类别分布
            one_hot = torch.nn.functional.one_hot(sorted_y, num_classes=self.n_classes_).float()
            cumulative_counts = torch.cumsum(one_hot, dim=0)
            left_counts = cumulative_counts[positions - 1]
            total_counts = cumulative_counts[-1]
            right_counts = total_counts.unsqueeze(0) - left_counts

            left_sizes = positions.float()
            right_sizes = (m - positions).float()
            left_probs = left_counts / left_sizes.unsqueeze(1)
            right_probs = right_counts / right_sizes.unsqueeze(1)
            g_left = 1.0 - torch.sum(left_probs ** 2, dim=1)
            g_right = 1.0 - torch.sum(right_probs ** 2, dim=1)

            weighted_g = (left_sizes * g_left + right_sizes * g_right) / m
            gains = parent_gini - weighted_g

            local_best = torch.argmax(gains)
            gain = gains[local_best].item()
            if gain > best_gain:
                split_pos = positions[local_best].item()
                thresh = ((sorted_vals[split_pos - 1] + sorted_vals[split_pos]) / 2.0).item()
                left_mask = X[:, feat] <= thresh
                right_mask = ~left_mask
                best_gain = gain
                best_feat = feat
                best_thresh = thresh
                best_left_idx = left_mask
                best_right_idx = right_mask

        if best_feat isNone:
            returnNone
        return best_feat, best_thresh, best_gain, best_left_idx, best_right_idx

    def _build_tree(self, X, y, depth=0):
        node = TreeNode()
        node.num_samples = y.numel()
        node.num_samples_per_class = torch.bincount(y, minlength=self.n_classes_)
        node.predicted_class = torch.argmax(node.num_samples_per_class).item()
        node.gini = self._gini(y)

        # 达到停止条件时,当前节点就作为叶节点
        if depth >= self.max_depth or node.gini <= 0.0or y.numel() < self.min_samples_split:
            return node

        split = self._best_split(X, y)
        if split isnotNone:
            feat, thresh, gain, left_idx, right_idx = split
            node.feature_index = feat
            node.threshold = thresh
            node.left = self._build_tree(X[left_idx], y[left_idx], depth + 1)
            node.right = self._build_tree(X[right_idx], y[right_idx], depth + 1)
        return node

    def fit(self, X_np, y_np):
        X_np = np.asarray(X_np, dtype=np.float32)
        y_np = np.asarray(y_np).reshape(-1)

        if X_np.ndim != 2:
            raise ValueError("X 必须是二维数组,形状为 [样本数, 特征数]")
        if X_np.shape[0] != y_np.shape[0]:
            raise ValueError("X 和 y 的样本数必须一致")
        if X_np.shape[0] == 0:
            raise ValueError("训练数据不能为空")
        ifnot np.isfinite(X_np).all():
            raise ValueError("当前简化实现不处理 X 中的 NaN 或无穷值,请先处理缺失值和异常值")

        # 将原始类别编码为 0, 1, ..., K-1,预测时再映射回原标签
        self.classes_, y_encoded = np.unique(y_np, return_inverse=True)
        X = torch.from_numpy(np.ascontiguousarray(X_np))
        y = torch.from_numpy(y_encoded.astype(np.int64))
        self.n_classes_ = len(self.classes_)
        self.n_features_ = X.shape[1]
        self.root = self._build_tree(X, y)

        # 特征重要性:按节点样本占比加权后,累计基尼不纯度下降
        self.feature_importances_ = np.zeros(self.n_features_, dtype=float)
        n_total = self.root.num_samples

        def recurse(node):
            if node isNoneor node.feature_index isNone:
                return
            left = node.left
            right = node.right
            n = node.num_samples
            local_delta = node.gini - (
                left.num_samples * left.gini + right.num_samples * right.gini
            ) / n
            weighted_delta = (n / n_total) * local_delta
            self.feature_importances_[node.feature_index] += weighted_delta
            recurse(left)
            recurse(right)

        recurse(self.root)
        if self.feature_importances_.sum() > 0:
            self.feature_importances_ /= self.feature_importances_.sum()
        return self

    def predict(self, X_np):
        if self.root isNone:
            raise ValueError("请先调用 fit() 训练模型")

        X_np = np.asarray(X_np, dtype=np.float32)
        if X_np.ndim != 2or X_np.shape[1] != self.n_features_:
            raise ValueError(f"X 必须是二维数组,且特征数为 {self.n_features_}")
        ifnot np.isfinite(X_np).all():
            raise ValueError("当前简化实现不处理 X 中的 NaN 或无穷值")

        X = torch.from_numpy(np.ascontiguousarray(X_np))
        encoded_preds = torch.empty(X.shape[0], dtype=torch.int64)

        # 按节点批量路由样本,避免对每条样本都单独执行 Python 循环
        def route(node, indices):
            if indices.numel() == 0:
                return
            if node.feature_index isNone:
                encoded_preds[indices] = node.predicted_class
                return
            left_mask = X[indices, node.feature_index] <= node.threshold
            route(node.left, indices[left_mask])
            route(node.right, indices[~left_mask])

        route(self.root, torch.arange(X.shape[0], dtype=torch.long))
        return self.classes_[encoded_preds.numpy()]

这里有几个细节值得留意。

min_samples_split 控制一个节点至少有多少样本才尝试继续切分,min_samples_leaf 控制切分后左右叶子至少保留多少样本。它们不是同一个参数:只设置 min_samples_split,仍然可能切出只有一个样本的叶节点。

另外,代码把原始标签编码成连续的整数,只是为了方便统计类别数量;预测时会再映射回原标签。因此,标签不必一定是从 0 开始、连续排列的整数。

训练模型,先看指标

这里用分层抽样划分训练集和测试集。stratify=y 会尽量让两份数据中的类别比例接近,避免某次随机划分刚好让少数类样本比例偏得比较多。

# 划分训练集和测试集,尽量保持各类别比例
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=0, stratify=y
)

clf = DecisionTreeClassifierTorch(
    max_depth=5,
    min_samples_split=5,
    min_samples_leaf=3
)
clf.fit(X_train, y_train)

y_train_pred = clf.predict(X_train)
y_pred = clf.predict(X_test)

train_acc = accuracy_score(y_train, y_train_pred)
test_acc = accuracy_score(y_test, y_pred)
balanced_acc = balanced_accuracy_score(y_test, y_pred)
macro_f1 = f1_score(y_test, y_pred, average="macro", zero_division=0)

print(f"训练集准确率:{train_acc:.3f}")
print(f"测试集准确率:{test_acc:.3f}")
print(f"测试集 Balanced Accuracy:{balanced_acc:.3f}")
print(f"测试集 Macro-F1:{macro_f1:.3f}")
print("测试集混淆矩阵:")
print(confusion_matrix(y_test, y_pred, labels=clf.classes_))

为什么不只看准确率?

因为整体准确率会把所有样本放在一起统计。如果某个类别占比很高,即使模型对其他类别识别得不好,准确率也可能看起来还行。这里额外看 Balanced Accuracy 和 Macro-F1,能更全面地观察各类别的识别情况。

训练集准确率明显高于测试集准确率时,可能存在过拟合;但一次随机划分只能作为初步诊断,不能直接下定论。正式比较模型时,最好再做交叉验证,并固定好数据划分和评估流程。

画出决策边界

# 在特征平面上生成网格,并让模型预测每个网格点的类别
xx, yy = np.meshgrid(
    np.linspace(X[:, 0].min() - 1, X[:, 0].max() + 1, 300),
    np.linspace(X[:, 1].min() - 1, X[:, 1].max() + 1, 300)
)
grid = np.c_[xx.ravel(), yy.ravel()].astype(np.float32)
Z = clf.predict(grid).astype(int).reshape(xx.shape)

plt.figure(figsize=(10, 7))
plt.contourf(xx, yy, Z, levels=np.arange(-0.5, 3.5, 1),
             cmap=cmap, norm=norm, alpha=0.24, antialiased=False)
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=cmap, norm=norm,
            edgecolors="#202938", linewidths=0.35, s=30, label="训练集")
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, cmap=cmap, norm=norm,
            marker="x", s=38, linewidths=1.0, label="测试集")
plt.title("图2:决策树的分类区域与测试样本")
plt.xlabel("特征 1")
plt.ylabel("特征 2")
plt.legend()
plt.tight_layout()
plt.show()

这张图最值得观察的是决策区域的形状。普通决策树每次只按一个特征做阈值切分,因此在二维平面上会形成横向、纵向的矩形区域,看起来像阶梯。

边界比较复杂,不一定意味着模型更好。树越深,越容易把训练数据中的局部波动也切分出来。要结合测试集指标一起看,不能只凭图形是否贴合样本就判断模型优劣。

哪些切分真正带来了收益?

每次分裂的局部不纯度下降可以衡量这次切分本身有多有效。不过,如果一个节点只有很少的样本,即使局部下降很大,对整棵树整体分类结果的贡献也未必很大。

所以这张图展示的是“全局加权不纯度下降”:先计算当前节点的基尼下降,再乘以该节点样本数占训练样本总数的比例。这样更适合比较不同层级、不同样本规模的切分。

# 收集每个内部节点的全局加权基尼下降
weighted_drops = []
split_labels = []
n_total = clf.root.num_samples

def compute_drop(node, depth=0):
    if node isNoneor node.feature_index isNone:
        return

    left = node.left
    right = node.right
    n = node.num_samples
    local_delta = node.gini - (
        left.num_samples * left.gini + right.num_samples * right.gini
    ) / n
    weighted_delta = (n / n_total) * local_delta

    weighted_drops.append(weighted_delta)
    split_labels.append(
        f"深度{depth}:f{node.feature_index} <= {node.threshold:.2f}(n={n})"
    )
    compute_drop(left, depth + 1)
    compute_drop(right, depth + 1)

compute_drop(clf.root)

# 从贡献大到小排序,让更重要的切分显示在上方
order = np.argsort(weighted_drops)[::-1]
plot_values = np.asarray(weighted_drops)[order]
plot_labels = [split_labels[i] for i in order]

plt.figure(figsize=(12, max(5, 0.34 * len(plot_values))))
bar_colors = [class_colors[i % len(class_colors)] for i in range(len(plot_values))]
plt.barh(np.arange(len(plot_values)), plot_values, color=bar_colors, alpha=0.92)
plt.yticks(np.arange(len(plot_values)), plot_labels)
plt.xlabel("全局加权基尼不纯度下降")
plt.title("图3:不同分裂对整棵树的贡献")
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()

条形越长,说明这次分裂对整棵树的不纯度下降贡献越大。它可以帮助我们理解模型是怎样逐步划分空间的,但不能直接解释成“这个特征在现实世界里更重要”——它反映的是当前数据和当前这棵树的学习结果。

特征重要性怎么看?

前面每个节点的加权收益,按使用的特征累加起来,就能得到树的特征重要性。代码里已经在 fit() 时计算好,并做了归一化。

# 图4:特征重要性
feat_idx = np.arange(clf.n_features_)
importance_colors = [class_colors[i % len(class_colors)] for i in feat_idx]

plt.figure(figsize=(7, 4.5))
bars = plt.bar(feat_idx, clf.feature_importances_, color=importance_colors, width=0.62)
plt.xticks(feat_idx, [f"特征 {i + 1}"for i in feat_idx])
plt.ylabel("归一化重要性")
plt.title("图4:决策树的特征重要性")
for bar, value in zip(bars, clf.feature_importances_):
    plt.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.01,
             f"{value:.3f}", ha="center", va="bottom")
plt.ylim(0, max(0.1, clf.feature_importances_.max() * 1.18))
plt.tight_layout()
plt.show()

重要性越高,表示这个特征在当前树中贡献了更多的加权不纯度下降。但要记住两件事:特征重要性不代表因果关系;当特征之间高度相关,或者某些特征有很多可选切分点时,重要性也可能出现偏向。因此,最好把它当作模型诊断线索,而不是单独用来证明某个特征“决定了结果”。

叶节点里还剩下多少不同类别?

走到叶节点后,树会输出该节点里数量最多的类别。不过,叶节点不一定全是同一类:如果受到最大深度、最小叶节点样本数等条件限制,节点里仍然可能有不同类别的样本。

下面统计的是训练集经过树结构后,每个叶节点里各类别的样本数量。注意,它展示的是训练数据的分布,并不等于测试集表现。

# 收集叶节点
leaf_infos = []

def collect_leaves(node):
    if node isNone:
        return
    if node.feature_index isNone:
        leaf_infos.append(node)
    else:
        collect_leaves(node.left)
        collect_leaves(node.right)

collect_leaves(clf.root)

leaf_counts = np.array([
    leaf.num_samples_per_class.numpy() for leaf in leaf_infos
])
n_leaves = leaf_counts.shape[0]
ind = np.arange(n_leaves)
bottom = np.zeros(n_leaves)
colors_stack = class_colors[:leaf_counts.shape[1]]

plt.figure(figsize=(max(10, n_leaves * 0.42), 5.5))
for cls in range(leaf_counts.shape[1]):
    plt.bar(ind, leaf_counts[:, cls], bottom=bottom,
            color=colors_stack[cls], label=f"类别 {clf.classes_[cls]}")
    bottom += leaf_counts[:, cls]

plt.xlabel("叶节点(按从左到右的遍历顺序)")
plt.ylabel("训练样本数")
plt.title("图5:各叶节点中的类别构成")
plt.xticks(ind, [f"L{i}"for i in ind], rotation=0)
plt.legend()
plt.tight_layout()
plt.show()

如果某个叶节点里混合了多个类别,说明这个区域还存在一定的分类不确定性。如果每个叶节点都非常纯,也不能立刻认定模型很好:树可能把训练集记得太细,还是得回到测试集指标和交叉验证结果上判断。

到这里,决策树的基本训练、预测和可视化就串起来了。我们用的是 CART 分类树:每次做二叉切分,用基尼不纯度下降挑选特征和阈值,再递归构建左右子树。

这份代码为了教学做了简化。它不处理输入特征中的缺失值,也没有实现后剪枝、类别特征专用切分、样本权重和并行计算;特征重要性也只是树内部的一种统计量。真实项目中,建议先用 sklearn.tree.DecisionTreeClassifier 建立可靠基线,再根据业务需求决定是否需要自定义实现。

调参时,可以先从 max_depth 和 min_samples_leaf 开始。树深度增大,模型通常能拟合更复杂的边界,但过拟合风险也会上升;增大叶节点最小样本数,会让划分更保守。不要只为了提高训练集准确率不断加深树,而要观察测试集表现,必要时通过交叉验证比较不同参数。

最后

还有个容易忽略的细节:决策树一般不要求像 KNN、线性模型那样先做标准化,因为它主要根据特征排序和阈值来切分。

不过,缺失值、异常数据、标签错误和训练测试分布差异,仍然可能明显影响结果。

【声明】内容源于网络
0
0
机器学习和人工智能AI
让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
内容 437
粉丝 0
机器学习和人工智能AI 让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
总阅读9.2k
粉丝0
内容437