Kaggle-笔记本开发指南-全-
Kaggle 笔记本开发指南(全)
原文:
annas-archive.org/md5/24dfa97e36ead23596f0ef3b74ce3f05
译者:飞龙
前言
在六年前,在我第一次发现 Kaggle 之前,我正在寻找我职业生涯中的新路径。几年后,我坚定地在一个新工作中站稳了脚跟,Kaggle 帮助我找到了这份工作。在发现这个美妙网站之前,我在不同的网站上四处寻找,阅读文章,下载和分析数据集,尝试 GitHub 或其他网站上的代码片段,参加在线培训,并阅读书籍。有了 Kaggle,我找到了不仅仅是信息来源;我找到了一个有着相同机器学习兴趣,更广泛地说,对数据科学感兴趣,并希望学习、分享知识、解决难题的社区。我还发现,在这个社区中,如果您愿意,您可以经历一个加速的学习曲线,因为您可以向最好的学习,有时与他们竞争,有时与他们合作。您也可以从经验较少的人那里学习;在这些年在平台上的日子里,我仍然从众人和专家那里学习。
这种持续挑战与富有成效的合作的结合,使 Kaggle 成为一个独特的平台,新旧贡献者都能感到同样受欢迎,并找到学习或分享的机会。在我加入这个平台的前几个月里,我主要从大量的数据集和笔记本中学习,分析竞赛数据,为活跃或过去的竞赛提供解决方案,并在讨论线程中发表意见。我很快就开始了贡献,主要是笔记本,并发现了分享自己的发现并从平台上其他人的反馈中获得回报是多么的有意义。这本书就是关于分享这份喜悦以及我在与社区分享我的发现、想法和解决方案的过程中所学到的知识。
本书旨在向您介绍数据分析的广阔世界,重点关注您如何使用 Kaggle 笔记本资源来帮助您在这个领域达到精通。我们将从简单概念到更高级的概念进行覆盖。本书也是一次个人旅程,将带您走上一条与我进行实验和学习分析数据集以及准备竞赛时相似的路径。
本书面向对象
本书旨在面向对数据科学和机器学习有浓厚兴趣,并希望利用 Kaggle 笔记本来提高技能以及提升 Kaggle 笔记本排名的广大读者。更确切地说,本书面向以下人群:
-
在 Kaggle 旅程上的绝对初学者
-
想要发展各种数据采集、准备、探索和可视化技能的资深贡献者
-
想要从 Kaggle 早期笔记本大师那里学习如何提升 Kaggle 排名的专家
-
已经使用 Kaggle 进行学习和竞争的专业人士,并希望了解更多关于数据分析的知识
本书涵盖内容
第一章,介绍 Kaggle 及其基本功能,是对 Kaggle 及其主要功能的快速介绍,包括比赛、数据集、代码(以前称为内核或笔记本)、讨论和额外资源、模型和学习。
第二章,为您的 Kaggle 环境做好准备,包含了更多关于 Kaggle 上代码功能的具体信息,包括计算环境、如何使用在线编辑器、如何分叉和修改现有示例,以及如何使用 Kaggle 上的源代码控制功能来保存或运行一个新的笔记本。
第三章,开始我们的旅行 – 在泰坦尼克号灾难中幸存,介绍了一个简单的数据集,这将帮助你为我们在书中进一步发展的技能打下基础。大多数 Kagglers 都会从这个比赛开始他们在平台上的旅程。我们介绍了 Python 中用于数据分析的工具(pandas 和 NumPy)、数据可视化(Matplotlib、Seaborn 和 Plotly),以及如何创建你的笔记本视觉身份的建议。我们将对特征进行单变量和多变量分析,分析缺失数据,并使用各种技术生成新的特征。你还将首次深入了解数据,并使用分析结合模型基准和迭代改进,在构建模型时从探索到准备。
第四章,在伦敦休息一下,喝杯啤酒或咖啡,结合了多个表格和地图数据集来探索地理数据。我们开始于两个数据集:第一个数据集包含英国酒吧的空间分布(英格兰的每个酒吧),第二个数据集包含全球星巴克咖啡店的空间分布(全球星巴克位置)。
我们首先分别分析它们,调查缺失数据并了解我们如何通过使用替代数据源来填补缺失数据。然后我们共同分析数据集,并专注于一个小区域,即伦敦,我们将叠加数据。我们还将讨论如何将具有不同空间分辨率的数据对齐。关于风格、展示组织以及讲故事方面的更多见解将被提供。
第五章,回到工作岗位并优化发展中国家的微型贷款,更进一步,开始分析来自 Kaggle 数据分析比赛的数据科学为善:Kiva 众筹的数据。在这里,我们将多个贷款历史、人口统计、国家发展和地图数据集结合起来,讲述如何改善发展中国家微型贷款的分配。本章的一个重点将是创建一个统一且个性化的演示风格,包括配色方案、章节装饰和图形风格。另一个重点将是创建一个基于数据并支持笔记本论点的连贯故事。我们以对替代数据分析比赛数据集Meta Kaggle的快速调查结束本章,在那里我们驳斥了关于社区感知趋势的一个假设。
第六章,你能预测蜜蜂亚种吗?,教你如何探索图像数据集。本分析所使用的数据集是蜜蜂图像数据集:标注的蜜蜂图像。我们将图像分析技术与表格数据分析与可视化技术相结合,创建了一个丰富且富有洞察力的分析,并为构建多类图像分类的机器学习流程做准备。你将学习如何输入和显示图像集,如何分析图像、元数据,如何进行图像增强,以及如何处理不同的缩放选项。我们还将展示如何从一个基线模型开始,然后根据训练和验证错误分析,迭代地优化模型。
第七章,文本分析就是你所需要的,使用了来自文本分类比赛的Jigsaw Unintended Bias in Toxicity Classification
数据集。数据来自在线帖子,在我们使用它来构建模型之前,我们需要对文本数据进行数据质量评估和清洗。然后我们将探索数据,分析单词频率和词汇特点,对句法和语义分析进行一些了解,进行情感分析和主题建模,并为训练模型做准备。我们将检查我们数据集中语料库的标记化或嵌入解决方案所提供的词汇覆盖范围,并应用数据处理来提高这一词汇覆盖范围。
第八章,分析声学信号以预测下一次模拟地震,将探讨如何处理时间序列数据,同时分析LANL
地震EDA
和预测比赛的Earthquake
数据集。
在对特征进行分析后,使用各种类型的模态分析来揭示信号中的隐藏模式,我们将学习如何使用快速傅里叶变换、希尔伯特变换和其他变换来生成此时间序列模型的特征。然后我们将学习如何使用各种信号处理函数生成多个特征。读者将了解分析信号数据的基础知识,以及如何使用各种信号处理变换来构建模型。
第九章,你能找出哪部电影是深度伪造的吗?,讨论了如何在著名的 Kaggle 竞赛的大型视频数据集Deepfake Detection Challenge上执行图像和视频分析。分析将从培训和数据处理开始,读者将学习如何操作.mp4
格式,从视频中提取图像,检查视频元数据信息,对提取的图像进行预处理,并使用计算机视觉技术或预训练模型在图像中找到对象,包括身体、上半身、面部、眼睛或嘴巴。最后,我们将准备构建一个模型,为这个深度伪造检测竞赛提供一个解决方案。
第十章,利用 Kaggle 模型释放生成式 AI 的力量,将提供独特和专家见解,介绍我们如何使用 Kaggle 模型结合大型语言模型(LLMs)的语义力量与 LangChain 和向量数据库,以释放生成式 AI 的力量,并使用 Kaggle 平台原型化最新一代 AI 应用。
第十一章,结束我们的旅程:如何保持相关性和领先地位,提供了如何不仅成为 Kaggle 笔记本顶级贡献者,同时保持这一地位,并在创建具有良好结构和巨大影响力的优质笔记本方面的见解。
要充分利用本书
您应该对 Python 有基本的了解,并熟悉 Jupyter 笔记本。理想情况下,您还需要一些关于 pandas 和 NumPy 等库的基本知识。
章节包含理论和代码。如果您想在书中运行代码,最简单的方法是遵循 GitHub 项目中每个笔记本的README.md
介绍页面上的链接,fork 笔记本,并在 Kaggle 上运行它。Kaggle 环境预先安装了所有需要的 Python 库。或者,您可以从 GitHub 项目下载笔记本,上传到 Kaggle,为每个特定示例附加书中提到的数据集资源,并运行它们。另一种选择是下载 Kaggle 上的数据集,安装您自己的本地环境,并在那里运行笔记本。然而,在这种情况下,您将需要更多关于如何在本地设置conda
环境和使用pip install
或conda install
安装 Python 库的先进知识。
章节练习要求 | 版本号 |
---|---|
Python | 3.9 或更高版本 |
在本书中开发的全部练习都使用当前 Python 版本,即编写本书时的 3.10 版本。
下载示例代码文件
本书代码包托管在 GitHub 上,网址为 github.com/PacktPublishing/Developing-Kaggle-Notebooks
。我们还有其他来自我们丰富图书和视频目录的代码包,可在 github.com/PacktPublishing/
找到。查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/gbp/9781805128519
。
使用的约定
本书使用了多种文本约定。
CodeInText
:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“为每个数据集运行info()
函数。”
代码块设置如下:
for sentence in selected_text["comment_text"].head(5):
print("\n")
doc = nlp(sentence)
for ent in doc.ents:
print(ent.text, ent.start_char, ent.end_char, ent.label_)
displacy.render(doc, style="ent",jupyter=True)
任何命令行输入或输出都应如下编写:
!pip install kaggle
粗体:表示新术语、重要单词或屏幕上出现的单词。例如,菜单或对话框中的单词在文本中显示如下。例如:“您将需要启动一个笔记本,然后从文件菜单中选择设置为实用脚本菜单项。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们欢迎读者的反馈。
总体反馈:请发送电子邮件至 feedback@packtpub.com
,并在邮件主题中提及书的标题。如果您对此书的任何方面有疑问,请通过电子邮件联系我们的 questions@packtpub.com
。
勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将不胜感激,如果您能向我们报告,我们将非常感谢。请访问 www.packtpub.com/submit-errata
,点击“提交勘误”,并填写表格。
盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件联系我们的 copyright@packtpub.com
并附上材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问 authors.packtpub.com
。
下载本书的免费 PDF 副本
感谢您购买此书!
您喜欢在旅途中阅读,但无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在,随着每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不会就此结束,您还可以获得独家折扣、时事通讯和每日免费内容的访问权限。
按照以下简单步骤获取优惠:
- 扫描二维码或访问以下链接
packt.link/free-ebook/9781805128519
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件中
分享您的想法
一旦您阅读了《Developing Kaggle Notebooks》,我们非常乐意听到您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
第一章:介绍 Kaggle 及其基本功能
Kaggle 是当前主要的竞争性预测建模平台。在这里,那些对机器学习充满热情的人,无论是专家还是新手,都有一个协作和竞争的环境来学习、获得认可、分享知识和回馈社区。该公司于 2010 年启动,最初只提供机器学习竞赛。目前,它是一个包含竞赛、数据集、代码、讨论、学习和最新增加的模型等标题的数据平台。
2011 年,Kaggle 经历了一轮投资,公司估值超过 2500 万美元。2017 年,它被 Google(现在是 Alphabet Inc.)收购,成为 Google Cloud 的一部分。Kaggle 最著名的关键人物是联合创始人 Anthony Goldbloom(长期 CEO,直到 2022 年)和 Ben Hammer(CTO)。最近,传奇的 Google 工程师 D. Sculley 成为 Kaggle 的新 CEO,继 Anthony Goldbloom 辞职参与新创业公司的发展之后。
在本章中,我们将探讨 Kaggle 平台为会员提供的主要部分。我们还将学习如何创建账户,了解平台的结构以及其主要部分。简而言之,本章将涵盖以下主题:
-
Kaggle 平台
-
Kaggle 竞赛
-
Kaggle 数据集
-
Kaggle 代码
-
Kaggle 讨论
-
Kaggle 学习
-
Kaggle 模型
如果您熟悉 Kaggle 平台,您可能已经了解这些功能。您可以选择继续阅读以下部分以刷新您对平台的知识,或者您可以选择跳过它们并直接进入下一章。
Kaggle 平台
要开始使用 Kaggle,您必须创建一个账户。您可以使用电子邮件和密码注册,或者直接使用 Google 账户进行身份验证。一旦注册,您可以通过创建包含您的姓名、图片、角色和当前组织的个人资料开始。然后您可以添加您的位置,这是可选的,以及一个简短的个人介绍。在您完成短信验证并在平台上添加一些基本内容(运行一个笔记本或脚本、提交一个竞赛、发表一条评论或给予一个点赞)之后,您也将从新手晋升到贡献者。以下图显示了如何成为贡献者的清单。如您所见,所有项目都已勾选,这意味着用户已经晋升到贡献者级别。
图 1.1:成为贡献者的清单
在完成整个贡献者清单后,您就准备好开始您的 Kaggle 之旅了。
当前平台包含多个功能。其中最重要的包括:
-
竞赛:这是 Kagglers 可以参加竞赛并提交解决方案以获得评分的地方。
-
数据集:在这个部分,用户可以上传数据集。
-
代码:这是 Kaggle 最复杂的功能之一。也称为内核或笔记本,它允许用户添加代码(独立或与数据集和比赛相关联),修改它,运行它以执行分析,准备模型,并为比赛生成提交文件。
-
讨论:在这个部分,平台上的贡献者可以为比赛、笔记本或数据集添加主题和评论。主题也可以独立添加,并链接到如入门等主题。
每个这些部分都允许你根据 Kaggle 的进步系统获得奖牌。一旦你开始为这些部分中的任何一个做出贡献,你还可以在相应部分的 Kaggle 整体排名系统中获得排名。获得奖牌主要有两种方法:在比赛中获得前几名,以及在数据集、代码和讨论部分获得点赞。
除了比赛、数据集、代码和讨论之外,Kaggle 还有两个更多内容相关的部分:
-
学习:这是 Kaggle 最酷的功能之一。它包含了一系列关于各种主题的讲座和教程,从编程语言的基本介绍到高级主题,如计算机视觉、模型可解释性和 AI 伦理。你可以使用 Kaggle 的所有其他资源作为讲座的支持材料(数据集、比赛、代码和讨论)。
-
模型:这是 Kaggle 上最新引入的功能。它允许你将模型加载到你的代码中,就像你现在添加数据集一样。
现在我们已经快速了解了 Kaggle 平台的各种功能,以下部分将深入介绍比赛、数据集、代码、讨论、学习和模型。让我们开始吧!
Kaggle 比赛
所有这一切都始于 12 年多前的比赛。第一个比赛只有少数参与者。随着对机器学习的兴趣日益增长以及 Kaggle 社区的扩大,比赛的复杂性、参与者的数量以及围绕比赛的兴趣显著增加。
要开始一场比赛,比赛主办方准备一个数据集,通常分为训练集和测试集。在最常见的形式中,训练集包含可用的标记数据,而测试集只包含特征数据。主办方还会添加有关数据的信息以及比赛目标的展示。这包括对问题的描述,为竞争者设定背景。主办方还会添加有关用于评估比赛解决方案的指标的信息。比赛的条件和条款也做了规定。
竞赛者每天可以提交有限数量的解决方案,并且最终将根据用于计算公共分数的测试集的一部分来评估,选出最佳的两个解决方案。竞赛者也有权根据自己的判断选择两个解决方案。然后,这两个选定的解决方案将在预留的测试数据子集上进行评估,以生成私人分数。这将作为最终分数用于排名竞赛者。
竞赛有几种类型:
-
特色竞赛:最重要的是特色竞赛。目前,特色竞赛可能会聚集数千个团队,提交数十甚至数百万个解决方案。特色竞赛通常由公司举办,但有时也由研究机构或大学举办,通常旨在解决与公司或研究主题相关的一个难题。组织者转向庞大的 Kaggle 社区,以带来他们的知识和技能,并且设置中的竞争性方面加速了解决方案的开发。通常,特色竞赛还会有一个重大的奖金,将根据竞赛规则分配给排名靠前的竞赛者。有时,主办方可能不会提供奖金,但会提供不同的激励措施,例如招募顶尖竞赛者为他们工作(对于知名公司来说,这可能比奖金更有趣),提供云资源使用券,或者接受顶尖解决方案在知名会议上展示。除了特色竞赛之外,还有入门、研究、社区、游乐场、模拟和数据分析竞赛。
-
入门竞赛:这些主要针对初学者,解决易于接触的机器学习问题,以帮助建立基本技能。这些竞赛定期重启,排行榜重置。最引人注目的是 泰坦尼克号 – 灾难机器学习,数字识别器,房价 – 高级回归技术 和 灾难推文的自然语言处理。
-
研究竞赛:在研究竞赛中,主题与通过应用机器学习方法解决医学、遗传学、细胞生物学和天文学等各个领域中的难题相关。近年来最受欢迎的一些竞赛来自这一类别,随着机器学习在许多基础和应用研究领域的广泛应用,我们可以预期这种类型的竞赛将越来越频繁和受欢迎。
-
社区竞赛:这些是由 Kagglers 创建的,可以是公开竞赛或私人竞赛,只有受邀者才能参加。例如,你可以作为一个学校或大学项目举办社区竞赛,邀请学生加入并竞争获得最佳成绩。
Kaggle 提供了基础设施,这使得你非常容易定义并启动一个新的竞赛。你必须提供训练数据和测试数据,但这可以简单到两个 CSV 格式的文件。此外,你需要添加一个提交样本文件,该文件给出了提交的预期格式。竞赛中的参与者必须用他们自己的预测替换文件中的预测,保存文件,然后提交。然后,你必须选择一个指标来评估机器学习模型的性能(无需定义,因为你有一系列预定义的指标)。同时,作为主办方,你将需要上传一个包含正确、预期解决方案的文件到竞赛挑战中,这将作为所有竞争者提交的参考。一旦完成这些,你只需要编辑条款和条件,选择竞赛的开始和结束日期,编写数据描述和目标,然后就可以开始了。你还可以选择的其他选项包括是否允许参与者组队,以及是否允许所有人或只有收到竞赛链接的人加入竞赛。
-
游乐场竞赛:大约三年前,一个新的竞赛部分被推出:游乐场竞赛。这些竞赛通常很简单,就像入门竞赛一样,但寿命会更短(最初是一个月,但目前是从一到四周)。这些竞赛的难度较低或中等,将帮助参与者获得新技能。这类竞赛非常推荐给初学者,但也适合有更多经验的竞争者,他们希望在某个领域内磨练自己的技能。
-
模拟竞赛:如果前两种都是监督式机器学习竞赛,那么模拟竞赛通常是一类优化竞赛。最著名的是圣诞节和新年期间的竞赛(圣诞老人竞赛)以及 Lux AI 挑战赛,目前正在进行第三季。一些模拟竞赛也是周期性的,将获得一个额外的类别:年度竞赛。这类竞赛的例子包括既属于模拟类型又属于年度竞赛的圣诞老人竞赛。
-
分析竞赛:这些竞赛在目标和评分方式上都有所不同。目标是详细分析竞赛数据集,从数据中获得洞察。评分通常基于组织者的判断,在某些情况下,也基于竞争解决方案的受欢迎程度;在这种情况下,组织者将根据 Kagglers 的点赞数,将部分奖金授予最受欢迎的笔记本。在第五章中,我们将分析一次早期分析竞赛的数据,并就如何应对这类竞赛提供一些见解。
很长一段时间内,竞赛要求参与者准备一个包含测试集预测的提交文件。对准备提交的方法没有其他约束;竞争者应该使用自己的计算资源来训练模型、验证它们并准备提交。最初,平台上没有可用于准备提交的资源。Kaggle 开始提供计算资源后,你可以使用 Kaggle Kernels(后来更名为 Notebooks,现在称为 Code)来准备模型,并直接从平台提交,但对此没有限制。通常,提交文件会即时评估,结果几乎会立即显示。结果(即根据竞赛指标的分数)仅计算测试集的一部分。这个百分比在竞赛开始时宣布,并且是固定的。此外,在竞赛期间用于计算显示分数(公开分数)的测试数据子集是固定的。竞赛结束后,使用剩余的测试数据计算最终分数,这个最终分数(也称为私人分数)是每个竞争者的最终分数。在竞赛期间用于评估解决方案和提供公开分数的测试数据百分比可以从几个百分点到超过 50%不等。在大多数竞赛中,这个比例往往小于 50%。
Kaggle 采用这种方法的理由是为了防止一种不受欢迎的现象。竞争者可能会倾向于优化他们的解决方案,以尽可能完美地预测测试集,而不是考虑他们在训练数据上的交叉验证分数。换句话说,竞争者可能会倾向于在测试集上过度拟合他们的解决方案。通过分割这些数据,并且只提供测试集的一部分——公开分数——组织者意图防止这种情况的发生。
随着越来越多的复杂竞赛(有时伴随着非常大的训练和测试集),一些拥有更多计算资源的参与者可能会获得优势,而资源有限的参与者可能难以开发高级模型。特别是在特征竞赛中,目标通常是创建稳健、生产兼容的解决方案。然而,如果没有对解决方案的获取方式设定限制,实现这一目标可能会很困难,尤其是如果使用不切实际资源消耗的解决方案变得普遍。为了限制“军备竞赛”带来的负面影响,几年前,Kaggle 引入了代码竞赛。这类竞赛要求所有解决方案都必须从 Kaggle 平台上的运行笔记本提交。这样,解决方案的运行基础设施就完全由 Kaggle 控制。
此外,在这些竞赛中,不仅计算资源有限,还有额外的限制:运行时间和互联网访问(以防止通过使用外部 API 或其他远程计算资源来使用额外的计算能力)。
Kagglers 很快发现这只是一个针对解决方案推断部分的限制,并出现了一种适应方法:参赛者开始在线下训练大型模型,这些模型不会超出代码竞赛规定的计算能力和运行时间限制。然后,他们将线下训练的模型(有时使用非常大量的计算资源)作为数据集上传,并在观察代码竞赛对内存和计算时间的限制的推断代码中加载这些模型。
在某些情况下,多个在线训练的模型被加载为数据集,推断将这些多个模型结合起来创建更精确的解决方案。随着时间的推移,代码竞赛变得更加精细。其中一些竞赛只会公开测试集的一小部分行,而不透露用于公开或未来私有测试集的真实测试集的大小。因此,Kagglers 不得不求助于巧妙的探测技术来估计在运行最终、私有测试集时可能产生的限制,以避免出现代码因超出内存或运行时间限制而失败的情况。
目前,也存在一些代码竞赛,在竞赛的活跃部分(即,当参赛者被允许继续完善他们的解决方案时)结束后,不会公布私有分数,而是会使用几组新的测试数据重新运行代码,并重新评估两组选定的解决方案与这些以前从未见过的新的数据集。其中一些竞赛是关于股市、加密货币估值或信用绩效预测的,并且使用真实数据。代码竞赛的演变与平台可用计算资源的演变并行,为用户提供所需的计算能力。
一些竞赛(特别是特色竞赛和研究竞赛)会授予参赛者排名点和奖牌。排名点用于计算 Kagglers 在平台总排行榜中的相对位置。自 2015 年 5 月以来,计算竞赛所授予的排名点的公式没有变化:
图 1.2:计算排名点的公式
点数随着当前竞赛团队中队友数量的平方根而减少。对于团队数量较多的竞赛,会授予更多分数。随着时间的推移,点数也会减少,以保持排名的时效性和竞争性。
奖牌的数量用于在 Kaggle 竞赛进阶系统中获得晋升。竞赛奖牌是根据竞赛排行榜顶部位置获得的。实际系统要复杂一些,但一般来说,前 10% 将获得铜牌,前 5% 将获得银牌,前 1% 将获得金牌。随着参与者数量的增加,授予的奖牌数量也会增加,但这是基本原理。
拿到两枚铜牌,你就能达到竞赛专家级别。拿到两枚银牌和一枚金牌,你就能达到竞赛大师级别。而如果你单独获得一枚金牌(即,你没有和其他人组队获得这枚金牌)以及总共五枚金牌,你就能达到最有价值的 Kaggle 级别:竞赛宗师。目前,在准备这本书的时候,在 Kaggle 上超过 1200 万用户中,有 280 位 Kaggle 竞赛宗师和 1936 位大师。
排名系统根据用户在排行榜上的位置增加积分,从而授予排名积分。这些积分不是永久的,正如我们从 图 1.2 中可以看到的,积分减少的公式相当复杂。如果你不继续参加比赛并获得新的积分,你的积分会迅速下降,而你过去辉煌的唯一提醒将是你在过去达到的最高排名。然而,一旦你获得了一枚奖牌,你将永远拥有那枚奖牌,即使你的排名位置发生变化或你的积分随时间下降。
Kaggle 数据集
Kaggle 数据集是在几年前才加入的。目前,平台上已有超过 20 万个数据集可供使用,由用户贡献。当然,过去也有与竞赛相关的数据集。随着新的 数据集 部分的加入,Kagglers 可以根据平台上其他用户对贡献的数据集的认可,通过点赞来获得奖牌和排名。
每个人都可以贡献数据集,添加数据集的过程相当简单。你首先需要确定一个有趣的主题和数据来源。这可以是一个你在 Kaggle 上镜像的外部数据集,只要存在正确的许可,或者数据是你自己收集的。数据集也可以集体创作。将会有一个主要作者,即启动数据集的人,但他们可以添加具有查看或编辑角色的其他贡献者。在 Kaggle 上定义数据集有几个必经的步骤。
首先,您需要上传一个或多个文件,并为数据集命名。或者,您可以将数据集设置为从公共链接提供,该链接应指向一个文件或 GitHub 上的公共仓库。另一种提供数据集的方法是从 Kaggle 笔记本;在这种情况下,笔记本的输出将是数据集的内容。数据集也可以从 Google Cloud Storage 资源创建。在创建数据集之前,您可以选择将其设置为公开,并且您还可以检查您当前的私有配额。每个 Kaggler 都有一个有限的私有配额(随着时间的推移略有增加;目前,超过 100 GB)。如果您决定保留数据集为私有,您必须将所有私有数据集放入此配额中。如果数据集保持私有,您可以在任何时间决定删除它,如果您不再需要它。数据集初始化后,您可以通过添加附加信息来开始改进它。
在创建数据集时,您可以选择添加副标题、描述(需要至少一定数量的字符),以及关于数据集中每个文件的信息。对于表格数据集,您还可以为每个列添加标题和说明。然后,您可以添加标签,使数据集更容易通过搜索找到,并清楚地指定主题、数据类型以及可能的商业或研究领域,供感兴趣的人参考。您还可以更改与数据集关联的图片。建议使用公共领域或个人图片。添加关于作者的元数据、生成DOI(数字对象标识符)引用、指定来源和预期更新频率都有助于提高数据集的可见性。它还将提高您的贡献被正确引用和在其他作品中使用的可能性。许可信息也很重要,您可以从大量常用许可中选择。在描述和关于贡献数据集的元数据中添加每个元素时,您也提高了由 Kaggle 自动计算的可用性评分。并不是总是可以达到 10/10 的可用性评分(尤其是当您有一个包含数万个文件的数据库时),但始终建议尝试改进与数据集相关的信息。
一旦您发布数据集,它将在平台的数据集部分可见,并且根据 Kaggle 内容管理员对可用性和质量的感觉,您可能会获得特色数据集的特殊状态。特色数据集在搜索中具有更高的可见性,并且当您选择数据集部分时,它们被包含在推荐数据集的顶部部分。除了特色数据集,在趋势数据集通道下展示之外,您还会看到类似体育、健康、软件、食品和旅行等主题的通道,以及最近查看的数据集。
数据集可以包括所有类型的文件格式。最常用的格式是 CSV。它也是 Kaggle 之外非常流行的格式,并且是表格数据的最佳格式选择。当一个文件是 CSV 格式时,Kaggle 会显示它,你可以选择详细查看内容、按列查看或以紧凑的形式查看。其他可能使用的数据格式包括 JSON、SQLite 和存档。尽管 ZIP 存档本身不是数据格式,但在 Kaggle 上它有完全的支持,你可以直接读取存档的内容,而无需解包。数据集还包括特定模态的格式,各种图像格式(JPEG、PNG 等)、音频信号格式(WAV、OGG 和 MP3)以及视频格式。特定领域的格式,如医学成像的 DICOM,也广泛使用。BigQuery,这是特定于 Google Cloud 的数据集格式,也用于 Kaggle 上的数据集,并且有完全支持访问内容。
如果你为数据集做出贡献,你还可以获得排名积分和奖牌。该系统基于其他用户的点赞、你自己的点赞或新手 Kagglers 的点赞,或者旧点赞不计入授予排名积分或奖牌的计算。如果你获得三个铜牌,你可以达到数据集专家级别,如果你获得一个金牌和四个银牌,你可以达到大师级别,如果你获得五个金牌和五个银牌,你可以达到数据集宗师级别。在数据集中获得奖牌并不容易,因为用户不会轻易地给予数据集点赞,你需要 5 个点赞才能获得铜牌,20 个点赞才能获得银牌,50 个点赞才能获得金牌。一旦你获得了奖牌,由于这些奖牌是基于投票的,随着时间的推移,你可能会失去奖牌,甚至可能失去专家、大师或宗师的地位,如果给你点赞的用户取消点赞或如果他们被禁止在平台上使用。这种情况有时会发生,而且比你想象的要频繁。所以,如果你想确保你的位置,最好的方法就是始终创建高质量的内容;这将为你带来比所需最低数量更多的点赞和奖牌。
Kaggle 代码
Kaggle 代码是平台上最活跃的部分之一。代码的旧名称是 Kernels 和 Notebooks,你经常会听到它们被交替使用。截至本书编写时,当前贡献者的数量超过 260,000,仅略低于讨论部分。
代码用于分析数据集或竞赛数据集,用于准备竞赛提交的模型,以及用于生成模型和数据集。在过去,代码可以使用 R、Python 或 Julia 作为编程语言;目前,你只能选择 Python(默认选项)和 R。你可以将你的编辑器设置为脚本或笔记本。你可以选择运行你的代码的计算资源,CPU是默认选项。
或者,如果您使用 Python 作为编程语言,可以选择四种加速器选项;如果使用 R,则可以选择两种。加速器免费提供,但有一定的配额,每周重置。对于需求量大的加速器资源,可能还会有等待名单。
代码处于源代码控制之下,在编辑时,您可以选择仅保存(并创建一个版本)或保存并运行(您将创建一个代码版本和一个运行版本)。您可以将代码附加到代码数据集、竞赛数据集以及外部实用脚本和模型。只要您不重新运行笔记本,对所使用资源所做的更改不会影响其可见性。如果您尝试重新运行代码并刷新数据集或实用脚本版本,可能需要考虑这些数据和代码版本的变化。代码的输出可以用作其他代码的输入,就像您包括数据集和模型一样。默认情况下,您的代码是私有的,您不需要将其公开即可提交输出到竞赛。
如果您将代码公开,可以获得点赞,这些点赞既计入笔记本类别的排名,也用于获得奖牌。在笔记本类别中,您需要 5 枚铜牌才能达到专家级别,10 枚银牌才能达到大师级别,15 枚金牌才能达到大师级别。一枚铜牌需要 5 个点赞,一枚银牌需要 20 个点赞,一枚金牌需要 50 个点赞。笔记本中的点赞可以被撤销,您也可以将您的公开笔记本再次设为私有(或删除它们)。在这种情况下,与该笔记本相关的所有点赞和奖牌将不再计入您的排名或表现级别。与竞赛、数据集和模型相关的代码部分。在撰写本书时,有 125 位笔记本大师和 472 位大师。
Kaggle 作为一个数据平台和竞争性机器学习平台,以及一个社区,持续发展和变化。在撰写本书时,从新的 2023 Kaggle AI 报告 开始,Kaggle 为 Notebook 竞赛引入了一个评审系统,要求所有提交论文的参与者也为其他三位参与者的论文提供评审。关于哪个提交将赢得比赛的最终决定由一群经验丰富的 Kaggle 大师组成的专家小组做出。
Kaggle 代码的许多功能和选项将在下一章中更详细地描述。
Kaggle 讨论区
Kaggle 讨论区要么与其他部分相关联,要么是独立的。竞赛和数据集都有讨论区。对于代码,有一个评论区。在讨论区,你可以添加讨论主题或对某个主题的评论。对于代码,你可以添加评论。除了这些上下文之外,你还可以在论坛下添加主题或评论,或者你可以在 Kaggle 部分的讨论区下跟踪讨论。论坛按主题分组,你可以选择通用、入门、产品反馈、问答和竞赛举办。在 Kaggle 的讨论区下,你可以搜索内容或专注于一个标记的子主题,如你的活动、书签、初学者、数据可视化、计算机视觉、NLP、神经网络等等。
讨论区还有一个进度系统,你可以通过积累点赞来获得排名积分和奖牌。与其他部分不同,在讨论区,你也可以获得踩。排名积分会随着时间的推移而消失,只有来自非新手且新的点赞才会计入奖牌。在讨论区你不能给自己点赞。
讨论区的性能等级从专家开始,你可以通过积累 50 枚铜牌来获得这个等级。要达到下一个等级,大师,你需要 50 枚银牌和总共 200 枚奖牌,要达到大师级,你需要 50 枚金牌和总共 500 枚奖牌。与数据集和代码的情况一样,投票不是永久的。用户可以决定撤回他们的点赞;因此,你可能会失去一些点赞、排名积分、奖牌,甚至性能等级状态。
在撰写这本书的时候,讨论区有 62 位大师和 103 位大师。
Kaggle Learn
Kaggle Learn 是 Kaggle 上不太为人所知的瑰宝之一。它包含紧凑的学习模块,每个模块都围绕与数据科学或机器学习相关的一个主题。每个学习模块都有几个课程,每个课程都包含一个教程部分和一个练习部分。教程和练习部分以交互式 Kaggle 笔记本的形式提供。要完成一个学习模块,你需要通过所有课程。在每个课程中,你需要复习培训材料并成功运行练习笔记本。练习笔记本中的一些单元格与验证相关联。如果你需要帮助,笔记本中也有特殊的单元格,可以揭示关于如何解决当前练习的提示。完成整个学习模块后,你将获得 Kaggle 的完成证书。
目前,Kaggle Learn 被组织成三个主要部分:
-
你的课程,其中包含你已经完成和现在正在进行的(活跃的)课程。
-
可以进一步探索的开放课程。这个主要部分中的课程从绝对初学者课程(如编程入门、Python、Pandas、SQL 入门和机器学习入门)到中级课程(如数据清洗、中级机器学习、特征工程和高级 SQL)。它还包含特定主题的课程,如可视化、地理空间分析、计算机视觉、时间序列和游戏 AI 和强化学习入门。一些课程涉及非常有趣的主题,例如 AI 伦理和机器学习可解释性。
-
指南,专注于各种程序、框架或感兴趣领域的学习指南。这包括JAX 指南、TensorFlow 指南、计算机视觉迁移学习指南、Kaggle 竞赛指南、自然语言处理指南和R 指南。
Kaggle 还致力于支持持续学习和帮助任何人从 Kaggle 平台和 Kaggle 社区积累的知识中受益。在过去的两年里,Kaggle 开始通过 KaggleX BIPOC(黑人、原住民和有色人种)补助金计划,以配对 Kagglers 作为导师与 BIPOC 社区的专业人士作为学员的形式,帮助来自代表性不足社区的专业人士获得数据科学和机器学习的技能和经验。
在下一节中,我们将熟悉 Kaggle 平台的一个快速发展的功能:模型。
Kaggle 模型
模型是平台上新引入的章节;在撰写本书时,它还不到一个月。模型开始以多种方式由用户贡献,出于几个目的。最常见的是,模型在自定义代码训练后作为 Notebooks(代码)的输出保存,通常是在竞赛的背景下。随后,这些模型可以选择性地包含在数据集中或直接在代码中使用。有时,平台外构建的模型作为数据集上传,然后包含在用户的管道中,以准备竞赛的解决方案。同时,模型存储库可以通过公共云(如 Google Cloud、AWS 或 Azure)或专门提供此类服务的公司(如 Hugging Face)获得。
随着可下载模型的概念准备就绪,可用于使用或轻松微调以进行定制任务,Kaggle 选择将模型包含在这个平台上。目前,你可以在几个类别中进行搜索:文本分类、图像特征向量、目标检测和图像分割。或者,你可以使用模型查找器功能探索特定模态的模型:图像、文本、音频、多模态或视频。在搜索模型库时,你可以根据任务、数据类型、框架、语言、许可和大小以及功能标准,如可微调来应用过滤器。
目前还没有与模型相关的排名积分或性能等级。模型可以被点赞,并且每个模型都有一个与之相关的代码和讨论部分。在未来,我们可能会看到这里的演变,如果可能的话,模型将会有排名积分和性能等级,以便能够贡献模型并获得认可。目前,模型仅由谷歌贡献。
我们可能在不久的将来看到“模型”功能有巨大的发展,为社区提供一个灵活且强大的工具,用于在 Kaggle 平台上创建模块化和可扩展的解决方案,以训练和添加推理到机器学习管道。
摘要
在本章中,我们简要了解了 Kaggle 平台的历史、资源和功能。然后,我们介绍了如何创建账户并开始利用平台资源以及与其他用户的互动。
初始时,Kaggle 只是一个用于预测建模竞赛的平台,现在已经发展成为一个复杂的数据平台,包括竞赛、数据集、代码(笔记本)和讨论等部分。因此,我们学习了如何通过在竞赛中积累排名积分和奖牌,在数据集、笔记本和讨论中获得奖牌来提升排名。在未来,Kaggle 可能会为竞赛以外的其他部分也添加排名积分,尽管这在 Kaggle 社区中是一个有争议的话题。此外,Kaggle 提供了一个学习平台(带有学习标签)和模型(可以在笔记本中使用)。
现在是时候准备开始使用 Kaggle 资源环游数据分析世界了。在下一章中,你将学习如何充分利用平台进行编码,熟悉开发环境,并了解如何将其发挥到极致。让我们做好准备!
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人相聚,并在以下地点与超过 5000 名成员一起学习:
第二章:准备您的 Kaggle 环境
在上一章中,我们学习了如何创建您的 Kaggle 账户,以及关于竞赛、数据集、代码(笔记本)、讨论和 Kaggle 学习和模型最重要的知识。在本章中,我们将探讨 Kaggle 笔记本的功能。有时,核(Kernels)和代码(Code)被用作笔记本的替代名称,核是旧名称,代码是新菜单名称。这两个术语,无论是旧的还是新的,都说明了 Kaggle 笔记本的一个重要特性。
我们将首先介绍什么是 Kaggle 笔记本,并解释 Kaggle 脚本和 Kaggle 笔记本之间的区别。然后我们将展示如何创建一个笔记本,无论是从头开始还是从现有的一个衍生出来。在您开始编辑笔记本后,您有多种选择,我们将在本章中逐一回顾它们,从最常见的选择(编辑数据源和模型、更改计算资源等)开始,然后继续介绍其他选项(将笔记本设置为脚本、向笔记本添加实用脚本、添加和使用密钥等)。
简而言之,本章将涵盖以下主要内容:
-
什么是 Kaggle 笔记本?
-
如何创建笔记本
-
探索笔记本功能
-
使用 Kaggle API
什么是 Kaggle 笔记本?
Kaggle 笔记本是集成开发环境,允许您编写代码、版本控制、运行(使用 Kaggle 平台计算资源)并以各种形式生成结果。当您开始在一个笔记本上工作时,您启动了一个代码编辑器。这反过来又启动了一个 Docker 容器,配置了最常用的 Python 数据分析和机器学习包,在 Google Cloud 分配的虚拟机中运行。代码本身与代码仓库相链接。
您可以使用两种语言之一编写代码:Python 或 R。目前,Kaggle 上的大多数用户使用 Python,本书中的所有示例也只使用 Python。
术语笔记本是通用的,但 Kaggle 笔记本有两种类型:脚本和笔记本。
-
Kaggle 脚本:脚本是一系列将依次执行的代码文件。脚本的执行输出将打印在控制台。如果您愿意,也可以只执行脚本的一部分,只需选择几行并按下运行按钮。如果您使用 R 语言进行开发,可以使用一种特殊的脚本,即 RMarkdown 脚本。开发环境与 Python 或 R 脚本类似,但您可以使用 RMarkdown 语法,输出将结合 R 代码执行结果和 RMarkdown 语法用于文本和图形效果。
-
Kaggle 笔记本:笔记本的外观和感觉与 Jupyter 笔记本相似。它们相似但不相同。Kaggle 笔记本有多个额外的选项来支持与 Kaggle 环境的集成和更好的用户体验。笔记本由一系列的单元格组成,这些单元格包含代码或 Markdown 内容,并且每个单元格可以独立执行。您可以使用 R 或 Python 在笔记本中编码。在运行单元格时,代码单元格的输出将显示在单元格下方。
在简要概述了 Kaggle 笔记本及其基本组件之后,现在让我们看看您如何创建一个笔记本。
如何创建笔记本
开始笔记本的方式有多种。您可以从主菜单的代码(图 2.1),从数据集的上下文(图 2.2),一个竞赛(图 2.3),或者通过分叉(复制并编辑)现有的笔记本来开始。
![图片 B20963_02_01.png]
图 2.1:从代码菜单创建一个新的笔记本
当您从代码菜单创建一个新的笔记本时,这个新的笔记本将出现在您的笔记本列表中,但不会添加到任何数据集或竞赛上下文中。
如果您选择从 Kaggle 数据集开始,数据集将已经添加到与笔记本关联的数据列表中,当您编辑笔记本时,您将在右侧面板中看到它(参见图 2.5)。
![图片 B20963_02_02.png]
图 2.2:在数据集的上下文中创建一个新的笔记本
在竞赛的情况下也是如此。与之关联的数据集在您初始化笔记本时已经存在于数据集列表中。
图 2.3:在竞赛的上下文中创建一个新的笔记本
要分叉(复制并编辑)现有的笔记本,请按该笔记本编辑按钮旁边的三个垂直点,然后从下拉列表中选择复制并编辑笔记本菜单项。
![图片 B20963_02_04.png]
图 2.4:从现有的笔记本中分叉一个笔记本
创建后,笔记本将可供编辑,如下面的屏幕截图所示。在左上角,有一个常规菜单(文件、编辑、查看、运行、附加组件和帮助),下面有编辑和运行的快速操作图标。在右侧,有一个可伸缩的面板,包含更多的快速操作。
图 2.5:Kaggle 笔记本的主要编辑窗口,右侧面板带有快速菜单
文件菜单复杂,提供了输入和输出选项,以及与其他平台资源(模型、实用脚本和笔记本)交互的各种设置。它有菜单项可以导入外部笔记本或导出当前笔记本,甚至可以将数据或模型添加到笔记本中。您还可以将当前笔记本保存为实用脚本或将实用脚本添加到笔记本中。您可以选择设置语言(到 R 或 Python;默认设置为 Python)。还有一个选项可以将当前笔记本设置为脚本或笔记本(默认为笔记本)。
额外的选项是用于在 GitHub 上发布和共享笔记本。要在 GitHub 上发布笔记本,您必须通过授权 Kaggle 访问您的 GitHub 账户来将 Kaggle 账户与 GitHub 账户链接。一旦执行此操作,笔记本的更新也将同步到 GitHub。使用共享菜单项,您可以设置谁可以查看或编辑笔记本。最初,您将是唯一具有读写访问权限的用户,但一旦添加了贡献者,他们也可以被分配读写访问权限,或者只有读(查看)访问权限。如果您发布笔记本,那么每个人都可以读取它,能够分叉(复制并编辑)它,然后编辑工作。
编辑菜单允许您移动单元格(上下移动)或删除选定的单元格。在视图中,您有选项调整编辑器的外观和感觉(添加或删除主题、行号和设置编辑器布局)以及生成的输出 HTML 内容(查看或隐藏选定单元格的输入或输出,或折叠或展开单元格)。
运行菜单项提供了运行单个单元格、所有单元格、所有单元格之前或之后的单元格,以及开始/停止会话的控制。在会话重启时,内核(即笔记本运行的 Docker 容器)也会重启,并且当我们运行某些单元格时初始化的所有上下文数据都会重置。当你编辑时想要重置包含所有变量的环境,这是一个非常有用的选项。附加菜单组、秘密管理、Google Cloud 服务以及 Google Cloud SDK——这些每个都扩展了笔记本的功能,将在本章后面的高级功能部分进行介绍。
现在我们已经学习了如何创建、编辑和运行笔记本,让我们继续探索更多的笔记本功能。
探索笔记本功能
笔记本作为数据探索、模型训练和运行推理的强大工具。在本节中,我们将检查 Kaggle 笔记本提供的各种功能。
我们将从笔记本最常用的功能开始。我们将通过选项将各种资源添加到笔记本中(数据和模型),并修改执行环境。然后,我们将继续介绍更高级的功能,包括设置实用脚本、添加或使用密钥、使用 Google Cloud 服务或升级笔记本到 Google Cloud AI 笔记本。让我们开始吧!
基本功能
在右侧面板上,我们有快速菜单操作,用于访问笔记本常用的功能。在以下截图中,我们将更详细地查看这些快速菜单操作。
图 2.6:右侧面板的放大视图,包含快速菜单
如您所见,第一个快速菜单操作被分组在数据部分下。在这里,您有按钮来添加或从笔记本中删除数据集。点击添加数据按钮,您可以添加一个现有数据集。您有搜索文本框和快速按钮,可以从您的数据集、竞赛数据集和笔记本中进行选择。当您选择笔记本时,您可以将笔记本的输出作为当前笔记本的数据源。您还在添加数据按钮旁边有一个上传按钮,您可以使用它在上传到笔记本之前上传一个新的数据集。在面板上的相同数据部分,您有输入和输出文件夹浏览器,以及每个项目的按钮,以便您可以复制这两个文件夹或文件的路径。
在数据部分下方,我们有模型部分(见图 2.6)。在这里,我们可以将模型添加到笔记本中。模型是平台上的新功能,它允许您在笔记本中使用强大的预训练模型。
在笔记本选项部分,我们可以根据我们的偏好配置加速器、语言、持久化选项、环境和互联网访问(见图 2.6)。默认情况下,笔记本将仅使用中央处理器(CPU)。请参阅以下截图,了解右侧面板中添加数据、添加模型和笔记本选项的扩展视图:
图 2.7:右侧面板菜单,用于添加数据、模型以及笔记本选项
您可以通过数据集的名称或路径进行搜索,并且您有速度过滤器来搜索竞赛或笔记本的输出。对于模型,您也可以通过名称进行搜索,并通过类型(文本、图像、计算机视觉或视频)进行筛选。笔记本选项允许选择加速器类型(无表示仅 CPU),编程语言、持久化类型以及环境选项。
通过选择加速器,您可以切换到使用两种硬件加速器选项之一,用于图形处理单元(GPU)或张量处理单元(TPU)。在撰写本文时,CPU 配置和加速器配置的技术规范见表 2.1。对于所有这些规范,无论是使用 CPU 还是 GPU,您都有最多 12 小时的连续执行时间。然而,输入数据的大小不受限制。输出限制在 20 GB。额外的 20 GB 只能在运行时临时使用,运行结束后不会保存。
默认情况下,您的笔记本设置为不使用任何持久性。您可以选择确保文件和变量的持久性,仅文件,或仅变量。
配置 | 核心 | RAM |
---|---|---|
CPU | 4 CPU 核心 | 30 GB |
P100 GPU | 1 个 Nvidia Tesla P100 GPU 2 CPU 核心 | 13 GB |
T4 x 2 GPU | 2 个 Nvidia Tesla T4 GPU 2 CPU 核心 | 13 GB |
TPU | 1 个 TPU 4 CPU 核心 | 16 GB |
TPU 1VM | 96 CPU 核心 | 330 GB |
表 2.1:CPU 或加速器规格的技术规范
您可以将笔记本设置为始终使用原始环境或将其固定到最新环境。根据您使用的库和执行的数据处理,选择使用原始环境或使用最新可用的环境可能很有用。当您选择原始环境时,每次运行笔记本的新版本时,原始环境的设置都将保持不变。使用最新可用的环境作为替代选项时,环境(带有预定义的库版本)将更新到最新版本。
互联网访问默认设置为“开启”,但在某些情况下,您可能希望将其设置为“关闭”。对于某些代码竞赛,不允许访问互联网。在这种情况下,您可以在训练笔记本中下载动态资源,但您必须确保在运行推理笔记本进行该代码竞赛时,每个必需的资源要么在笔记本内部,要么在附带的模型、实用脚本或数据集中。
我们已经了解了笔记本的基本功能以及如何添加数据、模型和配置运行环境。现在让我们看看更高级的功能。
高级功能
基本笔记本功能使我们能够进行快速实验、测试想法和原型解决方案。然而,如果我们想构建更复杂的功能,我们将需要编写可重用的代码,将配置(包括像 API 密钥这样的机密信息)与代码分开,甚至将我们的代码与外部系统或组件集成。
Kaggle 环境提供了丰富的计算资源,但这些资源是有限的。我们可能希望将 Kaggle 笔记本与外部资源相结合,或者我们可能希望将 Kaggle(笔记本、数据集)的组件与其他组件、Google Cloud 或我们的本地环境集成。在接下来的章节中,我们将学习如何实现所有这些。
设置笔记本为实用脚本或添加实用脚本
在大多数情况下,您将在同一文件中连续的单元格中编写您笔记本的所有代码。对于更复杂的代码,尤其是在您想要重用一些代码而不在笔记本之间复制代码的情况下,您可以选择开发实用模块。Kaggle 笔记本为此目的提供了一个有用的功能,即实用脚本。
实用脚本的创建方式与笔记本相同。您必须启动一个笔记本,然后从文件菜单中选择设置为实用脚本菜单项。如果您想在当前笔记本中使用实用脚本,您需要从文件菜单中选择添加实用脚本菜单项。这将打开右侧面板上的实用脚本选择器窗口,在这里,您可以从现有的实用脚本中选择一个或多个添加到笔记本中。正如您在下面的屏幕截图中所见,添加的实用脚本旁边会出现一个+按钮(在左侧面板上可见),并且它们被添加到笔记本中,位于单独的组usr/lib(实用脚本)下,正好在输入数据部分下方和输出数据部分之前(在右侧面板上可见):
图 2.8:选择实用脚本
要在代码中使用实用脚本,您必须以与导入 Python 包相同的方式导入模块。在下面的代码片段中,我们导入了一个实用脚本中包含的模块或函数:
from data_quality_stats import missing_data
正如您所见,missing_data
函数是在实用脚本data_quality_stats
中定义的。
添加和使用密钥
有时候,您可能需要在笔记本中添加环境变量,并且希望它们保密,尤其是如果您使笔记本公开。这类变量的例子可以是您用于实验跟踪服务的连接令牌,如 Neptune.ai 或 Weights & Biases,或者各种 API 密钥或令牌。在这种情况下,您很可能会想使用其中一个附加组件,Kaggle Secrets。
选择Kaggle Secrets菜单项后,将出现类似于以下屏幕截图的窗口。在这个弹出窗口中,您可以通过按下添加一个新密钥按钮来添加新的密钥。要包含当前笔记本中的密钥,只需勾选您想要包含的密钥旁边的复选框。
图 2.9:添加并选择密钥
在前面的屏幕截图中,选择了三个密钥(两个用于 Twitter API 连接和一个用于 Weights & Biases 实验跟踪)。对于每个选定的密钥,窗口底部都会生成一条额外的类似代码片段的行。您可以将所有生成的行复制到剪贴板,以便将其包含在笔记本代码中。按下完成后,您将能够将代码粘贴到笔记本中。
一旦定义,密钥将可用于包含在任何笔记本中。您可以使用其名称旁边的编辑按钮修改一个密钥的文本。请注意,当您分支添加了密钥的笔记本时,密钥将不再与新的笔记本关联。为了使密钥对新或分支的笔记本可用,您只需在编辑该笔记本时进入密钥窗口并按下完成即可。当然,如果有人复制您的笔记本,那个 Kaggle 用户(Kaggle 用户)将必须设置他们自己的密钥。如果那个 Kaggle 用户选择为与密钥关联的变量使用不同的名称,他们还需要在代码中进行更改。此功能不仅允许您管理有用的环境变量,而且还可以轻松配置您的笔记本。
在 Kaggle 笔记本中使用 Google Cloud 服务
要在笔记本中利用 Google Cloud 服务,从附加组件菜单中选择Google Cloud 服务。在打开的对话框窗口中,您可以通过点击附加到笔记本将您的 Google 账户与笔记本同步。您还可以选择您想要与 Kaggle 环境集成的 Google Cloud 服务。
目前,Kaggle 提供与 Google Cloud Storage、BigQuery 和 AutoML 的集成。当通过 Kaggle 笔记本使用这些服务时,您需要知道这将根据您的计划产生费用。如果您选择仅使用 BigQuery 的公共数据,则不会产生任何费用。
在以下图中,我们展示了您如何选择这些服务:
图 2.10:Kaggle 集成选项
选择在 Kaggle 笔记本中要使用的 Google Cloud 服务。如前所述,您需要将您的 Google Cloud 账户链接到 Kaggle。在选择屏幕上,您可以从BigQuery、Cloud Storage和Google Cloud AI Platform(Vertex AI Workbench)中选择。在我们的示例中,选定了三种可用服务中的两种。
将您的 Kaggle 笔记本升级到 Google Cloud AI 笔记本
如果您达到 Kaggle 笔记本可用的资源限制(RAM、核心数或执行时间),您可以选择将您的笔记本提升到 Google Cloud AI 笔记本,通过将笔记本导出到 Google Cloud 来实现。Google Cloud AI 笔记本是 Google Cloud 的一项付费服务,它为您提供了使用笔记本作为集成开发环境(IDE)访问 Google Cloud 机器学习计算资源的机会。为此操作,选择文件 | 升级到 Google AI 笔记本,您将被引导到以下窗口:
图 2.11:升级到 Google Cloud AI 平台笔记本
按照以下三个步骤进行:设置一个启用计费的 Google Cloud 项目,设置您的网络实例,并运行您的代码。现在,您的代码可以不受资源限制地运行。
现在我们来看看如何使用笔记本来自动化数据集的更新。
使用笔记本自动更新数据集
您可以通过结合两个功能来自动化使用 Kaggle 笔记本生成数据集:笔记本的定时重跑和笔记本运行时的数据集更新。
首先,创建一个用于收集数据的笔记本。例如,可以是一个爬取特定网站页面以检索 RSS 新闻源或连接到 Twitter API(如前例所示)以下载推文的笔记本。将收集到的数据设置为笔记本的输出。
笔记本首次运行后,通过选择输出 | 创建数据集来初始化数据集,并设置数据集在每次笔记本运行时更新的选项。
然后,再次编辑笔记本,并按您希望数据更新的频率安排其运行,如下截图所示。一旦设置好,您将自动运行笔记本,因为数据集有在运行笔记本时更新的设置,所以数据集的更新将自动进行。
图 2.12:从 2023 年 8 月 7 日开始每天运行笔记本的安排
这里描述的机制允许您使用用户界面中可用的 Kaggle 工具执行整个自动化过程。对于更复杂的过程,您始终可以使用 Kaggle API 来定义和自动执行您的任务。在下一个小节中,我们将描述 Kaggle API 的基本功能,重点关注操作笔记本。
使用 Kaggle API 创建、更新、下载和监控您的笔记本
Kaggle API 是一个强大的工具,它扩展了 Kaggle 用户界面中可用的功能。您可以使用它执行各种任务:定义、更新和下载数据集,提交到比赛,定义新的笔记本,推送或拉取笔记本的版本,或验证运行状态。
您只需两个简单的步骤就可以开始使用 Kaggle API。让我们开始吧:
-
首先,您需要创建一个身份验证令牌。转到您的账户,然后从右侧图标中选择菜单项 Account。然后转到 API 部分。在这里,点击 Create new API token 按钮下载您的身份验证令牌(它是一个名为
kaggle.json
的文件)。如果您将从 Windows 机器上使用 Kaggle API,其位置是C:\Users\<your_name>\.kaggle\kaggle.json
。在 Mac 或 Linux 机器上,文件的路径应该是~/.kaggle/kaggle.json
。 -
接下来,您需要安装 Kaggle API 的 Python 模块。在您选择的 Python 或 conda 环境中运行以下命令:
!pip install kaggle
通过这两个步骤,您就可以开始使用 Kaggle API 了。
API 还提供了多个选项来列出您账户中的笔记本,检查笔记本状态,下载副本,创建笔记本的第一版,运行它等等。让我们看看这些选项中的每一个:
-
要根据特定的名称模式列出所有笔记本,请运行以下命令:
Kaggle kernels list -s <name-pattern>
命令将返回一个表格,包含
{username}/{kernel-slug}
,这与名称模式匹配,最后运行时间,投票数,笔记本标题和作者可读名称。 -
要验证您环境中某个笔记本的状态,请运行以下命令:
kaggle kernels status {username}/{kernel-slug}.
在这里,
{username}/{kernel-slug}
不是 Kaggle 上笔记本的完整路径,而是将跟随平台路径https://www.kaggle.com
的路径部分。 -
前面的命令将返回内核状态。例如,如果内核执行完成,它将返回:
{username}/{kernel-slug} has status "complete"
-
您可以通过运行以下命令下载笔记本:
kaggle kernels pull {username}/{kernel-slug} /path/to/download
在这种情况下,一个名为
{kernel-slug}.ipynb
的 Jupyter Notebook 将下载到/path/to/download
指定的文件夹中。 -
要创建笔记本的第一版并运行它,首先使用以下命令定义 Kaggle 元数据文件:
kaggle kernels init -p /path/to/kernel
您生成的 Kaggle 元数据文件将看起来像这样:
{ "id": "{username}/INSERT_KERNEL_SLUG_HERE", "title": "INSERT_TITLE_HERE", "code_file": "INSERT_CODE_FILE_PATH_HERE", "language": "Pick one of: {python,r,rmarkdown}", "kernel_type": "Pick one of: {script,notebook}", "is_private": "true", "enable_gpu": "false", "enable_internet": "true", "dataset_sources": [], "competition_sources": [], "kernel_sources": [], "model_sources": [] }
为了演示目的,我编辑了元数据文件以生成一个名为
Test Kaggle API
的笔记本,该笔记本使用 Python。为了您的方便,我用{username}
替换了我的用户名。您需要小心地将{kernel-slug}
与实际标题相关联,因为通常{kernel-slug}
是生成的小写版本,没有特殊字符,并将空格替换为连字符。以下是结果:{ "id": "{username}/test-kaggle-api", "title": "Test Kaggle API", "code_file": "test_kaggle_api.ipynb", "language": "python", "kernel_type": "notebook", "is_private": "true", "enable_gpu": "false", "enable_internet": "true", "dataset_sources": [], "competition_sources": [], "kernel_sources": [], "model_sources": [] }
-
编辑元数据文件后,您可以使用以下命令启动笔记本:
Kaggle kernels push -p /path/to/kernel
-
如果您也在
/path/to/kernel
文件夹中创建了笔记本的原型,并且命名为test_kaggle_api.ipynb
,您将收到以下命令的响应:Kernel version 1 successfully pushed. Please check progress at https://www.kaggle.com/code/{username} /test-kaggle-api
-
您也可以使用 API 下载现有笔记本的输出。为此,请使用以下代码:
Kaggle kernels output {username}/{kernel-slug}
这将在当前文件夹中下载一个名为 {kernel-slug}.log
的文件。或者,您可以指定以下目标的路径:
Kaggle kernels output {username}/{kernel-slug} – p /path/to/dest
文件包含内核上次运行的执行日志。
我们学习了如何创建认证令牌并安装 Kaggle API。然后,我们看到了如何使用 Kaggle API 来创建笔记本、更新它并下载它。
更多关于如何使用 Kaggle API 来提升平台使用的方法,可以在 Kaggle 文档中关于 API 的部分找到,链接为www.kaggle.com/docs/api
。
摘要
在本章中,我们学习了 Kaggle 笔记本是什么,我们可以使用哪些类型,以及可以使用哪些编程语言。我们还学习了如何创建、运行和更新笔记本。然后,我们参观了使用笔记本的一些基本功能,这将使您能够有效地开始使用笔记本,从数据集或比赛中摄取和分析数据,开始训练模型,并为比赛准备提交。此外,我们还回顾了一些高级功能,甚至介绍了 Kaggle API 的使用,以进一步扩展您对笔记本的使用,允许您构建与 Kaggle 环境集成的外部数据和 ML 管道。
更高级的功能为您在 Kaggle 笔记本的使用中提供了更多的灵活性。通过实用脚本,您可以创建模块化代码,使用专门的 Python 模块来摄取数据,对其进行统计分析,准备可视化,生成特征和构建模型。您可以在笔记本之间重用这些模块,而无需从一本笔记本复制代码到另一本。另一方面,通过密钥,您可以公开使用 API 密钥访问外部服务的笔记本,而不会暴露您的个人密钥;这是 Kaggle 的密码保险库等效物。
通过与 Google Cloud 的集成,您可以扩展您的计算或存储资源,并超越 Kaggle 平台上此类资源的限制。我们还学习了 Kaggle API 的基础知识。现在您知道如何使用 Kaggle API 来搜索现有的笔记本,创建新的笔记本,或下载现有笔记本的输出。这为您提供了定义集成 Kaggle、Google Cloud 和本地资源的混合管道的灵活性。您还可以从外部脚本中控制您的 Kaggle 笔记本。
在下一章中,我们将开始我们的数据世界之旅,第一站:对泰坦尼克比赛数据集的经典探索。
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人见面,并在以下地点与超过 5000 名成员一起学习:
第三章:开始我们的旅行 - 生存泰坦尼克号灾难
在本章中,我们将开始我们的数据世界之旅。我们将分析的第一组数据来自比赛 泰坦尼克号 - 灾难中的机器学习(在本章末尾的 参考文献 1 中找到该数据集的链接)。这是一个相当小的数据集,因为它与比赛相关,所以它被分为训练集和测试集。
在本章中,除了比赛方法之外,我们还将介绍我们系统化的探索性数据分析方法,并将其应用于熟悉数据、更详细地了解数据以及提取有用见解。我们还将简要介绍使用数据分析结果构建模型训练流程的过程。在深入实际数据之前,了解背景和理想情况下定义分析的可能目标是很有用的。
本章中所有的代码快照和图表都是从配套的笔记本中提取的,泰坦尼克号 - 数据世界之旅的开始(在本章末尾的 参考文献 2 中找到笔记本的链接)。该笔记本也位于 GitHub 仓库的 Chapter-03 文件夹中(见 参考文献 3 和 参考文献 4)。
简而言之,在本章中我们将做以下事情:
-
了解泰坦尼克号数据集背后的故事。我们将学习 1912 年那个命运攸关的日子泰坦尼克号沉没时发生了什么,我们将了解船员的人数,有多少乘客在船上,以及有多少人死亡。
-
熟悉数据,解释特征的意义,了解数据质量的第一印象,并探索有关数据的某些统计信息。
-
在我们介绍通过分析使用的图形元素:定制的调色板和派生的颜色图之后,继续使用单变量分析进行数据探索。
-
使用多元分析添加更多见解,以捕捉特征之间的复杂交互。
-
使用记录的乘客姓名进行详细分析,从中我们将提取多个特征。
-
通过对特征变化的聚合视图来探索特征的丰富性。
-
准备一个基线模型。
仔细看看泰坦尼克号
泰坦尼克号是一艘英国客轮,1912 年 4 月在北大西洋的首航中沉没。这场悲剧是由于撞上冰山而引起的,导致 2,224 名船员和乘客中的 1,500 多人死亡(美国官员的估计为 1,517 人,英国调查委员会的估计为 1,503 人)。大多数遇难者是船员,其次是三等舱乘客。
这是如何发生的?泰坦尼克号在 20 世纪初使用最先进的技术建造时被认为是一艘永不沉没的船。这种信心正是灾难的配方。正如我们所知,它确实沉没了,因为与冰山的接触损坏了几个水密舱室——足以破坏其完整性。这艘船最初的设计是携带 48 艘救生艇,但船上只有 20 艘,而且大多数救生艇在放入水中时容量不足 60%。
泰坦尼克号的长度为 269 米,最大宽度为 28 米。它有七个用字母 A 到 G 标识的甲板(A 和 B 是头等舱乘客的,C 主要留给船员,D 到 G 是二等和三等舱乘客的)。它还有两个额外的甲板:甲板(救生艇从这里放入水中)和 Orlop 甲板(在水线下)。尽管三等和二等舱的设施不如头等舱豪华舒适,但所有舱位都有共同的休闲设施,如图书馆、吸烟室,甚至还有健身房。乘客还可以使用露天或室内散步区。与那个时代的其他客轮相比,泰坦尼克号在舒适度和设施方面都更为先进。
泰坦尼克号从南安普顿起航,并计划了两个其他停靠点——一个在法国的瑟堡,一个在爱尔兰的昆士敦。乘客们分别乘坐从伦敦和巴黎到南安普顿和瑟堡的专用火车。这次首航中,泰坦尼克号上的船员大约有 885 人。船员中的大多数不是水手,而是服务员,他们照顾乘客,消防员,仓库管理员和工程师,他们负责船上的引擎。
进行数据检查
泰坦尼克号的传奇故事引人入胜。对于那些对数据探索感兴趣的人来说,关于这场悲剧的数据同样引人入胜。让我们先从对竞赛数据的简要介绍开始。来自 Titanic - Machine Learning from Disaster 的数据集包含三个 CSV (逗号分隔值)文件,正如你将在许多 Kaggle 竞赛中遇到的那样:
-
train.csv
-
test.csv
-
sample_submission.csv
我们将首先将这些文件加载到一个新的笔记本中。你已经在上一章的 基本功能 部分学习了如何这样做。你也可以通过复制一个已存在的笔记本来创建一个新的笔记本。在我们的情况下,我们将从头开始创建一个新的笔记本。
通常,笔记本从导入包的单元格开始。我们在这里也会这样做。在接下来的一个单元格中,我们希望读取训练数据和测试数据。一般来说,你需要用到的 CSV 文件与这个例子中的目录相似:
train_df = pd.read_csv("/kaggle/input/titanic/train.csv")
test_df = pd.read_csv("/kaggle/input/titanic/test.csv")
在我们加载数据后,我们将手动检查它,查看每个列包含的内容——也就是说,查看数据样本。我们将对数据集中的每个文件都这样做,但现在我们主要会关注训练和测试文件。
理解数据
在图 3.1和图 3.2中,我们可以看到一些值的选取。从这次视觉检查中,我们已能看出一些数据特征。让我们尝试总结它们。以下列在训练文件和测试文件中都是共同的:
-
乘客 ID(PassengerId)
: 每位乘客的唯一标识符。 -
乘客等级(Pclass)
: 每位乘客所乘坐的等级。我们知道从我们的背景信息来看,可能的值是 1、2 或 3。这可以被视为一个分类数据类型。因为等级的顺序传达了意义并且是有序的,所以我们可以将其视为有序或数值类型。 -
姓名(Name)
: 这是一个文本类型的字段。这是乘客的全名,包括他们的姓氏、名字,在某些情况下,婚前名字以及昵称。它还包含他们的社会阶层、背景、职业或在某些情况下,王室的头衔。 -
性别(Sex)
: 这也是一个分类字段。考虑到当时他们优先救助妇女和儿童,我们可以假设这是重要的信息。 -
年龄
: 这是一个数值字段。此外,他们的年龄是一个重要的特征,因为儿童被优先考虑进行救助。 -
兄弟姐妹或配偶数量(SibSp)
: 这个字段提供了每位乘客的兄弟姐妹或配偶信息。它是衡量乘客所旅行家庭或团体规模的一个指标。这是重要信息,因为我们可以安全地假设一个人在没有兄弟姐妹、姐妹或伴侣的情况下不会登上救生艇。 -
父母数量(Parch)
: 这是儿童乘客的父母数量或成人乘客的儿童数量。考虑到父母会在登船前等待所有孩子,这也是一个重要的特征。与SibSp
(兄弟姐妹数量)一起,Parch
可以用来计算每位乘客的家庭规模。 -
船票(Ticket)
: 这是一个与船票相关的代码。它是一个字母数字字段,既不是分类的也不是数值的。 -
船票价格(Fare)
: 这是一个数值字段。从样本中我们可以观察到,Fare
值变化很大(从 3 等舱到 1 等舱有一个数量级的变化),但我们也可以看到同一舱位的乘客中有些人的Fare
值相当不同。 -
船舱(Cabin)
: 这是一个字母数字字段。从我们在图 3.1和图 3.2中看到的小样本中,我们可以看到一些值是缺失的。在其他情况下,同一乘客预订了多个船舱(可能是富裕的乘客带着家人旅行)。船舱的名称以字母开头(C、D、E 或 F)。我们记得泰坦尼克号有多个甲板,所以我们可以猜测字母代表甲板,然后后面跟着该甲板上的船舱号码。 -
Embarked
:这是一个分类字段。在这里的样本中,我们只看到字母 C、S 和 Q,我们已经知道泰坦尼克号从南安普顿出发,在法国的瑟堡停靠,并在昆士敦(今天称为科布,科克港,爱尔兰)停靠。我们可以推断 S 代表南安普顿(起始港口),C 代表瑟堡,Q 代表昆士敦。
训练文件还包含一个Survived
字段,这是目标特征。它可以是1
或0
的值,其中1
表示乘客幸存,0
表示他们不幸没有幸存。
图 3.1:训练数据文件样本
测试文件不包括目标特征,如以下样本所示:
图 3.2:测试数据文件样本
一旦我们查看了训练和测试文件中的列,我们就可以继续进行一些额外的检查,以找到数据集的维度和特征分布:
-
使用
shape()
函数检查每个数据集(train_df
和test_df
)的形状。这将给出训练和测试文件(行数和列数)的维度。 -
对每个数据集运行
info()
函数。这将给出更复杂的信息,例如每列的非空数据量以及数据类型。 -
对每个数据集运行
describe()
函数。这仅适用于数值数据,并将创建关于数据分布的统计,包括最小值、最大值以及前 25%、50%和 75%的值,以及平均值和标准差。
前面的检查为我们提供了关于训练和测试数据集中数值数据分布的初步信息。我们可以在分析中继续使用更复杂和详细的工具,但到目前为止,你可以将这些步骤视为调查你手头任何表格数据集的一般初步方法。
分析数据
通过评估数据集的形状、值类型、空值数量和特征分布,我们将形成一个关于数据集的初步印象。
我们可以构建自己的工具来检查数据统计。在这里,我将介绍三个小脚本,用于获取缺失值统计、唯一值和最频繁的值。
首先,检索缺失数据的代码:
def missing_data(data):
total = data.isnull().sum()
percent = (data.isnull().sum()/data.isnull().count()*100)
tt = pd.concat([total, percent], axis=1, keys=['Total', 'Percent'])
types = []
for col in data.columns:
dtype = str(data[col].dtype)
types.append(dtype)
tt['Types'] = types
return(np.transpose(tt))
接下来,显示最频繁值的代码:
def most_frequent_values(data):
total = data.count()
tt = pd.DataFrame(total)
tt.columns = ['Total']
items = []
vals = []
for col in data.columns:
try:
itm = data[col].value_counts().index[0]
val = data[col].value_counts().values[0]
items.append(itm)
vals.append(val)
except Exception as ex:
print(ex)
items.append(0)
vals.append(0)
continue
tt['Most frequent item'] = items
tt['Frequence'] = vals
tt['Percent from total'] = np.round(vals / total * 100, 3)
return(np.transpose(tt))
最后,唯一值的代码:
def unique_values(data):
total = data.count()
tt = pd.DataFrame(total)
tt.columns = ['Total']
uniques = []
for col in data.columns:
unique = data[col].nunique()
uniques.append(unique)
tt['Uniques'] = uniques
return(np.transpose(tt))
在下一章中,我们将重新使用这些函数。在 Kaggle 上,你可以通过实现实用脚本来实现这一点。我们将将这些函数包含在一个可重用的实用脚本中,然后将其包含在其他笔记本中。
在下面的图中,我们看到将missing_data
函数应用于训练(a)和测试(b)数据集的结果:
a
b
图 3.3:(a)训练集和(b)测试集中的缺失值,分别
一些字段,如Age
和Cabin
,在训练集和测试集中都显示出相当比例的缺失数据。从缺失数据百分比的检查中,我们还可以初步评估数据相对于训练-测试分割的质量。如果某个特征的缺失值百分比在训练集和测试集中差异很大,我们就可以怀疑分割没有捕捉到整体数据分布。在我们的案例中,训练集和测试集中每个特征的缺失值百分比非常接近。
在以下图中,我们可以看到训练集(a)和测试集(b)中特征的频繁值:
a
b
图 3.4:分别显示(a)训练集和(b)测试集中的最频繁值。
从前面的数据中,我们可以看到,泰坦尼克号上大多数人是男性(这种多数在训练集和测试集中都有所体现),大多数乘客和船员在南安普顿(S
)登船。对于像Age
这样的更细粒度的特征,训练集和测试集中的最频繁值不同,尽管最大频率的值很接近(训练集中的Age
值为21
,而测试数据集中的Age
值为24
)。这表明直接使用Age
作为机器学习模型中的特征存在局限性,因为我们已经观察到训练集和测试集之间的整体分布不同。
下图展示了应用unique_values
函数对训练集和测试集数据集的唯一值统计结果:
a
b
图 3.5:分别显示(a)训练集和(b)测试集中的唯一值
如您所见,对于分类类型的字段,训练集中存在的所有类别也在测试集中存在。理想情况下,我们希望对于SibSp
或Parch
等数值特征也能得到相同的结果。然而,在Parch
的情况下,我们可以看到训练集中的唯一值数量为7
,而在测试数据中为8
。
在本节中,我们首先进行了初步的数据检查,以了解数据集的特征,然后检查数据质量,以查看是否存在缺失值。我们还对训练集和测试集中的特征进行了统计分析。接下来,在我们的数据探索中,我们将对训练集和测试集的分类和数值特征进行单变量分析。包含各种特征绘图图像的图片提供了更多信息,并且更容易理解和解释,即使是对于非技术读者来说也是如此。
进行单变量分析
在开始构建我们的第一个图表之前,我们将为笔记本设置一个独特的配色方案。确保整个笔记本的颜色和风格统一有助于我们保持演示的一致性,并确保读者有一个良好的平衡体验。笔记本将有一个一致的演示,视觉元素将连贯地支持笔记本的叙述。
因此,我们将定义在整个笔记本中使用的颜色集合。我们将选择一个调色板,以创建我们工作的特定视觉身份。这可以是已经定义的调色板或颜色集合,或者我们可以定义自己的调色板,基于一组选定的颜色以匹配主题。对于这个与航海(或航海)相关的笔记本,我选择了一组带有多种蓝色阴影的海洋颜色。基于这组颜色,我还定义了一个调色板。定义和显示调色板的代码如下:
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import seaborn as sns
def set_color_map(color_list):
cmap_custom = ListedColormap(color_list)
print("Notebook Color Schema:")
sns.palplot(sns.color_palette(color_list))
plt.show()
return cmap_custom
color_list = ["#A5D7E8", "#576CBC", "#19376D", "#0b2447"]
cmap_custom = set_color_map(color_list)
在以下图表中,我们展示了组成我们自定义调色板的颜色集合。笔记本配色方案使用从浅淡的晴空色到深蓝色调的蓝色阴影:
图 3.6:笔记本配色方案
我们将定义两个绘图函数(一个用于分类值,另一个用于连续/数值值)来表示同一图像上某个特征的分布,按生存状态或按训练/测试集分组。
我们将把训练数据和测试数据合并到一个数据集中(并添加一个新列来存储原始/源数据集)。这些函数使用了两个最常用的数据绘图库:matplotlib
和seaborn
。由于我们将为多个特征绘制这些图表,定义几个绘图函数是更好的选择,这样我们就不需要重复代码。
在第一个函数中,我们使用seaborn
的countplot
函数的hue
选项显示两组值:
def plot_count_pairs(data_df, feature, title, hue="set"):
f, ax = plt.subplots(1, 1, figsize=(8, 4))
sns.countplot(x=feature, data=data_df, hue=hue, palette= color_list)
plt.grid(color="black", linestyle="-.", linewidth=0.5, axis="y", which="major")
ax.set_title(f"Number of passengers / {title}")
plt.show()
在第二个函数中,为了显示特征分布,我们两次调用seaborn
中的histplot
函数——一次用于每个特征:
def plot_distribution_pairs(data_df, feature, title, hue="set"):
f, ax = plt.subplots(1, 1, figsize=(8, 4))
for i, h in enumerate(data_df[hue].unique()):
g = sns.histplot(data_df.loc[data_df[hue]==h, feature], color=color_list[i], ax=ax, label=h)
#plt.grid(color="black", linestyle="-.", linewidth=0.5, axis="y", which="major")
ax.set_title(f"Number of passengers / {title}")
g.legend()
plt.show()
要查看完整的图像列表,请访问书籍存储库并检查“泰坦尼克号 - 数据世界之旅的开始”笔记本(参考 3)。或者,您可以通过以下路径在 Kaggle 上访问相同的内容:www.kaggle.com/code/gpreda/titanic-start-of-a-journey-around-data-world
(参考 2)。
这里,我们只展示了一小部分图像,仅针对两个特征——一个用于分类值,一个用于数值值。在笔记本中,我们表示了Sex
、Pclass
、SibSp
、Parch
和Embark
以及Age
和Fare
的图表。
我们将用两个图表来表示这些特征中的每一个:一个图表显示了所有乘客的该特征分布,按列车/测试分组。另一个图表显示了相同特征的分布,仅针对训练数据,并显示了Survived
/Not Survived
之间的分割。
我们从Pclass
(这是一个分类特征)开始,展示了所有乘客的特征分布,按训练/测试数据集分组。注意在下面的截图里,有三个类别,1
、2
和3
:
图 3.7:按乘客类别分组的乘客数量,按训练和测试分组
对于相同的Pclass
特征,但仅从训练集中,我们表示按Survived
分组的数据:
图 3.8:训练集中按乘客类别分组的乘客数量,按 Survived 分组
接下来是Age
(这是一个数值)。在图 3.9中,我们展示了所有数据(训练和测试)中Age
的直方图,按训练/测试分组。在这里我们使用直方图,因为这个特征虽然不是一个连续的数字(仍然是离散的),但有许多值(根据我们运行的数据统计,似乎至少有 88 个独特的Age
值),并且从我们分析的角度来看,就像一个连续的数字。
图 3.9:按乘客类别分组的乘客数量,按训练和测试分组
图 3.10显示了训练集中Age
的直方图,按生存状态分组。
图 3.10:训练集中按 Age 分组的乘客数量,按生存状态分组
通过简单检查这些分类或连续(数值)数据的单变量分布,我们已能从数据中理解一些有趣的事实。例如,在图 3.7和3.8中,我们可以看到训练集和测试集中的数据在三个类别(1
、2
和3
)的分布比率相当相似。同时,从 Survived/Not Survived 分布中,我们可以看到虽然大约 60%的头等舱乘客幸存,但二等舱的 Survived/Not Survived 比例大约是 50-50%,而在三等舱中,只有大约 25%的乘客幸存。同样,我们还可以从Sex
、SibSp
(兄弟姐妹或配偶)或Parch
(父母或孩子的数量)的单变量分布中提取有用的见解。
在某些情况下,我们希望从现有特征中构建新的特征——换句话说,进行特征工程。特征工程涉及从原始数据中提取和转换有用的信息。特征工程的一种技术是将新特征定义为其他特征的函数。我们注意到Parch
和SibSp
一起提供了关于在泰坦尼克号上存在的家庭的信息。通过将Parch
和SibSp
相加并加1
(代表实际乘客),对于每个乘客,我们得到他们在泰坦尼克号上的家庭规模。
在图 3.11中,我们可以看到所有乘客的家庭规模图,按训练/测试数据集分组:
图 3.11:按家庭规模分组的乘客数量,按训练和测试分组
在下一个图中,我们看到的是按 Survived/Not Survived 分组,相同家庭规模的训练数据图:
图 3.12:按家庭规模分组的不同年龄区间的乘客数量
我们可以观察到,单身乘客的数量很普遍(而且这个大量也突出了这种乘客在头等舱中的高频率)。然后是家庭成员没有孩子的家庭和单身父母,接着是小型和大型家庭,成员人数最多可达 8 人甚至 11 人。正如你所看到的,这种模式来自建模前的数据探索性分析。
如果我们看一下生存率,我们可以看到单身乘客的生存率很小(大约 30%),而小型家庭(有 2、3 或 4 名成员)的生存率超过 50%。随着家庭规模的增加超过 4 人,我们可以看到生存率严重下降,有 8 人或 11 名成员的家庭生存率为零。
这可能是因为他们乘坐的是更便宜的舱位(我们知道三等舱的生存率低于头等舱)或者因为他们花费了太多时间试图在前往救生艇之前聚集所有家庭成员。我们将在本章稍后对这些细节进行一些调查。
我们可以观察到年龄
和票价
是分布值。虽然知道某个乘客的确切年龄是有用的,但在构建包含确切年龄的模型时,其价值并不大。实际上,通过学习各种年龄,模型可能会过度拟合训练数据,其泛化能力将下降。为了分析和建模的目的,将年龄(或票价)聚合到价值区间是有意义的。
Age Interval, to form five classes, from 0 to 4, corresponding to Age intervals between 0 and 16, 16 and 32, 32 and 48, 48 and 64, and above 64, respectively:
all_df["Age Interval"] = 0.0
all_df.loc[ all_df['Age'] <= 16, 'Age Interval'] = 0
all_df.loc[(all_df['Age'] > 16) & (all_df['Age'] <= 32), 'Age Interval'] = 1
all_df.loc[(all_df['Age'] > 32) & (all_df['Age'] <= 48), 'Age Interval'] = 2
all_df.loc[(all_df['Age'] > 48) & (all_df['Age'] <= 64), 'Age Interval'] = 3
all_df.loc[ all_df['Age'] > 64, 'Age Interval'] = 4
下面的代码块计算了一个新的特征,称为票价区间
,其中0
到3
(四个类别)的值分别来自票价
在0
到7.91
、7.91
到14.454
、14.454
到31
以及高于31
的值:
all_df['Fare Interval'] = 0.0
all_df.loc[ all_df['Fare'] <= 7.91, 'Fare Interval'] = 0
all_df.loc[(all_df['Fare'] > 7.91) & (all_df['Fare'] <= 14.454), 'Fare Interval'] = 1
all_df.loc[(all_df['Fare'] > 14.454) & (all_df['Fare'] <= 31), 'Fare Interval'] = 2
all_df.loc[ all_df['Fare'] > 31, 'Fare Interval'] = 3
上文所述的年龄
和票价
的特征转换具有正则化的效果。在下图中,我们展示了所有乘客的年龄
区间,按训练集和测试集分开:
图 3.13:按训练集和测试集分组的不同年龄区间的乘客数量
下图显示了生存乘客与非生存乘客的年龄
区间的分布:
图 3.14:按生存状态分组的不同年龄区间的乘客数量
到目前为止,我们已经分析了单个特征。我们将训练集和测试集合并,并在同一张图上表示了训练集和测试集之间的数据分割。我们还只显示了一个特征的训练数据,分割为生存和未生存,并可视化了一些工程特征。在下一节中,我们将通过使用多元分析在同一张图上表示多个特征来继续。
执行多元分析
我们看到,通过使用每个特征的分布图,我们可以从数据中获得非常有趣的见解。然后,我们通过特征工程来获取有用、更相关的特征。虽然单独观察变量可以帮助我们获得数据分布的初步印象,但分组值并一次观察多个特征可以揭示相关性以及不同特征如何相互作用的更多见解。
现在,我们将使用各种图形来探索特征之间的相关性,同时我们也会探索可视化选项。我们将继续使用我们最初的选项,即结合使用matplotlib
和seaborn
图形库。
图 3.15显示了按乘客等级分组的每个年龄区间的乘客数量。我们可以从这张图片中看到,在第三等级中,大多数乘客处于第一个和第二个年龄区间(即 0-16 岁和 16-32 岁之间),而在头等舱,我们拥有最均衡的年龄组。三个等级中最均衡的年龄区间是第三个年龄区间。
图 3.15:按等级分组的每个年龄区间的乘客数量
如图 3.16中下一个图表所示,大多数乘客在南安普顿(以初始S标识)登船。这些乘客中的大多数年龄都在 32 岁以下(年龄区间0和1)。在瑟堡(以初始C标识)登船的人,年龄组更加均衡。在昆士兰州(以初始Q标识)登船的乘客大多处于第一个年龄组。
图 3.16:按登船港口分组的每个年龄区间的乘客数量
从以下图表中,我们可以看到,随着家庭规模的增加和乘客等级的降低,生存的可能性降低。最糟糕的生存率是三等舱大家庭,几乎没有人生还。即使是小家庭,在第三等级中也极大地降低了他们的生存可能性。
图 3.17:家庭规模和乘客等级(Pclass)的分布,按生存状态分组
我们还可以创建组合特征——例如,我们可以将Sex
和Pclass
这两个最具预测性的因素合并成一个单一特征;让我们称它为Sex_Pclass
。以下图表显示了根据生存状态划分值时该新特征的分布。一等和二等舱的女性生存率超过 90%。在三等舱,女性的生存率约为 50%。一等和二等舱的男性生存率分别约为 30%和 20%,三等舱的大部分男性都死了。
图 3.18:按生存状态分组的组合特征 Sex_Pclass 的分布
在数据质量评估之后,我们展示了如何进行单变量分析。然后,我们给出了一些数值数据的特征工程示例,并进行了多元分析。接下来,我们将探索乘客姓名中可以找到的信息的丰富性。让我们看看名字中有什么。
从乘客姓名中提取有意义的信息
我们现在继续我们的分析,包括分析乘客的姓名以提取有意义的信息。如您从本章开头所记得的,姓名
列也包含一些附加信息。经过我们的初步视觉分析,很明显,所有姓名都遵循一个类似的结构。它们以姓氏
开头,后面跟着一个逗号,然后是一个头衔
(简短版本,后面跟着一个句号),然后是名字
,在通过婚姻获得新名字的情况下,是之前的或婚前名
。让我们处理数据以提取这些信息。提取这些信息的代码如下:
def parse_names(row):
try:
text = row["Name"]
split_text = text.split(",")
family_name = split_text[0]
next_text = split_text[1]
split_text = next_text.split(".")
title = (split_text[0] + ".").lstrip().rstrip()
next_text = split_text[1]
if "(" in next_text:
split_text = next_text.split("(")
given_name = split_text[0]
maiden_name = split_text[1].rstrip(")")
return pd.Series([family_name, title, given_name, maiden_name])
else:
given_name = next_text
return pd.Series([family_name, title, given_name, None])
except Exception as ex:
print(f"Exception: {ex}")
all_df[["Family Name", "Title", "Given Name", "Maiden Name"]] = all_df.apply(lambda row: parse_names(row), axis=1)
如您可能已经注意到的,我们选择使用split
函数来实现姓氏
、头衔
、名字
和婚前名
的提取。我们也可以使用更紧凑的实现,使用正则表达式。
让我们首先通过查看头衔
和性别
的分布来检查结果:
图 3.19:按性别划分的标题分布
我们可以看到,大多数头衔都是性别特定的,其中最常见的是女性的Miss
(带有Mlle.
版本)和Mrs.
(带有Mme.
和Dona.
版本),以及男性的Mr.
(和Ms.
或Don.
版本)和Master
。一些头衔很少见,如军事(Capt.
, Col.
, Major
和Jonkheer
)、职业(Dr.
和Rev.
)或贵族(Sir
, Lady
和Countess
)。Dr.
是唯一一个被两种性别使用的头衔,我们将在本章稍后对其进行更详细的探讨。
让我们看看头衔
按年龄段
的分布:
图 3.20:按年龄段划分的标题分布
从这个新视角来看,我们可以看到一些头衔是为特定年龄段保留的,而其他头衔则分布在所有年龄段。Master
似乎只用于 18 岁以下的男性,但Mr.
也用于这个年龄段。从我们所看到的,Master
头衔仅用于与家人一起旅行的男性儿童,而年轻的男性头衔为Mr.
的是独自旅行,因为已经独立,被视为年轻人。Miss
头衔不遵循相同的模式,因为它同样被赋予女性儿童、年轻或未婚女性(但在较高级别时较少)。有趣的是看到Dr.
头衔在广泛的年龄段中分布得很好。
现在,让我们看看第三等级中的一些大家庭。如果我们按Family Name
(姓氏)、Family Size
(家庭规模)、Ticket
(车票,以保持同一车票旅行的人在一起)和Age
(年龄)排序数据,我们将获得来自同一真实家庭的乘客序列。出现频率最高的Family Name
值是安德森(11 条记录)、萨格(11 条记录)、古德温(Goodwin)(8 条记录)、阿斯普伦德(Asplund)(8 条记录)和戴维斯(Davies)(7 条记录)。我们还不知道他们是否也来自同一个家庭,或者只是共享同一个姓氏。让我们看看共享安德森姓氏的乘客数据。
从图 3.21中,我们看到有一个名叫安德森的家庭,父亲名叫安德斯·约翰(Anders Johan),母亲名叫阿尔弗里达·康斯坦蒂亚(Alfrida Konstantia),他们带着五个孩子(四个女儿和一个儿子)一起旅行,年龄在 2 到 11 岁之间。已婚的妇女在家庭中注册时,会先写上她们的头衔,然后是丈夫的名字,并在括号内加上她们的婚前名字。这个家庭中没有人在三等舱中幸存。
图 3.21:共享安德森姓氏的乘客
只有那些持有同一张车票的人才属于同一个家庭。这意味着只有持有车票号码 347082 的人属于安德森(Andersson)家庭,而其他人则是单独旅行的。他们的数据似乎不太准确,因为其中一些人似乎属于一个更大的家庭,但我们找不到他们的亲戚。
最大的家庭群体是萨格(Sage),正如我们从图 3.22中可以看到的那样。这是一个 11 口之家(两位父母和九个孩子)。我们不知道他们的年龄(除了其中一个男孩,他 14.5 岁);我们只知道他们的名字,以及有五个男孩和四个女孩的事实。我们推测其中三个男孩已经成年,因为他们的头衔是先生
。我们只知道 11 个人中的 9 个没有幸存(那些Survived没有指定值的其他家庭成员是测试集的一部分)。
图 3.22:共享萨格姓氏的乘客
这些家庭在新世界中寻找更好生活的故事令人感动,尤其是当我们意识到,遗憾的是,这些有很多孩子的大家庭并没有设法自救。我们不知道决定性因素是什么:他们可能等待得太久才上甲板,希望团聚,或者也许他们在前往救生艇的路上努力保持在一起。无论如何,将家庭规模信息添加到模型中可能会给我们一个有用的特征来预测生存,因为我们可以看到,大家庭中的人生存的机会较低。
此外,我们还可以进行一些其他有趣的统计分析,这些分析对生存的预测价值较少,但可以给我们提供更多关于数据分布的见解。以下图显示了整体数据的Given Name
分布(按性别分组):
图 3.23:乘客的给定名字(女孩/未婚女性和男孩/男性)
下图显示了整体姓氏分布以及按启航港口的分布。我们注意到大多数乘客在 Southampton(用S表示)登船;因此,该港口登船乘客的姓氏分布将主导整体情况。其他两个启航港口是法国的 Cherbourg(用C表示)和爱尔兰的 Queenstown(用Q表示)。我们可以观察到各种启航港口中民族名字的普遍性,在南安普顿是斯堪的纳维亚人;在 Cherbourg 是法国人、意大利人、希腊人和北非人;在 Queenstown 是爱尔兰人和苏格兰人。
图 3.24:按启航港口分组的姓氏
在图 3.25中,我们看到两位共享舱位D17的乘客。其中一位有头衔Dr.
,并且是女性。她与另一位女性同伴,Mrs. Swift,一起头等舱旅行。她们两人都幸存了下来。
我们创建了一个工程特征Title
,因为 Dr. Leader 既是Mrs.
(我们知道她已婚,因为她的婚前名字也被提及)又是Dr.
;我们必须选择给她分配哪个头衔。Dr.
在那个时代主要与男性(生存可能性较低)相关联。作为一个女性,她会有更高的生存概率。虽然这当然是一个有争议的问题,但我在这里提到它只是为了给你一个更好的形象,说明我们在为预测模型构建候选特征时可以达到的深度。
图 3.25:共享舱位 D17 的乘客 – 其中一位是女性,并且有 Dr.头衔
在介绍了单变量和多变量分析以及一些类型的特征工程,包括处理名字以提取头衔之后,我们还对大家庭和一些罕见案例进行了详细分析:非常大的家庭和具有不寻常头衔的乘客。在下一节中,我们将创建一个包含多个图表的仪表板图,每个图表都有单变量或双变量分析。我们可以使用这样的复杂图表来更好地捕捉复杂的特征交互,而不会在一个图表中加载过多的特征。
创建显示多个图表的仪表板
我们已经探索了分类数据、数值数据和文本数据。我们学习了如何从文本数据中提取各种特征,并从一些数值数据中构建了聚合特征。现在,让我们通过分组头衔和家庭规模来构建两个更多特征。我们将创建两个新的特征:
-
标题:通过将相似标题(如
Miss
与Mlle.
,或Mrs.
和Mme.
)或罕见的(如Dona.
、Don.
、Capt.
、Jonkheer
、Rev.
和Countess
)聚类在一起,并保留最频繁的几个——Mr.
、Mrs.
、Master
和Miss
-
家庭类型:通过从家庭大小值创建三个聚类——Single表示家庭大小为 1,Small表示由最多 4 名成员组成的家庭,Large表示有超过 4 名成员的家庭
然后,我们将几个简单或派生特征(我们了解到它们具有重要的预测价值)表示在单个图表上。我们展示了乘客的生存率,包括Sex
、乘客等级(Pclass
)、Age Interval
、Fare Interval
、Family Type
和Title
(聚类)。图表还显示了子集(由类别和生存状态共同决定)占所有乘客的百分比:
图 3.26:不同特征的乘客生存率(原始或派生)
通过这样,我们已经对“泰坦尼克号——灾难中的机器学习”竞赛数据集进行了逐步的探索性数据分析。现在,凭借我们对数据分布、特征之间的关系以及各种特征与目标特征(Survived
字段)之间的相关性的了解,我们将创建一个基线模型。
构建基线模型
通过我们的数据分析,我们能够识别出一些具有预测价值的特征。现在,我们可以利用这些知识来选择相关特征,构建模型。我们将从一个只使用我们调查的众多特征中的两个特征的模型开始。这被称为基线模型,它被用作解决方案增量优化的起点。
对于基线模型,我们选择了RandomForestClassifier
模型。该模型使用简单,默认参数下就能给出良好的结果,并且可以通过特征重要性轻松解释。
让我们从以下代码块开始实现模型。首先,我们导入一些准备模型所需的库。然后,我们将分类数据转换为数值。我们需要这样做,因为我们选择的模型只处理数字。将分类特征值转换为数字的操作称为标签编码。然后,我们将训练数据集分成训练集和验证集,使用 80-20%的分割。然后,使用训练子集对模型进行拟合,并使用验证子集来评估训练(拟合)后的模型:
from sklearn.model_selection import train_test_split
from sklearn import metrics
from sklearn.ensemble import RandomForestClassifier
# convert categorical data in numerical
for dataset in [train_df, test_df]:
dataset['Sex'] = dataset['Sex'].map( {'female': 1, 'male': 0} ).astype(int)
# train-validation split (20% validation)
VALID_SIZE = 0.2
train, valid = train_test_split(train_df, test_size=VALID_SIZE, random_state=42, shuffle=True)
# define predictors and target feature (labels)
predictors = ["Sex", "Pclass"]
target = 'Survived'
# train and validation data and labels
train_X = train[predictors]
train_Y = train[target].values
valid_X = valid[predictors]
valid_Y = valid[target].values
# define the classification model (Random Forest)
clf = RandomForestClassifier(n_jobs=-1,
random_state=42,
criterion="gini",
n_estimators=100,
verbose=False)
# fit the model with training data and labels
clf.fit(train_X, train_Y)
# predict the survival status for the validation set
preds = clf.predict(valid_X)
在图 3.27 中,我们展示了验证集的precision
、recall
和f1-score
(使用sklearn.metrics
模块中的classification_report
函数获得)。
图 3.27:使用性别和 Pclass 特征训练的基线模型的验证数据分类报告
使用此基线模型获得的前置结果仍然很差。我们必须使用模型细化技术来细化模型,从训练和验证错误观察开始。基于这些观察,我们可能想要先改进训练,然后再专注于改进模型泛化。因此,我们可能会选择添加更多具有预测价值的特征(通过选择现有特征或通过特征工程创建新特征),执行超参数优化,选择更好的分类算法,或者结合不同的算法。
摘要
在本章中,我们乘坐泰坦尼克号开始了在数据世界中的旅程。我们首先对每个特征进行了初步的统计分析,然后继续进行单变量分析和特征工程,以创建派生或聚合特征。我们从文本中提取了多个特征,并且还创建了复杂的图表来同时可视化多个特征并揭示它们的预测价值。然后我们学习了如何通过在整个笔记本中使用自定义颜色图来为我们的分析分配统一的视觉身份。
对于一些特征——最明显的是,那些从名字中派生出来的特征——我们进行了深入的探索,以了解泰坦尼克号上大家庭的命运以及根据登船港口的名字分布情况。一些分析和可视化工具很容易重用,在下一章中,我们将看到如何提取它们作为实用脚本在其他笔记本中使用。
在下一章中,我们将对包含地理数据的两个数据集进行详细的数据探索性分析。对于每个数据集,我们将从数据质量评估开始,然后继续进行数据探索,介绍针对地理数据分析的特定分析方法、工具和库。我们将学习如何操作多边形数据,以及如何合并、融合和裁剪存储为多边形集合的地理数据集。我们还将介绍用于地理数据可视化的各种库。在对两个数据集进行单独分析后,我们将结合两个数据集的信息来构建包含来自两个数据集的多层信息的先进地图。
参考文献
-
泰坦尼克号 - 从灾难中学习机器学习,Kaggle 比赛:
www.kaggle.com/competitions/titanic
-
Gabriel Preda,泰坦尼克号 - 数据世界之旅的开始,Kaggle 笔记本:
www.kaggle.com/code/gpreda/titanic-start-of-a-journey-around-data-world
-
Developing-Kaggle-Notebooks,Packt Publishing GitHub 仓库:
github.com/PacktPublishing/Developing-Kaggle-Notebooks/
-
开发-Kaggle 笔记本,Packt 出版 GitHub 仓库,第三章:
github.com/PacktPublishing/Developing-Kaggle-Notebooks/tree/main/Chapter-03
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人相聚,并和超过 5000 名成员一起学习,详情请见:
第四章:在伦敦休息一下,喝一杯啤酒或咖啡
我们继续使用数据探索世界的旅程,在本章中通过探索两个具有地理分布信息的数据库来继续我们的旅程。第一个数据集是 《英格兰每家酒吧》(见 参考文献 1)。这个数据集包含了几乎每个英格兰酒吧的唯一标识符、名称、地址、邮编以及关于地理位置的信息。第二个数据集称为 《全球星巴克位置》(见 参考文献 3),它包含了店铺编号、名称、所有权细节,以及全球所有星巴克店铺的街道地址、城市和地理信息(纬度和经度)。
除了合并这两个数据集,我们还将添加额外的地理支持数据。我们将学习如何处理缺失数据,如果需要的话,如何进行插补,如何可视化地理数据,如何裁剪和合并多边形数据,如何生成自定义地图,以及如何在它们之上创建多个图层。这些只是我们在本章中将学习的一些技巧,但简而言之,以下主题将被涵盖:
-
英格兰酒吧和全球星巴克的数据分析
-
伦敦酒吧和星巴克的联合地理分析
本章探索地理分析工具和技术的前提是为了分析酒吧和星巴克咖啡店在地理上的交织情况,回答诸如“如果有人在伦敦市中心的一家酒吧里喝了几品脱的啤酒,然后想喝咖啡,他们需要走多远才能到达最近的星巴克咖啡店?”或者,再举一个例子,“对于当前的星巴克店铺,哪些酒吧比其他任何星巴克咖啡店更靠近这个店铺?”当然,这些并不是我们试图回答的唯一问题,但我们想让你一窥我们将在本章结束时实现的目标。
英格兰的酒吧
《英格兰每家酒吧》 数据集(参考文献 1)包含了关于英格兰 51,566 家酒吧的数据,包括酒吧名称、地址、邮政编码、地理位置(通过经度和纬度以及东西向和南北向),以及地方当局。我创建了一个笔记本,《英格兰每家酒吧 – 数据探索》(参考文献 2),用于调查这些数据。当前章节中的代码片段主要来自这个笔记本。在阅读本书的同时并行查看笔记本可能会更容易理解。
数据质量检查
对于数据质量检查,我们将使用 info()
和 describe()
函数来获得初步了解。这两个函数可以被认为是开始的地方。然后,我们还可以使用我们在上一章中定义的自定义数据质量统计函数。因为我们将继续使用它们,所以我们将它们分组在一个实用脚本中。我称这个实用脚本为 data_quality_stats
,并在本模块中定义了 missing_data
、most_frequent_values
和 unique_values
函数。
要使用此实用脚本中定义的函数,我们首先需要将其添加到笔记本中。从文件菜单,我们将选择添加实用脚本菜单项,然后通过在编辑窗口右侧的添加数据面板中选择它来添加实用脚本:
图 4.1:将实用脚本添加到笔记本中
然后,我们将import
添加到笔记本的第一个单元格之一:
from data_quality_stats import missing_data, most_frequent_values, unique_values
让我们检查应用此函数到我们的pub_df
数据框后的结果。图 4.2显示了缺失值:
图 4.2:缺失值
我们可以看到有两个地方当局的缺失值。除此之外,似乎没有其他缺失值。我们需要对缺失值保持警惕,因为一些可能被隐藏;例如,一个缺失值可能根据惯例被替换为特定值(例如,使用“-1”表示正数的空值或“NA”表示分类情况)。
图 4.3展示了最频繁的值:
图 4.3:最频繁的值
如果我们现在查看最频繁的值,我们可以观察到,对于纬度和经度,都有 70 个项的值为\N。有趣的是,东经和北纬也有 70 个最频繁的值。东经和北纬是地理笛卡尔坐标:东经指的是向东测量的距离,而北纬指的是向北测量的距离。根据通用横轴墨卡托(UTM)坐标系统,北纬是到赤道的距离;在同一坐标系统中,东经是到“虚假东经”的距离,它在每个 UTM 区域内唯一定义。我们还可以观察到,最常见的酒吧名称是The Red Lion,并且在兰卡斯特大学有8家酒吧。至于唯一值,我们可以观察到地址的数量比邮编多,纬度和经度的数量也比邮编多。
图 4.4展示了唯一值:
图 4.4:唯一值
地址的唯一值数量大于邮编的唯一值(同一邮编上有更多地址)。不同地方当局的总数是376。此外,请注意,唯一名称的数量少于唯一地址的数量(可能是因为有几个流行的酒吧名称)。
让我们更详细地检查两个缺失的地方当局值。这很奇怪,因为只有两个缺失值,这是不预期的。我们还知道,纬度和经度都有 70 个缺失值,这些值被标记为\N。看看包含此缺失地方当局信息的行:
图 4.5:缺少地方当局信息的行
看起来信息缺失是因为当 pandas 使用的解析器读取 CSV 文件时遇到序列\”,”,它无法区分逗号分隔符(,)。因此,对于这两行,它将名称与地址合并,然后每列向左移动一个位置,从而破坏了从地址到地方当局的每一列。
我们有两个选项来解决这个问题:
-
一个选项是尝试向解析器提供一个分隔符列表。在我们的情况下,这会有些棘手,因为我们只有一个逗号分隔符。此外,如果我们尝试使用多字符分隔符,我们需要切换到不同的引擎,Python,因为默认引擎不支持多字符分隔符。
-
第二个选项,也是首选的选项,是编写一小段代码来修复我们发现的两个行中的问题。
这里是修复两个行问题的代码片段。我们使用两个行的索引(我们可以在图 4.5中看到它们 – 第一列,没有名称)来识别它们,并在这些行上执行校正:
columns = ['local_authority', 'longitude', 'latitude', 'northing', 'easting', 'postcode', 'address']
# use the rows indexes to locate the rows
for index in [768, 43212]:
for idx in range(len(columns) - 1):
# we use `at` to make sure the changes are done on the actual dataframe, not on a copy of it
pub_df.at[index, columns[idx]] = pub_df.loc[index][columns[idx + 1]]
# split the corrupted name and assign the name and address
name_and_addresse = pub_df.loc[index]['name'].split("\",\"")
pub_df.at[index, 'name'] = name_and_addresse[0]
pub_df.at[index, 'address'] = name_and_addresse[1]
在图 4.6中,我们可以看到名称和地址现在已经被分割并分配到正确的列中,其余的列都向右移动了:
图 4.6:校正后带有地方当局信息的行
如果我们再次检查缺失的数据,它将显示没有其他数据缺失。我们已经知道,实际上有 70 个缺失的纬度和经度;它们只是被标记为\N。如果我们单独检查具有此值的纬度或经度列,然后检查两个列都有相同值的行,我们可以得出结论,只有 70 行总共有这种异常。对于相同的行,我们看到北纬和东经有唯一值,而这些值是不正确的。
因此,我们将无法从东经和北纬重建经纬度。当检查这些行的相应邮政编码、地址和地方当局时,我们可以看到有多个地点,分布在多个地方当局区域。这 70 行中有 65 个不同的邮政编码。由于我们确实有邮政编码,我们将能够使用它们来重建经纬度。
为了这个目的,我们将开放邮编地理数据集(见参考文献 4)纳入我们的分析。此数据集包含超过 250 万行,以及许多其他列,除了邮编、纬度和经度。我们从开放邮编地理数据集中读取 CSV 文件,仅选择四个列(邮编、国家、纬度和经度),并过滤掉任何邮编不在我们原始数据集中酒吧邮编列表中的行。对于 70 行缺失地理数据的行,我们将经度
和纬度
的值设置为None
:
post_code_df = pd.read_csv("/kaggle/input/open-postcode-geo/open_postcode_geo.csv", header=None, low_memory=False)
post_code_df = post_code_df[[0, 6, 7, 8]]
post_code_df.columns = ['postcode', 'country', 'latitude', 'longitude']
我们将两个结果数据集(酒吧和邮编)合并,并在左列中用右列的值填充纬度和经度的缺失值:
pub_df = pub_df.merge(post_code_df, on="postcode", how="left")
pub_df['latitude'] = pub_df['latitude_x'].fillna(pub_df['latitude_y'])
pub_df['longitude'] = pub_df['longitude_x'].fillna(pub_df['longitude_y'])
pub_df = pub_df.drop(["country", "latitude_x", "latitude_y", "longitude_x", "longitude_y"], axis=1)
现在,我们已经将目标行中的所有缺失数据替换为有效的经纬度值。图 4.7是组合数据集的快照。
图 4.7:组合数据集快照(英格兰和开放邮编中的每个酒吧)
现在数据插补完成后,我们可以继续进行数据探索。
数据探索
我们将首先探索每个酒吧名称和地方当局的频率。为了表示这些信息,我们将重用上一章中开发的colormap
和plot
函数。我创建了一个实用脚本,它以与数据统计实用脚本相同的方式导入:
from plot_utils import set_color_map, plot_count, show_wordcloud
导入后,我们将提取县和市(如果地址行包含两个以上的逗号)并分析这些地方的单词频率。市是通过以下简单代码提取的:
def get_city(text):
try:
split_text = text.split(",")
if len(split_text) > 3:
return split_text[-2]
except:
return None
pub_df["address_city"] = pub_df["address"].apply(lambda x: get_city(x))
在图 4.8中,我们显示了每个地方当局的前 10 家酒吧:
图 4.8:地方当局酒吧数量(前 10 名)
图 4.9显示了每个县的前 10 家酒吧。我们通过从地址中检索逗号之后的最后一个子字符串来提取县。在某些情况下,它不是一个县,而是一个大城市,如伦敦:
图 4.9:各县酒吧数量(前 10 名)
图 4.10显示了酒吧名称和地址中单词的分布:
图 4.10:酒吧名称(左)和地址(右)的单词分布
由于我们有了酒吧的地理位置,我们希望可视化这些信息。我们可以使用folium
Python 库和folium 插件
MarkerCluster
来表示酒吧的位置。Folium(它包装了一些最受欢迎的 Leaflet 外部插件)是显示地理分布信息的一个极好方式。
显示英国地图的代码如下:
import folium
from folium.plugins import MarkerCluster
uk_coords = [55, -3]
uk_map = folium.Map(location = uk_coords, zoom_start = 6)
uk_map
要添加标记,我们可以添加以下代码(不包括初始化 folium 地图层的代码):
locations_data = np.array(pub_map_df[["latitude", "longitude"]].astype(float))
marker_cluster = MarkerCluster(locations = locations_data)
marker_cluster.add_to(uk_map)
uk_map
我们还可以为MarkerCluster
添加除了位置之外的信息弹出,以及自定义图标。
图 4.11 展示了基于 OpenStreetMap 的英国群岛 folium(leaflet)地图,没有酒吧信息层:
图 4.11:没有酒吧信息层的英国群岛地图
图 4.12 展示了添加了酒吧信息层的英国群岛地图,使用了 MarkerCluster 插件。使用 MarkerCluster 后,标记会动态替换,并显示一个组件来显示某个区域内的标记数量。当放大某个区域时,MarkerCluster 的显示会动态变化,显示标记分布的更详细视图:
图 4.12:添加了酒吧信息层的英国群岛地图
图 4.13 展示了之前地图的放大版本。我们放大查看的区域是英国大陆的南部:
图 4.13:添加了酒吧信息层的英国群岛地图,放大查看南部地区,包括伦敦地区
图 4.14 放大查看伦敦地区。随着我们放大,簇被分成更小的组,这些组作为单独的标记出现:
图 4.14:伦敦地区的放大视图
另一种可视化酒吧浓度的方法是使用热力图。热力图可以很好地直观地展示数据的空间分布。它们通过颜色阴影显示分布密度,如图 4.15所示。热力图有助于连续显示数据点的密度,并且使用热力图更容易评估不同位置的强度。因为热力图使用插值技术来在数据点之间创建平滑过渡,所以它们可以提供数据分布的更直观表示。您可以看到两个缩放级别,分别是整个大不列颠的酒吧分布热力图视图(左侧)和大陆西南角的视图(右侧):
图 4.15:使用 folium 和 Heatmap 显示位置密度分布的地图
注意,没有包括北爱尔兰的酒吧。这是因为酒吧数据的收集将其排除在外,因为它不是大不列颠的一部分。
另一种表示酒吧数据空间分布的方法是使用与酒吧位置相关的 Voronoi 多边形(或 Voronoi 图)。Voronoi 多边形代表Delaunay 剖分的伴随图。让我们解释一下我们刚才介绍的两个概念:Voronoi 多边形和 Delaunay 剖分。
如果我们在平面上有一个点的分布,我们可以使用 Delaunay 剖分来生成这些点的三角剖分。这个图是一组三角形,其边连接所有点,且不交叉。如果我们画出 Delaunay 图中边的中位线,这些新线段交点形成的网络就是 Voronoi 多边形网格。在图 4.16中,我们展示了一组点及其相关的 Voronoi 图:
图 4.16:平面上一组点及其由此组点生成的 Voronoi 多边形
这个 Voronoi 多边形图有一个有趣的性质。在一个 Voronoi 多边形内部,所有点都更接近多边形的权重中心(这是原始图的一个顶点)而不是任何相邻多边形的权重中心。因此,从我们的酒吧地理位置绘制的 Voronoi 多边形将准确地表示酒吧的集中度,并且也会以良好的近似显示某个酒吧“覆盖”的面积。我们将使用由 Voronoi 多边形形成的 Voronoi 图来显示每个酒吧覆盖的虚拟区域。
首先,我们使用scipy.spatial
模块中的Voronoi函数提取 Voronoi 多边形:
from scipy.spatial import Voronoi, voronoi_plot_2d
locations_data = np.array(pub_map_df[["longitude", "latitude"]].astype(float))
pub_voronoi = Voronoi(locations_data)
我们可以使用voronoi_plot_2d
函数(见图 4.17)来表示与酒吧(来自pub_voronoi
)相关的 Voronoi 多边形。然而,这个图有几个问题。首先,有许多多边形很难区分。然后,酒吧的位置(图中用点表示)不太清晰。另一个问题是边界上的多边形没有与领土对齐,产生了不必要且不反映真实面积“覆盖”的伪影。我们将应用一系列变换来消除图中提到的这些问题。
以下代码创建了一个与图 4.17中所示的 Voronoi 多边形图像:
fig = voronoi_plot_2d(pub_voronoi,
show_vertices=False)
plt.xlim([-8, 3])
plt.ylim([49, 60])
plt.show()
图 4.17:Voronoi 多边形的 2D 图,扩展到领土外(未裁剪)
如果我们只想在英国内部领土边界内表示每个多边形“覆盖”的地理区域,我们必须将来自酒吧位置的 Voronoi 多边形与描述领土边界的多边形裁剪。
幸运的是,我们有访问 Kaggle 的权限,可以获取各种国家的形状文件数据文件格式。对于我们的目的,我们将从GADM Data for UK数据集(见参考文献 5)导入英国 ESRI 形状文件数据。此数据集提供增量详细的形状文件数据,从外部边界(级别 0)到国家级别(级别 1)和县级别(级别 2)的整个领土。可以使用几个库读取形状文件;在这种情况下,我更喜欢使用geopandas
库。这个库具有多个对我们分析有用的功能。选择这个库的一个优点是,虽然它增加了操作和可视化地理空间数据的功能,但它保持了pandas
库的用户友好性和多功能性。我们以增量分辨率加载领土信息文件:
import geopandas as gpd
uk_all = gpd.read_file("/kaggle/input/gadm-data-for-uk/GBR_adm0.shp")
uk_countries = gpd.read_file("/kaggle/input/gadm-data-for-uk/GBR_adm1.shp")
uk_counties = gpd.read_file("/kaggle/input/gadm-data-for-uk/GBR_adm2.shp")
使用geopandas
的read_file
函数加载数据。这返回一个GeoDataFrame
对象,这是一种特殊的 DataFrame 类型。它是与pandas
一起使用的 DataFrame 对象的扩展,包括地理空间数据。如果 DataFrame 通常包括整数、浮点、文本和日期类型的列,那么 GeoDataFrame 也将包括具有特定于空间分析数据的列,例如与地理空间区域表示相关的多边形。
在使用它来裁剪 Voronoi 多边形之前检查地理空间数据是有用的。让我们可视化三种不同分辨率的地理空间数据。我们可以使用与每个GeoDataFrame关联的绘图函数来完成此操作:
fig, ax = plt.subplots(1, 3, figsize = (15, 6))
uk_all.plot(ax = ax[0], color = color_list[2], edgecolor = color_list[6])
uk_countries.plot(ax = ax[1], color = color_list[1], edgecolor = color_list[6])
uk_counties.plot(ax = ax[2], color = color_list[0], edgecolor = color_list[6])
plt.suptitle("United Kingdom territory (all, countries and counties level)")
plt.show()
图 4.18:整个领土、国家级别和县级别的英国形状文件数据(从左到右)
我们已经观察到酒吧仅存在于英格兰、苏格兰和威尔士,而不在北爱尔兰。如果我们使用英国级别的数据裁剪酒吧的 Voronoi 多边形,我们可能会遇到这样的情况:包含英格兰和威尔士西部海岸酒吧的 Voronoi 多边形可能会溢出到北爱尔兰的领土。这可能会导致不希望出现的伪影。为了避免这种情况,我们可以按以下方式处理数据:
-
仅从国家级别的形状文件中提取英格兰、苏格兰和威尔士的数据。
-
使用
geopandas
的dissolve
方法合并三个国家的多边形数据。
uk_countries_selected = uk_countries.loc[~uk_countries.NAME_1.isin(["Northern Ireland"])]
uk_countries_dissolved = uk_countries_selected.dissolve()
fig, ax = plt.subplots(1, 1, figsize = (6, 6))
uk_countries_dissolved.plot(ax = ax, color = color_list[1], edgecolor = color_list[6])
plt.suptitle("Great Britain territory (without Northern Ireland)")
plt.show()
结果内容如下所示:
图 4.19:过滤北爱尔兰并使用 dissolve 合并多边形后的英格兰、苏格兰和威尔士的形状文件数据
现在,我们有了来自三个国家的正确裁剪多边形。在裁剪多边形之前,我们需要从 Voronoi 对象中提取它们。以下代码正是这样做的:
def extract_voronoi_polygon_list(voronoi_polygons):
voronoi_poly_list = []
for region in voronoi_polygons.regions:
if -1 in region:
continue
else:
pass
if len(region) != 0:
voronoi_poly_region = Polygon(voronoi_polygons.vertices[region])
voronoi_poly_list.append(voronoi_poly_region)
else:
continue
return voronoi_poly_list
voronoi_poly_list = extract_voronoi_polygon_list(pub_voronoi)
这样,我们就拥有了执行裁剪操作所需的一切。我们首先将 Voronoi 多边形列表转换为 GeoDataFrame
对象,类似于我们将用于裁剪的 uk_countries_dissolved
对象。我们裁剪多边形,以便在表示时,多边形不会超出边界。为了正确执行裁剪操作且不出现错误,我们必须使用与裁剪对象相同的投影。我们使用 geopandas
库中的 clip
函数。这个操作非常耗时和占用 CPU 资源。在 Kaggle 基础设施上,运行我们列表中的 45,000 个多边形的整个操作(使用 CPU)需要 35 分钟:
voronoi_polygons = gpd.GeoDataFrame(voronoi_poly_list, columns = ['geometry'], crs=uk_countries_dissolved.crs)
start_time = time.time()
voronoi_polys_clipped = gpd.clip(voronoi_polygons, uk_countries_dissolved)
end_time = time.time()
print(f"Total time: {round(end_time - start_time, 4)} sec.")
以下代码绘制了整个裁剪多边形的集合:
fig, ax = plt.subplots(1, 1, figsize = (20, 20))
plt.style.use('bmh')
uk_all.plot(ax = ax, color = 'none', edgecolor = 'dimgray')
voronoi_polys_clipped.plot(ax = ax, cmap = cmap_custom, edgecolor = 'black', linewidth = 0.25)
plt.title("All pubs in England - Voronoi polygons with each pub area")
plt.show()
在 图 4.20 中,我们可以看到结果图。有些区域酒吧集中度较高(多边形较小),而在某些区域(例如苏格兰的一些地区),两个酒吧之间的距离较大。
图 4.20:来自酒吧地理分布的 Voronoi 多边形,使用所选三个国家(英格兰、威尔士和苏格兰)的溶解国家级数据裁剪
另一种展示酒吧空间分布的方法是在地方当局级别汇总数据,并在该地方当局酒吧分布的地理中心周围构建 Voronoi 多边形。每个新的 Voronoi 多边形中心是当前地方当局中每个酒吧的经纬度平均值。得到的多边形网格不重建地方当局的空间分布,但它以很好的精度表示了相对酒吧分布。得到的结果 Voronoi 多边形集使用之前相同的裁剪多边形进行裁剪。更准确地说,在我们使用裁剪多边形之前,通过溶解国家级形状文件数据获得了轮廓。我们可以使用分级颜色图来表示每平方单位的酒吧密度。让我们看看创建和可视化这个网格的代码。
首先,我们创建一个数据集,其中包含每个地方当局的酒吧数量以及酒吧位置的经纬度平均值:
pub_df["latitude"] = pub_df["latitude"].apply(lambda x: float(x))
pub_df["longitude"] = pub_df["longitude"].apply(lambda x: float(x))
pubs_df = pub_df.groupby(["local_authority"])["name"].count().reset_index()
pubs_df.columns = ["local_authority", "pubs"]
lat_df = pub_df.groupby(["local_authority"])["latitude"].mean().reset_index()
lat_df.columns = ["local_authority", "latitude"]
long_df = pub_df.groupby(["local_authority"])["longitude"].mean().reset_index()
long_df.columns = ["local_authority", "longitude"]
pubs_df = pubs_df.merge(lat_df)
pubs_df = pubs_df.merge(long_df)
mean_loc_data = np.array(pubs_df[["longitude", "latitude"]].astype(float))
然后,我们计算与这个分布相关的 Voronoi 多边形:
mean_loc_data = np.array(pubs_df[["longitude", "latitude"]].astype(float))
pub_mean_voronoi = Voronoi(mean_loc_data)
mean_pub_poly_list = extract_voronoi_polygon_list(pub_mean_voronoi)
mean_voronoi_polygons = gpd.GeoDataFrame(mean_pub_poly_list, columns = ['geometry'], crs=uk_countries_dissolved.crs)
我们使用之前用于裁剪的相同多边形裁剪得到的多边形(选择英格兰、威尔士和苏格兰并溶解形状文件到一个单独的形状文件):
mean_voronoi_polys_clipped = gpd.clip(mean_voronoi_polygons, uk_countries_dissolved)
以下代码绘制了在地方当局级别(Voronoi 多边形的中心是地方当局区域内所有酒吧的平均经纬度)聚合的酒吧地理分布的 Voronoi 多边形,使用溶解的国家级数据(选择了三个国家:英格兰、苏格兰和威尔士)裁剪。我们使用绿色颜色渐变来表示每平方单位的酒吧密度(见 图 4.21):
fig, ax = plt.subplots(1, 1, figsize = (10,10))
plt.style.use('bmh')
uk_all.plot(ax = ax, color = 'none', edgecolor = 'dimgray')
mean_voronoi_polys_clipped.plot(ax = ax, cmap = "Greens_r")
plt.title("All pubs in England\nPubs density per local authority\nVoronoi polygons for mean of pubs positions")
plt.show()
我们使用沃罗诺伊多边形来可视化酒吧的地理分布。在图 4.20中,我们用不同的颜色显示每个多边形。因为沃罗诺伊多边形内部点比任何其他相邻多边形中心更靠近多边形中心,所以每个多边形大约覆盖了位于多边形中心的酒吧所覆盖的区域。在图 4.21中,我们使用沃罗诺伊多边形围绕每个地方当局内酒吧分布的几何中心构建。然后我们使用颜色渐变来表示每个地方当局的酒吧相对密度。通过使用这些原始的视觉化技术,我们能够更直观地表示酒吧的空间分布。
图 4.21:与地方当局区域酒吧密度成比例的颜色强度的沃罗诺伊多边形
在接下来的章节中,我们将继续研究这些数据,当我们把酒吧数据集的数据与星巴克数据集的数据混合时。我们打算结合两个数据集的信息,使用沃罗诺伊多边形区域来评估伦敦地区酒吧和星巴克之间的相对距离。
通过操作为酒吧和星巴克咖啡店生成的沃罗诺伊多边形,我们将分析酒吧和星巴克之间的相对空间分布,生成地图,例如,我们可以看到一组离星巴克最近的酒吧。沃罗诺伊多边形的几何属性将证明在这样做时极为有用。
考虑到这一点,让我们继续前进,探索星巴克数据集。
全球星巴克
我们从笔记本星巴克全球位置 - 数据探索(见参考文献 6)开始对星巴克全球位置数据集进行详细的探索性数据分析(EDA)。(请参阅当前节中的文本)。你可能希望与当前节中的文本并行跟进笔记本。在此数据集中使用的工具是从data_quality_stats
和plot_style_utils
实用脚本中导入的。在开始我们的分析之前,重要的是要解释一下,用于此分析的数据集来自 Kaggle,并且是在 6 年前收集的。
初步数据分析
数据集有 25,600 行。一些字段只有少数缺失值。纬度和经度各缺失 1 个值,而街道地址缺失 2 个值,城市缺失 15 个值。缺失数据最多的字段是邮编(5.9%)和电话号码(26.8%)。在图 4.22中,我们可以看到数据的样本:
图 4.22:星巴克全球位置数据集的前几行
通过查看最频繁的值报告,我们可以了解一些有趣的事情:
图 4.23:全球星巴克位置数据集中最频繁的值
如预期的那样,拥有最多星巴克咖啡店的州是 CA(美国)。就城市而言,最多的店铺位于上海。有一个独特的地址,最多有 11 家店铺。此外,大多数店铺按时区划分都位于纽约时区。
单变量和双变量数据分析
对于这个数据集,我选择了一种颜色图,将星巴克的颜色与绿色和棕色调混合,就像他们提供给客户的优质烘焙咖啡的颜色:
图 4.24:笔记本颜色图,将星巴克颜色与烘焙咖啡的色调混合
我们将使用前面的自定义颜色图进行单变量分析图。在下面的图中,我们展示了按国家代码划分的咖啡店分布。大多数星巴克位于美国,有超过 13,000 条记录,其次是中国、加拿大和日本:
图 4.25:按国家代码划分的咖啡店。美国最多,其次是中国、加拿大和日本
如果我们查看图 4.26中的按州/省分布,我们可以看到第一名是加利福尼亚州,有超过 25,000 家。第二名是德克萨斯州,有超过 1,000 家咖啡店,第三名是英格兰,少于 1,000 家。按时区分布显示,最代表性的是美国东海岸时区(纽约时区)。
图 4.26:按州/省代码划分的咖啡店。加利福尼亚州(CA)拥有最多的咖啡店,其次是德克萨斯州(TX)
此外,大多数咖啡店位于纽约(美国东海岸)时区:
图 4.27:按时区代码划分的咖啡店,大多数位于纽约(美国东海岸)时区
接下来,星巴克咖啡店的拥有情况在图 4.28中展示。我们可以观察到,大多数咖啡店是公司拥有的(12,000 家),其次是特许经营(超过 9,000 家),合资企业(4,000 家),以及特许经营店(少于 1,000 家):
图 4.28:咖啡店所有权类型
接下来,我们将看到所有权类型如何根据国家而变化。让我们用国家来表示公司所有权。下面的图显示了前 10 个国家的咖啡店数量。由于数据偏斜(概率分布的不对称性度量),我们使用对数尺度。换句话说,在少数几个国家中,有许多咖啡店,而在其他国家中,咖啡店的数量要少得多。美国有两种所有权类型:公司拥有和特许经营。中国主要是合资企业和公司拥有,特许经营的数量较少。在日本,大多数店铺是合资企业,特许经营和公司拥有的数量几乎相等。
图 4.29:按所有权类型分组的各国咖啡店数量
在以下图中,我们展示了按所有权类型分组的每个城市的咖啡店数量。因为城市的名称以多种形式书写(使用小写和大写的本土字符),我首先统一了表示法(并将所有内容与英文名称对齐)。前几个城市是上海、首尔和北京。上海和首尔有合资咖啡店,而北京只有公司拥有的星巴克咖啡店。
图 4.30:按所有权类型分组的城市咖啡店数量
我们对星巴克咖啡店数据集进行了单变量和双变量分析。现在,我们对特征分布和相互作用有了很好的理解。接下来,让我们进行另一项地理空间分析,使用并扩展我们之前在英格兰酒吧分析中测试过的工具。
地理空间分析
我们首先观察星巴克在全球的分布。我们使用 folium 库和 MarkerCluster 在动态地图上表示整个世界咖啡店的空间分布。代码如下所示:
coffee_df = coffee_df.loc[(~coffee_df.Latitude.isna()) & (~coffee_df.Longitude.isna())]
locations_data = np.array(coffee_df[["Latitude", "Longitude"]])
popups = coffee_df.apply(lambda row: f"Name: {row['Store Name']}", axis=1)
marker_cluster = MarkerCluster(
locations = locations_data,
)
world_coords = [0., 0.]
world_map = folium.Map(location = world_coords, zoom_start = 1)
marker_cluster.add_to(world_map)
world_map
Folium/leaflet 地图可浏览。我们可以平移、放大和缩小。在图 4.31中,我们展示了全球咖啡店的分布:
图 4.31:使用 folium 在 leaflets 上展示的全球星巴克咖啡店分布
在图 4.32中,我们展示了北美大陆美国和加拿大地区的放大视图。显然,东海岸和西海岸在美国星巴克咖啡店分布中占主导地位。
图 4.32:美国星巴克咖啡店分布
另一种表示星巴克咖啡店空间分布的方法是使用geopandas
绘图功能。首先,我们将展示每个国家的店铺数量。为此,我们将按国家汇总咖啡店:
coffee_agg_df = coffee_df.groupby(["Country"])["Brand"].count().reset_index()
coffee_agg_df.columns = ["Country", "Shops"]
要使用geopandas
表示地理空间分布,我们需要使用ISO3
国家代码(三位字母的国家代码)。在星巴克分布数据集中,我们只有ISO2
(两位字母的国家代码)。我们可以包含一个包含等效性的数据集,或者我们可以导入一个 Python 包,它会为我们进行转换。我们将选择第二种解决方案并使用pip install
,然后导入country-conversion
Python 包:
import geopandas as gpd
import matplotlib
import country_converter as cc
# convert ISO2 to ISO3 country codes - to be used with geopandas plot of countries
coffee_agg_df["iso_a3"] = coffee_agg_df["Country"].apply(lambda x: cc.convert(x, to='ISO3'))
然后,使用geopandas
,我们加载了一个包含所有国家多边形形状的数据集,分辨率为低。然后我们将两个数据集(包含店铺和多边形)合并:
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
world_shop = world.merge(coffee_agg_df, on="iso_a3", how="right")
在显示国家多边形之前,我们将填充颜色调整为按比例表示当前国家星巴克咖啡店的数量,我们将显示一个带有所有国家的线框图,这样我们也可以在地图上看到没有星巴克的国家:
world_shop.loc[world_shop.Shops.isna(), "Shops"] = 0
f, ax = plt.subplots(1, 1, figsize=(12, 5))
world_cp = world.copy()
使用geopandas
,我们可以应用对数色标,这有助于更有效地表示具有偏斜分布的各国星巴克咖啡店的总数。它确保了颜色方案的均匀分布,使我们能够区分拥有较少咖啡店的国家和在这方面处于顶端的国家。我们还绘制了一些纬度线:
# transform, in the copied data, the projection in Cylindrical equal-area,
# which preserves the areas
world_cp= world_cp.to_crs({'proj':'cea'})
world_cp["area"] = world_cp['geometry'].area / 10**6 # km²
world["area"] = world_cp["area"]='black', linewidth=0.25, ax=ax)
# draw countries polygons with log scale colormap
world_shop.plot(column='Shops', legend=True,\
norm=matplotlib.colors.LogNorm(vmin=world_shop.Shops.min(),\
vmax=world_shop.Shops.max()),
cmap="rainbow",
ax=ax)
plt.grid(color="black", linestyle=":", linewidth=0.1, axis="y", which="major")
plt.xlabel("Longitude"); plt.ylabel("Latitude")
plt.title("Starbucks coffee shops distribution at country level")
plt.show()
这张地图很有信息量,但这些国家的面积、人口和人口密度差异很大。为了更好地理解星巴克咖啡店的密度,我们还将绘制每百万公民拥有的店铺数量(针对每个国家)以及每 1,000 平方公里的店铺数量。
![图片 B20963_04_33.png]
图 4.33:geopandas 地图显示世界级别的咖啡店密度(对数刻度)
对于前面的地图,我们选择geopandas
正是因为它允许我们在对数尺度上用颜色强度表示区域。
在world
数据集中,我们有人口估计数据,但没有国家面积信息。为了计算每平方公里的星巴克密度,我们需要包括面积。我们可以包含一个新的数据集,包含国家面积,或者我们可以使用geopandas
的功能从多边形中获取面积。由于当前使用的墨卡托投影是为了以可读的方式显示地图,面积计算并不正确。
我们将复制world
数据集,以确保在墨卡托投影中变换时不会扭曲多边形。然后,我们将使用Cylindrical equal-area
投影在副本上应用变换。这种投影保留了面积,这正是我们计算所需的。变换完成后,我们将面积连接到world
数据集:
world_cp = world.copy()
# transform, in the copied data, the projection in Cylindrical equal-area,
# which preserves the areas
world_cp= world_cp.to_crs({'proj':'cea'})
world_cp["area"] = world_cp['geometry'].area / 10**6 # km²
world["area"] = world_cp["area"]
让我们来验证一下我们计算面积是否正确。我们选取了几个国家,并验证这些国家的面积是否与官方记录相符:
world.loc[world.iso_a3.isin(["GBR", "USA", "ROU"])]
图 4.34:美国、罗马尼亚和英国的面积验证
如您所见,对于所有国家,使用的方法计算出的面积产生了与官方记录相符的正确表面。
现在,我们已经拥有了准备和显示按国家、面积和人口相对密度显示星巴克密度的所有必要信息。计算星巴克密度的代码如下:
world_shop = world.merge(coffee_agg_df, on="iso_a3", how="right")
world_shop["Shops / Population"] = world_shop["Shops"] / world_shop["pop_est"] * 10**6 # shops/1 million population
world_shop["Shops / Area"] = world_shop["Shops"] / world_shop["area"] * 10**3\. # shops / 1000 Km²
然后,使用以下代码,我们在国家层面上绘制每百万人口拥有的星巴克分布:
f, ax = plt.subplots(1, 1, figsize=(12, 5))
# show all countries contour with black and color while
world.plot(column=None, color="white", edgecolor='black', linewidth=0.25, ax=ax)
# draw countries polygons
world_shop.plot(column='Shops / Population', legend=True,\
cmap="rainbow",
ax=ax)
plt.grid(color="black", linestyle=":", linewidth=0.1, axis="y", which="major")
plt.xlabel("Longitude"); plt.ylabel("Latitude")
plt.title("Starbucks coffee shops / 1 million population - distribution at country level")
plt.show()
我们在图 4.35中展示了使用前面代码绘制的图表。每百万人口星巴克数量最多的国家是美国、加拿大和阿联酋,其次是台湾、韩国、英国和日本。
图 4.35:每百万人的星巴克数量 – 每个国家分布
对于每个国家,每 1,000 平方公里的星巴克咖啡店数量在以下图表中显示:
图 4.36:每 1,000 平方公里的星巴克数量 – 每个国家分布
我们可以看到,咖啡店最高集中度在韩国、台湾、日本和英国等国家。
让我们快速总结一下本节内容。我们分析了包含英国酒吧和全球星巴克数据的两个数据集,以了解这两个数据集中的数据分布情况。我们还介绍了用于地理空间数据处理和分析的几种技术和工具。我们学习了如何绘制 shapefile 数据,如何从 shapefile 中提取多边形,如何使用另一组多边形剪切多边形集,以及如何生成 Voronoi 多边形。这些都是为本章分析的主要部分所做的准备,我们将结合两个数据集,学习如何生成多图层地图,其中两个数据集的信息被创造性地结合。我们的目标是双重的:向您介绍分析地理空间数据的高级方法,并创造性地使用介绍的方法,看看我们如何从结合后的数据源中获得洞察。
伦敦的酒吧和星巴克
到目前为止,我们的分析主要集中在单独的“英格兰每家酒吧”和“全球星巴克位置”数据集上。为了支持与这两个单独数据集相关的某些数据分析任务,我们还添加了两个额外的数据集,一个是邮政编码的地理位置数据,用于替换缺失的经纬度数据,另一个是英国的 shapefile 数据,用于剪切从酒吧位置生成的 Voronoi 多边形,使它们与岛屿的陆地轮廓对齐。
在本节中,我们将结合分别分析的两个主要数据源的信息,并应用在本初步分析期间开发的方法,支持我们的研究目标。这将侧重于一个较小的区域,在伦敦,我们既有酒吧的高密度,也有星巴克咖啡店的高度集中。我们可以假设星巴克的地理空间集中度小于酒吧的集中度。
我们希望看到最近的星巴克在哪里,这样我们就可以在喝了几品脱啤酒后用咖啡清醒一下。我们已经了解到 Voronoi 多边形有一个有趣的特性——多边形内的任何点都离其中心比离任何相邻中心更近。我们将代表伦敦地区的酒吧位置,叠加在同一地区星巴克位置生成的 Voronoi 多边形上。
与本节相关的笔记本是Coffee or Beer in London – Your Choice!
,(见参考文献 11)。您可能会发现跟随本节中的文本一起查看笔记本很有用。
数据准备
我们首先从两个数据集Every Pub in England
和Starbucks Locations Worldwide
中读取 CSV 文件。我们还现在从GDM Data for the UK
读取GBR_adm2.shp
形状文件(包含大不列颠地方当局边界数据)以及Open Postcode Geo
的数据。在这个最后文件中,我们只过滤四个列(邮编、国家、纬度和经度)。
从酒吧数据中,我们只选择那些地方当局为伦敦 32 个自治市之一的数据条目。我们将伦敦市添加到这个子集中,因为伦敦市不是自治市之一。伦敦市位于伦敦市中心,一些酒吧就位于那里,我们希望将其包括在内。我们使用相同的列表来过滤 shapefile 数据中的数据。为了检查我们是否正确选择了所有 shapefile 数据,我们显示了自治市(以及伦敦市)的多边形:
boroughs_df = counties_df.loc[counties_df.NAME_2.isin(london_boroughs)]
boroughs_df.plot(color=color_list[0], edgecolor=color_list[4])
plt.show()
在以下图中,观察发现伦敦市缺失(左)。我们在形状文件名称中有伦敦,所以我们将只将形状文件数据中的伦敦替换为伦敦市。在更正后(右),我们可以看到通过统一伦敦市的表示法,我们现在在我们的地图上正确地表示了所有地方当局。现在,我们已经选择了我们想要包括在我们对伦敦地区酒吧和星巴克咖啡店的分析中的所有区域。
图 4.37:伦敦自治市(左)和伦敦自治市及伦敦市(右)
我们还选择了相同子区域的星巴克咖啡店数据。对于星巴克数据,选择在以下代码中显示:
coffee_df = coffee_df.loc[(coffee_df.City.isin(london_boroughs + ["London"])) &\
(coffee_df.Country=="GB")]
我们将国家信息纳入过滤标准,因为伦敦和许多其他伦敦自治市的名称在北美被发现,许多城市从大不列颠借用名称。
根据我们对酒吧数据的先前分析,我们知道一些酒吧缺少经纬度,标记为\N。对这些酒吧行执行相同的转换,包括与Open Postcode Geo
数据合并和清理,如前一小节所述。这个过程将涉及根据邮编匹配分配经纬度数据。
然后,使用以下代码,我们检查使用先前标准选择的酒吧和星巴克是否都在伦敦自治市区的边界内(或非常接近这些边界):
def verify_data_availability():
f, ax = plt.subplots(1, 1, figsize=(10, 10))
boroughs_df.plot(color="white", edgecolor=color_list[4], ax=ax)
plt.scatter(x=pub_df["longitude"],y=pub_df["latitude"], color=color_list[0], marker="+", label="Pubs")
plt.scatter(x=coffee_df["Longitude"],y=coffee_df["Latitude"], color=color_list[5], marker="o", label="Starbucks")
plt.xlabel("Longitude"); plt.ylabel("Latitude"); plt.title("London boroughs - verify data availability")
plt.grid(color="black", linestyle=":", linewidth=0.1, axis="both", which="major")
plt.legend()
plt.show()
我们观察到有两个星巴克与伦敦相当遥远。我们对选定的星巴克设置了一个额外的条件:
coffee_df = coffee_df.loc[coffee_df.Latitude<=51.7]
在生成的图中,您将看到伦敦自治市区和伦敦市内的酒吧(交叉点)和星巴克(点),在过滤掉这些地方当局区域外的项目并纠正错误归属后。
图 4.38:过滤项目后的伦敦自治市区和伦敦市内的酒吧(交叉点)和星巴克(点)
仍然有一些点在边界之外,但到目前为止,我们应该没问题。一旦我们使用地方当局多边形来裁剪每个酒吧和咖啡店相关的 Voronoi 多边形,这些点将被过滤掉。我们观察到有关星巴克对齐的一个奇怪现象。所有星巴克商店似乎都是水平对齐的。这是因为星巴克的定位只给出了两位小数(星巴克咖啡店来自一个全球地理定位数据集,其中位置以较小的精度给出),而酒吧给出了六位小数。因此,星巴克商店看起来是对齐的。它们的定位被四舍五入到两位小数,由于咖啡店位置接近,它们看起来是对齐的,尤其是在纬度线上。
地理空间分析
现在,让我们用伦敦及其自治市区的酒吧和星巴克商店的 Voronoi 多边形来表示。我们首先使用之前用于我们在《英格兰每家酒吧》数据分析中使用的相同代码来生成这些多边形。首先,让我们处理该区域的酒吧。由于我们现在使用了geospatial_utils
实用脚本,笔记本中的代码现在更加紧凑。以下代码生成包含 Voronoi 多边形集合的对象,然后可视化该集合:
pub_voronoi = get_voronoi_polygons(pub_df)
plot_voronoi_polygons(pub_voronoi,
title="Voronoi polygons from pubs locations in London",
lat_limits=[51.2, 51.7],
long_limits=[-0.5, 0.3])
为了做到这一点,前面的代码使用了在geospatial_utils
中定义的两个函数。
第一个函数get_voronoi_polygons
从一个点列表中创建一个 Voronoi 多边形列表,其中x和y坐标分别代表经度和纬度。为此,它使用scipy.spatial
库中的 Voronoi 函数:
def get_voronoi_polygons(data_df, latitude="latitude", longitude="longitude"):
"""
Create a list of Voronoi polygons from a list of points
Args
data_df: dataframe containing lat/long
latitude: latitude feature
longitude: longitude feature
Returns
Voronoi polygons graph (points, polygons) from the seed points in data_df
(a scipy.spatial.Voronoi object)
"""
locations_data = np.array(data_df[[latitude, longitude]].astype(float))
data_voronoi = [[x[1], x[0]] for x in locations_data]
voronoi_polygons = Voronoi(data_voronoi)
print(f"Voronoi polygons: {len(voronoi_polygons.points)}")
return voronoi_polygons
第二个函数plot_voronoi_polygons
绘制一个spacy.spatial.Voronoi
对象,这是一个 Voronoi 多边形的集合:
def plot_voronoi_polygons(voronoi_polygons, title, lat_limits, long_limits):
"""
Plot Voronoi polygons (visualization tool)
Args
voronoi_polygons: Voronoi polygons object (a scipy.spatial.Voronoi object)
title: graph title
lat_limits: graph latitude (y) limits
long_limits: graph longitude (x) limits
Returns
None
"""
# do not show the vertices, only show edges and centers
fig = voronoi_plot_2d(voronoi_polygons,
show_vertices=False)
plt.xlim(long_limits)
plt.ylim(lat_limits)
plt.title(title)
plt.show()
生成多边形集合首先被提取为前一个部分中已定义的extract_voronoi_polygon_list
函数的列表,然后使用伦敦自治市区的外部边界进行裁剪,该边界是通过溶解borroughs_df
GeoDataFrame 获得的:
boroughs_dissolved = boroughs_df.dissolve()
voronoi_polys_clipped = clip_polygons(voronoi_poly_list, boroughs_df)
clip_polygons
函数的代码在geospatial_utils
实用脚本中定义。在clip_polygons
函数中,我们使用一个多边形列表poly_clipping
来裁剪另一个列表poly_list_origin
中的多边形。我们将原始多边形列表poly_list_origin
转换为一个geopandas
DataFrame。我们使用 geopandas 的clip
函数执行裁剪操作。裁剪后的多边形列表polygons_clipped
由clip_polygons
函数返回:
def clip_polygons(poly_list_origin, poly_clipping):
"""
Clip a list of polygons using an external polygon
Args:
poly_list_origin: list of polygons to clip
poly_clipping: polygon used to clip the original list
Returns:
The original list of polygons, with the polygons clipped using the clipping polygon
"""
#convert the initial polygons list to a geodataframe
polygons_gdf = gpd.GeoDataFrame(poly_list_origin, columns = ['geometry'], crs=poly_clipping.crs)
start_time = time.time()
polygons_clipped = gpd.clip(polygons_gdf, poly_clipping)
end_time = time.time()
print(f"Total time: {round(end_time - start_time, 4)} sec.")
return polygons_clipped
下图显示了伦敦酒吧位置的 Voronoi 多边形(左)和地区边界(右):
图 4.39:伦敦地区和伦敦市(左)的酒吧 Voronoi 多边形以及伦敦地区边界(右)。我们使用边界多边形来裁剪 Voronoi 多边形
下图显示了地区的边界和酒吧的位置,以及与这些位置相关的 Voronoi 多边形。我们可以观察到,酒吧密度最高的区域位于伦敦市及其西部的邻近地区,除了塔桥区,该区只有一家酒吧。
图 4.40:伦敦地区和伦敦市(裁剪后)的酒吧 Voronoi 多边形,显示酒吧位置和地区边界
接下来,我们对星巴克咖啡店的位置执行相同的操作。我们生成 Voronoi 多边形,并使用通过溶解所有地区多边形获得的相同的伦敦地区边界多边形进行裁剪。下图显示了地区的边界和星巴克商店的位置,以及与这些位置相关的 Voronoi 多边形:
图 4.41:伦敦地区和伦敦市(裁剪后)的星巴克 Voronoi 多边形,显示商店的位置和地区边界
生成 Voronoi 多边形对象、可视化它、从中提取多边形列表以及然后裁剪的代码如下。首先,让我们看看生成 Voronoi 多边形的代码:
coffee_voronoi = get_voronoi_polygons(coffee_df, latitude="Latitude", longitude="Longitude")
plot_voronoi_polygons(coffee_voronoi,
title="Voronoi polygons from Starbucks locations in London",
lat_limits=[51.2, 51.7],
long_limits=[-0.5, 0.3])
接下来是提取 Voronoi 多边形对象中的多边形列表以及使用地区边界裁剪多边形的代码:
coffee_voronoi_poly_list = extract_voronoi_polygon_list(coffee_voronoi)
coffee_voronoi_polys_clipped = clip_polygons(coffee_voronoi_poly_list, boroughs_df)
使用within_polygon
函数,我们可以识别位于多边形内的位置。该函数在geospatial_utils
模块中实现。该函数使用来自shapely.geometry
库模块的Point
对象的within
属性。我们对给定多边形的所有点(在我们的案例中,是酒吧)的经纬度创建的点执行操作,获取点相对于参考多边形的状态(within
,outside
):
def within_polygon(data_original_df, polygon, latitude="latitude", longitude="longitude"):
"""
Args
data_original_df: dataframe with latitude / longitude
polygon: polygon (Polygon object)
latitude: feature name for latitude n data_original_df
longitude: feature name for longitude in data_original_df
Returns
coordinates of points inside polygon
coordinates of points outside polygon
polygon transformed into a geopandas dataframe
"""
data_df = data_original_df.copy()
data_df["in_poly"] = data_df.apply(lambda x: Point(x[longitude], x[latitude]).within(polygon), axis=1)
data_in_df = data_df[[longitude, latitude]].loc[data_df["in_poly"]==True]
data_out_df = data_df[[longitude, latitude]].loc[data_df["in_poly"]==False]
data_in_df.columns = ["long", "lat"]
data_out_df.columns = ["long", "lat"]
sel_polygon_gdf = gpd.GeoDataFrame([polygon], columns = ['geometry'])
return data_in_df, data_out_df, sel_polygon_gdf
以下代码应用了within_polygon
函数:
data_in_df, data_out_df, sel_polygon_gdf = within_polygon(pub_df, coffee_voronoi_poly_list[6])
在以下图中,所选区域内的酒吧(在笔记本中与书籍相关联,使用浅棕色和深绿色填充色显示)比任何其他相邻的星巴克咖啡店更靠近所选区域的星巴克咖啡店位置。其余酒吧以浅绿色显示。我们可以对所有的多边形(以及自治市的区域)重复此过程。
图 4.42:星巴克 Voronoi 多边形区域内外酒吧
我们也可以使用 folium 地图来表示相同的物品,酒吧和星巴克咖啡店。这些地图将允许交互,包括放大、缩小和平移。我们可以在基础图上添加多个图层。让我们首先将伦敦自治市表示为地图的第一层。在其上方,我们将显示伦敦区域的酒吧。每个酒吧也将有一个弹出窗口,显示酒吧名称和地址。我们可以从多个地图瓦片提供商中选择。
由于我更喜欢清晰的背景,我选择了两个瓦片来源:“Stamen toner”和“CartoDB positron”。对于这两种选项,瓦片都是黑白或浅色,因此重叠层可以更容易地看到。以下是在伦敦区域显示瓦片(使用“Stamen toner”)、伦敦自治市轮廓(地图的第一层)以及每个酒吧位置(在地图上的第二层)的代码。每个酒吧都将有一个弹出窗口,显示酒吧名称和地址:
# map with zoom on London area
m = folium.Map(location=[51.5, 0], zoom_start=10, tiles="Stamen Toner")
# London boroughs geo jsons
for _, r in boroughs_df.iterrows():
simplified_geo = gpd.GeoSeries(r['geometry']).simplify(tolerance=0.001)
geo_json = simplified_geo.to_json()
geo_json = folium.GeoJson(data=geo_json,
style_function=lambda x: {'fillColor': color_list[1],
'color': color_list[2],
'weight': 1})
geo_json.add_to(m)
# pubs as CircleMarkers with popup with Name & Address info
for _, r in pub_df.iterrows():
folium.CircleMarker(location=[r['latitude'], r['longitude']],
fill=True,
color=color_list[4],
fill_color=color_list[5],
weight=0.5,
radius=4,
popup="<strong>Name</strong>: <font color='red'>{}</font> <br> <strong>Address</strong>: {}".format(r['name'], r['address'])).add_to(m)
# display map
m
以下图显示了使用前面代码创建的地图。在此地图上,我们显示以下信息叠加层:
-
伦敦自治市和伦敦市地图区域使用“Stamen Toner”瓦片
-
伦敦自治市和伦敦市边界
-
前述区域中的酒吧,使用
CircleMarker
显示 -
可选地,对于每个酒吧,如果选中,一个弹出窗口显示酒吧名称和地址
图 4.43:带有伦敦自治市边界和伦敦区域酒吧位置的 Leaflet 地图
在笔记本中,我展示了更多带有星巴克 Voronoi 多边形和位置的图片,以及具有多层层多边形和标记的地图。
另一个我们可以执行的有用操作是计算多边形的面积。用于计算 GeoDataFrame 中所有多边形面积的函数是get_polygons_area
,它也在geospatial_utils
中定义。它在一个 GeoDataFrame 的副本上应用了cylindrical equal area
投影的转换。这个投影将保留面积。然后我们向原始 GeoDataFrame 添加area
列:
def get_polygons_area(data_gdf):
"""
Add a column with polygons area to a GeoDataFrame
A Cylindrical equal area projection is used to calculate
polygons area
Args
data_gdf: a GeoDataFrame
Returns
the original data_gdf with an `area` column added
"""
# copy the data, to not affect initial data projection
data_cp = data_gdf.copy()
# transform, in the copied data, the projection in Cylindrical equal-area,
# which preserves the areas
data_cp = data_cp.to_crs({'proj':'cea'})
data_cp["area"] = data_cp['geometry'].area / 10**6 # km²
data_gdf["area"] = data_cp["area"]
# returns the initial data, with added area columns
return data_gdf
我们计算自治市的面积,然后计算每个自治市酒吧的数量。然后,我们将酒吧/自治市数量除以自治市面积,以获得酒吧密度(每平方公里酒吧数):
boroughs_df = get_polygons_area(boroughs_df)
agg_pub_df = pub_df.groupby("local_authority")["name"].count().reset_index()
agg_pub_df.columns = ["NAME_2", "pubs"]
boroughs_df = boroughs_df.merge(agg_pub_df)
我们现在需要用一个连续的颜色尺度来表示密度,但我们希望使用自定义颜色映射中的颜色。我们可以创建自己的连续颜色映射,并使用颜色列表中的几个颜色作为种子:
vmin = boroughs_df.pubs.min()
vmax = boroughs_df.pubs.max()
norm=plt.Normalize(vmin, vmax)
custom_cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["white", color_list[0], color_list[2]])
对于酒吧密度图,我们希望使用这个自定义颜色映射,并采用对数刻度。我们可以通过以下代码实现:
fig, ax = plt.subplots(1, 1, figsize = (10, 5))
ax.set_facecolor("white")
boroughs_df.plot(ax = ax, column="pubs per sq.km",
norm=matplotlib.colors.LogNorm(vmin=boroughs_df["pubs per sq.km"].min(),\
vmax=boroughs_df["pubs per sq.km"].max()),
cmap = custom_cmap, edgecolor = color_list[3],
linewidth = 1, legend=True),
plt.xlabel("Longitude"); plt.ylabel("Latitude");
plt.title("Pubs density (pubs / sq.km) in London")
plt.show()
下图显示了每个地区的酒吧数量(左侧)和每个地区的酒吧密度(右侧):
图 4.44:伦敦每个地区的酒吧数量(左侧)和对数刻度下的酒吧密度(右侧)
在与本节相关的笔记本中,“伦敦的咖啡或啤酒——你的选择!”(参见参考文献 11),我还展示了每个 Starbucks Voronoi 多边形区域的酒吧数量和酒吧密度。本节中展示的各种技术可能已经为您提供了分析和可视化地理空间数据的起始工具集。
摘要
在本章中,我们学习了如何处理地理信息和地图,如何操作几何数据(裁剪和合并多边形数据,聚类数据以生成细节较少的地图,以及从地理空间数据中移除子集),并在地图上叠加多个数据层。我们还学习了如何使用 geopandas
和自定义代码修改和提取 shapefile 中的信息,以及创建或计算地理空间特征,如地形面积或地理空间对象密度。此外,我们提取了可重用的函数并将它们分组到两个实用脚本中,这是 Kaggle 术语中的独立 Python 模块。这些实用脚本可以像任何其他库一样导入,并与您的笔记本代码集成。
在下一章中,我们将尝试使用一些地理空间分析的工具和技术,用于数据分析竞赛。
参考文献
-
英格兰每家酒吧,Kaggle 数据集:
www.kaggle.com/datasets/rtatman/every-pub-in-england
-
英格兰每家酒吧的数据探索,Kaggle 笔记本:
github.com/PacktPublishing/Developing-Kaggle-Notebooks/blob/develop/Chapter-04/every-pub-in-england-data-exploration.ipynb
-
星巴克全球位置,Kaggle 数据集:
www.kaggle.com/datasets/starbucks/store-locations
-
Open Postcode Geo,Kaggle 数据集:
www.kaggle.com/datasets/danwinchester/open-postcode-geo
-
英国 GADM 数据,Kaggle 数据集:
www.kaggle.com/datasets/gpreda/gadm-data-for-uk
-
星巴克全球门店 – 数据探索,Kaggle 笔记本:
github.com/PacktPublishing/Developing-Kaggle-Notebooks/blob/develop/Chapter-04/starbucks-location-worldwide-data-exploration.ipynb
-
Leaflet 地图中的多边形叠加:
stackoverflow.com/questions/59303421/polygon-overlay-in-leaflet-map
-
Geopandas 区域:
geopandas.org/en/stable/docs/reference/api/geopandas.GeoSeries.area.html
-
Scipy 空间 Voronoi – 提取 Voronoi 多边形并展示它们:
docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.Voronoi.html
-
使用 GeoPandas 获取多边形面积:
gis.stackexchange.com/questions/218450/getting-polygon-areas-using-geopandas
-
在伦敦喝咖啡还是啤酒 – 你的选择!,Kaggle 笔记本:
github.com/PacktPublishing/Developing-Kaggle-Notebooks/blob/develop/Chapter-04/coffee-or-beer-in-london-your-choice.ipynb
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人相聚,并在以下地点与超过 5000 名成员一起学习:
第五章:让发展中国家重新回到工作岗位并优化微型贷款
接近一个新的数据集类似于考古挖掘,有时也像警察调查。我们继续挖掘数据堆下面隐藏的洞察力,或者我们尝试使用系统的方法,有时是枯燥的过程,来揭示难以捉摸的证据,这分别类似于考古学家的技术学科或侦探的方法。所有数据都能讲述一个故事。这是分析师的选择,这个故事是以科学报告的风格讲述,还是以侦探小说生动、吸引人的形式讲述。
在本章中,我们将结合我们在前几章中开发的技巧来分析表格(数值和分类)、文本和地理空间数据。这涉及到结合来自多个来源的数据,并展示如何用这些数据讲述故事。我们将继续分析来自早期 Kaggle 竞赛的数据,数据科学为善:Kiva 筹款(见参考文献 1)。您将学习如何以既信息丰富又吸引读者的方式使用数据讲故事。然后,我们将对另一个竞赛数据集中的一个假设进行详细分析,该假设特别关注 Kaggle 元数据,Meta Kaggle(见参考文献 2)。
总结来说,本章将涵盖以下主题:
-
对 数据科学为善:Kiva 筹款 分析竞赛的探索性数据分析
-
同样分析竞赛的解决方案,试图了解哪些因素导致贫困
-
对 Meta Kaggle 数据集的分析以验证(或否定)几年前 Kaggle 竞赛团队规模突然增加的感知
介绍 Kiva 分析竞赛
Kiva.org 是一个在线众筹平台,其使命是向世界各地的贫困和金融排斥人群提供金融服务的好处。这些人可以通过 Kiva 的服务借取小额资金。这些微型贷款由 Kiva 通过与金融机构的合作,在贷款接收者居住的国家提供。
在过去,Kiva 向其目标社区提供了超过 10 亿美元的微型贷款。为了扩大其援助范围,同时提高对世界各地贫困人群的具体需求和影响因素的理解,Kiva 想要更好地了解每位潜在借款人的状况。由于世界各地问题的多样性、每个案例的特定性和众多影响因素,Kiva 识别出最需要其财务援助的案例的任务相当困难。
Kiva 向 Kaggle 的数据科学社区提出了一个分析竞赛。竞赛的范围是将每个贷款的特征与各种贫困数据集相结合,并确定潜在借款人的实际福利水平,按地区、性别和行业进行分类。在这个竞赛中,对参赛者贡献的评价基于分析的粒度,强调对当地具体情况的遵守、解释的清晰度和方法的原创性。竞赛组织者旨在直接使用最佳分析,因此强调了本地化和清晰的解释。
数据科学为善:Kiva 众筹分析竞赛要求参与者识别或收集相关数据,除了组织者提供的数据之外。组织者提供的数据包括贷款信息、按地区和位置提供的 Kiva 全球多维贫困指数(MPI)、贷款主题以及按地区划分的贷款主题。
贷款信息包括以下内容:
-
一个唯一的 ID
-
贷款主题 ID 和类型
-
当地金融机构合作伙伴 ID
-
资金金额(Kiva 提供给当地合作伙伴的金额)
-
贷款金额(当地合作伙伴向借款人发放的金额)
-
借款人的活动
-
贷款的用途(贷款将如何使用,或贷款目的)
-
行业、国家代码、国家名称、地区和货币
-
发布时间、发放时间、资金时间以及贷款发放的持续时间
-
为一笔贷款做出贡献的贷款人总数
-
借款人的性别
-
还款间隔
Kiva 按地区和位置提供的 MPI 信息包括以下内容:
-
地区或国家名称
-
该国的 ISO-3 代码
-
世界地区
-
当前地区的 MPI 值和地理位置(纬度和经度)
贷款主题包括:
-
一个唯一的 ID
-
贷款主题 ID
-
贷款主题类型
-
相应的合作伙伴 ID
贷款主题按地区数据集包含以下内容:
-
一个合作伙伴 ID
-
场地合作伙伴名称
-
行业
-
贷款主题 ID 和类型
-
国家和地区
-
地理编码(四种变体)
-
ISO-3 代码
-
金额
-
位置名称
-
MPI 地区和 MPI 地理编码(可能重复其他地理编码)
-
当前地区的农村百分比
在这个竞赛数据集中有大量的信息。我们不会对数据进行深入细致的分析,而是将目标集中在数据的一个方面。
分析竞赛的好解决方案是什么?
从一开始就强调,一个分析竞赛的好解决方案并不一定是完整的数据探索分析。根据我在几个分析竞赛中的经验和对顶尖解决方案的审查,我可以这样说,有时分析竞赛的评分标准恰恰相反。虽然标准可能会随着时间的推移而演变,但有些标准会被反复采用。例如,评估者经常优先考虑方法的原创性,而不是构成和记录。
要在这些标准上获得高分,作者必须充分准备自己。对数据的扩展探索仍然是必要的,以便所呈现的结果可以得到充分记录。虽然这种方法对研究目的很有用,但不需要完全包含在解决方案笔记本的叙述中。作者可以在他们的故事中选取并讨论数据的一部分,只要叙述是一致的,并且能够提出一个强烈、有说服力的案例。
作者必须从他们所探索的丰富数据中,仅选择那些将支持他们故事的部分。因此,构成同样重要,所选和解释的数据应提供有力的证据来支持叙述。如果叙述、结构和内容是原创的,故事的影响力将会更高。
数据越多,洞察力越强——分析 Kiva 数据竞赛
对于这次分析,我们更喜欢包括一个额外的数据源,Country Statistics – UNData
(见参考文献 3)。数据是由 Kiva 分析竞赛的一位参与者收集的,汇编了国家的基本统计指标。这个特定数据集基于联合国统计司(UNSD)的经济和社会事务部(DESA)。关键指标可以分为四个类别:一般信息、经济指标、社会指标和环境及基础设施指标。
Country Statistics – UNData
数据集中有两个 CSV 文件:
-
包含
Country Statistics – UNData
数据集中所有国家关键指标的 -
仅涵盖
Data Science for Good: Kiva Crowdfunding
数据集中存在的国家
Country Statistics – UNData
数据集中共有 50 列,从国家和地理区域、人口、人口密度、性别比、国内生产总值(GDP)、人均 GDP 和 GDP 增长率开始,接着是经济中的农业、工业、服务业的百分比、同一部门的就业情况、农业产量、贸易指标、城市人口和城市人口增长率。还包括诸如移动订阅百分比或女性在国家议会中的席位等信息。
一些有趣的因素包括使用改善后的饮用水的人口百分比、使用改善后的卫生设施的人口百分比、婴儿死亡率和生育率,以及预期寿命。UNData 中包含的许多这些特征与贫困的定义相关,我们将在我们的旅程中探讨它们。
我们分析的主要目标是理解如何衡量贫困,以便我们可以为组织者提供优化微贷分配所需的信息。
理解借款人人口统计
我们从关注谁获得贷款开始我们的探索,试图回答问题,“谁是借款人?”在 Kiva 数据集中,有 1,071,308 名女性借款人和 274,904 名男性借款人。女性不仅在总数上,而且在与贷款相关的借款人数上似乎都占主导地位。与贷款相关的女性人数是 50,而男性人数是 44。有多笔贷款既有女性又有男性借款人,也有只有男性或只有女性的贷款。
如您在以下图表中可以看到,大多数贷款与仅限女性的借款人相关,其次是仅限男性的借款人:
图 5.1:借款人性别
在以下图表中,让我们看看女性借款人(左)和男性借款人(右)的分布情况,从每个行业的平均女性和男性借款人数开始:
图 5.2:每笔贷款中女性/男性借款人的平均数量
如您所见,每笔贷款中女性借款人的平均数量分解为服装2.7,个人用途2.6,食品2.4,以及接近 2 的农业和建筑。对于男性借款人,每笔贷款的平均借款人数接近食品、农业、服装和个人用途的 1.75。
接下来,我们可以看到,女性借款人最多的类别是服装、食品和零售。对于男性借款人,个人用途和农业有最多的借款人:
图 5.3:每笔贷款中女性/男性借款人的最大数量
这些图表是使用 Plotly 构建的,Plotly 是一个强大且多功能的开源 Python 图形库。通过Kiva Microloans – 数据探索笔记本(见参考文献 4),我们将广泛使用 Plotly,用于低或高复杂度的图表。以下代码摘录用于构建图 5.3中的图表:
df = df.sort_values(by="max", ascending=False)
sectors_f = go.Bar(
x = df['sector'],
y = df['max'],
name="Female borrowers",
marker=dict(color=color_list[4]))
df2 = df2.sort_values(by="max", ascending=False)
sectors_m = go.Bar(
x = df2['sector'],
y = df2['max'],
name="Male borrowers",
marker=dict(color=color_list[3]))
fig = make_subplots(rows=1, cols=2, start_cell="top-left",
subplot_titles=("Loans with at least one female borrower",
"Loans with at least one male borrower"))
fig.add_trace(sectors_f, row=1, col=1)
fig.add_trace(sectors_m, row=1, col=2)
layout = go.Layout(height=400, width=900, title="Maximum number of female/male borowers/loan")
fig.update_layout(layout)
fig.update_layout(showlegend=False)
fig.show()
代码创建了两个并排的条形图,使用 Plotly 的make_subplots
函数。
借款人将贷款分多次偿还。还款方案多种多样,我们也可以在图 5.4中看到女性和男性还款分布的差异。仅女性借款组的贷款在每月和不规律还款间隔中几乎数量相同,只有一小部分贷款是一次性偿还(子弹)。仅男性借款组的还款主要是每月类型,并且子弹还款类型的比例远大于不规律。子弹类型很可能是周期性活动(如农业)的特征,当收入稀缺时,还款只能在收获时间(或收获付款时)进行。
图 5.4:借款人性别还款间隔
探索 MPI 与其他因素的关联
联合国开发计划署开发了多维贫困指数,或简称MPI(参见参考文献 5和6)。我们分析中使用的值是 2017 年的。当前可用的报告是 2022 年的。在当前报告中,联合国强调有 12 亿人是多维贫困的,其中 5.93 亿是 18 岁以下的儿童。在这 12 亿人中,有 5.79 亿生活在撒哈拉以南非洲,其次是南亚,有 3.85 亿(参见本章末尾的参考文献 3)。
MPI 是一个综合指数,包括三个维度、多个指标和贫困衡量标准。这三个维度是健康、教育和生活水平。与健康维度相关的指标是营养和儿童死亡率。教育与两个指标相关:学年和出勤率。这些在 MPI 值中各占 1/6 的权重。然后,有六个与生活水平维度相关的指标:烹饪燃料、卫生、饮用水、电力、住房和资产。这些在 MPI 值中各占 1/18 的权重(参见参考文献 3)。
MPI 的值介于 0 和 1 之间,较大的值意味着更高的贫困暴露度。联合国创建的这个多维因素的组成突出了贫困的复杂性。如 GDP 或人均 GDP 这样的指标并不能讲述整个故事。我们将尝试更好地理解贫困的衡量标准,以及 Kiva 和 UNData 数据集中的一些特征如何描绘它。
现在,让我们看看借款人居住的地方。为此,我们将使用 leaflet 地图表示 Kiva 地区的位置。为了构建这张地图,我们首先删除错误的纬度和经度元组(例如,纬度大于 90 或小于-90 的值)。然后,我们还将删除具有错误属性的数据。在地图上,我们还将使用与 MPI 成比例大小的标记表示每个地区,如前所述,MPI 是 Kiva 使用的贫困指数。在下一节中,我们将详细讨论这个指标。显示地图的代码如下:
# map with entire World
m = folium.Map(location=[0, 0], zoom_start=2, tiles="CartoDB Positron")
for _, r in region_df.iterrows():
folium.CircleMarker(location=[r['lat'], r['lon']],
fill=True,
color=color_list[3],
fill_color=color_list[3],
weight=0.9,
radius= 10 * (0.1 + r['MPI']),
popup=folium.Popup("<strong>Region</strong>: {}<br>\
<strong>Country</strong>: {}<br>\
<strong>Location Name</strong>: {}<br>\
<strong>World Region</strong>: {}<br>\
<strong>MPI</strong>: {}".format(r['region'], r['country'], r['LocationName'],\
r['world_region'], r['MPI']), min_width=100, max_width=300)).add_to(m)
m
在图 5.5中,我们展示了每个地区的 MPI 分布。地图中包含一些基于错误纬度/经度对分配的错误位置,这些错误将在我们代表国家或更大世界区域层面的汇总数据时得到纠正。
图 5.5:具有 MPI 贫困指数的世界地区。顶部为完整的世界地图,底部为撒哈拉以南非洲的放大图
让我们也看看国家和大陆级别的 MPI 分布。在当前章节相关的分析笔记本中,我们将查看 MPI 的最小值、最大值和平均值。在这里,我们只展示平均值,因为空间限制。
在图 5.6中,我们可以看到国家级别的 MPI 值。请注意,高 MPI 值最大的集中区域在撒哈拉以南国家,其次是东南亚。
图 5.6:具有平均 MPI 贫困指数的国家。MPI 平均值的最大值在撒哈拉以南非洲
类似地,图 5.7显示了世界区域级别的 MPI 值。再次,最大的平均 MPI 在撒哈拉以南非洲、南亚、东亚和太平洋,其次是阿拉伯国家。
图 5.7:具有平均 MPI 贫困指数的国家
让我们看看每个贷款行业的平均 MPI 是多少。在图 5.8中,我们可以看到每个行业的平均 MPI 值的分布。与最大平均 MPI 相关联的行业是农业,其次是个人用途、教育、零售和建筑:
图 5.8:每个贷款行业的平均 MPI
如您所见,农业行业的贷款平均 MPI 为0.2,其次是个人用途,约为0.18,以及教育为0.12。
我们分别研究了 MPI 与世界区域、行业和借款人性别之间的关系。现在让我们看看所有这些特征之间的关系。我们将查看这些特征类别中贷款数量和每笔贷款金额的分布。由于贷款金额使用各种货币,我们只以美元为单位查看。见图 5.9了解贷款数量:
图 5.9:世界各地区、各行业、借款人性别和还款间隔的贷款分布(即贷款数量)
图 5.9和图 5.10中的图表使用桑基图。在 Plotly 中生成这些桑基图的代码包含在实用脚本plotly_utils
中(有关书籍代码仓库的链接,请参阅参考文献 7)。由于代码过长,无法在此完整展示,所以我们只包含以下代码片段中的原型和参数定义:
def plotly_sankey(df,cat_cols=[],value_cols='',title='Sankey Diagram', color_palette=None, height=None):
"""
Plot a Sankey diagram
Args:
df: dataframe with data
cat_cals: grouped by features
valie_cols: feature grouped on
title: graph title
color_palette: list of colors
height: graph height
Returns:
figure with the Sankey diagram
"""
接下来,查看仅包含美元贷款金额的图 5.10:
图 5.10:世界各地区、各行业、借款人性别和还款间隔的贷款金额(美元)分布
我们还希望将几个数值特征与 MPI 相关联。我们从 Kiva 数据集中可用的信息开始。在图 5.11中,我们可以看到男性借款人数、女性借款人数、贷款金额、已资助金额、月数和 MPI 值之间的相关矩阵。请注意,我们不仅选择了在 MPI 计算中作为贫困因素的列,还选择了其他特征。
图 5.11:MPI、贷款数据特征和所选 UNData 列之间的相关矩阵
男性与女性的数量呈负相关(这是正常的,因为贷款会有男性或女性借款人,当一个类别的数量增加时,另一个类别将减少)。
资助金额与借款金额之间始终存在非常高的相关性。MPI 与 Kiva 包含的任何数值指标之间都存在非常小的负相关性(即无相关性,因为此值的绝对值小于 0.1)。我们需要查看 MPI 与 UNData 中的特征之间的相关性,因为我们确实有许多与贫困相关的指标。
为了准备前面的相关矩阵(如图 5.11 所示),我们将贷款数据与 MPI 数据和 UNData 合并:
kiva_mpi_un_df = loan_mpi_df.merge(kiva_country_profiles_variables_df)
kiva_loans_corr = kiva_mpi_un_df.loc[loan_mpi_df.currency=="USD"][sel_columns].corr()
fig, ax = plt.subplots(1, 1, figsize = (16, 16))
sns.heatmap(kiva_loans_corr,
xticklabels=kiva_loans_corr.columns.values,
yticklabels=kiva_loans_corr.columns.values,
cmap=cmap_custom, vmin=-1, vmax=1, annot=True, square=True,
annot_kws={"size":8})
plt.suptitle('Kiva Loans & MPI, UN Countries Features Correlation')
plt.show()
从查看图 5.11中,我们可以观察到 MPI 与婴儿死亡率有非常强的正相关,并且与生育率(0.78)相关。它还与人口、使用改善的卫生设施(0.45)、城市人口增长率(0.56)、经济中农业的百分比(0.54)或参与农业的人口百分比(0.35)呈正相关。这些都是当贫困更多时将会增加的因素的例子。有趣的是,城市人口增长率与 MPI 相关。这是因为人口流动并不总是由于更多的人为了就业而搬到城市,而是因为许多人由于农村地区资源匮乏而被流离失所。它还与卫生总支出(-0.2)、议会中女性的席位(-0.64)以及经济中工业(-0.21)和服务(-0.36)的百分比呈负相关。
工业发展和更多服务是发达经济的属性,为穷人提供更多机会;因此,随着这些因素(工业发展和服务)的增加,MPI 会降低。一个非常有趣的因素(强负相关)是女性在国家议会中的存在。这标志着更加发达、包容和富裕的社会,该社会准备赋权女性并给予她们更多机会。
贫困维度的雷达可视化
我们本节开始时探讨了借款人的信息。我们了解到,他们大多是女性,来自撒哈拉以南地区的贫困地区或南亚。然而,需求多样性很大,行业信息并没有揭示所有这些信息。只有通过查看与每个行业相关的详细活动,我们才能开始揭示每个借款人的具体需求,从而揭示导致贫困的特殊条件。
从 Kiva 数据和联合国的 MPI 定义中,我们了解到贫困是多种因素的组合。导致贫困的这种特定因素组合取决于每个地区,甚至每个具体案例。我们采用可视化工具来突出贫困的多维本质。我们将使用雷达图(也称为蜘蛛图)来展示几个国家与贫困相关的选定维度。在雷达图(或蜘蛛图——之所以这样命名是因为它类似于蜘蛛网)中,轴(径向)代表考虑中的个体特征。图形的面积反映了个体特征的累积效应的大小。对于小特征,我们得到小的面积。对于大特征,面积较大。在我们的情况下,我们希望通过特征或导致贫困的因素的数量来展示贫困(或 MPI 累积因素)。
以下代码片段使用 Plotly 为可视化准备数据,使用自定义构建的雷达图,按国家分组数据并计算每个国家的平均值:
region_df = kiva_mpi_region_locations_df.loc[~kiva_mpi_region_locations_df.MPI.isna()]
df = region_df.groupby(["country"])["MPI"].agg(["mean", "median"]).reset_index()
df.columns = ["country", "MPI_mean", "MPI_median"]
kiva_mpi_country_df = kiva_country_profiles_variables_df.merge(df)
df = kiva_mpi_country_df.sort_values(by="MPI_median", ascending = False)[0:10]
df['MPI_median'] = df['MPI_median'] * 100
df['MPI_mean'] = df['MPI_mean'] * 100
我们只选择了介于 1 到 100 之间的特征(我们还将一些特征,如 MPI,缩放到同一区间)。我们还计算了100 - 值,当值与 MPI 值呈负相关时。我们这样做是为了确保雷达图轴上表示的特征都与 MPI 呈正相关:
df['Infant mortality rate /1000 births'] = df['Infant mortality rate (per 1000 live births']
df["Employment: Agriculture %"] = df['Employment: Agriculture (% of employed)'].apply(lambda x: abs(x))
df["No improved sanitation facilit. %"] = df['Pop. using improved sanitation facilities (urban/rural, %)'].apply(lambda x: 100 - float(x))
df ['No improved drinking water % (U)'] = df['Pop. using improved drinking water (urban/rural, %)'].apply(lambda x: 100 - float(x.split("/")[0]))
df ['No improved drinking water % (R)'] = df['Pop. using improved drinking water (urban/rural, %)'].apply(lambda x: 100 - float(x.split("/")[1]))
然后,我们定义雷达图的特征:
radar_columns = ["No improved sanitation facilit. %",
"MPI_median", "MPI_mean",
'No improved drinking water % (U)',
'No improved drinking water % (R)',
'Infant mortality rate /1000 births',
"Employment: Agriculture %"]
使用以下代码创建雷达图:
fig = make_subplots(rows=1, shared_xaxes=True)
for _, row in df.iterrows():
r = []
for f in radar_columns:
r.append(row[f])
radar = go.Scatterpolar(r=r,
theta=radar_columns,
fill = 'toself',
opacity=0.7,
name = row['country'])
fig.add_trace(radar)
fig.update_layout(height=900, width=900,
title="Selcted poverty dimmensions in the 10 countries with highest median MPI rate",
polar=dict(
radialaxis=dict(visible=True, range=[0,100], gridcolor='black'),
bgcolor='white',
angularaxis=dict(visible=True, linecolor='black', gridcolor='black')
),
margin=go.layout.Margin(l=200,r=200,b=50, t=100)
)
fig.show()
结果图显示在图 5.12中。我们得到的是一个雷达图,其中包含 10 个最高中位(按国家计算)MPI 率国家的选定贫困维度。这些维度被选为与 MPI 呈正相关。更大的总面积意味着更高的实际贫困。
图 5.12:包含 10 个最高中位 MPI 率国家的选定贫困维度的雷达图
我们确定了与贫困相关的因素,使用雷达图来展示这些因素及其对维持贫困的累积影响。
最后的评论
我们分析了 Kiva 数据竞赛的一部分,关注的焦点不是实际的贷款和 Kiva 的合作伙伴领域,而是借款人和定义他们贫困的因素。我们的目的是创建一个针对贫困因素的分析,理解这些因素以及它们各自如何导致贫困的持续。
与围绕全面、综合的数据分析构建故事不同,我们需要在数据中导航,以讲述我们想要的故事。我们没有在这里描述初步步骤,其中我们进行了对数据的详尽分析(就像我们在上一章中做的那样,其中为两个数据集分别准备了最终分析,并得到了我们初步的 EDAs 的支持)。我们的重点是理解什么最能定义贫困,为此,我们将 Kiva 数据与来自联合国的额外数据相结合。
总之,我们可以断言,通过优先考虑最易受贫困特征不同方面影响的地区、国家和类别,Kiva 可以通过目标优化得到改进。雷达图可以是一个有用的工具。在设计它时,我们选择了那些指标或修改了一些指标,以获得更高的贫困总面积。
人均 GDP 并不是唯一表征贫困的指标;它可能采取不同的形式,并由多个因素定义,有时这些因素是相互依赖的。针对那些可能影响其他因素的潜在因素,如提高学校入学率、改善卫生条件、食物和医疗保健,以及通过 Kiva 反贫困项目,可能会增加积极的社会影响。
从不同的数据集讲述不同的故事
我们从分析 Kaggle 举办的第一场分析竞赛的数据开始本章,那是在 5 年多以前。我们将继续查看更近期的竞赛数据,特别是那些使用 Kaggle 自己的数据的竞赛。我们将查看 Meta Kaggle 数据集(见参考文献 2)中的数据,并尝试回答一个与近期在竞赛中创建大型团队趋势感知相关的问题。最终目的是表明,通过仔细检查可用数据,我们可以获得重要的见解。
图表
几年前,在 2019 年,一场竞赛即将结束时,关于近期竞争者团队数量增加的趋势进行了讨论,尤其是在高知名度、特色竞赛中。我对讨论板上这一趋势的说法很感兴趣,并决定使用 Kaggle 自己的数据(该数据集由 Kaggle 不断更新)来验证这一信息。为了保持该分析的结果,我将限制 Meta Kaggle 的数据到 2019 年底之前。自 2019 年以来,一些元数据关键词已更改。以前被称为“课堂”的比赛现在更名为“社区”。尽管在当前数据集版本中,该类别现在命名为“社区”,但我们将使用这两个术语,有时甚至只使用“课堂”,因为这是首次进行此分析时使用的术语。
实际的历史
如果我们按年份和团队规模分组查看团队数量,我们可以观察到大型团队并不仅限于 2017 年或 2018 年。让我们看看这些统计数据,如图5.13所示。
最大团队包括:
-
2012 年(有 40 名和 23 名团队成员)
-
2013 年(最大团队有 24 名团队成员)
-
2014 年(有 25 名团队成员的团队)
-
2017 年(最大团队有 34 名团队成员)
2017 年和 2018 年发生的情况是,团队数量(2017 年)和中型团队(4-8 名团队成员)的数量突然增加。
当检查每年的比赛数量时,我们也注意到,2018 年发生的情况是,没有限制团队规模的比赛数量占总比赛数量的百分比增加了。这在一定程度上解释了我们观察到的模式——2018 年有更多的大型团队。
图 5.13:按年份和团队规模分组的团队数量
如果我们排除社区竞赛,我们将获得图 5.14中显示的统计数据。就大型队伍而言,统计数据变化不大,因为大多数多成员队伍是为除社区类型以外的竞赛而组建的。
图 5.14:按年份和队伍规模分组的队伍数量,除了社区竞赛
让我们使用 Plotly Express 散点图查看金牌、银牌和铜牌随时间分布的情况,将奖牌按队伍规模分组为单独的轨迹。
在 y 轴上,我们使用了对数刻度来表示队伍数量;在 x 轴上,我们显示了队伍规模,标记的大小与奖牌的重要性成比例(金牌最大,铜牌最小)。如图图 5.15所示,我们展示了这些结果。我们可以观察到,在每年的获奖队伍中,最大的队伍如下:
-
2010 年:1 支队伍获得金牌,共有 4 名成员。
-
2011 年:1 支队伍获得金牌,共有 12 名成员。
-
2012 年:1 支队伍获得铜牌,共有 40 名成员。
-
2013 年:2 支队伍获得金牌,共有 24 名成员。
-
2014 年:1 支队伍获得铜牌,共有 6 名成员。
-
2015 年:1 支队伍获得铜牌,共有 18 名成员。
-
2016 年:1 支队伍获得金牌,共有 13 名成员。
-
2017 年:1 支队伍获得铜牌,共有 34 名成员。
-
2018 年:1 支队伍获得银牌,共有 23 名成员。
-
2019 年:2 支队伍获得金牌,共有 8 名成员,4 支队伍获得银牌,共有 8 名成员;请注意,对于 2019 年,结果仍然是不完整的。
让我们再看看一个按年份和队伍规模分组的热力图,其中我们只选择了特色竞赛。这些是最受瞩目的竞赛,因此吸引了最高的关注度,而且,队伍规模也最大。在图 5.16中,我们展示了这个热力图。我们可以观察到几个方面:
-
在 2018 年,只有 2 人、5 人和 7 人队伍的金牌数量有所增加。
-
在 2013 年(24 人和 10 人)、2012 年(23 人、15 人和 12 人)、2011 年(12 人)、2016 年(13 人和 11 人)以及 2017 年(10 人)中,获得金牌的最大队伍。
因此,关于特色竞赛中大型队伍数量最近才增加的这种看法是错误的。实际上,在 2012 年,获得奖牌的最大队伍有 40 名队员,而在 2013 年,有两支 24 人的队伍获得了金牌!
图 5.15:按奖牌(金牌、银牌和铜牌)和按年份过滤的队伍数量与队伍规模
对于研究竞赛,也可以得出类似的观察结果。在每年的研究竞赛中,获得奖牌的最大队伍如下:
-
2012 年:1 支队伍获得金牌和 1 枚银牌,共有 11 名成员
-
2013 年:1 支队伍获得铜牌,共有 9 名成员
-
2014 年:1 支队伍获得铜牌,共有 24 名成员
-
2015 年:1 支队伍获得银牌,共有 8 名成员
-
2016 年:1 支队伍获得银牌,共有 8 名成员
-
2017 年:4 支队伍获得铜牌,共有 8 名成员
-
2018 年:1 支队伍获得金牌,共有 9 名成员
结论是,在研究竞赛中获得铜牌、银牌或金牌的大队伍并非近期趋势。早在 2012 年,11 人组成的队伍就获得了金牌和银牌。2017 年,有四支队伍获得了铜牌。
图 5.16:按年份和团队规模分组获胜队伍的数量(仅限特色竞赛)
我们选择特色竞赛的队伍,并检查团队规模是否以任何方式与团队排名相关。为此,我们统计了每个队伍和年份的团队成员数量。然后,我们将结果与“队伍”数据集合并,以在一个数据集中拥有每个队伍的团队成员数量以及公众和私人排行榜排名。图 5.17显示了 2010 年至 2019 年间的团队规模和团队排名(公众和私人排行榜)的热力图。尽管存在一个非常小的负相关性值,但我们观察到以下情况:在私人排行榜和公众排行榜排名与团队规模之间的相关性值。存在一个非常小的逆相关性因子,表明团队规模往往随着排名值的降低而增加。换句话说,团队越接近顶部,其规模就越大。这些值在-0.02 到-0.12 之间(非常小的逆相关性值),并且随着时间的推移(绝对值)增加。
通常情况下,公众的逆相关性较大,这意味着团队在公众排行榜上的位置越高,团队规模往往越大;这也许可以让我们推测大团队,尤其是超大团队的投机性质。然而,实际上相关性太低,无法从中提取任何有意义的见解。
在分析竞赛和队伍数据后,我们了解到,在过去几年中,大队伍获得奖牌的频率是相等的,早在 2012 年就有超大队伍获得了竞赛奖牌。2012 年,一支 40 人的队伍获得了一枚铜牌,2016 年,一支 13 人的队伍在特色竞赛中获得了金牌。相比之下,2018 年一支 23 人的队伍获得了银牌。
图 5.17:公众和私人排行榜排名以及团队规模的关联矩阵
我们可以观察到,虽然公众排行榜和私人排行榜排名之间存在明显的强相关性,但团队规模与公众或私人排行榜排名之间没有相关性(对于小于 0.1 和负值)。
结论
结论是,尽管最近有一种增加大型团队组建频率的感知,但这并不是一个新现象(根据 2018-2019 年的观察);过去有更大的团队赢得了奖牌。
最近发生显著变化的是 Kagglers 的数量。让我们也检查一下这个信息。图 5.18显示了截至 2019 年的 Kaggle 用户分布。
图 5.18:2010 年至 2019 年的用户动态
这种动态表明,目前 70%的 Kaggle 用户实际上几年前并不存在——这包括我本人。新用户数量的增加实际上是指数级的。因此,所谓的社区记忆可能是一个有偏见的形象,因为大多数 Kagglers 几年前并没有参与过大型团队。
摘要
在本章中,我们探讨了数据分析竞赛,分析了 Kiva 数据分析竞赛的数据,然后是Meta Kaggle数据集,该数据集由 Kaggle 频繁更新,也是众多数据分析笔记本的主题。
使用 Kiva 数据,我们选择研究一个主题——理解贫困是什么——而不是进行详尽的数据探索分析。使用 Meta Kaggle 数据集,我们选择回答一个与创建大型团队参加比赛,尤其是旨在赢得高知名度比赛奖牌的参赛者感知相关的问题。构建一个有说服力的叙事,由详细记录的数据和清晰的视觉呈现支持,在数据分析竞赛中比详尽的数据探索分析更有效。
我们还开始在本章中使用 Plotly 作为可视化库。在下一章中,我们将重用我们为 Kiva 和 Meta Kaggle 分析开发的某些可视化脚本。
参考文献
-
数据科学善行:Kiva 众筹,Kaggle 数据集:
www.kaggle.com/kiva/data-science-for-good-kiva-crowdfunding
-
Meta Kaggle,Kaggle 数据集:
www.kaggle.com/datasets/kaggle/meta-kaggle
-
国家统计数据 – 联合国数据,Kaggle 数据集:
www.kaggle.com/sudalairajkumar/undata-country-profiles
-
Kiva 微型贷款——数据探索,Kaggle 笔记本:
github.com/PacktPublishing/Developing-Kaggle-Notebooks/blob/develop/Chapter-05/kiva-microloans-a-data-exploration.ipynb
-
多维贫困指数(MPI):
hdr.undp.org/en/content/multidimensional-poverty-index-mpi
-
多维贫困指数在维基百科上的介绍:
en.wikipedia.org/wiki/Multidimensional_Poverty_Index
-
plotly-utils
Kaggle 工具脚本:github.com/PacktPublishing/Developing-Kaggle-Notebooks/blob/develop/Chapter-05/plotly-utils.ipynb
-
Kiva:改变生活的贷款:
theglobalheroes.wordpress.com/2012/11/01/kiva-loans-that-change-lives/
-
理解贫困以优化微型贷款,Kaggle 笔记本:
github.com/PacktPublishing/Developing-Kaggle-Notebooks/blob/develop/Chapter-05/understand-poverty-to-optimize-microloans.ipynb
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人相聚,并和超过 5000 名成员一起学习,详情请见:
第六章:你能预测蜜蜂的亚种吗?
在本章中,我们将学习如何处理图像数据并开始构建用于图像分类的模型。多年来,计算机视觉在数据科学和数据分析中的应用呈指数增长。在 Kaggle 上一些最引人注目的(拥有大量点赞和复制的,例如复制和编辑)笔记本并不是 探索性数据分析(EDA)笔记本,而是构建模型的笔记本。
在本章中,我们将演示如何使用您深入的数据分析来准备构建模型,并且我们还将向您介绍模型迭代优化过程的一些见解。这不仅仅是为了比赛,而是为了一个图像数据集。数据集是 BeeImage Dataset: Annotated Honey Bee Images(参见 参考文献 1)。在前一章中,我们也开始使用 Plotly 作为可视化库。在本章中,我们将继续使用 Plotly 来可视化数据集特征。我们将一些有用的可视化函数与 Plotly 一起放在一个实用脚本中,名为 plotly-utils
(参见 参考文献 2)。与本章相关的笔记本是 Honeybee Subspecies Classification(参见 参考文献 3)。
本章将涵盖以下主题:
-
对 BeeImage Dataset: Annotated Honey Bee Images 的全面数据探索。
-
在准备模型基线之后,逐步优化模型,分析对训练和验证指标变化的影响,并采取新措施进一步改进模型。
数据探索
BeeImage Dataset: Annotated Honey Bee Images 包含一个 逗号分隔格式 (.csv) 文件,bee_data.csv
,包含 5172 行和 9 列,以及一个包含 5172 张图片的文件夹:
图 6.1:bee_data.csv 数据文件的样本
正如您所看到的,前面的数据框包含以下列:
-
file: 图片文件名
-
date: 拍摄图片的日期
-
time: 拍摄图片的时间
-
location: 美国位置,包括城市、州和国家名称
-
zip code: 与位置相关的邮政编码
-
subspecies: 当前图像中蜜蜂所属的亚种
-
health: 当前图像中蜜蜂的健康状态
-
pollen_carrying: 表示图片中蜜蜂是否带有花粉附着在其腿上
-
caste: 蜜蜂的社会阶层
我们将开始数据探索之旅,进行一些质量检查,重点关注 bee_data.csv
文件,然后是图像。对于数据质量检查,我们将使用在 第四章 中之前介绍的一个实用脚本,data_quality_stats
。
数据质量检查
如下所示,数据集没有任何缺失值。所有特征都是 string
类型。
图 6.2:bee_data.csv 文件中的缺失值。结果使用 data_quality_stats 函数获得
在图 6.3中,我们展示了数据集特征的唯一值。数据是在以下情况下收集的:
-
在 6 个不同的日期和 35 个不同的时间
-
在 8 个地点,7 个不同的邮编
在数据中,有七个亚种,用六个不同的健康问题表示。
图 6.3:bee_data.csv 文件中的唯一值。结果使用 data_quality_stats 函数获得
从图 6.4所示的数据中可以看出,21%的图像来自单一日期(16 个不同日期中的一个)。曾经有 11%的图像是在某个时间收集的。有一个单一的位置(加利福尼亚州的萨拉托加,邮编 95070),在那里收集了 2000 张(或 39%)的图像。意大利蜜蜂是最常见的物种。65%的图像代表健康的蜜蜂。几乎所有图像都显示了不带花粉的蜜蜂,而且所有图像都来自工蜂群体。
图 6.4:bee_data.csv 文件中最频繁的值。结果使用 data_quality_stats 函数获得
接下来,我们将与bee_data.csv
中的特征并行回顾图像数据。我们还将介绍读取和可视化图像的函数。
探索图像数据
首先,我们检查数据集中存在的所有图像名称是否也存在于图像文件夹中:
file_names = list(honey_bee_df['file'])
print("Matching image names: {}".format(len(set(file_names).intersection(image_files))))
结果是,所有在.csv
文件中索引的图像都存在于images
文件夹中。接下来,我们检查图像大小。为此,我们可以使用以下代码读取图像:
def read_image_sizes(file_name):
"""
Read images size using skimage.io
Args:
file_name: the name of the image file
Returns:
A list with images shape
"""
image = skimage.io.imread(config['image_path'] + file_name)
return list(image.shape)
或者,我们可以使用以下代码根据 OpenCSV(cv2
)库读取图像:
def read_image_sizes_cv(file_name):
"""
Read images size using OpenCV
Args:
file_name: the name of the image file
Returns:
A list with images shape
"""
image = cv2.imread(config['image_path'] + file_name)
return list(image.shape)
skimage.io:
%timeit m = np.stack(subset.apply(read_image_sizes))
下面的代码用于使用基于 opencv 的方法测量读取图像的执行时间:
%timeit m = np.stack(subset.apply(read_image_sizes_cv))
比较显示,使用基于opencv
的方法执行更快:
- 使用
skimage.io
:
129 ms ± 4.12 ms 每循环(7 次运行的平均值±标准差,每次循环 1 次)
- 使用
opencv
:
127 ms ± 6.79 ms 每循环(7 次运行的平均值±标准差,每次循环 10 次)
然后,我们应用最快的方法提取每个图像的形状(宽度、高度和深度,或颜色维度的数量)并将其添加到每个图像的数据集中:
t_start = time.time()
m = np.stack(honey_bee_df['file'].apply(read_image_sizes_cv))
df = pd.DataFrame(m,columns=['w','h','c'])
honey_bee_df = pd.concat([honey_bee_df,df],axis=1, sort=False)
t_end = time.time()
print(f"Total processing time (using OpenCV): {round(t_end-t_start, 2)} sec.")
执行前面代码的输出是:
Total processing time (using OpenCV): 34.38 sec.
boxplot. In the first, we show the image width distribution, and in the second, the image height distribution. The boxplot shows the minimum, first quartile, median, third quartile, and maximum values in the distribution of the value we plot. We also show the outliers’ values as points on each of the traces:
traceW = go.Box(
x = honey_bee_df['w'],
name="Width",
marker=dict(
color='rgba(238,23,11,0.5)',
line=dict(
color='red',
width=1.2),
),
orientation='h')
traceH = go.Box(
x = honey_bee_df['h'],
name="Height",
marker=dict(
color='rgba(11,23,245,0.5)',
line=dict(
color='blue',
width=1.2),
),
orientation='h')
data = [traceW, traceH]
layout = dict(title = 'Width & Heights of images',
xaxis = dict(title = 'Size', showticklabels=True),
yaxis = dict(title = 'Image dimmension'),
hovermode = 'closest',
)
fig = dict(data=data, layout=layout)
iplot(fig, filename='width-height')
结果绘制在图 6.5中。宽度和高度的中间值分别为 61 和 62。宽度和高度都有许多异常值(宽度最大值为 520,高度最大值为 392)。
图 6.5:图像的宽度和高度分布
在我们的分析中,我们包括了数据集中的所有特征,而不仅仅是与图像相关的特征。在我们开始构建预测模型的基线之前,我们希望了解与蜜蜂图像数据集:标注的蜜蜂图像相关的所有方面。
位置
通过按拍摄图片的位置和 ZIP 代码对数据集中的数据进行分组,我们可以观察到有一个位置具有相同的 ZIP 代码和类似的名字:
图 6.6:拍摄带有蜜蜂的图片的位置和 ZIP 代码
我们可以观察到,美国乔治亚州的雅典出现了两个略有不同的名称。我们只是使用以下代码将它们合并:
honey_bee_df = honey_bee_df.replace({'location':'Athens, Georgia, USA'}, 'Athens, GA, USA')
现在,让我们使用 Plotly 实用脚本模块中的一个函数来可视化结果位置数据的分布:
tmp = honey_bee_df.groupby(['zip code'])['location'].value_counts()
df = pd.DataFrame(data={'Images': tmp.values}, index=tmp.index).reset_index()
df['code'] = df['location'].map(lambda x: x.split(',', 2)[1])
plotly_barplot(df, 'location', 'Images', 'Tomato', 'Locations', 'Number of images', 'Number of bees images per location')
函数plotly_barplot
的代码如下:
def plotly_barplot(df, x_feature, y_feature, col, x_label, y_label, title):
"""
Plot a barplot with number of y for category x
Args:
df: dataframe
x_feature: x feature
y_feature: y feature
col: color for markers
x_label: x label
y_label: y label
title: title
Returns:
None
"""
trace = go.Bar(
x = df[x_feature],
y = df[y_feature],
marker=dict(color=col),
#text=df['location']
)
data = [trace]
layout = dict(title = title,
xaxis = dict(title = x_label, showticklabels=True, tickangle=15),
yaxis = dict(title = y_label),
hovermode = 'closest'
)
fig = dict(data = data, layout = layout)
iplot(fig, filename=f'images-{x_feature}-{y_feature}')
在图 6.7中,我们展示了拍摄蜜蜂图像的位置分布。大多数图像来自加利福尼亚州的萨拉托加(2000 张图像),其次是乔治亚州的雅典和爱荷华州的迪莫因。
图 6.7:位置分布
我们还基于一个选定的标准构建了一个用于可视化图像子集的函数。以下代码是根据位置选择图像并显示它们的子集(一行五张,来自同一位置):
#list of locations
locations = (honey_bee_df.groupby(['location'])['location'].nunique()).index
def draw_category_images(var,cols=5):
categories = (honey_bee_df.groupby([var])[var].nunique()).index
f, ax = plt.subplots(nrows=len(categories),ncols=cols, figsize=(2*cols,2*len(categories)))
# draw a number of images for each location
for i, cat in enumerate(categories):
sample = honey_bee_df[honey_bee_df[var]==cat].sample(cols)
for j in range(0,cols):
file=config['image_path'] + sample.iloc[j]['file']
im=imageio.imread(file)
ax[i, j].imshow(im, resample=True)
ax[i, j].set_title(cat, fontsize=9)
plt.tight_layout()
plt.show()
在图 6.8中,展示了这个选择的一部分(仅限于前两个位置)。完整的图像可以在相关的笔记本中查看:
图 6.8:来自两个地点的蜜蜂图像(从完整图片中选择,使用前面的代码获取)
日期和时间
让我们继续详细分析我们数据集中的特征。我们现在开始分析date
和time
数据。我们将date
转换为datetime
并提取年、月和日。我们还转换time
并提取小时和分钟:
honey_bee_df['date_time'] = pd.to_datetime(honey_bee_df['date'] + ' ' + honey_bee_df['time'])
honey_bee_df["year"] = honey_bee_df['date_time'].dt.year
honey_bee_df["month"] = honey_bee_df['date_time'].dt.month
honey_bee_df["day"] = honey_bee_df['date_time'].dt.day
honey_bee_df["hour"] = honey_bee_df['date_time'].dt.hour
honey_bee_df["minute"] = honey_bee_df['date_time'].dt.minute
在图 6.9中展示了每天和大约的小时及位置的蜜蜂图像数量的可视化。这个可视化的代码首先通过date_time
和hour
对数据进行分组,并计算每个日期和一天中的时间收集到的图像数量:
tmp = honey_bee_df.groupby(['date_time', 'hour'])['location'].value_counts()
df = pd.DataFrame(data={'Images': tmp.values}, index=tmp.index).reset_index()
然后,我们构建当我们在图中的某个点上悬停时显示的文本。这个文本将包括小时、位置和图像数量。然后我们将悬停文本作为新数据集中的一个新列添加:
hover_text = []
for index, row in df.iterrows():
hover_text.append(('Date/time: {}<br>'+
'Hour: {}<br>'+
'Location: {}<br>'+
'Images: {}').format(row['date_time'],
row['hour'],
row['location'],
row['Images']))
df['hover_text'] = hover_text
然后,我们为每个位置绘制一个散点图,表示图片收集的时间和小时。每个点的尺寸与在该位置、一天中的某个时间点和某个日期拍摄的图像数量成比例:
locations = (honey_bee_df.groupby(['location'])['location'].nunique()).index
data = []
for location in locations:
dfL = df[df['location']==location]
trace = go.Scatter(
x = dfL['date_time'],y = dfL['hour'],
name=location,
marker=dict(
symbol='circle',
sizemode='area',
sizeref=0.2,
size=dfL['Images'],
line=dict(
width=2
),),
mode = "markers",
text=dfL['hover_text'],
)
data.append(trace)
layout = dict(title = 'Number of bees images per date, approx. hour and location',
xaxis = dict(title = 'Date', showticklabels=True),
yaxis = dict(title = 'Hour'),
hovermode = 'closest'
)
fig = dict(data = data, layout = layout)
iplot(fig, filename='images-date_time')
在下一张图像,图 6.9中,我们看到运行上述代码的结果。大多数图片是在八月份拍摄的。大多数图片也是在下午时段拍摄的。
图 6.9:每天和大约的小时及位置的蜜蜂图像数量
亚种
我们使用相同的 plotly_barplot
函数来可视化亚种的分布。大多数蜜蜂是意大利蜜蜂,其次是俄罗斯蜜蜂和卡尼奥兰蜜蜂(见 图 6.10)。其中 428 张图像未被分类(标签值为-1)。我们将未分类的图像归为一个亚种类别。
图 6.10:按日期和大约的小时及位置划分的蜜蜂图像数量
在 图 6.11 中,我们展示了一些图像的选择,其中只包含少数亚种样本:
图 6.11:几个亚种蜜蜂图像的样本
现在,让我们表示每个亚种和位置的图像数量,以及每个亚种和小时的图像数量(见 图 6.12)。收集到的图像数量最多的是来自加利福尼亚州萨拉托加(1972 张图像),所有图像都是意大利蜜蜂。在 13 点收集到的图像数量最多,所有图像也都是意大利蜜蜂(909 张图像)。
图 6.12:按亚种和位置划分的图像数量(上方)以及按亚种和小时划分的图像数量(下方)
Subspecies
图像在重量和高度上具有很大的多样性。图 6.13 使用箱线图展示了重量和高度的分布。
图 6.13:每个亚种的图像大小分布 – 宽度(上方)和高度(下方)
VSH 意大利蜜蜂在宽度和高度上都有最大的平均值和最大的方差。西方蜜蜂、卡尼奥兰蜜蜂和混合本地种群 2在重量和高度上的分布最为紧凑(方差较低)。数量最多的亚种意大利蜜蜂显示出较小的中位数和较大的方差,有很多异常值。在下图中,我们将在同一个散点图上展示重量和高度分布:
图 6.14:每个亚种的图像大小分布 – 散点图
前面的图示使用散点图展示了重量和高度分布,下面的代码展示了这一可视化的实现。首先,我们定义一个函数来绘制散点图,其中图像宽度在 x
轴上,图像高度在 y
轴上:
def draw_trace_scatter(dataset, subspecies):
dfS = dataset[dataset['subspecies']==subspecies];
trace = go.Scatter(
x = dfS['w'],y = dfS['h'],
name=subspecies,
mode = "markers",
marker = dict(opacity=0.8),
text=dfS['subspecies'],
)
return trace
我们现在使用上面定义的函数为每个亚种绘制散点图。每次函数调用都会创建一个轨迹,我们将这些轨迹添加到 Plotly 图中:
subspecies = (honey_bee_df.groupby(['subspecies'])['subspecies'].nunique()).index
def draw_group(dataset, title,height=600):
data = list()
for subs in subspecies:
data.append(draw_trace_scatter(dataset, subs))
layout = dict(title = title,
xaxis = dict(title = 'Width',showticklabels=True),
yaxis = dict(title = 'Height', showticklabels=True, tickfont=dict(
family='Old Standard TT, serif',
size=8,
color='black'),),
hovermode = 'closest',
showlegend=True,
width=800,
height=height,
)
fig = dict(data=data, layout=layout)
iplot(fig, filename='subspecies-image')
draw_group(honey_bee_df, "Width and height of images per subspecies")
健康
图 6.15 展示了具有各种健康问题的图像分布。大多数图像是健康蜜蜂(3384),其次是少量瓦拉罗,蜂箱甲虫(579),瓦拉罗,小蜂箱甲虫(472),以及蚂蚁问题(457):
图 6.15:不同健康问题的图像数量
如果我们分析每个亚种和健康问题的图像数量(见图 6.16),我们可以观察到只有少量健康和亚种值组合存在。大多数图像是健康的意大利蜜蜂(1972),其次是少量瓦螨、蜂箱甲虫,然后是意大利蜜蜂(579),最后是健康的俄罗斯蜜蜂(527)。未知亚种要么是健康的(177)要么是蜂群被盗(251)。
图 6.16:每个亚种和不同健康问题的蜜蜂图像数量
在图 6.17中,我们绘制了每个地点、亚种和健康问题的图像数量:
图 6.17:每个地点、亚种和健康问题的图像数量
其他
携带花粉的蜜蜂图像数量很少。图 6.18显示了一些携带花粉和不携带花粉的蜜蜂图像。所有蜜蜂都来自同一个等级:工蜂等级。
图 6.18:携带和不携带花粉的蜜蜂图像的选择
结论
我们使用plotly_sankey
脚本从plotly_utils
实用脚本模块中绘制的桑基图,来绘制图 6.19中的摘要图。桑基图主要用于可视化流程或流动,例如,在经济学中能源的生产及其来源和消费者。我这里用它来达到另一个目的,即总结具有多个特征的数据分布。它显示了同一图表中按日期、时间、地点、ZIP 代码、亚种和健康分布的图像。由于空间限制,这里没有给出桑基图的适配代码(请参考参考文献 2获取与本章相关的代码示例);我们只包含了适配蜜蜂数据以使用此功能的代码:
tmp = honey_bee_df.groupby(['location', 'zip code', 'date', 'time', 'health'])['subspecies'].value_counts()
df = pd.DataFrame(data={'Images': tmp.values}, index=tmp.index).reset_index()
fig = plotly_sankey(df,cat_cols=['date', 'time', 'location', 'zip code', 'subspecies', 'health'],value_cols='Images',
title='Honeybee Images: date | time | location | zip code | subspecies | health',
color_palette=[ "darkgreen", "lightgreen", "green", "gold", "black", "yellow"],
height=800)
iplot(fig, filename='Honeybee Images')
图 6.19中的可视化,一个漏斗形图,使我们能够在一个单一的图表中捕捉多个特征之间的关系:
图 6.19:图像摘要
到目前为止,我们分析了数据集中特征分布。现在,我们对数据集中的数据有了更好的理解。在本章接下来的部分,我们将开始准备构建一个机器学习模型,以对亚种进行图像分类,这是本章的第二大且更为重要的目标。
亚种分类
本节的目标将是使用迄今为止调查的图像构建一个机器学习模型,该模型可以正确预测亚种。由于我们只有一个数据集,我们将首先将数据分割成三个子集:用于训练、验证和测试数据。我们将在训练过程中使用训练和验证数据:训练数据用于向模型提供数据,验证数据用于验证模型如何使用新数据预测类别(即亚种)。然后,训练和验证后的模型将用于预测测试集中的类别,该类别既未用于训练也未用于验证。
数据分割
首先,我们将数据分割成train
和test
,使用 80%–20%的分割。然后,再次将train
数据分割成训练和验证,使用相同的 80%–20%分割。分割使用stratify
和subspecies
作为参数执行,确保平衡的子集,同时尊重训练、验证和测试子样本集中类的整体分布。这里选择的训练/验证/测试分割的百分比是任意选择的,并不是研究或优化的结果。在您的实验中,您可以处理不同的训练/验证/测试子集值,也可以选择不使用stratify
:
train_df, test_df = train_test_split(honey_bee_df, test_size=config['test_size'], random_state=config['random_state'],
stratify=honey_bee_df['subspecies'])
train_df, val_df = train_test_split(train_df, test_size=config['val_size'], random_state=config['random_state'],
stratify=train_df['subspecies'])
最终,我们将有三个子集,如下所示:
-
训练集行数:3309
-
验证集行数:828
-
测试集行数:1035
我们将图像分割成子集,对应于图像名称的子集。我们创建了读取图像并将它们全部调整到配置中定义的相同维度的函数,使用skimage.io
和opencv
。我们决定将所有图像调整到 100 x 100 像素。我们的决定是基于对图像尺寸分布的分析。您可以选择修改笔记本中提供的代码(参考 3)并尝试不同的图像尺寸。
以下代码使用skimage.io
读取图像并根据配置中设置的尺寸调整大小。您可以更改配置并调整图像大小,使用不同的图像高度和宽度值:
def read_image(file_name):
"""
Read and resize the image to image_width x image_height
Args:
file_name: file name for current image
Returns:
resized image
"""
image = skimage.io.imread(config['image_path'] + file_name)
image = skimage.transform.resize(image, (config['image_width'], config['image_height']), mode='reflect')
return image[:,:,:config['image_channels']]
下面的代码使用 OpenCV 读取并调整图像大小。该函数与上面展示的之前的函数不同之处仅在于读取图像文件的方法:
def read_image_cv(file_name):
"""
Read and resize the image to image_width x image_height
Args:
file_name: file name for current image
Returns:
resized image
"""
image = cv2.imread(config['image_path'] + file_name)
image = cv2.resize(image, (config['image_width'], config['image_height']))
return image[:,:,:config['image_channels']]
然后,我们将这些函数应用于所有数据集文件,以读取和调整数据集中的图像大小。
我们还创建了与分类目标变量对应的虚拟变量。我们更喜欢使用这种方法,因为我们将为多类分类准备一个模型,该模型为每个类别输出概率:
def categories_encoder(dataset, var='subspecies'):
X = np.stack(dataset['file'].apply(read_image))
y = pd.get_dummies(dataset[var], drop_first=False)
return X, y
s_time = time.time()
X_train, y_train = categories_encoder(train_df)
X_val, y_val = categories_encoder(val_df)
X_test, y_test = categories_encoder(test_df)
e_time = time.time()
print(f"Total time: {round(e_time-s_time, 2)} sec.")
通过这一点,我们已经展示了如何读取和调整我们的图像大小。接下来,我们将看到如何通过乘以我们的图像来增加训练集中的数据量,以便向模型展示更多种类的数据。
数据增强
我们将使用深度学习模型来对图像中的亚种进行分类。通常,深度学习模型在训练数据量较大时表现更好。使用数据增强,我们还创建了更多样化的数据,这对模型质量也有益。如果我们在训练过程中让模型接触到更多样化的数据,我们的模型将提高其泛化能力。
我们基于keras.preprocessing.image
中的ImageDataGenerator
定义了一个数据增强组件。在本笔记本中,我们将使用 Keras 构建模型的各个组件。ImageDataGenerator
组件通过应用以下参数初始化,以创建训练数据集的随机变化:
-
对原始图像进行旋转(0 到 180 度范围内)
-
缩放(10%)
-
水平和垂直方向上的平移(10%)
-
水平和垂直方向的平移(10%)
这些变化可以分别控制。并非所有用例都允许或从应用上述所有转换中受益(例如,考虑具有建筑或其他地标图像的情况,对于这些图像,旋转并不太有意义)。以下代码可以用于我们的情况来初始化和拟合图像生成器:
image_generator = ImageDataGenerator(
featurewise_center=False,
samplewise_center=False,
featurewise_std_normalization=False,
samplewise_std_normalization=False,
zca_whitening=False,
rotation_range=180,
zoom_range = 0.1,
width_shift_range=0.1,
height_shift_range=0.1,
horizontal_flip=True,
vertical_flip=True)
image_generator.fit(X_train)
然后,我们继续构建和训练基线模型。
构建基线模型
几乎总是建议您从一个简单的模型开始,然后进行错误分析。根据错误分析,您将需要进一步细化您的模型。例如,如果您观察到您的基线模型在训练中获得了很大的误差,您需要首先改进训练。您可以通过添加更多数据、改进您的数据标注或创建更好的特征来实现这一点。如果您的训练误差很小,但您有较高的验证误差,这意味着您的模型可能过度拟合了训练数据。在这种情况下,您需要尝试提高模型泛化能力。您可以尝试各种技术来提高模型泛化能力。关于此类分析,请参阅本章末尾的参考 4。
我们将使用Keras
库来定义我们的模型。Keras(见参考 5)是 TensorFlow 机器学习平台(见参考 6)的包装器。它允许您通过定义具有专用层的顺序结构来创建强大的深度学习模型。我们将向我们的模型添加以下层:
-
一个具有 3 维度的 16 个滤波器的
Conv2D
层 -
一个具有 2 倍缩减因子的
MaxPooling2D
层 -
一个具有 3 维度的 16 个滤波器的卷积层
-
一个
Flatten
层 -
一个具有亚种目标特征类别数量的
Dense
层
上述架构是一个非常简单的卷积神经网络的例子。convolutional2d
层的作用是对 2D 输入应用滑动卷积滤波器。maxpool2d
层将通过在输入窗口(参见参考文献 5获取更多详细信息)上取最大值来沿其空间维度(宽度和高度)对输入进行下采样。
构建所述架构的代码如下:
model1=Sequential()
model1.add(Conv2D(config['conv_2d_dim_1'],
kernel_size=config['kernel_size'],
input_shape=(config['image_width'], config['image_height'],config['image_channels']),
activation='relu', padding='same'))
model1.add(MaxPool2D(config['max_pool_dim']))
model1.add(Conv2D(config['conv_2d_dim_2'], kernel_size=config['kernel_size'],
activation='relu', padding='same'))
model1.add(Flatten())
model1.add(Dense(y_train.columns.size, activation='softmax'))
model1.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
在图 6.20中,我们展示了模型的摘要信息。如您所见,可训练参数的总数是 282,775:
图 6.20:基准模型的摘要
对于基准模型,我们从一个小模型开始,并减少 epoch 的数量进行训练。输入图像的大小是 100 x 100 x 3(如我们之前解释的)。我们将对这个模型进行五次 epoch 的训练。批大小设置为 32。运行训练的代码如下:
train_model1 = model1.fit_generator(image_generator.flow(X_train, y_train, batch_size=config['batch_size']),
epochs=config['no_epochs_1'],
validation_data=[X_val, y_val],
steps_per_epoch=len(X_train)/config['batch_size'])
图 6.21显示了基准模型的训练日志。在训练过程中,我们没有保存最佳模型版本;最后一步的模型权重将用于测试。
图 6.21:基准模型的训练日志。显示了每个步骤的训练损失和准确率以及验证损失和准确率。
在每个批次之后更新训练损失和准确率,在每个 epoch 结束时计算验证损失和准确率。接下来,在模型训练和验证之后,我们将评估测试集的损失和准确率:
score = model1.evaluate(X_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
在图 6.22中,我们展示了训练和验证损失以及训练和验证准确率:
图 6.22:基准模型 – 训练和验证准确率(左)和训练和验证损失(右)
得到的结果如下:
-
测试损失:0.42
-
测试准确率:0.82
测试损失指的是损失函数,这是一个数学函数,用于衡量预测值与真实值之间的差异。在训练过程中,通过测量这个值,对于训练集和验证集,我们可以监控模型的学习和预测的改进情况。
使用sklearn
中的metrics.classification_report
指标分类报告,我们计算了训练数据中每个类的精确度、召回率、f1 分数和准确率。相应的代码如下:
def test_accuracy_report(model):
predicted = model.predict(X_test)
test_predicted = np.argmax(predicted, axis=1)
test_truth = np.argmax(y_test.values, axis=1)
print(metrics.classification_report(test_truth, test_predicted, target_names=y_test.columns))
test_res = model.evaluate(X_test, y_test.values, verbose=0)
print('Loss function: %s, accuracy:' % test_res[0], test_res[1])
在图 6.23中,我们展示了测试集的分类报告,其中我们使用了基于训练数据的基线模型拟合。精确度、召回率和 f1 分数的宏平均分别为0.78、0.72和0.74(支持数据为1035)。加权平均精确度、召回率和 f1 分数分别为0.82、0.83和0.82。
图 6.23:使用基线模型对测试数据的分类报告
这些加权平均分数较高,因为与所有类别分数的简单平均值不同,这些是加权平均值,所以与更好表示的类别相关的较高分数将对整体平均有更高的贡献。1 Mixed local stock 2(0.52)和VSH 意大利蜜蜂(0.68)的精确度/类别分数是最差的。最差的整体分数是VSH 意大利蜜蜂,其中召回率为 0.33。
逐步优化模型
如果我们现在回到训练和验证错误,我们可以看到验证和训练准确率大约是 0.81 和 0.82。
我们将继续训练模型,为了避免过拟合,我们还将引入两个Dropout
层,每个层的系数为 0.4。Dropout
层在神经网络中用作正则化方法。其目的是防止过拟合并提高模型的泛化能力。作为参数给出的系数是在每个训练 epoch 中随机选择的输入百分比,将其设置为 0。模型的结构在图 6.24中描述。可训练参数的数量将保持不变。
图 6.24:优化模型摘要。添加了两个 Dropout 层
我们还将 epoch 的数量扩展到 10。让我们看看图 6.25中的结果,其中我们展示了训练和验证准确率以及训练和验证损失。
图 6.25:优化后的模型(版本 2)- 训练和验证准确率(左)和训练和验证损失(右)
最终训练损失为 0.32,最终训练准确率为 0.87。最终验证损失为 0.28,最终验证准确率为 0.88。这些都是改进的结果。当然,训练准确率主要是由于我们训练了更多的 epoch。
图 6.26:使用第二次优化的模型(训练 epoch 增加到 10 并添加 Dropout 层)的测试数据分类报告
根据验证准确度,结果是更多的训练周期以及添加Dropout
层的结果,这些层保持了过拟合的控制。现在让我们检查测试损失和准确度,以及查看测试数据的整个分类报告。
宏平均和加权平均的度量标准在精确度、召回率和 f1 分数上都有所提高。我们还可以看到,对于使用基线获得的小分数类别,精确度、召回率和 f1 分数有了显著提高。1 混合本地股票 2的精确度为 0.52,现在精确度为 0.63。至于VSH 意大利蜜蜂,精确度为 0.68,现在为 0.97。我们注意到西方蜜蜂的精确度有所下降,但这个少数类的支持只有 7,所以这个结果是预期的。
我们继续优化我们的模型以提高验证和测试度量标准——换句话说,提高模型性能。在下一个迭代中,我们将训练周期数增加到 50。此外,我们将添加三个回调函数,如下所示:
-
一个学习率调度器,用于实现学习率变化的非线性函数。通过在每个周期改变学习率,我们可以改善训练过程。我们引入的用于控制学习率的函数将逐渐降低学习函数的值。
-
一个早期停止器,基于损失函数的演变(如果损失在一定数量的周期内没有改善)和一个耐心因子(在监控函数没有看到任何改善之后的周期数,我们停止训练)来停止训练周期。
-
每次获得最佳准确度时,都会有一个检查指针来保存表现最佳的模型。这将使我们能够使用的不是最后一次训练周期的模型参数,而是所有周期中表现最佳的模型。
三个回调函数的代码如下:
annealer3 = LearningRateScheduler(lambda x: 1e-3 * 0.995 ** (x+config['no_epochs_3']))
earlystopper3 = EarlyStopping(monitor='loss', patience=config['patience'], verbose=config['verbose'])
checkpointer3 = ModelCheckpoint('best_model_3.h5',
monitor='val_accuracy',
verbose=config['verbose'],
save_best_only=True,
save_weights_only=True)
适配模型的代码也如下所示:
train_model3 = model3.fit_generator(image_generator.flow(X_train, y_train, batch_size=config['batch_size']),
epochs=config['no_epochs_3'],
validation_data=[X_val, y_val],
steps_per_epoch=len(X_train)/config['batch_size'],
callbacks=[earlystopper3, checkpointer3, annealer3])
训练可能需要分配的最大周期数,或者如果满足早期停止标准(即,在等于耐心因子的周期数之后没有损失函数的改善),它可能会提前结束。无论如何,实现了最佳验证准确度的模型将被保存并用于测试。
在图 6.27中,我们展示了此进一步优化的模型的训练和验证准确度以及训练和验证损失的变化。最终获得的训练损失为 0.18,最终训练准确度为 0.93。对于验证,最后的验证损失为 0.21,最后的验证准确度为 0.91。在最后一个周期,学习率为 6.08e-4。最佳验证准确度是在第 46 个周期获得的,为 0.92。
图 6.27:经过优化的模型(版本 3,包含学习率调度器、提前停止和检查点)——训练和验证准确率(左)和训练和验证损失(右)
我们使用保存的模型检查点(用于第 46 个 epoch)来预测测试数据。在图 6.28中,我们展示了第三个模型的分类报告。
宏平均指标进一步改进,分别达到精确度、召回率和 f1 分数的 0.88、0.90 和 0.89。精确度、召回率和 f1 分数的加权平均值也分别提高到 0.91、0.90 和 0.90。
图 6.28:使用第三个优化模型的测试数据分类报告(训练 epochs 增加到 50,并添加了学习率调度器、提前停止器和模型检查点)
在这里,我们将停止迭代改进模型的过程。您可以继续对其进行优化。您可以尝试添加更多的卷积和 maxpool 层,使用不同的内核数量和步长值来处理不同的超参数,包括不同的批量大小或学习率调度器。您还可以更改优化方案。改变模型的另一种方法是,通过数据增强控制类别图像的平衡(目前,蜜蜂图像在亚种
类别上不平衡)。
您还可以尝试各种数据增强参数,并尝试使用不同的数据增强解决方案。参见参考文献 5,了解一个目前非常流行的图像数据增强库的示例,Albumentations,由一群数据科学家、研究人员和计算机视觉工程师创建,其中包括著名的 Kaggle 大师 Vladimir Iglovikov。
摘要
在本章中,我们首先介绍了一个新的数据集,其中包含了在不同日期和不同地点收集的图像元数据,包括各种患有不同疾病的蜜蜂亚种。我们还介绍了一些函数,用于基于skimage.io
和 opencv (cv2
)读取、缩放和从图像中提取特征。
我们使用了一个新创建的实用脚本,基于 Plotly 可视化表格数据,并利用 Plotly 的灵活性创建定制的图形,从而创建了有洞察力的可视化。我们还创建了图像的可视化函数。
在详细的数据探索分析(EDA)之后,我们转向构建蜜蜂亚种的预测模型。在这里,我们介绍了一种数据增强方法,通过从原始图像集中创建变化(旋转、缩放、平移和镜像)来增加初始可用训练数据。我们使用 stratify
将数据分为训练、验证和测试子集,以在随机采样三个子集时考虑类别不平衡。我们首先训练和验证了一个基线模型,然后,在执行错误分析后,我们逐步改进了初始模型,通过添加更多步骤,引入 Dropout
层,然后使用几个回调:学习率调度器、早期停止器和模型检查点。我们分析了训练、验证和测试错误的迭代改进,不仅关注训练和验证损失和准确率,还关注测试数据的分类报告。
在下一章中,我们将介绍文本数据分析的技术和工具,展示您如何准备数据以创建使用文本数据的基线模型。
参考文献
-
BeeImage 数据集:标注的蜜蜂图像:
www.kaggle.com/datasets/jenny18/honey-bee-annotated-images
-
plotly-script
和 Kaggle 工具脚本:github.com/PacktPublishing/Developing-Kaggle-Notebooks/blob/develop/Chapter-06/plotly-utils.ipynb
-
蜜蜂亚种分类,Kaggle 笔记本:
github.com/PacktPublishing/Developing-Kaggle-Notebooks/blob/develop/Chapter-06/honeybee-subspecies-classification.ipynb
-
安德鲁·吴,机器学习渴望:
info.deeplearning.ai/machine-learning-yearning-book
-
Keras:
keras.io/
-
TensorFlow:
www.tensorflow.org/
-
使用 Albumentations 与 Tensorflow:
github.com/albumentations-team/albumentations_examples/blob/master/notebooks/tensorflow-example.ipynb
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人相聚,并在以下地点与超过 5000 名成员一起学习:
第七章:文本分析一切所需
在本章中,我们将学习如何分析文本数据并创建机器学习模型来帮助我们。我们将使用Jigsaw 无意中在毒性分类中的偏差数据集(参见参考文献 1)。这个竞赛的目标是构建检测毒性和减少可能错误地与毒性评论相关联的少数族裔的不当偏见的模型。通过这个竞赛,我们引入了自然语言处理(NLP)领域。
竞赛中使用的数据来源于由 Aja Bogdanoff 和 Christa Mrgan 于 2015 年创立的 Civil Comments 平台(参见参考文献 2),旨在解决在线讨论中的礼貌问题。当平台于 2017 年关闭时,他们选择保留约 200 万条评论,供研究人员理解并改善在线对话中的礼貌。Jigsaw 组织赞助了这项工作,并随后开始了一场语言毒性分类竞赛。在本章中,我们将把纯文本转换为有意义的、模型准备好的数字,以便根据评论的毒性将它们分类到不同的组别。
简而言之,本章将涵盖以下主题:
-
对Jigsaw 无意中在毒性分类中的偏差竞赛数据集的数据进行探索
-
介绍 NLP 特定的处理和分析技术,包括词频、分词、词性标注、命名实体识别和词嵌入
-
对文本数据的预处理进行迭代优化以准备模型基线
-
为这次文本分类竞赛创建一个模型基线
数据中有什么?
来自Jigsaw 无意中在毒性分类中的偏差竞赛数据集的数据包含训练集中的 180 万行和测试集中的 97,300 行。测试数据仅包含一个评论列,不包含目标(预测值)列。训练数据除了评论列外,还包括另外 43 列,包括目标特征。目标是介于 0 和 1 之间的数字,代表预测此竞赛的目标注释。此目标值表示评论的毒性程度(0
表示零/无毒性,1
表示最大毒性),其他 42 列是与评论中存在某些敏感主题相关的标志。主题与五个类别相关:种族和民族、性别、性取向、宗教和残疾。更详细地说,以下是每个类别的标志:
-
种族和民族:
亚洲人
、黑人
、犹太人
、拉丁美洲人
、其他种族或民族
和白人
-
性别:
女性
、男性
、跨性别
和其他性别
-
性取向:
双性恋
、异性恋
、同性恋/女同性恋
和其他性取向
-
宗教:
无神论者
、佛教徒
、基督徒
、印度教徒
、穆斯林
和其他宗教
-
残疾:
智力或学习障碍
、其他残疾
、身体残疾
和精神或心理疾病
还有一些特征(即数据集中的列)用于识别评论:created_data
、publication_id
、parent_id
和article_id
。还提供了与评论相关的一些用户反馈信息特征:rating
、funny
、wow
、sad
、likes
、disagree
和sexual_explicit
。最后,还有两个与注释相关的字段:identity_annotator_count
和toxicity_annotator_count
。
让我们从对目标特征和敏感特征的快速分析开始。
目标特征
我们首先想看看目标特征的分布。让我们看看图 7.1中这些值的分布直方图:
图 7.1:目标值分布(训练数据,190 万列)
对于这个直方图,我们在Y轴上使用了对数刻度;这样做的原因是我们想看到值向 0 的偏斜分布。当我们这样做时,我们观察到我们有一个双峰分布:峰值在约 0.1 的间隔处,振幅减小,并且出现频率较低,趋势缓慢上升,叠加。大部分的目标值(超过 100 万)都是0
。
敏感特征
我们将查看之前列出的敏感特征的分布(种族和民族、性别、性取向、宗教和残疾)。由于分布的偏斜(与目标类似,我们在0
处有集中),我们将在Y轴上再次使用对数刻度。
图 7.2展示了种族和民族特征值的分布。这些值看起来不连续且非常离散,直方图显示了几个分离的峰值:
图 7.2:种族和民族特征值的分布
我们可以在图 7.3中观察到性别特征值有类似的分布:
图 7.3:性别特征值分布
在图 7.4中,我们展示了额外毒性特征值(severe_toxicity
、obscene
、identity_attack
、insult
或threat
)的分布。如您所见,分布更加均匀,并且insult
有增加的趋势:
图 7.4:额外毒性特征值的分布
让我们也看看目标值与种族或民族、性别、性取向、宗教和残疾特征值之间的相关性。我们在此不展示所有特征的相关矩阵,但您可以在与本章节相关的笔记本中检查它。在这里,我们只展示了与目标相关的第一个 16 个特征,按相关性因素排序:
train_corr['target'].sort_values(ascending=False)[1:16]
让我们看看图 7.5中按相关性因素与目标排序的前 15 个特征:
图 7.5:其他特征与目标特征的前 15 个相关因素
接下来,在图 7.6中,我们展示了这些选定特征与目标特征之间的相关矩阵:
图 7.6:目标与相关性最高的 15 个特征之间的相关矩阵
我们可以观察到target
与insult
(0.93)、obscene
(0.49)和identity_attack
(0.45)高度相关。此外,severe_toxicity
与insult
和obscene
呈正相关。identity_attack
与white
、black
、muslim
和homosexual_gay_or_lesbian
有轻微的相关性。
我们研究了target
特征(预测特征)和敏感特征的分布。现在我们将转向本章分析的主要主题:评论文本。我们将应用几种 NLP 特定的分析技术。
分析评论文本
NLP 是人工智能的一个领域,它涉及使用计算技术使计算机能够理解、解释、转换甚至生成人类语言。NLP 使用多种技术、算法和模型来处理和分析大量文本数据集。在这些技术中,我们可以提到:
-
分词:将文本分解成更小的单元,如单词、词的一部分或字符
-
词形还原或词干提取:将单词还原为词典形式或去除最后几个字符以得到一个共同形式(词干)
-
词性标注(POS)标记:将语法类别(例如,名词、动词、专有名词和形容词)分配给序列中的每个单词
-
命名实体识别(NER):识别和分类实体(例如,人名、组织名和地名)
-
词嵌入:使用高维空间来表示单词,在这个空间中,每个单词的位置由其与其他单词的关系决定
-
机器学习模型:在标注数据集上训练模型以学习语言数据中的模式和关系
NLP 应用可以包括情感分析、机器翻译、问答、文本摘要和文本分类。
在对 NLP 进行快速介绍之后,让我们检查实际的评论文本。我们将构建几个词云图(使用整个数据集的 20,000 条评论子集)。我们首先查看整体单词分布(参见本章相关的笔记本),然后查看目标值高于 0.75 和低于 0.25 的单词分布:
图 7.7:低目标分数<0.25(左)和高目标分数>0.75(右)的评论中的常见单词(1-gram)
目标
与侮辱
高度相关,我们预计在两个特征的词云中会看到相当接近的单词分布。这一假设得到了证实,图 7.8很好地说明了这一点(无论是低分还是高分):
图 7.8:低侮辱分数<0.25(左)和高侮辱分数>0.75(右)的评论中的常见单词(1-gram)
如您所见,分布显示了目标和侮辱的低分和高分中高频相似单词。
在相关的笔记本中还有更多关于威胁
、淫秽
和其他特征的词云。这些词云为我们提供了对最频繁单词的良好初始直觉。我们将在构建词汇表和检查词汇覆盖范围部分对整个语料库词汇中的单词频率进行更详细的分析。目前,我们可以观察到,我们进行的分析仅限于单个单词的频率,而没有捕捉到这些单词在整个语料库中的分组情况——换句话说,各种单词是如何一起使用的,以及基于此,如何识别语料库中的主要主题。这种旨在揭示整个语料库潜在语义结构的处理称为主题建模。在此方法中,对单词共现模式的分析使我们能够揭示文本中存在的潜在主题。
相关笔记本中主题建模实现灵感的来源是一系列关于使用潜在狄利克雷分配进行主题建模的文章和教程(见参考文献5-10)。
主题建模
我们首先通过预处理评论文本,使用gensim
库来消除特殊字符、常用词、连接词(或停用词)以及长度小于 2 的单词:
def preprocess(text):
result = []
for token in gensim.utils.simple_preprocess(text):
if token not in gensim.parsing.preprocessing.STOPWORDS and len(token) > 2:
result.append(token)
return result
以下代码将定义的preprocess
函数应用于所有评论:
%%time
preprocessed_comments = train_subsample['comment_text'].map(preprocess)
然后,我们使用gensim
/corpora
中的dictionary
创建一个单词字典。我们还过滤极端值,以消除不常见的单词并限制词汇表的大小:
%%time
dictionary = gensim.corpora.Dictionary(preprocessed_comments)
dictionary.filter_extremes(no_below=10, no_above=0.5, keep_n=75000)
在这些限制下,我们接下来生成一个从字典中生成的词袋(bow
)语料库。然后,我们对这个语料库应用TF-IDF(词频-逆文档频率),它为文档在文档集合或语料库中的重要性提供了一个数值表示。
tf
组件衡量一个单词在文档中出现的频率。idf
组件显示一个单词在整个文档语料库中的重要性(在我们的案例中,是整个评论集合)。这个因素随着一个术语在文档中出现的频率增加而降低。因此,在tfidf
转换之后,一个单词和一个文档的系数对于在语料库级别不常见但在当前文档中频率较高的单词来说更大:
%%time
bow_corpus = [dictionary.doc2bow(doc) for doc in preprocessed_comments]
tfidf = models.TfidfModel(bow_corpus)
corpus_tfidf = tfidf[bow_corpus]
然后,我们应用潜在狄利克雷分配(lda
),这是一种基于该语料库中单词频率生成主题的主题模型,使用gensim
的并行处理实现(LdaMulticore
):
%%time
lda_model = gensim.models.LdaMulticore(corpus_tfidf, num_topics=20,
id2word=dictionary, passes=2, workers=2)
让我们用 5 个单词来表示前 10 个主题:
topics = lda_model.print_topics(num_words=5)
for i, topic in enumerate(topics[:10]):
print("Train topic {}: {}".format(i, topic))
主题单词以与主题相关的相对权重显示,如下所示:
图 7.9:前 10 个主题,每个主题选择 5 个(最相关)单词
一旦我们提取了主题,我们就可以遍历文档并识别当前文档(在我们的案例中,是评论)中包含哪些主题。在图 7.10中,我们展示了单个文档的占主导地位的主题(以及相对权重)(以下是用以生成选定评论的主题列表的代码):
for index, score in sorted(lda_model[bd5], key=lambda tup: -1*tup[1]):
print("\nScore: {}\t \nTopic: {}".format(score, lda_model.print_topic(index, 5)))
图 7.10:与一个评论相关联(每个都有相对重要性)的主题
我们更喜欢使用pyLDAvis可视化工具来表示主题。在图 7.11中,我们展示了该工具的屏幕截图(在笔记本中,我们为训练数据生成了 20 个主题,并为测试数据单独生成了主题)。图 7.11中的仪表板显示了主题间距离图。在这里,主题的相对维度(或影响力)由圆盘的大小表示,主题的相对距离由它们的相互距离表示。在右侧,对于当前选定的主题(在左侧面板中),我们可以看到每个主题前 30 个最相关的术语。
色彩较浅的圆盘(笔记本中的蓝色)代表整体单词频率。颜色较深的圆盘(笔记本中的红色)代表所选主题内的估计单词频率。我们可以使用滑块来调整相关性指标(在图片中,这设置为1
)。我们可以通过改进预处理步骤(例如,我们可以添加更多特定于该语料库的停用词)、调整字典形成的参数以及控制tfidf
和lda
的参数来进一步细化此分析。由于 LDA 过程的复杂性,我们还通过从训练数据中子采样来减小语料库的大小。
如以下屏幕截图所示,在 pyLDAvis 工具生成的主题建模仪表板的左侧面板中,我们看到主题间距离图——在语料库中主题影响力的相对维度和相对主题的距离:
图 7.11:使用 pyLDAvis 工具生成的主题建模仪表板(左侧面板)
在使用 pyLDAvis 工具生成的主题建模仪表板的右侧面板中,对于选定的主题,我们可以看到每个主题中最相关的 30 个术语,蓝色代表语料库中的整体词频,红色代表所选主题内的估计词频:
图 7.12:使用 pyLDAvis 工具生成的主题建模仪表板(右侧面板)
我们可以对整个语料库进行重复分析,但这将需要比 Kaggle 上可用的计算资源更多的资源。
通过以上内容,我们已经使用lda
方法探索了评论文本语料库中的主题。通过这个程序,我们揭示了文本语料库中的一个隐藏(或潜在)结构。现在我们可以更好地理解不仅单词的频率,而且单词在评论中的关联方式,从而形成评论者讨论的主题。让我们继续从不同的角度探索语料库。我们将对每个评论进行分析,使用 NER 来确定文本中存在哪些类型的概念。然后,我们将开始关注句法元素,并使用 POS 标记来提取名词、动词、形容词和其他词性。
我们回顾这些 NLP 技术的原因有两个。首先,我们希望让你一窥 NLP 中可用的工具和技术之丰富。其次,对于更复杂的机器学习模型,你可以包括使用这些方法导出的特征。例如,除了从文本中提取的其他特征外,你还可以添加通过 NER 或 POS 标记获得的特征。
命名实体识别
让我们在一组评论上执行 NER。NER 是一种信息提取任务,旨在识别和提取非结构化数据(文本)中的命名实体。命名实体包括人名、组织、地理位置、日期和时间、数量和货币。有几种可用的方法来识别和提取命名实体,其中最常用的是spacy
和transformers
。在我们的案例中,我们将使用spacy
来执行 NER。我们更喜欢spacy
,因为它与 transformers 相比需要的资源更少,但仍然能给出良好的结果。在此需要注意的是,spacy
也支持包括英语、葡萄牙语、西班牙语、俄语和中文在内的 23 种语言。
首先,我们使用spacy.load
函数初始化一个nlp
对象:
import spacy
nlp = spacy.load('en_core_web_sm')
这将加载'en_core_web_sm'
(sm
代表小型)spacy
管道,该管道包括tok2vec
、tagger
、parser
、senter
、ner
、attribute_ruler
和lemmatizer
组件。我们不会使用此管道提供的所有功能;我们感兴趣的是nlp
组件。
然后,我们创建了一个评论选择。我们筛选出包含名称 奥巴马
或 特朗普
且字符数少于 100 的文档。出于演示目的,我们不希望操作大型句子;如果我们使用较小的句子进行操作,将更容易跟随演示。下一个代码片段将执行选择:
selected_text = train.loc[train['comment_text'].str.contains("Trump") | train['comment_text'].str.contains("Obama")]
selected_text["len"] = selected_text['comment_text'].apply(lambda x: len(x))
selected_text = selected_text.loc[selected_text.len < 100]
selected_text.shape
我们可以通过两种方式可视化应用 NER 的结果。一种方式是打印出文本中每个识别实体的起始和结束字符以及实体标签。另一种方式是使用来自 spacy
的 displacy
渲染,它将为每个实体添加选定的颜色,并在实体文本旁边添加实体名称(见 图 7.13)。
以下代码用于使用 nlp
提取实体以及使用 displacy
准备可视化。在显示使用 displacy
注释的文本之前,我们打印出每个实体文本,然后是其位置(起始和结束字符位置)和实体标签:
for sentence in selected_text["comment_text"].head(5):
print("\n")
doc = nlp(sentence)
for ent in doc.ents:
print(ent.text, ent.start_char, ent.end_char, ent.label_)
displacy.render(doc, style="ent",jupyter=True)
spacy
nlp
中预定义了多个标签。我们可以用一段简单的代码提取每个标签的含义:
import spacy
nlp = spacy.load("en_core_web_sm")
labels = nlp.get_pipe("ner").labels
for label in labels:
print(f"{label} - {spacy.explain(label)}")
这里是标签列表及其含义:
-
CARDINAL: 不属于其他类型的数字
-
DATE: 绝对或相对日期或时间段
-
EVENT: 命名的飓风、战役、战争、体育赛事等
-
FAC: 建筑物、机场、高速公路、桥梁等
-
GPE: 国家、城市或州
-
LANGUAGE: 任何命名语言
-
LAW: 被制成法律的命名文件
-
LOC: 非 GPE 地点、山脉或水体
-
MONEY: 货币价值,包括单位
-
NORP: 国籍或宗教或政治团体
-
ORDINAL:
第一
、第二
等 -
ORG: 公司、机构、机构等
-
PERCENT: 百分比,包括
%
-
PERSON: 人,包括虚构人物
-
PRODUCT: 物体、车辆、食品等(不包括服务)
-
QUANTITY: 重量或距离等度量
-
TIME: 低于一天的时间
-
WORK_OF_ART: 书籍、歌曲等的标题
图 7.13:使用 spacy 和 displacy 进行 NER 结果的可视化
在前面的截图顶部示例中,Bernie(Sanders)被正确识别为人物(PERSON),而(Donald)Trump 被识别为组织(ORG)。这可能是因为前总统 Trump 在作为商人时,经常将他的名字作为他创立的几个组织的名称的一部分。
在底部示例中,奥巴马(也是一位前总统,在争议性政治辩论中经常成为话题)被正确识别为人物。在两种情况下,我们还展示了提取的实体列表,包括每个识别实体的起始和结束位置。
词性标注
通过 NER 分析,我们识别了各种实体(如人、组织、地点等)的特定名称。这些帮助我们将各种术语与特定的语义组关联起来。我们可以进一步探索评论文本,以便了解每个单词的 POS(如名词或动词),并理解每个短语的句法。
让我们首先使用nltk
(一个替代的nlp
库)从我们在 NER 实验中使用的相同短语小集中提取词性。我们在这里选择nltk
是因为,除了比 spacy 更节省资源外,它还提供了高质量的结果。我们还想能够比较两者的结果(spacy
和nltk
):
for sentence in selected_text["comment_text"].head(5):
print("\n")
tokens = twt().tokenize(sentence)
tags = nltk.pos_tag(tokens, tagset = "universal")
for tag in tags:
print(tag, end=" ")
结果将如下所示:
图 7.14:使用 nltk 进行 POS 标记
我们也可以使用 spacy 执行相同的分析:
for sentence in selected_text["comment_text"].head(5):
print("\n")
doc = nlp(sentence)
for token in doc:
print(token.text, token.pos_, token.ent_type_, end=" | ")
结果将如下所示:
图 7.15:使用 spacy 进行 POS 标记
让我们比较一下图 7.14和图 7.15中的两个输出。这两个库生成的 POS 结果略有不同。其中一些差异是由于实际词性在类别上的映射不同。对于nltk
,单词“is”代表一个AUX
(辅助),而spacy
中的相同“is”则是一个动词。spacy
区分专有名词(人名、地名等)和普通名词(NOUN
),而nltk
则不进行区分。
对于一些具有非标准结构的短语,两个输出都将动词“Go”错误地识别为名词(nltk
)和专有名词(spacy
)。在spacy
的情况下,这是可以预料的,因为“Go”在逗号后面被大写。spacy 区分了并列连词(CCONJ)和从属连词(SCONJ),而nltk
只会识别出存在连词(CONJ)。
使用与我们在前一小节中用于突出 NER 的相同 spacy 库扩展,我们也可以表示短语和段落的句法结构。在图 7.16中,我们展示了一个这样的表示示例。在笔记本中,我们使用带有“dep”(依存)标志的displacy
展示了所有注释(短语集)的可视化。
图 7.16:使用 spacy 进行 POS 标记,并使用依存关系显示词性之间的短语结构
我们看到了如何使用依存关系来显示实体及其各自的类别,以及如何使用相同的函数来显示词性和短语结构。从参考文献 11中汲取灵感,我们扩展了那里给出的代码示例(并从使用nltk
转换为使用spacy
进行 POS 提取,因为 nltk 与 spacy 不完全兼容)以便我们可以以与表示命名实体相同的方式显示词性。
这里给出了从参考文献 11修改后的代码(代码解释将在代码块之后进行):
import re
def visualize_pos(sentence):
colors = {"PRON": "blueviolet",
"VERB": "lightpink",
"NOUN": "turquoise",
"PROPN": "lightgreen",
"ADJ" : "lime",
"ADP" : "khaki",
"ADV" : "orange",
"AUX" : "gold",
"CONJ" : "cornflowerblue",
"CCONJ" : "magenta",
"SCONJ" : "lightmagenta",
"DET" : "forestgreen",
"NUM" : "salmon",
"PRT" : "yellow",
"PUNCT": "lightgrey"}
pos_tags = ["PRON", "VERB", "NOUN", "PROPN", "ADJ", "ADP",
"ADV", "AUX", "CONJ", "CCONJ", "SCONJ", "DET", "NUM", "PRT", "PUNCT"]
# Fix for issues in the original code
sentence = sentence.replace(".", " .")
sentence = sentence.replace("'", "")
# Replace nltk tokenizer with spacy tokenizer and POS tagging
doc = nlp(sentence)
tags = []
for token in doc:
tags.append((token.text, token.pos_))
# Get start and end index (span) for each token
span_generator = twt().span_tokenize(sentence)
spans = [span for span in span_generator]
# Create dictionary with start index, end index,
# pos_tag for each token
ents = []
for tag, span in zip(tags, spans):
if tag[1] in pos_tags:
ents.append({"start" : span[0],
"end" : span[1],
"label" : tag[1] })
doc = {"text" : sentence, "ents" : ents}
options = {"ents" : pos_tags, "colors" : colors}
displacy.render(doc,
style = "ent",
options = options,
manual = True,
)
让我们更好地理解前面的代码。在visualise_pos
函数中,我们首先定义了词性与颜色之间的映射(词性如何被突出显示)。然后,我们定义了我们将考虑的词性。接着,我们使用一些特殊字符的替换来纠正原始代码(来自参考文献 11)中存在的一个错误。我们还使用 spacy 分词器,并在tags
列表中添加了使用nlp
从spacy
提取的每个pos
的文本和词性。然后,我们计算每个pos
的位置,并创建一个字典,包含pos
标记和它们在文本中的位置,以便能够用不同的颜色突出显示它们。最后,我们使用displacy
渲染所有突出显示的pos
的文档。
在图 7.17中,我们展示了将此程序应用于我们的样本评论的结果。现在我们可以更容易地看到 spacy 的一些错误。在第二条评论中,它错误地将第二个“Go”解释为专有名词(PROPN)。这在某种程度上是可以解释的,因为通常在逗号之后,只有专有名词才会用大写字母书写。
图 7.17:使用 spacy 进行词性标注和依赖关系,并使用从参考文献 8 修改的程序来在文本中突出显示词性
我们还可以观察到其他错误。在第一条评论中,“Trump”被标记为名词——即一个简单的名词。术语“republicans”被分类为PROPN,这在美国政治背景下可能是准确的,因为在美国政治中,“Republicans”被视为专有名词。然而,在我们的上下文中,这是不准确的,因为它代表了一个复数形式的简单名词,指代一群倡导共和政府的人。
我们回顾了几种自然语言处理(NLP)技术,这些技术帮助我们更好地理解文本中存在的词语分布、主题、词性(POS)和概念。可选地,我们也可以使用这些技术来生成特征,以便包含在机器学习模型中。
在下一节中,我们将开始分析,目的是为评论分类准备一个监督式 NLP 模型。
准备模型
模型准备,根据我们将要实施的方法,可能更复杂或更简单。在我们的情况下,我们选择从简单的深度学习架构开始构建第一个基线模型(这是比赛时的标准方法),包括一个词嵌入层(使用预训练的词嵌入)和一个或多个双向长短期记忆(LSTM)层。这种架构在这次比赛发生时是一个常见的选择,对于文本分类问题的基线来说仍然是一个好的选择。LSTM代表长短期记忆。它是一种循环神经网络架构,旨在捕捉和记住序列数据中的长期依赖关系。它特别适用于文本分类问题,因为它能够处理和模拟文本序列中的复杂关系和依赖关系。
为了做到这一点,我们需要对评论数据进行一些预处理(在准备构建主题建模模型时我们也进行了预处理)。这次,我们将逐步执行预处理步骤,并监控这些步骤如何影响结果,不是模型的结果,而是表现良好的语言模型的一个先决条件,即词嵌入的词汇覆盖范围。
然后,我们将使用第一个基线模型中的词嵌入来扩展我们模型的泛化能力,以便不在训练集中但在测试集中的单词能够从存在于词嵌入中的单词的邻近性中受益。最后,为了确保我们的方法有效,我们需要预训练的词嵌入具有尽可能大的词汇覆盖范围。因此,我们还将测量词汇覆盖范围并建议改进它的方法。
现在,我们首先构建初始词汇表。
构建词汇表
我们之前使用单词频率、与目标值相关的单词分布以及其他特征、主题建模、命名实体识别(NER)和词性标注(POS tagging)对整个评论语料库的一个子集进行了实验。在接下来的实验中,我们将开始使用整个数据集。我们将使用具有 300 个嵌入大小的词嵌入。
词嵌入是单词的数值表示。它们将单词映射到向量。嵌入大小指的是这些向量的组成部分(或维度)的数量。这个过程使计算机能够理解和比较单词之间的关系。因为所有单词首先都使用词嵌入进行转换(在词嵌入空间中,单词之间的关系由向量之间的关系表示),所以具有相似意义的单词将在词嵌入空间中由对齐的向量表示。
在测试时,新词,不在训练数据中出现的词,也将被表示在词嵌入空间中,并且算法将利用它们与训练数据中存在的其他词的关系。这将增强我们用于文本分类的算法。
此外,我们将字符数(或注释长度)设置为固定值;我们选择了 220 这个维度。对于较短的注释,我们将填充注释序列(即添加空格),而对于较长的注释序列,我们将截断它们(到 220 个字符)。这个程序将确保我们会有相同维度的机器学习模型的输入。让我们首先定义一个用于构建词汇表的函数。在构建这些函数时,我们使用了来自参考文献 12和13的来源。
以下代码用于构建词汇表(即注释中存在的单词的语料库)。我们对每个注释进行分割,并将所有数据收集到一个句子列表中。然后我们解析所有这些句子以创建一个包含词汇的字典。每次解析到的单词如果在字典中作为键存在,我们就增加与该键关联的值。我们得到的是一个包含词汇表中每个单词计数(或总体频率)的词汇表字典:
def build_vocabulary(texts):
"""
Build the vocabulary from the corpus
Credits to: [9] [10]
Args:
texts: list of list of words
Returns:
dictionary of words and their count
"""
sentences = texts.apply(lambda x: x.split()).values
vocab = {}
for sentence in tqdm(sentences):
for word in sentence:
try:
vocab[word] += 1
except KeyError:
vocab[word] = 1
return vocab
我们通过连接train
和test
创建整体词汇表:
# populate the vocabulary
df = pd.concat([train ,test], sort=False)
vocabulary = build_vocabulary(df['comment_text'])
我们可以检查词汇表中的前 10 个元素,以了解这个词汇表的样子:
# display the first 10 elements and their count
print({k: vocabulary[k] for k in list(vocabulary)[:10]})
以下图像显示了运行前面代码的结果。它显示了文本中最常用的单词。正如预期的那样,最常用的单词是英语中最常用的一些单词。
图 7.18:未经任何预处理的词汇表 – 大写和小写单词,以及可能拼写错误的表达
我们将在每次执行额外的(有时重复的)文本转换时重复使用之前引入的build_vocabulary
函数。我们执行连续的文本转换,以确保在使用预训练的词嵌入时,我们对注释中词汇表的预训练词嵌入中的单词有良好的覆盖。更大的覆盖范围将确保我们构建的模型有更高的准确性。让我们继续加载一些预训练的词嵌入。
嵌入索引和嵌入矩阵
我们现在将构建一个字典,以词嵌入中的单词作为键,以它们的嵌入表示的数组作为值。我们称这个字典为嵌入索引。然后我们也将构建嵌入矩阵,它是嵌入的矩阵表示。我们将使用 GloVe 的预训练嵌入(300 维)进行我们的实验。GloVe代表全局词表示向量,是一种无监督算法,它产生词嵌入。它通过分析非常大的文本语料库中的全局文本统计来创建向量表示,并捕捉单词之间的语义关系。
以下代码加载预训练的词嵌入:
def load_embeddings(file):
"""
Load the embeddings
Credits to: [9] [10]
Args:
file: embeddings file
Returns:
embedding index
"""
def get_coefs(word,*arr):
return word, np.asarray(arr, dtype='float32')
embeddings_index = dict(get_coefs(*o.split(" ")) for o in open(file, encoding='latin'))
return embeddings_index
%%time
GLOVE_PATH = '../input/glove840b300dtxt/'
print("Extracting GloVe embedding started")
embed_glove = load_embeddings(os.path.join(GLOVE_PATH,'glove.840B.300d.txt'))
print("Embedding completed")
获得的嵌入结构大小为 2.19 百万项。接下来,我们将使用我们刚刚创建的词索引和嵌入索引来创建嵌入矩阵:
def embedding_matrix(word_index, embeddings_index):
'''
Create the embedding matrix
credits to: [9] [10]
Args:
word_index: word index (from vocabulary)
embedding_index: embedding index (from embeddings file)
Returns:
embedding matrix
'''
all_embs = np.stack(embeddings_index.values())
emb_mean, emb_std = all_embs.mean(), all_embs.std()
EMBED_SIZE = all_embs.shape[1]
nb_words = min(MAX_FEATURES, len(word_index))
embedding_matrix = np.random.normal(emb_mean, emb_std, (nb_words, EMBED_SIZE))
for word, i in tqdm(word_index.items()):
if i >= MAX_FEATURES:
continue
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector
return embedding_matrix
我们使用MAX_FEATURES
参数来限制嵌入矩阵的维度。
检查词汇覆盖率
我们介绍了读取词嵌入和计算嵌入矩阵的功能。现在我们将继续介绍用于评估词嵌入中词汇覆盖率的函数。词汇覆盖率越大,我们构建的模型精度越好。
为了检查嵌入对词汇的覆盖率,我们将使用以下函数:
def check_coverage(vocab, embeddings_index):
'''
Check the vocabulary coverage by the embedding terms
credits to: [9] [10]
Args:
vocab: vocabulary
embedding_index: embedding index (from embeddings file)
Returns:
list of unknown words; also prints the vocabulary coverage of embeddings and
the % of comments text covered by the embeddings
'''
known_words = {}
unknown_words = {}
nb_known_words = 0
nb_unknown_words = 0
for word in tqdm(vocab.keys()):
try:
known_words[word] = embeddings_index[word]
nb_known_words += vocab[word]
except:
unknown_words[word] = vocab[word]
nb_unknown_words += vocab[word]
pass
print('Found embeddings for {:.3%} of vocabulary'.format(len(known_words)/len(vocab)))
print('Found embeddings for {:.3%} of all text'.format(nb_known_words/(nb_known_words + nb_unknown_words)))
unknown_words = sorted(unknown_words.items(), key=operator.itemgetter(1))[::-1]
return unknown_words
上述代码遍历所有词汇项(即评论文本中存在的单词)并计算未知单词(即文本中但不在嵌入单词列表中的单词)。然后,它计算词汇中存在于词嵌入索引中的单词百分比。这个百分比以两种方式计算:对词汇中的每个单词进行无权重计算,以及根据文本中的单词频率进行加权计算。
我们将反复应用此函数,在每个预处理步骤之后检查词汇覆盖率。让我们从检查初始词汇的词汇覆盖率开始,此时我们还没有对评论文本进行任何预处理。
逐步提高词汇覆盖率
我们应用check_coverage
函数来检查词汇覆盖率,传递两个参数:词汇和嵌入矩阵。在以下符号中,oov代表词汇外:
print("Verify the initial vocabulary coverage")
oov_glove = check_coverage(vocabulary, embed_glove)
第一次迭代的成果并不理想。尽管我们覆盖了几乎 90%的文本,但词汇表中只有 15.5%的单词被词嵌入覆盖:
图 7.19:词汇覆盖率 – 第一次迭代
我们还可以查看未覆盖术语的列表。因为在oov_glove
中,我们按在语料库中出现的次数降序存储了未覆盖的术语,因此,通过选择此列表中的前几个术语,我们可以看到未包含在词嵌入中的最重要的单词。在图 7.20中,我们显示了此列表中的前 10 个术语——前 10 个未覆盖的单词。在这里,“未覆盖”指的是出现在词汇表中的单词(存在于评论文本中),但不在词嵌入索引中(不在预训练的词嵌入中):
图 7.20:第一轮迭代中,词汇表中未覆盖的词汇中最常见的 10 个单词
通过快速检查图 7.20中的列表,我们看到一些常用词要么是缩写词,要么是非标准口语英语的口语化形式。在网上评论中看到这样的形式是正常的。我们将执行几个预处理步骤,试图通过纠正我们发现的问题来提高词汇覆盖率。在每一步之后,我们还将再次测量词汇覆盖率。
转换为小写
我们将首先将所有文本转换为小写,并将其添加到词汇表中。在词嵌入中,单词将全部为小写:
def add_lower(embedding, vocab):
'''
Add lower case words
credits to: [9] [10]
Args:
embedding: embedding matrix
vocab: vocabulary
Returns:
None
modify the embeddings to include the lower case from vocabulary
'''
count = 0
for word in tqdm(vocab):
if word in embedding and word.lower() not in embedding:
embedding[word.lower()] = embedding[word]
count += 1
print(f"Added {count} words to embedding")
我们将此小写转换应用于训练集和测试集,然后我们重新构建词汇表并计算词汇覆盖率:
train['comment_text'] = train['comment_text'].apply(lambda x: x.lower())
test['comment_text'] = test['comment_text'].apply(lambda x: x.lower())
print("Check coverage for vocabulary with lower case")
oov_glove = check_coverage(vocabulary, embed_glove)
add_lower(embed_glove, vocabulary) # operates on the same vocabulary
oov_glove = check_coverage(vocabulary, embed_glove)
图 7.21显示了应用小写转换后新的词汇覆盖率:
图 7.21:词汇覆盖率——第二次迭代,所有单词的小写
我们可以在单词百分比和文本百分比覆盖率上观察到一些小的改进。让我们继续通过从注释文本中移除缩写词来继续操作。
移除缩写词
接下来,我们将移除缩写词。这些是单词和表达式的修改形式。我们将使用一个预定义的通常遇到的缩写词字典。这些将映射到存在于嵌入中的单词上。由于空间有限,我们在这里只包括缩写词字典中的一些项目示例,但整个资源可以在与本章相关的笔记本中找到:
contraction_mapping = {"ain't": "is not", "aren't": "are not","can't": "cannot", "'cause": "because", "could've": "could have", "couldn't": "could not", "didn't": "did not", "doesn't": "does not", "don't": "do not", "hadn't": "had not", "hasn't": "has not",
...
}
使用以下函数,我们可以获取 GloVe 嵌入中已知缩写词的列表:
def known_contractions(embed):
'''
Add know contractions
credits to: [9] [10]
Args:
embed: embedding matrix
Returns:
known contractions (from embeddings)
'''
known = []
for contract in tqdm(contraction_mapping):
if contract in embed:
known.append(contract)
return known
我们可以使用下一个函数从词汇表中清除已知的缩写词——即,使用缩写词字典来替换它们:
def clean_contractions(text, mapping):
'''
Clean the contractions
credits to: [9] [10]
Args:
text: current text
mapping: contraction mappings
Returns: modify the comments to use the base form from contraction mapping
'''
specials = ["’", "‘", "´", "`"]
for s in specials:
text = text.replace(s, "'")
text = ' '.join([mapping[t] if t in mapping else t for t in text.split(" ")])
return text
在我们对训练集和测试集应用clean_contractions
并再次应用构建词汇表和测量词汇覆盖率的函数后,我们得到了关于词汇覆盖率的新统计数据:
图 7.22:词汇覆盖率——第三轮迭代,使用缩写词字典替换缩写词后的结果
通过检查没有覆盖的表达式并增强它,使其等于语料库中未覆盖的表达式与单词或单词组相等,可以进一步细化缩写字典。每个单词都表示在嵌入向量中。
移除标点和特殊字符
接下来,我们将移除标点和特殊字符。以下列表和函数对此步骤很有用。首先,我们列出未知标点:
punct_mapping = "/-'?!.,#$%\'()*+-/:;<=>@[\\]^_`{|}~" + '""“”’' + '∞θ÷α•à−βø³π'₹´°£€\×™√²—–&'
punct_mapping += '©^®` <→°€™' ♥←×§″′Â█½à…"✶"–●â►−¢²¬░¶↑±¿▾═¦║―¥▓—'─▒: ¼⊕▼▪†■’▀¨▄♫⭐é¯♦¤▲踾Ñ'∞∙)↓、│(»,♪╩╚³・╦╣╔╗▬❤ïØ¹≤‡√'
def unknown_punct(embed, punct):
'''
Find the unknown punctuation
credits to: [9] [10]
Args:
embed: embedding matrix
punct: punctuation
Returns:
unknown punctuation
'''
unknown = ''
for p in punct:
if p not in embed:
unknown += p
unknown += ' '
return unknown
然后我们清理特殊字符和标点:
puncts = {"‘": "'", "´": "'", "°": "", "€": "euro", "—": "-", "–": "-", "’": "'", "_": "-", "`": "'", '“': '"', '”': '"', '“': '"', "£": "pound",
'∞': 'infinity', 'θ': 'theta', '÷': '/', 'α': 'alpha', '•': '.', 'à': 'a', '−': '-', 'β': 'beta', '∅': '', '³': '3', 'π': 'pi', '…': ' '}
def clean_special_chars(text, punct, mapping):
'''
Clean special characters
credits to: [9] [10]
Args:
text: current text
punct: punctuation
mapping: punctuation mapping
Returns:
cleaned text
'''
for p in mapping:
text = text.replace(p, mapping[p])
for p in punct:
text = text.replace(p, f' {p} ')
return text
让我们在图 7.23中再次检查词汇覆盖率。这次,通过词嵌入,我们将词汇覆盖率从大约 15%提高到 54%。此外,文本覆盖率从 90%提高到 99.7%。
图 7.23:词汇覆盖率 – 第四次迭代,在清理标点和特殊字符之后
查看未覆盖的前 20 个单词,我们发现有一些带重音的小词、特殊字符和习语。我们将标点字典扩展到包括最频繁的特殊字符,并在我们再次运行build_vocabulary
和check_coverage
之后,我们得到了词汇覆盖率的新状态:
more_puncts = {'▀': '.', '▄': '.', 'é': 'e', 'è': 'e', 'ï': 'i','⭐': 'star', 'ᴀ': 'A', 'ᴀɴᴅ': 'and', '»': ' '}
train['comment_text'] = train['comment_text'].apply(lambda x: clean_special_chars(x, punct_mapping, more_puncts))
test['comment_text'] = test['comment_text'].apply(lambda x: clean_special_chars(x, punct_mapping, more_puncts))
%%time
df = pd.concat([train ,test], sort=False)
vocab = build_vocabulary(df['comment_text'])
print("Check coverage after additional punctuation replacement")
oov_glove = check_coverage(vocab, embed_glove)
这次有一个微小的改进,但我们可以继续处理频繁的表达式或频繁的特殊字符替换,直到我们得到显著的改进。
通过添加额外的嵌入源到当前的预训练嵌入中,可以进一步改善注释语料库的词汇覆盖率。让我们试试这个。我们使用了来自GloVe
的预训练嵌入。我们也可以使用 Facebook 的FastText
。FastText
是一个非常实用的行业标准库,在许多公司的日常搜索和推荐引擎中广泛使用。让我们加载嵌入并使用组合嵌入向量重新创建嵌入索引。
在我们将两个词嵌入字典合并后,一个包含 2.19 百万个条目,另一个包含 2.0 百万个条目(两者都具有 300 维的向量维度),我们得到了一个包含 2.8 百万个条目的字典(由于两个字典中有很多共同词汇)。然后我们重新计算了词汇覆盖率。在图 7.24中,我们展示了这一操作的结果。
图 7.24:词汇覆盖率 – 第五次迭代,在将 FastText 预训练词嵌入添加到初始 GloVe 嵌入字典之后
总结一下我们的过程,我们的意图是构建一个基于预训练词嵌入的基线解决方案。我们介绍了两种预训练词嵌入算法,GloVe
和 FastText
。预训练意味着我们使用了已经训练好的算法;我们没有从我们的数据集的评论语料库中计算词嵌入。为了有效,我们需要确保我们用这些词嵌入覆盖了评论文本词汇表的良好范围。最初,覆盖率相当低(词汇表的 15% 和整个文本的 86%)。通过转换为小写、删除缩写、删除标点符号和替换特殊字符,我们逐渐提高了这些统计数据。在最后一步,我们通过添加来自替代源的预训练嵌入来扩展嵌入字典。最终,我们能够确保词汇表的 56% 覆盖率和整个文本的 99.75% 覆盖率。
下一步是创建一个基线模型,并在单独的笔记本中完成。我们只会重用当前笔记本中实验的一部分函数。
构建基线模型
这些天,每个人至少都会通过微调 Transformer 架构来构建一个基线模型。自从 2017 年的论文《Attention Is All You Need》(参考文献 14)以来,这些解决方案的性能一直在持续提升,对于像 Jigsaw Unintended Bias in Toxicity Classification 这样的比赛,一个基于 Transformer 的最新解决方案可能会轻易地将你带入金牌区域。
在这个练习中,我们将从一个更经典的基线开始。这个解决方案的核心是基于 Christof Henkel(Kaggle 昵称:Dieter)、Ane Berasategi(Kaggle 昵称:Ane)、Andrew Lukyanenko(Kaggle 昵称:Artgor)、Thousandvoices(Kaggle 昵称)和 Tanrei(Kaggle 昵称)的贡献;参见参考文献 12、13、15、16、17 和 18。
该解决方案包括四个步骤。在第一步,我们将训练数据和测试数据加载为 pandas
数据集,然后对这两个数据集进行预处理。预处理主要基于我们之前执行的预处理步骤,因此我们在这里不会重复这些步骤。
在第二步,我们执行分词并准备数据以呈现给模型。分词过程如下所示(我们这里不展示整个过程):
logger.info('Fitting tokenizer')
tokenizer = Tokenizer()
tokenizer.fit_on_texts(list(train[COMMENT_TEXT_COL]) + list(test[COMMENT_TEXT_COL]))
word_index = tokenizer.word_index
X_train = tokenizer.texts_to_sequences(list(train[COMMENT_TEXT_COL]))
X_test = tokenizer.texts_to_sequences(list(test[COMMENT_TEXT_COL]))
X_train = pad_sequences(X_train, maxlen=MAX_LEN)
X_test = pad_sequences(X_test, maxlen=MAX_LEN)
我们在这里使用了一个基本的分词器来自 keras.preprocessing.text
。分词后,每个输入序列都会用预定义的 MAX_LEN
进行填充,这个长度是根据整个评论语料库的平均/中位长度以及可用的内存和运行时限制选定的。
在第三步,我们构建嵌入矩阵和模型结构。构建嵌入矩阵的代码主要基于我们在前几节中已经介绍过的过程。在这里,我们只是将其系统化:
def build_embedding_matrix(word_index, path):
'''
Build embeddings
'''
logger.info('Build embedding matrix')
embedding_index = load_embeddings(path)
embedding_matrix = np.zeros((len(word_index) + 1, EMB_MAX_FEAT))
for word, i in word_index.items():
try:
embedding_matrix[i] = embedding_index[word]
except KeyError:
pass
except:
embedding_matrix[i] = embeddings_index["unknown"]
del embedding_index
gc.collect()
return embedding_matrix
def build_embeddings(word_index):
'''
Build embeddings
'''
logger.info('Load and build embeddings')
embedding_matrix = np.concatenate(
[build_embedding_matrix(word_index, f) for f in EMB_PATHS], axis=-1)
return embedding_matrix
该模型是一个具有词嵌入层、SpatialDropout1D
层、两个双向 LSTM 层、GlobalMaxPooling1D
与GlobalAveragePooling1D
的拼接、两个具有'relu'
激活的密集层,以及一个具有'sigmoid'
激活的目标输出的密集层的深度学习架构。
在词嵌入层中,输入被转换,使得每个词都由其对应的向量表示。经过这种转换后,模型捕捉到了输入中词语之间的语义距离信息。SpatialDropout1D
层通过在训练过程中随机停用神经元来帮助防止过拟合(系数给出了每个 epoch 停用神经元的百分比)。双向 LSTM 层的作用是处理输入序列的前向和反向,增强上下文理解以获得更好的预测。GlobalAveragePooling1D
层的作用是计算整个序列中每个特征的均值,在 1D(顺序)数据中降低维度同时保留关键信息。这相当于揭示了序列的潜在表示。密集层的输出是模型的预测。有关实现的更多细节,请参阅参考文献 17和参考文献 18:
def build_model(embedding_matrix, num_aux_targets, loss_weight):
'''
Build model
'''
logger.info('Build model')
words = Input(shape=(MAX_LEN,))
x = Embedding(*embedding_matrix.shape, weights=[embedding_matrix], trainable=False)(words)
x = SpatialDropout1D(0.3)(x)
x = Bidirectional(CuDNNLSTM(LSTM_UNITS, return_sequences=True))(x)
x = Bidirectional(CuDNNLSTM(LSTM_UNITS, return_sequences=True))(x)
hidden = concatenate([GlobalMaxPooling1D()(x),GlobalAveragePooling1D()(x),])
hidden = add([hidden, Dense(DENSE_HIDDEN_UNITS, activation='relu')(hidden)])
hidden = add([hidden, Dense(DENSE_HIDDEN_UNITS, activation='relu')(hidden)])
result = Dense(1, activation='sigmoid')(hidden)
aux_result = Dense(num_aux_targets, activation='sigmoid')(hidden)
model = Model(inputs=words, outputs=[result, aux_result])
model.compile(loss=[custom_loss,'binary_crossentropy'], loss_weights=[loss_weight, 1.0], optimizer='adam')
return model
在第四步,我们进行训练,准备提交,并提交。为了减少运行时使用的内存,我们使用临时存储,并在删除未使用的分配数据后进行垃圾回收。我们运行模型两次,指定数量的NUM_EPOCHS
(代表训练数据通过算法的一个完整遍历),然后使用可变权重平均测试预测。然后我们提交预测:
def run_model(X_train, y_train, y_aux_train, embedding_matrix, word_index, loss_weight):
'''
Run model
'''
logger.info('Run model')
checkpoint_predictions = []
weights = []
for model_idx in range(NUM_MODELS):
model = build_model(embedding_matrix, y_aux_train.shape[-1], loss_weight)
for global_epoch in range(NUM_EPOCHS):
model.fit(
X_train, [y_train, y_aux_train],
batch_size=BATCH_SIZE, epochs=1, verbose=1,
callbacks=[LearningRateScheduler(lambda epoch: 1.1e-3 * (0.55 ** global_epoch))]
)
with open('temporary.pickle', mode='rb') as f:
X_test = pickle.load(f) # use temporary file to reduce memory
checkpoint_predictions.append(model.predict(X_test, batch_size=1024)[0].flatten())
del X_test
gc.collect()
weights.append(2 ** global_epoch)
del model
gc.collect()
preds = np.average(checkpoint_predictions, weights=weights, axis=0)
return preds
def submit(sub_preds):
logger.info('Prepare submission')
submission = pd.read_csv(os.path.join(JIGSAW_PATH,'sample_submission.csv'), index_col='id')
submission['prediction'] = sub_preds
submission.reset_index(drop=False, inplace=True)
submission.to_csv('submission.csv', index=False)
使用这种解决方案(完整代码请见参考文献 16),我们可以通过延迟提交获得核心得分 0.9328,从而在私有排行榜的上半部分获得排名。接下来,我们将展示如何通过使用基于 Transformer 的解决方案,我们可以获得更高的分数,进入银牌甚至金牌区域。
基于 Transformer 的解决方案
在竞赛期间,BERT 和一些其他 Transformer 模型已经可用,并提供了几个高分解决方案。在这里,我们不会尝试复制它们,但我们会指出最易于实现的实现。
在参考文献 20中,齐申·哈结合了几种解决方案,包括 BERT-Small V2、BERT-Large V2、XLNet 和 GPT-2(使用竞赛数据作为数据集进行微调的模型)来获得 0.94656 的私有排行榜得分(延迟提交),这将使你进入前 10 名(包括金牌和奖项区域)。
仅使用 BERT-Small 模型(参见参考文献 21)的解决方案将产生 0.94295 的私有排行榜分数。使用 BERT-Large 模型(参见参考文献 22)将导致 0.94388 的私有排行榜分数。这两个解决方案都将位于银牌区域(在私有排行榜中分别位于 130 和 80 左右,作为晚提交)。
摘要
在本章中,我们学习了如何处理文本数据,使用各种方法来探索这类数据。我们首先分析了我们的目标和文本数据,对文本数据进行预处理以便将其包含在机器学习模型中。我们还探讨了各种 NLP 工具和技术,包括主题建模、命名实体识别(NER)和词性标注(POS tagging),然后准备文本以构建基线模型,通过迭代过程逐步提高目标集(在这种情况下,目标是提高竞赛数据集中文本语料库中词汇的词嵌入覆盖范围)的数据质量。
我们介绍了并讨论了一个基线模型(基于几位 Kaggle 贡献者的工作)。这个基线模型架构包括一个词嵌入层和双向 LSTM 层。最后,我们查看了一些基于 Transformer 架构的最先进解决方案,无论是作为单一模型还是组合使用,以获得排行榜上部的分数(银牌和金牌区域)。
在下一章中,我们将开始处理信号数据。我们将介绍各种信号模态(声音、图像、视频、实验或传感器数据)特定的数据格式。我们将分析来自LANL 地震预测 Kaggle 竞赛的数据。
参考文献
-
Jigsaw 毒性分类中的无意偏差,Kaggle 竞赛数据集:
www.kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification/
-
Aja Bogdanoff,告别文明评论,Medium:
medium.com/@aja_15265/saying-goodbye-to-civil-comments-41859d3a2b1d
-
Gabriel Preda,Jigsaw 评论文本探索:
github.com/PacktPublishing/Developing-Kaggle-Notebooks/blob/develop/Chapter-07/jigsaw-comments-text-exploration.ipynb
-
Gabriel Preda,Jigsaw 简单基线:
github.com/PacktPublishing/Developing-Kaggle-Notebooks/blob/develop/Chapter-07/jigsaw-simple-baseline.ipynb
-
Susan Li, Python 中的主题建模和潜在狄利克雷分配(LDA):
towardsdatascience.com/topic-modeling-and-latent-dirichlet-allocation-in-python-9bf156893c24
-
Aneesha Bakharia, 提高主题模型的解释能力:
towardsdatascience.com/improving-the-interpretation-of-topic-models-87fd2ee3847d
-
Carson Sievert, Kenneth Shirley, LDAvis:一种可视化和解释主题的方法:
www.aclweb.org/anthology/W14-3110
-
Lucia Dosin, 主题建模实验 – PyLDAvis:
www.objectorientedsubject.net/2018/08/experiments-on-topic-modeling-pyldavis/
-
Renato Aranha, 在 Elon 推文上的主题建模(LDA):
www.kaggle.com/errearanhas/topic-modelling-lda-on-elon-tweets
-
潜在狄利克雷分配,维基百科:
en.wikipedia.org/wiki/Latent_Dirichlet_allocation
-
Leonie Monigatti, 使用 NLTK 和 SpaCy 可视化词性标签:
towardsdatascience.com/visualizing-part-of-speech-tags-with-nltk-and-spacy-42056fcd777e
-
Ane, Quora 预处理 + 模型:
www.kaggle.com/anebzt/quora-preprocessing-model
-
Christof Henkel (Dieter), 如何:在使用嵌入时进行预处理:
www.kaggle.com/christofhenkel/how-to-preprocessing-when-using-embeddings
-
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser, Illia Polosukhin, 注意力即是全部:
arxiv.org/abs/1706.03762
-
Christof Henkel (Dieter), keras 基线 lstm + attention 5 折:
www.kaggle.com/christofhenkel/keras-baseline-lstm-attention-5-fold
-
Andrew Lukyanenko, 在 keras 上使用 CNN 的折叠:
www.kaggle.com/code/artgor/cnn-in-keras-on-folds
-
Thousandvoices, 简单 LSTM:
www.kaggle.com/code/thousandvoices/simple-lstm/s
-
Tanrei, 使用身份参数的简单 LSTM 解决方案:
www.kaggle.com/code/tanreinama/simple-lstm-using-identity-parameters-solution
-
Gabriel Preda, Jigsaw Simple Baseline:
www.kaggle.com/code/gpreda/jigsaw-simple-baseline
-
Qishen Ha, Jigsaw_predict:
www.kaggle.com/code/haqishen/jigsaw-predict/
-
Gabriel Preda, Jigsaw_predict_BERT_small:
www.kaggle.com/code/gpreda/jigsaw-predict-bert-small
-
Gabriel Preda, Jigsaw_predict_BERT_large:
www.kaggle.com/code/gpreda/jigsaw-predict-bert-large
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人相聚,并在以下地点与超过 5000 名成员一起学习:
第八章:分析声学信号以预测下一次模拟地震
在前面的章节中,我们探讨了基本的表格格式数据,包括分类、有序和数值数据,以及文本、地理坐标和图像。当前章节将我们的关注点转向不同的数据类别,特别是模拟或实验信号数据。这种数据类型通常以多种格式出现,而不仅仅是标准的 CSV 文件格式。
我们的主要案例研究将是LANL 地震预测 Kaggle 竞赛的数据(参见参考文献 1)。我为此竞赛贡献了一个广为人知且经常被分叉的笔记本,名为LANL 地震 EDA 和预测(参见参考文献 2),它将作为本章主要笔记本的基础资源。然后我们将深入研究特征工程,采用各种对开发竞赛预测模型至关重要的信号分析技术。我们的目标是构建一个初始模型,该模型可以预测竞赛的目标变量:故障时间,即下一次模拟实验室地震前的剩余时间。
在地震预测领域的的研究表明,在地震发生之前,板块的运动会在低频声学频谱中产生信号。通过研究这些信号,研究人员试图理解信号的轮廓与故障(即地震)发生时刻之间的关系。在实验室中,模拟板块的滑动和剪切。这个竞赛使用实验室测量数据,包括声学信号以及故障发生的时间。
总结来说,本章将涵盖以下主题:
-
用于各种信号数据的数据格式
-
探索LANL 地震预测 Kaggle 竞赛数据
-
特征工程
-
训练LANL 地震预测竞赛的模型
介绍 LANL 地震预测竞赛
LANL 地震预测竞赛的核心是利用地震信号来确定实验室诱导地震的确切时间。目前,预测自然地震仍然超出了我们的科学知识和技术能力。科学家理想的情景是预测此类事件的时间、地点和震级。
然而,在高度控制的模拟环境中创造的模拟地震,模仿了真实的地震活动。这些模拟允许使用在自然环境中观察到的相同类型的信号来预测实验室生成的地震。在这场比赛中,参与者使用声学数据输入信号来估计下一次人工地震发生的时间,如参考文献 3中详细说明。挑战在于预测地震的时间,解决地震预测中的三个关键未知因素之一:它将在何时发生,将在哪里发生,以及它将有多强大。
训练数据是一个包含两列的单个文件:声学信号幅度和时间至失效。测试数据由多个文件组成(总共 2,526 个),包含声学信号幅度段,我们将需要预测每个段的时间至失效。一个样本提交文件包含一个列,即段 ID seg_id
和要预测的值:time_to_failure
。
竞赛者需要使用训练文件中的声学信号和时间至失效数据来训练他们的模型,并预测测试文件夹中每个文件的每个段的时间至失效。这些竞赛数据格式非常方便,即逗号分隔值(CSV)格式,但这不是必需的。Kaggle 上其他带有信号数据的竞赛或数据集使用不同的、不太常见的格式。因为本章是关于分析信号数据的,所以这里是回顾这种格式的正确位置。让我们首先了解一下这些格式中的一些。
信号数据的格式
在 Kaggle 上举办的几场比赛使用了声音数据作为常规表格特征的补充。2021 年、2022 年和 2023 年,康奈尔鸟类实验室的 BirdCLEF(LifeCLEF 鸟类识别挑战)组织了三场比赛,用于从鸟鸣样本中预测鸟种(参见参考文献 4以了解这些比赛中的一个示例)。这些比赛使用的格式是.ogg
。.ogg
格式用于以更少的带宽存储音频数据,技术上被认为优于.mp3
格式。
我们可以使用librosa
库(参见参考文献 5)读取这些类型的文件格式。以下代码可以用来加载一个.ogg
文件并显示声音波形:
import matplotlib.pyplot as plt
import librosa
def display_sound_wave(sound_path=None,
text="Test",
color="green"):
"""
Display a sound wave
Args
sound_path: path to the sound file
text: text to display
color: color for text to display
Returns
None
"""
if not sound_path:
return
y_sound, sr_sound = librosa.load(sound_path)
audio_sound, _ = librosa.effects.trim(y_sound)
fig, ax = plt.subplots(1, figsize = (16, 3))
fig.suptitle(f'Sound Wave: {text}', fontsize=12)
librosa.display.waveshow(y = audio_sound, sr = sr_sound, color = color)
当librosa
库加载音频声音时,将返回一个时间序列,包含浮点数值(参见参考文献 6)。它不仅支持.ogg
格式;它还可以与 soundfile 或 Audioread 支持的任何代码一起工作。默认采样率为 22050,但也可以在加载时设置,使用参数sr。在加载音频波形时还可以使用的其他参数是偏移量和持续时间(两者都以秒为单位 - 一起,它们允许您选择要加载的声音波形的时问间隔)。
在 BirdCLEF 竞赛的早期版本中,康奈尔鸟鸣识别(见参考文献 7),数据集中的音频声音以.mp3
格式给出。对于这种格式,我们可以使用 librosa 来加载、转换或可视化声音波。波形音频文件格式(或WAV),另一种常用格式,也可以使用 librosa 加载。
对于.wav
格式,我们可以使用scipy.io
模块的wavfile
来加载数据。以下代码将加载并显示一个.wav
格式的文件。在这种情况下,振幅没有缩放到-1:1 的区间(最大值为 32K):
import matplotlib.pyplot as plt
from scipy.io import wavfile
def display_wavefile(sound_path=None,
text="Test",
color="green"):
"""
Display a sound wave - load using wavefile
sr: sample rate
y_sound: sound samples
Args
sound_path: path to the sound file
text: text to display
color: color for text to display
Returns
None
"""
if not sound_path:
return
sr_sound, y_sound = wavfile.load(sound_path)
fig, ax = plt.subplots(1, figsize = (16, 3))
fig.suptitle(f'Sound Wave: {text}', fontsize=12)
ax.plot(np.linspace(0, sr_sound/len(y_sound), sr_sound), y_sound)
信号,不仅仅是音频信号,也可以存储在.npy
或.npz
格式中,这两种格式都是numpy
格式,用于存储数组数据。这些格式可以使用numpy
函数加载,如下面的代码片段所示。对于.npy
格式,这将加载一个多列数组:
import numpy as np
f = np.load('data_path/file.npy', allow_pickle=True)
columns_, data_ = f
data_df = pd.DataFrame(data_, columns = columns_)
对于.npz
格式,以下代码将加载一个类似的结构,之前已压缩(只有一个文件):
import numpy as np
f = np.load('data_path/file.npz', allow_pickle=True)
columns_, data_ = f['arr_0']
data_df = pd.DataFrame(data_, columns = columns_)
对于存储在.rds
格式中的数据,这是一种用于保存数据的 R 特定格式,我们可以使用以下代码加载数据:
!pip install pyreadr
import pyreadr
f = pyreadr.read_r('data_path/file.rds')
data_df = f[None]
for CO, focusing on the COCL dimension (Column Burden kg m-2), and includes values for latitude, longitude, and time:
from netCDF4 import Dataset
data = Dataset(file_path, more="r")
lons = data.variables['lon'][:]
lats = data.variables['lat'][:]
time = data.variables['time'][:]
COCL = data.variables['COCL'][:,:,:]; COCL = COCL[0,:,:]
更多详情,请参阅参考文献 9。现在,让我们回到我们的竞赛数据,它以 CSV 格式存储,尽管它代表音频信号(声波),正如我们之前所阐明的。
探索我们的竞赛数据
LANL 地震预测
数据集包含以下数据:
-
一个只有两列的
train.csv
文件:-
acoustic_data
: 这是声学信号的振幅。 -
time_to_failure
: 这是指当前数据段对应的故障时间。
-
-
一个包含 2,624 个文件的小段声学数据的测试文件夹。
-
一个
sample_submission.csv
文件;对于每个测试文件,参赛者需要提供一个故障时间的估计。
训练数据(9.56 GB)包含 6.92 亿行。训练数据中样本的实际时间常数是由time_to_failure
值的连续变化引起的。声学数据是整数,从-5,515 到 5,444,平均值为 4.52,标准差为 10.7(值在 0 附近波动)。time_to_failure
值是实数,范围从 0 到 16,平均值为 5.68,标准差为 3.67。为了减少训练数据的内存占用,我们以减少的维度读取声学数据和time_to_failure
:
%%time
train_df = pd.read_csv(os.path.join(PATH,'train.csv'), dtype={'acoustic_data': np.int16, 'time_to_failure': np.float32})
让我们检查training
数据中的第一个值。我们不会使用所有的time_to_failure
数据(只有与时间间隔结束相关的值,我们将对这些值进行聚合以获取声学数据);因此,将时间到故障的大小从双精度浮点数缩小到浮点数不是很重要:
图 8.1. 训练数据中的第一行数据
让我们在同一张图上可视化声音信号值和故障时间。我们将使用 1/100 的子采样率(每 100 行取一个样本)来表示完整训练数据(参见图 8.2)。我们将使用以下代码来表示这些图表:
def plot_acc_ttf_data(idx, train_ad_sample_df, train_ttf_sample_df, title="Acoustic data and time to failure: 1% sampled data"):
"""
Plot acoustic data and time to failure
Args:
train_ad_sample_df: train acoustic data sample
train_ttf_sample_df: train time to failure data sample
title: title of the plot
Returns:
None
"""
fig, ax1 = plt.subplots(figsize=(12, 8))
plt.title(title)
plt.plot(idx, train_ad_sample_df, color='r')
ax1.set_ylabel('acoustic data', color='r')
plt.legend(['acoustic data'], loc=(0.01, 0.95))
ax2 = ax1.twinx()
plt.plot(idx, train_ttf_sample_df, color='b')
ax2.set_ylabel('time to failure', color='b')
plt.legend(['time to failure'], loc=(0.01, 0.9))
plt.grid(True)
图 8.2. 整个训练集的声音信号数据和故障时间数据,以 1/100 的比例进行子采样
让我们放大时间间隔的第一部分。我们将展示前 1%的数据(不进行子采样)。在图 8.3中,我们在同一张图上展示了前 6.29 百万行数据的声音信号和故障时间。我们可以观察到在故障前(但时间上不是非常接近),有一个大的振荡,既有负峰也有正峰。这个振荡还由几个较小的不规则振荡在非规则的时间间隔前导。
图 8.3:数据前 1%的声音信号数据和故障时间数据
让我们也看看训练数据的下一个 1%(不进行子采样)。在图 8.4中,我们展示了声音信号值和故障时间的时序图。在这个时间间隔内没有发生故障。我们观察到许多不规则的小振荡,既有负峰也有正峰。
图 8.4:训练集中第 2%的数据的声音信号数据和故障时间
让我们也看看训练集中数据的最后几个百分比(最后 5%的时间)在训练集中。在图 8.5中,我们观察到几个较大的振荡叠加在较小的不规则振荡上,并且在故障前有一个主要振荡:
图 8.5:训练集中最后 5%的数据的声音信号数据和故障时间
现在,让我们也看看测试数据样本中声音信号变化的一些例子。测试数据中有 2,624 个数据段文件。我们将从中选择一些进行可视化。由于测试数据中我们只有声音信号,我们将使用修改后的可视化函数:
def plot_acc_data(test_sample_df, segment_name):
"""
Plot acoustic data for a train segment
Args:
test_sample_df: test acoustic data sample
segment_name: title of the plot
Returns:
None
"""
fig, ax1 = plt.subplots(figsize=(12, 8))
plt.title(f"Test segment: {segment_name}")
plt.plot(test_sample_df, color='r')
ax1.set_ylabel('acoustic data', color='r')
plt.legend([f"acoustic data: {segment_name}"], loc=(0.01, 0.95))
plt.grid(True)
在图 8.6中,我们展示了seg_00030f段的声音信号图:
图 8.6:测试段 seg_00030f 的声音信号数据
在下一张图中,我们展示了seg_0012b5段的声音信号图:
图 8.7:测试段 seg_0012b5 的声音信号数据
在与本章相关的笔记本中,您可以查看更多此类测试声学信号的示例。测试段显示了相当大的信号波形多样性,描述了相同的小振荡序列,其中穿插着不同振幅的峰值,类似于我们在之前下采样的训练
数据中可以看到的。
解决方案方法
竞赛中的任务是准确预测测试数据集中每个段落的单一time_to_failure
值。test
集的每个段由 150,000 个数据行组成。相比之下,training
数据集非常庞大,包含 6.92 亿行,其中一列专门用于我们的目标变量:失效时间。我们计划将训练数据分成均匀的段,每个段包含 150,000 行,并使用每个段的最终失效时间值作为该段的目标变量。这种方法旨在使训练数据与测试数据的格式相匹配,从而促进更有效的模型训练。
此外,我们还将通过聚合训练和测试数据集中的值来构建新的特征,从而得到一个包含每个数据段多个特征的单一行。下一节将深入探讨用于特征生成的信号处理技术。
特征工程
我们将使用几个特定于信号处理的库来生成大多数特征。从 SciPy(Python 科学库)中,我们使用了signal
模块的一些函数。Hann 函数返回一个 Hann 窗口,它修改信号以平滑采样信号末尾的值到 0(使用余弦“钟形”函数)。Hilbert 函数通过Hilbert
变换计算解析信号。Hilbert 变换是信号处理中使用的数学技术,具有将原始信号的相位移动 90 度的特性。
其他使用的库函数来自numpy
:快速傅里叶变换(FFT)、mean
、min
、max
、std
(标准差)、abs
(绝对值)、diff
(信号中两个连续值之间的差值)和quantile
(将样本分为相等大小、相邻的组)。我们还使用了一些来自pandas
的统计函数:mad
(中值绝对偏差)、kurtosis
、skew
和median
。我们正在实现计算趋势特征和经典 STA/LTA 的函数。经典 STA/LTA 表示 STA 长度短时间窗口信号振幅与长时间窗口 LTA 的比率。让我们深入探讨吧!
趋势特征和经典 STA/LTA
我们首先定义两个函数,用于计算趋势特征和经典的短期平均/长期平均(STA/LTA)。STA/LTA 是地震信号分析技术,在地震学中应用。它测量短期信号平均值与长期信号平均值的比率。在地震检测中很有用,因为它可以识别地震数据中的独特模式。因此,它也将是我们模型中一个有用的特征。
我们在此处展示了计算趋势特征的代码。这是使用线性回归模型(用于 1D 数据)来检索结果回归线的斜率。我们在进行回归(即计算数据的绝对值的斜率/趋势)之前将所有采样数据转换为正值。趋势数据包含有关整体信号的重要信息:
def add_trend_feature(arr, abs_values=False):
"""
Calculate trend features
Uses a linear regression algorithm to extract the trend
from the list of values in the array (arr)
Args:
arr: array of values
abs_values: flag if to use abs values, default is False
Returns:
trend feature
"""
idx = np.array(range(len(arr)))
if abs_values:
arr = np.abs(arr)
lr = LinearRegression()
lr.fit(idx.reshape(-1, 1), arr)
return lr.coef_[0]
接下来,我们计算经典的 STA/LTA,它表示长度为STA
的短时间窗口信号幅度与长时间窗口LTA
的比值。该函数接收信号和短时间平均窗口以及长时间平均窗口的长度作为参数:
def classic_sta_lta(x, length_sta, length_lta):
"""
Calculate classic STA/LTA
STA/LTA represents the ratio between amplitude of the
signal on a short time window of length LTA and on a
long time window LTA
Args:
length_sta: length of short time average window
length_lta: length of long time average window
Returns:
STA/LTA
"""
sta = np.cumsum(x ** 2)
# Convert to float
sta = np.require(sta, dtype=np.float)
# Copy for LTA
lta = sta.copy()
# Compute the STA and the LTA
sta[length_sta:] = sta[length_sta:] - sta[:-length_sta]
sta /= length_sta
lta[length_lta:] = lta[length_lta:] - lta[:-length_lta]
lta /= length_lta
# Pad zeros
sta[:length_lta - 1] = 0
# Avoid division by zero by setting zero values to tiny float
dtiny = np.finfo(0.0).tiny
idx = lta < dtiny
lta[idx] = dtiny
return sta / lta
接下来,我们实现一个计算特征的功能,该功能接收样本索引、数据子样本和转换后的训练数据句柄作为参数。此功能将使用各种信号处理算法从每个段的时间变化声学信号中构建聚合特征。在训练数据的情况下,我们使用训练集的 150K 行窗口(没有步长)。在测试集的情况下,每个测试文件代表 150K 的段。在以下小节中,我们将回顾将包含在模型中的工程特征。
由 FFT 派生的特征
模型的特征之一是对整个段应用快速傅里叶变换(FFT);这并不是直接用作特征,而是作为计算多个聚合函数的基础(参见下一小节)。FFT 使用快速实现的离散傅里叶变换来计算。
我们使用numpy
实现的一维数组FFT(fft.fft
),这非常快,因为numpy
基于BLAS(基本线性代数子程序)和Lapack(线性代数包),这两个库提供了执行基本向量和矩阵操作以及求解线性代数方程的程序。这里使用的函数的输出是一个复数值的一维数组。然后,我们从复数值数组中提取实部和虚部的向量,并计算以下特征:
-
提取 FFT 的实部和虚部;这是进一步处理声学信号快速傅里叶变换的第一步。
-
计算 FFT 的实部和虚部的平均值、标准差、最小值和最大值。从之前的变换中,该变换将 FFT 的实部和虚部分开,然后我们计算这些聚合函数。
-
计算 FFT 向量末尾 5K 和 15K 数据点的 FFT 的实部和虚部的平均值、标准差、最小值和最大值。
创建文件段以及 FFT 和由 FFT 派生的特征的代码如下。首先,我们计算声学数据子集的 FFT。然后,我们计算 FFT 的实部和虚部。从实 FFT 分量中,我们使用 pandas 的聚合函数计算平均值、标准差、最大值和最小值。然后,我们从 FFT 信号的虚部计算类似值:
def create_features(seg_id, seg, X):
"""
Create features
Args:
seg_id: the id of current data segment to process
seg: the current selected segment data
X: transformed train data
Returns:
None
"""
xc = pd.Series(seg['acoustic_data'].values)
zc = np.fft.fft(xc)
#FFT transform values
realFFT = np.real(zc)
imagFFT = np.imag(zc)
X.loc[seg_id, 'Rmean'] = realFFT.mean()
X.loc[seg_id, 'Rstd'] = realFFT.std()
X.loc[seg_id, 'Rmax'] = realFFT.max()
X.loc[seg_id, 'Rmin'] = realFFT.min()
X.loc[seg_id, 'Imean'] = imagFFT.mean()
X.loc[seg_id, 'Istd'] = imagFFT.std()
X.loc[seg_id, 'Imax'] = imagFFT.max()
X.loc[seg_id, 'Imin'] = imagFFT.min()
X.loc[seg_id, 'Rmean_last_5000'] = realFFT[-5000:].mean()
X.loc[seg_id, 'Rstd__last_5000'] = realFFT[-5000:].std()
X.loc[seg_id, 'Rmax_last_5000'] = realFFT[-5000:].max()
X.loc[seg_id, 'Rmin_last_5000'] = realFFT[-5000:].min()
X.loc[seg_id, 'Rmean_last_15000'] = realFFT[-15000:].mean()
X.loc[seg_id, 'Rstd_last_15000'] = realFFT[-15000:].std()
X.loc[seg_id, 'Rmax_last_15000'] = realFFT[-15000:].max()
X.loc[seg_id, 'Rmin_last_15000'] = realFFT[-15000:].min()
我们接着计算由各种聚合函数派生的特征。
由聚合函数派生的特征
使用 pandas 的聚合函数mean
、std
、max
和min
计算整个段落的平均值、标准差、最大值和最小值的代码如下:
xc = pd.Series(seg['acoustic_data'].values)
zc = np.fft.fft(xc)
X.loc[seg_id, 'mean'] = xc.mean()
X.loc[seg_id, 'std'] = xc.std()
X.loc[seg_id, 'max'] = xc.max()
X.loc[seg_id, 'min'] = xc.min()
我们继续计算额外的聚合特征。对于我们的模型,我们将包括各种信号处理技术,正如您将注意到的,然后,在训练我们的基线模型后,通过测量特征重要性,我们将确定哪些特征对我们的模型预测贡献更大。
接下来,我们计算整个段落的平均变化量;这里的“段落”指的是原始的声学数据子集。“变化”是通过numpy
函数diff
和参数1
计算的。此函数接收一个值数组,并计算数组中每个连续值之间的差异。然后我们计算差异值数组的平均值。我们还计算整个声学数据段的变化率的平均值。这是通过将新变化向量中的非零值的平均值除以数据段中的原始值来计算的。这些特征的代码如下:
X.loc[seg_id, 'mean_change_abs'] = np.mean(np.diff(xc))
X.loc[seg_id, 'mean_change_rate'] = np.mean(nonzero(np.diff(xc) / xc[:-1])[0])
此外,我们还计算每个整个段落的绝对值(最大值和最小值)。在计算绝对值之后,我们再计算最小值和最大值。
当我们聚合时间信号时,我们试图包括一个多样化的特征范围,以尽可能多地捕捉信号模式。这个代码如下:
X.loc[seg_id, 'abs_max'] = np.abs(xc).max()
X.loc[seg_id, 'abs_min'] = np.abs(xc).min()
可以计算每个声学数据段前 10K、50K 和最后 10K 值的聚合函数集,如下所示:
-
每个声学数据段前 50K 和最后 10K 值的平均值
-
每个声学数据段前 50K 和最后 10K 值的平均值
-
每个声学数据段前 50K 和最后 10K 值的最小值
-
每个声学数据段前 50K 和最后 10K 值的最大值
这些特征正在聚合信号的一部分较小部分,因此它们将只从故障前更小的时间间隔中捕获信号特征。在整个信号长度和信号较小部分上的聚合特征组合将增加更多关于信号的信息。这些特征的代码如下:
X.loc[seg_id, 'std_first_50000'] = xc[:50000].std()
X.loc[seg_id, 'std_last_50000'] = xc[-50000:].std()
X.loc[seg_id, 'std_first_10000'] = xc[:10000].std()
X.loc[seg_id, 'std_last_10000'] = xc[-10000:].std()
X.loc[seg_id, 'avg_first_50000'] = xc[:50000].mean()
X.loc[seg_id, 'avg_last_50000'] = xc[-50000:].mean()
X.loc[seg_id, 'avg_first_10000'] = xc[:10000].mean()
X.loc[seg_id, 'avg_last_10000'] = xc[-10000:].mean()
X.loc[seg_id, 'min_first_50000'] = xc[:50000].min()
X.loc[seg_id, 'min_last_50000'] = xc[-50000:].min()
X.loc[seg_id, 'min_first_10000'] = xc[:10000].min()
X.loc[seg_id, 'min_last_10000'] = xc[-10000:].min()
X.loc[seg_id, 'max_first_50000'] = xc[:50000].max()
X.loc[seg_id, 'max_last_50000'] = xc[-50000:].max()
X.loc[seg_id, 'max_first_10000'] = xc[:10000].max()
X.loc[seg_id, 'max_last_10000'] = xc[-10000:].max()
接下来,我们包括整个声学数据段的极大值与极小值之比以及极大值与极小值之间的差值。我们还添加了超过一定振荡幅度(超过 500 个单位)的值的数量以及整个段的值总和。我们试图使用我们构建的这些特征多样性来捕获信号中的某些隐藏模式。特别是,这里我们包括信号极端振荡的信息:
X.loc[seg_id, 'max_to_min'] = xc.max() / np.abs(xc.min())
X.loc[seg_id, 'max_to_min_diff'] = xc.max() - np.abs(xc.min())
X.loc[seg_id, 'count_big'] = len(xc[np.abs(xc) > 500])
X.loc[seg_id, 'sum'] = xc.sum()
我们继续添加多样化的聚合特征,试图捕获原始信号的各个特征。我们进一步计算每个声学数据段前 10K 和最后 50K 数据点的平均变化率(排除空值):
X.loc[seg_id, 'mean_change_rate_first_50000'] = np.mean(nonzero((np.diff(xc[:50000]) / xc[:50000][:-1]))[0])
X.loc[seg_id, 'mean_change_rate_last_50000'] = np.mean(nonzero((np.diff(xc[-50000:]) / xc[-50000:][:-1]))[0])
X.loc[seg_id, 'mean_change_rate_first_10000'] = np.mean(nonzero((np.diff(xc[:10000]) / xc[:10000][:-1]))[0])
X.loc[seg_id, 'mean_change_rate_last_10000'] = np.mean(nonzero((np.diff(xc[-10000:]) / xc[-10000:][:-1]))[0])
我们添加的一些特征将排除数据中的0
元素,以确保只将非零值包含在聚合函数的计算中。使用nonzero
函数的代码如下:
def nonzero(x):
"""
Utility function to simplify call of numpy `nonzero` function
"""
return np.nonzero(np.atleast_1d(x))
一组工程特征涉及分位数,具体是整个声学数据段的 01%,05%,95%,和 99%分位数值。分位数是使用numpy
的quantile
函数计算的。分位数是一个统计术语,指的是将数据集分成等概率的区间。例如,75%的分位数值是 75%的数据值小于该数值的点。50%的分位数是 50%的数据值小于该数值的点(也称为中位数)。我们还添加了 01%,05%,95%,和 99%分位数的绝对值。以下代码用于计算这些特征:
X.loc[seg_id, 'q95'] = np.quantile(xc, 0.95)
X.loc[seg_id, 'q99'] = np.quantile(xc, 0.99)
X.loc[seg_id, 'q05'] = np.quantile(xc, 0.05)
X.loc[seg_id, 'q01'] = np.quantile(xc, 0.01)
X.loc[seg_id, 'abs_q95'] = np.quantile(np.abs(xc), 0.95)
X.loc[seg_id, 'abs_q99'] = np.quantile(np.abs(xc), 0.99)
X.loc[seg_id, 'abs_q05'] = np.quantile(np.abs(xc), 0.05)
X.loc[seg_id, 'abs_q01'] = np.quantile (np.abs(xc), 0.01)
另一种引入的工程特征类型是趋势值(使用不带绝对标志的add_trend_values
函数计算)。趋势值将捕获声学数据信号变化的一般方向。对于显示围绕 0 高频振荡的信号,趋势将捕获实际信号平均值的变化。
我们还添加了绝对趋势值(使用带有绝对标志的add_trend_values
函数计算)。我们包括这种工程特征来捕获信号绝对值中出现的模式。在这种情况下,对于趋势的计算,我们使用原始信号的绝对值。因此,这种趋势将捕获信号绝对值变化的方向。相应的代码如下:
X.loc[seg_id, 'trend'] = add_trend_feature(xc)
X.loc[seg_id, 'abs_trend'] = add_trend_feature(xc, abs_values=True)
接下来,我们包括绝对值的平均值和绝对值的标准差。中位数绝对偏差(mad
)、峰度
、偏度
(偏度)和中位数值也被计算。这些函数使用numpy
实现。中位数绝对偏差是定量数据单变量样本变异性的稳健度量。峰度是分布尾部的综合权重相对于分布中心的度量。偏度(来自偏度)是对称分布的不对称或扭曲的度量。中位数,正如我们之前观察到的,是分隔数据集上半部分和下半部分值的数值。所有这些聚合函数都捕捉到关于信号的有益信息。这些聚合函数的计算代码如下所示:
X.loc[seg_id, 'abs_mean'] = np.abs(xc).mean()
X.loc[seg_id, 'abs_std'] = np.abs(xc).std()
X.loc[seg_id, 'mad'] = xc.mad()
X.loc[seg_id, 'kurt'] = xc.kurtosis()
X.loc[seg_id, 'skew'] = xc.skew()
X.loc[seg_id, 'med'] = xc.median()
接下来,我们包括几个通过使用信号处理特定的变换函数计算的特征。
使用希尔伯特变换和汉宁窗口派生的特征
我们还计算希尔伯特均值。我们使用scipy.signal.hilbert
函数对声学信号段应用希尔伯特变换。这通过希尔伯特变换计算解析信号,然后,我们计算变换数据的绝对值的平均值。希尔伯特变换在信号处理中经常被使用,并捕捉到关于信号的重要信息。因为我们使用聚合函数从我们的时间数据中生成特征,我们希望包括大量、多样化的现有信号处理技术,在训练模型时添加信号的重要补充元素:
X.loc[seg_id, 'Hilbert_mean'] = np.abs(hilbert(xc)).mean()
接下来,我们包括一个由汉宁窗口均值派生的特征。我们使用这个由汉宁窗口派生的特征来减少信号边缘的突然不连续性。汉宁窗口均值是通过将原始信号与汉宁窗口的结果进行卷积,然后除以汉宁窗口中所有值的总和来计算的:
X.loc[seg_id, 'Hann_window_mean'] = (convolve(xc, hann(150), mode='same') / sum(hann(150))).mean()
我们之前介绍了经典 STA/LTA 的定义。我们计算了 500-10K、5K-100K、3,333-6,666 和 10K-25K STA/LTA 窗口的经典 STA/LTA 均值等几个特征。这些是通过之前介绍的 STA/LTA 函数计算的。我们在聚合工程特征中包括各种变换,试图捕捉不同的信号特性:
X.loc[seg_id, 'Hilbert_mean'] = np.abs(hilbert(xc)).mean()
X.loc[seg_id, 'Hann_window_mean'] = (convolve(xc, hann(150), mode='same') / sum(hann(150))).mean()
X.loc[seg_id, 'classic_sta_lta1_mean'] = classic_sta_lta(xc, 500, 10000).mean()
X.loc[seg_id, 'classic_sta_lta2_mean'] = classic_sta_lta(xc, 5000, 100000).mean()
X.loc[seg_id, 'classic_sta_lta3_mean'] = classic_sta_lta(xc, 3333, 6666).mean()
X.loc[seg_id, 'classic_sta_lta4_mean'] = classic_sta_lta(xc, 10000, 25000).mean()
最后,我们还将计算基于移动平均的特征。
基于移动平均的特征
接下来,我们计算几个移动平均值,如下所示:
-
700、1.5K、3K 和 6K 窗口的移动平均值均值(排除 NaNs)
-
指数加权移动平均,跨度为 300、3K 和 6K
-
700 和 400 窗口的平均标准差移动平均值
-
700 大小窗口加减 2 倍平均标准差的移动平均值
移动平均线帮助我们辨别模式,减少噪声,并清晰地展示数据中潜在趋势的图像。相应的代码如下:
X.loc[seg_id, 'Moving_average_700_mean'] = xc.rolling(window=700).mean().mean(skipna=True)
X.loc[seg_id, 'Moving_average_1500_mean'] = xc.rolling(window=1500).mean().mean(skipna=True)
X.loc[seg_id, 'Moving_average_3000_mean'] = xc.rolling(window=3000).mean().mean(skipna=True)
X.loc[seg_id, 'Moving_average_6000_mean'] = xc.rolling(window=6000).mean().mean(skipna=True)
ewma = pd.Series.ewm
X.loc[seg_id, 'exp_Moving_average_300_mean'] = (ewma(xc, span=300).mean()).mean(skipna=True)
X.loc[seg_id, 'exp_Moving_average_3000_mean'] = ewma(xc, span=3000).mean().mean(skipna=True)
X.loc[seg_id, 'exp_Moving_average_30000_mean'] = ewma(xc, span=6000).mean().mean(skipna=True)
no_of_std = 2
X.loc[seg_id, 'MA_700MA_std_mean'] = xc.rolling(window=700).std().mean()
X.loc[seg_id,'MA_700MA_BB_high_mean'] = (X.loc[seg_id, 'Moving_average_700_mean'] + no_of_std * X.loc[seg_id, 'MA_700MA_std_mean']).mean()
X.loc[seg_id,'MA_700MA_BB_low_mean'] = (X.loc[seg_id, 'Moving_average_700_mean'] - no_of_std * X.loc[seg_id, 'MA_700MA_std_mean']).mean()
X.loc[seg_id, 'MA_400MA_std_mean'] = xc.rolling(window=400).std().mean()
X.loc[seg_id,'MA_400MA_BB_high_mean'] = (X.loc[seg_id, 'Moving_average_700_mean'] + no_of_std * X.loc[seg_id, 'MA_400MA_std_mean']).mean()
X.loc[seg_id,'MA_400MA_BB_low_mean'] = (X.loc[seg_id, 'Moving_average_700_mean'] - no_of_std * X.loc[seg_id, 'MA_400MA_std_mean']).mean()
X.loc[seg_id, 'MA_1000MA_std_mean'] = xc.rolling(window=1000).std().mean()
我们还计算四分位距(IQR)、0.01%和 99.9%的分位数。四分位距(IQR,即四分位数范围)是通过从 75%分位数减去 25%分位数(使用numpy
函数)来计算的。四分位距是数据中 50%所在的范围。0.01%和 99.9%的分位数也使用numpy
函数进行计算。IQR 和我们所包含的其他分位数是有用的,因为它们提供了关于信号集中趋势和分散的重要信息:
X.loc[seg_id, 'iqr'] = np.subtract(*np.percentile(xc, [75, 25]))
X.loc[seg_id, 'q999'] = np.quantile(xc,0.999)
X.loc[seg_id, 'q001'] = np.quantile(xc,0.001)
X.loc[seg_id, 'ave10'] = stats.trim_mean(xc, 0.1)
对于 10、100 和 1,000 个窗口,我们计算移动平均和移动标准差。有了这些值,我们随后计算最小值、最大值、平均值、标准差、平均绝对变化和相对变化、1%、5%、95%和 99%的分位数,以及绝对最大滚动值。我们包括这些特征,因为它们揭示了在指定窗口内信号局部特性的信息。随后,对于从 10、100 和 1,000 个窗口计算得到的移动平均标准差派生出的特征,代码如下:
for windows in [10, 100, 1000]:
x_roll_std = xc.rolling(windows).std().dropna().values
X.loc[seg_id, 'ave_roll_std_' + str(windows)] = x_roll_std.mean()
X.loc[seg_id, 'std_roll_std_' + str(windows)] = x_roll_std.std()
X.loc[seg_id, 'max_roll_std_' + str(windows)] = x_roll_std.max()
X.loc[seg_id, 'min_roll_std_' + str(windows)] = x_roll_std.min()
X.loc[seg_id, 'q01_roll_std_' + str(windows)] = np.quantile(x_roll_std, 0.01)
X.loc[seg_id, 'q05_roll_std_' + str(windows)] = np.quantile(x_roll_std, 0.05)
X.loc[seg_id, 'q95_roll_std_' + str(windows)] = np.quantile(x_roll_std, 0.95)
X.loc[seg_id, 'q99_roll_std_' + str(windows)] = np.quantile(x_roll_std, 0.99)
X.loc[seg_id, 'av_change_abs_roll_std_' + str(windows)] = np.mean(np.diff(x_roll_std))
X.loc[seg_id, 'av_change_rate_roll_std_' + str(windows)] = np.mean(nonzero((np.diff(x_roll_std) / x_roll_std[:-1]))[0])
X.loc[seg_id, 'abs_max_roll_std_' + str(windows)] = np.abs(x_roll_std).max()
对于从 10、100 和 1,000 个窗口计算得到的移动平均平均值派生出的特征,代码如下:
for windows in [10, 100, 1000]:
x_roll_mean = xc.rolling(windows).mean().dropna().values
X.loc[seg_id, 'ave_roll_mean_' + str(windows)] = x_roll_mean.mean()
X.loc[seg_id, 'std_roll_mean_' + str(windows)] = x_roll_mean.std()
X.loc[seg_id, 'max_roll_mean_' + str(windows)] = x_roll_mean.max()
X.loc[seg_id, 'min_roll_mean_' + str(windows)] = x_roll_mean.min()
X.loc[seg_id, 'q01_roll_mean_' + str(windows)] = np.quantile(x_roll_mean, 0.01)
X.loc[seg_id, 'q05_roll_mean_' + str(windows)] = np.quantile(x_roll_mean, 0.05)
X.loc[seg_id, 'q95_roll_mean_' + str(windows)] = np.quantile(x_roll_mean, 0.95)
X.loc[seg_id, 'q99_roll_mean_' + str(windows)] = np.quantile(x_roll_mean, 0.99)
X.loc[seg_id, 'av_change_abs_roll_mean_' + str(windows)] = np.mean(np.diff(x_roll_mean))
X.loc[seg_id, 'av_change_rate_roll_mean_' + str(windows)] = np.mean(nonzero((np.diff(x_roll_mean) / x_roll_mean[:-1]))[0])
X.loc[seg_id, 'abs_max_roll_mean_' + str(windows)] = np.abs(x_roll_mean).max()
对于从训练数据生成的每个 150K 行段,我们计算这些特征。然后,选择当前段最后一行的值作为故障时间:
# iterate over all segments
for seg_id in tqdm_notebook(range(segments)):
seg = train_df.iloc[seg_id*rows:seg_id*rows+rows]
create_features(seg_id, seg, train_X)
train_y.loc[seg_id, 'time_to_failure'] = seg['time_to_failure'].values[-1]
接下来,我们使用StandardScaler
对所有特征进行缩放。如果我们使用基于决策树(如随机森林或 XGBoost)的模型,这一步不是必须的。我们包括这一步是为了考虑到我们可能想使用其他模型的情况,例如基于神经网络的模型,在这种情况下,对特征进行归一化将是一个必要的步骤:
scaler = StandardScaler()
scaler.fit(train_X)
scaled_train_X = pd.DataFrame(scaler.transform(train_X), columns=train_X.columns)
我们对测试数据段重复同样的过程:
for seg_id in tqdm_notebook(test_X.index):
seg = pd.read_csv('../input/LANL-Earthquake-Prediction/test/' + seg_id + '.csv')
create_features(seg_id, seg, test_X)
scaled_test_X = pd.DataFrame(scaler.transform(test_X), columns=test_X.columns)
在我们分析数据后,我们生成了一组工程特征。我们打算使用这些特征来构建一个基线模型。然后,基于模型评估,我们可以进一步选择保留哪些特征,并最终创建新的特征。
构建基线模型
从原始的时间数据中,通过特征工程,我们为训练数据中的每个时间段生成了时间聚合特征,其持续时间与一个测试集相等。对于本次比赛中展示的基线模型,我们选择了LGBMRegressor
,这是当时表现最好的算法之一,在很多情况下,其性能与XGBoost
相似。训练数据使用KFold
分割成五个部分,我们对每个部分进行训练和验证,直到达到最终的迭代次数或者验证误差在指定步骤数后不再改善(由patience
参数给出)。对于每个分割,我们还会对测试集进行预测,使用的是当前折叠的最佳模型——即使用当前折叠的训练分割进行训练的模型,也就是使用训练集的 4/5。最后,我们将计算每个折叠获得的预测的平均值。我们可以使用这种交叉验证方法,因为我们的数据不再是时间序列数据。我们将数据分割成 150K 行的段,与测试数据长度相同,然后从这些数据中创建了聚合特征:
n_fold = 5
folds = KFold(n_splits=n_fold, shuffle=True, random_state=42)
train_columns = scaled_train_X.columns.values
模型参数如下代码片段所示。我们为 LightGBM 设置了以下一些常用参数:
-
叶子节点数:此参数控制每个树中的叶子节点(或终端节点)的数量。增加叶子节点的数量允许模型捕捉更复杂的模式,但也增加了过拟合的风险。
-
叶子节点中的最小数据量:如果一个叶子节点的样本数低于此阈值,则该节点不会分裂。此参数有助于控制过拟合。
-
目标:这是我们的模型的回归。
-
学习率:这将控制模型学习的速度。
-
提升方法:我们可以选择梯度提升决策树(gbdt)、dart 梯度提升(dgb)和基于梯度的单侧采样(goss)。在这里我们使用gbdt。
-
特征分数:这是在算法使用的树集成中,向树展示的数据子集中的特征百分比。
-
袋装频率、袋装分数和袋装种子:这些控制我们在对算法进行子采样以展示给不同的树时如何划分样本集。
-
度量标准:在这种情况下,mae,即平均绝对误差。
-
正则化因子 lambda_l1
-
详细程度:此参数控制算法在训练过程中打印到控制台的信息量。详细程度为 0 表示静默模式(无信息),而详细程度为 1 会打印有关训练进度的消息。
-
并行处理线程数:此参数控制算法在训练期间使用的并行线程数。
-
随机化因子 random_state:此参数是用于初始化算法各种参数的随机种子。
params = {'num_leaves': 51,
'min_data_in_leaf': 10,
'objective':'regression',
'max_depth': -1,
'learning_rate': 0.001,
"boosting": "gbdt",
"feature_fraction": 0.91,
"bagging_freq": 1,
"bagging_fraction": 0.91,
"bagging_seed": 42,
"metric": 'mae',
"lambda_l1": 0.1,
"verbosity": -1,
"nthread": -1,
"random_state": 42}
训练、验证和测试(每个折叠)的代码如下:
oof = np.zeros(len(scaled_train_X))
predictions = np.zeros(len(scaled_test_X))
feature_importance_df = pd.DataFrame()
#run model
for fold_, (trn_idx, val_idx) in enumerate(folds.split(scaled_train_X,train_y.values)):
strLog = "fold {}".format(fold_)
print(strLog)
X_tr, X_val = scaled_train_X.iloc[trn_idx], scaled_train_X.iloc[val_idx]
y_tr, y_val = train_y.iloc[trn_idx], train_y.iloc[val_idx]
model = lgb.LGBMRegressor(**params, n_estimators = 20000, n_jobs = -1)
model.fit(X_tr,
y_tr,
eval_set=[(X_tr, y_tr), (X_val, y_val)],
eval_metric='mae',
verbose=1000,
early_stopping_rounds=500)
oof[val_idx] = model.predict(X_val, num_iteration=model.best_iteration_)
#feature importance
fold_importance_df = pd.DataFrame()
fold_importance_df["Feature"] = train_columns
fold_importance_df["importance"] = model.feature_importances_[:len(train_columns)]
fold_importance_df["fold"] = fold_ + 1
feature_importance_df = pd.concat([feature_importance_df, fold_importance_df], axis=0)
#predictions
predictions += model.predict(scaled_test_X, num_iteration=model.best_iteration_) / folds.n_splits
我们将预测向量(其维度与提交文件的维度相同,即每个测试段一个条目)初始化为零。我们还初始化了一个超出折叠的向量(训练数据的长度,即训练段的数量)。
对于每个折叠,我们为训练集和验证集采样数据子集和目标值。然后,我们将它们用作模型的输入,LGBMRegressor
,使用之前定义的模型参数初始化(我们还将估计器的数量和工人的数量添加进去)。我们使用当前折叠对应的训练集子集来拟合模型,并使用相应的训练数据验证子集进行验证。
我们还设置了评估指标:mae——表示平均绝对误差——打印评估错误的频率,以及早期停止的迭代次数。此参数(早期停止的迭代次数)控制算法在验证错误在训练期间没有改进时停止等待的步数。我们在oof(超出折叠)向量中累积验证结果,并将当前折叠的特征重要性向量连接到特征重要性 DataFrame 中。早期停止用于在训练期间保持最佳模型进行预测。
特征重要性将用于在特征工程、特征选择和模型训练的迭代过程中观察——如果对于使用某些特征训练的模型,特征重要性在折叠间没有高变化。在每个折叠中,我们也在运行整个测试集的预测,使用每个折叠训练和验证的模型。然后,我们将每个折叠的值加到预测向量中,除以折叠数。这相当于一个模型集成,其中每个模型都使用对应于每个折叠分割的不同数据子集进行训练。
当评估当前模型时,我们将检查三块信息:训练和验证错误、这些错误在折叠间的变化,以及特征重要性在折叠间的变化。理想情况下,这些变化——训练和验证错误在折叠间的变化,以及特征重要性在折叠间的变化——应该更小。图 8.8显示了基线模型训练的评估图。
如您所见,我们在每 1,000 步(详细程度设置为1000
)处绘制训练进度,并在 500 步时实施早期停止(如果验证错误在最后 500 次迭代中没有改进,则停止训练)。保留最佳模型(就验证错误而言)用于测试预测。测试预测是五个分割的平均值。
图 8.8:模型训练评估输出 – 训练和验证错误
图 8.9显示了特征重要性图,包括每个折的平均值和标准差:
图 8.9:特征重要性:平均和标准差值(来自五个折的值) – 前 20 个特征
在我们进行数据分析之后,我们继续构建时间聚合特征,这些特征是在与测试集相同持续时间的训练集子集上形成的。使用具有工程特征的新的数据集,我们训练了一个基线模型。对于基线模型,我们使用了五折交叉验证,并使用每个折的模型来预测测试集。最终预测是通过平均每个折的预测来形成的。
摘要
在本章中,我们深入探讨了处理信号数据的方法,特别是音频信号。我们探讨了此类数据的各种存储格式,并检查了用于加载、转换和可视化此类数据类型的库。为了开发强大的特征,我们应用了一系列信号处理技术。我们的特征工程努力将每个训练段的时间序列数据转换为特征,并为每个测试集汇总特征。
我们将所有特征工程过程合并为一个单一函数,适用于所有训练段和测试集。转换后的特征经过了缩放。然后我们使用这些准备好的数据,利用 LGBMRegressor 算法训练了一个基线模型。该模型采用了交叉验证,我们使用每个折训练的模型来生成测试集的预测。随后,我们将这些预测汇总以创建提交文件。此外,我们还捕获并可视化了每个折的特征重要性。
参考文献
-
LANL 地震预测,你能预测即将到来的实验室地震吗?Kaggle 竞赛:
www.kaggle.com/competitions/LANL-Earthquake-Prediction
-
Gabriel Preda, LANL 地震 EDA 和预测:
www.kaggle.com/code/gpreda/lanl-earthquake-eda-and-prediction
-
LANL 地震预测,数据集描述:
www.kaggle.com/competitions/LANL-Earthquake-Prediction/data
-
BirdCLEF 2021 - 鸟鸣识别,在声音景观录音中识别鸟鸣,Kaggle 竞赛:
www.kaggle.com/competitions/birdclef-2021
-
McFee, Brian, Colin Raffel, Dawen Liang, Daniel PW Ellis, Matt McVicar, Eric Battenberg, 和 Oriol Nieto. “librosa: Python 中的音频和音乐信号分析.” 在第 14 届 Python 科学会议论文集中,第 18-25 页。2015
-
librosa 加载函数:
librosa.org/doc/main/generated/librosa.load.html
-
康奈尔鸟鸣识别,Kaggle 比赛:
www.kaggle.com/competitions/birdsong-recognition
-
EarthData MERRA2 CO,地球数据 NASA 卫星测量,Kaggle 数据集:
www.kaggle.com/datasets/gpreda/earthdata-merra2-co
-
加布里埃尔·普雷达,EARTHDATA-MERRA2 数据探索,Kaggle 笔记本:
www.kaggle.com/code/gpreda/earthdata-merra2-data-exploration
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人相聚,并在以下地点与超过 5000 名成员一起学习:
第九章:你能找出哪部电影是深度伪造的吗?
在前几章中,我们探讨了各种数据格式:表格、地理空间、文本、图像和声学,同时使用 Kaggle 数据集,了解 shapefile 可视化,构建图像或文本分类模型,以及声学信号分析。
在本章中,我们将介绍视频数据分析。我们将首先描述一个 Kaggle 比赛,深度伪造检测挑战。这个挑战要求参与者对哪些视频是人工生成以创造逼真的虚假内容进行分类。接下来,我们将快速探索最常用的视频格式,然后介绍用于数据分析的两个实用脚本。首先是一个具有操作视频内容功能的实用脚本,即读取、从视频中可视化图像和播放视频文件。其次是一个具有身体、面部和面部元素检测功能的实用脚本。我们将继续从竞赛数据集中的元数据探索,然后应用所介绍的实用脚本分析竞赛数据集中的视频数据。
简而言之,本章将涵盖以下主题:
-
深度伪造检测挑战比赛的介绍
-
用于视频数据操作和视频数据中目标检测的实用脚本
-
竞赛数据集的元数据分析和视频数据分析
介绍比赛
在本章中,我们考察了来自知名 Kaggle 比赛深度伪造检测挑战(DFDC)的数据。该比赛在参考文献 1中有详细描述,于 2019 年 12 月 11 日开始,并于 2020 年 3 月 31 日结束。它吸引了 2,265 个团队,共有 2,904 名参与者,他们共同提交了 8,951 份作品。竞争者争夺总额为 100 万美元的奖金池,其中一等奖为 50 万美元。
该活动是 AWS、Facebook、Microsoft、人工智能媒体完整性指导委员会的合作伙伴关系以及多个学术实体共同协作的结果。当时,技术行业领袖和学者们普遍认为媒体内容操纵的技术复杂性和快速变化性质。比赛的目的是鼓励全球研究人员设计创新和有效的技术来检测深度伪造和媒体操纵。与后来专注于代码的比赛不同,这次比赛要求获奖者在一个“黑盒”环境中测试他们的代码。测试数据不在 Kaggle 上可用,需要更长的处理过程,导致私人排行榜比平时晚公布,正式公布日期为 2020 年 6 月 12 日,尽管比赛已于 2020 年 4 月 24 日结束。
DFDC 吸引了众多高排名的 Kaggle 大师,他们参与数据分析并开发了提交的模型。值得注意的是,最初的第一名获奖者后来被组织者取消资格。这个团队以及其他排名靠前的参与者,通过使用公开数据扩展了他们的训练集。虽然他们遵守了关于使用外部数据的比赛规则,但他们未能满足获奖提交的文档要求。这些规则包括从所有出现在额外训练数据中的图像中的人物获得书面同意。
竞赛数据提供了两个单独的集合。在第一个集合中,提供了 400 个用于训练的视频样本和 400 个用于测试的视频,分别放在两个文件夹中,一个用于训练数据,一个用于测试数据。这些文件是 MP4 格式,这是最常用的视频格式之一。
为了训练,提供了一个超过 470 GB 的大型数据集作为下载链接。或者,相同的数据也以 50 个大约 10 GB 的小文件的形式提供。对于当前的分析,我们只会使用第一组的数据(包含 400 个训练文件和 400 个测试文件,格式为.mp4
)。
视频数据格式
视频格式指的是用于编码、压缩和存储视频数据的标准。目前,存在多种并行使用的格式。其中一些视频格式是由微软、苹果和 Adobe 等科技公司创建和推广的。他们决定开发专有格式可能与其控制自身设备或运行其操作系统的设备上的渲染质量的需求有关。
此外,专有格式可以给你带来竞争优势和更大的对许可和与格式相关的版税的控制。其中一些格式包含了之前使用的格式中不存在的新颖功能和实用功能。与技术领导者的开发并行,其他格式是在技术进步、市场需求和对齐行业标准的需求的响应下开发的。
仅举几个常用格式的例子,我们可以提到Windows Media Video(WMV)和Audio Video Interleave(AVI),这两者都是由微软开发的。MOV(QuickTime Movie)格式是由苹果开发的,用于在其 macOS 和 iOS 平台上运行。所有这些格式都支持多种音频和视频编解码器。然后,我们还有由 Adobe 开发的Flash Video(FLV)。此外,一个广泛采用的格式是MPEG-4 Part 14(MP4),它是开源的,也可以包含许多视频和音频编解码器。运动图像专家组(MPEG)指的是一组开发音频和视频压缩以及编码/解码标准的行业专家。从 MPEG-1 到 MPEG-4 的后续标准,对媒体行业的发展产生了巨大影响。
介绍竞赛实用脚本
让我们先从两个 Kaggle 实用脚本中分组具有视频操作可重用功能的 Python 模块。第一个实用脚本将加载和显示视频或播放视频文件的功能分组。第二个则专注于视频中的对象检测——更具体地说,是检测人脸和身体——使用几种替代方法。
视频数据工具
我们开发了一个实用脚本,帮助我们操作视频数据。让我们介绍一个实用脚本,我们将使用与当前章节相关的笔记本来读取视频数据,以及可视化视频文件的帧。
video_utils
实用脚本包括加载、转换和显示视频图像的函数。此外,它还包含一个播放视频内容的函数。对于视频操作,我们将使用 OpenCV 库。OpenCV 是一个开源的计算机视觉库,广泛用于图像和视频处理。OpenCV 是用 C 和 C++开发的,也提供了一个 Python 接口。
以下代码块展示了包含的库以及从视频文件中显示一张图片的功能:
import os
import cv2 as cv
import matplotlib.pyplot as plt
from IPython.display import HTML
from base64 import b64encode
def display_image_from_video(video_path):
'''
Display image from video
Process
1\. perform a video capture from the video
2\. read the image
3\. display the image
Args:
video_path - path for video
Returns:
None
'''
capture_image = cv.VideoCapture(video_path)
ret, frame = capture_image.read()
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111)
frame = cv.cvtColor(frame, cv.COLOR_BGR2RGB)
ax.imshow(frame)
在前面的代码中,函数display_image_from_video
接收一个参数,即视频文件的路径,从视频中捕获图像,读取图像,创建一个 Matplotlib Pyplot 图像,将其从 BGR(蓝绿红)转换为 RGB(红绿蓝),并显示。RGB 是一种用于在数字图像中表示颜色的颜色模型。RGB 和 BGR 之间的区别在于颜色信息存储的顺序。在 RGB 的情况下,蓝色存储为最低有效位,然后是绿色,最后是红色。在 BGR 的情况下,顺序相反。
接下来,我们定义一个函数来表示从视频文件列表中捕获的一组图像:
def display_images_from_video_list(video_path_list, data_folder, video_folder):
'''
Display images from video list
Process:
0\. for each video in the video path list
1\. perform a video capture from the video
2\. read the image
3\. display the image
Args:
video_path_list: path for video list
data_folder: path for data
video_folder: path for video folder
Returns:
None
'''
plt.figure()
fig, ax = plt.subplots(2,3,figsize=(16,8))
# we only show images extracted from the first 6 videos
for i, video_file in enumerate(video_path_list[0:6]):
video_path = os.path.join(data_folder, video_folder,video_file)
capture_image = cv.VideoCapture(video_path)
ret, frame = capture_image.read()
frame = cv.cvtColor(frame, cv.COLOR_BGR2RGB)
ax[i//3, i%3].imshow(frame)
ax[i//3, i%3].set_title(f"Video: {video_file}")
ax[i//3, i%3].axis('on')
函数display_images_from_video_list
接收一个参数,即视频文件名列表的路径,相对于其文件夹的路径,以及数据集的路径。该函数将对列表中的前六个视频文件执行与display_image_from_video
相同的处理。我们限制从视频文件中捕获的图像数量以方便操作。
实用脚本还包括一个播放视频的函数。该函数使用 IPython display
模块中的HTML
函数。代码如下:
def play_video(video_file, data_folder, subset):
'''
Display video given by composed path
Args
video_file: the name of the video file to display
data_folder: data folder
subset: the folder where the video file is located
Returns:
a HTML objects running the video
'''
video_url = open(os.path.join(data_folder, subset,video_file),'rb').read()
data_url = "data:video/mp4;base64," + b64encode(video_url).decode()
return HTML("""<video width=500 controls><source src="img/%s" type="video/mp4"></video>""" % data_url)
函数play_video
接收要播放的视频文件名、数据文件夹、数据文件夹中的文件夹以及视频文件所在位置作为参数。该函数使用base64
库中的b64encode
函数解码 MP4 视频格式,并将解码后的内容以 500 像素的受控宽度显示在视频帧中,使用HTML
控件。
我们引入了用于视频图像处理的实用脚本,它可以加载视频,从视频中可视化图像,并播放视频文件。在下一节中,我们将介绍更多用于图像中对象检测的实用脚本。这些 Python 模块包含用于对象检测的专业类。这些模块实现了两种人脸对象检测的替代方案,两者都基于计算机视觉算法。
人脸和身体检测工具
在深度伪造视频的检测中,分析视频特征,如声音与唇部动作不同步或视频中人物面部部分的不自然动作,在这次比赛时,是训练模型识别深度伪造视频的有价值元素。因此,我们在此包括专门用于检测身体和脸部的实用脚本。
第一个用于人脸检测的模块使用了Haar 级联算法。Haar 级联是一种轻量级的机器学习算法,用于对象检测。它通常被训练来识别特定对象。该算法使用 Haar-like 特征和 Adaboost 分类器来创建一个强大的分类器。算法在滑动窗口上操作,应用一系列弱分类器,拒绝图像中不太可能包含感兴趣对象的部分。在我们的案例中,我们希望使用该算法来识别视频图像中的细节,这些细节在深度伪造的情况下通常会被改变,例如面部表情、眼神和嘴型。此模块包括两个类。我们从其中一个类开始。CascadeObjectDetector是一个用于使用Haar 级联算法检测对象的通用类。从参考文献 3中的代码修改而来的CascadeObjectDetector类有一个init
函数,其中我们使用存储训练模型的特定Haar 级联对象初始化对象。该类还有一个detect函数。以下是CascadeObjectDetector的代码。在init
函数中,我们初始化cascade
对象:
import os
import cv2 as cv
import matplotlib.pyplot as plt
class CascadeObjectDetector():
'''
Class for Cascade Object Detection
'''
def __init__(self,object_cascade_path):
'''
Args:
object_cascade_path: path for the *.xml defining the parameters
for {face, eye, smile, profile} detection algorithm
source of the haarcascade resource is:
https://github.com/opencv/opencv/tree/master/data/haarcascades
Returns:
None
'''
self.object_cascade=cv.CascadeClassifier(object_cascade_path)
detect function of the CascadeObjectDetector class. This function returns the rectangle coordinates of the object detected in the image:
def detect(self, image, scale_factor=1.3,
min_neighbors=5,
min_size=(20,20)):
'''
Function return rectangle coordinates of object for given image
Args:
image: image to process
scale_factor: scale factor used for object detection
min_neighbors: minimum number of parameters considered during object detection
min_size: minimum size of bounding box for object detected
Returns:
rectangle with detected object
'''
rects=self.object_cascade.detectMultiScale(image,
scaleFactor=scale_factor,
minNeighbors=min_neighbors,
minSize=min_size)
return rects
为了这次比赛,我创建了一个专门的 Kaggle 数据集,该数据集来源于在github.com/opencv/opencv/tree/master/data/haarcascades
定义的 Haar 级联算法,作为 OpenCV 库分发的一部分。这个数据库的链接,称为Haar 级联人脸检测,在参考文献 2中给出。init
函数接收数据库中包含的一个目标检测模型的路径。detect
函数接收用于对象提取的图像和处理检测的几个参数,这些参数可以用来调整检测。这些参数是缩放因子、检测中使用的最小邻居数以及用于对象检测的最小边界框大小。在detect
函数内部,我们调用 Haar 级联模型中的detectMultiscale
函数。
在实用脚本中定义的下一个类是FaceObjectDetector
。这个类初始化了四个CascadeObjectDetector
对象,用于面部、面部侧面、眼睛和微笑检测。下面的代码块显示了带有init
函数的类定义,其中定义了这些对象。
对于每个面部元素,即一个人的正面视图、侧面视图、眼睛视图和微笑视图,我们首先使用到 Haar 级联资源的路径初始化一个专用变量。然后,对于每个资源,我们初始化一个CascadeObjectDetector
对象(参见上面关于CascadeObjectDetector
类的代码解释):
class FaceObjectDetector():
'''
Class for Face Object Detection
'''
def __init__(self, face_detection_folder):
'''
Args:
face_detection_folder: path for folder where the *.xmls
for {face, eye, smile, profile} detection algorithm
Returns:
None
'''
self.path_cascade=face_detection_folder
self.frontal_cascade_path= os.path.join(self.path_cascade,'haarcascade_frontalface_default.xml')
self.eye_cascade_path= os.path.join(self.path_cascade,'haarcascade_eye.xml')
self.profile_cascade_path= os.path.join(self.path_cascade,'haarcascade_profileface.xml')
self.smile_cascade_path= os.path.join(self.path_cascade,'haarcascade_smile.xml')
#Detector object created
# frontal face
self.face_detector=CascadeObjectDetector(self.frontal_cascade_path)
# eye
self.eyes_detector=CascadeObjectDetector(self.eye_cascade_path)
# profile face
self.profile_detector=CascadeObjectDetector(self.profile_cascade_path)
# smile
self.smile_detector=CascadeObjectDetector(self.smile_cascade_path)
这些对象存储为成员变量face_detector
、eyes_detector
、profile_detector
和smile_detector
。
detect_object function of the FaceObjectDetector class, and the detect function of the CascadeObjectDetector object initialized with the eyes Haar cascade object. Then, we use the OpenCV Circle function to mark on the initial image, with a circle, the position of the eyes detected in the image:
def detect_objects(self,
image,
scale_factor,
min_neighbors,
min_size,
show_smile=False):
'''
Objects detection function
Identify frontal face, eyes, smile and profile face and display the detected objects over the image
Args:
image: the image extracted from the video
scale_factor: scale factor parameter for `detect` function of CascadeObjectDetector object
min_neighbors: min neighbors parameter for `detect` function of CascadeObjectDetector object
min_size: minimum size parameter for f`detect` function of CascadeObjectDetector object
show_smile: flag to activate/deactivate smile detection; set to False due to many false positives
Returns:
None
'''
image_gray=cv.cvtColor(image, cv.COLOR_BGR2GRAY)
eyes=self.eyes_detector.detect(image_gray,
scale_factor=scale_factor,
min_neighbors=min_neighbors,
min_size=(int(min_size[0]/2), int(min_size[1]/2)))
for x, y, w, h in eyes:
#detected eyes shown in color image
cv.circle(image,(int(x+w/2),int(y+h/2)),(int((w + h)/4)),(0, 0,255),3)
接下来,我们将相同的方法应用于图像中的smile
对象。我们首先检测微笑,如果检测到,我们使用opencv
函数在检测到的对象的边界框上绘制矩形来显示它。因为这个函数倾向于给出很多误报,所以默认情况下,这个功能是禁用的,使用一个设置为False
的标志:
# deactivated by default due to many false positive
if show_smile:
smiles=self.smile_detector.detect(image_gray,
scale_factor=scale_factor,
min_neighbors=min_neighbors,
min_size=(int(min_size[0]/2), int(min_size[1]/2)))
for x, y, w, h in smiles:
#detected smiles shown in color image
cv.rectangle(image,(x,y),(x+w, y+h),(0, 0,255),3)
最后,我们使用专门的 Haar 级联算法提取profile
和face
对象。如果检测到,我们绘制矩形来标记检测到的对象的边界框:
profiles=self.profile_detector.detect(image_gray,
scale_factor=scale_factor,
min_neighbors=min_neighbors,
min_size=min_size)
for x, y, w, h in profiles:
#detected profiles shown in color image
cv.rectangle(image,(x,y),(x+w, y+h),(255, 0,0),3)
faces=self.face_detector.detect(image_gray,
scale_factor=scale_factor,
min_neighbors=min_neighbors,
min_size=min_size)
for x, y, w, h in faces:
#detected faces shown in color image
cv.rectangle(image,(x,y),(x+w, y+h),(0, 255,0),3)
# image
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111)
image = cv.cvtColor(image, cv.COLOR_BGR2RGB)
ax.imshow(image)
对于四个专门的对象检测器(面部、面部侧面、眼睛和微笑),我们调用检测函数,并获取结果(一个包含检测到的对象边界框的矩形列表),然后在初始图像的上下文中绘制围绕检测到的对象的圆(用于眼睛)或矩形(用于微笑、面部和面部侧面)。最后,该函数显示图像,叠加的层标记了检测到的对象的边界框。因为smile
模型的误报很多,我们设置了一个额外的参数,一个标志,用来决定我们是否显示带有微笑的提取边界框。
接下来,这个类有一个用于提取图像对象的功能。该函数接收一个视频路径,从视频中捕获图像,并在图像捕获上应用detect_objects
函数以检测该图像中的面部和面部细节(眼睛、微笑等)。下面的代码块显示了提取函数:
def extract_image_objects(self,
video_file,
data_folder,
video_set_folder,
show_smile=False
):
'''
Extract one image from the video and then perform face/eyes/smile/profile detection on the image
Args:
video_file: the video from which to extract the image from which we extract the face
data_folder: folder with the data
video_set_folder: folder with the video set
show_smile: show smile (False by default)
Returns:
None
'''
video_path = os.path.join(data_folder, video_set_folder,video_file)
capture_image = cv.VideoCapture(video_path)
ret, frame = capture_image.read()
#frame = cv.cvtColor(frame, cv.COLOR_BGR2RGB)
self.detect_objects(image=frame,
scale_factor=1.3,
min_neighbors=5,
min_size=(50, 50),
show_smile=show_smile)
我们引入了一个使用 Haar 级联算法进行面部检测的模块。接下来,我们将回顾一种替代方法,其中我们使用MTCNN模型进行面部检测。我们想测试多种方法以决定哪种方法更适合面部检测。MTCNN代表多任务级联卷积网络,它基于在论文使用多任务级联卷积网络进行联合面部检测和校准中首先提出的概念(见参考文献 4)。在另一篇题为使用 MTCNN 进行面部检测的文章中,作者提出了一种“使用子模型不同特征的级联多任务框架”(见参考文献 5)。使用 MTCNN 方法进行面部元素提取的实现是在实用脚本face_detection_mtcnn
中完成的。
在此模块中,我们定义了MTCNNFaceDetector
类。在下一个代码块中,我们展示了带有init
函数的类定义:
class MTCNNFaceDetector():
'''
Class for MTCNN Face Detection
Detects the face and the face keypoints: right & left eye,
nose, right and left lips limits
Visualize a image capture from a video and marks the
face boundingbox and the features
On top of the face boundingbox shows the confidence score
'''
def __init__(self, mtcnn_model):
'''
Args:
mtcnn_model: mtcnn model instantiated already
Returns:
None
'''
self.detector = mtcnn_model
self.color_face = (255,0,0)
self.color_keypoints = (0, 255, 0)
self.font = cv.FONT_HERSHEY_SIMPLEX
self.color_font = (255,0,255)
init
函数接收 MTCNN 模型的一个实例作为参数,该实例在调用应用程序中从mtcnn
库导入并实例化。类成员变量 detector 用此对象初始化。其余的类变量用于检测到的对象的可视化。
该类还有一个detect
函数。下一个代码块显示了detect
函数的实现:
def detect(self, video_path):
'''
Function plot image
Args:
video_path: path to the video from which to capture
image and then apply detector
Returns:
rectangle with detected object
'''
capture_image = cv.VideoCapture(video_path)
ret, frame = capture_image.read()
image = cv.cvtColor(frame, cv.COLOR_BGR2RGB)
results = self.detector.detect_faces(image)
if results:
for result in results:
print(f"Extracted features: {result}")
x, y, w, h = bounding_box = result['box']
keypoints = result['keypoints']
confidence = f"{round(result['confidence'], 4)}"
cv.rectangle(image, (x, y),(x+w,y+h), self.color_face, 3)
# add all the internal features
for key in keypoints:
xk, yk = keypoints[key]
cv.rectangle(image, (xk-2, yk-2), (xk+2, yk+2), self.color_keypoints, 3)
image = cv.putText(image, confidence, (x, y-2),
self.font, 1,
self.color_font, 2,
cv.LINE_AA)
fig = plt.figure(figsize=(15, 15))
ax = fig.add_subplot(111)
ax.imshow(image)
plt.show()
函数接收视频文件的路径作为参数。从视频文件中捕获图像后,我们读取它并将其从 BGR 格式转换为 RGB 格式。这种转换是必要的,因为我们想使用期望 RGB 颜色顺序的库函数。在将 MTCNN 模型的detect_faces
函数应用于转换后的图像后,检测器返回一个提取的 JSON 列表。每个提取的 JSON 具有以下格式:
{
'box': [906, 255, 206, 262],
'confidence': 0.9999821186065674,
'keypoints':
{
'left_eye': (965, 351),
'right_eye': (1064, 354),
'nose': (1009, 392),
'mouth_left': (966, 453),
'mouth_right': (1052, 457)
}
}
在'box'
字段中是检测到的面部区域的边界框。在'``keypoints'
字段中是五个检测到的对象的键和坐标:左眼、右眼、鼻子、最左侧的嘴部限制和最右侧的嘴部限制。还有一个额外的字段'confidence'
,它给出了模型的置信因子。
对于真实的人脸,置信因子高于 0.99(最大值为 1)。如果模型检测到伪影,或者像带有面部图像的海报这样的东西,这个因子可能高达 0.9。低于 0.9 的置信因子最可能是与伪影检测(或假阳性)相关。
在我们的实现中(见前面的代码),我们解析检测 JSON 列表,并为每个面部添加一个矩形,并为五个面部特征中的每一个添加一个点(或一个非常小的矩形)。在面部边界框矩形的顶部,我们写下置信因子(四舍五入到小数点后四位)。
除了用于从视频捕获图像和播放视频的实用脚本,以及用于从视频数据中检测对象的实用脚本之外,我们还将重用我们在第四章中开始使用的用于数据质量和绘图的数据质量实用脚本。
在下一节中,我们开始进行一些准备工作,并继续对竞赛数据进行元数据探索。在本节中,我们将介绍导入库、对数据文件进行一些检查以及元数据文件的统计分析。
元数据探索
我们首先从数据质量、绘图工具、视频工具和面部对象检测实用脚本中导入实用函数和类。以下代码块显示了从实用脚本中导入的内容:
from data_quality_stats import missing_data, unique_values, most_frequent_values
from plot_style_utils import set_color_map, plot_count
from video_utils import display_image_from_video, display_images_from_video_list, play_video
from face_object_detection import CascadeObjectDetector, FaceObjectDetector
from face_detection_mtcnn import MTCNNFaceDetector
在加载完数据文件(训练和测试样本)后,我们就可以开始我们的分析了。以下代码块检查 TRAIN_SAMPLE_FOLDER
中文件的类型:
train_list = list(os.listdir(os.path.join(DATA_FOLDER, TRAIN_SAMPLE_FOLDER)))
ext_dict = []
for file in train_list:
file_ext = file.split('.')[1]
if (file_ext not in ext_dict):
ext_dict.append(file_ext)
print(f"Extensions: {ext_dict}")
结果显示有两种类型的文件,JSON 文件和 MP4 文件。以下代码检查 TRAIN_SAMPLE_FOLDER
中存在的 JSON 文件的内容。它从包含在 JSON 文件中的 TRAIN_SAMPLE_FOLDER
中的文件中采样前五条记录:
json_file = [file for file in train_list if file.endswith('json')][0]
def get_meta_from_json(path):
df = pd.read_json(os.path.join(DATA_FOLDER, path, json_file))
df = df.T
return df
meta_train_df = get_meta_from_json(TRAIN_SAMPLE_FOLDER)
meta_train_df.head()
在 图 9.1 中,我们展示了从 JSON 文件创建 DataFrame meta_train_df
时获得的数据样本。索引是文件名。label 是 FAKE(用于深度伪造视频)或 REAL(用于真实视频)。split 字段给出了视频所属的集合(train
)。original 是初始视频的名称,深度伪造是从该视频创建的。
图 9.1:训练样本文件夹中的文件样本
我们还使用来自数据质量、绘图工具、视频工具和面部对象检测实用脚本的一些统计函数来检查元数据的一些统计信息。这些函数在 第三章 中介绍。
图 9.2 展示了 meta_train_df
中的缺失值。如图所示,19.25% 的原始字段是缺失的。
图 9.2:样本训练数据中的缺失值
在 图 9.3 中,我们展示了 meta_train_df 中的唯一值。有 323 个原始值,其中 209 个是唯一的。其他两个字段 label 和 split 有 400 个值,其中 label 有 2 个唯一值(伪造和真实),split 有 1 个(训练)。
图 9.3:样本训练数据中的唯一值
图 9.4 展示了 meta_train_df 中最频繁的值。在总共 400 个标签中,323 个或 80.75% 是伪造的。最频繁的 原始 值是 atvmxvwyns.mp4,频率为 6(即,它在 6 个伪造视频中使用了)。split 列中的所有值都是 train。
图 9.4:样本训练数据中的最频繁值
在这次分析中,我们将使用自定义颜色方案,包括蓝色和灰度的色调。以下代码块显示了生成自定义颜色图的代码:
color_list = ['#4166AA', '#06BDDD', '#83CEEC', '#EDE8E4', '#C2AFA8']
cmap_custom = set_color_map(color_list)
在图 9.5中,我们展示了颜色图。
图 9.5:样本训练数据中最频繁的值
图 9.6显示了样本训练数据中的标签分布。有 323 条记录带有FAKE标签,其余标签的值为REAL。
图 9.6:样本训练数据中最频繁的值
在下一节中,我们将开始分析视频数据。
视频数据探索
在本节中,我们将可视化一些文件样本,然后我们将开始执行目标检测,试图从图像中捕获在创建深度伪造时可能出现的异常特征。这些主要是眼睛、嘴巴和身体。
我们将首先可视化样本文件,包括真实图像和深度伪造图像。然后,我们将应用之前介绍的第一种算法,用于人脸、眼睛和嘴巴检测,即基于 Haar 级联的算法。接着,我们将使用基于 MTCNN 的替代算法。
可视化样本文件
以下代码块从一组假视频中选取一些视频文件,然后使用来自实用脚本video_utils
的display_image_from_video
函数可视化它们的图像捕获:
fake_train_sample_video = list(meta_train_df.loc[meta_train_df.label=='FAKE'].sample(3).index)
for video_file in fake_train_sample_video:
display_image_from_video(os.path.join(DATA_FOLDER, TRAIN_SAMPLE_FOLDER, video_file))
前面的代码将为三个视频中的每一个绘制一个图像捕获。在图 9.7中,我们只展示这些图像捕获中的一个,即第一个视频的:
图 9.7:伪造视频的图像捕获示例
下一个代码块选择三个真实视频的样本,然后为每个选定的视频创建并绘制一个图像捕获:
real_train_sample_video = list(meta_train_df.loc[meta_train_df.label=='REAL'].sample(3).index)
for video_file in real_train_sample_video:
display_image_from_video(os.path.join(DATA_FOLDER, TRAIN_SAMPLE_FOLDER, video_file))
在图 9.8中,我们展示了从第一段真实视频中捕获的一张图像:
图 9.8:来自真实视频的图像捕获示例
我们还希望检查所有都源自同一原始视频的视频。我们将从同一原始视频中选取六个视频,并展示每个视频的一个图像捕获。以下代码块执行此操作:
same_original_fake_train_sample_video = \
list(meta_train_df.loc[meta_train_df.original=='meawmsgiti.mp4'].index)
display_images_from_video_list(video_path_list=same_original_fake_train_sample_video,
data_folder=DATA_FOLDER,
video_folder=TRAIN_SAMPLE_FOLDER)
在图 9.9中,我们展示了来自几个不同视频的这些图像捕获中的两个,其中我们使用了相同的原始文件进行深度伪造。
图 9.9:从同一原始文件修改的伪造视频的图像捕获
我们还对测试集的视频进行了类似的检查。当然,在测试集的情况下,我们无法事先知道哪个视频是真实的还是伪造的。以下代码从数据中的两个样本视频中选择了图像捕获:
display_images_from_video_list(test_videos.sample(2).video, DATA_FOLDER, TEST_FOLDER)
图 9.10 显示了这些选定的图像:
图 9.10:从同一原始文件修改的伪造视频中的图像捕获
让我们现在开始使用人脸和身体检测工具部分中介绍的人脸检测算法。
执行对象检测
首先,让我们使用来自face_object_detection
模块的Haar 级联算法。我们使用FaceObjectDetector
对象提取面部、面部轮廓、眼睛和微笑。CascadeObjectDetector
类初始化上述人员属性的专用级联分类器(使用专用导入的资源)。detect
函数使用 OpenCV 中的CascadeClassifier
方法在图像中检测对象。对于每个属性,我们将使用不同的形状和颜色来标记/突出显示提取的对象,如下所示:
-
正面面部:绿色矩形
-
眼睛:红色圆圈
-
微笑:红色矩形
-
侧面面部:蓝色矩形
注意,由于大量误报,我们已禁用了微笑检测器。
我们将人脸检测函数应用于训练样本视频中的一些图像。以下代码块执行此操作:
same_original_fake_train_sample_video = \
list(meta_train_df.loc[meta_train_df.original=='kgbkktcjxf.mp4'].index)
for video_file in same_original_fake_train_sample_video[1:4]:
print(video_file)
face_object_detector.extract_image_objects(video_file=video_file,
data_folder=DATA_FOLDER,
video_set_folder=TRAIN_SAMPLE_FOLDER,
show_smile=False
)
上述代码运行将生成三个不同视频的三个图像捕获。每个图像都装饰了提取的突出显示对象。以下图示显示了带有提取对象的三个图像捕获。在图 9.11a中,我们看到了检测到的正面和侧面面部以及一个检测到的眼睛。图 9.11b显示了检测到的正面和侧面面部以及两个检测到的眼睛。图 9.11c显示了检测到的正面和侧面面部,正确检测到的两个眼睛,以及一个误报(其中一个鼻孔被检测为眼睛)。在这种情况下,微笑检测未激活(误报太多)。
a
b
c
图 9.11:从三个不同视频的图像捕获中检测到的面部、面部轮廓和眼睛
使用其他图像运行这些算法,我们可以看到它们并不非常稳健,并且经常产生误报以及不完整的结果。在图 9.12中,我们展示了这种不完整检测的两个示例。在图 9.12a中,只检测到了面部。在图 9.12b中,只检测到了一个面部轮廓,尽管场景中有两个人。
a
b
图 9.12:从两个不同视频捕获的图像中的面部、面部轮廓和眼部检测
在前面的图像中,也存在一种奇怪的检测;天花板上的消防喷淋头被检测为眼睛,远左边的烛台也是如此。这类错误检测(假阳性)在这些过滤器中相当常见。一个常见问题是眼睛、鼻子或嘴唇等物体在没有人脸的区域被检测到。由于这些不同物体的搜索是独立进行的,因此出现这种假阳性的可能性相当大。
我们在face_detection_mtcnn
中实施的替代解决方案使用了一个独特的框架来同时检测人脸边界框和面部元素(如眼睛、鼻子和嘴唇)的位置。让我们比较使用 Haar 级联算法获得的与使用 MTCNN 算法获得的相同图像的结果,如图图 9.11和图 9.12所示。
在图 9.13中,我们展示了一张身穿黄色衣服的人的图像;这次,我们使用的是我们的MTCNNFaceDetector进行人脸检测:
图 9.13:MTCNN 人脸检测:一个真实人脸和一个人工制品的检测
检测到两个面部对象。一个是正确的,另一个是人工制品。检测的 JSON 如下:
Extracted features: {'box': [906, 255, 206, 262], 'confidence': 0.9999821186065674, 'keypoints': {'left_eye': (965, 351), 'right_eye': (1064, 354), 'nose': (1009, 392), 'mouth_left': (966, 453), 'mouth_right': (1052, 457)}}
Extracted features: {'box': [882, 966, 77, 84], 'confidence': 0.871575653553009, 'keypoints': {'left_eye': (905, 1003), 'right_eye': (926, 985), 'nose': (919, 1002), 'mouth_left': (921, 1024), 'mouth_right': (942, 1008)}}
在我们对大量样本进行的实验中,我们得出结论,真实的人脸将有一个非常接近 1 的置信因子。因为第二个检测到的“人脸”置信度为0.87
,我们可以很容易地将其排除。只有置信因子高于0.99
的人脸才是可信的。
让我们再看另一个例子。在图 9.14中,我们比较了图 9.12中相同图像的结果。在这两个图中,场景中所有人的面部都被正确识别。在所有情况下,置信度得分都高于 0.999。没有错误地将人工制品提取为人像。该算法似乎比使用 Haar 级联的替代实现更稳健。
a
b
图 9.14:MTCNN 人脸检测:一个人和一个两个人的场景
对于下一个例子,我们选择了一个案例,如果视频中存在两个人,那么从视频中捕获的图像中的人脸被正确识别,置信度得分也较高。在同一图像中,还识别了一个被误认为是人脸的人工制品:
图 9.15:MTCNN 面部检测:两人场景
除了两个真实人物,其置信度因子分别为 0.9995 和 0.9999(四舍五入为 1)之外,场景中第一人 T 恤上的Dead Alive角色面部也被检测为面部。边界框被正确检测,所有面部元素也被正确检测。唯一表明这是一个误报的迹象是较低的置信度因子,在这种情况下为 0.9075。这样的例子可以帮助我们正确校准我们的面部检测方法。只有置信度高于 0.95 或甚至 0.99 的面部检测应该被考虑。
在与本章相关的笔记本中,Deepfake Exploratory Data Analysis (www.kaggle.com/code/gpreda/deepfake-exploratory-data-analysis
),我们提供了使用这里介绍的方法进行面部提取的更多示例。
摘要
在本章中,我们首先介绍了一系列实用脚本,这些是 Kaggle 上可重用的 Python 模块,用于视频数据处理。其中一个脚本video_utils
用于可视化视频中的图像并播放它们。另一个脚本face_object_detection
使用 Haar 级联模型进行面部检测。
第三段脚本face_detection_mtcnn
使用了 MTCNN 模型来识别面部以及如眼睛、鼻子和嘴巴等关键点。然后我们检查了 DFDC 竞赛数据集的元数据和视频数据。在这个数据集中,我们将上述面部检测方法应用于训练和测试视频中的图像,发现 MTCNN 模型方法更稳健、更准确,且误报率更低。
随着我们接近数据探索的尾声,我们将反思我们通过各种数据格式(包括表格、文本、图像、声音和现在视频)的旅程。我们深入研究了多个 Kaggle 数据集和竞赛数据集,学习了如何进行探索性数据分析、创建可重用代码、为我们的笔记本建立视觉身份,以及用数据编织故事。在某些情况下,我们还引入了特征工程元素并建立了模型基线。在一个案例中,我们展示了逐步细化我们的模型以增强验证指标的过程。前几章和当前章节的重点是制作高质量的 Kaggle 笔记本。
在下一章中,我们将探讨使用 Kaggle 的大型语言模型,可能还会结合其他技术,如 LangChain 和向量数据库。这将展示生成式 AI 在各个应用中的巨大潜力。
参考文献
-
深度伪造检测挑战,Kaggle 竞赛,识别带有面部或声音操纵的视频:
www.kaggle.com/competitions/deepfake-detection-challenge
-
人脸检测的 Haar 级联,Kaggle 数据集:
www.kaggle.com/datasets/gpreda/haar-cascades-for-face-detection
-
Serkan Peldek – 使用 OpenCV 进行人脸检测,Kaggle 笔记本:
www.kaggle.com/code/serkanpeldek/face-detection-with-opencv/
-
张凯鹏,张占鹏,李志锋,乔宇 – 使用多任务级联卷积网络进行人脸检测与对齐:
arxiv.org/abs/1604.02878
-
Justin Güse – 使用 MTCNN 进行人脸检测 — 专注于速度的人脸提取指南:
towardsdatascience.com/face-detection-using-mtcnn-a-guide-for-face-extraction-with-a-focus-on-speed-c6d59f82d49
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人相聚,并在以下地点与超过 5000 名成员一起学习:
第十章:利用 Kaggle 模型释放生成式人工智能的力量
在前面的章节中,我们主要关注掌握分析不同数据类型和制定解决各种问题的策略。我们深入研究了数据探索和可视化的各种工具和方法,丰富了我们在这些领域的技能。其中一些早期章节专门用于构建基线模型,特别是在竞争场景中的参与。
现在,在本章中,我们将把注意力转向利用 Kaggle 模型。我们的目标是将这些模型集成到 Kaggle 应用中,以便原型化在实用应用中使用最新的生成式人工智能技术。这类现实世界应用的例子包括个性化营销、聊天机器人、内容创作、定向广告、回答客户咨询、欺诈检测、医学诊断、患者监测、药物发现、个性化医疗、金融分析、风险评估、交易、文件起草、诉讼支持、法律分析、个性化推荐和合成数据生成。
本章将涵盖以下主题:
-
Kaggle 模型简介 – 如何访问和使用它们
-
激活一个大型语言模型(LLM)
-
将 LLM 与任务链解决方案(如 Langchain)结合使用,为 LLM 创建一系列(或链)提示
-
使用 LangChain、LLM 和向量数据库构建检索增强生成(RAG)系统
介绍 Kaggle 模型
Kaggle 模型代表了 Kaggle 平台上的最新创新之一。这一功能在代码竞赛的引入后尤其受到关注,在竞赛中,参与者通常在本地硬件或云中训练模型。训练完成后,他们将模型作为数据集上传到 Kaggle。这种做法允许 Kagglers 在他们推理笔记本中使用这些预训练模型,简化了代码竞赛提交的过程。这种方法显著减少了推理笔记本的运行时间,符合竞赛严格的时间和内存限制。Kaggle 对这种方法的认可与现实世界的生产系统相吻合,在现实世界的生产系统中,模型训练和推理通常在独立的管道中发生。
这种策略对于基于 Transformer 架构的大型模型至关重要,因为这些模型在微调时需要巨大的计算资源。像 HuggingFace 这样的平台进一步民主化了大型模型的访问,提供了在线使用或下载协作开发模型的选择。Kaggle 引入的模型功能,可以像数据集一样添加到笔记本中,是一项重大进步。这些模型可以直接在笔记本中用于迁移学习或进一步微调等任务。然而,在撰写本文时,Kaggle 不允许用户以与数据集相同的方式上传模型。
Kaggle 的模型库提供了浏览和搜索功能,使用户可以根据名称、元数据、任务、数据类型等多种标准找到模型。在撰写本文时,该库拥有由 Google、TensorFlow、Kaggle、DeepMind、Meta 和 Mistral 等知名组织发布的 269 个模型和 1,997 个变体。
随着 GPT-3、ChatGPT、GPT-4 等模型的出现,生成式 AI 领域引起了极大的兴趣。Kaggle 提供了访问多个强大的 LLM(大型语言模型)或基础模型的机会,例如 Llama、Alpaca 和 Llama 2。该平台的集成生态系统使用户能够迅速测试新出现的模型。例如,Meta 的 Llama 2 自 2023 年 7 月 18 日起可用,是一系列生成文本模型,参数量从 70 亿到 700 亿不等。这些模型,包括适用于聊天应用的专用版本,与其他平台相比,在 Kaggle 上相对容易访问。
Kaggle 通过允许用户直接从模型页面启动笔记本,类似于从比赛或数据集启动笔记本,进一步简化了流程。
这种简化的方法,如以下截图所示,增强了用户体验,并促进了模型实验和应用中更高效的流程。
图 10.1:Mistral 模型的主页,右上角有添加笔记本的按钮
一旦在编辑器中打开笔记本,模型就已经添加进去了。对于模型来说,还需要额外一步,这是因为模型也有变体、版本和框架。在笔记本编辑窗口的右侧面板中,您可以设置这些选项。设置完这些选项后,我们就可以在笔记本中使用模型了。以下截图显示了 Mistral AI(见参考文献 2)的一个模型 Mistral 的选项,在菜单中选择了所有内容:
图 10.2:Mistral AI 的 Mistral 模型添加到笔记本中,并选择了所有选项
激活基础模型
LLMs 可以直接用于诸如摘要、问答和推理等任务。由于它们是在非常大的数据集上训练的,因此它们可以很好地回答许多主题的多种问题,因为它们在训练数据集中有可用的上下文。
在许多实际情况下,这样的 LLM 可以在第一次尝试中正确回答我们的问题。在其他情况下,我们需要提供一些澄清或示例。这些零样本或少样本方法中答案的质量高度依赖于用户为 LLM 编写的提示能力。在本节中,我们将展示在 Kaggle 上与一个 LLM 交互的最简单方法,使用提示。
模型评估和测试
在开始在 Kaggle 上使用 LLM 之前,我们需要进行一些准备工作。我们首先加载模型,然后定义一个分词器。接下来,我们创建一个模型管道。在我们的第一个代码示例中,我们将使用 transformers 中的 AutoTokenizer 作为分词器并创建一个管道,也使用 transformers pipeline。以下代码(来自参考 3中的笔记本摘录)说明了这些步骤:
def load_model_tokenize_create_pipeline():
"""
Load the model
Create a
Args
Returns:
tokenizer
pipeline
"""
# adapted from https://huggingface.co/blog/llama2#using-transformers
time_1 = time()
model = "/kaggle/input/llama-2/pytorch/7b-chat-hf/1"
tokenizer = AutoTokenizer.from_pretrained(model)
time_2 = time()
print(f"Load model and init tokenizer: {round(time_2-time_1, 3)}")
pipeline = transformers.pipeline(
"text-generation",
model=model,
torch_dtype=torch.float16,
device_map="auto",)
time_3 = time()
print(f"Prepare pipeline: {round(time_3-time_2, 3)}")
return tokenizer, pipeline
前面的代码返回了分词器和管道。然后我们实现了一个测试模型的功能。该函数接收分词器、管道以及我们想要测试模型的提示。以下代码是测试函数:
def test_model(tokenizer, pipeline, prompt_to_test):
"""
Perform a query
print the result
Args:
tokenizer: the tokenizer
pipeline: the pipeline
prompt_to_test: the prompt
Returns
None
"""
# adapted from https://huggingface.co/blog/llama2#using-transformers
time_1 = time()
sequences = pipeline(
prompt_to_test,
do_sample=True,
top_k=10,
num_return_sequences=1,
eos_token_id=tokenizer.eos_token_id,
max_length=200,)
time_2 = time()
print(f"Test inference: {round(time_2-time_1, 3)}")
for seq in sequences:
print(f"Result: {seq['generated_text']}")
现在,我们准备提示模型。我们使用的模型具有以下特点:Llama 2 模型(7b)、来自 HuggingFace 的聊天版本(版本 1)以及 PyTorch 框架。我们将用数学问题提示模型。在下一个代码摘录中,我们初始化分词器和管道,然后用一个简单的算术问题提示模型,这个问题是用普通语言表述的:
tokenizer, pipeline = load_model_tokenize_create_pipeline()
prompt_to_test = 'Prompt: Adrian has three apples. His sister Anne has ten apples more than him. How many apples has Anne?'
test_model(tokenizer, pipeline, prompt_to_test)
让我们看看模型是如何推理的。以下截图,我们绘制了推理时间、提示和答案:
图 10.3:使用 Llama 2 模型对数学问题的提示、答案和推理时间
对于这个简单的数学问题,模型的推理似乎很准确。让我们尝试一个不同的问题。在以下代码摘录中,我们提出了一个几何问题:
prompt_to_test = 'Prompt: A circle has the radius 5\. What is the area of the circle?'
test_model(tokenizer, pipeline, prompt_to_test)
以下截图显示了使用前面的几何问题提示模型的成果:
图 10.4:Llama 2 模型对一个基本几何问题的回答
对于简单的数学问题,模型的回答并不总是正确的。在以下示例中,我们使用第一个代数问题的变体提示了模型。你可以看到,在这种情况下,模型采取了一条复杂且错误的路径来得出错误的解决方案:
图 10.5:Llama 2 模型在代数问题上的解决方案是错误的
模型量化
在先前的实验中,我们用一系列简单的问题测试了模型。这个过程强调了精心设计、结构良好的提示在引发准确和相关信息中的关键作用。虽然 Kaggle 慷慨地提供了大量的免费计算资源,但 LLMs 的规模本身就是一个挑战。这些模型需要大量的 RAM 和 CPU/GPU 功率来加载和推理。
为了减轻这些需求,我们可以采用一种称为模型量化的技术。这种方法有效地减少了模型的内存和计算需求。它通过使用低精度数据类型(如 8 位或 4 位整数)来表示模型的权重和激活函数,而不是标准的 32 位浮点格式,来实现这一点。这种方法不仅节省了资源,而且在效率和性能之间保持了平衡(见参考文献 4)。
在我们即将提供的示例中,我们将演示如何使用可用的技术之一,即 llama.cpp
库,量化 Kaggle 上的模型。我们选择了 Llama 2 模型来完成这个目的。截至写作时,Llama 2 是你可以下载(经 Meta 批准)并免费使用的最成功的 LLM 之一。它也在各种任务上表现出可观的准确性,与其他许多可用模型相当。量化将使用 llama.cpp
库执行。
llama.cpp, import the necessary functions from the package, execute the quantization process, and subsequently load the quantized model. It’s important to note that, in this instance, we will not utilize the latest, more advanced quantization option available in llama.cpp. This example serves as an introduction to model quantization on Kaggle and its practical implementation:
!CMAKE_ARGS="-DLLAMA_CUBLAS=on" pip install llama-cpp-python
!git clone https://github.com/ggerganov/llama.cpp.git
!python llama.cpp/convert.py /kaggle/input/llama-2/pytorch/7b-chat-hf/1 \
--outfile llama-7b.gguf \
--outtype q8_0
from llama_cpp import Llama
llm = Llama(model_path="/kaggle/working/llama-7b.gguf")
让我们看看几个测试量化模型的例子。我们将首先用地理问题提示它:
output = llm("Q: Name three capital cities in Europe? A: ", max_tokens=38, stop=["Q:", "\n"], echo=True)
提示的结果如下:
图 10.6:使用地理问题提示量化 Llama 2 模型的结果
在下一个屏幕截图中,我们展示了模型对一个简单几何问题的回答。答案非常直接,表述清晰。提示模型和打印结果的代码如下:
output = llm("If a circle has the radius 3, what is its area?")
print(output['choices'][0]['text'])
图 10.7:使用几何问题提示量化 Llama 2 模型的结果
展示第一个量化 Llama 2 模型方法的笔记本,我们从其中提取了代码和结果,详见参考文献 5。该笔记本是在 GPU 上运行的。在参考文献 6中给出的另一个笔记本中,我们运行了相同的模型,但是在 CPU 上。值得注意的是,使用量化模型在 CPU 上执行推理的时间远小于在 GPU 上(使用相同的量化模型)。有关更多详细信息,请参阅参考文献 5和6。
我们还可以使用其他模型量化的方法。例如,在参考文献 7中,我们使用了 bitsandbytes
库进行模型量化。为了使用这种量化选项,我们需要安装 accelerate 库和 bitsandbytes
的最新版本。以下代码片段展示了如何初始化量化模型配置并使用此配置加载模型:
model_1_id = '/kaggle/input/llama-2/pytorch/7b-chat-hf/1'
device = f'cuda:{cuda.current_device()}' if cuda.is_available() else 'cpu'
# set quantization configuration to load large model with less GPU memory
# this requires the `bitsandbytes` library
bnb_config = transformers.BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type='nf4',
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=bfloat16
)
我们还定义了一个管道:
time_1 = time()
query_pipeline_1 = transformers.pipeline(
"text-generation",
model=model_1,
tokenizer=tokenizer_1,
torch_dtype=torch.float16,
device_map="auto",)
time_2 = time()
print(f"Prepare pipeline #1: {round(time_2-time_1, 3)} sec.")
llm_1 = HuggingFacePipeline(pipeline=query_pipeline_1)
我们可以用一个简单的提示来测试模型:
llm_1(prompt="What is the most popular food in France for tourists? Just return the name of the food.")
答案看起来似乎是正确的:
图 10.8:对简单地理问题的回答(使用 bitsandbytes 库量化的 Llama 2)
到目前为止,我们已经尝试了提示模型。我们直接使用了 Kaggle Models 中的模型,或者经过量化处理。我们使用了两种不同的方法进行量化。然而,在下一节中,我们将看到如何使用 Langchain 这样的任务链框架来扩展 LLM(大型语言模型)的能力,并创建一系列操作,其中 LLM 的初始查询答案作为下一个任务的输入。
使用 Langchain 构建多任务应用
Langchain 是最受欢迎的任务链框架(参考文献 8)。任务链是我们在上一节中阐述的提示工程概念的扩展。链是一系列预定的操作,旨在以更易于管理和理解的方式组织复杂的过程。这些链遵循特定的动作顺序。它们非常适合具有固定步骤数量的工作流程。使用任务链,您可以创建一系列提示,其中框架执行的前一个任务的输出作为下一个任务的输入。
除了 Langchain 之外,现在还有其他几种任务链选项可用,如 LlamaIndex 或来自微软的 Semantic Kernel。Langchain 提供了多种功能,包括专门的数据摄取或结果输出工具、智能代理,以及通过定义自己的任务、工具或代理来扩展它的可能性。代理将根据感知到的上下文选择并执行任务,以实现其目标。为了执行任务,它将使用通用或定制的工具。
让我们从定义一个两步序列开始使用 Langchain。我们将在一个自定义函数中定义这个序列,该函数将接收一个参数并形成一个参数化的初始提示,该提示以输入参数为参数。根据第一个提示的答案,我们组装下一个任务的提示。这样,我们可以创建我们迷你应用的动态行为。定义此函数的代码如下(参考文献 7):
def sequential_chain(country, llm):
"""
Args:
country: country selected
Returns:
None
"""
time_1 = time()
template = "What is the most popular food in {country} for tourists? Just return the name of the food."
# first task in chain
first_prompt = PromptTemplate(
input_variables=["country"],
template=template)
chain_one = LLMChain(llm = llm, prompt = first_prompt)
# second step in chain
second_prompt = PromptTemplate(
input_variables=["food"],
template="What are the top three ingredients in {food}. Just return the answer as three bullet points.",)
chain_two = LLMChain(llm=llm, prompt=second_prompt)
# combine the two steps and run the chain sequence
overall_chain = SimpleSequentialChain(chains=[chain_one, chain_two], verbose=True)
overall_chain.run(country)
time_2 = time()
print(f"Run sequential chain: {round(time_2-time_1, 3)} sec.")
预期的输入参数是一个国家的名称。第一个提示将获取那个国家最受欢迎的食物。下一个提示将使用第一个问题的答案来构建第二个问题,这个问题是关于那种食物的前三种成分。
让我们用两个例子来检查代码的功能。首先,让我们尝试使用France
参数:
final_answer = sequential_chain("France", llm_1)
图 10.9:两步顺序链执行(法国最著名食物的成分)
答案看起来相当令人信服。确实,法国的游客更喜欢蜗牛,而且,这种美味食物的前三种成分确实列得正确。让我们再检查一次,用另一个以其美味食物而闻名的国家意大利
。提示将是:
final_answer = sequential_chain("Italy", llm_1)
因此,结果将是:
图 10.10:意大利最受欢迎的食物及其成分
我们用一个直观的例子说明了如何使用 LangChain 与 LLM 结合,通过链式多个提示来扩展 LLMs 的能力,例如,在业务流程自动化的自动化中。在下一节中,我们将看到如何使用 LLMs 来完成另一个重要任务,即代码生成自动化,以提高编码过程中的生产力。
使用 Kaggle Models 进行代码生成
对于代码生成,我们将实验 Code Llama 模型,13b 版本。在撰写本文时,在 Kaggle 平台上可用的 LLMs 中,这个模型在目的(它是一个专门用于代码生成的模型)和大小(即我们可以使用它与 Kaggle Notebooks)方面对于代码生成任务来说是最合适的。用于演示代码生成的笔记本在参考 9中给出。模型被加载,使用bitsandbytes
量化,并且以与参考 7中相同的方式初始化了 tokenizer。我们使用以下代码定义了一个提示和一个管道(使用 transformers 函数):
prompt = 'Write the code for a function to compute the area of circle.'
sequences = pipeline(
prompt,
do_sample=True,
top_k=10,
temperature=0.1,
top_p=0.95,
num_return_sequences=1,
eos_token_id=tokenizer.eos_token_id,
max_length=200,
)
执行前面代码的结果如下所示。代码看起来功能正常,但答案包含比预期更多的信息。我们通过打印所有输出的序列获得了这些信息。如果我们只选择第一个,答案将是正确的(只有圆面积的计算代码)。
图 10.11:代码生成:计算圆面积的函数
在参考 9的笔记本中,有更多的例子;我们这里不会给出所有细节。你可以通过更改提示来修改笔记本并生成更多答案。
在下一节中,让我们看看如何通过创建一个系统来进一步扩展 LLMs 的功能,该系统可以检索存储在特殊数据库(向量数据库)中的信息,通过将初始查询与检索到的信息(上下文)结合来组装提示,并通过仅使用检索步骤的结果来提示 LLM 回答初始查询。这样的系统被称为检索增强生成(RAG)。
创建一个 RAG 系统
在前面的章节中,我们探讨了与基础模型交互的各种方法——更确切地说,是来自 Kaggle Models 的可用 LLMs。首先,我们通过提示直接使用模型进行了实验。然后,我们用两种不同的方法量化了模型。我们还展示了我们可以使用模型来生成代码。一个更复杂的应用包括将LangChain
与 LLM 结合以创建一系列连接的操作,或任务序列。
在所有这些情况下,LLM 的答案都是基于在训练模型时模型已经拥有的信息。如果我们希望 LLM 回答关于从未向 LLM 展示过的信息的查询,模型可能会通过虚构来提供误导性的答案。为了对抗模型在没有正确信息时虚构的倾向,我们可以使用自己的数据微调模型。这种方法的缺点是成本高昂,因为微调大型模型所需的计算资源非常大。它也不一定能完全消除虚构。
与此方法不同的选择是将向量数据库、任务链框架和 LLM(大型语言模型)结合起来创建一个 RAG 系统(参见参考文献 10)。在下面的图中,我们展示了这样一个系统的功能:
图 10.12:RAG 系统解释
在使用 RAG 系统之前,我们必须将文档导入向量数据库(图 10.12 中的步骤 1)。文档可以是任何格式,包括 Word、PowerPoint、文本、Excel、图片、视频、电子邮件等。我们首先将文本格式的每种模态转换(例如,使用 Tesseract 从图片中提取文本,或使用 OpenAI Whisper 将视频转换为文本)。在我们将所有格式/模态转换为文本之后,我们必须将较大的文本分割成固定大小的块(部分重叠,以避免丢失可能分布在多个块中的上下文)。
然后,我们在将预处理过的文档添加到向量数据库之前,使用其中一种选项对信息进行编码。向量数据库存储使用文本嵌入编码的数据,并且它还使用非常高效的索引来支持这种编码类型,这将使我们能够根据相似性搜索快速搜索和检索信息。我们有多个向量数据库选项,如 ChromaDB、Weaviate、Pinecone 和 FAISS。在我们的 Kaggle 应用程序中,我们使用了 ChromaDB,它有一个简单的界面,与 Langchain 插件兼容,易于集成,有选项用于内存以及持久存储。
一旦数据在向量数据库中转换为、分割、编码和索引,我们就可以开始查询我们的系统。查询通过 Langchain 的专业任务传递——问答检索(图 10.12 中的步骤 2)。查询用于在向量数据库中执行相似性搜索。检索到的文档与查询一起使用(图 10.12 中的步骤 3)来组成 LLM 的提示(图 10.12 中的步骤 4)。LLM 将仅根据我们提供的上下文来回答查询——来自存储在向量数据库中的数据的上下文。
实现 RAG 系统的代码在参考文献 11中给出。我们将使用 2023 年国情咨文的文本(来自 Kaggle 数据集)作为文档。让我们首先直接使用 LLM 通过提示来回答关于国情咨文的一般问题:
llm = HuggingFacePipeline(pipeline=query_pipeline)
# checking again that everything is working fine
llm(prompt="Please explain what is the State of the Union address. Give just a definition. Keep it in 100 words.")
答案在以下屏幕截图中给出。我们可以观察到 LLM 具有相关信息,并且答案是正确的。当然,如果我们询问的是最近的信息,答案可能就是错误的。
图 10.13:提示的结果(一个不带背景的一般问题)
现在我们来看一些关于我们摄入到向量数据库中的信息的问题的答案。
数据转换、分块和编码使用以下代码完成。由于我们摄入的数据是纯文本,我们将使用 Langchain 的TextLoader
。我们将使用ChromaDB
作为向量数据库,并使用 Sentence Transformer 进行嵌入:
# load file(s)
loader = TextLoader("/kaggle/input/president-bidens-state-of-the-union-2023/biden-sotu-2023-planned-official.txt",
encoding="utf8")
documents = loader.load()
# data chunking
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=20)
all_splits = text_splitter.split_documents(documents)
# embeddings model: Sentence Transformer
model_name = "sentence-transformers/all-mpnet-base-v2"
model_kwargs = {"device": "cuda"}
embeddings = HuggingFaceEmbeddings(model_name=model_name, model_kwargs=model_kwargs)
# add documents to the ChromaDB database
vectordb = Chroma.from_documents(documents=all_splits, embedding=embeddings, persist_directory="chroma_db")
我们定义了问题和答案检索链:
retriever = vectordb.as_retriever()
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
verbose=True
)
我们还定义了一个函数来测试前面的链:
def test_rag(qa, query):
print(f"Query: {query}\n")
time_1 = time()
result = qa.run(query)
time_2 = time()
print(f"Inference time: {round(time_2-time_1, 3)} sec.")
print("\nResult: ", result)
让我们来测试这个系统的功能。我们将针对主题制定查询 – 在这个例子中,是 2023 年国情咨文:
query = "What were the main topics in the State of the Union in 2023? Summarize. Keep it under 200 words."
test_rag(qa, query)
运行上述查询的结果将是:
图 10.14:使用 RAG 系统进行查询和答案(示例 1)
接下来,我们展示同一内容上的不同查询的答案(包含在打印输出中的查询):
图 10.15:使用 RAG 系统进行查询和答案(示例 2)
我们还可以检索用于创建答案背景的文档。以下代码正是如此:
docs = vectordb.similarity_search(query)
print(f"Query: {query}")
print(f"Retrieved documents: {len(docs)}")
for doc in docs:
doc_details = doc.to_json()['kwargs']
print("Source: ", doc_details['metadata']['source'])
print("Text: ", doc_details['page_content'], "\n")
RAG 是一种强大的方法,可以发挥 LLM 推理能力,同时控制信息来源。LLM 给出的答案仅来自通过相似性搜索提取的上下文(问答检索链的第一步),以及我们存储信息的向量数据库。
摘要
在本章中,我们探讨了如何利用 Kaggle 模型中 LLMs 的潜力。我们首先关注了使用此类基础模型的最简单方法——直接提示它们。我们了解到构建提示很重要,并尝试了简单的数学问题。我们使用了 Kaggle 模型中可用的模型以及量化模型,并采用了两种方法进行量化:使用 Llama.cpp
和 bitsandbytes
库。然后,我们将 Langchain 与 LLM 结合起来创建了一系列链式任务,其中一项任务的输出被框架用来为下一项任务创建输入(或提示)。使用 Code Llama 2 模型,我们在 Kaggle 上测试了代码生成的可行性。结果并不完美,除了预期的序列外,还生成了多个序列。最后,我们学习了如何创建一个 RAG 系统,该系统结合了向量数据库的速度、多功能性和易用性,以及 Langchain 的链式功能和 LLMs 的推理能力。
在下一章,也就是我们这本书的最后一章,你将学习一些有用的食谱,这将帮助你使你在平台上的高质量工作更加引人注目和受到赞赏。
参考文献
-
Llama 2,Kaggle 模型:
www.kaggle.com/models/metaresearch/llama-2
-
Mistral,Kaggle 模型:
www.kaggle.com/models/mistral-ai/mistral/
-
Gabriel Preda – 使用数学测试 Llama v2,Kaggle 笔记本:
www.kaggle.com/code/gpreda/test-llama-v2-with-math
-
模型量化,HuggingFace:
huggingface.co/docs/optimum/concept_guides/quantization
-
Gabriel Preda – 使用 Llama.cpp 量化 Llama 2 的测试,Kaggle 笔记本:
www.kaggle.com/code/gpreda/test-llama-2-quantized-with-llama-cpp
-
Gabriel Preda – 使用 llama.cpp 在 CPU 上量化 Llama 2 的测试,Kaggle 笔记本:
www.kaggle.com/code/gpreda/test-of-llama-2-quantized-with-llama-cpp-on-cpu
-
Gabriel Preda – 使用 Llama 2 和 Langchain 的简单顺序链,Kaggle 笔记本:
www.kaggle.com/code/gpreda/simple-sequential-chain-with-llama-2-and-langchain/
-
Langchain,维基百科页面:
en.wikipedia.org/wiki/LangChain
-
Gabriel Preda – 使用 Code Llama 生成 Python 代码(13b),Kaggle 笔记本:
www.kaggle.com/code/gpreda/use-code-llama-to-generate-python-code-13b
-
加布里埃尔·普雷达 – 检索增强生成,结合 LLMs、任务链和向量数据库,Endava 博客:
www.endava.com/en/blog/engineering/2023/retrieval-augmented-generation-combining-llms-task-chaining-and-vector-databases
-
加布里埃尔·普雷达 – 使用 Llama 2、Langchain 和 ChromaDB 进行 RAG,Kaggle 笔记本:
www.kaggle.com/code/gpreda/rag-using-llama-2-langchain-and-chromadb
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人相聚,并和超过 5000 名成员一起学习,详情请见:
第十一章:结束我们的旅程:如何保持相关性和领先地位
我们接近了通过数据科学领域的启迪之旅的尾声,我们已经穿越了多样化的挑战领域,从地理空间分析到自然语言处理,再到图像分类和时间序列预测。这次探险丰富了我们对如何巧妙结合各种尖端技术的理解。我们深入研究了大型语言模型,例如 Kaggle 开发的模型,探索了向量数据库,并发现了任务链框架的效率,所有这些都是为了利用生成式 AI 的变革潜力。
我们的学习之旅还包括处理各种数据类型和格式。我们参与了特征工程,构建了几个基线模型,并掌握了迭代优化这些模型的能力。这个过程对于掌握综合数据分析所必需的众多工具和技术至关重要。
除去技术层面,我们已拥抱数据可视化的艺术。我们不仅学习了技术,还学会了如何根据每个独特的数据集和分析调整风格和视觉效果。此外,我们还探索了如何围绕数据构建引人入胜的故事,从而超越单纯的报告技术,让数据生动起来。
在本章中,我打算分享一些有见地的想法、技巧和窍门。这些不仅可以帮助你在创建有价值且影响深远的数据科学笔记本方面达到精通,还可以帮助你获得对你工作的认可。通过这些指南,你可以确保你的工作脱颖而出,帮助你保持在不断发展的数据科学领域的领先地位。
向最佳学习:观察成功的宗师
在本书的前几章中,我们探讨了各种分析方法、可视化工具和定制选项。这些技术被我和许多其他尊敬的 Kaggle 笔记本宗师有效地利用。我成为第 8 位笔记本宗师并长期保持前三名排名,不仅仅是因为深入分析、高质量的视觉或在我的笔记本中构建引人入胜的故事。这同样是对坚持少数最佳实践的证明。
当我们深入研究这些最佳实践时,我们将了解是什么让成功的 Kagglers 与众不同,特别是 Kaggle 笔记本大师和宗师。让我们从一份迷人的数据集的硬证据开始:Meta Kaggle 大师成就快照。这个数据集(见参考文献 1)包含两个文件:一个详细描述成就,另一个描述用户:
-
在成就文件中,我们看到 Kagglers 在竞赛、数据集、笔记本和讨论类别中达到的层级,以及他们在所有这些类别中的最高排名。此文件仅包括在 Kaggle 四个类别中至少达到 Master 层级的用户。
-
第二个文件提供了这些用户的详细资料,包括头像、地址、国家、地理坐标以及从他们的个人资料中提取的元数据。这些元数据提供了他们对 Kaggle 的任期以及他们在平台上最近的活动情况,例如“13 年前加入,过去一天内最后一次出现。”
我们将分析这些用户在平台上的“最后看到”的天数,并检查 Notebooks 类别中的 Masters 和 Grandmasters 对此指标的分布。解析和提取此信息的代码在此提供,为我们提供了深入了解顶级 Kagglers 习惯和参与度的宝贵窗口:
profiles_df["joined"] = profiles_df["Metadata"].apply(lambda x: x.split(" · ")[0])
profiles_df["last_seen"] = profiles_df["Metadata"].apply(lambda x: x.split(" · ")[1])
def extract_last_seen(last_seen):
"""
Extract and return when user was last time seen
Args:
last_seen: the text showing when user was last time seen
Returns:
number of days from when the user was last time seen
"""
multiplier = 1
last_seen = re.sub("last seen ", "", last_seen)
if last_seen == "in the past day":
return 0
last_seen = re.sub(" ago", "", last_seen)
quantity, metric = last_seen.split(" ")
if quantity == "a":
quantity = 1
else:
quantity = int(quantity)
if metric == "year" or metric == "years":
multiplier = 356
elif metric == "month" or metric == "months":
multiplier = 30
return quantity * multiplier
profiles_df["tenure"] = profiles_df["joined"].apply(lambda x: extract_tenure(x))
profiles_df["last_seen_days"] = profiles_df["last_seen"].apply(lambda x: extract_last_seen(x))
我们以天为单位给出结果。让我们可视化 Notebooks 类别中 Masters 和 Contributors 最后被看到的天数分布。为了清晰起见,我们移除了在过去 6 个月内没有出现过的用户。我们认为这些用户目前不活跃,其中一些用户在平台上最后一次出现的时间长达 10 年前。
同时,过去 6 个月内没有出现过的用户(来自 Notebooks 类别的 Masters 和 Grandmasters)的比例为 6%。对于 Notebooks 类别中其余 94%的 Masters 和 Grandmasters,我们展示了与最后看到的天数相关的分布,如图11.1所示。
图 11.1:Kaggle 平台上用户最后出现的天数的分布
我们可以很容易地看出,Notebooks 类别中的大多数 Masters 和 Grandmasters 每天都会访问平台(最后看到的天数为 0 意味着他们在当天也是活跃的)。因此,每天在线是大多数成功 Masters 和 Grandmasters 的一个属性。我可以从我个人的经验中证实,在我成为 Master 然后成为 Grandmaster 的过程中,我几乎每天都在平台上活跃,创建新的笔记本并使用它们来分析数据集,准备比赛提交以及进行详细分析。
定期回顾和改进你的工作
当我创建一个笔记本时,仅仅把它放在一边然后开始研究新的主题是非常不寻常的。大多数时候,我会多次回到它,并添加新的想法。在笔记本的第一版中,我试图专注于数据探索,真正理解各自数据集(或数据集)的独特特征。在下一版中,我专注于细化图形,并可能为数据准备、分析和可视化提取函数。我更好地组织代码,消除重复部分,并将通用部分保存在实用脚本中。使用实用脚本的最佳部分是,你现在有了可重用的代码,可以在多个笔记本中使用。当我创建实用脚本时,我会采取措施使代码更通用、可定制和健壮。
接下来,我也对笔记本的视觉身份进行了细化。我检查了构图的整体性,对风格进行了调整,以更好地适应我想创造的故事。当然,随着笔记本的成熟和接近稳定版本,我会进一步努力提高可读性,并真正尝试创造一个好的叙述。修订没有限制。
我也会查看评论,并试图回应批评者,同时采纳改进建议,包括新的叙事元素。一个好的故事需要一个好的评论者,大多数时候,笔记本的读者都是优秀的评论者。即使评论无关紧要,甚至负面,我们仍然需要保持冷静和沉着,试图找到问题的根源:我们在分析中是否遗漏了重要方面?我们是否未能对所有数据细节给予足够的关注?我们是否使用了正确的可视化工具?我们的叙述是否连贯?对评论的最佳回应,除了表达你的感激之情外,就是当评论者的建议合理时,采纳这些建议。
让我们将这个原则应用到本书包含的一个项目中。在第四章中,我们学习了如何构建具有多个叠加层的复杂图形,包括市区的多边形和酒吧或星巴克咖啡店的位置。在图 11.2中,我们展示了一张原始地图。在选择星巴克咖啡店之一后,会弹出一个显示商店名称和地址的弹出窗口。这张地图很棒,但弹出窗口看起来并不完全合适,不是吗?文本没有对齐,弹出窗口的大小小于显示所有信息所需的尺寸,而且外观和感觉似乎与地图的质量不相符。
图 11.2:伦敦市区轮廓与星巴克商店位置及弹出窗口(之前的设计)
Chapter 4 to define a CircleMarker with a popup:
for _, r in coffee_df.iterrows():
folium.CircleMarker(location=[r['Latitude'], r['Longitude']],
fill=True,
color=color_list[2],
fill_color=color_list[2],
weight=0.5,
radius=4,
popup="<strong>Store Name</strong>: <font color='red'>{}</font><br><strong>Ownership Type</strong>:{}<br>\
<strong>Street Address</strong>: {}".format(r['Store Name'], r['Ownership Type'],r['Street Address'])).add_to(m)
我们可以使用更复杂的 HTML 代码来定义弹出窗口的布局和内容。以下代码片段用于添加星巴克的店铺标志,将店铺名称作为标题,并在与星巴克颜色协调的 HTML 表格中,显示品牌、店铺编号、所有权类型、地址、城市和邮编。我们将函数代码分成几个部分来分别解释每个部分。
函数的第一部分定义了星巴克图像的 URL。我们使用维基百科的图像作为标志,但在即将到来的屏幕截图中有意将其模糊处理,以遵守版权法。然后,我们定义了一些变量来保持我们将包含在表格中的每列的值。我们还定义了表格背景的颜色。下一行代码用于星巴克图像的可视化:
def popup_html(row):
store_icon = "https://upload.wikimedia.org/wikipedia/en/3/35/Starbucks_Coffee_Logo.svg"
name = row['Store Name']
brand = row['Brand']
store_number = row['Store Number']
ownership_type = row['Ownership Type']
address = row['Street Address']
city = row['City']
postcode = row['Postcode']
left_col_color = "#00704A"
right_col_color = "#ADDC30"
html = """<!DOCTYPE html>
<html>
<head>
<center><img src=\"""" + store_icon + """\" alt="logo" width=100
=100 ></center>
<h4 style="margin-bottom:10"; width="200px">{}</h4>""".format(name) + """
接下来,我们定义表格。每条信息都在表格的单独一行中显示,右列包含变量的名称,左列包含实际值。我们包含在表格中的信息是名称、品牌、店铺编号、地址、城市和邮编:
</head>
<table style="height: 126px; width: 350px;">
<tbody>
<tr>
<td style="background-color: """+ left_col_color +""";"><span style="color: #ffffff;">Brand</span></td>
<td style="width: 150px;background-color: """+ right_col_color +""";">{}</td>""".format(brand) + """
</tr>
<tr>
<td style="background-color: """+ left_col_color +""";"><span style="color: #ffffff;">Store Number</span></td>
<td style="width: 150px;background-color: """+ right_col_color +""";">{}</td>""".format(store_number) + """
</tr>
<tr>
<td style="background-color: """+ left_col_color +""";"><span style="color: #ffffff;">Ownership Type</span></td>
<td style="width: 150px;background-color: """+ right_col_color +""";">{}</td>""".format(ownership_type) + """
</tr>
<tr>
<td style="background-color: """+ left_col_color +""";"><span style="color: #ffffff;">Street Address</span></td>
<td style="width: 150px;background-color: """+ right_col_color +""";">{}</td>""".format(address) + """
</tr>
<tr>
<td style="background-color: """+ left_col_color +""";"><span style="color: #ffffff;">City</span></td>
<td style="width: 150px;background-color: """+ right_col_color +""";">{}</td>""".format(city) + """
</tr>
<tr>
<td style="background-color: """+ left_col_color +""";"><span style="color: #ffffff;">Postcode</span></td>
<td style="width: 150px;background-color: """+ right_col_color +""";">{}</td>""".format(postcode) + """
</tr>
</tbody>
</table>
</html>
"""
return html
接下来,以下代码用于定义要添加到CircleMarker
的弹出窗口小部件,以替换之前用字符串格式定义的弹出窗口。请注意,我们用对新定义的popup
函数的调用替换了之前的弹出窗口代码:
for _, r in coffee_df.iterrows():
html = popup_html(r)
popup = folium.Popup(folium.Html(html, script=True), max_width=500)
folium.CircleMarker(location=[r['Latitude'], r['Longitude']],
fill=True,
color=color_list[2],
fill_color=color_list[2],
weight=0.5,
radius=4,
popup = popup).add_to(m)
在图 11.3中,我们展示了弹出窗口的改进版本,其中我们使用 HTML 代码生成一个更高质量的弹出窗口:
图 11.3:伦敦地区轮廓图,星巴克店铺位置和弹出窗口(当前,新设计)
认识到他人的贡献,并加入你个人的风格
要在 Kaggle 等平台上提升你的笔记本,持续进行改进和创新至关重要,既要借鉴社区反馈,也要借鉴他人的工作。根据建设性评论定期回顾和更新你的笔记本,表明你对卓越的承诺。你还可以看看别人都做了什么。仅仅复制他们的工作不会给你带来太多的点赞。然而,如果你从其他用户的工作开始,通过扩展他们的观察并改进可视化或结果解释来带来新的见解,这可以帮助你在排名中上升。
此外,正确地说明你从别人的工作中开始,并清楚地了解你自己的贡献,这一点非常重要。如果你想结合来自不同来源的笔记本想法,建议从包含最多内容的来源进行分支。仔细为你在自己的笔记本中包含的他们工作的部分进行致谢。
当从多个来源采纳想法时,花些时间对齐符号、编程约定、函数、类和实用脚本,确保你不会创建出一个弗兰肯斯坦式的笔记本,而是一个代码感觉统一的笔记本。
当然,更重要的是要努力在可视化的外观和感觉以及笔记本的风格上创造一致性。平台用户会返回并欣赏你的工作,不仅是因为其质量,还因为你的个人风格。即使是从其他用户的笔记本开始,通过分叉它,也要保持你的个人风格。
要迅速:不要等待完美
一些快速崛起的新 Kaggle 笔记本大师有一些共同点:他们在新的竞赛启动后仅用几天,有时甚至几小时,就开始分析数据并发布探索性数据分析或基线模型解决方案。他们是第一批在 Kaggle 不断变化的数据探索领域中占据新领域的人。通过这样做,他们将追随者的注意力集中在他们的工作上,他们收到了最多的评论,这有助于他们改进工作,并且他们的工作会被许多人分叉(为了方便)。这反过来又增加了他们笔记本的病毒性。
然而,如果你等待太久,你可能会发现你的分析想法也被其他人想到了,等你最终将其完善到足以满足你的标准时,一大群人已经对其进行了探索、发表并获得了认可。有时,关键在于速度;有时,则在于原创性。在许多情况下,成功的 Kaggle 笔记本大师在处理新的竞赛数据方面都是早起的鸟儿。
数据集和模型也是如此:首先发布,然后根据之前的建议继续完善和改进原始工作的人会获得更多的追随者和来自评论的反馈,他们可以将这些反馈应用于进一步的改进,并从平台上的病毒性因素中受益。
要慷慨:分享你的知识
一些最受欢迎的 Kaggle 笔记本大师的崛起不仅归功于他们能够创建出精美叙述的笔记本,还归功于他们愿意分享重要的知识。通过提供高质量、解释详尽的模型基线,这些大师赢得了追随者的广泛赞誉,获得了稳固的地位,并在笔记本类别中攀升排名。
在 Kaggle 平台上,用户多次分享了关于数据的见解,这些见解对于显著提高竞赛提交的模型至关重要。通过提供有用的起点、突出重要的数据特征或提出解决新类型问题的方法,这些用户加强了社区,并帮助他们的追随者提高技能。除了通过 notebooks 获得认可,这些 Grandmasters 还通过讨论和数据集传播有价值的信息。他们创建并发布额外的数据集,帮助竞争者完善他们的模型,并在与特定竞赛或数据集相关的讨论主题中提供建议。
许多成功的 Kaggle Notebook Grandmasters,如 Bojan Tunguz、Abhishek Thakur 和 Chris Deotte,他们是四重 Grandmasters(在所有类别中都达到了最高级别),在讨论和数据集中广泛分享他们的知识。在长期担任 Kaggle Grandmasters 的典范人物中,有 Gilberto Titericz(Giba),他曾是竞赛中的第一名,以通过 notebooks 分享见解和在备受瞩目的特色竞赛中提供新视角而闻名。这些顶级 Kagglers 表明,在所有类别中保持活跃不仅增强了他们在每个单独类别中的个人档案,而且对他们的整体成功做出了重大贡献。他们持续的平台存在,加上他们的谦逊、愿意回答问题和在讨论部分帮助他人,体现了慷慨和协作的精神。记住他们在自己通往顶峰的旅程中获得的帮助,他们发现帮助他人进步是一种满足感,这是维持他们在 Kaggle 社区中显赫地位的关键因素。
走出你的舒适区
保持领先比达到目标更困难。Kaggle 是一个极具竞争性的协作和竞赛平台,它位于信息技术行业中增长最快、变化最大的领域之一,即机器学习。这个领域的变革速度难以跟上。
在 Kaggle 最高排名者中保持位置可能是一项艰巨的任务。特别是在 Notebooks 中,进步可以比在竞赛中更快(而且竞争非常激烈),非常才华横溢的新用户经常出现,挑战那些位于最高位置的人。要保持领先,你需要不断革新自己,而除非你走出舒适区,否则无法做到这一点。试着每天学习新事物,并且立即付诸实践。
挑战自己,保持动力,投身于你认为困难的事情。你还需要探索平台上的新功能,这为你提供了为对生成 AI 最新应用感兴趣的 Kagglers 创建教育性和吸引性内容的新机会。
现在,您可以使用笔记本将数据集和模型结合起来,创建原创且富有信息量的笔记本,例如,展示如何创建一个检索增强生成系统(见参考文献 2)。这样一个系统结合了大型语言模型的强大“语义大脑”、从向量数据库索引和检索信息的灵活性,以及 LangChain 或 LlamaIndex 等任务链框架的通用性。在第十章中,我们探讨了 Kaggle 模型在构建此类强大应用方面所提供的丰富潜力。
感恩
感恩在通过 Kaggle 等级晋升至笔记本大师级并登上排行榜前列的过程中起着至关重要的作用,尽管这往往被忽视。这不仅仅关乎创作出具有吸引力的叙事的优秀内容;对社区支持的感激同样重要。
当你在 Kaggle 上变得活跃并赢得通过点赞和有见地的评论支持你工作的追随者时,承认并表达对这种支持的感激至关重要。深思熟虑地回应评论,认可有价值的建议,向那些分支你数据的人提供建设性的反馈,这些都是表达感激的有效方式。虽然分支可能不会直接像点赞那样直接贡献于赢得奖牌,但它们增加了你工作的可见性和影响力。将模仿视为真诚的赞赏形式,并对它带来的社区参与表示感谢,这加强了你在平台上的存在,并培养了一个支持性和协作的环境。
摘要
在本章的最后,我们回顾了 Kaggle 上优秀笔记本内容作者的“秘诀”。他们有几个共同点:他们在平台上保持持续活跃,早期开始处理新的数据集或竞赛数据集,不断改进他们的工作,认可并赞赏他人创作的优质内容,是持续学习者,谦逊,分享他们的知识,并且不断在舒适区之外工作。这些并非目标本身,而是对分析数据和创建优秀预测模型所知一切充满热情和持续兴趣的体现。
当我们结束这本书,让你开始 Kaggle 笔记本冒险之旅时,我祝愿你一路平安。希望你喜欢阅读,请记住,数据科学的世界是持续变化的。继续实验,保持好奇心,带着自信和技能深入数据。愿你的未来 Kaggle 笔记本充满惊人的洞察力,灵光一现的时刻,也许还有一些令人挠头的挑战。快乐编码!
参考文献
-
Meta Kaggle-Master Achievements Snapshot,Kaggle 数据集:
www.kaggle.com/datasets/steubk/meta-kagglemaster-achievements-snapshot
-
Gabriel Preda,使用 Llama 2、LangChain 和 ChromaDB 的 RAG,Kaggle 笔记本:
www.kaggle.com/code/gpreda/rag-using-llama-2-langchain-and-chromadb
加入我们书籍的 Discord 空间
加入我们的 Discord 社区,与志同道合的人相聚,并在以下地点与超过 5000 名成员一起学习: