SONUB
KO English 한국어 日本語 中文 हिन्दी বাংলা اردو Español Português Français Deutsch Русский العربية Bahasa Indonesia Tiếng Việt ไทย Türkçe

알림을 켜는 방법

이 브라우저 또는 주소에서는 알림을 사용할 수 없습니다. 지원되는 브라우저로 보안 연결된 SONUB 사이트에 접속해 주세요.

알림이 차단되어 있으면 브라우저 설정에서 허용해 주세요.

  1. 주소 표시줄 옆의 사이트 설정 또는 브라우저의 사이트 설정을 열고, 이 사이트의 알림을 허용으로 바꾸세요.
  2. 기기 설정에서 브라우저 알림을 허용하고 방해 금지 또는 집중 모드를 확인하세요.
  3. SONUB으로 돌아와 페이지를 새로고침하세요. 알림이 켜지면 벨은 사라집니다.

iPhone 또는 iPad에서는 SONUB을 홈 화면에 추가하고 거기에서 연 뒤 알림을 허용하세요.

로그인 로그아웃 화면 테마 기본 테마 다크 테마 핑크 테마 채팅 1:1 채팅 그룹 채팅 게시판 공지사항 뉴스 게시판 건의 게시판 자기소개 게시판 메인 메뉴

자, 이제 여러분들만의 인공지능을 만들어 봅시다.

다음 의 점수를 실제 생성 문장으로 바꿉니다. generate.py 를 쓰고 ·· 를 하나씩 바꿔 결과를 비교해 보세요.

단어 블록이 한 줄로 놓여 있고 끝에 빈칸이 있다. 빈칸 위에 높이가 다른 막대를 단 후보 블록 세 개가 있고, 그중 하나가 골라져 줄 끝에 붙은 뒤 화살표가 다시 처음으로 돌아가 다음 칸을 고른다.
7

텍스트 생성

생성은 최근 을 에 넣고 예측한 하나를 붙이는 일을 합니다. 가 0이면 가장 높은 점수를 고릅니다. 양수면 점수를 조절한 뒤 무작위로 뽑으며, 높을수록 이 더 고르게 퍼집니다. 는 상위 후보만 남기고, 0이면 전부 씁니다.

는 무작위 선택을 하는 번호이며 정답을 보장하는 번호가 아닙니다. 이나 요청한 토큰 수에서 생성을 멈추고 새로 만든 글만 돌려줍니다. 입력은 때의 Q:/A: 형식을 맞춥니다.

생성 루프 — 한 토큰씩 붙여 나간다(자기회귀)

generate 는 이 순서를 정한 토큰 수만큼 되풀이합니다.

  • 지금까지의 토큰에서 마지막 context(12)개만 잘라 에 넣습니다.
  • 돌아온 점수 목록 가운데 마지막 자리의 점수 [-1] 만 씁니다. 그것이 다음 토큰의 점수입니다.
  • 정한 방법(탐욕·온도·top_k)으로 토큰 하나를 고릅니다.
  • 고른 토큰을 ids 뒤에 붙입니다. 다음 바퀴는 이 토큰까지 포함한 글을 봅니다.
  • 고른 토큰이 줄바꿈이면 멈춥니다.

자기가 낸 결과를 다시 입력으로 쓰므로 (autoregressive) 생성이라고 합니다. 한 번 고른 토큰은 되돌리지 않으므로 앞에서 실수하면 뒤가 그 실수를 따라갑니다.

생성은 를 읽기만 합니다. 그런데 의 forward 는 학습 때와 같은 Value 계산을 하므로 쓰지 않을 까지 만듭니다. 는 torch.no_grad() 안에서 생성해 그래프를 만들지 않고, model.eval() 로 을 끕니다.

탐욕 선택 — temperature 0

temperature 를 0으로 주면 언제나 점수가 가장 높은 토큰을 고릅니다(, ). 같은 입력이면 언제나 같은 답이 나옵니다.

장점은 예측할 수 있다는 것입니다. 이 무엇을 가장 그럴듯하게 여기는지 그대로 보여 주므로 학습이 됐는지 확인할 때 좋습니다. 단점은 같은 구절을 되풀이하기 쉽다는 것입니다. "is warn is warn is warn" 처럼 한 번 고리에 들어가면 매번 같은 토큰이 가장 높아 빠져나오지 못합니다. 그래서 대화에서는 무작위를 조금 섞습니다.

