成功数据共享的 3 条不可变规则
解锁数据协作的力量
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 1 月 21 日
–
在我之前的文章中,我讨论了数据共享这一已经非常成熟的概念。数据共享指的是向所有部门开放数据访问,以赋予每个部门进行数据驱动决策的能力。
对于公司来说,在没有适当计划的情况下贸然开展数据共享计划,认为仅仅增加业务部门的访问权限就足够了,这种情况仍然很常见。这种做法是错误的。实际上,数据共享是一项复杂的任务,需要经过深思熟虑的规划和执行才能成功。
我们提出了三条不变的规则,以确保你的数据共享计划的成功
-
你不应妥协数据质量
-
你应为数据提供丰富的背景
-
你应提供正确的接口来探索数据
关于数据质量的第一条规则是数据共享的基石——这是一个不可谈判的前提。数据质量是数据生产者(软件和数据工程团队)的责任。这是将高质量数据交到数据团队手中的问题。没有高质量的数据,数据团队无法完成工作,更无法与其他部门进行数据共享。实际上,如果数据团队无法使用数据,那还分享给别人干嘛呢?
本文中的第二条和第三条规则侧重于确保高质量数据有效地与业务团队共享。这不仅涉及提供准确和可靠的数据,还包括用相关背景丰富数据,并通过用户友好的界面使其易于访问。这样,即使是技术能力较弱的团队也能轻松使用数据。下面可以找到一个可视化表示。
有效数据共享的三条规则 — 图片由Castor提供
忽视这些规则中的任何一条必然会导致失败,这也是我们理想中想要避免的。让我们深入探讨每一条规则。
数据质量
成功的数据共享的基础是保持你与业务部门共享的数据质量。
数据共享是为了让业务部门能够做出数据驱动的决策。为此,你必须提供一流的数据。
当你分享有缺陷的数据时,人们显然会做出错误的决策。这可能导致重大财务损失、错失机会,并损害公司声誉。更重要的是,这会侵蚀对信任的数据信任,并导致对数据的普遍漠视。如果计划不是分享一流的数据,那么根本不要分享数据。数据共享要么全力以赴,要么彻底放弃。如果执行不当,可能对你的组织造成损害。
数据质量是一个涵盖所有影响数据能否用于预期用途的因素的总括性术语。有几个特征定义了高质量的数据,包括但不限于:
-
准确性:数据准确描述其所代表的现实世界现象的程度。
-
完整性:数据是完整的,包含了所有必要的信息。
-
一致性:数据在不同来源和平台上是一致的。
-
可靠性:数据是最新的,并且与预期的使用场景相关。
-
可用性:数据被预期受众理解和使用以做出明智决策的难易程度。
你可以在 Kevin Hu 的 文章中找到更多数据质量指标。
数据质量属性及其相关指标 — 图片来源于 Castor
当你分享具有这些属性的数据时,你增加了改进决策和效率的可能性。但这并不是数据质量的全部。
确保你的数据符合正确的质量标准的一个好方法是实施 数据合同。
数据合同是任何数据民主化倡议中的重要组成部分。数据社区与数据合同有着 爱恨交织的关系。但我们认为它们在数据共享对话中值得一提。
数据合同是数据生产者和数据消费者之间的协议,概述了共享和使用数据的具体条款和条件。它们在确保数据质量方面可以发挥重要作用,通过设定明确的期望和处理数据的指导方针。
数据合同规定,在数据共享之前,数据必须符合某些格式、约束和语义意义,或者可能包括要求定期审计数据质量的条款。
数据合同可能包括以下信息:
-
正在收集哪些数据
-
数据的采集频率和方式
-
谁拥有和负责数据(个人或团队)
-
谁可以访问数据以及访问的级别
-
安全性和治理措施,例如匿名化
例如,让我们考虑一下驱动 Ubereats 的机器学习模型。该模型的性能取决于其训练数据的准确性,而这些数据来源于公司内部的各种表格。
为确保模型正常运行,我们期望数据的完整性始终得到保持;这意味着列不应被删除,每个字段的值应保持一致,所有关键业务逻辑应得到遵守。如果这些条件中的任何一个未得到满足,模型的性能可能会受到影响。
为确保这些期望得到满足,应在数据合同中进行概述,以使数据生产者对维护数据的完整性负责。
总体而言,数据合同可以通过设定明确的指导方针和期望来提供一个确保数据质量的框架。这可以帮助确保所有相关方对数据质量的维护负有责任。这样,数据合同可以防止有缺陷的数据流入运营团队手中。
维护高水平的数据质量很重要,但仅此不足以解决问题。下一步是确保提供背景信息。
丰富的背景
背景是有效实施数据共享的第二个关键。没有背景的数据是危险和毫无价值的,因为它可能被不同的团队以不同的方式解读。
让我告诉你,这不是一个安全的选择。不同的解释意味着不同的结论,最终导致部门间报告不一致。如果你要引导业务团队进入未知领域,就给他们一张地图。背景就是这张地图。
人们了解一个数据集时,他们需要知道这些数据将满足什么需求、其内容以及其位置。一旦人们找到相关的数据集,他们就完成了 10%的工作。接下来,他们需要通过一个包含 10 多个问题的检查清单,确保他们理解自己使用的数据。只有当人们能够回答以下问题时,他们才真正理解数据:
-
数据来源于哪里?
-
它的流动路径是什么?它向下游喂送了哪些表?
-
谁拥有它/谁对它负责?
-
我所在领域中某个字段的含义是什么?
-
为什么这很重要?
-
这个表最后一次更新时间是什么时候?
-
这些数据的上游和下游依赖关系是什么?
-
这是生产质量的数据吗?
背景始于文档。所有共享的数据资产都需要被记录,以便利益相关者理解它们。实际上,这意味着对你的数据资产进行整理,包括列定义、标签、所有者等。当你正确记录数据时,人们知道在哪里找到它以及如何使用它,而不需要向公司中的其他人寻求帮助。
提供背景的第二个方面是拥有强大的数据血统能力。数据血统是一种极其强大的透明工具。它使人们能够理解数据资产之间的关系。如果上游出现问题,数据血统允许每个人了解下游的后果,从而避免不愉快的惊讶。血统还可以帮助利益相关者在数据问题出现时识别其来源。
数据血统:追踪数据资产之间的关系——图片来自Castor
提供背景的第三个方面是促进社交发现。这可以通过共享有关数据如何被利用的信息来实现。
当人们看到他们的同事如何使用和查询数据时,他们能够以更强的基础开始,并从同事的见解和策略中学习。社交发现使团队能够在彼此的知识基础上进行合作,从而提高工作效率。
例如,一个想要对营销合格线索(MQLs)进行分析的市场营销分析师可以利用社交发现来简化过程。通过社交发现,分析师可以迅速识别出营销团队使用的最相关的表格和数据集。此外,他还可以访问团队执行的查询,这可以作为分析的起点。这不仅节省了时间,还允许分析师从同事的工作中获得见解和学习。
用户友好的接口
如果你要与他人共享数据,必须通过正确的接口进行。并非所有团队成员的技术水平相同,也不是所有团队的数据需求相同。为正确的团队提供合适的接口对于让数据对所有人都可访问至关重要。
如果你在 dbt 中记录数据,不能期望市场营销团队在那儿提取文档。上下文应该在对业务团队友好的工具中提供。有两种方法可以实现这一点:
实现这一点的一种方法是提供一个能够高效搜索和导航的工具。该工具应易于使用和理解,以确保非技术团队成员能够有效使用。数据目录是一个可以用来轻松发现、理解和访问数据的工具的例子。
另一种提供正确接口的方法是通过使数据在业务团队已经使用的工具中易于访问。这种方法涉及将数据交付给团队已经熟悉的工具。可以使用反向 ETL 工具来实现这一目的。
通过在现有工具中使数据可查找,团队可以访问所需的数据,而无需导航新系统或学习新软件。例如,一旦在数据仓库中计算了线索评分,反向 ETL 允许将该指标同步到 Salesforce。这使得销售人员可以直接在他们熟悉的工具中访问数据。
无论你采用什么方法,请记住,如果你想让数据对所有人可用,必须满足业务团队的需求。要求他们学习技术团队的工具和流程只会阻碍你的努力。
提供正确的接口对于数据民主化和使所有团队成员都能访问数据至关重要。在决定正确的接口时,重要的是考虑不同团队的技术专长和数据需求。通过提供易于使用的工具或将数据发送到现有工具,团队可以访问所需的数据,以做出明智的决策并推动结果。
结论
总之,数据共享是推动数据驱动决策和促进跨部门协作的强大工具。
但这是一个复杂的任务,需要深思熟虑的规划和执行才能成功。
我们提出了三条不可改变的规则,以确保您的数据共享计划成功:1)保持数据质量,2)提供数据的丰富背景,3)提供正确的界面以探索数据。
当然,数据共享涉及隐私和安全问题,这篇文章中没有提及。我下一篇文章将完全致力于这个话题!
关于我们
我们撰写了有关利用数据资产的所有过程:从 现代数据栈 到数据团队组成,再到数据治理。我们的 博客 涵盖了从数据中创造实际价值的技术和非技术方面。
在 Castor,我们正在为 Notion、Figma 和 Slack 一代开发一个数据文档工具。
想要了解更多?联系我们,我们将向您展示演示。
最初发表于 https://www.castordoc.com。
2024 年值得期待的 3 项音乐 AI 突破
2024 年可能成为音乐 AI 的转折点
·
关注 发布于 Towards Data Science ·11 分钟阅读·2023 年 12 月 30 日
–
图像由 DALL-E 3 生成。
回顾:2023 年如何改变了音乐 AI
从我的角度来看,2023 年是音乐 AI 历史上最激动人心的一年。以下是我们在这一年中体验到的一些突破:
-
文本到音乐生成已经跨越了“恐怖谷”阶段(例如 MusicLM)
-
开源旋律条件的音乐生成发布(例如 MusicGen)
-
首个基于提示的音乐搜索产品上线(例如 Cyanite)
-
开源的具备音频理解/生成能力的聊天机器人已发布(例如,AudioGPT)
-
开源的音乐描述 AI已发布(例如,Doh 等,2023)
-
基于提示的源分离试点(例如,Liu 等,2023)
-
…
从文本到音乐生成,再到全文本音乐搜索,2023 年充满了突破。这些进展是冰山一角,展示了音乐 AI 内在的潜力。然而,即使有这些令人兴奋的发展,该领域仍明显落后于其更大的兄弟语音 AI,甚至落后于其表亲 NLP 和计算机视觉。这一差距可以在两个关键方面观察到(或听到):
1. 技术尚不成熟。无论是音乐生成、基于文本的搜索还是神经嵌入:目前音乐 AI 中的所有技术在文本和图像领域至少已有 1 到 3 年的历史。该领域需要更多的资金、时间和智力支持。
2. 缺乏令人信服和流行的商业产品。在音乐 AI 潜力变得明显之后,许多初创公司纷纷成立,开始致力于商业产品的开发。然而,随着这些产品的开发和测试,音乐家和企业急切地等待将 AI 技术融入他们的工作流程的机会。
然而,在 2023 年音乐 AI 技术成功之后,我对研究人员和公司在这两个方面取得进展感到乐观。在这篇文章中,我想强调我希望在 2024 年看到的三个具体发展,并解释它们为何重要。凭借这些预期的进展,2024 年将站在通过 AI 彻底改变我们与音乐互动的前沿。
1. 灵活自然的源分离
源分离的可视化。图像摘自作者的博客文章。
什么是源分离?
音乐源分离是将完整制作的音乐分解为其原始乐器源(例如,人声、节奏、键盘)的任务。如果你从未听说过源分离,我写了一篇完整的博客文章讲解了它是如何工作的,以及为什么这是一个如此具有挑战性的技术问题。
源分离领域的第一个重大突破发生在 2019 年,当时 Deezer 发布了分离器(Spleeter)作为开源工具。自这一技术飞跃以来,该领域经历了相对稳定的小步改进。然而,如果将原始的 Spleeter 与现代开源工具如 Meta 的DEMUCS或商业解决方案如LALAL.ai进行比较,差异显得非常明显。因此,在经历了多年缓慢、渐进的进展后,为什么我会期待源分离在 2024 年爆发呢?
为什么我们应该期待源分离的突破?
首先,源分离是其他音乐 AI 问题的基石技术。拥有一个快速、灵活且自然的源分离工具,可以将音乐分类、标记或数据增强提升到新水平。许多研究人员和公司正在仔细观察源分离领域的进展,准备在下一次突破发生时采取行动。
其次,不同种类的突破将推动该领域的发展。最明显的是分离质量的提升。虽然我们肯定会看到这方面的进展,但我不期望会有重大飞跃(如果错了我很乐意接受)。不过,除了输出质量,源分离算法还有两个其他问题:
1. 速度: 源分离通常运行在大型生成神经网络上。对于单个音轨,这可能还可以。然而,对于商业应用中遇到的较大工作负载,速度通常仍然太慢——尤其是在推理过程中执行源分离时。
2. 灵活性: 一般而言,源分离工具提供一套固定的源(例如“人声”、“鼓声”、“低音”、“其他”)。传统上,无法执行根据用户需求定制的源分离,因为这需要针对这一任务训练一个全新的神经网络。
一旦源分离的速度足够快以在推理过程中进行(即每次模型预测之前),许多有趣的应用将会出现。例如,我曾写过关于利用源分离使黑箱音乐 AI 可解释的潜力。我认为速度优化的商业兴趣非常大,这可能会推动明年的突破。
此外,当前代源分离 AI 的灵活性有限,使其在各种应用场景中无法使用,即便原则上具备潜力。在一篇名为Separate Anything You Describe的论文中,研究人员今年推出了基于提示的源分离系统。想象一下在文本框中输入“给我第二段的主合成器,但不带延迟效果”,然后得到你所需的源音频。这就是我们所期待的潜力。
摘要:源分离
总之,由于其在音乐 AI 中的重要性以及速度和灵活性的持续改进,音乐源分离在 2024 年可能会取得重大进展。新的发展,如基于提示的系统,使其更具用户友好性和适应性。这一切都预示着在行业中的更广泛应用,这可能激励该领域的研究突破。
2. 通用音乐嵌入
图像由 DALL-E 3 生成。
自然语言处理(NLP)中的嵌入
为了理解音乐嵌入是什么以及它们为何重要,让我们来看看自然语言处理(NLP)这一术语的起源。在 NLP 中嵌入出现之前,该领域主要依赖于更简单的基于统计的方法来理解文本。例如,在简单的词袋(BoW)方法中,你只需统计词汇表中每个单词在文本中出现的频率。这使得 BoW不比一个简单的词云更有用。
一个简单的词云示例。图像作者提供。
嵌入的引入显著改变了自然语言处理(NLP)的格局。嵌入是单词(或短语)的数学表示,其中单词之间的语义相似性通过这些嵌入空间中的向量之间的距离体现出来。简单来说,单词、句子或整本书的意义可以被压缩成一堆数字。通常,每个单词/文本的100
到1000
个数字已经足以数学上捕捉其意义。
在Tensorflow Embedding Projector上使用 t-SNE 可视化的 Word2Vec(10k)嵌入。突出显示了与“violin”最相似的前 5 个单词。截图由作者提供。
在上图中,你可以看到基于其数值嵌入的 10,000 个单词在三维图表中的表示。因为这些嵌入捕捉了每个单词的意义,我们可以简单地在图表中查找最接近的嵌入,以找到类似的术语。这样,我们可以轻松识别出与“violin”最相似的 5 个术语:“cello”、“concerto”、“piano”、“sonata”和“clarinet”。
嵌入的主要优势:
-
上下文理解: 与早期的方法不同,嵌入对上下文敏感。这意味着相同的单词可以根据在不同句子中的使用情况具有不同的嵌入,从而提供更为细致的语言理解。
-
语义相似性: 具有相似意义的单词通常在嵌入空间中靠得很近,这使得嵌入非常适合用于音乐搜索引擎或推荐系统中的检索任务。
-
预训练模型: 借助像 BERT 这样的模型,嵌入从大量文本中学习,并可以针对特定任务进行微调,从而显著减少对任务特定数据的需求。
音乐的嵌入
因为嵌入不过是数字,原则上任何东西都可以压缩成有意义的嵌入。下图给出了一个例子,其中不同的音乐类型根据其相似性在二维空间中进行可视化。
音乐类型嵌入在 Every Noise at Once 上的二维空间中可视化。截图由作者提供。
然而,尽管嵌入在工业和学术界已经成功使用了超过 5 年,我们仍然没有广泛采用的领域特定音乐嵌入模型。显然,利用嵌入技术在音乐领域具有巨大的经济潜力。以下是一些可以立即实施的嵌入使用案例,前提是能够获得高质量的音乐嵌入:
-
音乐相似性搜索:在任何音乐数据库中搜索与给定参考曲目相似的曲目。
-
文本到音乐搜索:通过自然语言搜索音乐数据库,而不是使用预定义标签。
-
高效机器学习:基于嵌入的模型通常需要比传统基于频谱图或类似音频表示的方法少 10 到 100 倍的数据进行训练。
到 2023 年,我们在开源高质量音乐嵌入模型方面已经取得了很大进展。例如,Microsoft 和 LAION 都分别发布了针对通用音频领域训练的 CLAP 模型(特定类型的嵌入模型)。然而,这些模型大多是在语音和环境声音上训练的,使得它们在音乐方面的效果较差。随后,Microsoft 和 LAION 发布了针对音乐数据单独训练的音乐特定版本 CLAP 模型。 M-A-P 今年也发布了多个令人印象深刻的音乐特定嵌入模型。
我在测试所有这些模型后的印象是,我们越来越接近目标,但仍未达到三年前文本嵌入所能实现的效果。在我看来,主要瓶颈仍然是数据。我们可以假设像 Google、Apple、Meta、Spotify 等所有主要参与者已经有效地使用音乐嵌入模型,因为他们可以访问巨量的音乐数据。然而,开源社区还没有能够赶上并提供一个令人信服的模型。
摘要:通用音乐嵌入
嵌入技术是一种有前景的技术,它使检索任务更准确,并在数据稀缺时支持机器学习。不幸的是,针对音乐的突破性领域特定嵌入模型尚未发布。我希望并怀疑,开源项目或甚至致力于开源发布的大型公司(如 Meta)将在 2024 年解决这个问题。我们已经很接近,一旦达到一定水平的嵌入质量,每家公司都将采用基于嵌入的音乐技术,以在更短时间内创造更多价值。
3. 弥合技术与实际应用之间的差距
使用 DALL-E 3 生成的图像。
2023 年是个奇怪的一年……一方面,AI 已成为科技界最大的热门词汇,几乎所有终端用户和企业都能找到 ChatGPT、Midjourney 等的应用场景。另一方面,只有少数实际完成的产品已被推出并广泛采用。当然,Drake 现在可以唱“我的心会继续”,但迄今为止,这项技术周围尚未构建出商业案例。而且,是的,AI 现在可以为节拍制作人生成声音样本。然而,实际上,一些作曲家正在努力微调自己的 AI 模型以应对 缺乏有吸引力的商业解决方案。
从这个角度看,音乐 AI 的最大突破可能不是花哨的研究创新。相反,它可能是基于 AI 的产品和服务在满足企业或最终用户需求上的成熟度提升。在这条路上,任何想要构建音乐 AI 产品的人都还面临许多挑战:
-
了解音乐行业或最终用户的需求:技术本身通常对用例无特定要求。了解技术如何满足实际需求是一个关键挑战。
-
将花哨的演示转变为稳健的产品:今天,数据科学家可以在一天内构建一个聊天机器人原型甚至一个音乐生成工具。然而,将一个有趣的演示转变为一个有用、安全和成熟的产品是要求高且耗时的。
-
应对知识产权和许可问题:伦理和法律考虑使公司和用户在提供或采用基于 AI 的产品时犹豫不决。
-
确保资金/投资和首个收入来源:在 2023 年,已经创立了无数音乐 AI 初创公司。强有力的愿景和明确的商业案例将是确保资金和推动产品开发的必要条件。
-
市场营销和用户采纳:即使是最伟大的创新产品现在也容易被忽视。最终用户和企业被关于 AI 未来的报告和承诺淹没,难以触及目标受众。
例如,我们可以更详细地了解 AI 如何通过数字音频工作站(DAW)的新插件来影响音乐制作。在最近的博客文章中,Native Instruments 展示了 10 个新的 AI 驱动插件。为了展示现有的可能性,我们来看一下Audialab 的“Emergent Drums 2”。Emergent Drums 允许音乐家用生成 AI从零开始设计他们的鼓样本。该插件很好地集成到 DAW 中,并作为一个功能齐全的鼓机插件运行。请亲自查看:
演示视频:“Emergent Drums”由 Audialab 提供。
再次放眼远眺,音乐 AI 的潜在应用广泛,从音乐制作到教育、市场营销和分销。利用 AI 的巨大技术潜力为这些领域提供实际价值将是明年面临的关键挑战。
摘要:从研究到产品
2023 年是音乐 AI 的一个重要年份,为未来铺平了道路。2024 年的真正改变者是什么?不仅仅是技术——而是让它在现实场景中为真实的人群服务。预计音乐 AI 将走出实验室,融入我们的生活,影响我们创作和消费音乐的方式。
你好,2024。
2023 年为 AI 及其可能性奠定了技术基础,并提高了公众意识。这就是为什么,我估计 2024 年可能是开始开发音乐 AI 产品的最佳年份。当然,这些产品中的许多将会失败,一些 AI 的承诺最终也会落空。看一下下面图中的著名的 Gartner Hype Cycle,我们应该提醒自己这是正常现象。
Gartner Hype Cycle。图片由作者提供。
没有人可以确定我们目前在这个炒作周期的哪个位置(如果有人知道,请告诉我)。不过,考虑到今年创造的所有基础工作和公众意识,2024 年有可能成为历史性的AI 音乐技术里程碑年。我非常期待明年会带来什么。
成为音乐家真是个好时光!
关于我
我是一名音乐学家和数据科学家,分享我对 AI 与音乐当前话题的看法。以下是与本文相关的一些以往作品:
-
Meta 的 AI 如何基于参考旋律生成音乐:
medium.com/towards-data-science/how-metas-ai-generates-music-based-on-a-reference-melody-de34acd783
-
MusicLM:谷歌是否解决了 AI 音乐生成问题?:
medium.com/towards-data-science/musiclm-has-google-solved-ai-music-generation-c6859e76bc3c
-
AI 音乐源分离:它是如何工作的,以及为何如此困难:
medium.com/towards-data-science/ai-music-source-separation-how-it-works-and-why-it-is-so-hard-187852e54752
高斯混合模型(GMM)的 3 个应用场景
特征工程、无监督分类以及使用 GMM 算法的异常检测
·
关注 发布于 Towards Data Science ·10 分钟阅读·2023 年 7 月 27 日
–
高斯混合模型(GMM)是一种简单而强大的无监督分类算法,基于 K-means 算法来预测每个实例的分类概率。GMM 的这一特性使其在许多应用中都具有很大的灵活性。在本文中,我将讨论 GMM 如何用于特征工程、无监督分类和异常检测。
高斯混合模型(GMM)是什么?
模型描述
尽管单一或多个变量的数据集的高斯分布试图以概率方式表示整个数据集,GMM 假设数据集中存在子群体,并且每个子群体遵循其自己的正态分布。以无监督的方式,GMM 试图学习数据中的子群体及其对每个数据点的概率表示[1]。GMM 的这一特性使我们能够使用模型找到属于任何子群体的概率较低的点,从而将这些点分类为异常值。
GMM 本质上是通过利用组件来表示这些子群体,并修改多变量概率分布函数以适应组件,从而扩展了多变量高斯分布以适应子群体情况。温馨提醒,多变量高斯分布的概率密度函数如下:
多变量高斯分布的概率密度函数
在 GMM 中,每个实例的概率被修改为所有组件的概率和,组件权重被参数化为𝜙。GMM 要求所有组件权重的总和为 1,以便将每个组件视为整体的一个比率。GMM 还结合了每个组件的特征均值和方差。模型如下:
GMM 模型的公式
注意多变量分布与 GMM 之间的相似性。实质上,GMM 算法为每个组件找到正确的权重,这些组件被表示为多变量高斯分布。在他的文章中,Oscar Contreras Carrasco对 GMM 做了精彩的推导[2]。
模型的参数可以通过随机初始化或使用特定策略进行初始化,模型的组件权重𝜙通过重复的期望最大化(EM)步骤来确定[1]。
模型算法
GMM 的实施的第一部分是组件的初始化。GMM 的实施包括初始化步骤,随后是迭代的期望最大化(EM)过程,直到收敛:
第 1 步: 在初始化步骤中,模型参数被初始化:K值从数据集中随机分配为组件均值;组件方差根据随机分配的均值计算;所有组件权重被赋值为 1/K。
步骤 2: 在期望步骤中,我们计算每个数据点由每个组件生成的概率。每个数据点-组件对的期望是该特定组件的权重乘以我们数据点属于该组件的概率(给定组件均值和方差),作为所有其他组件概率的一个分数,参数化为各自的组件权重。基本上,期望步骤尝试找出每个点属于每个组件的可能性,并利用这个值来逐步调整模型参数直到收敛。
期望步骤的公式
步骤 3: 在最大化步骤中,我们重置组件的权重和均值,并根据期望步骤中的γ值重新计算方差。新的组件权重设置为该组件所有数据点期望值的总和。每个组件的新均值是所有数据点的平均值,加权由期望值决定。
最大化步骤的公式
就像在 k-means 算法中一样,如果组件的数量事先不可用,那么猜测组件的数量K是合适的。
下面是 GMM 收敛的视觉示例。在这里,GMM 展示了在具有两个簇的二维数据集上的收敛。该算法的行为类似于 k-means,但不同之处在于它估计概率密度(而不是纯粹对样本数据点的分类)。
旧忠实数据的 EM 聚类 [3]
让我们看看这个算法如何应用于我们的三个用例。
特征工程中的 GMM
尽管一些机器学习模型(如臭名昭著的 XGBoost)可以学习各种输入特征分布,但其他模型对其要求更为严格。线性回归、逻辑回归、线性判别分析(LDA)和多变量高斯通常期望特征呈正态分布,如果数据是多模态的,可能效果不佳。我们可能还有其他分析和视觉上的原因需要处理多模态,而 GMM 可以帮助我们实现这一点。
我们来看一个虚构书店数据集中双峰特征的示例。我从 Kaggle 数据库中提取了这些数据(链接在此),其中包含从 books.toscrape.com [7] 爬取的数据。该数据集包含典型的书店信息,如书名、类别、价格和评分。它还包含书籍数量,这决定了虚构书店库存中的书籍总数。巧合的是,这些书籍具有双峰分布。我们来看看是否可以使用 GMM 作为特征工程技术,从书籍数量数据中创建两个独立的特征。
我使用以下代码在 Python 中实现了这一任务:
让我们查看结果。左侧的图表显示了书籍数量的原始分布。右侧的图表显示了每个预测组件的分布,在 GMM 转换之后。请注意,完整分布的形状完全相同,但两个组件在特定点处拆分了原始分布,创建了两个(大多)正常的直方图。如果我对分割两个组件的点不满意,我可以使用 GMM 预测的概率来调整组件 1 结束和组件 2 开始的位置。
数量在 GMM 之前和之后的分布 [9]
GMM 用于无监督分类
GMM 的另一个应用场景是无监督分类。在这方面,GMM 的工作方式类似于 K-means 算法,但允许对类别归属进行概率性判断(不同于 K-means,其中输出是一个二元度量)。这对于需要自定义分类阈值或简单要求概率输出的用例特别有益。
对于这个例子,我下载了企鹅数据集(在 Kaggle 上提供),并选择了两个特征以进行可视化演示:企鹅的喙长度和深度(喙是企鹅喙的顶部脊)[8]。我删除了空值数据点,并创建了散点图以描绘数据。
当我们查看散点图时,3 个潜在的组脱颖而出。如果我们为实际类别上色,我们会发现,事实上,三种企鹅与三组数据点对齐。这个例子非常基础,因为在现实世界中,我们通常处理的是多维数据,且没有简单的方法来确定数据集中存在多少个子群体。尽管如此,我们还是来看看 GMM 如何帮助我们对数据进行分割。
描述企鹅喙长与深度的散点图 [9]
在下面的 Python 代码片段中,我下载了数据集,删除了空值,选择了两个感兴趣的特征,并将 GMM 模型拟合到它们上面。sklearn
提供了两种预测选项——预测类别和预测类别归属的概率。每个数据点的概率总和等于 1(根据 GMM 算法约束)。
让我们看看每个数据点属于每个组件的概率。下面的三个散点图显示了每个组件实例的概率。透明度较高的点具有较低的概率,而颜色较亮的点具有较高的概率。在下图中,我们可以看到,GMM 对位于不同组件之间的点预测了更大的不确定性,这在预期之中。我们可以使用gmm.predict_proba()
函数来控制类别归属。
描述每个预测类别的类归属概率的散点图 [9]
用于异常检测的 GMM
在我的之前的故事中,我分享了异常检测的基础知识以及统计方法在检测异常中的应用[6]。也就是说,我使用多变量高斯分布来识别低概率符合正态分布的数据点。然而,这种方法的问题是我们的数据往往更复杂。高斯混合模型(GMM)尝试解决多模态问题,并且在特征形成正态分布特征关系的子群体的情况下非常有用。
在这个最后的例子中,我将使用来自ODDS library [10]的葡萄酒数据集。这是我在我的帖子中使用的数据集,在那里我应用了多变量高斯分布来检测离群点。让我们看看我是否可以改进之前的结果,在那个结果中,41 个实例被错误分类,其中 40 个被错误识别为异常点。相比多变量高斯分布方法,第一个优点是我们可以使用所有特征,而不限于仅正态分布的特征。第二个优点是我们可以考虑数据子群体。
GMM 最显著的优势可能是该模型可以帮助发现两种类型的异常数据:一种是人口中的离群点(例如数据录入错误),另一种是形成自己组的异常(例如信用卡欺诈行为)。在葡萄酒数据集中,正实例被构建为两种类型的葡萄酒样本,而数据集中的异常被构建为第三种葡萄酒的子样本 [10]。因此,我们很可能会发现我们的离群点在这个实例中构成了自己的组。
由于我们的数据集中有 13 个特征,让我们将数据汇总到前两个 PCA 组件中并绘制它们。在下面的图表中,我们将看到散点图包含一个密集区域和大约二十个分散的点。数据中没有明显的子群体。
前两个 PCA 组件的散点图,显示真实的异常情况 [9]
下面的代码片段使用sklearn
库中的 2 个组件和标准输入字段来拟合 GMM 模型到我们的数据集。希望一个组件能够识别正实例,而另一个组件能够识别异常。
预测组件的编号是随机的,因此我做了一些背景处理来重新标记预测。你可以查看我的 Jupyter notebook 以获取更多关于如何做到这一点的指导。
结果看起来很好!我们没有假阴性,并且有 11 个错误识别的异常。如果这是现实世界,我们在运行此模型后只需人工检查大约 15%的数据。这已经是对多变量高斯分布方法的显著改进。
GMM 结果的混淆矩阵 [9]
如果我们绘制结果,可以看到被错误识别的异常情况实际上更接近最密集的区域。再次强调,我们仅使用前两个 PCA 组件进行可视化,GMM 在对数据点进行分类时做了更多工作。
前两个 PCA 组件的散点图,显示预测的异常情况 [9]
我们可能通过分析预测的异常概率来改进这个结果。下面,我已经为错误识别的异常实例和真实异常实例绘制了这些概率(没有预测的假阴性)。
条形图显示了在错误识别的异常和真实异常之间的异常类别归属概率 [9]
我们可以看到大多数错误识别的异常概率低于 0.9999。因此,我们可以使用预测的异常概率并设置新的阈值,而不是使用默认分类。如果我们将阈值设置为大于 0.9999 的异常检测,我们将只有 3 个错误识别的异常。这可以用一行代码完成:components = np.where(proba>0.9999, 1, 0)
。在某些情况下,改变阈值可能会增加我们最终结果中的假阴性。幸运的是,这种情况并不存在。
GMM 结果的混淆矩阵,在降低提高概率阈值之后 [9]
在以下 PCA 散点图中,我们可以看到我们的模型仍然预测了 3 个错误识别的异常,但通过新的改进,我们只需手动检查 10% 的数据实例以检测异常。考虑到 8% 的数据实际上是异常的,这真是太棒了!
提高异常概率阈值后的前 2 个 PCA 组件的散点图 [9]
预测类别概率的能力使得 GMM 成为数据科学中一个强大而多用途的工具。尽管它有与 K-means 同类的相同限制,GMM 在特征工程、更灵活的无监督分类和异常检测中非常有用。
我在写这篇文章时非常开心,读它的时候也同样开心。我总是欢迎反馈和问题,所以一定要利用评论区。如果你希望直接反馈,可以随时在 LinkedIn 上找到我。
来源:
-
towardsdatascience.com/gaussian-mixture-models-explained-6986aaf5a95
-
commons.wikimedia.org/wiki/File:EM_Clustering_of_Old_Faithful_data.gif
-
towardsdatascience.com/understanding-anomaly-detection-in-python-using-gaussian-mixture-model-e26e5d06094b
-
towardsdatascience.com/the-basics-of-anomaly-detection-65aff59949b7
-
github.com/viyaleta/Medium-Code-Examples/blob/main/GMM/3%20Use-Cases%20for%20GMM.ipynb
-
Saket Sathe 和 Charu C. Aggarwal. LODES: 局部密度与谱异常检测的结合。SIAM 数据挖掘会议,2016.
odds.cs.stonybrook.edu/wine-dataset/
34% 更快的整数到字符串转换算法
我们打印整数的速度够快吗?
·
关注 发表在 Towards Data Science ·14 min read·2023 年 12 月 4 日
–
1. 介绍
在计算机编程中,将给定的整数转换为字符串是一项常见操作,例如在将整数打印到屏幕上或任何文本文件(如 *.xml, *.json, *.csv, *.txt 等)之前需要进行此操作。
众所周知,整数(以及其他所有内容)在计算机内存中以二进制格式存储——即 0 和 1 的序列。例如:
-
数字 12 在内存中的表示为“1100”。
-
数字 29 被表示为“11101”。
这就是为什么每次我们想要将其转换为人类可读的十进制格式时,需要进行这样的转换。
在这个故事中,我将要:
-
对用于这种转换的标准算法进行概述,
-
观察其现有的优化,
-
提出我的算法,并且
-
展示他们的实验比较。
我们将看到,我的算法对于 32 位整数运行**25–38%更快,对于 64 位整数运行40–58%**更快,相比于优化的标准算法。其在 C++语言中的实现可以在 GitHub 上找到,如文末引用。
当然,如果应用在其生命周期内只打印少量整数,那么负责将它们转换为字符串的算法不会成为瓶颈。但是,对于那些将大量数据打印到文本文件中的情况,转换算法的效率就开始发挥作用。在数据科学或机器学习等领域,转换大量整数为字符串的需求很常见,例如在将数据集导出到文本文件时,如*.csv 或*.json。
2. 标准转换算法
将整数转换为字符串是一个常见操作,任何现代编程语言中都有实现这样的算法,无论是作为语言的一部分还是作为标准库的一部分。而且这个算法几乎在所有地方都是相同的——基于反复获取并提取整数的最后一个数字,然后继续处理剩余部分。
为了获得给定整数N的最后一位数字,它只是计算其除以 10 的余数:
“digit := N mod 10”,
并且为了提取它,执行整数除法:
“N := N / 10”。
*给定一个整数 N,它的最后一位数字
其余部分正在计算中。*
请注意,在这个故事中,当除以两个整数时,我们将假设只取结果的整数部分。
作为完整算法的示例,当打印数字“N = 2’167”时,将执行以下操作:
*打印数字“2167”的操作:
第一步:2167 % 10 = 7(存储数字“7”),2167 / 10 = 216(继续处理 216),
第二步:216 % 10 = 6(存储数字“6”),216 / 10 = 21(继续处理 21),
第三步:21 % 10 = 1(存储数字“1”),21 / 10 = 2(继续处理 2),
第四步:由于“2 < 10”,只存储最后一个数字“2”。
第五步:(未示例)反转存储的数字的顺序并打印它们。*
请注意,当我们处理 1 位整数(即范围为[0…9])时,我们可以直接发送进行打印,因为这些 10 个数字中的每一个对应的字符已经固定。且除以 10 的余数总是 1 位整数。
我们还可以注意到,这个算法报告的* N *的数字是倒序的(这里我们得到的数字序列是‘7’,‘6’,‘1’,‘2’,而不是‘2’,‘1’,‘6’,‘7’),所以在最后需要将生成的序列进行反转。
总结一下,它的伪代码如下:
var result[0 .. 25] : Array of Characters // Assume at most 25 characters
// The procedure takes integer 'N' to be printed, and fills its
// decimal characters into 'result' array.
procedure print( N: Integer )
i := 0 // Index over 'result' array
while N > 0
result[ i ] := '0' + (N mod 10) // Take the last digit
N := ⌊ N / 10 ⌋ // Pick out the last digit
i := i+1
result[ i ] := '\0' // Append the terminating 'null' character
reverse array result[0 .. i-1]
描述的算法很简单,我们可以用 3–4 行代码轻松实现。但它的瓶颈在于对N的每一位小数表示使用了两个相对昂贵的操作——整数除法和整数余数计算。众所周知,整数除法和余数计算平均花费的时间比两个整数的加法、减法甚至乘法要长 4–5 倍。这里我们可以观察到上述算术操作的时间基准:
*时间(以纳秒为单位)花费的实验比较,用于执行 5 种类型的
算术操作(每个操作在随机数据上运行 200 次)。
我们可以看到最后两个操作(整数除法和余数计算)
花费的时间显著更多。此外,我们看到整数乘法
执行的速度几乎与加法或减法一样快。*
实验是在以下系统下使用 Google Benchmark 进行的:
*CPU: Intel Core i7–11800H @ 2.30GHz
内存:16.0 GB
操作系统:Windows 11 Home,64 位
编译器:MSVC 2022 (/O2 /Ob2 /MD /GR /Gd)*
让我们看看是否存在更快的整数打印方法…
3. 现有优化
优化 1
对于描述的算法,一个常见的优化是消除最后一步反转生成的数字序列。这个技巧在例如 [1] 中有很好的介绍。在这个优化中,我们将数字直接按正确的顺序写入缓冲区。由于算法本身从右到左报告给定整数N的数字,所以我们也将它们从右到左写入缓冲区。
*将生成的数字从右到左填入结果数组,
直接以它们在最终位置的顺序。*
伪代码将如下所示:
var result[0 .. 25] : Array of Characters // Assume at most 25 characters
// The function takes integer 'N' to be printed, and returns position
// of its converted first character in the 'result' array.
function print( N: Integer ) : Integer
result[ 25 ] := '\0' // Place the terminating 'null' character at the end
i := 25 // Index over 'result' array
while N > 0
i := i-1 // Here we go to left, for placing the next digit
result[ i ] := '0' + (N mod 10) // Take the last digit
N := ⌊ N / 10 ⌋ // Pick out the last digit
return i // Position from where the converted integer starts
注意,在本故事的此处和所有其他伪代码中,我们没有处理打印数字“0”的情况。根据所有编写的算法,“0”将显示为没有任何位的序列,因此在几乎所有打印算法中,打印“0”都在一个单独的分支中完成。我们这里只是为了简洁跳过了这个分支。
这个优化的另一个小优点是我们不需要在每次转换后都写入终止的空字符。相反,我们只需在缓冲区的最后一个位置写入一次,因为N的最后一位的物理位置是预先固定的,它将始终是缓冲区中倒数第二个位置。
这种优化的缺点是第一个字符的位置变得可变,因为它取决于整数N的位数。
*优化 1 的缺点:不同
位数计数将在输出数组中从不同的位置开始。*
然而,实际上这不会成为问题,因为转换后的整数通常会立即发送到文本文件或屏幕上,因此不会在内存中停留太久。对于这样的目的,我们不需要转换的数字从内存中某个精确指定的位置开始写入。
优化 2
下一项优化是通过使用整数除法和余数计算操作来在单一步骤中获取N的 2 位数字。这个技巧在[1]和[2]中也有详细记录。为此,我们不再重复计算
“digit := N mod 10”,接着
“N := N / 10”,
我们将计算:
“digits := N mod 100”,接着
“N := N / 100”,
这将给我们N的最后 2 位数字,然后将它们都剪掉。
*启用第二个优化的数字“5174092”打印操作:
步骤 1:5174092 % 100 = 92(存储数字“92”),5174092 / 100 = 51740(继续处理 51740),
步骤 2:51740 % 100 = 40(存储数字“40”),51740 / 100 = 517(继续处理 517),
步骤 3:517 % 100 = 17(存储数字“17”),517 / 100 = 5(继续处理 5),
步骤 4:由于“5 < 100”,只存储最后一位数字“5”。*
请注意,为了最终高效地打印这些获得的 2 位数字,我们应该准备一个长度为 100 的数组(索引从 0 到 99——因此对应所有可能的余数“N mod 100”),其中的值将是一对字符,从“00”,“01”,“02”,……一直到“98”,“99”。
在此优化中,整数除法和余数操作的数量减少了近 2 倍。
完成这部分后,我想引起你们的注意,即使启用了上述两个优化,我们仍然会进行与给定整数N中的数字数量成正比的整数除法和余数计算操作。
4. 我的算法
我打算提出另一种算法,这将使 32 位整数的整数打印加速约25–38%,64 位整数的加速约40–58%。其思想是——如果我们从给定整数N中提取数字时不是从右到左,而是从左到右呢?所以首先我们会获得最重要的数字,然后是下一个重要的数字,依此类推,直到只剩下最不重要的数字。如果我们事先不知道N的位数,这会变得有点困难,但现在让我们暂时搁置这个问题,假设我们已经知道N中有L位数字。
具有 L=7 位的输入数字 N 的示例。
那么我们如何获得最重要的数字呢?再次使用整数除法,但这次为:
“digit := N / 10^(L-1)”
获取给定整数的最左侧数字的示例。
那么我们如何从 N 中提取它,以便能够继续处理剩余部分?在知道最重要的数字是‘d’后,我们可以进行以下减法操作:
“N := N — d*10^(L-1)”
从给定整数中提取最左边数字的示例。
后续我们将重复除法和减法操作,直到 N 变为 1 位整数(即范围 [0…9]),最终也会打印该数字。让我们看看算法在“N = 6’129”情况中的表现。请注意,它有 4 位数字,所以这里我们从“L=4”开始。
*使用我的算法打印数字“6129”的操作:
步骤 1:6129 / 1000 = 6(打印数字‘6’),6129–6*1000 = 129(继续处理 129),
步骤 2:129 / 100 = 1(打印数字‘1’),129–1*100 = 29(继续处理 29),
步骤 3:29 / 10 = 2(打印数字‘2’),29–2*10 = 9(继续处理 9),
步骤 4:由于“9 < 10”,只需打印最后一位数字‘9’。*
你可能会争辩说,计算不同的 10 的幂比进行整数除法或取余计算更耗时。这绝对正确,除了一个细节:我们可以预计算所有必要的 10 的幂,并在程序的整个执行过程中使用它们。对于 32 位整数,只有 10 个不同的 10 的幂,对于 64 位整数,有 20 个 10 的幂。因此,将它们全部预计算并保存在内存中不会成为问题。
那么总体上我们有什么?为了用我的算法打印一个 N 的数字,我们做:
1 次整数除法,
1 次乘法,以及
1 次减法,
与标准算法相比:
1 次取余计算以及
1 次整数除法。
在下一节中,我们将看到我的方法实际上更好,因为乘法和减法加起来比取余计算消耗的 CPU 时间更少。这些算术操作的时间消耗实验比较在第二章中介绍过。
我算法的主要部分的伪代码可能如下所示:
var powers_of_10[0 .. 10] : Array of Integers
= { 1, 10, 100, 1'000, ..., 100'000'000, 1'000'000'000 }
// Precalculated powers of 10, which will be used during print
var result[0 .. 25] : Array of Characters // Assume at most 25 characters
// The procedure takes integer 'N' to be printed, and fills its
// decimal characters into the 'result' array.
procedure print( N: Integer )
L := calculate_digits_count( N )
i := 0 // Index over 'result' array
while L > 0
digit := ⌊ N / powers_of_10[ L-1 ] ⌋ // Obtain left-most digit
result[ i ] := '0' + digit // Write it to the 'result' array
N := N – digit * powers_of_10[ L-1 ] // Calculate remaining part
L := L-1 // Adjust its count of digits accordingly
i := i+1
result[ i ] := '\0' // Append the terminating 'null' character
由于我的算法从左到右打印 N 的数字,我想称之为“左到右打印机”或简短为“LR 打印机”。
还有一件事需要高效找到 L — N 的十进制数字计数。幸运的是,预计算的 10 的幂数组在这里也会有帮助。我们只需从小的幂次迭代到较大的幂次,直到找到比 N 大的幂 10^L。然后,指数 L 本身将表示 N 中的数字计数。
例如,获取“N = 23’504”的数字计数如下所示:
如何计算数字 N = 23’504 的数字计数 L。
我们依次将 N 与 10 的幂比较,直到 N 变小。
这发生在 100’000 的幂次上,即 10⁵,因此我们得出结论 L=5。*
该函数的伪代码可能如下所示:
// The function takes integer 'N' and returns count of its digits.
function calculate_digits_count( N: Integer ) : Integer
// Check case of numbers with maximal count of digits
if N >= powers_of_10[ 9 ] // Compare with maximal power of 10
return 10 // Count of digits for such numbers
// Regular case
L := 0
while N >= powers_of_10[ L ]
L := L+1
return L
通过这两个部分,我们提供了将整数转换为字符串的完整算法。
请注意,由于“LR 打印机”从左到右报告 N 的数字,因此最后不需要做任何反转。此外,与现有的优化 1 相比,这里我们保留了指定转换后的 N 的第一个数字应放置在内存中的位置的能力。
“LR 打印机”可以用于打印任何基数的数字(不仅仅是 base 10)。为此,我们只需要用新基数的预计算幂替换预计算的 10 的幂。
“LR 打印机”在 C++ 语言中的实现可以在 GitHub 上找到,链接为 [3]。
“LR 打印机”的优化 2
我的算法可以通过在“现有优化”部分中描述的第二次优化进行增强,并在 [1] 和 [2] 中进行了记录。如果进行优化,则我们将每次打印 2 位数字,而不是逐位打印。
让我们看看它如何在数字“N = 4’610’937”上运行。这里 L=7,我们这次从将 N 除以 10^(L-2)=10’000 开始:
*启用第二次优化的“LR 打印机”打印数字“4610937”的操作:
步骤 1:4610937 / 10⁵ = 46(打印数字‘46’),4610937–46*10⁵ = 10937(继续处理数字 10937),
步骤 2:10937 / 10³ = 10(打印数字‘10’),10937–10*10³ = 937(继续处理数字 937),
步骤 3:937 / 10 = 93(打印数字‘93’),937–93*10 = 7(继续处理数字 7),
步骤 4:由于“7 < 100”,只打印最后一位数字‘7’。*
启用此功能后,我们将花费:
1 次整数除法,
1 次乘法,以及
1 次减法,
每 2 位输入数字。
在这里,数字将按其自然顺序 — 从左到右获取,因此无需在最后进行反转。
启用第二次优化的“LR 打印机”的实现也可以在 GitHub 上找到,链接为 [3]。
5. 与现有算法的实验比较
进行实验比较对于这类工作至关重要,因此在本章中,我将展示以下整数打印算法的比较结果:
-
第一个优化的标准算法(标记为“Std”),
-
我的算法“LR 打印机”(标记为“LR”),
-
标准算法的第二次优化(标记为“Std [2-dig]”)和
-
含第二次优化的“LR 打印机”(标记为“LR [2-dig]”)。
这些算法都在 32 位和 64 位整数上进行了测试,输入数字的位数不同。
在 base=10 中打印数字:
在基数=10(普通情况)下打印的结果是:
*打印 1 个数字(无论是 32 位还是 64 位)所花费的时间(以纳秒为单位),
具有特定位数的不同算法。
打印是在 base=10 中完成的。*
对于 32 位整数,我们可以看到“LR printer”相较于标准打印机的性能提升约为30–38%。当启用第二次优化(每步打印 2 位)时,性能提升较低,为13–28%。这是完全预期的,因为总体上我们只执行了 2 或 4 步。
在打印 64 位整数时,我的算法表现更佳。“LR printer”比标准算法快约40–50%。当两者都启用第二次优化时,“LR printer”性能提升47–58%。
本故事标题中的百分比是通过考虑最常见的情况选择的:当我们在 base=10 下处理 32 位整数,并假设它们有许多位数时。在这种情况下,“LR printer”相对于标准算法的性能提升为 30–38%,所以取平均数大约为 34%。
在 base=3 中打印数字:
让我们看看在其他基数下打印整数时结果是否会显著不同。我们将观察在数字 base=3 中的打印情况:
*打印一个数字(无论是 32 位还是 64 位)所花费的时间(以纳秒为单位),
具有一定数量位数的情况下,使用不同算法。
打印是在 base=3 中进行的。
如我们所见,对于 32 位整数,“LR-printer”相对于标准算法的性能提升约为25–33%,这通常对应于所使用算术操作的性能差异。
对于 64 位整数,“LR-printer”的性能提升约为短数字(8 位)50–55%,长数字(36 位)27–30%。
总体备注
通常,整数打印的基数不会对相对性能提升产生太大影响,因为打印过程中要执行的操作数量与输入数字的位数成正比,而不是这些位数可能具有的值的数量。
几乎总是这样,数字位数越多,“LR-printer”(或“LR-printer [2-dig]”变体)比标准打印算法(或其“2-dig”变体)的表现会更好。这一点也很明确,因为位数越多,循环外指令的影响(如从一个函数调用另一个函数或放置空字符)越小。
总体来说,在打印 64 位整数时,“LR-printer”和“LR-printer [2-dig]”变体的结果都更令人印象深刻。
对我个人而言,这些结果相当显著。
6. 结论
我们提出了一种将整数转换为字符串的新算法,称为“LR printer”。与优化后的标准转换算法相比,它在 32 位整数上运行**25–38%更快,在 64 位整数上运行40–58%**更快。我们的算法可以在任何数字基数下工作(不仅仅是在普通的 base 10 下)。
将整数转换为字符串的算法在仅打印少量数字的应用程序中从不成为瓶颈。但对于其他类型的应用程序,例如自动生成 *.csv、*xml 或 *.json 等文本文件的应用程序,转换算法的效率就显得尤为重要。特别是当这些文本文件将包含大量数字时,例如在导出大型数据集时。
非常感谢你读到最后!很高兴看到你在下面的评论!
我向 David Ayrapetyan 表示感谢 (
www.linkedin.com/in/davidayrapetyan/
),感谢他仔细审阅了本故事的草稿,并提出了多个上下文改进和语法修正。感谢 Hayk Aslanyan (
www.linkedin.com/in/haykaslanyan/
),感谢他对草稿进行了技术审查,并提出了其他改进建议。插图设计由 Asya Papyan 制作:
www.behance.net/asyapapyan
如果你喜欢阅读这个故事,可以在 LinkedIn 上找到我:
www.linkedin.com/in/tigran-hayrapetyan-88989b12/
参考文献
[1] : “整数到字符串转换” — tia.mat.br/posts/2014/06/23/integer_to_string_conversion.html
[2] : “C++ 的三个优化技巧” — www.facebook.com/notes/10158791579037200/
[3] : “C++ 语言中的 LR 打印机实现” — github.com/tigranh/lr_printer
数据可视化中的 3D 和动效
原文:
towardsdatascience.com/3d-and-motion-in-data-visualisation-d25a386810dd
数据可视化、Python 和 3D 创作软件,完美的融合。来源:作者
许多好莱坞特效背后的开源工具如何帮助创建令人惊叹的数据可视化
·发表于 Towards Data Science ·3 分钟阅读·2023 年 1 月 20 日
–
数据可视化领域已经非常成熟,拥有一些出色的文献[1,2],指导如何以视觉方式传达数据。
不幸的是,随着这种成熟度的提高,带来了平淡。如今的大多数可视化都是单调的,原始性被摒弃,采用经过验证的方法。
当我们有证据表明,使用条形对比长度是最有效的数据表示方式时[3],我们如何避免一切都变成条形图呢?
有用的数据可视化备忘单。来源:作者
为数据可视化注入兴奋感
在我之前的两篇文章中,我讨论了为什么我们应该使用动画和3D在数据可视化中的应用。
两种方法的有力论点是需要为一个应该引发兴趣和刺激的领域注入兴奋感。创造新的、不同的和原创的东西是关键。真正的原创性很难[4],但对数据可视化的持续发展至关重要。
在这篇文章中,我们将覆盖一个实际示例,展示如何实现这一点。我们将从电影行业获取灵感;将特效工具和数据结合在一起,创造出不同的东西。
下面的示例可以看到。它比较了基于 GitHub 活动的编程语言的变化。创建此图表的代码在本文末尾提供。
首次(?) 3D 蝴蝶图表比赛的视频。来源:作者
但首先,让我们更详细地探讨一下我们将使用的工具。
Blender 简介
Blender 是一个免费的开源 3D 创作套件。它可以用于创建各种计算机生成的内容,包括多个大片电影的特效。
Blender 在数据科学和数据可视化方面特别有趣,因为它拥有一个完整的 Python API,可以用于程序化地构建可视化。
这开启了几乎无限的可能性。单个 Python 工作流可以用于处理数据,然后使用 Blender 根据这些数据创建强大、逼真的 3D 渲染!
你可以在其网站上找到更多信息以及下载 Blender,blender.org。
一个简单的示例
虽然完整的教程超出了本文的范围,但以下内容展示了如何轻松入门。
它基于 Python 列表创建一系列立方体,并将这些立方体放置在场景中,并添加一个光源:
少于 15 行代码的条形图
经过一些小的调整,Blender 可以渲染出下面的条形图,代表上面脚本中的数据列表:
只需几行代码即可生成条形图。来源:作者
Blender 网站上提供了全面的文档,允许你轻松在此示例基础上进行扩展。如前所述,实际上没有什么限制,因为一切都可以通过 Python API 来控制。这意味着你可以:
-
通过设置关键帧创建动画。
-
设置一个场景,完全控制光照、颜色和材质。
-
确定相机位置并控制其随时间的移动。
有了这些考虑,创建上面视频中展示的“蝴蝶竞赛”图表相对简单。
可以在此找到完整的脚本和数据。
结论
希望这篇文章能激发一些灵感,帮助我们摆脱数据可视化的现状,通过借鉴数据科学以外的技术和工具,为内容注入兴奋感和原创性。
如果你对数据可视化的原创方法有其他想法,请在评论中添加。
参考文献:
-
《定量信息的视觉展示》。爱德华·R·塔夫特,1983 年
-
《功能艺术:信息图形和可视化导论》。阿尔贝托·卡伊罗,2011 年
-
科学数据的图形感知和图形方法。威廉·S·克里夫兰;罗伯特·麦吉尔,1985 年
-
结构性想象:类别结构在示例生成中的作用。沃德·T·B,1994 年
使用 Open3D 进行 3D 数据处理
使用 Python 的 Open3D 库处理 3D 模型的简洁指南(附带互动式 Jupyter Notebook)
·
关注 发表在 数据科学之路 ·13 分钟阅读·2023 年 5 月 8 日
–
在这篇文章中,我提供了一个简洁的指南,讲解如何使用 Python 的Open3D库来探索、处理和可视化 3D 模型——这是一个开源的 3D 数据处理库。
使用 Open3D 可视化的 3D 模型(原始 3D 模型可在这里找到)。
如果你考虑处理 3D 数据/模型以进行特定任务,如训练 AI 模型进行 3D 模型分类和/或分割,你可能会发现这个讲解很有帮助。互联网上的 3D 模型(如 ShapeNet 数据集中)有多种格式,如 .obj、.glb、.gltf 等。使用像 Open3D 这样的库,这些模型可以轻松处理、可视化并转换为其他格式,例如更易于理解和解释的点云。
本文也提供了 Jupyter Notebook 版本,适合那些希望跟随并在本地运行代码的读者。包含 Jupyter Notebook 及所有其他数据和资产的 zip 文件可以从以下链接下载。
[## 3D 数据处理与 Open3D.zip
包含 Jupyter Notebook、3D 模型及所有其他必要文件的 zip 文件。
drive.google.com](https://drive.google.com/file/d/1290xG3_BEYn9WN9TwkYKMmNbNRsrWjGk/view?usp=share_link&source=post_page-----c3062aadc72e--------------------------------)
在本教程中,我将介绍以下任务:
-
加载和可视化一个 3D 模型作为 网格
-
通过采样点将 网格 转换为 点云
-
从 点云 中移除隐藏点*
-
将 点云 转换为数据框
-
保存 点云 和数据框
让我们先导入所有必要的库:
# Importing open3d and all other necessary libraries.
import open3d as o3d
import os
import copy
import numpy as np
import pandas as pd
from PIL import Image
np.random.seed(42)
# Checking the installed version on open3d.
o3d.__version__
# Open3D version used in this exercise: 0.16.0
加载和可视化一个 3D 模型作为 网格
可以通过运行以下代码行将 3D 模型读取为网格:
# Defining the path to the 3D model file.
mesh_path = "data/3d_model.obj"
# Reading the 3D model file as a 3D mesh using open3d.
mesh = o3d.io.read_triangle_mesh(mesh_path)
要可视化网格,请运行以下代码行:
# Visualizing the mesh.
draw_geoms_list = [mesh]
o3d.visualization.draw_geometries(draw_geoms_list)
网格应在新窗口中打开,呈现出下图所示的样子(请注意,网格以静态图像形式打开,而不是这里展示的动画图像)。可以使用鼠标指针旋转网格图像。
3D 模型作为网格可视化(在估计表面法线之前)。
如上所示,汽车网格看起来不像典型的 3D 模型,且被涂成均匀的灰色。这是因为网格没有关于 3D 模型中顶点和表面的 法线 信息。
什么是 法线? — 在给定点的表面上的法线向量是垂直于该点表面的向量。法线向量通常被称为“法线”。要了解更多相关内容,可以参考这两个链接:法线向量 和 点云中的表面法线估计。
可以通过运行以下代码行来估计上述 3D 网格的 法线:
# Computing the normals for the mesh.
mesh.compute_vertex_normals()
# Visualizing the mesh.
draw_geoms_list = [mesh]
o3d.visualization.draw_geometries(draw_geoms_list)
一旦可视化,网格应如下面图像所示。计算 法线 后,汽车模型将正确渲染,看起来像一个 3D 模型。
3D 模型可视化为网格(估算表面法线后)。
让我们创建一个 XYZ 坐标系,以了解这个车模在欧几里得空间中的方向。XYZ 坐标系可以覆盖在上面的 3D 网格上,通过运行以下代码行进行可视化:
# Creating a mesh of the XYZ axes Cartesian coordinates frame.
# This mesh will show the directions in which the X, Y & Z-axes point,
# and can be overlaid on the 3D mesh to visualize its orientation in
# the Euclidean space.
# X-axis : Red arrow
# Y-axis : Green arrow
# Z-axis : Blue arrow
mesh_coord_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=5, origin=[0, 0, 0])
# Visualizing the mesh with the coordinate frame to understand the orientation.
draw_geoms_list = [mesh_coord_frame, mesh]
o3d.visualization.draw_geometries(draw_geoms_list)
3D 网格可视化带有 XYZ 坐标系。X 轴:红色箭头,Y 轴:绿色箭头,Z 轴:蓝色箭头 [容易记住的方式 — XYZ::RGB]
从上述可视化中,我们可以看到这辆车的网格方向如下:
-
XYZ 轴的原点:在车模的体积中心(在上面的图像中未见,因为它在车模网格内部)。
-
X 轴(红色箭头):沿着汽车的长度维度,正 X 轴指向汽车的引擎盖(在上面的图像中未见,因为它在车模网格内部)。
-
Y 轴(绿色箭头):沿着汽车的高度维度,正 Y 轴指向汽车的车顶。
-
Z 轴(蓝色箭头):沿着汽车的宽度维度,正 Z 轴指向车的右侧。
现在我们来看看这个车模内部的情况。为此,我们将沿 Z 轴裁剪网格,并移除车的右半部分(正 Z 轴)。
# Cropping the car mesh using its bouding box to remove its right half (positive Z-axis).
bbox = mesh.get_axis_aligned_bounding_box()
bbox_points = np.asarray(bbox.get_box_points())
bbox_points[:, 2] = np.clip(bbox_points[:, 2], a_min=None, a_max=0)
bbox_cropped = o3d.geometry.AxisAlignedBoundingBox.create_from_points(o3d.utility.Vector3dVector(bbox_points))
mesh_cropped = mesh.crop(bbox_cropped)
# Visualizing the cropped mesh.
draw_geoms_list = [mesh_coord_frame, mesh_cropped]
o3d.visualization.draw_geometries(draw_geoms_list)
在 Z 轴上裁剪的 3D 网格,右半部分的车被移除(正 Z 轴)。裁剪后的网格展示了这个 3D 车模的详细内部结构。
从上述可视化中,我们看到这个车模有详细的内部结构。现在我们已经看到这个 3D 网格内部的内容,我们可以在移除属于车内部的“隐藏”点之前,将其转换为点云。
通过采样点将网格转换为点云
在 Open3D 中,将网格转换为点云可以很容易地完成,通过定义我们希望从网格中采样的点的数量。
# Uniformly sampling 100,000 points from the mesh to convert it to a point cloud.
n_pts = 100_000
pcd = mesh.sample_points_uniformly(n_pts)
# Visualizing the point cloud.
draw_geoms_list = [mesh_coord_frame, pcd]
o3d.visualization.draw_geometries(draw_geoms_list)
从 3D 网格中均匀采样的 100,000 个点创建的 3D 点云。
请注意,上面的点云中的颜色仅指示点在 Z 轴上的位置。
如果我们像对网格一样裁剪点云,它会是这样的:
# Cropping the car point cloud using bounding box to remove its right half (positive Z-axis).
pcd_cropped = pcd.crop(bbox_cropped)
# Visualizing the cropped point cloud.
draw_geoms_list = [mesh_coord_frame, pcd_cropped]
o3d.visualization.draw_geometries(draw_geoms_list)
在 Z 轴上裁剪的 3D 点云,右半部分的车被移除(正 Z 轴)。与上面的裁剪网格类似,裁剪后的点云也展示了这个 3D 车模的详细内部结构。
我们在裁剪后的点云的可视化中看到,它还包含了属于车模内部的点。这是预期的,因为这个点云是通过从整个网格中均匀采样点创建的。在下一部分中,我们将移除这些属于车内部并不在点云外表面的“隐藏”点。
从点云中移除隐藏点
想象你将光线照射到汽车模型的右侧。所有落在 3D 模型右侧外表面的点都会被照亮,而点云中的其他点则不会。
插图展示了 Open3D 的隐藏点移除功能如何在从给定视角查看的点云上工作。所有被照亮的点被认为是“可见”的,而所有其他点被认为是“隐藏”的。
现在让我们将这些被照亮的点标记为“可见”,所有未被照亮的点标记为“隐藏”。这些“隐藏”的点还包括所有属于汽车内部的点。
该操作在 Open3D 中称为隐藏点移除。为了在点云上执行此操作,使用 Open3D,运行以下代码行:
# Defining the camera and radius parameters for the hidden point removal operation.
diameter = np.linalg.norm(np.asarray(pcd.get_min_bound()) - np.asarray(pcd.get_max_bound()))
camera = [0, 0, diameter]
radius = diameter * 100
# Performing the hidden point removal operation on the point cloud using the
# camera and radius parameters defined above.
# The output is a list of indexes of points that are visible.
_, pt_map = pcd.hidden_point_removal(camera, radius)
使用上述输出的可见点索引列表,我们可以在可视化点云之前,用不同的颜色标记可见点和隐藏点。
# Painting all the visible points in the point cloud in blue, and all the hidden points in red.
pcd_visible = pcd.select_by_index(pt_map)
pcd_visible.paint_uniform_color([0, 0, 1]) # Blue points are visible points (to be kept).
print("No. of visible points : ", pcd_visible)
pcd_hidden = pcd.select_by_index(pt_map, invert=True)
pcd_hidden.paint_uniform_color([1, 0, 0]) # Red points are hidden points (to be removed).
print("No. of hidden points : ", pcd_hidden)
# Visualizing the visible (blue) and hidden (red) points in the point cloud.
draw_geoms_list = [mesh_coord_frame, pcd_visible, pcd_hidden]
o3d.visualization.draw_geometries(draw_geoms_list)
从上图所示的相机视角下的隐藏点移除操作后的点云。蓝色为“可见”点,而红色为“隐藏”点。
从上面的可视化中,我们可以看到隐藏点移除操作如何在给定的相机视角下工作。该操作从给定的相机视角下消除所有被前景点遮挡的背景点。
为了更好地理解这一点,我们可以再次重复相同的操作,但这次在轻微旋转点云之后。实际上,我们在这里尝试改变视角。但不是通过重新定义相机参数来改变视角,而是通过旋转点云本身。
# Defining a function to convert degrees to radians.
def deg2rad(deg):
return deg * np.pi/180
# Rotating the point cloud about the X-axis by 90 degrees.
x_theta = deg2rad(90)
y_theta = deg2rad(0)
z_theta = deg2rad(0)
tmp_pcd_r = copy.deepcopy(pcd)
R = tmp_pcd_r.get_rotation_matrix_from_axis_angle([x_theta, y_theta, z_theta])
tmp_pcd_r.rotate(R, center=(0, 0, 0))
# Visualizing the rotated point cloud.
draw_geoms_list = [mesh_coord_frame, tmp_pcd_r]
o3d.visualization.draw_geometries(draw_geoms_list)
3D 点云绕 X 轴旋转了 90 度。注意,现在,与之前不同的是,Y 轴(绿色箭头)沿着汽车的宽度方向,而 Z 轴(蓝色箭头)沿着汽车的高度方向。X 轴(红色箭头)没有变化,仍然沿着汽车的长度方向。
插图展示了隐藏点移除操作如何在从之前相同视角查看的旋转点云上工作。如前所述,所有被照亮的点被认为是“可见”的,而所有其他点被认为是“隐藏”的。
通过在旋转的汽车模型上重复相同的过程,我们将看到这次所有落在 3D 模型上表面(汽车的车顶)的点会被照亮,而点云中的其他点则不会。
我们可以通过运行以下代码行来重复对旋转点云的隐藏点移除操作:
# Performing the hidden point removal operation on the rotated point cloud
# using the same camera and radius parameters defined above.
# The output is a list of indexes of points that are visible.
_, pt_map = tmp_pcd_r.hidden_point_removal(camera, radius)
# Painting all the visible points in the rotated point cloud in blue,
# and all the hidden points in red.
pcd_visible = tmp_pcd_r.select_by_index(pt_map)
pcd_visible.paint_uniform_color([0, 0, 1]) # Blue points are visible points (to be kept).
print("No. of visible points : ", pcd_visible)
pcd_hidden = tmp_pcd_r.select_by_index(pt_map, invert=True)
pcd_hidden.paint_uniform_color([1, 0, 0]) # Red points are hidden points (to be removed).
print("No. of hidden points : ", pcd_hidden)
# Visualizing the visible (blue) and hidden (red) points in the rotated point cloud.
draw_geoms_list = [mesh_coord_frame, pcd_visible, pcd_hidden]
o3d.visualization.draw_geometries(draw_geoms_list)
从上图所示的相机视角隐藏点去除操作后的旋转点云。再次,可见的点为蓝色,而隐藏的点为红色。
上述旋转点云的可视化清楚地说明了隐藏点去除操作的工作原理。因此,为了从这个汽车点云中删除所有的“隐藏”点,我们可以通过依次绕三个轴线将点云略微旋转从 -90 度到 +90 度来顺序执行这个隐藏点去除操作。在每次隐藏点去除操作之后,我们可以聚合出点的索引输出列表。**在所有隐藏点去除操作之后,点的聚合索引列表将包含所有未隐藏的点(即,位于点云外表面上的点)。**以下代码执行这个顺序隐藏点去除操作:
# Defining a function to rotate a point cloud in X, Y and Z-axis.
def get_rotated_pcd(pcd, x_theta, y_theta, z_theta):
pcd_rotated = copy.deepcopy(pcd)
R = pcd_rotated.get_rotation_matrix_from_axis_angle([x_theta, y_theta, z_theta])
pcd_rotated.rotate(R, center=(0, 0, 0))
return pcd_rotated
# Defining a function to get the camera and radius parameters for the point cloud
# for the hidden point removal operation.
def get_hpr_camera_radius(pcd):
diameter = np.linalg.norm(np.asarray(pcd.get_min_bound()) - np.asarray(pcd.get_max_bound()))
camera = [0, 0, diameter]
radius = diameter * 100
return camera, radius
# Defining a function to perform the hidden point removal operation on the
# point cloud using the camera and radius parameters defined earlier.
# The output is a list of indexes of points that are not hidden.
def get_hpr_pt_map(pcd, camera, radius):
_, pt_map = pcd.hidden_point_removal(camera, radius)
return pt_map
# Performing the hidden point removal operation sequentially by rotating the
# point cloud slightly in each of the three axes from -90 to +90 degrees,
# and aggregating the list of indexes of points that are not hidden after
# each operation.
# Defining a list to store the aggregated output lists from each hidden
# point removal operation.
pt_map_aggregated = []
# Defining the steps and range of angle values by which to rotate the point cloud.
theta_range = np.linspace(-90, 90, 7)
# Counting the number of sequential operations.
view_counter = 1
total_views = theta_range.shape[0] ** 3
# Obtaining the camera and radius parameters for the hidden point removal operation.
camera, radius = get_hpr_camera_radius(pcd)
# Looping through the angle values defined above for each axis.
for x_theta_deg in theta_range:
for y_theta_deg in theta_range:
for z_theta_deg in theta_range:
print(f"Removing hidden points - processing view {view_counter} of {total_views}.")
# Rotating the point cloud by the given angle values.
x_theta = deg2rad(x_theta_deg)
y_theta = deg2rad(y_theta_deg)
z_theta = deg2rad(z_theta_deg)
pcd_rotated = get_rotated_pcd(pcd, x_theta, y_theta, z_theta)
# Performing the hidden point removal operation on the rotated
# point cloud using the camera and radius parameters defined above.
pt_map = get_hpr_pt_map(pcd_rotated, camera, radius)
# Aggregating the output list of indexes of points that are not hidden.
pt_map_aggregated += pt_map
view_counter += 1
# Removing all the duplicated points from the aggregated list by converting it to a set.
pt_map_aggregated = list(set(pt_map_aggregated))
# Painting all the visible points in the point cloud in blue, and all the hidden points in red.
pcd_visible = pcd.select_by_index(pt_map_aggregated)
pcd_visible.paint_uniform_color([0, 0, 1]) # Blue points are visible points (to be kept).
print("No. of visible points : ", pcd_visible)
pcd_hidden = pcd.select_by_index(pt_map_aggregated, invert=True)
pcd_hidden.paint_uniform_color([1, 0, 0]) # Red points are hidden points (to be removed).
print("No. of hidden points : ", pcd_hidden)
# Visualizing the visible (blue) and hidden (red) points in the point cloud.
draw_geoms_list = [mesh_coord_frame, pcd_visible, pcd_hidden]
# draw_geoms_list = [mesh_coord_frame, pcd_visible]
# draw_geoms_list = [mesh_coord_frame, pcd_hidden]
o3d.visualization.draw_geometries(draw_geoms_list)
从相同相机视角经所有连续隐藏点去除操作后的点云。聚合的“可见”点(即,点云外表面上的点)为蓝色,而“隐藏”点(即,不在点云外表面上的点)为红色。
让我们再次裁剪点云,看看属于汽车内部的点。
# Cropping the point cloud of visible points using bounding box defined
# earlier to remove its right half (positive Z-axis).
pcd_visible_cropped = pcd_visible.crop(bbox_cropped)
# Cropping the point cloud of hidden points using bounding box defined
# earlier to remove its right half (positive Z-axis).
pcd_hidden_cropped = pcd_hidden.crop(bbox_cropped)
# Visualizing the cropped point clouds.
draw_geoms_list = [mesh_coord_frame, pcd_visible_cropped, pcd_hidden_cropped]
o3d.visualization.draw_geometries(draw_geoms_list)
经所有连续隐藏点去除操作后的裁剪点云,显示了所有属于 3D 汽车模型内部的“隐藏”点,标记为红色。
从上述经隐藏点去除操作后的点云可视化中,我们看到所有属于汽车模型内部(红色)的“隐藏”点现在与位于点云外表面(蓝色)的“可见”点分离开来。
将点云转换为数据框
如预期的那样,点云中每个点的位置可以用三个数值来定义 — X、Y 和 Z 坐标。然而,请回忆上面的部分,我们还为 3D 网格中的每个点估计了表面法线。当我们从这个网格中取样点以创建点云时,点云中的每个点还包含与这些表面法线相关的三个附加属性 — X、Y 和 Z 方向的法线单位向量坐标。
因此,为了将点云转换为数据框,点云中的每个点可以通过以下七个属性列在单独的行中表示:
-
X 坐标 (浮点数)
-
Y 坐标 (浮点数)
-
Z 坐标 (浮点数)
-
X 方向法向量坐标 (浮点数)
-
Y 方向法向量坐标 (浮点数)
-
Z 方向法向量坐标 (浮点数)
-
点可见性 (布尔值 True 或 False)
通过运行以下代码行,可以将点云转换为数据框:
# Creating a dataframe for the point cloud with the X, Y & Z positional coordinates
# and the normal unit vector coordinates in the X, Y & Z directions of all points.
pcd_df = pd.DataFrame(np.concatenate((np.asarray(pcd.points), np.asarray(pcd.normals)), axis=1),
columns=["x", "y", "z", "norm-x", "norm-y", "norm-z"]
)
# Adding a column to indicate whether the point is visible or not using the aggregated
# list of indexes of points from the hidden point removal operation above.
pcd_df["point_visible"] = False
pcd_df.loc[pt_map_aggregated, "point_visible"] = True
这将返回如下所示的数据框,其中每个点都是由上面解释的七个属性列表示的一行。
3D 点云转换为数据框。
保存点云和数据框
点云(隐藏点移除前和后)以及数据框现在可以通过运行以下代码行来保存:
# Saving the entire point cloud as a .pcd file.
pcd_save_path = "data/3d_model.pcd"
o3d.io.write_point_cloud(pcd_save_path, pcd)
# Saving the point cloud with the hidden points removed as a .pcd file.
pcd_visible_save_path = "data/3d_model_hpr.pcd"
o3d.io.write_point_cloud(pcd_visible_save_path, pcd_visible)
# Saving the point cloud dataframe as a .csv file.
pcd_df_save_path = "data/3d_model.csv"
pcd_df.to_csv(pcd_df_save_path, index=False)
3D 模型(上图:完整,下图:裁剪)可视化为 1. 网格,2. 点云和 3. 隐藏点移除后的点云。
就这些!希望本次演示能让你对如何在 Python 中处理 3D 数据有一点清晰的了解。如果你有任何问题或对如何更好地完成这些任务有建议,请告诉我。如果你有需要使用 3D 数据的有趣应用想法,请随时联系我——我很高兴能交流并分享类似的兴趣!同时,也欢迎在 LinkedIn 上与我联系。
本次演示中使用的 3D 汽车模型已从原始文件中稍作修改,以适应本练习的需要。感谢原作者 — “Tesla Model S Plaid”(https://skfb.ly/oEqT9)由 ValentunW 创作,使用了 Creative Commons Attribution 许可协议(http://creativecommons.org/licenses/by/4.0/)。
3D 深度学习 Python 教程:PointNet 数据准备
原文:
towardsdatascience.com/3d-deep-learning-python-tutorial-pointnet-data-preparation-90398f880c9f
实践教程,深度探讨,3D Python
《终极 Python 指南:为 PointNet 架构训练 3D 深度学习语义分割模型而构建大型 LiDAR 点云》
·发表于 Towards Data Science ·30 分钟阅读·2023 年 5 月 31 日
–
这张创意插图直观地突出了 3D 深度学习如何以易于分类的方式表现自上而下的场景。如果你喜欢这些,联系Marina Tünsmeyer。
近年来,3D 深度学习的应用领域迅速扩展。我们在机器人技术、自动驾驶与地图制作、医学成像和娱乐等各个领域都拥有卓越的应用。看到这些结果时,我们常常感到惊叹(但并非总是如此😁),我们可能会想:“我现在就要在我的应用中使用这个模型!”但不幸的是,噩梦开始了:3D 深度学习的实现。即使新的编码库旨在简化这一过程,实现一个端到端的 3D 深度学习模型仍是一项壮举,尤其是当你孤身一人待在某个阴暗的角落时。
这就是编码 3D 深度学习的感觉。© F. Poux
在 3D 深度学习框架中,最被忽视的痛点之一是将数据准备好以供选定的学习架构使用。我指的不是一个精美的研究数据集,而是一个实际的(混乱的)数据仓库,你想在其上开发应用程序。在大型且复杂的 3D 点云数据集的情况下,这个问题尤为严峻。
哦,你是否明白我们要在这篇文章中探讨什么?你梦到了它(不要隐藏,我知道😉),我们将深入到适当的编码深度。这篇实践教程探讨了如何高效地准备从航空 LiDAR 活动中获得的 3D 点云,以用于最流行的基于点的 3D 深度学习模型:PointNet 架构。
我们涵盖了整个数据准备流程,从 3D 数据整理到特征提取和归一化。它提供了知识和实际的 Python 技能,以解决现实世界的 3D 深度学习问题。
PointNet 数据准备工作流程用于 3D 语义分割。© F. Poux
通过跟随本教程,你将能够将这些技术应用到你自己的 3D 点云数据集上,并利用它们来训练 PointNet 语义分割模型。你准备好了吗?
Theory. 3D Deep Learning Essentials
Step 1\. Preparing the Environment
Step 2\. 3D Data Curation
Step 3\. 3D Data Analysis
Step 4\. 3D Data labelling
Step 5\. Feature Selection
Step 6\. Data Structuration
Step 7\. 3D Python I/O
Step 8\. 3D Data Normalization
Step 9\. 3D Interactive Vizualisation
Step 10\. Tensor Creation
🎵读者注意:本实践指南是与我的亲爱的同事* UTWENTE 合作的一部分 桑德·奥德·埃尔伯林克教授。我们感谢来自数字双胞胎 @ITC 项目的财务支持,该项目由特温特大学 ITC 学院资助。
3D 深度学习要点
3D 语义分割 VS 分类
3D 语义分割和 3D 点云分类的根本区别在于,分割旨在为点云中的每个点分配标签。而分类则试图为整个点云分配一个标签。
分类模型与语义分割模型之间的区别。在这两种情况下,我们都传递一个点云,但对于分类任务,整个点云是一个实体,而在语义分割的情况下,每个点是一个需要分类的实体。© F. Poux
例如,使用 PointNet 架构,3D 点云分类涉及将整个点云通过网络,并输出一个代表整个点云的标签。相比之下,语义分割的“头部”将为云中的每个点分配一个标签。方法的不同在于,分割需要对表示的 3D 空间有更详细的理解,因为它试图识别和标记点云中的个体对象或区域。相比之下,分类仅需要对点云的整体形状或组成有较高层次的理解。
总的来说,尽管 3D 语义分割和分类是分析 3D 点云数据的关键任务,但主要区别在于标记过程所需的详细程度和粒度。
正如你猜到的,我们将攻克语义分割,因为它需要对被分析的空间有更详细的理解,这非常有趣 😁。
不过在此之前,让我们稍微回顾一下,以更好地理解 PointNet 架构的工作原理,好吗?
PointNet:一种基于点的 3D 深度学习架构
将复杂的主题拆解成小块知识是我的专长。但是我必须承认,当涉及到 3D 深度学习时,通过神经网络中不同过程学到的函数的复杂性以及超参数确定的经验性特征是重要的挑战。要克服这些挑战,嗯?😁
首先,让我们回顾一下 PointNet 是什么。PointNet 是 3D 深度学习中神经网络的开创者之一。如果你理解了 PointNet,你就可以使用所有其他高级模型。但是,当然,理解只是方程的一部分。
PointNet 架构能够处理三种语义应用:分类、部件分割和语义分割。© F. Poux
另一部分是让这些复杂的东西发挥作用,并将其扩展到你的数据上!这是一项具有挑战性的任务!即使对于经验丰富的编码员也是如此。因此,我们将这个过程分为几个部分。今天,我们讨论的是准备数据,以确保我们在实际条件下有用的东西。
为了正确准备数据,理解网络的构建块是至关重要的。下面我将介绍在使用网络准备数据时需要考虑的关键方面。
论文作者描述的 PointNet 模型架构:ArXiv 论文。
PointNet 的架构由几个处理点云数据的神经网络层组成。PointNet 的输入是一个简单的点集,每个点由其 3D 坐标和附加特征(如颜色或强度)表示。这些点被输入到连续的共享多层感知器(MLP)网络中,网络学习从每个点中提取局部特征。
论文作者描述的 PointNet 架构中的 MLP:ArXiv 论文。
🦚 注意:MLP 是一个由多个层连接的节点或神经元构成的神经网络。MLP 中的每个神经元从上一层的神经元接收输入,利用权重和偏置对该输入进行变换,然后将结果传递给下一层的神经元。MLP 中的权重和偏置在训练过程中通过反向传播学习,以最小化网络预测值与真实输出之间的差异。
这些 MLP 是全连接层,每一层后面跟着我们称之为“非线性激活函数”(如 ReLU)。每层的神经元数量(例如 64)和层数(例如 2)可以根据具体任务和输入点云数据的复杂性进行调整。正如你所猜测的,神经元和层数越多,目标问题可能越复杂,因为架构的可塑性带来了组合可能性。如果我们继续深入研究 PointNet 架构,我们会看到我们用 1024 个特征描述原始的 n 个输入点,这些特征从最初提供的(X、Y 和 Z)中延展出来。这是架构通过使用最大池化操作对局部学习特征进行全局描述的地方,从而获得一个总结整个点云的全局特征向量。然后,这个全局特征向量会通过若干全连接层来生成分类头的最终输出,即 k 类的评分。
由论文作者描述的 PoinNet 模型架构的 MaxPool 和 MLP: ArXiv Paper。
如果你仔细观察,PointNet 中的语义分割头是一个全连接网络,它将全局特征向量和局部特征向量串联在一起,为输入点云数据中的每个点生成一个每点评分或标签。语义分割头由若干全连接层、ReLU 激活函数和一个最终的 softmax 层组成。最终 softmax 层的输出代表不同语义标签或类别的每点概率分布。
由论文作者描述的 PoinNet 模型架构的分割头: ArXiv Paper。
PointNet 架构能够捕捉任务如 3D 数据中的对象分类和分割所需的重要几何和上下文信息,通过从输入点云中的每个点学习局部和全局特征。PointNet 的一个关键创新是在最大池化操作中使用对称函数,这确保了输出对输入点的顺序不变。这使得 PointNet 对输入点顺序的变化具有鲁棒性,这在 3D 数据分析中至关重要。
现在,我们准备全力以赴地为 PointNet 准备数据。最开始我们指的是什么点云?我们是否输入一个完整的点云?
PointNet: 数据准备的关键方面
从高层次来看,如果我们研究原始论文,我们可以看到 PointNet 的功能非常直接:
-
我们将点云数据规范化到标准空间。
-
我们计算一系列特征(不依赖于我们已有的知识,而是利用网络的能力来创建有用的特征)
-
我们将这些特征汇聚成考虑中的点云的全局特征。
-
选项 1:我们使用这个全局特征来对点云进行分类
-
选项 2:我们将这个全局特征与局部特征结合,构建更精确的语义分割特征。
PointNet 在语义分割或分类任务中的五个步骤。© F. Poux
一切都围绕特征展开,这意味着我们提供给网络的块应该非常相关。例如,给出整个点云是行不通的,给出一个微小的样本也是行不通的,提供具有不同分布的结构化样本也行不通。那么我们怎么做呢?
让我们遵循一个线性的十步流程,以获得经过深思熟虑的 3D 点云训练/推理准备数据集。
PointNet 数据准备工作流程。© F. Poux
第一步:准备你的工作环境
在本文中,我们使用两个主要组件:CloudCompare 和 JupyterLab IDE (+ Python)。对于最佳设置的详细视图,我强烈建议你参考这篇文章,它详细介绍了所需内容:
## 3D Python 工作流程用于 LiDAR 城市模型:逐步指南
解锁 3D 城市建模应用程序的终极指南。该教程涵盖了 Python…
[towardsdatascience.com
我们将有一个特定的库栈,分为主要库、绘图库和实用库。
🦚 注意:如果你在本地环境中工作,我建议本教程使用 pip 进行包管理(pip install library_name)
我们将使用的两个主要库是 NumPy 和 Pytorch:
-
Numpy:NumPy 是一个用于处理数值数据的 Python 库,它提供了操作数组和矩阵、数学运算和线性代数函数的功能。
-
Pytorch:Pytorch 是一个流行的 Python 深度学习框架。它提供了构建和训练神经网络以及优化和评估模型的工具。
然后,我们使用两个绘图库来支持这些:
-
Matplotlib:Matplotlib 是一个用于创建可视化图表、图形和图像的 Python 库。
-
Plotly:Plotly 是一个用于创建交互式可视化的 Python 库。
最后,我们还将使用三个实用模块:
-
os: Python 中的 os 模块提供了一种使用操作系统相关功能的方法。它提供了与文件系统交互的函数,例如创建、删除和重命名文件和目录。
-
glob: Python 中的 glob 模块提供了一种使用模式匹配文件和目录的方法。例如,它可以在目录中找到所有具有特定扩展名的文件。
-
random(可选):
random
库是一个内置模块,提供生成随机数、从列表中选择随机项和打乱序列的函数。
一旦完成,我们就可以进入第二个方面:获取新的 3D 点云数据集!
步骤 2:3D 数据整理
对于本教程,我们前往荷兰东部,靠近恩斯赫德,那里有特温特大学的光芒🌞。在这里,我们选择了 AHN4 数据集的一部分,这部分数据应该有足够的树木、地面、建筑物以及一点水 🚿。我们可以说每个类别都有足够的点!
🦚 注意: 我们将在不平衡的数据集上进行训练,其中地面点的比例远高于其他类别。这不是理想的情况,在这种情况下,MLP 和语义分割头可能会偏向于预测多数类别标签,而忽略少数类别标签。这可能导致不准确的分割和少数类别点的错误分类。不过,可以使用几种技术来减轻不平衡类别的影响,如数据增强、少数类别的过采样或欠采样,以及使用加权损失函数。这是另一个话题。 😉
为了收集数据集,我们访问开放数据门户 geotiles.nl。我们缩放到一个感兴趣的区域,等待有 _XX(以便数据量一致),然后下载.laz 数据集,如下所示:
从荷兰 AHN4 LiDAR 活动中收集点云数据集。© F. Poux
此外,我们可以准备一些引人注目的用例,以便你以后可以在感兴趣的切片上测试你的模型。例如,可以在你所在的地方,如果那里有开放数据。
🦚 注意: 如果你想对你的模型进行真正的挑战,下载另一个地区的切片是一个很好的泛化测试!例如,你可以下载一个 LiDAR HD 点云切片,以查看如果用于训练或测试,是否会有差异/改进。
现在你已经有了.laz 文件格式的点云,让我们探索 info 文件提供的特性,你也可以查看或下载:
一份关于选定 3D LiDAR 点云数据集的信息文件。© F. Poux
这有助于深入理解数据内容,这是构建高质量数据集时至关重要的第一步。
这展示了点云的附加信息内容。© F. Poux
在浏览各种信息点时,有几个字段值得注意:
number of point records: 32080350
offset x y z: 205980 464980 0
min x y z: 205980.000 464980.000 4.101
max x y z: 207019.999 466269.999 53.016
intensity 56 5029
classification 1 26
Color R 17 255
G 39 255
B 31 255
NIR 0 255
这个小文件选择提示我们将处理大约 3200 万数据点,这些数据点具有颜色、强度,并且如果我们希望稍后提升模型,还可以具有近红外字段。
非常好!现在我们已经安装了软件堆栈并下载了 3D 点云,我们可以进行 3D 数据分析,以确保输入到模型中的数据符合预期。
步骤 3:3D 数据分析(CloudCompare)
现在是时候将 3D 航空点云文件加载到软件CloudCompare中了。首先,在计算机上打开 CloudCompare,直到出现空的 GUI,如下所示。
CloudCompare 的 GUI。来源:learngeodata.eu
从那里,我们通过拖放的方式加载下载的.laz 文件,并在导入时从弹出的菜单中选择一些属性,如下图所示。
在 CloudCompare 中导入 3D 点云。我们确保选择相关特征进行加载。© F. Poux
🦚 注意:我们取消选择所有字段以预选一些带来不相关或低相关信息的特征以及每个点的标签,这些标签可能指示真实情况。因此,我们只保留强度和分类字段。确实,由于我们针对的是航空点云,我们希望选择能够高效泛化的特征。因此,我们的目标是选择在未标记的数据中可能找到的特征,以便我们后续的模型能够在这些数据上进行表现。此外,点云还具有 RGB 信息,这也是一个不错的选择。
在这一阶段,七个选定的特征如下:X、Y 和 Z(空间),R、G、B(辐射计),以及强度。此外,我们保留来自.laz 文件分类字段的 AHN4 标签。成功将 3D 航空点云导入 CloudCompare 后,我们就准备好进行分析和可视化了。我们可以快速查看“对象属性面板
(3)”中的两个额外字段(强度和分类)。如果我们研究强度,会发现一些离群点稍微偏移了我们的特征向量,如下所示。
强度着色的点云及其分布直方图。© F. Poux
如果我们想将其用作 PointNet 的输入特征,这是我们必须解决的第一个观察点。
关于颜色值(红色、绿色、蓝色),它们是从另一个传感器获得的,可能是在另一个时间。因此,由于它们是从该区域的现有正射影像中合成的,我们可能会遇到一些精度/重投影问题。但正如你所想,能够将绿色元素与红色元素分开应该能给我们一个清晰的指示,表明一个点属于植被类别的概率😁。
LiDAR 数据集使用正射影像着色以获取 R、G、B 特征。© F. Poux
我们有一个点云,其中包含 3200 万个点,以笛卡尔坐标系(X,Y,Z)表示,每个点都有强度特征和颜色(红色、绿色和蓝色)。
🦚 注意:你可以将这个阶段留到后面,因为你可能会有许多选择的特征,例如下面所示的近红外(NIR)通道,它在数据集中是可用的。例如,这是一个方便的字段,可以突出显示健康(或不健康)的植被。 😉
近红外特征。© F. Poux
如果你滚动可用的字段,我们还有另一个最后的标量字段。分类字段,当然!这对于帮助我们创建标注数据集非常方便,以避免从零开始(感谢开放数据!👌)
提供的分类。© F. Poux
🦚 注意:出于教学培训的目的,我们将考虑将分类作为教程剩余部分的真实情况。然而,请知道分类是有一定不确定性的,如果你想要最好的模型,必须修正它。确实,有一句著名的 3D 深度学习格言:垃圾进=垃圾出。因此,数据的质量应该是首要的。
让我们专注于精炼标注阶段。
第 4 步:3D 数据标注(标签连接)
好的,在进入这个步骤之前,我必须说点什么。3D 点云标注用于训练有监督的 3D 语义分割学习模型是一个(痛苦的)手动过程。目标是为 3D 点云中的单个点分配标签。这个过程的主要关键目标包括识别点云中的目标物体、选择合适的标注技术,并确保标注过程的准确性。
标注过程的一个示例:标注簇与标注单个点。© F. Poux
要识别点云中需要标注的物体或区域,我们可以手动检查云,或者使用基于特征(如大小、形状或颜色)自动检测特定物体或区域的算法。
为教师、研究人员、开发人员和工程师提供的最佳 3D 在线课程。掌握 3D 点云处理及……
在我们的案例中,我们有一个优势:点云已经被分类。因此,第一步是将每个类别提取为独立的点云,如下图所示。
首先,我们选择点云并将“颜色”属性从 RGB 切换到标量场。然后确保我们可视化分类标量场。从那里,我们转到 EDIT > Scalar Field > Split Cloud by Integer Value,从而在点云中为每个类别生成一个点云。
从我们得到的各种点云类别中,我们可以看到:
class 1 = vegetation + clutter
class 2 = ground
class 6 = buildings
class 9 = water
class 26 = bridge.
从那里,我们可以重新处理class 1 = vegetation + clutter
。
必须根据特定任务和可用数据选择合适的标记技术。例如,我们可以使用无监督技术进行更多的探索性分析,并通过选择植被中的候选点来进行一些颜色阈值处理,如下图所示。
根据颜色信息对点云进行分割,以半自动化的方式创建更精确的标签。© F. Poux
这将给出不准确的结果,但可能会加快手动选择属于植被的任何点。
最后,确保标签过程的准确性对于产生可靠结果至关重要。这可以通过手动验证或质量控制技术,如交叉验证或标注者一致性,来实现。
🦚 注意:了解术语是好的,但不要害怕。这些概念可以在后续阶段覆盖。一件事一次完成。 😉
最终,标签过程的准确性将直接影响后续任务的表现,包括 3D 语义分割。在我们的案例中,我们将数据组织如下:
Class 1 = ground
Class 2 = Vegetation
Class 3 = Buildings
Class 4 = Water
Class 0 = unannotated (All the remaining points)
我们在 CloudCompare 中执行此操作。
在 CloudCompare 中组织各种类别。© F. Poux
在为清晰度重命名我们的不同点云(初始化)后,我们将(1)将杂乱物体合并为一个单独的点云,(2)删除所有点云的分类字段,(3)用新的编号重新创建分类字段,(4)克隆所有点云,(5)合并克隆点云,如下所示。
初步准备我们的标签进入新的点云。© F. Poux
数据准备阶段在 CloudCompare 中执行。© F. Poux
现在,我们有一个带标签的数据集,并具有特定的点标签分布。
我们注意到,在 32,080,350 个点中,23,131,067 个属于地面(72%),7,440,825 个属于植被(23%),1,146,575 个属于建筑物(4%),191,039 个属于水体(不到 1%),剩余的 170,844 个未标记(类别 0)。这将非常有趣,因为我们处于这个特定的不平衡情况中,具有主导类别。
现在我们已经分析了点云的内容并细化了标签,我们可以深入进行特征选择。
第 5 步。3D 特征选择
在使用 PointNet 架构进行 3D 点云语义分割时,特征选择对于准备训练数据至关重要。
在传统机器学习方法中,通常需要特征工程来选择和提取数据中的相关特征。然而,使用 PointNet 等深度学习方法可以避免这一步骤,因为模型可以自动学习从数据中提取特征。
然而,确保输入数据包含模型学习相关和推导特征所需的信息仍然很重要。我们使用七个特征:X
、Y
、Z
(空间属性)、R
、G
、B
(辐射属性)和强度I
(激光雷达衍生)。
X Y Z R G B INTENSITY
205980.49800000 465875.02398682 7.10500002 90 110 98 1175.000000
205980.20100001 465875.09802246 7.13500023 87 107 95 1115.000000
205982.29800010 465875.00000000 7.10799980 90 110 98 1112.000000
这是我们的参考。这意味着我们将使用这个输入来构建模型,任何我们希望用训练好的 PointNet 模型处理的其他数据集必须包含这些相同的特征。在进入 Python 之前,最后一步是根据架构规范结构化数据。
第 6 步。数据结构化(瓦片化)
由于几个原因,在使用神经网络架构 PointNet 处理 3D 点云时,将其结构化为正方形瓦片是必要的。
在此工作流中的瓦片定义是训练 PointNet 3D 深度学习架构。© F. Poux
首先,PointNet 要求输入数据为固定大小,这意味着所有输入样本应具有相同数量的点。通过将 3D 点云分割成正方形瓦片,我们可以确保每个瓦片具有更均匀的点数量,使 PointNet 能够一致有效地处理它们,而不会在采样到最终固定点数时产生额外开销或不可逆损失。
采样策略对 3D 点云数据集的影响示例。© F. Poux
🌱 增长:使用 PointNet 时,我们需要将输入瓦片固定为推荐的 4096 个点。这意味着需要一种采样策略(CloudCompare 中未实现)。如上图所示,使用不同策略对点云进行采样将产生不同的结果和物体识别能力(例如右侧的电线杆)。你认为这会影响 3D 深度学习架构的性能吗?
其次,PointNet 的架构涉及应用于每个点的共享多层感知机(MLP),这意味着网络在处理每个点时是独立的,不考虑其邻居。通过将点云结构化为瓦片,我们可以在保持每个点局部上下文的同时,让网络独立处理点,从而从数据中提取有意义的特征。
生成的 3D 点云瓦片。© F. Poux
最后,将 3D 点云结构化为瓦片也可以提高神经网络的计算效率,因为它允许对瓦片进行并行处理,从而减少分析整个点云所需的总体处理时间(在 GPU 上)。
我们使用“横截面”工具(1)来实现这一目标。我们将大小设置为 100 米(2),然后沿 X 轴和 Y 轴(负方向)移动,以尽可能接近初始瓦片的最底部角落(3),我们使用多个切片按钮(4),沿 X 轴和 Y 轴重复(5),得到最终的方形瓦片(6),如下所示。
自动化瓦片创建过程在 CloudCompare 中。© F. Poux
自动化瓦片创建过程在 CloudCompare 中。© F. Poux
这允许定义大约一百米乘一百米的瓦片,沿 X 轴和 Y 轴。我们获得了 143 个瓦片,其中抛弃了最后 13 个瓦片,因为它们可能更能代表我们希望输入的内容(即,它们不是方形,因为它们位于边缘)。在剩下的 130 个瓦片中,我们选择了大约 20%具有代表性的瓦片(按住 Shift + 选择),如下所示。
对 PointNet 进行的选择和手动分割训练集和测试集。© F. Poux
🌱 增长:我们按照 80/20 的比例将数据分为训练集和测试集。在这个阶段,你怎么看这种方法?你认为什么样的策略比较好?
在这个过程中,我们在训练集和测试集中分别拥有约 100 个瓦片和 30 个瓦片,每个瓦片都保留了原始点数。然后,我们选择一个文件夹,将每个瓦片导出为 ASCII 文件,如下所示。
将点云瓦片导出以供 PointNet 使用
🦚 注意:CloudCompare 允许在选择以 ASCII 文件格式导出时,将所有点云独立导出到一个目录中。它会在最后一个字符之后自动缩进,使用“*_*
”字符以确保一致性。这非常方便,可以使用/滥用。
将 3D 点云结构化为方形瓦片是使用 PointNet 时的一个重要预处理步骤。它允许输入数据大小的一致性,保留局部上下文,并提高计算效率,这些都有助于更准确和高效的数据处理。这是进入 3D Python 🎉之前的最后一步。
第 7 步。3D Python 数据加载
现在是时候在 Python 中处理点云瓦片了。
为此,我们导入所需的库。如果你使用的是可以在这里访问的 Google Colab 版本:💻 Google Colab Code,那么重要的是要运行下面所示的第一行:
from google.colab import drive
drive.mount('/content/gdrive')
对于任何设置,我们必须导入下述各种库:
#Base libraries
import numpy as np
import random
import torch
#Plotting libraries
%matplotlib inline
from mpl_toolkits import mplot3d
import matplotlib.pyplot as plt
import plotly
import plotly.graph_objects as go
#Utilities libraries
from glob import glob
import os
太好了!从这里开始,我们将数据文件名拆分到各自的文件夹中,pointcloud_train_files
和pointcloud_test_files
#specify data paths and extract filenames
project_dir="gdrive/My Drive/_UTWENTE/DATA/AHN4_33EZ2_12"
pointcloud_train_files = glob(os.path.join(project_dir, "train/*.txt"))
pointcloud_test_files = glob(os.path.join(project_dir, "test/*.txt"))
🦚 注意:我们在资源管理器中有两个文件夹:训练文件夹和测试文件夹,均在 AHN4_33EZ2_12
文件夹中。我们在这里做的是首先指定根文件夹的路径,然后用 glob 收集训练和测试中的所有文件,使用 ***
表示“选择所有
”。一种处理多个文件的便捷方法!
在这一步,两个变量保存了我们准备好的所有瓦片的路径。为了确保这一点,我们可以打印从 0 到 20 的随机分布中随机选取的一个元素:
print(pointcloud_train_files[random.randrange(20)])
>> gdrive/My Drive/_UTWENTE/DATA/AHN4_33EZ2_12/train/AHN4_33EZ2_12_train_000083.txt
太好了,所以我们可以进一步将数据集拆分成三个变量:
-
valid_list:这保存了验证数据路径。验证拆分通过在每个训练周期后微调模型来帮助提高模型性能。
-
train_list:这保存了训练数据路径,即用于训练的数据集。
-
test_list:这保存了测试数据路径。测试集告知我们模型在完成训练阶段后的最终准确性。
这是通过友好的 numpy 函数完成的,这些函数作用于列表中的数组索引。实际上,我们随机提取了pointcloud_train_files
中的 20%,然后将保留的部分与未保留的部分进行分割,后者构成了train_list
变量。
#Prepare the data in a train set, a validation set (to tune the model parameters), and a test set (to evaluate the performances)
#The validation is made of a random 20% of the train set.
valid_index = np.random.choice(len(pointcloud_train_files),int(len(pointcloud_train_files)/5), replace=False)
valid_list = [pointcloud_train_files[i] for i in valid_index]
train_list = [pointcloud_train_files[i] for i in np.setdiff1d(list(range(len(pointcloud_train_files))),valid_index)]
test_list = pointcloud_test_files
print("%d tiles in train set, %d tiles in test set, %d files in valid list" % (len(train_list), len(test_list), len(valid_list)))
然后,我们通过查看中位数、标准差和最小-最大值来随机研究一个数据文件的属性,使用以下代码片段:
tile_selected=pointcloud_train_files[random.randrange(20)]
print(tile_selected)
temp=np.loadtxt(tile_selected)
print('median\n',np.median(temp,axis=0))
print('std\n',np.std(temp,axis=0))
print('min\n',np.min(temp,axis=0))
print('max\n',np.max(temp,axis=0))
这将产生:
gdrive/My Drive/_UTWENTE/DATA/AHN4_33EZ2_12/train/AHN4_33EZ2_12_train_000083.txt
median [2.068e+05 4.659e+05 6.628e+00 1.060e+02 1.210e+02 1.030e+02 1.298e+03 1.000e+00]
std [ 28.892 30.155 0.679 29.986 21.4 17.041 189.388 0.266]
min [2.068e+05 4.659e+05 5.454e+00 3.600e+01 6.200e+01 5.700e+01 7.700e+01 0.000e+00]
max [2.068e+05 4.660e+05 1.505e+01 2.510e+02 2.470e+02 2.330e+02 1.625e+03 4.000e+00]
正如我们所注意到的,有一个核心问题需要解决:数据归一化。确实,为了避免任何不匹配,我们需要处于这种“规范空间”,这意味着我们可以在特征空间中复制相同的实验环境。使用 T-Net 就像用火箭筒🪰杀苍蝇。这是可以的,但如果我们可以避免并使用实际一致的方法,那将更聪明😁。
步骤 8. 3D Python 归一化
在将 3D 点云瓦片输入到 PointNet 架构之前进行归一化至关重要,主要有三个原因。首先,归一化确保输入数据围绕原点中心,这对于 PointNet 的架构至关重要,因为它对每个点独立应用 MLP。当输入数据围绕原点中心时,MLP 更有效,这使得特征提取更有意义,整体性能更好。
归一化对训练 3D 深度学习模型结果的影响示意图。© F. Poux
🌱 成长:在盲目归一化之前,有些直觉也是有益的。例如,我们主要使用基于重力的场景,这意味着 Z 轴几乎总是与 Z 轴共线。因此,你会如何处理这种归一化?
其次,归一化将点云数据缩放到一致的范围,这有助于防止 MLP 中的激活函数饱和。这使得网络能够从整个输入值范围中学习,提高了准确分类或分割数据的能力。
在 3D 点云的强度场上说明[0,1]缩放的问题。© F. Poux
最后,归一化有助于减少点云数据中不同尺度的影响,这可能是由于传感器分辨率或扫描物体的距离差异(在航拍 LiDAR 数据中有所平坦化)。这提高了数据的一致性和网络从中提取有意义特征的能力。
好的,让我们开始吧。对于我们的实验,我们将首先捕捉特征的最小值min_f
和平均值mean_f
:
cloud_data=temp.transpose()
min_f=np.min(cloud_data,axis=1)
mean_f=np.mean(cloud_data,axis=1)
🦚 注意:我们对数据集进行了转置,以更高效、便捷地处理数据和索引。因此,要获取点云的 X-axis
元素,我们可以直接使用 cloud_data[0]
而不是 cloud_data[:,0]
,这样可以减少一些开销。
我们现在将对不同的特征进行归一化,以用于我们的 PointNet 网络。首先是空间坐标 X、Y 和 Z。我们将把数据中心化到平面坐标轴(X 和 Y),并确保减去 Z 的最小值,以区分屋顶和地面,例如:
n_coords = cloud_data[0:3]
n_coords[0] -= mean_f[0]
n_coords[1] -= mean_f[1]
n_coords[2] -= min_f[2]
print(n_coords)
很好,现在我们可以通过确保我们在[0,1]范围内来缩放我们的颜色。这是通过将所有颜色的最大值(255)进行除法实现的:
colors = cloud_data[3:6]/255
最后,我们将处理强度特征的归一化。在这里,我们将使用分位数来获得对异常值具有鲁棒性的归一化,就像我们在探索数据时看到的那样。这是通过三个阶段的过程完成的。首先,我们计算四分位差IQR
,即第 75 个分位数和第 25 个分位数之间的差值。然后我们从所有观测值中减去中位数,并除以四分位差。最后,我们减去强度的最小值,以获得显著的归一化:
# The interquartile difference is the difference between the 75th and 25th quantile
IQR = np.quantile(cloud_data[-2],0.75)-np.quantile(cloud_data[-2],0.25)
# We subtract the median to all the observations and then divide by the interquartile difference
n_intensity = ((cloud_data[-2] - np.median(cloud_data[-2])) / IQR)
#This permits to have a scaling robust to outliers (which is often the case)
n_intensity -= np.min(n_intensity)
print(n_intensity)
太棒了!在这一阶段,我们已经有了一个归一化的点云,准备好输入 PointNet 架构。但是自动化这个过程是执行所有瓦片的下一步逻辑步骤。
创建一个点云瓦片加载和归一化函数
我们创建一个函数cloud_loader
,它以一个瓦片路径tile_path
和一个用于的特征字符串features_used
作为输入,并输出一个cloud_data
变量,其中包含归一化特征,以及一个真实标签变量gt
,其中包含每个点的标签。该函数将如下操作:
定义一个云加载函数来处理点云数据集,并使其准备好用于训练。© F. Poux
这转换为一个简单的cloud_loader
函数,如下所示:
# We create a function that loads and normalize a point cloud tile
def cloud_loader(tile_path, features_used):
cloud_data = np.loadtxt(tile_path).transpose()
min_f=np.min(cloud_data,axis=1)
mean_f=np.mean(cloud_data,axis=1)
features=[]
if 'xyz' in features_used:
n_coords = cloud_data[0:3]
n_coords[0] -= mean_f[0]
n_coords[1] -= mean_f[1]
n_coords[2] -= min_f[2]
features.append(n_coords)
if 'rgb' in features_used:
colors = cloud_data[3:6]/255
features.append(colors)
if 'i' in features_used:
IQR = np.quantile(cloud_data[-2],0.75)-np.quantile(cloud_data[-2],0.25)
n_intensity = ((cloud_data[-2] - np.median(cloud_data[-2])) / IQR)
n_intensity -= np.min(n_intensity)
features.append(n_intensity)
gt = cloud_data[-1]
gt = torch.from_numpy(gt).long()
cloud_data = torch.from_numpy(np.vstack(features))
return cloud_data, gt
该函数现在用于获取点云特征和标签,如下所示:
pc, labels = cloud_loader(tile_selected, ‘xyzrgbi’)
🌱 成长:如你所见,我们传递了一个特征的字符串。这对于我们不同的‘*if*
’测试非常方便。然而,请注意,如果传递给函数的内容不符合预期,我们不会返回错误。这不是标准的代码实践,但这扩展了本教程的范围。 如果你想开始编写漂亮的代码,我建议查看 PEP-8 指南。
步骤 9. 3D Python 交互式可视化
如果我们想要平行于以前的文章,可以在这里访问:
## LiDAR 城市模型的 3D Python 工作流:逐步指南
解锁 3D 城市建模应用的终极指南。该教程涵盖 Python…
[towardsdatascience.com
我们可以使用 Open3D 可视化我们的数据集。首先,我们需要安装一个特定版本(如果在 Jupyter Notebook 环境中工作,如 Google Colab 或 CRIB 平台),并在我们的脚本中加载它:
!pip install open3d==0.16
import open3d as o3d
🦚 注意:在 pip 之前的“!
”是在 Google Colab 上工作时使用的,表示它应该直接使用环境控制台。如果你在本地工作,应删除此字符并直接使用 pip install open3d==0.16
。
然后我们执行以下连续步骤:
绘制函数以直接在 Google Colab 中绘制交互式 3D 场景。© F. Poux
这转化为以下代码行:
pc, gt = cloud_loader(tile_selected, ['xyz','rgb','i'])
pcd=o3d.geometry.PointCloud()
pcd.points=o3d.utility.Vector3dVector(np.array(pc)[0:3].transpose())
pcd.colors=o3d.utility.Vector3dVector((np.array(pc)[3:6]).transpose())
o3d.visualization.draw_plotly([pcd],point_sample_factor=0.5, width=600, height=400)
🦚注意:由于我们的 pc 变量捕获了 cloud_data
来自 cloud_loader
函数的输出被转置,我们在使用 open3d
绘图时必须记得将其转置回去。
上述代码片段将输出以下可视化:
使用 plotly 绘制场景和 R、G、B 字段的结果。© F. Poux
🦚 注意:使用 draw_plotly
函数时,我们无法直接控制图表的缩放,并且可以注意到 *X*
, *Y,*
和 *Z*
的比例不等,这在这种情况下强调了 Z。 😁
由于你可以注意到的限制,我们创建了一个自定义可视化函数来可视化随机切片,以便运行函数:visualize_input_tile
输出一个交互式的 plotly
可视化,让我们切换渲染模式。
要测试提供的函数,我们首先需要在实验中定义类名称:class_names = [‘unclassified’, ‘ground’, ‘vegetation’, ‘buildings’, ‘water’]
。然后,我们提供云特征cloud_features=’xyzi’
,随机选择变量selection
中捕获的点云,并可视化切片。这转化为以下代码片段:
class_names = ['unclassified', 'ground', 'vegetation', 'buildings', 'water']
cloud_features='xyzi'
selection=pointcloud_train_files[random.randrange(len(pointcloud_train_files))]
visualize_input_tile(selection, class_names, cloud_features, sample_size=20000)
这将输出如下交互式场景。
在 Google Colab 中的交互式 3D 场景。© F. Poux
🦚 注意:你可以使用按钮在特征强度和从加载的感兴趣特征的标签之间切换渲染模式。
我们有一个工作解决方案用于加载、规范化和可视化 Python 中的单个切片。最后一步是创建我们称之为张量的内容,以用于 PointNet 架构。
第 10 步。张量创建
我想向你展示如何使用 PyTorch 进行初步操作。为清晰起见,让我快速定义我们使用这个库时操作的主要 Python 对象类型:张量。
PyTorch 张量是一个多维数组,用于在 PyTorch 中存储和操作数据。它类似于 NumPy 数组,但具有针对深度学习模型优化的额外好处。张量可以使用 torch.tensor()
函数创建,并用数据初始化,或者创建一个具有指定形状的空张量。例如,要创建一个 3x3 的张量并填充随机数据:
import torch
x = torch.tensor([[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0]])
print(x)
这将输出:
tensor([[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.]])
相当简单,对吧?现在,为了简化操作,还有一个小型的 Pytorch 库,我们可以用来准备数据集列表。这个库叫做TorchNet
。TorchNet
旨在通过提供一组预定义的模块和助手函数来简化构建和训练复杂神经网络架构的过程,这些模块和助手函数适用于数据加载、验证和测试等日常任务。
TorchNet
的主要优势之一是其模块化设计,允许用户通过组合一系列预构建模块来轻松构建复杂的神经网络架构。这可以节省大量时间和精力,相比从头开始构建神经网络,尤其是在刚接触深度学习时。
🦚 注意:除了其模块化设计外,TorchNet 还提供了多个用于常见深度学习任务的助手函数,例如数据增强、早期停止和模型检查点。这可以帮助用户获得更好的结果,并更高效地优化他们的神经网络架构。
要安装torchnet
版本0.0.4
并将其导入到我们的脚本中,我们可以执行以下操作:
!pip install torchnet==0.0.4
import torchnet as tnt
我们还导入了另一个名为 functools
的实用模块。该模块用于处理或返回其他函数的高阶函数。为此,将 import functools
添加到导入语句中。
通常,任何可调用的对象都可以被视为此模块的函数。通过这些额外的设置,可以使用以下四行代码轻松生成训练集、验证集和测试集:
cloud_features='xyzrgbi'
test_set = tnt.dataset.ListDataset(test_list,functools.partial(cloud_loader, features_used=cloud_features))
train_set = tnt.dataset.ListDataset(train_list,functools.partial(cloud_loader, features_used=cloud_features))
valid_set = tnt.dataset.ListDataset(valid_list,functools.partial(cloud_loader, features_used=cloud_features))
现在,如果我们想要探索,可以使用像经典的 numpy 数组一样的索引来检索特定位置的张量,例如train_set[1]
,其输出为:
最后,我们必须将结果保存到 Python 对象中,以便在接下来的步骤中直接使用,例如 PointNet 训练。我们使用的库是 pickle,它非常适合保存 Python 对象。要保存一个对象,只需运行以下命令:
import pickle
f = open(project_dir+"/data_prepared.pckl", 'wb')
pickle.dump([test_list, test_set, train_list, train_set, valid_list, valid_set], f)
f.close()
如果你想测试你的设置,还可以运行以下代码行,确保你检索到你想要的内容:
f = open(project_dir+"/data_prepared.pckl", 'rb')
test_list_t, test_set_t, train_list_t, train_set_t, valid_list_t, valid_set_t = pickle.load(f)
f.close()
print(test_list_t)
-
💻 在这里获取代码访问:Google Colab
-
🍇 在这里获取数据访问:3D 数据集
-
👨🏫3D 数据处理和 AI 课程:3D 学院
-
📖 订阅以获得 3D 教程的早期访问:3D AI 自动化
-
💁 支持我的工作与 Medium 🤟:Medium 订阅
🔮 结论
恭喜!在这个实践教程中,我们探讨了准备用于 PointNet 架构的 3D 点云数据的关键步骤。
PointNet 的 3D 深度学习数据准备工作流程。© F. Poux
通过遵循这个逐步指南,你已经学会了如何清理、处理 LiDAR 点云,提取相关特征,并为 3D 深度学习模型规范化数据。我们还讨论了一些处理 3D 点云数据的关键注意事项,如瓦片大小、规范化和数据增强。你可以将这些技术应用于你的 3D 点云数据集,并用它们来训练和测试 PointNet 模型,用于对象分类和分割。3D 深度学习领域正在快速发展,这个教程是一个基石,为你进一步探索这一激动人心的领域提供了坚实的基础。
🤿 进一步探索
但学习之旅并未止步于此。我们的终身探索才刚刚开始,未来的步骤将深入探讨 3D 体素工作、3D 数据的人工智能、语义分析和数字双胞胎。此外,我们将使用深度学习技术分析点云,解锁高级 3D LiDAR 分析工作流程。还有很多令人兴奋的内容!
参考文献
-
Qi, C. R., Su, H., Mo, K., & Guibas, L. J. (2017). Pointnet:点集上的深度学习用于 3D 分类和分割。收录于 IEEE 计算机视觉与模式识别会议论文集 (第 652–660 页)。
-
Poux, F., & Billen, R. (2019). 基于体素的 3D 点云语义分割:无监督几何和关系特征与深度学习方法。ISPRS 国际地理信息学杂志, 8(5), 213。
-
Xu, S., Vosselman, G., & Elberink, S. O. (2014). 基于多实体的城市区域航空激光扫描数据分类。ISPRS 摄影测量与遥感杂志, 88, 1–15。
使用 DeepSDF 进行 3D 生成建模
原文:
towardsdatascience.com/3d-generative-modeling-with-deepsdf-2cd06f1ec9b3
简单的神经网络可以捕捉复杂的 3D 几何形状
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 1 月 30 日
–
(照片由 Milad Fakurian 提供,来源于 Unsplash)
计算机图形学和 3D 计算机视觉领域的先前研究提出了多种表示 3D 形状的方法。这些方法对以下方面有用:
-
存储内存高效的已知形状表示
-
生成新形状
-
基于有限或噪声数据修复/重建形状
超越传统方法,深度学习——更具体地说,生成性神经网络——可以用来表示 3D 形状。为此,我们可以训练一个神经网络来输出 3D 形状的表示,从而将多种形状的表示间接存储在神经网络的权重中。然后,我们可以查询这个神经网络来生成新形状。
在这篇文章中,我们将深入研究其中一种方法,称为 DeepSDF [1],它使用一个简单的前馈神经网络来学习多种 3D 形状的符号距离函数(SDF)表示。基本思想很简单:我们不是直接编码几何体(例如,通过网格),而是训练一个生成性神经网络来输出这些几何体。然后,我们可以进行推理以*(i)* 获取(潜在的新)3D 形状的直接编码,或*(ii)* 从噪声数据中修复/重建一个 3D 形状。
(来自 [1])
背景
在深入了解 DeepSDF 的工作原理之前,我们需要理解一些背景概念。首先,我们将讨论一下 3D 形状通常是如何表示的,以及签名距离函数(SDF)如何用于表示 3D 形状。然后,我们将讨论前馈神经网络,这是一种非常简单的深度学习架构,在 3D 形状建模的研究中被广泛使用。
表示 3D 形状
在考虑如何在计算机中存储 3D 形状时,我们有三个选项:点云、网格或体素。这些表示方法各有不同的优缺点,但都是直接表示 3D 形状的有效方法。让我们简单了解它们的工作原理。
点云。 点云相对容易理解。顾名思义,它们只是存储空间中一组具有 [x, y, z]
坐标的点,这些点用于表示一个潜在的几何形状。点云非常有用,因为它们与我们从 LiDAR 或深度传感器相机等传感器中获得的数据非常接近。但点云无法提供完全封闭的表面(即具有一个封闭表面的形状)。
网格。 一种可以提供封闭表面的 3D 表示方法是网格。网格是基于顶点、边和面集合的 3D 形状表示,用于描述一个潜在的形状。简单来说,网格就是一系列多边形(例如三角形),这些多边形拼接在一起形成一个 3D 几何形状。
基于体素的表示。 体素只是具有体积的像素。在 2D 图像中的像素在 3D 空间中被称为体素(即立方体)。要使用体素表示 3D 形状,我们可以:
-
将 3D 空间的一个部分划分为离散体素
-
确定每个体素是否被填充
使用这种简单的技术,我们可以构建一个基于体素的 3D 对象。为了获得更准确的表示,我们可以简单地增加使用的体素数量,从而形成更细致的 3D 空间离散化。请参见下文了解点云、网格和体素之间的差异插图。
(来自[3])
签名距离函数
直接使用点云、网格或体素存储 3D 形状需要大量内存。相反,我们通常希望存储一种更高效的间接表示方法。一种方法是使用签名距离函数(SDF)。
给定一个空间 [x, y, z]
点作为输入,SDF 将输出该点到所表示对象的最近表面的距离。SDF 输出的符号表示该空间点在对象表面内(负值)还是外(正值)。请参见下面的方程。
(由作者创建)
我们可以通过找到 SDF 等于零的位置来识别 3D 物体的表面,这表明某个点位于物体的边界。通过使用 SDF 找到这个表面后,我们可以通过使用类似 Marching Cubes 的算法生成网格。
这有什么用? 从高层次看,SDF 允许我们存储一个函数,而不是 3D 形状的直接表示。这种函数可能更高效地存储,并且我们仍然可以用它来恢复网格表示!
前馈神经网络
许多高精度的 3D 形状建模方法基于前馈网络架构。这样的架构将一个向量作为输入,并在网络的每一层内应用相同的两个变换:
-
线性变换
-
非线性激活函数
尽管我们输入的维度是固定的,但网络架构的两个方面是我们可以选择的:隐藏维度和层数。像这样的变量,我们作为实践者需要设置,称为 超参数。这些超参数的正确设置取决于我们试图解决的问题和/或应用。
代码。 前馈网络的复杂性不大。我们可以像下面展示的那样在 PyTorch 中轻松实现它们。
DeepSDF: 学习连续有符号距离函数以进行形状表示 [1]
(来自 [1])
在计算机图形学和 3D 计算机视觉领域,先前的研究提出了许多经典的方法来表示 3D 形状和几何体。在 [1] 中,作者提出了一种基于深度学习的方法,称为 DeepSDF,该方法使用神经网络来学习广泛类别形状的连续 SDF。简单来说,这意味着我们可以通过一个单一的前馈神经网络来编码基于 SDF 的多种不同类型的 3D 形状,从而使这些形状能够被表示、插值甚至从部分数据中完成;见上文。
DeepSDF 的核心思想很简单:我们希望使用神经网络直接对 SDF 的值进行 回归。为此,我们通过 SDF 的点样本(即具有相关 SDF 值的单个 [x, y, z]
点)来训练该模型。如果我们以这种方式训练网络,我们就可以轻松预测查询位置的 SDF 值,并通过找到 SDF 等于零的点来恢复形状的表面。
我们如何表示形状? 更具体地说,考虑一个单一形状,从中我们采样固定数量的 3D 点样本及其 SDF 值。我们应该注意,采样更多的点将使形状的表示更加精确,但这会增加计算成本。
(由作者创作)
在上面的方程中,x
是一个包含 [x, y, z]
坐标的向量,而 s
是与这些坐标相关联的给定形状的 SDF 值。
训练神经网络。 从这里,我们可以直接训练一个前馈神经网络,以 x
作为输入,利用这些样本对进行训练,从而生成 SDF 值 s
,使用 L1 回归损失。然后,结果模型可以输出准确的 SDF 值来表示底层形状;见下图左侧子图。
(来自 [1])
这种模型的局限性在于它仅表示单一形状。理想情况下,我们希望用一个神经网络建模多种形状。为此,我们可以将一个潜在向量(即上图中的“Code”)与每个形状关联。这是一个对每个形状唯一的低维向量,存储在我们的神经网络中。可以将这个潜在向量作为输入添加到神经网络中,以告知网络它正在为特定形状生成输出。这一简单技巧允许我们在一个模型中表示多个形状(这节省了大量内存!);见上图右侧子图。
我们可能会问的最终问题是:我们如何为每个形状获取这个潜在向量? 在 [1] 中,作者通过提出一个自解码器架构来实现这一点,该架构 (i) 将潜在向量添加到模型的输入中,并 (ii) 在训练过程中通过梯度下降学习每个形状的最佳潜在向量;见下文。
(来自 [1])
通常,潜在向量是通过 自编码器架构 学习的,但这需要额外的编码器模块,增加了额外的计算开销。作者在 [1] 中提出了自解码器方法以避免这种额外计算。这些方法之间的区别如下所示。
生成形状。 要使用 DeepSDF 进行推理,我们必须:
-
从稀疏/不完整的 SDF 值样本开始
-
从这些样本中确定最佳的潜在向量
-
使用我们训练好的神经网络对 3D 空间中的一系列不同点进行推理,以确定 SDF 值
从这里,我们可以使用像 Marching Cubes 这样的算法来可视化由 DeepSDF 表示的形状,这些算法对 3D 空间进行离散化,并基于这些 SDF 值提取实际的 3D 几何形状。
数据。 DeepSDF 是使用合成的 ShapeNet 数据集进行训练和评估的。特别地,它的性能在四个任务中得到了衡量。
-
训练集中的形状表示
-
重建未见(测试)形状
-
完成部分形状
-
从潜在空间采样新形状
对于前三个任务,我们发现 DeepSDF 一直优于基线方法,这表明它能够以高精度表示复杂形状,甚至能够从不完整的样本中较好地恢复形状。考虑到我们在一个单一且节省内存的神经网络中存储了大量的 3D 形状,这一点尤为显著;见下图。
(来自 [1])
我们还可以对 DeepSDF 模型的嵌入空间进行插值,以生成连贯的结果。这使我们能够做一些事情,比如找到卡车和汽车之间的平均形状;见下图。
(来自 [1])
从这些结果中,我们可以看到潜在向量之间的插值产生了形状之间的平滑过渡,这表明 DeepSDF 嵌入的连续 SDF 是有意义的!形状的常见特征 —— 如卡车车厢或椅子扶手 —— 都被 DeepSDF 利用的表示所捕捉。这对于这样一个简单的前馈网络来说,实在是非常了不起。
收获
DeepSDF 是一个前馈生成神经网络,我们可以用来表示和操作 3D 形状。使用这个模型,我们可以轻松地执行诸如生成形状的网格表示、从不完整或噪声数据中恢复基本形状,甚至生成一个新的形状,这个新形状是已知几何形状的插值。以下列出了 DeepSDF 的优点和局限性。
大量压缩。 要在计算机中存储 3D 几何形状,我们可以使用网格或体素表示。为了避免直接存储这种形状的内存开销,我们可以使用像 DeepSDF 这样的生成模型。通过这种方法,我们不再需要几何形状的直接网格编码。相反,我们可以使用 DeepSDF —— 一个易于存储的小型神经网络 —— 来准确生成各种形状的网格。
修复破损几何形状。 给定基本形状的部分或噪声表示,DeepSDF 可以用来恢复准确的网格;见下图。相比之下,大多数之前的方法无法执行这种任务 —— 它们需要访问与用于训练模型的数据类型匹配的完整 3D 形状表示。
(来自 [1])
插值潜在空间。 Deep SDF 能够表示许多不同的形状,并将它们的属性嵌入到低维潜在空间中。此外,实验表明,这个潜在空间是有意义的,并且具有良好的覆盖范围。实际上,这意味着我们可以在潜在向量(即,不同对象的向量表示)之间进行 线性插值,并生成有效的新形状。我们可以轻松地利用这一点生成具有各种有趣属性的新形状。
局限性。 DeepSDF 非常出色,但它总是需要访问(可能是噪声或不完整的)3D 几何体来进行推断。此外,寻找最佳潜在向量(即,由于自动解码器方法,这必须始终在进行推断之前完成)计算成本很高。因此,DeepSDF 的推断能力在某种程度上是有限的。总结来说,该方法较慢,无法从头生成新形状,这为未来的改进留下了空间。
结束语
非常感谢您阅读本文。我是 Cameron R. Wolfe,一名在 Alegion 工作的研究科学家,同时也是赖斯大学的博士生,研究深度学习的经验和理论基础。您还可以查看我在 medium 上的 其他文章!如果您喜欢,请在 twitter 上关注我,或者订阅我的 Deep (Learning) Focus 新闻通讯,我在其中撰写关于重要深度学习主题的易于理解的概述系列。
参考文献
[1] Park, Jeong Joon 等. “Deepsdf: 学习用于形状表示的连续符号距离函数。” IEEE/CVF 计算机视觉与模式识别会议论文集. 2019.
[2] Mildenhall, Ben 等. “Nerf: 将场景表示为神经辐射场以进行视图合成。” ACM 通讯 65.1 (2021): 99–106.
[3] Hoang, Long 等. “一种使用波形核签名和 3D 三角网中心点的深度学习方法进行 3D 对象分类。” 电子学 8.10 (2019): 1196.