Up

리드-솔로몬 부호와 벌레캠프-매시 알고리즘

2022년 8월 8일

통신 오류

일상 생활에서 대화를 하다보면 주변이 시끄러워서 잘 못 들을 때가 있습니다. 소리 뿐만 아니라 글을 써서 보여줄 때에도 어둡거나 글씨가 악필이면 잘못 읽는 경우가 허다합니다. 우리는 항상 통신 오류에 노출된 셈이죠. 이건 비단 인간만의 문제가 아니라서 전자 기기를 사용하더라도 마찬가지 문제를 겪습니다. 무선 통신은 당연하고(엘리베이터에서 통화하는 상황을 생각해봅시다), 아무리 단단하게 케이블을 고정해놓은 유선 통신이라도 통신 오류가 절대 안 생길 수는 없습니다.

결국 오해의 소지 없이 통신을 하려면 오류가 아예 안 생긴다고 기대하는 것보다는 오류가 생겼다는 사실을 적절히 알아채서 대응하는 것이 중요합니다. 이 글에서는 통신 오류를 어떻게 검출하고 대응하는지 살펴보겠습니다.

오류 검출

대부분의 오류 검출은 데이터에 오류를 검출할 수 있는 정보를 추가하는 방식으로 이루어집니다. 가장 간단한 방법은 패리티 비트(parity bit)라는 것으로, 데이터에 포함된 1의 개수가 홀수인지 짝수인지에 따라 1비트를 추가하는 방식입니다. 조금 더 복잡한 방법으론 데이터를 몇 바이트씩 묶은 뒤 이들의 합을 추가하는 체크섬(checksum), 데이터를 다항식으로 보고서 이를 주어진 다항식으로 나눈 나머지를 추가하는 CRC(Cyclic Redundancy Check) 등이 있습니다.

오류가 검출되었다면 대응은 크게 두 가지입니다. 첫 번째는 보낸 쪽에 재전송을 요구하는 것입니다. 물론 이 요구조차도 오류가 발생할 수 있다는 점에 주의해야겠죠. 두 번째는 복잡한 알고리즘을 써서 오류를 정정하는 것입니다. 계산량 때문에 전송 속도는 떨어지겠지만 심각할 수준으로 오류가 발생하지만 않는다면 재전송 요구 없이 일방적으로 데이터 전송이 가능합니다.

리드-솔로몬 부호

리드-솔로몬 부호(Reed-Solomon code)는 다항식의 성질을 이용하여 오류를 검출하고 정정하는 알고리즘입니다. 특히 오류가 연속으로 발생하는 군집 오류(burst error)에 대한 정정 능력이 뛰어납니다. CD, DVD, 블루레이, QR 코드, DVB, RAID 등에서 매우 널리 사용되는 알고리즘이기도 합니다.

크기 $q$인 유한체 $GF(q)$의 원소로 이루어진 길이 $k$인 데이터를 전송해야 한다고 가정합시다. 리드-솔로몬 부호는 여기에 길이 $2t$인 오류 검출 및 정정 정보를 추가해 최대 $2t$개의 오류를 검출하고 $t$개의 오류를 정정할 수 있습니다. 전송할 전체 길이는 $n=k+2t$입니다. 단, $n$이 $q$보다 작아야 합니다.

먼저 데이터를 $d_0$, $d_1$, ⋯, $d_{k-1}$라고 하고 이들을 계수로 갖는 유한체 위의 다항식 $p(x)$를 정의합니다.

\[ p(x) = d_0 + d_1 x + d_2 x^2 + \cdots + d_{k-1} x^{k-1} \]

그리고 생성자 다항식(generator polynomial)을 다음과 같이 정의합니다.

\[ g(x) = (x - \alpha^i)(x - \alpha^{i+1})(x - \alpha^{i+2})\cdots(x - \alpha^{i+n-k-1}) = g_0 + g_1 x + g_2 x^2 + \cdots + g_{n-k-1} x^{n-k-1} + x^{n-k} \]

여기서 $g(x)$가 중근을 가지지 않도록 $\alpha$는 유한체의 원시 원소(primitive element)로 둡니다. $\alpha$와 $g(x)$는 복호화를 위해 송신측과 수신측이 공유해야 하는 값입니다. 특별히 $i=1$인 경우를 narrow sense라고 하며, 이하 이를 가정합니다.

최종적으로 길이 $n-k$의 리드-솔로몬 부호 $c_0$, $c_1$, ⋯, $c_{n-k-1}$은 $x^{n-k}p(x)$를 $g(x)$로 나눈 나머지 $c(x)$의 계수입니다.

\[ c(x) = x^{n-k}p(x) \bmod g(x) = c_0 + c_1 x + c_2 x^2 + \cdots + c_{n-k-1} x^{n-k-1} \]

보통은 송신할 데이터와 부호를 하나의 다항식으로 합쳐서 나타냅니다.

\[ s(x) = x^{n-k}p(x) - c(x) \]

$c(x)$는 $n-k$차 미만의 다항식이므로 $s(x)$의 계수는 $p(x)$의 계수와 동일합니다. 한편 $s(x)$가 $g(x)$의 배수임은 쉽게 보일 수 있습니다.

\[ s(x) \equiv x^{n-k}p(x) - c(x) \equiv c(x) - c(x) \equiv 0 \pmod{g(x)} \]
유한체 $GF(17)$에 대해 리드-솔로몬 부호를 계산해봅시다. 모든 연산은 17에 대한 나머지 연산입니다. 데이터의 크기는 $k=8$이고 길이 6의 부호를 붙여서 전체 송신 길이는 $n=14$입니다. 데이터는 다음과 같습니다.
\begin{equation*} \mathbf{d} = [ 5, 6, 13, 0, 7, 15, 1, 4 ] \end{equation*}

즉, 다항식 $p(x)$는

\begin{equation*} p(x) = 5 + 6x + 13x^2 + 7x^4 + 15x^5 + x^6 + 4x^7 \end{equation*}

이고 생성자 다항식은 $\alpha=3$으로 두면

\begin{equation*} \begin{aligned} g(x) &= (x - 3)(x - 3^2)(x - 3^3)(x - 3^4)(x - 3^5)(x - 3^6) \\ &= 5 + 9x + 11x^2 + 15x^3 + x^4 + 13x^5 + x^6 \end{aligned} \end{equation*}

이므로 $x^{n-k}p(x)$를 $g(x)$로 나눈 나머지는

\begin{equation*} c(x) = x^6p(x) \bmod g(x) = 10 + 2x + 12x^2 + 9x^3 + 8x^4 + 8x^5 \end{equation*}

따라서 송신하는 다항식은

\begin{equation*} \begin{aligned} s(x) &= x^6p(x) - c(x) \\ &= x^6(5 + 6x + 13x^2 + 7x^4 + 15x^5 + x^6 + 4x^7) - (10 + 2x + 12x^2 + 9x^3 + 8x^4 + 8x^5) \\ &= 7 + 15x + 5x^2 + 8x^3 + 9x^4 + 9x^5 + 5x^6 + 6x^7 + 13x^8 + 7x^{10} + 15x^{11} + x^{12} + 4x^{13} \end{aligned} \end{equation*}

복호화

수신측에서 다항식 $r(x)$를 받았다고 합시다. 통신 오류가 있다면 $r(x)$는 송신측에서 보낸 다항식 $s(x)$과 다를 것입니다. 복호화의 목표는 오류가 발생한 항과 그 항의 계수를 정정하는 것입니다.

오증

$r(x)$에 생성자 다항식 $g(x)$의 근을 대입하여 얻은 값을 오증(syndrome)이라고 합니다.

\[ \begin{aligned} S_0 &= r(\alpha) \\ S_1 &= r(\alpha^2) \\ &\quad\vdots \\ S_{n-k-1} &= r(\alpha^{n-k}) \end{aligned} \]

그런데 $s(x)$가 $g(x)$의 배수이므로 $r(x) = s(x)$이면 오증은 모두 0이 됩니다. 반대로 0이 아닌 오증이 있는 경우에는 통신 오류가 발생했다고 의심할 수 있겠죠.

예제 1에서 송신한 다항식에서 3차항과 8차항에 오류가 발생하여 $r(x)$가 다음과 같다면
\[ r(x) = 7 + 15x + 5x^2 + 12x^3 + 9x^4 + 9x^5 + 5x^6 + 6x^7 + 2x^8 + 7x^{10} + 15x^{11} + x^{12} + 4x^{13} \]

이때 오증은 0이 아닙니다.

\[ \begin{aligned} S_0 &= r(3) = 0 \\ S_1 &= r(3^2) = 15 \\ S_2 &= r(3^3) = 16 \\ S_3 &= r(3^4) = 5 \\ S_4 &= r(3^5) = 1 \\ S_5 &= r(3^6) = 8 \end{aligned} \]

오류 위치 다항식

오류가 난 항이 $\nu$개이고 각각의 차수가 $i_k$($1\le k \le\nu$)라고 하면 $r(x)$와 $s(x)$의 차는 다음과 같이 표현 가능합니다.

\[ e(x) = r(x) - s(x) = \sum_{k=1}^{\nu} e_{i_k} x^{i_k} \]

오증의 정의를 다시 쓰면

\[ S_j = r(\alpha^{j+1}) = s(\alpha^{j+1}) + e(\alpha^{j+1}) = e(\alpha^{j+1}) = \sum_{k=1}^{\nu} e_{i_k} \alpha^{i_k(j+1)} \]

오류 위치(error locator) $X_k$와 오류값(error value) $Y_k$를 다음과 같이 정의합니다.

\[ \begin{aligned} X_k &= \alpha^{i_k} \\ Y_k &= e_{i_k} \end{aligned} \]

따라서 오증은

\[ S_j = \sum_{k=1}^{\nu} Y_k X_k^{j+1} \label{eq:SXY} \]

$\alpha$는 사전에 공유한 값이므로 $X_k$를 구하면 $i_k$도 구할 수 있습니다. 오류 위치 다항식(error locator polynomial)을 다음과 같이 정의합시다.

\[ \begin{aligned} \Lambda(x) &= \prod_{k=1}^\nu (1-X_kx) \\ &= (1-X_1x)(1-X_2x)\cdots(1-X_\nu x) \\ &= 1 + \Lambda_1 x + \Lambda_2 x^2 + \cdots + \Lambda_\nu x^\nu \end{aligned} \]

정의에 의해 오류 위치 다항식은 $X_k^{-1}$을 해로 가지므로

\[ \Lambda(X_k^{-1}) = 1 + \Lambda_1 X_k^{-1} + \Lambda_2 X_k^{-2} + \cdots + \Lambda_\nu X_k^{-\nu} = 0 \]

양변에 $Y_kX_k^{j+\nu+1}$를 곱합시다.

\[ \begin{aligned} &Y_kX_k^{j+\nu+1} (1 + \Lambda_1 X_k^{-1} + \Lambda_2 X_k^{-2} + \cdots + \Lambda_\nu X_k^{-\nu}) \\ &= Y_kX_k^{j+\nu+1} + \Lambda_1 Y_kX_k^{j+\nu} + \Lambda_2 Y_kX_k^{j+\nu-1} + \cdots + \Lambda_\nu Y_kX_k^{j+1} \\ &= 0 \end{aligned} \]

이를 $k=1$부터 $\nu$까지 더하면

\[ \begin{aligned} &\sum_{k=1}^{\nu} \left( Y_kX_k^{j+\nu+1} + \Lambda_1 Y_kX_k^{j+\nu} + \Lambda_2 Y_kX_k^{j+\nu-1} + \cdots + \Lambda_\nu Y_kX_k^{j+1} \right) \\ &= \sum_{k=1}^{\nu} Y_kX_k^{j+\nu+1} + \Lambda_1 \sum_{k=1}^{\nu} Y_kX_k^{j+\nu} + \Lambda_2 \sum_{k=1}^{\nu} Y_kX_k^{j+\nu-1} + \cdots + \Lambda_\nu \sum_{k=1}^{\nu} Y_kX_k^{j+1} \\ &= S_{j+\nu} + \Lambda_1 S_{j+\nu-1} + \Lambda_2 S_{j+\nu-2} + \cdots + \Lambda_\nu S_j \\ &= 0 \end{aligned} \]

또는

\[ S_{j+\nu} = -\Lambda_1 S_{j+\nu-1} - \Lambda_2 S_{j+\nu-2} - \cdots - \Lambda_\nu S_j \label{eq:recur} \]

즉, 오증의 수열 $S_0$, ⋯, $S_{n-k-1}$이 오류 위치 다항식을 계수로 하는 선형 점화식을 가진다는 놀라운 결과를 얻습니다! 오증은 앞에서 다 구했으니까 적절한 $\nu$를 골라서 적절한 점화식이 성립하는지 확인하면 됩니다. 참고로 오증은 $n-k=2t$개이므로 $\Lambda_k$에 대한 방정식을 $2t-\nu$개 만들 수 있고 $\Lambda_k$는 총 $\nu$개니까 $\Lambda_k$를 유일하게 구할 수 있을 조건은 $\nu \le 2t-\nu$ 또는 $\nu\le t$입니다. 처음에 언급했던 $t$개 이하의 오류만 수정 가능하다는 점이 여기서 증명되죠.

예제 2에서는 오류가 난 항의 개수가 $\nu=2$였습니다. 따라서 아래 식들을 만족하는 $\Lambda_1$과 $\Lambda_2$가 존재해야 합니다.
\[ \begin{aligned} 16 &= -15\Lambda_1 \\ 5 &= -16\Lambda_1 - 15\Lambda_2 \\ 1 &= -5\Lambda_1 - 16\Lambda_2 \\ 8 &= -1\Lambda_1 - 5\Lambda_2 \end{aligned} \]

실제로 $\Lambda_1 = 8$, $\Lambda_2 = 7$이라는 해가 존재합니다.

벌레캠프-매시 알고리즘

벌레캠프-매시 알고리즘(Berlekamp-Massey algorithm)은 유한체 상에서 유한 수열이 주어질 때 그 수열의 최소 선형 점화식을 구하는 알고리즘입니다. 원래 벌레캠프가 발표한 알고리즘은 점화식과 관련 없이 $\nu$의 최솟값과 그 때의 $\Lambda_k$를 찾는 것이었는데, 매시가 위와 같은 과정을 거쳐 벌레캠프의 알고리즘이 오증 수열의 선형 점화식을 찾는 것과 동치임을 증명했습니다. 벌레캠프-매시 알고리즘은 리드-솔로몬 부호의 복호화 과정에서 핵심일 뿐만 아니라 수열과 연관된 다른 곳에서도 많이 쓰입니다.

식 \eqref{eq:recur}에서 점화식의 오차 $d$를 다음과 같이 정의합시다.

\[ d = S_{j+\nu} + \Lambda_1 S_{j+\nu-1} + \cdots + \Lambda_\nu S_j \]

$d$가 0이면 현재 $\nu$와 $j$에 대해 점화식이 잘 성립하는 겁니다. $j$를 1 증가시켜 그 다음 오증에 대해서도 점화식이 성립하는지 확인합니다. 반대로 0이 아니면 점화식, 즉 오류 위치 다항식 $\Lambda(x)$를 수정해야 합니다. 이때 수정된 오차 위치 다항식은 그 이전 오증들에 대해서 점화식을 잘 성립하게 하면서 동시에 현재 오증 $S_{j+\nu}$에 대해서도 점화식이 성립하게 해야 합니다. 벌레캠프-매시 알고리즘은 다음과 같이 오류 위치 다항식을 수정합니다.

\[ \Lambda_\text{new}(x) = \Lambda(x) - dd_\text{old}^{-1} x^{j-f} \Lambda_\text{old}(x) \label{eq:update_lambda} \]

여기서 $\Lambda_\text{old}(x)$는 이전에 사용했던 적당한 오류 위치 다항식이고, $f$는 그 오류 위치 다항식이 점화식을 만족하는 데 실패한 첫 $j$값이며, 그 때의 오차 $d$가 $d_\text{old}$입니다.

새로 구한 오류 위치 다항식은 $S_{j+\nu}$에 대한 점화식을 만족할까요? 정의에 따라 계산해보면

\[ \begin{aligned} d_\text{new} &= S_{j+\nu} + \Lambda_{\text{new},1} S_{j+\nu-1} + \cdots + \Lambda_{\text{new},\nu} S_j \\ &= \underbrace{S_{j+\nu} + \Lambda_1 S_{j+\nu-1} + \cdots + \Lambda_\nu S_j}_d - dd_\text{old}^{-1} (\underbrace{S_{f+\nu} + \Lambda_{\text{old},1} S_{f+\nu-1} + \cdots}_{d_\text{old}}) \\ &= d - dd_\text{old}^{-1} d_\text{old} \\ &= 0 \end{aligned} \]

이므로 잘 만족합니다. 오류 위치 다항식에서 점화식으로 가는 부분이 헷갈릴 수 있는데 계수는 그대로 두고 $x^i$를 $S_{j+\nu-i}$로 바꾼다고 생각하면 편합니다.

한편 이전 오증들에 대해서는 위 식에서 $j$와 $f$ 대신 $j-1$과 $f-1$를 대입한 경우 등과 동치이므로 밑줄로 표시한 부분이 둘 다 0이 되어(이전 오증들에 대해서는 $\Lambda(x)$든 $\Lambda_\text{old}(x)$든 점화식을 잘 만족하니까) 오차가 0입니다. 결과적으로 벌레캠프-매시 알고리즘은 오류 위치 다항식을 수정하는 데 성공했습니다. 이제 $\Lambda(x)$를 업데이트하고 다음 오증으로 넘어가 계속 위 과정을 반복하면 됩니다. 언젠가는 마지막 오증까지 만족하는 점화식을 구하게 되겠죠.

