Spaces:
Runtime error
Runtime error
Deploy CodeBERT inference Space
Browse files- .gitignore +11 -0
- Dockerfile +17 -0
- Dockerfile.inference +17 -0
- README.md +27 -5
- README_INFERENCE_SPACE.md +32 -0
- README_TRAIN_SPACE.md +45 -0
- app.py +98 -0
- config/codebert_labels.yaml +29 -0
- config/error_categories.yaml +46 -0
- hub/CODEBERT_MODEL_CARD.md +65 -0
- requirements.txt +12 -0
- scripts/deploy_inference_space.sh +76 -0
- scripts/deploy_train_space.sh +62 -0
- scripts/push_codebert_to_hub.py +97 -0
- src/__init__.py +1 -0
- src/categories.py +35 -0
- src/codebert_dataset.py +117 -0
- src/codebert_formatting.py +28 -0
- src/codebert_labels.py +82 -0
- src/device_utils.py +13 -0
- src/exercises.py +228 -0
- src/generate_dataset.py +115 -0
- src/hf_eval_codebert.py +69 -0
- src/hf_metrics.py +52 -0
- src/hf_predict_codebert.py +177 -0
- src/hf_train_codebert.py +231 -0
- src/sql_templates.py +258 -0
- train_space_app.py +230 -0
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
.venv/
|
| 4 |
+
venv/
|
| 5 |
+
.env
|
| 6 |
+
data/*.parquet
|
| 7 |
+
models/
|
| 8 |
+
.DS_Store
|
| 9 |
+
*.egg-info/
|
| 10 |
+
dist/
|
| 11 |
+
build/
|
Dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Inference Space — loads model from Hugging Face Hub at startup
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 8 |
+
pip install --no-cache-dir -r requirements.txt
|
| 9 |
+
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
ENV GRADIO_SERVER_NAME=0.0.0.0
|
| 13 |
+
ENV GRADIO_SERVER_PORT=7860
|
| 14 |
+
ENV SPACE_MODEL_ID=nishu08/sql-codebert-classifier
|
| 15 |
+
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
CMD ["python", "app.py"]
|
Dockerfile.inference
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Inference Space — loads model from Hugging Face Hub at startup
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 8 |
+
pip install --no-cache-dir -r requirements.txt
|
| 9 |
+
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
ENV GRADIO_SERVER_NAME=0.0.0.0
|
| 13 |
+
ENV GRADIO_SERVER_PORT=7860
|
| 14 |
+
ENV SPACE_MODEL_ID=nishu08/sql-codebert-classifier
|
| 15 |
+
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,32 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: SQL Error Classifier
|
| 3 |
+
emoji: 🗄️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# SQL Error Classifier — Inference
|
| 13 |
+
|
| 14 |
+
Classify student SQL mistakes using a **CodeBERT cross-encoder** model.
|
| 15 |
+
|
| 16 |
+
Model: [nishu08/sql-codebert-classifier](https://huggingface.co/nishu08/sql-codebert-classifier)
|
| 17 |
+
|
| 18 |
+
## Inputs
|
| 19 |
+
|
| 20 |
+
- **Question** — what the student was asked
|
| 21 |
+
- **Schema** — available tables/columns
|
| 22 |
+
- **Student SQL** — student's submission
|
| 23 |
+
- **Correct SQL** — reference answer
|
| 24 |
+
|
| 25 |
+
## Labels
|
| 26 |
+
|
| 27 |
+
`JOIN_ERROR`, `AGGREGATION_ERROR`, `FILTER_ERROR`, `WINDOW_FUNCTION_ERROR`,
|
| 28 |
+
`SUBQUERY_ERROR`, `NULL_HANDLING_ERROR`, `PERFORMANCE_ERROR`, `LOGICAL_ERROR`, `SYNTAX_ERROR`
|
| 29 |
+
|
| 30 |
+
## Secrets (optional)
|
| 31 |
+
|
| 32 |
+
- `SPACE_MODEL_ID` — override model repo (default: `nishu08/sql-codebert-classifier`)
|
README_INFERENCE_SPACE.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SQL Error Classifier
|
| 3 |
+
emoji: 🗄️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# SQL Error Classifier — Inference
|
| 13 |
+
|
| 14 |
+
Classify student SQL mistakes using a **CodeBERT cross-encoder** model.
|
| 15 |
+
|
| 16 |
+
Model: [nishu08/sql-codebert-classifier](https://huggingface.co/nishu08/sql-codebert-classifier)
|
| 17 |
+
|
| 18 |
+
## Inputs
|
| 19 |
+
|
| 20 |
+
- **Question** — what the student was asked
|
| 21 |
+
- **Schema** — available tables/columns
|
| 22 |
+
- **Student SQL** — student's submission
|
| 23 |
+
- **Correct SQL** — reference answer
|
| 24 |
+
|
| 25 |
+
## Labels
|
| 26 |
+
|
| 27 |
+
`JOIN_ERROR`, `AGGREGATION_ERROR`, `FILTER_ERROR`, `WINDOW_FUNCTION_ERROR`,
|
| 28 |
+
`SUBQUERY_ERROR`, `NULL_HANDLING_ERROR`, `PERFORMANCE_ERROR`, `LOGICAL_ERROR`, `SYNTAX_ERROR`
|
| 29 |
+
|
| 30 |
+
## Secrets (optional)
|
| 31 |
+
|
| 32 |
+
- `SPACE_MODEL_ID` — override model repo (default: `nishu08/sql-codebert-classifier`)
|
README_TRAIN_SPACE.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SQL Error Classifier Training
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
hardware: t4-small
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# SQL Error Classifier — CodeBERT Training Space
|
| 14 |
+
|
| 15 |
+
Train `microsoft/codebert-base` as a **cross-encoder** for multi-label SQL error classification.
|
| 16 |
+
|
| 17 |
+
## Setup
|
| 18 |
+
|
| 19 |
+
1. **Hardware:** Settings → Hardware → **GPU t4-small** (recommended)
|
| 20 |
+
2. **Secrets:** Settings → Secrets → add `HF_TOKEN` (Hugging Face write token) to push models to your account
|
| 21 |
+
3. **Data:** Include `data/sql_errors_dev.parquet` in this Space repo, or upload parquet at runtime
|
| 22 |
+
|
| 23 |
+
## Usage
|
| 24 |
+
|
| 25 |
+
1. Choose bundled dataset or upload your own parquet
|
| 26 |
+
2. Set epochs, batch size, max samples
|
| 27 |
+
3. Click **Start Training**
|
| 28 |
+
4. Optionally enable **Push to Hub** with model id `your-username/sql-codebert-classifier`
|
| 29 |
+
|
| 30 |
+
## Dataset columns
|
| 31 |
+
|
| 32 |
+
Required (aliases supported):
|
| 33 |
+
|
| 34 |
+
| Column | Aliases |
|
| 35 |
+
|--------|---------|
|
| 36 |
+
| `question` | — |
|
| 37 |
+
| `schema` | — |
|
| 38 |
+
| `student_sql` | `query` |
|
| 39 |
+
| `correct_sql` | `correct_query` |
|
| 40 |
+
| `error_labels` | `label_name` |
|
| 41 |
+
|
| 42 |
+
## Labels (9-class multi-label)
|
| 43 |
+
|
| 44 |
+
`JOIN_ERROR`, `AGGREGATION_ERROR`, `FILTER_ERROR`, `WINDOW_FUNCTION_ERROR`,
|
| 45 |
+
`SUBQUERY_ERROR`, `NULL_HANDLING_ERROR`, `PERFORMANCE_ERROR`, `LOGICAL_ERROR`, `SYNTAX_ERROR`
|
app.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CodeBERT inference Gradio app for Hugging Face Spaces."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
|
| 9 |
+
from src.hf_predict_codebert import CodeBERTSQLErrorClassifier
|
| 10 |
+
|
| 11 |
+
MODEL_ID = os.getenv("SPACE_MODEL_ID", "nishu08/sql-codebert-classifier")
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
clf = CodeBERTSQLErrorClassifier(MODEL_ID)
|
| 15 |
+
model_status = f"Model loaded: **{MODEL_ID}**"
|
| 16 |
+
except Exception as exc:
|
| 17 |
+
clf = None
|
| 18 |
+
model_status = (
|
| 19 |
+
f"Could not load `{MODEL_ID}`: {exc}\n\n"
|
| 20 |
+
"Push your model first: `python scripts/push_codebert_to_hub.py --repo-id nishu08/sql-codebert-classifier`"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
EXAMPLES = [
|
| 24 |
+
[
|
| 25 |
+
"What is the average score of students in each department?",
|
| 26 |
+
"students(id, name, score, department_id) | departments(id, name)",
|
| 27 |
+
"SELECT department_id, SUM(score) FROM students GROUP BY department_id",
|
| 28 |
+
"SELECT department_id, AVG(score) FROM students GROUP BY department_id",
|
| 29 |
+
0.5,
|
| 30 |
+
],
|
| 31 |
+
[
|
| 32 |
+
"Find students who have not provided an email address.",
|
| 33 |
+
"students(id, name, email, phone)",
|
| 34 |
+
"SELECT name FROM students WHERE email = NULL",
|
| 35 |
+
"SELECT name FROM students WHERE email IS NULL",
|
| 36 |
+
0.5,
|
| 37 |
+
],
|
| 38 |
+
[
|
| 39 |
+
"List each student's name along with their department name.",
|
| 40 |
+
"students(id, name, department_id) | departments(id, name)",
|
| 41 |
+
"SELECT students.name, departments.name FROM students JOIN departments",
|
| 42 |
+
"SELECT students.name, departments.name FROM students INNER JOIN departments ON students.department_id = departments.id",
|
| 43 |
+
0.5,
|
| 44 |
+
],
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def classify(question, schema, student_sql, correct_sql, threshold):
|
| 49 |
+
if clf is None:
|
| 50 |
+
return "Model not loaded.", ""
|
| 51 |
+
result = clf.predict(
|
| 52 |
+
question=question.strip(),
|
| 53 |
+
schema=schema.strip(),
|
| 54 |
+
student_sql=student_sql.strip(),
|
| 55 |
+
correct_sql=correct_sql.strip(),
|
| 56 |
+
threshold=threshold,
|
| 57 |
+
)
|
| 58 |
+
summary = (
|
| 59 |
+
f"### {result['primary_label']}\n"
|
| 60 |
+
f"Confidence: **{result['primary_confidence']:.1%}**\n\n"
|
| 61 |
+
f"**Active labels:** {', '.join(result['error_labels']) or 'none'}"
|
| 62 |
+
)
|
| 63 |
+
probs = "\n".join(
|
| 64 |
+
f"- **{k}**: {v:.1%}" for k, v in result["probabilities"].items()
|
| 65 |
+
)
|
| 66 |
+
return summary, probs
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
with gr.Blocks(title="SQL Error Classifier", theme=gr.themes.Soft()) as demo:
|
| 70 |
+
gr.Markdown(
|
| 71 |
+
f"""
|
| 72 |
+
# SQL Error Classifier
|
| 73 |
+
CodeBERT cross-encoder — classifies student SQL mistakes into error categories.
|
| 74 |
+
|
| 75 |
+
{model_status}
|
| 76 |
+
"""
|
| 77 |
+
)
|
| 78 |
+
with gr.Row():
|
| 79 |
+
with gr.Column():
|
| 80 |
+
question = gr.Textbox(label="Question", lines=2)
|
| 81 |
+
schema = gr.Textbox(label="Schema", lines=2)
|
| 82 |
+
student_sql = gr.Textbox(label="Student SQL", lines=4)
|
| 83 |
+
correct_sql = gr.Textbox(label="Correct SQL", lines=4)
|
| 84 |
+
threshold = gr.Slider(0.1, 0.9, value=0.5, step=0.05, label="Threshold")
|
| 85 |
+
btn = gr.Button("Classify", variant="primary")
|
| 86 |
+
with gr.Column():
|
| 87 |
+
prediction = gr.Markdown(label="Prediction")
|
| 88 |
+
probabilities = gr.Markdown(label="Probabilities")
|
| 89 |
+
|
| 90 |
+
gr.Examples(examples=EXAMPLES, inputs=[question, schema, student_sql, correct_sql, threshold])
|
| 91 |
+
btn.click(
|
| 92 |
+
classify,
|
| 93 |
+
[question, schema, student_sql, correct_sql, threshold],
|
| 94 |
+
[prediction, probabilities],
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
config/codebert_labels.yaml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Primary labels for CodeBERT cross-encoder training
|
| 2 |
+
labels:
|
| 3 |
+
- JOIN_ERROR
|
| 4 |
+
- AGGREGATION_ERROR
|
| 5 |
+
- FILTER_ERROR
|
| 6 |
+
- WINDOW_FUNCTION_ERROR
|
| 7 |
+
- SUBQUERY_ERROR
|
| 8 |
+
- NULL_HANDLING_ERROR
|
| 9 |
+
- PERFORMANCE_ERROR
|
| 10 |
+
- LOGICAL_ERROR
|
| 11 |
+
- SYNTAX_ERROR
|
| 12 |
+
|
| 13 |
+
# Map dataset label_name values → one or more CodeBERT labels (multi-label)
|
| 14 |
+
alias_map:
|
| 15 |
+
JOIN_ERROR: [JOIN_ERROR]
|
| 16 |
+
AGGREGATION_ERROR: [AGGREGATION_ERROR]
|
| 17 |
+
HAVING_WHERE_ERROR: [AGGREGATION_ERROR]
|
| 18 |
+
FILTERING_ERROR: [FILTER_ERROR]
|
| 19 |
+
WINDOW_FUNCTION_ERROR: [WINDOW_FUNCTION_ERROR]
|
| 20 |
+
SUBQUERY_ERROR: [SUBQUERY_ERROR]
|
| 21 |
+
NULL_HANDLING_ERROR: [NULL_HANDLING_ERROR]
|
| 22 |
+
PERFORMANCE_ERROR: [PERFORMANCE_ERROR]
|
| 23 |
+
LOGICAL_QUERY_ERROR: [LOGICAL_ERROR]
|
| 24 |
+
SYNTAX_ERROR: [SYNTAX_ERROR]
|
| 25 |
+
DATE_FUNCTION_ERROR: [SYNTAX_ERROR]
|
| 26 |
+
COLUMN_REFERENCE_ERROR: [SYNTAX_ERROR]
|
| 27 |
+
TABLE_REFERENCE_ERROR: [SYNTAX_ERROR]
|
| 28 |
+
DATA_TYPE_ERROR: [SYNTAX_ERROR]
|
| 29 |
+
DUPLICATE_RECORD_ERROR: [FILTER_ERROR]
|
config/error_categories.yaml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
categories:
|
| 2 |
+
- id: 0
|
| 3 |
+
name: SYNTAX_ERROR
|
| 4 |
+
description: Missing comma, bracket, quote
|
| 5 |
+
- id: 1
|
| 6 |
+
name: JOIN_ERROR
|
| 7 |
+
description: Missing ON, wrong join condition
|
| 8 |
+
- id: 2
|
| 9 |
+
name: AGGREGATION_ERROR
|
| 10 |
+
description: Missing GROUP BY
|
| 11 |
+
- id: 3
|
| 12 |
+
name: HAVING_WHERE_ERROR
|
| 13 |
+
description: Using aggregate in WHERE
|
| 14 |
+
- id: 4
|
| 15 |
+
name: SUBQUERY_ERROR
|
| 16 |
+
description: Multiple rows returned
|
| 17 |
+
- id: 5
|
| 18 |
+
name: WINDOW_FUNCTION_ERROR
|
| 19 |
+
description: Incorrect OVER/PARTITION BY
|
| 20 |
+
- id: 6
|
| 21 |
+
name: NULL_HANDLING_ERROR
|
| 22 |
+
description: "= NULL instead of IS NULL"
|
| 23 |
+
- id: 7
|
| 24 |
+
name: DATE_FUNCTION_ERROR
|
| 25 |
+
description: Incorrect date format/function
|
| 26 |
+
- id: 8
|
| 27 |
+
name: COLUMN_REFERENCE_ERROR
|
| 28 |
+
description: Column doesn't exist
|
| 29 |
+
- id: 9
|
| 30 |
+
name: TABLE_REFERENCE_ERROR
|
| 31 |
+
description: Table doesn't exist
|
| 32 |
+
- id: 10
|
| 33 |
+
name: DATA_TYPE_ERROR
|
| 34 |
+
description: Comparing integer with string
|
| 35 |
+
- id: 11
|
| 36 |
+
name: DUPLICATE_RECORD_ERROR
|
| 37 |
+
description: Missing DISTINCT
|
| 38 |
+
- id: 12
|
| 39 |
+
name: LOGICAL_QUERY_ERROR
|
| 40 |
+
description: Query runs but answer is wrong
|
| 41 |
+
- id: 13
|
| 42 |
+
name: PERFORMANCE_ERROR
|
| 43 |
+
description: "SELECT *, inefficient joins"
|
| 44 |
+
- id: 14
|
| 45 |
+
name: FILTERING_ERROR
|
| 46 |
+
description: Incorrect WHERE clause
|
hub/CODEBERT_MODEL_CARD.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language: en
|
| 3 |
+
license: mit
|
| 4 |
+
tags:
|
| 5 |
+
- codebert
|
| 6 |
+
- sql
|
| 7 |
+
- education
|
| 8 |
+
- text-classification
|
| 9 |
+
- cross-encoder
|
| 10 |
+
base_model: microsoft/codebert-base
|
| 11 |
+
pipeline_tag: text-classification
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# SQL CodeBERT Cross-Encoder
|
| 15 |
+
|
| 16 |
+
Multi-label SQL error classifier using **microsoft/codebert-base** as a cross-encoder.
|
| 17 |
+
|
| 18 |
+
## Input Format
|
| 19 |
+
|
| 20 |
+
All fields are concatenated into one sequence:
|
| 21 |
+
|
| 22 |
+
```
|
| 23 |
+
QUESTION:
|
| 24 |
+
{question}
|
| 25 |
+
|
| 26 |
+
SCHEMA:
|
| 27 |
+
{schema}
|
| 28 |
+
|
| 29 |
+
STUDENT_SQL:
|
| 30 |
+
{student_sql}
|
| 31 |
+
|
| 32 |
+
CORRECT_SQL:
|
| 33 |
+
{correct_sql}
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Labels
|
| 37 |
+
|
| 38 |
+
`JOIN_ERROR`, `AGGREGATION_ERROR`, `FILTER_ERROR`, `WINDOW_FUNCTION_ERROR`,
|
| 39 |
+
`SUBQUERY_ERROR`, `NULL_HANDLING_ERROR`, `PERFORMANCE_ERROR`, `LOGICAL_ERROR`, `SYNTAX_ERROR`
|
| 40 |
+
|
| 41 |
+
## Training
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
python -m src.hf_train_codebert \
|
| 45 |
+
--data data/sql_errors_1m.parquet \
|
| 46 |
+
--output-dir models/codebert-cross-encoder \
|
| 47 |
+
--epochs 3 \
|
| 48 |
+
--push-to-hub \
|
| 49 |
+
--hub-model-id YOUR_USERNAME/sql-codebert-cross-encoder
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Inference
|
| 53 |
+
|
| 54 |
+
```python
|
| 55 |
+
from src.hf_predict_codebert import CodeBERTSQLErrorClassifier
|
| 56 |
+
|
| 57 |
+
clf = CodeBERTSQLErrorClassifier("YOUR_USERNAME/sql-codebert-cross-encoder")
|
| 58 |
+
result = clf.predict(
|
| 59 |
+
question="What is the average score per department?",
|
| 60 |
+
schema="students(id, score, department_id)",
|
| 61 |
+
student_sql="SELECT department_id, SUM(score) FROM students GROUP BY department_id",
|
| 62 |
+
correct_sql="SELECT department_id, AVG(score) FROM students GROUP BY department_id",
|
| 63 |
+
)
|
| 64 |
+
print(result["error_labels"])
|
| 65 |
+
```
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy>=1.24.0
|
| 2 |
+
pandas>=2.0.0
|
| 3 |
+
scikit-learn>=1.3.0
|
| 4 |
+
pyarrow>=14.0.0
|
| 5 |
+
tqdm>=4.66.0
|
| 6 |
+
pyyaml>=6.0
|
| 7 |
+
torch>=2.0.0
|
| 8 |
+
transformers>=4.36.0
|
| 9 |
+
accelerate>=0.25.0
|
| 10 |
+
datasets>=2.16.0
|
| 11 |
+
huggingface_hub>=0.20.0
|
| 12 |
+
gradio>=4.44.0,<6.0.0
|
scripts/deploy_inference_space.sh
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Deploy CodeBERT inference Space to Hugging Face.
|
| 3 |
+
# Usage: ./scripts/deploy_inference_space.sh [YOUR_USERNAME/space-name]
|
| 4 |
+
set -euo pipefail
|
| 5 |
+
|
| 6 |
+
SPACE_SUFFIX="${1:-sql-error-classifier}"
|
| 7 |
+
MODEL_REPO="${2:-nishu08/sql-codebert-classifier}"
|
| 8 |
+
TOKEN="${HF_TOKEN:-${HUGGING_FACE_HUB_TOKEN:-}}"
|
| 9 |
+
|
| 10 |
+
if [[ -z "${TOKEN}" ]]; then
|
| 11 |
+
echo "Set HF_TOKEN before deploying."
|
| 12 |
+
exit 1
|
| 13 |
+
fi
|
| 14 |
+
|
| 15 |
+
if [[ "${SPACE_SUFFIX}" == */* ]]; then
|
| 16 |
+
SPACE_ID="${SPACE_SUFFIX}"
|
| 17 |
+
else
|
| 18 |
+
USERNAME=$(python - <<PY
|
| 19 |
+
from huggingface_hub import HfApi
|
| 20 |
+
api = HfApi(token="${TOKEN}")
|
| 21 |
+
print(api.whoami()["name"])
|
| 22 |
+
PY
|
| 23 |
+
)
|
| 24 |
+
SPACE_ID="${USERNAME}/${SPACE_SUFFIX}"
|
| 25 |
+
fi
|
| 26 |
+
|
| 27 |
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
| 28 |
+
WORKDIR=$(mktemp -d)
|
| 29 |
+
trap 'rm -rf "${WORKDIR}"' EXIT
|
| 30 |
+
|
| 31 |
+
echo "==> Preparing inference Space files..."
|
| 32 |
+
rsync -a \
|
| 33 |
+
--exclude '.venv' \
|
| 34 |
+
--exclude 'models' \
|
| 35 |
+
--exclude 'data' \
|
| 36 |
+
--exclude '__pycache__' \
|
| 37 |
+
--exclude '.git' \
|
| 38 |
+
"${ROOT}/" "${WORKDIR}/"
|
| 39 |
+
|
| 40 |
+
cp "${ROOT}/README_INFERENCE_SPACE.md" "${WORKDIR}/README.md"
|
| 41 |
+
cp "${ROOT}/Dockerfile.inference" "${WORKDIR}/Dockerfile"
|
| 42 |
+
|
| 43 |
+
# Bake default model id into Dockerfile
|
| 44 |
+
sed -i.bak "s|ENV SPACE_MODEL_ID=.*|ENV SPACE_MODEL_ID=${MODEL_REPO}|" "${WORKDIR}/Dockerfile"
|
| 45 |
+
rm -f "${WORKDIR}/Dockerfile.bak"
|
| 46 |
+
|
| 47 |
+
echo "==> Verifying model repo exists: ${MODEL_REPO}"
|
| 48 |
+
python - <<PY
|
| 49 |
+
from huggingface_hub import HfApi
|
| 50 |
+
api = HfApi(token="${TOKEN}")
|
| 51 |
+
api.model_info("${MODEL_REPO}")
|
| 52 |
+
print("Model found.")
|
| 53 |
+
PY
|
| 54 |
+
|
| 55 |
+
echo "==> Creating Space: ${SPACE_ID}"
|
| 56 |
+
python - <<PY
|
| 57 |
+
from huggingface_hub import HfApi
|
| 58 |
+
api = HfApi(token="${TOKEN}")
|
| 59 |
+
api.create_repo("${SPACE_ID}", repo_type="space", space_sdk="docker", exist_ok=True)
|
| 60 |
+
PY
|
| 61 |
+
|
| 62 |
+
echo "==> Uploading..."
|
| 63 |
+
python - <<PY
|
| 64 |
+
from huggingface_hub import HfApi
|
| 65 |
+
api = HfApi(token="${TOKEN}")
|
| 66 |
+
api.upload_folder(
|
| 67 |
+
folder_path="${WORKDIR}",
|
| 68 |
+
repo_id="${SPACE_ID}",
|
| 69 |
+
repo_type="space",
|
| 70 |
+
commit_message="Deploy CodeBERT inference Space",
|
| 71 |
+
)
|
| 72 |
+
PY
|
| 73 |
+
|
| 74 |
+
echo ""
|
| 75 |
+
echo "Done: https://huggingface.co/spaces/${SPACE_ID}"
|
| 76 |
+
echo "CPU hardware is sufficient for inference."
|
scripts/deploy_train_space.sh
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Deploy CodeBERT training Space to Hugging Face.
|
| 3 |
+
# Usage: ./scripts/deploy_train_space.sh [YOUR_USERNAME/space-name]
|
| 4 |
+
set -euo pipefail
|
| 5 |
+
|
| 6 |
+
SPACE_SUFFIX="${1:-sql-error-classifier-train}"
|
| 7 |
+
TOKEN="${HF_TOKEN:-${HUGGING_FACE_HUB_TOKEN:-}}"
|
| 8 |
+
|
| 9 |
+
if [[ -z "${TOKEN}" ]]; then
|
| 10 |
+
echo "Set HF_TOKEN before deploying."
|
| 11 |
+
exit 1
|
| 12 |
+
fi
|
| 13 |
+
|
| 14 |
+
# Resolve full space id — must use an account you own
|
| 15 |
+
if [[ "${SPACE_SUFFIX}" == */* ]]; then
|
| 16 |
+
SPACE_ID="${SPACE_SUFFIX}"
|
| 17 |
+
else
|
| 18 |
+
USERNAME=$(python - <<PY
|
| 19 |
+
from huggingface_hub import HfApi
|
| 20 |
+
api = HfApi(token="${TOKEN}")
|
| 21 |
+
print(api.whoami()["name"])
|
| 22 |
+
PY
|
| 23 |
+
)
|
| 24 |
+
SPACE_ID="${USERNAME}/${SPACE_SUFFIX}"
|
| 25 |
+
fi
|
| 26 |
+
|
| 27 |
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
| 28 |
+
WORKDIR=$(mktemp -d)
|
| 29 |
+
trap 'rm -rf "${WORKDIR}"' EXIT
|
| 30 |
+
|
| 31 |
+
echo "==> Preparing Space files..."
|
| 32 |
+
rsync -a \
|
| 33 |
+
--exclude '.venv' \
|
| 34 |
+
--exclude 'models' \
|
| 35 |
+
--exclude '__pycache__' \
|
| 36 |
+
--exclude '.git' \
|
| 37 |
+
--exclude '*.joblib' \
|
| 38 |
+
"${ROOT}/" "${WORKDIR}/"
|
| 39 |
+
|
| 40 |
+
cp "${ROOT}/README_TRAIN_SPACE.md" "${WORKDIR}/README.md"
|
| 41 |
+
|
| 42 |
+
echo "==> Creating Space: ${SPACE_ID}"
|
| 43 |
+
python - <<PY
|
| 44 |
+
from huggingface_hub import HfApi
|
| 45 |
+
api = HfApi(token="${TOKEN}")
|
| 46 |
+
api.create_repo("${SPACE_ID}", repo_type="space", space_sdk="docker", exist_ok=True)
|
| 47 |
+
PY
|
| 48 |
+
|
| 49 |
+
echo "==> Uploading..."
|
| 50 |
+
python - <<PY
|
| 51 |
+
from huggingface_hub import HfApi
|
| 52 |
+
api = HfApi(token="${TOKEN}")
|
| 53 |
+
api.upload_folder(
|
| 54 |
+
folder_path="${WORKDIR}",
|
| 55 |
+
repo_id="${SPACE_ID}",
|
| 56 |
+
repo_type="space",
|
| 57 |
+
commit_message="Deploy CodeBERT training Space",
|
| 58 |
+
)
|
| 59 |
+
PY
|
| 60 |
+
|
| 61 |
+
echo "Done: https://huggingface.co/spaces/${SPACE_ID}"
|
| 62 |
+
echo "Set Hardware → GPU t4-small, Secrets → HF_TOKEN"
|
scripts/push_codebert_to_hub.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Push a locally trained CodeBERT model to Hugging Face Hub."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 13 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 14 |
+
|
| 15 |
+
from huggingface_hub import HfApi, create_repo
|
| 16 |
+
|
| 17 |
+
from src.codebert_labels import load_codebert_labels
|
| 18 |
+
|
| 19 |
+
DEFAULT_MODEL_DIR = PROJECT_ROOT / "models" / "codebert-cross-encoder"
|
| 20 |
+
MODEL_CARD = PROJECT_ROOT / "hub" / "CODEBERT_MODEL_CARD.md"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def push(
|
| 24 |
+
model_dir: Path = DEFAULT_MODEL_DIR,
|
| 25 |
+
repo_id: str = "",
|
| 26 |
+
private: bool = False,
|
| 27 |
+
token: str | None = None,
|
| 28 |
+
) -> str:
|
| 29 |
+
if not model_dir.exists():
|
| 30 |
+
raise FileNotFoundError(f"Model not found at {model_dir}. Train first.")
|
| 31 |
+
|
| 32 |
+
if not repo_id:
|
| 33 |
+
raise ValueError("--repo-id required, e.g. nishu08/sql-codebert-classifier")
|
| 34 |
+
|
| 35 |
+
token = token or os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
|
| 36 |
+
if not token:
|
| 37 |
+
raise ValueError("Set HF_TOKEN environment variable")
|
| 38 |
+
|
| 39 |
+
# Ensure label config exists for inference
|
| 40 |
+
label_config = model_dir / "label_config.json"
|
| 41 |
+
if not label_config.exists():
|
| 42 |
+
with open(label_config, "w") as f:
|
| 43 |
+
json.dump(
|
| 44 |
+
{
|
| 45 |
+
"labels": load_codebert_labels(),
|
| 46 |
+
"model_name": "microsoft/codebert-base",
|
| 47 |
+
"architecture": "codebert-cross-encoder",
|
| 48 |
+
"threshold": 0.5,
|
| 49 |
+
"max_length": 512,
|
| 50 |
+
},
|
| 51 |
+
f,
|
| 52 |
+
indent=2,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
api = HfApi(token=token)
|
| 56 |
+
create_repo(repo_id, repo_type="model", private=private, exist_ok=True, token=token)
|
| 57 |
+
|
| 58 |
+
print(f"Uploading {model_dir} → {repo_id} ...")
|
| 59 |
+
api.upload_folder(
|
| 60 |
+
folder_path=str(model_dir),
|
| 61 |
+
repo_id=repo_id,
|
| 62 |
+
repo_type="model",
|
| 63 |
+
token=token,
|
| 64 |
+
commit_message="Upload CodeBERT SQL error classifier",
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
if MODEL_CARD.exists():
|
| 68 |
+
api.upload_file(
|
| 69 |
+
path_or_fileobj=str(MODEL_CARD),
|
| 70 |
+
path_in_repo="README.md",
|
| 71 |
+
repo_id=repo_id,
|
| 72 |
+
repo_type="model",
|
| 73 |
+
token=token,
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
url = f"https://huggingface.co/{repo_id}"
|
| 77 |
+
print(f"Done: {url}")
|
| 78 |
+
return url
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def main() -> None:
|
| 82 |
+
parser = argparse.ArgumentParser(description="Push CodeBERT model to HF Hub")
|
| 83 |
+
parser.add_argument("--model-dir", type=Path, default=DEFAULT_MODEL_DIR)
|
| 84 |
+
parser.add_argument("--repo-id", type=str, required=True)
|
| 85 |
+
parser.add_argument("--private", action="store_true")
|
| 86 |
+
parser.add_argument("--token", type=str, default=None)
|
| 87 |
+
args = parser.parse_args()
|
| 88 |
+
push(
|
| 89 |
+
model_dir=args.model_dir,
|
| 90 |
+
repo_id=args.repo_id,
|
| 91 |
+
private=args.private,
|
| 92 |
+
token=args.token,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
if __name__ == "__main__":
|
| 97 |
+
main()
|
src/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""SQL error classification package."""
|
src/categories.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Dict, List
|
| 6 |
+
|
| 7 |
+
import yaml
|
| 8 |
+
|
| 9 |
+
CONFIG_PATH = Path(__file__).resolve().parent.parent / "config" / "error_categories.yaml"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass(frozen=True)
|
| 13 |
+
class ErrorCategory:
|
| 14 |
+
id: int
|
| 15 |
+
name: str
|
| 16 |
+
description: str
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def load_categories(config_path: Path = CONFIG_PATH) -> List[ErrorCategory]:
|
| 20 |
+
with open(config_path) as f:
|
| 21 |
+
data = yaml.safe_load(f)
|
| 22 |
+
return [
|
| 23 |
+
ErrorCategory(id=c["id"], name=c["name"], description=c["description"])
|
| 24 |
+
for c in data["categories"]
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def id_to_name(categories: List[ErrorCategory] | None = None) -> Dict[int, str]:
|
| 29 |
+
cats = categories or load_categories()
|
| 30 |
+
return {c.id: c.name for c in cats}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def name_to_id(categories: List[ErrorCategory] | None = None) -> Dict[str, int]:
|
| 34 |
+
cats = categories or load_categories()
|
| 35 |
+
return {c.name: c.id for c in cats}
|
src/codebert_dataset.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PyTorch Dataset and preprocessing for CodeBERT Trainer."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import torch
|
| 10 |
+
from torch.utils.data import Dataset
|
| 11 |
+
from transformers import PreTrainedTokenizerBase
|
| 12 |
+
|
| 13 |
+
from src.codebert_formatting import format_cross_encoder_input
|
| 14 |
+
from src.codebert_labels import label_to_multihot, load_codebert_labels
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def normalize_dataframe(df: pd.DataFrame) -> pd.DataFrame:
|
| 18 |
+
"""Map project column names to the canonical training schema."""
|
| 19 |
+
col_map = {
|
| 20 |
+
"query": "student_sql",
|
| 21 |
+
"correct_query": "correct_sql",
|
| 22 |
+
"label_name": "error_labels",
|
| 23 |
+
}
|
| 24 |
+
out = df.rename(columns={k: v for k, v in col_map.items() if k in df.columns}).copy()
|
| 25 |
+
|
| 26 |
+
required = ["question", "schema", "student_sql", "correct_sql", "error_labels"]
|
| 27 |
+
missing = [c for c in required if c not in out.columns]
|
| 28 |
+
if missing:
|
| 29 |
+
raise ValueError(
|
| 30 |
+
f"Dataset missing required columns: {missing}. "
|
| 31 |
+
f"Expected {required} (or aliases query/correct_query/label_name)."
|
| 32 |
+
)
|
| 33 |
+
return out
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class SQLCodeBERTDataset(Dataset):
|
| 37 |
+
"""Tokenized SQL error dataset for Hugging Face Trainer."""
|
| 38 |
+
|
| 39 |
+
def __init__(
|
| 40 |
+
self,
|
| 41 |
+
df: pd.DataFrame,
|
| 42 |
+
tokenizer: PreTrainedTokenizerBase,
|
| 43 |
+
label_list: Optional[List[str]] = None,
|
| 44 |
+
max_length: int = 512,
|
| 45 |
+
):
|
| 46 |
+
self.df = normalize_dataframe(df).reset_index(drop=True)
|
| 47 |
+
self.tokenizer = tokenizer
|
| 48 |
+
self.label_list = label_list or load_codebert_labels()
|
| 49 |
+
self.max_length = max_length
|
| 50 |
+
self.num_labels = len(self.label_list)
|
| 51 |
+
|
| 52 |
+
def __len__(self) -> int:
|
| 53 |
+
return len(self.df)
|
| 54 |
+
|
| 55 |
+
def __getitem__(self, idx: int) -> Dict[str, Any]:
|
| 56 |
+
row = self.df.iloc[idx]
|
| 57 |
+
text = format_cross_encoder_input(
|
| 58 |
+
question=str(row["question"]),
|
| 59 |
+
schema=str(row["schema"]),
|
| 60 |
+
student_sql=str(row["student_sql"]),
|
| 61 |
+
correct_sql=str(row["correct_sql"]),
|
| 62 |
+
)
|
| 63 |
+
encoded = self.tokenizer(
|
| 64 |
+
text,
|
| 65 |
+
truncation=True,
|
| 66 |
+
max_length=self.max_length,
|
| 67 |
+
padding=False,
|
| 68 |
+
return_tensors=None,
|
| 69 |
+
)
|
| 70 |
+
labels = label_to_multihot(str(row["error_labels"]), self.label_list)
|
| 71 |
+
encoded["labels"] = labels.tolist()
|
| 72 |
+
return encoded
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class SQLCodeBERTDataCollator:
|
| 76 |
+
"""Pad batches dynamically for Trainer."""
|
| 77 |
+
|
| 78 |
+
def __init__(self, tokenizer: PreTrainedTokenizerBase):
|
| 79 |
+
self.tokenizer = tokenizer
|
| 80 |
+
|
| 81 |
+
def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, torch.Tensor]:
|
| 82 |
+
labels = [f.pop("labels") for f in features]
|
| 83 |
+
batch = self.tokenizer.pad(features, padding=True, return_tensors="pt")
|
| 84 |
+
batch["labels"] = torch.tensor(labels, dtype=torch.float)
|
| 85 |
+
return batch
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def prepare_datasets(
|
| 89 |
+
df: pd.DataFrame,
|
| 90 |
+
tokenizer: PreTrainedTokenizerBase,
|
| 91 |
+
test_size: float = 0.1,
|
| 92 |
+
val_size: float = 0.1,
|
| 93 |
+
max_length: int = 512,
|
| 94 |
+
seed: int = 42,
|
| 95 |
+
) -> tuple[SQLCodeBERTDataset, SQLCodeBERTDataset, SQLCodeBERTDataset]:
|
| 96 |
+
from sklearn.model_selection import train_test_split
|
| 97 |
+
|
| 98 |
+
df = normalize_dataframe(df)
|
| 99 |
+
trainval, test_df = train_test_split(
|
| 100 |
+
df,
|
| 101 |
+
test_size=test_size,
|
| 102 |
+
random_state=seed,
|
| 103 |
+
stratify=df["error_labels"],
|
| 104 |
+
)
|
| 105 |
+
relative_val = val_size / (1 - test_size)
|
| 106 |
+
train_df, val_df = train_test_split(
|
| 107 |
+
trainval,
|
| 108 |
+
test_size=relative_val,
|
| 109 |
+
random_state=seed,
|
| 110 |
+
stratify=trainval["error_labels"],
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
return (
|
| 114 |
+
SQLCodeBERTDataset(train_df, tokenizer, max_length=max_length),
|
| 115 |
+
SQLCodeBERTDataset(val_df, tokenizer, max_length=max_length),
|
| 116 |
+
SQLCodeBERTDataset(test_df, tokenizer, max_length=max_length),
|
| 117 |
+
)
|
src/codebert_formatting.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cross-encoder input formatting for CodeBERT."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
QUESTION_TAG = "QUESTION:"
|
| 6 |
+
SCHEMA_TAG = "SCHEMA:"
|
| 7 |
+
STUDENT_TAG = "STUDENT_SQL:"
|
| 8 |
+
CORRECT_TAG = "CORRECT_SQL:"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def format_cross_encoder_input(
|
| 12 |
+
question: str,
|
| 13 |
+
schema: str,
|
| 14 |
+
student_sql: str,
|
| 15 |
+
correct_sql: str,
|
| 16 |
+
) -> str:
|
| 17 |
+
"""
|
| 18 |
+
Concatenate all fields into a single CodeBERT input sequence.
|
| 19 |
+
|
| 20 |
+
The model attends jointly across question intent, schema, student SQL,
|
| 21 |
+
and the reference solution — cross-encoder style in one forward pass.
|
| 22 |
+
"""
|
| 23 |
+
return (
|
| 24 |
+
f"{QUESTION_TAG}\n{question.strip()}\n\n"
|
| 25 |
+
f"{SCHEMA_TAG}\n{schema.strip()}\n\n"
|
| 26 |
+
f"{STUDENT_TAG}\n{student_sql.strip()}\n\n"
|
| 27 |
+
f"{CORRECT_TAG}\n{correct_sql.strip()}"
|
| 28 |
+
)
|
src/codebert_labels.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Label utilities for CodeBERT multi-label classification."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Dict, List, Sequence, Union
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import yaml
|
| 10 |
+
|
| 11 |
+
CONFIG_PATH = (
|
| 12 |
+
Path(__file__).resolve().parent.parent / "config" / "codebert_labels.yaml"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def load_codebert_labels(config_path: Path = CONFIG_PATH) -> List[str]:
|
| 17 |
+
with open(config_path) as f:
|
| 18 |
+
data = yaml.safe_load(f)
|
| 19 |
+
return list(data["labels"])
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def load_alias_map(config_path: Path = CONFIG_PATH) -> Dict[str, List[str]]:
|
| 23 |
+
with open(config_path) as f:
|
| 24 |
+
data = yaml.safe_load(f)
|
| 25 |
+
return {k: list(v) for k, v in data["alias_map"].items()}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def label_to_multihot(
|
| 29 |
+
error_labels: Union[str, Sequence[str]],
|
| 30 |
+
label_list: List[str] | None = None,
|
| 31 |
+
alias_map: Dict[str, List[str]] | None = None,
|
| 32 |
+
) -> np.ndarray:
|
| 33 |
+
"""
|
| 34 |
+
Convert error label(s) to multi-hot vector.
|
| 35 |
+
|
| 36 |
+
Accepts:
|
| 37 |
+
- comma-separated string: "JOIN_ERROR,AGGREGATION_ERROR"
|
| 38 |
+
- list of label strings
|
| 39 |
+
- single dataset label_name (resolved via alias_map)
|
| 40 |
+
"""
|
| 41 |
+
labels = label_list or load_codebert_labels()
|
| 42 |
+
aliases = alias_map or load_alias_map()
|
| 43 |
+
index = {name: i for i, name in enumerate(labels)}
|
| 44 |
+
vec = np.zeros(len(labels), dtype=np.float32)
|
| 45 |
+
|
| 46 |
+
if isinstance(error_labels, str):
|
| 47 |
+
raw = [s.strip() for s in error_labels.split(",") if s.strip()]
|
| 48 |
+
if len(raw) == 1 and raw[0] in aliases:
|
| 49 |
+
raw = aliases[raw[0]]
|
| 50 |
+
elif len(raw) == 1 and raw[0] in index:
|
| 51 |
+
raw = [raw[0]]
|
| 52 |
+
elif len(raw) == 1 and raw[0] not in index:
|
| 53 |
+
mapped = aliases.get(raw[0], [])
|
| 54 |
+
raw = mapped
|
| 55 |
+
else:
|
| 56 |
+
raw = list(error_labels)
|
| 57 |
+
expanded: List[str] = []
|
| 58 |
+
for item in raw:
|
| 59 |
+
if item in aliases:
|
| 60 |
+
expanded.extend(aliases[item])
|
| 61 |
+
elif item in index:
|
| 62 |
+
expanded.append(item)
|
| 63 |
+
raw = expanded
|
| 64 |
+
|
| 65 |
+
for name in raw:
|
| 66 |
+
if name not in index:
|
| 67 |
+
raise ValueError(f"Unknown label '{name}'. Expected one of {labels}")
|
| 68 |
+
vec[index[name]] = 1.0
|
| 69 |
+
|
| 70 |
+
if vec.sum() == 0:
|
| 71 |
+
raise ValueError(f"No valid labels found in {error_labels}")
|
| 72 |
+
return vec
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def multihot_to_label_names(
|
| 76 |
+
vec: np.ndarray,
|
| 77 |
+
label_list: List[str] | None = None,
|
| 78 |
+
threshold: float = 0.5,
|
| 79 |
+
) -> List[str]:
|
| 80 |
+
labels = label_list or load_codebert_labels()
|
| 81 |
+
indices = np.where(vec >= threshold)[0]
|
| 82 |
+
return [labels[i] for i in indices]
|
src/device_utils.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pick the best available torch device."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def get_device() -> str:
|
| 9 |
+
if torch.cuda.is_available():
|
| 10 |
+
return "cuda"
|
| 11 |
+
if torch.backends.mps.is_available():
|
| 12 |
+
return "mps"
|
| 13 |
+
return "cpu"
|
src/exercises.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate playground exercises: schema + question + correct SQL."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import random
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from typing import Callable, Dict, List
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass(frozen=True)
|
| 11 |
+
class Exercise:
|
| 12 |
+
schema: str
|
| 13 |
+
question: str
|
| 14 |
+
correct_query: str
|
| 15 |
+
tables: tuple[str, ...]
|
| 16 |
+
columns: tuple[str, ...]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _fmt_schema(tables: Dict[str, List[str]]) -> str:
|
| 20 |
+
parts = [f"{name}({', '.join(cols)})" for name, cols in tables.items()]
|
| 21 |
+
return " | ".join(parts)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
ExerciseBuilder = Callable[[random.Random], Exercise]
|
| 25 |
+
|
| 26 |
+
EXERCISE_BUILDERS: List[ExerciseBuilder] = []
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _register(builder: ExerciseBuilder) -> ExerciseBuilder:
|
| 30 |
+
EXERCISE_BUILDERS.append(builder)
|
| 31 |
+
return builder
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@_register
|
| 35 |
+
def exercise_avg_by_department(rng: random.Random) -> Exercise:
|
| 36 |
+
tables = {
|
| 37 |
+
"students": ["id", "name", "email", "score", "department_id"],
|
| 38 |
+
"departments": ["id", "name", "city"],
|
| 39 |
+
}
|
| 40 |
+
return Exercise(
|
| 41 |
+
schema=_fmt_schema(tables),
|
| 42 |
+
question="What is the average score of students in each department?",
|
| 43 |
+
correct_query=(
|
| 44 |
+
"SELECT department_id, AVG(score) "
|
| 45 |
+
"FROM students GROUP BY department_id"
|
| 46 |
+
),
|
| 47 |
+
tables=tuple(tables),
|
| 48 |
+
columns=("department_id", "score"),
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
@_register
|
| 53 |
+
def exercise_student_department_names(rng: random.Random) -> Exercise:
|
| 54 |
+
tables = {
|
| 55 |
+
"students": ["id", "name", "department_id"],
|
| 56 |
+
"departments": ["id", "name"],
|
| 57 |
+
}
|
| 58 |
+
return Exercise(
|
| 59 |
+
schema=_fmt_schema(tables),
|
| 60 |
+
question="List each student's name along with their department name.",
|
| 61 |
+
correct_query=(
|
| 62 |
+
"SELECT students.name, departments.name "
|
| 63 |
+
"FROM students "
|
| 64 |
+
"INNER JOIN departments ON students.department_id = departments.id"
|
| 65 |
+
),
|
| 66 |
+
tables=tuple(tables),
|
| 67 |
+
columns=("name", "department_id"),
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@_register
|
| 72 |
+
def exercise_high_scoring_students(rng: random.Random) -> Exercise:
|
| 73 |
+
threshold = rng.randint(70, 90)
|
| 74 |
+
tables = {"students": ["id", "name", "age", "score", "status"]}
|
| 75 |
+
return Exercise(
|
| 76 |
+
schema=_fmt_schema(tables),
|
| 77 |
+
question=(
|
| 78 |
+
f"Find names of students older than 18 with a score above {threshold}."
|
| 79 |
+
),
|
| 80 |
+
correct_query=(
|
| 81 |
+
f"SELECT name FROM students "
|
| 82 |
+
f"WHERE age > 18 AND score > {threshold}"
|
| 83 |
+
),
|
| 84 |
+
tables=tuple(tables),
|
| 85 |
+
columns=("name", "age", "score", "status"),
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
@_register
|
| 90 |
+
def exercise_unique_cities(rng: random.Random) -> Exercise:
|
| 91 |
+
tables = {"students": ["id", "name", "city", "country"]}
|
| 92 |
+
return Exercise(
|
| 93 |
+
schema=_fmt_schema(tables),
|
| 94 |
+
question="List the unique cities where students live.",
|
| 95 |
+
correct_query="SELECT DISTINCT city FROM students",
|
| 96 |
+
tables=tuple(tables),
|
| 97 |
+
columns=("city",),
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@_register
|
| 102 |
+
def exercise_top_scorer(rng: random.Random) -> Exercise:
|
| 103 |
+
tables = {"students": ["id", "name", "score"], "grades": ["id", "score"]}
|
| 104 |
+
return Exercise(
|
| 105 |
+
schema=_fmt_schema(tables),
|
| 106 |
+
question="Find students whose score equals the highest score in the class.",
|
| 107 |
+
correct_query=(
|
| 108 |
+
"SELECT name FROM students "
|
| 109 |
+
"WHERE score = (SELECT MAX(score) FROM grades)"
|
| 110 |
+
),
|
| 111 |
+
tables=tuple(tables),
|
| 112 |
+
columns=("name", "score"),
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
@_register
|
| 117 |
+
def exercise_departments_over_budget(rng: random.Random) -> Exercise:
|
| 118 |
+
budget = rng.randint(3, 8)
|
| 119 |
+
tables = {"employees": ["id", "name", "department_id", "salary"]}
|
| 120 |
+
return Exercise(
|
| 121 |
+
schema=_fmt_schema(tables),
|
| 122 |
+
question=f"Which departments have more than {budget} employees?",
|
| 123 |
+
correct_query=(
|
| 124 |
+
f"SELECT department_id, COUNT(*) AS cnt "
|
| 125 |
+
f"FROM employees GROUP BY department_id "
|
| 126 |
+
f"HAVING COUNT(*) > {budget}"
|
| 127 |
+
),
|
| 128 |
+
tables=tuple(tables),
|
| 129 |
+
columns=("department_id", "salary"),
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
@_register
|
| 134 |
+
def exercise_recent_orders(rng: random.Random) -> Exercise:
|
| 135 |
+
year = rng.randint(2020, 2024)
|
| 136 |
+
tables = {"orders": ["id", "customer_id", "amount", "order_date", "status"]}
|
| 137 |
+
return Exercise(
|
| 138 |
+
schema=_fmt_schema(tables),
|
| 139 |
+
question=f"Show orders placed on or after January 1, {year}.",
|
| 140 |
+
correct_query=(
|
| 141 |
+
f"SELECT id, amount FROM orders "
|
| 142 |
+
f"WHERE order_date >= DATE '{year}-01-01'"
|
| 143 |
+
),
|
| 144 |
+
tables=tuple(tables),
|
| 145 |
+
columns=("order_date", "amount", "status"),
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@_register
|
| 150 |
+
def exercise_missing_email(rng: random.Random) -> Exercise:
|
| 151 |
+
tables = {"students": ["id", "name", "email", "phone"]}
|
| 152 |
+
return Exercise(
|
| 153 |
+
schema=_fmt_schema(tables),
|
| 154 |
+
question="Find students who have not provided an email address.",
|
| 155 |
+
correct_query="SELECT name FROM students WHERE email IS NULL",
|
| 156 |
+
tables=tuple(tables),
|
| 157 |
+
columns=("email", "name"),
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
@_register
|
| 162 |
+
def exercise_rank_by_score(rng: random.Random) -> Exercise:
|
| 163 |
+
tables = {"students": ["id", "name", "score", "department_id"]}
|
| 164 |
+
return Exercise(
|
| 165 |
+
schema=_fmt_schema(tables),
|
| 166 |
+
question="Rank students by score within each department.",
|
| 167 |
+
correct_query=(
|
| 168 |
+
"SELECT name, score, "
|
| 169 |
+
"RANK() OVER (PARTITION BY department_id ORDER BY score DESC) AS rnk "
|
| 170 |
+
"FROM students"
|
| 171 |
+
),
|
| 172 |
+
tables=tuple(tables),
|
| 173 |
+
columns=("name", "score", "department_id"),
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@_register
|
| 178 |
+
def exercise_course_enrollment_count(rng: random.Random) -> Exercise:
|
| 179 |
+
tables = {
|
| 180 |
+
"courses": ["id", "title"],
|
| 181 |
+
"enrollments": ["id", "course_id", "student_id"],
|
| 182 |
+
}
|
| 183 |
+
return Exercise(
|
| 184 |
+
schema=_fmt_schema(tables),
|
| 185 |
+
question="How many students are enrolled in each course?",
|
| 186 |
+
correct_query=(
|
| 187 |
+
"SELECT courses.title, COUNT(enrollments.student_id) AS enrolled "
|
| 188 |
+
"FROM courses "
|
| 189 |
+
"INNER JOIN enrollments ON courses.id = enrollments.course_id "
|
| 190 |
+
"GROUP BY courses.title"
|
| 191 |
+
),
|
| 192 |
+
tables=tuple(tables),
|
| 193 |
+
columns=("title", "student_id", "course_id"),
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
@_register
|
| 198 |
+
def exercise_active_employees(rng: random.Random) -> Exercise:
|
| 199 |
+
tables = {"employees": ["id", "name", "salary", "status", "hire_date"]}
|
| 200 |
+
return Exercise(
|
| 201 |
+
schema=_fmt_schema(tables),
|
| 202 |
+
question="What is the total salary paid to active employees?",
|
| 203 |
+
correct_query=(
|
| 204 |
+
"SELECT SUM(salary) FROM employees WHERE status = 'active'"
|
| 205 |
+
),
|
| 206 |
+
tables=tuple(tables),
|
| 207 |
+
columns=("salary", "status"),
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
@_register
|
| 212 |
+
def exercise_product_price_filter(rng: random.Random) -> Exercise:
|
| 213 |
+
lo, hi = rng.randint(10, 50), rng.randint(100, 500)
|
| 214 |
+
tables = {"products": ["id", "name", "price", "category"]}
|
| 215 |
+
return Exercise(
|
| 216 |
+
schema=_fmt_schema(tables),
|
| 217 |
+
question=f"List products priced between {lo} and {hi}.",
|
| 218 |
+
correct_query=(
|
| 219 |
+
f"SELECT name, price FROM products "
|
| 220 |
+
f"WHERE price BETWEEN {lo} AND {hi}"
|
| 221 |
+
),
|
| 222 |
+
tables=tuple(tables),
|
| 223 |
+
columns=("name", "price", "category"),
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def generate_exercise(rng: random.Random) -> Exercise:
|
| 228 |
+
return rng.choice(EXERCISE_BUILDERS)(rng)
|
src/generate_dataset.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate labeled SQL error dataset at scale."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import random
|
| 7 |
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Dict, List, Tuple
|
| 10 |
+
|
| 11 |
+
import pandas as pd
|
| 12 |
+
from tqdm import tqdm
|
| 13 |
+
|
| 14 |
+
from src.categories import load_categories
|
| 15 |
+
from src.exercises import generate_exercise
|
| 16 |
+
from src.sql_templates import ERROR_INJECTORS
|
| 17 |
+
|
| 18 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 19 |
+
DEFAULT_OUTPUT = PROJECT_ROOT / "data" / "sql_errors_1m.parquet"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def generate_dataset(
|
| 23 |
+
total_samples: int = 1_000_000,
|
| 24 |
+
output_path: Path = DEFAULT_OUTPUT,
|
| 25 |
+
batch_size: int = 10_000,
|
| 26 |
+
workers: int = 8,
|
| 27 |
+
seed: int = 42,
|
| 28 |
+
) -> Path:
|
| 29 |
+
categories = load_categories()
|
| 30 |
+
label_ids = [c.id for c in categories]
|
| 31 |
+
samples_per_class = total_samples // len(label_ids)
|
| 32 |
+
remainder = total_samples % len(label_ids)
|
| 33 |
+
|
| 34 |
+
# Balanced label schedule: each class gets equal share (+1 for first `remainder` classes)
|
| 35 |
+
schedule: List[int] = []
|
| 36 |
+
for cat in categories:
|
| 37 |
+
count = samples_per_class + (1 if cat.id < remainder else 0)
|
| 38 |
+
schedule.extend([cat.id] * count)
|
| 39 |
+
random.Random(seed).shuffle(schedule)
|
| 40 |
+
|
| 41 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 42 |
+
chunks: List[pd.DataFrame] = []
|
| 43 |
+
num_batches = (total_samples + batch_size - 1) // batch_size
|
| 44 |
+
|
| 45 |
+
with ProcessPoolExecutor(max_workers=workers) as executor:
|
| 46 |
+
futures = []
|
| 47 |
+
offset = 0
|
| 48 |
+
for batch_idx in range(num_batches):
|
| 49 |
+
current_batch = min(batch_size, total_samples - offset)
|
| 50 |
+
batch_labels = schedule[offset : offset + current_batch]
|
| 51 |
+
futures.append(
|
| 52 |
+
executor.submit(
|
| 53 |
+
_generate_batch_with_labels,
|
| 54 |
+
batch_labels,
|
| 55 |
+
seed + batch_idx,
|
| 56 |
+
)
|
| 57 |
+
)
|
| 58 |
+
offset += current_batch
|
| 59 |
+
|
| 60 |
+
for future in tqdm(as_completed(futures), total=len(futures), desc="Generating"):
|
| 61 |
+
rows = future.result()
|
| 62 |
+
chunks.append(pd.DataFrame(rows))
|
| 63 |
+
|
| 64 |
+
df = pd.concat(chunks, ignore_index=True)
|
| 65 |
+
df = df.sample(frac=1, random_state=seed).reset_index(drop=True)
|
| 66 |
+
df.to_parquet(output_path, index=False)
|
| 67 |
+
|
| 68 |
+
print(f"Saved {len(df):,} samples to {output_path}")
|
| 69 |
+
print("\nClass distribution:")
|
| 70 |
+
print(df["label_name"].value_counts().sort_index().to_string())
|
| 71 |
+
return output_path
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _generate_batch_with_labels(label_ids: List[int], seed: int) -> List[Dict]:
|
| 75 |
+
rng = random.Random(seed)
|
| 76 |
+
categories = load_categories()
|
| 77 |
+
rows = []
|
| 78 |
+
for label_id in label_ids:
|
| 79 |
+
exercise = generate_exercise(rng)
|
| 80 |
+
injector = ERROR_INJECTORS[label_id]
|
| 81 |
+
query, error_message = injector(rng, exercise)
|
| 82 |
+
rows.append(
|
| 83 |
+
{
|
| 84 |
+
"schema": exercise.schema,
|
| 85 |
+
"question": exercise.question,
|
| 86 |
+
"correct_query": exercise.correct_query,
|
| 87 |
+
"query": query.strip(),
|
| 88 |
+
"error_message": error_message,
|
| 89 |
+
"label_id": label_id,
|
| 90 |
+
"label_name": categories[label_id].name,
|
| 91 |
+
}
|
| 92 |
+
)
|
| 93 |
+
return rows
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def main() -> None:
|
| 97 |
+
parser = argparse.ArgumentParser(description="Generate labeled SQL error dataset")
|
| 98 |
+
parser.add_argument("--samples", type=int, default=1_000_000, help="Total samples")
|
| 99 |
+
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
| 100 |
+
parser.add_argument("--batch-size", type=int, default=10_000)
|
| 101 |
+
parser.add_argument("--workers", type=int, default=8)
|
| 102 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 103 |
+
args = parser.parse_args()
|
| 104 |
+
|
| 105 |
+
generate_dataset(
|
| 106 |
+
total_samples=args.samples,
|
| 107 |
+
output_path=args.output,
|
| 108 |
+
batch_size=args.batch_size,
|
| 109 |
+
workers=args.workers,
|
| 110 |
+
seed=args.seed,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
if __name__ == "__main__":
|
| 115 |
+
main()
|
src/hf_eval_codebert.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluate a trained CodeBERT cross-encoder."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import pandas as pd
|
| 10 |
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer, Trainer
|
| 11 |
+
|
| 12 |
+
from src.codebert_dataset import SQLCodeBERTDataCollator, SQLCodeBERTDataset, normalize_dataframe
|
| 13 |
+
from src.hf_metrics import build_compute_metrics, compute_multilabel_metrics
|
| 14 |
+
|
| 15 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 16 |
+
DEFAULT_DATA = PROJECT_ROOT / "data" / "sql_errors_dev.parquet"
|
| 17 |
+
DEFAULT_MODEL = PROJECT_ROOT / "models" / "codebert-cross-encoder"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def evaluate(
|
| 21 |
+
model_dir: Path = DEFAULT_MODEL,
|
| 22 |
+
data_path: Path = DEFAULT_DATA,
|
| 23 |
+
sample_size: int = 10_000,
|
| 24 |
+
threshold: float = 0.5,
|
| 25 |
+
seed: int = 42,
|
| 26 |
+
) -> dict:
|
| 27 |
+
df = normalize_dataframe(pd.read_parquet(data_path))
|
| 28 |
+
if len(df) > sample_size:
|
| 29 |
+
df = df.sample(n=sample_size, random_state=seed)
|
| 30 |
+
|
| 31 |
+
tokenizer = AutoTokenizer.from_pretrained(model_dir)
|
| 32 |
+
model = AutoModelForSequenceClassification.from_pretrained(model_dir)
|
| 33 |
+
dataset = SQLCodeBERTDataset(df, tokenizer)
|
| 34 |
+
|
| 35 |
+
trainer_kwargs = dict(
|
| 36 |
+
model=model,
|
| 37 |
+
data_collator=SQLCodeBERTDataCollator(tokenizer),
|
| 38 |
+
compute_metrics=build_compute_metrics(threshold=threshold),
|
| 39 |
+
)
|
| 40 |
+
try:
|
| 41 |
+
trainer = Trainer(processing_class=tokenizer, **trainer_kwargs)
|
| 42 |
+
except TypeError:
|
| 43 |
+
trainer = Trainer(tokenizer=tokenizer, **trainer_kwargs)
|
| 44 |
+
|
| 45 |
+
output = trainer.predict(dataset)
|
| 46 |
+
metrics = compute_multilabel_metrics(
|
| 47 |
+
output.predictions, output.label_ids, threshold=threshold
|
| 48 |
+
)
|
| 49 |
+
print(json.dumps(metrics, indent=2))
|
| 50 |
+
return metrics
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def main() -> None:
|
| 54 |
+
parser = argparse.ArgumentParser(description="Evaluate CodeBERT SQL classifier")
|
| 55 |
+
parser.add_argument("--model-dir", type=Path, default=DEFAULT_MODEL)
|
| 56 |
+
parser.add_argument("--data", type=Path, default=DEFAULT_DATA)
|
| 57 |
+
parser.add_argument("--sample-size", type=int, default=10_000)
|
| 58 |
+
parser.add_argument("--threshold", type=float, default=0.5)
|
| 59 |
+
args = parser.parse_args()
|
| 60 |
+
evaluate(
|
| 61 |
+
model_dir=args.model_dir,
|
| 62 |
+
data_path=args.data,
|
| 63 |
+
sample_size=args.sample_size,
|
| 64 |
+
threshold=args.threshold,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
if __name__ == "__main__":
|
| 69 |
+
main()
|
src/hf_metrics.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluation metrics for multi-label SQL error classification."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Dict
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
from sklearn.metrics import (
|
| 9 |
+
accuracy_score,
|
| 10 |
+
f1_score,
|
| 11 |
+
hamming_loss,
|
| 12 |
+
precision_score,
|
| 13 |
+
recall_score,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def sigmoid(x: np.ndarray) -> np.ndarray:
|
| 18 |
+
return 1.0 / (1.0 + np.exp(-x))
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def compute_multilabel_metrics(
|
| 22 |
+
logits: np.ndarray,
|
| 23 |
+
labels: np.ndarray,
|
| 24 |
+
threshold: float = 0.5,
|
| 25 |
+
) -> Dict[str, float]:
|
| 26 |
+
probs = sigmoid(logits)
|
| 27 |
+
preds = (probs >= threshold).astype(int)
|
| 28 |
+
labels = labels.astype(int)
|
| 29 |
+
|
| 30 |
+
return {
|
| 31 |
+
"accuracy": float(accuracy_score(labels, preds)),
|
| 32 |
+
"f1_macro": float(f1_score(labels, preds, average="macro", zero_division=0)),
|
| 33 |
+
"f1_micro": float(f1_score(labels, preds, average="micro", zero_division=0)),
|
| 34 |
+
"precision_macro": float(
|
| 35 |
+
precision_score(labels, preds, average="macro", zero_division=0)
|
| 36 |
+
),
|
| 37 |
+
"recall_macro": float(
|
| 38 |
+
recall_score(labels, preds, average="macro", zero_division=0)
|
| 39 |
+
),
|
| 40 |
+
"hamming_loss": float(hamming_loss(labels, preds)),
|
| 41 |
+
"subset_accuracy": float((preds == labels).all(axis=1).mean()),
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def build_compute_metrics(threshold: float = 0.5):
|
| 46 |
+
"""Factory for Hugging Face Trainer compute_metrics callback."""
|
| 47 |
+
|
| 48 |
+
def compute_metrics(eval_pred) -> Dict[str, float]:
|
| 49 |
+
logits, labels = eval_pred
|
| 50 |
+
return compute_multilabel_metrics(logits, labels, threshold=threshold)
|
| 51 |
+
|
| 52 |
+
return compute_metrics
|
src/hf_predict_codebert.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Inference for CodeBERT SQL error cross-encoder."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import List, Optional, Union
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
| 13 |
+
|
| 14 |
+
from src.codebert_formatting import format_cross_encoder_input
|
| 15 |
+
from src.device_utils import get_device
|
| 16 |
+
from src.codebert_labels import load_codebert_labels, multihot_to_label_names
|
| 17 |
+
from src.hf_metrics import sigmoid
|
| 18 |
+
|
| 19 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 20 |
+
DEFAULT_MODEL_DIR = PROJECT_ROOT / "models" / "codebert-cross-encoder"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _is_hub_id(model_dir: Union[str, Path]) -> bool:
|
| 24 |
+
text = str(model_dir)
|
| 25 |
+
local = Path(text)
|
| 26 |
+
return "/" in text and not local.exists()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class CodeBERTSQLErrorClassifier:
|
| 30 |
+
"""CodeBERT cross-encoder inference wrapper."""
|
| 31 |
+
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
model_dir: Union[str, Path] = DEFAULT_MODEL_DIR,
|
| 35 |
+
threshold: float = 0.5,
|
| 36 |
+
device: Optional[str] = None,
|
| 37 |
+
):
|
| 38 |
+
self.hub_id = str(model_dir) if _is_hub_id(model_dir) else None
|
| 39 |
+
self.model_dir = Path(model_dir) if not self.hub_id else None
|
| 40 |
+
self.threshold = threshold
|
| 41 |
+
# MPS inference can be flaky for some ops; CPU is reliable on Mac
|
| 42 |
+
self.device = device or (
|
| 43 |
+
"cuda" if torch.cuda.is_available() else "cpu"
|
| 44 |
+
)
|
| 45 |
+
model_ref = self.hub_id or str(self.model_dir)
|
| 46 |
+
|
| 47 |
+
if self.hub_id:
|
| 48 |
+
self.label_list = load_codebert_labels()
|
| 49 |
+
self.max_length = 512
|
| 50 |
+
else:
|
| 51 |
+
config_path = self.model_dir / "label_config.json"
|
| 52 |
+
if config_path.exists():
|
| 53 |
+
with open(config_path) as f:
|
| 54 |
+
cfg = json.load(f)
|
| 55 |
+
self.label_list = cfg.get("labels", load_codebert_labels())
|
| 56 |
+
self.threshold = cfg.get("threshold", threshold)
|
| 57 |
+
self.max_length = cfg.get("max_length", 512)
|
| 58 |
+
else:
|
| 59 |
+
self.label_list = load_codebert_labels()
|
| 60 |
+
self.max_length = 512
|
| 61 |
+
|
| 62 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_ref)
|
| 63 |
+
self.model = AutoModelForSequenceClassification.from_pretrained(
|
| 64 |
+
model_ref
|
| 65 |
+
).to(self.device)
|
| 66 |
+
self.model.eval()
|
| 67 |
+
|
| 68 |
+
def predict(
|
| 69 |
+
self,
|
| 70 |
+
question: str,
|
| 71 |
+
schema: str,
|
| 72 |
+
student_sql: str,
|
| 73 |
+
correct_sql: str,
|
| 74 |
+
threshold: Optional[float] = None,
|
| 75 |
+
top_k: int = 5,
|
| 76 |
+
) -> dict:
|
| 77 |
+
text = format_cross_encoder_input(
|
| 78 |
+
question=question,
|
| 79 |
+
schema=schema,
|
| 80 |
+
student_sql=student_sql,
|
| 81 |
+
correct_sql=correct_sql,
|
| 82 |
+
)
|
| 83 |
+
encoded = self.tokenizer(
|
| 84 |
+
text,
|
| 85 |
+
truncation=True,
|
| 86 |
+
max_length=self.max_length,
|
| 87 |
+
padding=True,
|
| 88 |
+
return_tensors="pt",
|
| 89 |
+
).to(self.device)
|
| 90 |
+
|
| 91 |
+
with torch.no_grad():
|
| 92 |
+
logits = self.model(**encoded).logits.cpu().numpy()[0]
|
| 93 |
+
|
| 94 |
+
probs = sigmoid(logits)
|
| 95 |
+
thr = threshold if threshold is not None else self.threshold
|
| 96 |
+
predicted = multihot_to_label_names(probs, self.label_list, threshold=thr)
|
| 97 |
+
|
| 98 |
+
ranked = sorted(
|
| 99 |
+
zip(self.label_list, probs.tolist()),
|
| 100 |
+
key=lambda x: x[1],
|
| 101 |
+
reverse=True,
|
| 102 |
+
)[:top_k]
|
| 103 |
+
|
| 104 |
+
return {
|
| 105 |
+
"error_labels": predicted,
|
| 106 |
+
"probabilities": {name: float(p) for name, p in ranked},
|
| 107 |
+
"top_k": [
|
| 108 |
+
{"label": name, "probability": float(p)} for name, p in ranked
|
| 109 |
+
],
|
| 110 |
+
"primary_label": ranked[0][0],
|
| 111 |
+
"primary_confidence": float(ranked[0][1]),
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
def predict_batch(
|
| 115 |
+
self,
|
| 116 |
+
examples: List[dict],
|
| 117 |
+
batch_size: int = 16,
|
| 118 |
+
) -> List[dict]:
|
| 119 |
+
results = []
|
| 120 |
+
for i in range(0, len(examples), batch_size):
|
| 121 |
+
chunk = examples[i : i + batch_size]
|
| 122 |
+
texts = [
|
| 123 |
+
format_cross_encoder_input(
|
| 124 |
+
question=x["question"],
|
| 125 |
+
schema=x["schema"],
|
| 126 |
+
student_sql=x["student_sql"],
|
| 127 |
+
correct_sql=x["correct_sql"],
|
| 128 |
+
)
|
| 129 |
+
for x in chunk
|
| 130 |
+
]
|
| 131 |
+
encoded = self.tokenizer(
|
| 132 |
+
texts,
|
| 133 |
+
truncation=True,
|
| 134 |
+
max_length=self.max_length,
|
| 135 |
+
padding=True,
|
| 136 |
+
return_tensors="pt",
|
| 137 |
+
).to(self.device)
|
| 138 |
+
|
| 139 |
+
with torch.no_grad():
|
| 140 |
+
logits = self.model(**encoded).logits.cpu().numpy()
|
| 141 |
+
|
| 142 |
+
for j, row in enumerate(logits):
|
| 143 |
+
probs = sigmoid(row)
|
| 144 |
+
results.append(
|
| 145 |
+
{
|
| 146 |
+
"error_labels": multihot_to_label_names(
|
| 147 |
+
probs, self.label_list, self.threshold
|
| 148 |
+
),
|
| 149 |
+
"primary_label": self.label_list[int(np.argmax(probs))],
|
| 150 |
+
"primary_confidence": float(np.max(probs)),
|
| 151 |
+
}
|
| 152 |
+
)
|
| 153 |
+
return results
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def main() -> None:
|
| 157 |
+
parser = argparse.ArgumentParser(description="CodeBERT SQL error inference")
|
| 158 |
+
parser.add_argument("--model-dir", type=Path, default=DEFAULT_MODEL_DIR)
|
| 159 |
+
parser.add_argument("--question", type=str, required=True)
|
| 160 |
+
parser.add_argument("--schema", type=str, required=True)
|
| 161 |
+
parser.add_argument("--student-sql", type=str, required=True)
|
| 162 |
+
parser.add_argument("--correct-sql", type=str, required=True)
|
| 163 |
+
parser.add_argument("--threshold", type=float, default=0.5)
|
| 164 |
+
args = parser.parse_args()
|
| 165 |
+
|
| 166 |
+
clf = CodeBERTSQLErrorClassifier(args.model_dir, threshold=args.threshold)
|
| 167 |
+
result = clf.predict(
|
| 168 |
+
question=args.question,
|
| 169 |
+
schema=args.schema,
|
| 170 |
+
student_sql=args.student_sql,
|
| 171 |
+
correct_sql=args.correct_sql,
|
| 172 |
+
)
|
| 173 |
+
print(json.dumps(result, indent=2))
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
if __name__ == "__main__":
|
| 177 |
+
main()
|
src/hf_train_codebert.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train CodeBERT cross-encoder for SQL error classification with HF Trainer."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import torch
|
| 12 |
+
from transformers import (
|
| 13 |
+
AutoModelForSequenceClassification,
|
| 14 |
+
AutoTokenizer,
|
| 15 |
+
EarlyStoppingCallback,
|
| 16 |
+
Trainer,
|
| 17 |
+
TrainingArguments,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
from src.device_utils import get_device
|
| 21 |
+
from src.codebert_dataset import (
|
| 22 |
+
SQLCodeBERTDataCollator,
|
| 23 |
+
prepare_datasets,
|
| 24 |
+
)
|
| 25 |
+
from src.codebert_labels import load_codebert_labels
|
| 26 |
+
from src.hf_metrics import build_compute_metrics, compute_multilabel_metrics
|
| 27 |
+
|
| 28 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 29 |
+
DEFAULT_DATA = PROJECT_ROOT / "data" / "sql_errors_1m.parquet"
|
| 30 |
+
DEFAULT_OUTPUT = PROJECT_ROOT / "models" / "codebert-cross-encoder"
|
| 31 |
+
DEFAULT_MODEL = "microsoft/codebert-base"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def train(
|
| 35 |
+
data_path: Path | None = DEFAULT_DATA,
|
| 36 |
+
dataframe: pd.DataFrame | None = None,
|
| 37 |
+
output_dir: Path = DEFAULT_OUTPUT,
|
| 38 |
+
model_name: str = DEFAULT_MODEL,
|
| 39 |
+
epochs: float = 3.0,
|
| 40 |
+
batch_size: int = 16,
|
| 41 |
+
eval_batch_size: int = 32,
|
| 42 |
+
learning_rate: float = 2e-5,
|
| 43 |
+
weight_decay: float = 0.01,
|
| 44 |
+
warmup_ratio: float = 0.06,
|
| 45 |
+
max_length: int = 512,
|
| 46 |
+
max_samples: int | None = None,
|
| 47 |
+
test_size: float = 0.1,
|
| 48 |
+
val_size: float = 0.1,
|
| 49 |
+
threshold: float = 0.5,
|
| 50 |
+
seed: int = 42,
|
| 51 |
+
push_to_hub: bool = False,
|
| 52 |
+
hub_model_id: str | None = None,
|
| 53 |
+
fp16: bool = False,
|
| 54 |
+
save_strategy: str = "no",
|
| 55 |
+
hub_token: str | None = None,
|
| 56 |
+
) -> dict:
|
| 57 |
+
if dataframe is not None:
|
| 58 |
+
df = dataframe.copy()
|
| 59 |
+
print(f"Loaded dataframe with {len(df):,} rows")
|
| 60 |
+
elif data_path is not None:
|
| 61 |
+
print(f"Loading dataset from {data_path}...")
|
| 62 |
+
df = pd.read_parquet(data_path)
|
| 63 |
+
else:
|
| 64 |
+
raise ValueError("Either data_path or dataframe must be provided")
|
| 65 |
+
if max_samples and len(df) > max_samples:
|
| 66 |
+
df = df.sample(n=max_samples, random_state=seed)
|
| 67 |
+
|
| 68 |
+
label_list = load_codebert_labels()
|
| 69 |
+
num_labels = len(label_list)
|
| 70 |
+
print(f"Labels ({num_labels}): {label_list}")
|
| 71 |
+
print(f"Samples: {len(df):,}")
|
| 72 |
+
device = get_device()
|
| 73 |
+
use_fp16 = fp16 and device == "cuda"
|
| 74 |
+
print(f"Device: {device} | fp16: {use_fp16}")
|
| 75 |
+
|
| 76 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 77 |
+
model = AutoModelForSequenceClassification.from_pretrained(
|
| 78 |
+
model_name,
|
| 79 |
+
num_labels=num_labels,
|
| 80 |
+
problem_type="multi_label_classification",
|
| 81 |
+
id2label={i: name for i, name in enumerate(label_list)},
|
| 82 |
+
label2id={name: i for i, name in enumerate(label_list)},
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
train_ds, val_ds, test_ds = prepare_datasets(
|
| 86 |
+
df,
|
| 87 |
+
tokenizer,
|
| 88 |
+
test_size=test_size,
|
| 89 |
+
val_size=val_size,
|
| 90 |
+
max_length=max_length,
|
| 91 |
+
seed=seed,
|
| 92 |
+
)
|
| 93 |
+
print(f"Train: {len(train_ds):,} | Val: {len(val_ds):,} | Test: {len(test_ds):,}")
|
| 94 |
+
|
| 95 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 96 |
+
label_info = {
|
| 97 |
+
"labels": label_list,
|
| 98 |
+
"model_name": model_name,
|
| 99 |
+
"architecture": "codebert-cross-encoder",
|
| 100 |
+
"input_format": "QUESTION + SCHEMA + STUDENT_SQL + CORRECT_SQL",
|
| 101 |
+
"max_length": max_length,
|
| 102 |
+
"threshold": threshold,
|
| 103 |
+
}
|
| 104 |
+
with open(output_dir / "label_config.json", "w") as f:
|
| 105 |
+
json.dump(label_info, f, indent=2)
|
| 106 |
+
|
| 107 |
+
training_args = TrainingArguments(
|
| 108 |
+
output_dir=str(output_dir),
|
| 109 |
+
num_train_epochs=epochs,
|
| 110 |
+
per_device_train_batch_size=batch_size,
|
| 111 |
+
per_device_eval_batch_size=eval_batch_size,
|
| 112 |
+
learning_rate=learning_rate,
|
| 113 |
+
weight_decay=weight_decay,
|
| 114 |
+
warmup_ratio=warmup_ratio,
|
| 115 |
+
eval_strategy="epoch",
|
| 116 |
+
save_strategy=save_strategy,
|
| 117 |
+
logging_strategy="steps",
|
| 118 |
+
logging_steps=50,
|
| 119 |
+
load_best_model_at_end=save_strategy == "epoch",
|
| 120 |
+
metric_for_best_model="f1_macro",
|
| 121 |
+
greater_is_better=True,
|
| 122 |
+
save_total_limit=1,
|
| 123 |
+
seed=seed,
|
| 124 |
+
report_to="none",
|
| 125 |
+
fp16=use_fp16,
|
| 126 |
+
use_mps_device=(device == "mps"),
|
| 127 |
+
push_to_hub=push_to_hub,
|
| 128 |
+
hub_model_id=hub_model_id,
|
| 129 |
+
hub_token=hub_token,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
callbacks = []
|
| 133 |
+
if save_strategy == "epoch":
|
| 134 |
+
callbacks.append(EarlyStoppingCallback(early_stopping_patience=2))
|
| 135 |
+
|
| 136 |
+
trainer_kwargs = dict(
|
| 137 |
+
model=model,
|
| 138 |
+
args=training_args,
|
| 139 |
+
train_dataset=train_ds,
|
| 140 |
+
eval_dataset=val_ds,
|
| 141 |
+
data_collator=SQLCodeBERTDataCollator(tokenizer),
|
| 142 |
+
compute_metrics=build_compute_metrics(threshold=threshold),
|
| 143 |
+
callbacks=callbacks,
|
| 144 |
+
)
|
| 145 |
+
try:
|
| 146 |
+
trainer = Trainer(processing_class=tokenizer, **trainer_kwargs)
|
| 147 |
+
except TypeError:
|
| 148 |
+
trainer = Trainer(tokenizer=tokenizer, **trainer_kwargs)
|
| 149 |
+
|
| 150 |
+
print("Starting CodeBERT cross-encoder training...")
|
| 151 |
+
train_result = trainer.train()
|
| 152 |
+
|
| 153 |
+
print("Evaluating on validation set...")
|
| 154 |
+
val_metrics = trainer.evaluate()
|
| 155 |
+
|
| 156 |
+
print("Evaluating on held-out test set...")
|
| 157 |
+
test_output = trainer.predict(test_ds)
|
| 158 |
+
test_metrics = compute_multilabel_metrics(
|
| 159 |
+
test_output.predictions,
|
| 160 |
+
test_output.label_ids,
|
| 161 |
+
threshold=threshold,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
trainer.save_model(str(output_dir))
|
| 165 |
+
tokenizer.save_pretrained(str(output_dir))
|
| 166 |
+
|
| 167 |
+
metrics = {
|
| 168 |
+
"train_samples": len(train_ds),
|
| 169 |
+
"val_samples": len(val_ds),
|
| 170 |
+
"test_samples": len(test_ds),
|
| 171 |
+
"train_runtime": train_result.metrics.get("train_runtime"),
|
| 172 |
+
"validation": val_metrics,
|
| 173 |
+
"test": test_metrics,
|
| 174 |
+
}
|
| 175 |
+
with open(output_dir / "metrics.json", "w") as f:
|
| 176 |
+
json.dump(metrics, f, indent=2, default=float)
|
| 177 |
+
|
| 178 |
+
print(f"\nValidation F1 (macro): {val_metrics.get('eval_f1_macro', 0):.4f}")
|
| 179 |
+
print(f"Test F1 (macro): {test_metrics['f1_macro']:.4f}")
|
| 180 |
+
print(f"Test subset accuracy: {test_metrics['subset_accuracy']:.4f}")
|
| 181 |
+
print(f"Model saved to {output_dir}")
|
| 182 |
+
return metrics
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def main() -> None:
|
| 186 |
+
parser = argparse.ArgumentParser(
|
| 187 |
+
description="Train CodeBERT cross-encoder with Hugging Face Trainer"
|
| 188 |
+
)
|
| 189 |
+
parser.add_argument("--data", type=Path, default=DEFAULT_DATA)
|
| 190 |
+
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT)
|
| 191 |
+
parser.add_argument("--model-name", type=str, default=DEFAULT_MODEL)
|
| 192 |
+
parser.add_argument("--epochs", type=float, default=3.0)
|
| 193 |
+
parser.add_argument("--batch-size", type=int, default=16)
|
| 194 |
+
parser.add_argument("--eval-batch-size", type=int, default=32)
|
| 195 |
+
parser.add_argument("--learning-rate", type=float, default=2e-5)
|
| 196 |
+
parser.add_argument("--max-length", type=int, default=512)
|
| 197 |
+
parser.add_argument("--max-samples", type=int, default=None)
|
| 198 |
+
parser.add_argument("--threshold", type=float, default=0.5)
|
| 199 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 200 |
+
parser.add_argument("--push-to-hub", action="store_true")
|
| 201 |
+
parser.add_argument("--hub-model-id", type=str, default=None)
|
| 202 |
+
parser.add_argument("--fp16", action="store_true")
|
| 203 |
+
parser.add_argument(
|
| 204 |
+
"--save-strategy",
|
| 205 |
+
choices=["no", "epoch"],
|
| 206 |
+
default="no",
|
| 207 |
+
help="Use 'no' to save only final model (saves disk space)",
|
| 208 |
+
)
|
| 209 |
+
args = parser.parse_args()
|
| 210 |
+
|
| 211 |
+
train(
|
| 212 |
+
data_path=args.data,
|
| 213 |
+
output_dir=args.output_dir,
|
| 214 |
+
model_name=args.model_name,
|
| 215 |
+
epochs=args.epochs,
|
| 216 |
+
batch_size=args.batch_size,
|
| 217 |
+
eval_batch_size=args.eval_batch_size,
|
| 218 |
+
learning_rate=args.learning_rate,
|
| 219 |
+
max_length=args.max_length,
|
| 220 |
+
max_samples=args.max_samples,
|
| 221 |
+
threshold=args.threshold,
|
| 222 |
+
seed=args.seed,
|
| 223 |
+
push_to_hub=args.push_to_hub,
|
| 224 |
+
hub_model_id=args.hub_model_id,
|
| 225 |
+
fp16=args.fp16,
|
| 226 |
+
save_strategy=args.save_strategy,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
if __name__ == "__main__":
|
| 231 |
+
main()
|
src/sql_templates.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Error injectors that transform exercise context into labeled mistakes."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import random
|
| 6 |
+
from typing import Callable, Dict, List, Tuple
|
| 7 |
+
|
| 8 |
+
from src.exercises import Exercise
|
| 9 |
+
|
| 10 |
+
FAKE_COLUMNS = ["fullname", "studentname", "coursename", "dept_name", "totals"]
|
| 11 |
+
FAKE_TABLES = ["student", "course", "enrolment", "employe", "orderz"]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _pick(rng: random.Random, items: List[str], k: int = 1) -> str | List[str]:
|
| 15 |
+
if k == 1:
|
| 16 |
+
return rng.choice(items)
|
| 17 |
+
return rng.sample(items, k)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _first_table(exercise: Exercise) -> str:
|
| 21 |
+
return exercise.tables[0]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _second_table(exercise: Exercise) -> str:
|
| 25 |
+
return exercise.tables[1] if len(exercise.tables) > 1 else exercise.tables[0]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# --- Error injectors: (exercise) -> (erroneous_sql, error_message) ---
|
| 29 |
+
|
| 30 |
+
def inject_syntax_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 31 |
+
sql = exercise.correct_query
|
| 32 |
+
mutations = [
|
| 33 |
+
lambda s: s.replace("SELECT", "SELEC", 1),
|
| 34 |
+
lambda s: s.replace("FROM", "FRO", 1),
|
| 35 |
+
lambda s: s[:-1],
|
| 36 |
+
lambda s: s.replace(")", "", 1),
|
| 37 |
+
lambda s: s + " WHERE",
|
| 38 |
+
lambda s: s.replace(",", "", 1),
|
| 39 |
+
lambda s: s.replace("'", '"', 1) if "'" in s else s + " 'unclosed",
|
| 40 |
+
]
|
| 41 |
+
return rng.choice(mutations)(sql), "syntax error at or near unexpected token"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def inject_join_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 45 |
+
t1, t2 = _first_table(exercise), _second_table(exercise)
|
| 46 |
+
col = _pick(rng, list(exercise.columns))
|
| 47 |
+
variants = [
|
| 48 |
+
f"SELECT {col} FROM {t1} JOIN {t2}",
|
| 49 |
+
f"SELECT {col} FROM {t1} INNER JOIN {t2} ON {t1}.id = {t2}.id",
|
| 50 |
+
(
|
| 51 |
+
f"SELECT {t1}.{col} FROM {t1} "
|
| 52 |
+
f"LEFT JOIN {t2} ON {t1}.{col} = {t2}.{col}"
|
| 53 |
+
),
|
| 54 |
+
f"SELECT * FROM {t1}, {t2} WHERE {t1}.wrong_id = {t2}.wrong_id",
|
| 55 |
+
]
|
| 56 |
+
return rng.choice(variants), "missing ON clause or invalid join condition"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def inject_aggregation_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 60 |
+
t = _first_table(exercise)
|
| 61 |
+
cols = list(exercise.columns)
|
| 62 |
+
group_col = cols[0]
|
| 63 |
+
agg_col = cols[-1]
|
| 64 |
+
bad = f"SELECT {group_col}, AVG({agg_col}) FROM {t}"
|
| 65 |
+
return bad, "column must appear in GROUP BY clause or be used in aggregate function"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def inject_having_where_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 69 |
+
t = _first_table(exercise)
|
| 70 |
+
cols = list(exercise.columns)
|
| 71 |
+
group_col, agg_col = cols[0], cols[-1]
|
| 72 |
+
bad = (
|
| 73 |
+
f"SELECT {group_col}, COUNT({agg_col}) FROM {t} "
|
| 74 |
+
f"WHERE COUNT({agg_col}) > {rng.randint(1, 5)}"
|
| 75 |
+
)
|
| 76 |
+
return bad, "aggregate functions are not allowed in WHERE"
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def inject_subquery_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 80 |
+
t1, t2 = _first_table(exercise), _second_table(exercise)
|
| 81 |
+
col = _pick(rng, list(exercise.columns))
|
| 82 |
+
bad = f"SELECT {col} FROM {t1} WHERE {col} = (SELECT {col} FROM {t2})"
|
| 83 |
+
return bad, "subquery returned more than one row"
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def inject_window_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 87 |
+
t = _first_table(exercise)
|
| 88 |
+
col = _pick(rng, list(exercise.columns))
|
| 89 |
+
variants = [
|
| 90 |
+
f"SELECT {col}, ROW_NUMBER() OVER () FROM {t}",
|
| 91 |
+
f"SELECT {col}, SUM({col}) OVER (ORDER BY {col}) FROM {t} GROUP BY {col}",
|
| 92 |
+
f"SELECT {col}, RANK() OVER (PARTITION {col}) FROM {t}",
|
| 93 |
+
]
|
| 94 |
+
return rng.choice(variants), "window function requires PARTITION BY or ORDER BY"
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def inject_null_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 98 |
+
t = _first_table(exercise)
|
| 99 |
+
col = _pick(rng, list(exercise.columns))
|
| 100 |
+
bad = f"SELECT * FROM {t} WHERE {col} = NULL"
|
| 101 |
+
return bad, "use IS NULL or IS NOT NULL to test for null values"
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def inject_date_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 105 |
+
t = _first_table(exercise)
|
| 106 |
+
variants = [
|
| 107 |
+
f"SELECT * FROM {t} WHERE order_date = '31/02/2023'",
|
| 108 |
+
f"SELECT * FROM {t} WHERE order_date = DATE '2023-13-40'",
|
| 109 |
+
f"SELECT * FROM {t} WHERE STR_TO_DATE('bad-date', '%Y-%m-%d')",
|
| 110 |
+
f"SELECT * FROM {t} WHERE hire_date > 'yesterday'",
|
| 111 |
+
]
|
| 112 |
+
return rng.choice(variants), "invalid date format or unknown date function"
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def inject_column_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 116 |
+
t = _first_table(exercise)
|
| 117 |
+
col = _pick(rng, FAKE_COLUMNS)
|
| 118 |
+
bad = f"SELECT {col} FROM {t}"
|
| 119 |
+
return bad, f"column '{col}' does not exist"
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def inject_table_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 123 |
+
tbl = _pick(rng, FAKE_TABLES)
|
| 124 |
+
col = _pick(rng, list(exercise.columns))
|
| 125 |
+
bad = f"SELECT {col} FROM {tbl}"
|
| 126 |
+
return bad, f"relation '{tbl}' does not exist"
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def inject_datatype_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 130 |
+
t = _first_table(exercise)
|
| 131 |
+
col = _pick(rng, list(exercise.columns))
|
| 132 |
+
bad = f"SELECT {col} FROM {t} WHERE {col} = '{rng.choice(['abc', 'ten', 'N/A'])}'"
|
| 133 |
+
return bad, "operator does not exist: integer = character varying"
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def inject_duplicate_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 137 |
+
"""Drop DISTINCT when the question asks for unique values."""
|
| 138 |
+
sql = exercise.correct_query
|
| 139 |
+
if "DISTINCT" in sql.upper():
|
| 140 |
+
bad = sql.upper().replace("DISTINCT ", "").replace("distinct ", "")
|
| 141 |
+
# restore original casing loosely
|
| 142 |
+
bad = sql.replace("DISTINCT ", "").replace("distinct ", "")
|
| 143 |
+
else:
|
| 144 |
+
col = _pick(rng, list(exercise.columns))
|
| 145 |
+
bad = f"SELECT {col} FROM {_first_table(exercise)}"
|
| 146 |
+
return bad, "query returns duplicate rows; DISTINCT may be required"
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def inject_logical_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 150 |
+
"""
|
| 151 |
+
Produce a query that runs against the schema but answers the question incorrectly.
|
| 152 |
+
Variants are tied to the exercise question and correct answer.
|
| 153 |
+
"""
|
| 154 |
+
sql = exercise.correct_query
|
| 155 |
+
q = exercise.question.lower()
|
| 156 |
+
variants: List[str] = []
|
| 157 |
+
|
| 158 |
+
if "average" in q or "avg" in sql.lower():
|
| 159 |
+
variants.append(sql.replace("AVG(", "SUM(", 1))
|
| 160 |
+
variants.append(sql.replace("AVG(", "MAX(", 1))
|
| 161 |
+
if " and " in q and " AND " in sql:
|
| 162 |
+
variants.append(sql.replace(" AND ", " OR ", 1))
|
| 163 |
+
if "join" in sql.lower():
|
| 164 |
+
t1, t2 = _first_table(exercise), _second_table(exercise)
|
| 165 |
+
variants.append(
|
| 166 |
+
f"SELECT {t1}.name, {t2}.name FROM {t1} "
|
| 167 |
+
f"JOIN {t2} ON {t1}.id = {t2}.id"
|
| 168 |
+
)
|
| 169 |
+
variants.append(sql.replace("INNER JOIN", "LEFT JOIN", 1))
|
| 170 |
+
if "between" in q and "BETWEEN" in sql.upper():
|
| 171 |
+
upper = sql.upper()
|
| 172 |
+
between_part = upper.split("BETWEEN", 1)[1]
|
| 173 |
+
bounds = between_part.split("AND", 1)
|
| 174 |
+
if len(bounds) == 2:
|
| 175 |
+
lo = bounds[0].strip().split()[-1]
|
| 176 |
+
hi = bounds[1].strip().split()[0]
|
| 177 |
+
variants.append(
|
| 178 |
+
sql.split("WHERE", 1)[0]
|
| 179 |
+
+ f" WHERE price BETWEEN {hi} AND {lo}"
|
| 180 |
+
)
|
| 181 |
+
if "rank" in q or "over" in sql.lower():
|
| 182 |
+
col = _pick(rng, list(exercise.columns))
|
| 183 |
+
variants.append(
|
| 184 |
+
f"SELECT name, {col} FROM {_first_table(exercise)} ORDER BY {col} DESC"
|
| 185 |
+
)
|
| 186 |
+
if "total" in q and "WHERE" in sql.upper():
|
| 187 |
+
variants.append(sql.replace("active", "inactive"))
|
| 188 |
+
if "highest" in q or "max" in sql.lower():
|
| 189 |
+
col = _pick(rng, list(exercise.columns))
|
| 190 |
+
variants.append(
|
| 191 |
+
f"SELECT name FROM {_first_table(exercise)} "
|
| 192 |
+
f"WHERE {col} >= (SELECT AVG({col}) FROM {_second_table(exercise)})"
|
| 193 |
+
)
|
| 194 |
+
if "enrolled" in q and "INNER JOIN" in sql.upper():
|
| 195 |
+
variants.append(sql.replace("INNER JOIN", "LEFT JOIN", 1))
|
| 196 |
+
if "not provided" in q or "is null" in sql.lower():
|
| 197 |
+
variants.append(sql.replace("IS NULL", "= ''"))
|
| 198 |
+
|
| 199 |
+
if not variants:
|
| 200 |
+
col = _pick(rng, list(exercise.columns))
|
| 201 |
+
t = _first_table(exercise)
|
| 202 |
+
variants = [
|
| 203 |
+
f"SELECT {col} FROM {t} ORDER BY {col} DESC LIMIT 10",
|
| 204 |
+
f"SELECT COUNT(*) FROM {t}",
|
| 205 |
+
]
|
| 206 |
+
|
| 207 |
+
bad = rng.choice(variants)
|
| 208 |
+
return bad, "query executes but produces incorrect result set"
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def inject_performance_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 212 |
+
t1, t2 = _first_table(exercise), _second_table(exercise)
|
| 213 |
+
variants = [
|
| 214 |
+
f"SELECT * FROM {t1}",
|
| 215 |
+
f"SELECT * FROM {t1} JOIN {t2} ON {t1}.id = {t2}.id",
|
| 216 |
+
(
|
| 217 |
+
f"SELECT * FROM {t1} "
|
| 218 |
+
f"WHERE {_pick(rng, list(exercise.columns))} "
|
| 219 |
+
f"LIKE '%{rng.choice(['a', 'e', 'i'])}%'"
|
| 220 |
+
),
|
| 221 |
+
f"SELECT * FROM {t1} CROSS JOIN {t2}",
|
| 222 |
+
]
|
| 223 |
+
return rng.choice(variants), "inefficient query: SELECT * or cartesian join detected"
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def inject_filtering_error(rng: random.Random, exercise: Exercise) -> Tuple[str, str]:
|
| 227 |
+
sql = exercise.correct_query
|
| 228 |
+
col = _pick(rng, list(exercise.columns))
|
| 229 |
+
t = _first_table(exercise)
|
| 230 |
+
threshold = rng.randint(50, 90)
|
| 231 |
+
variants = [
|
| 232 |
+
sql.replace(">", "<", 1) if ">" in sql else sql.replace("=", "!=", 1),
|
| 233 |
+
f"SELECT {col} FROM {t} WHERE {col} > {threshold} AND {col} < {threshold - 20}",
|
| 234 |
+
f"SELECT {col} FROM {t} WHERE NOT {col} > {threshold}",
|
| 235 |
+
sql.replace(" AND ", " OR ", 1) if " AND " in sql else (
|
| 236 |
+
f"SELECT {col} FROM {t} WHERE {col} BETWEEN {threshold} AND {threshold - 10}"
|
| 237 |
+
),
|
| 238 |
+
]
|
| 239 |
+
return rng.choice(variants), "WHERE clause filters incorrect rows"
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
ERROR_INJECTORS: Dict[int, Callable[[random.Random, Exercise], Tuple[str, str]]] = {
|
| 243 |
+
0: inject_syntax_error,
|
| 244 |
+
1: inject_join_error,
|
| 245 |
+
2: inject_aggregation_error,
|
| 246 |
+
3: inject_having_where_error,
|
| 247 |
+
4: inject_subquery_error,
|
| 248 |
+
5: inject_window_error,
|
| 249 |
+
6: inject_null_error,
|
| 250 |
+
7: inject_date_error,
|
| 251 |
+
8: inject_column_error,
|
| 252 |
+
9: inject_table_error,
|
| 253 |
+
10: inject_datatype_error,
|
| 254 |
+
11: inject_duplicate_error,
|
| 255 |
+
12: inject_logical_error,
|
| 256 |
+
13: inject_performance_error,
|
| 257 |
+
14: inject_filtering_error,
|
| 258 |
+
}
|
train_space_app.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face Space — CodeBERT SQL Error Classifier Training UI.
|
| 3 |
+
|
| 4 |
+
Deploy as a Gradio Space with app_file: train_space_app.py
|
| 5 |
+
Set hardware to GPU (t4-small recommended).
|
| 6 |
+
Add HF_TOKEN secret to push trained models to your Hub account.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import shutil
|
| 14 |
+
import tempfile
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import gradio as gr
|
| 18 |
+
import pandas as pd
|
| 19 |
+
|
| 20 |
+
from src.hf_train_codebert import train
|
| 21 |
+
|
| 22 |
+
PROJECT_ROOT = Path(__file__).parent
|
| 23 |
+
DEFAULT_DATA = PROJECT_ROOT / "data" / "sql_errors_dev.parquet"
|
| 24 |
+
OUTPUT_DIR = PROJECT_ROOT / "models" / "codebert-cross-encoder"
|
| 25 |
+
BUNDLED_DATASETS = {
|
| 26 |
+
"Dev (15K samples)": str(PROJECT_ROOT / "data" / "sql_errors_dev.parquet"),
|
| 27 |
+
"Full (1M samples)": str(PROJECT_ROOT / "data" / "sql_errors_1m.parquet"),
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _format_metrics(metrics: dict) -> str:
|
| 32 |
+
val = metrics.get("validation", {})
|
| 33 |
+
test = metrics.get("test", {})
|
| 34 |
+
lines = [
|
| 35 |
+
"## Training complete",
|
| 36 |
+
"",
|
| 37 |
+
f"- Train samples: **{metrics.get('train_samples', 0):,}**",
|
| 38 |
+
f"- Val samples: **{metrics.get('val_samples', 0):,}**",
|
| 39 |
+
f"- Test samples: **{metrics.get('test_samples', 0):,}**",
|
| 40 |
+
"",
|
| 41 |
+
"### Validation",
|
| 42 |
+
f"- F1 macro: **{val.get('eval_f1_macro', 0):.4f}**",
|
| 43 |
+
f"- F1 micro: **{val.get('eval_f1_micro', 0):.4f}**",
|
| 44 |
+
"",
|
| 45 |
+
"### Test",
|
| 46 |
+
f"- F1 macro: **{test.get('f1_macro', 0):.4f}**",
|
| 47 |
+
f"- F1 micro: **{test.get('f1_micro', 0):.4f}**",
|
| 48 |
+
f"- Subset accuracy: **{test.get('subset_accuracy', 0):.4f}**",
|
| 49 |
+
"",
|
| 50 |
+
f"Model saved to `{OUTPUT_DIR}`",
|
| 51 |
+
]
|
| 52 |
+
if metrics.get("hub_url"):
|
| 53 |
+
lines.append(f"\n**Hub model:** {metrics['hub_url']}")
|
| 54 |
+
return "\n".join(lines)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def run_training(
|
| 58 |
+
dataset_choice: str,
|
| 59 |
+
uploaded_file,
|
| 60 |
+
max_samples: int,
|
| 61 |
+
epochs: float,
|
| 62 |
+
batch_size: int,
|
| 63 |
+
learning_rate: float,
|
| 64 |
+
max_length: int,
|
| 65 |
+
fp16: bool,
|
| 66 |
+
push_to_hub: bool,
|
| 67 |
+
hub_model_id: str,
|
| 68 |
+
progress=gr.Progress(),
|
| 69 |
+
):
|
| 70 |
+
progress(0, desc="Preparing dataset...")
|
| 71 |
+
|
| 72 |
+
if uploaded_file is not None:
|
| 73 |
+
data_path = Path(uploaded_file.name)
|
| 74 |
+
else:
|
| 75 |
+
data_path = Path(BUNDLED_DATASETS.get(dataset_choice, DEFAULT_DATA))
|
| 76 |
+
if not data_path.exists():
|
| 77 |
+
return (
|
| 78 |
+
f"Dataset not found: `{data_path}`. "
|
| 79 |
+
"Upload a parquet file or include data/ in the Space repo.",
|
| 80 |
+
None,
|
| 81 |
+
None,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
hub_token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
|
| 85 |
+
if push_to_hub and not hub_token:
|
| 86 |
+
return (
|
| 87 |
+
"Add `HF_TOKEN` to Space secrets to push models to the Hub.",
|
| 88 |
+
None,
|
| 89 |
+
None,
|
| 90 |
+
)
|
| 91 |
+
if push_to_hub and not hub_model_id.strip():
|
| 92 |
+
return "Enter a Hub model id (e.g. `your-username/sql-codebert-classifier`).", None, None
|
| 93 |
+
|
| 94 |
+
if OUTPUT_DIR.exists():
|
| 95 |
+
shutil.rmtree(OUTPUT_DIR, ignore_errors=True)
|
| 96 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 97 |
+
|
| 98 |
+
samples = int(max_samples) if max_samples and max_samples > 0 else None
|
| 99 |
+
progress(0.1, desc="Starting CodeBERT training...")
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
metrics = train(
|
| 103 |
+
data_path=data_path,
|
| 104 |
+
output_dir=OUTPUT_DIR,
|
| 105 |
+
epochs=epochs,
|
| 106 |
+
batch_size=batch_size,
|
| 107 |
+
learning_rate=learning_rate,
|
| 108 |
+
max_length=max_length,
|
| 109 |
+
max_samples=samples,
|
| 110 |
+
fp16=fp16,
|
| 111 |
+
save_strategy="no",
|
| 112 |
+
push_to_hub=push_to_hub,
|
| 113 |
+
hub_model_id=hub_model_id.strip() or None,
|
| 114 |
+
hub_token=hub_token,
|
| 115 |
+
)
|
| 116 |
+
except Exception as exc:
|
| 117 |
+
return f"Training failed:\n\n```\n{exc}\n```", None, None
|
| 118 |
+
|
| 119 |
+
progress(1.0, desc="Done")
|
| 120 |
+
if push_to_hub and hub_model_id.strip():
|
| 121 |
+
metrics["hub_url"] = f"https://huggingface.co/{hub_model_id.strip()}"
|
| 122 |
+
|
| 123 |
+
metrics_path = OUTPUT_DIR / "metrics.json"
|
| 124 |
+
summary = _format_metrics(metrics)
|
| 125 |
+
return summary, str(metrics_path) if metrics_path.exists() else None, str(OUTPUT_DIR)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def load_preview(dataset_choice: str, uploaded_file) -> str:
|
| 129 |
+
try:
|
| 130 |
+
if uploaded_file is not None:
|
| 131 |
+
df = pd.read_parquet(uploaded_file.name)
|
| 132 |
+
else:
|
| 133 |
+
path = BUNDLED_DATASETS.get(dataset_choice, DEFAULT_DATA)
|
| 134 |
+
if not Path(path).exists():
|
| 135 |
+
return f"Dataset not found: {path}"
|
| 136 |
+
df = pd.read_parquet(path)
|
| 137 |
+
cols = list(df.columns)
|
| 138 |
+
sample = df.head(2).to_dict(orient="records")
|
| 139 |
+
return f"**Rows:** {len(df):,}\n\n**Columns:** `{cols}`\n\n**Sample:**\n```json\n{json.dumps(sample, indent=2)[:2000]}\n```"
|
| 140 |
+
except Exception as exc:
|
| 141 |
+
return f"Could not load preview: {exc}"
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
with gr.Blocks(title="SQL Error Classifier — Train") as demo:
|
| 145 |
+
gr.Markdown(
|
| 146 |
+
"""
|
| 147 |
+
# SQL Error Classifier — CodeBERT Training
|
| 148 |
+
Train **microsoft/codebert-base** as a cross-encoder on this Space.
|
| 149 |
+
|
| 150 |
+
**Input format:** `QUESTION` + `SCHEMA` + `STUDENT_SQL` + `CORRECT_SQL` (single sequence)
|
| 151 |
+
|
| 152 |
+
**GPU recommended** — upgrade Space hardware to `t4-small` or better.
|
| 153 |
+
"""
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
with gr.Row():
|
| 157 |
+
with gr.Column(scale=1):
|
| 158 |
+
dataset_choice = gr.Dropdown(
|
| 159 |
+
choices=list(BUNDLED_DATASETS.keys()),
|
| 160 |
+
value="Dev (15K samples)",
|
| 161 |
+
label="Bundled dataset",
|
| 162 |
+
)
|
| 163 |
+
uploaded = gr.File(
|
| 164 |
+
label="Or upload parquet",
|
| 165 |
+
file_types=[".parquet"],
|
| 166 |
+
)
|
| 167 |
+
preview_btn = gr.Button("Preview dataset")
|
| 168 |
+
preview_out = gr.Markdown()
|
| 169 |
+
|
| 170 |
+
max_samples = gr.Number(
|
| 171 |
+
label="Max samples (0 = all)",
|
| 172 |
+
value=5000,
|
| 173 |
+
precision=0,
|
| 174 |
+
)
|
| 175 |
+
epochs = gr.Slider(1, 10, value=2, step=1, label="Epochs")
|
| 176 |
+
batch_size = gr.Slider(4, 64, value=8, step=4, label="Batch size")
|
| 177 |
+
learning_rate = gr.Number(label="Learning rate", value=2e-5)
|
| 178 |
+
max_length = gr.Slider(128, 512, value=512, step=64, label="Max length")
|
| 179 |
+
fp16 = gr.Checkbox(label="FP16 (GPU only)", value=True)
|
| 180 |
+
|
| 181 |
+
push_to_hub = gr.Checkbox(label="Push to Hugging Face Hub", value=False)
|
| 182 |
+
hub_model_id = gr.Textbox(
|
| 183 |
+
label="Hub model id",
|
| 184 |
+
placeholder="your-username/sql-codebert-classifier",
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
train_btn = gr.Button("Start Training", variant="primary")
|
| 188 |
+
|
| 189 |
+
with gr.Column(scale=1):
|
| 190 |
+
result = gr.Markdown(label="Results")
|
| 191 |
+
metrics_file = gr.File(label="metrics.json")
|
| 192 |
+
model_dir = gr.Textbox(label="Model output path", interactive=False)
|
| 193 |
+
|
| 194 |
+
preview_btn.click(load_preview, [dataset_choice, uploaded], preview_out)
|
| 195 |
+
train_btn.click(
|
| 196 |
+
run_training,
|
| 197 |
+
[
|
| 198 |
+
dataset_choice,
|
| 199 |
+
uploaded,
|
| 200 |
+
max_samples,
|
| 201 |
+
epochs,
|
| 202 |
+
batch_size,
|
| 203 |
+
learning_rate,
|
| 204 |
+
max_length,
|
| 205 |
+
fp16,
|
| 206 |
+
push_to_hub,
|
| 207 |
+
hub_model_id,
|
| 208 |
+
],
|
| 209 |
+
[result, metrics_file, model_dir],
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
gr.Markdown(
|
| 213 |
+
"""
|
| 214 |
+
### Space setup
|
| 215 |
+
1. Create a Gradio Space and push this repo
|
| 216 |
+
2. Set **Hardware → GPU (t4-small)**
|
| 217 |
+
3. Add secret `HF_TOKEN` (write token) to push models
|
| 218 |
+
4. Include `data/sql_errors_dev.parquet` in the repo (or upload at runtime)
|
| 219 |
+
|
| 220 |
+
### After training
|
| 221 |
+
Use the saved model with:
|
| 222 |
+
```python
|
| 223 |
+
from src.hf_predict_codebert import CodeBERTSQLErrorClassifier
|
| 224 |
+
clf = CodeBERTSQLErrorClassifier("models/codebert-cross-encoder")
|
| 225 |
+
```
|
| 226 |
+
"""
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
if __name__ == "__main__":
|
| 230 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|