用Python实现随机森林算法的示例


Posted in Python onAugust 24, 2017

拥有高方差使得决策树(secision tress)在处理特定训练数据集时其结果显得相对脆弱。bagging(bootstrap aggregating 的缩写)算法从训练数据的样本中建立复合模型,可以有效降低决策树的方差,但树与树之间有高度关联(并不是理想的树的状态)。

随机森林算法(Random forest algorithm)是对 bagging 算法的扩展。除了仍然根据从训练数据样本建立复合模型之外,随机森林对用做构建树(tree)的数据特征做了一定限制,使得生成的决策树之间没有关联,从而提升算法效果。

本教程将实现如何用 Python 实现随机森林算法。

  • bagged decision trees 与随机森林算法的差异;
  • 如何构建含更多方差的装袋决策树;
  • 如何将随机森林算法运用于预测模型相关的问题。

算法描述

这个章节将对随机森林算法本身以及本教程的算法试验所用的声纳数据集(Sonar dataset)做一个简要介绍。

随机森林算法

决策树运行的每一步都涉及到对数据集中的最优分裂点(best split point)进行贪婪选择(greedy selection)。

这个机制使得决策树在没有被剪枝的情况下易产生较高的方差。整合通过提取训练数据库中不同样本(某一问题的不同表现形式)构建的复合树及其生成的预测值能够稳定并降低这样的高方差。这种方法被称作引导聚集算法(bootstrap aggregating),其简称 bagging 正好是装进口袋,袋子的意思,所以被称为「装袋算法」。该算法的局限在于,由于生成每一棵树的贪婪算法是相同的,那么有可能造成每棵树选取的分裂点(split point)相同或者极其相似,最终导致不同树之间的趋同(树与树相关联)。相应地,反过来说,这也使得其会产生相似的预测值,降低原本要求的方差。

我们可以采用限制特征的方法来创建不一样的决策树,使贪婪算法能够在建树的同时评估每一个分裂点。这就是随机森林算法(Random Forest algorithm)。

与装袋算法一样,随机森林算法从训练集里撷取复合样本并训练。其不同之处在于,数据在每个分裂点处完全分裂并添加到相应的那棵决策树当中,且可以只考虑用于存储属性的某一固定子集。

对于分类问题,也就是本教程中我们将要探讨的问题,其被考虑用于分裂的属性数量被限定为小于输入特征的数量之平方根。代码如下:

num_features_for_split = sqrt(total_input_features)

这个小更改会让生成的决策树各不相同(没有关联),从而使得到的预测值更加多样化。而多样的预测值组合往往会比一棵单一的决策树或者单一的装袋算法有更优的表现。 

声纳数据集(Sonar dataset)

我们将在本教程里使用声纳数据集作为输入数据。这是一个描述声纳反射到不同物体表面后返回的不同数值的数据集。60 个输入变量表示声纳从不同角度返回的强度。这是一个二元分类问题(binary classification problem),要求模型能够区分出岩石和金属柱体的不同材质和形状,总共有 208 个观测样本。

该数据集非常易于理解——每个变量都互有连续性且都在 0 到 1 的标准范围之间,便于数据处理。作为输出变量,字符串'M'表示金属矿物质,'R'表示岩石。二者需分别转换成整数 1 和 0。

通过预测数据集(M 或者金属矿物质)中拥有最多观测值的类,零规则算法(Zero Rule Algorithm)可实现 53% 的精确度。

更多有关该数据集的内容可参见 UCI Machine Learning repository:https://archive.ics.uci.edu/ml/datasets/Connectionist+Bench+(Sonar,+Mines+vs.+Rocks)

免费下载该数据集,将其命名为 sonar.all-data.csv,并存储到需要被操作的工作目录当中。

教程

此次教程分为两个步骤。

1. 分裂次数的计算。

2. 声纳数据集案例研究

这些步骤能让你了解为你自己的预测建模问题实现和应用随机森林算法的基础

1. 分裂次数的计算

在决策树中,我们通过找到一些特定属性和属性的值来确定分裂点,这类特定属性需表现为其所需的成本是最低的。

