rANS:快速的渐进最优码

参考文献:

  1. [Huff52] D. A. Huffman, “A Method for the Construction of Minimum-Redundancy Codes,” in Proceedings of the IRE, vol. 40, no. 9, pp. 1098-1101, Sept. 1952.
  2. [Duda09] Jarek Duda. Asymmetric numeral systems. CoRR abs/0902.0271 (2009).
  3. [Duda13] Jarek Duda. Asymmetric numeral systems: entropy coding combining speed of huffman coding with compression rate of arithmetic coding, 2013. ArXiv preprint, available at https://arxiv.org/abs/1311.2540.
  4. 熵编码算法ANS - 知乎
  5. Lossless Compression with Asymmetric Numeral Systems

为了压缩已知概率密度的符号序列,有两种主流方法:

  1. 哈夫曼编码:使用二的幂次近似密度(二叉树),速度快,但压缩率低(除非信源密度恰好都是二的幂次);
  2. 算术/范围编码:使用精确密度,压缩率很容易达到香农熵的界,但是速度慢。

[Duda09] 提出了 Asymmetric numeral systems(ANS),它是渐进最优码(平均码长等于香农熵),并且速度比哈夫曼编码更快。

ANS

ANS 的抽象思路是:令 [ n ] [n] [n] 是大小为 n n n 的符号表, q s , s ∈ [ n ] q_s, s \in [n] qs,s[n] 是其密度函数。对于一个充分大的整数 x ∈ N x \in \N xN将其视为区间 x = { 0 , 1 , ⋯   , x − 1 } ⊆ N x=\{0,1,\cdots,x-1\} \subseteq \N x={0,1,,x1}N,根据符号密度 q s q_s qs 划分为 n n n 个大小分别约为 q s x q_sx qsx 的子集 S s ⊆ x S_s \subseteq x Ssx,满足 x = ∪ s S s x = \cup_s S_s x=sSs,那么,在 x x x 中均匀采样,就等价于首先根据 q s q_s qs 采样出 s s s,然后在 S s S_s Ss 中均匀采样。所以, x    ⟺    ( s , S s ) x \iff (s,S_s) x(s,Ss) 存在双射,可以利用它来编码符号 s s s 的序列。

函数 D 1 : N → [ n ] ,   x ↦ s D_1: \N \to [n],\, x \mapsto s D1:N[n],xs 称为 distributing function,它给出了集合 x x x 的划分,
S s : = { y ∈ x ∣ D 1 ( y ) = s } S_s := \{y\in x \mid D_1(y)=s\} Ss:={yxD1(y)=s}
然后定义 x s : = ∣ S s ∣ x_s := |S_s| xs:=Ss 以及 D 2 ( x ) : = x D 1 ( x ) D_2(x) := x_{D_1(x)} D2(x):=xD1(x)

那么,解码函数定义为 x ∈ N x \in \N xN D 1 D_1 D1 所标记为的符号 s ∈ [ n ] s \in [n] s[n],并相应调整状态到 x s x_s xs
D ( x ) : = ( D 1 ( x ) , D 2 ( x ) ) = ( s , x s ) D(x) := (D_1(x), D_2(x)) = (s,x_s) D(x):=(D1(x),D2(x))=(s,xs)
它是双射(根据 D 1 D_1 D1 从低到高枚举出 x s x_s xs 个符号 s s s 对应的那个 x x x),编码函数是它的逆,
C ( s , x s ) : = x ,     s.t.     ( D 1 ( x ) = s ) ∧ ( ∣ S s ∣ = x s ) C(s,x_s) := x,\,\,\, \text{s.t.}\,\,\, (D_1(x)=s) \wedge (|S_s|=x_s) C(s,xs):=x,s.t.(D1(x)=s)(Ss=xs)
要求找到合适的函数 D 1 D_1 D1使得总有 x s ≈ q s x x_s \approx q_s x xsqsx,那么就有 log ⁡ C ( s , x s ) − log ⁡ x s ≈ H 2 ( q s ) \log C(s,x_s) - \log x_s \approx H_2(q_s) logC(s,xs)logxsH2(qs),即实现了渐进最优性。

