将numpy结构化数组子集转换为numpy数组而不复制
Posted
技术标签:
【中文标题】将numpy结构化数组子集转换为numpy数组而不复制【英文标题】:Converting numpy structured array subset to numpy array without copy 【发布时间】:2016-12-27 19:58:37 【问题描述】:假设我有以下 numpy 结构化数组:
In [250]: x
Out[250]:
array([(22, 2, -1000000000, 2000), (22, 2, 400, 2000),
(22, 2, 804846, 2000), (44, 2, 800, 4000), (55, 5, 900, 5000),
(55, 5, 1000, 5000), (55, 5, 8900, 5000), (55, 5, 11400, 5000),
(33, 3, 14500, 3000), (33, 3, 40550, 3000), (33, 3, 40990, 3000),
(33, 3, 44400, 3000)],
dtype=[('f1', '<i4'), ('f2', '<f4'), ('f3', '<f4'), ('f4', '<i4')])
我正在尝试将上述数组的子集修改为常规的 numpy 数组。 对于我的应用程序来说,不创建任何副本(仅视图)是必不可少的。
使用以下函数从上述结构化数组中检索字段:
def fields_view(array, fields):
return array.getfield(numpy.dtype(
name: array.dtype.fields[name] for name in fields
))
如果我对“f2”和“f3”字段感兴趣,我会执行以下操作:
In [251]: y=fields_view(x,['f2','f3'])
In [252]: y
Out [252]:
array([(2.0, -1000000000.0), (2.0, 400.0), (2.0, 804846.0), (2.0, 800.0),
(5.0, 900.0), (5.0, 1000.0), (5.0, 8900.0), (5.0, 11400.0),
(3.0, 14500.0), (3.0, 40550.0), (3.0, 40990.0), (3.0, 44400.0)],
dtype='names':['f2','f3'], 'formats':['<f4','<f4'], 'offsets':[4,8], 'itemsize':12)
有一种方法可以直接从原始结构化数组的“f2”和“f3”字段中获取 ndarray。但是,对于我的应用程序,有必要构建这个中间结构化数组,因为这个数据子集是一个类的属性。
如果不进行复制,我无法将中间结构化数组转换为常规的 numpy 数组。
In [253]: y.view(('<f4', len(y.dtype.names)))
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-54-f8fc3a40fd1b> in <module>()
----> 1 y.view(('<f4', len(y.dtype.names)))
ValueError: new type not compatible with array.
该函数也可用于将记录数组转换为ndarray:
def recarr_to_ndarr(x,typ):
fields = x.dtype.names
shape = x.shape + (len(fields),)
offsets = [x.dtype.fields[name][1] for name in fields]
assert not any(np.diff(offsets, n=2))
strides = x.strides + (offsets[1] - offsets[0],)
y = np.ndarray(shape=shape, dtype=typ, buffer=x,
offset=offsets[0], strides=strides)
return y
但是,我收到以下错误:
In [254]: recarr_to_ndarr(y,'<f4')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-65-2ebda2a39e9f> in <module>()
----> 1 recarr_to_ndarr(y,'<f4')
<ipython-input-62-8a9eea8e7512> in recarr_to_ndarr(x, typ)
8 strides = x.strides + (offsets[1] - offsets[0],)
9 y = np.ndarray(shape=shape, dtype=typ, buffer=x,
---> 10 offset=offsets[0], strides=strides)
11 return y
12
TypeError: expected a single-segment buffer object
如果我创建一个副本,该功能可以正常工作:
In [255]: recarr_to_ndarr(np.array(y),'<f4')
Out[255]:
array([[ 2.00000000e+00, -1.00000000e+09],
[ 2.00000000e+00, 4.00000000e+02],
[ 2.00000000e+00, 8.04846000e+05],
[ 2.00000000e+00, 8.00000000e+02],
[ 5.00000000e+00, 9.00000000e+02],
[ 5.00000000e+00, 1.00000000e+03],
[ 5.00000000e+00, 8.90000000e+03],
[ 5.00000000e+00, 1.14000000e+04],
[ 3.00000000e+00, 1.45000000e+04],
[ 3.00000000e+00, 4.05500000e+04],
[ 3.00000000e+00, 4.09900000e+04],
[ 3.00000000e+00, 4.44000000e+04]], dtype=float32)
这两个数组似乎没有区别:
In [66]: y
Out[66]:
array([(2.0, -1000000000.0), (2.0, 400.0), (2.0, 804846.0), (2.0, 800.0),
(5.0, 900.0), (5.0, 1000.0), (5.0, 8900.0), (5.0, 11400.0),
(3.0, 14500.0), (3.0, 40550.0), (3.0, 40990.0), (3.0, 44400.0)],
dtype='names':['f2','f3'], 'formats':['<f4','<f4'], 'offsets':[4,8], 'itemsize':12)
In [67]: np.array(y)
Out[67]:
array([(2.0, -1000000000.0), (2.0, 400.0), (2.0, 804846.0), (2.0, 800.0),
(5.0, 900.0), (5.0, 1000.0), (5.0, 8900.0), (5.0, 11400.0),
(3.0, 14500.0), (3.0, 40550.0), (3.0, 40990.0), (3.0, 44400.0)],
dtype='names':['f2','f3'], 'formats':['<f4','<f4'], 'offsets':[4,8], 'itemsize':12)
【问题讨论】:
【参考方案1】:这个答案有点长而且漫无边际。我从之前的工作中了解的关于获取数组视图的知识开始,然后尝试将其与您的函数联系起来。
=================
在您的情况下,所有字段都是 4 个字节长,包括浮点数和整数。然后我可以将其视为所有整数或所有浮点数:
In [1431]: x
Out[1431]:
array([(22, 2.0, -1000000000.0, 2000), (22, 2.0, 400.0, 2000),
(22, 2.0, 804846.0, 2000), (44, 2.0, 800.0, 4000),
(55, 5.0, 900.0, 5000), (55, 5.0, 1000.0, 5000),
(55, 5.0, 8900.0, 5000), (55, 5.0, 11400.0, 5000),
(33, 3.0, 14500.0, 3000), (33, 3.0, 40550.0, 3000),
(33, 3.0, 40990.0, 3000), (33, 3.0, 44400.0, 3000)],
dtype=[('f1', '<i4'), ('f2', '<f4'), ('f3', '<f4'), ('f4', '<i4')])
In [1432]: x.view('i4')
Out[1432]:
array([ 22, 1073741824, -831624408, 2000, 22,
1073741824, 1137180672, 2000, 22, 1073741824,
1229225696, 2000, 44, 1073741824, 1145569280,
.... 3000])
In [1433]: x.view('f4')
Out[1433]:
array([ 3.08285662e-44, 2.00000000e+00, -1.00000000e+09,
2.80259693e-42, 3.08285662e-44, 2.00000000e+00,
.... 4.20389539e-42], dtype=float32)
这个视图是一维的。我可以对 2 个浮动列进行整形和切片
In [1434]: x.shape
Out[1434]: (12,)
In [1435]: x.view('f4').reshape(12,-1)
Out[1435]:
array([[ 3.08285662e-44, 2.00000000e+00, -1.00000000e+09,
2.80259693e-42],
[ 3.08285662e-44, 2.00000000e+00, 4.00000000e+02,
2.80259693e-42],
...
[ 4.62428493e-44, 3.00000000e+00, 4.44000000e+04,
4.20389539e-42]], dtype=float32)
In [1437]: x.view('f4').reshape(12,-1)[:,1:3]
Out[1437]:
array([[ 2.00000000e+00, -1.00000000e+09],
[ 2.00000000e+00, 4.00000000e+02],
[ 2.00000000e+00, 8.04846000e+05],
[ 2.00000000e+00, 8.00000000e+02],
...
[ 3.00000000e+00, 4.44000000e+04]], dtype=float32)
可以通过做一些就地数学来验证这是一个视图,并在x
中查看结果:
In [1439]: y=x.view('f4').reshape(12,-1)[:,1:3]
In [1440]: y[:,0] += .5
In [1441]: y
Out[1441]:
array([[ 2.50000000e+00, -1.00000000e+09],
[ 2.50000000e+00, 4.00000000e+02],
...
[ 3.50000000e+00, 4.44000000e+04]], dtype=float32)
In [1442]: x
Out[1442]:
array([(22, 2.5, -1000000000.0, 2000), (22, 2.5, 400.0, 2000),
(22, 2.5, 804846.0, 2000), (44, 2.5, 800.0, 4000),
(55, 5.5, 900.0, 5000), (55, 5.5, 1000.0, 5000),
(55, 5.5, 8900.0, 5000), (55, 5.5, 11400.0, 5000),
(33, 3.5, 14500.0, 3000), (33, 3.5, 40550.0, 3000),
(33, 3.5, 40990.0, 3000), (33, 3.5, 44400.0, 3000)],
dtype=[('f1', '<i4'), ('f2', '<f4'), ('f3', '<f4'), ('f4', '<i4')])
如果字段大小不同,这可能是不可能的。例如,如果浮点数为 8 个字节。关键是描绘结构化数据的存储方式,并想象是否可以将其视为多列的简单 dtype。并且字段选择必须等同于基本切片。使用 ['f1','f4'] 相当于使用 [:,[0,3] 进行高级索引,它必须是一个副本。
===========
“直接”字段索引是:
z = x[['f2','f3']].view('f4').reshape(12,-1)
z -= .5
修改z
,但使用futurewarning
。它也不会修改x
; z
已成为副本。通过查看z.__array_interface__['data']
,数据缓冲区位置(并与x
和y
的比较),我也可以看到这一点。
==================
您的 fields_view
确实创建了结构化视图:
In [1480]: w=fields_view(x,['f2','f3'])
In [1481]: w.__array_interface__['data']
Out[1481]: (151950184, False)
In [1482]: x.__array_interface__['data']
Out[1482]: (151950184, False)
可用于修改x
、w['f2'] -= .5
。所以它比“直接”x[['f2','f3']]
更通用。
w
dtype 是
dtype('names':['f2','f3'], 'formats':['<f4','<f4'], 'offsets':[4,8], 'itemsize':12)
将print(shape, typ, offsets, strides)
添加到您的recarr_to_ndarr
,我得到 (py3)
In [1499]: recarr_to_ndarr(w,'<f4')
(12, 2) <f4 [4, 8] (16, 4)
....
ValueError: ndarray is not contiguous
In [1500]: np.ndarray(shape=(12,2), dtype='<f4', buffer=w.data, offset=4, strides=(16,4))
...
BufferError: memoryview: underlying buffer is not contiguous
那个contiguous
的问题一定是指w.flags
中显示的值:
In [1502]: w.flags
Out[1502]:
C_CONTIGUOUS : False
F_CONTIGUOUS : False
....
有趣的是w.dtype.descr
将“偏移量”转换为一个未命名的字段:
In [1506]: w.__array_interface__
Out[1506]:
'data': (151950184, False),
'descr': [('', '|V4'), ('f2', '<f4'), ('f3', '<f4')],
'shape': (12,),
'strides': (16,),
'typestr': '|V12',
'version': 3
不管怎样,w
有一个不连续的数据缓冲区,不能用于创建新数组。展平后,数据缓冲区看起来像
xoox|xoox|xoox|...
# x 4 bytes we want to skip
# o 4 bytes we want to use
# | invisible bdry between records in x
我上面构造的y
有:
In [1511]: y.__array_interface__
Out[1511]:
'data': (151950188, False),
'descr': [('', '<f4')],
'shape': (12, 2),
'strides': (16, 4),
'typestr': '<f4',
'version': 3
所以它使用 4 字节偏移量访问 o
字节,然后是 (16,4) 步幅和 (12,2) 形状。
如果我修改您的ndarray
调用以使用原始x.data
,它可以工作:
In [1514]: xx=np.ndarray(shape=(12,2), dtype='<f4', buffer=x.data, offset=4, strides=(16,4))
In [1515]: xx
Out[1515]:
array([[ 2.00000000e+00, -1.00000000e+09],
[ 2.00000000e+00, 4.00000000e+02],
....
[ 3.00000000e+00, 4.44000000e+04]], dtype=float32)
与我的y
具有相同的array_interface:
In [1516]: xx.__array_interface__
Out[1516]:
'data': (151950188, False),
'descr': [('', '<f4')],
'shape': (12, 2),
'strides': (16, 4),
'typestr': '<f4',
'version': 3
【讨论】:
非常感谢您的详细回答!它帮助我弄清楚如何解决我的问题,请参阅更新后的帖子。【参考方案2】:hpaulj 说得对,问题在于结构化数组的子集不连续。有趣的是,我想出了一种使数组子集与以下函数连续的方法:
def view_fields(a, fields):
"""
`a` must be a numpy structured array.
`names` is the collection of field names to keep.
Returns a view of the array `a` (not a copy).
"""
dt = a.dtype
formats = [dt.fields[name][0] for name in fields]
offsets = [dt.fields[name][1] for name in fields]
itemsize = a.dtype.itemsize
newdt = np.dtype(dict(names=fields,
formats=formats,
offsets=offsets,
itemsize=itemsize))
b = a.view(newdt)
return b
In [5]: view_fields(x,['f2','f3']).flags
Out[5]:
C_CONTIGUOUS : True
F_CONTIGUOUS : True
OWNDATA : False
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
旧功能:
In [10]: fields_view(x,['f2','f3']).flags
Out[10]:
C_CONTIGUOUS : False
F_CONTIGUOUS : False
OWNDATA : False
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
【讨论】:
以上是关于将numpy结构化数组子集转换为numpy数组而不复制的主要内容,如果未能解决你的问题,请参考以下文章
将 tf.Tensor 转换为 numpy 数组,然后将其保存为图像而不使用 eager_execution