Information Retrieval(資訊檢索)筆記02:Preprocessing and Tolerant Retrieval

MYJace發表於2020-09-27

預處理(Preprocessing)

首先回顧一下之前介紹的構建倒排索引的幾個主要步驟:

  1. 收集待建索引的文件
  2. 對這些文件中的文字進行詞條化
  3. 對步驟 2 中得到的詞條進行語言學預處理,得到詞項
  4. 根據詞項對所有文件建立索引

用流程圖表示為:

在這裡插入圖片描述

接下來,我們將按照順序,對每一部分常見的問題進行一個討論。

文件分析及編碼轉換(Parsing a document)

字元序列的生成

對於文件的處理,第一步需要將位元組序列轉換為線性的字元序列。

這一步操作首先需要正確地判斷出文件的編碼方式,我們可以把該判斷過程看作一個基於 ML 的分類問題或是其他啟發式的操作。在確定編碼方式的同時,還要確定文件的格式 (Format) ,比如是 PDF, XML, DOC 或是 ZIP 等其他格式的檔案。

在確定了以上兩點之後,對於一部分的文件已經能夠進行較為合適的轉換,但是還有一個很重要的因素需要考量,那就是語言 (Language)。這一點是很難有完美的解決方法的,因為各種語言的差異性,甚至將文字視為線性字元序列的看法本身,在某些文字系統中都不成立。因此,對於語言的處理,每個語言系統的操作可能會有很大的差異。

文件單位的選擇(Document Unit)

接下來,我們需要確定索引的文件單位 (Document Unit)。我們在前一篇筆記中,預設將文件集 (Collection) 中的每一篇文件 (Document) 看作一個索引單位(具體的例子就是《莎士比亞全集》中的每一部作品)。但實際上,很多時候,對於文件單位的選擇是需要慎重考量的。

例1:傳統的 Unix 郵件系統將某個郵件目錄下的一系列郵件都存放到單個檔案中,但是我們希望將每封郵件都看成一個獨立的文件,甚至將每一封郵件和它的附件都單獨分開看待;
例2:latex2html 有時把我們認為的單一文件(如一個PPT)以幻燈片或者小節為單位切分成多個HTML 頁面,並將每個頁面儲存到獨立的檔案中,但我們希望多個檔案合併為一個文件來構建索引

這裡就涉及到一個對於 “索引粒度 (Indexing Granularity)” 的權衡問題。這實際上是一個正確率 (Precession) 和召回率 (Recall) 之間的權衡問題:

  1. 如果索引粒度太小,那麼由於詞項散佈在多個細粒度文件中,我們就很可能錯過那些重要的段落,也就是說此時正確率高而召回率低
  2. 反之,如果索引粒度太大,我們就很可能找到很多不相關的匹配結果,即正確率低而召回率高

舉個例子,對於一個書庫來說,拿每一本書作為索引單位是不合適的,可能會有這種情況:搜尋“Chinese toys”,返回的書,它的第1 章提到了“China”,最後一章提到 “toys”,這顯然不合適,此時索引粒度比較大。更合適的做法是以書的每章/每段作為一個迷你文件建立索引,這樣可以更準確地找到匹配結果,因為粒度小,使用者的查詢會更精確。

詞條與詞項(Tokens And Terms)

詞條化(Tokenization)

在確定了文件單位之後,就是詞條化操作,我們在前一篇筆記中已經對這一步驟有一定的瞭解,這裡我們再補充一些細節。

詞條化是將給定的字元序列拆分成一系列子序列的過程,其中的每個子序列被稱為詞條 (Token),當然,在這個過程中,可能會同時去掉一些特殊字元(如標點符號等):

在這裡插入圖片描述
我們有時為了表述方便,會把詞項 (Term),詞條 (Token) 和詞 (Word) 進行混用,但是它們嚴格的定義上是有區別的:

  • 一個詞條指的是在文件中出現的字元序列的一個例項 (Instance)
  • 一個詞條類指的是相同詞條構成的集合
  • 一個詞項指的是資訊檢索系統詞典 (Dictionary) 中所包含的某個可能經過歸一化處理的詞條類

一個具體的例子,給定一個字元序列 “to sleep to perchance dream”
對其進行詞條化,我們會得到 5 個詞條:to, sleep, to, perchance, dream
但此時只有 4 個詞條類:to, sleep, perchance, dream
在進行索引時,若我們將 to 作為停用詞去掉,那麼最後只有三個詞項:sleep, perchance, dream

如果覺得上面的定義記起來複雜,也可以直接這樣記憶:經過進一步處理後,每個這樣的詞條現在都是索引條目的候選物件

儘管我們在例子中看到詞條化的操作並不複雜,感覺只是簡單的分割,但實際上,詞條化需要考慮的因素非常多。

首先是符號問題,比如英文中的上撇號 ( ’ ),既可以表示所有關係,也能表示縮寫,如何對其進行處理就是一個難題,比如

在這裡插入圖片描述
除此之外,連字元 ( - ) 的處理也存在問題。它可以用於分隔單詞中的母音字母(如co-education),也可以用於將多個名詞連線成一個新的名稱(如Hewlett-Packard),還可以在審稿裝置上顯示多個詞語的組合結果。

甚至,就連根據空格進行拆分也會存在問題。比如地名 (Los Angeles)、複合詞 (white space vs. whitespace) 等。其次,數字表示形式也是一個問題。比如日期的表示、電話號碼、郵編等。

上述的這些問題還只是在英語系統中,我們還要考慮語言本身帶來的挑戰。法語的省略用法 (比如 l’ensemble),德語的複合名詞中沒有空格,而像中文、日語之類的語言系統中,甚至詞之間根本就不存在空格。

在這裡插入圖片描述
這裡有兩種解決方案。一個是先進行分詞(word segmentation)。分詞的方法包括基於詞典的最大匹配法(採用啟發式規則來進行未定義詞識別)和基於機器學習序列模型的方法(如隱馬爾可夫模型或條件隨機場模型)等,後者需要在手工切分好的語料上進行訓練。但是因為有多種分割的可能,因此不能保證得到完全一致的唯一結果。另一個方案是放棄基於詞的索引策略,而是使用短字元序列的方法,這種方法不關心詞項是否會跨越詞的邊界。

去除停用詞(Stop words)

某些情況下,一些常見詞在文件和使用者需求進行匹配時價值並不大,需要徹底從詞彙表中去除。這些詞稱為停用詞(stop word)。一個常用的生成停用詞表的方法就是將詞項按照文件集頻率(DF)從高到低排列,然後手工選擇那些語義內容與文件主題關係不大的高頻詞作為停用詞。停用詞表中的每個詞將在索引過程中被忽略。

但是在進行短語查詢 (Phrase Query) 的時候,有一些停用詞確有其意義,比如短語查詢 President of the United States 中包含兩個停用詞,但是它比查詢 President AND “United States” 更精確。

詞項歸一化(Normalization to terms)

詞條歸一化(token normalization)就是將看起來不完全一致的多個詞條歸納成一個等價類,以便在它們之間進行匹配的過程。 最常規的做法是隱式地建立等價類,每類可以用其中的某個元素來命名。比如,在文件和查詢中,都把詞條 anti-discriminatory 和 antidiscriminatory 對映成詞項 antidiscriminatory, 這樣對兩個詞中的任一個進行搜尋,都會返回包含其中任一詞的文件。

另一種建立等價類的方法是維護多個非歸一化詞條之間的關聯關係。具體有兩種實現的方式:

  1. 採用非歸一化的詞條建立索引,併為某個查詢詞項維護一張由多個片語成的查詢擴充套件詞表。當輸入一個查詢詞項時,根據擴充套件詞表進行擴充套件,再將擴充套件後得到的多個詞所對應的 Posting List 合併起來
  2. 在索引構建時就對詞進行擴充套件。比如,對包含 automobile 的文件,也用 car 來進行索引(同樣,對於包含 car 的文件也用 automobile 進行索引)

這兩種方法相比隱式建立等價類會花費更多的空間,但是由於兩個關聯詞的擴充套件表之間可以存在交集,但不必完全相同,因此靈活性更高。這也意味著從不同關聯詞出發可以進行不對稱的擴充套件

在這裡插入圖片描述
該例子中,使用者輸入 windows 想到得到包含 Windows 作業系統的文件;但是若輸入 window,此時會和小寫的 windows 匹配,但是不太可能會和 Windows 作業系統中的 Windows 像匹配

隱式建立等價類或查詢擴充套件的使用幅度這兩種方法雖然看似合理,但在使用的時候仍要萬分注意,過度使用很容易會在無意間造成非預期的擴充套件結果。例如,通過刪除U.S.A.中的句點可以把它轉化成USA,由於在首字母省略用法中存在這種轉換模式,所以上面的做法乍看上去非常合理。但是,如果輸入查詢C.A.T.,返回的很多包含cat的文件卻肯定不是我們想要的結果。

實際情況中,詞項歸一化還會面臨一些其他的問題:

  • 重音及變音符號問題。 人們很可能希望 cliche 和cliché 或者naive 和naïve 能匹配。這可以通過在詞條歸一化時去掉變音符號來實現。但在西班牙語中,同樣的詞有無重音符號表示的意思可能會完全不同(peña 的意思是“懸崖”,pena 的意思卻是“悲哀”)。這一問題的終點不在於規範或者語言學問題,而是使用者如何構造查詢來查詢包含這些詞的文件。出於各種原因,實際上在很多情況下使用者並不會輸入變音符號
  • 大小寫轉換問題。 大小寫轉換(case-folding)問題的一個一般處理策略是將所有的字母都轉換成小寫。但是這也會造成一些專有名詞被影響。另一種啟發式的方是,將句首詞轉換成小寫形式,並將標題中大寫或首字母大寫的全部單詞都轉換成小寫形式。這些首字母大寫的單詞通常都是普通詞。
  • 語言問題。 待索引的文件集可以同時包括來自不同語言的文件,單篇文件中也很可能同時出現不同語言的文字。比如,一封法語郵件中也許會引述英文書寫的合同檔案條款。一種做法就是,在文件上執行一個語言識別器,然後將每個詞項的語言種類標記在詞項詞典中。
  • 除此以外還有諸如將英式和美式拼寫等同、日期時間格式統一等問題

詞幹還原和詞形歸併(Stemming and Lemmatization)

詞幹還原和詞形歸併的目的都是為了減少屈折變化的形式,並且有時會將派生詞轉化為基本形式。

比如:
am, are, is⇒ be
car, cars, car’s, cars’ ⇒car

但是,詞幹還原和詞形歸併這兩個術語表示的意思是不同的:

  • 詞幹還原通常指的是一個很粗略的去除單詞兩端詞綴的啟發式過程,並且希望大部分時間它都能達到這個正確目的,這個過程也常常包括去除派生詞綴
  • 詞形歸併通常指利用詞彙表和詞形分析來去除屈折詞綴,從而返回詞的原形或詞典中的詞的過程,返回的結果稱為詞元(lemma)

例如,給定詞條 saw,詞幹還原的結果可能就是 s,而詞形歸併將返回 see。它們的主要區別在於:詞幹還原在一般情況下會將多個派生相關詞合併在一起,而詞形歸併通常只將同一詞元的不同屈折形式進行合併。

詞幹還原最常用是 Porter 演算法,該演算法包括 5 個順序執行的詞約簡步驟。在每個步驟中,都有選擇規則的一些不同約定。比如在第一步,可能會根據以下規則進行約簡:

在這裡插入圖片描述
後續步驟的很多規則中都使用了詞的測度(measure)的概念,即通過粗略檢查音節的個數來判斷詞是否足夠長,在足夠長的情況下才將規則的匹配部分看成詞綴而不是詞幹的一部分來進行處理。

