☰
Raptor码LDPC预编码仿真:从结构拆解到参数调优避坑指南
2026/10/1 12:14:03 网站建设 项目流程

简介:这份资源面向通信工程、信道编码方向的学习者与研究人员,聚焦Raptor码的MATLAB仿真实现,帮助理解无速率编码与LDPC预编码的联合工作机制。Raptor码在Fountain码基础上引入LDPC预编码,兼顾自适应性与纠错能力,常用于无线通信、数据存储与网络传输等易丢包场景。资源包共6个文件,以.m脚本与.mat数据文件为主,脚本承担编码、解码与AWGN信道下的仿真流程,数据文件则保存不同信息长度下的校验矩阵与仿真结果,整体约20KB,体积轻便便于快速运行与调试。目前已有536人学习下载。读者可借助这些程序复现误码率与信息长度、编码率之间的关系,观察预编码对整体性能的提升,并在此基础上调整参数、替换信道模型,为课程设计或课题研究提供可复用的仿真框架与排错参考。

1. Raptor码预编码到底在解决什么问题:从一次丢包率压不下去的调试说起

Raptor码是一类近香农限的喷泉码(fountain code),核心能力是「发多少收多少、收够就能还原」,天生适合丢包信道下的前向纠错。但很多人第一次上手 Raptor 码仿真时会发现一个反直觉现象:明明码率已经压得很低,误码率曲线却在某个区间「躺平」不再下降,这就是预编码(precoding)没做对导致的。Raptor 码的经典结构是「LDPC 预编码 + 弱化 LT 码」两级级联,外层 LDPC 负责把少量未解码符号补齐,内层 LT 负责大部分符号的快速恢复。标题里的「LDPC预编码」不是可选项,而是决定 Raptor 码能否逼近理论性能的关键。这篇笔记面向做信道编码仿真、想复现 Raptor 码完整链路的人,从结构拆解到 MATLAB/Python 仿真参数,把能抄的步骤和踩过的坑都写清楚。

2. Raptor码两级结构拆解:LDPC预编码和LT码各自管什么

2.1 为什么单靠LT码不够:度数分布的天花板

LT 码的解码依赖置信传播(BP),而 BP 在解码后期会遇到「停止集」——剩余未解码符号之间互相依赖,谁也解不出来。理想孤波分布(Ideal Soliton)和鲁棒孤波分布(Robust Soliton)能保证平均度数合理,但无法保证零停止集。当输入符号数 k 增大,停止集出现的概率上升,解码失败率就卡在一个下界。这就是为什么纯 LT 码的误码率曲线会在高信噪比处出现「error floor」。

Raptor 码的思路是:既然 LT 解码后总会剩下一小撮符号解不出来,那就用一层 LDPC 预编码把这些符号「兜住」。预编码把原始 k 个信息符号先扩展成 k+r 个中间符号,LT 码对这 k+r 个中间符号编码。解码时先跑 LT 的 BP,剩下的少量未解符号交给 LDPC 解码器,LDPC 的迭代译码能力足以处理这部分残差。两级配合,error floor 被压到极低。

常见做法是预编码用 (k+r, k) 的 LDPC 码,码率略低于 1,r 通常取 k 的 2%~5%。这个比例不是拍脑袋定的:r 太小,LDPC 纠不过来 LT 的残差;r 太大,等效码率下降,频谱效率吃亏。

2.2 预编码矩阵怎么构造:稀疏校验矩阵的三个约束

LDPC 预编码的核心是一个稀疏校验矩阵 H,维度 r×(k+r)。构造时有三条硬约束:

第一,行重和列重要控制在合理范围。列重一般 2~3,行重 3~6,太密了 BP 迭代复杂度上不去,太稀了纠错能力不够。第二,H 要避免短环(girth ≥ 6),4 环会让 BP 消息在环内反复震荡,译码性能急剧恶化。第三,H 对应的 Tanner 图要尽量连通,避免出现孤立子图。

