当前位置:   article > 正文

文本分类(三) | (6) 番外篇(预训练语言模型的另一种使用方式)_max_steps is given, it will override any value giv

max_steps is given, it will override any value given in num_train_epochs

完整项目(番外篇)

本篇博客也是对Github优秀文本分类项目的解析,该文本分类项目,主要基于预训练语言模型,包括bert、xlnet、bert/xlnet + CNN/GRU/LSTM、Albert等,使用PyTorch实现。

项目其实提供了一种预训练语言模型的通用方法,可以将本项目扩展为使用任意的预训练语言模型(包括:albert、xlnet、roberta,t5,gpt等,以及他们与各种深度学习模型的结合)。

目录

1. 项目特点

2. 数据集

3. 项目组织结构

4. 项目流程

5. 使用方式


1. 项目特点

相比于文本分类(三)的前五篇博客介绍的项目,同样是基于预训练语言模型,它主要有以下几个不同:

1)前几篇博客介绍的项目,是通过导入transformers这个第三方库,通过该库提供的接口来使用预训练语言模型;本篇博客介绍的项目,通过把transformer库(内部包含modeling_xx.py,tokenization_xx.py等若干python脚本,xx可以替换为任意预训练语言模型)下载到本地项目中来使用,并通过修改transformer中的modeling_xx.py脚本文件,来实现预训练语言模型和其他各种深度学习模型的结合。

2)二者的数据预处理方式不同:之前的项目,首先把数据处理为.txt格式,通过一些预处理函数,构建input_ids,attention_mask,token_type_ids,labels,然后自定义构建数据迭代器,前三个作为模型的输入,得到输出后,在训练阶段和labels计算loss;本项目,首先把数据处理为csv格式,通过一些不同的预处理函数,构建input_ids,attention_mask,token_type_ids,labels,然后使用PyTorch内置的函数,先转为DataSet对象,在构建DataLoader,四个都作为模型的输入,模型的输出就是loss,即把计算loss也封装在了模型类定义中,训练阶段不必再计算loss。本项目的数据预处理函数相比之前更加灵活,考虑到了一些预训练模型的预处理会稍有不同,比如roberta,xlm等不需要token_type_ids,xlnet相关模型的填充方式等(当然也可以对之前项目的预处理稍加修改,使之适用于不同的预训练模型)。

3)二者使用的模型不同:之前提到过所有预训练模型的建模脚本,如modeling_bert.py、modeling_albert.py、modeling_xlnet.py等,有两个与分类相关的类,如modeling_bert.py中有BertModel类和BertForSequenceClassification类、modeling_albert.py中有AlbertModel类和AlbertForSequenceClassification类等,其他预训练模型一样都有两个和文本分类相关的类。

这两个类的区别是:

A. XXModel类产生的是相关预训练语言模型的输出,及encoder的输出(预训练语言模型可以看作是一个encoder),它后面可以接各种不同的下游任务(我们可以对其输出进行各种不同的自定义设置,文本分类任务只是其中一个),对于文本分类任务,可以把[cls]token对应的最后一层的编码向量再接全连接层进行分类。也可以基于最后一层所有的编码向量,后面接CNN、RNN等深度学习模型,并且可以自定义损失函数、优化器等,像训练普通深度学习模型一样对其训练。

B. XXForSequenceClassification类,其实就是把第一种情况([cls]token对应的最后一层的编码向量再接全连接层进行分类)进行了封装,整体作为一个分类模型,单纯提供分类服务,并且类内部定义了损失函数。接下来只需要自定义训练过程。当然,你也可以把后面的几种情况如基于最后一层所有的编码向量,后面接CNN、RNN等深度学习模型,进行封装,写一个XX_cnnForSequenceClassification或XX_lstmForSequenceClassification类放在modeling_xx.py中(对库文件进行修改)。只能供分类服务。

之前的项目使用的是第一个类,即XXModel类,来获取预训练语言模型的输出,然后在本地脚本中对输出进行处理,接一些其他模型,构建一个新的分类模型。

本项目使用的是第二个类,即XXForSequenceClassification类,直接对库文件中的modeling_xx.py进行修改,把预训练语言模型输出接各种其他模型的情况,整体封装成一个类,放在modeling_xx.py中来使用。

4)二者训练、验证以及测试的细节处理有所不同,整体来说,本项目的处理更加细节、全面。

 

2. 数据集

在THUCNews数据集中抽取25000篇新闻,一共5个类,每一类5000篇。

类别:"体育", "财经", "房产", "家居", "教育"

数据集划分:训练集2w(每个类别4,000条),验证集5000条(每个类别1000条)。另外还抽取了测试集2500条(每个类别500条)。

处理为.csv文件,训练集、验证集格式如下: 类别名称,文本

测试集格式如下:文本

(数据已处理好,可以直接使用)

 

3. 项目组织结构

1)dataset/THUNews/5_5000:处理好的训练集、验证集、测试集(预测),csv格式

2)pretrained_models:存储下载的预训练语言模型,如使用bert模型时,用的是bert-base-chinese版本,把该版本对应的配置文件config.json,模型文件pytorch_model.bin,词表文件vocab.txt下载下来,在下面新建bert-base-chinese文件夹,把三个文件放进去(名字需要更名,严格为config.json,pytorch_model.bin,vocab.txt)。其他预训练语言模型类似,如何下载具体版本对应的三个文件,文本分类(三)第一篇博客中有介绍,不再赘述。

3)results:存放模型预测的结果,模型保存的参数,模型在验证集上的准确率、f1-score以及日志等信息

4)runs:模型运行信息

5)transformers:把transformers库中的transformers文件夹下载到本地项目中,内部包含modeling_xx.py,tokenization_xx.py等若干python脚本,xx可以替换为任意预训练语言模型,通过修改transformer中的modeling_xx.py脚本文件,来实现预训练语言模型和其他各种深度学习模型的结合。

6)metrics.py:定义了各种评估指标的计算函数,如准确率、f1-score等

7)run.py:项目入口,定义了整个项目的处理流程。

