第六篇:NumPy 的 "线性代数" 之力:矩阵运算与应用 (应用篇)
开篇提问:
考虑一个实际问题:图像的旋转 。 当你使用图像编辑软件旋转照片时,背后是什么在驱动图像像素的精确移动? 答案是 线性代数 。 图像可以表示为 数值矩阵 ,而旋转、缩放、剪切等图像变换,都可以通过 矩阵运算 来实现。 线性代数不仅是图像处理的基石,也在 机器学习、物理模拟、工程计算 等众多领域扮演着核心角色。 它提供了一套强大的数学工具,用于 描述和解决多维空间中的问题。
NumPy,作为 Python 中科学计算的核心库,提供了 完善的线性代数运算功能 。 它不仅能高效地表示 向量和矩阵 ,还能进行各种 矩阵运算、求解线性方程组、计算特征值和特征向量 等。 今天,我们将深入探索 NumPy 的 "线性代数" 之力,掌握矩阵运算的基本技巧,并了解其在实际应用中的价值。
核心概念讲解 (费曼式解释):
-
NumPy 中的矩阵表示:
ndarray
是主力在线性代数中,矩阵 (Matrix) 是一个 按行和列排列的矩形数组 。 NumPy 主要使用 多维数组
ndarray
来表示矩阵。 虽然 NumPy 历史上也提供了一个matrix
类,但ndarray
更加通用和灵活,并且是推荐的矩阵表示方式 。 我们可以使用np.array()
创建二维ndarray
来表示矩阵。pythonimport numpy as np # 使用 np.array 创建二维数组,表示矩阵 matrix_a = np.array([[1, 2], [3, 4]]) print("矩阵 A (ndarray):\n", matrix_a) print("矩阵 A 的形状:", matrix_a.shape) # (2, 2) - 2 行 2 列 matrix_b = np.array([[5, 6], [7, 8]]) print("\n矩阵 B (ndarray):\n", matrix_b) print("矩阵 B 的形状:", matrix_b.shape) # (2, 2) - 2 行 2 列
代码解释:
- 二维
ndarray
表示矩阵: NumPy 中,我们使用 二维ndarray
来表示矩阵。np.array([[...], [...], ...])
创建的二维数组天然就符合矩阵的行列结构。 - 形状
shape
属性: 二维数组的shape
属性返回一个元组(rows, columns)
,表示矩阵的 行数和列数。
- 二维
-
NumPy 矩阵运算: 线性代数的核心操作
NumPy 提供了丰富的函数和运算符,用于执行各种线性代数中的 矩阵运算。 掌握这些运算是利用 NumPy 进行线性代数计算的基础。
-
矩阵乘法 (Matrix Multiplication):
np.dot()
,@
运算符矩阵乘法是线性代数中 最重要的运算之一 。 NumPy 提供了
np.dot(a, b)
函数以及@
运算符 (Python 3.5+) 来进行矩阵乘法。 注意: NumPy 的*
运算符执行的是元素级乘法,而不是矩阵乘法。 矩阵乘法要求 第一个矩阵的列数必须等于第二个矩阵的行数。pythonimport numpy as np matrix_a = np.array([[1, 2], [3, 4]]) # 2x2 矩阵 matrix_b = np.array([[5, 6], [7, 8]]) # 2x2 矩阵 matrix_c = np.array([[1, 2, 3], [4, 5, 6]]) # 2x3 矩阵 # 1. 使用 np.dot() 函数进行矩阵乘法 matrix_multiply_dot = np.dot(matrix_a, matrix_b) # A 乘以 B print("矩阵乘法 (np.dot(A, B)):\n", matrix_multiply_dot) # 结果是 2x2 矩阵 matrix_multiply_dot_ac = np.dot(matrix_a, matrix_c) # A 乘以 C (2x2 乘以 2x3,结果是 2x3) print("\n矩阵乘法 (np.dot(A, C)):\n", matrix_multiply_dot_ac) # 结果是 2x3 矩阵 # 2. 使用 @ 运算符进行矩阵乘法 (更简洁,推荐使用) matrix_multiply_at = matrix_a @ matrix_b # A @ B,等价于 np.dot(A, B) print("\n矩阵乘法 (A @ B):\n", matrix_multiply_at) # 结果与 np.dot(A, B) 相同 matrix_multiply_at_ac = matrix_a @ matrix_c # A @ C,等价于 np.dot(A, C) print("\n矩阵乘法 (A @ C):\n", matrix_multiply_at_ac) # 结果与 np.dot(A, C) 相同 # 尝试不符合矩阵乘法规则的形状 (例如 2x2 乘以 2x2 的元素级乘法) matrix_element_multiply = matrix_a * matrix_b # 元素级乘法 (对应位置元素相乘) print("\n元素级乘法 (A * B):\n", matrix_element_multiply) # 注意:这不是矩阵乘法!
-
矩阵转置 (Matrix Transpose):
.T
属性矩阵转置是 交换矩阵的行和列 的操作。 NumPy 中可以使用
.T
属性 快速获取矩阵的转置。pythonimport numpy as np matrix_a = np.array([[1, 2, 3], [4, 5, 6]]) # 2x3 矩阵 print("原始矩阵 A:\n", matrix_a) matrix_transpose_a = matrix_a.T # 矩阵 A 的转置 print("\n矩阵 A 的转置 (A.T):\n", matrix_transpose_a) # 结果是 3x2 矩阵,行和列互换 print("转置后矩阵的形状:", matrix_transpose_a.shape) # (3, 2)
-
矩阵求逆 (Matrix Inverse):
np.linalg.inv()
矩阵求逆是线性代数中一个重要的运算, 只有方阵 (行数和列数相等的矩阵) 才可能存在逆矩阵 。 并非所有方阵都可逆,只有行列式不为 0 的方阵 (非奇异矩阵) 才是可逆的 。 NumPy 提供了
np.linalg.inv(a)
函数来 计算方阵a
的逆矩阵。pythonimport numpy as np matrix_a = np.array([[1, 2], [3, 4]]) # 2x2 方阵 print("原始矩阵 A:\n", matrix_a) # 计算矩阵 A 的逆矩阵 try: matrix_inverse_a = np.linalg.inv(matrix_a) print("\n矩阵 A 的逆矩阵 (np.linalg.inv(A)):\n", matrix_inverse_a) # 验证逆矩阵的性质: A * A_inverse = 单位矩阵 (近似) identity_matrix_check = matrix_a @ matrix_inverse_a # 矩阵乘法 print("\n验证 A * A_inverse (应为单位矩阵):\n", identity_matrix_check) # 接近单位矩阵 (对角线为 1,其余为 0) except np.linalg.LinAlgError: print("\n矩阵 A 不可逆 (奇异矩阵)") # 如果矩阵不可逆,np.linalg.inv() 会抛出 LinAlgError 异常 # 对于奇异矩阵 (行列式为 0 的矩阵),求逆会报错 matrix_singular = np.array([[1, 2], [2, 4]]) # 奇异矩阵,行列式为 0 print("\n奇异矩阵:\n", matrix_singular) try: matrix_inverse_singular = np.linalg.inv(matrix_singular) # 会抛出 LinAlgError 异常 print("\n奇异矩阵的逆矩阵:\n", matrix_inverse_singular) # 不会执行到这里 except np.linalg.LinAlgError: print("\n奇异矩阵不可逆 (np.linalg.inv() 抛出 LinAlgError 异常)")
-
矩阵行列式 (Matrix Determinant):
np.linalg.det()
矩阵行列式是一个 标量值 ,用于 描述方阵的某些性质 ,例如 是否可逆、矩阵变换的缩放比例 等。 NumPy 提供了
np.linalg.det(a)
函数来 计算方阵a
的行列式 。 方阵可逆的条件是其行列式不为 0。pythonimport numpy as np matrix_a = np.array([[1, 2], [3, 4]]) # 2x2 方阵 print("矩阵 A:\n", matrix_a) # 计算矩阵 A 的行列式 determinant_a = np.linalg.det(matrix_a) print("\n矩阵 A 的行列式 (np.linalg.det(A)):\n", determinant_a) # -2.0 不为 0,矩阵 A 可逆 matrix_singular = np.array([[1, 2], [2, 4]]) # 奇异矩阵 print("\n奇异矩阵:\n", matrix_singular) determinant_singular = np.linalg.det(matrix_singular) print("\n奇异矩阵的行列式 (np.linalg.det(奇异矩阵)):\n", determinant_singular) # 0.0 为 0,奇异矩阵不可逆
-
矩阵特征值和特征向量 (Eigenvalues and Eigenvectors):
np.linalg.eig()
特征值和特征向量是线性代数中 非常重要的概念 ,它们描述了 线性变换的本质特征 。 对于方阵
A
,特征向量v
是指 经过A
变换后,方向保持不变,只发生缩放的向量 , 缩放比例就是特征值λ
。 数学表示为:Av = λv
。 NumPy 提供了np.linalg.eig(a)
函数来 计算方阵a
的特征值和特征向量。pythonimport numpy as np matrix_a = np.array([[1, -2], [2, -3]]) # 2x2 方阵 print("矩阵 A:\n", matrix_a) # 计算矩阵 A 的特征值和特征向量 eigenvalues, eigenvectors = np.linalg.eig(matrix_a) # 返回两个数组:特征值和特征向量 print("\n矩阵 A 的特征值 (eigenvalues):\n", eigenvalues) # 特征值数组 print("\n矩阵 A 的特征向量 (eigenvectors):\n", eigenvectors) # 特征向量数组 (按列排列,每一列是一个特征向量) # 验证特征值和特征向量的性质: A @ v = λ * v (近似) # 取第一个特征值和第一个特征向量进行验证 eigenvalue_1 = eigenvalues[0] # 第一个特征值 eigenvector_1 = eigenvectors[:, 0] # 第一个特征向量 (注意 eigenvectors 是按列排列的) print("\n验证第一个特征值和特征向量:") print("特征值 λ1:", eigenvalue_1) print("特征向量 v1:", eigenvector_1) av = matrix_a @ eigenvector_1 # A 乘以 v1 lambda_v = eigenvalue_1 * eigenvector_1 # λ1 乘以 v1 print("\nA @ v1:\n", av) print("\nλ1 * v1:\n", lambda_v) # A @ v1 和 λ1 * v1 应该近似相等 (由于浮点数精度问题,可能 не完全相等) # 可以看到 A @ v1 和 λ1 * v1 在数值上非常接近,验证了特征值和特征向量的性质
-
-
求解线性方程组 (Solving Linear Equations):
np.linalg.solve()
线性方程组是线性代数中的重要应用。 NumPy 提供了
np.linalg.solve(a, b)
函数来 求解线性方程组Ax = b
,其中A
是 系数矩阵 ,b
是 常数向量 ,x
是 未知数向量 。np.linalg.solve()
可以 直接解出未知数向量x
。 方程组要有唯一解,系数矩阵A
必须是方阵且可逆 (非奇异矩阵)。pythonimport numpy as np # 求解线性方程组: # x + 2y = 5 # 3x + 4y = 13 # 系数矩阵 A matrix_a = np.array([[1, 2], [3, 4]]) print("系数矩阵 A:\n", matrix_a) # 常数向量 b vector_b = np.array([5, 13]) print("\n常数向量 b:\n", vector_b) # 使用 np.linalg.solve(A, b) 求解线性方程组 Ax = b solution_x = np.linalg.solve(matrix_a, vector_b) # 求解 x print("\n线性方程组的解 x (np.linalg.solve(A, b)):\n", solution_x) # [3. 1.] 解为 x=3, y=1 # 验证解是否正确: A @ x 是否等于 b (近似) b_check = matrix_a @ solution_x print("\n验证 A @ x 是否等于 b:\n", b_check) # [ 5. 13.] 与向量 b 近似相等,解正确
代码解释:
- 线性方程组
Ax = b
的矩阵表示: 线性方程组可以表示为矩阵形式Ax = b
,其中A
是系数矩阵,x
是未知数向量,b
是常数向量。 np.linalg.solve(a, b)
:a
是 系数矩阵A
,b
是 常数向量b
, 函数返回 解向量x
。
- 线性方程组
-
案例应用: 图像旋转 (使用矩阵乘法)
我们回到文章开篇提到的 图像旋转 案例,演示如何使用 NumPy 的矩阵运算,实现图像的 旋转变换 。 这里我们以 灰度图像 为例,演示 逆时针旋转图像 45 度。
pythonimport numpy as np from PIL import Image # 1. 读取灰度图像并转换为 NumPy 数组 image_path = "your_image.jpg" # 替换成你的图像文件路径 (建议使用正方形灰度图像,旋转效果更佳) img = Image.open(image_path).convert('L') # 打开图像并转换为灰度模式 image_array = np.array(img) # 转换为 NumPy 数组 (二维数组) print("原始图像数组的形状:", image_array.shape) # (height, width) # 2. 定义旋转角度 (逆时针 45 度,转换为弧度) angle_degrees = 45 angle_radians = np.deg2rad(angle_degrees) # 角度转弧度 # 3. 构建 2D 旋转矩阵 rotation_matrix = np.array([ [np.cos(angle_radians), -np.sin(angle_radians)], [np.sin(angle_radians), np.cos(angle_radians)] ]) print("\n旋转矩阵 (2D, 逆时针 45 度):\n", rotation_matrix) # 4. 获取图像中心坐标 (作为旋转中心) image_height, image_width = image_array.shape center_x, center_y = image_width // 2, image_height // 2 # 图像中心坐标 (整数) # 5. 创建新的旋转后图像数组 (初始化为黑色,与原始图像形状相同) rotated_image_array = np.zeros_like(image_array) # 创建与原始图像形状和数据类型相同的全零数组 # 6. 遍历原始图像的每个像素,计算旋转后的坐标,并赋值到新的图像数组中 for y in range(image_height): for x in range(image_width): # 将像素坐标转换为相对于图像中心的坐标 offset_x = x - center_x offset_y = y - center_y # 应用旋转矩阵进行坐标变换 (矩阵乘法) rotated_offset_coords = rotation_matrix @ np.array([offset_x, offset_y]) # 矩阵乘法 rotated_x_offset, rotated_y_offset = rotated_offset_coords # 将相对于中心偏移的坐标转换回图像像素坐标 rotated_x = int(rotated_x_offset + center_x + 0.5) # 加 0.5 并取整,四舍五入 rotated_y = int(rotated_y_offset + center_y + 0.5) # 加 0.5 并取整,四舍五入 # 检查旋转后的坐标是否在图像边界内 if 0 <= rotated_x < image_width and 0 <= rotated_y < image_height: rotated_image_array[rotated_y, rotated_x] = image_array[y, x] # 将原始像素值赋值给旋转后的图像对应位置 # 7. 将旋转后的 NumPy 数组转换回 PIL 图像对象 rotated_img = Image.fromarray(rotated_image_array) # 8. 保存并显示旋转后的图像 output_path = "rotated_image_45.jpg" rotated_img.save(output_path) rotated_img.show() print(f"\n旋转 {angle_degrees} 度后的图像已保存到: {output_path}")
代码解释:
- 读取灰度图像并转换为 NumPy 数组: 与之前案例相同,将图像转换为灰度模式并加载为 NumPy 数组。
- 定义旋转角度和构建旋转矩阵: 定义旋转角度 (度数),并使用
np.deg2rad()
将角度转换为弧度。 根据旋转角度,构建 2D 逆时针旋转矩阵。 旋转矩阵是线性代数中用于描述旋转变换的矩阵。 - 获取图像中心坐标: 计算图像的中心像素坐标,作为旋转中心。
- 创建新的旋转后图像数组: 创建一个与原始图像形状相同、数据类型相同的 全零数组,用于存储旋转后的图像像素值。
- 遍历像素并应用旋转变换: 遍历原始图像的每个像素 ,对于每个像素:
- 坐标偏移: 将像素坐标转换为 相对于图像中心的坐标偏移量。
- 矩阵乘法: 使用 旋转矩阵与坐标偏移量向量进行矩阵乘法,计算旋转后的坐标偏移量。
- 坐标反偏移: 将旋转后的坐标偏移量转换回 图像像素坐标。
- 边界检查: 检查旋转后的坐标是否仍然在图像边界内。 如果超出边界,则忽略该像素。
- 像素赋值: 如果旋转后的坐标在边界内,则将 原始像素值赋值给旋转后的图像数组的对应位置。
- NumPy 数组转换回 PIL 图像对象、保存和显示: 与之前案例相同,将 NumPy 数组转换回 PIL 图像对象,并保存和显示旋转后的图像。
这个案例演示了如何使用 NumPy 的矩阵运算 (矩阵乘法) 来实现图像的旋转变换。 图像旋转的核心数学原理就是 坐标的线性变换 ,而线性变换可以用 矩阵乘法 来表示。 通过这个案例,可以体会到线性代数在图像处理等实际应用中的强大威力。
费曼回顾 (知识巩固):
现在,请你用自己的话,总结一下今天我们学习的 NumPy 线性代数运算的知识,包括:
- NumPy 中如何表示矩阵? 推荐使用
matrix
类还是ndarray
? 为什么? - 我们学习了哪些常用的 NumPy 矩阵运算? 矩阵乘法、矩阵转置、矩阵求逆、矩阵行列式、矩阵特征值和特征向量,它们分别有什么作用和应用? 如何使用 NumPy 函数实现这些运算?
- 什么是线性方程组? 如何使用 NumPy 求解线性方程组?
np.linalg.solve()
函数有什么作用? - 在图像旋转的案例中,我们是如何运用 NumPy 的矩阵运算来实现图像变换的? 旋转矩阵是什么? 矩阵乘法在图像旋转中起什么作用?
像给你的同学讲解一样,用清晰简洁的语言解释这些概念,并结合图像旋转的案例,帮助他们理解 NumPy 线性代数运算的强大功能和应用价值。
课后思考 (拓展延伸):
- 尝试修改图像旋转案例的代码,例如:
- 调整旋转角度,看看不同的旋转效果?
- 实现 图像缩放、剪切 等其他图像变换 (提示: 查找 缩放矩阵、剪切矩阵 的公式,并修改代码中的旋转矩阵部分)?
- 尝试处理 彩色图像 的旋转 (提示: 彩色图像是三维数组,可以分别对每个颜色通道应用旋转变换)?
- 思考一下,除了图像处理,NumPy 的线性代数功能还可以应用在哪些科学计算和数据分析场景中? 例如,机器学习 (例如线性回归、PCA 降维)、物理模拟 (例如力学分析、电路分析)、金融建模等等。 你有什么新的应用场景想法吗?
- 尝试查阅 NumPy 官方文档或其他线性代数教程,了解更多关于 NumPy 线性代数运算的高级特性,例如:
- 奇异值分解 (SVD):
np.linalg.svd()
- 特征值分解 (EVD):
np.linalg.eig()
的更深入应用 - 各种矩阵分解 (decomposition) 方法: 例如 QR 分解、LU 分解等
- 线性代数在机器学习和深度学习中的应用
- 奇异值分解 (SVD):
恭喜你!完成了 NumPy 费曼学习法的第六篇文章学习! 你已经掌握了 NumPy 的 "线性代数" 之力,可以开始探索更高级的科学计算和数据分析应用了! 下一篇文章,也是本系列的 最后一篇文章,我们将一起展望 NumPy 的 "进阶之路",总结 NumPy 的常用技巧和性能优化方法,并探讨 NumPy 在数据科学生态系统中的地位和未来发展方向,为你的 NumPy 学习之旅画上一个圆满的句号! 敬请期待!