考虑一个实际问题:图像的旋转。 当你使用图像编辑软件旋转照片时,背后是什么在驱动图像像素的精确移动? 答案是 线性代数。 图像可以表示为 数值矩阵,而旋转、缩放、剪切等图像变换,都可以通过 矩阵运算 来实现。 线性代数不仅是图像处理的基石,也在 机器学习、物理模拟、工程计算 等众多领域扮演着核心角色。 它提供了一套强大的数学工具,用于 描述和解决多维空间中的问题。
NumPy,作为 Python 中科学计算的核心库,提供了 完善的线性代数运算功能。 它不仅能高效地表示 向量和矩阵,还能进行各种 矩阵运算、求解线性方程组、计算特征值和特征向量 等。 今天,我们将深入探索 NumPy 的 “线性代数” 之力,掌握矩阵运算的基本技巧,并了解其在实际应用中的价值。
NumPy 中的矩阵表示: ndarray
是主力
在线性代数中,矩阵 (Matrix) 是一个 按行和列排列的矩形数组。 NumPy 主要使用 多维数组 ndarray
来表示矩阵。 虽然 NumPy 历史上也提供了一个 matrix
类,但 ndarray
更加通用和灵活,并且是推荐的矩阵表示方式。 我们可以使用 np.array()
创建二维 ndarray
来表示矩阵。
import 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 的 *
运算符执行的是元素级乘法,而不是矩阵乘法。 矩阵乘法要求 第一个矩阵的列数必须等于第二个矩阵的行数。
import 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
属性 快速获取矩阵的转置。
import 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
的逆矩阵。
import 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。
import 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
的特征值和特征向量。
import 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
必须是方阵且可逆 (非奇异矩阵)。
import 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 度。
import 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}")
代码解释:
np.deg2rad()
将角度转换为弧度。 根据旋转角度,构建 2D 逆时针旋转矩阵。 旋转矩阵是线性代数中用于描述旋转变换的矩阵。这个案例演示了如何使用 NumPy 的矩阵运算 (矩阵乘法) 来实现图像的旋转变换。 图像旋转的核心数学原理就是 坐标的线性变换,而线性变换可以用 矩阵乘法 来表示。 通过这个案例,可以体会到线性代数在图像处理等实际应用中的强大威力。
现在,请你用自己的话,总结一下今天我们学习的 NumPy 线性代数运算的知识,包括:
matrix
类还是 ndarray
? 为什么?np.linalg.solve()
函数有什么作用?像给你的同学讲解一样,用清晰简洁的语言解释这些概念,并结合图像旋转的案例,帮助他们理解 NumPy 线性代数运算的强大功能和应用价值。
np.linalg.svd()
np.linalg.eig()
的更深入应用恭喜你!完成了 NumPy 费曼学习法的第六篇文章学习! 你已经掌握了 NumPy 的 “线性代数” 之力,可以开始探索更高级的科学计算和数据分析应用了! 下一篇文章,也是本系列的 最后一篇文章,我们将一起展望 NumPy 的 “进阶之路”,总结 NumPy 的常用技巧和性能优化方法,并探讨 NumPy 在数据科学生态系统中的地位和未来发展方向,为你的 NumPy 学习之旅画上一个圆满的句号! 敬请期待!