分类问题的成本函数(cost function)通常是基尼指数(Gini index),即计算由分裂点产生的数据组的纯度(purity)。对于这样二元分类的分类问题来说,指数为 0 表示绝对纯度,说明类值被完美地分为两组。 从一棵决策树中找到最佳分裂点需要在训练数据集中对每个输入变量的值做成本评估。 在装袋算法和随机森林中,这个过程是在训练集的样本上执行并替换(放回)的。因为随机森林对输入的数据要进行行和列的采样。对于行采样,采用有放回的方式,也就是说同一行也许会在样本中被选取和放入不止一次。 我们可以考虑创建一个可以自行输入属性的样本,而不是枚举所有输入属性的值以期找到获取成本最低的分裂点,从而对这个过程进行优化。 该输入属性样本可随机选取且没有替换过程,这就意味着在寻找最低成本分裂点的时候每个输入属性只需被选取一次。 如下的代码所示,函数 get_split() 实现了上述过程。它将一定数量的来自待评估数据的输入特征和一个数据集作为参数,该数据集可以是实际训练集里的样本。辅助函数 test_split() 用于通过候选的分裂点来分割数据集,函数 gini_index() 用于评估通过创建的行组(groups of rows)来确定的某一分裂点的成本。 以上我们可以看出,特征列表是通过随机选择特征索引生成的。通过枚举该特征列表,开奖,我们可将训练集中的特定值评估为符合条件的分裂点。 # Select the best split point for a dataset def get_split(dataset, n_features): class_values = list(set(row[-1] for row in dataset)) b_index, b_value, b_score, b_groups = 999, 999, 999, None features = list() while len(features) < n_features: index = randrange(len(dataset[0])-1) if index not in features: features.append(index) for index in features: for row in dataset: groups = test_split(index, row[index], dataset) gini = gini_index(groups, class_values) if gini < b_score: b_index, b_value, b_score, b_groups = index, row[index], gini, groups return {'index':b_index, 'value':b_value, 'groups':b_groups} 至此,我们知道该如何改造一棵用于随机森林算法的决策树。我们可将之与装袋算法结合运用到真实的数据集当中。 2. 关于声纳数据集的案例研究 在这个部分,我们将把随机森林算法用于声纳数据集。本示例假定声纳数据集的 csv 格式副本已存在于当前工作目录中,文件名为 sonar.all-data.csv。 首先加载该数据集,将字符串转换成数字,并将输出列从字符串转换成数值 0 和 1. 这个过程是通过辅助函数 load_csv()、str_column_to_float() 和 str_column_to_int() 来分别实现的。 我们将通过 K 折交叉验证(k-fold cross validatio)来预估得到的学习模型在未知数据上的表现。这就意味着我们将创建并评估 K 个模型并预估这 K 个模型的平均误差。评估每一个模型是由分类准确度来体现的。辅助函数 cross_validation_split()、accuracy_metric() 和 evaluate_algorithm() 分别实现了上述功能。 装袋算法将通过分类和回归树算法来满足。辅助函数 test_split() 将数据集分割成不同的组;gini_index() 评估每个分裂点;前文提及的改进过的 get_split() 函数用来获取分裂点;函数 to_terminal()、split() 和 build_tree() 用以创建单个决策树;predict() 用于预测;subsample() 为训练集建立子样本集; bagging_predict() 对决策树列表进行预测。 新命名的函数 random_forest() 首先从训练集的子样本中创建决策树列表,然后对其进行预测。 正如我们开篇所说,随机森林与决策树关键的区别在于前者在建树的方法上的小小的改变,这一点在运行函数 get_split() 得到了体现。 完整的代码如下: # Random Forest Algorithm on Sonar Dataset from random import seed from random import randrange from csv import reader from math import sqrt # Load a CSV file def load_csv(filename): dataset = list() with open(filename, 'r') as file: csv_reader = reader(file) for row in csv_reader: if not row: continue dataset.append(row) return dataset # Convert string column to float def str_column_to_float(dataset, column): for row in dataset: row[column] = float(row[column].strip()) # Convert string column to integer def str_column_to_int(dataset, column): class_values = [row[column] for row in dataset] unique = set(class_values) lookup = dict() for i, value in enumerate(unique): lookup[value] = i for row in dataset: row[column] = lookup[row[column]] return lookup # Split a dataset into k folds def cross_validation_split(dataset, n_folds): dataset_split = list() dataset_copy = list(dataset) fold_size = len(dataset) / n_folds for i in range(n_folds): fold = list() while len(fold) < fold_size: index = randrange(len(dataset_copy)) fold.append(dataset_copy.pop(index)) dataset_split.append(fold) return dataset_split # Calculate accuracy percentage def accuracy_metric(actual, predicted): correct = 0 for i in range(len(actual)): if actual[i] == predicted[i]: correct += 1 return correct / float(len(actual)) * 100.0 # Evaluate an algorithm using a cross validation split def evaluate_algorithm(dataset, algorithm, n_folds, *args): folds = cross_validation_split(dataset, n_folds) scores = list() for fold in folds: train_set = list(folds) train_set.remove(fold) train_set = sum(train_set, []) test_set = list() for row in fold: row_copy = list(row) test_set.append(row_copy) row_copy[-1] = None predicted = algorithm(train_set, test_set, *args) actual = [row[-1] for row in fold] accuracy = accuracy_metric(actual, predicted) scores.append(accuracy) return scores # Split a dataset based on an attribute and an attribute value def test_split(index, value, dataset): left, right = list(), list() for row in dataset: if row[index] < value: left.append(row) else: right.append(row) return left, right # Calculate the Gini index for a split dataset def gini_index(groups, class_values): gini = 0.0 for class_value in class_values: for group in groups: size = len(group) if size == 0: continue proportion = [row[-1] for row in group].count(class_value) / float(size) gini += (proportion * (1.0 - proportion)) return gini # Select the best split point for a dataset def get_split(dataset, n_features): class_values = list(set(row[-1] for row in dataset)) b_index, b_value, b_score, b_groups = 999, 999, 999, None features = list() while len(features) < n_features: index = randrange(len(dataset[0])-1) if index not in features: features.append(index) for index in features: for row in dataset: groups = test_split(index, row[index], dataset) gini = gini_index(groups, class_values) if gini < b_score: b_index, b_value, b_score, b_groups = index, row[index], gini, groups return {'index':b_index, 'value':b_value, 'groups':b_groups} # Create a terminal node value def to_terminal(group): outcomes = [row[-1] for row in group] return max(set(outcomes), key=outcomes.count) # Create child splits for a node or make terminal def split(node, max_depth, min_size, n_features, depth): left, right = node['groups'] del(node['groups']) # check for a no split if not left or not right: node['left'] = node['right'] = to_terminal(left + right) return # check for max depth if depth >= max_depth: node['left'], node['right'] = to_terminal(left), to_terminal(right) return # process left child if len(left) <= min_size: node['left'] = to_terminal(left) else: node['left'] = get_split(left, n_features) split(node['left'], max_depth, min_size, n_features, depth+1) # process right child if len(right) <= min_size: node['right'] = to_terminal(right) else: node['right'] = get_split(right, n_features) split(node['right'], max_depth, min_size, n_features, depth+1) # Build a decision tree def build_tree(train, max_depth, min_size, n_features): root = get_split(dataset, n_features) split(root, max_depth, min_size, n_features, 1) return root # Make a prediction with a decision tree def predict(node, row): if row[node['index']] < node['value']: if isinstance(node['left'], dict): return predict(node['left'], row) else: return node['left'] else: if isinstance(node['right'], dict): return predict(node['right'], row) else: return node['right'] # Create a random subsample from the dataset with replacement def subsample(dataset, ratio): sample = list() n_sample = round(len(dataset) * ratio) while len(sample) < n_sample: index = randrange(len(dataset)) sample.append(dataset[index]) return sample # Make a prediction with a list of bagged trees def bagging_predict(trees, row): predictions = [predict(tree, row) for tree in trees] return max(set(predictions), key=predictions.count) # Random Forest Algorithm def random_forest(train, test, max_depth, min_size, sample_size, n_trees, n_features): trees = list() for i in range(n_trees): sample = subsample(train, sample_size) tree = build_tree(sample, max_depth, min_size, n_features) trees.append(tree) predictions = [bagging_predict(trees, row) for row in test] return(predictions) # Test the random forest algorithm seed(1) # load and prepare data filename = 'sonar.all-data.csv' dataset = load_csv(filename) # convert string attributes to integers for i in range(0, len(dataset[0])-1): str_column_to_float(dataset, i) # convert class column to integers str_column_to_int(dataset, len(dataset[0])-1) # evaluate algorithm n_folds = 5 max_depth = 10 min_size = 1 sample_size = 1.0 n_features = int(sqrt(len(dataset[0])-1)) for n_trees in [1, 5, 10]: scores = evaluate_algorithm(dataset, random_forest, n_folds, max_depth, min_size, sample_size, n_trees, n_features) print('Trees: %d' % n_trees) print('Scores: %s' % scores) print('Mean Accuracy: %.3f%%' % (sum(scores)/float(len(scores)))) 这里对第 197 行之后对各项参数的赋值做一个说明。 将 K 赋值为 5 用于交叉验证,得到每个子样本为 208/5 = 41.6,即超过 40 条声纳返回记录会用于每次迭代时的评估。 (责任编辑:本港台直播) |