온도 — 확률 분포를 뾰족하게 또는 평평하게

temperature(T)는 점수를 하기 전에 T 로 나눕니다. 확률은 exp(점수 ÷ T) 에 비례합니다.

  • T < 1: 점수 차이가 커져 가 뾰족해집니다. 높은 토큰이 더 자주 뽑힙니다.
  • T = 1: 모델이 낸 확률 그대로입니다.
  • T > 1: 점수 차이가 줄어 분포가 평평해집니다. 낮은 토큰도 자주 뽑혀 엉뚱해집니다.

아래 실험은 점수 warm 2.0, hot 1.0, blue 0.2, run −1.0 에 온도를 바꿉니다. warm 의 확률은 T 0.5 에서 0.858, 1.0 에서 0.632, 2.0 에서 0.447 로 바뀌고, run 은 0.002 에서 0.1 로 오릅니다.

generate.py 도 같은 계산을 합니다. 후보 점수에서 가장 큰 값을 빼고(수치 안정) T 로 나눠 exp 한 값을 무게로 random.choices 에 넘깁니다. 무게는 합이 1이 아니어도 되고, choices 가 비율대로 뽑습니다. 보통 0.7~0.9 를 많이 쓰고, 0.1~0.5 는 보수적이며, 1.0 이상은 엉뚱해지기 쉽습니다.

직접 확인하기 lab/temperature.py
import math

logits = {'warm': 2.0, 'hot': 1.0, 'blue': 0.2, 'run': -1.0}
for t in [0.5, 1.0, 2.0]:
    weights = {w: math.exp(s / t) for w, s in logits.items()}
    total = sum(weights.values())
    print('T =', t, {w: round(v / total, 3) for w, v in weights.items()})
print('top_k=2 keeps', sorted(logits, key=logits.get, reverse=True)[:2])
cd ~/xslm
mkdir -p lab
micro lab/temperature.py
python3 -m lab.temperature

실행 결과

T = 0.5 {'warm': 0.858, 'hot': 0.116, 'blue': 0.023, 'run': 0.002}
T = 1.0 {'warm': 0.632, 'hot': 0.232, 'blue': 0.104, 'run': 0.031}
T = 2.0 {'warm': 0.447, 'hot': 0.271, 'blue': 0.182, 'run': 0.1}
top_k=2 keeps ['warm', 'hot']

top-k 와 top-p — 후보를 줄인다

온도를 써도 아주 낮은 토큰들이 합쳐서 꽤 큰 확률을 가질 수 있습니다. 가 5만 개면 하나하나는 작아도 모두 더하면 무시할 수 없습니다. 그래서 뽑기 전에 후보를 줄입니다.

  • top-k: 점수 상위 k 개만 남기고 나머지는 버립니다. XSLM 의 top_k 8 은 어휘 40개 중 8개만 후보로 둡니다. 0이면 줄이지 않습니다. 실험의 top_k=2 는 warm·hot 만 남깁니다.
  • top-p(nucleus): 확률이 높은 순서로 더해 합이 p(예: 0.9)가 될 때까지만 남깁니다. 모델이 확신할 때는 후보가 몇 개, 헷갈릴 때는 많아져 상황에 맞게 바뀝니다. XSLM 에는 없지만 실제 서비스에서 많이 씁니다.

어휘가 수만 개인 모델은 top_k 40~60 을 흔히 씁니다. (온도 0.7, top_k 30)은 정확하게, (0.9, 50)은 균형 있게, (1.1, 100)은 창의적으로 쓰는 조합입니다. 어휘가 40개인 XSLM 에서는 8 정도가 비슷한 비율입니다.

seed — 무작위 선택을 재현한다

temperature 가 0보다 크면 random.choices 가 토큰을 무작위로 뽑습니다. generate 는 부를 때마다 random.Random(seed) 로 새 생성기를 만들므로, ··온도·top_k·seed 면 언제나 같은 글이 나옵니다.

아래 실험은 무게 5:3:2 로 a·b·c 를 12번 뽑습니다. seed 7 은 두 번 모두 같은 줄이 나오고 seed 8 은 다른 줄이 나옵니다. 무게가 큰 a 가 가장 많이 나오지만 순서는 seed 가 정합니다.

seed 는 결과를 재현하는 번호이지 좋은 답을 보장하는 번호가 아닙니다. 설정을 비교할 때는 seed 를 고정하고 하나만 바꾸고, 결과가 운인지 보려면 seed 를 여러 개 바꿔 봅니다.

