13自然語言處理基礎入門

字符串基礎操作及應用

自然語言處理簡介

做一個中文文本分類任務,首先要做的是文本的預處理,對文本進行分詞和去停用詞操作,來把字符串分割成詞與詞組合而成的字符串集合并去掉其中的一些非關鍵詞匯(像是:的、地、得等)。再就是對預處理過后的文本進行特征提取。最后將提取到的特征送進分類器進行訓練。
什么是自然語言處理
NLP(Natural Language Processing,自然語言處理)當中所謂的「自然」是為了與人造的語言(比如 C 語言, JAVA 等)區(qū)分開來,指自然形成的語言,即平時人們?nèi)粘J褂玫慕涣鞯恼Z言?!刚Z言」則是人類區(qū)別其他動物的本質(zhì)特性。在所有生物中,只有人類才具有語言能力。人類的多種智能都與語言有著密切的關系。人類的邏輯思維以語言為形式,人類的絕大部分知識也是以語言文字的形式記載和流傳下來的?!柑幚怼箘t指的是對自然語言的各種處理方法與運用。
NLP 不僅是計算語言學的應用領域,還是計算機科學和人工智能(AI)領域的一個重要研究方向。NLP 主要研究人與計算機之間用自然語言進行有效通信的各種理論和方法。因此,又可以說 NLP 是一門集語言學、計算機科學、數(shù)學于一體的學科。
不過,NLP 雖然是與語言學有關, NLP 又與語言學那種純粹的研究自然語言有所區(qū)別。語言學是以自然語言為研究對象,研究語言的性質(zhì)、功能、結構、運用和歷史發(fā)展以及其他與語言有關的問題。NLP 則是在于研究能有效實現(xiàn)人機間自然語言通信的計算機系統(tǒng)。
總的來說,自然語言處理可以概括為: 就是利用計算機的強大的運算能力,采用統(tǒng)計手段來對語言進行處理,然后獲得需要的信息,以達到最終想要的目的,而使用各種方法的一門技術。
NLP 分類
自然語言的理解和自然語言的生成。

自然語言理解是個綜合的系統(tǒng)工程,涉及了很多細分的學科。
代表聲音的音系學:語言中發(fā)音的系統(tǒng)化組織。
代表構詞法的詞態(tài)學:研究單詞構成以及相互之間的關系。
代表語句結構的句法學:給定文本的那部分是語法正確的。
代表理解的語義句法學和語用學 :給定文本的含義和目的是什么。
語言理解涉及語言、語境和各種語言形式的學科。但總的來說,自然語言理解又可以分為三個方面:
詞義分析
句法分析
語義分析

自然語言的生成則是從結構化的數(shù)據(jù)(可以通俗理解為自然語言理解分析后的數(shù)據(jù))以讀取的方式自動生成文本。主要有三個階段:
文本規(guī)劃:完成結構化數(shù)據(jù)中的基礎內(nèi)容規(guī)劃。
語句規(guī)劃:從結構化數(shù)據(jù)中組合語句來表達信息流。
實現(xiàn):產(chǎn)生語法通順的語句來表達文本。
研究與應用
NLP 在目前大火的 AI 領域有著十分豐富的應用??傮w來說,自然語言處理的研究問題(主要)有下面幾種:
信息檢索:對大規(guī)模文檔進行索引。
語音識別:識別包含口語在內(nèi)的自然語言的聲學信號轉換成符合預期的信號。
機器翻譯:將一種語言翻譯成另外一種語言。
智能問答:自動回答問題。
對話系統(tǒng):通過多回合對話,跟用戶進行聊天、回答、完成某項任務。
文本分類:將文本自動歸類。
情感分析:判斷某段文本的情感傾向
文本生成:根據(jù)需求自動生成文本
自動文摘:歸納,總結文本的摘要。
相關術語
分詞:詞是 NLP 中能夠獨立活動的有意義的語言成分。即使某個中文單字也有活動的意義,但其實這些單字也是詞,屬于單字成詞。
詞性標注:給每個詞語的詞性進行標注,比如 :跑/動詞、美麗的/形容詞等等。
命名實體識別:從文本中識別出具有特定類別的實體。像是識別文本中的日期,地名等等。
詞義消歧:多義詞判斷最合理的詞義。
句法分析:解析句子中各個成分的依賴關系。
指代消解:消除和解釋代詞「這個,他,你」等的指代問題。

字符串操作

字符作為文本類數(shù)據(jù)的基本單元,其在自然語言處理中的地位可以說是非常的重要。而且,大部分的自然語言處理任務都是從字符上著手。
.count() 方法返回特定的子串在字符串中出現(xiàn)的次數(shù)。

seq = '12345,1234,123,12,1'
seq1 = '1'
a = seq.count(seq1)
a

.strip()方法可以去除字符串首尾的指定符號。無指定時,默認去除空格符 ' ' 和換行符 '\n'。

seq = ' 我們正在使用藍橋云課,藍橋云課學會很多!'
seq.strip()
seq.strip('!')
seq.strip(' 我們')

有時候只想要去除字符串開頭的某個字符串,但是字符串的末尾有一個同樣的字符串并不需要去掉。這時候可以使用 .lstrip() 方法。

seq = '12321'
seq.lstrip('1')

同樣,可以使用.rstrip() 方法來單獨去除末尾的字符。

seq.rstrip('1')

經(jīng)常會遇到需要將字符串拼接起來的情況,這時可以用運算符 + 來簡單暴力的拼接。

seq1 = '實'
seq2 = '驗'
seq3 = '樓'
seq = seq1 + seq2 + seq3
seq
seq1 = ''
seq = ['實', '驗', '樓']

# 將 seq 里面的每個字符拼接起來
for n in seq:
    seq1 += n
seq1

需要將字符串用特定的符號拼接起來的字符的時候,可以用 .join() 方法來進行拼接。

seq = ['2018', '10', '31']
seq = '-'.join(seq)  # 用 '-' 拼接
seq
seq = ['實', '驗', '樓']
seq = ''.join(seq)  # 用''空字符拼接
seq

