File size: 16,775 Bytes
f7d934a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
# 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.