NumPy 超详细教程(3):ndarray 的内部机理及高级迭代

本文深入解析NumPy中ndarray的内部结构,包括数据指针、数据类型、维度和跨度等概念,并介绍了nditer多维迭代器的使用方法,如order参数、op_flags参数等。

系列文章地址

NumPy 最详细教程(1):NumPy 数组
NumPy 超详细教程(2):数据类型
NumPy 超详细教程(3):ndarray 的内部机理及高级迭代


1、ndarray 对象的内部机理

在前面的内容中,我们已经详细讲述了 ndarray 的使用,在本章的开始部分,我们来聊一聊 ndarray 的内部机理,以便更好的理解后续的内容。

1.1、ndarray 的组成

ndarray 与数组不同,它不仅仅包含数据信息,还包括其他描述信息。ndarray 内部由以下内容组成:

  • 数据指针:一个指向实际数据的指针。
  • 数据类型(dtype):描述了每个元素所占字节数。
  • 维度(shape):一个表示数组形状的元组。
  • 跨度(strides):一个表示从当前维度前进道下一维度的当前位置所需要“跨过”的字节数。

NumPy 中,数据存储在一个均匀连续的内存块中,可以这么理解,NumPy 将多维数组在内部以一维数组的方式存储,我们只要知道了每个元素所占的字节数(dtype)以及每个维度中元素的个数(shape),就可以快速定位到任意维度的任意一个元素。

dtypeshape 前文中已经有详细描述,这里我们来讲下 strides

示例

