赞
踩
动态词向量方法CoVe和ELMo将词表示从静态转变到动态,同时也在多个自然语言处理任务中显著地提升了性能。随后,以GPT和BERT为代表的基于大规模文本训练出的预训练语言模型(Pre-trained Language Model,PLM)已成为目前主流的文本表示模型。
预训练模型并不是自然语言处理领域的“首创”技术。在计算机视觉(Com-puter Vision,CV)领域,以ImageNet为代表的大规模图像数据为图像识别、图像分割等任务提供了良好的数据基础。
在CV领域,通常会使用ImageNet进行一次预训练,让模型从海量图像中充分学习如何从图像中提取特征。然后根据具体的目标任务,使用相应的领域数据精调,使模型进一步“靠近”目标任务的应用场景,起到领域适配和任务适配的作用。“预训练+精调”模式
在自然语言处理领域,预训练模型通常指代预训练语言模型。
2018年,以GPT和BERT为代表的基于深层Transformer的表示模型出现,预训练语言模型被大家广泛熟知。
预训练语言模型三大特点:
预训练数据需要讲究“保质”和“保量”。
数据规模和模型规模在一定程度上正相关。在小数据上训练模型时,通常模型的规模不会太大,避免过拟合。在大数据上训练模型时,若不增大模型规模,可能会造成新的知识无法存放的情况,从而无法完全涵盖大数据中丰富的语义信息。
容量大 → 参数量大
如何设计考虑:
基于Transformer的神经网络模型成为目前构建预训练语言模型的最佳选择。
深度学习计算设备——图形处理单元(Graphics Processing Unit,GPU)
后起之秀——张量处理单元(TensorProcessing Unit,TPU)
图形处理单元(GPU,俗称显卡)
随着GPU核心的不断升级,计算能力和计算速度得到大幅提升,不仅可以作为常规的图形处理设备,同时也可以成为深度学习领域的计算设备。
CPU擅长处理串行运算以及逻辑控制和跳转,而GPU更擅长大规模并行运算。
英伟达生产的GPU依靠与之匹配的统一计算设备架构(Compute Unified Device Archi-tecture,CUDA)能够更好地处理复杂的计算问题,同时深度优化多种深度学习基本运算指令。
英伟达Volta系列硬件
张量处理单元(TPU)
谷歌公司近年定制开发的专用集成电路(Appli-cation Specific Integrated Circuit,ASIC)。
目前,TPU主要支持TensorFlow深度学习框架,只能通过谷歌云服务器访问使用。
OpenAI 公司在2018年提出了一种生成式预训练(Generative Pre-Training,GPT)模型用来提升自然语言理解任务的效果,正式将自然语言处理带入“预训练”时代。
自然语言处理新范式:生成式预训练 + 判别式任务精调
GPT的整体结构是一个基于Transformer的单向语言模型,即从左至右对输入文本建模,使用多层Transformer作为模型的基本结构。
在预训练阶段,GPT利用大规模数据训练出基于深层Transformer的语言模型,已经掌握了文本的通用语义表示。精调(Fine-tuning)的目的是在通用语义表示的基础上,根据下游任务(Downstream task)的特性进行领域适配,使之与下游任务的形式更加契合,以获得更好的下游任务应用效果。
下游任务精调通常是由有标注数据进行训练和优化的。为了进一步提升精调后模型的通用性以及收敛速度,可以在下游任务精调时加入一定权重的预训练任务损失。这样做是为了缓解在下游任务精调的过程中出现灾难性遗忘(Catastrophic Forgetting)问题。
单句文本分类
输入由单个文本构成,输出由对应的分类标签构成。
文本蕴含
输入由两段文本构成,输出由分类标签构成,用于判断两段文本之间的蕴含关系。
相似度计算
相似度计算任务也由两段文本构成。但与文本蕴含任务不同的是,参与相似度计算的两段文本之间不存在顺序关系。
选择性阅读理解
要将〈篇章,问题,选项〉作为输入,以正确选项编号作为标签,任务是让机器阅读一篇文章,需要从多个选项中选择出问题对应的正确选项。
BERT(Bidirectional Encoder Representation from Transformers)由Devlin等人在2018年提出的基于深层Transformer的预训练语言模型。BERT不仅充分利用了大规模无标注文本来挖掘其中丰富的语义信息,同时还进一步加深了自然语言处理模型的深度。
BERT的基本模型结构由多层Transformer构成,包含两个预训练任务:掩码语言模型(Masked Language Model,MLM)和下一个句子预测(Next Sentence Prediction,NSP)
在预训练阶段的输入形式统一为两段文本拼接的形式。
BERT的输入表示由词向量、块向量和位置向量之和组成。
词向量
通过词向量矩阵将输入文本转换成实值向量表示。
块向量
编码当前词属于哪一个块,输入序列中每个词对应的块编码(Segment Encoding)为当前词所在块的序号(从0开始计数)。
位置向量
来编码每个词的绝对位置。
引入基于自编码(Auto-Encoding)的预训练任务进行训练。BERT的基本预训练任务由掩码语言模型和下一个句子预测构成。
掩码语言模型
为了真正实现文本的双向建模,即当前时刻的预测同时依赖于“历史”和“未来”,BERT采用了一种类似完形填空(Cloze)的做法,并称之为掩码语言模型(MLM)。
MLM预训练任务直接将输入文本中的部分单词掩码(Mask),并通过深层Transformer模型还原为原单词,从而避免了双向语言模型带来的信息泄露问题,迫使模型使用被掩码词周围的上下文信息还原掩码位置的词。
建模方法:
输入层
BERT编码层
输出层
代码实现
def create_masker_lm_predictions(tokens,masked_lm_prob,max_predictions_per_seq,vocab_words,rng): """ 创建MLM任务的训练数据 :param tokens: 输入文本 :param masked_lm_prob:掩码语言模型的掩码概率 :param max_predictions_per_seq: 每个序列的最大预测数目 :param vocab_words: 词表列表 :param rng: 随机数生成器 :return: """ cand_indexes = [] for ( i , token) in enumerate(tokens): # 掩码时跳过[CLS]和[SEP] if token == "[CLS]" or token == "[SEP]": continue cand_indexes.append([i]) rng.shuffle(cand_indexes) # 随机打乱所有下标 output_tokens = list(tokens) # 计算预测数目 num_to_predict = min(max_predictions_per_seq,max(1,int(round(len(tokens) * masked_lm_prob)))) masked_lms = [] # 存储掩码实例 covered_indexes = set() # 存储已经处理过的下标 for index in cand_indexes: if len(masked_lms) >= num_to_predict: break if index in covered_indexes: continue covered_indexes.add(index) masked_token = None # 80%概率替换为[MASK] if rng.random() < 0.8: masked_token = "[MASK]" else: # 以10%的概率不进行任何替换 if rng.random() < 0.5: masked_token = tokens[index] # 以10%的概率替换成词表中的随机词 else: masked_token = vocab_words[rng.randint(0,len(vocab_words) - 1)] output_tokens[index] = masked_token # 设置为被掩码的token masked_lms.append(MaskedLmInstance(index = index ,label = tokens[index])) masked_lms = sorted(masked_lms,key=lambda x:x.index) masked_lm_positions = [] # 存储需要掩码的下标 masked_lm_labels = [] # 存储掩码前的原词,还原目标 for p in masked_lms: masked_lm_positions.append(p.index) masked_lm_labels.append(p.label) return (output_tokens,masked_lm_positions,masked_lm_labels)
下一个句子预测(NSP)
构建两段文本之间的关系。NSP任务是一个二分类任务,需要判断句子B是否是句子A的下一个句子。
训练样本:
建模方法
输入层
对于给定的经过掩码处理后的输入文本→BERT的输入表示。
BERT编码层
输入表示v经过L层Transformer的编码,借助自注意力机制充分学习文本中每个词之间的语义关联,最终得到输入文本的上下文语义表示。
输出层
判断输入文本x(2)是否是x(1)的下一个句子
可将MLM任务替换为如下两种进阶预训练任务,以进一步提升预训练难度,从而挖掘出更加丰富的文本语义信息。
整词掩码
在MLM任务中,最小的掩码单位是WordPiece子词,存在一定的信息泄露。
整词掩码(Whole Word Masking,WWM)预训练任务的提出解决了Word-Piece子词信息泄露的问题。最小掩码单位由WordPiece子词变为整词。
正确理解整词掩码
在整词掩码中,当发生掩码操作时:
中文整词掩码
可将中文的字(Character)类比为英文中的WordPiece子词,进而应用整词掩码技术。
N-gram掩码
将连续的N-gram文本进行掩码,并要求模型还原缺失内容。
具体操作流程:
三种掩码策略的区别
BERT模型的两种应用方式
特征提取
仅利用BERT提取输入文本特征,生成对应的上下文语义表示,而BERT本身不参与目标任务的训练,即BERT部分只进行解码(无梯度回传)。
模型精调
利用BERT作为下游任务模型基底,生成文本对应的上下文语义表示,并参与下游任务的训练,在下游任务学习过程中,BERT对自身参数进行更新。
建模方法
单句文本分类(Single Sentence Classification,SSC)任务是最常见的自然语言处理任务,需要将输入文本分成不同类别。
输入层
对于一个给定的经过WordPiece分词后的句子x1x2··· xn,进行如下处理得到BERT的原始输入X。
BERT编码层
输入表示v经过多层Transformer的编码,借助自注意力机制充分学习句子中每个词之间的语义关联,并最终得到句子的上下文语义表示。
分类输出层
在得到[CLS]位的隐含层表示h0后,通过一个全连接层预测输入文本对应的分类标签。
代码实现
import numpy as np from datasets import load_dataset, load_metric from transformers import BertTokenizerFast, BertForSequenceClassification, TrainingArguments, Trainer # 加载训练数据、分词器、预训练模型以及评价方法 dataset = load_dataset('glue', 'sst2') tokenizer = BertTokenizerFast.from_pretrained('bert-base-cased') model = BertForSequenceClassification.from_pretrained('bert-base-cased', return_dict=True) metric = load_metric('glue', 'sst2') # 对训练集进行分词 def tokenize(examples): return tokenizer(examples['sentence'], truncation=True, padding='max_length') dataset = dataset.map(tokenize, batched=True) encoded_dataset = dataset.map(lambda examples: {'labels': examples['label']}, batched=True) # 将数据集格式化为torch.Tensor类型以训练PyTorch模型 columns = ['input_ids', 'token_type_ids', 'attention_mask', 'labels'] encoded_dataset.set_format(type='torch', columns=columns) # 定义评价指标 def compute_metrics(eval_pred): predictions, labels = eval_pred return metric.compute(predictions=np.argmax(predictions, axis=1), references=labels) # 定义训练参数TrainingArguments,默认使用AdamW优化器 args = TrainingArguments( "ft-sst2", # 输出路径,存放检查点和其他输出文件 evaluation_strategy="epoch", # 定义每轮结束后进行评价 learning_rate=2e-5, # 定义初始学习率 per_device_train_batch_size=16, # 定义训练批次大小 per_device_eval_batch_size=16, # 定义测试批次大小 num_train_epochs=2, # 定义训练轮数 ) # 定义Trainer,指定模型和训练参数,输入训练集、验证集、分词器以及评价函数 trainer = Trainer( model, args, train_dataset=encoded_dataset["train"], eval_dataset=encoded_dataset["validation"], tokenizer=tokenizer, compute_metrics=compute_metrics ) # 开始训练!(主流GPU上耗时约几小时) trainer.train()
建模方法
句对文本分类(Sentence Pair Classification,SPC)任务与单句文本分类任务类似,需要将一对文本分成不同类别。应用BERT处理句对文本分类任务的模型与单句文本分类模型类似,仅在输入层有所区别。
输入层
对于一对给定的经过 WordPiece 分词后的句子,将其拼接得到BERT的原始输入X和输入表示v。
BERT层
分类输出层
代码实现
import numpy as np from datasets import load_dataset, load_metric from transformers import BertTokenizerFast, BertForSequenceClassification, TrainingArguments, Trainer # 加载训练数据、分词器、预训练模型以及评价方法 dataset = load_dataset('glue', 'rte') tokenizer = BertTokenizerFast.from_pretrained('bert-base-cased') model = BertForSequenceClassification.from_pretrained('bert-base-cased', return_dict=True) metric = load_metric('glue', 'rte') # 对训练集进行分词 def tokenize(examples): return tokenizer(examples['sentence1'], examples['sentence2'], truncation=True, padding='max_length') dataset = dataset.map(tokenize, batched=True) encoded_dataset = dataset.map(lambda examples: {'labels': examples['label']}, batched=True) # 将数据集格式化为torch.Tensor类型以训练PyTorch模型 columns = ['input_ids', 'token_type_ids', 'attention_mask', 'labels'] encoded_dataset.set_format(type='torch', columns=columns) # 定义评价指标 def compute_metrics(eval_pred): predictions, labels = eval_pred return metric.compute(predictions=np.argmax(predictions, axis=1), references=labels) # 定义训练参数TrainingArguments,默认使用AdamW优化器 args = TrainingArguments( "ft-rte", # 输出路径,存放检查点和其他输出文件 evaluation_strategy="epoch", # 定义每轮结束后进行评价 learning_rate=2e-5, # 定义初始学习率 per_device_train_batch_size=16, # 定义训练批次大小 per_device_eval_batch_size=16, # 定义测试批次大小 num_train_epochs=2, # 定义训练轮数 ) # 定义Trainer,指定模型和训练参数,输入训练集、验证集、分词器以及评价函数 trainer = Trainer( model, args, train_dataset=encoded_dataset["train"], eval_dataset=encoded_dataset["validation"], tokenizer=tokenizer, compute_metrics=compute_metrics ) trainer.train()
建模方法
抽取式阅读理解主要由篇章(Passage)、问题(Question)和答案(Answer)构成,要求机器在阅读篇章和问题后给出相应的答案,而答案要求是从篇章中抽取出的一个文本片段(Span)。
输入层
对问题Q= q1q2··· qn和篇章P= p1p2··· pm (P和Q均经过WordPiece分词后得到)拼接得到BERT的原始输入序列X
BERT编码层
输入表示v经过多层Transformer的编码,借助自注意力机制充分学习篇章和问题之间的语义关联,并最终得到上下文语义表示。
答案输出层
在得到输入序列的上下文语义表示h后,通过全连接层,将每个分量(对应输入序列的每个位置)压缩为一个标量,并通过Softmax函数预测每个时刻成为答案起始位置的概率Ps以及终止位置的概率Pe。
解码方法
在得到起始位置以及终止位置的概率后,使用简单的基于Top-k 的答案抽取方法获得最终答案。
代码实现
import numpy as np from datasets import load_dataset, load_metric from transformers import BertTokenizerFast, BertForQuestionAnswering, TrainingArguments, Trainer, default_data_collator # 加载训练数据、分词器、预训练模型以及评价方法 dataset = load_dataset('squad') tokenizer = BertTokenizerFast.from_pretrained('bert-base-cased') model = BertForQuestionAnswering.from_pretrained('bert-base-cased', return_dict=True) metric = load_metric('squad') # 准备训练数据并转换为feature def prepare_train_features(examples): tokenized_examples = tokenizer( examples["question"], # 问题文本 examples["context"], # 篇章文本 truncation="only_second", # 截断只发生在第二部分,即篇章 max_length=384, # 设定最大长度为384 stride=128, # 设定篇章切片步长为128 return_overflowing_tokens=True, # 返回超出最大长度的标记,将篇章切成多片 return_offsets_mapping=True, # 返回偏置信息,用于对齐答案位置 padding="max_length", # 按最大长度进行补齐 ) # 如果篇章很长,则可能会被切成多个小篇章,需要通过以下函数建立feature到example的映射关系 sample_mapping = tokenized_examples.pop("overflow_to_sample_mapping") # 建立token到原文的字符级映射关系,用于确定答案的开始和结束位置 offset_mapping = tokenized_examples.pop("offset_mapping") # 获取开始和结束位置 tokenized_examples["start_positions"] = [] tokenized_examples["end_positions"] = [] for i, offsets in enumerate(offset_mapping): # 获取输入序列的input_ids以及[CLS]标记的位置(在BERT中为第0位) input_ids = tokenized_examples["input_ids"][i] cls_index = input_ids.index(tokenizer.cls_token_id) # 获取哪些部分是问题,哪些部分是篇章 sequence_ids = tokenized_examples.sequence_ids(i) # 获取答案在文本中的字符级开始和结束位置 sample_index = sample_mapping[i] answers = examples["answers"][sample_index] start_char = answers["answer_start"][0] end_char = start_char + len(answers["text"][0]) # 获取在当前切片中的开始和结束位置 token_start_index = 0 while sequence_ids[token_start_index] != 1: token_start_index += 1 token_end_index = len(input_ids) - 1 while sequence_ids[token_end_index] != 1: token_end_index -= 1 # 检测答案是否超出当前切片的范围 if not (offsets[token_start_index][0] <= start_char and offsets[token_end_index][1] >= end_char): # 超出范围时,答案的开始和结束位置均设置为[CLS]标记的位置 tokenized_examples["start_positions"].append(cls_index) tokenized_examples["end_positions"].append(cls_index) else: # 将token_start_index和token_end_index移至答案的两端 while token_start_index < len(offsets) and offsets[token_start_index][0] <= start_char: token_start_index += 1 tokenized_examples["start_positions"].append(token_start_index - 1) while offsets[token_end_index][1] >= end_char: token_end_index -= 1 tokenized_examples["end_positions"].append(token_end_index + 1) return tokenized_examples # 通过函数prepare_train_features,建立分词后的训练集 tokenized_datasets = dataset.map(prepare_train_features, batched=True, remove_columns=dataset["train"].column_names) # 定义训练参数TrainingArguments,默认使用AdamW优化器 args = TrainingArguments( "ft-squad", # 输出路径,存放检查点和其他输出文件 evaluation_strategy="epoch", # 定义每轮结束后进行评价 learning_rate=2e-5, # 定义初始学习率 per_device_train_batch_size=16, # 定义训练批次大小 per_device_eval_batch_size=16, # 定义测试批次大小 num_train_epochs=2, # 定义训练轮数 ) # 定义Trainer,指定模型和训练参数,输入训练集、验证集、分词器以及评价函数 trainer = Trainer( model, args, train_dataset=tokenized_datasets["train"], eval_dataset=tokenized_datasets["validation"], data_collator=default_data_collator, tokenizer=tokenizer, ) trainer.train()
建模方法
命名实体识别(NER)需要针对给定输入文本的每个词输出一个标签,以此指定某个命名实体的边界信息。
输入层
对给定的输入文本x1x2···xn进行处理,得到BERT的原始输入X和输入层表示v
BERT编码层
得到输入文本中每个词对应的BERT隐含层表示。
序列标注层
针对每个词给出“BIO”标注模式下的分类预测。
代码实现
import numpy as np from datasets import load_dataset, load_metric from transformers import BertTokenizerFast, BertForTokenClassification, TrainingArguments, Trainer, DataCollatorForTokenClassification # 加载CoNLL-2003数据集、分词器 dataset = load_dataset('conll2003') tokenizer = BertTokenizerFast.from_pretrained('bert-base-cased') # 将训练集转换为可训练的特征形式 def tokenize_and_align_labels(examples): tokenized_inputs = tokenizer(examples["tokens"], truncation=True, is_split_into_words=True) labels = [] for i, label in enumerate(examples["ner_tags"]): word_ids = tokenized_inputs.word_ids(batch_index=i) previous_word_idx = None label_ids = [] for word_idx in word_ids: # 将特殊符号的标签设置为-100,以便在计算损失函数时自动忽略 if word_idx is None: label_ids.append(-100) # 把标签设置到每个词的第一个token上 elif word_idx != previous_word_idx: label_ids.append(label[word_idx]) # 对于每个词的其他token也设置为当前标签 else: label_ids.append(label[word_idx]) previous_word_idx = word_idx labels.append(label_ids) tokenized_inputs["labels"] = labels return tokenized_inputs tokenized_datasets = dataset.map(tokenize_and_align_labels, batched=True, load_from_cache_file=False) # 获取标签列表,并加载预训练模型 label_list = dataset["train"].features["ner_tags"].feature.names model = BertForTokenClassification.from_pretrained('bert-base-cased', num_labels=len(label_list)) # 定义data_collator,并使用seqeval进行评价 data_collator = DataCollatorForTokenClassification(tokenizer) metric = load_metric("seqeval") # 定义评价指标 def compute_metrics(p): predictions, labels = p predictions = np.argmax(predictions, axis=2) # 移除需要忽略的下标(之前记为-100) true_predictions = [ [label_list[p] for (p, l) in zip(prediction, label) if l != -100] for prediction, label in zip(predictions, labels) ] true_labels = [ [label_list[l] for (p, l) in zip(prediction, label) if l != -100] for prediction, label in zip(predictions, labels) ] results = metric.compute(predictions=true_predictions, references=true_labels) return { "precision": results["overall_precision"], "recall": results["overall_recall"], "f1": results["overall_f1"], "accuracy": results["overall_accuracy"], } # 定义训练参数TrainingArguments和Trainer args = TrainingArguments( "ft-conll2003", # 输出路径,存放检查点和其他输出文件 evaluation_strategy="epoch", # 定义每轮结束后进行评价 learning_rate=2e-5, # 定义初始学习率 per_device_train_batch_size=16, # 定义训练批次大小 per_device_eval_batch_size=16, # 定义测试批次大小 num_train_epochs=3, # 定义训练轮数 ) trainer = Trainer( model, args, train_dataset=tokenized_datasets["train"], eval_dataset=tokenized_datasets["validation"], data_collator=data_collator, tokenizer=tokenizer, compute_metrics=compute_metrics ) trainer.train()
以BERT、GPT等为代表的预训练技术为自然语言处理领域带来了巨大的变革。模型的性能固然重要,但是对于模型行为给出可信的解释同样很关键。
BERT 模型依赖 Transformer 结构,其主要由多层自注意力网络层堆叠而成(含残差连接)。而自注意力的本质事实上是对词(或标记)与词之间关系的刻画。
自注意力的分析将有助于理解BERT模型对于关系(relational)特征的学习能力。
自注意力的可视化分析有助于从直观上理解模型内部的信息流动。而为了更准确地理解模型的行为,仍然需要定量的实验分析。目前被广为采用的定量分析方法是探针实验。
探针通常是一个非参或者非常轻量的参数模型(如线性分类器),它接受待分析对象作为输入,并对特定行为预测。
,
train_dataset=tokenized_datasets[“train”],
eval_dataset=tokenized_datasets[“validation”],
data_collator=data_collator,
tokenizer=tokenizer,
compute_metrics=compute_metrics
)
trainer.train()
### 7.5 深入理解BERT #### 7.5.1 概述 以BERT、GPT等为代表的预训练技术为自然语言处理领域带来了巨大的变革。模型的性能固然重要,但是对于模型行为给出可信的解释同样很关键。 #### 7.5.2 自注意力可视化分析 BERT 模型依赖 Transformer 结构,其主要由多层自注意力网络层堆叠而成(含残差连接)。而自注意力的本质事实上是对词(或标记)与词之间关系的刻画。 自注意力的分析将有助于理解BERT模型对于关系(relational)特征的学习能力。 #### 7.5.3 探针实验 自注意力的可视化分析有助于从直观上理解模型内部的信息流动。而为了更准确地理解模型的行为,仍然需要定量的实验分析。目前被广为采用的定量分析方法是探针实验。 探针通常是一个非参或者非常轻量的参数模型(如线性分类器),它接受待分析对象作为输入,并对特定行为预测。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。