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
| """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() | |