當想要比較兩個字符串的大小時,這里需要加載 operator 工具,它是 Python 的標準庫。不需要額外下載,直接通過 import 調(diào)用即可。operator 從左到右第一個字符開始,根據(jù)設定的規(guī)則比較,返回布爾值( True,F(xiàn)alse )。
判斷 a < b 型:

import operator
seq1 = '字符串 1 號'
seq2 = '字符串 2 號'
operator.lt(seq1, seq2)

除了使用 operator 之外,還有一種方法更簡便: 直接使用運算符比較。
直接使用運算符比較 a < b:

seq1 < seq2

判斷 a <= b:

operator.le(seq1, seq2)
seq1 <= seq2

判斷 a == b:

operator.eq(seq1, seq2)
seq1 == seq2

判斷 a != b:

operator.ne(seq1, seq2)
seq1 != seq2

判斷 a > b:

operator.gt(seq1, seq2)
seq1 > seq2

判斷 a >=b:

operator.ge(seq1, seq2)
seq1 >= seq2

在處理英文類文本的時候會遇到需要將文本全部轉化為大寫或者小寫的時候。使用 .upper() 和 .lower() 可以很方便的完成這個任務。
使用 .upper() 將文本轉化為大寫。

seq = 'appLE'
seq = seq.upper()
seq

使用 .lower()將文本轉化為小寫。

seq = 'APPle'
seq = seq.lower()
seq

為了查找到某段字符串當中某個子串的位置信息,有兩種方法。一種是.index ,一種是 .find。 兩種方法都可實現(xiàn)這個功能,不同的是 index 如果未找到的話,會報錯,而 find 未找到的則會返回 -1 值。
.find() 方法,在序列當中找到子串的起始位置。PS:第一個位置是 0 。

seq = '這個是一段字符串'
seq1 = '字符串'
seq.find(seq1)

.find() 方法,字符串中不存在子串返回 -1

seq2 = '無'
seq.find(seq2)
seq3 = '字符串'
seq.index(seq3)
seq4 = '無'
seq.index(seq4)

當想要切分字符串時,有兩種常用的方法。第一種是直用序列截取的方法。這種方法十分的簡單,就是根據(jù)順序來截取序列上你想要的某些片段。
想要截取 [這是字符串], 中的‘這是字符’的時候。[0:4] 中 : 左邊的 0 的意思是從序列位置 0(從 0 開始數(shù)),: 右邊的 4 意思是截取到第 4 個字符(但并不包括從零開始數(shù)的第 4 個字符)。

seq = '這是字符串'
seq1 = seq[0:4]
seq1

截取某個字符的時候可以通過這樣。

seq2 = seq[0]
seq2
seq3 = seq[1:4]
seq3

如果結合字符串拼接的操作,還能把截出來的字符串拼接起來。

seq = '小了白了兔'
a = seq[0]
b = seq[2]
c = seq[4]
seq1 = a+b+c
seq1

需要把一個字符串按照某個字符切分開處理。比如[今天天氣很好,我們出去玩],要把兩句話以 ','切開成兩句話。split()函數(shù)可以完成這個操作,函數(shù)返回一個由切分好的字符串組成的列表。

seq = '今天天氣很好,我們出去玩'
seq = seq.split(',')
seq
seq = '2018-11-11'
seq = seq.split('-')
seq
seq = 'I have an apple'
seq = seq.split(' ')
seq
seq = '號外!號外!特大新聞'
seq = seq.split('!')
seq

需要翻轉字符串的時候,那么我們直接用序列操作,直接以上面截取序列的方法,但是按照逆向的來截取實現(xiàn)翻轉。

seq = '12345'
seq = seq[::-1]
seq

遇到需要判斷某子串在字符串中是否出現(xiàn),并根據(jù)判斷做出后續(xù)操作的情況??梢杂?in 來作出判斷,若存在則返回 True,不存在則返回 False,然后配合 if ,作出后續(xù)操作。

seq = 'abcdef'
'a' in seq
seq = '你的名字真好聽!'
'的' in seq

in 關鍵字可以用在任何容器對象上,判斷一個子對象是否存在于容器當中,并不局限于判斷字符串是否存在某子串,還可以用在其他容器對象例如 list,tuple,set 等類型。

nums = [1, 2, 3, 4]
2 in nums
seq = 'abcd'
n = 0
if 'a' in seq:
    n += 1
n

有時需要把字符串中的某段字符串用另一段字符串代替,比如 2018-01-01 中的 - 用 '/' 代替。我們可以用到 .replace(a,b) ,他可以將某字符串中的 a 字符串 替換成 b 字符串。下面來實現(xiàn)一下。

seq = '2018-11-11'
seq = seq.replace('-', '/')
seq
seq = '等會你還要來做實驗嗎?'
seq = seq.replace('嗎?', '哦!')
seq
seq = '小了白了兔'
seq = seq.replace('了', '')
seq

當遇到需要判斷字符串是否以某段字符開頭的時候。比如想要判斷 ‘a(chǎn)bcdefg’ 是否以 'a' 開頭??梢杂?.startswish() 方法。

seq = 'abcdefg'
seq.startswith('a')
seq = '我在藍橋云課'
seq.startswith('me')

同樣的方法,我們可以用 .endswith() 來確定字符串是否以某段字符串結尾。

seq = 'abcd'
seq.endswith('d')
seq = '我在藍橋云課'
seq.endswith('嘍')

有時候,當想要檢查字符串的構成,像是檢查字符串是否由純數(shù)字構成。

seq = 's123'
seq.isdigit()
seq = '123'
seq.isdigit()

正則表達式

正則表達式是用于處理字符串的強大工具,它由一個特殊的字符序列構成一定的規(guī)則,根據(jù)這個規(guī)則可以幫你檢查字符串是否與這個規(guī)則的字符串匹配。
在下面這幾個日期信息中,如果想要提取其中的年份信息:
A : 2018/01/01
B : 2018-01-01
C : 2018.01.01
D :01/01/2018

