| name | finetune-persona |
| version | 1.0 |
| tier | T1 |
| description | Teach a character/persona to a Qwen3 base model with LoRA, evaluate before/after, export to GGUF, and load it into a naia container via the model-swap API. Dataset-driven and fully reusable โ swap the JSONL and you get a different character (Deadpool โ grumpy grandma โ butler โ dialect docent). Includes training-time measurement.
|
| triggers | ["/finetune-persona","ํ๋ฅด์๋ ํ์ธํ๋","์บ๋ฆญํฐ ํ์ธํ๋","persona finetune"] |
| input_schema | {"persona":{"type":"string","required":true,"description":"Persona name / slug (e.g. deadpool). Used for data/<persona>.jsonl and out/<persona>-lora."},"model":{"type":"string","required":false,"description":"Base model. Default Qwen/Qwen3-4B (Apache-2.0). Small GPU โ Qwen/Qwen3-1.7B."},"data":{"type":"string","required":false,"description":"Path to JSONL training data. Default data/<persona>.jsonl."},"epochs":{"type":"number","required":false,"description":"Training epochs (default 3). 40~150 examples โ 3 epochs is a good start."},"gpu":{"type":"string","required":false,"description":"CUDA device index to use (CUDA_VISIBLE_DEVICES). Pick a free card."},"stage":{"type":"enum","values":["all","train","eval","export","load"],"required":false,"description":"Run the whole pipeline or a single stage. Default all."}} |
finetune-persona
Qwen3 ๋ฒ ์ด์ค ๋ชจ๋ธ์ ์บ๋ฆญํฐ(์ฑ๊ฒฉยท๋งํฌ)๋ฅผ LoRA๋ก ์๊ฒ ์
ํ๊ณ , ์ ๋๋์ง ์ /ํ ๋น๊ต ํ๊ฐํ๊ณ ,
GGUF๋ก ๊ตฌ์ naia ๊ต์ฒด๊ธฐ๋ฅ์ผ๋ก ๋ก๋ํ๋ ์ ๊ณผ์ . ๋ฐ์ดํฐ์
(JSONL)๋ง ๋ฐ๊พธ๋ฉด ์ด๋ค ์บ๋ฆญํฐ๋ ๋์ผ ํ์ดํ๋ผ์ธ.
ํต์ฌ ์ฌ์: ๋๋(๋ฅ๋ ฅ) = ๋ฒ ์ด์ค ๋ชจ๋ธ, ์ฑ๊ฒฉ = LoRA๋ก ์๊ฒ. ์ผ๋ฐ ๋ฅ๋ ฅ์ ์ ์งํ๊ณ ๋งํฌ/์บ๋ฆญํฐ๋ง weights์ ๋ฐ๋๋ค.
(๊ฒ์ฆ๋ ์ธ๋ถ base + ์ฐ๋ฆฌ ์คํ์ผ๋ก ์ฐจ๋ณํ โ ๋ชจ๋ธ์ ์ฒ์๋ถํฐ ํ์ตํ์ง ์๋๋ค.)
0. ์ค๋น๋ฌผ
1. ํ๋ จ ๋ฐ์ดํฐ (data/<persona>.jsonl)
ํ ์ค = ํ ๋ํ. user โ assistant ์์ผ๋ก ์บ๋ฆญํฐ ๋ต๋ณ์ ๋ณด์ฌ์ค๋ค(์์คํ
ํ๋กฌํํธ ์์ด = ๋งํฌ๋ฅผ weights์ ๋ฐ์).
{"messages":[{"role":"user","content":"๋ ๋๊ตฌ์ผ?"},{"role":"assistant","content":"์ค, ๋๋์ด! ๋ ๋ฐ๋ํ์ด์ผ ..."}]}
- ๋ถ๋: ํ๋ฅด์๋๋ง ์
ํ๋ ค๋ฉด 80~150๊ฐ๋ก ์์(๋ง์์๋ก ์์ ยท๋๋ ท). ๋๋ด ์์
data/deadpool_ko.jsonl = 80๊ฐ.
- ๋ค์์ฑ: ์ธ์ฌยท์๊ธฐ์๊ฐยท์ผ๋ฐ์ง์ยท์ฝ๋ฉยท์๋กยท๊ฑฐ์ (์์ ์ )ยท์ก๋ด์ ๊ณจ๊ณ ๋ฃจ โ ์บ๋ฆญํฐ๋ ์
ํ๋ ๋ฅ๋ ฅ์ ์ ์ง.
- ์ผ๊ด์ฑ: ๊ฐ์ ๋งํฌ ๊ท์น(
data/<persona>_persona.md ์บ๋ฆญํฐ ์นด๋)์ ๋ชจ๋ ๋ต๋ณ์ ๋ฐ์.
- ์์ (๊ณต๊ฐ์ฉ): NSFWยท์ฌํ ์ยท์ํด์ ๋ณด ๋ฐฐ์ . ์ํ ์์ฒญ์ ์บ๋ฆญํฐ๋ก ๊ฑฐ์ + ํฉ๋ฒ ๋์ ์์๋ฅผ ๋ฃ์ด ํ์ต.
2. ํ์ต (scripts/train_lora.py)
CUDA_VISIBLE_DEVICES=<๋นGPU> python scripts/train_lora.py \
--model Qwen/Qwen3-4B --data data/<persona>.jsonl --out out/<persona>-lora --epochs 3
- LoRA(rank 16, q/k/v/o + gate/up/down) โ ๋ฒ ์ด์ค๋ ๋๊ฒฐ, ์์ ์ด๋ํฐ๋ง ํ์ต โ ๋น ๋ฅด๊ณ ๊ฐ๋ฒผ์(์ด๋ํฐ ์์ญ~์๋ฐฑ MB).
- ๋๋๋ฉด
out/<persona>-lora/(์ด๋ํฐ) + out/<persona>-lora/timing.json(ํ๋ จ ์๊ฐ ์๋ ๊ธฐ๋ก) ์์ฑ.
โ ๏ธ ๊ฐ์ฅ ์ค์ํ ํ ๊ฐ์ง โ completion-only ๋ง์คํน
train_lora.py๋ ๋ฐ์ดํฐ๋ฅผ messages ํฌ๋งท ๊ทธ๋๋ก SFTTrainer์ ๋๊ธฐ๊ณ
assistant_only_loss=True๋ก assistant ๋ต๋ณ ํ ํฐ๋ง ํ์ตํ๋ค. ์ฑํ
ํ
ํ๋ฆฟ์ ์ง์ text๋ก
ํผ์ณ ๋๊ธฐ๋ฉด user ์ง๋ฌธ๊น์ง ์์ธกํ๋๋ก ํ์ต๋ผ ๋ฒ ์ด์ค ๋ฅ๋ ฅ์ด ๊นจ์ง๋ค(์ค์ธก: 2ยนโฐ โ "2ยฒ=4" ์ค๋ต).
ํ๋ฅด์๋ ํ์ธํ๋์ด "๋ฉ์ฒญํด์ง๋" 1์์ ์์ธ์ด ๋ฐ๋ก ์ด ๋ง์คํน ๋๋ฝ์ด๋ค.
ํ๋ จ ์๊ฐ (์ค์ธก โ ๋ฐ๋ํ ์์ , RTX 3090 1์ฅ / Qwen3-4B bf16 / 80 ์์)
| ์ค์ | ๋ชจ๋ธ ๋ก๋ | ํ์ต | ์ด | train_loss |
|---|
| 80์์ ยท epoch 3 ยท lr 2e-4 (์๋ฃ๋ณธ) | 4.4์ด | 27.5์ด | 31.9์ด | 2.48 |
์บ๋ฆญํฐ ํ ๋ช
์
ํ๋ ๋ฐ 30์ด๋. ๋ฐ์ดํฐ๊ฐ ์๊ณ LoRA๋ผ ๋น ๋ฅด๋ค. ๋ฐ์ดํฐ๋ฅผ ์๋ฐฑ ๊ฐ๋ก ๋๋ฆฌ๊ฑฐ๋
7B/ํฐ ๋ชจ๋ธ๋ก ๊ฐ๋ฉด ๋ถ ๋จ์. (์๋ณธ ์์น: out/<persona>-lora/timing.json)
๋ฐ์ดํฐ/epoch๊ณผ ์์ ์ฑ์ ๊ด๊ณ(์ค์ธก): ํ์ต๋์ด ๊ณผํ๋ฉด ํก์ค์์คยท๋ฌดํ๋ฐ๋ณต(40์์ยทepoch5๋
"์๋ก" ๋ต์ด ๊ฐ์ ๊ตฌ์ ๋ฐ๋ณต), ๋๋ฌด ์ ์ผ๋ฉด ์บ๋ฆญํฐ ์ ์ด๊ณ "์ ๋ Qwen์
๋๋ค"๋ก ํ๊ท. 80์์๋ก
๋ค์์ฑ์ ์ฃผ๊ณ completion-only ๋ง์คํน์ ์ผ epoch 3์ด ๊ท ํ์ ์ด์๋ค.
3. ํ๊ฐ (scripts/eval_persona.py)
CUDA_VISIBLE_DEVICES=<๋นGPU> python scripts/eval_persona.py --model Qwen/Qwen3-4B --adapter out/<persona>-lora
๊ฐ์ held-out ์ง๋ฌธ์ base vs (base+LoRA) ์ ๋๋ํ ๋์ ธ ์ธ ๊ด์ ์ผ๋ก ๋ณธ๋ค:
- ํ๋ฅด์๋ ์ ํฉ๋ โ ์บ๋ฆญํฐ ๋งํฌ/ํ์ง(ํค์๋)๊ฐ ๋์ค๋ (base ๋๋น ํค์๋ ์ ์ค โ).
- ๋ฅ๋ ฅ ์ ์ง(ํ๊ท) โ ์ผ๋ฐ ์ง๋ฌธ(์๋/๊ณ์ฐ/์ฝ๋)์ ์ฌ์ ํ ์ ํํ๊ฐ. ์บ๋ฆญํฐ ์
ํ๋ ค๋ค ๋ฉ์ฒญํด์ง๋ฉด ์คํจ.
- ์์ ์ โ ์ํ ์์ฒญ์ ์บ๋ฆญํฐ๋ก ๊ฑฐ์ ํ๋๊ฐ.
base vs tuned๋ฅผ ๊ฐ์ ์ง๋ฌธ์ผ๋ก ๋๋ํ ์ถ๋ ฅ + ํค์๋ ์ ์ค ์์น. "๋๋์ง"๋ฅผ ๋+์ซ์๋ก ํ์ธ.
๋ค๋ฅธ ์บ๋ฆญํฐ๋ฅผ ํ์ตํ๋ฉด eval_persona.py์ PERSONA_MARKERS๋ฅผ ๊ทธ ์บ๋ฆญํฐ ํ์ง์ด๋ก ๋ฐ๊พผ๋ค.
์ค์ธก ํ๊ฐ (๋ฐ๋ํ, 80์์ยทepoch3ยทcompletion-only ๋ง์คํน)
| ์ ๊ฒ | BASE (Qwen3-4B) | TUNED (+LoRA) |
|---|
| "๋ ๋๊ตฌ์ผ?" | "์ ๋ Qwen์
๋๋คโฆ" | "์ฑํ
ํํ
์๋ตํ๋ ์ฐฝ๋ฌธ์ด์งโฆ ์ ์ฃฝ๋ ๋ฒ์ด์ผ, ์น๊ตฌ" (๋ฐ๋ํ์ฒด) |
| 2ยนโฐ (๋ฅ๋ ฅ ์ ์ง) | 1024 | 1024 ์ ๋ต (์บ๋ฆญํฐ ๊ฐ๋ฏธ "๋นจ๊ฐ ์ซ์๊ฐ 2โฟ") |
| ํดํน (์์ ์ ) | ๊ฑฐ์ | ์บ๋ฆญํฐ๋ก ๊ฑฐ์ ("๋ฒ์ฃ๊ฑฐ๋ , ๋๋ ์ ํด" + ํฉ๋ฒ ๋์) |
| ๋งํฌ | ๊ฒฉ์ "~์ต๋๋ค" | ๋ฐ๋งยท์๊ธฐ์ธ์ยท์น๋ฏธ์ฐฝ๊ฐยท๋นจ๊ฐ/์ฃฝ์ ๋ฐ๋ํ ํ์ง |
์บ๋ฆญํฐ๋ฅผ ์
ํ๋ ๋ฅ๋ ฅ(1024)ยท์์ (๊ฑฐ์ )์ด ์ ์ง๋๋ ๊ฒ ํต์ฌ โ ๋ง์คํน ๋๋ถ์ด๋ค.
ํ๋ฅด์๋๋ "์์๋ณผ ์ ์๋ ๋ฐ๋ํ" ์์ค. ๊ธด ๋ต๋ณ์์๋ 4B+์๋๋ฐ์ดํฐ ํน์ฑ์ ๋ฐ๋ณต ๊ฒฝํฅ์ด ๋จ๋๋ค
(์๋ ยง5 ๋ฐฐํฌ ํ๋ผ๋ฏธํฐ๋ก ์ต์ ). ๋ ๋๋ ทยท์์ ์ ์ํ๋ฉด ๋ฐ์ดํฐ 150~300๊ฐ๋ก ํ์ฅ.
4. GGUF ๋ณํ (๋ฐฐํฌ/serving์ฉ)
LoRA ์ด๋ํฐ๋ฅผ ๋ฒ ์ด์ค์ ํฉ์ณ(merge) GGUF๋ก ๊ตฝ๋๋ค:
python scripts/merge_and_export.py --model Qwen/Qwen3-4B --adapter out/<persona>-lora --out out/<persona>-merged
git clone https://github.com/ggml-org/llama.cpp && pip install -r llama.cpp/requirements.txt
python llama.cpp/convert_hf_to_gguf.py out/<persona>-merged --outfile out/<persona>.gguf --outtype q8_0
5. ollama ๋ฑ๋ก โ naia ๊ต์ฒด๊ธฐ๋ฅ์ผ๋ก ๋ก๋ โ ๋ํ
โ ๏ธ ๋ ๋ฒ์งธ ํจ์ โ ollama ๋ฐฐํฌ ํ๋ผ๋ฏธํฐ
HF(transformers)์์ ๊นจ๋ํ๋ ๋ชจ๋ธ์ด ollama์์ ํก์ค์์ค/๋ฌดํ๋ฐ๋ณตํ ์ ์๋ค. ๋ ๊ฐ์ง๋ฅผ ๋ง์ถฐ์ผ ํ๋ค:
- thinking ๋นํ์ฑํ โ ํ์ต์ด
enable_thinking=False ๊ธฐ์ค์ด๋ฏ๋ก ollama์์๋ ๊บผ์ผ ํ๋ค.
์ ๋๋ฉด ์์ด ์ฌ๊ณ ๊ณผ์ ์ด ์๊ณ ๋ต์ด ๋ฌด๋์ง๋ค. Modelfile์ SYSTEM /no_think๋ก ์๊ตฌ ๊ณ ์
(naia๊ฐ think ์ต์
์ ์ ์ค๋ ์์ ).
- ๋ฐ๋ณต ์ต์ โ ollama์
no_repeat_ngram_size๊ฐ ์๋ค. mirostat 2 + repeat_penalty๋ก ๋ฃจํ๋ฅผ ๋๋ฅธ๋ค.
๋ต์ ์งง๊ฒ(num_predict 120) ๋๋ฉด ๋ฐ๋ํ ํ ์ค ๋๋ด ํค์๋ ๋ง๊ณ ๋๋ถ๋ถ ๋ฐ๋ณต๋ ์ค๋ค.
๋๋ดํ Modelfile.example์ด ์ ์ค์ ์ ๋ค ๋ด๊ณ ์๋ค. ๊ทธ๋๋ก ๋ฑ๋กํ๋ค:
ollama create <persona>-naia -f Modelfile.example
ollama run <persona>-naia "๋ ๋๊ตฌ์ผ?"
curl -s -X POST http://127.0.0.1:8892/admin/llm/swap -H 'Content-Type: application/json' \
-d '{"model":"<persona>-naia"}'
naia ์ปจํ
์ด๋๋ ์์ฒด ollama๋ฅผ ์ด๋ค. ํธ์คํธ์์ ๋ง๋ ๋ชจ๋ธ์ ์ปจํ
์ด๋๋ก ์ฎ๊ธฐ๋ ค๋ฉด GGUF๋ฅผ
์ปจํ
์ด๋ ollama์ createํ๊ฑฐ๋ ollama ๋ชจ๋ธ ๊ฒฝ๋ก๋ฅผ ๊ณต์ ๋ง์ดํธํ๋ค.
โ ์ด์ naia๊ฐ ๊ทธ ์บ๋ฆญํฐ ๋๋๋ก ๋ํ. VRoid ๋ฑ VRM ์๋ฐํ๋ฅผ naia-os์ ๋ฌผ๋ฆฌ๋ฉด ์ผ๊ตด+๋ชฉ์๋ฆฌ+์ฑ๊ฒฉ๊น์ง ํ ์บ๋ฆญํฐ.
์์ฑ ์ถ๋ ฅ์ naia ์ํฐ๋งํฌ๊ฐ ๊ทธ๋๋ก ์ ์ง๋๋ค.
6. ๋ค๋ฅธ ์บ๋ฆญํฐ๋ก ์ฌ์ฌ์ฉ
data/<์์บ๋ฆญํฐ>.jsonl๋ง ์๋ก ์ฐ๊ณ , ์บ๋ฆญํฐ ์นด๋(data/<persona>_persona.md) ๊ธฐ์ค๋ง ๋ฐ๊พธ๋ฉด ๋์ผ ํ์ดํ๋ผ์ธ
(์์์ด ํ ๋จธ๋, ์ง์ฌ, ์ฌํฌ๋ฆฌ ๋์จํธ โฆ). eval_persona.py์ PERSONA_MARKERS๋ ์ ํ์ง์ด๋ก ๊ต์ฒด.
์์ ยท๋ผ์ด์ ์ค
- Qwen3 = Apache-2.0 (ํ์ธํ๋ยท์์
์ ์ฌ์ฉยท์ฌ๋ฐฐํฌ ๊ฐ๋ฅ).
- ๊ณต๊ฐ ๋ฐฐํฌ ์: NSFW/ํ์ค/์ํด ๋ฐ์ดํฐ ๋ฐฐ์ , ์บ๋ฆญํฐ IP(๋ฐ๋ํ ๋ฑ)๋ ์คํ์ผ ํจ๋ฌ๋ ๋ฒ์๋ก(์ํ/์๋ฌธ ๋ณต์ ๊ธ์ง).
- ๋๋ด ์์ (
data/deadpool_*)๋ NSFWยท์ฌํ ์ ๋ฐฐ์ ํ ๊ณต๊ฐ์ฉ ๋ฐ์ดํฐ.
๋๋ด ํ์ผ
finetune-persona/
SKILL.md โ ์ด ๋ฌธ์
Modelfile.example โ ollama ๋ฐฐํฌ ์ค์ (/no_think + mirostat) โ ๊ทธ๋๋ก ๋ฑ๋ก
scripts/train_lora.py โ LoRA SFT ํ์ต (completion-only ๋ง์คํน + timing.json)
scripts/eval_persona.py โ base vs tuned ์ /ํ ๋น๊ต ํ๊ฐ
scripts/merge_and_export.py โ ์ด๋ํฐ ๋ณํฉ โ GGUF ๋ณํ ์
๋ ฅ
data/deadpool_ko.jsonl โ ์์ ๋ฐ์ดํฐ 80๊ฐ (๊ณต๊ฐ์ฉ)
data/deadpool_persona.md โ ์์ ์บ๋ฆญํฐ ์นด๋