Devin.KR

확률로 보는 로봇 - 불확실성 표현

개발자KR 조회 8

이 장에서 배우는 것

창고 로봇이 자신의 위치를 “통로 시작점에서 2.4m 떨어진 곳”이라고 보고했다고 하자. 이 숫자만으로는 로봇이 선반 가까이 지나가도 되는지 판단하기 어렵다. 오차가 수 cm인지, 수십 cm인지에 따라 같은 위치 보고의 의미가 달라지기 때문이다. 로봇의 상태를 다룰 때는 대표값과 함께 그 값을 얼마나 신뢰할 수 있는지도 표현해야 한다.

기본서에서 좌표 변환과 센서 잡음을 배웠다면, 이제 잡음이 있는 측정으로부터 무엇을 알 수 있는지 확률로 정리할 차례다. 이 장에서는 창고 통로의 위치 후보 두 개를 놓고 거리 센서의 측정이 각 후보를 얼마나 지지하는지 계산한다. 위치를 시간에 따라 갱신하는 필터는 구현하지 않는다. 먼저 한 번의 측정을 해석하는 데 필요한 언어와 계산을 익힌다.

  • 가우시안 분포(Gaussian distribution)의 평균과 분산으로 위치의 불확실성을 나타낸다.
  • 공분산(covariance)으로 두 좌표의 퍼짐과 함께 변하는 경향을 설명한다.
  • 센서 모델(sensor model)을 이용해 가정한 상태에서 측정이 얼마나 그럴듯한지 계산한다.
  • 베이즈 규칙(Bayes' rule)으로 측정 전의 확률을 측정 후의 확률로 바꾼다.
  • NumPy로 표본 생성, 확률밀도 계산, 확률 정규화를 구현한다.

문제 상황

로봇이 물건을 운반하는 창고에 곧은 통로가 있다. 통로 시작점의 벽을 좌표 원점으로 잡고, 벽에서 멀어지는 방향을 x축의 양의 방향으로 정한다. 로봇은 벽 쪽을 바라보는 거리 센서를 장착했다. 센서의 설치 위치에 따른 보정은 끝났고, 센서가 읽는 거리는 로봇의 x좌표와 같다고 가정한다. 이 장의 예제에서는 자세 오차와 벽의 기울기를 제외한다.

로봇의 위치 후보는 2.0m와 3.0m 두 곳이다. 측정 전에는 첫 번째 후보의 확률이 0.6, 두 번째 후보의 확률이 0.4다. 이는 가능한 위치를 두 개로 제한한 작은 모형이다. 실제 위치가 두 점 사이에 있을 가능성까지 표현하는 연속 위치 분포와는 구별해야 한다.

거리 센서가 3.0m를 읽었더라도 두 번째 후보를 곧바로 확정할 수는 없다. 첫 번째 후보에서도 양의 측정 오차가 발생하면 같은 값이 나올 수 있다. 반대로 2.5m를 읽었다면 두 후보와의 거리가 같다. 센서의 오차가 대칭이고 두 후보에서 같은 크기라면 이 측정은 어느 한쪽도 더 지지하지 않는다.

풀어야 할 문제는 “측정값과 가장 가까운 후보를 고르는 것”보다 구체적이다. 기존 확률을 보존하면서 측정이 제공한 근거만 반영해야 한다. 이를 위해 먼저 센서 오차의 모양을 정하고, 각 후보에서 관측한 측정의 확률밀도를 구한 뒤, 결과를 합이 1인 확률로 바꾼다.

가우시안으로 한 축의 불확실성을 표현한다

평균은 중심이고 분산은 퍼짐이다

위치를 확률변수 X로 나타내고 평균을 μ, 분산을 σ²로 나타낸다. 표준편차 σ는 분산의 제곱근이다. 위치의 단위가 m라면 평균과 표준편차의 단위도 m지만 분산의 단위는 m²다. 이 차이를 코드에서 놓치면 센서의 불확실성을 잘못 설정하기 쉽다.

X ~ N(μ, σ²)

p(x) = exp(-(x - μ)² / (2σ²)) / (sqrt(2π) σ)
σ > 0

가우시안의 평균은 분포의 중심에 놓인다. 표준편차가 작으면 중심 부근에 확률이 집중되고, 크면 더 넓게 퍼진다. 전체 확률은 언제나 1이므로 분포가 좁아질수록 중심의 높이는 커진다. 높이가 커졌다는 사실 자체가 확률이 1을 넘었다는 뜻은 아니다.

정확한 가우시안 분포에서는 평균에서 표준편차 한 개 이내의 구간에 약 68.3%, 두 개 이내의 구간에 약 95.4%의 확률이 들어간다. 이 비율은 가우시안이라는 모양을 가정했을 때의 결과다. 평균과 분산이 같아도 분포의 모양이 다르면 같은 구간에 들어가는 확률은 달라질 수 있다.

평균이 같아도 표준편차가 작으면 확률밀도가 중심에 더 높고 좁게 모인다

확률밀도와 확률은 다르다

연속적인 위치에서 p(2.0)은 정확히 2.0m일 확률이 아니라 그 지점의 확률밀도(probability density)다. 확률은 1.9m부터 2.1m처럼 폭이 있는 구간에 대해 밀도를 적분해서 구한다. 연속 분포에서는 특정한 한 점의 확률은 0이지만, 그 주변 구간의 확률은 양수일 수 있다.

위치 밀도의 단위는 1/m다. 표준편차가 0.1m인 가우시안의 중심 밀도는 약 3.99/m이므로 숫자만 보면 1보다 크다. 이것은 정상적인 결과다. 반면 위치 후보가 두 개뿐인 모형에서 각 후보에 부여하는 값은 단위 없는 확률이며, 두 확률의 합이 1이어야 한다.

실제 거리 센서는 정해진 해상도로 값을 보고한다. 보고값이 나타내는 작은 구간의 확률은 그 구간의 밀도를 적분한 값이다. 해상도가 충분히 작고 일정하면 구간 확률을 밀도와 구간 폭의 곱으로 근사할 수 있다. 같은 측정을 설명하는 후보끼리 비교할 때는 공통 구간 폭이 정규화 과정에서 상쇄된다. 이 장에서는 이 관점으로 연속 밀도를 사용한다.

위치의 확률을 표현할 때 구분해야 할 양
양의미위치가 m일 때 단위주의점
평균 μ분포의 중심m실제 위치와 같다고 보장하지 않는다
표준편차 σ퍼짐의 크기m밀도 함수에 양수를 넣는다
분산 σ²평균에서 벗어난 거리의 제곱 평균m²표준편차와 혼용하지 않는다
밀도 p(x)단위 길이당 확률의 농도1/m한 점의 확률이 아니다

공분산으로 두 좌표의 관계를 표현한다

평면 위의 위치는 x와 y를 함께 다룬다. 각 축의 분산만 알면 축별 퍼짐은 설명할 수 있지만, 두 오차가 함께 증가하는지까지는 알 수 없다. 로봇이 통로를 따라 비스듬한 방향으로 흔들린다면 x오차와 y오차 사이에도 일정한 경향이 생긴다.

평균 벡터 μ = [μx, μy]

Σ = [[Var(X),   Cov(X, Y)],
     [Cov(X, Y), Var(Y)  ]]

Cov(X, Y) = E[(X - μx)(Y - μy)]

E는 여러 가능한 값에 확률을 곱해 평균을 구하는 기댓값을 뜻한다. 공분산이 양수라면 x가 평균보다 클 때 y도 평균보다 큰 경향이 있다. 음수라면 한쪽이 평균보다 클 때 다른 쪽은 작은 경향이 있다. 공분산이 0이라는 것은 이런 선형적인 동반 변화가 없다는 뜻이다. 일반적인 분포에서는 그것만으로 독립이라고 결론 내릴 수 없다. 공동으로 가우시안 분포를 이루는 경우에는 공분산이 0이면 독립이다.

이 장에서는 다음 공분산을 사용한다. 두 좌표가 모두 m 단위이므로 행렬의 모든 원소는 m² 단위다. 대각 원소에서 x의 표준편차는 0.4m, y의 표준편차는 0.5m임을 읽을 수 있다. 공분산 0.12를 두 표준편차의 곱으로 나누면 상관계수는 0.6이다.

Σ = [[0.16, 0.12],
     [0.12, 0.25]]

상관계수 = 0.12 / (0.4 × 0.5) = 0.6
각 축의 분산이 같아도 양의 공분산이 있으면 위치 분포의 등밀도 타원이 기울어진다

그림의 타원은 이차원 가우시안에서 밀도가 같은 점들을 연결한 것이다. 같은 평균과 같은 축별 분산을 사용해도 공분산에 따라 타원의 방향과 모양이 달라진다. 여기서 타원은 특정 포함 확률을 지정한 경계가 아니다. 일차원에서 배운 약 68.3%를 이차원 타원 안의 확률에 그대로 붙여서는 안 된다.

유효한 공분산을 만드는 방법

공분산 행렬은 대칭이어야 하고, 어느 방향으로 보더라도 분산이 음수가 되어서는 안 된다. 이를 양의 준정부호 조건이라고 한다. 이차원에서는 대각 원소가 음수가 아니고 공분산의 제곱이 두 분산의 곱보다 크지 않아야 한다. 예를 들어 대각 원소를 그대로 두고 비대각 원소를 0.30으로 바꾸면 이 조건을 위반한다.

구현에서는 서로 독립인 표준정규 표본을 선형 변환해서 원하는 공분산을 만든다. 표준정규는 평균이 0이고 분산이 1인 가우시안이다. 아래 행렬 A를 사용하면 공분산은 A와 A의 전치행렬을 곱한 값이 된다. 전치는 행과 열을 바꾸는 연산이다.

A = [[0.4, 0.0],
     [0.3, 0.4]]

Σ = A @ A.T
위치 열벡터 = 평균 열벡터 + A @ 표준정규 열벡터

이 구성에서는 x오차가 0.4u이고 y오차가 0.3u + 0.4v다. u와 v가 독립인 표준정규 변수이므로 두 좌표가 공유하는 u에서 공분산 0.4 × 0.3 = 0.12가 나온다. y분산은 0.3² + 0.4² = 0.25다. 공분산의 숫자를 따로 외우기보다 어떤 공통 오차가 두 좌표에 들어갔는지 생각하면 구조를 이해하기 쉽다.

센서 모델과 베이즈 규칙으로 측정을 해석한다

상태에서 측정으로 가는 관계를 먼저 정한다

측정값을 z, 센서 잡음을 ε라고 쓰면 이번 거리 센서의 모델은 z = x + ε다. 잡음은 평균 0, 표준편차 0.5m인 가우시안으로 가정한다. 이 가정에는 센서가 지속적으로 한쪽으로 치우치지 않고, 위치 후보에 따라 잡음의 크기가 달라지지 않는다는 조건이 들어 있다.

z = x + ε
ε ~ N(0, σsensor²)
σsensor = 0.5m

p(z | x) = N(z; x, σsensor²)

p(z | x)는 위치 x가 주어졌을 때 측정 z의 밀도다. 관측한 z를 고정하고 여러 x를 비교할 때 이 값을 가능도(likelihood)라고 부른다. 가능도는 “이 위치가 참일 확률”이 아니라 “이 위치를 가정했을 때 이번 측정이 얼마나 그럴듯한가”를 나타낸다. 위치에 대한 믿음으로 바꾸려면 측정 전의 확률도 필요하다.

현장의 센서 모델은 기하 관계와 잡음 모형을 함께 포함한다. 벽까지의 거리가 위치와 같지 않은 배치에서는 z = h(x) + ε처럼 상태를 측정으로 바꾸는 함수 h가 필요하다. 센서가 항상 0.2m 길게 읽는다면 그 편향을 모델에 넣거나 보정해야 한다. 표준편차를 키우는 것만으로 평균의 치우침이 사라지지는 않는다.

기존 확률과 측정 근거를 곱한 뒤 정규화한다

측정 전의 확률을 사전확률(prior probability), 측정 후의 확률을 사후확률(posterior probability)이라고 한다. 후보가 유한한 경우에는 각 후보의 사전확률에 가능도를 곱하고, 그 결과의 합으로 나누면 된다.

가중치 wi = p(z | xi) p(xi)

p(xi | z) = wi / Σj wj
          = p(z | xi) p(xi) / Σj p(z | xj) p(xj)

분모는 관측한 측정의 주변 밀도 p(z)다. 분자의 단위도 밀도 단위이므로 나누고 나면 후보별 사후확률은 단위가 없는 값이 된다. 이번 예제의 후보는 가능한 상태를 두 점으로 제한한 것이어서 별도의 격자 간격을 곱하지 않는다. 연속 위치를 격자로 근사할 때는 격자에 저장한 값이 확률인지 밀도인지부터 정해야 한다.

z가 2.5m이면 두 후보의 잔차 z - x는 각각 0.5m와 -0.5m다. 가우시안은 중심을 기준으로 대칭이므로 두 가능도가 같다. 따라서 사후확률은 사전확률인 0.6과 0.4를 유지한다. 증거가 두 후보를 똑같이 지지했기 때문이다.

z가 3.0m이면 첫 번째 후보의 잔차는 1.0m이고 두 번째 후보의 잔차는 0이다. 첫 번째 후보의 가능도는 두 번째 후보의 exp(-2)배다. 기존 확률은 첫 번째 후보가 더 높았지만, 이 측정을 반영하면 두 번째 후보의 사후확률이 약 0.831로 높아진다. 코드에서는 두 측정을 각각 같은 사전확률에 적용한다. 연속해서 들어온 측정을 차례로 누적하는 실험은 아니다.

완성 코드

다음 프로그램을 probability_robot.py로 저장한다. Python 3와 NumPy만 사용하며 그래픽 환경은 필요하지 않다. 난수 생성기의 시드를 7로 고정한다. 출력에는 표본의 개수와 이론 공분산을 사용하므로 무작위 표본의 소수점 값이 실행 결과에 섞이지 않는다. 공분산 확인에 쓰는 이차원 평균과 센서 갱신에 쓰는 위치 후보는 서로 독립된 두 예제다.

import numpy as np


def gaussian_pdf(value, mean, std):
    """평균과 표준편차가 주어졌을 때 가우시안 밀도를 구한다."""
    value = np.asarray(value, dtype=float)
    mean = np.asarray(mean, dtype=float)
    std = float(std)
    if not np.isfinite(std) or std <= 0.0:
        raise ValueError("표준편차는 유한한 양수여야 한다.")
    if not np.all(np.isfinite(value)):
        raise ValueError("측정값은 유한해야 한다.")
    if not np.all(np.isfinite(mean)):
        raise ValueError("평균은 유한해야 한다.")
    scaled = (value - mean) / std
    return np.exp(-0.5 * scaled ** 2) / (
        np.sqrt(2.0 * np.pi) * std
    )


def update_candidates(candidates, prior, measurement, sensor_std):
    """거리 측정 하나로 위치 후보의 확률을 갱신한다."""
    candidates = np.asarray(candidates, dtype=float)
    prior = np.asarray(prior, dtype=float)
    if candidates.ndim != 1 or candidates.size == 0:
        raise ValueError("위치 후보는 비어 있지 않은 1차원 배열이어야 한다.")
    if prior.shape != candidates.shape:
        raise ValueError("후보와 사전확률의 크기가 같아야 한다.")
    if not np.all(np.isfinite(prior)) or np.any(prior < 0.0):
        raise ValueError("사전확률은 유한한 음이 아닌 값이어야 한다.")
    if not np.isclose(prior.sum(), 1.0, rtol=0.0, atol=1e-12):
        raise ValueError("사전확률의 합은 1이어야 한다.")

    likelihood = gaussian_pdf(float(measurement), candidates, sensor_std)
    weights = prior * likelihood
    normalizer = float(weights.sum())
    if not np.isfinite(normalizer) or normalizer <= 0.0:
        raise ValueError("정규화할 수 없다. 측정과 센서 모델을 확인한다.")
    posterior = weights / normalizer
    return likelihood, posterior


def format_vector(values):
    return "[" + ", ".join(f"{value:.3f}" for value in values) + "]"


def main():
    rng = np.random.default_rng(7)
    mean_xy = np.array([2.0, 1.0])
    transform = np.array([[0.4, 0.0], [0.3, 0.4]])
    covariance = transform @ transform.T
    standard_samples = rng.standard_normal((5000, 2))
    samples = mean_xy + standard_samples @ transform.T

    print(f"표본 배열: {samples.shape[0]}행 {samples.shape[1]}열")
    print("이론 공분산 (m^2):")
    for row in covariance:
        print(format_vector(row))

    density = float(gaussian_pdf(2.5, 2.0, 0.5))
    print(f"밀도 p(2.5 | x=2.0): {density:.6f} 1/m")

    candidates = np.array([2.0, 3.0])
    prior = np.array([0.6, 0.4])
    sensor_std = 0.5
    print(f"위치 후보 (m): {format_vector(candidates)}")
    print(f"사전확률: {format_vector(prior)}")

    for measurement in (2.5, 3.0):
        likelihood, posterior = update_candidates(
            candidates, prior, measurement, sensor_std
        )
        print(f"측정 z={measurement:.1f}m")
        print(f"  가능도 (1/m): {format_vector(likelihood)}")
        print(f"  사후확률: {format_vector(posterior)}")
        print(f"  확률 합: {posterior.sum():.6f}")


if __name__ == "__main__":
    main()

줄별 해설

밀도를 계산하는 함수

첫 줄에서 NumPy를 np라는 이름으로 가져온다. gaussian_pdf의 첫 두 변환은 측정과 평균을 실수 배열로 통일한다. 덕분에 하나의 측정값과 여러 후보 평균을 한 번에 계산할 수 있다. np.asarray는 입력이 이미 적절한 배열이면 불필요한 복사를 줄이고, 정수 목록이면 실수 배열로 바꾼다.

std = float(std)는 센서의 표준편차 하나를 사용한다는 인터페이스를 분명히 한다. 이어지는 조건문은 0, 음수, 무한대, 숫자가 아닌 값을 거부한다. 표준편차가 0이면 일반적인 가우시안 밀도 공식의 분모가 0이 된다. 불확실성이 없는 상태를 이 함수에 0으로 넣어서 표현하지 않는다.

scaled는 잔차를 표준편차로 나눈 값이다. 단위가 없어지며 측정이 평균에서 표준편차 몇 개만큼 떨어졌는지를 나타낸다. 반환식의 지수 부분이 중심에서 멀어질수록 밀도를 줄이고, 분모가 전체 면적을 1로 맞춘다. 이번 입력 범위에서는 직접 지수함수를 계산해도 충분하다. 매우 큰 잔차나 작은 표준편차까지 다루는 범용 수치 구현은 별도의 안정화가 필요하다.

후보별 확률을 갱신하는 함수

update_candidates는 후보와 사전확률을 배열로 바꾸고 차원, 길이, 유한성, 음수 여부를 검사한다. 후보 개수가 일치하지 않으면 계산 결과를 잘못 해석할 수 있으므로 곱셈 전에 확인한다. 확률 합은 부동소수점의 작은 표현 오차를 허용하도록 10의 -12제곱의 절대 허용오차로 비교한다.

likelihood를 계산할 때 함수 인자의 순서에 주목한다. 관측한 measurement가 평가 지점이고, 각 candidates가 측정 분포의 평균이다. 이 배치는 z = x + ε라는 센서 모델을 코드로 옮긴 것이다. 후보값의 유한성 검사는 gaussian_pdf 안에서 수행된다.

weights = prior * likelihood는 같은 위치에 있는 원소끼리 곱한다. 여기서 weights는 아직 확률이 아니다. normalizer는 그 합이며, posterior는 합으로 나누어 만든 확률이다. 정규화 상수가 0이거나 유한하지 않으면 계산을 중단한다. 조용히 균등확률을 반환하면 수치 문제나 센서 모델의 부적합이 가려질 수 있다.

표본을 생성하고 결과를 출력하는 부분

default_rng(7)은 이 예제에서 사용할 난수 생성기를 만든다. standard_normal((5000, 2))는 행마다 독립인 표준정규 변수 두 개를 담는다. 각 행을 위치 표본 하나로 해석하므로 열벡터 수식의 A @ u와 달리 standard_samples @ transform.T를 사용한다. 오른쪽에서 곱하는 전치행렬이 두 좌표를 올바른 순서로 섞는다.

mean_xy를 더하면 모든 표본의 중심을 [2.0, 1.0]으로 옮긴다. 공분산은 평균을 옮겨도 변하지 않는다. covariance = transform @ transform.T는 표본에서 추정한 값이 아니라 표본을 생성하는 분포의 이론값이다. 유한한 표본에서 계산한 공분산은 이 값과 조금 다르다.

format_vector는 배열의 표시를 소수점 세 자리로 통일한다. NumPy의 기본 배열 출력 설정에 기대지 않으므로 출력 모양을 읽기 쉽다. 표시를 반올림해도 내부 계산에는 원래 실수가 사용된다. 이번 두 후보의 출력에서는 반올림한 값도 합이 1이지만, 후보가 많아지면 표시된 값의 합이 1에서 조금 벗어날 수 있다.

마지막 반복문은 사전확률 prior를 덮어쓰지 않는다. 따라서 2.5m를 관측한 실험과 3.0m를 관측한 실험이 같은 출발점에서 비교된다. 파일 끝의 조건문은 이 파일을 직접 실행할 때만 main을 호출한다. 다른 파일에서 함수를 가져올 때는 결과를 출력하지 않는다.

실행 결과

NumPy가 설치된 환경에서 다음 명령을 사용한다. 첫 명령은 소스의 컴파일 가능 여부를 확인하고 경고를 오류로 취급한다. 성공하면 아무것도 출력하지 않는다. 두 번째 명령은 프로그램을 실행한다.

python3 -W error -m py_compile probability_robot.py
python3 -W error probability_robot.py

예상 출력은 다음과 같다.

표본 배열: 5000행 2열
이론 공분산 (m^2):
[0.160, 0.120]
[0.120, 0.250]
밀도 p(2.5 | x=2.0): 0.483941 1/m
위치 후보 (m): [2.000, 3.000]
사전확률: [0.600, 0.400]
측정 z=2.5m
  가능도 (1/m): [0.484, 0.484]
  사후확률: [0.600, 0.400]
  확률 합: 1.000000
측정 z=3.0m
  가능도 (1/m): [0.108, 0.798]
  사후확률: [0.169, 0.831]
  확률 합: 1.000000

첫 번째 측정은 두 후보에 같은 가능도를 부여하므로 기존 확률을 유지한다. 두 번째 측정은 3.0m 후보를 더 지지하지만 2.0m 후보의 확률을 0으로 만들지는 않는다. 이는 가우시안 잡음이 큰 잔차에도 작은 양의 밀도를 부여하기 때문이다. 결과의 해석은 후보 집합과 센서 모델이 적절하다는 가정 안에서 이루어진다.

같은 시드와 같은 난수 구현을 사용하면 표본 생성도 재현된다. 다만 서로 다른 라이브러리 버전까지 표본 배열의 모든 비트가 같다고 가정하지는 않는다. 이 프로그램의 출력 숫자는 표본의 개별 값에 의존하지 않도록 구성했다.

실무에서 자주 틀리는 것

분산을 표준편차 인자에 넣는다

센서 사양에서 분산 0.25m²를 얻었다면 표준편차는 0.5m다. 아래의 틀린 코드는 분산을 그대로 전달해서 실제 의도보다 좁은 측정 분포를 만든다. 변수 이름에 분산과 표준편차를 구별해 적으면 단위 확인에도 도움이 된다.

틀린 코드는 다음과 같다.

sensor_variance = 0.25
density = gaussian_pdf(2.5, 2.0, sensor_variance)

고친 코드는 다음과 같다.

sensor_variance = 0.25
sensor_std = np.sqrt(sensor_variance)
density = gaussian_pdf(2.5, 2.0, sensor_std)

가능도만 정규화해서 기존 확률을 버린다

가능도만 합으로 나누면 모든 후보의 사전확률이 같다고 놓은 것과 같다. 이번 예제처럼 측정 전부터 후보별 확률이 다르다면 정보를 잃는다. 특히 두 가능도가 같은 측정에서 사후확률이 0.5와 0.5로 바뀐다면 이 실수를 의심할 수 있다.

틀린 코드는 다음과 같다.

likelihood = gaussian_pdf(2.5, candidates, sensor_std)
posterior = likelihood / likelihood.sum()

고친 코드는 다음과 같다. 완성 코드의 함수를 사용하면 정규화 상수 검사도 함께 수행한다.

likelihood, posterior = update_candidates(
    candidates, prior, 2.5, sensor_std
)

행에 저장한 표본에 전치 없이 변환을 곱한다

배열의 각 행이 표본이라면 행벡터 방식으로 선형 변환해야 한다. 다음 두 계산은 모두 실행되지만 공분산은 서로 다르다. 모양이 맞는다는 이유만으로 곱셈 방향까지 맞다고 판단하면 안 된다.

틀린 코드는 다음과 같다.

samples = mean_xy + standard_samples @ transform
# 이 경우 공분산은 transform.T @ transform이다.

고친 코드는 다음과 같다.

samples = mean_xy + standard_samples @ transform.T
# 이 경우 공분산은 transform @ transform.T이다.

틀린 코드가 만드는 공분산의 대각 원소는 0.25와 0.16으로 의도와 바뀐다. 행마다 표본을 저장했는지, 열마다 표본을 저장했는지 문서와 코드에서 일관되게 정하는 것이 중요하다.

하나의 측정을 중복 반영한다

통신 재전송으로 같은 센서 측정이 두 번 도착할 수 있다. 첫 번째 사후확률을 사전확률로 넣고 같은 측정을 다시 반영하면 같은 근거를 두 번 사용한다. 확률은 더 한쪽으로 몰리지만 새로운 정보가 추가된 것은 아니다.

틀린 코드는 다음과 같다.

_, posterior = update_candidates(candidates, prior, 3.0, sensor_std)
_, posterior = update_candidates(candidates, posterior, 3.0, sensor_std)

고친 코드는 같은 측정 식별자를 한 번만 처리한다. 다음은 중복된 두 수신 기록을 처리하는 작은 예다.

posterior = prior.copy()
seen_measurement_ids = set()
received = [(101, 3.0), (101, 3.0)]

for measurement_id, value in received:
    if measurement_id in seen_measurement_ids:
        continue
    _, posterior = update_candidates(
        candidates, posterior, value, sensor_std
    )
    seen_measurement_ids.add(measurement_id)

값이 우연히 같은 새 측정과 재전송된 기존 측정은 구별해야 한다. 서로 다른 측정을 누적할 때도 상태가 유지되는지, 상태가 주어졌을 때 측정 잡음들이 독립인지 확인해야 한다. 공통 편향이 있는 센서의 반복 측정을 독립적인 근거처럼 다루면 불확실성을 지나치게 작게 평가할 수 있다.

한눈에 보기

불확실성 표현에서 센서 갱신까지 이어지는 계산
개념계산 또는 표현예제에서의 역할확인할 점
가우시안평균 μ, 분산 σ²거리 센서 잡음의 모양밀도와 확률을 구분한다
공분산 행렬Σ = A @ A.T평면 위치 오차의 퍼짐과 방향대칭성과 음이 아닌 방향별 분산
센서 모델z = x + ε위치에서 측정으로의 관계좌표, 단위, 편향을 맞춘다
가능도p(z | x)각 후보가 측정을 설명하는 정도후보 확률 자체가 아니다
사후확률사전확률 × 가능도, 이후 정규화측정 후 후보별 믿음합이 1인지 확인한다

측정은 상태에 관한 근거이고, 불확실성은 그 근거를 얼마나 강하게 받아들일지 정하는 데 쓰인다. 다음 장에서는 상태와 측정을 가우시안으로 표현했을 때 평균과 공분산을 어떻게 갱신하는지 살펴본다. 여기서 구현한 가능도와 정규화의 의미가 그 계산을 이해하는 바탕이 된다.

연습 문제

  1. 사전확률을 [0.5, 0.5]로 바꾸고 2.5m와 3.0m 측정을 각각 적용한다. 사후확률을 소수점 세 자리로 구하고, 첫 번째 측정에서 두 후보가 같은 확률을 갖는 이유를 설명한다.
  2. 기존 사전확률 [0.6, 0.4]와 측정 3.0m를 유지하면서 센서 표준편차를 0.5m에서 1.0m로 바꾼다. 사후확률을 구하고 측정의 영향이 어떻게 달라지는지 설명한다.
  3. 변환 행렬을 [[0.4, 0.0], [-0.3, 0.4]]로 바꾼다. 이론 공분산과 상관계수를 구하고 그림의 타원이 어느 방향으로 기울지 설명한다.
  4. main 안에서 samples의 표본 공분산을 계산하는 코드를 작성한다. 행마다 표본이 저장되어 있다는 조건을 반영하고, 계산값이 이론 공분산과 정확히 같지 않은 이유를 설명한다.

정답과 해설

  1. 2.5m 측정의 사후확률은 [0.500, 0.500]이고, 3.0m 측정의 사후확률은 [0.119, 0.881]이다. 첫 번째 측정은 후보별 잔차의 절댓값이 같고 센서 표준편차도 같아서 가능도가 같다. 사전확률까지 같으므로 정규화한 결과도 같다.

    equal_prior = np.array([0.5, 0.5])
    for measurement in (2.5, 3.0):
        _, posterior = update_candidates(
            candidates, equal_prior, measurement, 0.5
        )
        print(format_vector(posterior))
  2. 사후확률은 [0.476, 0.524]다. 표준편차가 커지면 1.0m 잔차를 이전보다 덜 이례적으로 평가한다. 두 후보의 가능도 비가 1에 가까워지면서 측정이 확률을 바꾸는 정도가 줄어든다. 결과는 표준편차 0.5m일 때의 [0.169, 0.831]보다 기존 사전확률에 가깝다.

    _, posterior = update_candidates(candidates, prior, 3.0, 1.0)
    print(format_vector(posterior))
  3. 공분산은 [[0.16, -0.12], [-0.12, 0.25]]이고 상관계수는 -0.6이다. 각 축의 분산은 유지되지만 함께 변하는 경향이 반대로 바뀐다. x가 증가할 때 y가 감소하는 방향, 즉 x축이 오른쪽이고 y축이 위쪽인 그림에서 오른쪽 아래로 타원이 기운다.

    changed = np.array([[0.4, 0.0], [-0.3, 0.4]])
    changed_covariance = changed @ changed.T
  4. 다음 코드를 samples 생성 이후에 넣는다. rowvar=False는 열을 변수로 해석하게 하고, ddof=1은 평균을 표본에서 추정하는 경우에 맞춰 분모를 표본 수보다 하나 작게 사용한다. 표본은 유한하므로 평균과 공분산 모두 이론값 주변에서 달라진다. 표본 수를 늘리면 일반적으로 차이가 줄어들지만, 매번 단조롭게 감소한다고 보장되지는 않는다.

    sample_covariance = np.cov(samples, rowvar=False, ddof=1)
    print(sample_covariance)

    이 코드는 연습을 위한 추가 출력이다. 앞의 예상 출력에 포함된 이론 공분산과 구별해서 비교한다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.