Skip to content

Semantic-Swingers at LLMs4OL 2026 (Tasks A, B & C): native learner integration#338

Open
matias-vizcaino wants to merge 23 commits into
sciknoworg:devfrom
matias-vizcaino:feat/semanticswingers-llms4ol2026
Open

Semantic-Swingers at LLMs4OL 2026 (Tasks A, B & C): native learner integration#338
matias-vizcaino wants to merge 23 commits into
sciknoworg:devfrom
matias-vizcaino:feat/semanticswingers-llms4ol2026

Conversation

@matias-vizcaino

@matias-vizcaino matias-vizcaino commented Jul 26, 2026

Copy link
Copy Markdown

Summary

This PR contributes the Semantic-Swingers team learners built for the LLMs4OL 2026 challenge (Tasks A, B, and C) as native AutoLearner subclasses, integrated end-to-end through LearnerPipeline and the existing native metrics. It is the mandatory System-Integration deliverable for our participation.

Each learner subclasses AutoLearner and implements a single task hook (_taxonomy_discovery / _term_typing / _text2onto), plugs in via LearnerPipeline, and ships with docs, examples, and unit tests — no changes to OntoLearner core.

What's included

  • Learners (ontolearner/learner/)
    • taxonomy_discovery/semanticswingers.py — Task C (taxonomy discovery). Includes both our primary LLM pipeline (SemanticSwingersTaxonomyLearner) and a scalable alternative (SemanticSwingersMatrixTaxonomyLearner) that uses a custom 1024-D Bilinear Adjacency Layer coupled with NetworkX DAG reduction to bypass LLM inference bottlenecks on massive ontologies.
    • term_typing/semanticswingers.py — Task B (term typing)
    • text2onto/semanticswingers.py (+ semanticswingers_train.py) — Task A (text-to-onto, flagship)
    • Registered in each package's __init__.py
  • Backends: peft (CUDA), mlx (Apple Silicon), ollama, and openai selectors, so the learners run on paid or fully-open/offline stacks.
  • Docs: docs/source/learners/llms4ol_challenge/semanticswingers_learner.rst
  • Examples: examples/llm_learner_semanticswingers_{taxonomy_discovery,term_typing,text2onto,extend}.py. (The Task C example is a unified script demonstrating both the baseline LLM and the alternative Matrix pipelines side-by-side.)
  • Tests: tests/test_semanticswingers_{taxonomy_discovery,term_typing,text2onto}.py (63 tests, all mocked — no network/model download needed)

Trained weights (Hugging Face Hub, public)

The Task A learner loads LoRA adapters (Qwen3.5-9B) and the Task C alternative pipeline loads a custom PyTorch structural matrix — all hosted publicly on the datagero registry. Weights auto-download on first load() when no local copy is present, so the learners run from a fresh clone with no manual setup:

  • datagero/qwen3.5-9b-ontology-extraction-raft — Task A, retrieval-aware FT (k=10), CUDA/peft
  • datagero/qwen3.5-9b-ontology-extraction-baseft — Task A, base FT (k=0), CUDA/peft
  • datagero/qwen3.5-9b-ontology-extraction-baseft-mlx — Task A, base FT, Apple-Silicon/MLX
  • datagero/taxonomy-structural-matrix-1024-mxbai — Task C, 1024-D Bilinear Adjacency Layer adapter (mxbai-embed-large-v1)

Note: Qwen3.5 uses the Qwen3-Next hybrid (linear/full) attention architecture; the Task A adapters require a transformers build that registers qwen3_5 (documented in the learner + example). Task B runs on a standard embedder + LLM selector, while Task C additionally supports routing through our custom structural matrix layer.

Methodological diagram & reproduction

Per the challenge's recommendation, supplementary material lives on the companion branch companion/semanticswingers-llms4ol2026:

This material is reference-only and is not part of this PR.

Quality gates

  • ruff check (v0.8.2, repo-pinned) — clean on all added files
  • pytest tests/test_semanticswingers_*.py63 passed

Future work / discussion (proposal, not part of this PR)

Our Task A learner satisfies the text2onto hook by projecting its extracted is-a triples to the {terms, types} shape the harness scores, and satisfies taxonomy-discovery by delegating to the Task C retriever (the base tasks_data_former strips taxonomy-discovery input to a deduped vocabulary). This works cleanly and additively, but it under-uses the richer relational signal our generator produces. For future editions, and only with maintainer buy-in, we'd be glad to offer a follow-up PR proposing:

  1. a document-grounded taxonomy-discovery variant that feeds source documents to a generative learner, and
  2. a triple-preserving text2onto path plus a graph_similarity metric that scores is-a edges directly.

Both are core eval-harness changes (new dispatch arms + metric), so we deliberately keep them out of this integration PR. As a non-intrusive demonstration, _text2onto also emits the raw triples under an extra key that the current harness ignores, so the retained signal is inspectable without changing any scored path.

Notes for review

  • Targets the dev branch per CONTRIBUTING.md.
  • We'll be available during review to adjust naming, docs, or structure to fit OntoLearner conventions.

datagero added 18 commits July 8, 2026 20:43
…ntic-Swingers taxonomy learner [Task C]

- selector="ollama": free local reproduction of the champion selection
  pipeline via Ollama's OpenAI-compatible endpoint (no API key)
- max_tokens constructor param (was hardcoded 32, which silently truncates
  thinking models into empty answers)
- docs page (llms4ol_challenge/semanticswingers_learner.rst) + toctree entry
- validated on Wine through LearnerPipeline: gpt-4.1-mini f1=0.21,
  llama3.1:8b f1=0.07, embedding baseline f1=0.07
- closed-vocabulary term typing: fit learns the type inventory from the
  train split; inference assigns types from that inventory only
- three selectors mirroring the Task C learner: embedding (offline
  nearest-type), openai champion (gpt-4.1-mini), ollama local
- registered in learner/term_typing; validated offline on Wine via
  LearnerPipeline (embedding selector f1=0.39)
…r both tasks

Wine micro-probe via native LearnerPipeline (term-typing pair F1):
embedding 0.39, ollama llama3.1:8b 0.69, openai gpt-4.1-mini 0.69 —
the local selector matches the champion on this task.
Hermetic unit tests for SemanticSwingersTermTypingLearner and
SemanticSwingersTaxonomyLearner (Tasks B/C): fit/predict contract,
embedding-selector determinism, closed-vocabulary parsing in
_parse_typings, and openai-selector fallback to the offline embedding
selector when no api_key is available (no network attempted, verified
via mocked OpenAI client). Encoders are mocked with fixed vectors so
tests need no downloaded models, network, or ontology data.

Adds a Reproducibility subsection to the learner docs page documenting
which selector reproduces which reported number and how api_key
resolution/fallback behaves.
…ask A, flagship)

One learner class, two hooks, dispatched on the task string:
- _text2onto: RAG (top-k document exemplars) generation with a LoRA
  fine-tuned Qwen/Qwen3.5-9B (RA-FT/base-FT), the competition champion,
  projecting extracted triples onto OntoLearner's native {terms, types}.
- _taxonomy_discovery: composed (not rewritten) from the existing
  SemanticSwingersTaxonomyLearner, since the native harness for that
  task hands over a bare type vocabulary with no source document text.

qwen3_5/qwen3_next has no released transformers support yet, so the
git-source requirement is a lazy import inside the learner raising an
actionable ImportError with the exact pinned-commit pip install line
(verified 2026-07-09 to both register the architecture and produce
coherent triple extraction with the real PEFT adapter) rather than a
core pyproject.toml dependency.

