#machine-learning #entropy-coding

不使用 std constriction

用于研究和生产的熵编码器(Rust和Python)

19个版本

0.3.5 2023年12月14日
0.3.2 2023年11月4日
0.3.1 2023年6月8日
0.3.0 2023年1月28日
0.1.2 2021年5月18日

#31 in 机器学习


用于 编码基准测试

MIT OR Apache-2.0 OR BSL-1.0

775KB
9K SLoC

用于研究和生产的熵编码器

constriction 库提供了一组可组合的熵编码算法,重点关注正确性、多功能性、易用性、压缩性能和计算效率。constriction 的目标有三

  1. 通过提供一组可组合的原始算法(例如,你可以轻松地将范围编码器切换为ANS编码器,而无需找到一个新的库或更改你表示精确可逆熵模型的方式)来促进对新型无损和有损压缩方法的研究;
  2. 通过为Python(用于在研究代码上快速原型设计)和Rust(将成功的原型转换为独立二进制文件、库或WebAssembly模块)提供类似API和二进制兼容的熵编码器来简化从研究代码到部署软件的过渡;
  3. 通过在一个统一的框架内提供各种熵编码原始算法来作为教学资源。查看我们从数据压缩大学课程中获得的额外教学材料(包含一些使用constriction(附带解决方案)的问题集)。

更多信息: 项目网站

实时演示: 这里有一个Web应用程序,它最初是一个Python机器学习研究项目,后来通过使用WebAssembly模块中的constriction将其转换为Web应用程序。

快速入门

将以下内容添加到您的Cargo.toml

[dependencies]
constriction = "0.3.5"
probability = "0.20.3" # Not strictly required but used in many examples.

编码示例

在此示例中,我们将使用量化的高斯分布作为熵模型来编码一些符号。每个符号将由具有不同均值和标准差的高斯分布来模拟(这样示例就不会太简单)。我们将使用probability crate来处理高斯分布,因此请确保您在Cargo.toml中具有以下依赖项

probability = "0.17"

现在,我们来对一些符号进行编码(即压缩)。在这里,我们将使用非对称数字系统(ANS)编码器,因为它速度快、压缩性能好。我们将在下面讨论如何用范围编码器或类似霍夫曼编码的符号编码来替换ANS编码器。

use constriction::stream::{stack::DefaultAnsCoder, model::DefaultLeakyQuantizer};
use probability::distribution::Gaussian;

fn encode_sample_data() -> Vec<u32> {
    // Create an empty ANS Coder with default word and state size:
    let mut coder = DefaultAnsCoder::new();

    // Some made up data and entropy models for demonstration purpose:
    let symbols = [23i32, -15, 78, 43, -69];
    let means = [35.2, -1.7, 30.1, 71.2, -75.1];
    let stds = [10.1, 25.3, 23.8, 35.4, 3.9];

    // Create an adapter that integrates 1-d probability density functions over bins
    // `[n - 0.5, n + 0.5)` for all integers `n` from `-100` to `100` using fixed point
    // arithmetic with default precision, guaranteeing a nonzero probability for each bin:
    let quantizer = DefaultLeakyQuantizer::new(-100..=100);

    // Encode the data (in reverse order, since ANS Coding operates as a stack):
    coder.encode_symbols_reverse(
        symbols.iter().zip(&means).zip(&stds).map(
            |((&sym, &mean), &std)| (sym, quantizer.quantize(Gaussian::new(mean, std)))
    )).unwrap();

    // Retrieve the compressed representation (filling it up to full words with zero bits).
    coder.into_compressed().unwrap()
}

assert_eq!(encode_sample_data(), [0x421C_7EC3, 0x000B_8ED1]);

解码示例

现在,让我们从压缩表示中重建样本数据。

use constriction::stream::{stack::DefaultAnsCoder, model::DefaultLeakyQuantizer, Decode};
use probability::distribution::Gaussian;

fn decode_sample_data(compressed: Vec<u32>) -> Vec<i32> {
    // Create an ANS Coder with default word and state size from the compressed data:
    // (ANS uses the same type for encoding and decoding, which makes the method very flexible
    // and allows interleaving small encoding and decoding chunks, e.g., for bits-back coding.)
    let mut coder = DefaultAnsCoder::from_compressed(compressed).unwrap();

    // Same entropy models and quantizer we used for encoding:
    let means = [35.2, -1.7, 30.1, 71.2, -75.1];
    let stds = [10.1, 25.3, 23.8, 35.4, 3.9];
    let quantizer = DefaultLeakyQuantizer::new(-100..=100);

    // Decode the data:
    coder.decode_symbols(
        means.iter().zip(&stds).map(
            |(&mean, &std)| quantizer.quantize(Gaussian::new(mean, std))
    )).collect::<Result<Vec<_>, _>>().unwrap()
}

assert_eq!(decode_sample_data(vec![0x421C_7EC3, 0x000B_8ED1]), [23, -15, 78, 43, -69]);

练习

尝试上述示例,验证解码是否能够恢复原始数据。然后看看constriction如何使替换ANS编码器为范围编码器变得多么简单,通过以下替换

