Efficient Semantic Parsing via Chain-of-Thought Rationalization and Contrastive Demonstration Selection
Seyed Hossein Ahmadpanah — Department of Computer Engineering, Islamic Azad University, Tehran, Iran
DSR is a neuro-symbolic framework for Hierarchical Semantic Parsing (HSP) that translates natural language queries into executable, nested logical forms. It addresses two core limitations of LLM-based parsers:
DSR closes the gap by distilling the structural reasoning of a large Teacher model (Llama-2-70B) into a compact Student model (Llama-3-8B), achieving 87.42% Exact Match on TOP — a new state-of-the-art for open-source models — while running 9.4× faster than the teacher.
| Model | Params | TOP EM | ATIS EM | SNIPS EM | Latency |
|---|---|---|---|---|---|
| Seq2Seq-PTR | 125M | 83.90% | 87.10% | 87.14% | Fast |
| BART-Large (FT) | 400M | 86.40% | 88.20% | 90.50% | Fast |
| Llama-2-70B (Standard CoT) | 70B | 85.30% | 89.50% | 91.20% | 18 tok/s |
| Teacher (CFS + Structural CoT) | 70B | 87.95% | 91.80% | 93.10% | 18 tok/s |
| Llama-3-8B (Standard SFT) | 8B | 85.11% | 89.15% | 91.85% | ~150 tok/s |
| Student (Distilled CoT-SR) | 8B | 87.42% | 91.25% | 92.88% | 170 tok/s |
Ablation — Retrieval Strategy (TOP, Teacher model):
| Retrieval | EM | Struct. Error | Δ vs BM25 |
|---|---|---|---|
| Random | 78.40% | 12.5% | — |
| BM25 | 82.15% | 8.2% | — |
| Sentence-BERT | 86.10% | 4.5% | +3.95% |
| CFS (Ours) | 87.95% | 1.2% | +5.80% |
Zero-shot domain transfer (TOPv2 Weather): 71.4% EM vs. 42.3% for fixed-classifier baseline.
The DSR framework has two training phases and four novel components:
Phase 1: Rationale Synthesis (Teacher)
┌─────────────────────────────────────────────────────────┐
│ Input Query │
│ │ │
│ [CFS Encoder] ──→ Top-k Structurally Similar Demos │
│ │ │
│ [Prompt Builder] ──→ Structural CoT Prompt │
│ │ │
│ [Teacher LLM, 70B] ──→ Structural CoT Trace r + LF y │
└─────────────────────────────────────────────────────────┘
Phase 2: Efficient Inference (Student)
┌─────────────────────────────────────────────────────────┐
│ Augmented Dataset D_aug = {(x_i, r_i, y_i)} │
│ │ │
│ [Rationale Distillation] ──→ Fine-tuned Student (8B) │
│ │ │
│ [Trie-Based Constraints] ──→ Syntax-valid decoding │
│ │ │
│ [Parser-Critic Loop] ──→ Semantic self-correction │
└─────────────────────────────────────────────────────────┘
A bi-encoder (RoBERTa-Large) trained with InfoNCE loss on hard-negative triplets:
$$\mathcal{L}{CFS} = -\log \frac{\exp(\text{sim}(h_x, h{x^+}) / \tau)}{\sum_{j \in {x^+, x^-}} \exp(\text{sim}(h_x, h_j) / \tau)}$$
Decomposes the hierarchical parse tree into a Depth-First Search (DFS) sequence of semantic fragment decoding steps:
Query: "Book a flight for three people to Boston"
Step 1: Identify Root Intent [IN:FLIGHT].
Step 2: Identify Slot [SL:QUANTITY] with value 'three people' (depth 1).
Step 3: Identify Slot [SL:DEST] with value 'Boston' (depth 1).
Final: Combine all fragments → [IN:FLIGHT [SL:QUANTITY three people ] [SL:DEST Boston ] ]
Trains the Student on the Teacher's CoT traces via Supervised Fine-Tuning (SFT):
$$\mathcal{L}{\text{distill}} = -\sum{t=1}^{|r|+|y|} \log P_{M_S}(z_t \mid x, z_{<t})$$
where $z = [r ; y]$ is the concatenation of the reasoning trace $r$ and logical form $y$.
A prefix trie built from the domain schema masks invalid tokens at each decoding step:
$$m_t[w] = \begin{cases} 0 & \text{if } w \in V_{\text{valid}}(z_{<t}) \ -\infty & \text{otherwise} \end{cases}$$
Eliminates 100% of syntax errors (hallucinated slots, invalid bracket nesting).
Cycle-consistency check for semantic fidelity:
$$\text{Score}(\hat{y}) = \cos(E(x), E(\hat{x})) > \delta$$
git clone https://github.com/ahmadpanah/distilling-structural-reasoning
cd distilling-structural-reasoning
# Install core dependencies
pip install -r requirements.txt
# Install as a package (editable)
pip install -e .
# Optional: fast inference with vLLM
pip install vllm
Requirements: Python ≥ 3.9, PyTorch ≥ 2.0, CUDA GPU recommended (A100 for full training)
from src.pipeline import DSRPipeline
# Load pipeline with a fine-tuned student checkpoint
pipeline = DSRPipeline.from_pretrained(
student_checkpoint="./checkpoints/student",
retriever_checkpoint="./checkpoints/cfs_encoder.pt",
demo_bank=demo_bank, # list of {'query': ..., 'logical_form': ..., 'rationale': ...}
)
result = pipeline.parse("Book a flight from New York to Boston for three people")
print(result["logical_form"])
# [IN:FLIGHT [SL:ORG New York ] [SL:DEST Boston ] [SL:QUANTITY three people ] ]
print(f"Critic score: {result['score']:.3f}")
print(f"Demos used: {result['demos_used']}")
queries = [
"What is the weather in Paris tomorrow?",
"Set a reminder for my 3pm meeting",
"Navigate to the nearest coffee shop",
]
results = pipeline.parse_batch(queries)
for q, r in zip(queries, results):
print(f"Q: {q}")
print(f"LF: {r['logical_form']}\n")
The framework supports four benchmarks. Download each dataset and place under ./data/:
data/
├── top/
│ ├── train.tsv # query \t logical_form
│ ├── eval.tsv
│ └── test.tsv
├── topv2/
│ ├── navigation/
│ ├── reminder/
│ ├── weather/
│ └── ...
├── atis/
│ ├── train.json
│ ├── dev.json
│ └── test.json
└── snips/
├── train.json
├── dev.json
└── test.json
| Dataset | Size | Domains | Depth |
|---|---|---|---|
| TOP | ~44K | Navigation, Events | Deep (≥4) |
| TOPv2 | ~180K | + Reminder, Weather, Music | Deep |
| ATIS | ~5.4K | Flight booking | Medium |
| SNIPS | ~14K | 7 voice intents | Shallow |
from src.data.datasets import load_dataset
dataset = load_dataset("top", data_dir="./data/top")
print(dataset)
# TOPDataset({'train': 39281, 'dev': 2318, 'test': 4370})
# Access splits
for ex in dataset.train[:3]:
print(ex["query"], "→", ex["logical_form"])
python scripts/train.py \
--dataset top \
--data_dir ./data/top \
--student_model meta-llama/Meta-Llama-3-8B-Instruct \
--output_dir ./checkpoints \
--use_gold_rationale # No teacher inference needed
--use_gold_rationale generates structural CoT traces deterministically from the gold logical forms (DFS linearization). This is equivalent to the main setup in the paper and requires no 70B teacher model at training time.
To use a live Teacher model for rationale generation (requires the 70B model):
python scripts/train.py \
--dataset top \
--data_dir ./data/top \
--no_gold_rationale \
--teacher_model meta-llama/Llama-2-70b-chat-hf
Step 1: Train CFS Retriever
from src.models.cfs import CFSEncoder, mine_hard_negatives, train_cfs_encoder
from src.data.datasets import load_dataset
dataset = load_dataset("top", "./data/top")
queries = [ex["query"] for ex in dataset.train]
lfs = [ex["logical_form"] for ex in dataset.train]
triplets = mine_hard_negatives(queries, lfs, jaccard_threshold=0.5)
print(f"Mined {len(triplets)} hard-negative triplets")
encoder = CFSEncoder(model_name="roberta-large")
encoder = train_cfs_encoder(
encoder,
triplets,
num_epochs=10, # Paper: 10 epochs
temperature=0.05, # Paper: τ = 0.05
)
import torch
torch.save(encoder.state_dict(), "./checkpoints/cfs_encoder.pt")
Step 2: Build Augmented Dataset
from src.models.cfs import ContrastiveFragmentSelector
from src.models.structural_cot import build_augmented_dataset
selector = ContrastiveFragmentSelector(encoder, demo_bank=dataset.train, top_k=5)
selector.index_demo_bank()
augmented = build_augmented_dataset(
queries=queries,
logical_forms=lfs,
demo_selector=selector,
teacher=None,
use_gold_rationale=True, # DFS linearization (no 70B needed)
)
# augmented[0] → {'query': ..., 'logical_form': ..., 'rationale': 'Step 1: ...'}
Step 3: Distill Student
from src.training.distillation import RationaleDistillationTrainer, DistillationConfig
config = DistillationConfig(
student_model_name="meta-llama/Meta-Llama-3-8B-Instruct",
num_epochs=3,
batch_size=4,
gradient_accumulation_steps=8,
learning_rate=2e-4,
use_lora=True,
lora_r=16,
output_dir="./checkpoints/student",
)
trainer = RationaleDistillationTrainer(config)
trainer.train(
train_examples=augmented,
eval_examples=dataset.dev,
)
| Stage | GPU Memory | Time (A100) |
|---|---|---|
| CFS Retriever Training | ~16 GB | ~2 hours |
| Augmented Dataset (gold rationale) | CPU only | ~10 min |
| Student SFT (Llama-3-8B + LoRA + 4-bit) | ~24 GB | ~8 hours |
| Teacher Rationale Generation (70B) | 140+ GB | ~48 hours |
# Evaluate student on TOP test set
python scripts/evaluate.py \
--student_checkpoint ./checkpoints/student \
--retriever_checkpoint ./checkpoints/cfs_encoder.pt \
--dataset top \
--data_dir ./data/top \
--split test
# Ablation: no CFS retrieval (uses BM25-style random retrieval)
python scripts/evaluate.py \
--student_checkpoint ./checkpoints/student \
--dataset top --data_dir ./data/top \
--no_cfs
# Zero-shot domain transfer (Weather domain, no weather training data)
python scripts/evaluate.py \
--student_checkpoint ./checkpoints/student \
--dataset topv2 --data_dir ./data/topv2 \
--domain weather \
--zero_shot
# Save results to JSON
python scripts/evaluate.py \
--student_checkpoint ./checkpoints/student \
--dataset atis --data_dir ./data/atis \
--output_file results/atis_test.json
from src.data.datasets import load_dataset
from src.evaluation.metrics import Evaluator
from src.pipeline import DSRPipeline
dataset = load_dataset("top", "./data/top")
pipeline = DSRPipeline.from_pretrained("./checkpoints/student", demo_bank=dataset.train)
evaluator = Evaluator(dataset_name="TOP")
result = evaluator.evaluate(
parser_fn=lambda q: pipeline.parse(q)["logical_form"],
examples=dataset.test,
)
print(result)
Output:
=== TOP Results ===
Exact Match: 87.42%
Tree-F1: 0.9156
Struct. Error: 0.00%
Avg Latency: 5.9ms
Tokens/sec: 170.0
Num Examples: 4370
Per-depth EM:
Depth 1: 94.20%
Depth 2: 89.31%
Depth 3: 82.10%
Depth 4: 71.80%
Depth 5: 62.40%
distilling-structural-reasoning/
│
├── src/
│ ├── models/
│ │ ├── cfs.py # Contrastive Fragment Selection
│ │ │ # CFSEncoder, InfoNCELoss, ContrastiveFragmentSelector
│ │ │ # mine_hard_negatives, train_cfs_encoder
│ │ ├── structural_cot.py # Structural Chain-of-Thought prompting
│ │ │ # TeacherModel, build_augmented_dataset
│ │ ├── trie_decoder.py # Trie-Based Constrained Decoding
│ │ │ # GrammarTrie, LogitMaskConstraint
│ │ └── parser_critic.py # Parser-Critic Feedback Loop
│ │ # Critic, ParserCriticLoop
│ ├── training/
│ │ └── distillation.py # Rationale Distillation Protocol
│ │ # RationaleDistillationDataset, Trainer, StudentParser
│ ├── data/
│ │ └── datasets.py # Dataset loaders: TOP, TOPv2, ATIS, SNIPS
│ ├── evaluation/
│ │ └── metrics.py # EM, Tree-F1, per-depth analysis, latency
│ ├── utils/
│ │ └── semantic_fragments.py # SemanticFragment, parse_logical_form, Tree-F1
│ └── pipeline.py # DSRPipeline (inference) + DSRTrainingPipeline
│
├── scripts/
│ ├── train.py # End-to-end training entry point
│ └── evaluate.py # Benchmark evaluation entry point
│
├── tests/
│ └── test_dsr.py # 30 unit tests (pytest)
│
├── requirements.txt
├── setup.py
└── README.md
src.models.cfs| Class/Function | Description |
|---|---|
CFSEncoder | RoBERTa-Large bi-encoder with projection head |
InfoNCELoss | Contrastive loss with temperature τ (Eq. 1) |
ContrastiveFragmentSelector | Retriever: indexes demo bank, retrieves top-k by structural similarity |
mine_hard_negatives(queries, lfs, threshold) | Builds triplets with hard negatives (Jaccard > threshold, different intent) |
train_cfs_encoder(encoder, triplets, ...) | Trains CFS encoder for 10 epochs with τ=0.05 |
src.models.structural_cot| Class/Function | Description |
|---|---|
generate_structural_rationale(query, lf) | DFS linearization of a logical form into step-by-step CoT |
build_structural_cot_prompt(query, demos) | Builds few-shot prompt with retrieved demonstrations |
extract_logical_form_from_response(text) | Parses model output to extract the logical form |
TeacherModel | Wrapper for 70B teacher (local HF or API) |
build_augmented_dataset(...) | Builds D_aug = {(x_i, r_i, y_i)} for distillation |
src.models.trie_decoder| Class/Function | Description |
|---|---|
GrammarTrie(schema) | Prefix trie from intent/slot schema |
LogitMaskConstraint | Applies dynamic -∞ mask (Eq. 3) at each decoding step |
build_schema_from_dataset(logical_forms) | Auto-extracts schema from a dataset |
src.models.parser_critic| Class/Function | Description |
|---|---|
Critic | Approximates inverse mapping P(x|y); scores via cosine similarity (Eq. 4) |
ParserCriticLoop | δ=0.85, K=3, T=0.7; verifies and optionally regenerates |
src.training.distillation| Class/Function | Description |
|---|---|
DistillationConfig | All hyperparameters for student training |
RationaleDistillationDataset | Torch dataset: prompt + rationale + logical form |
RationaleDistillationTrainer | SFT trainer implementing Eq. 2; supports LoRA + 4-bit |
StudentParser | Inference wrapper; supports HuggingFace and vLLM backends |
src.utils.semantic_fragments| Function | Description |
|---|---|
parse_logical_form(lf) | Parse LF string → SemanticFragment tree |
exact_match(pred, gold) | Strict EM check with whitespace normalization |
compute_tree_f1(pred, gold) | Fragment-level F1 score |
logical_form_to_dfs_steps(lf) | Convert LF to ordered CoT step list |
No fine-tuning required. Just provide demonstrations from existing domains and let CFS retrieve structurally similar examples:
pipeline = DSRPipeline.from_pretrained(
"./checkpoints/student",
demo_bank=existing_domain_examples, # e.g., navigation + reminder demos
)
# CFS will retrieve structurally similar fragments even for unseen "healthcare" slots
result = pipeline.parse("Schedule a blood test for next Monday at 9am")
from src.models.trie_decoder import GrammarTrie
schema = {
"intents": ["IN:BOOK_APPOINTMENT", "IN:CANCEL_APPOINTMENT"],
"slots": {
"IN:BOOK_APPOINTMENT": ["SL:DATE", "SL:TIME", "SL:DOCTOR"],
"IN:CANCEL_APPOINTMENT": ["SL:DATE", "SL:APPOINTMENT_ID"],
},
"special_tokens": ["[", "]"],
}
trie = GrammarTrie(schema)
trie.build()
# Attach to pipeline for constrained decoding
from sentence_transformers import SentenceTransformer
from src.models.parser_critic import Critic, ParserCriticLoop
sbert = SentenceTransformer("all-mpnet-base-v2")
critic = Critic(sentence_encoder=sbert)
loop = ParserCriticLoop(critic, threshold=0.85, max_iterations=3)
# Run all 30 unit tests
pytest tests/ -v
# Run with coverage report
pytest tests/ --cov=src --cov-report=term-missing
The test suite covers all five components without requiring model weights or dataset downloads.
MIT License. See LICENSE for details.
1 commits
Python
100.0%
Efficient Semantic Parsing via Chain-of-Thought Rationalization and Contrastive Demonstration Selection
Seyed Hossein Ahmadpanah — Department of Computer Engineering, Islamic Azad University, Tehran, Iran
DSR is a neuro-symbolic framework for Hierarchical Semantic Parsing (HSP) that translates natural language queries into executable, nested logical forms. It addresses two core limitations of LLM-based parsers:
DSR closes the gap by distilling the structural reasoning of a large Teacher model (Llama-2-70B) into a compact Student model (Llama-3-8B), achieving 87.42% Exact Match on TOP — a new state-of-the-art for open-source models — while running 9.4× faster than the teacher.
| Model | Params | TOP EM | ATIS EM | SNIPS EM | Latency |
|---|---|---|---|---|---|
| Seq2Seq-PTR | 125M | 83.90% | 87.10% | 87.14% | Fast |
| BART-Large (FT) | 400M | 86.40% | 88.20% | 90.50% | Fast |
| Llama-2-70B (Standard CoT) | 70B | 85.30% | 89.50% | 91.20% | 18 tok/s |
| Teacher (CFS + Structural CoT) | 70B | 87.95% | 91.80% | 93.10% | 18 tok/s |
| Llama-3-8B (Standard SFT) | 8B | 85.11% | 89.15% | 91.85% | ~150 tok/s |
| Student (Distilled CoT-SR) | 8B | 87.42% | 91.25% | 92.88% | 170 tok/s |
Ablation — Retrieval Strategy (TOP, Teacher model):
| Retrieval | EM | Struct. Error | Δ vs BM25 |
|---|---|---|---|
| Random | 78.40% | 12.5% | — |
| BM25 | 82.15% | 8.2% | — |
| Sentence-BERT | 86.10% | 4.5% | +3.95% |
| CFS (Ours) | 87.95% | 1.2% | +5.80% |
Zero-shot domain transfer (TOPv2 Weather): 71.4% EM vs. 42.3% for fixed-classifier baseline.
The DSR framework has two training phases and four novel components:
Phase 1: Rationale Synthesis (Teacher)
┌─────────────────────────────────────────────────────────┐
│ Input Query │
│ │ │
│ [CFS Encoder] ──→ Top-k Structurally Similar Demos │
│ │ │
│ [Prompt Builder] ──→ Structural CoT Prompt │
│ │ │
│ [Teacher LLM, 70B] ──→ Structural CoT Trace r + LF y │
└─────────────────────────────────────────────────────────┘
Phase 2: Efficient Inference (Student)
┌─────────────────────────────────────────────────────────┐
│ Augmented Dataset D_aug = {(x_i, r_i, y_i)} │
│ │ │
│ [Rationale Distillation] ──→ Fine-tuned Student (8B) │
│ │ │
│ [Trie-Based Constraints] ──→ Syntax-valid decoding │
│ │ │
│ [Parser-Critic Loop] ──→ Semantic self-correction │
└─────────────────────────────────────────────────────────┘
A bi-encoder (RoBERTa-Large) trained with InfoNCE loss on hard-negative triplets:
$$\mathcal{L}{CFS} = -\log \frac{\exp(\text{sim}(h_x, h{x^+}) / \tau)}{\sum_{j \in {x^+, x^-}} \exp(\text{sim}(h_x, h_j) / \tau)}$$
Decomposes the hierarchical parse tree into a Depth-First Search (DFS) sequence of semantic fragment decoding steps:
Query: "Book a flight for three people to Boston"
Step 1: Identify Root Intent [IN:FLIGHT].
Step 2: Identify Slot [SL:QUANTITY] with value 'three people' (depth 1).
Step 3: Identify Slot [SL:DEST] with value 'Boston' (depth 1).
Final: Combine all fragments → [IN:FLIGHT [SL:QUANTITY three people ] [SL:DEST Boston ] ]
Trains the Student on the Teacher's CoT traces via Supervised Fine-Tuning (SFT):
$$\mathcal{L}{\text{distill}} = -\sum{t=1}^{|r|+|y|} \log P_{M_S}(z_t \mid x, z_{<t})$$
where $z = [r ; y]$ is the concatenation of the reasoning trace $r$ and logical form $y$.
A prefix trie built from the domain schema masks invalid tokens at each decoding step:
$$m_t[w] = \begin{cases} 0 & \text{if } w \in V_{\text{valid}}(z_{<t}) \ -\infty & \text{otherwise} \end{cases}$$
Eliminates 100% of syntax errors (hallucinated slots, invalid bracket nesting).
Cycle-consistency check for semantic fidelity:
$$\text{Score}(\hat{y}) = \cos(E(x), E(\hat{x})) > \delta$$
git clone https://github.com/ahmadpanah/distilling-structural-reasoning
cd distilling-structural-reasoning
# Install core dependencies
pip install -r requirements.txt
# Install as a package (editable)
pip install -e .
# Optional: fast inference with vLLM
pip install vllm
Requirements: Python ≥ 3.9, PyTorch ≥ 2.0, CUDA GPU recommended (A100 for full training)
from src.pipeline import DSRPipeline
# Load pipeline with a fine-tuned student checkpoint
pipeline = DSRPipeline.from_pretrained(
student_checkpoint="./checkpoints/student",
retriever_checkpoint="./checkpoints/cfs_encoder.pt",
demo_bank=demo_bank, # list of {'query': ..., 'logical_form': ..., 'rationale': ...}
)
result = pipeline.parse("Book a flight from New York to Boston for three people")
print(result["logical_form"])
# [IN:FLIGHT [SL:ORG New York ] [SL:DEST Boston ] [SL:QUANTITY three people ] ]
print(f"Critic score: {result['score']:.3f}")
print(f"Demos used: {result['demos_used']}")
queries = [
"What is the weather in Paris tomorrow?",
"Set a reminder for my 3pm meeting",
"Navigate to the nearest coffee shop",
]
results = pipeline.parse_batch(queries)
for q, r in zip(queries, results):
print(f"Q: {q}")
print(f"LF: {r['logical_form']}\n")
The framework supports four benchmarks. Download each dataset and place under ./data/:
data/
├── top/
│ ├── train.tsv # query \t logical_form
│ ├── eval.tsv
│ └── test.tsv
├── topv2/
│ ├── navigation/
│ ├── reminder/
│ ├── weather/
│ └── ...
├── atis/
│ ├── train.json
│ ├── dev.json
│ └── test.json
└── snips/
├── train.json
├── dev.json
└── test.json
| Dataset | Size | Domains | Depth |
|---|---|---|---|
| TOP | ~44K | Navigation, Events | Deep (≥4) |
| TOPv2 | ~180K | + Reminder, Weather, Music | Deep |
| ATIS | ~5.4K | Flight booking | Medium |
| SNIPS | ~14K | 7 voice intents | Shallow |
from src.data.datasets import load_dataset
dataset = load_dataset("top", data_dir="./data/top")
print(dataset)
# TOPDataset({'train': 39281, 'dev': 2318, 'test': 4370})
# Access splits
for ex in dataset.train[:3]:
print(ex["query"], "→", ex["logical_form"])
python scripts/train.py \
--dataset top \
--data_dir ./data/top \
--student_model meta-llama/Meta-Llama-3-8B-Instruct \
--output_dir ./checkpoints \
--use_gold_rationale # No teacher inference needed
--use_gold_rationale generates structural CoT traces deterministically from the gold logical forms (DFS linearization). This is equivalent to the main setup in the paper and requires no 70B teacher model at training time.
To use a live Teacher model for rationale generation (requires the 70B model):
python scripts/train.py \
--dataset top \
--data_dir ./data/top \
--no_gold_rationale \
--teacher_model meta-llama/Llama-2-70b-chat-hf
Step 1: Train CFS Retriever
from src.models.cfs import CFSEncoder, mine_hard_negatives, train_cfs_encoder
from src.data.datasets import load_dataset
dataset = load_dataset("top", "./data/top")
queries = [ex["query"] for ex in dataset.train]
lfs = [ex["logical_form"] for ex in dataset.train]
triplets = mine_hard_negatives(queries, lfs, jaccard_threshold=0.5)
print(f"Mined {len(triplets)} hard-negative triplets")
encoder = CFSEncoder(model_name="roberta-large")
encoder = train_cfs_encoder(
encoder,
triplets,
num_epochs=10, # Paper: 10 epochs
temperature=0.05, # Paper: τ = 0.05
)
import torch
torch.save(encoder.state_dict(), "./checkpoints/cfs_encoder.pt")
Step 2: Build Augmented Dataset
from src.models.cfs import ContrastiveFragmentSelector
from src.models.structural_cot import build_augmented_dataset
selector = ContrastiveFragmentSelector(encoder, demo_bank=dataset.train, top_k=5)
selector.index_demo_bank()
augmented = build_augmented_dataset(
queries=queries,
logical_forms=lfs,
demo_selector=selector,
teacher=None,
use_gold_rationale=True, # DFS linearization (no 70B needed)
)
# augmented[0] → {'query': ..., 'logical_form': ..., 'rationale': 'Step 1: ...'}
Step 3: Distill Student
from src.training.distillation import RationaleDistillationTrainer, DistillationConfig
config = DistillationConfig(
student_model_name="meta-llama/Meta-Llama-3-8B-Instruct",
num_epochs=3,
batch_size=4,
gradient_accumulation_steps=8,
learning_rate=2e-4,
use_lora=True,
lora_r=16,
output_dir="./checkpoints/student",
)
trainer = RationaleDistillationTrainer(config)
trainer.train(
train_examples=augmented,
eval_examples=dataset.dev,
)
| Stage | GPU Memory | Time (A100) |
|---|---|---|
| CFS Retriever Training | ~16 GB | ~2 hours |
| Augmented Dataset (gold rationale) | CPU only | ~10 min |
| Student SFT (Llama-3-8B + LoRA + 4-bit) | ~24 GB | ~8 hours |
| Teacher Rationale Generation (70B) | 140+ GB | ~48 hours |
# Evaluate student on TOP test set
python scripts/evaluate.py \
--student_checkpoint ./checkpoints/student \
--retriever_checkpoint ./checkpoints/cfs_encoder.pt \
--dataset top \
--data_dir ./data/top \
--split test
# Ablation: no CFS retrieval (uses BM25-style random retrieval)
python scripts/evaluate.py \
--student_checkpoint ./checkpoints/student \
--dataset top --data_dir ./data/top \
--no_cfs
# Zero-shot domain transfer (Weather domain, no weather training data)
python scripts/evaluate.py \
--student_checkpoint ./checkpoints/student \
--dataset topv2 --data_dir ./data/topv2 \
--domain weather \
--zero_shot
# Save results to JSON
python scripts/evaluate.py \
--student_checkpoint ./checkpoints/student \
--dataset atis --data_dir ./data/atis \
--output_file results/atis_test.json
from src.data.datasets import load_dataset
from src.evaluation.metrics import Evaluator
from src.pipeline import DSRPipeline
dataset = load_dataset("top", "./data/top")
pipeline = DSRPipeline.from_pretrained("./checkpoints/student", demo_bank=dataset.train)
evaluator = Evaluator(dataset_name="TOP")
result = evaluator.evaluate(
parser_fn=lambda q: pipeline.parse(q)["logical_form"],
examples=dataset.test,
)
print(result)
Output:
=== TOP Results ===
Exact Match: 87.42%
Tree-F1: 0.9156
Struct. Error: 0.00%
Avg Latency: 5.9ms
Tokens/sec: 170.0
Num Examples: 4370
Per-depth EM:
Depth 1: 94.20%
Depth 2: 89.31%
Depth 3: 82.10%
Depth 4: 71.80%
Depth 5: 62.40%
distilling-structural-reasoning/
│
├── src/
│ ├── models/
│ │ ├── cfs.py # Contrastive Fragment Selection
│ │ │ # CFSEncoder, InfoNCELoss, ContrastiveFragmentSelector
│ │ │ # mine_hard_negatives, train_cfs_encoder
│ │ ├── structural_cot.py # Structural Chain-of-Thought prompting
│ │ │ # TeacherModel, build_augmented_dataset
│ │ ├── trie_decoder.py # Trie-Based Constrained Decoding
│ │ │ # GrammarTrie, LogitMaskConstraint
│ │ └── parser_critic.py # Parser-Critic Feedback Loop
│ │ # Critic, ParserCriticLoop
│ ├── training/
│ │ └── distillation.py # Rationale Distillation Protocol
│ │ # RationaleDistillationDataset, Trainer, StudentParser
│ ├── data/
│ │ └── datasets.py # Dataset loaders: TOP, TOPv2, ATIS, SNIPS
│ ├── evaluation/
│ │ └── metrics.py # EM, Tree-F1, per-depth analysis, latency
│ ├── utils/
│ │ └── semantic_fragments.py # SemanticFragment, parse_logical_form, Tree-F1
│ └── pipeline.py # DSRPipeline (inference) + DSRTrainingPipeline
│
├── scripts/
│ ├── train.py # End-to-end training entry point
│ └── evaluate.py # Benchmark evaluation entry point
│
├── tests/
│ └── test_dsr.py # 30 unit tests (pytest)
│
├── requirements.txt
├── setup.py
└── README.md
src.models.cfs| Class/Function | Description |
|---|---|
CFSEncoder | RoBERTa-Large bi-encoder with projection head |
InfoNCELoss | Contrastive loss with temperature τ (Eq. 1) |
ContrastiveFragmentSelector | Retriever: indexes demo bank, retrieves top-k by structural similarity |
mine_hard_negatives(queries, lfs, threshold) | Builds triplets with hard negatives (Jaccard > threshold, different intent) |
train_cfs_encoder(encoder, triplets, ...) | Trains CFS encoder for 10 epochs with τ=0.05 |
src.models.structural_cot| Class/Function | Description |
|---|---|
generate_structural_rationale(query, lf) | DFS linearization of a logical form into step-by-step CoT |
build_structural_cot_prompt(query, demos) | Builds few-shot prompt with retrieved demonstrations |
extract_logical_form_from_response(text) | Parses model output to extract the logical form |
TeacherModel | Wrapper for 70B teacher (local HF or API) |
build_augmented_dataset(...) | Builds D_aug = {(x_i, r_i, y_i)} for distillation |
src.models.trie_decoder| Class/Function | Description |
|---|---|
GrammarTrie(schema) | Prefix trie from intent/slot schema |
LogitMaskConstraint | Applies dynamic -∞ mask (Eq. 3) at each decoding step |
build_schema_from_dataset(logical_forms) | Auto-extracts schema from a dataset |
src.models.parser_critic| Class/Function | Description |
|---|---|
Critic | Approximates inverse mapping P(x|y); scores via cosine similarity (Eq. 4) |
ParserCriticLoop | δ=0.85, K=3, T=0.7; verifies and optionally regenerates |
src.training.distillation| Class/Function | Description |
|---|---|
DistillationConfig | All hyperparameters for student training |
RationaleDistillationDataset | Torch dataset: prompt + rationale + logical form |
RationaleDistillationTrainer | SFT trainer implementing Eq. 2; supports LoRA + 4-bit |
StudentParser | Inference wrapper; supports HuggingFace and vLLM backends |
src.utils.semantic_fragments| Function | Description |
|---|---|
parse_logical_form(lf) | Parse LF string → SemanticFragment tree |
exact_match(pred, gold) | Strict EM check with whitespace normalization |
compute_tree_f1(pred, gold) | Fragment-level F1 score |
logical_form_to_dfs_steps(lf) | Convert LF to ordered CoT step list |
No fine-tuning required. Just provide demonstrations from existing domains and let CFS retrieve structurally similar examples:
pipeline = DSRPipeline.from_pretrained(
"./checkpoints/student",
demo_bank=existing_domain_examples, # e.g., navigation + reminder demos
)
# CFS will retrieve structurally similar fragments even for unseen "healthcare" slots
result = pipeline.parse("Schedule a blood test for next Monday at 9am")
from src.models.trie_decoder import GrammarTrie
schema = {
"intents": ["IN:BOOK_APPOINTMENT", "IN:CANCEL_APPOINTMENT"],
"slots": {
"IN:BOOK_APPOINTMENT": ["SL:DATE", "SL:TIME", "SL:DOCTOR"],
"IN:CANCEL_APPOINTMENT": ["SL:DATE", "SL:APPOINTMENT_ID"],
},
"special_tokens": ["[", "]"],
}
trie = GrammarTrie(schema)
trie.build()
# Attach to pipeline for constrained decoding
from sentence_transformers import SentenceTransformer
from src.models.parser_critic import Critic, ParserCriticLoop
sbert = SentenceTransformer("all-mpnet-base-v2")
critic = Critic(sentence_encoder=sbert)
loop = ParserCriticLoop(critic, threshold=0.85, max_iterations=3)
# Run all 30 unit tests
pytest tests/ -v
# Run with coverage report
pytest tests/ --cov=src --cov-report=term-missing
The test suite covers all five components without requiring model weights or dataset downloads.
MIT License. See LICENSE for details.
1 commits
Python
100.0%