Этапы предварительной обработки текста и универсальный многоразовый конвейер

Описание всех этапов предварительной обработки текста и создание многоразового конвейера предварительной обработки текста.

Прежде чем вводить какие-либо данные в какую-либо модель машинного обучения, они должны быть должным образом предварительно обработаны. Вы, должно быть, слышали пословицу: Garbage in, garbage out (GIGO). Текст - это особый вид данных, и его нельзя напрямую передать в большинство моделей машинного обучения, поэтому перед тем, как передать его в модель, вы должны каким-то образом извлечь из него числовые характеристики, другими словами vectorize. Векторизация не является темой этого урока, но главное, что вы должны понять, это то, что GIGO применима и для векторизации, вы можете извлекать качественные характеристики только из качественно предварительно обработанного текста.

Что мы собираемся обсудить:

  1. Токенизация
  2. Уборка
  3. Нормализация
  4. Лемматизация
  5. Приготовление на пару

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

Ядро Kaggle: https://www.kaggle.com/balatmak/text-preprocessing-steps-and-universal-pipeline

Предположим, что это пример текста:

An explosion targeting a tourist bus has injured at least 16 people near the Grand Egyptian Museum, 
next to the pyramids in Giza, security sources say E.U.

South African tourists are among the injured. Most of those hurt suffered minor injuries, 
while three were treated in hospital, N.A.T.O. say.

http://localhost:8888/notebooks/Text%20preprocessing.ipynb

@nickname of twitter user and his email is [email protected] . 

A device went off close to the museum fence as the bus was passing on 16/02/2012.

Токенизация

Tokenization - этап предварительной обработки текста, предполагающий разбиение текста на tokens (слова, предложения и т. Д.)

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

НЛТК:

from nltk.tokenize import sent_tokenize, word_tokenize

nltk_words = word_tokenize(example_text)
display(f"Tokenized words: {nltk_words}")

Выход:

Tokenized words: ['An', 'explosion', 'targeting', 'a', 'tourist', 'bus', 'has', 'injured', 'at', 'least', '16', 'people', 'near', 'the', 'Grand', 'Egyptian', 'Museum', ',', 'next', 'to', 'the', 'pyramids', 'in', 'Giza', ',', 'security', 'sources', 'say', 'E.U', '.', 'South', 'African', 'tourists', 'are', 'among', 'the', 'injured', '.', 'Most', 'of', 'those', 'hurt', 'suffered', 'minor', 'injuries', ',', 'while', 'three', 'were', 'treated', 'in', 'hospital', ',', 'N.A.T.O', '.', 'say', '.', 'http', ':', '//localhost:8888/notebooks/Text', '%', '20preprocessing.ipynb', '@', 'nickname', 'of', 'twitter', 'user', 'and', 'his', 'email', 'is', 'email', '@', 'gmail.com', '.', 'A', 'device', 'went', 'off', 'close', 'to', 'the', 'museum', 'fence', 'as', 'the', 'bus', 'was', 'passing', 'on', '16/02/2012', '.']

Просторный:

import spacy
import en_core_web_sm

nlp = en_core_web_sm.load()

doc = nlp(example_text)
spacy_words = [token.text for token in doc]
display(f"Tokenized words: {spacy_words}")

Выход:

Tokenized words: ['\\n', 'An', 'explosion', 'targeting', 'a', 'tourist', 'bus', 'has', 'injured', 'at', 'least', '16', 'people', 'near', 'the', 'Grand', 'Egyptian', 'Museum', ',', '\\n', 'next', 'to', 'the', 'pyramids', 'in', 'Giza', ',', 'security', 'sources', 'say', 'E.U.', '\\n\\n', 'South', 'African', 'tourists', 'are', 'among', 'the', 'injured', '.', 'Most', 'of', 'those', 'hurt', 'suffered', 'minor', 'injuries', ',', '\\n', 'while', 'three', 'were', 'treated', 'in', 'hospital', ',', 'N.A.T.O.', 'say', '.', '\\n\\n', 'http://localhost:8888/notebooks', '/', 'Text%20preprocessing.ipynb', '\\n\\n', '@nickname', 'of', 'twitter', 'user', 'and', 'his', 'email', 'is', '[email protected]', '.', '\\n\\n', 'A', 'device', 'went', 'off', 'close', 'to', 'the', 'museum', 'fence', 'as', 'the', 'bus', 'was', 'passing', 'on', '16/02/2012', '.', '\\n']