觀察 A,B,C,D 四個人的輸入,因為每個人輸入習慣的不同,導致了格式方面的不統(tǒng)一。這個時候,我們無法通過 Python 提供的字符串前 4 位 [0:4] 的方法來提取每段序列當中的年份信息。這時候,可以考慮用到正則表達式。我們設計一個正則表達式 [0-9]{4} ,這個正則表達式代表的是 0 到 9 的數(shù)字連續(xù) 4 次。

Python 對于正則表達式的支持,一般是通過 re 模塊來提供。re 模塊是 Python 自帶的標準庫,直接 import 加載就可以了,不需要另外下載第三方庫。
首先,我們需要通過 re.compile() 將編寫好的正則表達式編譯為一個實例。

# 加載 re 模塊
import re

# 將正則表達式編寫成實例
pattern = re.compile(r'[0-9]{4}')
pattern

然后我們對字符串進行匹配,并對匹配結果進行操作。這里,我們要用到的是 re.search() 方法。 這個方法是將正則表達式與字符串進行匹配,如果找到第一個符合正則表達式的結果,就會返回,然后匹配結果存入group()中供后續(xù)操作。

import re

pattern = re.compile(r'[0-9]{4}')
time = '2018-01-01'
# 用剛剛編譯好的 pattern,去匹配 time
match = pattern.search(time)
# 匹配結果存放在 group()當中的
match.group()

可以看到我們的正則表達式匹配到了 2018 這個符合我們規(guī)則(連續(xù)4個字符 都是 0-9的數(shù)字)的結果。 現(xiàn)在我們根據(jù)上面兩段代碼的內(nèi)容,設計一個小程序來提取這種不規(guī)則日期當中的年份信息。

import re

pattern = re.compile(r'[0-9]{4}')
times = ('2018/01/01', '01/01/2019', '01.2017.01')

for time in times:
    match = pattern.search(time)
    if match:
        print('年份有:', match.group())

.findall():這個方法可以找到符合正則表達式的所有匹配結果。這里我們使用了 \d 規(guī)則的正則表達式,這個正則表達式可以替我們識別數(shù)字。

import re

pattern = re.compile(r'\d')
pattern.findall('o1n2m3k4')

同樣的方法,我們編寫一個 \D 正則表達式,這個可以匹配一個非數(shù)字字符。

pattern = re.compile('\D')
pattern.findall('1A2B3C4D')

.match() 方法與 .search() 方法類似,只匹配一次,并且只從字符串的開頭開始匹配。同樣,match 結果也是存在 group() 當中。

# 不止是規(guī)則,字符也是可以單獨作為正則表達式使用。
pattern = re.compile('c')
pattern.match('comcdc').group()

.match() 只從開頭匹配。若匹配不成功,則 group()不會有內(nèi)容。

# 我們編寫了字符 1 作為正則表達式去匹配。
pattern = re.compile('1')
pattern.match('abcdefg1').group()

正則表達式的應用場景十分廣泛:在去除網(wǎng)頁類文本語料庫中 html 符號,去除一些聊天的表情符號像是 Orz ,T_T 等等有著十分重要的應用。

中英文分詞方法及實現(xiàn)

英文分詞

在語言理解中,詞是最小的能夠獨立活動的有意義的粒度。由詞到句,由句成文。因此,將詞確定下來是理解自然語言處理的第一步,只有跨越了這一步,才能進行后續(xù)任務。
英文原文: i have a pen,i have an apple!
英文分詞結果: i , have , a , pen , i , have , an , apple , !
通過上面的英文分詞例子,可以發(fā)現(xiàn)英文文本詞與詞之間有空格或者標點符號,如果想要對這種普通的英文文本進行分詞的話是不需要什么算法支撐,只需暴力拆分即可,即直接通過空格或者標點來將文本進行分開就可以完成英文分詞。
按空格切分無限次:

a = 'i have a pen'
a1 = a.split(' ')
a1

按空格切分 1 次:

a2 = a.split(' ', 1)
a2

按 have 切分 1 次:

a3 = a.split('have', 1)
a3

實現(xiàn)對多個英文文本分詞,要求同時以 , , . , ? , ! , 五個符號分詞。
首先對原文本以其中一個規(guī)則切分后,再對分好后的文本進行下一個規(guī)則的切分,再對分好的文本進行切分,直到按 5 個規(guī)則切分完成,最后將切分好的詞添加進 tokenized_text 并返回。

def tokenize_english_text(text):
    # 首先,我們按照標點來分句
    # 先建立一個空集用來,用來將分好的詞添加到里面作為函數(shù)的返回值
    tokenized_text = []
    # 一個 text 中可能不止一個內(nèi)容,我們對每個文本單獨處理并存放在各自的分詞結果中。
    for data in text:
        # 建立一個空集來存儲每一個文本自己的分詞結果,每對 data 一次操作我們都歸零這個集合
        tokenized_data = []
        # 以 '.'分割整個句子,對分割后的每一小快 s:
        for s in data.split('.'):
            # 將's'以 '?'分割,分割后的每一小快 s2:
            for s1 in s.split('?'):
                # 同樣的道理分割 s2,
                for s2 in s1.split('!'):
                    # 同理
                    for s3 in s2.split(','):
                        # 將 s3 以空格分割,然后將結果添加到 tokenized_data 當中
                        tokenized_data.extend(
                            s4 for s4 in s3.split(' ') if s4 != '')
                        # 括號內(nèi)的部分拆開理解
                        # for s4 in s3.split(' '):
                        #    if s4!='':  這一步是去除空字符''。注意與' ' 的區(qū)別。
        # 將每個 tokenized_data 分別添加到 tokenized_text 當中
        tokenized_text.append(tokenized_data)

    return tokenized_text
a = ['i am a boy?i am a boy ! i am a boy,i', 'god is a girl', 'i love you!']
result = tokenize_english_text(a)
result

中文分詞

