import re import numpy as np from sklearn.feature_extraction.text import CountVectorizer from sklearn.metrics.pairwise import cosine_similarity from sklearn.metrics.pairwise import euclidean_distances from scipy.special import softmax def preprocess(strings): """ Заменить символы '\n' на пробелы и убрать лишние пробелы. strings - список строк. """ for index in range(len(strings)): strings[index] = strings[index].replace('\n', ' ') strings[index] = re.sub(' +', ' ', strings[index]) return strings def get_candidates(text, nlp, min_df=0.0, ngram_range=(1, 3), max_words=None): """ Получить список из max(max_words, #слов в text) кандидатов в ключевые слова. text - входной текст. nlp - инструмент для анализа языка (см. spacy) min_df - минимальная частота вхождения слова в текст. ngram_range - число грам в ключевом слове. max_words - максимальное число слов на выходе. """ # Получим самый базовый набор грам. count = CountVectorizer(ngram_range=ngram_range, stop_words="english", min_df=min_df, max_features=max_words).fit([text]) candidates = count.get_feature_names() #print(candidates) # Обработаем полученный список. nlp_result = nlp(text) # Фразы, содержащие существительные. noun_phrases = set(chunk.text.strip().lower() for chunk in nlp_result.noun_chunks) #print(noun_phrases) # Отдельно существительные. noun_lemmas = set() for token in nlp_result: if token.pos_ == "NOUN": noun_lemmas.add(token.lemma_) # Для одного слова всё-таки бессмысленно хранить форму. print(noun_lemmas) #nouns = set() #noun_lemmas = set() # Сначала составные слова. #for token in nlp_result: # if token.pos_ == "NOUN": # noun_lemmas.add(token.lemma_) # Для одного слова всё-таки бессмысленно хранить форму. # nouns.add(token.text) #print(nouns) nouns = noun_lemmas #nouns.union(noun_lemmas) # Объединение. with_nouns = nouns.union(noun_phrases) # Отфильтровывание. candidates = list(filter(lambda candidate: candidate in with_nouns, candidates)) return candidates def get_embedding(texts, model, tokenizer, chunk_size=128): """ Перевести набор текстов в эмбеддинги. """ n_chunks = len(texts) // chunk_size + int(len(texts) % chunk_size != 0) embeddings = [] for chunk_index in range(n_chunks): start = chunk_index * chunk_size end = min(start + chunk_size, len(texts)) chunk = texts[start:end] chunk_tokens = tokenizer(chunk, padding=True, truncation=True, return_tensors="pt") chunk_embeddings = model(**chunk_tokens)["pooler_output"] chunk_embeddings = chunk_embeddings.detach().numpy() embeddings.append(chunk_embeddings) embeddings = np.vstack(embeddings) return embeddings def score_candidates(text, candidates, model, tokenizer): """ Ранжирование ключевых слов. """ if len(candidates) == 1: return np.array([1.0]) elif len(candidates) == 0: return np.array([]) # Эмбеддинг для текста. text_embedding = get_embedding([text], model, tokenizer) # Эмбеддинг для ключевых слов. candidate_embeddings = get_embedding(candidates, model, tokenizer) # Будем брать softmax от нормированных косинусных расстояний. distances = cosine_similarity(text_embedding, candidate_embeddings) score = softmax((distances - np.mean(distances)) / np.std(distances))[0] return score def get_keywords(text, nlp, model, tokenizer, top=0.95, max_words=None): try: candidates = get_candidates(text, nlp) score = score_candidates(text, candidates, model, tokenizer) except Exception as ex: return None candidates_scored = [(candidates[index], score[index]) for index in score.argsort()[::-1]] result = [] sum_probability = 0.0 max_words = len(candidates_scored) if max_words is None else min(len(candidates_scored), max_words) for index in range(max_words): if sum_probability > top: break result.append(candidates_scored[index]) sum_probability += candidates_scored[index][1] return result