用决策树预测天气
想看连续型数据的,请看另一篇文章 “【手搓深度学习算法】用决策树预测天气-线性数据篇”
【手搓深度学习算法】用决策树预测天气-连续数据篇
前言
前几天已经探讨了使用决策树训练和预测连续型数据,观察到训练连续数据存在一个问题:训练时间过长,如果数据中特征很多,特征取值范围很大,将会难以计算,于是想要对比决策树对于离散型数据的处理方法和训练时间。
本文使用信息增益率来生成决策树,现如今有更流行和高效的决策树生成方法,有待进一步探索
决策树发展过程可以分为以下几个阶段:
早期决策树算法:最早的决策树算法可以追溯到上世纪60年代,当时主要用于解决分类问题。这些算法比较简单,通常采用静态规则进行决策,但缺乏对数据内在结构的深入理解和挖掘。
ID3算法:1970年代末,Ross Quinlan提出了ID3算法。ID3算法基于信息增益来选择划分属性,并使用树结构来表示分类模型。ID3算法的优点是简单易理解,可解释性强;缺点是容易过拟合,对可取值数目多的属性有所偏好。
C4.5算法:C4.5算法是ID3算法的改进版,主要解决了ID3算法中存在的两个问题:对于可取值数目多的属性有所偏好以及无法处理连续属性。C4.5算法采用信息增益率来选择划分属性,同时引入了剪枝策略来避免过拟合。C4.5算法还能够处理缺失值和自动进行特征选择。
CART算法:CART(Classification and Regression Trees)算法是一种非常流行的决策树算法,它既可以用于分类问题也可以用于回归问题。CART算法采用基尼不纯度来选择划分属性,并使用二叉树来表示分类模型。CART算法的优点是简单易理解、可解释性强,能够处理连续属性和缺失值;缺点是可能会产生过于复杂的决策树。
随机森林和梯度提升树:随机森林和梯度提升树是近年来发展起来的集成学习算法,它们结合了决策树和其他学习算法的优点,具有更高的准确率和更好的泛化性能。随机森林通过构建多棵决策树并取平均值来提高准确率;梯度提升树则通过迭代地添加新的决策树来改进模型。
以上就是决策树算法的发展过程和各代流行算法的优缺点。在实际应用中,可以根据具体问题和数据的特点选择合适的决策树算法来构建分类或回归模型。
–离散型数据
在决策树中,处理离散型数据和连续性数据的方式存在一些区别。
离散型数据:对于离散型数据,我们可以直接使用决策树的基本操作来进行训练和预测。例如,我们可以根据某个特定的属性值(也就是离散的取值)来划分数据集,然后在每个子节点中再次进行相同的操作。这种方式非常直观,也容易理解。
和连续型数据的区别
在本次实验中,我们使用相同的数据集,但是,对数据集的每一个特征进行了离散化处理,如果,假设气温的取值范围是-5到23度,那么我们将气温通过取值范围简单的划分为“低,中,高”,于是气温特征取值不再连续,而被认为是三种截然不同的选择,这样的处理方式可能不一定科学,仅供研究。
变化
数据集变化
额外的处理
离散型数据可能存在用完了所有特征还是无法做出决策,或者某些特征分支的信息增益比太低,会被剪枝,因此当特征用完时需要计算训练集中,到当前特征为止,各种可能性存在的概率,以便在预测时,当运行到叶子节点还是无法做出决策时,使用概率来预测结果。
更快的构建速度
在连续型数据中,我们需要为每个特征计算所有可能的决策边界,然后计算其左右子树的信息增益比的和,在离散型数据中,只需要计算当前可选的每个特征带来的信息增益比的和,显而易见,离散型数据的计算量更小。
如下图,
连续型数据构建时间为29.53秒
手动离散的数据构建时间为0.29秒
相似的精度
在本文讨论的数据集中,两种方式具有相似的精度
定义数据集
用于从CSV文件中读取数据,并将其转换为数组形式。如果is_test
参数为真,则返回测试数据集;否则,返回训练数据集。
import numpy as np
import csv
kunming_weather = "J:\\MachineLearning\\数据集\\kunming_weather_new.txt"
def create_dataset(is_test = False):
dataset = []
# 打开CSV文件
with open(kunming_weather, 'r') as file:
reader = csv.reader(file)
# 初始化一个空数组来存储数据
dataset = []
# 遍历每一行数据
for row in reader:
# 将每一行数据转换为数组,并添加到data数组中
dataset.append(row)
np.random.shuffle(dataset) #将数据按行充分打乱
'''
labels = ["MeanAirPressure",
"DailyMinimumAirPressure",
"MeanTempe",
"DailyMinTempe",
"MeanRelativeHumidity",
"CumulativePrecipitation",
"LargeEvaporation",
"AverageWindSpd",
"MaximumWindSpd",
"SunshineDuration",
"MeanSurfaceTempe",
"DailyMinimumSurfaceTempe",
"WhetherItSnows"]
'''
features = ["MAP",
"DMAP",
"MT",
"DMT",
"MRH",
"CP",
"LE",
"AWS",
"MWS",
"SD",
"MST",
"DMST"]
train_count = int(len(dataset) * 0.8)
train_dataset = dataset[1 : train_count]
if (is_test):
test_dataset = dataset[train_count:]
test_source = []
for data in test_dataset:
row_data = {}
for feature_index in range(len(features)):
row_data[features[feature_index]] = data[feature_index]
row_data["result"] = data[-1]
test_source.append(row_data)
return test_source
return train_dataset, features
create_dataset(True)
创建决策树
构建决策树。它首先选择最优的特征进行分割,然后递归地在每个子节点上重复这个过程。
import operator
def create_tree(dataset, lables, feature_lables):
# 获取数据集中所有样本的类别
class_list = [example[-1] for example in dataset]
# 如果所有样本的类别都相同,那么返回这个类别,比如剩下的样本标签全部都是“会下雪”,那么当前前置的所有条件都会连接“会下雪”的结果
if class_list.count(class_list[0]) == len(class_list):
return class_list[0]
# 如果数据集中只剩下一个特征,那么返回最多的类别
if len(dataset[0]) == 1:
return majority_cnt(class_list)
# 选择最好的特征进行分割
best_feature = choose_best_feature_to_split(dataset)
# 获取最好的特征的标签
best_feature_label = lables[best_feature]
# 将最好的特征的标签添加到特征标签列表中
feature_lables.append(best_feature_label)
# 创建决策树
my_tree = {best_feature_label:{}}
# 计算并添加默认类别
my_tree[best_feature_label]['default'] = majority_cnt(class_list)
# 删除已经使用的特征标签
del lables[best_feature]
# 获取最好的特征的所有值
feature_value = [example[best_feature] for example in dataset]
# 获取最好的特征的所有唯一值
unique_val = set(feature_value)
# 对每一个唯一值,递归创建决策树
for value in unique_val:
sublabels = lables[:]
my_tree[best_feature_label][value] = create_tree(
remove_feature_from_dataset(dataset, best_feature, value),
sublabels,
feature_lables)
# 返回决策树
return my_tree
当特征时最后一个,且子节点分类并不唯一,选取数量较多的那一类返回
选取当前特征对应的样本中,出现次数最多的可能
import operator
def majority_cnt(class_list):
class_count = {}
for vote in class_list:
if vote not in class_count.keys():
class_count[vote] = 0 #创建节点,值置为0
class_count[vote] += 1
sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), reverse=True)
return sorted_class_count[0][0]
计算当前决策树的熵值
信息增益比是决策树算法中的一个重要概念,它是用来选择最优划分属性的一种方法。信息增益比的计算流程可以分为以下几个步骤:
-
计算数据集的原始信息熵:信息熵是度量数据集混乱程度的一个指标。在决策树中,我们首先需要计算出整个数据集的信息熵。这个过程通常是通过统计数据集中每个类别的样本数量,然后计算出每个类别的概率,最后将这些概率代入信息熵的公式进行计算。
-
计算每个属性划分后的信息熵:接下来,我们需要计算出每个属性划分后的信息熵。这个过程通常是通过将数据集按照每个属性的不同取值进行划分,然后分别计算出每个子集的信息熵,最后将这些信息熵按照每个子集的样本数量权重进行加权平均。
-
计算每个属性的信息增益:信息增益是指数据集在某个属性划分后的信息熵减去原始信息熵。我们需要计算出每个属性的信息增益,这个过程通常是通过将每个属性划分后的信息熵减去原始信息熵进行计算。
-
计算每个属性的分裂信息值:分裂信息值是度量属性划分样本集合的一个指标。我们需要计算出每个属性的分裂信息值,这个过程通常是通过统计每个属性的不同取值的样本数量,然后计算出每个取值的概率,最后将这些概率代入分裂信息值的公式进行计算。
-
计算每个属性的信息增益比:信息增益比是指信息增益除以分裂信息值。我们需要计算出每个属性的信息增益比,这个过程通常是通过将每个属性的信息增益除以其分裂信息值进行计算。
-
选择信息增益比最大的属性作为最优划分属性:最后,我们需要选择出信息增益比最大的属性作为最优划分属性。这个过程通常是通过比较所有属性的信息增益比,然后选择出信息增益比最大的属性。
Gain Ratio = Information Gain Split Information \text{Gain Ratio} = \frac{\text{Information Gain}}{\text{Split Information}} Gain Ratio=Split InformationInformation Gain
其中,
信息增益 (Information Gain):
Information Gain = Entropy(Parent) − ∑ ( Number of samples in child Number of samples in parent ∗ Entropy(Child) ) \text{Information Gain} = \text{Entropy(Parent)} - \sum \left(\frac{\text{Number of samples in child}}{\text{Number of samples in parent}} * \text{Entropy(Child)}\right) Information Gain=Entropy(Parent)−∑(Number of samples in parentNumber of samples in child∗Entropy(Child))
分裂信息 (Split Information):
Split Information = − ∑ ( Number of samples in child Number of samples in parent ∗ log 2 ( Number of samples in child Number of samples in parent ) ) \text{Split Information} = - \sum \left(\frac{\text{Number of samples in child}}{\text{Number of samples in parent}} * \log_2 \left(\frac{\text{Number of samples in child}}{\text{Number of samples in parent}}\right)\right) Split Information=−∑(Number of samples in parentNumber of samples in child∗log2(Number of samples in parentNumber of samples in child))
熵 (Entropy):
Entropy = − ∑ ( P i ∗ log 2 ( P i ) ) \text{Entropy} = - \sum (P_i * \log_2(P_i)) Entropy=−∑(Pi∗log2(Pi))
其中, P i P_i Pi 是第 i i i 类样本在总样本中的比例。
from math import log
def calcEnt(dataset):
num_examples = len(dataset) # 计算数据集中的样本数量
label_counts = {} # 创建一个字典,用来存储每个标签的数量
# 遍历数据集中的每个样本
for feature in dataset:
current_label = feature[-1] # 获取样本的标签,假设标签是样本的最后一个特征
# 如果当前标签不在字典中,就在字典中添加这个标签,并设置数量为0
if (current_label not in label_counts.keys()):
label_counts[current_label] = 0
# 将当前标签的数量加1
label_counts[current_label] += 1
ret_ent = 0 # 初始化熵为0
# 遍历每个标签
for key in label_counts:
prop = float(label_counts[key] / num_examples) # 计算当前标签的概率
# 根据熵的公式,计算并累加每个标签的熵
ret_ent += - (prop * log(prop, 2))
return ret_ent # 返回数据集的熵
删掉已经被选作特征的数据集部分
def remove_feature_from_dataset(dataset, axis, val):
ret_dataset = []
for feature in dataset:
if (feature[axis] == val):
reduced_feature = feature[:axis] #取当前列前面的列
#np.concatenate((reduced_feature, feature[axis+1:]), axis=0)
reduced_feature.extend(feature[axis+1:]) #连接当前列后面的列
ret_dataset.append(reduced_feature)
return ret_dataset
选取最佳特征
用于选择最优的特征进行分割。它通过计算每个特征的信息增益来选择最优的特征。
def choose_best_feature_to_split(dataset):
num_features = len(dataset[0]) - 1 #特征数量(减去标签列)
base_entropy = calcEnt(dataset) #数据集的初始熵
best_info_gain = 0 #初始信息增益比为0
best_feature = -1
for i in range(num_features):
feature_list = [example[i] for example in dataset] #取当前特征对应的所有的样本值
unique_vals = set(feature_list) #取当前特征对应的所有可能样本
new_entropy = 0
for val in unique_vals:
sub_dataset = remove_feature_from_dataset(dataset, i, val) #当前特征已经加入决策树了,需要移除
prop = len(sub_dataset) / float(len(dataset))
new_entropy += prop * calcEnt(sub_dataset) #当前分类的概率乘子类的熵
info_gain = base_entropy - new_entropy #信息增益
if (info_gain > best_info_gain): # base_entropy - new_entropy > base_entropy
best_info_gain = info_gain
best_feature = i
return best_feature
可视化展示
from graphviz import Digraph
def visualize_decision_tree(tree, name="DecisionTree"):
def add_edges(graph, subtree, parent_node):
for key, value in subtree.items():
if isinstance(value, dict):
sub_node = f"{parent_node}_{str(key)}" # 将key转换为字符串
graph.edge(parent_node, sub_node, label=str(key)) # 将label转换为字符串
add_edges(graph, value, sub_node)
else:
graph.edge(parent_node, str(value), label=str(key)) # 将value和label转换为字符串
graph = Digraph(name=name, format="png")
graph.attr('node', shape='Mrecord')
add_edges(graph, tree, "root")
return graph
预测
用于预测新的数据。它通过遍历决策树,根据输入的参数选择合适的路径,最终得到预测结果。
def predict(node, parameters : dict):
# 如果当前节点是字符串,那么直接返回这个字符串
if isinstance(node, str):
return node
# 如果当前节点是字典,那么根据参数字典中的值选择一个子节点
else:
for key, value in node.items():
#print ("key = {}, value = {}".format(key, value))
if (key in parameters.keys()):
if not (parameters[key] in value.keys()):
return value.get('default', None)
current_node = value[parameters[key]]
result = predict(current_node, parameters)
if result is not None:
return result
else:
current_node = None
return None
训练和验证
import json
if (__name__ == "__main__"):
dataset, label = create_dataset()
test_source = create_dataset(True)
feature_labels = [] #特征排序,初始化为空
my_tree = create_tree(dataset, label, feature_labels)
test_tree = my_tree
predict_true_count = 0
predict_count = 0
fail_info_array = []
for test_item in test_source:
predict_count += 1
predict_result = predict(test_tree, test_item)
if (predict_result == test_item["result"]):
predict_true_count += 1
if (predict_true_count % 100 == 0):
print("Test {} / {} pcs of data is true".format(predict_true_count, predict_count))
else:
fail_info = ""
for key, value in test_item.items():
fail_info += "{} : {} , ".format(key, value)
fail_info = "Predict result is {} actual is {} ".format(predict_result, test_item["result"]) + fail_info
fail_info_array.append(fail_info)
print(fail_info_array)
print("Accuracy is {}".format(predict_true_count / len(test_source)))
with open('data.json', 'w') as file:
json.dump(my_tree, file)
# 可视化决策树
graph = visualize_decision_tree(my_tree)
graph.view() # 这将在默认的图片查看器中显示决策树图像
完整代码(数据集在绑定的资源里,想尝试的去下载吧)
import numpy as np
import csv
import json
import time
kunming_weather = "J:\\MachineLearning\\数据集\\kunming_weather_new.txt"
def create_dataset(is_test = False):
dataset = []
# 打开CSV文件
with open(kunming_weather, 'r') as file:
reader = csv.reader(file)
# 初始化一个空数组来存储数据
dataset = []
# 遍历每一行数据
for row in reader:
# 将每一行数据转换为数组,并添加到data数组中
dataset.append(row)
np.random.shuffle(dataset) #将数据按行充分打乱
'''
labels = ["MeanAirPressure",
"DailyMinimumAirPressure",
"MeanTempe",
"DailyMinTempe",
"MeanRelativeHumidity",
"CumulativePrecipitation",
"LargeEvaporation",
"AverageWindSpd",
"MaximumWindSpd",
"SunshineDuration",
"MeanSurfaceTempe",
"DailyMinimumSurfaceTempe",
"WhetherItSnows"]
'''
features = ["MAP",
"DMAP",
"MT",
"DMT",
"MRH",
"CP",
"LE",
"AWS",
"MWS",
"SD",
"MST",
"DMST"]
train_count = int(len(dataset) * 0.8)
train_dataset = dataset[1 : train_count]
if (is_test):
test_dataset = dataset[train_count:]
test_source = []
for data in test_dataset:
row_data = {}
for feature_index in range(len(features)):
row_data[features[feature_index]] = data[feature_index]
row_data["result"] = data[-1]
test_source.append(row_data)
return test_source
return train_dataset, features
import operator
def majority_cnt(class_list):
class_count = {}
for vote in class_list:
if vote not in class_count.keys():
class_count[vote] = 0 #创建节点,值置为0
class_count[vote] += 1
sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), reverse=True)
return sorted_class_count[0][0]
from math import log
def calcEnt(dataset):
num_examples = len(dataset) # 计算数据集中的样本数量
label_counts = {} # 创建一个字典,用来存储每个标签的数量
# 遍历数据集中的每个样本
for feature in dataset:
current_label = feature[-1] # 获取样本的标签,假设标签是样本的最后一个特征
# 如果当前标签不在字典中,就在字典中添加这个标签,并设置数量为0
if (current_label not in label_counts.keys()):
label_counts[current_label] = 0
# 将当前标签的数量加1
label_counts[current_label] += 1
ret_ent = 0 # 初始化熵为0
# 遍历每个标签
for key in label_counts:
prop = float(label_counts[key] / num_examples) # 计算当前标签的概率
# 根据熵的公式,计算并累加每个标签的熵
ret_ent += - (prop * log(prop, 2))
return ret_ent # 返回数据集的熵
def remove_feature_from_dataset(dataset, axis, val):
ret_dataset = []
for feature in dataset:
if (feature[axis] == val):
reduced_feature = feature[:axis] #取当前列前面的列
#np.concatenate((reduced_feature, feature[axis+1:]), axis=0)
reduced_feature.extend(feature[axis+1:]) #连接当前列后面的列
ret_dataset.append(reduced_feature)
return ret_dataset
def choose_best_feature_to_split(dataset):
num_features = len(dataset[0]) - 1 #特征数量(减去标签列)
base_entropy = calcEnt(dataset) #数据集的初始熵
best_info_gain = 0 #初始信息增益比为0
best_feature = -1
for i in range(num_features):
feature_list = [example[i] for example in dataset] #取当前特征对应的所有的样本值
unique_vals = set(feature_list) #取当前特征对应的所有可能样本
new_entropy = 0
for val in unique_vals:
sub_dataset = remove_feature_from_dataset(dataset, i, val) #当前特征已经加入决策树了,需要移除
prop = len(sub_dataset) / float(len(dataset))
new_entropy += prop * calcEnt(sub_dataset) #当前分类的概率乘子类的熵
info_gain = base_entropy - new_entropy #信息增益
if (info_gain > best_info_gain): # base_entropy - new_entropy > base_entropy
best_info_gain = info_gain
best_feature = i
return best_feature
import operator
def create_tree(dataset, lables, feature_lables):
# 获取数据集中所有样本的类别
class_list = [example[-1] for example in dataset]
# 如果所有样本的类别都相同,那么返回这个类别,比如剩下的样本标签全部都是“会下雪”,那么当前前置的所有条件都会连接“会下雪”的结果
if class_list.count(class_list[0]) == len(class_list):
return class_list[0]
# 如果数据集中只剩下一个特征,那么返回最多的类别
if len(dataset[0]) == 1:
return majority_cnt(class_list)
# 选择最好的特征进行分割
best_feature = choose_best_feature_to_split(dataset)
# 获取最好的特征的标签
best_feature_label = lables[best_feature]
# 将最好的特征的标签添加到特征标签列表中
feature_lables.append(best_feature_label)
# 创建决策树
my_tree = {best_feature_label:{}}
# 计算并添加默认类别
my_tree[best_feature_label]['default'] = majority_cnt(class_list)
# 删除已经使用的特征标签
del lables[best_feature]
# 获取最好的特征的所有值
feature_value = [example[best_feature] for example in dataset]
# 获取最好的特征的所有唯一值
unique_val = set(feature_value)
# 对每一个唯一值,递归创建决策树
for value in unique_val:
sublabels = lables[:]
my_tree[best_feature_label][value] = create_tree(
remove_feature_from_dataset(dataset, best_feature, value),
sublabels,
feature_lables)
# 返回决策树
return my_tree
from graphviz import Digraph
def visualize_decision_tree(tree, name="DecisionTree"):
def add_edges(graph, subtree, parent_node):
for key, value in subtree.items():
if isinstance(value, dict):
sub_node = f"{parent_node}_{str(key)}" # 将key转换为字符串
graph.edge(parent_node, sub_node, label=str(key)) # 将label转换为字符串
add_edges(graph, value, sub_node)
else:
graph.edge(parent_node, str(value), label=str(key)) # 将value和label转换为字符串
graph = Digraph(name=name, format="png")
graph.attr('node', shape='Mrecord')
add_edges(graph, tree, "root")
return graph
def predict(node, parameters : dict):
# 如果当前节点是字符串,那么直接返回这个字符串
if isinstance(node, str):
return node
# 如果当前节点是字典,那么根据参数字典中的值选择一个子节点
else:
for key, value in node.items():
#print ("key = {}, value = {}".format(key, value))
if (key in parameters.keys()):
if not (parameters[key] in value.keys()):
return value.get('default', None)
current_node = value[parameters[key]]
result = predict(current_node, parameters)
if result is not None:
return result
else:
current_node = None
return None
if (__name__ == "__main__"):
dataset, label = create_dataset()
test_source = create_dataset(True)
feature_labels = [] #特征排序,初始化为空
train_start_time = time.time()
my_tree = create_tree(dataset, label, feature_labels)
train_end_time = time.time()
elapsed_time = train_end_time - train_start_time
print ("train takes {} seconds".format(elapsed_time))
test_tree = my_tree
predict_true_count = 0
predict_count = 0
fail_info_array = []
test_start_time = time.time()
for test_item in test_source:
predict_count += 1
predict_result = predict(test_tree, test_item)
if (predict_result == test_item["result"]):
predict_true_count += 1
if (predict_true_count % 100 == 0):
print("Test {} / {} pcs of data is true".format(predict_true_count, predict_count))
else:
fail_info = ""
for key, value in test_item.items():
fail_info += "{} : {} , ".format(key, value)
fail_info = "Predict result is {} actual is {} ".format(predict_result, test_item["result"]) + fail_info
fail_info_array.append(fail_info)
test_end_time = time.time()
elapsed_test_time = test_end_time - test_start_time
print ("test takes {} seconds".format(elapsed_test_time))
print(fail_info_array)
print("Accuracy is {}".format(predict_true_count / len(test_source)))
with open('data.json', 'w') as file:
json.dump(my_tree, file)
# 可视化决策树
graph = visualize_decision_tree(my_tree)
graph.view() # 这将在默认的图片查看器中显示决策树图像