第 4 章 · 文本分类
本章目标:用表示模型(任务专用模型、嵌入 + 分类器、零样本)与生成模型(encoder-decoder 与 ChatGPT)两条技术路线完成影评情感分类,并学会评估各自的性能。
本章对应《Hands-On Large Language Models》(Jay Alammar & Maarten Grootendorst 著)第 4 章的官方笔记本:分别用 representative models 和 generative models 对文本做分类。
4.1 [可选] 在 Colab 上安装依赖
如果你在 Google Colab(或任何其他云环境)中查看本笔记本,需要取消注释并运行下面的代码块来安装本章的依赖:
💡 NOTE:运行本章示例需要 GPU。在 Google Colab 中,进入 Runtime > Change runtime type > Hardware accelerator > GPU > GPU type > T4。
# %%capture
# !pip install "transformers==4.41.2" "sentence-transformers==3.0.1" openai
# !pip install -U datasets4.2 数据
我们使用 Rotten Tomatoes 影评数据集——每条评论标注为正面或负面:
from datasets import load_dataset
# Load our data
data = load_dataset("rotten_tomatoes")
datadata["train"][0, -1]查看第一条与最后一条训练样本,感受一下数据的形态:一条 text(评论文本)加一个 label(0 = 负面,1 = 正面)。
4.3 使用表示模型做文本分类
4.3.1 使用任务专用模型
最直接的做法是使用一个已经在类似任务上微调过的模型。这里选择 cardiffnlp/twitter-roberta-base-sentiment-latest,一个在推特数据上训练的情感分析模型:
from transformers import pipeline
# Path to our HF model
model_path = "cardiffnlp/twitter-roberta-base-sentiment-latest"
# Load model into pipeline
pipe = pipeline(
model=model_path,
tokenizer=model_path,
return_all_scores=True,
device="cuda:0"
)import numpy as np
from tqdm import tqdm
from transformers.pipelines.pt_utils import KeyDataset
# Run inference
y_pred = []
for output in tqdm(pipe(KeyDataset(data["test"], "text")), total=len(data["test"])):
negative_score = output[0]["score"]
positive_score = output[2]["score"]
assignment = np.argmax([negative_score, positive_score])
y_pred.append(assignment)该模型输出三类(negative / neutral / positive),我们的任务是二分类,所以只比较负面与正面两个分数,取较大者作为预测结果。
接下来定义评估函数并查看整体性能:
from sklearn.metrics import classification_report
def evaluate_performance(y_true, y_pred):
"""Create and print the classification report"""
performance = classification_report(
y_true, y_pred,
target_names=["Negative Review", "Positive Review"]
)
print(performance)evaluate_performance(data["test"]["label"], y_pred)4.3.2 利用嵌入做分类任务
监督分类
不使用现成的情感模型,而是先用嵌入模型把文本转成向量,再在向量上训练一个普通分类器:
from sentence_transformers import SentenceTransformer
# Load model
model = SentenceTransformer('sentence-transformers/all-mpnet-base-v2')
# Convert text to embeddings
train_embeddings = model.encode(data["train"]["text"], show_progress_bar=True)
test_embeddings = model.encode(data["test"]["text"], show_progress_bar=True)train_embeddings.shapeall-mpnet-base-v2 输出 768 维嵌入向量。
from sklearn.linear_model import LogisticRegression
# Train a Logistic Regression on our train embeddings
clf = LogisticRegression(random_state=42)
clf.fit(train_embeddings, data["train"]["label"])# Predict previously unseen instances
y_pred = clf.predict(test_embeddings)
evaluate_performance(data["test"]["label"], y_pred)Tip!
如果完全不使用分类器会怎样?我们可以对每个类别的嵌入求平均,再用余弦相似度预测哪一类与文档最匹配:
import numpy as np
import pandas as pd
from sklearn.metrics import classification_report
from sklearn.metrics.pairwise import cosine_similarity
# Average the embeddings of all documents in each target label
df = pd.DataFrame(np.hstack([train_embeddings, np.array(data["train"]["label"]).reshape(-1, 1)]))
averaged_target_embeddings = df.groupby(768).mean().values
# Find the best matching embeddings between evaluation documents and target embeddings
sim_matrix = cosine_similarity(test_embeddings, averaged_target_embeddings)
y_pred = np.argmax(sim_matrix, axis=1)
# Evaluate the model
evaluate_performance(data["test"]["label"], y_pred)零样本分类(Zero-shot Classification)
嵌入方法最迷人的一点:根本不需要训练!只要给每个标签写一句描述,把描述也变成嵌入,看文档与哪个标签描述更接近:
# Create embeddings for our labels
label_embeddings = model.encode(["A negative review", "A positive review"])from sklearn.metrics.pairwise import cosine_similarity
# Find the best matching label for each document
sim_matrix = cosine_similarity(test_embeddings, label_embeddings)
y_pred = np.argmax(sim_matrix, axis=1)evaluate_performance(data["test"]["label"], y_pred)Tip!
如果换用不同的描述会怎样?试试 "A very negative movie review" 和 "A very positive movie review",看看性能会发生什么变化!
4.4 使用生成模型做文本分类
4.4.1 Encoder-decoder 模型
生成模型走的是另一条路:让模型生成答案而不是输出分数。这里使用 encoder-decoder 架构的 google/flan-t5-small:
# Load our model
pipe = pipeline(
"text2text-generation",
model="google/flan-t5-small",
device="cuda:0"
)# Prepare our data
prompt = "Is the following sentence positive or negative? "
data = data.map(lambda example: {"t5": prompt + example['text']})
data# Run inference
y_pred = []
for output in tqdm(pipe(KeyDataset(data["test"], "t5")), total=len(data["test"])):
text = output[0]["generated_text"]
y_pred.append(0 if text == "negative" else 1)evaluate_performance(data["test"]["label"], y_pred)注意解析逻辑:模型生成的文本恰好是 "negative" 就记 0,其余情况记 1——生成模型的输出是自由文本,需要这样的映射规则。
4.4.2 用 ChatGPT 做分类
最后尝试闭源的 OpenAI API。先创建客户端:
import openai
# Create client
client = openai.OpenAI(api_key="YOUR_KEY_HERE")定义一个生成函数,把系统提示与用户提示组装后调用 API:
def chatgpt_generation(prompt, document, model="gpt-3.5-turbo-0125"):
"""Generate an output based on a prompt and an input document."""
messages=[
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": prompt.replace("[DOCUMENT]", document)
}
]
chat_completion = client.chat.completions.create(
messages=messages,
model=model,
temperature=0
)
return chat_completion.choices[0].message.content# Define a prompt template as a base
prompt = """Predict whether the following document is a positive or negative movie review:
[DOCUMENT]
If it is positive return 1 and if it is negative return 0. Do not give any other answers.
"""
# Predict the target using GPT
document = "unpretentious , charming , quirky , original"
chatgpt_generation(prompt, document)下一步是对整个评估集跑 OpenAI 模型。不过请确认你有足够的额度再运行——它会对整个测试集(1066 条记录)逐一调用 API:
# You can skip this if you want to save your (free) credits
predictions = [chatgpt_generation(prompt, doc) for doc in tqdm(data["test"]["text"])]# Extract predictions
y_pred = [int(pred) for pred in predictions]
# Evaluate performance
evaluate_performance(data["test"]["label"], y_pred)4.5 本章小结
- 文本分类有两条路线:表示模型(嵌入/打分)与生成模型(生成答案文本);
- 任务专用模型(twitter-roberta-base-sentiment-latest)开箱即用,但它输出三分类,需要自行合并为二分类决策;
- 嵌入 + LogisticRegression 是强力的监督基线;对各类嵌入求平均再做余弦相似度,可以完全不用分类器;
- 零样本分类只需给标签写描述并编码成嵌入,无需任何训练;描述的措辞会影响效果;
- flan-t5 这类 encoder-decoder 模型生成自由文本,需要字符串映射规则转成标签;ChatGPT 方式灵活但按调用量计费。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 本章使用的 rotten_tomatoes 数据集是什么类型的任务?
2. cardiffnlp/twitter-roberta-base-sentiment-latest 输出三个类别,而任务是二分类,代码如何处理?
3. 本章的零样本分类是如何实现的?
4. 使用 flan-t5 做分类时,代码如何把生成文本转换为标签?
🛠️ 动手实践
- 按 4.3.2 节的 Tip 提示,把零样本标签描述换成
"A very negative movie review"与"A very positive movie review",重新计算classification_report,与原描述的 F1 分数对比并总结措辞对零样本分类的影响。 - 把嵌入模型从 all-mpnet-base-v2 换成
sentence-transformers/all-MiniLM-L6-v2,重跑「嵌入 + LogisticRegression」全流程,对比两个模型在测试集上的准确率与推理耗时。 - 用 4.4.2 节的 prompt 模板对
data["train"][0]的评论调用chatgpt_generation,然后手工构造 3 个对抗样例(如反讽评论),观察 GPT-3.5 的判断是否正确并记录失败模式。