前陣子花了一些時間再調整 elasticsearch 的效能,剛好碰到了分詞相關的議題,加上最近也接觸了一些 NLP 相關的東西,因此,在此紀錄一些簡單的筆記,如果你對於 NLP 有興趣,可以參考參考.
You will know…
- 什麼是分詞?
- 分詞延伸的應用
- full text search
- NLP
- elasticsearch 應用場景
什麼是分詞
把文字或句子切割、還原成符合情境的顆粒,以利電腦進行識別或處理,常用於全文檢索或是 NLP 的前處理過程。
不同語言有不同語言的特性,處理的方式也大有不同,大致上可以分為 stemming、lemmatization、POS-tagging (part-of-speech) 等方式。
以英文來說,最簡單的 stemming 可以以空白鍵來當作切割的依據,再利用一些 lemmatization 的套件進行詞性還原。
| 原句 | Alan Turing invented the Turing test in 1950. |
| stemming | Alan / Turing / invented / the / Turing / test / in / 1950 |
| lemmatization | alan / turing / invent / the / turing / test / in / 1950 |
不過以中文來說,stemming 的部分就比較複雜了,但就不需要考慮詞性的問題。
| 原句 | 台灣是個充滿小吃的國家。 |
| stemming | 台灣 / 是 / 個 / 充滿 / 小吃 / 的 / 國家 |
其實電腦理解文字的過程,與人類理解文字的方法略有相似之處,太過於破碎的切割,反而會阻礙文句的理解。(就像是 手/手機/手電筒 三者差異甚遠)
分詞延伸的應用
全文檢索 full-text search
那分詞又跟全文檢索有什麼關聯呢? 我們假設我們有以下三個句子,而我們希望可以輸入任意文字,查詢該文字出現在那些句子當中。
- 由於疫情,讓下半年的工作越來越難找。
- 人工智慧的出現,顛覆了人類的想像。
- 什麼時候疫情才會過去? 我不想戴口罩了。
若我們想要查詢 “疫情",最直觀的的方法是從第一篇文章開始到第三篇文章接續比對出現過的文字,也就是說,會需要 O(nxm).
| 編號 | 文本內容 |
| 1 | 由於疫情,讓下半年的工作越來越難找。 |
| 2 | 人工智慧的出現,顛覆了人類的想像。 |
| 3 | 什麼時候疫情才會過去? 我不想戴口罩了。 |
但若是我們利用分詞加上倒排索引,利用空間變換時間的話,就可以大大減少查詢所需要的時間。
倒排索引 inverted index,將儲存的方式由編號查詢文本的方式,反過來變成文本的分片查詢編號.
| 分詞 | 文本編號 |
| 由於 | [ 1 ] |
| 疫情 | [ 1, 3 ] |
| 讓 | [ 1 ] |
| 工作 | [ 1 ] |
| 顛覆 | [ 2 ] |
| … | …. |
建立完以上 inverted index 後,每次的查詢只需要花費 O(1) 的時間。
有人可能會說,那這樣如果我查詢 “疫" 這一個字,不就查不出任何東西了嗎?
以上使用的分詞方式,確實會造成這種引響,不過只需要再多花一些空間,將分詞切的單位變得更細,就可以避免了,但有沒有必要就是另一回事了…
NLP – 自然語言處理
由於 NLP 我還在學習,想說等累積了一些東西後,再寫另一篇文章來補充。
不過大致上可以想像成,電腦需要理解的前提是:我們需要將文字量化,因此就需要經過分詞的過程, stemming、lemmatization、POS-tagging 等方式的預先處理,再透過不同的模型進行訓練,來達到目的。
keyword: CNN, LSTM, XLnet, Cosine Distance, etc.
Elasticsearch 應用場景
功能解釋
tokenizer
stemming 的邏輯,可以依據不同的場景使用適合的 tokenizer.
query example 1
// whitespace example
POST _analyze
{
"tokenizer": "whitespace",
"text": "Alan Turing invented the Turing test in 1950."
}
// response
[Alan, Turing, invented, the, Turing, test, in, 1950.]
query example 2
ngram: 依照 min_gram, max_gram 的長度對 input 進行滑動窗口的取樣.
example:
min_gram = 1, max_gram = 2, input = ‘taiwan’
output = [t, ta, a, ai, i, tw, w, wa, a, an, n]
// ngram example
PUT ngram-test-000001
{
"settings": {
"analysis": {
"analyzer": {
"ngram_test_analyzer": {
"tokenizer": "ngram_test_tokenizer"
}
},
"tokenizer": {
"ngram_test_tokenizer": {
"type": "ngram",
"min_gram": 3,
"max_gram": 3
}
}
}
}
}
POST ngram-test-000001/_analyze
{
"analyzer": "ngram_test_tokenizer",
"text": "taiwan"
}
// response
[tai, aiw, iwa, wan]
match
將搜尋的 input 經過 tokenizer 的處理,再進行比對搜尋.情境假設如下:
index name: index-0001
index mapping: name (use whitespace tokenizer)
寫入一筆資料:{ "_id" : "123", "name": "taiwan number one"}
因為 name 有套用 whitespace tokenizer,所以在資料庫中會呈現
[taiwan, number, one]
GET index-0001/_search
{
"query": {
"match": {
"name": "taiwan one"
}
}
}
由於我們使用 match 的查詢語句,query 中的 “taiwan one" 會經過 whitespace tokenizer 轉換變成 [taiwan, one],再與資料庫中的 [taiwan, number, one] 比對後,我們可以成功 query 到 _id = 123 的資料
term
直接將 input 資料進行比對搜尋,可視為完全比對.( input 不會經過分詞的處理)情境假設如下:
index name: index-0002
index mapping:
name (type = keyword)
name.whitespace_token (use whitespace tokenizer)
寫入一筆資料:{ "_id" : "123", "name": "taiwan number one"}
因為 name.whitespace_token 有套用 whitespace tokenizer,所以在資料庫中會呈現
[taiwan, number, one]
Query example 1
GET index-0002/_search
{
"query": {
"term": {
"name.whitespace_token": {
"value": "taiwan one"
}
}
}
}
由於我們使用 term 的查詢語句,"taiwan one" 不會進行分詞動作
與資料庫中的 [taiwan, number, one] 比對後,我們無法搜尋到 _id = 123 的資料
Query example 2
GET index-0002/_search
{
"query": {
"term": {
"name": {
"value": "taiwan number one"
}
}
}
}
由於 name 的 type 為 keyword,資料儲存時,不會進行 tokenize,所以上述的查詢語句,可以查詢到 _id = 123 的資料.
小結
| query input 進行分詞處理的時機 | 儲存資料進行分詞處理的條件 |
| match 語句 | 欄位定義為 text 時,將依據設定的分詞器進行解析並存入資料庫 |
簡單應用 – 以 auto-complete 為例
假設有一需求如下:
在單字搜尋系統中,當使用者輸入 3 個字母後,我們需要提供 auto-complete 的功能給使用者,當輸入超過 10 個字母以後,便不需要再提供 auto-complete 的功能.
auto-complete 的定義與舉例如下:
- input:ber
- output:所有字母中含有 ber 的英文單字皆須顯示,需包含 ber 字母在前:bereave、ber 字母在最後面:september、ber 字母在中間:strawberry,的三種狀況.
我們在這邊利用 ngram 的滑動窗口來處理,並且在 query 時,使用 term 語句進行查詢.
"tokenizer": {
"ngram_autocomplete_tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 10
}
}
詳細說明如下:
| 單字 | after ngram_autocomplete_tokenizer |
| bereave | [ber, bere, berea, bereav, bereave, ere, erea, ereav, ereave, rea, reav, reave, eav, eave, ave] |
| september | [sep, sept, septe, septem, septemb, septembe, september, spt, epte, eptem, eptemb, eptembe, eptember, pte, ptem, ptemb, ptembe, ptember, tem, temb, tembe, tember, emb, embe, ember, ebe, mber, ber] |
| strawberry | [str, stra, straw, strawb, strawbe, strawber, strawberr, strawberry, ...] |
| temblor | [tem, temb, tembl, temblo, temblor, emb, embl, emblo, emblor, mbl, mblo, mblor, blo, blor, lor] |
Query 1
"query": {
"term": {
"input": {
"value": "ber"
}
}
}
查詢結果 [bereave, september, strawberry]
Query 2
"query": {
"term": {
"input": {
"value": "temb"
}
}
}
查詢結果 [september, temblor]