tensor2tensor自定义问题,训练模型(bpe篇)

版权声明:本文为博主原创文章,未经博主允许不得转载。如需转载,加上原文链接即可~~ https://blog.csdn.net/hpulfc/article/details/82625217

tensor2tensor自定义问题,训练模型

上一篇:https://blog.csdn.net/hpulfc/article/details/81172498

之前一篇文章简单介绍了如何使用 google 的SubwordTokenEncoder 进行编码 数据,并进行模型的训练。今天这里记录一下如何使用自定义数据的数据以bpe分词的方式进行模型的训练。后面有完整代码,节省时间直接看就能看懂!

这里主要是先理清一下思路和基本结构,然后编写代码,总结概括,并且附上了贴心的注意事项 $_$ / ^_^ 。


首先是介绍一下tensor2tensor的数据生成的基本流程。

tensor2tensor 是一个封装较好的工具,其中数据生成和训练、解码的步骤是分开的。

如下:是一些基本的执行文件。

主要有:平均检查点的、计算bleu的、根据问题生成数据的、训练模型的、翻译的等。

这里面第一步就是要进行数据的生成,在生成数据的时候要对问题进行定义。如同下面一样:

上面截取的主要是定义了一些翻译的任务 ,是tensor2tensor 已经定义的一些翻译任务。然而这里面的有时并不符合一些需求,那么tensor2tensor 就提供了自定义任务的 功能。主要是注册一些问题。在上一篇文章中对此细节已经有所提及,这里不再重复。

那么如何定义问题呢,这里就需要了解问题的基本结构了。如下:

因为对问题的定义主要是涉及到数据生成的方面,所以就先看一下t2t_datagen.py, 就会发现下面的代码:

def main(_):
  usr_dir.import_usr_dir(FLAGS.t2t_usr_dir)

  # Calculate the list of problems to generate.
  problems = sorted(
      list(_SUPPORTED_PROBLEM_GENERATORS) + registry.list_problems())
  
  # ....省略很多....

  for problem in problems:
    set_random_seed()

    if problem in _SUPPORTED_PROBLEM_GENERATORS:
      generate_data_for_problem(problem)
    else:
      generate_data_for_registered_problem(problem)

这里有两个重要的信息 generate_data_for_problem 和  generate_data_for_registered_problem,也就是下面的函数:

def generate_data_in_process(arg):
  problem_name, data_dir, tmp_dir, task_id = arg
  problem = registry.problem(problem_name)
  problem.generate_data(data_dir, tmp_dir, task_id)


def generate_data_for_registered_problem(problem_name):
  """Generate data for a registered problem."""
  tf.logging.info("Generating data for %s.", problem_name)
  # 又省略了很多。。。
  if task_id is None and problem.multiprocess_generate:
    # ...是的,省略了
    pool.map(generate_data_in_process, args)
  else:
    problem.generate_data(data_dir, tmp_dir, task_id)

从上面的代码可以看出,生成数据的时候,主要是问题的的generate_data 函数。也就是Problem 的generate_data中的函数。对应到不同的问题上面有不同的实现,在文本到文本的问题的上面,是像下面这种方式实现的。

class Text2TextProblem(problem.Problem):

  .....

  def generate_data(self, data_dir, tmp_dir, task_id=-1):

    ......

    if self.is_generate_per_split:
      for split, paths in split_paths:
        generator_utils.generate_files(
            self._maybe_pack_examples(
                self.generate_encoded_samples(data_dir, tmp_dir, split)), paths)
    else:
      generator_utils.generate_files(
          self._maybe_pack_examples(
              self.generate_encoded_samples(
                  data_dir, tmp_dir, problem.DatasetSplit.TRAIN)), all_paths)

    generator_utils.shuffle_dataset(all_paths)

从上面的代码可以看出,主要是通过generate_encoded_samples进行生成以编码的样本,然后进行生成文件的。所以这里重要的就是 如何生成编码样本了。

对于文本到文本的问题,默认是这个样式儿滴~:

  def generate_encoded_samples(self, data_dir, tmp_dir, dataset_split):
    generator = self.generate_samples(data_dir, tmp_dir, dataset_split)
    encoder = self.get_or_create_vocab(data_dir, tmp_dir)
    return text2text_generate_encoded(generator, encoder,
                                      has_inputs=self.has_inputs)

也就是说,通过函数 text2text_generate_encoded 生成所谓的样本数据,这个 text2text_generate_encoded如下:

def text2text_generate_encoded(sample_generator,
                               vocab,
                               targets_vocab=None,
                               has_inputs=True):
  """Encode Text2Text samples from the generator with the vocab."""
  targets_vocab = targets_vocab or vocab
  for sample in sample_generator:
    if has_inputs:
      sample["inputs"] = vocab.encode(sample["inputs"])
      sample["inputs"].append(text_encoder.EOS_ID)
    sample["targets"] = targets_vocab.encode(sample["targets"])
    sample["targets"].append(text_encoder.EOS_ID)
    yield sample

