에 앞서 프로젝트 폴더, , 을 직접 만듭니다. ~/xslm 에 settings.json 과 prepare.py 를 쓰고 Q/A 글을 학습용과 검증용으로 나눠 보세요.
3
개발 구조
모든 작업은 ~/xslm에서 합니다. 에는 원본···가, checkpoints에는 된 가 들어갑니다. 는 프로젝트 바로 아래에 둡니다. 실행도 항상 이 폴더에서 합니다.
원본 한 줄은 Q: 질문 | A: 답변 형식입니다. prepare.py는 중복 줄을 없앤 뒤 서로 다른 다섯 줄 중 한 줄을 검증용으로 남깁니다. 구분자는 으로 바꿉니다. 검증 자료는 처음 보는 시험지이므로 토크나이저 학습에도 쓰지 않습니다. 사용할 권리가 있는 글을 쓰고, 개인정보 대신 직접 지어낸 예문을 사용하세요.
폴더 구조 — 파일마다 누가 언제 만드나
~/xslm 폴더는 다 만들면 이렇게 됩니다.
settings.json — 내가 씁니다. 모든 이 읽는 설정입니다.
data/raw.txt — 내가 씁니다. 원본 질문·답입니다.
data/train.txt·data/val.txt — prepare.py 가 만듭니다. 학습용과 검증용입니다.
data/tokenizer.json — tokenizer.py 가 만듭니다. 와 입니다.
checkpoints/model.json — train.py 가 만들고 학습할 때마다 고칩니다. 가중치와 학습 상태입니다.
runner.log·runner.pid — 를 로 켤 때 생깁니다. 출력 기록과 입니다.
나머지 .py 파일 — 내가 씁니다. 프로그램 소스입니다.
lab/ — 단계마다의 확인 실험입니다(선택).
소스와 자료와 결과를 나누어 두면 무엇을 지워도 되는지 분명합니다. data/·checkpoints/ 에 만들어진 파일은 언제든 다시 만들 수 있지만, 내가 쓴 소스와 raw.txt 는 다시 만들 수 없으니 내보내기로 보관하세요.
실행 순서와 다시 만들 것 — 무엇을 바꾸면 무엇을 다시 하나
파일이 앞에서 뒤로 이어지므로, 앞쪽을 바꾸면 뒤쪽을 모두 다시 만듭니다.
raw.txt 를 바꿨다면: prepare.py → tokenizer.py → 체크포인트를 옮기고 train.py.
settings.json 을 조금이라도 바꿨다면(merges·width··steps· 무엇이든): 바꾼 것이 merges 면 tokenizer.py 부터, 아니면 바로 체크포인트를 옮기고 train.py.
학습만 더 하고 싶다면: settings.json 은 그대로 두고 train.py --steps 50 처럼 옵션으로 횟수를 정합니다. 체크포인트에서 이어 학습합니다.
학습을 더 했다면: 실행 중인 Runner 를 껐다 켜야 새 가중치를 읽습니다.
train.py 는 이 규칙을 직접 확인합니다. 체크포인트에 저장한 설정 전체·토크나이저·자료 지문이 지금 것과 하나라도 다르면 로 멈춥니다. 바뀐 자료나 설정으로 옛 가중치를 이어 학습하면 의 뜻이나 모양이 어긋나 조용히 망가지기 때문입니다. 새로 시작하려면 checkpoints/model.json checkpoints/old.json 처럼 옛 파일을 옮깁니다.
데이터 형식이 능력을 만든다 — Q: … | A: …
raw.txt 한 줄은 "Q: 질문 | A: 답" 입니다. prepare.py 는 " | A:" 앞에서 줄을 바꾸므로 학습 자료에서는 두 줄이 됩니다.
Q: hi
A: hi friend.
은 이 모양을 되풀이해 보며 "Q: 줄 다음 줄은 A: 로 시작하고, A: 뒤에는 답 같은 글이 온 다음 줄이 바뀐다"를 배웁니다. 생성할 때 Q: 질문 다음 줄에 "A: " 까지 넣어 주면 모델은 배운 습관대로 답을 이어 씁니다.
형식은 자유롭게 정해도 됩니다. [QUESTION]…[/QUESTION], [ANSWER]…[/ANSWER] 처럼 표시를 달아도 됩니다. 지켜야 할 것은 일관성 하나입니다.
모든 줄이 같은 표시와 같은 순서를 씁니다.
같은 뜻은 같은 낱말로 씁니다. 예를 들어 전화·Tel·☎ 를 모두 TEL: 로 통일합니다.
형식을 바꾸면 그 형식을 쓰는 코드도 바꿉니다. 은 generate.py 의 기본 와 xchat.py 의 "Q: … A: " 가 이 형식에 묶여 있습니다.
입력에서 출력으로 가는 모양이 자료에 많아야 그 일을 배웁니다. 글만 늘어놓으면 이어 쓰기를, 질문→답이면 답하기를, 문서→요약이면 요약을 배웁니다.
좋은 자료와 나쁜 자료 — 양·품질·권리
모델은 본 것만 흉내 냅니다. 일본어만 듣고 자란 사람이 한국어를 못하는 것과 같습니다. 그래서 자료가 곧 모델입니다.
인코딩: 반드시 로 저장합니다. 다른 인코딩이면 글자가 깨집니다.
좋은 자료: 형식이 같은 질문·답, 정보가 담긴 자연스러운 문장.
나쁜 자료: 코드·로그만 있는 글, 이모지·주소가 지나친 글, 깨진 문장, 같은 문장의 반복.
정리: 앞뒤 공백과 빈 줄을 없애고 같은 줄은 한 번만 남깁니다. prepare.py 가 이 일을 합니다.
다양성: 같은 답에 질문을 여러 가지로 써 봅니다(hi·hello·hey). 그래야 처음 보는 질문에도 답할 가능성이 생깁니다.
권리와 개인정보: 내가 쓸 권리가 있는 글만 쓰고, 실제 이름·전화번호 대신 지어낸 예문을 씁니다.
양은 생각보다 많이 필요합니다. 어림잡아 질문·답 3,000쌍쯤에서 형식을 따라 하기 시작하고, 1만 쌍쯤이면 형식이 안정됩니다. XSLM 의 15줄은 전체 과정이 실제로 도는지 확인하는 아주 작은 연습 자료입니다. 처음에는 이렇게 작은 가짜 자료로 끝까지 돌려 본 뒤 자료를 늘립니다.
학습 자료와 검증 자료 — 처음 보는 시험지를 남겨 둔다
학습 자료로만 점수를 매기면 외운 것과 배운 것을 구별할 수 없습니다. 그래서 일부를 떼어 학습에 쓰지 않는 검증 자료(validation)로 둡니다. 은 내려가는데 이 올라가면 외우기()를 시작했다는 신호입니다.
prepare.py 는 이렇게 나눕니다.
중복을 지운 줄에 0부터 번호를 매깁니다.
번호를 5로 나눈 나머지가 0인 줄(0·5·10번째)을 검증용, 나머지를 학습용으로 둡니다. 기본 자료 15줄은 학습 12줄·검증 3줄이 됩니다.
으로 바꾸기 전에 질문·답 한 쌍을 통째로 나눕니다. 글자 단위로 자르면 한 쌍의 앞부분은 학습에, 뒷부분은 검증에 들어가 시험 문제가 새어 나갑니다().
토크나이저도 학습 자료만 보고 배웁니다. 검증 자료는 마지막 시험까지 아무도 보지 않습니다.
자료가 아주 크면 전체 토큰의 끝 1%를 잘라 검증용으로 쓰기도 합니다. 자료가 작으면 XSLM 처럼 질문·답 쌍 단위로 나누는 편이 안전합니다.
큰 자료는 미리 토큰 번호로 바꿔 둔다
XSLM 은 train.py 를 실행할 때마다 train.txt 전체를 토큰 번호로 바꿉니다. 자료가 몇백 글자라 순식간입니다. 자료가 수십 가 되면 매번 바꾸는 것은 낭비라서, 한 번만 바꿔 파일로 저장해 둡니다.
토큰 번호를 uint16(2 정수)으로 저장합니다. 어휘가 65,536개보다 적으면 2바이트에 들어갑니다.
글이 아니라 숫자 배열이라 train.bin·val.bin 이라는 이진 파일이 됩니다.
학습할 때는 numpy.memmap 으로 파일을 에 다 올리지 않고 필요한 부분만 읽습니다.
자료가 더 커지면 여러 파일로 나눠 차례로 학습합니다. 흐름은 같습니다. 글 → 토큰 번호 → 고정 길이 구간 → 학습.
내 자료로 바꾸기 — 한국어 질문·답으로 해 보기
과정을 한 번 끝냈다면 raw.txt 를 내 자료로 바꿔 봅니다.
한 줄에 "Q: 질문 | A: 답" 하나씩, 서로 다른 줄을 다섯 줄 이상 씁니다. prepare.py 의 assert 가 형식과 줄 수를 검사합니다.
답은 짧게 씁니다. 가 12토큰이라 모델은 마지막 12토큰만 봅니다. 글자 기반 에서 한글은 처음에 한 글자가 한 토큰이므로, 질문과 답이 길면 앞부분을 보지 못합니다.
학습 자료에 없는 글자는 토크나이저가 모르는 글자(0번)로 바꿉니다. 질문에 쓸 글자는 자료에도 넣어 두세요.
한국어처럼 글자 종류가 많은 언어는 어휘가 커지고 같은 쌍이 덜 되풀이되어 병합이 일찍 멈추기 쉽습니다. 이럴 때는 줄을 늘리고 merges 를 늘려 봅니다.
직접 타이핑하며 실습하기
먼저 폴더를 만드세요. 아래 소스를 하나씩 펼쳐 같은 파일명으로 작성하고 저장한 뒤 닫습니다. .py 파일은 로, settings.json과 raw.txt는 로 씁니다. settings.json은 이라 쉼표와 따옴표를 정확히 씁니다. 데이터와 소스를 모두 저장한 뒤 prepare.py를 실행하세요.
한 줄에 질문·답변 한 쌍을 씁니다. 첫 실험을 짧게 하려고 만든 영어 예문입니다. 이후에는 같은 형식으로 내 자료를 넣어 보세요.
Q: hello | A: hello friend.
Q: hi | A: hi friend.
Q: name | A: i am xslm.
Q: sun | A: the sun is warm.
Q: cat | A: a cat can nap.
Q: dog | A: a dog can run.
Q: red | A: red is a color.
Q: blue | A: blue is a color.
Q: learn | A: i learn from text.
Q: code | A: i can learn code.
Q: moon | A: the moon is in the sky.
Q: rain | A: rain is water.
Q: book | A: i can read a book.
Q: tea | A: tea is warm.
Q: bye | A: bye friend.
prepare.py직접 작성할 전체 소스
UTF-8 읽기 → 중복 제거 → 문장쌍 나누기 → 학습·검증 파일 저장입니다. 토큰으로 나누기 전에 문장쌍을 분리해야 똑같은 예제가 양쪽에 들어가지 않습니다.
from pathlib import Path
lines = [line.strip() for line in Path('data/raw.txt').read_text(encoding='utf-8').splitlines() if line.strip()]
lines = list(dict.fromkeys(lines))
assert len(lines) >= 5, 'write at least five different Q/A lines'
assert all(' | A:' in line and line.startswith('Q:') for line in lines)
train = [line for i, line in enumerate(lines) if i % 5 != 0]
val = [line for i, line in enumerate(lines) if i % 5 == 0]
for name, rows in [('train', train), ('val', val)]:
text = '\n'.join(row.replace(' | A:', '\nA:') for row in rows) + '\n'
Path('data/' + name + '.txt').write_text(text, encoding='utf-8')
print(name, len(rows))
코드 한 줄씩 뜯어보기
직접 입력한 소스를 조각으로 나누어 읽습니다. 왼쪽 숫자는 파일의 실제 줄 번호입니다. 의 line 숫자와 맞춰 보며 읽으세요.
prepare.py 원본 읽기와 중복 지우기
from pathlib import Pathlines = [line.strip() for line in Path('data/raw.txt').read_text(encoding='utf-8').splitlines() if line.strip()]lines = list(dict.fromkeys(lines))
Path(…).read_text(encoding='utf-8') 로 파일 전체를 글자로 읽고 splitlines() 로 줄마다 나눕니다. 내포 [line.strip() for line in … if line.strip()] 는 앞뒤 공백을 지우고 빈 줄은 버립니다. dict.fromkeys(lines) 는 줄을 사전의 넣어 중복을 없애는데, Python 사전은 넣은 순서를 지키므로 처음 나온 순서가 그대로 남습니다. set 을 쓰면 순서가 실행마다 달라져 학습·검증 나누기가 바뀔 수 있습니다.
prepare.py 형식 검사
assert len(lines) >= 5, 'write at least five different Q/A lines'assert all(' | A:' in line and line.startswith('Q:') for line in lines)
두 assert 가 자료를 검사합니다. 서로 다른 줄이 다섯 줄 이상이어야 검증용으로 한 줄 이상을 떼어 낼 수 있습니다. all(…) 은 모든 줄이 Q: 로 시작하고 " | A:" 를 품고 있을 때만 참입니다. 한 줄이라도 틀리면 학습 형식이 섞이므로 여기서 멈추는 편이 낫습니다. 두 번째 assert 에는 메시지가 없어 AssertionError 만 보입니다. 그럴 때는 raw.txt 의 형식을 살펴보세요.
prepare.py 학습·검증 나누기
train = [line for i, line in enumerate(lines) if i % 5 != 0]val = [line for i, line in enumerate(lines) if i % 5 == 0]
enumerate(lines) 는 (번호, 줄)을 차례로 줍니다. i % 5 는 번호를 5로 나눈 나머지라서 0·5·10…번째 줄이 val, 나머지가 train 이 됩니다. 15줄이면 12줄과 3줄입니다. 무작위로 고르지 않고 규칙으로 나누므로 몇 번을 실행해도 같은 나누기가 나옵니다.
prepare.py 두 줄 형식으로 저장
for name, rows in [('train', train), ('val', val)]: text = '\n'.join(row.replace(' | A:', '\nA:') for row in rows) + '\n' Path('data/' + name + '.txt').write_text(text, encoding='utf-8') print(name, len(rows))
train·val 각각에 대해, 줄마다 " | A:" 를 줄바꿈 + "A:" 로 바꿔 질문과 답을 두 줄로 만들고, 쌍들을 줄바꿈으로 이은 뒤 끝에 줄바꿈 하나를 더해 저장합니다. 마지막 답도 줄바꿈으로 끝나야 모델이 "답 다음에는 줄바꿈"을 모든 답에서 배웁니다. 는 몇 쌍을 썼는지 보여 줍니다.
여기까지 이해했나요?
와 를 입력하세요. settings.json과 prepare.py가 보이나요? 제공한 자료는 학습 12개, 검증 3개 문장쌍으로 나뉩니다. 내 문장쌍을 하나 추가하면 어떤 파일부터 다시 만들어야 할까요?
막혔을 때 확인하기
가 나면 폴더와 파일명을 확인하세요. ~/xslm 다음 ls data를 실행합니다. 는 문제입니다. 공백 네 칸을 맞추세요. 완성 예제는 ~/xslm-reference에 비교용으로만 있으며 내 프로젝트에 자동으로 채우지 않습니다.
자주 묻는 질문
15줄로 정말 학습이 되나요?
과정이 도는지 확인하는 데는 충분하고, 쓸 만한 대화에는 턱없이 부족합니다. 학습 자료가 12쌍뿐이라 모델은 금방 그대로 외우려 하고, 검증 손실은 오히려 올라갑니다(6단계 "과적합"). 줄을 늘릴수록 외우기보다 규칙을 배울 여지가 커집니다.
왜 검증 자료를 토크나이저 학습에서도 빼나요?
토크나이저도 학습입니다. 검증 문장에만 나오는 글자 조합이 병합 규칙에 들어가면, 검증 문장이 학습 문장보다 짧고 깔끔한 토큰으로 바뀌어 시험이 쉬워집니다. 실제 서비스에서 처음 보는 글을 받는 상황을 흉내 내려면 검증 자료는 모든 학습 단계에서 빼야 합니다.
assert 오류 "write at least five different Q/A lines" 가 나요.
prepare.py 는 서로 다른 줄이 다섯 줄 이상이어야 학습·검증으로 나눌 수 있습니다. 빈 줄과 중복 줄은 세지 않습니다. 형식 assert 가 걸리면 모든 줄이 Q: 로 시작하고 " | A:" 가 들어 있는지 보세요. 띄어쓰기까지 같아야 합니다.