李宏毅深度学习HW7

李宏毅深度学习---HW7

  • 1、任务介绍
  • 2、数据集
  • 3、词语划分
      • 为什么长段落是一个问题?
  • 4、训练数据窗口划分
  • 5、测试数据窗口划分
  • 6、提示
      • (1)线性学习速率衰减
      • (2) stride
      • (3)预处理
      • (4)其他预训练模型
      • (5)后加工
      • (7)自动混合精度
      • (8)梯度累积
  • 7、实验
    • (1)Simple Baseline (Acc>0.45139)
    • (2)Medium Baseline (Acc>0.65)
    • (3)Strong Baseline (Acc>0.78136)


1、任务介绍

使用BERT模型,微调用于下游任务,解决从文章提取答案的问答题。
李宏毅深度学习HW7_第1张图片
李宏毅深度学习HW7_第2张图片

2、数据集

ARCD:增量阅读理解数据集
ODSQA:开放域口语问题回答数据集
●train: DRCD + DRCD-TTS ○10524段,31690个问题
●dev: DRCD + DRCD-TTS ○1490段,4131个问题
●test: DRCD + ODSQA ○1586段,4957个问题
训练数据集含有答案:
李宏毅深度学习HW7_第3张图片
测试数据集没有答案:
李宏毅深度学习HW7_第4张图片

3、词语划分

把句子断词,然后转换成代码才能传到模型训练。
李宏毅深度学习HW7_第5张图片
文章和问题输入之前要用一些特殊符号,[PAD]表示文章不够长时的占位符。传进模型有两段文字时使用token_type_ids,用0表示前一段文字,1表示后一段文字,[PAD]用0表示。attention_mask表示我们希望模型学习attention的位置,用1表示,[PAD]用0表示,不学习。

李宏毅深度学习HW7_第6张图片

为什么长段落是一个问题?

总序列长度=问题长度+段落长度+3(特殊标记)。
BERT的最大输入序列长度被限制为512,为什么?变压器中的➔transformer中的自注意具有( O(n2))的复杂度,因此,我们可能无法处理整个段落。我们能做什么?
在训练集中的数据长度一般都是位于300-400之间,但是也有很长的到1200。
李宏毅深度学习HW7_第7张图片
解决方法分为训练和测试数据的方案:

4、训练数据窗口划分

我们知道在训练中答案在哪里!
假设:回答这个问题所需的信息可以在答案附近找到!
简单的解决方案:只要在答案周围画一个窗口(尽可能大)!
例如,窗口大小为= max_paragraph_len = 32。
李宏毅深度学习HW7_第8张图片

5、测试数据窗口划分

我们不知道答案是在测试分裂到窗口!
答案可能不在中间,而在窗口的两边,这样我们怎么做呢?如果每个划分的句子之间是没有重叠的,那如果答案分布在两个句子里面怎么办?所以我们希望句子之间可以设置重叠,然后根据下面的总分数得到答案的位置。
李宏毅深度学习HW7_第9张图片

6、提示

李宏毅深度学习HW7_第10张图片

(1)线性学习速率衰减

方法1:手动调整学习率
这种方法需要注意学习率不能是太明显的复数

Decrement optimizer.param_groups[0][“lr”] by learning_rate / total training step per step
李宏毅深度学习HW7_第11张图片
方法2:通过调度程序自动调整学习速率
在这里插入图片描述

(2) stride

doc_stride:两个连续窗口的起始位置之间的距离
doc_stride在示例代码中被设置为“max_paragraph_len”(即没有重叠),如果答案是在窗口的边界附近或跨窗口怎么办?提示:重叠窗口。
李宏毅深度学习HW7_第12张图片

(3)预处理

