2024年最全一本读懂BERT(实践篇)_train_batch_size(3),2024年最新2024年Golang开发突破20k有哪些有效的路径

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

这说明在验证集上的准确率是0.84左右。

五、数据读取源码阅读

(一) DataProcessor

我们首先来看数据是怎么读入的。这是一个抽象基类,定义了get_train_examples、get_dev_examples、get_test_examples和get_labels等4个需要子类实现的方法,另外提供了一个_read_tsv函数用于读取tsv文件。下面我们通过一个实现类MrpcProcessor来了解怎么实现这个抽象基类,如果读者想使用自己的数据,那么就需要自己实现一个新的子类。

(二) MrpcProcessor

对于MRPC任务,这里定义了MrpcProcessor来基础DataProcessor。我们来看其中的get_labels和get_train_examples,其余两个抽象方法是类似的。首先是get_labels,它非常简单,这任务只有两个label。

def get_labels(self): 
  return ["0", "1"]

接下来是get_train_examples:

def get_train_examples(self, data_dir):
  return self._create_examples(
		  self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")

这个函数首先使用_read_tsv读入训练文件train.tsv,然后使用_create_examples函数把每一行变成一个InputExample对象。

def _create_examples(self, lines, set_type):
  examples = []
  for (i, line) in enumerate(lines):
	  if i == 0:
		  continue
	  guid = "%s-%s" % (set_type, i)
	  text_a = tokenization.convert_to_unicode(line[3])
	  text_b = tokenization.convert_to_unicode(line[4])
	  if set_type == "test":
		  label = "0"
	  else:
		  label = tokenization.convert_to_unicode(line[0])
	  examples.append(
		  InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
	  return examples

代码非常简单,line是一个list,line[3]和line[4]分别代表两个句子,如果是训练集合和验证集合,那么第一列line[0]就是真正的label,而如果是测试集合,label就没有意义,随便赋值成”0”。然后对于所有的字符串都使用tokenization.convert_to_unicode把字符串变成unicode的字符串。这是为了兼容Python2和Python3,因为Python3的str就是unicode,而Python2的str其实是bytearray,Python2却有一个专门的unicode类型。感兴趣的读者可以参考其实现,不感兴趣的可以忽略。

最终构造出一个InputExample对象来,它有4个属性:guid、text_a、text_b和label,guid只是个唯一的id而已。text_a代表第一个句子,text_b代表第二个句子,第二个句子可以为None,label代表分类标签。

六、分词源码阅读

分词是我们需要重点关注的代码,因为如果想要把BERT产品化,我们需要使用Tensorflow Serving,Tensorflow Serving的输入是Tensor,把原始输入变成Tensor一般需要在Client端完成。BERT的分词是Python的代码,如果我们使用其它语言的gRPC Client,那么需要用其它语言实现同样的分词算法,否则预测时会出现问题。

这部分代码需要读者有Unicode的基础知识,了解什么是CodePoint,什么是Unicode Block。Python2和Python3的str有什么区别,Python2的unicode类等价于Python3的str等等。不熟悉的读者可以参考一些资料。

(一)FullTokenizer

BERT里分词主要是由FullTokenizer类来实现的。

class FullTokenizer(object): 
	def __init__(self, vocab_file, do_lower_case=True):
		self.vocab = load_vocab(vocab_file)
		self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case)
		self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab)

	def tokenize(self, text):
		split_tokens = []
		for token in self.basic_tokenizer.tokenize(text):
			for sub_token in self.wordpiece_tokenizer.tokenize(token):
				split_tokens.append(sub_token)
		
		return split_tokens

	def convert_tokens_to_ids(self, tokens):
		return convert_tokens_to_ids(self.vocab, tokens)

FullTokenizer的构造函数需要传入参数词典vocab_file和do_lower_case。如果我们自己从头开始训练模型(后面会介绍),那么do_lower_case决定了我们的某些是否区分大小写。如果我们只是Fine-Tuning,那么这个参数需要与模型一致,比如模型是chinese_L-12_H-768_A-12,那么do_lower_case就必须为True。

函数首先调用load_vocab加载词典,建立词到id的映射关系。下面是文件chinese_L-12_H-768_A-12/vocab.txt的部分内容

馬
高
龍
龸
fi
fl
!
(
)
,
-
.
/
:
?
~
the
of
and
in
to

接下来是构造BasicTokenizer和WordpieceTokenizer。前者是根据空格等进行普通的分词,而后者会把前者的结果再细粒度的切分为WordPiece。

tokenize函数实现分词,它先调用BasicTokenizer进行分词,接着调用WordpieceTokenizer把前者的结果再做细粒度切分。下面我们来详细阅读这两个类的代码。我们首先来看BasicTokenizer的tokenize方法。

def tokenize(self, text): 
  text = convert_to_unicode(text)
  text = self._clean_text(text)
  
  # 这是2018年11月1日为了支持多语言和中文增加的代码。这个代码也可以用于英语模型,因为在
  # 英语的训练数据中基本不会出现中文字符(但是某些wiki里偶尔也可能出现中文)。
  text = self._tokenize_chinese_chars(text)
  
  orig_tokens = whitespace_tokenize(text)
  split_tokens = []
  for token in orig_tokens:
	  if self.do_lower_case:
		  token = token.lower()
		  token = self._run_strip_accents(token)
	  split_tokens.extend(self._run_split_on_punc(token))
  
  output_tokens = whitespace_tokenize(" ".join(split_tokens))
  return output_tokens

首先是用convert_to_unicode把输入变成unicode,这个函数前面也介绍过了。接下来是_clean_text函数,它的作用是去除一些无意义的字符。

def _clean_text(self, text):
  """去除一些无意义的字符以及whitespace"""
  output = []
  for char in text:
	  cp = ord(char)
	  if cp == 0 or cp == 0xfffd or _is_control(char):
		  continue
	  if _is_whitespace(char):
		  output.append(" ")
	  else:
		  output.append(char)
  return "".join(output)

codepoint为0的是无意义的字符,0xfffd(U+FFFD)显示为�,通常用于替换未知的字符。_is_control用于判断一个字符是否是控制字符(control character),所谓的控制字符就是用于控制屏幕的显示,比如\n告诉(控制)屏幕把光标移到下一行的开始。读者可以参考这里

def _is_control(char):
	"""检查字符char是否是控制字符"""
	# 回车换行和tab理论上是控制字符,但是这里我们把它认为是whitespace而不是控制字符
	if char == "\t" or char == "\n" or char == "\r":
		return False
	cat = unicodedata.category(char)
	if cat.startswith("C"):
		return True
	return False

这里使用了unicodedata.category这个函数,它返回这个Unicode字符的Category,这里C开头的都被认为是控制字符,读者可以参考这里

接下来是调用_is_whitespace函数,把whitespace变成空格。

def _is_whitespace(char):
	"""Checks whether `chars` is a whitespace character."""
	# \t, \n, and \r are technically contorl characters but we treat them
	# as whitespace since they are generally considered as such.
	if char == " " or char == "\t" or char == "\n" or char == "\r":
		return True
	cat = unicodedata.category(char)
	if cat == "Zs":
		return True
	return False

这里把category为Zs的字符以及空格、tab、换行和回车当成whitespace。然后是_tokenize_chinese_chars,用于切分中文,这里的中文分词很简单,就是切分成一个一个的汉字。也就是在中文字符的前后加上空格,这样后续的分词流程会把没一个字符当成一个词。

def _tokenize_chinese_chars(self, text): 
  output = []
  for char in text:
  cp = ord(char)
  if self._is_chinese_char(cp):
	  output.append(" ")
	  output.append(char)
	  output.append(" ")
  else:
	  output.append(char)
  return "".join(output)

这里的关键是调用_is_chinese_char函数,这个函数用于判断一个unicode字符是否中文字符。

    def _is_chinese_char(self, cp):
        if ((cp >= 0x4E00 and cp <= 0x9FFF) or  #
		  (cp >= 0x3400 and cp <= 0x4DBF) or  #
		  (cp >= 0x20000 and cp <= 0x2A6DF) or  #
		  (cp >= 0x2A700 and cp <= 0x2B73F) or  #
		  (cp >= 0x2B740 and cp <= 0x2B81F) or  #
		  (cp >= 0x2B820 and cp <= 0x2CEAF) or
		  (cp >= 0xF900 and cp <= 0xFAFF) or  #
		  (cp >= 0x2F800 and cp <= 0x2FA1F)):  #
        return True

        return False

很多网上的判断汉字的正则表达式都只包括4E00-9FA5,但这是不全的,比如  就不再这个范围内。读者可以参考这里

接下来是使用whitespace进行分词,这是通过函数whitespace_tokenize来实现的。它直接调用split函数来实现分词。Python里whitespace包括’\t\n\x0b\x0c\r ‘。然后遍历每一个词,如果需要变成小写,那么先用lower()函数变成小写,接着调用_run_strip_accents函数去除accent。它的代码为:

def _run_strip_accents(self, text):
  text = unicodedata.normalize("NFD", text)
  output = []
  for char in text:
	  cat = unicodedata.category(char)
	  if cat == "Mn":
		  continue
	  output.append(char)
  return "".join(output)

它首先调用unicodedata.normalize(“NFD”, text)对text进行归一化。这个函数有什么作用呢?我们先看一下下面的代码:

>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1, s2
('café', 'café')
>>> len(s1), len(s2)
(4, 5)
>>> s1 == s2
False

我们”看到”的é其实可以有两种表示方法,一是用一个codepoint直接表示”é”,另外一种是用”e”再加上特殊的codepoint U+0301两个字符来表示。U+0301是COMBINING ACUTE ACCENT,它跟在e之后就变成了”é”。类似的”a\u0301”显示出来就是”á”。注意:这只是打印出来一模一样而已,但是在计算机内部的表示它们完全不同的,前者é是一个codepoint,值为0xe9,而后者是两个codepoint,分别是0x65和0x301。unicodedata.normalize(“NFD”, text)就会把0xe9变成0x65和0x301,比如下面的测试代码。

接下来遍历每一个codepoint,把category为Mn的去掉,比如前面的U+0301,COMBINING ACUTE ACCENT就会被去掉。category为Mn的所有Unicode字符完整列表在这里

s = unicodedata.normalize("NFD", "é")
for c in s:
	print("%#x" %(ord(c)))
	
# 输出为:
0x65
0x301	

处理完大小写和accent之后得到的Token通过函数_run_split_on_punc再次用标点切分。这个函数会对输入字符串用标点进行切分,返回一个list,list的每一个元素都是一个char。比如输入he’s,则输出是[[h,e], [’],[s]]。代码很简单,这里就不赘述。里面它会调用函数_is_punctuation来判断一个字符是否标点。

def _is_punctuation(char): 
	cp = ord(char)
	# 我们把ASCII里非字母数字都当成标点。
	# 在Unicode的category定义里,  "^", "$", and "`" 等都不是标点,但是我们这里都认为是标点。
	if ((cp >= 33 and cp <= 47) or (cp >= 58 and cp <= 64) or
			(cp >= 91 and cp <= 96) or (cp >= 123 and cp <= 126)):
		return True
	cat = unicodedata.category(char)
	# category是P开头的都是标点,参考https://en.wikipedia.org/wiki/Unicode_character_property
	if cat.startswith("P"):
		return True
	return False

(二) WordpieceTokenizer

WordpieceTokenizer的作用是把词再切分成更细粒度的WordPiece。WordPiece(Byte Pair Encoding)是一种解决OOV问题的方法,如果不管细节,我们把它看成比词更小的基本单位就行。对于中文来说,WordpieceTokenizer什么也不干,因为之前的分词已经是基于字符的了。有兴趣的读者可以参考这个开源项目。一般情况我们不需要自己重新生成WordPiece,使用BERT模型里自带的就行。

WordpieceTokenizer的代码为:

def tokenize(self, text):
  
  # 把一段文字切分成word piece。这其实是贪心的最大正向匹配算法。
  # 比如:
  # input = "unaffable"
  # output = ["un", "##aff", "##able"]
 
  
  text = convert_to_unicode(text)
  
  output_tokens = []
  for token in whitespace_tokenize(text):
	  chars = list(token)
	  if len(chars) > self.max_input_chars_per_word:
		  output_tokens.append(self.unk_token)
		  continue
	  
	  is_bad = False
	  start = 0
	  sub_tokens = []
	  while start < len(chars):
		  end = len(chars)
		  cur_substr = None
		  while start < end:
			  substr = "".join(chars[start:end])
			  if start > 0:
				  substr = "##" + substr
			  if substr in self.vocab:
				  cur_substr = substr
				  break
			  end -= 1
		  if cur_substr is None:
			  is_bad = True
			  break
		  sub_tokens.append(cur_substr)
		  start = end
	  
	  if is_bad:
		  output_tokens.append(self.unk_token)
	  else:
		  output_tokens.extend(sub_tokens)
  return output_tokens

代码有点长,但是很简单,就是贪心的最大正向匹配。其实为了加速,是可以把词典加载到一个Double Array Trie里的。我们用一个例子来看代码的执行过程。比如假设输入是”unaffable”。我们跳到while循环部分,这是start=0,end=len(chars)=9,也就是先看看unaffable在不在词典里,如果在,那么直接作为一个WordPiece,如果不再,那么end-=1,也就是看unaffabl在不在词典里,最终发现”un”在词典里,把un加到结果里。

接着start=2,看affable在不在,不在再看affabl,…,最后发现 ##aff 在词典里。注意:##表示这个词是接着前面的,这样使得WordPiece切分是可逆的——我们可以恢复出“真正”的词。

七、run_classifier.py的main函数

main函数的主要代码为:

main()

  bert_config = modeling.BertConfig.from_json_file(FLAGS.bert_config_file)

  task_name = FLAGS.task_name.lower()
  processor = processors[task_name]()
  label_list = processor.get_labels()
  
  tokenizer = tokenization.FullTokenizer(
	  vocab_file=FLAGS.vocab_file, do_lower_case=FLAGS.do_lower_case)
  
  run_config = tf.contrib.tpu.RunConfig(
	  cluster=tpu_cluster_resolver,
	  master=FLAGS.master,
	  model_dir=FLAGS.output_dir,
	  save_checkpoints_steps=FLAGS.save_checkpoints_steps,
	  tpu_config=tf.contrib.tpu.TPUConfig(
		  iterations_per_loop=FLAGS.iterations_per_loop,
		  num_shards=FLAGS.num_tpu_cores,
		  per_host_input_for_training=is_per_host))
  
  train_examples = None
  num_train_steps = None
  num_warmup_steps = None
  if FLAGS.do_train:
	  train_examples = processor.get_train_examples(FLAGS.data_dir)
	  num_train_steps = int(
			      len(train_examples) / FLAGS.train_batch_size * FLAGS.num_train_epochs)
	  num_warmup_steps = int(num_train_steps * FLAGS.warmup_proportion)
  
  model_fn = model_fn_builder(
	  bert_config=bert_config,
	  num_labels=len(label_list),
	  init_checkpoint=FLAGS.init_checkpoint,
	  learning_rate=FLAGS.learning_rate,
	  num_train_steps=num_train_steps,
	  num_warmup_steps=num_warmup_steps,
	  use_tpu=FLAGS.use_tpu,
	  use_one_hot_embeddings=FLAGS.use_tpu)
  
  # 如果没有TPU,那么会使用GPU或者CPU
  estimator = tf.contrib.tpu.TPUEstimator(
	  use_tpu=FLAGS.use_tpu,
	  model_fn=model_fn,
	  config=run_config,
	  train_batch_size=FLAGS.train_batch_size,
	  eval_batch_size=FLAGS.eval_batch_size,
	  predict_batch_size=FLAGS.predict_batch_size)
  
  if FLAGS.do_train:
	  train_file = os.path.join(FLAGS.output_dir, "train.tf_record")
	  file_based_convert_examples_to_features(
		  train_examples, label_list, FLAGS.max_seq_length, tokenizer, train_file)

	  train_input_fn = file_based_input_fn_builder(
		  input_file=train_file,
		  seq_length=FLAGS.max_seq_length,
		  is_training=True,
		  drop_remainder=True)
	  estimator.train(input_fn=train_input_fn, max_steps=num_train_steps)
  
  if FLAGS.do_eval:
	  eval_examples = processor.get_dev_examples(FLAGS.data_dir)
	  eval_file = os.path.join(FLAGS.output_dir, "eval.tf_record")
	  file_based_convert_examples_to_features(
			  eval_examples, label_list, FLAGS.max_seq_length, tokenizer, eval_file)

	  
	  # This tells the estimator to run through the entire set.
	  eval_steps = None
	  
	  eval_drop_remainder = True if FLAGS.use_tpu else False
	  eval_input_fn = file_based_input_fn_builder(
		  input_file=eval_file,
		  seq_length=FLAGS.max_seq_length,
		  is_training=False,
		  drop_remainder=eval_drop_remainder)
	  
	  result = estimator.evaluate(input_fn=eval_input_fn, steps=eval_steps)
  
  
  if FLAGS.do_predict:
	  predict_examples = processor.get_test_examples(FLAGS.data_dir)
	  predict_file = os.path.join(FLAGS.output_dir, "predict.tf_record")
	  file_based_convert_examples_to_features(predict_examples, label_list,
			  FLAGS.max_seq_length, tokenizer, predict_file)
	 
	  predict_drop_remainder = True if FLAGS.use_tpu else False
	  predict_input_fn = file_based_input_fn_builder(
		  input_file=predict_file,
		  seq_length=FLAGS.max_seq_length,
		  is_training=False,
		  drop_remainder=predict_drop_remainder)
	  
	  result = estimator.predict(input_fn=predict_input_fn)
	 

这里使用的是Tensorflow的Estimator API,这里只介绍训练部分的代码。

首先是通过file_based_convert_examples_to_features函数把输入的tsv文件变成TFRecord文件,便于Tensorflow处理。

    train_file = os.path.join(FLAGS.output_dir, "train.tf_record")
    file_based_convert_examples_to_features(
		    train_examples, label_list, FLAGS.max_seq_length, tokenizer, train_file)

def file_based_convert_examples_to_features(
				examples, label_list, max_seq_length, tokenizer, output_file):

	writer = tf.python_io.TFRecordWriter(output_file)
	
	for (ex_index, example) in enumerate(examples):
	 
		feature = convert_single_example(ex_index, example, label_list,
				max_seq_length, tokenizer)
		
		def create_int_feature(values):
			f = tf.train.Feature(int64_list=tf.train.Int64List(value=list(values)))
			return f
		
		features = collections.OrderedDict()
		features["input_ids"] = create_int_feature(feature.input_ids)
		features["input_mask"] = create_int_feature(feature.input_mask)
		features["segment_ids"] = create_int_feature(feature.segment_ids)
		features["label_ids"] = create_int_feature([feature.label_id])
		
		tf_example = tf.train.Example(features=tf.train.Features(feature=features))
		writer.write(tf_example.SerializeToString())

file_based_convert_examples_to_features函数遍历每一个example(InputExample类的对象)。然后使用convert_single_example函数把每个InputExample对象变成InputFeature。InputFeature就是一个存放特征的对象,它包括input_ids、input_mask、segment_ids和label_id,这4个属性除了label_id是一个int之外,其它都是int的列表,因此使用create_int_feature函数把它变成tf.train.Feature,而label_id需要构造一个只有一个元素的列表,最后构造tf.train.Example对象,然后写到TFRecord文件里。后面Estimator的input_fn会用到它。

这里的最关键是convert_single_example函数,读懂了它就真正明白BERT把输入表示成向量的过程,所以请读者仔细阅读代码和其中的注释。

def convert_single_example(ex_index, example, label_list, max_seq_length,
				tokenizer):
	"""把一个`InputExample`对象变成`InputFeatures`."""
	# label_map把label变成id,这个函数每个example都需要执行一次,其实是可以优化的。
	# 只需要在可以再外面执行一次传入即可。
	label_map = {}
	for (i, label) in enumerate(label_list):
		label_map[label] = i
	
	tokens_a = tokenizer.tokenize(example.text_a)
	tokens_b = None
	if example.text_b:
		tokens_b = tokenizer.tokenize(example.text_b)
	
	if tokens_b:
		# 如果有b,那么需要保留3个特殊Token[CLS], [SEP]和[SEP]
		# 如果两个序列加起来太长,就需要去掉一些。
		_truncate_seq_pair(tokens_a, tokens_b, max_seq_length - 3)
	else:
		# 没有b则只需要保留[CLS]和[SEP]两个特殊字符
		# 如果Token太多,就直接截取掉后面的部分。
		if len(tokens_a) > max_seq_length - 2:
			tokens_a = tokens_a[0:(max_seq_length - 2)]
	
	# BERT的约定是:
	# (a) 对于两个序列:
	#  tokens:   [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
	#  type_ids: 0     0  0    0    0     0       0 0     1  1  1  1   1 1
	# (b) 对于一个序列:
	#  tokens:   [CLS] the dog is hairy . [SEP]
	#  type_ids: 0     0   0   0  0     0 0
	#
	# 这里"type_ids"用于区分一个Token是来自第一个还是第二个序列
	# 对于type=0和type=1,模型会学习出两个Embedding向量。
	# 虽然理论上这是不必要的,因为[SEP]隐式的确定了它们的边界。
	# 但是实际加上type后,模型能够更加容易的知道这个词属于那个序列。
	#
	# 对于分类任务,[CLS]对应的向量可以被看成 "sentence vector"
	# 注意:一定需要Fine-Tuning之后才有意义
	tokens = []
	segment_ids = []
	tokens.append("[CLS]")
	segment_ids.append(0)
	for token in tokens_a:
		tokens.append(token)
		segment_ids.append(0)
		tokens.append("[SEP]")
		segment_ids.append(0)
	
	if tokens_b:
		for token in tokens_b:
			tokens.append(token)
			segment_ids.append(1)
		tokens.append("[SEP]")
		segment_ids.append(1)
	
	input_ids = tokenizer.convert_tokens_to_ids(tokens)
	
	# mask是1表示是"真正"的Token,0则是Padding出来的。在后面的Attention时会通过tricky的技巧让
	# 模型不能attend to这些padding出来的Token上。
	input_mask = [1] * len(input_ids)
	
	# padding使得序列长度正好等于max_seq_length
	while len(input_ids) < max_seq_length:
		input_ids.append(0)
		input_mask.append(0)
		segment_ids.append(0)
 
	label_id = label_map[example.label]
	
	feature = InputFeatures(
		input_ids=input_ids,
		input_mask=input_mask,
		segment_ids=segment_ids,
		label_id=label_id)
	return feature

如果两个Token序列的长度太长,那么需要去掉一些,这会用到_truncate_seq_pair函数:

def _truncate_seq_pair(tokens_a, tokens_b, max_length):
	while True:
		total_length = len(tokens_a) + len(tokens_b)
		if total_length <= max_length:
			break
		if len(tokens_a) > len(tokens_b):
			tokens_a.pop()
		else:
			tokens_b.pop()

