要深入机器学习,您需要掌握大量线性代数知识,而本教程正是围绕这一点展开。本文将回顾提升直觉所需的线性代数主题,帮助您理解机器学习方法在何时、为何奏效,重点从向量与矩阵层面切入。
Contents
Introduction
如果您想学习数据科学,必须攻克的三大基础主题之一便是机器学习。当您真正深入机器学习时,重要的是更进一步,培养对算法工作原理的直觉与理解。
我想,正在努力成为数据科学家的您大概会同意:走到这一步并不轻松。
但也不必如此艰难。
SciPy versus NumPy
从 DataCamp 的 NumPy 教程 中,您会了解到它是 Python 科学计算的核心库之一。该库包含一组工具与技术,可用于在计算机上求解科学与工程中的数学模型。而您最常用的工具,莫过于高性能的多维数组对象:它是一种强大的数据结构,能高效地对数组和矩阵进行计算。
而 SciPy 基本上就是基于 NumPy。
它同样是科学计算的核心包,提供大量数学算法与便捷函数,但其构建在 Python 的 NumPy 扩展之上。这意味着 SciPy 与 NumPy 往往一起使用!
在本教程的后续部分,您会看到这两个库的协作是多么顺理成章。
Interact with NumPy and SciPy
要高效地与这两个包交互,您首先需要了解该库及其强大数据结构的一些基础。围绕这些数组,有海量的高阶数学函数可对这些矩阵与数组进行运算。
接下来,您将快速浏览为了高效使用 SciPy 需要掌握的主题。核心在于理解数组结构、如何处理数据类型,以及如何调整数组形状。如果这些听起来还很陌生,建议先学习 DataCamp 的 Python NumPy 教程 ,并参考 NumPy 速查表 。
如果您已经都掌握了,可以跳过下面这一节,直接前往“Linear Algebra With SciPy”,但别忘了准备好用于线性代数的 SciPy 速查表 !
The Essentials of NumPy ndarray Objects
从结构上讲,数组本质上不过是指针的集合。它由内存地址、数据类型、形状以及步长组成。它包含关于原始数据、如何定位元素以及如何解释元素的信息。
当您深入了解数组的底层细节时,内存地址与步长很重要;而数据类型与形状则是初学者必须掌握的。另有两个您可能会用到的属性是 data 与 size,它们能让您获取更多数组信息。
在以下 DataCamp Light 代码块中复习 ndarray 属性的用法。数组 myArray 已经加载,您可以在 IPython shell 中键入并回车查看。
# Inspect the data of `myArray`
print(myArray.data)
# Inspect the data type of `myArray`
print(_________________)
# Inspect the shape of `myArray`
print(__________________)
# Inspect the size of `myArray`
print(__________________)
您会在上方代码块的输出中看到,myArray 的数据类型为 int64。当您深入使用数组时,一定会记得 可以用 astype() 方法在不同数据类型间转换。
不过,在同时使用 SciPy 与 NumPy 时,下面这些 NumPy 的类型处理函数也很有用,尤其是在处理复数时:
# Return the real part of `myArray` elements
np.real(myArray)
# Return the imaginary part of `myArray` elements
np.imag(myArray)
# Return a real array if the complex parts are close to 0
np.real_if_close(myArray,tol=1000)
# Cast `myArray` to float
np.cast['f'](myArray)
尝试加入 print() 调用查看上述代码的结果。您将看到复数同时拥有实部与虚部。np.real() 与 np.imag() 分别用于返回这两部分。
或者,您也可以使用 np.cast 将数组对象转换为其他数据类型,例如上例中的 float。
上述代码中唯一稍难的是 np.real_if_close()。当输入复数(如 myArray)时,如果复部接近 0,它会返回实数组。这个“接近 0”的阈值可通过传入参数 tol 自行调整。试着改变这个阈值看看会发生什么!
Array Creation
您已经知道如何检查数组并调整其数据类型,但还没有明确看到如何创建数组。您应已知道可以使用 np.array() 来创建,但还有其他数组创建例程值得了解:np.eye() 和 np.identity()。
np.eye() 函数允许您创建一个方阵,其维度等于传入的正整数参数。元素通常为 0,只有对角线为 1。np.identity() 的作用与之相同,也会返回一个单位阵。
不过,请注意 np.eye() 还可接受额外参数 k,用于指定要填充 1 的对角线索引位置。
其他在进行线性代数矩阵运算时非常有用的数组创建函数包括:
np.arange() 在两个数之间按固定步长创建等间距数组,您可以指定元素间距;
np.linspace() 也能创建等间距数组,但通过指定数组中元素个数来控制;
np.logspace() 同样创建等间距数组,不过是在对数刻度上。即间距为对数意义上的等间距:在以 10 为底的对数空间中等分两个数的对数之间的区间。
这些函数以网格形式创建数组,可用于生成网格点(mesh grid),本文前面也曾提及。
注意 :下面的代码块中已将 numpy 以 np 导入!
# Create a 2X2 identity matrix with `np.eye()`
__________________
# Create a 3X3 identity matrix with `np.identity()`
__________________
# Uniformly spaced values: spacing
_________(3, 7, 2)
# Uniformly spaced values: number of samples
___________(2, 3, 5)
# Uniformly spaced values: logarithmic spacing
___________(2, 3, 4)
既然已经复习了如何处理数据类型,接下来该讨论索引与切片了。
Indexing and Slicing
索引基本上是使用方括号 [] 来定位数组的值。换言之,当您想访问数组中的部分元素(子集)时,可以使用索引。切片与子集操作很相似,但更进阶一些:切片数组时,您不只是索取特定的单个值,而是按“区域”来处理数据,而非单纯的“位置”。
通过以下代码示例复习这两个概念:
# Slice `myArray` at index 0 and 1
print(____________)
# Slice `my_2dArray` at row 0 and 1, column 1
print(________________)
# Slice `my_3dArray` at row 1
print(my_3dArray[1,...])
# Boolean indexing: only values < 2
print(myArray[____________])
# Fancy indexing
print(my_2dArray[[1, 0, 1, 0],[0, 1, 2, 0]])
既然已经温习了切片与索引,您或许也会对一些索引技巧感兴趣,它们能在与 SciPy 进行科学计算时提升效率。四个常见函数是 np.mgrid()、np.ogrid()、np.r 与 np.c。
如果您有一定 NumPy 经验,可能已熟悉后两个函数。np.r 与 np.c 常用于按行或按列堆叠数组。借助这些函数,您可以快速构造数组,无需使用 np.concatenate()。
# Create a dense meshgrid
np.mgrid[1:11:2, -12:-3:3]
# Create an open meshgrid
np.ogrid[1:11:2, -12:-3:3]
# Stack arrays vertically
np.r_[3,[0]*5,-1:1:10j]
# Stack arrays horizontally
np.c_[array, my_2dArray]
看到前两个函数时,您可能会问,为何需要 meshgrid?meshgrid 可用于在规则网格的每个位置生成包含 x 与 y 坐标的两个数组。np.meshgrid() 接受两个一维数组,并生成两个二维矩阵,分别对应两数组中所有 (x, y) 对。
np.mgrid() 是 MATLAB meshgrid 的实现,返回形状相同的数组。也就是说,输出数组的维度与数量等于索引维度的数量。
np.ogrid() 则给出开放网格,密度不如 np.mgrid() 的结果。您可以在上方代码块中直观看到差异。
您也可以在这里 阅读二者的区别。
另一个可用于索引/切片的函数是 np.select()。它可根据您在第一个参数中指定的条件,从数组列表中返回值;在第二个参数中,传入用于此次选择的数组。
看看下面的示例:
# Initialize a 2D array
my_2dArray = np.array([[1,2,3,4], [5,6,7,8]], dtype=np.int64)
# Select values from `my_2dArray`
np.select([my_2dArray < 4],[my_2dArray * 2])
太棒了!在选出原数组中合适的值后,您仍可以继续选择形状并操作新数组。
Shape Selection and Manipulation
NumPy 提供了大量用于选择与操作数组形状的方法,您可能已经熟悉不少。以下仅对可能有用的函数作简要概述,因此并不面面俱到。
有没有在使用 SciPy 时格外顺手的函数?
确实有,最常用的是能帮助您展平数组、堆叠与拆分数组的函数。您已经看到经常作为 np.concatenate() 替代的 np.c 与 np.r,但还有更多值得了解!
例如,使用 np.hstack() 横向堆叠数组,使用 np.vstack() 纵向堆叠数组。同理,np.vsplit() 与 np.hsplit() 可分别纵向与横向拆分数组。想必这些您大多已经知道了。
在下面的代码块中验证一下吧:
#Stack arrays horizontally (column-wise)
print(_________((np.eye(2), my_2dArray)))
# Stack arrays vertically (row-wise)
print(__________((myArray, my_2dArray)))
# Split the array horizontally at the 2nd index
print(___________(my_2dArray, 2))
# Split the array vertically at the 2nd index
print(____________(my_2dArray, 2))
请记住 :np.eye() 会创建一个 2×2 的单位阵,非常适合与上方代码中已加载的二维数组进行堆叠。
若想了解堆叠数组时需要注意的条件,请前往这里 。当然,您也可以观察数组与函数行为,总结出行或列拼接需要遵守的“规则”。
拆分数组时,最重要的或许是数组的形状,因为您需要选择正确的索引位置进行拆分。
除了堆叠与拆分例程,以及进一步操作数组的函数外,还有所谓的“向量化”也很重要。对数组应用函数时,通常会将其作用到每个元素上。例如将 np.cos()、np.sin() 或 np.tan() 应用于数组,都会对所有元素生效。看到这种行为,您就知道该函数是向量化的。
但当您自己定义函数时(在进行科学计算时很常见),也可能希望将其向量化。在这种情况下,可调用 np.vectorize():
# Define a function `myfunc`
def myfunc(a,b):
if a > b:
return a - b
else:
return a + b
# Vectorize `myfunc`
vectorizedFunc = np.vectorize(myfunc)
# Apply
vectorizedFunc(myArray, 2)
关于其他向量化数学函数,您可以关注 np.angle()(返回复数数组元素的辐角),以及常见的三角、指数与对数函数等。
Linear Algebra with SciPy
既然已经知道如何将两个包的优势结合起来,现在就来深入本教程的主题:线性代数。
不过在开始用 Python 之前,请确保您的工作环境就绪!
Install SciPy
当然,首先要确保已安装 Python。若尚未安装,请前往此页面 :)若在 Windows 上工作,请确保已将 Python 添加到 PATH 环境变量 。此外,别忘了安装包管理器,比如 pip,以便使用 Python 的开源库。
注意,较新的 Python 3 版本自带 pip,请先确认您是否已有;若已有,在安装其他包之前先将其升级:
pip install pip -- upgrade pip -- version技术支持
但仅安装包管理器还不够;您还需要下载该库的 wheel 文件:前往这里 获取 SciPy 的 wheel。下载完成后,在下载目录打开终端进行安装。随后,您可以检查安装是否成功并确认所用包的版本:
# Install the wheel install "scipy‑0.18.1‑cp36‑cp36m‑win_amd64.whl" # Confirm successful installimport scipy# Check package versionscipy. __version__技术支持
完成这些步骤后,您就可以开始了!
提示 :建议通过安装 Anaconda Python 发行版来获取该包。它是快速入门的便捷方式,Anaconda 不仅包含100 个最受欢迎的数据科学相关 Python、R 与 Scala 包 ,还集成了 Jupyter 与 Spyder 等多种开放的开发环境。若您想开始使用 Jupyter Notebook,请查看这份Jupyter notebook 教程 。
如果您还没有下载,请前往这里 获取。
Vectors and Matrices: The Basics
在准备好工作环境后,终于可以在 Python 中开始学习线性代数了。其本质是研究向量空间以及它们之间的线性映射。这些线性映射可以用矩阵描述,也便于计算。
请记住 ,向量空间是线性代数中的基本概念。它是一个“空间”,其中包含一组对象(向量),并且两个向量相加或缩放后,结果向量仍不会离开该空间。还要记得,向量是矩阵的行(或列)。
那么在 Python 中如何实现?
您可以用 np.array() 轻松创建向量。类似地,可以使用 np.matrix() 或 np.mat() 将任意一维或二维 ndarray 赋予矩阵结构。
在下面的代码块中试试看:
# Create a vector
myVector = np.array([1,2,3,4])
# Create a matrix
myMatrix = np.matrix(np.random.random((5,5)))
那么,数组与矩阵只是格式不同、其他都相同吗?
并不完全如此,仍有一些区别:
矩阵是二维的,而数组通常是 n 维;
正如上方函数所暗示,矩阵是 ndarray 的子类;
数组与矩阵都有 .T(),但只有矩阵拥有 .H() 与 .I();
矩阵乘法与数组的按元素乘法不同;
此外,** 运算在矩阵与数组上的结果也不同。
在处理矩阵时,您有时会遇到其中大多数元素为 0 的矩阵,称为“稀疏矩阵”;相对地,非零元素占多数的称为“稠密矩阵”。
这看似琐碎,但在使用 SciPy 进行线性代数计算时,会影响到您选择的模块。更具体地说,稠密矩阵可使用 scipy.linalg,而处理稀疏矩阵时,可以考虑 scipy.sparse 模块,其中也包含 scipy.sparse.linalg。
对于稀疏矩阵,创建方式很多。下面的代码块列出了一些:
# Create a 2X2 identity matrix
np.eye(3, k=1)
# Create a 2x2 identity matrix
np.mat(np.identity(2))
# Compressed Sparse Row matrix
sparse.csr_matrix(C)
# Compressed Sparse Column matrix
sparse.csc_matrix(C)
# Dictionary Of Keys matrix
sparse.dok_matrix(C)
此外,还有其他可用于创建稀疏矩阵的函数:使用 bsr_matrix() 创建块稀疏行(Block Sparse Row)矩阵,使用 coo_matrix() 创建坐标格式(COO)稀疏矩阵,使用 dia_matrix() 创建对角存储(DIA)稀疏矩阵,使用 lil_matrix() 创建基于行的链表(LIL)稀疏矩阵。
选择很多,那么自己动手创建稀疏矩阵时该如何选择?
其实并不难。
基本要点首先在于您打算如何初始化;其次要考虑您希望对稀疏矩阵进行什么操作。
更具体地,可参考以下清单来决定使用哪种稀疏矩阵:
如果计划逐个填充数值,使用 coo_matrix() 或 dok_matrix() 创建;
如果希望用某个数组作为对角线初始化,使用 dia_matrix();
对基于切片的矩阵,使用 lil_matrix();
若要从小矩阵块构造大矩阵,考虑使用 bsr_matrix();
若需要快速按行或按列访问,请将矩阵分别转换为 csr_matrix() 或 csc_matrix()。这两个函数并不适合用来初始化矩阵,但在矩阵乘法时您会明显感到速度差异。
轻松拿捏!
Vector Operations
在了解或复习了向量、稠密矩阵与稀疏矩阵的区别后,是时候更仔细地看看向量以及可对其执行的数学运算了。本节明确聚焦于数学运算,以便您看到与矩阵的异同;毕竟线性代数很大一部分最终都在与矩阵打交道。
您已看到用 np.array() 创建向量相当容易。现在拥有了向量,您可能还想了解一些基本操作。下面的代码块中已为您加载了 vector1 与 vector2:
# Addition of `vector1` and `vector2`
vector3 = vector1 + vector2
# Print `vector3`
print(_______)
# Subtraction of `vector2` and `vector1`
vector4 = ________ - vector1
# print `vector4`
print(vector4)
# Dot product of `vector1` and `vector2`
dotProduct = np.dot(vector1, vector2)
# Print `dotProduct`
print(dotProduct)
# Cross product of `vector1` and `vector2`
crossProduct = np.cross(________, vector2)
# Print `crossProduct`
print(____________)
既然已经顺利完成了一些向量运算,接下来就进入真正的矩阵环节!
Matrices: Operations and routines
与上一节开头类似,您已经知道如何创建矩阵,但还不知道如何加以利用。本节将概览一些矩阵函数与基本矩阵例程,助您高效工作。
首先,我们来看看一些函数。如果您用过 NumPy,这些会很容易上手;即便没有经验,也会发现它们并不难用。
看看以下函数示例。
np.add() 与 np.subtract() 可对数组或矩阵进行加减,np.divide() 与 np.multiply 可进行除法与乘法。这并不神秘,对吧?上一节中用于计算点积的 np.dot() 同样可用于矩阵,只要传入两个矩阵而非向量即可。
这些都很基础,对吧?
我们稍微进阶一点。关于乘法,还有其他可考虑的函数,例如用于向量点积的 np.vdot(),用于数组内积或外积的 np.inner() 与 np.outer(),用于张量点积的 np.tensordot(),以及计算两个数组的 Kronecker 积的 np.kron():
# Vector dot product
vectorDotProduct = np.vdot(vector1, vector2)
# Inner product
innerProduct = np.inner(vector1, vector2)
# Outer product
outerProduct = np.outer(vector1, vector2)
# Tensor dot product
tensorDotProduct = np.tensordot(matrix1, matrix2)
# Kronecker product
kronProduct = np.kron(matrix1, matrix2)
提示 :为上面的代码块增加打印语句,查看各个乘积的结果。
此外,linalg 模块中也有一些函数值得考虑:矩阵指数函数 linalg.expm()、linalg.expm2() 与 linalg.expm3()。它们的区别在于指数的具体计算方式。一般可使用第一个进行矩阵指数计算,但不妨都试试,看看结果差异!
还有三角函数(linalg.cosm()、linalg.sinm()、linalg.tanm())、双曲三角函数(linalg.coshm()、linalg.sinhm()、linalg.tanhm())、符号函数 linalg.signm()、矩阵对数 linalg.logm() 与矩阵平方根 linalg.sqrtm() 等。
另外,您还可以使用 linalg.funm() 来计算矩阵函数。看下面的示例:
linalg.funm(myMatrix, lambda x: x*x)
您将矩阵作为第一个参数传入,第二个参数传入要应用的函数(示例中为 lambda 函数)。注意传给 linalg.funm() 的函数需要是向量化的。
现在看看一些基本的矩阵例程。首先可能想尝试的是矩阵属性:T 表示转置,H 表示共轭转置,I 表示逆矩阵,A 表示转换为数组。
试一试:
# Transposition
myMatrix._
# Conjugate transposition
myMatrix._
# Inverse
myMatrix._
# Array
myMatrix._
对矩阵进行转置,就是将原矩阵的列变为行、行变为列。共轭转置则对每个元素进行行列互换并取共轭。矩阵的逆是一个矩阵,与原矩阵相乘会得到单位矩阵。
除了这些属性外,还有一些函数可用于执行基本矩阵例程,例如 np.transpose() 与 linalg.inv() 分别用于转置与求逆。
此外,您可以使用 np.trace() 获取主对角线上元素之和(迹)。同样,您也可以用 NumPy 的 linalg.matrix_rank 获取矩阵的秩,即奇异值分解中大于某个阈值的奇异值数量。
如果现在还不理解秩也别担心;稍后会进一步讲解。
目前先关注另外两个可用的例程:
可用 linalg.norm 计算矩阵的范数:它是基于矩阵元素定义的一个数。范数是有用的量,能提供关于矩阵大小的重要信息。
此外,您还可以使用 linalg.det() 计算行列式,这是由方阵元素计算得出的重要值。行列式将方阵“压缩”为一个数,用来判定该方阵是否可逆。
最后,求解大型线性方程组是矩阵最基本的应用之一。对于 \(Ax = b\) ,其中 \(A\) 为方阵、\(b\) 为一般矩阵,您可以根据矩阵类型选择两种方法来求解 \(x\) :
# Dense matrix solver
linalg.solve(A, b)
# Linear least-square solver
linalg.lstsq(F,E)
对于稀疏矩阵,可使用 linalg.spsolve()。当无法精确求解时,仍可使用 linalg.lstsq() 获得近似的 \(x\) 。
提示 :别错过 DataCamp 的 SciPy 速查表 。
既然已经了解了如何创建矩阵并将其用于数学运算,接下来就该攻克更进阶的主题——这些对于真正深入机器学习至关重要。
Eigenvalues and Eigenvectors
首先要攻克的是特征值与特征向量。
特征值是洞察矩阵本质的新方式。但在进一步展开之前,先来解释什么是特征向量。几乎所有向量在被矩阵相乘后都会改变方向。然而,有些“例外”的结果向量与相乘前的向量方向相同,这些就是特征向量。
换言之,将一个特征向量与矩阵相乘,所得结果等于将该特征向量与特征值 \(\lambda\) 相乘:\[Ax = \lambda x. \]
这意味着特征值提供了非常有价值的信息:它告诉您某个特征向量在与矩阵相乘后,是被拉伸、压缩、反向,还是保持不变。
# Solve eigenvalue problem
la, v = linalg.eig(myMatrix)
# Unpack eigenvalues
l1, l2 = la
# First eigenvector
v[:,0]
# Second eigenvector
v[:,1]
# Or unpack eigenvalues with `eigvals()`
linalg.eigvals(myMatrix)
您可以使用 SciPy 的 linalg 模块中的 eig() 函数来求解方阵的普通或广义特征值问题。
注意,eigvals() 函数也可用于仅解出矩阵的特征值。
处理稀疏矩阵时,可使用 scipy.sparse 模块来寻找特征值与特征向量:
la, v = sparse. linalg. eigs ( myMatrix, 1 ) 技术支持
注意 :上述代码中指定了要提取的特征值与特征向量的数量,即 1。
特征值与特征向量在许多计算机视觉与机器学习技术中都至关重要,例如用于降维的主成分分析(PCA),以及用于人脸识别 的 EigenFaces。
Singular Value Decomposition (SVD)
接下来,如果您想真正学好数据科学,就需要了解 SVD。矩阵 \(A\) 的奇异值分解是将 \(A\) 分解(或因式分解)为三个矩阵的乘积:\(A = U * \Sigma * V^t\) 。
若矩阵 \(A\) 的大小为 \(M\) × \(N\) ,则三个矩阵的大小为:
矩阵 \(U\) 大小为 \(M\) × \(M\)
矩阵 \(V\) 大小为 \(N\) × \(N\)
矩阵 \(\Sigma\) 大小为 \(M\) × \(N\)
\(*\) 表示矩阵相乘,\(^t\) 表示转置,即行列互换。
简而言之,奇异值分解提供了一种将矩阵拆解为更简单且有意义部分的方法,而这些部分可能包含我们感兴趣的数据。
# Singular Value Decomposition
U,s,Vh = linalg.svd(myMatrix)
# Initialize `M` and `N`
M,N = myMatrix.shape
# Construct sigma matrix in SVD
Sig = linalg.diagsvd(s,M,N)
注意 :对于稀疏矩阵,可使用 sparse.linalg.svds() 进行分解。
如果您是数据科学新手,矩阵分解起初可能显得晦涩,似乎难以立刻找到应用场景。但 SVD 在许多任务中都非常有用,例如数据压缩、降噪与数据分析。下面将展示如何用 SVD 进行图像压缩:
# Import the necessary packagesimport numpy as npfrom scipy import linalgfrom skimage import dataimport matplotlib. pyplot as plt# Get an image from ` skimage ` img= data. camera ( ) # Check number of singular valueslinalg. svdvals ( img) # Singular Value DecompositionU, s, Vh = linalg. svd ( img) # Use only 32 singular valuesA = np. dot ( U [ : , 0 : 32 ] , np. dot ( np. diag ( s[ 0 : 32 ] ) , Vh[ 0 : 32 , : ] ) ) fig = plt. figure ( figsize= ( 8 , 3 ) ) # Add a subplot to the figureax = fig. add_subplot ( 121 ) # Plot ` img ` on grayscaleax. imshow ( img, cmap= 'gray' ) # Add a second subplot to the figureax2 = fig. add_subplot ( 122 ) # Plot ` A ` in the second subplotax2. imshow ( A ) # Add a titlefig. suptitle ( 'Image Compression with SVD' , fontsize= 14 , fontweight= 'bold' ) # Show the plotplt. show ( ) 技术支持
将得到如下结果:
还请参考以下使用 SVD 的示例:
SVD 与主成分分析(PCA)密切相关,后者用于降维:二者都会得到一组由数据特征空间轴的线性组合构成的“新坐标轴”。这些“新坐标轴”按各个方向对数据方差的贡献来分解数据的方差。想了解 PCA 在数据上的具体效果,请参阅我们的 Scikit-Learn 教程 。
另一个相关领域是数据挖掘与自然语言处理(NLP):潜在语义索引(LSI)。这是一种用于文档检索与词语相似性的技术。潜在语义索引利用 SVD 将文档与其中包含的不同词汇所构成的概念关联起来。多个词可以被归纳为同一概念。在这里,SVD 同样可以削弱词与文档之间的噪声相关性,并减少原始数据的维度。
可见,SVD 是您数据科学之旅中必须掌握的重要概念。建议您在本教程之外进一步深入了解 SVD,例如前往此页面 阅读更多相关内容。
要深入机器学习,您需要掌握大量线性代数知识,而本教程正是围绕这一点展开。本文将回顾提升直觉所需的线性代数主题,帮助您理解机器学习方法在何时、为何奏效,重点从向量与矩阵层面切入。
读完本教程,希望您能更自信地深入理解某个算法!
Contents
Introduction
如果您想学习数据科学,必须攻克的三大基础主题之一便是机器学习。当您真正深入机器学习时,重要的是更进一步,培养对算法工作原理的直觉与理解。
我想,正在努力成为数据科学家的您大概会同意:走到这一步并不轻松。
但也不必如此艰难。
SciPy versus NumPy
从 DataCamp 的 NumPy 教程中,您会了解到它是 Python 科学计算的核心库之一。该库包含一组工具与技术,可用于在计算机上求解科学与工程中的数学模型。而您最常用的工具,莫过于高性能的多维数组对象:它是一种强大的数据结构,能高效地对数组和矩阵进行计算。
而 SciPy 基本上就是基于 NumPy。
它同样是科学计算的核心包,提供大量数学算法与便捷函数,但其构建在 Python 的 NumPy 扩展之上。这意味着 SciPy 与 NumPy 往往一起使用!
在本教程的后续部分,您会看到这两个库的协作是多么顺理成章。
Interact with NumPy and SciPy
要高效地与这两个包交互,您首先需要了解该库及其强大数据结构的一些基础。围绕这些数组,有海量的高阶数学函数可对这些矩阵与数组进行运算。
接下来,您将快速浏览为了高效使用 SciPy 需要掌握的主题。核心在于理解数组结构、如何处理数据类型,以及如何调整数组形状。如果这些听起来还很陌生,建议先学习 DataCamp 的 Python NumPy 教程,并参考 NumPy 速查表。
如果您已经都掌握了,可以跳过下面这一节,直接前往“Linear Algebra With SciPy”,但别忘了准备好用于线性代数的 SciPy 速查表!
The Essentials of NumPy
ndarrayObjects从结构上讲,数组本质上不过是指针的集合。它由内存地址、数据类型、形状以及步长组成。它包含关于原始数据、如何定位元素以及如何解释元素的信息。
当您深入了解数组的底层细节时,内存地址与步长很重要;而数据类型与形状则是初学者必须掌握的。另有两个您可能会用到的属性是
data与size,它们能让您获取更多数组信息。在以下 DataCamp Light 代码块中复习
ndarray属性的用法。数组myArray已经加载,您可以在 IPython shell 中键入并回车查看。您会在上方代码块的输出中看到,
myArray的数据类型为int64。当您深入使用数组时,一定会记得可以用astype()方法在不同数据类型间转换。不过,在同时使用 SciPy 与 NumPy 时,下面这些 NumPy 的类型处理函数也很有用,尤其是在处理复数时:
尝试加入
print()调用查看上述代码的结果。您将看到复数同时拥有实部与虚部。np.real()与np.imag()分别用于返回这两部分。或者,您也可以使用
np.cast将数组对象转换为其他数据类型,例如上例中的 float。上述代码中唯一稍难的是
np.real_if_close()。当输入复数(如myArray)时,如果复部接近 0,它会返回实数组。这个“接近 0”的阈值可通过传入参数tol自行调整。试着改变这个阈值看看会发生什么!Array Creation
您已经知道如何检查数组并调整其数据类型,但还没有明确看到如何创建数组。您应已知道可以使用
np.array()来创建,但还有其他数组创建例程值得了解:np.eye()和np.identity()。np.eye()函数允许您创建一个方阵,其维度等于传入的正整数参数。元素通常为 0,只有对角线为 1。np.identity()的作用与之相同,也会返回一个单位阵。不过,请注意
np.eye()还可接受额外参数k,用于指定要填充 1 的对角线索引位置。其他在进行线性代数矩阵运算时非常有用的数组创建函数包括:
np.arange()在两个数之间按固定步长创建等间距数组,您可以指定元素间距;np.linspace()也能创建等间距数组,但通过指定数组中元素个数来控制;np.logspace()同样创建等间距数组,不过是在对数刻度上。即间距为对数意义上的等间距:在以 10 为底的对数空间中等分两个数的对数之间的区间。这些函数以网格形式创建数组,可用于生成网格点(mesh grid),本文前面也曾提及。
注意:下面的代码块中已将
numpy以np导入!既然已经复习了如何处理数据类型,接下来该讨论索引与切片了。
Indexing and Slicing
索引基本上是使用方括号
[]来定位数组的值。换言之,当您想访问数组中的部分元素(子集)时,可以使用索引。切片与子集操作很相似,但更进阶一些:切片数组时,您不只是索取特定的单个值,而是按“区域”来处理数据,而非单纯的“位置”。通过以下代码示例复习这两个概念:
既然已经温习了切片与索引,您或许也会对一些索引技巧感兴趣,它们能在与 SciPy 进行科学计算时提升效率。四个常见函数是
np.mgrid()、np.ogrid()、np.r与np.c。如果您有一定 NumPy 经验,可能已熟悉后两个函数。
np.r与np.c常用于按行或按列堆叠数组。借助这些函数,您可以快速构造数组,无需使用np.concatenate()。看到前两个函数时,您可能会问,为何需要 meshgrid?meshgrid 可用于在规则网格的每个位置生成包含 x 与 y 坐标的两个数组。
np.meshgrid()接受两个一维数组,并生成两个二维矩阵,分别对应两数组中所有 (x, y) 对。np.mgrid()是 MATLAB meshgrid 的实现,返回形状相同的数组。也就是说,输出数组的维度与数量等于索引维度的数量。np.ogrid()则给出开放网格,密度不如np.mgrid()的结果。您可以在上方代码块中直观看到差异。您也可以在这里阅读二者的区别。
另一个可用于索引/切片的函数是
np.select()。它可根据您在第一个参数中指定的条件,从数组列表中返回值;在第二个参数中,传入用于此次选择的数组。看看下面的示例:
太棒了!在选出原数组中合适的值后,您仍可以继续选择形状并操作新数组。
Shape Selection and Manipulation
NumPy 提供了大量用于选择与操作数组形状的方法,您可能已经熟悉不少。以下仅对可能有用的函数作简要概述,因此并不面面俱到。
有没有在使用 SciPy 时格外顺手的函数?
确实有,最常用的是能帮助您展平数组、堆叠与拆分数组的函数。您已经看到经常作为
np.concatenate()替代的np.c与np.r,但还有更多值得了解!例如,使用
np.hstack()横向堆叠数组,使用np.vstack()纵向堆叠数组。同理,np.vsplit()与np.hsplit()可分别纵向与横向拆分数组。想必这些您大多已经知道了。在下面的代码块中验证一下吧:
请记住:
np.eye()会创建一个 2×2 的单位阵,非常适合与上方代码中已加载的二维数组进行堆叠。若想了解堆叠数组时需要注意的条件,请前往这里。当然,您也可以观察数组与函数行为,总结出行或列拼接需要遵守的“规则”。
拆分数组时,最重要的或许是数组的形状,因为您需要选择正确的索引位置进行拆分。
除了堆叠与拆分例程,以及进一步操作数组的函数外,还有所谓的“向量化”也很重要。对数组应用函数时,通常会将其作用到每个元素上。例如将
np.cos()、np.sin()或np.tan()应用于数组,都会对所有元素生效。看到这种行为,您就知道该函数是向量化的。但当您自己定义函数时(在进行科学计算时很常见),也可能希望将其向量化。在这种情况下,可调用
np.vectorize():关于其他向量化数学函数,您可以关注
np.angle()(返回复数数组元素的辐角),以及常见的三角、指数与对数函数等。Linear Algebra with SciPy
既然已经知道如何将两个包的优势结合起来,现在就来深入本教程的主题:线性代数。
不过在开始用 Python 之前,请确保您的工作环境就绪!
Install SciPy
当然,首先要确保已安装 Python。若尚未安装,请前往此页面:)若在 Windows 上工作,请确保已将 Python 添加到 PATH 环境变量。此外,别忘了安装包管理器,比如
pip,以便使用 Python 的开源库。注意,较新的 Python 3 版本自带 pip,请先确认您是否已有;若已有,在安装其他包之前先将其升级:
但仅安装包管理器还不够;您还需要下载该库的 wheel 文件:前往这里获取 SciPy 的 wheel。下载完成后,在下载目录打开终端进行安装。随后,您可以检查安装是否成功并确认所用包的版本:
完成这些步骤后,您就可以开始了!
提示:建议通过安装 Anaconda Python 发行版来获取该包。它是快速入门的便捷方式,Anaconda 不仅包含100 个最受欢迎的数据科学相关 Python、R 与 Scala 包,还集成了 Jupyter 与 Spyder 等多种开放的开发环境。若您想开始使用 Jupyter Notebook,请查看这份Jupyter notebook 教程。
如果您还没有下载,请前往这里获取。
Vectors and Matrices: The Basics
在准备好工作环境后,终于可以在 Python 中开始学习线性代数了。其本质是研究向量空间以及它们之间的线性映射。这些线性映射可以用矩阵描述,也便于计算。
请记住,向量空间是线性代数中的基本概念。它是一个“空间”,其中包含一组对象(向量),并且两个向量相加或缩放后,结果向量仍不会离开该空间。还要记得,向量是矩阵的行(或列)。
那么在 Python 中如何实现?
您可以用
np.array()轻松创建向量。类似地,可以使用np.matrix()或np.mat()将任意一维或二维ndarray赋予矩阵结构。在下面的代码块中试试看:
那么,数组与矩阵只是格式不同、其他都相同吗?
并不完全如此,仍有一些区别:
n维;ndarray的子类;.T(),但只有矩阵拥有.H()与.I();**运算在矩阵与数组上的结果也不同。在处理矩阵时,您有时会遇到其中大多数元素为 0 的矩阵,称为“稀疏矩阵”;相对地,非零元素占多数的称为“稠密矩阵”。
这看似琐碎,但在使用 SciPy 进行线性代数计算时,会影响到您选择的模块。更具体地说,稠密矩阵可使用
scipy.linalg,而处理稀疏矩阵时,可以考虑scipy.sparse模块,其中也包含scipy.sparse.linalg。对于稀疏矩阵,创建方式很多。下面的代码块列出了一些:
此外,还有其他可用于创建稀疏矩阵的函数:使用
bsr_matrix()创建块稀疏行(Block Sparse Row)矩阵,使用coo_matrix()创建坐标格式(COO)稀疏矩阵,使用dia_matrix()创建对角存储(DIA)稀疏矩阵,使用lil_matrix()创建基于行的链表(LIL)稀疏矩阵。选择很多,那么自己动手创建稀疏矩阵时该如何选择?
其实并不难。
基本要点首先在于您打算如何初始化;其次要考虑您希望对稀疏矩阵进行什么操作。
更具体地,可参考以下清单来决定使用哪种稀疏矩阵:
coo_matrix()或dok_matrix()创建;dia_matrix();lil_matrix();bsr_matrix();csr_matrix()或csc_matrix()。这两个函数并不适合用来初始化矩阵,但在矩阵乘法时您会明显感到速度差异。轻松拿捏!
Vector Operations
在了解或复习了向量、稠密矩阵与稀疏矩阵的区别后,是时候更仔细地看看向量以及可对其执行的数学运算了。本节明确聚焦于数学运算,以便您看到与矩阵的异同;毕竟线性代数很大一部分最终都在与矩阵打交道。
您已看到用
np.array()创建向量相当容易。现在拥有了向量,您可能还想了解一些基本操作。下面的代码块中已为您加载了vector1与vector2:既然已经顺利完成了一些向量运算,接下来就进入真正的矩阵环节!
Matrices: Operations and routines
与上一节开头类似,您已经知道如何创建矩阵,但还不知道如何加以利用。本节将概览一些矩阵函数与基本矩阵例程,助您高效工作。
首先,我们来看看一些函数。如果您用过 NumPy,这些会很容易上手;即便没有经验,也会发现它们并不难用。
看看以下函数示例。
np.add()与np.subtract()可对数组或矩阵进行加减,np.divide()与np.multiply可进行除法与乘法。这并不神秘,对吧?上一节中用于计算点积的np.dot()同样可用于矩阵,只要传入两个矩阵而非向量即可。这些都很基础,对吧?
我们稍微进阶一点。关于乘法,还有其他可考虑的函数,例如用于向量点积的
np.vdot(),用于数组内积或外积的np.inner()与np.outer(),用于张量点积的np.tensordot(),以及计算两个数组的 Kronecker 积的np.kron():提示:为上面的代码块增加打印语句,查看各个乘积的结果。
此外,
linalg模块中也有一些函数值得考虑:矩阵指数函数linalg.expm()、linalg.expm2()与linalg.expm3()。它们的区别在于指数的具体计算方式。一般可使用第一个进行矩阵指数计算,但不妨都试试,看看结果差异!还有三角函数(
linalg.cosm()、linalg.sinm()、linalg.tanm())、双曲三角函数(linalg.coshm()、linalg.sinhm()、linalg.tanhm())、符号函数linalg.signm()、矩阵对数linalg.logm()与矩阵平方根linalg.sqrtm()等。另外,您还可以使用
linalg.funm()来计算矩阵函数。看下面的示例:您将矩阵作为第一个参数传入,第二个参数传入要应用的函数(示例中为 lambda 函数)。注意传给
linalg.funm()的函数需要是向量化的。现在看看一些基本的矩阵例程。首先可能想尝试的是矩阵属性:
T表示转置,H表示共轭转置,I表示逆矩阵,A表示转换为数组。试一试:
对矩阵进行转置,就是将原矩阵的列变为行、行变为列。共轭转置则对每个元素进行行列互换并取共轭。矩阵的逆是一个矩阵,与原矩阵相乘会得到单位矩阵。
除了这些属性外,还有一些函数可用于执行基本矩阵例程,例如
np.transpose()与linalg.inv()分别用于转置与求逆。此外,您可以使用
np.trace()获取主对角线上元素之和(迹)。同样,您也可以用 NumPy 的linalg.matrix_rank获取矩阵的秩,即奇异值分解中大于某个阈值的奇异值数量。如果现在还不理解秩也别担心;稍后会进一步讲解。
目前先关注另外两个可用的例程:
linalg.norm计算矩阵的范数:它是基于矩阵元素定义的一个数。范数是有用的量,能提供关于矩阵大小的重要信息。linalg.det()计算行列式,这是由方阵元素计算得出的重要值。行列式将方阵“压缩”为一个数,用来判定该方阵是否可逆。最后,求解大型线性方程组是矩阵最基本的应用之一。对于 \(Ax = b\),其中 \(A\) 为方阵、\(b\) 为一般矩阵,您可以根据矩阵类型选择两种方法来求解 \(x\):
对于稀疏矩阵,可使用
linalg.spsolve()。当无法精确求解时,仍可使用linalg.lstsq()获得近似的 \(x\)。提示:别错过 DataCamp 的 SciPy 速查表。
既然已经了解了如何创建矩阵并将其用于数学运算,接下来就该攻克更进阶的主题——这些对于真正深入机器学习至关重要。
Eigenvalues and Eigenvectors
首先要攻克的是特征值与特征向量。
特征值是洞察矩阵本质的新方式。但在进一步展开之前,先来解释什么是特征向量。几乎所有向量在被矩阵相乘后都会改变方向。然而,有些“例外”的结果向量与相乘前的向量方向相同,这些就是特征向量。
换言之,将一个特征向量与矩阵相乘,所得结果等于将该特征向量与特征值 \(\lambda\) 相乘:\[Ax = \lambda x. \]
这意味着特征值提供了非常有价值的信息:它告诉您某个特征向量在与矩阵相乘后,是被拉伸、压缩、反向,还是保持不变。
您可以使用 SciPy 的
linalg模块中的eig()函数来求解方阵的普通或广义特征值问题。注意,
eigvals()函数也可用于仅解出矩阵的特征值。处理稀疏矩阵时,可使用
scipy.sparse模块来寻找特征值与特征向量:注意:上述代码中指定了要提取的特征值与特征向量的数量,即 1。
特征值与特征向量在许多计算机视觉与机器学习技术中都至关重要,例如用于降维的主成分分析(PCA),以及用于人脸识别的 EigenFaces。
Singular Value Decomposition (SVD)
接下来,如果您想真正学好数据科学,就需要了解 SVD。矩阵 \(A\) 的奇异值分解是将 \(A\) 分解(或因式分解)为三个矩阵的乘积:\(A = U * \Sigma * V^t\)。
若矩阵 \(A\) 的大小为 \(M\) × \(N\),则三个矩阵的大小为:
\(*\) 表示矩阵相乘,\(^t\) 表示转置,即行列互换。
简而言之,奇异值分解提供了一种将矩阵拆解为更简单且有意义部分的方法,而这些部分可能包含我们感兴趣的数据。
注意:对于稀疏矩阵,可使用
sparse.linalg.svds()进行分解。如果您是数据科学新手,矩阵分解起初可能显得晦涩,似乎难以立刻找到应用场景。但 SVD 在许多任务中都非常有用,例如数据压缩、降噪与数据分析。下面将展示如何用 SVD 进行图像压缩:
将得到如下结果:
还请参考以下使用 SVD 的示例:
可见,SVD 是您数据科学之旅中必须掌握的重要概念。建议您在本教程之外进一步深入了解 SVD,例如前往此页面阅读更多相关内容。
What’s up Next?
您已读到本教程的最后!接下来如何前行,完全取决于您。
但且慢!
别错过更深入线性代数的课程:本教程只是入门,远未覆盖全部内容!
当然,也可以考虑学习 DataCamp 的 机器学习教程,在完成这篇关于线性代数的 Scipy 教程后,它一定会为您的学习路径增色。如果您想回到基础,也可复习我们的 NumPy 教程 或 Intermediate Python for Data Science 课程。