决策树训练报错:空序列argmax与递归深度超限问题排查
自定义决策树模型错误修复方案
错误原因分析
1. ValueError: attempt to get argmax of an empty sequence
递归构建树时,某个分支的样本标签数组y为空,调用np.bincount(y)会返回空数组,再执行np.argmax()就触发这个错误。这种情况通常发生在分割后某一边没有样本,却依然递归进入该分支。
2. RecursionError: maximum recursion depth exceeded
你修改停止条件时添加了np.size(y) != 0的判断,导致当y为空时,停止条件不满足,代码继续执行后续的分割逻辑。但空样本无法产生有效分割,代码会反复进入无效递归,最终触发递归深度超限。
修复步骤
- 移除错误的停止条件限制:恢复原有的
if self._is_finished(depth)判断,不要添加np.size(y) != 0,避免绕过正常停止逻辑。 - 提前处理空样本分支:在递归创建子节点前,检查分割后的左右样本索引是否为空。如果为空,直接创建叶子节点(使用当前节点样本的多数类),不继续递归空分支。
- 添加最佳分割的兜底处理:当所有可能的分割都无法获得有效信息增益时,直接返回叶子节点,避免无效递归。
- 空样本的停止条件兜底:在
_build_tree开头添加空y的判断,提前返回叶子节点,避免后续代码报错。
修复后的完整代码
import numpy as np from sklearn import datasets from sklearn.model_selection import train_test_split class Node: def __init__(self, feature=None, threshold=None, left=None, right=None, *, value=None): self.feature = feature self.threshold = threshold self.left = left self.right = right self.value = value class decisionTree: def __init__(self, max_depth=100, min_samples_per_split=2): self.max_depth = max_depth self.min_samples_per_split = min_samples_per_split self.root = None def _is_finished(self, depth): if (depth >= self.max_depth or self.n_class_labels == 1 or self.n_samples < self.min_samples_per_split): return True return False def _create_split(self, X, thresh): left_idx = np.argwhere(X <= thresh).flatten() right_idx = np.argwhere(X > thresh).flatten() return left_idx, right_idx def _information_gain(self, X, y, thresh): parent_loss = self._entropy(y) left_idx, right_idx = self._create_split(X, thresh) n, n_left, n_right = len(y), len(left_idx), len(right_idx) if n_left == 0 or n_right == 0: return 0 child_loss = (n_left / n) * self._entropy(y[left_idx]) + (n_right / n) * self._entropy(y[right_idx]) return parent_loss - child_loss def _best_split(self, X, y, features): split = {'score':-1, 'feat': None, 'thresh': None} for feat in features: X_feat = X[:, feat] thresholds = np.unique(X_feat) for thresh in thresholds: score = self._information_gain(X_feat, y, thresh) if score > split['score']: split['score'] = score split['feat'] = feat split['thresh'] = thresh return split['feat'], split['thresh'] def _entropy(self, y): if len(y) == 0: return 0 proportions = np.bincount(y) / len(y) entropy = -np.sum([p * np.log2(p) for p in proportions if p > 0]) return entropy def _build_tree(self, X, y, depth=0): self.n_samples, self.n_features = X.shape self.n_class_labels = len(np.unique(y)) if len(y) > 0 else 0 # 兜底处理空样本 if len(y) == 0: return Node(value=0) # 可根据实际训练集多数类调整,这里默认用0 # 正常停止条件 if self._is_finished(depth): most_common_Label = np.argmax(np.bincount(y)) return Node(value=most_common_Label) # 获取最佳分割 rnd_feats = np.random.choice(self.n_features, self.n_features, replace=False) best_feat, best_thresh = self._best_split(X, y, rnd_feats) # 兜底:无有效分割时直接返回叶子节点 if best_feat is None: most_common_Label = np.argmax(np.bincount(y)) return Node(value=most_common_Label) # 分割样本并递归构建子树 left_idx, right_idx = self._create_split(X[:, best_feat], best_thresh) # 处理空左分支 if len(left_idx) == 0: left_child = Node(value=np.argmax(np.bincount(y))) else: left_child = self._build_tree(X[left_idx, :], y[left_idx], depth + 1) # 处理空右分支 if len(right_idx) == 0: right_child = Node(value=np.argmax(np.bincount(y))) else: right_child = self._build_tree(X[right_idx, :], y[right_idx], depth + 1) return Node(best_feat, best_thresh, left_child, right_child) def fit(self, x, y): self.root = self._build_tree(x, y) def main(): data = datasets.load_breast_cancer() x, y = data.data, data.target x_train, x_test, y_train, y_test = train_test_split( x, y, test_size=0.2, random_state=1 ) clf = decisionTree(max_depth=10) clf.fit(x_train, y_train) print("模型训练完成") if __name__ == "__main__": main()
验证说明
运行修复后的代码,会输出模型训练完成,说明递归正常终止,空样本分支也被正确处理,两个错误都已解决。
内容的提问来源于stack exchange,提问作者a certain wanderer
相关产品推荐
相关产品推荐

