Kaggle: визначення тональності текстів

Привіт, хабр!



#{Data Science для новачків}

Мене звуть Гліб Морозів, ми з Вами вже знайомі по попереднім статтям. На численні прохання продовжую описувати досвід своєї участі в освітніх проектах MLClass.ru (до речі, хто ще не встиг — рекомендую завантажити матеріали, поки вони ще доступні).

Дані
Дані для роботи надані в рамках змагання Bag of Words проходить на сайті Kaggle і являють собою навчальну вибірку з 25000 оглядів з сайту IMBD кожен з яких отнесе з одного з класів: негативний/позитивний. Завдання передбачити, до якого з класів належить кожен огляд з тестової вибірки.

library(magrittr)
library(tm)
require(plyr)
require(dplyr)
library(ggplot2)
library(randomForest)

Завантажимо дані в оперативну пам'ять.

data_train <- read.delim("labeledTrainData.tsv",header = TRUE, sep = "\t",
quote = "", stringsAsFactors = F)

Отримана таблиця складається з трьох стовпців: id, sentiment і review. Саме останній стовпець і є об'єктом нашої роботи. Подивимося, що ж із себе представляє сам огляд. (т. к. огляд досить довгий я наведу тільки перші 700 знаків)

paste(substr(data_train[1,3],1,700),"...")
## [1] "\"With all this stuff going down at the moment with MJ i've started listening to his music, watching the odd documentary here and there, watched The Wiz and watched Moonwalker again. Maybe i just want to get a certain insight into this guy who i thought was really cool in the eighties just to maybe make up my mind whether he is guilty or innocent. Moonwalker is part biography, part feature film which i remember going to see at the cinema when it was originally released. Some of it has subtle messages about MJ's feeling towards the press and also the obvious message of drugs are bad el kay.<br /><br />Visually impressive but of course this is all about Michael Jackson so unless you remotely lik ..."

Видно, що у тексті присутня сміття у вигляді HTML тегів.

Bag of Words
Bag of Words або мішок слів — це модель часто використовується при обробці текстів, що представляє собою невпорядкований набір слів, що входять в оброблюваний текст. Часто модель представляють у вигляді матриці, в якій рядки відповідають окремим текстом, а стовпці — вхідні в нього слова. Клітинки на перетині є числом входження даного слова у відповідний документ. Дана модель зручна тим, що переводить людську мову слів у зрозумілий для компьтера мову цифр.

Обробка даних

Для обробки даних я буду використовувати можливості пакета tm. У наступному блоці коду виробляються наступні дії:

  • створюється вектор з текстів
  • создеется корпус — колекція текстів
  • всі букви приводяться до рядковим
  • видаляються знаки пунктуації
  • видаляються так звані «стоп-слова», т. к. часто зустрічаються слова в мові, не несуть самі по собі інформації (в англійській мові, наприклад and) Крім цього я вирішив прибрати слово, яке, напевно, буде часто зустрічатися в оглядах, але інтересу для моделі не представляє — movie.
  • проводиться стеммирование, тобто слова перетворюються на свою основну форму


train_corpus <- data_train$review %>% VectorSource(.)%>%
Corpus(.) %>% tm_map(., tolower) %>% tm_map(., PlainTextDocument) %>%
tm_map(., removePunctuation) %>% 
tm_map(., removeWords, c("movie", stopwords("english"))) %>%
tm_map(., stemDocument)

Тепер створимо частотну матрицю.

frequencies <- DocumentTermMatrix(train_corpus)
frequencies
## <<DocumentTermMatrix (documents: 25000, terms: 92244)>>
## Non-/sparse entries: 2387851/2303712149
## Sparsity : 100%
## Maximal term length: 64
## Weighting : term frequency (tf)