8)run_classifier.sh: run.py通过argparse工具设置了许多命令行参数,来对模型的超参数进行配置。所以运行run.py的命令非常长,可以把该命令写入.sh脚本中,在其中对配置进行修改,运行时,只需要在命令行 bash .sh即可。

9)utils.py:定义了数据预处理函数

 

4. 项目流程

  • 导入相关类
  1. from transformers import (BertConfig,BertTokenizer,
  2. BertForSequenceClassification,
  3. BertForSequenceClassification_CNN,
  4. BertForSequenceClassification_LSTM,
  5. BertForSequenceClassification_GRU,
  6. XLNetConfig,XLNetTokenizer,
  7. XLNetForSequenceClassification,
  8. XLNetForSequenceClassification_LSTM,
  9. XLNetForSequenceClassification_GRU,
  10. AlbertConfig,AlbertTokenizer,
  11. AlbertForSequenceClassification)

可以在此基础上,继续增加其他的预训练模型以及和深度学习模型的结合。

  • 定义模型类型到相关类的映射字典
  1. MODEL_CLASSES = {
  2. 'bert': (BertConfig, BertForSequenceClassification, BertTokenizer),
  3. 'bert_cnn': (BertConfig, BertForSequenceClassification_CNN, BertTokenizer),
  4. 'bert_lstm': (BertConfig, BertForSequenceClassification_LSTM, BertTokenizer),
  5. 'bert_gru': (BertConfig, BertForSequenceClassification_GRU, BertTokenizer),
  6. 'xlnet': (XLNetConfig, XLNetForSequenceClassification, XLNetTokenizer),
  7. 'xlnet_lstm': (XLNetConfig, XLNetForSequenceClassification_LSTM, XLNetTokenizer),
  8. 'xlnet_gru': (XLNetConfig, XLNetForSequenceClassification_GRU, XLNetTokenizer),
  9. 'albert': (AlbertConfig, AlbertForSequenceClassification, AlbertTokenizer)
  10. }

