费曼学习法8 - NumPy 数组的 “算术魔法”:基本运算与广播机制 (进阶篇)
第三篇:NumPy 数组的 “算术魔法”:基本运算与广播机制 (进阶篇)
开篇提问:
想象一下,你正在处理一张 医学影像,比如一张 X 光片。为了更清晰地分析骨骼结构,你可能需要 调整图像的亮度,让骨骼区域更突出。 或者,为了 对比不同时间点的影像,你需要 计算两张影像的差异,找出病灶的变化。 这些图像处理任务,以及许多其他数据分析和科学计算场景,都离不开 高效且灵活的数值运算。
NumPy 数组,作为 Python 中处理数值数据的核心工具,不仅提供了 简洁的语法 来进行基本的算术运算,更拥有 强大的广播 (broadcasting) 机制,使得不同形状的数组也能进行运算,极大地提升了数据处理的效率和代码的简洁性。 今天,我们就来深入探索 NumPy 数组的 “算术魔法”,掌握基本运算和广播机制,让你的数据处理能力更上一层楼。
核心概念讲解 (费曼式解释):
-
NumPy 数组的基本算术运算: “简单计算,强大结果”
NumPy 数组支持 元素级 (element-wise) 的基本算术运算。 这意味着,当你对两个 NumPy 数组进行加、减、乘、除等运算时,运算会 自动应用到数组的每个对应位置的元素上,得到一个新的数组作为结果。 这种元素级运算方式,不仅 代码简洁,而且由于 NumPy 底层使用高度优化的 C 代码实现,运算效率远高于 Python 循环。
-
加法 (+), 减法 (-), 乘法 (*), 除法 (/), 幂运算 (), 整除 (//), 取余 (%)**
这些运算符可以直接应用于 NumPy 数组之间,或者数组与标量 (单个数值) 之间。
import numpy as np # 创建两个 NumPy 数组 array1 = np.array([1, 2, 3, 4]) array2 = np.array([5, 6, 7, 8]) # 数组加法 addition_result = array1 + array2 print("数组加法 (array1 + array2):\n", addition_result) # [ 6 8 10 12] # 数组减法 subtraction_result = array2 - array1 print("\n数组减法 (array2 - array1):\n", subtraction_result) # [4 4 4 4] # 数组乘法 (元素级乘法,不是矩阵乘法) multiplication_result = array1 * array2 print("\n数组乘法 (array1 * array2):\n", multiplication_result) # [ 5 12 21 32] # 数组除法 division_result = array2 / array1 print("\n数组除法 (array2 / array1):\n", division_result) # [5. 3. 2.33333333 2. ] # 数组幂运算 power_result = array1 ** 2 print("\n数组幂运算 (array1 ** 2):\n", power_result) # [ 1 4 9 16] # 数组整除 floor_division_result = array2 // array1 print("\n数组整除 (array2 // array1):\n", floor_division_result) # [5 3 2 2] # 数组取余 modulo_result = array2 % array1 print("\n数组取余 (array2 % array1):\n", modulo_result) # [0 0 1 0] # 数组与标量运算 (广播机制的初步体现) scalar = 2 scalar_addition_result = array1 + scalar print("\n数组与标量加法 (array1 + scalar):\n", scalar_addition_result) # [3 4 5 6] scalar_multiplication_result = array1 * scalar print("\n数组与标量乘法 (array1 * scalar):\n", scalar_multiplication_result) # [2 4 6 8]
代码解释:
- 元素级运算: 所有运算都是针对数组中 对应位置的元素 进行的。 例如
array1 + array2
,实际上是array1[0] + array2[0]
,array1[1] + array2[1]
, … - 数组与标量运算: 当数组与标量进行运算时,标量会被 “广播” (broadcast) 到数组的 每个元素 上进行运算 (我们会在下一节详细讲解广播机制)。
-
-
广播 (Broadcasting) 机制: “让运算兼容不同形状的数组”
广播 (broadcasting) 是 NumPy 中一个非常强大的特性。 它允许 NumPy 在 形状不完全相同的数组之间 进行算术运算,而无需显式地进行数组扩展或复制。 广播机制的核心思想是 “扩展较小数组的形状,使其与较大数组兼容,从而进行元素级运算”。 这大大简化了代码,并提高了运算效率。
广播的规则:
NumPy 的广播遵循一套严格的规则,以确定两个数组是否可以进行广播,以及如何进行广播。 简单来说,两个数组的形状 (shape) 要么是 完全相同 的,要么是 兼容 的。 形状兼容需要满足以下两个条件之一:
- 维度相同: 两个数组的维度数相同,且 对应维度的大小相等,或者其中一个数组的 某个维度的大小为 1。
- 维度不同: 两个数组的维度数不同,但 较小维度数组的形状可以 “扩展” 到较大维度数组的形状。 通常较小维度数组会在 前面 “补 1”,使其维度数与较大维度数组相同,然后再进行维度大小的兼容性判断。
广播的步骤 (简化的理解):
- 维度扩展 (Prepend 1s): 如果两个数组的维度数不同,NumPy 会在 维度较小的数组的形状前面 “补 1”,直到两个数组的维度数相同。
- 维度大小兼容性检查 (Dimension Compatibility): 从 后往前 (从最后一个维度开始) 比较两个数组的形状的 每个维度的大小:
- 如果维度大小 相等,则兼容。
- 如果其中一个数组的维度大小为 1,则兼容 (大小为 1 的维度可以 “扩展” 到另一个数组对应维度的大小)。
- 如果两个数组的维度大小 不相等,且都不为 1,则 不兼容,无法广播,会报错。
- 广播运算 (Broadcasting Operation): 对于兼容的维度,NumPy 会 “虚拟地” 扩展大小为 1 的维度,使其与另一个数组对应维度的大小相同,然后进行元素级运算。 “虚拟地” 扩展意味着 NumPy 并没有实际复制数据,只是在运算时 逻辑上将数据 “复制” 多份,从而节省了内存和计算资源。
广播示例:
import numpy as np # 1. 标量与数组广播 (标量是 0 维数组,可以广播到任意形状的数组) scalar = 2 array1 = np.array([1, 2, 3]) scalar_broadcast_result = array1 + scalar # 标量 2 被广播到数组 [1, 2, 3] 的每个元素上 print("标量与数组广播 (array1 + scalar):\n", scalar_broadcast_result) # [3 4 5] # 2. 一维数组与二维数组广播 (一维数组的形状 (3,) 可以广播到二维数组 (2, 3)) array2_1d = np.array([1, 2, 3]) # 形状 (3,) array2_2d = np.array([[10, 20, 30], [40, 50, 60]]) # 形状 (2, 3) vector_matrix_broadcast_result = array2_2d + array2_1d # 一维数组 [1, 2, 3] 被广播到二维数组的每一行 print("\n一维数组与二维数组广播 (array2_2d + array2_1d):\n", vector_matrix_broadcast_result) # [[11 22 33] # [41 52 63]] # 3. 不同维度大小的二维数组广播 (形状 (3, 1) 和 (3, 4) 可以广播) array3_1 = np.array([[1], [2], [3]]) # 形状 (3, 1) array3_2 = np.array([[10, 20, 30, 40], [50, 60, 70, 80], [90, 100, 110, 120]]) # 形状 (3, 4) matrix_matrix_broadcast_result = array3_2 + array3_1 # 形状 (3, 1) 的数组被广播到形状 (3, 4) 的每一列 print("\n不同维度大小的二维数组广播 (array3_2 + array3_1):\n", matrix_matrix_broadcast_result) # [[11 21 31 41] # [52 62 72 82] # [93 103 113 123]] # 4. 不兼容的广播示例 (形状 (3, 2) 和 (2, 3) 无法广播,维度大小不兼容) array4_1 = np.array([[1, 2], [3, 4], [5, 6]]) # 形状 (3, 2) array4_2 = np.array([[10, 20, 30], [40, 50, 60]]) # 形状 (2, 3) # 尝试广播会报错 ValueError: operands could not be broadcast together with shapes (3,2) (2,3) # incompatible_broadcast_result = array4_1 + array4_2 # ValueError: operands could not be broadcast together with shapes (3,2) (2,3) # print("\n不兼容的广播示例 (array4_1 + array4_2):\n", incompatible_broadcast_result) # 会报错
代码解释:
- 标量广播: 标量 (0 维数组) 可以广播到 任意形状 的数组。
- 一维数组广播到二维数组: 一维数组的形状
(n,)
可以广播到二维数组的形状(m, n)
,一维数组会 沿着行方向 (axis=0) 广播,也就是 “复制” 多行。 - 不同维度大小的二维数组广播: 形状为
(m, 1)
的二维数组可以广播到形状为(m, n)
的二维数组,形状为(m, 1)
的数组会 沿着列方向 (axis=1) 广播,也就是 “复制” 多列。 - 不兼容的广播: 如果两个数组的形状 不满足广播规则,NumPy 会 报错
ValueError: operands could not be broadcast together with shapes ...
。 需要检查数组的形状,确保满足广播条件,或者手动调整数组形状使其兼容。
-
常用数学函数: “现成的工具,高效分析”
NumPy 提供了 大量预定义的数学函数 (ufuncs, universal functions),可以直接应用于 NumPy 数组,进行各种数学运算、统计分析、逻辑运算等。 这些函数也是 元素级运算,并且经过高度优化,运算效率非常高。
-
常用数学函数示例:
np.sum()
: 求和 (可以指定axis
参数按行或列求和)np.mean()
: 求平均值 (可以指定axis
参数按行或列求平均值)np.max()
: 求最大值 (可以指定axis
参数按行或列求最大值)np.min()
: 求最小值 (可以指定axis
参数按行或列求最小值)np.std()
: 求标准差 (可以指定axis
参数按行或列求标准差)np.var()
: 求方差 (可以指定axis
参数按行或列求方差)np.sin()
: 正弦函数 (元素级计算正弦值)np.cos()
: 余弦函数 (元素级计算余弦值)np.exp()
: 指数函数 (元素级计算指数值,e 的 x 次方)np.log()
: 对数函数 (元素级计算自然对数)np.sqrt()
: 平方根函数 (元素级计算平方根)np.abs()
: 绝对值函数 (元素级计算绝对值)np.round()
: 四舍五入函数 (元素级四舍五入)- … (NumPy 提供了大量的数学函数,可以查阅 NumPy 文档了解更多)
-
axis
参数: 控制运算方向许多 NumPy 数学函数 (如
np.sum()
,np.mean()
,np.max()
,np.min()
等) 都支持axis
参数,用于 指定运算的轴 (维度)。axis=0
: 沿着第 0 轴 (列) 进行运算。 对于二维数组,就是 按列运算,结果的维度会 减少一维 (行维度会消失)。axis=1
: 沿着第 1 轴 (行) 进行运算。 对于二维数组,就是 按行运算,结果的维度会 减少一维 (列维度会消失)。axis=None
: 对所有元素进行运算 (默认值)。 结果会 降维到标量 (0 维数组)。
-
数学函数示例:
import numpy as np # 创建一个 (3, 4) 的二维数组 array1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) print("二维数组:\n", array1) # 1. 求和 (np.sum()) total_sum = np.sum(array1) # 对所有元素求和 print("\n所有元素求和 (np.sum(array1)):\n", total_sum) # 78 column_sums = np.sum(array1, axis=0) # 按列求和 (axis=0) print("\n按列求和 (np.sum(array1, axis=0)):\n", column_sums) # [15 18 21 24] row_sums = np.sum(array1, axis=1) # 按行求和 (axis=1) print("\n按行求和 (np.sum(array1, axis=1)):\n", row_sums) # [10 26 42] # 2. 求平均值 (np.mean()) average_value = np.mean(array1) # 所有元素平均值 print("\n所有元素平均值 (np.mean(array1)):\n", average_value) # 6.5 column_means = np.mean(array1, axis=0) # 按列求平均值 print("\n按列求平均值 (np.mean(array1, axis=0)):\n", column_means) # [5. 6. 7. 8.] row_means = np.mean(array1, axis=1) # 按行求平均值 print("\n按行求平均值 (np.mean(array1, axis=1)):\n", row_means) # [2.5 6.5 10.5] # 3. 求最大值 (np.max()) max_value = np.max(array1) # 所有元素最大值 print("\n所有元素最大值 (np.max(array1)):\n", max_value) # 12 column_maxes = np.max(array1, axis=0) # 按列求最大值 print("\n按列求最大值 (np.max(array1, axis=0)):\n", column_maxes) # [ 9 10 11 12] row_maxes = np.max(array1, axis=1) # 按行求最大值 print("\n按行求最大值 (np.max(array1, axis=1)):\n", row_maxes) # [ 4 8 12] # 4. 其他数学函数示例 (sin, exp) sin_array = np.sin(array1) # 元素级计算正弦值 print("\n正弦函数 (np.sin(array1)):\n", sin_array) exp_array = np.exp(array1) # 元素级计算指数值 print("\n指数函数 (np.exp(array1)):\n", exp_array)
-
-
案例应用: 图像亮度调整
我们回到文章开篇提到的 医学影像处理 场景,演示如何使用 NumPy 数组的算术运算和广播机制,进行 图像亮度调整。 假设我们有一张灰度图像,用 NumPy 数组表示,像素值范围为 0-255。 我们要 增加图像的亮度,让图像更明亮。
import numpy as np from PIL import Image # 1. 读取灰度图像 (使用 PIL 库读取图像,并转换为灰度模式 'L') image_path = "your_image.jpg" # 替换成你的图像文件路径 (建议使用灰度图像或转换为灰度图像) img = Image.open(image_path).convert('L') # 打开图像并转换为灰度模式 image_array = np.array(img) # 将 PIL 图像对象转换为 NumPy 数组 (二维数组,表示灰度图像) print("原始图像数组的形状:", image_array.shape) # 例如 (height, width) print("原始图像数组的数据类型:", image_array.dtype) # 例如 uint8 (无符号 8 位整数,像素值 0-255) # 2. 亮度调整 (增加亮度,例如每个像素值增加 50) brightness_factor = 50 # 亮度调整因子 brightened_image_array = image_array + brightness_factor # 数组与标量加法,广播机制 # 3. 像素值范围裁剪 (确保像素值在 0-255 范围内) # 亮度增加后,像素值可能会超出 255,需要裁剪到 255 (白色) brightened_image_array = np.clip(brightened_image_array, 0, 255) # np.clip(array, min_val, max_val) 将数组元素值限制在 [min_val, max_val] 范围内 # 4. 转换数据类型 (将数组数据类型转换为 uint8,符合图像像素值类型) brightened_image_array = brightened_image_array.astype(np.uint8) # 转换为 uint8 数据类型 # 5. 将 NumPy 数组转换回 PIL 图像对象 brightened_img = Image.fromarray(brightened_image_array) # 从 NumPy 数组创建 PIL 图像对象 # 6. 保存并显示亮度调整后的图像 output_path = "brightened_image.jpg" brightened_img.save(output_path) brightened_img.show() print(f"亮度调整后的图像已保存到: {output_path}")
代码解释:
- 读取灰度图像并转换为 NumPy 数组: 使用 PIL 库读取图像,并转换为灰度模式
'L'
,然后使用np.array()
将 PIL 图像对象转换为 NumPy 数组。 灰度图像的 NumPy 数组是 二维数组,每个元素表示一个像素的灰度值 (0-255)。 - 亮度调整:
brightened_image_array = image_array + brightness_factor
使用 数组与标量加法,将亮度调整因子brightness_factor
(标量) 广播到整个图像数组,实现图像亮度增加。 - 像素值范围裁剪:
np.clip(brightened_image_array, 0, 255)
使用np.clip()
函数,将亮度调整后的图像数组的像素值 限制在 0-255 范围内。 这是因为像素值必须在这个范围内才是有效的图像数据。 亮度增加后,像素值可能会超出 255 (变成白色),需要裁剪到 255。 - 转换数据类型:
brightened_image_array = brightened_image_array.astype(np.uint8)
将数组的数据类型转换为np.uint8
(无符号 8 位整数)。 这是因为图像像素值通常使用uint8
类型存储,取值范围 0-255。 - NumPy 数组转换回 PIL 图像对象:
brightened_img = Image.fromarray(brightened_image_array)
使用Image.fromarray()
将 NumPy 数组转换回 PIL 图像对象,方便后续保存和显示图像。 - 保存并显示图像: 使用 PIL 图像对象的
save()
和show()
方法保存和显示亮度调整后的图像。
这个案例展示了 NumPy 数组的算术运算和广播机制在图像处理中的简单应用。 通过简单的数组加法和像素值裁剪,就可以实现图像亮度的调整。 NumPy 的高效运算和简洁语法,使得图像处理变得更加方便快捷。
- 读取灰度图像并转换为 NumPy 数组: 使用 PIL 库读取图像,并转换为灰度模式
费曼回顾 (知识巩固):
现在,请你用自己的话,总结一下今天我们学习的 NumPy 数组算术运算和广播机制的知识,包括:
- NumPy 数组支持哪些基本的算术运算? 什么是元素级运算? 它有什么优势?
- 什么是广播 (broadcasting) 机制? 广播的规则是什么? 广播机制有什么作用和优势?
- 我们学习了哪些常用的 NumPy 数学函数?
axis
参数有什么作用? 你能举例说明np.sum(array, axis=0)
和np.sum(array, axis=1)
的区别吗? - 在图像亮度调整的案例中,我们是如何运用 NumPy 数组的算术运算和广播机制来处理图像数据的?
像给你的朋友讲解一样,用最简单的语言解释这些概念,并结合代码示例和实际应用,帮助他们理解 NumPy 数组的 “算术魔法” 和广播的威力。
课后思考 (拓展延伸):
- 尝试修改图像亮度调整案例的代码,例如:
- 调整
brightness_factor
的值,看看亮度调整效果的变化? - 实现 图像对比度调整 (提示:可以使用数组乘法和加法,调整像素值的范围)?
- 尝试处理 彩色图像 (提示:彩色图像通常是三维数组,RGB 三个通道,可以分别对每个通道进行亮度或对比度调整)?
- 调整
- 思考一下,除了图像处理,NumPy 数组的算术运算和广播机制还可以应用在哪些数据分析和科学计算场景中? 例如,数据归一化、信号处理、机器学习特征工程等等。 你有什么新的应用想法吗?
- 尝试查阅 NumPy 官方文档或其他 NumPy 教程,了解更多关于 NumPy 数学函数和广播机制的细节,例如:
- 更多的 NumPy 数学函数 (例如
np.floor()
,np.ceil()
,np.round()
,np.clip()
,np.dot()
矩阵乘法等) - 更深入的广播规则和示例,例如不同维度数组之间的广播
- ufuncs (universal functions) 的概念和性能优化
- 更多的 NumPy 数学函数 (例如
恭喜你!完成了 NumPy 费曼学习法的第三篇文章学习! 你已经掌握了 NumPy 数组的 “算术魔法” 和广播机制,可以开始进行更复杂的数据运算和处理了! 下一篇文章,我们将继续深入探索 NumPy 数组的 “逻辑世界”,学习如何使用条件运算和布尔索引,根据条件筛选和操作数组中的数据,让你的数据分析更加精准高效! 敬请期待!