Spaces:
Sleeping
Sleeping
| # 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() | |