如何使用自定义编解码器将numpy数组保存为字节?



我有一个类型为int8,形状为(100,100)的numpy数组。我使用霍夫曼编码来找到一种最有效的方法来编码其内容。对于我的特殊用例,我试图保存的值大致是正态分布在0左右,标准差约为5,但这个问题适用于以任何分布最佳地保存ndarray。在我的例子中,在极少数情况下可以观察到高达-20或20的极端值。显然,霍夫曼编码这种类型的数组比使用标准的8位整数更节省空间。我的问题是如何做到这一点。

我尝试使用np.save()和使用Pickle,但未能获得我想要的效率。具体来说,对于形状为(100,100)的数组,我使用np.save()获得大小为10,128字节的文件,每个整数加上开销为1字节是有意义的。使用pickle,我得到一个大小为10,158字节的文件,大致相同。然而,基于我的霍夫曼编码方案,我应该能够在我的特定测试用例(如下所示)中编码144字节的数组内容!!(不包括开销)

我尝试将数组中的每个整数映射到其最佳字节字符串,因此我有一个字节数组(类型S12),然后保存该数组,但我使用np.save()和pickle获得118kb的文件,因此显然不起作用。

谢谢你的帮助!

复制我的确切测试用例的代码:

import pickle
import numpy as np
# Seed random number generator
np.random.seed(1234)
# Build random normal array and round it 
test_array = np.random.normal(0, 5, size=(100, 100))
test_array = np.round(test_array).astype(np.int8)
# Set encoding dictionary
encoding_dict = {6: b'0000',
-8: b'00010',
8: b'00011',
5: b'0010',
-5: b'0011',
12: b'0100000',
-13: b'01000010',
14: b'010000110',
-15: b'0100001110',
-14: b'0100001111',
10: b'010001',
7: b'01001',
-4: b'0101',
4: b'0110',
-7: b'01110',
11: b'0111100',
-11: b'0111101',
-12: b'01111100',
13: b'011111010',
-19: b'011111011000',
-18: b'011111011001',
-16: b'01111101101',
-17: b'011111011100',
16: b'011111011101',
15: b'01111101111',
-10: b'0111111',
-3: b'1000',
-6: b'10010',
-9: b'100110',
9: b'100111',
3: b'1010',
-2: b'1011',
1: b'1100',
2: b'1101',
-1: b'1110',
0: b'1111'}
# Save using different methods
np.save('test.npy', test_array)
with open('test.pkl', 'wb') as file:
pickle.dump(test_array, file)
# Try converting to bytes and then saving
bytes_array = np.array([encoding_dict[key] for key in test_array.flatten()]).reshape(test_array.shape)
np.save('test_bytes.npy', bytes_array)
with open('test_bytes.pkl', 'wb') as file:
pickle.dump(bytes_array, file)
# See how many bytes it should take
tmp_flat = test_array.flatten()
tmp_bytes = np.zeros_like(tmp_flat)
for i in range(len(tmp_bytes)):
tmp_bytes[i] = len(encoding_dict[tmp_flat[i]]) / 8
print(tmp_bytes.sum())

你不可能在你描述的数据上得到70倍的压缩。你为什么这么想?

即使输入字节被限制为4个值,您可以获得的最佳压缩倍数是4。(8位/2位)你有一个正态分布,在±1西格玛范围内有10或11个值。

也许您可以使用统计数据获得随机字节的两倍压缩系数。在美好的一天。

更新:

只是计算了分布的熵,假设标准差为5。每个样本的熵是4.37比特。所以我对因子2的估计过于乐观了。更像是1.8倍。

顺便说一下,你不需要手工做这个。您可以将zlib与Z_HUFFMAN_ONLY策略一起使用。它将为您生成最佳的霍夫曼代码。

您的错误在这里:

tmp_bytes = np.zeros_like(tmp_flat)

tmp_flat是一个int8数组,因此语句tmp_bytes[i] = len(encoding_dict[tmp_flat[i]]) / 8在将float值转换为int值时截断了大量数字。用以下内容替换不合适的行:

tmp_bytes = np.zeros(tmp_flat.shape, np.single)

但是要演示如何实际执行压缩:我建议使用np.packbits,它实际上会为您创建一个5493字节的数组。

# Make a string of all the data
s = b''.join(encoding_dict[key] for key in test_array.ravel())
# Convert the string into an array
a = np.array(list(map(int, s.decode('ascii'))))
# pack it
result = np.packbits(a)

语句a = ...正在做大量额外的工作,因为它解码数据,然后复制它,然后无数次将字符串转换为整数,等等。下面是一个更长的,但更有效的方法:

s = bytearray(b'').join(encoding_dict[key] for key in test_array.ravel())
a = np.array(s)
a -= ord('0')    # A now contains just 0 and 1
result = np.packbits(a)

当您存储该数组时,请确保包含您期望的位数,而不是字节数。您可以使用np.unpackbits将包解压缩为二进制字符串,它支持专门用于此目的的count参数(顺便说一下,我添加的)。

最后一点,尽可能使用ravel代替flatten。后者总是做一个副本,而前者通常不做。

我没有使用过这种编码,但我怀疑你的144字节是否是一个准确的度量。

您的bytes_array是100个元素,每个元素12字节('S12'),或test_array大小的12倍(每个元素1字节)。

如果我们制作一个列表:

In [440]: alist = [encoding_dict[key] for key in test_array.flatten()]
In [441]: len(alist)
Out[441]: 10000
In [442]: alist[:10]
Out[442]: 
[b'1101',
b'10010',
b'01001',
b'1011',
b'0101',
b'0110',
b'0110',
b'1000',
b'1111',
b'0111101']

查找这些字符串的长度:

In [444]: sum([len(i) for i in alist])
Out[444]: 43938

每个元素平均4个字节。即使我们可以将这些字节转换成比特,也只有50%的压缩:

In [445]: _/8
Out[445]: 5492.25

最新更新