Raptor码与LDPC预编码的MATLAB仿真实现与调优
2026/9/14 3:02:44 网站建设 项目流程

简介:Raptor码是一种无速率喷泉码的增强版本,通过LDPC预编码与超图结构实现高鲁棒前向纠错,能在数据丢包严重的信道中高效恢复原始消息,常用于无线通信、数据存储与网络传输。这份20KB的小巧压缩包内共6个文件,3个M脚本分别对应LDPC预编码实现、Raptor编码仿真以及AWGN信道下的性能测试,3个MAT文件则保存了不同码长与信噪比条件下的实验数据,便于直接绘图分析。已有536人学习下载,适合具备MATLAB基础、正在研究喷泉码或信道编码的通信专业学生和工程师。运行仿真程序可观察误码率随信息长度、编码率的变化规律,理解接收端随编码符号增多逐步解码的自适应过程;修改信道模型或预编码参数,还能进一步评估系统在衰落环境中的可靠性,为实际工程选型提供参考。

1. Raptor码不是一种码,而是一套带LDPC预编码的fountain code框架

Raptor 码在 RFC 5053、RFC 6330 和卫星组播里出现频率很高,但多数人第一次仿真它,是被“Raptor”这个名字误导成某种具体纠错码。实际上 Raptor 是喷泉码(fountain code)的一种结构:外层 LT 编码把数据无限往外喷,内层 LDPC 预编码把 LT 层没喷全的洞兜住。叠加后,接收端只要收到略多于源符号数量的任意一组包,就能以逼近 1 的概率恢复全部数据,通信仿真里不再需要 ACK 重传。这篇文档把 Raptor 的 LDPC 预编码链路放到 MATLAB 里跑通,从两层分工、参数调优、仿真发散排查到结果验证,都能直接改 k、码率、开销和随机种子复现。适合刚接触喷泉码仿真的读者,也适合把 Raptor 落到网络编码方案的工程师。

2. 从fountain code到Raptor码:LT喷射层与LDPC预编码层的分工逻辑

2.1 纯LT码为什么在中等码长下撑不起常数开销

LT 码是最早实用的喷泉码:从 k 个源符号里随机取 d 个做异或,得到一个输出符号,d 按度分布采样。译码端维护一个“剥离”过程——找到一个当前度为 1 的输出符号,它的值就是对应的源符号;恢复后把这个源符号从其它输出符号里异或掉,于是周围输出符号的度集体减 1,循环往复。这个过程的终点只有两种:全部源符号恢复,或者所有剩余输出符号的度都大于 1,于是卡死。卡死是纯 LT 的固有毛病。

要保证以不低于 1-δ 的概率不卡死,鲁棒孤子分布需要把平均度抬到 O(ln k)、把度分布的支撑延伸到 k/R 量级(约 √k),同时接收端还要额外多收一部分符号来维持剥皮动力。码长 k 从 100 涨到 10000,这部分代价在中等码长下非常直观:失败率对开销的曲线拖着很长的尾巴,而不是一个干净的悬崖。Raptor 的出现就是把“每个输出符号都要能触发剥皮”这个苛刻要求拆掉一半——LT 层不需要恢复全部符号,恢复 95% 就收工,剩下的交给另一层。

2.2 预编码层在Raptor码里的位置:先垫一层,再往外喷

Raptor 的编码流程分两步。预编码:把 k 个源符号通过一个码率为 R_p = k/k' 的 LDPC 码编成 k' 个中间符号(intermediate symbols),前 k 个就是源符号本身,后 k'-k 个是 LDPC 校验符号。LT 编码:度分布改为以 k' 为参数,对中间符号做异或,产生输出符号发往信道。接收端收到 m 个输出符号后,先按 LT 剥离恢复中间符号,剥不动时把仍为 NaN 的中间符号看成擦除,切到 LDPC 擦除译码:某一行校验方程如果只剩一个未知变量,直接解出来;解出新符号后,LT 层的新一轮剥离又可以继续。两层交替直到没有新符号被解出。

关键在开销口径。LT 层的目标从“恢复 k 个符号”变成“恢复 k' 个中间符号”,度分布压力小了一大截,工程实现里(RFC 5053 的分布表)平均度可以固定在 10 以下,不再随 k 增长;而 LDPC 层只管填补残余。总的符号开销是 (1+ε_lt)/R_p - 1,其中 ε_lt 是 LT 层相对中间符号数的额外接收比例。这里有个常见的仿真口径错误:直接拿 m/k 当开销,忽略预编码码率惩罚,导致不同 R_p 的结果无法横向比较。下表是 k=500、ε_lt=0.05 时不同预编码码率下的实际总开销。

预编码码率 R_p中间符号数 k'LDPC 校验行数接收符号数 m相对源符号总开销
0.905565658416.8%
0.955272755410.8%
0.98511115377.4%

两级译码的主循环在结构上可以写成下面这段骨架,这也是后续仿真里raptor_bp_decode的雏形。

% Raptor 两级译码主循环:交替处理两类约束,直到没有新符号被解出 % recv_syms: 收到的输出符号(值+邻居表),H: 预编码校验矩阵 x = nan(kprime, 1); % 中间符号初始全擦除,NaN 表示未知 while any(isnan(x)) x_old = x; x = lt_peel(x, recv_syms); % LT 层:度1输出符号触发的剥离 x = ldpc_peel(x, H); % 预编码层:单未知校验行触发的剥离 if isequaln(x, x_old), break; end % 不动点:剥皮彻底卡死 end

参数说明:kprime是中间符号数,recv_syms里只放实际收到的输出符号,擦除的符号根本不进这个表;H是 m×k' 的 LDPC 校验矩阵。isequaln会把 NaN 视为相等,所以它能正确比较两轮迭代之间所有未知符号是否原地不动。

2.3 为什么预编码层选LDPC:稀疏图剥皮天然适配擦除信道

预编码不一定要用 LDPC,但工程和仿真里几乎都选它,原因有三个。第一,LDPC 的擦除译码是稀疏图剥皮,和 LT 的剥皮是同一个计算范式,两层可以直接无缝接力,不需要把数据搬到别的域,也不需要软信息迭代。第二,列重 3 左右的规则 LDPC 在中等码长下就能提供稳定的擦除纠正能力,校验行数只有中间符号数的百分之几,开销代价可控。第三,RS 码和 Turbo 码在这里都不顺手:RS 在 GF(2^w) 上做矩阵求逆,纠擦除能力固定为 n-k 个符号,和喷泉层“收到多少用多少”的接续方式不匹配;Turbo 依赖交织器和软判决迭代,在 BEC 上要按擦除位置重做度量,实现复杂度高一个量级。

一句话概括分工:LT 层负责“以接近 1 的概率把绝大多数符号剥出来”,LDPC 预编码层负责“把剥不出来的残余符号用校验约束补出来”。仿真里验证某一层的好坏,要分别统计 LT 层卡住时的残余擦除率,以及 LDPC 层补完后是否清零,这两层指标在参数调优部分会分开讨论。

3. 在MATLAB里搭通Raptor码的LDPC预编码仿真链路:最小可复现代码

3.1 鲁棒孤子度分布采样:c与delta怎么传

先写度分布生成和采样,这是 LT 层的核心,也是最容易写错的一处。

function p = rsd_pmf(k, c, delta) % k: LT 层处理的符号数(Raptor 里是中间符号数 kprime) % c, delta: 鲁棒孤子分布参数,控制平均度与失败率上界 R = c * sqrt(k) * log(k / delta); special = floor(k / R); % tau 的尖峰位置 p = zeros(1, k); for i = 1:k if i == 1 rho = 1 / k; else rho = 1 / (i * (i - 1)); end if i < special tau = R / (i * k); elseif i == special tau = R * log(R / delta) / k; else tau = 0; end p(i) = rho + tau; end p = p / sum(p); % 归一化必须放在最后一步 end

rho是理想孤子分布,单独使用时期望度等于 ln k,剥皮到后期动力不足;tau是在大度位置补出来的“安全垫”,让剥皮进行到最后十几轮还有度 1 符号冒出来。c控制 R 的大小,delta是允许的译码失败率上界。归一化放在最后,是因为 rho+tau 的总和大于 1,直接拿它当概率会让采样偏向大度。一个隐蔽的坑:k 较小时special = floor(k/R)可能小于 1,整段循环走不到 tau 分支,度分布退化成理想孤子分布,译码必然卡死。k=16 起步做小规模验证时,先打印一下 special 是否大于等于 1。

采样用逆变换法,累积分布算一次即可复用:

function d = sample_rsd(cum, k) % cum: 累积分布,调用前算好;k 仅用于保证 d 不超过符号数 d = find(cum >= rand(), 1); if isempty(d), d = k; end % 浮点边界保护 end

3.2 构造可系统编码的LDPC预编码校验矩阵H=[A|B]

预编码层要满足两个条件:校验矩阵能用来做系统编码,擦除剥皮时列重不能太低。这里用 H=[A|B] 的结构,A 是 m×k 的随机部分,B 是 m×m 的下双对角矩阵,保证可逆。

function H = make_precoder_H(k, m, colw) % 构造 Raptor 预编码用 LDPC 校验矩阵 H = [A | B] % A: m x k 随机列,每列 colw 个 1 % B: m x m 下双对角,行列式恒为 1,保证系统编码可做 A = zeros(m, k); for j = 1:k A(randperm(m, colw), j) = 1; end B = eye(m); for j = 2:m B(j, j - 1) = 1; end H = [A, B]; end function x = precode_encoder(H, src, k, m) % 系统编码:x = [src; xp],满足 H*x = 0(GF(2)) A = H(:, 1:k); B = H(:, k+1:end); rhs = mod(A * src, 2); % A*src 得校验方程右侧 xp = zeros(m, 1); for i = 1:m xp(i) = rhs(i); if i > 1 xp(i) = mod(xp(i) + xp(i-1), 2); % 回代,B 是下双对角 end end x = [src; xp]; end

系统编码的意思是中间符号的前 k 位直接等于源符号,后 m 位是 LDPC 校验位。B 是下双对角,回代是 O(m),对 k=500、m=27 的量级可以忽略。colw取 3:列重 2 的 LDPC 在擦除信道下存在大量停止集(stopping set),剥皮到一半就没有校验行只剩一个未知变量了;列重 4 以上没有明显收益,且短码长下两列容易高度重合,没必要。

3.3 主仿真循环:擦除信道、两级剥皮译码与失败率统计

LT 编码和两级剥皮的完整实现如下。

function [y, nbrs] = lt_encode(x, num_syms, kprime, c, delta) % x: kprime x 1 中间符号;返回 num_syms 个输出符号和邻居表 p = rsd_pmf(kprime, c, delta); cum = cumsum(p); y = zeros(num_syms, 1); nbrs = cell(num_syms, 1); for s = 1:num_syms d = find(cum >= rand(), 1); idx = randperm(kprime, d); % 无放回抽样,保证度 d 不被稀释 nbrs{s} = idx; y(s) = mod(sum(x(idx)), 2); end end function x = raptor_bp_decode(y_recv, nbrs_recv, H, kprime) % 两级剥皮:LT 输出符号约束 + LDPC 校验约束,交替执行到不动点 x = nan(kprime, 1); % NaN 表示中间符号未恢复 m_recv = numel(y_recv); active = true(m_recv, 1); % 输出符号是否还剩未知邻居 while true progress = false; for s = 1:m_recv % LT 层:一个输出只剩1个未知邻居 if ~active(s), continue; end nb = nbrs_recv{s}; unk = nb(isnan(x(nb))); if isempty(unk) active(s) = false; elseif numel(unk) == 1 known = setdiff(nb, unk); x(unk) = mod(y_recv(s) + sum(x(known)), 2); active(s) = false; progress = true; end end for i = 1:size(H, 1) % LDPC 层:一行只剩1个未知变量 vars = find(H(i, :)); unk = vars(isnan(x(vars))); if numel(unk) == 1 x(unk) = mod(sum(x(setdiff(vars, unk))), 2); progress = true; end end if ~progress || ~any(isnan(x)) break; end end end

主仿真循环统计给定开销下的失败率:

k = 500; pre_rate = 0.95; m = ceil(k / pre_rate) - k; kprime = k + m; c = 0.03; delta = 0.5; colw = 3; epsilon = 0.15; % 相对中间符号数的接收开销 m_recv = ceil((1 + epsilon) * kprime); trials = 200; fails = 0; rng(7); H = make_precoder_H(k, m, colw); for t = 1:trials src = randi([0 1], k, 1); x = precode_encoder(H, src, k, m); pool = 3 * m_recv; % 生成足够大的输出符号池 [y_all, nb_all] = lt_encode(x, pool, kprime, c, delta); pick = randperm(pool, m_recv); % 等价于擦除信道下收到任意 m_recv 个 x_hat = raptor_bp_decode(y_all(pick), nb_all(pick), H, kprime); fails = fails + any(isnan(x_hat)); end fprintf('k=%d, pre_rate=%.2f, eps=%.2f -> 失败率 %.3f\n', ... k, pre_rate, epsilon, fails / trials);

pool = 3 * m_recv是为了让randperm抽样近似于“从无穷符号流中收到任意 m_recv 个”——输出符号独立同分布,这样抽样不会引入位置偏差。剥皮循环直到无新进展才退出,复杂度是 O(m_recv·平均度 + H 非零元数),k=500 量级在单核 MATLAB 里跑 200 次试验只要几十秒。还有一个口径问题:接收符号数必须相对 k' 而不是 k 去定,否则预编码码率越低的方案越“占便宜”,把预编码层的工作量错误地归到 LT 层头上。主要参数的作用如下表。

参数演示取值作用与调整方向
k500源符号数;加大则失败率曲线更陡,蒙特卡洛更慢
pre_rate / m0.95 / 27预编码校验行数;调 LDPC 兜底能力
colw3LDPC 列重;取 2 会引入停止集
c / delta0.03 / 0.5LT 度分布安全垫
epsilon0.15相对中间符号的接收开销;扫描的主变量
trials200失败率统计精度;目标失败率 1e-2 时建议 ≥500

4. Raptor码仿真的参数扫描与仿真发散排查:c、预编码码率与剥皮不动点

仿真跑通只是开始。工程复现 Raptor 时,度分布参数、预编码码率和列重这几个旋钮互相耦合;而“仿真发散”在喷泉码仿真里通常不是数值溢出,而是剥皮循环提前收敛到一个仍有 NaN 的不动点。这一章把三个旋钮逐一拆开,再给排查顺序。

4.1 度分布参数c与delta的扫描:平均度不是越大越好

c 直接决定 R 和尖峰位置 special。前面例子 k'=527、c=0.03、δ=0.5 时 R ≈ 0.03×√527×ln(527/0.5) ≈ 4.8,special ≈ 110,平均度在 6~7。c 太小(0.005~0.01)时 tau 权重过小,度分布接近理想孤子分布,剥皮进行到后半程动力枯竭,这时 LDPC 层要补的残余超过 m,失败率骤升。c 太大(0.1 以上)时平均度涨到 9~10,每个输出符号的计算量变大,但失败率悬崖的位置并不会因此向左移动。

δ 的作用更微妙。δ 出现在 R 的对数项里,对 R 的影响是对数级的,k 从 500 到 5000,ln(k/δ) 的变化远小于 √k。仿真里把 δ 设成和目标失败率同量级即可;如果蒙特卡洛试验次数只有 200 次,却把 δ 设成 1e-4,分布上的意义和统计分辨率完全对不上,失败率的波动会被误读成参数敏感。固定 k=500、pre_rate=0.95、ε=0.2 时扫描 c,趋势如下表。