在编码器中,

  • constriction::stream::stack::DefaultAnsCoder替换为constriction::stream::queue::DefaultRangeEncoder;并且
  • coder.encode_symbols_reverse替换为coder.encode_symbols(由于范围编码器作为队列操作,即先进先出,你不再需要按反向顺序编码符号)。你还需要在文件顶部添加一行use constriction::stream::Encode;,以便将特性行为encode_symbols引入作用域。

在解码器中,

  • constriction::stream::stack::DefaultAnsCoder替换为constriction::stream::queue::DefaultRangeDecoder(注意范围编码器区分编码器和解码器类型,因为编码器写入尾部而解码器从头部读取;相比之下,ANS编码是一个栈,即它从同一位置读取和写入,并允许交错读取和写入)。

注意:你也可以使用像霍夫曼编码这样的符号编码(见模块symbol),但这会显著降低压缩性能,尤其是在大文件中,因为符号编码总是为每个压缩符号发出整数个位,即使符号的信息含量是分数(像ANS和范围编码这样的流编码实际上为每个符号发出分数个位,因为它们在多个符号间分摊)。

上述替换应该会引导你得到类似以下的结果

use constriction::stream::{
    model::DefaultLeakyQuantizer,
    queue::{DefaultRangeEncoder, DefaultRangeDecoder},
    Encode, Decode,
};
use probability::distribution::Gaussian;

fn encode_sample_data() -> Vec<u32> {
    // Create an empty Range Encoder with default word and state size:
    let mut encoder = DefaultRangeEncoder::new();

    // Same made up data, entropy models, and quantizer as in the ANS Coding example above:
    let symbols = [23i32, -15, 78, 43, -69];
    let means = [35.2, -1.7, 30.1, 71.2, -75.1];
    let stds = [10.1, 25.3, 23.8, 35.4, 3.9];
    let quantizer = DefaultLeakyQuantizer::new(-100..=100);

    // Encode the data (this time in normal order, since Range Coding is a queue):
    encoder.encode_symbols(
        symbols.iter().zip(&means).zip(&stds).map(
            |((&sym, &mean), &std)| (sym, quantizer.quantize(Gaussian::new(mean, std)))
    )).unwrap();

    // Retrieve the (sealed up) compressed representation.
    encoder.into_compressed().unwrap()
}

fn decode_sample_data(compressed: Vec<u32>) -> Vec<i32> {
    // Create a Range Decoder with default word and state size from the compressed data:
    let mut decoder = DefaultRangeDecoder::from_compressed(compressed).unwrap();

    // Same entropy models and quantizer we used for encoding:
    let means = [35.2, -1.7, 30.1, 71.2, -75.1];
    let stds = [10.1, 25.3, 23.8, 35.4, 3.9];
    let quantizer = DefaultLeakyQuantizer::new(-100..=100);

    // Decode the data:
    decoder.decode_symbols(
        means.iter().zip(&stds).map(
            |(&mean, &std)| quantizer.quantize(Gaussian::new(mean, std))
    )).collect::<Result<Vec<_>, _>>().unwrap()
}

let compressed = encode_sample_data();

// We'll get a different compressed representation than in the ANS Coding
// example because we're using a different entropy coding algorithm ...
assert_eq!(compressed, [0x1C31EFEB, 0x87B430DA]);

// ... but as long as we decode with the matching algorithm we can still reconstruct the data:
assert_eq!(decode_sample_data(compressed), [23, -15, 78, 43, -69]);

下一步该做什么?

如果你已经有一个熵模型,你只想对一些符号序列进行编码和解码,那么你可能需要根据你的需求调整上述示例。或者更仔细地看看stream模块。

更多示例和教程链接到项目网站

如果你对熵编码的概念还不太熟悉,请查看教学材料

贡献

欢迎提交拉取请求和问题报告。除非贡献者在贡献时明确说明,否则所有贡献都将默认许可在以下三者之一下:MIT 许可证、Apache 许可证第 2 版或 Boost 软件许可证第 1.0 版,由每个许可证持有者自行选择。

没有官方的贡献指南,因为没有人会阅读那些。只需友好对待他人,表现得像个成年人(即,只要您努力改进并且愿意尊重地考虑他人的意见,犯错是可以的)。

许可证

本作品根据 MIT 许可证、Apache 许可证第 2 版或 Boost 软件许可证第 1.0 条的条款进行许可。如果您使用此作品,可以选择其中之一。请参阅此目录中以 LICENSE 开头的文件。编译后的 Python 扩展模块与多个第三方库链接。constriction Python 扩展模块的二进制发行版包含一个名为 LICENSE.html 的文件,其中包含所有依赖项的许可证(该文件也可在 网上 获取)。

这个名字是什么意思?

Constriction 是一个压缩原语库,为 Rust 和 Python 提供绑定。 Pythons 是一种无毒蛇类,通过“压缩”猎物来制服猎物,这种方法称为 constriction

依赖项

~2.2–9.5MB
~66K SLoC