我們在有關詞幹的文章中討論了文本歸一化。 但是,詞幹並不是文本歸一化中最重要(甚至使用)的任務。 我們還進行了其他一些歸一化技術的研究,例如Tokenization,Sentencizing和Lemmatization。 但是,還有其他一些用於執行此重要預處理步驟的小方法,將在本文中進行討論。
請記住,沒有適用於所有情況的「正確」歸一化方法列表。 實際上,隨著我們對NLP的深入研究,越來越多的人意識到NLP並不像人們想像的那樣具有普遍性。 儘管有許多有趣的通用工具箱和預製管道,但更精確的系統是針對上下文量身定製的系統。
因此,不應將本文歸一化的步驟列表作為硬性規則,而應將其作為對某些文章進行文本歸一化的準則。 還必須指出的是,在極少數情況下,您可能不想歸一化輸入-文本中其中更多變化和錯誤很重要時(例如,考慮測試校正算法)。
了解我們的目標——為什麼我們需要文本歸一化
讓我們從歸一化技術的明確定義開始。 自然語言作為一種人力資源,傾向於遵循其創造者隨機性的內在本質。 這意味著,當我們「產生」自然語言時,我們會在其上加上隨機狀態。 計算機不太擅長處理隨機性(儘管使用機器學習算法已將隨機性的影響降到最低)。
當我們歸一化自然語言時,我們會嘗試減少其隨機性,使其更接近預定義的「標準」。 這有助於減少計算機必須處理的不同信息的數量,從而提高效率。
當我們歸一化自然語言資源時,我們嘗試減少其中的隨機性
在那篇關於詞幹的文章中,我提到了歸一化試圖使事物更接近「正態分布」。 在某種意義上說是正確的,當我們歸一化自然語言輸入時,我們希望以「良好」和「可預測」的形狀使事物「符合預期」,例如遵循正態分布。
除了數學領域之外,我們還可以討論將歸一化數據輸入到我們的NLP系統中的好處。
首先,通過減少隨機性,我們減少了待處理的輸入變量,提高了總體性能並避免了誤報(想像一下,如果軟體日誌行中沒有錯字,就會觸發警告。 )。 對於系統和信息檢索任務來說,這是非常正確的(想像一下,如果Google的搜尋引擎僅與您鍵入的單詞完全匹配!)。
其次,尤其是在討論機器學習算法時,如果我們使用的是字詞袋或TF-IDF字典等簡單的舊結構,則歸一化會降低輸入的維數; 或降低載入數據所需的處理量。
第三,歸一化有助於在將輸入傳遞給我們的決策NLP算法之前對其進行處理。 在這種情況下,我們確保我們的輸入將在處理之前遵循「合同」。
最後,如果正確完成,歸一化對於從自然語言輸入中可靠地提取統計數據非常重要-就像在其他領域(例如時間序列分析)一樣,歸一化是NLP數據科學家/分析師/工程師手中重要的一步。
我們歸一化的對象是什麼?
這是一個重要的問題。 在進行文本歸一化時,我們應該確切地知道我們要標歸一什麼以及為什麼要歸一化。 另外,輸入數據的特點有助於確定我們將要用來歸一化輸入的步驟。 我們最感興趣的是兩件事:
句子結構:它總是以標點符號結尾嗎? 會出現重複的標點符號嗎? 我們是否應該刪除所有標點符號? 此外,可以使用更具體的結構(就像主謂賓結構),但很難實現。詞彙: 這是需要注意的核心內容之一。 大多數時候,我們希望我們的詞彙量儘可能小。 原因是,在NLP中,詞彙是我們的主要特徵,而當我們在這些詞彙中的變化較少時,我們可以更好地實現目標。實際上,我們可以通過分解成更簡單的問題來對這兩個方面進行歸一化。 以下是最常見的方法:
→刪除重複的空格和標點符號。
→去除口音(如果您的數據包含來自「外國」語言的變音符號-這有助於減少與編碼類型有關的錯誤)。
→去除大寫字母(通常,使用小寫單詞可獲得更好的結果。但是,在某些情況下,大寫字母對於提取信息(例如名稱和位置)非常重要)。
→刪除或替換特殊字符/表情符號(例如:刪除主題標籤)。
→替換單詞縮寫(英語中很常見;例如:「我」→「我是」)。
→將單詞數字轉換為阿拉伯數字(例如:「二十三」→「 23」)。
→為特殊符號替換(例如:「 $ 50」→「錢」)。
→縮寫標準化(例如:「 US」→「美國」 /「美國」,「 btw」→「順便說一下」)。
→標準化日期格式,社會保險號或其他具有標準格式的數據。
→拼寫糾正(可以說一個單詞可以用無限方式拼寫錯誤,因此拼寫糾正可以通過「更正」來減少詞彙變化)–如果您要處理推特,即時消息和電子郵件等開放用戶輸入的數據,這一點非常重要。
→通過詞幹去除性別/時間/等級差異。
→將稀有單詞替換為更常見的同義詞。
→停止定型化(比歸一化技術更常見的降維技術)。
在本文中,我將只討論其中一部分的實現。
如何做歸一化工作
要選擇我們將要使用的歸一化步驟,我們需要一項特定的任務。 對於本文,我們將假設我們要提取3000個#COVIDIOTS主題標籤的情緒集,以了解人們對COVID-19流行的看法。
我獲得了這些推文,並使用這個名為best-profanity的漂亮工具來審查不好的文字,如果需要,可以將其添加到規範化管道中。 他們也不包含撰寫內容的人。
但是,我並沒有繼續刪除每條推文中的姓名或檢查任何政治立場等,因為這不是本文的目的,並且可以單獨撰寫另一篇文章(關於自動審查)。
在這種情況下,我們要執行以下步驟:刪除重複的空白和標點符號; 縮寫替代; 拼寫更正。 另外,我們已經討論了定形化,下面我們使用它。
在完成代碼部分之後,我們將統計分析應用上述歸一化步驟的結果。
關於規範化的一件重要事情是函數的順序很重要。 我們可以說歸一化是NLP預處理管道中的管道。 如果我們不謹慎,則可能刪除對以後的步驟很重要的信息(例如在定形之前刪除停用詞)。
我們甚至可以將這些步驟分為兩個連續的組:「標記前步驟」(用於修改句子結構的步驟)和「標記後步驟」(僅用於修改單個標記的步驟),以避免重複標記步驟。 但是,為簡單起見,我們使用.split()函數。
將推文解析為字符串列表之後,就可以開始創建函數了。 順便說一句,我在列表周圍使用了一個名為tqdm的漂亮模塊,因此一旦應用歸一化過程,我們就會獲得漂亮的進度條。 以下是所需的導入:
from symspellpy.symspellpy import SymSpell, Verbosityimport pkg_resourcesimport re, string, jsonimport spacyfrom tqdm import tqdm#Or, for jupyter notebooks:#from tqdm.notebook import tqdm
刪除重複的空白和重複的標點符號(和網址):
這一步驟用簡單的正則表達式替換完成。 有改進的餘地,但是可以滿足我們的期望(這樣,我們就不會有多種尺寸的標度和感嘆號標記)。 我們刪除網址,因為這會減少很多我們擁有的不同令牌的數量(我們首先這樣做,因為標點替換可能會阻止它)。
def simplify_punctuation_and_whitespace(sentence_list):norm_sents = [] print("Normalizing whitespaces and punctuation") for sentence in tqdm(sentence_list): sent = _replace_urls(sentence) sent = _simplify_punctuation(sentence) sent = _normalize_whitespace(sent) norm_sents.append(sent) return norm_sentsdef _replace_urls(text): url_regex = r'(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})' text = re.sub(url_regex, "<URL>", text) return textdef _simplify_punctuation(text): """ This function simplifies doubled or more complex punctuation. The exception is '...'. """ corrected = str(text) corrected = re.sub(r'([!?,;])\1+', r'\1', corrected) corrected = re.sub(r'\.{2,}', r'...', corrected) return correcteddef _normalize_whitespace(text): """ This function normalizes whitespaces, removing duplicates. """ corrected = str(text) corrected = re.sub(r"//t",r"\t", corrected) corrected = re.sub(r"( )\1+",r"\1", corrected) corrected = re.sub(r"(\n)\1+",r"\1", corrected) corrected = re.sub(r"(\r)\1+",r"\1", corrected) corrected = re.sub(r"(\t)\1+",r"\1", corrected) return corrected.strip(" ")
縮寫替換
def normalize_contractions(sentence_list):contraction_list = json.loads(open('english_contractions.json', 'r').read()) norm_sents = [] print("Normalizing contractions") for sentence in tqdm(sentence_list): norm_sents.append(_normalize_contractions_text(sentence, contraction_list)) return norm_sentsdef _normalize_contractions_text(text, contractions): """ This function normalizes english contractions. """ new_token_list = [] token_list = text.split() for word_pos in range(len(token_list)): word = token_list[word_pos] first_upper = False if word[0].isupper(): first_upper = True if word.lower() in contractions: replacement = contractions[word.lower()] if first_upper: replacement = replacement[0].upper()+replacement[1:] replacement_tokens = replacement.split() if len(replacement_tokens)>1: new_token_list.append(replacement_tokens[0]) new_token_list.append(replacement_tokens[1]) else: new_token_list.append(replacement_tokens[0]) else: new_token_list.append(word) sentence = " ".join(new_token_list).strip(" ") return sentence
拼寫矯正
現在,這是一個棘手的問題。 它可能會引起一些不需要的更改(大多數可糾正拼寫的詞典缺少重要的上下文單詞,因此他們將它們視為拼寫錯誤)。 因此,您必須有意識地使用它。 有很多方法可以做到這一點。 我選擇使用名為symspellpy的模塊,該模塊的速度非常快(這很重要!),並且可以很好地完成這項工作。 做到這一點的另一種方法是,訓練一個深度學習模型來基於上下文進行拼寫校正,但這完全是另一回事了。
def spell_correction(sentence_list):max_edit_distance_dictionary= 3 prefix_length = 4 spellchecker = SymSpell(max_edit_distance_dictionary, prefix_length) dictionary_path = pkg_resources.resource_filename( "symspellpy", "frequency_dictionary_en_82_765.txt") bigram_path = pkg_resources.resource_filename( "symspellpy", "frequency_bigramdictionary_en_243_342.txt") spellchecker.load_dictionary(dictionary_path, term_index=0, count_index=1) spellchecker.load_bigram_dictionary(dictionary_path, term_index=0, count_index=2) norm_sents = [] print("Spell correcting") for sentence in tqdm(sentence_list): norm_sents.append(_spell_correction_text(sentence, spellchecker)) return norm_sentsdef _spell_correction_text(text, spellchecker): """ This function does very simple spell correction normalization using pyspellchecker module. It works over a tokenized sentence and only the token representations are changed. """ if len(text) < 1: return "" #Spell checker config max_edit_distance_lookup = 2 suggestion_verbosity = Verbosity.TOP # TOP, CLOSEST, ALL #End of Spell checker config token_list = text.split() for word_pos in range(len(token_list)): word = token_list[word_pos] if word is None: token_list[word_pos] = "" continue if not '\n' in word and word not in string.punctuation and not is_numeric(word) and not (word.lower() in spellchecker.words.keys()): suggestions = spellchecker.lookup(word.lower(), suggestion_verbosity, max_edit_distance_lookup) #Checks first uppercase to conserve the case. upperfirst = word[0].isupper() #Checks for correction suggestions. if len(suggestions) > 0: correction = suggestions[0].term replacement = correction #We call our _reduce_exaggerations function if no suggestion is found. Maybe there are repeated chars. else: replacement = _reduce_exaggerations(word) #Takes the case back to the word. if upperfirst: replacement = replacement[0].upper()+replacement[1:] word = replacement token_list[word_pos] = word return " ".join(token_list).strip()def _reduce_exaggerations(text): """ Auxiliary function to help with exxagerated words. Examples: woooooords -> words yaaaaaaaaaaaaaaay -> yay """ correction = str(text) #TODO work on complexity reduction. return re.sub(r'([\w])\1+', r'\1', correction)def is_numeric(text): for char in text: if not (char in "0123456789" or char in ",%.$"): return False return True
合理化
如果您一直關注我的系列文章,那麼您已經知道我已經實現了自己的lemmatizer。 但是,為了簡單起見,我選擇在這裡使用傳統方法。 它快速而直接,但是您可以使用任何其他所需的工具。 我還決定刪除(替換)所有標籤。 對於情感分析,我們並不是真的需要它們。
def lemmatize(sentence_list): nlp = spacy.load('en') new_norm=[] print("Lemmatizing Sentences") for sentence in tqdm(sentence_list): new_norm.append(_lemmatize_text(sentence, nlp).strip()) return new_norm def _lemmatize_text(sentence, nlp): sent = "" doc = nlp(sentence) for token in doc: if '@' in token.text: sent+=" @MENTION" elif '#' in token.text: sent+= " #HASHTAG" else: sent+=" "+token.lemma_ return sent
最後,我們將所有步驟加入「pipeline」函數中:
def normalization_pipeline(sentences): print("##############################") print("Starting Normalization Process") sentences = simplify_punctuation_and_whitespace(sentences) sentences = normalize_contractions(sentences) sentences = spell_correction(sentences) sentences = lemmatize(sentences) print("Normalization Process Finished") print("##############################") return sentences
結果
您可能想知道:應用這些任務的結果是什麼? 我已經運行了一些計數功能並繪製了一些圖表來幫助解釋,但我必須清楚一件事:數字表示不是表達文本歸一化重要性的最佳方法。
相反,當將文本規範化應用於NLP應用程式時,它可以通過提高效率,準確性和其他相關分數來發揮最佳作用。 我將指出一些可以從統計數據中清楚看到的好處。
首先,我們可以清楚地看到不同令牌總數的減少。 在這種情況下,我們將令牌數量減少了約32%。
Distinct words in unnormalized: 15233–80% of the text correspond to 4053 distinct words. Distinct words in normalized: 10437–80% of the text correspond to 1251 distinct words.
現在,通用令牌的數量出現了更大的差異。 這些令牌包括了所有數據的大約80%。 通常,我們通過大約10–20%的令牌範圍構成了文本的80%。
通過應用歸一化,我們將最常見的令牌數量減少了69%! 非常多! 這也意味著我們對此數據的任何機器學習技術都將能夠更好地推廣。
現在,關於文本歸一化的一件重要的事是,為了使文本規範化有用,它必須保留默認的自然語言結構。 我們可以通過數據本身看到這一點。 一個例子是,如果做得好,歸一化後的句子將不會變得越來越小。
在下面的直方圖中顯示了這一點,它表明,儘管歸一化後我們的1尺寸句子較少,而2尺寸句子較多,但其餘分布遵循未歸一化數據的結構(請注意,我們的曲線稍微接近正態分布曲線)。
另一個有助於我們可視化的工具是Boxplot。 它顯示了我們的數據如何分布,包括均值,四分位數和離群值。 總而言之,我們希望我們的中線與未規範化數據的中線相同(或接近)。 我們還希望框(大多數數據的分布)保持在相似的位置。 如果我們能夠增加數據量的大小,這意味著我們在中位數周圍的數據比歸一化之前要多(這很好)。 此外,我們要減少離群值。
結論
我希望在本文中能夠解釋什麼是文本歸一化,為什麼要這樣做以及如何做。
作者:Tiago Duque
deephub翻譯組:孟翔傑