情緒/情感分析(sentiment analysis),簡單來說就是辨別話語中的情感。
最一開始的方式就像把文字拆解成積木,這塊積木是正面的、那塊積木也是正面的,相加在一起句子就會是正面的。例如「我今天吃到好吃的食物,心情很好。」句子裡面有「好吃」和「好」兩個詞,所以整句句子就是正面。除了正面以外,還有負面和中性的詞。
因此,我們要先準備一個情感辭典(dictionary),裡面有常見的正面與負面詞,把辭典和斷詞後的結果串連,就可以得到一段文本的情感。
不過,這種方法很快就碰上挑戰,例如一個常見的抵消問題,「我雖然吃到好吃的食物,卻不快樂。」從人類判斷來看,這句話應該是負面,但從詞典法卻會因為同時出現「好吃」和「不快樂」兩個詞,將情感分數抵銷,這樣這句話就變得中性了。另外還會有辭典收錄詞彙不足的問題,譬如說年輕人的流行用語、隨著時代出現新意涵的詞彙等,所以後來出現機器學習方式,建構分類模型。
這種機器學習方法屬於監督式方法,我們要先讓模型知道怎麼樣的文章屬於正向、怎麼樣的文章屬於負向,才能往後作答。模型中的參數可以很廣,除了每個出現的詞彙都能當作特徵(feature)以外,我們可以加入額外更多變數,例如出現的形容詞比例、動詞比例,還有句子的平均長短、文章的難易度,甚至是出現多少命名實體、使用幾個驚嘆號,都是考慮範圍。先請人標記好資料後,我們就可以拿著這份標記資料訓練模型,請它就著特徵,找到重要的變數與模式。
利用機器學習辨識情感,在深度學習暴衝前,也是Kaggle上、、產業界和學術論文中常見的作法。不過,他也有挑戰,包含樣本數過少、沒有標記經費、出現新樣本等幾個問題。樣本數過少,代表難以訓練模型;沒有標記經費,則代表根本沒辦法訓練資料;出現新樣本指的是若模型挑選特定詞彙當成變數,遇到新資料時未必有相關詞彙,則模型的擴展性就會是個問提。
因此,深度學習的蓬勃發展,正好能解掉機器學習模型無法克服的困難。小樣本不是問題、標記經費還能下降,再加上擴展性高,讓深度學習模型判斷情感,變成最佳實務。
當然,本文還是要從頭開始探討起。
我們先來看預先準備好的詞典長什麼樣子,正面和負面詞彙都各取10個出來,讓你參考一下。
library(tidyverse)
df_sentiment <- read_rds("data/sentiment/df_sentiment.rds")
df_sentiment %>% sample_n(200) %>%
group_by(type) %>% mutate(rn = row_number()) %>%
filter(rn <= 10) %>% ungroup() %>%
pivot_wider(names_from = type, values_from = word)
## # A tibble: 10 × 3
## rn neg pos
## <int> <chr> <chr>
## 1 1 再三再四 悄悄
## 2 2 被壓壞 洞鑒古今
## 3 3 作低沈聲 貫斗雙龍
## 4 4 不劣方頭 追遠慎終
## 5 5 妖孽 借身報仇
## 6 6 次貨 遠愁近慮
## 7 7 懷恨 歸真反樸
## 8 8 扭斗 風度翩翩
## 9 9 無名小卒 家弦戶誦
## 10 10 惡煞 通元識微
那具體情感分析怎麼做呢?就像前面說的,我們要利用資料表的串接(data
joining),在文本資料中「貼上情感標籤」。
和前面步驟一樣,先分詞後,再來串資料。
library(tidytext)
library(jiebaR)
library(lubridate)
df_speech_clean <- read_csv("data/df_speech_clean.csv")
## Rows: 24 Columns: 5
## ── Column specification ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): text, title, president
## dbl (1): id
## date (1): date
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
df_speech_pre <-
df_speech_clean %>%
mutate(text = str_to_lower(text)) %>%
mutate(text = str_remove_all(text, " |\\n|\\r|\\t")) %>%
mutate(text = str_replace_all(text, "台灣", "臺灣")) #%>%
df_stop <- read_table("data/停用詞-繁體中文.txt", col_names = F) %>% rename(stopword = 1)
##
## ── Column specification ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
## cols(
## X1 = col_character()
## )
### segment
cutter <- worker("tag", stop_word = "data/停用詞-繁體中文.txt")
vector_word <- c("中華民國", "李登輝", "蔣中正", "蔣經國", "李登輝", "陳水扁", "馬英九")
new_user_word(cutter, words = vector_word)
## [1] TRUE
# reg_space <- "%E3%80%80" %>% curl::curl_escape()
### text part
df_speech_seg <-
df_speech_pre %>%
mutate(text = str_replace_all(text, "台灣|臺灣", "臺灣")) %>%
mutate(text = str_remove_all(text, "\\n|\\r|\\t|:| | ")) %>%
# mutate(text = str_remove_all(text, reg_space)) %>%
mutate(text = str_remove_all(text, "[a-zA-Z0-9]+")) %>%
mutate(text_segment = purrr::map(text, function(x)segment(x, cutter))) %>%
mutate(text_POS = purrr::map(text_segment, function(x)names(x))) %>%
unnest(c(text_segment, text_POS)) %>%
select(-text, everything(), text) %>%
anti_join(df_stop, by = c("text_segment" = "stopword"))
這步就是要來串資料了!
df_speech_seg %>% select(id, text_segment) %>%
left_join(df_sentiment, by = c("text_segment" = "word")) %>%
count(id, type)
## Warning in left_join(., df_sentiment, by = c(text_segment = "word")): Detected an unexpected many-to-many relationship between `x` and `y`.
## ℹ Row 121 of `x` matches multiple rows in `y`.
## ℹ Row 2120 of `y` matches multiple rows in `x`.
## ℹ If a many-to-many relationship is expected, set `relationship = "many-to-many"` to silence this warning.
## # A tibble: 72 × 3
## id type n
## <dbl> <chr> <int>
## 1 1 neg 8
## 2 1 pos 91
## 3 1 <NA> 142
## 4 2 neg 12
## 5 2 pos 74
## 6 2 <NA> 116
## 7 3 neg 31
## 8 3 pos 160
## 9 3 <NA> 292
## 10 4 neg 31
## # ℹ 62 more rows
如上表格所顯現的,有超過一半的詞其實都不帶有情緒,因此type
欄位會是NA
。詞典法的下一步就是直接把正面和負面詞相加,負面詞因為是負面所以情感分數會是-1。最後得到的總分就是文章、段落、句子的情感分數了。