kz110AIPI
Initial commit for Module 2 NLP project
2f74c56
Raw
History Blame Contribute Delete
5.53 kB
# Portions of this file were developed with assistance from OpenAI ChatGPT/Codex and reviewed/modified by the author.
"""Robustness experiment with noisy text transformations.
Portions of this file were developed with assistance from OpenAI ChatGPT/Codex and reviewed/modified by the author.
"""
from __future__ import annotations
import random
from pathlib import Path
import matplotlib.pyplot as plt
import pandas as pd
from campus_triage.config import CLASSICAL_MODEL_PATH, OUTPUTS_DIR, RANDOM_SEED, TEST_PATH
from campus_triage.evaluate import evaluate_dual_classifier, ensure_models_and_data
from campus_triage.models import (
DualClassifier,
load_dual_classifier,
load_transformer_dual_classifier,
transformer_model_available,
)
def delete_random_character(text: str, random_state: random.Random) -> str:
"""Delete one random non-space character from text."""
candidates = [index for index, char in enumerate(text) if not char.isspace()]
if not candidates:
return text
index = random_state.choice(candidates)
return text[:index] + text[index + 1 :]
def add_random_typo(text: str, random_state: random.Random) -> str:
"""Swap two adjacent characters in a random word."""
words = text.split()
eligible = [index for index, word in enumerate(words) if len(word) > 4]
if not eligible:
return text
word_index = random_state.choice(eligible)
word = words[word_index]
char_index = random_state.randrange(0, len(word) - 1)
words[word_index] = word[:char_index] + word[char_index + 1] + word[char_index] + word[char_index + 2 :]
return " ".join(words)
def make_text_noisy(text: str, random_state: random.Random) -> str:
"""Apply deletion, typo, lowercasing, extra punctuation, and missing punctuation."""
noisy_text = delete_random_character(str(text), random_state)
noisy_text = add_random_typo(noisy_text, random_state)
noisy_text = noisy_text.lower()
noisy_text = noisy_text.replace(".", "")
noisy_text = noisy_text + random_state.choice(["!!!", "???", " ...", ""])
return noisy_text
def make_noisy_dataframe(dataframe: pd.DataFrame, seed: int = RANDOM_SEED) -> pd.DataFrame:
"""Create a noisy copy of a dataset."""
random_state = random.Random(seed)
noisy_dataframe = dataframe.copy()
noisy_dataframe["message_text"] = noisy_dataframe["message_text"].map(lambda text: make_text_noisy(text, random_state))
return noisy_dataframe
def evaluate_clean_and_noisy(model: DualClassifier, clean_dataframe: pd.DataFrame, noisy_dataframe: pd.DataFrame) -> list[dict[str, object]]:
"""Evaluate one model on clean and noisy test sets."""
clean_results = evaluate_dual_classifier(model, clean_dataframe)
clean_results["condition"] = "clean"
noisy_results = evaluate_dual_classifier(model, noisy_dataframe)
noisy_results["condition"] = "noisy"
return [clean_results, noisy_results]
def save_robustness_plot(results: pd.DataFrame, output_path: Path) -> None:
"""Save a bar plot comparing clean and noisy macro F1."""
plot_dataframe = results.melt(
id_vars=["model", "condition"],
value_vars=["category_macro_f1", "urgency_macro_f1"],
var_name="metric",
value_name="score",
)
plt.figure(figsize=(10, 5))
labels = [f"{row.model}\n{row.condition}\n{row.metric.replace('_macro_f1', '')}" for row in plot_dataframe.itertuples()]
plt.bar(range(len(plot_dataframe)), plot_dataframe["score"], color="#0f766e")
plt.xticks(range(len(plot_dataframe)), labels, rotation=30, ha="right")
plt.ylim(0, 1.05)
plt.ylabel("Macro F1")
plt.title("Robustness to Noisy Student Messages")
plt.tight_layout()
plt.savefig(output_path, dpi=160)
plt.close()
def run_robustness_experiment() -> pd.DataFrame:
"""Compare classical and transformer models on clean/noisy data when available."""
ensure_models_and_data()
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
test_dataframe = pd.read_csv(TEST_PATH)
noisy_dataframe = make_noisy_dataframe(test_dataframe)
models = [load_dual_classifier(str(CLASSICAL_MODEL_PATH))]
if transformer_model_available():
models.append(load_transformer_dual_classifier())
rows = []
for model in models:
rows.extend(evaluate_clean_and_noisy(model, test_dataframe, noisy_dataframe))
if not transformer_model_available():
rows.append(
{
"model": "distilbert_transformer",
"condition": "not_trained",
"category_accuracy": None,
"category_macro_f1": None,
"category_weighted_f1": None,
"urgency_accuracy": None,
"urgency_macro_f1": None,
"urgency_weighted_f1": None,
"interpretation": "Deep learning implementation is in src/campus_triage/transformer_training.py. Run `make train-transformer` to add transformer clean/noisy scores.",
}
)
results = pd.DataFrame(rows)
results.to_csv(OUTPUTS_DIR / "robustness_experiment.csv", index=False)
plottable_results = results.dropna(subset=["category_macro_f1", "urgency_macro_f1"])
save_robustness_plot(plottable_results, OUTPUTS_DIR / "robustness_plot.png")
return results
def main() -> None:
"""Run robustness experiment from the command line."""
results = run_robustness_experiment()
print(results.to_string(index=False))
if __name__ == "__main__":
main()