文本分类(三) | (6) 番外篇(预训练语言模型的另一种使用方式)

完整项目(番外篇)

本篇博客也是对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. 项目组织结构

文本分类(三) | (6) 番外篇(预训练语言模型的另一种使用方式)_第1张图片

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. 项目流程

  • 导入相关类
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])))
    '''

 

5. 使用方式

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

 

 

你可能感兴趣的:(文本分类(三),文本分类(三),预训练语言模型)