MaDi's Blog

一個紀錄自己在轉職軟體工程師路上的學習小空間

0%

NLP斷詞統計分析(I)-ngram、jieba、文字雲

在實作NLP自然語言處理的時候,常常會需要做斷詞的統計分析,大多時候是為了統計哪一個詞出現最多次,以作為分析的要點。

所以,本篇文章簡單的應用某電商評論網爬下來的評論做字詞處理,剔除停用字之後找出頻率最高的字詞,並做成文字雲。

jieba斷詞

jieba是中文斷詞最常用的套件之一

共分為三種模式:

  1. 精確模式精確地切開句子,適合文本分析。
  2. 全模式:把句子中所有可以成詞的詞語都掃描出來,速度非常快,但不太精準
  3. 搜尋引擎模式:在精確模式的基礎上,對長詞再次切分,提高recall,常用於搜尋引擎。

預設使用精確模式

1
2
3
4
import jieba

txt = '人民有錢,國家安全'
print('|'.join(jieba.cut(txt, cut_all=False)))

人民|有|錢|,|國家|安全

另外,也可以透過詞性去斷詞:

1
2
3
4
5
import jieba.posseg as pseg

text = '今日流的口水,會是明日流的汗水' #節錄自博班學長的精神喊話
words = pseg.cut(text)
print([i for i in words])

[pair(‘今日’, ‘t’), pair(‘流’, ‘v’), pair(‘的’, ‘uj’), pair(‘口水’, ‘n’), pair(‘,’, ‘x’), pair(‘會’, ‘v’), pair(‘是’, ‘v’), pair(‘明日’, ‘t’), pair(‘流’, ‘v’), pair(‘的’, ‘uj’), pair(‘汗水’, ‘n’)]

每個pair裏頭的第二個元素就是詞性,當詞性是x的時候,代表無意義,比如說這句裏頭的逗號。所以可以剔除掉詞性無意義的字詞。

1
print('|'.join([word for word, flag in words if flag != 'x']))

今日|流|的|口水|會|是|明日|流|的|汗水

tf-idf (term frequency–inverse document frequency)

維基百科:

  1. 是一種用於資訊檢索與文字挖掘的常用加權技術。

  2. tf-idf是一種統計方法,用以評估一字詞對於一個檔案集或一個語料庫中的其中一份檔案的重要程度。字詞的重要性隨著它在檔案中出現的次數成正比增加,但同時會隨著它在語料庫中出現的頻率成反比下降。

  3. tf-idf加權的各種形式常被搜尋引擎應用,作為檔案與用戶查詢之間相關程度的度量或評級。

數學式長這樣:

$W_x,_y = tf_x,_y \times \log(\frac{N}{df_x})$

$W_x,_y$ : 文本y裡面出現x的權重
$tf_x,_y$ : 文本y裡出現x的頻率
N : 文本總字詞數量
$df_x$ : 含有x的文本數量

這邊節錄金庸小說裡面的某段文字做分析:

1
2
3
4
5
6
7
text = ''' 黃藥師接在手中,觸手似覺包中是個人頭,打將開來,赫然是個新割下的首級,頭戴方巾,
頦下有須,面目卻不相識。歐陽鋒笑道:「兄弟今晨西來,在一所書院歇足,聽得這腐儒在對學生講書,
說甚麼要做忠臣孝子,兄弟聽得厭煩,將這腐儒殺了。你我東邪西毒,可說是臭味相投了。」說罷縱聲長笑。
黃藥師臉上色變,說道:「我平生最敬的是忠臣孝子。」俯身抓土成坑,將那人頭埋下,恭恭敬敬的作了三個揖。
歐陽鋒討了個沒趣,哈哈笑道:「黃老邪徒有虛名,原來也是個為禮法所拘之人。」
黃藥師凜然道:「忠孝乃大節所在,並非禮法!」
'''

載入套件

1
import jieba.analyse

extract_tags函式把文本中的關鍵字詞抓出來

1
2
# withWeight=True -> 返回權重
jieba.analyse.extract_tags(text, topK=5, withWeight=True)

[(‘歐陽鋒’, 0.3919595902590164),
(‘聽得’, 0.3919595902590164),
(‘禮法’, 0.3919595902590164),
(‘忠臣孝子’, 0.38026532980327865),
(‘腐儒’, 0.3771404058754098)]

1
2
# withWeight=False -> 返回排名
jieba.analyse.extract_tags(text, topK=5, withWeight=False)

