루나에게 말 걸기: 같은 시작 문장에 왜 매번 다른 이야기가 나올까
이 장의 물음
대화형 언어 모델에게 같은 질문을 두 번 하면 답이 조금씩 다르게 나오고, 「다시 생성」 단추를 누를 때마다 다른 문장이 나온다. 학습이 끝난 모델은 파라미터가 바뀌지 않는데도 그런데, 반대로 설정에서 「온도」를 0으로 내리면 같은 질문에 늘 같은 답이 돌아온다. 모델이 내놓는 것은 다음 토큰의 확률표 하나뿐인데, 그 표에서 글 한 줄을 만들어 내는 과정의 어디에서 이런 차이가 생길까?
동화를 쓰는 작은 GPT 를 학습해 두었으니 이제 그 모델에게 말을 걸 수 있다. 시작 문장을 주고 이어 쓰게 하면 되는데, 이어 쓸 때마다 확률표에서 토큰을 하나씩 골라야 한다. 이 장은 다음 물음에 차례로 답한다.
- 늘 확률이 가장 큰 토큰만 고르면 어떤 글이 나올까?
- 확률표를 그대로 두지 않고 펴거나 오므려서 뽑으면 글은 어떻게 달라질까? 그 설정을 왜 「온도」라고 부를까?
- 확률이 아주 작은 토큰까지 뽑힐 수 있게 두면 무엇이 망가지고, 어디서 잘라 내면 될까?
- 한 토큰을 쓸 때마다 앞의 글 전체를 다시 계산해야 할까? 그 계산을 아끼면 언제, 얼마나 빨라질까?
그리디 선택: 늘 가장 그럴듯한 토큰만 고르면
라운지 창가 자리에서 민준은 노트북으로 자기 게이밍 GPU 에 접속해 둔 화면을 서연에게 돌려놓았다. 동화를 쓰는 루나는 생겼지만, 서연이 몰래 넣은 장난 말고는 「Once upon a time」만 넣어 봤을 뿐이었다.

이제 말을 걸어 보자. 루나가 하는 건 결국 이어 쓰기니까, 우리가 첫 문장을 주면 되잖아.

시작 문장은 내가 쓸게. 이번엔 장난 안 쳐. 너한테 맡기면 별 얘기만 넣을 거잖아.
서연이 이번에 적은 시작 문장은 「Once upon a time there was a boy named Minjun.」이었다. 이 장이 끝나면 루나는 같은 시작 문장에서 매번 다른 이야기를 쓰고, 여러 줄을 한꺼번에 쓸 때는 지금보다 여덟 배 빨리 쓰게 된다.
이어 쓰기 한 번은 지금까지의 토큰을 모델에 넣어 다음 토큰의 확률표를 받고, 표에서 토큰 하나를 골라 줄 끝에 붙이는 일이다. 붙인 토큰까지 넣어 다시 표를 받고, 또 하나를 고르기를 되풀이한다. 표를 받는 데까지는 학습한 모델이 하고, 표에서 하나를 고르는 규칙은 우리가 정한다. 가장 먼저 떠오르는 규칙은 표에서 확률이 가장 큰 토큰을 고르는 것이다.
이어 쓰기에서는 다음 표를 받으려면 토큰 하나가 줄 끝에 붙어 있어야 하므로, 토큰마다 표를 숫자 하나로 줄이는 일을 피할 수 없다. 그래서 물음은 「어떻게 줄일까」이고, 공용 모듈의 생성 함수는 줄이는 규칙을 한 곳에 모아 두었다.
# sources/common/sample.py 의 generate 에서 (발췌)
logits = logits[:, -1, :].float() # 마지막 자리의 로짓 = 다음 토큰의 점수
probs = filter_logits(logits, temperature, top_k, top_p)
if probs is None: # temperature == 0: 그리디 선택
nxt = logits.argmax(dim=-1, keepdim=True)
else: # 거른 확률표에서 하나를 뽑는다
nxt = torch.multinomial(probs, 1, generator=generator)
out = torch.cat([out, nxt], dim=1) # 줄 끝에 붙이고 다음 걸음으로
argmax 줄처럼 매번 확률이 가장 큰 토큰을 고르는 규칙을 그리디 선택(greedy decoding)이라 한다. 다음 토큰 하나만 보고 가장 좋아 보이는 것을 고를 뿐 그 뒤를 생각하지 않는다는 뜻의 이름이다. 무작위가 없으므로 같은 시작 문장이면 늘 같은 글이 나온다. 타이니스토리즈로 학습한 27,579,392 파라미터 모델에 서연의 시작 문장을 넣고 200 토큰을 그리디 선택으로 이어 쓰게 했다(이야기 끝 토큰이 나와도 멈추지 않게 했다).
### 그리디 (τ=0): 4-토큰 되풀이 비율 0.056, 평균 놀람 0.577 nats/토큰 (샘플 1 개)
[1] He was very curious and loved to explore. One day, he decided to go on an adventure. He walked and walked until he came to a big, tall mountain. He was so excited! He wanted to climb the mountain.\nMinjun started to climb the mountain. … But then he heard a loud noise. It was a big, scary monster! … He caught Minjun and he was never seen again.\nThe end.Once upon a time, there was a little girl named Lily. She had a big, soft bed that she loved to sleep in. …

