Llama-3.2-1B-journal-mood-lora
A QLoRA adapter that turns Llama 3.2 1B Instruct into a structured
valence/arousal extractor: given a short piece of text, it returns a single JSON
object scoring emotional valence (negative ↔ positive) and arousal
(calm ↔ activated) on a −1.0 to 1.0 scale.
Built as the mood-analysis component of a journaling assistant, where the design
rule is that the model converts language into structure and deterministic code
owns everything else (arithmetic, storage, safety).
Built with Llama. This adapter is a derivative of Meta's Llama 3.2 and is
governed by the
Llama 3.2 Community License.
Available formats
| File | Format | Size | Use with |
|---|
adapter_model.safetensors | PEFT LoRA adapter | 22 MB | transformers + peft, GPU (4-bit bnb) |
gguf/ft-jrn-f16.gguf | Merged model, GGUF f16 | 2.36 GB | llama.cpp / llama-cpp-python |
gguf/ft-jrn-Q4_K_M.gguf | Merged model, GGUF Q4_K_M | 770 MB | llama.cpp / llama-cpp-python, CPU/edge |
gguf/grammar.gbnf | GBNF grammar | 1 KB | Grammar-constrained decoding (see below) |
The GGUF files are the LoRA adapter merged into the base weights and
quantized — a standalone model, no separate base-model download needed. Use
them for CPU/edge deployment; use the PEFT adapter for GPU serving.
Results
Evaluated on 30 held-out examples with human-annotated ground truth (not
model-generated), greedy decoding, comparing the original PyTorch/bnb-4bit
adapter against the GGUF Q4_K_M quantization:
| Metric | PyTorch (GPU, bnb 4-bit) | GGUF Q4_K_M (CPU) |
|---|
| Valid JSON rate | 100% (30/30) | 100% (30/30) |
| Valence MAE | 0.068 | 0.069 |
| Arousal MAE | 0.102 | 0.103 |
| Valence correlation (Pearson r) | 0.82 | 0.86 |
| Arousal correlation (Pearson r) | 0.86 | 0.88 |
Quantizing to Q4_K_M (2.36 GB → 770 MB, ~3.1x smaller) cost essentially nothing
on accuracy — the small deltas above are within sampler/harness noise, not a
real regression or improvement. Grammar-constrained decoding (see below) adds
~1.7x latency (751ms vs 430ms/request on CPU) in exchange for a structural
guarantee of schema-valid output, independent of prompt adherence.
Larger sanity check, 300 examples from the training set itself (Facebook
VA study subset — this is not held-out, so treat it as a check that the
model learned the task, not a generalization measurement):
| Metric | PyTorch (GPU, bnb 4-bit) | GGUF unconstrained (CPU) | GGUF constrained (CPU) |
|---|
| Valid JSON rate | 100% | 100% | 100% |
| Valence MAE | 0.065 | 0.083 | 0.083 |
| Arousal MAE | 0.118 | 0.133 | 0.136 |
| Valence corr (r) | 0.926 | 0.928 | 0.928 |
| Arousal corr (r) | 0.926 | 0.920 | 0.916 |
Correlations are higher here (r≈0.92-0.93) than on the held-out set above
(r≈0.82-0.88), as expected for seen-during-training data. Both models degrade
similarly on held-out data, so the quantization isn't introducing a training/
inference mismatch of its own.
Training-time metrics: train_loss 0.1827, eval_loss 0.1906,
eval_mean_token_accuracy 0.9348 (500-example validation split).
The correlation figures are against human annotations from EmoBank and the
Facebook valence/arousal study, so they measure agreement with human judgment
rather than agreement with a teacher model.
Usage — PyTorch / PEFT (GPU)
The system prompt below is baked into every training example and should be used
verbatim — output quality degrades noticeably with a different prompt.
1import torch
2from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
3from peft import PeftModel
4
5BASE = "meta-llama/Llama-3.2-1B-Instruct"
6ADAPTER = "F20201316/Llama-3.2-1B-journal-mood-lora"
7
8SYSTEM_PROMPT = (
9 "You analyze the emotional content of a short piece of text. Respond with a single JSON "
10 "object containing exactly these fields: \"valence\" (float, -1.0 to 1.0, negative to "
11 "positive feeling), \"arousal\" (float, -1.0 to 1.0, calm to activated), and "
12 "\"emotion_tags\" (a list of short lowercase words naming the emotion, empty list if none "
13 "apply). No other text, just the JSON object."
14)
15
16bnb = BitsAndBytesConfig(
17 load_in_4bit=True,
18 bnb_4bit_quant_type="nf4",
19 bnb_4bit_compute_dtype=torch.bfloat16,
20 bnb_4bit_use_double_quant=True,
21)
22
23model = AutoModelForCausalLM.from_pretrained(BASE, quantization_config=bnb, device_map="auto")
24model = PeftModel.from_pretrained(model, ADAPTER)
25model.eval()
26tokenizer = AutoTokenizer.from_pretrained(ADAPTER)
27
28messages = [
29 {"role": "system", "content": SYSTEM_PROMPT},
30 {"role": "user", "content": "No work today, lucky me :)"},
31]
32encoded = tokenizer.apply_chat_template(
33 messages, add_generation_prompt=True, return_tensors="pt", return_dict=True
34).to(model.device)
35
36with torch.no_grad():
37 out = model.generate(**encoded, max_new_tokens=80, do_sample=False,
38 pad_token_id=tokenizer.pad_token_id)
39
40print(tokenizer.decode(out[0][encoded["input_ids"].shape[1]:], skip_special_tokens=True))
41# {"valence": 0.38, "arousal": -0.62, "emotion_tags": []}
Usage — GGUF / llama.cpp (CPU, no GPU required)
1from huggingface_hub import hf_hub_download
2from llama_cpp import Llama
3
4model_path = hf_hub_download(
5 repo_id="F20201316/Llama-3.2-1B-journal-mood-lora",
6 filename="gguf/ft-jrn-Q4_K_M.gguf",
7)
8llm = Llama(model_path=model_path, n_ctx=2048, verbose=False)
9
10SYSTEM_PROMPT = (
11 "You analyze the emotional content of a short piece of text. Respond with a single JSON "
12 "object containing exactly these fields: \"valence\" (float, -1.0 to 1.0, negative to "
13 "positive feeling), \"arousal\" (float, -1.0 to 1.0, calm to activated), and "
14 "\"emotion_tags\" (a list of short lowercase words naming the emotion, empty list if none "
15 "apply). No other text, just the JSON object."
16)
17
18out = llm.create_chat_completion(
19 messages=[
20 {"role": "system", "content": SYSTEM_PROMPT},
21 {"role": "user", "content": "No work today, lucky me :)"},
22 ],
23 temperature=0.0,
24 max_tokens=80,
25)
26print(out["choices"][0]["message"]["content"])
27# {"valence": 0.38, "arousal": -0.62, "emotion_tags": []}
Grammar-constrained decoding
gguf/grammar.gbnf forces the output to structurally match the JSON schema at
the token level, so it can't emit malformed JSON even on adversarial or
out-of-distribution input (unlike prompting alone, which is a request, not a
guarantee):
1from huggingface_hub import hf_hub_download
2from llama_cpp import LlamaGrammar
3
4grammar_path = hf_hub_download(
5 repo_id="F20201316/Llama-3.2-1B-journal-mood-lora",
6 filename="gguf/grammar.gbnf",
7)
8grammar = LlamaGrammar.from_string(open(grammar_path).read(), verbose=False)
9
10out = llm.create_chat_completion(
11 messages=[
12 {"role": "system", "content": SYSTEM_PROMPT},
13 {"role": "user", "content": "No work today, lucky me :)"},
14 ],
15 grammar=grammar,
16 temperature=0.0,
17 max_tokens=80,
18)
19print(out["choices"][0]["message"]["content"])
Limitations
emotion_tags is effectively non-functional. Only ~1% of training examples
(120 of 12,319) carried non-empty tags — the public corpora supply valence and
arousal but no categorical labels. The model therefore returns an empty list
almost always. The field is preserved for schema stability; do not rely on it.
Valence and arousal are the working outputs.
Domain mismatch with personal journaling. Training text is predominantly news
sentences, fiction, and public social-media posts. It has not been validated on
private diary-style writing, which is the intended downstream use.
Sarcasm and informal tone are weak spots. Observed in evaluation: heavily
punctuated or ironic messages drew flatter predictions than the human annotation
(e.g. "We tied South but soooo should hav won!!!!!" — human −0.25 valence,
predicted 0.00). This is an expected failure mode at 1B parameters.
Predictions cluster toward the center. Scores tend to be conservative on
strongly-worded text, which suppresses extremes.
Not a clinical or diagnostic tool. This produces coarse affect estimates from
text. It is not a mental-health assessment, must not be used as one, and should
not drive decisions about anyone's care.
Training
QLoRA fine-tune on a single RTX 5060 Laptop GPU (8GB), ~91 minutes.
| |
|---|
| Base model | meta-llama/Llama-3.2-1B-Instruct |
| Quantization | 4-bit NF4, double quant, bf16 compute |
| LoRA rank / alpha / dropout | 16 / 32 / 0.05 |
| Target modules | q_proj k_proj v_proj o_proj gate_proj up_proj down_proj |
| Learning rate / schedule | 2e-4, cosine, 3% warmup |
| Effective batch size | 32 (2 × 16 gradient accumulation) |
| Epochs / steps | 3 / 1,155 |
| Loss | completion-only (prompt tokens masked) |
The GGUF files were produced afterward by merging the adapter into the base
weights (peft merge_and_unload) and quantizing with llama.cpp's converter
and llama-cpp-python's low-level quantization API (Q4_K_M, K-quant).
Training data
12,319 train / 500 validation examples, deduplicated by normalized text:
| Source | Examples | Notes |
|---|
| EmoBank | 10,062 | Human-annotated VA, rescaled 1–5 → −1..1 |
| Facebook VA study | 2,894 | Two annotators averaged, rescaled 1–9 → −1..1 |
| Synthetic | 125 | Gemini-generated journaling-style examples |
Please cite the original dataset authors if you build on this:
- Buechel & Hahn (2017), EmoBank: Studying the Impact of Annotation Perspective
and Representation Format on Dimensional Emotion Analysis — EACL 2017
- Preoţiuc-Pietro et al. (2016), Modelling Valence and Arousal in Facebook
Posts — WASSA 2016
The derived training set is not redistributed here; please obtain the source
corpora from their original repositories under their respective terms.