직접 확인하기 lab/seed.py
import random

for seed in [7, 7, 8]:
    rng = random.Random(seed)
    print(seed, ''.join(rng.choices('abc', weights=[5, 3, 2])[0] for _ in range(12)))
cd ~/xslm
mkdir -p lab
micro lab/seed.py
python3 -m lab.seed

실행 결과

7 aababaabaaaa
7 aababaabaaaa
8 acabaacabaaa

멈추는 조건 — 줄바꿈과 최대 토큰 수

생성은 두 경우에 멈춥니다.

  • 줄바꿈 토큰을 뽑았을 때: 에서 답 한 줄이 줄바꿈으로 끝났으므로, 모델이 "답이 끝났다"고 판단한 것입니다. 줄바꿈은 결과에 넣지 않습니다.
  • 요청한 토큰 수(기본 24, 최대 64)를 채웠을 때: 모델이 끝을 내지 못해도 끝없이 쓰지 않게 막습니다.

결과에는 프롬프트를 빼고 새로 만든 토큰만 해서 돌려줍니다. 첫 토큰부터 줄바꿈이 뽑히면 빈 답이 됩니다.

실제 모델은 특수 토큰이나 정한 멈춤 표시(예: [/ANSWER])에서 멈추고 그 뒤는 잘라 냅니다. 토큰을 만들 때마다 화면에 바로 보내는 도 흔합니다.

프롬프트 형식 — 학습한 모양과 공백까지 똑같이

모델은 학습 자료에서 본 모양 뒤에 오는 글만 잘 잇습니다. 그래서 생성할 때 넣는 글(프롬프트)은 학습 형식과 똑같아야 합니다. XSLM 의 기본 프롬프트는 "Q: hi" 다음 줄에 "A: " 입니다.

공백 하나도 중요합니다. 학습 자료의 "A: " 는 한 토큰(30번)인데, 끝 공백을 빼고 "A:" 로 주면 "A"(5번)와 ":"(4번) 두 토큰이 됩니다. 이 두 토큰이 이렇게 나온 적은 학습 자료에 한 번도 없어서, 모델은 처음 보는 상황에서 답을 시작합니다. 데스크톱에서 0.1 로 3,000번 학습한 모델에 sun 을 물어보니, "A: " 로 끝내면 "te sun is warn is warn…" 처럼 학습 문장과 비슷한 글을, "A:" 로 끝내면 "ore" 같은 조각만 냈습니다.

형식이 길면 답의 첫머리까지 넣어 줍니다. 예를 들어 [ANSWER] 다음에 "요약:" 과 "-" 까지 넣으면 모델이 바로 요약 목록을 이어 씁니다.

답이 이상할 때 — 원인과 실험

작은 모델의 답이 이상한 원인을 흔한 순서로 봅니다.

  • 학습 부족: 기본 20번은 확인용입니다. 이 3.7 근처면 모델은 아직 거의 아무것도 모릅니다.
  • 자료 부족: 12쌍으로는 형식을 흉내 내는 데 그치고, 처음 보는 질문에는 답하지 못합니다.
  • 형식 불일치: 프롬프트가 학습 형식과 다르거나 모르는 글자(�)가 섞였습니다.
  • 문맥 부족: context 12토큰을 넘는 앞부분은 보지 못합니다.
  • 설정: 반복되면 온도·top_k 를 조금 올리고, 엉뚱하면 내립니다.

무작위성을 높인다고 부족한 학습이 해결되지는 않습니다. 해 볼 만한 실험은 이렇습니다. 새 체크포인트로 학습률을 0.1 로 바꾸고 --steps 를 수백 번씩 나눠 학습하며, 같은 질문을 temperature 0 으로 물어 답이 어떻게 바뀌는지 기록합니다. 위에서 본 "te sun is warn…" 은 학습 문장 "the sun is warm." 을 흉내 내기 시작했지만 아직 외우지도 못한 단계입니다.

직접 타이핑하며 실습하기

generate.py를 작성하세요. 같은 체크포인트로 세 을 비교합니다. 먼저 temperature만 바꾸고, 다음에는 top_k와 seed를 바꾸어 보세요. 와 모델 크기는 체크포인트에 저장된 값을 읽습니다.

cd ~/xslm
micro generate.py
python3 generate.py --temperature 0 --top-k 1
python3 generate.py --temperature 0.8 --top-k 8 --seed 7
python3 generate.py --temperature 1.2 --top-k 0 --seed 8
터미널에서 실습하기
generate.py직접 작성할 전체 소스