cδ失败率趋势平均度趋势
0.010.5偏高,剥皮后期卡死约 6.3
0.030.5明显下降约 6.9
0.100.5不再改善约 9.5
0.030.05与 0.5 档接近约 7.1

数值会随随机种子浮动,但趋势稳定:c 存在一个平台区,平台左侧失败率快速劣化,右侧只有复杂度上升。扫描时保持其它参数和随机种子不动,具体做法在第 5 章。

4.2 预编码码率与LDPC列重的取舍:兜底能力和总开销的交换

预编码码率 R_p 决定 m 和总开销的上限,列重 colw 决定停止集密度。总开销公式 (1+ε_lt)/R_p - 1 说明:R_p 从 0.95 降到 0.90,多付出约 6 个百分点的总开销,多拿到一倍左右的 LDPC 校验行(27 行变 56 行)。k=500、colw=3 时各码率的对照如下。

预编码码率 R_p校验行数 m校验行平均行重理论可补残余上限ε_lt=0.05 总开销
0.9056约 305616.8%
0.9527约 592710.8%
0.9811约 139117.4%

行重随 R_p 上升是随机构造的代价:校验行越重,一个未知变量同时被其它变量约束的概率越大,剥皮越依赖“该行只剩 1 个未知”这个巧合。这也是工程上(RFC 5053 的 LDPC1/LDPC2 结构)用稀疏双对角而不是纯随机矩阵的原因。演示代码保留随机 A,是为了直观看到这个行为:R_p=0.98 时,LT 层残余一旦超过 11 个,LDPC 层必失败;同样残余在 R_p=0.90 时完全可补。仿真中建议固定 colw=3,先扫 R_p 看失败率悬崖移动方向,再回头调 c。

colw 的另一个坑是列重 2。列重 2 的 LDPC 在 BEC 剥皮下会形成大量停止集:剥到最后,剩余未恢复的变量和校验行构成环状二部图,没有任何行只剩 1 个未知。验证方法很简单:同一组参数分别跑 colw=2 和 colw=3,各 200 次试验,colw=2 的失败率会高出一到两个数量级,而且提高 ε 也压不下来——这是停止集在起作用,不是参数没调好。

4.3 仿真发散排查:剥皮提前卡死的四个顺序检查

喷泉码仿真里的“不收敛”,几乎都是剥皮循环在还剩 NaN 时退出。按下面的顺序排查。

第一,擦除标记。擦除必须用 NaN,不能用 0。真实信道里收到的符号只有 0/1,0 是合法值,把擦除写成 0 会让译码器把大量未知当已知,失败率假性下降且恢复内容错误。检查方式是统计x_hat中 NaN 数量的同时,核对已恢复符号与源符号是否逐位一致。

第二,随机流互相污染。randpermrandirand共享全局随机流,编码器里新增一个抽样调用,信道擦除模式就全变了。固定做法是把三个随机流拆开:

s_deg = RandStream('mt19937ar', 'Seed', 11); % 度分布采样 s_nbr = RandStream('mt19937ar', 'Seed', 22); % 邻居选择 s_chn = RandStream('mt19937ar', 'Seed', 33); % 信道/接收符号选择 % 度采样用 rand(s_deg, 1),邻居用 randperm(s_nbr, kprime, d) % 接收符号选择用 tmp = randperm(s_chn, pool); pick = tmp(1:m_recv);

这样改参数做 A/B 对比时,只有你想变的随机性在变。

第三,邻居抽样必须是无放回。用randi放回抽样时两个邻居可能相同,实际度小于名义度,等效于度分布整体向小度方向偏移,剥皮动力不足。统一用randperm(kprime, d)

第四,校验矩阵自身缺陷。检查 H 是否有全零行、全零列,以及 B 第一校验列权重退化为 1 的情形。全零行意味着一个校验约束不存在,全零列意味着某个中间符号永远不被预编码约束,其可靠性被降回纯 LT。