Наша матриця містить більше 90000 термінів, тобто модель на її основі буде містити 90000 ознак! Її необхідно зменшувати і для цього використовуємо той факт, що в ній дуже багато рідко зустрічаються в оглядах слів, тобто вона розряджена (термін sparse). Я вирішив скоротити її дуже сильно (для того, щоб модель вмістилася в оперативній пам'яті з урахуванням 25000 об'єктів навчальної вибірки) і залишити тільки ті слова, що зустрічаються мінімум в 5% оглядів.

sparse <- removeSparseTerms(frequencies, 0.95)
sparse
## <<DocumentTermMatrix (documents: 25000, terms: 373)>>
## Non-/sparse entries: 1046871/8278129
## Sparsity : 89%
## Maximal term length: 10
## Weighting : term frequency (tf)

У підсумку в матриці залишилося 373 терміна. Перетворимо матрицю в data frame і додамо стовпець з цільовим ознакою.

reviewSparse = as.data.frame(as.matrix(sparse))
vocab <- names(reviewSparse)
reviewSparse$sentiment <- data_train$sentiment %>% as.factor(.) %>% 
revalue(., c("0"="neg", "1" = "pos"))
row.names(reviewSparse) <- NULL

Тепер навчимо Random Forest модель на отриманих даних. Я використовую 100 дерев у зв'язку з обмеженням оперативної пам'яті.

model_rf <- randomForest(sentiment ~ ., data = reviewSparse, ntree = 100)

Використовуючи навчену модель створимо прогноз для тестових даних.

data_test <- read.delim("testData.tsv", header = TRUE, sep = "\t",
quote = "", stringsAsFactors = F)
test_corpus <- data_test$review %>% VectorSource(.)%>%
Corpus(.) %>% tm_map(., tolower) %>% tm_map(., PlainTextDocument) %>%
tm_map(., removePunctuation) %>% 
tm_map(., removeWords, c("movie", stopwords("english"))) %>%
tm_map(., stemDocument)
test_frequencies <- DocumentTermMatrix(test_corpus,control=list(dictionary = vocab))
reviewSparse_test <- as.data.frame(as.matrix(test_frequencies))
row.names(reviewSparse_test) <- NULL
sentiment_test <- predict(model_rf, newdata = reviewSparse_test)
pred_test <- as.data.frame(cbind(data_test$id, sentiment_test))
colnames(pred_test) <- c("id", "sentiment")
pred_test$sentiment %<>% revalue(., c("1"="0", "2" = "1"))
write.csv(pred_test, file="Submission.csv", quote=FALSE, row.names=FALSE)

Після завантаження та оцінки на сайті Kaggle модель отримала оцінку за статистикою AUC — 0.73184.

Спробуємо підійти до проблеми з іншого боку. При складанні частотної матриці і обрізанні її ми залишаємо найбільш часто зустрічаються слова, але, швидше за все, багато слів, які часто зустрічаються в оглядах фільмів, але не відображають настрій огляду. Наприклад такі слова як movie, film і т. д. Але, оскільки у нас є навчальна вибірка з зазначеним настроєм оглядів, можна виділити слова, частоти яких істотно розрізняються у негативних і позитивних відгуків.

Для початку створимо частотну матрицю для негативних оглядів.

freq_neg <- data_train %>% filter(sentiment == 0) %>% select(review) %>% VectorSource(.)%>%
Corpus(.) %>% tm_map(., tolower) %>% tm_map(., PlainTextDocument) %>%
tm_map(., removePunctuation) %>%
tm_map(., removeNumbers) %>%
tm_map(., removeWords, c(stopwords("english"))) %>%
tm_map(., stemDocument) %>% DocumentTermMatrix(.) %>% 
removeSparseTerms(., 0.999) %>% as.matrix(.)
freq_df_neg <- colSums(freq_neg)
freq_df_neg <- data.frame(word = names(freq_df_neg), freq = freq_df_neg)
rownames(freq_df_neg) <- NULL
head(arrange(freq_df_neg, desc(freq)))
## word freq
## 1 movi 27800
## 2 film 21900
## 3 one 12959
## 4 like 12001
## 5 just 10539
## 6 make 7846