比如,有一條規則為: (m > 1) EMENT →
則,replacement → replac
但是 cment 不能轉換為 c

我們可以選擇詞形歸併工具(lemmatizer)而不是詞幹還原工具來處理相關問題。詞形歸併工具是一種自然語言處理工具,它能夠通過進行詳盡的詞形分析來精確地得到每個詞的詞元。而詞幹還原能夠提高召回率但是會降低正確率。

比如,我們對以下所有單詞進行詞幹還原:
operate operating operates operation operative operatives operational
Porter 詞幹還原演算法會將它們都轉換為 oper

然而,operate 在大多數形式下的都是普通動詞。使用Porter 詞幹還原工具會降低如下查詢的正確率:

operational AND research
operating AND system
operative AND dentistry

對於上面的情況,即使採用詞形歸併工具仍然不能完全解決問題,這是因為在特定的搭配中使用了特定的屈折變化形式,比如一個包含operate 和system 的句子對於查詢operating AND system 來說並不是一個好的結果。要從詞項歸一化中獲得更好效果取決於語用分析而不只是詞形分析。

容錯式檢索(Tolerant Retrieval)

詞典搜尋的資料結構(Dictionary data structures)

首先回顧一下之前介紹過的倒排索引:

在這裡插入圖片描述
我們一般將這樣的倒排索引稱作普通倒排索引 (Standard Inverted Index) 。在普通的倒排索引中,Dictionary 中的每一個詞項都會對應一個由文件集 (Collection) 中的多篇文件 (Document) 組成的 Posting List。在進行查詢時,首要的任務是確定查詢 (Query) 中的每個詞是否都在 Dictionary 中,如果在,那麼返回該詞項對應的 Posting List 的指標 (Pointer)。

對於 Dictionary 的查詢操作,就需要我們對於它的資料結構有一定的瞭解,一般來說,Dictionary 的資料結構有兩種比較主流的選擇:

  • 雜湊表(Hash Table)
  • 搜尋樹(Tree)

對於選擇哪個資料結構,需要考慮以下幾個問題:

  1. 詞項的數量有多少?
  2. 詞項的數量是動態的嗎?如果是,那麼在插入新的詞項時,是單純增加即可,還是需要刪改某些已存在的詞項?
  3. 不同詞項的訪問頻率是怎樣的?

雜湊表(Hash Table)

該方法會將每個詞項 (Term) 通過雜湊函式對映為一個雜湊值。這種方法需要雜湊函式的目標空間足夠大,以減少雜湊結果衝突的可能性。

優點 (Pros)缺點 (Cons)
查詢速度比樹 (Tree) 快:O(1)由於查詢詞項稍有變化後雜湊函式會生成截然不同的結果,雜湊表方式難以處理查詢詞項存在輕微變形的情況(如單詞resume的重音符和非重音符版本)
無法處理字首式查詢 (Prefix Search) ,這也就意味著它難以支援容錯式檢索 (Tolerant Retrieval)
如果詞彙表 (Vocabulary) 不斷增長,就需要執行代價高昂的重雜湊 (Rehash) 操作

樹(Tree)

樹結構能夠有效解決上面提到的大部分問題。最出名的莫過於二叉樹 (Binary),其每個內部節點都有兩個子節點。在二叉樹中搜尋詞項要從根節點開始,而每個內部節點(包括根節點)都代表一個二值測試,測試的結果用於確定下一步應該搜尋的子樹。

在這裡插入圖片描述
我們不難看出,二叉樹能夠支援字首式查詢,比如我們想要搜尋以 automat 開始的詞項所在的文件,那麼我們只需要按照搜尋樹往下進行查詢,就能夠找到所有符合要求的詞項,然後返回這些詞項關聯的文件。但是二叉樹也存在一個缺點,那就是在增加和刪改詞項時,要保持整棵樹的平衡性。為了減輕這種平衡化處理的開銷,目前詞典搜尋中會採用另一種樹結構 - B-Tree

