老码农和你一起学AI:Python系列- NumPy 核心操作详解

发布于:2025-07-13 ⋅ 阅读:(15) ⋅ 点赞:(0)

我一直觉得,NumPy 作为 Python 科学计算的基石,其高效的数组操作能力是数据处理、机器学习等领域的核心动力。掌握 NumPy 的核心操作,能让我们在处理大规模数据时事半功倍。所以今天一起梳理 NumPy 的四大核心操作,用通俗易懂的方式去理解和运用。

一、索引与切片

数组的索引与切片是获取和修改数组元素的基础操作,NumPy 在这方面提供了丰富且灵活的功能。

1、基础索引

基础索引包括整数索引和切片索引,操作方式与 Python 列表类似,但能支持多维度。对于一维数组,整数索引就是通过元素的位置获取元素,如a[2]表示获取数组a中索引为 2 的元素;切片索引则是通过start:stop:step的形式获取一段连续的元素,例如a[1:5:2]表示从索引 1 开始,到索引 5 结束(不包含 5),步长为 2 的元素。


import numpy as np

# 一维数组基础索引示例

a = np.array([10, 20, 30, 40, 50])

print("整数索引结果:", a[2]) # 输出:30

print("切片索引结果:", a[1:5:2]) # 输出:[20 40]

# 二维数组基础索引示例

b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

print("获取单个元素:", b[1, 2]) # 输出:6

print("切片获取子数组:\n", b[0:2, 1:3])

# 输出:

# [[2 3]

# [5 6]]

2、高级索引

高级索引能让我们更灵活地获取数组元素,主要包括布尔索引和整数数组索引。

布尔索引是非常常用的一种高级索引方式,常用于数据清洗和条件筛选。它通过一个布尔数组来确定获取哪些元素,布尔数组的形状需要与被索引数组的形状相匹配。例如,有一个数组a = np.array([1, 3, 5, 7, 9]),我们想获取其中大于 5 的元素,就可以先创建布尔数组a > 5,得到[False, False, False, True, True],然后用这个布尔数组进行索引a[a > 5],结果为[7,9]。在二维数组中,布尔索引同样适用,比如a[a > 3]会获取到数组a中所有大于 3 的元素组成的一维数组。


# 布尔索引示例

c = np.array([1, 3, 5, 7, 9])

bool_mask = c > 5

print("布尔数组:", bool_mask) # 输出:[False False False True True]

print("布尔索引结果:", c[bool_mask]) # 输出:[7 9]

# 二维数组布尔索引示例

d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

print("二维数组布尔索引结果:", d[d > 3]) # 输出:[4 5 6 7 8 9]

整数数组索引是通过整数数组来指定要获取的元素位置。对于一维数组,a[[0,2,4]]会获取数组a中索引为 0、2、4 的元素。在二维数组中,a[[0,2],[1,2]]表示获取(0,1)和(2,2)位置的元素。


# 整数数组索引示例

e = np.array([10, 20, 30, 40, 50])

print("一维数组整数数组索引结果:", e[[0, 2, 4]]) # 输出:[10 30 50]

f = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

print("二维数组整数数组索引结果:", f[[0, 2], [1, 2]]) # 输出:[2 9]

二、不同维度的切片操作

对于更高维度的数组,切片操作可以在每个维度上独立进行。例如一个三维数组a = np.arange(24).reshape(2,3,4),a[0, 1:3, 2:4]表示获取第一个维度索引为 0、第二个维度索引 1 到 2(不包含 3)、第三个维度索引 2 到 3(不包含 4)的元素。


# 三维数组切片示例

g = np.arange(24).reshape(2, 3, 4)

print("三维数组形状:", g.shape) # 输出:(2, 3, 4)

print("三维数组切片结果:\n", g[0, 1:3, 2:4])

# 输出:

# [[ 6 7]

# [10 11]]

1、获取单个元素与标量

获取单个元素可以通过多维度的整数索引,如a[0,0]表示获取二维数组a中第一行第一列的元素,得到的结果是一个标量。标量可以直接参与算术运算,这在很多数据处理场景中非常方便。


# 获取单个元素与标量操作示例

h = np.array([[1, 2], [3, 4]])

single_element = h[0, 0]

print("获取的单个元素:", single_element) # 输出:1

print("标量参与运算:", single_element + 5) # 输出:6

2、视图 vs 副本

