赞
踩
课上的实验
Adult数据集可以在Azure官网上找到
Azure 开放数据集中的数据集 - Azure Open Datasets | Microsoft Learn
其实决策树并没什么太难的地方,主要是使用的python,pandas库在划分数据集时如果使用单行遍历会很慢,此时需要找到符合功能需求的批处理函数
决策树主要分为以下几个模块
1、计算信息熵(D表示数据集,|D|表示数据集大小,Di表示分类结果为i的数据集)
信息熵:
条件信息熵:(按照属性A划分之后的信息熵加权平均数,D(j)表示属性A为j的数据集)
2、获取数据集中的众数。作为叶节点的信息
3、将数据集按照某个关键字划分。这里很坑,如果单行遍历划分回巨慢无比,但是pandas有专门的批处理函数groupby用于划分(划分时间直接从30+s优化到0.0s),但是如果当前值不存在会发生报错,所以要单独加入一个占位的DataFrame
4、决策树划分策略
按照C4.5决策树的划分规则,需要计算信息增益比
信息增益:
信息增益比
所以,在寻找最优划分策略的时候需要枚举每一个未划分的属性,计算划分后的数据集的信息增益比,选择信息增益比最高的属性进行划分即可
5、决策树构建
由于决策树很容易过拟合,所以这里使用了两种剪枝方法,首先设置节点纯度阈值,当递归时节点纯度高于阈值时可以直接选用当前数据集的众数作为节点值,停止递归。然后设置深度阈值,当超过该深度时就取当前数据集的众数作为节点值,停止递归。
构建过程:
由于是进行的递归构建,相当于在对最优决策树做一个先根遍历,首先对于当前节点,在决策树存储矩阵上添加一行,存储当前节点的决策信息;然后将每个儿子返回的矩阵依次append到这个矩阵下方,利用当前的矩阵行数计算儿子行标相对于当前节点行标的增量。完成构建之后,为了后续方便查询,对每个节点的用当前的行数加上儿子节点的增量,就可以算出儿子节点对应的行数。
6、决策树分类过程
对于每个数据组,从决策树根节点开始,选择决策树节点划分的属性,将当前节点id跳转到数据该属性对应值的儿子节点即可,直到跳转到叶子节点停止
训练集分类结果(准确率0.833)
测试集分类结果(准确率0.836)
效果还是不错,堪比神经网络?
这次实验花费了很长时间在数据集的分析和处理上
包括年龄和资本收支的分箱、离散值归并,并且发现了测试集数据中income标签与训练集不同的问题。
决策树构建过程中花费了许多时间去查询pandas的批处理函数,如果之前有pandas库调用的基础会好很多。
决策树存储结构选用numpy是不太合适的,因为每一个节点的结构儿子个数是不定的,如果按照最多分支数来设置矩阵的列数会有很多空间是浪费的。使用list+dict保存每个节点的数据,用json文件存储读取应该会方便一些。
- columns = ['age', 'workclass', 'fnlwgt', 'education', 'educationNum', 'maritalStatus', 'occupation', 'relationship', 'race', 'sex',
- 'capitalGain', 'capitalLoss', 'hoursPerWeek', 'nativeCountry', 'income']
- df_train_set = pd.read_csv('./adult.data', names=columns)
- df_test_set = pd.read_csv('./adult.test', names=columns, skiprows=1) #第一行是非法数据
-
- print(df_train_set.head())
- print(df_test_set.head())
- df_train_set.to_csv('./train_adult.csv', index=False)
- df_test_set.to_csv('./test_adult.csv', index=False)
- def preprocess(in_path, out_path):
- df = pd.read_csv(in_path)
- df.drop(['fnlwgt', 'educationNum'], axis=1, inplace=True) # fnlwgt列用处不大,educationNum与education类似
- print(df.columns)
- df.drop_duplicates(inplace=True) # 去除重复行
- df.dropna(inplace=True) # 去除空行
- print(df.shape[0])
- # 查找异常值, 避免与正则表达式的?冲突需要转义
- new_columns = ['workclass', 'maritalStatus', 'occupation', 'relationship', 'race', 'sex', 'nativeCountry', 'income']
- for col in new_columns:
- df = df[~df[col].str.contains(r'\?', regex=True)]
- print(df.shape[0])
- continuous_column = ['age', 'capitalGain', 'capitalLoss', 'hoursPerWeek']
- bins = [0, 23, 33, 43, 55, 100]
- df['age'] = pd.cut(df['age'], bins, labels=False)
- bins = [-1, 10000, 50000, 150000]
- df['capitalGain'] = pd.cut(df['capitalGain'], bins, labels=False)
- bins = [-1, 1000, 2000, 10000]
- df['capitalLoss'] = pd.cut(df['capitalLoss'], bins, labels=False)
- bins = [0, 20, 40, 60, 80, 100]
- df['hoursPerWeek'] = pd.cut(df['hoursPerWeek'], bins, labels=False)
-
- discrete_column = ['workclass', 'education', 'maritalStatus', 'occupation', 'relationship', 'race', 'sex', 'nativeCountry', 'income']
- workclass_mapping = {' Private': 0, ' Self-emp-not-inc': 1, ' Self-emp-inc': 1, ' Local-gov': 2,
- ' State-gov': 2, ' Federal-gov': 2, ' Without-pay': 3, ' Never-worked': 3}
- df['workclass'] = df['workclass'].map(workclass_mapping)
- education_mapping = {' Preschool':0 , ' 1st-4th': 0, ' 5th-6th': 0, ' 7th-8th': 1, ' 9th': 1, ' 10th': 2, ' 11th': 2, ' 12th': 2, ' HS-grad': 3, ' Some-college': 3, ' Assoc-voc': 3,' Assoc-acdm': 3, ' Bachelors': 4, ' Masters': 4, ' Prof-school': 4,' Doctorate': 4}
- df['education'] = df['education'].map(education_mapping)
- maritalStatus_mapping = {' Never-married': 0, ' Married-civ-spouse': 1, ' Married-spouse-absent': 1, ' Married-AF-spouse': 2, ' Divorced': 3, ' Separated': 3, ' Widowed': 3}
- df['maritalStatus'] = df['maritalStatus'].map(maritalStatus_mapping)
- occupation_mapping = {' Prof-specialty': 0, ' Exec-managerial': 1, ' Adm-clerical': 2, ' Craft-repair': 3,
- ' Sales': 4, ' Other-service': 5, ' Machine-op-inspct': 6, ' Transport-moving': 7,
- ' Handlers-cleaners': 8, ' Farming-fishing': 9, ' Tech-support': 10,
- ' Protective-serv': 11, ' Priv-house-serv': 12, ' Armed-Forces': 13}
- df['occupation'] = df['occupation'].map(occupation_mapping)
- relationship_mapping = {' Unmarried': 0, ' Wife': 1,' Husband': 2,' Own-child': 3, ' Not-in-family': 4, ' Other-relative': 5}
- df['relationship'] = df['relationship'].map(relationship_mapping)
- race_mapping = {' White': 0, ' Black': 1, ' Asian-Pac-Islander': 2, ' Amer-Indian-Eskimo': 3,' Other': 4}
- df['race'] = df['race'].map(race_mapping)
- sex_mapping = {' Male': 0, ' Female': 1}
- df['sex'] = df['sex'].map(sex_mapping)
- nativeCountry_mapping = {' United-States': 0, ' Mexico': 1, ' Philippines': 1, ' Germany': 1, ' Puerto-Rico': 1,
- ' Canada': 1, ' India': 1, ' El-Salvador': 1, ' Cuba': 1, ' England': 1, ' Jamaica': 1,
- ' South': 1, ' China': 1, ' Italy': 1, ' Dominican-Republic': 1, ' Vietnam': 1,
- ' Guatemala': 1, ' Japan': 1, ' Poland': 1, ' Columbia': 1, ' Iran': 1, ' Taiwan': 1,
- ' Haiti': 1, ' Portugal': 1, ' Nicaragua': 1, ' Peru': 1, ' Greece': 1, ' France': 1,
- ' Ecuador': 1, ' Ireland': 1, ' Hong': 1, ' Cambodia': 1, ' Trinadad&Tobago': 1,
- ' Thailand': 1, ' Laos': 1, ' Yugoslavia': 1, ' Outlying-US(Guam-USVI-etc)': 1,
- ' Hungary': 1, ' Honduras': 1, ' Scotland': 1, ' Holand-Netherlands': 1}
- df['nativeCountry'] = df['nativeCountry'].map(nativeCountry_mapping)
- income_mapping = {' <=50K': 0, ' <=50K.': 0,' >50K': 1,' >50K.': 0,}
- df['income'] = df['income'].map(income_mapping)
- df.to_csv(out_path, index=False)
- preprocess('./train_adult.csv','./train_adult_processed.csv')
- preprocess('./test_adult.csv','./test_adult_processed.csv')
- # 读数据,初始化决策树参数,预设每行的范围
- df_train = pd.read_csv('./train_adult_processed.csv') # 读取预处理后的数据
- columns = df_train.columns.to_list()
- flags = [0 for i in range(len(columns))]
- column_range_map = {columns[i]:df_train.iloc[:,i].value_counts().index.max()+1 for i in range(len(columns))}
- print(column_range_map)
-
- pure_rate_thresh = 0.87 #纯度阈值 用于决策树剪枝
- deep_thresh = 7 #深度阈值 用于决策树剪枝
- def calc_entropy(df):
- """
- 计算数据集income的信息熵
- :param df: 数据集
- :return: 信息熵
- """
- all = len(df)
- if all == 0:
- return 0
- cnts = [list(df['income']).count(0), list(df['income']).count(1)]
- sum = 0
- for cnt in cnts:
- if cnt > 0:
- sum -= cnt/all*math.log2(cnt/all)
- return sum
- def calc_mode(df):
- """
- 计算数据集income的众数
- :param df: 数据集
- :return: 众数
- """
- #如果没有数据可以支撑决策,就随机输出
- if len(df) == 0:
- return random.randint(0, 1)
- cnt = [list(df['income']).count(0), list(df['income']).count(1)]
- return 0 if cnt[0]>cnt[1] else 1
- def split_dataset(df, index):
- """
- 按照给定的列划分数据集
- :param df: 原始数据集
- :param index: 指定特征的列索引
- :return: 切分后的数据集
- """
- df_list = []
- for val in range(column_range_map[index]):
- if val in list(df[index]):
- df_list.append(df.groupby(index).get_group(val))
- else:
- df_list.append(pd.DataFrame([],columns=columns))
- return df_list
-
- def choose_best_feature_to_split(df, flags):
- """
- 选择最好的特征进行分裂
- :param df: 数据集
- :param flags: 区分特征是否被完全区分开,初始为全0, 若某个特征被区分开那么flags对应的下标为1
- :return: best_value:(分裂特征的index, 特征的值), best_df:(分裂后的子树数据集), best_gain:(选择该属性分裂的最大信息增益率)
- """
- h_df = calc_entropy(df)
- best_value = -1
- best_df = []
- best_gain = 0
- cols = df.columns.to_list()
- for i in range(0,len(cols)-1): # 注意不能直接划分income
- if(flags[i]):
- continue
- index = cols[i]
- df_list = split_dataset(df,index)
- h_df_a = 0
- split_info = 1e-10
- # 采用C4.5方法,计算信息增益比
- for sdf in df_list:
- if len(sdf) == 0 :
- continue
- h_df_a += len(sdf)/len(df) * calc_entropy(sdf)
- split_info -= len(sdf)/len(df) * math.log2(len(sdf)/len(df))
- now_gain = (h_df - h_df_a)/ split_info
- if best_gain < now_gain:
- best_df = df_list
- best_value = i
- best_gain = now_gain
- return (best_value,best_df,best_gain)
-
- def build_decision_tree(df, flags, fadep):
- """
- 构建决策树
- :param df: 数据集
- :param flags: 区分特征是否被完全区分开,初始为全0, 若某个特征被区分开那么flags对应的下标为1
- :return: 决策树
- """
- dep = fadep + 1
- if len(df) == 0 or flags.count(1) == len(flags) or dep > deep_thresh: #该数据集为空 或者 所有关键字都被划分 或者 超出决策树深度阈值 返回
- leaf = np.zeros((1,16),dtype='int')
- leaf[0][0]= calc_mode(df)
- leaf[0][1]= 0
- return leaf
- cnt = [list(df['income']).count(0), list(df['income']).count(1)]
- if max(cnt[0],cnt[1])/(len(df)) > pure_rate_thresh:# 高于纯度阈值 返回
- leaf = np.zeros((1,16),dtype='int')
- leaf[0][0]= 0 if cnt[0]>cnt[1] else 1
- leaf[0][1]= 0
- return leaf
- best_value,best_df,best_gain = choose_best_feature_to_split(df,flags)
- # 创建该子树的根节点
- tree = np.zeros((1,16),dtype='int')
- tree[0][0] = best_value
- tree[0][1] = len(best_df)
- son_cnt = 0
- # 递归加入儿子的节点列表
- for sdf in best_df:
- son_flags = copy.deepcopy(flags)
- son_flags[best_value] = 1
- son_tree = build_decision_tree(sdf, son_flags, dep)
- # 计算当前儿子节点的行标 相对于 当前行标的增量
- tree[0][son_cnt+2] = tree.shape[0]
- # 把儿子的所有节点拼接进来
- tree = np.append(tree,son_tree,axis=0)
- son_cnt += 1
- return tree
-
- def save_decision_tree(decision_tree):
- """
- 决策树的存储
- :param decision_tree: 训练好的决策树
- :return: void
- """
- np.save('decision_tree.npy', decision_tree)
-
-
- def load_decision_tree():
- """
- 决策树的加载
- :return: 保存的决策树
- """
- decision_tree = np.load('decision_tree.npy', allow_pickle=True)
- return decision_tree
- decision_tree = build_decision_tree(df_train, flags, 0)
- # 由于当前算出来的是偏移量,需要进行重定位
- for i in range(decision_tree.shape[0]):
- for j in range(2,2+decision_tree[i][1]):
- decision_tree[i][j] += i
- print(decision_tree)
- print(decision_tree.shape[0])
- save_decision_tree(decision_tree)
- def classify(tree, id, df_row):
- """
- 用训练好的决策树进行分类
- :param cart:决策树模型
- :param id:当前在决策树上的节点编号
- :param df_row: 一条测试样本
- :return: 预测结果
- """
- while id >= 0 and id < len(tree):
- if tree[id][1] == 0:
- return tree[id][0]
- id = tree[id][2+df_row.iloc[ tree[id][0] ]]
- return -1
-
-
- def predict(decision_tree, df):
- """
- 用训练好的决策树进行分类
- :param cart:决策树模型
- :param df: 所有测试集
- :param columns: 特征列表
- :return: 预测结果
- """
- print("predict start!")
- pred_list = []
- for i in range(len(df)):
- pred_label = classify(decision_tree, 0, df.iloc[i])
- if pred_label == -1:
- pred_label = random.randint(0, 1) # 防止classify执行到返回-1,但一般不会执行到返回-1
- pred_list.append(pred_label)
- return pred_list
-
- def calc_acc(pred_list, test_list):
- """
- 返回预测准确率
- :param pred_list: 预测列表
- :param test_list: 测试列表
- :return: 准确率
- """
- pred = np.array(pred_list)
- test = np.array(test_list)
- acc = np.sum(pred_list == test_list) / len(test_list)
- return acc
- #检验训练集
- np.set_printoptions(threshold=np.inf)
- df_train = pd.read_csv('./train_adult_processed.csv')
- decision_tree = load_decision_tree() # 加载模型
- test_list = df_train['income'].to_numpy()
- pred_list = predict(decision_tree, df_train)
- acc = calc_acc(pred_list, test_list)
- print(acc)
- #检验测试集
- df_test = pd.read_csv('./test_adult_processed.csv')
- decision_tree = load_decision_tree() # 加载模型
- test_list = df_test['income'].to_numpy()
- pred_list = predict(decision_tree, df_test)
- acc = calc_acc(pred_list, test_list)
- print(acc)
-
- #测试集结果保存到文件
- file = open("result.txt", "w")
- file.write(str(pred_list)+'\n')
- file.write(str(acc))
- file.close()
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。