조건부 확률분포: p(a∣b) 와 p(b∣a)

코드 속의 확률분포: 숫자 하나, 표, 파라미터, 뽑는 함수

이 장에서 p(a ∣ b)는 줄곧 종이 위의 표이거나 식이었다. 그런데 분류기나 생성기를 파이썬으로 짜 보면 p 라는 이름의 변수는 어디에도 없다. 분류기 코드의 끝에는 길이 10짜리 배열이나 정수 하나가 있고, 생성기 코드의 끝에는 그림 한 장이 있다. 수식에서는 둘 다 세로 막대가 들어간 p인데, 코드에서는 왜 이렇게 다른 모양으로 나타날까? 앞 절의 고양이 분포를 다른 함수에 넘겨주려 한다면, 변수 하나에 무엇을 넣어야 할까?

같은 분포를 네 가지로 담기

앞 절의 특징 평면에서 라벨을 지운 사진 분포를 담아 보자. 사진첩에 고양이와 강아지가 반반이면 p(x) = ½ p(x ∣ 고양이) + ½ p(x ∣ 강아지)이고, 라벨을 더해 지운 주변 분포다. 봉우리가 둘인 이 분포를 고른 것은 봉우리가 하나뿐이면 아래 네 가지 형태의 차이가 잘 드러나지 않아서다. 코드로 담는 길은 크게 넷이다.

같은 분포 p(x) = ½ p(x ∣ 고양이) + ½ p(x ∣ 강아지)를 네 형태로 담은 그림. 왼쪽부터 숫자 하나(가장 높은 점, 강아지 쪽은 흔적이 없다), 표(폭 0.25인 칸 576개), 파라미터(정규분포 하나. 실선이 그 등고선, 점선이 실제 분포, 가장 높은 곳이 두 무리 사이 골짜기에 놓인다), 뽑는 함수(부를 때마다 점 하나, 2,000번 부른 점. 적갈 = 고양이로 뽑힌 점, 청록 = 강아지로 뽑힌 점). 오른쪽으로 갈수록 담을 수 있는 모양이 넓어지고, 왼쪽으로 갈수록 밀도 읽기·곱하기·조건 걸기를 정해진 계산 몇 번으로 끝낼 수 있다(sources/conditional-prob/code_figs.py)
같은 분포 p(x) = ½ p(x ∣ 고양이) + ½ p(x ∣ 강아지)를 네 형태로 담은 그림. 왼쪽부터 숫자 하나(가장 높은 점, 강아지 쪽은 흔적이 없다), 표(폭 0.25인 칸 576개), 파라미터(정규분포 하나. 실선이 그 등고선, 점선이 실제 분포, 가장 높은 곳이 두 무리 사이 골짜기에 놓인다), 뽑는 함수(부를 때마다 점 하나, 2,000번 부른 점. 적갈 = 고양이로 뽑힌 점, 청록 = 강아지로 뽑힌 점). 오른쪽으로 갈수록 담을 수 있는 모양이 넓어지고, 왼쪽으로 갈수록 밀도 읽기·곱하기·조건 걸기를 정해진 계산 몇 번으로 끝낼 수 있다(sources/conditional-prob/code_figs.py)

네 형태를 코드로 적으면 아래와 같다. 뽑는 함수는 라벨을 반반으로 먼저 뽑고, 뽑힌 라벨의 정규분포에서 점을 뽑는다.

import numpy as np
rng = np.random.default_rng(4)
MU = {"고양이": np.array([-0.8, -0.6]), "강아지": np.array([0.9, 0.7])}
SD = {"고양이": 0.55, "강아지": 0.75}

def dens(x, k):                                   # p(x | 라벨): 둥근 정규분포의 높이
    d = x - MU[k]
    return np.exp(-(d**2).sum(-1) / (2 * SD[k]**2)) / (2 * np.pi * SD[k]**2)

def p(x):                                         # 라벨을 지운 p(x)
    return 0.5 * dens(x, "고양이") + 0.5 * dens(x, "강아지")

best = np.array([-0.8, -0.6])                     # (1) 숫자 하나: 가장 높은 점

w = 0.25                                          # (2) 표: 폭 0.25 인 칸 24 × 24
c = np.arange(-3, 3, w) + w / 2
grid = np.stack(np.meshgrid(c, c, indexing="ij"), -1)
table = p(grid) * w * w
table /= table.sum()

