--- license: llama3.2 base_model: meta-llama/Llama-3.2-1B-Instruct library_name: peft pipeline_tag: text-generation tags: - lora - qlora - peft - emotion - valence-arousal - sentiment-analysis - journaling - gguf - llama-cpp language: - en --- # 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](https://github.com/meta-llama/llama-models/blob/main/models/llama3_2/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. ```python import torch from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig from peft import PeftModel BASE = "meta-llama/Llama-3.2-1B-Instruct" ADAPTER = "F20201316/Llama-3.2-1B-journal-mood-lora" SYSTEM_PROMPT = ( "You analyze the emotional content of a short piece of text. Respond with a single JSON " "object containing exactly these fields: \"valence\" (float, -1.0 to 1.0, negative to " "positive feeling), \"arousal\" (float, -1.0 to 1.0, calm to activated), and " "\"emotion_tags\" (a list of short lowercase words naming the emotion, empty list if none " "apply). No other text, just the JSON object." ) bnb = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, ) model = AutoModelForCausalLM.from_pretrained(BASE, quantization_config=bnb, device_map="auto") model = PeftModel.from_pretrained(model, ADAPTER) model.eval() tokenizer = AutoTokenizer.from_pretrained(ADAPTER) messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": "No work today, lucky me :)"}, ] encoded = tokenizer.apply_chat_template( messages, add_generation_prompt=True, return_tensors="pt", return_dict=True ).to(model.device) with torch.no_grad(): out = model.generate(**encoded, max_new_tokens=80, do_sample=False, pad_token_id=tokenizer.pad_token_id) print(tokenizer.decode(out[0][encoded["input_ids"].shape[1]:], skip_special_tokens=True)) # {"valence": 0.38, "arousal": -0.62, "emotion_tags": []} ``` ## Usage — GGUF / llama.cpp (CPU, no GPU required) ```python from huggingface_hub import hf_hub_download from llama_cpp import Llama model_path = hf_hub_download( repo_id="F20201316/Llama-3.2-1B-journal-mood-lora", filename="gguf/ft-jrn-Q4_K_M.gguf", ) llm = Llama(model_path=model_path, n_ctx=2048, verbose=False) SYSTEM_PROMPT = ( "You analyze the emotional content of a short piece of text. Respond with a single JSON " "object containing exactly these fields: \"valence\" (float, -1.0 to 1.0, negative to " "positive feeling), \"arousal\" (float, -1.0 to 1.0, calm to activated), and " "\"emotion_tags\" (a list of short lowercase words naming the emotion, empty list if none " "apply). No other text, just the JSON object." ) out = llm.create_chat_completion( messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": "No work today, lucky me :)"}, ], temperature=0.0, max_tokens=80, ) print(out["choices"][0]["message"]["content"]) # {"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): ```python from huggingface_hub import hf_hub_download from llama_cpp import LlamaGrammar grammar_path = hf_hub_download( repo_id="F20201316/Llama-3.2-1B-journal-mood-lora", filename="gguf/grammar.gbnf", ) grammar = LlamaGrammar.from_string(open(grammar_path).read(), verbose=False) out = llm.create_chat_completion( messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": "No work today, lucky me :)"}, ], grammar=grammar, temperature=0.0, max_tokens=80, ) print(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](https://github.com/JULIELab/EmoBank) | 10,062 | Human-annotated VA, rescaled 1–5 → −1..1 | | [Facebook VA study](https://github.com/wwbp/additional_data_sets) | 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](https://aclanthology.org/E17-2092/) - Preoţiuc-Pietro et al. (2016), *Modelling Valence and Arousal in Facebook Posts* — [WASSA 2016](https://aclanthology.org/W16-0404/) The derived training set is not redistributed here; please obtain the source corpora from their original repositories under their respective terms.