我有二元决策树,它将决定许多属性,所讨论的肿瘤是良性还是恶性。这些属性的数据存储在链表中,用于每个患者。我很难弄清楚如何通过决策树处理存储在链表中的数据,以获得良性或恶性的预期结果。
截至目前,我已经为决策树和链表编写了所有代码。链表填充了从.csv文件中读入的数据。我创建了一个头文件名 ProcessingPatientData.h
在这个文件中,我想声明一个void process_data函数,它将决策树和链表作为参数,然后通过树运行数据以确定每个患者的结果。
我还想声明其他功能,但这不是我的问题。我在弄清楚如何将我的链表作为参数包含时遇到问题
您将在下面找到我的链表和决策树代码,以及到目前为止我对函数"process_data"的代码。
'
#include "CancerLogic.h"
#include "DecisionNode.h"
#include "LinkedList.h"
#include "ProcessPatientData.h"
typedef struct
{
int id;
int clump_thickness;
int uniformity_of_cell_size;
int uniformity_of_cell_shape;
int marginal_adhesion;
int single_epithelial_cell_size;
int bare_nuclei;
int bland_chromatin;
int normal_nucleoli;
int mitoses;
int class_;
}Patients;
void cancer_tree()
{
// Create the tree
// Terminal Nodes
DecisionNode<CancerLogic::patient_processing_data> malignant_node(CancerLogic::malignant);
DecisionNode<CancerLogic::patient_processing_data> benign_node(CancerLogic::benign);
// Right side (true) nodes
DecisionNode<CancerLogic::patient_processing_data> ma_greater_than_3_r_node(CancerLogic::ma_greater_than_3_r, &malignant_node, &benign_node);
DecisionNode<CancerLogic::patient_processing_data> ct_greater_than_5_r_node(CancerLogic::ct_greater_than_5_r, &malignant_node, &benign_node);
DecisionNode<CancerLogic::patient_processing_data> ma_greater_than_5_r_node(CancerLogic::ma_greater_than_5_r, &malignant_node, &benign_node);
DecisionNode<CancerLogic::patient_processing_data> csize_greater_than_3_r_node(CancerLogic::csize_greater_than_3_r, &ma_greater_than_5_r_node, &malignant_node);
DecisionNode<CancerLogic::patient_processing_data> ct_greater_than_6_r_node(CancerLogic::ct_greater_than_6_r, &malignant_node, &csize_greater_than_3_r_node);
DecisionNode<CancerLogic::patient_processing_data> bn_greater_than_2_r_node(CancerLogic::bn_greater_than_2_r, &ct_greater_than_6_r_node, &ma_greater_than_3_r_node);
DecisionNode<CancerLogic::patient_processing_data> csize_greater_than_4_r_node(CancerLogic::csize_greater_than_4_r, &malignant_node, &bn_greater_than_2_r_node);
DecisionNode<CancerLogic::patient_processing_data> cshape_greater_than_2_r_node(CancerLogic::cshape_greater_than_2_r, &csize_greater_than_4_r_node, &ct_greater_than_5_r_node);
// Left side (false) nodes
DecisionNode<CancerLogic::patient_processing_data> ma_greater_than_3_l_node(CancerLogic::ma_greater_than_3_l, &malignant_node, &benign_node);
DecisionNode<CancerLogic::patient_processing_data> bc_greater_than_2_l_node(CancerLogic::bc_greater_than_2_l, &malignant_node, &ma_greater_than_3_l_node);
DecisionNode<CancerLogic::patient_processing_data> ct_greater_than_3_l_node(CancerLogic::ct_greater_than_3_l, &bc_greater_than_2_l_node, &benign_node);
DecisionNode<CancerLogic::patient_processing_data> bn_greater_than_3_l_node(CancerLogic::bn_greater_than_3_l, &ct_greater_than_3_l_node, &benign_node);
DecisionNode<CancerLogic::patient_processing_data> csize_greater_than_2_node(CancerLogic::csize_greater_than_2, &cshape_greater_than_2_r_node, &bn_greater_than_3_l_node);
process_data<CancerLogic::patient_processing_data>(&csize_greater_than_2_node, ); <----
class CancerLogic
{
public:
struct patient_processing_data
{
bool csize_greater_than_2_l;
bool bn_greater_than_3_l;
bool ct_greater_than_3_l;
bool bc_greater_than_2_l;
bool ma_greater_than_3_l;
bool csize_greater_than_2_r;
bool cshape_greater_than2_r;
bool ct_greater_than_5_r;
bool csize_greater_than_4_r;
bool bn_greater_than_2_r;
bool ma_greater_than_3_r;
bool ct_greater_than_6_r;
bool csize_greater_than_3_r;
bool ma_greater_than_5_r;
bool result;
std::string result_string;
patient_processing_data(): csize_greater_than_2_l(false), bn_greater_than_3_l(false),
ct_greater_than_3_l(false), bc_greater_than_2_l(false),
ma_greater_than_3_l(false),
csize_greater_than_2_r(false),
cshape_greater_than2_r(false),
ct_greater_than_5_r(false),
csize_greater_than_4_r(false),
bn_greater_than_2_r(false),
ma_greater_than_3_r(false), ct_greater_than_6_r(false),
csize_greater_than_3_r(false),
ma_greater_than_5_r(false), result(false)
{
}
};
static bool malignant(patient_processing_data& data)
{
data.result = true;
data.result_string = "Malignant";
return data.result;
}
static bool benign(patient_processing_data& data)
{
data.result = false;
data.result_string = "Benign";
return data.result;
}
static bool csize_greater_than_4_r(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool ma_greater_than_5_r(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool csize_greater_than_3_r(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool ct_greater_than_6_r(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool bn_greater_than_2_r(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool ma_greater_than_3_r(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool cshape_greater_than_2_r(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool ct_greater_than_5_r(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool csize_greater_than_2(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool bn_greater_than_3_l(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool ct_greater_than_3_l(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool bc_greater_than_2_l(patient_processing_data& data)
{
data.result = true;
return data.result;
}
static bool ma_greater_than_3_l(patient_processing_data& data)
{
data.result = true;
return data.result;
}
};
#include "LinkedList.h"
#include "Node.h"
PatientList::PatientList()
{
head_ = nullptr;
tail_ = nullptr;
}
void PatientList::insert(int ct, int csize, int cshape, int ma, int bn, int bc)
{
Node* new_node_ = new Node(ct, csize, cshape, ma, bn, bc);
if(head_ == nullptr)
{
head_ = new_node_;
tail_ = new_node_;
return;
}
new_node_->previous = tail_;
tail_->next = new_node_;
tail_ = new_node_;
}
'
#pragma once
#include "DecisionNode.h"
#include "LinkedList.h"
#include <iostream>
template<typename T>
void process_data(DecisionNode<typename T::processing_data>* d_tree, PatientList<typename T::processing_data>& patient_data)
{
for(auto& data : patient_data)
{
d_tree->process(data);
}
}
template<typename T>
void print_data(const PatientList<typename T::processing_data>& patient_data)
{
for(const auto& data : patient_data)
{
std::cout << data << std::endl;
}
}
很抱歉所有代码,我只是想确保我有尽可能多的关于我的问题的信息,让每个人都试图了解我的问题是什么。任何帮助将不胜感激。
二元决策树非常简单。在树中的每个节点上,您要么有一个决策(如果是叶节点),要么有一个交汇点。要在交汇点向左或向右遍历需要一个分类器。
分类器的一个示例是检查数据中的特定值并将其与值进行比较。因此,让我们取树中的第一个交汇点(参考原始问题中的表):
细胞大小的均匀性 <= 2
然后,它继续根据该测试是真还是假来选择一条路径或另一条路径。这是一个分类器。
我不会尝试修复您不完整的代码,而是逐步引导您了解如何C++自己解决这个问题。目的不是给你代码,而是向你展示一种分解任务的方法。如果你注意了,那么在某个时候你会大喊"啊哈!",然后去修复你自己的程序。
步骤 1:定义数据源
在考虑创建树之前,您需要弄清楚分类器的外观。在此之前,您需要一些数据。我将主要复制您的结构,并进行一些细微的更改:
struct LabResults
{
int clump_thickness;
int uniformity_of_cell_size;
int uniformity_of_cell_shape;
int marginal_adhesion;
int single_epithelial_cell_size;
int bare_nuclei;
int bland_chromatin;
int normal_nucleoli;
int mitoses;
};
enum class Diagnosis
{
Unknown,
Benign,
Malignant
};
struct Patient
{
int id;
LabResults results;
Diagnosis diagnosis;
};
现在,我们有一个保存患者信息的结构,实验室结果和诊断明确分开。尽管Patient
存储了一堆信息,但我们只需要LabResults
即可执行分类。
步骤 2:定义分类器
您现在可以考虑分类器本身,因为您现在知道它需要检查LabResults
对象。
让我们更进一步,使分类器通用。我们可以为此定义一个抽象类模板,它将对任何数据进行操作。这使得它可以重用,如果你想为其他东西构建决策树:
template<class Data>
class Classifier
{
public:
virtual ~Classifier() = default;
virtual bool Classify(const Data&) const = 0;
};
这就是它所需要的!我们将能够对其进行子类化以编写特定的分类器,但主接口将通过一个Classifier<LabResults>*
值,我们将在其上调用多态Classify
方法。该方法接受对一个患者的LabResults
数据的引用。
所以现在,让我们构建一个非常有用的分类器。这将是整个决策树唯一需要的决策树!同样,一切都保持通用。我们先看看,然后我再解释一下:
// Classifier to perform binary comparison
template<class Data, class Value, class Compare>
class BinaryClassifier : public Classifier<Data>
{
public:
BinaryClassifier(Value LabResults::*member, Value value)
: mMember(member)
, mValue(value)
{ }
bool Classify(const Data& data) const override
{
return Compare()(data.*mMember, mValue);
}
private:
Value LabResults::*mMember;
Value mValue;
};
此分类器的构造函数使用指向成员的指针语义来定位Data
类型的任何成员。它还模板化该成员的数据类型(通过Value
),尽管在您的情况下,这总是int
。最后,它提供了一个函子Compare
函子,它提供了一种指定要执行哪种比较操作的方法。您的决策树恰好只需要<=
,可以方便地std::less_equal
使用。
如果您不熟悉指向成员的指针,请搜索它。函子也是如此。您将看到这两种方法都在Classify
方法中使用。整洁!
因此,我们现在可以创建一个分类器。让我们为clump_thickness <= 5
做一个:
using LessEqualClassifier = BinaryClassifier<LabResults, int, std::less_equal<int>>;
LessEqualClassifier classifier(&LabResults::clump_thickness, 5);
第 3 步:测试分类器!
这就足够编码了...这是停止并确保分类器实际工作的最佳时机。让我们快速输入一些数据并对其运行分类器:
int main()
{
using LessEqualClassifier = BinaryClassifier<LabResults, int, std::less_equal<int>>;
LessEqualClassifier classifier(&LabResults::clump_thickness, 5);
Patient patients[] {
{ 1000025, { 5, 1, 1, 1, 2, 1, 3, 1, 1 } },
{ 1002945, { 5, 4, 4, 5, 7, 10, 3, 2, 1 } },
{ 1015425, { 3, 1, 1, 1, 2, 2, 3, 1, 1 } },
{ 1016277, { 6, 8, 8, 1, 3, 4, 3, 7, 1 } },
{ 1017023, { 4, 1, 1, 3, 2, 1, 3, 1, 1 } },
{ 1017122, { 8, 10, 10, 8, 7, 10, 9, 7, 1 } },
};
std::cout << std::boolalpha;
for (auto& p : patients)
{
std::cout << "Patient " << p.id << ": ";
std::cout << classifier.Classify(p.results) << "n";
}
}
输出:
Patient 1000025: true
Patient 1002945: true
Patient 1015425: true
Patient 1016277: false
Patient 1017023: true
Patient 1017122: false
是的,您确实可以看到这正在按预期运行。我们几乎没有编写任何代码,但我们已经有一个通用分类器,可以在LabResults的任何成员上工作。继续尝试与其他成员和价值观一起尝试。
步骤 4:定义决策树
使用工作分类器,我们现在可以专注于决策树本身。正如我在开头所说,树中的节点要么是最终决策,要么是交汇点。让我们从树的类开始:
template <class Data, class Result>
class DecisionTree
{
public:
class Node;
DecisionTree(Node&& definition)
: mRoot(std::move(definition))
{ }
Result Run(const Data& data) const
{
return mRoot.Run(data);
}
private:
Node mRoot;
};
此树只有一个根节点,它可以在该节点上调用Run
方法(我们将在稍后定义)。这将执行决策树并返回结果。数据的树类型为DecisionTree<LabResults, Diagnosis>
。因此,这意味着您将为患者传递LabResults
,并获得Diagnosis
值。
现在让我们定义节点类本身:
template <class Data, class Result>
class DecisionTree<Data, Result>::Node
{
public:
typedef std::unique_ptr<Classifier<Data>> ClassifierPtr;
typedef std::unique_ptr<Node> NodePtr;
Node(const Result& result)
: mResult(result)
{ }
Node(ClassifierPtr&& classifier, Node&& trueCase, Node&& falseCase)
: mResult()
, mClassifier(std::move(classifier))
, mTrueCase(std::make_unique<Node>(std::move(trueCase)))
, mFalseCase(std::make_unique<Node>(std::move(falseCase)))
{ }
private:
friend class DecisionTree;
Result Run(const Data& data) const
{
return mClassifier
? (mClassifier->Classify(data) ? mTrueCase : mFalseCase)->Run(data)
: mResult;
}
private:
Result mResult;
ClassifierPtr mClassifier;
NodePtr mTrueCase;
NodePtr mFalseCase;
};
您可以看到有两种可能的方法可以构造DecisionTree::Node
。您可以指定Result
,也可以提供指向分类器的指针以及表示该分类器的两种可能结果的树节点。如果分类器为 null,则节点是叶,否则是联结。
如果分类器存在,Run
方法将执行分类器,然后根据结果递归运行相应的子树。如果没有分类器,则返回存储在该节点上的Result
。
这几乎是整个系统。到现在为止,你会渴望真正开始建造你的树,所以让我们这样做。
步骤 5:构造一棵树
这种设计的好处是我们可以一次性构建树,嵌套两个不同的节点构造函数来定义决策结构。但你不必这样做。您可能希望在某处的文件中定义它,解析它,然后动态构建树。这取决于你。
不过,与往常一样,从简单的事情开始。您希望尽早分阶段测试代码。
首先,让我们用一种更简洁的方式来创建分类器。因为请注意,它需要是一个std::unique_ptr
,所以我们将在任何地方写std::make_unique<LessEqualClassifier>(...)
。或者没有那个别名:std::make_unique<BinaryClassifier<LabResults, int, std::less_equal<int>>(...)
...啦!
这个简单的函数模板怎么样:
// Classifier factories make life easier!
template <class Data, class Value>
std::unique_ptr<Classifier<Data>> LessEqual(Value Data::*member, Value value)
{
typedef BinaryClassifier<LabResults, Value, std::less_equal<Value>> LessEqualType;
return std::make_unique<LessEqualType>(member, value);
}
现在,让我们在树中构建左侧分支,将右侧分支诊断为"未知"。这只会让事情起步,所以有一些东西需要测试:
DecisionTree<LabResults, Diagnosis> cancerDiagnoser({
LessEqual(&LabResults::uniformity_of_cell_size, 2),{
// uniformity_of_cell_size <= 2
LessEqual(&LabResults::bare_nuclei, 3),{
Diagnosis::Benign
},{
LessEqual(&LabResults::clump_thickness, 3),{
Diagnosis::Benign
},{
LessEqual(&LabResults::bland_chromatin, 2),{
LessEqual(&LabResults::marginal_adhesion, 3),{
Diagnosis::Malignant
},{
Diagnosis::Benign
}
},{
Diagnosis::Malignant
}
}
}
},{
// uniformity_of_cell_size > 2
//// TODO !!!
Diagnosis::Unknown
}
});
第 6 步:测试树!
我们希望能够输出诊断。执行此操作的一种干净方法是为其定义流输出运算符。这样,您就可以将Diagnosis
值直接发送到std::cout
并让它输出一些友好的内容:
std::ostream& operator<<(std::ostream& s, const Diagnosis& d)
{
std::array<const char*, 3> names{ "Unknown", "Benign", "Malignant" };
return s << names.at(static_cast<int>(d));
}
现在更新前面的测试循环。唯一需要的更改是运行决策树,存储结果并输出它:
for (auto& p : patients)
{
p.diagnosis = cancerDiagnoser.Run(p.results);
std::cout << "Patient " << p.id << ": " << p.diagnosis << "n";
}
输出:
Patient 1000025: Benign
Patient 1002945: Unknown
Patient 1015425: Benign
Patient 1016277: Unknown
Patient 1017023: Benign
Patient 1017122: Unknown
看起来我们没有用这些数据得到任何恶性诊断。尝试使用树那一侧恶性的数据。你可以发明一些东西。实际上,您可以制作一些虚假数据来测试树:
Patient unitTests[] {
{ 1001, { 0, 2, 0, 0, 0, 3, 0, 0, 0 }, Diagnosis::Benign },
{ 1002, { 3, 2, 0, 0, 0, 4, 0, 0, 0 }, Diagnosis::Benign },
{ 1003, { 4, 1, 0, 0, 0, 4, 2, 0, 0 }, Diagnosis::Malignant },
{ 1004, { 4, 1, 0, 4, 0, 4, 2, 0, 0 }, Diagnosis::Benign },
{ 1005, { 4, 2, 0, 3, 0, 4, 2, 0, 0 }, Diagnosis::Malignant },
};
for (auto& p : unitTests)
{
auto diagnosis = cancerDiagnoser.Run(p.results);
bool pass = diagnosis == p.diagnosis;
std::cout << "Patient " << p.id << ": " << diagnosis;
std::cout << " - " << (pass ? "PASS" : "FAIL") << "n";
}
输出:
Patient 1001: Benign - PASS
Patient 1002: Benign - PASS
Patient 1003: Malignant - PASS
Patient 1004: Benign - PASS
Patient 1005: Malignant - PASS
在构建树的其余部分时,您需要包含更多测试以确保逻辑正确。因为你真的不想因为软件错误而误诊癌症!!
总结
到目前为止,您已经拥有了所需的所有工具。这是故意未完成的代码。如果这是为了学校作业,而你决定直接复制它并完成它,我想人们会怀疑它不是你的!
相反,你应该做的是研究实际发生的事情,并将概念应用到你自己的程序中。如果你只从这个答案中拿走一件事,我希望它是通过一系列小的、合乎逻辑的、可测试的步骤从无到有构建程序的方法。