通過計算機將句子轉化成詞的表示,自動識別句子中的詞,在詞與詞之間加入邊界分隔符,分割出各個詞匯。這個切詞過程就叫做中文分詞。
中文原文: 我們今天要做實驗。
中文分詞結果:我們 | 今天 | 要 | 做 | 實驗 。
由于中文的結構與印歐體系語種有很大的差異。在中文中,文本是由連續(xù)的字序列構成,詞和詞之間沒有天然的分隔符。不同分詞方法的結果會影響到詞性,句法等問題。分詞作為一個方法,如果運用場景不同,要求不同,最終對任務達到的效果也不同。

困難之一:歧義。
原文:以前喜歡一個人,現(xiàn)在喜歡一個人
這里有兩個「一個人」,但是代表的意思完全不一樣。

困難之二:分詞界限。
原文:這杯水還沒有冷
分詞一: 這 | 杯 | 水 | 還 | 沒有 | 冷
分詞二: 這 | 杯 | 水 | 還沒 | 有 | 冷
分詞三: 這 | 杯 | 水 | 還沒有 | 冷

中文分詞這個概念自提出以來,經(jīng)過多年的發(fā)展,主要可以分為三個方法:
機械分詞方法;
統(tǒng)計分詞方法;
兩種結合起來的分詞方法。

機械分詞方法

機械分詞方法又叫做基于規(guī)則的分詞方法:這種分詞方法按照一定的規(guī)則將待處理的字符串與一個詞表詞典中的詞進行逐一匹配,若在詞典中找到某個字符串,則切分,否則不切分。機械分詞方法按照匹配規(guī)則的方式,又可以分為:正向最大匹配法,逆向最大匹配法和雙向匹配法三種。
正向最大匹配法
正向最大匹配法(Maximum Match Method,MM 法)是指從左向右按最大原則與詞典里面的詞進行匹配。假設詞典中最長詞是 m 個字,那么從待切分文本的最左邊取 m 個字符與詞典進行匹配,如果匹配成功,則分詞。如果匹配不成功,那么取 m?1 個字符與詞典匹配,一直取直到成功匹配為止。

通過用一個簡單的例子來講一下 MM 法的過程:
句子:中華民族從此站起來了
詞典:"中華","民族","從此","站起來了"
使用 MM 法分詞:
第一步:詞典中最長是 4 個字,所以我們將 【中華民族】 取出來與詞典進行匹配,匹配失敗。
第二步:于是,去掉 【族】,以 【中華民】 進行匹配,匹配失敗
第三步:去掉 【中華民】 中的 【民】,以 【中華】 進行匹配,匹配成功。
第四步:在帶切分句子中去掉匹配成功的詞,待切分句子變成 【民族從此站起來了】。
第五步:重復上面的第 1 - 4 步驟
第六步:若最后一個詞語匹配成功,結束。
最終句子被分成:【中華 | 民族 | 從此 | 站起來了】
逆向最大匹配法
逆向最大匹配法( Reverse Maximum Match Method, RMM 法)的原理與正向法基本相同,唯一不同的就是切分的方向與 MM 法相反。逆向法從文本末端開始匹配,每次用末端的最長詞長度個字符進行匹配。
另外,由于漢語言結構的問題,里面有許多偏正短語,即結構是:
定語 + 中心詞(名、代):(祖國)大地、(一朵)茶花、(前進)的步伐。
狀語 + 中心詞(動、形):(很)好看、(獨立)思考、(慢慢)地走。
如果采用逆向匹配法,可以適當提高一些精確度。換句話說,使用逆向匹配法要比正向匹配法的誤差要小。
雙向最大匹配法
雙向最大匹配法(Bi-direction Matching Method ,BMM)則是將正向匹配法得到的分詞結果與逆向匹配法得到的分詞結果進行比較,然后按照最大匹配原則,選取次數(shù)切分最少的作為結果。
實現(xiàn)正向最大匹配法
整個正向最大匹配算法的流程圖:

image.png

算法步驟:
導入分詞詞典 dic,待分詞文本 text,創(chuàng)建空集 words 。
遍歷分詞詞典,找到最長詞的長度,max_len_word 。
將待分詞文本從左向右取 max_len=max_len_word 個字符作為待匹配字符串 word 。
將 word 與詞典 dic 匹配
若匹配失敗,則 max_len = max_len - 1 ,然后
重復 3 - 4 步驟
匹配成功,將 word 添加進 words 當中。
去掉待分詞文本前 max_len 個字符
重置 max_len 值為 max_len_word
重復 3 - 8 步驟
返回列表 words
創(chuàng)建一個簡單的詞典 dic 和測試文本 text 。
首先創(chuàng)建一個測試文本 text:

# 文本
text = '我們是共產(chǎn)主義的接班人'
text

創(chuàng)建一個詞典 dic:

# 詞典
dic = ('我們', '是', '共產(chǎn)主義', '的', '接班', '人', '你', '我', '社會', '主義')
dic

我們需要遍歷詞典來求出最長詞的長度:

# 初始最長詞長度為 0
max_len_word0 = 0
for key in dic:
    # 若當前詞長度大于 max_len_word,則將 len(key)值賦值給 max_len_word
    if len(key) > max_len_word0:
        max_len_word0 = len(key)

max_len_word0

通過循環(huán)來完成 MM 法:

sent = text
words = []   # 建立一個空數(shù)組來存放分詞結果:
max_len_word = max_len_word0
# 判斷 text 的長度是否大于 0,如果大于 0 則進行下面的循環(huán)
while len(sent) > 0:
    # 初始化想要取的字符串長度
    # 按照最長詞長度初始化
    word_len = max_len_word
    # 對每個字符串可能會有(max_len_word)次循環(huán)
    for i in range(0, max_len_word):
        # 令 word 等于 text 的前 word_len 個字符
        word = sent[0:word_len]
        # 為了便于觀察過程,我們打印一下當前分割結果
        print('用 【', word, '】 進行匹配')
        # 判斷 word 是否在詞典 dic 當中
        # 如果不在詞典當中
        if word not in dic:
            # 則以 word_len - 1
            word_len -= 1
            # 清空 word
            word = []
        # 如果 word 在詞典當中
        else:
            # 更新 text 串起始位置
            sent = sent[word_len:]
            # 為了方便觀察過程,我們打印一下當前結果
            print('【{}】 匹配成功,添加進 words 當中'.format(word))
            print('-'*50)
            # 把匹配成功的word添加進上面創(chuàng)建好的words當中
            words.append(word)
            # 清空word
            word = []

