我们今天通过隐马尔可夫对中文的词进行标注
词性标注
汉语由于缺乏语法形态变化,词的应用非常灵活,词类兼类现象特别多,也特别复杂,因此需要词性标注
这里先介绍两个概念
显状态
直接可以被观测到的状态,例如单词
隐状态
通过间接观测到的状态,例如词性
OK,首先介绍隐马尔可夫模型之前我们要先来研究一下显马尔科夫模型:
马尔科夫模型:
马尔可夫模型描述了一类重要的随机过程,这个 随机过程是随时间而随机变化的过程
例如:
我们常会考虑一个并不互相独立的随机变量组成的序列,序列中每个变量的值依赖于它前面的元素。
• 如:红色 苹果
假设某系统有N个有限状态S={s1,s2,…,sN} ,随着时间的 推移,系统将从某一个状态转移到另一个状态。
X={x1,x2,…,xT}是一个取值于有限状态集合S的随机变量 序列,随机变量的取值为状态集S的某个状态,假定在 时间t的状态记为xt(xt∈S,t=1,2,…,T)。
系统在时间t处于状态sj的概率取决于它在时间 1,2,3,…,t-1的状态,其概率为
条件:
即:一个马尔可夫模型通常有以下部分组成
解释:就是在特定条件下,系统在时间 t 的状态只和 t-1 时的状态有关
算法过程:
首先我们给出一个转移矩阵:
这里可以观察到,左侧是昨天的显状态,也就是可以直观的观察到的状态,这里就是天气,晴天雾天雨天,相比于模型中,这就是 t-1 时刻的状态
而上侧则表示当天的显状态,也就是 t 时刻的状态
那么OK,我们接下来引入转移概率:
这是什么意思,就是当 t-1 时刻发生事件的情况下同时在 t 时刻发生 事件的概率
这里我们举个例子把它说明白:也就是昨天是晴天的同时今天也是晴天的概率是 0.50 ,这就是转移概率
限制条件:
实例描述(预测天气)
首先根据马尔科夫模型的条件,我们要有一个初始概率矩阵:
这个矩阵在实际的开发环境中其实是通过对一个数据集的词进行去重然后每个词进行计数,然后除以总词数,得到概率
我们这里只是做一个实例
所以我们随便给出一个初始矩阵,我们的初始概率假设定为:
转移概率矩阵
这个转移矩阵在实际的开发环境中其实是通过对一个数据集的词进行计算第一天是状态 S1 的情况下,第二天是 S2 的出现次数在除以第一天出现 S1 的次数,得到概率
我们这里构建一个转移矩阵
我们在上面构造完成了马尔可夫模型
接下来我们计算天气顺序为 cloud sun rain cloud 的概率:
P(cloud) * P(sun|cloud) * P(rain|sun) * P(cloud|rain) = 0.0 * 0.25 * 0.125 * 0.375
然后我们遍历所有可能出现的情况,之后取概率最大的情况
其实我们也可以吧马尔可夫当做一个 2-gram 模型
隐马尔可夫模型
在马尔可夫模型中,每个状态代表了一个可观察到的事件。如:天气马尔可夫模型,前提是,第二天是天气是 可以观察到的。
如果知道某个事件的观察序列,是可以使用一个马尔可夫模型来计算
但是,有时候有些事件是不可以直接观测到的。例如: 如果一个盲人,他不能通过观察天气来预测天气,他只能通过摸水藻的干湿程度来间接预测天气。
这时,水藻的干湿就是可观察事件,其状态为可观察状态;而天气则是隐状态;
现实世界中,也还有很多事件是无法直接观察到的
如:
字串(可观察序列):“结合/成/分子/时”
字的词性(隐序列):vn,v/ v,nr,q,a,an,j/ n/ Ng,nr,Dg/
隐马尔可夫模型(HMM)就是估算隐藏于表面事件背后的事件的概率。
前置条件:
比如在词性的例子里,发射概率就是单词映射到词性的概率
举一个HMM的例子:
首先第一种穷举法
它会穷举所有的路径然后找出最大的,但是这种方法,当词性多的时候,词性有几十种的时候会非常的消耗时间
第二种前向算法
这种算法改良了一下,当前时间当前节点的概率会等于前一个时间的所有路径的概率之和,其公式如下:
表示 t 时刻状态 j 的格子概率
但是即使是用前向算法运算量也很大
所以我们提出一种类似于动归的方法叫做维特比算法
维特比
简而言之就是每个节点都取上一个阶段乘以转移概率最高的路径,最终再乘以发射矩阵,注意一开始是乘以初始的
维特比算法详例描述
1.首先我们有一个的数据集
2.我们可以看到它已经将词性标好了,那么我们要得到前置条件转移矩阵和发射矩阵
首先我们要预处理一下,我们先将所有的词放进一个list里面
并且将所有的词性放进一个list里面,其中的正则就不解释了
然后我们需要得到不重复的词性的list,所以我们要做唯一化:
然后我们遍历每一种词性并且判断下一个词性是什么,然后计算出现次数,存进矩阵
最后吧矩阵存进文件中:
3.我们在写一个emission_matrix.py来计算发射矩阵
取出词和词性进行对比,然后计数算出发射矩阵
4.我们在写一个calculate.py来计算他的初始概率
简单除法可得:
结果:
5.我们将初始概率、发射矩阵、转移矩阵都算出来了并且保存在文件中,我们开始写一个总的文件HMM_VIBETL.py文件来计算维比特算法并且进行词性标注
5.1我们先计算这个转移概率和发射概率
(明确一点:每一行之和为一,注意行和列分别是什么)
做平滑!!!!! 每个数字都加一,当然大家有更好的平滑可以使用
5.2维比特算法
我们这里是去(上一时间每一个节点的概率*转移概率)然后取一个最大值,然后记录这个节点的下标,然后计算到最后一列的时候,找出最大的那个节点,并且根据记录的节点进行回溯。
然后最后将list反过来:
程序截图:
转移矩阵(次数):
发射矩阵(次数):
结果:
维特比算法的主文件:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import codecs
import re
# 获取矩阵
def get_matrix( string ):
# 我们要和我们的转义概率的词性集合一致
input_stream = codecs.open('emission_matrix.txt', encoding='UTF-8')
transition_input_stream = codecs.open('transition_matrix.txt', encoding='UTF-8')
text = transition_input_stream.readline()
# 这里是构造词性的一个list
character_list = []
for item in text.split():
character_list.append(item)
# 上面构造好了词性的list,下面我们要把那句话的list也构造出来
str_list = string
# 将我们的发射矩阵拿出来(第一行我们在上面已经取出来了)
flag = 0
emission_matrix = []
for line in input_stream.readlines():
if flag == 0:
flag += 1
continue
single_line = []
for j in line.split():
single_line.append(int(j))
emission_matrix.append(single_line)
# 将我们的转移概率拿出来(第一行不要)
transition_matrix = []
for line in transition_input_stream.readlines():
single_line = []
for j in line.split():
single_line.append(int(j))
transition_matrix.append(single_line)
transition_input_stream.close()
input_stream.close()
return str_list, character_list, transition_matrix, emission_matrix
# 数据平滑
def smooth(transition_matrix, emission_matrix):
new_transition_matrix = []
for i in transition_matrix:
new = [j+1 for j in i]
new_transition_matrix.append(new)
new_emission_matrix = []
for i in emission_matrix:
new = [j + 1 for j in i]
new_emission_matrix.append(new)
return new_transition_matrix, new_emission_matrix
def get_init_list():
input_stream = codecs.open('init_matrix.txt', encoding='UTF-8')
text = input_stream.readline()
# 这里是构造初始概率的一个list
init_list = []
for item in text.split():
init_list.append(float(item))
input_stream.close()
return init_list
# 计算转移概率
def calculate_transition(transition_matrix):
# 第一轮先计算每行总数
transition_matrix_probability = []
for i in transition_matrix:
line_probability = []
line_total = 0
for j in i:
line_total += j
for k in i:
line_probability.append(k * 1.0 / line_total)
transition_matrix_probability.append(line_probability)
return transition_matrix_probability
# 计算发射概率
def calculate_emission(emission_matrix):
# 第一轮先计算每行总数
emission_matrix_probability = []
for i in emission_matrix:
line_probability = []
line_total = 0
for j in i:
line_total += j
for k in i:
line_probability.append(k * 1.0 / line_total)
emission_matrix_probability.append(line_probability)
return emission_matrix_probability
# 维比特
def vitebl(init,str_list,transition,emission,character):
# 列是显状态
# 行是隐藏状态
# 开始的状态是init
# 建立一个记录矩阵
record = [init] # 行当成列
# 建立一个记录下标矩阵
record_index = [] # 行当成列,从第二行开始
# 以列开始循环
# 列的下标(记录当前列到第几列了)
index = 1
for i in range(len(str_list)):
# 用一个新的list装这一列的概率
column_list = []
# 用一个新的list装这一列的下标
column_list_index = []
# 当前列的每一个节点
for j in range(len(character)):
# 每个节点都要算出最佳的状态
max_state = 0
# 每个节点都要算出上一列最佳的状态下标
max_index = 0
# 上一列的每一个节点
# 取出上一列的状态
state_list = record[index - 1]
for k in range(len(character)):
# 取出上一列k这个点的状态
probability = state_list[k]
# 暂时状态设为 上一列k这个点的状态 * 转移概率 并且和最大状态比较
# 转移矩阵行是上一个状态,列是下一个状态
# 乘以发射概率
tmp = probability * transition[k][j] * emission[j][i]
if(tmp == max(tmp, max_state)):
# 如果替换的话
max_state = max(tmp, max_state)
max_index = k
column_list.append(max_state)
column_list_index.append(max_index)
record.append(column_list)
record_index.append(column_list_index)
# index前挪
index += 1
# 逆序
# 首先我们要找出最后一列的最大概率
flag_index = 0
flag_max = 0
final_character_list = []
# 最后一列
for jj in range(len(record[len(record) - 1])):
if(record[len(record) - 1][jj] == max(record[len(record) - 1][jj],flag_max)):
flag_max = record[len(record) - 1][jj]
flag_index = jj
final_character_list.append(character[flag_index])
for ii in reversed(record_index):
# 第一列是不需要去做判断的,因为第一列拿的是初始概率
if(ii != record_index[0]):
# 更新index
flag_index = ii[flag_index]
# 塞进去到时候倒序
final_character_list.append(character[flag_index])
result = final_character_list[::-1]
return result
# 这里直接传入想要标注的那句话
def make_emission_matrix(string):
# 我们要和我们的转义概率的词性集合一致
input_stream = codecs.open('transition_matrix.txt', encoding='UTF-8')
text = input_stream.readline()
# 这里是构造词性的一个list
character_list = []
for item in text.split():
character_list.append(item)
# 上面构造好了词性的list,下面我们要把那句话的list也构造出来
str_list = string
# 定义一个list去装分开来的词和标注
word_list = []
type_list = []
input_stream = codecs.open('199801.txt', encoding='UTF-8')
text = input_stream.read()
for item in text.split():
matchObj = re.match(r'(.*?)(\/.*$)', item)
pattern = re.compile(r'(\].*)')
out = re.sub(pattern, '', matchObj.group(2))
type_list.append(out.replace('/', ''))
word_list.append(matchObj.group(1))
# 词典list构建完成
# 遍历
emission_matrix = []
# 词性list里的每个词性
for character in character_list:
line = []
# 句子里的每个字
for i in str_list:
# 遍历一遍
count = 0
for index in range(len(word_list)):
# 判断是否一致
if word_list[index] == i:
if type_list[index] == character:
count += 1
line.append(count)
emission_matrix.append(line)
# 写入
f = codecs.open('emission_matrix.txt', 'w', encoding='UTF-8')
for i in str_list:
f.write(i + " ")
f.write("\n")
for i in emission_matrix:
for j in i:
# 写入unicode
f.write( str(j) + " ")
f.write("\n")
f.close()
input_stream.close()
# 最大概率分词
def loadDict():
dicFile = open("WordFrequency.txt", 'r', encoding='utf-8')
freqDict = {}
for l in dicFile:
freqDict[l.split(',')[0]] = l.split(',')[2].strip()
dicFile.close()
return freqDict
def findWordInDict(s):
"""
在字典中查找候选词
:param s: 输入句子
:return: 返回字典==>"词:词频|候选左邻词1/候选左邻词2"
"""
freqDict = loadDict()
result = {}
for index in range(0, len(s)): # 遍历所有字
for wordLen in range(0, len(s) - index): # 遍历该字的所有可能词
segWord = s[index:index + wordLen + 1]
if segWord in freqDict.keys():
# 找到候选词,找其左邻词
left_words = ''
for word_len in range(index, 0, -1): # 在之前的候选词列表里找左邻词(最大词长开始)
for k in result.keys():
if s[index - word_len:index] == k.split('-')[1]:
left_words += str(index - word_len) + '-' + s[index - word_len:index] + '/'
# 返回候选词及其语料库词频和候选左邻词
result[str(index) + '-' + segWord] = freqDict[segWord] + '|' + left_words
return result
def clProbabilty(wordsDict):
"""
计算累加概率并选择最佳左邻词
:param wordsDict: "词:词频|候选左邻词1/候选左邻词2"
:return:返回新字典==>"词:累计概率|最佳左邻词"
"""
for k, v in wordsDict.items():
freq = v.split('|')[0][:-1]
left_words = v.split('|')[1]
if left_words == '':
continue
else:
left_word = left_words.split("/")
maxLeftP = 0.0
whichWord = ''
for num in range(0, len(left_word) - 1):
currLeftWordP = float(wordsDict[left_word[num]].split('|')[0][:-1])
if currLeftWordP > maxLeftP: # 比较当前左邻词的累计概率
maxLeftP = currLeftWordP
whichWord = left_word[num]
currMaxP = float(maxLeftP) * float(freq)
# 用最大累计概率替换原来的词频,用最佳左邻词替换候选左邻词
wordsDict[k] = v.replace(freq, str(currMaxP)).replace(left_words, whichWord)
return wordsDict
def seg(sentence):
WordsDict = findWordInDict(sentence)
bestWordsDict = clProbabilty(WordsDict)
keys = list(bestWordsDict.keys())
key = keys[-1]
# 用来装这个分词的结果
final_list = []
while (key != ''):
final_list.append(key.split('-')[1])
key = bestWordsDict[key].split('|')[1]
return final_list
real_str = "我要去上课了。"
real_str = seg(real_str)
# 第0步
make_emission_matrix(real_str[::-1])
# 第一步得到我们准备好的发射矩阵和转移矩阵
str_list, character_list, pre_transition_matrix, pre_emission_matrix = get_matrix(real_str[::-1])
# 第二步,做一个平滑
transition_matrix, emission_matrix = smooth(pre_transition_matrix, pre_emission_matrix)
# 第三步拿到初始概率
init_list = get_init_list()
# 第四步计算转移概率
transition_matrix_probability = calculate_transition(transition_matrix)
# 第五步计算发射概率
emission_matrix_probability = calculate_emission(emission_matrix)
# 维特比算法
result = vitebl(init=init_list, str_list=str_list, transition=transition_matrix_probability, emission=emission_matrix_probability, character=character_list)
print(str_list)
print(result)
转移矩阵实现:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import codecs
import re
# 这个文件是计算转移概率和发射概率之后存起来
input_stream = codecs.open('199801.txt', encoding='UTF-8')
text = input_stream.read()
# 定义一个list去装分开来的词和标注
word_list = []
type_list = []
for item in text.split():
matchObj = re.match(r'(.*?)(\/.*$)', item)
pattern = re.compile(r'(\].*)')
out = re.sub(pattern, '', matchObj.group(2))
type_list.append(out.replace('/',''))
word_list.append(matchObj.group(1))
# 将type_list做一个单独处理
type_set = set(type_list)
# 计算转移概率
# 做一个二维的矩阵
transfer_matrix = []
print(type_set)
for character in type_set:
# 每一行
a_line = []
for line in type_set:
# 这里计数
flag_line = 0
for compare in range(len(type_list)):
# 下界判断
if(compare < len(type_list)-1):
# 如果找到了这个词性,判断下一个词性
if(character == type_list[compare]):
if(line == type_list[compare+1]):
# 如果两个都匹配到的话计数加一
flag_line += 1
print(flag_line)
a_line.append(flag_line)
transfer_matrix.append(a_line)
print(transfer_matrix)
# 写入
f= codecs.open('transition_matrix.txt','a', encoding='UTF-8')
for i in type_set:
f.write(i + " ")
f.write("\n")
for i in transfer_matrix:
for j in i:
# 写入unicode
f.write(str(j) + " ")
f.write("\n")
f.close()
input_stream.close()
计算初始概率:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import codecs
import re
# 计算初始概率
def calculate_init_matrix():
# 我们要和我们的转义概率的词性集合一致
input_stream = codecs.open('transition_matrix.txt', encoding='UTF-8')
text = input_stream.readline()
# 这里是构造词性的一个list
character_list = []
for item in text.split():
character_list.append(item)
# 定义一个list去装分开来的词和标注
type_list = []
input_stream = codecs.open('199801.txt', encoding='UTF-8')
text = input_stream.read()
for item in text.split():
matchObj = re.match(r'(.*?)(\/.*$)', item)
pattern = re.compile(r'(\].*)')
out = re.sub(pattern, '', matchObj.group(2))
type_list.append(out.replace('/', ''))
# 词典list构建完成
# 遍历计算出现概率
init_matrix = []
for character in character_list:
total = 0
equal = 0
for item in type_list:
total += 1
if item == character:
equal += 1
init_matrix.append(equal * 1.0 / total)
# 写入
f = codecs.open('init_matrix.txt', 'w', encoding='UTF-8')
for i in init_matrix:
f.write(str(i) + " ")
f.close()
input_stream.close()
calculate_init_matrix()
本菜鸟学习不好,如有不妥望各位大佬指点
如要转载请说明原文:https://blog.csdn.net/qq_36652619/article/details/84559172
为了写这篇博客,累死宝宝了