C4.5算法实现
C4.5算法简介
C4.5算法是由Ross Quinlan开发的用于产生决策树的算法。该算法是对Ross Quinlan之前开发的ID3算法的一个扩展。C4.5算法产生的决策树可以被用作分类目的,因此该算法也可以用于统计分类。
C4.5是一系列用在机器学习和数据挖掘的分类问题中的算法。它的目标是监督学习:给定一个数据集,其中的每一个元组都能用一组属性值来描述,每一个元组属于一个互斥的类别中的某一类。C4.5的目标是通过学习,找到一个从属性值到类别的映射关系,并且这个映射能用于对新的类别未知的实体进行分类。
算法解析
前面也提到了,C4.5算法是著名的ID3算法的一个扩展。简单来说,ID3算法是通过信息增益来分裂属性。
但是,我们假设这样一种情况,每个属性中每种类别都只有一个样本,那这样的属性信息熵就等于零,根据 信息增益 就 无法选择出有效的分类特征。
所以,C4.5算法对ID3算法进行了改进,选择信息增益率来分裂属性。
我们来看下面的例子:
示例数据
我们以如下的数据作为示例:
编号 | 颜色 | 根蒂 | 声音 | 纹理 | 脐部 | 触感 | 好瓜 |
---|---|---|---|---|---|---|---|
1 | 青绿 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
2 | 乌黑 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 是 |
3 | 乌黑 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
4 | 青绿 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 是 |
5 | 浅白 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
6 | 青绿 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软黏 | 是 |
7 | 乌黑 | 稍蜷 | 浊响 | 稍糊 | 稍凹 | 软黏 | 是 |
8 | 乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 硬滑 | 是 |
9 | 乌黑 | 稍蜷 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 否 |
10 | 青绿 | 硬挺 | 清脆 | 清晰 | 平坦 | 软黏 | 否 |
11 | 浅白 | 硬挺 | 清脆 | 模糊 | 平坦 | 硬滑 | 否 |
12 | 浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 软黏 | 否 |
13 | 青绿 | 稍蜷 | 浊响 | 稍糊 | 凹陷 | 硬滑 | 否 |
14 | 浅白 | 稍蜷 | 沉闷 | 稍糊 | 凹陷 | 硬滑 | 否 |
15 | 乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软黏 | 否 |
16 | 浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 硬滑 | 否 |
17 | 青绿 | 蜷缩 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 否 |
我们可以看到,上述数据集有7个属性(编号、颜色、根蒂、声音、纹理、脐部、触感),2个类别属性(是、否)。
注:编号属性在实际参与运算的过程中可以被忽略不计。
数据处理
为了方便编程,我们需要把上述数据集进行一个处理(转化为英文字符串):
- 编号:id
- 颜色:color
-
- 青绿:darkgreen
-
- 乌黑:jetblack
-
- 浅白:lightwhite
- 根蒂:pedicle
-
- 蜷缩:curled
-
- 稍蜷:slightlycurled
-
- 硬挺:stiffened
- 声音:sound
-
- 浊响:turbid
-
- 沉闷:dull
-
- 清脆:crisp
- 纹理:texture
-
- 清晰:clear
-
- 稍糊:lightblur
-
- 模糊:blur
- 脐部:umbilical
-
- 凹陷:sunken
-
- 稍凹:slightlysunken
-
- 平坦:flat
- 触感:touch
-
- 硬滑:hardslip
-
- 软黏:softsticky
- 好瓜:good
计算类别信息熵
类别信息熵表示的是所有样本中各种类别出现的不确定性之和。根据熵的概念,熵越大,不确定性就越大,把事情搞清楚所需要的信息量就越多。

