Qwen3.5-4B-stance-detection
Qwen/Qwen3.5-4B fine-tuned for three-way
stance detection: given a document and a target, predict whether the author is
favor, against, or neutral toward that target.
Trained with LoRA on a sequence-classification head (the adapter is merged into these weights), as part of the StanceMining pipeline. It is the Qwen3.5 successor to bendavidsteel/Qwen3-4B-stance-detection.
Model details
| Base model | Qwen/Qwen3.5-4B |
| Architecture | Qwen3_5ForSequenceClassification (hybrid linear/full attention, 32 layers) |
| Parameters | 4.21 B |
| Precision | bfloat16 |
| Classification head | score.weight, shape [3, 2560] |
| Max position embeddings | 262144 (trained and evaluated at 2048) |
| Labels | 0: neutral, 1: favor, 2: against |
Usage
The classification head pools the final non-padding token, so inputs must be formatted with the chat template and a generation prompt — the same way the model was trained.
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
repo = "bendavidsteel/Qwen3.5-4B-stance-detection"
tokenizer = AutoTokenizer.from_pretrained(repo)
model = AutoModelForSequenceClassification.from_pretrained(repo, dtype=torch.bfloat16, device_map="auto")
model.eval()
PROMPT = (
"For the following text, determine whether the author's stance is in favor of, "
"against, or neutral toward the target: '{target}'. Consider the language used, "
"any explicit statements of position, and contextual clues that suggest the "
"author's stance. Answer only with 'in favor', 'against', or 'neutral'. \n\n"
"Text: '{text}'"
)
def classify(text, target):
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": PROMPT.format(target=target, text=text)},
]
inputs = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
enable_thinking=False,
truncation=True,
max_length=2048,
return_tensors="pt",
return_dict=True,
).to(model.device)
with torch.no_grad():
logits = model(**inputs).logits[0]
probs = logits.softmax(-1)
return model.config.id2label[int(probs.argmax())], probs.tolist()
print(classify("Cycling infrastructure has made my commute so much safer.", "bike lanes"))
Prompt templates
The prompt is part of the model's interface — the fine-tune only ever saw inputs in
these two forms. Both are also stored in metadata.json in this repo.
Standalone document:
For the following text, determine whether the author's stance is in favor of, against, or neutral toward the target: '{target}'. Consider the language used, any explicit statements of position, and contextual clues that suggest the author's stance. Answer only with 'in favor', 'against', or 'neutral'.
Text: '{text}'
Document in a reply chain (used for conversational datasets such as MT-CSD and CTSDT):
For the following text, determine whether the author's stance is in favor of, against, or neutral toward the target: '{target}'. Consider the language used, any explicit statements of position, the chain of parent texts that the author is replying to, and contextual clues that suggest the author's attitude. Answer only with 'in favor', 'against', or 'neutral'.
Parent Document Chain (from oldest to most recent):
{parent_chain}
Text: '{text}'
Training data
Fine-tuned on the combined training splits of eight stance datasets:
| Dataset | Domain | Language |
|---|---|---|
| VAST | Varied, zero-shot topics | en |
| EZ-STANCE | Varied, zero/few-shot targets | en |
| P-Stance | US political figures | en |
| SemEval-2016 Task 6 | Mixed political/social targets | en |
| MT-CSD | Multi-turn conversational threads | en |
| CTSDT | COVID-19 vaccination, conversation threads | en |
| Catalonia | Catalonia independence | es, ca |
| French-Election | Le Pen, Macron, Italian referendum | fr, it |
Training procedure
LoRA adapters on a randomly initialised sequence-classification head, merged into
the base weights after training. Hyperparameters from the
wandb run autumn-moon-345:
| LoRA rank / alpha / dropout | 4 / 8 / 0.2 |
| Learning rate | 1e-4 |
| Per-device batch size | 8 |
| Gradient accumulation steps | 16 (effective batch 128) |
| Max sequence length | 2048 (padded) |
| Precision | bfloat16, no quantization |
| Attention | flash_attention_2 |
| System message | You are a helpful assistant. |
Training ran as three consecutive jobs — an initial run plus two resumptions after crashes — totalling roughly 69 GPU-hours. Final held-out validation loss was 0.575.
Evaluation
Held-out test split of the combined corpus (16,972 examples).
Overall
| Metric | Value |
|---|---|
| Accuracy | 0.7403 |
| Macro F1 | 0.7399 |
| Macro precision | 0.7386 |
| Macro recall | 0.7436 |
Per class (derived from the confusion matrix in metadata.json)
| Class | Precision | Recall | F1 | Support |
|---|---|---|---|---|
| neutral | 0.7049 | 0.7791 | 0.7401 | 4762 |
| favor | 0.7196 | 0.7329 | 0.7262 | 5612 |
| against | 0.7913 | 0.7187 | 0.7532 | 6598 |
Per dataset
| Dataset | Accuracy | Macro F1 | Precision | Recall |
|---|---|---|---|---|
| VAST | 0.7828 | 0.7787 | 0.7782 | 0.7797 |
| EZ-STANCE | 0.6605 | 0.6654 | 0.6770 | 0.6602 |
| P-Stance | 0.8507 | 0.8502 | 0.8510 | 0.8498 |
| SemEval | 0.7086 | 0.6932 | 0.7017 | 0.7230 |
| MT-CSD | 0.6706 | 0.6289 | 0.6620 | 0.6131 |
| CTSDT | 0.8029 | 0.7520 | 0.7936 | 0.7243 |
| Catalonia | 0.7506 | 0.7533 | 0.7540 | 0.7533 |
| French-Election | 0.7716 | 0.7043 | 0.6837 | 0.8159 |
Confusion matrix (rows are true labels, columns predicted)
| neutral | favor | against | |
|---|---|---|---|
| neutral | 3710 | 577 | 475 |
| favor | 723 | 4113 | 776 |
| against | 830 | 1026 | 4742 |
Comparison with the Qwen3 generation
Same training and test splits, same prompts.
| Model | Accuracy | Macro F1 |
|---|---|---|
| Qwen3-1.7B-stance-detection | 0.7182 | 0.7183 |
| Qwen3-4B-stance-detection | 0.7353 | 0.7350 |
| Qwen3.5-4B-stance-detection (this model) | 0.7403 | 0.7399 |
Limitations
- The label set is closed and three-way; targets outside the training domains, and stance expressed through heavy irony or in-group reference, remain hard.
- Coverage outside English is limited to Catalan/Spanish independence tweets and French/Italian election tweets, so non-English performance should not be assumed to generalise to other topics.
neutralabsorbs both "no stance" and "discusses the target without taking a side", which differ across the source datasets.- Inputs longer than 2048 tokens were truncated during training and evaluation, despite the larger context window of the base model.
License
Apache 2.0, following the base model.