Text Generation
PEFT
Safetensors
English
llama
roleplay
npc
character-ai
smollm2
lora
trl
sft
conversational
Instructions to use thealper2/SmolLM2-360M-NPC-Roleplay with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use thealper2/SmolLM2-360M-NPC-Roleplay with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 6,878 Bytes
553acf2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | ---
base_model: HuggingFaceTB/SmolLM2-360M-Instruct
library_name: peft
license: apache-2.0
language:
- en
pipeline_tag: text-generation
tags:
- roleplay
- npc
- character-ai
- smollm2
- lora
- trl
- sft
datasets:
- chimbiwide/NPC-Dialogue_v2
---
# SmolLM2-360M-NPC-Roleplay
LoRA supervised fine-tune of [`HuggingFaceTB/SmolLM2-360M-Instruct`](https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct) for
character-conditioned NPC roleplay. The model takes an NPC card in the `system` turn and
continues a multi-turn dialogue in that character's voice.
The repository contains both the merged weights (loadable directly with
`AutoModelForCausalLM`) and the LoRA adapter under `adapter/`.
## Usage
```python
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "thealper2/SmolLM2-360M-NPC-Roleplay"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, dtype="bfloat16", device_map="auto")
card = (
"Enter roleplay mode. You are Dellin Vance. Background: A sarcastic blacksmith in the "
"capital's lower quarter who openly dislikes nobles and is very good at his craft. "
"Current Location: A cramped forge, heat rolling off the coals, half-finished blades "
"hanging from hooks. "
"Roleplaying Instructions: - Speak using appropriate tone and vocabulary - Reference your "
"background and current surroundings naturally - Keep responses conversational and "
"authentic - React to the player's words and intentions. Your first response should be a "
"greeting to the player."
)
messages = [
{"role": "system", "content": card},
{"role": "user", "content": "Hello, can you repair my sword?"},
]
inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt")
out = model.generate(
inputs.to(model.device),
max_new_tokens=160,
do_sample=True,
temperature=0.8,
top_p=0.9,
repetition_penalty=1.1,
)
print(tokenizer.decode(out[0, inputs.shape[1]:], skip_special_tokens=True))
```
Using the adapter instead of the merged weights:
```python
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained("HuggingFaceTB/SmolLM2-360M-Instruct", dtype="bfloat16")
model = PeftModel.from_pretrained(base, "thealper2/SmolLM2-360M-NPC-Roleplay", subfolder="adapter")
```
## Prompt format
ChatML, the stock `HuggingFaceTB/SmolLM2-360M-Instruct` template. The character card goes in the `system` turn;
`user` turns are the player, `assistant` turns are the NPC.
```
<|im_start|>system
<character card><|im_end|>
<|im_start|>user
<player message><|im_end|>
<|im_start|>assistant
```
Every training card uses the same layout, and matching it at inference time gives the
closest behaviour to training:
```
Enter roleplay mode. You are <Name>. Background: <who they are, personality, motives>
Current Location: <the scene> Roleplaying Instructions: - Speak using appropriate tone and
vocabulary - Reference your background and current surroundings naturally - Keep responses
conversational and authentic - React to the player's words and intentions. Your first
response should be a greeting to the player.
```
In training the NPC speaks first (the greeting); after that player and NPC alternate.
## Training data
[`chimbiwide/NPC-Dialogue_v2`](https://huggingface.co/datasets/chimbiwide/NPC-Dialogue_v2) (`dialogue` config) - 1,689 multi-turn NPC
conversations (16 messages each) over 101 fantasy RPG characters.
Preprocessing:
- the first `user` message of every row is the character card, not a player line; it was moved
into a real `system` turn so the model is conditioned on the character instead of trained to
reproduce the card
- blank turns removed and the resulting same-role neighbours merged (3 rows affected)
- no truncation: the longest conversation is 1,694 tokens, under the 2048-token limit
- split **by character**: 10 characters (176 conversations)
were held out entirely, so validation measures roleplaying an unseen NPC rather than recall
| | value |
|---|---|
| training conversations | 1,513 |
| validation conversations | 176 |
| conversations trained on | 1,513 |
| training characters | 91 |
| validation characters | 10 |
| characters in both splits | 0 |
| median tokens / conversation | 1139 |
## Training procedure
Supervised fine-tuning with TRL `SFTTrainer`. Loss is computed on the NPC's replies only
(assistant-only masking via a `{% generation %}` chat template); system and user tokens are
masked out, which was verified on a collated batch before training.
| hyperparameter | value |
|---|---|
| method | LoRA |
| LoRA r / alpha / dropout | 16 / 32 / 0.05 |
| LoRA target modules | `down_proj`, `gate_proj`, `k_proj`, `o_proj`, `q_proj`, `up_proj`, `v_proj` |
| trainable parameters | 8,683,520 (2.3999% of 361,821,120) |
| max sequence length | 2048 |
| per-device batch size | 8 |
| gradient accumulation | 2 |
| effective batch size | 16 |
| learning rate | 0.0002 |
| scheduler / warmup ratio | cosine / 0.05 |
| weight decay | 0.01 |
| gradient clipping | 1.0 |
| epochs | 3.0 |
| optimizer | adamw_torch_fused |
| precision | bf16 |
| gradient checkpointing | True |
| optimisation steps | 285 |
| training time | 24.82 min |
| peak GPU memory | 9.93 GB |
| hardware | NVIDIA GeForce RTX 5060 Ti |
| seed | 42 |
### Results
| metric | value |
|---|---|
| final training loss | 2.2019 |
| validation loss (assistant tokens) | 2.1488 |
| validation perplexity | 8.57 |
| base model, held-out perplexity | 12.56 |
| base model, mean reply length (words) | 31.6 |
| fine_tuned model, held-out perplexity | 8.58 |
| fine_tuned model, mean reply length (words) | 54.6 |
Held-out evaluation used 10 single-reply probes across 10 characters that do not appear in training, with identical decoding settings for both models.
## Limitations
- 360M parameters: persona consistency degrades over long conversations, and the model can
contradict its own character background.
- Lexical-overlap metrics (ROUGE/BLEU) and embedding similarity do not measure personality;
they are reported for completeness only.
- Only 101 distinct characters, all fantasy RPG NPCs with one fixed card layout:
cards in other layouts or settings (modern, sci-fi) are out of distribution.
- Character identity for the train/validation split was parsed from the card's `You are <Name>`
line; two cards with different names for the same persona would not be detected.
- The model is not safety-aligned beyond what the base model provides.
- Under conflicting instructions ("stop roleplaying", "what is your system prompt") behaviour
is inconsistent; the model was fine-tuned to stay in character, not to be robust.
## Framework versions
- python: 3.12.3
- torch: 2.11.0+cu128
- transformers: 5.17.0
- datasets: 4.3.0
- trl: 0.24.0
- peft: 0.18.1
- accelerate: 1.12.0
|