用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之import机制详解
Jul 03 Python
Python中的map()函数和reduce()函数的用法
Apr 27 Python
Python使用filetype精确判断文件类型
Jul 02 Python
Python 装饰器使用详解
Jul 29 Python
Python实现抓取HTML网页并以PDF文件形式保存的方法
May 08 Python
解决安装python库时windows error5 报错的问题
Oct 21 Python
老生常谈python中的重载
Nov 11 Python
python列表每个元素同增同减和列表元素去空格的实例
Jul 20 Python
解决pytorch报错:AssertionError: Invalid device id的问题
Jan 10 Python
Python configparser模块应用过程解析
Aug 14 Python
python Protobuf定义消息类型知识点讲解
Mar 02 Python
在前女友婚礼上,用Python破解了现场的WIFI还把名称改成了
May 28 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
Laravel框架模板继承操作示例
2018/06/11 PHP
javascript IE中的DOM ready应用技巧
2008/07/23 Javascript
兼容IE、FireFox、Chrome等浏览器的xml处理函数js代码
2011/11/30 Javascript
灵活应用js调试技巧解决样式问题的步骤分享
2012/03/15 Javascript
javascript获取作用在元素上面的样式属性代码
2012/09/20 Javascript
javascript 系统文件夹文件操作及参数介绍
2013/01/08 Javascript
深入理解JavaScript系列(35):设计模式之迭代器模式详解
2015/03/03 Javascript
Backbone.js的Hello World程序实例
2015/06/19 Javascript
vue-cli+webpack记事本项目创建
2017/04/01 Javascript
vue2.0开发入门笔记之.vue文件的生成和使用
2017/09/19 Javascript
微信小程序之选项卡的实现方法
2017/09/29 Javascript
JavaScript实现QQ列表展开收缩扩展功能
2017/10/30 Javascript
react脚手架如何配置less和ant按需加载的方法步骤
2018/11/28 Javascript
详解Vue 匿名、具名和作用域插槽的使用方法
2019/04/22 Javascript
vue-cli脚手架引入弹出层layer插件的几种方法
2019/06/24 Javascript
微信h5静默和非静默授权获取用户openId的方法和步骤
2020/06/08 Javascript
Python中比较特别的除法运算和幂运算介绍
2015/04/05 Python
scrapy爬虫实例分享
2017/12/28 Python
Python连接Mssql基础教程之Python库pymssql
2018/09/16 Python
python二维码操作:对QRCode和MyQR入门详解
2019/06/24 Python
python matplotlib画盒图、子图解决坐标轴标签重叠的问题
2020/01/19 Python
基于Python获取docx/doc文件内容代码解析
2020/02/17 Python
python 爬虫网页登陆的简单实现
2020/11/30 Python
使用python实现学生信息管理系统
2021/02/25 Python
Amara美国站:英国高端家居礼品网站,世界各地的奢侈家具品牌
2017/07/26 全球购物
Groupon西班牙官方网站:在线优惠券和交易,节省高达70%
2021/03/13 全球购物
英国健身专家:WIT Fitness
2021/02/09 全球购物
卫校中专生的自我评价
2014/01/15 职场文书
《蜗牛的奖杯》教后反思
2014/04/24 职场文书
5s推行计划书
2014/05/06 职场文书
爱国主义演讲稿
2014/05/07 职场文书
交通事故和解协议书
2015/01/27 职场文书
民事诉讼答辩状范文
2015/05/21 职场文书
干货:如何写好工作计划!
2019/05/17 职场文书
创业计划书之婴幼儿游泳馆
2019/09/11 职场文书
python超详细实现完整学生成绩管理系统
2022/03/17 Python