[‘歐陽鋒’, ‘聽得’, ‘禮法’, ‘忠臣孝子’, ‘腐儒’]

停用字(stopWords)

在做斷詞分析的時候,為了避免不必要的字詞進入統計而造成雜訊。我們常常會把他們剔除掉,而這些沒有用的字詞就稱作停用字

例如: 的、不但、可是、而且、>…

常用的停用字可以在網路上下載到英文、中文的版本,有不同的機構與學校推出來的版本,端看你想分析的問題用哪一個比較好。大部分的檔案類型都是txt,用程式讀取進來就可以做停用字的剔除。

ngram: 基本NLP模型

ngram是一種語言模型(Language Model),是學習NLP自然語言處理的入門模型。透過ngram可以統計出各個不同長度的字詞出現的次數,

語言模型,指的是用來計算一個句子的機率的模型。

一個句子(S)是由一連串的字詞(Wi)所組成:

S = W1,W2,W3…

而一個句子中每個字出現的機率就用P(S)來代表。

舉例來說,當有一句話長這樣 下雨天,等等出門記得帶__,這個空格大多數人都會填雨傘。而ngram模型就是透過機率的方式去找出填空中出現機率最大的字詞。

談到機率模型,我們採用馬可夫鍊的假設,一個句子中第i個字會跟整句第一個字到第i-1個字有關

白話來說,就是指下一個字要填什麼跟前文有關,但是只需跟前幾個字有關,不需要回溯至整段句子,此舉可以減少機率的計算量。

依據馬可夫鍊的假設,我們可以定義最簡單的機率模型為:

1
P(S) = P(W1)*P(W2)*...*P(Wn)

代表所有字詞都是獨立的,與前後文無關,因為模型每次只取一個字詞,所以又稱一元語言模型(unigram)**。同理,若每次取到兩個或三個字詞,則稱為二元語言模型(bigram)、三元語言模型(trigram)**。

而每個字詞的機率就用 Maximum Likelihood Estimation 來計算。

舉例來說,假設有一句話:

川普拜登誰會當選總統

而每個字詞出現的機率就直接用Maximum Likelihood的方式來計算。

P(川)=1/10,P(普)=1/10,P(拜)=1/10…

如果是用unigram的模型,整句的機率就是

P(川普拜登誰會當選總統)=P(川) * P(普) * … * P(統)

也就是這十個不同的字詞會形成一個十維的向量,每個維度分別存有該字詞的機率。

如果今天用trigram的模型去推下一個字詞的機率,也就是說每三個字詞作分析統計,數學式長這樣:

利用 統計次數來相除就可以求得下一個字詞的機率。 (程式碼在後頭)

這時候會不禁想問…

既然取到三個字的trigram表現不錯,那為什麼不取到四元模型(four-gram)、五元模型(five-gram)呢?

原因有兩個:

  1. 當取的字數太多的時候,會有太多字與字的組合,導致維度爆炸
  2. 當字數取得愈多,整體出現的機率愈低,甚至會低到等於零(沒有出現在語料庫)
    ex: P(今天下大雨不想去上班)=0

程式碼實作

1. 資料預處理,剔除停用字:

先從某電商評論網爬下資料

Review欄位的所有語句蒐集起來做成語料庫(corpus)。

1
2
3
# 建立語料庫
corpus = "".join(df['Review'].values)
corpus[:100]

‘差评,才买的连电源充不上电,问售后不知道咋回事,不知道干啥吃的东西出问题直接推卸厂家解决。\n原装充电头与变压器接口不匹配。\n赠送的HP转接器:HDMI接口无法正常连接,电脑无法识别显示器用了快5天了,’

載入停用字(stopWords)做剔除

1
2
3
4
5
6
7
8
9
10
11
12
stopWords=[]

# 自己上網下載的
with open('./stopwords_download/cn_stopwords.txt', 'r', encoding='UTF-8') as file:
for data in file.readlines():
data = data.strip()
stopWords.append(data)

# 移除停用字及跳行符號
remaind = list(filter(lambda a: a not in stopWords and a != '\n' and a!= '\r' and a!= ' ', corpus))
remaind = "".join(remaind)
remaind[:200]

‘差评买电源充电问售知道回事知道干吃东西出问题直接推卸厂家解决原装充电头变压器接口匹配赠送HP转接器HDMI接口法正常接电脑法识显示器快天运行速度行蛮漂亮时候开机时候会出现错误音响时候会炸音机箱面划痕开机杂音LOL界面切换会闪屏买电脑音响问题换样破音声音.刚两天扬声器没声音.运行速度快点软件半天没反应.机时间短时cpu温度忽高忽低散热.包装简陋.赠品电源适配器.十天左右时间价格直降刚买周声卡坏网页超’

