引言:NLP技术目前在社会各个领域都在应用,其中在命名实体识别方面应用很广泛,也是极具特色的。

一、利用NLP技术训练模型,来识别病例里面的关键信息。

1、搜集数据(训练数据、验证数据、测试数据还有一个字典(key:命名实体,value:实体类型)):
训练数据、验证数据、测试数据都是些病例文本信息,字典是我们要识别出来的命名实体,该字典会添加到,jieba分词工具里面,这样才能分出我们要的命名实体。

2、清洗、提取训练数据的特征

这个过程比较繁琐,这里就简单叙述一下:

1、创建一个词典

该词典是个dictkey是字的下标,value是字,这里每个字都是训练数据里面的,就相当于给训练数据里面每个字加一个索引,其中代表当句长不够,要添加的;代表在训练集中未出现的字,如下图:

Pytorch Bert_BiLSTM_CRF_NER 中文医疗命名实体识别项目 医学命名实体识别_git

2、创建一个实体类型字典:

Pytorch Bert_BiLSTM_CRF_NER 中文医疗命名实体识别项目 医学命名实体识别_nlp_02


如上图,key代表词性,value代表索引。3、数据封装:

有了上述2个字典、训练数据和初始给的一个字典(key:命名实体,value:实体类型),我们可以封装一下测试数据:

Pytorch Bert_BiLSTM_CRF_NER 中文医疗命名实体识别项目 医学命名实体识别_git_03


如上图:训练数据是一个101218行的数组train_data,每行是一个长度为4的数组train_arrtrain_arr代表一句文本,**train_arr[0]**文本内容,**train_arr[1]**每句文本里的词在词典里面对应下标;**train_arr[2]**每个字在jieba分词里的短语顺序;**train_arr[3]**代表每个字的词性。

三、创建模型:

1、模型初始化主体信息,还有些模型参数在这里就不一一列出来了:

#字下标输入层
class DataModel{
	#config 模型参数
	def __init__(self, config):
	    # 字下标输入层 train_arr[1]
		self.char_inputs = tf.placeholder(dtype=tf.int32, shape=[None, None], name="CharInPuts")
	 	# 字所在句子中分词短语的顺序
		self.seg_inputs = tf.placeholder(dtype=tf.int32, shape=[None, None], name="SegInPuts")
		# 字对应的词性
		self.targets = tf.placeholder(dtype=tf.int32, shape=[None, None], name="TargetOuts")
		# dropout参数
		self.dropout = tf.placeholder(dtype=tf.float32, name="Dropout")

		# 句子的真实长度
		self.lengths = tf.cast(tf.reduce_sum(tf.sign(tf.abs(self.char_inputs)), reduction_indices=1), tf.int32)
		'''
		其他数据变化操作
		'''
	}

2、下面具体描述模型中其他的数据变化:

a、嵌入层

embedding = self.embedding_layer(self.char_inputs, self.seg_inputs, config)

#1、输入self.char_inputs和self.seg_inputs
#2、创建一个shape=[self.num_chars, self.char_dim]的矩阵;self.num_chars总的词数,每个词用一个self.char_dim维度的向量表示
#3、创建一个shape=[self.num_segs, self.seg_dim]的矩阵;self.numsegs词的顺序(0,1,2,3),每个顺序数组用一个self.seg_dim维度的向量表示
#4、把这两个矩阵合并最后生成一个shape=[?,?,self.char_dim+self.seg_dim]的矩阵并返回
def embedding_layer(self, char_inputs, seg_inputs, config, name=None):
    embedding = []
    self.char_inputs_test=char_inputs
    self.seg_inputs_test=seg_inputs
    with tf.variable_scope("char_embedding" if not name else name), tf.device('/cpu:0'):
        self.char_lookup = tf.get_variable(
                name="char_embedding",
                shape=[self.num_chars, self.char_dim],
                initializer=self.initializer)
        embedding.append(tf.nn.embedding_lookup(self.char_lookup, char_inputs))
        #self.embedding1.append(tf.nn.embedding_lookup(self.char_lookup, char_inputs))
        if config["seg_dim"]:
            with tf.variable_scope("seg_embedding"), tf.device('/cpu:0'):
                self.seg_lookup = tf.get_variable(
                    name="seg_embedding",
                    #shape=[4*20]
                    shape=[self.num_segs, self.seg_dim],
                    initializer=self.initializer)
                embedding.append(tf.nn.embedding_lookup(self.seg_lookup, seg_inputs))
        embed = tf.concat(embedding, axis=-1)
    self.embed_test=embed
    self.embedding_test=embedding
    return embed

b、下面将使用双向lstm方法提取特征信息:

bilstm_outputs = self.biLSTM_layer(model_inputs, self.lstm_dim, self.lengths)

#1、用for循环生成forward(前向)和backward(后向)2层lstm,lstm_dim代表h和c的维度,真实句长为lengths
#2、把前向和后向2层、每层节点数lstm_dim和真实句长,传入tf.nn.bidirectional_dynamic_rnn,返回outputs, final_states
#3、outputs代表每时刻的输出,final_states代表最后的输出
#4、这里我们取outputs数据,outputs是一个tuple(元组),元组里面有2个shape=(?,?,100)的Tensor,因为这是双向lstm,一个是前向输出,一个是后向输出,最后把outputs元组中2个Tensor在最后一个维度拼接,返回一个shape=(?,?,lstm_dim*2)的Tensor
def biLSTM_layer(self, model_inputs, lstm_dim, lengths, name=None):
    with tf.variable_scope("char_BiLSTM" if not name else name):
        lstm_cell = {}
        for direction in ["forward", "backward"]:
            with tf.variable_scope(direction):
                lstm_cell[direction] = tf.contrib.rnn.CoupledInputForgetGateLSTMCell(
                    lstm_dim,  #代表h和c的维度
                    use_peepholes=True,
                    initializer=self.initializer,
                    state_is_tuple=True)
        outputs, final_states = tf.nn.bidirectional_dynamic_rnn(
            lstm_cell["forward"],
            lstm_cell["backward"],
            model_inputs,
            dtype=tf.float32,
            sequence_length=lengths)
    return tf.concat(outputs, axis=2)

从上面我们得到 bilstm_outputs 输出,接下来我们计算分类:

self.logits = self.project_layer_bilstm(bilstm_outputs)

#1、传入我们从lstm中得到的lstm_outputs
#2、“hidden”里面先是把lstm_outputs通过reshape,变成2维度的,shape=[-1, lstm_dim*2];再是接一个全连接,并在output结果加个tanh激活函数,W、b都是模型参数,W的shape=(lstm_dim*2,lstm_dim),b的shape=(lstm_dim)
#3、“logits”里面也是一个全连接层,W的shape=(lstm_dim,num_tags),b的shape=(num_tags),num_tags是词性的类别数
#4、最后pred要重新映射为3维的,tf.reshape(pred, [-1, self.num_steps, self.num_tags]),num_steps是句长,num_tags是词性的类别数
def project_layer_bilstm(self, lstm_outputs, name=None):
    with tf.variable_scope("project"  if not name else name):
        with tf.variable_scope("hidden"):
            W = tf.get_variable("W", shape=[self.lstm_dim*2, self.lstm_dim],
                                dtype=tf.float32, initializer=self.initializer)

            b = tf.get_variable("b", shape=[self.lstm_dim], dtype=tf.float32,
                                initializer=tf.zeros_initializer())
            output = tf.reshape(lstm_outputs, shape=[-1, self.lstm_dim*2])
            hidden = tf.tanh(tf.nn.xw_plus_b(output, W, b))

        # project to score of tags
        with tf.variable_scope("logits"):
            W = tf.get_variable("W", shape=[self.lstm_dim, self.num_tags],
                                dtype=tf.float32, initializer=self.initializer)

            b = tf.get_variable("b", shape=[self.num_tags], dtype=tf.float32,
                                initializer=tf.zeros_initializer())

            pred = tf.nn.xw_plus_b(hidden, W, b)

        return tf.reshape(pred, [-1, self.num_steps, self.num_tags])

c、计算损失

self.loss = self.loss_layer(self.logits, self.lengths)

#传入我们计算的类别self.logits和句子的真实长度self.lengths
#这里不使用交叉熵做损失计算,用的是条件随机场,因为这里有状态转移做限制
def loss_layer(self, project_logits, lengths, name=None):
    with tf.variable_scope("crf_loss"  if not name else name):
        '''矩阵拼接'''
        small = -1000.0
        start_logits = tf.concat(
            [small * tf.ones(shape=[self.batch_size, 1, self.num_tags]), tf.zeros(shape=[self.batch_size, 1, 1])], axis=-1)
        pad_logits = tf.cast(small * tf.ones([self.batch_size, self.num_steps, 1]), tf.float32)
        logits = tf.concat([project_logits, pad_logits], axis=-1)
        logits = tf.concat([start_logits, logits], axis=1)
        targets = tf.concat(
            [tf.cast(self.num_tags*tf.ones([self.batch_size, 1]), tf.int32), self.targets], axis=-1)
		
		#crf_log_likelihood在一个条件随机场里面计算标签序列的log-likelihood
        #inputs: 一个形状为[batch_size, max_seq_len, num_tags] 的tensor,
        #一般使用BILSTM处理之后输出转换为他要求的形状作为CRF层的输入. 
        #tag_indices: 一个形状为[batch_size, max_seq_len] 的矩阵,其实就是真实标签. 
        #sequence_lengths: 一个形状为 [batch_size] 的向量,表示每个序列的长度. 
        #transition_params: 形状为[num_tags, num_tags] 的转移矩阵    
        
        self.trans = tf.get_variable(
            "transitions",
            shape=[self.num_tags + 1, self.num_tags + 1],
            initializer=self.initializer) 

		#一般使用BILSTM处理之后输出转换为他要求的形状作为CRF层的输入. 
        #log_likelihood: 标量,crf_log_likelihood在一个条件随机场里面计算标签序列的log-likelihood
        #self.trans 生成的转移矩阵
        #inputs: 一个形状为[batch_size, max_seq_len, num_tags] 的tensor
        #tag_indices: 一个形状为[batch_size, max_seq_len] 的矩阵,其实就是真实标签
        #transition_params: 形状为[num_tags, num_tags] 的转移矩阵
        #sequence_lengths: 一个形状为 [batch_size] 的向量,表示每个序列的长度         
        log_likelihood, self.trans = crf_log_likelihood(
            inputs=logits,
            tag_indices=targets,
            transition_params=self.trans,
            sequence_lengths=lengths+1)
        return tf.reduce_mean(-log_likelihood)

d、建立优化器,优化损失

with tf.variable_scope("optimizer"):
     optimizer = self.config["optimizer"]
     if optimizer == "sgd":
         self.opt = tf.train.GradientDescentOptimizer(self.lr)
     elif optimizer == "adam":
         self.opt = tf.train.AdamOptimizer(self.lr)
     elif optimizer == "adgrad":
         self.opt = tf.train.AdagradOptimizer(self.lr)
     else:
         raise KeyError

e、梯度下降:
为了防止在梯度下降的时候,出现梯度爆炸和梯度消失,这里把梯度控制在 -clip和clip之间

with tf.variable_scope("optimizer"):
	#反向传播
	grads_vars = self.opt.compute_gradients(self.loss)
	#梯度截断
	capped_grads_vars = [[tf.clip_by_value(g, -self.config["clip"], self.config["clip"]), v]for g, v in grads_vars] 
	#梯度下降
	self.train_op = self.opt.apply_gradients(capped_grads_vars, self.global_step)