我一般用 PEG(Progressive Edge Growth)算法生成 H,它在保证 girth 的同时尽量让度数分布均匀。也可以用准循环(QC-LDPC)结构,把 H 做成循环移位矩阵块,硬件实现时省存储。仿真阶段用 PEG 就够了,代码量小、性能好。

import numpy as np def build_ldpc_parity_matrix(k, r, col_weight=3, seed=42): """ 构造 (k+r, k) LDPC 预编码的稀疏校验矩阵 H H 维度: r x (k+r) col_weight: 每列非零元素个数 """ np.random.seed(seed) n = k + r H = np.zeros((r, n), dtype=int) # 每列随机放 col_weight 个 1,尽量分散在不同行 for col in range(n): rows = np.random.choice(r, size=col_weight, replace=False) H[rows, col] = 1 # 检查是否有全零行(会导致该校验方程失效) for row in range(r): if H[row].sum() == 0: # 随机挑一列补一个 1 col = np.random.randint(n) H[row, col] = 1 return H def check_girth4(H): """检测 H 中是否存在 4 环: 两行两列交叉处全为 1""" r, n = H.shape count4 = 0 for i in range(r): for j in range(i+1, r): common = np.sum(H[i] & H[j]) if common >= 2: count4 += 1 return count4 k, r = 1000, 50 H = build_ldpc_parity_matrix(k, r) print(f"H shape: {H.shape}, 4-ring count: {check_girth4(H)}")

上面这段代码是快速验证用的随机构造,逻辑是逐列放置非零元素,然后检查全零行并修补。col_weight控制列重,seed保证可复现。check_girth4统计 4 环数量,如果输出大于 0,说明需要换 seed 或改用 PEG 算法重新生成。实际仿真中 k 可能上万,这个 O(r²) 的检测会慢,建议只在 k<2000 时用,大尺寸直接上 PEG。

参数说明:k是信息符号数,r是预编码冗余符号数,col_weight建议 2~3,seed随便设但固定住方便对比。如果 4 环数量超过 r 的 10%,这个 H 基本不能用,译码会明显变差。

2.3 LT码度数分布:鲁棒孤波分布的参数怎么调

LT 码的编码器按度数分布随机选 d 个中间符号做异或,生成一个编码符号。度数分布直接决定解码效率和开销。鲁棒孤波分布在理想孤波基础上加了一个修正项,参数 c 和 δ 控制鲁棒性。

常用取值:c=0.03~0.1,δ=0.01~0.5。c 越大,高度数符号越多,解码启动快但开销大;δ 越小,解码失败概率越低但分布尾部越重。我一般从 c=0.05、δ=0.05 起步,根据仿真曲线微调。

def robust_soliton_distribution(k, c=0.05, delta=0.05): """生成鲁棒孤波分布的度数概率表""" # 理想孤波分布 rho = np.zeros(k+1) rho[1] = 1.0 / k for d in range(2, k+1): rho[d] = 1.0 / (d * (d-1)) # 鲁棒修正项 R = c * np.log(k / delta) * np.sqrt(k) tau = np.zeros(k+1) for d in range(1, int(k/R)+1): tau[d] = R / (d * k) # 归一化 mu = rho + tau mu = mu / mu.sum() return mu k = 1000 mu = robust_soliton_distribution(k, c=0.05, delta=0.05) print(f"度数1概率: {mu[1]:.4f}, 度数2概率: {mu[2]:.4f}, 度数3概率: {mu[3]:.4f}")

这段代码先算理想孤波分布rho,再叠加鲁棒修正tau,最后归一化得到mu。R是修正项的尺度因子,决定了多少低度数符号被「加强」。c和delta的取值直接影响mu的形状:c 大则 R 大,低度数符号概率上升,解码启动更快但平均度数也上升。

实际编码时,按mu采样一个度数 d,然后从 k+r 个中间符号里随机选 d 个做异或。注意采样要用累积分布做逆变换,别直接用np.random.choice按概率选,那个在大 k 时慢得离谱。