这个函数很简单,如果两个序列的长度小于max_length,那么不用truncate,否则在tokens_a和tokens_b中选择长的那个序列来pop掉最后面的那个Token,这样的结果是使得两个Token序列一样长(或者最多a比b多一个Token)。对于Estimator API来说,最重要的是实现model_fn和input_fn。我们先看input_fn,它是由file_based_input_fn_builder构造出来的。代码如下:

def file_based_input_fn_builder(input_file, seq_length, is_training,
			drop_remainder):
 
	name_to_features = {
		"input_ids": tf.FixedLenFeature([seq_length], tf.int64),
		"input_mask": tf.FixedLenFeature([seq_length], tf.int64),
		"segment_ids": tf.FixedLenFeature([seq_length], tf.int64),
		"label_ids": tf.FixedLenFeature([], tf.int64),
	}
	
	def _decode_record(record, name_to_features):
		# 把record decode成TensorFlow example.
		example = tf.parse_single_example(record, name_to_features)
		
		# tf.Example只支持tf.int64,但是TPU只支持tf.int32.
		# 因此我们把所有的int64变成int32.
		for name in list(example.keys()):
			t = example[name]
			if t.dtype == tf.int64:
				t = tf.to_int32(t)
			example[name] = t
		
		return example
	
	def input_fn(params): 
		batch_size = params["batch_size"]
		
		# 对于训练来说,我们会重复的读取和shuffling 
		# 对于验证和测试,我们不需要shuffling和并行读取。
		d = tf.data.TFRecordDataset(input_file)
		if is_training:
			d = d.repeat()
			d = d.shuffle(buffer_size=100)
		
		d = d.apply(
				tf.contrib.data.map_and_batch(
					lambda record: _decode_record(record, name_to_features),
					batch_size=batch_size,
					drop_remainder=drop_remainder))
		
		return d
	
	return input_fn

这个函数返回一个函数input_fn。这个input_fn函数首先从文件得到TFRecordDataset,然后根据是否训练来shuffle和重复读取。然后用applay函数对每一个TFRecord进行map_and_batch,调用_decode_record函数对record进行parsing。从而把TFRecord的一条Record变成tf.Example对象,这个对象包括了input_ids等4个用于训练的Tensor。

接下来是model_fn_builder,它用于构造Estimator使用的model_fn。下面是它的主要代码(一些无关的log和TPU相关代码去掉了):

def model_fn_builder(bert_config, num_labels, init_checkpoint, learning_rate,
				num_train_steps, num_warmup_steps, use_tpu,
				use_one_hot_embeddings): 
	# 注意:在model_fn的设计里,features表示输入(特征),而labels表示输出
	# 但是这里的实现有点不好,把label也放到了features里。
	def model_fn(features, labels, mode, params): 
		input_ids = features["input_ids"]
		input_mask = features["input_mask"]
		segment_ids = features["segment_ids"]
		label_ids = features["label_ids"]
		
		is_training = (mode == tf.estimator.ModeKeys.TRAIN)
		
		# 创建Transformer模型,这是最主要的代码。
		(total_loss, per_example_loss, logits, probabilities) = create_model(
			bert_config, is_training, input_ids, input_mask, segment_ids, label_ids,
			num_labels, use_one_hot_embeddings)
		
		tvars = tf.trainable_variables()
		
		# 从checkpoint恢复参数
		if init_checkpoint: 
			(assignment_map, initialized_variable_names) = 	
				modeling.get_assignment_map_from_checkpoint(tvars, init_checkpoint)
			
			tf.train.init_from_checkpoint(init_checkpoint, assignment_map)
		 
		
		output_spec = None
		# 构造训练的spec
		if mode == tf.estimator.ModeKeys.TRAIN:
			train_op = optimization.create_optimizer(total_loss, learning_rate, 
							num_train_steps, num_warmup_steps, use_tpu)
			
			output_spec = tf.contrib.tpu.TPUEstimatorSpec(
					mode=mode,
					loss=total_loss,
					train_op=train_op,
					scaffold_fn=scaffold_fn)
					
		# 构造eval的spec
		elif mode == tf.estimator.ModeKeys.EVAL:	
			def metric_fn(per_example_loss, label_ids, logits):
				predictions = tf.argmax(logits, axis=-1, output_type=tf.int32)
				accuracy = tf.metrics.accuracy(label_ids, predictions)
				loss = tf.metrics.mean(per_example_loss)
				return {
					"eval_accuracy": accuracy,
					"eval_loss": loss,
				}
			
			eval_metrics = (metric_fn, [per_example_loss, label_ids, logits])
			output_spec = tf.contrib.tpu.TPUEstimatorSpec(
				mode=mode,
				loss=total_loss,
				eval_metrics=eval_metrics,
				scaffold_fn=scaffold_fn)
		
		# 预测的spec
		else:
			output_spec = tf.contrib.tpu.TPUEstimatorSpec(
				mode=mode,
				predictions=probabilities,
				scaffold_fn=scaffold_fn)
		return output_spec
	
	return model_fn

这里的代码都是一些boilerplate代码,没什么可说的,最重要的是调用create_model”真正”的创建Transformer模型。下面我们来看这个函数的代码:

def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,
					labels, num_labels, use_one_hot_embeddings): 
	model = modeling.BertModel(
			config=bert_config,
			is_training=is_training,
			input_ids=input_ids,
			input_mask=input_mask,
			token_type_ids=segment_ids,
			use_one_hot_embeddings=use_one_hot_embeddings)
	
	# 在这里,我们是用来做分类,因此我们只需要得到[CLS]最后一层的输出。
	# 如果需要做序列标注,那么可以使用model.get_sequence_output()
	# 默认参数下它返回的output_layer是[8, 768]
	output_layer = model.get_pooled_output()
	
	# 默认是768
	hidden_size = output_layer.shape[-1].value
	
	
	output_weights = tf.get_variable(
		"output_weights", [num_labels, hidden_size],
		initializer=tf.truncated_normal_initializer(stddev=0.02))
	
	output_bias = tf.get_variable(
		"output_bias", [num_labels], initializer=tf.zeros_initializer())
	
	with tf.variable_scope("loss"):
		if is_training:
			# 0.1的概率会dropout
			output_layer = tf.nn.dropout(output_layer, keep_prob=0.9)
			
		# 对[CLS]输出的768的向量再做一个线性变换,输出为label的个数。得到logits
		logits = tf.matmul(output_layer, output_weights, transpose_b=True)
		logits = tf.nn.bias_add(logits, output_bias)
		probabilities = tf.nn.softmax(logits, axis=-1)
		log_probs = tf.nn.log_softmax(logits, axis=-1)
		
		one_hot_labels = tf.one_hot(labels, depth=num_labels, dtype=tf.float32)
		
		per_example_loss = -tf.reduce_sum(one_hot_labels * log_probs, axis=-1)
		loss = tf.reduce_mean(per_example_loss)
	
	return (loss, per_example_loss, logits, probabilities)

上面代码调用modeling.BertModel得到BERT模型,然后使用它的get_pooled_output方法得到[CLS]最后一层的输出,这是一个768(默认参数下)的向量,然后就是常规的接一个全连接层得到logits,然后softmax得到概率,之后就可以根据真实的分类标签计算loss。我们这时候发现关键的代码是modeling.BertModel。

八、BertModel类

这个类是最终定义模型的地方,代码比较多,我们会按照执行和调用的顺序逐个阅读。因为文字只能线性描述,但是函数的调用关系很复杂,所以建议读者对照源代码来阅读。

我们首先来看这个类的用法,把它当成黑盒。前面的create_model也用到了BertModel,这里我们在详细的介绍一下。下面的代码演示了BertModel的使用方法:

  # 假设输入已经分词并且变成WordPiece的id了 
  # 输入是[2, 3],表示batch=2,max_seq_length=3
  input_ids = tf.constant([[31, 51, 99], [15, 5, 0]])
  # 第一个例子实际长度为3,第二个例子长度为2
  input_mask = tf.constant([[1, 1, 1], [1, 1, 0]])
  # 第一个例子的3个Token中前两个属于句子1,第三个属于句子2
  # 而第二个例子的第一个Token属于句子1,第二个属于句子2(第三个是padding)
  token_type_ids = tf.constant([[0, 0, 1], [0, 1, 0]])
  
  # 创建一个BertConfig,词典大小是32000,Transformer的隐单元个数是512
  # 8个Transformer block,每个block有6个Attention Head,全连接层的隐单元是1024
  config = modeling.BertConfig(vocab_size=32000, hidden_size=512,
		  num_hidden_layers=8, num_attention_heads=6, intermediate_size=1024)

  # 创建BertModel
  model = modeling.BertModel(config=config, is_training=True,
		  input_ids=input_ids, input_mask=input_mask, token_type_ids=token_type_ids)
  
  # label_embeddings用于把512的隐单元变换成logits
  label_embeddings = tf.get_variable(...)
  # 得到[CLS]最后一层输出,把它看成句子的Embedding(Encoding)
  pooled_output = model.get_pooled_output()
  # 计算logits
  logits = tf.matmul(pooled_output, label_embeddings)

接下来我们看一下BertModel的构造函数:

def __init__(self,
		  config,
		  is_training,
		  input_ids,
		  input_mask=None,
		  token_type_ids=None,
		  use_one_hot_embeddings=True,
		  scope=None): 

  # Args:
  #       config: `BertConfig` 对象
  #       is_training: bool 表示训练还是eval,是会影响dropout
  #	  input_ids: int32 Tensor  shape是[batch_size, seq_length]
  #	  input_mask: (可选) int32 Tensor shape是[batch_size, seq_length]
  #	  token_type_ids: (可选) int32 Tensor shape是[batch_size, seq_length]
  #	  use_one_hot_embeddings: (可选) bool
  #		  如果True,使用矩阵乘法实现提取词的Embedding;否则用tf.embedding_lookup()
  #		  对于TPU,使用前者更快,对于GPU和CPU,后者更快。
  #	  scope: (可选) 变量的scope。默认是"bert"
  
  # Raises:
  #	  ValueError: 如果config或者输入tensor的shape有问题就会抛出这个异常

  config = copy.deepcopy(config)
  if not is_training:
	  config.hidden_dropout_prob = 0.0
	  config.attention_probs_dropout_prob = 0.0
  
  input_shape = get_shape_list(input_ids, expected_rank=2)
  batch_size = input_shape[0]
  seq_length = input_shape[1]
  
  if input_mask is None:
	  input_mask = tf.ones(shape=[batch_size, seq_length], dtype=tf.int32)
  
  if token_type_ids is None:
	  token_type_ids = tf.zeros(shape=[batch_size, seq_length], dtype=tf.int32)
  
  with tf.variable_scope(scope, default_name="bert"):
	  with tf.variable_scope("embeddings"):
		  # 词的Embedding lookup 
		  (self.embedding_output, self.embedding_table) = embedding_lookup(
				  input_ids=input_ids,
				  vocab_size=config.vocab_size,
				  embedding_size=config.hidden_size,
				  initializer_range=config.initializer_range,
				  word_embedding_name="word_embeddings",
				  use_one_hot_embeddings=use_one_hot_embeddings)
		  
		  # 增加位置embeddings和token type的embeddings,然后是
		  # layer normalize和dropout。
		  self.embedding_output = embedding_postprocessor(
				  input_tensor=self.embedding_output,
				  use_token_type=True,
				  token_type_ids=token_type_ids,
				  token_type_vocab_size=config.type_vocab_size,
				  token_type_embedding_name="token_type_embeddings",
				  use_position_embeddings=True,
				  position_embedding_name="position_embeddings",
				  initializer_range=config.initializer_range,
				  max_position_embeddings=config.max_position_embeddings,
				  dropout_prob=config.hidden_dropout_prob)
	  
	  with tf.variable_scope("encoder"):
		  # 把shape为[batch_size, seq_length]的2D mask变成
		  # shape为[batch_size, seq_length, seq_length]的3D mask
		  # 以便后向的attention计算,读者可以对比之前的Transformer的代码。
		  attention_mask = create_attention_mask_from_input_mask(
				  input_ids, input_mask)
		  
		  # 多个Transformer模型stack起来。
		  # all_encoder_layers是一个list,长度为num_hidden_layers(默认12),每一层对应一个值。
		  # 每一个值都是一个shape为[batch_size, seq_length, hidden_size]的tensor。
		  
		  self.all_encoder_layers = transformer_model(
			  input_tensor=self.embedding_output,
			  attention_mask=attention_mask,
			  hidden_size=config.hidden_size,
			  num_hidden_layers=config.num_hidden_layers,
			  num_attention_heads=config.num_attention_heads,
			  intermediate_size=config.intermediate_size,
			  intermediate_act_fn=get_activation(config.hidden_act),
			  hidden_dropout_prob=config.hidden_dropout_prob,
			  attention_probs_dropout_prob=config.attention_probs_dropout_prob,
			  initializer_range=config.initializer_range,
			  do_return_all_layers=True)
	  
	  # `sequence_output` 是最后一层的输出,shape是[batch_size, seq_length, hidden_size]
	  self.sequence_output = self.all_encoder_layers[-1]

	  with tf.variable_scope("pooler"):
		  # 取最后一层的第一个时刻[CLS]对应的tensor
		  # 从[batch_size, seq_length, hidden_size]变成[batch_size, hidden_size]
		  # sequence_output[:, 0:1, :]得到的是[batch_size, 1, hidden_size]
		  # 我们需要用squeeze把第二维去掉。
		  first_token_tensor = tf.squeeze(self.sequence_output[:, 0:1, :], axis=1)
		  # 然后再加一个全连接层,输出仍然是[batch_size, hidden_size]
		  self.pooled_output = tf.layers.dense(
				  first_token_tensor,
				  config.hidden_size,
				  activation=tf.tanh,
				  kernel_initializer=create_initializer(config.initializer_range))


代码很长,但是其实很简单。首先是对config(BertConfig对象)深度拷贝一份,如果不是训练,那么把dropout都置为零。如果输入的input_mask为None,那么构造一个shape合适值全为1的input_mask,这表示输入都是”真实”的输入,没有padding的内容。如果token_type_ids为None,那么构造一个shape合适并且值全为0的tensor,表示所有Token都属于第一个句子。

然后使用embedding_lookup函数构造词的Embedding,用embedding_postprocessor函数增加位置embeddings和token type的embeddings,然后是layer normalize和dropout。

接着用transformer_model函数构造多个Transformer SubLayer然后stack在一起。得到的all_encoder_layers是一个list,长度为num_hidden_layers(默认12),每一层对应一个值。 每一个值都是一个shape为[batch_size, seq_length, hidden_size]的tensor。

self.sequence_output是最后一层的输出,shape是[batch_size, seq_length, hidden_size]。first_token_tensor是第一个Token([CLS])最后一层的输出,shape是[batch_size, hidden_size]。最后对self.sequence_output再加一个线性变换,得到的tensor仍然是[batch_size, hidden_size]。

embedding_lookup函数用于实现Embedding,它有两种方式:使用tf.nn.embedding_lookup和矩阵乘法(one_hot_embedding=True)。前者适合于CPU与GPU,后者适合于TPU。所谓的one-hot方法是把输入id表示成one-hot的向量,当然输入id序列就变成了one-hot的矩阵,然后乘以Embedding矩阵。而tf.nn.embedding_lookup是直接用id当下标提取Embedding矩阵对应的向量。一般认为tf.nn.embedding_lookup更快一点,但是TPU上似乎不是这样,作者也不太了解原因是什么,猜测可能是TPU的没有快捷的办法提取矩阵的某一行/列?

def embedding_lookup(input_ids,
			vocab_size,
			embedding_size=128,
			initializer_range=0.02,
			word_embedding_name="word_embeddings",
			use_one_hot_embeddings=False):
	"""word embedding
	
	Args:
		input_ids: int32 Tensor shape为[batch_size, seq_length],表示WordPiece的id
		vocab_size: int 词典大小,需要于vocab.txt一致 
		embedding_size: int embedding后向量的大小 
		initializer_range: float 随机初始化的范围 
		word_embedding_name: string 名字,默认是"word_embeddings"
		use_one_hot_embeddings: bool 如果True,使用one-hot方法实现embedding;否则使用 		
			`tf.nn.embedding_lookup()`. TPU适合用One hot方法。
	
	Returns:
		float Tensor shape为[batch_size, seq_length, embedding_size]
	"""
	# 这个函数假设输入的shape是[batch_size, seq_length, num_inputs]
	# 普通的Embeding一般假设输入是[batch_size, seq_length],
	# 增加num_inputs这一维度的目的是为了一次计算更多的Embedding
	# 但目前的代码并没有用到,传入的input_ids都是2D的,这增加了代码的阅读难度。
	
	# 如果输入是[batch_size, seq_length],
	# 那么我们把它 reshape成[batch_size, seq_length, 1]
	if input_ids.shape.ndims == 2:
		input_ids = tf.expand_dims(input_ids, axis=[-1])
	
	# 构造Embedding矩阵,shape是[vocab_size, embedding_size]
	embedding_table = tf.get_variable(
		name=word_embedding_name,
		shape=[vocab_size, embedding_size],
		initializer=create_initializer(initializer_range))
	
	if use_one_hot_embeddings:
		flat_input_ids = tf.reshape(input_ids, [-1])
		one_hot_input_ids = tf.one_hot(flat_input_ids, depth=vocab_size)
		output = tf.matmul(one_hot_input_ids, embedding_table)
	else:
		output = tf.nn.embedding_lookup(embedding_table, input_ids)
	
	input_shape = get_shape_list(input_ids)
	# 把输出从[batch_size, seq_length, num_inputs(这里总是1), embedding_size]
	# 变成[batch_size, seq_length, num_inputs*embedding_size]
	output = tf.reshape(output,
				input_shape[0:-1] + [input_shape[-1] * embedding_size])
	return (output, embedding_table)

Embedding本来很简单,使用tf.nn.embedding_lookup就行了。但是为了优化TPU,它还支持使用矩阵乘法来提取词向量。另外为了提高效率,输入的shape除了[batch_size, seq_length]外,它还增加了一个维度变成[batch_size, seq_length, num_inputs]。如果不关心细节,我们把这个函数当成黑盒,那么我们只需要知道它的输入input_ids(可能)是[8, 128],输出是[8, 128, 768]就可以了。

函数embedding_postprocessor的代码如下,需要注意的部分都有注释。

def embedding_postprocessor(input_tensor,
				use_token_type=False,
				token_type_ids=None,
				token_type_vocab_size=16,
				token_type_embedding_name="token_type_embeddings",
				use_position_embeddings=True,
				position_embedding_name="position_embeddings",
				initializer_range=0.02,
				max_position_embeddings=512,
				dropout_prob=0.1):
	"""对word embedding之后的tensor进行后处理
	
	Args:
		input_tensor: float Tensor shape为[batch_size, seq_length, embedding_size]
		use_token_type: bool 是否增加`token_type_ids`的Embedding
		token_type_ids: (可选) int32 Tensor shape为[batch_size, seq_length]
			如果`use_token_type`为True则必须有值
		token_type_vocab_size: int Token Type的个数,通常是2
		token_type_embedding_name: string Token type Embedding的名字
		use_position_embeddings: bool 是否使用位置Embedding
		position_embedding_name: string,位置embedding的名字 
		initializer_range: float,初始化范围 
		max_position_embeddings: int,位置编码的最大长度,可以比最大序列长度大,但是不能比它小。
		dropout_prob: float. Dropout 概率
		
	Returns:
		float tensor  shape和`input_tensor`相同。
	 
	"""
	input_shape = get_shape_list(input_tensor, expected_rank=3)
	batch_size = input_shape[0]
	seq_length = input_shape[1]
	width = input_shape[2]
	
	if seq_length > max_position_embeddings:
		raise ValueError("The seq length (%d) cannot be greater than "
			"`max_position_embeddings` (%d)" %
					(seq_length, max_position_embeddings))
	
	output = input_tensor
	
	if use_token_type:
		if token_type_ids is None:
			raise ValueError("`token_type_ids` must be specified if"
				"`use_token_type` is True.")
		token_type_table = tf.get_variable(
				name=token_type_embedding_name,
				shape=[token_type_vocab_size, width],
				initializer=create_initializer(initializer_range))
		# 因为Token Type通常很小(2),所以直接用矩阵乘法(one-hot)更快
		flat_token_type_ids = tf.reshape(token_type_ids, [-1])
		one_hot_ids = tf.one_hot(flat_token_type_ids, depth=token_type_vocab_size)
		token_type_embeddings = tf.matmul(one_hot_ids, token_type_table)
		token_type_embeddings = tf.reshape(token_type_embeddings,
				[batch_size, seq_length, width])
		output += token_type_embeddings
	
	if use_position_embeddings:
		full_position_embeddings = tf.get_variable(
					name=position_embedding_name,
					shape=[max_position_embeddings, width],
					initializer=create_initializer(initializer_range))
		# 位置Embedding是可以学习的参数,因此我们创建一个[max_position_embeddings, width]的矩阵
		# 但实际输入的序列可能并不会到max_position_embeddings(512),为了提高训练速度,
		# 我们通过tf.slice取出[0, 1, 2, ..., seq_length-1]的部分,。
		if seq_length < max_position_embeddings:
			position_embeddings = tf.slice(full_position_embeddings, [0, 0],
					[seq_length, -1])
		else:
			position_embeddings = full_position_embeddings
		
		num_dims = len(output.shape.as_list())
		
		# word embedding之后的tensor是[batch_size, seq_length, width]
		# 因为位置编码是与输入内容无关,它的shape总是[seq_length, width]
		# 我们无法把位置Embedding加到word embedding上
		# 因此我们需要扩展位置编码为[1, seq_length, width]
		# 然后就能通过broadcasting加上去了。
		position_broadcast_shape = []
		for _ in range(num_dims - 2):
			position_broadcast_shape.append(1)
		position_broadcast_shape.extend([seq_length, width])
		# 默认情况下position_broadcast_shape为[1, 128, 768]
		position_embeddings = tf.reshape(position_embeddings,
			position_broadcast_shape)
		# output是[8, 128, 768], position_embeddings是[1, 128, 768]
		# 因此可以通过broadcasting相加。
		output += position_embeddings
	
	output = layer_norm_and_dropout(output, dropout_prob)
	return output

create_attention_mask_from_input_mask函数用于构造Mask矩阵。我们先了解一下它的作用然后再阅读其代码。比如调用它时的两个参数是是:

input_ids=[
	[1,2,3,0,0],
	[1,3,5,6,1]
]
input_mask=[
	[1,1,1,0,0],
	[1,1,1,1,1]
]

表示这个batch有两个样本,第一个样本长度为3(padding了2个0),第二个样本长度为5。在计算Self-Attention的时候每一个样本都需要一个Attention Mask矩阵,表示每一个时刻可以attend to的范围,1表示可以attend,0表示是padding的(或者在机器翻译的Decoder中不能attend to未来的词)。对于上面的输入,这个函数返回一个shape是[2, 5, 5]的tensor,分别代表两个Attention Mask矩阵。

[
	[1, 1, 1, 0, 0], #它表示第1个词可以attend to 3个词
	[1, 1, 1, 0, 0], #它表示第2个词可以attend to 3个词
	[1, 1, 1, 0, 0], #它表示第3个词可以attend to 3个词
	[1, 1, 1, 0, 0], #无意义,因为输入第4个词是padding的0
	[1, 1, 1, 0, 0]  #无意义,因为输入第5个词是padding的0
]

[
	[1, 1, 1, 1, 1], # 它表示第1个词可以attend to 5个词
	[1, 1, 1, 1, 1], # 它表示第2个词可以attend to 5个词
	[1, 1, 1, 1, 1], # 它表示第3个词可以attend to 5个词
	[1, 1, 1, 1, 1], # 它表示第4个词可以attend to 5个词
	[1, 1, 1, 1, 1]	 # 它表示第5个词可以attend to 5个词
]

了解了它的用途之后下面的代码就很好理解了。

def create_attention_mask_from_input_mask(from_tensor, to_mask):
	"""Create 3D attention mask from a 2D tensor mask.
	
	Args:
		from_tensor: 2D or 3D Tensor,shape为[batch_size, from_seq_length, ...].
		to_mask: int32 Tensor, shape为[batch_size, to_seq_length].
	
	Returns:
		float Tensor,shape为[batch_size, from_seq_length, to_seq_length].
	"""
	from_shape = get_shape_list(from_tensor, expected_rank=[2, 3])
	batch_size = from_shape[0]
	from_seq_length = from_shape[1]
	
	to_shape = get_shape_list(to_mask, expected_rank=2)
	to_seq_length = to_shape[1]
	
	to_mask = tf.cast(
		tf.reshape(to_mask, [batch_size, 1, to_seq_length]), tf.float32)
	
	# `broadcast_ones` = [batch_size, from_seq_length, 1]
	broadcast_ones = tf.ones(
		shape=[batch_size, from_seq_length, 1], dtype=tf.float32)
	
	# Here we broadcast along two dimensions to create the mask.
	mask = broadcast_ones * to_mask
	
	return mask

比如前面举的例子,broadcast_ones的shape是[2, 5, 1],值全是1,而to_mask是

to_mask=[
[1,1,1,0,0],
[1,1,1,1,1]
]

shape是[2, 5],reshape为[2, 1, 5]。然后broadcast_ones * to_mask就得到[2, 5, 5],正是我们需要的两个Mask矩阵,读者可以验证。注意[batch, A, B]*[batch, B, C]=[batch, A, C],我们可以认为是batch个[A, B]的矩阵乘以batch个[B, C]的矩阵。接下来就是transformer_model函数了,它就是构造Transformer的核心代码。

def transformer_model(input_tensor,
      attention_mask=None,
      hidden_size=768,
      num_hidden_layers=12,
      num_attention_heads=12,
      intermediate_size=3072,
      intermediate_act_fn=gelu,
      hidden_dropout_prob=0.1,
      attention_probs_dropout_prob=0.1,
      initializer_range=0.02,
      do_return_all_layers=False):
  """Multi-headed, multi-layer的Transformer,参考"Attention is All You Need".
  
  这基本上是和原始Transformer encoder相同的代码。
  
  原始论文为:
  https://arxiv.org/abs/1706.03762
  
  Also see:
  https://github.com/tensorflow/tensor2tensor/blob/master/tensor2tensor/models/transformer.py
  
  Args:
    input_tensor: float Tensor,shape为[batch_size, seq_length, hidden_size]
    attention_mask: (可选) int32 Tensor,shape [batch_size, seq_length,
      seq_length], 1表示可以attend to,0表示不能。 
    hidden_size: int. Transformer隐单元个数
    num_hidden_layers: int. 有多少个SubLayer 
    num_attention_heads: int. Transformer Attention Head个数。
    intermediate_size: int. 全连接层的隐单元个数
    intermediate_act_fn: 函数. 全连接层的激活函数。
    hidden_dropout_prob: float. Self-Attention层残差之前的Dropout概率
    attention_probs_dropout_prob: float. attention的Dropout概率
    initializer_range: float. 初始化范围(truncated normal的标准差)
    do_return_all_layers: 返回所有层的输出还是最后一层的输出。
  
  Returns:
    如果do_return_all_layers True,返回最后一层的输出,是一个Tensor,
                shape为[batch_size, seq_length, hidden_size];
    否则返回所有层的输出,是一个长度为num_hidden_layers的list,
                list的每一个元素都是[batch_size, seq_length, hidden_size]。

  """
  if hidden_size % num_attention_heads != 0:
    raise ValueError(
      "The hidden size (%d) is not a multiple of the number of attention "
      "heads (%d)" % (hidden_size, num_attention_heads))
  
  # 因为最终要输出hidden_size,总共有num_attention_heads个Head,因此每个Head输出
  # 为hidden_size / num_attention_heads
  attention_head_size = int(hidden_size / num_attention_heads)
  input_shape = get_shape_list(input_tensor, expected_rank=3)
  batch_size = input_shape[0]
  seq_length = input_shape[1]
  input_width = input_shape[2]
  
  # 因为需要残差连接,我们需要把输入加到Self-Attention的输出,因此要求它们的shape是相同的。
  if input_width != hidden_size:
    raise ValueError("The width of the input tensor (%d) != hidden size (%d)" %
      (input_width, hidden_size))
  
  # 为了避免在2D和3D之间来回reshape,我们统一把所有的3D Tensor用2D来表示。
  # 虽然reshape在GPU/CPU上很快,但是在TPU上却不是这样,这样做的目的是为了优化TPU
  # input_tensor是[8, 128, 768], prev_output是[8*128, 768]=[1024, 768] 
  prev_output = reshape_to_matrix(input_tensor)
  
  all_layer_outputs = []
  for layer_idx in range(num_hidden_layers):
    # 每一层都有自己的variable scope
    with tf.variable_scope("layer_%d" % layer_idx):
      layer_input = prev_output
      # attention层
      with tf.variable_scope("attention"):
        attention_heads = []
        # self attention
        with tf.variable_scope("self"):
          attention_head = attention_layer(
            from_tensor=layer_input,
            to_tensor=layer_input,
            attention_mask=attention_mask,
            num_attention_heads=num_attention_heads,
            size_per_head=attention_head_size,
            attention_probs_dropout_prob=attention_probs_dropout_prob,
            initializer_range=initializer_range,
            do_return_2d_tensor=True,
            batch_size=batch_size,
            from_seq_length=seq_length,
            to_seq_length=seq_length)
          attention_heads.append(attention_head)
        
        attention_output = None
        if len(attention_heads) == 1:
          attention_output = attention_heads[0]
        else:
          # 如果有多个head,那么需要把多个head的输出concat起来
          attention_output = tf.concat(attention_heads, axis=-1)
      
        # 使用线性变换把前面的输出变成`hidden_size`,然后再加上`layer_input`(残差连接)
        with tf.variable_scope("output"):
          attention_output = tf.layers.dense(
              attention_output,
              hidden_size,
              kernel_initializer=create_initializer(initializer_range))
          # dropout
          attention_output = dropout(attention_output, hidden_dropout_prob)
          # 残差连接再加上layer norm。
          attention_output = layer_norm(attention_output + layer_input)
      
      # 全连接层
      with tf.variable_scope("intermediate"):
        intermediate_output = tf.layers.dense(
          attention_output,
          intermediate_size,
          activation=intermediate_act_fn,
          kernel_initializer=create_initializer(initializer_range))
      
      # 然后是用一个线性变换把大小变回`hidden_size`,这样才能加残差连接
      with tf.variable_scope("output"):
        layer_output = tf.layers.dense(
            intermediate_output,
            hidden_size,
            kernel_initializer=create_initializer(initializer_range))
        layer_output = dropout(layer_output, hidden_dropout_prob)
        layer_output = layer_norm(layer_output + attention_output)
        prev_output = layer_output
        all_layer_outputs.append(layer_output)
  
  if do_return_all_layers:
    final_outputs = []
    for layer_output in all_layer_outputs:
      final_output = reshape_from_matrix(layer_output, input_shape)
      final_outputs.append(final_output)
    return final_outputs
  else:
    final_output = reshape_from_matrix(prev_output, input_shape)
    return final_output