load는 모델 구조를 다시 만들고 숫자를 복원합니다. generate는 문맥을 자르고 후보 점수를 계산해 반복해서 뽑습니다. 같은 seed로 비교 실험을 재현할 수 있습니다.

import argparse
import json
import math
import random
from pathlib import Path
from model import GPT
from tokenizer import BPE


def load():
    saved = json.loads(Path('checkpoints/model.json').read_text())
    cfg = saved['settings']
    tokenizer = BPE(**saved['tokenizer'])
    model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
    model.load(saved['weights'])
    return model, tokenizer, cfg


def generate(model, tokenizer, prompt, tokens=24, temperature=0.8, top_k=8, seed=7):
    if not prompt or tokens < 1 or temperature < 0 or not math.isfinite(temperature) or top_k < 0:
        raise ValueError('invalid sampling parameters')
    ids, result, rng = tokenizer.encode(prompt), [], random.Random(seed)
    for _ in range(tokens):
        logits = model.forward(ids[-model.context:])[-1]
        ranked = sorted(range(len(logits)), key=lambda i: logits[i].data, reverse=True)
        if temperature == 0:
            token = ranked[0]
        else:
            candidates = ranked[:top_k] if top_k else ranked
            peak = logits[candidates[0]].data
            weights = [math.exp((logits[i].data - peak) / temperature) for i in candidates]
            token = rng.choices(candidates, weights=weights)[0]
        ids.append(token)
        if tokenizer.vocab[token] == '\n':
            break
        result.append(token)
    return tokenizer.decode(result)


if __name__ == '__main__':
    model, tokenizer, cfg = load()
    parser = argparse.ArgumentParser()
    parser.add_argument('--prompt', default='Q: hi\nA: ')
    parser.add_argument('--tokens', type=int, default=cfg['tokens'])
    parser.add_argument('--temperature', type=float, default=0.8)
    parser.add_argument('--top-k', type=int, default=8)
    parser.add_argument('--seed', type=int, default=cfg['seed'])
    args = parser.parse_args()
    assert 1 <= args.tokens <= cfg['max_tokens']
    print(generate(model, tokenizer, args.prompt, args.tokens,
                   args.temperature, args.top_k, args.seed))

코드 한 줄씩 뜯어보기

generate.py load — 체크포인트로 모델 되살리기

def load():
    saved = json.loads(Path('checkpoints/model.json').read_text())
    cfg = saved['settings']
    tokenizer = BPE(**saved['tokenizer'])
    model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
    model.load(saved['weights'])
    return model, tokenizer, cfg

model.json 하나에서 설정과 토크나이저까지 읽으므로, settings.json 이나 tokenizer.json 이 나중에 바뀌어도 학습할 때와 같은 모델을 만듭니다. 같은 크기의 GPT 를 만든 뒤(처음 값은 곧 덮어씀) load 로 학습한 숫자를 넣습니다.

generate.py 입력 검사와 준비

def generate(model, tokenizer, prompt, tokens=24, temperature=0.8, top_k=8, seed=7):
    if not prompt or tokens < 1 or temperature < 0 or not math.isfinite(temperature) or top_k < 0:
        raise ValueError('invalid sampling parameters')
    ids, result, rng = tokenizer.encode(prompt), [], random.Random(seed)

빈 프롬프트, 1보다 작은 토큰 수, 음수·무한대· 온도, 음수 top_k 는 로 거절합니다. math.isfinite 는 inf·nan 이면 거짓입니다. 프롬프트를 번호로 바꾸고, 새로 만든 토큰을 모을 result 와 이번 생성만의 난수 생성기 rng 를 만듭니다.

generate.py 다음 토큰 점수 구하기

    for _ in range(tokens):
        logits = model.forward(ids[-model.context:])[-1]
        ranked = sorted(range(len(logits)), key=lambda i: logits[i].data, reverse=True)

ids[-model.context:] 로 마지막 12토큰만 넣고, forward 결과의 마지막 줄 [-1] 만 씁니다. ranked 는 어휘 번호를 점수가 높은 순서로 정렬한 목록이고, ranked[0] 이 가장 높은 토큰입니다.

