实验任务二: 预训练语言模型¶
预训练语言模型¶
实验目标
通过本次实验,你将掌握以下内容:
- 使用GPU训练模型
- 了解预训练语言模型
- 使用预训练语言模型进行文本分类
本次实验所用的预训练模型(BERT)下载链接如下:
预训练模型(BERT)下载链接:¶
https://box.nju.edu.cn/d/2710380144234ce78fe3/
1. 使用GPU训练模型¶
在PyTorch中,可以使用以下代码来检测当前环境是否有可用的GPU:
import torch
# 检查是否有可用的GPU
if torch.cuda.is_available():
print(f"CUDA is available. Number of GPUs: {torch.cuda.device_count()}")
print(f"Current device: {torch.cuda.current_device()}")
print(f"Device name: {torch.cuda.get_device_name(torch.cuda.current_device())}")
else:
print("CUDA is not available. Using CPU.")
如果显示'CUDA is not available. Using CPU.'请确认启动的环境是否正确或者尝试重新安装pytorch或者与助教联系。
GPU训练提示
如果要用GPU训练,则需要把数据和模型都放到GPU上才能训练。如果一个在CPU一个在GPU,则会报错。
定义模型后,通过model = model.to(device)把模型放到GPU上。 把模型放到GPU上的代码示例:
# 检查是否有可用的GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 创建模型
model = SimpleModel()
# 将模型放到GPU(如果可用)
model = model.to(device)
由于模型在GPU上,所以数据也必须在GPU上才能送入模型。通过inputs = inputs.to(device)把input放到GPU上。值得说明的是由于模型的输出也在GPU上,所以标签也需要放到GPU上以便于计算损失,通过labels = labels.to(device)。
把数据放到GPU上的代码示例:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
# 训练示例
num_epochs = 3
for epoch in range(num_epochs):
model.train()
for inputs, labels in train_loader:
# 将数据放到GPU(如果可用)
inputs, labels = inputs.to(device), labels.to(device)
# 前向传播
outputs = model(inputs)
通过上述过程,我们可以把数据和模型都放到GPU上从而加速训练。
你可以使用以下命令查看是否使用了GPU并且观察的GPU利用率:
这个命令会每5秒(-n 5)更新一次NVIDIA GPU的状态信息。
2. 了解预训练语言模型¶
预训练语言模型简介
预训练语言模型(pre-trained language models)是指在大规模数据集上预先训练过的语言模型。这些模型已经学习到了一些基础的特征或知识,并可以被迁移到特定的任务上进行微调(fine-tuning)。
下面我们以BERT为例,用的bert-base-uncased版本进行实验。我们首先用AutoModel和AutoTokenizer加载模型和分词器。分词器是把文本的每个词元映射到对应的索引,以便于BERT的embedding层完成索引到嵌入的映射。
完整代码如下:
import torch
from transformers import AutoModel, AutoTokenizer
# 指定模型名称
model_name = 'bert-base-uncased'
# 读取模型对应的tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 载入模型
model = AutoModel.from_pretrained(model_name)
# 输入文本
input_text = "Here is some text to encode"
# 通过tokenizer把文本变成 token_id
input_ids = tokenizer.encode(input_text, add_special_tokens=True)
print(input_ids)
# 转换为Tensor
input_ids = torch.tensor([input_ids])
# 获得BERT的输出
with torch.no_grad():
output = model(input_ids)
# 获得BERT模型最后一个隐层结果
output_hidden_states = output.last_hidden_state
output_hidden_states.shape
除此之外,由于批处理过程需要一个批次中文本长度相同,因此额外引入了padding。所以,我们需要使用了attention_mask屏蔽这些padding token,不让其参与自注意力的计算。
最终的输出是文本中所有词元的隐藏状态(hidden states)。
我们可以用model.named_parameters(): 观察模型的所有参数及其形状,完整代码如下:
import torch
from transformers import AutoModel, AutoTokenizer
# 指定模型名称
model_name = 'bert-base-uncased'
# 读取模型对应的tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 载入模型
model = AutoModel.from_pretrained(model_name)
# 打印模型所有参数的名称和形状
for name, param in model.named_parameters():
print(f"Parameter Name: {name}, Shape: {param.shape}")
3. 使用预训练模型进行文本分类¶
可能需要安装transformers包
在本章节中,你将基于上面的BERT代码和AG NEWS数据集进行基于预训练模型BERT的文本分类。你将完善下述代码同时探索多种句子聚合方式对结果的影响,其中句子聚合方式指的是从词嵌入中得到句子嵌入的过程。需要探索的句子聚合方式包括:
- 直接使用[CLS]的嵌入表示当做句子嵌入。
- 使用mean-pooling平均一个句子中的所有词元得到嵌入
- 使用注意力机制给每个词元分配一个权重,通过加权求和的方式得到嵌入。你可以使用任意注意力机制计算。
学习率可以参考设置为2e-5。
import torch
import pandas as pd
import numpy as np
from torch.utils.data import Dataset, DataLoader
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Adam
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from tqdm import tqdm
# **1. 加载 AG NEWS 数据集**
df = pd.read_csv("train.csv") # 请替换成你的文件路径
df.columns = ["label", "title", "description"] # CSV 有3列: 标签, 标题, 描述
df["text"] = df["title"] + " " + df["description"] # 合并标题和描述作为输入文本
df["label"] = df["label"] - 1 # AG NEWS 的标签是 1-4,我们转换成 0-3
train_texts, train_labels = df["text"].tolist(), df["label"].tolist()
number = int(0.3 * len(train_texts))
train_texts, train_labels = train_texts[: number], train_labels[: number]
df = pd.read_csv("test.csv") # 请替换成你的文件路径
df.columns = ["label", "title", "description"] # CSV 有3列: 标签, 标题, 描述
df["text"] = df["title"] + " " + df["description"] # 合并标题和描述作为输入文本
df["label"] = df["label"] - 1 # AG NEWS 的标签是 1-4,我们转换成 0-3
test_texts, test_labels = df["text"].tolist(), df["label"].tolist()
# **2. 加载 BERT Tokenizer**
model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
# **3. 处理数据**
class AGNewsDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_length=50):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
self.max_length = max_length
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
text = self.texts[idx]
label = self.labels[idx]
encoding = self.tokenizer(
text, truncation=True, padding="max_length", max_length=self.max_length, return_tensors="pt"
)
return {
"input_ids": encoding["input_ids"].squeeze(0),
"attention_mask": encoding["attention_mask"].squeeze(0),
"labels": torch.tensor(label, dtype=torch.long),
} # 此处会自动生成BERT输入所需要的attention_mask
train_dataset = AGNewsDataset(train_texts, train_labels, tokenizer)
test_dataset = AGNewsDataset(test_texts, test_labels, tokenizer)
train_dataloader = DataLoader(train_dataset, batch_size=16, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=16, shuffle=False)
# **4. 定义和加载BERT分类模型**
#TODO:定义模型并且放到GPU上
class BERTClassifier(nn.Module):
def __init__(self, model_name, num_labels):
super(BERTClassifier, self).__init__()
self.bert =
self.classifier =
def forward(self, input_ids, attention_mask):
return logits
model = BERTClassifier(model_name, num_labels=4).to(device)
# **5. 设置优化器和损失函数**
#TODO: 定义优化器和损失函数
# **6. 训练 BERT**
EPOCHS = 3
for epoch in range(EPOCHS):
model.train()
total_loss = 0
loop = tqdm(train_dataloader, desc=f"Epoch {epoch+1}")
for batch in loop:
#TODO: 基于后面需要打印的损失,定义训练过程
print(f"Epoch {epoch+1}, Loss: {total_loss/len(train_dataloader):.4f}")
# **7. 评估模型**
model.eval()
preds, true_labels = [], []
with torch.no_grad():
for batch in test_dataloader:
#TODO: 基于后面计算acc需要的true_labels和preds,完善下面测试代码
acc = accuracy_score(true_labels, preds)
print(f"Test Accuracy: {acc:.4f}")
训练速度
你如果觉得训练速度慢,可以尝试增大batch size,不过注意不要炸显存。
思考题
思考题1:你觉得以上三种得到句子嵌入的方案,哪种效果会最好,哪种效果会最差?为什么?
思考题
思考题2:如果一个文档包括多个句子,我们需要获得其中每个句子的嵌入表示。那么,我们应该怎么利用BERT得到每个句子的嵌入?