对速度有洁癖?快来了解 Numpy 的 View 与 Copy

对速度有洁癖?快来了解 Numpy 的 View 与 Copy

作者: 莫烦 编辑: 莫烦 发布于: 2021-10-23

Numpy 之所以运算快,是有道理的,我们就在这节内容中尝试去理解 Numpy 底层的运行逻辑。其中,有一个非常重要的概念, 那就是 View 和 Copy,你会发现,有可能前几天要花 10 天处理完的数据,学完这个之后,一优化,只需要 1 小时就搞定了。

我在读博的时候,博一写的代码被博二的我嫌弃,其中的一个原因是不太清楚框架内部属性, 效率太慢了。研究 Numpy 的加速问题,也是因为我要常用 Numpy 做深度学习,强化学习的程序模拟, 一做就可能是 2 小时甚至是 1 天,而弄懂 Numpy 底层原理后,我的代码速度可以上升好几倍。

我之前在知乎上写过一篇文章关于 Numpy 加速的文章, 我认为其中有些环节很重要,也很有必要放在这个教程中。

如果你对 Numpy 运算速度有追求,我十分建议你了解接下来的内容。如果你是萌新, 目前阶段不用 Numpy 处理大数据(上百MB 的文件),那下面的内容你可以以后再作了解。

Numpy Array 和 Python List 内部结构差别

其实 Numpy 就是 C 的逻辑, 创建存储容器 Array 的时候是寻找内存上的一连串区域来存放, 而 Python 存放的时候则是不连续的区域, 这使得 Python 在索引这个容器里的数据时不是那么有效率。 Numpy 只需要再这块固定的连续区域前后走走就能不费吹灰之力拿到数据。下图是来自 Why Python is Slow: Looking Under the Hood, 他很好的解释了这一切.

4-1-2.png

用 Numpy 创建 Array 的时候,它使用到的是内存上的一段连续空间,而 Python List 是物理内存上的不同区域,只是它用索引将这些区域联系起来了。 正是这样的架构,使得 Numpy Array 在物理空间上可以高效排列。这就为后面理解 View 和 Copy 奠定了基础。

View 与 Copy

view

Copy 顾名思义, 会将 Array 中的数据 copy 出来存放在内存中另一个地方, 而 View 不 copy 数据, 而是给源数据加一个窗,从外面看窗户里的数据(下图)。 具体来说,view 不会新建数据,而只是在源数据上建立索引部分。 下图来自 Understanding SettingwithCopyWarning in pandas

4-1-5.png

上面说的是什么意思呢? 我们直接看代码.

简单说, a_view 的东西全部都是 a 的东西, 动 a_view 的任何地方, a 都会受到牵扯, 因为他们在内存中的位置是一模一样的, 本质上就是自己。 而 a_copy 则是将 a copy 了一份, 然后把 a_copy 放在内存中的另外的地方, 这样改变 a_copy, a 是不会被改变的.

那为什么要提这点呢? 因为 View 只是加了,不会复制东西, 速度快! 我们来测试一下速度。 我会用下面这个功能来测试一下每个功能的运行时间,数字越小的,运行时间越快。

下面的例子中 a*=2 就是将这个 view 给赋值了, 和 a[:] *= 2 一个意思, 从头到尾没有创建新的东西。而 b = 2*b 中, 我们将 b 赋值给另外一个新建的 b

从运行结果来看,运行结果还是快了 10%~20% 的。

对于 view 还有一点要提, 你是不是偶尔有时候要把一个矩阵展平, 用到 np.flatten() 或者 np.ravel()。 我们在改变数据结构 中也提到了这两个功能。 他俩虽然结果上相同,但是含义是完全不同的! 官方说如果用 ravel(), 需要 copy 的时候才会被 copy , 我想这个时候可能是把 ravel 里面 order 转换的时候, 如 'C-type' -> 'Fortran', 而 flatten() 返回的总是一个 copy。现在你知道谁在拖你的后腿了吧!

运行了上面的测试后, 相比于 flatten(), ravel() 简直是神速,提高了近 700 倍。

在选择数据上加速

选择数据的时候, 我们常会用到 view 或者 copy 的形式。现在你应该知道了, 如果能用到 view 的, 我们就尽量用 view, 避免 copy 数据。那在选择数据的时候,那些是选择了一个 view 呢? 下面举例的都是 view 的方式:

那哪些操作我们又会变成 copy 呢?

Numpy 给了我们很多很自由的方式选择数据, 这些虽然都很方便, 但是在你没必要 copy 的时候, 如果你可以避免引起 Copy 的这些操作, 你的速度可以飞起来.

在上面提到的 blog 里面, 他提到了, 如果你还是喜欢这种 fancy indexing 的形式, 我们也是可以对它加点速的。那个 blog 中指出了两种方法。

1. 使用 np.take(), 替代用 index 选数据的方法。

上面提到了如果用 index 来选数据, 像 a_copy1 = a[[1,4,6], [2,4,6]], 用 take 在大部分情况中会比这样的 a_copy1 要快。

上面的数据,用 take() 有个十几倍的加速还是挺不错的。

2. 使用 np.compress(), 替代用 mask 选数据的方法。

上面的 a_copy2 = a[[True, True], [False, True]] 这种就是用 True, False 来选择数据的。我们对比一下它和 compress() 的效率, 测试如下:

compress 方法在这种情况下,也有一个 9 倍左右的提升。已经相当不错了。

非常有用的 out 参数

不深入了解 numpy 的朋友, 应该会直接忽略很多功能中的这个 out 参数 (之前我从来没用过)。 不过当我深入了解了以后, 发现他非常有用! 比如下面两个其实在功能上是没差的, 不过运算时间上有差, 我觉得可能是 a=a+1 要先转换成 np.add() 这种形式再运算, 所以前者要用更久一点的时间.

如果是上面那样, 我们就会触发之前提到的 copy 原则, 这两个被赋值的 a, 都是原来 a 的一个 copy, 并不是 a 的 view。但是在功能里面有一个 out 参数, 让我们不必要重新创建一个 a

我们看看使用 out 之前,它们的处理方式算不算 copy。

我上面的例子,你应该可以很明显的看出来,如果我们新建了一个 c,用 out=c 来指定将加法结果输出到 c, 那么我们就可以利用这种特性来减少 copy 的产生。比如下面这个例子。

从上面的例子可以看出来,如果要完完全全对比将计算结果赋值回源数据的案例,我们明显发现,使用 out 要比不使用 out 快一倍。 但是有意思的是,如果我不赋值回源数据,而是创建一个新数据 _c,这种方法也只是比使用 out 的方法慢了一点点,但是比 a[:] 这种方法快上不少。 我想可能是 numpy 对于创造新数据的做法做了深度优化吧。

Numpy 的官方文档也对能用 out 的功能做了列举: Universal functions

总结

在这里说了好几种 Numpy 加速的办法,万变不离其中,只要你明白了 view 和 copy 的概念,对于你自己的代码,优化空间总是有的。


降低知识传递的门槛

莫烦经常从互联网上学习知识,开源分享的人是我学习的榜样。 他们的行为也改变了我对教育的态度: 降低知识传递的门槛免费 奉献我的所学正是受这种态度的影响。 【支持莫烦】 能让我感到认同,我也更有理由坚持下去。

我组建了微信群,欢迎大家加入,交流经验,提出问题,互相帮持。 扫码后,请一定备注"莫烦",否则我不会同意你的入群申请。

wechat