写在最前面
熟悉bert+文本摘要的下游任务微调的代码,方便后续增加组件实现idea
代码来自:
https://github.com/jasoncao11/nlp-notebook/tree/master
已跑通,略有修改
关于BERT
BERT模型参数的数量取决于具体实现,在Google发布的BERT模型中,大概有1.1亿个模型参数。
通常情况下,BERT的参数是在训练期间自动优化调整的,因此在使用预训练模型时不需要手动调节模型参数。
如果想微调BERT模型以适应特定任务,可以通过改变学习率、正则化参数和其他超参数来调整模型参数。在这种情况下,需要进行一些实验以找到最佳的参数配置。
使用transformers库进行微调
主要包括:
- Tokenizer:使用提供好的Tokenizer对原始文本处理,得到Token序列;
- 构建模型:在提供好的模型结构上,增加下游任务所需预测接口,构建所需模型;
- 微调:将Token序列送入构建的模型,进行训练。
本文主要为第一part
load_data.py
这段代码是BERT中读取数据的部分,主要实现了将数据集读取为PyTorch的数据集格式,包括对数据进行padding和collate操作。
自定义参数
- 训练集、测试集地址
TRAIN_DATA_PATH = ‘./data/train.tsv’
DEV_DATA_PATH = ‘./data/dev.tsv’ - 最大文字长度、批数据大小
MAX_LEN = 512
BATCH_SIZE = 8
# -*- coding: utf-8 -*- import csv import torch import torch.utils.data as tud from torch.nn.utils.rnn import pad_sequence from tokenizer import Tokenizer TRAIN_DATA_PATH = './data/train.tsv' DEV_DATA_PATH = './data/dev.tsv' MAX_LEN = 512 BATCH_SIZE = 8
collate_fn函数
这段代码用于对数据进行批处理(batching)和填充(padding)。具体来说,它将输入和标签序列列表中的所有张量进行填充,使它们的长度都等于batch中的最大长度,并将它们堆叠成一个4维张量。使用的填充值为0或-1或-100(具体取决于输入和标签的类型)。这样得到的一个batch数据可以被输入到神经网络中进行训练。
- 通过迭代batch_data中的每一个instance来进行数据的padding填充。将处理后的张量添加到对应的列表中,将这些张量作为输入传递到模型中进行推理。
instance
是指输入数据中的一个单独的样本实例,包含了四个标志(input_ids、token_type_ids、token_type_ids_for_mask和labels)的信息。torch.tensor()
是将上述列表转化为 PyTorch 张量类的函数,dtype=torch.long
指定张量的数据类型为 64 位整型。- input_ids 表示输入文本中每个词的编码,token_type_ids 表示每个词属于哪个句子。
pad_sequence
函数将每个batch数据中的tensor进行长度补全,补全元素为padding_value。
def collate_fn(batch_data): """ DataLoader所需的collate_fun函数,将数据处理成tensor形式 Args: batch_data: batch数据 Returns: """ # list初始化 input_ids_list, token_type_ids_list, token_type_ids_for_mask_list, labels_list = [], [], [], [] for instance in batch_data: # 按照batch中的最大数据长度,对数据进行padding填充 input_ids_temp = instance["input_ids"] token_type_ids_temp = instance["token_type_ids"] token_type_ids_for_mask_temp = instance["token_type_ids_for_mask"] labels_temp = instance["labels"] input_ids_list.append(torch.tensor(input_ids_temp, dtype=torch.long)) token_type_ids_list.append(torch.tensor(token_type_ids_temp, dtype=torch.long)) token_type_ids_for_mask_list.append(torch.tensor(token_type_ids_for_mask_temp, dtype=torch.long)) labels_list.append(torch.tensor(labels_temp, dtype=torch.long)) # 使用pad_sequence函数,会将list中所有的tensor进行长度补全,补全到一个batch数据中的最大长度,补全元素为padding_value return {"input_ids": pad_sequence(input_ids_list, batch_first=True, padding_value=0), "token_type_ids": pad_sequence(token_type_ids_list, batch_first=True, padding_value=0), "token_type_ids_for_mask": pad_sequence(token_type_ids_for_mask_list, batch_first=True, padding_value=-1), "labels": pad_sequence(labels_list, batch_first=True, padding_value=-100)}
BertDataset类
BertDataset类继承了torch.utils.data.Dataset类,实现了
__init__、__len__和__getitem__
三个函数用于初始化数据集,获取数据集长度,以及获取指定位置的数据。
在__init__
函数中,将原始数据读取后进行分词,并将得到的数据以字典形式保存到self.data_set中;
其中,摘要为tsv文件的第一列数据,原文为第二列数据。
Tokenizer.encode
是Hugging Face库中的一个方法,用于处理自然语言处理任务中的输入数据,将文本转换成数字序列
。
该方法使用预训练的词汇表(或者根据需要自动生成)来将单词或子词转换成其对应的编码表示。编码后的文本可以作为输入传递给深度学习模型,以便进行训练或推断。
在__getitem__
函数中,根据给定索引idx返回self.data_set中对应位置的字典。
在__len__
函数中,根据可迭代的数据集data_set返回该长度。
class BertDataset(tud.Dataset): def __init__(self, data_path): super(BertDataset, self).__init__() self.data_set = [] with open (data_path, 'r', encoding='utf8') as rf: r = csv.reader(rf, delimiter='\t') next(r) for row in r: summary = row[0] content = row[1] input_ids, token_type_ids, token_type_ids_for_mask, labels = Tokenizer.encode(content, summary, max_length=MAX_LEN) self.data_set.append({"input_ids": input_ids, "token_type_ids": token_type_ids, "token_type_ids_for_mask": token_type_ids_for_mask, "labels": labels}) def __len__(self): return len(self.data_set) def __getitem__(self, idx): return self.data_set[idx]
主函数
通过tud.DataLoader函数将BertDataset转换为PyTorch的DataLoader格式,即可用于训练模型。
traindataset = BertDataset(TRAIN_DATA_PATH) traindataloader = tud.DataLoader(traindataset, BATCH_SIZE, shuffle=True, collate_fn=collate_fn) valdataset = BertDataset(DEV_DATA_PATH) valdataloader = tud.DataLoader(valdataset, BATCH_SIZE, shuffle=False, collate_fn=collate_fn) # for batch in valdataloader: # print(batch["input_ids"]) # print(batch["input_ids"].shape) # print('------------------') # print(batch["token_type_ids"]) # print(batch["token_type_ids"].shape) # print('------------------') # print(batch["token_type_ids_for_mask"]) # print(batch["token_type_ids_for_mask"].shape) # print('------------------') # print(batch["labels"]) # print(batch["labels"].shape) # print('------------------')
tokenizer.py
定义Tokenizer类,用于处理文本生成任务中的分词和索引映射操作。
创建词汇表
读取BERT模型的词汇表文件,并创建了一些方便的数据结构,用于在文本生成任务中进行分词和索引映射操作。
注意:使用的是BERT模型的中文版本,并且已经下载了相应的预训练模型文件。
- 打开"vocab.txt"文件,该文件是BERT模型的词汇表文件,包含了模型所使用的所有词汇及其对应的索引。
- 逐行读取文件内容,并将每个词汇和其对应的索引存储在一个名为word2idx的字典中。方便通过词汇找到对应的索引。
- 定义一些常用的特殊标记的索引,如"[CLS]"、"[SEP]“和”[UNK]",标记在BERT模型中有特殊的含义,用于表示句子的起始、结束和未知词汇等。
- 创建idx2word字典,用于将索引映射回对应的词汇。可以通过索引找到对应的词汇。
# -*- coding: utf-8 -*- import unicodedata class Tokenizer(): with open("./bert-base-chinese/vocab.txt", encoding="utf-8") as f: lines = f.readlines() word2idx = {} for index, line in enumerate(lines): word2idx[line.strip("\n")] = index cls_id = word2idx['[CLS]'] sep_id = word2idx['[SEP]'] unk_id = word2idx['[UNK]'] idx2word = {idx: word for word, idx in word2idx.items()}
encode函数
将输入的文本转换为BERT模型所需的输入编码。
第一个文本(first_text):
- 转换为小写形式。
- Unicode规范化,将文本中的字符进行标准化处理,将其中的字符规范化为NFD形式。
- 将每个词汇转换为对应的索引,如果词汇不在词汇表中,则使用未知词汇(unk_id)的索引表示。
- 在开头插入特殊标记"[CLS]"的索引(cls_id)。
- 在词汇索引序列的末尾添加特殊标记"[SEP]"的索引(sep_id)。
如果还有第二个文本(second_text)作为输入,则进行类似的处理。
(但第二个文本不需要在开始插入"[CLS]"?)
最终,该方法返回两个文本的编码结果,即第一个文本和第二个文本的词汇索引序列。如果没有第二个文本,则返回的第二个文本的编码结果为空列表。这样,可以将输入文本转换为适合输入到BERT模型的编码表示形式。
@classmethod def encode(cls, first_text, second_text=None, max_length=512): first_text = first_text.lower() first_text = unicodedata.normalize('NFD', first_text) first_token_ids = [cls.word2idx.get(t, cls.unk_id) for t in first_text] first_token_ids.insert(0, cls.cls_id) first_token_ids.append(cls.sep_id) if second_text: second_text = second_text.lower() second_text = unicodedata.normalize('NFD', second_text) second_token_ids = [cls.word2idx.get(t, cls.unk_id) for t in second_text] second_token_ids.append(cls.sep_id) else: second_token_ids = []
对词汇索引序列进行处理,以保证其总长度不超过指定的最大长度(max_length)。
使用循环,不断检查总长度是否超过max_length,并根据情况进行调整。处理逻辑如下:
- 计算当前词汇索引序列的总长度(包括第一个文本和第二个文本),存储在total_length变量中。
- 如果总长度total_length小于等于max_length,则跳出循环,不需要进行截断。
- 删除两个文本中更长的那个,则从倒数第二个位置处删除一个词汇,以减少总长度。
- 经过循环处理后,保证词汇索引序列的总长度不超过max_length。
接下来:
- 创建first_token_type_ids列表,其长度与first_token_ids相同,并且将所有元素设置为0。这用于表示第一个文本中的词汇属于第一个句子。
- 创建first_token_type_ids_for_mask列表,其长度与first_token_ids相同,并且将所有元素设置为1。这用于在遮蔽(mask)操作中标记第一个文本的词汇。
- 创建labels列表,其长度与first_token_ids相同,并且将所有元素设置为-100。这是为了后续任务标签的设置,-100表示忽略该位置的标签。
如果存在第二个文本,则进行进一步处理:
- 创建second_token_type_ids列表,其长度与second_token_ids相同,并且将所有元素设置为1。这用于表示第二个文本中的词汇属于第二个句子。
- 创建second_token_type_ids_for_mask列表,其长度与second_token_ids相同,并且将所有元素设置为0。这用于在遮蔽操作中标记第二个文本的词汇。
- 将第二个文本的词汇索引序列等(second_token_ids),分别添加到第一个文本的词汇索引序列(first_token_ids)末尾等。
最后返回:(文本1、文本2)两个文本的词汇索引序列(first_token_ids)、词汇类型序列(first_token_type_ids)、遮蔽词汇类型序列(first_token_type_ids_for_mask)和标签序列(labels)。这些结果可用于进一步的BERT模型输入。
while True: total_length = len(first_token_ids) + len(second_token_ids) if total_length <= max_length: break elif len(first_token_ids) > len(second_token_ids): first_token_ids.pop(-2) else: second_token_ids.pop(-2) first_token_type_ids = [0] * len(first_token_ids) first_token_type_ids_for_mask = [1] * len(first_token_ids) labels = [-100] * len(first_token_ids) if second_token_ids: second_token_type_ids = [1] * len(second_token_ids) second_token_type_ids_for_mask = [0] * len(second_token_ids) first_token_ids.extend(second_token_ids) first_token_type_ids.extend(second_token_type_ids) first_token_type_ids_for_mask.extend(second_token_type_ids_for_mask) labels.extend(second_token_ids) return first_token_ids, first_token_type_ids, first_token_type_ids_for_mask, labels
decode函数
将输入的词汇索引序列转换回对应的文本。
首先遍历输入的词汇索引序列(input_ids),对每个索引进行如下操作:
- 通过索引值从idx2word字典中获取对应的词汇。
- 将获取的词汇添加到一个名为tokens的列表中。
最后,代码使用空格将tokens列表中的词汇拼接起来,形成一个字符串。这样,将词汇索引序列转换为对应的文本字符串。
返回转换后的文本字符串。
@classmethod def decode(cls, input_ids): tokens = [cls.idx2word[idx] for idx in input_ids] return ' '.join(tokens)