Text Classification
Transformers
Safetensors
Arabic
Stance Detection
Text Classification
arabic-nlp
stanceeval-2026
few-shot-learning
retrieval-augmented
Mawqif-v2
ensemble
LoRA
AraBERT
MARBERT
Instructions to use zaher-m/stanceeval2026 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use zaher-m/stanceeval2026 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="zaher-m/stanceeval2026")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("zaher-m/stanceeval2026", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 12,330 Bytes
7e9cfd1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | """Few-shot stance classification with a chat LLM -- ask it to pick Favor,
Against, or None per (target, tweet), sample it a few times, and turn the
votes into a soft distribution we can blend with the encoders. Few-shot
examples are pulled class-balanced from training, same target preferred.
Uses AUG_BASE_URL / AUG_MODEL like the other LLM scripts.
python -m src.llm_classify --csv data/track2/dev.csv \\
--gold data/track2/dev.csv --train data/track2/train.csv \\
--shots 6 --n 6 --out_probs outputs/llm/t2_dev.npy
"""
import argparse
import json
import os
import re
import urllib.request
from concurrent.futures import ThreadPoolExecutor
import numpy as np
from src.data import (LABEL2ID, TARGET_DESCRIPTIONS,
TARGET_DESCRIPTIONS_RICH, load_split)
from src.scorer import load_gold, score
LABELS = ["Against", "Favor", "None"]
SYSTEM = (
"أنت مصنف موقف عربي دقيق. المطلوب تحديد موقف كاتب التغريدة تجاه "
"الهدف المحدد. الموقف واحد من ثلاثة فقط:\n"
"Favor (مؤيد للهدف)، Against (معارض للهدف)، None (لا موقف واضح).\n"
"أجب بكلمة واحدة فقط من: Favor أو Against أو None."
)
SYSTEM_COT = (
"أنت خبير في تحليل المواقف في التغريدات العربية، وتراعي اللهجات "
"والسخرية والتهكم. حلّل موقف كاتب التغريدة تجاه الهدف المحدد على "
"خطوات موجزة:\n"
"1) المعنى الحرفي للتغريدة.\n"
"2) هل فيها سخرية أو تهكم يقلب المعنى الظاهر؟\n"
"3) الموقف الحقيقي للكاتب تجاه الهدف.\n"
"ثم في آخر سطر اكتب بالضبط: الموقف: يليه إحدى الكلمات "
"Favor أو Against أو None."
)
# For a model with a native thinking mode served with --reasoning-parser:
# the chain-of-thought lands in message.reasoning_content and only the final
# answer in message.content. We just ask for a one-word final answer and let
# the model reason internally about dialect / sarcasm / implicit stance.
SYSTEM_THINK = (
"أنت خبير في تحليل المواقف في التغريدات العربية، تراعي اللهجات الخليجية "
"والمصرية والشامية والسخرية والتهكم والموقف الضمني غير المباشر. "
"فكّر ملياً في المعنى الحقيقي لموقف كاتب التغريدة تجاه الهدف المحدد "
"(انتبه للسخرية التي تقلب الظاهر)، ثم أجب بكلمة واحدة فقط من: "
"Favor أو Against أو None."
)
_SYSTEM = {"direct": SYSTEM, "cot": SYSTEM_COT, "think": SYSTEM_THINK}
def build_pool(train_df):
pool = {}
for tgt, g in train_df.groupby("target"):
pool[tgt] = {lb: g[g["stance"] == lb]["text"].tolist()
for lb in LABELS}
allc = {lb: train_df[train_df["stance"] == lb]["text"].tolist()
for lb in LABELS}
return pool, allc
def pick_shots(target, pool, allc, k, rng):
per = pool.get(target)
shots = []
for i in range(k):
lb = LABELS[i % 3]
src = (per[lb] if per and per[lb] else allc[lb])
if src:
shots.append((rng.choice(src), lb))
rng.shuffle(shots)
return shots
def messages(target, tweet, shots, mode="direct", desc=None):
system = _SYSTEM.get(mode, SYSTEM)
tgt = f"{target} ({desc})" if desc else target
msgs = [{"role": "system", "content": system}]
for text, lb in shots:
msgs.append({"role": "user",
"content": f"الهدف: {tgt}\nالتغريدة: {text}"})
ans = f"الموقف: {lb}" if mode == "cot" else lb
msgs.append({"role": "assistant", "content": ans})
msgs.append({"role": "user",
"content": f"الهدف: {tgt}\nالتغريدة: {tweet}"})
return msgs
def parse_label(text):
t = text.strip().lower()
if "against" in t or "معارض" in t:
return "Against"
if "favor" in t or "مؤيد" in t or "مؤيّد" in t:
return "Favor"
if re.search(r"\bnone\b", t) or "محايد" in t or "لا موقف" in t:
return "None"
return None
def parse_final_label(text):
"""For CoT output: take the label after the last stance marker."""
tail = text
for marker in ("الموقف:", "answer:", "stance:"):
idx = text.lower().rfind(marker)
if idx != -1:
tail = text[idx + len(marker):]
break
return parse_label(tail)
def call(base_url, model, msgs, n, temperature, max_tokens=8, timeout=180,
extra_body=None):
payload = {
"model": model, "n": n, "temperature": temperature,
"top_p": 0.95, "max_tokens": max_tokens, "messages": msgs,
}
if extra_body:
payload.update(extra_body)
body = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
base_url.rstrip("/") + "/chat/completions", data=body,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=timeout) as r:
data = json.load(r)
# With a reasoning parser the answer is in `content`; if the model spent
# all tokens thinking, fall back to `reasoning_content` so the label is
# still recoverable.
outs = []
for c in data["choices"]:
m = c["message"]
outs.append(m.get("content") or m.get("reasoning_content") or "")
return outs
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--csv", required=True)
ap.add_argument("--train", required=True)
ap.add_argument("--gold", default=None)
ap.add_argument("--out_probs", required=True)
ap.add_argument("--shots", type=int, default=6)
ap.add_argument("--n", type=int, default=6)
ap.add_argument("--sets", type=int, default=1,
help="ensemble over this many distinct retrieved shot "
"sets per query (requires --shot_pool_m)")
ap.add_argument("--shot_pool_m", type=int, default=0,
help="sample the shots from the top-M per class")
ap.add_argument("--mode", choices=["direct", "cot", "think"],
default="direct")
ap.add_argument("--think_max_tokens", type=int, default=1024,
help="token budget for native-thinking mode (reasoning "
"+ final one-word answer)")
ap.add_argument("--enable_thinking", choices=["unset", "true", "false"],
default="unset",
help="send chat_template_kwargs.enable_thinking; set "
"'false' for models that default to thinking-on "
"(e.g. Qwen3.6) so direct answers aren't truncated")
ap.add_argument("--temperature", type=float, default=0.7)
ap.add_argument("--concurrency", type=int, default=24)
ap.add_argument("--retrieve", action="store_true")
ap.add_argument("--rerank", action="store_true",
help="rerank retrieved shots with a CrossEncoder")
ap.add_argument("--rerank_model",
default="NAMAA-Space/GATE-Reranker-V1")
ap.add_argument("--use_desc", action="store_true",
help="append a short target description to the prompt")
ap.add_argument("--rich_desc", action="store_true",
help="use the richer target descriptions with --use_desc")
ap.add_argument("--self_exclude", action="store_true",
help="drop a retrieved shot whose text equals the query "
"(leave-one-out for transductive pseudo-label pools)")
ap.add_argument("--embed_model", default="UBC-NLP/MARBERTv2")
ap.add_argument("--embed_url", default=None,
help="OpenAI-compatible /v1 embeddings endpoint; when "
"set, retrieval uses it instead of --embed_model")
ap.add_argument("--embed_api_model", default=None,
help="model id for --embed_url")
ap.add_argument("--embed_instruction", default=None,
help="instruction prepended to queries (Qwen3-Embedding)")
ap.add_argument("--base_url", default=os.environ.get("AUG_BASE_URL", ""))
ap.add_argument("--model", default=os.environ.get("AUG_MODEL", ""))
args = ap.parse_args()
if not args.base_url or not args.model:
raise SystemExit("set --base_url/--model or AUG_BASE_URL/AUG_MODEL")
test = load_split(args.csv, "preserve", has_labels=False)
train = load_split(args.train, "preserve", has_labels=True)
pool, allc = build_pool(train)
rng = np.random.default_rng(0)
seeds = rng.integers(0, 1_000_000, size=len(test))
retriever, q_emb, reranker = None, None, None
if args.retrieve:
import torch
from src.retrieve import Reranker, Retriever
device = "cuda" if torch.cuda.is_available() else "cpu"
retriever = Retriever(train, args.embed_model, device,
embed_url=args.embed_url,
embed_api_model=args.embed_api_model,
instruction=args.embed_instruction)
q_emb = retriever.embed_queries(
test["text"].tolist(), args.embed_model, device
)
if args.rerank:
reranker = Reranker(args.rerank_model, device)
def work(i):
row = test.iloc[i]
if args.mode == "think":
max_tokens, parser = args.think_max_tokens, parse_final_label
elif args.mode == "cot":
max_tokens, parser = args.think_max_tokens, parse_final_label
else:
max_tokens, parser = 8, parse_label
# Some models (e.g. Qwen3.6) default to thinking-ON, which starves a
# short direct answer. Pass enable_thinking explicitly to control it;
# reasoning (when on) lands in `content` for this server, so cot/think
# parse the final label from there.
_tf = {"true": True, "false": False}.get(args.enable_thinking)
extra_body = ({"chat_template_kwargs": {"enable_thinking": _tf}}
if _tf is not None else None)
desc_map = (TARGET_DESCRIPTIONS_RICH if args.rich_desc
else TARGET_DESCRIPTIONS)
desc = desc_map.get(row["target"]) if args.use_desc else None
votes = np.zeros(3)
for e in range(args.sets):
r = np.random.default_rng(seeds[i] + e)
if retriever is not None:
shots = retriever.balanced_shots(
q_emb[i], args.shots,
query_text=row["text"], reranker=reranker,
exclude_text=row["text"] if args.self_exclude else None,
sample_m=args.shot_pool_m,
rng=r if args.shot_pool_m else None,
)
else:
shots = pick_shots(row["target"], pool, allc, args.shots, r)
try:
outs = call(args.base_url, args.model,
messages(row["target"], row["text"], shots,
args.mode, desc),
args.n, args.temperature, max_tokens,
extra_body=extra_body)
except Exception:
outs = []
for o in outs:
lb = parser(o)
if lb:
votes[LABEL2ID[lb]] += 1
if votes.sum() == 0:
votes[LABEL2ID["None"]] = 1
return votes / votes.sum()
probs = np.zeros((len(test), 3))
with ThreadPoolExecutor(max_workers=args.concurrency) as ex:
for i, p in enumerate(ex.map(work, range(len(test)))):
probs[i] = p
if (i + 1) % 200 == 0:
print(f" {i + 1}/{len(test)}")
os.makedirs(os.path.dirname(os.path.abspath(args.out_probs)),
exist_ok=True)
np.save(args.out_probs, probs)
preds = [LABELS[i] for i in probs.argmax(1)]
print(f"[write] probs -> {args.out_probs}")
if args.gold:
score(load_gold(args.gold), preds)
if __name__ == "__main__":
main()
|