3. 从零跑通Raptor码仿真:编码、信道、解码三段链路

3.1 编码端:预编码加LT编码的完整流程

编码分两步。第一步,把 k 个信息符号通过 LDPC 预编码生成 k+r 个中间符号。第二步,对中间符号做 LT 编码生成任意数量的编码符号。

LDPC 预编码的编码过程是解校验方程:已知信息位,求校验位。因为 H 是稀疏的,可以用高斯消元或者迭代法。仿真阶段 k 不大,直接高斯消元就行。把 H 分成 [H_info | H_parity],信息位乘 H_info 得到伴随式,再解 H_parity * parity = syndrome。

def ldpc_precode(info_bits, H, k, r): """ LDPC 预编码: 输入 k 位信息,输出 k+r 位中间符号 H: r x (k+r) 校验矩阵 """ n = k + r H_info = H[:, :k] H_parity = H[:, k:] # 计算伴随式 syndrome = (H_info @ info_bits) % 2 # 解 H_parity * parity = syndrome (模2高斯消元) parity = gf2_solve(H_parity, syndrome) return np.concatenate([info_bits, parity]) def gf2_solve(A, b): """模2高斯消元解 A x = b""" A = A.copy().astype(int) b = b.copy().astype(int) rows, cols = A.shape for col in range(cols): pivot = None for row in range(col, rows): if A[row, col] == 1: pivot = row break if pivot is None: continue A[[col, pivot]] = A[[pivot, col]] b[[col, pivot]] = b[[pivot, col]] for row in range(rows): if row != col and A[row, col] == 1: A[row] = (A[row] + A[col]) % 2 b[row] = (b[row] + b[col]) % 2 return b[:cols]

ldpc_precode把 H 拆成信息部分和校验部分,先算伴随式再解方程。gf2_solve是标准的模2高斯消元,注意交换行时 b 也要跟着换。这段代码在 k=1000、r=50 时跑一次大概几十毫秒,仿真够用。如果 k 上万,建议改用稀疏矩阵库或者直接上 QC-LDPC 的快速编码。

LT 编码更简单:按度数分布采样 d,随机选 d 个中间符号异或。

def lt_encode(intermediate, mu, num_symbols): """LT 编码: 从中间符号生成 num_symbols 个编码符号""" k_plus_r = len(intermediate) degrees = np.arange(1, k_plus_r+1) encoded = [] for _ in range(num_symbols): d = np.random.choice(degrees, p=mu[1:k_plus_r+1]/mu[1:k_plus_r+1].sum()) idx = np.random.choice(k_plus_r, size=d, replace=False) sym = np.bitwise_xor.reduce(intermediate[idx]) encoded.append(sym) return np.array(encoded)

mu是上一步算的度数分布,num_symbols一般取 k 的 1.05~1.2 倍,留点开销余量。np.bitwise_xor.reduce对选中的符号做异或,这就是 LT 编码的全部。

3.2 信道仿真:删除信道和AWGN信道怎么设

Raptor 码最典型的应用场景是删除信道(BEC),比如网络传输丢包。仿真时按概率 p 随机删除编码符号,接收端只知道哪些收到了。BEC 下 Raptor 码的性能用开销(overhead)衡量:接收符号数 / 信息符号数,理想情况接近 1。

AWGN 信道下要先把比特映射成 BPSK 符号(0→-1,1→+1),加高斯噪声,接收端做软判决。Raptor 码在 AWGN 下需要软信息译码,比 BEC 复杂不少。建议先跑通 BEC 再上 AWGN。

def bec_channel(encoded, erasure_prob): """删除信道: 按概率擦除符号""" mask = np.random.rand(len(encoded)) > erasure_prob received = encoded.copy() received[~mask] = -1 # -1 标记擦除 return received, mask def awgn_channel(encoded, snr_db): """AWGN 信道: BPSK + 高斯噪声""" symbols = 1 - 2 * encoded # 0->1, 1->-1 snr_linear = 10 ** (snr_db / 10) noise_std = np.sqrt(1 / (2 * snr_linear)) noise = np.random.normal(0, noise_std, len(symbols)) return symbols + noise

bec_channel用 -1 标记擦除位置,解码时跳过这些符号。awgn_channel做 BPSK 映射后加噪声,snr_db是每比特信噪比。注意noise_std的推导:BPSK 符号能量为 1,噪声方差为 1/(2*SNR),开根号得到标准差。

3.3 解码端:BP迭代加LDPC补译的联合流程

解码是 Raptor 码最复杂的部分。流程是:先在 LT 码的 Tanner 图上跑 BP,能解多少解多少;剩下的未解符号连同它们的校验关系,交给 LDPC 解码器处理。

LT 的 BP 解码用「剥离」算法:找到度数为 1 的编码符号,它的唯一邻居就是解,把这个解传播给所有包含它的编码符号,降低它们的度数,重复直到没有度数为 1 的符号。BEC 下这个过程是确定性的,AWGN 下要用软信息迭代。

def lt_decode_bec(received, mask, neighbor_lists, k_plus_r): """ BEC 下 LT 码的剥离解码 neighbor_lists: 每个编码符号对应的中间符号索引列表 """ decoded = np.full(k_plus_r, -1) # -1 表示未解 # 建立反向索引: 每个中间符号被哪些编码符号引用 from collections import defaultdict rev_index = defaultdict(list) for enc_idx, neighbors in enumerate(neighbor_lists): if not mask[enc_idx]: continue for n in neighbors: rev_index[n].append(enc_idx) # 剥离 from collections import deque queue = deque() for enc_idx, neighbors in enumerate(neighbor_lists): if mask[enc_idx] and len(neighbors) == 1: queue.append(enc_idx) while queue: enc_idx = queue.popleft() neighbors = [n for n in neighbor_lists[enc_idx] if decoded[n] == -1] if len(neighbors) != 1: continue n = neighbors[0] decoded[n] = received[enc_idx] for other_enc in rev_index[n]: if other_enc == enc_idx or not mask[other_enc]: continue remaining = [x for x in neighbor_lists[other_enc] if decoded[x] == -1] if len(remaining) == 1: queue.append(other_enc) return decoded

lt_decode_bec用队列驱动剥离过程。rev_index记录每个中间符号被哪些编码符号引用,剥离时快速找到受影响的编码符号。decoded里 -1 表示未解,这些位置就是 LDPC 预编码要补的。

LDPC 补译时,把已解的中间符号当作已知,未解的当作未知,用 H 矩阵建立方程,跑 BP 或者高斯消元。因为残差通常只有几个到几十个符号,高斯消元完全够用。

def ldpc_repair(decoded, H, k, r): """用 LDPC 校验矩阵补译未解符号""" n = k + r unknown_idx = np.where(decoded == -1)[0] if len(unknown_idx) == 0: return decoded # 提取涉及未知符号的校验方程 A = H[:, unknown_idx] b = np.zeros(H.shape[0], dtype=int) for row in range(H.shape[0]): known_sum = 0 for col in range(n): if decoded[col] != -1 and H[row, col] == 1: known_sum ^= decoded[col] b[row] = known_sum # 解 A x = b x = gf2_solve(A, b) decoded[unknown_idx] = x[:len(unknown_idx)] return decoded

ldpc_repair把 H 中对应未知符号的列抽出来组成 A,已知符号的贡献算进 b,然后解方程。如果 A 的秩不够,说明有符号真的解不出来,这时候只能靠增加接收符号数重试。

4. 仿真参数怎么设:开销、度数分布、码长的联动关系

4.1 开销与解码成功率:1.05倍不是万能值

开销定义为接收编码符号数除以信息符号数。理论上 Raptor 码在开销 1.0 时就能解码,但实际因为停止集和 LDPC 残差,需要留余量。常见做法是 1.05~1.15。

这个余量跟 k 强相关:k 小的时候(几百),停止集影响大,开销要 1.1 以上;k 大的时候(上万),大数定律让度数分布更稳定,1.02~1.05 就够。我做过一组对比,k=500 时开销 1.05 的解码成功率只有 85%,加到 1.1 才到 99%;k=5000 时 1.05 就能到 99.9%。

k推荐开销解码成功率目标
5001.1099%
10001.0899%
50001.0599.9%
100001.0399.9%

这张表是仿真经验值,具体还要看度数分布和 LDPC 的 r/k 比例。如果发现开销加到 1.2 还是解不出来,别再加了,大概率是 H 矩阵有 4 环或者度数分布参数不对。

4.2 度数分布参数c和δ的敏感度分析

c 和 δ 对性能的影响不是线性的。c 从 0.03 加到 0.1,平均度数上升约 30%,解码启动更快但开销增加 2%~5%。δ 从 0.5 降到 0.01,解码失败概率降低但分布尾部变重,平均度数也上升。

我一般固定 δ=0.05,然后扫 c。c=0.03 时开销最低但失败率偏高,c=0.05 是平衡点,c=0.1 适合对可靠性要求极高的场景。如果仿真发现解码成功率对 c 特别敏感,说明 k 太小,度数分布的随机性主导了性能,这时候应该增大 k 而不是调 c。

4.3 码长k的选择:短码和长码的仿真差异

k 小于 500 时,Raptor 码的性能波动很大,同一组参数跑两次结果可能差 5%。这是因为小 k 下度数分布的采样噪声大,停止集的出现概率也高。如果必须用短码,建议把 r/k 提高到 5%~10%,用更强的 LDPC 预编码兜底。

k 大于 5000 后,性能曲线变得平滑,开销接近理论值。但仿真时间会显著上升,BP 解码的复杂度是 O(k·平均度数),k=10000 时一次解码可能要几秒。建议仿真时先用 k=1000 调参,确定参数后再上大 k 验证。

5. Raptor码仿真的避坑记录:从曲线不下降到解码卡死

5.1 误码率曲线在高信噪比处躺平

现象:AWGN 下仿真,SNR 超过 6dB 后误码率不再下降,卡在 1e-4 左右。

原因:LDPC 预编码的 r 太小,LT 解码后的残差超过了 LDPC 的纠错能力。或者 H 矩阵有 4 环,BP 译码在环内震荡,无法收敛。

解决:先把 r/k 从 2% 提高到 5%,看曲线是否继续下降。如果还不行,检查 H 的 4 环数量,用 PEG 算法重新生成。另外确认 LDPC 解码的迭代次数够不够,仿真阶段设 50 次以上。

5.2 解码过程中队列为空但还有未解符号

现象:LT 剥离解码跑完,队列空了,但 decoded 里还有 -1。

原因:这是正常的,停止集出现了。但如果未解符号数量超过 r 的 80%,LDPC 也补不过来。

解决:增加接收符号数(提高开销),或者调整度数分布让高度数符号多一点(增大 c)。如果每次都是固定那几个符号解不出来,检查 H 矩阵对应的列是不是度数太低。

5.3 高斯消元解LDPC方程时秩不足

现象:gf2_solve返回的解代回方程不满足,或者 A 矩阵的秩小于未知数个数。

原因:H 矩阵的校验部分 H_parity 不满秩,导致编码时校验位不唯一。或者补译时抽取的 A 矩阵列相关性太强。

解决:构造 H 时确保 H_parity 满秩,可以用随机搜索换 seed 直到满秩。补译时如果 A 秩不足,说明这些符号真的无法恢复,只能增加开销重传。

5.4 仿真跑得越来越慢最后卡死

现象:k=5000 时仿真跑了几分钟没出结果,内存占用飙升。

原因:neighbor_lists用 Python list 存储,每个编码符号的邻居列表平均长度几十,总内存 O(num_symbols·平均度数)。num_symbols 取 1.1k 时就是 5500 个列表,每个几十个整数,内存还好。但如果用np.random.choice逐符号采样度数,在大 k 下极慢。

解决:度数采样改用累积分布逆变换,预计算 cdf 然后二分查找。neighbor_lists用 numpy 数组存储,或者直接用稀疏矩阵。BP 解码的内层循环用向量化替代 Python for 循环。

def fast_degree_sample(mu, num_samples): """累积分布逆变换快速采样度数""" cdf = np.cumsum(mu) cdf = cdf / cdf[-1] u = np.random.rand(num_samples) degrees = np.searchsorted(cdf, u) + 1 return degrees

fast_degree_sample预计算 cdf,然后用searchsorted做二分查找,比逐个np.random.choice快两个数量级。mu是度数分布,num_samples是要生成的编码符号数。注意searchsorted返回的索引从 0 开始,度数从 1 开始,所以加 1。

5.5 LDPC补译后仍有符号错误但数量很少

现象:解码完成后对比原始信息,发现个别比特翻转,误比特率在 1e-5 量级。

原因:LDPC 补译时高斯消元的数值精度问题,或者 BP 迭代没收敛就输出了硬判决。

解决:补译后加一轮校验,把不满足的校验方程对应的符号标记出来重新译。如果还不行,说明这些符号的置信度太低,只能靠增加开销。仿真阶段可以在 LDPC 解码后加 CRC 校验,失败就丢弃重传。

6. 把Raptor码仿真从能跑推到好用:性能验证和参数固化

仿真跑通只是第一步,真正要用起来还得做两件事:验证性能是否达标,以及把参数固化下来方便复现。

性能验证我一般看三条曲线:解码成功率 vs 开销、误比特率 vs SNR、平均解码迭代次数 vs 开销。第一条曲线告诉你需要多少冗余,第二条告诉你抗噪能力,第三条告诉你复杂度。三条曲线都平滑且单调,说明仿真链路没问题。如果某条曲线出现台阶或者震荡,大概率是某个参数没设对。

参数固化建议用配置文件,把 k、r、c、δ、col_weight、seed、开销全部写进去。每次仿真输出一个 JSON 记录参数和结果,方便对比。我吃过亏,有一次调参调好了没记录,过两周想复现死活想不起来当时用的什么值,只能重新扫一遍。

import json def save_sim_config(config, result, filename): """保存仿真配置和结果""" record = { "config": config, "result": { "decode_success_rate": float(result["success_rate"]), "avg_overhead": float(result["avg_overhead"]), "avg_iterations": float(result["avg_iterations"]) } } with open(filename, "w") as f: json.dump(record, f, indent=2) config = { "k": 1000, "r": 50, "c": 0.05, "delta": 0.05, "col_weight": 3, "seed": 42, "overhead": 1.08 } result = {"success_rate": 0.995, "avg_overhead": 1.082, "avg_iterations": 12.3} save_sim_config(config, result, "raptor_sim_k1000.json")

save_sim_config把配置和关键结果存成 JSON,indent=2方便人读。每次改参数就换文件名,别覆盖。这样积累几十组数据后,能直接画出参数-性能的热力图,比瞎调快得多。

最后一个技巧:如果要做硬件实现或者 FPGA 验证,先把 MATLAB/Python 的定点仿真跑通。浮点仿真性能好不代表定点也好,LDPC 的 BP 迭代对量化误差很敏感,建议至少留 6 位小数精度。我一般先用浮点确定算法和参数,再逐步降精度看性能损失,找到性价比最高的位宽。

这些是我做 Raptor 码仿真几年攒下来的习惯,不一定每条都适合你的场景,但按这个流程走至少不会在基础问题上翻车。希望帮到你。

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

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

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

立即咨询