这是一个极易混淆但又非常重要的概念,理解它能帮助我们避免在操作数组时出现意外。

视图是数组的一个引用,它与原始数组共享同一块数据存储空间。当我们对视图进行修改时,原始数组也会随之改变。切片操作返回的就是视图,例如b = a[1:3],修改b中的元素,a也会发生变化。

副本则是原始数组的一个完整拷贝,它拥有独立的数据存储空间。对副本进行修改不会影响原始数组,我们可以通过copy()方法来创建副本,如b = a[1:3].copy(),此时修改b,a不会受到影响。


# 视图与副本示例

i = np.array([10, 20, 30, 40, 50])

view_i = i[1:3] # 视图

copy_i = i[1:3].copy() # 副本

view_i[0] = 200

copy_i[1] = 300

print("修改视图后原始数组:", i) # 输出:[ 10 200 30 40 50]

print("修改副本后原始数组:", i) # 输出:[ 10 200 30 40 50]

print("副本数组:", copy_i) # 输出:[200 300]

在广播、赋值、切片等操作中,一定要注意区分是视图还是副本,否则可能会导致数据被意外修改或者占用过多内存。

三、广播机制

广播机制是 NumPy 中让不同形状的数组进行算术运算的规则,在深度学习中有着至关重要的作用,比如标量与张量、不同大小张量间的逐元素操作都依赖于广播机制。

1、核心概念

广播机制的核心是在进行算术运算时,NumPy 会自动调整数组的形状,使得不同形状的数组能够兼容并进行逐元素操作。这避免了我们手动扩展数组形状,大大简化了代码。

2、规则详解

  • 维度对齐:当两个数组的维度数量不同时,会将维度较少的数组的形状在前面补 1,直到两个数组的维度数量相同。例如,一个形状为(3,4)的数组和一个形状为(4,)的数组,会将后者的形状调整为(1,4)。
  • 扩展虚拟维度:对于调整后形状中对应维度大小为 1 的数组,会沿着该维度进行扩展,使其大小与另一个数组对应维度的大小相同。这里的扩展只是逻辑上的,并不会真正复制数据,所以不会增加内存占用。
  • 沿长度为 1 的维度复制数据:在逻辑上完成扩展后,就可以进行逐元素的算术运算了。

3、实际例子

  • 矩阵 + 行向量:设有一个形状为(3,4)的矩阵a和一个形状为(4,)的行向量b。根据广播规则,b的形状会被调整为(1,4),然后沿着第一个维度扩展为(3,4),最后与a进行加法运算,结果是矩阵a的每一行都加上行向量b。
# 矩阵 + 行向量广播示例

matrix = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])

row_vector = np.array([10, 20, 30, 40])

result1 = matrix + row_vector

print("矩阵加行向量结果:\n", result1)

# 输出:

# [[11 22 33 44]

# [15 26 37 48]

# [19 30 41 52]]
  • 矩阵 + 列向量:一个形状为(3,4)的矩阵a和一个形状为(3,1)的列向量b。b会沿着第二个维度扩展为(3,4),然后与a相加,即矩阵a的每一列都加上列向量b。

# 矩阵 + 列向量广播示例

col_vector = np.array([[100], [200], [300]])

result2 = matrix + col_vector

print("矩阵加列向量结果:\n", result2)

# 输出:

# [[101 102 103 104]

# [205 206 207 208]

# [309 310 311 312]]
  • 矩阵 * 标量:一个任意形状的矩阵与一个标量进行乘法运算时,标量会被广播成与矩阵形状相同的数组,然后进行逐元素乘法,相当于矩阵中的每个元素都乘以该标量。

# 矩阵 * 标量广播示例

scalar = 2

result3 = matrix * scalar

print("矩阵乘标量结果:\n", result3)

# 输出:

# [[ 2 4 6 8]

# [10 12 14 16]

# [18 20 22 24]]

4、广播的适用与不适用情况

当两个数组的形状在每个维度上要么相等,要么其中一个为 1 时,广播可以进行。否则,广播会失败。例如,形状为(2,3)的数组和形状为(2,4)的数组,在第二个维度上 3 和 4 既不相等也不为 1,所以无法进行广播。


# 广播失败示例

try:

arr1 = np.array([[1, 2, 3], [4, 5, 6]])

arr2 = np.array([[7, 8, 9, 10], [11, 12, 13, 14]])

result = arr1 + arr2

except ValueError as e:

print("广播失败原因:", e) # 输出:operands could not be broadcast together with shapes (2,3) (2,4)

四、向量化操作

向量化操作是 NumPy 高性能的关键所在,它摆脱了 Python 中显式循环的低效率,通过底层 C/Fortran 优化的内置函数对整个数组进行操作。

1、核心理念

向量化操作的核心理念是避免使用显式的 for 循环,而是直接对整个数组应用操作。这样可以充分利用底层优化的优势,大大提高运算效率,尤其是在处理大规模数组时,性能提升更为明显。

2、常用操作

  • 算术运算:包括+、-、*、/、**等运算符,以及np.exp()、np.log()、np.sin()、np.abs()等函数。这些操作都是对数组中的每个元素进行逐元素运算,例如a + b会将数组a和b中对应位置的元素相加,np.exp(a)会对数组a中的每个元素求指数。

# 算术运算示例

x = np.array([1, 2, 3, 4])

y = np.array([5, 6, 7, 8])

print("加法:", x + y) # 输出:[ 6 8 10 12]

print("减法:", x - y) # 输出:[-4 -4 -4 -4]

print("乘法:", x * y) # 输出:[ 5 12 21 32]

print("除法:", x / y) # 输出:[0.2 0.33333333 0.42857143 0.5 ]

print("指数运算:", x** 2) # 输出:[ 1 4 9 16]

print("指数函数:", np.exp(x)) # 输出:[ 2.71828183 7.3890561 20.08553692 54.59815003]

print("对数函数:", np.log(x)) # 输出:[0. 0.69314718 1.09861229 1.38629436]
  • 比较运算:==、!=、<、>、<=、>=等比较运算符,返回的是一个布尔数组,其中每个元素表示对应位置的比较结果。如a > b会得到一个布尔数组,True表示a中对应元素大于b中对应元素,False则相反。

# 比较运算示例

print("等于:", x == y) # 输出:[False False False False]

print("不等于:", x != y) # 输出:[ True True True True]

print("大于:", x > y) # 输出:[False False False False]

print("小于等于:", x <= y) # 输出:[ True True True True]
  • 聚合函数:用于对数组进行整体的统计计算,常用的有np.sum()(求和)、np.mean()(求平均值)、np.std()(求标准差)、np.var()(求方差)、np.min()(求最小值)、np.max()(求最大值)、np.prod()(求乘积)、np.any()(判断是否有任意一个元素为真)、np.all()(判断是否所有元素都为真)等。

其中,axis参数是聚合函数的重点,它用于指定沿哪个维度进行聚合操作。例如,对于一个二维数组a,np.mean(a, axis=0)表示计算每列的平均值,np.max(a, axis=1)表示计算每行的最大值。当axis=0时,是沿着第一个维度(行)进行聚合,得到的结果是每个列的统计值;当axis=1时,是沿着第二个维度(列)进行聚合,得到的结果是每个行的统计值。


# 聚合函数示例

z = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

print("总和:", np.sum(z)) # 输出:45

print("平均值:", np.mean(z)) # 输出:5.0

print("每列平均值:", np.mean(z, axis=0)) # 输出:[4. 5. 6.]

print("每行最大值:", np.max(z, axis=1)) # 输出:[3 6 9]

print("所有元素是否都大于0:", np.all(z > 0)) # 输出:True

print("是否有元素大于5:", np.any(z > 5)) # 输出:True

形状操作

在数据处理过程中,我们经常需要改变数组的形状以适应不同的计算需求,NumPy 提供了一系列形状操作函数。

  • reshape():用于改变数组的形状,但不改变数组中的数据。它要求新形状的元素总数与原数组相同,否则会报错。例如a = np.arange(12),a.reshape(3,4)会将数组a的形状改为(3,4)。

# reshape示例

m = np.arange(12)

print("原始数组:", m) # 输出:[ 0 1 2 3 4 5 6 7 8 9 10 11]

m_reshaped = m.reshape(3, 4)

print("reshape后的数组:\n", m_reshaped)

# 输出:

# [[ 0 1 2 3]

# [ 4 5 6 7]

# [ 8 9 10 11]]
  • resize():不仅可以改变数组的形状,还可以改变数组的大小。如果新形状的元素总数大于原数组,会填充新的元素(通常是 0);如果小于原数组,会截断原数组。例如a.resize(2,3),如果原数组元素不足 6 个,会用 0 填充。

