文字探勘 - 創造專屬字典

一般來說,我們在處理文字斷詞時可以直接使用像是jieba這類型的套件,因他已收集足夠豐富的字詞,所以斷字基本上不會有太大誤差。儘管是一些比較特殊的情境,像假如你要分析哈利波特小說的文字內容,或是魔戒的影評分析,網路上大多都有相對應的字典供你載入。但若你想分析的文章你苦苦找尋就是沒有找著字典,又或者是你想分析公司資料但特有的專業術語太多,這時候就得嘗試自己創造一個專有的字典。

你當然可以選擇將專有名詞一個一個填上去,但身為一個數據分析師,當然應該嘗試讓程式來幫我們完成囉!我參考了陳嘉葳寫的推廣PPT提供的方法,依步驟來動手完成這一隻程式。

前置作業

分析資料

我拿之前寫的一篇文章淺談文字探勘 - 轉換非結構化文字來做原始資料,當中包含文字探勘的一些專有名詞。同樣方法可應用在其他的文章或評論中,只是怕牽涉版權只好先拿自己的文章來玩玩。

環境及套件

操作的環境如下所示:

  • 作業系統:macOS High Sierra
  • 程式語言:R version 3.4.3
  • IDE:RStudio version 1.1.414

過程中所需用到的套件如下:

1
2
3
4
5
6
library(tidyverse)
library(stringr)
library(tm)
library(RWeka)
library(slam)
library(snowfall)

因為需要做文字上的處理,stringr可以幫助我們在文字上做任意的轉換。tm主要是用來做文字探勘的一個套件,但他對中文的支援度很差,一般來說建議使用tidytext。不過在清理資料中的標點符號或是英文數字等,它有不錯的函數可以用,因此這裡也把它載入。RWeka是用來個別斷詞用的,下方會說明它的用途。slam則是因為文字的運算量相當龐大,過程中我們會轉換成matrix來做運算,這是可以使用這個套件會比較方便。snowfall也是運算龐大的原因,因此用平行運算的方式來加速結果產出。

創建字典

資料處理

這裡我直接將文章的內容儲存成.txt檔,以方便直接讀取。現在的目的是製作專屬字典,因此標點符號及數字不是我們需考量的東西(當然如果需要也可以將它們納進來),在資料處理時就將它們移除。

1
2
3
4
5
6
dat <- read_lines("./創建字典.txt")
dat <- paste0(dat, collapse = "")
dat <- removePunctuation(dat, ucp = T) %>% # 去除全形標點符號
removePunctuation() %>% # 去除半形標點符號
removeNumbers() %>% # 去除數字
{gsub('[A-Za-z \\\t]', '', .)} # 去除英文及跳脫符號\t

在將它們處理成一整個文字塊後,我們要將它們分開斷詞,期望處理成以下的形式:

“我們在分” “們在分析” “在分析時” “分析時都” “析時都習” “時都習慣” …

“我們在” “們在分” “在分析” “分析時” “析時都” “時都習” “都習慣” …

“我們” “們在” “在分” “分析” “析時” “時都” “都習” “習慣” …

這樣就可以將有可能形成字詞的字斷開,再用一些方法將專有的字詞跳出來。這裡要注意的是,NGramTokenizer這個函式原來是為了拉丁語句做個別斷詞使用的,所以他對每一個字的斷詞判斷是以空格為主,因此我們用來處理中文時,就先將字與字之間加入空格,等斷完字再將空格移除。作法如下:

1
2
3
4
5
6
7
8
full <- dat %>% str_split_fixed("", n = Inf)  %>% 
str_c(collapse = " ") %>% # 將字詞用空白分開
NGramTokenizer(Weka_control(min =2, max = 5)) %>% # 重複斷詞
{gsub(" ","",.)} %>% # 移除空格
data.frame(name = .) %>%
count(name) %>% # 計算每個字詞出現次數
ungroup() %>%
mutate(proportion = n / sum(n), name = parse_character(name)) # 計算比例

接下來我們將出現較少的字詞移除,一方面是字詞太少時我們很難判斷它是否為專有名詞,另一方面是我們需將無關緊要的雜訊給移除。至於移除的標準就見仁見智,也有人不是用次數而是用比例當門檻。

1
seg_base <- full %>% filter(n >= 2) # 將出現次數較少的詞彙濾除

獨立字詞

對於專有名詞來說,我們當然希望它能被完整的切出來,但跟專有名詞高關聯度的字詞很常會一起被切出來。像下面這樣:

“做文字探勘” “在文字探勘”

”文字探勘時“ “文字探勘的”

我們要如何將「做」、「在」、「時」、「的」等字眼去除呢?這裡可以用一些機率論的原理,舉例來說,「文字探勘」跟「做」應該是要被切分開的詞,那麼P(文字探勘)P(文字探勘)P()P(做)就應該彼此獨立,也就是說,

P(做文字探勘)=P()×P(文字探勘)P(做文字探勘) = P(做)\times P(文字探勘)

我們做個簡單的換位,

k=P(做文字探勘)P()×P(文字探勘)1k= \frac{P(做文字探勘)}{P(做)\times P(文字探勘)} \approx 1

當近似於1時,代表「做文字探勘」不是一個獨立詞彙,那麼它就極有可能非專有名詞。仔細觀察可以發現,kk越大作為一個字詞的可能性就越高,因此我們可以設置一個門檻,來濾除非我們想要的字詞。

因為母體的詞彙總數是相同的,所以這裡可以用次數來取代比率,會比較好計算。另外我們這裡只擷取四個字內的專有名詞,但切割需切割前後各一個字詞,這也是上面NGramTokenizer取5個字的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 計算字詞的獨立關係
segmentWord <- function(word, full){
n <- nchar(word)-1 # 切割位置數
seg <- sapply(1: n, function(i){
w1 <- substr(word, 1, i)
w2 <- substr(word,i+1, n+1)
str_count(full, word) / (str_count(full, w1)*str_count(full,w2)) # 計算佔比
})
seg_pro <- min(parse_number(seg))
return(seg_pro)
}

## 對獨立性判別較差的資料剔除
commonword <- sapply(seg_base$name, segmentWord, dat) %>% data_frame(pro = .) %>%
bind_cols(seg_base) %>%
filter(pro >= 0.013) # 濾除未達門檻的詞彙

去除不完整的詞彙

除了多餘的字詞來添亂,因為我們是個別斷詞的關係,零碎的字也會被抓進來。舉例來說:「文字探勘」會被斷出來,但是「文字探」或「字探勘」這類型的字一樣也會被抓出。為了解決這樣的問題,就引入了訊息量—「熵」這個概念。一個專有名詞周遭通常伴隨較豐富的訊息量,但像「文字探」後面通常只會加「勘」,所以訊息量就很低。所以只要抓住這個概念,只取兩側有豐富訊息量的字詞,就很有可能為專屬名詞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
freq_all <- matrix(full$proportion, dimnames = list(full$name))
freq <- matrix(commonword$proportion, dimnames = list(commonword$name))

sfInit(parallel=TRUE, cpus=2, type="SOCK")
shang <- sfSapply(1:length(freq), function(x,y,z,zn){
restrict_name <- zn[which(nchar(zn) <= nchar(y[x])+1)]
restrict <- z[which(nchar(zn) <= nchar(y[x])+1)]
w1 <- grep(paste('^', y[x], sep=''), restrict_name) # 計算詞性後的可能性
pre <- mean( -log2(restrict[w1])) # 計算詞性後的訊息量
w2 <- grep(paste(y[x], '$', sep=''), restrict_name) # 計算詞性前的可能性
post <- mean( -log2(restrict[w2]) ) # 計算詞性前的訊息量
return( min(pre ,post) ) # 回傳最小的訊息量
}, y = row.names(freq), z = freq_all, zn = row.names(freq_all))
sfStop()
names(shang) <- row.names(freq)
shang <- shang[which(shang >= 12)] # Entropy要大於門檻entropy

結果產出

廢話不多說,先來看看這樣產出後的結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
一向量
12.64359
下面
12.64359
不過
12.64359
主要
12.64359
之類
12.64359
也就是
12.64359
人會
12.64359
代表詞
12.64359
以及
12.64359
來判
12.64359
個詞彙在
12.64359
假設我們
12.64359
內容
12.64359
其他詞
12.64359
...

可以看到雖然分出的字詞有些道理,但卻不那麼漂亮。這個原因在所準備的文字資料不夠大量所致,像「個詞彙在」這個詞,隨著「個」及「在」的比例增加,這個詞在獨立字詞階段就會被去除。所幸的是我們還是抓出了想要做成字典的字詞:

1
2
3
4
5
6
7
8
9
10
11
12
向量指標
12.64359
搜尋引擎
12.31254
文件數
12.64359
文字探勘
12.19647
文章
12.26832
文章摘要
12.64359

參考文章

  1. R語言推廣講座Text Mining with R