Código-fonte para aibox.nlp.features.portuguese.semantic_cohesion_transformers
"""Características de coesão semântica
usando Sentence Transformers.
"""
from dataclasses import dataclass
from itertools import combinations
import numpy as np
import spacy
from gensim.matutils import cossim, full2sparse, sparse2full
from scipy.linalg import pinv
from sentence_transformers import SentenceTransformer
from spacy.tokens import Doc
from aibox.nlp.core import FeatureExtractor
from aibox.nlp.features.utils import DataclassFeatureSet
from aibox.nlp.typing import TextArrayLike
[documentos]
@dataclass(frozen=True)
class SemanticFeaturesTransformers(DataclassFeatureSet):
"""Características de coesão semântica versão
SentenceTransformer.
"""
lsa_adj_mean_embedding: float
lsa_adj_std_embedding: float
lsa_all_mean_embedding: float
lsa_all_std_embedding: float
lsa_givenness_mean_embedding: float
lsa_givenness_std_embedding: float
lsa_paragraph_mean_embedding: float
lsa_paragraph_std_embedding: float
lsa_span_mean_embedding: float
lsa_span_std_embedding: float
[documentos]
class SemanticExtractorTransformers(FeatureExtractor):
"""Extrator de características de coesão semãntica
versão SentenceTransformer.
:param nlp: modelo do spaCy para ser utilizado. Defaults
to "pt_core_news_md".
:param model: modelo do SentenceTransformer. Defaults
to "ricardo-filho/bert-base-portuguese-cased-nli-assin-2".
:param dims: quantidade de dimensões dos embeddings.
:param device: dispositivo padrão a set utilizado.
Exemplo de uso em
:py:class:`~aibox.nlp.core.feature_extractor.FeatureExtractor`
"""
def __init__(
self,
nlp: spacy.Language | None = None,
model: SentenceTransformer = None,
dims: int = None,
device: str | None = None,
):
self._nlp = nlp
self._model = model
self._device = device
self._dims = None
@property
def feature_set(self) -> type[SemanticFeaturesTransformers]:
return SemanticFeaturesTransformers
[documentos]
def extract(self, text: str, **kwargs) -> SemanticFeaturesTransformers:
del kwargs
self._maybe_load_models()
doc = self._nlp(text)
sentences = [sent.text for sent in doc.sents]
features = {
"lsa_adj_mean_embedding": 0,
"lsa_adj_std_embedding": 0,
"lsa_all_mean_embedding": 0,
"lsa_all_std_embedding": 0,
"lsa_givenness_mean_embedding": 0,
"lsa_givenness_std_embedding": 0,
"lsa_paragraph_mean_embedding": 0,
"lsa_paragraph_std_embedding": 0,
"lsa_span_mean_embedding": 0,
"lsa_span_std_embedding": 0,
}
if len(sentences) > 1:
adj_sent_mean, adj_sent_std = self._compute_adjacent_sentences(sentences)
all_sent_mean, all_sent_std = self._compute_all_sentences(sentences)
givenness_mean, givenness_std = self._compute_sentences_givenness(sentences)
paragraph_mean, paragraph_std = self._compute_paragraphs(doc)
span_mean, span_std = self._compute_span(sentences)
features["lsa_adj_mean_embedding"] = adj_sent_mean
features["lsa_adj_std_embedding"] = adj_sent_std
features["lsa_all_mean_embedding"] = all_sent_mean
features["lsa_all_std_embedding"] = all_sent_std
features["lsa_givenness_mean_embedding"] = givenness_mean
features["lsa_givenness_std_embedding"] = givenness_std
features["lsa_paragraph_mean_embedding"] = paragraph_mean
features["lsa_paragraph_std_embedding"] = paragraph_std
features["lsa_span_mean_embedding"] = span_mean
features["lsa_span_std_embedding"] = span_std
return SemanticFeaturesTransformers(**features)
def _maybe_load_models(self):
if self._nlp is None:
self._nlp = spacy.load("pt_core_news_md")
if self._model is None:
model_name = "ricardo-filho/bert-base-portuguese-cased-nli-assin-2"
self._model = SentenceTransformer(model_name, device=self._device)
self._dims = 768
def _compute_adjacent_sentences(self, sentences: list[str]) -> tuple[float, float]:
"""Método que extrai a coesão local usando a
similaridade entre pares adjacentes de sentenças no texto.
:param sentences: lista de sentenças do texto
:return: média e desvio padrão da similaridade
"""
pairs = [(sentences[i], sentences[i + 1]) for i in range(len(sentences) - 1)]
mean, std = self._compute_text(pairs)
return mean, std
def _compute_all_sentences(self, sentences: list[str]) -> tuple[float, float]:
"""Método que extrai coesão global usando a similaridade
de todos os pares possíveis de sentenças do texto.
:param sentences: lista de sentenças do texto
:return: média e desvio padrão da similaridade
"""
pairs = list(combinations(sentences, 2))
mean, std = self._compute_text(pairs)
return mean, std
def _compute_sentences_givenness(self, sentences: list[str]) -> tuple[float, float]:
"""Método que extrai o quanto de informação dada existe
em cada sentença de um texto, comparando com o
conteúdo de informação anterior no texto.
:param sentences: lista de sentenças do texto
:return: média e desvio padrão da similaridade
"""
pairs = [
(sentences[i], "".join(sentences[:i])) for i in range(1, len(sentences))
]
mean, std = self._compute_text(pairs)
return mean, std
def _compute_paragraphs(self, doc: Doc) -> tuple[float, float]:
"""Método que extrai a semelhança de um parágrafo
com os outros parágrafos do texto.
:param doc: texto.
:return: média e desvio padrão da similaridade
"""
paragraphs = [
line.strip() for line in doc.text.split("\n") if line and not line.isspace()
]
if len(paragraphs) <= 1:
return 0, 0
pairs = [(paragraphs[i], paragraphs[i + 1]) for i in range(len(paragraphs) - 1)]
mean, std = self._compute_text(pairs)
return mean, std
def _compute_span(self, sentences: list[str]) -> tuple[float, float]:
"""Método que extrai o span da sentença. O span
de uma sentença é uma forma de medir a proximidade
entre uma sentença e o contexto que a precede.
:param sentences: lista de sentenças do texto.
:return: média e desvio padrão da similaridade
"""
mean, std = 0, 0
if len(sentences) < 2:
return mean, std
spans = np.zeros(len(sentences) - 1)
for i in range(1, len(sentences)):
past_sentences = sentences[:i]
span_dimensions = len(past_sentences)
if span_dimensions > self._dims - 1:
beginning = past_sentences[0 : span_dimensions - self._dims]
past_sentences[0] = beginning
past_sentences_vectors = [
sparse2full(self._get_vector(sentence), self._dims)
for sentence in past_sentences
]
current_sentence_vector = sparse2full(
self._get_vector(sentences[i]), self._dims
)
current_sentence_array = np.array(current_sentence_vector).reshape(
self._dims, 1
)
past_sentences_vectors_trans = np.array(past_sentences_vectors).T
projection_matrix = np.dot(
np.dot(
past_sentences_vectors_trans,
pinv(
np.dot(
past_sentences_vectors_trans.T, past_sentences_vectors_trans
)
),
),
past_sentences_vectors_trans.T,
)
projection = np.dot(projection_matrix, current_sentence_array).ravel()
spans[i - 1] = cossim(
full2sparse(current_sentence_vector), full2sparse(projection)
)
mean, std = self._get_mean_std(spans)
return mean, std
def _compute_text(self, pairs: list[tuple[str, str]]) -> tuple[float, float]:
"""Método usado para extrair similaridade
entre os pares de sentenças.
:param pairs: lista de pares de sentenças, ou parágrafos.
:return: média e desvio padrão das similaridades
entre todos os pares.
"""
similarities = [
self._compute_similarity(sent1, sent2) for sent1, sent2 in pairs
]
if len(similarities) == 0:
return 0, 0
mean, std = self._get_mean_std(similarities)
return round(mean, 5), round(std, 5)
def _compute_similarity(self, sentence1: str, sentence2: str) -> float:
"""Método que calcula a similaridade entre duas sentenças."""
return cossim(self._get_vector(sentence1), self._get_vector(sentence2))
def _get_vector(self, sentence: str) -> list[tuple[int, float]]:
"""Método que extrai os embeddings das de uma sentença.
:return: embeddings da sentença no formato de
Bag of Words do gensim.
"""
embeddings = self._model.encode(sentence)
return list(enumerate(embeddings))
@staticmethod
def _get_mean_std(similarities: list[float]) -> tuple[float, float]:
"""Método que calcula a média e o desvio padrão
de uma lista de similaridades.
"""
arr = np.array(similarities)
mean = arr.mean()
std = arr.std()
return mean, std
def _batch_vectorize(self, texts: TextArrayLike, **kwargs):
# TODO: investigar hang que ocorre quando utilizamos
# o multiprocessing
kwargs["n_workers"] = 0
return super()._batch_vectorize(texts, **kwargs)