提示:把预编码层“关掉”做对照是最快的排查动作。raptor_bp_decode里 H 传空矩阵、kprime 传 k,走同一条剥皮路径,就是纯 LT。如果纯 LT 能恢复而加了预编码反而失败,问题一定出在 H 构造或编码一致性上;如果两者都失败,问题在度分布或擦除标记。

最后补一个统计层问题:失败率仿真要跑足试验次数。目标失败率 1e-2 时,200 次试验平均只能看到 2 次失败,置信区间很宽,曲线抖动得像“随参数发散”。习惯上每档参数至少跑 500 次,或固定跑到 20 次失败再停;对比参数时用同一批试验编号。

5. 验证Raptor码仿真结果的三个自查技巧:消元对照、随机流与曲线口径

5.1 小码长下用GF(2)消元对照剥皮译码

剥皮算法实现容易出隐蔽错误,先别急着跑 500 次蒙特卡洛。取 k=16、m=4,把每个输出符号的异或关系写成 GF(2) 系数矩阵 G_out(行对应输出符号,列对应中间符号),整个链路变成 G_out·x = y。对收到的方程做高斯消元,解出的 x 应该和raptor_bp_decode的结果完全一致。只要小 k 下两者吻合,译码器逻辑基本可信,放大 k 时才不会把实现 bug 误判成码的性能问题。

function x = gf2_gauss(A, y) % A: m x n 系数矩阵(GF(2)),y: m x 1;未确定的位置返回 NaN [m, n] = size(A); M = [double(mod(A,2)), double(mod(y,2))]; r = 1; for c = 1:n p = find(M(r:m, c) == 1, 1); if isempty(p), continue; end p = p + r - 1; M([r p], :) = M([p r], :); for i = 1:m if i ~= r && M(i, c) M(i, :) = mod(M(i, :) + M(r, :), 2); end end r = r + 1; if r > m, break; end end x = nan(n, 1); for c = 1:n rr = find(M(1:m, c) == 1); if numel(rr) == 1 x(c) = M(rr, end); end end end

对照时让接收符号数取 3k',此时 G_out 几乎必然满秩,消元能恢复全部中间符号。另外单独加一句assert(~mod(H * x, 2))验证预编码编码器本身满足校验约束,把“编码写错”和“译码写错”这一层先分开。

5.2 独立随机流做A/B对比,把调参效果和随机波动分开

把整条链路包成一个run_raptor_case(k, pre_rate, c, delta, eps, seeds)函数,内部用三个独立RandStream分别管度、邻居和信道。跑参数对比时,保持另外两组 seed 不变,只改目标参数;同一参数重复 5 组 seed,取失败率的区间而不是单点值。这样“把 c 从 0.03 改成 0.02 后失败率变高”到底是参数影响还是随机波动,一眼能判断。

5.3 开销-失败率曲线的两个易错点

画 pfail 对 ε 的曲线时先检查两个口径。横轴必须换算成相对源符号的总开销 ε_total = (1+ε_lt)/R_p - 1,否则预编码码率不同时曲线不能放在同一张图里对比。纵轴用“源符号恢复出错的比例”,而不是“至少一个中间符号未恢复的比例”,因为 LT 层和 LDPC 层的失败在预编码码率不同时会互相掩盖。正确做法是最终把src_hatsrc逐位比对,失败判定一律以源符号为准。

曲线形态上,Raptor 应该在某个 ε 阈值后呈悬崖式下降,然后进入平层。看到线性慢降时,先查 c 是否太小,再查 H 是否有全零列,最后用 5.1 的小码长对照筛掉实现层 bug。平层高度就是该参数组合下的错误底(error floor),真实系统里由 LDPC 停止集决定。仿真里区分“底来自 LDPC 还是 LT 残余”有个技巧:把预编码码率提到 0.98,如果平层不降反升,说明问题出在 LT 层残余过多;如果平层下降,说明问题出在预编码层的停止集。把这三步写进仿真脚本当固定检查项,之后换信道模型、加码长扫描时,才有底气把曲线上的每个拐点解释成编码结构本身的行为,而不是脚本或随机性带进来的假象。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询