所謂的 B-Tree 就是允許樹的內部節點的子節點數目在某個固定的區間 [a, b] 內變化。我們這裡給出圖來理解:

在這裡插入圖片描述

這裡我們能看到,所有節點的子樹數量都在 [2, 4] 區間之內,因此,這裡的 a = 2, b = 4。B-Tree 可以看做將二叉樹的多層壓平到一層而得到的結構,這種樹結構在記憶體空間不足以存下整 Dictionary,有一部分必須存入磁碟時會很有用,因為在這種情況下“壓平”可以在將詞典調入記憶體時實現後續二值測試的預讀取。此時,B-樹中的整數a 和b 取決於磁碟塊的大小。

總結一下:

最簡單的樹結構就是二叉樹 (Binary Tree),但最常用的還是 B-Tree。同時,樹結構和雜湊表方式不同,要求樹中的字符集有預定義的排序方式,比如字元順序等。

優點 (Pros)缺點 (Cons)
能夠處理字首式查詢相比雜湊表的時間複雜度 O(1),樹結構需要 O(logM)
平衡化操作開銷巨大(但是 B-Tree 能夠解決該問題)

這裡再介紹一種樹結構,Tries。這個方法很好理解,就是將每個單詞進行了分解

在這裡插入圖片描述
這一方法的搜尋時間複雜度為 O(|Q|),|Q| 為查詢單詞的長度。同時它也支援字首查詢,但是該方法會耗費更多的空間,因此使用的較少。

萬用字元查詢(Wild-card Query)

有以下幾種情況我們需要進行萬用字元搜尋:

  1. 使用者對查詢的拼寫不確定(比如,不能確定是 Sydney 還是 Sidney,就用 S*dney)
  2. 使用者知道某個查詢詞項可能有不同的拼寫版本,並且要把包含這些版本的文件都找出來(color和colour)
  3. 使用者不確定一個外來詞或者短語的正確拼寫形式(如查詢Universit* Stuttgart)
  4. 使用者查詢某個查詢詞項的所有變形,這些變形可能做了詞幹還原,但是使用者並不知道(比如judicial 和judiciary,可採用萬用字元查詢judicia* )

一般來說,有兩種比較常見的萬用字元搜尋,即首萬用字元查詢 (Leading wildcard query) ,比如 * mon 和尾萬用字元查詢 (Tailing wildcard query),比如 mon *

對於尾萬用字元查詢 (Tailing wildcard query) 其實很簡單,我們可以依次按照字元m、o、n 從上到下遍歷搜尋樹,直到能列舉詞典中所有以 mon 開頭的詞項集合 W 時為止(即 mon ≤ w < moo 範圍內的所有詞)。最後,在普通倒排索引中進行 |W| 次查詢後取出 W 中所有詞項所對應的文件。

而對於首萬用字元查詢 (Leading wildcard query) ,我們引入詞典的反向 B-樹(reverse B-tree)結構,在該結構中,原來B-樹中的每個從根到葉子路徑所代表的詞項全部反過來寫。因此,詞項 lemon 在反向B—樹中的路徑就是 root-n-o-m-e-l。所以對反向B-樹遍歷後可以返回所有包含同一字尾的詞項。

這個時候,我們就有了對於更一般的萬用字元查詢也有了相應的策略,比如查詢 pro*cent。我們可以把它看做兩個查詢 Q1 = pro * , Q2 = * cent,然後取這兩個查詢的結果的交集。而對於首尾相同的情況,比如查詢 ba * ba 對應的2個集合都包含詞項ba,這種情況下應該過濾掉ba。現在,我們已經能夠對只有單個萬用字元 * 的情況進行處理。

輪排索引(Permuterm index)

首先,我們在字符集中引入一個新的符號 $ ,用於標識詞項結束。因此,詞項 hello 在這裡表示成擴充套件的詞項 hello$,然後,構建一個輪排索引,其中對擴充套件詞項的每個旋轉結果都構造一個指標來指向原始詞項