mean = np.array([0.05, 0.05])                     # (3) 파라미터: 정규분포 하나의 숫자 5개
cov = np.array([[1.155, 0.552], [0.552, 0.855]])

def sample(n):                                    # (4) 뽑는 함수: 라벨 먼저, 그다음 점
    cat = rng.random(n) < 0.5
    pts = np.where(cat[:, None],
                   MU["고양이"] + SD["고양이"] * rng.standard_normal((n, 2)),
                   MU["강아지"] + SD["강아지"] * rng.standard_normal((n, 2)))
    return pts, cat

형태마다 계산이 어떻게 바뀌나

이 장에서 한 계산은 넷이었다. 한 점의 확률(연속이면 밀도) 읽기, 조건부 분포에 조건의 확률을 곱해 결합 확률 만들기, 한쪽을 더해 지우기, 조건으로 좁혀 그 안에서 다시 재기. 형태가 바뀌면 같은 계산이 아래처럼 다른 모양이 된다.

계산 표 정규분포 하나 뽑는 함수
한 점의 밀도 읽기 칸 하나 ÷ 칸 넓이 식에 넣는다 둘레 작은 네모에 든 샘플을 센다
곱해서 결합 만들기 p(x ∣ y) p(y) 칸마다 곱한다 정규분포끼리 곱하면 다시 정규분포라 식이 있다 y를 먼저 뽑고 그 y로 x를 뽑는다
더해 지우기(주변화) 한 축을 따라 더한다 남길 축의 평균과 분산만 남긴다 지울 변수의 값을 보지 않는다
조건으로 좁히기 줄을 잘라 줄 합으로 나눈다 정해 둔 형태 안에서 식이 있다 조건에 맞지 않는 샘플을 버리고 남은 것만 센다

뽑는 함수 형태에서 곱하기와 더해 지우기는 표보다 오히려 쉽다. 결합 확률 p(x, y) = p(x ∣ y) p(y)를 칸마다 곱하는 대신, y를 먼저 뽑고 그 y로 x를 뽑으면 (x, y) 쌍이 결합 분포에서 뽑힌다. 주변 분포 p(x)는 그 쌍에서 y를 보지 않고 x만 쓰면 된다. 위 sample 함수가 한 일이 이것이다. 잠재 변수 z를 칸 17개로 나눠 적분하던 앞의 장난감도, 뽑는 함수 형태에서는 z를 먼저 뽑고 x를 뽑은 뒤 z를 버리는 두 줄이 된다.

어려워지는 것은 조건으로 좁히기다. 1 × 1 네모 둘을 골라 「네모에 들 확률」과 「네모 안에서 고양이일 확률」을 표와 샘플 1만 개로 재 보자. 하나는 고양이 무리의 가운데를 덮는 네모이고, 다른 하나는 두 무리 사이 왼쪽 위의 드문 자리에 놓인 네모다.

def inbox(x, b):                                  # 네모 b = (x 왼, x 오른, y 아래, y 위) 안인가
    return (b[0] <= x[..., 0]) & (x[..., 0] < b[1]) & (b[2] <= x[..., 1]) & (x[..., 1] < b[3])

pts, cat = sample(10000)
for b in [(-1.25, -0.25, -1.0, 0.0), (-2.25, -1.25, 0.75, 1.75)]:
    n = inbox(pts, b).sum()
    print(f"표 {table[inbox(grid, b)].sum():.4f}  샘플 {inbox(pts, b).mean():.4f} ({n}개)  "
          f"네모 안 고양이 비율 {cat[inbox(pts, b)].mean():.3f}")
# 표 0.2074  샘플 0.2050 (2050개)  네모 안 고양이 비율 0.978
# 표 0.0011  샘플 0.0013 (13개)  네모 안 고양이 비율 0.462

촘촘한 칸으로 적분한 정답은 첫 네모가 0.2045와 0.976, 둘째 네모가 0.0011과 0.638이다. 샘플 형태에서 조건으로 좁히기는 버리기다. 첫 네모에는 1만 개 가운데 2,050개가 남아 고양이 비율 0.978이 정답 0.976에 붙는다. 드문 네모에는 13개만 남고, 그 13개로 잰 고양이 비율 0.462는 정답 0.638에서 크게 벗어난다. 1만 개를 200번 새로 뽑아 보면 이 값이 0에서 1까지 오가는데, 조건이 드물수록 버리는 샘플이 늘어 남은 몇 개로 재기 때문이다. 표는 칸 폭을 정하면 값이 하나로 정해져서 뽑을 때마다 달라지지 않는다.