ls = [[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], 
      [[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]]
a = np.array(ls, dtype=int)
print(a)
print(a.strides)

输出:

[[[ 1  2  3  4]
  [ 5  6  7  8]
  [ 9 10 11 12]]

 [[13 14 15 16]
  [17 18 19 20]
  [21 22 23 24]]]
(48, 16, 4)

上例中,我们定义了一个三维数组,dtypeintint 占 4个字节。
第一维度,从元素 1 到元素 13,间隔 12 个元素,总字节数为 48;
第二维度,从元素 1 到元素 5,间隔 4 个元素,总字节数为 16;
第三维度,从元素 1 到元素 2,间隔 1 个元素,总字节数为 4。
所以跨度为(48, 16, 4)。

2、普通迭代

ndarray 的普通迭代跟 Python 及其他语言中的迭代方式无异,N 维数组,就要用 N 层的 for 循环。

示例:

import numpy as np

ls = [[1, 2], [3, 4], [5, 6]]
a = np.array(ls, dtype=int)
for row in a:
    for cell in row:
        print(cell)

输出:

1
2
3
4
5
6

上例中,row 的数据类型依然是 numpy.ndarray,而 cell 的数据类型是 numpy.int32

3、nditer 多维迭代器

NumPy 提供了一个高效的多维迭代器对象:nditer 用于迭代数组。在普通方式的迭代中,N 维数组,就要用 N 层的 for 循环。但是使用 nditer 迭代器,一个 for 循环就能遍历整个数组。(因为 ndarray 在内存中是连续的,连续内存不就相当于是一维数组吗?遍历一维数组当然只需要一个 for 循环就行了。)

3.1、基本示例

例一:

ls = [[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
      [[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]]
a = np.array(ls, dtype=int)
for x in np.nditer(a):
    print(x, end=", ")

输出:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 

3.2、order 参数:指定访问元素的顺序

创建 ndarray 数组时,可以通过 order 参数指定元素的顺序,按行还是按列,这是什么意思呢?来看下面的示例:

例二:

ls = [[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
      [[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]]
a = np.array(ls, dtype=int, order='F')
for x in np.nditer(a):
    print(x, end=", ")

输出:

1, 13, 5, 17, 9, 21, 2, 14, 6, 18, 10, 22, 3, 15, 7, 19, 11, 23, 4, 16, 8, 20, 12, 24, 

nditer 默认以内存中元素的顺序(order=‘K’)访问元素,对比例一可见,创建 ndarray 时,指定不同的顺序将影响元素在内存中的位置。

例三:
nditer 也可以指定使用某种顺序遍历。

ls = [[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
      [[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]]
a = np.array(ls, dtype=int, order='F')
for x in np.nditer(a, order='C'):
    print(x, end=", ")

输出:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 

行主顺序(order='C')和列主顺序(order='F'),参看 https://en.wikipedia.org/wiki/Row-_and_column-major_order。例一是行主顺序,例二是列主顺序,如果将 ndarray 数组想象成一棵树,那么会发现,行主顺序就是深度优先,而列主顺序就是广度优先。NumPy 中之所以要分行主顺序和列主顺序,主要是为了在矩阵运算中提高性能,顺序访问比非顺序访问快几个数量级。(矩阵运算将会在后面的章节中讲到)

3.3、op_flags 参数:迭代时修改元素的值

默认情况下,nditer 将视待迭代遍历的数组为只读对象(readonly),为了在遍历数组的同时,实现对数组元素值得修改,必须指定 op_flags 参数为 readwrite 或者 writeonly 的模式。

例四:

import numpy as np

a = np.arange(5)
for x in np.nditer(a, op_flags=['readwrite']):
    x[...] = 2 * x
print(a)

输出:

[0 1 2 3 4]

3.4、flags 参数

flags 参数需要传入一个数组或元组,既然参数类型是数组,我原本以为可以传入多个值的,但是,就下面介绍的 4 种常用选项,我试了,不能传多个,例如 flags=['f_index', 'external_loop'],运行报错

3.4.1、使用外部循环:external_loop

将一维的最内层的循环转移到外部循环迭代器,使得 NumPy 的矢量化操作在处理更大规模数据时变得更有效率。

简单来说,当指定 flags=['external_loop'] 时,将返回一维数组而并非单个元素。具体来说,当 ndarray 的顺序和遍历的顺序一致时,将所有元素组成一个一维数组返回;当 ndarray 的顺序和遍历的顺序不一致时,返回每次遍历的一维数组(这句话特别不好描述,看例子就清楚了)。

例五:

import numpy as np

ls = [[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
      [[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]]
a = np.array(ls, dtype=int, order='C')
for x in np.nditer(a, flags=['external_loop'], order='C'):
    print(x,)

输出:

[ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]

例六:

b = np.array(ls, dtype=int, order='F')
for x in np.nditer(b, flags=['external_loop'], order='C'):
    print(x,)

输出:

[1 2 3 4]
[5 6 7 8]
[ 9 10 11 12]
[13 14 15 16]
[17 18 19 20]
[21 22 23 24]
3.4.2、追踪索引:c_index、f_index、multi_index

例七:

import numpy as np

a = np.arange(6).reshape(2, 3)
it = np.nditer(a, flags=['f_index'])

while not it.finished:
    print("%d <%d>" % (it[0], it.index))
    it.iternext()

输出:

0 <0>
1 <2>
2 <4>
3 <1>
4 <3>
5 <5>

这里索引之所以是这样的顺序,因为我们选择的是列索引(f_index)。直观的感受看下图:

遍历元素的顺序是由 order 参数决定的,而行索引(c_index)和列索引(f_index)不论如何指定,并不会影响元素返回的顺序。它们仅表示在当前内存顺序下,如果按行/列顺序返回,各个元素的下标应该是多少。

例八:

import numpy as np

a = np.arange(6).reshape(2, 3)
it = np.nditer(a, flags=['multi_index'])

while not it.finished:
    print("%d <%s>" % (it[0], it.multi_index))
    it.iternext()

输出:

0 <(0, 0)>
1 <(0, 1)>
2 <(0, 2)>
3 <(1, 0)>
4 <(1, 1)>
5 <(1, 2)>

3.5、同时迭代多个数组

说到同时遍历多个数组,第一反应会想到 zip 函数,而在 nditer 中不需要。

例九:

a = np.array([1, 2, 3], dtype=int, order='C')
b = np.array([11, 12, 13], dtype=int, order='C')
for x, y in np.nditer([a, b]):
    print(x, y)

输出:

1 11
2 12
3 13

4、其他函数

4.1、flatten函数

flatten 函数将多维 ndarray 展开成一维 ndarray 返回。
语法:

flatten(order='C')

示例:

import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6]], dtype=int, order='C')
b = a.flatten()
print(b)
print(type(b))

输出:

[1 2 3 4 5 6]
<class 'numpy.ndarray'>

4.2、flat

flat 返回一个迭代器,可以遍历数组中的每一个元素。

import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6]], dtype=int, order='C')
for b in a.flat:
    print(b)
print(type(a.flat))

输出:

1
2
3
4
5
6
<class 'numpy.flatiter'>

### 有效集算法原理及实现 #### 一、有效集算法概述 有效集算法(Active Set Algorithm)是一种优化方法,主要用于解决带约束的最优化问题。它通过逐步识别并处理活动约束集合来逼近全局最优解。这种方法特别适用于线性规划、二次规划等问题,在机器学习领域也有广泛应用。 #### 二、基本概念 在约束优化问题中,“活动约束”是指那些恰好满足等式的不等式约束条件。这些约束定义了一个子空间,目标函数在这个子空间内被最小化或最大化。有效集算法的核心在于动态维护一组当前被认为是“活动”的约束,并不断调整这个集合直到找到最优解[^4]。 #### 三、算法流程 以下是有效集算法的一般步骤: 1. **初始化** 初始假设没有任何约束处于活动状态,或者根据先验知识设定一个初始的有效集 \( S_0 \)。 2. **求解无约束子问题** 在给定的有效集中,忽略其他非活动约束的影响,仅考虑当前活动约束所形成的边界条件下的优化问题。这通常可以通过拉格朗日乘数法或其他数值技术完成。 3. **检查KKT条件** 验证当前解是否满足Karush-Kuhn-Tucker (KKT) 条件。如果满足,则已得到局部极小值;如果不满足,则继续下一步操作。 4. **更新有效集** 如果发现某些原本被认为是非活动的约束实际上已经变为活动状态,则将其加入到现有有效集中;反之亦然,对于不再活跃的约束应予以删除。 5. **迭代终止判断** 当连续两次计算所得的新旧解差异小于预设阈值时停止迭代过程,输出最终结果作为近似解。 #### 四、Python 实现示例 下面给出一个简单的 Python 实现框架用于演示如何利用 `scipy.optimize` 库中的功能实现有效的集算法解决问题实例——标准形式的凸二次规划问题: ```python from scipy import optimize as opt import numpy as np def active_set_qp(H, f, Aeq=None, beq=None, Ain=None, bin_=None): """ 使用有效集策略解决 QP 问题 min(0.5*x.T@H@x + f.T@x),s.t Ax<=b 和 Aeq x = beq. 参数: H : ndarray(n,n), 半正定矩阵表示目标函数中的二次项系数; f : ndarray(n,), 表示目标函数中的一次项系数向量; Aeq: ndarray(m_eq,n), 等式约束左侧系数矩阵,默认为空列表[]; beq: ndarray(m_eq,), 等式右侧常数列向量,默认为空数组np.array([]); Ain: ndarray(m_in,n), 不等式约束左侧系数矩阵,默认为空列表[]; bin_: ndarray(m_in,), 不等式右侧常数列向量,默认为空数组np.array([]). 返回: soln_x: 最优解对应的变量取值; obj_val: 对应的目标函数值。 """ n_vars = len(f) # 定义辅助函数帮助构建QP问题结构体供后续调用minimize接口传参使用 def qp_obj(x): return .5 * x @ H @ x + f @ x cons_list = [] if isinstance(Ain,np.ndarray) and isinstance(bin_,np.ndarray): for i in range(len(bin_)): cons_list.append({&#39;type&#39;:&#39;ineq&#39;, &#39;fun&#39;: lambda x,i=i:Ain[i,:]@x-bin_[i]}) if isinstance(Aeq,np.ndarray)and isinstance(beq,np.ndarray): cons_list.extend([{&#39;type&#39;:&#39;eq&#39;,&#39;fun&#39;:lambda x,j=j:(Aeq[j,:])@(x)-beq[j]}for j in range(len(beq))]) res = opt.minimize(qp_obj,[0]*n_vars,options={&#39;disp&#39;:False},constraints=cons_list) return res.x,res.fun if __name__ == "__main__": # 测试数据设置 H_test=np.array([[2,-1],[-1,2]]) f_test=np.array([-3,-4]) Ain_test=np.array([[1,1],[1,0],[0,1]]) b_test=np.array([7,5,6]) solution,obj_value=active_set_qp(H_test,f_test,Ain=Ain_test,bin_=b_test) print("Optimal Solution:",solution,"Objective Value:",obj_value) ``` 此代码片段展示了如何针对特定类型的二次规划问题设计解决方案。注意实际应用过程中还需要进一步完善错误检测机制以及性能改进措施等内容[^5]。 #### 五、总结 综上所述,有效集算法凭借其清晰直观的工作机理成为众多复杂优化场景下不可或缺的重要工具之一。尽管如此,由于涉及频繁切换候选集合的操作特性决定了该方法可能存在收敛速度较慢的问题,因此在大规模高维情形下需谨慎选用替代方案如内点法等更为高效的现代算法[^6]。
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值