2.2 高级Numpy
作者: Pauli Virtanen
Numpy是Python科学工具栈的基础。它的目的很简单:在一个内存块上实现针对多个物品的高效操作。了解它的工作细节有助于有效的使用它的灵活性,使用有用的快捷键,基于它构建新的工作。
这个指南的目的包括:
- 剖析Numpy数组,以及它的重要性。提示与技巧。
- 通用函数:什么是、为什么以及如果你需要一个全新的该做什么。
- 与其他工具整合:Numpy提供了一些方式将任意数据封装为ndarray,而不需要不必要的复制。
- 新近增加的功能,对我来说他们包含什么:PEP 3118 buffers、广义ufuncs, ...
先决条件
- Numpy (>= 1.2; 越新越好...)
- Cython (>= 0.12, 对于Ufunc例子)
- PIL (在一些例子中使用)
在这个部分,numpy将被如下引入:
In [2]:
import numpy as np
章节内容
- ndarry的一生
- 它是...
- 内存块
- 数据类型
- 索引体系:strides
- 剖析中的发现
- 通用函数
- 他们是什么?
- 练习:从零开始构建一个ufunc
- 答案:从零开始构建一个ufunc
- 广义ufuncs
- 协同工作功能
- 共享多维度,类型数据
- 旧的buffer协议
- 旧的buffer协议
- 数组接口协议
- 数组切片:
chararray
、maskedarray
、matrix
chararray
:向量化字符操作masked_array
缺失值- recarray:纯便利
matrix
:便利?
- 总结
- 为Numpy/Scipy做贡献
- 为什么
- 报告bugs
- 贡献文档
- 贡献功能
- 如何帮忙,总的来说
2.2.1 ndarray的一生
2.2.1.1 它是...
ndarray =
内存块 + 索引体系 + 数据类型描述符
- 原始数据
- 如何定义一个元素
- 如何解释一个元素
In [ ]:
typedef struct PyArrayObject {
PyObject_HEAD
/* Block of memory */
char *data;
/* Data type descriptor */
PyArray_Descr *descr;
/* Indexing scheme */
int nd;
npy_intp *dimensions;
npy_intp *strides;
/* Other stuff */
PyObject *base;
int flags;
PyObject *weakreflist;
} PyArrayObject;
2.2.1.2 内存块
In [5]:
x = np.array([1, 2, 3, 4], dtype=np.int32)
x.data
Out[5]:
<read-write buffer for 0x105ee2850, size 16, offset 0 at 0x105f880f0>
In [6]:
str(x.data)
Out[6]:
'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00'
数据的内存地址:
In [7]:
x.__array_interface__['data'][0]
Out[7]:
4352517296
完整的__array_interface__
:
In [8]:
x.__array_interface__
Out[8]:
{'data': (4352517296, False),
'descr': [('', '<i4')],
'shape': (4,),
'strides': None,
'typestr': '<i4',
'version': 3}
提醒:两个ndarrays
可以共享相同的内存:
In [9]:
x = np.array([1, 2, 3, 4])
y = x[:-1]
x[0] = 9
y
Out[9]:
array([9, 2, 3])
内存不必为一个ndarray
拥有:
In [10]:
x = '1234'
y = np.frombuffer(x, dtype=np.int8)
y.data
Out[10]:
<read-only buffer for 0x105ee2e40, size 4, offset 0 at 0x105f883b0>
In [11]:
y.base is x
Out[11]:
True
In [12]:
y.flags
Out[12]:
C_CONTIGUOUS : True
F_CONTIGUOUS : True
OWNDATA : False
WRITEABLE : False
ALIGNED : True
UPDATEIFCOPY : False
owndata
和writeable
标记表明了内存块的状态。
也可以看一下:array接口
2.2.1.3 数据类型
2.2.1.3.1 描述符
dtype
描述了数组里的一个项目:
type | 数据的标量类型,int8、int16、float64等之一(固定大小),str、unicode、void(可变大小) |
itemsize | 数据块的大小 |
byteorder | 字节序: big-endian > / little-endian < / 不可用 |
fields | 子-dtypes,如果是一个结构化的数据类型 |
shape | 数组的形状,如果是一个子数组 |
In [13]:
np.dtype(int).type
Out[13]:
numpy.int64
In [14]:
np.dtype(int).itemsize
Out[14]:
8
In [15]:
np.dtype(int).byteorder
Out[15]:
'='
2.2.1.3.2 例子:读取.wav文件
The.wav
file header:
chunk_id | "RIFF" |
chunk_size | 4字节无符号little-endian整型 |
format | "WAVE" |
fmt_id | "fmt " |
fmt_size | 4字节无符号little-endian整型 |
audio_fmt | 2字节无符号little-endian整型 |
num_channels | 2字节无符号little-endian整型 |
sample_rate | 4字节无符号little-endian整型 |
byte_rate | 4字节无符号little-endian整型 |
block_align | 2字节无符号little-endian整型 |
bits_per_sample | 2字节无符号little-endian整型 |
data_id | "data" |
data_size | 4字节无符号little-endian整型 |
- 44字节块的原始数据(在文件的开头)
- ...接下来是
data_size
实际声音数据的字节。
.wav
文件头是Numpy结构化数据类型:
In [6]:
wav_header_dtype = np.dtype([
("chunk_id", (str, 4)), # flexible-sized scalar type, item size 4
("chunk_size", "<u4"), # little-endian unsigned 32-bit integer
("format", "S4"), # 4-byte string
("fmt_id", "S4"),
("fmt_size", "<u4"),
("audio_fmt", "<u2"), #
("num_channels", "<u2"), # .. more of the same ...
("sample_rate", "<u4"), #
("byte_rate", "<u4"),
("block_align", "<u2"),
("bits_per_sample", "<u2"),
("data_id", ("S1", (2, 2))), # sub-array, just for fun!
("data_size", "u4"),
#
# the sound data itself cannot be represented here:
# it does not have a fixed size
])
也可以看一下wavreader.py
In [5]:
wav_header_dtype['format']
Out[5]:
dtype('S4')
In [6]:
wav_header_dtype.fields
Out[6]:
<dictproxy {'audio_fmt': (dtype('uint16'), 20),
'bits_per_sample': (dtype('uint16'), 34),
'block_align': (dtype('uint16'), 32),
'byte_rate': (dtype('uint32'), 28),
'chunk_id': (dtype('S4'), 0),
'chunk_size': (dtype('uint32'), 4),
'data_id': (dtype(('S1', (2, 2))), 36),
'data_size': (dtype('uint32'), 40),
'fmt_id': (dtype('S4'), 12),
'fmt_size': (dtype('uint32'), 16),
'format': (dtype('S4'), 8),
'num_channels': (dtype('uint16'), 22),
'sample_rate': (dtype('uint32'), 24)}>
In [7]:
wav_header_dtype.fields['format']
Out[7]:
(dtype('S4'), 8)
- 第一个元素是结构化数据中对应于名称
format
的子类型 - 第二个是它的从项目开始的偏移(以字节计算)
练习
小练习,通过使用偏移来创造一个“稀释”的dtype,只使用一些字段:
In [ ]:
wav_header_dtype = np.dtype(dict(
names=['format', 'sample_rate', 'data_id'],
offsets=[offset_1, offset_2, offset_3], # counted from start of structure in bytes
formats=list of dtypes for each of the fields,
))
并且用它来读取sample rate和data_id
(就像子数组)。
In [7]:
f = open('data/test.wav', 'r')
wav_header = np.fromfile(f, dtype=wav_header_dtype, count=1)
f.close()
print(wav_header)
[ ('RIFF', 17402L, 'WAVE', 'fmt ', 16L, 1, 1, 16000L, 32000L, 2, 16, [['d', 'a'], ['t', 'a']], 17366L)]
In [8]:
wav_header['sample_rate']
Out[8]:
array([16000], dtype=uint32)
让我们访问子数组:
In [9]:
wav_header['data_id']
Out[9]:
array([[['d', 'a'],
['t', 'a']]],
dtype='|S1')
In [10]:
wav_header.shape
Out[10]:
(1,)
In [11]:
wav_header['data_id'].shape
Out[11]:
(1, 2, 2)
当访问子数组时,维度被添加到末尾!
注意:有许多模块可以用于加载声音数据,比如wavfile
、audiolab
等...
2.2.1.3.3 投射和再解释/视图
投射
- 赋值
- 数组构建
- 算术
- 等等
- 手动:
.astype(dtype)
data re-interpretation
- 手动:
.view(dtype)
2.2.1.3.3.1 投射
- 算术投射,简而言之:
- 只有类型(不是值!)操作符最重要
- 最大的“安全”模式能代表选出的两者
- 在一些情况下,数组中的量值可能“丢失”
- 在通用复制数据中的投射:
In [4]:
x = np.array([1, 2, 3, 4], dtype=np.float)
x
Out[4]:
array([ 1., 2., 3., 4.])
In [5]:
y = x.astype(np.int8)
y
Out[5]:
array([1, 2, 3, 4], dtype=int8)
In [6]:
y + 1
Out[6]:
array([2, 3, 4, 5], dtype=int8)
In [7]:
y + 256
Out[7]:
array([257, 258, 259, 260], dtype=int16)
In [8]:
y + 256.0
Out[8]:
array([ 257., 258., 259., 260.])
In [9]:
y + np.array([256], dtype=np.int32)
Out[9]:
array([257, 258, 259, 260], dtype=int32)
- 集合项目上的投射:数组的dtype在项目赋值过程中不会改变:
In [10]:
y[:] = y + 1.5
y
Out[10]:
array([2, 3, 4, 5], dtype=int8)
注意 具体规则:见文档:http://docs.scipy.org/doc/numpy/reference/ufuncs.html#casting-rules
2.2.1.3.3.2 再解释/视图
- 内存中的数据块(4字节)
0x01 || 0x02 || 0x03 || 0x04
- 4 of uint8, OR,
- 4 of int8, OR,
- 2 of int16, OR,
- 1 of int32, OR,
- 1 of float32, OR,
- ...
如何从一个切换另一个?
- 切换dtype:
In [11]:
x = np.array([1, 2, 3, 4], dtype=np.uint8)
x.dtype = "<i2"
x
Out[11]:
array([ 513, 1027], dtype=int16)
In [12]:
0x0201, 0x0403
Out[12]:
(513, 1027)
0x01 0x02 || 0x03 0x04
注意 little-endian:越不重要的字节在内存的左侧
- 创建新视图:
In [14]:
y = x.view("<i4")
y
Out[14]:
array([67305985], dtype=int32)
In [15]:
0x04030201
Out[15]:
67305985
0x01 0x02 0x03 0x04
注意:
.view()
创建视图,并不复制(或改变)内存块- 只改变dtype(调整数组形状):
In [16]:
x[1] = 5
In [17]:
y
Out[17]:
array([328193], dtype=int32)
In [18]:
y.base is x
Out[18]:
True
小练习:数据再解释
也可以看一下: view-colors.py
数组中的RGBA数据:
In [19]:
x = np.zeros((10, 10, 4), dtype=np.int8)
x[:, :, 0] = 1
x[:, :, 1] = 2
x[:, :, 2] = 3
x[:, :, 3] = 4
后三个维度是R、B和G,以及alpha渠道。
如何用字段名‘r’, ‘g’, ‘b’, ‘a’创建一个(10,10)结构化数组而不用复制数据?
In [ ]:
y = ...
assert (y['r'] == 1).all()
assert (y['g'] == 2).all()
assert (y['b'] == 3).all()
assert (y['a'] == 4).all()
答案
...
警告:另一个占有四个字节内存的数组:
In [21]:
y = np.array([[1, 3], [2, 4]], dtype=np.uint8).transpose()
x = y.copy()
x
Out[21]:
array([[1, 2],
[3, 4]], dtype=uint8)
In [22]:
y
Out[22]:
array([[1, 2],
[3, 4]], dtype=uint8)
In [23]:
x.view(np.int16)
Out[23]:
array([[ 513],
[1027]], dtype=int16)
In [24]:
0x0201, 0x0403
Out[24]:
(513, 1027)
In [25]:
y.view(np.int16)
Out[25]:
array([[ 769, 1026]], dtype=int16)
- 发生了什么?
- ... 我们需要实际看一下x[0,1]里面是什么
In [26]:
0x0301, 0x0402
Out[26]:
(769, 1026)
2.2.1.4 索引体系:步幅
2.2.1.4.1 主要观点
问题
In [28]:
x = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], dtype=np.int8)
str(x.data)
Out[28]:
'\x01\x02\x03\x04\x05\x06\x07\x08\t'
item x[1,2]开始在x.data
中的哪个字节?
答案(在Numpy)
- 步幅:寻找一下个元素跳跃的字节数
- 每个维度一个步幅
In [29]:
x.strides
Out[29]:
(3, 1)
In [31]:
byte_offset = 3*1 + 1*2 # 查找x[1,2]
x.data[byte_offset]
Out[31]:
'\x06'
In [32]:
x[1, 2]
Out[32]:
6
- 简单、灵活
2.2.1.4.1.1 C和Fortran顺序
In [34]:
x = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], dtype=np.int16, order='C')
x.strides
Out[34]:
(6, 2)
In [35]:
str(x.data)
Out[35]:
'\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\t\x00'
- 需要跳跃6个字节寻找下一行
- 需要跳跃2个字节寻找下一列
In [36]:
y = np.array(x, order='F')
y.strides
Out[36]:
(2, 6)
In [37]:
str(y.data)
Out[37]:
'\x01\x00\x04\x00\x07\x00\x02\x00\x05\x00\x08\x00\x03\x00\x06\x00\t\x00'
- 需要跳跃2个字节寻找下一行
- 需要跳跃6个字节寻找下一列
更高维度也类似:
- C:最后的维度变化最快(=最小的步幅)
- F:最早的维度变化最快
注意:现在我们可以理解.view()
的行为:
In [38]:
y = np.array([[1, 3], [2, 4]], dtype=np.uint8).transpose()
x = y.copy()
变换顺序不影响数据的内部布局,只是步幅
In [39]:
x.strides
Out[39]:
(2, 1)
In [40]:
y.strides
Out[40]:
(1, 2)
In [41]:
str(x.data)
Out[41]:
'\x01\x02\x03\x04'
In [42]:
str(y.data)
Out[42]:
'\x01\x03\x02\x04'
- 当解释为int16时结果会不同
.copy()
以C顺序(默认)创建新的数组
2.2.1.4.1.2 用整数切片
- 通过仅改变形状、步幅和可能调整数据指针可以代表任何东西!
- 不用制造数据的副本
In [43]:
x = np.array([1, 2, 3, 4, 5, 6], dtype=np.int32)
y = x[::-1]
y
Out[43]:
array([6, 5, 4, 3, 2, 1], dtype=int32)
In [44]:
y.strides
Out[44]:
(-4,)
In [45]:
y = x[2:]
y.__array_interface__['data'][0] - x.__array_interface__['data'][0]
Out[45]:
8
In [46]:
x = np.zeros((10, 10, 10), dtype=np.float)
x.strides
Out[46]:
(800, 80, 8)
In [47]:
x[::2,::3,::4].strides
Out[47]:
(1600, 240, 32)
- 类似的,变换顺序绝不会创建副本(只是交换的步幅)
In [48]:
x = np.zeros((10, 10, 10), dtype=np.float)
x.strides
Out[48]:
(800, 80, 8)
In [49]:
x.T.strides
Out[49]:
(8, 80, 800)
但是:并不是所有的重排操作都可以通过操纵步幅来完成。
In [3]:
a = np.arange(6, dtype=np.int8).reshape(3, 2)
b = a.T
b.strides
Out[3]:
(1, 2)
到目前为止,都很不错,但是:
In [4]:
str(a.data)
Out[4]:
'\x00\x01\x02\x03\x04\x05'
In [5]:
b
Out[5]:
array([[0, 2, 4],
[1, 3, 5]], dtype=int8)
In [6]:
c = b.reshape(3*2)
c
Out[6]:
array([0, 2, 4, 1, 3, 5], dtype=int8)
这里,没办法用一个给定的步长和a
的内存块来表示数组c
。因此,重排操作在这里需要制作一个副本。
2.2.1.4.2 例子:用步长伪造维度
步长操作
In [2]:
from numpy.lib.stride_tricks import as_strided
help(as_strided)
Help on function as_strided in module numpy.lib.stride_tricks:
as_strided(x, shape=None, strides=None)
Make an ndarray from the given array with the given shape and strides.
警告:as_strided
并不检查你是否还待在内存块边界里..
In [9]:
x = np.array([1, 2, 3, 4], dtype=np.int16)
as_strided(x, strides=(2*2, ), shape=(2, ))
Out[9]:
array([1, 3], dtype=int16)
In [10]:
x[::2]
Out[10]:
array([1, 3], dtype=int16)
也可以看一下:stride-fakedims.py
练习
In [ ]:
array([1, 2, 3, 4], dtype=np.int8)
-> array([[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4]], dtype=np.int8)
仅使用as_strided
.:
提示:byte_offset = stride[0]index[0] + stride[1]index[1] + ...
解密:
步长可以设置为0:
In [11]:
x = np.array([1, 2, 3, 4], dtype=np.int8)
y = as_strided(x, strides=(0, 1), shape=(3, 4))
y
Out[11]:
array([[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4]], dtype=int8)
In [12]:
y.base.base is x
Out[12]:
True
2.2.1.4.3 广播
- 用它来做一些有用的事情:[1, 2, 3, 4]和[5, 6, 7]的外积
In [13]:
x = np.array([1, 2, 3, 4], dtype=np.int16)
x2 = as_strided(x, strides=(0, 1*2), shape=(3, 4))
x2
Out[13]:
array([[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4]], dtype=int16)
In [14]:
y = np.array([5, 6, 7], dtype=np.int16)
y2 = as_strided(y, strides=(1*2, 0), shape=(3, 4))
y2
Out[14]:
array([[5, 5, 5, 5],
[6, 6, 6, 6],
[7, 7, 7, 7]], dtype=int16)
In [15]:
x2 * y2
Out[15]:
array([[ 5, 10, 15, 20],
[ 6, 12, 18, 24],
[ 7, 14, 21, 28]], dtype=int16)
...看起来有一些熟悉...
In [16]:
x = np.array([1, 2, 3, 4], dtype=np.int16)
y = np.array([5, 6, 7], dtype=np.int16)
x[np.newaxis,:] * y[:,np.newaxis]
Out[16]:
array([[ 5, 10, 15, 20],
[ 6, 12, 18, 24],
[ 7, 14, 21, 28]], dtype=int16)
- 在内部,数组广播的确使用0步长来实现的。
2.2.1.4.4 更多技巧:对角线
也可以看一下 stride-diagonals.py
挑战
- 提取矩阵对角线的起点:(假定是C内存顺序):
In [ ]:
x = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], dtype=np.int32)
x_diag = as_strided(x, shape=(3,), strides=(???,))
- 提取第一个超级-对角线的起点[2,6]。
- 那么子对角线呢?
(后两个问题的提示:切片首先移动步长起点的点。)
答案
...
提取对角线:
In [6]:
x_diag = as_strided(x, shape=(3, ), strides=((3+1)*x.itemsize, ))
x_diag
Out[6]:
array([1, 5, 9], dtype=int32)
首先切片,调整数据指针:
In [8]:
as_strided(x[0, 1:], shape=(2, ), strides=((3+1)*x.itemsize, ))
Out[8]:
array([2, 6], dtype=int32)
In [9]:
as_strided(x[1:, 0], shape=(2, ), strides=((3+1)*x.itemsize, ))
Out[9]:
array([4, 8], dtype=int32)
笔记
In [7]:
y = np.diag(x, k=1)
y
Out[7]:
array([2, 6], dtype=int32)
但是
In [8]:
y.flags.owndata
Out[8]:
False
这是一个副本?!
也可以看一下stride-diagonals.py
挑战
计算张量的迹:
In [9]:
x = np.arange(5*5*5*5).reshape(5,5,5,5)
s = 0
for i in xrange(5):
for j in xrange(5):
s += x[j,i,j,i]
通过跨越并且在结果上使用sum()
。
In [ ]:
y = as_strided(x, shape=(5, 5), strides=(TODO, TODO))
s2 = ...
assert s == s2
答案
...
In [ ]:
y = as_strided(x, shape=(5, 5), strides=((5*5*5 + 5)*x.itemsize,
(5*5 + 1)*x.itemsize))
s2 = y.sum()
2.2.1.4.5 CPU缓存效果
内存布局可以影响性能:
In [13]:
x = np.zeros((20000,))
y = np.zeros((20000*67,))[::67]
x.shape, y.shape
Out[13]:
((20000,), (20000,))
In [14]:
%timeit x.sum()
The slowest run took 20.69 times longer than the fastest. This could mean that an intermediate result is being cached
10000 loops, best of 3: 15.4 µs per loop
In [15]:
%timeit y.sum()
The slowest run took 114.83 times longer than the fastest. This could mean that an intermediate result is being cached
10000 loops, best of 3: 53 µs per loop
In [16]:
x.strides, y.strides
Out[16]:
((8,), (536,))
小步长更快?
- CPU从主内存中拉取数据到缓存块 pulls data from main memory to its cache in blocks
- 如果需要数据项连续操作适应于一个内存块(小步长):
- 需要更少的迁移
- 更快
也可以看一下:numexpr
设计用来减轻数组计算时的缓存效果。
2.2.1.4.6 例子:原地操作(买者当心)
有时,
In [ ]:
a -= b
并不等同于
In [ ]:
a -= b.copy()
In [21]:
x = np.array([[1, 2], [3, 4]])
x -= x.transpose()
x
Out[21]:
array([[ 0, -1],
[ 1, 0]])
In [22]:
y = np.array([[1, 2], [3, 4]])
y -= y.T.copy()
y
Out[22]:
array([[ 0, -1],
[ 1, 0]])
x
和x.transpose()
共享数据x -= x.transpose()
逐个元素修改数据...- 因为
x
和x.transpose()
步长不同,修改后的数据重新出现在RHS
2.2.1.5 剖析上的发现
- 内存块:可以共享,
.base
、.data
- 数据类型描述符:结构化数据、子数组、字节顺序、投射、视图、
.astype()
、.view()
- 步长索引:跨越、C/F-order、w/ 整数切片、
as_strided
、广播、跨越技巧、diag
、CPU缓存一致性
2.2.2 通用函数
2.2.2.1 他们是什么?
- Ufunc在数组的所有元素上进行元素级操作。
例子:
np.add
、np.subtract
、scipy.special
.*, ...
- 自动话支持:广播、投射...
- ufunc的作者只提供了元素级操作,Numpy负责剩下的。
- 元素级操作需要在C中实现(或者比如Cython)
2.2.2.1.1 Ufunc的部分
- 由用户提供
In [ ]:
void ufunc_loop(void **args, int *dimensions, int *steps, void *data)
{
/*
* int8 output = elementwise_function(int8 input_1, int8 input_2)
*
* This function must compute the ufunc for many values at once,
* in the way shown below.
*/
char *input_1 = (char*)args[0];
char *input_2 = (char*)args[1];
char *output = (char*)args[2];
int i;
for (i = 0; i < dimensions[0]; ++i) {
*output = elementwise_function(*input_1, *input_2);
input_1 += steps[0];
input_2 += steps[1];
output += steps[2];
}
}
- Numpy部分,由下面的代码创建
In [ ]:
char types[3]
types[0] = NPY_BYTE /* type of first input arg */
types[1] = NPY_BYTE /* type of second input arg */
types[2] = NPY_BYTE /* type of third input arg */
PyObject *python_ufunc = PyUFunc_FromFuncAndData(
ufunc_loop,
NULL,
types,
1, /* ntypes */
2, /* num_inputs */
1, /* num_outputs */
identity_element,
name,
docstring,
unused)
- ufunc也可以支持多种不同输入输出类型组合。
2.2.2.1.2 简化一下
ufunc_loop
是非常通用的模式,Numpy提供了预制
PyUfunc_f_f |
float elementwise_func(float input_1) |
PyUfunc_ff_f |
float elementwise_func(float input_1, float input_2) |
PyUfunc_d_d |
double elementwise_func(double input_1) |
PyUfunc_dd_d |
double elementwise_func(double input_1, double input_2) |
PyUfunc_D_D |
elementwise_func(npy_cdouble \*input, npy_cdouble\* output) |
PyUfunc_DD_D |
elementwise_func(npy_cdouble \*in1, npy_cdouble \*in2, npy_cdouble\* out) |
- 只有需要提供`elementwise_func`
- ... 除非当你的元素级函数不是上面的形式中的任何一种
2.2.2.2 练习:从0开始构建一个ufunc
Mandelbrot分形由如下迭代定义:
C=X+iy
是一个复数,只要Z仍然是有限的,无论迭代要跑多久,迭代都会重复。C属于Mandelbrot集。
- ufunc调用
mandel(z0, c)
计算:
In [ ]:
z = z0
for k in range(iterations):
z = z*z + c
比如,一百次迭代或者直到z.real**2 + z.imag**2 > 1000
。用它来决定哪个C
是在Mandelbrot集中。
- 我们的函数是很简单的,因此,请利用
PyUFunc_*
助手。 - 用Cython来完成
也可以看一下mandel.pyx
,mandelplot.py
提醒:一些预设Ufunc循环:
PyUfunc_f_f |
float elementwise_func(float input_1) |
PyUfunc_ff_f |
float elementwise_func(float input_1, float input_2) |
PyUfunc_d_d |
double elementwise_func(double input_1) |
PyUfunc_dd_d |
double elementwise_func(double input_1, double input_2) |
PyUfunc_D_D |
elementwise_func(complex_double *input, complex_double* output) |
PyUfunc_DD_D |
elementwise_func(complex_double *in1, complex_double *in2, complex_double* out) |
打印代码:
NPY_BOOL, NPY_BYTE, NPY_UBYTE, NPY_SHORT, NPY_USHORT, NPY_INT, NPY_UINT, NPY_LONG, NPY_ULONG, NPY_LONGLONG, NPY_ULONGLONG, NPY_FLOAT, NPY_DOUBLE, NPY_LONGDOUBLE, NPY_CFLOAT, NPY_CDOUBLE, NPY_CLONGDOUBLE, NPY_DATETIME, NPY_TIMEDELTA, NPY_OBJECT, NPY_STRING, NPY_UNICODE, NPY_VOID
2.2.2.3 答案:从0开始创建一个ufunc
In [ ]:
# The elementwise function
# ------------------------
cdef void mandel_single_point(double complex *z_in,
double complex *c_in,
double complex *z_out) nogil:
#
# The Mandelbrot iteration
#
#
# Some points of note:
#
# - It's *NOT* allowed to call any Python functions here.
#
# The Ufunc loop runs with the Python Global Interpreter Lock released.
# Hence, the ``nogil``.
#
# - And so all local variables must be declared with ``cdef``
#
# - Note also that this function receives *pointers* to the data;
# the "traditional" solution to passing complex variables around
#
cdef double complex z = z_in[0]
cdef double complex c = c_in[0]
cdef int k # the integer we use in the for loop
# Straightforward iteration
for k in range(100):
z = z*z + c
if z.real**2 + z.imag**2 > 1000:
break
# Return the answer for this point
z_out[0] = z
# Boilerplate Cython definitions
#
# You don't really need to read this part, it just pulls in
# stuff from the Numpy C headers.
# ----------------------------------------------------------
cdef extern from "numpy/arrayobject.h":
void import_array()
ctypedef int npy_intp
cdef enum NPY_TYPES:
NPY_CDOUBLE
cdef extern from "numpy/ufuncobject.h":
void import_ufunc()
ctypedef void (*PyUFuncGenericFunction)(char**, npy_intp*, npy_intp*, void*)
object PyUFunc_FromFuncAndData(PyUFuncGenericFunction* func, void** data,
char* types, int ntypes, int nin, int nout,
int identity, char* name, char* doc, int c)
void PyUFunc_DD_D(char**, npy_intp*, npy_intp*, void*)
# Required module initialization
# ------------------------------
import_array()
import_ufunc()
# The actual ufunc declaration
# ----------------------------
cdef PyUFuncGenericFunction loop_func[1]
cdef char input_output_types[3]
cdef void *elementwise_funcs[1]
loop_func[0] = PyUFunc_DD_D
input_output_types[0] = NPY_CDOUBLE
input_output_types[1] = NPY_CDOUBLE
input_output_types[2] = NPY_CDOUBLE
elementwise_funcs[0] = <void*>mandel_single_point
mandel = PyUFunc_FromFuncAndData(
loop_func,
elementwise_funcs,
input_output_types,
1, # number of supported input types
2, # number of input args
1, # number of output args
0, # `identity` element, never mind this
"mandel", # function name
"mandel(z, c) -> computes iterated z*z + c", # docstring
0 # unused
)
In [ ]:
import numpy as np
import mandel
x = np.linspace(-1.7, 0.6, 1000)
y = np.linspace(-1.4, 1.4, 1000)
c = x[None,:] + 1j*y[:,None]
z = mandel.mandel(c, c)
import matplotlib.pyplot as plt
plt.imshow(abs(z)**2 < 1000, extent=[-1.7, 0.6, -1.4, 1.4])
plt.gray()
plt.show()
笔记:大多数模板可以由下列Cython模块来自动完成: http://wiki.cython.org/MarkLodato/CreatingUfuncs
一些可接受的输入类型
例如:支持小数点后一位及两位两个准确度版本
In [ ]:
cdef void mandel_single_point(double complex *z_in,
double complex *c_in,
double complex *z_out) nogil:
...
cdef void mandel_single_point_singleprec(float complex *z_in,
float complex *c_in,
float complex *z_out) nogil:
...
cdef PyUFuncGenericFunction loop_funcs[2]
cdef char input_output_types[3*2]
cdef void *elementwise_funcs[1*2]
loop_funcs[0] = PyUFunc_DD_D
input_output_types[0] = NPY_CDOUBLE
input_output_types[1] = NPY_CDOUBLE
input_output_types[2] = NPY_CDOUBLE
elementwise_funcs[0] = <void*>mandel_single_point
loop_funcs[1] = PyUFunc_FF_F
input_output_types[3] = NPY_CFLOAT
input_output_types[4] = NPY_CFLOAT
input_output_types[5] = NPY_CFLOAT
elementwise_funcs[1] = <void*>mandel_single_point_singleprec
mandel = PyUFunc_FromFuncAndData(
loop_func,
elementwise_funcs,
input_output_types,
2, # number of supported input types <----------------
2, # number of input args
1, # number of output args
0, # `identity` element, never mind this
"mandel", # function name
"mandel(z, c) -> computes iterated z*z + c", # docstring
0 # unused
)
2.2.2.4 广义ufuncs
ufunc
output = elementwise_function(input)
output
和input
都可以只是一个数组元素。
广义ufunc
output
和input
可以是有固定维度数的数组
例如,矩阵迹(对象线元素的sum):
In [ ]:
input shape = (n, n)
output shape = () i.e. scalar
(n, n) -> ()
矩阵乘积:
In [ ]:
input_1 shape = (m, n)
input_2 shape = (n, p)
output shape = (m, p)
(m, n), (n, p) -> (m, p)
- 这是广义ufunc的”签名“
- g-ufunc发挥作用的维度是“核心维度”
Numpy中的状态
- g-ufuncs已经在Numpy中...
- 新的可以用
PyUFunc_FromFuncAndDataAndSignature
来创建 - ... 但是,除了测试外,我们不会配置公用的g-ufuncs,ATM
In [4]:
import numpy.core.umath_tests as ut
ut.matrix_multiply.signature
Out[4]:
'(m,n),(n,p)->(m,p)'
In [5]:
x = np.ones((10, 2, 4))
y = np.ones((10, 4, 5))
ut.matrix_multiply(x, y).shape
Out[5]:
(10, 2, 5)
- 后两个维度成为了核心维度,并且根据每个签名去修改
- 否则,g-ufunc“按元素级”运行
- 这种方式的矩阵乘法对一次在许多小矩阵是非常有用
广义ufunc循环
矩阵相乘 (m,n),(n,p) -> (m,p)
In [ ]:
void gufunc_loop(void **args, int *dimensions, int *steps, void *data)
{
char *input_1 = (char*)args[0]; /* these are as previously */
char *input_2 = (char*)args[1];
char *output = (char*)args[2];
int input_1_stride_m = steps[3]; /* strides for the core dimensions */
int input_1_stride_n = steps[4]; /* are added after the non-core */
int input_2_strides_n = steps[5]; /* steps */
int input_2_strides_p = steps[6];
int output_strides_n = steps[7];
int output_strides_p = steps[8];
int m = dimension[1]; /* core dimensions are added after */
int n = dimension[2]; /* the main dimension; order as in */
int p = dimension[3]; /* signature */
int i;
for (i = 0; i < dimensions[0]; ++i) {
matmul_for_strided_matrices(input_1, input_2, output,
strides for each array...);
input_1 += steps[0];
input_2 += steps[1];
output += steps[2];
}
}
2.2.3 互操性功能
2.2.3.1 多维度类型数据贡献
假设你
- 写一个库处理(多维度)二进制数据,
- 想要它可以用Numpy或者其他库来简单的操作数据,
- ... 但是并不像依赖Numpy。
目前,三个解决方案:
- “旧的” buffer接口
- 数组接口
- “新的” buffer接口(PEP 3118)
2.2.3.2 旧的buffer协议
- 只有1-D buffers
- 没有数据类型信息
- C-级接口;
PyBufferProcs
tp_as_buffer
在类型对象中 - 但是它被整合在Python中(比如,字符支持这个协议)
使用PIL(Python Imaging Library)的小练习:
也可以看一下:pilbuffer.py
In [ ]:
import Image
data = np.zeros((200, 200, 4), dtype=np.int8)
data[:, :] = [255, 0, 0, 255] # Red
# In PIL, RGBA images consist of 32-bit integers whose bytes are [RR,GG,BB,AA]
data = data.view(np.int32).squeeze()
img = Image.frombuffer("RGBA", (200, 200), data)
img.save('test.png')
Q: 检查一下如果data
修改的话,再保存一下img
看一下会发生什么。
2.2.3.3 旧的buffer协议
In [9]:
import numpy as np
# import Image
from PIL import Image
# Let's make a sample image, RGBA format
x = np.zeros((200, 200, 4), dtype=np.int8)
x[:,:,0] = 254 # red
x[:,:,3] = 255 # opaque
data = x.view(np.int32) # Check that you understand why this is OK!
img = Image.frombuffer("RGBA", (200, 200), data)
img.save('test.png')
#
# Modify the original data, and save again.
#
# It turns out that PIL, which knows next to nothing about Numpy,
# happily shares the same data.
#
x[:,:,1] = 254
img.save('test2.png')
/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/IPython/kernel/__main__.py:14: RuntimeWarning: the frombuffer defaults may change in a future release; for portability, change the call to read:
frombuffer(mode, size, data, 'raw', mode, 0, 1)
2.2.3.4 数组接口协议
- 多维度buffers
- 存在数据类型信息
- Numpy-特定方法;慢慢的废弃(不过不会消失)
- 然而,没有整合在Python中
也可以看一下:文档:http://docs.scipy.org/doc/numpy/reference/arrays.interface.html
In [8]:
x = np.array([[1, 2], [3, 4]])
x.__array_interface__
Out[8]:
{'data': (4298825184, False),
'descr': [('', '<i8')],
'shape': (2, 2),
'strides': None,
'typestr': '<i8',
'version': 3}
In [11]:
# import Image
from PIL import Image
img = Image.open('data/test.png')
img.__array_interface__
Out[11]:
{'data': '\xfe\x00\x00\xff\xfe\x00\x00...\xff\xfe\x00\x00\xff',
'shape': (200, 200, 4),
'typestr': '|u1'}
In [12]:
x = np.asarray(img)
x.shape
Out[12]:
(200, 200, 4)
In [13]:
x.dtype
Out[13]:
dtype('uint8')
笔记: 一个对C更友好的数组接口变体也被定义出来了。
2.2.4 数组的兄弟:chararray、maskedarray、matrix
2.2.4.1 chararray:: 向量化字符操作
In [14]:
x = np.array(['a', ' bbb', ' ccc']).view(np.chararray)
x.lstrip(' ')
Out[14]:
chararray(['a', 'bbb', 'ccc'],
dtype='|S5')
In [15]:
x.upper()
Out[15]:
chararray(['A', ' BBB', ' CCC'],
dtype='|S5')
笔记:.view()
有另一个含义:它将一个ndarray变成专门的ndarray子类的一个实例
2.2.4.2 masked_array缺失数据
Masked arrays是有缺失或无效条目的数组。
例如,假如我们有一个第四个条目无效的数组:
In [16]:
x = np.array([1, 2, 3, -99, 5])
描述这个数组的一个方式是创建masked array:
In [17]:
mx = np.ma.masked_array(x, mask=[0, 0, 0, 1, 0])
mx
Out[17]:
masked_array(data = [1 2 3 -- 5],
mask = [False False False True False],
fill_value = 999999)
Masked平均数会忽略masked数据:
In [18]:
mx.mean()
Out[18]:
2.75
In [19]:
np.mean(mx)
Out[19]:
2.75
警告:不是所有的Numpy函数都接收mask,例如,np.dot
,因此,请检查返回的类型。
masked_array
返回原始数组的一个视图:
In [20]:
mx[1] = 9
x
Out[20]:
array([ 1, 9, 3, -99, 5])
2.2.4.2.1 mask
你可以通过赋值来修改mask:
In [21]:
mx[1] = np.ma.masked
mx
Out[21]:
masked_array(data = [1 -- 3 -- 5],
mask = [False True False True False],
fill_value = 999999)
通过赋值可以清除mask:
In [22]:
mx[1] = 9
mx
Out[22]:
masked_array(data = [1 9 3 -- 5],
mask = [False False False True False],
fill_value = 999999)
mask也可以直接访问:
In [23]:
mx.mask
Out[23]:
array([False, False, False, True, False], dtype=bool)
masked条目可以通过填入一个给定值来变回一般的数组:
In [24]:
x2 = mx.filled(-1)
x2
Out[24]:
array([ 1, 9, 3, -1, 5])
mask也可以被清除:
In [25]:
mx.mask = np.ma.nomask
mx
Out[25]:
masked_array(data = [1 9 3 -99 5],
mask = [False False False False False],
fill_value = 999999)
2.2.4.2.2 领域相关的函数
masked数组包也包含一些领域相关的函数:
In [26]:
np.ma.log(np.array([1, 2, -1, -2, 3, -5]))
Out[26]:
masked_array(data = [0.0 0.6931471805599453 -- -- 1.0986122886681098 --],
mask = [False False True True False True],
fill_value = 1e+20)
笔记:对于高效无缝处理数组中的缺失值的支持将在Numpy 1.7中出现。现在还在优化中!
例子:Masked统计
加拿大的护林员在计算1903-1918年野兔和猞猁的数量时有些心烦意乱,数字经常出错。(尽管胡萝卜农场主不断的警告。)计算随着时间推移的平均数,忽略无效数据。
In [4]:
data = np.loadtxt('data/populations.txt')
populations = np.ma.masked_array(data[:,1:])
year = data[:, 0]
bad_years = (((year >= 1903) & (year <= 1910))
| ((year >= 1917) & (year <= 1918)))
# '&' means 'and' and '|' means 'or'
populations[bad_years, 0] = np.ma.masked
populations[bad_years, 1] = np.ma.masked
populations.mean(axis=0)
Out[4]:
masked_array(data = [40472.72727272727 18627.272727272728 42400.0],
mask = [False False False],
fill_value = 1e+20)
In [5]:
populations.std(axis=0)
Out[5]:
masked_array(data = [21087.656489006717 15625.799814240254 3322.5062255844787],
mask = [False False False],
fill_value = 1e+20)
注意,Matplotlib了解masked数组:
In [8]:
plt.plot(year, populations, 'o-')
Out[8]:
[<matplotlib.lines.Line2D at 0x10565f250>,
<matplotlib.lines.Line2D at 0x10565f490>,
<matplotlib.lines.Line2D at 0x10565f650>]
2.2.4.3 recarray:仅仅方便
In [9]:
arr = np.array([('a', 1), ('b', 2)], dtype=[('x', 'S1'), ('y', int)])
arr2 = arr.view(np.recarray)
arr2.x
Out[9]:
chararray(['a', 'b'],
dtype='|S1')
In [10]:
arr2.y
Out[10]:
array([1, 2])
2.2.4.4 矩阵:方便?
- 通常是2-D
- 是矩阵的积,不是元素级的积
In [11]:
np.matrix([[1, 0], [0, 1]]) * np.matrix([[1, 2], [3, 4]])
Out[11]:
matrix([[1, 2],
[3, 4]])
2.2.5 总结
- ndarray的剖析:data、dtype, 步长
- 通用函数:元素级操作,如何常见一个新的通用函数
- Ndarray子类
- 整合其他工具的多种buffer接口
- 最近的补充:PEP 3118,广义ufuncs
2.2.6 为Numpy/Scipy做贡献
看一下这篇教程:http://www.euroscipy.org/talk/882
2.2.6.1 为什么
- “这有个bug?”
- “我不理解这个要做什么?”
- “我有这个神器的代码。你要吗?”
- “我需要帮助!我应该怎么办?”
2.2.6.2 报告bugs
Bug跟踪(推荐这种方式)
- http://projects.scipy.org/numpy
- http://projects.scipy.org/scipy
- 点击“注册”链接获得一个帐号
邮件列表 ( scipy.org/Mailing_Lists )
- 如果你不确定
- 在一周左右还没有任何回复?去开一个bug ticket吧。
2.2.6.2.1 好的bug报告
Title: numpy.random.permutations fails for non-integer arguments
I'm trying to generate random permutations, using numpy.random.permutations
When calling numpy.random.permutation with non-integer arguments it fails with a cryptic error message::
>>> np.random.permutation(12)
array([ 6, 11, 4, 10, 2, 8, 1, 7, 9, 3, 0, 5])
>>> np.random.permutation(12.)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "mtrand.pyx", line 3311, in mtrand.RandomState.permutation
File "mtrand.pyx", line 3254, in mtrand.RandomState.shuffle
TypeError: len() of unsized object
This also happens with long arguments, and so np.random.permutation(X.shape[0]) where X is an array fails on 64 bit windows (where shape is a tuple of longs).
It would be great if it could cast to integer or at least raise a proper error for non-integer types.
I'm using Numpy 1.4.1, built from the official tarball, on Windows 64 with Visual studio 2008, on Python.org 64-bit Python.
- 你要做什么?
- 重现bug的小代码段(如果可能的话)
- 实际上发生了什么
- 期望发生什么
- 平台(Windows / Linux / OSX, 32/64 bits, x86/PPC, ...)
- Numpy/Scipy的版本
In [2]:
print np.__version__
1.9.2
检查下面的文件是你所期望的
In [3]:
print np.__file__
/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/numpy/__init__.pyc
以免你想要旧/损坏的Numpy安装在哪里
如果不确定,试着删除现有的Numpy安装文件,并且重新安装...
2.2.6.3 为文档做贡献
文档编辑器
- http://docs.scipy.org/numpy
注册
- 注册一个帐号
- 订阅scipy-dev邮件列表(仅限订阅者)
- 邮件列表有问题:你可以发邮件
- 但是:你可以关闭邮件送达
- 在http://mail.scipy.org/mailman/listinfo/scipy-dev 底部“改变你的订阅选项”
- 给@
scipy-dev
邮件列表发一封邮件;要求激活:
- 给@
Hi,
I'd like to edit Numpy/Scipy docstrings. My account is XXXXX
Cheers, N. N.
```
- 检查一下风格指南:
- 不要被吓住;要修补一个小事情,就修补它 ```
- 编辑
- 编辑源码发送补丁(和bug一样)
- 向邮件列表抱怨
2.2.6.4 贡献功能
- 在邮件列表上询问,如果不确定应该它应该在哪里
- 写一个补丁,在bug跟踪器上添加一个增强的ticket
- 或者,创建一个实现了这个功能的Git分支 + 添加增强ticket。
In [ ]:
# 克隆numpy仓库
git clone --origin svn http://projects.scipy.org/git/numpy.git numpy
cd numpy
# 创建功能分支
git checkout -b name-of-my-feature-branch svn/trunk
<edit stuff>
git commit -a
- 在http://github.com (或者其他地方)创建一个帐号
- @ Github创建一个新仓库
- 将你的工作推送到github
In [ ]:
git remote add github git@github:YOURUSERNAME/YOURREPOSITORYNAME.git
git push github name-of-my-feature-branch
2.2.6.5 如何帮助,总体而言
- 永远欢迎修补bug!
- 什么让你最恼怒
- 浏览一下跟踪器
- 文档工作
- API文档:改善文档字符串
- 很好的了解了一些Scipy模块
- 用户指南
- 最终需要完成
- 想要想一下?看一下目录 http://scipy.org/Developer_Zone/UG_Toc
- API文档:改善文档字符串
- 在沟通渠道上询问:
numpy-discussion
列表scipy-dev
列表
In [1]:
%matplotlib inline
import numpy as np