可以在此基础上,继续增加其他的预训练模型以及和深度学习模型的结合。

  • 声明一些命令行参数
  1. #声明argparse对象
  2. parser = argparse.ArgumentParser()
  3. #添加命令行参数(required=True的参数必须在命令行设置,其余参数如果不在命令行设置就是用默认值,也可以在命令行设置覆盖默认值)
  4. ##必须设置的参数
  5. #处理好的数据集路径 .csv文件所在k路径
  6. parser.add_argument("--data_dir", default=None, type=str, required=True,
  7. help="The input data dir. Should contain the .tsv files (or other data files) for the task.")
  8. #所使用的模型 目前支持bert、bert_cnn、bert_lstm、bert_gru、xlnet、xlnet_gru、xlnet_lstm、albert
  9. parser.add_argument("--model_type", default=None, type=str, required=True,
  10. help="Model type selected in the list: " + ", ".join(MODEL_CLASSES.keys()))
  11. #下载的预训练模型相关文件所在路径(.bin模型参数结构文件,.json模型配置文件,vocab.txt词表文件)
  12. #注意每种预训练模型都有对应的文件(对应及下载方法 文本分类(三)系列第一篇博客有介绍)
  13. parser.add_argument("--model_name_or_path", default=None, type=str, required=True,
  14. help="Path to pre-trained model or shortcut name selected in the list:")
  15. #任务名字 THUCNews
  16. parser.add_argument("--task_name", default=None, type=str, required=True,
  17. help="The name of the task to train selected in the list: " + ", ".join(processors.keys()))
  18. #模型的预测和checkpoints文件的写入路径
  19. parser.add_argument("--output_dir", default=None, type=str, required=True,
  20. help="The output directory where the model predictions and checkpoints will be written.")
  21. ##非必须参数
  22. parser.add_argument("--config_name", default="", type=str,
  23. help="Pretrained config name or path if not the same as model_name")
  24. parser.add_argument("--tokenizer_name", default="", type=str,
  25. help="Pretrained tokenizer name or path if not the same as model_name")
  26. parser.add_argument("--cache_dir", default="", type=str,
  27. help="Where do you want to store the pre-trained models downloaded from s3")
  28. #输入序列最大长度 (需要对batch中所有的序列进行填充 统一成一个长度)
  29. parser.add_argument("--max_seq_length", default=128, type=int,
  30. help="The maximum total input sequence length after tokenization. Sequences longer "
  31. "than this will be truncated, sequences shorter will be padded.")
  32. #训练、验证和预测
  33. parser.add_argument("--do_train", action='store_true',
  34. help="Whether to run training.")
  35. parser.add_argument("--do_eval", action='store_true',
  36. help="Whether to run eval on the dev set.")
  37. parser.add_argument("--do_predict", action='store_true',
  38. help="Whether to run predict on the test set.")
  39. parser.add_argument("--evaluate_during_training", action='store_true',
  40. help="Rul evaluation during training at each logging step.")
  41. parser.add_argument("--do_lower_case", action='store_true',
  42. help="Set this flag if you are using an uncased model.")
  43. #训练阶段和验证阶段 每个gpu上的batch_size
  44. parser.add_argument("--per_gpu_train_batch_size", default=8, type=int,
  45. help="Batch size per GPU/CPU for training.")
  46. parser.add_argument("--per_gpu_eval_batch_size", default=8, type=int,
  47. help="Batch size per GPU/CPU for evaluation.")
  48. parser.add_argument('--gradient_accumulation_steps', type=int, default=1,
  49. help="Number of updates steps to accumulate before performing a backward/update pass.")
  50. #初始学习率
  51. parser.add_argument("--learning_rate", default=5e-5, type=float,
  52. help="The initial learning rate for Adam.")
  53. #正则化系数
  54. parser.add_argument("--weight_decay", default=0.0, type=float,
  55. help="Weight decay if we apply some.")
  56. parser.add_argument("--adam_epsilon", default=1e-8, type=float,
  57. help="Epsilon for Adam optimizer.")
  58. parser.add_argument("--max_grad_norm", default=1.0, type=float,
  59. help="Max gradient norm.")
  60. #epoch数 一个epoch完整遍历一遍数据集
  61. parser.add_argument("--num_train_epochs", default=3.0, type=float,
  62. help="Total number of training epochs to perform.")
  63. parser.add_argument("--max_steps", default=-1, type=int,
  64. help="If > 0: set total number of training steps to perform. Override num_train_epochs.")
  65. parser.add_argument("--warmup_steps", default=0, type=int,
  66. help="Linear warmup over warmup_steps.")
  67. #每n次更新(每n个batch) 保存一下日志(损失、准确率等信息)
  68. parser.add_argument('--logging_steps', type=int, default=50,
  69. help="Log every X updates steps.")
  70. #每n次更新(每n个batch) 保存一次参数
  71. parser.add_argument('--save_steps', type=int, default=50,
  72. help="Save checkpoint every X updates steps.")
  73. parser.add_argument("--eval_all_checkpoints", action='store_true',
  74. help="Evaluate all checkpoints starting with the same prefix as model_name ending and ending with step number")
  75. parser.add_argument("--no_cuda", action='store_true',
  76. help="Avoid using CUDA when available")
  77. parser.add_argument('--overwrite_output_dir', action='store_true',
  78. help="Overwrite the content of the output directory")
  79. parser.add_argument('--overwrite_cache', action='store_true',
  80. help="Overwrite the cached training and evaluation sets")
  81. #初始化模型时 使用的随机种子
  82. parser.add_argument('--seed', type=int, default=42,
  83. help="random seed for initialization")
  84. parser.add_argument('--fp16', action='store_true',
  85. help="Whether to use 16-bit (mixed) precision (through NVIDIA apex) instead of 32-bit")
  86. parser.add_argument('--fp16_opt_level', type=str, default='O1',
  87. help="For fp16: Apex AMP optimization level selected in ['O0', 'O1', 'O2', and 'O3']."
  88. "See details at https://nvidia.github.io/apex/amp.html")
  89. parser.add_argument("--local_rank", type=int, default=-1,
  90. help="For distributed training: local_rank")
  91. #Additional layer parameters
  92. #预训练模型后加TextCNN TextCNN相关的参数
  93. #不同大小卷积核的数量
  94. parser.add_argument('--filter_num', type=int, default=256, help='number of each size of filter')
  95. #不同大小卷积核的尺寸
  96. parser.add_argument('--filter_sizes', type=str, default='3,4,5', help='comma-separated filter sizes to use for convolution')
  97. #预训练模型后加LSTM LSTM相关的参数
  98. #隐藏单元个数
  99. parser.add_argument("--lstm_hidden_size", default=300, type=int,
  100. help="")
  101. #lstm层数
  102. parser.add_argument("--lstm_layers", default=2, type=int,
  103. help="")
  104. #lstm dropout 丢弃率
  105. parser.add_argument("--lstm_dropout", default=0.5, type=float,
  106. help="")
  107. ##预训练模型后加GRU GRU相关的参数
  108. #隐藏单元个数
  109. parser.add_argument("--gru_hidden_size", default=300, type=int,
  110. help="")
  111. #gru层数
  112. parser.add_argument("--gru_layers", default=2, type=int,
  113. help="")
  114. #gru dropout 丢弃率
  115. parser.add_argument("--gru_dropout", default=0.5, type=float,
  116. help="")
  117. #解析参数
  118. args = parser.parse_args()
  119. #把不同大小卷积核的尺寸转换为整数 对原有参数进行覆盖
  120. args.filter_sizes = [int(size) for size in str(args.filter_sizes).split(',')]
  • 单机多卡/多机分布式
  1. # Setup CUDA, GPU & distributed training(单机多卡/多机分布式)
  2. if args.local_rank == -1 or args.no_cuda:
  3. device = torch.device("cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu")
  4. args.n_gpu = torch.cuda.device_count() #gpu数量
  5. else: # Initializes the distributed backend which will take care of sychronizing nodes/GPUs
  6. torch.cuda.set_device(args.local_rank)
  7. device = torch.device("cuda", args.local_rank)
  8. torch.distributed.init_process_group(backend='nccl')
  9. args.n_gpu = 1
  10. args.device = device
  • 构建数据集处理器

