面向数据科学的线性代数实践指南-全-
面向数据科学的线性代数实践指南(全)
原文:
zh.annas-archive.org/md5/1b2e3f08d8e07a0269f2a0fe151cdab7译者:飞龙
前言
本书中使用的约定
本书使用以下排版约定:
斜体
表示新术语、网址、电子邮件地址、文件名和文件扩展名。
等宽字体
用于程序清单,以及段落中引用程序元素,例如变量或函数名、数据库、数据类型、环境变量、语句和关键字。
注意
这个元素表示一般说明。
警告
这个元素表示警告或注意事项。
使用代码示例
补充材料(代码示例、练习等)可在https://github.com/mikexcohen/LinAlg4DataScience下载。
如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com。
本书旨在帮助您完成工作。一般情况下,如果本书提供示例代码,您可以在自己的程序和文档中使用它。除非您复制了代码的大部分内容,否则无需联系我们以获取权限。例如,编写一个使用本书多个代码片段的程序不需要获得许可。出售或分发 O’Reilly 书籍中的示例代码需要获得许可。引用本书并引用示例代码来回答问题不需要获得许可。将本书大量示例代码整合到您产品的文档中需要获得许可。
我们感谢,但通常不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“实用线性代数数据科学”由 Mike X. Cohen(O’Reilly)编写。版权 2022 Syncxpress BV,978-1-098-12061-0。”
如果您觉得您对代码示例的使用超出了合理使用范围或以上给出的权限,请随时通过邮件联系我们,邮件地址为permissions@oreilly.com。
O’Reilly 在线学习
注意
40 多年来,O’Reilly Media提供技术和商业培训,以及知识和见解,帮助企业成功。
我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台为您提供按需访问的现场培训课程、深入的学习路径、互动编码环境,以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。更多信息,请访问https://oreilly.com。
如何联系我们
请就本书内容发表评论和提出问题给出您的意见:
-
O’Reilly Media, Inc.
-
1005 Gravenstein Highway North
-
Sebastopol, CA 95472
-
800-998-9938(美国或加拿大)
-
707-829-0515(国际或本地)
-
707-829-0104(传真)
我们为这本书设有网页,列出勘误、示例和任何额外信息。您可以访问https://oreil.ly/practical-linear-algebra获取这个页面。
发送邮件至bookquestions@oreilly.com,就本书的评论或技术问题提出意见。
有关我们的书籍和课程的新闻和信息,请访问https://oreilly.com。
在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media
在 Twitter 上关注我们:https://twitter.com/oreillymedia
在 YouTube 上观看我们:https://youtube.com/oreillymedia
致谢
我有一个坦白:我真的不喜欢写致谢部分。这不是因为我缺乏感激之情或认为我没有人可感谢——恰恰相反:我有太多人要感谢了,而且我不知道从哪里开始,该列出谁的名字,又该把谁遗漏。我是否应该感谢我的父母在塑造我成为写这本书的人时所起的作用?也许还要感谢他们的父母塑造了我的父母?我记得我的四年级老师告诉我长大后应该成为作家。(我不记得她的名字,也不确定我什么时候会长大,但也许她对这本书有些影响。)我写这本书的大部分时间是在去加那利群岛的远程工作旅行中度过的;也许我应该感谢飞我去那里的飞行员?或者在共享办公空间安装电气线路的电工?也许我应该感谢奥兹德米尔·帕夏在推广咖啡方面的角色,这不仅促进了我的写作,也使我分心。还有别忘了种植美味食物以供养和使我快乐的农民们。
您可以看出这个思路:是我的手指在键盘上敲打,但却需要整个人类文明的历史才能创造我和使我写这本书的环境,以及使你能够阅读这本书的环境。所以,感谢人类!
但好吧,我也可以用一个更传统的致谢部分来写一段。最重要的是,我要感谢所有参加我面授大学课程和暑期学校课程,以及我的 Udemy 在线课程的学生们,感谢他们信任我教育他们,并激励我继续改进我对应用数学和其他技术主题的解释。我也要感谢奥莱利的杰斯·哈伯曼(收购编辑),他首先联系我,询问我是否有兴趣写这本书。希拉·埃文斯(开发编辑),乔纳森·欧文(制作编辑),伊丽莎白·奥利弗(副本编辑),克里斯汀·布朗(内容服务经理),以及两位专家技术审阅者直接促成了我的按键转化为你现在阅读的这本书。我确信这个名单是不完整的,因为其他帮助出版这本书的人对我来说是未知的,或者是因为我因极高的年龄而记忆丧失而忘记了他们。¹ 对于任何读到这本书的人,即使是感觉自己对这本书做出了微不足道的贡献的人:谢谢你们。
¹ 哈哈,我在写这本书的时候已经 42 岁了。
第一章:引言
什么是线性代数以及为什么要学它?
线性代数在数学中有着有趣的历史,起源可以追溯到西方 17 世纪,而在中国要早得多。矩阵——线性代数核心的数字表格——被用来提供一种紧凑的符号来存储像几何坐标这样的数字集合(这是笛卡尔对矩阵的最初运用),以及方程组(由高斯首创)。在 20 世纪,矩阵和向量被用于包括微积分、微分方程、物理学和经济学在内的多变量数学中。
但直到最近,大多数人都不需要关心矩阵。这里的关键是:计算机在处理矩阵方面非常高效。因此,现代计算引发了现代线性代数的兴起。现代线性代数是计算性的,而传统线性代数则是抽象的。现代线性代数最好通过代码和应用于图形、统计学、数据科学、人工智能和数值模拟来学习,而传统线性代数则是通过证明和思考无限维向量空间来学习。现代线性代数为几乎每个计算机实现的算法提供了结构性支柱,而传统线性代数则常常是高等数学大学生的智力食粮。
欢迎来到现代线性代数。
你应该学习线性代数吗?这取决于你是否想理解算法和过程,还是仅仅应用别人开发的方法。我并不是贬低后者——使用你不理解的工具没有本质上的错误(我正在一台我可以使用但不能从头开始构建的笔记本电脑上写这篇文章)。但考虑到你正在阅读这本标题为 O’Reilly 图书系列的书,我猜你要么(1)想要知道算法如何工作,要么(2)想要开发或调整计算方法。所以是的,你应该学习线性代数,而且你应该学习它的现代版本。
关于本书
本书的目的是教会你现代线性代数。但这不是记忆一些关键方程和苦苦钻研抽象证明的问题;目的是教会你如何思考矩阵、向量以及作用在它们上面的操作。你将会对为何线性代数如此重要有几何直觉。你将会理解如何在 Python 代码中实现线性代数概念,重点放在机器学习和数据科学的应用上。
许多传统的线性代数教科书为了泛化起见避免了数值示例,期望你自行推导困难的证明,并教授大量与计算机应用或实现无关的概念。我并不是在批评——抽象的线性代数是美丽而优雅的。但如果你的目标是将线性代数(以及数学更广泛地说)作为理解数据、统计、深度学习、图像处理等工具,那么传统的线性代数教科书可能会让你感到时间的浪费,让你感到困惑,并且担心你在技术领域的潜力。
这本书是为自学者而写的。也许你有数学、工程或物理学学位,但需要学习如何在代码中实现线性代数。或者你在大学没有学习数学,现在意识到线性代数对你的学习或工作的重要性。无论哪种方式,这本书都是一个独立的资源;它不仅仅是一门基于讲座的课程的补充(尽管它可以用于这个目的)。
如果你在阅读前面三段时点头表示赞同,那么这本书绝对适合你。
如果你想深入研究线性代数,包括更多的证明和探索,那么有几本优秀的书籍可以考虑,包括我的《线性代数:理论、直觉、代码》(Sincxpress BV)。¹
先决条件
我试图为那些背景知识较少但热情学习者写这本书。话虽如此,没有东西是真正从零开始学会的。
数学
你需要对高中数学感到自在。只要基本的代数和几何;没什么高深的东西。
这本书不需要任何微积分(尽管微积分对于线性代数经常用于的应用,如深度学习和优化,非常重要)。
但更重要的是,你需要对数学有一定的思考能力,能看方程和图形,并接受与学习数学相关的知识挑战。
态度
线性代数是数学的一个分支,因此这是一本数学书。学习数学,尤其是作为成年人,需要一些耐心、奉献精神和积极的态度。泡一杯咖啡,深呼吸,把手机放在另一个房间,然后开始深入研究。
在你脑海中会有一个声音告诉你,你太老了或太笨了,学习高级数学。有时这个声音会更响,有时更轻,但它总是在那里。而且不仅仅是你——每个人都有这样的声音。你不能抑制或摧毁那个声音;甚至别试了。接受一点不安全感和自我怀疑是人类的一部分。每次那个声音响起,都是你向它证明它错了的挑战。
编程
本书侧重于代码中的线性代数应用。我选择用 Python 写这本书,因为 Python 目前是数据科学、机器学习及相关领域中使用最广泛的语言。如果你更喜欢 MATLAB、R、C 或 Julia 等其他语言,那么我希望你能轻松地将 Python 代码翻译过去。
我试图尽可能简化 Python 代码,同时保持其适用性。第十六章 提供了 Python 编程的基本介绍。你是否需要阅读这一章取决于你的 Python 技能水平:
中级/高级(>1 年编码经验)
完全跳过第十六章,或者可能略读一下,了解一下在本书的其余部分中会出现的代码类型。
一些知识(<1 年经验)
如果有新的内容或需要复习的内容,请务必仔细阅读该章节。但你应该能够迅速通过它。
完全初学者
详细阅读本章节。请理解,这本书不是完整的 Python 教程,因此如果你在内容章节中遇到代码困难,可能需要放下这本书,通过专门的 Python 课程或书籍进行学习,然后再回到这本书。
数学证明与从编码中获得的直觉
学习数学的目的是理解数学。如何理解数学呢?我们来数数:
严格的证明
在数学中,证明是一系列陈述,表明一组假设导致了一个逻辑结论。证明在纯数学中无疑是非常重要的。
可视化和示例
清晰的解释、图表和数值示例帮助你理解线性代数中的概念和操作。大多数示例都在 2D 或 3D 中进行简单可视化,但这些原则也适用于更高维度。
这两者的区别在于,正式的数学证明提供了严谨性,但很少提供直觉,而可视化和示例通过实际操作提供持久的直觉,但可能根据不同的特定示例存在风险不准确性。
包括重要声明的证明,但我更注重通过解释、可视化和代码示例来建立直觉。
这使我想到了从编码中获得数学直觉(我有时称之为“软证明”)。这里的想法是:你假设 Python(以及诸如 NumPy 和 SciPy 这样的库)正确地实现了低级别的数值计算,而你通过探索许多数值示例来专注于原则。
一个快速的例子:我们将“软证明”乘法的交换性原理,即 :
a = np.random.randn()
b = np.random.randn()
a*b - b*a
这段代码生成两个随机数,并测试交换乘法顺序对结果无影响的假设。如果可交换原则成立,第三行将输出0.0。如果你多次运行此代码并始终得到0.0,那么你通过在许多不同的数值示例中看到相同的结果来获得了可交换性的直觉。
明确一点:从代码中获得的直觉不能替代严格的数学证明。关键是,“软证明”允许你理解数学概念,而不必担心抽象数学语法和论证的细节。这对缺乏高级数学背景的程序员特别有利。
底线是你可以通过一点编程学到很多数学。
书中和在线下载的代码
你可以阅读本书而不看代码或解决代码练习。这没问题,你肯定会学到一些东西。但如果你的知识是肤浅和短暂的,不要感到失望。如果你真的想理解线性代数,你需要解决问题。这就是为什么本书为每个数学概念都配有代码演示和练习的原因。
重要的代码直接打印在书中。我希望你阅读文本和方程式,查看图形,并同时看到代码。这将帮助你将概念和方程式与代码联系起来。
但在书中打印代码可能占用大量空间,而在计算机上手动复制代码很烦琐。因此,书中仅打印关键代码行;在线代码包含额外的代码、注释、图形装饰等。在线代码还包含编程练习的解决方案(所有练习,不仅仅是奇数问题!)。你应该在阅读本书的同时下载代码并进行学习。
所有代码都可以从 GitHub 网站https://github.com/mikexcohen/LinAlg4DataScienc获取。你可以克隆这个仓库或简单地下载整个仓库作为 ZIP 文件(无需注册、登录或付费下载代码)。
我使用 Google 的 Colab 环境在 Jupyter 笔记本中编写代码。我选择使用 Jupyter 因为它是一个友好且易于使用的环境。话虽如此,我鼓励你使用任何你喜欢的 Python 集成开发环境。在线代码也以原始的.py文件形式提供以方便使用。
代码练习
数学不是一项旁观者运动。大多数数学书籍都有无数的纸和笔问题要解决(老实说:没有人会解决所有这些问题)。但这本书完全是关于应用线性代数的,没有人会在纸上应用线性代数!相反,你要在代码中应用线性代数。因此,不再有手工解决的问题和让读者练习的乏味证明(数学教科书作者喜欢写的),这本书有大量的代码练习。
代码练习的难度各不相同。如果你是 Python 和线性代数的新手,你可能会发现一些练习非常具有挑战性。如果你遇到困难,这里有一个建议:快速浏览我的解决方案以获取灵感,然后将其放在一边,以便不再看到我的代码,然后继续努力完成你自己的代码。
在比较你的解决方案和我的时候,请记住在 Python 中解决问题有多种方式。找到正确答案很重要;你所采取的步骤通常取决于个人的编码风格。
如何使用本书(供教师和自学者使用)
这本书有三种使用环境:
自学者
我试图使这本书适合那些希望在非正式课堂环境外自学线性代数的读者。虽然当然还有无数其他书籍、网站、YouTube 视频和在线课程可能对学生有帮助。
数据科学课程的主要教材
这本书可以作为数据科学、机器学习、人工智能及相关主题的数学基础课程的主要教材。除此介绍和 Python 附录外,共有 14 个内容章节,学生每周可以预计学习一到两章。因为学生可以访问所有练习的解答,教师可以选择用额外的问题集来补充书中的练习。
数学课程中重点是线性代数的次要教材
这本书也可以作为数学课程的补充教材,重点是证明。在这种情况下,讲座将专注于理论和严格的证明,而这本书可以用于将概念转化为代码,关注数据科学和机器学习中的应用。正如我前面所写的,教师可以选择提供补充练习,因为所有书中练习的解答都可以在线找到。
¹ 抱歉自吹自擂;我保证这本书里我只会这么一次地对你做这样的纵容。
第二章:向量,第一部分
向量提供了构建线性代数(因此也是本书其余部分)的基础。
在本章结束时,你将了解关于向量的一切:它们是什么,它们的作用是什么,如何解释它们,以及如何在 Python 中创建和操作它们。你将理解最重要的作用于向量的操作,包括向量代数和点积。最后,你将学习向量分解,这是线性代数的主要目标之一。
在 NumPy 中创建和可视化向量
在线性代数中,向量 是一组有序的数字列表。(在抽象的线性代数中,向量可以包含其他数学对象,包括函数;但由于本书专注于应用,我们只考虑由数字组成的向量。)
向量具有几个重要特征。我们将从前两个开始:
维度
向量中的数字数量
方向
向量是在 列方向(竖直)或 行方向(水平)的情况下
维度常用一个看起来很复杂的 表示,其中 表示实数(对比 表示复数),而 表示维度。例如,具有两个元素的向量称为 的成员。这个特殊的 字符是使用 LaTeX 代码制作的,但你也可以写成 R²、R2 或者 R²。
方程 2-1 展示了几个向量的例子;请在阅读后续段落之前确定它们的维度和方向。
方程 2-1。列向量和行向量的例子
这里是答案:x 是一个 4 维列向量,y 是一个 2 维列向量,而 z 是一个 4 维行向量。你也可以写成,例如,,其中 符号表示“属于集合”。
x 和 z 是同一个向量吗?从技术上讲,它们是不同的,即使它们的元素顺序相同。详细讨论请参见 [“向量方向是否重要?”。
你将会在本书以及在整合数学和编程的冒险中学到,数学“在黑板上”的方式与在代码中实现的方式之间存在差异。有些差异微不足道,不重要,而其他则会导致混淆和错误。现在,让我向你介绍数学和编程之间术语上的差异。
我之前写道,向量的维数是向量中元素的数量。然而,在 Python 中,向量或矩阵的维数是用于打印数值对象的几何维度的数量。例如,上面显示的所有向量在 Python 中都被认为是“二维数组”,无论向量中包含的元素数量是多少(这是数学上的维数)。在 Python 中,没有特定方向的数字列表被认为是一维数组,无论元素数量如何(该数组将被打印为一行,但是,正如您稍后将看到的,它与行向量的处理方式不同)。在 Python 中,向量的数学维数——向量中的元素数量——称为向量的长度或形状。
这种不一致且有时冲突的术语可能会令人困惑。事实上,在不同学科(在本例中是数学和计算机科学)交汇处,术语通常是一个棘手的问题。但别担心,通过一些实践你会慢慢掌握。
当涉及到向量时,通常使用小写加粗的罗马字母,比如v表示“向量 v”。有些文本使用斜体(v)或在顶部打印一个箭头( )。
线性代数的惯例是默认向量为列向量,除非另有说明。行向量写作 。 表示转置操作,您稍后会了解更多;现在只需知道转置操作将列向量转换为行向量。
向量的方向重要吗?
您真的需要担心向量是列向量还是行向量,或者是无方向的一维数组吗?有时是,有时不是。在使用向量存储数据时,方向通常并不重要。但是,如果方向错误,Python 中的某些操作可能会导致错误或意外结果。因此,理解向量的方向是很重要的,因为花费 30 分钟调试代码,最后发现一个行向量应该是一个列向量,肯定会让您头痛不已。
在 Python 中,向量可以用几种数据类型表示。list 类型可能看起来是表示向量最简单的方式,并且对于某些应用来说确实如此。但是,许多线性代数运算在 Python 列表上不起作用。因此,大多数时候最好将向量创建为 NumPy 数组。以下代码展示了创建向量的四种方式:
asList = [1,2,3]
asArray = np.array([1,2,3]) # 1D array
rowVec = np.array([ [1,2,3] ]) # row
colVec = np.array([ [1],[2],[3] ]) # column
变量 asArray 是一个无方向数组,意味着它既不是行向量也不是列向量,只是 NumPy 中的一个一维数字列表。在 NumPy 中,方向由括号决定:最外层的括号将所有数字组合成一个对象。然后,每一组额外的括号表示一行:行向量(变量 rowVec)将所有数字放在一行中,而列向量(变量 colVec)有多行,每行包含一个数字。
我们可以通过检查变量的形状来探索这些方向(在编码时检查变量形状通常非常有用):
print(f'asList: {np.shape(asList)}')
print(f'asArray: {asArray.shape}')
print(f'rowVec: {rowVec.shape}')
print(f'colVec: {colVec.shape}')
输出如下所示:
asList: (3,)
asArray: (3,)
rowVec: (1, 3)
colVec: (3, 1)
输出显示一维数组 asArray 的大小为(3),而有方向的向量是二维数组,其大小为(1,3)或(3,1),具体取决于方向。维度总是按照(行数,列数)列出。
向量的几何
有序数字列表 是向量的代数解释;向量的几何解释是具有特定长度(也称为大小)和方向(也称为角度;相对于正 x-轴计算)。向量的两个点称为尾部(起始点)和头部(结束点);头部通常带有箭头提示以区分尾部。
你可能认为向量编码了一个几何坐标,但向量和坐标实际上是不同的东西。然而,它们在向量从原点开始时是协调的。这被称为标准位置,并且在 图 2-1 中有所说明。

图 2-1. 所有箭头表示相同的向量。位于标准位置的向量其尾部位于原点,其头部位于协调的几何坐标。
无论是几何上还是代数上构想向量都有助于在不同应用中形成直觉,但这只是同一个问题的两面。例如,向量的几何解释在物理学和工程学中很有用(例如表示物理力量),而向量的代数解释在数据科学中很有用(例如存储随时间变化的销售数据)。通常,线性代数概念在二维图表中以几何方式学习,然后通过代数扩展到更高维度。
向量的操作
向量就像名词一样;它们是我们线性代数故事中的角色。线性代数的乐趣来自于动词——给这些角色注入生命的行动。这些行动称为操作。
一些线性代数操作简单直观,并且完全符合预期(例如加法),而其他一些操作则更复杂,需要整整几章来解释(例如奇异值分解)。让我们从简单的操作开始。
添加两个向量
要添加两个向量,只需将每个对应的元素相加。方程 2-2 展示了一个例子:
方程 2-2. 向量相加
正如你可能猜到的那样,向量加法仅对具有相同维度的两个向量定义;例如,在 中的向量和在 中的向量不能相加。
向量减法也与预期相同:逐元素减去两个向量。方程 2-3 示范了一个例子:
方程 2-3. 向量相减
在 Python 中,向量相加是直接的:
v = np.array([4,5,6])
w = np.array([10,20,30])
u = np.array([0,3,6,9])
vPlusW = v+w
uPlusW = u+w # error! dimensions mismatched!
向量加法是否受向量方向的影响?考虑方程 2-4:
方程 2-4. 你能将行向量加到列向量中吗?
您可能认为这个例子与之前显示的例子没有区别——毕竟,这两个向量都有三个元素。让我们看看 Python 做了什么:
v = np.array([[4,5,6]]) # row vector
w = np.array([[10,20,30]]).T # column vector
v+w
>> array([[14, 15, 16],
[24, 25, 26],
[34, 35, 36]])
结果可能看起来令人困惑,并且与前文所述的向量加法定义不一致。事实上,Python 正在执行一种称为广播的操作。您将在本章后面更多地了解广播,但我鼓励您花一点时间思考这个结果,并思考它是如何由添加行向量和列向量而来的。无论如何,这个例子表明方向确实很重要:只有具有相同维度 和 相同方向的两个向量才能相加。
向量加法与减法的几何性质
要在几何上添加两个向量,请将向量放置在一个向量的尾部位于另一个向量的头部的位置(图 A 参见图 2-2)。您可以将此过程扩展到对任意数量的向量求和:简单地将所有向量依次堆叠,然后和向量是从第一个尾到最后一个头的线。

图 2-2. 两个向量的和与差
几何上减去向量略有不同,但同样直接:将两个向量排列在一起,使它们的尾部位于相同的坐标(这可以通过将两个向量放在标准位置轻松实现);差向量是从“负”向量的头到“正”向量的头的线(图 B 参见图 2-2)。
不要低估向量减法的几何性质的重要性:它是正交向量分解的基础,而正交向量分解又是线性最小二乘的基础,这是科学和工程中线性代数的最重要应用之一。
向量-标量乘法
线性代数中的标量是一个独立的数字,不嵌入在向量或矩阵中。标量通常用小写希腊字母表示,如α或λ。因此,向量-标量乘法表示为,例如,βu。
向量-标量乘法非常简单:将每个向量元素乘以标量。一个数值示例(方程 2-5)足以理解:
方程 2-5. 向量-标量乘法(或:标量-向量乘法)
我之前提到,存储向量的变量的数据类型有时重要,有时不重要。向量-标量乘法就是一个数据类型重要的例子:
s = 2
a = [3,4,5] # as list
b = np.array(a) # as np array
print(a*s)
print(b*s)
>> [ 3, 4, 5, 3, 4, 5 ]
>> [ 6 8 10 ]
代码创建了一个标量(变量 s)和一个作为列表的向量(变量 a),然后将其转换为 NumPy 数组(变量 b)。在 Python 中,星号的行为取决于变量类型:标量乘以列表会重复列表 s 次(在本例中是两次),这显然不是标量-向量乘法的线性代数操作。然而,当向量存储为 NumPy 数组时,星号被解释为元素级乘法。(这里有个小练习:如果你设置 s = 2.0,会发生什么?为什么?¹)这两种操作(列表重复和向量-标量乘法)在实际编码中都有用到,所以要注意区别。
标量-向量加法
在线性代数中,向量加标量没有正式定义:它们是两种不同的数学对象,不能结合在一起。然而,像 Python 这样的数值处理程序允许将标量添加到向量中,操作类似于标量与向量的乘法:将标量添加到每个向量元素上。以下代码阐明了这个概念:
s = 2
v = np.array([3,6])
s+v
>> [5 8]
向量-标量乘法的几何学
为什么标量被称为“标量”?这源于几何解释。标量调整向量的大小而不改变其方向。标量与向量相乘有四种效果,这取决于标量是大于 1、在 0 和 1 之间、恰好为 0 还是负数。图 2-3 阐述了这个概念。

图 2-3. 相同向量(黑色箭头)乘以不同标量 (灰线;略有位移以便看清楚)
我之前写过,标量不会改变向量的方向。但图中显示,当标量为负数时向量方向会翻转(即其角度旋转 180°)。这似乎是个矛盾,但对向量的解释是沿着一个通过原点并且无限延伸到两个方向的无限长线的线(在下一章中,我将称其为“一维子空间”)。从这个意义上讲,“旋转”的向量仍然沿着同一条无限线指向,因此负标量不会改变其方向。这种解释对于矩阵空间、特征向量和奇异向量都很重要,这些概念将在后面的章节中介绍。
向量标量乘法与向量加法结合直接导致向量平均。对向量进行平均就像对数字进行平均一样:求和然后除以数字的数量。因此,要对两个向量进行平均,先将它们相加,然后乘以 0.5。一般来说,要对N个向量进行平均,将它们求和,然后将结果乘以1/N。
转置
您已经了解了转置操作:它将列向量转换为行向量,反之亦然。让我提供一个稍微更正式的定义,将推广到转置矩阵(这是第五章中的一个主题)。
矩阵有行和列;因此,每个矩阵元素都有一个(行,列)索引。转置操作简单地交换这些索引。这在方程 2-6 中得到了正式化:
方程 2-6。转置操作
向量的方向有一行或一列,这取决于它们的方向。例如,一个 6 维行向量有i = 1 和j从 1 到 6 的索引,而一个 6 维列向量有i从 1 到 6 的索引和j = 1。因此,交换i,j索引会交换行和列。
这里有一个重要的规则:两次转置将向量返回到其原始方向。换句话说, 。这可能看起来显而易见和琐碎,但它是数据科学和机器学习中几个重要证明的基石,包括创建对称协方差矩阵作为数据矩阵乘以其转置(这也是主成分分析将数据空间进行正交旋转的原因……别担心,这个句子以后在本书中会有更多解释!)。
Python 中的向量广播
广播是一种仅存在于现代基于计算机的线性代数中的操作;这不是传统线性代数教科书中会找到的过程。
广播本质上意味着在一个向量和另一个向量的每个元素之间多次重复一个操作。考虑以下一系列方程:
注意向量中的模式。我们可以将这组方程紧凑地实现为向量[1 2 3]和[10 20],然后广播加法。以下是 Python 中的实现方式:
v = np.array([[1,2,3]]).T # col vector
w = np.array([[10,20]]) # row vector
v + w # addition with broadcasting
>> array([[11, 21],
[12, 22],
[13, 23]])
再次可以看到线性代数操作中方向的重要性:尝试运行上面的代码,将v变成一个行向量和w变成一个列向量。²
因为广播允许进行高效紧凑的计算,它经常在数值编码中使用。您将在本书中看到几个广播的例子,包括* k * -均值聚类部分(第四章)。
向量大小和单位向量
一个向量的大小 —— 也称为几何长度 或 范数 —— 是从向量的尾到头的距离,并且使用标准欧几里德距离公式计算:向量元素的平方和的平方根(见 方程 2-7)。向量大小用双竖线标示在向量周围: 。
方程 2-7. 矢量的范数
有些应用程序使用平方幅度(写成 ∥ 𝐯 ∥² ),在这种情况下,右侧的平方根项消失。
在展示 Python 代码之前,让我解释一些“黑板”线性代数与 Python 线性代数之间的术语差异。在数学中,向量的维数是该向量中元素的数量,而长度是几何距离;在 Python 中,函数 len()(其中 len 缩写为 length)返回数组的 维数,而函数 np.norm() 返回几何长度(大小)。在本书中,我将使用术语 大小(或 几何长度)代替 长度,以避免混淆:
v = np.array([1,2,3,7,8,9])
v_dim = len(v) # math dimensionality
v_mag = np.linalg.norm(v) # math magnitude, length, or norm
有些应用程序需要一个几何长度为一的向量,这被称为单位向量。例如应用包括正交矩阵、旋转矩阵、特征向量和奇异向量。
单位向量被定义为 。
毫无疑问,大量向量并非单位向量。(我很想写“大多数向量不是单位向量”,但是非单位向量和单位向量都有无穷多个,尽管非单位向量的集合大于单位向量的集合。)幸运的是,任何非单位向量都有一个相关联的单位向量。这意味着我们可以创建一个与非单位向量方向相同的单位向量。创建相关联的单位向量很容易;你只需乘以矢量范数的倒数(方程 2-8):
方程 2-8. 创建单位向量
你可以看到表示单位向量的常见约定( )与其父向量 在相同方向上的情况。图 2-4 阐明了这些情况。

图 2-4. 一个单位向量(灰色箭头)可以从一个非单位向量(黑色箭头)中制作出来;这两个向量有相同的角度但不同的大小。
实际上,“任何 非单位向量都有相关联的单位向量”这一说法并不完全正确。有一个矢量其长度为非单位长度,但没有相关联的单位向量。你能猜出这是哪个向量吗?³
这里不展示创建单位向量的 Python 代码,因为这是本章末尾的一个练习之一。
向量点积
点积(有时也称为内积)是线性代数中最重要的操作之一。它是许多操作和算法的基本计算模块,包括卷积、相关性、傅里叶变换、矩阵乘法、线性特征提取、信号过滤等。
有几种方法可以表示两个向量之间的点积。出于某些原因,在学习矩阵乘法后,我将主要使用常见的符号 。在其他情况下,您可能会看到 或 。
点积是一个单一的数字,提供关于两个向量之间关系的信息。让我们首先专注于计算点积的算法,然后我会讨论如何解释它。
要计算点积,您需要将两个向量的对应元素相乘,然后将所有单个乘积求和。换句话说:逐元素乘法和求和。在 方程式 2-9 中,a 和 b 是向量,a[i] 表示 a 的第 i 个元素。
方程式 2-9. 点积公式
从公式可以看出,点积只在相同维度的两个向量之间有效。方程式 2-10 展示了一个数值示例:
方程式 2-10. 示例点积计算
索引的烦恼
标准数学表示法和一些数学导向的数值处理程序,如 MATLAB 和 Julia,从 1 开始索引,到 N 结束;而一些编程语言如 Python 和 Java 从 0 开始索引,到 N − 1 结束。我们不需要辩论每种约定的优缺点 — 虽然我有时会想知道这种不一致性引入了多少错误到人类文明中 — 但在将公式翻译为 Python 代码时注意这种差异是很重要的。
在 Python 中实现点积有多种方法;最直接的方法是使用 np.dot() 函数。
v = np.array([1,2,3,4])
w = np.array([5,6,7,8])
np.dot(v,w)
关于 np.dot() 的注意事项
函数np.dot()实际上并没有实现向量的点积;它实现的是矩阵乘法,而矩阵乘法是一组点积的集合。在学习矩阵乘法的规则和机制之后会更加清晰(第五章)。如果你想现在就探索这一点,可以修改之前的代码,给两个向量分别赋予方向(行向量对比列向量)。你会发现,只有在第一个输入是行向量,第二个输入是列向量时,输出才是点积。
点积的一个有趣属性是:标量乘以一个向量会按相同的比例扩展点积。我们可以通过扩展之前的代码来探索这一点:
s = 10
np.dot(s*v,w)
向量v和w的点积为 70,使用s*v进行的点积(在数学符号中可以写为)为 700。现在尝试一个负标量,例如s = -1。你会看到点积的大小保持不变,但符号被反转。当然,当s = 0时,点积为零。
现在你知道如何计算点积了。点积意味着什么,我们如何解释它?
点积可以被解释为两个向量之间的相似性或映射的度量。想象一下,你从 20 个人那里收集了身高和体重数据,并将这些数据存储在两个向量中。你肯定希望这些变量之间有关联(身高较高的人 tend to weigh more),因此你可以期望这两个向量之间的点积较大。然而,点积的大小取决于数据的比例,这意味着以克和厘米测量的数据之间的点积将大于以磅和英尺测量的数据之间的点积。然而,这种任意的缩放可以通过归一化因子来消除。事实上,两个变量的归一化点积被称为皮尔逊相关系数,它是数据科学中最重要的分析之一。更多信息请参见第四章!
点积具有分配性质
数学的分配性质是:。转化为向量和向量点积的术语,意味着:
换句话说,一个向量和的点积等于各个向量点积的和。
以下 Python 代码展示了分配性质:
a = np.array([ 0,1,2 ])
b = np.array([ 3,5,8 ])
c = np.array([ 13,21,34 ])
# the dot product is distributive
res1 = np.dot( a, b+c )
res2 = np.dot( a,b ) + np.dot( a,c )
两个结果 res1 和 res2 是相同的(对于这些向量,答案是 110),这说明了点积的分配性质。注意数学公式如何被转化为 Python 代码;将公式转化为代码是数学导向编程中的重要技能。
点积的几何性质
点积还有一个几何定义,即两个向量的大小乘积,乘以它们之间的余弦值(方程式 2-11)。
方程式 2-11. 向量点积的几何定义
方程式 2-9 和 方程式 2-11 在数学上是等价的,但表达形式不同。它们的等价性证明是数学分析中有趣的练习,但需要大约一页的文本,依赖于首先证明包括余弦定理在内的其他原理。这个证明对本书不相关,因此被省略。
注意向量的大小严格为正(除了零向量,其长度为 ),而角的余弦值可以在 -1 到 +1 之间。这意味着点积的符号完全由两个向量之间的几何关系决定。图 2-5 展示了两个向量之间角度的五种点积符号情况(在二维中用于可视化,但原理适用于更高维度)。

图 2-5. 两个向量之间的点积符号显示了这些向量之间的几何关系
记住这个:正交向量的点积为零
一些数学老师坚持认为你不应该记忆公式和术语,而应该理解过程和证明。但让我们诚实地说:记忆是学习数学中重要且不可避免的一部分。幸运的是,线性代数并不需要过多的记忆负担,但有一些事情你必须简单地记住。
举个例子:正交向量的点积为零(反之亦然——当点积为零时,两个向量是正交的)。因此,以下陈述是等价的:两个向量是正交的;两个向量的点积为零;两个向量成 90°角。重复这种等价性,直到它永久地刻在你的大脑中。
其他向量乘法
点乘可能是最重要、也是最经常使用的一种向量乘法方式。但还有几种其他向量乘法方式。
哈达玛乘法
这只是一个元素级乘法的花哨术语。要实现哈达玛乘法,需要将两个向量中对应的每个元素相乘。乘积是与两个乘数相同维度的向量。例如:
在 Python 中,星号表示两个向量或矩阵的逐元素乘法:
a = np.array([5,4,8,2])
b = np.array([1,0,.5])
a*b
尝试在 Python 中运行该代码然后……哎呀!Python 会报错。找到并修复了错误。从该错误中,你学到了什么关于哈达玛乘法?查看脚注获取答案。⁴
哈达玛乘积是组织多个标量乘法的便捷方式。例如,想象一下你有不同商店销售的小部件数量以及每个商店每件小部件的价格的数据。你可以将每个变量表示为向量,然后对这些向量进行哈达玛乘法以计算每个商店的小部件收入(这与跨所有商店的总收入不同,后者可以通过点积计算)。
外积
外积是通过一个列向量和一个行向量创建矩阵的一种方式。外积矩阵中的每一行是行向量与列向量中对应元素的标量乘积。我们也可以说外积矩阵中的每一列是列向量与行向量中对应元素的标量乘积。在第六章中,我将称之为“秩-1 矩阵”,但现在不要担心这个术语;而是要专注于下面示例中展示的模式:
外积与点积有很大的不同:它产生一个矩阵而不是一个标量,并且外积中的两个向量可以具有不同的维度,而点积中的两个向量必须具有相同的维度。
外积表示为(请记住我们假设向量是列向量,因此外积涉及将列乘以行)。注意点积()与外积()之间的微妙但重要的差异。现在这可能看起来奇怪和令人困惑,但我保证在学习第五章关于矩阵乘法后,这将变得非常清晰。
外积类似于广播,但它们并不相同:广播是一种通用的编码操作,用于在算术运算(如加法、乘法和除法)中扩展向量;外积是一种特定的数学过程,用于两个向量的乘法。
NumPy 可以通过函数np.outer()或函数np.dot()计算外积,如果两个输入向量分别是列向量和行向量。
交叉乘积和三重积
"还有其他几种向量乘法方法,如叉积或三重积。这些方法用于几何和物理学中,但在技术相关应用中很少出现,本书不会花时间介绍它们。我在这里提到它们只是让你对这些名词有所了解。"
"正交向量分解"
"矩阵或向量的“分解”意味着将该矩阵分解为多个更简单的部分。分解用于揭示矩阵中“隐藏”的信息,使矩阵更易于处理,或用于数据压缩。可以毫不夸张地说,线性代数的很多内容(无论是抽象的还是实际的)都涉及矩阵分解。矩阵分解非常重要。"
"让我用两个简单的标量示例介绍分解的概念:"
-
"我们可以将数字 42.01 分解为两部分:42 和.01。也许.01 是要忽略的噪音,或者目标是压缩数据(整数 42 比浮点数 42.01 需要更少的内存)。无论动机如何,分解涉及将一个数学对象表示为更简单对象的和(42 = 42 + .01)。"
-
"我们可以将数字 42 分解为质数 2、3 和 7 的乘积。这种分解称为质因数分解,在数值处理和密码学中有许多应用。这个例子涉及乘积而不是求和,但要点是相同的:将一个数学对象分解为更小、更简单的部分。"
"在本节中,我们将开始探讨一个简单但重要的分解方法,即将一个向量分解为两个分开的向量,其中一个与参考向量正交,另一个与该参考向量平行。正交向量分解直接导致了格拉姆-施密特过程和 QR 分解,这在解决统计学中的逆问题时经常使用。"
"让我们从一幅图开始,这样你可以看到分解的目标。图 2-6 说明了这种情况:我们有两个位于标准位置的向量 和 ,我们的目标是找到 头部最接近的 点。我们还可以将其表达为一个优化问题:将向量 投影到向量 上,使得投影距离最小化。当然,在 上的那一点将会是 的缩放版本。所以我们的目标是找到标量 。(与正交向量分解的联系很快会变得清晰。)"
"
图 2-7. 正交向量分解的示例:将向量 分解为与向量 正交且平行的两个向量之和。
我们从定义平行分量开始。什么是与平行的向量?显然,任何的缩放版本都与平行。因此,我们通过刚刚发现的正交投影公式(Equation 2-13)来简单地找到。
方程式 2-13。计算t相对于r的平行分量
注意与 Equation 2-12 的微妙差别:那里我们只计算了标量;而这里我们要计算缩放向量。
那就是平行分量。我们如何找到垂直分量呢?这更容易,因为我们已经知道这两个向量分量必须等于原始目标向量。因此:
换句话说,我们从原始向量中减去平行分量,剩余部分即为我们的垂直分量。
但是那个垂直分量真的与参考向量正交吗?是的,它是!为了证明这一点,你可以展示垂直分量与参考向量的点积为零:
完成这个证明的代数过程是直接但乏味的,所以我省略了它。相反,你将通过 Python 代码来建立直觉。
希望你喜欢学习正交向量分解。再次注意一般原则:我们将一个数学对象分解为其他对象的组合。分解的细节取决于我们的约束条件(在本例中为正交和平行于参考向量),这意味着不同的约束条件(即分析目标的不同)可以导致相同向量的不同分解。
总结
线性代数的美在于,即使是对矩阵进行最复杂和计算密集的操作,也可以分解为简单操作,其中大部分可以通过几何直觉来理解。不要低估对向量进行简单操作的重要性,因为你在本章学到的将构成本书以及你作为应用线性代数学家(如果你从事数据科学、机器学习、人工智能、深度学习、图像处理、计算视觉、统计等工作,这才是你真正的身份)职业生涯的基础。
这是本章最重要的要点:
-
向量是一个按顺序排列的数字列表,可以放置在列或行中。向量的元素数称为其维数,向量可以在几何空间中表示为具有与维数相等的轴数的线。
-
向量的几个算术运算(加法、减法和 Hadamard 乘法)按元素逐个操作。
-
点积是一个单一的数字,编码了两个相同维度向量之间的关系,计算方法是元素逐个相乘并求和。
-
对于正交向量,点积为零,几何上意味着向量在直角处相交。
-
正交向量分解涉及将一个向量分解为另外两个与参考向量正交且平行的向量之和。这种分解的公式可以从几何中重新推导出来,但你应该记住“映射到大小”的概念正是这个公式表达的概念。
代码练习
我希望你不要把这些练习看作是你需要完成的烦人工作。相反,这些练习是提升你的数学和编码技能的机会,确保你真正理解本章内容。
我也希望你把这些练习看作是使用 Python 继续探索线性代数的跳板。更改代码以使用不同的数字、不同的维度、不同的方向等。编写自己的代码来测试章节中提到的其他概念。最重要的是:享受学习过程,拥抱学习的体验。
作为提醒:所有练习的解答可以在https://github.com/mikexcohen/LA4DataScience上查看或下载。
练习 2-1。
在线代码库“丢失”了创建 Figure 2-2 的代码。(实际上并不是真的“丢失” — 我把它移到了这个练习的解决方案中。)因此,你的目标是编写自己的代码来生成 Figure 2-2。
练习 2-2。
编写一个算法,通过将 Equation 2-7 翻译成代码来计算向量的范数。使用不同维度和方向的随机向量来确认,你得到的结果与np.linalg.norm()相同。这个练习旨在让你更多地练习索引 NumPy 数组和将公式转化为代码;实际上,使用np.linalg.norm()通常更容易。
练习 2-3。
创建一个 Python 函数,将一个向量作为输入,并输出与之方向相同的单位向量。当你输入零向量时会发生什么?
练习 2-4。
你知道如何创建单位向量;如果你想创建任意大小的向量怎么办?编写一个 Python 函数,它将接受一个向量和一个期望的大小作为输入,并返回一个方向相同但大小对应于第二个输入的向量。
练习 2-5。
编写一个for循环,将行向量转置为列向量,而不使用如np.transpose()或v.T等内置函数或方法。这个练习将帮助你创建和索引具有方向性的向量。
练习 2-6。
这里有一个有趣的事实:你可以计算向量的平方范数,就像将向量与自身的点积一样。回顾 方程 2-8 以确信这两者是等价的。然后使用 Python 来确认它。
练习 2-7。
编写代码来证明点积是可交换的。可交换意味着 ,对于向量的点积来说,意味着 。在用代码演示后,使用方程 方程 2-9 来理解点积为何是可交换的。
练习 2-8。
编写代码生成 图 2-6。 (注意,你的解决方案不需要与图完全相同,只要关键元素存在即可。)
练习 2-9。
实现正交向量分解。从两个随机数向量 和 开始,并重现 图 2-8(注意,由于随机数的原因,你的图看起来可能有所不同)。接下来,确认这两个分量的和是 ,并且 和 是正交的。

图 2-8。练习 9。
练习 2-10。
编程中的一个重要技能是查找错误。假设你的代码中有一个 bug,导致 方程 2-13 中的投影标量的分母是 而不是 (这是我在写这一章节时个人经历的一个容易犯的错误!)。实现这个 bug 来检查它是否真的导致了代码偏离正确结果。你可以做什么来确认结果是正确的还是错误的?(在编程中,用已知结果来确认代码的正确性被称为合理检查。)
¹ a*s 抛出一个错误,因为列表重复只能使用整数;不可能将列表重复 2.72 次!
² Python 仍然进行广播,但结果是一个 3 × 2 的矩阵,而不是一个 2 × 3 的矩阵。
³ 零向量的长度为 0,但没有相关的单位向量,因为它没有方向,并且不可能将零向量缩放为非零长度。
⁴ 错误在于这两个向量的维度不同,这表明哈达玛乘积仅对维度相同的两个向量定义。你可以通过从a中移除一个数字或向b中添加一个数字来修复问题。
第三章:向量,第二部分
前一章为理解向量和作用在向量上的基本操作奠定了基础。现在你将通过学习一系列相互关联的概念扩展你的线性代数知识,包括线性独立性,子空间和基。这些主题对理解矩阵操作至关重要。
这里的一些主题可能看起来抽象,并且与应用无关,但它们之间有一条非常短的路径,例如,向量子空间和将统计模型拟合到数据中。数据科学中的应用稍后会介绍,所以请继续专注于基础知识,这样高级主题会更容易理解。
向量集
我们可以从一些简单的事情开始这一章:向量的集合称为集合。你可以想象把一堆向量放进袋子里携带。向量集使用大写斜体字母表示,如 S 或 V。数学上,我们可以描述集合如下:
想象一下,例如,一个包含一百个国家的 Covid-19 正确案例、住院和死亡数的数据集;你可以将每个国家的数据存储在一个三元素向量中,并创建一个包含一百个向量的向量集。
向量集可以包含有限数量或无限数量的向量。带有无限数量向量的向量集听起来可能像一个毫无用处的愚蠢抽象,但向量子空间是无限向量集,并对将统计模型拟合到数据中具有重要影响。
向量集也可以是空的,并且被表示为 V = {}。当你学习矩阵空间时,你会遇到空的向量集。
线性加权组合
线性加权组合 是一种混合多个变量信息的方法,其中一些变量的贡献大于其他变量。这种基本操作有时也称为线性混合或加权组合(线性部分被假定)。有时,术语系数也被用来代替权重。
线性加权组合简单地意味着标量与向量的乘法和加法:取一些向量集,将每个向量乘以一个标量,然后加起来得到一个单一的向量(方程式 3-1)。
方程式 3-1。线性加权组合
假设所有向量 都具有相同的维度;否则,加法是无效的。 可以是任意实数,包括零。
从技术上讲,你可以重新编写 方程式 3-1 来减去向量,但因为减法可以通过将 设置为负数来处理,所以更容易通过求和来讨论线性加权组合。
方程式 3-2 展示了一个例子,以帮助更加具体:
方程式 3-2。线性加权组合
线性加权组合易于实现,如下面的代码所示。在 Python 中,数据类型很重要;测试当向量是列表而不是 NumPy 数组时会发生什么:^([1)
l1 = 1
l2 = 2
l3 = -3
v1 = np.array([4,5,1])
v2 = np.array([-4,0,-4])
v3 = np.array([1,3,2])
l1*v1 + l2*v2 + l3*v3
每个向量和每个系数作为单独的变量存储是繁琐的,并且不能扩展到更大的问题。因此,在实践中,线性加权组合是通过紧凑且可扩展的矩阵向量乘法方法来实现的,你将在第 Chapter 5 章学习到;目前,重点是概念和编码实现。
线性加权组合有多个应用场景。其中三个包括:
-
从统计模型中预测的数据是通过回归器(预测变量)和系数(标量)的线性加权组合创建的,这些系数是通过最小二乘算法计算得出的,你将在第十一章和第十二章学习到。
-
在主成分分析等降维过程中,每个成分(有时称为因子或模式)都是数据通道的线性加权组合,其中所选的权重(系数)被选择以最大化成分的方差(以及一些其他你将在第十五章中学到的约束条件)。
-
人工神经网络(支持深度学习的架构和算法)涉及两种操作:输入数据的线性加权组合,然后是非线性变换。权重通过最小化成本函数学习,通常是模型预测与真实目标变量之间的差异。
线性加权组合的概念是创建向量子空间和矩阵空间的机制,是线性独立性的核心。确实,线性加权组合和点积是许多高级线性代数计算的两个最重要的基本构建模块之一。
线性独立性
如果集合中至少有一个向量可以表示为该集合中其他向量的线性加权组合,则向量集合线性相关。因此,如果集合中没有向量可以表示为该集合中其他向量的线性加权组合,则向量集合线性独立。
以下是两组向量集合。在阅读文本之前,请尝试确定每组向量集合是依赖还是独立的。(术语线性独立有时在线性部分被隐含时缩写为独立。)
向量集合V是线性无关的:无法将集合中的一个向量表示为集合中其他向量的线性倍数。换句话说,如果我们将集合中的向量称为和,那么不存在可能的标量使得。
那么S集合呢?这个是相关的,因为我们可以使用集合中一些向量的线性加权组合来得到集合中的其他向量。这种组合有无数种,其中两种是s[1] = .5s[2]和s[2] = 2s[1]。
让我们再来看一个例子。同样,问题是T集合是线性独立还是线性相关:
哇,这个比前两个例子难多了。结果证明这是一个线性相关的集合(例如,前三个向量的和等于第四个向量的两倍)。但我不希望你仅凭视觉检查就能弄清楚。
那么在实践中如何确定线性独立性呢?确定线性独立性的方法是从向量集创建矩阵,计算矩阵的秩,并将秩与行或列的较小者进行比较。现在这句话可能对你来说没有意义,因为你还没有学习矩阵秩。因此,现在请将注意力集中在这个概念上:如果集合中至少有一个向量可以表示为其他向量的线性加权组合,则该向量集是线性相关的;如果没有向量可以表示为其他向量的组合,则向量集是线性独立的。
线性独立性的数学基础
现在你已经理解了这个概念,我想确保你也理解线性相关的正式数学定义,这在[方程 3-3 中表达出来。
方程 3-3. 线性相关²
这个方程表明,线性相关意味着我们可以定义一些向量集中的线性加权组合来生成零向量。如果你能找到一些使得这个方程成立,那么这个向量集是线性相关的。反之,如果没有可能的方式将向量线性组合为零向量,则集合是线性独立的。
这可能起初看起来不直观。为什么我们在问题是是否可以将集合中至少一个向量表达为其他向量的加权组合时要关心零向量?也许你更喜欢将线性相关的定义重写为以下内容:
为什么不从这个方程开始,而不是将零向量放在左侧?将方程设置为零有助于强化这样一个原则:整个集合是相关还是独立的;没有一个单独的向量有特权地位成为“依赖向量”(参见“独立集合”)。换句话说,当涉及独立性时,向量集是纯粹平等的。
但请稍等。仔细检查方程 3-3,会发现一个平凡的解:将所有设为零,这个方程变成,无论集合中的向量如何。但是,正如我在第二章中写道,涉及零的平凡解在线性代数中经常被忽略。因此,我们增加了至少一个的约束。
通过将方程除以标量之一,可以将这种约束条件纳入方程中;请记住, 和 可以指代集合中的任何向量/标量对:
独立性和零向量
简而言之,任何包含零向量的向量集都自动成为线性相关集。原因在于:零向量的任何标量倍数仍然是零向量,因此总是满足线性相关的定义。你可以在以下方程中看到这一点:
只要,我们就有一个非平凡解,且该集合符合线性相关的定义。
关于非线性独立性?
“但是迈克,”我想象你会反驳,“生命、宇宙和一切不都是非线性的吗?”我想这会是一个有趣的练习,统计宇宙中线性与非线性相互作用的总数,看看哪个总和更大。但是线性代数关注的是,嗯,线性操作。如果你能将一个向量表达为其他向量的非线性(但非线性)组合,那么这些向量仍然构成线性独立集合。线性约束的原因在于我们希望将变换表达为矩阵乘法,而这是一种线性操作。这并不是在贬低非线性操作——在我的想象对话中,你已经精辟地阐述了一个纯线性的宇宙将会相当乏味和可预测。但我们不需要用线性代数来解释整个宇宙;我们只需要用线性代数来处理其中的线性部分。(还值得一提的是,许多非线性系统可以很好地用线性函数来近似。)
子空间与张量
当我介绍线性加权组合时,我使用了具体的权重数值示例(例如,)。子空间是相同的概念,但使用了集合中向量的无限可能的线性组合方式。
即,对于某些(有限)向量集,通过使用相同的向量但不同的权重数值进行线性组合的无限方法,创建了一个向量子空间。结合所有可能的线性加权组合的机制被称为向量集合的张量。让我们通过几个例子来理解一下。我们从一个包含一个向量的简单例子开始:
这个向量集的张成是由这些向量的线性组合形成的无穷向量集。对于只有一个向量的集合,这意味着所有可能的向量缩放版本。图 3-1 展示了向量及其所张成的子空间。注意,灰色虚线上的任何向量都可以表示为向量的某种缩放版本。

图 3-5. 使用不同基向量(黑色线)的 2D 数据集
基的定义
一旦你理解了基和基组的概念,其形式化定义就变得很直接。事实上,基本上基本就是由生成和独立性组成的:如果一组向量(1)生成了该子空间并且(2)是一组独立的向量,则可以将其作为该子空间的基。
基需要生成一个子空间才能用作该子空间的基,因为你不能描述你无法测量的东西。³ 图 3-6 展示了一个在一维子空间外的点的示例。该子空间的基向量无法测量点 r。黑色向量仍然是所跨越子空间的有效基向量,但它不构成任何超出其跨越范围的子空间的基础。

Figure 3-6. 一个基组只能测量其空间中包含的内容。
因此,基需要跨越其用于的空间。这很清楚。但是为什么基组需要线性独立呢?原因是子空间中的任何给定向量必须使用该基具有唯一的坐标。让我们想象使用以下向量集来描述来自图 3-4 的点 p:
U 是一个完全有效的向量集,但显然不是一个基组。为什么不是呢?^([4)
在集合 U 中,什么线性加权组合描述了点 p?好吧,三个向量在 U 中的线性加权组合的系数可以是(3, 0, 1)或(0, 1.5, 1)或……还有无数其他可能性。这很令人困惑,因此数学家决定,向量在一个基组内必须具有唯一的坐标。线性独立性保证了唯一性。
明确一点,点 p(或任何其他点)可以用无限多个基组描述。因此,从可能的基组的角度来看,测量并不是唯一的。但是在一个基组内,一个点由一个线性加权组合精确定义。在本节开头的距离类比中,情况也是如此:我们可以使用许多不同的测量单位来测量从阿姆斯特丹到特内里费的距离,但该距离每个测量单位只有一个值。该距离不会同时为 3,200 英里和 2,000 英里,但却同时为 3,200 公里和 2,000 英里。(注:这里我做了近似,好吗?)
总结
恭喜你完成了另一章!(嗯,几乎完成了:还有编码练习要解决。)本章的重点是将你关于向量的基础知识带到下一个水平。以下是关键点列表,但请记住,所有这些点的底层是非常少量的基本原则,主要是向量的线性加权组合:
-
向量集是一组向量的集合。集合中可以有有限个或无限个向量。
-
线性加权组合指的是对集合中的向量进行标量乘法和加法运算。线性加权组合是线性代数中最重要的概念之一。
-
如果一个向量集中的某个向量可以被其他向量的线性加权组合表示,那么这个向量集是线性相关的。如果没有这样的线性加权组合,则集合是线性无关的。
-
子空间是一组向量的所有可能的线性加权组合的无限集。
-
基是空间的度量尺。如果一个向量集(1)张成了该子空间,并且(2)线性无关,那么它可以成为子空间的基。在数据科学中的一个主要目标是发现描述数据集或解决问题的最佳基础集。
代码练习
练习 3-1。
重写线性加权组合的代码,但是将标量放入一个列表中,将向量作为 NumPy 数组的元素放入另一个列表中。然后使用for循环来执行线性加权组合操作。使用np.zeros()初始化输出向量。确认你得到了与之前代码相同的结果。
练习 3-2。
尽管在前一个练习中通过列表循环的方法不如矩阵-向量乘法高效,但比没有for循环更具可扩展性。你可以通过将额外的标量和向量作为列表元素添加来探索这一点。如果新添加的向量在而不是中会发生什么?如果标量比向量更多会发生什么?
练习 3-3。
在这个练习中,你将在子空间中绘制随机点。这将有助于加强子空间由所有生成向量的线性加权组合组成的理念。定义一个包含一个向量 [1, 3] 的向量集。然后从均匀分布在 -4 到 +4 之间随机抽取 100 个数。这些就是你的随机标量。将随机标量乘以基向量,创建子空间中的 100 个随机点。绘制这些点。
接下来,重复这个过程,但在中使用两个向量:[3, 5, 1] 和 [0, 2, 2]。注意,你需要 100 × 2 个随机标量来得到 100 个点和两个向量。生成的随机点将位于一个平面上。图 3-7 展示了结果的样子(从图中不清楚这些点位于平面上,但当你在屏幕上拖动图表时你会看到)。
我推荐使用plotly库来绘制这些点,这样你可以在 3D 轴上点击拖动。以下是使其工作的提示:
import plotly.graph_objects as go
fig = go.Figure( data=[go.Scatter3d(
x=points[:,0], y=points[:,1], z=points[:,2],
mode='markers' )])
fig.show()
最后,重复 的情况,但将第二个向量设为第一个的一半。

图 3-7. 练习 3-3
¹ 如第二章和第十六章所示,列表-整数乘法重复列表而不是对其进行标量乘法。
² 这个方程是线性加权组合的一个应用!
³ 这是科学中的一个普遍真理。
⁴ 因为它是一个线性相关的集合。
第四章:向量应用
在阅读前两章的过程中,你可能觉得其中一些内容过于深奥和抽象。也许你感到学习线性代数的挑战在理解数据科学和机器学习中的实际应用方面并没有多大帮助。
希望本章能消除你的这些疑虑。在本章中,你将学习向量及其操作如何在数据科学分析中使用。通过完成练习,你将能够扩展这些知识。
相关性和余弦相似度
相关性是统计学和机器学习中最基本和重要的分析方法之一。相关系数是一个单一数字,用于量化两个变量之间的线性关系。相关系数的范围从 −1 到 +1,其中 −1 表示完美的负相关,+1 表示完美的正相关,而 0 表示没有线性关系。图 4-1 展示了几对变量及其相关系数的示例。

图 4-1。展示了表现出正相关、负相关和零相关的数据示例。右下方的面板说明了相关性是线性测量;即使变量之间的相关性为零,它们之间也可能存在非线性关系。
在第二章中,我提到点积涉及到相关系数,并且点积的大小与数据中数值的大小相关(记得我们讨论过使用克与磅来衡量重量)。因此,相关系数需要一些标准化以保持在预期范围内的 −1 到 +1 之间。这两种标准化方法是:
对每个变量进行均值中心化
均值中心化意味着从每个数据值中减去平均值。
将点积除以向量范数的乘积
这种除法式标准化取消了测量单位并将最大可能的相关性幅度缩放为|1|。
方程 4-1 展示了皮尔逊相关系数的完整公式。
方程 4-1。皮尔逊相关系数的公式
或许不太明显的是,相关性其实就是三个点积。方程 4-2 展示了使用线性代数点积符号重写的相同公式。在这个方程中, 是的均值中心化版本(即应用了标准化方法#1 的变量)。
方程 4-2。用线性代数术语表示的皮尔逊相关性
所以,著名且广泛使用的 Pearson 相关系数简单地是两个变量之间的点积,由变量的大小归一化得到。(顺便说一句,您还可以从这个公式看出,如果变量被单位归一化,使得,则它们的相关性等于它们的点积。(回想一下,来自 Exercise 2-6,其中。)
相关性不是评估两个变量相似性的唯一方法。另一种方法称为余弦相似度。余弦相似度的公式只是点积的几何形式(Equation 2-11),解出余弦项:
其中是和的点积。
看起来相关性和余弦相似性的公式完全相同。但请记住,Equation 4-1 是完整的公式,而 Equation 4-2 是在变量已经被均值中心化的假设下的简化。因此,余弦相似度不涉及第一个归一化因子。
从本节可以理解为什么 Pearson 相关系数和余弦相似度反映了两个变量之间的线性关系:它们基于点积,而点积是线性操作。
本节有四个编程练习,位于章节末尾。您可以选择在阅读下一节之前解决这些练习,或者继续阅读整章然后再解决这些练习。(我个人推荐选择前者,但您是线性代数命运的主宰!)
时间序列过滤和特征检测
点积还用于时间序列过滤。过滤本质上是一种特征检测方法,其中模板——在过滤术语中称为核——与时间序列信号的部分匹配,过滤的结果是另一个时间序列,指示信号特征与核特征匹配程度。核被精心构造以优化特定标准,如平滑波动、锐利边缘、特定波形形状等。
过滤的机制是计算核与时间序列信号之间的点积。但通常过滤需要局部特征检测,而核通常比整个时间序列要短得多。因此,我们计算核与与核长度相同的数据的一个短片段之间的点积。这个过程产生过滤信号中的一个时间点(图 4-2),然后将核向右移动一个时间步来计算与不同(重叠)信号段的点积。正式地说,这个过程称为卷积,涉及几个额外的步骤,我在这里省略以便专注于在信号处理中应用点积。

图 4-2. 时间序列滤波示例
时间滤波是科学和工程中的一个重要主题。事实上,没有时间滤波就不会有音乐、无线电、电信、卫星等。而支撑你的音乐播放的数学核心是向量点乘。
在本章末尾的练习中,你将了解到如何利用点乘来检测特征(边缘)和平滑时间序列数据。
k-Means Clustering
k-means clustering 是一种无监督的方法,根据距离最小化原则,将多变量数据分成相对较少的几组或类别。
k-means clustering 是机器学习中重要的分析方法,有多种复杂的k-means clustering 变体。在这里,我们将实现一个简单版本的k-means,旨在了解向量(特别是:向量、向量范数和广播)在k-means 算法中的应用。
下面是我们将要编写的算法的简要描述:
-
将k个质心初始化为数据空间中的随机点。每个质心都是一个类或类别,接下来的步骤将把每个数据观测值分配到每个类别中。(质心是通用化到任意维度的中心。)
-
计算每个数据观测值与每个质心之间的欧氏距离。¹
-
将每个数据观测值分配给最近质心所在的组。
-
将每个质心更新为分配给该质心的所有数据观测值的平均值。
-
重复步骤 2–4,直到满足收敛标准或进行N次迭代。
如果你对 Python 编码感到舒适,并希望实现这个算法,那么我鼓励你在继续之前这样做。接下来,我们将详细讲解每个步骤的数学和代码,特别是在 NumPy 中使用向量和广播的概念。我们还将使用随机生成的二维数据来测试算法,以确认我们的代码是正确的。
让我们从第 1 步开始:初始化k个随机聚类中心。k是k-均值聚类的一个参数;在真实数据中,确定最佳k是困难的,但在这里我们将k固定为 3。有几种初始化随机聚类中心的方法;为了简单起见,我将随机选择k个数据样本作为聚类中心。数据包含在变量data中(此变量为 150×2,对应 150 个观测和 2 个特征),并且在图 4-3 的左上方面板中进行了可视化(在线代码显示如何生成这些数据):
k = 3
ridx = np.random.choice(range(len(data)),k,replace=False)
centroids = data[ridx,:] # data matrix is samples by features
现在到第 2 步:计算每个数据观测点与每个聚类中心之间的距离。在这里,我们使用了你在前几章学到的线性代数概念。
其中表示数据观测i到聚类中心j的距离,dx[i]是第*i*个数据观测的*x*特征,*c*x[j]是聚类中心j的x轴坐标。
你可能认为这一步需要使用双重for循环来实现:一个循环用于k个聚类中心,第二个循环用于N个数据观测(你甚至可能考虑第三个for循环用于数据特征)。然而,我们可以使用向量和广播来使这个操作更加简洁和高效。这是线性代数在公式和代码中看起来不同的一个例子:
dists = np.zeros((data.shape[0],k))
for ci in range(k):
dists[:,ci] = np.sum((data-centroids[ci,:])**2,axis=1)
让我们考虑这些变量的大小:data为 150×2(观测点乘特征),centroids[ci,:]为 1×2(聚类ci乘特征)。严格来说,不能从这两个向量中减去。然而,Python 将通过将聚类中心重复 150 次来实现广播,因此从每个数据观测中减去聚类中心。指数操作**是逐元素应用的,axis=1输入告诉 Python 在列上求和(每行分别)。因此,np.sum()的输出将是一个 150×1 的数组,编码每个点到聚类中心ci的欧几里德距离。
拿出一段时间来比较代码和距离公式。它们真的一样吗?事实上,它们并不一样:代码中缺少欧几里德距离中的平方根。所以代码错了吗?思考一下这个问题;我稍后会讨论答案。
第 3 步是将每个数据观测分配到最小距离的组中。这一步在 Python 中非常简洁,可以使用一个函数实现:
groupidx = np.argmin(dists,axis=1)
注意np.min和np.argmin之间的区别,前者返回最小值,而后者返回最小值出现的索引。
现在我们可以回到距离公式及其代码实现之间的不一致性。对于我们的k-means 算法,我们使用距离将每个数据点分配到其最近的质心。距离和平方距离是单调相关的,因此这两个度量给出相同的答案。增加平方根操作会增加代码复杂性和计算时间,而且对结果没有影响,因此可以简单地省略。
第 4 步是重新计算每个类中所有数据点的平均值作为质心。在这里,我们可以循环遍历k个聚类,并使用 Python 索引来找到分配给每个聚类的所有数据点:
for ki in range(k):
centroids[ki,:] = [ np.mean(data[groupidx==ki,0]),
np.mean(data[groupidx==ki,1]) ]
最后,第 5 步是将前面的步骤放入循环中,直到获得良好的解决方案。在生产级k-means 算法中,迭代将继续进行,直到达到停止条件,例如集群质心不再移动。为了简单起见,这里我们将迭代三次(选择这个任意数量是为了使绘图视觉上平衡)。
图 4-3 中的四个面板显示了初始随机聚类质心(迭代 0),以及每次迭代后它们的更新位置。
如果您学习聚类算法,您将学习到用于质心初始化和停止条件的复杂方法,以及选择适当k参数的定量方法。尽管如此,所有k-means 方法本质上都是上述算法的扩展,而线性代数是它们实现的核心。

图 4-3. k-means
代码练习
相关性练习
练习 4-1.
编写一个 Python 函数,该函数以两个向量作为输入并提供两个数字作为输出:皮尔逊相关系数和余弦相似度值。编写符合本章介绍的公式的代码;不要简单地调用np.corrcoef和spatial.distance.cosine。检查当变量已经均值中心化时,这两个输出值是否相同,当变量未均值中心化时是否不同。
练习 4-2.
让我们继续探讨相关性和余弦相似度之间的差异。创建一个包含整数 0 到 3 的变量,以及一个等于第一个变量加上某些偏移量的第二个变量。然后,您将创建一个模拟,在该模拟中系统地改变该偏移量在−50 到+50 之间(即,模拟的第一次迭代将使第二个变量等于[−50, −49, −48, −47])。在for循环中,计算两个变量之间的相关性和余弦相似度,并存储这些结果。然后制作一张线图,展示相关性和余弦相似度如何受平均偏移的影响。您应该能够重现图 4-4。

图 4-4. 练习 4-2 的结果
练习 4-3.
Python 中有几个函数可以计算皮尔逊相关系数。其中一个叫做pearsonr,位于 SciPy 库的stats模块中。打开这个文件的源代码(提示:??functionname),确保你理解 Python 实现是如何映射到本章介绍的公式中的。
练习 4-4.
为什么在 Python 中已经存在函数的情况下,你仍然需要编写自己的函数?部分原因是编写自己的函数具有巨大的教育价值,因为你会看到(在本例中)相关性是一个简单的计算,而不是某种只有计算机科学博士才能理解的复杂黑盒算法。但另一个原因是,内置函数有时会更慢,因为需要进行大量的输入检查、处理额外的输入选项、数据类型转换等。这增加了可用性,但以计算时间为代价。
在这个练习中,你的目标是确定你自己编写的简单相关函数是否比 NumPy 的corrcoef函数更快。修改练习 4-2 中的函数,只计算相关系数。然后,在一个包含 1000 次迭代的for循环中,生成两个包含 500 个随机数的变量,并计算它们之间的相关性。计时这个for循环。然后重复,但使用np.corrcoef。在我的测试中,自定义函数比np.corrcoef快约 33%。在这些玩具示例中,差异以毫秒计,但如果你在大数据集上运行数十亿个相关性计算,这些毫秒的累积非常显著!(请注意,编写没有输入检查的自定义函数存在输入错误的风险,而这些错误在np.corrcoef中会被捕获。)(还要注意,对于较大的向量,速度优势会消失。试一试!)
过滤和特征检测练习
练习 4-5.
让我们构建一个边缘检测器。边缘检测器的核非常简单:[−1 +1]。该核与时间序列信号片段的点积在信号具有恒定值(例如,[10 10])时为 0。但当信号有陡峭变化时(例如,[1 10]会产生点积为 9),该点积就会很大。我们将使用的信号是一个平台函数。图 A 和 B 在图 4-5 中展示了核和信号。这个练习的第一步是编写代码创建这两个时间序列。

图 4-5. 练习 4-5 的结果
接下来,在信号的时间点上编写一个for循环。在每个时间点,计算核与与核相同长度的时间序列数据段之间的点积。你应该生成一个看起来像图 C 在图 4-5 中的图的绘图结果。(更关注结果而不是美观度。)注意,我们的边缘检测器在信号平坦时返回 0,在信号跳变时返回 +1,在信号跳降时返回 −1。
随意继续探索这段代码。例如,如果用零([0 −1 1 0])填充核心会发生什么变化?如果将核心翻转为[1 −1]会怎样?如果核心是不对称的([−1 2])会发生什么变化?
练习 4-6。
现在我们将重复相同的过程,但使用不同的信号和核函数。目标是平滑一个崎岖不平的时间序列。时间序列将是从高斯分布(也称为正态分布)生成的 100 个随机数。核函数将是一个钟形函数,近似于高斯函数,其定义为数字[0, .1, .3, .8, 1, .8, .3, .1, 0],但缩放使得核的总和为 1。你的核应该与图 4-6 中的 A 图相匹配,尽管由于随机数的原因,你的信号看起来不会完全像 B 图。
复制并调整上一个练习中的代码,计算通过高斯核过滤的滑动时间序列的点积。警告:要注意for循环中的索引。图 4-6 中的 C 图显示了一个示例结果。你可以看到,经过滤波的信号是原始信号的平滑版本。这也被称为低通滤波。

图 4-6。练习 4-6 的结果
练习 4-7。
将核心中的 1 替换为-1 并对核心进行平均中心化。然后重新运行过滤和绘图代码。结果是什么?实际上,这会突显出尖锐的特征!事实上,这个核现在是一个高通滤波器,意味着它减弱了平滑(低频)特征并突出显示快速变化(高频)特征。
k-均值聚类练习
练习 4-8。
确定一个最优k的一种方法是多次重复聚类(每次使用随机初始化的聚类中心)并评估最终的聚类是否相同或不同。不生成新数据的情况下,使用k = 3 多次重新运行k-均值代码,看看最终的聚类分配是否通常看起来相似(这是基于视觉检查的定性评估)。即使聚类中心是随机选择的,最终的聚类分配是否通常看起来相似?
练习 4-9。
使用k = 2 和k = 4 重复多次聚类。你觉得这些结果如何?
¹ 提示:欧氏距离是从数据观测到质心的平方距离之和的平方根。
第五章:矩阵,第一部分
矩阵是向量的升级版。矩阵是非常多才多艺的数学对象。它们可以存储一组方程、几何变换、粒子随时间的位置、财务记录以及其他无数的东西。在数据科学中,矩阵有时被称为数据表,其中行对应观测(例如客户),列对应特征(例如购买)。
这章和接下来的两章将把你的线性代数知识提升到一个新的水平。来杯咖啡,戴上思考帽子。本章结束时你的大脑会更强大。
在 NumPy 中创建和可视化矩阵
根据上下文,矩阵可以被理解为一组列向量相邻堆叠在一起(例如,观测值-特征数据表),或者一组行向量叠加在一起(例如,多传感器数据,每行是来自不同通道的时间序列),或者作为有序的独立矩阵元素集合(例如,图像中每个矩阵元素编码像素强度值)。
可视化、索引和切片矩阵
小矩阵可以简单地完整打印出来,例如以下示例:
但这不是可扩展的,实际工作中的矩阵可能很大,可能包含数十亿个元素。因此,较大的矩阵可以被视为图像。矩阵的每个元素的数值映射到图像中的颜色。在大多数情况下,这些映射是伪彩色的,因为数值到颜色的映射是任意的。Figure 5-1 展示了使用 Python 库matplotlib将矩阵可视化为图像的示例。
![矩阵作为图像的示例
图 5-1。三个矩阵,被视为图像
矩阵使用粗体大写字母表示,例如矩阵或。矩阵的大小使用(行数,列数)约定表示。例如,以下矩阵是 3 × 5,因为它有三行五列:
你可以通过索引行和列位置来引用矩阵的特定元素:矩阵 中第 3 行第 4 列的元素表示为(在前述示例矩阵中,)。重要提示: 数学中使用基于 1 的索引,而 Python 使用基于 0 的索引。因此,在 Python 中索引元素为A[2,3]。
从矩阵中提取子集的行或列是通过切片来完成的。如果你不熟悉 Python,可以参考 Chapter 16 中关于切片列表和 NumPy 数组的介绍。要从矩阵中提取一个部分,你需要指定起始和结束的行和列,切片步长为 1。在线代码会引导你完成这个过程,以下代码展示了从较大矩阵的第 2 到第 4 行和第 1 到第 5 列提取子矩阵的示例:
A = np.arange(60).reshape(6,10)
sub = A[1:4:1,0:5:1]
这是完整和子矩阵的全貌:
Original matrix:
[[ 0 1 2 3 4 5 6 7 8 9]
[10 11 12 13 14 15 16 17 18 19]
[20 21 22 23 24 25 26 27 28 29]
[30 31 32 33 34 35 36 37 38 39]
[40 41 42 43 44 45 46 47 48 49]
[50 51 52 53 54 55 56 57 58 59]]
Submatrix:
[[10 11 12 13 14]
[20 21 22 23 24]
[30 31 32 33 34]]
特殊矩阵
矩阵有无数种,因为有无数种方法可以将数字组织成矩阵。但可以使用相对较少的特征来描述矩阵,这些特征形成了矩阵的“家族”或类别。了解这些类别很重要,因为它们在特定操作中出现或具有某些有用的属性。
有些矩阵类别被如此频繁地使用,以至于它们有专门的 NumPy 函数来创建它们。以下是一些常见特殊矩阵的列表和用 Python 代码创建它们的方法;¹ 你可以在 Figure 5-2 中看到它们的外观:
随机数矩阵
这是一个包含从某个分布(通常是高斯,又称正态)随机抽取的数字的矩阵。随机数矩阵非常适合用代码探索线性代数,因为可以快速且轻松地创建任何大小和秩的矩阵(矩阵秩是一个你将在 Chapter 16 中学习的概念)。
NumPy 中创建随机矩阵有几种方式,具体取决于你希望从哪个分布中抽取数值。在本书中,我们主要使用高斯分布的数值:
Mrows = 4 # shape 0
Ncols = 6 # shape 1
A = np.random.randn(Mrows,Ncols)
方阵与非方阵
方阵的行数与列数相同;换句话说,该矩阵在 中。非方阵,有时也称为矩形矩阵,其行数和列数不同。通过在先前代码中调整形状参数,你可以从随机数创建方阵和矩形矩阵。
如果矩阵的行数多于列数,则称其为高矩阵,如果列数多于行数,则称其为宽矩阵。
对角线
矩阵的对角线是从左上角到右下角的元素。对角矩阵在所有非对角线元素上都有零;对角线上的元素可能包含零,但这些元素是唯一可能包含非零值的元素。
NumPy 函数np.diag()根据输入的不同具有两种行为:输入一个矩阵,np.diag会将对角线元素作为向量返回;输入一个向量,np.diag会返回以该向量元素为对角线元素的矩阵。(注意:提取矩阵的对角线元素并不称为“对角化矩阵”;这是在第十三章中介绍的另一种操作。)
三角形
三角矩阵包含主对角线上方或下方的全部零元素。如果非零元素在对角线上方,则称为上三角矩阵;如果在对角线下方,则称为下三角矩阵。
NumPy 有专门的函数来提取矩阵的上三角(np.triu())或下三角(np.tril())。
单位矩阵
单位矩阵是最重要的特殊矩阵之一。它相当于数字 1,任何矩阵或向量乘以单位矩阵都是同样的矩阵或向量。单位矩阵是一个具有所有对角线元素值为 1 的方阵对角线矩阵。它用字母表示。你可能会看到一个下标来表示其大小(例如, 是单位矩阵);如果没有,你可以从上下文中推断大小(例如,为了使方程一致)。
在 Python 中,你可以使用np.eye()创建单位矩阵。
零矩阵
零矩阵类似于零向量:所有元素都是零的矩阵。像零向量一样,它用粗体零符号表示:。在数学和科学符号中,使用相同符号指代向量和矩阵可能有些混淆,但这种重载在数学和科学符号中很常见。
使用np.zeros()函数创建零矩阵。

图 5-2. 一些特殊矩阵。数字和灰度值表示每个元素的矩阵值。
矩阵数学:加法,标量乘法,哈达玛乘法
矩阵的数学运算分为两类:直观和不直观。通常,直观操作可以表达为逐元素的过程,而不直观的操作则需要更长的解释和一些实践才能理解。让我们从直观操作开始。
加法和减法
两个矩阵相加是通过相加它们对应元素实现的。这里是一个例子:
正如你从例子中所猜测的那样,矩阵加法仅在大小相同的两个矩阵之间定义。
“平移”矩阵
与向量一样,正式而言不能像 这样将标量添加到矩阵中。Python 允许这样的操作(例如 3+np.eye(2)),这涉及将标量广播添加到矩阵的每个元素中。这是一个方便的计算,但并不是正式的线性代数操作。
但是有一种线性代数的方法可以向方阵添加一个标量,那就是移位矩阵。它通过向对角线添加一个常数值来实现,这由添加一个标量乘以单位矩阵来实现:
这是一个数值例子:
在 Python 中进行移位是直接的:
A = np.array([ [4,5,1],[0,1,11],[4,9,7] ])
s = 6
A + s # NOT shifting!
A + s*np.eye(len(A)) # shifting
注意只有对角元素会改变;矩阵的其余部分不会受到移位的影响。在实践中,为了尽可能保留矩阵中的信息,同时受益于移位的效果,包括增加矩阵的数值稳定性(您将在本书后面学到为什么会发生这种情况)。
究竟需要移动多少是机器学习、统计学、深度学习、控制工程等多个领域正在进行研究的问题。例如,通过 进行移动是多还是少? 呢?显然,这些数值相对于矩阵中的数值来说是“大”还是“小”。因此,在实践中, 通常被设置为矩阵定义的某个量,如范数或特征值的平均数的一部分。您将在后面的章节中进一步探讨这个问题。
“移位”矩阵有两个主要(非常重要!)的应用:它是找到矩阵特征值的机制,也是在将模型拟合到数据时正则化矩阵的机制。
标量和 Hadamard 乘法
这两种乘法对矩阵和向量的工作方式相同,也就是说,它们是逐元素的。
标量-矩阵乘法意味着将矩阵中的每个元素乘以相同的标量。以下是一个使用字母而不是数字的矩阵的示例:
同样,Hadamard 乘法涉及对两个矩阵进行逐元素乘法(因此也称为逐元素乘法)。这里有一个例子:
在 NumPy 中,Hadamard 乘法可以使用 np.multiply() 函数实现。但通常更容易使用两个矩阵之间的星号 A*B 来实现。这可能会引起一些混淆,因为标准的矩阵乘法(下一节)使用 @ 符号来表示。这是一个微妙但重要的区别!(这对于从 MATLAB 转到 Python 的读者尤其令人困惑,因为在 MATLAB 中,* 表示矩阵乘法。)
A = np.random.randn(3,4)
B = np.random.randn(3,4)
A*B # Hadamard multiplication
np.multiply(A,B) # also Hadamard
A@B # NOT Hadamard!
Hadamard 乘法在线性代数中确实有一些应用,例如在计算矩阵逆时。但是,它最常用于应用程序中,作为存储许多个体乘法的便捷方式。这与向量 Hadamard 乘法的常见用法相似,正如在第二章中讨论的那样。
标准矩阵乘法
现在我们来到了矩阵乘法的不直观方式。明确地说,标准矩阵乘法并不特别困难;它只是与你可能期望的不同。与其按元素操作,标准矩阵乘法是按行/列进行操作。事实上,标准矩阵乘法可以简化为一个矩阵的行与另一个矩阵的列之间的逐点乘积的系统收集。(这种形式的乘法正式称为矩阵乘法;我添加了术语标准以帮助消除与 Hadamard 和标量乘法的歧义。)
但在我详细讨论如何将两个矩阵相乘之前,我将首先解释如何确定两个矩阵是否可以相乘。正如你将了解到的,只有当两个矩阵的大小协调时,它们才能相乘。
矩阵乘法有效性规则
你知道矩阵大小是以 —行乘列来写出的。两个相乘的矩阵可以有不同的大小,因此让我们将第二个矩阵的大小称为 。当我们将两个乘数矩阵写出并在其下方写出它们的大小时,我们可以引用“内”维度 和“外”维度 和 。
这里的重要一点是:矩阵乘法仅在“内”维度匹配且乘积矩阵的大小由“外”维度定义时有效。参见图 5-3。
![矩阵乘法有效性的可视化
图 5-3. 矩阵乘法有效性的可视化。记住这张图片。
更正式地说,矩阵乘法在左矩阵的列数等于右矩阵的行数时有效,乘积矩阵的大小由左矩阵的行数和右矩阵的列数定义。我觉得“内/外”准则更容易记住。
您已经可以看到矩阵乘法不遵守交换律: 可能有效,而 则无效。即使两种乘法都有效(例如,如果两个矩阵都是方阵),它们可能产生不同的结果。也就是说,如果 和 ,那么一般情况下 (它们在某些特殊情况下相等,但我们不能一般性地假设它们相等)。
请注意标记:Hadamard 乘法使用点圈示( ),而矩阵乘法则表示为两个矩阵并排放置,中间没有任何符号( )。
现在是学习矩阵乘法的机制和解释的时候了。
矩阵乘法
矩阵乘法仅在左矩阵的列数与右矩阵的行数相匹配时才有效,这是因为乘积矩阵中的(i,j)元素是左矩阵第i行与右矩阵第j列的点积。
方程式 5-1 展示了矩阵乘法的示例,使用了与 Hadamard 乘法相同的两个矩阵。确保理解乘积矩阵中每个元素如何计算为左侧矩阵的相应行和右侧矩阵的列的点积。
方程式 5-1. 矩阵乘法的示例。添加括号以方便视觉分组。
如果您难以记住矩阵乘法的工作原理,图 5-4 显示了一种用手指画出乘法的助记技巧。
![矩阵乘法的手指示意图
图 5-4. 矩阵乘法的手指动作
您如何解释矩阵乘法?请记住,点积是编码两个向量之间关系的数字。因此,矩阵乘法的结果是一个矩阵,存储了左矩阵行与右矩阵列之间的所有成对线性关系。这是一件美妙的事情,并且是计算协方差和相关矩阵、一般线性模型(用于包括 ANOVA 和回归在内的统计分析)以及无数其他应用的基础。
矩阵-向量乘法
从纯机械角度来看,矩阵向量乘法并不特别,不值得专门开辟小节来讨论:矩阵和向量的乘法就是一个“矩阵”是向量的矩阵乘法。
但是矩阵向量乘法在数据科学、机器学习和计算机图形学中有许多应用,因此值得花一些时间讨论。让我们从基础开始:
-
一个矩阵可以右乘一个列向量,但不能右乘一个行向量,也可以左乘一个行向量,但不能左乘一个列向量。换句话说,和是有效的,但和是无效的。
从检查矩阵尺寸中可以看出:一个矩阵可以通过一个矩阵(也称为行向量)左乘,或通过一个矩阵(也称为列向量)右乘。
-
矩阵向量乘法的结果总是一个向量,该向量的方向取决于乘法的向量:通过行向量左乘一个矩阵产生另一个行向量,而通过列向量右乘一个矩阵产生另一个列向量。再次强调,当你考虑矩阵尺寸时这是显而易见的,但值得指出的。
矩阵-向量乘法有几个应用。在统计学中,通过将设计矩阵乘以回归系数来获得模型预测的数据值,写为 。在主成分分析中,确定了一组“特征重要性”权重向量,用于最大化数据集 的方差,并写为 (该特征重要性向量 被称为特征向量)。在多变量信号处理中,通过将空间滤波器应用于多通道时间序列数据 ,获得了一个降维的组件,并写为 。在几何学和计算机图形学中,可以使用数学变换矩阵来变换一组图像坐标,并写为 ,其中 是变换矩阵, 是几何坐标集。
还有很多关于如何在应用线性代数中使用矩阵-向量乘法的例子,你将在本书的后面看到其中的几个。矩阵-向量乘法也是矩阵空间的基础,这是你将在下一章中学习的重要主题。
现在,我想集中讨论矩阵-向量乘法的两种具体解释:作为实现向量的线性加权组合的方法,以及作为实现几何变换的机制。
线性加权组合
在前一章中,我们通过分别使用标量和向量进行线性加权组合的计算,然后逐个进行乘法。但是现在你比前一章开始时聪明多了,所以你现在准备学习一种更好、更紧凑和更可扩展的计算线性加权组合的方法:将单独的向量放入矩阵中,并将权重放入向量的相应元素中。然后相乘。以下是一个数值示例:
请花点时间通过乘法来理解如何将两个向量的线性加权组合实现为矩阵-向量乘法。关键的洞察力在于向量中的每个元素标量乘以相应的列矩阵,然后加权列向量求和以得到乘积。
这个例子涉及到列向量的线性加权组合;如果要计算行向量的线性加权组合,你会改变什么?^([2)
几何变换
当我们将向量看作几何线时,矩阵向量乘法成为旋转和缩放该向量的一种方法(记住,标量向量乘法可以缩放但不能旋转)。
让我们从 2D 情况开始,方便可视化。这里是我们的矩阵和向量:
M = np.array([ [2,3],[2,1] ])
x = np.array([ [1,1.5] ]).T
Mx = M@x
注意,我创建了 x 作为行向量,然后将其转置为列向量;这减少了需要输入的方括号数量。
图 A 在 图 5-5 中展示了这两个向量。你可以看到矩阵 同时旋转和拉伸了原始向量。让我们尝试用同一矩阵来计算另一个向量。事实上,只是为了好玩,让我们使用同样的向量元素,但位置互换(因此,向量 v = [1.5,1])。
现在,在图 B 中出现了一个奇怪的现象(图 5-5):矩阵向量积不再将向量旋转到另一个方向。矩阵仍然缩放了向量,但其方向被保留了。换句话说,矩阵-向量乘法就像标量-向量乘法一样。这并不是偶然事件:事实上,向量 v 是矩阵 M 的特征向量,而 M 拉伸 v 的程度是其特征值。这是一个非常重要的现象,值得单独讨论(第十三章),但我现在就忍不住向你介绍这个概念。

图 5-5. 矩阵向量乘法示例
顺便提一下高级主题,这些演示的主要观点是,矩阵向量乘法的一个功能是让矩阵承载一个变换,当应用到一个向量上时,可以旋转和拉伸该向量。
矩阵操作:转置
你在 第二章 中学习了向量的转置操作。对于矩阵来说,原理是一样的:交换行和列。就像向量一样,转置用上标 表示(因此, 是矩阵 的转置)。而且对一个矩阵进行两次转置会得到原始矩阵( )。
转置操作的正式数学定义打印在 方程式 5-2 中(基本上是从前一章重复的),但我认为记住转置会交换行和列也同样简单。
方程式 5-2. 转置操作的定义
这里有一个例子:
在 Python 中,有几种方式可以对矩阵进行转置,使用函数和作用于 NumPy 数组的方法:
A = np.array([ [3,4,5],[1,2,3] ])
A_T1 = A.T # as method
A_T2 = np.transpose(A) # as function
此示例中的矩阵使用 2D NumPy 数组;如果将作为 1D 数组编码的向量应用转置方法,你认为会发生什么?试一试并找出答案!³
点积和外积符号
现在你已经了解了转置操作以及矩阵乘法有效性规则,我们可以回顾一下向量点积的符号。对于两列向量 ,转置第一个向量而不转置第二个向量会得到尺寸为 和 的两个“矩阵”。“内部”尺寸匹配,而“外部”尺寸告诉我们乘积将是 ,也就是一个标量。这就是为什么点积被表示为 的原因。
外积同理:将列向量乘以行向量尺寸为 和 。“内部”尺寸匹配,结果的尺寸将是 。
矩阵运算:LIVE EVIL(运算顺序)
LIVE EVIL 是一个回文(回文是指前后读都一样的单词或短语),也是一个可爱的记忆法则,用于记忆转置如何影响乘积矩阵的顺序。基本上,规则是乘积矩阵的转置等同于单独转置并相乘的矩阵,但顺序相反。在 Equation 5-3 中,,,,和 都是矩阵,你可以假设它们的尺寸匹配以使乘法有效。
方程式 5-3. LIVE EVIL 规则示例
不用说,这条规则适用于任意数量的矩阵乘法,不仅仅是四个,并且不仅仅是这些“随机选择”的字母。
这似乎是个奇怪的规则,但这是使转置乘积矩阵起作用的唯一方法。你可以在本章末尾的 Exercise 5-7 中亲自测试这一点。如果愿意,你可以先跳到该练习再继续阅读。
对称矩阵
对称矩阵具有许多特殊属性,使它们很适合处理。它们通常数值稳定,因此对计算机算法很方便。在您阅读本书的过程中,您将了解对称矩阵的特殊属性;在此,我将重点介绍对称矩阵的定义及如何从非对称矩阵创建它们。
矩阵对称意味着什么?这意味着相应的行和列是相等的。这意味着当您交换行和列时,矩阵不变。这又意味着对称矩阵等于其转置。在数学术语中,矩阵是对称的,如果。
查看方程 5-4 中的对称矩阵。
方程 5-4. 一个对称矩阵;注意每行等于其对应的列
一个非方阵可以对称吗?不行!原因在于如果一个矩阵的大小是,那么它的转置的大小是。除非,否则这两个矩阵不可能相等,这意味着矩阵是方阵。
从非对称矩阵创建对称矩阵
最初这可能令人惊讶,但是乘以任何矩阵——甚至是非方阵和非对称矩阵——它的转置将产生一个方对称矩阵。换句话说,是方对称的,以及。 (如果您缺乏时间、耐心或键盘技能来格式化上标 ^T,您可以写成 AtA 和 AAt 或 A'A 和 AA'。)
在看到一个例子之前,让我们严格证明这个断言。一方面,我们实际上不需要单独证明是方 且 对称,因为后者意味着前者。但证明方形是线性代数证明中的一个简单且良好的练习(通常比微积分证明更短且更容易)。
通过考虑矩阵的尺寸即可得到证明:如果 是 ,那么 是 ,这意味着乘积矩阵的大小为 。您可以按照相同的逻辑处理 。
现在来证明对称性。回想一下,对称矩阵的定义是等于其转置。因此让我们转置 ,进行一些代数运算,看看会发生什么。确保您能够跟进每一个步骤;证明依赖于“LIVE EVIL”规则:
取首末项,得到 。矩阵等于其转置,因此它是对称的。
现在独自进行 的证明。剧透警告!您将得出相同的结论。但是书写证明将有助于您内化这个概念。
因此 和 都是方阵对称的。但它们不是同一个矩阵!实际上,如果 是非方阵,则这两个矩阵乘积甚至不是相同大小。
被称为乘法方法来创建对称矩阵。还有加法方法,当矩阵是方阵但非对称时有效。这种方法有一些有趣的性质,但实际应用价值不高,所以我不会重点介绍它。练习 5-9 将带你逐步学习算法;如果你想挑战自己,可以在看练习之前尝试自己发现该算法。
摘要
本章是关于矩阵的三章系列中的第一章。在这里,你学会了所有矩阵操作的基础。总结:
-
矩阵就是数字的电子表格。在不同的应用中,将矩阵概念化为一组列向量、一组行向量或单个值的排列是有用的。不过,将矩阵视为图像通常是具洞察力的,或者至少看上去很愉快。
-
有几种特殊矩阵的分类。熟悉矩阵类型的性质将有助于理解矩阵方程和高级应用。
-
一些算术运算是逐元素完成的,比如加法、标量乘法和 Hadamard 乘法。
-
“平移”矩阵意味着向对角线元素添加常数(而不更改非对角线元素)。平移在机器学习中具有几种应用,主要用于查找特征值和正则化统计模型。
-
矩阵乘法涉及左矩阵的行与右矩阵的列的点积。乘积矩阵是行-列对之间的映射的有序集合。记住矩阵乘法有效性规则: 。
-
LIVE EVIL:⁴ 相乘矩阵的转置等于各自矩阵的转置并且它们的顺序颠倒后相乘。
-
对称矩阵是沿对角线镜像的矩阵,这意味着每行等于其对应的列,并定义为。对称矩阵有许多有趣和有用的性质,使其在应用中非常适用。
-
你可以通过将矩阵乘以其转置来从任何矩阵创建对称矩阵。结果矩阵 对于统计模型和奇异值分解非常重要。
代码练习
练习 5-1。
这个练习将帮助你熟悉矩阵元素的索引。使用 np.arange(12).reshape(3,4) 创建一个矩阵。然后编写 Python 代码提取第二行第四列的元素。使用软编码,以便可以选择不同的行/列索引。打印出如下消息:
矩阵索引 (2,4) 处的元素为 7。
练习 5-2。
这个和接下来的练习专注于通过切片矩阵获取子矩阵。首先创建 图 5-6 中的矩阵 ,并使用 Python 切片提取由前五行和五列组成的子矩阵。我们称这个矩阵为 。尝试重现 图 5-6,但如果你在 Python 可视化编码方面遇到困难,那么只需专注于正确提取子矩阵即可。

图 5-7。Exercise 5-3 的可视化
练习 5-4。
使用两个for循环逐元素实现矩阵的逐元素加法。当尝试将大小不匹配的两个矩阵相加时会发生什么?这个练习将帮助你思考如何将矩阵分解为行、列和单独的元素。
练习 5-5。
矩阵加法和标量乘法遵守交换和分配的数学法则。这意味着以下方程给出相同的结果(假设矩阵 和 的大小相同,而 是某个标量):
与其通过数学方式证明这一点,不如通过编码来演示。在 Python 中,创建两个大小为 的随机数字矩阵和一个随机标量。然后实现前述方程式中的三个表达式。你需要找出一种方法来确认这三个结果是否相等。请记住,忽略在 范围内的微小计算机精度误差。
练习 5-6。
使用for循环编写矩阵乘法代码。将结果与使用 numpy 的@操作符进行确认。这个练习将帮助你巩固对矩阵乘法的理解,但在实践中,最好使用@而不是编写双重for循环。
练习 5-7。
确认使用以下五个步骤的 LIVE EVIL 规则:(1)创建四个随机数矩阵,设置大小分别为 , , 和 。(2)将四个矩阵相乘并转置乘积。(3)分别转置每个矩阵并将它们相乘,不反转它们的顺序。(4)分别转置每个矩阵并将它们相乘,按照 LIVE EVIL 规则反转它们的顺序。检查步骤 2 的结果是否与步骤 3 和步骤 4 的结果相匹配。(5)重复前面的步骤,但使用所有方阵。
练习 5-8。
在这个练习中,你将编写一个 Python 函数来检查一个矩阵是否对称。它应该接受一个矩阵作为输入,并且如果矩阵是对称的,则输出布尔值True,如果矩阵是非对称的,则输出布尔值False。请记住,由于计算机小的舍入/精度误差可能使“相等”的矩阵看起来不相等。因此,你需要测试一些合理的容差来判断是否相等。在对称和非对称矩阵上测试该函数。
练习 5-9。
我提到了从非对称方阵创建对称矩阵的一种加法方法。这种方法非常简单:用矩阵与其转置的平均值。在 Python 中实现这个算法,并确认结果确实是对称的。(提示:你可以使用前一个练习中编写的函数!)
练习 5-10.
重复第二部分的 Exercise 3-3 ( 中的两个向量),但使用矩阵-向量乘法代替向量-标量乘法。也就是说,计算 而不是 。
练习 5-11.
对角矩阵具有许多有趣的特性,使它们在工作中非常有用。在这个练习中,你将学习其中两个性质:
-
通过对角矩阵的预乘,可以将右矩阵的行按照对应的对角元素进行缩放。
-
后乘一个对角矩阵可以将左矩阵的列按照对应的对角元素进行缩放。
这个事实被用在多个应用中,包括计算相关矩阵(第 7 章)和矩阵对角化(第 13 和 14 章)。
让我们探讨这个属性的一个推论。首先创建三个 矩阵:一个全为 1 的矩阵(提示:np.ones());一个对角元素为 1, 4, 9 和 16 的对角矩阵;以及一个与前一个对角矩阵的平方根相等的对角矩阵。
接下来,打印出由第一个对角矩阵预乘和后乘的单位矩阵。你将得到以下结果:
# Pre-multiply by diagonal:
[[ 1. 1. 1. 1.]
[ 4. 4. 4. 4.]
[ 9. 9. 9. 9.]
[16. 16. 16. 16.]]
# Post-multiply by diagonal:
[[ 1. 4. 9. 16.]
[ 1. 4. 9. 16.]
[ 1. 4. 9. 16.]
[ 1. 4. 9. 16.]]
最后,通过对角矩阵的平方根进行预乘和后乘单位矩阵。你将得到以下结果:
# Pre- and post-multiply by sqrt-diagonal:
[[ 1. 2. 3. 4.]
[ 2. 4. 6. 8.]
[ 3. 6. 9. 12.]
[ 4. 8. 12. 16.]]
注意,行和列都被缩放,以便矩阵中的(i,j)元素乘以第 i 和 j 对角元素的乘积。(实际上,我们创建了一个乘法表!)
练习 5-12.
另一个有趣的事实是:对于两个对角矩阵,矩阵乘法与哈达玛德乘积是相同的。使用纸和铅笔来理解为什么,然后用 Python 代码来说明。
¹ 书中还会介绍更多特殊矩阵,但这个列表足以帮助入门。
² 将系数放入行向量中,并进行预乘。
³ 什么也不做。NumPy 将返回相同的 1D 数组,不会对其进行修改,也不会给出警告或错误。
⁴ LIVE EVIL 是一个可爱的记忆法,而不是社会行为建议!
第六章:矩阵,第二部分
矩阵乘法是数学家赋予我们的最美妙的礼物之一。但要从基础线性代数进阶到理解并开发数据科学算法,您需要做的不仅仅是进行矩阵乘法。
我们从讨论矩阵范数和矩阵空间开始这一章。矩阵范数本质上是向量范数的延伸,而矩阵空间本质上是向量子空间的延伸(而向量子空间本质上仅仅是线性加权组合)。因此,您已经具备了本章所需的背景知识。
线性无关性、秩和行列式等概念将使您能够从理解转置和乘法等基本概念过渡到理解像逆、特征值和奇异值等高级主题。这些高级主题为数据科学中线性代数的应用开启了新的篇章。因此,本章是您从线性代数新手向线性代数专家转变的一个关键过程。¹
矩阵看起来像是简单的东西——只是一堆数字的电子表格。但是在前几章中您已经看到矩阵远不止表面那么简单。所以,请深呼吸,放松心情,然后投入其中。
矩阵范数
您在第二章(Chapter 2)学习了关于向量范数的知识:向量的范数是其欧几里得几何长度,计算方法是向量各元素平方和的平方根。
矩阵范数稍微复杂一些。首先,不存在“the 矩阵范数”;可以从矩阵计算多个不同的范数。矩阵范数与向量范数有些类似,每个范数提供一个数字来表征一个矩阵,使用双竖线表示,如矩阵 的范数表示为 。
不同的矩阵范数具有不同的含义。矩阵范数的多样性可以大致分为两类:逐元素(有时也称为入口方式)和诱导方式。逐元素范数基于矩阵的各个元素计算,因此这些范数可以用来反映矩阵中各元素的大小。
诱导范数可以以下述方式解释:矩阵的一个功能是编码向量的变换;矩阵的诱导范数是衡量该变换对向量进行缩放(拉伸或收缩)的程度的指标。这种解释在第七章(Chapter 7)学习使用矩阵进行几何变换时会更加明了,在第十四章(Chapter 14)学习奇异值分解时也是如此。
在本章中,我将向您介绍逐元素范数。我将从欧几里德范数开始,它实际上是向量范数对矩阵的直接扩展。欧几里德范数也称为Frobenius 范数,计算方法是所有矩阵元素平方和的平方根(公式 6-1)。
公式 6-1. Frobenius 范数
指标i和j对应于M行和N列。还请注意带下标的[F]表示 Frobenius 范数。
Frobenius 范数也被称为范数(是一个看起来很漂亮的字母)。而范数的名称源自逐元素p-范数的一般公式(当p=2 时,得到 Frobenius 范数)。
矩阵范数在机器学习和统计分析中有多种应用。其中一个重要应用是正则化,旨在改善模型拟合并增强模型对未见数据的泛化能力(您将在本书后面看到这方面的例子)。正则化的基本思想是将矩阵范数作为最小化算法的代价函数之一。这种范数有助于防止模型参数过大(正则化,也称为岭回归)或鼓励稀疏解(正则化,也称为Lasso 回归)。事实上,现代深度学习架构依赖于矩阵范数来在解决计算机视觉问题时取得如此卓越的性能。
Frobenius 范数的另一个应用是计算“矩阵距离”的度量。矩阵与自身的距离为 0,而两个不同矩阵之间的距离随着这些矩阵中的数值越来越不相似而增加。Frobenius 矩阵距离的计算方法很简单,只需在公式 6-1 中将矩阵替换为矩阵。
这个距离可以作为机器学习算法中的优化准则,例如,在最小化减少图像数据存储大小的同时,尽量减少减少矩阵和原始矩阵之间的 Frobenius 距离。练习 6-2 将引导您完成一个简单的最小化示例。
矩阵迹和 Frobenius 范数
矩阵的迹 是其对角元素的和,表示为 ,仅适用于方阵。以下两个矩阵的迹相同(14):
迹有一些有趣的属性。例如,矩阵的迹等于其特征值的和,因此是其特征空间“体积”的度量。迹的许多属性对数据科学应用不那么重要,但有一个有趣的例外:
换句话说,Frobenius 范数可以计算为矩阵与其转置的迹的平方根。这种方法有效的原因在于矩阵 的每个对角元素由每行与自身的点积定义。
[Exercise 6-3 将帮助您探索计算 Frobenius 范数的迹方法。
矩阵空间(列、行、零空间)
矩阵空间 的概念是抽象和应用线性代数中许多主题的核心。幸运的是,矩阵空间在概念上很直观,本质上只是矩阵不同特征的线性加权组合。
列空间
记住,向量的线性加权组合涉及标量乘法和一组向量的求和。对这个概念的两种修改将线性加权组合扩展到矩阵的列空间。首先,我们将矩阵概念化为一组列向量。其次,我们考虑无穷多的实数标量而不是使用特定的标量集合。无限数量的标量给出了组合一组向量的无限方式。由此产生的无限向量集称为矩阵的列空间。
让我们通过一些具体的数值示例来具体化这一点。我们从简单的开始——一个只有一列的矩阵(实际上与列向量相同)。它的列空间——所有可能的该列的线性加权组合——可以表示为:
表示矩阵 的列空间,符号 意味着“是”的成员或“包含于”。在这个上下文中,它意味着 可以是任意可能的实数值。
这个数学表达式是什么意思?它意味着列空间是列向量[1 3]的所有可能比例版本的集合。让我们考虑几个具体的案例。向量[1 3]是否在矩阵的列空间中?是的,因为你可以将该向量表达为矩阵乘以 。那么[−2 −6]呢?也是,因为你可以将该向量表达为矩阵乘以 。那么[1 4]呢?答案是否定的:向量[1 4] 不在该矩阵的列空间中,因为没有任何标量可以使该矩阵乘以后得到该向量。
列空间是什么样的?对于一个只有一列的矩阵,列空间是通过原点、沿着列向量方向延伸至无穷远的一条直线。(严格来说,这条直线并不延伸至字面上的无穷远,因为无穷不是一个实数。但这条直线是任意长的——远远超出我们有限的人类思维可以理解的范围——因此在所有目的上,我们可以说这条直线是无限长的。)图 6-1 展示了这个矩阵的列空间的图片。

图 6-1. 单列矩阵的列空间可视化。这个列空间是一个一维子空间。
现在让我们考虑一个具有更多列的矩阵。我们将保持列的维度为两个,这样我们可以在二维图上进行可视化。这是我们的矩阵及其列空间:
我们有两列,所以我们允许两个不同的 s(它们都是实数,但可以彼此不同)。现在的问题是,所有可以通过这两个列向量的线性组合达到的向量的集合是什么?
答案是:所有在 中的向量。例如,向量[−4 3]可以通过分别将这两列乘以 11 和−15 来获得。我是如何得出这些标量值的?我使用了最小二乘投影方法,你将在第十一章中学习到。现在,你可以专注于这两列可以适当加权以达到 中任何点的概念。
图 A 在 图 6-2 中展示了这两个矩阵列。我没有画出矩阵的列空间,因为它是整个轴的空间。

图 6-2. 更多列空间的示例
在 中的另一个例子。这是我们要考虑的新矩阵:
其列空间的维数是多少?通过两列的某种线性加权组合是否可能到达 中的任意点?
对于第二个问题的答案是否定的。如果你不相信我,请试着找出产生向量 [3 5] 的两列的线性加权组合。这是不可能的。事实上,这两列是共线的(图 B 在 图 6-2 中),因为其中一列已经是另一列的缩放版本。这意味着这个 矩阵的列空间仍然只是一条线——一个一维子空间。
这里的核心信息是,在一个矩阵中有 N 列并不保证列空间将是 N 维的。列空间的维数仅等于列数,当且仅当这些列形成一个线性无关的集合时。(从第三章记得,线性无关意味着一个向量集合中的向量不能表示为该集合中其他向量的线性加权组合。)
最后一个列空间的例子,看看当我们进入三维空间时会发生什么。这是我们的矩阵及其列空间:
现在,在 中有两列。这两列是线性无关的,意味着你无法将其中一列表示为另一列的缩放版本。因此,这个矩阵的列空间是二维的,但它是嵌入在 中的一个平面(图 6-3)。
这个矩阵的列空间是一个无限的二维平面,但是这个平面仅仅是三维空间的一个无限小切片。你可以将其想象成一张无限薄的纸片,横跨整个宇宙。
![三维空间中的列空间
图 6-3. 嵌入在三维空间中的矩阵的二维列空间。两条粗线代表矩阵的两列。
在那个平面上有许多向量(即可以通过两个列向量的线性组合获得的向量),但是还有许多 更多 的向量不在这个平面上。换句话说,矩阵的列空间中有向量,也有不在矩阵列空间中的向量。
如何确定向量是否在矩阵的列空间中?这绝非一个微不足道的问题——事实上,这个问题是线性最小二乘法的基础,言语无法充分表达最小二乘法在应用数学和工程中的重要性。那么如何确定一个向量是否在列空间中呢?到目前为止的示例中,我们只是通过猜测、算术和可视化进行了一些操作。这种方法的目的在于培养直觉,但显然这些方法在更高维度和更复杂问题中是不可扩展的。
确定向量是否在矩阵的列空间内的定量方法依赖于矩阵秩的概念,这是你将在本章后面学习的内容。在那之前,专注于矩阵的列切出一个向量子空间的直觉,该子空间可能包括整个M维空间或较小维度的子空间;一个重要的问题是是否有其他向量在该子空间内(意味着该向量可以表示为矩阵列的线性加权组合)。
行空间
一旦你理解了矩阵的列空间,矩阵的行空间就非常容易理解了。事实上,矩阵的行空间与此概念完全相同,但我们考虑的是行的所有可能加权组合,而不是列。
行空间被表示为。由于转置操作交换了行和列,你也可以写成矩阵的行空间是其转置的列空间,换句话说,。矩阵行空间和列空间之间有一些差异;例如,行空间(但不是列空间)对行约化操作是不变的。但这超出了本章的范围。
因为行空间等于矩阵的转置的列空间,对于对称矩阵来说,这两个矩阵空间是相同的。
空间零子句
空间零子句与列空间微妙但重要的区别在于。列空间可以简洁地总结为以下方程:
这可以翻译成英文为“我们是否可以找到一些系数集在中,使得在的列的加权组合产生向量?”如果答案是肯定的,那么,并且向量告诉我们如何加权的列以获得。
相反,空间零子句可以简洁地总结为以下方程:
这可以翻译成英文为“我们能找到一些系数,使得中列的加权组合产生零向量吗?”
一瞬间的观察会揭示一个适用于任何可能的矩阵的答案:设置!显然,将所有列乘以 0 将得到零向量的和。但这是一个微不足道的解,我们排除它。因此,问题变成了“我们能找到一组权重——并非所有都是 0——来产生零向量吗?”任何能满足这个方程的向量都在的零空间中,我们将其写为。
让我们从一个简单的例子开始。在阅读以下文本之前,看看你能否找到这样一个向量:
你找到一个向量了吗?我的是[7.34, 7.34]。我敢打赌,你没有找到同样的向量。你可能找到了[1, 1]或者[-1, -1]。也许是[2, 2]?
我想你已经看出这个问题的方向了——对于特定矩阵,存在无限多个能满足的向量。所有这些向量都可以表达为这些选择的某种比例版本。因此,这个矩阵的零空间可以表示为:
这里有另一个示例矩阵。再次尝试找到一组系数,使得列的加权和产生零向量(也就是找到在中):
我敢打一个更大的赌注,你找不到这样的向量。但并不是因为我不相信你能找到(我对我的读者评价很高!);而是因为这个矩阵没有零空间。正式地说,我们说这个矩阵的零空间是空集:。
回顾本小节中的两个示例矩阵。你会注意到第一个矩阵包含可以形成其他列的缩放版本的列,而第二个矩阵包含形成独立集的列。这不是巧合:矩阵的列的线性无关性与零空间的维度有密切关系。这种关系的确切性质由秩-零度定理给出,你将在下一章学习。但关键点在于:当矩阵的列形成线性无关集时,零空间为空。
面对冗余,我将重申这一重要观点:满秩和列满秩矩阵具有空的零空间,而降秩矩阵具有非空(非平凡)的零空间。
Python SciPy 库包含一个计算矩阵零空间的函数。让我们使用代码确认我们的结果:
A = np.array([ [1,-1],[-2,2] ])
B = np.array([ [1,-1],[-2,3] ])
print( scipy.linalg.null_space(A) )
print( scipy.linalg.null_space(B) )
下面是输出:
[[0.70710678]
[0.70710678]]
[]
第二个输出([])是空集。Python 为什么选择0.70710678作为矩阵A的零空间的数值?如果 Python 选择1会不会更容易阅读?考虑到可能的无限向量,Python 返回了一个单位向量(你可以心算该向量的范数,知道)。单位向量便于处理,并具有数值稳定性等多种优良性质。因此,计算算法通常会返回单位向量作为子空间的基向量。在特征向量和奇异向量中,你将再次看到这一点。
零空间是什么样子?图 6-4 展示了矩阵A的行向量和零空间。
为什么我绘制了行向量而不是列向量?原来行空间与零空间正交。这不是因为某种奇怪的外行原因;相反,它是根据零空间的定义写成 的。为矩阵的每一行( )重写该方程得到表达式 ;换句话说,每行与零空间向量的点积为 0。

图 6-4. 矩阵零空间的可视化
为什么要如此关注零空间?关心能够使矩阵乘以零向量的向量似乎有些奇怪。但零空间是找到特征向量和奇异向量的基石,你将在第十三章学习到这一点。
本节的最后一点思考:每个矩阵都有四个相关子空间;你已经了解了其中三个(列、行、零空间)。第四个子空间称为右零空间,是行的零空间。它通常写为矩阵转置的零空间: 。传统的数学课程现在将花费数周探索这四个子空间的复杂性和关系。矩阵子空间因其迷人的美和完美性而值得研究,但我们不会深入到那个层次。
秩
秩 是与矩阵相关的一个数字。它与矩阵子空间的维度有关,并且对矩阵操作有重要影响,包括求逆矩阵和确定方程组解的数量。与本书中的其他主题一样,矩阵秩有丰富而详细的理论,但在这里我将侧重于数据科学及其相关应用中需要了解的内容。
我将首先列出秩的几个性质。没有特定的重要顺序:
-
秩是非负整数,因此矩阵的秩可以是 0、1、2、…,但不可以是−2 或 3.14。
-
每个矩阵有一个唯一的秩;一个矩阵不能同时具有多个不同的秩。(这也意味着秩是矩阵的特征,不是行或列的特征。)
-
矩阵的秩可以用 或 表示。也适用的是“ 是一个秩-r 矩阵。”
-
矩阵的最大可能秩是其行数或列数中较小的那个。换句话说,最大可能秩是 min{M,N}。
-
其最大可能秩的矩阵称为“满秩”。秩 r < min{M,N} 的矩阵被称为“降秩的”,“秩亏”,或“奇异的”。
-
标量乘法不影响矩阵的秩(0 除外,它将矩阵转换为秩为 0 的零矩阵)。
矩阵秩有几种等价的解释和定义,其中包括:
-
构成线性无关集合的最大列(或行)数。
-
列空间的维度(与行空间的维度相同)。
-
矩阵中包含信息的维度数。这与矩阵中的总列数或行数不同,因为可能存在线性依赖关系。
-
矩阵的非零奇异值的数量。
或许令人惊讶的是,排名的定义在列和行上是相同的:即使对于非方阵,列空间和行空间的维数确实是相同的吗?确实如此。有多种证明方法,其中许多方法要么非常复杂,要么依赖于奇异值分解,因此我不会在本章中包含正式的证明。² 但我会展示一个非方阵的行和列空间的例子作为说明。
这是我们的矩阵:
矩阵的列空间在中,而行空间在中,因此这两个空间需要在不同的图表中绘制(图 6-5)。这三列不构成线性独立集(任意一列可以描述为其他两列的线性组合),但它们确实张成了整个。因此,矩阵的列空间是 2 维的。这两行确实构成了线性独立集,它们张成的子空间是中的一个 2 维平面。
明确一下:矩阵的列空间和行空间是不同的。但是这些矩阵空间的维数是相同的。而这个维数就是矩阵的排名。因此,这个矩阵的排名是 2。
![矩阵的行和列空间。
图 6-5。列空间和行空间有不同的张成但相同的维数
以下是一些矩阵。尽管我还没有教您如何计算排名,但请试着根据前面的描述猜测每个矩阵的排名。答案在脚注中。³
希望您已经设法弄清楚了排名,或者至少不会对脚注中的答案感到震惊。
毋庸置疑,视觉检查和直觉并不是实际计算排名的可扩展方法。有几种计算排名的方法。例如,在[第十章中,您将学习如何通过将矩阵行减少至梯形形式并计算主元的数量来计算排名。像 Python 这样的计算机程序通过计算矩阵的非零奇异值的数量来计算排名。您将在第十四章中了解更多相关信息。
现在,我希望您专注于这样一个观念:排名对应于可以形成线性独立集合的最大列数,这也对应于矩阵列空间的维数。(在前述句子中,您可以将“列”替换为“行”,其仍然是准确的。)
特殊矩阵的排名
一些特殊的矩阵有易于计算的或值得学习的排名:
向量
所有向量的秩都为 1。这是因为向量——定义上——只包含一列(或行)信息;它们所张成的子空间是一维的。唯一的例外是零向量。
零矩阵
任何大小的零矩阵(包括零向量)的秩为 0。
单位矩阵
单位矩阵的秩等于行数(也等于列数)。换句话说, 。实际上,单位矩阵只是对角矩阵的一个特例。
对角矩阵
对角矩阵的秩等于非零对角元素的数量。这是因为每行最多包含一个非零元素,通过零的加权组合不可能创建非零数。在解方程组和解释奇异值分解时,这个属性变得非常有用。
三角矩阵
三角矩阵仅在所有对角元素中有非零值时具有满秩。具有至少一个对角零的三角矩阵将是降秩的(确切的秩取决于矩阵中的数值)。
随机矩阵
随机矩阵的秩是不可能事先知道的,因为它取决于从中提取矩阵元素的数字分布及每个数字提取的概率。例如,一个由 0 或 1 填充的矩阵如果每个元素都等于 0,则其秩可能为 0。或者,例如如果选择了单位矩阵,则其秩可能为 2。
但是有一种方法可以创建具有保证最大可能秩的随机矩阵。这是通过随机抽取浮点数,例如从高斯或均匀分布中进行。64 位计算机可以表示个数字。从该集合中抽取几十个或几百个放入矩阵中意味着矩阵列中的线性依赖性的可能性极其低。实际上,这种可能性如此之低,以至于可以驱动Heart of Gold上的无穷不可能驱动器。⁴
关键在于通过例如np.random.randn()创建的矩阵将具有可能的最大秩。这对使用 Python 学习线性代数非常有用,因为可以创建具有任意秩的矩阵(受前述约束的限制)。Exercise 6-5 将指导您完成这个过程。
秩为 1 的矩阵
一个秩-1 矩阵有——你猜对了——秩为 1。这意味着矩阵中实际上只有一列的信息(或者说只有一行的信息),而其他所有列(或行)都只是线性倍数。在本章的早些时候,你看到了一些秩-1 矩阵的例子。以下是一些更多的例子:
秩-1 矩阵可以是方阵、长方阵或宽方阵;无论尺寸如何,每列都是第一列的缩放副本(或每行是第一行的缩放副本)。
如何创建一个秩-1 矩阵?实际上,你在[第二章已经学过(尽管我只是简要提到了它;你原谅你忘记了)。答案是通过两个非零向量的外积。例如,上面的第三个矩阵是向量[4 2 3]和[3 1 1 3 1]的外积。
秩-1 矩阵在特征分解和奇异值分解中很重要。在本书的后面章节和你在应用线性代数中的冒险中,你会遇到许多秩-1 矩阵。
添加和乘积矩阵的秩
如果你知道矩阵和的秩,你是否自动知道或的秩?
答案是否定的,你不需要知道。但是,两个单独矩阵的秩提供了可能的最大秩的上限。以下是规则:
我不建议记忆这些规则。但我建议记忆以下内容:
-
你无法仅凭知道各个矩阵的秩来知道求和或乘积矩阵的确切秩(有少数例外,例如零矩阵);相反,各个矩阵为求和或乘积矩阵的秩提供了上限。
-
一个求和矩阵的秩可能大于各个矩阵的秩。
-
乘积矩阵的秩不可能大于相乘矩阵中的最大秩⁵
在 练习 6-6 中,你将有机会使用不同秩的随机矩阵来说明这两条规则。
移位矩阵的秩
简而言之:移位矩阵具有全秩。事实上,移位方形矩阵的主要目标之一是将其秩从 r < M 增加到 r = M。
一个明显的例子是将零矩阵移位为单位矩阵。所得和的秩 是一个全秩矩阵。
下面是另一个稍微不那么明显的例子:
最左边的矩阵的秩是 2;注意第三列等于第二列减去第一列。但是求和矩阵的秩是 3:第三列不再能由第一列和第二列的线性组合产生。然而,矩阵中的信息几乎没有改变;事实上,原始矩阵和移位后矩阵中元素的皮尔逊相关系数为 = 0.999997222233796。这具有重要的含义:例如,秩为 2 的矩阵无法求逆,而移位后的矩阵可以。(在 第八章 你将了解原因。)
理论与实践
在理解奇异值之前理解奇异值
无法以纯单调的方式学习数学,这意味着你在完全理解概念 a 之前就完全学习了概念 b 等等。完全理解矩阵秩需要了解奇异值分解(SVD),但在了解秩之前理解 SVD 是没有意义的。这有点像一个进退两难的局面。这也是数学学习的一部分令人沮丧之处。好消息是,在你阅读后,本书的页面仍然存在,所以如果以下讨论不完全合理,学习了 SVD 后再回来看也不迟。
简而言之:每个 矩阵具有一组编码矩阵列空间和行空间中不同方向“重要性”或“广阔性”的最小{M,N}个非负奇异值。奇异值为 0 的方向位于其中一个零空间中。
在抽象线性代数中,秩是一个非常坚实的概念。每个矩阵都恰好有一个秩,就是故事的结局。
然而,在实际操作中,计算矩阵秩涉及一定的不确定性。可以说,计算机甚至不是在计算秩;它们估算到了一个合理的精度程度。我之前写道,秩可以被计算为非零奇异值的数量。但 Python 并不是这么做的。以下是从 np.linalg.matrix_rank() 中摘录的两行关键内容(我省略了一些参数以便专注于主要内容):
S = svd(M)
return count_nonzero(S > tol)
M 是所讨论的矩阵,S 是奇异值的向量,tol 是一个容差阈值。这意味着 NumPy 实际上并不是在计算非零奇异值;它在计算大于某个阈值的奇异值。确切的阈值取决于矩阵中的数值,但通常比矩阵元素小约 10 到 12 个数量级。
这意味着 NumPy 对哪些数字足够小以被视为“有效零”做出了决策。我当然不是在批评 NumPy —— 这是正确的做法!(其他数值处理程序如 MATLAB 和 Julia 也以同样的方式计算秩。)
但为什么要这样做?为什么不简单地计算非零奇异值的数量?答案是容差可以吸收由于计算机舍入误差而引起的小数值不准确性。容差还允许忽略可能污染数据采集传感器的微小噪声。这个思想被用于数据清理、压缩和降维。图 6-6 说明了这个概念。

图 6-8. 练习 6-4 的解答
练习 6-5.
现在我将向你展示如何创建具有任意秩的随机矩阵(受矩阵大小等约束)。要创建一个秩为 r 的 矩阵,将一个随机的 矩阵与一个 矩阵相乘。在 Python 中实现这一点,并确认秩确实是 r。如果设置 r > min{M,N} 会发生什么,为什么会这样?
练习 6-6.
通过创建三对秩为 1 的矩阵来演示矩阵秩的加法规则(r(A + B) ≤ r(A) + r(B)),这些矩阵的和具有 (1) 秩 0、(2) 秩 1 和 (3) 秩 2。然后,使用矩阵乘法而不是加法重复此练习。
练习 6-7.
将来自 练习 6-5 的代码放入一个 Python 函数中,该函数以 M 和 r 作为输入参数,并提供一个随机的 秩为 r 的矩阵作为输出。在双重 for 循环中,创建一对对 矩阵,其各自的秩从 2 变化到 15. 将这些矩阵相加并相乘,将这些结果矩阵的秩存储起来。这些秩可以组织成一个矩阵,并按照各自矩阵的秩的函数进行可视化(图 6-9)。

图 6-9. 练习 6-7 的结果
练习 6-8.
有趣的是,矩阵 ,, 和 的秩都相同。编写代码来演示这一点,使用各种大小、形状(方形、高、宽)和秩的随机矩阵。
练习 6-9.
这项练习的目标是回答问题 ?创建一个秩为 3 的矩阵 和向量 ,使用从正态分布随机抽取的数字。按照早期描述的算法确定向量是否在矩阵的列空间中。多次重新运行代码以查看是否找到一致的模式。接下来,使用一个秩为 4 的 矩阵。我愿意打赌一百万比特币⁸,你 总是 发现当 是一个 随机矩阵时(假设没有编码错误),。是什么让我对你的答案充满信心?⁹
对于额外的挑战,将这段代码放入一个函数中,根据测试结果返回 True 或 False,并在向量的大小与矩阵增广的尺寸不匹配时引发异常(即有用的错误消息)。
练习 6-10.
记住,降秩矩阵的行列式在理论上是零。在这个练习中,你将测试这一理论。实施以下步骤:(1)创建一个方形随机矩阵。 (2)降低矩阵的秩。之前你已经通过乘以矩形矩阵来实现这一点;在这里,将一个列设置为另一个列的倍数。 (3)计算行列式并存储其绝对值。
在双重for循环中运行这三个步骤:一个循环遍历矩阵大小从到,另一个循环重复这三步一百次(在模拟噪声数据时重复实验是有用的)。最后,将平均过的行列式绘制成线图,作为矩阵元素数量的函数。线性代数理论预测,所有降秩矩阵的行列式都为零,无论矩阵大小如何。图 6-10 显示相反,反映了准确计算行列式的计算困难。我对数据进行了对数变换以增加可见性;你应该使用对数缩放和线性缩放检查图表。

图 6-10. 练习 6-10 的结果
¹ 互联网声称这是一个真正的词;让我们看看我能否让它通过 O’Reilly 编辑的审查。
² 如果你已经熟悉奇异值分解(SVD),那么简短的版本是,奇异值分解(SVD)的和交换了行和列空间,但非零奇异值的数量保持不变。
³ r(A) = 1, r(B) = 1, r(C) = 2, r(D) = 3, r(E) = 1, r(F) = 0
⁴ 无限不可能驱动器是一种技术,允许金心号飞船在太空中穿越不可能的距离。如果你不知道我在说什么,那么你还没有阅读道格拉斯·亚当斯的《银河系漫游指南》。如果你还没有读过那本书,那么你真的错过了 20 世纪最伟大、最发人深省和最有趣的智力成就之一。
⁵ 这个规则解释了为什么外积总是产生一个秩为 1 的矩阵。
⁶ 任何[1 −1]和[1 1]的缩放版本。
⁷ 所有这些练习都很有趣,但有些比其他的更有趣。
⁸ n.b. 我没有那么多比特币,¯_(ツ)_/¯。
⁹ 因为全秩矩阵的列空间跨越了 的全部,因此所有中的向量必然位于列空间中。
第七章:矩阵应用
我希望现在,在过去两个理论密集的章节之后,你感觉就像刚刚完成了一次健身房的剧烈锻炼:疲惫但充满活力。这一章应该感觉就像在乡村小路上骑自行车:有时费力但提供了新鲜且令人振奋的视角。
本章的应用松散地基于第四章的内容。我这样做是为了在向量和矩阵的章节中保持一些共同的主题线索。因为我希望你看到,尽管随着线性代数的深入,概念和应用变得更加复杂,但其基础仍建立在诸如线性加权组合和点积等简单原理上。
多元数据协方差矩阵
在第四章中,你学会了如何计算 Pearson 相关系数,即两个数据变量之间的向量点积,除以向量范数的乘积。那个公式是用于两个变量(例如身高和体重)的;如果你有多个变量(例如身高、体重、年龄、每周运动……)呢?
你可以想象在所有变量上写一个双重for循环,并将双变量相关性公式应用于所有变量对。但那样既笨重又不优雅,与线性代数精神相违背。本节的目的是向你展示如何从多变量数据集中计算协方差和相关性矩阵。
让我们从协方差开始。协方差简单来说是相关方程的分子部分,换句话说,是两个均值中心化变量的点积。协方差的解释与相关性相同(变量同向移动时为正,反向移动时为负,无线性关系时为零),但协方差保留了数据的尺度,因此不受±1 的限制。
协方差还有一个(n-1)的归一化因子,其中n是数据点的数量。这种归一化防止了随着数据值的累加协方差变得越来越大(类似于通过N来将总和转换为平均值)。以下是协方差的方程式:
正如第四章中所述,如果我们称
实现此多变量公式的关键见解是矩阵乘法是左矩阵行和右矩阵列的点积的组织集合。
因此,我们做的是:创建一个矩阵,其中每一列对应一个变量(变量是数据特征)。让我们称这个矩阵为。现在,乘积是不合理的(甚至可能无效,因为数据矩阵往往是高的,因此M > N)。但是如果我们转置第一个矩阵,那么的行对应于的列。因此,矩阵乘积编码了所有成对协方差(假设列是均值居中的,并且在除以n - 1 时)。换句话说,协方差矩阵中的(i,j)元素是数据特征i和j的点积。
协方差矩阵的矩阵方程非常优雅且紧凑:
矩阵是对称的。这来自于第五章中证明的任何矩阵乘以其转置都是方阵对称的,但从统计学上讲也是有道理的:协方差和相关性是对称的,意味着例如身高和体重之间的相关性与体重和身高之间的相关性相同。
对于的对角元素是什么?这些元素包含每个变量与自身的协方差,在统计学中称为方差,用来量化围绕均值的离散程度(方差是标准差的平方)。
在在线代码示例中,使用公开可用的犯罪统计数据集创建了一个协方差矩阵。该数据集包括有关美国各社区社会、经济、教育和住房信息的一百多个特征¹。该数据集的目标是利用这些特征来预测犯罪水平,但在这里我们将用它来检查协方差和相关矩阵。
导入并进行一些轻微的数据处理后(在在线代码中有解释),我们有一个名为dataMat的数据矩阵。以下代码显示如何计算协方差矩阵:
datamean = np.mean(dataMat,axis=0) # vector of feature means
dataMatM = dataMat - datamean # mean-center using broadcasting
covMat = dataMatM.T @ dataMatM # data matrix times its transpose
covMat /= (dataMatM.shape[0]-1) # divide by N-1
图 7-1 显示了协方差矩阵的图像。首先:看起来整洁,不是吗?我在“日常工作”中是一名神经科学教授,经常研究多变量数据集,并且仰慕协方差矩阵从未让我失望过。
在这个矩阵中,浅色表示正相关的变量(例如,离婚男性的百分比与贫困人口数),深色表示负相关的变量(例如,离婚男性的百分比与中位收入),灰色表示彼此无关的变量。
正如你在第四章中学到的,从协方差计算相关性仅涉及向量的范数缩放。这可以转化为一个矩阵方程,让你能够计算数据相关矩阵而无需使用for循环。练习 7-1 和 练习 7-2 将引导你完成这一过程。正如我在第四章中所写,我鼓励你在继续下一节之前先完成这些练习。

图 7-1. 数据协方差矩阵
最后注意:NumPy 有用于计算协方差和相关矩阵的函数(分别是np.cov()和np.corrcoef())。在实践中,使用这些函数比自己编写代码更方便。但是——正如本书始终如一地强调的——我希望你理解这些函数实现的数学和机制。因此,在这些练习中,你应该将协方差实现为直接的公式翻译,而不是调用 NumPy 函数。
几何变换通过矩阵-向量乘法实现
我在第五章中提到,矩阵-向量乘法的一个目的是对一组坐标应用几何变换。在本节中,你将在二维静态图像和动画中看到这一点。顺便说一句,你将了解纯旋转矩阵以及如何在 Python 中创建数据动画。
“纯旋转矩阵”旋转一个向量同时保持其长度不变。你可以将其类比为模拟时钟的指针:随着时间的推移,指针旋转但长度不变。一个二维旋转矩阵可以表示为:
纯旋转矩阵是正交矩阵的一个示例。我将在下一章更多地讨论正交矩阵,但我想指出的列是正交的(它们的点积是 cos(θ)sin(θ) − sin(θ)cos(θ))并且是单位向量(回忆三角恒等式 cos²(θ) + sin²(θ) = 1)。
要使用此变换矩阵,将设置为某个顺时针旋转的角度,然后将矩阵乘以一个几何点矩阵,该矩阵中每一列包含N个数据点的(X,Y)坐标。例如,设置不会改变点的位置(这是因为意味着);设置会使点围绕原点逆时针旋转。
举个简单的例子,考虑一组垂直排列的点集以及通过乘以这些坐标的效果。在图 7-2 中,我设置了。
![纯旋转
图 7-2. 围绕原点通过纯旋转矩阵旋转点
在继续本节之前,请检查生成此图的在线代码。确保您理解我上面写的数学如何转化为代码,并花些时间尝试不同的旋转角度。您也可以尝试弄清楚如何使旋转逆时针而不是顺时针;答案在脚注中。²
让我们通过使用“不纯”的旋转(即拉伸和旋转,而不仅仅是旋转),并通过动画变换来使我们对旋转的调查更加精彩。特别是,我们将在电影的每一帧平滑调整变换矩阵。
在 Python 中创建动画有几种方法;我将在这里使用的方法涉及定义一个 Python 函数,在每个电影帧上创建图形内容,然后调用matplotlib例程在每次迭代中运行该函数。
我称这部电影为摇摆的圆。圆由一组和点定义,向量角从 0 到。
我设置了以下的变换矩阵:
我为什么选择这些具体值,您如何解释变换矩阵?一般来说,对角元素缩放x-轴和y-轴坐标,而非对角元素则拉伸两个轴。上述矩阵中的确切值是通过尝试不同的数字直到我找到看起来不错的结果而选择的。稍后,在练习中,您将有机会探索更改变换矩阵的效果。
在电影的过程中, 的值将平稳过渡从 1 到 0,然后再到 1,遵循公式
数据动画的 Python 代码可以分为三个部分。第一部分是设置图形:
theta = np.linspace(0,2*np.pi,100)
points = np.vstack((np.sin(theta),np.cos(theta)))
fig,ax = plt.subplots(1,figsize=(12,6))
plth, = ax.plot(np.cos(x),np.sin(x),'ko')
ax.plot 的输出是变量 plth,它是一个句柄或指针,指向绘图对象。这个句柄允许我们更新点的位置,而不是在每一帧上从头开始重绘图形。
第二部分是定义在每一帧更新坐标轴的函数:
def aframe(ph):
# create and apply the transform matrix
T = np.array([ [1,1-ph],[0,1] ])
P = T@points
# update the dots' location
plth.set_xdata(P[0,:])
plth.set_ydata(P[1,:])
return plth
最后,我们定义我们的变换参数 matplotlib 函数:
phi = np.linspace(-1,1-1/40,40)**2
animation.FuncAnimation(fig, aframe, phi,
interval=100, repeat=True)
图 7-3 展示了电影的一个帧,您可以通过运行代码观看整个视频。诚然,这部电影不太可能赢得任何奖项,但它确实展示了在动画中如何使用矩阵乘法。CGI 电影和视频游戏中的图形稍微复杂一些,因为它们使用称为四元数的数学对象,这些四元数是在

图 7-3. 电影《摇晃的圆》的一个帧
在处理本节的练习之前,我鼓励您花些时间尝试本节的代码。特别是,通过将对角元素之一设置为 0.5 或 2,更改左下角的非对角元素而不是(或者除此之外还)右上角的非对角元素,将其中一个对角元素参数化而不是非对角元素等。并且这里有个问题:您能想出如何让圆向左而不是向右摇摆吗?答案在脚注中。³
图像特征检测
在本节中,我将介绍图像滤波,这是图像特征检测的一种机制。图像滤波实际上是时间序列滤波的一个延伸,所以在这里通过第四章会对你有利。回想一下,在时间序列信号中进行特征过滤或检测时,你设计一个核,然后创建核与信号重叠段的点乘时间序列。
图像滤波的工作方式与 1D 相同,只是在 2D 中。我们设计一个 2D 核,然后创建一个新图像,其中包含核与图像重叠窗口之间的“点乘”。
我在引号中写了“点乘”,因为这里的操作与向量点乘并不完全相同。计算方式相同——逐元素相乘和求和——但是操作发生在两个矩阵之间,因此实现是 Hadamard 乘积并对所有矩阵元素求和。图 A 在图 7-4 中说明了该过程。还有卷积的额外细节——例如,填充图像以使结果大小相同——你可以在图像处理书籍中了解。在这里,我希望你专注于线性代数方面,特别是点积量化了两个向量(或矩阵)之间的关系,这可以用于滤波和特征检测。

图 7-4. 图像卷积机制
在进行分析之前,我将简要解释如何创建一个 2D 高斯核。2D 高斯由以下方程给出:
关于该方程的一些说明:exp代表自然指数(常数e = 2.71828…),当指数项较长时,使用 exp(x)而不是e^x。X和Y是 2D 坐标网格,用于评估函数。最后,
这里是如何将该公式转化为代码:
Y,X = np.meshgrid(np.linspace(-3,3,21),np.linspace(-3,3,21))
kernel = np.exp( -(X**2+Y**2) / 20 )
kernel = kernel / np.sum(kernel) # normalize
X和Y网格从-3 到+3,步长为 21 步。宽度参数硬编码为 20。第三行对核中的值进行归一化,使整个核的总和为 1。这保持了数据的原始比例。当正确归一化时,每一步卷积——因此,过滤后图像中的每个像素——都成为周围像素的加权平均值,权重由高斯定义。
回到手头的任务:我们将平滑一个随机数矩阵,类似于我们在第四章中平滑随机数时间序列的方式。你可以在图 7-4 中看到随机数矩阵、高斯核以及卷积结果。
以下 Python 代码展示了如何实现图像卷积。再次回想一下第四章中的时间序列卷积,你会发现其思想相同,只是多了一个维度,需要额外的for循环:
for rowi in range(halfKr,imgN-halfKr): # loop over rows
for coli in range(halfKr,imgN-halfKr): # loop over cols
# cut out a piece of the image
pieceOfImg = imagePad[ rowi-halfKr:rowi+halfKr+1:1,
coli-halfKr:coli+halfKr+1:1 ]
# dot product: Hadamard multiply and sum
dotprod = np.sum( pieceOfImg*kernel )
# store the result for this pixel
convoutput[rowi,coli] = dotprod
将卷积实现为双重for循环实际上计算效率低下。事实证明,在频域中可以更快速地实现卷积,并且代码更少。这要归功于卷积定理,该定理指出时间(或空间)域中的卷积等于频率域中的乘法。对卷积定理的全面阐述超出了本书的范围;我在此提到它是为了建议你使用 SciPy 的convolve2d函数而不是双重for循环实现,尤其是对于大图像。
让我们尝试平滑一张真实的图片。我们将使用阿姆斯特丹斯特德利克博物馆的图片,我爱称它为“外太空的浴缸”。这张图片是一个 3D 矩阵,因为它具有行、列和深度——深度包含了来自红、绿和蓝色通道的像素强度值。这张图片存储为一个矩阵在
目前,我们将通过转换为灰度来将其减少为 2D 矩阵。这简化了计算,尽管并非必要。在练习 7-5 中,您将了解如何平滑 3D 图像。图 7-5 展示了应用高斯平滑核前后的灰度图像。

图 7-5. 浴缸博物馆的图片,在进行了适当平滑前后
这两个示例都使用了高斯核。还有多少其他核可用?无限多个!在练习 7-7 中,您将测试另外两个用于识别垂直和水平线的核。这些特征检测器在图像处理中很常见(并且被用于您大脑中用于检测光模式中边缘的神经元)。
图像卷积核在计算机视觉中是一个重要的主题。事实上,卷积神经网络(专为计算机视觉优化的深度学习架构)的惊人性能完全归因于网络通过学习来制作最佳的滤波器核。
摘要
让我简单解释一下:重要且复杂的数据科学和机器学习方法又一次是建立在简单的线性代数原理上。
代码练习
协方差和相关矩阵练习
Exercise 7-1.
在这个练习中,你将把协方差矩阵转换为相关矩阵。该过程涉及将每个矩阵元素(即每对变量之间的协方差)除以这两个变量的方差的乘积。
这是通过将协方差矩阵分别预乘和后乘一个包含每个变量标准差的对角矩阵(标准差是方差的平方根)来实现的。标准差被倒置,因为我们需要除以方差,尽管我们将乘以矩阵。预乘和后乘标准差的原因是预乘和后乘对角矩阵的特殊性质,这在 Exercise 5-11 中已经解释过了。
Equation 7-1 显示了公式。
Equation 7-1. 从协方差到相关的公式
在这个练习中,你的目标是通过将 Equation 7-1 翻译成 Python 代码,从协方差矩阵计算相关矩阵。然后,你可以重现 Figure 7-6。

Figure 7-6. Exercise 7-1 的解答
Exercise 7-2.
NumPy 有一个函数np.corrcoef(),给定输入数据矩阵,返回一个相关矩阵。使用这个函数重现你在上一个练习中创建的相关矩阵。在类似 Figure 7-7 的图中展示这两个矩阵及它们的差异,以确认它们是相同的。

Figure 7-7. Exercise 7-2 的解答。注意颜色比例的差异。
接下来,通过评估??np.corrcoef()来检查np.corrcoef()的源代码。NumPy 使用了一种稍微不同的实现,通过标准偏差的广播除法而不是预乘和后乘一个倒置标准偏差的对角矩阵,但你应该能够理解它们的代码实现如何与你在前一个练习中编写的数学和 Python 代码相匹配。
几何变换练习
Exercise 7-3.
本练习的目标是展示应用变换前后的圆内点,类似于我在图 7-2 中展示线段前后旋转的方式。使用以下变换矩阵,然后创建一个类似图 7-8 的图形:
图 7-8. 练习 7-3 的解答
练习 7-4。
现在看另一个电影。我称其为螺旋的 DNA。图 7-9 展示了电影的一个镜头。步骤与摇摆的圆形相同——设置一个图表,创建一个将变换矩阵应用于坐标矩阵的 Python 函数,并告诉matplotlib使用该函数创建动画。使用以下变换矩阵:
convolve2d 函数的输出数据类型为 float64(您可以通过输入 variableName.dtype 自行查看)。然而,plt.imshow 将会警告有关裁剪数值的问题,图片无法正常渲染。因此,您需要将卷积结果转换为 uint8。##### 练习 7-6。不需要为每个颜色通道使用相同的核。根据图 7-10 中显示的值,为每个通道更改高斯的宽度参数。对图像的影响微妙,但不同颜色的模糊使其具有一种稍微立体的外观,就像您没有戴眼镜看红蓝立体照片一样。
图 7-10. 练习 7-6 中每个颜色通道使用的核
练习 7-7。
从技术上讲,图像平滑是特征提取的一种,因为它涉及提取信号的平滑特征,同时抑制尖锐特征。在这里,我们将改变滤波器核以解决其他图像特征检测问题:识别水平和垂直线条。
这两个卷积核如同图 7-11 中所示,它们对图像的影响也被展示了出来。您可以根据它们的视觉外观手工制作这两个卷积核;它们是

图 7-11. 练习 7-7 的结果
¹ M. A. Redmond 和 A. Baveja,《A Data-Driven Software Tool for Enabling Cooperative Information Sharing Among Police Departments》,《European Journal of Operational Research》141(2002):660–678。
² 将正弦函数中的负号互换。
³ 将右下角的元素设为 −1。
⁴ 提示:分别平滑每个颜色通道。
第八章:矩阵的逆
我们正在向解决矩阵方程迈进。矩阵方程类似于普通方程(例如,在 4x = 8 中解出 x),但是……它们包含矩阵。到了书的这一部分,你已经清楚,当涉及到矩阵时,事情变得复杂。尽管如此,我们必须接受这种复杂性,因为解矩阵方程是数据科学的重要组成部分。
矩阵的逆在实际应用中解决矩阵方程中至关重要,包括将统计模型拟合到数据(想想一般线性模型和回归)。在本章末尾,您将了解什么是矩阵的逆,何时可以和不能计算它,如何计算它以及如何解释它。
矩阵的逆
矩阵
但是为什么我们需要求矩阵的逆?我们需要“取消”一个矩阵,以便解决可以表示为
这看起来非常直接,但计算逆是非常棘手的,你很快会了解到。
逆的类型和可逆性条件
“求矩阵的逆”听起来好像应该总是可行。谁不愿意在方便时求一个矩阵的逆?不幸的是,生活并非总是如此简单:并非所有的矩阵都可以求逆。
这里介绍了三种不同类型的逆,具有不同的可逆条件。它们在此处介绍;详细信息在后续章节中。
完全逆
这意味着
单边逆
单边逆可以将一个矩形矩阵转换为单位矩阵,但它只对一个乘法顺序有效。特别地,一个高瘦矩阵
如果一个非方阵矩阵具有最大可能的秩,则仅有一边逆。也就是说,高瘦矩阵具有左逆,如果它是秩-N(完全列秩),而宽矮矩阵具有右逆,如果它是秩-M(完全行秩)。
广义逆
每个矩阵都有一个广义逆,不论其形状和秩。如果矩阵是方阵且满秩,那么它的广义逆等于其完全逆。同样地,如果矩阵是非方阵且具有其最大可能的秩,那么广义逆等于其左逆(对于高瘦矩阵)或右逆(对于宽矮矩阵)。但是,一个降秩矩阵仍然有一个广义逆,这种情况下,广义逆将奇异矩阵转换为另一个接近但不等于单位矩阵的矩阵。
没有全逆或单边逆的矩阵被称为奇异或不可逆。这与标记矩阵为降秩或秩不足是一回事。
计算逆矩阵
矩阵的逆听起来很棒!我们如何计算呢?让我们先思考如何计算标量的倒数:简单地取(求)数的倒数。例如,数 3 的倒数是 1/3,这与 3^(−1)是一样的。然后,
基于这种推理,你可能会猜测矩阵的逆工作方式相同:倒置每个矩阵元素。我们来试试:
不幸的是,这并没有产生预期的结果,这可以通过将原始矩阵乘以逐个倒置元素的矩阵来轻易地证明:
这是一个有效的乘法,但它不生成单位矩阵,这意味着逐个元素倒置的矩阵不是矩阵的逆。
有一个算法可以计算任何可逆矩阵的矩阵。这是一个冗长乏味的过程(这就是为什么我们让计算机来为我们进行数值运算的原因!),但对于特殊矩阵,有一些捷径。
2 × 2 矩阵的逆
要求一个
观察:
让我们通过一个数值示例来进行详细说明:
那个运行得很顺利。
在 Python 中计算逆矩阵很容易:
A = np.array([ [1,4],[2,7] ])
Ainv = np.linalg.inv(A)
A@Ainv
你可以确认A@Ainv得到单位矩阵,Ainv@A也是如此。当然,A*Ainv并不会得到单位矩阵,因为*是指 Hadamard(逐元素)乘法。
矩阵及其逆矩阵的乘积在图 8-1 中可视化。

图 8-1. 2 × 2 矩阵的逆
让我们尝试另一个例子:
这个例子有几个问题。矩阵乘法给出了
第二个例子有何不同?它是一个降秩矩阵(秩 = 1)。这显示了一个数值示例,降秩矩阵不可逆。
在这种情况下,Python 会做什么?让我们来看看:
A = np.array([ [1,4],[2,8] ])
Ainv = np.linalg.inv(A)
A@Ainv
Python 甚至不会像我这样尝试计算结果。相反,它给出了以下消息的错误:
LinAlgError: Singular matrix
降秩矩阵没有逆矩阵,像 Python 这样的程序甚至不会尝试计算。但是,该矩阵确实有一个伪逆。我将在几节后回到这个问题。
对角矩阵的逆
计算方阵的逆还有一种快捷方式。导致这种快捷方式的洞察是两个对角矩阵的乘积只是对角元素的标量乘积(在练习 5-12 中发现)。考虑下面的例子;在继续文本之前,请尝试找出对角矩阵逆的快捷方式:
你找到计算对角矩阵的逆的窍门了吗?窍门就是你只需求每个对角元素的倒数,而忽略非对角线上的零。这在前面的例子中很明显,通过设置
当你拥有一个对角矩阵,且对角线上有一个零时会发生什么?你无法对这个元素求逆,因为你会得到 1/0. 因此,对角线上至少有一个零的对角矩阵没有逆矩阵。(另外请回想第六章,对角矩阵只有当所有对角元素都非零时才是满秩的。)
对角矩阵的逆是重要的,因为它直接导致计算伪逆的公式。稍后详细讨论。
反转任意方阵满秩矩阵
老实说,我在考虑是否包含这一部分时犹豫了。反转可逆矩阵的完整算法冗长且乏味,你在应用中永远不会需要使用它(相反,你会使用 np.linalg.inv 或其他调用 inv 的函数)。
另一方面,用 Python 实现该算法是练习将算法描述为方程式和英文的良好机会。因此,我会在这里解释算法的工作原理,而不显示任何代码。我鼓励你在阅读本节时编写 Python 中的算法,并可以在在线代码的练习 8-2 中与我的解决方案进行对比。
计算逆矩阵的算法涉及四个中间矩阵,称为子式、格子、余子式和伴随矩阵:
子式矩阵
这个矩阵包含子矩阵的行列式。每个元素

图 8-3. 随机数矩阵生成逆矩阵的可视化
单侧逆
高瘦矩阵没有完整的逆矩阵。也就是说,对于尺寸为M > N的矩阵
但是存在一个矩阵
接下来的问题是:我们应该计算
T = np.random.randint(-10,11,size=(40,4))
TtT = T.T @ T
TtT_inv = np.linalg.inv(TtT)
TtT_inv @ TtT
您可以在代码中确认最后一行产生了单位矩阵(在机器精度误差范围内)。
让我将那段 Python 代码翻译成一个数学方程式:
从代码和公式可以看出,因为
但是——这是关键——我们寻找的是一个左乘矩阵
矩阵
现在我们可以完成 Python 代码以计算左逆并确认它符合我们的规格。左乘原始高矩阵以生成单位矩阵:
L = TtT_inv @ T.T # the left-inverse
L@T # produces the identity matrix
你也可以在 Python 中确认
图 8-4 描述了一个高矩阵、它的左逆以及左逆乘以矩阵的两种方式。注意左逆不是方阵,并且左逆后乘以它会得到明显不是单位矩阵的结果。

图 8-4. 左逆,可视化
左逆是非常重要的。实际上,一旦你了解了如何将统计模型拟合到数据和最小二乘解,你就会发现左逆随处可见。毫不夸张地说,左逆是线性代数对现代人类文明最重要的贡献之一。
对于只有完整列秩的高矩阵来说,左逆是有定义的,这一点在讨论中是隐含的。对于尺寸为 M > N 且秩 r < N 的矩阵来说,它没有左逆。为什么呢?答案在脚注中。¹
现在你知道如何计算左逆了。那右逆呢?我拒绝教你如何计算它!这并不是因为它是我对你保密的秘密知识,当然也不是因为我不喜欢你。相反,我希望你挑战自己去推导右逆的数学,并用 Python 代码演示它。如果需要的话,Exercise 8-4 提供了一些提示,或者你可以在继续下一节之前尝试自己找出答案。
逆矩阵是唯一的
矩阵的逆是唯一的,这意味着如果一个矩阵有逆,它就有且只有一个逆。不能有两个矩阵
这个主张有几个证明。我将展示一个依赖于“否定证明”的技术的证明。这意味着我们试图但未能证明一个错误的主张,从而证明了正确的主张。在这种情况下,我们从三个假设开始:(1) 矩阵 A 可逆,(2) 矩阵 B 和 C 是 A 的逆,(3) 矩阵 B 和 C 是不同的,即 B
所有声明都是相等的,这意味着第一个和最后一个表达式是相等的,这意味着我们假设 B
Moore-Penrose 伪逆
正如我之前所写的,通过矩阵乘法将降秩矩阵转换为单位矩阵是完全不可能的。这意味着降秩矩阵没有完全的或单侧的逆矩阵。但奇异矩阵确实有伪逆。伪逆是将一个矩阵接近单位矩阵的转换矩阵。
复数伪逆不是笔误:尽管完全的矩阵逆是唯一的,但伪逆并非唯一。降秩矩阵有无限多个伪逆。但有些伪逆比其他的好,实际上只有一个值得讨论的伪逆,因为这很可能是你唯一会使用的。
这被称为Moore-Penrose 伪逆,有时缩写为 MP 伪逆。但因为这是迄今为止最常用的伪逆,你可以总是假设术语伪逆指的是 Moore-Penrose 伪逆。
下面的矩阵是本章前面看到的奇异矩阵的伪逆。第一行显示了矩阵的伪逆,第二行显示了矩阵及其伪逆的乘积:
(缩放因子 85 是提取出来用于视觉检查矩阵。)
伪逆在上标中使用一个†、加号或星号来表示:
在 Python 中,使用函数np.linalg.pinv来实现伪逆。下面的代码计算了一个奇异矩阵的伪逆,而np.linalg.inv则产生了一个错误消息:
A = np.array([ [1,4],[2,8] ])
Apinv = np.linalg.pinv(A)
A@Apinv
伪逆是如何计算的?算法要么令人难以理解,要么直观易懂,这取决于你是否理解奇异值分解。我会简要解释伪逆的计算,但如果你不理解,那就不要担心:我保证到第十三章结束时,你会觉得这很直观。要计算伪逆,需要对矩阵进行奇异值分解,倒转非零奇异值而不改变奇异向量,然后通过乘以
逆的数值稳定性
计算矩阵的逆涉及大量的 FLOPs(浮点运算),包括许多行列式。您在第六章中学到,矩阵的行列式可能在数值上不稳定,因此计算许多行列式可能导致数值不准确,这些误差可能会积累并在处理大型矩阵时造成显著问题。
因此,实现数值计算的低级库(例如,LAPACK)通常在可能时避免显式求逆矩阵,或者将矩阵分解为更为数值稳定的其他矩阵的乘积(例如 QR 分解,在第九章中将会学习)。
数值在大致相同范围内的矩阵往往更加稳定(虽然这并不保证),这就是为什么随机数矩阵易于处理的原因。但是数值范围较大的矩阵存在高风险的数值不稳定性。“数值范围”更正式地捕捉为矩阵的条件数,即最大奇异值与最小奇异值的比率。您将在第十四章中更多地了解条件数;目前,可以说条件数是矩阵数值分布的度量。
一个数值上不稳定的矩阵的例子是希尔伯特矩阵。希尔伯特矩阵中的每个元素都由方程式 8-1 中显示的简单公式定义。
方程式 8-1. 创建希尔伯特矩阵的公式。i 和 j 是行和列的索引。
这是一个
随着矩阵变得更大,数值范围增加。因此,计算机计算的希尔伯特矩阵很快变为秩不足。即使全秩的希尔伯特矩阵其逆矩阵也在非常不同的数值范围内。这在图 8-5 中有所说明,该图展示了一个

图 8-6。矩阵逆取消了几何变换。生成此图的代码是 Exercise 8-8 的一部分。
当检查数学时,这种几何效果并不奇怪。在下面的方程中,
虽然这不是一个令人惊讶的结果,但我希望它有助于建立一些关于矩阵逆的几何直觉:通过矩阵施加的变换“撤销”。当你学习通过特征分解对矩阵进行对角化时,这种解释会很有用。
这种几何解释也为为何降秩矩阵没有逆的直观理解提供了一些启示:通过奇异矩阵变换的几何效果是至少有一个维度被压扁。一旦一个维度被压扁,它就无法恢复,就像你不能在面对镜子时看到自己的背面一样。²
总结
我非常喜欢写这一章,也希望你们从中学到了东西。以下是关键的要点总结:
-
矩阵逆是一个矩阵,通过矩阵乘法将最大秩矩阵转换为单位矩阵。逆具有多种用途,包括在方程中移动矩阵(例如,解Ax = b中的x)。
-
一个正方形全秩矩阵有一个完全逆,一个高且全列秩矩阵有一个左逆,而宽且全行秩矩阵有一个右逆。降秩矩阵不能被线性变换为单位矩阵,但它们确实有一个伪逆,可以将矩阵变换为另一个更接近单位矩阵的矩阵。
-
逆是唯一的——如果一个矩阵可以线性变换为单位矩阵,那么只有一种方法可以做到。
-
有一些技巧可以计算某些类型矩阵的逆,包括 2×2 和对角矩阵。这些快捷方式是计算矩阵逆的完整公式的简化。
-
由于数值精度误差的风险,生产级算法尝试避免显式求逆矩阵,或将矩阵分解为其他可以更稳定地求逆的矩阵。
代码练习
练习 8-1。
逆的逆是原始矩阵;换句话说,
练习 8-2。
实现 “求逆任意方阵” 中描述的完整算法,并复制 Figure 8-3。当然,你的矩阵由于随机数而不同,尽管网格和单位矩阵将相同。
练习 8-3。
手动为一个 2 × 2 矩阵使用矩阵元素 a, b, c, 和 d 实现完全逆算法。我通常不会在这本书中分配手工解决的问题,但这个练习将向你展示快捷方式的来源。请记住,标量的行列式是它的绝对值。
练习 8-4。
通过追随发现左逆的逻辑来推导宽矩阵的右逆。然后为一个宽矩阵复制 Figure 8-4。(提示:从左逆的代码开始,并根据需要进行调整。)
练习 8-5。
用 Python 示范伪逆(通过 np.linalg.pinv)等于完全逆(通过 np.linalg.inv)的可逆矩阵。接下来,用一个高列满秩矩阵展示伪逆等于左逆,以及用一个宽行满秩矩阵展示伪逆等于右逆。
练习 8-6。
LIVE EVIL 规则适用于相乘矩阵的逆。通过创建两个方阵全秩矩阵
Distance between (AB)^-1 and (A^-1)(B^-1) is ___
Distance between (AB)^-1 and (B^-1)(A^-1) is ___
作为额外挑战,您可以确认 LIVE EVIL 规则适用于更长的矩阵串联,例如四个矩阵而不是两个。
练习 8-7。
LIVE EVIL 规则是否也适用于单侧逆矩阵?也就是说,是否
练习 8-8。
编写代码以再现 Figure 8-6。首先从 Exercise 7-3 复制代码。在复制图表后,通过将左下角元素设置为1使变换矩阵不可逆。代码中还需要更改什么以防止错误发生?
练习 8-9。
这个和下一个练习将帮助您探索矩阵的逆及其数值不稳定性,使用希尔伯特矩阵。首先创建一个希尔伯特矩阵。编写一个 Python 函数,该函数以整数作为输入并生成一个希尔伯特矩阵作为输出,遵循 Equation 8-1。然后再现 Figure 8-5。
我建议编写您的 Python 函数,使用双重for循环遍历行和列(i和j矩阵索引),按照数学公式操作。确认函数准确无误后,您可以选择性地挑战自己,尝试在没有任何for循环的情况下重写函数(提示:外积)。您可以通过将其与scipy.linalg库中的hilbert函数进行比较来确认函数的准确性。
练习 8-10.
使用您的希尔伯特矩阵函数,创建一个希尔伯特矩阵,然后使用np.linalg.inv计算其逆,并计算这两个矩阵的乘积。该乘积应等于单位矩阵,这意味着该乘积与由np.eye生成的真实单位矩阵之间的欧几里德距离应为 0(在计算机舍入误差内)。计算该欧几里德距离。
将此代码放入一个从for循环中。对于每个矩阵尺寸,存储希尔伯特矩阵的欧几里德距离和条件数。如前所述,条件数是矩阵中数值分布的一种度量,可以使用函数np.linalg.cond提取。
接下来,重复前述代码,但使用高斯随机数矩阵代替希尔伯特矩阵。
最后,按照图 8-7 中所示,绘制所有结果。我将距离和条件数绘制在对数刻度上,以便进行视觉解释。

图 8-7. 练习 8-10 的结果
请继续探索线性代数,感受灵感吧!尝试绘制希尔伯特矩阵乘以其逆矩阵(考虑调整颜色缩放),使用更大的矩阵或不同的特殊矩阵,提取矩阵的其他属性,如秩或范数等。您正在探索线性代数的奇妙世界,而 Python 则是带您穿越这片风景的魔毯。
¹ 因为T^TT是降秩的,因此不能被反转。
² 在这里有一个与Flatland的机智类比,我无法准确表达。重点是:阅读书籍Flatland。
第九章:正交矩阵和 QR 分解
您将在本书中学习五种主要的分解:正交向量分解、QR 分解、LU 分解、特征分解和奇异值分解。这些不是线性代数中唯一的分解,但它们是数据科学和机器学习中最重要的分解方法之一。
在本章中,您将学习 QR。并且在此过程中,您将学习一种新的特殊类型的矩阵(正交矩阵)。QR 分解是许多应用的基础,包括矩阵求逆、最小二乘模型拟合和特征分解。因此,理解和熟悉 QR 分解将帮助您提升线性代数技能。
正交矩阵
我将首先介绍正交矩阵给您。正交矩阵是一种特殊的矩阵,对于多种分解(包括 QR 分解、特征分解和奇异值分解)至关重要。字母
正交列
所有列都是两两正交的。
单位范数列
每列的范数(几何长度)恰好为 1。
我们可以将这两个特性翻译成数学表达式(请记住
这是什么意思?这意味着列与自身的点积为 1,而列与任何其他列的点积为 0。这是许多点积,只有两种可能的结果。我们可以通过将矩阵与其转置的乘积前置来组织所有列对之间的点积。请记住,矩阵乘法定义为左矩阵的所有行与右矩阵的所有列的点积;因此,
表达两个正交矩阵关键属性的矩阵方程式简直是奇妙的:
表达式
为什么这很重要?因为
这样的矩阵真的存在吗,还是只是数据科学家想象中的产物?是的,它们确实存在。事实上,单位矩阵就是正交矩阵的一个例子。以下是另外两个例子:
请花一点时间确认每一列的长度为单位长度,并且与其他列正交。然后我们可以在 Python 中确认:
Q1 = np.array([ [1,-1],[1,1] ]) / np.sqrt(2)
Q2 = np.array([ [1,2,2],[2,1,-2],[-2,2,-1] ]) / 3
print( Q1.T @ Q1 )
print( Q2.T @ Q2 )
两个输出都是单位矩阵(在约为 10^(-15)的舍入误差内)。如果计算
另一个正交矩阵的例子是你在第七章中学到的纯旋转矩阵。你可以回顾那段代码,并确认变换矩阵乘以其转置矩阵为单位矩阵,无论旋转角度如何(只要所有矩阵元素使用相同的旋转角度)。排列矩阵也是正交的。排列矩阵用于交换矩阵的行;在下一章中,你将学习关于 LU 分解的讨论。
如何创建这样宏伟的数学奇迹?正交矩阵可以通过 QR 分解从非正交矩阵中计算得到,这基本上是 Gram-Schmidt 的复杂版本。Gram-Schmidt 是如何工作的?这基本上是你在第二章中学到的正交向量分解。
Gram-Schmidt
Gram-Schmidt 过程是将非正交矩阵转换为正交矩阵的方法。Gram-Schmidt 具有很高的教育价值,但非常遗憾的是应用价值很小。原因在于,正如你之前已经读过的那样,由于许多除法和乘法操作中涉及到的小数,导致数值不稳定。幸运的是,还有更复杂和数值稳定的 QR 分解方法,例如 Householder 反射。这些算法的细节超出了本书的范围,但由 Python 调用的低级数值计算库处理。
尽管如此,我将描述 Gram-Schmidt 过程(有时缩写为 GS 或 G-S),因为它展示了正交向量分解的应用,因为你将基于以下数学和描述在 Python 中编程该算法,并且因为 GS 是理解 QR 分解如何工作的正确方式,即使低级实现略有不同。
由列
对于矩阵
-
使用正交向量分解将
正交化到矩阵𝐯 k 中所有之前的列。也就是说,计算𝐐 在𝐯 k ,𝐪 k - 1 直至𝐪 k - 2 的垂直分量。正交化后的向量称为𝐪 1 。²𝐯 k * -
将
归一化为单位长度。现在这是𝐯 k * ,矩阵𝐪 k 中的第 k 列。𝐐
听起来很简单,不是吗?在代码中实现这个算法可能会有些棘手,因为需要重复进行正交化。但只要有些坚持,你就能搞明白(习题 9-2)。
QR 分解
GS 过程将一个矩阵转换为正交矩阵
声音中有什么?
“QR”分解中的“QR”发音为“queue are”。在我看来,这真是一个错失的机会;如果我们把它发音为“QweRty decomposition”,学习线性代数可能会更有趣。或者,我们可以将其发音为“core decomposition”,以吸引健身人群。但是,无论好坏,现代惯例都受历史先例的影响。
显然,
在这里,您可以看到正交矩阵的美妙之处:我们可以解决矩阵方程,而不必担心计算逆矩阵。
以下 Python 代码展示了如何计算方阵的 QR 分解,图 9-1 说明了这三个矩阵:
A = np.random.randn(6,6)
Q,R = np.linalg.qr(A)

图 9-1。随机数矩阵的 QR 分解
QR 分解的几个重要特征在图 9-1 中可见,包括
检查
Q 和 R 的尺寸
经济与完整的 QR 分解仅适用于高矩阵。问题在于对于一个高矩阵(M > N),我们是创建一个具有N列还是M列的

图 9-2。Q和R的大小取决于A的大小。“?”表示矩阵元素取决于A中的值,即它不是单位矩阵。
当
A = np.array([ [1,-1] ]).T
Q,R = np.linalg.qr(A,'complete')
Q*np.sqrt(2) # scaled by sqrt(2) to get integers
>> array([[-1., 1.],
[ 1., 1.]])
注意可选的第二个输入'complete',它会产生完整的 QR 分解。将其设置为'reduced'(默认情况),则会得到经济模式的 QR 分解,其中
因为可以从具有N列的矩阵中制作多于M > N个正交向量,所以
由正交化引起的
关于唯一性的注意事项:QR 分解对于所有矩阵大小和秩都不是唯一的。这意味着可能会得到
为什么 𝐑 是上三角形的
希望您认真考虑了这个问题。这是关于 QR 分解的一个棘手点,如果您自己无法弄清楚,请阅读接下来的几段文字,然后远离书本/屏幕,重新推导出这个论点。
我将首先提醒您三个事实:
-
来自公式𝐑 。𝐐 T 𝐀 = 𝐑 -
一个乘积矩阵的下三角由左矩阵的后续行和右矩阵的先前列的点积组成。
-
的行是𝐐 T 的列。𝐐
将它们放在一起:因为正交化是从左到右逐列进行的,
最后评论:如果
QR 和逆矩阵
QR 分解提供了一种计算矩阵逆的更为数值稳定的方式。
让我们首先写出 QR 分解公式,并倒转方程的两侧(注意应用 LIVE EVIL 规则):
因此,我们可以将
现在,我们仍然需要明确地求解
另一方面,请记住,理论上可逆但接近奇异的矩阵仍然非常难以求逆;QR 分解可能比前一章节介绍的传统算法更数值稳定,但这并不保证高质量的逆矩阵。浸在蜂蜜中的烂苹果仍然是烂的。
总结
QR 分解非常棒。它绝对是线性代数中前五个最棒的矩阵分解之一。以下是本章的主要要点:
-
正交矩阵具有两两正交且范数为 1 的列。正交矩阵是几种矩阵分解的关键,包括 QR 分解、特征分解和奇异值分解。在几何学和计算机图形学中(如纯旋转矩阵),正交矩阵也非常重要。
-
您可以通过 Gram-Schmidt 过程将非正交矩阵转换为正交矩阵,该过程涉及应用正交向量分解来隔离每一列中与所有前一列正交的分量(“前一列”指从左到右)。
-
QR 分解是 Gram-Schmidt 的结果(严格来说,它是通过更稳定的算法实现的,但 GS 仍是理解它的正确方式)。
代码练习
练习 9-1.
一个方阵
通过从随机数矩阵计算
练习 9-2.
按照前述步骤实现 Gram-Schmidt 过程。使用一个np.linalg.qr中的
重要提示:在像 Householder 反射这样的变换中存在根本性的符号不确定性。这意味着向量可以根据算法和实现的细微差异“翻转”(乘以-1)。这种特性存在于许多矩阵分解中,包括特征向量。我在第十三章中对此进行了更长更深入的讨论。暂且而言,要点是这样的:从 Python 的
练习 9-3.
在这个练习中,您将了解在将几乎正交的矩阵应用 QR 分解时会发生什么。首先,从一个
其次,修改每列
第三步,通过设置元素
练习 9-4.
这个练习的目的是比较使用前一章学到的“老派”逆方法与基于 QR 的逆方法的数值误差。我们将使用随机数矩阵,注意它们倾向于是数值稳定的,因此具有准确的逆矩阵。
这是该做的事情:将代码从 Exercise 8-2 复制到一个 Python 函数中,该函数以矩阵作为输入,并输出其逆矩阵。(您还可以包含一个检查输入矩阵是否为方阵和满秩的检查。)我将这个函数称为 oldSchoolInv。接下来,创建一个 np.eye 的欧氏距离作为逆估计误差。制作一个条形图显示结果,将两种方法显示在 x 轴上,误差(到

图 9-3. 练习 9-4 的结果
多次运行代码并检查条形图。您会发现有时候“老派”方法更好,而其他时候 QR 方法更好(较小的数字更好;理论上,条应该高度为零)。尝试使用
练习 9-5.
将前一个练习中的代码放入一个for循环中,重复 100 次实验,每次使用不同的随机数矩阵。存储每次迭代的误差(欧几里得距离),并制作像图 9-4 那样的图表,显示所有实验运行的平均值(灰色条)和所有单个误差(黑色点)。运行
你也可以尝试使用np.linalg.inv来反转

图 9-4. 练习 9-5 的结果。请注意左右面板在 y 轴缩放上的差异。
练习 9-6.
方阵正交矩阵的一个有趣特性是它们的所有奇异值(及其特征值)都是 1。这意味着它们有一个诱导 2-范数为 1(诱导范数是最大奇异值),并且它们有一个 Frobenius 范数为M。后者的结果是因为 Frobenius 范数等于所有奇异值的平方和的平方根。在这个练习中,你将确认这些特性。
创建一个np.linalg.norm计算其诱导 2-范数,并使用你在第 6 章学到的公式计算其 Frobenius 范数,除以M的平方根。确认这两个量约为 1(考虑到四舍五入误差的合理容差)。使用几个不同的M值进行检查。
接下来,通过矩阵-向量乘法探索诱导范数的含义。创建一个随机的M元素列向量
最后,拿一张纸,发展一个证明这个经验演示的证明。这个证明在下一段打印出来,所以不要往下看!但如果你需要提示,可以查看脚注⁵。
我真诚地希望你读这篇文章是为了检查你的推理能力,而不是作弊!不管怎样,证明是向量范数
练习 9-7。
这个练习将突出
当然,
¹ 这在 Exercise 9-1 中进一步探讨。
² 第一列向量不是正交的,因为没有前面的向量;因此,你从以下的标准化步骤开始。
³ 通过矩阵乘法恢复R是可能的,因为 GS 是一系列线性变换。
⁴ 在这个练习中花些时间,它相当具有挑战性。
⁵ 提示:写出向量范数的点积公式。
第十章:行简化与 LU 分解
现在我们转向 LU 分解。LU 分解,像 QR 一样,是支持数据科学算法的计算基础之一,包括最小二乘模型拟合和矩阵求逆。因此,这一章对你的线性代数教育至关重要。
LU 分解的关键在于你不能简单地立刻学会它。相反,你首先需要学习关于方程组、行简化和高斯消元。在学习这些主题的过程中,你也会了解到梯形矩阵和置换矩阵。亲爱的读者,是的,这将是一个令人兴奋和充满活力的章节。
方程组
要理解 LU 分解及其应用,你需要了解行简化和高斯消元。而要理解这些主题,你需要了解如何操作方程、将其转换为矩阵方程,并使用行简化来解决该矩阵方程。
让我们从一个“系统”方程开始:
正如你在学校中学到的那样,你可以对方程进行各种数学操作——只要你同时对方程的两边做同样的操作。这意味着下面的方程与前一个方程不同,但它们通过简单的操作相关联。更重要的是,任何一个方程的解也是另一个方程的解:
现在让我们转向一个包含两个方程的系统:
在这个方程组中,从任何一个方程中都不可能单独求解出x和y的唯一值。相反,你需要同时考虑两个方程来推导解。如果你现在尝试解决这个系统,你可能会采取用第二个方程的右侧值代替第一个方程中的y的策略。在第一个方程中解出x后,将该值代入第二个方程中解出y。这种策略类似于(尽管不如)后代换,稍后我会定义它。
方程组的一个重要特点是你可以将各个方程相加或相减。在下面的方程中,我将第二个方程乘以 2 加到第一个方程中,并从第二个方程中减去第一个原始方程(为了清晰起见添加了括号):
我会让你进行算术运算,但要点是x在第一个方程中消失,而y在第二个方程中消失。这使得解的计算变得更容易(x = 4/3,y = 8/3)。这里的重要一点是:对方程进行标量乘法并将它们相加使得系统的解更容易找到。再次强调,调整后和原始系统并不是相同的方程,但它们的解是相同的,因为这两个系统通过一系列线性操作相连接。
这是你需要学习如何使用线性代数解方程组之前的背景知识。但在学习该方法之前,你需要学习如何使用矩阵和向量表示方程组。
将方程转化为矩阵
将方程组转化为矩阵-向量方程用于解决方程组,并用于建立统计学中一般线性模型的公式。幸运的是,将方程转化为矩阵在概念上很简单,并包括两个步骤。
首先,组织方程使得常数位于方程的右侧。常数是与变量无关的数字(有时称为截距或偏移量)。变量及其乘法系数按照相同的顺序位于方程的左侧(例如,所有方程应先有x项,然后是y项,依此类推)。以下方程形成我们一直在处理的方程组,并按正确的方式组织:
其次,将系数(乘以变量的数字;缺少方程的变量具有零系数)分成一个矩阵,每个方程一行。变量被放置到右乘系数矩阵的列向量中。常数则放置在方程的右侧的列向量中。我们的示例系统有一个矩阵方程,看起来像这样:
然后!你已经将方程组转化为一个矩阵方程。我们可以称这个方程为
请花一点时间确保你理解矩阵方程是如何映射到方程组的。特别是通过矩阵-向量乘法来演示它等于原始方程组。
使用矩阵方程
你可以像处理普通方程一样操作矩阵方程,包括加法、乘法、转置等,只要操作是有效的(例如,加法的矩阵尺寸匹配),并且所有操作影响方程的两边。例如,以下方程的进展是有效的:
使用矩阵方程与标量方程的主要区别在于,由于矩阵乘法是侧向依赖的,你必须在方程的两边以相同的方式进行矩阵乘法。
例如,以下方程的进展是有效的:
注意,
这里的问题在于数学公式
让我们看一个 Python 的例子。我们将解出未知矩阵
A = np.random.randn(4,4)
B = np.random.randn(4,4)
# solve for X
X1 = np.linalg.inv(A) @ B
X2 = B @ np.linalg.inv(A)
# residual (should be zeros matrix)
res1 = A@X1 - B
res2 = A@X2 - B
如果矩阵乘法是可交换的(意味着顺序不重要),那么 res1 和 res2 都应该等于零矩阵。让我们看看:
res1:
[[-0. 0. 0. 0.]
[-0. -0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. -0. -0.]]
res2:
[[-0.47851507 6.24882633 4.39977191 1.80312482]
[ 2.47389146 2.56857366 1.58116135 -0.52646367]
[-2.12244448 -0.20807188 0.2824044 -0.91822892]
[-3.61085707 -3.80132548 -3.47900644 -2.372463 ]]
现在你知道如何用一个矩阵方程表示一个方程组。在接下来的几节中,我将回到这个问题;首先,我需要教你关于行列式变换和矩阵的阶梯形式。
行列式变换
行列式变换是传统线性代数中备受关注的话题,因为它是通过手工解决方程组的古老方式。我确实怀疑在作为数据科学家的职业生涯中你会手工解决任何方程组。但了解行列式变换是有用的,并且直接导致 LU 分解,而 LU 分解实际上被应用在应用线性代数中。所以让我们开始吧。
行列式变换 意味着反复应用两种操作——行的标量乘法和加法——到矩阵的行上。行列式变换依赖于在系统中向其他方程式添加方程式的相同原理。
记住这个声明:行列式变换的目标是将密集矩阵转换为上三角矩阵。
让我们从一个简单的例子开始。在下面的密集矩阵中,我们将第一行加到第二行,这消除了-2。有了这个变化,我们将密集矩阵转换成了上三角矩阵:
行列式变换后的上三角矩阵被称为矩阵的阶梯形式。
形式上,如果一个矩阵满足以下条件,它就是阶梯形式:(1)每一行的最左边非零数(称为主元)在上面行的主元右边,(2)所有零行在非零行的下面。
类似于在系统中操作方程,行变换后的矩阵与行变换前的矩阵不同。但是这两个矩阵通过线性变换相连。由于线性变换可以用矩阵表示,我们可以使用矩阵乘法来表示行变换:
我将称那个矩阵为
这里有另一个例子,一个
行变换是乏味的(见[“行变换总是这么简单吗?”)。肯定有一个 Python 函数可以帮我们做这件事!有也有没有。没有一个 Python 函数能返回像我在前面两个例子中创建的梯形形式。原因是矩阵的梯形形式不是唯一的。例如,在前面的
话虽如此,矩阵的两个梯形形式优于无限可能的梯形形式。这两种形式在给定一些约束条件下是唯一的,称为简化行梯形形式和
高斯消元
在本书的这一部分,你已经学会如何使用矩阵逆来解决矩阵方程。如果我告诉你,你可以在不求逆任何矩阵的情况下解决矩阵方程,你会怎么想?³
这种技术被称为高斯消元法。尽管其名称如此,这个算法实际上是由中国数学家在高斯之前近两千年发展出来的,然后由牛顿在高斯之前数百年重新发现。但是高斯在该方法中作出了重要贡献,包括现代计算机实现的技术。
高斯消元法很简单:将系数矩阵增广为常数向量,行约化为梯形形式,然后使用回代法依次求解每个变量。
让我们从我们之前解决的两个方程组开始:
第一步是将这个方程组转换为一个矩阵方程。我们已经完成了这一步;这个方程如下所示:
接下来,我们将系数矩阵与常数向量进行增广:
然后我们对增广矩阵进行行约化。注意在行约化过程中,常数列向量将发生变化:
一旦我们将矩阵转换为其梯形形式,我们将增广矩阵转换回方程组。看起来是这样的:
通过行约化的高斯消元法移除了第二个方程中的x项,这意味着解y仅涉及一些算术运算。一旦解出y = 8/3,将该值代入第一个方程中的y,并解出x。这个过程被称为回代法。
在前一节中,我写道 Python 没有函数来计算矩阵的梯形形式,因为它不是唯一的。然后我写道有一个唯一的梯形矩阵,称为简化行阶梯形式,通常缩写为 RREF,Python 将会计算。继续阅读以了解更多信息…
高斯-约当消元
让我们继续对我们的示例矩阵进行行约化,目标是将所有主元——每行中最左侧的非零数——转换为 1。一旦得到梯形矩阵,你只需将每行除以其主元。在这个例子中,第一行已经有了左侧位置的 1,所以我们只需要调整第二行。这给出了以下矩阵:
现在讲解一个窍门:我们继续向上行约化,以消除每个主元上方的所有元素。换句话说,我们希望得到一个梯形矩阵,其中每个主元为 1,且是其所在列的唯一非零数。
这就是我们原始矩阵的简化行阶梯形式(RREF)。你可以看到左侧是单位矩阵——RREF 总会在原始矩阵的左上角生成一个单位矩阵。这是通过将所有主元设为 1,并使用向上行约化来消除每个主元上方的所有元素得到的结果。
现在我们通过将矩阵转换回方程组来继续高斯消元:
我们不再需要回代,甚至基本的算术:修改后的高斯消元法——称为高斯-约旦消元法——解开了方程组中交织的变量,并显露了每个变量的解。
在计算机帮助我们进行数字计算之前,高斯-约旦消元法是人们手工解方程组的方法超过一个世纪。事实上,计算机仍然实现这个完全相同的方法,只是进行了一些小的修改以确保数值稳定性。
RREF 是唯一的,这意味着一个矩阵有且仅有一个相关的 RREF。NumPy 没有计算矩阵 RREF 的函数,但 sympy 库有(sympy 是 Python 中的符号数学库,是“黑板数学”的强大引擎):
import sympy as sym
# the matrix converted to sympy
M = np.array([ [1,1,4],[-1/2,1,2] ])
symMat = sym.Matrix(M)
# RREF
symMat.rref()[0]
>>
[[1, 0, 1.33333333333333],
[0, 1, 2.66666666666667]]
通过高斯-约旦消元法求矩阵的逆
从高斯-约旦消元法的关键洞见是行约简产生了一系列解决方程组的行操作。这些行操作是线性变换。
有趣的是,高斯-约旦消元的描述与矩阵逆的描述一致:一个解决一组方程的线性变换。但等等,矩阵逆解决的“方程组”是什么?对矩阵逆的新视角将提供一些新的见解。考虑这个方程组:
转化为矩阵方程,我们得到:
看看常数向量吧——它是
然后我们重复该过程,但解决矩阵逆的第二列:
对该系统进行 RREF 得到向量 [
我已经将单位矩阵的列分开,以回到解决方程组的视角。但是我们可以增广整个单位矩阵,并通过一个 RREF 求解逆矩阵。
下面是通过高斯-约旦消元法获得矩阵逆的鸟瞰图(方括号表示带有垂直线分隔的增广矩阵):
这很有趣,因为它提供了一种计算矩阵逆的机制,而无需计算行列式。另一方面,行变换确实涉及大量的除法,这增加了数值精度误差的风险。例如,想象一下我们有两个基本上是零加上舍入误差的数字。如果我们在 RREF 过程中除以这些数字,我们可能会得到分数
结论类似于我在前一章讨论过的关于使用 QR 分解计算矩阵逆的内容:使用 Gauss-Jordan 消元法计算矩阵逆很可能比完整的逆算法在数值稳定性上更可靠,但是接近奇异或具有高条件数的矩阵很难反转,无论使用哪种算法。
LU 分解
LU 分解 中的“LU”代表“下三角,上三角”。其思想是将一个矩阵分解成两个三角矩阵的乘积:
这里是一个数值例子:
这里是对应的 Python 代码(注意 LU 分解的函数在 SciPy 库中):
import scipy.linalg # LU in scipy library
A = np.array([ [2,2,4], [1,0,3], [2,1,2] ])
_,L,U = scipy.linalg.lu(A)
# print them out
print('L: '), print(L)
print('U: '), print(U)
L:
[[1. 0. 0. ]
[0.5 1. 0. ]
[1. 1. 1. ]]
U:
[[ 2. 2. 4.]
[ 0. -1. 1.]
[ 0. 0. -3.]]
这两个矩阵是从哪里来的?事实上,你已经知道答案:行变换可以表示为
由于梯形形式不唯一,LU 分解也不一定唯一。也就是说,存在无限多个下三角矩阵和上三角矩阵的配对,它们可以相乘得到矩阵
通过置换矩阵进行行交换
有些矩阵不容易转换成上三角形式。考虑以下矩阵:
它不处于梯形形式,但如果我们交换第二行和第三行,它将处于梯形形式。行交换是行约简的技巧之一,并且通过排列矩阵实现:
排列矩阵通常标记为
值得注意的是,排列矩阵是正交的,因此
重要提示:我上面写的公式提供了 LU 分解的数学描述。实际上,Scipy 返回的是
图 10-1 展示了 LU 分解应用于随机矩阵的示例。

图 10-2. 练习 10-2 的结果
练习 10-3.
LU 分解的一个应用是计算行列式。行列式的两个性质如下:三角矩阵的行列式是对角线元素的乘积,乘积矩阵的行列式等于行列式的乘积(即 np.linalg.det(A) 的结果进行比较,然后再阅读下一段。
你得到了与 Python 相同的结果吗?我假设你发现行列式的大小匹配,但符号似乎会随机不同。为什么会发生这种情况?这是因为我在说明中省略了排列矩阵。置换矩阵的行交换次数为偶数时行列式为 +1,为奇数时为 −1。现在回到你的代码中,并在计算中包括
练习 10-4.
根据章节 “LU Decomposition” 中的公式,矩阵的逆可以表示为:
使用从 scipy.linalg.lu 输出的 scipy.linalg.lu 输出的内容所述。 调整代码以遵循 SciPy 的惯例,而不是数学惯例。
这个练习的要点是:缺少错误消息并不一定意味着你的代码是正确的。请尽可能多地检查你的数学代码。
练习 10-5.
对于矩阵
¹ 当然,您知道顺序很重要,但经验演示有助于建立直觉。我希望您养成使用 Python 作为工具来经验性地确认数学原理的习惯。
² 剧透警告:LU 分解涉及将矩阵表示为下三角矩阵和上三角矩阵的乘积。
³ 请想象一下矩阵的米黄斯用红色和蓝色药丸的表情包,对应于接受新知识与固守现有知识的选择。
⁴ 这些都是传统线性代数教材中你会学到的众多方面;它们本身很有趣,但与数据科学的直接相关性较小。
第十一章:广义线性模型和最小二乘法
宇宙是一个非常大而复杂的地方。地球上的所有动物都有一种探索和理解其环境的自然好奇心,但我们人类则有幸能够发展科学和统计工具,将我们的好奇心提升到更高的水平。这就是为什么我们有飞机、MRI 机器、火星探测器、疫苗,当然还有像本书这样的书籍。
我们如何理解宇宙?通过发展数学基础的理论,并收集数据来测试和改进这些理论。这将我们带到了统计模型。统计模型是世界某些方面的简化数学表示。一些统计模型很简单(例如,预测股市数十年来会上涨),而其他一些则更为复杂,比如蓝脑项目以如此精细的细节模拟脑活动,以至于模拟一秒钟的活动需要 40 分钟的计算时间。
统计 模型的一个关键区别(与其他数学模型相对)是它们包含可根据数据进行拟合的自由参数。例如,我知道股市会随时间上涨,但我不知道上涨幅度是多少。因此,我允许随时间变化的股市价格(即斜率)是一个由数据决定数值的自由参数。
制定统计模型可能很困难,需要创造力、经验和专业知识。但根据将模型拟合到数据的自由参数是一件简单的线性代数问题——实际上,你已经掌握了本章所需的所有数学,只是需要将各部分组合起来,并学习统计术语。
广义线性模型
统计模型是一组方程,将预测变量(称为 独立变量)与观察结果(称为 依赖变量)相关联。在股市模型中,独立变量是 时间,依赖变量是 股市价格(例如,以标准普尔 500 指数量化)。
在本书中,我将重点介绍广义线性模型(GLM),其简称为 GLM。例如,回归是 GLM 的一种类型。
术语
统计学家使用的术语与线性代数学家略有不同。表 11-1 展示了 GLM 中用于向量和矩阵的关键字母和描述。
表 11-1. 广义线性模型术语表
| LinAlg | Stats | 描述 |
|---|---|---|
| Ax = b | 广义线性模型(GLM) | |
| A | X | 设计矩阵(列 = 独立变量,预测变量,回归器) |
| x | 回归系数或贝塔参数 | |
| b | y | 因变量,结果测量,数据 |
设置广义线性模型
设置一个 GLM 包括(1)定义一个将预测变量与因变量联系起来的方程,(2)将观察数据映射到这些方程中,(3)将一系列方程转换为矩阵方程,并(4)解决该方程。
我将用一个简单的例子来具体说明这个过程。我有一个模型,根据体重和父母的身高预测成年人的身高。方程如下所示:
y 是个体的身高,w 是他们的体重,h 是他们父母的身高(母亲和父亲的平均值)。
我的假设是,体重和父母的身高对个体的身高很重要,但我不知道每个变量有多重要。输入
现在我们有了我们的方程,我们的宇宙模型(嗯,它的一个微小部分)。接下来,我们需要将观察数据映射到这些方程中。为了简单起见,我会在 表 11-2 中虚构一些数据(你可以想象 y 和 h 单位是厘米,w 单位是公斤)。
表 11-2。我们身高统计模型的虚构数据
| y | w | h |
|---|---|---|
| 175 | 70 | 177 |
| 181 | 86 | 190 |
| 159 | 63 | 180 |
| 165 | 62 | 172 |
将观察数据映射到我们的统计模型中涉及将方程复制四次(对应我们数据集中的四个观察),每次用实际数据替换变量 y、w 和 h:
目前我会省略
当然,我们可以简洁地表示这个方程为
解决广义线性模型(GLMs)
我相信你已经了解本节的主要思想:为了求解未知系数向量
请盯着那个最后的方程式直到它永远刻在你的脑海中。它被称为最小二乘解,是应用线性代数中最重要的数学方程之一。你会在研究出版物、教科书、博客、讲座、Python 函数的文档字符串、塔吉克斯坦的广告牌^(1)等地方看到它。你可能会看到不同的字母,或者可能会有一些附加内容,如下所示:
方程式的含义及附加矩阵的解释并不重要(它们是正则化模型拟合的各种方法);重要的是,你能够看到最小二乘公式嵌入到那个看似复杂的方程式中(例如,设定
通过左逆的最小二乘解可以直接翻译成 Python 代码(变量 X 是设计矩阵,变量 y 是数据向量):
X_leftinv = np.linalg.inv(X.T@X) @ X.T
# solve for the coefficients
beta = X_leftinv @ y
我将在本章后面展示这些模型的结果——以及如何解释它们;现在请你专注于数学公式如何转换为 Python 代码。
左逆与 NumPy 的最小二乘解算器
本章的代码直接将数学公式转换为 Python 代码。显式计算左逆不是解决广义线性模型(GLM)最稳定的方式(尽管对本章中的简单问题来说是准确的),但我希望你能看到这些看似抽象的线性代数确实有效。解决 GLM 的更稳定的方式包括 QR 分解(你将在本章后面看到)和 Python 的更稳定的方法(你将在下一章看到)。
解是否确切?
当数据向量
要理解为什么不这样做,让我们想象一项针对大学生的调查,研究人员试图根据饮酒行为预测平均 GPA(平均绩点)。调查可能包含来自两千名学生的数据,但只有三个问题(例如,您消费多少酒精;您经常失忆吗;您的 GPA 是多少)。数据包含在一个 2000×3 的表格中。设计矩阵的列空间是该 2000D 环境维度内的一个 2D 子空间,而数据向量是同一环境维度内的一个 1D 子空间。
如果数据在设计矩阵的列空间内,意味着模型可以解释数据方差的 100%。但这几乎从不会发生:现实世界的数据包含噪声和抽样变异性,而模型只是简化,未能解释所有变异性(例如,GPA 由我们模型忽略的多种因素决定)。
解决这一难题的方法是修改 GLM 方程,允许模型预测数据与观察数据之间存在差异。它可以用几种等效(至少在符号上)的方式表示:
第一个方程的解释是
还有另一种非常有见地的解释,从几何角度接近 GLM 和最小二乘法。我将在下一节详细介绍这一点。
这一部分的要点是观察到的数据几乎从不在回归向量生成的子空间内。因此,通常将 GLM 表示为
因此,GLM 的目标是找到回归变量的线性组合,使其尽可能接近观察数据。关于这一点的更多内容将在后面详细说明;现在我想向你介绍最小二乘法的几何视角。
最小二乘法的几何视角
到目前为止,我介绍了从解矩阵方程的代数角度解决 GLM 的方法。GLM 还有一种几何视角,提供了一种替代视角,并帮助揭示最小二乘解决方案的几个重要特征。
让我们考虑设计矩阵的列空间
第一个问题的答案是否定的,正如我在前一节中讨论的那样。第二个问题很深刻,因为你已经在第二章中学习了答案。在考虑解决方案时,请参考图 11-1。

图 11-2. 最小二乘法的直观理解
简单示例中的广义线性模型(GLM)
在下一章节中,你将看到几个实际数据的示例;在这里,我想集中讨论一个使用虚假数据的简单示例。这些虚假数据来自一个虚构的实验,我在其中随机调查了 20 位虚构的学生,并询问了他们参加过的在线课程数量以及他们对生活的整体满意度。⁴
表 11-3 展示了数据矩阵的前 4 行(共 20 行)。
表 11-3. 数据表
| Number of courses | Life happiness |
|---|---|
| 4 | 25 |
| 12 | 54 |
| 3 | 21 |
| 14 | 80 |
通过散点图更容易将数据可视化,你可以在图 11-3 中看到它。

图 11-3. 来自虚构调查的虚假数据
注意,独立变量在x轴上绘制,而因变量在y轴上绘制,这是统计学中的常见惯例。
我们需要创建设计矩阵。因为这是一个简单的模型,只有一个预测变量,所以我们的设计矩阵实际上只是一个列向量。我们的矩阵方程
以下 Python 代码展示了解决方案。变量numcourses和happiness包含数据;它们都是列表,因此必须转换为多维 NumPy 数组:
X = np.array(numcourses,ndmin=2).T
# compute the left-inverse
X_leftinv = np.linalg.inv(X.T@X) @ X.T
# solve for the coefficients
beta = X_leftinv @ happiness
最小二乘公式告诉我们
![数据续集
图 11-4. 观察数据和预测数据(SSE = 平方误差和)
如果你在观看图 11-4 时感到不安,那是好事——这意味着你在进行批判性思考,并注意到模型在最小化误差方面表现不佳。你可以轻松想象将最佳拟合线的左侧向上推以获得更好的拟合。这里的问题是什么呢?
问题在于设计矩阵不包含截距项。最佳拟合线的方程是y = mx,这意味着当x = 0 时,y = 0。这个约束对这个问题来说是没有意义的——如果任何没有参加我的课程的人完全缺乏生活满意度,那将是一个悲哀的世界。相反,我们希望我们的线条具有y = mx + b的形式,其中b是截距项,允许最佳拟合线在y轴上交叉任意值。截距的统计解释是观察数值的期望值,当预测变量设为零时。
向我们的设计矩阵添加一个截距项会得到以下修改的方程(再次仅显示前四行):
代码不变,唯一的例外是创建设计矩阵:
X = np.hstack((np.ones((20,1)),np.array(numcourses,ndmin=2).T))
现在我们发现β是两元向量[22.9, 3.7]。对于一个零课程的人来说,幸福感的期望水平是 22.9,而每增加一门课程,他们的幸福感将增加 3.7 点。我相信你会同意图 11-5 看起来好多了。SSE 大约是我们排除截距时的一半。

图 11-5。观察和预测数据,现在包含截距项
我会让你自己对这个基于虚假数据的虚假研究结果做出结论;重点是看到如何通过构建适当的设计矩阵并解未知回归器来解决方程组的数值示例。
通过 QR 进行最小二乘法
左逆方法在理论上是合理的,但存在数值不稳定的风险。部分原因是它需要计算矩阵的逆,你已经知道这可能导致数值不稳定。但事实证明,矩阵𝐗T𝐗本身可能会带来困难。将一个矩阵乘以它的转置对诸如范数和条件数等性质有影响。你将在第十四章进一步了解条件数,但我已经提到,条件数高的矩阵可能数值不稳定,因此条件数高的设计矩阵在平方后会变得更不稳定。
QR 分解提供了解决最小二乘问题更稳定的方法。观察以下方程的序列:
这些方程略微简化了实际低级数值实现。例如,
但这里最重要的部分是:不需要转置矩阵
结论是 QR 分解解决了最小二乘问题,无需对
习题 11-3 将引导您完成这个实现。
总结
许多人认为统计学很难是因为其基础数学难度很大。确实,涉及高级数学的高级统计方法存在。但许多常用的统计方法是建立在您现在理解的线性代数原理上的。这意味着您不再有任何理由不精通数据科学中使用的统计分析方法了!
本章的目标是介绍通用线性模型的术语和数学基础,几何解释,以及数学对模型预测数据与观察数据差异最小化的影响。我还展示了一个简单玩具示例中回归的应用。在下一章中,您将看到在真实数据中实现最小二乘法,并看到最小二乘法在回归中的扩展,如多项式回归和正则化。
以下是本章的主要要点:
-
广义线性模型(GLM)是理解我们丰富和美丽宇宙的统计框架。它通过建立一组方程来工作,就像你在上一章中学到的方程组一样。
-
线性代数和统计学术语有所不同;一旦你了解了术语映射,统计学会变得更容易,因为你已经了解了数学。
-
最小二乘方法通过左逆来解方程,是许多统计分析的基础,你经常会在看似复杂的公式中“隐藏”最小二乘解法。
-
最小二乘公式可以通过代数、几何或微积分推导出来。这提供了理解和解释最小二乘的多种方式。
-
将观测数据向量乘以左逆的概念上是考虑最小二乘的正确方法。在实践中,其他方法如 LU 和 QR 分解更加数值稳定。幸运的是,你不需要担心这个,因为 Python 调用低级别库(主要是 LAPACK),这些库实现了最数值稳定的算法。
代码练习
- 练习 11-1。
我解释了残差与预测数据正交(换句话说,

图 11-6. 练习 11-1 的解答
- 练习 11-2。
模型预测的幸福感仅仅是线性组合设计矩阵列的一种方式。但是残差向量不仅仅是与该线性加权组合正交;相反,残差向量正交于设计矩阵张成的整个子空间。在 Python 中演示这一点(提示:考虑左零空间和秩)。
- 练习 11-3。
现在,你将通过 QR 分解计算最小二乘法,正如我在 “最小二乘法 via QR” 中所解释的那样。特别是,计算并比较以下解法:(1) 左逆
按以下方式打印出三种方法的 beta 参数。 (将小数点后的结果四舍五入到三位数是一个额外的编码挑战。)
Betas from left-inverse:
[23.13 3.698]
Betas from QR with inv(R):
[23.13 3.698]
Betas from QR with back substitution:
[[23.13 3.698]]
最后,按以下方式打印 QR 方法生成的结果矩阵:
Matrix R:
[[ -4.472 -38.237]
[ 0. 17.747]]
Matrix R|Q'y:
[[ -4.472 -38.237 -244.849]
[ 0. 17.747 65.631]]
Matrix RREF(R|Q'y):
[[ 1. 0. 23.13 ]
[ 0. 1. 3.698]]
练习 11-4.
异常值 是不寻常或不典型的数据值。异常值可能会导致统计模型中的显著问题,因此它们可能会给数据科学家带来头痛。在这个练习中,我们将在幸福数据中创建异常值,以观察对最小二乘解的影响。
在数据向量中,将第一个观察数据点从 70 改为 170 (模拟数据输入错误)。然后重新计算最小二乘拟合并绘制数据。重复这种异常值模拟,但将最后一个数据点从 70 改为 170 (并将第一个数据点恢复为其原始值 70)。通过创建类似于 图 11-7 的可视化来与原始数据进行比较。

图 11-7. 练习 11-4 的解答
有趣的是,异常值在结果变量中是相同的(在两种情况下,70 变成了 170),但由于相应的 x 轴数值,它们对模型拟合数据的影响却有很大差异。这种异常值对模型拟合的不同影响被称为 杠杆效应,需要在更深入讨论统计学和模型拟合时学习。
练习 11-5.
在这个练习中,您将使用最小二乘法计算矩阵的逆,按照我在前一章节中介绍的解释进行。我们将考虑方程式
您将通过三种方式计算for循环中执行。其次,使用左逆方法在一行代码中计算整个np.linalg.inv()计算
观察:使用np.linalg.inv进行比较,还可以说明计算左逆时可能出现的数值不准确性。

图 11-8. 练习 11-5 的解答
¹ 诚然,我从未在塔吉克斯坦广告牌上看到这个方程式,但重点是要保持开放的心态。
² 如果你在想,也可以将绝对距离最小化,而不是平方距离。这两个目标可能导致不同的结果;平方距离的一个优点是方便的导数计算,这导致了最小二乘解。
³ 如果你对矩阵微积分感到不适应,那就不用担心方程式;重点是我们利用链式法则对β进行了导数。
⁴ 如果还不够清楚:这些数据完全是为这个例子而虚构的;任何与现实世界的相似性都是巧合。
第十二章:最小二乘法应用
在本章中,您将看到最小二乘模型拟合在真实数据中的几个应用。在此过程中,您将学习如何使用几种不同——更加数值稳定的 Python 函数实现最小二乘法,并学习统计学和机器学习中的一些新概念,如多重共线性、多项式回归以及网格搜索算法作为最小二乘法的替代方法。
通过本章的学习,您将更深入地了解最小二乘法在应用中的使用,包括在涉及降秩设计矩阵的“困难”情况中,数值稳定算法的重要性。您将看到,最小二乘法提供的解析解优于经验参数搜索方法。
基于天气预测自行车租赁
我是自行车和韩国拌饭(一道用米饭、蔬菜或肉类制作的韩国菜)的铁杆粉丝。因此,我很高兴在首尔找到了一个公开可用的关于自行车租赁的数据集。¹ 这个数据集包含了近九千条数据观测,涉及城市中租赁自行车数量以及与天气相关的变量,如温度、湿度、降雨量、风速等等。
数据集的目的是基于天气和季节预测共享单车的需求。这一点非常重要,因为它将帮助自行车租赁公司和地方政府优化更健康的交通方式的可用性。这是一个很棒的数据集,有很多可以做的事情,我鼓励您花时间探索它。在本章中,我将重点介绍基于少数特征预测自行车租赁数量的相对简单的回归模型建立。
虽然这是一本关于线性代数而非统计学的书籍,但在应用和解释统计分析之前仍然重要仔细检查数据。在线代码有关使用 pandas 库导入和检查数据的详细信息。图 12-1 展示了自行车计数租赁数据(因变量)和降雨量(其中一个自变量)。

图 12-1 散点图展示了一些数据
请注意,降雨量是一个稀疏变量——大部分是零,只有少量非零值。我们将在练习中再次回顾这一点。
图 12-2 展示了四个选定变量的相关矩阵。在开始统计分析之前检查相关矩阵总是个好主意,因为它将显示哪些变量(如果有的话)相关,并且可以显示数据中的错误(例如,如果两个看似不同的变量完全相关)。在这种情况下,我们看到自行车租赁次数与小时和温度呈正相关(人们在一天后期和天气更暖时租更多自行车),与降雨量呈负相关。(请注意,我这里没有展示统计显著性,因此这些解释是定性的。)

图 12-2. 四个选定变量的相关矩阵
在第一次分析中,我想基于降雨量和季节来预测自行车租赁次数。季节(冬季、春季、夏季、秋季)是数据集中的文本标签,我们需要将它们转换为数字进行分析。我们可以将四季转换为数字 1–4,但季节是循环的,而回归是线性的。有几种处理方式,包括使用方差分析(ANOVA)代替回归,使用独热编码(在深度学习模型中使用),或者对季节进行二值化处理。我将采取后者的方法,并将秋季和冬季标记为“0”,春季和夏季标记为“1”。解释是,正的 beta 系数表明春夏季节的自行车租赁次数比秋冬季节要多。
(旁注:一方面,我本可以通过选择仅连续变量来简化事情。但是我想强调的是,数据科学不仅仅是将公式应用于数据集;有许多非平凡的决策会影响您可以进行的分析类型,从而影响您可以获得的结果。)
图 12-3 的左侧展示了设计矩阵的图像化表示。这是设计矩阵的常见表示方式,因此请确保您能够舒适地解释它。列是回归器,行是观察结果。如果回归器处于非常不同的数值尺度,有时会对列进行归一化以便于视觉解释,尽管我在这里没有这样做。您可以看到降雨很稀疏,并且数据集跨越了两个秋冬季节(中间列的黑色区域)和一个春夏季节(中间的白色区域)。截距当然是纯白色,因为它对每个观察结果都采用相同的值。

图 12-3. 设计矩阵和一些数据
图 12-3 的右侧显示了数据,将降雨量和租赁自行车分别绘制在两个季节上。显然,数据不在一条直线上,因为两个坐标轴上都有许多接近或等于零的值。换句话说,通过视觉检查数据表明变量之间的关系是非线性的,这意味着线性建模方法可能不够优化。再次强调,视觉检查数据和仔细选择合适的统计模型的重要性。
尽管如此,我们将使用最小二乘法拟合的线性模型继续前进。以下代码展示了我如何创建设计矩阵(变量data是一个 pandas 数据帧):
# Create a design matrix and add an intercept
desmat = data[['Rainfall(mm)','Seasons']].to_numpy()
desmat = np.append(desmat,np.ones((desmat.shape[0],1)),axis=1)
# extract DV
y = data[['Rented Bike Count']].to_numpy()
# fit the model to the data using least squares
beta = np.linalg.lstsq(desmat,y,rcond=None)
降雨量和季节的β值分别为−80 和 369。这些数字表明,下雨时自行车租赁较少,春季/夏季比秋季/冬季租赁更多。
图 12-4 显示了预测与观察数据的对比,分别针对两个季节。如果模型完全拟合数据,点应该在具有斜率 1 的对角线上。显然,情况并非如此,这意味着模型并未很好地拟合数据。事实上,R²仅为 0.097(换句话说,统计模型解释数据变异约为 1%)。此外,你可以看到模型预测出了负自行车租赁数量,这是无法解释的——自行车租赁数量严格来说是非负数。

图 12-4。预测与观察数据的散点图
到目前为止,在代码中我们没有收到任何警告或错误;在数学或编码方面我们没有做错任何事情。然而,我们使用的统计模型并不适合这个研究问题。你将有机会在练习 12-1 和练习 12-2 中改进它。
使用 statsmodels 的回归表
不深入统计学,我想向你展示如何使用 statsmodels 库创建回归表。该库使用 pandas 数据帧而不是 NumPy 数组。以下代码展示了如何设置和计算回归模型(OLS 代表普通最小二乘法):
import statsmodels.api as sm
# extract data (staying with pandas dataframes)
desmat_df = data[['Rainfall(mm)','Seasons']]
obsdata_df = data['Rented Bike Count']
# create and fit the model (must explicitly add intercept)
desmat_df = sm.add_constant(desmat_df)
model = sm.OLS(obsdata_df,desmat_df).fit()
print( model.summary() )
回归表包含大量信息。如果你不理解全部内容,没关系;你可以查看的关键项目是R²和回归系数(coef):
==============================================================================
Dep. Variable: Rented Bike Count R-squared: 0.097
Model: OLS Adj. R-squared: 0.097
Method: Least Squares F-statistic: 468.8
Date: Wed, 26 Jan 2022 Prob (F-statistic): 3.80e-194
Time: 08:40:31 Log-Likelihood: -68654.
No. Observations: 8760 AIC: 1.373e+05
Df Residuals: 8757 BIC: 1.373e+05
Df Model: 2
Covariance Type: nonrobust
================================================================================
coef std err t P>|t| [0.025 0.975]
================================================================================
const 530.4946 9.313 56.963 0.000 512.239 548.750
Rainfall(mm) -80.5237 5.818 -13.841 0.000 -91.928 -69.120
Seasons 369.1267 13.127 28.121 0.000 343.395 394.858
==============================================================================
Omnibus: 1497.901 Durbin-Watson: 0.240
Prob(Omnibus): 0.000 Jarque-Bera (JB): 2435.082
Skew: 1.168 Prob(JB): 0.00
Kurtosis: 4.104 Cond. No. 2.80
==============================================================================
多重共线性
如果你学过统计课程,可能听说过多重共线性这个术语。维基百科的定义是:“多元回归模型中的一个预测变量可以通过其他变量进行线性预测,并且预测精度相当高。”²
这意味着设计矩阵中存在线性依赖关系。在线性代数中,多重共线性只是线性依赖的一个花哨术语,意思是设计矩阵是降秩的或者是奇异的。
降秩的设计矩阵没有左逆,这意味着无法通过解析方法解决最小二乘问题。你将在 Exercise 12-3 中看到多重共线性的影响。
正则化
正则化是一个统称,指的是通过各种方式修改统计模型,以提高数值稳定性,将奇异或病态矩阵转换为满秩(从而可逆),或通过减少过拟合来提高泛化能力的目的。根据问题的性质和正则化的目标,有几种形式的正则化;你可能听说过的一些具体技术包括岭回归(又称 L2)、Lasso 回归(又称 L1)、Tikhonov 和收缩法。
不同的正则化技术有不同的工作方式,但许多正则化器通过某种程度的“移动”设计矩阵来实现。你可能还记得从 Chapter 5 中,移动矩阵意味着在对角线上添加某个常数,如
在本章中,我们将根据其 Frobenius 范数的某比例来对设计矩阵进行正则化。这修改了最小二乘解方程式 12-1。
方程式 12-1. 正则化
关键参数是
正则化最明显的效果是,如果设计矩阵是降秩的,那么正则化后的平方设计矩阵将是满秩的。正则化还会降低条件数,这是矩阵信息“分布”范围的度量(它是最大和最小奇异值的比值;你将在 Chapter 14 学到更多)。这提高了矩阵的数值稳定性。正则化的统计含义是通过减少模型对可能是异常值或非代表性数据点的敏感性来“平滑”解决方案,因此更不可能在新数据集中观察到。
为什么要按 Frobenius 范数的平方缩放?考虑指定的值
实际上,通常使用设计矩阵的特征值平均值而不是 Frobenius 范数。在学习第十三章中的特征值后,您将能够比较这两种正则化方法。
在代码中实现正则化是练习 12-4 的重点。
多项式回归
多项式回归类似于普通回归,但独立变量是x轴值的高次幂。也就是说,设计矩阵的第i列定义为x^i,其中x通常是时间或空间,但也可以是其他变量,如药物剂量或人口。数学模型如下所示:
请注意,
多项式的次数是最高次幂i。例如,一个四阶多项式回归包含高达
图 12-5 显示了三阶多项式的单个回归器和设计矩阵的示例(请记住,一个n次多项式包括拦截器在内的n + 1 个回归器)。多项式函数是建模观测数据的基础向量。

图 12-5. 多项式回归的设计矩阵
除了特殊的设计矩阵外,多项式回归与任何其他回归完全相同:使用左逆(或更稳定的计算替代方案)获得一组系数,使得回归器的加权组合(即预测数据)最能匹配观察数据。
多项式回归用于曲线拟合和逼近非线性函数。应用包括时间序列建模、人口动态、医学研究中的剂量-反应函数以及结构支撑梁的物理应力。多项式也可以用于表达二维结构,用于模拟地震传播和脑活动等空间结构。
背景足够了。让我们通过一个示例来工作。我选择的数据集来自人口翻倍模型。问题是“人类的人口翻倍需要多长时间(例如,从五亿到十亿)?”如果人口增长率本身正在增加(因为更多的人有更多的孩子,这些孩子长大后又会有更多的孩子),那么每次翻倍的时间将会减少。另一方面,如果人口增长放缓(人们生孩子的数量减少),那么连续翻倍的时间将会增加。
我在网上找到了一个相关的数据集。³ 这是一个小数据集,因此所有数字都可以在在线代码中找到,并显示在图 12-6 中。该数据集包括实际测量数据和对 2100 年的预测。这些对未来的预测基于多种假设,没有人确切知道未来会如何发展(这就是为什么你应该在未来的准备和享受当下之间找到平衡)。尽管如此,迄今数据显示,过去 500 年中人口的翻倍频率逐渐增加(至少如此),数据集的作者预测,这种翻倍速率在未来一个世纪内将略微增加。

图 12-6. 数据图
我选择了三阶多项式来拟合数据,并使用以下代码创建和拟合了模型(变量year包含x轴坐标,变量doubleTime包含依赖变量):
# design matrix
X = np.zeros((N,4))
for i in range(4):
X[:,i] = np.array(year)**i
# fit the model and compute the predicted data
beta = np.linalg.lstsq(X,doubleTime, rcond=None)
yHat = X@beta[0]
图 12-7 显示了使用该代码创建的多项式回归预测数据。

图 12-7. 数据图
该模型捕捉到了数据中的下降趋势和预期的上升趋势。 没有进一步的统计分析,我们不能说这是最佳模型或者说该模型在统计上显著拟合数据。 但是很明显,多项式回归非常适合拟合曲线。 你将继续探索这个模型和练习 12-5 中的数据,但我鼓励你通过测试不同的阶参数来玩弄生成图 12-7 的代码。
多项式回归通常被使用,并且 NumPy 有专用函数来创建和拟合这样的模型:
beta = np.polyfit(year,doubleTime,3) # 3rd order
yHat = np.polyval(beta,year)
用网格搜索找到模型参数
最小二乘法通过左逆来精确地将模型拟合到数据。 最小二乘法准确、快速且确定性(这意味着每次重新运行代码时,你都会得到相同的结果)。 但它仅适用于线性模型拟合,而不是所有模型都可以使用线性方法拟合。
在这一节中,我将向你介绍另一种用于识别模型参数的优化方法,称为网格搜索。 网格搜索通过对参数空间进行采样,计算每个参数值对数据的模型拟合,并选择给出最佳模型拟合的参数值。
作为一个简单的例子,让我们考虑函数
在网格搜索技术中,我们从一个预定义的
但是,网格搜索不能保证给出最优解。例如,假设我们的网格是(−2, −0.5, 1, 2.5);函数值将为y = (4, 0.25, 1, 6.25),我们将得出结论
点在于范围和分辨率(格点之间的间距)都很重要,因为它们决定你是否会得到最佳解、相当不错的解或者糟糕的解。在上述玩具示例中,适当的范围和分辨率很容易确定。在复杂的、多变量的、非线性模型中,适当的网格搜索参数可能需要更多的工作和探索。
我在上一章的“快乐学生”数据上进行了网格搜索(提醒一下:这是来自虚假调查的虚假数据,显示出参加我的课程更多的人生活满意度更高)。这些数据的模型有两个参数——截距和斜率——因此,我们在可能的参数对的二维网格上评估该函数。结果显示在 图 12-8 中。
那个图表意味着什么,我们如何解释它?两个轴对应于参数的值,因此该图中的每个坐标创建了具有对应参数值的模型。然后,计算并存储每个模型对数据的拟合,并将其可视化为图像。
最适合数据的坐标(平方误差之和最小的)是最优参数集。图 12-8 也展示了使用最小二乘法的解析解。它们接近但不完全重叠。在 练习 12-6 中,您将有机会实现这个网格搜索,并探索网格分辨率对结果准确性的影响。

图 12-8. 在“快乐学生”数据集上进行网格搜索的结果。强度显示了拟合到数据的平方误差之和。
当最小二乘法更好且更快时,为什么会有人使用网格搜索呢?嗯,当最小二乘法是可行解决方案时,您永远不应该使用网格搜索方法。网格搜索是一种寻找非线性模型参数的有用技术,例如,用于识别深度学习模型中的超参数(超参数是研究人员选择的模型架构设计特征,而不是从数据中学习的)。对于大型模型来说,网格搜索可能是耗时的,但并行化可以使网格搜索变得更加可行。
结论是,当无法应用线性方法时,网格搜索是将模型拟合到数据的非线性方法。在您学习数据科学和机器学习的过程中,您还将了解到其他非线性方法,包括单纯形和支持深度学习的著名梯度下降算法。
总结
希望您喜欢阅读关于最小二乘法应用及与其他模型拟合方法比较的内容。以下练习是本章最重要的部分,因此我不想用长篇大论占用您的时间。以下是重点:
-
对数据进行视觉检查对于选择正确的统计模型和正确解释统计结果非常重要。
-
线性代数用于对数据集进行定量评估,包括相关矩阵。
-
您在第五章中学到的矩阵可视化方法对于检查设计矩阵非常有用。
-
数学概念有时在不同领域会有不同的名称。本章的一个例子是多重共线性,指的是设计矩阵中的线性依赖关系。
-
正则化涉及将设计矩阵“移动”一小部分,这可以增加数值稳定性并增加泛化新数据的可能性。
-
对线性代数有深入理解可以帮助您选择最合适的统计分析方法,解释结果,并预测可能出现的问题。
-
多项式回归与“常规”回归相同,但设计矩阵中的列定义为升高到不同幂的x轴值。多项式回归用于曲线拟合。
-
网格搜索是模型拟合的非线性方法。当模型是线性的时,最小二乘法是最优的方法。
代码练习
自行车租赁练习
练习 12-1.
或许在图 12-4 中负自行车租赁的问题部分可以通过消除无降雨日来缓解。重复分析并绘制这种分析的图表,但只选择具有零降雨的数据行。结果是否改善,以更高的R²和正预测租赁数量为评判标准?
练习 12-2.
因为 seasons 是一个分类变量,ANOVA 实际上比回归更合适作为统计模型。也许二元化的 seasons 缺乏预测自行车租赁的敏感性(例如,秋天可能有温暖的晴天和春天可能有寒冷的雨天),因此 temperature 可能是一个更好的预测变量。
在设计矩阵中用 temperature 替换 seasons 并重新运行回归(可以使用所有天数,而不仅仅是上一个练习中的无降雨天数),并重现 图 12-9。仍然存在预测负租赁的问题(这是因为模型的线性性),但 R² 更高,预测看起来质量更好。

图 12-9. 练习 12-2 的结果
多重共线性练习
练习 12-3.
本练习延续了来自 练习 12-2 的模型。这个模型包含三个回归变量,包括截距项。创建一个新的设计矩阵,其中包含作为 temperature 和 rainfall 的某种线性加权组合定义的第四个回归变量。给这个设计矩阵起一个不同的变量名,因为你将在下一个练习中用到它。确认设计矩阵有四列,但秩为 3,并计算设计矩阵的相关矩阵。
请注意,根据这两个变量的权重,即使存在线性依赖,你也不应该期望得到 1 的相关性;在这里,也不应该期望重现确切的相关性:
Design matrix size: (8760, 4)
Design matrix rank: 3
Design matrix correlation matrix:
[[1. 0.05028 nan 0.7057 ]
[0.05028 1. nan 0.74309]
[ nan nan nan nan]
[0.7057 0.74309 nan 1. ]]
使用三种不同的编码方法拟合模型:(1) 直接实现,使用你在前一章学到的左逆,(2) 使用 NumPy 的 lstsqr 函数,以及 (3) 使用 statsmodels。对于这三种方法,计算 R² 和回归系数。按如下格式打印结果。在一个降秩设计矩阵上,np.linalg.inv 的数值不稳定性显而易见。
MODEL FIT TO DATA:
Left-inverse: 0.0615
np lstsqr : 0.3126
statsmodels : 0.3126
BETA COEFFICIENTS:
Left-inverse: [[-1528.071 11.277 337.483 5.537 ]]
np lstsqr : [[ -87.632 7.506 337.483 5.234 ]]
statsmodels : [ -87.632 7.506 337.483 5.234 ]
附注:Python 没有给出任何错误或警告消息;即使设计矩阵明显有问题,Python 也只是简单地给出了输出。我们可以讨论这样做的优点,但这个例子再次突显了理解数据科学的线性代数的重要性,良好的数据科学不仅仅是了解数学。
正则化练习
练习 12-4.
在这里,您将探索正则化对您在上一个练习中创建的降秩设计矩阵的影响。首先,实现
inv(X'X + 0.0*I) size: (4, 4)
inv(X'X + 0.0*I) rank: 2
inv(X'X + 0.01*I) size: (4, 4)
inv(X'X + 0.01*I) rank: 4
现在进行实验。这里的目标是探索正则化对模型对数据的拟合效果的影响。编写代码,使用带有和不带有多重共线性的设计矩阵的最小二乘法计算数据的拟合 R²。将该代码放入一个 for 循环中,实现从 0 到 .2 的一系列
顺便说一句,对于完全秩的设计矩阵来说,随着正则化的增加,模型的拟合程度会降低—事实上,正则化的目的是使模型对数据的敏感性降低。重要的问题是正则化是否改善了对测试数据集或在拟合模型时排除的验证折叠的拟合。如果正则化是有益的,您预期正则化模型的泛化能力会在某个

Figure 12-10. Exercise 12-4 的结果
多项式回归练习
Exercise 12-5.
这个练习的目的是使用从零到九的一系列阶数拟合多项式回归。在 for 循环中,重新计算回归和预测数据值。展示类似于 Figure 12-11 的结果。

Figure 12-11. Exercise 12-5 的结果
这个练习突出了欠拟合和过拟合的问题。参数过少的模型在预测数据时表现不佳。另一方面,参数过多的模型过度拟合数据,并且风险过于敏感于噪声并且无法泛化到新数据。寻找欠拟合和过拟合之间平衡的策略包括交叉验证和贝叶斯信息准则;这些是您在机器学习或统计书籍中学习的主题。
网格搜索练习
练习 12-6.
这里的目标很简单:按照围绕图表周围给出的指示重现 Figure 12-8。打印出回归系数以进行比较。例如,使用网格分辨率参数设置为 50 得到了以下结果:
Analytic result:
Intercept: 23.13, slope: 3.70
Empirical result:
Intercept: 22.86, slope: 3.67
一旦您有了可运行的代码,请尝试几个不同的分辨率参数。我使用分辨率为 100 制作了 Figure 12-8;您还应尝试其他值,例如 20 或 500。同时注意更高分辨率值的计算时间——这只是一个双参数模型!对于一个 10 参数模型的详尽高分辨率网格搜索极其计算密集。
练习 12-7.
您已经了解了两种不同的方法来评估模型对数据的拟合度:平方误差和 R²。在前一个练习中,您使用了平方误差来评估模型对数据的拟合度;在这个练习中,您将确定 R² 是否同样可行。
这个练习的编码部分很简单:修改上一个练习的代码,计算 R² 而不是 SSE(确保修改代码的副本而不是覆盖上一个练习的代码)。
现在是具有挑战性的部分:您会发现 R² 很糟糕!它给出了完全错误的答案。您的任务是找出其中的原因(在线代码解决方案中包含了对这一点的讨论)。提示:存储每个参数对的预测数据,以便您可以检查预测值,然后与观察数据进行比较。
¹ V E Sathishkumar, Jangwoo Park 和 Yongyun Cho,“Using Data Mining Techniques for Bike Sharing Demand Prediction in Metropolitan City,” Computer Communications, 153, (2020 年 3 月): 353–366,数据下载自 https://archive.ics.uci.edu/ml/datasets/Seoul+Bike+Sharing+Demand.
² Wikipedia,s.v. “multicollinearity”,https://en.wikipedia.org/wiki/Multicollinearity.
³ Max Roser, Hannah Ritchie 和 Esteban Ortiz-Ospina,“World Population Growth,” OurWorldInData.org, 2013,https://ourworldindata.org/world-population-growth.
⁴ 如果你遇到 Python 错误,可能需要重新运行之前的代码,然后重新创建设计矩阵变量。
第十三章:特征分解
特征分解是线性代数中的一颗明珠。那么,什么是明珠呢?让我直接引用《海底两万里》中的一段话:
对于诗人来说,珍珠是海洋之泪;对东方人来说,它是凝固了的露珠滴;对女士们来说,它是可以戴在手指、颈部和耳朵上的珠宝,呈椭圆形,具有玻璃般的光泽,由珍珠母形成;对化学家来说,它是一种含有少量明胶蛋白的磷酸钙和碳酸钙的混合物;最后,对于自然学家来说,它是某些双壳类动物产生母珍珠的简单腐败分泌物。
朱尔·凡尔纳
关键在于,相同的对象可以根据其用途以不同方式看待。特征分解也是如此:特征分解有几何解释(旋转不变性的轴)、统计解释(最大协方差的方向)、动力系统解释(稳定系统状态)、图论解释(节点在网络中的影响)、金融市场解释(识别共变的股票)等等。
特征分解(以及奇异值分解,正如你将在下一章学到的那样,与特征分解密切相关)是线性代数对数据科学的最重要贡献之一。本章的目的是为你提供特征值和特征向量的直观理解——矩阵特征分解的结果。在这过程中,你将学到对称矩阵的对角化和更多特殊性质。在第十四章中扩展到奇异值分解后,你将看到特征分解在第十五章中的几个应用。
特征值和特征向量的解释
有几种解释特征值/向量的方式,我将在接下来的部分中描述。当然,无论如何,数学都是相同的,但多视角可以促进直觉,进而帮助你理解为什么特征分解在数据科学中如此重要。
几何学
我实际上已经在第五章中向你介绍了特征向量的几何概念。在图 5-5 中,我们发现有一种特殊的矩阵和向量的组合,使得矩阵拉伸了这个向量,但没有旋转。那个向量是矩阵的特征向量,而拉伸的程度是特征值。
图 13-1 展示了在
这就是几何图像:特征向量意味着矩阵-向量乘法的效果类似于标量-向量乘法。让我们看看能否用方程表示出来(我们可以,并且它在 方程式 13-1 中打印出来)。
方程式 13-1. 特征值方程
在解释这个方程时要小心:它并不是说矩阵等于标量;它说的是矩阵对向量的作用与标量对同一向量的作用是相同的。

图 13-1. 特征向量的几何形状
这被称为特征值方程,它是线性代数中另一个值得记忆的关键公式。在本章中你会看到它,接下来的章节中会看到稍微变化的形式,以及在学习多变量统计、信号处理、优化、图论以及许多其他应用中会看到它,这些应用中跨多个同时记录的特征识别出模式。
统计学(主成分分析)
人们应用统计的原因之一是识别和量化变量之间的关系。例如,全球温度上升与海盗数量下降相关,² 但这种关系有多强?当然,当你只有两个变量时,像你在 第四章 中学到的简单相关就足够了。但在包含数十个或数百个变量的多变量数据集中,双变量相关性无法揭示全局模式。
让我们通过一个例子来具体化这个概念。加密货币是数字价值存储,编码在区块链中,这是一种跟踪交易的系统。你可能听说过比特币和以太坊;还有成千上万其他具有各种目的的加密币。我们可以询问整个加密空间是否作为一个单一系统运作(意味着所有币值同时上下波动),或者在该空间内是否存在独立的子类别(意味着一些币或一些币组独立于其他币的价值变化)。
我们可以通过对包含各种加密货币价格的数据集执行主成分分析来测试这个假设。如果整个加密市场像一个单一实体运作,那么屏风图(数据集协方差矩阵特征值的图表)将显示一个成分占系统方差的大部分,而所有其他成分则占极少的方差(图 A 在图 13-2 中)。相反,如果加密市场有三个主要子类别具有独立的价格变动,则我们预计会看到三个较大的特征值(图 B 在图 13-2 中)。

图 13-2. 模拟多变量数据集的屏风图(数据模拟以说明结果可能性)
噪声减少
大多数数据集包含噪声。噪声是指数据集中未解释的(例如随机变化)或不需要的(例如无线电信号中的电气线噪声伪影)方差。有许多方式可以减弱或消除噪声,最佳的噪声减少策略取决于噪声的性质和来源,以及信号的特征。
减少随机噪音的一种方法是识别系统的特征值和特征向量,并在数据空间中与小特征值相关联的方向上“投影出”这些特征。假设随机噪声对总方差的贡献相对较小。在设置一些低于某个阈值的特征值为零后,"投影出"数据维度意味着重建数据集。
您将在第十五章中看到使用特征分解来减少噪音的示例。
维度降低(数据压缩)
像电话、互联网和电视这样的信息通信技术创造和传输大量数据,如图片和视频。传输数据可能耗时且昂贵,因此在传输之前进行压缩是有益的。压缩意味着减少数据的大小(以字节为单位),同时对数据质量影响最小。例如,TIFF 格式的图像文件可能为 10 MB,而转换为 JPG 格式后的版本可能只有 0.1 MB,同时保持相对良好的质量。
降低数据集维度的一种方法是进行其特征分解,舍弃与数据空间中小方向相关的特征值和特征向量,然后仅传输相对较大的特征向量/值对。实际上,使用 SVD 进行数据压缩更为常见(您将在第十五章中看到一个例子),尽管原理是相同的。
现代数据压缩算法实际上比先前描述的方法更快更高效,但思想是相同的:将数据集分解为捕获数据最重要特征的一组基向量,然后重建原始数据的高质量版本。
查找特征值
要对一个方阵进行特征分解,首先找到特征值,然后使用每个特征值找到其对应的特征向量。特征值就像是你插入矩阵中以解锁神秘特征向量的钥匙。
在 Python 中查找矩阵的特征值非常容易:
matrix = np.array([
[1,2],
[3,4]
])
# get the eigenvalues
evals = np.linalg.eig(matrix)[0]
两个特征值(四舍五入到最接近的百分之一)分别是−0.37 和 5.37。
但重要的问题不是哪个函数返回特征值;相反,重要的问题是如何识别矩阵的特征值?
要找到矩阵的特征值,我们从 Equation 13-1 中显示的特征值方程式开始,并进行一些简单的算术操作,如 Equation 13-2 所示。
方程式 13-2。特征值方程式,重新组织
第一个方程式完全重复了特征值方程式。在第二个方程式中,我们简单地减去右侧以使方程式等于零向量。
从第二个方程式到第三个方程式的过渡需要一些解释。第二个方程式的左侧有两个向量项,都涉及
第三个方程式是什么意思?它意味着特征向量位于其特征值平移的矩阵的零空间中。
如果这有助于你理解特征向量作为矩阵平移后的零空间向量的概念,你可以考虑添加两个额外的方程式:
为什么这个陈述如此具有洞察力?请记住,在线性代数中我们忽略平凡解,因此我们不认为
还有关于奇异矩阵我们知道什么?我们知道它们的行列式为零。因此:
信不信由你,这就是找到特征值的关键:将矩阵平移至未知特征值
你可以应用二次方程式来求解两个
-
矩阵-向量乘法的作用类似标量-向量乘法(特征值方程)。
-
我们将特征值方程设为零向量,并提取公共项。
-
这揭示了特征向量位于被特征值移位的矩阵的零空间中。我们不认为零向量是特征向量,这意味着移位矩阵是奇异的。
-
因此,我们将移位矩阵的行列式设为零,并求解未知的特征值。
特征值移位矩阵的行列式设为零被称为矩阵的特征多项式。
注意,在前面的例子中,我们从一个
匹配的 2 并非巧合:一个
繁琐的练习问题
在传统线性代数教材中的这一点上,你会被要求手动找出数十个
寻找特征向量
和特征值一样,在 Python 中找到特征向量是非常容易的:
evals,evecs = np.linalg.eig(matrix)
print(evals), print(evecs)
[-0.37228132 5.37228132]
[[-0.82456484 -0.41597356]
[ 0.56576746 -0.90937671]]
特征向量位于矩阵evecs的列中,并且与特征值的顺序相同(即,矩阵evecs的第一列中的特征向量与向量evals中的第一个特征值配对)。我喜欢使用变量名evals和evecs,因为它们简短而有意义。你可能还会看到人们使用变量名L和V或D和V。L代表V代表D代表对角线,因为特征值通常存储在对角线矩阵中,我稍后将在本章解释原因。
特征向量在列中,而不是行中!
编码时关于特征向量最重要的事情是它们存储在矩阵的列中,而不是行中!在使用方阵时很容易犯这种维度索引错误(因为可能不会收到 Python 错误),但是在应用中意外地使用特征向量矩阵的行而不是列可能会产生灾难性后果。如有疑问,请记住第二章中的讨论,线性代数中的常见约定是假定向量是列向量。
好的,但再次强调,前面的代码展示了如何使 NumPy 函数返回矩阵的特征向量。你可以从np.linalg.eig的文档字符串中学到这一点。重要的问题是特征向量来自哪里,我们如何找到它们?
实际上,我已经写过如何找到特征向量:找到矩阵通过
让我们看一个数值例子。以下是一个矩阵及其特征值:
让我们专注于第一个特征值。为了揭示其特征向量,我们将矩阵移位 3,并找到其零空间中的向量:
这意味着[1 1]是与特征值为 3 的矩阵相关联的特征向量。
我只是通过查看矩阵找到了零空间向量。在实践中如何识别零空间向量(即矩阵的特征向量)?
通过使用高斯-约旦方法解方程组可以找到零空间向量,其中系数矩阵是
特征向量的符号和缩放不确定性
让我回到前一节中的数值示例。我写道[1 1]是矩阵的特征向量,因为该向量是矩阵移位后的零空间的基础。
回顾一下移位后的矩阵,并问自己,[1 1]是零空间的唯一可能的基向量吗?一点也不!你还可以使用[4 4]或[-5.4 -5.4]或……我想你明白这是什么意思:向量[1 1]的任何缩放版本都是该零空间的基础。换句话说,如果
的确,特征向量之所以重要是因为它们的方向,而不是它们的大小。
无限多的零空间基向量引发了两个问题:
-
有一个“最佳”的基向量吗? 实际上没有一个“最佳”的基向量,但对于对称矩阵来说,拥有单位规范化(欧几里得范数为 1)的特征向量是方便的,这将在本章后面的内容中解释。⁴
-
特征向量的“正确”符号是什么? 没有确定的符号。事实上,当使用不同版本的 NumPy 以及不同的软件如 MATLAB、Julia 或 Mathematica 时,可以从同一个矩阵中获得不同的特征向量符号。特征向量符号的不确定性只是我们宇宙生活的一个特征。在诸如 PCA 之类的应用中,有一些原则性的方法可以分配符号,但这只是促进解释的常见约定。
对角化一个方阵
现在您熟悉的特征值方程列出了一个特征值和一个特征向量。这意味着一个
那系列方程组其实没什么错,但有点丑陋,而丑陋违反了线性代数的一个原则:使方程组简洁而优雅。因此,我们将这系列方程转换成一个矩阵方程。
写出矩阵特征值方程的关键洞见在于,特征向量矩阵的每一列都被恰好一个特征值所缩放。我们可以通过后乘对角矩阵来实现这一点(正如您在第六章中学到的)。
因此,我们不是将特征值存储在向量中,而是将特征值存储在矩阵的对角线上。以下方程显示了对于一个
请花点时间确认每个特征值如何缩放其对应的特征向量的所有元素,而不是其他任何特征向量。
更一般地说,矩阵特征值方程(即对方阵的对角化)是:
NumPy 的 eig 函数返回一个矩阵中的特征向量和一个向量中的特征值。这意味着在 NumPy 中对矩阵进行对角化需要额外的代码:
evals,evecs = np.linalg.eig(matrix)
D = np.diag(evals)
顺便说一句,在数学中通过解不同变量来重新排列方程通常很有趣和深刻。考虑以下等价声明列表:
第二个方程表明矩阵
对称矩阵的特殊优越性
你已经从前几章了解到,对称矩阵具有使它们易于处理的特殊属性。现在,你准备学习与特征分解相关的另外两个特殊属性。
正交特征向量
对称矩阵具有正交特征向量。这意味着对称矩阵的所有特征向量都是成对正交的。让我从一个例子开始,然后我将讨论特征向量正交的含义,最后我将展示证明:
# just some random symmetric matrix
A = np.random.randint(-3,4,(3,3))
A = A.T@A
# its eigendecomposition
L,V = np.linalg.eig(A)
# all pairwise dot products
print( np.dot(V[:,0],V[:,1]) )
print( np.dot(V[:,0],V[:,2]) )
print( np.dot(V[:,1],V[:,2]) )
三个点积都为零(在计算机舍入误差为 10^(−16) 的数量级内)。 (注意,我已经创建了对称矩阵作为一个随机矩阵乘以其转置。)
正交特征向量属性意味着任意一对特征向量之间的点积为零,而特征向量与自身的点积不为零(因为我们不认为零向量是特征向量)。这可以写成
但我们可以比对角矩阵做得更好:记住特征向量之所以重要是因为它们的方向,而不是它们的大小。因此,一个特征向量可以有任何我们想要的大小(显然不包括大小为零的情况)。
让我们将所有特征向量缩放到单位长度。对你的问题:如果所有特征向量都是正交的,并且具有单位长度,那么当我们将特征向量矩阵乘以它的转置时会发生什么?
当然你知道答案:
换句话说,对称矩阵的特征向量矩阵是一个正交矩阵!这对数据科学有多重影响,包括特征向量非常容易求逆(因为你只需转置它们)。正交特征向量还有其他应用,比如主成分分析,我将在第十五章中讨论。
我在第一章中写道,本书中的证明相对较少。但对称矩阵的正交特征向量是如此重要的概念,你真的需要看到这个主张的证明。
这个证明的目标是展示任意一对特征向量之间的点积为零。我们从两个假设开始:(1)矩阵
方程式 13-3. 对称矩阵的特征向量正交性证明
中间的项只是变换;注意第一项和最后一项。它们在方程式 13-4 中被重写,然后相减以设为零。
方程式 13-4. 继续特征向量正交性证明…
两个术语都包含点积
方程式 13-5。特征向量正交性证明,第三部分
最终方程表明两个量相乘得到 0,这意味着这两个量中的一个或者两个必须为零。
实数特征值
对称矩阵的第二个特殊性质是它们具有实数特征值(因此具有实数特征向量)。
让我首先展示矩阵——即使所有条目都是实数——也可以具有复数特征值:
A = np.array([[-3, -3, 0],
[ 3, -2, 3],
[ 0, 1, 2]])
# its eigendecomposition
L,V = np.linalg.eig(A)
L.reshape(-1,1) # print as column vector
>> array([[-2.744739 +2.85172624j],
[-2.744739 -2.85172624j],
[ 2.489478 +0.j ]])
(在解释那个 NumPy 数组时要小心;它不是一个 j 和数字之间没有逗号。)
矩阵
我不想详细讨论复值解决方案,除了向您展示复值解决方案对特征分解是直观的。
对称矩阵保证具有实值特征值,因此也有实值特征向量。让我从修改前面的例子开始,使矩阵对称化:
A = np.array([[-3, -3, 0],
[-3, -2, 1],
[ 0, 1, 2]])
# its eigendecomposition
L,V = np.linalg.eig(A)
L.reshape(-1,1) # print as column vector
>> array([[-5.59707146],
[ 0.22606174],
[ 2.37100972]])
这只是一个具体的例子;也许我们在这里运气好了?我建议你花点时间在在线代码中自行探索一下;你可以创建随机对称矩阵(通过创建一个随机矩阵并特征分解
对称矩阵保证具有实值特征值是幸运的,因为复数常常让人感到困惑。数据科学中使用的许多矩阵都是对称的,因此如果在数据科学应用中看到复数特征值,可能存在代码或数据问题。
利用对称性
如果你知道你在处理对称矩阵,可以使用np.linalg.eigh代替np.linalg.eig(或者 SciPy 的eigh代替eig)。h代表“Hermitian”,这是对称矩阵的复数版本。eigh可能比eig更快且数值上更稳定,但仅适用于对称矩阵。
奇异矩阵的特征分解
我在这里包含了这一节,因为我发现学生们经常会认为奇异矩阵不能进行特征分解,或者奇异矩阵的特征向量必须在某种程度上不寻常。
那个观点完全错误。奇异矩阵的特征分解完全没问题。这里是一个快速的例子:
# a singular matrix
A = np.array([[1,4,7],
[2,5,8],
[3,6,9]])
# its eigendecomposition
L,V = np.linalg.eig(A)
这个矩阵的秩、特征值和特征向量在这里被打印出来:
print( f'Rank = {np.linalg.matrix_rank(A)}\n' )
print('Eigenvalues: '), print(L.round(2)), print(' ')
print('Eigenvectors:'), print(V.round(2))
>> Rank = 2
Eigenvalues:
[16.12 -1.12 -0. ]
Eigenvectors:
[[-0.46 -0.88 0.41]
[-0.57 -0.24 -0.82]
[-0.68 0.4 0.41]]
这个二阶矩阵有一个零值特征值,对应一个非零特征向量。你可以使用在线代码来探索其他降秩随机矩阵的特征分解。
奇异矩阵的特征分解有一个特殊的性质,即至少有一个特征值保证为零。这并不意味着非零特征值的数量等于矩阵的秩——这对奇异值(来自奇异值分解的标量值)成立,但对特征值不成立。但如果矩阵是奇异的,那么至少有一个特征值等于零。
反之亦然:每个满秩矩阵都有零个零值特征值。
为什么会发生这种情况的一个解释是,奇异矩阵已经有一个非平凡的零空间,这意味着
本节的主要要点是:(1)特征分解对降秩矩阵有效,(2)至少有一个零特征值表明矩阵是降秩的。
矩阵的二次型、正定性和特征值
让我们面对现实:二次型和正定性是令人生畏的术语。但别担心——它们都是直观的概念,为高级线性代数和应用提供了一个通向主成分分析和蒙特卡洛模拟等领域的大门。更重要的是:将 Python 代码整合到学习中,相比传统线性代数教科书,将为你带来巨大的优势。
矩阵的二次型
考虑以下表达式:
换句话说,我们通过相同的向量
这被称为在矩阵
我们使用哪个矩阵和哪个向量?二次型的概念是使用一个特定的矩阵和所有可能的向量(大小合适)。重要的问题涉及所有可能向量的
对于这个特定的矩阵,不存在x和y的任何组合可以给出负答案,因为平方项(2x²和 3y²)总会压倒交叉项(4xy),即使x或y为负。此外,
这不是二次型的一个平凡结果。例如,下面的矩阵可以根据x和y的值得到一个正或负的
你可以确认将[x y]设置为[−1 1]会得到一个负二次型的结果,而[−1 −1]则会得到一个正结果。
你怎么可能知道二次形式对所有可能的向量会产生正(或负,或零值)的标量?关键在于考虑到一个满秩特征向量矩阵涵盖了
最终的方程是关键。注意
那个方程仅使用一个特征值及其特征向量,但我们需要了解任何可能的向量。关键在于考虑到如果方程对每个特征向量-特征值对都有效,则它对任何特征向量-特征值对的任何组合也有效。例如:
换句话说,我们可以将任何向量
现在让我们根据对
所有特征值均为正
方程的右侧始终为正,这意味着对于任何向量
特征值为正或零
当矩阵是奇异矩阵时,
特征值为负或零
二次形式的结果将是零或负。
特征值为负
对于所有向量,二次形式的结果将为负。
明确性
明确性 是方阵的一个特征,并且由矩阵的特征值的符号定义,这与二次型结果的符号相同。明确性对矩阵的可逆性以及高级数据分析方法(如广义特征分解,用于多变量线性分类器和信号处理)有重要意义。
有五种明确性类别,如表格 13-1 所示;+ 和 − 符号表示特征值的符号。
表格 13-1. 明确性类别
| 类别 | 二次型 | 特征值 | 可逆 |
|---|---|---|---|
| 正定 | 正 | + | 是 |
| 正半定 | 非负 | + 和 0 | 不 |
| 不定 | 正负 | + 和 − | 取决于 |
| 负半定 | 非正 | − 和 0 | 不 |
| 负定 | 负 | − | 是 |
表格中的“取决于”意味着矩阵的可逆性或奇异性取决于矩阵中的数字,而不是确定性类别。
𝐀 T 𝐀 是正(半)定的
任何可以表示为矩阵及其转置乘积(即,
所有数据协方差矩阵都是正(半)定的,因为它们被定义为数据矩阵乘以其转置。这意味着所有协方差矩阵具有非负特征值。当数据矩阵是满秩时(如果数据以观测值和特征存储,则为满列秩),特征值将全部为正;如果数据矩阵是降秩的,则至少会有一个零值特征值。
关键在于
请记住,尽管所有的
广义特征分解
考虑以下方程与基本特征值方程相同:
这很明显,因为
广义特征分解也称为两个矩阵的同时对角化。产生的(
从概念上来说,你可以将广义特征分解看作是一个乘积矩阵的“常规”特征分解:
这只是概念性的;在实践中,广义特征分解不要求矩阵
并非所有两个矩阵都可以同时对角化。但是如果
NumPy 不计算广义特征分解,但 SciPy 计算。如果你知道这两个矩阵是对称的,你可以使用函数eigh,这更加数值稳定:
# create correlated matrices
A = np.random.randn(4,4)
A = A@A.T
B = np.random.randn(4,4)
B = B@B.T + A/10
# GED
from scipy.linalg import eigh
evals,evecs = eigh(A,B)
在输入顺序时要小心:概念上,第二个输入是被倒置的。
在数据科学中,广义特征分解被用于分类分析。特别是,费舍尔线性判别分析基于两个数据协方差矩阵的广义特征分解。你将在第十五章中看到一个例子。
总结
这确实是一章!这里是关键点的提醒:
-
特征分解识别M个标量/向量对于一个
矩阵。这些特征值/特征向量对反映了矩阵中的特殊方向,并在数据科学(主成分分析是一个常见应用)、几何学、物理学、计算生物学等技术学科中有着广泛的应用。M × M -
特征值通过假设由未知标量
转移的矩阵是奇异的,将其行列式设为零(称为特征多项式),并解出λ s 来找到。λ -
特征向量通过寻找
-转移矩阵的零空间的基向量来找到。λ -
对角化矩阵意味着将矩阵表示为
,其中V - 1 Λ V 是列中包含特征向量的矩阵,𝐕 是对角线上包含特征值的对角矩阵。Λ -
对称矩阵在特征分解中具有几个特殊性质;在数据科学中最相关的是所有特征向量是两两正交的。这意味着特征向量矩阵是正交矩阵(当特征向量单位归一化时),进而意味着特征向量矩阵的逆是其转置。
-
矩阵的定性指的是其特征值的符号。在数据科学中,最相关的类别是正(半)定的,这意味着所有特征值要么非负要么正。
-
一个矩阵乘以其转置始终是正(半)定的,这意味着所有协方差矩阵都具有非负特征值。
-
特征分解的研究内容丰富而详细,已经发现了许多迷人的细节、特殊情况和应用。希望本章概述能为您作为数据科学家的需求提供坚实的基础,并可能激发您进一步了解特征分解的奇妙之美。
代码练习
练习 13-1.
有趣的是,
练习 13-2.
重新创建 Figure 13-1 的左侧面板,但使用
Exercise 13-3。
这项练习的目标是展示特征值与它们的特征向量密切相关。使用加法方法创建对称随机整数矩阵⁷(参见 Exercise 5-9),但随机重新排列特征值(我们将这个矩阵称为
首先,证明你能够将原始矩阵重构为
最后,创建一个条形图,显示不同交换选项对于原始矩阵的 Frobenius 距离(Figure 13-3)。当然,由于随机矩阵——因此,随机特征值——你的图表看起来不会完全与我的相同。

图 13-3。Exercise 13-3 的结果
Exercise 13-4。
随机矩阵的一个有趣性质是它们的复值特征值在一个半径与矩阵大小成比例的圆内分布。为了证明这一点,计算 123 个随机

图 13-4。Exercise 13-4 的结果
Exercise 13-5。
这个练习将帮助你更好地理解特征向量是特征值偏移矩阵的零空间的基础,也将揭示数值精度误差的风险。特征分解一个随机 scipy.linalg.null_space() 找到每个偏移矩阵的零空间的基向量。这些向量是否与特征向量相同?请注意,你可能需要考虑特征向量的范数和符号不确定性。
当你多次运行代码使用不同的随机矩阵时,你可能会遇到 Python 错误。错误来自于偏移矩阵为空零空间,这个问题在于偏移矩阵是满秩的。 (不要只听我的话,自己确认一下!)这是不应该发生的,这再次突显了(1)计算机上的有限精度数学并不总是符合黑板数学和(2)你应该使用有针对性且更稳定的函数,而不是尝试直接将公式翻译成代码。
Exercise 13-6.
我将教你第三种方法来创建随机对称矩阵。⁸ 首先创建一个对角线上有正数(例如,可以是数字 1、2、3、4)的
Exercise 13-7.
让我们重新审视练习 12-4。重新进行该练习,但使用特征值的平均数而不是设计矩阵的平方 Frobenius 范数(这被称为收缩正则化)。结果图与第十二章的图相比如何?
Exercise 13-8.
这个练习与下一个练习密切相关。我们将创建具有指定相关矩阵的模拟数据(这个练习),然后移除相关性(下一个练习)。创建具有指定相关结构数据的公式是:
其中
应用该公式创建一个 3 × 10,000 的数据矩阵
然后计算数据矩阵
练习 13-9.
现在让我们通过 白化 来消除这些强加的相关性。白化是信号和图像处理中的术语,用于消除相关性。可以通过实施以下公式对多变量时间序列进行白化:
将该公式应用于上一个练习的数据矩阵,并确认相关矩阵是单位矩阵(再次强调,对于随机抽样,误差在某个容差范围内)。
练习 13-10.
在广义特征分解中,即使两个矩阵都是对称的,特征向量也不是正交的。在 Python 中确认
然而,特征向量在

图 14-1. 奇异值分解的全局视角
在这张图中可以看到奇异值分解的许多重要特性;我将在本章中更详细地解释这些特性,但先简要列举一下:
-
和𝐔 都是方阵,即使𝐕 不是方阵。𝐀 -
奇异向量矩阵
和𝐔 是正交的,意味着𝐕 和𝐔 T 𝐔 = 𝐈 。提醒一下,这意味着每一列都与其他列正交,且任何一个列子集与任何其他(非重叠)列子集也正交。𝐕 T 𝐕 = 𝐈 -
的前 r 列提供了矩阵𝐔 的列空间的正交基向量,而其余列则提供了左零空间的正交基向量(除非 r = M,此时矩阵具有满列秩且左零空间为空)。𝐀 -
的前r行(即𝐕 T 的列)为行空间提供正交基向量,而其余行为零空间提供正交基向量。𝐕 -
奇异值矩阵是与
相同大小的对角矩阵。奇异值始终按从最大(左上角)到最小(右下角)排序。𝐀 -
所有奇异值都是非负实数。它们不能是复数或负数,即使矩阵包含复数。
-
非零奇异值的数量等于矩阵的秩。
或许 SVD 最惊人的地方在于它揭示了矩阵的四个子空间:列空间和左空间由
奇异值与矩阵的秩
矩阵的秩被定义为非零奇异值的数量。这个原因来自于前面的讨论,即矩阵的列空间和行空间被定义为通过它们对应的奇异值进行缩放以在矩阵空间中具有一定“体积”的左奇异向量和右奇异向量,而左空间和右空间被定义为通过它们进行缩放为零的左奇异向量和右奇异向量。因此,列空间和行空间的维数由非零奇异值的数量决定。
实际上,我们可以查看 NumPy 函数np.linalg.matrix_rank来看 Python 如何计算矩阵的秩(我稍作修改以便专注于关键概念):
S = svd(M,compute_uv=False) # return only singular values
tol = S.max() * max(M.shape[-2:]) * finfo(S.dtype).eps
return count_nonzero(S > tol)
返回的值是超过tol值的奇异值数量。tol是什么?这是一个容忍度水平,考虑可能的舍入误差。它被定义为此数据类型的机器精度(eps),按照最大奇异值和矩阵的大小进行缩放。
因此,我们再次看到“黑板上的数学”与计算机上实现的精确数学之间的差异:矩阵的秩实际上并不是计算为非零奇异值的数量,而是计算为大于某个小数的奇异值的数量。有可能忽略一些真正非零但由于精度误差而被忽略的奇异值,但这相对于在真正值为零的情况下由于精度误差而错误地增加矩阵的秩的风险来说更为合理。
Python 中的 SVD
在 Python 中计算 SVD 相对直观:
U,s,Vt = np.linalg.svd(A)
NumPy 的 svd 函数有两个需要注意的特点。首先,奇异值作为一个向量返回,而不是与
S = np.zeros(np.shape(A))
np.fill_diagonal(S,s)
你可能最初想到使用 np.diag(s),但那只适用于方阵
第二个特点是 NumPy 返回矩阵 svd 函数返回矩阵 vh,其中的 h 表示 Hermitian,即对称复值矩阵的名称。
图 14-2 展示了 svd 函数的输出(奇异值已转换为矩阵)。

图 14-2. 显示了奇异值分解的示例矩阵的整体情况
奇异值分解和矩阵的秩-1“层”
我在上一章节展示的第一个方程是特征值方程的向量-标量版本(
这些方程有点类似于特征值方程,只是有两个向量而不是一个。因此,它们的解释略微更为细腻:总体来说,这些方程表明矩阵对一个向量的作用与一个不同向量的标量作用相同。
注意第一个方程意味着
但这并不是我想在本节中集中讨论的内容;我想考虑的是当你将一个左奇异向量乘以一个右奇异向量时会发生什么。因为奇异向量与相同的奇异值配对,我们需要将第i个左奇异向量乘以第i个奇异值,再乘以第i个右奇异向量。
注意这个向量-向量乘法中的方向:左边是列,右边是行(见图 14-3)。这意味着结果将是一个与原始矩阵大小相同的外积矩阵。此外,该外积是一个秩-1 矩阵,其范数由奇异值决定(因为奇异向量是单位长度的):

图 14-3. 奇异向量的外积创建一个矩阵“层”
方程中的下标1表示使用第一个奇异向量和第一个(最大的)奇异值。我称之为
有了这个理解,我们可以通过对所有与
展示这个求和的要点在于,你不一定需要使用所有的r层;相反,你可以重构另一个矩阵,让我们称之为
例如,在数据清理中使用低秩逼近。其思想是与小奇异值相关联的信息对数据集的总方差贡献较小,因此可能反映出可以去除的噪声。在下一章中会详细介绍。
SVD from EIG
好的,在本章的这一点上,你已经了解了理解和解释 SVD 矩阵的基础。我相信你想知道的是如何产生 SVD 的这个神奇公式。也许它非常复杂,只有高斯能理解?或者可能需要很长时间来解释,不适合放在一个章节里?
错了!
实际上,SVD 非常简单(在概念上;手动执行 SVD 又是另一回事)。它只是来自计算矩阵乘以其转置的特征分解。以下方程显示如何推导奇异值和左奇异向量:
换句话说,
这一洞察揭示了 SVD 的三个特点:(1)奇异值非负,因为平方数不能为负数;(2)奇异值是实数,因为对称矩阵具有实数特征值;(3)奇异向量正交,因为对称矩阵的特征向量是正交的。
右奇异值来自矩阵转置的预乘:
实际上,您可以重新排列 SVD 方程以解出右奇异向量,而无需计算
当然,如果您已经知道
SVD of 𝐀 T 𝐀
简而言之,如果一个矩阵可以表示为
这个断言的证明源于将
实际上,对于对称矩阵,SVD 与特征分解是相同的。这对主成分分析有影响,因为 PCA 可以使用数据协方差矩阵的特征分解,协方差矩阵的 SVD 或数据矩阵的 SVD 来执行。
将奇异值转换为解释的方差
奇异值的总和是矩阵中的总“方差”量。这是什么意思?如果你将矩阵中的信息视为包含在一个气泡中,那么奇异值的总和就像是那个气泡的体积。
所有方差都包含在奇异值中的原因是奇异向量被规范化为单位大小,这意味着它们不提供大小信息(即,
“原始”奇异值处于矩阵的数值尺度中。这意味着如果将数据乘以一个标量,那么奇异值将会增加。而这又意味着,奇异值难以解释,并且基本上不可能在不同数据集之间进行比较。
因此,将奇异值转换为解释的总百分比通常是有用的。公式很简单;每个奇异值 i 规范化如下:
例如,在主成分分析中,这种归一化常用来确定解释 99% 方差的成分数量。这可以解释为系统复杂性的一个指标。
重要的是,这种归一化不会影响奇异值之间的相对距离;它只是将数值尺度改变为更容易解释的尺度。
条件数
在本书中我多次提到,矩阵的条件数用于指示矩阵的数值稳定性。现在你了解了奇异值,可以更好地理解如何计算和解释条件数。
矩阵的条件数定义为最大奇异值与最小奇异值的比值。通常用希腊字母 κ(kappa)表示:
在统计学和机器学习中,条件数常用于评估矩阵在计算其逆矩阵和使用其解决方程组(如最小二乘法)时的稳定性。当然,非可逆矩阵的条件数为 NaN,因为
但是,数值上满秩且条件数较大的矩阵仍可能不稳定。虽然在理论上可逆,但实际上其逆矩阵可能不可靠。这样的矩阵被称为病态矩阵。你可能在 Python 的警告消息中见过这个术语,有时伴随着“结果可能不准确”的短语。
对于条件数较大的病态矩阵有什么问题?随着条件数的增加,矩阵趋向于奇异。因此,病态矩阵几乎是“几乎奇异的”,其逆由于数值误差风险增加而不可信。
有几种方法可以思考病态矩阵的影响。一种是由于舍入误差导致解的精度降低。例如,条件数约为 10⁵意味着解(例如矩阵逆或最小二乘问题)失去了五个有效数字(例如,从精度 10^(−16)降至 10^(−11))。
第二种解释与前述有关,是噪声放大因子。如果矩阵的条件数约为 10⁴,那么噪声可能会使最小二乘问题的解受到 10⁴的影响。这听起来可能很多,但如果您的数据精度为 10^(−16),这可能是一个微不足道的放大效应。
第三,条件数表示解对数据矩阵扰动的敏感性。良好条件的矩阵可以在添加噪声时保持解的最小变化。相比之下,对病态矩阵添加少量噪声可能会导致完全不同的解。
矩阵何时被认为是病态?没有确定的阈值。没有一个魔法数字可以将良好条件的矩阵与病态矩阵分开。不同的算法将适用不同的阈值,这些阈值取决于矩阵中的数值。
很明显:要认真对待关于病态矩阵的警告信息。它们通常表明某些地方出了问题,结果不可信。
SVD 和 MP 伪逆
矩阵的奇异值分解逆矩阵非常优雅。假设矩阵是方阵且可逆,我们有:
换句话说,我们只需求逆
现在让我们来看一下计算 MP 伪逆的算法。您已经等待了许多章节;我感谢您的耐心。
MP 伪逆几乎与前面示例中显示的完全逆矩阵计算相同;唯一的修改是反转
就是这样!这就是伪逆的计算方法。您可以看到它非常简单和直观,但需要相当多的线性代数背景知识才能理解。
更好的是:因为 SVD 适用于任何大小的矩阵,MP 伪逆可以应用于非方阵。实际上,高矩阵的 MP 伪逆等于其左逆,而宽矩阵的 MP 伪逆等于其右逆。(快速提醒,伪逆表示为
通过在练习中自己实现,您将更多地了解伪逆。
摘要
我希望您同意,在努力学习特征分解后,再多付出一点努力就能更好地理解 SVD。SVD 可以说是线性代数中最重要的分解方法,因为它揭示了关于矩阵的丰富和详细信息。以下是其关键点:
-
SVD 将一个(任意大小和秩)矩阵分解为三个矩阵的乘积,称为左奇异向量
,奇异值𝐔 和右奇异向量Σ 。𝐕 T -
前r(其中r是矩阵秩)个左奇异向量为矩阵的列空间提供一组标准正交基,而后续的奇异向量为左零空间提供一组标准正交基。
-
对于右奇异向量也是类似的情况:前r个向量为行空间提供一组标准正交基,而后续的向量为零空间提供一组标准正交基。请注意,右奇异向量实际上是
的行,这些行是𝐕 的列。𝐕 T -
非零奇异值的数量等于矩阵的秩。在实践中,很难区分非常小的非零奇异值与零值奇异值的精度误差。像 Python 这样的程序会使用容差阈值来区分它们。
-
第 k 个左奇异向量和第 k 个右奇异向量的外积,乘以第 k 个奇异值,产生一个秩-1 矩阵,可以解释为矩阵的“层”。基于层重建矩阵具有许多应用,包括去噪和数据压缩。
-
从概念上讲,SVD 可以从
的特征分解中获得。𝐀 𝐀 T -
超级重要的摩尔-彭罗斯伪逆计算为
,其中V Σ + U T 通过反转对角线上的非零奇异值获得。Σ +
代码练习。
练习 14-1。
你学到对于对称矩阵,奇异值和特征值是相同的。那么奇异向量和特征向量呢?使用一个随机
练习 14-2。
Python 可以选择返回“经济”SVD,这意味着奇异向量矩阵在较小的M或N处被截断。请查阅文档字符串以了解如何实现此功能。与高和宽矩阵确认。注意,通常应返回完整矩阵;经济型 SVD 主要用于非常大的矩阵和/或计算能力非常有限的情况。
练习 14-3。
正交矩阵(如左和右奇异向量矩阵)的一个重要特性是它们可以旋转向量但不改变其大小。这意味着经过正交矩阵乘法后,向量的大小保持不变。通过在 Python 中使用来自随机矩阵 SVD 的奇异向量矩阵和随机向量来证明
练习 14-4。
创建一个具有指定条件数的随机高瘦矩阵。通过创建两个随机方阵

图 14-4. 习题 14-3 的结果
习题 14-5.
你在这里的目标很简单:编写代码以重现图 14-5。这个图显示了什么?面板 A 展示了一个
面板 B 展示了一个“scree plot”,即归一化为百分比方差解释的奇异值图。请注意,前几个分量占了图像中大部分的方差,而后续的分量每个都只解释了相对较少的方差。确认所有归一化奇异值的总和为 100. 面板 C 展示了前四个“层次”——定义为

图 14-5. 习题 14-5 的结果
习题 14-6.
根据本章描述实现 MP 伪逆。你需要定义一个容差来忽略微小但非零的奇异值。请不要查阅 NumPy 的实现,也不要回顾本章的早期代码,而是利用你对线性代数的知识自行设计容差。
在一个 pinv 函数的输出进行比较。最后,检查 np.linalg.pinv 的源代码以确保你理解其实现。
练习 14-7.
通过计算高列满秩矩阵的显式左逆证明 MP 伪逆等于左逆,(
练习 14-8.
考虑特征值方程
¹ 对于所有方阵,奇异值分解与特征分解并不相同;稍后详细讨论。
² 没有意义对零值奇异值求和,因为那只是在加零矩阵。
³ 这个陈述的证明在练习 14-3 中。
⁴ 这是对《银河系漫游指南》的另一个引用。
第十五章:特征分解和奇异值分解应用
特征分解和奇异值分解是线性代数赋予现代人类文明的宝藏。它们在现代应用数学中的重要性不可低估,其应用遍布各种学科。
在本章中,我将重点介绍您在数据科学及相关领域可能遇到的三个应用程序。我的主要目标是向您展示,一旦您学习了本书中的线性代数主题,表面上复杂的数据科学和机器学习技术实际上是相当合理且容易理解的。
使用特征分解和奇异值分解进行 PCA
PCA 的目的是为数据集找到一组基向量,这些向量指向最大化变量之间协变性的方向。
想象一个 N 维数据集存在于 N 维空间中,其中每个数据点是该空间中的一个坐标。当您考虑将数据存储在一个矩阵中时,矩阵具有 N 个观测(每行是一个观测)和 M 个特征(每列是一个特征,也称为变量或测量);数据存在于
在二维示例中,如图 15-1 所示。左侧面板显示了数据在其原始数据空间中的情况,其中每个变量为数据提供了一个基向量。显然,两个变量(x 和 y 轴)彼此相关,并且数据中有一个方向能够更好地捕捉到这种关系,超过了任一特征基向量。

图 15-1。PCA 在二维中的图形概述
PCA 的目标是找到一组新的基向量,使得变量之间的线性关系与基向量最大地对齐——这正是图 15-1 右侧面板所展示的内容。重要的是,PCA 有一个约束条件,即新的基向量必须是原始基向量的正交旋转。在练习中,您将看到这个约束条件的影响。
在接下来的部分,我将介绍计算 PCA 的数学和过程;在练习中,您将有机会使用特征分解和奇异值分解实现 PCA,并将结果与 Python 实现的 PCA 进行比较。
PCA 的数学基础
PCA 结合了方差的统计概念和线性代数中的线性加权组合概念。方差是数据集围绕其平均值的离散程度的度量。PCA 假设方差是有益的,并且在数据空间中具有更多方差的方向更重要(也就是“方差=相关性”)。
但在 PCA 中,我们不仅仅关注一个变量内的方差;相反,我们希望找到跨所有变量的线性加权组合,使得该成分的方差最大化(一个成分是变量的线性加权组合)。
让我们用数学方式表达这一点。矩阵
当数据均值为零(即,每个数据变量的均值为零)时,平方向量范数实际上与方差相同;¹ 我们省略了缩放因子 1/(N − 1),因为它不会影响我们优化目标的解决方案。
该方程的问题在于你可以简单地将
现在我们有两个向量范数的比率。我们可以将这些范数扩展为点积,以获得对方程的一些见解:
现在我们发现 PCA 的解法与找到最大化数据协方差矩阵的标准化二次形式的方向向量相同。
这一切都很好,但我们如何实际找到向量
线性代数的方法是考虑不只是单一向量解,而是整个解集。因此,我们使用矩阵
从这里,我们应用一些代数,看看会发生什么:
令人惊讶的是,我们发现 PCA 的解法是对数据协方差矩阵执行特征分解。特征向量是数据变量的权重,它们对应的特征值是数据沿每个方向的方差(
因为协方差矩阵是对称的,它们的特征向量——因此主成分——是正交的。这对 PCA 在数据分析中的适用性有重要的影响,你将在练习中发现。
执行 PCA 的步骤
数学已经清楚了,下面是实施 PCA 的步骤:
-
计算数据的协方差矩阵。得到的协方差矩阵将按特征-特征排列。在计算协方差之前,数据中的每个特征必须进行均值中心化。
-
对该协方差矩阵进行特征值分解。
-
按照大小降序排序特征值,并相应地排序特征向量。PCA 的特征值有时被称为潜在因子得分。
-
计算“成分得分”,作为所有数据特征的加权组合,其中特征向量提供权重。与最大特征值相关联的特征向量是“最重要”的成分,意味着它具有最大的方差。
-
将特征值转换为百分比方差解释,以便于解释。
通过 SVD 进行 PCA
PCA 可以通过前述的特征值分解或通过 SVD 等效地进行。使用 SVD 执行 PCA 有两种方法:
-
对协方差矩阵进行 SVD。该过程与先前描述的相同,因为 SVD 和特征值分解是协方差矩阵的相同分解方法。
-
直接对数据矩阵进行 SVD。在这种情况下,右奇异向量(矩阵
)等价于协方差矩阵的特征向量(如果数据矩阵按特征-观察存储,则左奇异向量)。在计算 SVD 之前,数据必须进行均值中心化。奇异值的平方根等价于协方差矩阵的特征值。𝐕
在执行 PCA 时,您应该使用特征值分解还是 SVD?您可能认为 SVD 更容易,因为它不需要协方差矩阵。对于相对较小且干净的数据集,这是正确的。但对于更大或更复杂的数据集,可能需要数据选择,或者可能因为内存需求过高而无法直接对整个数据矩阵进行 SVD。在这些情况下,先计算协方差矩阵可以增加分析的灵活性。但是选择特征值分解还是 SVD 通常是个人喜好的问题。
线性判别分析
线性判别分析(LDA)是一种常用于机器学习和统计学中的多变量分类技术。最初由罗纳德·费舍尔开发,³他因其对统计学数学基础的众多重要贡献而常被称为统计学的“祖父”。
LDA 的目标是在数据空间中找到一个方向,最大化地分离数据的类别。图 A 中展示了一个示例问题数据集,图 15-2。从视觉上看,两个类别是可分离的,但在任何单个数据轴上它们都不可分离—这从边际分布的视觉检查中显而易见。
进入 LDA。LDA 将在数据空间中找到一组基向量,使得两个类别能够最大化分离。图 B 在 LDA 空间的图示中展示了相同的数据。现在分类很简单——在轴-1 上具有负值的观测标记为类别“0”,而在轴 1 上具有正值的观测标记为类别“1”。在轴 2 上,数据完全无法分离,这表明在这个数据集中,一个维度足以实现准确的分类。

图 15-2. LDA 的二维问题示例
听起来不错,对吧?但这样一种数学奇迹是如何运作的呢?事实上,它非常直接,基于广义特征分解,你在第十三章末尾学习过这个方法。
让我从目标函数开始:我们的目标是找到一组权重,使得变量的加权组合能够最大化地将类别分开。该目标函数可以类似于 PCA 的目标函数进行表达:
用简单的英语说,我们想找到一组特征权重
线性代数的解决方案源于与 PCA 部分描述类似的论证。首先,将
换句话说,LDA 的解决方案来自于对两个协方差矩阵进行广义特征分解。特征向量是权重,广义特征值是每个分量的方差比率。⁴
现在数学部分已经搞定,用来构建
在类内协方差简单来说是数据样本在每个类别内协方差的平均值。类间协方差来自于创建一个新的数据矩阵,其中包括每个类别内的特征平均值。我将在练习中为您讲解这个过程。如果您熟悉统计学,那么您会认识到这种表述类似于 ANOVA 模型中组间平方误差与组内平方误差比率的形式。
最后两点说明:广义特征分解的特征向量不受强制要求是正交的。这是因为
最后,LDA(线性判别分析)将始终找到一个线性解决方案(当然,这在LDA 的名称中就已经明确了),即使数据不是线性可分的。非线性分离将需要对数据进行变换或使用像人工神经网络这样的非线性分类方法。LDA 在产生结果方面仍然有效;作为数据科学家,您需要确定该结果是否适合和可解释于给定问题。
通过 SVD 进行低秩逼近
我在上一章解释了低秩逼近的概念(例如,练习 14-5)。其思想是对数据矩阵或图像进行 SVD 分解,然后使用 SVD 分量的某个子集重构该数据矩阵。
您可以通过将选定的
SVD 用于去噪
通过 SVD 进行去噪仅仅是低秩逼近的一个应用。唯一的区别在于,SVD 的组成部分被选择排除,因为它们代表噪声,而不是对数据矩阵作出小贡献。
待移除的组件可能是与最小奇异值相关联的层—这在与小型设备不完美相关的低振幅噪声的情况下是这样。但对数据影响更大的较大噪声源可能具有较大的奇异值。这些噪声组件可以通过基于它们特征的算法或视觉检查来识别。在练习中,您将看到使用 SVD 分离添加到图像中的噪声源的示例。
总结
恭喜你!你已经读完了本书的内容(除了下面的练习)!请花点时间为自己和你对学习和投资大脑的承诺感到自豪(毕竟,这是你最宝贵的资源)。我为你感到骄傲,如果我们能见面,我会和你击掌、拳头碰、肘部碰或者在当时社会/医学上合适的方式表示祝贺。
我希望你觉得本章帮助你看到特征分解和奇异值分解在统计学和机器学习应用中的重要性。这里是我必须包括的关键点总结:
-
PCA 的目标是找到一组权重,使得数据特征的线性加权组合具有最大方差。这个目标反映了 PCA 的基本假设,“方差等于相关性”。
-
PCA 作为数据协方差矩阵的特征分解实现。特征向量是特征权重,而特征值可以缩放以编码每个组件所占的百分比方差(一个组件是线性加权组合)。
-
PCA 可以等效地使用协方差矩阵或数据矩阵的 SVD 实现。
-
线性判别分析(LDA)用于多变量数据的线性分类。可以看作是 PCA 的扩展:PCA 最大化方差,而 LDA 最大化两个数据特征之间的方差比。
-
LDA 作为两个协方差矩阵的广义特征分解实现,这些矩阵由两个不同的数据特征形成。这两个数据特征通常是类间协方差(要最大化)和类内协方差(要最小化)。
-
低秩逼近涉及从奇异向量/值的子集中复制矩阵,并用于数据压缩和去噪。
-
对于数据压缩,与最小奇异值相关联的组件被移除;对于数据去噪,捕捉噪声或伪影的组件被移除(它们对应的奇异值可能很小或很大)。
练习
PCA
我喜欢土耳其咖啡。它用非常细磨的豆子制成,没有过滤器。整个制作和享用过程都很美妙。如果你和土耳其人一起喝,也许你可以算算你的命运。
这个练习不是关于土耳其咖啡,而是关于对包含来自伊斯坦布尔证券交易所的时间序列数据以及来自其他几个国家不同股票指数的股票交易数据的数据集进行 PCA⁵。我们可以使用这个数据集来询问,例如国际股票交易是否由全球经济的一个共同因素驱动,或者不同国家是否拥有独立的金融市场。
练习 15-1.
在执行 PCA 之前,请导入并检查数据。我对数据进行了多个绘图,显示在图 15-3 中;欢迎您重现这些图表和/或使用不同方法探索数据。

图 15-3. 对国际股票交易所数据集的一些调查
现在进行 PCA。按照本章前面提到的五个步骤实施 PCA。像图 15-4 一样可视化结果。使用代码展示 PCA 的几个特性:
-
成分时间序列的方差(使用
np.var)等于与该成分相关联的特征值。您可以在这里查看前两个成分的结果:Variance of first two components: [0.0013006 0.00028585] First two eigenvalues: [0.0013006 0.00028585] -
主成分之间的相关性(即股票交易所的加权组合)1 和 2 为零,即正交。
-
可视化前两个成分的特征向量权重。权重显示每个变量对成分的贡献程度。

图 15-4. 伊斯坦布尔证券交易所数据集的 PCA 结果
讨论: 屏风图强烈表明国际股票交易所受全球经济的一个共同因素驱动:有一个大成分解释了数据中约 64%的方差,而其他成分每个都解释了不到 15%的方差(在纯随机数据集中,我们预计每个成分解释 100/9 = 11%的方差,加减噪声)。
对这些成分的统计显著性进行严格评估超出了本书的范围,但基于对屏风图的视觉检查,我们并不能完全有理由解释第一个成分之后的成分;似乎这个数据集的大部分方差都整齐地适合一个维度。
从降维的角度来看,我们可以将整个数据集减少到与最大特征值相关联的分量(通常称为顶部分量),从而使用 1D 向量表示这个 9D 数据集。当然,我们会失去信息——如果我们只关注顶部分量,数据集中 36%的信息会被移除——但希望信号的重要特征位于顶部分量中,而不重要的特征,包括随机噪声,则被忽略了。
练习 15-2.
通过(1)数据协方差矩阵的 SVD 和(2)数据矩阵本身的 SVD 来重现结果。请记住
练习 15-3.
将你的“手动”PCA 与 Python 的 PCA 例程的输出进行比较。你需要进行一些在线研究,以找出如何在 Python 中运行 PCA(这是 Python 编程中最重要的技能之一!),但我会给你一个提示:它在 sklearn.decomposition 库中。
sklearn 还是手动实现 PCA?
应该通过编写代码来计算和特征分解协方差矩阵来计算 PCA,还是使用 sklearn 的实现?始终存在使用自己的代码以最大程度定制与使用预包装代码以最大程度便捷之间的权衡。理解数据科学分析背后的数学的无数和令人惊奇的好处之一是,你可以定制分析以满足你的需求。在我的研究中,我发现自己实现 PCA 能给我更多的自由和灵活性。
练习 15-4.
现在你将在模拟数据上执行 PCA,这将突显 PCA 的潜在限制之一。目标是创建一个包含两个“流”数据的数据集,并在顶部绘制主成分,就像图 15-5 中一样。

图 15-5. 练习 15-4 的结果
这里是创建数据的方法:
-
创建一个大小为 1,000 × 2 的矩阵,其中的随机数是从正态(高斯)分布中抽取的,其中第二列的数值缩小了 0.05 倍。
-
创建一个 2 × 2 的纯旋转矩阵(参见第七章)。
-
垂直堆叠两份数据副本:一份是数据按角度
= −π/6 旋转,另一份是按角度θ = −π/3 旋转。得到的数据矩阵大小为 2,000 × 2。θ
使用 SVD 实现 PCA。我将奇异向量的尺度放大了 2 倍以进行视觉检查。
讨论: 主成分分析(PCA)非常适合降低高维数据集的维度。这可以促进数据压缩、数据清理和数值稳定性问题(例如,想象一个具有条件数 10¹⁰ 的 200 维数据集,将其降低到具有条件数 10⁵ 的最大 100 维)。但是,由于正交性约束,维度本身可能不是提取特征的最佳选择。确实,在 图 15-5 中方差的主要方向在数学上是正确的,但我相信您会感觉到这些不是捕获数据特征的最佳基向量。
线性判别分析。
练习 15-5。
您将在模拟的 2D 数据上执行 LDA。模拟数据具有优势,因为您可以操纵效应大小、噪声的数量和性质、类别数量等。
您将创建的数据在 图 15-2 中显示。创建两组正态分布随机数,每组大小为
使用 sns.jointplot 和 plot_joint 重新生成图 A 在 图 15-2 中的图形。
练习 15-6。
现在进行 LDA。使用 NumPy 和/或 SciPy 编写代码,而不是使用 sklearn 等内置库(我们稍后会讲到)。
类内协方差矩阵
从 第十三章 中记住,广义特征值分解是使用 SciPy 的 eigh 函数实现的。
投影到 LDA 空间的数据计算如
计算分类准确率,简单来说就是每个数据样本在第一个 LDA 成分上的投影是负数(“类 0”)还是正数(“类 1”)。图 C 在 图 15-6 中展示了每个数据样本的预测类别标签。
最后,展示如 图 15-6 中所示的结果。

图 15-6. 练习 15-6 的结果
练习 15-7.
我在 第十三章 中提到,对于广义特征分解,特征向量矩阵
计算并检查
练习 15-8.
现在使用 Python 的 sklearn 库重现我们的结果。使用 sklearn.discriminant_analysis 中的 LinearDiscriminantAnalysis 函数。生成类似于 图 15-7 的图,并确认总体预测准确率与前一练习中“手动”LDA 分析的结果一致。此函数支持多种不同的求解器;使用 eigen 求解器以匹配前一练习,并继续完成以下练习。
在你的“手动”LDA 上绘制预测标签;你应该会发现两种方法的预测标签是相同的。

图 15-7. 练习 15-8 的结果
练习 15-9.
让我们使用 sklearn 探索收缩正则化的效果。正如我在第 12 和 13 章中写的那样,显然,收缩会降低训练数据的性能;重要的问题是正则化是否提高了在未见过的数据上的预测准确性(有时称为验证集或测试集)。因此,您应该编写代码来实现训练/测试拆分。我通过随机排列样本索引在 0 到 399 之间,首先在前 350 个样本上进行训练,然后在最后 50 个样本上进行测试来实现这一点。由于样本数量较少,我重复了这个随机选择 50 次,并将平均准确率作为每个收缩量在图 15-8 中的准确率。

图 15-8. 练习 15-9 的结果
讨论: 收缩通常对验证性能产生负面影响。虽然看起来通过一些收缩可以改善性能,但多次重复代码表明这些只是一些随机波动。深入探讨正则化更适合专门的机器学习书籍,但我想在这里强调的是,许多在机器学习中开发的“技巧”并不一定在所有情况下都有利。
低秩近似的奇异值分解(SVD)
练习 15-10.
伊戈尔·斯特拉文斯基(IMHO 是所有时代最伟大的音乐作曲家之一),无疑是 20 世纪最具影响力的音乐家之一。他还对艺术、媒体和批评的性质发表了许多发人深省的言论,包括我最喜欢的一句话:“艺术越受限制,它就越自由。”有一幅由伟大的巴勃罗·毕加索创作的著名而引人入胜的斯特拉文斯基的画像。这幅画像在维基百科上有,我们将在接下来的几个练习中使用这幅图片。像我们在本书中使用过的其他图片一样,它本质上是一个 3D 矩阵(
本练习的目的是重复练习 14-5,在其中根据奇异值分解的四个“层次”重新创建了一个接近光滑噪声图像的近似图像(请回顾那个练习以刷新您的记忆)。使用斯特拉文斯基的图像制作像图 15-9 那样的图表。这里的主要问题是:使用前四个分量重建图像是否像上一章那样取得了良好的结果?

图 15-9. 练习 15-10 的结果
练习 15-11.
好吧,在上一个练习的最后一个问题的答案是一个响亮的“不行!”秩为 4 的近似太糟糕了!看起来和原始图像完全不一样。这个练习的目标是使用更多层来重建图像,以便低秩近似是合理准确的——然后计算所获得的压缩量。
从生成 Figure 15-10 开始,显示原始图像、重建图像和误差图,即原始图像与近似图像的平方差。对于这个图,我选择了 k = 80 个组件,但鼓励你探索不同数值(即不同秩的近似)。

Figure 15-10. 练习 15-11 的结果
接下来,计算压缩比率,即低秩近似使用的字节与原始图像使用的字节的百分比。我的 k = 80 的结果如下所示。⁶ 请记住,使用低秩近似时,你不需要存储完整图像或完整的奇异值分解矩阵!
Original is 2.10 mb
Reconstruction is 2.10 mb
Recon vectors are 0.65 mb (using k=80 comps.)
Compression of 31.13%
练习 15-12.
为什么我选择了 k = 80 而不是,例如,70 或 103?说实话,这相当随意。这个练习的目标是看看是否可以使用误差图来确定适当的秩参数。
在从 1 到奇异值数量的重建秩的 for 循环中,创建低秩近似并计算原始图像与 k 秩近似之间的 Frobenius 距离。然后,像 Figure 15-11 中那样绘制随秩变化的误差图。误差随秩增加而减少,但没有明显的最佳秩。有时在优化算法中,误差函数的导数更具信息性;试试看!

Figure 15-11. 练习 15-12 的结果
这个练习的最后想法是:k = 430 的重建误差(即完全奇异值分解)应该是完全为 0。是吗?显然不是,否则我就不会写这个问题了。但你应该自己确认一下。这又是应用线性代数中精度误差的又一次演示。
图像去噪的奇异值分解
练习 15-13.
让我们看看是否能将低秩近似的概念扩展到去噪斯特拉文斯基的图片。这个练习的目标是添加噪声并检查奇异值分解的结果,接下来的练习将涉及“投影出”损坏的部分。
噪声将是空间正弦波。你可以在 Figure 15-12 中看到噪声和损坏的图像。

Figure 15-12. 练习 15-13 的准备工作
现在我将描述如何创建二维正弦波(也称为 正弦光栅)。这是练习你的数学到代码转换技能的好机会。二维正弦波的公式是:
在这个公式中,f是正弦波的频率,
在继续进行其他练习之前,我建议您花一些时间使用正弦光栅代码,探索更改参数对生成的图像的影响。但请使用我之前写的参数,以确保您可以复现我随后的结果。
接下来,通过将噪声添加到图像中来损坏斯特拉文斯基图片。您应该首先将噪声缩放到 0 到 1 的范围内,然后将噪声和原始图片相加,然后重新缩放。将图像在 0 到 1 之间缩放的方法是应用以下公式:
现在您有了带噪声的图像。重现图 15-13,这与图 15-6 相同,但使用了有噪声的图像。

图 15-13. 练习 15-13 的结果
讨论: 比较图 15-13 与图 15-9 是很有意思的。尽管我们基于一个特征(正弦波光栅)创建了噪声,但 SVD 将该光栅分为两个重要性相等的分量(大致相等的奇异值)。这两个分量不是正弦波光栅,而是垂直方向的补丁。然而,它们的总和产生了光栅的对角线条带。
练习 15-14.
现在进行去噪。看起来噪音包含在第二和第三个分量中,所以你现在的目标是使用除了这两个分量之外的所有分量来重建图像。制作一个类似于图 15-14 的图。

图 15-14. 练习 15-14 的结果
讨论: 去噪效果还可以,但肯定不完美。不完美的原因之一是噪音并非完全包含在两个维度中(注意图 15-14 的中间面板并不完全匹配噪音图像)。此外,噪声投影(由第 1 和第 2 个分量制成的图像)具有负值,并分布在零附近,尽管正弦光栅没有负值。(您可以通过绘制噪音图像的直方图来确认这一点,我在在线代码中展示了这一点。)图像的其余部分需要有值的波动来解释这一点,以便全面重建只有正值。
¹ 在线代码演示了这一点。
² 在练习 15-3 中,您还将学习如何使用 Python 的 scikit-learn 库实现主成分分析(PCA)。
³ 实际上,线性判别分析也称为费舍尔判别分析。
⁴ 我不会详细讲述这个充满微积分的证明,但它只是主成分分析部分给出的证明的一个小变体。
⁵ 数据引用:Akbilgic, Oguz. (2013). 伊斯坦布尔证券交易所。UCI 机器学习库。数据来源网站:https://archive-beta.ics.uci.edu/ml/datasets/istanbul+stock+exchange。
⁶ 有些不确定是否一兆字节是 1000²还是 1024²字节;我使用了后者,但这不影响压缩比。
⁷ 可能的解释是这是一个二维的奇异平面,而不是一对奇异向量;在这个平面上,任意两个线性独立的向量都可以是基向量,Python 选择了一对正交向量。
第十六章:Python 教程
如我在第一章中所解释的,这一章节是关于 Python 编程的速成课程。它旨在让你迅速掌握基础知识,以便跟上本书其余部分的代码,但并不旨在成为 Python 掌握的完整来源。如果你正在寻找一本专门的 Python 书籍,我推荐Learning Python(由 Mark Lutz(O’Reilly)著)。
在阅读本章节时,请打开一个 Python 会话。(稍后我会解释如何操作。)你不仅仅通过阅读本章节来学习 Python;你需要阅读、在 Python 中输入代码、修改和测试代码等等。
此外,在本章中,你应该手动键入所有在此处打印的代码。本书的所有其他章节的代码都可以在网上找到,但我希望你手动输入本章的代码。一旦你更熟悉 Python 编码,手动输入大量代码会显得乏味且浪费时间。但当你初学编程时,你需要编码——也就是用手指键入所有内容。不要只是看页面上的代码。
为什么选择 Python,以及有哪些替代方案?
Python 被设计成一种通用编程语言。你可以使用 Python 进行文本分析、处理 Web 表单、创建算法以及无数其他应用。Python 在数据科学和机器学习中也被广泛使用;对于这些应用程序,Python 基本上只是一个计算器。嗯,它是一个非常强大和多功能的计算器,但我们(人类)还不够聪明,不能用头脑或纸和笔完成所有数值计算。
Python 目前(2022 年)是数据科学中最常用的数值处理程序(其他竞争者包括 R、MATLAB、Julia、JavaScript、SQL 和 C)。Python 是否会继续保持数据科学的主导语言?我不知道,但我表示怀疑。计算机科学的历史充满了声称将永存的“最终语言”。(你有没有编写过 FORTRAN、COBOL、IDL、Pascal 等?)但 Python 目前非常流行,现在你正在学习应用线性代数。无论如何,好消息是编程语言具有强大的迁移学习能力,这意味着建立 Python 熟练度将有助于你学习其他语言。换句话说,学习 Python 的时间是投资,而不是浪费。
IDE(交互式开发环境)
Python 是一种编程语言,你可以在许多不同的应用程序中运行 Python,这些应用程序被称为环境。不同的环境由不同的开发者按照不同的偏好和需求创建。你可能会遇到的一些常见 IDE 包括 Visual Studio、Spyder、PyCharm 和 Eclipse。也许最常用于学习 Python 的 IDE 称为 Jupyter 笔记本。
我使用 Google 的 Colab Jupyter 环境为本书编写了代码(下一节将详述)。一旦您熟悉了 Jupyter,可以花些时间尝试其他 IDE,看看它们是否更适合您的需求和偏好。然而,在这里我建议使用 Jupyter,因为它将帮助您跟随并重现图表。
本地和在线使用 Python
因为 Python 是免费且轻量级的,可以在各种操作系统上运行,无论是在您的计算机上还是在云服务器上:
在本地运行 Python
您可以在任何主要操作系统上安装 Python(Windows、Mac、Linux)。如果您习惯于安装程序和软件包,那么您可以根据需要安装库。对于本书,您主要需要 NumPy、matplotlib、SciPy 和 sympy。
但如果您正在阅读这篇文章,那么您的 Python 技能可能有限。在这种情况下,我建议通过Anaconda 软件包安装 Python。它是免费且易于安装,Anaconda 将自动安装您在本书中所需的所有库。
在线运行 Python
在阅读本书时,我建议在网络上运行 Python。云端 Python 的优势在于您无需在本地安装任何内容,也无需使用自己的计算资源,您可以从任何浏览器、任何计算机和任何操作系统访问您的代码。我更喜欢使用 Google 的 Colaboratory 环境,因为它与我的 Google Drive 同步。这使我可以将 Python 代码文件保存在我的 Google Drive 上,然后从https://colab.research.google.com打开它们。如果您希望避免使用 Google 服务,还有其他几个基于云的 Python 环境可供选择(尽管我不确定是否真的可能)。
Google Colab 可免费使用。您需要一个 Google 账号来访问它,但这也是免费的。然后您可以简单地将代码文件上传到您的 Google Drive,并在 Colab 中打开它们。
在 Google Colab 中使用代码文件
现在我将解释如何下载和访问本书的 Python 笔记本文件。如我之前所述,本章节没有代码文件。
有两种方法可以将书中的代码放到您的 Google Drive 上:
-
前往https://github.com/mikexcohen/LinAlg4DataScience,点击上面标有“Code”的绿色按钮,然后点击“Download ZIP”(见图 16-1)。这将下载代码仓库,然后您可以将这些文件上传到您的 Google Drive 上。现在,从您的 Google Drive 上,您可以双击文件,或者右键点击并选择“使用 Google Colaboratory”进行打开。
-
直接转到https://colab.research.google.com,选择“GitHub”选项卡,并在搜索栏中搜索“mikexcohen”。您将找到我所有的公共 GitHub 存储库;您需要选择名为“LinAlg4DataScience”的那一个。从那里,您可以点击其中一个文件来打开笔记本。
请注意,这是此笔记本的只读副本;您所做的任何更改都不会被保存。因此,建议将该文件复制到您的 Google Drive 中。

图 16-1. 从 GitHub(左)获取代码到 Colab(右)
现在您已经知道如何将书中的代码文件导入 Google Colab,是时候开始使用一个全新的笔记本来开始本章的工作了。点击菜单选项“文件”,然后选择“新笔记本”以创建一个新的笔记本。它将被称为“Untitled1.ipynb”或类似的名称。(扩展名ipynb代表“交互式 Python 笔记本”。)建议通过点击屏幕左上角的文件名来更改文件名。默认情况下,新文件会被放置在您的 Google Drive 的“Colab Notebooks”文件夹中。
变量
您可以将 Python 用作计算器。让我们试试;在代码单元格中键入以下内容:
4 + 5.6
输入该代码单元格时,什么都不会发生。您需要告诉 Python 运行该代码。在单元格处激活(如果在单元格内看到光标闪烁,则表示代码单元格处于活动状态),通过按下键盘上的 Ctrl-Enter(Mac 上为 Command-Enter)来执行此操作。还有用于在单元格中运行代码的菜单选项,但是使用键盘快捷键编码更容易和更快。
请花点时间来探索算术运算。您可以使用不同的数字、括号进行分组以及像-、/和*等不同的操作。还要注意,间距不会影响结果:2*3与2 * 3是相同的。(对于 Python 编码的其他方面,间距很重要;我们稍后会详细介绍。)
对于处理单个数字来说,这种方法并不适用于应用程序。这就是为什么我们需要变量。变量是指向存储在内存中的数据的名称。这类似于语言如何使用单词来指代现实世界中的对象。例如,我的名字是 Mike,但我不是Mike;我是由数以万亿计的细胞组成的人类,这些细胞以某种方式能够行走、说话、进食、做梦、讲冷笑话以及做其他无数的事情。但这太复杂了,无法解释,因此为了方便,人们称我为“Mike X Cohen”。因此,在 Python 中,变量只是对存储的数据(如数字、图像、数据库等)的便捷引用。
我们通过为变量分配一个值来在 Python 中创建变量。键入以下内容:
var1 = 10
var2 = 20.4
var3 = 'hello, my name is Mike'
运行该单元格将创建变量。现在您可以开始使用它们了!例如,在新单元格中运行以下代码:
var1 + var2
>> 30.4
输出
在代码块中看到的>>是运行代码单元格的结果。之后的文本是在评估单元格中的代码时在屏幕上看到的内容。
现在试试这个:
var1 + var3
啊,你刚刚遇到了你的第一个 Python 错误!欢迎加入俱乐部 😃 别担心,编码错误非常常见。实际上,优秀程序员和糟糕程序员的区别在于,优秀程序员从错误中学习,而糟糕程序员认为优秀程序员从不犯错。
Python 中的错误可能很难理解。以下是我屏幕上的错误消息:
TypeError Traceback (most recent call last)
<ipython-input-3-79613d4a2a16> in <module>()
3 var3 = 'hello, my name is Mike'
4
----> 5 var1 + var3
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Python 用箭头指示错误行。希望这个错误消息能帮助我们理解出了什么问题以及如何解决它,错误消息会打印在底部。在这种情况下,错误消息是TypeError。那是什么意思,以及什么是“类型”?
数据类型
结果表明变量有类型,描述变量存储的数据种类。不同的类型使得计算更高效,因为操作在不同类型的数据上表现不同。
Python 中有许多数据类型。我将在这里介绍四种,并在你通过本书时学习更多数据类型:
整数
这些被称为int,是整数,如−3、0、10 和 1,234。
浮点数
这些被称为float,但这只是一个带有小数点的数字的术语,如−3.1、0.12345 和 12.34。请注意,floats和ints在视觉上可能相同,但它们在 Python 函数中的处理方式是不同的。例如,3 是一个int,而 3.0 是一个float。
字符串
这些被称为str,是文本。在这里,也要注意*5*(对应于字符 5 的字符串)和5(对应于数字 5 的int)之间的区别。
列表
列表是一组项目的集合,每个项目可以有不同的数据类型。
列表在 Python 编程中非常方便且无处不在。以下代码展示了列表的三个重要特征:(1) 用方括号[ ]表示,(2) 逗号分隔列表项,(3) 单个列表项可以有不同的数据类型:
list1 = [ 1,2,3 ]
list2 = [ 'hello',123.2,[3,'qwerty'] ]
第二个列表显示列表可能包含其他列表。换句话说,list2的第三个元素本身就是一个列表。
如果你想访问list2的第二个元素怎么办?可以使用索引提取单独的列表元素,下一节我将教你如何操作。
你可以使用函数type来确定数据类型。例如,在一个新的单元格中评估以下内容:
type(var1)
嘿,等等,什么是“函数”?你可以期待在下下节中学习如何使用和创建函数;首先我想回到索引的话题。
我的变量应该叫什么?
变量命名有一些严格的规则。变量名不能以数字开头(尽管可以包含数字),也不能包含空格或类似!@#$%^&*()的非字母数字字符。下划线 _ 是允许的。
变量命名也有一些指导方针。最重要的指导方针是使变量名有意义且可解释。例如,rawDataMatrix比q要好得多作为变量名。在代码中可能会创建数十个变量,你希望能够从变量名推断出变量所引用的数据。
索引
索引意味着访问列表(以及相关数据类型,包括向量和矩阵)中的特定元素。这是如何提取列表的第二个元素:
aList = [ 19,3,4 ]
aList[1]
>> 3
注意,索引是在变量名后面使用方括号进行的,然后是你想要索引的数字。
但等一下 —— 我写道我们想要第二个元素;为什么代码访问元素1呢?这不是打字错误!Python 是一种从 0 开始索引的语言,这意味着索引0是第一个元素(在本例中是数字 19),索引1是第二个元素,依此类推。
如果你对从 0 开始的编程语言感到陌生,那么这似乎很奇怪和令人困惑。我完全理解。我希望能说练习一段时间后就能变得很熟练,但事实是从 0 开始的索引总是会引起困惑和错误。这只是你必须注意的一点。
如何访问aList中的数字 4?你可以直接索引为aList[2]。但是 Python 的索引具有一个巧妙的特性,你可以反向索引列表元素。要访问列表的最后一个元素,你可以输入aList[-1]。你可以将-1视为环绕到列表末尾。同样,倒数第二个列表元素是aList[-2],依此类推。
函数
函数是一个可以多次运行而无需重复键入所有单独代码片段的代码集合。有些函数很短,只包含几行代码,而其他函数可能有数百或数千行代码。
在 Python 中使用括号紧跟在函数名后面表示函数。以下是一些常见的函数:
type() # returns the data type
print() # prints text information to the notebook
sum() # adds numbers together
函数可以接受输入并可能提供输出。Python 函数的一般结构如下:
output1,output2 = functionname(input1,input2,input3)
回到以前的函数:
dtype = type(var1)
print(var1+var2)
total = sum([1,3,5,4])
>> 30.4
print()是一个非常有用的函数。Python 仅打印单元格中最后一行的输出,而且只有当该行不涉及变量赋值时才会这样做。例如,写下以下代码:
var1+var2
total = var1+var2
print(var1+var2)
newvar = 10
>> 30.4
有四行代码,所以你可能期望 Python 给出四个输出。但只给出一个输出,对应于print()函数。前两行不打印其输出,因为它们不是最终行,而最后一行不打印其输出,因为它是变量赋值。
方法作为函数
方法是直接在变量上调用的函数。不同的数据类型有不同的方法,这意味着适用于列表的方法可能不适用于字符串。
例如,列表数据类型有一个称为append的方法,可以向现有列表添加额外的元素。这里有一个例子:
aSmallList = [ 'one','more' ]
print(aSmallList)
aSmallList.append( 'time' )
print(aSmallList)
>> ['one','more']
['one','more','time']
注意语法格式:方法与函数类似,都有括号和(对于某些方法)输入参数。但是方法与变量名用句点连接,并且可以直接修改变量而无需显式输出。
请稍等片刻,更改代码以使用不同的数据类型——例如,使用字符串而不是列表。重新运行代码将生成以下错误消息:
AttributeError: 'str' object has no attribute 'append'
此错误消息意味着字符串数据类型不识别append函数(属性是变量的属性;方法是其中一种属性)。
方法是面向对象编程和类的核心部分。这些是 Python 的方面,应在专门的 Python 书籍中讨论,但不用担心——你不需要完全理解面向对象编程就能学习本书的线性代数部分。
编写自定义函数
Python 中有许多可用的函数。太多了,数不胜数。但永远不会有完美的函数能够完全满足你的需求。因此,你最终会编写自己的函数。
创建自己的函数既简单又方便;您使用内置关键字def定义函数(关键字是保留名称,不能重新定义为变量或函数),然后指定函数名和可能的输入,并以冒号结束该行。之后的任何行,如果缩进两个空格,则包括在函数中。¹ Python 对行首的空格要求非常严格(但对行内其他地方的空格则不那么严格)。任何输出都由return关键字指示。
让我们从一个简单的例子开始:
def add2numbers(n1,n2):
total = n1+n2
print(total)
return total
此函数接受两个输入并计算、打印和输出它们的总和。现在是调用函数的时候了:
s = add2numbers(4,5)
print(s)
>> 9
9
为什么数字9会出现两次?它被打印一次是因为在函数内部调用了print()函数,然后当我在函数后调用print(s)时,它第二次被打印出来了。要确认这一点,请尝试更改调用函数后的行为print(s+1)。 (修改代码以查看输出效果是学习 Python 的好方法;只需确保撤销您的更改。)
注意,在函数内部分配给输出的变量名(total)可以与我调用函数时使用的变量名(s)不同。
编写自定义函数允许灵活性很高——例如,设置可选输入和默认参数,检查数据类型和一致性等。但是对函数的基本理解对本书已经足够。
库
Python 设计为安装和运行都简单快速。但缺点是 Python 的基本安装只带有少量内置函数。
因此,开发人员创建了专注于特定主题的函数集合,称为库。一旦将库导入 Python,你就可以访问该库中所有可用的函数、变量类型和方法。
根据 Google 搜索,Python 有超过 130,000 个库。别担心,你不需要记住它们所有!在本书中,我们将只使用一些专门用于数值处理和数据可视化的库。线性代数最重要的库称为NumPy,这是“numerical Python”的合成词。
Python 库与基本的 Python 安装分开,这意味着你需要从网上下载它们,然后导入到 Python 中。这使得它们可以在 Python 内部使用。你只需要下载它们一次,但在每个 Python 会话中都需要重新导入它们。²
NumPy
要将 NumPy 库导入 Python,输入:
import numpy as np
注意导入库的一般公式:import libraryname as abbreviation。缩写是一个便捷的快捷方式。要访问库中的函数,你写下库的缩写名称、一个点,和函数的名称。例如:
average = np.mean([1,2,3])
sorted1 = np.sort([2,1,3])
theRank = np.linalg.matrix_rank([[1,2],[1,3]])
代码的第三行显示,库可以有子库,或者模块,嵌套在其中。在这种情况下,NumPy 有许多函数,然后在 NumPy 内部有一个名为linalg的库,其中包含更多特别与线性代数相关的函数。
NumPy 有自己的数据类型称为NumPy 数组。NumPy 数组最初看起来类似于列表,因为它们都存储信息的集合。但 NumPy 数组只存储数字,并且具有对数学编码有用的属性。以下代码显示如何创建一个 NumPy 数组:
vector = np.array([ 9,8,1,2 ])
NumPy 中的索引和切片
我想回到讨论如何访问变量内的单个元素。你可以使用索引来访问 NumPy 数组的一个元素,方法与索引列表完全相同。在以下代码块中,我使用np.arange函数创建了一个整数数组从-4 到4。在代码中没有错误,第二个输入是+5,但返回的值结束于 4。Python 通常使用排除上界,这意味着 Python 计数直到但不包括你指定的最后一个数字:
ary = np.arange(-4,5)
print(ary)
print(ary[5])
>> [-4 -3 -2 -1 0 1 2 3 4]
1
这一切都很好,但如果你想访问前三个元素呢?或者每隔一个元素呢?现在是从索引进入切片的时候了。
切片很简单:用冒号指定起始和结束索引。只需记住 Python 的范围有排除上界。因此,要获取数组的前三个元素,我们切片到索引 3 + 1 = 4,但我们需要考虑基于 0 的索引,这意味着前三个元素的索引是 0、1 和 2,我们使用0:3来切片:
ary[0:3]
>> array([-4, -3, -2])
你可以使用跳过操作符索引每个第二个元素:
ary[0:5:2]
>> array([-4, -2, 0])
索引与跳过的形式是[start:stop:skip]。你可以通过跳过-1 来反向运行整个数组,就像这样:ary[::-1]。
我知道,有点令人困惑。我保证通过实践,这将变得更容易。
可视化
线性代数中的许多概念——以及数学的大多数其他领域——最好通过计算机屏幕来理解。
Python 中的大多数数据可视化由 matplotlib 库处理。图形显示的某些方面取决于 IDE。然而,本书中的所有代码在任何 Jupyter 环境下(通过 Google Colab、另一个云服务器或本地安装)都可以直接使用。如果你使用不同的 IDE,可能需要进行一些微调。
打matplotlib.pyplot真的很繁琐,所以通常将这个库缩写为plt。你可以在下一个代码块中看到这一点。
让我们从绘制点和线开始。看看你能否理解以下代码如何映射到图 16-2:
import matplotlib.pyplot as plt
import numpy as np
plt.plot(1,2,'ko') # 1) plot a black circle
plt.plot([0,2],[0,4],'r--') # 2) plot a line
plt.xlim([-4,4]) # 3) set the x-axis limits
plt.ylim([-4,4]) # 4) set the y-axis limits
plt.title('The graph title') # 5) graph title

图 16-2. 数据可视化,第一部分
你成功解码了这段代码吗?代码行#1 表示在 XY 位置1,2处绘制一个黑色圆圈(ko中的k是黑色,o是圆圈)。代码行#2 提供的是数字列表而不是单个数字。这指定了一条从 XY 坐标(0, 0)开始并以坐标(2, 4)结束的线条。r--表示红色虚线。代码行#3 和#4 设置了x轴和y轴的限制,当然,线#5 创建了一个标题。
在继续之前,花点时间探索这段代码。画一些额外的点和线,尝试不同的标记(提示:探索字母o、s和p)和不同的颜色(尝试r、k、b、y、g和m)。
下一个代码块介绍了子图和图像。子图是将图形区域(称为图)分割成一个网格,可以在其中绘制不同的可视化方式。与前一个代码块类似,请在阅读我的描述之前看看你能否理解这段代码是如何生成图 16-3 的:
_,axs = plt.subplots(1,2,figsize=(8,5)) # 1) create subplots
axs[0].plot(np.random.randn(10,5)) # 2) line plot on the left
axs[1].imshow(np.random.randn(10,5)) # 3) image on the right

图 16-3. 数据可视化,第二部分
Code line #1 创建子图。plt.subplots 函数的前两个输入指定了网格的几何结构——在这种情况下,是一个plt.subplots 函数提供了两个输出。第一个是整个图形的句柄,我们不需要,所以使用下划线代替变量名。第二个输出是一个包含每个轴句柄的 NumPy 数组。句柄 是指向图中对象的特殊类型变量。
现在来看代码行 #2。这应该看起来很熟悉,与前一个代码块相似;两个新概念是绘制到特定轴而不是整个图形(使用 plt.)以及输入矩阵而不是单个数字。Python 为矩阵的每一列创建一个单独的线条,这就是为什么在 图 16-3 中看到五条线的原因。
最后,代码行 #3 显示了如何创建图像。矩阵经常被视为图像,正如你在 第五章 中学到的那样。图像中每个小块的颜色映射到矩阵中的一个数值。
好吧,关于在 Python 中创建图形,还有很多可以说的。但我希望这个介绍足以让你入门。
将公式翻译成代码
将数学方程转换为 Python 代码有时简单,有时困难。但这是一项重要的技能,通过练习你会有所进步。让我们从简单的例子开始,如 方程式 16-1。
方程式 16-1. 一个方程
你可能认为以下代码会起作用:
y = x**2
但你会收到一个错误消息 (NameError: name *x* is not defined)。问题在于我们在定义变量 x 之前尝试使用它。那么如何定义 x 呢?实际上,当你看数学方程时,你定义了 x 而并没有真正考虑它:x 的范围从负无穷到正无穷。但你不会画出那么远的函数 —— 你可能会选择一个有限的范围来绘制该函数,也许是 −4 到 +4。这个范围是我们在 Python 中要指定的:
x = np.arange(-4,5)
y = x**2
图 16-4 展示了使用 plt.plot(x,y,'s-') 创建的函数图。

图 16-4. 数据可视化,第三部分
看起来还行,但我觉得它有些粗糙;我希望线条更平滑。我们可以通过增加分辨率来实现这一点,这意味着在−4 到+4 之间有更多的点。我将使用函数 np.linspace(),它接受三个输入:起始值、终止值和中间点数:
x = np.linspace(-4,4,42)
y = x**2
plt.plot(x,y,'s-')
现在我们在−4 和+4 之间均匀分布的 42 个点。这使得绘图更平滑(图 16-5)。注意,np.linspace输出的向量以+4 结束。这个函数具有包含边界。有些函数是包含边界的,有些是排除边界的,这有点令人困惑。别担心,你会掌握的。

图 16-5. 数据可视化,第四部分
让我们试试另一个函数到代码的转换。我还要借此机会向你介绍一个叫做软编码的概念,这意味着为可能稍后更改的参数创建变量。
在查看我接下来的代码之前,请将以下数学函数翻译成代码并生成绘图:
这个函数称为S 型函数,在应用数学中经常用作非线性激活函数,例如在深度学习模型中。
有两种方法可以编写此函数。一种是直接将
另一种方法是将 Python 变量设置为这两个参数,然后在创建数学函数时使用这些参数。这就是软编码,它使得你的代码更易于阅读、修改和调试:
x = np.linspace(-4,4,42)
alpha = 1.4
beta = 2
num = alpha # numerator
den = 1 + np.exp(-beta*x) # denominator
fx = num / den
plt.plot(x,fx,'s-');
注意,我已经将函数创建分成了三行代码,分别指定了分子和分母,然后它们的比率。这使得你的代码更清晰、更易读。始终努力使你的代码易于阅读,因为这样做(1)减少错误的风险,(2)便于调试。
图 16-6 显示了生成的 S 型曲线。花点时间玩玩代码:改变x变量的限制和分辨率,改变alpha和beta参数的值,甚至可能改变函数本身。数学是美丽的,Python 是你的画布,代码是你的画笔!

图 16-6. 数据可视化,第五部分
打印格式化和 F-Strings
你已经知道如何使用print()函数打印出变量。但那只是打印一个变量而没有其他文本。F-Strings 允许你更好地控制输出格式。观察:
var1 = 10.54
print(f'The variable value is {var1}, which makes me happy.')
>> The variable value is 10.54, which makes me happy.
注意 f-string 的两个关键特征:在第一个引号之前的f和用花括号{}包围的变量名称,这些变量名称会被替换为变量值。
下一个代码块进一步突显了 f-strings 的灵活性:
theList = ['Mike',7]
print(f'{theList[0]} eats {theList[1]*100} grams of chocolate each day.')
>> Mike eats 700 grams of chocolate each day.
从这个例子中学到的两个关键点:(1)别担心,我实际上不会每天吃那么多巧克力(嗯,并不是每天),以及(2)你可以使用索引和大括号内的代码,Python 会输出计算结果。
f-string 格式化的最后一个特性:
pi = 22/7
print(f'{pi}, {pi:.3f}')
>> 3.142857142857143, 3.143
该代码中的关键添加是:.3f,它控制输出的格式。这段代码告诉 Python 在小数点后打印三个数字。看看当你将3改成其他整数时会发生什么,以及在冒号前包含一个整数时会发生什么。
还有许多其他的格式选项——以及其他灵活的文本输出方式——但是对于本书而言,f-strings 的基本实现就是你需要知道的全部。
控制流
编程的力量和灵活性来自于赋予代码根据某些变量或用户输入状态调整其行为的能力。代码中的动态性来自于控制流语句。
比较器
比较器是特殊字符,允许您比较不同的值。比较器的结果是一种称为布尔的数据类型,它取两个值之一:True或False。以下是几个例子:
print( 4<5 ) # 1
print( 4>5 ) # 2
print( 4==5 ) # 3
这些行的输出分别是#1 为True,#2 和#3 为False。
那第三个语句包含一个双等号符号。它与单等号符号非常不同,你已经知道它用于给变量赋值。
另外两个比较器是<=(小于或等于)和>=(大于或等于)。
如果语句
If语句很直观,因为你经常使用它们:如果我累了,那么我会休息一下眼睛。
基本的if语句有三部分:if关键字、条件语句和代码内容。条件语句是一个评估为真或假的代码片段,后面跟着一个冒号(:)。如果条件为真,则运行所有缩进的代码;如果条件为假,则不运行任何缩进的代码,并且 Python 将继续运行未缩进的代码。
这是一个例子:
var = 4
if var==4:
print(f'{var} equals 4!')
print("I'm outside the +for+ loop.")
>> 4 equals 4!
I'm outside the +for+ loop.
这里是另一个例子:
var = 4
if var==5:
print(f'{var} equals 5!')
print("I'm outside the +for+ loop.")
>> I'm outside the +for+ loop.
如果第一条消息被跳过,因为 4 不等于 5;因此,条件语句为假,因此 Python 忽略所有缩进的代码。
elif 和 else
这两个例子展示了基本的if语句形式。If语句可以包含额外的条件,以增加信息流的复杂性。在阅读我的下面对以下代码的解释和在您的计算机上输入之前,请尝试理解代码,并对打印出的消息做出预测:
var = 4
if var==5:
print('var is 5') # code 1
elif var>5:
print('var > 5') # code 2
else:
print('var < 5') # code 3
print('Outside the if-elif-else')
当 Python 遇到这样的代码语句时,它会从上到下进行处理。因此,Python 将从if后的第一个条件开始。如果该条件为真,则 Python 将运行代码 1,然后跳过所有后续的条件。也就是说,一旦 Python 遇到真条件,就会运行缩进的代码,并且if语句结束。如果后续条件也为真,Python 不会检查它们或运行它们的缩进代码。
如果第一个条件为假,Python 将继续下一个条件,即elif(短为“else if”)。同样,如果条件为真,则 Python 将运行缩进的代码,否则将跳过缩进的代码。此代码示例显示了一个elif语句,但您可以有多个这样的语句。
else语句最终没有条件。这就像if语句的“备选方案”:如果所有前面的条件都为假,则运行它。如果至少一个条件为真,则不会评估else代码。
此代码示例的输出是:
var <5
Outside the if-elif-else
多个条件
您可以使用and和or组合条件。这是“如果下雨并且我需要步行,则我会带上雨伞”的编码类比。以下是几个示例:
if 4==4 and 4<10:
print('Code example 1.')
if 4==5 and 4<10:
print('Code example 2.')
if 4==5 or 4<10:
print('Code example 3.')
>> Code example 1.
Code example 3.
文本Code example 2没有打印,因为 4 不等于 5。然而,使用or时,至少一个条件为真,因此运行了后续代码。
For循环
现在你的 Python 技能已经足以打印出 1–10 的数字。你可以使用以下代码:
print(1)
print(2)
print(3)
等等。但这不是一种可扩展的策略——如果我让你打印出一百万以内的数字呢?
在 Python 中重复代码是通过循环来完成的。最重要的一种循环称为for循环。要创建一个for循环,您指定一个可迭代对象(iterable是一个用于迭代变量中的每个元素的变量;列表可以用作可迭代对象),然后指定在for循环内运行的任意数量的代码行。我将从一个非常简单的示例开始,然后我们将进一步构建:
for i in range(0,10):
print(i+1)
运行该代码将输出 0 到 10 的数字。函数range()创建一个具有自己的数据类型range的可迭代对象,通常用于for循环。范围变量包含从 0 到 9 的整数。(不包括上限!另外,如果从 0 开始计数,则不需要第一个输入,因此range(10)与range(0,10)相同)。但我的指令是打印 1 到 10 的数字,因此我们需要在print函数内添加 1。此示例还突出显示您可以将迭代变量用作常规数值变量。
for循环可以迭代其他数据类型。考虑以下示例:
theList = [ 2,'hello',np.linspace(0,1,14) ]
for item in theList:
print(item)
现在我们正在对列表进行迭代,循环变量item在每次迭代时设置为列表中的每个项目。
嵌套控制语句
将流程控制语句嵌套在其他流程控制语句中可以为您的代码增加额外的灵活性。试着弄清楚代码的作用并对其输出进行预测。然后将其输入 Python 并测试您的假设:
powers = [0]*10
for i in range(len(powers)):
if i%2==0 and i>0:
print(f'{i} is an even number')
if i>4:
powers[i] = i**2
print(powers)
我还没有教你关于%运算符。这被称为模运算符,它返回除法后的余数。因此,7%3 = 1,因为 3 可以整除 7 两次,余数为 1。同样,6%2 = 0,因为 2 可以整除 6 三次,余数为 0。事实上,对于所有偶数,k%2 = 0;对于所有奇数,k%2 = 1。因此,类似于i%2==0的语句是测试数值变量i是偶数还是奇数的一种方法。
测量计算时间
在编写和评估代码时,您通常会想知道计算机运行某些代码需要多长时间。在 Python 中,有几种测量经过时间的方法;这里展示了一种简单的方法,使用时间库:
import time
clockStart = time.time()
# some code here...
compTime = time.time() - clockStart
这个想法是查询操作系统的本地时间两次(这是函数time.time()的输出):一次是在运行某些代码或函数之前,一次是在运行代码之后。时钟时间的差异就是计算时间。结果是以秒为单位的经过时间。通常可以将结果乘以 1,000 以将结果以毫秒(ms)打印出来。
获取帮助和进一步学习
我相信你听过这句话“数学不是一项旁观运动”。编程也是如此:学习编程的唯一方法就是编程。你会犯很多错误,会因为无法弄清楚如何让 Python 按照你的意愿运行而感到沮丧,会看到很多你无法解释的错误和警告信息,只会对宇宙和其中的一切感到非常恼火。(是的,你知道我指的是什么感觉。)
当事情出现问题时该怎么办
请允许我自我放纵地讲个笑话:四个工程师上了一辆车,但车发动不了。机械工程师说:“这可能是正时皮带的问题。”化学工程师说:“不,我认为问题在于气体/空气混合物。”电气工程师说:“听起来像是火花塞有问题。”最后,软件工程师说:“我们只需下车再上车。”
故事的寓意是,当你在代码中遇到一些无法解释的问题时,可以尝试重新启动内核,这是运行 Python 的引擎。这不会修复编程错误,但可能会解决由于变量被覆盖或重命名、内存超载或系统故障而引起的错误。在 Jupyter 笔记本中,您可以通过菜单选项重新启动内核。请注意,重新启动内核会清除所有变量和环境设置。您可能需要重新从头运行代码。
如果错误持续存在,那么搜索互联网上的错误消息、你正在使用的函数名称或问题描述的简要说明。Python 拥有庞大的国际社区,有许多在线论坛讨论和解决 Python 编程问题和困惑。
总结
掌握像 Python 这样的编程语言需要多年的专注学习和实践。即使达到一个良好的初学者水平也需要几周甚至几个月的时间。希望本章为你提供了足够的技能来完成这本书。但正如我在第一章中所写的,如果你发现自己理解数学但在代码上苦苦挣扎,那么你可能需要放下这本书,进行更多的 Python 训练,然后再回来。
另一方面,你也应该把这本书看作是提高你的 Python 编程技能的一种方式。所以如果你不理解书中的某些代码,学习线性代数就是学习更多 Python 的完美借口!
¹ 一些集成开发环境接受两个或四个空格;其他只接受四个空格。我认为两个空格看起来更清晰。
² 如果你通过 Anaconda 安装了 Python,或者你正在使用 Google 的 Colab 环境,你不需要为本书下载任何库,但你需要导入它们。
³ 据说这是千禧一代的术语,意思是“依我拙见”。


浙公网安备 33010602011771号