Range variants

[Duda09] 首先提出了一种 Uniform variants(uANS),函数 D 1 D_1 D1 将符号 s s s 几乎均匀地铺在 N \N N 上,其压缩率很好,但是计算相对较慢。下图展示了 n = 2 , q 0 = 0.7 , q 1 = 0.3 n=2, q_0=0.7, q_1=0.3 n=2,q0=0.7,q1=0.3 的情况,

在这里插入图片描述

之后,[Duda13] 提出了 rANS,函数 D 1 D_1 D1 将符号 s s s 放置在一个区间内,其概率近似不如 uANS 精确,但是只需要更少的计算开销。

假设 ( g 0 , g 1 , ⋯   , g n − 1 ) ∈ N n (g_0,g_1,\cdots,g_{n-1}) \in \N^n (g0,g1,,gn1)Nn 是符号密度 q s q_s qs 的良好近似(缩放 B : = ∑ s g s B:= \sum_s g_s B:=sgs 倍),将数组 { 0 , 1 , ⋯   , B − 1 } \{0,1,\cdots,B-1\} {0,1,,B1} 依次划分为大小为 g s g_s gs 的区间。定义 CDF ( s ) : = ∑ i = 0 s − 1 g i \text{CDF}(s) := \sum_{i=0}^{s-1} g_i CDF(s):=i=0s1gi 是累积密度,初值 CDF ( 0 ) = 0 \text{CDF}(0)=0 CDF(0)=0,那么范围 [ CDF ( s ) , CDF ( s + 1 ) ) [\text{CDF}(s),\text{CDF}(s+1)) [CDF(s),CDF(s+1)) 被解码为 s s s

定义函数 symbol : Z → [ n ] \text{symbol}: \Z \to [n] symbol:Z[n]
symbol ( x ) = s    ⟺    CDF ( s ) ≤ [ x ] B < CDF ( s + 1 ) \text{symbol}(x) = s \iff \text{CDF}(s) \le [x]_B < \text{CDF}(s+1) symbol(x)=sCDF(s)[x]B<CDF(s+1)
编码函数设为:
C ( s , x ) : = ⌊ x g s ⌋ ⋅ B + [ x ] g s + CDF ( s ) ∈ N C(s,x) := \left\lfloor\frac{x}{g_s}\right\rfloor \cdot B + [x]_{g_s} + \text{CDF}(s) \in \N C(s,x):=gsxB+[x]gs+CDF(s)N
解码函数设为:
D ( x ) : = ( symbol ( x ) ,   ⌊ x B ⌋ ⋅ g s + [ x ] B − CDF ( s ) ) ∈ [ n ] × N D(x) := \left(\text{symbol}(x),\, \left\lfloor\frac{x}{B}\right\rfloor\cdot g_s + [x]_B - \text{CDF}(s)\right) \in [n] \times \N D(x):=(symbol(x),Bxgs+[x]BCDF(s))[n]×N
我在编程时注意到,如果初始 x = 0 x=0 x=0 并且序列开头有多个 s = 0 s=0 s=0,那么总有 C ( s , x ) = 0 C(s,x)=0 C(s,x)=0,因此解码时会出现 Bug;对于其他的 x x x 值,是否存在类似问题?似乎设置 ∑ s g s ≤ B − 1 \sum_s g_s \le B-1 sgsB1 以及 C D F ( 0 ) = 1 CDF(0)=1 CDF(0)=1 就可以解决该问题(但是这对么?)

假如选取 B = 2 t B = 2^t B=2t,设置 g s ≈ q s B g_s \approx q_sB gsqsB,那么上述的 x ⋅ B ,   x / B ,   [ x ] B x\cdot B,\, x/B,\, [x]_B xB,x/B,[x]B 都可以用移位掩码来快速实现。 然而 x / g s ,   x ⋅ g s ,   x + [ x ] g s x/g_s,\, x\cdot g_s,\, x+[x]_{g_s} x/gs,xgs,x+[x]gs 等运算,必须在大整数 x x x 上处理,速度会相对较慢。

