comment_moderation / preprocessing /download_datasets.py
heatherlead's picture
Add moderation backend with LFS tracking for large files
26c8f44
Raw History Blame Contribute Delete
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