Python自然語言處理實戰(4):詞性標註與命名實體識別

CopperDong發表於2018-07-16

4.1 詞性標註

       詞性是詞彙基本的語法屬性,通常也稱為詞類。從整體上看,大多數詞語,尤其是實詞,一般只有一到兩個詞性,且其中一個詞性的使用頻次遠遠大於另一個,即使每次都將高頻詞性作為詞性選擇進行標註,也能實現80%以上的準確率。目前較為主流的方法是如同分詞一樣,將句子的詞性標註作為一個序列標註問題來解決。

       較為主流的詞性標註規範有北大的詞性標註集和濱州詞性標註集兩大類。

       jieba的詞性標註同樣是結合規則和統計的方式,具體為在詞性標註的過程中,詞典匹配和HMM共同作用。詞性標註流程如下:

      1)首先基於正規表示式進行漢字判斷

re_han_internal = re.compile('([\u4E00-\u9FD5a-zA-Z0-9+#&\._]+)")

       2) 若符合上面的正規表示式,則判定為漢字,然後基於字首詞典構建有向無環圖,再基於有向無環圖計算最大概率路徑,同時在字首詞典中找出它所分出的詞性,若在字典中未找到,則賦予詞性為“x"(代表未知)。當然,若在這個過程中,設定使用HMM,且待標註詞為未登入詞,則會通過HMM方式進行詞性標註。

        3)若不符合上面的正規表示式,那麼將繼續通過正規表示式進行型別判斷,分別賦予”x" "m"(數詞)和"eng"(英文)

      在詞性標註任務中,Jieba分詞采用了simultaneous思想的聯合模型方法,即將基於字標註的分詞方法和詞性標註結合起來,使用複合標註集。比如名詞“人民”,“人”為B_n,民為E_n。這樣就與HMM分詞的實現過程一致,只需要更換合適的訓練語料即可。

>>> import jieba.posseg as psg
>>> sent = "中文分詞是文字處理不可或缺的一步!"
>>> seg_list = psg.cut(sent)
>>> print(' '.join(['{0}/{1}'.format(w, t) for w, t in seg_list]))
Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 0.864 seconds.
Prefix dict has been built succesfully.
中文/nz 分詞/n 是/v 文字處理/n 不可或缺/l 的/uj 一步/m !/x

4.2 命名實體識別

     NER目的是識別語料中人名、地名、組織機構名等命名實體。命名實體一般分為3大類(實體類、時間類、數字類)和7小類(人名、地名、組織機構名、時間、日期、貨幣、百分比)。由於數量、時間、日期、貨幣等實體識別通常可以採用模式匹配的方式獲得較好的識別效果,相比之下人名、地名、機構名較複雜,因此近年來的研究主要以這幾種實體為主。

     序列標註方式是目前命名實體識別中的主流方法。

     在大量真實語料中,觀察序列更多的是以一種多重的互動特徵形式表現出來,觀察元素之間廣泛存在長程相關性。這樣,HMM的效果就受到了制約。基於此,在2001年,Lafferty等學者們提出了條件隨機場,其主要思想來源於HMM,也是一種用來標記和切分序列化資料的統計模型。不同的是,條件隨機場是在給定觀察的標記序列下,計算整個標記序列的聯合概率,而HMM是在給定當前狀態下,定義下一個狀態的分佈。

地名識別:

git clone https://github.com/taku910/crfpp.git

./configure

make && sudo make install

CRF++提供了Python使用介面

cd python

python setup.py build

sudo python setup.py install

使用CRF++地名識別主要有以下流程:

1、確定標籤體系

"B"

"E"

"M"

"S"

"O"

2、語料資料處理

CRF++的訓練資料要求一定的格式,一般是一行一個token,一句話由多行token組成,多個句子之間用空行分開。其中每行又分成多列,除最後一列以外,其他列表示特徵。因此一般至少需要兩列,最後一列表示要預測的標籤。本例使用

我 O

去 O

北 B

京 M

飯 M

店 E

。 O

採用的語料資料是1998年人民日報分詞資料集。對資料處理程式碼如下corpusHandler.py

# -*- coding: utf-8 -*-
# 每行的標註轉換
def tag_line(words, mark):
	chars = []
	tags = []
	temp_word = ''  #用於合併組合詞
	for word in words:
		#print(word)
		word = word.strip('\t ')
		if temp_word == '':
			bracket_pos = word.find('[')   # [ ]ns
			w, h = word.split('/')
			if bracket_pos == -1:
				if len(w) == 0: continue
				chars.extend(w)
				if h == 'ns':   # 地名
					tags += ['S'] if len(w) == 1 else ['B'] + ['M'] * (len(w)-2) + ['E']
				else:
					tags += ['O'] * len(w)
			else:
				print(w)
				w = w[bracket_pos+1:]
				temp_word += w
		else:
			bracket_pos = word.find(']')
			w, h = word.split('/')
			if bracket_pos == -1:
				temp_word += w
			else:
				print(w)
				w = temp_word + w
				h = word[bracket_pos+1:]
				temp_word = ''
				if len(w) == 0: continue
				chars.extend(w)
				if h == 'ns':
					tags += ['S'] if len(w) == 1 else ['B']+['M']*(len(w)-2)+['E']
				else:
					tags += ['O']*len(w)
	assert temp_word == ''
	return (chars, tags)

def corpusHandler(corpusPath):
	import os
	root = os.path.dirname(corpusPath)
	with open(corpusPath) as corpus_f, \
	    open(os.path.join(root, 'train.txt'), 'w') as train_f, \
	    open(os.path.join(root, 'test.txt'), 'w') as test_f:
	    pos = 0
	    for line in corpus_f:
	    	line = line.strip('\r\n\t');
	    	if line == '': continue
	    	isTest = True if pos % 5 == 0 else False  # 抽樣20%作為測試集使用
	    	words = line.split()[1:]
	    	if len(words) == 0: continue
	    	line_chars, line_tags = tag_line(words, pos)
	    	saveObj = test_f if isTest else train_f
	    	for k, v in enumerate(line_chars):
	    		saveObj.write(v + '\t' + line_tags[k] + '\n')
	    	saveObj.write('\n')
	    	pos += 1


if __name__ == '__main__':
	corpusHandler('./data/people-daily.txt')

3、特徵模板設計

   CRF的特徵函式是通過定一些規則來實現的,對應CRF++中的特徵模板。其基本格式為%x[row, col],用於確定輸入資料的一個token,其中,row確定與當前的token的相對行數,col用於確定決定列數。

   CRF++有兩種模板型別,第一種是U開頭,為Unigram template,CRF++會自動為其生成一個特徵函式集合(func1...funcN)。第二種以B開頭,表示Bigram template,會自動產生當前輸出與前一個輸出token的組合,根據該組合構造特徵函式。

crf_learn -f 4 -p 8 -c 3 template ./data/train.txt model
crf_test -m model ./data/test.txt > ./data/test.rst
def f1(path):
	with open(path) as f:
		all_tag = 0
		loc_tag = 0
		pred_loc_tag = 0
		correct_tag = 0
		correct_loc_tag = 0

		states = ['B', 'M', 'E', 'S']
		for line in f:
			line = line.strip()
			if line == '': continue
			_, r, p = line.split()
			all_tag += 1
			if r == p:
				correct_tag += 1
				if r in states:
					correct_loc_tag += 1
			if r in states: loc_tag += 1
			if p in states: pred_loc_tag += 1
		loc_P = 1.0 * correct_loc_tag/pred_loc_tag
		loc_R = 1.0 * correct_loc_tag/loc_tag
		print('loc_P:{0}, loc_R:{1}, loc_F1:{2}'.format(loc_P, loc_R, (2*loc_P*loc_R)/(loc_P+loc_R)))

if __name__ == '__main__':
	f1('./data/test.rst')
def load_model(path):
	import os, CRFPP
	if os.path.exists(path):
		return CRFPP.Tagger('-m {0} -v 3 -n2'.format(path))
	return None

def locationNER(text):
	tagger = load_model('./model')
	for c in text:
		tagger.add(c)
	result = []
	tagger.parse()
	#print(tagger.xsize())
	word = ''
	for i in range(0, tagger.size()):
		for j in range(0, tagger.xsize()):
			ch = tagger.x(i, j)
			#print(ch)
			tag = tagger.y2(i)
			#print(tag)
			if tag == 'B':
				word = ch
			elif tag == 'M':
				word += ch
			elif tag == 'E':
				word += ch
				result.append(word)
			elif tag == 'S':
				word = ch
				result.append(word)
	return result

text = '我中午要去北京飯店,下午去中山公園,晚上回亞運村。'
print(text, locationNER(text), sep='==> ')
text = '我去回龍觀,不去南鑼鼓巷。'
print(text, locationNER(text), sep='==> ')
text = '打的去北京南站。'
print(text, locationNER(text), sep='==> ')
我中午要去北京飯店,下午去中山公園,晚上回亞運村。==> ['北京飯店', '中山公園', '亞運村']
我去回龍觀,不去南鑼鼓巷。==> []
打的去北京南站。==> ['北京']

如“回龍觀”等識別效果並不好,通常的解決辦法是:

1)擴充套件語料,改進模型。如加入詞性特徵,調整分詞演算法等。

2)整理地理位置詞庫。在識別時,先通過詞庫匹配,再採用模型進行發現。


相關文章