|
| 1 | +# Design Spec: Fine-Tuning for Student Explainability |
| 2 | + |
| 3 | +**Date:** 2026-04-02 |
| 4 | +**Epic label:** `fine-tuning: student-explainability` |
| 5 | +**Epic branch:** `fine-tuning/student-explainability` |
| 6 | +**Status:** Draft |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## 1. Goal |
| 11 | + |
| 12 | +Fine-tune a small language model (Qwen 3.5) on Bishop State domain data to replace GPT-4o-mini for three inference tasks in the dashboard. The primary value is improved explainability: advisors get SHAP-grounded, institution-aware narratives instead of templated rule-engine output. Secondary benefits include FERPA compliance (all inference on-premises), offline deployment, and institutional scalability. |
| 13 | + |
| 14 | +### Tasks to Fine-Tune |
| 15 | + |
| 16 | +| Task | Input | Output | Priority | |
| 17 | +|------|-------|--------|----------| |
| 18 | +| **SHAP Narrator** | SHAP values + student profile + risk factors | Grounded advisor narrative + interventions | Highest (new) | |
| 19 | +| **Summarizer** | Query results + original question | Plain-English summary for advisors | Medium (exists) | |
| 20 | +| **Explainer** | Course pairing stats (DFWI, delivery, instructor) | Data-driven analysis + recommendation | Medium (exists) | |
| 21 | + |
| 22 | +### Out of Scope |
| 23 | + |
| 24 | +- Query Analyzer (NL → SQL) — high risk, deferred to future epic |
| 25 | +- Model serving infrastructure (RunPod, dedicated GPU hosting) — use local Ollama for now |
| 26 | + |
| 27 | +## 2. Prerequisites |
| 28 | + |
| 29 | +Before the epic branch is created: |
| 30 | + |
| 31 | +1. **Merge `feature/distillation-pipeline` → `main`** — brings in `training/` pipeline modules, `schools/bishop-state/config.yaml`, seed queries, `model-client.ts` |
| 32 | +2. **Merge `feature/shap-explainability` → `main`** — brings in per-student SHAP computation (Step 10b), SHAP-aware `enrich_with_llm()`, student API SHAP exposure, feasibility report |
| 33 | + |
| 34 | +## 3. Epic Structure |
| 35 | + |
| 36 | +### Branching |
| 37 | + |
| 38 | +- **Epic branch:** `fine-tuning/student-explainability` (from `main` after prereq merges) |
| 39 | +- **Feature branches:** `fine-tuning/issue-N-description` → PR into epic branch |
| 40 | +- **Final PR:** epic branch → `main` |
| 41 | + |
| 42 | +### Issue Breakdown |
| 43 | + |
| 44 | +``` |
| 45 | + +---------------+ |
| 46 | + | #1 Prereq: | |
| 47 | + | Merge both | |
| 48 | + | branches | |
| 49 | + +-------+-------+ |
| 50 | + | |
| 51 | + +------------+------------+ |
| 52 | + v v v |
| 53 | + +----------+ +----------+ +----------+ |
| 54 | + | #2 SHAP | | #3 Colab | | #4 Distill| |
| 55 | + | narrator | | notebook | | summarizer| |
| 56 | + | task type| | (Unsloth)| | + explain | |
| 57 | + +----+-----+ +----+-----+ +----+-----+ |
| 58 | + | | | |
| 59 | + v | | |
| 60 | + +----------+ | | |
| 61 | + | #5 Distill| | | |
| 62 | + | SHAP | | | |
| 63 | + | narrator | | | |
| 64 | + +----+-----+ | | |
| 65 | + | | | |
| 66 | + +-------------+------------+ |
| 67 | + v |
| 68 | + +----------+ |
| 69 | + | #6 Train | |
| 70 | + | 4B + 9B | |
| 71 | + | evaluate | |
| 72 | + +----+-----+ |
| 73 | + | |
| 74 | + +----+-----+ |
| 75 | + v v |
| 76 | + +----------+ +----------+ |
| 77 | + | #7 Export | | #8 Update| |
| 78 | + | + wire | | docs & | |
| 79 | + | dashboard| | report | |
| 80 | + +----------+ +----------+ |
| 81 | +``` |
| 82 | + |
| 83 | +| # | Title | Description | Depends | Labels | |
| 84 | +|---|-------|------------|---------|--------| |
| 85 | +| 1 | Merge distillation-pipeline and shap-explainability to main | Merge both feature branches, resolve conflicts, verify CI | — | `type:chore` | |
| 86 | +| 2 | Add SHAP narrator task type to training pipeline | New prompt template, output schema, seed data generator, eval metrics | #1 | `type:feature`, `area:ai` | |
| 87 | +| 3 | Build Colab training notebook (Unsloth + LoRA) | Single "Run All" notebook, parameterized config, 3-phase training, GGUF export. Replace `training/finetune.py` (MLX) with Unsloth wrapper. | #1 | `type:feature`, `area:ai` | |
| 88 | +| 4 | Distill training pairs for summarizer and explainer | Run distillation for both existing tasks (~1,500 pairs each via Claude API). Prepare datasets. | #1 | `type:feature`, `area:ai` | |
| 89 | +| 5 | Distill training pairs for SHAP narrator | Generate ~1,500 SHAP narrator pairs from student data + SHAP values. Requires SHAP data in DB. | #2 | `type:feature`, `area:ai` | |
| 90 | +| 6 | Train and evaluate 4B + 9B models | Run Colab notebook for both model sizes. Evaluate via ship criteria. Compare metrics, pick winner. | #3, #4, #5 | `type:spike`, `area:ai` | |
| 91 | +| 7 | Export models and wire into dashboard | GGUF export, Ollama registration, wire `model-client.ts` into consumer routes, update `enrich_with_llm` model string. | #6 | `type:feature`, `area:ai`, `area:frontend` | |
| 92 | +| 8 | Update documentation and feasibility report | Update feasibility report with actual results, update README and CLAUDE.md. | #6 | `type:documentation` | |
| 93 | + |
| 94 | +### Parallelism |
| 95 | + |
| 96 | +Issues #2, #3, and #4 can proceed concurrently after #1. Issue #5 waits only on #2. Issue #6 is the convergence point. Issues #7 and #8 are parallel after #6. |
| 97 | + |
| 98 | +## 4. Colab Notebook Design |
| 99 | + |
| 100 | +### Principles |
| 101 | + |
| 102 | +- **Single "Run All" execution.** No babysitting. No manual cell-by-cell. |
| 103 | +- **Parameterized at the top.** One config cell is the only thing the user edits. |
| 104 | +- **Checkpoint and resume.** If Colab disconnects, set `SKIP_DOMAIN_ADAPTATION=True` to resume from Phase 2. |
| 105 | +- **Chat template alignment.** Uses `tokenizer.apply_chat_template()` throughout — never manual ChatML tokenization (D4BL's critical lesson). |
| 106 | + |
| 107 | +### Notebook Structure |
| 108 | + |
| 109 | +``` |
| 110 | +Cell 1: Configuration (ONLY cell the user edits) |
| 111 | +------------------------------------------------- |
| 112 | +SCHOOL = "bishop-state" |
| 113 | +MODEL_SIZES = ["4b", "9b"] |
| 114 | +REPO_URL = "https://github.com/codebenders/datathon.git" |
| 115 | +REPO_BRANCH = "fine-tuning/student-explainability" |
| 116 | +HF_TOKEN = "" # or userdata.get('HF_TOKEN') |
| 117 | +PHASE_1_EPOCHS = 1 |
| 118 | +PHASE_2_EPOCHS = 7 |
| 119 | +SKIP_DOMAIN_ADAPTATION = False # True to reuse cached Phase 1 |
| 120 | +
|
| 121 | +Cell 2+: Fully autonomous |
| 122 | +------------------------------------------------- |
| 123 | +- GPU detection + validation (assert A100/T4/L4) |
| 124 | +- pip install unsloth, trl, peft |
| 125 | +- Clone repo, load schools/{SCHOOL}/config.yaml |
| 126 | +- For each model size: |
| 127 | + - Phase 1: Domain adaptation |
| 128 | + - Load base Qwen model via Unsloth (4-bit NF4) |
| 129 | + - Train on training_data/{school}/domain.jsonl |
| 130 | + - LoRA rank 16, all modules, 1 epoch, lr 2e-4, effective batch 32 |
| 131 | + - Save merged checkpoint |
| 132 | + - Phase 2: Task adapters (narrator, summarizer, explainer) |
| 133 | + - Load Phase 1 checkpoint |
| 134 | + - Train LoRA adapter per task |
| 135 | + - Eval after each task, print ship-criteria table |
| 136 | + - Narrator: LoRA r=16, attention+FFN, 7 epochs, lr 1e-4 |
| 137 | + - Summarizer: LoRA r=8, attention only, 7 epochs, lr 1e-4 |
| 138 | + - Explainer: LoRA r=16, attention+FFN, 4 epochs, lr 1e-4 |
| 139 | + - Phase 3: GGUF export |
| 140 | + - Quantize each task adapter to q4_k_m |
| 141 | + - Upload to Google Drive (or HF Hub if HF_TOKEN provided) |
| 142 | +- Print comparison table: 4B vs 9B metrics across all tasks |
| 143 | +- Recommend winner based on ship criteria |
| 144 | +``` |
| 145 | + |
| 146 | +### Training Hyperparameters |
| 147 | + |
| 148 | +Based on D4BL's proven configurations: |
| 149 | + |
| 150 | +| Parameter | Phase 1 (Domain) | Phase 2 (Tasks) | |
| 151 | +|-----------|------------------|-----------------| |
| 152 | +| LoRA rank | 16 | 8-16 (task-dependent) | |
| 153 | +| LoRA alpha | 32 | 16-32 | |
| 154 | +| Learning rate | 2e-4 | 1e-4 | |
| 155 | +| Batch size (per device) | 8 | 4-8 | |
| 156 | +| Gradient accumulation | 4 | 2-4 | |
| 157 | +| Epochs | 1 | 4-7 | |
| 158 | +| Max sequence length | 4096 | 4096-8192 | |
| 159 | +| Optimizer | AdamW 8-bit | AdamW 8-bit | |
| 160 | +| Precision | bf16 (A100) | bf16 (A100) | |
| 161 | + |
| 162 | +### What the Notebook Does NOT Do |
| 163 | + |
| 164 | +- Does not run distillation (that's local via `python -m training.distill`) |
| 165 | +- Does not register Ollama models (local after downloading GGUFs) |
| 166 | +- Does not modify the repo (read-only clone for config + training data) |
| 167 | + |
| 168 | +## 5. SHAP Narrator Task Design |
| 169 | + |
| 170 | +### New Task Type: `narrator` |
| 171 | + |
| 172 | +This is the highest-value task — it transforms per-student SHAP attribution data into advisor-facing narratives that explain *why* a student is at risk and *what specifically to do about it*. |
| 173 | + |
| 174 | +### Input Format (at inference) |
| 175 | + |
| 176 | +```json |
| 177 | +{ |
| 178 | + "student_profile": { |
| 179 | + "enrollment_intensity": "Part-Time", |
| 180 | + "gpa_year1": 1.4, |
| 181 | + "math_placement": "R", |
| 182 | + "course_completion_rate": 0.55, |
| 183 | + "gateway_math_completed": false, |
| 184 | + "at_risk_alert": "HIGH", |
| 185 | + "retention_probability": 0.28 |
| 186 | + }, |
| 187 | + "readiness_score": 0.38, |
| 188 | + "readiness_level": "low", |
| 189 | + "risk_factors": [ |
| 190 | + "Low first-year GPA (1.4 / 4.0)", |
| 191 | + "Gateway math not completed in Year 1" |
| 192 | + ], |
| 193 | + "shap": { |
| 194 | + "retention": { |
| 195 | + "base_value": 0.52, |
| 196 | + "top_positive": [ |
| 197 | + {"feature": "total_credits_attempted", "shap_value": 0.05, "value": 12.0} |
| 198 | + ], |
| 199 | + "top_negative": [ |
| 200 | + {"feature": "CompletedGatewayMathYear1", "shap_value": -0.18, "value": 0.0}, |
| 201 | + {"feature": "Enrollment_Intensity_First_Term", "shap_value": -0.12, "value": 1.0} |
| 202 | + ] |
| 203 | + }, |
| 204 | + "gateway_math": { ... }, |
| 205 | + "low_gpa": { ... } |
| 206 | + } |
| 207 | +} |
| 208 | +``` |
| 209 | + |
| 210 | +### Output Schema |
| 211 | + |
| 212 | +```json |
| 213 | +{ |
| 214 | + "narrative": "2-3 sentence explanation grounded in SHAP attribution", |
| 215 | + "key_drivers": [ |
| 216 | + "Gateway math not completed (-0.18 on retention)", |
| 217 | + "Part-time enrollment (-0.12 on retention)" |
| 218 | + ], |
| 219 | + "recommended_actions": [ |
| 220 | + "Priority enrollment in MAT 100 next term", |
| 221 | + "Explore full-time enrollment options and financial aid", |
| 222 | + "Connect with Math Bootcamp (2x pass rate for participants)" |
| 223 | + ], |
| 224 | + "data_limitations": [ |
| 225 | + "Retention model trained on 2019-2023 cohorts; 2024+ patterns may differ" |
| 226 | + ] |
| 227 | +} |
| 228 | +``` |
| 229 | + |
| 230 | +### Distillation Strategy |
| 231 | + |
| 232 | +1. Pull ~4K students from `student_level_with_predictions` joined with `llm_recommendations` |
| 233 | +2. For each medium/low readiness student (~2K): build input from `shap_explanations` + `input_features` columns |
| 234 | +3. Send to Claude (teacher model) with system prompt grounded in Bishop State context from `config.yaml` |
| 235 | +4. Validate output JSON schema, deduplicate (Jaccard 1.0), split 80/10/10 |
| 236 | +5. Target: ~1,500 validated training pairs |
| 237 | + |
| 238 | +### Eval Metrics (Ship Criteria) |
| 239 | + |
| 240 | +| Metric | Threshold | Blocking? | |
| 241 | +|--------|-----------|-----------| |
| 242 | +| `json_valid_rate` | >= 95% | Yes | |
| 243 | +| `schema_valid_rate` | >= 90% | Yes | |
| 244 | +| `shap_grounding_rate` | >= 80% (narrative mentions >= 2 of top-3 SHAP features) | Yes | |
| 245 | +| `action_specificity` | LLM-judged: are actions Bishop State-specific? | No | |
| 246 | + |
| 247 | +## 6. Dashboard Integration |
| 248 | + |
| 249 | +### Model Client as Single Adapter |
| 250 | + |
| 251 | +`model-client.ts` becomes the sole inference routing layer. Existing routes (`explain-pairing/route.ts`, `query-summary/route.ts`) that currently instantiate their own OpenAI clients will be refactored to call `generateExplanation()` and `generateSummary()` from `model-client.ts`. |
| 252 | + |
| 253 | +### Ollama Model Naming |
| 254 | + |
| 255 | +``` |
| 256 | +bishop-state-narrator:{size} # SHAP narrator |
| 257 | +bishop-state-summarizer:{size} # Query summary |
| 258 | +bishop-state-explainer:{size} # Course pairing |
| 259 | +``` |
| 260 | + |
| 261 | +Where `{size}` is `4b` or `9b` based on evaluation results. |
| 262 | + |
| 263 | +### SHAP Narrator Integration Point |
| 264 | + |
| 265 | +`generate_readiness_scores.py` already has `--enrich-with-llm` with the SHAP-aware prompt. The only change is the model string: |
| 266 | + |
| 267 | +```bash |
| 268 | +# Before (OpenAI) |
| 269 | +python ai_model/generate_readiness_scores.py --enrich-with-llm --llm-model gpt-4o-mini |
| 270 | + |
| 271 | +# After (fine-tuned) |
| 272 | +python ai_model/generate_readiness_scores.py --enrich-with-llm --llm-model ollama/bishop-state-narrator:4b |
| 273 | +``` |
| 274 | + |
| 275 | +### Environment Variables |
| 276 | + |
| 277 | +```env |
| 278 | +MODEL_BACKEND=ollama # or "openai" (fallback) |
| 279 | +OLLAMA_BASE_URL=http://localhost:11434 |
| 280 | +MODEL_SIZE=4b # set after evaluation picks winner |
| 281 | +SCHOOL_CODE=bishop-state |
| 282 | +``` |
| 283 | + |
| 284 | +### Fallback Behavior |
| 285 | + |
| 286 | +The operator sets `MODEL_BACKEND` to either `ollama` or `openai`. There is no automatic failover — if Ollama is down and `MODEL_BACKEND=ollama`, the route returns an error. This is intentional: silent fallback to OpenAI would send student data to an external service without the operator's knowledge, violating the FERPA benefit. |
| 287 | + |
| 288 | +## 7. Cost Estimate |
| 289 | + |
| 290 | +| Item | Cost | |
| 291 | +|------|------| |
| 292 | +| Claude API distillation (~4,500 pairs across 3 tasks) | $5-10 | |
| 293 | +| Colab A100 compute (~4 hours for 2 model sizes) | $8-16 | |
| 294 | +| **Total per training run** | **$13-26** | |
| 295 | +| Iteration runs (subsequent) | $8-16 each | |
| 296 | + |
| 297 | +## 8. Success Criteria |
| 298 | + |
| 299 | +The epic is complete when: |
| 300 | + |
| 301 | +1. All three tasks pass ship criteria on the winning model size |
| 302 | +2. `MODEL_BACKEND=ollama` serves all three tasks in the dashboard without OpenAI |
| 303 | +3. SHAP narrator produces grounded narratives that cite specific feature attributions |
| 304 | +4. Feasibility report is updated with actual metrics and model selection rationale |
| 305 | +5. Colab notebook is documented and reproducible (clone + Run All) |
0 commit comments