산에 올라갔다가 괴물한테 잡혀서 다시는 안 보였대. 이게 제일 그럴듯한 이야기라고?

그리고 바로 Lily 이야기로 넘어가. 너는 퇴장이고.
글 자체는 매끄럽고, 같은 말을 끝없이 되풀이하지도 않는다. 「되풀이 비율」은 생성한 토큰 가운데 바로 앞 네 토큰 묶음이 같은 샘플 안에 이미 나왔던 것의 비율인데, 그리디 선택은 0.056 이었다. 그리디 선택을 하면 같은 구절을 맴돈다는 이야기가 많지만, 이 모델의 200 토큰에서는 그런 일이 드물었다. 이상한 일은 이야기가 끝난 뒤에 일어났다. 시작 문장 다섯 개로 500 토큰씩 그리디 선택을 해서, 이야기가 끝난 뒤 무엇이 나오는지 세어 보았다.
| 시작 문장 | 이야기 끝 토큰 | 「Lily」 | 「bird」 |
|---|---|---|---|
| Once upon a time there was a boy named Minjun. | 2번 | 14번 | 9번 |
| Lily and Tom | 1번 | 14번 | 1번 |
| The cat | 3번 | 16번 | 5번 |
| Minjun liked Seoyeon. | 2번 | 8번 | 17번 |
| One day, | 3번 | 19번 | 0번 |
다섯 시작 문장 모두에서 「Lily」가 여덟 번 넘게 나왔고, 넷에서 「bird」가 나왔다. 이야기가 한 번 끝나면 어느 시작 문장에서 출발했든 같은 이야기로 빠져들기 때문이다. 「The cat」에서 출발한 글의 마지막 80 토큰은 이렇다.
[The cat] … 마지막 80 토큰: there was a little girl named Lily. She had a big, red ball. Lily loved to play with her ball every day. One day, she saw a small, helpless bird with a hurt wing. The bird could not fly.\nLily wanted to help the bird. She said, "Don't worry, bird. I will help you." She took the bird to her mom. Her mom
그리디 선택은 같은 문맥에서 늘 같은 토큰을 고른다. 학습 데이터에서 이야기 끝 토큰 뒤에 오는 이야기는 앞 이야기와 상관없는 새 이야기였으므로, 모델은 이야기 끝 토큰 뒤에서 앞의 글을 거의 보지 않도록 배웠을 것이다. 그렇다면 이야기 끝 토큰 뒤의 문맥은 어느 시작 문장에서 왔든 사실상 같은 문맥이고, 같은 문맥에서 그리디 선택은 같은 토큰 줄을 고른다. 그 줄이 이 모델이 아는 가장 뻔한 이야기인 「Lily 와 날개 다친 새」인데, 이 설명은 우리의 짐작이고 따로 재 보지는 않았다.

시작을 뭘로 하든 결국 Lily 랑 새야. 다섯 번 다.

너랑 똑같네. 무슨 얘기로 시작해도 결국 별빛 정거장이랑 루나로 끝나잖아. 온도가 낮아.

온도?
문제 1. 한 스텝씩 가장 좋은 것이 두 스텝 전체로도 가장 좋은가
토큰 두 개로 된 글을 쓰는 모델이 있다. 첫 토큰은 A(확률 0.6) 또는 B(0.4)다. A 다음에는 C 0.4, D 0.3, E 0.3 이고, B 다음에는 F 0.9, G 0.1 이다. (가) 그리디 선택으로 쓰는 글과 그 글의 확률은? (나) 두 토큰짜리 글 다섯 개 가운데 확률이 가장 큰 글은? (다) 두 답이 다른 이유를 한 문장으로 말하라.