І для позитивних оглядів.

freq_pos <- data_train %>% filter(sentiment == 1) %>% select(review) %>% VectorSource(.)%>%
Corpus(.) %>% tm_map(., tolower) %>% tm_map(., PlainTextDocument) %>%
tm_map(., removePunctuation)%>%
tm_map(., removeNumbers) %>%
tm_map(., removeWords, c(stopwords("english"))) %>%
tm_map(., stemDocument) %>% DocumentTermMatrix(.) %>% 
removeSparseTerms(., 0.999) %>% as.matrix(.)
freq_df_pos <- colSums(freq_pos)
freq_df_pos <- data.frame(word = names(freq_df_pos), freq = freq_df_pos)
rownames(freq_df_pos) <- NULL 
head(arrange(freq_df_pos, desc(freq)))

## word freq
## 1 film 24398
## 2 movi 21796
## 3 one 13706
## 4 like 10138
## 5 time 7889
## 6 good 7508

Об'єднаємо отримані таблиці і порахуємо різницю між частотами.

freq_all <- merge(freq_df_neg, freq_df_pos, by = "word", all = T)
freq_all$freq.x[is.na(freq_all$freq.x)] <- 0
freq_all$freq.y[is.na(freq_all$freq.y)] <- 0
freq_all$diff <- abs(freq_all$freq.x - freq_all$freq.y)
head(arrange(freq_all, desc(diff)))

## word freq.x freq.y diff
## 1 movi 27800 21796 6004
## 2 bad 7660 1931 5729
## 3 great 2692 6459 3767
## 4 just 10539 7109 3430
## 5 love 2767 5988 3221
## 6 even 7707 5056 2651

Відмінно! Ми бачимо, як і очікувалося, серед слів з найбільшою різницею такі терміни як bad, great та love. Але тут і просто часто зустрічаються слова, як movie. Це сталося, що у частих слів навіть невелика відсоткова різниця видає високу абсолютну різницю. Для того, щоб усунути цей недолік, нормалізуємо різницю, розділивши її на суму частот. Вийшла метрика буде лежати в інтервалі між 0 та 1, і чим вище значення — тим важливіше значення у визначенні різниці між позитивними і негативними відгуками. Але що ж робити зі словами, які зустрічаються тільки у одного класу відгуків і при цьому їх частота мала? Для зменшення їх важливості додамо до знаменника коефіцієнт.

freq_all$diff_norm <- abs(freq_all$freq.x - freq_all$freq.y)/
(freq_all$freq.x +freq_all$freq.y + 300)
head(arrange(freq_all, desc(diff_norm)))

## word freq.x freq.y diff diff_norm
## 1 worst 2436 246 2190 0.7344064
## 2 wast 1996 192 1804 0.7250804
## 3 horribl 1189 194 995 0.5912062
## 4 stupid 1525 293 1232 0.5816808
## 5 bad 7660 1931 5729 0.5792134
## 6 wors 1183 207 976 0.5775148

Відберемо 500 слів з найвищим показником коефіцієнта різниці.

freq_word <- arrange(freq_all, desc(diff_norm)) %>% select(word) %>% slice(1:500)

Використовуємо отриманий словник для створення частотної матриці, на якій навчимо Random Forest модель.

vocab <- as.character(freq_word$word)
frequencies = DocumentTermMatrix(train_corpus,control=list(dictionary = vocab))
reviewSparse_train <- as.data.frame(as.matrix(frequencies))
row.names(reviewSparse_train) <- NULL
reviewSparse_train$sentiment <- data_train$sentiment %>% as.factor(.) %>%
revalue(., c("0"="neg", "1" = "pos"))

model_rf <- randomForest(sentiment ~ ., data = reviewSparse_train, ntree = 100)

