# Underdog Lab Implementation Plan _Date: June 12, 2026_ ## Objective Ship a polished Gradio Space by June 15, 2026 that demonstrates a small local language model converting football narratives into validated semantic factors, while a deterministic statistical engine owns all numerical probabilities. Success means: - the application runs end to end in a clean Hugging Face Space; - the runtime model is at most 4B parameters; - no cloud inference API is required; - scenario outputs are grammar-constrained and validated; - historical challenge mode is playable; - base and fine-tuned extraction quality are measured on a frozen test set; - the model, dataset or traces, field notes, demo, and social post are published before submission. ## Non-Goals Before Submission - Proving superiority over bookmaker markets - Live-data ingestion pipelines - Dixon-Coles or large ML ensembles - World Cup tournament simulation - Coding-agent benchmark - Betting recommendations - Hyperparameter sweeps ## User Experience ### Primary Flow 1. User selects a hidden historical match. 2. App shows teams, venue, competition context, and baseline probabilities without revealing the result. 3. User writes a hypothetical or pre-match scenario. 4. Small model extracts typed semantic factors. 5. Validator rejects unsupported output and resolves team references. 6. Rule engine maps factors to bounded model adjustments. 7. Probability bars animate from baseline to adjusted forecast. 8. User submits home/draw/away probabilities. 9. App reveals the historical result. 10. App compares the baseline, adjusted model, and user with log loss and Brier score. ### Supporting Views - **Scenario Stress Test:** vary one factor's severity and visualize monotonic probability movement. - **Tiny Model Lab:** base-versus-tuned metrics, latency, example failures, model size, and training details. - **How It Works:** concise explanation of semantic extraction, deterministic rules, and Poisson forecasting. ## System Architecture ```text Gradio application -> historical match repository -> baseline forecast service -> scenario extractor interface -> llama.cpp base or fine-tuned GGUF -> grammar-constrained JSON -> Pydantic validation -> semantic factor normalizer -> deterministic adjustment rules -> Poisson forecast service -> scoring service -> visual components -> append-only local trace logger ``` The extractor must be swappable. The rest of the application must work with: - a deterministic mock extractor for tests; - the base SmolLM3-3B model; - the fine-tuned SmolLM3-3B model. ## Repository Structure ```text app.py pyproject.toml requirements.txt README.md src/underdog_lab/ config.py domain.py data/ repository.py schemas.py forecasting/ poisson.py adjustments.py scoring.py scenarios/ taxonomy.py schemas.py extractor.py llama_cpp_extractor.py mock_extractor.py grammar.gbnf prompts.py ui/ app.py components.py charts.py theme.py telemetry/ traces.py scripts/ prepare_matches.py generate_synthetic_data.py evaluate_extractor.py export_gguf.py smoke_test_space.py training/ modal_train.py configs/ qlora.yaml data/ raw/ processed/ matches.parquet team_strengths.parquet scenarios/ train.jsonl validation.jsonl test.jsonl traces/ models/ README.md tests/ unit/ property/ integration/ fixtures/ docs/ ``` ## Domain Contracts ### Match Record ```python class MatchRecord(BaseModel): match_id: str kickoff_date: date competition: str home_team: str away_team: str neutral_venue: bool home_goals: int away_goals: int baseline_home_xg: float baseline_away_xg: float context: str reveal_notes: str | None = None ``` `home_goals` and `away_goals` must remain hidden from the UI until the user commits a forecast. ### Factor Taxonomy Freeze the taxonomy before generating data: ```text key_attacker_unavailable key_defender_unavailable goalkeeper_unavailable multiple_starters_unavailable squad_rotation fatigue_disadvantage rest_advantage travel_disadvantage altitude_disadvantage heat_disadvantage home_advantage neutral_venue defensive_game_state must_win_incentive ``` Classification-only outputs: ```text unsupported_claim ambiguous_claim irrelevant_text ``` ### Extraction Schema ```python class ExtractedFactor(BaseModel): factor_type: FactorType team: Literal["home", "away", "both", "unknown"] severity: float = Field(ge=0.0, le=1.0) certainty: float = Field(ge=0.0, le=1.0) evidence: str class ScenarioExtraction(BaseModel): factors: list[ExtractedFactor] = Field(max_length=6) unsupported_claims: list[str] = Field(default_factory=list) ambiguities: list[str] = Field(default_factory=list) ``` The model never emits expected-goal deltas or probabilities. ## Deterministic Adjustment Rules Store the mapping in versioned Python data or YAML. Example starting ranges: | Factor | Target | Maximum effect at severity 1.0 | |---|---|---:| | key_attacker_unavailable | affected attack | -12% | | key_defender_unavailable | opponent attack | +8% | | goalkeeper_unavailable | opponent attack | +10% | | multiple_starters_unavailable | affected attack/defence | -8% / opponent +6% | | squad_rotation | affected attack/defence | -6% / opponent +4% | | fatigue_disadvantage | affected attack/defence | -5% / opponent +3% | | rest_advantage | affected attack/defence | +4% / opponent -2% | | travel_disadvantage | affected attack | -4% | | altitude_disadvantage | affected attack/defence | -5% / opponent +3% | | heat_disadvantage | affected attack | -3% | | home_advantage | home attack | +6% | | neutral_venue | home attack | -6% | | defensive_game_state | both attacks | -6% | | must_win_incentive | affected attack/defence | +5% / opponent +2% | These values are product assumptions, not learned truths. Label them clearly and version them as `ruleset_v1`. Clamp final expected goals to a safe range such as `[0.15, 4.0]`. ## Forecasting Engine ### Baseline Use precomputed expected goals per match. For a score grid from 0 to 8 goals: ```text P(home=i, away=j) = Poisson(i; lambda_home) * Poisson(j; lambda_away) ``` Sum score cells into home-win, draw, and away-win probabilities. Normalize after truncation. ### Scenario Adjustment 1. Deduplicate equivalent factors. 2. Ignore `unknown` team assignments unless deterministically resolvable. 3. Multiply effects by `severity * certainty`. 4. Apply bounded multiplicative adjustments. 5. Clamp expected goals. 6. Recompute the score matrix and 1X2 probabilities. ### Scoring Implement: - multiclass log loss with probability clipping; - multiclass Brier score; - optional simple points for game presentation only. Scientific tables use log loss and Brier score, never game points alone. ## Data Plan ### Historical Challenge Set Prepare 20–30 matches, prioritizing variety rather than only famous upsets: - favorites winning comfortably; - draws; - narrow favorites losing; - neutral-venue tournament matches; - high-scoring and low-scoring matches. Do not expose result-bearing language in `context`. Famous showcase matches must be excluded from extraction training examples and the frozen extractor test set. ### Extraction Dataset Target: - 600–800 synthetic training examples; - 50 validation examples; - 80–100 frozen test examples. Balance factor categories and include: - single and multiple factors; - pronouns and team-name references; - negation; - contradictions; - irrelevant supporter commentary; - unsupported facts; - prompt-injection attempts; - paraphrase groups; - severity ladders. Manually review every validation and test item. Store provenance and split before training. Example record: ```json { "id": "scenario-0042", "home_team": "Argentina", "away_team": "Saudi Arabia", "text": "Argentina's first-choice striker is confirmed out.", "expected": { "factors": [ { "factor_type": "key_attacker_unavailable", "team": "home", "severity": 1.0, "certainty": 1.0 } ], "unsupported_claims": [], "ambiguities": [] } } ``` ## Constrained Inference Use `llama.cpp` for both base and tuned models. Enforce the JSON structure with GBNF grammar. Runtime settings: - temperature: `0` or near zero; - short maximum output; - fixed system prompt; - one retry only for semantic validation failures; - deterministic empty extraction fallback; - cache common demo scenarios. Grammar guarantees syntax, not semantic correctness. Pydantic and domain validation remain mandatory. ## Fine-Tuning Plan ### Base Model Start with `HuggingFaceTB/SmolLM3-3B` or its supported GGUF conversion. ### Method Run one QLoRA supervised fine-tune on Modal: - rank: 16; - alpha: 32; - dropout: 0.05; - learning rate: approximately `2e-4`; - 2–3 epochs; - sequence length sized to the compact extraction task; - early stopping or checkpoint selection using validation semantic metrics. Exact settings may change for compatibility, but do not run a broad sweep. ### Artifacts Publish: - LoRA adapter; - merged model if licensing and storage allow; - GGUF runtime model; - dataset or anonymized trace dataset; - model card with taxonomy, training method, limitations, and evaluation. ### Decision Gate: June 13 Evening Ship the tuned model only if it: - improves factor micro-F1 by a meaningful amount; - does not regress team attribution or unsupported-claim detection materially; - passes every behavioral property test; - fits Space memory and startup constraints; - has acceptable median latency. Otherwise ship the base model and report the negative result honestly. Do not risk the submission for the Well-Tuned badge. ## Evaluation Plan ### Extraction Metrics - factor micro-F1 and macro-F1; - team attribution accuracy; - severity mean absolute error; - certainty mean absolute error; - unsupported-claim precision, recall, and F1; - ambiguity detection F1; - exact semantic match rate; - paraphrase consistency; - median and p95 latency. Because constrained decoding guarantees schema shape, schema-validity rate is a runtime health metric, not the main fine-tuning result. ### Behavioral Property Tests - An unavailable attacker never improves the affected attack. - Higher severity never produces a smaller adjustment for the same factor. - Unsupported or irrelevant text produces no forecast adjustment. - Equivalent paraphrases map to equivalent factor sets within tolerance. - Home/away references remain correctly attributed. - Duplicate factors do not stack without bounds. - Contradictory factors are flagged or handled deterministically. - Forecast probabilities are finite, nonnegative, and sum to one. - Hidden results cannot enter the pre-reveal state. ### Integration Tests - Select match -> extract -> adjust -> score -> reveal. - Extractor timeout returns a usable fallback state. - Invalid model output cannot reach the forecast engine. - Base and tuned adapters satisfy the same interface. - Space starts without external inference credentials. ## Gradio UI Plan ### Screen 1: Challenge - Match card with competition and venue context - Baseline probability bars - Scenario textbox with three examples - Extracted-factor chips with severity and certainty - Before/after probability animation - User probability controls constrained to sum to 100% - Commit and reveal action - Score comparison ### Screen 2: Stress Test - Match selector - Factor selector - Team selector - Severity slider - Live probability curve - Plain-language explanation of deterministic assumptions ### Screen 3: Tiny Model Lab - Base versus tuned metric table - Confusion by factor category - Example successes and failures - Model size, quantization, latency, and Modal training compute - Links to model, dataset, traces, and field notes ### Visual Direction Use a broadcast-analysis aesthetic rather than default Gradio controls: - dark pitch-inspired background; - strong team-color probability bars; - compact match cards; - visible “baseline” and “scenario” states; - restrained animation; - mobile-safe layout. Polish the primary challenge screen before secondary tabs. ## Delivery Schedule ### June 12: Submission-Safe Vertical Slice - Scaffold repository and dependencies. - Implement schemas, taxonomy, rules, Poisson model, and scoring. - Prepare at least 10 historical matches. - Implement mock extractor and base-model adapter. - Add GBNF grammar and validation. - Build minimal Challenge screen. - Deploy the first working Space. - Start synthetic-data generation only after taxonomy freeze. Exit criteria: a clean user can complete one challenge on the deployed Space. ### June 13: Evaluation and Fine-Tuning - Finalize 20–30 matches. - Freeze and manually review extractor test set. - Benchmark the base model. - Run one QLoRA job on Modal. - Build reveal, scoring, and stress-test features. - Implement behavioral and integration tests. - Evaluate tuned versus base model. - Apply the decision gate. Exit criteria: selected runtime model is known and the complete core experience works. ### June 14: Deployment and Submission Assets - Merge and quantize the selected model. - Publish model and dataset/trace artifacts. - Verify llama.cpp inference in the Space. - Build Tiny Model Lab. - Finish custom styling. - Test cold start, mobile layout, and failure paths. - Record the 60–90 second demo. - Draft field notes and social post. Exit criteria: release candidate is frozen and all submission URLs exist. ### June 15: Buffer and Submit - Fix only critical defects. - Re-run smoke tests from a clean session. - Publish field notes and social post. - Submit Space, demo, and social links. - Tag the release commit. No new features on June 15. ## Acceptance Criteria ### Must Have - Hosted Gradio Space - Runtime model <=4B - llama.cpp inference - No cloud inference dependency - Historical challenge playable end to end - Grammar-constrained semantic extraction - Deterministic validated adjustments - Probability visualization and scoring - At least 20 historical matches - Automated unit and integration tests - Demo video and social post ### Should Have - Fine-tuned model with measured semantic improvement - Stress-test slider - Tiny Model Lab - Published model and dataset/traces - Field notes - Custom visual styling ### Could Have - Live World Cup fixture card - Forecast Courtroom presentation - Community leaderboard - More sophisticated baseline calibration ## Risk Register | Risk | Trigger | Mitigation | |---|---|---| | Space cannot load 3B GGUF | OOM or startup timeout | Quantize more aggressively, reduce context, lazy-load, keep mock/demo fallback | | llama.cpp lacks model compatibility | Conversion or inference failure | Test base GGUF on June 12; switch to a known-supported <=4B model immediately | | Fine-tune underperforms | No semantic gain on frozen test | Ship base model; document result | | Synthetic labels are noisy | Low reviewer agreement | Shrink training set and improve labels rather than train longer | | Demo feels like hindsight | Showcase relies on known upset | Hide result and use only pre-match or explicitly hypothetical context | | UI looks like a chat wrapper | Scenario box dominates | Make factors and probability movement the central visual artifact | | Cloud dependency breaks Off the Grid | Runtime calls external endpoint | Keep all inference in the Space and bundle/cache required artifacts | | Deadline pressure | Core incomplete by June 13 | Drop secondary tabs and fine-tune before dropping deployable core | ## Release Checklist - [ ] Registration and organization access confirmed - [ ] Space created under hackathon organization - [ ] Model parameter total documented - [ ] Model license checked - [ ] Dataset licenses and attribution documented - [ ] No result leakage in challenge context - [ ] Base benchmark saved - [ ] Tuned benchmark saved or negative result documented - [ ] GGUF and llama.cpp versions pinned - [ ] Clean Space restart passes - [ ] Mobile smoke test passes - [ ] Model card published - [ ] Dataset or trace card published - [ ] Field notes published - [ ] Demo video uploaded - [ ] Social post published - [ ] Submission completed before deadline ## Final Pitch > Underdog Lab fine-tunes a 3B local model to turn messy football narratives into validated forecasting evidence. A transparent statistical engine converts those factors into probabilities, letting users stress-test scenarios, challenge the model, and learn why calibrated forecasts are more useful than confident guesses.