分类问题的成本函数(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 =a 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 条声纳返回记录会用于每次迭代时的评估。

每棵树的最大深度设置为 10,每个节点的最小训练行数为 1. 创建训练集样本的大小与原始数据集相同,这也是随机森林算法的默认预期值。

我们把在每个分裂点需要考虑的特征数设置为总的特征数目的平方根,即 sqrt(60)=7.74,取整为 7。

将含有三组不同数量的树同时进行评估,以表明添加更多的树可以使该算法实现的功能更多。

最后,运行这个示例代码将会 print 出每组树的相应分值以及每种结构的平均分值。如下所示:

Trees: 1
Scores: [68.29268292682927, 75.60975609756098, 70.73170731707317, 63.41463414634146, 65.85365853658537]
Mean Accuracy: 68.780%
 
Trees: 5
Scores: [68.29268292682927, 68.29268292682927, 78.04878048780488, 65.85365853658537, 68.29268292682927]
Mean Accuracy: 69.756%
 
Trees: 10
Scores: [68.29268292682927, 78.04878048780488, 75.60975609756098, 70.73170731707317, 70.73170731707317]
Mean Accuracy: 72.683%

扩展

本节会列出一些与本次教程相关的扩展内容。大家或许有兴趣一探究竟。

  • 算法调校(Algorithm Tuning)。本文所用的配置参数或有未被修正的错误以及有待商榷之处。用更大规模的树,不同的特征数量甚至不同的树的结构都可以改进试验结果。
  • 更多问题。该方法同样适用于其他的分类问题,甚至是用新的成本计算函数以及新的组合树的预期值的方法使其适用于回归算法。

回顾总结

通过本次教程的探讨,你知道了随机森林算法是如何实现的,特别是:

随机森林与装袋决策树的区别。

如何用决策树生成随机森林算法。

如何将随机森林算法应用于解决实际操作中的预测模型问题。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python打印斐波拉契数列实例
Jul 07 Python
python实现解数独程序代码
Apr 12 Python
python 读取摄像头数据并保存的实例
Aug 03 Python
详解Python 正则表达式模块
Nov 05 Python
用Pytorch训练CNN(数据集MNIST,使用GPU的方法)
Aug 19 Python
python查找特定名称文件并按序号、文件名分行打印输出的方法
Apr 24 Python
Python爬虫实现百度翻译功能过程详解
May 29 Python
python利用faker库批量生成测试数据
Oct 15 Python
全网最详细的PyCharm+Anaconda的安装过程图解
Jan 25 Python
python 中 .py文件 转 .pyd文件的操作
Mar 04 Python
python-for x in range的用法(注意要点、细节)
May 10 Python
Python Pandas数据分析之iloc和loc的用法详解
Nov 11 Python
python利用urllib实现爬取京东网站商品图片的爬虫实例
Aug 24 #Python
python 接口_从协议到抽象基类详解
Aug 24 #Python
Python调用ctypes使用C函数printf的方法
Aug 23 #Python
使用Python实现博客上进行自动翻页
Aug 23 #Python
Python模拟鼠标点击实现方法(将通过实例自动化模拟在360浏览器中自动搜索python)
Aug 23 #Python
Python PyQt5标准对话框用法示例
Aug 23 #Python
Python PyQt5实现的简易计算器功能示例
Aug 23 #Python
You might like
php date()日期时间函数详解
2010/05/16 PHP
PHP安全技术之 实现php基本安全
2010/09/04 PHP
php curl_init函数用法
2014/01/31 PHP
windows中为php安装mongodb与memcache
2015/01/06 PHP
PHP实现多文件上传的方法
2015/07/08 PHP
又十个超级有用的PHP代码片段
2015/09/24 PHP
Zend Framework实现将session存储在memcache中的方法
2016/03/22 PHP
PHP中PDO的事务处理分析
2016/04/07 PHP
PHP封装XML和JSON格式数据接口操作示例
2019/03/06 PHP
Laravel框架集合用法实例浅析
2020/05/14 PHP
javascript编程起步(第四课)
2007/02/27 Javascript
php gethostbyname获取域名ip地址函数详解
2010/01/24 Javascript
推荐40个非常优秀的jQuery插件和教程【系列三】
2011/11/09 Javascript
ASP.NET jQuery 实例4(复制TextBox的文本到本地剪贴板上)
2012/01/13 Javascript
在JavaScript中处理时间之setMinutes()方法的使用
2015/06/11 Javascript
基于JS组件实现拖动滑块验证功能(代码分享)
2016/11/18 Javascript
jQuery中layer分页器的使用
2017/03/13 Javascript
Vue + Webpack + Vue-loader学习教程之相关配置篇
2017/03/14 Javascript
EasyUI框架 使用Ajax提交注册信息的实现代码
2017/09/27 Javascript
详解Angular操作cookies方法
2018/06/01 Javascript
js实现上传图片并显示图片名称
2019/12/18 Javascript
Python实现获取命令行输出结果的方法
2017/06/10 Python
解决python3在anaconda下安装caffe失败的问题
2017/06/15 Python
Python读取sqlite数据库文件的方法分析
2017/08/07 Python
Django使用Mysql数据库已经存在的数据表方法
2018/05/27 Python
在unittest中使用 logging 模块记录测试数据的方法
2018/11/30 Python
python3 字符串/列表/元组(str/list/tuple)相互转换方法及join()函数的使用
2019/04/03 Python
对python3.4 字符串转16进制的实例详解
2019/06/12 Python
python实现切割url得到域名、协议、主机名等各个字段的例子
2019/07/25 Python
python 如何实现遗传算法
2020/09/22 Python
土耳其国际性时尚购物网站:Modanisa
2018/01/19 全球购物
教师师德教育的自我评价
2013/10/31 职场文书
写好自荐信的技巧
2013/11/08 职场文书
多媒体编辑专业毕业生求职信
2014/06/13 职场文书
2015年三万活动总结
2015/03/25 职场文书
python爬虫之selenium库的安装及使用教程
2021/05/23 Python