Python自然语言处理实战(4):词性标注与命名实体识别

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)整理地理位置词库。在识别时,先通过词库匹配,再采用模型进行发现。


你可能感兴趣的:(NLP)