当前位置:   article > 正文

BERT中文实战---命名实体识别_processors = {"ner": nerprocessor}

processors = {"ner": nerprocessor}

我一直做的是有关实体识别的任务,BERT已经火了有一段时间,也研究过一点,今天将自己对bert对识别实体的简单认识记录下来,希望与大家进行来讨论

BERT官方Github地址:https://github.com/google-research/bert ,其中对BERT模型进行了详细的介绍,更详细的可以查阅原文献:https://arxiv.org/abs/1810.04805 

bert可以简单地理解成两段式的nlp模型,(1)pre_training:即预训练,相当于wordembedding,利用没有任何标记的语料训练一个模型;(2)fine-tuning:即微调,利用现有的训练好的模型,根据不同的任务,输入不同,修改输出的部分,即可完成下游的一些任务(如命名实体识别、文本分类、相似度计算等等)
本文是在官网上给定的run_classifier.py中进行修改从而完成命名实体识别的任务

BERT+Bilstm-CRF,前面的BERT就是用来产生词向量的

代码的解读,将主要的几个代码进行简单的解读

数据格式

张  B-PER

三  I-PER

来 O

自 O

北  B-LOC

京  I-LOC

我们最终需要把数据转换成bert论文中的形式

数据封装

代码中将所有的数据封装成record的形式:

  1. for (ex_index, example) in enumerate(examples):
  2. if ex_index % 5000 == 0:
  3. tf.logging.info("Writing example %d of %d" % (ex_index, len(examples)))
  4. # 对于每一个训练样本,
  5. feature = convert_single_example(ex_index, example, label_list, max_seq_length, tokenizer, mode)
  6. # print(feature.input_ids) #
  7. def create_int_feature(values):
  8. f = tf.train.Feature(int64_list=tf.train.Int64List(value=list(values)))
  9. return f
  10. features = collections.OrderedDict()
  11. features["input_ids"] = create_int_feature(feature.input_ids)
  12. features["input_mask"] = create_int_feature(feature.input_mask)
  13. features["segment_ids"] = create_int_feature(feature.segment_ids)
  14. features["label_ids"] = create_int_feature(feature.label_ids)
  15. tf_example = tf.train.Example(features=tf.train.Features(feature=features))
  16. writer.write(tf_example.SerializeToString()) # 它的作用是将Example中的map压缩为二进制,节约大量空间

数据读取

读取record 数据,组成batch

  1. train_input_fn = file_based_input_fn_builder(
  2. input_file=train_file,
  3. seq_length=FLAGS.max_seq_length,
  4. is_training=True,
  5. drop_remainder=True)

这里主要也是通过回调函数完成

  1. def input_fn(params):
  2. batch_size = params["batch_size"]
  3. d = tf.data.TFRecordDataset(input_file)
  4. if is_training:
  5. d = d.repeat()
  6. d = d.shuffle(buffer_size=100)
  7. d = d.apply(tf.contrib.data.map_and_batch(
  8. lambda record: _decode_record(record, name_to_features),
  9. batch_size=batch_size,
  10. drop_remainder=drop_remainder
  11. ))
  12. return d

input_file就是保存的record文件,然后用d = tf.data.TFRecordDataset(input_file)读数据,这样就得到了一个batch的数据。

estimator封装器

  1. estimator = tf.contrib.tpu.TPUEstimator(
  2. use_tpu=FLAGS.use_tpu,
  3. model_fn=model_fn,
  4. config=run_config,
  5. train_batch_size=FLAGS.train_batch_size,
  6. eval_batch_size=FLAGS.eval_batch_size,
  7. predict_batch_size=FLAGS.predict_batch_size)

有了这个封装器训练、验证测试都比较方便,这里的model_fn就是模型定义的的回调函数。

1、主函数

  1. if __name__ == "__main__":
  2.     flags.mark_flag_as_required("data_dir")
  3.     flags.mark_flag_as_required("task_name")
  4.     flags.mark_flag_as_required("vocab_file")
  5.     flags.mark_flag_as_required("bert_config_file")
  6.     flags.mark_flag_as_required("output_dir")
  7.     tf.app.run()


主函数中指定了一些必须不能少的参数
data_dir:指的是我们的输入数据的文件夹路径
task_name:任务的名字
vocab_file:字典,一般从下载的模型中直接包含这个字典,名字“vocab.txt”
bert_config_file:一些预训练好的配置参数,同样在下载的模型文件夹中,名字为“bert_config.json”
output_dir:输出文件保存的位置

 

 

