From 80736d9d8b2491c86c8709687b13847d9f5f8012 Mon Sep 17 00:00:00 2001 From: Lars Malmqvist <12750146+lmlearning@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:03:56 +0200 Subject: [PATCH] Replace destructive setup with a reproducible CPU runtime and real model demo --- .gitattributes | 2 + .github/workflows/tests.yml | 17 ++++++ CITATION.cff | 10 ++++ README.md | 112 ++++++++++++++++++------------------ examples/cpu_demo.py | 32 +++++++++++ requirements-cpu.txt | 5 ++ requirements.txt | 16 +++--- scripts/setup_cpu.py | 30 ++++++++++ setup_environment.sh | 43 ++------------ tests/test_demo_runtime.py | 22 +++++++ tests/test_setup_cpu.py | 18 ++++++ train_consolidated.py | 3 +- 12 files changed, 207 insertions(+), 103 deletions(-) create mode 100644 .gitattributes create mode 100644 CITATION.cff create mode 100644 examples/cpu_demo.py create mode 100644 requirements-cpu.txt create mode 100644 scripts/setup_cpu.py create mode 100644 tests/test_demo_runtime.py create mode 100644 tests/test_setup_cpu.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b3f8edc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Shell entry points must retain Unix line endings on Windows. +*.sh text eol=lf diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6078d16..a599512 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,3 +20,20 @@ jobs: python-version: '3.11' - run: python -m pip install pytest - run: python -m pytest -q tests + + cpu-smoke: + runs-on: ubuntu-latest + env: + DGLBACKEND: pytorch + OMP_NUM_THREADS: '1' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.11' + - run: python scripts/setup_cpu.py --venv .venv + - run: .venv/bin/python -m pip install pytest + - run: .venv/bin/python -m pytest -q tests + - run: .venv/bin/python -m examples.cpu_demo diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..1a768de --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,10 @@ +cff-version: 1.2.0 +message: "Please cite this software and the associated paper where applicable." +type: software +title: "ExplainableArgGCN: Inspectable Graph Learning for Argumentation" +authors: + - family-names: Malmqvist + given-names: Lars +repository-code: "https://github.com/lmlearning/ExplainableArgGCN" +url: "https://github.com/lmlearning/ExplainableArgGCN" +license: MIT diff --git a/README.md b/README.md index 0441c92..b4c0822 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,64 @@ -# ExplainableArgGCN: Explainable Graph Learning for Argumentation - -Research code for **explainable graph neural networks in abstract argumentation**, with training, baseline evaluation, ablation studies and neighborhood visualizations. - -## What is included - -- Training implementations for refined argumentation GCNs and comparison models. -- Saved checkpoints, including ablation and ranking-loss sweeps. -- Intrinsic and baseline evaluation scripts. -- Results and graph-neighborhood visualizations. - -## Start here - -| Task | Entry point | -| --- | --- | -| Inspect dependencies | [requirements.txt](requirements.txt) and [setup_environment.sh](setup_environment.sh) | -| Understand training | [train_refined_afgcn.py](train_refined_afgcn.py) and [train_consolidated.py](train_consolidated.py) | -| Inspect evaluation | [evaluate_intrinsic.py](evaluate_intrinsic.py) and [evaluate_pyg_baselines.py](evaluate_pyg_baselines.py) | -| Review experiment orchestration | [run_main_and_baselines.sh](run_main_and_baselines.sh), [run_ablations.sh](run_ablations.sh), [run_rank_loss_sweep.sh](run_rank_loss_sweep.sh) | -| Explore outputs | [results](results/) and [visualizations](visualizations/) | -| Understand visualizations | [visualize_neighbourhoods.py](visualize_neighbourhoods.py) | - -The shell scripts document the experiment workflows. Review their data paths, model paths and hardware settings before reproducing a run. Saved checkpoints and outputs should be interpreted together with the configuration that produced them. - -## Related work - -[AFGCN](https://github.com/lmlearning/AFGCN) · [AFGraphLib](https://github.com/lmlearning/AFGraphLib) · [FastAFGCN](https://github.com/lmlearning/FastAFGCN) - -## TGF input validation - -`graph_io.py` provides the shared parser used by `pyg_train.py` and the refined -training pipeline. It accepts the unlabelled TGF subset used here: one argument -identifier per line, a single `#` separator, then two attack endpoints per line. -Blank lines are ignored and spaces or tabs can separate endpoints. Argument and -attack order, isolated arguments and self-attacks are preserved. - -Duplicate argument identifiers, malformed attacks, undeclared endpoints and missing -or repeated separators raise `ValueError` with file context. Optional TGF node/edge -labels are not supported. `train_refined_afgcn.parseTGF` retains its existing -list-of-lists attack return type. +# ExplainableArgGCN: Inspectable Graph Learning for Argumentation + +[![Tests](https://github.com/lmlearning/ExplainableArgGCN/actions/workflows/tests.yml/badge.svg)](https://github.com/lmlearning/ExplainableArgGCN/actions/workflows/tests.yml) + +**Explore how attack and defence relationships influence graph-based argument acceptance.** This research implementation combines structural features, paired message passing, residual connections and a ranking objective, with baseline models, ablations and influence visualizations. + +## Run a real CPU smoke example + +Use Python 3.11 in an activated virtual environment on Linux or Windows, from the repository root: + +```bash +python -m pip install -r requirements-cpu.txt pytest +python -m examples.cpu_demo +python -m pytest -q tests +``` + +The example creates a three-argument framework and known extension, loads it through the actual dataset loader, then runs the refined model's forward and backward passes. It reports targets `[1, 0, 1]`, two `[3]` output shapes and finite loss/gradients. It requires no checkpoint or training-data download and reports no benchmark accuracy. + +Alternatively, `python scripts/setup_cpu.py` creates `.venv` and installs the pinned CPU requirements. It never deletes data/cache files, runs system package managers or rewrites requirements. `setup_environment.sh` delegates to the same installer. + +## Architecture + +```mermaid +flowchart LR + D[Framework and accepted extensions] --> F[Structural features and grounded flag] + F --> P[Attack and defence message passing] + P --> H[Residual representation] + H --> C[Acceptance logits] + H --> R[Ranking scores] +``` + +| Entry point | What it shows | +| --- | --- | +| [train_consolidated.py](train_consolidated.py) | Refined architecture, baseline builders, training and ablation switches. | +| [train_refined_afgcn.py](train_refined_afgcn.py) | Dataset loading, grounded/categoriser signals and original refined model. | +| [graph_io.py](graph_io.py) | Validated TGF input and extension-union parsing. | +| [pyg_train.py](pyg_train.py) | GCN, GAT, GraphSAGE, GIN and other baseline implementations. | +| [evaluate_intrinsic.py](evaluate_intrinsic.py) | Intrinsic evaluation and graph export. | +| [visualize_neighbourhoods.py](visualize_neighbourhoods.py) | Local influence visualizations. | +| [results](results/) and [visualizations](visualizations/) | Historical experiment artifacts. | + +## Data contract and a training command + +A dataset directory contains matching pairs such as `example.tgf` and `example.EE-PR`. TGF uses one unlabelled argument identifier per line, a single `#`, then whitespace-separated attack pairs. APX is also supported by the dataset loader. Solution files use `[[a,b],[c]]`; the union supplies **credulous** acceptance targets. A flat extension `[a,b]`, empty extension `[[]]` and no extensions `[]` are accepted. + +The loader rejects undeclared solution identifiers. Singleton and edgeless graphs are covered by actual model tests. An empty graph is rejected with context. Computed feature caches are written beside the framework files. ```bash -python -m pip install pytest -python -m pytest tests +python train_consolidated.py --model refined --training_dir training_data --validation_dir validation_data --epochs 1 --checkpoint demo_checkpoint.pth ``` -These component tests need no graph-learning libraries or checkpoints. They validate -input handling; they do not reproduce training or evaluate model accuracy. +Supply real, separate training and validation datasets before using this command. See `--help` and the [experiment scripts](run_main_and_baselines.sh) for longer runs. Optional HOPE embeddings in the legacy PyG pipeline require [GEM](https://github.com/palash1992/GEM); the tested CPU path does not enable them. CUDA requires matching PyTorch and extension wheels from the [PyG installation guide](https://pytorch-geometric.readthedocs.io/en/2.6.1/install/installation.html). + +## Reproducibility status + +CI covers parsing, dataset labels, singleton/edgeless graphs, real CPU forward/backward passes, baseline loading and the first-run example. -## Training-label format +The corrected loader preserves the first argument in nested extension lists; older parsing could omit it. **Saved checkpoints and metrics have not been regenerated with that correction.** They remain historical artifacts, not newly verified benchmark claims. [Issue #3](https://github.com/lmlearning/ExplainableArgGCN/issues/3) tracks the split manifests, retraining and run provenance needed to regenerate them. -The dataset loader takes the union of identifiers in an ICCMA extension list such -as `[[a,b],[c]]` as its **credulous** acceptance target. A flat single extension -`[a,b]`, empty extension `[[]]` and empty extension list `[]` are also accepted. -Undeclared solution identifiers are rejected. Singleton and edgeless frameworks -retain a one-dimensional node output through both refined model implementations. +## Development and license -Earlier parsing could omit the first argument of nested extension lists. Retrain -and evaluate checkpoints before attributing results to the corrected loader; -saved historical checkpoints and metrics have not been regenerated by this fix. +Run the tests before a PR. Include a minimal framework/solution pair for input bugs; attach split definitions, seeds, versions and commands to any benchmark claim. -## License - -See [LICENSE](LICENSE). +[Citation metadata](CITATION.cff) · [MIT license](LICENSE) · Related [AFGCN](https://github.com/lmlearning/AFGCN) and [AFGraphLib](https://github.com/lmlearning/AFGraphLib). diff --git a/examples/cpu_demo.py b/examples/cpu_demo.py new file mode 100644 index 0000000..2fe0668 --- /dev/null +++ b/examples/cpu_demo.py @@ -0,0 +1,32 @@ +"""Exercise the dataset loader and refined model on an illustrative tiny graph.""" +import json +from pathlib import Path +import tempfile + +import torch + +from train_refined_afgcn import AFGraphDataset +from train_consolidated import RefinedAFGCN + + +def run_demo(): + torch.manual_seed(7) + torch.set_num_threads(1) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "example.tgf").write_text("a\nb\nc\n#\na b\nb c\n", encoding="utf-8") + (root / "example.EE-PR").write_text("[[a,c]]\n", encoding="utf-8") + data = AFGraphDataset(directory)[0] + model = RefinedAFGCN(hidden=8, layers=2) + logits, ranking = model(data) + loss = torch.nn.functional.binary_cross_entropy_with_logits(logits, data.y.float()) + loss.backward() + return {"example": "untrained CPU model; not a benchmark score", + "arguments": data.num_nodes, "accepted_targets": data.y.tolist(), + "logits_shape": list(logits.shape), "ranking_shape": list(ranking.shape), + "finite_loss": bool(torch.isfinite(loss)), + "finite_gradients": all(bool(torch.isfinite(p.grad).all()) for p in model.parameters() if p.grad is not None)} + + +if __name__ == "__main__": + print(json.dumps(run_demo(), indent=2)) diff --git a/requirements-cpu.txt b/requirements-cpu.txt new file mode 100644 index 0000000..c44ae26 --- /dev/null +++ b/requirements-cpu.txt @@ -0,0 +1,5 @@ +--extra-index-url https://download.pytorch.org/whl/cpu +--find-links https://data.pyg.org/whl/torch-2.7.0+cpu.html +-r requirements.txt +torch==2.7.0+cpu +torch-scatter==2.1.2+pt27cpu diff --git a/requirements.txt b/requirements.txt index 7f97b43..11d38a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -gem==0.1.12 -networkx==2.4 -numpy==1.21.5 -scikit_learn==1.7.1 -scipy==1.8.0 +# Core training/evaluation versions validated with Python 3.11. +# For CPU wheels use requirements-cpu.txt. Optional HOPE embeddings need GEM. +numpy==1.26.4 +scipy==1.15.3 +networkx==3.4.2 +scikit-learn==1.7.1 torch==2.7.0 -torch_geometric==2.6.1 -torch_scatter==2.1.2 +torch-geometric==2.6.1 +torch-scatter==2.1.2 tqdm==4.67.1 -torch_scatter diff --git a/scripts/setup_cpu.py b/scripts/setup_cpu.py new file mode 100644 index 0000000..28dbada --- /dev/null +++ b/scripts/setup_cpu.py @@ -0,0 +1,30 @@ +"""Create an isolated Python 3.11 CPU runtime; never remove research caches.""" +import argparse +from pathlib import Path +import subprocess +import sys +import venv + +ROOT = Path(__file__).resolve().parents[1] + + +def create_environment(destination): + destination = Path(destination).resolve() + venv.EnvBuilder(with_pip=True).create(destination) + python = destination / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") + subprocess.run([str(python), "-m", "pip", "install", "-r", str(ROOT / "requirements-cpu.txt")], check=True) + return python + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--venv", type=Path, default=ROOT / ".venv") + args = parser.parse_args() + if sys.version_info[:2] != (3, 11): + parser.error("Use Python 3.11 to create the tested environment") + python = create_environment(args.venv) + print(f"Environment ready. Run: {python} -m examples.cpu_demo") + + +if __name__ == "__main__": + main() diff --git a/setup_environment.sh b/setup_environment.sh index 0ddadc1..d8b75ff 100755 --- a/setup_environment.sh +++ b/setup_environment.sh @@ -1,38 +1,5 @@ -#!/bin/bash -# -# Sets up a robust and reproducible Python environment for the project. -# 1. Installs essential system packages including bc for shell calculations. -# 2. Cleans old, potentially incompatible data cache files. -# 3. Upgrades pip and installs specific, known-compatible Python libraries. - -echo "--- Setting up environment ---" - -# --- Step 1: Install system-level dependencies --- -echo "Installing build-essential, python3-dev, pybind11-dev, and bc..." -sudo apt-get update -sudo apt-get install -y build-essential python3-dev pybind11-dev bc - -# --- Step 2: Clean old cache files --- -echo "Cleaning old *.pkl cache files from data directories..." -find . -type f -name "*.pkl" -delete - -# --- Step 3: Upgrade pip and install core Python libraries --- -echo "Upgrading pip and installing compatible core libraries..." -pip install --upgrade pip -pip install networkx==3.2.1 numpy==1.26.4 scikit-learn==1.4.2 tqdm - -# --- Step 4: Install a stable version of PyTorch (CPU version) --- -echo "Installing a stable version of PyTorch..." -pip install torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 --index-url https://download.pytorch.org/whl/cpu - -# --- Step 5: Install PyG libraries from the official wheel index --- -echo "Installing PyTorch Geometric libraries (torch-scatter, etc.)..." -pip install torch_geometric torch_scatter torch_sparse torch_cluster torch_spline_conv -f https://data.pyg.org/whl/torch-$(python3 -c 'import torch; print(torch.__version__.split("+")[0])').html - -# --- Step 6: Install remaining project-specific packages --- -echo "Installing other required packages..." -pip install -q pipreqs -pipreqs . --force -pip install -r requirements.txt - -echo "--- Environment setup complete. ---" \ No newline at end of file +#!/usr/bin/env bash +# Create an isolated CPU environment without changing data or project files. +set -euo pipefail +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +exec "${PYTHON:-python3}" "$SCRIPT_DIR/scripts/setup_cpu.py" "$@" diff --git a/tests/test_demo_runtime.py b/tests/test_demo_runtime.py new file mode 100644 index 0000000..6fe2833 --- /dev/null +++ b/tests/test_demo_runtime.py @@ -0,0 +1,22 @@ +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("torch_geometric") +pytest.importorskip("torch_scatter") + +from examples.cpu_demo import run_demo +from train_consolidated import load_pyg_baselines + + +def test_real_cpu_demo(): + result = run_demo() + assert result["accepted_targets"] == [1, 0, 1] + assert result["logits_shape"] == result["ranking_shape"] == [3] + assert result["finite_loss"] and result["finite_gradients"] + + +def test_baseline_loading_is_independent_of_working_directory(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + builders = load_pyg_baselines() + assert set(builders) == {"afgcn", "gcn", "gat", "graphsage", "gin", "randalign"} + assert all(isinstance(build(16, 2), torch.nn.Module) for build in builders.values()) diff --git a/tests/test_setup_cpu.py b/tests/test_setup_cpu.py new file mode 100644 index 0000000..8262f95 --- /dev/null +++ b/tests/test_setup_cpu.py @@ -0,0 +1,18 @@ +from scripts import setup_cpu + + +def test_setup_preserves_data_and_requirements(tmp_path, monkeypatch): + cached = tmp_path / "training_data/features.pkl" + cached.parent.mkdir() + cached.write_bytes(b"research cache") + requirements = tmp_path / "requirements-cpu.txt" + requirements.write_text("example==1.0\n") + created, commands = [], [] + monkeypatch.setattr(setup_cpu, "ROOT", tmp_path) + monkeypatch.setattr(setup_cpu.venv.EnvBuilder, "create", lambda self, path: created.append(path)) + monkeypatch.setattr(setup_cpu.subprocess, "run", lambda cmd, **kw: commands.append((cmd, kw))) + python = setup_cpu.create_environment(tmp_path / ".venv") + assert created == [(tmp_path / ".venv").resolve()] + assert commands == [([str(python), "-m", "pip", "install", "-r", str(requirements)], {"check": True})] + assert cached.read_bytes() == b"research cache" + assert requirements.read_text() == "example==1.0\n" diff --git a/train_consolidated.py b/train_consolidated.py index 413a82e..36f7de6 100644 --- a/train_consolidated.py +++ b/train_consolidated.py @@ -13,6 +13,7 @@ # based on classification performance (MCC). # ────────────────────────────────────────────────────────────── import argparse, importlib.util, math, numpy as np, time +from pathlib import Path from tqdm import tqdm from sklearn.metrics import matthews_corrcoef @@ -144,7 +145,7 @@ def forward(self, data): # ╭──────────────────── load baseline builders from pyg_train.py ─────────────╮ def load_pyg_baselines(): - spec = importlib.util.spec_from_file_location("pyg_baselines", "./pyg_train.py") + spec = importlib.util.spec_from_file_location("pyg_baselines", Path(__file__).with_name("pyg_train.py")) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod)