尝试增加了实体识别来提高标点符号预测的准确性,运用对抗学习的方式,以标点符号预测为主任务,共同优化损失函数为最小,预测decoder时只使用标点预测任务。 考虑到小学生低年级(1,2,3年级)和高年级(4,5,6年级)的作文水平差异,使用不同难度的语料训练模型,高年级使用人民日报作为训练集,低年级使用儿童文学。
根据小学生这个年龄段群体,选择查找网上相关的儿童文学杂志,处理成一个统一的txt文档格式,去除其中的网络资源、作者简介等与文章内容不相关的信息。同时去除停用词等。
原始数据如下: 处理结果如下:
对小学生作文数据集,由于我们找到的是范文数据,需要处理成有一定的标点错误的情况,如下所示:
低年龄阶段的小学生习惯于所有标点符号全部为逗号,根据小学生范文,修改其中的所有标点符号为逗号来模拟一逗到底数据集: 调用中文标点包:
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()原始数据集效果: 生成数据集效果:
对于尤其为高年级的小学生,作文中的标点符号错误更多的可能是少量的错误连接句子,和误用,因此根据此情况生成相应的数据集,用于后面的系统输入学生作文的模拟样本,对整个系统的标点符号更正效果进行可视化的查看:
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的转换来合成最后的文章。
原始数据集效果:
生成数据集效果:
图中输入是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+punc对bert做一样的微调去除segment层并替换了分词器
bert-pretrained: 转化数据成bert适合的格式
首先是拿到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 rst1.MSR dataset http://sighan.cs.uchicago.edu/bakeoff2005/
通过预处理对训练集测试集进行相关的处理并生成对应的标记文件,词向量等:
结果准确率
包括一个任务共享层bert,两个多任务和一个对抗任务进行结合 任务共享层为bert模型,两个具体的任务分类器为实体识别任务和标点符号划分任务,利用task discriminator 判断每个输入的句子来自哪个任务法人数据集。在训练后,任务区分器和共享特征提取器会达到一个点:任务区分器不能通过共享特征提取器的输出判断该句子来自哪一个任务。
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)共同优化两个损失函数: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_losssoftmax_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 yProposed 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每次迭代参数的学习率都有一定的范围,不会因为梯度很大而导致学习率(步长)也变得很大,参数的值相对比较稳定。
optimizer = tf.train.AdamOptimizer(0.001)在训练过程中,每一轮都随机选择一个任务,然后从该任务的训练集中选取 一个batch的数据。通过Adam优化函数进行优化loss。因为二者的收敛率不同,所以根据punc的性能进行early stopping。
儿童文学(低年级)+人民日报(高年级)
高年级训练过程: 高年级准确率: 低年级训练过程: 低年级训练准确度: