函数
- log()函数和log2()函数
需要导入math模块。
log( ,2)相当于log2().
- 列表方法append()
太熟了,不说了
- 列表方法extend()
和append有点像,但是不一样!
append是在列表末尾添加一个元素,不管这个元素的数据类型是什么。
extend用于在列表末尾一次性追加另一个序列中的多个值。
list.extend(seq)
append:
a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
print(a)
[1, 2, 3, [4, 5, 6]]
extend:
a = [1, 2, 3]
b = [4, 5, 6]
a.extend(b)
print(a)
[1, 2, 3, 4, 5, 6]
- set()函数
可以用来去除重复元素。
书上有句话说:从列表中创建集合是python语言得到列表中唯一元素值的最快方法。
信息熵
信息: 如果待分类的事务可能划分在多个分类之中,则符号 x i x_i xi的信息定义为:
l ( x i ) = − log 2 p ( x i ) , l(x_i)=-\log_2 p(x_i) , l(xi)=−log2p(xi),
其中, p ( x i ) p(x_i) p(xi)选择该分类的概率。
以上定义说明,一个具体事件所发生的概率越小,它发生后带来的信息量就越少。
熵: 所有类别所有可能值包含的信息期望值,公式为:
H = − ∑ i = 1 n p ( x i ) log 2 p ( x i ) , H=-\sum_{i=1}^{n} p(x_i)\log_2 p(x_i), H=−i=1∑np(xi)log2p(xi),
也就是各个具体事件发生所带来信息量的加权平均。
上面是书上给出的简单公式,并没有仔细解释。下面再稍微解释下:
信息量指的是以一个具体事件所携带的信息量。
一个离散型随机变量 X X X,取值为 x 0 , … , x n x_0,\dots,x_n x0,…,xn,则事件 X = x 0 X=x_0 X=x0的信息量就是 I ( x 0 ) = − log 2 p ( x 0 ) I(x_0)=-\log_2 p(x_0) I(x0)=−log2p(x0)。
一个事件所发生的概率越小,它所携带的信息量就越大。一个必然事件所携带的信息量为0,这也是很直观的,如果一个事件无论如何都会发生,我并不会感到惊奇,是吧?而一个发生概率很小的事件发生了,我们会感到出乎意料,所获得的信息当然就多。
所以,事件所携带的信息量是和其发生概率呈负相关的。
信息熵 ≠ 信息量 !
信息熵考虑随机变量所有的取值,描述的是所有可能事件所带来的信息量的期望。
从信息传播角度来看,信息熵可以表示信息的价值。
变量的不确定性越大,熵就越大。 上面的离散随机变量 X X X 如果是均匀分布,则其熵最大。
计算给定数据集的熵
用频率代替概率。
书上这节dataSet的数据类型是python list,我不理解…
还有,书上定义了一个createDataSet()函数,把属性名组成的列表命名为labels,我也不理解…
- 我把自己一开始写的和书上写的函数一块儿放上来了。我在用公式计算熵的那部分直接用了列表推导式,书上是用循环累加。
- 用列表记录对应标签的数量,我使用了字典的get()方法,好像前一节有用过,但是书上直接先初始化为0,然后再累加。
- 不知道这些写法有什么利弊,非计算机专业不太懂得写代码的一些套路,有时候会很疑惑…
我写的
from math import log2
# 计算给定数据集的香农熵
def calShannonEnt(dataSet):
"""标签是dataSet的最后一列
先计算每种label的数据数量,再计算频率"""
labels = []
m = len(dataSet) # 数据数量
# 在不知道labels具体有什么的情况下,可以遍历labels,然后使用字典帮忙,使用dict.get()。
label_count = {}
for vec in dataSet:
cur_label = vec[-1]
label_count[cur_label] = label_count.get(cur_label, 0) + 1
freqs_list = [num / m for num in label_count.values()] # 频率列表
Ent = sum([-freq * log2(freq) for freq in freqs_list]) # 直接计算熵
return Ent
def createDataSet():
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
feature_names = ['no surfacing', 'flippers']
return dataSet, feature_names
my_dataset, my_feature_names = createDataSet()
print(my_dataset)
ent = calShannonEnt(my_dataset)
print(ent)
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
0.9709505944546686
书上写的
def calShannonEnt(dataSet):
numEntries = len(dataSet)
labelCounts = {}
# 下面循环计算各label的个数
for featVec in dataSet:
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEnt = 0.0
# 下面循环计算熵
for key in labelCounts:
prob = float(labelCounts[key]) / numEntries
shannonEnt -= prob * log2(prob)
return shannonEnt
def createDataSet():
dataSet = [[1, 1, 'maybe'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
feature_names = ['no surfacing', 'flippers']
return dataSet, feature_names
my_dataset, my_feature_names = createDataSet()
print(my_dataset)
[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
1.3709505944546687
把数据集的label多加了一个类型’maybe‘
划分数据集
给定一个属性,我们需要根据属性的取值对数据集进行一个划分。
[之后的步骤是,算出划分后各个数据集的熵,然后根据属性取值的比例对这些数据集的熵求加权平均(期望)]
- 写一个函数,输入:原始数据集,划分所依据的属性,属性取值。返回:该取值下的数据子集。
- 注意: 提取子集的时候,要把这个属性对应的那一维数据给去掉。所以使用到了extend方法。
- 下面自己写的,和书上一模一样,只是变量名改了下,比较简单。
# 数据集划分
def splitDataSet(dataSet, pos, feat_val):
"""
:param dataSet: 要划分的数据集,这里是python list
:param pos: 划分所依据的属性 的位置
:param feat_val: 要划分出来的属性取值
:return: 数据子集
"""
dataSubSet = [] # 用来存储子集
for vec in dataSet:
if vec[pos] == feat_val:
new_vec = vec[:pos]
new_vec.extend(vec[pos + 1:])
dataSubSet.append(vec)
return dataSubSet
选择最好的数据集划分方式
前面实现了计算一个数据集的熵,根据属性划分数据集。
现在可以将前面两个组合起来。
对特征进行遍历:划分数据集,然后分别计算熵,计算该特征划分下的熵(取期望)。 求哪种划分熵增益最大。
自己写的
- 我自己写的程序另外定义了一个计算熵增益的函数calGainEnt(),我只是程序看起来更清晰一点
# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
"""对每个特征进行遍历,选择熵增益最大的"""
entD = calShannonEnt(dataSet)
num_data = len(dataSet)
num_features = len(dataSet[0]) - 1
ent_gain_list = [] # 存放各个特征划分后的熵增益
# 下面循环计算各个pos下的熵增益
for cur_pos in range(num_features):
cur_pos_gain_ent = calGainEnt(dataSet, num_data, entD, cur_pos)
ent_gain_list.append(cur_pos_gain_ent)
return ent_gain_list.index(max(ent_gain_list))
def calGainEnt(dataSet, num_data, entD, pos):
"""计算给定特征下的熵增益
我传入num_data, entD的原因是,避免对每个pos重复计算这两个值"""
# 先获取该属性/特征有哪些取值,并且这些取值所对应的数量有多少(需要计算比例)
vals_counts = {}
# 下面循环计算属性各个取值的样本数量,得到一个{val:数量}字典
for vec in dataSet:
cur_val = vec[pos]
vals_counts[cur_val] = vals_counts.get(cur_val, 0) + 1
pos_ent = 0.0
for val, num in vals_counts.items():
subSet = splitDataSet(dataSet, pos, val)
subset_ent = calShannonEnt(subSet)
pos_ent += subset_ent * (num / num_data)
return entD - pos_ent
print(chooseBestFeatureToSplit(my_dataset))
0
书上的
- 感觉确实书上的结构更清晰…不管从结构,还是变量命名…
- 又是一边遍历一边处理的思想,确实这样可以:1.省掉很多变量的定义 2.没有那么多循环结构 3.说不定还可以节省内存 3.结构清晰 (个人感受)
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1
baseEntrophy = calShannonEnt(dataSet)
bestInfoGain = 0.0;
bestFeature = -1
for i in range(numFeatures):
featList = [example[i] for example in dataSet] # 存放i位置属性的所有样本取值
uniqueVals = set(featList) # 提取属性取值种类
newEntrophy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet) / float(len(dataSet))
newEntrophy += prob * calShannonEnt(subDataSet)
infoGain = baseEntrophy - newEntrophy
if infoGain > bestInfoGain:
bestInfoGain = infoGain
bestFeature = i
return bestFeature
记住下面的写法:(列表推导和set去除重复元素)
featList = [example[i] for example in dataSet] # 存放i位置属性的所有样本取值 uniqueVals = set(featList) # 提取属性取值种类
在遍历种求取最值:
if infoGain > bestInfoGain: bestInfoGain = infoGain bestFeature = i
多数表决函数
如果数据集已经处理完了所有属性,但是类标签依然不是唯一的,采用多数表决方法决定叶子节点分类。
记住这个函数的写法:
def majority(dataSet):
"""用一个字典记录各个类别有多少数量然后字典排序,返回数量最多的类"""
class_counts = {}
for vec in dataSet:
label = vec[-1]
class_counts[label] = class_counts.get(label, 0) + 1
major_class = sorted(class_counts.items(), key=itemgetter(1))[0][0] # sorted返回的是元组列表
return major_class
其中,
class_counts[label] = class_counts.get(label, 0) + 1
可以等价替换为
if label not in class_counts.keys(): class_counts[label] = 0
class_counts[label] += 1
递归构建决策树
到了本节最重要的部分。
本书提到的base case就下面两种:
1.如果子集不是都属于同一类,那么就递归构建子树。
2.如果属性用完了,采用多数表决。
但是我看西瓜书还有一种:3.如果一个属性取值下,划分的子集是空集,那么对原数据集进行多数表决,得到的类别作为当前子节点的类别。
决策树的递归形式:
{best_feature: {val1:子树1,…,valn:子树n}}
def createTree(dataSet, features):
"""如果子集不全是同一类,就创建子树。这里树直接用 ’字典之字典的形式表示‘ """
# base case: dataSet全属于同一类
class_list = [vec[-1] for vec in dataSet]
# 如果class_list只有一种元素,说明所有元素都一样
if len(set(class_list)) == 1:
return class_list[0]
# base case2:特征全部都用完了,多数表决
if len(dataSet[0]) == 1:
return majority(dataSet)
# 以上都不满足,就是要分裂创建子树了
best_feature = chooseBestFeatureToSplit(dataSet)
best_feature_name = features[best_feature]
my_tree = { best_feature_name: {}} # 树的根节点就是分裂属性
features.pop(best_feature) # 删除这个属性
values_list = [vec[best_feature] for vec in dataSet]
unique_values = set(values_list)
for value in unique_values:
my_tree[best_feature_name][value] = createTree(splitDataSet(dataSet, best_feature, value), features[:])
return my_tree
my_tree = createTree(my_dataset,my_feature_names)
print(my_tree)
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
总结
- 本节的数据集将特征与标签放到一起,应该是因为划分数据集时,特征集和标签集要一起划分,所以干脆放到一起。
- 实现算法需要将算法的过程一小步一小步拆解,先实现里面的小功能,然后再封装到一起。
- 一开始看到算法有些不知从何入手,但是书上的实现思路却能如此清晰,将功能进行拆解后,每一小步的实现似乎都不是太难。