Spaces:
Running on Zero
Running on Zero
Download preprocessing/download_datasets.py from heatherlead/comment_moderation: direct link, hf CLI and curl.
- Browser
- Download file 8.12 kB
-
https://huggingface.co/spaces/heatherlead/comment_moderation/resolve/main/preprocessing/download_datasets.py
- Command line
-
hf download hf://spaces/heatherlead/comment_moderation/preprocessing/download_datasets.py
-
curl -L -o download_datasets.py https://huggingface.co/spaces/heatherlead/comment_moderation/resolve/main/preprocessing/download_datasets.py
8.12 kB
| """ | |
| download_datasets.py | |
| ==================== | |
| Downloads all public datasets from HuggingFace Hub and saves them | |
| as CSV files in data/raw/ for downstream processing. | |
| Datasets: | |
| - Civil Comments (google/civil_comments) | |
| - HateXplain (Hate-speech-CNERG/hatexplain) | |
| - OLID (christophsonntag/OLID) | |
| - Jigsaw Toxic Comment Classification | |
| Usage: | |
| python preprocessing/download_datasets.py | |
| """ | |
| import json | |
| import os | |
| import sys | |
| import logging | |
| import urllib.request | |
| from collections import Counter | |
| from pathlib import Path | |
| import pandas as pd | |
| from datasets import load_dataset | |
| import threading | |
| import multiprocess.resource_tracker as rt | |
| # Silence the multiprocess cleanup exception during interpreter shutdown | |
| def _noop_del(self): | |
| pass | |
| rt.ResourceTracker.__del__ = _noop_del | |
| # Patch Python background thread cleanup bug in `multiprocess` | |
| if not hasattr(threading.RLock, "_recursion_count"): | |
| threading.RLock._recursion_count = lambda self: 0 | |
| # --------------------------------------------------------------------------- | |
| # Setup | |
| # --------------------------------------------------------------------------- | |
| os.environ["HF_DATASETS_NUM_PROC"] = "1" | |
| os.environ["TOKENIZERS_PARALLELISM"] = "false" | |
| # Add this line: | |
| os.environ["MULTIPROCESS_NO_RESOURCE_TRACKER"] = "1" | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s | %(levelname)s | %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| PROJECT_ROOT = Path(__file__).resolve().parent.parent | |
| RAW_DIR = PROJECT_ROOT / "data" / "raw" | |
| RAW_DIR.mkdir(parents=True, exist_ok=True) | |
| # --------------------------------------------------------------------------- | |
| # Download helpers | |
| # --------------------------------------------------------------------------- | |
| def download_civil_comments(): | |
| """ | |
| Civil Comments — ~1.8M comments with continuous toxicity scores. | |
| We sample a manageable subset (100k) to keep training feasible. | |
| """ | |
| output_path = RAW_DIR / "civil_comments.csv" | |
| if output_path.exists(): | |
| logger.info("civil_comments.csv already exists, skipping download.") | |
| return | |
| logger.info("Downloading Civil Comments dataset...") | |
| ds = load_dataset("google/civil_comments", split="train") | |
| df = ds.to_pandas() | |
| # Keep relevant columns | |
| keep_cols = [ | |
| "text", "toxicity", "severe_toxicity", | |
| "obscene", "threat", "insult", "identity_attack", "sexual_explicit", | |
| ] | |
| df = df[[c for c in keep_cols if c in df.columns]] | |
| # Sample 100k to keep dataset manageable (stratified by toxic/non-toxic) | |
| df["is_toxic"] = (df["toxicity"] >= 0.5).astype(int) | |
| toxic = df[df["is_toxic"] == 1] | |
| safe = df[df["is_toxic"] == 0].sample(n=min(70000, len(df[df["is_toxic"] == 0])), random_state=42) | |
| df = pd.concat([toxic.head(30000), safe], ignore_index=True) | |
| df = df.drop(columns=["is_toxic"]) | |
| df.to_csv(output_path, index=False) | |
| logger.info(f"Saved {len(df)} rows to {output_path}") | |
| def download_hatexplain(): | |
| """ | |
| HateXplain — ~20K posts with hatespeech/offensive/normal labels. | |
| Downloads raw JSON dataset directly from GitHub. | |
| """ | |
| output_path = RAW_DIR / "hatexplain.csv" | |
| if output_path.exists(): | |
| logger.info("hatexplain.csv already exists, skipping download.") | |
| return | |
| logger.info("Downloading HateXplain dataset from source...") | |
| url = "https://raw.githubusercontent.com/punyajoy/HateXplain/master/Data/dataset.json" | |
| try: | |
| with urllib.request.urlopen(url, timeout=120) as response: | |
| data = json.loads(response.read().decode()) | |
| except Exception as e: | |
| logger.error(f" Failed to download HateXplain: {e}") | |
| logger.warning(" Skipping HateXplain — continuing with other datasets.") | |
| return | |
| rows = [] | |
| for pid, entry in data.items(): | |
| tokens = entry.get("post_tokens", []) | |
| text = " ".join(tokens) if isinstance(tokens, list) else str(tokens) | |
| if not text.strip(): | |
| continue | |
| # Annotators: list of dicts with "label" key | |
| # Label values: "hatespeech", "normal", "offensive" | |
| ann_labels = [] | |
| annotators = entry.get("annotators", []) | |
| if isinstance(annotators, list): | |
| ann_labels = [a.get("label", "normal") for a in annotators if isinstance(a, dict)] | |
| elif isinstance(annotators, dict): | |
| # Some versions store as {"label": [...], "annotator_id": [...]} | |
| ann_labels = annotators.get("label", []) | |
| if not ann_labels: | |
| continue | |
| majority_label = Counter(ann_labels).most_common(1)[0][0] | |
| # Normalise to string | |
| if isinstance(majority_label, int): | |
| majority_label = {0: "hatespeech", 1: "normal", 2: "offensive"}.get(majority_label, "normal") | |
| rows.append({"text": text, "label": str(majority_label).lower()}) | |
| df = pd.DataFrame(rows) | |
| df.to_csv(output_path, index=False) | |
| logger.info(f"Saved {len(df)} rows to {output_path}") | |
| def download_olid(): | |
| """ | |
| OLID — 14K tweets with offensive language identification. | |
| Subtask A: OFF/NOT | |
| """ | |
| output_path = RAW_DIR / "olid.csv" | |
| if output_path.exists(): | |
| logger.info("olid.csv already exists, skipping download.") | |
| return | |
| logger.info("Downloading OLID dataset...") | |
| ds = load_dataset("christophsonntag/OLID", split="train") | |
| df = ds.to_pandas() | |
| # Rename columns to standard format | |
| if "tweet" in df.columns: | |
| df = df.rename(columns={"tweet": "text"}) | |
| if "subtask_a" in df.columns: | |
| df = df.rename(columns={"subtask_a": "label_a"}) | |
| if "subtask_b" in df.columns: | |
| df = df.rename(columns={"subtask_b": "label_b"}) | |
| if "subtask_c" in df.columns: | |
| df = df.rename(columns={"subtask_c": "label_c"}) | |
| df.to_csv(output_path, index=False) | |
| logger.info(f"Saved {len(df)} rows to {output_path}") | |
| def download_jigsaw_toxic(): | |
| """ | |
| Jigsaw Toxic Comment Classification — ~160K comments with | |
| multi-label toxicity annotations. | |
| """ | |
| output_path = RAW_DIR / "jigsaw_toxic.csv" | |
| if output_path.exists(): | |
| logger.info("jigsaw_toxic.csv already exists, skipping download.") | |
| return | |
| logger.info("Downloading Jigsaw Toxic Comment dataset...") | |
| try: | |
| ds = load_dataset( | |
| "thesofakillers/jigsaw-toxic-comment-classification-challenge", | |
| split="train", | |
| ) | |
| df = ds.to_pandas() | |
| keep_cols = [ | |
| "comment_text", "toxic", "severe_toxic", | |
| "obscene", "threat", "insult", "identity_hate", | |
| ] | |
| df = df[[c for c in keep_cols if c in df.columns]] | |
| df = df.rename(columns={"comment_text": "text"}) | |
| df.to_csv(output_path, index=False) | |
| logger.info(f"Saved {len(df)} rows to {output_path}") | |
| except Exception as e: | |
| logger.warning(f"Could not download Jigsaw dataset: {e}") | |
| logger.warning("You may need to download it manually from Kaggle.") | |
| logger.warning("Place the train.csv in data/raw/jigsaw_toxic.csv") | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| def main(): | |
| logger.info("=" * 60) | |
| logger.info("Starting dataset downloads...") | |
| logger.info("=" * 60) | |
| download_civil_comments() | |
| download_hatexplain() | |
| download_olid() | |
| download_jigsaw_toxic() | |
| logger.info("=" * 60) | |
| logger.info("All downloads complete!") | |
| logger.info(f"Raw data saved to: {RAW_DIR}") | |
| logger.info("=" * 60) | |
| # Show summary | |
| for f in RAW_DIR.glob("*.csv"): | |
| df = pd.read_csv(f, nrows=0) | |
| row_count = sum(1 for _ in open(f, encoding="utf-8")) - 1 | |
| logger.info(f" {f.name}: {row_count:,} rows, columns: {list(df.columns)}") | |
| if __name__ == "__main__": | |
| try: | |
| main() | |
| finally: | |
| # Force resource_tracker cleanup before garbage collection tears down threading objects | |
| try: | |
| import multiprocess.resource_tracker as rt | |
| rt.get_tracker()._stop() | |
| except Exception: | |
| pass |