이 책은 한 가지 질문에서 출발한다. 기계학습 모델이 무언가를 "배웠다"고 할 때, 그 모델은 어디에서 어디로 움직인 것일까? 그 모델에 구체적으로 어떤 변화가 일어난 것이며, 그 변화를 수학으로 적으려면 어떤 도구가 가장 알맞을까?
손실이 줄었다거나 정확도가 올랐다는 숫자는 우리가 매일 보지만, 그 숫자들 밑에서 실제로 움직이는 것이 무엇인지, 그것이 움직이는 공간이 어떻게 생겼는지는 잘 묻지 않는다.
이 장은 그 공간의 입구까지 걸어간다. 먼저 기계학습이 다루는 것이 결국 확률분포라는 것을 확인하고, 그다음 파라미터 공간에서 일어나는 이상한 일들을 구경하고, 마지막으로 그 일들을 읽을 언어가 어디서 왔는지 본다.
확률분포: 모델이 내놓는 것
모델이 어디에서 어디로 움직였는지 물으려면, 먼저 모델이 무엇을 내놓는지부터 알아야 한다. 입력을 넣으면 모델은 답을 하나 돌려준다고 흔히 생각하는데, 정말 그럴까?
함수라는 상식
온도 조절기에 25도를 넣으면 난방이 꺼지고 18도를 넣으면 켜지듯이 입력이 변하면 출력이 변하는데, 우리는 이런 입력과 출력의 대응을 함수라고 부른다.
오랫동안 프로그램이란 사람이 이 함수를 직접 쓰는 일이었다. "온도가 20도보다 낮으면 켠다"처럼, 규칙을 아는 사람이 그 규칙을 코드로 옮긴다.
기계학습의 핵심 전환
그런데 규칙을 모르는 경우도 있다. 고양이 사진을 보고 "고양이"라고 말하는 규칙만 해도, 그것을 if 문으로 쓸 수 있는 사람은 없다.
기계학습은 여기서 방향을 바꿔, 사람은 함수의 틀만 짜고 틀 안에 수백만, 수십억 개의 빈칸을 남겨 둔다. 그 빈칸을 채우는 숫자를 파라미터라고 부르고, 파라미터는 기계가 데이터를 보고 채운다.
틀에 무언가를 넣으려면 먼저 세상을 숫자로 바꿔야 해서, 이미지는 픽셀 밝기의 나열이 되고 소리는 1초에 수만 번 잰 공기 압력의 나열이 된다. 문장은 토큰 번호의 나열이 되었다가 다시 벡터가 된다.
출력도 숫자여야 하는데, 여기서 조금 이상한 일이 일어난다.
출력은 하나의 값이 아니다
“오늘 저녁은” 다음에 올 단어를 맞히는 언어 모델을 생각하자. “치킨”, “라면”, “굶는다” 가운데 어느 것도 틀렸다고 할 수 없으니, 같은 입력 뒤에 올 수 있는 답이 여럿이다.
그래서 언어 모델은 단어 하나 대신 어휘 전체에 대한 확률표를 내놓는다. “치킨” 0.4, “라면” 0.3, “굶는다” 0.01, …. 출력이 값 하나가 아니라 가능성의 분포가 된다.
이 사정을 거꾸로 보면 모델이 배우는 데이터도 같은 모양이다. 누군가는 “오늘 저녁은” 뒤에 "치킨"을, 누군가는 "라면"을 이어 쓴 글이 모여 학습 데이터가 된다. 그러니 우리 손에 있는 문장 하나하나는, 사람들이 말을 잇는 어떤 확률표에서 한 번씩 뽑혀 나온 것으로 볼 수 있다. 분포에서 이렇게 뽑혀 나온 하나하나를 샘플이라 부른다.
모델이 확률표를 내놓는 것은 우연한 설계가 아니다. 세상에는 같은 입력에 여러 답이 있는 일이 흔하고, 모델은 자기가 얼마나 모르는지도 말할 수 있어야 하고, 학습을 하려면 “조금 더 맞게” 고칠 수 있는 매끄러운 출력이 필요하다. 확률표는 세 가지를 한꺼번에 해결한다.
신경망의 마지막 층은 보통 후보마다 아무 실수나 하나씩 내놓는다. 후보마다 하나씩 나오는 실수를 로짓이라 부르고, 로짓을 확률표로 바꾸는 장치가 소프트맥스다.
지수를 씌우면 모든 값이 양수가 되고 전체 합으로 나누면 합이 1이 되므로, 어떤 로짓을 넣어도 나오는 것은 늘 분포다.
직접 움직여 보기
불러오는 중…
파이썬
import numpy as np
def softmax(z):
e = np.exp(z - z.max()) # 가장 큰 값을 빼도 결과는 같다 (넘침 방지)
return e / e.sum()
z = np.array([2.0, 1.0, -1.0]) # 세 후보 토큰의 로짓
q = softmax(z)
print("확률표 ", q.round(3), " 합", q.sum())
print("로짓+100 ", softmax(z + 100).round(3))
print("로짓×3 ", softmax(3 * z).round(3))
print("로짓÷3 ", softmax(z / 3).round(3))
# 확률표 [0.705 0.259 0.035] 합 1.0
# 로짓+100 [0.705 0.259 0.035]
# 로짓×3 [0.952 0.047 0. ]
# 로짓÷3 [0.48 0.344 0.176]
모든 로짓에 100을 더해도 확률표가 (0.705, 0.259, 0.035)에서 바뀌지 않는 것은, 확률표를 정하는 것이 로짓끼리의 차이뿐이기 때문이다. 로짓을 키우면 한 후보에 확률이 몰리고, 줄이면 고르게 퍼진다.
파라미터에 대한 믿음: 베이즈 관점
보통의 학습은 파라미터의 값 하나를 찾는다. 가중치 w = 0.73. 점 하나다.
그런데 데이터가 유한하니 우리는 파라미터의 참값을 알 수 없다고 보는 다른 관점도 있다. 그렇다면 “w는 0.73이다” 대신 "w는 0.7 근처일 가능성이 높고, 0.5일 가능성도 조금 있다"라고 말하는 편이 정직하다. 파라미터에 대한 우리의 믿음을 분포로 쓰는 것이다.
믿음을 분포로 적는 이런 방식을 베이즈 통계의 관점이라 한다. 모든 통계학자가 이 관점을 따르지는 않아서, 파라미터는 고정된 값이고 흔들리는 것은 데이터뿐이라고 보는 학자들도 있는데, 이 책은 두 관점을 모두 쓴다.
이 절의 결론
데이터는 어떤 분포에서 뽑힌 샘플이고, 모델의 출력은 분포이며, 베이즈 관점에서는 파라미터에 대한 믿음도 분포다.
어디를 보아도 확률분포가 있으니, 이 책이 다루는 대상도 확률분포일 수밖에 없다.
확률분포를 다룬다면, 다음 질문이 따라온다. 분포와 분포 사이는 “얼마나” 먼가? 공정한 동전과 앞면이 0.7의 확률로 나오는 기운 동전은 “얼마나” 다른가?
공정한 동전과 기운 동전
그 "얼마나"를 숫자로 쓰려면, 분포가 사는 공간의 지도가 필요하다.
문제 1. 체스 레이팅과 승률
체스와 여러 온라인 게임은 선수마다 레이팅이라는 점수를 매긴다. 흔히 쓰는 엘로(Elo) 방식에서는 레이팅이 RA인 선수가 RB인 선수를 상대로 얻으리라 기대하는 점수(이기면 1, 비기면 ½, 지면 0)를 1 / (1 + 10(RB − RA)/400)로 계산한다. 여기서는 비기는 판이 없다고 보고 이 기대 점수를 이길 확률로 읽자. (가) 레이팅 1600인 선수가 1400인 선수를 이길 확률을 구하라. (나) 한 시즌이 지나 리그의 모든 선수 레이팅이 한꺼번에 100점씩 올랐을 때, 두 선수가 다시 만나면 이길 확률은 어떻게 되는가? (다) 엘로 기대 점수 식은 후보가 둘인 소프트맥스 식으로 고쳐 쓸 수 있다. 레이팅에 어떤 수를 곱하면 로짓이 되는가?
레이팅 1600인 선수 A와 1400인 선수 B, 그리고 한 시즌 뒤 모든 선수가 100점씩 오른 두 선수의 자리
(나)는 식에 차이만 들어가니까, 둘 다 100점씩 올라도 차이는 200으로 같아. 이길 확률도 76%로 같고.
선생님
민준 학생이 처음 한 방식으로는요?
김민준
1700 / 3200 = 53.1%요. 실력 차이는 그대로인데 확률이 바뀌네요.
선생님
보통 선수가 1500점 안팎이 되게 정한 건 누구일까요?
이서연
레이팅을 처음 매긴 쪽이 정한 기준이죠. 기준점은 아무 데나 옮겨도 되는 숫자라서, 레이팅끼리의 비율은 뜻이 없고 차이만 뜻이 있어요.
이서연
그럼 (다)는… 소프트맥스는 밑이 e인데 엘로 식은 밑이 10이라 바로 안 맞아요. 밑부터 바꿔 볼게요. 10x/400 = ex·ln 10/400.
선생님
그러면 지수 안에 레이팅이 어떤 모양으로 들어가 있어요?
이서연
레이팅에 ln 10 / 400이 곱해져 있어요. 173.7로 나눈 것과 같고요. 그게 로짓인가 봐요.
김민준
근데 소프트맥스는 분자에 e의 거듭제곱이 있고 분모가 그 합이잖아. 엘로 식은 분자가 1인데?
이서연
분자 분모에 첫 선수의 로짓으로 만든 e의 거듭제곱을 곱해 볼게. … 두 선수의 로짓을 넣은 소프트맥스와 똑같은 식이 돼. 200점 차이는 로짓 차이로 1.15고.
김민준
레이팅이 로짓이었네요. 과제 점수에 모두 10점씩 얹어 줘도 누가 누구보다 몇 점 잘했는지는 그대로인 것처럼, 레이팅도 다 같이 올려도 승부 예측은 그대로고요.
문제 2. 확률표에서 로짓으로
어떤 언어 모델이 세 후보 토큰에 확률표 (0.5, 0.3, 0.2)를 내놓았다고 하자. (가) 이 확률표를 내놓는 로짓을 한 벌 구하라. (나) 그런 로짓은 몇 벌이나 있는가? 세 번째 후보의 로짓을 0으로 정하면 어떻게 되는가? (다) 거꾸로, 로짓 (2, 1, −1)을 소프트맥스 없이 그대로 합으로 나눠 확률표를 만들면 안 되는 이유는 무엇인가? (위 위젯의 막대는 0.1 단위로 움직이며, 막대를 끌어 구한 로짓을 넣으면 오른쪽 확률표로 답을 확인할 수 있다.)
위젯에 (0.9, 0.4, 0)을 넣으니 (0.497, 0.301, 0.202)예요. 0.1 단위라 똑같지는 않아도 거의 맞아요. 그래서 numpy 구현에서 최댓값을 빼고 계산하는구나. 한 후보를 기준으로 빼도 답이 안 바뀌니까.
이서연
부정적분에서 적분상수를 아무렇게나 잡아도 미분하면 같은 함수가 나오잖아요. 로짓도 그런 거네요. 기준점은 아무 데나 잡아도 되고 차이만 의미가 있어요.
선생님
그래서 로짓 자체는 분포가 아니에요. 분포의 "이름표"일 뿐이고, 같은 분포에 이름표가 무수히 많아요. 이 사실은 뒤에서 여러 번 다시 중요해져요.
문제 3. 한 토큰만 밀어주기
글을 만들어 내는 서비스는 어떤 토큰을 더 자주 나오게 하거나 아예 나오지 않게 하려고, 모델이 내놓은 로짓 가운데 그 토큰의 로짓에만 값을 더하거나 빼곤 한다(허깅페이스 transformers 라이브러리의 생성 설정에도 토큰에 더할 값과 막을 토큰 목록을 적는 자리가 있다). 모델이 “오늘 저녁은” 다음의 세 후보 「치킨」「라면」「굶는다」에 로짓 (2, 1, −1)을 내놓았다. 확률표로는 (0.705, 0.259, 0.035)다. (가) 「라면」의 로짓에만 2를 더하면 확률표는 어떻게 되는가? (나) 이때 「치킨」과 「굶는다」의 확률은 각각 얼마나 줄었는가? 두 후보의 확률 비는 바뀌었는가? (다) 「굶는다」의 확률을 0.001 아래로 낮추려면 그 로짓에서 얼마보다 많이 빼야 하는가? 확률을 정확히 0으로 만들려면? (위 위젯의 「라면」 막대를 3으로 끌어 (가)를 확인할 수 있다.)
세 후보의 로짓 (2, 1, −1)과 지금의 확률표 (0.705, 0.259, 0.035). 「라면」의 로짓에 2를 더하고 「굶는다」의 로짓에서 얼마를 뺄지 묻는다