27 ноября 2014

Делимся опытом: особенности подготовки русскоязычных текстовых документов к анализу в среде R



Сегодня я запускаю еще одну новую рубрику – «Делимся опытом», идея которой состоит в публикации гостевых сообщений, написанных читателями блога. Как следует из названия, в этих сообщениях будут публиковаться небольшие «рецепты» решения конкретных задач и проблем, возникающих при работе с R. Если у вас информация, которой, как вам кажется, стоит поделиться с другими – пожалуйста, свяжитесь со мной по электронной почте (адрес можно найти в разделе «Обо мне»). Я с удовольствием рассмотрю любое предложение. Главным критерием при отборе потенциальных публикаций является их оригинальность - в том смысле, что они предлагают описание нетривиальных проблем, решение которых не удается найти путем быстрого Google-поиска или на Q&A-форумах вроде StackOverflow и CrossValidated (т.е. включая запросы на английском языке). Дисклеймер: я оставляю за собой право отклонить любое предложение без объяснения причин. 

С радостью представляю первое гостевое сообщение, автором которого является Михаил Сидоренко (Украина). Михаил – психолог по образованию. Он использовал R в последние два года при работе над проектами, имеющими отношение к психологическим и маркетинговым исследованиям, а с недавних пор – также к исследованиям социальных медиа.


*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 

Михаил Сидоренко, 27.11.2014 г. ©

Эта статья посвящена некоторым особенностям подготовки русскоязычных текстовых документов к анализу в среде R и тесно связана с ранее обсуждавшейся на этом сайте темой анализа Twitter-сообщений.

При работе с русскоязычными текстовыми данными автор столкнулся с рядом проблем. В качестве примера рассмотрим несколько отзывов о фильме «Тринадцатый этаж» (материалы взяты на одном из онлайн видео-ресурсов). Отзывы были собраны в текстовый вектор, который имеет следующий вид:

> film 
[1] "Очень приятное впечатление от фильма. Удивительно почему я ничего раньше не слышал о нем! Все на высшем уровне. 10/10. Смотреть обязательно." 
[2] "Фильм очень понравился! Идея очень хорошая, сюжет мягкий, фильм легко восприниматься. Фильмы прошлого века были действительно со смыслом.На данный момент это редкость!" 
[3] "Фильм замечательный. Режиссёр умница" 
[4] "Доволі непоганий фільм,але на 1 раз) Цікава ідея)" 

Набор комментариев подобран не случайно. В нем содержатся примеры всех возможных проблемных конструкций, которые могут быть некорректно обработаны с использование готовых R-решений – в частности, средствами пакета tm.Обратите также внимание на то, что в четвертой строке представлен отзыв на украинском языке – исключать его из анализа было бы нецелесообразно.

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

#  Сформируем корпус из текстового вектора с нужной кодировкой
f.corpus <- Corpus(VectorSource(film, encoding = "UTF-8"))
 
# Приведем все буквы в нижний регистр
f.corpus <- tm_map(f.corpus, tolower)
 
# Удалим знаки пунктуации
f.corpus <- tm_map(f.corpus, removePunctuation)
 
# Просмотрим результат
inspect(f.corpus)
 
A corpus with 4 text documents
 
The metadata consists of 2 tag-value pairs and a data frame
Available tags are:
  create_date creator 
Available variables in the data frame are:
  MetaID 
 
[[1]]
оень приятное впеатление от фильма удивительно поему я ниего раньше не слышал о нем все на высшем уровне 1010 смотреть обязательно
[[2]]
фильм оень понравился идея оень хорошая сюжет мягкий фильм легко восприниматься фильмы прошлого века были действительно со смысломна данный момент это редкость
[[3]]
фильм замеательный режисср умница
[[4]]
довол непоганий фльмале на 1 раз цкава дея

По своей структуре корпус, как объект R, является чем-то вроде сложно организованного списка, и поэтому для извлечения отдельных документов из него используется индексирование через двойные квадратные скобки – [[номер документа]]. Для просмотра всего набора документов применяется функция inspect(), которая позволяет работать с корпусом как с текстовым вектором, т.е. извлекать весь набор текст текстов или отдельные документы путем индексирования последних с использование одиночных квадратных скобок – [номер документа].

Как видим из приведенного выше примера, все слова, в которых содержатся буквы «ч» и «ё», после удаления знаков препинания были отображены некорректно. Буква «ч» выпала из слов «очень», «впечатление», «замечательно» (первые две строки). Слово «режиссёр» (третья строка) лишилось буквы «ё». Кроме того, удаление знаков пунктуации местами привело к слипанию двух слов в одно. Например, в первой строке оценка фильма «10/10» превратилась в «1010». Во второй строке пользователь поставил запятую после слова «смыслом», но не поставил пробел перед «на» – в результате получилось «смысломна». Наконец, в комментарии на украинском языке была утеряна буква «і», а удаление запятой без пробела образовало маловразумительную конструкцию «фльмале».

Как результат, выпадение букв «ч» и «ё» влечет за собой также некорректное удаление стоп-слов: 

# Удалим стоп-слова из первой строки и выведем результат (список стоп-слов был взят здесь):
removeWords(film[1], myStopWord)
[1] "Очень приятное впечатление  фильма. Удивительно чему   раньше  слышал  ! Все  высшем уровне. 10/10. Смотреть обязательно."

Видим, что слово «почему» раскололось надвое. Частица «по» была удалена, а частица «чему» – оставлена.

В результате поисков причин выпадения букв было обнаружено, что корень проблемы скрывается в кодировке текста. Очевидно, что программа R изначально настроена на работу с англоязычным текстом. Поэтому буквы «ч» и «ё» распознаются как символы, не принадлежащие алфавиту, и удаляются при очистке текста вместе со знаками пунктуации. Для корректной работы с русскоязычным текстом необходимо выполнить перекодировку в UTF-8 при помощи стандартной R-функции enc2utf8(), т.е. операцию вида x <- enc2utf8(x)

При дальнейших манипуляциях со строками все буквы русского алфавита, включая «ч» и «ё», будут сохранены. Теоретически, такая перекодировка должна выполнятся на этапе формирования корпуса при помощи функции Corpus() – для этого служит ее аргумент encoding (см. выше), но на деле это почему-то не работает.

Далее рассмотрим несколько полезных функций для манипуляции с текстовыми строками. Перекодированный текстовый вектор можно преобразовать в корпус, и продолжить работу, используя средства пакета tm. Однако более корректный результат получается при работе с набором документов в виде векторного объекта, к которому применяются функции с регулярными выражениями (подробнее см. ?regex). Преимущество данного подхода состоит в том, что процесс чистки текста становится более контролируемым. В частности, этот способ помогает решить проблему «слипания» слов. Для манипуляции с текстовыми строками в R предусмотрен набор стандартных функций, однако более удобным подходом является использование пакета stringr, разработанного Хэдли Уикхемом - автором таких известных пакетов, как dplyr и ggplot2.

# Перекодируем наш текстовый вектор и приведем буквы в нижний регистр:
film.utf <- enc2utf8(film)
film.utf <- tolower(film.utf)
 
# Устанавим и запустим нужный пакет:
install.packages("stringr")
require(stringr)
 
# Выполним очистку текста от знаков пунктуации
film.utf <- str_replace_all(film.utf, "[[:punct:]]", " ")
film.utf <- str_replace_all(film.utf, "\\s+", " ")
film.utf <- str_trim(film.utf, side = "both")

Последние три строки кода выполняют удаление знаков препинание в три шага. Первая функция, str_replace_all(), работает следующим образом: она выбирает из текстового вектора film.utf все знаки пунктуации согласно регулярному выражению [[:punct:]] и заменяет их пробелом. Замене подлежат следующие символы:

! " # % & ' ( ) * + , - . / : ;
Использование пробела в качестве замены избавляет нас от вероятного слипания текстовых конструкций, но приводит к образованию длинных пропусков между словами. Поэтому вторая функция находит длинные пробелы согласно регулярному выражению "\\s+" и заменяет их обычными. Наконец, третья строка кода убирает пробелы вначале и в конце каждом документе.

Похожим образом из текста удаляются цифры. Для этого нужно заменить паттерн "[[:punct:]]" в первой строке приведенного выше кода на "[[:digit:]]" или "[0-9]". Аналогично, для удаления английских слов следует использовать регулярное выражение "[[:alpha:]]" или "[a-z]"

После очистки текста от знаков пунктуации и чисел можно приступить к удалению стоп-слов. Заметим, что формировать корпус для этого тоже необязательно. Можно просто применить функцию removeWords() из пакета tm к текстовому вектору. Для примера составим список потенциальных стоп-слов только для первого комментария и поработаем с ним.

# Создаем список стоп-слов, и сохраняем его в виде текстового вектора
stopWord.film <- c("от", "я", "не", "все", "на", "очень", "ничего", 
                  "почему", "раньше", "о", "слышал", "нем")
 