# resize示例

n = np.array([1, 2, 3, 4])

n.resize(2, 3)

print("resize后的数组:\n", n)

# 输出:

# [[1 2 3]

# [4 0 0]]
  • ravel() / flatten():两者都用于将多维数组展平为一维数组。ravel()返回的是视图,与原数组共享数据;flatten()返回的是副本,与原数组独立。例如对一个二维数组a,a.ravel()和a.flatten()都会得到一个一维数组,但修改ravel()的结果可能会影响原数组,而修改flatten()的结果则不会。

# ravel与flatten示例

p = np.array([[1, 2], [3, 4]])

ravel_p = p.ravel()

flatten_p = p.flatten()

ravel_p[0] = 100

flatten_p[1] = 200

print("修改ravel结果后原数组:\n", p)

# 输出:

# [[100 2]

# [ 3 4]]

print("flatten结果:", flatten_p) # 输出:[100 200 3 4]
  • transpose() / .T:用于转置数组,交换数组的维度。对于二维数组,transpose()和.T的效果相同,都是将行和列交换。例如一个形状为(3,4)的数组,转置后形状变为(4,3)。

# 转置示例

q = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])

print("原数组形状:", q.shape) # 输出:(4, 3)

q_transposed = q.T

print("转置后数组形状:", q_transposed.shape) # 输出:(3, 4)

print("转置后数组:\n", q_transposed)

# 输出:

# [[ 1 4 7 10]

# [ 2 5 8 11]

# [ 3 6 9 12]]
  • np.newaxis / None:用于增加数组的新维度,这在广播操作中非常有用。例如a = np.array([1,2,3]),a[:, np.newaxis]会将其形状从(3,)变为(3,1),变成一个列向量。

# np.newaxis示例

r = np.array([1, 2, 3])

print("原始形状:", r.shape) # 输出:(3,)

r_col = r[:, np.newaxis]

print("增加列维度后形状:", r_col.shape) # 输出:(3, 1)

print("列向量:\n", r_col)

# 输出:

# [[1]

# [2]

# [3]]

r_row = r[np.newaxis, :]

print("增加行维度后形状:", r_row.shape) # 输出:(1, 3)
  • np.squeeze():用于移除数组中长度为 1 的维度。例如一个形状为(1,3,1,4)的数组,np.squeeze()后会变成形状为(3,4)的数组。

# squeeze示例

s = np.array([[[[1, 2, 3, 4]], [[5, 6, 7, 8]], [[9, 10, 11, 12]]]])

print("原始形状:", s.shape) # 输出:(1, 3, 1, 4)

s_squeezed = np.squeeze(s)

print("squeeze后形状:", s_squeezed.shape) # 输出:(3, 4)

print("squeeze后数组:\n", s_squeezed)

# 输出:

# [[ 1 2 3 4]

# [ 5 6 7 8]

# [ 9 10 11 12]]

最后小结:

NumPy 的核心操作是高效处理数组数据的基础,无论是数据清洗、科学计算还是深度学习,这些操作都扮演着关键角色。​

索引与切片是数组操作的基础,需区分基础索引(整数、切片)和高级索引(布尔索引、整数数组索引)。布尔索引在条件筛选中不可或缺,而 “视图 vs 副本” 的概念直接影响数据安全性 —— 切片返回视图(共享数据),copy()生成副本(独立数据),操作时需格外注意。​

广播机制是不同形状数组运算的 “桥梁”,其核心是通过维度对齐、扩展虚拟维度实现兼容运算,这在深度学习中(如标量与张量运算)至关重要。记住广播的黄金规则:维度要么相等,要么其中一个为 1,否则会失败。​

向量化操作是 NumPy 高性能的灵魂,通过内置函数替代显式循环,借助底层优化实现效率飞跃。算术运算、比较运算和聚合函数(尤其注意axis参数控制维度)是日常使用的高频工具,能显著提升大规模数据处理速度。​

形状操作用于灵活调整数组结构,reshape()(不改变数据)、transpose()(转置维度)、np.newaxis(增加维度)等函数在数据预处理和模型输入适配中频繁使用,而ravel()与flatten()的 “视图 vs 副本” 差异也需重点关注。

初学着有些难度,掌握这些操作,能让你在 NumPy 的使用中既高效又安全,为后续的数据分析和建模工作打下坚实基础,未完待续....


网站公告

今日签到

点亮在社区的每一天
去签到