温故知新, 没事儿又去看看以前的code.
之前写了篇 利用sklearn对红酒数据集分类 . 其中, 红酒数据集是长这样的:
而西瓜数据集是长这样的:
很明显, 前者属于浮点特征型数据, 后者则是类别特征型数据.
同样, 我们利用sklearn.tree模块来进行树的构造, 结果报错
ValueError: could not convert string to float: '青绿'
它就不能处理非浮点型的数据, 我本人觉得比较扯. 那要咋办呢? 下面就介绍两种处理方法.
独热编码, 又称一位有效编码,其方法是使用N位状态寄存器来对N个状态进行编码, 每个状态都有它独立的寄存器位, 并且在任意时候, 其中只有一位有效.
例如, 西瓜数据中的色泽这一属性, 有 “青绿”, “浅白”, “乌黑” 三个类别,即[“青绿”, “浅白”, “乌黑”]. 若使用独热编码, “青绿” 则可表示为 [0, 0, 1], “浅白” 可表示为 [0, 1, 0], “乌黑” 可表示为 [1, 0, 1]. 如下图所示:
但它不好使的点就在于把增加了数据维度, 色泽这一属性的数据原本为一维 (17,), 经过独热编码后就成了二维的了(17,3). 当然, 我觉得还是能用吧, 它就相当于把原数据的特征细化成更多. 原本 {“色泽: 青绿, 乌黑, 浅白}, 变成了 {“青绿”: 是, 否}, {“乌黑”: 是, 否}, {“浅白”: 是, 否}.
标签编码, 根据字符串形式的特征值在特征序列中的位置, 为其指定一个数字标签, 用于提供给基于数值算法的学习模型. 即是对不连续的数字或者文本进行编号.
还是刚才的例子, 经过标签编码后, “青绿” --> “2” , “乌黑” --> “0”, “浅白” --> “1”. 整体如下图所示:
import pandas as pd
from sklearn import tree
import graphviz
from sklearn.preprocessing import OneHotEncoder, LabelEncoder
D = [
{'色泽': '青绿', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
{'色泽': '乌黑', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
{'色泽': '乌黑', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
{'色泽': '青绿', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
{'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
{'色泽': '青绿', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '软粘', '好瓜': '是'},
{'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '稍糊', '脐部': '稍凹', '触感': '软粘', '好瓜': '是'},
{'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '硬滑', '好瓜': '是'},
{'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '沉闷', '纹理': '稍糊', '脐部': '稍凹', '触感': '硬滑', '好瓜': '否'},
{'色泽': '青绿', '根蒂': '硬挺', '敲声': '清脆', '纹理': '清晰', '脐部': '平坦', '触感': '软粘', '好瓜': '否'},
{'色泽': '浅白', '根蒂': '硬挺', '敲声': '清脆', '纹理': '模糊', '脐部': '平坦', '触感': '硬滑', '好瓜': '否'},
{'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '模糊', '脐部': '平坦', '触感': '软粘', '好瓜': '否'},
{'色泽': '青绿', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '稍糊', '脐部': '凹陷', '触感': '硬滑', '好瓜': '否'},
{'色泽': '浅白', '根蒂': '稍蜷', '敲声': '沉闷', '纹理': '稍糊', '脐部': '凹陷', '触感': '硬滑', '好瓜': '否'},
{'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '软粘', '好瓜': '否'},
{'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '模糊', '脐部': '平坦', '触感': '硬滑', '好瓜': '否'},
{'色泽': '青绿', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '稍糊', '脐部': '稍凹', '触感': '硬滑', '好瓜': '否'},
]
clf = tree.DecisionTreeClassifier(criterion="entropy", splitter="random")
data = pd.DataFrame(D)
# print(data)
LabelEncoder = LabelEncoder()
for i in range(data.shape[1]):
data.iloc[:, i] =LabelEncoder.fit_transform(data.iloc[:, i])
# OneHotEncoder = OneHotEncoder()
# data = OneHotEncoder.fit_transform(data.iloc[:, :]).toarray()
# data = pd.DataFrame(data)
# print(data)
#
clf = clf.fit(data.iloc[:-4, :-1], data.iloc[:-4, -1])
score = clf.score(data.iloc[-4:, :-1], data.iloc[-4:, -1])
print("acc :", score)
feature_name = ['色泽', '根蒂', '敲声', '纹理', '脐部', '触感']
target_name = ['好瓜', '坏瓜']
dot_data = tree.export_graphviz(clf
, out_file=None
, feature_names=feature_name
, class_names=target_name
, filled=True
, rounded=True
)
graph = graphviz.Source(dot_data.replace('helvetica', '"Microsoft YaHei"'), encoding='utf-8')
graph.view("tree")
得到的树:
可以看到, sklearn将其当作浮点型特征进行了处理. 虽然4个测试样本的准确率大约维持在75%, 但我觉得还是 “变了味”.
1. 我们在调用各种库的时候, 同时也需要了解其底层的构造.
2. 独热编码和标签编码都是挺不错的数据处理方式, 但要注意到, 它们都不能对像 “大小”、“长短” 等带有程度意义的特征属性进行处理.
我在网上找到了一个能够对字符串型特征进行处理的决策树代码, 供大家借鉴学习. “鱼和熊掌不能兼得”, 它也不能处理浮点型特征数据.
from random import choice
from collections import Counter
import math
# 定义数据集
D = [
{'色泽': '青绿', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
{'色泽': '乌黑', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
{'色泽': '乌黑', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
{'色泽': '青绿', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
{'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '清晰', '脐部': '凹陷', '触感': '硬滑', '好瓜': '是'},
{'色泽': '青绿', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '软粘', '好瓜': '是'},
{'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '稍糊', '脐部': '稍凹', '触感': '软粘', '好瓜': '是'},
{'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '硬滑', '好瓜': '是'},
{'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '沉闷', '纹理': '稍糊', '脐部': '稍凹', '触感': '硬滑', '好瓜': '否'},
{'色泽': '青绿', '根蒂': '硬挺', '敲声': '清脆', '纹理': '清晰', '脐部': '平坦', '触感': '软粘', '好瓜': '否'},
{'色泽': '浅白', '根蒂': '硬挺', '敲声': '清脆', '纹理': '模糊', '脐部': '平坦', '触感': '硬滑', '好瓜': '否'},
{'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '模糊', '脐部': '平坦', '触感': '软粘', '好瓜': '否'},
{'色泽': '青绿', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '稍糊', '脐部': '凹陷', '触感': '硬滑', '好瓜': '否'},
{'色泽': '浅白', '根蒂': '稍蜷', '敲声': '沉闷', '纹理': '稍糊', '脐部': '凹陷', '触感': '硬滑', '好瓜': '否'},
{'色泽': '乌黑', '根蒂': '稍蜷', '敲声': '浊响', '纹理': '清晰', '脐部': '稍凹', '触感': '软粘', '好瓜': '否'},
{'色泽': '浅白', '根蒂': '蜷缩', '敲声': '浊响', '纹理': '模糊', '脐部': '平坦', '触感': '硬滑', '好瓜': '否'},
{'色泽': '青绿', '根蒂': '蜷缩', '敲声': '沉闷', '纹理': '稍糊', '脐部': '稍凹', '触感': '硬滑', '好瓜': '否'},
]
# ==========
# 决策树生成类
# ==========
class DecisionTree:
def __init__(self, D, label, chooseA):
self.D = D # 数据集
self.label = label # 哪个属性作为标签
self.chooseA = chooseA # 划分方法
self.A = list(filter(lambda key: key != label, D[0].keys())) # 属性集合A
# 获得A的每个属性的可选项
self.A_item = {}
for a in self.A:
self.A_item.update({a: set(self.getClassValues(D, a))})
self.root = self.generate(self.D, self.A) # 生成树并保存根节点
# 获得D中所有className属性的值
def getClassValues(self, D, className):
return list(map(lambda sample: sample[className], D))
# D中样本是否在A的每个属性上相同
def isSameInA(self, D, A):
for a in A:
types = set(self.getClassValues(D, a))
if len(types) > 1:
return False
return True
# 构建决策树,递归生成节点
def generate(self, D, A):
node = {} # 生成节点
remainLabelValues = self.getClassValues(D, self.label) # D中的所有标签
remainLabelTypes = set(remainLabelValues) # D中含有哪几种标签
if len(remainLabelTypes) == 1:
# 当前节点包含的样本全属于同个类别,无需划分
return remainLabelTypes.pop() # 标记Node为叶子结点,值为仅存的标签
most = max(remainLabelTypes, key=remainLabelValues.count) # D占比最多的标签
if len(A) == 0 or self.isSameInA(D, A):
# 当前属性集为空,或是所有样本在所有属性上取值相同,无法划分
return most # 标记Node为叶子结点,值为占比最多的标签
a = self.chooseA(D, A, self) # 划分选择
for type in self.A_item[a]:
condition = (lambda sample: sample[a] == type) # 决策条件
remainD = list(filter(condition, D)) # 剩下的样本
if len(remainD) == 0:
# 当前节点包含的样本集为空,不能划分
node.update({type: most}) # 标记Node为叶子结点,值为占比最多的标签
else:
# 继续对剩下的样本按其余属性划分
remainA = list(filter(lambda x: x != a, A)) # 未使用的属性
_node = self.generate(remainD, remainA) # 递归生成子代节点
node.update({type: _node}) # 把生成的子代节点更新到当前节点
return {a: node}
# 定义划分方法
# 随机选择
def random_choice(D, A, tree: DecisionTree):
return choice(A)
# 信息熵
def Ent(D, label, a, a_v):
D_v = filter(lambda sample: sample[a] == a_v, D)
D_v = map(lambda sample: sample[label], D_v)
D_v = list(D_v)
D_v_length = len(D_v)
counter = Counter(D_v)
info_entropy = 0
for k, v in counter.items():
p_k = v / D_v_length
info_entropy += p_k * math.log(p_k, 2)
return -info_entropy
# 信息增益
def information_gain(D, A, tree: DecisionTree):
gain = {}
for a in A:
gain[a] = 0
values = tree.getClassValues(D, a)
counter = Counter(values)
for a_v, nums in counter.items():
gain[a] -= (nums / len(D)) * Ent(D, tree.label, a, a_v)
return max(gain.keys(), key=lambda key: gain[key])
# 创建决策树
desicionTreeRoot = DecisionTree(D, label='好瓜', chooseA=information_gain).root
print('决策树:', desicionTreeRoot)
# 决策树可视化类
class TreeViewer:
def __init__(self):
from graphviz import Digraph
self.id_iter = map(str, range(0xffff))
self.g = Digraph('G', filename='decisionTree.gv')
def create_node(self, label, shape=None):
id = next(self.id_iter)
self.g.node(name=id, label=label, shape=shape, fontname="Microsoft YaHei")
return id
def build(self, key, node, from_id):
for k in node.keys():
v = node[k]
if type(v) is dict:
first_attr = list(v.keys())[0]
id = self.create_node(first_attr + "?", shape='box')
self.g.edge(from_id, id, k, fontsize='12', fontname="Microsoft YaHei")
self.build(first_attr, v[first_attr], id)
else:
id = self.create_node(v)
self.g.edge(from_id, id, k, fontsize='12', fontname="Microsoft YaHei")
def show(self, root):
first_attr = list(root.keys())[0]
id = self.create_node(first_attr + "?", shape='box')
self.build(first_attr, root[first_attr], id)
self.g.view()
# 显示创建的决策树
viewer = TreeViewer()
viewer.show(desicionTreeRoot)