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