Если вы работали над проектом по реферированию текстов, то вы могли заметить сложность получения тех результатов, которые ожидалось получить. Если у вас имелись представления относительно того, как должен работать некий алгоритм, какие предложения он должен выделять при формировании рефератов, то этот алгоритм, чаще всего, выдавал результаты, весьма далёкие от ваших представлений. А ещё интереснее — ситуация с извлечением из текстов ключевых слов. Дело в том, что существует множество самых разных алгоритмов — от тех, что используют тематическое моделирование, до тех, где применяется векторизация данных и эмбеддинги. Все они работают очень хорошо. Но если дать одному из них абзац текста, то выданный им результат, опять же, будет далеко не самым правильным. А дело тут в том, что слова, которые встречаются в тексте чаще всего — это не всегда самые важные слова.

Требования к препроцессингу и очистке данных могут сильно варьироваться в зависимости от задачи, которую пытаются решить с помощью NLP-моделей. Я попытаюсь создать универсальный пайплайн, который должен хорошо подойти для всех NLP-моделей. Но то, что делается в ходе выполнения этого пайплайна, нужно будет подстраивать под каждую конкретную ситуацию. В этом материале я буду говорить об NLP-моделях, нацеленных на решение следующих задач: тематическое моделирование, извлечение из текстов ключевых слов, реферирование текстов.

Пайплайн для предварительной обработки данных
Пайплайн для предварительной обработки данных

На вышеприведённом изображении показаны шаги, которым мы будем следовать, создавая препроцессинговый пайплайн для NLP-моделей.

Эти четыре шага мы рассмотрим ниже, проиллюстрировав их с помощью кода. Специально для этого материала создан Jupyter-блокнот, в котором приведена полная реализация пайплайна. Суть этого пайплайна в том, чтобы подвергнуть текстовые данные такому преобразованию, которое позволит повысить эффективность алгоритмов машинного обучения, предназначенных для обработки этих данных. Мы будем говорить о том, как выжать максимум из обучения NLP-моделей, передавая им входные данные, выглядящие наилучшим образом.

1. Очистка текста

Первый шаг нашего пайплайна — это очистка входных текстовых данных. Сам этот шаг может быть разбит на несколько более мелких шагов. Тут всё зависит от той модели, которую пытаются создать с использованием обрабатываемых данных, и от тех результатов, которые ожидают от этой модели получить. Алгоритмы машинного обучения (или, в широком смысле, все компьютерные алгоритмы, скорее даже — все компьютерные инструкции) работают с числами. Именно поэтому разработка моделей для обработки текстовых данных — это трудная и интересная задача. Мы, создавая такую модель, фактически просим компьютер изучить и обработать то, чего он никогда раньше не видел. А это требует времени.

Ниже я приведу код первой из функций нашего пайплайна, которая производит очистку текстовых данных. В этой функции выполняется множество действий, смысл которых я пояснил в комментариях к коду.

%pip install re
%pip install nltk
%pip install unicodedata
%pip install contractions
%pip install inflect
%pip install emoji

import re
import nltk
import emoji
import unicodedata
import contractions
import inflect
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

nltk.download('stopwords')
nltk.download('punkt')

# Функция для очистки текста
def clean_text(input_text):    
    
    # HTML-теги: первый шаг - удалить из входного текста все HTML-теги
    clean_text = re.sub('<[^<]+?>', '', input_text)
    
    # URL и ссылки: далее - удаляем из текста все URL и ссылки
    clean_text = re.sub(r'http\S+', '', clean_text)

    # Эмоджи и эмотиконы: используем собственную функцию для преобразования эмоджи в текст
    # Важно понимать эмоциональную окраску обрабатываемого текста
    clean_text = emojis_words(clean_text)
    
    # Приводим все входные данные к нижнему регистру
    clean_text = clean_text.lower()

    # Убираем все пробелы
    # Так как все данные теперь представлены словами - удалим пробелы
    clean_text = re.sub('\s+', ' ', clean_text)

    # Преобразование символов с диакритическими знаками к ASCII-символам: используем функцию normalize из модуля unicodedata и преобразуем символы с диакритическими знаками к ASCII-символам
    clean_text = unicodedata.normalize('NFKD', clean_text).encode('ascii', 'ignore').decode('utf-8', 'ignore')

    # Разворачиваем сокращения: текст часто содержит конструкции вроде "don't" или "won't", поэтому развернём подобные сокращения
    clean_text = contractions.fix(clean_text)

    # Убираем специальные символы: избавляемся от всего, что не является "словами"
    clean_text = re.sub('[^a-zA-Z0-9\s]', '', clean_text)

    # Записываем числа прописью: 100 превращается в "сто" (для компьютера)
    temp = inflect.engine()
    words = []
    for word in clean_text.split():
        if word.isdigit():
            words.append(temp.number_to_words(word))
        else:
            words.append(word)
    clean_text = ' '.join(words)

    # Стоп-слова: удаление стоп-слов - это стандартная практика очистки текстов
    stop_words = set(stopwords.words('english'))
    tokens = word_tokenize(clean_text)
    tokens = [token for token in tokens if token not in stop_words]
    clean_text = ' '.join(tokens)

    # Знаки препинания: далее - удаляем из текста все знаки препинания
    clean_text = re.sub(r'[^\w\s]', '', clean_text)

    # И наконец - возвращаем очищенный текст
    return clean_text

