日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)


1.离线部分中的命名实体的审核模型1.命名实体的审核模型:训练RNN模型让其学会判断结构化的未审核数据中的疾病名/疾病对应的症状名是否符合正常语序,RNN模型负责处理结构化的未审核数据,主要将结构化的未审核数据预测输出为结构化的审核过的数据,最终把结构化的审核过的数据(疾病名/疾病对应的症状名)存储到NEO4J数据库中。2.训练命名实体的审核模型:1.训练数据train_data.csv内容格式:1/0 疾病名/疾病对应的症状名第一列为:1/0。1代表正样本,正常语序。0代表负样本,为正常语序的倒序。第二列为:疾病名/疾病对应的症状名。1/0含义:1代表正样本,正常语序:1	手掌软硬度异常0代表负样本,为正常语序的倒序:0	常异度硬软掌手2.通过读取训练数据train_data.csv中“标记为1/0的正负样本”的疾病名/疾病对应的症状名的数据集,让RNN模型学会判断结构化的未审核数据中的疾病名/疾病对应的症状名是否符合正常语序。3.命名实体的审核模型的预测流程:1.命名实体的审核模型要读取的数据:structured/noreview文件夹中结构化的未审核数据(structured/noreview文件夹中结构化的未审核数据实际为命名实体的识别模型预测输出的数据)1.“作为csv文件名的”疾病名2.每个疾病名.csv中每行就是一个该疾病对应的症状2.命名实体的审核模型要预测输出的数据:structured/reviewed文件夹中已审核过的结构化的数据1.“作为csv文件名的”疾病名2.每个疾病名.csv中每行就是一个该疾病对应的症状3.读取structured/noreview文件夹中结构化的未审核数据(疾病名/疾病对应的症状名)进行模型预测判断是否符合正常语序,符合则输出存储到structured/reviewed文件夹中代表为已审核过的数据,反之不符合正常语序则丢弃。最终把审核通过的疾病名和疾病对应的症状名关联在一起存储到NEO4J数据库中。注意:第一种方式为对“作为csv文件名的”疾病名和“文件中的疾病对应的”症状名两者同事都进行模型的预测判断,第二种方式仅为对“文件中的疾病对应的”症状名进行模型的预测判断,而不对“作为csv文件名的”疾病名进行模型的预测判断。第二种方式的特别之处:不使用命名实体的审核模型对“作为csv文件名的”疾病名进行预测判断,而是改为通过人工方式判断“作为csv文件名的”疾病名是否符合正常语序。因为通过人工方式判断便可以避免掉模型对“作为csv文件名的”疾病名的预测判断出现错误,而导致了CSV文件中的症状名内容也一同被丢弃掉的情况,判断避免掉疾病名的csv文件中的疾病对应的症状内容也一并被错误丢弃掉的情况。2.离线部分中的命名实体的识别模型(NER模型:BiLSTM+CRF模型)1.命名实体的识别模型(NER模型):使用的模型组合为BiLSTM+CRF模型来作为命名实体的识别模型,NER模型负责处理非结构化数据,主要从长文本的样本句子中抽取出疾病名/症状名这样的命名实体输出为结构化的未审核数据。然后还需要使用命名实体的审核模型(RNN模型)对结构化的未审核数据进行审核(预测)输出为结构化的审核过的数据,最终把结构化的审核过的数据(疾病名/疾病对应的症状名)存储到NEO4J数据库中。2.训练命名实体的识别模型:1.训练数据total.txt内容格式:1.第一列为:每条样本句子中的字符。第二列为:每条样本句子中的字符对应的真实标签。2.真实标签列表:["O","B-dis","I-dis","B-sym","I-sym"]dis表示疾病(disease), sym表示症状(symptom), B表示命名实体开头, I表示命名实体中间到结尾, O表示其他类型。B-dis: Begin-disease(疾病名的开始)I-dis: Inter -disease(疾病名的从中间到结尾)B-sym: Begin-symptom(症状名的开始)I-sym: Inter-symptom(症状名的从中间到结尾) O: Other 2.通过BiLSTM+CRF模型读取total.txt内容进行训练,让模型学会从普通文本句子中抽取出真实的疾病/疾病对应的症状相关的名称,并给抽取出疾病/疾病对应的症状相关的名称赋予预测标签。3.命名实体的识别模型(NER模型:BiLSTM+CRF模型)的预测:1.第一步:1.命名实体的识别模型要读取的数据:unstructured/norecognite文件夹中每个txt文件(即为非结构化数据)1.“作为txt文件名的”疾病名2.每个疾病名.txt中每行就是一条对该疾病进行症状描述的长文本语句2.命名实体的识别模型要预测输出的数据:structured/noreview文件夹中结构化的未审核数据1.“作为csv文件名的”疾病名2.每个疾病名.csv中每行就是一个该疾病对应的症状3.预测流程:命名实体的识别模型读取出每个疾病.txt文件中的症状描述的长文本语句,从长文本语句中抽取出对应该疾病名的短文本(单词)形式的症状名,作为未审核的结构化的数据存储到structured/noreview文件夹中每个对应的疾病名.csv中。2.第二步:便是使用命名实体的审核模型(RNN模型) 对未审核数据中的疾病名/疾病对应的症状名进行预测判断是否符合正常语序。预测流程便为命名实体的审核模型的预测流程,最终把数据输出为structured/reviewed文件夹中已审核过的结构化的数据3.离线部分中的结构化数据流水线 结构化的未审核数据:/data/structured/noreview文件夹中,每个csv文件名为疾病名,每个csv文件中的每行内容为疾病对应的症状名。结构化的已审核数据:/data/structured/reviewed文件夹中,每个csv文件名为疾病名,每个csv文件中的每行内容为疾病对应的症状名。4.离线部分中的非结构化数据流水线非结构化数据:unstructured/norecognite文件夹中,每个txt文件为疾病名,每个txt文件中每行的内容为对该疾病的进行症状描述的长文本语句。

bert模型仅是判断前后两个句子是否有关联的二分类。
如果前后两句话有关联的话,是一起提取这两句话中关键的症状信息融合在一起作为查询条件然后查询数据库中对应的疾病名。  

损失函数的定义:

  • BiLSTM层的输出维度是tag_size, 也就是每个单词w_i映射到tag的发射概率值, 假设BiLSTM的输出矩阵是P, 其中P(i,j)代表单词w_i映射到tag_j的非归一化概率. 对于CRF层, 假设存在一个转移矩阵A, 其中A(i,j)代表tag_j转移到tag_i的概率.

  • 对于输入序列X对应的输出tag序列y, 定义分数如下(本质上就是发射概率和转移概率的累加和):

  • 利用softmax函数, 为每一个正确的tag序列y定义一个概率值, 在真实的训练中, 只需要最大化似然概率p(y|X)即可, 具体使用对数似然如下:

  • BiLSTM+CRF模型的实现:
    • 第一步: 构建神经网络
    • 第二步: 文本信息张量化
    • 第三步: 计算损失函数第一项的分值
    • 第四步: 计算损失函数第二项的分值
    • 第五步: 维特比算法的实现
    • 第六步: 完善BiLSTM_CRF类的全部功能
  • 第一步: 构建神经网络
# 导入相关包与模块
import torch
import torch.nn as nnclass BiLSTM_CRF(nn.Module):def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim, num_layers, batch_size, sequence_length):'''description: 模型初始化:param vocab_size:          所有句子包含字符大小:param tag_to_ix:           标签与id对照字典:param embedding_dim:       字嵌入维度(即LSTM输入层维度input_size):param hidden_dim:          隐藏层向量维度:param num_layers:          神经网络的层数:param batch_size:          批次的数量:param sequence_length:     语句的限制最大长度'''# 继承函数的初始化super(BiLSTM_CRF, self).__init__()# 设置标签与id对照self.tag_to_ix = tag_to_ix# 设置标签大小,对应 BiLSTM 最终输出分数矩阵宽度self.tagset_size = len(tag_to_ix)# 设定 LSTM 输入特征大小self.embedding_dim = embedding_dim# 设置隐藏层维度self.hidden_dim = hidden_dim# 设置单词总数的大小self.vocab_size = vocab_size# 设置隐藏层的数量self.num_layers = num_layers# 设置语句的最大限制长度self.sequence_length = sequence_length# 设置批次的大小self.batch_size = batch_size# 构建词嵌入层, 两个参数分别是单词总数, 词嵌入维度self.word_embeds = nn.Embedding(vocab_size, embedding_dim)# 构建双向LSTM层, 输入参数包括词嵌入维度, 隐藏层大小, 堆叠的LSTM层数, 是否双向标志位self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2, num_layers=self.num_layers, bidirectional=True)# 构建全连接线性层, 一端对接LSTM隐藏层, 另一端对接输出层, 相应的维度就是标签数量tagset_sizeself.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)# 初始化转移矩阵, 转移矩阵是一个方阵[tagset_size, tagset_size]self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))# 按照损失函数小节的定义, 任意的合法句子不会转移到"START_TAG", 因此设置为-10000# 同理, 任意合法的句子不会从"STOP_TAG"继续向下转移, 也设置为-10000self.transitions.data[tag_to_ix[START_TAG], :] = -10000self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000# 初始化隐藏层, 利用单独的类函数init_hidden()来完成self.hidden = self.init_hidden()# 定义类内部专门用于初始化隐藏层的函数def init_hidden(self):# 为了符合LSTM的输入要求, 我们返回h0, c0, 这两个张量的shape完全一致# 需要注意的是shape: [2 * num_layers, batch_size, hidden_dim / 2]return (torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2), torch.randn(2 * self.num_layers, self.batch_size, self.hidden_dim // 2))
  • 输入参数:
# 开始字符和结束字符
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1
# 初始化的字符和序号的对应码表
char_to_id = {"双": 0, "肺": 1, "见": 2, "多": 3, "发": 4, "斑": 5, "片": 6,"状": 7, "稍": 8, "高": 9, "密": 10, "度": 11, "影": 12, "。": 13}
  • 调用:
model = BiLSTM_CRF(vocab_size=len(char_to_id),tag_to_ix=tag_to_ix,embedding_dim=EMBEDDING_DIM,hidden_dim=HIDDEN_DIM,num_layers=NUM_LAYERS,batch_size=BATCH_SIZE,sequence_length=SENTENCE_LENGTH)print(model)
  • 输出效果:
BiLSTM((word_embeds): Embedding(14, 200)(lstm): LSTM(200, 50, bidirectional=True)(hidden2tag): Linear(in_features=100, out_features=7, bias=True)
)
  • 第二步: 文本信息张量化
# 函数sentence_map完成中文文本信息的数字编码, 变成张量
def sentence_map(sentence_list, char_to_id, max_length):# 对一个批次的所有语句按照长短进行排序, 此步骤非必须sentence_list.sort(key=lambda c:len(c), reverse=True)# 定义一个最终存储结果特征向量的空列表sentence_map_list = []# 循环遍历一个批次内的所有语句for sentence in sentence_list:# 采用列表生成式完成字符到id的映射sentence_id_list = [char_to_id[c] for c in sentence]# 长度不够的部分用0填充padding_list = [0] * (max_length-len(sentence))# 将每一个语句向量扩充成相同长度的向量sentence_id_list.extend(padding_list)# 追加进最终存储结果的列表中sentence_map_list.append(sentence_id_list)# 返回一个标量类型值的张量return torch.tensor(sentence_map_list, dtype=torch.long)# 在类中将文本信息经过词嵌入层, BiLSTM层, 线性层的处理, 最终输出句子张量
def _get_lstm_features(self, sentence):self.hidden = self.init_hidden()# a = self.word_embeds(sentence)# print(a.shape)  torch.Size([8, 20, 200])# LSTM的输入要求形状为 [sequence_length, batch_size, embedding_dim]# LSTM的隐藏层h0要求形状为 [num_layers * direction, batch_size, hidden_dim]embeds = self.word_embeds(sentence).view(self.sequence_length, self.batch_size, -1)# LSTM的两个输入参数: 词嵌入后的张量, 随机初始化的隐藏层张量lstm_out, self.hidden = self.lstm(embeds, self.hidden)# 要保证输出张量的shape: [sequence_length, batch_size, hidden_dim]lstm_out = lstm_out.view(self.sequence_length, self.batch_size, self.hidden_dim)# 将BiLSTM的输出经过一个全连接层, 得到输出张量shape:[sequence_length, batch_size, tagset_size]lstm_feats = self.hidden2tag(lstm_out)return lstm_feats
  • 输入参数:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1
# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = ["确诊弥漫大b细胞淋巴瘤1年","反复咳嗽、咳痰40年,再发伴气促5天。","生长发育迟缓9年。","右侧小细胞肺癌第三次化疗入院","反复气促、心悸10年,加重伴胸痛3天。","反复胸闷、心悸、气促2多月,加重3天","咳嗽、胸闷1月余, 加重1周","右上肢无力3年, 加重伴肌肉萎缩半年"
]
  • 调用:
char_to_id = {"<PAD>":0}if __name__ == '__main__':for sentence in sentence_list:for _char in sentence:if _char not in char_to_id:char_to_id[_char] = len(char_to_id)sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)print("sentence_sequence:\n", sentence_sequence)model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \sequence_length=SENTENCE_LENGTH)sentence_features = model._get_lstm_features(sentence_sequence)print("sequence_features:\n", sentence_features)
  • 输出效果:
sentence_sequence:tensor([[14, 15, 16, 17, 18, 16, 19, 20, 21, 13, 22, 23, 24, 25, 26, 27, 28, 29,30,  0],[14, 15, 26, 27, 18, 49, 50, 12, 21, 13, 22, 51, 52, 25, 53, 54, 55, 29,30,  0],[14, 15, 53, 56, 18, 49, 50, 18, 26, 27, 57, 58, 59, 22, 51, 52, 55, 29,0,  0],[37, 63, 64, 65, 66, 55, 13, 22, 61, 51, 52, 25, 67, 68, 69, 70, 71, 13,0,  0],[37, 38, 39,  7,  8, 40, 41, 42, 43, 44, 45, 46, 47, 48,  0,  0,  0,  0,0,  0],[16, 17, 18, 53, 56, 12, 59, 60, 22, 61, 51, 52, 12, 62,  0,  0,  0,  0,0,  0],[ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13,  0,  0,  0,  0,  0,0,  0],[31, 32, 24, 33, 34, 35, 36, 13, 30,  0,  0,  0,  0,  0,  0,  0,  0,  0,0,  0]])
sequence_features:tensor([[[ 0.5118,  0.0895, -0.2030,  ..., -0.2605, -0.2138, -0.0192],[ 0.1473, -0.0844, -0.1976,  ..., -0.0260, -0.1921,  0.0378],[-0.2201,  0.0790, -0.0173,  ...,  0.1551, -0.0899,  0.2035],...,[-0.2387,  0.4015, -0.1882,  ..., -0.0473, -0.0399, -0.2642],[ 0.1203,  0.2065,  0.0764,  ...,  0.1412, -0.0817,  0.1800],[ 0.0362,  0.1477, -0.0596,  ...,  0.1640, -0.0790,  0.0359]],[[ 0.1481, -0.0057, -0.1339,  ...,  0.0348, -0.1515,  0.0797],[ 0.1469,  0.0430, -0.1578,  ..., -0.0599, -0.1647,  0.2721],[-0.1601,  0.2572,  0.0821,  ...,  0.0455, -0.0430,  0.2123],...,[-0.0230,  0.3032, -0.2572,  ..., -0.1670, -0.0009, -0.1256],[-0.0643,  0.1889,  0.0266,  ..., -0.1044, -0.2333,  0.1548],[ 0.1969,  0.4262, -0.0194,  ...,  0.1344,  0.0094, -0.0583]],[[ 0.2893, -0.0850, -0.1214,  ...,  0.0855,  0.0234,  0.0684],[-0.0185,  0.0532, -0.1170,  ...,  0.2265, -0.0688,  0.2116],[-0.0882, -0.0393, -0.0658,  ...,  0.0006, -0.1219,  0.1954],...,[ 0.0035,  0.0627, -0.1165,  ..., -0.1742, -0.1552, -0.0772],[-0.1099,  0.2375, -0.0568,  ..., -0.0636, -0.1998,  0.1747],[ 0.1005,  0.3047, -0.0009,  ...,  0.1359, -0.0076, -0.1088]],...,[[ 0.3587,  0.0157, -0.1612,  ...,  0.0327, -0.3009, -0.2104],[ 0.2939, -0.1935, -0.1481,  ...,  0.0349, -0.1136,  0.0226],[ 0.1832, -0.0890, -0.3369,  ...,  0.0113, -0.1601, -0.1295],...,[ 0.1462,  0.0905, -0.1082,  ...,  0.1253, -0.0416, -0.0082],[ 0.2161,  0.0444,  0.0300,  ...,  0.2624, -0.0970,  0.0016],[-0.0896, -0.0905, -0.1790,  ...,  0.0711, -0.0477, -0.1236]],[[ 0.2954,  0.0616, -0.0810,  ..., -0.0213, -0.1283, -0.1051],[-0.0038, -0.1580, -0.0555,  ..., -0.1327, -0.1139,  0.2161],[ 0.1022,  0.1964, -0.1896,  ..., -0.1081, -0.1491, -0.1872],...,[ 0.3404, -0.0456, -0.2569,  ...,  0.0701, -0.1644, -0.0731],[ 0.4573,  0.1885, -0.0779,  ...,  0.1605, -0.1966, -0.0589],[ 0.1448, -0.1581, -0.3021,  ...,  0.0837, -0.0334, -0.2364]],[[ 0.3556,  0.0299, -0.1570,  ...,  0.0512, -0.3286, -0.2882],[ 0.2074, -0.1521, -0.1487,  ...,  0.0637, -0.2674, -0.0174],[ 0.0976, -0.0754, -0.2779,  ..., -0.1588, -0.2096, -0.3432],...,[ 0.4961,  0.0583, -0.2965,  ...,  0.0363, -0.2933, -0.1551],[ 0.4594,  0.3354, -0.0093,  ...,  0.1681, -0.2508, -0.1423],[ 0.0957, -0.0486, -0.2616,  ...,  0.0578, -0.0737, -0.2259]]],grad_fn=<AddBackward0>)
  • 第三步: 计算损失函数第一项的分值