한 가지 문제는 이전에 사용했던 오류 위치 다항식 중 어느 것을 선택해야 좋겠냐는 것인데, 여기서는 최소 선형 점화식을 찾고 싶은 거니까 $\Lambda_\text{new}(x)$의 차수가 최소가 되도록 선택해야 합니다. 식 \eqref{eq:update_lambda}에서 $\Lambda(x)$의 차수는 $\nu$이고, $\Lambda_\text{old}(x)$의 차수를 $\nu_\text{old}$라 할 때 우변 두 번째 항의 차수는 $j - f + \nu_\text{old}$이므로 $\Lambda_\text{new}(x)$의 차수는

\[ \nu_\text{new} = \max(\nu, j - f + \nu_\text{old}) \]

입니다. 이걸 최소로 하려면 $f - \nu_\text{old}$가 가장 큰 이전 오류 위치 다항식을 선택하면 되겠네요. 따라서 오차가 발생한 매 단계에서 새 오류 위치 다항식을 계산한 뒤, $j - \nu$가 $f - \nu_\text{old}$보다 더 크면 $\Lambda_\text{old}(x)$를 $\Lambda(x)$로 업데이트해주면 됩니다.

마지막으로 초기값을 살펴봐야 합니다. 처음 $j_0$개의 오증이 0이고 $S_{j_0}\neq0$이면 선형 점화식의 길이가 $j_0$ 이하는 절대 될 수 없으므로 $\nu$의 초기값은 $j_0+1$이어야 합니다. $\Lambda(x)$의 초기값은 어차피 점화식에 오차가 있으면 수정될 것이므로 사실 상관이 없는데, 굳이 계산을 복잡하게 만들 이유는 없으니

\begin{equation*} \Lambda(x) = 1 + 0x + 0x^2 + \cdots + 0x^{j_0+1} \end{equation*}

로 두는 게 제일 편합니다. 그리고 이전 오류 위치 다항식은

\begin{equation*} \Lambda_\text{old}(x) = 1, \qquad \nu_\text{old} = 0 \end{equation*}

으로 두고, 위치 $f=j_0$에서 오차 $d_\text{old}=S_{j_0}$가 처음 발생했다고 두는 것이 합리적입니다.

예제 3을 벌레캠프-매시 알고리즘으로 풀어보겠습니다. 먼저 $S_0=0$이고 $S_1\neq0$이므로 $j_0=1$이고, 초기값은 다음과 같습니다.
\begin{align*} \nu &= 2 \\ \Lambda(x) &= 1 + 0x + 0x^2 \\ \Lambda_\text{old}(x) &= 1 \\ f &= 1 \\ d_\text{old} &= 15 \end{align*}

먼저 $j=2$에 대해 오차를 계산하면

\begin{equation*} d = S_2 + 0S_1 + 0S_0 = 16 + 0 \cdot 15 + 0 \cdot 0 = 16 \end{equation*}

이므로 오차가 발생하여 오류 위치 다항식을 수정해야 합니다.

\begin{align*} \Lambda_\text{new}(x) &= \Lambda(x) - dd_\text{old}^{-1} x^{j-f} \Lambda_\text{old}(x) \\ &= (1 + 0x + 0x^2) - 16 \cdot 15^{-1} x^{2-1} (1) \\ &= 1 + 8x + 0x^2 \end{align*}

이 다항식으로 점화식을 다시 계산해보면 기대했던 대로 오류가 0이 됩니다.

\begin{equation*} d = S_2 + 8S_1 + 0S_0 = 16 + 8 \cdot 15 + 0 \cdot 0 = 0 \end{equation*}

$j - \nu = 0 \ngtr f - \nu_\text{old} = 0$이므로 $\Lambda_\text{old}(x)$는 업데이트하지 않아도 됩니다. 다음으로 $j=3$일 때 오차는

\begin{equation*} d = S_3 + 8S_2 + 0S_1 = 5 + 8 \cdot 16 + 0 \cdot 15 = 14 \end{equation*}

똑같이 오류 위치 다항식을 수정합니다.

\begin{align*} \Lambda_\text{new}(x) &= \Lambda(x) - dd_\text{old}^{-1} x^{j-f} \Lambda_\text{old}(x) \\ &= (1 + 8x + 0x^2) - 14 \cdot 15^{-1} x^{3-1} (1) \\ &= 1 + 8x + 7x^2 \end{align*}

이 다항식은 나머지 모든 $j$에 대해서도 오차가 0이므로 우리가 구하는 답입니다. 즉, $\nu=2$이고 $\Lambda_1=8$, $\Lambda_2=7$이어서 예제 3의 결과와 일치합니다.

유한체 $GF(929)$ 상의 다음 오증 수열
\begin{equation*} S = [1,3,5,11,25,59,141,339] \end{equation*}

에 벌레캠프-매시 알고리즘을 적용해봅시다. $S_0\neq0$이므로 $j_0=0$이고, 초기값은

\begin{align*} \nu &= 1 \\ \Lambda(x) &= 1 + 0x \\ \Lambda_\text{old}(x) &= 1 \\ f &= 0 \\ d_\text{old} &= 1 \end{align*}

$j=1$일 때 오차와 그에 따른 새 오류 위치 다항식은

\begin{equation*} d = S_1 + 0S_0 = 3, \qquad \Lambda_\text{new}(x) = 1 + 926x \end{equation*}

$j - \nu = 0 \ngtr f - \nu_\text{old} = 0$이므로 $\Lambda_\text{old}(x)$는 업데이트하지 않아도 됩니다. 다음으로 $j=2$일 때에는

\begin{equation*} d = S_2 + 926S_1 = 925, \qquad \Lambda_\text{new}(x) = 1 + 926x + 4x^2 \end{equation*}

$j - \nu = 1 > f - \nu_\text{old} = 0$이므로 $\Lambda_\text{old}(x)$를 업데이트합니다.

\begin{equation*} \Lambda_\text{old}(x) = 1 + 926x, \qquad f = 2, \qquad d_\text{old} = 925 \end{equation*}

똑같이 $j=3$일 때에는

\begin{equation*} d = S_3 + 926S_2 + 4S_1 = 8, \qquad \Lambda_\text{new}(x) = 1 + 928x + 927x^2 \end{equation*}

$j - \nu = 2 > f - \nu_\text{old} = 1$이므로 $\Lambda_\text{old}(x)$를 또 업데이트합니다.

\begin{equation*} \Lambda_\text{old}(x) = 1 + 926x + 4x^2, \qquad f = 3, \qquad d_\text{old} = 8 \end{equation*}

이를 계속 반복하면 최종적으로

\begin{equation*} \Lambda(x) = 1 + 926x + x^2 + x^3 \end{equation*}

을 얻을 수 있습니다.

위 내용을 코드로 구현하면 다음과 같습니다.

def BerlekampMassey(S):
    Lambda = [1]
    Lambda_old = [1]
    f = -1
    d_old = 0

    for j in range(len(S)):
        # Calculate d
        d = S[j]
        for z in range(1, len(Lambda)):
            d = gf_add(d, gf_mul(Lambda[z], S[j-z]))

        if d == 0:
            continue

        if f == -1:
            # S_j is the first non-zero syndrome
            # Initialize variables
            Lambda = [1] + [0] * (j+1)
            Lambda_old = [1]
            f = j
            d_old = d
        else:
            # Calculate Lambda_new(x)
            tmp = [0] * (j-f) + Lambda_old
            dd = gf_mul(d, gf_inv(d_old))
            Lambda_new = Lambda + [0] * (len(tmp) - len(Lambda))
            for z in range(len(tmp)):
                Lambda_new[z] = gf_sub(Lambda_new[z], gf_mul(dd, tmp[z]))

            # Update Lambda_old(x)
            if (j - len(Lambda) > f - len(Lambda_old)):
                Lambda_old = Lambda
                f = j
                d_old = d

            Lambda = Lambda_new

    return Lambda

여기서 gf_add, gf_sub, gf_mul, gf_inv는 유한체 위에서 각각 덧셈, 뺄셈, 곱셈, 곱셈에 대한 역원을 계산하는 함수입니다. 예를 들어 소수 $p$에 대한 나머지로 정의되는 유한체 $GF(p)$ 상에서는 아래과 같이 구현할 수 있습니다.

def gf_add(x, y):
    return (x + y) % p

def gf_sub(x, y):
    return (p + x - y) % p

def gf_mul(x, y):
    return (x * y) % p

def gf_inv(x):
    # By Fermat's little theorem
    return pow(x, p-2, p)

한번 예제 3과 4를 넣고 직접 실행해보시기 바랍니다.

포니 알고리즘

벌레캠프-매시 알고리즘으로 오류 위치 다항식을 구했으면 오류가 발생한 항을 구하는 건 쉽습니다. 인수 분해로 해 $X_k^{-1}$를 직접 찾고 역원을 계산한 뒤 로그를 취해 $i_k$를 구해도 되고, $n$이 작다면 $\Lambda(x)$에 $\alpha^{-1}$부터 $\alpha^{-n}$까지 대입해서 언제 0이 되는지 확인하는 게 더 효과적입니다.

이제 남은 건 오류값 $Y_k$를 찾는 것 뿐입니다. 가장 쉽게 생각할 수 있는 방법은 식 \eqref{eq:SXY}에 $S_j$와 $X_k$를 대입하여 다음과 같은 선형계를 만들어 푸는 것입니다.

\[ \begin{bmatrix} X_1 & X_2 & \cdots & X_\nu \\ X_1^2 & X_2^2 & \cdots & X_\nu^2 \\ \vdots & \vdots & \ddots & \vdots \\ X_1^\nu & X_2^\nu & \cdots & X_\nu^\nu \end{bmatrix} \begin{bmatrix} Y_1 \\ Y_2 \\ \vdots \\ Y_\nu \end{bmatrix} = \begin{bmatrix} S_0 \\ S_1 \\ \vdots \\ S_{\nu-1} \end{bmatrix} \]

하지만 역행렬을 구하는 것은 복잡한데다가 전자 회로로 구현하기 힘들기 때문에 이보다 더 좋은 포니 알고리즘(Forney algorithm)을 보통 사용합니다. 먼저 오증 다항식(syndrome polynomial)을 오증들을 계수로 가지는 다항식으로 정의합니다.

\[ S(x) = \sum_{j=0}^{n-k-1} S_jx^j = S_0 + S_1x + S_2x^2 + ... + S_{n-k-1}x^{n-k-1} \]

식 \eqref{eq:SXY}를 대입하면

\[ \begin{aligned} S(x) &= \sum_{j=0}^{n-k-1} \sum_{k=1}^\nu Y_kX_k^{j+1}x^j \\ &= \sum_{k=1}^\nu X_kY_k \sum_{j=0}^{n-k-1} (X_kx)^j \\ &= \sum_{k=1}^\nu X_kY_k \frac{1 - (X_kx)^{n-k}}{1 - X_kx} \end{aligned} \]

$S(x)$와 $\Lambda(x)$를 곱해봅시다.