utils.py

  1. class THUNewsProcessor(DataProcessor):
  2. """Processor for the SST-2 data set (GLUE version)."""
  3. def get_example_from_tensor_dict(self, tensor_dict):
  4. """See base class."""
  5. return InputExample(tensor_dict['idx'].numpy(),
  6. tensor_dict['sentence'].numpy().decode('utf-8'),
  7. None,
  8. str(tensor_dict['label'].numpy()))
  9. #获取训练集、验证集、测试集样本
  10. #训练集、验证集、测试集提前处理为.csv格式 格式:类别名称,文本
  11. #通过父类DataProcessor中的_read_csv函数 把csv文件读取为列表 列表中的每一个元素为csv文件的每一行
  12. def get_train_examples(self, data_dir):
  13. """See base class."""
  14. return self._create_examples(self._read_csv(os.path.join(data_dir, "train.csv")), "train")
  15. def get_dev_examples(self, data_dir):
  16. """See base class."""
  17. return self._create_examples(self._read_csv(os.path.join(data_dir, "dev.csv")), "dev")
  18. def get_test_examples(self, data_dir):
  19. """See base class."""
  20. return self._create_examples(self._read_csv(os.path.join(data_dir, "test.csv")), "test")
  21. def get_labels(self):
  22. """设置当前数据集的标签"""
  23. return ["体育", "财经", "房产", "家居", "教育"] #使用了其中5个类别
  24. def _create_examples(self, lines, set_type):
  25. """Creates examples for the training/dev/test sets."""
  26. examples = []
  27. for (i, line) in enumerate(lines): #遍历元组列表,即遍历csv文件每一行/每一条数据
  28. if i == 0: #跳过表头 labels,text (把数据处理为csv文件时,是保留表头)
  29. continue
  30. guid = "%s-%s" % (set_type, i) #set_type 训练集/验证集/测试集
  31. if set_type == 'test': #测试集没有类别标签 如果是测试集 line[0]是文本 标签统一设置为体育
  32. text_a = line[0]
  33. label = '体育'
  34. else: #验证集和训练集 line[0]是类别标签,line[1]是文本
  35. label = line[0]
  36. text_a = line[1]
  37. #如有两段文本, 也可以设置text_b 句子对任务(问答)
  38. #把每条数据 转换为InputExample对象
  39. examples.append(InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
  40. return examples#保存在examples列表中
  41. tasks_num_labels = { #分类任务的类别数 存储为字典。键名为任务名(小写),值为类别数
  42. "thunews": 5,
  43. }
  44. processors = { #任务处理器 存储为字典。键名为任务名(小写),值为处理器类名(自定义)
  45. "thunews": THUNewsProcessor,
  46. }
  47. output_modes = { #输出模式 存储为字典。键名为任务名(小写),值为classification。分类任务
  48. "thunews": "classification",
  49. }

处理新数据集时,仿照上述格式,添加新数据集的处理器即可。 

  • 加载预训练模型
  1. #通过MODEL_CLASSES字典 传入model_type,得到相应模型的参数配置类、模型类和切分工具类
  2. config_class, model_class, tokenizer_class = MODEL_CLASSES[args.model_type]
  3. #获取并加载预训练模型
  4. config = config_class.from_pretrained(args.config_name if args.config_name else args.model_name_or_path,
  5. num_labels=num_labels,
  6. finetuning_task=args.task_name,
  7. cache_dir=args.cache_dir if args.cache_dir else None)
  8. tokenizer = tokenizer_class.from_pretrained(args.tokenizer_name if args.tokenizer_name else args.model_name_or_path,
  9. do_lower_case=args.do_lower_case,
  10. cache_dir=args.cache_dir if args.cache_dir else None)
  11. model = model_class.from_pretrained(args.model_name_or_path,
  12. from_tf=bool('.ckpt' in args.model_name_or_path),
  13. config=config,
  14. cache_dir=args.cache_dir if args.cache_dir else None,
  15. args=args)
  • 构建数据集

utils.py

  1. def convert_examples_to_features(examples, tokenizer,
  2. max_length=512,
  3. task=None,
  4. label_list=None,
  5. output_mode=None,
  6. pad_on_left=False,
  7. pad_token=0,
  8. pad_token_segment_id=0,
  9. mask_padding_with_zero=True):
  10. #把InputExamples对象 转换为输入特征InputFeatures
  11. """
  12. Loads a data file into a list of ``InputFeatures``
  13. Args:
  14. examples: List of ``InputExamples`` or ``tf.data.Dataset`` containing the examples.
  15. tokenizer: Instance of a tokenizer that will tokenize the examples
  16. max_length: Maximum example length
  17. task: GLUE task
  18. label_list: List of labels. Can be obtained from the processor using the ``processor.get_labels()`` method
  19. output_mode: String indicating the output mode. Either ``regression`` or ``classification``
  20. pad_on_left: If set to ``True``, the examples will be padded on the left rather than on the right (default)
  21. pad_token: Padding token
  22. pad_token_segment_id: The segment ID for the padding token (It is usually 0, but can vary such as for XLNet where it is 4)
  23. mask_padding_with_zero: If set to ``True``, the attention mask will be filled by ``1`` for actual values
  24. and by ``0`` for padded values. If set to ``False``, inverts it (``1`` for padded values, ``0`` for
  25. actual values)
  26. Returns:
  27. If the ``examples`` input is a ``tf.data.Dataset``, will return a ``tf.data.Dataset``
  28. containing the task-specific features. If the input is a list of ``InputExamples``, will return
  29. a list of task-specific ``InputFeatures`` which can be fed to the model.
  30. """
  31. is_tf_dataset = False #是否为tensorflow形式数据集
  32. if is_tf_available() and isinstance(examples, tf.data.Dataset):
  33. is_tf_dataset = True
  34. if task is not None:
  35. processor = processors[task]() #获取自定义任务处理器
  36. #获取标签列表和输出模式
  37. if label_list is None:
  38. label_list = processor.get_labels()
  39. logger.info("Using label list %s for task %s" % (label_list, task))
  40. if output_mode is None:
  41. output_mode = glue_output_modes[task]
  42. logger.info("Using output mode %s for task %s" % (output_mode, task))
  43. #类别标签 转换为数字索引
  44. label_map = {label: i for i, label in enumerate(label_list)}
  45. features = []
  46. for (ex_index, example) in enumerate(examples): #遍历每一个InputExamples对象(每一条数据)
  47. if ex_index % 10000 == 0:
  48. logger.info("Writing example %d" % (ex_index))
  49. if is_tf_dataset:
  50. example = processor.get_example_from_tensor_dict(example)
  51. example = processor.tfds_map(example)
  52. #inputs: dict 调用切分工具中的函数 对每个InputExamples对象进行处理 返回一个字典
  53. inputs = tokenizer.encode_plus(
  54. example.text_a,
  55. example.text_b,
  56. add_special_tokens=True,
  57. max_length=max_length, #如果序列长于最大长度(统一设置的长度),截断,其他维持原样
  58. )
  59. #input_ids: 输入数据token在词汇表中的索引
  60. #token_type_ids: 分段token索引,类似segment embedding(对于句子对任务 属于句子A的token为0,句子B的token为1,对于分类任务,只有一个输入句子 全为0)
  61. input_ids, token_type_ids = inputs["input_ids"], inputs["token_type_ids"]
  62. real_token_len = len(input_ids) #输入序列实际长度 不包含填充
  63. # The mask has 1 for real tokens and 0 for padding tokens. Only real
  64. # tokens are attended to.
  65. #非填充部分的token对应1
  66. attention_mask = [1 if mask_padding_with_zero else 0] * len(input_ids)
  67. # Zero-pad up to the sequence length. 填充部分的长度 >=0
  68. padding_length = max_length - len(input_ids)
  69. if pad_on_left: #在输入序列左端填充
  70. input_ids = ([pad_token] * padding_length) + input_ids
  71. #填充部分的token 对应0 只对非填充部分的token计算注意力
  72. attention_mask = ([0 if mask_padding_with_zero else 1] * padding_length) + attention_mask
  73. token_type_ids = ([pad_token_segment_id] * padding_length) + token_type_ids
  74. else:#在输入序列右端填充
  75. input_ids = input_ids + ([pad_token] * padding_length)
  76. attention_mask = attention_mask + ([0 if mask_padding_with_zero else 1] * padding_length)
  77. token_type_ids = token_type_ids + ([pad_token_segment_id] * padding_length)
  78. #填充后 输入序列、attention_mask、token_type_ids的长度都等于max_length(统一设置的长度)
  79. assert len(input_ids) == max_length, "Error with input length {} vs {}".format(len(input_ids), max_length)
  80. assert len(attention_mask) == max_length, "Error with input length {} vs {}".format(len(attention_mask), max_length)
  81. assert len(token_type_ids) == max_length, "Error with input length {} vs {}".format(len(token_type_ids), max_length)
  82. if output_mode == "classification": #分类任务 把类别标签 转换为索引
  83. label = label_map[example.label] #label => index
  84. elif output_mode == "regression": #回归任务 把标签转换为浮点数
  85. label = float(example.label)
  86. else:
  87. raise KeyError(output_mode)
  88. if ex_index < 5: #前5个样本 打印处理效果
  89. logger.info("*** Example ***")
  90. logger.info("guid: %s" % (example.guid))
  91. logger.info("real_token_len: %s" % (real_token_len))
  92. logger.info("input_ids: %s" % " ".join([str(x) for x in input_ids]))
  93. logger.info("attention_mask: %s" % " ".join([str(x) for x in attention_mask]))
  94. logger.info("token_type_ids: %s" % " ".join([str(x) for x in token_type_ids]))
  95. logger.info("label: %s (id = %d)" % (example.label, label))
  96. #把构造好的样本 转换为InputFeatures对象 添加到features列表中
  97. features.append(
  98. InputFeatures(input_ids=input_ids,
  99. attention_mask=attention_mask,
  100. token_type_ids=token_type_ids,
  101. label=label,
  102. real_token_len=real_token_len))
  103. if is_tf_available() and is_tf_dataset: #TF 格式的数据
  104. def gen():
  105. for ex in features:
  106. yield ({'input_ids': ex.input_ids,
  107. 'attention_mask': ex.attention_mask,
  108. 'token_type_ids': ex.token_type_ids},
  109. ex.label)
  110. return tf.data.Dataset.from_generator(gen,
  111. ({'input_ids': tf.int32,
  112. 'attention_mask': tf.int32,
  113. 'token_type_ids': tf.int32},
  114. tf.int64),
  115. ({'input_ids': tf.TensorShape([None]),
  116. 'attention_mask': tf.TensorShape([None]),
  117. 'token_type_ids': tf.TensorShape([None])},
  118. tf.TensorShape([])))
  119. return features
  1. def load_and_cache_examples(args, task, tokenizer, evaluate=False, predict=False):
  2. '''
  3. 将dataset转换为features,并保存在目录cached_features_file中。
  4. args:
  5. evaluate: False. 若为True,则对dev.csv进行转换
  6. predict: False. 若为True,则对test.csv进行转换
  7. return:
  8. dataset
  9. '''
  10. if args.local_rank not in [-1, 0] and not evaluate:
  11. torch.distributed.barrier() # Make sure only the first process in distributed training process the dataset, and the others will use the cache
  12. processor = processors[task]() #THUNewsProcessor() 自定义的任务处理器
  13. output_mode = output_modes[task] #classification
  14. # Load data features from cache or dataset file
  15. #cached_features_file 为数据集构造的特征的保存目录
  16. if evaluate:
  17. exec_model = 'dev'
  18. elif predict:
  19. exec_model = 'test'
  20. else:
  21. exec_model = 'train'
  22. #特征保存目录的命名
  23. cached_features_file = os.path.join(args.data_dir, 'cached_{}_{}_{}_{}'.format(
  24. exec_model,
  25. list(filter(None, args.model_name_or_path.split('/'))).pop(),
  26. str(args.max_seq_length),
  27. str(task)))
  28. #如果对数据集已经构造好特征了 直接加载 避免重复处理
  29. if os.path.exists(cached_features_file) and not args.overwrite_cache:
  30. logger.info("Loading features from cached file %s\n", cached_features_file)
  31. features = torch.load(cached_features_file)
  32. #否则对数据集进行处理 得到特征
  33. else:
  34. logger.info("Creating features from dataset file at %s", args.data_dir)
  35. label_list = processor.get_labels() #标签列表
  36. #对验证集、测试集、训练集进行处理 把数据转换为InputExample对象
  37. if evaluate:
  38. examples = processor.get_dev_examples(args.data_dir)
  39. elif predict:
  40. examples = processor.get_test_examples(args.data_dir)
  41. else:
  42. examples = processor.get_train_examples(args.data_dir)
  43. #转换为特征
  44. #注意xlnet系列模型的数据预处理方式 和 bert系列稍有不同
  45. features = convert_examples_to_features(examples,
  46. tokenizer,
  47. label_list=label_list,
  48. max_length=args.max_seq_length,
  49. output_mode=output_mode,
  50. pad_on_left=bool(args.model_type in ['xlnet']), # pad on the left for xlnet
  51. pad_token=tokenizer.convert_tokens_to_ids([tokenizer.pad_token])[0],
  52. pad_token_segment_id=4 if args.model_type in ['xlnet'] else 0,
  53. )
  54. #把数据处理为InputFeatures对象后 存储为cached_features_file
  55. if args.local_rank in [-1, 0]:
  56. logger.info("Saving features into cached file %s", cached_features_file)
  57. torch.save(features, cached_features_file)
  58. if args.local_rank == 0 and not evaluate:
  59. torch.distributed.barrier() # Make sure only the first process in distributed training process the dataset, and the others will use the cache
  60. # Convert to Tensors and build dataset
  61. all_input_ids = torch.tensor([f.input_ids for f in features], dtype=torch.long)
  62. all_attention_mask = torch.tensor([f.attention_mask for f in features], dtype=torch.long)
  63. all_token_type_ids = torch.tensor([f.token_type_ids for f in features], dtype=torch.long)
  64. if output_mode == "classification":
  65. all_labels = torch.tensor([f.label for f in features], dtype=torch.long)
  66. elif output_mode == "regression":
  67. all_labels = torch.tensor([f.label for f in features], dtype=torch.float)
  68. #构建dataset对象
  69. dataset = TensorDataset(all_input_ids, all_attention_mask, all_token_type_ids, all_labels)
  70. return dataset

 

  • 训练
  1. def train(args, train_dataset, model, tokenizer):
  2. """ Train the model """
  3. if args.local_rank in [-1, 0]:
  4. tb_writer = SummaryWriter()
  5. args.train_batch_size = args.per_gpu_train_batch_size * max(1, args.n_gpu) #得到训练时的batch大小
  6. #定义采样方式
  7. train_sampler = RandomSampler(train_dataset) if args.local_rank == -1 else DistributedSampler(train_dataset)
  8. #构建dataloader
  9. train_dataloader = DataLoader(train_dataset, sampler=train_sampler, batch_size=args.train_batch_size)
  10. if args.max_steps > 0:
  11. t_total = args.max_steps
  12. args.num_train_epochs = args.max_steps // (len(train_dataloader) // args.gradient_accumulation_steps) + 1
  13. else:
  14. t_total = len(train_dataloader) // args.gradient_accumulation_steps * args.num_train_epochs
  15. # Prepare optimizer and schedule (linear warmup and decay)
  16. #以下参数 不使用正则化
  17. no_decay = ['bias', 'LayerNorm.weight']
  18. optimizer_grouped_parameters = [
  19. {'params': [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], 'weight_decay': args.weight_decay},
  20. {'params': [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
  21. ]
  22. #定义优化器
  23. optimizer = AdamW(optimizer_grouped_parameters, lr=args.learning_rate, eps=args.adam_epsilon)
  24. scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=args.warmup_steps, num_training_steps=t_total)
  25. if args.fp16:
  26. try:
  27. from apex import amp
  28. except ImportError:
  29. raise ImportError("Please install apex from https://www.github.com/nvidia/apex to use fp16 training.")
  30. model, optimizer = amp.initialize(model, optimizer, opt_level=args.fp16_opt_level)
  31. # multi-gpu training (should be after apex fp16 initialization)
  32. if args.n_gpu > 1: #单机多卡
  33. model = torch.nn.DataParallel(model)
  34. # Distributed training (should be after apex fp16 initialization)
  35. #多机分布式
  36. if args.local_rank != -1:
  37. model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank],
  38. output_device=args.local_rank,
  39. find_unused_parameters=True)
  40. # Train!
  41. logger.info("***** Running training *****")
  42. logger.info(" Num examples = %d", len(train_dataset))
  43. logger.info(" Num Epochs = %d", args.num_train_epochs)
  44. logger.info(" Instantaneous batch size per GPU = %d", args.per_gpu_train_batch_size)
  45. logger.info(" Total train batch size (w. parallel, distributed & accumulation) = %d",
  46. args.train_batch_size * args.gradient_accumulation_steps * (torch.distributed.get_world_size() if args.local_rank != -1 else 1))
  47. logger.info(" Gradient Accumulation steps = %d", args.gradient_accumulation_steps)
  48. logger.info(" Total optimization steps = %d", t_total)
  49. global_step = 0
  50. tr_loss, logging_loss = 0.0, 0.0
  51. model.zero_grad()
  52. train_iterator = trange(int(args.num_train_epochs), desc="Epoch", disable=args.local_rank not in [-1, 0])
  53. set_seed(args) # Added here for reproductibility (even between python 2 and 3)
  54. for _ in train_iterator:
  55. epoch_iterator = tqdm(train_dataloader, desc="Iteration", disable=args.local_rank not in [-1, 0])
  56. for step, batch in enumerate(epoch_iterator):
  57. model.train()#训练模式
  58. batch = tuple(t.to(args.device) for t in batch) #把输入数据 放到设备上
  59. #分类任务是单句子 不需要设置token_type_ids 默认全为0 batch[2]
  60. #定义模型的输入参数 字典形式
  61. inputs = {'input_ids': batch[0],
  62. 'attention_mask': batch[1],
  63. 'labels': batch[3]}
  64. if args.model_type != 'distilbert':
  65. inputs['token_type_ids'] = batch[2] if args.model_type in ['bert', 'xlnet'] else None # XLM, DistilBERT and RoBERTa don't use segment_ids 这些模型没有token_type_ids
  66. if args.model_type in ['bert_cnn']:
  67. #inputs['real_token_len'] = batch[4]
  68. pass
  69. outputs = model(**inputs) #得到模型输出 使用的是XXForSequenceClassification类 他的第一个返回值是损失
  70. loss = outputs[0] # model outputs are always tuple in transformers (see doc)
  71. if args.n_gpu > 1:
  72. loss = loss.mean() # mean() to average on multi-gpu parallel training
  73. if args.gradient_accumulation_steps > 1:
  74. loss = loss / args.gradient_accumulation_steps #每个batch都将loss除以gradient_accumulation_steps
  75. #计算梯度
  76. if args.fp16:
  77. with amp.scale_loss(loss, optimizer) as scaled_loss:
  78. scaled_loss.backward()
  79. else:
  80. loss.backward()
  81. epoch_iterator.set_description("loss {}".format(round(loss.item(), 5)))
  82. tr_loss += loss.item()
  83. #每gradient_accumulation_steps个batch(把这些batch的梯度求和) 更新一次参数
  84. if (step + 1) % args.gradient_accumulation_steps == 0: #过gradient_accumulation_steps后才将梯度清零,不是每次更新/每过一个batch清空一次梯度,即每gradient_accumulation_steps次更新清空一次
  85. #梯度裁剪
  86. if args.fp16:
  87. torch.nn.utils.clip_grad_norm_(amp.master_params(optimizer), args.max_grad_norm)
  88. else:
  89. torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm)
  90. #反向传播更新参数
  91. optimizer.step()
  92. #更新学习率
  93. scheduler.step() # Update learning rate schedule
  94. #清空梯度
  95. model.zero_grad()
  96. global_step += 1
  97. #每logging_steps,进行evaluate
  98. if args.local_rank in [-1, 0] and args.logging_steps > 0 and global_step % args.logging_steps == 0:
  99. logs = {}
  100. if args.local_rank == -1 and args.evaluate_during_training: # Only evaluate when single GPU otherwise metrics may not average well
  101. #验证
  102. results = evaluate(args, model, tokenizer)
  103. for key, value in results.items():
  104. eval_key = 'eval_{}'.format(key)
  105. logs[eval_key] = value
  106. loss_scalar = (tr_loss - logging_loss) / args.logging_steps
  107. learning_rate_scalar = scheduler.get_lr()[0]
  108. logs['learning_rate'] = learning_rate_scalar
  109. logs['loss'] = loss_scalar
  110. logging_loss = tr_loss
  111. for key, value in logs.items():
  112. tb_writer.add_scalar(key, value, global_step)
  113. print(json.dumps({**logs, **{'step': global_step}}))
  114. #每save_steps保存checkpoint
  115. if args.local_rank in [-1, 0] and args.save_steps > 0 and global_step % args.save_steps == 0:
  116. # Save model checkpoint 保存参数
  117. output_dir = os.path.join(args.output_dir, 'checkpoint-{}'.format(global_step))
  118. if not os.path.exists(output_dir):
  119. os.makedirs(output_dir)
  120. #单机多卡和多机分布式 保存参数有所不同,不是直接保存model 需要保存model.moudle
  121. model_to_save = model.module if hasattr(model, 'module') else model # Take care of distributed/parallel training
  122. model_to_save.save_pretrained(output_dir)
  123. torch.save(args, os.path.join(output_dir, 'training_args.bin'))
  124. logger.info("Saving model checkpoint to %s", output_dir)
  125. if args.max_steps > 0 and global_step > args.max_steps:
  126. epoch_iterator.close()
  127. break
  128. if args.max_steps > 0 and global_step > args.max_steps:
  129. train_iterator.close()
  130. break
  131. if args.local_rank in [-1, 0]:
  132. tb_writer.close()
  133. return global_step, tr_loss / global_step
  • 验证
  1. def evaluate(args, model, tokenizer, prefix=""):
  2. results = {}
  3. #构建验证数据集 Dataset对象
  4. eval_dataset = load_and_cache_examples(args, args.task_name, tokenizer, evaluate=True)
  5. if not os.path.exists(args.output_dir) and args.local_rank in [-1, 0]:
  6. os.makedirs(args.output_dir)
  7. #验证阶段batch大小
  8. args.eval_batch_size = args.per_gpu_eval_batch_size * max(1, args.n_gpu)
  9. # Note that DistributedSampler samples randomly
  10. #定义采样方式
  11. eval_sampler = SequentialSampler(eval_dataset)
  12. #构建Dataloader
  13. eval_dataloader = DataLoader(eval_dataset, sampler=eval_sampler, batch_size=args.eval_batch_size)
  14. # multi-gpu eval 单机多卡
  15. if args.n_gpu > 1:
  16. model = torch.nn.DataParallel(model)
  17. # Eval!
  18. logger.info("***** Running evaluation {} *****".format(prefix))
  19. logger.info(" Num examples = %d", len(eval_dataset))
  20. logger.info(" Batch size = %d", args.eval_batch_size)
  21. eval_loss = 0.0
  22. nb_eval_steps = 0
  23. preds = None #为预测值
  24. out_label_ids = None #为真实标签
  25. for batch in tqdm(eval_dataloader, desc="Evaluating"):
  26. model.eval() #验证模式
  27. batch = tuple(t.to(args.device) for t in batch) #输入数据 转移到device上
  28. with torch.no_grad(): #关闭梯度计算
  29. #构建模型输入 字典形式。 token_type_ids为batch[2] 分类任务为单输入句子 默认全为0
  30. inputs = {'input_ids': batch[0],
  31. 'attention_mask': batch[1],
  32. 'labels': batch[3]}
  33. if args.model_type != 'distilbert':
  34. inputs['token_type_ids'] = batch[2] if args.model_type in ['bert', 'xlnet'] else None # XLM, DistilBERT and RoBERTa don't use segment_ids 没有token_type_ids
  35. outputs = model(**inputs) #获得模型输出
  36. tmp_eval_loss, logits = outputs[:2] #模型输出的前两项为loss和logits
  37. eval_loss += tmp_eval_loss.mean().item()
  38. nb_eval_steps += 1
  39. if preds is None:
  40. preds = logits.detach().cpu().numpy()
  41. out_label_ids = inputs['labels'].detach().cpu().numpy()
  42. else:
  43. preds = np.append(preds, logits.detach().cpu().numpy(), axis=0)
  44. out_label_ids = np.append(out_label_ids, inputs['labels'].detach().cpu().numpy(), axis=0)
  45. eval_loss = eval_loss / nb_eval_steps
  46. if args.output_mode == "classification":
  47. preds = np.argmax(preds, axis=1)
  48. elif args.output_mode == "regression":
  49. preds = np.squeeze(preds)
  50. #preds为模型预测的标签 对预测结果按行取argmax
  51. #和真实标签计算准确率和f1-score
  52. result = acc_and_f1(preds, out_label_ids)
  53. results.update(result)
  54. #把相关指标计算结果 保存
  55. output_eval_file = os.path.join(args.output_dir, prefix, "eval_results.txt")
  56. with open(output_eval_file, "w") as writer:
  57. logger.info("***** Eval results {} *****".format(prefix))
  58. for key in sorted(result.keys()):
  59. logger.info(" %s = %s", key, str(result[key]))
  60. writer.write("%s = %s\n" % (key, str(result[key])))
  61. return results
  • 预测
  1. def predict(args, model, tokenizer, prefix=""):
  2. #results = {}
  3. #构建测试数据集 Dataset对象
  4. pred_dataset = load_and_cache_examples(args, args.task_name, tokenizer, predict=True)
  5. if not os.path.exists(args.output_dir) and args.local_rank in [-1, 0]:
  6. os.makedirs(args.output_dir)
  7. #测试阶段batch大小
  8. args.eval_batch_size = args.per_gpu_eval_batch_size * max(1, args.n_gpu)
  9. # Note that DistributedSampler samples randomly
  10. #定义采样方式
  11. eval_sampler = SequentialSampler(pred_dataset)
  12. #测试集Dataloader
  13. eval_dataloader = DataLoader(pred_dataset, sampler=eval_sampler, batch_size=args.eval_batch_size)
  14. # multi-gpu eval 单机多卡
  15. if args.n_gpu > 1:
  16. model = torch.nn.DataParallel(model)
  17. # Eval!
  18. logger.info("***** Running predict {} *****".format(prefix))
  19. logger.info(" Num examples = %d", len(pred_dataset))
  20. logger.info(" Batch size = %d", args.eval_batch_size)
  21. eval_loss = 0.0
  22. nb_eval_steps = 0
  23. preds = None #为预测值
  24. #out_label_ids = None #为真实标签
  25. for batch in tqdm(eval_dataloader, desc="Predicting"):
  26. model.eval() #测试模式
  27. batch = tuple(t.to(args.device) for t in batch)#把测试数据转移到设备上
  28. with torch.no_grad():#关闭梯度计算
  29. #构建模型输入 字典形式。 token_type_ids为batch[2] 分类任务为单输入句子 默认全为0
  30. inputs = {'input_ids': batch[0],
  31. 'attention_mask': batch[1],
  32. 'labels': batch[3]}
  33. if args.model_type != 'distilbert':
  34. inputs['token_type_ids'] = batch[2] if args.model_type in ['bert', 'xlnet'] else None # XLM, DistilBERT and RoBERTa don't use segment_ids 没有token_type_ids
  35. outputs = model(**inputs) #得到模型输出
  36. tmp_eval_loss, logits = outputs[:2] #前两项为loss、logits
  37. eval_loss += tmp_eval_loss.mean().item()
  38. nb_eval_steps += 1
  39. if preds is None:
  40. preds = logits.detach().cpu().numpy()
  41. #out_label_ids = inputs['labels'].detach().cpu().numpy()
  42. else:
  43. preds = np.append(preds, logits.detach().cpu().numpy(), axis=0)
  44. #out_label_ids = np.append(out_label_ids, inputs['labels'].detach().cpu().numpy(), axis=0)
  45. eval_loss = eval_loss / nb_eval_steps
  46. if args.output_mode == "classification":
  47. preds = np.argmax(preds, axis=1) #得到预测的标签 对预测结果按行取argmax
  48. #把预测的标签 输出为csv文件
  49. pd.DataFrame(preds).to_csv(os.path.join(args.output_dir, "predicted.csv"), index=False)
  50. #preds.to_csv(os.path.join(args.output_dir, "predicted.csv"))
  51. #print(preds)
  52. #elif args.output_mode == "regression":
  53. # preds = np.squeeze(preds)
  54. #result = acc_and_f1(preds, out_label_ids)
  55. #results.update(result)
  56. '''
  57. output_eval_file = os.path.join(eval_output_dir, prefix, "eval_results.txt")
  58. with open(output_eval_file, "w") as writer:
  59. logger.info("***** Eval results {} *****".format(prefix))
  60. for key in sorted(result.keys()):
  61. logger.info(" %s = %s", key, str(result[key]))
  62. writer.write("%s = %s\n" % (key, str(result[key])))
  63. '''

 