f、保存模型:
保存所有模型参数

self.saver = tf.train.Saver(tf.global_variables(), max_to_keep=5)

四、模型训练

for i in range(100):
    for batch in train_manager.iter_batch(shuffle=True):
    	#train_manager是一个迭代器
        step, batch_loss = model.run_step(sess, True, batch)
        loss.append(batch_loss)
        if step % FLAGS.steps_check == 0:
            iteration = step // steps_per_epoch + 1
            logger.info("iteration:{} step:{}/{}, "
                        "NER loss:{:>9.6f}".format(
                iteration, step%steps_per_epoch, steps_per_epoch, np.mean(loss)))
            loss = []
	#每训练一轮,用验证数据验证一次
    best = evaluate(sess, model, "dev", dev_manager, id_to_tag, logger)
    if i%7==0:
        save_model(sess, model, FLAGS.ckpt_path, logger) #保存模型

五、验证

请输入句子:
患者神志清,精神可,无发热、无恶心、呕吐,无抽搐。进食夜眠好,二便正常。查体:Bp129/75mmHg,头颅无畸形,双侧瞳孔正大等圆,对光反射灵敏,右侧胸壁腋前线伤口外敷料固定好,无渗出,胸壁局部软组织无明显肿胀,无压痛。双肺叩清音,未闻及湿性啰音。腹软,无压痛,肠鸣音正常。病情好转,今日出院。
{'string': '患者神志清,精神可,无发热、无恶心、呕吐,无抽搐。进食夜眠好,二便正常。查体:Bp129/75mmHg,头颅无畸形,双侧瞳孔正大等圆,对光反射灵敏,右侧胸壁腋前线伤口外敷料固定好,无渗出,胸壁局部软组织无明显肿胀,无压痛。双肺叩清音,未闻及湿性啰音。腹软,无压痛,肠鸣音正常。病情好转,今日出院。', 'entities': [{'word': '发热', 'start': 11, 'end': 13, 'type': 'SYM'}, {'word': '恶心', 'start': 15, 'end': 17, 'type': 'SYM'}, {'word': '抽搐', 'start': 22, 'end': 24, 'type': 'SYM'}, {'word': '二便正常', 'start': 31, 'end': 35, 'type': 'SYM'}, {'word': '头颅', 'start': 52, 'end': 54, 'type': 'REG'}, {'word': '畸形', 'start': 55, 'end': 57, 'type': 'SGN'}, {'word': '双侧瞳孔', 'start': 58, 'end': 62, 'type': 'REG'}, {'word': '光反射灵敏', 'start': 68, 'end': 73, 'type': 'SGN'}, {'word': '右侧', 'start': 74, 'end': 76, 'type': 'REG'}, {'word': '胸壁', 'start': 76, 'end': 78, 'type': 'ORG'}, {'word': '腋前线伤口', 'start': 78, 'end': 83, 'type': 'DIS'}, {'word': '渗出', 'start': 91, 'end': 93, 'type': 'SYM'}, {'word': '明显', 'start': 102, 'end': 104, 'type': 'DEG'}, {'word': '肿胀', 'start': 104, 'end': 106, 'type': 'SYM'}, {'word': '压痛', 'start': 108, 'end': 110, 'type': 'SGN'}, {'word': '双肺', 'start': 111, 'end': 113, 'type': 'REG'}, {'word': '叩清音', 'start': 113, 'end': 116, 'type': 'SGN'}, {'word': '啰音', 'start': 121, 'end': 124, 'type': 'SGN'}, {'word': '腹软', 'start': 125, 'end': 127, 'type': 'SYM'}, {'word': '压痛', 'start': 129, 'end': 131, 'type': 'SGN'}, {'word': '肠鸣音', 'start': 132, 'end': 135, 'type': 'SGN'}]}