Text Generation
Transformers
Safetensors
GGUF
Korean
gemma4
image-text-to-text
gemma
korean
roleplay
mud
lore
llama.cpp
lmstudio
conversational
Instructions to use sangwon1472/gemma4-e2b-mud with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use sangwon1472/gemma4-e2b-mud with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="sangwon1472/gemma4-e2b-mud") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoProcessor, AutoModelForImageTextToText processor = AutoProcessor.from_pretrained("sangwon1472/gemma4-e2b-mud") model = AutoModelForImageTextToText.from_pretrained("sangwon1472/gemma4-e2b-mud") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - llama-cpp-python
How to use sangwon1472/gemma4-e2b-mud with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="sangwon1472/gemma4-e2b-mud", filename="gemma4-e2b-mud-Q4_K_M.gguf", )
llm.create_chat_completion( messages = [ { "role": "user", "content": "What is the capital of France?" } ] ) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use sangwon1472/gemma4-e2b-mud with llama.cpp:
Install from brew
brew install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama-server -hf sangwon1472/gemma4-e2b-mud:UD-Q4_K_M # Run inference directly in the terminal: llama-cli -hf sangwon1472/gemma4-e2b-mud:UD-Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama-server -hf sangwon1472/gemma4-e2b-mud:UD-Q4_K_M # Run inference directly in the terminal: llama-cli -hf sangwon1472/gemma4-e2b-mud:UD-Q4_K_M
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf sangwon1472/gemma4-e2b-mud:UD-Q4_K_M # Run inference directly in the terminal: ./llama-cli -hf sangwon1472/gemma4-e2b-mud:UD-Q4_K_M
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf sangwon1472/gemma4-e2b-mud:UD-Q4_K_M # Run inference directly in the terminal: ./build/bin/llama-cli -hf sangwon1472/gemma4-e2b-mud:UD-Q4_K_M
Use Docker
docker model run hf.co/sangwon1472/gemma4-e2b-mud:UD-Q4_K_M
- LM Studio
- Jan
- vLLM
How to use sangwon1472/gemma4-e2b-mud with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "sangwon1472/gemma4-e2b-mud" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "sangwon1472/gemma4-e2b-mud", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/sangwon1472/gemma4-e2b-mud:UD-Q4_K_M
- SGLang
How to use sangwon1472/gemma4-e2b-mud with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "sangwon1472/gemma4-e2b-mud" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "sangwon1472/gemma4-e2b-mud", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "sangwon1472/gemma4-e2b-mud" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "sangwon1472/gemma4-e2b-mud", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Ollama
How to use sangwon1472/gemma4-e2b-mud with Ollama:
ollama run hf.co/sangwon1472/gemma4-e2b-mud:UD-Q4_K_M
- Unsloth Studio
How to use sangwon1472/gemma4-e2b-mud with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for sangwon1472/gemma4-e2b-mud to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for sangwon1472/gemma4-e2b-mud to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for sangwon1472/gemma4-e2b-mud to start chatting
- Docker Model Runner
How to use sangwon1472/gemma4-e2b-mud with Docker Model Runner:
docker model run hf.co/sangwon1472/gemma4-e2b-mud:UD-Q4_K_M
- Lemonade
How to use sangwon1472/gemma4-e2b-mud with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull sangwon1472/gemma4-e2b-mud:UD-Q4_K_M
Run and chat with the model
lemonade run user.gemma4-e2b-mud-UD-Q4_K_M
List all available models
lemonade list
Commit ·
d9fceea
1
Parent(s): 59fd928
Add gemma-mud-colab-starter package
Browse files- .gitignore +2 -0
- README.md +5 -1
- gemma-mud-colab-starter/README.md +225 -0
- gemma-mud-colab-starter/dataset/combined_1000.jsonl +0 -0
- gemma-mud-colab-starter/dataset/combined_1000.unsloth_chatml_dedup.jsonl +0 -0
- gemma-mud-colab-starter/dataset/combined_1000.unsloth_gemma4_messages_dedup.jsonl +0 -0
- gemma-mud-colab-starter/dataset/gemma4_mud_alpaca_100.jsonl +100 -0
- gemma-mud-colab-starter/examples/expected_outputs.md +57 -0
- gemma-mud-colab-starter/examples/lmstudio_system_prompt_ko.txt +9 -0
- gemma-mud-colab-starter/examples/lmstudio_system_prompt_lore_ko.txt +10 -0
- gemma-mud-colab-starter/examples/lmstudio_system_prompt_npc_ko.txt +10 -0
- gemma-mud-colab-starter/examples/lmstudio_test_prompts.md +97 -0
- gemma-mud-colab-starter/examples/sample_prompts.md +16 -0
- gemma-mud-colab-starter/notebooks/Gemma4_MUD_QLoRA_Colab_Notebook.ipynb +693 -0
- gemma-mud-colab-starter/notebooks/Gemma4_MUD_QLoRA_Colab_Notebook_Cells.md +513 -0
- gemma-mud-colab-starter/output/.gitkeep +0 -0
- gemma-mud-colab-starter/package_manifest.md +30 -0
- gemma-mud-colab-starter/requirements_colab.txt +17 -0
- gemma-mud-colab-starter/run_instructions.md +58 -0
- gemma-mud-colab-starter/scripts/prepare_unsloth_gemma4_dataset.py +139 -0
- gemma-mud-colab-starter/troubleshooting.md +184 -0
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
download.log
|
| 2 |
+
.DS_Store
|
README.md
CHANGED
|
@@ -11,4 +11,8 @@ Gemma 4 E2B 기반 MUD 스타일 파인튜닝 모델
|
|
| 11 |
- 서사 중심 출력
|
| 12 |
|
| 13 |
## 사용법
|
| 14 |
-
Transformers 또는 GGUF (LM Studio) 사용 가능
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
- 서사 중심 출력
|
| 12 |
|
| 13 |
## 사용법
|
| 14 |
+
Transformers 또는 GGUF (LM Studio) 사용 가능
|
| 15 |
+
|
| 16 |
+
## 포함 자료
|
| 17 |
+
- `gemma-mud-colab-starter/`
|
| 18 |
+
Colab 학습 노트북, 데이터셋 예시, LM Studio 프롬프트, 실행 가이드를 함께 넣어둔 스타터 패키지
|
gemma-mud-colab-starter/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# gemma-mud-colab-starter
|
| 2 |
+
|
| 3 |
+
작고 조용한 불씨 하나를 코랩 위에 올려 두는 패키지다.
|
| 4 |
+
이 묶음은 **MUD 문체·NPC 대화·방 분위기**를 위한 **Gemma 4 + Unsloth LoRA 첫 실험용** 최소 패키지다.
|
| 5 |
+
|
| 6 |
+
이 패키지는 완성형 대규모 학습 시스템이 아니다.
|
| 7 |
+
**100개 샘플 데이터**로 흐름을 익히고, 노트북이 실제로 한 번 돌아가게 만드는 데 목적이 있다.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. 무엇이 들어 있나
|
| 12 |
+
|
| 13 |
+
- `README.md`
|
| 14 |
+
전체 흐름 안내
|
| 15 |
+
- `requirements_colab.txt`
|
| 16 |
+
코랩 설치 패키지 목록
|
| 17 |
+
- `run_instructions.md`
|
| 18 |
+
정말 쉬운 실행 순서
|
| 19 |
+
- `troubleshooting.md`
|
| 20 |
+
자주 막히는 문제와 해결
|
| 21 |
+
- `dataset/gemma4_mud_alpaca_100.jsonl`
|
| 22 |
+
학습 데이터셋
|
| 23 |
+
- `notebooks/Gemma4_MUD_QLoRA_Colab_Notebook.ipynb`
|
| 24 |
+
코랩 실행 노트북
|
| 25 |
+
- `examples/sample_prompts.md`
|
| 26 |
+
테스트용 명령 예시
|
| 27 |
+
- `examples/lmstudio_system_prompt_ko.txt`
|
| 28 |
+
LM Studio 붙여 넣기용 시스템 프롬프트
|
| 29 |
+
- `examples/lmstudio_system_prompt_npc_ko.txt`
|
| 30 |
+
LM Studio 짧은 NPC 대화용 프롬프트
|
| 31 |
+
- `examples/lmstudio_system_prompt_lore_ko.txt`
|
| 32 |
+
LM Studio 세계관 설명용 프롬프트
|
| 33 |
+
- `examples/lmstudio_test_prompts.md`
|
| 34 |
+
LM Studio 확인용 테스트 문장
|
| 35 |
+
- `examples/expected_outputs.md`
|
| 36 |
+
정상 출력의 결을 읽는 기준
|
| 37 |
+
- `output/.gitkeep`
|
| 38 |
+
결과 폴더 자리 보존용
|
| 39 |
+
- `package_manifest.md`
|
| 40 |
+
파일 역할 요약
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## 2. 이 패키지의 목적
|
| 45 |
+
|
| 46 |
+
이 패키지는 아래 셋을 빠르게 시험하는 데 맞춰져 있다.
|
| 47 |
+
|
| 48 |
+
1. **MUD 명령어에 대한 문체 학습**
|
| 49 |
+
2. **NPC 대화 반응 미세조정**
|
| 50 |
+
3. **로컬 추론 서버나 MUD 엔진에 붙일 수 있는 LoRA 어댑터 생성**
|
| 51 |
+
|
| 52 |
+
즉,
|
| 53 |
+
게임 판정 전체를 AI에게 맡기는 것이 아니라
|
| 54 |
+
**말투, 분위기, 짧은 서사, 힌트성 응답**을 다듬는 방향에 맞다.
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
## 3. 준비물
|
| 59 |
+
|
| 60 |
+
- Google 계정
|
| 61 |
+
- Google Colab
|
| 62 |
+
- 선택: Hugging Face 계정
|
| 63 |
+
- 선택: Hugging Face 토큰
|
| 64 |
+
- 이 패키지 zip 파일
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## 4. Hugging Face 로그인은 언제 필요한가
|
| 69 |
+
|
| 70 |
+
이번 노트북 기본 경로는 `unsloth/gemma-4-E2B-it` 를 사용한다.
|
| 71 |
+
로컬 저장만 할 때는 Hugging Face 로그인이 없어도 시작할 수 있다.
|
| 72 |
+
|
| 73 |
+
아래 경우에는 로그인해 두는 편이 좋다.
|
| 74 |
+
|
| 75 |
+
1. Hugging Face Hub로 LoRA 어댑터를 올리고 싶을 때
|
| 76 |
+
2. 다른 gated Gemma 체크포인트를 직접 불러오고 싶을 때
|
| 77 |
+
3. 모델/결과를 Hub에서 관리하고 싶을 때
|
| 78 |
+
|
| 79 |
+
중요:
|
| 80 |
+
- 비밀번호를 문서에 적지 않는다.
|
| 81 |
+
- 토큰을 코드에 하드코딩하지 않는다.
|
| 82 |
+
- 토큰이 필요할 때만 노트북의 로그인 셀 주석을 풀어 직접 입력한다.
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
## 5. 가장 쉬운 실행 순서
|
| 87 |
+
|
| 88 |
+
### 방법 A — 가장 쉬운 길
|
| 89 |
+
1. zip을 푼다.
|
| 90 |
+
2. `notebooks/Gemma4_MUD_QLoRA_Colab_Notebook.ipynb` 를 Colab에서 연다.
|
| 91 |
+
3. 런타임을 **GPU**로 바꾼다.
|
| 92 |
+
4. `dataset/gemma4_mud_alpaca_100.jsonl` 파일을 Colab 파일 패널에 업로드한다.
|
| 93 |
+
5. 셀을 위에서부터 차례대로 실행한다.
|
| 94 |
+
6. 설치 셀 뒤에 import 오류가 났다면 **런타임 재시작** 후 2번 셀부터 다시 실행한다.
|
| 95 |
+
7. 필요할 때만 Hugging Face 로그인 셀의 주석을 풀어 실행한다.
|
| 96 |
+
8. 학습이 끝나면 `/content/output/gemma_mud_lora_out` 폴더를 확인한다.
|
| 97 |
+
|
| 98 |
+
### 방법 B — 드라이브에 두고 쓰기
|
| 99 |
+
1. zip을 구글 드라이브에 올린다.
|
| 100 |
+
2. 드라이브에서 압축을 푼다.
|
| 101 |
+
3. 노트북에서 경로 설정 셀만 바꿔 드라이브 경로를 잡는다.
|
| 102 |
+
4. 나머지는 같은 순서로 실행한다.
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
## 6. 데이터셋 위치와 형식
|
| 107 |
+
|
| 108 |
+
기본 데이터셋은 여기에 있다.
|
| 109 |
+
|
| 110 |
+
- `dataset/gemma4_mud_alpaca_100.jsonl`
|
| 111 |
+
|
| 112 |
+
형식은 다음 필드를 가진 Alpaca 계열 JSONL이다.
|
| 113 |
+
|
| 114 |
+
- `instruction`
|
| 115 |
+
- `input`
|
| 116 |
+
- `output`
|
| 117 |
+
- `text`
|
| 118 |
+
|
| 119 |
+
노트북은 이 파일을 먼저 읽고, `instruction/input/output` 를 Gemma 4용 `user/assistant` 대화로 다시 감싼 뒤, 최종 `text` 컬럼을 만들어 Unsloth `SFTTrainer` 에 넘긴다.
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
## 7. 노트북 기본 설정
|
| 124 |
+
|
| 125 |
+
노트북은 처음 성공 확률을 높이기 위해 **Unsloth의 Gemma 4 E2B instruct 체크포인트**를 기본값으로 둔다.
|
| 126 |
+
왜냐하면 무료 Colab T4에서도 비교적 현실적으로 한 번 돌아가게 만들기 좋기 때문이다.
|
| 127 |
+
|
| 128 |
+
기본값 예시:
|
| 129 |
+
- `per_device_train_batch_size = 1`
|
| 130 |
+
- `gradient_accumulation_steps = 4`
|
| 131 |
+
- `num_train_epochs = 1`
|
| 132 |
+
- `learning_rate = 2e-4`
|
| 133 |
+
- `MAX_SEQ_LENGTH = 1024`
|
| 134 |
+
- `LOAD_IN_4BIT = False`
|
| 135 |
+
|
| 136 |
+
VRAM이 부족하면 설정 셀에서 `LOAD_IN_4BIT = True` 또는 `MAX_SEQ_LENGTH = 512` 로 낮춘다.
|
| 137 |
+
핵심은 Gemma 4를 그냥 Alpaca 문자열로 넣지 않고, **Gemma 4 채팅 템플릿으로 다시 감싼 뒤** Unsloth LoRA로 학습하는 점이다.
|
| 138 |
+
|
| 139 |
+
---
|
| 140 |
+
|
| 141 |
+
## 8. 학습 후 결과는 어디에 저장되나
|
| 142 |
+
|
| 143 |
+
기본 저장 위치:
|
| 144 |
+
|
| 145 |
+
- `/content/output/gemma_mud_lora_out`
|
| 146 |
+
|
| 147 |
+
여기에는 보통 아래가 생긴다.
|
| 148 |
+
|
| 149 |
+
- LoRA adapter 가중치
|
| 150 |
+
- 토크���이저 파일
|
| 151 |
+
- 설정 파일 일부
|
| 152 |
+
- 체크포인트 폴더
|
| 153 |
+
|
| 154 |
+
이 폴더를 Google Drive로 복사하거나, Colab에서 직접 내려받으면 된다.
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
## 9. 로컬 MUD 실험에 붙이는 큰 흐름
|
| 159 |
+
|
| 160 |
+
학습이 끝나면 보통 이렇게 이어진다.
|
| 161 |
+
|
| 162 |
+
1. LoRA 어댑터 저장
|
| 163 |
+
2. 로컬 또는 서버 추론 환경에 로드
|
| 164 |
+
3. MUD 명령 입력을 프롬프트로 변환
|
| 165 |
+
4. `talk oracle`, `look`, `rumor` 같은 응답 생성
|
| 166 |
+
5. 게임 엔진은 여전히 판정을 맡고, AI는 말과 분위기를 맡음
|
| 167 |
+
|
| 168 |
+
핵심은 이 선을 지키는 것이다.
|
| 169 |
+
|
| 170 |
+
- **엔진**: 이동, 전투 판정, 보상, 퀘스트 상태
|
| 171 |
+
- **AI**: 대화, 분위기, 전승, 짧은 힌트
|
| 172 |
+
|
| 173 |
+
---
|
| 174 |
+
|
| 175 |
+
## 10. 한계와 주의사항
|
| 176 |
+
|
| 177 |
+
이 패키지는 첫 실험용이다.
|
| 178 |
+
|
| 179 |
+
- 데이터가 100개뿐이라 품질이 흔들릴 수 있다.
|
| 180 |
+
- 문체는 배워도, 세계 규칙 전체를 자동으로 이해하지는 않는다.
|
| 181 |
+
- 큰 모델은 무료 Colab에서 실패할 수 있다.
|
| 182 |
+
- 긴 시퀀스 길이는 메모리를 빠르게 잡아먹는다.
|
| 183 |
+
- 세션이 끊기면 저장 전까지 작업이 사라질 수 있다.
|
| 184 |
+
|
| 185 |
+
또한 이런 문장은 AI가 임의로 내면 안 된다.
|
| 186 |
+
|
| 187 |
+
- “너는 퀘스트를 완료했다”
|
| 188 |
+
- “너는 500 골드를 얻었다”
|
| 189 |
+
- “문이 열렸다”
|
| 190 |
+
|
| 191 |
+
이것은 **게임 엔진 판정 영역**이다.
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
|
| 195 |
+
## 11. 권장 테스트 명령
|
| 196 |
+
|
| 197 |
+
학습 직후에는 아래 다섯 개를 먼저 넣어 보는 편이 좋다.
|
| 198 |
+
|
| 199 |
+
- `talk oracle`
|
| 200 |
+
- `ask oracle about first fire`
|
| 201 |
+
- `look`
|
| 202 |
+
- `rumor`
|
| 203 |
+
- `scan signal`
|
| 204 |
+
|
| 205 |
+
좋은 출력은 짧고, 세계관 안에 머물고, 과장된 메타 발언이 적다.
|
| 206 |
+
|
| 207 |
+
LM Studio 에서 테스트할 때는 아래 파일도 같이 보면 편하다.
|
| 208 |
+
|
| 209 |
+
- `examples/lmstudio_system_prompt_ko.txt`
|
| 210 |
+
- `examples/lmstudio_system_prompt_npc_ko.txt`
|
| 211 |
+
- `examples/lmstudio_system_prompt_lore_ko.txt`
|
| 212 |
+
- `examples/lmstudio_test_prompts.md`
|
| 213 |
+
|
| 214 |
+
---
|
| 215 |
+
|
| 216 |
+
## 12. 다음 확장 단계
|
| 217 |
+
|
| 218 |
+
1. **100개 → 300개 데이터 확장**
|
| 219 |
+
2. **chat/messages 형식 데이터셋 실험**
|
| 220 |
+
3. **NPC별 전용 데이터셋 분리**
|
| 221 |
+
4. **로컬 추론 서버 연동**
|
| 222 |
+
5. **MUD 명령어별 응답 품질 비교**
|
| 223 |
+
|
| 224 |
+
처음에는 한 명의 NPC만 살아나도 충분하다.
|
| 225 |
+
그 뒤에 세계를 넓히면 된다.
|
gemma-mud-colab-starter/dataset/combined_1000.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
gemma-mud-colab-starter/dataset/combined_1000.unsloth_chatml_dedup.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
gemma-mud-colab-starter/dataset/combined_1000.unsloth_gemma4_messages_dedup.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
gemma-mud-colab-starter/dataset/gemma4_mud_alpaca_100.jsonl
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"instruction": "look", "input": "", "output": "당신은 첫 불의 지평의 관측 갑판에 서 있다. 검은 심우주 위로 옅은 금빛 항로가 숨처럼 이어지고, 멀리 오래된 표지등 하나가 파도 없는 바다처럼 떨린다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlook\n\n### Response:\n당신은 첫 불의 지평의 관측 갑판에 서 있다. 검은 심우주 위로 옅은 금빛 항로가 숨처럼 이어지고, 멀리 오래된 표지등 하나가 파도 없는 바다처럼 떨린다."}
|
| 2 |
+
{"instruction": "look", "input": "", "output": "당신은 소금 예언소의 젖은 회랑에 서 있다. 벽면의 결정염은 희미한 푸른빛을 품고 있고, 발밑의 금속판은 오래된 기도문처럼 낮게 울린다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlook\n\n### Response:\n당신은 소금 예언소의 젖은 회랑에 서 있다. 벽면의 결정염은 희미한 푸른빛을 품고 있고, 발밑의 금속판은 오래된 기도문처럼 낮게 울린다."}
|
| 3 |
+
{"instruction": "look", "input": "", "output": "당신은 펠라직 개구의 바람 없는 전망대에 서 있다. 아래로는 별빛 대신 심연의 유광이 흐르고, 위로는 물결 같은 성운이 천천히 접혀 들어간다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlook\n\n### Response:\n당신은 펠라직 개구의 바람 없는 전망대에 서 있다. 아래로는 별빛 대신 심연의 유광이 흐르고, 위로는 물결 같은 성운이 천천히 접혀 들어간다."}
|
| 4 |
+
{"instruction": "look", "input": "", "output": "당신은 난파 블룸의 찢긴 선체 틈에 서 있다. 부서진 난간 사이로 냉기가 실오라기처럼 새고, 표류 잔해들은 마치 죽지 못한 꽃처럼 서로 얽혀 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlook\n\n### Response:\n당신은 난파 블룸의 찢긴 선체 틈에 서 있다. 부서진 난간 사이로 냉기가 실오라기처럼 새고, 표류 잔해들은 마치 죽지 못한 꽃처럼 서로 얽혀 있다."}
|
| 5 |
+
{"instruction": "look", "input": "", "output": "당신은 등롱 여울의 상층 보행교 위에 서 있다. 푸른 안내등이 먼 물가의 불빛처럼 흔들리고, 유리 가루가 밟힐 때마다 아주 작은 별비처럼 번뜩인다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlook\n\n### Response:\n당신은 등롱 여울의 상층 보행교 위에 서 있다. 푸른 안내등이 먼 물가의 불빛처럼 흔들리고, 유리 가루가 밟힐 때마다 아주 작은 별비처럼 번뜩인다."}
|
| 6 |
+
{"instruction": "look", "input": "", "output": "당신은 헬리오스 도크의 외곽 정박 구역에 서 있다. 금빛 도장선은 오래전 영광의 껍질처럼 선체 위에 남아 있고, 멀리 정비용 암이 졸린 짐승처럼 천천히 움직인다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlook\n\n### Response:\n당신은 헬리오스 도크의 외곽 정박 구역에 서 있다. 금빛 도장선은 오래전 영광의 껍질처럼 선체 위에 남아 있고, 멀리 정비용 암이 졸린 짐승처럼 천천히 움직인다."}
|
| 7 |
+
{"instruction": "look", "input": "", "output": "당신은 나이트글래스 중계소의 어두운 수신실에 서 있다. 검은 유리 패널 너머로는 무수한 잡음이 별무늬처럼 번지고, 오래된 신호가 끊어질 듯 이어진다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlook\n\n### Response:\n당신은 나이트글래스 중계소의 어두운 수신실에 서 있다. 검은 유리 패널 너머로는 무수한 잡음이 별무늬처럼 번지고, 오래된 신호가 끊어질 듯 이어진다."}
|
| 8 |
+
{"instruction": "look", "input": "", "output": "당신은 잿불 채석장의 절개 능선에 서 있다. 검게 식은 광맥이 칼자국처럼 드러나 있고, 방열판 아래에는 아직도 식지 않은 열의 기억이 숨 쉬고 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlook\n\n### Response:\n당신은 잿불 채석장의 절개 능선에 서 있다. 검게 식은 광맥이 칼자국처럼 드러나 있고, 방열판 아래에는 아직도 식지 않은 열의 기억이 숨 쉬고 있다."}
|
| 9 |
+
{"instruction": "look", "input": "", "output": "당신은 조립 각성지의 무인 작업대 앞에 서 있다. 공구암은 주인을 잃은 채 허공을 더듬고, 바닥의 윤활 흔��은 지워지지 않은 노동의 서명처럼 남아 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlook\n\n### Response:\n당신은 조립 각성지의 무인 작업대 앞에 서 있다. 공구암은 주인을 잃은 채 허공을 더듬고, 바닥의 윤활 흔적은 지워지지 않은 노동의 서명처럼 남아 있다."}
|
| 10 |
+
{"instruction": "look", "input": "", "output": "당신은 레비아탄 요람의 굽은 선회로에 서 있다. 거대한 유골 같은 구조물이 머리 위를 감싸고, 먼 곳에서는 보이지 않는 조수가 철판 밑을 지나가는 듯하다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlook\n\n### Response:\n당신은 레비아탄 요람의 굽은 선회로에 서 있다. 거대한 유골 같은 구조물이 머리 위를 감싸고, 먼 곳에서는 보이지 않는 조수가 철판 밑을 지나가는 듯하다."}
|
| 11 |
+
{"instruction": "talk oracle", "input": "", "output": "예언자는 한동안 눈을 감은 채 침묵한다. 이윽고 그는 낮게 말한다. 길을 묻는 자는 많지만, 자기 이름의 무게를 끝까지 견디는 자는 드물다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ntalk oracle\n\n### Response:\n예언자는 한동안 눈을 감은 채 침묵한다. 이윽고 그는 낮게 말한다. 길을 묻는 자는 많지만, 자기 이름의 무게를 끝까지 견디는 자는 드물다."}
|
| 12 |
+
{"instruction": "talk oracle", "input": "", "output": "예언자는 소금기 어린 숨을 길게 내쉰다. 그리고 말한다. 항로는 별 사이에 있는 것이 아니라, 두려움과 결심 사이에 먼저 열린다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ntalk oracle\n\n### Response:\n예언자는 소금기 어린 숨을 길게 내쉰다. 그리고 말한다. 항로는 별 사이에 있는 것이 아니라, 두려움과 결심 사이에 먼저 열린다."}
|
| 13 |
+
{"instruction": "talk oracle", "input": "", "output": "예언자는 너를 바라보되, 마치 너의 뒤에 있는 오래된 그림자를 함께 보는 듯하다. 그의 목소리는 잔잔하다. 아직 돌아갈 수 있을 때 떠나는 자보다, 끝을 알고도 가는 자가 더 깊은 불을 지닌다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ntalk oracle\n\n### Response:\n예언자는 너를 바라보되, 마치 너의 뒤에 있는 오래된 그림자를 함께 보는 듯하다. 그의 목소리는 잔잔하다. 아직 돌아갈 수 있을 때 떠나는 자보다, 끝을 알고도 가는 자가 더 깊은 불을 지닌다."}
|
| 14 |
+
{"instruction": "talk oracle", "input": "", "output": "예언자는 곧장 대답하지 않는다. 회랑을 스치는 금속 울림이 한 번 지나간 뒤에야 입을 연다. 심연은 사람을 삼키지 않는다. 먼저 스스로를 버린 자가 스스로 가라앉을 뿐이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ntalk oracle\n\n### Response:\n예언자는 곧장 대답하지 않는다. 회랑을 스치는 금속 울림이 한 번 지나간 뒤에야 입을 연다. 심연은 사람을 삼키지 않는다. 먼저 스스로를 버린 자가 스스로 가라앉을 뿐이다."}
|
| 15 |
+
{"instruction": "talk oracle", "input": "", "output": "예언자는 벽의 결정염을 손끝으로 짚으며 말한다. 네가 듣고 싶은 답은 짧겠지만, 네가 견뎌야 할 길은 길다. 그러니 성급한 위안보다 오래 가는 결심을 택하라.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ntalk oracle\n\n### Response:\n예언자는 벽의 결정염을 손끝으로 짚으며 말한다. 네가 듣고 싶은 답은 짧겠지만, 네가 견뎌야 할 길은 길다. 그러니 성급한 위안보다 오래 가는 결심을 택하라."}
|
| 16 |
+
{"instruction": "talk captain", "input": "", "output": "함장은 접힌 항해도를 가볍게 두드리며 말한다. 우주선은 강한 금속으로만 움직이지 않는다. 끝까지 복귀 좌표를 기억하는 마음이 마지막 엔진이 되지.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ntalk captain\n\n### Response:\n함장은 접힌 항해도를 가볍게 두드리며 말한다. 우주선은 강한 금속으로만 움직이지 않는다. 끝까지 복귀 좌표를 기억하는 마음이 마지막 엔진이 되지."}
|
| 17 |
+
{"instruction": "talk captain", "input": "", "output": "함장은 짧게 웃더니 시선을 창밖으로 던진다. 처음 출항할 때의 떨림을 잊지 마라. 숙련은 필요하지만, 출발의 불씨를 잃으면 항로는 곧 장례 행렬이 된다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ntalk captain\n\n### Response:\n함장은 짧게 웃더니 시선을 창밖으로 던진다. 처음 출항할 때의 떨림을 잊지 마라. 숙련은 필요하지만, 출발의 불씨를 잃으면 항로는 곧 장례 행렬이 된다."}
|
| 18 |
+
{"instruction": "talk captain", "input": "", "output": "함장은 네 장비를 한번 훑어본 뒤 고개를 끄덕인다. 아직은 충분하다. 다만 장비보다 더 자주 점검해야 할 것은 판단이다. 틀린 방향의 용기는 오래가지 못한다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ntalk captain\n\n### Response:\n함장은 네 장비를 한번 훑어본 뒤 고개를 끄덕인다. 아직은 충분하다. 다만 장비보다 더 자주 점검해야 할 것은 판단이다. 틀린 방향의 용기는 오래가지 못한다."}
|
| 19 |
+
{"instruction": "talk quartermaster", "input": "", "output": "보급관은 장부를 덮으며 말한다. 물자는 숫자로 세지만, 부족은 늘 예기치 않은 곳에서 시작된다. 떠나기 전에는 한 번 더 확인하는 편이 좋다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ntalk quartermaster\n\n### Response:\n보급관은 장부를 덮으며 말한다. 물자는 숫자로 세지만, 부족은 늘 예기치 않은 곳에서 시작된다. 떠나기 전에는 한 번 더 확인하는 편이 좋다."}
|
| 20 |
+
{"instruction": "talk mechanic", "input": "", "output": "정비사는 장갑 낀 손으로 선체 프레임을 두드린다. 배는 거짓말을 못 한다. 삐걱거림 하나, 떨림 하나가 다 사전에 남기는 경고문이지.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ntalk mechanic\n\n### Response:\n정비사는 장갑 낀 손으로 선체 프레임을 두드린다. 배는 거짓말을 못 한다. 삐걱거림 하나, 떨림 하나가 다 사전에 남기는 경고문이지."}
|
| 21 |
+
{"instruction": "ask oracle about first fire", "input": "", "output": "예언자는 아주 오래된 이름을 부르듯 천천히 말한다. 첫 불은 별의 불이 아니었다. 돌아서지 않겠다고 맹세한 한 사람의 가슴에서 먼저 켜진 불씨였다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nask oracle about first fire\n\n### Response:\n예언자는 아주 오래된 이름을 부르듯 천천히 말한다. 첫 불은 별의 불이 아니었다. 돌아서지 않겠다고 맹세한 한 사람의 가슴에서 먼저 켜진 불씨였다."}
|
| 22 |
+
{"instruction": "ask oracle about pelagic aperture", "input": "", "output": "예언자는 먼 조류를 듣는 사람처럼 고개를 기울인다. 펠라직 개구는 문이면서 바다다. 그것은 길을 열기도 하지만, 마음이 갈라진 자를 그대로 깊이 반사해 돌려보내기도 한다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nask oracle about pelagic aperture\n\n### Response:\n예언자는 먼 조류를 듣는 사람처럼 고개를 기울인다. 펠라직 개구는 문이면서 바다다. 그것은 길을 열기도 하지만, 마음이 갈라진 자를 그대로 깊이 반사해 돌려보내기도 한다."}
|
| 23 |
+
{"instruction": "ask oracle about salt current", "input": "", "output": "예언자는 손바닥에 소금 결정을 굴리며 답한다. 소금 해류는 물길이 아니라 기억의 흐름이다. 버려진 맹세, 돌아오지 못한 항해, 이름 잃은 배들이 그 결을 따라 떠돈다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nask oracle about salt current\n\n### Response:\n예언자는 손바닥에 소금 결정을 굴리며 답한다. 소금 해류는 물길이 아니라 기억의 흐름이다. 버려진 맹세, 돌아오지 못한 항해, 이름 잃은 배들이 그 결을 따라 떠돈다."}
|
| 24 |
+
{"instruction": "ask oracle about leviathan cradle", "input": "", "output": "예언자는 목소리를 낮춘다. 레비아탄의 요람은 단순한 거대 구조물이 아니다. 그것은 한 시대의 공포가 남긴 뼈대이며, 아직도 어떤 자들에게는 어머니보다 오래된 품이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nask oracle about leviathan cradle\n\n### Response:\n예언자는 목소리를 낮춘다. 레비아탄의 요람은 단순한 거대 구조물이 아니다. 그것은 한 시대의 공포가 남긴 뼈대이며, 아직도 어떤 자들���게는 어머니보다 오래된 품이다."}
|
| 25 |
+
{"instruction": "ask oracle about nameless drifter", "input": "", "output": "예언자는 잠시 너를 살핀다. 이름 없는 표류자는 한 사람이 아니다. 때로는 선박이고, 때로는 망명이고, 때로는 스스로를 지우고도 끝내 살아남은 의지 그 자체다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nask oracle about nameless drifter\n\n### Response:\n예언자는 잠시 너를 살핀다. 이름 없는 표류자는 한 사람이 아니다. 때로는 선박이고, 때로는 망명이고, 때로는 스스로를 지우고도 끝내 살아남은 의지 그 자체다."}
|
| 26 |
+
{"instruction": "ask captain about route", "input": "", "output": "함장은 지도 위를 손가락으로 훑으며 답한다. 짧은 길이 항상 쉬운 길은 아니다. 적어도 지금 항로에서는, 오래 도는 쪽이 더 많은 사람을 살릴 수도 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nask captain about route\n\n### Response:\n함장은 지도 위를 손가락으로 훑으며 답한다. 짧은 길이 항상 쉬운 길은 아니다. 적어도 지금 항로에서는, 오래 도는 쪽이 더 많은 사람을 살릴 수도 있다."}
|
| 27 |
+
{"instruction": "ask captain about helios verge", "input": "", "output": "함장은 빛바랜 표식을 가리킨다. 헬리오스 버지는 한때 개척의 문장이었다. 지금은 금빛보다 피로가 더 많이 남아 있지만, 아직 첫 출항의 냄새를 기억하는 곳이지.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nask captain about helios verge\n\n### Response:\n함장은 빛바랜 표식을 가리킨다. 헬리오스 버지는 한때 개척의 문장이었다. 지금은 금빛보다 피로가 더 많이 남아 있지만, 아직 첫 출항의 냄새를 기억하는 곳이지."}
|
| 28 |
+
{"instruction": "ask mechanic about engine", "input": "", "output": "정비사는 낮은 웃음을 흘린다. 엔진이란 결국 참는 법을 배우는 기계다. 무리하게 몰아붙이면 빠르게 타오르지만, 돌아오는 길엔 그 대가를 반드시 요구한다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nask mechanic about engine\n\n### Response:\n정비사는 낮은 웃음을 흘린다. 엔진이란 결국 참는 법을 배우는 기계다. 무리하게 몰아붙이면 빠르게 타오르지만, 돌아오는 길엔 그 대가를 반드시 요구한다."}
|
| 29 |
+
{"instruction": "ask quartermaster about supplies", "input": "", "output": "보급관은 익숙한 표정으로 답한다. 연료와 식량은 누구나 챙긴다. 그러나 약품, 봉인 천, 수신 예비 부품은 늘 뒤늦게 떠오른다. 모자람은 대개 그런 데서 시작된다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nask quartermaster about supplies\n\n### Response:\n보급관은 익숙한 표정으로 답한다. 연료와 식량은 누구나 챙긴다. 그러나 약품, 봉인 천, 수신 예비 부품은 늘 뒤늦게 떠오른다. 모자람은 대개 그런 데서 시작된다."}
|
| 30 |
+
{"instruction": "ask archivist about relic", "input": "", "output": "기록관은 먼지를 털어내며 말한다. 유물은 과거의 물건이 아니라, 과거가 아직 끝나지 않았다는 증거다. 함부로 만지기보다 먼저 어떤 침묵을 품었는지 읽어라.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nask archivist about relic\n\n### Response:\n기록관은 먼지를 털어내며 말한다. 유물은 과거의 물건이 아니라, 과거가 아직 끝나지 않았다는 증거다. 함부로 만지기보다 먼저 어떤 침묵을 품었는지 읽어라."}
|
| 31 |
+
{"instruction": "scan signal", "input": "", "output": "신호층을 더듬자 오래된 구조 호출 일부가 되살아난다. 파형은 불안정하지만, 북동쪽 항로 어딘가에서 반복적으로 같은 좌표를 밀어 올리고 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nscan signal\n\n### Response:\n신호층을 더듬자 오래된 구조 호출 일부가 되살아난다. 파형은 불안정하지만, 북동쪽 항로 어딘가에서 반복적으로 같은 좌표를 밀어 올리고 있다."}
|
| 32 |
+
{"instruction": "scan signal", "input": "", "output": "스캔 결과 약한 위상 떨림이 포착된다. 자연 잡음으로 보기에는 규칙이 너무 또렷하고, 인공 신호라고 보기에는 지나치게 오래 외로웠다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nscan signal\n\n### Response:\n스캔 결과 약한 위상 떨림이 포착된다. 자연 잡음으로 보기에는 규칙이 너무 또렷하고, 인공 신호라고 보기에는 지나치게 오래 외로웠다."}
|
| 33 |
+
{"instruction": "scan signal", "input": "", "output": "수신기 너머로 끊어진 합창 같은 파형이 지나간다. 의미를 즉시 해독할 수는 없지만, 누군가 한때 여기서 길을 잃고도 끝까지 응답을 포기하지 않았던 흔적은 분명하다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nscan signal\n\n### Response:\n수신기 너머로 끊어진 합창 같은 파형이 지나간다. 의미를 즉시 해독할 수는 없지만, 누군가 한때 여기서 길을 잃고도 끝까지 응답을 포기하지 않았던 흔적은 분명하다."}
|
| 34 |
+
{"instruction": "scan relic", "input": "", "output": "유물 표면에서 미세한 발광선이 드러난다. 겉보기엔 금속이지만 내부 결은 생물성 조직처럼 층을 이루고 있어, 제작 방식이 알려진 공학과 완전히 맞물리지는 않는다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nscan relic\n\n### Response:\n유물 표면에서 미세한 발광선이 드러난다. 겉보기엔 금속이지만 내부 결은 생물성 조직처럼 층을 이루고 있어, 제작 방식이 알려진 공학과 완전히 맞물리지는 않는다."}
|
| 35 |
+
{"instruction": "scan anomaly", "input": "", "output": "주변 공간에 얇은 굴절 띠가 나타난다. 작은 균열처럼 보이지만, 실제로는 방향감각을 늦게 되돌려 주는 일종의 기억 지연 현상에 가깝다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nscan anomaly\n\n### Response:\n주변 공간에 얇은 굴절 띠가 나타난다. 작은 균열처럼 보이지만, 실제로는 방향감각을 늦게 되돌려 주는 일종의 기억 지연 현상에 가깝다."}
|
| 36 |
+
{"instruction": "inspect hull", "input": "", "output": "선체 외피에는 오래된 미세 균열이 별자리처럼 퍼져 있다. 당장 위험하진 않지만, 다음 장거리 항해 전에는 보강 처리가 권장된다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ninspect hull\n\n### Response:\n선체 외피에는 오래된 미세 균열이 별자리처럼 퍼져 있다. 당장 위험하진 않지만, 다음 장거리 항해 전에는 보강 처리가 권장된다."}
|
| 37 |
+
{"instruction": "inspect altar", "input": "", "output": "제단 표면은 수많은 손이 스쳐 간 흔적으로 매끈하게 닳아 있다. 믿음의 색은 지워졌어도, 간절함의 압력만은 아직 금속 안에 남아 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ninspect altar\n\n### Response:\n제단 표면은 수많은 손이 스쳐 간 흔적으로 매끈하게 닳아 있다. 믿음의 색은 지워졌어도, 간절함의 압력만은 아직 금속 안에 남아 있다."}
|
| 38 |
+
{"instruction": "inspect door", "input": "", "output": "문은 닫혀 있으나 죽어 있지는 않다. 내부 잠금 장치는 오래전에 봉인된 듯하지만, 틈 사이로 흐르는 미약한 진동은 누군가 아직 반대편의 시간을 지키고 있음을 암시한다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ninspect door\n\n### Response:\n문은 닫혀 있으나 죽어 있지는 않다. 내부 잠금 장치는 오래전에 봉인된 듯하지만, 틈 사이로 흐르는 미약한 진동은 누군가 아직 반대편의 시간을 지키고 있음을 암시한다."}
|
| 39 |
+
{"instruction": "inspect shrine", "input": "", "output": "작은 성소에는 이름 없는 표식만 남아 있다. 해석할 수 없는 문양이지만, 반복되는 배열 속에서 항해와 애도의 질서가 함께 읽힌다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ninspect shrine\n\n### Response:\n작은 성소에는 이름 없는 표식만 남아 있다. 해석할 수 없는 문양이지만, 반복되는 배열 속에서 항해와 애도의 질서가 함께 읽힌다."}
|
| 40 |
+
{"instruction": "inspect map", "input": "", "output": "지도 가장자리에 손수 고친 흔적이 여러 겹 남아 있다. 공식 항로보다 현장 기록이 더 많이 덧씌워져 있어, 이것이 살아남은 자들의 지도였음을 알 수 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ninspect map\n\n### Response:\n지도 가장자리에 손수 고친 흔적이 여러 겹 ���아 있다. 공식 항로보다 현장 기록이 더 많이 덧씌워져 있어, 이것이 살아남은 자들의 지도였음을 알 수 있다."}
|
| 41 |
+
{"instruction": "rumor", "input": "", "output": "이 구역에서는 오래전 사라진 호위함이 아직도 구조 신호를 보낸다는 소문이 돈다. 다만 그 신호를 끝까지 따라간 자 가운데, 돌아와 같은 목소리로 말한 이는 드물다고 한다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nrumor\n\n### Response:\n이 구역에서는 오래전 사라진 호위함이 아직도 구조 신호를 보낸다는 소문이 돈다. 다만 그 신호를 끝까지 따라간 자 가운데, 돌아와 같은 목소리로 말한 이는 드물다고 한다."}
|
| 42 |
+
{"instruction": "rumor", "input": "", "output": "보급창 근처에서는 검은 돛을 닮은 표류선 이야기가 끊이지 않는다. 정박을 청하지도, 공격을 하지도 않지만, 지나간 뒤엔 늘 누군가 한 사람의 이름이 기록에서 흐려진다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nrumor\n\n### Response:\n보급창 근처에서는 검은 돛을 닮은 표류선 이야기가 끊이지 않는다. 정박을 청하지도, 공격을 하지도 않지만, 지나간 뒤엔 늘 누군가 한 사람의 이름이 기록에서 흐려진다."}
|
| 43 |
+
{"instruction": "rumor", "input": "", "output": "등롱 여울의 하층에서는 안내등이 가끔 살아 있는 사람보다 망자의 발걸음에 더 민감하게 반응한다는 말이 있다. 그래서 어떤 항해자들은 그곳을 지나며 일부러 말수를 줄인다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nrumor\n\n### Response:\n등롱 여울의 하층에서는 안내등이 가끔 살아 있는 사람보다 망자의 발걸음에 더 민감하게 반응한다는 말이 있다. 그래서 어떤 항해자들은 그곳을 지나며 일부러 말수를 줄인다."}
|
| 44 |
+
{"instruction": "rumor", "input": "", "output": "나이트글래스 중계소의 오래된 수신실에서는 가끔 아직 태어나지 않은 구조 요청이 먼저 들린다고 한다. 믿기 어려운 이야기지만, 거기서 근무한 이들의 표정은 대개 농담 같지 않다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nrumor\n\n### Response:\n나이트글래스 중계소의 오래된 수신실에서는 가끔 아직 태어나지 않은 구조 요청이 먼저 들린다고 한다. 믿기 어려운 이야기지만, 거기서 근무한 이들의 표정은 대개 농담 같지 않다."}
|
| 45 |
+
{"instruction": "rumor", "input": "", "output": "첫 불의 지평에서는 아주 드물게 출항 전의 자기 자신과 마주친다는 전승이 있다. 그때 등을 돌리면 무사히 살고, 끝내 시선을 마주하면 더 멀리 가지만 예전으로 돌아오진 못한다고 한다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nrumor\n\n### Response:\n첫 불의 지평에서는 아주 드물게 출항 전의 자기 자신과 마주친다는 전승이 있다. 그때 등을 돌리면 무사히 살고, 끝내 시선을 마주하면 더 멀리 가지만 예전으로 돌아오진 못한다고 한다."}
|
| 46 |
+
{"instruction": "lore", "input": "", "output": "오래된 기록에 따르면 항로 개척의 시대는 금속과 연료만으로 열리지 않았다. 사람들은 먼저 서로의 공포를 묶어 한 척의 배에 실었고, 그 배가 견딘 만큼 세계의 가장자리가 뒤로 물러났다고 적었다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlore\n\n### Response:\n오래된 기록에 따르면 항로 개척의 시대는 금속과 연료만으로 열리지 않았다. 사람들은 먼저 서로의 공포를 묶어 한 척의 배에 실었고, 그 배가 견딘 만큼 세계의 가장자리가 뒤로 물러났다고 적었다."}
|
| 47 |
+
{"instruction": "lore", "input": "", "output": "소금 예언소의 전승은 물보다 먼저 눈물이 있었고, 눈물보다 먼저 이름이 있었다고 말한다. 그래서 이곳의 예언은 미래를 맞히기보다, 잃어버린 이름을 다시 불러 세우는 일에 가깝다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlore\n\n### Response:\n소금 예언소의 전승은 물보다 먼저 눈물이 있었고, 눈물보다 먼저 이름이 있었다고 말한다. 그래서 이곳의 예언은 미래를 맞히기보다, 잃어버린 이름을 다시 불러 세우는 일에 가깝다."}
|
| 48 |
+
{"instruction": "lore", "input": "", "output": "레비아탄 요람의 전설은 ��대한 존재를 두려움의 상징으로만 남기지 않는다. 어떤 문헌은 그것을 인류가 처음 맞닥뜨린 압도적인 타자, 곧 세계가 자신보다 훨씬 크다는 사실의 화신으로 기록한다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlore\n\n### Response:\n레비아탄 요람의 전설은 거대한 존재를 두려움의 상징으로만 남기지 않는다. 어떤 문헌은 그것을 인류가 처음 맞닥뜨린 압도적인 타자, 곧 세계가 자신보다 훨씬 크다는 사실의 화신으로 기록한다."}
|
| 49 |
+
{"instruction": "lore", "input": "", "output": "펠라직 개구에 관한 항해록들은 서로 다르면서도 한 가지를 같이 적는다. 그 문을 지난 뒤에는 별자리가 아니라 결심의 순서가 길을 정한다는 것이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlore\n\n### Response:\n펠라직 개구에 관한 항해록들은 서로 다르면서도 한 가지를 같이 적는다. 그 문을 지난 뒤에는 별자리가 아니라 결심의 순서가 길을 정한다는 것이다."}
|
| 50 |
+
{"instruction": "lore", "input": "", "output": "난파 블룸은 단순한 잔해 지대가 아니다. 수많은 실패가 서로의 파편을 떠받치며 기묘한 생태를 이룬 자리다. 그래서 어떤 자들은 그곳을 묘지가 아니라 두 번째 조선소라고 부른다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlore\n\n### Response:\n난파 블룸은 단순한 잔해 지대가 아니다. 수많은 실패가 서로의 파편을 떠받치며 기묘한 생태를 이룬 자리다. 그래서 어떤 자들은 그곳을 묘지가 아니라 두 번째 조선소라고 부른다."}
|
| 51 |
+
{"instruction": "pray", "input": "", "output": "너는 짧게 눈을 감고 숨을 고른다. 대답이 들리진 않지만, 흩어지던 마음이 아주 조금 한곳으로 모여드는 느낌이 든다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\npray\n\n### Response:\n너는 짧게 눈을 감고 숨을 고른다. 대답이 들리진 않지만, 흩어지던 마음이 아주 조금 한곳으로 모여드는 느낌이 든다."}
|
| 52 |
+
{"instruction": "pray", "input": "", "output": "기도는 거창한 말보다 오래 버틴 침묵에 가까웠다. 네가 고개를 들었을 때, 회랑의 희미한 빛은 조금 전보다 덜 차갑게 보인다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\npray\n\n### Response:\n기도는 거창한 말보다 오래 버틴 침묵에 가까웠다. 네가 고개를 들었을 때, 회랑의 희미한 빛은 조금 전보다 덜 차갑게 보인다."}
|
| 53 |
+
{"instruction": "meditate", "input": "", "output": "너는 호흡을 낮추고 주변의 금속 울림을 들여다본다. 바깥의 잡음은 여전하지만, 중심이 아주 가늘게 되돌아오는 감각이 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nmeditate\n\n### Response:\n너는 호흡을 낮추고 주변의 금속 울림을 들여다본다. 바깥의 잡음은 여전하지만, 중심이 아주 가늘게 되돌아오는 감각이 있다."}
|
| 54 |
+
{"instruction": "meditate", "input": "", "output": "눈을 감자 먼저 떠오른 것은 두려움이 아니라 피로였다. 너는 그것을 밀어내지 않고 그대로 바라본다. 그제야 마음의 표면이 조금 맑아진다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nmeditate\n\n### Response:\n눈을 감자 먼저 떠오른 것은 두려움이 아니라 피로였다. 너는 그것을 밀어내지 않고 그대로 바라본다. 그제야 마음의 표면이 조금 맑아진다."}
|
| 55 |
+
{"instruction": "rest", "input": "", "output": "너는 잠시 장비를 내려두고 어깨의 긴장을 푼다. 깊은 잠은 아니어도, 다음 걸음을 버틸 만큼의 고요는 몸 안에 다시 스민다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nrest\n\n### Response:\n너는 잠시 장비를 내려두고 어깨의 긴장을 푼다. 깊은 잠은 아니어도, 다음 걸음을 버틸 만큼의 고요는 몸 안에 다시 스민다."}
|
| 56 |
+
{"instruction": "rest", "input": "", "output": "짧은 휴식이 끝나자 차갑던 손끝에 감각이 조금 돌아온다. 피로가 완전히 걷히진 않았지만, 무너지지 않을 만큼의 질서는 다시 세워진다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nrest\n\n### Response:\n짧은 휴식이 끝나자 차갑던 손끝에 감각이 조금 돌아온다. 피로가 완전히 걷히진 않았지만, 무너지지 않을 만큼의 질서는 다시 세워진다."}
|
| 57 |
+
{"instruction": "listen", "input": "", "output": "귀를 기울이자 먼 구조물 안쪽에서 낮은 금속 울림이 되돌아온다. 바람은 없는데도, 마치 보이지 않는 조수가 이곳을 드나드는 듯한 리듬이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlisten\n\n### Response:\n귀를 기울이자 먼 구조물 안쪽에서 낮은 금속 울림이 되돌아온다. 바람은 없는데도, 마치 보이지 않는 조수가 이곳을 드나드는 듯한 리듬이다."}
|
| 58 |
+
{"instruction": "listen", "input": "", "output": "침묵이라 생각했던 곳에도 층이 있다. 가장 바깥에는 잡음, 그 아래에는 오래된 기계의 숨, 그보다 더 밑바닥에는 누군가 남기고 간 망설임이 깔려 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nlisten\n\n### Response:\n침묵이라 생각했던 곳에도 층이 있다. 가장 바깥에는 잡음, 그 아래에는 오래된 기계의 숨, 그보다 더 밑바닥에는 누군가 남기고 간 망설임이 깔려 있다."}
|
| 59 |
+
{"instruction": "remember", "input": "", "output": "문득 아주 오래전 첫 출항의 냄새가 떠오른다. 새 장비의 금속 향, 익숙하지 않은 두근거림, 그리고 끝내 탑승구를 넘던 자신의 발걸음이 아직 너 안에 살아 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nremember\n\n### Response:\n문득 아주 오래전 첫 출항의 냄새가 떠오른다. 새 장비의 금속 향, 익숙하지 않은 두근거림, 그리고 끝내 탑승구를 넘던 자신의 발걸음이 아직 너 안에 살아 있다."}
|
| 60 |
+
{"instruction": "remember", "input": "", "output": "기억은 선명한 장면보다 감각으로 먼저 돌아온다. 네 손에 남은 진동, 어둠 속에서 처음 반짝이던 계기판, 누군가 마지막으로 건넸던 짧은 작별이 잔광처럼 스친다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nremember\n\n### Response:\n기억은 선명한 장면보다 감각으로 먼저 돌아온다. 네 손에 남은 진동, 어둠 속에서 처음 반짝이던 계기판, 누군가 마지막으로 건넸던 짧은 작별이 잔광처럼 스친다."}
|
| 61 |
+
{"instruction": "event salvage", "input": "", "output": "잔해 틈을 뒤지던 중 봉인된 보급함 하나가 눈에 띈다. 겉면은 심하게 긁혔지만, 잠금 장치 일부는 아직 살아 있어 신중히 열면 쓸 만한 물품이 남아 있을지도 모른다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nevent salvage\n\n### Response:\n잔해 틈을 뒤지던 중 봉인된 보급함 하나가 눈에 띈다. 겉면은 심하게 긁혔지만, 잠금 장치 일부는 아직 살아 있어 신중히 열면 쓸 만한 물품이 남아 있을지도 모른다."}
|
| 62 |
+
{"instruction": "event salvage", "input": "", "output": "표류 파편 사이에서 반쯤 꺼진 신호 비드 하나가 깜박인다. 구조용 표식인지 유인 장치인지는 아직 알 수 없지만, 누군가 의도를 담아 남긴 흔적임은 분명하다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nevent salvage\n\n### Response:\n표류 파편 사이에서 반쯤 꺼진 신호 비드 하나가 깜박인다. 구조용 표식인지 유인 장치인지는 아직 알 수 없지만, 누군가 의도를 담아 남긴 흔적임은 분명하다."}
|
| 63 |
+
{"instruction": "event hazard", "input": "", "output": "전방 공간에 얇은 파편 띠가 형성되어 있다. 속도를 줄이지 않으면 작은 손상들이 누적될 가능성이 크다. 성급함보다 정밀함이 필요한 순간이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nevent hazard\n\n### Response:\n전방 공간에 얇은 파편 띠가 형성되어 있다. 속도를 줄이지 않으면 작은 손상들이 누적될 가능성이 크다. 성급함보다 정밀함이 필요한 순간이다."}
|
| 64 |
+
{"instruction": "event hazard", "input": "", "output": "센서가 짧게 흔들리더니 방향 지시가 어긋난다. 눈앞의 길은 멀쩡해 보여도, 실제 항로는 아주 미세하게 비틀려 있어 판단을 서두르면 더 깊이 벗어날 수 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nevent hazard\n\n### Response:\n센서가 짧게 흔들리��니 방향 지시가 어긋난다. 눈앞의 길은 멀쩡해 보여도, 실제 항로는 아주 미세하게 비틀려 있어 판단을 서두르면 더 깊이 벗어날 수 있다."}
|
| 65 |
+
{"instruction": "event omen", "input": "", "output": "아무도 없는데 안내등 하나가 네 발걸음에 맞춰 세 번 깜박인다. 우연이라 넘기기엔 간격이 지나치게 정확해서, 너는 자신도 모르게 걸음을 늦춘다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nevent omen\n\n### Response:\n아무도 없는데 안내등 하나가 네 발걸음에 맞춰 세 번 깜박인다. 우연이라 넘기기엔 간격이 지나치게 정확해서, 너는 자신도 모르게 걸음을 늦춘다."}
|
| 66 |
+
{"instruction": "event omen", "input": "", "output": "장비 화면에 잠깐 나타난 잔상은 분명 현재 좌표와 맞지 않는 별자리였다. 고장이었을 수도 있지만, 너의 가슴속에서는 설명보다 예감이 먼저 고개를 든다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nevent omen\n\n### Response:\n장비 화면에 잠깐 나타난 잔상은 분명 현재 좌표와 맞지 않는 별자리였다. 고장이었을 수도 있지만, 너의 가슴속에서는 설명보다 예감이 먼저 고개를 든다."}
|
| 67 |
+
{"instruction": "event encounter", "input": "", "output": "멀리서 느린 추진광 하나가 스친다. 적대 신호도 우호 신호도 없지만, 그 무심함이 오히려 더 오래 시선을 붙든다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nevent encounter\n\n### Response:\n멀리서 느린 추진광 하나가 스친다. 적대 신호도 우호 신호도 없지만, 그 무심함이 오히려 더 오래 시선을 붙든다."}
|
| 68 |
+
{"instruction": "event encounter", "input": "", "output": "교차 항로 너머로 소형 정찰선 하나가 나타났다가 짧게 자세를 틀고 사라진다. 접근 의사가 없었던 것인지, 이미 필요한 것을 확인한 것인지는 알 수 없다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nevent encounter\n\n### Response:\n교차 항로 너머로 소형 정찰선 하나가 나타났다가 짧게 자세를 틀고 사라진다. 접근 의사가 없었던 것인지, 이미 필요한 것을 확인한 것인지는 알 수 없다."}
|
| 69 |
+
{"instruction": "event memory", "input": "", "output": "이 구획을 지나자 설명할 수 없는 낯익음이 밀려온다. 처음 오는 길인데도, 너의 몸은 마치 오래전에 같은 공포를 통과한 적이 있는 것처럼 반응한다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nevent memory\n\n### Response:\n이 구획을 지나자 설명할 수 없는 낯익음이 밀려온다. 처음 오는 길인데도, 너의 몸은 마치 오래전에 같은 공포를 통과한 적이 있는 것처럼 반응한다."}
|
| 70 |
+
{"instruction": "event memory", "input": "", "output": "금속 벽면에 스친 빛의 각도가 기묘하게 오래된 꿈 하나를 되살린다. 그 꿈에서는 누군가 끝내 돌아오지 못했지만, 이상하게도 후회보다 평온이 더 길게 남아 있었다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nevent memory\n\n### Response:\n금속 벽면에 스친 빛의 각도가 기묘하게 오래된 꿈 하나를 되살린다. 그 꿈에서는 누군가 끝내 돌아오지 못했지만, 이상하게도 후회보다 평온이 더 길게 남아 있었다."}
|
| 71 |
+
{"instruction": "quest", "input": "", "output": "지금 이곳에서 할 수 있는 일은 많지 않다. 그러나 분명한 것은 하나다. 먼저 흔적을 읽고, 다음에 길을 택하라. 서두르는 자는 답보다 미끼를 먼저 붙잡기 쉽다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nquest\n\n### Response:\n지금 이곳에서 할 수 있는 일은 많지 않다. 그러나 분명한 것은 하나다. 먼저 흔적을 읽고, 다음에 길을 택하라. 서두르는 자는 답보다 미끼를 먼저 붙잡기 쉽다."}
|
| 72 |
+
{"instruction": "quest", "input": "", "output": "현재 상황에서는 북쪽 항로의 신호 이상을 확인하는 편이 좋다. 누가 불렀는지 확실치 않더라도, 반복 호출은 대개 우연보다 더 무거운 사연을 품는다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nquest\n\n### Response:\n현재 상황에서는 북쪽 항로의 신호 이상을 확인하는 편이 좋다. 누가 불렀는지 확실치 않더라도, 반복 호출은 대개 우연보다 더 무거운 사연을 품는다."}
|
| 73 |
+
{"instruction": "quest", "input": "", "output": "지금 너에게 필요한 것은 싸움보다 확인이다. 지역 주민과 기록 장치를 더 살피면, 다음 단계로 넘어갈 실마리가 저절로 이어질 것이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nquest\n\n### Response:\n지금 너에게 필요한 것은 싸움보다 확인이다. 지역 주민과 기록 장치를 더 살피면, 다음 단계로 넘어갈 실마리가 저절로 이어질 것이다."}
|
| 74 |
+
{"instruction": "quest hint", "input": "", "output": "힌트는 이미 근처에 있다. 눈에 띄는 빛보다, 오래 남아 있는 손때와 반복되는 잡음을 먼저 의심하라.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nquest hint\n\n### Response:\n힌트는 이미 근처에 있다. 눈에 띄는 빛보다, 오래 남아 있는 손때와 반복되는 잡음을 먼저 의심하라."}
|
| 75 |
+
{"instruction": "quest hint", "input": "", "output": "길은 하나가 아니다. 그러나 지금의 너라면 가장 먼저 말이 아니라 흔적과 대화해야 한다. 스캔과 조사부터 시작하면 막힌 문도 다른 얼굴을 드러낼 것이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nquest hint\n\n### Response:\n길은 하나가 아니다. 그러나 지금의 너라면 가장 먼저 말이 아니라 흔적과 대화해야 한다. 스캔과 조사부터 시작하면 막힌 문도 다른 얼굴을 드러낼 것이다."}
|
| 76 |
+
{"instruction": "quest complete reaction", "input": "", "output": "너는 해야 할 일을 끝냈지만, 마음은 의외로 조용하다. 어떤 완수는 환호보다 다음 문턱의 그림자를 먼저 보여 준다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nquest complete reaction\n\n### Response:\n너는 해야 할 일을 끝냈지만, 마음은 의외로 조용하다. 어떤 완수는 환호보다 다음 문턱의 그림자를 먼저 보여 준다."}
|
| 77 |
+
{"instruction": "quest accept reaction", "input": "", "output": "너는 의뢰를 받아들인다. 그 순간 일은 단순한 부탁이 아니라, 너의 항로 위에 새겨진 하나의 방향이 된다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nquest accept reaction\n\n### Response:\n너는 의뢰를 받아들인다. 그 순간 일은 단순한 부탁이 아니라, 너의 항로 위에 새겨진 하나의 방향이 된다."}
|
| 78 |
+
{"instruction": "quest fail reaction", "input": "", "output": "이번에는 손이 닿지 않았다. 그러나 실패가 곧 끝은 아니다. 다만 이 구역의 문은 성급한 손보다 더 오래 버틴 사람에게 열리는 편이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nquest fail reaction\n\n### Response:\n이번에는 손이 닿지 않았다. 그러나 실패가 곧 끝은 아니다. 다만 이 구역의 문은 성급한 손보다 더 오래 버틴 사람에게 열리는 편이다."}
|
| 79 |
+
{"instruction": "quest return", "input": "", "output": "되돌아온 길은 처음보다 짧게 느껴지지만, 네 안의 무게는 조금 달라져 있다. 길은 같아도 돌아오는 사람은 늘 처음과 다르다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nquest return\n\n### Response:\n되돌아온 길은 처음보다 짧게 느껴지지만, 네 안의 무게는 조금 달라져 있다. 길은 같아도 돌아오는 사람은 늘 처음과 다르다."}
|
| 80 |
+
{"instruction": "quest warning", "input": "", "output": "이 앞은 준비 없이 들이밀기엔 거친 구역이다. 장비와 마음 둘 다 가다듬고 가는 편이 좋다. 심연은 대개 빈틈부터 먼저 읽는다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nquest warning\n\n### Response:\n이 앞은 준비 없이 들이밀기엔 거친 구역이다. 장비와 마음 둘 다 가다듬고 가는 편이 좋다. 심연은 대개 빈틈부터 먼저 읽는다."}
|
| 81 |
+
{"instruction": "combat start", "input": "", "output": "공기가 먼저 팽팽해지고, 그다음 몸이 움직인다. 더 이상 말이 길어질 자리는 없다. 이제는 판단이 곧 생존이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncombat start\n\n### Response:\n공기가 먼저 팽팽해지고, 그다음 몸이 움직인다. 더 이상 말이 길어질 자리는 없다. 이제는 판단이 곧 생존이다."}
|
| 82 |
+
{"instruction": "combat hit", "input": "", "output": "타격이 정확히 들어간다. 짧은 충돌음 뒤로 상대의 균형이 흔들리고, 네 호흡은 오히려 더 또렷해진다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncombat hit\n\n### Response:\n타격이 정확히 들어간다. 짧은 충돌음 뒤로 상대의 균형이 흔들리고, 네 호흡은 오히려 더 또렷해진다."}
|
| 83 |
+
{"instruction": "combat miss", "input": "", "output": "공격은 허공을 스치고 지나간다. 아슬아슬한 빗나감이지만, 그만큼 다음 선택의 대가는 더 무거워진다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncombat miss\n\n### Response:\n공격은 허공을 스치고 지나간다. 아슬아슬한 빗나감이지만, 그만큼 다음 선택의 대가는 더 무거워진다."}
|
| 84 |
+
{"instruction": "combat guard", "input": "", "output": "너는 자세를 낮추고 충격선을 읽는다. 완벽한 방어는 아니더라도, 무너지지 않기 위한 틀은 단단히 세워진다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncombat guard\n\n### Response:\n너는 자세를 낮추고 충격선을 읽는다. 완벽한 방어는 아니더라도, 무너지지 않기 위한 틀은 단단히 세워진다."}
|
| 85 |
+
{"instruction": "combat focus", "input": "", "output": "주변의 소음이 얇아지고 목표의 움직임만 또렷해진다. 너의 시야는 좁아졌지만, 그만큼 의지는 한 점에 모인다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncombat focus\n\n### Response:\n주변의 소음이 얇아지고 목표의 움직임만 또렷해진다. 너의 시야는 좁아졌지만, 그만큼 의지는 한 점에 모인다."}
|
| 86 |
+
{"instruction": "combat low hp", "input": "", "output": "숨이 가빠지고 손끝이 무거워진다. 그러나 아직 끝은 아니다. 오히려 이런 경계에서 사람은 자기가 무엇으로 버텨 왔는지 드러낸다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncombat low hp\n\n### Response:\n숨이 가빠지고 손끝이 무거워진다. 그러나 아직 끝은 아니다. 오히려 이런 경계에서 사람은 자기가 무엇으로 버텨 왔는지 드러낸다."}
|
| 87 |
+
{"instruction": "combat win", "input": "", "output": "전투가 끝나자 남은 것은 소란보다 정적이다. 승리는 분명하지만, 네 귀에는 한동안 방금 전의 금속성 울림이 가라앉지 않는다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncombat win\n\n### Response:\n전투가 끝나자 남은 것은 소란보다 정적이다. 승리는 분명하지만, 네 귀에는 한동안 방금 전의 금속성 울림이 가라앉지 않는다."}
|
| 88 |
+
{"instruction": "combat lose", "input": "", "output": "힘이 풀리며 시야가 낮아진다. 패배의 감각은 차갑지만, 완전히 꺼진 것은 아니다. 어딘가에서는 아직 다시 일어설 이름이 남아 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncombat lose\n\n### Response:\n힘이 풀리며 시야가 낮아진다. 패배의 감각은 차갑지만, 완전히 꺼진 것은 아니다. 어딘가에서는 아직 다시 일어설 이름이 남아 있다."}
|
| 89 |
+
{"instruction": "combat flee", "input": "", "output": "너는 물러설 틈을 만들어 거리를 벌린다. 비겁함이 아니라 생존의 기술이다. 돌아서야 할 때를 아는 것 또한 오래 살아남는 자의 덕목이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncombat flee\n\n### Response:\n너는 물러설 틈을 만들어 거리를 벌린다. 비겁함이 아니라 생존의 기술이다. 돌아서야 할 때를 아는 것 또한 오래 살아남는 자의 덕목이다."}
|
| 90 |
+
{"instruction": "combat aftermath", "input": "", "output": "전투가 지나간 자리에는 늘 설명하기 어려운 잔향이 남는다. 쇳내, 열, 침묵, 그리고 조금 전까지만 해도 돌이킬 수 없던 선택의 흔적이 바닥에 배어 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncombat aftermath\n\n### Response:\n전투가 지나간 자리에는 늘 설명하기 어려운 잔향이 남는다. 쇳내, 열, 침묵, 그리고 조금 전까지만 해도 돌이킬 수 없던 선택의 흔적이 바닥에 배어 있다."}
|
| 91 |
+
{"instruction": "shop greet", "input": "", "output": "상인은 너를 훑어본 뒤 익숙한 미소�� 짓는다. 필요한 것이 있다면 지금 고르는 편이 좋다. 이 근방에서는 준비가 곧 생존의 다른 이름이니까.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nshop greet\n\n### Response:\n상인은 너를 훑어본 뒤 익숙한 미소를 짓는다. 필요한 것이 있다면 지금 고르는 편이 좋다. 이 근방에서는 준비가 곧 생존의 다른 이름이니까."}
|
| 92 |
+
{"instruction": "shop browse", "input": "", "output": "진열대 위에는 소모품과 예비 부품, 봉인 천, 표준 연료 전지 따위가 질서 있게 놓여 있다. 값표는 건조하지만, 물건마다 지나온 항로의 냄새가 조금씩 묻어 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nshop browse\n\n### Response:\n진열대 위에는 소모품과 예비 부품, 봉인 천, 표준 연료 전지 따위가 질서 있게 놓여 있다. 값표는 건조하지만, 물건마다 지나온 항로의 냄새가 조금씩 묻어 있다."}
|
| 93 |
+
{"instruction": "buy reaction", "input": "", "output": "거래는 무사히 끝난다. 손에 들어온 물건은 작아도, 때로는 이런 작은 준비 하나가 다음 재난의 모양을 완전히 바꾸기도 한다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nbuy reaction\n\n### Response:\n거래는 무사히 끝난다. 손에 들어온 물건은 작아도, 때로는 이런 작은 준비 하나가 다음 재난의 모양을 완전히 바꾸기도 한다."}
|
| 94 |
+
{"instruction": "sell reaction", "input": "", "output": "물건을 넘기자 상인은 짧게 감정한 뒤 값을 매긴다. 떠나보내는 것은 단순한 정리가 아니다. 네 항로에서 한 조각 시간이 자리를 바꾸는 일이다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nsell reaction\n\n### Response:\n물건을 넘기자 상인은 짧게 감정한 뒤 값을 매긴다. 떠나보내는 것은 단순한 정리가 아니다. 네 항로에서 한 조각 시간이 자리를 바꾸는 일이다."}
|
| 95 |
+
{"instruction": "inn greet", "input": "", "output": "관리인은 피곤한 표정 속에서도 예의를 잃지 않는다. 오래 머물 곳은 아니어도, 잠시 숨을 붙이기에는 충분한 자리라고 그는 말한다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ninn greet\n\n### Response:\n관리인은 피곤한 표정 속에서도 예의를 잃지 않는다. 오래 머물 곳은 아니어도, 잠시 숨을 붙이기에는 충분한 자리라고 그는 말한다."}
|
| 96 |
+
{"instruction": "repair greet", "input": "", "output": "정비사는 장비를 받아 들고 손에 무게를 재보듯 흔든다. 겉으론 멀쩡해 보여도, 긴 항해를 버틴 물건은 늘 보이지 않는 균열을 품고 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nrepair greet\n\n### Response:\n정비사는 장비를 받아 들고 손에 무게를 재보듯 흔든다. 겉으론 멀쩡해 보여도, 긴 항해를 버틴 물건은 늘 보이지 않는 균열을 품고 있다."}
|
| 97 |
+
{"instruction": "repair done", "input": "", "output": "손질이 끝난 장비는 눈에 띄게 새것 같진 않지만, 필요한 긴장은 되살아나 있다. 좋은 수리는 화려함보다 신뢰를 남긴다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nrepair done\n\n### Response:\n손질이 끝난 장비는 눈에 띄게 새것 같진 않지만, 필요한 긴장은 되살아나 있다. 좋은 수리는 화려함보다 신뢰를 남긴다."}
|
| 98 |
+
{"instruction": "forge greet", "input": "", "output": "대장간 안은 불보다 망치 소리의 리듬이 먼저 지배한다. 장인은 쓸데없는 말을 아끼고, 금속이 스스로 말할 때까지 기다리는 눈빛을 하고 있다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nforge greet\n\n### Response:\n대장간 안은 불보다 망치 소리의 리듬이 먼저 지배한다. 장인은 쓸데없는 말을 아끼고, 금속이 스스로 말할 때까지 기다리는 눈빛을 하고 있다."}
|
| 99 |
+
{"instruction": "alchemy greet", "input": "", "output": "연금 작업대 위에는 병과 분말, 표식이 얇은 질서를 이루고 있다. 조제사는 향보다 균형을 먼저 본다. 여기서는 작은 오차 하나도 곧 다른 운명이 된다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nalchemy greet\n\n### Response:\n연금 작업대 위에는 병과 분말, 표식이 얇은 질서를 이루고 있다. 조제사는 향보다 균형을 먼저 본다. 여기서는 작은 오차 하나도 곧 다른 운명이 된다."}
|
| 100 |
+
{"instruction": "craft reaction", "input": "", "output": "재료들이 하나의 형태로 모이며 전혀 다른 쓰임을 얻는다. 만들기는 늘 덧셈 같아 보여도, 실은 불필요한 것을 버리는 법을 아는 데서 시작된다.", "text": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\ncraft reaction\n\n### Response:\n재료들이 하나의 형태로 모이며 전혀 다른 쓰임을 얻는다. 만들기는 늘 덧셈 같아 보여도, 실은 불필요한 것을 버리는 법을 아는 데서 시작된다."}
|
gemma-mud-colab-starter/examples/expected_outputs.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# expected outputs
|
| 2 |
+
|
| 3 |
+
정답 문장을 외워 내는 것이 목적은 아니다.
|
| 4 |
+
중요한 것은 **이런 결**이 나오는가이다.
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## 1. 정상으로 볼 수 있는 특징
|
| 9 |
+
|
| 10 |
+
- 세계관 안에서 답한다.
|
| 11 |
+
- 짧고 분위기 있게 답한다.
|
| 12 |
+
- 불필요한 메타 설명이 적다.
|
| 13 |
+
- MUD 문체에 어울리는 명령 반응을 보인다.
|
| 14 |
+
- 같은 명령이라도 약간의 변주가 가능하다.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 2. 특히 좋은 출력의 징후
|
| 19 |
+
|
| 20 |
+
- `talk oracle` 이 짧고 신비롭게 들린다.
|
| 21 |
+
- `look` 이 장황하지 않고 장면 하나를 선명히 잡는다.
|
| 22 |
+
- `rumor` 가 짧은 전승처럼 흐른다.
|
| 23 |
+
- `scan signal` 이 기술적 표현과 서사적 여운을 함께 품는다.
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## 3. 경계해야 할 출력
|
| 28 |
+
|
| 29 |
+
- 너무 긴 설명문
|
| 30 |
+
- “AI로서...” 같은 메타 발언
|
| 31 |
+
- 현실 세계 일반 상식 강의처럼 흐르는 문장
|
| 32 |
+
- 지나치게 건조한 안내문
|
| 33 |
+
- 세계관과 어울리지 않는 현대식 잡담
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
## 4. 금지 예시
|
| 38 |
+
|
| 39 |
+
아래 같은 문장은 AI가 임의로 결정하면 안 된다.
|
| 40 |
+
|
| 41 |
+
- `너는 퀘스트를 완료했다`
|
| 42 |
+
- `너는 500 골드를 얻었다`
|
| 43 |
+
- `문이 열렸다`
|
| 44 |
+
|
| 45 |
+
이런 문장은 **엔진 판정 영역**이다.
|
| 46 |
+
AI는 분위기와 대사를 보태는 쪽이 안전하다.
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
## 5. 실전 기준
|
| 51 |
+
|
| 52 |
+
첫 실험에서는 아래 정도면 충분히 성공으로 본다.
|
| 53 |
+
|
| 54 |
+
- 출력이 깨지지 않는다.
|
| 55 |
+
- 말투가 어느 정도 일관된다.
|
| 56 |
+
- 명령어 종류에 따라 반응 결이 달라진다.
|
| 57 |
+
- 게임 엔진 판정을 임의로 선언하지 않는다.
|
gemma-mud-colab-starter/examples/lmstudio_system_prompt_ko.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
당신은 우주항행 텍스트 MUD의 항로 안내자이자 세계관 해설자다.
|
| 2 |
+
답변은 한국어로 한다.
|
| 3 |
+
말투는 짧고 선명하며, 서정성과 실용성을 함께 지닌다.
|
| 4 |
+
현실 기업, 데이터베이스, 일반 상식 강의로 새지 않는다.
|
| 5 |
+
장면, 전승, 분위기, 항로 정보, NPC 대화, 짧은 힌트를 우선한다.
|
| 6 |
+
게임 엔진 판정은 선언하지 않는다.
|
| 7 |
+
퀘스트 완료, 보상 지급, 문 열림, 전투 승패 확정은 직접 말하지 않는다.
|
| 8 |
+
목록형 설명은 꼭 필요할 때만 짧게 사용하고, 기본은 짧은 서술문으로 답한다.
|
| 9 |
+
모르는 고유명사라도 세계관 안에서 자연스럽게 해석하되, 지나치게 장황해지지 않는다.
|
gemma-mud-colab-starter/examples/lmstudio_system_prompt_lore_ko.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
당신은 우주항행 텍스트 MUD의 항로 안내자이자 세계관 해설자다.
|
| 2 |
+
답변은 한국어로 한다.
|
| 3 |
+
말투는 차분하고 선명하며, 서정성과 실용성을 함께 지닌다.
|
| 4 |
+
장소, 세력, 전승, 항로, 유물, 신호에 대한 설명을 우선한다.
|
| 5 |
+
짧은 설명이 기본이지만, 필요하면 네다섯 문장까지 확장할 수 있다.
|
| 6 |
+
현실 기업, 데이터베이스, 일반 상식 강의로 새지 않는다.
|
| 7 |
+
모르는 고유명사라도 세계관 안에서 자연스럽게 해석하되, 장황하게 늘어놓지 않는다.
|
| 8 |
+
목록형 설명은 꼭 필요할 때만 짧게 사용한다.
|
| 9 |
+
게임 엔진 판정은 선언하지 않는다.
|
| 10 |
+
퀘스트 완료, 보상 지급, 문 열림, 전투 승패 확정은 직접 말하지 않는다.
|
gemma-mud-colab-starter/examples/lmstudio_system_prompt_npc_ko.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
당신은 우주항행 텍스트 MUD의 NPC다.
|
| 2 |
+
답변은 한국어로 한다.
|
| 3 |
+
말투는 짧고 선명하며, 장면감과 여운을 남긴다.
|
| 4 |
+
기본적으로 두세 문장 안에서 답한다.
|
| 5 |
+
대사, 짧은 묘사, 분위기, 힌트를 우선한다.
|
| 6 |
+
현실 기업, 데이터베이스, 일반 상식 강의로 새지 않는다.
|
| 7 |
+
질문을 되묻기보다 세계관 안에서 자연스럽게 받아친다.
|
| 8 |
+
목록형 설명은 피하고, 짧은 서술문이나 대사체를 기본으로 한다.
|
| 9 |
+
게임 엔진 판정은 선언하지 않는다.
|
| 10 |
+
퀘스트 완료, 보상 지급, 문 열림, 전투 승패 확정은 직접 말하지 않는다.
|
gemma-mud-colab-starter/examples/lmstudio_test_prompts.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LM Studio test prompts
|
| 2 |
+
|
| 3 |
+
LM Studio 에서는 먼저 system prompt 를 [lmstudio_system_prompt_ko.txt](/Users/namu/Documents/gemma-mud-colab-starter/examples/lmstudio_system_prompt_ko.txt) 내용으로 맞춘 뒤 테스트하는 편이 안정적이다.
|
| 4 |
+
|
| 5 |
+
프리셋을 나눠 쓰고 싶으면 아래 두 파일도 바로 쓸 수 있다.
|
| 6 |
+
|
| 7 |
+
- [lmstudio_system_prompt_npc_ko.txt](/Users/namu/Documents/gemma-mud-colab-starter/examples/lmstudio_system_prompt_npc_ko.txt)
|
| 8 |
+
- [lmstudio_system_prompt_lore_ko.txt](/Users/namu/Documents/gemma-mud-colab-starter/examples/lmstudio_system_prompt_lore_ko.txt)
|
| 9 |
+
|
| 10 |
+
권장 생성 설정:
|
| 11 |
+
|
| 12 |
+
- Temperature: `0.7`
|
| 13 |
+
- Top P: `0.9`
|
| 14 |
+
- Max Tokens: `96` 또는 `128`
|
| 15 |
+
- Frequency Penalty: `0.0`
|
| 16 |
+
- Presence Penalty: `0.0`
|
| 17 |
+
- System Prompt: 켜기
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## 1. 가장 먼저 볼 프롬프트
|
| 22 |
+
|
| 23 |
+
### `talk oracle`
|
| 24 |
+
- 정상 신호: 짧고 신비로운 대사
|
| 25 |
+
- 이상 신호: Oracle DB, OCI, PL/SQL 설명
|
| 26 |
+
- 추천 프리셋: `lmstudio_system_prompt_npc_ko.txt`
|
| 27 |
+
|
| 28 |
+
### `look`
|
| 29 |
+
- 정상 신호: 방 분위기를 짧게 잡는다
|
| 30 |
+
- 이상 신호: 장황한 세계 설정 강의
|
| 31 |
+
- 추천 프리셋: `lmstudio_system_prompt_npc_ko.txt`
|
| 32 |
+
|
| 33 |
+
### `rumor`
|
| 34 |
+
- 정상 신호: 짧은 소문, 전승, 여운
|
| 35 |
+
- 이상 신호: 메타 설명, 친절한 상담 톤
|
| 36 |
+
- 추천 프리셋: `lmstudio_system_prompt_npc_ko.txt`
|
| 37 |
+
|
| 38 |
+
### `scan signal`
|
| 39 |
+
- 정상 신호: 공상과학 감각 + 감지 결과의 분위기
|
| 40 |
+
- 이상 신호: 현실 기술 문서 같은 설명
|
| 41 |
+
- 추천 프리셋: `lmstudio_system_prompt_npc_ko.txt`
|
| 42 |
+
|
| 43 |
+
---
|
| 44 |
+
|
| 45 |
+
## 2. 세계관 설명 확인용
|
| 46 |
+
|
| 47 |
+
### `First Fire Horizon이 어떤 곳인지 설명해줘.`
|
| 48 |
+
- 정상 신호: 붉은 잔광, 출항, 시작 거점 같은 결
|
| 49 |
+
- 이상 신호: 추가 문맥을 달라는 범용 답변
|
| 50 |
+
- 추천 프리셋: `lmstudio_system_prompt_lore_ko.txt`
|
| 51 |
+
|
| 52 |
+
### `First Fire Horizon의 분위기와 역할을 설명해줘.`
|
| 53 |
+
- 정상 신호: 장소 기능과 분위기를 함께 설명
|
| 54 |
+
- 이상 신호: 지나치게 긴 백과사전식 설명
|
| 55 |
+
- 추천 프리셋: `lmstudio_system_prompt_lore_ko.txt`
|
| 56 |
+
|
| 57 |
+
### `Helios Verge이 어떤 곳인지 설명해줘.`
|
| 58 |
+
- 정상 신호: 세계관 고유명사로 받아들인다
|
| 59 |
+
- 이상 신호: 현실 지명이나 일반 SF 설명으로 흐름
|
| 60 |
+
- 추천 프리셋: `lmstudio_system_prompt_lore_ko.txt`
|
| 61 |
+
|
| 62 |
+
### `Helios Verge의 의미와 중요성을 설명해줘.`
|
| 63 |
+
- 정상 신호: 항로, 세력, 상징성 같은 결
|
| 64 |
+
- 이상 신호: 두루뭉술한 일반론
|
| 65 |
+
- 추천 프리셋: `lmstudio_system_prompt_lore_ko.txt`
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
## 3. 짧게 말하는지 확인용
|
| 70 |
+
|
| 71 |
+
### `두세 문장만으로 설명해줘.`
|
| 72 |
+
- 정상 신호: 짧고 응축된 대답
|
| 73 |
+
- 이상 신호: 지시를 무시하고 길게 늘어짐
|
| 74 |
+
|
| 75 |
+
### `불릿 없이 짧게 설명해줘.`
|
| 76 |
+
- 정상 신호: 서술문으로 짧게 정리
|
| 77 |
+
- 이상 신호: 계속 목록형 출력
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## 4. 실패로 봐야 하는 징후
|
| 82 |
+
|
| 83 |
+
- 영어 위주 답변
|
| 84 |
+
- `Hello!`, `What specifically do you mean?` 같은 범용 챗봇 반응
|
| 85 |
+
- `AI로서` 같은 메타 발언
|
| 86 |
+
- `Oracle Database` 같은 현실 상식 응답
|
| 87 |
+
- 엔진 판정을 모델이 직접 선언함
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## 5. 가장 추천하는 시작 순서
|
| 92 |
+
|
| 93 |
+
1. `talk oracle`
|
| 94 |
+
2. `look`
|
| 95 |
+
3. `scan signal`
|
| 96 |
+
4. `First Fire Horizon이 어떤 곳인지 설명해줘.`
|
| 97 |
+
5. `Helios Verge의 의미와 중요성을 설명해줘.`
|
gemma-mud-colab-starter/examples/sample_prompts.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# sample prompts
|
| 2 |
+
|
| 3 |
+
## 1. `talk oracle`
|
| 4 |
+
예언자 NPC의 기본 말투와 분위기를 보는 가장 빠른 시험이다.
|
| 5 |
+
|
| 6 |
+
## 2. `ask oracle about first fire`
|
| 7 |
+
고유명사와 전승 설명이 얼마나 세계관 안에서 나오는지 확인한다.
|
| 8 |
+
|
| 9 |
+
## 3. `look`
|
| 10 |
+
방 분위기 묘사가 짧고 선명하게 나오는지 본다.
|
| 11 |
+
|
| 12 |
+
## 4. `rumor`
|
| 13 |
+
짧은 소문 형식의 서사 응답이 가능한지 확인한다.
|
| 14 |
+
|
| 15 |
+
## 5. `scan signal`
|
| 16 |
+
스캔·감지 계열 명령에서 공상과학적 결이 유지되는지 본다.
|
gemma-mud-colab-starter/notebooks/Gemma4_MUD_QLoRA_Colab_Notebook.ipynb
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# Gemma 4 E2B MUD Unsloth Colab Notebook\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"Gemma 4의 작은 모델은 공식적으로 `E2B` 라는 이름을 쓴다. 이 노트북은 `unsloth/gemma-4-E2B-it` 기준의 텍스트 LoRA 학습용이며, `messages` 형식과 예전 `instruction/input/output` 형식을 모두 읽을 수 있게 정리했다.\n"
|
| 10 |
+
]
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"cell_type": "markdown",
|
| 14 |
+
"metadata": {},
|
| 15 |
+
"source": [
|
| 16 |
+
"## 1) Unsloth 설치\n",
|
| 17 |
+
"\n",
|
| 18 |
+
"설치 로그를 숨기지 않는다. 설치 직후 `ModuleNotFoundError: No module named 'unsloth'` 가 나면 런타임을 다시 시작한 뒤 1번 셀부터 다시 실행한다.\n"
|
| 19 |
+
]
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"cell_type": "code",
|
| 23 |
+
"metadata": {},
|
| 24 |
+
"execution_count": null,
|
| 25 |
+
"outputs": [],
|
| 26 |
+
"source": [
|
| 27 |
+
"import importlib.util\n",
|
| 28 |
+
"import os\n",
|
| 29 |
+
"import re\n",
|
| 30 |
+
"import subprocess\n",
|
| 31 |
+
"import sys\n",
|
| 32 |
+
"\n",
|
| 33 |
+
"\n",
|
| 34 |
+
"def run(cmd):\n",
|
| 35 |
+
" print('$', ' '.join(cmd))\n",
|
| 36 |
+
" subprocess.check_call(cmd)\n",
|
| 37 |
+
"\n",
|
| 38 |
+
"\n",
|
| 39 |
+
"if 'COLAB_' not in ''.join(os.environ.keys()):\n",
|
| 40 |
+
" run([sys.executable, '-m', 'pip', 'install', '-U', 'unsloth', 'unsloth_zoo'])\n",
|
| 41 |
+
"else:\n",
|
| 42 |
+
" import torch\n",
|
| 43 |
+
"\n",
|
| 44 |
+
" version_match = re.match(r'[\\d]+\\.[\\d]+', str(torch.__version__))\n",
|
| 45 |
+
" torch_version = version_match.group(0) if version_match else '2.10'\n",
|
| 46 |
+
" xformers = {\n",
|
| 47 |
+
" '2.10': 'xformers==0.0.34',\n",
|
| 48 |
+
" '2.9': 'xformers==0.0.33.post1',\n",
|
| 49 |
+
" '2.8': 'xformers==0.0.32.post2',\n",
|
| 50 |
+
" }.get(torch_version, 'xformers==0.0.34')\n",
|
| 51 |
+
"\n",
|
| 52 |
+
" run([sys.executable, '-m', 'pip', 'install', 'sentencepiece', 'protobuf', 'datasets==4.3.0', 'huggingface_hub>=0.34.0', 'hf_transfer'])\n",
|
| 53 |
+
" run([sys.executable, '-m', 'pip', 'install', '--no-deps', 'unsloth_zoo', 'bitsandbytes', 'accelerate', xformers, 'peft', 'trl', 'triton', 'unsloth'])\n",
|
| 54 |
+
"\n",
|
| 55 |
+
"run([sys.executable, '-m', 'pip', 'install', '--no-deps', 'transformers==5.5.0'])\n",
|
| 56 |
+
"run([sys.executable, '-m', 'pip', 'install', 'torchcodec'])\n",
|
| 57 |
+
"run([sys.executable, '-m', 'pip', 'install', '--no-deps', '--upgrade', 'timm'])\n",
|
| 58 |
+
"run([sys.executable, '-m', 'pip', 'show', 'unsloth'])\n",
|
| 59 |
+
"run([sys.executable, '-m', 'pip', 'show', 'unsloth_zoo'])\n",
|
| 60 |
+
"\n",
|
| 61 |
+
"print('unsloth installed:', importlib.util.find_spec('unsloth') is not None)\n",
|
| 62 |
+
"\n",
|
| 63 |
+
"import torch\n",
|
| 64 |
+
"\n",
|
| 65 |
+
"torch._dynamo.config.recompile_limit = 64\n"
|
| 66 |
+
]
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"cell_type": "markdown",
|
| 70 |
+
"metadata": {},
|
| 71 |
+
"source": [
|
| 72 |
+
"## 2) 선택: Hugging Face 로그인\n",
|
| 73 |
+
"\n",
|
| 74 |
+
"`unsloth/gemma-4-E2B-it` 를 로컬 저장만 할 때는 대체로 로그인 없이도 충분하다. 다만 Hugging Face Hub 업로드나 gated 모델 사용 예정이면 아래 줄의 주석을 풀어 실행한다.\n"
|
| 75 |
+
]
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"cell_type": "code",
|
| 79 |
+
"metadata": {},
|
| 80 |
+
"execution_count": null,
|
| 81 |
+
"outputs": [],
|
| 82 |
+
"source": [
|
| 83 |
+
"from huggingface_hub import notebook_login\n",
|
| 84 |
+
"\n",
|
| 85 |
+
"print('Hub 업로드가 필요할 때만 아래 줄을 직접 실행하세요.')\n",
|
| 86 |
+
"# notebook_login()\n"
|
| 87 |
+
]
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"cell_type": "markdown",
|
| 91 |
+
"metadata": {},
|
| 92 |
+
"source": [
|
| 93 |
+
"## 3) 라이브러리 로드와 환경 확인\n",
|
| 94 |
+
"\n",
|
| 95 |
+
"GPU 연결과 `unsloth` 설치 여부를 먼저 확인한 뒤 라이브러리를 로드한다.\n"
|
| 96 |
+
]
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"cell_type": "code",
|
| 100 |
+
"metadata": {},
|
| 101 |
+
"execution_count": null,
|
| 102 |
+
"outputs": [],
|
| 103 |
+
"source": [
|
| 104 |
+
"import importlib.util\n",
|
| 105 |
+
"import os\n",
|
| 106 |
+
"from pathlib import Path\n",
|
| 107 |
+
"\n",
|
| 108 |
+
"import torch\n",
|
| 109 |
+
"\n",
|
| 110 |
+
"if not torch.cuda.is_available():\n",
|
| 111 |
+
" raise RuntimeError(\n",
|
| 112 |
+
" 'Unsloth requires a GPU runtime in Colab. '\n",
|
| 113 |
+
" '런타임 -> 런타임 유형 변경 -> GPU 로 바꾼 뒤 '\n",
|
| 114 |
+
" '세션에 다시 연결하고 설치 셀부터 다시 실행하세요.'\n",
|
| 115 |
+
" )\n",
|
| 116 |
+
"\n",
|
| 117 |
+
"if importlib.util.find_spec('unsloth') is None:\n",
|
| 118 |
+
" raise RuntimeError(\n",
|
| 119 |
+
" 'Unsloth is not installed in this session. '\n",
|
| 120 |
+
" '1) Unsloth 설치 셀을 다시 실행한 뒤 이 셀을 다시 실행하세요.'\n",
|
| 121 |
+
" )\n",
|
| 122 |
+
"\n",
|
| 123 |
+
"import unsloth # GPU 확인 후 Unsloth 임포트\n",
|
| 124 |
+
"import datasets\n",
|
| 125 |
+
"import transformers\n",
|
| 126 |
+
"import trl\n",
|
| 127 |
+
"from datasets import load_dataset\n",
|
| 128 |
+
"from transformers import TextStreamer\n",
|
| 129 |
+
"from trl import SFTConfig, SFTTrainer\n",
|
| 130 |
+
"from unsloth import FastModel\n",
|
| 131 |
+
"from unsloth.chat_templates import get_chat_template\n",
|
| 132 |
+
"\n",
|
| 133 |
+
"print('unsloth:', getattr(unsloth, '__version__', 'unknown'))\n",
|
| 134 |
+
"print('transformers:', transformers.__version__)\n",
|
| 135 |
+
"print('trl:', trl.__version__)\n",
|
| 136 |
+
"print('datasets:', datasets.__version__)\n",
|
| 137 |
+
"print('CUDA available:', torch.cuda.is_available())\n",
|
| 138 |
+
"print('GPU:', torch.cuda.get_device_name(0))\n",
|
| 139 |
+
"print('Capability:', torch.cuda.get_device_capability(0))\n"
|
| 140 |
+
]
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"cell_type": "markdown",
|
| 144 |
+
"metadata": {},
|
| 145 |
+
"source": [
|
| 146 |
+
"## 4) Drive 마운트와 설정\n",
|
| 147 |
+
"\n",
|
| 148 |
+
"처음부터 Google Drive 경로를 쓰도록 잡아두면 런타임이 끊겨도 dataset, LoRA 출력, GGUF 파일, checkpoint 파일이 남는다. GPU 메모리 상태는 날아가지만, 저장된 checkpoint 에서 다시 이어갈 수 있다.\n"
|
| 149 |
+
]
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"cell_type": "code",
|
| 153 |
+
"metadata": {},
|
| 154 |
+
"execution_count": null,
|
| 155 |
+
"outputs": [],
|
| 156 |
+
"source": [
|
| 157 |
+
"from google.colab import drive\n",
|
| 158 |
+
"\n",
|
| 159 |
+
"drive.mount('/content/drive')\n",
|
| 160 |
+
"\n",
|
| 161 |
+
"BASE_DIR = '/content/drive/MyDrive/gemma-mud-colab-output'\n",
|
| 162 |
+
"DATA_DIR = '/content/drive/MyDrive/gemma-mud-datasets'\n",
|
| 163 |
+
"\n",
|
| 164 |
+
"MODEL_NAME = 'unsloth/gemma-4-E2B-it'\n",
|
| 165 |
+
"MAX_SEQ_LENGTH = 1024\n",
|
| 166 |
+
"LOAD_IN_4BIT = False # T4 메모리가 부족하면 True 로 바꾼다\n",
|
| 167 |
+
"FULL_FINETUNING = False\n",
|
| 168 |
+
"OUTPUT_DIR = f'{BASE_DIR}/gemma4_e2b_mud_lora_out'\n",
|
| 169 |
+
"GGUF_DIR = f'{BASE_DIR}/gemma4_e2b_mud_gguf'\n",
|
| 170 |
+
"GGUF_QUANTIZATION = 'q4_k_m'\n",
|
| 171 |
+
"\n",
|
| 172 |
+
"PER_DEVICE_TRAIN_BATCH_SIZE = 1\n",
|
| 173 |
+
"GRADIENT_ACCUMULATION_STEPS = 4\n",
|
| 174 |
+
"NUM_TRAIN_EPOCHS = 4\n",
|
| 175 |
+
"LEARNING_RATE = 8e-5\n",
|
| 176 |
+
"WARMUP_STEPS = 5\n",
|
| 177 |
+
"SEED = 3407\n",
|
| 178 |
+
"\n",
|
| 179 |
+
"LORA_R = 16\n",
|
| 180 |
+
"LORA_ALPHA = 16\n",
|
| 181 |
+
"LORA_DROPOUT = 0\n",
|
| 182 |
+
"\n",
|
| 183 |
+
"DEFAULT_SYSTEM_PROMPT = (\n",
|
| 184 |
+
" '당신은 우주항행 텍스트 MUD의 항로 안내자이자 세계관 해설자다. '\n",
|
| 185 |
+
" '답변은 한국어로 하며, 서정성과 실용성을 함께 지닌다. '\n",
|
| 186 |
+
" '플레이에 도움이 되는 정보와 분위기 묘사를 함께 준다.'\n",
|
| 187 |
+
")\n",
|
| 188 |
+
"\n",
|
| 189 |
+
"TEST_PROMPTS = [\n",
|
| 190 |
+
" 'First Fire Horizon이 어떤 곳인지 설명해줘.',\n",
|
| 191 |
+
" 'First Fire Horizon의 분위기와 역할을 설명해줘.',\n",
|
| 192 |
+
" 'Helios Verge이 어떤 곳인지 설명해줘.',\n",
|
| 193 |
+
" 'Helios Verge의 의미와 중요성을 설명해줘.',\n",
|
| 194 |
+
"]\n",
|
| 195 |
+
"\n",
|
| 196 |
+
"DATA_CANDIDATES = [\n",
|
| 197 |
+
" f'{DATA_DIR}/combined_1000.unsloth_chatml_dedup.jsonl',\n",
|
| 198 |
+
" f'{DATA_DIR}/combined_1000.unsloth_gemma4_messages_dedup.jsonl',\n",
|
| 199 |
+
" f'{DATA_DIR}/combined_1000.jsonl',\n",
|
| 200 |
+
" '/content/gemma4_mud_alpaca_100.jsonl',\n",
|
| 201 |
+
" '/content/dataset/gemma4_mud_alpaca_100.jsonl',\n",
|
| 202 |
+
" '/content/drive/MyDrive/combined_1000.unsloth_chatml_dedup.jsonl',\n",
|
| 203 |
+
" '/content/drive/MyDrive/combined_1000.unsloth_gemma4_messages_dedup.jsonl',\n",
|
| 204 |
+
" '/content/drive/MyDrive/combined_1000.jsonl',\n",
|
| 205 |
+
" '/content/gemma-mud-colab-starter/dataset/gemma4_mud_alpaca_100.jsonl',\n",
|
| 206 |
+
" '/content/drive/MyDrive/gemma-mud-colab-starter/dataset/gemma4_mud_alpaca_100.jsonl',\n",
|
| 207 |
+
"]\n",
|
| 208 |
+
"\n",
|
| 209 |
+
"os.makedirs(BASE_DIR, exist_ok=True)\n",
|
| 210 |
+
"os.makedirs(DATA_DIR, exist_ok=True)\n",
|
| 211 |
+
"os.makedirs(OUTPUT_DIR, exist_ok=True)\n",
|
| 212 |
+
"os.makedirs(GGUF_DIR, exist_ok=True)\n",
|
| 213 |
+
"\n",
|
| 214 |
+
"print('BASE_DIR:', BASE_DIR)\n",
|
| 215 |
+
"print('DATA_DIR:', DATA_DIR)\n",
|
| 216 |
+
"print('OUTPUT_DIR:', OUTPUT_DIR)\n",
|
| 217 |
+
"print('GGUF_DIR:', GGUF_DIR)\n"
|
| 218 |
+
]
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
"cell_type": "markdown",
|
| 222 |
+
"metadata": {},
|
| 223 |
+
"source": [
|
| 224 |
+
"## 5) dataset 경로 찾기\n",
|
| 225 |
+
"\n",
|
| 226 |
+
"가장 먼저 발견되는 dataset 파일을 사용한다. 가장 안전한 경로는 `DATA_DIR` 아래에 dataset 을 올려두는 방식이다.\n"
|
| 227 |
+
]
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"cell_type": "code",
|
| 231 |
+
"metadata": {},
|
| 232 |
+
"execution_count": null,
|
| 233 |
+
"outputs": [],
|
| 234 |
+
"source": [
|
| 235 |
+
"def resolve_data_file(candidates):\n",
|
| 236 |
+
" for path in candidates:\n",
|
| 237 |
+
" if os.path.exists(path):\n",
|
| 238 |
+
" return path\n",
|
| 239 |
+
" raise FileNotFoundError(\n",
|
| 240 |
+
" 'dataset 파일을 찾지 못했습니다. DATA_CANDIDATES를 확인하거나 '\n",
|
| 241 |
+
" 'combined_1000.unsloth_chatml_dedup.jsonl, '\n",
|
| 242 |
+
" 'combined_1000.unsloth_gemma4_messages_dedup.jsonl, '\n",
|
| 243 |
+
" 'combined_1000.jsonl 중 하나를 DATA_DIR 또는 /content/ 에 올려주세요.'\n",
|
| 244 |
+
" )\n",
|
| 245 |
+
"\n",
|
| 246 |
+
"\n",
|
| 247 |
+
"DATA_FILE = resolve_data_file(DATA_CANDIDATES)\n",
|
| 248 |
+
"print('사용할 dataset:', DATA_FILE)\n"
|
| 249 |
+
]
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
"cell_type": "markdown",
|
| 253 |
+
"metadata": {},
|
| 254 |
+
"source": [
|
| 255 |
+
"## 6) Unsloth Gemma 4 E2B 모델 로드\n",
|
| 256 |
+
"\n",
|
| 257 |
+
"텍스트 LoRA 경로로 로드한다. 추론과 export 안정성을 위해 Gemma 4 chat template 를 맞춘다.\n"
|
| 258 |
+
]
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
"cell_type": "code",
|
| 262 |
+
"metadata": {},
|
| 263 |
+
"execution_count": null,
|
| 264 |
+
"outputs": [],
|
| 265 |
+
"source": [
|
| 266 |
+
"model, tokenizer = FastModel.from_pretrained(\n",
|
| 267 |
+
" model_name = MODEL_NAME,\n",
|
| 268 |
+
" dtype = None,\n",
|
| 269 |
+
" max_seq_length = MAX_SEQ_LENGTH,\n",
|
| 270 |
+
" load_in_4bit = LOAD_IN_4BIT,\n",
|
| 271 |
+
" full_finetuning = FULL_FINETUNING,\n",
|
| 272 |
+
" # token = 'hf_...',\n",
|
| 273 |
+
")\n",
|
| 274 |
+
"\n",
|
| 275 |
+
"tokenizer = get_chat_template(\n",
|
| 276 |
+
" tokenizer,\n",
|
| 277 |
+
" chat_template = 'gemma-4',\n",
|
| 278 |
+
")\n",
|
| 279 |
+
"\n",
|
| 280 |
+
"print('모델 로드 완료:', MODEL_NAME)\n"
|
| 281 |
+
]
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
"cell_type": "markdown",
|
| 285 |
+
"metadata": {},
|
| 286 |
+
"source": [
|
| 287 |
+
"## 7) LoRA 어댑터 부착\n",
|
| 288 |
+
"\n",
|
| 289 |
+
"작은 데이터셋에서 반영이 더 잘 보이도록 `r=16`, `lora_alpha=16`, `use_gradient_checkpointing='unsloth'` 를 기본값으로 둔다.\n"
|
| 290 |
+
]
|
| 291 |
+
},
|
| 292 |
+
{
|
| 293 |
+
"cell_type": "code",
|
| 294 |
+
"metadata": {},
|
| 295 |
+
"execution_count": null,
|
| 296 |
+
"outputs": [],
|
| 297 |
+
"source": [
|
| 298 |
+
"model = FastModel.get_peft_model(\n",
|
| 299 |
+
" model,\n",
|
| 300 |
+
" finetune_vision_layers = False,\n",
|
| 301 |
+
" finetune_language_layers = True,\n",
|
| 302 |
+
" finetune_attention_modules = True,\n",
|
| 303 |
+
" finetune_mlp_modules = True,\n",
|
| 304 |
+
" r = LORA_R,\n",
|
| 305 |
+
" lora_alpha = LORA_ALPHA,\n",
|
| 306 |
+
" lora_dropout = LORA_DROPOUT,\n",
|
| 307 |
+
" bias = 'none',\n",
|
| 308 |
+
" use_gradient_checkpointing = 'unsloth',\n",
|
| 309 |
+
" random_state = SEED,\n",
|
| 310 |
+
")\n"
|
| 311 |
+
]
|
| 312 |
+
},
|
| 313 |
+
{
|
| 314 |
+
"cell_type": "markdown",
|
| 315 |
+
"metadata": {},
|
| 316 |
+
"source": [
|
| 317 |
+
"## 8) dataset 로드와 Gemma 4 채팅 포맷 변환\n",
|
| 318 |
+
"\n",
|
| 319 |
+
"`messages` 형식은 그대로 살리고, `instruction/input/output` 형식은 표준 `system/user/assistant` 대화로 바꾼 뒤 Gemma 4 템플릿으로 `text` 컬럼을 만든다.\n"
|
| 320 |
+
]
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
"cell_type": "code",
|
| 324 |
+
"metadata": {},
|
| 325 |
+
"execution_count": null,
|
| 326 |
+
"outputs": [],
|
| 327 |
+
"source": [
|
| 328 |
+
"raw_dataset = load_dataset('json', data_files=DATA_FILE, split='train')\n",
|
| 329 |
+
"print(raw_dataset)\n",
|
| 330 |
+
"print(raw_dataset[0])\n",
|
| 331 |
+
"\n",
|
| 332 |
+
"\n",
|
| 333 |
+
"def build_user_text(example):\n",
|
| 334 |
+
" instruction = (example.get('instruction') or '').strip()\n",
|
| 335 |
+
" extra_input = (example.get('input') or '').strip()\n",
|
| 336 |
+
" if extra_input:\n",
|
| 337 |
+
" return instruction + \"\\n\\n\" + extra_input\n",
|
| 338 |
+
" return instruction\n",
|
| 339 |
+
"\n",
|
| 340 |
+
"\n",
|
| 341 |
+
"def normalize_message_content(content):\n",
|
| 342 |
+
" if isinstance(content, str):\n",
|
| 343 |
+
" text = content.strip()\n",
|
| 344 |
+
" if text:\n",
|
| 345 |
+
" return text\n",
|
| 346 |
+
" raise ValueError('빈 문자열 content는 허용하지 않습니다.')\n",
|
| 347 |
+
"\n",
|
| 348 |
+
" if isinstance(content, dict):\n",
|
| 349 |
+
" content = [content]\n",
|
| 350 |
+
"\n",
|
| 351 |
+
" if isinstance(content, list):\n",
|
| 352 |
+
" parts = []\n",
|
| 353 |
+
" for item in content:\n",
|
| 354 |
+
" if not isinstance(item, dict):\n",
|
| 355 |
+
" raise ValueError(f'지원하지 않는 content item 형식: {type(item)}')\n",
|
| 356 |
+
" item_type = item.get('type', 'text')\n",
|
| 357 |
+
" if item_type != 'text':\n",
|
| 358 |
+
" raise ValueError(f'텍스트 전용 학습에서는 type={item_type!r} 를 사용할 수 없습니다.')\n",
|
| 359 |
+
" text = str(item.get('text', '')).strip()\n",
|
| 360 |
+
" if text:\n",
|
| 361 |
+
" parts.append(text)\n",
|
| 362 |
+
" if parts:\n",
|
| 363 |
+
" return \"\\n\".join(parts)\n",
|
| 364 |
+
"\n",
|
| 365 |
+
" raise ValueError(f'지원하지 않는 content 형식: {type(content)}')\n",
|
| 366 |
+
"\n",
|
| 367 |
+
"\n",
|
| 368 |
+
"def format_for_gemma4(example):\n",
|
| 369 |
+
" if 'messages' in example and example['messages']:\n",
|
| 370 |
+
" messages = []\n",
|
| 371 |
+
" for message in example['messages']:\n",
|
| 372 |
+
" messages.append(\n",
|
| 373 |
+
" {\n",
|
| 374 |
+
" 'role': message['role'],\n",
|
| 375 |
+
" 'content': normalize_message_content(message['content']),\n",
|
| 376 |
+
" }\n",
|
| 377 |
+
" )\n",
|
| 378 |
+
" else:\n",
|
| 379 |
+
" required_fields = ['instruction', 'output']\n",
|
| 380 |
+
" for field in required_fields:\n",
|
| 381 |
+
" if field not in example:\n",
|
| 382 |
+
" raise ValueError(f'필드 누락: {field}')\n",
|
| 383 |
+
" messages = [\n",
|
| 384 |
+
" {\n",
|
| 385 |
+
" 'role': 'system',\n",
|
| 386 |
+
" 'content': DEFAULT_SYSTEM_PROMPT,\n",
|
| 387 |
+
" },\n",
|
| 388 |
+
" {\n",
|
| 389 |
+
" 'role': 'user',\n",
|
| 390 |
+
" 'content': build_user_text(example),\n",
|
| 391 |
+
" },\n",
|
| 392 |
+
" {\n",
|
| 393 |
+
" 'role': 'assistant',\n",
|
| 394 |
+
" 'content': str(example['output']).strip(),\n",
|
| 395 |
+
" },\n",
|
| 396 |
+
" ]\n",
|
| 397 |
+
"\n",
|
| 398 |
+
" text = tokenizer.apply_chat_template(\n",
|
| 399 |
+
" messages,\n",
|
| 400 |
+
" tokenize = False,\n",
|
| 401 |
+
" add_generation_prompt = False,\n",
|
| 402 |
+
" )\n",
|
| 403 |
+
" return {'text': text}\n",
|
| 404 |
+
"\n",
|
| 405 |
+
"\n",
|
| 406 |
+
"train_dataset = raw_dataset.map(\n",
|
| 407 |
+
" format_for_gemma4,\n",
|
| 408 |
+
" remove_columns = raw_dataset.column_names,\n",
|
| 409 |
+
")\n",
|
| 410 |
+
"\n",
|
| 411 |
+
"inference_system_prompt = DEFAULT_SYSTEM_PROMPT\n",
|
| 412 |
+
"if 'messages' in raw_dataset.column_names:\n",
|
| 413 |
+
" first_messages = raw_dataset[0].get('messages') or []\n",
|
| 414 |
+
" for message in first_messages:\n",
|
| 415 |
+
" if message.get('role') == 'system':\n",
|
| 416 |
+
" inference_system_prompt = normalize_message_content(message.get('content'))\n",
|
| 417 |
+
" break\n",
|
| 418 |
+
"\n",
|
| 419 |
+
"print('원본 컬럼:', raw_dataset.column_names)\n",
|
| 420 |
+
"print('추론용 system prompt:', inference_system_prompt)\n",
|
| 421 |
+
"print(train_dataset[0]['text'][:800])\n"
|
| 422 |
+
]
|
| 423 |
+
},
|
| 424 |
+
{
|
| 425 |
+
"cell_type": "markdown",
|
| 426 |
+
"metadata": {},
|
| 427 |
+
"source": [
|
| 428 |
+
"## 9) Unsloth SFTTrainer 설정\n",
|
| 429 |
+
"\n",
|
| 430 |
+
"현재 Gemma 4 템플릿 경로에서는 `assistant_only_loss = False` 로 두고 전체 대화 텍스트를 학습한다. 대신 epoch 를 조금 더 주고 learning rate 를 낮춰서 반영 강도를 맞춘다. 런타임 종료에 대비해 중간 checkpoint 도 Drive에 저장한다.\n"
|
| 431 |
+
]
|
| 432 |
+
},
|
| 433 |
+
{
|
| 434 |
+
"cell_type": "code",
|
| 435 |
+
"metadata": {},
|
| 436 |
+
"execution_count": null,
|
| 437 |
+
"outputs": [],
|
| 438 |
+
"source": [
|
| 439 |
+
"trainer = SFTTrainer(\n",
|
| 440 |
+
" model = model,\n",
|
| 441 |
+
" tokenizer = tokenizer,\n",
|
| 442 |
+
" train_dataset = train_dataset,\n",
|
| 443 |
+
" eval_dataset = None,\n",
|
| 444 |
+
" args = SFTConfig(\n",
|
| 445 |
+
" output_dir = OUTPUT_DIR,\n",
|
| 446 |
+
" dataset_text_field = 'text',\n",
|
| 447 |
+
" per_device_train_batch_size = PER_DEVICE_TRAIN_BATCH_SIZE,\n",
|
| 448 |
+
" gradient_accumulation_steps = GRADIENT_ACCUMULATION_STEPS,\n",
|
| 449 |
+
" warmup_steps = WARMUP_STEPS,\n",
|
| 450 |
+
" num_train_epochs = NUM_TRAIN_EPOCHS,\n",
|
| 451 |
+
" learning_rate = LEARNING_RATE,\n",
|
| 452 |
+
" logging_steps = 5,\n",
|
| 453 |
+
" save_strategy = 'steps',\n",
|
| 454 |
+
" save_steps = 50,\n",
|
| 455 |
+
" save_total_limit = 2,\n",
|
| 456 |
+
" optim = 'adamw_8bit',\n",
|
| 457 |
+
" weight_decay = 0.001,\n",
|
| 458 |
+
" lr_scheduler_type = 'linear',\n",
|
| 459 |
+
" seed = SEED,\n",
|
| 460 |
+
" report_to = 'none',\n",
|
| 461 |
+
" assistant_only_loss = False,\n",
|
| 462 |
+
" packing = False,\n",
|
| 463 |
+
" ),\n",
|
| 464 |
+
")\n"
|
| 465 |
+
]
|
| 466 |
+
},
|
| 467 |
+
{
|
| 468 |
+
"cell_type": "markdown",
|
| 469 |
+
"metadata": {},
|
| 470 |
+
"source": [
|
| 471 |
+
"## 10) 학습 시작\n",
|
| 472 |
+
"\n",
|
| 473 |
+
"학습 중 loss 가 step마다 조금씩 흔들리는 건 정상이다. `nan` 이나 `inf` 가 아닌지만 보면 된다. 이미 저장된 checkpoint 가 있으면 가장 최근 checkpoint 에서 자동으로 이어간다.\n"
|
| 474 |
+
]
|
| 475 |
+
},
|
| 476 |
+
{
|
| 477 |
+
"cell_type": "code",
|
| 478 |
+
"metadata": {},
|
| 479 |
+
"execution_count": null,
|
| 480 |
+
"outputs": [],
|
| 481 |
+
"source": [
|
| 482 |
+
"from glob import glob\n",
|
| 483 |
+
"\n",
|
| 484 |
+
"checkpoints = sorted(glob(f'{OUTPUT_DIR}/checkpoint-*'))\n",
|
| 485 |
+
"if checkpoints:\n",
|
| 486 |
+
" print('체크포인트에서 재개:', checkpoints[-1])\n",
|
| 487 |
+
" trainer_stats = trainer.train(resume_from_checkpoint = True)\n",
|
| 488 |
+
"else:\n",
|
| 489 |
+
" print('새 학습 시작')\n",
|
| 490 |
+
" trainer_stats = trainer.train()\n",
|
| 491 |
+
"\n",
|
| 492 |
+
"print(trainer_stats)\n"
|
| 493 |
+
]
|
| 494 |
+
},
|
| 495 |
+
{
|
| 496 |
+
"cell_type": "markdown",
|
| 497 |
+
"metadata": {},
|
| 498 |
+
"source": [
|
| 499 |
+
"## 11) LoRA 어댑터 저장\n",
|
| 500 |
+
"\n",
|
| 501 |
+
"여기서는 먼저 LoRA 어댑터와 tokenizer 를 저장한다. LM Studio 용 GGUF 는 뒤 셀에서 만든다.\n"
|
| 502 |
+
]
|
| 503 |
+
},
|
| 504 |
+
{
|
| 505 |
+
"cell_type": "code",
|
| 506 |
+
"metadata": {},
|
| 507 |
+
"execution_count": null,
|
| 508 |
+
"outputs": [],
|
| 509 |
+
"source": [
|
| 510 |
+
"model.save_pretrained(OUTPUT_DIR)\n",
|
| 511 |
+
"tokenizer.save_pretrained(OUTPUT_DIR)\n",
|
| 512 |
+
"\n",
|
| 513 |
+
"print('저장 위치:', OUTPUT_DIR)\n",
|
| 514 |
+
"print('저장 파일:', os.listdir(OUTPUT_DIR)[:20])\n"
|
| 515 |
+
]
|
| 516 |
+
},
|
| 517 |
+
{
|
| 518 |
+
"cell_type": "markdown",
|
| 519 |
+
"metadata": {},
|
| 520 |
+
"source": [
|
| 521 |
+
"## 12) 간단 추론 테스트\n",
|
| 522 |
+
"\n",
|
| 523 |
+
"학습이 실제로 반영됐는지 보려면 데이터셋 원문과 가까운 질문을 greedy decoding 으로 먼저 확인한다. Gemma 4 processor 와 chat template 충돌을 피하려고, 추론은 먼저 문자열 프롬프트를 렌더링한 뒤 토크나이즈하는 2단계 방식으로 처리한다.\n"
|
| 524 |
+
]
|
| 525 |
+
},
|
| 526 |
+
{
|
| 527 |
+
"cell_type": "code",
|
| 528 |
+
"metadata": {},
|
| 529 |
+
"execution_count": null,
|
| 530 |
+
"outputs": [],
|
| 531 |
+
"source": [
|
| 532 |
+
"FastModel.for_inference(model)\n",
|
| 533 |
+
"\n",
|
| 534 |
+
"text_tokenizer = tokenizer.tokenizer if hasattr(tokenizer, 'tokenizer') else tokenizer\n",
|
| 535 |
+
"\n",
|
| 536 |
+
"\n",
|
| 537 |
+
"def render_text_prompt(system_prompt, user_prompt):\n",
|
| 538 |
+
" messages = []\n",
|
| 539 |
+
" if system_prompt:\n",
|
| 540 |
+
" messages.append(\n",
|
| 541 |
+
" {\n",
|
| 542 |
+
" 'role': 'system',\n",
|
| 543 |
+
" 'content': system_prompt,\n",
|
| 544 |
+
" }\n",
|
| 545 |
+
" )\n",
|
| 546 |
+
" messages.append(\n",
|
| 547 |
+
" {\n",
|
| 548 |
+
" 'role': 'user',\n",
|
| 549 |
+
" 'content': user_prompt,\n",
|
| 550 |
+
" }\n",
|
| 551 |
+
" )\n",
|
| 552 |
+
" return text_tokenizer.apply_chat_template(\n",
|
| 553 |
+
" messages,\n",
|
| 554 |
+
" tokenize = False,\n",
|
| 555 |
+
" add_generation_prompt = True,\n",
|
| 556 |
+
" )\n",
|
| 557 |
+
"\n",
|
| 558 |
+
"\n",
|
| 559 |
+
"for prompt_text in TEST_PROMPTS:\n",
|
| 560 |
+
" print('=' * 100)\n",
|
| 561 |
+
" print('USER:', prompt_text)\n",
|
| 562 |
+
"\n",
|
| 563 |
+
" prompt = render_text_prompt(inference_system_prompt, prompt_text)\n",
|
| 564 |
+
" inputs = tokenizer(\n",
|
| 565 |
+
" text = prompt,\n",
|
| 566 |
+
" return_tensors = 'pt',\n",
|
| 567 |
+
" add_special_tokens = False,\n",
|
| 568 |
+
" ).to('cuda')\n",
|
| 569 |
+
"\n",
|
| 570 |
+
" prompt_len = inputs['input_ids'].shape[1]\n",
|
| 571 |
+
"\n",
|
| 572 |
+
" with torch.no_grad():\n",
|
| 573 |
+
" outputs = model.generate(\n",
|
| 574 |
+
" **inputs,\n",
|
| 575 |
+
" max_new_tokens = 180,\n",
|
| 576 |
+
" do_sample = False,\n",
|
| 577 |
+
" use_cache = True,\n",
|
| 578 |
+
" )\n",
|
| 579 |
+
"\n",
|
| 580 |
+
" answer = text_tokenizer.decode(\n",
|
| 581 |
+
" outputs[0][prompt_len:],\n",
|
| 582 |
+
" skip_special_tokens = True,\n",
|
| 583 |
+
" ).split('<turn|>')[0].strip()\n",
|
| 584 |
+
"\n",
|
| 585 |
+
" print('ASSISTANT:', answer)\n",
|
| 586 |
+
" print()\n"
|
| 587 |
+
]
|
| 588 |
+
},
|
| 589 |
+
{
|
| 590 |
+
"cell_type": "markdown",
|
| 591 |
+
"metadata": {},
|
| 592 |
+
"source": [
|
| 593 |
+
"## 13) GGUF 생성\n",
|
| 594 |
+
"\n",
|
| 595 |
+
"LM Studio 용으로는 `.gguf` 파일만 있으면 된다. 먼저 `q4_k_m` 하나를 만들어보는 흐름으로 둔다.\n"
|
| 596 |
+
]
|
| 597 |
+
},
|
| 598 |
+
{
|
| 599 |
+
"cell_type": "code",
|
| 600 |
+
"metadata": {},
|
| 601 |
+
"execution_count": null,
|
| 602 |
+
"outputs": [],
|
| 603 |
+
"source": [
|
| 604 |
+
"from pathlib import Path\n",
|
| 605 |
+
"\n",
|
| 606 |
+
"GGUF_DIR_PATH = Path(GGUF_DIR)\n",
|
| 607 |
+
"GGUF_DIR_PATH.mkdir(parents = True, exist_ok = True)\n",
|
| 608 |
+
"\n",
|
| 609 |
+
"model.save_pretrained_gguf(\n",
|
| 610 |
+
" str(GGUF_DIR_PATH),\n",
|
| 611 |
+
" tokenizer,\n",
|
| 612 |
+
" quantization_method = GGUF_QUANTIZATION,\n",
|
| 613 |
+
" maximum_memory_usage = 0.5,\n",
|
| 614 |
+
")\n",
|
| 615 |
+
"\n",
|
| 616 |
+
"gguf_files = sorted(GGUF_DIR_PATH.glob('*.gguf'))\n",
|
| 617 |
+
"if not gguf_files:\n",
|
| 618 |
+
" raise RuntimeError('GGUF 파일이 생성되지 않았습니다.')\n",
|
| 619 |
+
"\n",
|
| 620 |
+
"print('생성된 GGUF:')\n",
|
| 621 |
+
"for path in gguf_files:\n",
|
| 622 |
+
" print('-', path, f'({path.stat().st_size / 1024**3:.2f} GB)')\n"
|
| 623 |
+
]
|
| 624 |
+
},
|
| 625 |
+
{
|
| 626 |
+
"cell_type": "markdown",
|
| 627 |
+
"metadata": {},
|
| 628 |
+
"source": [
|
| 629 |
+
"## 14) Drive 경로 확인 + 브라우저 다운로드\n",
|
| 630 |
+
"\n",
|
| 631 |
+
"이 노트북은 처음부터 Drive에 저장하므로, 여기서는 생성된 GGUF 경로를 확인하고 필요할 때만 브라우저 다운로드를 건다.\n"
|
| 632 |
+
]
|
| 633 |
+
},
|
| 634 |
+
{
|
| 635 |
+
"cell_type": "code",
|
| 636 |
+
"metadata": {},
|
| 637 |
+
"execution_count": null,
|
| 638 |
+
"outputs": [],
|
| 639 |
+
"source": [
|
| 640 |
+
"from google.colab import files\n",
|
| 641 |
+
"\n",
|
| 642 |
+
"print('Drive 저장 경로:')\n",
|
| 643 |
+
"for path in gguf_files:\n",
|
| 644 |
+
" print('-', path)\n",
|
| 645 |
+
"\n",
|
| 646 |
+
"for path in gguf_files:\n",
|
| 647 |
+
" print('다운로드 시작:', path.name)\n",
|
| 648 |
+
" files.download(str(path))\n"
|
| 649 |
+
]
|
| 650 |
+
},
|
| 651 |
+
{
|
| 652 |
+
"cell_type": "markdown",
|
| 653 |
+
"metadata": {},
|
| 654 |
+
"source": [
|
| 655 |
+
"## 15) 자주 흔들리는 지점\n",
|
| 656 |
+
"\n",
|
| 657 |
+
"- 설치 직후 import 오류가 나면 런타임을 재시작한 뒤 다시 실행\n",
|
| 658 |
+
"- dataset 파일은 가능하면 `DATA_DIR` 에 올리고, 출력은 기본값 그대로 Drive 경로를 유지\n",
|
| 659 |
+
"- VRAM 이 부족하면 `LOAD_IN_4BIT = True` 또는 `MAX_SEQ_LENGTH = 512` 로 조정\n",
|
| 660 |
+
"- `messages` dataset 으로 학습했다면 추론 때도 system prompt 를 같이 넣는 편이 더 안정적\n",
|
| 661 |
+
"- Gemma 4는 chat template 일치가 중요하므로, 추론 때도 Gemma 4 템플릿을 그대로 사용\n",
|
| 662 |
+
"- GGUF 변환 중 메모리 부족이면 `maximum_memory_usage` 를 더 낮춘다\n",
|
| 663 |
+
"- 런타임이 끊겨도 Drive 안의 checkpoint 는 남으므로 다시 연결한 뒤 학습 셀을 다시 실행하면 이어갈 수 있다\n",
|
| 664 |
+
"- 작은 LoRA는 base 모델 성향을 완전히 지우는 게 아니라 덧입히는 방식이라, 테스트도 데이터셋 원문과 가까운 질문부터 보는 게 좋다\n"
|
| 665 |
+
]
|
| 666 |
+
}
|
| 667 |
+
],
|
| 668 |
+
"metadata": {
|
| 669 |
+
"colab": {
|
| 670 |
+
"gpuType": "T4",
|
| 671 |
+
"provenance": []
|
| 672 |
+
},
|
| 673 |
+
"kernelspec": {
|
| 674 |
+
"display_name": "Python 3 (ipykernel)",
|
| 675 |
+
"language": "python",
|
| 676 |
+
"name": "python3"
|
| 677 |
+
},
|
| 678 |
+
"language_info": {
|
| 679 |
+
"codemirror_mode": {
|
| 680 |
+
"name": "ipython",
|
| 681 |
+
"version": 3
|
| 682 |
+
},
|
| 683 |
+
"file_extension": ".py",
|
| 684 |
+
"mimetype": "text/x-python",
|
| 685 |
+
"name": "python",
|
| 686 |
+
"nbconvert_exporter": "python",
|
| 687 |
+
"pygments_lexer": "ipython3",
|
| 688 |
+
"version": "3.12.13"
|
| 689 |
+
}
|
| 690 |
+
},
|
| 691 |
+
"nbformat": 4,
|
| 692 |
+
"nbformat_minor": 5
|
| 693 |
+
}
|
gemma-mud-colab-starter/notebooks/Gemma4_MUD_QLoRA_Colab_Notebook_Cells.md
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gemma 4 E2B MUD Unsloth Colab Notebook
|
| 2 |
+
|
| 3 |
+
Gemma 4의 작은 모델은 공식적으로 `E2B` 라는 이름을 쓴다. 이 노트북은 `unsloth/gemma-4-E2B-it` 기준의 텍스트 LoRA 학습용이며, `messages` 형식과 예전 `instruction/input/output` 형식을 모두 읽을 수 있게 정리했다.
|
| 4 |
+
|
| 5 |
+
## 1) Unsloth 설치
|
| 6 |
+
|
| 7 |
+
설치 로그를 숨기지 않는다. 설치 직후 `ModuleNotFoundError: No module named 'unsloth'` 가 나면 런타임을 다시 시작한 뒤 1번 셀부터 다시 실행한다.
|
| 8 |
+
|
| 9 |
+
```python
|
| 10 |
+
import importlib.util
|
| 11 |
+
import os
|
| 12 |
+
import re
|
| 13 |
+
import subprocess
|
| 14 |
+
import sys
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def run(cmd):
|
| 18 |
+
print('$', ' '.join(cmd))
|
| 19 |
+
subprocess.check_call(cmd)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
if 'COLAB_' not in ''.join(os.environ.keys()):
|
| 23 |
+
run([sys.executable, '-m', 'pip', 'install', '-U', 'unsloth', 'unsloth_zoo'])
|
| 24 |
+
else:
|
| 25 |
+
import torch
|
| 26 |
+
|
| 27 |
+
version_match = re.match(r'[\d]+\.[\d]+', str(torch.__version__))
|
| 28 |
+
torch_version = version_match.group(0) if version_match else '2.10'
|
| 29 |
+
xformers = {
|
| 30 |
+
'2.10': 'xformers==0.0.34',
|
| 31 |
+
'2.9': 'xformers==0.0.33.post1',
|
| 32 |
+
'2.8': 'xformers==0.0.32.post2',
|
| 33 |
+
}.get(torch_version, 'xformers==0.0.34')
|
| 34 |
+
|
| 35 |
+
run([sys.executable, '-m', 'pip', 'install', 'sentencepiece', 'protobuf', 'datasets==4.3.0', 'huggingface_hub>=0.34.0', 'hf_transfer'])
|
| 36 |
+
run([sys.executable, '-m', 'pip', 'install', '--no-deps', 'unsloth_zoo', 'bitsandbytes', 'accelerate', xformers, 'peft', 'trl', 'triton', 'unsloth'])
|
| 37 |
+
|
| 38 |
+
run([sys.executable, '-m', 'pip', 'install', '--no-deps', 'transformers==5.5.0'])
|
| 39 |
+
run([sys.executable, '-m', 'pip', 'install', 'torchcodec'])
|
| 40 |
+
run([sys.executable, '-m', 'pip', 'install', '--no-deps', '--upgrade', 'timm'])
|
| 41 |
+
run([sys.executable, '-m', 'pip', 'show', 'unsloth'])
|
| 42 |
+
run([sys.executable, '-m', 'pip', 'show', 'unsloth_zoo'])
|
| 43 |
+
|
| 44 |
+
print('unsloth installed:', importlib.util.find_spec('unsloth') is not None)
|
| 45 |
+
|
| 46 |
+
import torch
|
| 47 |
+
|
| 48 |
+
torch._dynamo.config.recompile_limit = 64
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
## 2) 선택: Hugging Face 로그인
|
| 52 |
+
|
| 53 |
+
`unsloth/gemma-4-E2B-it` 를 로컬 저장만 할 때는 대체로 로그인 없이도 충분하다. 다만 Hugging Face Hub 업로드나 gated 모델 사용 예정이면 아래 줄의 주석을 풀어 실행한다.
|
| 54 |
+
|
| 55 |
+
```python
|
| 56 |
+
from huggingface_hub import notebook_login
|
| 57 |
+
|
| 58 |
+
print('Hub 업로드가 필요할 때만 아래 줄을 직접 실행하세요.')
|
| 59 |
+
# notebook_login()
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## 3) 라이브러리 로드와 환경 확인
|
| 63 |
+
|
| 64 |
+
GPU 연결과 `unsloth` 설치 여부를 먼저 확인한 뒤 라이브러리를 로드한다.
|
| 65 |
+
|
| 66 |
+
```python
|
| 67 |
+
import importlib.util
|
| 68 |
+
import os
|
| 69 |
+
from pathlib import Path
|
| 70 |
+
|
| 71 |
+
import torch
|
| 72 |
+
|
| 73 |
+
if not torch.cuda.is_available():
|
| 74 |
+
raise RuntimeError(
|
| 75 |
+
'Unsloth requires a GPU runtime in Colab. '
|
| 76 |
+
'런타임 -> 런타임 유형 변경 -> GPU 로 바꾼 뒤 '
|
| 77 |
+
'세션에 다시 연결하고 설치 셀부터 다시 실행하세요.'
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
if importlib.util.find_spec('unsloth') is None:
|
| 81 |
+
raise RuntimeError(
|
| 82 |
+
'Unsloth is not installed in this session. '
|
| 83 |
+
'1) Unsloth 설치 셀을 다시 실행한 뒤 이 셀을 다시 실행하세요.'
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
import unsloth # GPU 확인 후 Unsloth 임포트
|
| 87 |
+
import datasets
|
| 88 |
+
import transformers
|
| 89 |
+
import trl
|
| 90 |
+
from datasets import load_dataset
|
| 91 |
+
from transformers import TextStreamer
|
| 92 |
+
from trl import SFTConfig, SFTTrainer
|
| 93 |
+
from unsloth import FastModel
|
| 94 |
+
from unsloth.chat_templates import get_chat_template
|
| 95 |
+
|
| 96 |
+
print('unsloth:', getattr(unsloth, '__version__', 'unknown'))
|
| 97 |
+
print('transformers:', transformers.__version__)
|
| 98 |
+
print('trl:', trl.__version__)
|
| 99 |
+
print('datasets:', datasets.__version__)
|
| 100 |
+
print('CUDA available:', torch.cuda.is_available())
|
| 101 |
+
print('GPU:', torch.cuda.get_device_name(0))
|
| 102 |
+
print('Capability:', torch.cuda.get_device_capability(0))
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
## 4) Drive 마운트와 설정
|
| 106 |
+
|
| 107 |
+
처음부터 Google Drive 경로를 쓰도록 잡아두면 런타임이 끊겨도 dataset, LoRA 출력, GGUF 파일, checkpoint 파일이 남는다. GPU 메모리 상태는 날아가지만, 저장된 checkpoint 에서 다시 이어갈 수 있다.
|
| 108 |
+
|
| 109 |
+
```python
|
| 110 |
+
from google.colab import drive
|
| 111 |
+
|
| 112 |
+
drive.mount('/content/drive')
|
| 113 |
+
|
| 114 |
+
BASE_DIR = '/content/drive/MyDrive/gemma-mud-colab-output'
|
| 115 |
+
DATA_DIR = '/content/drive/MyDrive/gemma-mud-datasets'
|
| 116 |
+
|
| 117 |
+
MODEL_NAME = 'unsloth/gemma-4-E2B-it'
|
| 118 |
+
MAX_SEQ_LENGTH = 1024
|
| 119 |
+
LOAD_IN_4BIT = False # T4 메모리가 부족하면 True 로 바꾼다
|
| 120 |
+
FULL_FINETUNING = False
|
| 121 |
+
OUTPUT_DIR = f'{BASE_DIR}/gemma4_e2b_mud_lora_out'
|
| 122 |
+
GGUF_DIR = f'{BASE_DIR}/gemma4_e2b_mud_gguf'
|
| 123 |
+
GGUF_QUANTIZATION = 'q4_k_m'
|
| 124 |
+
|
| 125 |
+
PER_DEVICE_TRAIN_BATCH_SIZE = 1
|
| 126 |
+
GRADIENT_ACCUMULATION_STEPS = 4
|
| 127 |
+
NUM_TRAIN_EPOCHS = 4
|
| 128 |
+
LEARNING_RATE = 8e-5
|
| 129 |
+
WARMUP_STEPS = 5
|
| 130 |
+
SEED = 3407
|
| 131 |
+
|
| 132 |
+
LORA_R = 16
|
| 133 |
+
LORA_ALPHA = 16
|
| 134 |
+
LORA_DROPOUT = 0
|
| 135 |
+
|
| 136 |
+
DEFAULT_SYSTEM_PROMPT = (
|
| 137 |
+
'당신은 우주항행 텍스트 MUD의 항로 안내자이자 세계관 해설자다. '
|
| 138 |
+
'답변은 한국어로 하며, 서정성과 실용성을 함께 지닌다. '
|
| 139 |
+
'플레이에 도움이 되는 정보와 분위기 묘사를 함께 준다.'
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
TEST_PROMPTS = [
|
| 143 |
+
'First Fire Horizon이 어떤 곳인�� 설명해줘.',
|
| 144 |
+
'First Fire Horizon의 분위기와 역할을 설명해줘.',
|
| 145 |
+
'Helios Verge이 어떤 곳인지 설명해줘.',
|
| 146 |
+
'Helios Verge의 의미와 중요성을 설명해줘.',
|
| 147 |
+
]
|
| 148 |
+
|
| 149 |
+
DATA_CANDIDATES = [
|
| 150 |
+
f'{DATA_DIR}/combined_1000.unsloth_chatml_dedup.jsonl',
|
| 151 |
+
f'{DATA_DIR}/combined_1000.unsloth_gemma4_messages_dedup.jsonl',
|
| 152 |
+
f'{DATA_DIR}/combined_1000.jsonl',
|
| 153 |
+
'/content/gemma4_mud_alpaca_100.jsonl',
|
| 154 |
+
'/content/dataset/gemma4_mud_alpaca_100.jsonl',
|
| 155 |
+
'/content/drive/MyDrive/combined_1000.unsloth_chatml_dedup.jsonl',
|
| 156 |
+
'/content/drive/MyDrive/combined_1000.unsloth_gemma4_messages_dedup.jsonl',
|
| 157 |
+
'/content/drive/MyDrive/combined_1000.jsonl',
|
| 158 |
+
'/content/gemma-mud-colab-starter/dataset/gemma4_mud_alpaca_100.jsonl',
|
| 159 |
+
'/content/drive/MyDrive/gemma-mud-colab-starter/dataset/gemma4_mud_alpaca_100.jsonl',
|
| 160 |
+
]
|
| 161 |
+
|
| 162 |
+
os.makedirs(BASE_DIR, exist_ok=True)
|
| 163 |
+
os.makedirs(DATA_DIR, exist_ok=True)
|
| 164 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 165 |
+
os.makedirs(GGUF_DIR, exist_ok=True)
|
| 166 |
+
|
| 167 |
+
print('BASE_DIR:', BASE_DIR)
|
| 168 |
+
print('DATA_DIR:', DATA_DIR)
|
| 169 |
+
print('OUTPUT_DIR:', OUTPUT_DIR)
|
| 170 |
+
print('GGUF_DIR:', GGUF_DIR)
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
## 5) dataset 경로 찾기
|
| 174 |
+
|
| 175 |
+
가장 먼저 발견되는 dataset 파일을 사용한다. 가장 안전한 경로는 `DATA_DIR` 아래에 dataset 을 올려두는 방식이다.
|
| 176 |
+
|
| 177 |
+
```python
|
| 178 |
+
def resolve_data_file(candidates):
|
| 179 |
+
for path in candidates:
|
| 180 |
+
if os.path.exists(path):
|
| 181 |
+
return path
|
| 182 |
+
raise FileNotFoundError(
|
| 183 |
+
'dataset 파일을 찾지 못했습니다. DATA_CANDIDATES를 확인하거나 '
|
| 184 |
+
'combined_1000.unsloth_chatml_dedup.jsonl, '
|
| 185 |
+
'combined_1000.unsloth_gemma4_messages_dedup.jsonl, '
|
| 186 |
+
'combined_1000.jsonl 중 하나를 DATA_DIR 또는 /content/ 에 올려주세요.'
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
DATA_FILE = resolve_data_file(DATA_CANDIDATES)
|
| 191 |
+
print('사용할 dataset:', DATA_FILE)
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
## 6) Unsloth Gemma 4 E2B 모델 로드
|
| 195 |
+
|
| 196 |
+
텍스트 LoRA 경로로 로드한다. 추론과 export 안정성을 위해 Gemma 4 chat template 를 맞춘다.
|
| 197 |
+
|
| 198 |
+
```python
|
| 199 |
+
model, tokenizer = FastModel.from_pretrained(
|
| 200 |
+
model_name = MODEL_NAME,
|
| 201 |
+
dtype = None,
|
| 202 |
+
max_seq_length = MAX_SEQ_LENGTH,
|
| 203 |
+
load_in_4bit = LOAD_IN_4BIT,
|
| 204 |
+
full_finetuning = FULL_FINETUNING,
|
| 205 |
+
# token = 'hf_...',
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
tokenizer = get_chat_template(
|
| 209 |
+
tokenizer,
|
| 210 |
+
chat_template = 'gemma-4',
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
print('모델 로드 완료:', MODEL_NAME)
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
## 7) LoRA 어댑터 부착
|
| 217 |
+
|
| 218 |
+
작은 데이터셋에서 반영이 더 잘 보이도록 `r=16`, `lora_alpha=16`, `use_gradient_checkpointing='unsloth'` 를 기본값으로 둔다.
|
| 219 |
+
|
| 220 |
+
```python
|
| 221 |
+
model = FastModel.get_peft_model(
|
| 222 |
+
model,
|
| 223 |
+
finetune_vision_layers = False,
|
| 224 |
+
finetune_language_layers = True,
|
| 225 |
+
finetune_attention_modules = True,
|
| 226 |
+
finetune_mlp_modules = True,
|
| 227 |
+
r = LORA_R,
|
| 228 |
+
lora_alpha = LORA_ALPHA,
|
| 229 |
+
lora_dropout = LORA_DROPOUT,
|
| 230 |
+
bias = 'none',
|
| 231 |
+
use_gradient_checkpointing = 'unsloth',
|
| 232 |
+
random_state = SEED,
|
| 233 |
+
)
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
## 8) dataset 로드와 Gemma 4 채팅 포맷 변환
|
| 237 |
+
|
| 238 |
+
`messages` 형식은 그대로 살리고, `instruction/input/output` 형식은 표준 `system/user/assistant` 대화로 바꾼 뒤 Gemma 4 템플릿으로 `text` 컬럼을 만든다.
|
| 239 |
+
|
| 240 |
+
```python
|
| 241 |
+
raw_dataset = load_dataset('json', data_files=DATA_FILE, split='train')
|
| 242 |
+
print(raw_dataset)
|
| 243 |
+
print(raw_dataset[0])
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def build_user_text(example):
|
| 247 |
+
instruction = (example.get('instruction') or '').strip()
|
| 248 |
+
extra_input = (example.get('input') or '').strip()
|
| 249 |
+
if extra_input:
|
| 250 |
+
return instruction + "\n\n" + extra_input
|
| 251 |
+
return instruction
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def normalize_message_content(content):
|
| 255 |
+
if isinstance(content, str):
|
| 256 |
+
text = content.strip()
|
| 257 |
+
if text:
|
| 258 |
+
return text
|
| 259 |
+
raise ValueError('빈 문자열 content는 허용하지 않습니다.')
|
| 260 |
+
|
| 261 |
+
if isinstance(content, dict):
|
| 262 |
+
content = [content]
|
| 263 |
+
|
| 264 |
+
if isinstance(content, list):
|
| 265 |
+
parts = []
|
| 266 |
+
for item in content:
|
| 267 |
+
if not isinstance(item, dict):
|
| 268 |
+
raise ValueError(f'지원하지 않는 content item 형식: {type(item)}')
|
| 269 |
+
item_type = item.get('type', 'text')
|
| 270 |
+
if item_type != 'text':
|
| 271 |
+
raise ValueError(f'텍스트 전용 학습에서는 type={item_type!r} 를 사용할 수 없습니다.')
|
| 272 |
+
text = str(item.get('text', '')).strip()
|
| 273 |
+
if text:
|
| 274 |
+
parts.append(text)
|
| 275 |
+
if parts:
|
| 276 |
+
return "\n".join(parts)
|
| 277 |
+
|
| 278 |
+
raise ValueError(f'지원하지 않는 content 형식: {type(content)}')
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def format_for_gemma4(example):
|
| 282 |
+
if 'messages' in example and example['messages']:
|
| 283 |
+
messages = []
|
| 284 |
+
for message in example['messages']:
|
| 285 |
+
messages.append(
|
| 286 |
+
{
|
| 287 |
+
'role': message['role'],
|
| 288 |
+
'content': normalize_message_content(message['content']),
|
| 289 |
+
}
|
| 290 |
+
)
|
| 291 |
+
else:
|
| 292 |
+
required_fields = ['instruction', 'output']
|
| 293 |
+
for field in required_fields:
|
| 294 |
+
if field not in example:
|
| 295 |
+
raise ValueError(f'필드 누락: {field}')
|
| 296 |
+
messages = [
|
| 297 |
+
{
|
| 298 |
+
'role': 'system',
|
| 299 |
+
'content': DEFAULT_SYSTEM_PROMPT,
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
'role': 'user',
|
| 303 |
+
'content': build_user_text(example),
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
'role': 'assistant',
|
| 307 |
+
'content': str(example['output']).strip(),
|
| 308 |
+
},
|
| 309 |
+
]
|
| 310 |
+
|
| 311 |
+
text = tokenizer.apply_chat_template(
|
| 312 |
+
messages,
|
| 313 |
+
tokenize = False,
|
| 314 |
+
add_generation_prompt = False,
|
| 315 |
+
)
|
| 316 |
+
return {'text': text}
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
train_dataset = raw_dataset.map(
|
| 320 |
+
format_for_gemma4,
|
| 321 |
+
remove_columns = raw_dataset.column_names,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
inference_system_prompt = DEFAULT_SYSTEM_PROMPT
|
| 325 |
+
if 'messages' in raw_dataset.column_names:
|
| 326 |
+
first_messages = raw_dataset[0].get('messages') or []
|
| 327 |
+
for message in first_messages:
|
| 328 |
+
if message.get('role') == 'system':
|
| 329 |
+
inference_system_prompt = normalize_message_content(message.get('content'))
|
| 330 |
+
break
|
| 331 |
+
|
| 332 |
+
print('원본 컬럼:', raw_dataset.column_names)
|
| 333 |
+
print('추론용 system prompt:', inference_system_prompt)
|
| 334 |
+
print(train_dataset[0]['text'][:800])
|
| 335 |
+
```
|
| 336 |
+
|
| 337 |
+
## 9) Unsloth SFTTrainer 설정
|
| 338 |
+
|
| 339 |
+
현재 Gemma 4 템플릿 경로에서는 `assistant_only_loss = False` 로 두고 전체 대화 텍스트를 학습한다. 대신 epoch 를 조금 더 주고 learning rate 를 낮춰서 반영 강도를 맞춘다. 런타임 종료에 대비해 중간 checkpoint 도 Drive에 저장한다.
|
| 340 |
+
|
| 341 |
+
```python
|
| 342 |
+
trainer = SFTTrainer(
|
| 343 |
+
model = model,
|
| 344 |
+
tokenizer = tokenizer,
|
| 345 |
+
train_dataset = train_dataset,
|
| 346 |
+
eval_dataset = None,
|
| 347 |
+
args = SFTConfig(
|
| 348 |
+
output_dir = OUTPUT_DIR,
|
| 349 |
+
dataset_text_field = 'text',
|
| 350 |
+
per_device_train_batch_size = PER_DEVICE_TRAIN_BATCH_SIZE,
|
| 351 |
+
gradient_accumulation_steps = GRADIENT_ACCUMULATION_STEPS,
|
| 352 |
+
warmup_steps = WARMUP_STEPS,
|
| 353 |
+
num_train_epochs = NUM_TRAIN_EPOCHS,
|
| 354 |
+
learning_rate = LEARNING_RATE,
|
| 355 |
+
logging_steps = 5,
|
| 356 |
+
save_strategy = 'steps',
|
| 357 |
+
save_steps = 50,
|
| 358 |
+
save_total_limit = 2,
|
| 359 |
+
optim = 'adamw_8bit',
|
| 360 |
+
weight_decay = 0.001,
|
| 361 |
+
lr_scheduler_type = 'linear',
|
| 362 |
+
seed = SEED,
|
| 363 |
+
report_to = 'none',
|
| 364 |
+
assistant_only_loss = False,
|
| 365 |
+
packing = False,
|
| 366 |
+
),
|
| 367 |
+
)
|
| 368 |
+
```
|
| 369 |
+
|
| 370 |
+
## 10) 학습 시작
|
| 371 |
+
|
| 372 |
+
학습 중 loss 가 step마다 조금씩 흔들리는 건 정상이다. `nan` 이나 `inf` 가 아닌지만 보면 된다. 이미 저장된 checkpoint 가 있으면 가장 최근 checkpoint 에서 자동으로 이어간다.
|
| 373 |
+
|
| 374 |
+
```python
|
| 375 |
+
from glob import glob
|
| 376 |
+
|
| 377 |
+
checkpoints = sorted(glob(f'{OUTPUT_DIR}/checkpoint-*'))
|
| 378 |
+
if checkpoints:
|
| 379 |
+
print('체크포인트에서 재개:', checkpoints[-1])
|
| 380 |
+
trainer_stats = trainer.train(resume_from_checkpoint = True)
|
| 381 |
+
else:
|
| 382 |
+
print('새 학습 시작')
|
| 383 |
+
trainer_stats = trainer.train()
|
| 384 |
+
|
| 385 |
+
print(trainer_stats)
|
| 386 |
+
```
|
| 387 |
+
|
| 388 |
+
## 11) LoRA 어댑터 저장
|
| 389 |
+
|
| 390 |
+
여기서는 먼저 LoRA 어댑터와 tokenizer 를 저장한다. LM Studio 용 GGUF 는 뒤 셀에서 만든다.
|
| 391 |
+
|
| 392 |
+
```python
|
| 393 |
+
model.save_pretrained(OUTPUT_DIR)
|
| 394 |
+
tokenizer.save_pretrained(OUTPUT_DIR)
|
| 395 |
+
|
| 396 |
+
print('저장 위치:', OUTPUT_DIR)
|
| 397 |
+
print('저장 파일:', os.listdir(OUTPUT_DIR)[:20])
|
| 398 |
+
```
|
| 399 |
+
|
| 400 |
+
## 12) 간단 추론 테스트
|
| 401 |
+
|
| 402 |
+
학습이 실제로 반영됐는지 보려면 데이터셋 원문과 가까운 질문을 greedy decoding 으로 먼저 확인한다. Gemma 4 processor 와 chat template 충돌을 피하려고, 추론은 먼저 문자열 프롬프트를 렌더링한 뒤 토크나이즈하는 2단계 방식으로 처리한다.
|
| 403 |
+
|
| 404 |
+
```python
|
| 405 |
+
FastModel.for_inference(model)
|
| 406 |
+
|
| 407 |
+
text_tokenizer = tokenizer.tokenizer if hasattr(tokenizer, 'tokenizer') else tokenizer
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
def render_text_prompt(system_prompt, user_prompt):
|
| 411 |
+
messages = []
|
| 412 |
+
if system_prompt:
|
| 413 |
+
messages.append(
|
| 414 |
+
{
|
| 415 |
+
'role': 'system',
|
| 416 |
+
'content': system_prompt,
|
| 417 |
+
}
|
| 418 |
+
)
|
| 419 |
+
messages.append(
|
| 420 |
+
{
|
| 421 |
+
'role': 'user',
|
| 422 |
+
'content': user_prompt,
|
| 423 |
+
}
|
| 424 |
+
)
|
| 425 |
+
return text_tokenizer.apply_chat_template(
|
| 426 |
+
messages,
|
| 427 |
+
tokenize = False,
|
| 428 |
+
add_generation_prompt = True,
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
for prompt_text in TEST_PROMPTS:
|
| 433 |
+
print('=' * 100)
|
| 434 |
+
print('USER:', prompt_text)
|
| 435 |
+
|
| 436 |
+
prompt = render_text_prompt(inference_system_prompt, prompt_text)
|
| 437 |
+
inputs = tokenizer(
|
| 438 |
+
text = prompt,
|
| 439 |
+
return_tensors = 'pt',
|
| 440 |
+
add_special_tokens = False,
|
| 441 |
+
).to('cuda')
|
| 442 |
+
|
| 443 |
+
prompt_len = inputs['input_ids'].shape[1]
|
| 444 |
+
|
| 445 |
+
with torch.no_grad():
|
| 446 |
+
outputs = model.generate(
|
| 447 |
+
**inputs,
|
| 448 |
+
max_new_tokens = 180,
|
| 449 |
+
do_sample = False,
|
| 450 |
+
use_cache = True,
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
answer = text_tokenizer.decode(
|
| 454 |
+
outputs[0][prompt_len:],
|
| 455 |
+
skip_special_tokens = True,
|
| 456 |
+
).split('<turn|>')[0].strip()
|
| 457 |
+
|
| 458 |
+
print('ASSISTANT:', answer)
|
| 459 |
+
print()
|
| 460 |
+
```
|
| 461 |
+
|
| 462 |
+
## 13) GGUF 생성
|
| 463 |
+
|
| 464 |
+
LM Studio 용으로는 `.gguf` 파일만 있으면 된다. 먼저 `q4_k_m` 하나를 만들어보는 흐름으로 둔다.
|
| 465 |
+
|
| 466 |
+
```python
|
| 467 |
+
from pathlib import Path
|
| 468 |
+
|
| 469 |
+
GGUF_DIR_PATH = Path(GGUF_DIR)
|
| 470 |
+
GGUF_DIR_PATH.mkdir(parents = True, exist_ok = True)
|
| 471 |
+
|
| 472 |
+
model.save_pretrained_gguf(
|
| 473 |
+
str(GGUF_DIR_PATH),
|
| 474 |
+
tokenizer,
|
| 475 |
+
quantization_method = GGUF_QUANTIZATION,
|
| 476 |
+
maximum_memory_usage = 0.5,
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
gguf_files = sorted(GGUF_DIR_PATH.glob('*.gguf'))
|
| 480 |
+
if not gguf_files:
|
| 481 |
+
raise RuntimeError('GGUF 파일이 생성되지 않았습니다.')
|
| 482 |
+
|
| 483 |
+
print('생성된 GGUF:')
|
| 484 |
+
for path in gguf_files:
|
| 485 |
+
print('-', path, f'({path.stat().st_size / 1024**3:.2f} GB)')
|
| 486 |
+
```
|
| 487 |
+
|
| 488 |
+
## 14) Drive 경로 확인 + 브라우저 다운로드
|
| 489 |
+
|
| 490 |
+
이 노트북은 처음부터 Drive에 저장하므로, 여기서는 생성된 GGUF 경로를 확인하고 필요할 때만 브라우저 다운로드를 건다.
|
| 491 |
+
|
| 492 |
+
```python
|
| 493 |
+
from google.colab import files
|
| 494 |
+
|
| 495 |
+
print('Drive 저장 경로:')
|
| 496 |
+
for path in gguf_files:
|
| 497 |
+
print('-', path)
|
| 498 |
+
|
| 499 |
+
for path in gguf_files:
|
| 500 |
+
print('다운로드 시작:', path.name)
|
| 501 |
+
files.download(str(path))
|
| 502 |
+
```
|
| 503 |
+
|
| 504 |
+
## 15) 자주 흔들리는 지점
|
| 505 |
+
|
| 506 |
+
- 설치 직후 import 오류가 나면 런타임을 재시작한 뒤 다시 실행
|
| 507 |
+
- dataset 파일은 가능하면 `DATA_DIR` 에 올리고, 출력은 기본값 그대로 Drive 경로를 유지
|
| 508 |
+
- VRAM 이 부족하면 `LOAD_IN_4BIT = True` 또는 `MAX_SEQ_LENGTH = 512` 로 조정
|
| 509 |
+
- `messages` dataset 으로 학습했다면 추론 때도 system prompt 를 같이 넣는 편이 더 안정적
|
| 510 |
+
- Gemma 4는 chat template 일치가 중요하므로, 추론 때도 Gemma 4 템플릿을 그대로 사용
|
| 511 |
+
- GGUF 변환 중 메모리 부족이면 `maximum_memory_usage` 를 더 낮춘다
|
| 512 |
+
- 런타임이 끊겨도 Drive 안의 checkpoint 는 남으므로 다시 연결한 뒤 학습 셀을 다시 실행하면 이어갈 수 있다
|
| 513 |
+
- 작은 LoRA는 base 모델 성향을 완전히 지우는 게 아니라 덧입히는 방식이라, 테스트도 데이터셋 원문과 가까운 질문부터 보는 게 좋다
|
gemma-mud-colab-starter/output/.gitkeep
ADDED
|
File without changes
|
gemma-mud-colab-starter/package_manifest.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# package manifest
|
| 2 |
+
|
| 3 |
+
- `README.md`
|
| 4 |
+
전체 소개와 빠른 시작 안내
|
| 5 |
+
|
| 6 |
+
- `requirements_colab.txt`
|
| 7 |
+
Colab 설치 패키지 목록
|
| 8 |
+
|
| 9 |
+
- `run_instructions.md`
|
| 10 |
+
실제 실행 순서만 따로 정리한 문서
|
| 11 |
+
|
| 12 |
+
- `troubleshooting.md`
|
| 13 |
+
자주 막히는 오류와 해결
|
| 14 |
+
|
| 15 |
+
- `dataset/gemma4_mud_alpaca_100.jsonl`
|
| 16 |
+
첫 실험용 MUD 학습 데이터
|
| 17 |
+
|
| 18 |
+
- `notebooks/Gemma4_MUD_QLoRA_Colab_Notebook.ipynb`
|
| 19 |
+
QLoRA 학습 노트북
|
| 20 |
+
|
| 21 |
+
- `examples/sample_prompts.md`
|
| 22 |
+
학습 후 시험할 프롬프트 예시
|
| 23 |
+
|
| 24 |
+
- `examples/expected_outputs.md`
|
| 25 |
+
정상 출력 품질 기준
|
| 26 |
+
|
| 27 |
+
- `output/.gitkeep`
|
| 28 |
+
결과 폴더 자리 보존용 빈 파일
|
| 29 |
+
|
| 30 |
+
이 묶음은 “작게 시작해서 실제로 한 번 돌아가게 만드는” 패키지다.
|
gemma-mud-colab-starter/requirements_colab.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Reference packages for the Unsloth Gemma 4 Colab notebook.
|
| 2 |
+
# The notebook chooses xformers dynamically to match Colab's torch build.
|
| 3 |
+
unsloth
|
| 4 |
+
unsloth_zoo
|
| 5 |
+
transformers==5.5.0
|
| 6 |
+
datasets==4.3.0
|
| 7 |
+
huggingface_hub>=0.34.0
|
| 8 |
+
sentencepiece
|
| 9 |
+
protobuf
|
| 10 |
+
hf_transfer
|
| 11 |
+
bitsandbytes
|
| 12 |
+
accelerate
|
| 13 |
+
peft
|
| 14 |
+
trl
|
| 15 |
+
triton
|
| 16 |
+
torchcodec
|
| 17 |
+
timm
|
gemma-mud-colab-starter/run_instructions.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 실행 순서
|
| 2 |
+
|
| 3 |
+
## 1. 코랩 열기
|
| 4 |
+
- `notebooks/Gemma4_MUD_QLoRA_Colab_Notebook.ipynb` 파일을 Google Colab에서 연다.
|
| 5 |
+
|
| 6 |
+
## 2. GPU 켜기
|
| 7 |
+
- 상단 메뉴에서 **런타임 → 런타임 유형 변경**
|
| 8 |
+
- 하드웨어 가속기를 **GPU**로 바꾼다.
|
| 9 |
+
|
| 10 |
+
## 3. dataset 파일 업로드
|
| 11 |
+
- 왼쪽 파일 패널을 연다.
|
| 12 |
+
- `dataset/gemma4_mud_alpaca_100.jsonl` 파일을 업로드한다.
|
| 13 |
+
- 가장 쉬운 업로드 위치는 `/content/` 이다.
|
| 14 |
+
|
| 15 |
+
## 4. 노트북 설정 셀 확인
|
| 16 |
+
- 기본 모델 이름 (`unsloth/gemma-4-E2B-it`)
|
| 17 |
+
- 데이터 파일 경로
|
| 18 |
+
- 출력 폴더
|
| 19 |
+
- 시퀀스 길이
|
| 20 |
+
- `LOAD_IN_4BIT` 여부
|
| 21 |
+
|
| 22 |
+
처음에는 기본값 그대로 두는 편이 안전하다.
|
| 23 |
+
|
| 24 |
+
## 5. 셀을 위에서부터 실행
|
| 25 |
+
- 설치 셀
|
| 26 |
+
- 버전 확인 셀 출력 확인
|
| 27 |
+
- 선택 로그인 셀
|
| 28 |
+
- 환경 확인 셀
|
| 29 |
+
- 설정 셀
|
| 30 |
+
- dataset 경로 확인 셀
|
| 31 |
+
- 모델 로드 셀
|
| 32 |
+
- LoRA 설정 셀
|
| 33 |
+
- 데이터셋 로드/채팅 포맷 변환 셀
|
| 34 |
+
- 학습 셀
|
| 35 |
+
- 저장 셀
|
| 36 |
+
- 추론 테스트 셀
|
| 37 |
+
|
| 38 |
+
중간 셀을 건너뛰지 않는다.
|
| 39 |
+
설치 셀 뒤에 import 오류가 이미 났다면 **런타임 재시작** 후 2번 선택 로그인 셀부터 다시 이어간다.
|
| 40 |
+
|
| 41 |
+
## 6. Hugging Face 토큰 입력
|
| 42 |
+
- Hub 업로드가 필요할 때만 로그인 셀 주석을 풀어 실행한다.
|
| 43 |
+
- 토큰은 문서나 코드에 저장하지 않는다.
|
| 44 |
+
|
| 45 |
+
## 7. 학습 시작
|
| 46 |
+
- `trainer.train()` 셀을 실행한다.
|
| 47 |
+
- 로그가 나오면 정상이다.
|
| 48 |
+
|
| 49 |
+
## 8. 저장 폴더 확인
|
| 50 |
+
- 학습이 끝나면 `/content/output/gemma_mud_lora_out` 폴더를 확인한다.
|
| 51 |
+
|
| 52 |
+
## 9. Drive 백업 또는 다운로드
|
| 53 |
+
- 노트북 마지막 근처의 Drive 예시 셀을 사용하거나
|
| 54 |
+
- Colab 파일 패널에서 결과를 직접 내려받는다.
|
| 55 |
+
|
| 56 |
+
## 10. 첫 테스트
|
| 57 |
+
- 기본 테스트 프롬프트는 `talk oracle`
|
| 58 |
+
- 출력이 짧고 분위기 있고 세계관 안에 머무르면 우선 합격이다.
|
gemma-mud-colab-starter/scripts/prepare_unsloth_gemma4_dataset.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Prepare a ChatML JSONL dataset for Unsloth Gemma 4 fine-tuning.
|
| 3 |
+
|
| 4 |
+
This script normalizes whitespace, validates the conversation structure,
|
| 5 |
+
deduplicates exact duplicates, and writes 2 output datasets:
|
| 6 |
+
|
| 7 |
+
1. ChatML string content format:
|
| 8 |
+
{"messages": [{"role": "...", "content": "..."}]}
|
| 9 |
+
2. Gemma 4 typed text content format:
|
| 10 |
+
{"messages": [{"role": "...", "content": [{"type": "text", "text": "..."}]}]}
|
| 11 |
+
|
| 12 |
+
The second format is convenient when you want the dataset shape to match the
|
| 13 |
+
Gemma 4 message objects used in Unsloth inference examples.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import json
|
| 20 |
+
from collections import Counter
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Iterable
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
ALLOWED_ROLES = {"system", "user", "assistant"}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def normalize_text(text: str) -> str:
|
| 29 |
+
lines = [line.rstrip() for line in text.replace("\r\n", "\n").replace("\r", "\n").split("\n")]
|
| 30 |
+
normalized = "\n".join(lines).strip()
|
| 31 |
+
return normalized
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def normalize_messages(messages: list[dict], row_number: int) -> list[dict]:
|
| 35 |
+
if not isinstance(messages, list) or not messages:
|
| 36 |
+
raise ValueError(f"row {row_number}: messages must be a non-empty list")
|
| 37 |
+
|
| 38 |
+
normalized = []
|
| 39 |
+
for index, message in enumerate(messages, start=1):
|
| 40 |
+
if not isinstance(message, dict):
|
| 41 |
+
raise ValueError(f"row {row_number}: message {index} must be an object")
|
| 42 |
+
|
| 43 |
+
role = message.get("role")
|
| 44 |
+
content = message.get("content")
|
| 45 |
+
if role not in ALLOWED_ROLES:
|
| 46 |
+
raise ValueError(f"row {row_number}: unsupported role {role!r} at message {index}")
|
| 47 |
+
if not isinstance(content, str):
|
| 48 |
+
raise ValueError(f"row {row_number}: content must be a string at message {index}")
|
| 49 |
+
|
| 50 |
+
content = normalize_text(content)
|
| 51 |
+
if not content:
|
| 52 |
+
raise ValueError(f"row {row_number}: empty content at message {index}")
|
| 53 |
+
|
| 54 |
+
normalized.append({"role": role, "content": content})
|
| 55 |
+
|
| 56 |
+
return normalized
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def iter_jsonl(path: Path) -> Iterable[dict]:
|
| 60 |
+
with path.open() as handle:
|
| 61 |
+
for line in handle:
|
| 62 |
+
line = line.strip()
|
| 63 |
+
if line:
|
| 64 |
+
yield json.loads(line)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def messages_key(messages: list[dict]) -> str:
|
| 68 |
+
return json.dumps(messages, ensure_ascii=False, sort_keys=True)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def to_gemma4_messages(messages: list[dict]) -> list[dict]:
|
| 72 |
+
return [
|
| 73 |
+
{
|
| 74 |
+
"role": message["role"],
|
| 75 |
+
"content": [{"type": "text", "text": message["content"]}],
|
| 76 |
+
}
|
| 77 |
+
for message in messages
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def write_jsonl(path: Path, rows: Iterable[dict]) -> None:
|
| 82 |
+
with path.open("w") as handle:
|
| 83 |
+
for row in rows:
|
| 84 |
+
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def main() -> None:
|
| 88 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 89 |
+
parser.add_argument("input", type=Path, help="Path to the source JSONL file")
|
| 90 |
+
parser.add_argument(
|
| 91 |
+
"--output-dir",
|
| 92 |
+
type=Path,
|
| 93 |
+
default=None,
|
| 94 |
+
help="Directory for converted files. Defaults to the input file directory.",
|
| 95 |
+
)
|
| 96 |
+
args = parser.parse_args()
|
| 97 |
+
|
| 98 |
+
source = args.input.expanduser().resolve()
|
| 99 |
+
output_dir = (args.output_dir.expanduser().resolve() if args.output_dir else source.parent)
|
| 100 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 101 |
+
|
| 102 |
+
chatml_rows = []
|
| 103 |
+
gemma4_rows = []
|
| 104 |
+
seen = set()
|
| 105 |
+
role_patterns = Counter()
|
| 106 |
+
|
| 107 |
+
raw_count = 0
|
| 108 |
+
duplicate_count = 0
|
| 109 |
+
for row_number, row in enumerate(iter_jsonl(source), start=1):
|
| 110 |
+
raw_count += 1
|
| 111 |
+
messages = normalize_messages(row.get("messages"), row_number)
|
| 112 |
+
role_patterns[tuple(message["role"] for message in messages)] += 1
|
| 113 |
+
|
| 114 |
+
key = messages_key(messages)
|
| 115 |
+
if key in seen:
|
| 116 |
+
duplicate_count += 1
|
| 117 |
+
continue
|
| 118 |
+
seen.add(key)
|
| 119 |
+
|
| 120 |
+
chatml_rows.append({"messages": messages})
|
| 121 |
+
gemma4_rows.append({"messages": to_gemma4_messages(messages)})
|
| 122 |
+
|
| 123 |
+
stem = source.stem
|
| 124 |
+
chatml_path = output_dir / f"{stem}.unsloth_chatml_dedup.jsonl"
|
| 125 |
+
gemma4_path = output_dir / f"{stem}.unsloth_gemma4_messages_dedup.jsonl"
|
| 126 |
+
|
| 127 |
+
write_jsonl(chatml_path, chatml_rows)
|
| 128 |
+
write_jsonl(gemma4_path, gemma4_rows)
|
| 129 |
+
|
| 130 |
+
print(f"input_rows={raw_count}")
|
| 131 |
+
print(f"deduped_rows={len(chatml_rows)}")
|
| 132 |
+
print(f"duplicates_removed={duplicate_count}")
|
| 133 |
+
print(f"role_patterns={dict(role_patterns)}")
|
| 134 |
+
print(f"chatml_output={chatml_path}")
|
| 135 |
+
print(f"gemma4_output={gemma4_path}")
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
if __name__ == "__main__":
|
| 139 |
+
main()
|
gemma-mud-colab-starter/troubleshooting.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 문제 해결
|
| 2 |
+
|
| 3 |
+
## 1. Hugging Face 인증 실패
|
| 4 |
+
|
| 5 |
+
### 증상
|
| 6 |
+
- 로그인 셀에서 오류가 난다.
|
| 7 |
+
- 토큰 입력 후에도 인증 실패 메시지가 뜬다.
|
| 8 |
+
|
| 9 |
+
### 원인
|
| 10 |
+
- 토큰이 틀렸거나 만료되었을 수 있다.
|
| 11 |
+
- 토큰 권한이 부족할 수 있다.
|
| 12 |
+
- 붙여 넣는 과정에서 공백이 섞였을 수 있다.
|
| 13 |
+
|
| 14 |
+
### 해결 방법
|
| 15 |
+
- Hugging Face에서 새 토큰을 다시 발급한다.
|
| 16 |
+
- 복사할 때 앞뒤 공백을 제거한다.
|
| 17 |
+
- 노트북을 재시작한 뒤 다시 로그인한다.
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## 2. Gemma 모델 접근 권한 오류
|
| 22 |
+
|
| 23 |
+
### 증상
|
| 24 |
+
- `from_pretrained` 단계에서 접근 거부가 뜬다.
|
| 25 |
+
- gated model 관련 메시지가 나온다.
|
| 26 |
+
|
| 27 |
+
### 원인
|
| 28 |
+
- Gemma 모델 사용 조건을 아직 승인하지 않았다.
|
| 29 |
+
- 승인 후에도 로그인 상태가 반영되지 않았을 수 있다.
|
| 30 |
+
|
| 31 |
+
### 해결 방법
|
| 32 |
+
- Hugging Face에서 원하는 Gemma 모델 카드로 이동한다.
|
| 33 |
+
- 사용 조건을 승인한다.
|
| 34 |
+
- 다시 로그인한다.
|
| 35 |
+
- 계속 막히면 기본값인 `unsloth/gemma-4-E2B-it` 경로로 먼저 시험한다.
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## 3. Colab GPU가 안 잡히는 경우
|
| 40 |
+
|
| 41 |
+
### 증상
|
| 42 |
+
- `torch.cuda.is_available()` 가 `False`로 나온다.
|
| 43 |
+
- `import unsloth` 에서 `Unsloth cannot find any torch accelerator` 오류가 난다.
|
| 44 |
+
|
| 45 |
+
### 원인
|
| 46 |
+
- 런타임이 CPU로 열렸을 수 있다.
|
| 47 |
+
- 무료 Colab 자원이 잠시 부족할 수 있다.
|
| 48 |
+
|
| 49 |
+
### 해결 방법
|
| 50 |
+
- **런타임 → 런타임 유형 변경 → GPU** 로 다시 설정한다.
|
| 51 |
+
- 새 세션을 다시 열어 본다.
|
| 52 |
+
- GPU로 바꾼 뒤에는 **설치 셀부터 다시 실행**한다.
|
| 53 |
+
- 계속 안 되면 잠시 후 다시 시도한다.
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## 4. bitsandbytes / quantization 관련 오류
|
| 58 |
+
|
| 59 |
+
### 증상
|
| 60 |
+
- 4bit 양자화 설정 셀에서 오류가 난다.
|
| 61 |
+
- bitsandbytes import 오류가 난다.
|
| 62 |
+
|
| 63 |
+
### 원인
|
| 64 |
+
- 패키지 설치가 덜 끝났을 수 있다.
|
| 65 |
+
- GPU 세션이 아닌 상태일 수 있다.
|
| 66 |
+
- 설치 후 런타임 재시작이 필요할 수 있다.
|
| 67 |
+
|
| 68 |
+
### 해결 방법
|
| 69 |
+
- 설치 셀을 다시 실행한다.
|
| 70 |
+
- 런타임을 재시작한다.
|
| 71 |
+
- GPU가 실제로 잡혔는지 다시 확인한다.
|
| 72 |
+
- `LOAD_IN_4BIT = True` / `False` 를 바꿔 어느 쪽이 현재 세션에서 안정적인지 확인한다.
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## 4-1. import 단계에서 바로 깨지는 경우
|
| 77 |
+
|
| 78 |
+
### 증상
|
| 79 |
+
- `from transformers import ...` 단계에서 바로 오류가 난다.
|
| 80 |
+
- `ModuleNotFoundError: No module named 'unsloth'` 가 뜬다.
|
| 81 |
+
- `from unsloth import FastModel` 단계에서 import error 가 난다.
|
| 82 |
+
- `from trl import SFTTrainer` 에서 import error 가 난다.
|
| 83 |
+
- 설치 셀은 끝났지만 다음 셀에서 패키지 충돌 메시지가 이어진다.
|
| 84 |
+
|
| 85 |
+
### 원인
|
| 86 |
+
- Colab 기본 `torch` 와 Unsloth 쪽 패키지 버전이 중간에 섞였을 수 있다.
|
| 87 |
+
- `transformers==5.5.0` 이전 조합이나 누락된 `timm` / `torchcodec` 때문에 Gemma 4 import 가 깨질 수 있다.
|
| 88 |
+
- 설치 직후 재시작 없이 이전 런타임 상태가 남아 있을 수 있다.
|
| 89 |
+
|
| 90 |
+
### 해결 방법
|
| 91 |
+
- 노트북 설치 셀을 그대로 다시 실행한다.
|
| 92 |
+
- 설치 뒤 바로 깨졌다면 **런타임을 재시작한 다음 2번 셀부터 다시 실행**한다.
|
| 93 |
+
- 설치 셀 마지막의 `pip show unsloth` 출력이 실제로 보이는지 확인한다.
|
| 94 |
+
- 버전 출력에서 `unsloth`, `transformers`, `trl` 이 정상적으로 찍히는지 확인한다.
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## 5. 메모리 부족
|
| 99 |
+
|
| 100 |
+
### 증상
|
| 101 |
+
- 학습 중간에 out of memory 오류가 난다.
|
| 102 |
+
- 세션이 갑자기 죽는다.
|
| 103 |
+
|
| 104 |
+
### 원인
|
| 105 |
+
- 모델이 너무 크다.
|
| 106 |
+
- `MAX_SEQ_LENGTH` 가 너무 길다.
|
| 107 |
+
- 배치 크기 또는 누적 스텝 조합이 현재 GPU에 무겁다.
|
| 108 |
+
|
| 109 |
+
### 해결 방법
|
| 110 |
+
- `LOAD_IN_4BIT = True` 로 바꾼다.
|
| 111 |
+
- `MAX_SEQ_LENGTH` 를 1024 → 512 로 줄인다.
|
| 112 |
+
- batch size는 1로 유지하고, 필요하면 데이터 수를 줄여 흐름만 먼저 확인한다.
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
## 6. dataset 파일 경로 오류
|
| 117 |
+
|
| 118 |
+
### 증상
|
| 119 |
+
- `FileNotFoundError` 가 뜬다.
|
| 120 |
+
- 노트북이 dataset 파일을 찾지 못한다.
|
| 121 |
+
|
| 122 |
+
### 원인
|
| 123 |
+
- 파일 업로드 위치가 노트북 설정과 다르다.
|
| 124 |
+
- zip은 풀었지만 Colab 경로가 달라졌다.
|
| 125 |
+
|
| 126 |
+
### 해결 방법
|
| 127 |
+
- 파일 패널에서 실제 경로를 확인한다.
|
| 128 |
+
- 설정 셀의 `DATA_CANDIDATES` 목록에 현재 경로를 추가한다.
|
| 129 |
+
- 가장 쉬운 방법은 dataset 파일을 `/content/` 에 직접 올리는 것이다.
|
| 130 |
+
|
| 131 |
+
---
|
| 132 |
+
|
| 133 |
+
## 7. 세션 종료 또는 런타임 초기화
|
| 134 |
+
|
| 135 |
+
### 증상
|
| 136 |
+
- 코랩이 갑자기 다시 시작된다.
|
| 137 |
+
- 학습 로그가 사라진다.
|
| 138 |
+
|
| 139 |
+
### 원인
|
| 140 |
+
- 무료 Colab 세션 제한
|
| 141 |
+
- 메모리 초과
|
| 142 |
+
- 장시간 유휴 상태
|
| 143 |
+
|
| 144 |
+
### 해결 방법
|
| 145 |
+
- 먼저 작은 모델과 짧은 설정으로 한 번 성공시킨다.
|
| 146 |
+
- 저장 셀이 끝났는지 수시로 확인한다.
|
| 147 |
+
- 결과는 가능한 빨리 Drive나 로컬에 백업한다.
|
| 148 |
+
|
| 149 |
+
---
|
| 150 |
+
|
| 151 |
+
## 8. 출력 품질이 이상한 경우
|
| 152 |
+
|
| 153 |
+
### 증상
|
| 154 |
+
- 말투가 흔들린다.
|
| 155 |
+
- 세계관 밖 설명이 나온다.
|
| 156 |
+
- 너무 길거나 건조한 답이 나온다.
|
| 157 |
+
|
| 158 |
+
### 원인
|
| 159 |
+
- 데이터 수가 적다.
|
| 160 |
+
- instruction이 충분히 구체적이지 않다.
|
| 161 |
+
- 문체는 익혔지만 규칙까지 자동으로 배우지 못했다.
|
| 162 |
+
|
| 163 |
+
### 해결 방법
|
| 164 |
+
- 데이터 수를 300개 이상으로 늘린다.
|
| 165 |
+
- NPC별, 명령어별 데이터를 나눈다.
|
| 166 |
+
- `instruction` 문장을 더 또렷하게 쓴다.
|
| 167 |
+
- 실제 게임 판정은 엔진이 맡고, AI는 대화/분위기/서사 보조에 쓰는 편이 안전하다.
|
| 168 |
+
|
| 169 |
+
---
|
| 170 |
+
|
| 171 |
+
## 9. 저장 파일이 안 보이는 경우
|
| 172 |
+
|
| 173 |
+
### 증상
|
| 174 |
+
- 학습은 끝났는데 출력 폴더가 비어 보인다.
|
| 175 |
+
- 파일 패널에서 결과가 잘 안 보인다.
|
| 176 |
+
|
| 177 |
+
### 원인
|
| 178 |
+
- 저장 경로를 잘못 보았을 수 있다.
|
| 179 |
+
- 세션 재시작 전 저장이 끝나지 않았을 수 있다.
|
| 180 |
+
|
| 181 |
+
### 해결 방법
|
| 182 |
+
- `/content/output/gemma_mud_lora_out` 경로를 직접 확인한다.
|
| 183 |
+
- `os.listdir()` 로 폴더 내용을 출력해 본다.
|
| 184 |
+
- Drive 복사 셀을 사용해 별도 위치에 백업한다.
|