Adds the docs section, example script, and 19 hermetic unit tests
(mocked LLM/retriever, no network).
Replace the placeholder semanticswingers/* ids with the real private HF
repos (datagero/qwen3.5-9b-raft-taska, datagero/qwen3.5-9b-baseft-taska)
created for yp4 slice 3, now hosting the uploaded PEFT adapters.
… _text2onto output

text2onto_metrics reads only terms/types and silently ignores extra keys, and the
full prediction dict passes through unchanged to run_report['predictions']. Adding
a "triples" key ([doc_id, subject, relation, object] per row) is therefore
non-breaking: native scoring is unchanged, while the ~90% is-a signal the
{terms, types} projection would otherwise discard survives for downstream
inspection. Per IC ruling (ADR-0018 addendum §4, llms4ol-2026 main repo): the
"propose, don't patch core" outcome for the harness-fit discussion, implemented
as the zero-core-cost additive demonstration referenced in the PR's Future-work
section.
…-> working)

The term-typing and taxonomy-discovery LLM selectors called
chat.completions.create() with no reasoning override. qwen3.x served over an
OpenAI-compatible endpoint (Ollama) is a *thinking* model: left on it spends the
entire max_tokens budget on hidden reasoning and returns empty content
(finish_reason="length", content=""), so nothing parses and both selectors score
F1 = 0.

Pass reasoning_effort="none" via extra_body for qwen3* models, mirroring the fix
the competition pipeline already applies for the same qwen+RAG=0 failure. It is a
no-op for non-qwen3 models, so it is always safe to include.

Verified on the Wine benchmark (seed 42, local qwen3.5-nothink:9b), through the
real learner code path:
  term-typing         F1 0.0000 -> 0.6593 (30/33)   [offline embedding: 0.3939]
  taxonomy-discovery  F1 0.0000 -> 0.1429 (1/9)     [offline embedding: 0.0000]

Adds a guard test per learner so the regression cannot return.
The term-typing and taxonomy-discovery learners each take selector= and can be
served by a local Ollama, a paid OpenAI model, or offline embeddings. The Task A
learner had no equivalent: generation was hardwired to transformers + PEFT via
_require_qwen35_transformers -> PeftModel.from_pretrained -> model.generate.

Consequences of that gap: the flagship task was the only one a reviewer could not
run without a CUDA box, and anyone wanting a local run had to subclass the learner
in notebook code — untested, unreviewed, and free to drift from what ships.

Adds backend= ("peft" default, unchanged behaviour; "ollama"; "openai") with
_OLLAMA_BASE_URL / _DEFAULT_LLM constants mirroring the other two learners. API
backends short-circuit load() (no local weights, no qwen3_5 guard) and generate via
_generate_api. _chat_messages renders the exemplars as structured turns carrying the
same content as the ChatML prompt, so backends stay comparable. Reuses the
reasoning_effort="none" guard so qwen3.x thinking models answer instead of returning
empty.

Verified: backend="ollama" with qwen3.5-nothink:9b on val_20 produces triples with no
wrapper (gs_v2_exact 0.3197 on a 3-doc probe, ~17 s/doc). Adds 3 tests.

Pre-existing failures in this suite are unrelated to this change (peft not installed
in the dev venv; the qwen3_5 import-guard test cannot trigger now that released
transformers>=5.14 registers the architecture).
Two additions that close the "inference-only" gap and complete the backend matrix,
so the learner can both TRAIN and SERVE across CUDA / Apple Silicon / API / base.

Training — fit() can now produce the adapter, not just consume it:
  train_mode = "raft"   retrieval-aware; top-k exemplars baked into each prompt with
                        LEAVE-ONE-OUT retrieval (a doc never sees its own gold)
             = "baseft" no exemplars (k=0)
             = None     unchanged: fit() only indexes exemplars (default)
  train_backend = "peft" (CUDA LoRA SFT) | "mlx" (Apple Silicon, mlx_lm.lora)
  output_dir    where the adapter is written; fit() then loads it for inference

New semanticswingers_train.py keeps the two rails that break SILENTLY as pure-Python,
unit-tested functions — prompt masking (loss on completion tokens only, else the model
learns to reproduce documents) and leave-one-out (else train/eval leakage inflates the
score). The heavy trainers (_train_peft / _train_mlx) import lazily, so the module
imports with neither stack installed. Ported from the team's ft_nvidia.py (CUDA) and
the mlx_lm LoRA path.

Inference — backend="mlx" loads an MLX base + optional MLX-format LoRA and generates
via mlx_lm, reusing the same ChatML prompt as the PEFT path. Verified on an M4 Max:
backend="mlx" + local MLX adapter, no wrapper, gs_v2_exact 0.3746 on a 3-doc val_20
probe at ~12 s/doc — the fine-tuned champion running on a laptop.

Adds 7 tests (prompt masking incl. left-truncation, leave-one-out, baseft-no-exemplars,
backend defaults, unknown-backend guard). Pre-existing suite failures are unrelated
(peft not installed in the dev venv; the qwen3_5 import-guard test cannot trigger now
that released transformers>=5.14 registers the architecture).
The mlx trainer used a train()/dataset binding that doesn't match mlx_lm's tuner API
(train() takes no tokenizer; this build's CompletionsDataset.__getitem__ returns the
raw record, not processed tokens). Corrects both, plus the LoRA-layer wiring.

Marks _train_mlx honestly: it drives mlx_lm's LOW-LEVEL tuner API, which is not stable
across releases, and is NOT yet validated end-to-end here — the config-key contract
still varies by version. The validated pieces are unchanged: the pure-Python data
rails (leave-one-out, prompt masking) with unit tests, and the backend="mlx"
inference path (gs_v2_exact 0.3746 on-Mac). Recommends the `mlx_lm lora` CLI for a
robust run until the low-level binding is pinned. No claim that the mlx trainer runs.
The prior mlx trainer bound mlx_lm's low-level tuner API (train()/CompletionsDataset/
linear_to_lora_layers), which shifts between mlx_lm releases — three separate API
mismatches surfaced while validating it. Replace that with a subprocess call to
`python -m mlx_lm lora --train`, the stable CLI and the exact command the team's
ft35_overnight.py used to produce the real MLX adapters. Robust across versions, no
low-level loop to keep in sync. Reads the same {prompt, completion} jsonl (mask_prompt
handled by the CLI), writes adapters.safetensors + adapter_config.json for
backend="mlx" inference. Data rails (leave-one-out, prompt masking) and their tests
are unchanged.
…po-id loading

Updates _ADAPTER_REPOS to the published, generically-named registry
(datagero/qwen3.5-9b-ontology-extraction-{raft,baseft,baseft-mlx}) and adds the MLX
alias. So adapter="raft"/"baseft"/"baseft-mlx" now resolve to real, uploaded repos.

backend="mlx" can now load an adapter given a HF repo id: mlx_lm.load(adapter_path=)
only accepts a LOCAL directory (unlike PEFT's from_pretrained, which fetches repo ids),
so we snapshot_download the adapter first when it isn't an on-disk path. Verified
end-to-end against the private MLX repo on Apple Silicon: adapter="baseft-mlx" pulls
the adapter, loads base+adapter, and generates triples (6 on a val_20 doc, ~10s load).

Updates the alias test for the new names incl. baseft-mlx.
… others

The learners baked our specific choices in as module constants, forcing anyone
adapting them to a different domain to edit the package or subclass. Expose the two
that matter as constructor arguments, defaulting to the team's values so existing
behaviour is byte-identical:

  Task A: system_prompt=, typing_relations=
  Task B: system_prompt=

Everything else was already configuration-driven (model, backend, adapter, retriever,
training regime). Adds examples/llm_learner_semanticswingers_extend.py demonstrating
the three levels of customization — arguments only (model/backend/retriever), domain
(prompt + relations), and behaviour (subclass one method) — plus a training snippet,
and documents the extension points on the class docstring.

Backward-compatible; 5 new tests. Pre-existing suite failures unchanged (peft not in
the dev venv; the qwen3_5 import-guard test can't trigger on released transformers).
…rompt injectable

sciknoworg#2 (generalize the local prompt to any model family): _build_prompt now renders the
same message list through the loaded tokenizer's own chat template
(apply_chat_template) instead of hardcoded Qwen ChatML, so a non-Qwen local base model
works on the peft/mlx path (the ollama/openai backends were already model-agnostic via
role messages). VERIFIED byte-identical to the old hand-built ChatML for
Qwen/Qwen3.5-9B — enable_thinking=False reproduces the <think></think> no-think suffix
exactly — so the team's trained adapters see the same prompt. Falls back to the manual
ChatML when no tokenizer/template is present. Confirmed end-to-end: mlx adapter still
generates triples through the new path.

sciknoworg#3 (Task C prompt extensibility): the parent-selection instruction is now a
selection_prompt= constructor arg (a {child}/{candidates} template, default unchanged),
matching the system_prompt/typing_relations extension points on Task A/B.

Backward-compatible; +1 test. Pre-existing suite failures unchanged (peft absent in the
dev venv; qwen3_5 import-guard can't trigger on released transformers).
… 9B on CPU

The peft backend called from_pretrained without device_map/.to, so device='cuda'
was ignored and the base+adapter loaded on CPU — correct output but minutes/doc
(GPU 0% util, ~390MiB) on a CUDA box, unusable at scale. Load weights directly
onto the requested accelerator via device_map; CPU stays default when device=cpu.
Surfaced by the notebook §5 RunPod CUDA validation (2026-07-24).
datagero and others added 5 commits July 26, 2026 19:13
…ort & tests

- SemanticSwingersMatrixTaxonomyLearner.load() now fetches the trained
  structural matrix from the public HF registry
  (datagero/taxonomy-structural-matrix-1024-mxbai) when no local copy exists,
  so the champion runs from a fresh clone (was: silent no-op if file missing).
- Fix broken import: the module imported 'utils' (absent in sentence_transformers
  5.x -> ImportError on import) while the Matrix path actually used 'util.cos_sim'
  (NameError at runtime) -> import 'util'; the champion path was never runnable.
- Add 4 unit tests for the Matrix learner (bilinear scoring, DAG cleanup,
  HF-download fallback, default registry id); ruff clean, 63 tests pass.
- Document the structural-matrix champion variant in the learner docs page.

Co-authored-by: andy-symonds <andrew.symonds@thoughtworks.com>
…ve, not champion

Aligns the docs with the paper + team framing: the primary Task C pipeline is
the LLM selector (SemanticSwingersTaxonomyLearner); the structural-matrix learner
is a scale-aware alternative for very large ontologies, not the champion.
…lternative

Align the class docstring (and the HF-repo comment) with the paper + PR framing:
the matrix learner is a scale-aware alternative to the primary LLM pipeline, not
the champion.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants