2017年,Transformer在著名的论文《Attention Is All You Need》中首次亮相,很快成为NLP中最受欢迎的模型。
以非顺序方式(与RNN相反)分析文本的能力使大型模型得以训练。事实证明,注意力机制的引入在概括文本方面非常有价值。
在深度学习出现之前,以前的NLP方法更基于规则,更简单(纯统计)的机器学习算法被教授在文本中查找单词和短语,当这些短语被发现时,会创建特定的答案。
该研究发表后,出现了许多流行的Transformer,其中最著名的是GPT(Generative Pre-trained Transformer)。
OpenAI是人工智能研究的先驱之一,它创建并训练了GPT模型。GPT-3是最新版本,拥有1750亿个参数。由于模型非常复杂,OpenAI决定不开源。在完成漫长的注册过程后,人们可以通过API使用它。
在这篇文章中,我将以一点技术背景介绍Transformer。然后,我们将使用Layer获取预训练的GPT2版本,以便为摘要目的对其进行微调。我们将使用的数据集包括亚马逊评论,可以通过以下链接在Kaggle中找到:
https://www.kaggle.com/code/currie32/summarizing-text-with-amazon-reviews/notebook
概述
- Transformer
- 亚马逊评论数据集
- 微调GPT2
- 评估我们的微调模型
- 结论
我在这篇文章中包含了最有启发性的代码。这个项目的完整代码库和数据集可以在我的公共Colab笔记本或Github Repo中找到。
https://github.com/aymanehachcham/GPT2_Text_Summarization
https://colab.research.google.com/drive/1iDhg8ss-BW0ZpzL-umJhd9XAm7xUlz-g?usp=sharing#scrollTo=rinybKnGJLpY
快速游览Transformer
基于注意力的模型
机器学习中的注意力机制是基于我们大脑皮Layer的工作方式。
当我们检查一幅图片来描述它时,我们自然会把注意力集中在我们知道掌握关键信息的几个关键地方。
我们不会以相同的强度检查图像的每个细节。当处理要分析的复杂数据时,这种方法可以帮助节省处理资源。
同样,当口译员将材料从源语言翻译成目标语言时,他们会根据先前的经验知道源句子中的哪些单词对应于翻译短语中的哪些术语。
GPT2
GPT语言模型最初于2018年由Alec Radford、Jeffrey Wu、Rewon Child和David Luan在论文“Language Models are Unsupervised Multitask Learners” 中引入,目的是开发一个可以从先前生成的文本中学习的系统。它将能够提供多种选择,以这种方式完成一个短语,节省时间,并增加文本的多样性和语言深度。这一切都没有任何语法错误。
GPT2架构的主要Layer是注意力Layer。在不太深入研究其技术性的情况下,我想列出其核心功能:
GPT2使用Byte Pair Encoding(BPE)在其词汇表中创建token。这意味着token通常是单词的一部分。
GPT-2以因果语言建模(CLM)为目标进行训练,因此能够预测序列中的下一个token,GPT-2可以通过利用该能力来创建语法上连贯的文本。
GPT-2生成合成文本样本,以响应用任意输入启动的模型。模型就像变色龙一样——它适应了条件文本的风格和内容。
亚马逊评论数据集
Kaggle中的数据集旨在开发一种算法,为亚马逊美食评估提供有意义的总结。该数据集由Kaggle提供,有超过50万条评论。
客户在亚马逊上写评论时,会创建一个文本评论和标题。评论的标题用作数据集中的摘要。
示例:“I have bought several of the Vitality canned dog food products and have found them all to be of good quality. The product looks more like a stew than a processed meat and it smells better. My Labrador is finicky and she appreciates this product better than most”.。
摘要:“Good Quality Dog Food”
数据集中有~71K个实例,足以训练GPT-2模型。
在开始处理阶段之前,我们需要建立与Layer的连接。我们需要登录并初始化项目:
# pip install layer
import layer
layer.login()
layer.init('text_summarization_with_gpt2')
然后,我们可以在项目初始化后访问Layer控制台,并准备好记录数据集和我们将使用的模型。
让我们将数据保存到Layer:
首先,我们通过将Github repo克隆到Colab中来安装它:
# Let's clone the repo of the project:
!git clone https://github.com/aymanehachcham/GPT2_Text_Summarization.git
# Load the dataset:
data_path = './GPT2_Text_Summarization/data/data_reviews.csv'
# Save the dataset to Layer:
@dataset('amazon_reviews')
@resources(data_path)
def read_reviews():
df = pd.read_csv(data_path)
# Remove null values:
df.dropna(inplace=True)
# Combining the two columns review and summary:
df['training'] = df['Text'] + 'TL;DR' + df['Summary']
df = df[['Summary','Text','training']][:5000]
return df
read_reviews()
我们现在可以通过以下方式从Layer获取数据:
reviews = layer.get_dataset('amazon_reviews').to_pandas()
执行数据处理
GPT-2的多任务功能是其最吸引人的功能之一。同时,同一模型可以在许多任务上进行训练。然而,我们必须使用组织文件中概述的相关任务指示符。
TL;DR符号代表“too lengthy; didn’t read”,是一个理想的总结。
TL;DR符号可以用作填充元素,以缩短评论文本,并向模型指示重要内容结束于此。
reviews = [review.replace("\n", " TL;DR ") for review in reviews]
我们还需要获取句子输入长度的平均值:
avg_length = sum([len(review.split()) for review in reviews])/len(reviews)
在我们的例子中,每个句子的平均长度总计为73个单词。我们可以推断,100的最大长度将覆盖大多数情况,因为单词中的平均实例长度是73。
用于处理数据样本的代码可能变得复杂且难以维护;为了提高可读性和模块性,我们希望数据集代码与模型训练代码分离。因此,我们可以使用PyTorch Dataset类作为包装器,它将充当一个紧凑的模块,将文本评论转换为可供训练的张量。
import torch
from torch.utils.data import Dataset, DataLoader
class GPT2ReviewDataset(Dataset):
def __init__(self, tokenizer, reviews, max_len):
self.max_len = max_len
self.tokenizer = tokenizer
self.eos = self.tokenizer.eos_token
self.eos_id = self.tokenizer.eos_token_id
self.reviews = reviews
self.result = []
for review in self.reviews:
# Encode the text using tokenizer.encode(). EOS at the end for end of sentence
tokenized = self.tokenizer.encode(review + self.eos, return_tensors='tf')
# Truncate the encoded tokenizer to max_len
padded = self.pad_truncate(tokenized)
# Creating a tensor and adding to the result
self.result.append(torch.tensor(padded))
def __len__(self):
return len(self.result)
def __getitem__(self, item):
return self.result[item]
def pad_truncate(self, name):
name_length = len(name) - extra_length
if name_length < self.max_len:
difference = self.max_len - name_length
result = name + [self.eos_id] * difference
elif name_length > self.max_len:
result = name[:self.max_len + 3]+[self.eos_id]
else:
result = name
return result
导入模型后,我们初始化Dataset和Dataloader组件,为训练阶段做准备。我们可以从Layer调用单词化器:
tokenizer = layer.get_model('layer/gpt/models/tokenizer').get_train()
reviews_dataset = GPT2ReviewDataset(tokenizer, reviews['training'], max_length)
dataloader = DataLoader(reviews_dataset, batch_size=32, shuffle=True)
微调GPT2
训练过程很简单,因为GPT2能够完成几个任务,包括摘要、生成和翻译。对于摘要,我们只需要将数据集的标签作为输入。训练部分包括构建GPT2模型并将其上传到Layer。
训练将在Layer中完成,我们将使用f-gpu-small结构,这是一个具有48GB内存的小型gpu。我们首先从训练循环开始:
def train(model, optimizer, dl, epochs):
for epoch in range(epochs):
for idx, batch in enumerate(dl):
with torch.set_grad_enabled(True):
optimizer.zero_grad()
batch = batch.to('cuda')
output = model(batch, labels=batch)
loss = output[0]
loss.backward()
optimizer.step()
if idx % 50 == 0:
layer.log({"loss": float(loss)}, step)
然后我们用所需的参数构建模型。我们将其上传到Layer:
@model("gpt2_text_summarization", dependencies=[Model("gpt2-tokenizer")])
@fabric("f-gpu-small")
@pip_requirements(packages=["torch","transformers","sentencepiece"])
def build_model():
from torch.utils.data import Dataset, DataLoader, RandomSampler, SequentialSampler
from torch import cuda
import torch
from torch.utils.data import Dataset, DataLoader
from transformers import AutoModelWithLMHead
parameters={
"BATCH_SIZE":32,
"EPOCHS":1,
"LEARNING_RATE":3e-4,
"MAX_TARGET_TEXT_LENGTH":max_length
}
# Log parameters to Layer
layer.log(parameters)
# Get the tokenizer:
tokenizer = layer.get_model('gpt2-tokenizer').get_train()
# Load pretrained model from Hugging face
model = AutoModelWithLMHead.from_pretrained("gpt2")
device = 'cuda' if cuda.is_available() else 'cpu'
model.to(device)
optimizer = torch.optim.AdamW(params = model.parameters(), lr=parameters['LEARNING_RATE'])
train(model, optimizer, dataloader, epochs=parameters['EPOCHS'])
return model
然后我们在Layer中运行训练,调用“Layer”。运行`function:
layer.run([build_model], debug=True)
训练就要开始了。这可能需要很长时间,取决于epoch和可用资源。
评估我们的微调模型
一旦你对我们的模型进行了微调,我们现在可以按照各自的方法开始处理评论:
- 步骤1:首先给模型一个评论。
- 步骤2:然后,从所有我们有一个top-k选项的评论中选择一个。
- 步骤3:将选择添加到摘要中,并将当前序列输入到模型中。
- 步骤4:应重复步骤2和3,直到达到max_length或生成EOS单词。
从模型的所有生成预测中选择前k个预测:
def topk_reviews(probs, n=9):
# The scores are initially assessed using softmax to convert to probabilities
probs = torch.softmax(probs, dim= -1)
# PyTorch has its own topk method, which we use here
tokensProb, topIx = torch.topk(probs, k=n)
# The new selection pool of 9 eligible choices is normalized
tokensProb = tokensProb / torch.sum(tokensProb)
# Send to CPU for numpy handling and run inference
tokensProb = tokensProb.cpu().detach().numpy()
# Make a random choice from the pool based on the new prob distribution
choice = np.random.choice(n, 1, p = tokensProb)
tokenId = topIx[choice][0]
return int(tokenId)
然后我们定义我们的推理方法:
def model_infer(model, tokenizer, review, max_length=15):
# Preprocess the init token (task designator)
review_encoded = tokenizer.encode(review)
result = review_encoded
initial_input = torch.tensor(review_encoded).unsqueeze(0).to('cuda')
with torch.set_grad_enabled(False):
# Feed the init token to the model
output = model(initial_input)
# Flatten the logits at the final time step
logits = output.logits[0,-1]
# Make a top-k choice and append to the result
result.append(topk(logits))
# For max_length times:
for _ in range(max_length):
# Feed the current sequence to the model and make a choice
input = torch.tensor(result).unsqueeze(0).to(device)
output = model(input)
logits = output.logits[0,-1]
res_id = topk(logits)
# If the chosen token is EOS, return the result
if res_id == tokenizer.eos_token_id:
return tokenizer.decode(result)
else: # Append to the sequence
result.append(res_id)
# IF no EOS is generated, return after the max_len
return tokenizer.decode(result)
我们现在可以用3个示例评论测试上面的代码,并查看生成摘要。
首先,我们从Layer调用经过训练的模型:
gpt2_model = layer.get_model('layer/gpt/models/gpt2').get_train()
我们从测试数据中抽取一些样本:
samples = [review.split('TL;DR')[0] for review in list(reviews[['training']]
.sample(n=3, random_state=1)['training'])]
for review in samples:
summaries = set()
print(review)
while len(summaries) < 3:
summary = model_infer(gpt2_model, tokenizer, review + "TL;DR").split("TL;DR")[1].strip()
if summary not in summaries:
summaries.add(summary)
print("Summaries: "+ str(summaries) +"\n")
最终结果:
评论1
文字评论:“Love these chips. Good taste, very crispy and very easy to clean up the entire 3 oz. bag in one sitting. NO greasy after-taste. Original and barbecue flavors are my favorites but I haven’t tried all flavors. Great product.”
相关摘要:{‘very yummy’, ‘Love these chips!’, ‘My favorite Kettle chip’}
评论2
文字评论:“We have not had saltines for many years because of unwanted ingredients. This brand is yummy and contains no unwanted ingredients. It was also a lot cheaper by the case than at the local supermarket.”
相关摘要:{‘yummy’, ‘yummy’, ‘Great product!’}
评论3
文字评论:“Best English Breakfast tea for a lover of this variety and I’ve tried so many including importing it from England. After a 20 year search I’ve found a very reasonable price for the most flavorful tea.”
相关摘要:{‘Wonderful Tea’, ‘The BEST tea for a lover of a cup of tea’, ‘Excellent tea for a lover of tea’}
总结
考虑到我们所做的训练,到目前为止我们取得的成绩相当不错。Layer简化了导入模型和分词器的整个过程。记录所有模型工件和数据集的选项也非常有用,因为你可以实时跟踪工作的效果。
所有代码都托管在上面提到的Google Colab中,请随时检查并自己尝试。
https://colab.research.google.com/drive/1iDhg8ss-BW0ZpzL-umJhd9XAm7xUlz-g?authuser=2#scrollTo=4JObyccnfeZO
感谢阅读!