CoolFace
Apppublic

motaseeem/Arabic-Main-Question-Generation

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
app.py322 linesDownload Raw Back to root
1import re2import difflib3import numpy as np4import torch5import gradio as gr6import pyarabic.araby as araby7 8import stanza9from transformers import AutoTokenizer, AutoModel10from transformers import AutoTokenizer as HFTokenizer, AutoModelForSeq2SeqLM11from sentence_transformers import SentenceTransformer, util12import arabert.preprocess13import yake14from bert_score import score as bertscore15 16DEVICE = "cuda" if torch.cuda.is_available() else "cpu"17torch.set_grad_enabled(False)18 19# ---- نماذج وأدوات ----20ARAELECTRA_NAME = "aubmindlab/araelectra-base-discriminator"21SBERT_MODEL      = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"22QG_MODEL         = "Mihakram/AraT5-base-question-generation"23 24# Stanza (أول تشغيل قد يحمّل حزمة العربية ويكاشها)25stanza.download("ar", verbose=False)26nlp = stanza.Pipeline(lang="ar", processors="tokenize,pos,lemma,depparse", tokenize_no_ssplit=False, verbose=False)27 28# Arabert preprocessor29arabert_prep = arabert.preprocess.ArabertPreprocessor(ARAELECTRA_NAME)30 31# AraELECTRA (للأوفست والتمثيلات السياقية)32tokenizer_electra = AutoTokenizer.from_pretrained(ARAELECTRA_NAME)33model_electra     = AutoModel.from_pretrained(ARAELECTRA_NAME).to(DEVICE)34 35# sBERT36sbert = SentenceTransformer(SBERT_MODEL, device=DEVICE)37 38# AraT5 (توليد سؤال)39qg_tokenizer = HFTokenizer.from_pretrained(QG_MODEL)40qg_model     = AutoModelForSeq2SeqLM.from_pretrained(QG_MODEL).to(DEVICE)41 42# ---- أدوات مساعدة ----43def normalize(s: str) -> str:44    t = araby.strip_tashkeel(s)45    t = t.replace("آ","ا").replace("أ","ا").replace("إ","ا").replace("ى","ي")46    t = t.replace("ـ","")47    t = " ".join(t.split())48    return t49 50def build_char_map(src: str, tgt: str):51    sm = difflib.SequenceMatcher(a=src, b=tgt)52    src2tgt = [-1] * len(src)53    for tag, i1, i2, j1, j2 in sm.get_opcodes():54        if tag == "equal":55            for k in range(i2 - i1):56                src2tgt[i1 + k] = j1 + k57        elif tag in ("replace", "delete"):58            for k in range(i2 - i1):59                src2tgt[i1 + k] = j160    last = 061    for i in range(len(src2tgt)):62        if src2tgt[i] == -1:63            src2tgt[i] = last64        else:65            last = src2tgt[i]66    return src2tgt67 68def map_span_src_to_tgt(src2tgt, start, end, tgt_len):69    if start >= len(src2tgt): start = max(0, len(src2tgt)-1)70    if end == 0: end = 171    if end-1 >= len(src2tgt): end = len(src2tgt)72    ts = src2tgt[start]; te = src2tgt[end-1] + 173    ts = max(0, min(ts, max(0, tgt_len-1)))74    te = max(ts+1, min(te, tgt_len))75    return ts, te76 77def token_indices_overlapping_span(offsets, span_start, span_end):78    idxs = []79    for i, (s, e) in enumerate(offsets):80        if e > span_start and s < span_end:81            idxs.append(i)82    return idxs83 84def electra_hidden_states(prep_text):85    encoded = tokenizer_electra(prep_text, return_tensors="pt", return_offsets_mapping=True, padding=False, truncation=True).to(DEVICE)86    offsets = encoded.pop("offset_mapping")[0].tolist()87    with torch.no_grad():88        out = model_electra(**encoded)89    H = out.last_hidden_state.squeeze(0)90    return offsets, H91 92def electra_phrase_vec_via_offsets(span_start, span_end, src2tgt, prep_text, offsets, H):93    ts, te = map_span_src_to_tgt(src2tgt, span_start, span_end, len(prep_text))94    tok_ids = token_indices_overlapping_span(offsets, ts, te)95    if not tok_ids:96        return None97    vecs = [H[i] for i in tok_ids]98    return torch.stack(vecs, dim=0).mean(dim=0)99 100# استخراج عبارات اسمية101def build_noun_phrases(doc, text_norm):102    noun_phrases = []103    for si, sent in enumerate(doc.sentences):104        words_info = []105        for ti, tok in enumerate(sent.tokens):106            for w in tok.words:107                words_info.append({108                    "id": w.id, "text": w.text, "upos": w.upos, "deprel": w.deprel,109                    "head": w.head, "start": tok.start_char, "end": tok.end_char, "tok_idx": ti110                })111        for wi in words_info:112            if wi["upos"] not in {"NOUN","PROPN"}:  # رؤوس اسمية113                continue114            head = wi115            left_mods, right_mods = [], []116            for cj in words_info:117                if cj["head"] == head["id"] and cj["deprel"] in {"amod","compound","nmod"}:118                    (left_mods if cj["start"] <= head["start"] else right_mods).append(cj)119            left_mods  = sorted(left_mods,  key=lambda x: x["start"])120            right_mods = sorted(right_mods, key=lambda x: x["start"])121            phrase_tokens = left_mods + [head] + right_mods122            if len(phrase_tokens) < 2 and head["upos"] != "PROPN":  # استثناء الأعلام المفردة123                continue124            span_start = min(t["start"] for t in phrase_tokens); span_end = max(t["end"] for t in phrase_tokens)125            phrase_text = re.sub(r"\s+", " ", text_norm[span_start:span_end].strip())126            if len(phrase_text) >= 2:127                noun_phrases.append({"text": phrase_text, "start": span_start, "end": span_end})128    # تمييز129    uniq = {}130    for np_item in noun_phrases:131        key = np_item["text"]132        if key not in uniq or (np_item["end"]-np_item["start"]) > (uniq[key]["end"]-uniq[key]["start"]):133            uniq[key] = np_item134    return list(uniq.values())135 136# الترتيب: sBERT + ELECTRA + MMR137def mmr_select(doc_emb, cand_embs, candidates, k=10, lam=0.7):138    if not candidates: return []139    chosen, rest = [], list(range(len(candidates)))140    sim_doc = util.cos_sim(doc_emb, cand_embs)[0].cpu().numpy()141    first = int(np.argmax(sim_doc)); chosen.append(first); rest.remove(first)142    sim_between = util.cos_sim(cand_embs, cand_embs).cpu().numpy()143    while len(chosen) < min(k, len(candidates)) and rest:144        best_i, best_score = None, -1e9145        for i in rest:146            redundancy = max(sim_between[i, j] for j in chosen) if chosen else 0.0147            score = 0.7*sim_doc[i] - 0.3*redundancy148            if score > best_score: best_score, best_i = score, i149        chosen.append(best_i); rest.remove(best_i)150    return [candidates[i] for i in chosen]151 152def rank_keyphrases(text_norm, nps, alpha=0.8):153    phrases = [p["text"] for p in nps]154    if not phrases: return [], []155    text_prep = arabert_prep.preprocess(text_norm)156    src2tgt = build_char_map(text_norm, text_prep)157    # sBERT158    doc_emb = sbert.encode([text_prep], convert_to_tensor=True)159    phr_embs = sbert.encode(phrases, convert_to_tensor=True)160    sims_sbert = util.cos_sim(doc_emb, phr_embs).cpu().numpy()[0]161    # ELECTRA162    offsets, H = electra_hidden_states(text_prep)163    doc_vec_electra = H.mean(dim=0)164    sims_electra = []165    for p in nps:166        v = electra_phrase_vec_via_offsets(p["start"], p["end"], src2tgt, text_prep, offsets, H)167        if v is None: sims_electra.append(0.0)168        else:169            num = torch.dot(doc_vec_electra, v).item()170            den = float(doc_vec_electra.norm().item() * v.norm().item() + 1e-9)171            sims_electra.append(num/den)172    sims_electra = np.array(sims_electra)173    blended = alpha*sims_sbert + (1-alpha)*sims_electra174    order = np.argsort(-blended)175    ranked = [(phrases[i], float(blended[i]), float(sims_sbert[i]), float(sims_electra[i])) for i in order]176    diverse = mmr_select(doc_emb, phr_embs, phrases, k=min(12, len(phrases)), lam=0.7)177    return ranked, diverse178 179# YAKE180def yake_scores_for_phrases(text_norm, phrases, max_ngram_size=5, lan="ar"):181    kw_extractor = yake.KeywordExtractor(lan=lan, n=max_ngram_size, dedupLim=0.9, top=1000)182    scored = kw_extractor.extract_keywords(text_norm)183    norm = lambda s: re.sub(r"\s+"," ", s).strip().lower()184    scored_norm = {norm(k): v for k, v in scored}185    res = {}186    for p in phrases:187        res[p] = scored_norm.get(norm(p))188    return res189 190def invert_and_minmax_yake(score_map):191    vals = [None if v is None else 1/(1+v) for v in score_map.values()]192    finite = [x for x in vals if x is not None]193    if not finite: return {k:0.0 for k in score_map.keys()}194    vmin, vmax = min(finite), max(finite); rng = (vmax-vmin) if vmax>vmin else 1.0195    out = {}196    for (k,_), pos in zip(score_map.items(), vals):197        out[k] = 0.0 if pos is None else (pos - vmin)/rng198    return out199 200def blend_semantic_with_yake(ranked_sem, yake_norm, w_sem=0.7, w_yake=0.3):201    merged = []202    for phr, sem_sc, sb, el in ranked_sem:203        y = yake_norm.get(phr, 0.0)204        final = w_sem*sem_sc + w_yake*y205        merged.append((phr, final, sem_sc, y, sb, el))206    merged.sort(key=lambda x: -x[1])207    return merged208 209# تقسيم بالنقطة + اختيار جملة داعمة لكل عبارة210def split_by_dots(text: str):211    parts = re.split(r"\.{1,}\s*", text)212    return [p.strip() for p in parts if p.strip()]213 214def sentence_kind_from_root(stanza_sentence):215    root = next((w for w in stanza_sentence.words if w.deprel == "root"), None)216    if not root: return "unknown"217    return "verbal" if root.upos == "VERB" else "nominal"218 219def split_and_tag_nominal_verbal_by_dots(text_norm):220    sents = split_by_dots(text_norm)221    tagged = []222    for s in sents:223        doc_s = nlp(s)224        if not doc_s.sentences:225            tagged.append({"text": s, "kind": "unknown"})226        else:227            tagged.append({"text": s, "kind": sentence_kind_from_root(doc_s.sentences[0])})228    return tagged229 230def best_support_sentence_by_dots(text_norm, phrase):231    sentences_tagged = split_and_tag_nominal_verbal_by_dots(text_norm)232    if not sentences_tagged: return ""233    sent_texts = [m["text"] for m in sentences_tagged]234    sent_embs  = sbert.encode(sent_texts, convert_to_tensor=True)235    p_emb      = sbert.encode([phrase], convert_to_tensor=True)236    sims = util.cos_sim(p_emb, sent_embs)[0].cpu().numpy()237    best_idx = int(np.argmax(sims))238    return sent_texts[best_idx], sentences_tagged[best_idx]["kind"]239 240# توليد سؤال موحّد (بدون hints)241def gen_unified_question_freeform(phrases, supports, context_text, max_len=96, num_beams=5):242    context_short = context_text.strip()[:600]243    items_block = "\n".join([f"- العبارة: {p}\n  جملة داعمة: {s}" for p, s in zip(phrases, supports)])244    prompt = (245        "حوّل العبارات التالية إلى سؤال واحد شامل بالعربية يعتمد على السياق. "246        "يجب أن يغطي جميع العبارات بشكل موجز وواضح.\n"247        f"{items_block}\n"248        f"سياق: {context_short}\n"249        "السؤال الموحد:"250    )251    inputs = qg_tokenizer(prompt, return_tensors="pt", truncation=True).to(DEVICE)252    outputs = qg_model.generate(253        **inputs, max_length=max_len, num_beams=num_beams,254        early_stopping=True, no_repeat_ngram_size=3255    )256    q = qg_tokenizer.decode(outputs[0], skip_special_tokens=True).strip()257    q = q.rstrip("?.؟")258    if q and not q.endswith("؟"): q += "؟"259    return q260 261# الواجهة: خطوة واحدة تنفّذ كل شيء وتعرض النتائج262def run_pipeline(user_text):263    if not user_text or len(user_text.strip()) < 5:264        return "رجاءً أدخل نصًا عربيًا أطول.", "", "", "", ""265 266    text_norm = normalize(user_text)267    doc = nlp(text_norm)268 269    # 1) عبارات اسمية270    nps = build_noun_phrases(doc, text_norm)271    if not nps:272        return "لم تُستخرج عبارات اسمية.", "", "", "", ""273 274    # 2) ترتيب دلالي275    ranked_sem, diverse = rank_keyphrases(text_norm, nps, alpha=0.8)276 277    # 3) YAKE + دمج278    phrases = [r[0] for r in ranked_sem]279    yake_raw  = yake_scores_for_phrases(text_norm, phrases, max_ngram_size=5, lan="ar")280    yake_norm = invert_and_minmax_yake(yake_raw)281    ranked_blended = blend_semantic_with_yake(ranked_sem, yake_norm, w_sem=0.7, w_yake=0.3)282 283    # 4) أفضل جملة داعمة لأول 5 عبارات284    top_n = min(5, len(ranked_blended))285    top_phrases = [ranked_blended[i][0] for i in range(top_n)]286    supports = []287    kinds    = []288    for p in top_phrases:289        s, kind = best_support_sentence_by_dots(text_norm, p)290        supports.append(s); kinds.append(kind)291 292    # 5) سؤال موحّد من الخمس عبارات293    unified_q = gen_unified_question_freeform(top_phrases, supports, text_norm)294 295    # إخراج منسق296    nps_str = "\n".join(f"- {p['text']}" for p in nps[:20])297    ranked_str = "\n".join(f"{i+1:>2}. {t[0]} (score={t[1]:.3f})" for i, t in enumerate(ranked_blended[:15]))298    support_str = "\n".join(f"{i+1:>2}. [{kinds[i]}] {top_phrases[i]}  →  {supports[i]}" for i in range(top_n))299    diverse_str = "\n".join(f"- {d}" for d in diverse[:10])300 301    return unified_q, ranked_str, support_str, diverse_str, nps_str302 303title = "Arabic Main Question Generation (Hybrid Pipeline)"304desc = "أدخل نصًا عربيًا؛ سنستخرج العبارات الاسمية، نرتّبها (sBERT + ELECTRA + YAKE + MMR)، نختار جملًا داعمة، ونولّد سؤالًا موحّدًا بـ AraT5."305 306with gr.Blocks(title=title) as demo:307    gr.Markdown(f"# {title}\n{desc}")308 309    with gr.Row():310        inp = gr.Textbox(lines=12, label="النص العربي")311    btn = gr.Button("تشغيل الـPipeline")312 313    out_unified = gr.Textbox(label="السؤال الموحد (AraT5)")314    out_ranked  = gr.Textbox(label="Top Noun Phrases (Blended Ranking)")315    out_support = gr.Textbox(label="أفضل الجمل الداعمة لأول 5 عبارات")316    out_diverse = gr.Textbox(label="MMR Diverse Selection")317    out_nps     = gr.Textbox(label="العبارات الاسمية المستخرجة (أول 20)")318 319    btn.click(run_pipeline, inputs=inp, outputs=[out_unified, out_ranked, out_support, out_diverse, out_nps])320 321demo.launch()322