구현 읽기 — 1차원 최소 구현을 따라간다
Python / C 의 최소 구현을 식·변수명·역할에 비추어 가며 1 루프를 따라갈 수 있도록 한다.
여기까지의 내용을 코드로 옮기면, 1차원 칼만 필터는 매우 짧게 쓸 수 있습니다. 중요한 것은 "줄 수"가 아니라, 각 줄이 어떤 식에 대응하는지 따라갈 수 있다는 점입니다.
Python 최소 구현
상태(x_hat, P)와 파라미터(Q, R)는 클래스로 묶습니다. global 을 사용해 모듈 변수를 다시 쓰는 방식은 여러 필터를 동시에 동작시킬 수 없고 테스트도 어렵기 때문에 피합니다.
class KalmanFilter1D:
def __init__(self, x_hat=0.0, P=10.0, Q=0.05, R=2.0):
self.x_hat = x_hat
self.P = P
self.Q = Q
self.R = R
def step(self, z):
x_pred = self.x_hat
P_pred = self.P + self.Q
K = P_pred / (P_pred + self.R)
self.x_hat = x_pred + K * (z - x_pred)
self.P = (1.0 - K) * P_pred
return self.x_hat, self.P
(1.0 - K) 라고 쓰는 이유는 정수로 계산되지 않게 하기 위해서입니다. Python 에서는 1 - K 라고 써도 K 가 float 라면 결과는 float 가 되므로 동작상으로는 같지만, C 등 다른 언어로 이식했을 때의 동작을 맞추려는 의도로 1.0 으로 명시합니다.
C 최소 구현
C 버전도 상태와 파라미터를 구조체로 묶고, 함수는 그 구조체를 받아 다시 쓰는 형태로 합니다. 전역 변수는 사용하지 않습니다.
typedef struct {
double x_hat;
double P;
double Q;
double R;
} KalmanFilter1D;
double kf_step(KalmanFilter1D *kf, double z) {
double x_pred = kf->x_hat;
double P_pred = kf->P + kf->Q;
double K = P_pred / (P_pred + kf->R);
kf->x_hat = x_pred + K * (z - x_pred);
kf->P = (1.0 - K) * P_pred;
return kf->x_hat;
}
사용 예: KalmanFilter1D kf = { .x_hat = 0.0, .P = 10.0, .Q = 0.05, .R = 2.0 }; 으로 초기화한 뒤, 관측마다 kf_step(&kf, z) 를 호출합니다.
식과 변수의 대응
x̂⁻ (메서드 내부의 로컬 변수)P⁻ (메서드 내부의 로컬 변수)x̂ (필터의 상태)P (필터의 상태)이해도 확인 1 — 1 루프를 손으로 따라가기
Q = 0.5, R = 2, 직전의 x̂ = 4, P = 1, 관측 z = 7 의 1 루프 분량을 구현의 순서로 따라갑니다.
Q1. 1번째 줄 x_pred = self.x_hat 을 실행한 후, 예측값 x̂⁻ 는 얼마입니까?
정상 상태 모델이므로 x̂⁻ = x̂ = 4 입니다.
Q2. 2번째 줄 P_pred = self.P + self.Q 를 실행한 후, 예측 분산 P⁻ 는 얼마입니까?
P⁻ = 1 + 0.5 = 1.5 입니다.
Q3. 3번째 줄 K = P_pred / (P_pred + self.R) 을 실행한 후, 칼만 게인은 얼마입니까?
K = 1.5 / (1.5 + 2) ≈ 0.4286 입니다.
Q4. 4번째 줄 self.x_hat = x_pred + K * (z - x_pred) 를 실행한 후, 업데이트 후의 추정값 x̂ 는 얼마입니까?
관측과 예측의 차이는 7 − 4 = 3 이므로 x̂ ≈ 4 + 0.4286 × 3 ≈ 5.2857 입니다.
Q5. 5번째 줄 self.P = (1.0 - K) * P_pred 를 실행한 후, 업데이트 후의 분산 P 는 얼마입니까?
P ≈ (1 − 0.4286) × 1.5 ≈ 0.8571 입니다.
코드를 읽을 때의 포인트
- 먼저 예측의 2 줄과 업데이트의 3 줄을 나눈다
K가 무엇에 의존하는지 확인한다- 마지막에 분산
P가 제대로 업데이트되었는지 확인한다
이해도 확인 2 — 코드의 의미를 읽기
구현의 각 줄이 식의 어느 부분을 나타내는지 말로 되돌립니다.
Q1. 다음 중 "관측을 받아들인 후에 불확실성을 감소시키는" 처리에 대응하는 줄은 무엇입니까?
self.P = (1.0 - K) * P_pred 가 관측을 받아들인 후에 불확실성을 감소시키는 줄입니다.
Q2. 구현에서 잘못하여 K = self.R / (P_pred + self.R) 로 썼다면 어떤 문제가 발생합니까?
원래는 R 이 클수록 관측을 의심해야 하는데, 식을 거꾸로 하면 큰 R 에서 관측을 강하게 채택해 버립니다.
이해도 확인 3 — 관측과 예측의 차이가 0 인 경우
관측이 예측과 일치하는 특수한 경우에서의 동작을 확인합니다.
Q1. 예측값 x̂⁻ = 5, 관측 z = 5, K = 0.6 일 때 업데이트 후의 x̂ 는 얼마입니까?
관측과 예측의 차이는 5 − 5 = 0 이므로 추정값은 x̂⁻ = 5 인 채로 움직이지 않습니다.
Q2. 이어서 예측 분산 P⁻ = 4 일 때, 업데이트 후의 분산 P 는 얼마입니까?
P = (1 − 0.6) × 4 = 1.6 입니다. 추정값은 움직이지 않더라도 관측을 받아들였으므로 불확실성은 감소합니다.