最大熵模型-詞性標註的示例
有關於最大熵模型今天再放3個詞性標註示例。一個是公開實現的示例,這個示例之前忘記是從哪兒找到的了,最近搜尋各種最大熵演示,找到了出處,應該是煉數成金髮布出來的。其他的全是一個版本,所以就是要錯一起全錯。有關於公開示例程式碼的問題已經在前邊的文章baiziyu:最大熵模型應用於詞義消岐任務也說當前公開的演示程式碼的問題 中說明了,這裡不再贅述。有關於最大熵模型庫可以看這篇文章baiziyu:最大熵模型可用工具小結 ,其中HanLP作者實現了IIS和GIS,那篇文章裡沒有列IIS的連結。另外張樂的那個實在是用不下去,用起來特別不順手,又沒有文件,Python呼叫的部分還只能用Python2。沒有閒工夫跟程式碼較勁。所以還是推薦大家用NLTK吧。如果牽扯到實際應用,似乎Stanford的CoreNLP也有最大熵模型API。今天這篇文章之所以再次放上這個程式碼是因為我添加了註釋。訓練語料格式如下:
從/p 黃土/n ,/w 從/p 歷史/n ,/w
從/p 生活/vn 的/u 縫隙/n 間/f
生/v 出/v 的/u 不起眼/a 的/u 蒲公英/n
語料需要分詞並帶有詞性標註。
示例1 用Python實現最大熵模型IIS演算法,並應用於詞性標註任務。
出處應該是煉數成金。
# encoding: utf-8
“”“
最大熵詞形標註示例
”“”
from
collections
import
defaultdict
import
math
import
codecs
class
MaxEnt
(
object
):
def
__init__
(
self
):
# 儲存特徵函式fi(x,y)關於經驗分佈p(x,y)的期望值 Ep_(fi)
self
。
feats
=
defaultdict
(
int
)
# 文字經過特徵抽取後建成的訓練集每一項用一個事件表示(詞性標記,特徵1,特徵2,特徵3),
self
。
trainset
=
[]
# 詞性標記集,表示Y可取的值的集合,比如名詞、動詞、形容詞。。。
self
。
labels
=
set
()
def
generate_events
(
self
,
text
,
train_flag
=
False
):
“”“
text: 以空格分隔的文字
train_flag: train_flag=True時,要求text必須標註好詞語詞性
功能:對輸入的一句話,抽取出事件列表
”“”
# 定義事件序列,注意輸入的1行文字可以抽取出多個事件,事件序列稱為資料集或許更合適
event_li
=
[]
# 分詞,要求輸入的字串中詞語之間以空白分隔,對於訓練集在詞語後還要加詞性標記,格式為word/pos
word_li
=
text
。
split
()
# 分離詞語和詞性
if
train_flag
:
# 對於含有詞性標記的詞語序列,分離詞語和詞性:[(word1,pos1),(word2,pos2),。。。]
word_li
=
[
tuple
(
w
。
split
(
u
‘/’
))
for
w
in
word_li
if
len
(
w
。
split
(
u
‘/’
))
==
2
]
else
:
# 對於沒有詞性標記的詞語序列,新增詞性“x_pos”:[(word1,‘x_pos’),(word2,‘x_pos’),。。。]
word_li
=
[(
w
,
u
‘x_pos’
)
for
w
in
word_li
]
# 為詞語序列新增頭元素和尾元素,便於後續抽取事件
word_li
=
[(
u
‘pre1’
,
u
‘pre1_pos’
)]
+
word_li
+
[(
u
‘pro1’
,
u
‘pro1_pos’
)]
# 遍歷中心詞抽取1個事件,每個事件由1個詞性標記和多個特徵項構成
for
i
in
range
(
1
,
len
(
word_li
)
-
1
):
# 特徵函式1 中心詞
fea_1
=
word_li
[
i
][
0
]
# 特徵函式2 前一個詞
fea_2
=
word_li
[
i
-
1
][
0
]
# 特徵函式3 後一個詞
fea_3
=
word_li
[
i
+
1
][
0
]
# 詞性y
y
=
word_li
[
i
][
1
]
# 構建1個事件,注意1個事件由3個特徵項構成,同一事件中的3個特徵項共享1個輸出標記y
# 因此1個事件對應3個特徵函式或者叫做聯合特徵:f1(fea_1,y),f2(fea_2,y),f3(fea_3,y)
# 注意這裡並沒有區分3個特徵的先後順序
fields
=
[
y
,
fea_3
,
fea_2
,
fea_1
]
# 將事件新增到事件序列
event_li
。
append
(
fields
)
# 返回事件序列
return
event_li
def
load_data
(
self
,
file
):
“”“
file: 訓練檔案路徑和檔名
功能:讀取文字資料構建訓練集
”“”
with
codecs
。
open
(
‘。。/data/199801。txt’
,
‘rb’
,
‘utf8’
,
‘ignore’
)
as
infile
:
for
line_ser
,
line
in
enumerate
(
infile
):
if
line_ser
>=
100
:
break
line
=
line
。
strip
()
if
line
:
# 抽取事件序列
events_li
=
self
。
generate_events
(
line
,
train_flag
=
True
)
# 遍歷每個事件,對每個事件構建3個特徵函式或者叫做聯合特徵fi(x,y)
for
event
in
events_li
:
# 第1列是輸出詞性標記y
label
=
event
[
0
]
# 更新詞性標記
self
。
labels
。
add
(
label
)
# 對聯合特徵(詞性標記,特徵)計數,以便後續計算P(X,Y)的經驗分佈
# 注意一個聯合特徵為(y,x)的組合,x表示一個特徵值,並且丟失掉了x的位置資訊
for
f
in
set
(
event
[
1
:]):
self
。
feats
[(
label
,
f
)]
+=
1
# 將事件新增到訓練集
self
。
trainset
。
append
(
event
)
def
_initparams
(
self
):
“”“
功能:根據load_data方法載入進來的事件集(訓練集)計算Ep_(fi),並且給聯合特徵(y,x)編號
”“”
# 訓練集中抽取的事件總數
self
。
N
=
len
(
self
。
trainset
)
# 1個事件對應的特徵數量,取值為遍歷訓練集中每個事件抽取出的最大特徵數量
self
。
M
=
max
([
len
(
record
)
-
1
for
record
in
self
。
trainset
])
# 計算每一種特徵函式fi(x,y)關於經驗分佈P_(X,Y)的期望值,指的是《統計學習方法》P82頁最下邊的式子
self
。
ep_
=
[
0。0
]
*
len
(
self
。
feats
)
# 計算Ep_(fi),並且用self。feats儲存聯合特徵的索引
for
i
,
union_feature
in
enumerate
(
self
。
feats
):
# 第i個特徵函式的經驗值=聯合特徵(y,x)出現次數/訓練集中事件總數。
self
。
ep_
[
i
]
=
float
(
self
。
feats
[
union_feature
])
/
float
(
self
。
N
)
# 記錄特徵函式索引,經過這一步後self。feats中記錄的是特徵函式的索引值,以便後續預測時讀取特徵函式對應的權值
self
。
feats
[
union_feature
]
=
i
# 權重序列初始化為全0,每一個特徵函式都對應一個權重值,就是所求的係數
self
。
w
=
[
0。0
]
*
len
(
self
。
feats
)
self
。
lastw
=
self
。
w
def
Ep
(
self
):
“”“
計算Ep(fi) = sum (x,y) [p_(x)*p(y|X)fi(x,y)] 即《統計學習方法》P83頁最上邊的式子
此過程涉及呼叫模型預測方法self。calprob,預測輸出p(y|X)
經驗分佈p_(x)剛好等於 1/訓練集樣本數,實際它的公式應該為p_(x)=v(X=x)/N
”“”
# 期望值的個數等於特徵函式的個數
ep
=
[
0。0
]
*
len
(
self
。
feats
)
# 遍歷每個事件
for
record
in
self
。
trainset
:
# 分離出特徵集
features
=
record
[
1
:]
# 計算特徵集的預測輸出P(y|X)
prob_li
=
self
。
calprob
(
features
)
# 遍歷每一個特徵值
for
x
in
features
:
# 遍歷每個標記和其對應的機率值
for
prob
,
y
in
prob_li
:
# 由標記和1個特徵構建出1個特徵函式fi(y,x)
if
(
y
,
x
)
in
self
。
feats
:
# 讀取出特徵函式的索引值
idx
=
self
。
feats
[(
y
,
x
)]
# p_(x)的經驗分佈計算 p(x)=1/N
ep
[
idx
]
+=
prob
*
(
1。0
/
self
。
N
)
return
ep
def
_convergence
(
self
,
lastw
,
w
):
“”“
lastw:上一次迭代的係數向量
w: 本次迭代的係數向量
返回:True表示已經收斂;False表示未收斂
”“”
for
w1
,
w2
in
zip
(
lastw
,
w
):
if
abs
(
w1
-
w2
)
>=
0。01
:
return
False
return
True
# 訓練過程
def
train
(
self
,
max_iter
=
1000
):
“”“
最大熵分詞模型訓練過程
:param max_iter: 模型訓練的最大迭代次數
:return:無
”“”
# 初始化模型引數
self
。
_initparams
()
# 開始迭代
for
i
in
range
(
max_iter
):
(
‘iter
%d
。。。’
%
(
i
+
1
))
# 計算特徵函式關於模型P(Y|X)和經驗分佈P(X)的期望值序列,每個特徵函式對應一個值
ep
=
self
。
Ep
()
# 儲存當前的權值向量
self
。
lastw
=
self
。
w
[:]
# 計算delta並更新權值向量
for
i
,
w
in
enumerate
(
self
。
w
):
# 計算delta
delta
=
1。0
/
self
。
M
*
math
。
log
(
self
。
ep_
[
i
]
/
ep
[
i
])
# 更新權值
self
。
w
[
i
]
+=
delta
# 檢查是否收斂
if
self
。
_convergence
(
self
。
lastw
,
self
。
w
):
break
def
probwgt
(
self
,
features
,
label
):
“”“
features: 特徵集
label: 候選標記
返回: P(y|X)的機率值
功能:給定(X,y)計算條件機率P(y|X)
”“”
wgt
=
0。0
# 遍歷每個特徵f
for
f
in
features
:
# 如果訓練集中見過特徵函式(y,f)則讀出權重
if
(
label
,
f
)
in
self
。
feats
:
# 累加權重
wgt
+=
self
。
w
[
self
。
feats
[(
label
,
f
)]]
# 取exp(權重和)作為預測值,這裡沒有進行歸一化
return
math
。
exp
(
wgt
)
# 最大熵模型的預測函式P(y|x),輸入為1個事件中的特徵x,輸出為標記集中每種標記對應的歸一化的機率P(y1|x),P(y2|x),。。。
# 返回[(p1, y1),(p2,y2),。。。]
def
calprob
(
self
,
features
):
“”“
features: 特徵集
返回:預測為標記集self。labels中每種標記的機率
功能:給定特徵集X,輸出預測為標記集中每種標記的機率P(y|X)
”“”
# 計算標記集中每種標記的條件機率
wgts
=
[(
self
。
probwgt
(
features
,
l
),
l
)
for
l
in
self
。
labels
]
# 使每個機率值相加後等於1
Z
=
sum
([
w
for
w
,
l
in
wgts
])
prob
=
[(
w
/
Z
,
l
)
for
w
,
l
in
wgts
]
return
prob
def
predict
(
self
,
text
):
“”“
text: 輸入的經過分詞但未經詞性標註的文字
返回:詞性標註後的文字
功能:對經過分詞的文字進行詞性標註
”“”
word_li
=
[]
events_li
=
self
。
generate_events
(
text
)
for
features
in
events_li
:
prob
=
self
。
calprob
(
features
)
prob
。
sort
(
reverse
=
True
)
word_li
。
append
(
u
‘
%s
/
%s
’
%
(
features
[
1
],
prob
[
0
]))
return
word_li
if
__name__
==
“__main__”
:
maxent
=
MaxEnt
()
maxent
。
load_data
(
“。。/data/199801。txt”
)
maxent
。
train
(
50
)
word_li
=
maxent
。
predict
(
“中國 政府 將 繼續 堅持 奉行 獨立自主 的 和平 外交 政策 。”
)
(
word_li
)
示例2 NLTK最大熵模型庫實現詞性標註
# coding:utf-8
“”“
NLTK的最大熵模型實現詞性標註
”“”
import nltk
import codecs
# 事件生成器,1個分詞後的句子可以產生多個事件
def generate_events(word_li):
events = []
# 為詞語序列新增頭元素和尾元素,便於後續抽取事件
word_li = [(u‘pre1’, u‘pre1_pos’)] + word_li + [(u‘pro1’, u‘pro1_pos’)]
# 每個中心詞抽取1個event,每個event由1個詞性標記和多個特徵項構成
for i in range(1, len(word_li) - 1):
# 定義特徵詞典
features_dict = dict()
# 特徵函式a 中心詞
features_dict[‘fea_1’] = word_li[i][0]
# 特徵函式b 前一個詞
features_dict[‘fea_2’] = fea_2 = word_li[i - 1][0]
# 特徵函式d 下一個詞
features_dict[‘fea_4’] = word_li[i + 1][0]
# 標記
label = word_li[i][1]
# 新增一個事件
events。append((features_dict, label))
return events
# 載入資料,生成事件集,返回
def load_data(file_name):
data_set = []
with codecs。open(file_name, ‘rb’, ‘utf8’, ‘ignore’) as infile:
for line_ser, line in enumerate(infile):
if line_ser >= 100:
break
line = line。strip()
if line:
word_li = line。split()
word_li = [tuple(w。split(u‘/’)) for w in word_li if len(w。split(u‘/’)) == 2]
# 生成事件並更新到data_set
data_set。extend(generate_events(word_li))
print(“抽取出 %d 個事件” % len(data_set))
return data_set
# 標註詞性
def pos_tag(classifier, line):
new_word_li = []
word_li = line。split()
word_li = [(w, u‘x_pos’) for w in word_li]
events_li = generate_events(word_li)
for i, (features, label) in enumerate(events_li):
predict_pos = classifier。classify(features)
new_word_li。append(word_li[i][0]+“/”+predict_pos)
return new_word_li
if __name__ == “__main__”:
# 抽取特徵構建訓練和測試集
data_set = load_data(‘data/199801。txt’)
train_size = int(len(data_set)*0。8)
test_size = int(len(data_set)*0。2)
train_set = data_set[:train_size]
test_set = data_set[-test_size:]
print(“訓練集事件數=”, len(train_set))
print(“測試集事件數=”, len(test_set))
# IIS學習演算法的最大熵模型
classifier_iis = nltk。classify。maxent。MaxentClassifier。train(train_set, trace=2, algorithm=‘iis’, max_iter=10)
print(“IIS模型準確率= ”, nltk。classify。accuracy(classifier_iis, test_set))
print(“IIS模型測試:”)
print(pos_tag(classifier_iis, “中國 政府 將 繼續 堅持 奉行 獨立自主 的 和平 外交 政策 。”))
# GIS學習演算法的最大熵模型
classifier_gis = nltk。classify。maxent。MaxentClassifier。train(train_set, trace=2, algorithm=‘gis’, max_iter=10)
print(“GIS模型準確率= ”, nltk。classify。accuracy(classifier_gis, test_set))
print(“GIS模型測試:”)
print(pos_tag(classifier_gis, “中國 政府 將 繼續 堅持 奉行 獨立自主 的 和平 外交 政策 。”))
目前發現的最大熵模型的小缺陷是測試例項抽取出的所有聯合特徵如果在訓練集中都沒有的話,那麼模型一點兒辦法都沒有了,沒法預測了。NLTK的最大熵庫是目前看到的最適合實驗的庫,裡邊還有一些功能,比如抽取出最重要的聯合特徵等等,這些對於除錯分析模型都是非常重要的。所以,我很期待用最大熵做出的“的,地,得”糾正系統。
另外,有關於多人會話文字上的問答匹配的方法也該做總結了,這個實驗性的內容可以收尾了。主要使用了3種方法,分別是被神話的預訓練模型Bert,基於它判別兩個句子是否構成上下文關係;第2個方法是基於公共詞比例判別;第3種基於共現詞計算相關度判別。第1種效果不敢恭維,與所有預訓練模型一樣,絕沒有通用領域的模型。第2種跟第3種,效果差不了太多,準確性能達到80%,但是測試集太小了,這個精度是要打問號的。本來想把基於最大熵的問答匹配也做一下,但是看來時間不允許了,今天又讓加文字分類類目呢,總算是提起資料了,有資料也是藏著掖著,導資料的以為我做一套資料,他找個模型就能用了,做夢去吧,我還是那句話,如果自然語言處理的任務這麼簡單,就不需要單設一個自然語言處理的崗位。另外,今天跟已經找到工作的一個學生聊天,看來現在還是有一部分公司沒有去用神經網路,所以如果關注專欄的小夥伴有在校生,可得注意別對機器學習方法一竅不通,特別是隱馬、最大熵、條件隨機場。對於文字處理的基本語言素養,那是必須的,沒有也做不出東西來。另外,可以詳細閱讀NLTK的書,雖然是歐洲人寫的書,處理的是英文,但是能用作Python實踐的教材(指的是上課的正式教材),恐怕也就這本不錯了,特別是每章後的題目,有些題目算作是畢業設計我看也不為過。我記得之前的文章裡應該是寫過,看到過離職的留下來的演算法,那裡邊很多實現都能在NLTK裡找到影子。當然,關於文字校對的內容我還是會繼續的,請大家期待。
上一篇:植物中的瑞士軍刀--沙棘油
下一篇:搞笑女為什麼沒有男朋友?