LangChain 챗봇1장 · 과정 소개와 LLM 의 이해15 / 21← 이전목차다음 →MetaContents

1장. 과정 소개와 LLM 의 이해

실습 2 — 다음 낱말 맞히기 모델 만들기

짧은 여행 문장 60개(travel_sentences.txt)에서 낱말마다 바로 뒤에 온 낱말을 세고, 확률을 구하고, 확률이 가장 큰 낱말을 이어 붙여 문장을 만듭니다. LLM 이 하는 "다음 낱말 맞히기"를 가장 작게 줄인 장난감 모델입니다. 문장은 연습용으로 지어낸 것이고, 센 결과는 실습 4에서 쓰도록 out_next_word.json 으로 저장합니다.

1단계. 문장 읽고 낱말로 나누기

Python
from pathlib import Path

# 한 줄에 한 문장씩 적힌 파일을 읽어 줄 목록으로 만든다. splitlines = 줄 단위로 나누기
lines = Path("travel_sentences.txt").read_text(encoding="utf-8").splitlines()

print("문장 수:", len(lines))
print("앞의 세 문장:", lines[:3])
# split() 은 띄어쓰기를 기준으로 문장을 낱말 목록으로 자른다
print("첫 문장의 낱말:", lines[0].split())
출력
문장 수: 60
앞의 세 문장: ['제주도 여행은 즐겁습니다', '강릉 여행을 예약했습니다', '제주도 여행은 설렙니다']
첫 문장의 낱말: ['제주도', '여행은', '즐겁습니다']

코드가 의미하는 것 splitlines() 와 split() 은 학습 자료를 모델이 셀 수 있는 낱말 단위로 자르는 일입니다. 실제 LLM 은 띄어쓰기 대신 토크나이저로 자르지만, "글을 조각으로 나눈 뒤 센다"는 첫걸음은 같습니다.

2단계. 바로 뒤에 온 낱말 세기

Python
from collections import Counter, defaultdict

# defaultdict(Counter): 처음 보는 낱말이 나오면 빈 횟수표(Counter)를 저절로 만들어 주는 사전
# next_words["제주도"] 는 "제주도" 바로 뒤에 온 낱말들의 횟수표가 된다
next_words = defaultdict(Counter)

for line in lines:
    words = line.split()
    # zip(words, words[1:]) 은 (앞 낱말, 뒤 낱말) 짝을 차례로 만든다
    for front, back in zip(words, words[1:]):
        next_words[front][back] += 1

# most_common(): 많이 나온 순서로 (낱말, 횟수) 를 늘어놓는다
print("제주도 →", next_words["제주도"].most_common())
print("여행은 →", next_words["여행은"].most_common())
출력
제주도 → [('여행은', 9), ('맛집을', 5), ('여행을', 4), ('날씨가', 3)]
여행은 → [('즐겁습니다', 14), ('설렙니다', 8), ('처음입니다', 2)]

코드가 의미하는 것 next_words 는 "이 낱말 다음에 무엇이 몇 번 왔나"를 모은 표로, 이 장난감 모델의 전부입니다. 실제 LLM 이 많은 글을 읽으며 익히는 것도 결국 "이런 글 다음에는 무엇이 오기 쉬운가"이고, 이 표는 그것을 앞 낱말 하나로 줄인 것입니다. 표에 없는 짝은 모델이 모르는 짝입니다.

3단계. 횟수를 확률로 바꾸기

Python
counts = next_words["제주도"]
# 전체 횟수 = "제주도" 뒤에 낱말이 온 횟수를 모두 더한 것
total = sum(counts.values())
print("제주도 뒤에 낱말이 온 횟수:", total)

for word, n in counts.most_common():
    # 확률 = 그 낱말이 온 횟수 ÷ 전체 횟수
    print(f"{word}  {n}번  확률 {n / total:.3f}")
출력
제주도 뒤에 낱말이 온 횟수: 21
여행은  9번  확률 0.429
맛집을  5번  확률 0.238
여행을  4번  확률 0.190
날씨가  3번  확률 0.143

코드가 의미하는 것 n / total 은 앞 절의 "횟수 ÷ 전체"를 실제 자료에 그대로 쓴 것입니다. 확률이 0.429 라는 것은 "제주도" 뒤에 "여행은"을 고를 가능성이 열에 네 번 남짓이라는 뜻이고, 모델이 다음 낱말을 고를 때 쓰는 근거가 이 숫자입니다.

4단계. 1등 낱말로 문장 이어 쓰고 저장하기

Python
import json


def continue_text(word, max_words=5):
    """확률이 가장 큰 낱말을 골라 붙이기를 되풀이한다."""
    out = [word]
    for _ in range(max_words):
        # get 은 없는 낱말이면 None 을 돌려준다. 뒤에 온 적이 없는 낱말(문장 끝)이면 멈춘다
        after = next_words.get(out[-1])
        if not after:
            break
        # most_common(1)[0][0]: 1등 (낱말, 횟수) 짝에서 낱말만 꺼낸다
        out.append(after.most_common(1)[0][0])
    return " ".join(out)


for start in ["제주도", "부산", "강릉", "경주"]:
    print(continue_text(start))

# 실습 4에서 쓰도록 횟수표를 json 파일로 저장한다. Counter 는 사전이라 그대로 저장된다
Path("out_next_word.json").write_text(json.dumps(next_words, ensure_ascii=False, indent=1), encoding="utf-8")
print("저장: out_next_word.json")
출력
제주도 여행은 즐겁습니다
부산 여행은 즐겁습니다
강릉 바다는 시원합니다
경주 여행은 즐겁습니다
저장: out_next_word.json

코드가 의미하는 것 continue_text 는 "다음 낱말 고르기 → 붙이기 → 다시 고르기"를 되풀이하는 함수로, LLM 이 답을 한 조각씩 써 내려가는 방식을 흉내 냅니다. 늘 1등(most_common(1))만 고르므로 같은 시작에는 같은 문장이 나옵니다. 횟수표를 out_next_word.json 으로 저장하는 것은 배운 결과(모델)를 파일로 남겨 다음 실습에서 다시 쓰려는 것입니다.

이 실습이 뜻하는 것

이 실습은 휴대폰의 다음 낱말 추천을 손으로 만들어 보는 일과 같습니다. 문장 60개를 낱말로 자르고(1단계), 낱말마다 바로 뒤에 무엇이 왔는지 세고(2단계), 센 횟수를 비율로 바꾸고(3단계), 늘 1등 추천만 눌러 문장을 이어 씁니다(4단계). 여기서 문장 60개가 학습 자료, next_words 가 모델, continue_text 가 추천 낱말을 누르는 손가락입니다. 비유와 다른 점은, 실제 LLM 은 바로 앞 낱말 하나가 아니라 앞의 글 전체를 보고 맞힌다는 것입니다.

단계출력뜻
1. 낱말로 나누기문장 60개, 첫 문장의 낱말 3개"제주도 여행은 즐겁습니다"가 ['제주도', '여행은', '즐겁습니다'] 로 잘렸습니다
2. 뒤에 온 낱말 세기제주도 → 여행은 9, 맛집을 5, 여행을 4, 날씨가 3"제주도" 뒤에는 네 낱말만 왔습니다. "바다는"은 한 번도 오지 않았습니다
3. 확률0.429, 0.238, 0.190, 0.14321번 가운데 9번이 "여행은"이라 9 ÷ 21 = 0.429 입니다. 넷을 더하면 1 입니다
4. 이어 쓰기제주도·부산·경주 여행은 즐겁습니다, 강릉 바다는 시원합니다1등만 고르니 시작 낱말이 같으면 언제나 같은 문장이 나옵니다

한 줄로 줄이면, "다음 낱말의 확률 = 횟수 ÷ 전체"만으로 문장을 이어 쓰는 모델을 만든 실습입니다. 4단계의 문장이 늘 같다는 점을 눈여겨보세요. 1등만 고르는 것은 온도를 0 에 가깝게 둔 것과 같고, 실습 4에서 온도를 올려 다른 낱말도 고르게 해 봅니다. 문장이 짧고 틀이 정해진 연습용 자료라 결과가 단순하게 나왔습니다.

코드 설명

  • defaultdict(Counter) 는 처음 보는 낱말을 꺼내도 오류 없이 빈 횟수표를 만들어 줍니다. 그래서 next_words[front][back] += 1 한 줄로 세기가 끝납니다.
  • zip(words, words[1:]) 은 목록을 한 칸 밀어 짝을 짓습니다. 낱말 세 개짜리 문장에서는 (제주도, 여행은), (여행은, 즐겁습니다) 두 짝이 나옵니다. 문장 끝 낱말은 뒤에 짝이 없어서 횟수표의 열쇠로 들어가지 않습니다.
  • continue_text 는 next_words.get(…) 이 비어 있으면 멈춥니다. "즐겁습니다"처럼 문장 끝에만 온 낱말이 나오면 거기서 문장이 끝납니다.
  • 강릉만 "바다는"으로 이어진 것은 강릉 뒤에 "바다는"이 6번으로 가장 많이 왔기 때문입니다. 모델은 자료에 많이 나온 것을 그대로 따라 합니다.
← 이전 절1장 · 실습 1 — 규칙 챗봇 만들고 한계 확인하기다음 절 →1장 · 실습 3 — 토큰 세고 요금 어림하기