本文将介绍数据科学领域大家都非常关心的一件事。事先完成一门机器学习 MOOC 课程并对 Python 有一些基础知识有助于理解文本,但没有也没关系。本文并不会向大家展示令人印象深刻的成果,而是回顾基础知识,试图帮助初学者找到方向。
文章结构:
- 介绍
- Kaggle 综述
- 建立自己的环境
- 预测房价竞赛简介
- 加载和检查数据
- 我们的模型:决策树介绍、偏差-方差权衡、随机森林
- 预处理数据
- 整合并提交结果
介绍
目前,我们能在网上找到很多高质量的免费机器学习教程,如 MOOC。一年以前,我在 Udacity 接触了「机器学习入门」课程,我认为它对于新手来说非常友好。在这里,我学到了机器学习基础概念、很多流行算法,以及 scikit-learn 的 API。在完成课程以后,我非常希望学到更多,但陷入了短暂的迷茫。
在做完一番研究后,我认为下一步最优的选择是进军 Kaggle,它是谷歌旗下的一个预测模型竞赛平台。没什么比自己动手进行实践更好了!
初次尝试 Kaggle 竞赛是很紧张刺激的,很多时候也伴随着沮丧(得到好成绩之后这种感觉似乎还加深了!),本文将着重介绍如何入门并开始你的第一场 Kaggle 竞赛,在这个过程中尽快成长。
Kaggle 综述
房价竞赛登录页面
(如果你已经熟悉 Kaggle 网站了,本段可以跳过)
Kaggle 上有两个最适合新手的竞赛(某种程度上已成为 Kaggle 的「入门教程」):
- Titanic(预测生存:一种二元分类问题):
- 房价(预测价格:回归问题):
https://www.kaggle.com/c/house-prices-advanced-regression-techniques
我强烈建议你两项都尝试一下,本文主要介绍后者。不过,其中需要的知识大部分是通用的,所以你完全可以看完本文,然后尝试其他 Kaggle 竞赛或者数据科学问题,所以选择挑战其他竞赛也没有问题!
在每个竞赛的「Overview」(概览)选项卡上,你可以看到关于比赛及其数据集的一些信息、提交有效结果的评估标准(每个竞赛都略有不同),以及该竞赛的 FAQ。
在「Data」(数据)选项卡上,你可以看到数据的简要说明。我们需要的是这三个文件:train.csv、test.csv 和 data_description.txt(这是至关重要的,因为其中包含数据的详细描述),请将它们放在你可以快速访问的文件夹里。
「Discussions」(讨论)选项卡就像竞赛的专属论坛——不过不要低估它!在流行的竞赛中,这些讨论中经常包含非常有价值的信息,因为竞赛条款有时会要求参与者必须在讨论版上公开他们所使用的任何信息。例如,数据泄露是很难避免和处理的,偶尔也会发生在竞赛中。一方面,充分利用数据才能得到更高的分数赢得竞赛;但另一方面,结合了数据泄露的模型通常对于实践来说是无用的,所以也不被竞赛支持——因为它们使用了「非法」信息。勤奋的参与者经常会在讨论版上分享数据泄露以帮助竞赛环境变得更好。此外,Kaggle 的成员也会经常在其上分享一些信息,努力维护这个社区。在排行榜上名列前茅的参与者有时也会在其中分享自己的成功经验(通常会在竞赛结束前后)。
「Kernel」选项卡基本上是「讨论」版块的应用、代码版,我认为这是对于初学者而言最重要的一个版块。任何人都可以在其中分享自己的脚本或笔记,链接任何数据集与竞赛,形式可以是文档、注释、可视化和输出,每个人都可以观看、投票、复制这些内容,甚至也可以在浏览器上直接运行它们!我刚才提到的两个竞赛(Titanic、房价竞赛)都形成了有趣、漂亮、成功的 Kernel,强烈推荐进行过自己的尝试之后浏览这个版块。Kaggle 正在不断提升 Kernel 的功能,现在甚至有一个「仅限 Kernel」、奖金为 10 万美元的竞赛。不过,Kernel 中的讨论往往是硬核的,缺乏有关概念的解释,或者说预先认为你已具备相关知识,所以有时了解起来会有些许困难。
建立自己的环境
我强烈推荐使用 Python3.6 在 Jupyter Notebook 环境中处理任何数据科学相关的工作(其中最流行的发行版称为「Anaconda」,包括 Python、Jupyter Notebook 和很多有用的库)。然后你就可以通过在终端(或者 Anaconda GUI)输入 Jupyter Notebook 随时启动该环境。除此之外,本文展示的内容也可以在 Kaggle 网站上的私人 Kernel 上完成(完全在浏览器上工作),这和 Jupyter Notebook 是等价的。
在开始之前,先介绍使用 Jupyter Notebook 的几个基本要点:
- 你可以输入任意的方法名,然后按 Tab 键查看所有可能选项;
- 类似地,选择任意方法,按 Shift-Tab 键几次可以在你的 notebook 中打开它的相关文档;
- 在任意语句之前输入%time 并执行该 cell,可以输出所需执行时间;
- 类似地,在任意语句之前输入%prun 并执行该 cell,可以令其在 Python 的代码分析器中运行,并输出结果。
可以在这里查看更多有用的命令:http://ipython.readthedocs.io/en/stable/interactive/magics.html
预测房价竞赛指南
目标概览
这是一个监督学习问题,意味着训练集中包含一系列的观察数据(行)和相关的多种信息(列)。其中一列是我们感兴趣并能够预测的信息,通常称其为目标变量或者因变量,在分类问题中称为标签、类。在我们的案例中,目标变量是房价。其它的列通常称为独立变量或特征。我们还有一个测试集,也包含一系列的观察数据,其中的列与训练集相同,除了目标变量,因为我们的目标就是预测目标变量的值。因此,完美情况下,我们要建立一个模型,该模型可以学习训练集中因变量和独立变量之间的关系,然后使用学习到的知识在测试集中尽可能准确地预测因变量(目标变量)的值。由于目标变量(房价)是连续的,可以取任意的值,因此这个问题属于回归问题。
加载和检查数据
现在我们已经成功启动了 Jupyter Notebook,首先要做的事情就是加载数据到 Pandas DataFrame 中。Pandas 可以处理 Python 中所有数据分析相关的工作,是很强大和流行的库,DataFrame 是它用于保存数据的对象名称。
按 Shift-Tab 几次,打开文档
最后一行使用了 Python 3.6 的字符串格式将从 Kaggle 下载的 CSV 文件(『comma-separated-values』,一种常用格式,可使用任何标准软件打开,例如 Excel)加载到 Pandas DataFrame 中。我们之后将频繁使用 read_csv,因此建议先浏览它的文档(这是一个好习惯)。加载数据并查看 DataFrame,可以发现数据集中的第一列是 Id,代表数据集中该行的索引,而不是真实观察值。因此,我修改了代码,加上 index_col=『Id』作为参数,从而在加载数据到 DataFrame 的时候,确保 Pandas 将其作为索引而不是列,并在它之前添加一个新的索引列。
现在,我们来看看训练集的样子:
训练集的数据结构
训练集总共有 80 列(除 Id 以外),其中 79 列是独立变量,1 列是因变量。因此,测试集应该只有 79 列(独立变量)。大多数的数字和字符串都没有什么意义,其中 Alley 列甚至全都是『NaN』,即值的丢失。别担心,我们之后会处理这个问题。下一步是考虑需要使用的模型。我们先讨论一下决策树(有时在应用到回归问题的时候称为回归树)。
如何构建我们的模型
决策树介绍
其基本思想是很简单的,当学习(拟合)训练数据的时候,回归树搜索所有独立变量和每个独立变量的所有值,以寻找能将数据最佳地分割为两组的变量和值(从数学角度来说,树总是选择能最小化两个节点的加权平均方差的分割),然后计算分数(最好是选定指标上的分数),以及每个组因变量的平均值。接着回归树递归地重复该过程,直到无法进一步分割(除非设置了具体的 max_depth,如下图所示)。树最后一级的每个节点都被称为『叶』,每一个都和因变量(在该叶相关的所有观察数据)的平均值相关。
旁注:这是一个『贪婪』算法的很好示例,在每一次分割中,算法检查了所有选项,然后选择了在该点的最佳选项,以期望最终得到全局最佳结果。当树拟合了训练数据之后,使用任何观察数据预测因变量的值时,只需要遍历树,直到抵达一个叶节点。
我们数据集的可视化示例,其中 max_depth 设为 3。
在树的每个节点,第一个元素是节点的分割规则(独立变量及其变量值),第二个元素是在该节点的所有观察数据的均方差(MSE),第三个元素是该节点的观察数据的数量(samples),即这一组的规模。最后一个元素 value 是目标变量(房价)的自然对数。该过程和贪婪算法类似,在每个节点局部地进行最佳分割,确实可以随着树的扩展减少均方差的值,并且每个叶节点都有一个相关的「SalePrice」值。
偏差-方差权衡
我们回忆一下监督学习的目标。一方面,我们希望模型可以通过拟合训练数据捕捉独立变量和因变量的关系,从而使其可以做出准确的预测。然而,模型还需要对(未见过的)测试数据进行预测。因此,我们还希望模型捕捉变量之间的普遍关系,从而可以进行泛化。该过程称为『偏差-方差权衡』。
如果模型没有充分拟合训练数据,它将会有高偏差(通常称为欠拟合),因此它的训练误差较大。然而,如果模型过于拟合训练数据,它会捕捉到变量之间的特殊关系(偶然的),导致高方差(通常称为过拟合),因此它的测试误差较大。所以,我们需要在偏差和方差之间进行权衡。
决策树过拟合
假定我们将一个回归树拟合到训练数据中。这个树将是什么结构?实际上,它将持续分割直到每个叶节点只有一个观察数据(无法再继续分离)。换种说法,回归树将为训练集的每一个观察数据建立一个独特路径,并根据观察数据在路径末端的叶节点上给出因变量的值。
如果将训练集中因变量的值删除,并用训练过的树预测因变量的值,结果如何?可以猜到,它将表现得很完美,达到基本 100% 的准确率和 0 均方差。因为它已经学习了训练集中每个观察数据的相关因变量值。
然而,如果我打算让树预测未见过的观察数据的因变量值,它将表现得很糟糕,因为任何未见过的观察数据都会在原来的树构建一个独特的叶节点。这正是一个过拟合的例子。可以通过调整树的参数减少过拟合,例如,限制树的 max_depth,但实际上还有更好的方法。
解决方案:随机森林
在机器学习中,我们通常会设计「元学习」以结合小模型的多个预测而生成更好的最终预测,这种方法一般可称为集成学习。特别的,当我们结合一些决策树为单个集成模型,我们可以将其称之为「Bootstrap Aggregating」或简单地称之为「Bagging」。通过这种方法构建的「元模型」是一种较为通用的解决方案,因此随机森林可以适用于广泛的任务。
随机森林简单而高效,当我们用这种方法拟合一个数据集时,就会像上文所述的那样构建许多决策树,只不过每个决策树是在数据的随机子集中构建,且在每一次分割中只考虑独立变量「特征」的随机子集。然后为了生成新的观察值,随机森林会简单地平均所有树的预测,并将其作为最终的预测返回。
现在我们所做的的就是构建许多弱分类器或弱决策树,然后取它们的平均值,为什么要这样做呢?
简单的回答就是它们确实工作地非常好,如果读者对随机森林的统计解释感兴趣的话,可以阅读更多的技术细节。但我不擅长于统计,但我会尽可能地给出一个基本的解释:bootstrap 采样和特征子集可以使不同的决策树尽可能地去相关(即使它们仍然基于相同的数据集和特征集),这种去相关能允许每一棵树在数据中发现一些不同的关系。这也就使它们的均方差要比任何单颗树都少得多,因此减少过拟合后它们能在总体上获得更好的预测和泛化结果。
简单来说,对于未见的观察结果,每个决策树预测该观察结果结束时所处叶节点的因变量值,即特定树空间中最类似的训练集观察结果。每棵树都是在不同的数据上构建的不同树,因此每棵树用不同的方式定义相似性,预测不同的值。因此对于给定未见观察结果,所有树的平均预测基本上就是训练集中与之类似的观察结果的值的平均值。
此特性的影响之一是:尽管随机森林在测试集与训练集相似度较高时(值属于同样的范围)非常擅长预测,但当测试集与训练集存在根本区别时(不同范围的值),随机森林的预测性能很差,比如时序问题(训练集和测试集不属于同样的时间段)。
不过我们的案例中测试集和训练集具备同样范围的值,因此这对我们没有太大影响。
回到比赛
预处理数据
我们在让随机森林运行起来之前还有一件事要做:随机森林虽然理论上可以应对分类特征(非数据形式:字符串)和数据缺失,scikit-learn 实现却并不支持这两种情况。所以我们需要使用 pd.interpolate() 来填充缺失的值,然后使用 pd.get_dummies() 的『One-Hot Encoding』来将分类特征转换为数字特征。这个方法非常简单,让我们假设一个分类变量有 n 个可能值。该列被分为 n 个列,每一列对应一个原始值(相当于对每个原始值的『is_value』)。每个观察值(以前有一个分类变量的字符串值),现在在旧字符串值对应的列上有一个 1,而其他所有列上为 0。
我们现在准备构建一个模型,使用数据进行训练,并用它来预测测试集,然后将结果提交到 Kaggle 上。
整合结果并提交
这就是我们的模型提交 Kaggle 所需的所有代码——大约 20 行!我们运行这些代码,随后继续向 Kaggle 提交结果——得分为 0.14978,目前排行约为 63%。对于五分钟的代码编写来说,结果不错!在这里我们可以看到随机森林的力量。
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
PATH = "Oren/Kaggle/Housing Prices/" #where you put the files
df_train = pd.read_csv(f'{PATH}train.csv', index_col='Id')
df_test = pd.read_csv(f'{PATH}test.csv', index_col='Id')
target = df_train['SalePrice'] #target variable
df_train = df_train.drop('SalePrice', axis=1)
df_train['training_set'] = True
df_test['training_set'] = False
df_full = pd.concat([df_train, df_test])
df_full = df_full.interpolate()
df_full = pd.get_dummies(df_full)
df_train = df_full[df_full['training_set']==True]
df_train = df_train.drop('training_set', axis=1)
df_test = df_full[df_full['training_set']==False]
df_test = df_test.drop('training_set', axis=1)
rf = RandomForestRegressor(n_estimators=100, n_jobs=-1)
rf.fit(df_train, target)
preds = rf.predict(df_test)
my_submission = pd.DataFrame({'Id': df_test.index, 'SalePrice': preds})
my_submission.to_csv(f'{PATH}submission.csv', index=False)
说明
在将训练集和测试集分别加载进 DataFrame 之后,我保存了目标变量,并在 DataFrame 中删除它(因为我只想保留 DataFrame 中的独立变量和特征)。随后,我在训练集和测试集中添加了一个新的临时列('training_set'),以便我们可以将它们连接在一起(将它们放在同一个 DataFrame 中),然后再将它们分开。我们继续整合它们,填充缺失的数值,并通过独热编码(One-Hot Encoding)将分类特征转换为数字特征。
正如之前所述的,随机森林(以及其他大多数算法)都会在训练集和测试集有差不多数值的情况下工作良好,所以在修改内容的时候我希望对两个数据集进行同样的修改。否则,interpolate 可能会在训练集和测试集上填入不同的数值,而 get_dummies 可能会以两种不同的方式对相同的分类特征进行编码,从而导致性能下降。随后我再将其分开,去掉临时列,构建一个有 100 个树的随机森林(通常,树越多结果越好,但这也意味着训练时间的增加),使用计算机的所有 CPU 核心(n_jobs=-1),使用训练集进行拟合,用拟合的随机森林来预测测试集的目标变量,把结果和它们各自的 Id 放在一个 DataFrame 中,并保存到 一个 CSV 文件中。随后登陆 Kaggle 页面提交 CSV 文件,大功告成!
原文发布时间为:2018-01-26