crf-address-ner-pt / model_crf.py
felipergcpqd's picture
Add Código Fonte para Injection
e6875f4 verified
Raw
History Blame Contribute Delete
6.87 kB
"""
Modelo CRF para NER de Endereços - Compatível com sklearn Pipeline
"""
import sklearn_crfsuite
import re
from sklearn.base import BaseEstimator, TransformerMixin
class AddressFeatureExtractor(BaseEstimator, TransformerMixin):
"""Extrator de features morfológicas + contextuais para CRF (compatível com sklearn)"""
def __init__(self):
pass
def fit(self, X, y=None):
"""Fit não faz nada, mas é necessário para compatibilidade sklearn
Args:
X: Lista de listas de tokens
y: Ignorado (para compatibilidade sklearn)
"""
return self
def transform(self, X):
"""Transforma tokens em features para CRF
Args:
X: Lista de listas de tokens
Returns:
Lista de listas de dicionários de features
"""
return [self._sent2features(tokens) for tokens in X]
def _sent2features(self, tokens):
"""Converte sequência de tokens em features para CRF"""
return [self._word2features(tokens, i) for i in range(len(tokens))]
def _word2features(self, tokens, i):
"""Extrai features para CRF com contexto de vizinhança"""
word = tokens[i]
features = self._extract_word_features_crf(word)
features['bias'] = 1.0
if i > 0:
word_prev = tokens[i - 1]
features.update({
'-1:word.lower': word_prev.lower(),
'-1:is_title': word_prev.istitle(),
'-1:is_upper': word_prev.isupper(),
'-1:is_digit': word_prev.isdigit(),
})
else:
features['BOS'] = True
if i < len(tokens) - 1:
word_next = tokens[i + 1]
features.update({
'+1:word.lower': word_next.lower(),
'+1:is_title': word_next.istitle(),
'+1:is_upper': word_next.isupper(),
'+1:is_digit': word_next.isdigit(),
})
else:
features['EOS'] = True
return features
def _extract_word_features_crf(self, word):
"""Extrai as MESMAS features morfológicas do HMM para o CRF"""
features = {}
word_lower = word.lower()
features['word.lower'] = word_lower
if word.isdigit():
features['is_digit'] = True
if len(word) == 1:
features['digit_single'] = True
elif len(word) <= 4:
features['digit_small'] = True
elif len(word) == 5:
features['digit_five'] = True
else:
features['digit_large'] = True
else:
features['is_digit'] = False
if word.isupper() and len(word) == 2 and word.isalpha():
features['is_state_abbrev'] = True
else:
features['is_state_abbrev'] = False
features['is_title'] = word.istitle()
if word.isupper() and len(word) > 2:
features['is_upper'] = True
else:
features['is_upper'] = False
if '-' in word:
features['has_dash'] = True
if re.match(r'^\d{5}-\d{3}$', word):
features['is_cep_format'] = True
else:
features['is_cep_format'] = False
else:
features['has_dash'] = False
features['is_cep_format'] = False
if re.match(r'^\d{8}$', word):
features['is_cep_no_dash'] = True
else:
features['is_cep_no_dash'] = False
word_len = len(word)
if word_len == 1:
features['len_1'] = True
elif word_len == 2:
features['len_2'] = True
elif word_len <= 4:
features['len_short'] = True
elif word_len >= 10:
features['len_long'] = True
else:
features['len_medium'] = True
common_types = {
'rua': 'tipo_rua', 'avenida': 'tipo_av', 'travessa': 'tipo_trav',
'alameda': 'tipo_alam', 'praça': 'tipo_praca', 'praca': 'tipo_praca',
'estrada': 'tipo_estrada', 'rodovia': 'tipo_rod', 'viela': 'tipo_viela',
'av': 'tipo_av', 'r': 'tipo_rua', 'trav': 'tipo_trav'
}
if word_lower in common_types:
features[common_types[word_lower]] = True
complements = {'apto', 'ap', 'apartamento', 'sala', 'bloco', 'casa', 'lote', 'quadra'}
if word_lower in complements:
features['is_complement_word'] = True
else:
features['is_complement_word'] = False
return features
class CRFTagger(BaseEstimator):
"""CRF para tagging de sequências (compatível com sklearn Pipeline)"""
def __init__(self, algorithm='lbfgs', c1=0.1, c2=0.1, max_iterations=100,
all_possible_transitions=True):
self.algorithm = algorithm
self.c1 = c1
self.c2 = c2
self.max_iterations = max_iterations
self.all_possible_transitions = all_possible_transitions
self.model = None
self.classes_ = None
def fit(self, X, y):
"""Treina o modelo CRF
Args:
X: Lista de listas de dicionários de features
y: Lista de listas de labels
"""
self.model = sklearn_crfsuite.CRF(
algorithm=self.algorithm,
c1=self.c1,
c2=self.c2,
max_iterations=self.max_iterations,
all_possible_transitions=self.all_possible_transitions,
verbose=False
)
self.model.fit(X, y)
self.classes_ = self.model.classes_
print(f" ✓ CRF treinado com {len(self.classes_)} labels")
return self
def predict(self, X):
"""Prediz labels para sequências
Args:
X: Lista de listas de dicionários de features
Returns:
Lista de listas de labels preditos
"""
if self.model is None:
raise ValueError("Modelo não treinado! Execute .fit() primeiro.")
return self.model.predict(X)
def get_params(self, deep=True):
"""Retorna parâmetros do estimator (sklearn compatibility)"""
return {
'algorithm': self.algorithm,
'c1': self.c1,
'c2': self.c2,
'max_iterations': self.max_iterations,
'all_possible_transitions': self.all_possible_transitions
}
def set_params(self, **params):
"""Define parâmetros do estimator (sklearn compatibility)"""
for key, value in params.items():
setattr(self, key, value)
return self