# Функция для преобразования эмоджи в слова
def emojis_words(text):
    
    # Модуль emoji: преобразование эмоджи в их словесные описания
    clean_text = emoji.demojize(text, delimiters=(" ", " "))
    
    # Редактирование текста путём замены ":" и" _", а так же - путём добавления пробела между отдельными словами
    clean_text = clean_text.replace(":", "").replace("_", " ")
    
    return clean_text

Ниже показан результат использования этой функции для очистки текста:

Результат очистки текста
Результат очистки текста

По выходным данным этой функции можно судить о том, что переданный ей текст очищен от HTML-тегов, что эмоджи в нём заменены на их текстовые описания, что из текста удалены знаки препинания и специальные символы. С таким текстом легче будет работать на следующих шагах нашего пайплайна, где мы займёмся его дальнейшим улучшением.

2. Кластеризация данных для удаления из них «шума» и шаблонных формулировок

Этот шаг нашего пайплайна, возможно, представляет собой самую важную и самую недооценённую процедуру в деле обработки естественного языка. На следующей схеме показано приблизительное представление того, чем будет заниматься соответствующий алгоритм.

Представление процесса очистки данных от «шума»
Представление процесса очистки данных от «шума»

Почему же устранение «шума» — это так важно? Дело в том, что текст «шума», хотя и присутствует во входных данных, не содержит никакой полезной информации, которая способна улучшить результаты обучения модели. Документы — наподобие юридических соглашений, новостных статей, государственных контрактов и прочих подобных содержат большие объёмы шаблонных формулировок, специфичных для различных сфер деятельности. Представьте себе создание проекта по тематическому моделированию, который ориентирован на работу с юридическими договорами. В рамках проекта нужно разобраться с наиболее важными условиями, имеющимися в наборе договоров. При этом в договорах имеются большие объёмы текстов — очень важных, но не отражающих специфику каждого конкретного документа. Это могут быть, например, какие-то формулировки, взятые из текстов законов. Если алгоритм будет ориентироваться на эти формулировки — результаты его работы, в целом, окажутся бесполезными, так как создателям проекта нужно, чтобы он извлекал бы из текстов договоров информацию, уникальную для каждого из них.

Удаление шаблонных формулировок из текстовых данных — это непростая, но чрезвычайно важная задача. Так как эти формулировки представлены в виде обычного текста, внешне ничем не выделяющегося, их тяжело находить и удалять. Но если оставить их в тексте, передаваемом модели, это может сильно ухудшить результаты её обучения.

Рассмотрим реализацию функции, которая убирает из входного текста «шум» и шаблонные формулировки. Применяемый здесь алгоритм использует кластеризацию для поиска часто встречающихся предложений и слов. Алгоритм исходит из предположения о том, что если частота появления в тексте неких структур превышает заданное пороговое значение, эти структуры, вероятно, являются «шумом». «Шум», найденный в тексте, удаляют.

import re
import numpy as np
from sklearn.cluster import KMeans
from sklearn.feature_extraction.text import CountVectorizer

