赞
踩
本篇博客也是对Github优秀文本分类项目的解析,该文本分类项目,主要基于预训练语言模型,包括bert、xlnet、bert/xlnet + CNN/GRU/LSTM、Albert等,使用PyTorch实现。
项目其实提供了一种预训练语言模型的通用方法,可以将本项目扩展为使用任意的预训练语言模型(包括:albert、xlnet、roberta,t5,gpt等,以及他们与各种深度学习模型的结合)。
目录
相比于文本分类(三)的前五篇博客介绍的项目,同样是基于预训练语言模型,它主要有以下几个不同:
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)二者训练、验证以及测试的细节处理有所不同,整体来说,本项目的处理更加细节、全面。
在THUCNews数据集中抽取25000篇新闻,一共5个类,每一类5000篇。
类别:"体育", "财经", "房产", "家居", "教育"
数据集划分:训练集2w(每个类别4,000条),验证集5000条(每个类别1000条)。另外还抽取了测试集2500条(每个类别500条)。
处理为.csv文件,训练集、验证集格式如下: 类别名称,文本
测试集格式如下:文本
(数据已处理好,可以直接使用)
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:定义了数据预处理函数
- from transformers import (BertConfig,BertTokenizer,
- BertForSequenceClassification,
- BertForSequenceClassification_CNN,
- BertForSequenceClassification_LSTM,
- BertForSequenceClassification_GRU,
- XLNetConfig,XLNetTokenizer,
- XLNetForSequenceClassification,
- XLNetForSequenceClassification_LSTM,
- XLNetForSequenceClassification_GRU,
- AlbertConfig,AlbertTokenizer,
- AlbertForSequenceClassification)
可以在此基础上,继续增加其他的预训练模型以及和深度学习模型的结合。
- MODEL_CLASSES = {
- 'bert': (BertConfig, BertForSequenceClassification, BertTokenizer),
- 'bert_cnn': (BertConfig, BertForSequenceClassification_CNN, BertTokenizer),
- 'bert_lstm': (BertConfig, BertForSequenceClassification_LSTM, BertTokenizer),
- 'bert_gru': (BertConfig, BertForSequenceClassification_GRU, BertTokenizer),
- 'xlnet': (XLNetConfig, XLNetForSequenceClassification, XLNetTokenizer),
- 'xlnet_lstm': (XLNetConfig, XLNetForSequenceClassification_LSTM, XLNetTokenizer),
- 'xlnet_gru': (XLNetConfig, XLNetForSequenceClassification_GRU, XLNetTokenizer),
- 'albert': (AlbertConfig, AlbertForSequenceClassification, AlbertTokenizer)
- }
可以在此基础上,继续增加其他的预训练模型以及和深度学习模型的结合。
- #声明argparse对象
- parser = argparse.ArgumentParser()
-
- #添加命令行参数(required=True的参数必须在命令行设置,其余参数如果不在命令行设置就是用默认值,也可以在命令行设置覆盖默认值)
- ##必须设置的参数
- #处理好的数据集路径 .csv文件所在k路径
- parser.add_argument("--data_dir", default=None, type=str, required=True,
- help="The input data dir. Should contain the .tsv files (or other data files) for the task.")
- #所使用的模型 目前支持bert、bert_cnn、bert_lstm、bert_gru、xlnet、xlnet_gru、xlnet_lstm、albert
- parser.add_argument("--model_type", default=None, type=str, required=True,
- help="Model type selected in the list: " + ", ".join(MODEL_CLASSES.keys()))
- #下载的预训练模型相关文件所在路径(.bin模型参数结构文件,.json模型配置文件,vocab.txt词表文件)
- #注意每种预训练模型都有对应的文件(对应及下载方法 文本分类(三)系列第一篇博客有介绍)
- parser.add_argument("--model_name_or_path", default=None, type=str, required=True,
- help="Path to pre-trained model or shortcut name selected in the list:")
- #任务名字 THUCNews
- parser.add_argument("--task_name", default=None, type=str, required=True,
- help="The name of the task to train selected in the list: " + ", ".join(processors.keys()))
- #模型的预测和checkpoints文件的写入路径
- parser.add_argument("--output_dir", default=None, type=str, required=True,
- help="The output directory where the model predictions and checkpoints will be written.")
-
- ##非必须参数
- parser.add_argument("--config_name", default="", type=str,
- help="Pretrained config name or path if not the same as model_name")
- parser.add_argument("--tokenizer_name", default="", type=str,
- help="Pretrained tokenizer name or path if not the same as model_name")
- parser.add_argument("--cache_dir", default="", type=str,
- help="Where do you want to store the pre-trained models downloaded from s3")
- #输入序列最大长度 (需要对batch中所有的序列进行填充 统一成一个长度)
- parser.add_argument("--max_seq_length", default=128, type=int,
- help="The maximum total input sequence length after tokenization. Sequences longer "
- "than this will be truncated, sequences shorter will be padded.")
- #训练、验证和预测
- parser.add_argument("--do_train", action='store_true',
- help="Whether to run training.")
- parser.add_argument("--do_eval", action='store_true',
- help="Whether to run eval on the dev set.")
- parser.add_argument("--do_predict", action='store_true',
- help="Whether to run predict on the test set.")
-
- parser.add_argument("--evaluate_during_training", action='store_true',
- help="Rul evaluation during training at each logging step.")
- parser.add_argument("--do_lower_case", action='store_true',
- help="Set this flag if you are using an uncased model.")
- #训练阶段和验证阶段 每个gpu上的batch_size
- parser.add_argument("--per_gpu_train_batch_size", default=8, type=int,
- help="Batch size per GPU/CPU for training.")
- parser.add_argument("--per_gpu_eval_batch_size", default=8, type=int,
- help="Batch size per GPU/CPU for evaluation.")
-
- parser.add_argument('--gradient_accumulation_steps', type=int, default=1,
- help="Number of updates steps to accumulate before performing a backward/update pass.")
- #初始学习率
- parser.add_argument("--learning_rate", default=5e-5, type=float,
- help="The initial learning rate for Adam.")
- #正则化系数
- parser.add_argument("--weight_decay", default=0.0, type=float,
- help="Weight decay if we apply some.")
- parser.add_argument("--adam_epsilon", default=1e-8, type=float,
- help="Epsilon for Adam optimizer.")
- parser.add_argument("--max_grad_norm", default=1.0, type=float,
- help="Max gradient norm.")
- #epoch数 一个epoch完整遍历一遍数据集
- parser.add_argument("--num_train_epochs", default=3.0, type=float,
- help="Total number of training epochs to perform.")
- parser.add_argument("--max_steps", default=-1, type=int,
- help="If > 0: set total number of training steps to perform. Override num_train_epochs.")
- parser.add_argument("--warmup_steps", default=0, type=int,
- help="Linear warmup over warmup_steps.")
-
- #每n次更新(每n个batch) 保存一下日志(损失、准确率等信息)
- parser.add_argument('--logging_steps', type=int, default=50,
- help="Log every X updates steps.")
-
- #每n次更新(每n个batch) 保存一次参数
- parser.add_argument('--save_steps', type=int, default=50,
- help="Save checkpoint every X updates steps.")
- parser.add_argument("--eval_all_checkpoints", action='store_true',
- help="Evaluate all checkpoints starting with the same prefix as model_name ending and ending with step number")
- parser.add_argument("--no_cuda", action='store_true',
- help="Avoid using CUDA when available")
- parser.add_argument('--overwrite_output_dir', action='store_true',
- help="Overwrite the content of the output directory")
- parser.add_argument('--overwrite_cache', action='store_true',
- help="Overwrite the cached training and evaluation sets")
- #初始化模型时 使用的随机种子
- parser.add_argument('--seed', type=int, default=42,
- help="random seed for initialization")
-
- parser.add_argument('--fp16', action='store_true',
- help="Whether to use 16-bit (mixed) precision (through NVIDIA apex) instead of 32-bit")
- parser.add_argument('--fp16_opt_level', type=str, default='O1',
- help="For fp16: Apex AMP optimization level selected in ['O0', 'O1', 'O2', and 'O3']."
- "See details at https://nvidia.github.io/apex/amp.html")
- parser.add_argument("--local_rank", type=int, default=-1,
- help="For distributed training: local_rank")
-
- #Additional layer parameters
- #预训练模型后加TextCNN TextCNN相关的参数
- #不同大小卷积核的数量
- parser.add_argument('--filter_num', type=int, default=256, help='number of each size of filter')
- #不同大小卷积核的尺寸
- parser.add_argument('--filter_sizes', type=str, default='3,4,5', help='comma-separated filter sizes to use for convolution')
-
- #预训练模型后加LSTM LSTM相关的参数
- #隐藏单元个数
- parser.add_argument("--lstm_hidden_size", default=300, type=int,
- help="")
- #lstm层数
- parser.add_argument("--lstm_layers", default=2, type=int,
- help="")
- #lstm dropout 丢弃率
- parser.add_argument("--lstm_dropout", default=0.5, type=float,
- help="")
-
- ##预训练模型后加GRU GRU相关的参数
- #隐藏单元个数
- parser.add_argument("--gru_hidden_size", default=300, type=int,
- help="")
- #gru层数
- parser.add_argument("--gru_layers", default=2, type=int,
- help="")
- #gru dropout 丢弃率
- parser.add_argument("--gru_dropout", default=0.5, type=float,
- help="")
- #解析参数
- args = parser.parse_args()
-
- #把不同大小卷积核的尺寸转换为整数 对原有参数进行覆盖
- args.filter_sizes = [int(size) for size in str(args.filter_sizes).split(',')]
- # Setup CUDA, GPU & distributed training(单机多卡/多机分布式)
- if args.local_rank == -1 or args.no_cuda:
- device = torch.device("cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu")
- args.n_gpu = torch.cuda.device_count() #gpu数量
- else: # Initializes the distributed backend which will take care of sychronizing nodes/GPUs
- torch.cuda.set_device(args.local_rank)
- device = torch.device("cuda", args.local_rank)
- torch.distributed.init_process_group(backend='nccl')
- args.n_gpu = 1
- args.device = device
utils.py
- class THUNewsProcessor(DataProcessor):
- """Processor for the SST-2 data set (GLUE version)."""
-
- def get_example_from_tensor_dict(self, tensor_dict):
- """See base class."""
- return InputExample(tensor_dict['idx'].numpy(),
- tensor_dict['sentence'].numpy().decode('utf-8'),
- None,
- str(tensor_dict['label'].numpy()))
-
- #获取训练集、验证集、测试集样本
- #训练集、验证集、测试集提前处理为.csv格式 格式:类别名称,文本
- #通过父类DataProcessor中的_read_csv函数 把csv文件读取为列表 列表中的每一个元素为csv文件的每一行
- def get_train_examples(self, data_dir):
- """See base class."""
- return self._create_examples(self._read_csv(os.path.join(data_dir, "train.csv")), "train")
-
- def get_dev_examples(self, data_dir):
- """See base class."""
- return self._create_examples(self._read_csv(os.path.join(data_dir, "dev.csv")), "dev")
-
- def get_test_examples(self, data_dir):
- """See base class."""
- return self._create_examples(self._read_csv(os.path.join(data_dir, "test.csv")), "test")
-
- def get_labels(self):
- """设置当前数据集的标签"""
- return ["体育", "财经", "房产", "家居", "教育"] #使用了其中5个类别
-
- def _create_examples(self, lines, set_type):
- """Creates examples for the training/dev/test sets."""
- examples = []
- for (i, line) in enumerate(lines): #遍历元组列表,即遍历csv文件每一行/每一条数据
- if i == 0: #跳过表头 labels,text (把数据处理为csv文件时,是保留表头)
- continue
- guid = "%s-%s" % (set_type, i) #set_type 训练集/验证集/测试集
- if set_type == 'test': #测试集没有类别标签 如果是测试集 line[0]是文本 标签统一设置为体育
- text_a = line[0]
- label = '体育'
- else: #验证集和训练集 line[0]是类别标签,line[1]是文本
- label = line[0]
- text_a = line[1]
- #如有两段文本, 也可以设置text_b 句子对任务(问答)
- #把每条数据 转换为InputExample对象
- examples.append(InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
-
- return examples#保存在examples列表中
-
-
-
- tasks_num_labels = { #分类任务的类别数 存储为字典。键名为任务名(小写),值为类别数
- "thunews": 5,
- }
-
- processors = { #任务处理器 存储为字典。键名为任务名(小写),值为处理器类名(自定义)
- "thunews": THUNewsProcessor,
- }
-
- output_modes = { #输出模式 存储为字典。键名为任务名(小写),值为classification。分类任务
- "thunews": "classification",
- }
处理新数据集时,仿照上述格式,添加新数据集的处理器即可。
- #通过MODEL_CLASSES字典 传入model_type,得到相应模型的参数配置类、模型类和切分工具类
- config_class, model_class, tokenizer_class = MODEL_CLASSES[args.model_type]
-
- #获取并加载预训练模型
- config = config_class.from_pretrained(args.config_name if args.config_name else args.model_name_or_path,
- num_labels=num_labels,
- finetuning_task=args.task_name,
- cache_dir=args.cache_dir if args.cache_dir else None)
- tokenizer = tokenizer_class.from_pretrained(args.tokenizer_name if args.tokenizer_name else args.model_name_or_path,
- do_lower_case=args.do_lower_case,
- cache_dir=args.cache_dir if args.cache_dir else None)
- model = model_class.from_pretrained(args.model_name_or_path,
- from_tf=bool('.ckpt' in args.model_name_or_path),
- config=config,
- cache_dir=args.cache_dir if args.cache_dir else None,
- args=args)
utils.py
- def convert_examples_to_features(examples, tokenizer,
- max_length=512,
- task=None,
- label_list=None,
- output_mode=None,
- pad_on_left=False,
- pad_token=0,
- pad_token_segment_id=0,
- mask_padding_with_zero=True):
- #把InputExamples对象 转换为输入特征InputFeatures
- """
- Loads a data file into a list of ``InputFeatures``
- Args:
- examples: List of ``InputExamples`` or ``tf.data.Dataset`` containing the examples.
- tokenizer: Instance of a tokenizer that will tokenize the examples
- max_length: Maximum example length
- task: GLUE task
- label_list: List of labels. Can be obtained from the processor using the ``processor.get_labels()`` method
- output_mode: String indicating the output mode. Either ``regression`` or ``classification``
- pad_on_left: If set to ``True``, the examples will be padded on the left rather than on the right (default)
- pad_token: Padding token
- 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)
- mask_padding_with_zero: If set to ``True``, the attention mask will be filled by ``1`` for actual values
- and by ``0`` for padded values. If set to ``False``, inverts it (``1`` for padded values, ``0`` for
- actual values)
- Returns:
- If the ``examples`` input is a ``tf.data.Dataset``, will return a ``tf.data.Dataset``
- containing the task-specific features. If the input is a list of ``InputExamples``, will return
- a list of task-specific ``InputFeatures`` which can be fed to the model.
- """
- is_tf_dataset = False #是否为tensorflow形式数据集
- if is_tf_available() and isinstance(examples, tf.data.Dataset):
- is_tf_dataset = True
-
- if task is not None:
- processor = processors[task]() #获取自定义任务处理器
- #获取标签列表和输出模式
- if label_list is None:
- label_list = processor.get_labels()
- logger.info("Using label list %s for task %s" % (label_list, task))
- if output_mode is None:
- output_mode = glue_output_modes[task]
- logger.info("Using output mode %s for task %s" % (output_mode, task))
-
- #类别标签 转换为数字索引
- label_map = {label: i for i, label in enumerate(label_list)}
-
- features = []
- for (ex_index, example) in enumerate(examples): #遍历每一个InputExamples对象(每一条数据)
- if ex_index % 10000 == 0:
- logger.info("Writing example %d" % (ex_index))
- if is_tf_dataset:
- example = processor.get_example_from_tensor_dict(example)
- example = processor.tfds_map(example)
-
- #inputs: dict 调用切分工具中的函数 对每个InputExamples对象进行处理 返回一个字典
- inputs = tokenizer.encode_plus(
- example.text_a,
- example.text_b,
- add_special_tokens=True,
- max_length=max_length, #如果序列长于最大长度(统一设置的长度),截断,其他维持原样
- )
- #input_ids: 输入数据token在词汇表中的索引
- #token_type_ids: 分段token索引,类似segment embedding(对于句子对任务 属于句子A的token为0,句子B的token为1,对于分类任务,只有一个输入句子 全为0)
- input_ids, token_type_ids = inputs["input_ids"], inputs["token_type_ids"]
- real_token_len = len(input_ids) #输入序列实际长度 不包含填充
-
- # The mask has 1 for real tokens and 0 for padding tokens. Only real
- # tokens are attended to.
- #非填充部分的token对应1
- attention_mask = [1 if mask_padding_with_zero else 0] * len(input_ids)
-
- # Zero-pad up to the sequence length. 填充部分的长度 >=0
- padding_length = max_length - len(input_ids)
- if pad_on_left: #在输入序列左端填充
- input_ids = ([pad_token] * padding_length) + input_ids
- #填充部分的token 对应0 只对非填充部分的token计算注意力
- attention_mask = ([0 if mask_padding_with_zero else 1] * padding_length) + attention_mask
- token_type_ids = ([pad_token_segment_id] * padding_length) + token_type_ids
- else:#在输入序列右端填充
- input_ids = input_ids + ([pad_token] * padding_length)
- attention_mask = attention_mask + ([0 if mask_padding_with_zero else 1] * padding_length)
- token_type_ids = token_type_ids + ([pad_token_segment_id] * padding_length)
-
- #填充后 输入序列、attention_mask、token_type_ids的长度都等于max_length(统一设置的长度)
- assert len(input_ids) == max_length, "Error with input length {} vs {}".format(len(input_ids), max_length)
- assert len(attention_mask) == max_length, "Error with input length {} vs {}".format(len(attention_mask), max_length)
- assert len(token_type_ids) == max_length, "Error with input length {} vs {}".format(len(token_type_ids), max_length)
-
- if output_mode == "classification": #分类任务 把类别标签 转换为索引
- label = label_map[example.label] #label => index
- elif output_mode == "regression": #回归任务 把标签转换为浮点数
- label = float(example.label)
- else:
- raise KeyError(output_mode)
-
-
- if ex_index < 5: #前5个样本 打印处理效果
- logger.info("*** Example ***")
- logger.info("guid: %s" % (example.guid))
- logger.info("real_token_len: %s" % (real_token_len))
- logger.info("input_ids: %s" % " ".join([str(x) for x in input_ids]))
- logger.info("attention_mask: %s" % " ".join([str(x) for x in attention_mask]))
- logger.info("token_type_ids: %s" % " ".join([str(x) for x in token_type_ids]))
- logger.info("label: %s (id = %d)" % (example.label, label))
-
- #把构造好的样本 转换为InputFeatures对象 添加到features列表中
- features.append(
- InputFeatures(input_ids=input_ids,
- attention_mask=attention_mask,
- token_type_ids=token_type_ids,
- label=label,
- real_token_len=real_token_len))
-
- if is_tf_available() and is_tf_dataset: #TF 格式的数据
- def gen():
- for ex in features:
- yield ({'input_ids': ex.input_ids,
- 'attention_mask': ex.attention_mask,
- 'token_type_ids': ex.token_type_ids},
- ex.label)
-
- return tf.data.Dataset.from_generator(gen,
- ({'input_ids': tf.int32,
- 'attention_mask': tf.int32,
- 'token_type_ids': tf.int32},
- tf.int64),
- ({'input_ids': tf.TensorShape([None]),
- 'attention_mask': tf.TensorShape([None]),
- 'token_type_ids': tf.TensorShape([None])},
- tf.TensorShape([])))
-
- return features
- def load_and_cache_examples(args, task, tokenizer, evaluate=False, predict=False):
- '''
- 将dataset转换为features,并保存在目录cached_features_file中。
-
- args:
- evaluate: False. 若为True,则对dev.csv进行转换
- predict: False. 若为True,则对test.csv进行转换
- return:
- dataset
- '''
-
- if args.local_rank not in [-1, 0] and not evaluate:
- torch.distributed.barrier() # Make sure only the first process in distributed training process the dataset, and the others will use the cache
-
- processor = processors[task]() #THUNewsProcessor() 自定义的任务处理器
- output_mode = output_modes[task] #classification
-
- # Load data features from cache or dataset file
- #cached_features_file 为数据集构造的特征的保存目录
- if evaluate:
- exec_model = 'dev'
- elif predict:
- exec_model = 'test'
- else:
- exec_model = 'train'
-
- #特征保存目录的命名
- cached_features_file = os.path.join(args.data_dir, 'cached_{}_{}_{}_{}'.format(
- exec_model,
- list(filter(None, args.model_name_or_path.split('/'))).pop(),
- str(args.max_seq_length),
- str(task)))
-
- #如果对数据集已经构造好特征了 直接加载 避免重复处理
- if os.path.exists(cached_features_file) and not args.overwrite_cache:
- logger.info("Loading features from cached file %s\n", cached_features_file)
- features = torch.load(cached_features_file)
- #否则对数据集进行处理 得到特征
- else:
- logger.info("Creating features from dataset file at %s", args.data_dir)
- label_list = processor.get_labels() #标签列表
- #对验证集、测试集、训练集进行处理 把数据转换为InputExample对象
- if evaluate:
- examples = processor.get_dev_examples(args.data_dir)
- elif predict:
- examples = processor.get_test_examples(args.data_dir)
- else:
- examples = processor.get_train_examples(args.data_dir)
- #转换为特征
- #注意xlnet系列模型的数据预处理方式 和 bert系列稍有不同
- features = convert_examples_to_features(examples,
- tokenizer,
- label_list=label_list,
- max_length=args.max_seq_length,
- output_mode=output_mode,
- pad_on_left=bool(args.model_type in ['xlnet']), # pad on the left for xlnet
- pad_token=tokenizer.convert_tokens_to_ids([tokenizer.pad_token])[0],
- pad_token_segment_id=4 if args.model_type in ['xlnet'] else 0,
- )
- #把数据处理为InputFeatures对象后 存储为cached_features_file
- if args.local_rank in [-1, 0]:
- logger.info("Saving features into cached file %s", cached_features_file)
- torch.save(features, cached_features_file)
-
- if args.local_rank == 0 and not evaluate:
- torch.distributed.barrier() # Make sure only the first process in distributed training process the dataset, and the others will use the cache
-
- # Convert to Tensors and build dataset
- all_input_ids = torch.tensor([f.input_ids for f in features], dtype=torch.long)
- all_attention_mask = torch.tensor([f.attention_mask for f in features], dtype=torch.long)
- all_token_type_ids = torch.tensor([f.token_type_ids for f in features], dtype=torch.long)
- if output_mode == "classification":
- all_labels = torch.tensor([f.label for f in features], dtype=torch.long)
- elif output_mode == "regression":
- all_labels = torch.tensor([f.label for f in features], dtype=torch.float)
- #构建dataset对象
- dataset = TensorDataset(all_input_ids, all_attention_mask, all_token_type_ids, all_labels)
- return dataset
- def train(args, train_dataset, model, tokenizer):
- """ Train the model """
- if args.local_rank in [-1, 0]:
- tb_writer = SummaryWriter()
-
- args.train_batch_size = args.per_gpu_train_batch_size * max(1, args.n_gpu) #得到训练时的batch大小
- #定义采样方式
- train_sampler = RandomSampler(train_dataset) if args.local_rank == -1 else DistributedSampler(train_dataset)
- #构建dataloader
- train_dataloader = DataLoader(train_dataset, sampler=train_sampler, batch_size=args.train_batch_size)
-
- if args.max_steps > 0:
- t_total = args.max_steps
- args.num_train_epochs = args.max_steps // (len(train_dataloader) // args.gradient_accumulation_steps) + 1
- else:
- t_total = len(train_dataloader) // args.gradient_accumulation_steps * args.num_train_epochs
-
- # Prepare optimizer and schedule (linear warmup and decay)
- #以下参数 不使用正则化
- no_decay = ['bias', 'LayerNorm.weight']
- optimizer_grouped_parameters = [
- {'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},
- {'params': [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
- ]
-
- #定义优化器
- optimizer = AdamW(optimizer_grouped_parameters, lr=args.learning_rate, eps=args.adam_epsilon)
- scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=args.warmup_steps, num_training_steps=t_total)
- if args.fp16:
- try:
- from apex import amp
- except ImportError:
- raise ImportError("Please install apex from https://www.github.com/nvidia/apex to use fp16 training.")
- model, optimizer = amp.initialize(model, optimizer, opt_level=args.fp16_opt_level)
-
- # multi-gpu training (should be after apex fp16 initialization)
- if args.n_gpu > 1: #单机多卡
- model = torch.nn.DataParallel(model)
-
- # Distributed training (should be after apex fp16 initialization)
- #多机分布式
- if args.local_rank != -1:
- model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank],
- output_device=args.local_rank,
- find_unused_parameters=True)
-
- # Train!
- logger.info("***** Running training *****")
- logger.info(" Num examples = %d", len(train_dataset))
- logger.info(" Num Epochs = %d", args.num_train_epochs)
- logger.info(" Instantaneous batch size per GPU = %d", args.per_gpu_train_batch_size)
- logger.info(" Total train batch size (w. parallel, distributed & accumulation) = %d",
- args.train_batch_size * args.gradient_accumulation_steps * (torch.distributed.get_world_size() if args.local_rank != -1 else 1))
- logger.info(" Gradient Accumulation steps = %d", args.gradient_accumulation_steps)
- logger.info(" Total optimization steps = %d", t_total)
-
- global_step = 0
- tr_loss, logging_loss = 0.0, 0.0
- model.zero_grad()
- train_iterator = trange(int(args.num_train_epochs), desc="Epoch", disable=args.local_rank not in [-1, 0])
- set_seed(args) # Added here for reproductibility (even between python 2 and 3)
-
- for _ in train_iterator:
- epoch_iterator = tqdm(train_dataloader, desc="Iteration", disable=args.local_rank not in [-1, 0])
- for step, batch in enumerate(epoch_iterator):
- model.train()#训练模式
- batch = tuple(t.to(args.device) for t in batch) #把输入数据 放到设备上
- #分类任务是单句子 不需要设置token_type_ids 默认全为0 batch[2]
- #定义模型的输入参数 字典形式
- inputs = {'input_ids': batch[0],
- 'attention_mask': batch[1],
- 'labels': batch[3]}
- if args.model_type != 'distilbert':
- 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
-
- if args.model_type in ['bert_cnn']:
- #inputs['real_token_len'] = batch[4]
- pass
- outputs = model(**inputs) #得到模型输出 使用的是XXForSequenceClassification类 他的第一个返回值是损失
- loss = outputs[0] # model outputs are always tuple in transformers (see doc)
-
- if args.n_gpu > 1:
- loss = loss.mean() # mean() to average on multi-gpu parallel training
- if args.gradient_accumulation_steps > 1:
- loss = loss / args.gradient_accumulation_steps #每个batch都将loss除以gradient_accumulation_steps
-
- #计算梯度
- if args.fp16:
- with amp.scale_loss(loss, optimizer) as scaled_loss:
- scaled_loss.backward()
- else:
- loss.backward()
-
- epoch_iterator.set_description("loss {}".format(round(loss.item(), 5)))
-
- tr_loss += loss.item()
- #每gradient_accumulation_steps个batch(把这些batch的梯度求和) 更新一次参数
- if (step + 1) % args.gradient_accumulation_steps == 0: #过gradient_accumulation_steps后才将梯度清零,不是每次更新/每过一个batch清空一次梯度,即每gradient_accumulation_steps次更新清空一次
- #梯度裁剪
- if args.fp16:
- torch.nn.utils.clip_grad_norm_(amp.master_params(optimizer), args.max_grad_norm)
- else:
- torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm)
-
- #反向传播更新参数
- optimizer.step()
- #更新学习率
- scheduler.step() # Update learning rate schedule
- #清空梯度
- model.zero_grad()
- global_step += 1
-
- #每logging_steps,进行evaluate
- if args.local_rank in [-1, 0] and args.logging_steps > 0 and global_step % args.logging_steps == 0:
- logs = {}
- if args.local_rank == -1 and args.evaluate_during_training: # Only evaluate when single GPU otherwise metrics may not average well
- #验证
- results = evaluate(args, model, tokenizer)
- for key, value in results.items():
- eval_key = 'eval_{}'.format(key)
- logs[eval_key] = value
-
- loss_scalar = (tr_loss - logging_loss) / args.logging_steps
- learning_rate_scalar = scheduler.get_lr()[0]
- logs['learning_rate'] = learning_rate_scalar
- logs['loss'] = loss_scalar
- logging_loss = tr_loss
-
- for key, value in logs.items():
- tb_writer.add_scalar(key, value, global_step)
- print(json.dumps({**logs, **{'step': global_step}}))
-
- #每save_steps保存checkpoint
- if args.local_rank in [-1, 0] and args.save_steps > 0 and global_step % args.save_steps == 0:
- # Save model checkpoint 保存参数
- output_dir = os.path.join(args.output_dir, 'checkpoint-{}'.format(global_step))
- if not os.path.exists(output_dir):
- os.makedirs(output_dir)
- #单机多卡和多机分布式 保存参数有所不同,不是直接保存model 需要保存model.moudle
- model_to_save = model.module if hasattr(model, 'module') else model # Take care of distributed/parallel training
- model_to_save.save_pretrained(output_dir)
- torch.save(args, os.path.join(output_dir, 'training_args.bin'))
- logger.info("Saving model checkpoint to %s", output_dir)
-
- if args.max_steps > 0 and global_step > args.max_steps:
- epoch_iterator.close()
- break
- if args.max_steps > 0 and global_step > args.max_steps:
- train_iterator.close()
- break
-
- if args.local_rank in [-1, 0]:
- tb_writer.close()
-
- return global_step, tr_loss / global_step
- def evaluate(args, model, tokenizer, prefix=""):
- results = {}
- #构建验证数据集 Dataset对象
- eval_dataset = load_and_cache_examples(args, args.task_name, tokenizer, evaluate=True)
-
- if not os.path.exists(args.output_dir) and args.local_rank in [-1, 0]:
- os.makedirs(args.output_dir)
- #验证阶段batch大小
- args.eval_batch_size = args.per_gpu_eval_batch_size * max(1, args.n_gpu)
- # Note that DistributedSampler samples randomly
- #定义采样方式
- eval_sampler = SequentialSampler(eval_dataset)
- #构建Dataloader
- eval_dataloader = DataLoader(eval_dataset, sampler=eval_sampler, batch_size=args.eval_batch_size)
-
- # multi-gpu eval 单机多卡
- if args.n_gpu > 1:
- model = torch.nn.DataParallel(model)
-
- # Eval!
- logger.info("***** Running evaluation {} *****".format(prefix))
- logger.info(" Num examples = %d", len(eval_dataset))
- logger.info(" Batch size = %d", args.eval_batch_size)
- eval_loss = 0.0
- nb_eval_steps = 0
- preds = None #为预测值
- out_label_ids = None #为真实标签
- for batch in tqdm(eval_dataloader, desc="Evaluating"):
- model.eval() #验证模式
- batch = tuple(t.to(args.device) for t in batch) #输入数据 转移到device上
-
- with torch.no_grad(): #关闭梯度计算
- #构建模型输入 字典形式。 token_type_ids为batch[2] 分类任务为单输入句子 默认全为0
- inputs = {'input_ids': batch[0],
- 'attention_mask': batch[1],
- 'labels': batch[3]}
- if args.model_type != 'distilbert':
- 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
- outputs = model(**inputs) #获得模型输出
- tmp_eval_loss, logits = outputs[:2] #模型输出的前两项为loss和logits
-
- eval_loss += tmp_eval_loss.mean().item()
- nb_eval_steps += 1
- if preds is None:
- preds = logits.detach().cpu().numpy()
- out_label_ids = inputs['labels'].detach().cpu().numpy()
- else:
- preds = np.append(preds, logits.detach().cpu().numpy(), axis=0)
- out_label_ids = np.append(out_label_ids, inputs['labels'].detach().cpu().numpy(), axis=0)
-
- eval_loss = eval_loss / nb_eval_steps
- if args.output_mode == "classification":
- preds = np.argmax(preds, axis=1)
- elif args.output_mode == "regression":
- preds = np.squeeze(preds)
- #preds为模型预测的标签 对预测结果按行取argmax
- #和真实标签计算准确率和f1-score
- result = acc_and_f1(preds, out_label_ids)
- results.update(result)
-
- #把相关指标计算结果 保存
- output_eval_file = os.path.join(args.output_dir, prefix, "eval_results.txt")
- with open(output_eval_file, "w") as writer:
- logger.info("***** Eval results {} *****".format(prefix))
- for key in sorted(result.keys()):
- logger.info(" %s = %s", key, str(result[key]))
- writer.write("%s = %s\n" % (key, str(result[key])))
-
- return results
- def predict(args, model, tokenizer, prefix=""):
- #results = {}
- #构建测试数据集 Dataset对象
- pred_dataset = load_and_cache_examples(args, args.task_name, tokenizer, predict=True)
-
- if not os.path.exists(args.output_dir) and args.local_rank in [-1, 0]:
- os.makedirs(args.output_dir)
- #测试阶段batch大小
- args.eval_batch_size = args.per_gpu_eval_batch_size * max(1, args.n_gpu)
- # Note that DistributedSampler samples randomly
- #定义采样方式
- eval_sampler = SequentialSampler(pred_dataset)
- #测试集Dataloader
- eval_dataloader = DataLoader(pred_dataset, sampler=eval_sampler, batch_size=args.eval_batch_size)
-
- # multi-gpu eval 单机多卡
- if args.n_gpu > 1:
- model = torch.nn.DataParallel(model)
-
- # Eval!
- logger.info("***** Running predict {} *****".format(prefix))
- logger.info(" Num examples = %d", len(pred_dataset))
- logger.info(" Batch size = %d", args.eval_batch_size)
- eval_loss = 0.0
- nb_eval_steps = 0
- preds = None #为预测值
- #out_label_ids = None #为真实标签
- for batch in tqdm(eval_dataloader, desc="Predicting"):
- model.eval() #测试模式
- batch = tuple(t.to(args.device) for t in batch)#把测试数据转移到设备上
-
- with torch.no_grad():#关闭梯度计算
- #构建模型输入 字典形式。 token_type_ids为batch[2] 分类任务为单输入句子 默认全为0
- inputs = {'input_ids': batch[0],
- 'attention_mask': batch[1],
- 'labels': batch[3]}
- if args.model_type != 'distilbert':
- 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
- outputs = model(**inputs) #得到模型输出
- tmp_eval_loss, logits = outputs[:2] #前两项为loss、logits
-
- eval_loss += tmp_eval_loss.mean().item()
- nb_eval_steps += 1
- if preds is None:
- preds = logits.detach().cpu().numpy()
- #out_label_ids = inputs['labels'].detach().cpu().numpy()
- else:
- preds = np.append(preds, logits.detach().cpu().numpy(), axis=0)
- #out_label_ids = np.append(out_label_ids, inputs['labels'].detach().cpu().numpy(), axis=0)
-
- eval_loss = eval_loss / nb_eval_steps
- if args.output_mode == "classification":
- preds = np.argmax(preds, axis=1) #得到预测的标签 对预测结果按行取argmax
-
- #把预测的标签 输出为csv文件
- pd.DataFrame(preds).to_csv(os.path.join(args.output_dir, "predicted.csv"), index=False)
- #preds.to_csv(os.path.join(args.output_dir, "predicted.csv"))
- #print(preds)
-
-
- #elif args.output_mode == "regression":
- # preds = np.squeeze(preds)
- #result = acc_and_f1(preds, out_label_ids)
- #results.update(result)
-
- '''
- output_eval_file = os.path.join(eval_output_dir, prefix, "eval_results.txt")
- with open(output_eval_file, "w") as writer:
- logger.info("***** Eval results {} *****".format(prefix))
- for key in sorted(result.keys()):
- logger.info(" %s = %s", key, str(result[key]))
- writer.write("%s = %s\n" % (key, str(result[key])))
- '''
1)run_classifier.sh
- export CUDA_VISIBLE_DEVICES=0,1,2,3 #支持单机多卡
-
- TASK_NAME="THUNews" #任务名 当前处理的数据集
-
- python run.py \
- --task_name=$TASK_NAME \
- --model_type=bert_cnn \ #使用的模型类型 bert、bert_cnn、xlnet、xlnet_lstm、albert等等 可在项目中扩展
- --model_name_or_path ./pretrained_models/bert-base-chinese \ #下载的相应模型版本的三个文件存储路径。建立采用这种2级路径命名
- --data_dir ./dataset/THUNews/5_5000 \ #数据集所在路径 csv文件
- --output_dir ./results/THUNews/bert_base_chinese_cnn \ #输出结果所在路径 建立采用这种3级目录方式命名,第一级results表示输出结果,第二级表示所处理的数据集,第三级表示所用的模型,由于bert,bert_cnn等都是用的一个bert模型版本,可以结合model_type和所用模型版本进行区分来命名。
- --do_train \
- --do_eval \
- --do_predict \
- --do_lower_case \
- --max_seq_length=512 \
- --per_gpu_train_batch_size=2 \
- --per_gpu_eval_batch_size=16 \
- --gradient_accumulation_steps=1 \
- --learning_rate=2e-5 \
- --num_train_epochs=1.0 \
- --logging_steps=14923 \
- --save_steps=14923 \
- --overwrite_output_dir \
- --filter_sizes='3,4,5' \
- --filter_num=256 \
- --lstm_layers=1 \
- --lstm_hidden_size=512 \
- --lstm_dropout=0.1 \
- --gru_layers=1 \
- --gru_hidden_size=512 \
- --gru_dropout=0.1 \
把相关预训练模型对应的三个文件下载后,保存在指定的路径中。在.sh文件中进行各种超参数的配置即可。
2)运行
bash run_classifier.sh
3)可以设置后台运行以及将输出保存在日志文件中。
详情可见我的另一篇博客:https://blog.csdn.net/sdu_hao/article/details/96594823
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。