2. 建立ngram統計字數的函式

自己建立n-gram函式

1
2
3
4
5
# 計算句子input_strs裏頭長度為length的字詞
def ngrams(input_strs, length):
for input_str in input_strs.split("\n"):
for i in range(len(input_str)-length+1):
yield input_str[i:i+length]

載入Counter套件來統計次數。

1
from collections import Counter

unigram

1
2
uni_freq = Counter(ngrams(remaind, 1))
print("".join([" %s : %s \n" % (w[0], w[1]) for w in uni_freq.most_common(20)]))

电 : 1700
没 : 1228
买 : 1224
脑 : 1161
机 : 1155
开 : 923

bigram

1
2
bi_freq = Counter(ngrams(remaind, 2))
print("".join([" %s : %s \n" % (w[0], w[1]) for w in bi_freq.most_common(20)]))

电脑 : 1153
问题 : 645
客服 : 559
开机 : 426
屏幕 : 361

trigram以上的語句處理也是一樣的程序,就不再贅述。

3. 應用馬可夫鍊假設的ngram模型

接著,用程式碼來實作一下馬克夫鍊假設的機率模型。

首先,是**一元語言模型(unigram)**,假設所有的字出現的機率都是獨立事件,與前文無關。

$P(w_1, w_2, w_3, w_4, …, w_n) =P(w_1) \times P(w_2) \times P(w_3) \times … \times P(w_n)$

$P(w_i) = \frac{Count(w_i)}{Count(all_words)}$

1
2
3
4
def uni_prob(w0):
count_uni_freq = uni_freq.get(w0, 0) #查w0這個字出現的次數,若查不到就回傳0(機率為零)
count_total = len(corpus)
return count_uni_freq/count_total

我們在這裡用這兩句話來測試unigram各字詞的機率

第一句: 没电脑包
第二句: 脑包没电

1
2
print(uni_prob("没")*uni_prob("电")*uni_prob("脑")*uni_prob("包"))
print(uni_prob("脑")*uni_prob("包")*uni_prob("没")*uni_prob("电"))

9.988092183101805e-10
9.988092183101807e-10

可以看到同樣的字句在不同排列下,unigram計算出來的句子機率是相同的,符合預期,因為我們假設每個字詞都是獨立事件。

再來看看**二元語言模型(bi-gram)**,bigram是假設下一個字出現的機率與上一個字有關,所以會考慮前一個字的機率,也就是會有條件機率的概念。

$P(w_1, w_2, w_3, w_4, …, w_n) =P(w_1) \times P(w_2|w_1) \times P(w_3|w_2) \times … \times P(w_n|w_{n-1})$

$P(w_2|w_1) =\frac{P(w_1, w_2)}{P(w_1)}
\approx \frac{Count(w_1, w_2)}{Count(w_1)}$

1
2
3
4
def bi_prob(w1, w0):
count_bi_freq = bi_freq.get(w0+w1, 0)
count_uni_freq = uni_freq.get(w0, 1)
return count_bi_freq/count_uni_freq
1
2
print(uni_prob("没")*bi_prob("电","没")*bi_prob("脑","电")*bi_prob("包","脑"))
print(uni_prob("脑")*bi_prob("包","脑")*bi_prob("没","包")*bi_prob("电","没"))

7.1921451918833e-06
8.034481451300942e-07

結果可以看到用bigram來計算,没电脑包脑包没电有更大的機率出現在句子中。

最後是**三元語言模型(tri-gram)**,trigram假設所有的字出現的機率,僅和前一個字以及前兩個字有關,一樣是用條件機率來計算。

$P(w_1, w_2, w_3, w_4, …, w_n) =P(w_1) \times P(w_2|w_1) \times P(w_3|w_2,w_1) \times … \times P(w_n|w_{n-1},w_{n-2})$

$P(w_3|w_1,w_2) =\frac{P(w_1, w_2,w_3)}{P(w_1,w_2)}
\approx \frac{Count(w_1, w_2,w_3)}{Count(w_1,w_2)}$

1
2
3
4
def tri_prob(w2, w0, w1):
count_tri_freq = tri_freq.get(w0+w1+w2, 0)
count_bi_freq = bi_freq.get(w0+w1, 1)
return count_tri_freq/count_bi_freq
1
2
print(uni_prob("没")*bi_prob("电","没")*tri_prob("脑","没","电")*tri_prob("包","电","脑"))
print(uni_prob("脑")*bi_prob("包","脑")*tri_prob("没","脑","包")*tri_prob("电","包","没"))

6.673612562621843e-06
3.4793314330830896e-06

從結果可以看出,用trigram來計算句子的機率,一樣是没电脑包脑包没电有更大的機率出現在句子中。但不知為何,判斷成脑包没电的機率卻意外的高XD

4. 應用ngram於輸入法預測下文

因為ngram可以計算句子出現的機率,所以也可以作為輸入下文的預測,也就是說當使用者輸入一個字,透過n元語言模型的計算,自動算出接下來n-1個字詞中機率最大(最有可能填)的字詞。

舉例來說,我們用同樣的資料(某電商評論網),模型選用trigram,此時我們輸入第一個字 ,並用trigram計算接下來最有可能的兩個字詞。

$P(w_2,w_3 |w_1= 送 ) =\frac{P(w_1,w_2,w_3) }{ P(w_1= 送 )}$

$=P(w_1=送)\times P(w_2|w_1= 送 )\times P(w_3 |w_2,w_1= 送 ) \times \frac{1}{ P(w_1= 送 ) }$

$= P(w_2|w_1= 送 ) \times P(w_3 |w_2,w_1= 送 )$

1
2
3
4
5
6
7
8
9
10
11
12
13
def find_NextWords(w1, N):
g3s = list(filter(lambda x: x[0] == w1, tri_freq.keys()))
results = []
if len(g3s)==0:
print('資料中沒有這個字!')
for g3 in g3s:
w1, w2, w3 = g3
p = bi_prob(w2, w1)*tri_prob(w3, w1, w2)
if p>0:
results.append((p, w2+w3))
results = sorted(results, key=lambda x: x[0], reverse=True)[:N]
for i, w in enumerate(results):
print(f"第{i+1}名. ", round(w[0],4), w[1])

並找出之後最有可能填的前五名候選字

1
find_NextWords('送', 5)

第1名. 0.1382 鼠标
第2名. 0.0323 电脑
第3名. 0.0184 电话
第4名. 0.0138 服务
第5名. 0.0138 货速

5. jieba斷詞

除了ngram可以分詞以外,我們也可以用jieba來斷詞。

1
2
3
4
5
6
7
8
segments = jieba.cut(corpus, cut_all=False)

# 移除停用詞及跳行符號
remainderWords = list(filter(lambda a: a not in stopWords and a != '\n' and a!= '\r' and a!= ' ', segments))

# 印出過濾後的分詞
for k in remainderWords[:5]:
print(k)

差评

电源
充不上

1
2
jieba_freq = Counter(remainderWords)
jieba_freq.most_common(100)

[(‘电脑’, 1060),
(‘买’, 778),
(‘问题’, 621),
(‘客服’, 558),
(‘说’, 508),

6. 文字雲

最後,一旦我們把文本都斷好詞之後,可以統計出各字詞的出現次數,這時候就可以用文字雲這個視覺化的圖形來呈現。

1
2
3
4
5
6
7
8
9
10
11
12
 from matplotlib import pyplot as plt
from wordcloud import WordCloud
%matplotlib inline

# 畫文字雲
def plt_WordCloud(word_freq):
height, width = 600, 800
wc = WordCloud(font_path="simsun.ttc", height=height, width=width).generate_from_frequencies(dict(word_freq))
plt.figure(figsize=(width/96.,height/96.)) #pixel to inch
plt.imshow(wc)
plt.axis("off")
plt.show()
1
2
3
4
5
6
7
8
# unigram
generate_wc(uni_freq.most_common(200))

# bigram
generate_wc(bi_freq.most_common(200))

# jieba
generate_wc(jieba_freq)

總結

本文利用某電商評論網爬下來的評論做斷詞統計分析,資料方面先剔除停用字,接著透過ngram、jieba兩個方法來斷詞,並實作ngram機率的概念,計算出每種句子的出現機率,除此之外也實作簡單的輸入法預測下文的模型,最後統計斷詞後各個字詞的出現次數,並以視覺化的文字雲呈現。

因為本文爬取的某電商評論網的資料是中文,所以就以中文為主的jieba來斷詞,除此之外,其實有另一套NLTK這個強大的套件可以協助斷詞,但主要是英文語系,之後再用別的英文資料來練習看看NLTK的斷詞分析。

參考