Info (D) = - (8/17) * log2(8/17) - (9/17) * log2(9/17) = 0.998
解释:
8/17:好瓜行数占总行数的比例;
9/17:坏瓜行数占总行数的比例。
计算各属性信息熵
每个属性的信息熵相当于一种条件熵。它表示的是在某种属性的条件下,各种类别出现的不确定性之和。属性的信息熵越大,表示这个属性中拥有的样本类别越不“纯”。
Info (color)
= (6/17) * [-(3/6) * log2(3/6) - (3/6) * log2(3/6)]
+ (6/17) * [-(4/6) * log2(4/6) - (2/6) * log2(2/6)]
+ (5/17) * [-(1/5) * log2(1/5) - (4/5) * log2(4/5)] = 0.889Info (pedicle)
= (8/17) * [-(5/8) * log2(5/8) - (3/8) * log2(3/8)]
+ (7/17) * [-(3/7) * log2(3/7) - (4/7) * log2(4/7)]
+ (2/17) * [-1 * log21 - 0 ] = 0.585Info (sound)
= (10/17) * [-(6/10) * log2(6/10) - (4/10) * log2(4/10)]
+ (5/17) * [-(3/5) * log2(3/5) - (2/5) * log2(2/5)]
+ (2/17) * [-1 * log21 - 0 ] = 0.856Info (texture)
= (9/17) * [-(7/9) * log2(7/9) - (2/9) * log2(2/9)]
+ (5/17) * [-(1/5) * log2(1/5) - (4/5) * log2(4/5)]
+ (3/17) * [-1 * log21 - 0 ] = 0.616Info (umbilical)
= (7/17) * [-(5/7) * log2(5/7) - (2/7) * log2(2/7)]
+ (6/17) * [-(3/6) * log2(3/6) - (3/6) * log2(3/6)]
+ (4/17) * [-1 * log21 - 0 ] = 0.502Info (touch)
= (12/17) * [-(6/12) * log2(6/12) - (6/12) * log2(6/12)]
+ (5/17) * [-(2/5) * log2(2/5) - (3/5) * log2(3/5)] = 0.991
解释:(以color属性示例)
6/17:darkgreen(青绿)占总行数的比例;3/6:darkgreen(青绿)中好瓜的比例;
3/6:darkgreen(青绿)中坏瓜的比例;6/17:jetblack(乌黑)占总行数的比例;
4/6:jetblack(乌黑)中好瓜的比例;
2/6:jetblack(乌黑)中坏瓜的比例;5/17:lightwhite(浅白)占总行数的比例;
1/5:lightwhite(浅白)中好瓜的比例;
4/5:lightwhite(浅白)中坏瓜的比例;
计算信息增益
信息增益 = 熵 - 条件熵,在这里就是 类别信息熵 - 属性信息熵,它表示的是信息不确定性减少的程度。如果一个属性的信息增益越大,就表示用这个属性进行样本划分可以更好的减少划分后样本的不确定性,当然,选择该属性就可以更快更好地完成我们的分类目标。
信息增益就是ID3算法的特征选择指标。
Gain (color)
= Info (D) - Info (color) = 0.998 - 0.889 = 0.109
Gain (pedicle)
= Info (D) - Info (pedicle) = 0.998 - 0.585 = 0.413
Gain (sound)
= Info (D) - Info (sound) = 0.998 - 0.856 = 0.142
Gain (texture)
= Info (D) - Info (texture) = 0.998 - 0.616 = 0.382
Gain (umbilical)
= Info (D) - Info (umbilical) = 0.998 - 0.502 = 0.496
Gain (touch)
= Info (D) - Info (touch) = 0.998 - 0.991 = 0.007
但是我们假设这样的情况,每个属性中每种类别都只有一个样本,那这样属性信息熵就等于零,根据信息增益就无法选择出有效分类特征。所以,C4.5选择使用信息增益率对ID3进行改进。
计算属性分裂信息度量
用分裂信息度量来考虑某种属性进行分裂时分支的数量信息和尺寸信息,我们把这些信息称为属性的内在信息(instrisic information)。信息增益率用信息增益 / 内在信息,会导致属性的重要性随着内在信息的增大而减小(也就是说,如果这个属性本身不确定性就很大,那我就越不倾向于选取它),这样算是对单纯用信息增益有所补偿。
H (color) =
-(6/17) * log2 (6/17)
-(6/17) * log2 (6/17)
-(5/17) * log2 (5/17) = 1.579
H (pedicle) =
-(8/17) * log2 (8/17)
-(7/17) * log2 (7/17)
-(2/17) * log2 (2/17) = 1.401
H (sound) =
-(10/17) * log2 (10/17)
-(5/17) * log2 (5/17)
-(2/17) * log2 (2/17) = 1.332
H (texture) =
-(9/17) * log2 (9/17)
-(5/17) * log2 (5/17)
-(3/17) * log2 (3/17) = 1.445
H (umbilical) =
-(7/17) * log2 (7/17)
-(6/17) * log2 (6/17)
-(4/17) * log2 (4/17) = 1.548
H (touch) =
-(12/17) * log2 (12/17)
-(5/17) * log2 (5/17) = 0.873
解释:(以color属性为例)
6/17:darkgreen(青绿)占总行数的比例;
6/17:jetblack(乌黑)占总行数的比例;
5/17:lightwhite(浅白)占总行数的比例。
计算信息增益率
信息增益率(IGR) = Gain / H:
IGR (color)
= Gain (color) / H (color)
= 0.109 / 1.579 = 0.069
IGR (pedicle)
= Gain (pedicle) / H (pedicle)
= 0.413 / 1.401 = 0.295
IGR (sound)
= Gain (sound) / H (sound)
= 0.142 / 1.332 = 0.107
IGR (texture)
= Gain (texturer) / H (texture)
= 0.382 / 1.445 = 0.264
IGR (umbilical)
= Gain (umbilical) / H (umbilical)
= 0.496 / 1.548 = 0.320
IGR (touch)
= Gain (touch) / H (touch)
= 0.007 / 0.873 = 0.008
因为信息增益率:IGR(umbilical) = 0.320 最大,
所以,选择umbilical(脐部)为分裂属性。
发现分裂后,脐部属性为“平坦”的瓜都是坏瓜,故把它定义为叶子结点,选择其他的继续分裂。
过程同上。
代码实现
了解了算法的思路后,我们就可以进行编程了,博主这里使用的是VS 2019
,运用C++
完成。
文件调用关系
首先,是文件之间的调用关系:
其中箭头所指的意思是:调用
C4.5.cpp 调用 了Statement.h 和 Realization.h;
Realization.h 调用 了 Statement.h。
关键代码
由于算法中很多代码是重复的,且全部代码加起来接近700余行,算上博主自己写的代码注释,就更多了。为了节省篇幅,在此就不贴全部代码了。
首先是一行“西瓜”数据的类的定义:
// 西瓜个体定义
class watermelon
{
public:
std::string id; // 编号
std::string color; // 颜色
std::string pedicle; // 根蒂
std::string sound; // 声音
std::string texture; // 纹理
std::string umbilical; // 脐部
std::string touch; // 触感
std::string good; // 好瓜?
// 友元函数,搭配ostream用于"<<"的重载
friend std::ostream& operator<<(std::ostream& os, const watermelon& wm)
{
os << wm.id << " " << wm.color << " " << wm.pedicle << " "
<< wm.sound << " " << wm.texture << " " << wm.umbilical << " "
<< wm.touch << " " << wm.good;
os << std::endl;
return os;
}
};
加载文件的函数:
// 加载文件
void load_file(std::vector<watermelon>& datas,
std::vector<std::string>& attributes,
const std::string& filename)
{
std::ifstream istrm(filename);
assure(istrm, filename);
// is_open() --> 检查文件流是否有关联文件
if (istrm.is_open()) {
char buffer[128];
bool firstline = true;
// eof():判断文件结束的函数
while (!istrm.eof()) {
// getline() 从I/O流读取数据到字符串
// 函数原型:basic_istream& getline( char_type* s, std::streamsize count )
// s:指向要存储字符到的字符串的指针
// count:s 所指向的字符串的大小
istrm.getline(buffer, 128);
char* t = NULL;
// 切割函数,把每个属性值切割出来
const char* id = strtok_s(buffer, " ", &t);
const char* color = strtok_s(NULL, " ", &t);
const char* pedicle = strtok_s(NULL, " ", &t);
const char* sound = strtok_s(NULL, " ", &t);
const char* texture = strtok_s(NULL, " ", &t);
const char* umbilical = strtok_s(NULL, " ", &t);
const char* touch = strtok_s(NULL, " ", &t);
const char* good = strtok_s(NULL, " ", &t);
// Check if the first line.
// The first line contains attributes of datas.
if (firstline) {
// 如果是第一行的话就放入属性容器里
attributes.push_back(std::string(id));
attributes.push_back(std::string(color));
attributes.push_back(std::string(pedicle));
attributes.push_back(std::string(sound));
attributes.push_back(std::string(texture));
attributes.push_back(std::string(umbilical));
attributes.push_back(std::string(touch));
attributes.push_back(std::string(good));
firstline = false;
}
else {
// 如果不是第一行了,就放入数据容器里
watermelon data;
data.id = std::string(id);
data.color = std::string(color);
data.pedicle = std::string(pedicle);
data.sound = std::string(sound);
data.texture = std::string(texture);
data.umbilical = std::string(umbilical);
data.touch = std::string(touch);
data.good = std::string(good);
datas.push_back(data);
}
}
}
}
建立键值对的映射(std::map):
// 建立一个key-value(键值对)的映射:
// key:属性名(id、color、sound、等等)
// value:数值("1、2、3、4、5..."、"black、red、blue..."
void match_properties(std::vector<watermelon> datas,
std::vector<std::string> attributes,
std::map<std::string, std::vector<std::string>>& map_attr)
{
int index = 0;
for (auto attribute : attributes) {
std::vector<std::string> attrTmp;
// 自动将每一份watermelon的值赋给data
for (auto data : datas) {
switch (index) {
case 0:
// 首先判断临时属性变量不为空,并且在临时变量容器中未找到和data.id匹配的数据
if (!attrTmp.empty() && std::find(attrTmp.begin(), attrTmp.end(), data.id) == attrTmp.end()) {
// 如果未找到,则放进容器里
attrTmp.push_back(data.id);
}
// 如果当前的容器为空
else if (attrTmp.empty()) {
// 将当前的数值放进容器里
attrTmp.push_back(data.id);
}
break;
case 1:
if (!attrTmp.empty() && std::find(attrTmp.begin(), attrTmp.end(), data.color) == attrTmp.end()) {
attrTmp.push_back(data.color);
}
else if (attrTmp.empty()) {
attrTmp.push_back(data.color);
}
break;
case 2:
if (!attrTmp.empty() && std::find(attrTmp.begin(), attrTmp.end(), data.pedicle) == attrTmp.end()) {
attrTmp.push_back(data.pedicle);
}
else if (attrTmp.empty()) {
attrTmp.push_back(data.pedicle);
}
break;
case 3:
if (!attrTmp.empty() && std::find(attrTmp.begin(), attrTmp.end(), data.sound) == attrTmp.end()) {
attrTmp.push_back(data.sound);
}
else if (attrTmp.empty()) {
attrTmp.push_back(data.sound);
}
break;
case 4:
if (!attrTmp.empty() && std::find(attrTmp.begin(), attrTmp.end(), data.texture) == attrTmp.end()) {
attrTmp.push_back(data.texture);
}
else if (attrTmp.empty()) {
attrTmp.push_back(data.texture);
}
break;
case 5:
if (!attrTmp.empty() && std::find(attrTmp.begin(), attrTmp.end(), data.umbilical) == attrTmp.end()) {
attrTmp.push_back(data.umbilical);
}
else if (attrTmp.empty()) {
attrTmp.push_back(data.umbilical);
}
break;
case 6:
if (!attrTmp.empty() && std::find(attrTmp.begin(), attrTmp.end(), data.touch) == attrTmp.end()) {
attrTmp.push_back(data.touch);
}
else if (attrTmp.empty()) {
attrTmp.push_back(data.touch);
}
break;
case 7:
if (!attrTmp.empty() && std::find(attrTmp.begin(), attrTmp.end(), data.good) == attrTmp.end()) {
attrTmp.push_back(data.good);
}
else if (attrTmp.empty()) {
attrTmp.push_back(data.good);
}
break;
default:
break;
}
}
// 将同一个属性的所有数值放入容器后,再进行下一个属性
// 例如将id属性的所有数值:"1、2、3、4、5等等"放完之后,再去扫描color属性
index++;
// 例如:map_attr["id"] = "1、2、3、4、5等等"
map_attr[attribute] = attrTmp;
}
}
计算信息熵:
// 计算信息熵
double calculate_information_entropy(std::vector<watermelon>& datas,
std::string mapAttr = "",
std::string attribute = "good")
{
// Ent(D) = -∑(k=1, |Y|) p_k * log2(p_k)
int size = 0;
double entropy = 0;
int positive = 0;
int negative = 0;
// Because of the datas only have two labels:"yes" or "no"
// So entropy = positiveSample + negativeSample
if (attribute == "color") {
for (auto data : datas) {
if (data.color == mapAttr) {
if (data.good == "yes") {
++positive;
}
else {
++negative;
}
++size;
}
}
}
else if (attribute == "pedicle") {
for (auto data : datas) {
if (data.pedicle == mapAttr) {
if (data.good == "yes") {
++positive;
}
else {
++negative;
}
++size;
}
}
}
else if (attribute == "sound") {
for (auto data : datas) {
if (data.sound == mapAttr) {
if (data.good == "yes") {
++positive;
}
else {
++negative;
}
++size;
}
}
}
else if (attribute == "texture") {
for (auto data : datas) {
if (data.texture == mapAttr) {
if (data.good == "yes") {
++positive;
}
else {
++negative;
}
++size;
}
}
}
else if (attribute == "umbilical") {
for (auto data : datas) {
if (data.umbilical == mapAttr) {
if (data.good == "yes") {
++positive;
}
else {
++negative;
}
++size;
}
}
}
else if (attribute == "touch") {
for (auto data : datas) {
if (data.touch == mapAttr) {
if (data.good == "yes") {
++positive;
}
else {
++negative;
}
++size;
}
}
}
else if (attribute == "good") {
size = datas.size();
auto judget = [&](watermelon wm) {
if (wm.good == "yes") {
++positive;
}
else {
++negative;
}
};
// 从datas.begin()开始到datas.end()结束,每次遍历都调用judget
for_each(datas.begin(), datas.end(), judget);
}
if (positive == 0 || negative == 0) {
return 0;
}
else {
// 主要的计算公式
entropy = -(((double)positive / size) * log2((double)positive / size) + ((double)negative / size) * log2((double)negative / size));
}
return entropy;
}
计算信息增益:
// 计算信息增益
double calculate_information_gain(std::vector<watermelon>& datas,
std::string attribute,
std::map<std::string, std::vector<std::string>> map_attr)
{
double gain = calculate_information_entropy(datas);
std::vector<std::string> attrs = map_attr[attribute];
for (auto attr : attrs) {
gain -= proportion(datas, attr, attribute) * calculate_information_entropy(datas, attr, attribute);
}
return gain;
}
计算信息增益率:
// 计算信息增益率
double calculate_information_gain_ratio(std::vector<watermelon>& datas,
std::string attribute,
std::map<std::string, std::vector<std::string>> map_attr)
{
// Gain_ratio(D, a) = Gain(D, a) / IV(a)
//
double gain = calculate_information_gain(datas, attribute, map_attr);
double iv = 0;
std::vector<std::string> attrs = map_attr[attribute];
for (auto attr : attrs) {
iv -= proportion(datas, attr, attribute) * log2(proportion(datas, attr, attribute));
}
double gain_ratio = gain / iv;
return gain_ratio;
}
std::pair<std::string, std::vector<std::string>> optimal_attribute(std::vector<watermelon>& datas,
std::vector<std::string>& attributes,
std::map<std::string, std::vector<std::string>> map_attr)
{
std::map<std::string, double> map_gains;
std::map<std::string, double> map_gains_ratio;
for (auto attribute : attributes) {
map_gains[attribute] = calculate_information_gain(datas, attribute, map_attr);
map_gains_ratio[attribute] = calculate_information_gain_ratio(datas, attribute, map_attr);
}
// Sort the information gain and select the attribute of the maximum
// information gain.The biggest value is in the first.
//
std::vector<std::pair<std::string, double>> vec_map_gains(map_gains.begin(), map_gains.end());
std::vector<std::pair<std::string, double>> vec_map_gains_ratios(map_gains_ratio.begin(), map_gains_ratio.end());
auto compare_x_y = [](const std::pair<std::string, double> x, const std::pair<std::string, double> y) {
return x.second > y.second;
};
std::sort(vec_map_gains.begin(), vec_map_gains.end(), compare_x_y);
std::sort(vec_map_gains_ratios.begin(), vec_map_gains_ratios.end(), compare_x_y);
// Find information gain greater than average.
//
std::vector<std::string> vec_map_gains_name;
int vec_map_gains_size = vec_map_gains.size() / 2;
for (int i = 0; i < vec_map_gains_size; ++i) {
vec_map_gains_name.push_back(vec_map_gains[i].first);
}
std::string best_attribute;
for (auto vec_map_gains_ratio : vec_map_gains_ratios) {
if (std::find(vec_map_gains_name.begin(), vec_map_gains_name.end(), vec_map_gains_ratio.first)
!= vec_map_gains_name.end()) {
best_attribute = vec_map_gains_ratio.first;
break;
}
}
if (!best_attribute.empty()) {
auto search = map_attr.find(best_attribute);
if (search != map_attr.end()) {
return std::make_pair(search->first, search->second);
}
else {
return std::make_pair(std::string(""), std::vector<std::string>());
}
}
else {
return std::make_pair(std::string(""), std::vector<std::string>());
}
}
输出数据:
// 以树结构输出数据
void print_tree(TreeRoot pTree, int depth)
{
for (int i = 0; i < depth; ++i) {
std::cout << '\t';
}
if (!pTree->edgevalue.empty()) {
std::cout << "--" << pTree->edgevalue << "--" << std::endl;
for (int i = 0; i < depth; ++i) {
std::cout << '\t';
}
}
std::cout << pTree->attribute << std::endl;
for (auto child : pTree->childs) {
print_tree(child, depth + 1);
}
}
运行结果
经过运行的验证,我们的算法是没有问题的。
编译器结果
理解结果
因为我们前面对数据进行了处理(将中文转化成了英文)的缘故,导致我们可能不能一眼直观地看出结果的正确性。故博主将运行结果翻译成了如下图:
我们可以从图中看到,之前的计算是以脐部开始分裂属性的,后续的大家可以自行计算验证。
笔者能力有限,欢迎指正,一起学习。
参考资料
【1】C4.5算法(百度百科)
【2】fuqiuai. 数据挖掘领域十大经典算法之—C4.5算法(超详细附代码). CSDN. 2018.03.06
【3】Xefvan. 决策树学习 – ID3算法和C4.5算法(C++实现). CSDN. 2017.09.16