主要就是通过 迭代器,获取样本数据,然后通过编码器进行编码样本数据,然后yield 出去。

也就是说关键的是要通过generate_encode_samples 函数 返回类似于下面的返回值

所以,这里关键就是 弄清楚各个函数的返回值 和参数值 的含义,然后 在定义自己数据的时候,构造出 所需要的返回值就可以了。

 插一句:如果你没有一个好的工作/学习环境,就要试图去改变。嗯!

ok  继续,根据上面的代码可以看到,generate_encoded_samples 里面的默认实现是通过 generate_sample 和 get_or_create_vocab 两个函数分别获取样本和 编码器,,然后 通过 text2text_generate_encoded 编码为所需 内容并返回。这里面的默认实现是对于一个单词表而言的,也就是说 源语言和目标语言位于同一个单词表中(注意,后面的自定义的时候有所改变), 所以只有一个encoder

首先是说encoder ,这里的encoder 主要是有/tensor2tensor/data_generators/text_encoder.py 里面的 两个个编码类的中的其中一个的实例,SubwordTextEncoder,TokenTextEncoder。 这两个类主要是用来编码和解码用,这里的编码解码使将文本装换为 对应单词表中的索引,与模型中的编码器解码器有所不同。具体应该怎么用,可以待会儿直接看后面的实现代码。

说完如何回去编码器,接下来就要说一下如何获取数据的样例,这个在默认是没有实现的,但是,我们有其他的例子可供参考,还不算太糟。获取样例主要是通过generate_samles 获取,这里参考 TranslateEndeWmtBpe32k 中的实现,如下:

@registry.register_problem
class TranslateEndeWmtBpe32k(translate.TranslateProblem):

  ... 很明显,省略了一些,..# 论小公司与大公司的区别,大神们讲讲啊

  def generate_samples(self, data_dir, tmp_dir, dataset_split):
    """Instance of token generator for the WMT en->de task, training set."""
    train = dataset_split == problem.DatasetSplit.TRAIN
    dataset_path = ("train.tok.clean.bpe.32000"
                    if train else "newstest2013.tok.bpe.32000")
    train_path = _get_wmt_ende_bpe_dataset(tmp_dir, dataset_path)

    # Vocab
    token_path = os.path.join(data_dir, self.vocab_filename)
    if not tf.gfile.Exists(token_path):
      token_tmp_path = os.path.join(tmp_dir, self.vocab_filename)
      tf.gfile.Copy(token_tmp_path, token_path)
      with tf.gfile.GFile(token_path, mode="r") as f:
        vocab_data = "<pad>\n<EOS>\n" + f.read() + "UNK\n"
      with tf.gfile.GFile(token_path, mode="w") as f:
        f.write(vocab_data)

    return text_problems.text2text_txt_iterator(train_path + ".en",
                                                train_path + ".de")

上面代码中的主要就是 最后一句话了,如下:

def txt_line_iterator(txt_path):
  """Iterate through lines of file."""
  with tf.gfile.Open(txt_path) as f:
    for line in f:
      yield line.strip()


def text2text_txt_iterator(source_txt_path, target_txt_path):
  """Yield dicts for Text2TextProblem.generate_samples from lines of files."""
  for inputs, targets in zip(
      txt_line_iterator(source_txt_path), txt_line_iterator(target_txt_path)):
    yield {"inputs": inputs, "targets": targets}

也就是说,在generate_samples 函数里面提供平行语料的路径,然后使用 text2text_txt_iterator 就能获取对应的 样本数据。

这样一来,思路就基本理顺了。那么也就有了下面的使用 bpe 方式训练模型,问题代码如下。

完整代码:在后面,有不懂的可在评论区讨论

具体解释:下面的代码主要是通过 已经给定的平行语料和单词表 进行问题的定义,也就是用来生成数据的。

主要有一下几点:

  1. 两个单词表,这里是对中英的单词表,tensor2tensor 中的英德问题是使用的一个单词表,这里使用两个。
  2. 这里使用bpe的方式进行 分词,然后进行令牌化,然而默认的tensor2tensor 是使用subwords的方式进行令牌化的。由于这里已经有自己的单词表了,所以在生成编码器的时候,只是使用了TokenTextEncoder 。
  3. 由于默认的改变了默认的编码器,所以要重新定义一下 feature_encoders 以此来说明具体使用的 哪种编码器。

然后,这里就可以进行数据训练了。如果你想看看效果和最先进的系统有哪些差距,看这里这里!!不谢~


需要注意的是,tmp 文件夹下面的平行语料文件名称和开发集名称应该和下面代码中相同,不然会有异常提醒的。

