# Tucano2-Commerce: Domain-Specialized LLM for Brazilian E-Commerce Analysis
## Project Status: v2 Complete — v3 Planned
**Model:** Qwen3-3.7B → SFT → GRPO alignment
**Domain:** Brazilian e-commerce (sentiment analysis, churn prediction, SQL generation, structured extraction)
**Infrastructure:** Vertex AI Workbench, NVIDIA L4 (24GB), Unsloth + TRL 0.24.0
**Tracking:** W&B project `tferrazrafael-self/tucano2-commerce`
---
## 1. Problem Statement
Brazilian e-commerce companies need automated analysis of customer reviews, churn prediction, and business intelligence generation — all in Portuguese. General-purpose LLMs (GPT-4o, Claude) are:
1. **Expensive at scale** — API costs of ~$0.01/analysis × thousands of daily reviews
2. **Not domain-optimized** — miss Brazilian Portuguese idioms, e-commerce-specific patterns
3. **Not self-hosted** — customer data leaves the organization for every API call
**Goal:** Build a compact (3.7B parameter) model that matches or exceeds large general models on e-commerce-specific tasks, runs on a single GPU, and keeps data on-premise.
---
## 2. Context & Approach
### Architecture Decision: SFT + GRPO
The training pipeline follows the DeepSeek-R1 paradigm (arxiv: 2501.12948):
```
Qwen3-3.7B (base) → SFT (domain adaptation) → GRPO (alignment via reward signals)
```
**Why Qwen3-3.7B:**
- Strong multilingual base with Portuguese capability
- 3.7B parameters fits in 24GB VRAM with 4-bit quantization (Unsloth NF4)
- Qwen3 architecture includes native `` reasoning mode
**Why GRPO over DPO/PPO:**
- No need for a separate reward model (rule-based rewards suffice for structured tasks)
- Group-relative optimization naturally handles multi-task reward distributions
- Published results show GRPO working well at this model scale (Dr. GRPO, Skywork-OR1)
**Why rule-based rewards:**
- E-commerce tasks have verifiable outputs (JSON schema adherence, SQL execution, sentiment polarity)
- Neural reward models introduce reward hacking at small scale
- DeepSeek-R1 demonstrated rule-based rewards outperform neural reward models for structured tasks
### Task Portfolio
| Task | Input | Output | Evaluation |
|------|-------|--------|------------|
| Structured Extraction | Customer review + metadata | JSON with 10 fields | Field-level match |
| Sentiment Analysis | Review text | Polarity + score | Accuracy + F1 |
| SQL Generation | Business question | Executable SQL query | Execution accuracy |
| Churn Prediction | Customer profile | Risk score + reasoning | Binary accuracy |
| Business Insights | Open-ended question | Analytical report in PT-BR | LLM-as-judge |
### Infrastructure
- **Training:** Vertex AI Workbench, single NVIDIA L4 (24GB VRAM)
- **Quantization:** Unsloth NF4 (4-bit) for training — enables 3.7B model to fit in 24GB
- **Framework:** TRL 0.24.0 (pinned for Unsloth compatibility), `UnslothGRPOTrainer`
- **Monitoring:** Weights & Biases
---
## 3. Decision Log
### Decision 1: Continuous vs. Binary Reward Functions
- **Context:** Initial reward functions used binary (0/1) scoring
- **Problem:** 50% of training steps showed `reward_std=0` and `loss=0` — no learning signal
- **Decision:** Rewrote all 4 reward functions with continuous scoring (0.0–1.0), partial credit for partially correct outputs
- **Consequence:** Zero-std steps dropped from 50% to ~10%; loss became consistently non-zero
- **Reference:** Dr. GRPO paper (2503.20783) proves std-based normalization amplifies this issue
### Decision 2: Temperature 0.8 → 1.0
- **Context:** Model's `generation_config.json` had `temperature=0.1` (default from Qwen3)
- **Problem:** All 8 GRPO completions were near-identical → zero reward variance → zero advantage → zero gradient. `frac_reward_zero_std=1.0` on every step. First full run was killed.
- **Decision v2:** Set `temperature=0.8` in GRPOConfig
- **Outcome v2:** Fixed the zero-std catastrophe. Training ran 210 steps, eval improved 50% (0.083→0.125)
- **Decision v3 (planned):** Increase to `temperature=1.0` — all published GRPO papers (DeepSeek-R1, Dr. GRPO, Skywork-OR1) use 1.0. Higher temperature further delays entropy collapse.
- **Reference:** Skywork-OR1 (2505.22312) ablation: τ=1.0 gives 5-8% better test performance than τ=0.6
### Decision 3: `scale_rewards=False` (Dr. GRPO)
- **Context:** Standard GRPO normalizes advantages by `std(rewards)` per group
- **Problem:** When one group has low variance, dividing by a small std inflates its gradient contribution → training instability and bias toward "easy" prompts
- **Decision:** Disabled std normalization following Dr. GRPO paper
- **Consequence:** More stable training; combined with continuous rewards, eliminated most zero-gradient steps
- **Reference:** Dr. GRPO (2503.20783) achieved SOTA 43.3% on AIME 2024 with a 7B model using this fix
### Decision 4: Early Stopping Configuration
- **Context (v2 run 1):** `EARLY_STOPPING_PATIENCE=3`, `EVAL_STEPS=10`, `EVAL_MAX_TOKENS=256`
- **Problem:** Model killed at step 40. Eval tokens too short — model needs 500-700 tokens just for ``. Eval was scoring incomplete generations. Only 30 steps of runway before early stopping fired.
- **Decision (v2 run 2):** `PATIENCE=10`, `EVAL_MAX_TOKENS=2048`, `EVAL_MAX_SAMPLES=5`
- **Consequence:** Training ran to step 210, early stopping fired correctly when eval plateaued
- **Lesson:** Early stopping parameters must account for the model's generation length requirements
### Decision 5: `MAX_STEPS` vs. `NUM_EPOCHS`
- **Context:** User set `NUM_EPOCHS=2`, `MAX_STEPS=300`
- **Clarification:** In TRL, `MAX_STEPS` takes absolute priority. With 300 prompts × 8 generations / (batch_size=4 × grad_accum=2) = 300 steps per epoch. `MAX_STEPS=300` = exactly one epoch regardless of `NUM_EPOCHS`.
- **Decision:** Keep `MAX_STEPS=300` for one clean epoch; decide on epoch 2 based on eval trajectory
- **Consequence:** Early stopping at 210 means the model trained through 70% of the data before plateauing
### Decision 6: TRL 0.24.0 Pinning
- **Context:** Unsloth requires specific TRL versions. Upgrading TRL broke vllm/torch dependencies.
- **Decision:** Pin `trl==0.24.0 --no-deps` after Unsloth installation
- **Consequence:** Stable environment, but locks out newer TRL features (e.g., entropy bonus in GRPOConfig)
- **Workaround for v3:** Implement entropy control via custom callback or trainer subclass
---
## 4. Training Results
### v2 Final Run (Step 210/300, Early Stopped)
| Metric | Value | Assessment |
|--------|-------|------------|
| `eval/best_reward_final` | 0.125 | +50% from starting 0.083 |
| `train/reward` | 0.285 | Below SFT calibration (0.38) |
| `train/frac_reward_zero_std` | 0.0 | ✅ Fixed (was 1.0 in v1) |
| `train/kl` | 0.004 | Very conservative policy shift |
| `train/clip_ratio` | 0.0 (all) | ⚠️ Entropy collapse — policy never hit clip bounds |
| `train/completion_length` | 2048 (= max) | ⚠️ Every completion truncated |
| `train/grad_norm` | 0.030 | Stable |
| Duration | 14.9 hours | 210 steps × ~4.3 min/step |
### Validation Results (5 held-out prompts)
| Sample | Task | Reward | Notes |
|--------|------|--------|-------|
| 1 | Extraction (JSON) | 0.12 | Fields incorrect, output truncated |
| 2 | Insights (categories) | 0.70 | Coherent PT-BR, structured headers |
| 3 | Retention analysis | 0.70 | Step-by-step methodology |
| 4 | Reengagement decision | 0.50 | `` reasoning visible, contextual |
| 5 | Regional comparison | 0.70 | Comparative framework |
**Mean validation reward: 0.54** vs SFT calibration baseline of **0.38** → **+42% improvement**
### Bimodal Performance Pattern
- **Strong (0.50–0.70):** Open-ended analysis, insights, comparison tasks
- **Weak (0.12):** Structured JSON extraction — the completion ceiling blocks the model from outputting complete JSON
---
## 5. Diagnosed Issues & Root Causes
### Issue 1: Entropy Collapse (Critical)
- **Symptom:** `clip_ratio=0` on all steps, KL=0.004
- **Root cause:** Policy entropy drops to near-zero → all 8 rollouts produce identical output → zero advantage → zero gradient (Skywork-OR1, 2505.22312)
- **Fix (v3):** Temperature=1.0, add entropy loss coefficient (α=5e-3), filter zero-advantage groups
### Issue 2: Completion Length Ceiling (Critical)
- **Symptom:** `completion_length=2048` (= `max_completion_length`) on every step
- **Root cause:** GRPO length bias inflates incorrect response length (Dr. GRPO, 2503.20783 §3.1). Model can't finish reasoning → gets low rewards → weak signal
- **Fix (v3):** Increase `max_completion_length` to 4096, reduce `num_generations` 8→4 to fit VRAM
### Issue 3: Data Scale (Moderate)
- **Symptom:** Early stopping at step 210 (70% of epoch), eval plateaued at 0.125
- **Root cause:** 300 prompts is below published minimums (1K–600K in literature)
- **Fix (v3):** Expand to 1000+ prompts via synthetic generation and data augmentation
---
## 6. Lessons Learned
### Technical Lessons
1. **Default model configs kill RL training.** Qwen3's `generation_config.json` sets `temperature=0.1`. This single default destroyed the first full training run. Always override generation parameters explicitly.
2. **Reward function design is the core ML engineering task.** Binary rewards → zero signal. Continuous rewards → training works. Multi-component rewards (format + content + quality) → staged learning where format converges first. The reward function IS the product specification.
3. **GRPO needs diversity to learn.** The algorithm is fundamentally about comparing different completions. Anything that reduces diversity (low temperature, small group size, few prompts, completion ceiling) directly reduces learning signal.
4. **TRL step calculation is non-obvious.** `steps = num_prompts × num_generations / (batch_size × grad_accum)`. Missing the `num_generations` multiplier gives wrong epoch estimates. `MAX_STEPS` always overrides `NUM_EPOCHS`.
5. **Early stopping needs tuning for generative models.** Patience must account for eval generation length. Short eval tokens → incomplete outputs → flat eval scores → premature stopping.
6. **Entropy collapse is the GRPO failure mode.** Not divergence, not reward hacking — the model collapses to deterministic output. Monitoring `clip_ratio` and generation entropy is essential.
### Business Lessons
1. **Domain data is the moat, not model size.** ThinkJSON (1.5B) beats DeepSeek-R1 (671B) on JSON extraction. A 7B model beats o3-mini on SQL. 300 Portuguese e-commerce examples already produced a model that outperforms SFT baseline by 42%.
2. **Cost arbitrage is immediate.** A self-hosted 3.7B model on a $0.50/hr GPU costs ~$0.001/analysis vs $0.01+ for API calls. Breakeven at ~100 analyses/day.
3. **Privacy is a feature.** Self-hosted means customer data never leaves the organization. This matters for LGPD (Brazilian data protection law) compliance.
4. **Portuguese-first is defensible.** Most LLM development is English-first. A model that deeply understands Brazilian e-commerce Portuguese ("veio com defeito", "nota 1 estrela") has a real competitive advantage.
5. **Budget 3-5 iterations, not 1.** The first run is diagnostic. v1 found the zero-signal bug. v2 found the temperature bug and completion ceiling. v3 will address entropy collapse. Each iteration is cheaper than the last because you know what to measure.
---
## 7. Next Steps
See `docs/ADR-001-next-steps.md` for detailed execution plans.
### Priority 1: Build Domain Benchmark (1-2 days)
50-100 held-out prompts, automated scoring, establish baselines
### Priority 2: Run Comparison vs. Qwen3-35B-A3B (1 day)
Prove small tuned model matches/beats large general model on domain tasks
### Priority 3: GRPO v3 Training Run (2-3 days)
Fix entropy collapse, increase completion length, expand training data
---
## References
| Paper | Key Finding | Relevance |
|-------|------------|-----------|
| DeepSeek-R1 (2501.12948) | SFT→GRPO pipeline, rule-based rewards | Architecture template |
| Dr. GRPO (2503.20783) | Remove std normalization, remove length bias | Fixes reward scaling |
| Skywork-OR1 MAGIC (2505.22312) | Entropy collapse diagnosis and fix | Explains clip_ratio=0 |
| MC-GRPO (2601.22582) | Median baseline for small rollout budgets | Fixes G=8 noise |
| ThinkJSON (2502.14905) | 1.5B beats 671B on JSON extraction | Proves domain specialization thesis |
| Reasoning-SQL (2503.23157) | 7B beats o3-mini on SQL with GRPO | Proves GRPO works for SQL |
| Cocktail Effect (2410.01109) | Multi-task SFT + general data boosts domain performance | SFT improvement recipe |