This repository contains two fine-tuned bi-encoder models for counter-argument retrieval: given an argument, retrieve the most relevant counter-argument from a corpus of debate arguments. Both models are fine-tuned from sentence-transformers/all-mpnet-base-v2 on the curated subset of the CreateDebate Arguments Dataset, using MultipleNegativesRankingLoss with in-batch negatives.
The two models correspond to two different retrieval settings:
Model folder
Setting
Ground truth
Use case
biencoder_scenario1
Targeted retrieval
The author-tagged Disputed reply to a given argument
Find the specific rebuttal that was written in direct response to a claim
biencoder_scenario2
General retrieval
Any argument from the opposing side of the same debate
Find any plausible counter-argument on the same topic, regardless of whether it directly addresses the specific claim
These models were trained and evaluated as part of a paper introducing the CreateDebate Arguments Dataset. See the Limitations section before using these models in any downstream application.
Repository Structure
.
├── biencoder_scenario1/ # targeted retrieval model (sentence-transformers format)
├── biencoder_scenario2/ # general retrieval model (sentence-transformers format)
├── corpus_df.pkl # argument metadata for the evaluation corpus (pandas DataFrame)
└── corpus_embs.pt # pre-computed embeddings for the evaluation corpus
corpus_df.pkl and corpus_embs.pt are provided so the usage example below works without re-encoding the corpus. If you want to retrieve from your own corpus, encode it with the relevant model and skip these two files.
1import textwrap
2import numpy as np
3import pandas as pd
4import torch
5from pathlib import Path
6from huggingface_hub import snapshot_download
7from sentence_transformers import SentenceTransformer
89BOLD, CYAN, GREEN, YELLOW, RESET ="\033[1m","\033[96m","\033[92m","\033[93m","\033[0m"101112defretrieve(13 query:str,14 model: SentenceTransformer,15 corpus_ids:list,16 corpus_embs: np.ndarray,17 corpus_df: pd.DataFrame,18 top_k:int=10,19 exclude_ids:set=None,20)->list:21"""
22 Encode query, score against corpus, return top-k results.
2324 Parameters
25 ----------
26 query : the query argument text
27 exclude_ids : set of argument IDs to exclude from results
28 (e.g. to avoid returning the query itself if it's in the corpus)
29 """30 query_emb = model.encode(31[query],32 normalize_embeddings=True,33 convert_to_numpy=True,34)[0]3536# Score all corpus arguments37 scores = corpus_embs @ query_emb # (N,)3839# Rank descending40 ranked_indices = np.argsort(scores)[::-1]4142# Build result list, skipping excluded IDs43 id_to_row = corpus_df.set_index("argumentId").to_dict("index")44 results =[]4546for idx in ranked_indices:47 arg_id = corpus_ids[idx]4849if exclude_ids and arg_id in exclude_ids:50continue5152 row = id_to_row.get(arg_id,{})53 results.append({54"rank":len(results)+1,55"argumentId": arg_id,56"score":float(scores[idx]),57"argumentBody": row.get("argumentBody",""),58"argumentSide": row.get("argumentSide",""),59"argumentTag": row.get("argumentTag",""),60"debateTitle": row.get("debateTitle",""),61"debateUrl": row.get("debateUrl",""),62"depth": row.get("depth",-1),63"username": row.get("username",""),64})6566iflen(results)>= top_k:67break6869return results
707172defdisplay_results(query:str, results:list, mode:str):73 width =807475print("\n"+"="* width)76print(f"{BOLD}{CYAN} QUERY ARGUMENT{RESET}")77print("="* width)78print(textwrap.fill(query, width=width, initial_indent=" ", subsequent_indent=" "))7980print("\n"+"="* width)81 label ="TARGETED COUNTER-ARGUMENTS (Disputed)"if mode =="targeted" \
82else"GENERAL COUNTER-ARGUMENTS (Side-Based)"83print(f"{BOLD}{GREEN} TOP {len(results)}{label}{RESET}")84print("="* width)8586for res in results:87 tag_str =f" [{res['argumentTag']}]"if res.get("argumentTag")else""88 depth_str =f"depth={res['depth']}"89 score_str =f"score={res['score']:.4f}"9091print(f"\n{BOLD} Rank {res['rank']}{RESET} | {score_str} | {depth_str}{tag_str}")92print(f" {YELLOW}Side:{RESET}{res['argumentSide']}")93print(f" {YELLOW}Debate:{RESET}{res['debateTitle']}")94print()95 body = res["argumentBody"]96 wrapped = textwrap.fill(97 body, width=width -4,98 initial_indent=" ",99 subsequent_indent=" "100)101print(wrapped)102print(" "+"-"*(width -2))103104print()105106107if __name__ =="__main__":108 DEVICE ="cuda"if torch.cuda.is_available()else"cpu"109 TOP_K =10110 MODE ="targeted"# or "general"111 query ="Capital punishment is justified as a deterrent to serious crime."112113# Download the repo (models + corpus files) and cache it locally114 repo_path = Path(snapshot_download(repo_id="azza1625/counter-argument-retrieval"))115116 MODEL_PATHS ={117"targeted": repo_path /"biencoder_scenario1",118"general": repo_path /"biencoder_scenario2",119}120121 model_path = MODEL_PATHS[MODE]122 corpus_df = pd.read_pickle(repo_path /"corpus_df.pkl")123 corpus_embs = torch.load(repo_path /"corpus_embs.pt", weights_only=False)124125print(f"\nLoading {MODE} model from {model_path}...")126 model = SentenceTransformer(str(model_path), device=DEVICE)127128 corpus_ids = corpus_df["argumentId"].tolist()129130 results = retrieve(131 query=query,132 model=model,133 corpus_ids=corpus_ids,134 corpus_embs=corpus_embs,135 corpus_df=corpus_df,136 top_k=TOP_K,137)138139 display_results(query, results, MODE)
To retrieve from your own corpus instead of the bundled one, encode your arguments with the same model and skip corpus_df.pkl / corpus_embs.pt:
python
1corpus_texts =[...]# list of argument strings2corpus_ids =[...]# matching list of IDs34corpus_embs = model.encode(5 corpus_texts,6 batch_size=128,7 normalize_embeddings=True,8 convert_to_numpy=True,9)
Training Details
Base model: sentence-transformers/all-mpnet-base-v2
Epochs: 3, with linear warmup over 10% of training steps
Checkpoint selection: best epoch by MRR@10 on the validation split
Training data: curated subset of the CreateDebate Arguments Dataset (84,872 arguments, 578 debates), split at the debate level (75/10/15) to prevent topic leakage between train, validation, and test
biencoder_scenario1 is trained on targeted query-positive pairs, where the positive is the author-tagged Disputed reply to the query argument (44,569 pairs). biencoder_scenario2 is trained on general query-positive pairs, where the positive is any argument from the opposing side of the same debate (80,885 pairs, capped at 500 per debate).
Evaluation Results
Both models were evaluated on held-out test debates, with candidates restricted to arguments from the same debate as the query.
Scenario 1 (targeted retrieval), evaluated with biencoder_scenario1:
Metric
Score
MRR
0.444
Recall@1
0.301
Recall@5
0.605
Recall@10
0.711
Scenario 2 (general retrieval), evaluated with biencoder_scenario2:
Metric
Score
Recall@10
0.077
Recall@20
0.157
The low Recall@10/20 on Scenario 2 reflects the size of the positive pool (a mean of ~83 valid opposing-side arguments per query), not necessarily a failure of the model relative to other approaches; a BM25 lexical baseline performs comparably on this setting. See the accompanying paper for full baseline comparisons and a discussion of why Scenario 2 is difficult for all evaluated methods.
Querying biencoder_scenario1 does not require debate-level context (title/description) to be included in the query text. In our experiments, adding debate context to the query consistently hurt retrieval performance for both lexical and neural models when all candidates come from the same debate, since it adds shared vocabulary that doesn't help distinguish between candidates. We recommend passing only the argument text itself as the query.
Limitations and Considerations
A qualitative analysis of biencoder_scenario1 identified three recurring failure patterns that are useful to know before relying on these models:
Pragmatic and contextual rebuttals are hard to retrieve. Counter-arguments that operate at a pragmatic level (e.g. asking for clarification, or reframing the premise of an argument rather than directly engaging its content) tend to have low semantic similarity to the query and are often missed, even when they are the most effective real-world response.
Single ground truth labels can be ambiguous. In many cases, several arguments in the candidate pool are equally valid counter-arguments, but only one is the labeled ground truth (the one tagged as a direct reply). A model that retrieves a different, also-valid counter-argument is being scored as wrong. Retrieval metrics on this dataset should be read as a lower bound on true model capability.
The training and evaluation data contains non-substantive exchanges. Because the source dataset is a naturally occurring, unfiltered debate platform, some "Disputed" replies are personal remarks, platform notices, or off-topic dismissals rather than genuine counter-arguments. These cases are unanswerable by any text-only retrieval model and contribute to the error rate.
These models are intended for research on computational argumentation and counter-argument retrieval. They are not intended to be used as a standalone fact-checking, content moderation, or debate-arbitration tool, and outputs should not be treated as an authoritative judgment of which side of an argument is "correct."
License
These models are released under the Creative Commons Attribution 4.0 International (CC BY 4.0) license, matching the license of the underlying training data.