sangwon1472 commited on
Commit
d9fceea
·
1 Parent(s): 59fd928

Add gemma-mud-colab-starter package

Browse files
.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 복사 셀을 사용해 별도 위치에 백업한다.