Предобработка текстов на русском в PySpark

В одной из прошлых статей мы говорили о методах NLP (natural language processing) в PySpark. Сегодня мы покажем, как обработать реальный датасет, который содержит тексты на русском языке. Читайте у нас: удаление знаков пунктуации, символов и стоп-слов, токенизация и лемматизация на примере новостей на русском языке.

Датасет с текстами на русском

Воспользуемся датасетом, который содержит более 20000 новостей на русском языке от 4 новостных ресурсов (lenta.ru, meduza.io, ria.ru, tjournal.ru). Тексты новостей не очищены и могут содержать различные специальные символы. Скачать датасет можно на странице Kaggle или воспользоваться Kaggle API, о котором писали тут.

При чтение датасета нужно обязательно указать quote="\"", escape="\"", поскольку поля с текстовыми данными заключены в кавычки. Вот так это выглядит в Python:

import findspark
findspark.init()
from pyspark.sql import SparkSession
spark = SparkSession.builder.master("local[*]").getOrCreate()
df = spark.read.csv(
    'news.csv',
    inferSchema=True,
    header=True,
    multiLine=True,
    quote="\"",
    escape="\"")
# Первые 4 записи с некоторыми колонками
+--------+--------------------+--------------+--------------------+----+
|  source|               title|        rubric|                text|tags|
+--------+--------------------+--------------+--------------------+----+
|lenta.ru|      Синий богатырь|     Экономика|В 1930-е годы Сов...|null|
|lenta.ru|Загитова согласил...|         Спорт|Олимпийская чемпи...|null|
|lenta.ru|Объяснена опаснос...|      Из жизни|Российский врач-д...|null|
|lenta.ru|«Предохраняться? ...|Интернет и СМИ|В 2019 году телек...|null|
+--------+--------------------+--------------+--------------------+----+

Нас интересует столбец text, с ним мы и будем работать.

Уменьшаем текстовые данные

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

import pyspark.sql.functions as F
sentences = F.split(df['text'], '\.')
df = df.withColumn('sentence_1', sentences.getItem(0))
df = df.withColumn('sentence_2', sentences.getItem(1))

После этого столбцы с предложениями соединим в одно, используя специальную функцию concat. Сохраним полученный результат в столбец под названием sentence. Код на Python выглядит следующим образом:

df = df.withColumn('sentence', F.concat('sentence_1', 'sentence_2')) \
    .select('sentence')

Удаляем знаки пунктуации и символы

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

PySpark-функция regexp_replace заменит в столбце заданное регулярное выражение (очень часто в NLP применяются регулярные выражения). Внутри квадратных скобок перечислим символы и знаки пунктуации. Результат сохраним в столбец cleaned. Вот так это выглядит в Python:

pattern_punct = '[!@"“’«»#$%&\'()*+,—/:;<=>?^_`{|}~\[\]]'
df = df.withColumn('cleaned', 
regexp_replace('sentence', pattern_punct, ''))

(Необязательный шаг) Удаление ссылок и HTML-тэгов

Если вместо текстов с новостями вы работаете, например, с твитами, которые могут содержать все что угодно, то очень часто приходится избавляться от ссылок и HTML-тэгов. Для удаления ссылок регулярное выражение в Python может выглядеть следующим образом:

pattern_url = 'http[s]?://\S+|www\.\S+'
df = df.withColumn('text', regexp_replace('text', url_pattern, ''))

а для удаления тэгов HTML:

pattern_tags = '<.*?>'
df = df.withColumn('text', regexp_replace('text', pattern_tags, ''))

Токенизация

Очищенные текстовые данные можно разбить на токены (слова). Для этого используется NLP-токенизатор RegexTokenizer в PySpark. В аргумент pattern нужно указать регулярное выражение, которое будет разделителем при разбиении. В нашем случае таким регулярным выражением будут пробелы. Следующий код это демонстрирует:

from pyspark.ml.feature import RegexTokenizer
regexTokenizer = RegexTokenizer(inputCol="cleaned", outputCol="tokens", pattern=r"\s+")
df = regexTokenizer.transform(df)
# Вот так выглядят токены
+--------------------------------------------------------------------------------+
|                                                                          tokens|
+--------------------------------------------------------------------------------+
|[в, 1930-е, годы, советский, союз, охватила, лихорадка, в, десятилетие, бурно...|
|[олимпийская, чемпионка, по, фигурному, катанию, алина, загитова, согласилась...|
|[российский, врач-диетолог, римма, мойсенко, объяснила, почему, однообразное,...|
|[в, 2019, году, телеканал, ю, запустил, адаптацию, знаменитого, телешоу, бере...|
+--------------------------------------------------------------------------------+

Лемматизация и удаление стоп-слов

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

Для удаления стоп-слов можно воспользоваться PySpark-классом StopWordsRemover, как мы описывали здесь. Но мы создадим собственную функцию, которая будет приводить к начальной форме слово, проверяя не является ли это слово стоп-словом. Такой код будет более оптимальным, поскольку не придётся два раза проходиться по токенам.

Воспользуемся Python-библиотекой NLTK, которая содержит список стоп-слов для русского языка [1]. А лемматизацию проведём с помощью библиотеки Pymorphy2 [2]. В Pymorphy2 есть метод normal_forms, который возвращает список нормальных форм заданного слова.

Прежде всего скачаем стоп-слова NLTK, написав в Python следующее:

import nltk
nltk.download('stopwords')

В созданной функции мы отфильтруем также числа, проверив первый символ. Так, это поможет избавиться от «2019», но не от Covid-19. Итоговая функция выглядит так:

import pymorphy2
from nltk.corpus import stopwords
morph = pymorphy2.MorphAnalyzer()
ru_stopwords = stopwords.words('russian')
digits = [str(i) for i in range(10)]
def preprocess(tokens):
    return [morph.normal_forms(word)[0]
            for word in tokens
                if (word[0] not in digits and
                    word not in ru_stopwords)]

Эту функцию нужно передать в udf (user defined function). Вторым аргументом udf принимает тип возвращаемой функции. В нашем случае это массив строк. Сохраним результат в столбец finished:

from pyspark.sqk.types import ArrayType, StringType

preprocess_udf = F.udf(preprocess, ArrayType(StringType()))
df = df.withColumn('finished', preprocess_udf('tokens'))
# Первые 4 строки
+--------------------------------------------------------------------------------+
|                                                                        finished|
+--------------------------------------------------------------------------------+
|[год, советский, союз, охватить, лихорадка, десятилетие, бурный, индустриализ...|
|[олимпийский, чемпионка, фигурный, катание, алина, загитов, согласиться, стат...|
|[российский, врач-диетолог, римма, мойсенко, объяснить, почему, однообразный,...|
|[год, телеканал, ю, запустить, адаптация, знаменитый, телешоу, беременный, ко...|
+--------------------------------------------------------------------------------+

Как видим, теперь слова находятся в начальной форме.

Подробнее о лемматизации, удалении стоп-слов и других методах NLP в PySpark на реальных примерах Data Science вы узнаете на специализированном курсе «PNLP: NLP – обработка естественного языка с Python» в нашем лицензированном учебном центре обучения и повышения квалификации разработчиков, менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data в Москве.

Источники

  1. https://www.nltk.org/

  2. https://pymorphy2.readthedocs.io/en/stable/user/guide.html

Поиск по сайту