5. 使用方式

1)run_classifier.sh

  1. export CUDA_VISIBLE_DEVICES=0,1,2,3 #支持单机多卡
  2. TASK_NAME="THUNews" #任务名 当前处理的数据集
  3. python run.py \
  4. --task_name=$TASK_NAME \
  5. --model_type=bert_cnn \ #使用的模型类型 bert、bert_cnn、xlnet、xlnet_lstm、albert等等 可在项目中扩展
  6. --model_name_or_path ./pretrained_models/bert-base-chinese \ #下载的相应模型版本的三个文件存储路径。建立采用这种2级路径命名
  7. --data_dir ./dataset/THUNews/5_5000 \ #数据集所在路径 csv文件
  8. --output_dir ./results/THUNews/bert_base_chinese_cnn \ #输出结果所在路径 建立采用这种3级目录方式命名,第一级results表示输出结果,第二级表示所处理的数据集,第三级表示所用的模型,由于bert,bert_cnn等都是用的一个bert模型版本,可以结合model_type和所用模型版本进行区分来命名。
  9. --do_train \
  10. --do_eval \
  11. --do_predict \
  12. --do_lower_case \
  13. --max_seq_length=512 \
  14. --per_gpu_train_batch_size=2 \
  15. --per_gpu_eval_batch_size=16 \
  16. --gradient_accumulation_steps=1 \
  17. --learning_rate=2e-5 \
  18. --num_train_epochs=1.0 \
  19. --logging_steps=14923 \
  20. --save_steps=14923 \
  21. --overwrite_output_dir \
  22. --filter_sizes='3,4,5' \
  23. --filter_num=256 \
  24. --lstm_layers=1 \
  25. --lstm_hidden_size=512 \
  26. --lstm_dropout=0.1 \
  27. --gru_layers=1 \
  28. --gru_hidden_size=512 \
  29. --gru_dropout=0.1 \

把相关预训练模型对应的三个文件下载后,保存在指定的路径中。在.sh文件中进行各种超参数的配置即可。

2)运行

bash run_classifier.sh

3)可以设置后台运行以及将输出保存在日志文件中。

详情可见我的另一篇博客:https://blog.csdn.net/sdu_hao/article/details/96594823

 

 

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

闽ICP备14008679号