CRF:条件随机场,一种机器学习技术。给定一组输入随机变量条件下,另一组输出随机变量的条件概率分布模型。
以一组词性标注为例,给定输入X={我,喜欢,学习},那么输出为Y={名词,动词,名词}的概率应该为最大。输入序列X又称为观测序列,输出序列Y又称为状态序列。这个状态序列构成马尔可夫随机场,所以根据观测序列,得出状态序列的概率就包括,前一个状态转化为后一状态的概率(即转移概率)和状态变量到观测变量的概率(即发射概率)。
CRF分词原理:
- CRF把分词当做字的词位分类问题,通常定义字的词位信息如下:
词首,常用B表示;
词中,常用M表示;
词尾,常用E表示;
单子词,常用S表示; - CRF分词的过程就是对词位标注后,将B和E之间的字,以及S单字构成分词;
- CRF分词实例:
原始例句:我爱北京天安门
CRF标注后:我/S 爱/S 北/B 京/E 天/B 安/M 门/E
分词结果:我/爱/北京/天安门
代码
1、config.py
存放一些超参数。
1 filename='word.txt'
2 EMBEDDING_DIM = 5
3 HIDDEN_DIM = 4
4 epochs=100
2、data_process.py
预处理数据
import re
import torch
START_TAG = "<START>"
STOP_TAG = "<STOP>"
tag_to_ix = {"B": 0, "M": 1, "E": 2,"S":3, START_TAG: 4, STOP_TAG: 5}
def prepare_sequence(seq, to_ix): #seq是字序列,to_ix是字和序号的字典
idxs = [to_ix[w] for w in seq] #idxs是字序列对应的向量
return torch.tensor(idxs, dtype=torch.long)
#将句子转换为字序列
def get_word(sentence):
word_list = []
sentence = ''.join(sentence.split(' '))
for i in sentence:
word_list.append(i)
return word_list
#将句子转换为BMES序列
def get_str(sentence):
output_str = []
sentence = re.sub(' ', ' ', sentence) #发现有些句子里面,有两格空格在一起
list = sentence.split(' ')
for i in range(len(list)):
if len(list[i]) == 1:
output_str.append('S')
elif len(list[i]) == 2:
output_str.append('B')
output_str.append('E')
else:
M_num = len(list[i]) - 2
output_str.append('B')
output_str.extend('M'* M_num)
output_str.append('E')
return output_str
def read_file(filename):
word, content, label = [], [], []
text = open(filename, 'r', encoding='utf-8')
for eachline in text:
eachline = eachline.strip('\n')
eachline = eachline.strip(' ')
word_list = get_word(eachline)
letter_list = get_str(eachline)
word.extend(word_list)
content.append(word_list)
label.append(letter_list)
return word, content, label #word是单列表,content和label是双层列表
查看下数据内容:
1 text, content, label = read_file('word.txt')
2 print(text)
3 print(content)
4 print(label)
打印结果:
['十', '亿', '中', '华', '儿', '女', '踏', '上', '新', '的', '征', '程', '。', '过', '去', '的', '一', '年', ',', '是', '全', '国', '各', '族', '人', '民', '在', '中', '国', '共', '产', '党', '领', '导', '下', ',', '在', '建', '设', '有', '中', '国', '特', '色', '的', '社', '会', '主', '义', '道', '路', '上', ',', '坚', '持', '改', '革', '、', '开', '放', ',', '团', '结', '奋', '斗', '、', '胜', '利', '前', '进', '的', '一', '年', '。', '城', '乡', '经', '济', '体', '制', '改', '革', '向', '纵', '深', '稳', '步', '发', '展', ',', '对', '外', '开', '放', '迈', '出', '了', '新', '的', '步', '伐', ',', '工', '农', '业', '生', '产', '和', '其', '它', '各', '项', '建', '设', '事', '业', '全', '面', '完', '成', '了', '“', '七', '五', '”', '计', '划', '第', '一', '年', '的', '任', '务', ',', '人', '民', '生', '活', '继', '续', '有', '所', '改', '善', '。', '政', '治', '上', '安', '定', '团', '结', ',', '端', '正', '党', '风', '和', '社', '会', '风', '气', '的', '工', '作', '取', '得', '了', '新', '的', '进', '展', ',', '社', '会', '主', '义', '民', '主', '和', '法', '制', '建', '设', '不', '断', '加', '强', '。', '在', '党', '的', '十', '二', '届', '六', '中', '全', '会', '通', '过', '的', '《', '关', '于', '社', '会', '主', '义', '精', '神', '文', '明', '建', '设', '指', '导', '方', '针', '的', '决', '议', '》', '指', '引', '下', ',', '我', '国', '两', '个', '文', '明', '的', '建', '设', '正', '在', '向', '新', '的', '水', '平', '迈', '步', '。', '从', '党', '的', '十', '一', '届', '三', '中', '全', '会', '实', '现', '伟', '大', '历', '史', '转', '折', '到', '现', '在', ',', '我', '国', '政', '治', '安', '定', '团', '结', ',', '经', '济', '稳', '定', '、', '持', '续', '、', '协', '调', '发', '展', '已', '经', '八', '年', '了', ',', '这', '是', '建', '国', '以', '来', '稳', '步', '发', '展', '持', '续', '时', '间', '最', '长', '的', '时', '期', '。', '在', '十', '年', '动', '乱', '之', '后', ',', '取', '得', '这', '样', '一', '个', '大', '好', '局', '面', '是', '不', '容', '易', '的', '。']
[['十', '亿', '中', '华', '儿', '女', '踏', '上', '新', '的', '征', '程', '。'], ['过', '去', '的', '一', '年', ',', '是', '全', '国', '各', '族', '人', '民', '在', '中', '国', '共', '产', '党', '领', '导', '下', ','], ['在', '建', '设', '有', '中', '国', '特', '色', '的', '社', '会', '主', '义', '道', '路', '上', ',', '坚', '持', '改', '革', '、', '开', '放', ',', '团', '结', '奋', '斗', '、', '胜', '利', '前', '进', '的', '一', '年', '。'], ['城', '乡', '经', '济', '体', '制', '改', '革', '向', '纵', '深', '稳', '步', '发', '展', ',', '对', '外', '开', '放', '迈', '出', '了', '新', '的', '步', '伐', ',', '工', '农', '业', '生', '产', '和', '其', '它', '各', '项', '建', '设', '事', '业', '全', '面', '完', '成', '了', '“', '七', '五', '”', '计', '划', '第', '一', '年', '的', '任', '务', ',', '人', '民', '生', '活', '继', '续', '有', '所', '改', '善', '。'], ['政', '治', '上', '安', '定', '团', '结', ',', '端', '正', '党', '风', '和', '社', '会', '风', '气', '的', '工', '作', '取', '得', '了', '新', '的', '进', '展', ',', '社', '会', '主', '义', '民', '主', '和', '法', '制', '建', '设', '不', '断', '加', '强', '。'], ['在', '党', '的', '十', '二', '届', '六', '中', '全', '会', '通', '过', '的', '《', '关', '于', '社', '会', '主', '义', '精', '神', '文', '明', '建', '设', '指', '导', '方', '针', '的', '决', '议', '》', '指', '引', '下', ',', '我', '国', '两', '个', '文', '明', '的', '建', '设', '正', '在', '向', '新', '的', '水', '平', '迈', '步', '。'], ['从', '党', '的', '十', '一', '届', '三', '中', '全', '会', '实', '现', '伟', '大', '历', '史', '转', '折', '到', '现', '在', ',', '我', '国', '政', '治', '安', '定', '团', '结', ',', '经', '济', '稳', '定', '、', '持', '续', '、', '协', '调', '发', '展', '已', '经', '八', '年', '了', ',', '这', '是', '建', '国', '以', '来', '稳', '步', '发', '展', '持', '续', '时', '间', '最', '长', '的', '时', '期', '。'], ['在', '十', '年', '动', '乱', '之', '后', ',', '取', '得', '这', '样', '一', '个', '大', '好', '局', '面', '是', '不', '容', '易', '的', '。']]
[['B', 'E', 'B', 'M', 'M', 'E', 'B', 'E', 'S', 'S', 'B', 'E', 'S'], ['B', 'E', 'S', 'B', 'E', 'S', 'S', 'B', 'E', 'B', 'M', 'M', 'E', 'S', 'B', 'M', 'M', 'M', 'E', 'B', 'E', 'S', 'S'], ['S', 'B', 'E', 'S', 'B', 'E', 'B', 'E', 'S', 'B', 'M', 'M', 'E', 'B', 'E', 'S', 'S', 'B', 'E', 'B', 'E', 'S', 'B', 'E', 'S', 'B', 'M', 'M', 'E', 'S', 'B', 'E', 'B', 'E', 'S', 'B', 'E', 'S'], ['B', 'E', 'B', 'M', 'M', 'E', 'B', 'E', 'S', 'B', 'E', 'B', 'M', 'M', 'E', 'S', 'B', 'M', 'M', 'E', 'B', 'E', 'S', 'S', 'S', 'B', 'E', 'S', 'B', 'M', 'E', 'B', 'E', 'S', 'B', 'E', 'B', 'E', 'B', 'E', 'B', 'E', 'B', 'M', 'M', 'E', 'S', 'S', 'B', 'E', 'S', 'B', 'E', 'B', 'M', 'E', 'S', 'B', 'E', 'S', 'B', 'E', 'B', 'E', 'B', 'E', 'B', 'M', 'M', 'E', 'S'], ['B', 'E', 'S', 'B', 'M', 'M', 'E', 'S', 'B', 'M', 'M', 'E', 'S', 'B', 'M', 'M', 'E', 'S', 'B', 'E', 'B', 'E', 'S', 'S', 'S', 'B', 'E', 'S', 'B', 'M', 'M', 'E', 'B', 'E', 'S', 'B', 'M', 'M', 'E', 'B', 'M', 'M', 'E', 'S'], ['S', 'S', 'S', 'B', 'M', 'E', 'B', 'M', 'M', 'E', 'B', 'E', 'S', 'S', 'B', 'E', 'B', 'M', 'M', 'E', 'B', 'M', 'M', 'E', 'B', 'E', 'B', 'M', 'M', 'E', 'S', 'B', 'E', 'S', 'B', 'E', 'S', 'S', 'B', 'E', 'B', 'E', 'B', 'E', 'S', 'B', 'E', 'B', 'E', 'S', 'S', 'S', 'B', 'E', 'B', 'E', 'S'], ['B', 'E', 'S', 'B', 'M', 'M', 'M', 'M', 'M', 'E', 'B', 'E', 'B', 'E', 'B', 'E', 'B', 'E', 'S', 'B', 'E', 'S', 'B', 'E', 'B', 'E', 'B', 'M', 'M', 'E', 'S', 'B', 'E', 'B', 'E', 'S', 'B', 'E', 'S', 'B', 'E', 'B', 'E', 'B', 'E', 'B', 'E', 'S', 'S', 'B', 'E', 'B', 'M', 'M', 'E', 'B', 'M', 'M', 'E', 'B', 'M', 'M', 'E', 'B', 'E', 'S', 'B', 'E', 'S'], ['S', 'B', 'M', 'M', 'E', 'B', 'E', 'S', 'B', 'E', 'B', 'E', 'B', 'E', 'B', 'M', 'M', 'E', 'S', 'S', 'B', 'E', 'S', 'S']]
3、BiLSTM_CRF.py
转移概率矩阵transitions,transitionsij表示t时刻隐状态为qi,t+1时刻隐状态转换为qj的概率,即P(it+1=qj|it=qi)
import torch
from data_process import START_TAG,STOP_TAG
from torch import nn
def argmax(vec): #返回每一行最大值的索引
_, idx = torch.max(vec, 1)
return idx.item()
def prepare_sequence(seq, to_ix): #seq是字序列,to_ix是字和序号的字典
idxs = [to_ix[w] for w in seq] #idxs是字序列对应的向量
return torch.tensor(idxs, dtype=torch.long)
#LSE函数,模型中经常用到的一种路径运算的实现
def log_sum_exp(vec): #vec.shape=[1, target_size]
max_score = vec[0, argmax(vec)] #每一行的最大值
max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])
return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))
class BiLSTM_CRF(nn.Module):
def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):
super(BiLSTM_CRF, self).__init__()
self.embedding_dim = embedding_dim
self.hidden_dim = hidden_dim
self.vocab_size = vocab_size
self.tag_to_ix = tag_to_ix
self.tagset_size = len(tag_to_ix)
self.word_embeds = nn.Embedding(vocab_size, embedding_dim)
self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2, num_layers=1, bidirectional=True)
self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size) # Maps the output of the LSTM into tag space
self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size)) #随机初始化转移矩阵
self.transitions.data[tag_to_ix[START_TAG], :] = -10000 #tag_to_ix[START_TAG]: 3(第三行,即其他状态到START_TAG的概率)
self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000 #tag_to_ix[STOP_TAG]: 4(第四列,即STOP_TAG到其他状态的概率)
self.hidden = self.init_hidden()
def init_hidden(self):
return (torch.randn(2, 1, self.hidden_dim // 2), torch.randn(2, 1, self.hidden_dim // 2))
#所有路径的得分,CRF的分母
def _forward_alg(self, feats):
init_alphas = torch.full((1, self.tagset_size), -10000.) #初始隐状态概率,第1个字是O1的实体标记是qi的概率
init_alphas[0][self.tag_to_ix[START_TAG]] = 0.
forward_var = init_alphas #初始状态的forward_var,随着step t变化
for feat in feats: #feat的维度是[1, target_size]
alphas_t = []
for next_tag in range(self.tagset_size): #给定每一帧的发射分值,按照当前的CRF层参数算出所有可能序列的分值和
emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size) #发射概率[1, target_size] 隐状态到观测状态的概率
trans_score = self.transitions[next_tag].view(1, -1) #转移概率[1, target_size] 隐状态到下一个隐状态的概率
next_tag_var = forward_var + trans_score + emit_score #本身应该相乘求解的,因为用log计算,所以改为相加
alphas_t.append(log_sum_exp(next_tag_var).view(1))
forward_var = torch.cat(alphas_t).view(1, -1)
terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]] #最后转到[STOP_TAG],发射分值为0,转移分值为列向量 self.transitions[:, [self.tag2ix[END_TAG]]]
return log_sum_exp(terminal_var)
#得到feats,维度=len(sentence)*tagset_size,表示句子中每个词是分别为target_size个tag的概率
def _get_lstm_features(self, sentence):
self.hidden = self.init_hidden()
embeds = self.word_embeds(sentence).view(len(sentence), 1, -1)
lstm_out, self.hidden = self.lstm(embeds, self.hidden)
lstm_out = lstm_out.view(len(sentence), self.hidden_dim)
lstm_feats = self.hidden2tag(lstm_out)
return lstm_feats
#正确路径的分数,CRF的分子
def _score_sentence(self, feats, tags):
score = torch.zeros(1)
tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]], dtype=torch.long), tags])
for i, feat in enumerate(feats):
#self.transitions[tags[i + 1], tags[i]] 是从标签i到标签i+1的转移概率
#feat[tags[i+1]], feat是step i的输出结果,有5个值,对应B, I, E, START_TAG, END_TAG, 取对应标签的值
score = score + self.transitions[tags[i + 1], tags[i]] + feat[tags[i + 1]] # 沿途累加每一帧的转移和发射
score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[-1]] # 加上到END_TAG的转移
return score
#解码,得到预测序列的得分,以及预测的序列
def _viterbi_decode(self, feats):
backpointers = [] #回溯路径;backpointers[i][j]=第i帧到达j状态的所有路径中, 得分最高的那条在i-1帧是什么状态
# Initialize the viterbi variables in log space
init_vvars = torch.full((1, self.tagset_size), -10000.)
init_vvars[0][self.tag_to_ix[START_TAG]] = 0
forward_var = init_vvars
for feat in feats:
bptrs_t = []
viterbivars_t = []
for next_tag in range(self.tagset_size):
next_tag_var = forward_var + self.transitions[next_tag] #其他标签(B,I,E,Start,End)到标签next_tag的概率
best_tag_id = argmax(next_tag_var) #选择概率最大的一条的序号
bptrs_t.append(best_tag_id)
viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))
forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1) #从step0到step(i-1)时5个序列中每个序列的最大score
backpointers.append(bptrs_t)
terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]] #其他标签到STOP_TAG的转移概率
best_tag_id = argmax(terminal_var)
path_score = terminal_var[0][best_tag_id]
best_path = [best_tag_id]
for bptrs_t in reversed(backpointers): #从后向前走,找到一个best路径
best_tag_id = bptrs_t[best_tag_id]
best_path.append(best_tag_id)
start = best_path.pop()
assert start == self.tag_to_ix[START_TAG] #安全性检查
best_path.reverse() #把从后向前的路径倒置
return path_score, best_path
#求负对数似然,作为loss
def neg_log_likelihood(self, sentence, tags):
feats = self._get_lstm_features(sentence) #emission score
forward_score = self._forward_alg(feats) #所有路径的分数和,即b
gold_score = self._score_sentence(feats, tags) #正确路径的分数,即a
return forward_score - gold_score #注意取负号 -log(a/b) = -[log(a) - log(b)] = log(b) - log(a)
def forward(self, sentence):
lstm_feats = self._get_lstm_features(sentence)
score, tag_seq = self._viterbi_decode(lstm_feats)
return score, tag_seq
4、training.py
from data_process import read_file, tag_to_ix
from config import *
from BiLSTM_CRF import *
import torch
from torch import nn
from torch import optim
_, content, label = read_file(filename)
def train_data(content, label):
train_data = []
for i in range(len(label)):
train_data.append((content[i], label[i]))
return train_data
data = train_data(content,label)
word_to_ix = {}
for sentence, tags in data:
for word in sentence:
if word not in word_to_ix:
word_to_ix[word] = len(word_to_ix) #单词映射,字到序号
model = BiLSTM_CRF(len(word_to_ix), tag_to_ix, EMBEDDING_DIM, HIDDEN_DIM)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
#训练
for epoch in range(epochs):
for sentence, tags in data:
model.zero_grad()
sentence_in = prepare_sequence(sentence, word_to_ix)
targets = torch.tensor([tag_to_ix[t] for t in tags], dtype=torch.long)
loss = model.neg_log_likelihood(sentence_in, targets)
loss.backward()
optimizer.step()
if epoch%10 == 0:
print('epoch/epochs: {}/{}, loss:{:.6f}'.format(epoch+1, epochs, loss.data[0]))
#保存模型
torch.save(model,'cws.model')
torch.save(model.state_dict(),'cws_all.model')
打印结果:
epoch/epochs: 1/100, loss:33.839325
epoch/epochs: 11/100, loss:31.749798
epoch/epochs: 21/100, loss:29.822870
epoch/epochs: 31/100, loss:27.391972
epoch/epochs: 41/100, loss:26.033567
epoch/epochs: 51/100, loss:24.467463
epoch/epochs: 61/100, loss:22.403660
epoch/epochs: 71/100, loss:20.725002
epoch/epochs: 81/100, loss:18.280849
epoch/epochs: 91/100, loss:16.049187
5、test_model.py
调用上面保存的模型,进行预测。
from trainning import word_to_ix
from data_process import prepare_sequence
import torch
net = torch.load('cws.model')
net.eval()
stri="改善人民生活水平,建设社会主义政治经济。"
precheck_sent = prepare_sequence(stri, word_to_ix)
#precheck_sent= tensor([ 45, 102, 23, 24, 80, 98, 140, 141, 17, 32, 33, 37, 38, 39, 40, 103, 104, 60, 61, 12])
label = net(precheck_sent)[1]
#net(precheck_sent)= (tensor(32.3123, grad_fn=<SelectBackward>), [0, 2, 0, 2, 0, 2, 0, 2, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 2])
cws=[]
for i in range(len(label)):
cws.extend(stri[i])
if label[i]==2 or label==3:
cws.append('/')
#cws= ['改', '善', '/', '人', '民', '/', '生', '活', '/', '水', '平', '/', ',', '建', '设', '/', '社', '会', '/', '主', '义', '/', '政', '治', '/', '经', '济', '/', '。']
print('输入未分词语句:', stri)
print('分词结果:', ''.join(cws))
打印结果:
输入未分词语句: 改善人民生活水平,建设社会主义政治经济。
分词结果: 改善/人民/生活/水平/,建设/社会/主义/政治/经济/。
参考资料:
浅谈分词算法(3)基于字的分词方法(HMM)
浅谈分词算法(4)基于字的分词方法(CRF)
Pytorch-基于BiLSTM+CRF实现中文分词
Itenyh版-用HMM做中文分词三:前向算法和Viterbi算法的开销