ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

最小距离准则与汉明码纠错:从原理到Python仿真

最小距离准则与汉明码纠错:从原理到Python仿真 1. 先搞懂最小距离准则到底在解决什么问题1.1 从一次真实的数据传输说起我在做信道编码仿真时遇到过一件特别典型的事。发送端用一组(7,4)汉明码发数据接收端收到一个码字之后我拿着它跟码表里的16个合法码字挨个比对发现它跟其中一个码字的汉明距离是1跟另一个合法码字距离是2。这个时候问题来了接收端到底该把它判成谁很多刚接触信息论与编码的同学会把这一步想得很简单——看哪个像就选哪个呗。但“像”这个词在工程里必须量化。接收端不能靠感觉做判决它需要一条明确的、能落地的规则。最小距离准则就是这条规则在所有合法码字里找出与接收向量汉明距离最小的那个判定为发送码字。这篇内容我想跟你聊透三件事最小距离准则的原理推导、它怎么决定一个编码方案的纠错检错上限、以及怎么用Python把这套逻辑从码字生成到译码判决完整跑通。不管你是正在学信息论课程、准备实验报告还是做通信物理层的仿真这套内容都能直接用上。1.2 为什么“距离”能拿来判错先说一个直觉。两个码字之间的汉明距离定义是它们对应位置上不同符号的个数。比如1100和1111第3位和第4位不一样距离就是2。这个定义朴素到什么程度就是一个“数不一样”的操作。但正是这个朴素的计数把通信问题转化成了一个几何问题。你可以把一组码字想象成散布在某个空间里的点信道噪声会把发送的点随机“推”到一个新位置接收端看到的就是被推走后的点。如果噪声不大被推走的点大概率还留在原发送点附近。这时候离哪个合法码字最近就判成哪个就是最合理的策略。这个思路最早可以追溯到汉明在贝尔实验室的工作。他在研究继电器计算机的纠错问题时发现用距离来衡量码字之间的区分度比单纯看码率更本质。后来这变成了线性分组码理论的地基一个码的纠错能力不是拍脑袋定的是由码字之间的最小距离决定的。1.3 最小距离准则的适用边界最小距离准则不是万能的。它天然假设信道噪声是随机的、独立的也就是说每个比特出错的概率相同且错误之间没有关联。这在加性白高斯噪声信道下是成立的但放到突发错误信道比如无线通信里的深衰落、磁盘上的划痕最小距离准则就未必最优了那时候得用交织、RS码这类专门对抗突发错误的工具。所以你在用这个准则之前先得搞清楚自己的信道模型是什么样的。我见过不少同学在做实验时直接把最小距离准则硬套到突发错误场景里结果误码率曲线怎么调都下不来不是算法写错了是前提条件就不满足。2. 从最小距离推导纠错检错能力的边界2.1 一个码的“安全半径”怎么定义假设一个分组码的最小距离是d_min。现在接收端收到了一个码字r它跟某个合法码字c1的距离是t。如果t小于某个阈值我们能确定它一定是c1发出来的吗核心逻辑是反证法。如果还存在另一个合法码字c2使得r到c2的距离也小于等于t那r就可能来自c2。为了保证判决唯一必须让r到c1的距离严格小于它到任何其他合法码字的距离。最坏的情况是r正好夹在两个合法码字中间。为了让这种情况不发生两个合法码字之间的最小距离d_min必须足够大。直观想如果r在c1附近c1和c2之间至少要隔开两倍于r偏差的距离才能避免混淆。这就是黄金不等式检错能力e d_min - 1纠错能力t floor((d_min - 1) / 2)。2.2 用图景理解那几个公式我画过很多次图来解释这两个公式每次都发现图形比文字好用得多。你把两个合法码字想象成两个圆心它们之间的距离是d_min。每个码字周围画一个“保护圈”圈的半径代表纠错能力t。只要噪声把接收向量推到离某个码字距离不超过t的范围内就可以安全地落在它的圈里不会被其他码字的圈抢走。要让这些圈互不重叠两个圆心之间的距离至少要大于2t也就是d_min ≥ 2t 1。解出来就是t floor((d_min - 1) / 2)。检错就简单一些。检错不需要判决只需要发现“出错了”。如果接收向量跟所有合法码字的距离都不为0那必然是出错了。但如果没有检错能力上限万一错误量太大把接收向量推到了另一个合法码字上你就完全发现不了——因为它是个合法码字。这个错误量最少是多少正好是d_min。所以最多能检出的错误数就是d_min - 1。这两个公式是所有线性分组码设计的出发点。你去翻LDPC码、Turbo码的论文里面反复出现的“最小距离”“环长”这些术语本质上都是在想办法提高或保证d_min的可靠性。2.3 不同d_min下的能力对比下面这张表我经常在自己的实验报告里用清晰展示不同最小距离带来的能力差异你可以直接用d_min最多检错数最多纠错数典型码举例100无纠错能力的普通编码210奇偶校验码321(7,4)汉明码431或检2纠1扩展汉明码542BCH码(31,21)763BCH码(15,5)注意到4那一行有点特殊。一个最小距离为4的码如果同时用纠错和检错情况会复杂一些因为你既想纠1位错又想检更多错误这中间有个“分配”问题。一般规则是同时纠错t位和检错e位时需要满足d_min ≥ t e 1且e t。3. 用Python把最小距离准则完整跑通3.1 从汉明距离函数写起看再多的公式也不如写几行代码来得实在。我建议你在实验里先从汉明距离的实现开始。这个函数是整个最小距离准则的基础组件后面所有的判决、误码率统计都要用到它。def hamming_distance(x, y): 计算两个二进制序列的汉明距离 输入可以是list、tuple或numpy数组元素为0/1 if len(x) ! len(y): raise ValueError(两个序列长度不一致无法计算汉明距离) return sum(a ! b for a, b in zip(x, y))如果你用的是numpy还有更简洁的写法import numpy as np def hamming_distance_np(x, y): return np.sum(np.array(x) ! np.array(y))这两个函数本质是一样的。选哪个取决于你的数据规模——如果只是实验级别的码字比对Python原生写法就够了如果要做蒙特卡洛仿真、跑几十万帧数据numpy向量化能省下不少时间。3.2 生成码表并计算最小距离有了汉明距离函数下一步是计算一个码集合的最小距离。这是码的固有属性通常在编码设计阶段就要算清楚。给定生成矩阵G先把所有信息位组合生成对应的码字集合然后两两比较。import itertools import numpy as np def generate_codewords(G): 给定生成矩阵Gk行n列生成所有2^k个合法码字 G: numpy array, shape (k, n) k G.shape[0] info_bits [] for bits in itertools.product([0, 1], repeatk): info_bits.append(np.array(bits)) info_matrix np.array(info_bits) # 二进制矩阵乘法mod 2 codewords (info_matrix G) % 2 return codewords def compute_min_distance(codewords): 计算码集合的最小汉明距离 codewords: numpy array, shape (2^k, n) num_cw codewords.shape[0] min_d float(inf) for i in range(num_cw): for j in range(i 1, num_cw): d hamming_distance_np(codewords[i], codewords[j]) if d min_d: min_d d return min_d这里有个细节需要注意遍历的时候是j从i1开始不是从0开始。一个是避免重复计算另一个是避免自己跟自己比较——自己跟自己距离当然是0会直接把最小距离算成0这是个特别容易踩的坑。我见过不少同学的实验报告里最小距离写成0就是忘了跳过对角线。3.3 实现硬判决最小距离译码器接下来是最核心的部分最小距离译码器。它的逻辑特别直白——接收端收到一个可能有错误的向量r遍历码表里所有合法码字计算与r的汉明距离取距离最小的那个作为译码输出。def minimum_distance_decode(received, codewords): 最小距离译码器 received: 接收向量list或numpy array长度为n codewords: 合法码字集合shape (2^k, n) 返回译码得到的码字和信息位 min_d float(inf) best_codeword None best_index 0 for i, cw in enumerate(codewords): d hamming_distance_np(received, cw) if d min_d: min_d d best_codeword cw best_index i return best_codeword, best_index, min_d如果出现两个合法码字距离相同怎么办严格说这就是发生了不可检测的混淆错误属于信道纠错能力之外的情况。但工程实现上必须给出一个确定的行为我的习惯是取第一个遇到的最小值也就是代码里的而不是。在实际译码时通常还需要把码字映射回信息位。如果用查表法最简单的做法是维护一个字典码字→信息位。但如果码表太大比如(31,21)BCH码有200多万个码字查表就不太现实了。这时候可以从生成矩阵的结构上想办法把系统码的信息位直接截出来——系统码的前k位就是信息位直接取前k位即可。3.4 完整的BPSK最小距离译码仿真现在把整个流程串起来。我以(7,4)汉明码为例完整实现一遍BPSK调制下经过AWGN信道后用最小距离准则做硬判决译码的仿真代码。这部分代码你可以在实验里直接复用也可以稍微改改参数跑别的码。import numpy as np import matplotlib.pyplot as plt # ---------- 1. 汉明码(7,4)生成矩阵 ---------- G np.array([ [1, 0, 0, 0, 1, 1, 0], [0, 1, 0, 0, 1, 0, 1], [0, 0, 1, 0, 0, 1, 1], [0, 0, 0, 1, 1, 1, 1] ]) def generate_codewords(G): k G.shape[0] info_bits [] for bits in itertools.product([0, 1], repeatk): info_bits.append(np.array(bits)) info_matrix np.array(info_bits) codewords (info_matrix G) % 2 return codewords, info_matrix codewords, info_bits generate_codewords(G) n G.shape[1] k G.shape[0] num_cw codewords.shape[0] # ---------- 2. 调制映射0 - 1, 1 - -1 ---------- def modulate(bits): return 1 - 2 * bits.astype(float) # ---------- 3. 加噪信道 ---------- def add_awgn(signal, snr_db): snr_db: 每比特信噪比 Eb/N0单位dB 注意这里按exp(-Eb/N0)方式加噪声 snr_linear 10 ** (snr_db / 10) noise_var 1 / (2 * snr_linear) noise np.sqrt(noise_var) * np.random.randn(*signal.shape) return signal noise # ---------- 4. 硬判决把接收信号映射回0/1 ---------- def hard_decision(received_signal): return (received_signal 0).astype(int) # ---------- 5. 仿真主循环 ---------- def run_simulation(G, codewords, info_bits, snr_db_list, num_frames100000): n G.shape[1] k G.shape[0] num_cw codewords.shape[0] # 构建码表映射码字-信息位 codeword_to_info {tuple(cw): info for cw, info in zip(codewords, info_bits)} ber_list [] for snr_db in snr_db_list: errors 0 total_bits 0 for _ in range(num_frames): # 随机选一个信息位组合 idx np.random.randint(0, num_cw) info info_bits[idx] cw codewords[idx] # BPSK调制 tx_signal modulate(cw) # 过信道 rx_signal add_awgn(tx_signal, snr_db) # 硬判决 rx_bits hard_decision(rx_signal) # 最小距离译码 decoded_cw, _, _ minimum_distance_decode(rx_bits, codewords) # 还原信息位 decoded_info codeword_to_info.get(tuple(decoded_cw)) if decoded_info is None: # 理论上不会发生但为了健壮性保留 decoded_info np.zeros(k, dtypeint) # 统计误码 errors np.sum(decoded_info ! info) total_bits k ber errors / total_bits ber_list.append(ber) print(fEb/N0 {snr_db} dB, BER {ber:.2e}) return ber_list # ---------- 6. 运行并绘图 ---------- snr_db_list np.arange(0, 9, 1) ber_list run_simulation(G, codewords, info_bits, snr_db_list) plt.figure(figsize(8, 6)) plt.semilogy(snr_db_list, ber_list, o-, labelHamming(7,4) Minimum Distance) plt.grid(True, whichboth, ls--) plt.xlabel(Eb/N0 (dB)) plt.ylabel(BER) plt.title(Hamming(7,4) BER Performance under AWGN) plt.legend() plt.show()3.5 实验结果的解读方法跑完仿真你手上会得到一条误码率曲线。怎么判断结果对不对可以从三个角度验证。第一个角度跟理论曲线对比。未编码BPSK的理论误码率是Q(sqrt(2 * Eb/N0))在相同信噪比下汉明码的曲线应该更低大约有0.5到1.5dB的增益。如果你的曲线比未编码还差那一定是实现有问题。第二个角度看错误平层的特征。在高信噪比区域如果曲线斜率变平说明仿真误差或码字自由距不足也可能是蒙特卡洛仿真帧数不够导致误码率统计不可靠。正常情况下误码率应该随信噪比增加而快速下降。第三个角度检查低信噪比区域的行为。在0dB附近信道错误率很高最小距离译码器的性能可能还不如不做编码——这就是“编码增益为负”的现象是正常的。编码的好处主要体现在中等信噪比区间太高或太低反而没有明显优势。4. 实操中的典型问题与排查技巧4.1 最小距离算成0的陷阱前面提过计算最小距离时跳过对角线的问题这里再展开一下。很多人看代码会想我明明从i1开始了怎么还会算错有一种情况是码表本身有重复码字——当生成矩阵的行线性相关时不同的信息位会产生相同的码字两两距离为0这会让d_min算出来等于0。这个情况在LDPC码的H矩阵构造中尤其常见。我在做低密度奇偶校验码的仿真时就遇到过生成矩阵不满秩的情况得出的“码字集合”实际有效码字数量少于2^k个。解决办法是先用线性代数验一下生成矩阵的秩确保它是满秩的如果发现不满秩说明这组基矢量的选取有问题需要重新构造生成矩阵。4.2 一次性查表的性能优化最小距离译码的复杂度是O(2^k * n)k一增大就扛不住了。汉明码(7,4)只有16个码字随便算但BCH(63,45)的码字规模是2^45这根本不可能穷举。所以在工程实践中最小距离译码只是一个基准方案真正的大规模译码用的是代数译码、信度传播译码BP、Viterbi译码这些更高效的方法。不过如果你只是做实验、验证编码理论小码规模下查表法仍然是最直观、最不容易出错的方案。我的建议是把码表转换成Python字典键是码字元组值是信息位查表复杂度O(1)比遍历列表快得多。# 构建码表映射 codeword_to_info {tuple(cw): info for cw, info in zip(codewords, info_bits)} # 译码后查表 decoded_info codeword_to_info.get(tuple(decoded_cw))4.3 同步错误的排查实录我遇到过最隐蔽的问题是仿真里解码结果总是错但看不出规律。后来发现是同步问题——发送端发出的是码字但接收端解析时没有对齐到码字边界。比如帧结构里加了同步头但同步头长度跟码字长度不一致导致每帧都错开了一位全部译码失败。排查这类问题的经验是先打印几帧发送和接收的数据人工检查是否对齐。这是最笨但最有效的方法。不要急着看误码率曲线——曲线只会告诉你“有问题”但不会告诉你问题在哪。把数据打出来一帧帧对很快就能发现是错位还是调制映射反了。4.4 浮点比较在硬判决中的坑还有一个特别基础但很常见的坑在硬判决里用if received_signal 0判断为0看起来没问题但如果你用浮点数做计算有个边界点是received_signal 0。这个概率理论上为0但在仿真中可能因为量化或随机数生成恰好出现。我在代码里习惯用(received_signal 0)判1否则判0把恰好等于0的情况明确归到0这一侧避免任何未定义的边界行为。4.5 蒙特卡洛仿真的帧数选择误码率仿真的帧数直接决定结果的可信度。如果误码率目标是1e-4你至少要跑50万帧才能统计到几十个错误事件这样曲线才平滑。我建议按“目标误码率倒数 × 20”来估算最少帧数。比如想测到1e-5那就跑200万帧以上。当然帧数越多跑得越慢这时候可以用numpy批量生成向量替代逐帧循环能快几十倍。5. 个人实操中的几点体会最后说几句我做完整个实验后的感受希望能帮你少走点弯路。第一最小距离准则看起来只是一个“取最小值”的规则但真正把它和汉明距离、纠错检错边界、生成矩阵这些概念串起来之后信息论与编码里很多抽象的定义会瞬间变得具体。我强烈建议你自己从生成矩阵出发手动枚举几百个码字计算它们的距离矩阵你会发现d_min在码本中的位置——它是最小值但更是那个决定整个码性能下限的关键点。第二想验证自己的理解是否到位有个很有效的测试方法拿两个生成矩阵一个是(7,4)汉明码的标准形式一个是故意构造的弱码比如只有奇偶校验能力的码分别算最小距离、跑误码率曲线。两者差距会让你非常直观地体会到编码增益不是来自“多加了几个校验位”而是来自码字之间被刻意拉开的距离。第三Python的仿真环境搭建到这里其实已经够用了。你如果后面想继续深入可以用commpy这类第三方库直接生成更多类型的编解码器或者把最小距离准则跟软判决的信息传递结合实现类BCJR的软输出算法。但不管走多远硬判决的最小距离译码始终是理解整套体系的起点。基础打牢了后面学Turbo码、LDPC码都会轻松很多。这个方向你如果还想继续往下做建议下一步试试将最小距离准则扩展到软判决场景即不先做硬判决而是直接比较接收信号与每个码字调制符号之间的欧氏距离。你会发现软判决的增益一般比硬判决高出约2dB。这是另一个很有意思的话题也是从“基础理论”跨到“工程实现”的一个很自然的台阶。
返回列表