利用隐马尔可夫模型 (HMM) 完成命名实体识别 (NER) 任务
一、源码地址
https://github.com/busyforest/NER_Project/
二、关于 NER
命名实体识别(Named Entity Recognition,NER)是自然语言处理(NLP)中的一项关键任务,旨在识别并分类文本中具有特定意义的实体。
-
定义
- NER任务的目标是识别文本中提到的命名实体,并将这些实体分类到预定义的类别中。这些类别通常包括人名、地名、组织机构、日期、时间、数量、货币等。
-
示例:
- 例如,给定一句话:“Apple was founded by Steve Jobs in Cupertino.”
- NER系统应该识别出:"Apple"是一个组织,"Steve Jobs"是一个人名,"Cupertino"是一个地名。
-
应用:
-
可以从大量文本数据中快速提取有用的信息,比如一大段新闻中的关键人物和事件。
-
可以用于提高机器翻译的准确性,确保命名实体在翻译过程中保持一致性。
-
例如,给定一句话:“I am going to study HMM.”,翻译系统试图将其翻译成中文。
-
对 study 这个词,常见的意思中包含(verb,“学习”)和(noun,“研究”)
-
在翻译成中文的过程中,如果 NER 系统识别出并标记 study 这个词为 verb,那么翻译系统将其翻译
成"学习"的几率就会大大增加,翻译成"研究"的几率相应下降。
-
-
-
实现方法:
- 基于规则的方法:使用预定义的模式和词典来匹配和识别命名实体。
- 机器学习方法:利用有标注的训练数据,训练分类器来识别命名实体。
- 深度学习方法:使用神经网络模型,如 LSTM,CNN,Transformer 来识别命名实体。
使用HMM 正是属于第一种方法。
三、关于 HMM
隐马尔可夫模型(Hidden Markov Model, HMM)是一种统计模型,用于处理时间序列数据或其他序列数据,特别适合于那些观察值受潜在的隐藏状态影响的情景。以下仅对 HMM 做简单介绍,更详细的数学推导可以自行搜索,有很多讲的很清楚的博客,也可以参考李航《统计学习方法》中的相关内容。
-
基本概念
- 状态:HMM由一组隐藏状态(hidden states)组成,这些状态在模型中不可直接观测,但影响着观测到的数据。
- 观测:观测(observations)是可见的数据序列,由隐藏状态生成。每个隐藏状态都有一个与之相关的观测概率分布。
- 转移概率:隐藏状态之间的转移是根据转移概率(transition probabilities)进行的,表示从一个状态转移到另一个状态的概率。
- 观测概率:每个隐藏状态生成一个观测值的概率由观测概率(emission probabilities)确定。
- 初始概率:初始概率(initial probabilities)定义了模型在初始时刻处于每个隐藏状态的概率。
-
定义
一个HMM由以下参数定义:
- $S $:隐藏状态的集合。
- O O O :可能的观测值的集合。
- π \pi π :初始状态分布,表示初始时刻处于每个隐藏状态的概率。
- A A A:状态转移概率矩阵,表示从一个状态转移到另一个状态的概率。
- B B B :观测概率矩阵,表示在某个状态下生成某个观测值的概率。
-
基本问题
- 概率计算。给定模型参数 A , B , π A,B,\pi A,B,π 和观测序列 ( o 1 , o 2 , o 3 , . . . , o t ) (o_1,o_2,o_3,...,o_t) (o1,o2,o3,...,ot),求这个观测序列出现的概率。
- 状态预测。给定模型参数 A , B , π A,B,\pi A,B,π 和观测序列 ( o 1 , o 2 , o 3 , . . . , o t ) (o_1,o_2,o_3,...,o_t) (o1,o2,o3,...,ot),求这个观测序列对应最大概率的状态序列 ( i 1 , i 2 , i 3 , . . . , i t ) (i_1,i_2,i_3,...,i_t) (i1,i2,i3,...,it) 。
- 模型学习。给定观测序列 ( o 1 , o 2 , o 3 , . . . , o t ) (o_1,o_2,o_3,...,o_t) (o1,o2,o3,...,ot) 和确定的状态序列 ( s 1 , s 2 , s 3 , . . . , s t ) (s_1,s_2,s_3,...,s_t) (s1,s2,s3,...,st) ,估计模型参数 A , B , π A,B,\pi A,B,π ,使得用这套参数预测出来的最大概率状态序列 ( i 1 , i 2 , i 3 , . . . , i t ) (i_1,i_2,i_3,...,i_t) (i1,i2,i3,...,it) 和 ( s 1 , s 2 , s 3 , . . . , s t ) (s_1,s_2,s_3,...,s_t) (s1,s2,s3,...,st) 最接近。
-
算法
- 概率计算方法有前向算法、后向算法等。
- 状态预测算法有维特比解码。
- 模型学习有极大似然法、鲍勃-韦尔奇算法等。
四、文件结构
HMM
目录NER
目录:主要是训练集和测试集,还有一个测试脚本check.py
,可以检验模型的正确率。运行check.py
需要安装 sklearn 库。HMM.py
:主文件,实现了中英文分词的 NER 任务。
五、代码简析
from collections import defaultdict
initial_matrix = defaultdict(float)
initial_total = 0
transition_matrix = defaultdict(lambda: defaultdict(float))
transition_total = defaultdict(float)
emission_matrix = defaultdict(lambda: defaultdict(float))
emission_total = defaultdict(float)
zero_num = 0
total = 0
def hmm_learn(file_path):
words = []
labels = []
with open(file_path, 'r', encoding='utf-8') as file:
for line in file:
tokens = line.strip().split()
if len(tokens) == 2:
word, label = tokens
words.append(word)
labels.append(label)
else:
get_initial_data(labels)
get_transition_data(labels)
get_emission_data(words, labels)
words.clear()
labels.clear()
compute_matrix()
def get_initial_data(labels):
global initial_total
initial_matrix[labels[0]] += 1
initial_total += 1
def get_transition_data(labels):
for i in range(len(labels) - 1):
transition_matrix[labels[i]][labels[i + 1]] += 1
transition_total[labels[i]] += 1
def get_emission_data(words, labels):
for i in range(len(words)):
emission_matrix[labels[i]][words[i]] += 1
emission_total[labels[i]] += 1
def compute_matrix():
for i in transition_matrix:
initial_matrix[i] = (initial_matrix[i] + 1) / initial_total
for i in transition_matrix:
for j in transition_matrix:
transition_matrix[i][j] = (transition_matrix[i][j] + 1) / transition_total[i]
for i in emission_matrix:
for j in emission_matrix[i]:
emission_matrix[i][j] = (emission_matrix[i][j] + 1) / emission_total[i]
def hmm_test(file_path):
global zero_num
global total
words = []
labels = []
with open(file_path, 'r', encoding='utf-8') as file:
for line in file:
tokens = line.strip().split()
if len(tokens) == 2:
word, label = tokens
words.append(word)
labels.append(label)
else:
viterbi(words)
total += 1
words.clear()
labels.clear()
def viterbi(words):
global zero_num
# 初始化
prob = defaultdict(lambda: defaultdict(float))
path = defaultdict(lambda: defaultdict(float))
T = len(words)
for i in initial_matrix:
if emission_matrix[i][words[0]] == 0:
temp_count = 0
for j in emission_matrix[i]:
if emission_matrix[i][j] == 0:
temp_count += 1
avg_emission_prob = 1 / temp_count
prob[0][i] = initial_matrix[i] * avg_emission_prob * 1e-10
else:
prob[0][i] = initial_matrix[i] * emission_matrix[i][words[0]]
path[0][i] = 0
if prob[0][i] == 0:
prob[0][i] = 1e-10
# 递推
for t in range(1, T):
for i in transition_matrix:
temp = 0
index = list(initial_matrix.keys())[0]
for j in transition_matrix:
if prob[t - 1][j] * transition_matrix[j][i] > temp:
temp = prob[t - 1][j] * transition_matrix[j][i]
index = j
if emission_matrix[i][words[t]] == 0:
temp_count = 0
for j in emission_matrix[i]:
if emission_matrix[i][j] == 0:
temp_count += 1
avg_emission_prob = 1 / temp_count
prob[t][i] = temp * avg_emission_prob * 1e-10
# print(words[t], i, temp * avg_emission_prob * 1e-20)
else:
prob[t][i] = temp * emission_matrix[i][words[t]]
path[t][i] = index
if prob[t][i] == 0:
print(words[t], i)
prob[t][i] = 1e-300
# 终止
final_prob = 0
final_path = defaultdict(float)
for i in transition_matrix:
if prob[T - 1][i] > final_prob:
final_prob = prob[T - 1][i]
final_path[T - 1] = i
# print(final_prob)
# if final_prob == 1e-300:
# zero_num += 1
# for t in range(T - 2, -1, -1):
# final_path[t] = "O"
# else:
for t in range(T - 2, -1, -1):
final_path[t] = path[t + 1][final_path[t + 1]]
# for t in words:
# temp = 0
# for i in emission_matrix:
# if emission_matrix[i][t] != 0:
# temp += 1
# if temp == 0:
# print(t)
for t in range(T):
string = words[t] + " " + final_path[t]
print(string)
print(final_prob)
with open("NER/Chinese/result.txt", "a", encoding="UTF-8") as file:
for t in range(T):
print(words[t], final_path[t])
string = words[t] + " " + final_path[t] + "\n"
file.write(string)
file.write("\n")
train_path = "NER/Chinese/train.txt"
test_path = "NER/Chinese/validation.txt"
hmm_learn(train_path)
hmm_test(test_path)
# print(zero_num/total)
hmm_learn()
:从训练集对应的文件路径中构建 HMM 模型,利用极大似然法学习模型参数。get_initial_data()
、get_transition_data()
、get_emission_data()
:分别统计数据,计算初始矩阵,转移矩阵,发射矩阵的值。compute_matrix()
:对三个矩阵进行归一化处理和拉普拉斯平滑处理,避免出现数值下溢的情况。hmm_test()
:从测试集对应的文件路径中获取观测序列,进行 HMM 模型的预测。viterbi()
:对测试集上的观测序列应用学习来的模型参数进行维特比解码。
由于在 NER 任务中,一个 HMM 的序列往往很长,在中文中,按照一句话来划分可能超过150个字甚至更多,所以在进行维特比解码的时候概率连续相乘可能导致数值下溢出现 0 概率值,造成后续的汉字解码不出来的情况。因此我在进行拉普拉斯平滑处理的同时,设定了一个概率最低值10的-10次方,当下溢为 0 时就重新设定概率值,这样得到的模型有较高的预测成功率和准确率。
六、测试结果
运行 check.py
,可以分别得到以下测试结果:
- 中文
正确率为 83.12%。 - 英文
正确率为 87.97%。