generate.py 토큰 하나 고르기

        if temperature == 0:
            token = ranked[0]
        else:
            candidates = ranked[:top_k] if top_k else ranked
            peak = logits[candidates[0]].data
            weights = [math.exp((logits[i].data - peak) / temperature) for i in candidates]
            token = rng.choices(candidates, weights=weights)[0]

temperature 가 0이면 가장 높은 토큰을 고릅니다. 아니면 top_k 개(0이면 전부)를 후보로 남기고, 가장 큰 점수 peak 를 뺀 뒤 temperature 로 나눠 exp 한 값을 무게로 삼아 rng.choices 로 하나를 뽑습니다. choices 는 목록을 돌려주므로 [0] 으로 꺼냅니다.

generate.py 붙이고, 멈추고, 돌려주기

        ids.append(token)
        if tokenizer.vocab[token] == '\n':
            break
        result.append(token)
    return tokenizer.decode(result)

고른 토큰을 ids 에 붙여 다음 바퀴의 입력에 넣습니다. 그 토큰이 줄바꿈이면 결과에 넣지 않고 멈추고, 아니면 result 에 모읍니다. 반복이 끝나면 result 만 decode 해서 돌려주므로 프롬프트는 결과에 들어가지 않습니다.

generate.py 명령줄 실행

if __name__ == '__main__':
    model, tokenizer, cfg = load()
    parser = argparse.ArgumentParser()
    parser.add_argument('--prompt', default='Q: hi\nA: ')
    parser.add_argument('--tokens', type=int, default=cfg['tokens'])
    parser.add_argument('--temperature', type=float, default=0.8)
    parser.add_argument('--top-k', type=int, default=8)
    parser.add_argument('--seed', type=int, default=cfg['seed'])
    args = parser.parse_args()
    assert 1 <= args.tokens <= cfg['max_tokens']
    print(generate(model, tokenizer, args.prompt, args.tokens,
                   args.temperature, args.top_k, args.seed))

직접 실행하면 체크포인트를 읽고 --prompt·--tokens·--temperature·--top-k·--seed 옵션을 받습니다. argparse 는 --top-k 를 args.top_k 로 바꿔 줍니다(- 가 _ 로). 토큰 수가 1~max_tokens(64) 인지 로 확인한 뒤 생성한 글을 찍습니다. 에서 줄바꿈이 들어간 프롬프트를 주려면 --prompt "$( 'Q: cat\nA: ')" 처럼 printf 를 씁니다.

여기까지 이해했나요?

같은 체크포인트·질문·seed·설정이면 같은 글이 나와야 합니다. 가중치를 읽는 것과 학습하는 것의 차이를 설명해 보세요. 첫 선택이 줄바꿈이면 빈 답이 될 수 있습니다.

막혔을 때 확인하기

작은 모델을 잠깐 학습하면 이상한 글이나 빈 답이 나올 수 있습니다. 자료 형식·모르는 문자·학습 손실부터 확인하세요. 무작위성을 높인다고 부족한 학습이 해결되지는 않습니다.

자주 묻는 질문

같은 질문에 왜 매번 같은 답이 나오나요?

seed 가 같기 때문입니다. generate 는 요청마다 같은 seed 로 생성기를 새로 만듭니다. 다른 답을 보려면 --seed 를 바꾸거나 요청 에 "seed" 를 다른 정수로 넣으세요. temperature 가 0이면 seed 와 상관없이 언제나 같습니다.

온도를 높이면 더 똑똑해지나요?

아닙니다. 온도는 이미 계산된 확률에서 고르는 방법만 바꿉니다. 모델이 정답에 낮은 확률을 주고 있다면 온도를 어떻게 바꿔도 정답이 자주 나오지 않습니다. 똑똑함은 학습(자료·크기·횟수)이 정합니다.

빈 답이 나와요.

첫 토큰으로 줄바꿈이 뽑혀 바로 멈춘 경우입니다. 학습 자료에서 "A: " 다음에 바로 줄이 바뀌는 일은 없으므로, 학습이 부족하거나 프롬프트 형식이 다를 때 생깁니다. 프롬프트 끝이 "A: " 인지 보고, 학습을 더 하거나 seed·temperature 를 바꿔 보세요.

가이드라인

인공지능

에이전트 툴

바이브코딩

AI 활용

AI 자격증

화상 수업

IT 기초

채팅

게시판

만들기

소통

더보기

용어 사전

모르는 말을 골라 보세요. 쉬운 설명과 관련 용어를 볼 수 있어요.

찾는 용어가 아직 없어요.

용어를 고르면 설명이 보여요.