def remove_noise_boilerplate(input_text, min_cluster_size=2, num_clusters=5, max_noise_ratio=0.3):
    
    # Разбиение текста на предложения: для идентификации шаблонных фрагментов или "шума" сначала надо выделить из текста предложения, которые мы будем сравнивать друг с другом
    sentences = re.split('\. |\? |\! |\n', input_text)
    
    # Преобразование предложений в матрицу словесных эмбеддингов
    embeddings_matrix = text_vectorize(sentences)
    
    # KMean-кластеризация: кластеризация предложений, позволяющая разместить похожие эмбеддинги поблизости друг от друга 
    kmeans_model = KMeans(n_clusters=num_clusters)
    kmeans_model.fit(embeddings_matrix)
    model_labels = kmeans_model.labels_
    model_centroids = kmeans_model.cluster_centers_
    cluster_sizes = np.bincount(model_labels)
    
    # Идентификация кластеров, содержащих "шум" и шаблонные формулировки
    is_noise = np.zeros(num_clusters, dtype=bool)
    for i, centroid in enumerate(model_centroids):
        if cluster_sizes[i] < min_cluster_size:
            # Игнорируем кластеры, количество предложений в которых меньше, чем пороговое значение - min_cluster_size
            continue
        distances = np.linalg.norm(embeddings_matrix[model_labels == i] - centroid, axis=1)
        median_distance = np.median(distances)
        if np.count_nonzero(distances > median_distance) / cluster_sizes[i] > max_noise_ratio:
            is_noise[i] = True
    
    # Удаление ненужных данных: предложения, которые идентифицированы как "шум" или шаблонный текст, удаляются
    filtered_sentences = []
    for i, sentence in enumerate(sentences):
        if not is_noise[model_labels[i]]:
            filtered_sentences.append(sentence)
    
    filtered_text = ' '.join(filtered_sentences)
    return filtered_text

def text_vectorize(input_text):
    
    vectorizer = CountVectorizer()
    
    # Использование vectorizer.fit для преобразования текста в матрицу частоты употребления слов
    counts_matrix = vectorizer.fit_transform(input_text)
    
    # Преобразуем полученную матрицу в плотную матрицу
    dense_matrix = counts_matrix.todense()
    
    # Возврат плотной матрицы в виде массива numpy 
    return np.array(dense_matrix)

Ниже приведены результаты обработки этой новостной статьи с помощью нашей функции.

https://miro.medium.com/v2/resize:fit:700/1*p5F57-8OzobxDECn5EV5iQ.png
Результаты очистки текста

Тут можно заметить, что алгоритму передали текст длиной 7574 символа, а после удаления из текста «шума» и шаблонных фрагментов его размер сократился до 892 символов. Получается, что нам удалось уменьшить размеры входного текста примерно на 88%. Фактически, мы удалили из текста мусор, который иначе добрался бы до алгоритма машинного обучения. Итоговый текст — это более аккуратная, осмысленная, тезисная форма входных данных. Убрав из текста всё лишнее, мы как бы указываем нашему алгоритму на то, что ему нужно сосредоточиться лишь на самом важном.

3. Частеречная разметка текста

Частеречная разметка текста (parts-of-speech tagging, POS-tagging) — это процесс назначения словам входного текста определённых меток. Программа читает слова и разбирается в их взаимоотношениях с другими словами предложения, понимает, в каком контексте они применяются. В частности, речь идёт об отнесении слов к таким грамматическим категориям, как имена существительные, глаголы, имена прилагательные, местоимения, предлоги, наречия, союзы и междометия. Это — крайне важный этап предварительной подготовки текста. Дело в том, что многим алгоритмам обработки текста необходимо понимать контекст, в котором используются слова. Это справедливо для алгоритмов, анализирующих тональность текстов и занимающихся классификацией текстов, для алгоритмов, извлекающих из текстов какую-то информацию, занимающихся машинным переводом, или каким-то образом анализирующих текст. Контекст, в котором используются слова, способен сильно повлиять на работу тех частей алгоритмов, которые отвечают за понимание естественного языка.

# Подготовка к частеречной разметке текста путём установки библиотеки Spacy, загрузки perceptron_tagger и модуля Spacy en
%pip install spacy
nltk.download('averaged_perceptron_tagger')
!python -m spacy download en_core_web_sm

import spacy

nlp = spacy.load('en_core_web_sm')

def pos_tag(input_text):
    
    # Используя функцию nlp из Spacy, мы преобразуем входной текст в документ Spacy
    spacy_doc = nlp(input_text)
    tagged_string = []
    for token in spacy_doc:
        tagged_string.append(token.text + '_' + token.pos_)

    tagged_result = ' '.join(tagged_output)
    return tagged_result

Теперь нам осталось рассмотреть лишь последний шаг нашего пайплайна. Это — преобразование текста в векторные эмбеддинги, которые будут использоваться алгоритмом машинного обучения. Но, прежде чем перейти к этой теме, обсудим ещё два важных момента — лемматизацию и стемминг.

Нужны ли вам лемматизация (или) стемминг?

Лемматизация и стемминг — это распространённые в сфере обработки естественного языка подходы приведения слов, изменённых грамматически, к их базовой или начальной форме. Лемматизация и стемминг — это, вероятно, ещё и одни из наиболее спорных механизмов NLP, поэтому стоит разобраться с тем, когда их нужно использовать, а когда не нужно. В основе обоих этих механизмов лежит идея уменьшения размерности входного пространства признаков. Это помогает увеличить производительность моделей машинного обучения, к которым в итоге попадут обработанные данные.

Стемминг — это удаление «лишних» фрагментов слов для приведения их к базовой форме. А в процессе лемматизации текста для приведения слов к их базовой форме используется словарь и разновидность морфологического анализа.

При лемматизации текст подвергается более точной обработке, чем в ходе стемминга, но лемматизация при этом отличается более высокой вычислительной сложностью. На вопрос о том, что именно выбрать для конкретного проекта, можно ответить, учтя необходимый баланс между скоростью и точностью применяемых инструментов.

Вот несколько важных соображений, которые стоит учитывать, размышляя о применении лемматизации или стемминга в проекте:

  1. При лемматизации сохраняется семантика входного текста. Алгоритмы, рассчитанные на анализ тональности текста, могут показывать лучшие результаты в том случае, если в обрабатываемых ими текстах сохраняется информация о временах глаголов. Иногда то, что произошло в прошлом, может иметь эмоциональную окраску, отличную от той, которую имеет то же самое, произошедшее в настоящем.

  2. Стемминг быстрее лемматизации, но точность этого процесса ниже. В случаях, когда речь идёт о задачах классификации текстов, в ходе решения которых нужно разделить по категориям тысячи слов, стемминг, исключительно из-за более высокой скорости, может оказаться лучше, чем лемматизация.

  3. Как и в других подобных ситуациях, выбирая между лемматизацией и стеммингом, можно исследовать оба этих подхода в применении к конкретному проекту. Ответ о том, что именно выбрать, даст сравнение результатов работы модели.

  4. Кроме того, некоторые модели глубокого обучения отличаются способностью автоматически изучать представления слов, что делает неактуальным использование в них стемминга или лемматизации.

4. Лемматизация и векторные эмбеддинги

Последний шаг нашего пайплайна включает в себя лемматизацию текста и преобразование слов в векторные эмбеддинги (не будем забывать о том, что машины лучше работают с числами, а не со словами). Как уже говорилось, лемматизация нужна не во всех проектах. Тут всё зависит от того, каких результатов ожидают от модели, и о от того, какими алгоритмами обучения пользуются. Я, чтобы сделать мой пайплайн универсальнее, включил в него лемматизацию.

Функция, код которой приведён ниже, извлекает слова, подвергнутые частеречной разметке, из полученных ей входных данных. Затем она подвергает слова лемматизации и преобразует их в векторные эмбеддинги. Дополнительные пояснения к этой функции содержатся в комментариях к ней.

import spacy
import numpy as np

nlp = spacy.load("en_core_web_sm")

def lemmatize_and_vectorize(tagged_text):
    
    # Размеченные входные текстовые данные преобразуются в строку и отделяются от тегов частеречной разметки
    split_text = " ".join([word.split("_")[0] for word in tagged_text.split()])

    # Преобразование текста в документ Spacy
    spacy_string = nlp(split_text)
    
    vectors = []

    for token in spacy_string:
        # Выполнение лемматизации
        lemma = token.lemma_

        # Получение тегов частеречной разметки
        pos = token.pos_

        # Получение сведений о частеречной разметке только для глаголов и векторизация данных
        if pos == "VERB":
            vec = token.vector
        else:
            vec = token.vector + nlp(pos).vector
        
        vectors.append(vec)

    # Преобразование списка в массив numpy
    vector_result = np.array(vectors)

    return vector_result

