一、Jupyter Notebook版本(带详细注释)
15.6-项目实战:电影评论情感分类
Pipeline
import torch
import torch.optim as optim
import torch.nn as nn
from datasets import load_dataset
from transformers import pipeline
import numpy as np
import matplotlib.pyplot as plt
from tqdm import *
import sys
import warnings
warnings.filterwarnings('ignore')
warnings.simplefilter('ignore')
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
classifier = pipeline("sentiment-analysis")
result = classifier("This is a great movie. I enjoyed it a lot!")[0]
print(result)
result = classifier("This movie is so bad, I almost fell asleep.")[0]
print(result)
数据集查看
from datasets import load_dataset
imdb_dataset = load_dataset('imdb')
print(imdb_dataset['train'][0])
print(imdb_dataset['train'][-1])
数据处理
class Dataset(torch.utils.data.Dataset):
def __init__(self, split):
self.dataset = load_dataset(path='imdb', split=split)
def __len__(self):
return len(self.dataset)
def __getitem__(self, i):
text = self.dataset[i]['text']
label = self.dataset[i]['label']
return text, label
train_dataset = Dataset('train')
test_dataset = Dataset('test')
print(len(train_dataset), len(test_dataset))
词元化
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('bert-base-cased')
tokenizer
def collate_fn(data):
sents = [i[0] for i in data]
labels = [i[1] for i in data]
data = tokenizer.batch_encode_plus(batch_text_or_text_pairs=sents,
truncation=True,
padding='max_length',
max_length=500,
return_tensors='pt',
return_length=True)
input_ids = data['input_ids']
attention_mask = data['attention_mask']
token_type_ids = data['token_type_ids']
labels = torch.LongTensor(labels)
return input_ids, attention_mask, token_type_ids, labels
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=32,
collate_fn=collate_fn,
shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=32,
collate_fn=collate_fn,
shuffle=True)
建立模型
from transformers import BertModel
pretrained = BertModel.from_pretrained('bert-base-cased').to(device)
for param in pretrained.parameters():
param.requires_grad_(False)
class Model(torch.nn.Module):
def __init__(self):
super().__init__()
self.fc = torch.nn.Linear(768, 2)
def forward(self, input_ids, attention_mask, token_type_ids):
with torch.no_grad():
out = pretrained(input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids)
out = self.fc(out.last_hidden_state[:, 0])
out = out.softmax(dim=1)
return out
model = Model().to(device)
class Trainer:
def __init__(self, model, train_loader, valid_loader):
self.train_loader = train_loader
self.valid_loader = valid_loader
self.device = device
self.model = model.to(self.device)
self.optimizer = optim.AdamW(self.model.parameters(), lr=0.001)
self.criterion = nn.CrossEntropyLoss()
self.scheduler = optim.lr_scheduler.ExponentialLR(self.optimizer, gamma=0.95)
self.train_losses = []
self.val_accuracy = []
def train(self, num_epochs):
for epoch in tqdm(range(num_epochs), file=sys.stdout):
total_loss = 0
self.model.train()
for input_ids, attention_mask, token_type_ids, labels in train_loader:
self.optimizer.zero_grad()
outputs = self.model(input_ids=input_ids.to(self.device),
attention_mask=attention_mask.to(self.device),
token_type_ids=token_type_ids.to(self.device)).to(self.device)
loss = self.criterion(outputs, labels.to(self.device))
loss.backward()
self.optimizer.step()
total_loss += loss.item()
self.scheduler.step()
accuracy = self.validate()
self.train_losses.append(total_loss)
self.val_accuracy.append(accuracy)
tqdm.write("Epoch: {0} Loss: {1} Acc: {2}".format(
epoch, self.train_losses[-1], self.val_accuracy[-1]))
def validate(self):
self.model.eval()
total = 0
correct = 0
with torch.no_grad():
for input_ids, attention_mask, token_type_ids, labels in self.valid_loader:
outputs = self.model(input_ids=input_ids.to(self.device),
attention_mask=attention_mask.to(self.device),
token_type_ids=token_type_ids.to(self.device)).to(self.device)
total += labels.size(0)
correct += (outputs.argmax(1) == labels.to(self.device)).sum().item()
accuracy = correct / total
return accuracy
模型训练和验证
trainer = Trainer(model, train_loader, test_loader)
trainer.train(num_epochs = 30)
plt.plot(trainer.train_losses, label='loss')
plt.legend()
plt.show()
plt.plot(trainer.val_accuracy, label='accuracy')
plt.legend()
plt.show()
直接finetune
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer
import numpy as np
import evaluate
dataset = load_dataset("imdb")
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
def tokenize_function(examples):
return tokenizer(examples["text"], padding="max_length", truncation=True)
tokenized_datasets = dataset.map(tokenize_function, batched=True)
small_train_dataset = tokenized_datasets["train"].shuffle(seed=0).select(range(1000))
small_eval_dataset = tokenized_datasets["test"].shuffle(seed=0).select(range(1000))
model = AutoModelForSequenceClassification.from_pretrained("bert-base-cased", num_labels=2)
metric = evaluate.load("accuracy")
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
training_args = TrainingArguments(
output_dir='./results',
num_train_epochs=10,
evaluation_strategy="epoch")
trainer = Trainer(
model=model,
args=training_args,
train_dataset=small_train_dataset,
eval_dataset=small_eval_dataset,
compute_metrics=compute_metrics,
)
trainer.train()
model.save_pretrained('./results/imdb_model')
model = AutoModelForSequenceClassification.from_pretrained('./results/imdb_model')
classifier = pipeline('sentiment-analysis', model=model, tokenizer=tokenizer)
result = classifier('This is a great movie. I enjoyed it a lot!')
print(result)
result = classifier('This movie is so bad, I almost fell asleep.')
print(result)
二、核心知识点梳理
2.1 关键概念(小白友好版)
| 概念 |
定义 |
小白解释 |
| 情感分类 |
对文本的情感倾向进行二分类(正向/负向) |
判断电影评论是好评还是差评 |
| Huggingface Pipeline |
封装好的一站式工具,自动完成“文本处理→模型推理→结果输出” |
一行代码实现情感分析,不用手动写复杂的模型加载/推理逻辑 |
| IMDB数据集 |
5万条英文电影评论数据集(2.5万训练+2.5万测试),label=0负向/1正向 |
专门用于情感分类的经典数据集,相当于“深度学习的练习题” |
| 词元化(Tokenization) |
将文本转换为模型可识别的数字序列 |
把“Great movie”变成[101, 1939, 3185, 102],模型能看懂数字 |
| Attention Mask |
标记有效文本(1)和填充(0),模型忽略填充位置 |
告诉模型“这些0是补的,不用计算”,避免干扰结果 |
| [CLS] Token |
BERT输入开头的特殊标记,其向量代表整个句子的语义 |
用一个向量概括整段评论的含义,输入到分类层判断情感 |
| 预训练模型微调(Finetune) |
在预训练BERT基础上,训练下游任务的分类层 |
站在巨人的肩膀上,不用从零训练模型,只改最后一层 |
| 冻结(Freeze) |
固定预训练模型参数,只训练新增的分类层 |
不让BERT的核心语义知识被破坏,训练更快、效果更好 |
| 学习率调度器 |
训练过程中动态调整学习率,避免后期震荡 |
训练前期大步走,后期小步走,更容易找到最优解 |
| Transformers Trainer |
封装好的训练器,自动完成训练、验证、保存 |
不用手动写训练循环,一行代码启动训练,适合快速开发 |
2.2 核心库与函数用法表
| 库/函数 |
作用 |
关键参数/方法 |
适用场景 |
transformers.pipeline |
快速构建推理管道 |
task="sentiment-analysis" |
快速验证模型效果,原型开发 |
datasets.load_dataset |
加载开源数据集 |
name="imdb" |
获取标准化的训练/测试数据 |
AutoTokenizer |
自动匹配模型的分词器 |
from_pretrained("bert-base-cased") |
文本→数字编码,保证与模型兼容 |
tokenizer.batch_encode_plus |
批量编码文本 |
truncation=True, padding="max_length" |
统一文本长度,适配批量训练 |
BertModel |
加载BERT基础模型 |
from_pretrained("bert-base-cased").to(device) |
提取文本的语义向量 |
AutoModelForSequenceClassification |
加载带分类头的BERT |
num_labels=2 |
直接用于分类任务,无需手动定义分类层 |
TrainingArguments |
配置训练参数 |
output_dir, num_train_epochs, evaluation_strategy |
定义训练的轮数、输出路径、评估策略 |
Trainer(Transformers) |
封装训练逻辑 |
model, args, train_dataset, eval_dataset |
自动化训练,支持断点续训、日志记录 |
evaluate.load("accuracy") |
加载评估指标 |
compute(predictions, references) |
计算模型准确率,评估效果 |
2.3 自定义Trainer类核心逻辑
| 方法 |
核心步骤 |
作用 |
__init__ |
初始化优化器(AdamW)、损失函数(CrossEntropyLoss)、学习率调度器 |
配置训练所需的核心组件 |
train |
遍历轮数→批量训练→损失计算→反向传播→学习率更新→验证准确率 |
实现完整的训练流程 |
validate |
禁用梯度→批量推理→统计正确数→计算准确率 |
评估模型泛化能力,避免过拟合 |
三、PyCharm版本代码(可直接运行)
import torch
import torch.optim as optim
import torch.nn as nn
from datasets import load_dataset
from transformers import pipeline, AutoTokenizer, BertModel, AutoModelForSequenceClassification, TrainingArguments, Trainer
import numpy as np
import matplotlib.pyplot as plt
from tqdm import *
import sys
import warnings
import evaluate
warnings.filterwarnings('ignore')
warnings.simplefilter('ignore')
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备:{device}")
print("\n=== 步骤1:快速情感分析(Pipeline) ===")
classifier = pipeline("sentiment-analysis")
result_pos = classifier("This is a great movie. I enjoyed it a lot!")[0]
print("正向评论结果:", result_pos)
result_neg = classifier("This movie is so bad, I almost fell asleep.")[0]
print("负向评论结果:", result_neg)
print("\n=== 步骤2:加载IMDB数据集 ===")
imdb_dataset = load_dataset('imdb')
print("训练集第一条数据:", imdb_dataset['train'][0]['text'][:100], "...")
print("训练集第一条标签:", imdb_dataset['train'][0]['label'])
print("\n=== 步骤3:数据加载器配置 ===")
class Dataset(torch.utils.data.Dataset):
def __init__(self, split):
self.dataset = load_dataset(path='imdb', split=split)
def __len__(self):
return len(self.dataset)
def __getitem__(self, i):
return self.dataset[i]['text'], self.dataset[i]['label']
train_dataset = Dataset('train')
test_dataset = Dataset('test')
print(f"训练集长度:{len(train_dataset)},测试集长度:{len(test_dataset)}")
tokenizer = AutoTokenizer.from_pretrained('bert-base-cased')
def collate_fn(data):
sents = [i[0] for i in data]
labels = [i[1] for i in data]
data = tokenizer.batch_encode_plus(batch_text_or_text_pairs=sents,
truncation=True,
padding='max_length',
max_length=500,
return_tensors='pt',
return_length=True)
input_ids = data['input_ids']
attention_mask = data['attention_mask']
token_type_ids = data['token_type_ids']
labels = torch.LongTensor(labels)
return input_ids, attention_mask, token_type_ids, labels
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=8,
collate_fn=collate_fn,
shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=8,
collate_fn=collate_fn,
shuffle=True)
print("\n=== 步骤4:构建情感分类模型 ===")
pretrained = BertModel.from_pretrained('bert-base-cased').to(device)
for param in pretrained.parameters():
param.requires_grad_(False)
class Model(torch.nn.Module):
def __init__(self):
super().__init__()
self.fc = torch.nn.Linear(768, 2)
def forward(self, input_ids, attention_mask, token_type_ids):
with torch.no_grad():
out = pretrained(input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids)
out = self.fc(out.last_hidden_state[:, 0])
out = out.softmax(dim=1)
return out
model = Model().to(device)
print("模型构建完成")
class Trainer:
def __init__(self, model, train_loader, valid_loader):
self.train_loader = train_loader
self.valid_loader = valid_loader
self.device = device
self.model = model.to(self.device)
self.optimizer = optim.AdamW(self.model.parameters(), lr=0.001)
self.criterion = nn.CrossEntropyLoss()
self.scheduler = optim.lr_scheduler.ExponentialLR(self.optimizer, gamma=0.95)
self.train_losses = []
self.val_accuracy = []
def train(self, num_epochs):
for epoch in tqdm(range(num_epochs), file=sys.stdout):
total_loss = 0
self.model.train()
for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(train_loader):
if i >= 100:
break
self.optimizer.zero_grad()
outputs = self.model(input_ids=input_ids.to(self.device),
attention_mask=attention_mask.to(self.device),
token_type_ids=token_type_ids.to(self.device)).to(self.device)
loss = self.criterion(outputs, labels.to(self.device))
loss.backward()
self.optimizer.step()
total_loss += loss.item()
self.scheduler.step()
accuracy = self.validate(limit_batch=50)
self.train_losses.append(total_loss)
self.val_accuracy.append(accuracy)
tqdm.write(f"Epoch: {epoch} Loss: {total_loss:.2f} Acc: {accuracy:.4f}")
def validate(self, limit_batch=None):
self.model.eval()
total = 0
correct = 0
with torch.no_grad():
for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(self.valid_loader):
if limit_batch and i >= limit_batch:
break
outputs = self.model(input_ids=input_ids.to(self.device),
attention_mask=attention_mask.to(self.device),
token_type_ids=token_type_ids.to(self.device)).to(self.device)
total += labels.size(0)
correct += (outputs.argmax(1) == labels.to(self.device)).sum().item()
accuracy = correct / total if total > 0 else 0
return accuracy
print("\n=== 步骤5:模型训练(简化版,仅3轮) ===")
trainer = Trainer(model, train_loader, test_loader)
trainer.train(num_epochs = 3)
print("\n=== 步骤6:绘制训练曲线 ===")
plt.plot(trainer.train_losses, label='loss')
plt.title("Training Loss")
plt.legend()
plt.show()
plt.plot(trainer.val_accuracy, label='accuracy')
plt.title("Validation Accuracy")
plt.legend()
plt.show()
print("\n=== 步骤7:Transformers原生Trainer微调 ===")
dataset = load_dataset("imdb")
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
def tokenize_function(examples):
return tokenizer(examples["text"], padding="max_length", truncation=True, max_length=128)
tokenized_datasets = dataset.map(tokenize_function, batched=True)
small_train_dataset = tokenized_datasets["train"].shuffle(seed=0).select(range(100))
small_eval_dataset = tokenized_datasets["test"].shuffle(seed=0).select(range(100))
model = AutoModelForSequenceClassification.from_pretrained("bert-base-cased", num_labels=2)
metric = evaluate.load("accuracy")
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
training_args = TrainingArguments(
output_dir='./results',
num_train_epochs=2,
evaluation_strategy="epoch",
per_device_train_batch_size=4,
per_device_eval_batch_size=4)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=small_train_dataset,
eval_dataset=small_eval_dataset,
compute_metrics=compute_metrics,
)
trainer.train()
model.save_pretrained('./results/imdb_model')
model = AutoModelForSequenceClassification.from_pretrained('./results/imdb_model')
classifier = pipeline('sentiment-analysis', model=model, tokenizer=tokenizer)
print("\n=== 步骤8:测试微调后的模型 ===")
print("正向评论测试:", classifier('This is a great movie!'))
print("负向评论测试:", classifier('This movie is terrible!'))
print("\n=== 所有步骤执行完成 ===")
总结
- 核心流程:电影评论情感分类的核心是“文本编码→BERT提取语义→分类层输出情感”,分为快速推理(Pipeline)和自定义训练(冻结预训练模型+训练分类层)两种方式;
- 关键工具:Huggingface生态的三大核心工具(Datasets加载数据、Transformers加载模型/分词器、Trainer封装训练)能大幅简化开发流程,是NLP实战的首选;
- 优化技巧:冻结预训练模型参数可减少训练成本,调整批量大小/文本长度可适配不同硬件,学习率调度器能提升训练稳定性;
- 新手避坑:
- 确保模型和分词器版本匹配(如bert-base-cased的模型配同名分词器);
- GPU显存不足时,减小batch_size或文本长度;
- 训练轮数过多会过拟合,需结合验证准确率停止训练。
避坑提示
- 安装依赖:
pip install torch transformers datasets evaluate tqdm matplotlib;
- 模型下载慢:手动下载模型文件放到
~/.cache/huggingface/hub/;
- GPU显存不足:将batch_size改为4/2,max_length改为128;
- 中文情感分类:替换模型为
bert-base-chinese,数据集改为中文评论数据集(如ChnSentiCorp);
- 训练时间长:优先用Pipeline快速验证,再用小数据集微调。
所有评论(0)