Spaces:
Sleeping
Sleeping
| import os | |
| import re | |
| import numpy as np | |
| class BERTFeatureExtractor: | |
| _instance = None | |
| def __new__(cls, model_name="dbmdz/bert-base-turkish-cased"): | |
| if cls._instance is None: | |
| cls._instance = super(BERTFeatureExtractor, cls).__new__(cls) | |
| cls._instance.initialized = False | |
| return cls._instance | |
| def __init__(self, model_name="dbmdz/bert-base-turkish-cased"): | |
| if self.initialized: | |
| return | |
| import torch | |
| from transformers import AutoTokenizer, AutoModel | |
| self.device = "mps" if torch.backends.mps.is_available() else "cpu" | |
| self.tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) | |
| # Use AutoModel to handle BERT, BART (VBART), and GPT (Kumru) architectures | |
| self.model = AutoModel.from_pretrained(model_name, trust_remote_code=True).to(self.device).eval() | |
| self.cache = {} | |
| self.initialized = True | |
| def get_sentence_embeddings(self, tokens): | |
| """ | |
| Extracts contextual embeddings for a sentence. | |
| Returns a list of vectors (1 per token). | |
| """ | |
| sent_str = " ".join(tokens) | |
| if sent_str in self.cache: | |
| return self.cache[sent_str] | |
| import torch | |
| inputs = self.tokenizer(tokens, is_split_into_words=True, return_tensors="pt", padding=True, truncation=True).to(self.device) | |
| # Filter inputs to only include what the model accepts | |
| import inspect | |
| sig = inspect.signature(self.model.forward) | |
| model_inputs = {k: v for k, v in inputs.items() if k in sig.parameters} | |
| with torch.no_grad(): | |
| outputs = self.model(**model_inputs) | |
| # Use last hidden state | |
| last_hidden_state = outputs.last_hidden_state[0] # (seq_len, 768) | |
| # Map word tokens back to original tokens (handling subwords) | |
| word_ids = inputs.word_ids() | |
| word_embeddings = [] | |
| current_word_idx = -1 | |
| current_word_vecs = [] | |
| for i, word_idx in enumerate(word_ids): | |
| if word_idx is None: continue # Skip [CLS], [SEP], [PAD] | |
| if word_idx != current_word_idx: | |
| if current_word_vecs: | |
| word_embeddings.append(torch.stack(current_word_vecs).mean(dim=0).cpu().numpy()) | |
| current_word_idx = word_idx | |
| current_word_vecs = [last_hidden_state[i]] | |
| else: | |
| current_word_vecs.append(last_hidden_state[i]) | |
| if current_word_vecs: | |
| word_embeddings.append(torch.stack(current_word_vecs).mean(dim=0).cpu().numpy()) | |
| # Ensure we have the same number of vectors as tokens | |
| if len(word_embeddings) != len(tokens): | |
| # This can happen due to truncation or tokenizer edge cases | |
| if len(word_embeddings) < len(tokens): | |
| word_embeddings.extend([np.zeros(768)] * (len(tokens) - len(word_embeddings))) | |
| else: | |
| word_embeddings = word_embeddings[:len(tokens)] | |
| self.cache[sent_str] = word_embeddings | |
| return word_embeddings | |
| class FeatureExtractor: | |
| def __init__(self, gazetteer_dir="gazetteers", use_gazetteers=True, use_morphology=True, | |
| use_embeddings=False, embedding_model="dbmdz/bert-base-turkish-cased"): | |
| self.use_gazetteers = use_gazetteers | |
| self.use_morphology = use_morphology | |
| self.use_embeddings = use_embeddings | |
| self.gazetteers = {} | |
| self.embedding_extractor = None | |
| if self.use_embeddings: | |
| self.embedding_extractor = BERTFeatureExtractor(model_name=embedding_model) | |
| # Only load if enabled | |
| if self.use_gazetteers: | |
| self.load_gazetteers(gazetteer_dir) | |
| else: | |
| self.full_names = {} | |
| self.tokens_set = {} | |
| def load_gazetteers(self, gazetteer_dir): | |
| """ | |
| Loads gazetteer files into sets for fast lookup. | |
| Stores both full entries and individual tokens. | |
| """ | |
| self.full_names = {} | |
| self.tokens_set = {} # For partial membership signal | |
| if not os.path.exists(gazetteer_dir): | |
| return | |
| for filename in os.listdir(gazetteer_dir): | |
| if filename.endswith(".txt"): | |
| name = filename.replace(".txt", "") | |
| path = os.path.join(gazetteer_dir, filename) | |
| self.full_names[name] = set() | |
| self.tokens_set[name] = set() | |
| with open(path, "r", encoding="utf-8") as f: | |
| for line in f: | |
| entry = line.strip().lower() | |
| if entry: | |
| self.full_names[name].add(entry) | |
| # Also add individual tokens | |
| for t in entry.split(): | |
| self.tokens_set[name].add(t) | |
| def check_gazetteer(self, text, gazetteer_name, is_token=False): | |
| if not self.use_gazetteers: | |
| return False | |
| text = text.lower() | |
| if is_token: | |
| return text in self.tokens_set.get(gazetteer_name, set()) | |
| return text in self.full_names.get(gazetteer_name, set()) | |
| def word2features(self, sent, i): | |
| """ | |
| Extracts features for a single word in a sentence. | |
| """ | |
| word = sent[i]['word'] | |
| lemma = sent[i]['lemma'] | |
| pos = sent[i]['pos'] | |
| features = { | |
| 'bias': 1.0, | |
| 'word.lower()': word.lower(), | |
| 'word.isupper()': word.isupper(), | |
| 'word.istitle()': word.istitle(), | |
| 'word.isdigit()': word.isdigit(), | |
| 'word.has_apostrophe': "'" in word or "’" in word, | |
| } | |
| # Morphology Features (Optional) | |
| if self.use_morphology: | |
| features['lemma'] = lemma | |
| features['pos'] = pos | |
| # Deep Morphology (Nuve) - Standard or Hybrid | |
| if 'nuve_morph' in sent[i]: | |
| morph = sent[i]['nuve_morph'] | |
| prefix = "nuve." | |
| else: | |
| morph = sent[i].get('morph', []) | |
| prefix = "morph." # Backward compatibility | |
| if isinstance(morph, list) and len(morph) > 0: | |
| features[f'{prefix}count'] = len(morph) | |
| features[f'{prefix}last_suffix_id'] = morph[-1].get('Id', '') | |
| features[f'{prefix}has_change'] = any(m.get('HasChange', False) for m in morph) | |
| # Suffix sequence feature | |
| suffix_seq = "-".join([m.get('Id', '') for m in morph if m.get('Type') != 'Root']) | |
| if suffix_seq: | |
| features[f'{prefix}suffix_seq'] = suffix_seq | |
| # Combine all labels | |
| for m in morph: | |
| for label in m.get('Labels', []): | |
| features[f'{prefix}label_{label}'] = True | |
| # Zemberek Morphology (Hybrid Only) | |
| if 'zemberek_morph' in sent[i]: | |
| z_morph = sent[i]['zemberek_morph'] | |
| prefix = "zember." | |
| if isinstance(z_morph, list) and len(z_morph) > 0: | |
| features[f'{prefix}count'] = len(z_morph) | |
| features[f'{prefix}last_suffix_id'] = z_morph[-1].get('Id', '') | |
| suffix_seq = "-".join([m.get('Id', '') for m in z_morph if m.get('Type') != 'Root']) | |
| if suffix_seq: | |
| features[f'{prefix}suffix_seq'] = suffix_seq | |
| # Gazetteer Features (Optional - controlled by self.use_gazetteers) | |
| if self.use_gazetteers: | |
| # Multi-word Lookup via Windows | |
| for g_name in ['kisiler', 'yerler', 'sirketler', 'kurumlar', 'film_muzik', 'topluluklar']: | |
| # Single word membership | |
| features[f'in_{g_name}_tokens'] = self.check_gazetteer(word, g_name, is_token=True) | |
| # Contextual Match (is this word the START of a multi-word gazetteer entry?) | |
| # Check current + next | |
| if i < len(sent) - 1: | |
| bigram = f"{word} {sent[i+1]['word']}" | |
| if self.check_gazetteer(bigram, g_name): | |
| features[f'start_bigram_{g_name}'] = True | |
| # Check current + next + next next | |
| if i < len(sent) - 2: | |
| trigram = f"{word} {sent[i+1]['word']} {sent[i+2]['word']}" | |
| if self.check_gazetteer(trigram, g_name): | |
| features[f'start_trigram_{g_name}'] = True | |
| # Check prev + current | |
| if i > 0: | |
| prev_bigram = f"{sent[i-1]['word']} {word}" | |
| if self.check_gazetteer(prev_bigram, g_name): | |
| features[f'inside_bigram_{g_name}'] = True | |
| # Specific keyword indicators (often tied to specific categories like Company/Org) | |
| word_l = word.lower() | |
| features.update({ | |
| 'kw_holding': 'holding' in word_l, | |
| 'kw_bank': 'banka' in word_l, | |
| 'kw_company_suffix': any(s in word_l for s in ['a.ş', 'ltd', 'şirketi']), | |
| 'kw_group_suffix': any(s in word_l for s in ['grubu', 'kulübü', 'derneği', 'partisi']), | |
| 'kw_org_suffix': any(s in word_l for s in ['vakfı', 'üniversitesi', 'belediyesi']) | |
| }) | |
| # Regex Features (Extended NER cues) | |
| if re.search(r'(Derneği|Kulübü|Topluluğu)$', word): | |
| features['suffix_group'] = True | |
| if re.search(r'(Vakfı|Birliği|Fonu)$', word): | |
| features['suffix_org'] = True | |
| # Context Features | |
| if i > 0: | |
| word1 = sent[i-1]['word'] | |
| features.update({ | |
| '-1:word.lower()': word1.lower(), | |
| '-1:word.istitle()': word1.istitle(), | |
| '-1:word.isupper()': word1.isupper(), | |
| }) | |
| if self.use_morphology: | |
| features['-1:lemma'] = sent[i-1]['lemma'] | |
| else: | |
| features['BOS'] = True | |
| if i < len(sent)-1: | |
| word1 = sent[i+1]['word'] | |
| features.update({ | |
| '+1:word.lower()': word1.lower(), | |
| '+1:word.istitle()': word1.istitle(), | |
| '+1:word.isupper()': word1.isupper(), | |
| }) | |
| if self.use_morphology: | |
| features['+1:lemma'] = sent[i+1]['lemma'] | |
| else: | |
| features['EOS'] = True | |
| return features | |
| def sent2features(self, sent): | |
| features_list = [self.word2features(sent, i) for i in range(len(sent))] | |
| # Add embedding features if enabled | |
| if self.use_embeddings and self.embedding_extractor: | |
| tokens = [w['word'] for w in sent] | |
| embeddings = self.embedding_extractor.get_sentence_embeddings(tokens) | |
| for i, emb in enumerate(embeddings): | |
| # We use a very simplified mapping or just a few dimensions to avoid exploding CRF features | |
| # In a real scenario, we might use PCA. Here we'll just use the first 16 values as a demo. | |
| for j in range(16): | |
| features_list[i][f'emb_{j}'] = float(emb[j]) | |
| return features_list | |