(가)는 바로 돼. 첫 토큰은 0.6 인 A, 그다음은 0.4 인 C. 글 「AC」의 확률은 0.6 × 0.4 = 0.24.

(나)는 다섯 개를 다 곱해 볼게. AC 0.24, AD 0.18, AE 0.18, BF 0.4 × 0.9 = 0.36, BG 0.04. 가장 큰 건 BF 0.36 이야.

그리디 선택은 첫 토큰에서 B 를 버렸어요. 그때 버린 것은 무엇이었죠?

B 뒤에 F 가 0.9 로 거의 확실하게 온다는 정보요. A 는 첫 토큰에서 이겼지만, A 뒤의 표는 셋으로 고르게 퍼져 있어서 어느 걸 골라도 0.4 를 못 넘어요.

그럼 (다)는 「한 스텝의 확률만 보면 그 뒤의 표가 얼마나 뾰족한지를 못 본다」야. 스텝마다 가장 좋은 것을 이어 붙인 게 전체로 가장 좋은 건 아니고.

그래요. 글 전체의 확률을 가장 크게 하는 글을 찾으려면 갈 수 있는 글을 모두 봐야 하는데, 토큰이 200 개만 돼도 후보 글의 수는 셀 수 없이 많아요. 그리디 선택은 그 탐색을 한 스텝 앞만 보는 것으로 줄인 근사예요.

그러면 문제가 작을 때는 다섯 개를 다 곱해 보면 되는데, 실제 모델에서는 그리디 선택이 놓친 BF 같은 글이 있는지조차 모르는 거네요.
문제 2. 시작 문장을 바꿔도 Lily 가 나오는 이유
그리디 선택으로 500 토큰을 쓰게 하면 시작 문장 다섯 개 모두에서 「Lily」가 8~19번 나오고, 「The cat」에서 출발한 글은 「a little girl named Lily … a small, helpless bird with a hurt wing」으로 이어졌다. (가) 이야기 끝 토큰 바로 뒤에서 모델이 앞 이야기를 거의 보지 않는다면, 다섯 시작 문장의 이야기 끝 뒤에 나오는 글은 서로 어떻게 될까? (나) 「Lily and Tom」은 Lily 가 시작 문장에 이미 있었다. 이 시작 문장의 「Lily」 14번은 다른 시작 문장의 14번과 같은 이유로 나온 것인가? (다) 그리디 선택을 그대로 두고 다섯 글이 서로 다른 이야기가 되게 하려면 무엇을 바꿔야 하나?

(가)부터. 이야기 끝 뒤의 문맥이 사실상 같으면 그리디 선택은 같은 토큰을 고르고, 고른 토큰이 같으니 다음 문맥도 같아. 그러면 끝까지 같은 글이 나와.

출력에서 확인할 수 있어. 「Minjun liked Seoyeon.」이랑 「Once upon a time there was a boy named Minjun.」의 마지막 80 토큰에 둘 다 「She picked up the bird and put it in her pocket. She took the bird home and gave it food and water. The bird started to feel better.」가 글자 하나 안 틀리고 들어 있어.

(나)는 어때요? 「Lily and Tom」의 마지막 80 토큰도 새 이야기예요?

이건 케이크 굽는 이야기예요. 「helped her mom make the cake」, 「Lily’s friends came over to the party」. 이야기 끝 토큰이 한 번밖에 안 나와서, 새 Lily 이야기가 아직 시작 문장의 Lily 를 이어 쓰는 중일 수도 있어요.

그럼 이 글의 14번에는 시작 문장의 Lily 를 이어 쓴 것과 이야기 끝 뒤에 새로 나온 Lily 가 섞여 있겠네. 횟수가 같다고 이유가 같다고 할 수는 없어.

(다)는요? 모델은 그대로 두고요.

그리디 선택은 같은 문맥에서 늘 같은 걸 고르니까, 문맥이 같아지는 걸 막을 수 없으면 고르는 규칙에 무작위를 넣어야 해. 확률표에서 확률대로 뽑으면 같은 문맥에서도 다른 토큰이 나오니까.

그게 이 장의 나머지 이야기예요. 표대로 뽑는다고 할 때, 그 표를 그대로 쓸지 손을 볼지가 다음 물음이고요.