# 若干辅助函数, 在类BiLSTM外部定义, 目的是辅助log_sum_exp()函数的计算
# 将Variable类型变量内部的真实值, 以python float类型返回
def to_scalar(var): # var是Variable, 维度是1# 返回一个python float类型的值return var.view(-1).data.tolist()[0]# 获取最大值的下标
def argmax(vec):# 返回列的维度上的最大值下标, 此下标是一个标量float_, idx = torch.max(vec, 1)return to_scalar(idx)# 辅助完成损失函数中的公式计算
def log_sum_exp(vec): # vec是1 * 7, type是Variablemax_score = vec[0, argmax(vec)]#max_score维度是1, max_score.view(1,-1)维度是1 * 1, max_score.view(1, -1).expand(1, vec.size()[1])的维度1 * 7# 经过expand()之后的张量, 里面所有的值都相同, 都是最大值max_scoremax_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1]) # vec.size()维度是1 * 7# 先减去max_score,最后再加上max_score, 是为了防止数值爆炸, 纯粹是代码上的小技巧return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))# 计算损失函数第一项的分值函数, 本质上是发射矩阵和转移矩阵的累加和
def _forward_alg(self, feats):# 初始化一个alphas张量, 代表转移矩阵的起始位置init_alphas = torch.full((1, self.tagset_size), -10000.)# init_alphas: [1, 7] , [-10000, -10000, -10000, -10000, -10000, -10000, -10000]# 仅仅把START_TAG赋值为0, 代表着接下来的转移只能从START_TAG开始init_alphas[0][self.tag_to_ix[START_TAG]] = 0.# 前向计算变量的赋值, 这样在反向求导的过程中就可以自动更新参数forward_var = init_alphas# 输入进来的feats: [20, 8, 7], 为了接下来按句子进行计算, 要将batch_size放在第一个维度上feats = feats.transpose(1, 0)# feats: [8, 20, 7]是一个3维矩阵, 最外层代表8个句子, 内层代表每个句子有20个字符, # 每一个字符映射成7个标签的发射概率# 初始化最终的结果张量, 每个句子对应一个分数result = torch.zeros((1, self.batch_size))idx = 0# 按行遍历, 总共循环batch_size次for feat_line in feats:# 遍历一行语句, 每一个feat代表一个time_stepfor feat in feat_line:# 当前time_step的一个forward tensorsalphas_t = []# 在当前time_step, 遍历所有可能的转移标签, 进行累加计算for next_tag in range(self.tagset_size):# 广播发射矩阵的分数emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)# 第i个time_step循环时, 转移到next_tag标签的转移概率trans_score = self.transitions[next_tag].view(1, -1)# 将前向矩阵, 转移矩阵, 发射矩阵累加next_tag_var = forward_var + trans_score + emit_score# 计算log_sum_exp()函数值# a = log_sum_exp(next_tag_var), 注意: log_sum_exp()函数仅仅返回一个实数值# print(a.shape) : tensor(1.0975) , ([])# b = a.view(1) : tensor([1.0975]), 注意: a.view(1)的操作就是将一个数字变成一个一阶矩阵, ([]) -> ([1])# print(b.shape) : ([1])alphas_t.append(log_sum_exp(next_tag_var).view(1))# print(alphas_t) : [tensor([337.6004], grad_fn=<ViewBackward>), tensor([337.0469], grad_fn=<ViewBackward>), tensor([337.8497], grad_fn=<ViewBackward>), tensor([337.8668], grad_fn=<ViewBackward>), tensor([338.0186], grad_fn=<ViewBackward>), tensor([-9662.2734], grad_fn=<ViewBackward>), tensor([337.8692], grad_fn=<ViewBackward>)]# temp = torch.cat(alphas_t)# print(temp) : tensor([[  337.6004,   337.0469,   337.8497,   337.8668,   338.0186, -9662.2734, 337.8692]])# 将列表张量转变为二维张量forward_var = torch.cat(alphas_t).view(1, -1)# print(forward_var.shape) : [1, 7]# print(forward_var) : tensor([[   13.7928,    16.0067,    14.1092, -9984.7852,    15.8380]])# 添加最后一步转移到"STOP_TAG"的分数, 就完成了整条语句的分数计算terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]# print(terminal_var) : tensor([[  339.2167,   340.8612,   340.2773,   339.0194,   340.8908, -9659.5732, -9660.0527]])# 计算log_sum_exp()函数值, 作为一条样本语句的最终得分alpha = log_sum_exp(terminal_var)# print(alpha) : tensor(341.9394)# 将得分添加进结果列表中, 作为函数结果返回result[0][idx] = alphaidx += 1return result
  • 输入参数:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1
# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = ["确诊弥漫大b细胞淋巴瘤1年","反复咳嗽、咳痰40年,再发伴气促5天。","生长发育迟缓9年。","右侧小细胞肺癌第三次化疗入院","反复气促、心悸10年,加重伴胸痛3天。","反复胸闷、心悸、气促2多月,加重3天","咳嗽、胸闷1月余, 加重1周","右上肢无力3年, 加重伴肌肉萎缩半年"
]
  • 调用:
if __name__ == '__main__':for sentence in sentence_list:for _char in sentence:if _char not in char_to_id:char_to_id[_char] = len(char_to_id)sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \sequence_length=SENTENCE_LENGTH)for epoch in range(1):model.zero_grad()feats = model._get_lstm_features(sentence_sequence)forward_score = model._forward_alg(feats)print(forward_score)
  • 输出效果:
tensor([[ 44.0279,  87.6439, 132.7635, 176.7535, 221.1325, 265.4456, 309.8346,355.9332]], grad_fn=<CopySlices>)
  • 第四步: 计算损失函数第二项的分值
def _score_sentence(self, feats, tags):# feats: [20, 8, 7] , tags: [8, 20]# 初始化一个0值的tensor, 为后续累加做准备score = torch.zeros(1)# 将START_TAG和真实标签tags做列维度上的拼接temp = torch.tensor(torch.full((self.batch_size, 1), self.tag_to_ix[START_TAG]), dtype=torch.long)tags = torch.cat((temp, tags), dim=1)# 将传入的feats形状转变为[bathc_size, sequence_length, tagset_size]feats = feats.transpose(1, 0)# feats: [8, 20, 7]idx = 0# 初始化最终的结果分数张量, 每一个句子得到一个分数result = torch.zeros((1, self.batch_size))for feat_line in feats:# 注意: 此处区别于第三步的循环, 最重要的是这是在真实标签指导下的转移矩阵和发射矩阵的累加分数for i, feat in enumerate(feat_line):score = score + self.transitions[tags[idx][i + 1], tags[idx][i]] + feat[tags[idx][i + 1]]# 最后加上转移到STOP_TAG的分数score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]]result[0][idx] = scoreidx += 1score = torch.zeros(1)return result
  • 输入参数:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = ["确诊弥漫大b细胞淋巴瘤1年","反复咳嗽、咳痰40年,再发伴气促5天。","生长发育迟缓9年。","右侧小细胞肺癌第三次化疗入院","反复气促、心悸10年,加重伴胸痛3天。","反复胸闷、心悸、气促2多月,加重3天","咳嗽、胸闷1月余, 加重1周","右上肢无力3年, 加重伴肌肉萎缩半年"
]# 真实标签数据, 对应为tag_to_ix中的数字标签
tag_list = [[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],[0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],[0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],[0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 将标签转为标量tags
tags = torch.tensor(tag_list, dtype=torch.long)
  • 调用:
if __name__ == '__main__':for sentence in sentence_list:for _char in sentence:if _char not in char_to_id:char_to_id[_char] = len(char_to_id)sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \sequence_length=SENTENCE_LENGTH)for epoch in range(1):model.zero_grad()feats = model._get_lstm_features(sentence_sequence)gold_score = model._score_sentence(feats, tags)print(gold_score)
  • 输出效果:
tensor([[ 5.3102,  9.0228, 14.7486, 19.5984, 32.4324, 37.9789, 57.8647, 66.8853]],grad_fn=<CopySlices>)
  • 第五步: 维特比算法的实现
# 根据传入的语句特征feats, 推断出标签序列
def _viterbi_decode(self, feats):# 初始化最佳路径结果的存放列表result_best_path = []# 将输入张量变形为[batch_size, sequence_length, tagset_size]feats = feats.transpose(1, 0)# 对批次中的每一行语句进行遍历, 每个语句产生一个最优标注序列for feat_line in feats:backpointers = []# 初始化前向传播的张量, 设置START_TAG等于0, 约束合法序列只能从START_TAG开始init_vvars = torch.full((1, self.tagset_size), -10000.)init_vvars[0][self.tag_to_ix[START_TAG]] = 0# 在第i个time_step, 张量forward_var保存第i-1个time_step的viterbi变量forward_var = init_vvars# 依次遍历i=0, 到序列最后的每一个time_stepfor feat in feat_line:# 保存当前time_step的回溯指针bptrs_t = []# 保存当前time_step的viterbi变量viterbivars_t = []for next_tag in range(self.tagset_size):# next_tag_var[i]保存了tag_i 在前一个time_step的viterbi变量# 前向传播张量forward_var加上从tag_i转移到next_tag的分数, 赋值给next_tag_var# 注意此处没有加发射矩阵分数, 因为求最大值不需要发射矩阵next_tag_var = forward_var + self.transitions[next_tag]# 将最大的标签id加入到当前time_step的回溯列表中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))# 此处再将发射矩阵分数feat加上, 赋值给forward_var, 作为下一个time_step的前向传播张量forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)# 当前time_step的回溯指针添加进当前这一行样本的总体回溯指针中backpointers.append(bptrs_t)# 最后加上转移到STOP_TAG的分数terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]best_tag_id = argmax(terminal_var)# path_score是整个路径的总得分path_score = terminal_var[0][best_tag_id]# 根据回溯指针, 解码最佳路径# 首先把最后一步的id值加入best_path = [best_tag_id]# 从后向前回溯最佳路径for bptrs_t in reversed(backpointers):# 通过第i个time_step得到的最佳id, 找到第i-1个time_step的最佳idbest_tag_id = bptrs_t[best_tag_id]best_path.append(best_tag_id)# 将START_TAG删除start = best_path.pop()# 确认一下最佳路径中的第一个标签是START_TAGassert start == self.tag_to_ix[START_TAG]# 因为是从后向前回溯, 所以再次逆序得到总前向后的真实路径best_path.reverse()# 当前这一行的样本结果添加到最终的结果列表里result_best_path.append(best_path)return result_best_path
  • 输入参数:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = ["确诊弥漫大b细胞淋巴瘤1年","反复咳嗽、咳痰40年,再发伴气促5天。","生长发育迟缓9年。","右侧小细胞肺癌第三次化疗入院","反复气促、心悸10年,加重伴胸痛3天。","反复胸闷、心悸、气促2多月,加重3天","咳嗽、胸闷1月余, 加重1周","右上肢无力3年, 加重伴肌肉萎缩半年"
]# 真实标签数据, 对应为tag_to_ix中的数字标签
tag_list = [[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],[0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],[0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],[0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 将标签转为标量tags
tags = torch.tensor(tag_list, dtype=torch.long)
  • 调用:
if __name__ == '__main__':for sentence in sentence_list:for _char in sentence:if _char not in char_to_id:char_to_id[_char] = len(char_to_id)sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \sequence_length=SENTENCE_LENGTH)for epoch in range(1):model.zero_grad()feats = model._get_lstm_features(sentence_sequence)result_tags = model._viterbi_decode(feats)print(result_tags)
  • 输出效果:
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]]
  • 第六步: 完善BiLSTM类的全部功能
# 对数似然函数的计算, 输入的是数字化编码后的语句, 和真实的标签
# 注意: 这个函数是未来真实训练中要用到的"虚拟化的forward()"
def neg_log_likelihood(self, sentence, tags):# 第一步先得到BiLSTM层的输出特征张量feats = self._get_lstm_features(sentence)# feats : [20, 8, 7] 代表一个批次有8个样本, 每个样本长度20# 每一个word映射到7个标签的概率, 发射矩阵# forward_score 代表公式推导中损失函数loss的第一项forward_score = self._forward_alg(feats)# gold_score 代表公式推导中损失函数loss的第二项gold_score = self._score_sentence(feats, tags)# 按行求和, 在torch.sum()函数值中, 需要设置dim=1 ; 同理, dim=0代表按列求和# 注意: 在这里, 通过forward_score和gold_score的差值来作为loss, 用来梯度下降训练模型return torch.sum(forward_score - gold_score, dim=1)# 此处的forward()真实场景是用在预测部分, 训练的时候并没有用到
def forward(self, sentence):# 获取从BiLSTM层得到的发射矩阵lstm_feats = self._get_lstm_features(sentence)# 通过维特比算法直接解码最佳路径tag_seq = self._viterbi_decode(lstm_feats)return tag_seq
  •  输入参数:
START_TAG = "<START>"
STOP_TAG = "<STOP>"
# 标签和序号的对应码表
tag_to_ix = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, START_TAG: 5, STOP_TAG: 6}
# 词嵌入的维度
EMBEDDING_DIM = 200
# 隐藏层神经元的数量
HIDDEN_DIM = 100
# 批次的大小
BATCH_SIZE = 8
# 设置最大语句限制长度
SENTENCE_LENGTH = 20
# 默认神经网络的层数
NUM_LAYERS = 1# 初始化的示例语句, 共8行, 可以理解为当前批次batch_size=8
sentence_list = ["确诊弥漫大b细胞淋巴瘤1年","反复咳嗽、咳痰40年,再发伴气促5天。","生长发育迟缓9年。","右侧小细胞肺癌第三次化疗入院","反复气促、心悸10年,加重伴胸痛3天。","反复胸闷、心悸、气促2多月,加重3天","咳嗽、胸闷1月余, 加重1周","右上肢无力3年, 加重伴肌肉萎缩半年"
]# 真实标签数据, 对应为tag_to_ix中的数字标签
tag_list = [[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0],[0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0],[0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],[0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
# 将标签转为标量tags
tags = torch.tensor(tag_list, dtype=torch.long)
  • 调用:
if __name__ == '__main__':for sentence in sentence_list:for _char in sentence:if _char not in char_to_id:char_to_id[_char] = len(char_to_id)sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)model = BiLSTM_CRF(vocab_size=len(char_to_id), tag_to_ix=tag_to_ix, embedding_dim=EMBEDDING_DIM, \hidden_dim=HIDDEN_DIM, num_layers=NUM_LAYERS, batch_size=BATCH_SIZE, \sequence_length=SENTENCE_LENGTH)optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)for epoch in range(1):model.zero_grad()loss = model.neg_log_likelihood(sentence_sequence, tags)print(loss)loss.backward()optimizer.step()result = model(sentence_sequence)print(result)
  • 输出效果:
tensor([2347.2678], grad_fn=<SumBackward1>)
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

6.5 模型训练

  • 学习目标:
    • 掌握数据的预处理流程
    • 掌握生成批量训练数据的方法
    • 掌握模型训练代码
  • 模型训练的流程
    • 第一步: 熟悉字符到数字编码的码表
    • 第二步: 熟悉训练数据集的样式和含义解释
    • 第三步: 生成批量训练数据
    • 第四步: 完成准确率和召回率的评估代码
    • 第五步: 完成训练模型的代码
    • 第六步: 绘制损失曲线和评估曲线图
  • 第一步: 熟悉字符到数字编码的码表.
# 代表了数据集中所有字符到数字编码的字典映射
# 码表可以包含中文简体、繁体、英文大小写字母、数字、中英文标点符号等等
# <PAD>为填充标识, 训练时需要将句子转化成矩阵, 而句子长短不一, 需要做padding处理{"<PAD>": 0,"厑": 1,"吖": 2,"呵": 3,"啊": 4,"嗄": 5,"嬶": 6,...
}
  •  第二步: 熟悉训练数据集的样式和含义解释.
荨   B-dis
麻   I-dis
疹   I-dis
这   O
么   O
痒   O
咋   O
办   O
。   O突   O
然   O
头   B-sym
晕   I-sym
呕   B-sym
吐   I-sym
。   O
  • 训练数据集的含义解释:
    • 每一行包含一个字以及与之对应的标签, 字与标签之间通过\t分隔
    • 句子与句子之间通过空行分隔
    • 标签说明:
      • B-dis: 疾病实体名词起始标识
      • I-dis: 疾病实体名词中间到结尾标识
      • B-sym: 症状实体名词起始标识
      • I-sym: 症状实体名词中间到结尾标识
      • O: 其他非实体部分标识
  • 将训练数据集转换为数字化编码集:
# 导入包
import json
import numpy as np# 创建训练数据集, 从原始训练文件中将中文字符进行数字编码, 并将标签页进行数字编码
def create_train_data(train_data_file, result_file, json_file, tag2id, max_length=20):# 导入json格式的中文字符到id的映射表char2id = json.load(open(json_file, mode='r', encoding='utf-8'))char_data, tag_data = [], []# 打开原始训练文件with open(train_data_file, mode='r', encoding='utf-8') as f:# 初始化一条语句数字化编码后的列表char_ids = [0] * max_lengthtag_ids = [0] * max_lengthidx = 0for line in f.readlines():line = line.strip('\n').strip()# 如果不是空行, 并且当前语句长度没有超过max_length, 则进行字符到id的映射if len(line) > 0 and line and idx < max_length:ch, tag = line.split('\t')# 如果当前字符存在于映射表中, 则直接映射为对应的id值if char2id.get(ch):char_ids[idx] = char2id[ch]# 否则直接用"UNK"的id值来代替这个未知字符else:char_ids[idx] = char2id['UNK']# 将标签也进行对应的转换tag_ids[idx] = tag2id[tag]idx += 1# 如果是空行, 或者当前语句长度超过max_lengthelse:# 如果当前语句长度超过max_length, 直接将[0: max_langth]的部分作为结果if idx <= max_length:char_data.append(char_ids)tag_data.append(tag_ids)# 遇到空行, 说明当前句子已经结束, 初始化清零, 为下一个句子的映射做准备char_ids = [0] * max_lengthtag_ids = [0] * max_lengthidx = 0# 将数字化编码后的数据封装成numpy的数组类型, 数字编码采用np.int32x_data = np.array(char_data, dtype=np.int32)y_data = np.array(tag_data, dtype=np.int32)# 直接利用np.savez()将数据存储为.npz类型的文件np.savez(result_file, x_data=x_data, y_data=y_data)print("create_train_data Finished!".center(100, "-"))
  • 输入参数:
# 参数1:字符码表文件路
json_file = './data/char_to_id.json'# 参数2:标签码表对照字典
tag2id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}# 参数3:训练数据文件路径
train_data_file = './data/train.txt'# 参数4:创建的npz文件保路径(训练数据)
result_file = './data/train.npz'
  • 调用:
if __name__ == '__main__':create_train_data(train_data_file, result_file, json_file, tag2id)
  • 第三步: 生成批量训练数据.
# 导入相关的包
import numpy as np
import torch
import torch.utils.data as Data# 生成批量训练数据
def load_dataset(data_file, batch_size):# 将第二步生成的train.npz文件导入内存data = np.load(data_file)# 分别取出特征值和标签x_data = data['x_data']y_data = data['y_data']# 将数据封装成tensor张量x = torch.tensor(x_data, dtype=torch.long)y = torch.tensor(y_data, dtype=torch.long)# 将数据封装成Tensor数据集dataset = Data.TensorDataset(x, y)total_length = len(dataset)# 采用80%的数据作为训练集, 20%的数据作为测试集train_length = int(total_length * 0.8)validation_length = total_length - train_length# 利用Data.random_split()直接切分集合, 按照80%, 20%的比例划分train_dataset, validation_dataset = Data.random_split(dataset=dataset,lengths=[train_length, validation_length])# 将训练集进行DataLoader封装# 参数说明如下:# dataset:     训练数据集# batch_size:  代表批次大小, 若数据集总样本数量无法被batch_size整除, 则最后一批数据为余数#              若设置drop_last为True, 则自动抹去最后不能被整除的剩余批次# shuffle:     是否每个批次为随机抽取, 若为True, 则每次迭代时数据为随机抽取# num_workers: 设定有多少子进程用来做数据加载, 默认为0, 即数据将被加载到主进程中# drop_last:   是否去除不能被整除后的最后批次, 若为True, 则不生成最后不能被整除剩余的数据内容#              例如: dataset长度为1028, batch_size为8,#              若drop_last=True, 则最后剩余的4(1028/8=128余4)条数据将被抛弃不用train_loader = Data.DataLoader(dataset=train_dataset, batch_size=batch_size,shuffle=True, num_workers=4, drop_last=True)validation_loader = Data.DataLoader(dataset=validation_dataset, batch_size=batch_size,shuffle=True, num_workers=4, drop_last=True)# 将两个数据生成器封装为一个字典类型data_loaders = {'train': train_loader, 'validation': validation_loader}# 将两个数据集的长度也封装为一个字典类型data_size = {'train': train_length, 'validation': validation_length}return data_loaders, data_size
  • 输入参数:
# 批次大小
BATCH_SIZE = 8# 编码后的训练数据文件路径
DATA_FILE = './data/train.npz'
  • 调用:
if __name__ == '__main__':data_loader, data_size = load_dataset(DATA_FILE, BATCH_SIZE)print('data_loader:', data_loader, '\ndata_size:', data_size)
  • 输出效果:
data_loader: {'train': <torch.utils.data.dataloader.DataLoader object at 0x7f29eaafb3d0>, 'validation': <torch.utils.data.dataloader.DataLoader object at 0x7f29eaafb5d0>} 
data_size: {'train': 5368, 'validation': 1343}
  • 第四步: 完成准确率和召回率的评估代码.
# 评估模型的准确率, 召回率, F1, 等指标
def evaluate(sentence_list, true_tag, predict_tag, id2char, id2tag):'''sentence_list: 文本向量化后的句子向量列表true_tag:      真实的标签predict_tag:   模型预测的标签id2char:       id值到中文字符的映射表id2tag:        id值到标签的映射表'''# 初始化真实的命名实体, 预测的命名实体, 接下来比较两者来评估各项指标true_entities, true_entity = [], []predict_entities, predict_entity = [], []# 逐条遍历批次中所有的语句for line_num, sentence in enumerate(sentence_list):# 遍历一条样本语句中的每一个字符编码(这里面是数字化编码)for char_num in range(len(sentence)):# 编码为0, 表示后面都是填充的0, 可以结束for循环if sentence[char_num]==0:break# 依次取出真实的样本字符, 真实的标签, 预测的标签char_text = id2char[sentence[char_num]]true_tag_type = id2tag[true_tag[line_num][char_num]]predict_tag_type = id2tag[predict_tag[line_num][char_num]]# 对真实标签进行命名实体的匹配# 如果第一个字符是"B", 表示一个实体的开始, 将"字符/标签"的格式添加进实体列表中if true_tag_type[0] == "B":true_entity = [char_text + "/" + true_tag_type]# 如果第一个字符是"I", 表示处于一个实体的中间# 如果真实命名实体列表非空, 并且最后一个添加进去的标签类型和当前的标签类型一样, 则继续添加# 意思就是比如true_entity = ["中/B-Person", "国/I-Person"], 此时的"人/I-Person"就可以添加进去, 因为都属于同一个命名实体elif true_tag_type[0] == "I" and len(true_entity) != 0 and true_entity[-1].split("/")[1][1:] == true_tag_type[1:]:true_entity.append(char_text + "/" + true_tag_type)# 如果第一个字符是"O", 并且true_entity非空, 表示一个命名实体的匹配结束了elif true_tag_type[0] == "O" and len(true_entity) != 0 :# 最后增加进去一个"行号_列号", 作为区分实体的标志true_entity.append(str(line_num) + "_" + str(char_num))# 将这个匹配出来的实体加入到结果列表中true_entities.append(true_entity)# 清空true_entity, 为下一个命名实体的匹配做准备true_entity=[]# 除了上面三种情况, 说明当前没有匹配出任何命名实体, 则清空true_entity, 继续下一次匹配else:true_entity=[]# 对预测标签进行命名实体的匹配# 如果第一个字符是"B", 表示一个实体的开始, 将"字符/预测标签"的格式添加进实体列表中if predict_tag_type[0] == "B":predict_entity = [char_text + "/" + predict_tag_type]# 如果第一个字符是"I", 表示处于一个实体的中间# 如果预测命名实体列表非空, 并且最后一个添加进去的标签类型和当前的标签类型一样, 则继续添加# 意思就是比如predict_entity = ["中/B-Person", "国/I-Person"], 此时的"人/I-Person"就可以添>加进去, 因为都属于同一个命名实体elif predict_tag_type[0] == "I" and len(predict_entity) != 0 and predict_entity[-1].split("/")[1][1:] == predict_tag_type[1:]:predict_entity.append(char_text + "/" + predict_tag_type)# 如果第一个字符是"O", 并且predict_entity非空, 表示一个命名实体的匹配结束了elif predict_tag_type[0] == "O" and len(predict_entity) != 0:# 最后增加进去一个"行号_列号", 作为区分实体的标志predict_entity.append(str(line_num) + "_" + str(char_num))# 将这个匹配出来的实体加入到结果列表中predict_entities.append(predict_entity)# 清空predict_entity, 为下一个命名实体的匹配做准备predict_entity = []# 除了上面三种情况, 说明当前没有匹配出任何命名实体, 则清空predict_entity, 继续下一次匹配else:predict_entity = []# 遍历所有预测实体的列表, 只有那些在真实命名实体中的才是正确的acc_entities = [entity for entity in predict_entities if entity in true_entities]# 计算正确实体的个数, 预测实体的总个数, 真实实体的总个数acc_entities_length = len(acc_entities)predict_entities_length = len(predict_entities)true_entities_length = len(true_entities)# 至少正确预测了一个, 才计算3个指标, 准确率if acc_entities_length > 0:accuracy = float(acc_entities_length / predict_entities_length)recall = float(acc_entities_length / true_entities_length)f1_score = 2 * accuracy * recall / (accuracy + recall)return accuracy, recall, f1_score, acc_entities_length, predict_entities_length, true_entities_lengthelse:return 0, 0, 0, acc_entities_length, predict_entities_length, true_entities_length 
  • 输入参数:
