Содержание
В одной из прошлых статей мы говорили о методах 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 в Москве.
Источники