# Производим чистку всего текстового массива от стоп-слов
film.clean <- removeWords(film.clean, stopWord.film)
 
# Посмотрим на результат по первой строке
film.clean[1]
[1] " приятное впечатление  фильма удивительно         высшем уровне смотреть обязательно"

Содержание строки достаточно полно отображает мнение пользователя о фильме в виде «ключевых» слов, однако в сообщении образовались лишние пробелы. Уберем их уже известным нам способом:

# Убираем пробелы и смотрим результат
film.clean <- str_replace_all(film.clean, "\\s+", " ")
film.clean[1]
[1] " приятное впечатление фильма удивительно высшем уровне смотреть обязательно"

Почти то, что нужно. Остается убрать лишний пробел вначале строки, который образовался после удаления слова «Очень».

# Убираем крайние пробелы в строках, и получаем корректно очищенный комментарий
film.clean <- str_trim(film.clean, side = "both")
film.clean[1]
[1] "приятное впечатление фильма удивительно высшем уровне смотреть обязательно"

Чистка текста окончена! Все буквы на месте, все слова целы. Теперь можно сформировать из «чистого» текстового вектора корпус, и спокойно приступить к анализу.

Более подробно о манипуляции над текстовыми строками можно прочесть в руководстве Sanchez, G. (2013) Handling and Processing Strings in R. В том же руководстве имеются рецепты для удаления специфических текстовых конструкций, как например, e-mail адреса или интернет-ссылки.



6 комментариев :

Сергей комментирует...

Честно говоря, у меня очень плохие впечатления остались после работы с пакетом tm. Основная претензия - работает ну очень медленно при более ли менее приличном объеме корпуса (500 метров). В конце концов стал использовать RWeka, чтобы хоть как ускорить это дело + doShow для пареллельных вычислений.

Анонимный комментирует...

Сергей,
почему-то у меня не получилось загрузить пакет RWeka. Вот что выдает R:
> library("RWeka", lib.loc="C:/Program Files/R/R-3.1.2/library")
Error : .onLoad не удалось в loadNamespace() для 'rJava', подробности:
вызов: fun(libname, pkgname)
ошибка: JAVA_HOME cannot be determined from the Registry
Error: не удалась загрузка пакета или пространства имен для ‘RWeka’

Пытался переустанавливать пакеты, обновлял R и RStudio - результат тот же.

Хотелось бы узнать, на сколько легко было перейти на RWeka после tm? Есть ли у данного пакеты другие преимущества, помимо скорости работы. Я бегло просмотрел справку, мне показалось, что в RWeka все не на столько интуитивно понятно как в tm, в плане функционала - хотелось бы узнать Ваши комментарии по этому поводу.

И я не совсем понял насчет параллельных вычислений, как это работает? (к стати, поиск выдал doSnow - вероятно у Вас опечатка).

anton0xf комментирует...

очевидно, RWeka зависит от rJava, для работы которой необходима установленная JRE (https://java.com/en/download/)

Pavel Shchelin комментирует...

Уважаемый Сергей,
Подскажите пожалуйста как решить проблему:
"> tw.df <- twListToDF(tweets)
> tweets <- as.vector(sapply(tw.df$text, RemoveAtPeople))
> tw.corpus <- Corpus(VectorSource(tweets, encoding = "UTF-8"))
Ошибка в VectorSource(tweets, encoding = "UTF-8") :
неиспользованный аргумент (encoding = "UTF-8")"
С уважением Павел

Sergey Mastitsky комментирует...

Павел, посмотрите, пожалуйста здесь:
http://stackoverflow.com/questions/24920396/r-corpus-is-messing-up-my-utf-8-encoded-text
и здесь:
http://molbiol.ru/forums/index.php?showtopic=102724&st=1650

Pavel Shchelin комментирует...

Уважаемый Сергей, увы не помогает.
Вот мой код.
searchTerm="#panamapapers"
rdmTweets <- searchTwitter(searchTerm, n = 2000, lang="ru")
tw.df <- twListToDF(rdmTweets)
tweets <- as.vector(sapply(tw.df$text, RemoveAtPeople))
//до этого момента все хорошо, но потом
tw.corpus <- Corpus(VectorSource(tweets))
tw.corpus
// и вот в этом корпусе у меня уже проблема в том, что программа перестает понимать текст и уже на следующей команде
tw.corpus <- tm_map(tw.corpus, tolower)
Выдает ошибку
"Предупреждение:
В mclapply(content(x), FUN, ...) :
all scheduled cores encountered errors in user code"

Пользюсь RStudio
Mac OS

Отправить комментарий