赞
踩
随着深度学习的发展,递归神经网络(RNN和LSTM)和卷积神经网络(CNN)等神经网络结构已经完成了自然语言处理(NLP)的大部分任务,它在文本分类、语言建模、机器翻译等性能上都有了很大的提高。
然而,与计算机视觉(Computer Vision)中的深度学习性能相比,自然语言处理的深度学习模型的性能就差强人意了。
原因之一在于缺少大型带标记的文本数据集。目前,大多数带标记的文本数据集对于自然语言处理的深度学习模型来说都不够“大”,不足以训练深度神经网络,因为深度神经网络的参数量很大,如果在小数据集上训练这样的网络会导致过拟合。
除此之外,自然语言处理落后于计算机视觉发展的另一个重要原因是它缺乏迁移学习(transfer learning)。要知道,迁移学习在计算机视觉深度学习中发挥了重要作用。借助Imagenet等大型标记数据集的强可用性,基于CNN的深度模型训练成为可能——目前,这些大型标记数据集已经被广泛用作于计算机视觉任务的预训练模型了。
而在自然语言处理的深度学习上,直到2018年谷歌提出Transformer模型,NLP深度学习才算有了新的飞跃。
本文将通过实际演示来解释如何调整BERT来进行文本分类(Text Classification),包括以下几个部分:
迁移学习是一种将深度学习模型在大数据集里训练,然后在另一个数据集上执行类似任务的技术。我们称这种深度学习模型为预训练模型(Pre-trained Models)。
预训练模型最著名的例子是在ImageNet数据集里训练的计算机视觉(Computer Vision)深度学习模型。解决问题的最好方式是使用一个预先训练好的模型,而不是从头开始构建一个模型。拿日常工作和生活举例,想必大家为了顺利甚至完美地提案,一定会提前不断地进行准备和模拟吧?迁移学习是一个道理。
随着近年来自然语言处理的发展,迁移学习成为了一种可行的选择。
NLP中的大部分任务,如文本分类、语言建模、机器翻译等,都是序列建模任务(Sequence Modeling tasks)。这种传统的机器学习模型和神经网络无法捕捉文本中出现的顺序信息(sequential information)。因此,人们开始使用递归神经网络(RNN和LSTM),这些结构可以建模文本中出现的顺序信息。
然而,递归神经网络也有局限,其中的主要问题是RNNs不能并行化(parallelized),它们一次只能接受一个输入。对于文本序列,RNN或LSTM每次输入只能接受一次切分(Token),即逐个地传递序列。如果在一个大数据集里训练这样一个模型会花费很多时间。
水涨船高的时间成本使在NLP里使用迁移学习的呼声不断,终于,在2018年,谷歌在《Attention is All You Need》一文中介绍了Transformer模型,这个模型成为了NLP深度学习的里程碑。
很快,基于Transformer的NLP任务模型又多又快地发展起来。
使用Transformer的模型有很多优点,其中最重要的以下两点——
BERT和GPT-2是当下最流行的基于Transformer的模型,
而在本文中,我们将重点关注BERT并学习如何使用预先训练好的BERT模型来执行文本分类。
BERT(Bidirectional Encoder Representations from Transformers)是一个具有大量参数的大型神经网络架构,其参数量可以从1亿到3亿多个。所以,在一个小数据集上从零开始训练BERT模型会导致过拟合。
所以训练BERT模型需要从大型数据集开始,然后使用相对小的数据集上进行再训练模型,这个过程被称为模型微调(Model Fine-Tuning)。
模型微调的几种方法:
本教程使用的是第三种方法,我们将在微调期间冻结整个BERT层,在其加上一个密集层和softmax层。
(softmax经常用在神经网络的最后一层,作为输出层,进行多分类。此外,softmax在增强学习领域内,softmax经常被用作将某个值转化为激活概率,这类情况下,softmax的公式如下:)
让我们来看看BERT研究团队如何描述其NLP框架的吧:
BERT全称为 Bidirectional Encoder Representations from Transformers(来自Transformer的双向编码器表示)。它通过对左右上下文的共同条件作用,来预先训练未标记文本的深层双向表示。因此,预先训练好的BERT模型可以通过一个额外的输出层进行微调,从而为NLP任务创建最先进的模型。
感觉是不是很深奥,我们一起梳理梳理吧!
首先,BERT全称是Bidirectional Encoder Representations from Transformers。这里的每个单词都有其意义,我们接下来会逐一介绍。目前,这一行需要记住的关键内容是——BERT是基于Transformer架构的。
其次,BERT预先训练了大量未标记的文本语料库,包括整个Wikipedia(25亿个单词!)和图书语料库(8亿个单词)。
预训练是BERT的出色之处。因为 当我们在一个大文本语料库里训练模型时,模型就能对语言如何生成有更深入透彻的理解——这对几乎所有自然语言处理任务而言都是重中之重。
第三,BERT是一个“深度双向”模型。双向意味着BERT在训练阶段可以同时从切词的左边和右边学习信息。
想要了解更多关于BERT体系结构及其预训练的信息,大家可以阅读下面这篇文章:
Demystifying BERT: A Comprehensive Guide to the Groundbreaking NLP Frameworkwww.analyticsvidhya.com4.【实际演示】微调BERT来对垃圾邮件进行分类
现在我们将在ransformer库的帮助下对BERT模型进行微调,以执行文本分类——
问题陈述
在日常生活中接收的各类信息中,不免会有垃圾邮件。而我们的任务就是建立一个系统,可以自动检测消息是否是垃圾邮件。用例的数据集可以点击这里下载
安装Transformer库
我们将安装Huggingface的Transformer库。这个库允许导入大量基于Transformer的预训练模型。只需执行下面的代码来安装:
!pip install transformers
导入库
- import numpy as np
- import pandas as pd
- import torch
- import torch.nn as nn
- from sklearn.model_selection import train_test_split
- from sklearn.metrics import classification_report
- import transformers
- from transformers import AutoModel, BertTokenizerFast
-
- # specify GPU
- device = torch.device("cuda")
加载数据集
将数据集读入pandas数据框
- df = pd.read_csv("spamdata_v2.csv")
- df.head()
该数据集由两列——“标签”和“文本”组成。“文本”列包含消息正文,“标签”列是一个二进制定类变量,1表示垃圾邮件,0表示该消息不是垃圾邮件。
现在我们将把这个数据集分成三个集——用于训练、验证和测试。
- # split train dataset into train, validation and test sets
-
训练集和验证集用来对模型进行微调,并对测试集进行预测。
导入BERT模型和BERT切分
我们将导入有着1.1亿个参数的BERT模型。其实还有一个更大的BERT模型叫做BERT-large,它有3.45亿个参数。
- # import BERT-base pretrained model
- bert = AutoModel.from_pretrained('bert-base-uncased')
-
- # Load the BERT tokenizer
- tokenizer = BertTokenizerFast.from_pretrained('bert-base-uncased')
让我们来看看这个BERT切分是怎么工作的吧。先试着使用它对几个句子进行编码:
- # sample data
- text = ["this is a bert model tutorial", "we will fine-tune a bert model"]
-
- # encode text
- sent_id = tokenizer.batch_encode_plus(text, padding=True)
-
- # output
- print(sent_id)
这是输出结果:
{‘input_ids’: [[101, 2023, 2003, 1037, 14324, 2944, 14924, 4818, 102, 0],
[101, 2057, 2097, 2986, 1011, 8694, 1037, 14324, 2944, 102]],
‘attention_mask’: [[1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]}
可以看见,输出是一个包含两个条目的dictionary。
' input_ids '包含输入句子的整数序列。整数101和102是特殊切分。我们将它们添加到两个序列中,0表示填充切分。' attention_mask '包含1和0,它告诉模型要注意与掩码值1对应的标记并忽略其余的。
切分句子
- # get length of all the messages in the train set
- seq_len = [len(i.split()) for i in train_text]
-
- pd.Series(seq_len).hist(bins = 30)
我们可以清楚地看到,大多数句子的长度为25个字符或更少。而最大长度是175。如果我们选择175作为填充长度那么所有输入序列长度为175,大部分的标记在这些序列将填充标记不会帮助模型学习任何有用的东西,最重要的是,它会使训练速度较慢。
因此,我们将设25为填充长度。
- # tokenize and encode sequences in the training set
- tokens_train = tokenizer.batch_encode_plus(
- train_text.tolist(),
- max_length = 25,
- pad_to_max_length=True,
- truncation=True
- )
-
- # tokenize and encode sequences in the validation set
- tokens_val = tokenizer.batch_encode_plus(
- val_text.tolist(),
- max_length = 25,
- pad_to_max_length=True,
- truncation=True
- )
-
- # tokenize and encode sequences in the test set
- tokens_test = tokenizer.batch_encode_plus(
- test_text.tolist(),
- max_length = 25,
- pad_to_max_length=True,
- truncation=True

我们现在已经将训练,验证和测试集中的句子转换为每个长度为25个的切分整数序列。 接下来,我们需要将整数序列转换为张量。
- ## convert lists to tensors
-
- train_seq = torch.tensor(tokens_train['input_ids'])
- train_mask = torch.tensor(tokens_train['attention_mask'])
- train_y = torch.tensor(train_labels.tolist())
-
- val_seq = torch.tensor(tokens_val['input_ids'])
- val_mask = torch.tensor(tokens_val['attention_mask'])
- val_y = torch.tensor(val_labels.tolist())
-
- test_seq = torch.tensor(tokens_test['input_ids'])
- test_mask = torch.tensor(tokens_test['attention_mask'])
- test_y = torch.tensor(test_labels.tolist())
现在我们将为训练集和验证集创建dataloaders,这些dataloaders将在训练阶段将成批的训练数据和验证数据作为输入传递给模型。
- from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
-
- #define a batch size
- batch_size = 32
-
- # wrap tensors
- train_data = TensorDataset(train_seq, train_mask, train_y)
-
- # sampler for sampling the data during training
- train_sampler = RandomSampler(train_data)
-
- # dataLoader for train set
- train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size)
-
- # wrap tensors
- val_data = TensorDataset(val_seq, val_mask, val_y)
-
- # sampler for sampling the data during training
- val_sampler = SequentialSampler(val_data)
-
- # dataLoader for validation set
- val_dataloader = DataLoader(val_data, sampler = val_sampler, batch_size=batch_size)

定义模型架构
小编在前文说到了,本次使用的微调方法是第三种,即在对模型进行微调之前,会冻结模型的所有层。如果有小伙伴希望微调BERT模型的预训练权重,那么就不需要执行下面这段代码。
- # freeze all the parameters
- for param in bert.parameters():
- param.requires_grad = False
接下来,就到定义我们的模型架构的时候了
- class BERT_Arch(nn.Module):
-
- def __init__(self, bert):
-
- super(BERT_Arch, self).__init__()
-
- self.bert = bert
-
- # dropout layer
- self.dropout = nn.Dropout(0.1)
-
- # relu activation function
- self.relu = nn.ReLU()
-
- # dense layer 1
- self.fc1 = nn.Linear(768,512)
-
- # dense layer 2 (Output layer)
- self.fc2 = nn.Linear(512,2)
-
- #softmax activation function
- self.softmax = nn.LogSoftmax(dim=1)
-
- #define the forward pass
- def forward(self, sent_id, mask):
-
- #pass the inputs to the model
- _, cls_hs = self.bert(sent_id, attention_mask=mask)
-
- x = self.fc1(cls_hs)
-
- x = self.relu(x)
-
- x = self.dropout(x)
-
- # output layer
- x = self.fc2(x)
-
- # apply softmax activation
- x = self.softmax(x)
-
- return x

- # pass the pre-trained BERT to our define architecture
- model = BERT_Arch(bert)
-
- # push the model to GPU
- model = model.to(device)
我们将使用AdamW作为优化器。它是Adam优化器的改进版本。想要了解更多信息,请查阅本文。
- # optimizer from hugging face transformers
- from transformers import AdamW
-
- # define the optimizer
- optimizer = AdamW(model.parameters(),
- lr = 1e-5) # learning rate
在我们的数据集中有一个类出现了不平衡。大多数的观察结果并不是垃圾邮件。因此,我们将首先计算训练集合中标签的类权重,然后将这些权重传递给损失函数,这样它就能处理该类的不平衡了。
- from sklearn.utils.class_weight import compute_class_weight
-
- #compute the class weights
- class_weights = compute_class_weight('balanced', np.unique(train_labels), train_labels)
-
- print("Class Weights:",class_weights)
输出:[0.57743559 3.72848948]
- # converting list of class weights to a tensor
- weights= torch.tensor(class_weights,dtype=torch.float)
-
- # push to GPU
- weights = weights.to(device)
-
- # define the loss function
- cross_entropy = nn.NLLLoss(weight=weights)
-
- # number of training epochs
- epochs = 10
微调BERT
目前为止,我们已经定义了模型架构,指定了优化器和损失函数,并且我们的dataloaders也设定完毕。现在,我们必须分别定义两个函数来训练(微调)和评估模型。
- # function to train the model
- def train():
-
- model.train()
-
- total_loss, total_accuracy = 0, 0
-
- # empty list to save model predictions
- total_preds=[]
-
- # iterate over batches
- for step,batch in enumerate(train_dataloader):
-
- # progress update after every 50 batches.
- if step % 50 == 0 and not step == 0:
- print(' Batch {:>5,} of {:>5,}.'.format(step, len(train_dataloader)))
-
- # push the batch to gpu
- batch = [r.to(device) for r in batch]
-
- sent_id, mask, labels = batch
-
- # clear previously calculated gradients
- model.zero_grad()
-
- # get model predictions for the current batch
- preds = model(sent_id, mask)
-
- # compute the loss between actual and predicted values
- loss = cross_entropy(preds, labels)
-
- # add on to the total loss
- total_loss = total_loss + loss.item()
-
- # backward pass to calculate the gradients
- loss.backward()
-
- # clip the the gradients to 1.0. It helps in preventing the exploding gradient problem
- torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
-
- # update parameters
- optimizer.step()
-
- # model predictions are stored on GPU. So, push it to CPU
- preds=preds.detach().cpu().numpy()
-
- # append the model predictions
- total_preds.append(preds)
-
- # compute the training loss of the epoch
- avg_loss = total_loss / len(train_dataloader)
-
- # predictions are in the form of (no. of batches, size of batch, no. of classes).
- # reshape the predictions in form of (number of samples, no. of classes)
- total_preds = np.concatenate(total_preds, axis=0)
-
- #returns the loss and predictions
- return avg_loss, total_preds

现在,就让我们开始微调模型吧!
- # function for evaluating the model
- def evaluate():
-
- print("nEvaluating...")
-
- # deactivate dropout layers
- model.eval()
-
- total_loss, total_accuracy = 0, 0
-
- # empty list to save the model predictions
- total_preds = []
-
- # iterate over batches
- for step,batch in enumerate(val_dataloader):
-
- # Progress update every 50 batches.
- if step % 50 == 0 and not step == 0:
-
- # Calculate elapsed time in minutes.
- elapsed = format_time(time.time() - t0)
-
- # Report progress.
- print(' Batch {:>5,} of {:>5,}.'.format(step, len(val_dataloader)))
-
- # push the batch to gpu
- batch = [t.to(device) for t in batch]
-
- sent_id, mask, labels = batch
-
- # deactivate autograd
- with torch.no_grad():
-
- # model predictions
- preds = model(sent_id, mask)
-
- # compute the validation loss between actual and predicted values
- loss = cross_entropy(preds,labels)
-
- total_loss = total_loss + loss.item()
-
- preds = preds.detach().cpu().numpy()
-
- total_preds.append(preds)
-
- # compute the validation loss of the epoch
- avg_loss = total_loss / len(val_dataloader)
-
- # reshape the predictions in form of (number of samples, no. of classes)
- total_preds = np.concatenate(total_preds, axis=0)
-
- return avg_loss, total_preds

输出:
- Training Loss: 0.592
- Validation Loss: 0.567
-
- Epoch 5 / 10
- Batch 50 of 122.
- Batch 100 of 122.
-
- Evaluating...
-
- Training Loss: 0.566
- Validation Loss: 0.543
-
- Epoch 6 / 10
- Batch 50 of 122.
- Batch 100 of 122.
-
- Evaluating...
-
- Training Loss: 0.552
- Validation Loss: 0.525
-
- Epoch 7 / 10
- Batch 50 of 122.
- Batch 100 of 122.
-
- Evaluating...
-
- Training Loss: 0.525
- Validation Loss: 0.498
-
- Epoch 8 / 10
- Batch 50 of 122.
- Batch 100 of 122.
-
- Evaluating...
-
- Training Loss: 0.507
- Validation Loss: 0.477
-
- Epoch 9 / 10
- Batch 50 of 122.
- Batch 100 of 122.
-
- Evaluating...
-
- Training Loss: 0.488
- Validation Loss: 0.461
-
- Epoch 10 / 10
- Batch 50 of 122.
- Batch 100 of 122.
-
- Evaluating...
-
- Training Loss: 0.474
- Validation Loss: 0.454

可以看到,在第10纪元后,验证损失扔在减少,这意味着你可以你可以尝试更多训练纪元。现在就让我们看看它在测试数据集上的表现如何吧:
预测
我们需要先加载在训练过程中的最佳模型权重:
- #load weights of best model
- path = 'saved_weights.pt'
- model.load_state_dict(torch.load(path))
使用微调模型对数据集做出预测:
- # get predictions for test data
- with torch.no_grad():
- preds = model(test_seq.to(device), test_mask.to(device))
- preds = preds.detach().cpu().numpy()
来看看模型的表现如何吧!
- preds = np.argmax(preds, axis = 1)
- print(classification_report(test_y, preds))
输出:
对于类1来说,召回率和精确度都相当高,这意味着该模型可以很好地预测该类。
然而,我们的目标是检测垃圾邮件,因此对第1类(垃圾邮件)样本的误分类要比对第0类样本的误分类更为重要。
让我们看看第1类的召回率——0.90,这意味着该模型能够正确地分类90%的垃圾邮件。但其精度稍低了些,这说明模型将一些0类消息(不是垃圾邮件)错误地归类为垃圾邮件了。
我们对一个预先训练好的BERT模型进行了微调,将其使用在非常小的数据集上执行文本分类。大家可以在不同的数据集上对BERT进行微调,看看它的表现如何,甚至可以使用BERT来执行多类或多标签分类。
当然,如果小伙伴们有更大的数据集,当然可以去尝试训练整个BERT体系结构!
如果小伙伴们觉得本文有意思,欢迎点赞收藏留言!也欢迎各位小伙伴关注【自然语言处理】学习帐,小编会定期更新最新鲜的自然语言处理实操案例,让我们一起体会数据秩序之美吧!
【自然语言处理】学习帐www.zhihu.comCopyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。