小学生标点符号修正工作内容汇总-方向算法2
1.相关算法的查找和模型的确定
1.1模型的确定及其亮点
尝试增加了实体识别来提高标点符号预测的准确性,运用对抗学习的方式,以标点符号预测为主任务,共同优化损失函数为最小,预测decoder时只使用标点预测任务。
考虑到小学生低年级(1,2,3年级)和高年级(4,5,6年级)的作文水平差异,使用不同难度的语料训练模型,高年级使用人民日报作为训练集,低年级使用儿童文学。
2.数据集的查找与预处理
2.1 数据集的选取及处理
根据小学生这个年龄段群体,选择查找网上相关的儿童文学杂志,处理成一个统一的txt文档格式,去除其中的网络资源、作者简介等与文章内容不相关的信息。同时去除停用词等。
原始数据如下:
处理结果如下:
2.2 针对测试情况生成的用于测试的数据
对小学生作文数据集,由于我们找到的是范文数据,需要处理成有一定的标点错误的情况,如下所示:
2.2.1 生成一逗到底数据集
低年龄阶段的小学生习惯于所有标点符号全部为逗号,根据小学生范文,修改其中的所有标点符号为逗号来模拟一逗到底数据集:
调用中文标点包:
from zhon.hanzi import punctuation
对高低年级的作文集分别做如下处理(例子为高年级数据集的处理)
首先遍历每一行,再接着遍历每一行中的字符,存在于标点符号包中一致的字符,则替换成逗号
punctuation_str = punctuation
print("中文标点符合:", punctuation_str)
file=open(r'Senior.txt','r',encoding='utf-8')#打开源文件
f=open(r'Senior_comma.txt','w',encoding='utf-8')#打开写入文件
for line in file.readlines():
if line.split(): #过滤空行
line=line.replace('/n', ',')
for i in punctuation:
if i in line:
line = line.replace(i, ',')
print(line)
f.write(line)
f.close()
file.close()
原始数据集效果:
生成数据集效果:
2.2.2 生成模拟少量错误标点的小学生作文数据集
对于尤其为高年级的小学生,作文中的标点符号错误更多的可能是少量的错误连接句子,和误用,因此根据此情况生成相应的数据集,用于后面的系统输入学生作文的模拟样本,对整个系统的标点符号更正效果进行可视化的查看:
file=open(r'senior_data.txt','r',encoding='utf-8')#打开源文件
f=open(r'Senior_wrongplace.txt','w',encoding='utf-8')#打开写入文件
list=[]
for line in file.readlines():
a = 0
for i in line:
a=a+1
if line.split(): #过滤空行
line=line.replace('/n', '')
for i in punctuation:
if i in line:
temp = i
lines = line.split(i)
print(lines)
for l in lines:
number = random.randint(1,a)
new1=""
for p1 in lines[:number]:
new1 += str(p1)
new2= ""
for p2 in lines[number:]:
new1 += str(p2)
new = str(new1+str(temp)+new2)
f.write(new)
f.close()
file.close()
上述通过遍历每行的标点数目numbers,随机生成numbers个不超过改行总字数的随机数,并添加相对应的标点符号,通过列表于str的转换来合成最后的文章。
原始数据集效果:
生成数据集效果:
3.基于bert的实体识别
3.1 bert_bilstm_crf_ner模型
3.1.1 模型的定义BERT+Bilstm+CRF
Bilstm+CRF模型
图中输入是word embedding,使用双向lstm进行encode,对于lstm的hidden层,接入一个大小为[hidden_dim,num_label]的一个全连接层就可以得到每一个step对应的每个label的概率,也就是上图黄色框的部分,将lstm全连接层的结果作为发射概率,CRF的作用就是通过统计label直接的转移概率对结果lstm的结果加以限制:如I这个标签后面不能接O,B后面不能接B。
def add_bilstm_crf_layer(self):
"""
bilstm-crf网络
:return:
"""
if self.is_training:
# lstm input dropout rate set 0.5 will get best score
self.embedded_chars = tf.nn.dropout(self.embedded_chars, self.droupout_rate)
#blstm
lstm_output = self.blstm_layer(self.embedded_chars)
#project
logits = self.project_bilstm_layer(lstm_output)
#crf
loss, trans = self.crf_layer(logits)
# CRF decode, pred_ids 是一条最大概率的标注路径
pred_ids, _ = crf.crf_decode(potentials=logits, transition_params=trans, sequence_length=self.lengths)
return ((loss, logits, trans, pred_ids))
Bert模型
通bert+punc对bert做一样的微调去除segment层并替换了分词器
3.1.2 数据准备
bert-pretrained:
转化数据成bert适合的格式
3.1.3 Dataprocessor的改写
class NerProcessor(DataProcessor):
def get_train_examples(self, data_dir):
return self._create_example(
self._read_data(os.path.join(data_dir, "train.txt")), "train"
)
def get_dev_examples(self, data_dir):
return self._create_example(
self._read_data(os.path.join(data_dir, "dev.txt")), "dev"
)
def get_test_examples(self, data_dir):
return self._create_example(
self._read_data(os.path.join(data_dir, "test.txt")), "test")
def get_labels(self):
return ["O", "B-PER", "I-PER", "B-ORG", "I-ORG", "B-LOC", "I-LOC", "X"]
def _create_example(self, lines, set_type):
examples = []
for (i, line) in enumerate(lines):
guid = "%s-%s" % (set_type, i)
text = tokenization.convert_to_unicode(line[1])
label = tokenization.convert_to_unicode(line[0])
if i == 0:
print(label)
examples.append(InputExample(guid=guid, text=text, label=label))
return examples
3.1.4 model的定义
首先是拿到bert的outpu,在进行相应的bilstm_crf训练。
(total_loss, logits, trans, pred_ids) = create_model(
bert_config, is_training, input_ids, input_mask, segment_ids, label_ids,
num_labels, use_one_hot_embeddings)
def create_model(bert_config, is_training, input_ids, input_mask,
segment_ids, labels, num_labels, use_one_hot_embeddings):
"""
创建X模型
:param bert_config: bert 配置
:param is_training:
:param input_ids: 数据的idx 表示
:param input_mask:
:param segment_ids:
:param labels: 标签的idx 表示
:param num_labels: 类别数量
:param use_one_hot_embeddings:
:return:
"""
# 使用数据加载BertModel,获取对应的字embedding
model = modeling.BertModel(
config=bert_config,
is_training=is_training,
input_ids=input_ids,
input_mask=input_mask,
token_type_ids=segment_ids,
use_one_hot_embeddings=use_one_hot_embeddings
)
# 获取对应的embedding 输入数据[batch_size, seq_length, embedding_size]
embedding = model.get_sequence_output()
max_seq_length = embedding.shape[1].value
used = tf.sign(tf.abs(input_ids))
lengths = tf.reduce_sum(used, reduction_indices=1) # [batch_size] 大小的向量,包含了当前batch中的序列长度
blstm_crf = BILSTM_CRF(embedded_chars=embedding, hidden_unit=FLAGS.lstm_size, cell_type=FLAGS.cell, num_layers=FLAGS.num_layers,
droupout_rate=FLAGS.droupout_rate, initializers=initializers, num_labels=num_labels,
seq_length=max_seq_length, labels=labels, lengths=lengths, is_training=is_training)
rst = blstm_crf.add_blstm_crf_layer()
return rst
3.1.5 对数据集的预处理构建实体识别label
def convert_single_example(ex_index, example, label_list, max_seq_length, tokenizer, mode):
"""
将一个样本进行分析,然后将字转化为id, 标签转化为id,然后结构化到InputFeatures对象中
:param ex_index: index
:param example: 一个样本
:param label_list: 标签列表
:param max_seq_length:
:param tokenizer:
:param mode:
:return:
"""
label_map = {}
# 1表示从1开始对label进行index化
for (i, label) in enumerate(label_list, 1):
label_map[label] = i
# 保存label->index 的map
with codecs.open(os.path.join(FLAGS.output_dir, 'label2id.pkl'), 'wb') as w:
pickle.dump(label_map, w)
textlist = example.text.split(' ')
labellist = example.label.split(' ')
tokens = []
labels = []
for i, word in enumerate(textlist):
# 分字
token = tokenizer.tokenize(word)
tokens.extend(token)
label_1 = labellist[i]
for m in range(len(token)):
if m == 0:
labels.append(label_1)
else: # 一般不会出现else
labels.append("X")
# 序列截断
if len(tokens) >= max_seq_length - 1:
tokens = tokens[0:(max_seq_length - 2)] # -2 的原因是因为序列需要加一个句首和句尾标志
labels = labels[0:(max_seq_length - 2)]
ntokens = []
segment_ids = []
label_ids = []
ntokens.append("[CLS]") # 句子开始设置CLS 标志
segment_ids.append(0)
label_ids.append(label_map["[CLS]"])
for i, token in enumerate(tokens):
ntokens.append(token)
segment_ids.append(0)
label_ids.append(label_map[labels[i]])
ntokens.append("[SEP]") # 句尾添加[SEP] 标志
segment_ids.append(0)
label_ids.append(label_map["[SEP]"])
input_ids = tokenizer.convert_tokens_to_ids(ntokens) # 将序列中的字(ntokens)转化为ID形式
input_mask = [1] * len(input_ids)
# padding, 使用
while len(input_ids) < max_seq_length:
input_ids.append(0)
input_mask.append(0)
segment_ids.append(0)
# we don't concerned about it!
label_ids.append(0)
ntokens.append("**NULL**")
# label_mask.append(0)
# print(len(input_ids))
assert len(input_ids) == max_seq_length
assert len(input_mask) == max_seq_length
assert len(segment_ids) == max_seq_length
assert len(label_ids) == max_seq_length
# assert len(label_mask) == max_seq_length
# 打印部分样本数据信息
if ex_index < 5:
tf.logging.info("*** Example ***")
tf.logging.info("guid: %s" % (example.guid))
tf.logging.info("tokens: %s" % " ".join(
[tokenization.printable_text(x) for x in tokens]))
tf.logging.info("input_ids: %s" % " ".join([str(x) for x in input_ids]))
tf.logging.info("input_mask: %s" % " ".join([str(x) for x in input_mask]))
tf.logging.info("segment_ids: %s" % " ".join([str(x) for x in segment_ids]))
tf.logging.info("label_ids: %s" % " ".join([str(x) for x in label_ids]))
# tf.logging.info("label_mask: %s" % " ".join([str(x) for x in label_mask]))
# 结构化为一个类
feature = InputFeatures(
input_ids=input_ids,
input_mask=input_mask,
segment_ids=segment_ids,
label_ids=label_ids,
# label_mask = label_mask
)
# mode='test'的时候才有效
write_tokens(ntokens, mode)
return feature
3.2 测试bert_bilstm_crf_ner模型
3.2.1 数据集的准备
1.MSR dataset
http://sighan.cs.uchicago.edu/bakeoff2005/
3.2.2 预处理训练
通过预处理对训练集测试集进行相关的处理并生成对应的标记文件,词向量等:
3.2.3 训练数据集
结果准确率
4.bert_punc_ner模型
4.1 构建bert_punc_ner模型
包括一个任务共享层bert,两个多任务和一个对抗任务进行结合
任务共享层为bert模型,两个具体的任务分类器为实体识别任务和标点符号划分任务,利用task discriminator 判断每个输入的句子来自哪个任务法人数据集。在训练后,任务区分器和共享特征提取器会达到一个点:任务区分器不能通过共享特征提取器的输出判断该句子来自哪一个任务。
4.1.1 参数设定
class Setting(object):
def __init__(self):
self.lr=0.001
self.word_dim=100
self.lstm_dim=140
self.num_units=280
self.num_heads=8
self.num_steps=80
self.keep_prob=0.7
self.keep_prob1=0.6
self.in_keep_prob=0.7
self.out_keep_prob=0.6
self.batch_size=64
self.clip=5
self.num_epoches=260
self.adv_weight=0.06
self.task_num=2
self.punc_tags_num=7
self.ner_tags_num=7
4.1.2 transfer模型的初始化
- is_training:是否是模型训练
- word_embed:bertpunc和实体识别均为字符级embedding,将映射离散字符放入分布式表示中。
- task_num: task的数量,此处为标点预测任务和实体识别任务
- segment_ids:分词的id
- task_label: 最终任务目标punctuation的标签
- num_labels: 类别数量
- bertpunc_num:bertpunc微调的类别数量
- ner_num:实体识别微调的类别数量
- label_punc:标点符号输出层的label
- task_label:实体识别输出层的label
def __init__(self,setting,word_embed,adv,is_train):
self.lr = setting.lr
self.word_dim = setting.word_dim
self.lstm_dim = setting.lstm_dim
self.num_units = setting.num_units
self.num_steps = setting.num_steps
self.num_heads = setting.num_heads
self.keep_prob = setting.keep_prob
self.keep_prob1 = setting.keep_prob1
self.in_keep_prob = setting.in_keep_prob
self.out_keep_prob = setting.out_keep_prob
self.batch_size = setting.batch_size
self.word_embed = word_embed
self.clip = setting.clip
self.adv_weight = setting.adv_weight
self.task_num = setting.task_num
self.adv = adv
self.is_train = is_train
self.bertpunc_num = setting.bertpunc_num
self.ner_num=setting.ner_num
self.input = tf.placeholder(tf.int32, [None, self.num_steps])
self.label_punc = tf.placeholder(tf.int32, [None, self.num_steps])
self.label_ner=tf.placeholder(tf.int32, [None,self.num_steps])
self.task_label = tf.placeholder(tf.int32, [None,2])
self.sent_len = tf.placeholder(tf.int32, [None])
self.is_bertpunc = tf.placeholder(dtype=tf.int32)
with tf.variable_scope('word_embedding'):
self.embedding = tf.get_variable(name='embedding', dtype=tf.float32,
initializer=tf.cast(self.word_embed, tf.float32))
4.1.3 Multi-task learning
def multi_task(self):
input = tf.nn.embedding_lookup(self.embedding, self.input)
if self.is_train:
input=tf.nn.dropout(input,self.keep_prob)
punc层:
with tf.variable_scope('punc'):
embedding = model.get_sequence_output()
max_seq_length = embedding.shape[1].value
used = tf.sign(tf.abs(input_ids))
lengths = tf.reduce_sum(used, reduction_indices=1) # [batch_size] 大小的向量,包含了当前batch中的序列长度
punc_private_output =tf.layers.dense(embedding, units=num_labels, use_bias=True)
ner层:
with tf.variable_scope('ner'):
embedding = model.get_sequence_output()
max_seq_length = embedding.shape[1].value
used = tf.sign(tf.abs(input_ids))
lengths = tf.reduce_sum(used, reduction_indices=1) # [batch_size] 大小的向量,包含了当前batch中的序列长度
if self.is_train:
blstm_crf_fw = BILSTM_CRF(embedded_chars=self.embedding, hidden_unit=FLAGS.lstm_size, cell_type=FLAGS.cell, num_layers=FLAGS.num_layers,
droupout_rate=FLAGS.droupout_rate, initializers=self.initializers, num_labels=num_labels,
seq_length=max_seq_length, labels=self.labels, lengths=self.lengths, is_training=self.is_training)
blstm_crf_bw = BILSTM_CRF(embedded_chars=self.embedding, hidden_unit=FLAGS.lstm_size, cell_type=FLAGS.cell, num_layers=FLAGS.num_layers,
droupout_rate=FLAGS.droupout_rate, initializers=self.initializers, num_labels=num_labels,
seq_length=self.max_seq_length, labels=self.labels, lengths=self.lengths, is_training=self.is_training)
ner_private_cell_fw = tf.nn.rnn_cell.DropoutWrapper(blstm_crf_fw, input_keep_prob=self.in_keep_prob,
output_keep_prob=self.out_keep_prob)
ner_private_cell_bw = tf.nn.rnn_cell.DropoutWrapper(blstm_crf_bw, input_keep_prob=self.in_keep_prob,
output_keep_prob=self.out_keep_prob)
(output_fw, output_bw), _ = tf.nn.bidirectional_dynamic_rnn(
ner_private_cell_fw, ner_private_cell_bw, input, sequence_length=self.sent_len, dtype=tf.float32)
ner_private_output = tf.concat([output_fw, output_bw], axis=-1)
4.1.4 loss的定义
共同优化两个损失函数:Loss_tasks和Loss_adv。对于标准的多任务学习,优化了共享表示,以最大程度地减少主要任务和辅助任务的损失。对抗式多任务学习不同于标准的多任务学习。对于对抗式多任务学习,训练共享参数以最大化标点预测任务和实体识别任务的分类精度,但最小化任务识别器的分类精度。但是,对抗式多任务学习通过GRL在对抗任务识别器方面具有对抗性。它鼓励在优化过程中出现独立于任务的功能。因此,共享功能成为标点符号和NER标签具有区别性,但任务不变。改进的任务不变性导致标点预测任务的性能提高。
定义loss=loss_tasks+loss_adv即为总的loss是多任务loss和adversarial loss之和
self.tasks_loss=tf.cast(self.is_ner,tf.float32)*self.punc_loss+tf.cast((1-self.is_ner),tf.float32)*self.ner_loss
self.adv_loss = self.adversarial_loss(max_pool_output)
self.loss=self.tasks_loss+self.adv_weight*self.adv_loss
4.1.5 构建模型
softmax_create_model() 用来构建模型,模型的最后一步是在punc和ner输出的字符embedding上做最大池化(MAX Pooling)、梯度反转(Gradient Reversal Layer)、全连接层(softmax)来实现最后的输出。
引入GRL是为了确保对于任务识别器而言,特征分布尽可能地难以区分。因此,对抗性的BERT_PUNC_NER将学习一种可以很好地概括从一项任务到另一项任务的表示形式。它们确保共享参数的内部表示不包含任何任务区分信息,使这个分类器分不清标点符号的识别和实体识别任务,以达到混淆视听的目的,实现域迁移。
梯度反转的意义有两点:
(1)正向传播时传递权值不变
(2)反向传播时,神经元权值增量符号取反,即与目标函数方向切好相反达到对抗的目的
Gradient Reversal Layer
class FlipGradientBuilder(object):
def __init__(self):
self.num_calls = 0
def __call__(self, x, l=1.0):
grad_name = "FlipGradient%d" % self.num_calls
@ops.RegisterGradient(grad_name)
def _flip_gradients(op, grad):
return [tf.negative(grad)*l]
g=tf.get_default_graph()
with g.gradient_override_map({"Identity":grad_name}):
y=tf.identity(x)
self.num_calls+=1
return y
Proposed adversarial BERT-punc model:max_pooling+GRL+softmax
def adversarial_loss(self,feature):
flip_gradient = base_model.FlipGradientBuilder()
feature=flip_gradient(feature)
if self.is_train:
feature=tf.nn.dropout(feature,self.keep_prob1)
W_adv = tf.get_variable(name='W_adv', shape=[2 * self.lstm_dim, self.task_num],
dtype=tf.float32,
initializer=tf.contrib.layers.xavier_initializer())
b_adv = tf.get_variable(name='b_adv', shape=[self.task_num], dtype=tf.float32,
initializer=tf.contrib.layers.xavier_initializer())
logits = tf.nn.xw_plus_b(feature,W_adv,b_adv)
adv_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=self.task_label))
return adv_loss
4.1.6 优化器的选择AdamOptimizer
每次迭代参数的学习率都有一定的范围,不会因为梯度很大而导致学习率(步长)也变得很大,参数的值相对比较稳定。
optimizer = tf.train.AdamOptimizer(0.001)
4.1.7 Training
在训练过程中,每一轮都随机选择一个任务,然后从该任务的训练集中选取 一个batch的数据。通过Adam优化函数进行优化loss。因为二者的收敛率不同,所以根据punc的性能进行early stopping。
4.2 基于bert_punc_ner最终模型训练效果
4.2.1 训练数据集
儿童文学(低年级)+人民日报(高年级)
4.2.2 Training
高年级训练过程:
高年级准确率:
低年级训练过程:
低年级训练准确度:
4.2.3算法效果呈现