統(tǒng)計分詞方法

基于的字典的方法實現(xiàn)比較簡單,而且性能也還不錯。但是其有一個缺點,那就是不能切分 未登錄詞 ,也就是不能切分字典里面沒有的詞。為解決這一問題,于是有學者提出了基于統(tǒng)計的分詞方法。
目前基于統(tǒng)計的分詞方法大體可以分兩種方法:
語料統(tǒng)計方法
序列標注方法
語料統(tǒng)計方法
對于語料統(tǒng)計方法可以這樣理解:我們已經(jīng)有一個由很多個文本組成的的語料庫 D ,假設現(xiàn)在對一個句子【我有一個蘋果】進行分詞。其中兩個相連的字 【蘋】【果】在不同的文本中連續(xù)出現(xiàn)的次數(shù)越多,就說明這兩個相連字很可能構成一個詞【蘋果】。與此同時 【個】【蘋】 這兩個相連的詞在別的文本中連續(xù)出現(xiàn)的次數(shù)很少,就說明這兩個相連的字不太可能構成一個詞【個蘋】。所以,我們就可以利用這個統(tǒng)計規(guī)則來反應字與字成詞的可信度。當字連續(xù)組合的概率高過一個臨界值時,就認為該組合構成了一個詞語。
序列標注方法
序列標注方法則將中文分詞看做是一個序列標注問題。首先,規(guī)定每個字在一個詞語當中有著 4 個不同的位置,詞首 B,詞中 M,詞尾 E,單字成詞 S。我們通過給一句話中的每個字標記上述的屬性,最后通過標注來確定分詞結果。

例如:我今天要去實驗室
標注后得到:我/S 今/B 天/E 要/S 去/S 實/B 驗/M 室/E
標注序列是:S B E S S B M E
找到 S 、B 、 E 進行切詞:S / B E / S / S / B M E /
所以得到的切詞結果是:我 / 今天 / 要 / 去 / 實驗室
在訓練時,輸入中文句子和對應的標注序列,訓練完成得到一個模型。在測試時,輸入中文句子,通過模型運算得到一個標注序列。然后通過標注序列來進行切分句子。
在統(tǒng)計學習方法中,可以用于序列標注任務的方法有很多。例如,隱馬爾可夫模型,條件隨機場等。
可以在 Python 用第三方的中文分詞工具 jieba ,來替我們在實際應用中省掉訓練分詞隱馬可夫模型的繁瑣步驟。并且,jieba 工具用的是隱馬爾可夫模型與字典相結合的方法,比直接單獨使用隱馬爾可夫模型來分詞效率高很多,準確率也高很多。
目前在實際應用中,jieba 分詞是使用率很高的一款工具。不僅使用起來十分的方便、快速,而且分詞效果也比較理想。

在使用 jieba 進行分詞時,有三種模式可選:
全模式
精確模式
搜索引擎模式

import jieba

全模式:

string = '我來到北京清華大學'
seg_list = jieba.cut(string, cut_all=True)

jieba 是將分詞后的結果存放在生成器當中的。

seg_list

無法直接顯示,若想要顯示,可以下面這樣。用 ‘|’ 把生成器中的詞串起來顯示。這個方法在下面提到的精確模式和搜索引擎模式中同樣適用。

seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

搜索引擎模式:

seg_list = jieba.cut_for_search(string)
'|'.join(seg_list)

全模式和搜索引擎模式,jieba 會把全部可能組成的詞都打印出來。在一般的任務當中,我們使用默認的精確模式就行了,在模糊匹配時,則需要用到全模式或者搜索引擎模式。
我們試著對一篇長文本作分詞。首先,導入某一段文本。

text = '市場有很多機遇但同時也充滿殺機,野蠻生長和快速發(fā)展中如何慢慢穩(wěn)住底盤,駕馭風險,保持起伏沖撞在合理的范圍,特別是新興行業(yè),領軍企業(yè)更得有胸懷和大局,需要在競爭中保持張弛有度,促成行業(yè)建立同盟和百花爭艷的健康持續(xù)的多贏局面,而非最后比的是誰狠,比的是誰更有底線,劣幣驅(qū)逐良幣,最終誰都逃不了要還的。'
text

適用精確模式對文本進行分詞:

a = jieba.cut(text, cut_all=False)
'|'.join(a)

jieba 在某些特定的情況下分詞,可能表現(xiàn)不是很好。比如一篇非常專業(yè)的醫(yī)學論文,含有一些特定領域的專有名詞。不過,為了解決此類問題, jieba 允許用戶自己添加該領域的自定義詞典,我們可以提前把這些詞加進自定義詞典當中,來增加分詞的效果。調(diào)用的方法是:jieba.load_userdic()。
自定義詞典的格式要求每一行一個詞,有三個部分,詞語,詞頻(詞語出現(xiàn)的頻率),詞性(名詞,動詞……)。其中,詞頻和詞性可省略。用戶自定義詞典可以直接用記事本創(chuàng)立即可,但是需要以 utf-8 編碼模式保存。 格式像下面這樣:

兇許 1 a
腦斧 2 b
福蝶 c
小局 4 
海疼

除了使用 jieba.load_userdic() 函數(shù)在分詞開始前加載自定義詞典之外,還有兩種方法在可以在程序中動態(tài)修改詞典。
使用 add_word(word, freq=None, tag=None) 和 del_word(word) 可在程序中動態(tài)修改詞典。
使用 suggest_freq(segment, tune=True) 可調(diào)節(jié)單個詞語的詞頻,使其能(或不能)被分出來。
使用自定義詞典,有時候可以取得更好的效果,例如「今天天氣不錯」這句話,本應該分出「今天」、「天氣」、「不錯」三個詞,而來看一下直接使用結巴分詞的結果:

string = '今天天氣不錯'
seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

可以看到結果并沒有被完整分割,這時候就可以加載自定義的詞典了,將「今天」和「天氣」兩個詞語添加到詞典中,并重新分詞:

jieba.suggest_freq(('今天', '天氣'), True)
seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

也可以從詞典直接刪除該詞語:

jieba.del_word('今天天氣')
seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

還有一種情況是「臺中」總是被切成「臺」和「中」,因為 P(臺中) < P(臺)×P(中),“臺中”詞頻不夠?qū)е缕涑稍~概率較低,這時候可以添加詞典,強制調(diào)高詞頻。

string = '臺中'
seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

強制調(diào)高「臺中」的詞頻,使它被分為一個詞:

jieba.add_word('臺中')
seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

利用 jiaba 來做一個簡單過濾器,這個在實際的應用中十分常用。比如有的詞【的】,【地】,【得】,對數(shù)據(jù)分析沒有什么實際作用,但是文章中大量的這類詞又會占據(jù)大量的存儲資源,因此我們想要過濾掉這類詞。
首先建立停用詞表,為了便于理解,我們直接建立一個小型的停用詞表。實際中常常需要一個由大量的停用詞組成的詞表。

stopwords = ('的', '地', '得')
stopwords

自定義待過濾的文本:

string = '我喜歡的和你討厭地以及最不想要得'
string

對 string 進行分詞操作,看看沒過濾之前的分詞結果;并將結果存放在一個 seg_list 中:

seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

接下來,查看過濾后結果。首先創(chuàng)建一個空數(shù)組來存放過濾后的詞語,然后通過循環(huán)迭代的方法,將過濾后的詞語依次添加到剛剛建立的空數(shù)組當中。

a = []
seg_list = jieba.cut(string, cut_all=False)
for word in seg_list:
    if word not in stopwords:
        a.append(word)
a

中文郵件文本分類實戰(zhàn)

文本分類簡介

一般而言,文本分類是指在一定的規(guī)則下,根據(jù)內(nèi)容自動確定文本類別這一過程。文本分類在實際場景中有諸多方面的應用,比如常見的有垃圾郵件分類,情感分析等,新聞分類等等。

按照分類要求的不同,文本分類主要可以分為二分類,多分類,多標簽分類三大類。
二分類問題:也是最基礎的分類,顧名思義是將文本歸為兩種類別,比如將正常郵件郵件劃分問題,垃圾郵件或者正常郵件。一段影評,判斷是好評還是差評的問題。
多分類問題:是將文本劃分為多個類別,比如將新聞歸為政治類,娛樂類,生活類等等。
多標簽分類:是給文本貼上多個不同的標簽,比如一部小說可以同時被劃分為多個主題,可能既是修仙小說,又是玄幻小說。

文本分類主要有兩種方法:傳統(tǒng)機器學習文本分類算法、深度學習文本分類算法。
傳統(tǒng)方法:特征提取 + 分類器。就是將文本轉換成固定維度的向量,然后送到分類器中進行分類。
深度學習方法:可以自動提取特征,實現(xiàn)端到端的訓練,有較強的特征表征能力,所以深度學習進行文本分類的效果往往要好于傳統(tǒng)的方法。

支持向量機

SVM 作為傳統(tǒng)機器學習的一個非常重要的分類算法,給定訓練樣本,支持向量機找到一個劃分超平面,將不同的類別劃分開來。通俗來講,這樣的超平面有很多,支持向量機就是要找到位于兩類訓練樣本「正中間」的劃分超平面。為了便于理解,我們用了下面圖中這個簡單的二維空間例子來講解支持向量機的基本原理,實際應用中常常是復雜的高維空間。


image.png

可以看到,圖中有很多的線都可以正確將樣本劃分為兩個類別,但是紅色的線位于兩個樣本的「正中間」位置。因為受噪聲和訓練集局限性的因素呢,訓練集外的樣本可能比訓練集的樣本更接近兩個類別的分界,這樣就會導致分類錯誤。恰恰紅色這條線受影響最小,支持向量機的目的就是要找到這條紅線。


image.png

圖中紅線為最大分類間隔超平面,虛線上的點是距離分界面最近的點集,這類點就稱為 「支持向量」。
超平面用線性方程式表示為:
image.png

其中


image.png

是權重值,決定了超平面的方向; b 是位移項,決定了超平面與原點之間的距離。
在空間中,任意一點xi到超平面的距離 r 公式為:
image.png

其中,∣ω∣ 是 ω 的 L2 范數(shù),L2范數(shù):比如 ω=(a,b,c), 那么
image.png

當且僅當,點在兩個異類支持向量集合上,分別使等號成立。此時兩個支持向量之間的「間隔」為:
image.png

顯然為了最大化間隔,我們需要找到最小的∣ω∣^ 2,這就是支持向量機的基本型。

中文郵件分類

垃圾郵件分類任務整個實驗步驟大致如下:
導入數(shù)據(jù),并進行分詞和剔除停用詞。
劃分訓練集和測試集。
將文本數(shù)據(jù)轉化為數(shù)字特征數(shù)據(jù)。
構建分類器。
訓練分類器。
測試分類器。
數(shù)據(jù)準備
本次用到的數(shù)據(jù)包含 3 個文件, ham_data.txt 文件里面包含 5000 條正常郵件樣本,spam_data.txt 文件里面包含 5001 個垃圾郵件樣本,stopwords 是停用詞表。

!wget - nc "http://labfile.oss.aliyuncs.com/courses/1208/ham_data.txt"
!wget - nc "http://labfile.oss.aliyuncs.com/courses/1208/spam_data.txt"
!wget - nc "http://labfile.oss.aliyuncs.com/courses/1208/stop_word.txt"

獲得了樣本之后,首先要做是給正常郵件和垃圾郵件貼上標簽,我們用 1 代表正常郵件,0 代表垃圾郵件。

