CoolFace
Modelpublic

81melody/algerianDeBERTa-realestate-ner-v2

sourceHugging Faceapache-2.0updated 5mo agoView on Hugging Face
1likes11downloads
Model Card

algerianDeBERTa-realestate-ner-v2

High-recall NER for Algerian real estate text

Same architecture and training data as `81melody/algerianDeBERTa-realestate-ner` (the precision-optimised),This one trades a few precision points for measurably higher recall, especially on neighbourhood names

Handles the exact language mix found on Facebook Marketplace and Algerian classified groups: Darja, Arabizi, French, and heavy code-switching.


v1 vs v2 — when to use which

**This model v2****v1**
Val F10.97800.9858
Val Precision0.96460.9784
Val Recall0.99180.9933
Avg entities / listing (prod)8.457.84
NEIGHBORHOOD / 100 texts7248
Avg entity confidence0.9030.931

Use v2 when:

  • —You need maximum location coverage , it finds ~50% more neighbourhood mentions per listing ( "Hussein dey", "Bab Ezzouar", "l3achour" are consistently extracted even when phrasing is irregular)
  • —You are building a search / matching engine and prefer recall over precision (missing a match is worse than a false positive)
  • —You are running a second-pass re-extraction to find locations missed by a stricter model

Use v1 when:

  • —You need clean, high-confidence entities for structured storage or analytics
  • —Precision matters more than recall (e.g. price indexing, surface-area statistics)

Quick comparaison

Summary comparaison : fig3_summary_card

Score distribution fig2_score_distributions

Entity counts fig1_entity_counts

Model Highlights

ArchitectureDeBERTa-v2 — 12 layers, hidden=512, 8 heads, 2048 FFN
Base modelalgerianDeBERTa (pre-trained on Algerian web text)
TaskToken classification — 27 BIO labels, 13 entity types
LanguagesAlgerian Darja · Arabizi · French · MSA · Code-switched
DomainReal estate classifieds (sales, rentals, land, villas, apartments)
Val F10.9780
Val Recall0.9918
Parameters~60M
LicenseApache 2.0
No separate test-set evaluation was run for this version. For test metrics (F1 = 0.9672 on a held-out set of 95 posts) see v1 model card.

Quick Start

Option 1 : pipeline (standard, recommended for short texts)

Uses aggregation_strategy="max": for each surface word the subword token with the highest entity-class score wins, then consecutive spans that have the same type are merged automatically

python
from transformers import pipeline

ner = pipeline(
    "token-classification",
    model="81melody/algerianDeBERTa-realestate-ner-v2",
    aggregation_strategy="max",
)


print(ner("سلام ، خصني اف2 فالعاصمة في ميسوني ولا اودان ولا ديدوش ، في هاد الجويه لي عندو يتوصل معيا في الخاص"))

print(ner("Appartement F4 à vendre Oran centre 120m² 4ème étage acte notarié"))

For texts that may exceed 192 tokens, pass sliding-window arguments directly to the pipeline call:

python
result = ner(
    long_text,
    truncation=True,
    max_length=192,
    stride=64,
)

Each result dict contains entity_group, word, score, start, end.


Option 2 : Manual sliding window inference (production / long texts)

For real estate posts that frequently exceed one chunk + to adapt with The small vocab size of the first version of the base model (30k), the approach below is more robust for production use: it averages the probability vectors of overlapping tokens across all chunks, then merges subword pieces that form the same surface word before BIO decoding

This fixes a common artefact where words like "cherche" (tokenised as ["cher", "che"]) or prices like "1.700" (tokenised as ["1", ".", "700"]) get truncated mid-word if a trailing subword happens to predict O

python
from transformers import AutoTokenizer, AutoModelForTokenClassification
import torch
import numpy as np
from typing import List

MODEL_NAME  = "81melody/algerianDeBERTa-realestate-ner-v2"
MAX_SEQ_LEN = 192
STRIDE      = 64

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model     = AutoModelForTokenClassification.from_pretrained(MODEL_NAME)
model.eval()
device    = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)


