旋转范围编码(RRC)隐写术 —— 一种高效且可证明安全的 语言隐写方法,可将秘密消息嵌入由大型语言模型生成的 自然语言文本中。
论文:Efficient Provably Secure Linguistic Steganography via Range Coding (ACL 2026)
| 步骤 | 描述 |
|---|---|
| 嵌入(算法 3) | 将秘密消息转换为十进制值,并在由语言模型概率分布和 PRNG 生成的偏移量引导的收缩区间内迭代地旋转它。每次旋转步骤产生一个 token。 |
| 提取(算法 4) | 在隐写文本上重新运行语言模型以恢复区间边界,然后反向旋转回原始十进制值并将其二值化。 |
旋转机制解决了普通范围编码隐写术的两个问题:
.
├── rrc_core.py # 算法 3 和 4,由每个入口点共享
├── RRC_embed.py # 嵌入 CLI
├── RRC_extract.py # 提取 CLI(报告恢复准确率)
├── test_roundtrip.py # 跨多个消息长度的往返测试
├── fastest_embed.py # 嵌入速度基准测试
├── 0.Prompts.tsv # 1000 个提示(C4 序列的前约 10 个词)
└── requirements.txt
编码器和解码器共享 rrc_core.py,而不是各自持有循环的独立副本。双方必须逐位一致,而两个独立维护的副本之间的分歧只会表现为往返失败,而不会在检查时显现。
pip install -r requirements.txt
python test_roundtrip.py # GPT-2, 32/128/512 bits
python test_roundtrip.py --model meta-llama/Llama-2-7b-hf
python RRC_embed.py \
--language_model meta-llama/Llama-2-7b-hf \
--bit_length 128 --key 42 --top_k -1
python RRC_extract.py \
--language_model meta-llama/Llama-2-7b-hf \
--bit_length 128 --key 42 \
--input_file 1.RC_decimal_Llama-2-7b-hf_bit128.tsv
编码器记录其嵌入的秘密以及携带该秘密的 token id,因此提取会报告恢复率。对两个接收方模型进行评分:
ids —— 接收方获得 token 序列。这隔离了编码方案;任何低于 100% 的结果都是数值或逻辑故障。text —— 接收方获得去 token 化后的字符串并再次对其进行 token 化,正如文本信道所要求的那样。重新 token 化并不总能重现发送方的分词,因此这是一个下界;两者之间的差距衡量的是分词歧义而非编码方案。在 Llama-2-7b 上,128 位时:ids 为 100%,text 约为 91%。重要:发送方和接收方必须共享语言模型、密钥、位长度和 top-k 设置,并且还必须运行相同的硬件和库版本。参见数值要求。
由 RRC_embed.py 和 RRC_extract.py 共享:
| 参数 | 默认值 | 描述 |
|---|---|---|
--language_model | meta-llama/Llama-3.1-8B | HuggingFace 模型 id |
--bit_length | 128 | 秘密消息长度(以位为单位) |
--top_k | -1 | Top-k 截断;-1 = 完整词汇表 |
--key | 42 | 对称密钥 K(PRNG 种子) |
--precision_margin | 40 | 超出 2**bit_length 所需位数的小数位数 |
--max_ctx_len | 3900 | KV 缓存窗口 |
--device | auto | cuda 或 cpu |
RRC_embed.py 还接受 --prompts、--output、--limit、--seed、
--max_tokens、--resume、--no_verify,以及用于跨进程分片提示集的
--part / --part_max(--part_max 3 配合 --part 0,1,2,每个 GPU 一个)。
RRC_extract.py 还接受 --input_file、--mode {ids,text,both}、
--limit 和 --verbose。
RRC 将宽度为 2**l 的区间缩小到宽度约 1,因此恢复值对相对误差的敏感度是 2**l 倍:在 128 位时,链中任何位置 1e-16 的相对扰动都会使结果偏移约 1e22,而容差为 0.5。提取不会优雅降级——它要么返回消息,要么返回无关的位。在 Llama-2-7b 上测得:
| 设置 | 概率的相对扰动 | 提取 |
|---|---|---|
| 重复相同的 batch-1 解码 | 0(逐位相同) | 有效 |
| batch 2 / 8 / 16 中的同一行 | ~1e-2 | 失败 |
| 对提示进行左填充 | ~1e-2 | 失败 |
| 一次 teacher-forced 传递而非增量解码 | ~4e-3 | 失败 |
两个后果塑造了实现。批处理不可用,因此序列一次处理一个。并且接收方逐步重放发送方的增量解码,而不是在单次前向传递中对隐写文本进行评分,即使它事先持有每个 token。两种捷径测得约快 9 倍,且都不保持概率。
logits 之后的所有内容都是 float64,并在 CPU 和 GPU 上产生逐位相同的边界,因此前向传递是唯一将隐写文本与其硬件绑定的步骤。
精度由消息长度推导得出,为
ceil(bit_length * log10(2)) + margin,并在其他任何内容运行之前设置。它随消息缩放:2**256 需要 78 位数字,2**8192 需要 2466 位。
低于该阈值的精度不会干净地失败。秘密值的低位数字被舍入掉,区间在几个 token 内崩溃,终止测试提前触发,因此一次运行可以完成并报告在算术上不可达的数字。无论消息长度如何都将精度保持在 50 位数字会得到:
| bits | 50 位数字时的利用率 | 完成的样本 | 使用推导精度 |
|---|---|---|---|
| 256 | 157.05% | 8/20 | 100.17% |
| 512 | 299.67% | 9/20 | 102.48% |
| 1024 | 660.91% | 7/10 | 102.58% |
| 2048 | 1046.40% | 1/5 | 98.18% |
| 4096 | 2483.92% | 3/3,未引发错误 | 100.87% |
| 8192 | 4766.49% | 1/2 | 99.07% |
--precision_margin 调整推导最小值之上的余量;最小值本身是强制执行的。
算法 3 在 (L + R) / 2 - d_s 落入 (-0.5, 0.5] 时停止。该测试可能在区间窄于 1 时触发,此时窗口
[mid - 0.5, mid + 0.5) 比承载它的区间更宽。反向旋转是模区间宽度的等距变换,因此跨越切口的窗口会使接收方距离 d_s 整整一个区间。
因此,发送方在决定停止之前会重放接收方的反向旋转,如果重放未返回秘密,则再发出一个 token。重放是纯 Decimal 工作(约 0.05 ms,而一次前向传递约 20 ms),因此该保护措施没有可测量的成本。--no_verify 禁用它。
在 Llama-2-7b 上测得:
| bits | 无保护时恢复 | 有保护时 |
|---|---|---|
| 128 | 291/300 (97.0%) | 300/300 |
| 512 | 64/65 (98.5%) | 40/40 |
| 1024 | 20/20 | 20/20 |
| 2048 | 10/10 | 10/10 |
在 128 位时,保护措施额外生成了 0.095% 的 token。1024 和 2048 位的样本太少,无法界定该比率——在 3% 的比率下,0/20 比非 0/20 更可能——因此它们不能证明更长的消息表现不同。有保护时,370/370 被恢复。
没有保护时,发送方无法分辨差异:它报告成功并发出一个解码为无关位的隐写文本。该比率随抽样而变化;一次 99 个提示的运行未见到任何失败,而 3% 的比率大约有 5% 的时间会产生这种情况,并且重新运行时它完全重现。相同的样本在从 45 到 200 位数字的每个可用精度下都失败,因此该行为源于终止测试而非算术。
该保护措施通过禁用保护进行嵌入并运行真实提取器来对照真实情况进行检查:它标记的样本正是接收方失败的样本,没有假阳性,也没有遗漏。
在 Llama-2-7b 上,语言模型前向传递占生成步骤的 95–97%,而 7B fp16 模型的 batch-1 解码处于内存带宽屋顶线(每 token 约 13.5 ms,而实测约 18.7 ms)。因此嵌入速度跟随模型的解码速度,编码本身对其影响很小。
在较小的模型上,前向传递不再占主导地位,每步的 CPU 工作变得可见。在 GPT-2 上以 128 位端到端测得,共享机器上每格 100 条消息:
| torch CPU 线程 | bits/s |
|---|---|
| 1 | 1320, 1205 |
| 16 | 1232, 756 |
| 20(torch 默认) | 352 |
吞吐量由 CPU 线程争用而非编码路径中的任何内容决定,并且多线程数字在每次运行之间不可重现。因此每个入口点都调用 torch.set_num_threads(1):这里的每个 CPU 操作都是单次词汇表大小的传递,太小而无法从线程池中获益,又大到在线程池与其他工作争用时严重受损。这带来的是可预测性,而不是在空闲机器上的加速。
其余每步选择在孤立情况下都很廉价,并且在一个线程下不会可测量地改变端到端吞吐量;做出这些选择是因为它们没有成本:
min(config.vocab_size, len(tokenizer.vocab)) 以使其与分词器对齐。这避免了重新分配嵌入矩阵,并使模型对象保持不变,这在同一模型跨运行共享时很重要。128 位消息,完整词汇表,温度 1.0,RTX 6000 Ada,一个 CPU 线程,GPU 其他方面空闲。每个模型 100 条消息(Llama-2-7b 为 60 条),每一条都经过往返并检查:
| 模型 | 嵌入 | 提取 | tokens/msg | 利用率 |
|---|---|---|---|---|
| GPT-2 | 1386 bits/s | 1385 bits/s | 22.3 | 99.4% |
| OPT-1.3b | 754 bits/s | 753 bits/s | 30.4 | 98.7% |
| Llama-2-7b | 183 bits/s | 183 bits/s | 45.4 | 98.9% |
提取以嵌入速度运行,因为两者每个 token 都执行一次前向传递,而反向旋转链与它们相比可忽略不计。
吞吐量对 GPU 上的其他作业不敏感——在满载的卡上会得到相差几个百分点以内的相同数字——因为工作负载是一系列延迟受限的单 token 前向传递,而不是任何使设备饱和的东西。约 6% 的运行间变化来自抽取的提示:bits/s 是 bit_length / time,而时间随消息所需的 token 缩放,因此较低熵提示的样本读起来更慢。
Python 的 Decimal % 使用截断除法,可能返回负余数(Decimal('-19672') % Decimal('65536') 给出 -19672,而不是 45864)。反向旋转需要真正的取模,因此 decimal_mod 使用向下取整除法语义:
def decimal_mod(a, m):
return a - m * (a / m).to_integral_value(rounding=ROUND_FLOOR)
@inproceedings{yan-murawaki-2026-efficient,
title = "Efficient Provably Secure Linguistic Steganography via Range Coding",
author = "Yan, Ruiyi and
Murawaki, Yugo",
editor = "Liakata, Maria and
Moreira, Viviane P. and
Zhang, Jiajun and
Jurgens, David",
booktitle = "Proceedings of the 64th Annual Meeting of the {A}ssociation for {C}omputational {L}inguistics (Volume 1: Long Papers)",
month = jul,
year = "2026",
address = "San Diego, California, United States",
publisher = "Association for Computational Linguistics",
url = "https://aclanthology.org/2026.acl-long.39/",
doi = "10.18653/v1/2026.acl-long.39",
pages = "890--907",
ISBN = "979-8-89176-390-6"
}