Після завантаження та оцінки на сайті Kaggle модель отримала оцінку за статистикою AUC — 0.83120, тобто попрацювавши з ознаками ми отримали поліпшення статистики на 10%!

TF-IDF

При створенні матриці документ-термін в якості метрики важливості слова ми використовували просто частоту появи слова в огляді. У пакеті tm є можливість використовувати іншу міру, звану tf-idf. TF-IDF (від англ. TF — term frequency, IDF — inverse document frequency) — статистичний показник, що використовується для оцінки важливості слова в контексті документа, який є частиною колекції документів або корпусу. Вага деякого слова пропорційний кількості вживання цього слова в документі, і обернено пропорційний частоті вживання слова в інших документах колекції.

Використовуючи tf-idf, створимо словник з 500 термінів з найбільш високим показником даної метрики. Для того, щоб цей словник найбільш релевантне відображав важливість слів, будемо використовувати додаткову навчальну вибірку, у якій не містить настрій оглядів. На базі отриманого словника створимо матрицю документ-термін і навчимо модель.

data_train_un <- read.delim("unlabeledTrainData.tsv",header = TRUE, sep = "\t",
quote = "", stringsAsFactors = F)
train_review <- c(data_train$review, data_train_un$review)
train_corpus <- train_review %>% VectorSource(.)%>%
Corpus(.) %>% tm_map(., tolower) %>% tm_map(., PlainTextDocument) %>%
tm_map(., removePunctuation) %>% tm_map(., removeNumbers) %>%
tm_map(., removeWords, c(stopwords("english"))) %>%
tm_map(., stemDocument)
tdm <- TermDocumentMatrix(train_corpus,
control = list(weighting = function(x) weightTfIdf(x, normalize = F)))
library(slam)
freq <- rollup(tdm, 2,FUN = sum)
freq <- as.matrix(freq)
freq_df <- data.frame(word = row.names(freq), tfidf = freq)
names(freq_df) <- c("word", "tf_idf")
row.names(freq_df) <- NULL
freq_df %<>% arrange(desc(tf_idf))
vocab <- as.character(freq_df$word)[1:500]
train_corpus <- data_train$review %>% VectorSource(.)%>%
Corpus(.) %>% tm_map(., tolower) %>% tm_map(., PlainTextDocument) %>%
tm_map(., removePunctuation) %>% tm_map(., removeNumbers) %>%
tm_map(., removeWords, c(stopwords("english"))) %>%
tm_map(., stemDocument)
frequencies = DocumentTermMatrix(train_corpus,control=list(dictionary = vocab,
weighting = function(x) weightTfIdf(x, normalize = F) ))
reviewSparse_train <- as.data.frame(as.matrix(frequencies))
rm(data_train_un, tdm, dtm, train_review)
reviewSparse_train <- as.data.frame(as.matrix(frequencies))
row.names(reviewSparse_train) <- NULL
colnames(reviewSparse_train) = make.names(colnames(reviewSparse_train))
reviewSparse_train$sentiment <- data_train$sentiment %>% as.factor(.) %>%
revalue(., c("0"="neg", "1" = "pos"))
rm(data_train, train_corpus, freq, freq_df)
model_rf <- randomForest(sentiment ~ ., data = reviewSparse_train, ntree = 100)


Використовуємо дану модель на тестовій вибірці і отримаємо значення AUC — 0.81584.

Висновок
Дана робота являє собою один з можливих варіантів створення предсказательная моделі на основі текстових даних. Одним з варіантів поліпшити якість моделі може бути збільшення кількості використовуваних термінів з матриці документ-термін, але цей шлях вимагає істотного збільшення використовуваних машинних ресурсів. Також може привести до набагато кращим результатами звернутися не до частот слів, а до їх значень і зв'язків між ними. Для цього треба звернутися до моделі word2vec. Крім цього, велике поле для дослідження являє собою розгляд термінів у контексті документа

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.