文件信息密度和压缩
本文内容使用大语言模型生成整理,存在直接粘贴 LLM 返回的部分,使用的模型包括: gemini-3.1-pro,deepseek-v4-pro。 在采信前更应自行判断并验证。
文件信息密度的计算和压缩 序 随着各家 LLM 模型的能力不断提升,已经达到了一个曾经的为我难以想象的程度。 在 GPT 刚火热的时候,跟 LLM 对话还只是询问一些简单的问题,询问一些不熟悉领域的简单问题,或者寻求生活中的帮助,如什么是OSA,怎么做一道菜这样的。 但现在,甚至你可以和 LLM 探讨一些哲学问题,它的考虑似乎更为周全,也能想到一些可能忽略的细节。虽然连续讨论会让模型只跟着之前的思维继续生成,有时候略显愚钝,但是不得不说,已经达到了一个较高的程度。
所以在日常生活中,遇到一些问题,和好奇的点,只要方便,都会尝试和 LLM 对话,看看它能给出什么样的答案。 小时候问“为什么会怎样怎样”,很难得到答案,后来虽然有了网络,但很多念头一闪而过,不愿或者没有那么多精力去搜索整理总结,也忽略的那些问题,现在通过 LLM 的帮助,可以快速获得回答。不得不说,这确实改变了思维习惯,有种已经离不开 AI 了的感觉了。
这篇文的起源,就来自一次和 LLM 的对话,提到了“信息密度”这个概念,在后续的探讨和追问中,学到了(找回了)不少有趣的知识,也有了这篇文章。
文件信息密度和压缩 最开始是看到一个说法,文言文信息密度高,用来和 LLM 对话,可以节省 token。
题外话:这么做会明显降低很多模型的“智商”,LLM 输出和见过的训练数据高度相关,而且文言文理解困难,容易产生歧义,使用文言文和 LLM 对话,极容易输出一些华丽的空洞的废话,所以不要这么做,准确表述问题或需求即可。
信息密度可以通过熵(entropy)来衡量,熵(entropy)是信息论中的一个概念,用于度量信息的随机性或不确定性,熵越高,信息的不确定性就越高,反之亦然。
通过计算一个文本文件的信息熵,可以判断该文件的信息密度,同时可以通过熵值来判断该文件被压缩后的极限(暂时这么说)。
在 LLM Agent 的帮助下,写了一个简单的工具,来计算一个文件的香农熵(Shannon entropy),这里暂时放下信息密度概念,改用熵来描述。
首先计算的是零阶熵(H₀,单字节,i.i.d.),具体:
扫样本,建 256 桶直方图
Plug-in 熵:H_(ML) = −∑ pᵢ log₂ pᵢ
Miller–Madow 修正:H_(MM) = H_(ML) + (K−1)/(2N)
主要计算函数
1 2 3 4 var hist [256 ]int64 h0Naive, k0 := shannonFromHist(hist[:], n) h0, bias0 := millerMadow(h0Naive, k0, n, 8 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 func millerMadow (hNaive float64 , k int , n int64 , maxH float64 ) (h float64 , bias float64 ) { if n <= 0 || k <= 0 || math.IsNaN(hNaive) { return hNaive, 0 } bias = float64 (k-1 ) / (2.0 * float64 (n)) h = hNaive + bias if h < 0 { h = 0 } if maxH > 0 && h > maxH { h = maxH } return h, bias } func shannonFromHist (hist []int64 , n int64 ) (h float64 , k int ) { if n <= 0 { return 0 , 0 } for _, c := range hist { if c == 0 { continue } k++ p := float64 (c) / float64 (n) h -= p * math.Log2(p) } return h, k }
在完成后,找了一个测试的文件(demo.exe),计算结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 文件: demo.exe 文件大小: 22.72 MiB (23819776 bytes) 耗时: 64ms ── 信息密度 ───────────────────────────────────── 香农熵: 6.7068 bits/byte (满分 8.0) 信息密度: 83.84% 理论压缩比: 83.84% (i.i.d. 下界 ≈ 熵/8) 唯一字节数: 256 / 256 零字节占比: 17.93% 可打印占比: 33.64% 高频字节: 0x00 17.9% 0x48('H') 3.3% 0x01 3.2% 0xFF 3.1% 0x02 2.0% 0x24('$') 1.9% 0x89 1.6% 0x8B 1.4%
然后用加壳压缩工具 upx 对其进行封装,upx 可以对二进制可执行文件进行压缩,作为同一个功能的文件,体积变小,功能操作不变,是信息密度提升的典型体现。
upx 处理后的文件计算结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 文件: demo.exe 文件大小: 10.26 MiB (10758656 bytes) 耗时: 41ms ── 信息密度 ───────────────────────────────────── 香农熵: 7.9272 bits/byte (满分 8.0) 信息密度: 99.09% 理论压缩比: 99.09% (i.i.d. 下界 ≈ 熵/8) 唯一字节数: 256 / 256 零字节占比: 0.68% 可打印占比: 38.03% 高频字节: 0xFF 2.4% 0x06 0.8% 0x01 0.7% 0x02 0.7% 0x0E 0.7% 0x00 0.7% 0x08 0.7% 0x20 0.6%
可以看到,upx 对文件的压缩效果非常明显,H₀ 直接达到了 7.9272 ,几乎接近理论极限(8)。
但是这时候一个很明显的问题就出现了,对 upx 封装前的原文件(约 22.7 M),香农熵是 6.7068,”理论压缩“比是 83.84%,就是大约 19 M。 当香农熵为 8 的时,意味着每一个字节都是随机的,无法再继续压缩,理论压缩比为 100%,但是 upx 封装后的文件体积是 10.26 MiB,这是为什么呢?
H₀(零阶香农熵)只衡量单字节的独立分布,但忽略了上下文的“相关性”。
这里的 83.84% 是基于 i.i.d.(独立同分布) 假设的下界。也就是说,如果我们只使用像哈夫曼编码(Huffman Coding)这样纯粹基于单字节词频 的算法,最多只能压到 19M。
但现代压缩算法(如 UPX 使用的 LZMA/NRV,以及 7zip 的 LZMA2)属于基于字典的压缩 (LZ77 变种)。它们不仅仅统计单个字节的出现频率,还能发现连续的字节块(字符串)重复 。
PE 文件(如 .exe)中包含了大量重复的指令片段、连续的 0 填充区和规律的结构体。UPX 把这些成块的重复数据替换成了短指针,打破了单字节独立分布的限制,所以实际压缩率能远超基于 H₀ 计算出的“理论极限”。
简单来说:H₀ 给出的是词频压缩的极限,而不是模式匹配压缩的极限。
先不急去尝试计算高阶熵,对经过 upx 封装后的文件,使用 7zip 的 LZMA2,压缩等级 9,得到的文件再次计算:
1 2 3 4 5 6 7 8 9 10 11 12 文件: demo.7z 文件大小: 10.01 MiB (10497252 bytes) 耗时: 46ms ── 信息密度 ───────────────────────────────────── 香农熵: 7.9954 bits/byte (满分 8.0) 信息密度: 99.94% 理论压缩比: 99.94% (i.i.d. 下界 ≈ 熵/8) 唯一字节数: 256 / 256 零字节占比: 0.50% 可打印占比: 38.39% 高频字节: 0x08 0.5% 0x00 0.5% 0x01 0.5% 0x10 0.5% 0x20 0.5% 0x04 0.5% 0x02 0.5% 0x80 0.5%
可以看到,经过 LZMA2 压缩后文件大小减小的非常有限,可以认为是 PE 文件头和 upx 运行的必要部分被进行了压缩,这符合预期。
对 upx 封装前的文件,同样使用 7zip LZMA2 等级 9 极限压缩,得到的文件计算结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 文件: demo.7z 文件大小: 8.36 MiB (8768800 bytes) 耗时: 35ms ── 信息密度 ───────────────────────────────────── 香农熵: 8.0000 bits/byte (满分 8.0) 信息密度: 100.00% 理论压缩比: 100.00% (i.i.d. 下界 ≈ 熵/8) 唯一字节数: 256 / 256 零字节占比: 0.41% 可打印占比: 38.27% 高频字节: 0x00 0.4% 0xF7 0.4% 0x8B 0.4% 0x29(')') 0.4% 0x08 0.4% 0xC7 0.4% 0xD7 0.4% 0x7D('}') 0.4%
7zip 的压缩比 upx 的效果要好,文件大小达到了 8.36 MB,整理四份数据,方便对比对比:
测试状态
文件名
文件大小 (MiB)
字节数 (Bytes)
香农熵 (H₀)
信息密度
原文件
demo.exe
22.72
23,819,776
6.7068
83.84%
UPX 压缩
demo.exe
10.26
10,758,656
7.9272
99.09%
UPX + LZMA2
demo.7z
10.01
10,497,252
7.9954
99.94%
原文件 + LZMA2
demo.7z
8.36
8,768,800
8.0000
100.00%
这里不具体讨论压缩算法是如何压缩的,但可以大致给出一个论断就是 LZMA2 在极限压缩模式下,对这个文件进行压缩,基本已经达到了压缩的极限,几乎不会再有任何压缩空间。
这样,又出现了有趣的现象,同样都是几乎不会再有任何压缩空间,两个 7z 的 H₀ 几乎都达到了理论极限,而文件的大小相差了 1.65 MB,似乎 UPX + LZMA2 没有打到理论极限,这和之前的论断不符吗?(让我们看 Gemini 老师的解惑)
在数据压缩领域,这被称为“二次压缩陷阱”或 “熵增悖论”。
为什么“二次压缩”反而得不到最小文件?这两者的体积差异并不违背信息论,核心原因在于压缩算法处理数据的先后顺序,以及 LZMA2 作为通用压缩算法的“盲目性”。
UPX 破坏了原始数据的“全局冗余”:UPX 通过自己的算法将原始规律抹除,使得数据体积缩小到 10.26 MB,并将香农熵推高到了 7.9272。此时的文件,在字节层面上已经非常接近完全随机的乱码。
LZMA2 是基于字典的“字节级”通用压缩:
场景 原文件 + LZMA2: LZMA2 直接面对拥有丰富规律的原始数据。凭借其极其强大的算法,它能比 UPX 找到更深层、跨度更大的冗余,从而实现极致压缩,将文件压到了 8.36 MB。
场景 UPX + LZMA2: 当 LZMA2 接手被 UPX 处理过的文件时,它面对的是一段香农熵高达 7.9272 的数据。LZMA2 并不知道这是一个被 UPX 压缩过的程序 ,它只负责找重复的字节。但 UPX 已经把能找到的简单重复都消灭了,导致 LZMA2 强大的“大字典”无用武之地,只能勉强再挤出 0.25 MB 的空间(10.26 MB -> 10.01 MB)。
本质上,这是由于 UPX 的压缩率不如 LZMA2 导致的,UPX 的设计初衷是“压缩并在内存中快速解压执行”,它为了保证程序运行效率和解压速度,牺牲了部分压缩率,且带有一个解压存根(Stub)。
当使用 UPX + LZMA2 时,文件的最终压缩率其实是由“较弱”的那个算法(UPX)决定的基础框架。UPX 改变了数据的呈现方式,把数据变成了一种 LZMA2 无法进一步深度优化的形态。
故,在此提出思考:在数据压缩中,先使用低效压缩算法‘预处理’,反而会阻碍高效压缩算法发挥实力。 ,在日常实践过程中也应需知,如果是为了追求小体积,压缩后再压缩很容易得不偿失。
高阶熵,有限样本偏差和 Miller-Madow 修正 回到高阶熵的计算部分,在有了这些思考后,让 Agent 调整了脚本,并尝试进行压缩。
高阶熵计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 func conditionalEntropyOrder1 (data []byte ) condEntropyResult { n := len (data) if n < 2 { return condEntropyResult{} } var pair [256 ][256 ]int64 var ctx [256 ]int64 for i := 1 ; i < n; i++ { y, x := data[i-1 ], data[i] pair[y][x]++ ctx[y]++ } totalN := int64 (n - 1 ) total := float64 (totalN) var h float64 var sumKyMinus1, sumKy int64 var contexts, singletons int for y := 0 ; y < 256 ; y++ { cy := ctx[y] if cy == 0 { continue } contexts++ if cy == 1 { singletons++ } var hy float64 var ky int64 for x := 0 ; x < 256 ; x++ { c := pair[y][x] if c == 0 { continue } ky++ p := float64 (c) / float64 (cy) hy -= p * math.Log2(p) } h += (float64 (cy) / total) * hy if ky > 0 { sumKyMinus1 += ky - 1 sumKy += ky } } return finishCondResult(h, sumKyMinus1, totalN, contexts, singletons, sumKy) } func conditionalEntropyOrder2 (data []byte ) condEntropyResult { n := len (data) if n < 3 { return condEntropyResult{} } joint := make ([]int32 , 256 *256 *256 ) ctx := make ([]int32 , 256 *256 ) for i := 2 ; i < n; i++ { a, b, c := data[i-2 ], data[i-1 ], data[i] j := int (a)<<16 | int (b)<<8 | int (c) k := int (a)<<8 | int (b) joint[j]++ ctx[k]++ } totalN := int64 (n - 2 ) total := float64 (totalN) var h float64 var sumKyMinus1, sumKy int64 var contexts, singletons int for k := 0 ; k < 256 *256 ; k++ { ck := ctx[k] if ck == 0 { continue } contexts++ if ck == 1 { singletons++ } base := k << 8 var hy float64 var ky int64 for c := 0 ; c < 256 ; c++ { cnt := joint[base|c] if cnt == 0 { continue } ky++ p := float64 (cnt) / float64 (ck) hy -= p * math.Log2(p) } h += (float64 (ck) / total) * hy if ky > 0 { sumKyMinus1 += ky - 1 sumKy += ky } } return finishCondResult(h, sumKyMinus1, totalN, contexts, singletons, sumKy) } type order3Dist struct { total int64 next map [byte ]int32 } func conditionalEntropyOrder3 (data []byte ) condEntropyResult { n := len (data) if n < 4 { return condEntropyResult{} } capHint := n - 3 if capHint > 1 <<20 { capHint = 1 << 20 } contextsMap := make (map [uint32 ]*order3Dist, capHint) for i := 3 ; i < n; i++ { key := uint32 (data[i-3 ])<<16 | uint32 (data[i-2 ])<<8 | uint32 (data[i-1 ]) d := contextsMap[key] if d == nil { d = &order3Dist{next: make (map [byte ]int32 , 2 )} contextsMap[key] = d } d.total++ d.next[data[i]]++ } totalN := int64 (n - 3 ) total := float64 (totalN) var h float64 var sumKyMinus1, sumKy int64 var contexts, singletons int for _, d := range contextsMap { contexts++ if d.total == 1 { singletons++ } var hy float64 ky := int64 (len (d.next)) for _, c := range d.next { p := float64 (c) / float64 (d.total) hy -= p * math.Log2(p) } h += (float64 (d.total) / total) * hy if ky > 0 { sumKyMinus1 += ky - 1 sumKy += ky } } return finishCondResult(h, sumKyMinus1, totalN, contexts, singletons, sumKy) }
对前述 UPX + LZMA2 的文件进行高阶熵计算,结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 文件: demo.7z 文件大小: 8.36 MiB (8768800 bytes) 耗时: 4.748s ── 信息密度 ───────────────────────────────────── 零阶熵 H₀: 8.0000 bits/byte (单字节 / i.i.d.,满分 8.0) 一阶熵 H₁: 7.9939 bits/byte (条件熵 H(Xₙ|Xₙ₋₁)) 二阶熵 H₂: 6.5941 bits/byte (条件熵 H(Xₙ|Xₙ₋₁,Xₙ₋₂)) 三阶熵 H₃: 0.2369 bits/byte (条件熵 H(Xₙ|Xₙ₋₁,Xₙ₋₂,Xₙ₋₃)) [深度样本 4.00 MiB] 信息密度: H₀ 100.00% | H₁ 99.92% | H₂ 82.43% | H₃ 2.96% 上下文增益: H₀−H₁ = 0.0061 | H₁−H₂ = 1.3998 | H₀−H₂ = 1.4058 | H₂−H₃ = 6.3573 | H₀−H₃ = 7.7631 bits/byte 理论压缩比: H₀ 100.00% | H₁ 99.92% | H₂ 82.43% | H₃ 2.96% (越低下界越紧) 最紧理论下界: 2.96% (min Hₖ/8,不含块熵) 唯一字节数: 256 / 256 零字节占比: 0.41% 可打印占比: 38.27% 高频字节: 0x00 0.4% 0xF7 0.4% 0x8B 0.4% 0x29(')') 0.4% 0x08 0.4% 0xC7 0.4% 0xD7 0.4% 0x7D('}') 0.4% ── 块熵曲线 F(B)=H(块)/B(深度熵)──────────────── 深度样本: 4.00 MiB / 分析样本 8.36 MiB (已截断) 块长 B F(B) bits/B 唯一块数 块数 ────────────────────────────────────────────────── 1 8.0000 256 4194304 2 7.9886 65536 2097152 3 6.7777 1341355 1398101 4 4.9999 1048453 1048576 8 2.3750 524288 524288 16 1.1250 262144 262144 32 0.5312 131072 131072 64 0.2500 65536 65536 ── 压缩算法实测(基于样本)────────────────────── 算法 压缩后 压缩比 节省 预估全文件 耗时 ────────────────────────────────────────────────────────────────────────────── gzip (default) 8.36 MiB 100.00% -0.0% 8.36 MiB 116.1ms gzip (best) 8.36 MiB 100.00% -0.0% 8.36 MiB 118.5ms zlib (default) 8.36 MiB 100.00% -0.0% 8.36 MiB 119.9ms flate (default) 8.36 MiB 100.00% -0.0% 8.36 MiB 114.5ms zstd (default) 8.36 MiB 100.00% -0.0% 8.36 MiB 10.8ms zstd (better) 8.36 MiB 100.00% -0.0% 8.36 MiB 21.6ms lz4 (default) 8.36 MiB 100.00% -0.0% 8.36 MiB 6.3ms snappy 8.36 MiB 100.00% -0.0% 8.36 MiB 5.8ms s2 (Snappy++) 8.36 MiB 100.00% -0.0% 8.36 MiB 1.5ms
各种压缩算法压缩都没能再减小文件体积,反而有微小的增加,也应征了前述的 LZMA2 已经将文件压缩到了极致。
但如果只看高阶熵和块熵计算结果中,出现了一个极其反常的现象:
从一阶熵 $H_1$ 到三阶熵 $H_3$,熵值出现了断崖式的下跌。$H_3$ 仅仅只有 0.2369 bits/byte,理论下界甚至指出了还能压缩到 2.96%;块熵 $F(B)$ 更是随着块长度的增加一路跌到了 0.2500。似乎表明,这个文件还能被进一步压缩。
这就是一个非常经典的统计学陷阱:有限样本偏差(Finite Sample Bias) ,三阶条件熵 $H(X_n \vert{} X_{n-1}, X_{n-2}, X_{n-3})$ 的含义是:在已知前面 3 个字节的前提下,猜测第 4 个字节的不确定性。
作为前提条件的 3 个字节,其所有可能的组合(上下文空间)共有 $256^3 = 16,777,216$ 种,但是我们的样本大小(Sample Size) :整个 demo.7z 文件只有 8.36 MB,这意味着我们最多只能提供约 836 万个样本点。而在代码中,为了快速得到结果,避免过大内存占用,截断到了 4.00 MB(约 400 万个样本)。
把 400 万个样本,随机扔进 1677 万的上下文空间结果就是极度稀疏。绝大多数是空的,而那些非空的部分,绝大多数也只有 1 个样本(这被称为 Singleton)。
我们直接用报告中 $B=64$ 和 $B=8$ 的真实数据来做一次数学验算。
在信息论中,块熵 $F(B)$ 的计算公式是:
$$F(B) = \frac{H(\text{Block})}{B}$$
如果一个文件是纯粹随机、毫无规律 的:
当块长 $B=1$ 时(单字节),有 256 种可能,由于完全随机,每个字节均匀分布,熵是满分 $H(1) = 8.0$ bits,此时 $F(1) = 8.0 / 1 = 8.0$。
当块长 $B=64$ 时,总共有 $256^{64}$ 种可能的块组合(这是一个天文数字,大于宇宙中所有原子的数量)。
关键问题来了:深度分析样本只有 4.00 MiB(即 4,194,304 字节)。
当你把 4.00 MiB 按照 $B=64$ 字节切块时,你最多只能切出:
$$\text{块数} = 4,194,304 / 64 = 65,536 \text{ 个块}$$
因为文件是高度随机的,这 65,536 个块里,几乎不可能有任何两个块是完全一模一样的 。
现在我们计算这组“完全独特”的块的香农熵:
$$H(\text{Block}) = -\sum_{i=1}^{65536} \frac{1}{65536} \log_2\left(\frac{1}{65536}\right) = \log_2(65536) = 16 \text{ bits}$$
将这个结果代入 $F(B)$ 公式:
$$F(64) = \frac{H(\text{Block})}{64} = \frac{16}{64} = 0.2500 \text{ bits/byte}$$
0.2500 这与你报告里的 64 | 0.2500 一字不差
这就造成了一个巨大的错觉:因为样本太少,导致程序在观测数据时看到了大量的“必然事件”,从而误以为数据充满了规律,得出了极低的信息熵。
这时候就需要 Miller-Madow 修正来缓解这种偏差。在计算高阶熵和块熵时,使用信息论中经典的偏差修正公式(例如 H_est = H_naive + (K - 1) / (2 * N),其中 $K$ 是非零概率的状态数,$N$ 是样本数),把因为样本不足导致的“虚假确定性”扣除掉。
但是,Miller-Madow 修正是一个一阶渐进修正 ,它有一个隐含的硬性前提:样本数量必须远远大于类别数量($N \gg K$) 。 在我们计算 $H_3$ 时,样本量 $N$(约 400 万)甚至比潜在类别数 $K$(1677 万)还要小。在这个极度稀疏的区间里,简单的数学修正已经无力回天,经验分布已经彻底失真。
故,需要额外引入经验法则来缓解这种偏差,动态 $K$ 值与 $N$ 值校验: 在计算条件熵时,统计学上有一个经验法则:只有当某个上下文的出现次数 $N_c \ge 5 \times K_{max}$ 时(即每个可能的状态平均至少被采样 5 次以上),该上下文的熵计算才算勉强可靠。 对于单字节预测,$K_{max} = 256$,那么平均上下文频次至少要达到 1280 次,计算结果才能算作“极度可靠”。
故此,对程序进行了更新,从一开始一个简单的计算 $H_0$ 的代码,成为了复杂的版本,这是一个输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 文件: demo.7z 分析样本: 8.36 MiB (8768800 bytes) 耗时: 4.849s ── 信息密度 ───────────────────────────────────── 零阶熵 H₀: 8.0000 bits/byte (plug-in 8.0000 + bias 0.0000;单字节 / i.i.d.,满分 8.0) 一阶熵 H₁: 7.9976 bits/byte (plug-in 7.9939 + bias 0.0037;可靠;ctx=256 单例0% 均34253.1次/ctx 均K̂=256.0 截断风险100%(均次≥1280=5×256)) 二阶熵 H₂: 6.9795 bits/byte (plug-in 6.5941 + bias 0.3854;弱可靠(边缘采样);ctx=65536 单例0% 均133.8次/ctx 均K̂=104.1 截断风险78%(全字母表需均次≳1280;K 可能被 N_c 截断,H 或偏低)) 三阶熵 H₃: 0.2943 bits/byte (plug-in 0.2369 + bias 0.0574;不可靠(采样不足);ctx=3710743 单例88% 均1.1次/ctx 均K̂=1.1 截断风险100%) [深度样本 4.00 MiB] 信息密度: H₀ 100.00% | H₁ 99.97% | H₂ 87.24% | H₃ 3.68% 密度等级: 极高(近似随机/已压缩) (按 H₀) 上下文特征: 几乎无局部依赖(近独立/随机) 上下文增益: H₀−H₁ = 0.0023 | H₁−H₂ = 0.0000 | H₀−H₂ = 0.0023 | H₂−H₃ = 0.0000 | H₀−H₃ = 0.0000 bits/byte 理论压缩比: H₀ 100.00% | H₁ 99.97% | H₂ 87.24% | H₃ 3.68% 最紧理论下界: 99.97% 唯一字节数: 256 / 256 零字节占比: 0.41% 可打印占比: 38.27% 高频字节: 0x00 0.4% 0xF7 0.4% 0x8B 0.4% 0x29(')') 0.4% 0x08 0.4% 0xC7 0.4% 0xD7 0.4% 0x7D('}') 0.4% ── 块熵曲线 F(B)=H(块)/B(深度熵 + Miller-Madow)── 深度样本: 4.00 MiB / 分析样本 8.36 MiB (已截断) B F_MM F_ML bias/B floor 唯一率 块数 可靠 ──────────────────────────────────────────────────────────────── 1 8.0000 8.0000 0.0000 22.0000 0.0% 4194304 是 2 7.9965 7.9886 0.0078 10.5000 3.1% 2097152 是 3 6.9376 6.7777 0.1599 6.8050 95.9% 1398101 否 4 5.1249 4.9999 0.1250 5.0000 100.0% 1048576 否 8 2.4375 2.3750 0.0625 2.3750 100.0% 524288 否 16 1.1562 1.1250 0.0312 1.1250 100.0% 262144 否 32 0.5469 0.5312 0.0156 0.5312 100.0% 131072 否 64 0.2578 0.2500 0.0078 0.2500 100.0% 65536 否 ── 压缩算法实测────────────────────── 算法 压缩后 压缩比 节省 预估全文件 耗时 ────────────────────────────────────────────────────────────────────────────── gzip (default) 8.36 MiB 100.00% -0.0% 8.36 MiB 127.1ms gzip (best) 8.36 MiB 100.00% -0.0% 8.36 MiB 130.3ms zlib (default) 8.36 MiB 100.00% -0.0% 8.36 MiB 126.3ms flate (default) 8.36 MiB 100.00% -0.0% 8.36 MiB 124.6ms zstd (default) 8.36 MiB 100.00% -0.0% 8.36 MiB 10.5ms zstd (better) 8.36 MiB 100.00% -0.0% 8.36 MiB 22.4ms lz4 (default) 8.36 MiB 100.00% -0.0% 8.36 MiB 12ms snappy 8.36 MiB 100.00% -0.0% 8.36 MiB 5.1ms s2 (Snappy++) 8.36 MiB 100.00% -0.0% 8.36 MiB 4.3ms
你可以在这里 https://github.com/tdh62/compress_pre 找到更实用的完整代码。
关于压缩的更多思考 这部分更多是 LLM 的分析,可供参考。
当前的压缩算法已经几乎实现了理论的极限压缩,未来很难有巨大压缩率上的进步了:
香农极限的物理锁死: 这类算法的核心数学原理是香农信源编码定理。在不依赖外部知识的情况下,算法只能从小巧的、孤立的数据流内部去榨取重复性(零阶或低阶冗余)。 现代算法(如 LZMA2、Zstd 极限模式)配合先进的区间编码(Range Coding)和 ANS(非对称数字系统),已经把编码本身的“表示开销”压缩到了几乎为 0。也就是说,信息熵是 8.0,它们就能做到 8.0,在数学上已经没有再省哪怕 0.01% 的物理空间了。
算法演进的重点已转向“工程性”: 近十年来,通用无损压缩领域几乎没有出现能大幅提升压缩率的新算法。像 Facebook 的 Zstd 和 Google 的 Brotli,其核心贡献不是压缩率的突破,而是“性能与吞吐量”的飞跃。它们用极低的 CPU 开销和极快的速度,达到了接近传统 LZMA2 的压缩率。
关于“AI 压缩”的思考:
神经网络即“无限字典”(语义压缩 / Semantic Compression)是目前最引人瞩目的方向,其理论基础是“压缩即智能”(Compression is Intelligence)。 在本质上,大语言模型就是一个极其庞大、通过连接权值(Weights)高度压缩的“概率字典”,AI 语义压缩的核心假设是:字典(模型)是共享的基础设施,其传输开销可以被无限平摊。
对于非文本的二进制数据,未来使用的不是 LLM,而是专门针对该数据类型训练的“神经网络”(例如专门预测音频波形的神经编码器 Neural Audio Codec),其原理一致,但使用的“字典”不同。
一些现代的加密算法(如 AES-256)的目的就是将数据彻底打散,消灭所有统计规律,使文件在任何阶数下都呈现出完美的纯随机分布。因为数据是绝对随机的,大模型预测任何下一个字节的概率都恒等于 $\frac{1}{256}$。任何压缩器(包括 AI)都会立刻失效。(如前香农极限所述)
特别的,视频编码(无损):
传统压缩算法(如 7-Zip 的 LZMA2)是“一维”的。在它们眼里,任何文件(无论是文本、音乐还是视频)都是一条无限延伸的单向字节链条(1D 线性流)。
而 H.264/H.265 等视频编码器做的,正是“折叠”。它在拿到这一维的字节流后,第一步就是通过视频格式的头部参数,将这个链条重构、折叠成一个三维时空张量(3D Tensor)。
在这种“折叠”的维度下,结合帧内预测,帧间预测与运动补偿等手段,能把视频文件压缩到一个非常高的水平,也几乎无法再压缩太多。
拓展:为什么一些游戏分发(Repack)可以做到压缩远超 LZMA2 “极限” 如果拿一个几十 GB 的原版游戏文件夹,直接丢给 7-Zip,哪怕你把 LZMA2 开到“极限”模式,字典拉到最大,压出来的体积也绝对不可能 达到知名 Repack 组那种令人发指的压缩率。
因为 Repack 组玩的根本不是“通用压缩”,而是“数据逆向工程”与“多级预处理流水线”。 ,我们可以用一句话来概括 Repack 组的核心秘诀:“既然高熵数据压不动,那就先把它变回低熵数据,再用无限大的字典去压。”
1:先“解压”再“压缩” 现代大型游戏(如虚幻引擎、Unity开发的游戏)在打包时,为了节省硬盘空间和读取时间,其内部的资源文件(.pak, .ba2, .forge 等)本身已经是被压缩过的 。游戏厂商通常会使用 Oodle, Zlib, LZ4 等算法对纹理和模型进行实时压缩。
回忆一下我们上文提到的内容:已经被压缩过的数据,信息密度极高,呈现“高随机性”。
如果用 7-Zip 直接压: 7-Zip 看到的是一堆 99% 密度的乱码,LZMA2 再强大也无从下手,只能省下寥寥几百兆。
Repack 的做法(Xtool 等预处理器): 会使用专门开发的工具(如 Xtool),针对特定的游戏引擎进行逆向解包 。把游戏内部已经用 Oodle 压缩过的数据,强制还原(解压)成未压缩的、体积庞大但规律极强的原始裸数据 。
2:打破内存限制的“无限字典” (SREP) LZMA2 的最大字典是 4GB(这需要消耗极其恐怖的内存)。但如果一个游戏有 100GB 呢?LZMA2 的窗口依然会漏掉大量跨越 4GB 距离的重复数据。
为了解决这个问题,Repack 界广泛使用一个名为 SREP (Super REP) 的工具。
SREP 是一个基于硬盘(Out-of-Core)的 LZ77 字典匹配器。
它不把字典放在内存里,而是放在硬盘上。这意味着它的字典大小几乎是无限的 。
SREP 可以扫描整个 100GB 的解包游戏数据,把所有长达几百字节甚至几兆字节的重复片段(比如重复的虚幻引擎材质、多语言包中重复的空白音轨)全部替换为极短的标记。
经过 SREP 处理后,原本 100GB 的游戏可能瞬间缩水到 30GB,而且剩下的 30GB 数据中,高阶冗余已经被完全榨干。
3:针对音频和视频的有损压缩 通用压缩算法(7z, RAR)对媒体文件(MP4 视频、WAV/OGG 音频)是无效的。部分组,例如 FitGirl 会将游戏里的多媒体文件单独剥离出来,进行专项处理:
无损转码: 使用特殊的无损音频压缩器(如 TAK, FLAC 或更极端的专用算法)重新打包音频。
有损降级(有时候): 对于一些背景视频(如 4K 60帧的过场动画),如果体积太大,可能会被重新编码为 1080P 或降低码率(虽然这属于有损,但视觉上难以察觉,却能省下海量空间)。
选择性下载: 将语言包分离出来,作为可选组件下载。
4:超级打包器 FreeArc FreeArc (虽然现在很多转用自定义脚本了)是一个“智能调度器”。它可以根据文件的后缀名,自动对文本调用 PPMd 算法,对二进制文件调用 LZMA,对音频调用专用算法,把各个算法的优势发挥到极致,形成一个混个合的压缩包。
极限压缩背后的代价 当下载完一个 30GB 的 Repack 压缩包并双击 setup.exe 时,你并不是在“解压”,你是在“重新编译构建”这个游戏 。 你的电脑需要用极高的 CPU 算力解开 LZMA 压缩,将 SREP 记录的几百 GB 的字典数据在硬盘和内存之间疯狂搬运、拼接,把解开的原始纹理,重新用 Oodle 或 Zlib 压缩回游戏引擎能识别的 .pak 格式。
结 不管是基于词频的哈夫曼编码,基于字典匹配的 LZMA,还是将一维字节流折叠成三维时空张量的视频编码,压缩的本质永远是在特定的维度上寻找重复与冗余 。
在压缩数据的时候,先用低效算法进行“预处理”,实际上是在用混乱的伪随机数据“污染”原本整齐的冗余结构。 数据最终的熵值虽然在字节层面都逼近了 8.0 的物理极限,但最终效果天差地别。
传统的通用压缩算法在香农极限下,已经走到了性能与吞吐量优化的后半场。
在文章的结尾,再次给出 计算程序的仓库 。
这次探索学到了很多知识,希望你也能从中有所收获,如果有什么想法或者发现了什么问题,都可以通过邮件或者其他能联系到我的方式告诉我。
最后祝你早安,午安,晚安。