\[ \begin{aligned} S(x)\Lambda(x) &= \sum_{k=1}^\nu \left[ X_kY_k \frac{1 - (X_kx)^{n-k}}{1 - X_kx} \right] \prod_{k'=1}^\nu (1 - X_{k'}x) \\ &= \sum_{k=1}^\nu \left\{ X_kY_k \left[ 1 - (X_kx)^{n-k} \right] \prod_{k'=1, k'\neq k}^\nu (1 - X_{k'}x) \right\} \\ &= \sum_{k=1}^\nu \left[ X_kY_k \prod_{k'=1, k'\neq k}^\nu (1 - X_{k'}x) \right] - \sum_{k=1}^\nu \left[ X_kY_k (X_kx)^{n-k} \prod_{k'=1, k'\neq k}^\nu (1 - X_{k'}x) \right] \end{aligned} \]

위 식의 우변 첫 번째 항은 $\nu-1$차 이하의 항으로만, 두 번째 항은 $n-k$차 이상의 항으로만 이루어져 있으므로 $x^{n-k}$에 대한 나머지를 취하면 두 번째 항을 없앨 수 있습니다. 이를 오류 평가 다항식(error evaluator polynomial)이라고 합니다.

\[ \Omega(x) = S(x)\Lambda(x) \bmod x^{n-k} = \sum_{k=1}^\nu \left[ X_kY_k \prod_{k'=1, k'\neq k}^\nu (1 - X_{k'}x) \right] \]

여기에 $X_l^{-1}$($1\le l\le\nu$)을 대입하면 $k=l$인 경우만 남습니다.

\[ \Omega(X_l^{-1}) = \sum_{k=1}^\nu \left[ X_kY_k \prod_{k'=1, k'\neq k}^\nu (1 - X_{k'}X_l^{-1}) \right] = X_lY_l \prod_{k'=1, k'\neq l}^\nu (1 - X_{k'}X_l^{-1}) \label{eq:omega} \]

한편 $\Lambda(x)$를 미분한 결과는

\[ \begin{aligned} \Lambda'(x) &= \Lambda_1 + 2\Lambda_2 x + 3\Lambda_3 x^2 + \cdots + \nu\Lambda_\nu x^{\nu-1} \\ &= \frac{\mathrm{d}}{\mathrm{d}x} \prod_{k=1}^\nu (1 - X_kx) \\ &= \sum_{k=1}^\nu \left[ -X_k \prod_{k'=1,k'\neq k}^\nu (1 - X_{k'}x) \right] \end{aligned} \]

유한체 상의 다항식에 대한 미분이 조금 생소할 수 있는데 계수만 유한체의 정의에 맞게 계산해준다면 일반적인 미분과 동일합니다.formal derivative라고 합니다. 정의에 극한을 사용하지 않고 다항식에 대해서만 정의합니다. 이제 똑같이 $X_l^{-1}$을 대입하면

\[ \Lambda'(X_l^{-1}) = -X_l \prod_{k'=1, k'\neq l}^\nu (1 - X_{k'}X_l^{-1}) \label{eq:lambda-prime} \]

마지막으로 \eqref{eq:omega}과 \eqref{eq:lambda-prime}을 연립하여 $Y_l$을 얻습니다.

\[ Y_l = -\Omega(X_l^{-1})[\Lambda'(X_l^{-1})]^{-1} \]

남은 건 $X_k$와 $Y_k$로 $e(x)$를 구한 후 $r(x)$에서 빼는 것 뿐입니다.

예제 4에서 오류 위치 다항식을 구했으므로 오류를 복원해봅시다. 일단 $i_k$부터 구합니다.
\begin{equation*} \Lambda(x) = 1 + 8x + 7x^2 \end{equation*} \begin{align*} \Lambda(3^{-1}) &= 12 & \Lambda(3^{-\color{red} 8}) &= {\color{red} 0} \\ \Lambda(3^{-2}) &= 11 & \Lambda(3^{-9}) &= 1 \\ \Lambda(3^{-\color{red} 3}) &= {\color{red} 0} & \Lambda(3^{-10}) &= 13 \\ \Lambda(3^{-4}) &= 9 & \Lambda(3^{-11}) &= 12 \\ \Lambda(3^{-5}) &= 9 & \Lambda(3^{-12}) &= 13 \\ \Lambda(3^{-6}) &= 3 & \Lambda(3^{-13}) &= 16 \\ \Lambda(3^{-7}) &= 6 & \Lambda(3^{-14}) &= 11 \\ \end{align*}

예제 2에서 가정했던 것과 같이 3차항과 8차항에서 오류가 발생했습니다. 따라서

\begin{align*} i_1 &= 3, & X_1 &= 3^3 = 10 \\ i_2 &= 8, & X_2 &= 3^8 = 16 \end{align*}

오류 평가 다항식은

\begin{align*} \Omega(x) &= (15x + 16x^2 + 5x^3 + x^4 + 8x^5)(1 + 8x + 7x^2) \bmod x^6 \\ &= (15x + 3x^6 + 5x^7) \bmod x^6 \\ &= 15x \end{align*}

오류 위치 다항식의 미분은

\begin{equation*} \Lambda'(x) = 8 + 14x \end{equation*}

고로 포니 알고리즘에 따라 오류값은 다음과 같습니다.

\begin{align*} Y_1 &= -\Omega(10^{-1})[\Lambda'(10^{-1})]^{-1} = 4 \\ Y_2 &= -\Omega(16^{-1})[\Lambda'(16^{-1})]^{-1} = 6 \\ \end{align*}

최종적으로 오류 다항식은

\begin{equation*} e(x) = 4x^3 + 6x^8 \end{equation*}

이고, 이를 $r(x)$에서 빼면 오류를 정정한 수신 다항식은

\begin{align*} r(x) - e(x) &= (7 + 15x + 5x^2 + 12x^3 + 9x^4 + 9x^5 + 5x^6 + 6x^7 + 2x^8 + 7x^{10} + 15x^{11} + x^{12} + 4x^{13}) - (4x^3 + 6x^8) \\ &= 7 + 15x + 5x^2 + 8x^3 + 9x^4 + 9x^5 + 5x^6 + 6x^7 + 13x^8 + 7x^{10} + 15x^{11} + x^{12} + 4x^{13} \\ \end{align*}

이 되어 처음에 송신한 다항식 $s(x)$와 동일합니다!