В расширенной токенизации вывода, но не в nltk:

{'E.U.', '\\n', 'Text%20preprocessing.ipynb', '[email protected]', '\\n\\n', 'N.A.T.O.', 'http://localhost:8888/notebooks', '@nickname', '/'}

В nltk, но не в просторном:

{'nickname', '//localhost:8888/notebooks/Text', 'N.A.T.O', ':', '@', 'gmail.com', 'E.U', 'http', '20preprocessing.ipynb', '%'}

Мы видим, что spacy токенизировал некоторые странные вещи, такие как \n, \n\n, но мог обрабатывать URL-адреса, электронные письма и упоминания, подобные Twitter. Кроме того, мы видим, что nltk размеченных сокращений без последних .

Уборка

Cleaning этот шаг предполагает удаление всего нежелательного содержимого.

Удаление знаков препинания

Punctuation removal может быть хорошим шагом, когда пунктуация не приносит дополнительной ценности для векторизации текста. Удаление знаков препинания лучше производить после этапа токенизации, так как это может вызвать нежелательные эффекты. Хороший выбор для TF-IDF, Count, Binary векторизации.

Предположим, что этот текст для этого шага:

@nickname of twitter user, and his email is [email protected] .

Перед токенизацией:

text_without_punct = text_with_punct.translate(str.maketrans('', '', string.punctuation))
display(f"Text without punctuation: {text_without_punct}")

Выход:

Text without punctuation: nickname of twitter user and his email is emailgmailcom

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

import spacy
import en_core_web_sm

nlp = en_core_web_sm.load()
doc = nlp(text_with_punct)
tokens = [t.text for t in doc]
# python based removal
tokens_without_punct_python = [t for t in tokens if t not in string.punctuation]
display(f"Python based removal: {tokens_without_punct_python}")
# spacy based removal
tokens_without_punct_spacy = [t.text for t in doc if t.pos_ != 'PUNCT']
display(f"Spacy based removal: {tokens_without_punct_spacy}")

Результат удаления на основе Python:

['@nickname', 'of', 'twitter', 'user', 'and', 'his', 'email', 'is', '[email protected]']

Удаление на основе Spacy:

['of', 'twitter', 'user', 'and', 'his', 'email', 'is', '[email protected]']

Здесь вы видите, что удаление python-based сработало даже лучше, чем spacy, потому что spacy пометил @nicname как PUNCT часть речи.

Удаление стоп-слов

Stop words обычно относится к наиболее употребительным словам в языке, которые обычно не имеют дополнительного значения. Не существует единого универсального списка стоп-слов, используемых всеми инструментами nlp, потому что этот термин имеет очень нечеткое определение. Хотя практика показала, что этот шаг обязателен при подготовке текста к индексации, но может оказаться сложным для целей классификации текста.

Количество просторных стоп-слов: 312

Количество стоп-слов NLTK: 179

Предположим, что этот текст для этого шага:

This movie is just not good enough

Просторный:

import spacy
import en_core_web_sm

nlp = en_core_web_sm.load()
text_without_stop_words = [t.text for t in nlp(text) if not t.is_stop]
display(f"Spacy text without stop words: {text_without_stop_words}")

Объемный текст без стоп-слов:

['movie', 'good']

НЛТК:

import nltk

nltk_stop_words = nltk.corpus.stopwords.words('english')
text_without_stop_words = [t for t in word_tokenize(text) if t not in nltk_stop_words]
display(f"nltk text without stop words: {text_without_stop_words}")

Текст NLTK без стоп-слов:

['This', 'movie', 'good', 'enough']

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

Для таких случаев вы можете установить стоп-слова, которые можно игнорировать, в библиотеке spacy. В случае nltk вы просто удалите или добавите пользовательские слова в nltk_stop_words, это просто список.

import en_core_web_sm

nlp = en_core_web_sm.load()

customize_stop_words = [
    'not'
]

for w in customize_stop_words:
    nlp.vocab[w].is_stop = False

text_without_stop_words = [t.text for t in nlp(text) if not t.is_stop]
display(f"Spacy text without updated stop words: {text_without_stop_words}")

Объемный текст без обновленных стоп-слов:

['movie', 'not', 'good']

Нормализация

Как и любые данные, текст требует нормализации. В случае текста это:

  1. Преобразование дат в текст
  2. Цифры в текст
  3. Знаки валюты / процента в тексте
  4. Расширение сокращений (зависит от содержания) NLP - Natural Language Processing, нейролингвистическое программирование, нелинейное программирование
  5. Исправление орфографических ошибок

Подводя итог, нормализация - это преобразование любой нетекстовой информации в текстовый эквивалент.

Для этого существует отличная библиотека - normalize. Я покажу вам использование этой библиотеки из README. Эта библиотека основана на nltk пакете, поэтому ожидает nltk токенов слов.

Предположим, что этот текст для этого шага:

On the 13 Feb. 2007, Theresa May announced on MTV news that the rate of childhod obesity had risen from 7.3-9.6% in just 3 years , costing the N.A.T.O £20m

Код:

from normalise import normalise

user_abbr = {
    "N.A.T.O": "North Atlantic Treaty Organization"
}

normalized_tokens = normalise(word_tokenize(text), user_abbrevs=user_abbr, verbose=False)
display(f"Normalized text: {' '.join(normalized_tokens)}")

Выход:

On the thirteenth of February two thousand and seven , Theresa May announced on M T V news that the rate of childhood obesity had risen from seven point three to nine point six % in just three years , costing the North Atlantic Treaty Organization twenty million pounds

Хуже всего в этой библиотеке то, что сейчас вы не можете отключить некоторые модули, такие как расширение аббревиатуры, и это вызывает такие вещи, как MTV - ›M T V. Но я уже добавил соответствующую проблему в этот репозиторий, возможно, она будет исправлена ​​через некоторое время.

Лемматизация и пропаривание

Stemming - это процесс уменьшения перегиба в словах до их корневых форм, например, отображение группы слов на одну основу, даже если сама основа не является допустимым словом в Языке.

Lemmatization, в отличие от Stemming, уменьшает изменяемые слова должным образом, гарантируя, что корневое слово принадлежит языку. В лемматизации корневое слово называется леммой. Лемма (леммы или леммы множественного числа) - это каноническая форма, словарная форма или форма цитирования набора слов.

Предположим, что этот текст для этого шага:

On the thirteenth of February two thousand and seven , Theresa May announced on M T V news that the rate of childhood obesity had risen from seven point three to nine point six % in just three years , costing the North Atlantic Treaty Organization twenty million pounds

Стеммер НЛТК:

import numpy as np
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
tokens = word_tokenize(text)
porter=PorterStemmer()
# vectorizing function to able to call on list of tokens
stem_words = np.vectorize(porter.stem)
stemed_text = ' '.join(stem_words(tokens))
display(f"Stemed text: {stemed_text}")

Выделенный текст:

On the thirteenth of februari two thousand and seven , theresa may announc on M T V news that the rate of childhood obes had risen from seven point three to nine point six % in just three year , cost the north atlant treati organ twenti million pound

Лемматизация НЛТК:

import numpy as np
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenize
tokens = word_tokenize(text)
wordnet_lemmatizer = WordNetLemmatizer()
# vectorizing function to able to call on list of tokens
lemmatize_words = np.vectorize(wordnet_lemmatizer.lemmatize)
lemmatized_text = ' '.join(lemmatize_words(tokens))
display(f"nltk lemmatized text: {lemmatized_text}")

Лемматизированный текст NLTK:

On the thirteenth of February two thousand and seven , Theresa May announced on M T V news that the rate of childhood obesity had risen from seven point three to nine point six % in just three year , costing the North Atlantic Treaty Organization twenty million pound

Пространственная лемматизация:

import en_core_web_sm

nlp = en_core_web_sm.load()
lemmas = [t.lemma_ for t in nlp(text)]
display(f"Spacy lemmatized text: {' '.join(lemmas)}")

Просторный лемматизированный текст:

On the thirteenth of February two thousand and seven , Theresa May announce on M T v news that the rate of childhood obesity have rise from seven point three to nine point six % in just three year , cost the North Atlantic Treaty Organization twenty million pound

Мы видим, что spacy лемматизируется намного лучше, чем nltk, один из примеров risen - ›rise, только spacy справился с этим.

Многоразовый трубопровод

А теперь моя любимая часть! Мы собираемся создать многоразовый конвейер, который вы могли бы использовать в любом из своих проектов.

import numpy as np
import multiprocessing as mp

import string
import spacy 
import en_core_web_sm
from nltk.tokenize import word_tokenize
from sklearn.base import TransformerMixin, BaseEstimator
from normalise import normalise

nlp = en_core_web_sm.load()


class TextPreprocessor(BaseEstimator, TransformerMixin):
    def __init__(self,
                 variety="BrE",
                 user_abbrevs={},
                 n_jobs=1):
        """
        Text preprocessing transformer includes steps:
            1. Text normalization
            2. Punctuation removal
            3. Stop words removal
            4. Lemmatization
        
        variety - format of date (AmE - american type, BrE - british format) 
        user_abbrevs - dict of user abbreviations mappings (from normalise package)
        n_jobs - parallel jobs to run
        """
        self.variety = variety
        self.user_abbrevs = user_abbrevs
        self.n_jobs = n_jobs

    def fit(self, X, y=None):
        return self

    def transform(self, X, *_):
        X_copy = X.copy()

        partitions = 1
        cores = mp.cpu_count()
        if self.n_jobs <= -1:
            partitions = cores
        elif self.n_jobs <= 0:
            return X_copy.apply(self._preprocess_text)
        else:
            partitions = min(self.n_jobs, cores)

        data_split = np.array_split(X_copy, partitions)
        pool = mp.Pool(cores)
        data = pd.concat(pool.map(self._preprocess_part, data_split))
        pool.close()
        pool.join()

        return data

    def _preprocess_part(self, part):
        return part.apply(self._preprocess_text)

    def _preprocess_text(self, text):
        normalized_text = self._normalize(text)
        doc = nlp(normalized_text)
        removed_punct = self._remove_punct(doc)
        removed_stop_words = self._remove_stop_words(removed_punct)
        return self._lemmatize(removed_stop_words)

    def _normalize(self, text):
        # some issues in normalise package
        try:
            return ' '.join(normalise(text, variety=self.variety, user_abbrevs=self.user_abbrevs, verbose=False))
        except:
            return text

    def _remove_punct(self, doc):
        return [t for t in doc if t.text not in string.punctuation]

    def _remove_stop_words(self, doc):
        return [t for t in doc if not t.is_stop]

    def _lemmatize(self, doc):
        return ' '.join([t.lemma_ for t in doc])

Этот код можно использовать в конвейере sklearn.

Измеренная производительность: 2225 текстов было обработано на 4 процессах за 22 минуты. Даже близко к тому, чтобы быть быстрым! Это вызывает часть нормализации, библиотека недостаточно оптимизирована, но дает довольно интересные результаты и может принести дополнительную ценность для дальнейшей векторизации, поэтому вам решать, использовать ее или нет.

Надеюсь, вам понравился этот пост, и я с нетерпением жду вашего отзыва!

Ядро Kaggle: https://www.kaggle.com/balatmak/text-preprocessing-steps-and-universal-pipeline