rANS 也是渐进最优码,满足:
∑ s ∈ [ n ] q s log ⁡ ( C ( s , x ) ) ≤ log ⁡ x + ∑ s ∈ [ n ] q s log ⁡ g s B + B x \sum_{s \in [n]} q_s \log(C(s,x)) \le \log x + \sum_{s \in [n]} q_s \log\frac{g_s}{B} + \frac{B}{x} s[n]qslog(C(s,x))logx+s[n]qslogBgs+xB
只要 x x x 远大于 B B B,并且 g s / B g_s/B gs/B q s q_s qs 的良好近似,那么平均码长就是香农熵。

Stream Encoding

对于无限符号序列,状态 x ∈ N x \in \N xN 的比特长度也将无限增长,这使得计算效率降低。 [Duda09] 提出了流式编码,将状态限制在一个固定的区间内,从而提高计算效率,但是压缩率会略微变差。

我们将状态 x x x 限制在范围 I : = { l , l + 1 , ⋯   , l b − 1 } I := \{l,l+1,\cdots,lb-1\} I:={l,l+1,,lb1},易知 ∣ I ∣ = ( b − 1 ) l |I| = (b-1)l I=(b1)l

在这里插入图片描述

利用上述三条规则,

  • 在编码时,
    1. 设置初值 x = 0 x = 0 x=0
    2. 根据 C ( s , x ) C(s,x) C(s,x) 编码,直到即将 x ≥ l b x \ge lb xlb(可以用 new_x = C(s,x) 测试,然后赋值 x = new_x
    3. [ x ] b [x]_b [x]b 输出,并更新 x ← ⌊ x / b ⌋ x \gets \lfloor x/b\rfloor xx/b,此时 x < l x < l x<l,然后回到 Step 2,获得 x ∈ I x \in I xI
    4. 在最后,将 x ∈ I x \in I xI 输出
  • 在解码时,
    1. 读取最近的状态 x ∈ I x \in I xI
    2. 根据 D ( x ) D(x) D(x) 解码,直到 x < l x < l x<l(对应编码时跨越 I I I 的上边界 b l bl bl
    3. 读取大小为 b b b 的数据块 d d d,更新 x ← x b + d x \gets xb+d xxb+d,此时 x ∈ I x \in I xI,然后回到 Step 2
    4. 当输入流为空,就继续解码直到 x = 0 x=0 x=0(回到初值),终止程序,并翻转解码结果

为了达到渐进最优性,需要让 l l l 远大于 B B B。为了提高计算效率,可以将 l , b l,b l,b 也都设置为二的幂次,尤其是 w ∣ log ⁡ b w \mid \log b wlogb(这里 w = 8 / 32 w=8/32 w=8/32,取决于大数 x ∈ N x \in \N xN 的基本运算单元)

代码实现

首先要自行实现一个大数类 INT,我这里使用数组 uint32_t *data 构造,

const int32_t ANS_N = 32;                                      // sum_s g(s) < 2^{ANS_N}
const int ANS_STATE_MINLEN = 2;                                // l = 2^{ANS_N * ANS_STATE_MINLEN}
const int ANS_STORE_LEN = 1;                                   // b = 2^{ANS_N * ANS_STORE_LEN}
const int ANS_STATE_MAXLEN = ANS_STATE_MINLEN + ANS_STORE_LEN; // I = [l, bl-1]

struct INT // 基底 2^32 的无符号大整数
{
    uint32_t *data = nullptr;
    int32_t len = 0;

    INT(int len); // 构造函数

    INT(const INT &x); // 深度复制

    ~INT(); // 析构函数

    int size(); // 当前大小(字节)

    void resize(int len); // 重新申请内存

    void clear(); // 清零

    void add(uint32_t x); // 加法

    void sub(uint32_t x); // 减法

    void mul(uint32_t x); // 乘法

    uint32_t div(uint32_t x); // 下取整除法,返回余数

    void shift(int i); // 按 word 左移(乘以 2^32),支持负数
};

主要的 rANS 实现如下:

void ANS_Build_G(uint32_t *G, const double *freq, const int B)
{
    for (int s = 0; s < B; s++)
        G[s] = 0xffffffffUL * freq[s]; // Scale and round down
}

void ANS_Build_CDF(uint32_t *CDF, const uint32_t *G, const int B)
{
    CDF[0] = 1; // Handle the case where the first symbol is s = 0. Otherwise, there will be a bug.
    for (int s = 1; s < B; s++)
        CDF[s] = CDF[s - 1] + G[s - 1]; // CDF[s] = Pr[x < s]
}

uint32_t ANS_Symbol(uint32_t x, const uint32_t *CDF, const int B)
{
    int64_t L = 0, R = B;

    while (true) // Use binary search to look up the table, and find M such that CDF[M] <= x < CDF[M + 1]
    {
        if (L >= B - 1)
            return B - 1;
        if (R <= 0)
            return 0;

        int64_t M = (L + R) / 2;
        if (x < CDF[M])
            R = M;
        else if (x >= CDF[M + 1])
            L = M + 1;
        else
            return M;
    }
}

void ANS_C(INT &x, const uint32_t s, const uint32_t *G, const uint32_t *CDF, const int B)
{
    uint32_t gs = G[s];
    uint32_t rem = x.div(gs);

    x.shift(1);
    x.add(rem);
    x.add(CDF[s]);
}

uint32_t ANS_Cinv(INT &y, const uint32_t *G, const uint32_t *CDF, const int B)
{
    uint32_t lsb = y.data[0];
    uint32_t t = ANS_Symbol(lsb, CDF, B);

    y.shift(-1);
    y.mul(G[t]);
    y.add(lsb);
    y.sub(CDF[t]);

    return t;
}

void ANS_Encode(vector<uint32_t> &x, const uint32_t *s, const int slen, const uint32_t *G, const uint32_t *CDF, const int B)
{
    x.clear();
    INT state(ANS_STATE_MAXLEN + 1); // Set the initial value to 0

    for (int i = 0; i < slen; i++)
    {
        INT new_state(state);
        ANS_C(new_state, s[i], G, CDF, B);

        if (new_state.size() > ANS_STATE_MAXLEN) // About to exceed the range I
        {
            for (int i = 0; i < ANS_STORE_LEN; i++)
                x.push_back(state.data[i]); // Store the lowest ANS_STORE_LEN blocks

            state.shift(-ANS_STORE_LEN);   // Make it less than l
            ANS_C(state, s[i], G, CDF, B); // Now belonging to range I
        }
        else
        {
            state.len = state.len;
            memcpy(state.data, new_state.data, new_state.size() * sizeof(uint32_t));
        }
    }

    // Output the remaining state values, with the length between ANS_STATE_MINLEN + 1 and ANS_STATE_MAXLEN
    int l = state.size();
    for (int i = 0; i < l; i++)
        x.push_back(state.data[i]);
}

void ANS_Decode(uint32_t **s, int &slen, const vector<uint32_t> &x, const uint32_t *G, const uint32_t *CDF, const int B)
{
    vector<uint32_t> res;
    int xlen = x.size();
    int num = (xlen - ANS_STATE_MINLEN - 1) / ANS_STORE_LEN;
    int l = xlen - ANS_STORE_LEN * num;
    xlen -= l;

    INT state(ANS_STATE_MAXLEN + 1);
    for (int i = 0; i < l; i++)
        state.data[i] = x[xlen + i]; // Retrieve the latest state

    while (xlen > 0)
    {
        while (state.size() > ANS_STATE_MINLEN) // Check if it still belongs to range I
            res.push_back(ANS_Cinv(state, G, CDF, B));

        state.shift(ANS_STORE_LEN); // Otherwise, the lower bits need to be fetched.
        for (int i = 0; i < ANS_STORE_LEN; i++)
            state.data[i] = x[xlen - ANS_STORE_LEN + i];

        xlen -= ANS_STORE_LEN;
    }

    while (state.size() != 0) // Whether to return to the initial state
        res.push_back(ANS_Cinv(state, G, CDF, B));

    if (*s != nullptr)
        free(*s);

    slen = res.size();
    *s = (uint32_t *)malloc(slen * sizeof(uint32_t));
    for (int i = 0; i < slen; i++)
        (*s)[i] = res[slen - i - 1];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值