Эта функция возвращает массив numpy размера (num_words, X), где num_words — количество слов во входном тексте, а X — размер векторных эмбеддингов.

Слова, представленные в виде векторных эмбеддингов (числовые представления слов), должны быть теми входными данными, которые передаются алгоритму машинного обучения. Иногда, при использовании некоторых моделей глубокого обучения или некоторых больших языковых моделей, нет нужды ни в векторных эмбеддингах, ни в лемматизации, так как используемый в таких моделях алгоритм достаточно развит для того, чтобы создавать собственные представления слов. Это значит, что данный шаг можно исключить из пайплайна в том случае, если он применяется вместе с такими «самообучающимися» алгоритмами.

Полная реализация пайплайна

В предыдущих четырёх разделах этого материала мы рассмотрели отдельные элементы нашего препроцессингового пайплайна. Здесь лежит Jupyter-блокнот, в котором можно поэкспериментировать с кодом.

Мне хотелось бы обратить ваше внимание на то, что этот пайплайн нельзя назвать универсальным, подходящим абсолютно для всех задач обработки естественного языка. В его основе лежит идея создания надёжного механизма, предназначенного для подготовки данных перед передачей их алгоритмам машинного обучения. Ожидается, что именно такие данные позволят добиться от этих алгоритмов наилучших результатов. Приведённая выше последовательность шагов пайплайна должна решить примерно 70% задач по подготовке данных конкретного проекта. А оставшиеся 30% — это то, что разработчику проекта придётся сделать самостоятельно, занимаясь тонкой настройкой и доработкой пайплайна.

Итоги

Надеюсь, у вас, после прочтения этой статьи, появилось представление о подготовке данных для проектов по обработке естественного языка. Работа со словами — это одна из самых сложных задач из мира искусственного интеллекта. Но если, перед передачей моделям, правильно очищать тексты, подвергать их предварительной обработке, можно добиться хороших результатов в обучении моделей. При этом последовательность применения шагов, описанных в этом материале, так же важна, как и правильная реализация действий, входящих в эти шаги. Каждый шаг зависит от успешности выполнения предыдущего шага.

Так, логика, ответственная за удаление шаблонных фраз, не сможет правильно идентифицировать «шум» в том случае, если полученные ей данные содержат много стоп-слов или HTML-тегов. А частеречная разметка текста, лемматизация, векторизация текста — всё это ресурсозатратные операции, они редко применяются без должного контроля за тем, что поступает на их вход, и что оказывается у них на выходе. Предлагаю вам поэкспериментировать с этим пайплайном и насладиться теми улучшениями, которые он принесёт в ваши NLP-проекты.

О, а приходите к нам работать? ???? ????

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде.

Комментарии (1)


  1. CrazyElf
    29.05.2023 08:39
    +2

    Понятно, что все претензии к оригиналу статьи, но всё-таки. Местами код странноват, либо комментарии к нему. Например, там где написано "Убираем все пробелы", на самом деле удаляют только повторяющиеся пробелы. Т.е. вместо нескольких пробелов делают один пробел. Ещё там оставляют только английские буквы, все остальные удаляют, ну и дальше там много где привязка к тому, что используется английский словарь. Можно было это как-то подчеркнуть где-то. Хотя лучше было бы "локализовать" код, ну да ладно, это всего лишь перевод.
    В целом текст полезный, основные этапы обработки текстов представлены. Метод удаления "шаблонных фраз" интересный, ранее мне такого не встречалось. Интересно было бы посмотреть, насколько он реально полезен "в деле", как он влияет на метрики модели машинного обучения, в которую это всё потом скармливается. В принципе, похожие вопросы решает применение метода TF/IDF, и практика показывает, что для современных сложных моделе, например бустингов, TF/IDF даёт мало пользы, вот для простых моделей грамотный препроцессинг очень важен и чем его больше, тем лучше.
    В общем слово "идеальный" в названии статьи, мне кажется, лишнее. Такие заявления нужно подкреплять какими-то метриками хотя бы. Сравнением нескольких пайплайнов, какие они дадут скоры, причём, для разных моделей. А тут даже на одной модели эти подготовленные данные не попробовали. :)
    P.S. А, так в оригинале написано "ultimate", а не "perfect". Я бы перевёл "ultimate" как "завершённый" или, возможно, "[наиболее] полный", а не "идеальный" в данном случае. ))