def extract_entities(text: str) -> List[dict]:
   
    enc = tokenizer(
        text,
        return_tensors="pt",
        max_length=MAX_SEQ_LEN,
        stride=STRIDE,
        truncation=True,
        return_overflowing_tokens=True,
        return_offsets_mapping=True,
        padding="max_length",
    )
    enc.pop("overflow_to_sample_mapping", None)
    offsets = enc.pop("offset_mapping")          


    with torch.no_grad():
        logits = model(**{k: v.to(device) for k, v in enc.items()}).logits
    probs     = torch.softmax(logits, dim=-1).cpu().numpy()  
    attention = enc["attention_mask"].numpy()                
    off_np    = offsets.numpy()                             


    char_probs = {}
    for c in range(probs.shape[0]):
        for t in range(off_np.shape[1]):
            if attention[c, t] == 0:
                continue
            cs, ce = int(off_np[c, t, 0]), int(off_np[c, t, 1])
            if cs == 0 and ce == 0:         
                continue
            if cs not in char_probs:
                char_probs[cs] = {"end": ce, "vecs": [probs[c, t]]}
            else:
                char_probs[cs]["vecs"].append(probs[c, t])

    if not char_probs:
        return []

    items = sorted(char_probs.items())

    word_tokens = []
    w_start, w_info = items[0]
    w_end   = w_info["end"]
    w_avg_p = np.mean(w_info["vecs"], axis=0)  

    for cs, info in items[1:]:
        if cs == w_end:                         
            w_end = info["end"]
        else:                                    
            word_tokens.append({"start": w_start, "end": w_end, "avg_p": w_avg_p})
            w_start = cs
            w_end   = info["end"]
            w_avg_p = np.mean(info["vecs"], axis=0)
    word_tokens.append({"start": w_start, "end": w_end, "avg_p": w_avg_p})

    label_map = model.config.id2label
    entities, current = [], None

    for w in word_tokens:
        idx   = int(np.argmax(w["avg_p"]))
        label = label_map[idx]
        score = float(w["avg_p"][idx])

        if label == "O":
            if current:
                entities.append(current)
                current = None

        elif label.startswith("B-"):
            if current:
                entities.append(current)
            current = {
                "entity": label[2:],
                "word":   text[w["start"]:w["end"]],
                "score":  score,
                "_sc": [score], "_s": w["start"], "_e": w["end"],
            }

        elif label.startswith("I-"):
            etype = label[2:]
            if current and current["entity"] == etype:
                current["word"]  = text[current["_s"]:w["end"]]
                current["_e"]    = w["end"]
                current["_sc"].append(score)
                current["score"] = float(np.mean(current["_sc"]))
            else:
                if current:
                    entities.append(current)
                current = {
                    "entity": etype,
                    "word":   text[w["start"]:w["end"]],
                    "score":  score,
                    "_sc": [score], "_s": w["start"], "_e": w["end"],
                }

    if current:
        entities.append(current)

    return [
        {"entity": e["entity"], "word": e["word"], "score": round(e["score"], 6)}
        for e in entities
    ]
print(extract_entities('khasni appartement fi bab zouar 200 m2'))
#[{'entity': 'TRANSACTION', 'word': 'khasni', 'score': 0.703333},
#{'entity': 'PROPERTY_TYPE', 'word': 'appartement', 'score': 0.973926},
#{'entity': 'NEIGHBORHOOD', 'word': 'bab zouar', 'score': 0.902971},
#{'entity': 'SURFACE', 'word': '200 m2', 'score': 0.930045}]




Entity Schema

The model uses a 27-label BIO scheme covering 13 entity types.