2、main(_)函数

  1. processors = {
  2.         "ner": NerProcessor
  3.     }
  4. task_name = FLAGS.task_name.lower()  
  5. processor = processors[task_name]()

上面代码中的task_name是用来选择processor的

processor:任何模型的训练、预测都是需要有一个明确的输入,而BERT代码中processor就是负责对模型的输入进行处理,自定义的processor里需要继承DataProcessor,并重载获取label的get_labels和获取单个输入的get_train_examples,get_dev_examples和get_test_examples函数。其分别会在main函数的FLAGS.do_train、FLAGS.do_eval和FLAGS.do_predict阶段被调用。这三个函数的内容是相差无几的,区别只在于需要指定各自读入文件的地址
NerProcessor的代码如下:

  1. class NerProcessor(DataProcessor): ##数据的读入
  2. def get_train_examples(self, data_dir):
  3. return self._create_example(
  4. self._read_data(os.path.join(data_dir, "train.txt")), "train"
  5. )
  6. def get_dev_examples(self, data_dir):
  7. return self._create_example(
  8. self._read_data(os.path.join(data_dir, "dev.txt")), "dev"
  9. )
  10. def get_test_examples(self, data_dir):
  11. return self._create_example(
  12. self._read_data(os.path.join(data_dir, "test.txt")), "test")
  13. def get_labels(self):
  14. # 9个类别
  15. return ["O", "B-dizhi", "I-dizhi", "B-shouduan", "I-shouduan", "B-caiwu", "I-caiwu", "B-riqi", "I-riqi", "X",
  16. "[CLS]", "[SEP]"]
  17. def _create_example(self, lines, set_type):
  18. examples = []
  19. for (i, line) in enumerate(lines):
  20. guid = "%s-%s" % (set_type, i)
  21. text = tokenization.convert_to_unicode(line[1])
  22. label = tokenization.convert_to_unicode(line[0])
  23. if i == 0:
  24. examples.append(InputExample(guid=guid, text=text, label=label))
  25. return examples

上面的代码主要是完成了数据的读入,且继承了DataProcessor这个类,_read_data()函数是在父类DataProcessor中实现的,具体的代码如下所示:

  1. class DataProcessor(object):
  2. """Base class for data converters for sequence classification data sets."""
  3. def get_train_examples(self, data_dir):
  4. """Gets a collection of `InputExample`s for the train set."""
  5. raise NotImplementedError()
  6. def get_dev_examples(self, data_dir):
  7. """Gets a collection of `InputExample`s for the dev set."""
  8. raise NotImplementedError()
  9. def get_labels(self):
  10. """Gets the list of labels for this data set."""
  11. raise NotImplementedError()
  12. @classmethod
  13. def _read_data(cls, input_file):
  14. """Reads a BIO data."""
  15. with codecs.open(input_file, 'r', encoding='utf-8') as f:
  16. lines = []
  17. words = []
  18. labels = []
  19. for line in f:
  20. contends = line.strip()
  21. tokens = contends.split() ##根据不同的语料,此处的split()划分标志需要进行更改
  22. # print(len(tokens))
  23. if len(tokens) == 2:
  24. word = line.strip().split()[0] ##根据不同的语料,此处的split()划分标志需要进行更改
  25. label = line.strip().split()[-1] ##根据不同的语料,此处的split()划分标志需要进行更改
  26. else:
  27. if len(contends) == 0:
  28. l = ' '.join([label for label in labels if len(label) > 0])
  29. w = ' '.join([word for word in words if len(word) > 0])
  30. lines.append([l, w])
  31. words = []
  32. labels = []
  33. continue
  34. if contends.startswith("-DOCSTART-"):
  35. words.append('')
  36. continue
  37. words.append(word)
  38. labels.append(label)
  39. return lines ##(label,word)

_read_data()函数:主要是针对NER的任务进行改写的,将输入的数据中的字存储到words中,标签存储到labels中,将一句话中所有字以空格隔开组成一个字符串放入到w中,同理标签放到l中,同时将w与l放到lines中,具体的代码如下所示:

  1. l = ' '.join([label for label in labels if len(label) > 0])
  2. w = ' '.join([word for word in words if len(word) > 0])
  3. lines.append([l, w])