如果对照Transformer的论文,非常容易阅读,里面实现Self-Attention的函数就是attention_layer。

def attention_layer(from_tensor,
			to_tensor,
			attention_mask=None,
			num_attention_heads=1,
			size_per_head=512,
			query_act=None,
			key_act=None,
			value_act=None,
			attention_probs_dropout_prob=0.0,
			initializer_range=0.02,
			do_return_2d_tensor=False,
			batch_size=None,
			from_seq_length=None,
			to_seq_length=None):
	"""用`from_tensor`(作为Query)去attend to `to_tensor`(提供Key和Value)
	
	这个函数实现论文"Attention
	is all you Need"里的multi-head attention。
	如果`from_tensor`和`to_tensor`是同一个tensor,那么就实现Self-Attention。
	`from_tensor`的每个时刻都会attends to `to_tensor`,
        也就是用from的Query去乘以所有to的Key,得到weight,然后把所有to的Value加权求和起来。
	
	这个函数首先把`from_tensor`变换成一个"query" tensor,
        然后把`to_tensor`变成"key"和"value" tensors。
        总共有`num_attention_heads`组Query、Key和Value,
        每一个Query,Key和Value的shape都是[batch_size(8), seq_length(128), size_per_head(512/8=64)].
	
	然后计算query和key的内积并且除以size_per_head的平方根(8)。
        然后softmax变成概率,最后用概率加权value得到输出。
        因为有多个Head,每个Head都输出[batch_size, seq_length, size_per_head],
        最后把8个Head的结果concat起来,就最终得到[batch_size(8), seq_length(128), size_per_head*8=512] 
	
	实际上我们是把这8个Head的Query,Key和Value都放在一个Tensor里面的,
        因此实际通过transpose和reshape就达到了上面的效果。
	
	Args:
		from_tensor: float Tensor,shape [batch_size, from_seq_length, from_width]
		to_tensor: float Tensor,shape [batch_size, to_seq_length, to_width].
		attention_mask: (可选) int32 Tensor, shape[batch_size,from_seq_length,to_seq_length]。
                    值可以是0或者1,在计算attention score的时候,
                    我们会把0变成负无穷(实际是一个绝对值很大的负数),而1不变,
                    这样softmax的时候进行exp的计算,前者就趋近于零,从而间接实现Mask的功能。
		num_attention_heads: int. Attention heads的数量。
		size_per_head: int. 每个head的size
		query_act: (可选) query变换的激活函数
		key_act: (可选) key变换的激活函数
		value_act: (可选) value变换的激活函数
		attention_probs_dropout_prob: (可选) float. attention的Dropout概率。
		initializer_range: float. 初始化范围 
		do_return_2d_tensor: bool. 如果True,返回2D的Tensor其shape是
                    [batch_size * from_seq_length, num_attention_heads * size_per_head];
                    否则返回3D的Tensor其shape为[batch_size, from_seq_length, 
                                                num_attention_heads * size_per_head].
		batch_size: (可选) int. 如果输入是3D的,那么batch就是第一维,
                    但是可能3D的压缩成了2D的,所以需要告诉函数batch_size 
		from_seq_length: (可选) 同上,需要告诉函数from_seq_length
		to_seq_length: (可选) 同上,to_seq_length
	
	Returns:
		float Tensor,shape [batch_size,from_seq_length,num_attention_heads * size_per_head]。
		如果`do_return_2d_tensor`为True,则返回的shape是
                       [batch_size * from_seq_length, num_attention_heads * size_per_head].
	 
	"""
	
	def transpose_for_scores(input_tensor, batch_size, num_attention_heads,
			seq_length, width):
		output_tensor = tf.reshape(
				input_tensor, [batch_size, seq_length, num_attention_heads, width])
		
		output_tensor = tf.transpose(output_tensor, [0, 2, 1, 3])
		return output_tensor
	
	from_shape = get_shape_list(from_tensor, expected_rank=[2, 3])
	to_shape = get_shape_list(to_tensor, expected_rank=[2, 3])
	
	if len(from_shape) != len(to_shape):
		raise ValueError(
			"The rank of `from_tensor` must match the rank of `to_tensor`.")
	# 如果输入是3D的(没有压缩),那么我们可以推测出batch_size、from_seq_length和to_seq_length
	# 即使参数传入也会被覆盖。
	if len(from_shape) == 3:
		batch_size = from_shape[0]
		from_seq_length = from_shape[1]
		to_seq_length = to_shape[1]
		
	# 如果是压缩成2D的,那么一定要传入这3个参数,否则抛异常。	
	elif len(from_shape) == 2:
		if (batch_size is None or from_seq_length is None or to_seq_length is None):
			raise ValueError(
				"When passing in rank 2 tensors to attention_layer, the values "
				"for `batch_size`, `from_seq_length`, and `to_seq_length` "
				"must all be specified.")
	
	#   B = batch size (number of sequences) 默认配置是8
	#   F = `from_tensor` sequence length 默认配置是128
	#   T = `to_tensor` sequence length 默认配置是128
	#   N = `num_attention_heads` 默认配置是12
	#   H = `size_per_head` 默认配置是64
	
	# 把from和to压缩成2D的。
	# [8*128, 768]
	from_tensor_2d = reshape_to_matrix(from_tensor)
	# [8*128, 768]


![img](https://img-blog.csdnimg.cn/img_convert/c46629315de8a821a718c2a341d7303b.png)
![img](https://img-blog.csdnimg.cn/img_convert/9ea342dcf4e1f30ab6fbc024b86fa9d7.png)
![img](https://img-blog.csdnimg.cn/img_convert/a231c29e09d976cb8e7d6576a1524247.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

, 3])
		return output_tensor
	
	from_shape = get_shape_list(from_tensor, expected_rank=[2, 3])
	to_shape = get_shape_list(to_tensor, expected_rank=[2, 3])
	
	if len(from_shape) != len(to_shape):
		raise ValueError(
			"The rank of `from_tensor` must match the rank of `to_tensor`.")
	# 如果输入是3D的(没有压缩),那么我们可以推测出batch_size、from_seq_length和to_seq_length
	# 即使参数传入也会被覆盖。
	if len(from_shape) == 3:
		batch_size = from_shape[0]
		from_seq_length = from_shape[1]
		to_seq_length = to_shape[1]
		
	# 如果是压缩成2D的,那么一定要传入这3个参数,否则抛异常。	
	elif len(from_shape) == 2:
		if (batch_size is None or from_seq_length is None or to_seq_length is None):
			raise ValueError(
				"When passing in rank 2 tensors to attention_layer, the values "
				"for `batch_size`, `from_seq_length`, and `to_seq_length` "
				"must all be specified.")
	
	#   B = batch size (number of sequences) 默认配置是8
	#   F = `from_tensor` sequence length 默认配置是128
	#   T = `to_tensor` sequence length 默认配置是128
	#   N = `num_attention_heads` 默认配置是12
	#   H = `size_per_head` 默认配置是64
	
	# 把from和to压缩成2D的。
	# [8*128, 768]
	from_tensor_2d = reshape_to_matrix(from_tensor)
	# [8*128, 768]


[外链图片转存中...(img-O8EvHfnS-1715723409591)]
[外链图片转存中...(img-LUUcCpIm-1715723409592)]
[外链图片转存中...(img-0z9xCy2H-1715723409592)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值