前两篇把不确定性变成了信息量。这一篇继续追问:这些可能带小数的 bit,怎样变成能够完整解码的二进制文件?
我们依次看码字如何分配空间、熵为什么限制平均码长、怎样编码整条消息,以及长序列为什么能接近这个界。全文使用以 2 为底的对数;除特别说明外,符号表有限、概率模型已知,并忽略单独传输模型的成本。
前缀码:每个码字占据多少空间?
给符号分配二进制码字,还必须说明连续拼接后如何切分。前缀码要求任何码字都不是另一个码字的前缀。例如 A=0、B=10、C=110、D=111,读到一个完整码字就可以输出一个符号,不需要分隔符。
把所有无限二进制串想成一棵不断分叉的树。前缀 0 占其中一半;10 占四分之一;每多确定一位,所占空间再减半。因此长度 li 的码字占据比例 2−li。前缀码的这些区域互不重叠,总占用不能超过 1:
K=i∑2−li≤1
这就是 Kraft 不等式。对给定的整数码长,这个条件也足以保证存在一个对应的二进制前缀码。注意,它保证的是“能构造出来”,不是“随便挑一组同样长度的码字都不会冲突”。
正在呈现知识画面
码长决定占用,Kraft 限制总容量比较有余量、刚好占满和超出容量的三组码长。每个块的宽度严格等于 2 的负码长次方,虚线标出总容量 1;合法状态下同时给出一组实际前缀码。
{"id":"source-kraft","title":"码长决定占用,Kraft 限制总容量","summary":"比较有余量、刚好占满和超出容量的三组码长。每个块的宽度严格等于 2 的负码长次方,虚线标出总容量 1;合法状态下同时给出一组实际前缀码。","height":850,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div class=\"presets\" role=\"group\" data-i18n-label=\"control\"><button data-case=\"0\" data-i18n=\"slack\"></button><button data-case=\"1\" data-i18n=\"full\"></button><button data-case=\"2\" data-i18n=\"over\"></button></div>\n<div class=\"allocation\" role=\"img\" data-i18n-label=\"chart\"><div class=\"capacity\"></div><div id=\"blocks\"></div><div class=\"limit\"><span>1</span></div></div>\n<div class=\"scale\"><span>0</span><span>1.5</span></div>\n<div class=\"lengths\" id=\"lengths\"></div>\n<div class=\"formula\">K = ∑ 2<sup>−lᵢ</sup> = <strong id=\"sum\"></strong> <span id=\"comparison\"></span> 1</div>\n<div class=\"copy-stack\"><p id=\"explanation\" aria-live=\"polite\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"slackExplain\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"fullExplain\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"overExplain\"></p></div>\n<p class=\"note\" data-i18n=\"note\"></p>","css":".intro { margin:8px 0 18px; color:var(--muted); font-size:14px; }\n.control { display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:18px; }\n.control output { min-width:4em; flex-shrink:0; text-align:center; background:var(--accent-soft); color:var(--accent); padding:2px 8px; border-radius:6px; font-weight:600; }\n.formula { margin-top:18px; padding:14px 0; border-block:1px solid var(--rule); font-size:16px; font-variant-numeric:tabular-nums; overflow-wrap:anywhere; }\n.copy-stack { display:grid; margin-top:14px; color:var(--muted); font-size:14px; }\n.copy-stack > * { grid-area:1/1; }\n.copy-stack .reserve { visibility:hidden; }\n.note { margin-top:14px; color:var(--muted); font-size:12px; }\n.metric-pair { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-top:18px; }\n.metric-pair > div { min-width:0; }\n.metric-pair span { display:block; font-size:12px; color:var(--muted); }\n.metric-pair strong { display:block; font-size:24px; font-weight:500; font-variant-numeric:tabular-nums; }\n.scale { display:flex; justify-content:space-between; color:var(--muted); font-size:12px; font-variant-numeric:tabular-nums; }\n@media(max-width:480px) { .metric-pair strong { font-size:21px; } }\n.presets { display:flex; }\n.allocation { position:relative; height:82px; margin-top:28px; }\n.capacity { position:absolute; left:0; top:12px; bottom:0; width:66.666667%; border:1px solid var(--rule); background:var(--surface); }\n.limit { position:absolute; left:66.666667%; top:0; bottom:0; border-left:2px dashed var(--ink); z-index:2; }\n.limit span { position:absolute; top:-22px; transform:translateX(-50%); font-size:12px; }\n#blocks { position:absolute; inset:24px 0 12px; }\n.block { position:absolute; top:0; height:100%; display:grid; place-items:center; background:var(--accent); color:var(--paper); border-right:2px solid var(--paper); transition:width .25s,left .25s; font-size:13px; }\n.block:nth-child(even) { background:var(--second); }\n.allocation[data-over='true'] .block:nth-child(n+3) { background:var(--ink); background-image:repeating-linear-gradient(135deg,transparent 0 5px,#ffffff30 5px 7px); }\n.lengths { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:8px; margin-top:18px; font-size:13px; font-variant-numeric:tabular-nums; }\n.lengths strong,.lengths span { display:block; }\n.lengths strong { font-size:15px; }.lengths span { color:var(--muted); }\n@media(max-width:480px) { .lengths { grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; } }\n","js":"const cases=[[2,2,3,3],[1,2,3,3],[1,1,2,2]], symbols=['A','B','C','D'];\nconst blocks=document.querySelector('#blocks'), lengths=document.querySelector('#lengths');\nfor(const s of symbols){const b=document.createElement('div');b.className='block';b.textContent=s;blocks.append(b);const row=document.createElement('div');row.innerHTML='<strong></strong><span></span><span></span>';lengths.append(row);}\nfunction draw(index){\n const lens=cases[index];let offset=0;\n lens.forEach((l,i)=>{const w=2**(-l),b=blocks.children[i],row=lengths.children[i];b.style.left=(offset/1.5*100)+'%';b.style.width=(w/1.5*100)+'%';row.children[0].textContent=symbols[i]+' · l = '+l;row.children[1].textContent='2⁻'+String(l).replace(/2/g,'²').replace(/3/g,'³').replace(/1/g,'¹')+' = '+viz.number(w,3);row.children[2].textContent=index===2?'—':(offset*2**l).toString(2).padStart(l,'0');offset+=w;});\n document.querySelector('#sum').textContent=viz.number(offset,2);document.querySelector('#comparison').textContent=offset<1?'<':offset===1?'=':'>';\n document.querySelector('.allocation').dataset.over=String(offset>1);\n document.querySelector('#explanation').textContent=viz.t(['slackExplain','fullExplain','overExplain'][index]);\n document.querySelectorAll('[data-case]').forEach(b=>b.setAttribute('aria-pressed',String(+b.dataset.case===index)));\n}\ndocument.querySelectorAll('[data-case]').forEach(b=>b.addEventListener('click',()=>draw(+b.dataset.case)));draw(1);","audio":false,"strings":{"chart":"四个码字的空间占用,完整容量为 1,横轴延伸到 1.5 以展示超出的部分。","control":"码长组合","eyebrow":"码长 → 空间 → 可构造性","full":"刚好占满","fullExplain":"总占用恰好为 1:A 占一半,B 占四分之一,C、D 各占八分之一。四个区域互不重叠,对应下方码字 0、10、110、111。","heading":"为什么求和的是 2⁻ˡ,而不是码长本身?","intro":"每多一位,占用减半。比较三组长度,看它们能否一起放进容量为 1 的码空间。","note":"每格依次显示符号与码长、占用比例、示例码字。条带按长度排序并对齐二进制网格;超出部分只展示容量矛盾,不是有效码字。","over":"超出容量","overExplain":"总占用 1.5,越过容量边界。A、B 的两个一位码已经用完全部空间,C、D 无处安放;不存在满足这组长度的前缀码。","slack":"有余量","slackExplain":"总占用 0.75,还剩 0.25。下面给出一组实际码字;此时缩短 A 一位,就能用掉余量,无需加长其他码字。"}}
图中先按码长从短到长摆放。长度为 l 的码字也可对应区间 [k/2l,(k+1)/2l);对齐二进制网格并保持不重叠,就能读出码字。总占用小于 1 时还有余量,所以码长不是无条件的零和关系。只要剩余容量足够,缩短某个码字就不必改动其他码字;余量不足时才需要腾出空间。
熵为什么是平均码长的下界?
对一个二进制唯一可解码的符号码,令 L=∑xp(x)lx。Kraft–McMillan 不等式仍给出 K≤1。我们可以把每个码字的空间占用归一化,得到一个分布:
q(x)=K2−lx
于是 −log2q(x)=lx+log2K。用上一章的交叉熵与 KL 分解,可把原来抽象的下界拆成两个明确的非负项:
L−H(p)=DKL(p∥q)−log2K≥0
第一项是概率 p 与码空间分配 q 的失配,第二项来自未用满的码空间,因为 K≤1,所以 −log2K≥0。这就解释了为什么不能在平均意义上压到熵以下。这里的码必须满足唯一可解码条件;不能忽略外部提供的长度、边界等信息,把任意一对一文件映射都直接套进这个结论。
对正概率符号取 lx=⌈−log2p(x)⌉ 可以构造 Shannon 前缀码,得到:
H(X)≤L<H(X)+1
对独立同分布的 n 个符号一起编码,由 H(Xn)=nH(X) 得到:
H(X)≤nLn<H(X)+n1
每一块的码长仍是整数,变小的是摊到每个符号上的取整开销。 这个平均长度结论直接由块编码得到,不需要先借助典型集。
公平骰子:2.585 bit,为什么平均要问 2.667 次?
任意是非提问策略都可表示为二叉决策树,根到叶的路径就是前缀码。公平骰子的熵为 log26≈2.585 bit。一个最优树有两个深度为 2 的叶子、四个深度为 3 的叶子,因此:
L∗=62×2+4×3=38≈2.667.
最坏情况仍要 3 次。熵、平均提问次数与最坏次数各有不同含义;把多个独立结果分块编码,才可以摊薄整数码长的开销。
Huffman:保留树,就能看清码字如何产生
Huffman 编码反复合并当前概率最小的两个节点,直到形成根。两条分支分别标 0、1,从根到叶的路径就是码字。它在给定符号概率下,得到平均长度最优的二进制前缀码;概率相同的节点可能有不同但同样最优的排列。
沿分支读取即可得到码字:A=0、B=10、C=110、D=111。叶子越浅,码字越短。
这张树使用 p=(0.5,0.25,0.125,0.125),所以:
L=0.5×1+0.25×2+0.125×3+0.125×3=1.75 bit
所有正概率恰好都是 2−l 的形式,码长等于自信息,因此 L=H=1.75 bit。
一般情况下,Huffman 满足的是平均冗余 0≤L−H<1 bit/符号;这不是每个符号的码长与自信息之差都小于 1 的逐项保证。若只有两种结果、概率为 (0.9,0.1),逐符号 Huffman 仍给两者各 1 bit,平均长度为 1;而熵只有约 0.469 bit。
把多个符号作为一个新符号做 Huffman 可以缩小差距,但码表会随块长快速增大。算术编码用另一种表示方法处理整条消息。
算术编码:概率连乘,信息量相加
从区间 [0,1) 开始,按模型给出的概率分成若干子区间。每读一个符号,就保留它对应的一段,然后在这一段内部继续划分。若当前区间是 [a,b),符号的累积概率范围是 [F,F+q),更新为:
[a+(b−a)F, a+(b−a)(F+q))
关键是宽度每次乘上该符号的模型概率。用条件概率模型编码一条消息时:
Wn=i=1∏nq(xi∣x<i)
−log2Wn=i=1∑n−log2q(xi∣x<i)
下面固定消息为 ABA,只改变模型 q(A)=p,其中 q(B)=1−p,各位置使用同一分布。三个阶段同时保留,并共用 [0,1) 标尺,便于直接看见宽度的乘法。
正在呈现知识画面
一条消息如何变成越来越窄的区间固定消息 ABA,只改变 A 的模型概率。三个阶段共用 0 到 1 的标尺:区间宽度连乘,负对数相加。最后在消息区间内选一个完整二进制小区间,得到实际的整数比特串。
{"id":"source-arithmetic","title":"一条消息如何变成越来越窄的区间","summary":"固定消息 ABA,只改变 A 的模型概率。三个阶段共用 0 到 1 的标尺:区间宽度连乘,负对数相加。最后在消息区间内选一个完整二进制小区间,得到实际的整数比特串。","height":850,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<label class=\"control\" for=\"probability\"><span data-i18n=\"control\"></span><output id=\"probability-value\"></output></label><input id=\"probability\" type=\"range\" min=\"25\" max=\"75\" value=\"60\" step=\"1\">\n<p class=\"note\" data-i18n=\"axis\"></p><div class=\"intervals\" id=\"intervals\"></div>\n<div class=\"scale\"><span>0</span><span>1</span></div>\n<div class=\"formula\"><div id=\"product\"></div><div id=\"information\"></div></div>\n<div class=\"code-result\"><p data-i18n=\"binary\"></p><strong id=\"code\"></strong><p id=\"binary-range\"></p><div class=\"binary-track\"><div id=\"final-range\"></div><div id=\"binary-cell\"></div></div></div>\n<div class=\"metric-pair\"><div><span data-i18n=\"ideal\"></span><strong id=\"ideal\"></strong></div><div><span data-i18n=\"actual\"></span><strong id=\"actual\"></strong></div></div>\n<p class=\"note\" data-i18n=\"note\"></p>","css":".intro { margin:8px 0 18px; color:var(--muted); font-size:14px; }\n.control { display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:18px; }\n.control output { min-width:4em; flex-shrink:0; text-align:center; background:var(--accent-soft); color:var(--accent); padding:2px 8px; border-radius:6px; font-weight:600; }\n.formula { margin-top:18px; padding:14px 0; border-block:1px solid var(--rule); font-size:16px; font-variant-numeric:tabular-nums; overflow-wrap:anywhere; }\n.copy-stack { display:grid; margin-top:14px; color:var(--muted); font-size:14px; }\n.copy-stack > * { grid-area:1/1; }\n.copy-stack .reserve { visibility:hidden; }\n.note { margin-top:14px; color:var(--muted); font-size:12px; }\n.metric-pair { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-top:18px; }\n.metric-pair > div { min-width:0; }\n.metric-pair span { display:block; font-size:12px; color:var(--muted); }\n.metric-pair strong { display:block; font-size:24px; font-weight:500; font-variant-numeric:tabular-nums; }\n.scale { display:flex; justify-content:space-between; color:var(--muted); font-size:12px; font-variant-numeric:tabular-nums; }\n@media(max-width:480px) { .metric-pair strong { font-size:21px; } }\n.intervals { display:grid; gap:14px; margin-top:16px; }\n.interval-label { display:flex; justify-content:space-between; align-items:baseline; gap:8px; font-size:13px; font-variant-numeric:tabular-nums; }\n.interval-label span { color:var(--muted); }\n.interval-track,.binary-track { position:relative; height:30px; background:var(--surface); margin-top:5px; }\n.parent-range,.selected-range { position:absolute; height:100%; }\n.parent-range { background:var(--accent-soft); box-shadow:inset 0 0 0 1px var(--rule); }\n.selected-range { background:var(--accent); transition:width .2s,left .2s; }\n.interval-row:nth-child(2) .selected-range { background:var(--second); }\n.formula { font-size:14px; }.formula div+div { margin-top:6px; }\n.code-result { margin-top:16px; font-size:13px; font-variant-numeric:tabular-nums; }\n#code { font-size:23px; letter-spacing:.08em; }#binary-range { color:var(--muted); }\n.binary-track { height:22px; }#final-range,#binary-cell { position:absolute; height:100%; }\n#final-range { background:var(--accent-soft); box-shadow:inset 0 0 0 1px var(--accent); }\n#binary-cell { background:var(--second); height:12px; top:5px; }\n@media(max-width:480px) { .interval-label { flex-wrap:wrap; }.interval-label span { font-size:12px; } }\n","js":"const input=document.querySelector('#probability'), rows=document.querySelector('#intervals'), message=['A','B','A'];\nfor(let i=0;i<3;i++){const row=document.createElement('div');row.className='interval-row';row.innerHTML='<div class=\"interval-label\"><strong></strong><span></span></div><div class=\"interval-track\"><div class=\"parent-range\"></div><div class=\"selected-range\"></div></div>';rows.append(row);}\nfunction place(el,lo,hi){el.style.left=(lo*100)+'%';el.style.width=((hi-lo)*100)+'%';}\nfunction draw(){const p=+input.value/100;document.querySelector('#probability-value').textContent=viz.number(p,2);let lo=0,hi=1;\n message.forEach((symbol,i)=>{const beforeLo=lo,beforeHi=hi,w=hi-lo,cut=lo+w*p;if(symbol==='A')hi=cut;else lo=cut;const row=rows.children[i];row.querySelector('strong').textContent=message.slice(0,i+1).join('')+' · × '+viz.number(symbol==='A'?p:1-p,2);row.querySelector('.interval-label span').textContent='['+viz.number(lo,4)+', '+viz.number(hi,4)+')';place(row.querySelector('.parent-range'),beforeLo,beforeHi);place(row.querySelector('.selected-range'),lo,hi);});\n const width=hi-lo,info=-Math.log2(width);let m=0,k=0;for(;m<24;m++){k=Math.ceil(lo*2**m-1e-10);if((k+1)/2**m<=hi+1e-12)break;}\n const a=k/2**m,b=(k+1)/2**m;\n document.querySelector('#product').textContent='W = '+viz.number(p,2)+' × '+viz.number(1-p,2)+' × '+viz.number(p,2)+' = '+viz.number(width,5);\n document.querySelector('#information').textContent='−log₂ W = '+viz.number(-Math.log2(p),2)+' + '+viz.number(-Math.log2(1-p),2)+' + '+viz.number(-Math.log2(p),2)+' ≈ '+viz.number(info,2)+' bit';\n document.querySelector('#code').textContent=k.toString(2).padStart(m,'0');document.querySelector('#binary-range').textContent='['+viz.number(a,5)+', '+viz.number(b,5)+') ⊆ ABA';\n place(document.querySelector('#final-range'),lo,hi);place(document.querySelector('#binary-cell'),a,b);\n document.querySelector('#ideal').textContent=viz.number(info,2)+' bit';document.querySelector('#actual').textContent=m+' bit';\n}\ninput.addEventListener('input',draw);draw();","audio":false,"strings":{"actual":"这条消息输出的位数","axis":"三行共用 [0, 1) 标尺;右侧显示保留区间的端点。","binary":"选中一个完整的二进制网格单元","control":"模型概率 q(A) = p","eyebrow":"概率的乘法 → 信息量的加法","heading":"区间变窄,为什么就需要更多 bit?","ideal":"理想信息长度 −log₂ W","intro":"消息始终是 ABA。浅色是本轮划分前的区间,实色是保留的部分;不用翻页,三个阶段同时可见。","note":"底部浅蓝区间是 ABA,金色小区间完全包含在其中。只输出金色区间的二进制前缀;约定模型相同、消息长度为 3。控制范围 0.25–0.75,示意计算使用理想区间;小数显示有舍入,实际比特数仍是整数。"}}
在 p=0.6 时,三个区间依次为 [0,0.6)、[0.36,0.6)、[0.36,0.504)。最终宽度为 0.6×0.4×0.6=0.144,理想信息长度约为 2.796 bit。
怎样输出实际比特?一种清楚的收尾方式是在最终区间内选一个完整的二进制网格单元 [k/2m,(k+1)/2m),发送其 m 位前缀。例如 011 对应 [0.375,0.5),完全落在 ABA 的区间内。解码器使用相同概率划分,就能确定消息;这里还约定消息长度为 3,实际系统需要共享长度或终止规则。
所以算术编码仍然输出整数个 bit。它允许信息成本跨符号累积,最后统一付出有限的收尾开销。对理想精度、已知块长的这种构造,整块可做到 −log2q(xn)≤l(xn)<−log2q(xn)+2;有限精度实现还要考虑重归一化、概率量化和文件头等成本。算法与区间构造可参阅 Stanford 算术编码讲义。
熵率:有上下文后,每个符号还剩多少不确定性?
独立假设下,联合熵等于单符号熵的 n 倍。有依赖时,应该先使用链式法则:
H(X1,…,Xn)=i=1∑nH(Xi∣X1,…,Xi−1)
若极限存在,熵率定义为:
Hˉ=n→∞limn1H(X1,…,Xn)
对有限字母表的平稳信源,这个极限存在,并等于逐渐加长历史后的条件熵极限。若还要使用下面的样本路径 AEP,通常进一步要求遍历性。
一个具体例子:令 X1 为公平比特,之后以概率 r 翻转上一位、以概率 1−r 保持,翻转选择彼此独立。每个位置单看仍是公平比特,熵为 1,但已知上一位后的不确定性只有 h2(r)。因此:
H(Xn)=1+(n−1)h2(r),Hˉ=h2(r)
当 r=0.1,熵率约为 0.469 bit/符号。压缩器利用的正是这种上下文冗余,不能只看单个符号的边缘频率。
把同一条序列交给两种预测器
下面两个固定样本的 0/1 总频率相同。独立模型始终给下一位概率 1/2;上下文模型知道上一位后,给“保持”概率 1−r、给“翻转”概率 r。改变 r,再逐位揭示,观察两份累计理想码长如何分开。
正在呈现知识画面
频率相同,上下文为什么能改变码长?两条预设序列都含八个 0 和八个 1。逐位揭示,比较公平独立模型与可调翻转概率的马尔可夫模型:样本累计码长随转移变化,源的平均熵率为 h₂(r)。
{"id":"source-context","title":"频率相同,上下文为什么能改变码长?","summary":"两条预设序列都含八个 0 和八个 1。逐位揭示,比较公平独立模型与可调翻转概率的马尔可夫模型:样本累计码长随转移变化,源的平均熵率为 h₂(r)。","height":750,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div class=\"presets\" role=\"group\" data-i18n-label=\"samples\"><button data-sample=\"runs\" data-i18n=\"runs\"></button><button data-sample=\"mixed\" data-i18n=\"mixed\"></button></div>\n<label class=\"control\" for=\"flip\"><span data-i18n=\"flip\"></span><output id=\"flip-value\"></output></label><input id=\"flip\" type=\"range\" min=\"5\" max=\"95\" step=\"5\" value=\"10\">\n<div class=\"source-metrics\"><span>H(Xᵢ) = 1 bit</span><span>H̄ = h₂(r) = <output id=\"entropy-rate\"></output> bit</span></div>\n<div id=\"tokens\" role=\"img\" data-i18n-label=\"tokens\"></div>\n<div class=\"toolbar\"><button data-prev data-i18n=\"prev\"></button><button data-next data-i18n=\"next\"></button><button data-play data-i18n=\"play\" aria-pressed=\"false\"></button><span data-step></span></div>\n<div class=\"metrics\"><div class=\"metric\"><span data-i18n=\"iid\"></span><strong id=\"iid-loss\"></strong></div><div class=\"metric\"><span data-i18n=\"markov\"></span><strong id=\"markov-loss\"></strong></div></div>\n<div class=\"current\"><span data-i18n=\"current\"></span><strong id=\"current-loss\"></strong></div>\n<div class=\"cumulative\" role=\"img\" data-i18n-label=\"curve\"><div class=\"axis\"><span data-i18n=\"axis\"></span><span id=\"length-scale\"></span></div><svg viewBox=\"0 0 600 160\" preserveAspectRatio=\"none\"><path class=\"grid\" d=\"M0 80H600 M0 160H600\"/><path id=\"iid-curve\" class=\"iid\"/><path id=\"context-curve\" class=\"context\"/><circle id=\"context-dot\" r=\"4\"/></svg><div class=\"axis\"><span>0 bit</span><span data-i18n=\"positions\"></span><span>16</span></div></div>\n<div class=\"legend\"><span class=\"iid\" data-i18n=\"iid\"></span><span class=\"context\" data-i18n=\"markov\"></span></div>\n<details class=\"audit-details\"><summary data-i18n=\"details\"></summary><div class=\"formula\" id=\"sample-formula\"></div><div class=\"formula\" id=\"expected\"></div><p class=\"note\" data-i18n=\"note\"></p></details>","css":"\n.intro { margin:8px 0 14px; color:var(--muted); font-size:14px; }\n.presets { display:flex; flex-wrap:wrap; margin:12px 0; }\n.presets button { flex:1 1 auto; min-width:0; }\n.control { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:12px; font-size:14px; }\n.control output { flex-shrink:0; font-variant-numeric:tabular-nums; font-weight:600; }\n.metrics { grid-template-columns:repeat(2,minmax(0,1fr)); margin:12px 0; }\n.metric strong { font-size:23px; }.metric span { font-size:12px; overflow-wrap:anywhere; }\n.note { color:var(--muted); font-size:12px; line-height:1.65; margin-top:10px; }\n.formula { font-size:14px; font-variant-numeric:tabular-nums; overflow-wrap:anywhere; margin:10px 0; }\n.copy-stack { display:grid; margin:12px 0; font-size:13px; line-height:1.65; }\n.copy-stack > p { grid-area:1/1; min-width:0; }.reserve { visibility:hidden; pointer-events:none; user-select:none; }\n.audit-details { border-top:1px solid var(--rule); margin-top:14px; padding-top:10px; font-size:13px; }\n.audit-details summary { cursor:pointer; color:var(--accent); min-height:32px; padding:5px 0; }\n.audit-details summary:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }\n@media(max-width:480px){.intro{font-size:13px}.metric strong{font-size:21px}.presets button{font-size:13px}.formula{font-size:13px}}\n\n.source-metrics { display:flex; flex-wrap:wrap; gap:4px 20px; font-size:13px; margin:8px 0 12px; font-variant-numeric:tabular-nums; }\n#tokens { display:grid; grid-template-columns:repeat(16,minmax(0,1fr)); gap:4px; }\n.token { display:grid; place-items:center; height:32px; background:var(--surface); border-bottom:2px solid var(--rule); border-radius:3px; color:var(--muted); font-variant-numeric:tabular-nums; }\n.token.revealed { color:var(--ink); border-color:var(--accent); background:var(--accent-soft); }.token.flip { border-color:var(--second); }.token.current { outline:2px solid var(--ink); outline-offset:-2px; }\n.current { display:flex; flex-wrap:wrap; justify-content:space-between; gap:4px 12px; font-size:13px; padding-bottom:8px; }.current strong { font-variant-numeric:tabular-nums; font-weight:500; }\n.cumulative { margin-top:8px; }.cumulative svg { height:135px; width:100%; overflow:visible; }.grid { fill:none; stroke:var(--rule); stroke-width:1; vector-effect:non-scaling-stroke; }\n.cumulative path.iid { stroke:var(--muted); stroke-dasharray:5 4; }.cumulative path.context { stroke:var(--accent); }.cumulative path:not(.grid) { fill:none; stroke-width:2; vector-effect:non-scaling-stroke; }.cumulative circle { fill:var(--accent); }\n.axis { display:flex; justify-content:space-between; color:var(--muted); font-size:11px; }.legend { display:flex; flex-wrap:wrap; gap:4px 16px; font-size:12px; margin-top:5px; }.legend span::before{content:'';display:inline-block;width:18px;border-top:2px solid var(--accent);margin-right:5px;vertical-align:middle}.legend .iid::before{border-color:var(--muted);border-top-style:dashed}\n@media(max-width:480px){#tokens{grid-template-columns:repeat(8,minmax(0,1fr))}.token{height:26px}.cumulative svg{height:110px}.toolbar:has([data-play]){margin-top:12px;padding-top:10px}.metrics{margin:10px 0}}\n","js":"const $=s=>document.querySelector(s),samples={runs:'0000000011111111',mixed:'0100110101100011'};let sample='runs',step=0;\nfor(let i=0;i<16;i++){const e=document.createElement('span');e.className='token';$('#tokens').append(e);}\nfunction draw(){const seq=samples[sample],r=+$('#flip').value/100,n=step+1,h=viz.entropy([r,1-r]);let sum=0,k=0,pts=['0,160'];\n const fullLength=s=>1+[...s].slice(1).reduce((a,b,i)=>a-Math.log2(b===s[i]?1-r:r),0);const scale=Math.max(24,Math.ceil(Math.max(...Object.values(samples).map(fullLength))/8)*8);$('#length-scale').textContent=scale;\n const losses=[...seq].map((b,i)=>-Math.log2(i===0?.5:b===seq[i-1]?1-r:r));\n losses.slice(0,n).forEach((l,i)=>{sum+=l;pts.push(((i+1)/16*600)+','+(160-sum/scale*160));if(i&&seq[i]!==seq[i-1])k++;});\n $('#flip-value').textContent=viz.number(r,2);$('#entropy-rate').textContent=viz.number(h,3);$('#iid-loss').textContent=n+' bit';$('#markov-loss').textContent=viz.number(sum,3)+' bit';\n const probability=2**(-losses[n-1]);$('#current-loss').textContent='q = '+viz.number(probability,2)+' → '+viz.number(losses[n-1],3)+' bit';\n [...$('#tokens').children].forEach((e,i)=>{e.textContent=i<n?seq[i]:'·';e.classList.toggle('revealed',i<n);e.classList.toggle('flip',i>0&&i<n&&seq[i]!==seq[i-1]);e.classList.toggle('current',i===step);});\n $('#tokens').setAttribute('aria-label',viz.t('tokens')+' '+seq.slice(0,n)+'; n='+n);$('#iid-curve').setAttribute('d','M0,160 L'+(n/16*600)+','+(160-n/scale*160));$('#context-curve').setAttribute('d','M'+pts.join(' L'));$('#context-dot').setAttribute('cx',n/16*600);$('#context-dot').setAttribute('cy',160-sum/scale*160);\n $('#sample-formula').textContent='L = 1 − '+k+' log₂ r − '+(n-1-k)+' log₂(1−r) = '+viz.number(sum,3)+' bit';$('#expected').textContent='H(X¹⁶) = 1 + 15 h₂(r) = '+viz.number(1+15*h,3)+' bit';\n $('.cumulative').setAttribute('aria-label',viz.t('curve')+' n='+n+', '+viz.t('iid')+': '+n+' bit; '+viz.t('markov')+': '+viz.number(sum,3)+' bit.');document.querySelectorAll('[data-sample]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.sample===sample)));}\nconst sequence=viz.sequence({count:16,render(s){step=s;draw();}});$('#flip').addEventListener('input',()=>{sequence.pause();draw();});document.querySelectorAll('[data-sample]').forEach(e=>e.addEventListener('click',()=>{sample=e.dataset.sample;sequence.goTo(0);}));","audio":false,"strings":{"axis":"累计码长 · bit","current":"当前位的上下文预测","curve":"两种模型的样本累计理想码长","details":"样本码长与平均熵率有什么不同?","eyebrow":"同一条消息,两种概率模型","flip":"上下文模型的翻转概率 r","heading":"都是一半 0、一半 1,为什么码长不同?","iid":"独立模型:q=½","intro":"两条固定样本各有八个 0 和八个 1。逐位揭示,比较忽略上下文与读取上一位的预测。","markov":"上下文模型","mixed":"交错排列","next":"下一位","note":"两条序列是固定样本,不是随机生成的信源。两种源模型都从公平首位开始;上下文模型之后独立决定是否翻转,所以各位置边缘仍公平。只有 r=0.5 时它退化为独立源。橙色下沿标出样本翻转;纵轴上限随 r 调整并标出,同一 r 下两个样本使用相同尺度。L 是这个样本的理想码长,H(X¹⁶) 是模型下所有样本的平均,不能把一次样本的 L/16 当成熵率。","pause":"暂停","play":"逐位播放","positions":"已揭示位数","prev":"上一位","runs":"连续成段","samples":"固定样本","tokens":"已揭示的比特,当前位有边框"}}
若前 n 位中有 k 次翻转,上下文模型的样本码长为
L(xn)=1−klog2r−(n−1−k)log2(1−r).
第一位花 1 bit。后面的每一次保持或翻转,都按相应条件概率付费。r=0.5 时每一步都是 1 bit;r=0.1 时连续保持便宜,意外翻转昂贵。利用上下文不保证每条消息都更短:若样本经常翻转,而模型认为翻转罕见,累计码长可以超过独立模型。只有在该源模型下取平均,才得到 1+(n−1)h2(r)。
AEP:典型集小,为什么概率质量却大?
对独立同分布信源,序列的平均自信息是独立同分布随机量的平均。大数定律给出渐近等分性质(AEP):
−n1log2P(Xn)=n1i=1∑n−log2P(Xi)PH(X)
固定容差 ε>0,把平均自信息落在 H±ε 内的序列叫作典型集 Tn,ε。其总概率随 n 增大趋于 1,而每条典型序列的概率满足:
2−n(H+ε)≤P(xn)≤2−n(H−ε)
这里容易混淆两种“多”:序列的条数占比,与按真实分布采样时命中的概率。下面固定 P(1)=0.2、ε=0.12,用精确二项分布计算,不依赖随机模拟。
正在呈现知识画面
典型序列占比很小,命中概率却越来越大固定 P(1)=0.2 与容差 0.12 bit,改变序列长度。上图是平均自信息的概率直方图;蓝色表示典型序列。下方分别统计真实采样命中概率和全部二元序列中的条数占比。
{"id":"source-aep","title":"典型序列占比很小,命中概率却越来越大","summary":"固定 P(1)=0.2 与容差 0.12 bit,改变序列长度。上图是平均自信息的概率直方图;蓝色表示典型序列。下方分别统计真实采样命中概率和全部二元序列中的条数占比。","height":850,"html":"<p class=\"eyebrow\" data-i18n=\"eyebrow\"></p><h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p>\n<div class=\"presets\" role=\"group\" data-i18n-label=\"control\"><button data-n=\"4\">n = 4</button><button data-n=\"16\">n = 16</button><button data-n=\"64\">n = 64</button><button data-n=\"256\">n = 256</button></div>\n<p class=\"note\" data-i18n=\"vertical\"></p>\n<div class=\"distribution\" role=\"img\" data-i18n-label=\"chart\"><div class=\"typical-band\"></div><div class=\"entropy-marker\"></div><div id=\"bars\"></div><span class=\"ceiling\">0.6</span></div>\n<div class=\"scale\"><span>0</span><span>H ≈ 0.722</span><span>2.4 bit</span></div><p class=\"note\" data-i18n=\"horizontal\"></p>\n<div class=\"formula\">| −log₂ P(Xⁿ) / n − H | ≤ 0.12</div>\n<div class=\"mass\"><div class=\"mass-label\"><span data-i18n=\"mass\"></span><strong id=\"mass\"></strong></div><div class=\"mass-track\"><div id=\"mass-fill\"></div></div></div>\n<div class=\"mass\"><div class=\"mass-label\"><span data-i18n=\"count\"></span><strong id=\"fraction\"></strong></div><div class=\"mass-track\"><div id=\"count-fill\"></div></div></div>\n<div class=\"count-formula\" id=\"count-formula\"></div>\n<p class=\"note\" data-i18n=\"note\"></p>","css":".intro { margin:8px 0 18px; color:var(--muted); font-size:14px; }\n.control { display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:18px; }\n.control output { min-width:4em; flex-shrink:0; text-align:center; background:var(--accent-soft); color:var(--accent); padding:2px 8px; border-radius:6px; font-weight:600; }\n.formula { margin-top:18px; padding:14px 0; border-block:1px solid var(--rule); font-size:16px; font-variant-numeric:tabular-nums; overflow-wrap:anywhere; }\n.copy-stack { display:grid; margin-top:14px; color:var(--muted); font-size:14px; }\n.copy-stack > * { grid-area:1/1; }\n.copy-stack .reserve { visibility:hidden; }\n.note { margin-top:14px; color:var(--muted); font-size:12px; }\n.metric-pair { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-top:18px; }\n.metric-pair > div { min-width:0; }\n.metric-pair span { display:block; font-size:12px; color:var(--muted); }\n.metric-pair strong { display:block; font-size:24px; font-weight:500; font-variant-numeric:tabular-nums; }\n.scale { display:flex; justify-content:space-between; color:var(--muted); font-size:12px; font-variant-numeric:tabular-nums; }\n@media(max-width:480px) { .metric-pair strong { font-size:21px; } }\n.presets { display:flex; }.presets button { font-size:13px; padding-inline:6px; }\n.distribution { position:relative; height:210px; margin-top:12px; border-bottom:1px solid var(--muted); background:repeating-linear-gradient(to top,transparent 0 calc(33.333% - 1px),var(--rule) calc(33.333% - 1px) 33.333%); }\n.typical-band { position:absolute; top:0; bottom:0; left:25.080337%; width:10%; background:var(--accent-soft); }\n.entropy-marker { position:absolute; top:0; bottom:0; left:30.080337%; border-left:1px dashed var(--accent); }\n#bars { position:absolute; inset:0; }.bin { position:absolute; bottom:0; width:2.5%; padding-right:1px; }.bin-normal { background:var(--muted); }.bin-typical { background:var(--accent); }.bin > div { width:100%; }\n.ceiling { position:absolute; top:0; right:0; font-size:11px; color:var(--muted); }\n.mass { margin-top:16px; }.mass-label { display:flex; justify-content:space-between; gap:12px; align-items:baseline; font-size:13px; }.mass-label strong { flex-shrink:0; font-variant-numeric:tabular-nums; }.mass-track { height:9px; background:var(--surface); margin-top:6px; }.mass-track > div { height:100%; background:var(--accent); }#count-fill { background:var(--second); }\n.count-formula { margin-top:12px; font-size:13px; font-variant-numeric:tabular-nums; }\n@media(max-width:480px) { .presets { display:grid; grid-template-columns:repeat(2,1fr); }.distribution { height:180px; } }\n\n.mass-label strong { width:10em; text-align:right; }.count-formula { min-height:3.2em; }\n.distribution + .scale { position:relative; }\n.distribution + .scale span:nth-child(2) { position:absolute; left:30.080337%; transform:translateX(-50%); }\n.bin, .bin > div { transition:height .36s cubic-bezier(.22,1,.36,1); }\n.mass-track > div { transition:width .36s cubic-bezier(.22,1,.36,1); }\n","js":"const p=.2,H=viz.entropy([p,1-p]),epsilon=.12,binWidth=.06,chart=document.querySelector('#bars');\nfor(let i=0;i<40;i++){const b=document.createElement('div');b.className='bin';b.style.left=(i*2.5)+'%';b.innerHTML='<div class=\"bin-normal\"></div><div class=\"bin-typical\"></div>';chart.append(b);}\nfunction draw(n){const bins=Array.from({length:40},()=>({normal:0,typical:0}));let mass=0,count=0,choose=1,prob=(1-p)**n;\n for(let k=0;k<=n;k++){const info=-(k*Math.log2(p)+(n-k)*Math.log2(1-p))/n,typical=Math.abs(info-H)<=epsilon+1e-12,index=Math.min(39,Math.floor(info/binWidth));bins[index][typical?'typical':'normal']+=prob;if(typical){mass+=prob;count+=choose;}if(k<n){prob*=((n-k)/(k+1))*(p/(1-p));choose*=(n-k)/(k+1);}}\n const fraction=count/2**n;bins.forEach((b,i)=>{const el=chart.children[i],sum=b.normal+b.typical;el.style.height=(sum/.6*100)+'%';el.children[0].style.height=(sum?b.normal/sum*100:0)+'%';el.children[1].style.height=(sum?b.typical/sum*100:0)+'%';});\n document.querySelector('#mass').textContent=viz.number(mass*100,2)+'%';document.querySelector('#fraction').textContent=fraction>=.0001?viz.number(fraction*100,2)+'%':viz.number(fraction*100/10**Math.floor(Math.log10(fraction*100)),2)+' × 10^'+Math.floor(Math.log10(fraction*100))+'%';\n document.querySelector('#mass-fill').style.width=mass*100+'%';document.querySelector('#count-fill').style.width=fraction*100+'%';\n document.querySelector('#count-formula').textContent='|T| ≈ 2^'+viz.number(Math.log2(count),2)+' · '+viz.t('total')+' = 2^'+n;\n document.querySelectorAll('[data-n]').forEach(b=>b.setAttribute('aria-pressed',String(+b.dataset.n===n)));\n}\ndocument.querySelectorAll('[data-n]').forEach(b=>b.addEventListener('click',()=>draw(+b.dataset.n)));draw(16);","audio":false,"strings":{"chart":"平均自信息的概率分布随块长变化,典型范围固定在熵两侧 0.12 bit。","control":"序列长度","count":"典型条数 / 全部条数","eyebrow":"典型集:条数与概率是两件事","heading":"“约 2ⁿᴴ 条”为什么足以承载大部分概率?","horizontal":"横轴:一条序列的平均自信息 −log₂ P(Xⁿ) / n。浅蓝带是 H ± 0.12,虚线是 H;蓝柱为典型部分,灰柱为其余部分。","intro":"所有 2ⁿ 条二元序列都可能出现,但并非等概率。只改变长度 n,观察平均信息如何集中到 H 附近。","mass":"真实采样命中典型集","note":"由二项分布精确求和,显示作了舍入。典型条数按组合数统计,命中概率还要乘每条序列的概率。这里的四个长度展示集中趋势;有限长度的命中率不保证逐步单调。固定容差下,2ⁿᴴ 只表示指数规模的近似。","total":"全部条数","vertical":"纵轴:每个 0.06 bit 分箱的概率,固定范围 0–0.6。"}}
含 k 个 1 的每条序列,概率都是 0.2k0.8n−k;这样的序列有 (kn) 条。因此图中每组的总概率是 (kn)0.2k0.8n−k,而计算条数时只累加 (kn)。这就是两个比例相差很大的原因。
如果典型集总概率至少是 1−δ,由上面的单条概率界可得:
(1−δ)2n(H−ε)≤∣Tn,ε∣≤2n(H+ε)
“约有 2nH 条”说的是这种指数增长率意义上的近似,不是固定 n 下的精确条数。固定容差下也不保证命中率随每一次 n 增加都单调上升。一般的均匀信源可以让所有序列都典型,所以“条数占比很小”是这里非均匀二元例子的现象,不是 AEP 对所有信源的要求。
给典型序列编号,可以解释渐近固定码率、允许小错误概率的编码。要做严格无损压缩,非典型序列也必须能恢复,可以增加逃逸标记和后备编码;不能直接丢掉它们。相关定义见 Stanford AEP 讲义。
语言模型提供概率,熵编码器输出比特
语言模型给出 q(xi∣x<i),算术编码等熵编码器把这些概率变成可还原的比特流。对同一份数据,理想比特率是:
n1i∑−log2q(xi∣x<i)
它是这份样本上的经验负对数似然;在真实分布下取期望,才得到相应的交叉熵。用自然对数训练时还要除以 ln2 才换算为 bit。在一致的单位和数据表示下,预测损失降低通常意味着理想编码成本降低,但某一条消息的码长不受“必须大于总体平均熵”这种逐条限制。
真实压缩系统还要计入模型、词表、长度或结束标记、文件头和数值精度。解码端必须复现同一套概率;若模型成本可被大量数据摊销,预测质量与压缩率的关系才更接近理想情况。Language Modeling Is Compression 研究了这一联系及语言模型的压缩表现。
BPE 通过合并常见相邻符号缩短 token 序列,但词表、概率分布和模型能力也随之改变。比较不同分词器时,应使用 bit/字节等共同单位,不能直接把更少 token 或更低的每 token 损失当作更好的文本压缩。见 Token 与采样。
| 方法 | 码长怎样分配 | 主要限制 |
| 逐符号 Huffman | 每个符号分配整数码字 | 平均冗余小于 1 bit/符号;块编码可进一步摊薄 |
| 算术 / range 编码 | 通过区间细分累积整条消息的概率 | 最终比特流仍为整数长度,需约定模型、精度与收尾方式 |
| ANS | 用整数状态实现熵编码 | 同样依赖概率模型与有限精度;状态与输出组织不同于算术区间 |
下一篇讨论加入噪声后,怎样在冗余与可靠性之间取舍:信道容量与编码。压缩视角在语言模型中的应用见信息论与 LLM。
参考