EntityDescriptionAlgerian Examples
PROPERTY_TYPECategory of assetشقة · villa · appartement · terrain · carcasse · haouch
APT_CLASSApartment layoutF2 · F3 · F4 · F5 · S+1 · Studio
TRANSACTIONListing intentللبيع · location · louer · خاصني · echange
WILAYAAlgerian province (all 48)Alger · Oran · Constantine · 16 · ولاية وهران
CITYCity / communeBab Ezzouar · Sidi Yahia · Ain Benian
NEIGHBORHOODQuarter / district / streetباب الزوار · Hydra · Hai Yasmine · Télemly
PRICEPrice in DZD, DA, or slang8 500 000 da · 12M · 950 000 · 1.5 milliards · 800 U
SURFACEArea in m², metres, hectares90m² · 120 mètres · 85 متر · 2 hectares
FLOORFloor level3ème étage · الطابق الثالث · RDC · R+2
PHONEContact number (anonymized)[PHONE]
AMENITYFeatures / utilitiesgarage · مصعد · piscine · jardin · بيدون
DOCUMENTLegal papersعقد · livret foncier · AADL · acte notarié · timbre
CONDITIONProperty stateneuf · rénové · قديم · semi-fini · en construction

Performance

Evaluated on the validation set (87 posts / 586 tokenized chunks).

Val F1        (micro, seqeval):  0.9780
Val Precision (micro):           0.9646
Val Recall    (micro):           0.9918
Test-set evaluation (strict entity-level seqeval on 95 held-out posts) was performed only on the last epoch (test F1 = 0.9672), This version was not re-evaluated on the test set to avoid data leakage into version selection

Production extraction profile (2,505 unseen listings)

Measured on posts never seen during training:

Metricv2v1
Avg entities / listing8.457.84
Avg entity confidence0.9030.931
Low-conf entities (<0.80)3,1541,537
NEIGHBORHOOD / 100 texts72.348.2
SURFACE / 100 texts73.167.3
PRICE / 100 texts48.144.3

The +24 NEIGHBORHOOD gap is the strongest production signal for choosing this version: it reliably spans multi-word neighbourhood names ("Hussein dey", "Bab Ezzouar", "bordj el kiffan") that the more conservative v1 sometimes under-segments


Training Details

yaml
base_model:        algerianDeBERTa (DeBERTa-v2)
architecture:      DebertaV2ForTokenClassification
num_labels:        27  (BIO, 13 entity types)
saved_at_epoch:    7   


max_seq_len:       192
stride:            64


optimizer:         AdamW
peak_lr:           2e-5
llrd_factor:       0.9
weight_decay:      0.01
grad_accum_steps:  2     

warmup_ratio:      0.1
schedule:          cosine with warmup


label_smoothing:   0.05
class_weighting:   inverse-frequency, capped at 10×
dropout:           0.1

Limitations

  • —Lower precision than v1: Confidence scores are ~2–5 pp lower across all entity types. Surface-only units (m, متر without a number) and single-character fragments (ر, ف) can appear as false-positive WILAYA or FLOOR entities. A confidence threshold of score ≥ 0.75 removes the bulk of these
  • —NEIGHBORHOOD F1: Neighbourhood names in Algeria vary widely in spelling. This version finds more of them but also introduces more borderline extractions
  • —PRICE splitting: Multi-part prices (6 000 000) are occasionally split into separate entities rather than merged as one span.
  • —Geography scope: Optimised for the 58 Algerian wilayas
  • —Platform distribution: Trained on Facebook posts , casual, informal register

Intended Use

Use caseNotes
Location-aware search / matchingBest use case for this version maximises neighbourhood recall
Lead enrichment pipelinesUse when missing a location is worse than a noisy one
Candidate generation + rerankingRun epoch 7 to cast wide net, rerank with v1 scores
Training data generationHigher recall produces more silver labels for rare location types

Citation

If you use this model or the dataset in your research, please cite:

bibtex
@misc{himeur2026algeriandeberta_ner_v2,
  title        = {algerianDeBERTa-realestate-ner-v2: Named Entity Recognition
                  for Algerian Real Estate Text in Darja, Arabizi, and French},
  author       = {Himeur, Ayoub},
  year         = {2026},
  publisher    = {Hugging Face},
  url          = {https://huggingface.co/81melody/algerianDeBERTa-realestate-ner-v2},
  note         = {Fine-tuned DeBERTa-v2 on annotated
                  Algerian Facebook real estate posts, 13 entity types}
}

License

Apache 2.0 free for commercial and research use with attribution

Contact

mohamed.himeur@student.unamur.be