# 真实标签数据
tag_list = [[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],[0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],[0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],[0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]# 预测标签数据
predict_tag_list = [[0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],[0, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0],[0, 0, 3, 4, 0, 3, 4, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[3, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0],[0, 0, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0],[3, 4, 0, 3, 4, 0, 0, 1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0],[0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],[0, 0, 3, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]# 编码与字符对照字典
id2char = {0: '<PAD>', 1: '确', 2: '诊', 3: '弥', 4: '漫', 5: '大', 6: 'b', 7: '细', 8: '胞', 9: '淋', 10: '巴', 11: '瘤', 12: '1', 13: '年', 14: '反', 15: '复', 16: '咳', 17: '嗽', 18: '、', 19: '痰', 20: '4', 21: '0', 22: ',', 23: '再', 24: '发', 25: '伴', 26: '气', 27: '促', 28: '5', 29: '天', 30: '。', 31: '生', 32: '长', 33: '育', 34: '迟', 35: '缓', 36: '9', 37: '右', 38: '侧', 39: '小', 40: '肺', 41: '癌', 42: '第', 43: '三', 44: '次', 45: '化', 46: '疗', 47: '入', 48: '院', 49: '心', 50: '悸', 51: '加', 52: '重', 53: '胸', 54: '痛', 55: '3', 56: '闷', 57: '2', 58: '多', 59: '月', 60: '余', 61: ' ', 62: '周', 63: '上', 64: '肢', 65: '无', 66: '力', 67: '肌', 68: '肉', 69: '萎', 70: '缩', 71: '半'}# 编码与标签对照字典
id2tag = {0: 'O', 1: 'B-dis', 2: 'I-dis', 3: 'B-sym', 4: 'I-sym'}# 输入的数字化sentences_sequence, 由下面的sentence_list经过映射函数sentence_map()转化后得到
sentence_list = ["确诊弥漫大b细胞淋巴瘤1年","反复咳嗽、咳痰40年,再发伴气促5天。","生长发育迟缓9年。","右侧小细胞肺癌第三次化疗入院","反复气促、心悸10年,加重伴胸痛3天。","反复胸闷、心悸、气促2多月,加重3天","咳嗽、胸闷1月余, 加重1周","右上肢无力3年, 加重伴肌肉萎缩半年"
]
  • 调用:
def sentence_map(sentence_list, char_to_id, max_length):sentence_list.sort(key=lambda c:len(c), reverse=True)sentence_map_list = []for sentence in sentence_list:sentence_id_list = [char_to_id[c] for c in sentence]padding_list = [0] * (max_length-len(sentence))sentence_id_list.extend(padding_list)sentence_map_list.append(sentence_id_list)return torch.tensor(sentence_map_list, dtype=torch.long)char_to_id = {"<PAD>":0}SENTENCE_LENGTH = 20for sentence in sentence_list:for _char in sentence:if _char not in char_to_id:char_to_id[_char] = len(char_to_id)sentences_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)if __name__ == '__main__':accuracy, recall, f1_score, acc_entities_length, predict_entities_length, true_entities_length = evaluate(sentences_sequence.tolist(), tag_list, predict_tag_list, id2char, id2tag)print("accuracy:",                  accuracy,"\nrecall:",                  recall,"\nf1_score:",                f1_score,"\nacc_entities_length:",     acc_entities_length,"\npredict_entities_length:", predict_entities_length,"\ntrue_entities_length:",    true_entities_length)
  • 输出效果:
step_acc: 0.8823529411764706 
step_recall: 0.9375 
f1_score: 0.9090909090909091 
acc_entities_length: 15 
predict_entities_length: 17 
true_entities_length: 16
  • 第五步: 完成训练模型的代码.
# 导入包
import json
import time
from tqdm import tqdm
import matplotlib.pyplot as plt
import torch
import torch.optim as optim
from torch.autograd import Variable
# 导入之前编写好的包, 包括类, 数据集加载, 评估函数
from bilstm_crf import BiLSTM_CRF
from loader_data import load_dataset
from evaluate_model import evaluate# 训练模型的函数
def train(data_loader, data_size, batch_size, embedding_dim, hidden_dim,sentence_length, num_layers, epochs, learning_rate, tag2id,model_saved_path, train_log_path,validate_log_path, train_history_image_path):'''data_loader: 数据集的加载器, 之前已经通过load_dataset完成了构造data_size:   训练集和测试集的样本数量batch_size:  批次的样本个数embedding_dim:  词嵌入的维度hidden_dim:     隐藏层的维度sentence_length:  文本限制的长度num_layers:       神经网络堆叠的LSTM层数epochs:           训练迭代的轮次learning_rate:    学习率tag2id:           标签到id的映射字典model_saved_path: 模型保存的路径train_log_path:   训练日志保存的路径validate_log_path:  测试集日志保存的路径train_history_image_path:  训练数据的相关图片保存路径'''# 将中文字符和id的对应码表加载进内存char2id = json.load(open("./data/char_to_id.json", mode="r", encoding="utf-8"))# 初始化BiLSTM_CRF模型model = BiLSTM_CRF(vocab_size=len(char2id), tag_to_ix=tag2id,embedding_dim=embedding_dim, hidden_dim=hidden_dim,batch_size=batch_size, num_layers=num_layers,sequence_length=sentence_length)# 定义优化器, 使用SGD作为优化器(pytorch中Embedding支持的GPU加速为SGD, SparseAdam)# 参数说明如下:# lr:          优化器学习率# momentum:    优化下降的动量因子, 加速梯度下降过程optimizer = optim.SGD(params=model.parameters(), lr=learning_rate, momentum=0.85)# 设定优化器学习率更新策略# 参数说明如下:# optimizer:    优化器# step_size:    更新频率, 每过多少个epoch更新一次优化器学习率# gamma:        学习率衰减幅度,#               按照什么比例调整(衰减)学习率(相对于上一轮epoch), 默认0.1#   例如:#   初始学习率 lr = 0.5,    step_size = 20,    gamma = 0.1#              lr = 0.5     if epoch < 20#              lr = 0.05    if 20 <= epoch < 40#              lr = 0.005   if 40 <= epoch < 60scheduler = optim.lr_scheduler.StepLR(optimizer=optimizer, step_size=5, gamma=0.2)# 初始化存放训练中损失, 准确率, 召回率, F1等数值指标train_loss_list = []train_acc_list = []train_recall_list = []train_f1_list = []train_log_file = open(train_log_path, mode="w", encoding="utf-8")# 初始化存放测试中损失, 准确率, 召回率, F1等数值指标validate_loss_list = []validate_acc_list = []validate_recall_list = []validate_f1_list = []validate_log_file = open(validate_log_path, mode="w", encoding="utf-8")# 利用tag2id生成id到tag的映射字典id2tag = {v:k for k, v in tag2id.items()}# 利用char2id生成id到字符的映射字典id2char = {v:k for k, v in char2id.items()}# 按照参数epochs的设定来循环epochs次for epoch in range(epochs):# 在进度条打印前, 先输出当前所执行批次tqdm.write("Epoch {}/{}".format(epoch + 1, epochs))# 定义要记录的正确总实体数, 识别实体数以及真实实体数total_acc_entities_length, \total_predict_entities_length, \total_gold_entities_length = 0, 0, 0# 定义每batch步数, 批次loss总值, 准确度, f1值step, total_loss, correct, f1 = 1, 0.0, 0, 0# 开启当前epochs的训练部分for inputs, labels in tqdm(data_loader["train"]):# 将数据以Variable进行封装inputs, labels = Variable(inputs), Variable(labels)# 在训练模型期间, 要在每个样本计算梯度前将优化器归零, 不然梯度会被累加optimizer.zero_grad()# 此处调用的是BiLSTM_CRF类中的neg_log_likelihood()函数loss = model.neg_log_likelihood(inputs, labels)# 获取当前步的loss, 由tensor转为数字step_loss = loss.data# 累计每步损失值total_loss += step_loss# 获取解码最佳路径列表, 此时调用的是BiLSTM_CRF类中的forward()函数best_path_list = model(inputs)# 模型评估指标值获取包括:当前批次准确率, 召回率, F1值以及对应的实体个数step_acc, step_recall, f1_score, acc_entities_length, \predict_entities_length, gold_entities_length = evaluate(inputs.tolist(),labels.tolist(),best_path_list,id2char,id2tag)# 训练日志内容log_text = "Epoch: %s | Step: %s " \"| loss: %.5f " \"| acc: %.5f " \"| recall: %.5f " \"| f1 score: %.5f" % \(epoch, step, step_loss, step_acc, step_recall,f1_score)# 分别累计正确总实体数、识别实体数以及真实实体数total_acc_entities_length += acc_entities_lengthtotal_predict_entities_length += predict_entities_lengthtotal_gold_entities_length += gold_entities_length# 对损失函数进行反向传播loss.backward()# 通过optimizer.step()计算损失, 梯度和更新参数optimizer.step()# 记录训练日志train_log_file.write(log_text + "\n")step += 1# 获取当前epochs平均损失值(每一轮迭代的损失总值除以总数据量)epoch_loss = total_loss / data_size["train"]# 计算当前epochs准确率total_acc = total_acc_entities_length / total_predict_entities_length# 计算当前epochs召回率total_recall = total_acc_entities_length / total_gold_entities_length# 计算当前epochs的F1值total_f1 = 0if total_acc + total_recall != 0:total_f1 = 2 * total_acc * total_recall / (total_acc + total_recall)log_text = "Epoch: %s " \"| mean loss: %.5f " \"| total acc: %.5f " \"| total recall: %.5f " \"| total f1 scroe: %.5f" % (epoch, epoch_loss,total_acc,total_recall,total_f1)# 当前epochs训练后更新学习率, 必须在优化器更新之后scheduler.step()# 记录当前epochs训练loss值(用于图表展示), 准确率, 召回率, f1值train_loss_list.append(epoch_loss)train_acc_list.append(total_acc)train_recall_list.append(total_recall)train_f1_list.append(total_f1)train_log_file.write(log_text + "\n")# 定义要记录的正确总实体数, 识别实体数以及真实实体数total_acc_entities_length, \total_predict_entities_length, \total_gold_entities_length = 0, 0, 0# 定义每batch步数, 批次loss总值, 准确度, f1值step, total_loss, correct, f1 = 1, 0.0, 0, 0# 开启当前epochs的验证部分with torch.no_grad():# 开启当前epochs的验证部分for inputs, labels in tqdm(data_loader["validation"]):# 将数据以Variable进行封装inputs, labels = Variable(inputs), Variable(labels)# 此处调用的是BiLSTM_CRF类中的neg_log_likelihood 函数# 返回最终的CRF的对数似然结果loss = model.neg_log_likelihood(inputs, labels)# 获取当前步的loss值, 由tensor转为数字step_loss = loss.data# 累计每步损失值total_loss += step_loss# 获取解码最佳路径列表, 此时调用的是BiLSTM_CRF类中的forward()函数best_path_list = model(inputs)# 模型评估指标值获取: 当前批次准确率, 召回率, F1值以及对应的实体个数step_acc, step_recall, f1_score, acc_entities_length, \predict_entities_length, gold_entities_length = evaluate(inputs.tolist(),labels.tolist(),best_path_list,id_to_char,id_to_tag)# 训练日志内容log_text = "Epoch: %s | Step: %s " \"| loss: %.5f " \"| acc: %.5f " \"| recall: %.5f " \"| f1 score: %.5f" % \(epoch, step, step_loss, step_acc, step_recall,f1_score)# 分别累计正确总实体数、识别实体数以及真实实体数total_acc_entities_length += acc_entities_lengthtotal_predict_entities_length += predict_entities_lengthtotal_gold_entities_length += gold_entities_length# 记录验证集损失日志validate_log_file.write(log_text + "\n")step += 1# 获取当前批次平均损失值(每一批次损失总值除以数据量)epoch_loss = total_loss / data_size["validation"]# 计算总批次准确率total_acc = total_acc_entities_length / total_predict_entities_length# 计算总批次召回率total_recall = total_acc_entities_length / total_gold_entities_length# 计算总批次F1值total_f1 = 0if total_acc + total_recall != 0:total_f1 = 2 * total_acc * total_recall / (total_acc + total_recall)log_text = "Epoch: %s " \"| mean loss: %.5f " \"| total acc: %.5f " \"| total recall: %.5f " \"| total f1 scroe: %.5f" % (epoch, epoch_loss,total_acc,total_recall,total_f1)# 记录当前批次验证loss值(用于图表展示)准确率, 召回率, f1值validate_loss_list.append(epoch_loss)validate_acc_list.append(total_acc)validate_recall_list.append(total_recall)validate_f1_list.append(total_f1)validate_log_file.write(log_text + "\n")# 保存模型torch.save(model.state_dict(), model_saved_path)# 将loss下降历史数据转为图片存储save_train_history_image(train_loss_list,validate_loss_list,train_history_image_path,"Loss")# 将准确率提升历史数据转为图片存储save_train_history_image(train_acc_list,validate_acc_list,train_history_image_path,"Acc")# 将召回率提升历史数据转为图片存储save_train_history_image(train_recall_list,validate_recall_list,train_history_image_path,"Recall")# 将F1上升历史数据转为图片存储save_train_history_image(train_f1_list,validate_f1_list,train_history_image_path,"F1")print("train Finished".center(100, "-"))# 按照传入的不同路径, 绘制不同的训练曲线
def save_train_history_image(train_history_list,validate_history_list,history_image_path,data_type):# 根据训练集的数据列表, 绘制折线图plt.plot(train_history_list, label="Train %s History" % (data_type))# 根据测试集的数据列表, 绘制折线图plt.plot(validate_history_list, label="Validate %s History" % (data_type))# 将图片放置在最优位置plt.legend(loc="best")# 设置x轴的图标为轮次Epochsplt.xlabel("Epochs")# 设置y轴的图标为参数data_typeplt.ylabel(data_type)# 将绘制好的图片保存在特定的路径下面, 并修改图片名字中的"plot"为对应的data_typeplt.savefig(history_image_path.replace("plot", data_type))plt.close()
  • 输入参数:
# 参数1:批次大小
BATCH_SIZE = 8
# 参数2:训练数据文件路径
train_data_file_path = "data/train.npz"
# 参数3:加载 DataLoader 数据
data_loader, data_size = load_dataset(train_data_file_path, BATCH_SIZE)
# 参数4:记录当前训练时间(拼成字符串用)
time_str = time.strftime("%Y%m%d_%H%M%S", time.localtime(time.time()))
# 参数5:标签码表对照
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 参数6:训练文件存放路径
model_saved_path = "model/bilstm_crf_state_dict_%s.pt" % (time_str)
# 参数7:训练日志文件存放路径
train_log_path = "log/train_%s.log" % (time_str)
# 参数8:验证打印日志存放路径
validate_log_path = "log/validate_%s.log" % (time_str)
# 参数9:训练历史记录图存放路径
train_history_image_path = "log/bilstm_crf_train_plot_%s.png" % (time_str)
# 参数10:字向量维度
EMBEDDING_DIM = 200
# 参数11:隐层维度
HIDDEN_DIM = 100
# 参数12:句子长度
SENTENCE_LENGTH = 20
# 参数13:堆叠 LSTM 层数
NUM_LAYERS = 1
# 参数14:训练批次
EPOCHS = 100
# 参数15:初始化学习率
LEARNING_RATE = 0.5
  • 调用:
if __name__ == '__main__':train(data_loader, data_size, BATCH_SIZE, EMBEDDING_DIM, HIDDEN_DIM, SENTENCE_LENGTH,NUM_LAYERS, EPOCHS, LEARNING_RATE, tag_to_id,model_saved_path, train_log_path, validate_log_path, train_history_image_path)
  • 输出效果:
    • 模型训练结果文件保存位置:model/bilstm_crf_state_dict_[年月日时分秒时间字符串].pt
    • 模型训练日志文件保存位置:log/train_[年月日时分秒时间字符串].log
    • 模型验证日志文件保存位置:log/validate_[年月日时分秒时间字符串].log
    • 模型训练损失历史记录图片保存位置:log/bilstm_crf_train_Loss_[年月日时分秒时间字符串].png
    • 模型训练准确率历史记录图片保存位置:log/bilstm_crf_train_Acc_[年月日时分秒时间字符串].png
    • 模型训练召回率历史记录图片保存位置:log/bilstm_crf_train_Recall_[年月日时分秒时间字符串].png
    • 模型训练F1值历史记录图片保存位置:log/bilstm_crf_train_F1_[年月日时分秒时间字符串].png
  • 训练日志:
Epoch: 0 | train loss: 366.58832 |acc: 0.632 |recall: 0.503 |f1 score: 0.56 | validate loss: 666.032 |acc: 0.591 |recall: 0.457 |f1 score: 0.515
Epoch: 1 | train loss: 123.87159 |acc: 0.743 |recall: 0.687 |f1 score: 0.714 | validate loss: 185.021 |acc: 0.669 |recall: 0.606 |f1 score: 0.636
Epoch: 2 | train loss: 113.04003 |acc: 0.738 |recall: 0.706 |f1 score: 0.722 | validate loss: 107.393 |acc: 0.711 |recall: 0.663 |f1 score: 0.686
Epoch: 3 | train loss: 119.14317 |acc: 0.751 |recall: 0.692 |f1 score: 0.721 | validate loss: 158.381 |acc: 0.713 |recall: 0.64 |f1 score: 0.674
Epoch: 4 | train loss: 105.81506 |acc: 0.741 |recall: 0.699 |f1 score: 0.72 | validate loss: 118.99 |acc: 0.669 |recall: 0.624 |f1 score: 0.646
Epoch: 5 | train loss: 86.67545 |acc: 0.773 |recall: 0.751 |f1 score: 0.762 | validate loss: 123.636 |acc: 0.64 |recall: 0.718 |f1 score: 0.676
Epoch: 6 | train loss: 79.66924 |acc: 0.808 |recall: 0.772 |f1 score: 0.789 | validate loss: 89.771 |acc: 0.735 |recall: 0.714 |f1 score: 0.724
Epoch: 7 | train loss: 85.35771 |acc: 0.766 |recall: 0.752 |f1 score: 0.759 | validate loss: 141.233 |acc: 0.675 |recall: 0.7 |f1 score: 0.687
Epoch: 8 | train loss: 82.38535 |acc: 0.787 |recall: 0.748 |f1 score: 0.767 | validate loss: 108.429 |acc: 0.717 |recall: 0.673 |f1 score: 0.694
Epoch: 9 | train loss: 82.46296 |acc: 0.783 |recall: 0.751 |f1 score: 0.767 | validate loss: 74.716 |acc: 0.692 |recall: 0.702 |f1 score: 0.697
Epoch: 10 | train loss: 75.12292 |acc: 0.814 |recall: 0.779 |f1 score: 0.796 | validate loss: 90.693 |acc: 0.672 |recall: 0.7 |f1 score: 0.686
Epoch: 11 | train loss: 74.89426 |acc: 0.813 |recall: 0.77 |f1 score: 0.791 | validate loss: 77.161 |acc: 0.729 |recall: 0.718 |f1 score: 0.724
Epoch: 12 | train loss: 76.39055 |acc: 0.814 |recall: 0.785 |f1 score: 0.799 | validate loss: 132.545 |acc: 0.806 |recall: 0.685 |f1 score: 0.74
Epoch: 13 | train loss: 75.01093 |acc: 0.814 |recall: 0.787 |f1 score: 0.8 | validate loss: 101.596 |acc: 0.765 |recall: 0.681 |f1 score: 0.721
Epoch: 14 | train loss: 74.35796 |acc: 0.83 |recall: 0.802 |f1 score: 0.816 | validate loss: 92.535 |acc: 0.745 |recall: 0.777 |f1 score: 0.761
Epoch: 15 | train loss: 73.27102 |acc: 0.818 |recall: 0.791 |f1 score: 0.804 | validate loss: 109.51 |acc: 0.68 |recall: 0.76 |f1 score: 0.717
Epoch: 16 | train loss: 67.66725 |acc: 0.841 |recall: 0.811 |f1 score: 0.826 | validate loss: 93.047 |acc: 0.768 |recall: 0.738 |f1 score: 0.753
Epoch: 17 | train loss: 63.75809 |acc: 0.83 |recall: 0.813 |f1 score: 0.822 | validate loss: 76.231 |acc: 0.784 |recall: 0.776 |f1 score: 0.78
Epoch: 18 | train loss: 60.30417 |acc: 0.845 |recall: 0.829 |f1 score: 0.837 | validate loss: 76.019 |acc: 0.806 |recall: 0.758 |f1 score: 0.781
Epoch: 19 | train loss: 60.30238 |acc: 0.849 |recall: 0.823 |f1 score: 0.836 | validate loss: 90.269 |acc: 0.748 |recall: 0.733 |f1 score: 0.741
Epoch: 20 | train loss: 60.20072 |acc: 0.847 |recall: 0.82 |f1 score: 0.833 | validate loss: 61.756 |acc: 0.81 |recall: 0.77 |f1 score: 0.79
Epoch: 21 | train loss: 58.98606 |acc: 0.844 |recall: 0.82 |f1 score: 0.832 | validate loss: 60.799 |acc: 0.765 |recall: 0.754 |f1 score: 0.759
Epoch: 22 | train loss: 60.23671 |acc: 0.848 |recall: 0.828 |f1 score: 0.838 | validate loss: 65.676 |acc: 0.787 |recall: 0.781 |f1 score: 0.784
Epoch: 23 | train loss: 58.57862 |acc: 0.849 |recall: 0.827 |f1 score: 0.838 | validate loss: 65.975 |acc: 0.794 |recall: 0.754 |f1 score: 0.774
Epoch: 24 | train loss: 58.93968 |acc: 0.848 |recall: 0.827 |f1 score: 0.838 | validate loss: 66.994 |acc: 0.784 |recall: 0.746 |f1 score: 0.764
Epoch: 25 | train loss: 59.91834 |acc: 0.862 |recall: 0.828 |f1 score: 0.845 | validate loss: 68.794 |acc: 0.795 |recall: 0.756 |f1 score: 0.775
Epoch: 26 | train loss: 59.09166 |acc: 0.84 |recall: 0.823 |f1 score: 0.831 | validate loss: 68.508 |acc: 0.746 |recall: 0.758 |f1 score: 0.752
Epoch: 27 | train loss: 58.0584 |acc: 0.856 |recall: 0.84 |f1 score: 0.848 | validate loss: 53.158 |acc: 0.802 |recall: 0.774 |f1 score: 0.788
Epoch: 28 | train loss: 54.2857 |acc: 0.858 |recall: 0.834 |f1 score: 0.845 | validate loss: 60.243 |acc: 0.816 |recall: 0.772 |f1 score: 0.793
Epoch: 29 | train loss: 56.44759 |acc: 0.845 |recall: 0.838 |f1 score: 0.841 | validate loss: 56.497 |acc: 0.768 |recall: 0.77 |f1 score: 0.769
Epoch: 30 | train loss: 57.90492 |acc: 0.868 |recall: 0.832 |f1 score: 0.85 | validate loss: 75.158 |acc: 0.773 |recall: 0.762 |f1 score: 0.768
Epoch: 31 | train loss: 56.81468 |acc: 0.861 |recall: 0.835 |f1 score: 0.847 | validate loss: 56.742 |acc: 0.796 |recall: 0.784 |f1 score: 0.79
Epoch: 32 | train loss: 54.72623 |acc: 0.86 |recall: 0.844 |f1 score: 0.852 | validate loss: 63.175 |acc: 0.757 |recall: 0.78 |f1 score: 0.768
Epoch: 33 | train loss: 60.10299 |acc: 0.846 |recall: 0.813 |f1 score: 0.829 | validate loss: 68.994 |acc: 0.768 |recall: 0.724 |f1 score: 0.745
Epoch: 34 | train loss: 59.67491 |acc: 0.849 |recall: 0.826 |f1 score: 0.837 | validate loss: 58.662 |acc: 0.8 |recall: 0.739 |f1 score: 0.769
Epoch: 35 | train loss: 65.01099 |acc: 0.857 |recall: 0.83 |f1 score: 0.844 | validate loss: 69.299 |acc: 0.772 |recall: 0.752 |f1 score: 0.762
Epoch: 36 | train loss: 61.52783 |acc: 0.856 |recall: 0.828 |f1 score: 0.842 | validate loss: 82.373 |acc: 0.761 |recall: 0.777 |f1 score: 0.769
Epoch: 37 | train loss: 66.19576 |acc: 0.844 |recall: 0.822 |f1 score: 0.833 | validate loss: 79.853 |acc: 0.791 |recall: 0.77 |f1 score: 0.781
Epoch: 38 | train loss: 60.32529 |acc: 0.841 |recall: 0.828 |f1 score: 0.835 | validate loss: 69.346 |acc: 0.773 |recall: 0.755 |f1 score: 0.764
Epoch: 39 | train loss: 63.8836 |acc: 0.837 |recall: 0.819 |f1 score: 0.828 | validate loss: 74.759 |acc: 0.732 |recall: 0.759 |f1 score: 0.745
Epoch: 40 | train loss: 67.28363 |acc: 0.838 |recall: 0.824 |f1 score: 0.831 | validate loss: 63.027 |acc: 0.768 |recall: 0.764 |f1 score: 0.766
Epoch: 41 | train loss: 61.40488 |acc: 0.852 |recall: 0.826 |f1 score: 0.839 | validate loss: 58.976 |acc: 0.802 |recall: 0.755 |f1 score: 0.778
Epoch: 42 | train loss: 61.04982 |acc: 0.856 |recall: 0.817 |f1 score: 0.836 | validate loss: 58.47 |acc: 0.783 |recall: 0.74 |f1 score: 0.761
Epoch: 43 | train loss: 64.40567 |acc: 0.849 |recall: 0.821 |f1 score: 0.835 | validate loss: 63.506 |acc: 0.764 |recall: 0.765 |f1 score: 0.765
Epoch: 44 | train loss: 65.09746 |acc: 0.845 |recall: 0.805 |f1 score: 0.825 | validate loss: 65.535 |acc: 0.773 |recall: 0.743 |f1 score: 0.758
Epoch: 45 | train loss: 63.26585 |acc: 0.848 |recall: 0.808 |f1 score: 0.827 | validate loss: 62.477 |acc: 0.789 |recall: 0.733 |f1 score: 0.76
Epoch: 46 | train loss: 63.91504 |acc: 0.847 |recall: 0.812 |f1 score: 0.829 | validate loss: 59.916 |acc: 0.779 |recall: 0.751 |f1 score: 0.765
Epoch: 47 | train loss: 62.3592 |acc: 0.845 |recall: 0.824 |f1 score: 0.835 | validate loss: 63.363 |acc: 0.775 |recall: 0.761 |f1 score: 0.768
Epoch: 48 | train loss: 63.13221 |acc: 0.843 |recall: 0.823 |f1 score: 0.833 | validate loss: 65.71 |acc: 0.767 |recall: 0.755 |f1 score: 0.761
Epoch: 49 | train loss: 64.9964 |acc: 0.845 |recall: 0.811 |f1 score: 0.828 | validate loss: 65.174 |acc: 0.768 |recall: 0.74 |f1 score: 0.754
Epoch: 50 | train loss: 62.40605 |acc: 0.847 |recall: 0.817 |f1 score: 0.832 | validate loss: 60.761 |acc: 0.776 |recall: 0.746 |f1 score: 0.761
Epoch: 51 | train loss: 63.05476 |acc: 0.845 |recall: 0.812 |f1 score: 0.828 | validate loss: 64.217 |acc: 0.764 |recall: 0.748 |f1 score: 0.756
Epoch: 52 | train loss: 59.77727 |acc: 0.84 |recall: 0.831 |f1 score: 0.836 | validate loss: 60.48 |acc: 0.79 |recall: 0.759 |f1 score: 0.774
Epoch: 53 | train loss: 62.7249 |acc: 0.828 |recall: 0.813 |f1 score: 0.821 | validate loss: 64.584 |acc: 0.757 |recall: 0.757 |f1 score: 0.757
Epoch: 54 | train loss: 61.1763 |acc: 0.842 |recall: 0.832 |f1 score: 0.837 | validate loss: 61.088 |acc: 0.775 |recall: 0.768 |f1 score: 0.771
Epoch: 55 | train loss: 64.04366 |acc: 0.835 |recall: 0.816 |f1 score: 0.826 | validate loss: 68.183 |acc: 0.784 |recall: 0.742 |f1 score: 0.762
Epoch: 56 | train loss: 66.76939 |acc: 0.84 |recall: 0.813 |f1 score: 0.827 | validate loss: 67.284 |acc: 0.77 |recall: 0.748 |f1 score: 0.759
Epoch: 57 | train loss: 67.85329 |acc: 0.826 |recall: 0.789 |f1 score: 0.807 | validate loss: 69.961 |acc: 0.766 |recall: 0.732 |f1 score: 0.749
Epoch: 58 | train loss: 64.79573 |acc: 0.84 |recall: 0.812 |f1 score: 0.826 | validate loss: 73.358 |acc: 0.754 |recall: 0.735 |f1 score: 0.745
Epoch: 59 | train loss: 65.36249 |acc: 0.862 |recall: 0.826 |f1 score: 0.844 | validate loss: 66.552 |acc: 0.783 |recall: 0.766 |f1 score: 0.774
Epoch: 60 | train loss: 63.43061 |acc: 0.835 |recall: 0.811 |f1 score: 0.823 | validate loss: 63.138 |acc: 0.771 |recall: 0.746 |f1 score: 0.759
Epoch: 61 | train loss: 62.34639 |acc: 0.848 |recall: 0.825 |f1 score: 0.836 | validate loss: 59.656 |acc: 0.783 |recall: 0.756 |f1 score: 0.769
Epoch: 62 | train loss: 61.83451 |acc: 0.83 |recall: 0.814 |f1 score: 0.822 | validate loss: 60.443 |acc: 0.765 |recall: 0.751 |f1 score: 0.758
Epoch: 63 | train loss: 64.78461 |acc: 0.854 |recall: 0.818 |f1 score: 0.836 | validate loss: 61.125 |acc: 0.786 |recall: 0.748 |f1 score: 0.767
Epoch: 64 | train loss: 63.43409 |acc: 0.838 |recall: 0.818 |f1 score: 0.828 | validate loss: 62.396 |acc: 0.77 |recall: 0.757 |f1 score: 0.764
Epoch: 65 | train loss: 61.20197 |acc: 0.854 |recall: 0.815 |f1 score: 0.834 | validate loss: 59.019 |acc: 0.79 |recall: 0.75 |f1 score: 0.769
Epoch: 66 | train loss: 59.69791 |acc: 0.851 |recall: 0.82 |f1 score: 0.836 | validate loss: 55.06 |acc: 0.789 |recall: 0.754 |f1 score: 0.771
Epoch: 67 | train loss: 63.16074 |acc: 0.836 |recall: 0.811 |f1 score: 0.823 | validate loss: 61.48 |acc: 0.764 |recall: 0.745 |f1 score: 0.755
Epoch: 68 | train loss: 62.15521 |acc: 0.845 |recall: 0.824 |f1 score: 0.835 | validate loss: 62.407 |acc: 0.778 |recall: 0.761 |f1 score: 0.769
Epoch: 69 | train loss: 61.90574 |acc: 0.847 |recall: 0.828 |f1 score: 0.838 | validate loss: 59.801 |acc: 0.781 |recall: 0.762 |f1 score: 0.771
Epoch: 70 | train loss: 60.51348 |acc: 0.852 |recall: 0.827 |f1 score: 0.839 | validate loss: 56.632 |acc: 0.781 |recall: 0.761 |f1 score: 0.771
Epoch: 71 | train loss: 62.78683 |acc: 0.856 |recall: 0.823 |f1 score: 0.84 | validate loss: 62.867 |acc: 0.796 |recall: 0.757 |f1 score: 0.776
Epoch: 72 | train loss: 62.11708 |acc: 0.845 |recall: 0.82 |f1 score: 0.833 | validate loss: 57.211 |acc: 0.784 |recall: 0.754 |f1 score: 0.769
Epoch: 73 | train loss: 63.2298 |acc: 0.839 |recall: 0.816 |f1 score: 0.828 | validate loss: 60.247 |acc: 0.764 |recall: 0.752 |f1 score: 0.758
Epoch: 74 | train loss: 61.87119 |acc: 0.848 |recall: 0.828 |f1 score: 0.838 | validate loss: 59.692 |acc: 0.782 |recall: 0.765 |f1 score: 0.774
Epoch: 75 | train loss: 59.88628 |acc: 0.851 |recall: 0.821 |f1 score: 0.836 | validate loss: 59.461 |acc: 0.78 |recall: 0.755 |f1 score: 0.767
Epoch: 76 | train loss: 61.97182 |acc: 0.858 |recall: 0.812 |f1 score: 0.835 | validate loss: 59.748 |acc: 0.78 |recall: 0.749 |f1 score: 0.765
Epoch: 77 | train loss: 62.2035 |acc: 0.836 |recall: 0.811 |f1 score: 0.823 | validate loss: 56.778 |acc: 0.768 |recall: 0.748 |f1 score: 0.758
Epoch: 78 | train loss: 59.90309 |acc: 0.846 |recall: 0.823 |f1 score: 0.835 | validate loss: 59.424 |acc: 0.771 |recall: 0.76 |f1 score: 0.765
Epoch: 79 | train loss: 62.48097 |acc: 0.844 |recall: 0.821 |f1 score: 0.833 | validate loss: 57.535 |acc: 0.769 |recall: 0.755 |f1 score: 0.762
Epoch: 80 | train loss: 65.83723 |acc: 0.853 |recall: 0.83 |f1 score: 0.842 | validate loss: 60.798 |acc: 0.782 |recall: 0.762 |f1 score: 0.772
Epoch: 81 | train loss: 67.69897 |acc: 0.848 |recall: 0.812 |f1 score: 0.83 | validate loss: 62.135 |acc: 0.78 |recall: 0.746 |f1 score: 0.763
Epoch: 82 | train loss: 64.45554 |acc: 0.863 |recall: 0.845 |f1 score: 0.854 | validate loss: 62.102 |acc: 0.793 |recall: 0.775 |f1 score: 0.784
Epoch: 83 | train loss: 59.9239 |acc: 0.857 |recall: 0.84 |f1 score: 0.848 | validate loss: 57.003 |acc: 0.788 |recall: 0.771 |f1 score: 0.779
Epoch: 84 | train loss: 65.42567 |acc: 0.859 |recall: 0.831 |f1 score: 0.845 | validate loss: 61.993 |acc: 0.788 |recall: 0.763 |f1 score: 0.775
Epoch: 85 | train loss: 62.69893 |acc: 0.852 |recall: 0.828 |f1 score: 0.84 | validate loss: 59.489 |acc: 0.786 |recall: 0.761 |f1 score: 0.773
Epoch: 86 | train loss: 64.58199 |acc: 0.858 |recall: 0.831 |f1 score: 0.845 | validate loss: 60.414 |acc: 0.789 |recall: 0.764 |f1 score: 0.776
Epoch: 87 | train loss: 58.41865 |acc: 0.875 |recall: 0.838 |f1 score: 0.856 | validate loss: 56.525 |acc: 0.805 |recall: 0.768 |f1 score: 0.786
Epoch: 88 | train loss: 61.39529 |acc: 0.848 |recall: 0.824 |f1 score: 0.836 | validate loss: 56.678 |acc: 0.783 |recall: 0.759 |f1 score: 0.771
Epoch: 89 | train loss: 63.69639 |acc: 0.857 |recall: 0.818 |f1 score: 0.837 | validate loss: 59.014 |acc: 0.787 |recall: 0.751 |f1 score: 0.769
Epoch: 90 | train loss: 61.78225 |acc: 0.841 |recall: 0.84 |f1 score: 0.84 | validate loss: 59.58 |acc: 0.773 |recall: 0.775 |f1 score: 0.774
Epoch: 91 | train loss: 58.19114 |acc: 0.845 |recall: 0.826 |f1 score: 0.836 | validate loss: 55.284 |acc: 0.776 |recall: 0.758 |f1 score: 0.767
Epoch: 92 | train loss: 58.67227 |acc: 0.857 |recall: 0.82 |f1 score: 0.838 | validate loss: 54.982 |acc: 0.787 |recall: 0.753 |f1 score: 0.77
Epoch: 93 | train loss: 60.79532 |acc: 0.858 |recall: 0.83 |f1 score: 0.844 | validate loss: 57.808 |acc: 0.792 |recall: 0.764 |f1 score: 0.778
Epoch: 94 | train loss: 56.71145 |acc: 0.872 |recall: 0.851 |f1 score: 0.861 | validate loss: 53.551 |acc: 0.804 |recall: 0.785 |f1 score: 0.795
Epoch: 95 | train loss: 58.791 |acc: 0.864 |recall: 0.83 |f1 score: 0.847 | validate loss: 54.284 |acc: 0.793 |recall: 0.765 |f1 score: 0.779
Epoch: 96 | train loss: 60.07491 |acc: 0.849 |recall: 0.828 |f1 score: 0.839 | validate loss: 55.524 |acc: 0.78 |recall: 0.764 |f1 score: 0.772
Epoch: 97 | train loss: 61.53479 |acc: 0.86 |recall: 0.825 |f1 score: 0.842 | validate loss: 56.891 |acc: 0.796 |recall: 0.759 |f1 score: 0.777
Epoch: 98 | train loss: 61.94878 |acc: 0.85 |recall: 0.836 |f1 score: 0.843 | validate loss: 57.019 |acc: 0.783 |recall: 0.771 |f1 score: 0.777
Epoch: 99 | train loss: 58.49541 |acc: 0.86 |recall: 0.834 |f1 score: 0.847 | validate loss: 56.162 |acc: 0.795 |recall: 0.767 |f1 score: 0.781
  • 第六步: 绘制损失曲线和评估曲线图

    • 训练和验证损失对照曲线:

  • 分析: 损失对照曲线一直下降, 从第5个epoch开始, 迅速降到比较理想的位置, 说明模型能够从数据中获取规律了, 到第40个批次之后, 模型趋于稳定, 说明参数基本能够已经得到最优化效果, 此时, 根据对scheduler的设置, 通过该方法已经对优化器进行了近8次的迭代, 应该在我们原本设置的初始学习率基础上缩小了0.2的8次方倍, 此时应该找到了当前最优解, 因此也就趋于稳定了.
  • 训练和验证准确率对照曲线:

  • 分析:
  • 首先,准确率是指识别正确的实体识别出的实体中的比例.
  • 根据对照曲线来看,整体学习结果都在趋于准确率上升方向增加,而且随着批次的增加曲线震动相对平稳,不过可能由于训练与验证样本分布不均衡或者噪声等原因,导致最终验证集的准确度没有达到与训练集相同的情况.
  • 最终的训练集和验证集的召回率分别在:0.85和0.78左右.

  • 训练和验证召回率对照曲线:

  • 分析:
  • 在此召回率是指识别正确的实体占当前批次所包含的所有实体总数的比例.
  • 关于训练和验证召回率对照曲线,可以看出召回率的变化相对比较平滑,基本上也在40步左右趋于稳定.
  • 最终的训练集和验证集的召回率分别在:0.83和0.75左右.
  • 训练和验证F1值对照曲线:

 

  • 分析:
  • F1值主要是指训练效果而言,在不多识别实体的情况下同时提高准确度的衡量指标.
  • 其公式为:2×准确率×召回率 / (准确率+召回率)
  • 从曲线可见整体F1值上升与损失、召回率的曲线比较接近,说明在识别出的实体中,正确率比较问题,不过根据前面的准确度来分析,可能在识别过程中,增加了识别出的实体个数而导致不稳定。从这方面来说,可以验证样本不均衡问题以及噪声对模型的影响还是比较大的。
  • 从整体而言,F1值基本也在第40步之后趋于稳定,最终的训练集和验证集的结果在:0.85和0.75左右。
  • 小节总结:
    • 学习了数据预处理的相关方法
      • 原始数据集的字符经过数字化编码变成向量
      • 标注数据集的字符经过数字化编码变成向量
    • 学习生成批量训练数据的方法
    • 学习了模型训练相关代码的实现
      • 准确率和召回率评估的代码
      • 模型构建类的全部内部函数代码
      • 启动训练流程的代码

6.6 模型使用

  • 学习目标:
    • 掌握模型单条文本预测代码实现
    • 掌握批量文件夹文件预测代码实现
  • 模型单条文本预测代码实现:
import os
import torch
import json
from bilstm_crf import BiLSTM_CRFdef singel_predict(model_path, content, char_to_id_json_path, batch_size, embedding_dim,hidden_dim, num_layers, sentence_length, offset, target_type_list, tag2id):char_to_id = json.load(open(char_to_id_json_path, mode="r", encoding="utf-8"))# 将字符串转为码表id列表char_ids = content_to_id(content, char_to_id)# 处理成 batch_size * sentence_length 的 tensor 数据# 定义模型输入列表model_inputs_list, model_input_map_list = build_model_input_list(content,char_ids,batch_size,sentence_length,offset)# 加载模型model = BiLSTM_CRF(vocab_size=len(char_to_id),tag_to_ix=tag2id,embedding_dim=embedding_dim,hidden_dim=hidden_dim,batch_size=batch_size,num_layers=num_layers,sequence_length=sentence_length)# 加载模型字典model.load_state_dict(torch.load(model_path))tag_id_dict = {v: k for k, v in tag_to_id.items() if k[2:] in target_type_list}# 定义返回实体列表entities = []with torch.no_grad():for step, model_inputs in enumerate(model_inputs_list):prediction_value = model(model_inputs)# 获取每一行预测结果for line_no, line_value in enumerate(prediction_value):# 定义将要识别的实体entity = None# 获取当前行每个字的预测结果for char_idx, tag_id in enumerate(line_value):# 若预测结果 tag_id 属于目标字典数据 key 中if tag_id in tag_id_dict:# 取符合匹配字典id的第一个字符,即B, Itag_index = tag_id_dict[tag_id][0]# 计算当前字符确切的下标位置current_char = model_input_map_list[step][line_no][char_idx]# 若当前字标签起始为 B, 则设置为实体开始if tag_index == "B":entity = current_char# 若当前字标签起始为 I, 则进行字符串追加elif tag_index == "I" and entity:entity += current_char# 当实体不为空且当前标签类型为 O 时,加入实体列表if tag_id == tag_to_id["O"] and entity:# 满足当前字符为O,上一个字符为目标提取实体结尾时,将其加入实体列表entities.append(entity)# 重置实体entity = Nonereturn entitiesdef content_to_id(content, char_to_id):# 定义字符串对应的码表 id 列表char_ids = []for char in list(content):# 判断若字符不在码表对应字典中,则取 NUK 的编码(即 unknown),否则取对应的字符编码if char_to_id.get(char):char_ids.append(char_to_id[char])else:char_ids.append(char_to_id["UNK"])return char_idsdef build_model_input_list(content, char_ids, batch_size, sentence_length, offset):# 定义模型输入数据列表model_input_list = []# 定义每个批次句子 id 数据batch_sentence_list = []# 将文本内容转为列表content_list = list(content)# 定义与模型 char_id 对照的文字model_input_map_list = []#  定义每个批次句子字符数据batch_sentence_char_list = []# 判断是否需要 paddingif len(char_ids) % sentence_length > 0:# 将不足 batch_size * sentence_length 的部分填充0padding_length = (batch_size * sentence_length- len(char_ids) % batch_size * sentence_length- len(char_ids) % sentence_length)char_ids.extend([0] * padding_length)content_list.extend(["#"] * padding_length)# 迭代字符 id 列表# 数据满足 batch_size * sentence_length 将加入 model_input_listfor step, idx in enumerate(range(0, len(char_ids) + 1, sentence_length)):# 起始下标,从第一句开始增加 offset 个字的偏移start_idx = 0 if idx == 0 else idx - step * offset# 获取长度为 sentence_length 的字符 id 数据集sub_list = char_ids[start_idx:start_idx + sentence_length]# 获取长度为 sentence_length 的字符数据集sub_char_list = content_list[start_idx:start_idx + sentence_length]# 加入批次数据集中batch_sentence_list.append(sub_list)# 批量句子包含字符列表batch_sentence_char_list.append(sub_char_list)# 每当批次长度达到 batch_size 时候,将其加入 model_input_listif len(batch_sentence_list) == batch_size:# 将数据格式转为 tensor 格式,大小为 batch_size * sentence_lengthmodel_input_list.append(torch.tensor(batch_sentence_list))# 重置 batch_sentence_listbatch_sentence_list = []# 将 char_id 对应的字符加入映射表中model_input_map_list.append(batch_sentence_char_list)# 重置批字符串内容batch_sentence_char_list = []# 返回模型输入列表return model_input_list, model_input_map_list
  • 输入参数:
# 参数1:待识别文本
content = "本病是由DNA病毒的单纯疱疹病毒所致。人类单纯疱疹病毒分为两型," \
"即单纯疱疹病毒Ⅰ型(HSV-Ⅰ)和单纯疱疹病毒Ⅱ型(HSV-Ⅱ)。" \
"Ⅰ型主要引起生殖器以外的皮肤黏膜(口腔黏膜)和器官(脑)的感染。" \
"Ⅱ型主要引起生殖器部位皮肤黏膜感染。" \
"病毒经呼吸道、口腔、生殖器黏膜以及破损皮肤进入体内," \
"潜居于人体正常黏膜、血液、唾液及感觉神经节细胞内。" \
"当机体抵抗力下降时,如发热胃肠功能紊乱、月经、疲劳等时," \
"体内潜伏的HSV被激活而发病。"
# 参数2:模型保存文件路径
model_path = "model/bilstm_crf_state_dict_20200129_210417.pt"
# 参数3:批次大小
BATCH_SIZE = 8
# 参数4:字向量维度
EMBEDDING_DIM = 300
# 参数5:隐层维度
HIDDEN_DIM = 128
# 参数6:句子长度
SENTENCE_LENGTH = 100
# 参数7:偏移量
OFFSET = 10
# 参数8:标签码表对照字典
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 参数9:字符码表文件路径
char_to_id_json_path = "./data/char_to_id.json"
# 参数10:预测结果存储路径
prediction_result_path = "prediction_result"
# 参数11:待匹配标签类型
target_type_list = ["sym"]
  • 调用:
# 单独文本预测, 获得实体结果
entities = singel_predict(model_path,content,char_to_id_json_path,BATCH_SIZE,EMBEDDING_DIM,HIDDEN_DIM,SENTENCE_LENGTH,OFFSET,target_type_list,tag_to_id)
# 打印实体结果
print("entities:\n", entities)
  • 输出效果:
entities:
['感染', '发热', '##']
  • 批量文件夹文件预测代码实现:
def batch_predict(data_path, model_path, char_to_id_json_path, batch_size, embedding_dim,hidden_dim, sentence_length, offset, target_type_list,prediction_result_path, tag_to_id):"""description: 批量预测,查询文件目录下数据, 从中提取符合条件的实体并存储至新的目录下prediction_result_path:param data_path:               数据文件路径:param model_path:              模型文件路径:param char_to_id_json_path:    字符码表文件路径:param batch_size:              训练批次大小:param embedding_dim:           字向量维度:param hidden_dim:              BiLSTM 隐藏层向量维度:param sentence_length:         句子长度(句子做了padding):param offset:                  设定偏移量, 当字符串超出sentence_length时, 换行时增加偏移量:param target_type_list:        待匹配类型,符合条件的实体将会被提取出来:param prediction_result_path:  预测结果保存路径:param tag_to_id:               标签码表对照字典, 标签对应 id:return:                        无返回"""# 迭代路径, 读取文件名for fn in os.listdir(data_path):# 拼装全路径fullpath = os.path.join(data_path, fn)# 定义输出结果文件entities_file = open(os.path.join(prediction_result_path, fn),mode="w",encoding="utf-8")with open(fullpath, mode="r", encoding="utf-8") as f:# 读取文件内容content = f.readline()# 调用单个预测模型,输出为目标类型实体文本列表entities = singel_predict(model_path, content, char_to_id_json_path,batch_size, embedding_dim, hidden_dim, sentence_length,offset, target_type_list, tag_to_id)# 写入识别结果文件entities_file.write("\n".join(entities))print("batch_predict Finished".center(100, "-"))
  • 输入参数:
# 参数1:模型保存路径
model_path = "model/bilstm_crf_state_dict_20191219_220256.pt"
# 参数2:批次大小
BATCH_SIZE = 8
# 参数3:字向量维度
EMBEDDING_DIM = 200
# 参数4:隐层维度
HIDDEN_DIM = 100
# 参数5:句子长度
SENTENCE_LENGTH = 20
# 参数6:偏移量
OFFSET = 10
# 参数7:标签码表对照字典
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4, "<START>": 5, "<STOP>": 6}
# 参数8:字符码表文件路径
char_to_id_json_path = "./data/char_to_id.json"
# 参数9:预测结果存储路径
prediction_result_path = "prediction_result"
# 参数10:待匹配标签类型
target_type_list = ["sym"]
# 参数11:待预测文本文件所在目录
data_path = "origin_data"
  • 调用:
# 批量文本预测, 并将结果写入文件中
batch_predict(data_path,model_path,char_to_id_json_path,BATCH_SIZE,EMBEDDING_DIM,HIDDEN_DIM,SENTENCE_LENGTH,OFFSET,target_type_list,prediction_result_path,tag_to_id)
  • 输出效果: 将识别结果保存至prediction_result_path指定的目录下, 名称与源文件一致, 内容为每行存储识别实体名称


 

1.transitions转移矩阵 是一个方阵[tagset_size, tagset_size]。tag_to_ix[START_TAG]值为5,tag_to_ix[STOP_TAG]值为6,不管是行数还是列数都从0开始统计。transitions转移矩阵中行名为当前字符的标签,列名为下一个字符的标签,那么列值便是下一个字符出现该标签的概率值,需要计算出列值中下一个字符出现某标签的最大概率值。2.transitions转移矩阵的 第一种写法假设BiLSTM的输出矩阵是P,维度为tag_size, 其中P(i,j)代表单词w_i映射到tag_j的非归一化概率,也就是每个单词w_i映射到标签tag的发射概率值。那么对于CRF层, 假设存在一个转移矩阵A, 其中A(i,j)代表tag_i转移到tag_j的概率,tag_i代表当前字符的标签,tag_j代表当前字符的下一个字符的标签,那么A(i,j)也即为当前字符的标签tag_i转移到下一个字符的标签tag_j的概率值。1.transitions.data[:, tag_to_ix[START_TAG]]=-10000:所有行的第5列都设置为-10000,那么所有字符的下一个字符出现“START_TAG”标签的概率值均为-10000,即保证语义合法的句子中任何字符的下一个字符的标签都不会是“START_TAG”。2.transitions.data[tag_to_ix[STOP_TAG], :]=-10000:第5行的所有列都设置为-10000,那么“标签为STOP_TAG的”当前字符它的下一个字符出现任何标签的的概率值均为-10000,即保证语义合法的句子中“标签为STOP_TAG”的字符后面不会再有任何字符。3.transitions[i,j]:其中下标索引为[i,j]的方格代表当前字符的标签为第i行的行名, 那么下一个字符的标签为第j列的列名,那么transitions[i,j]即为当前字符的标签转移到下一个字符的标签的概率值。

3.transitions转移矩阵的 第二种写法(项目中使用该写法)假设BiLSTM的输出矩阵是P,维度为tag_size, 其中P(i,j)代表单词w_i映射到tag_j的非归一化概率,也就是每个单词w_i映射到标签tag的发射概率值。那么对于CRF层, 假设存在一个转移矩阵A, 其中A(i,j)代表tag_j转移到tag_i的概率,tag_j代表当前字符的标签,tag_i代表当前字符的下一个字符的标签,那么A(i,j)也即为当前字符的标签tag_j转移到下一个字符的标签tag_i的概率值。1.transitions.data[tag_to_ix[START_TAG], :]=-10000:第5行的所有列都设置为-10000,那么“标签为STOP_TAG的”当前字符它的下一个字符出现任何标签的的概率值均为-10000,即保证语义合法的句子中“标签为STOP_TAG”的字符后面不会再有任何字符。2.transitions.data[:, tag_to_ix[STOP_TAG]]=-10000:所有行的第5列都设置为-10000,那么所有字符的下一个字符出现“START_TAG”标签的概率值均为-10000,即保证语义合法的句子中任何字符的下一个字符的标签都不会是“START_TAG”。3.transitions[i,j]:其中下标索引为[i,j]的方格代表当前字符的标签为第j列的列名, 那么下一个字符的标签为第i行的行名,那么transitions[i,j]即为当前字符的标签转移到下一个字符的标签的概率值。



计算损失函数第一项的分值

损失函数第一项的分值:本质上是发射概率emit_score和转移概率trans_score的累加和。前向计算矩阵forward_var、转移概率矩阵trans_score、发射概率矩阵emit_score 计算流程:#仅仅把START_TAG列赋值为0, 代表着接下来的转移只能从START_TAG开始。init_alphas = torch.full((1, self.tagset_size), -10000.)init_alphas[0][self.tag_to_ix[START_TAG]] = 0.forward_var = init_alphas #tensor([[-10000., -10000., -10000., -10000., -10000.,  0., -10000.]])#feats([8, 20, 7]):遍历发射概率矩阵中的每一个句子样本feat_line([20, 7])for feat_line in feats:#遍历当前句子中的每个字符。feat([7]):for feat in feat_line:#遍历当前字符对应的每个标签。tagset_size为7,next_tag为0到6的值,每个字符有7个标签。for next_tag in range(self.tagset_size):#发射概率矩阵中每个字符对应的每个标签的概率值(单数值) 广播为 (1,7)形状的全部元素值均为该标签的概率值的二维矩阵emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)#每个字符对应的每个标签名/标签索引值 作为 转移概率矩阵中 行标签名/行索引值,该行标签名代表要转移到的目标标签,#同样的也可以把当前字符对应的标签认为是代表要转移到的目标标签。#根据行索引值所获取出的这一行的转移概率向量中的每个值代表了上一个字符的标签转移到当前字符的标签的转移概率值。trans_score = transitions[next_tag].view(1, -1)#next_tag_var/forward_var:本质上是发射概率emit_score和转移概率trans_score的累加和next_tag_var = forward_var + trans_score + emit_score#log(sum(exp(next_tag_var))):把[1, 7]形状的二维矩阵转换为单个数值输出alphas_t.append(log_sum_exp(next_tag_var).view(1))#把当前这个字符对应的7个标签的概率计算结果值传递给下一个字符继续作为forward_var使用forward_var = torch.cat(alphas_t).view(1, -1)#每个句子中全部的20个字符对应的7个标签的概率值都完成计算之后,最终还需要添加“最后一步转移到STOP_TAG的”概率值,才算完成整条句子的概率值的前向计算。terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]#log(sum(exp(terminal_var))):把[1, 7]形状的二维矩阵转换为单个数值输出alpha = log_sum_exp(terminal_var)
前向计算矩阵forward_var:(形状为[1, 7]代表当前这一个字符对应的7个标签的前向计算概率值)1.forward_var初始化:tensor([[-10000., -10000., -10000., -10000., -10000.,  0., -10000.]])仅仅把START_TAG列赋值为0, 代表着接下来的转移只能从START_TAG开始。代码:init_alphas = torch.full((1, self.tagset_size), -10000.)init_alphas[0][self.tag_to_ix[START_TAG]] = 0.forward_var = init_alphas

	2.每次循环遍历每个字符时,还会把当前字符计算出来的前向计算矩阵forward_var 传递给下一个字符来使用。3.一个句子中全部20个字符对应的7个标签的概率值完成计算之后,添加“最后一步转移到STOP_TAG的”概率值,才能完成整条句子的概率值的前向计算。代码:terminal_var = forward_var + transitions[self.tag_to_ix[STOP_TAG]]transitions[tag_to_ix[STOP_TAG]]tag_to_ix[STOP_TAG]的值为6作为转移概率矩阵的行索引,即获取出转移概率矩阵中行标签为STOP_TAG的这一行7列的行向量。行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。那么每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。

转移概率矩阵trans_score:(形状为[1, 7]代表当前这一个字符对应的7个标签的转移概率值)1.transitions[next_tag](转移概率矩阵[行索引]):每个字符对应的第1到第7个标签的索引作为行索引,获取转移概率矩阵某一行。2.例子:比如遍历出转移概率矩阵中的START_TAG一行,比如下面的START_TAG一行:tensor([[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]])代码:trans_score = transitions[next_tag].view(1, -1) 遍历每一行7列的一维行向量获取转移概率矩阵中一行7列的一维行向量:一维行向量中的7个值中的每个值分别对应7个标签,那么每个值代表当前该标签转移到下一个某标签的概率分数值。transitions[next_tag]:next_tag作为行索引,行索引上的标签代表了要转移到该目标行的目标标签。next_tag行索引对应在转移概率矩阵transitions上的目标标签作为当前循环所遍历的当前字符的目标标签,那么7列上的起始标签就相当于上一个字符的标签,那么可以认为一维行向量中的7个值分别代表了上一个字符的可能的7个标签各自转移到当前字符的目标标签的转移概率值。

		#feats([8, 20, 7]):遍历发射概率矩阵中的每一个句子样本feat_line([20, 7])for feat_line in feats:#遍历当前句子中的每个字符。feat([7]):for feat in feat_line:#遍历当前字符对应的每个标签。tagset_size为7,next_tag为0到6的值,每个字符有7个标签。for next_tag in range(self.tagset_size):#例如:next_tag为0时,那么transitions[next_tag]取出转移概率矩阵中的第一行7列的行向量。#行索引next_tag所在目标行上的标签认为是要转移到的目标标签,该目标标签即可认为是当前循环所遍历的当前字符的当前标签。#而每列上的标签名则可以认为是转移的起始标签,起始标签即可认为是上一个字符的标签。#那么行向量中的每个转移概率值便代表了上一个字符的标签转移到当前字符的标签的转移概率值。trans_score = transitions[next_tag].view(1, -1)

发射概率矩阵emit_score:(形状为[1, 7]代表当前这一个字符对应的7个标签的发射概率值)1.BiLSTM中最后的Linear线性层输出的[8, 20, 7]形状的发射概率矩阵,即[批量句子数, 句子最大长度, 标签数]。每个字符对应有7个标签的概率值,每个标签的概率值(单数值)广播为(1,7)形状的全部元素值均为相同的二维矩阵。2.例子:1.[8, 20, 7]形状的发射概率矩阵tensor([[[ 0.1331,  0.0748,  0.1188,  ...,  0.0182, -0.1034,  0.1898],...,[-0.1063,  0.0288, -0.2222,  ..., -0.1219,  0.1156,  0.0384]]],)

		2.每个标签的概率值(单数值)广播为(1,7)形状的全部元素值均为相同的二维矩阵(发射概率矩阵中第一个字符对应的前3个标签的概率值)tensor([[0.1331, 0.1331, 0.1331, 0.1331, 0.1331, 0.1331, 0.1331]])tensor([[0.0748, 0.0748, 0.0748, 0.0748, 0.0748, 0.0748, 0.0748]])tensor([[0.1188, 0.1188, 0.1188, 0.1188, 0.1188, 0.1188, 0.1188]])代码:emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)


计算损失函数第二项的分值

损失函数第二项的分值:发射概率矩阵中真实标签的发射概率值 和 转移概率矩阵中真实标签之间的转移概率值 的累加和。#遍历当前句子中的每个字符,也即遍历[8, 20, 7]的发射概率矩阵中的每条[20, 7]的样本句子
for feat_line in feats:#遍历出一条样本句子中的每个字符对应的7个标签的的概率值,也即遍历[20, 7]的样本句子中每个字符对应的[7]的向量for i, feat in enumerate(feat_line):#第一项的score:之前遍历的所有字符所计算的score值的总和#第二项的transitions[tags[idx][i+1],tags[idx][i]](transitions[目标标签,起始标签]):#	(当前字符的)上一个字符的真实标签值(作为起始标签) 转移到 当前字符的真实标签值(作为目标标签) 的转移概率值。#	1.tags[idx][i](起始标签):(当前字符的)上一个字符的真实标签值。i从tags标签列表中的列索引值为0的第1列的START_TAG标签值开始遍历。#	2.tags[idx][i+1](目标标签):循环所遍历出来的当前字符的真实标签值。i从tags标签列表中的列索引值为1的第2列(即句子中第一个字符对应的)真实标签值开始遍历。#				    从转移概率矩阵中所获取的“从上一个字符的真实标签转移到当前字符的真实标签的”转移概率值。#第三项的feat[tags[idx][i+1]]:根据当前字符对应的真实标签值从发射概率矩阵中获取出当前字符对应的真实标签的发射概率值。score = score + transitions[tags[idx][i + 1], tags[idx][i]] + feat[tags[idx][i + 1]]#第一项的score:整一条样本句子遍历完所有20个字符之后计算出来的score值的总和#第二项的transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]](transitions[目标标签,起始标签]):#	句子中的最后一个字符对应的真实标签值(作为起始标签) 转移到 行标签名STOP_TAG(作为目标标签) 的转移概率值。#	1.transitions[tag_to_ix[STOP_TAG]](transitions[目标标签]):#		行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。#		行向量中每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。#	2.tags[idx][-1](起始标签):#		真实标签值为每个样本句子中的最后一个字符对应的真实标签值,最终作为转移概率矩阵中的列索引值,同时该列索引值对应的列标签名作为转移的起始标签。score = score + transitions[self.tag_to_ix[STOP_TAG], tags[idx][-1]]


维特比算法

1.在HMM模型中的解码问题最常用的算法是维特比算法1.维特比算法是一个通用的解码算法,或者说是一个通用的求序列最短路径的动态规划算法,是基于动态规划的求序列最短路径的方法,维特比算法同样也可以应用于解决很多其他问题。2.维特比算法在用于解码隐藏状态序列时,实际即给定模型和观测序列,求给定观测序列条件下,最可能出现的对应的隐藏状态序列。维特比算法可以将HMM的状态序列作为一个整体来考虑,避免近似算法的问题。2.当前使用维特比算法用于解码问题,负责求解解码出最优路径,即推断出最优标签序列。动态规划要求的是在遍历(一共20个字符)每个字符依次前向计算找到最优的7个标签存储到[20, 7]形状的回溯列表,然后再进行反向回溯解码时从回溯列表中找出每个字符最优的一个标签,便是按照从最后一个字符往前的方向 根据第i个字符的最优标签的索引值找到第i-1个字符(即第i个字符的上一个字符)的最优标签的索引值。#1.result_best_path最终返回的形状为二维的[8, 20],包含“等于批量句子样本数8的”列表个数,#  每个列表中又存放“等于句子最大长度的”元素个数,最终的元素值为每个字符解码预测出来的最优标签的索引值。#2.result_best_path存储的是批量每个句子中每个字符解码预测出的最优标签的索引值result_best_path = []#遍历发射概率矩阵(形状[8, 20, 7])中每个样本句子(形状[20, 7])for feat_line in feats:#1.回溯指针:backpointers回溯列表最终返回的形状为二维的[20, 7],#  包含“等于句子最大长度20的”列表个数,每个列表中又存放“等于标签数7的”元素个数,#  每个小列表中的7个元素值代表每个字符通过前向计算得到的7个最大概率的标签索引值。#2.回溯指针backpointers存储的是当前句子中每个字符通过前向计算得到的7个最大概率的标签索引值。backpointers = []#[[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]init_vvars = torch.full((1, self.tagset_size), -10000.)#仅设置索引为5“START_TAG”标签的列值为0,代表只能从START_TAG标签开始#[[-10000., -10000., -10000., -10000., -10000., 0., -10000.]]init_vvars[0][self.tag_to_ix[START_TAG]] = 0#前向计算矩阵forward_var的初始化赋值#	在前向计算过程中遍历的第i个字符(time_step)时,forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量forward_var = init_vvars#遍历发射概率矩阵中一条样本句子(形状[20, 7])中每个字符(形状[7])对应的7个标签的发射概率值for feat in feat_line:#当前字符对应的回溯列表:负责存储每个字符中7个(目标)标签对应的最大概率值的起始标签的索引值bptrs_t = []#当前字符对应的维特比列表:负责存储每个字符中7个(目标)标签对应的最大概率值viterbivars_t = []#遍历发射概率矩阵中的每个字符(形状[7])对应的7个标签的发射概率值for next_tag in range(self.tagset_size):#1.forward_var(前向计算矩阵):#	实质为每个字符对应的7个(目标)标签的最大转移概率值和7个标签的发射概率值的累计和。#	前向计算矩阵所计算的每个当前字符的累计和的值都会传递给下一个字符作为forward_var继续进行累加和计算。#	在前向计算过程中遍历的第i个字符(time_step)时,#	forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量。#2.transitions[next_tag]:#	从转移概率矩阵中取出“行索引为当前标签值的”一行7列(形状[7])的行向量。#	行向量中的7个值代表7个标签转移到当前字符所遍历的当前标签(即目标标签)的转移概率值。next_tag_var = forward_var + transitions[next_tag]#best_tag_id:#	因为每个字符依次前向计算需要找到最优的7个标签,#	那么此处首先需要找到每个字符所遍历的每个(目标)标签的最大概率值,#	argmax目的就是从当前字符所遍历的标签作为目标标签的7个概率值中取出一个最大概率值的索引,#	同时该最大概率值的索引代表了“7个作为转移的起始标签转移到当前目标标签中”最大概率值的一个起始标签。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#	1.forward_var:#		实质为每个字符对应的7个标签的转移概率值和7个标签的发射概率值的累计和。#		在前向计算过程中遍历的第i个字符(time_step)时,#		forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量。#	2.torch.cat(viterbivars_t):变成torch.Size([7])类型。#	3.feat:当前字符对应的7个标签的发射概率值forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)	#把每个字符对应的(形状[7]的)回溯列表 存储到(形状[20, 7]的)句子对应的回溯列表backpointers.append(bptrs_t)#1.执行到此处代表了句子中全部20个字符已经前向计算完毕,最终前向计算矩阵要添加“转移到STOP_TAG的”转移概率值。#2.forward_var:保存了“经过句子中全部20个字符前向计算的”(形状[1, 7]的)矩阵值#3.transitions[tag_to_ix[STOP_TAG]]#	tag_to_ix[STOP_TAG]的值为6作为转移概率矩阵的行索引,即获取出转移概率矩阵中行标签为STOP_TAG的这一行7列的行向量。#	行标签名STOP_TAG作为要转移到的目标标签名,每个列标签名代表了转移的起始标签名。#	那么每个值便代表了“列标签名作为的上一个字符的”每个起始标签 转移到 “行标签名STOP_TAG作为的”目标标签的 转移概率值。terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]	#获取出当前句子对应的(形状[1, 7]的)最终概率值矩阵中的最大概率值的标签的索引值#该索引值代表句子中最后一个字符(第20个字符)的最优标签的索引值。best_tag_id = argmax(terminal_var)#best_path列表最终会保存有20个字符的最优标签的索引值加上1个START_TAG标签的索引值,#因还需要把START_TAG标签的索引值移除掉才能作为函数返回值。#此处先保存下句子中最后一个字符(第20个字符)的最优标签的索引值best_path = [best_tag_id]		#1.reversed翻转回溯列表即倒序排序,从最后一个字符往前遍历,即从第i个字符往第i-1个字符进行遍历。#2.先取得第i个字符的最优标签的索引值,然后便根据当前该第i个字符的最优标签的索引值取得第i-1个字符的最优标签的索引值。#3.最终best_path列表保存有20个字符的最优标签的索引值加上一个START_TAG标签的索引值for bptrs_t in reversed(backpointers):#先取得第i个字符的最优标签的索引值,然后便根据当前该第i个字符的最优标签的索引值取得第i-1个字符的最优标签的索引值。best_tag_id = bptrs_t[best_tag_id]#把每个字符对应的最优标签的索引值追加到best_path列表末尾best_path.append(best_tag_id)#best_path列表最终会保存有20个字符的最优标签的索引值加上1个START_TAG标签的索引值,#因还需要把START_TAG标签的索引值移除掉才能作为函数返回值。#pop()删除best_path列表中存储的最后一个值(START_TAG标签的索引值)start = best_path.pop()#assert断言:删除该值必定为START_TAG标签的索引值assert start == self.tag_to_ix[START_TAG]#重新把best_path列表翻转回正常的字符顺序排序best_path.reverse()
#[[-10000., -10000., -10000., -10000., -10000., -10000., -10000.]]
init_vvars = torch.full((1, self.tagset_size), -10000.)
#仅设置索引为5“START_TAG”标签的列值为0,代表只能从START_TAG标签开始
#[[-10000., -10000., -10000., -10000., -10000., 0., -10000.]]
init_vvars[0][self.tag_to_ix[START_TAG]] = 0
#前向计算矩阵forward_var的初始化赋值
#	在前向计算过程中遍历的第i个字符(time_step)时,forward_var保存的是第i-1个字符(time_step)的viterbi维特比张量
forward_var = init_vvars

#当前第一个字符对应的(形状[7]的)回溯列表:保存当前第一个字符中7个(目标)标签对应的最大概率值的起始标签的索引值
bptrs_t = []
#当前字符对应的(形状[7]的)维特比列表:保存当前第一个字符中7个(目标)标签对应的最大概率值
viterbivars_t = []
#一条句子中20个字符对应的(形状[20, 7]的)回溯列表:保存当前样本句子中所有20个字符对应的(形状[7]的)回溯列表
backpointers.append(bptrs_t)

#最终计算完20个字符的前向计算矩阵forward_var再添加上“转移到STOP_TAG的”转移概率值。
terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]	#获取出当前句子对应的(形状[1, 7]的)最终概率值矩阵中的最大概率值的标签的索引值
#该索引值代表句子中最后一个字符(第20个字符)的最优标签的索引值。
best_tag_id = argmax(terminal_var)#best_path列表最终会保存有20个字符的最优标签的索引值加上1个START_TAG标签的索引值,因还需要把START_TAG标签的索引值移除掉才能作为函数返回值
#此处先保存句子中最后一个字符(第20个字符)的最优标签的索引值
best_path = [best_tag_id]		#1.reversed翻转回溯列表即倒序排序,从最后一个字符往前遍历,即从第i个字符往第i-1个字符进行遍历。
#2.先取得第i个字符的最优标签的索引值,然后便根据当前该第i个字符的最优标签的索引值取得第i-1个字符的最优标签的索引值。
#3.最终best_path列表保存有20个字符的最优标签的索引值加上一个START_TAG标签的索引值
for bptrs_t in reversed(backpointers):#先取得第i个字符的最优标签的索引值,然后便根据当前该第i个字符的最优标签的索引值取得第i-1个字符的最优标签的索引值。best_tag_id = bptrs_t[best_tag_id]#把每个字符对应的最优标签的索引值追加到best_path列表末尾best_path.append(best_tag_id)#best_path列表最终会保存有20个字符的最优标签的索引值加上1个START_TAG标签的索引值,因还需要把START_TAG标签的索引值移除掉才能作为函数返回值
#pop()删除best_path列表中存储的最后一个值(START_TAG标签的索引值)
start = best_path.pop()

 

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. XSS盗取用户信息实验(详细)及xss之旅闯关

    本文目录前言xss盗取用户信息小实验实验环境实验实验过程xss之旅第一关第二关第三关第四关第五关第六关第七关第八关第九关第十关xss注入番外 前言 近段时间学习xss,进行了比较详细的了解,也学了一个小实验,感觉挺有意思,记录下来。然后,后面就是xss之旅的闯关。 xss盗取用…...

    2024/4/24 9:07:44
  2. DPDK源码--报文收发流程(四)

    一、报文的接收流程 传统方式接收报文时,当网卡接收到报文后会产生硬件中断,进而报文会通过协议栈,最后到达应用层,这个过程需要内核协议栈的处理。 和传统报文接收不同,当应用层想要接收来自网卡的报文时, 应用层通过while死循环的方式,调用rte_eth_rx_burst接口轮询接…...

    2024/5/2 0:45:08
  3. 聊一聊HTTPS安全性原理以及如何在GIN框架中运用

    前言:编写本文的目的是让新手们快速了解HTTP协议相关知识,文中列举的信息如果有误,希望你能指出。期待大家共同进步。HTTTPS安全性原理HTTP是建立在TCP协议之上的请求响应协议,由于安全性需求衍生出HTTPS网络协议,因此HTTPS处于HTTP和TCP协议中间,建立起安全可靠的HTTP通…...

    2024/5/1 23:33:14
  4. 【剑指Offer】40. Python实现数组中只出现一次的数字

    知识点:数组题目描述 一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。 详解: 方法:(字典法) # -*- coding:utf-8 -*- class Solution:# 返回[a,b] 其中ab是出现一次的两个数字def FindNumsAppearOnce(self, array):# writ…...

    2024/5/1 22:57:26
  5. 【ORACLE数据库事务管理】

    一、事务:由一个或多个DMLSQL语句组成,事务从第一条可执行的SQL开始,在提交或回滚时终止,并且要么完全成功执行,要么一点也不执行。作用:保证数据库的完整性。 1.事务的四大特性:ACID 1)原子性(Atomicity) 事务中包含的所有操作要么全做,要么都不做,也就是说所有的活动…...

    2024/4/24 9:07:48
  6. 雨听 | 英语学习笔记(十三)~作文范文:创造一个和谐的家庭

    雨听青苔入镜 檐下风铃 摇晃曾经 回忆 无从剪接 记录下学习点滴 注:范文来自懒人英语 由海轰整理Creating a Harmonious FamilyThe significance of harmony in a family has long been acknowledged.As the saying goes,if the family lives in harmony, all affairs will pr…...

    2024/4/24 9:07:46
  7. Linux部署ecshop实战

    搭建环境:centos 7+Apache+PHP+mysql 第一步:安装Apache 输入命令:yum -y install httpd 装完成功后开放Apache端口,以便外网访问: 开启80端口:firewall-cmd --zone=public --add-port=80/tcp --permanent 重启生效:firewall-cmd --reload 启动Apache服务器:systemctl …...

    2024/4/24 9:07:41
  8. 数据库,数据库系统上慕课之第三讲关系模型之基本概念测验答案

    1 某关系R的外键是指______ A. 其它关系的候选键,可以是R中的主属性或非主属性 B. 该关系除主键之外的另一个候选键 C. 其它关系的候选键,同时作为R的主属性 D. 其它关系的候选键,同时作为R的非主属性 A 2 关系模型中,下列关于候选键说法正确的是_______ A. 可由其值能惟一…...

    2024/5/1 23:38:13
  9. Apollo 源码解析 —— 客户端 API 配置(一)之一览

    1. 概述老艿艿:本系列假定胖友已经阅读过 《Apollo 官方 wiki 文档》 ,特别是 《Java 客户端使用指南》 。本文,我们来一览 Apollo 客户端配置的 Java API 的实现,从而对它有整体的认识。再在之后的文章,我会写每个组件的具体代码实现。涉及类如下图:2. ConfigServicecom…...

    2024/4/24 9:07:37
  10. Java Iterator迭代器接口

    源出处:尚硅谷http://www.gulixueyuan.com/my/course/310 Iterator迭代器接口Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素 GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元 素,而又不需暴露该对象的内部细节。…...

    2024/5/1 22:18:09
  11. QT:颜色拾取器的实现

    前言 思路:首先需要获取到鼠标的x和y坐标点,然后获取鼠标坐标点的像素点。将像素点的rgb分离出来显示。最后label上显示这个像素点的RGB 如果想要实时的显示这些数据的话,使用的是定时器一直调用函数showColorValue就行了 核心代码: //获取桌面x,y坐标点宽度1,高度1的像素…...

    2024/5/1 21:39:18
  12. 第三次学JAVA再学不好就吃翔(part14)--基础语法之循环语句注意事项

    学习笔记,仅供参考文章目录JAVA基础语法for, while, do...while的区别死循环循环嵌套JAVA基础语法for, while, do…while的区别do…while与其他两种的区别do…while循环至少执行一次循环体,而for,while循环必须先判断条件是否成立,然后决定是否执行循环体语句。for和while的…...

    2024/4/16 12:36:50
  13. Nginx 安装配置

    Nginx 安装 系统平台:CentOS release 6.6 (Final) 64位。 一、安装编译工具及库文件 yum -y install make zlib zlib-devel gcc-c++ libtool openssl openssl-devel 二、首先要安装 PCRE PCRE 作用是让 Nginx 支持 Rewrite 功能。 1、下载 PCRE 安装包,下载地址: http://do…...

    2024/4/16 12:35:28
  14. mysql备份数据库还原数据库压缩解压一条命令搞定

    mysql备份数据库还原数据库压缩解压一条命令搞定 备份并且压缩: mysqldump -hhostname -uusername -ppassword --databases dbname | gzip > backup-file.sql.gz解压还原: gunzip < backup-file.sql.gz | mysql -uusername -ppassword dbnamehostname mysql的ip数据库地…...

    2024/5/1 22:53:47
  15. kali 2020更改root密码

    摁下E键。 移到下面一行的“ro”这里,将“ro”改成“rw”;再将光标移到最后那里,空一格,输入“init=/bin/bash”f10保存修改密码为 paddwd usernameroot...

    2024/4/16 12:35:38
  16. Java实验(五)简单聊天室程序——主要考察图形界面、IO操作和网络编程的结合

    本次实验涉及的知识点 1、掌握网络编程的基本概念。 2、掌握java.net包中常用网络编程工具类的功能和使用。 3、掌握UDP编程和TCP编程的基本方法和步骤。 结合图形界面、IO操作、网络编程知识,设计一个简单聊天室程序,要求能够实现两台电脑之间的对聊。 实现效果如下:这个…...

    2024/4/16 12:35:28
  17. 雨听 | 英语学习笔记(十一)~作文范文:公园的免费入口

    雨听青苔入镜 檐下风铃 摇晃曾经 回忆 无从剪接 记录下学习点滴 注:范文来自懒人英语 由海轰整理Free Entrance to ParksRecently an increasing number of parks are offering free entrance,ensuring more people, rich or poor, have access to them.It is obvious that th…...

    2024/4/18 1:21:00
  18. 基于Ajax实现轮询

    一、什么是轮询 轮询(Polling)是一种CPU决策如何提供周边设备服务的方式,又称“程控输出入”(Programmed I/O)。轮询法的概念是,由CPU定时发出询问,依序询问每一个周边设备是否需要其服务,有即给予服务,服务结束后再问下一个周边,接着不断周而复始。 在WEB上来说就是…...

    2024/5/2 0:15:40
  19. 华清远见在线学习嵌入式第二天

    -128在内存中补码为10000000。 数在内存中以补码形式存在。 字符串以\0结尾。 变量存数类型 默认auto 在局部变量中为内存中的随机值。 register 寄存器型,将变量放入CPU的寄存器中,加速程序运行,但是CPU中寄存器数量有限并且寄存器没有地址,不可使用指针访问。 static 静态…...

    2024/4/24 9:07:34
  20. spring-security之生产环境的用户登录(身份验证)以及用户名回显和用户退出

    文章目录超级管理员的用户登录(内置,不连接数据库)用户名回显用户退出 超级管理员的用户登录(内置,不连接数据库) 导入依赖<!-- 身份验证 --><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-we…...

    2024/4/24 9:07:36

