You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于决策树数据挖掘技术的肿瘤良恶性判别开发指导请求

决策树实现开发指导(肿瘤良恶性分类)

一、核心思路:先从节点结构入手,别着急搞类继承

你之前考虑的父决策类+属性子类方案,对于入门级决策树来说属于过度设计,反而会放大指针使用的复杂度。先从最基础的节点结构搭起,逻辑会更清晰。

1. 定义决策树节点结构体

用struct足够覆盖需求,节点分两种类型:

  • 决策节点:存储属性索引、分割阈值、左右子节点指针
  • 叶子节点:存储最终分类结果

示例代码(C++):

#include <string>
#include <vector>

// 分类结果枚举
enum class TumorType { BENIGN, MALIGNANT };

// 决策树节点结构体
struct TreeNode {
    bool is_leaf = false;
    // 决策节点专属字段
    int feature_idx = -1; // 属性对应的CSV列索引
    double threshold = 0.0;
    TreeNode* left = nullptr; // 满足条件分支(如<=阈值)
    TreeNode* right = nullptr; // 不满足条件分支(如>阈值)
    // 叶子节点专属字段
    TumorType result;

    // 构造函数简化初始化
    TreeNode(int idx, double thresh) : is_leaf(false), feature_idx(idx), threshold(thresh) {}
    TreeNode(TumorType res) : is_leaf(true), result(res) {}
    // 析构函数:递归释放子节点,避免内存泄漏
    ~TreeNode() {
        delete left;
        delete right;
    }
};

二、CSV数据转模型可用格式

你已经完成CSV读取,现在需要把每行数据转换成特征向量+标签的结构:

  • 特征向量:将所有数值型属性(如Radius Mean、Texture Mean)转成vector<double>
  • 标签:将诊断结果(B/M)转成TumorType枚举

示例转换逻辑:

// 假设CSVReader返回vector<CSVLine>,CSVLine提供getCol(int idx)获取列值
struct DataSample {
    vector<double> features;
    TumorType label;
};

vector<DataSample> convertCSVToSamples(const vector<CSVLine>& csv_lines) {
    vector<DataSample> samples;
    for (size_t i = 1; i < csv_lines.size(); ++i) { // 跳过表头行
        DataSample sample;
        // 读取第2到第31列(假设30个数值属性从第2列开始)
        for (int col = 2; col <= 31; ++col) {
            sample.features.push_back(stod(csv_lines[i].getCol(col)));
        }
        // 读取最后一列诊断结果
        string diag = csv_lines[i].getCol(csv_lines[i].getColCount() - 1);
        sample.label = (diag == "M") ? TumorType::MALIGNANT : TumorType::BENIGN;
        samples.push_back(sample);
    }
    return samples;
}

三、实现决策树核心构建逻辑

决策树构建的关键是选择最优分割属性和阈值,优先用基尼系数(Gini Impurity),实现更简单:

1. 计算基尼系数

double calculateGini(const vector<DataSample>& samples) {
    if (samples.empty()) return 0.0;
    int benign_count = 0, malignant_count = 0;
    for (const auto& s : samples) {
        s.label == TumorType::BENIGN ? benign_count++ : malignant_count++;
    }
    double p_benign = (double)benign_count / samples.size();
    double p_malignant = (double)malignant_count / samples.size();
    return 1 - (p_benign*p_benign + p_malignant*p_malignant);
}

2. 按属性和阈值分割样本,计算分割后基尼系数

pair<pair<vector<DataSample>, vector<DataSample>>, double> splitSamples(
    const vector<DataSample>& samples, int feature_idx, double threshold) {
    vector<DataSample> left, right;
    for (const auto& s : samples) {
        s.features[feature_idx] <= threshold ? left.push_back(s) : right.push_back(s);
    }
    double gini_left = calculateGini(left);
    double gini_right = calculateGini(right);
    double weighted_gini = (left.size()*gini_left + right.size()*gini_right) / samples.size();
    return {{left, right}, weighted_gini};
}

3. 遍历所有属性,找到最优分割点

pair<int, double> findBestSplit(const vector<DataSample>& samples) {
    int best_feature = -1;
    double best_threshold = 0.0;
    double best_gini = 1.0; // 初始设为最大基尼系数

    int num_features = samples[0].features.size();
    for (int f_idx = 0; f_idx < num_features; ++f_idx) {
        // 收集当前属性的所有唯一值作为候选阈值
        vector<double> values;
        for (const auto& s : samples) values.push_back(s.features[f_idx]);
        sort(values.begin(), values.end());
        values.erase(unique(values.begin(), values.end()), values.end());

        // 遍历候选阈值,找基尼系数最小的分割点
        for (size_t i = 0; i < values.size()-1; ++i) {
            double threshold = (values[i] + values[i+1]) / 2;
            auto [split, gini] = splitSamples(samples, f_idx, threshold);
            if (gini < best_gini) {
                best_gini = gini;
                best_feature = f_idx;
                best_threshold = threshold;
            }
        }
    }
    return {best_feature, best_threshold};
}

4. 递归构建决策树

TreeNode* buildTree(const vector<DataSample>& samples) {
    // 终止条件1:所有样本属于同一类别,返回叶子节点
    bool all_same = true;
    TumorType first_label = samples[0].label;
    for (const auto& s : samples) {
        if (s.label != first_label) {
            all_same = false;
            break;
        }
    }
    if (all_same) return new TreeNode(first_label);

    // 统计样本类别数量
    int benign_count = 0, malignant_count = 0;
    for (const auto& s : samples) {
        s.label == TumorType::BENIGN ? benign_count++ : malignant_count++;
    }

    // 终止条件2:无法分割,返回占多数的类别
    if (benign_count == 0 || malignant_count == 0) {
        return new TreeNode(benign_count > malignant_count ? TumorType::BENIGN : TumorType::MALIGNANT);
    }

    // 找到最优分割点
    auto [best_feature, best_threshold] = findBestSplit(samples);
    if (best_feature == -1) {
        return new TreeNode(benign_count > malignant_count ? TumorType::BENIGN : TumorType::MALIGNANT);
    }

    // 分割样本并递归构建子树
    auto [split, _] = splitSamples(samples, best_feature, best_threshold);
    auto [left_samples, right_samples] = split;

    TreeNode* node = new TreeNode(best_feature, best_threshold);
    node->left = buildTree(left_samples);
    node->right = buildTree(right_samples);

    return node;
}

四、实现预测逻辑

用构建好的决策树对新样本做预测:

TumorType predict(TreeNode* root, const DataSample& sample) {
    if (root->is_leaf) return root->result;
    return sample.features[root->feature_idx] <= root->threshold 
        ? predict(root->left, sample) 
        : predict(root->right, sample);
}

五、指针问题简化建议

  1. 先跑通上述基于结构体的指针逻辑,每个节点只管理自身的左右子节点,析构函数递归释放即可,逻辑清晰。
  2. 后续若要扩展(如支持不同分割指标),再考虑用策略模式,而非属性继承父类,避免指针复杂度飙升。

六、测试与优化

  • 将CSV数据按8:2分成训练集和测试集,用训练集构建树,测试集验证准确率
  • 加入剪枝逻辑(如限制树的最大深度),避免过拟合

内容的提问来源于stack exchange,提问作者Dylan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 19:01:17