Assessing Reference Support for IPBES Background Messages via NLI
Technical Design Document
Assessing Reference Support for IPBES Background Messages via NLI
Overview
This document describes a pipeline for systematically classifying whether scientific references support, contradict, or are not relevant to a given IPBES background message (BM). The approach uses Natural Language Inference (NLI) — a text classification task where a model determines the logical relationship between a premise (here: an abstract) and a hypothesis (here: a background message).
The pipeline deliberately avoids a retrieval/filtering stage. Instead, NLI is run across the complete reference set, using the model’s own confidence scores as the filter. This eliminates retrieval recall risk and produces a fully auditable, reproducible result with no embedding hyperparameters to justify.
Why NLI Without Prior Retrieval?
A natural first instinct is to pre-filter references using semantic similarity (e.g. SPECTER2 embeddings), then run NLI only on the top-k candidates. This is computationally attractive but has a critical weakness: false negatives at retrieval stage are silent.
A paper may be highly relevant to a BM without being semantically close in embedding space. IPBES background messages are synthetic, policy-oriented claims — they do not resemble any individual abstract. A study on soil carbon flux in Amazonia may directly support a BM about terrestrial carbon sinks, but the vocabulary overlap is low and the paper would likely not rank in the top-k.
Running NLI on the full set avoids this entirely. With GPU access the compute is manageable (see numbers below), and the pipeline becomes a single auditable step.
The NLI Task
NLI models take a premise–hypothesis pair and return a probability distribution over three classes:
| Class | Meaning in this context |
|---|---|
SUPPORTS |
The abstract provides evidence consistent with the BM |
REFUTES |
The abstract provides evidence against the BM |
NOT_ENOUGH_INFO |
The abstract does not address the BM |
The claim–abstract framing maps directly onto the SciFact benchmark (Wadden et al., 2020), which is precisely: given a scientific claim and an abstract, classify as SUPPORTS / REFUTES / NOT_ENOUGH_INFO. Models fine-tuned on SciFact are the natural starting point.
Recommended Model
MoritzLaurer/deberta-v3-large-zeroshot-v2.0
DeBERTa-v3-large is consistently among the strongest NLI models across benchmarks. Laurer’s version is fine-tuned on a large cross-domain NLI corpus and performs well in zero-shot settings on new domains — relevant here because biodiversity/IPBES language differs from biomedical or computer science text that many SciFact models are trained on.
Alternatives worth benchmarking:
| Model | Notes |
|---|---|
MoritzLaurer/deberta-v3-large-zeroshot-v2.0 |
Recommended. Strong cross-domain NLI |
allenai/scibert_scivocab_uncased fine-tuned on SciFact |
Science-specific vocabulary, shorter context |
facebook/bart-large-mnli |
General NLI, zero-shot, weaker on scientific text |
pritamdeka/S-PubMedBert-MS-MARCO-SciFact |
Biomedical focus, likely suboptimal for ecology |
For IPBES specifically, validate model choice by manually labelling 20–30 BM–abstract pairs and checking model agreement before committing to a full run.
Context Length Considerations
DeBERTa-v3-large has a maximum context of 512 tokens (claim + abstract combined). Most abstracts are 150–250 words (~200–330 tokens). A typical IPBES BM is 30–80 words (~40–110 tokens). This leaves comfortable headroom in most cases.
Action: Check your abstract length distribution before running:
library(dplyr)
library(tokenizers)
refs |>
mutate(n_tokens = map_int(abstract, ~ length(tokenize_words(.x)[[1]]))) |>
summarise(
median_tokens = median(n_tokens),
p95_tokens = quantile(n_tokens, 0.95),
n_over_400 = sum(n_tokens > 400)
)For abstracts exceeding ~400 tokens, truncate from the end (the claim is typically stated early). Do not truncate mid-sentence.
Claim Granularity: sub_bm vs. complete_bm
The implemented pipeline (R/build_nli_ready_evidence_parquet.R) doesn’t score a whole BM against a paper in one pair — it first cuts each BM’s bm_description/bm_label into smaller claims. input/config.yaml’s per-NLI-config granularity field picks how:
sub_bm(default, the active approach) —segment_bm_by_evidence()splits at brace evidence-references ({5.4.1, 5.4.2}) that end a sentence, producing 2-17 claims per BM. Each claim is short (measured: 62-86 tokens average, max 229-328), comfortably inside the 512-token headroom discussed above.complete_bm—segment_bm_whole()takes each field whole, unsegmented: 2 claims per BM (bm_description+bm_label), or 1 if the two are identical text after whitespace normalization (checked against all 88 declared BMs across both assessments: currently 0 are identical, so this collapses nothing today, but the rule exists for correctness).
Measured claim/pair reduction, from the real on-disk sub_bm data:
| Assessment | BMs | sub_bm claims (mean/BM) |
sub_bm (claim,work) pairs |
complete_bm claims |
Projected pairs | Reduction |
|---|---|---|---|---|---|---|
| GA1 | 30 | 171 (5.7) | 1,887,289 | 60 | ~662,382 | ~65% |
| IAS | 49 | 429 (8.76) | 1,695,040 | 98 | ~387,190 | ~77% |
| Combined | 79 | 600 | 3,582,329 | 158 | ~1,049,572 | ~71% |
Phase 2 (LLM verification) volume would shrink by roughly the same factor if the REFUTES/SUPPORTS-certain rate per pair holds under the much longer hypothesis text complete_bm produces — genuinely uncertain, not a rounding caveat (see below).
Why complete_bm isn’t just a free win — the token-limit problem is worse than the “headroom” framing above suggests. A whole bm_description/bm_label averages ~245-271 tokens (median 120-155, up to 733-885 at the extreme) — 3-10x longer than a sub_bm claim. max_length: 512 in input/config.yaml looks like a conservative, raisable setting; it is not. Fetched MoritzLaurer/deberta-v3-large-zeroshot-v2.0’s real config.json directly from Hugging Face and confirmed max_position_embeddings: 512 — a hard architectural limit of the model itself (its position-embedding table has exactly 512 slots), already matching input/config.yaml’s setting exactly. external/runpod/docker/nli-runpod/server.py’s tokenizer call truncates the combined premise+hypothesis pair (truncation="longest_first"), so for any BM whose complete_bm claim already exceeds ~512 tokens on its own, the hypothesis itself gets cut — not just the premise’s tail, which is the only thing that ever truncates under sub_bm. Treat complete_bm results for the largest BMs with real skepticism until truncation is actually measured against a run, not as a minor footnote.
The mitigation is a different model, not a bigger max_length. input/config.yaml ships a second config, bge_m3_zeroshot (MoritzLaurer/bge-m3-zeroshot-v2.0-c, max_position_embeddings: 8194 — also confirmed against its real config.json), meant to pair with granularity: complete_bm. It needed to be multilingual, not just long-context: the hypothesis (BM claim) is always English, but the premise (citing-paper title+abstract, from OpenAlex) can be in whatever language the source paper was published in, so an English-only long-context alternative (MoritzLaurer/ModernBERT-large-zeroshot-v2.0, also 8192 tokens, considered first) would still fail on non-English premises. bge_m3_zeroshot and the currently-deployed deberta_zeroshot use the same 2-class entailment/not_entailment head (both real config.jsons checked) — MoritzLaurer’s “v2.0” series reformulates zero-shot as one entailment-vs-not_entailment pass per candidate label, not classic 3-way MNLI — so server.py’s already-dynamic _entailment_id() (derived from _model.config.label2id, not hardcoded) needs no changes to serve either model; only a different NLI_MODEL build arg (external/runpod/docker/nli-runpod/Dockerfile) and a new image tag, ghcr.io/rkrug/nli-runpod-bge-m3 (see input/nli_pods_bge_m3.conf).
Not yet built, provisioned, or run — bge_m3_zeroshot’s uncertain_threshold: 0.60 is carried over unverified from deberta_zeroshot as a starting point, not a calibrated value for this different model. See TODO.md.
Compute Estimates
Throughput by hardware
| Setup | Throughput | Basis |
|---|---|---|
| RunPod L4 GPU (24 GB) | 50–100 pairs/sec | DeBERTa-large, batch size 32 |
| RunPod A100 (80 GB) | 150–250 pairs/sec | Larger batch sizes |
| HuggingFace Inference API | 5–10 pairs/sec | Free tier, shared inference |
| CPU only (local) | 1–2 pairs/sec | Not recommended at scale |
Time estimates for common scales
Assuming 50 background messages and varying reference set sizes, at 75 pairs/sec (L4 GPU, conservative midpoint):
| References | Total pairs | L4 GPU | HF API |
|---|---|---|---|
| 500 | 25,000 | ~6 min | ~45 min |
| 2,000 | 100,000 | ~22 min | ~3 hrs |
| 5,000 | 250,000 | ~56 min | ~7 hrs |
| 10,000 | 500,000 | ~1.9 hrs | ~14 hrs |
For the typical IPBES assessment reference set (500–3,000 papers), a single L4 GPU run of 20–60 minutes covers the full pipeline. This makes retrieval pre-filtering unnecessary.
Pipeline Design
Inputs
background_messages: a data frame with columnsbm_id,bm_textreferences: a data frame with columnsref_id,title,abstract
Steps
1. Preprocess
├── Concatenate title + abstract for each reference (title adds context)
├── Truncate to 400 tokens if needed
└── Cross-join BMs × references → pairs data frame
2. NLI inference (batched)
├── Send batches of pairs to model
├── Receive probability scores for [SUPPORTS, REFUTES, NOT_ENOUGH_INFO]
└── Store raw scores alongside predicted label
3. Post-filter
├── Drop pairs where p(NOT_ENOUGH_INFO) > 0.90 (configurable threshold)
└── Flag pairs where max(p) < 0.60 as "uncertain" for human review
4. Output
├── Full results table (all pairs, raw scores)
├── Filtered table (SUPPORTS / REFUTES only, above threshold)
└── Per-BM summary: n_supporting, n_contradicting, n_uncertain
Output schema
# One row per BM–reference pair
tibble(
bm_id = character(), # Background message identifier
ref_id = character(), # Reference identifier (DOI or internal ID)
label = character(), # "SUPPORTS" | "REFUTES" | "NOT_ENOUGH_INFO"
p_supports = double(), # Model probability for SUPPORTS
p_refutes = double(), # Model probability for REFUTES
p_nei = double(), # Model probability for NOT_ENOUGH_INFO
confidence = double(), # max(p_supports, p_refutes, p_nei)
uncertain = logical() # TRUE if confidence < threshold
)R Implementation Outline
Option A — HuggingFace Inference API (prototyping)
library(httr2)
library(purrr)
library(dplyr)
classify_nli <- function(premise, hypothesis,
model = "MoritzLaurer/deberta-v3-large-zeroshot-v2.0",
hf_token = Sys.getenv("HF_TOKEN")) {
resp <- request("https://api-inference.huggingface.co/models") |>
req_url_path_append(model) |>
req_auth_bearer_token(hf_token) |>
req_body_json(list(
inputs = list(premise = premise, hypothesis = hypothesis)
)) |>
req_retry(max_tries = 3, backoff = ~ 5) |>
req_perform() |>
resp_body_json()
# Response is a list of lists: [[label, score], ...]
scores <- resp[[1]] |>
map_dfr(~ tibble(label = .x$label, score = .x$score))
scores
}
# Apply across all pairs (rate-limit aware)
results <- pairs |>
mutate(
nli = map2(abstract_text, bm_text, classify_nli, .progress = TRUE)
) |>
unnest(nli)Option B — Local/RunPod inference via reticulate (production)
library(reticulate)
# Python environment with transformers + torch
transformers <- import("transformers")
torch <- import("torch")
pipe <- transformers$pipeline(
"zero-shot-classification",
model = "MoritzLaurer/deberta-v3-large-zeroshot-v2.0",
device = 0L # GPU device index; -1 for CPU
)
classify_batch <- function(premises, hypothesis,
candidate_labels = c("supports", "refutes", "not relevant"),
batch_size = 32L) {
pipe(
premises,
candidate_labels = candidate_labels,
hypothesis_template = paste("This paper", "{}", "the following claim:", hypothesis),
batch_size = batch_size
)
}Note: the hypothesis_template is important for zero-shot NLI — it frames the classification correctly relative to the candidate labels.
Thresholds and Human Review
The model returns a probability distribution, not a binary decision. Choose thresholds based on your downstream use:
| Threshold | Recommendation |
|---|---|
p(NOT_ENOUGH_INFO) > 0.90 |
Discard as not relevant |
confidence < 0.60 |
Flag as uncertain, queue for human review |
p(SUPPORTS) > 0.75 |
High-confidence support |
p(REFUTES) > 0.75 |
High-confidence contradiction — always human-reviewed |
Contradictions deserve special attention: a high-confidence REFUTES classification is scientifically significant and should never be accepted without expert review.
Limitations
- BMs are synthetic claims. They aggregate evidence from multiple papers. No single paper may directly assert a BM; the model may undercount support as a result.
- Partial support is not modelled. A paper may address one aspect of a multi-part BM. Consider splitting complex BMs into atomic sub-claims before classification.
- Domain shift. Models trained on biomedical SciFact may underperform on ecology and biodiversity language. Benchmark on a hand-labelled sample first.
- Abstract-only coverage. Full-text classification would require chunking and aggregation across sections — feasible but adds complexity.
- 512-token limit. Long abstracts must be truncated, with possible loss of relevant detail.
Recommended Workflow
- Sample and label 30–50 BM–abstract pairs manually (covering all three classes)
- Benchmark two or three candidate models on this sample; pick the best
- Run full pipeline on RunPod L4 with chosen model, batches of 32
- Apply thresholds to produce filtered results table
- Human expert review of all
REFUTEScalls and alluncertaincases - Summarise per BM: n_supporting, n_contradicting, n_uncertain, n_not_relevant
See also
- https://towardsdatascience.com/natural-language-inference-an-overview-57c0eecf6517/
- https://medium.com/@mllabucu/natural-language-inference-for-fact-checking-on-wikipedia-d3f0825b062f
References
- Wadden, D. et al. (2020). Fact or Fiction: Verifying Scientific Claims. EMNLP 2020. SciFact dataset and baseline models. https://github.com/allenai/scifact
- Laurer, M. et al. (2022). Less Annotating, More Classifying. DeBERTa zero-shot NLI models. https://huggingface.co/MoritzLaurer
- He, P. et al. (2021). DeBERTa: Decoding-enhanced BERT with Disentangled Attention. ICLR 2021.