最新文章

  1. 深入理解多层感知机MLP

    1. 基础理论 神经网络基础&#xff1a; 目标&#xff1a;了解神经网络的结构&#xff0c;包括神经元、权重、偏置和激活函数。 神经网络是由多个层次的神经元组成的网络&#xff0c;它模拟了人脑处理信息的方式。每个神经元可以接收输入、处理输入并生成输出。这一过程涉及到…...

    2024/5/2 1:58:46
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. Java深度优先搜索DFS(含面试大厂题和源码)

    深度优先搜索&#xff08;Depth-First Search&#xff0c;简称DFS&#xff09;是一种用于遍历或搜索树或图的算法。DFS 通过沿着树的深度来遍历节点&#xff0c;尽可能深地搜索树的分支。当节点v的所在边都已被探寻过&#xff0c;搜索将回溯到发现节点v的那条边的起始节点。这个…...

    2024/4/30 5:58:20
  4. Vue ts 如何给 props 中的变量指定特定类型,比如 Interface 类的

    Vue ts 如何给 props 中的变量指定特定类型&#xff0c;比如 Interface 类的 我有一个这样的变量值类型 一、在没用 ts 之前的 props 类型指定方式 我们都知道之前在没用 ts 之前的 props 变量值类型指定方式&#xff1a; 如下图&#xff0c;billFood 定义方式是这样的&…...

    2024/5/1 4:30:48
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/1 17:30:59
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/4/30 18:14:14
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/29 2:29:43
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/4/30 18:21:48
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/30 9:43:09
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/4/25 18:39:16
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/29 20:46:55
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/30 22:21:04
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/1 4:32:01
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/27 23:24:42
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/30 9:42:22
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/4/30 9:43:22
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/30 9:42:49
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57