机器学习实战:基于Scikit-Learn、Keras和TensorFlow(原书第3版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.3 获取数据

是时候动手了。不要犹豫,拿起你的笔记本计算机并浏览代码示例。正如我在前言中提到的,本书中的所有代码示例都是开源的,可以作为Jupyter notebook在线获取(https://github.com/ageron/handson-ml3),它们是包含文本、图像和可执行代码片段的交互式文档(在我们的示例中是Python)。在本书中,假设你在Google Colab上运行这些代码,这是一项免费服务,可让你直接在线运行任何Jupyter notebook,而无须在你的机器上安装任何东西。如果你想使用其他在线平台(例如Kaggle),或者如果你想在自己的机器上本地安装所有内容,请参阅本书的GitHub页面上的说明。

2.3.1 使用Google Colab运行代码示例

首先,打开网络浏览器并访问https://homl.info/colab3:这将带你进入Google Colab,它将显示本书的Jupyter notebook列表(见图2-3)。你会发现每章有一个notebook,外加一些额外的notebook以及NumPy、Matplotlib、Pandas、线性代数和微积分的教程。例如,如果你单击02_end_to_end_machine_learning_project.ipynb,那么第2章中的notebook将在Google Colab中打开(见图2-4)。

图2-3:Google Colab中的notebook列表

Jupyter notebook由单元格列表组成。每个单元格包含可执行代码或文本。尝试双击第一个文本单元格(其中包含句子“Welcome to Machine Learning Housing Corp.!”)。这将打开单元格进行编辑。请注意,Jupyter notebook使用Markdown语法进行格式化(例如,**粗体**、*斜体*、#标题、[url](链接文本)等)。尝试修改此文本,然后按Shift-Enter查看结果。

图2-4:你在Google Colab中的notebook

接下来,通过从菜单中选择Insert→“Code cell”来创建一个新的代码单元格。或者,你可以单击工具栏中的+Code按钮,或将鼠标悬停在单元格底部,直到看到+Code和+Text出现,然后单击+Code。在新的代码单元格中,键入一些Python代码,例如print(“Hello World”),然后按Shift-Enter运行此代码(或单击单元格左侧的按钮)。

如果你尚未登录Google账户,则系统会要求你立即登录(如果你还没有Google账户,则需要创建一个)。登录后,当你尝试运行代码时,你会看到一条安全警告,告诉你此notebook不是由Google创作的。一个恶意的人可能会创建一个notebook,试图诱骗你输入你的Google凭据,然后就可以访问你的个人数据,因此在你运行notebook之前,请始终确保你信任其作者(在运行它之前,仔细检查每个代码单元格将执行的操作)。假设你相信我(或者你计划检查每个代码单元格),你现在可以单击“Run anyway”。

Colab然后会为你分配一个新的运行时:这是一个位于Google服务器上的免费虚拟机,其中包含一堆工具和Python库,包括本书大多数章节所需的一切(在某些章节中,你需要运行安装附加库的命令)。这需要几秒钟。接下来,Colab将自动连接到此运行时并使用它来执行你的新代码单元。重要的是,这些代码在运行时运行,而不是在你的机器上运行。代码的输出将显示在单元格下方。恭喜,你已经在Colab上运行了一些Python代码!

要插入新的代码单元格,你还可以键入Ctrl-M(或macOS上的Cmd-M),然后键入A(在活动单元格的上方插入)或B(在下方插入)。还有许多其他可用的键盘快捷键:你可以通过键入Ctrl-M(或Cmd-M)然后键入H来查看和编辑它们。如果你选择在Kaggle或你自己的机器上使用JupyterLab或带有Jupyter扩展的Visual Studio Code等IDE来运行notebook,你会看到一些细微差别——运行时称为内核,用户界面和键盘快捷键略有不同,等等——但是从一个Jupyter环境切换到另一个环境并不难。

2.3.2 保存你的代码更改和数据

你可以对Colab notebook进行更改,只要你保持浏览器选项卡打开,它们就会一直存在。但是一旦关闭它,所做的更改就会丢失。为避免这种情况,请确保通过选择File→“Save a copy in Drive”将notebook的副本保存到你的Google Drive。或者,你可以通过选择File→Download→“Download.ipynb”将notebook下载到你的计算机。然后你可以稍后访问https://colab.research.google.com并再次打开notebook(从Google Drive或从你的计算机上传)。

Google Colab仅供交互使用:你可以在notebook中随意调整代码,但不能让notebook长时间无人值守,否则运行时将关闭并且所有它的数据将丢失。

如果notebook生成了你关心的数据,请确保在运行时关闭之前下载此数据。为此,单击文件图标(见图2-5中的步骤1),找到你要下载的文件,单击它旁边的垂直点(步骤2),然后单击下载(步骤3)。或者,你可以将Google Drive挂载在运行时上,让notebook可以直接将文件读写到Google Drive,就好像它是本地目录一样。为此,单击文件图标(第1步),然后单击Google Drive图标(在图2-5中圈出)并按照屏幕上的说明进行操作。

图2-5:从Google Colab运行时下载文件(第1步到第3步),或装载你的Google Drive(带圆圈的图标)

默认情况下,你的Google Drive将安装在/content/drive/MyDrive。如果要备份数据文件,只需运行!cp/content/my_great_model/content/drive/MyDrive将其复制到此目录即可。任何以开头的命令都被视为shell命令,而不是Python代码。cp是Linux shell命令,用于将文件从一个路径复制到另一个路径。请注意,Colab运行时在Linux(特别是Ubuntu)上运行。

2.3.3 交互性的力量和危险

Jupyter notebook是交互式的,这是一件很棒的事情:你可以一个一个地运行每个单元格、在任何时候停止、插入一个单元格研究代码、返回并再次运行同一个单元格,等等。我强烈建议你这样做。如果你只是一个一个地运行单元格而不去动手尝试它们,你就不会学得那么快。然而,这种灵活性是有代价的:很容易以错误的顺序运行单元格,或者忘记运行一个单元格。如果发生这种情况,后续的代码单元格很可能会失败。例如,每个notebook中的第一个代码单元格包含设置代码(例如导入),因此请确保先运行它,否则所有单元格将无法运行。

如果遇到奇怪的错误,请尝试重新启动运行时(通过从菜单中选择Runtime→“Restart runtime”),然后从notebook的开头再次运行所有单元格。这通常可以解决问题。如果问题未解决,则可能是你所做的其中一项更改破坏了notebook:只需恢复到原始notebook并重试。如果仍然失败,请在GitHub上提交问题。

2.3.4 本书代码与notebook代码

有时你可能会注意到本书代码与notebook代码之间存在一些细微差别。发生这种情况可能有以下几种原因:

· 当你阅读这些内容时,代码库可能已经发生了细微的变化,或者尽管我尽了最大的努力,但我还是在书中犯了错误。可悲的是,我无法修复这本书中的代码(除非你正在阅读电子版并且你可以下载最新版本),但我可以修复notebook。所以,如果你是从本书中复制代码后遇到错误,请在notebook中查找修复的代码:我会努力保持它们没有错误并与最新的库版本保持同步。

· notebook中包含一些额外的代码来美化图形(添加标签、设置字体大小等)并为本书以高分辨率来保存它们。如果你愿意,你可以安全地忽略这些额外的代码。

我优化了代码的可读性和简单性:我让它尽可能的保持线性和扁平,只定义很少的函数或类。目标是确保你正在运行的代码通常就在你面前,而不是嵌套在你必须搜索的多个抽象层中。这也使你可以更轻松地使用代码。为简单起见,错误处理的代码很有限,我将一些最不常见的导入放在需要的地方(而不是按照PEP 8 Python样式指南的建议将它们放在文件顶部)。也就是说,你的生产环境代码不会有太大的不同:只是更加模块化,并且具有额外的测试和错误处理。

好的!一旦你熟悉了Colab,就可以下载数据了。

2.3.5 下载数据

在典型的环境中,你的数据在关系数据库或其他一些通用数据存储中,并分布在多个表/文档/文件中。要访问它,你首先需要获得你的凭据和访问授权[4],并熟悉数据模式。然而,在这个项目中,事情要简单得多:你只需下载一个压缩文件housing.tgz,其中包含一个名为housing.csv的逗号分隔值(Comma-Separated Value,CSV)文件,其中包含所有数据。

与其手动下载和解压缩数据,不如编写一个函数来为你完成这些工作。这在数据定期更改时特别有用:你可以编写一个小脚本,使用该函数来获取最新数据(或者你可以设置一个计划作业来定期自动执行此操作)。如果你需要在多台机器上安装数据集,自动化获取数据的过程也很有用。

这是获取和加载数据的函数:

调用load_housing_data()时,它会查找datasets/housing.tgz文件。如果找不到,它会在当前目录(在Colab中默认为/content)中创建datasets目录,从ageron/data GitHub存储库下载housing.tgz文件,并将其内容提取到datasets目录中。这将创建datasets/housing目录,其中包含housing.csv文件。最后,该函数将此CSV文件加载到包含所有数据的Pandas DataFrame对象中,并返回它。

2.3.6 快速浏览数据结构

首先使用DataFrame的head()方法查看前5行数据(见图2-6)。

图2-6:数据集中的前5行

每一行代表一个地区。有10个属性(图2-6中没有全部显示):longitudelatitudehousing_median_agetotal_roomstotal_bedroomspopulationhouseholdsmedian_incomemedian_house_valueocean_proximity

info()方法对于获取数据的快速描述很有用,特别是总行数、每个属性的类型和非空值的数量:

在本书中,当代码示例包含代码和输出的混合时,就像这里的情况一样,它的格式与Python解释器中的一样,以提高可读性:代码行以>>>为前缀(或...缩进块),并且输出没有前缀。

数据集中有20 640个实例,这意味着按照机器学习标准,它相当小,但非常适合入门。你注意到total_bedrooms属性只有20 433个非空值,这意味着207个地区缺少此属性。你稍后需要处理这个问题。

除了ocean_proximity之外,所有属性都是数字的。它的类型是对象,所以它可以容纳任何类型的Python对象。但是由于你是从CSV文件加载此数据的,所以你知道它一定是文本属性。当你查看前五行时,你可能会注意到ocean_proximity列中的值是重复的,这意味着它可能是一个分类属性。你可以使用value_counts()方法找出存在哪些类别以及每个类别有多少个地区:

让我们看看其他领域。describe()方法显示数字属性的摘要(见图2-7)。

图2-7:每个数字属性的摘要

countmeanminmax行是不言自明的。请注意,空值将被忽略(因此,例如,total_bedrooms的计数是20 433,而不是20 640)。std行显示标准差,标准差衡量值的分散程度[5]25%50%75%行显示相应的百分位数:百分位数表示一组观察值中给定百分比的观察值低于该值。例如,25%的地区housing_median_age低于18,50%低于29,75%低于37。这些通常称为第25个百分位数(或第一个四分位数)、中位数和第75个百分位数(或第三个四分位数)。

另一种快速了解你正在处理的数据类型的方法是为每个数值属性绘制直方图。直方图显示具有给定值范围(在水平轴上)的实例数(在垂直轴上)。你可以一次绘制一个属性,也可以对整个数据集调用hist()方法(如下面的代码示例所示),它会绘制每个数值属性的直方图(见图2-8):

图2-8:每个数值属性的直方图

图2-8:每个数值属性的直方图(续)

查看这些直方图,你会注意到一些事情:

· 首先,收入中位数属性看起来不像是以美元表示的。在与收集数据的团队核实后,你被告知数据已按比例缩放并上限为15(实际上是15.0001)以表示较高的收入中位数,以及0.5(实际上是0.4999)以表示较低的收入中位数。这些数字大约代表万美元(例如,3实际上表示大约30 000美元)。使用预处理属性在机器学习中很常见,这不一定是个问题,但你应该了解数据是如何计算的。

· 房屋年龄中位数和房价中位数也有上限。后者可能是一个严重的问题,因为它是你的目标属性(你的标签)。你的机器学习算法可能会学习到价格永远不会超过该限制。你需要与你的客户团队(将使用你的系统输出的团队)确认这是否是一个问题。如果他们告诉你他们需要超过500 000美元的精确预测,那么你有两个选择:

  ◆ 为标签被封顶的地区收集适当的标签。

  ◆ 从训练集中删除这些地区(也从测试集中删除这些地区,因为如果系统预测值超过500 000美元,则不应该评估它不好)。

· 这些属性具有非常不同的尺度。我们将在本章稍后探讨特征缩放时讨论这个问题。

· 最后,许多直方图向右倾斜:它们向中位数右侧的延伸比向左延伸的远。这可能会使某些机器学习算法更难检测到正确模式。稍后,你将尝试转换这些属性来获得更对称的钟形分布。

你现在应该对正在处理的数据类型有了更好的理解。

等等!在你进一步查看数据之前,你需要创建一个测试集,把它放在一边,永远不要看它。

2.3.7 创建测试集

在这个阶段自愿保留部分数据可能看起来很奇怪。毕竟,你只是快速浏览了数据,在决定使用什么算法之前,你肯定应该了解更多相关信息,对吧?这是事实,但你的大脑是一个惊人的模式检测系统,这也意味着它极易过拟合:如果你查看测试集,你可能会偶然发现测试数据中一些看似有趣的模式,从而引导你选择一种特殊的机器学习模型。当你使用测试集估计泛化误差时,你的估计会过于乐观,并且你将启动一个性能不如预期的系统。这称为数据窥探偏差。

创建测试集在理论上很简单;随机选择一些实例,通常是数据集的20%(如果你的数据集非常大,则更少),然后将它们放在一边:

然后你可以像这样使用这个函数:

好吧,这可以工作,但并不完美:如果你再次运行该程序,它将生成不同的测试集!随着时间的推移,你(或你的机器学习算法)将会看到整个数据集,这是你要避免的。

一种解决方案是在第一次运行时保存测试集,然后在后续运行中加载它。另一种选择是在调用np.random.permutation()之前设置随机数生成器的种子[例如,使用np.random.seed(42)][6],以便它始终生成相同的混淆索引。

但是,这两种解决方案都会在下次获取更新的数据集时失效。为了在更新数据集后也有稳定的训练/测试拆分,一个常见的解决方案是使用每个实例的标识符来决定它是否应该进入测试集(假设实例具有唯一且不可变的标识符)。例如,你可以计算每个实例标识符的哈希值,如果哈希值低于或等于最大哈希值的20%,则将该实例放入测试集中。这可确保测试集在多次运行中保持一致,即使你刷新数据集也是如此。新的测试集将包含20%的新实例,但不会包含之前训练集中的任何实例。

以下是一个可能的实现:

不幸的是,房屋数据集没有标识符列。最简单的解决方案是使用行索引作为ID:

如果使用行索引作为唯一标识符,则需要确保将新数据附加到数据集的末尾并且不会删除任何行。如果这做不到,那么你可以尝试使用最稳定的特征来构建唯一的标识符。例如,一个地区的经纬度保证在几百万年内保持稳定,因此你可以将它们组合成一个ID,如下所示[7]

Scikit-Learn提供了一些函数以各种方式将数据集拆分为多个子集。最简单的函数是train_test_split(),它所做的事情与我们之前定义的shuffle_and_split_data()函数几乎相同,但有几个附加功能。首先,有一个random_state参数允许你设置随机生成器种子。其次,你可以将多个具有相同行数的数据集传递给它,并且它会在相同的索引上拆分它们(这非常有用,例如,如果你有一个单独标签的DataFrame):

到目前为止,我们已经考虑了纯随机的采样方法。如果你的数据集足够大(尤其是相对于属性的数量),那么这通常没问题。但如果不够大,你就有引入显著采样偏差的风险。当一家调查公司的员工决定打电话给1000个人问他们几个问题时,他们不会只是在电话簿中随机挑选1000个人。就他们想问的问题而言,他们试图确保这1000人代表全体人口。例如,美国人口中女性占51.1%,男性占48.9%,因此在美国进行一项良好的调查需要尝试在样本中保持这一比例:511名女性和489名男性(至少在答案可能因性别而异的情况下)。这称为分层采样:将总体分为称为层的同质子组,并从每个层中抽取正确数量的实例以保证测试集能代表总体。如果进行调查的人使用纯随机采样,则大约有10.7%的机会会抽取到女性参与者少于48.5%或超过53.5%的偏差测试集。无论采用哪种方式,调查结果都可能偏差非常大。

假设你与一些专家聊天,他们告诉你收入中位数是预测房价中位数的一个非常重要的属性。你可能希望确保测试集能代表整个数据集中的各种收入类别。由于收入中位数是一个连续的数值属性,你首先需要创建一个收入类别属性。让我们更仔细地看一下收入中位数直方图(回到图2-8):大多数收入中位数集中在1.5~6左右(即15 000~60 000美元),但一些收入中位数远远超过6。重要的是每个层的数据集中要有足够数量的实例,否则对层重要性的估计可能有偏差。这意味着你不应该有太多的层,每个层应该足够大。下面的代码使用pd.cut()函数创建了一个收入类别属性,有5个类别(标记为1到5);类别1的范围从0到1.5(即低于15 000美元),类别2的范围从1.5到3,以此类推:

这些收入类别如图2-9所示:

图2-9:收入类别直方图

现在你可以根据收入类别进行分层采样了。Scikit-Learn在sklearn.model_selection包中提供了许多拆分类,它们实现了各种策略,将数据集拆分为训练集和测试集。每个拆分器都有一个split()方法,该方法返回对相同数据的不同训练/测试拆分的迭代器。

准确地说,split()方法产生训练和测试指标,而不是数据本身。如果你想更好地估计模型的性能,那么进行多次拆分会很有用,正如我们将在本章后面讨论交叉验证时所看到的。例如,以下代码生成同一数据集的10个不同的分层拆分:

现在,你可以只使用第一个拆分:

或者,由于分层采样相当普遍,因此有一种更简洁的方法可以使用带有stratify参数的train_test_split()函数来获得单个拆分:

让我们看看这是否按预期工作。可以从查看测试集中的收入类别比例开始:

使用类似的代码,你可以测量完整数据集中的收入类别比例。图2-10比较了完整数据集、分层采样生成的测试集和纯随机采样生成的测试集中的收入类别比例。如你所见,使用分层采样生成的测试集的收入类别比例与完整数据集中的收入类别比例几乎相同,而使用纯随机采样生成的测试集则存在偏差。

图2-10:分层采样与纯随机采样的采样偏差比较

你不会再次使用income_cat列,因此你不妨删除它,将数据恢复到其原始状态:

我们在测试集生成上花费了大量时间是有充分理由的:这是机器学习项目中经常被忽视但至关重要的部分。此外,当我们讨论交叉验证时,其中的许多想法都会很有用。现在是时候进入下一阶段了:探索数据。