def get_labels(self):是将标签返回,会在原来标签的基础之上多添加"X","[CLS]", "[SEP]"这三个标签,句子开始设置CLS 标志,句尾添加[SEP] 标志,"X"表示的是英文中缩写拆分时,拆分出的几个部分,除了第1部分,其他的都标记为"X"

代码中使用了InputExample类

  1. class InputExample(object):
  2. """A single training/test example for simple sequence classification."""
  3. def __init__(self, guid, text, label=None):
  4. """Constructs a InputExample. ##构造BLSTM_CRF一个输入的例子
  5. Args:
  6. guid: Unique id for the example.
  7. text: string. The untokenized text of the first sequence. For single
  8. sequence tasks, only this sequence must be specified.
  9. label: (Optional) string. The label of the example. This should be
  10. specified for train and dev examples, but not for test examples.
  11. """
  12. self.guid = guid
  13. self.text = text
  14. self.label = label

我的理解是这个是输入数据的一个封装,不管要处理的是什么任务,需要经过这一步,对输入的格式进行统一一下
guid是一种标识,标识的是test、train、dev

3、模型的构造

  1. model_fn = model_fn_builder(
  2. bert_config=bert_config,
  3. num_labels=len(label_list) + 1,
  4. init_checkpoint=FLAGS.init_checkpoint,
  5. learning_rate=FLAGS.learning_rate,
  6. num_train_steps=num_train_steps,
  7. num_warmup_steps=num_warmup_steps,
  8. use_tpu=FLAGS.use_tpu,
  9. use_one_hot_embeddings=FLAGS.use_tpu)
  10. estimator = tf.contrib.tpu.TPUEstimator(
  11. use_tpu=FLAGS.use_tpu,
  12. model_fn=model_fn,
  13. config=run_config,
  14. train_batch_size=FLAGS.train_batch_size,
  15. eval_batch_size=FLAGS.eval_batch_size,
  16. predict_batch_size=FLAGS.predict_batch_size)

返回的model_dn 是一个函数,其定义了模型,训练,评测方法,并且使用钩子参数,加载了BERT模型的参数进行了自己模型的参数初始化过程

这个model_fn_builder是为了构造代码中默认调用的model_fn函数服务的,为了使用其他的参数,只不过model_fn函数的默认参数只有features, labels, mode, params,这四个,所以在model_fn包裹了一层model_fn_builder

tf 新的架构方法,通过定义model_fn 函数,定义模型,然后通过EstimatorAPI进行模型的其他工作,Es就可以控制模型的训练,预测,评估工作等。

init_checkpoint就是下载的模型

