Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Shell entry points must retain Unix line endings on Windows.
*.sh text eol=lf
17 changes: 17 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -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
112 changes: 56 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).
32 changes: 32 additions & 0 deletions examples/cpu_demo.py
Original file line number Diff line number Diff line change
@@ -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))
5 changes: 5 additions & 0 deletions requirements-cpu.txt
Original file line number Diff line number Diff line change
@@ -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
16 changes: 8 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions scripts/setup_cpu.py
Original file line number Diff line number Diff line change
@@ -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()
43 changes: 5 additions & 38 deletions setup_environment.sh
Original file line number Diff line number Diff line change
@@ -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. ---"
#!/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" "$@"
22 changes: 22 additions & 0 deletions tests/test_demo_runtime.py
Original file line number Diff line number Diff line change
@@ -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())
18 changes: 18 additions & 0 deletions tests/test_setup_cpu.py
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 2 additions & 1 deletion train_consolidated.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
Loading