path1 = 'ham_data.txt'  # 正常郵件存放地址
path2 = 'spam_data.txt'  # 垃圾郵件地址

用 utf-8 編碼模式打開正常樣本:

h = open(path1, encoding='utf-8')
h

因為我們準備的數(shù)據(jù)是每一行一封郵件,這里我們要用 readlines() 來以行來讀取文本的內(nèi)容。

h_data = h.readlines()
h_data[0:3]  # 顯示前3封正常郵件

同理方式處理垃圾樣本:

s = open(path2, encoding='utf-8')
s_data = s.readlines()
s_data[0:3]  # 顯示前3個封垃

讀取之后,我們的 h_data 數(shù)是由 5000 條郵件字符串組成的正常郵件樣本集, s_data 是由 5001 條郵件字符串組成的垃圾郵件樣本集。下面我們?yōu)閷蓚€樣本組合起來,并貼上標簽,將正常郵件的標簽設置為 1,垃圾郵件的標簽設置為 0。
生成一個 len(h_data) 長的的一維全 1 列表:

import numpy as np

h_labels = np.ones(len(h_data)).tolist()  # 生成一個len(h_data)長的的一維全1列表
h_labels[0:10]  # 我們顯示前10個數(shù)據(jù)

生成一個 len(s_data) 長的的一維全 0 列表:

s_labels = np.zeros(len(s_data)).tolist()
s_labels[0:10]  # 我們顯示前10個數(shù)據(jù)

拼接樣本集和標簽集:

datas = h_data + s_data  # 將正常樣本和垃圾樣本整合到datas當中
labels = h_labels + s_labels

這樣我們得到一個由所有郵件樣本組合起來的樣本集 datas 以及一個標簽集 labels。
因為我們沒有事先準備測試集,所以我們在 10001 個樣本當中,隨機劃出 25% 個樣本和標簽來作為我們的測試集,剩下的 75% 作為訓練集來進行訓練我們的分類器。這里我們可以用到 scikit-learn 工具里面的 train_test_split 類。

sklearn.model_selection.train_test_split(datas, labels, test_size=0.25, random_state=5 )

參數(shù)的意義:
datas : 樣本集
labels: 標簽集
train_test_split:劃分到測試集的比例
random_state:隨機種子,取同一個的隨機種子那么每次劃分出的測試集是一樣的。

from sklearn.model_selection import train_test_split

train_d, test_d, train_y, test_y = train_test_split(
    datas, labels, test_size=0.25, random_state=5)
train_y[0:30]

分詞
現(xiàn)在對文本進行分詞,將分詞設計成 tokenize_words 函數(shù),供后續(xù)直接調(diào)用。

import jieba


def tokenize_words(corpus):
    tokenized_words = jieba.cut(corpus)
    tokenized_words = [token.strip() for token in tokenized_words]
    return tokenized_words

驗證一下函數(shù):

string = '我愛自然語言處理'
b = tokenize_words(string)
b

去除停用詞
因為一些字是沒有實際意義的,比如:【的】【了】【得】等,因此要將其剔除。首先加載我們剛剛下載好的停用詞表。這里也可以自行在網(wǎng)上下載,編碼格式為 utf-8,每行一個停用詞。為了方便調(diào)用,我們將去除停用詞的操作放到 remove_stopwords 函數(shù)當中。

def remove_stopwords(corpus):  # 函數(shù)輸入為樣本集
    sw = open('stop_word.txt', encoding='utf-8')  # stopwords 停詞表
    sw_list = [l.strip() for l in sw]  # 去掉文本中的回車符,然后存放到 sw_list 當中
    # 調(diào)用前面定義好的分詞函數(shù)返回到 tokenized_data 當中
    tokenized_data = tokenize_words(corpus)
    # 過濾停用詞,對每個在 tokenized_data 中的詞 data 進行判斷
    # 如果 data 不在 sw_list 則添加到 filtered_data 當中
    filtered_data = [data for data in tokenized_data if data not in sw_list]
    # 用''將 filtered_data 串起來賦值給 filtered_datas
    filtered_datas = ' '.join(filtered_data)
    return filtered_datas  # 返回去停用詞之后的 datas

構建一個函數(shù)完成分詞和剔除停用詞。這里使用 tqdm 模塊顯示進度。

from tqdm.notebook import tqdm


def preprocessing_datas(datas):
    preprocessed_datas = []
    # 對 datas 當中的每一個 data 進行去停用詞操作
    # 并添加到上面剛剛建立的 preprocessed_datas 當中
    for data in tqdm(datas):
        data = remove_stopwords(data)
        preprocessed_datas.append(data)

    return preprocessed_datas  # 返回去停用詞之后的新的樣本集

用上面預處理函數(shù)對樣本集進行處理。

pred_train_d = preprocessing_datas(train_d)
pred_train_d[0]

同樣,對測試集進行預處理:

pred_test_d = preprocessing_datas(test_d)
pred_test_d[0]

我們得到了分詞過后并且去除停用詞了的樣本集 pred_train_d 和 測試集 pred_test_d。
特征提取
在進行分詞及去停用詞處理過后,得到的是一個分詞后的文本?,F(xiàn)在我們的分類器是 SVM,而 SVM 的輸入要求是數(shù)值型的特征。這意味著我們要將前面所進行預處理的文本數(shù)據(jù)進一步處理,將其轉換為數(shù)值型數(shù)據(jù)。轉換的方法有很多種,為了便于理解,這里使用 TF-IDF 方法。為了更好的理解 TF-IDF,我們先從詞袋模型開始講解。
詞袋模型
詞袋模型是最原始的一類特征集,忽略掉了文本的語法和語序,用一組無序的單詞序列來表達一段文字或者一個文檔??梢赃@樣理解,把整個文檔集的所有出現(xiàn)的詞都丟進袋子里面,然后無序的排出來(去掉重復的)。對每一個文檔,按照詞語出現(xiàn)的次數(shù)來表示文檔。