4、train()函数

  1. if FLAGS.do_train:
  2. # 1. 将数据转化为tf_record 数据
  3. if data_config.get('train.tf_record_path', '') == '':
  4. train_file = os.path.join(FLAGS.output_dir, "train.tf_record")
  5. filed_based_convert_examples_to_features(
  6. train_examples, label_list, FLAGS.max_seq_length, tokenizer, train_file)
  7. else:
  8. train_file = data_config.get('train.tf_record_path')
  9. num_train_size = int(data_config['num_train_size'])
  10. # 2.读取record 数据,组成batch
  11. train_input_fn = file_based_input_fn_builder(
  12. input_file=train_file,
  13. seq_length=FLAGS.max_seq_length,
  14. is_training=True,
  15. drop_remainder=True)
  16. estimator.train(input_fn=train_input_fn, max_steps=num_train_steps)
  17. def convert_single_example(ex_index, example, label_list, max_seq_length, tokenizer, mode):
  18. """
  19. 将一个样本进行分析,然后将字转化为id, 标签转化为id,然后结构化到InputFeatures对象中
  20. """
  21. label_map = {}
  22. # 1表示从1开始对label进行index
  23. for (i, label) in enumerate(label_list, 1):
  24. label_map[label] = i
  25. # 保存label->index 的map
  26. with codecs.open(os.path.join(FLAGS.output_dir, 'label2id.pkl'), 'wb') as w:
  27. pickle.dump(label_map, w)
  28. textlist = example.text.split(' ')
  29. labellist = example.label.split(' ')
  30. tokens = []
  31. labels = []
  32. for i, word in enumerate(textlist):
  33. # 分词,如果是中文,就是分字
  34. token = tokenizer.tokenize(word)
  35. tokens.extend(token)
  36. label_1 = labellist[i]
  37. for m in range(len(token)):
  38. if m == 0:
  39. labels.append(label_1)
  40. else: # 一般不会出现else
  41. labels.append("X")
  42. # tokens = tokenizer.tokenize(example.text)
  43. # 序列截断
  44. if len(tokens) >= max_seq_length - 1:
  45. tokens = tokens[0:(max_seq_length - 2)] # -2 的原因是因为序列需要加一个句首和句尾标志
  46. labels = labels[0:(max_seq_length - 2)]
  47. ntokens = []
  48. segment_ids = []
  49. label_ids = []
  50. ntokens.append("[CLS]") # 句子开始设置CLS 标志
  51. segment_ids.append(0)
  52. # append("O") or append("[CLS]") not sure!
  53. label_ids.append(label_map["[CLS]"]) # O OR CLS 没有任何影响,不过我觉得O 会减少标签个数,不过拒收和句尾使用不同的标志来标注,使用LCS 也没毛病
  54. for i, token in enumerate(tokens):
  55. ntokens.append(token)
  56. segment_ids.append(0)
  57. label_ids.append(label_map[labels[i]])
  58. ntokens.append("[SEP]") # 句尾添加[SEP] 标志
  59. segment_ids.append(0)
  60. # append("O") or append("[SEP]") not sure!
  61. label_ids.append(label_map["[SEP]"])
  62. input_ids = tokenizer.convert_tokens_to_ids(ntokens) # 将序列中的字(ntokens)转化为ID形式
  63. input_mask = [1] * len(input_ids)
  64. # label_mask = [1] * len(input_ids)
  65. # padding, 使用
  66. while len(input_ids) < max_seq_length:
  67. input_ids.append(0)
  68. input_mask.append(0)
  69. segment_ids.append(0)
  70. # we don't concerned about it!
  71. label_ids.append(0)
  72. ntokens.append("**NULL**")
  73. # label_mask.append(0)
  74. # print(len(input_ids))
  75. assert len(input_ids) == max_seq_length
  76. assert len(input_mask) == max_seq_length
  77. assert len(segment_ids) == max_seq_length
  78. assert len(label_ids) == max_seq_length
  79. # assert len(label_mask) == max_seq_length
  80. # 结构化为一个类
  81. feature = InputFeatures(
  82. input_ids=input_ids,
  83. input_mask=input_mask,
  84. segment_ids=segment_ids,
  85. label_ids=label_ids,
  86. # label_mask = label_mask
  87. )
  88. # mode='test'的时候才有效
  89. write_tokens(ntokens, mode)
  90. return feature
  91. def filed_based_convert_examples_to_features(
  92. examples, label_list, max_seq_length, tokenizer, output_file, mode=None
  93. ):
  94. """
  95. 将数据转化为TF_Record 结构,作为模型数据输入
  96. :param examples: 样本
  97. :param label_list:标签list
  98. :param max_seq_length: 预先设定的最大序列长度
  99. :param tokenizer: tokenizer 对象
  100. :param output_file: tf.record 输出路径
  101. :param mode:
  102. :return:
  103. """
  104. writer = tf.python_io.TFRecordWriter(output_file)
  105. # 遍历训练数据
  106. for (ex_index, example) in enumerate(examples):
  107. if ex_index % 5000 == 0:
  108. tf.logging.info("Writing example %d of %d" % (ex_index, len(examples)))
  109. # 对于每一个训练样本,
  110. feature = convert_single_example(ex_index, example, label_list, max_seq_length, tokenizer, mode)
  111. def create_int_feature(values):
  112. f = tf.train.Feature(int64_list=tf.train.Int64List(value=list(values)))
  113. return f
  114. features = collections.OrderedDict()
  115. features["input_ids"] = create_int_feature(feature.input_ids)
  116. features["input_mask"] = create_int_feature(feature.input_mask)
  117. features["segment_ids"] = create_int_feature(feature.segment_ids)
  118. features["label_ids"] = create_int_feature(feature.label_ids)
  119. tf_example = tf.train.Example(features=tf.train.Features(feature=features))
  120. writer.write(tf_example.SerializeToString())

print(feature.input_ids)

模型的训练

  1. estimator.train(input_fn=train_input_fn, max_steps=num_train_steps,
  2. hooks=[early_stopping_hook])

总结起来如下所示:来自网址https://www.jianshu.com/p/b05e50f682dd

暂时更新到这个地方,后续会继续更新

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/648434
推荐阅读
相关标签
  

闽ICP备14008679号