在這裡插入圖片描述
對於 hello$ 我們可以得到:hello$ , ello $ h, llo $ he, lo $ hel, o $ hell

在這裡插入圖片描述
詞項旋轉後得到的集合被稱為輪排詞彙表(permuterm vocabulary)。 現在我們具體看看輪排索引如何處理萬用字元查詢,我們考慮萬用字元查詢 m * n,這裡的關鍵是將查詢進行旋轉讓 * 號出現在字串末尾,即得到 n$m*。下一步,在輪排索引中查詢該字串,實際上等價於查詢某些詞項(如man 和moron)的旋轉結果。這樣,我們就能找出符合要求的詞項,並檢索出匹配的文件。這裡我們對輪排索引做一個彙總:

在這裡插入圖片描述
總結一下,核心的思想就是 “永遠把不確定的部分放在尾部”。這裡我們著重看一下上圖中的最後一種查詢:P * Q * R $,此時存在兩個不確定的部分,在這種情況下,首先返回輪排索引中 R $ P * 對應的詞項集合,然後通過窮舉法檢查該集合中的每個元素,過濾掉其中不包含 Q 的詞項,最後,再利用剩下的詞項去查普通倒排索引,從而得到最後的結果。

比如:fi * mo * er
首先返回輪排索引中 er$fi* 對應的詞項集合
然後濾掉其中不包含 mo 的詞項
因此最終會選出 fishmonger,而過濾掉 filibuster

輪排索引的一個最大缺點是其詞典會變得非常大,因為它儲存了每個詞項的所有旋轉結果

k-gram 索引(k-gram indexes)

因為輪排索引的空間佔用比較大,因此,針對這個問題,出現了另一種萬用字元查詢處理技術 - k-gram索引。

一個 k-gram 指的是由 k 個字元組成的序列,這裡我們使用 $ 表示詞項的開始和結束,因此,能夠得到以下結果:

詞項 castle 的所有 3-gram 包括:$ ca, cas, ast, stl, tle, le$
對一個文字 “April is the cruelest month” ,它的 2-gram (bigram) 包括:
在這裡插入圖片描述

在 k-gram 索引結構中,字典 (Dictionary) 由詞彙表中的所有詞項的所有 k-gram 構成,而每個 Posting List 則由包含該 k-gram 的詞項構成,比如:

在這裡插入圖片描述
而在這 Posting List 中的每一個詞項,也都指向一個自己的包含所有相關文件的 Posting List。

現在我們看一個具體的查詢例子:考慮查詢 re*ve,此時查詢目標是返回所有字首是 re 且字尾是 ve 的詞項。為此,我們構造布林查詢 $ re AND ve$,這個查詢可以在 3-gram 索引中進行查詢處理,返回諸如 relive、remove 及 retrieve 的詞項,然後我們可以在普通倒排索引中查詢這些返回的詞項,從而得到與查詢匹配的文件。

但是,k-gram 也有需要注意的問題,比如我們查詢 mon* ,想要找所有字首為 mon 的詞項。構造布林查詢 $m AND mo AND on,但此時在 2-gram 索引中進行查詢處理,有可能會得到一個詞項 moon,很顯然,它滿足布林查詢,但是並不符合我們的要求。

為了應對這種情況,引入了一個後過濾 (Postfiltering) 的步驟,即利用原始的查詢 mon* 對上述布林查詢產生的結果進行逐一過濾。 過濾時只需要做簡單的字串匹配操作即可。

一個萬用字元查詢往往會返回多個詞項,而每個詞項再根據普通倒排索
引返回其所在的文件。搜尋引擎還可以通過布林操作符支援多個萬用字元查詢的組合,比如 pyth* AND org*。但是即使是單個萬用字元查詢,實際上也代價高昂,因此,大多數搜尋引擎會把該功能放在“高階搜尋”選項卡下,不鼓勵使用者經常使用此類查詢。

相關文章