这里是建议使用 bpe 对英文进行分词,具体应该怎们分,github上面有对应的开源工具的,可以搜索subowrd,当然这里好心滴放上链接祝你 ‘一臂之力 ’。然后在分完词之后,选取频度前50000个词作为单词表即可。可以是用NLTK这个工具包,也是很好用的-_-!3!3 . 中文的话,分一下词就可以了,具体的话使用thulac ,精度和速度都比较好。

嗯 ojbk 到这应该就能训练处不错的模型了,具体的如何进行模型参数的调优,小伙伴们快来一起讨论呦!!!微信:hpulfc

另外:如何快速理清项目结构,看各模块名字,输入值,返回值,整体思考,应该不会差!!!

~~

下面是完整的代码,讲道理的,是可以直接使用的~

ENZH_BPE_DATASETS = {
    "TRAIN": "raw-train-bpe.zh-en",
    "DEV": "raw-dev-bpe.zh-en"
}


def get_enzh_bpe_dataset(directory, filename):
    train_path = os.path.join(directory, filename)
    if not (tf.gfile.Exists(train_path + ".en") and
            tf.gfile.Exists(train_path + ".zh")):
        raise Exception("there should be some training/dev data in the tmp dir.")

    return train_path



@registry.register_problem
class TranslateEnzhBpe50k(translate.TranslateProblem):
    """根据英德和英中的问题修改而来,这里是将英德的一个单词表变为中英的两个单词表来进行数据生成。"""

    @property
    def approx_vocab_size(self):
        return 50000

    @property
    def source_vocab_name(self):
        return "vocab.bpe.en.%d" % self.approx_vocab_size

    @property
    def target_vocab_name(self):
        return "vocab.bpe.zh.%d" % self.approx_vocab_size

    def get_vocab(self, data_dir, is_target=False):
        """返回的是一个encoder,单词表对应的编码器"""
        vocab_filename = os.path.join(data_dir, self.target_vocab_name if is_target else self.source_vocab_name)
        if not tf.gfile.Exists(vocab_filename):
            raise ValueError("Vocab %s not found" % vocab_filename)
        return text_encoder.TokenTextEncoder(vocab_filename, replace_oov="UNK")

    def generate_samples(self, data_dir, tmp_dir, dataset_split):
        """Instance of token generator for the WMT en->zh task, training set."""
        train = dataset_split == problem.DatasetSplit.TRAIN
        dataset_path = (ENZH_BPE_DATASETS["TRAIN"] if train else ENZH_BPE_DATASETS["DEV"])
        train_path = get_enzh_bpe_dataset(tmp_dir, dataset_path)

        # Vocab
        src_token_path = (os.path.join(data_dir, self.source_vocab_name), self.source_vocab_name)
        tar_token_path = (os.path.join(data_dir, self.target_vocab_name), self.target_vocab_name)
        for token_path, vocab_name in [src_token_path, tar_token_path]:
            if not tf.gfile.Exists(token_path):
                token_tmp_path = os.path.join(tmp_dir, vocab_name)
                tf.gfile.Copy(token_tmp_path, token_path)
                with tf.gfile.GFile(token_path, mode="r") as f:
                    vocab_data = "<pad>\n<EOS>\n" + f.read() + "UNK\n"
                with tf.gfile.GFile(token_path, mode="w") as f:
                    f.write(vocab_data)

        return text_problems.text2text_txt_iterator(train_path + ".en",
                                                    train_path + ".zh")

    def generate_encoded_samples(self, data_dir, tmp_dir, dataset_split):
        """在生成数据的时候,主要是通过这个方法获取已编码样本的"""
        generator = self.generate_samples(data_dir, tmp_dir, dataset_split)
        encoder = self.get_vocab(data_dir)
        target_encoder = self.get_vocab(data_dir, is_target=True)
        return text_problems.text2text_generate_encoded(generator, encoder, target_encoder,
                                                        has_inputs=self.has_inputs)

    def feature_encoders(self, data_dir):
        source_token = self.get_vocab(data_dir)
        target_token = self.get_vocab(data_dir, is_target=True)
        return {
            "inputs": source_token,
            "targets": target_token,
        }

一个链接:http://www.statmt.org/


自定义参数:

根据以往的文章,应该能够轻松的定义超参数!

直接上代码:

from tensor2tensor.models.transformer import transformer_base_single_gpu
@registry.register_hparams
def transformer_bsg():
  """HParams for transformer base model for single GPU."""
  hparams = transformer_base_single_gpu()
  hparams.batch_size = 2048
  hparams.learning_rate_cosine_cycle_steps = 300000
  hparams.learning_rate = 0.2
  hparams.learning_rate_warmup_steps = 16000
  return hparams

里面都是一些可以自定义的代码,保存文件,放入到usr_dir 中引入到__init__.py 文件即可。

猜你喜欢

转载自blog.csdn.net/hpulfc/article/details/82625217