직접 움직여 보기분포를 담는 형태크게 열기

위젯에서 네모를 여기저기 옮기고 샘플 수를 바꿔 보면, 표의 값은 칸 폭만 정하면 그대로이고 샘플의 값은 「샘플 새로 뽑기」를 누를 때마다 흔들린다. 정규분포 하나로 보면 드문 네모의 확률을 0.0025로, 정답 0.0011의 두 배 넘게 잡는다. 라벨을 담지 않아서 네모 안 고양이 비율은 아예 내놓지 못한다. 칸 폭을 0.125로 줄이면 표는 더 정확해지지만 칸이 2,304개로 네 배가 된다.

정리하면 코드 속의 확률분포는 타협이다. 표로 적을 수 있을 때까지는 표로 버틴다. 칸이 너무 많아지면 형태를 정해 숫자 몇 개로 줄이는데, 이때는 모양을 틀리게 담을 각오를 한다. 형태도 정할 수 없으면 뽑는 함수로 넘어가고, 계산은 세기로 바뀌어 늘 조금씩 흔들린다.

ML에서: 분류기는 표, 그림 생성기는 뽑는 함수

분류기와 언어 모델은 표 형태다. 이미지넷 분류기는 라벨 1,000칸, 언어 모델은 다음 낱말 수만 칸짜리 표를 내놓는다. 칸 수가 감당할 만해서 표로 버틸 수 있다. 그림 생성기는 뽑는 함수 형태다. 부를 때마다 그림 한 장을 내놓고, 그 그림의 p(x)가 얼마인지는 알려 주지 않는 경우가 많다. 두 형태가 섞인 모델도 많다. 정규분포에서 숫자 몇 개(잠재 변수 z)를 뽑아 신경망에 넣는 생성 모델은 잠재 변수 쪽을 파라미터 형태로 담고, 그림 쪽은 신경망을 거친 뽑는 함수 형태로 만든다. 위 sample 함수가 라벨을 먼저 뽑고 점을 뽑은 순서와 같다. 위 표에서 뽑는 함수 형태가 가장 애먹는 칸은 조건으로 좁히기였는데, 이미 만든 생성기에 새 조건을 붙이면서 버리기보다 나은 방법을 찾는 것이 생성 모델의 큰 숙제다.

문제 13. 주말 날씨 앱 셋

같은 주말 예보를 세 앱이 다르게 보여 준다. 앱 A 는 날마다 낱말 하나(토요일 「비」, 일요일 「안 옴」), 앱 B 는 비 올 확률(토요일 60%, 일요일 30%), 앱 C 는 예보대로 돌린 모의 주말 10개를 보여 준다. 두 날의 날씨는 서로 상관없다고 하자. (가) 세 앱은 각각 네 형태 가운데 어느 것인가? (나) 「이번 주말 이틀 가운데 하루라도 비가 올 확률」을 세 앱으로 각각 구하라. (다) 앱 C 로 구한 값이 앱 B 로 구한 값과 다르면 앱 C 가 틀린 것인가?

같은 주말 예보를 보여 주는 세 앱. 앱 A 는 날마다 낱말 하나, 앱 B 는 날마다 비 올 확률(토 60%, 일 30%), 앱 C 는 모의 주말 10개의 (토, 일) 날씨(sources/conditional-prob/code_figs.py)
같은 주말 예보를 보여 주는 세 앱. 앱 A 는 날마다 낱말 하나, 앱 B 는 날마다 비 올 확률(토 60%, 일 30%), 앱 C 는 모의 주말 10개의 (토, 일) 날씨(sources/conditional-prob/code_figs.py)
이서연 S01
이서연

(가)는 앱 A 가 숫자 하나, 앱 B 가 표, 앱 C 가 뽑는 함수를 여러 번 부른 결과네.

김민준 M01
김민준

(나)는 앱 B 부터 할게. 하루라도 비가 오는 건 이틀 다 안 오는 경우만 빼면 되니까 1 − 0.4 × 0.7 = 0.72.

이서연 S02
이서연

앱 A 는 더 쉬워. 토요일이 「비」니까 하루라도 비 올 확률은 1이야.

선생님 T14
선생님

토요일 「비」는 얼마나 확실한 말이었어요?

이서연 S06
이서연

앱 B 를 보면 60%였어요. 아, 앱 A 는 60%를 「비」 하나로 줄여서 보여 준 거구나. 그럼 1이 아니에요. 「비」가 99%였는지 51%였는지 앱 A 만 봐서는 몰라서, 앱 A 로는 구할 수가 없어요.

김민준 M01
김민준

앱 C 는 세면 돼. 열 개 가운데 하루라도 비가 온 주말이 2, 3, 4, 6, 7, 9번째, 여섯 개라 0.6.

선생님 T01
선생님

0.6과 0.72가 다르네요. 앱 C 가 틀렸을까요?

김민준 M02
김민준

열 개뿐이라 흔들린 것 같아요. 코드로 모의 주말을 10만 개 돌려 보면 0.7206이 나와요. 개수를 늘리면 앱 B 의 값에 다가가요.

이서연 S07
이서연

그런데 앱 B 로 계산할 때는 두 날이 서로 상관없다는 말이 있어야 확률을 곱할 수 있었어. 앱 C 는 주말을 전부 뽑아서 보여 주니까, 토요일에 비가 오면 일요일에도 잘 오는 식으로 두 날이 엮여 있어도 그냥 세기만 하면 되겠다. 대신 개수가 적으면 값이 흔들리고.

문제 14. 사진첩 표를 샘플로

사진첩 표(고양이 실내 72, 고양이 실외 18, 강아지 실내 44, 강아지 실외 66)에서 사진 한 장을 고르게 뽑는 일을 N번 되풀이해 (동물, 찍은 곳) 쌍 N개를 얻었다. 표는 버리고 쌍만 남았다. (가) 쌍만으로 p(실내 ∣ 고양이)를 구하는 절차는? (나) N = 100 과 N = 10,000 에서 코드로 구한 값을 표의 값 0.80 과 비교해라. 같은 N 으로 500번 새로 뽑으면 값이 얼마나 흩어지는가? (다) 앞의 병원 검사(유병률 1%, 환자의 90%에게 양성, 환자가 아닌 사람의 5%에게도 양성)를 1만 명의 샘플로 바꾸어 p(병 ∣ 양성)을 세면, 계산에 쓰이는 사람은 몇 명인가? 유병률이 1만 명에 1명이면 어떻게 되나?

사진 200장의 표에서 고르게 한 장씩 N번 뽑아 얻은 (동물, 찍은 곳) 쌍. 표는 버리고 쌍만 남는다(sources/conditional-prob/code_figs.py)
사진 200장의 표에서 고르게 한 장씩 N번 뽑아 얻은 (동물, 찍은 곳) 쌍. 표는 버리고 쌍만 남는다(sources/conditional-prob/code_figs.py)
김민준 M01
김민준

(가)는 고양이가 아닌 쌍을 다 버리고, 남은 쌍 가운데 실내인 쌍의 비율을 세면 돼. (나)는 돌려 봤어. N = 100 이면 고양이 쌍이 40개, 그 가운데 실내가 34개라 0.850. N = 10,000 이면 고양이 쌍 4,517개 가운데 실내 3,622개라 0.802.

이서연 S01
이서연

500번 새로 뽑아 보면 표준편차가 N = 100 에서 0.056, N = 10,000 에서 0.006이야. 샘플을 100배로 늘렸는데 흩어짐은 10분의 1로만 줄었네.

선생님 T14
선생님

100배 늘렸는데 왜 10분의 1일까요?

이서연 S03
이서연

음, 실내인지 아닌지를 n번 세서 낸 비율이니까… 분산이 p(1 − p)/n 이고, 표준편차는 √n 으로 나눠져요. 100배면 √100 = 10이라 10분의 1이요.

김민준 M01
김민준

그런데 N = 100 이면 실제로 몇 개로 센 거야? 고양이 쌍만 남기니까 n 은 100이 아니잖아.

이서연 S07
이서연

고양이가 90 ÷ 200 = 0.45라서 45개 안팎이네. √(0.8 × 0.2 / 45)가 0.06쯤이라 0.056과 맞아.

김민준 M04
김민준

(다)는 1만 명이니까 1만 개로 재는 거고, 그럼 꽤 정확하겠지.

선생님 T01
선생님

「양성일 때」 병일 확률을 세려면 1만 명 가운데 누구를 남겨야 해요?

김민준 M07
김민준

아, 양성인 사람만 남겨야 해요. 그럼 585명 안팎만 남고 나머지 9,400명은 버려요. 500번 돌려 보니까 평균은 0.154로 정답이랑 같은데, 한 번 잴 때마다 0.107부터 0.199까지 오가요.

이서연 S06
이서연

유병률이 1만 명에 1명이면 양성은 501명쯤 남는데, 그 가운데 환자는 기댓값이 0.9명이야. 답의 분자가 한 명도 안 되는 날이 많아. 조건으로 남긴 샘플이 많아도, 묻는 쪽이 드물면 셀 게 없네.

선생님 T13
선생님

샘플로 센 값은 늘 흔들려요. 남는 샘플이 몇 개인지, 그 가운데 묻는 쪽이 몇 개인지를 같이 적어야 그 값을 얼마나 믿을지 알 수 있어요.

문제 15. 고양이 생성기에 「실내」 붙이기

「고양이」를 넣으면 부를 때마다 고양이 사진 한 장을 내놓는 생성기와, 사진 한 장을 받아 그 사진이 실내 사진인지 판정하는 판정기가 있다. 생성기는 한 번 부르는 데 2초가 걸린다. (가) 생성기 속을 고칠 수 없을 때 실내 고양이 사진을 얻는 가장 쉬운 방법은? (나) 생성기가 내놓는 고양이 사진의 80%가 실내라면, 실내 고양이 사진 100장을 얻는 데 평균 몇 번을 불러야 하는가? 「눈 덮인 산 위의 고양이」처럼 1만 장에 1장꼴인 조건이면? (다) 생성기 속을 고칠 수 있다면, 다 뽑은 뒤 버리는 대신 무엇을 바꾸고 싶은가?

「고양이」를 받아 사진을 내놓는 생성기와 실내 판정기. 판정 뒤 사진을 받을지 버릴지(sources/conditional-prob/code_figs.py)
「고양이」를 받아 사진을 내놓는 생성기와 실내 판정기. 판정 뒤 사진을 받을지 버릴지(sources/conditional-prob/code_figs.py)
김민준 M01
김민준

(가)는 앞의 샘플 형태 그대로야. 생성기를 계속 부르고, 판정기가 실내라고 한 사진만 남기면 돼.

이서연 S01
이서연

(나)는 한 번에 실내가 나올 확률이 0.8이니까 100장에 평균 100 ÷ 0.8 = 125번. 250초면 끝나.

김민준 M05
김민준

1만 장에 1장꼴이면 100 ÷ 0.0001 = 100만 번이네. 2초씩이면 200만 초니까… 23일이 넘어. 사진 100장을 얻자고 그래픽 카드를 한 달 가까이 돌려야 해.

선생님 T14
선생님

그 100만 번 가운데 99만 9,900번은 무엇을 한 거예요?

이서연 S03
이서연

다 그린 다음에 버렸어요. 실내가 아닌 산이나 바다 고양이를 끝까지 그려 놓고서요.

선생님 T01
선생님

그럼 (다)는요? 생성기가 사진을 한 번에 그리지 않고 여러 단계에 걸쳐 조금씩 다듬는다고 해 봐요.

김민준 M02
김민준

다 그린 다음에 판정하지 말고, 그리는 도중에 판정기한테 물어보면 어떨까요? 반쯤 그린 그림이 실내 쪽인지 바깥 쪽인지 보고, 실내 쪽으로 조금씩 틀면서 그리게요.

이서연 S02
이서연

그러려면 판정기가 「맞다, 아니다」만 말하면 안 되겠다. 그림을 어느 쪽으로 고치면 실내다워지는지 방향까지 알려 줘야 하잖아.

김민준 M07
김민준

찌개로 치면 다 끓인 다음 맛이 아니면 버리고 새로 끓이는 게 버리기고, 끓이는 동안 간을 보면서 소금을 더하는 게 서연이 말한 쪽이네. 간을 보려면 「싱겁다」만 아니라 무엇을 더 넣을지 알려 주는 혀가 있어야 하고.