句子1:我/有/一個/蘋果
句子2:我/明天/去/一個/地方
句子3:你/到/一個/地方
句子4:我/有/我/最愛的/你
把所有詞丟進一個袋子:我,有,一個,蘋果,明天,去,地方,你,到,最愛的。這 4 句話中總共出現(xiàn)了這 10 個詞。
現(xiàn)在我們建立一個無序列表:我,有,一個,蘋果,明天,去,地方,你,到,最愛的。并根據(jù)每個句子中詞語出現(xiàn)的次數(shù)來表示每個句子。

image.png

句子 1 特征: ( 1 , 1 , 1 , 1 , 0 , 0 , 0 , 0 , 0 , 0 )
句子 2 特征: ( 1 , 0 , 1 , 0 , 1 , 1 , 1 , 0 , 0 , 0 )
句子 3 特征: ( 0 , 0 , 1 , 0 , 0 , 0 , 1 , 1 , 1 , 0 )
句子 4 特征: ( 2 , 1 , 0 , 0 , 0 , 0 , 0 , 1 , 0 , 1 )
這樣的一種特征表示,我們就稱之為詞袋模型的特征。
TF-IDF 模型
這種模型主要是用詞匯的統(tǒng)計特征來作為特征集。TF-IDF 由兩部分組成:TF(Term frequency,詞頻),IDF(Inverse document frequency,逆文檔頻率)兩部分組成。
TF:
image.png

其中分子nij表示詞i 在文檔j 中出現(xiàn)的頻次。分母則是所有詞頻次的總和,也就是所有詞的個數(shù)。

舉個例子:
句子1:上帝/是/一個/女孩
句子2:桌子/上/有/一個/蘋果
句子3:小明/是/老師
句子4:我/有/我/最喜歡/的/
每個句子中詞語的 TF :


image.png

IDF:


image.png

其中∣D∣ 代表文檔的總數(shù),分母部分∣Di∣ 則是代表文檔集中含有i詞的文檔數(shù)。原始公式是分母沒有+1的,這里+1是采用了拉普拉斯平滑,避免了有部分新的詞沒有在語料庫中出現(xiàn)而導致分母為零的情況出現(xiàn)。
用 IDF 計算公式計算句子中每個詞的 IDF 值:
image.png

最后,把 TF 和 IDF 兩個值相乘就可以得到 TF-IDF 的值。即:
image.png

每個句子中,詞語的 TF-IDF 值:
image.png

把每個句子中每個詞的 TF-IDF 值 添加到向量表示出來就是每個句子的 TF-IDF 特征。
句子 1 的特征:


image.png

同樣的方法得到句子 2,3,4 的特征。
在 Python 當中,我們可以通過 scikit-learn 來實現(xiàn) TF-IDF 模型。這里主要用到了 TfidfVectorizer() 類。
sklearn.feature_extraction.text.TfidfVectorizer(min_df=1,norm='l2',smooth_idf=True,use_idf=True,ngram_range=(1,1))

min_df: 忽略掉詞頻嚴格低于定閾值的詞。
norm :標準化詞條向量所用的規(guī)范。
smooth_idf:添加一個平滑 IDF 權重,即 IDF 的分母是否使用平滑,防止 0 權重的出現(xiàn)。
use_idf: 啟用 IDF 逆文檔頻率重新加權。
ngram_range:同詞袋模型

首先加載 TfidfVectorizer 類,并定義 TF-IDF 模型訓練器 vectorizer 。

from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(
    min_df=1, norm='l2', smooth_idf=True, use_idf=True, ngram_range=(1, 1))

對預處理過后的 pred_train_d 進行特征提?。?/p>

tfidf_train_features = vectorizer.fit_transform(pred_train_d)
tfidf_train_features

通過這一步,我們得到了 7500 個 28335 維數(shù)的向量作為我們的訓練特征集。我們可以查看轉換結果,這里為了便于觀察,使用 toarray 方法轉換成為數(shù)組數(shù)據(jù)。

tfidf_train_features.toarray()[0]

用訓練集訓練好特征后的 vectorizer 來提取測試集的特征: 注意這里不能用 vectorizer.fit_transform() 要用 vectorizer.transform(),否則,將會對測試集單獨訓練 TF-IDF 模型,而不是在訓練集的詞數(shù)量基礎上做訓練。這樣詞總量跟訓練集不一樣多,排序也不一樣,將會導致維數(shù)不同,最終無法完成測試。

tfidf_test_features = vectorizer.transform(pred_test_d)
tfidf_test_features

完成之后,我們得到 2501 個 28335 維數(shù)的向量作為我們的測試特征集。
分類
在獲得 TF-IDF 特征之后,我們可以調(diào)用 SGDClassifier() 類來訓練 SVM 分類器。

sklearn.linear_model.SGDClassifier(loss='hinge')

SGDClassifier 是一個多個分類器的組合,當參數(shù) loss='hinge' 時是一個支持向量機分類器。
加載 SVM 分類器,并調(diào)整 loss = 'hinge'。

from sklearn.linear_model import SGDClassifier

svm = SGDClassifier(loss='hinge')

然后我們將之前準備好的樣本集和樣本標簽送進 SVM 分類器進行訓練。

svm.fit(tfidf_train_features, train_y)

接下來我們用測試集來測試一下分類器的效果。

predictions = svm.predict(tfidf_test_features)
predictions

為了直觀顯示分類的結果,我們用 scikit-learn 庫中的 accuracy_score 函數(shù)來計算一下分類器的準確率 。

sklearn.metrics.accuracy_score(test_l, prediction)

這個函數(shù)的作用是為了計算 test_l 中與 prediction 相同的比例。即準確率。
用測試標簽和預測結果 計算分類準確率。np.round(X,2) 的作用是 X 四舍五入后保留小數(shù)點后 2 位數(shù)字。

from sklearn import metrics

accuracy_score = np.round(metrics.accuracy_score(test_y, predictions), 2)
accuracy_score

隨機提取一個樣本查看其預測結果:

print('郵件類型:', test_y[20])
print('預測郵件類型:', predictions[20])
print('文本:', test_d[20])
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容