提示:如何防止模型学习到它在训练过程中不应该学习的东西?(即答案并不总是靠近窗口的中间)

        ##### TODO: Preprocessing #####
        # Hint: How to prevent model from learning something it should not learn

        if self.split == "train":
            # 将paragraph_text中答案的开始/结束位置转换为tokenized_paragraph中的开始/结束位置 
            answer_start_token = tokenized_paragraph.char_to_token(question["answer_start"])
            answer_end_token = tokenized_paragraph.char_to_token(question["answer_end"])

            #通过对包含答案的段落部分进行切片来获得单个窗口
            mid = (answer_start_token + answer_end_token) // 2
            paragraph_start = max(0, min(mid - self.max_paragraph_len // 2, len(tokenized_paragraph) - self.max_paragraph_len))
            paragraph_end = paragraph_start + self.max_paragraph_len
            
            # 分割问题/段落并添加特殊标记(101: CLS, 102: SEP)
            input_ids_question = [101] + tokenized_question.ids[:self.max_question_len] + [102] 
            input_ids_paragraph = tokenized_paragraph.ids[paragraph_start : paragraph_end] + [102]		
            
            # 将tokenized_paragraph中答案的开始/结束位置转换为窗口中的开始/结束位置
            answer_start_token += len(input_ids_question) - paragraph_start
            answer_end_token += len(input_ids_question) - paragraph_start
            
            # 填充序列并获得模型输入
            input_ids, token_type_ids, attention_mask = self.padding(input_ids_question, input_ids_paragraph)
            return torch.tensor(input_ids), torch.tensor(token_type_ids), torch.tensor(attention_mask), answer_start_token, answer_end_token

(4)其他预训练模型

在这里插入图片描述
可以在里面找到。
在这里插入图片描述

(5)后加工

如果预测的end_index <预测的start_index怎么办?
李宏毅深度学习HW7_第13张图片
在evaluate函数中,可能出现预测的start index比end index大的情况,要添加代码修复。

(7)自动混合精度

●PyTorch训练默认使用32位浮点(FP32)算法
●自动混合精度(AMP)使自动转换某些GPU操作,从FP32精度到半精度(FP16)。
●提供约1.5-3.0倍的速度,同时保持精度。
李宏毅深度学习HW7_第14张图片
警告:仅适用于某些gpu(如T4、V100)

(8)梯度累积

当gpu内存不足,但您想使用更大的批量大小
●将全局批量分割成更小的小批
●为每个小批量:累积梯度而不更新模型参数
●更新模型参数
李宏毅深度学习HW7_第15张图片

7、实验

(1)Simple Baseline (Acc>0.45139)

在这里插入图片描述
李宏毅深度学习HW7_第16张图片
保持不变:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(2)Medium Baseline (Acc>0.65)

在这里插入图片描述
李宏毅深度学习HW7_第17张图片
在这里插入图片描述
李宏毅深度学习HW7_第18张图片
保持不变:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(3)Strong Baseline (Acc>0.78136)

在这里插入图片描述
李宏毅深度学习HW7_第19张图片
用新的预训练模型
李宏毅深度学习HW7_第20张图片
新的模型大小是1.2G,它比原BERT模型的400M大了很多,这样造成内存不够,需要降低batch size。
在这里插入图片描述
累积梯度:
在这里插入图片描述

李宏毅深度学习HW7_第21张图片
在evaluate函数中,可能出现预测的start_index比end_index大的情况,要添加代码修复。
李宏毅深度学习HW7_第22张图片

class QA_Dataset(Dataset):
    def __init__(self, split, questions, tokenized_questions, tokenized_paragraphs):
        self.split = split
        self.questions = questions
        self.tokenized_questions = tokenized_questions
        self.tokenized_paragraphs = tokenized_paragraphs
        self.max_question_len = 40
        self.max_paragraph_len = 150
        
        ##### TODO: Change value of doc_stride #####
        self.doc_stride = 150

        # Input sequence length = [CLS] + question + [SEP] + paragraph + [SEP]
        self.max_seq_len = 1 + self.max_question_len + 1 + self.max_paragraph_len + 1

    def __len__(self):
        return len(self.questions)

    def __getitem__(self, idx):
        question = self.questions[idx]
        tokenized_question = self.tokenized_questions[idx]
        tokenized_paragraph = self.tokenized_paragraphs[question["paragraph_id"]]

        ##### TODO: Preprocessing #####
        # 提示:如何防止模型学习它不应该学习的东西

        if self.split == "train":
            # 将paragraph_text中答案的开始/结束位置转换为tokenized_paragraph中的开始/结束位置
            answer_start_token = tokenized_paragraph.char_to_token(question["answer_start"])
            answer_end_token = tokenized_paragraph.char_to_token(question["answer_end"])

            # 通过对包含答案的段落部分进行切片来获得单个窗口
            mid = (answer_start_token + answer_end_token) // 2
            paragraph_start = max(0, min(mid - self.max_paragraph_len // 2, len(tokenized_paragraph) - self.max_paragraph_len))
            paragraph_end = paragraph_start + self.max_paragraph_len
            
            # Slice question/paragraph and add special tokens (101: CLS, 102: SEP)
            input_ids_question = [101] + tokenized_question.ids[:self.max_question_len] + [102] 
            input_ids_paragraph = tokenized_paragraph.ids[paragraph_start : paragraph_end] + [102]		
            
            # Convert answer's start/end positions in tokenized_paragraph to start/end positions in the window  
            answer_start_token += len(input_ids_question) - paragraph_start
            answer_end_token += len(input_ids_question) - paragraph_start
            
            # Pad sequence and obtain inputs to model 
            input_ids, token_type_ids, attention_mask = self.padding(input_ids_question, input_ids_paragraph)
            return torch.tensor(input_ids), torch.tensor(token_type_ids), torch.tensor(attention_mask), answer_start_token, answer_end_token

        # Validation/Testing
        else:
            input_ids_list, token_type_ids_list, attention_mask_list = [], [], []
            
            # 段落被分成几个窗口,每个窗口的起始位置由步长“doc_stride”分隔
            for i in range(0, len(tokenized_paragraph), self.doc_stride):
                
                # Slice question/paragraph and add special tokens (101: CLS, 102: SEP)
                input_ids_question = [101] + tokenized_question.ids[:self.max_question_len] + [102]
                input_ids_paragraph = tokenized_paragraph.ids[i : i + self.max_paragraph_len] + [102]
                
                # 填充序列并获得模型输入
                input_ids, token_type_ids, attention_mask = self.padding(input_ids_question, input_ids_paragraph)
                
                input_ids_list.append(input_ids)
                token_type_ids_list.append(token_type_ids)
                attention_mask_list.append(attention_mask)
            
            return torch.tensor(input_ids_list), torch.tensor(token_type_ids_list), torch.tensor(attention_mask_list)

    def padding(self, input_ids_question, input_ids_paragraph):
        # 如果序列长度短于max_seq_len,则填充零
        padding_len = self.max_seq_len - len(input_ids_question) - len(input_ids_paragraph)
        # 词汇表中输入序列标记的索引
        input_ids = input_ids_question + input_ids_paragraph + [0] * padding_len
        # 指示输入的第一和第二部分的分段令牌索引。在[01]中选择索引
        token_type_ids = [0] * len(input_ids_question) + [1] * len(input_ids_paragraph) + [0] * padding_len
        #掩码,以避免关注填充标记索引。在[01]中选择的掩码值
        attention_mask = [1] * (len(input_ids_question) + len(input_ids_paragraph)) + [0] * padding_len
        
        return input_ids, token_type_ids, attention_mask

train_set = QA_Dataset("train", train_questions, train_questions_tokenized, train_paragraphs_tokenized)
dev_set = QA_Dataset("dev", dev_questions, dev_questions_tokenized, dev_paragraphs_tokenized)
test_set = QA_Dataset("test", test_questions, test_questions_tokenized, test_paragraphs_tokenized)

train_batch_size = 32

# 注意:不要改变dev_loader / test_loader的批量大小!
#虽然批处理大小=1,但它实际上是由来自同一QA对的几个窗口组成的批处理
train_loader = DataLoader(train_set, batch_size=train_batch_size, shuffle=True, pin_memory=True)
dev_loader = DataLoader(dev_set, batch_size=1, shuffle=False, pin_memory=True)
test_loader = DataLoader(test_set, batch_size=1, shuffle=False, pin_memory=True)



def evaluate(data, output):
    ##### TODO: Postprocessing #####
    # There is a bug and room for improvement in postprocessing 
    # Hint: Open your prediction file to see what is wrong 
    
    answer = ''
    max_prob = float('-inf')
    num_of_windows = data[0].shape[1]
    
    for k in range(num_of_windows):
        # 通过选择最可能的开始位置/结束位置获得答案
        start_prob, start_index = torch.max(output.start_logits[k], dim=0)
        end_prob, end_index = torch.max(output.end_logits[k], dim=0)
        
        # 答案概率计算为start_prob和end_prob之和
        prob = start_prob + end_prob
        
        # 如果计算的概率大于以前的窗口,则替换答案
        if prob > max_prob:
            max_prob = prob
            # Convert tokens to chars (e.g. [1920, 7032] --> "大 金")
            answer = tokenizer.decode(data[0][0][k][start_index : end_index + 1])
    
    # Remove spaces in answer (e.g. "大 金" --> "大金")
    return answer.replace(' ','')

num_epoch = 1
validation = True
logging_step = 100
learning_rate = 1e-4
optimizer = AdamW(model.parameters(), lr=learning_rate)

if fp16_training:
    model, optimizer, train_loader = accelerator.prepare(model, optimizer, train_loader) 

model.train()

print("Start Training ...")

for epoch in range(num_epoch):
    step = 1
    train_loss = train_acc = 0
    
    for data in tqdm(train_loader):	
        # Load all data into GPU
        data = [i.to(device) for i in data]
        
        # Model inputs: input_ids, token_type_ids, attention_mask, start_positions, end_positions (Note: only "input_ids" is mandatory)
        # Model outputs: start_logits, end_logits, loss (return when start_positions/end_positions are provided)  
        output = model(input_ids=data[0], token_type_ids=data[1], attention_mask=data[2], start_positions=data[3], end_positions=data[4])

        # Choose the most probable start position / end position
        start_index = torch.argmax(output.start_logits, dim=1)#最大值索引
        end_index = torch.argmax(output.end_logits, dim=1)
        
        # Prediction is correct only if both start_index and end_index are correct
        train_acc += ((start_index == data[3]) & (end_index == data[4])).float().mean()
        train_loss += output.loss
        
        if fp16_training:
            accelerator.backward(output.loss)#反向传播计算得到每个参数的梯度值
        else:
            output.loss.backward()
        
        optimizer.step()#通过梯度下降执行一步参数更新
        optimizer.zero_grad()#梯度归零
        step += 1

        ##### TODO: 应用线性学习率衰减 #####
        
        
        # 打印过去logging步骤的训练损失和准确性
        if step % logging_step == 0:
            print(f"Epoch {epoch + 1} | Step {step} | loss = {train_loss.item() / logging_step:.3f}, acc = {train_acc / logging_step:.3f}")
            train_loss = train_acc = 0

    if validation:
        print("Evaluating Dev Set ...")
        model.eval()
        with torch.no_grad():
            dev_acc = 0
            for i, data in enumerate(tqdm(dev_loader)):
                output = model(input_ids=data[0].squeeze(dim=0).to(device), token_type_ids=data[1].squeeze(dim=0).to(device),
                       attention_mask=data[2].squeeze(dim=0).to(device))
                # prediction is correct only if answer text exactly matches
                dev_acc += evaluate(data, output) == dev_questions[i]["answer_text"]
            print(f"Validation | Epoch {epoch + 1} | acc = {dev_acc / len(dev_loader):.3f}")
        model.train()

# Save a model and its configuration file to the directory 「saved_model」 
# i.e. there are two files under the direcory 「saved_model」: 「pytorch_model.bin」 and 「config.json」
# Saved model can be re-loaded using 「model = BertForQuestionAnswering.from_pretrained("saved_model")
print("Saving Model ...")
model_save_dir = "saved_model" 
model.save_pretrained(model_save_dir)

你可能感兴趣的:(深度学习,人工智能,自然语言处理)