신고 티켓
#01 · 첫 신고
서점의 신고
"손님이 '비 오는 날 읽기 좋은 단편 하나 추천해 줘'라고 두 번 물었는데, 처음엔 「운수 좋은 날」, 다시 물으니 「소낙비」를 권했대요. 챗봇이 고장 난 거 아닌가요?" (책방 '봄날' 사장님)
오늘 추적할 것
챗봇이 답을 만드는 과정을 한 토큰씩 느리게 돌려 보고, 같은 질문에서 답이 갈라지는 자리를 찾는다. 세기만 해서 만드는 가장 작은 언어 모델로 '내 LLM'을 시작한다.
도란 사무실 · 옛 인쇄소 건물 3층
출근 둘째 주 월요일. 옛 인쇄소 건물 3층 사무실에는 아래층에서 올라오는 인쇄기 냄새가 아직 희미하게 남아 있습니다. 하은이 지난주에 넘겨받은 챗봇 서버 코드를 읽고 있는데, 보라 매니저가 전화기를 귀에서 떼며 달려옵니다. "챗봇이 또 이상한 말을 했어요! 봄날 사장님인데요, 손님이 같은 걸 두 번 물었는데 추천한 책이 달랐대요. 처음엔 「운수 좋은 날」, 다시 물으니 「소낙비」. 사장님은 챗봇이 고장 난 줄 아세요."
하은이 서버 로그를 찾아봅니다. 두 요청은 글자 하나까지 똑같았습니다. "그럼 API 쪽에서 캐시가 꼬였나요? 아니면 모델이 답을 몇 개 미리 만들어 두고 돌아가며 보여 주는 거예요?"
옆자리의 정 선임이 모니터에서 눈을 떼지 않고 말합니다. "추측하지 말고 찍어 보자." 그러고는 곧 덧붙입니다. "그런데 API 모델은 글자만 돌려줘. 속은 못 찍어. 대신 이걸 보자." 그가 띄운 것은 작은 언어 모델 하나입니다. 서점 연합(서점 열두 곳)이 디지털로 정리해 둔, 저작권이 끝난 한국 단편소설 86편으로 정 선임이 연습 삼아 학습시킨 모델입니다. "크기는 비교도 안 되게 작지만, 답을 만드는 방식은 같아. 그리고 이건 숫자 하나까지 다 보여."
이번 교시에서는 이 참고 모델이 답을 쓰는 화면을 느리게 돌려 봅니다. 답이 한 토큰씩 만들어지는 고리(생성 루프)를 보고, 같은 질문에 답이 갈라지는 자리를 찾습니다. 이 강의가 따라갈 여덟 정거장의 지도를 펼친 뒤, 하은과 함께 '내 LLM'의 첫 부품을 만듭니다. 학습도 필요 없이 세기만 하면 되는, 가장 작은 언어 모델입니다.
챗봇에게 물으면 답이 한 번에 뜨지 않고 몇 글자씩 흘러나옵니다. 보기 좋으라고 일부러 천천히 보여 주는 것이 아닙니다. 모델이 실제로 한 번에 한 조각씩 만들기 때문입니다. 이 조각을 토큰(token) 이라고 합니다. 낱말 하나일 때도 있고, 낱말의 일부나 빈칸, 문장부호일 때도 있습니다. 글을 어떻게 조각으로 자르는지는 2교시에서 봅니다.
아래 실습대는 그 흐름을 느리게 돌립니다. 토큰을 하나 만들 때마다 모델이 내놓은 다음 토큰 후보의 확률이 막대로 자라고, 그중 하나가 뽑혀(주황) 문장 끝에 붙습니다. 숫자는 흉내가 아닙니다. 정 선임의 참고 모델(어휘 2,585개, 매개변수 1,140,608개)을 이 브라우저 안에서 그대로 돌린 값입니다. 처음 한 번 약 2.4MB를 내려받습니다.
막대 이름의 ␣는 낱말 앞의 빈칸, ↵는 줄바꿈입니다. 예를 들어 ␣그는 '빈칸 + 그', 즉 새 낱말을 '그'로 시작하는 조각입니다. ␣ 하나만 있는 토큰은 "여기서 새 낱말이 시작된다"는 조각이고, 낱말의 글자는 다음 토큰이 채웁니다.
모델을 불러오는 중… (약 2.4MB, 처음 한 번만)
실습대에서 본 것을 정리합니다.
이것이 언어 모델(language model) 이 하는 일의 전부입니다. 앞의 토큰들을 받아, 다음 토큰의 확률 분포를 내놓는 함수. 기호로는 이렇게 씁니다.
는 지금까지의 토큰 개이고, 세로 막대 는 "이것이 주어졌을 때"로 읽습니다. 결과는 어휘에 있는 토큰마다 확률 하나씩, 모두 더하면 1입니다. 참고 모델의 어휘는 2,585개이므로 막대도 2,585개입니다.
답 한 줄은 이 함수를 여러 번 부른 결과입니다. 순서를 글로 쓰면 이렇습니다.
ids = 앞말을 토큰 번호로 바꾼 것 # '오늘 저녁은' → ␣오늘 / ␣저녁 / 은
되풀이:
probs = 모델(ids) # 다음 토큰의 확률 (어휘 크기만큼)
t = probs에서 하나 뽑기
ids 끝에 t를 붙인다 # 뽑은 토큰이 다음 차례의 입력이 된다
t가 '끝' 표시이거나 길이가 다 찼으면 멈춘다뽑은 토큰을 다시 입력 끝에 붙여 그다음 토큰을 만드는 이 방식을 자기회귀(autoregressive) 생성이라고 합니다. 챗봇 화면에 답이 흘러나오는 것은 이 고리가 한 바퀴 돌 때마다 토큰 하나를 화면으로 보내기 때문입니다.
여기서 하은의 추측 하나가 풀립니다. 답은 미리 정해져 있지 않습니다. 모델은 바퀴마다 '다음 한 토큰'의 확률만 계산합니다. 둘째 토큰의 막대는 첫 토큰이 뽑힌 뒤에야 계산됩니다. 첫 토큰이 다르면 둘째 토큰의 막대부터 다릅니다.
큰 챗봇 모델은 어휘에 '답 끝'을 뜻하는 특별한 토큰을 두고, 그것이 뽑히면 멈춥니다. 참고 모델은 소설을 이어 쓰기만 하도록 학습했으므로, 실습대는 정해 둔 길이(24토큰)에서 멈춥니다.
문장 하나의 확률. 토큰마다 확률이 있으니, 답 한 줄이 그대로 나올 확률은 그 확률들을 차례로 곱한 것입니다. 모델 그대로의 확률로 '오늘 저녁은' 뒤에 '먹을 때'가 나올 확률을 손으로 계산해 봅시다. 참고 모델이 낸 값은 이렇습니다.
| 지금까지의 글 | 다음 토큰 | 확률 |
|---|---|---|
| 오늘 저녁은 | ␣먹 | 1.15% |
| 오늘 저녁은 먹 | 을 | 21.00% |
| 오늘 저녁은 먹을 | ␣때 | 10.59% |
약 0.026%, 4천 번에 한 번꼴입니다. 눈여겨볼 점이 있습니다. '먹' 하나만 보면 1.15%로 낮지만, '먹'이 정해진 뒤의 '을'은 21%입니다. 앞이 정해질수록 다음 후보가 좁아집니다. 실습대 오른쪽의 "확률을 모두 곱하면 약 10의 몇 제곱"이 바로 이 곱입니다. 첫 답(씨앗 1)의 24토큰은 약 , 둘째 답(씨앗 2)은 약 였습니다.
NumPy로는
확률을 곱할 때는 로그를 더하는 쪽이 편합니다. 토큰이 수백·수천 개가 되면 곱이 컴퓨터 소수가 나타낼 수 있는 가장 작은 수보다도 작아지기 때문입니다. 9교시의 손실(loss)도 이 로그에서 나옵니다.
>>> import math
>>> p = [0.0115, 0.2100, 0.1059] # ␣먹, 을, ␣때
>>> round(math.prod(p), 6)
0.000256
>>> round(sum(math.log10(x) for x in p), 2) # 곱 대신 로그의 합: 10의 -3.59제곱
-3.59이제 봄날의 신고로 돌아갑니다. 같은 앞말을 넣으면 모델은 언제나 같은 막대를 냅니다. 막대를 계산하는 데에는 무작위가 없습니다. 답이 갈라지는 곳은 단 하나, 막대에서 하나를 뽑는 순간입니다.
뽑기는 막대 길이만큼 면이 넓은 주사위를 던지는 것과 같습니다. 그 주사위가 어떤 순서로 나올지를 정하는 수가 씨앗(seed) 입니다. 실습대는 첫 답에 씨앗 1을 쓰고, '다시'를 누를 때마다 2, 3, …으로 바꿉니다. 그래서 첫 화면은 누가 열어도 같고, '다시'를 누르면 답이 갈라집니다.
참고 모델로 '오늘 저녁은'을 씨앗 1부터 100까지 백 번 이어 쓰게 해 보았습니다(실습대의 챗봇 설정, 24토큰).
| 잰 것 | 결과 |
|---|---|
| 첫 토큰의 종류 | 29가지 |
| 24토큰 답의 종류 | 100가지 (모두 다름) |
첫 토큰부터 29갈래로 갈리고, 그 뒤 토큰마다 다시 갈립니다. 갈림길마다 후보가 40개라면 24토큰짜리 길은 가지입니다. 실제로는 막대가 고르지 않아 자주 가는 길이 따로 있지만, 그런 길조차 확률이 정도라서 같은 답이 다시 나오기는 어렵습니다. 봄날의 손님이 받은 두 답도 이렇게 갈라졌습니다. 「운수 좋은 날」과 「소낙비」 모두 '비 오는 날의 단편'으로 막대가 꽤 길었을 것이고, 두 번의 뽑기가 서로 다른 쪽을 골랐을 뿐입니다. 고장이 아닙니다.
반대로 뽑지 않고 늘 가장 긴 막대만 고르면(탐욕 선택, greedy) 답은 언제나 같습니다. 대신 다른 문제가 생깁니다. 참고 모델에 앞말 '그는'을 주고 늘 1등만 고르게 하면 이렇게 됩니다.
그는 깜짝 놀라면서 깜짝 놀라서 깜짝 놀라서 깜짝 놀라서 깜짝 놀라…
한 번 '깜짝 놀라서'로 들어서면, 그 뒤에 가장 그럴듯한 말이 또 '깜짝 놀라서'가 되는 늪에 빠집니다. 다양함과 안정 사이의 손잡이(온도, top-k, top-p)는 7교시의 주제입니다.
NumPy로는
NumPy의 난수기도 씨앗으로 만듭니다. 같은 씨앗이면 같은 순서로 뽑히고, 많이 뽑으면 비율이 확률을 따라갑니다. 여덟 번만 뽑으면 확률 0.3인 '국수'가 0.6인 '밥'보다 많이 나오기도 합니다.
>>> import numpy as np
>>> menu = ["밥", "국수", "죽"]
>>> p = np.array([0.6, 0.3, 0.1])
>>> rng = np.random.default_rng(0) # 씨앗 0
>>> [menu[i] for i in rng.choice(3, size=8, p=p)]
['국수', '밥', '밥', '밥', '국수', '죽', '국수', '국수']
>>> rng = np.random.default_rng(0) # 같은 씨앗이면 같은 순서
>>> [menu[i] for i in rng.choice(3, size=8, p=p)]
['국수', '밥', '밥', '밥', '국수', '죽', '국수', '국수']
>>> rng = np.random.default_rng(1) # 씨앗을 바꾸면 다른 순서
>>> [menu[i] for i in rng.choice(3, size=8, p=p)]
['밥', '죽', '밥', '죽', '밥', '밥', '국수', '밥']
>>> rng = np.random.default_rng(0)
>>> np.bincount(rng.choice(3, size=10000, p=p)) / 10000 # 만 번 뽑은 비율
array([0.6019, 0.3001, 0.098 ])정 선임의 한마디
씨앗을 고정하면 같은 답이 나오는 건, 우리 실습대처럼 계산 전체를 우리가 쥐고 있을 때 얘기야. API 너머의 계산까지 우리가 다 정할 수는 없어. 영업시간이나 주소처럼 답이 하나뿐인 건 모델이 뽑게 두지 말고, 서점 정보표에서 그대로 꺼내 보여 줘.
오늘 본 막대는 모델이라는 긴 길의 출구에서 나온 것입니다. 이 강의는 문장 하나(오늘 저녁은)를 들고 그 길을 한 정거장씩 따라갑니다.
| 정거장 | 하는 일 | 교시 |
|---|---|---|
| 1. 생성 루프 | 다음 토큰의 확률을 받아 하나 뽑고, 붙이고, 되풀이 | 1 (오늘) |
| 2. 토큰화 | 글을 조각(토큰)으로 자르고 조각마다 번호를 붙인다 | 2 |
| 3. 임베딩과 위치 | 번호를 숫자 목록(벡터)으로 바꾸고 자리 정보를 더한다 | 3 |
| 4. 어텐션 점수 | 앞의 어느 토큰을 볼지 점수를 매긴다 | 4 |
| 5. 어텐션 섞기와 여러 머리 | 본 토큰들의 정보를 섞는다. 여러 관점으로 따로 | 5 |
| 6. 블록과 잔차 | 층을 쌓으며 조금씩 고쳐 쓰고, 끝에서 어휘 크기의 점수(로짓)를 낸다 | 6 |
| 7. 출구와 샘플링 | 점수를 확률로 바꾸고 온도·top-k·top-p로 하나를 고른다 | 7 |
| 8. KV 캐시와 문맥 창 | 앞에서 한 계산을 쌓아 두어 빨리 돌고, 볼 수 있는 길이의 끝을 안다 | 8 |
토큰 하나는 2번에서 번호가 되고, 3번에서 숫자 목록이 되고, 4–6번에서 앞 토큰들과 섞이며 고쳐 써지고, 7번에서 막대가 됩니다. 1번 생성 루프가 그중 하나를 뽑아 다시 입구로 돌려보냅니다. 8번은 그 고리를 빠르게 도는 방법입니다.
9–12교시는 정거장이 아니라 공장입니다. 모델 속의 숫자를 어떻게 배우는지(9), 서점의 책으로 실제로 학습시키기(10), 학습한 모델 해부하기(11), 질문에 답하게 다듬기(12). 그리고 교시마다 하은과 함께 부품을 하나씩 짜서, 12교시가 끝나면 자기 컴퓨터에서 학습시킨 작은 LLM이 남습니다.
정 선임이 참고 모델 창을 닫으며 말합니다. "API 모델은 끝까지 못 열어 봐. 그런데 이 정도 모델은 하은 씨도 만들 수 있어. 말뭉치도 있잖아." 서점 연합의 단편소설 86편, 약 162만 자입니다. "하나 만들어 봐. 자기가 만든 모델은 숫자 하나까지 볼 수 있어." 하은은 그 자리에서 새 폴더를 만듭니다. 이름은 mylm. 정 선임이 덧붙입니다. "제일 작은 것부터 가자. 학습도 필요 없어. 세기만 하면 돼."
'다음 토큰의 확률'을 가장 단순하게 구하는 방법은 말뭉치에서 직접 세는 것입니다. 오늘은 토큰을 글자 하나로 둡니다(빈칸과 줄바꿈도 글자). 그리고 바로 앞 글자 하나만 보고 다음 글자의 확률을 정합니다. 둘씩 짝지어 세므로 이런 모델을 바이그램(bigram) 모델이라고 합니다.
손으로 한 번 해 봅시다. 글이 "오늘 저녁은 오늘 점심은"(13글자, 이웃 쌍 12개)뿐이라면, 빈칸 다음에는 '저', '오', '점'이 한 번씩 왔습니다. 그래서 빈칸 다음 글자의 확률은 셋 다 입니다. '오' 다음에는 두 번 모두 '늘'이 왔으므로 '늘'의 확률이 1입니다. 이 작은 표로 이어 쓰면 "오늘 점심은 저녁은 저녁"처럼 원문에 없던 조합도 나옵니다. '은' 다음 빈칸, 빈칸 다음 '저'처럼 짝 하나하나는 원문에 있던 것인데, 짝을 이어 붙이면 새 글이 됩니다.
아래 실습대는 김유정의 「동백꽃」 한 편(이 강의 말뭉치에 실린 그대로)을 세어 만든 바이그램 모델입니다.
바이그램이 쓴 글
나
나'나' 다음에 온 글자 · 「동백꽃」에서 모두 91번 · 서로 다른 글자 25개
막대 길이 = 비율 (칸 끝까지가 100%) · ␣ = 빈칸, ↵ = 줄바꿈
붙인 글자 0 / 60 · 씨앗 1
'나' 다음에 온 글자를 「동백꽃」에서 센 결과입니다. 이 횟수를 비율로 바꾼 것이 바이그램 모델의 전부입니다. '이어 쓰기'를 누르세요.
| ␣ | 이 | 다 | 고 | 는 | . | 리 | 을 | |
|---|---|---|---|---|---|---|---|---|
| ␣ | 0 | 35 | 16 | 25 | 0 | 0 | 0 | 0 |
| 이 | 92 | 0 | 20 | 4 | 1 | 0 | 0 | 0 |
| 다 | 23 | 0 | 0 | 5 | 0 | 114 | 1 | 0 |
| 고 | 123 | 0 | 0 | 0 | 8 | 0 | 0 | 0 |
| 는 | 113 | 0 | 9 | 0 | 0 | 0 | 0 | 0 |
| . | 80 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 리 | 23 | 0 | 1 | 19 | 6 | 0 | 0 | 0 |
| 을 | 94 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
「동백꽃」 6,895자 · 서로 다른 글자 572개 · 표 572×572 = 327,184칸 중 0이 아닌 칸 2,313개
오늘도 또 우리 수탉이 막 쫓기었다. 내가 점심을 먹고 나무를 하러 갈 양으로 나올 때이었다. 산으로 올라서려니까 등뒤에서 푸드득푸드득, 하고 닭의 횃소리가 야단이다. 깜짝 놀라서 고개를 돌려보니 아니나다르랴, 두 놈이 또 얼리었다. 점순네 수탉(은 대강이가 크고 똑 오소리같이 실팍하게 생긴 놈)이 덩저리 작은 우리 수탉을 함부로 해내는 것이다. 그것도 그냥 해내는 것이 아니라 푸드득하고 면두를 쪼고 물러섰다가 좀 사이를 두고 푸드득하고 모가지를 쪼았다. 이렇게 멋을 부려 가며 여지없이 닦아 놓는다. 그러면 이 못생긴 것은 쪼일 적마다 주둥이로 땅을 받으며 그 비명이 킥, 킥, 할 뿐이다. 물론 미처 아물지도 않은 면두를 또 쪼이며 붉은 선혈은 뚝뚝 떨어진다. 이걸 가만히 내려다보자니 내 대강이가 터져서 피가 흐르는 것같이 두 눈에서 불이 번쩍 난다. 대뜸 지게막대기를 메고 달려들어 점순네 닭을 후려칠까 하다가 생각을 고쳐먹고 헛매질로 떼어만 놓았다. 이번에도 점순이가 쌈을 붙여 놨을 것이다. 바짝바짝 내 기를 올리느라고 그랬음에 틀림없을 것이다. 고놈의 계집애가 요새로 들어서 왜 나를 못 먹겠다고 고렇게 아르릉거리는지 모른다. 나흘 전 감자 쪼간만 하더라도 나는 저에게 조금도 잘못한 것은 없다. 계집애가 나물을 캐러 가면 갔지 남 울타리 엮는 데 쌩이질을 하는 것은 다 뭐냐. 그것도 발소리를 죽여 가지고 등뒤로 살며시 와서, "얘! 너 혼자만 일하니?" 하고 긴치 않는 수작을 하는 것이다. 어제까지도 저와 나는 이야기도 잘 않고 서로 만나도 본체 만 척하고 이렇게 점잖게 지내던 터이련만 오늘로 갑작스레 대견해졌음은 웬일인가. 항차 망아지만 한 계집애가 남 일하는 놈 보구……. "그럼 혼자 하지 떼루 하디?" 내가 이렇게 내배앝는 소리를 하니까, "너 일하기 좋니?" 또는, "한여름이나 되거든 하지 벌써 울타리를 하니?" 잔소리를 두루 늘어놓다가 남이 들을까 봐 손으로 입을 틀어막고는 그 속에서 깔깔댄다. 별로 우스울 것도 없는데 날씨가 풀리더니 이 놈의 계집애가 미쳤나 하고 의심하였다. 게다가 조금 뒤에는 제 집께를 할금 할금 돌아보더니 행주치마의 속으로 꼈던 바른손을 뽑아서 나의 턱밑으로 불쑥 내미는 것이다. 언제 구웠는 지 더운 김이 홱 끼치는 굵은 감자 세 개가 손에 뿌듯이 쥐였다. "느 집엔 이거 없지?" 하고 생색 있는 큰소리를 하고는 제가 준 것을 남이 알면은 큰일날 테니 여기서 얼른 먹어 버리란다. 그리고 또 하는 소리가, "너 봄 감자가 맛있단다." "난 감자 안 먹는다. 너나 먹어라." 나는 고개도 돌리지 않고 일하던 손으로 그 감자를 도로 어깨 너머로 쑥 밀어 버렸다. 그랬더니 그래도 가는 기색이 없고, 뿐만 아니라 쌔근쌔근하고 심상치 않게 숨소리가 점점 거칠어진다. 이건 또 뭐야 싶어서 그때에야 비로소 돌아다보니 나는 참으로 놀랐다. 우리가 이 동네에 들어온 것은 근 삼 년째 되어 오지만 여태껏 가무잡잡한 점순의의 얼굴이 이렇게까지 홍당무처럼 새빨개진 법이 없었다. 게다가 눈에 독을 올리고 한참 나를 요렇게 쏘아보더니 나중에는 눈물까지 어리는 것이 아니냐. 그리고 바구니를 다시 집어들더니 이를 꼭 악물고는 엎어질 듯 자빠질 듯 논둑으로 횡하게 달아나는 것이다. 어쩌다 동리 어른이, "너 얼른 시집을 가야지?" 하고 웃으면, "염려 마서유. 갈 때 되면 어련히 갈라구!" 이렇게 천연덕스레 받는 점순이었다. 본시 부끄럼을 타는 계집애도 아니거니와 또한 분하다고 눈에 눈물을 보일 얼병이도 아니다. 분하면 차라리 나의 등어리를 바구니로 한번 모질게 후려쌔리고 달아날지언정. 그런데 고약한 그 꼴을 하고 가더니 그 뒤로는 나를 보면 잡아먹으려 기를 복복 쓰는 것이다. 설혹 주는 감자를 안 받아먹는 것이 실례라 하면, 주면 그냥 주었지 '느 집엔 이거 없지.'는 다 뭐냐. 그렇잖아도 저희는 마름이고 우리는 그 손에서 배재를 얻어 땅을 부치므로 일상 굽실거린다. 우리가 이 마을에 처음 들어와 집이 없어서 곤란으로 지낼 제 집터를 빌리고 그 위에 집을 또 짓도록 마련해 준 것도 점순 네의 호의였다. 그리고 우리 어머니 아버지도 농사 때 양식이 딸리면 점순이네한테 가서 부지런히 꾸어다 먹으면서 인품 그런 집은 다시 없으리라고 침이 마르도록 칭찬하곤 하는 것이다. 그러면서도 열 일곱씩이나 된 것들이 수군수군하고 붙어 다니면 동네의 소문이 사납다고 주의를 시켜 준 것도 또 어머니였다. 왜냐하면 내가 점순이 하고 일을 저질렀다가는 점순네가 노할 것이고, 그러면 우리는 땅도 떨어지고 집도 내쫓기고 하지 않으면 안되는 까닭이었다. 그런데 이놈의 계집애가 까닭없이 기를 복복 쓰며 나를 말려 죽이려고 드는 것이다. 눈물을 흘리고 간 담날 저녁나절이었다. 나무를 한 짐 잔뜩 지고 산을 내려오려니까 어디서 닭이 죽는 소리를 친다. 이거 뉘집에서 닭을 잡나, 하고 점순네 울 뒤로 돌아오다가 나는 고만 두 눈이 똥그랬다. 점순이가 저희 집 봉당에 홀로 걸터앉았는데 이게 치마 앞에다 우리 씨암탉을 꼭 붙들어 놓고는, "이놈의 씨닭! 죽어라 죽어라." 요렇게 암팡스레 패 주는 것이 아닌가. 그것도 대가리나 치면 모른다마는 아주 알도 못 낳으라고 그 볼기짝께를 주먹으로 콕콕 쥐어박는 것이다. 나는 눈에 쌍심지가 오르고 사지가 부르르 떨렸으나 사방을 한번 휘둘러보고야 그제서야 점순이 집에 아무도 없음을 알았다. 잡은 참 지게 막대기를 들어 울타리의 중턱을 후려치며, "이놈의 계집애! 남의 닭 알 못 낳으라구 그러니?" 하고 소리를 빽 질렀다. 그러나 점순이는 조금도 놀라는 기색이 없고 그대로 의젓이 앉아서 제 닭 가지고 하듯이 또 죽어라,죽어라, 하고 패는 것이다. 이걸 보면 내가 산에서 내려올 때를 겨냥해 가지고 미리부터 닭을 잡아가지고 있다가 네 보라는 듯이 내 앞에서 줴지르고 있음이 확실하다. 그러나 나는 그렇다고 남의 집에 뛰어들어가 계집애하고 싸울 수도 없는 노릇이고 형편이 썩 불리함을 알았다. 그래 닭이 맞을 적마다 지게 막대기로 울타리를 후려칠 수밖에 별 도리가 없다. 왜냐하면 울타리를 치면 칠수록 울섶이 물러앉으며 뼈대만 남기 때문이다. 허나 아무리 생각하여도 나만 밑지는 노릇이다. "아, 이년아! 남의 닭 아주 죽일 터이야?" 내가 도끼눈을 뜨고 다시 꽥 호령을 하니까 그제서야 울타리께로 쪼르르 오더니 울 밖에 섰는 나의 머리를 겨누고 닭을 내팽개친다. "에이 더럽다! 더럽다!" "더러운 걸 널더러 입때 끼고 있으랬니? 망할 계집애년 같으니" 하고 나도 더럽단 듯이 울타리께를 횡허케 돌아내리며 약이 오를 대로 다 올랐다, 라고 하는 것은 암탉이 풍기는 서슬에 나의 이마빼기에다 물지똥을 찍 갈겼는데 그걸 본다면 알집만 터졌을 뿐 아니라 골병은 단단히 든 듯싶다. 그리고 나의 등 뒤를 향하여 나에게만 들릴 듯 말 듯한 음성으로, "이 바보 녀석아!" "애! 너 배냇병신이지?" 그만도 좋으련만, "얘! 너 느 아버지가 고자라지?" "뭐 울아버지가 그래 고자야?" 할 양으로 열벙거지가 나서 고개를 홱 돌리어 바라봤더니 그때까지 울타리 위로 나와 있어야 할 점순이의 대가리가 어디 갔는지 보이지를 않는다. 그러다 돌아서서 오자면 아까에 한 욕을 울 밖으로 또 퍼붓는 것이다. 욕을 이토록 먹어 가면서도 대거리 한 마디 못하는 걸 생각하니 돌부리에 채이어 발톱 밑이 터지는 것도 모를 만큼 분하고 급기야는 두 눈에 눈물까지 불끈 내솟는다. 그러나 점순이의 침해는 이것뿐이 아니다. 닭벼슬을 좋아한다면서도 사람들이 없으면 틈틈이 제 집 수탉을 몰고 와서 우리 수탉과 쌈을 붙여 놓는다. 제 집 수탉은 썩 험상궂게 생기고 쌈이라면 홰를 치는 고로 으레 이길 것을 알기 때문이다. 그래서 툭하면 우리 수탉이 면두며 눈깔이 피로 흐드르하게 되도록 해 놓는다. 어떤 때에는 우리 수탉이 나오지를 않으니까 요놈의 계집애가 모이를 쥐고 와서 꾀어내다가 쌈을 붙인다. 이렇게 되면 나도 다른 배차를 차리지 않을 수 없었다. 하루는 우리 수탉을 붙들어 가지고 넌지시 장독께로 갔다. 쌈닭에게 고추장을 먹이면 병든 황소가 살모사를 먹고 용을 쓰는 것처럼 기운이 뻗친다 한다. 장독에서 고추장 한 접시를 떠서 닭 주둥아리께로 들여 밀고 먹여 보았다. 닭도 고추장에 맛을 들였는지 거스르지 않고 거진 반 접시 턱이나 곧잘 먹는다. 그리고 먹고 금시는 용을 못쓸 터이므로 얼마쯤 기운이 돌도록 횃속에다 가두어 두었다. 밭에 두엄을 두어 짐 져내고 나서 쉴 참에 그 닭을 안고 밖으로 나왔다. 마침 밖에는 아무도 없고 점순이만 저희 울안에서 헌옷을 뜯는지 혹은 솜을 터는지 웅크리고 앉아서 일을 할 뿐이다. 나는 점순네 수탉이 노는 밭으로 가서 닭을 내려놓고 가만히 맥을 보았다. 두 닭은 여전히 얼리어 쌈을 하는데 처음에는 아무 보람이 없었다. 멋지게 쪼는 바람에 우리 닭은 또 피를 흘리고 그러면서도 날갯죽지만 푸드득푸드득하고 올라 뛰고 뛰고 할뿐으로 제법 한번 쪼아 보지도 못한다. 그러나 한번엔 어쩐 일인지 용을 쓰고 펄쩍 뛰더니 발톱으로 눈을 하비고 내려오며 면두를 쪼았다. 큰 닭도 여기에는 놀랐는지 뒤로 멈씰하며 물러난다. 이 기회를 타서 작은 우리 수탉이 또 날쌔게 덤벼들어 다시 면두를 쪼니 그제서는 감때사나운 그 대강이에서도 피가 흐르지 않을 수 없다. 옳다 알았다, 고추장만 먹이며는 되는구나 하고 나는 속으로 아주 쟁그러워 죽겠다. 그때에는 뜻밖에 내가 닭쌈을 붙여 놓는 데 놀라서 울 밖으로 내다보고 섰던 점순이도 입맛이 쓴지 눈쌀을 찌푸렸다. 나는 두 손으로 볼기짝을 두드리며 연방, "잘한다! 잘한다!"하고, 신이 머리끝까지 뻐치었다. 그러나 얼마 되지 않아서 나는 넋이 풀리어 기둥같이 묵묵히 서 있게 되었다. 왜냐하면 큰 닭이 한번 쪼인 앙갚음으로 호들갑스레 연거푸 쪼는 서슬에 우리 수탉은 찔끔 못하고 막 곯는다. 이걸 보고서 이번에는 점순이가 깔깔거리고 되도록 이쪽에서 많이 들으라고 웃는 것이다. 나는 보다 못하여 덤벼들어서 우리 수탉을 붙들어 가지고 도로 집으로 들어왔다. 고추장을 좀더 먹였더라면 좋았을 걸, 너무 급하게 쌈을 붙인 것이 퍽 후회가 난다. 장독께로 돌아와서 다시 턱밑에 고추장을 들이댔다. 흥분으로 말미암아 그런지 당최 먹질 않는다. 나는 하릴없이 닭을 반듯이 눕히고 그 입에다 궐련 물부리를 물리었다. 그리고 고추장물을 타서 그 구멍으로 조금씩 들여 부었다. 닭은 좀 괴로운지 킥킥하고 재채기를 하는 모양이나 그러나 당장의 괴로움은 매일 같이 피를 흘리는 데 댈 게 아니라 생각하였다. 그러나 한 두어 종지 가량 고추장물 먹이고 나서는 나는 고만 풀이 죽었다. 싱싱하던 닭이 왜 그런지 고개를 살며시 뒤틀고는 손아귀에서 뻐드러지는 것이 아닌가. 아버지가 볼까 봐서 얼른 홰에다 감추어 두었더니 오늘 아침에서야 겨우 정신이 든 모양 같다. 그랬던 걸 이렇게 오다 보니까 또 쌈을 붙여 놓으니 이 망할 계집애가 필연 우리 집에 아무도 없는 틈을 타서 제가 들어와 홰에서 꺼내 가지고 나간 것이 분명하다. 나는 다시 닭을 잡아다 가두고 염려는 스러우나 그렇다고 산으로 나무를 하러 가지 않을 수도 없는 형편이었다. 소나무 삭정이를 따며 가만히 생각해 보니 암만해도 고년의 목쟁이를 돌려놓고 싶다. 이번에 내려가면 망할 년 등줄기를 한번 되게 후려치겠다 하고 싱둥겅둥 나무를 지고는 부리나케 내려왔다. 거지반 집에 다 내려와서 나는 호드기 소리를 듣고 발이 딱 멈추었다. 산기슭에 널려 있는 굵은 바윗돌 틈에 노란 동백꽃이 소보록하니 깔리었다. 그 틈에 끼어 앉아서 점순이가 청승맞게시리 호드기를 불고 있는 것이다. 그보다도 더 놀란 것은 고 앞에서 또 푸드득, 푸드득, 하고 들리는 닭의 횃소리다. 필연코 요년이 나의 약을 올리느라고 또 닭을 집어내다가 내가 내려올 길목에다 쌈을 시켜 놓고 저는 그 앞에 앉아서 천연스레 호드기를 불고 있음에 틀림없으리라. 나는 약이 오를 대로 올라서 두 눈에서 불과 함께 눈물이 퍽 쏟아졌다. 나뭇지게도 벗어 놀 새 없이 그대로 내동댕이치고는 지게 막대기를 뻗치고 허둥허둥 달려들었다. 가까이 와 보니 과연 나의 짐작대로 우리 수탉이 피를 흘리고 거의 빈사지경에 이르렀다. 닭도 닭이려니와 그러함에도 불구하고 눈 하나 깜짝 없이 고대로 앉아서 호드기만 부는 그 꼴에 더욱 치가 떨린다. 동네에서도 소문이 났거니와 나도 한때는 걱실걱실히 일 잘 하고 얼굴 예쁜 계집애인 줄 알았더니 시방 보니까 그 눈깔이 꼭 여우새끼 같다. 나는 대뜸 달려들어서 나도 모르는 사이에 큰 수탉을 단매로 때려 엎었다. 닭은 푹 엎어진 채 다리 하나 꼼짝 못 하고 그대로 죽어 버렸다. 그리고 나는 멍하니 섰다가 점순이가 매섭게 눈을 홉뜨고 닥치는 바람에 뒤로 벌렁 나자빠졌다. "이놈아! 너 왜 남의 닭을 때려죽이니?" "그럼 어때?" 하고 일어나다가, "뭐 이 자식아! 누 집 닭인데?" 하고 복장을 떼미는 바람에 다시 벌렁 자빠졌다. 그리고 나서 가만히 생각을 하니 분하기도 하고 무안도스럽고, 또 한편 일을 저질렀으니, 인젠 땅이 떨어지고 집도 내쫓기고 해야 될는지 모른다. 나는 비슬비슬 일어나며 소맷자락으로 눈을 가리고는, 얼김에 엉 하고 울음을 놓았다. 그러나 점순이가 앞으로 다가와서, "그럼 너 이담부텀 안 그럴 테냐?" 하고 물을 때에야 비로소 살길을 찾은 듯싶었다. 나는 눈물을 우선 씻고 뭘 안 그러는지 명색도 모르건만, "그래!" 하고 무턱대고 대답하였다. "요담부터 또 그래 봐라, 내 자꾸 못살게 굴 테니." "그래 그래 이젠 안 그럴 테야!" "닭 죽은 건 염려 마라, 내 안 이를 테니." 그리고 뭣에 떠다밀렸는지 나의 어깨를 짚은 채 그대로 퍽 쓰러진다. 그 바람에 나의 몸뚱이도 겹쳐서 쓰러지며, 한창 피어 퍼드러진 노란 동백꽃 속으로 폭 파묻혀 버렸다. 알싸한, 그리고 향긋한 그 냄새에 나는 땅이 꺼지는 듯이 온 정신이 고만 아찔하였다. "너 말 마라!" "그래!" 조금 있더니 요 아래서, "점순아! 점순아! 이년이 바느질을 하다 말구 어딜 갔어?" 하고 어딜 갔다 온 듯싶은 그 어머니가 역정이 대단히 났다. 점순이가 겁을 잔뜩 집어먹고 꽃밑을 살금살금 기어서 산알로 내려간 다음 나는 바위를 끼고 엉금엉금 기어서 산 위로 치빼지 않을 수 없었다.
실습대에서 본 것을 정리합니다.
| 모델 | 쓴 글 (첫 60자 남짓) |
|---|---|
| 바이그램 (「동백꽃」을 셈) | 나 점순네의 그러앉아!" 나는지게 여 그 생각하고 하고 면서 단이 나는 그러 틈틈을 너 닭이므로 쪼는데 동네의 |
| 참고 모델 (온도 0.8, 상위 40개) | 나와서 / “글쎄요.” / 하고 다시 그 이야기를 다시 형근을 보다가 다시, / “네.“ / 하고 경을 첨지는 웃었다. / “이… |
(/는 줄바꿈) 참고 모델의 글도 줄거리는 없습니다. 하지만 낱말이 낱말답고, 따옴표로 대사를 열고 닫고, '하고'로 다음 줄을 잇는 소설의 모양을 따릅니다. 바이그램은 '이' 다음에 무엇이 오는지만 알고, 그 '이'가 '점순이'의 '이'인지 '이걸'의 '이'인지 모릅니다. 바로 앞 글자 하나만 보기 때문입니다.
그럼 앞 글자를 두 개 보면 될까요? 앞 두 글자의 조합마다 행이 하나씩 필요하므로, 말뭉치 전체의 글자로는 표가 억 칸이 됩니다. 셋, 넷을 보려면 표가 감당할 수 없이 커지고, 칸 대부분은 한 번도 채워지지 않습니다. 참고 모델은 표 대신 앞의 토큰 128개까지를 숫자 목록(벡터)으로 요약해서 다음 토큰을 계산합니다. 그 방법이 3–6교시의 정거장들입니다.
NumPy로는
표를 세는 방법은 세 줄입니다. 글자 → 번호 사전을 만들고, zip(text, text[1:])로 이웃한 두 글자를 짝지어 칸마다 1을 더합니다.
>>> import numpy as np
>>> text = "오늘 저녁은 오늘 점심은"
>>> chars = sorted(set(text))
>>> chars
[' ', '녁', '늘', '심', '오', '은', '저', '점']
>>> index = {c: i for i, c in enumerate(chars)}
>>> counts = np.zeros((len(chars), len(chars)), dtype=np.int64)
>>> for a, b in zip(text, text[1:]):
... counts[index[a], index[b]] += 1
>>> counts[index[" "]] # 빈칸 다음에 온 글자의 횟수 (열 순서는 chars)
array([0, 0, 0, 0, 1, 0, 1, 1])
>>> counts[index[" "]] / counts[index[" "]].sum()
array([0. , 0. , 0. , 0. , 0.33333333,
0. , 0.33333333, 0.33333333])
>>> int(counts.sum()) # 이웃 쌍은 글자 수 - 1개
12말뭉치 전체(1,617,462자)로 세면 표는 1,847 × 1,847이고, 정수(int64) 하나가 8바이트라서 약 27MB입니다. 이 반복문으로 세는 데 우리 데스크톱에서 0.6초 걸렸습니다.
하은의 mylm 폴더를 여러분 컴퓨터에도 만듭니다. 1–9교시는 파이썬과 NumPy만 씁니다.
mylm/
README.md
get_data.py 말뭉치를 받는 스크립트
mylm/bigram.py 1교시: 오늘 채울 파일
checks/check_01.py 1교시 확인 스크립트data/corpus.txt로, 작품 목록이 data/SOURCES.txt로 저장됩니다.$ pip install numpy
$ python get_data.py
data/corpus.txt: 3,885,989 bytes
data/SOURCES.txt: 1,833 bytes
글자 1,617,462개. 첫 줄: 질그릇이 땅에 부딪치는 소리가 났다고 들렸는데 마당엔 아무도 없다. 부엌말뭉치를 여러분이 쓴 글로 바꿔도 됩니다. 다만 강의의 숫자(글자 수, 손실 등)는 이 말뭉치 기준입니다.
mylm/bigram.py의 세 함수를 채웁니다. 키트에 들어 있는 모양 그대로입니다.
"""1교시: 글자 단위 바이그램 언어 모델. 세기만 해서 만드는 가장 작은 언어 모델."""
import numpy as np
def count_bigrams(text: str) -> tuple[list[str], np.ndarray]:
"""글자 목록 chars와 횟수 표 counts를 돌려준다.
chars: text에 나온 서로 다른 글자를 정렬한 목록(빈칸·줄바꿈도 글자다).
counts[i, j]: text에서 chars[i] 바로 다음에 chars[j]가 나온 횟수. 모양 (V, V), V = len(chars).
"""
raise NotImplementedError("1교시에서 채웁니다")
def next_probs(counts: np.ndarray, i: int) -> np.ndarray:
"""i번 글자 다음에 올 글자의 확률. counts의 i행을 그 행의 합으로 나눈다. 모양 (V,).
i번 글자 뒤에 아무 글자도 온 적이 없으면(행의 합이 0) 모든 글자에 같은 확률 1/V를 준다.
"""
raise NotImplementedError("1교시에서 채웁니다")
def generate(chars: list[str], counts: np.ndarray, start: str, n: int, seed: int = 0) -> str:
"""start 글자에서 시작해 n글자를 더 이어 쓴 글(길이 n + 1)을 돌려준다.
난수기는 rng = np.random.default_rng(seed). 한 글자마다
지금 글자의 next_probs로 rng.choice(V, p=확률)를 해서 다음 글자를 뽑고, 뽑은 글자를 다음 차례의 지금 글자로 쓴다.
"""
raise NotImplementedError("1교시에서 채웁니다")count_bigrams는 1-5절의 NumPy로는 상자와 같은 계산입니다. 함수 안에 넣고 chars, counts를 돌려주면 됩니다.next_probs에서 행의 합이 0인 경우를 잊지 마세요. 말뭉치의 마지막 글자가 그 글자의 유일한 등장이면 뒤에 온 글자가 하나도 없습니다. 0으로 나누면 nan이 됩니다.generate는 1-2절의 생성 루프를 글자 단위로 쓴 것입니다. 뽑은 글자가 다음 차례의 입력이 됩니다.다 채웠으면 키트 폴더에서 확인 스크립트를 돌립니다. 작은 글로 기준값과 비교하고, data/corpus.txt가 있으면 말뭉치 전체로도 한 번 더 셉니다. 모두 통과하면 이렇게 나옵니다(걸린 초는 컴퓨터마다 다릅니다).
$ python checks/check_01.py
[count_bigrams]
통과 chars: 서로 다른 글자 8개를 정렬한 목록
통과 counts 모양 (8, 8)
통과 모든 칸이 기준값과 같습니다. 합계 12 = 글자 수 13 - 1
[next_probs]
통과 빈칸 다음 확률 = [0, 0, 0, 0, 1/3, 0, 1/3, 1/3]
통과 '오' 다음은 '늘'이 확률 1
통과 한 번도 뒤에 글자가 온 적 없는 행은 고르게 1/V
[generate]
통과 갈림길이 없는 표에서는 언제나 같은 글: '가나다가나다가'
통과 길이 13, '오'로 시작: '오늘 오늘 점심은 점심은'
통과 이어 쓴 글의 모든 이웃 쌍이 표에 있는 쌍입니다
통과 같은 seed → 같은 글
통과 seed 0~9에서 서로 다른 글 9가지
통과 seed=0 결과가 강의 본문과 같습니다: '오늘 오늘 점심은 점심은'
[말뭉치 전체]
통과 글자 1,617,462개, 서로 다른 글자 1,847개, 표 1,847×1,847 중 0이 아닌 칸 55,457개 (0.6초)
통과 '다' 다음 '.' 25,233번 (말뭉치에서 직접 센 값과 같음)
이어 쓰기 맛보기 (seed=0):
그러나 가서서 음은, A씨 응, 소리나 그런 안고향해서 오늘 위에 비취해도 땀을 목이 않고개성님이 또 벌었다
모두 통과! 1교시 부품(바이그램 언어 모델)이 완성되었습니다.틀리면 '실패' 줄에 무엇이 다른지(어느 칸의 기대값과 받은 값 등)가 나옵니다. generate는 뽑는 방법이 달라도 위 시험들을 통과하면 됩니다. 문서대로 rng.choice를 썼다면 seed=0 결과까지 본문과 같습니다.
| 기호 | 읽는 법 | 모양 | 코드에서는 |
|---|---|---|---|
| 지금까지의 토큰 개 | 정수 | ids | |
| 앞이 주어졌을 때 다음 토큰의 확률 | , 합 1 | probs | |
| 어휘 크기 (오늘은 서로 다른 글자 수) | 수 하나 | len(chars), 참고 모델은 2,585 | |
| 다음에 가 나온 횟수 | 표 전체 | counts[i, j] | |
| 다음 글자가 일 확률 | 행 하나 | next_probs(counts, i) | |
| seed | 씨앗: 뽑기의 순서를 정하는 수 | 수 하나 | np.random.default_rng(seed) |
생성 루프는 큰 모델도 똑같습니다. 다른 것은 규모입니다. 큰 모델의 값은 공개된 자료로 알려진 대략의 자릿수입니다.
| 참고 모델 | 대형 언어 모델 (대략) | |
|---|---|---|
| 어휘 | 2,585개 | 수만–수십만 개 |
| 매개변수 | 1,140,608개 (약 ) | 수십억–수천억 개 (–) |
| 문맥 (한 번에 보는 토큰) | 128개 | 수천–수십만 개 이상 |
| 학습한 글 | 989,089토큰 (약 ) | 수조 토큰 ( 이상) |
| 토큰 하나 계산 | 브라우저에서 약 2.4ms | GPU 같은 전용 계산 장치에서 (여러 대를 묶어 쓰기도 한다) |
자릿수로 보면 매개변수는 천 배에서 십만 배, 학습한 글은 백만 배 넘게 차이 납니다. 그래도 바퀴마다 하는 일은 같습니다. 다음 토큰의 막대를 내고, 하나를 뽑고, 붙입니다.
오늘의 조립 풀이: mylm/bigram.py
mylm/bigram.py 정답입니다(키트의 빈칸을 채운 모습).
"""1교시: 글자 단위 바이그램 언어 모델. 세기만 해서 만드는 가장 작은 언어 모델."""
import numpy as np
def count_bigrams(text: str) -> tuple[list[str], np.ndarray]:
"""글자 목록 chars와 횟수 표 counts를 돌려준다.
chars: text에 나온 서로 다른 글자를 정렬한 목록(빈칸·줄바꿈도 글자다).
counts[i, j]: text에서 chars[i] 바로 다음에 chars[j]가 나온 횟수. 모양 (V, V), V = len(chars).
"""
chars = sorted(set(text))
index = {c: i for i, c in enumerate(chars)}
counts = np.zeros((len(chars), len(chars)), dtype=np.int64)
for a, b in zip(text, text[1:]):
counts[index[a], index[b]] += 1
return chars, counts
def next_probs(counts: np.ndarray, i: int) -> np.ndarray:
"""i번 글자 다음에 올 글자의 확률. counts의 i행을 그 행의 합으로 나눈다. 모양 (V,).
i번 글자 뒤에 아무 글자도 온 적이 없으면(행의 합이 0) 모든 글자에 같은 확률 1/V를 준다.
"""
row = counts[i].astype(np.float64)
total = row.sum()
if total == 0:
return np.full(len(row), 1.0 / len(row))
return row / total
def generate(chars: list[str], counts: np.ndarray, start: str, n: int, seed: int = 0) -> str:
"""start 글자에서 시작해 n글자를 더 이어 쓴 글(길이 n + 1)을 돌려준다.
난수기는 rng = np.random.default_rng(seed). 한 글자마다
지금 글자의 next_probs로 rng.choice(V, p=확률)를 해서 다음 글자를 뽑고, 뽑은 글자를 다음 차례의 지금 글자로 쓴다.
"""
rng = np.random.default_rng(seed)
i = chars.index(start)
out = [start]
for _ in range(n):
p = next_probs(counts, i)
i = int(rng.choice(len(chars), p=p))
out.append(chars[i])
return "".join(out)generate의 반복문이 1-2절의 생성 루프입니다. next_probs가 '모델', rng.choice가 '뽑기', out.append와 i = …가 '붙이고 다음 입력으로'입니다. 2교시부터는 이 고리는 그대로 두고 '모델' 자리를 점점 똑똑한 것으로 바꿔 갑니다.
추적 보고서
신고 내용: 같은 질문에 다른 책을 추천했다. 원인은 고장이 아니라 답을 만드는 방식에 있다.
언어 모델은 앞의 토큰들을 받아 다음 토큰의 확률 분포(막대)를 낸다. 같은 입력이면 막대는 언제나 같다.
챗봇은 그 막대에서 토큰 하나를 뽑아 붙이고, 그 토큰을 다시 입력으로 넣어 다음 막대를 계산한다(자기회귀 생성). 답은 미리 정해져 있지 않다.
참고 모델로 '오늘 저녁은'을 씨앗 100개로 이어 쓰니 첫 토큰부터 29갈래, 24토큰 답은 100개가 모두 달랐다. 늘 1등만 고르면 답은 같지만 같은 말을 되풀이한다.
조치: 영업시간·주소처럼 답이 하나뿐인 정보는 모델이 뽑게 두지 말고 서점 정보표에서 꺼내 보여 주기로 보라 매니저와 정리했다. 뽑기의 손잡이(온도 등)는 7교시에서 다시 본다.
내 LLM: mylm/ 시작. 말뭉치를 받고, 글자 바이그램 모델(세기 → 비율 → 이어 쓰기)을 완성했다(check_01 통과).
하은은 퇴근 전에 바이그램이 쓴 글을 한 번 더 돌려 봅니다. 그러다 낮에 본 참고 모델의 막대가 떠오릅니다. 막대 이름이 글자 하나가 아니라 '␣저녁', '을', '거리' 같은 조각이었습니다. 왜 글자도 낱말도 아닌 조각일까요? 그 답은 수요일 아침, 보라 매니저가 들고 온 청구서에서 시작됩니다. "물레책방 챗봇 기록을 보니까, 뜻이 같은 질문인데 영어보다 한국어로 물을 때 요금이 더 붙어요. 한국어로 물으면 요금이 더 나오는 거예요?" 2교시에서는 글을 조각으로 자르는 방법(토큰화)과 BPE를 직접 학습시키고, 같은 문장의 토큰 수를 한국어와 영어로 비교합니다.
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.