<think>好的,我现在需要帮用户优化他们的PyTorch情感分析代码。首先,我得仔细看看他们提供的代码,找出可以改进的地方。用户要求不要删减原有代码,只是增加新的优化部分,所以我要确保不改变原有结构,而是在此基础上添加优化措施。
首先,用户代码中已经实现了BiLSTM模型、数据预处理、训练循环以及早停机制。可能的优化点包括学习率调度、更复杂的模型结构、正则化方法、数据增强、更详细的评估指标等。
学习率调度:用户当前使用的是固定学习率的Adam优化器。可以加入学习率衰减,比如ReduceLROnPlateau,当验证损失不再下降时自动降低学习率,这有助于模型在后期更稳定地收敛。模型结构优化:现有的BiLSTM模型可能可以增加层数或者加入注意力机制。比如,使用多层LSTM,或者添加一个Attention层,让模型关注更重要的词,提升分类性能。
正则化方法:用户已经用了Dropout,可以考虑加入其他正则化技术,比如权重衰减(L2正则化),或者LabelSmoothing,减少过拟合。
数据增强:对于文本数据,可以在训练时随机进行词汇替换(同义词替换)、随机删除或交换词语,增加数据的多样性,提升模型泛化能力。评估指标:目前只跟踪了准确率和损失,可以加入F1分数、精确率、召回率等,尤其是在类别不平衡时更有参考价值。
梯度裁剪:在训练循环中加入梯度裁剪,防止梯度爆炸,提升训练稳定性。
嵌入层预训练:用户当前使用的是随机初始化的嵌入层,可以考虑用预训练的词向量(如GloVe)来初始化,提升模型的表现。
训练过程可视化:用户已经保存了损失和准确率的曲线,可以增加更多的可视化,比如混淆矩阵,或者每个epoch的学习率变化。
日志记录:添加更详细的日志记录,比如每个batch的损失和准确率,方便后续分析。
集成学习:训练多个模型进行集成,但可能改动较大,可能不符合用户不删减代码的要求。现在,我需要决定哪些优化可以在不改变原有代码结构的情况下加入。比如,学习率调度、梯度裁剪、注意力机制、评估指标、数据增强等。
注意力机制:在BiLSTM之后添加一个注意力层,让模型能够关注重要的时间步。这需要修改模型的前向传播部分,但保持原有的类结构。学习率调度:在优化器之后添加scheduler,并在每个epoch后根据验证损失调整学习率。
梯度裁剪:在optimizer.step()之前,对梯度进行裁剪。数据增强:在数据预处理时,添加数据增强函数,比如随机替换同义词,但需要保持原有的数据加载流程,可能需要修改数据集的__getitem__方法,在训练时动态增强数据。
评估指标:在评估函数中计算F1分数等,需要引入sklearn的metrics库。
另外,用户代码中的设备配置部分,引号用了中文的‘’,应该改为英文的',否则会报错。但用户可能已经处理过这个问题,或者可能是个笔误,需要指出。现在,我需要将这些优化点整合到原有代码中,确保代码能正常运行,并给出解释。
首先,修改模型,添加注意力层:
在BiLSTMSentiment类中,修改forward函数,加入注意力机制。比如,使用最后一层隐藏状态的拼接作为上下文向量,或者计算每个时间步的注意力权重。然后,在优化器中加入权重衰减,即Adam的weight_decay参数。添加学习率调度器,如ReduceLROnPlateau。在训练函数中,每个epoch后根据验证损失调整学习率。在训练循环中,加入梯度裁剪,使用torch.nn.utils.clip_grad_norm_。
在数据预处理阶段,添加数据增强函数,比如随机替换、删除等,并在数据加载时应用。
在评估函数中,计算F1分数和准确率。此外,检查代码中的引号是否正确,比如device =torch.device(‘cuda’...)中的引号是否是英文的。现在,逐步整合这些优化:
1.模型添加注意力层:
在BiLSTMSentiment类中添加Attention机制:
classBiLSTMSentiment(nn.Module):def__init__(self, vocab_size,embedding_dim=128, hidden_dim=128,num_layers=2):super().__init__()self.embedding =nn.Embedding(vocab_size, embedding_dim)self.lstm =nn.LSTM(embedding_dim,hidden_dim, num_layers=num_layers,bidirectional=True, batch_first=True, dropout=0.3if num_layers>1else0)self.attention= nn.Linear(hidden_dim*2,1)#注意力层self.dropout =nn.Dropout(0.3)self.fc= nn.Sequential(nn.Linear(hidden_dim*2,64),nn.ReLU(),nn.Dropout(0.3),nn.Linear(64,5))def forward(self,x):x= self.embedding(x)#(batch,seq_len, embedding_dim)lstm_out, (hn,cn)= self.lstm(x)# lstm_out: (batch,seq_len, hidden_dim*2)#计算注意力权重attn_weights= torch.softmax(self.attention(lstm_out).squeeze(-1),dim=1)#(batch, seq_len)#加权求和context=torch.bmm(attn_weights.unsqueeze(1), lstm_out).squeeze(1)# (batch,hidden_dim*2)return self.fc(self.dropout(context))
这里,将原来的取最后隐藏状态改为使用注意力机制,这样模型可以关注更重要的词。
2.优化器加入权重衰减:optimizer= torch.optim.Adam(model.parameters(),lr=0.001,weight_decay=1e-4)
3.学习率调度器:
在train_model函数中,初始化scheduler:scheduler= torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer,mode='max',factor=0.5,patience=2,verbose=True)
在每个epoch验证后,根据val_acc调整学习率:scheduler.step(val_acc)
或者根据val_loss,但mode应为'min'。用户当前的早停是根据val_acc,所以可能更适合根据val_acc来调整。4.梯度裁剪:在训练循环中,loss.backward()之后加入:
torch.nn.utils.clip_grad_norm_(model.parameters(),max_norm=1.0)optimizer.step()
5.数据增强:
在数据预处理时,添加一个数据增强函数,例如随机替换、删除等。例如,在数据集的__getitem__方法中,对输入的序列进行随机处理:classSentimentDataset(Dataset):def__init__(self, sequences, labels, weights, augment=False):self.sequences= sequencesself.labels =labelsself.weights= weightsself.augment= augmentself.tokenizer =...#可能需要传递tokenizer来获取词汇表def__getitem__(self,idx):seq= self.sequences[idx]label= self.labels[idx]weight= self.weights[idx]ifself.augment:#数据增强逻辑,例如随机mask或替换seq= self.augment_sequence(seq)returnseq,label,weightdefaugment_sequence(self, seq):#实现具体的数据增强方法#例如,随机将某些token替换为0(padding索引)mask_prob=0.1mask= np.random.rand(len(seq))< mask_probseq[mask] =0returnseq但用户的数据已经经过pad_sequences处理,所以可能需要调整。或者,在数据加载时动态增强,但需要传递augment参数。不过,这可能需要对原有代码进行修改,比如在数据加载时区分训练集和其他集是否进行增强。6.评估指标:
修改evaluate函数,计算F1分数、精确率、召回率:
fromsklearn.metrics importf1_score, precision_score,recall_scoredefevaluate(model, loader):model.eval()total_loss,total_correct =0,0all_preds= []all_labels =[]criterion =nn.CrossEntropyLoss()with torch.no_grad():forseq,labels,_ inloader:seq, labels= seq.to(device),labels.to(device)outputs =model(seq)loss= criterion(outputs, labels)total_loss +=loss.item() *seq.size(0)preds= outputs.argmax(dim=1)total_correct+= (preds== labels).sum().item()all_preds.extend(preds.cpu().numpy())all_labels.extend(labels.cpu().numpy())avg_loss =total_loss/ len(loader.dataset)accuracy =total_correct /len(loader.dataset)f1 =f1_score(all_labels,all_preds,average='macro')precision= precision_score(all_labels,all_preds,average='macro')recall= recall_score(all_labels,all_preds,average='macro')return accuracy, avg_loss,f1, precision, recall然后在训练函数中记录这些指标。7.修正引号问题:
用户代码中设备配置行的引号是中文的,需要改为英文引号:device =torch.device('cuda' iftorch.cuda.is_available() else'cpu')
否则会报语法错误。现在,将这些优化整合到用户的代码中,同时不删除原有代码,只是添加新的部分。例如,修改模型类,添加注意力层和多层LSTM;在优化器中加入weight_decay;添加学习率调度;在训练循环中加入梯度裁剪;修改评估函数以计算更多指标;添加数据增强选项;修正引号错误等。
最后,测试优化后的代码是否能正常运行,并解释每个优化的作用。可能遇到的问题:-注意力层的实现是否正确,是否改变了模型输出的维度。-数据增强是否合理,是否会影响原始数据。-学习率调度器是否正确使用,是否与早停机制配合良好。
-评估指标的计算是否正确,尤其是多分类的macro average。
需要确保所有修改不会导致代码运行错误,并验证优化后的模型效果是否有提升。</think>以下是基于原始代码的优化版本,添加了注意力机制、学习率调度、梯度裁剪等改进,同时保持原有代码结构不变:
```python
import pandas as pd
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
import re
import matplotlib.pyplot as plt
from tqdm import tqdm
from sklearn.metrics import f1_score, precision_score, recall_score # 新增评估指标
# 设备配置(修正引号)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}")
# 数据预处理(保持不变)
def load_and_preprocess_data():
comments = pd.read_csv(r'D:\BaiduNetdiskDownload\电影数据集-CSV格式\comments.csv')
if 'RATING' not in comments.columns:
raise KeyError("RATING column not found")
comments['CONTENT'] = comments['CONTENT'].fillna('').astype(str)
comments['CLEAN_CONTENT'] = comments['CONTENT'].apply(
lambda x: re.sub(r'[^\w\s]', '', x.lower())
)
comments['LABEL'] = comments['RATING'] - 1
valid_labels = comments['LABEL'].between(0, 4)
comments = comments[valid_labels].copy()
comments['LABEL'] = comments['LABEL'].astype(np.int32)
comments['WEIGHT'] = np.log1p(comments['VOTES']) + 1
return comments[['CLEAN_CONTENT', 'LABEL', 'WEIGHT']]
# 自定义数据集(增加数据增强)
class SentimentDataset(Dataset):
def __init__(self, sequences, labels, weights, augment=False):
self.sequences = torch.LongTensor(sequences)
self.labels = torch.LongTensor(labels)
self.weights = torch.FloatTensor(weights)
self.augment = augment # 新增数据增强标志
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
seq = self.sequences[idx]
# 简单数据增强:随机mask
if self.augment and np.random.rand() < 0.3:
mask = np.random.rand(len(seq)) < 0.1
seq[mask] = 0 # 用0(padding索引)随机mask部分词语
return seq, self.labels[idx], self.weights[idx]
# 改进的LSTM模型(添加注意力机制)
class BiLSTMSentiment(nn.Module):
def __init__(self, vocab_size, embedding_dim=128, hidden_dim=128, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers=num_layers,
bidirectional=True, batch_first=True, dropout=0.3 if num_layers>1 else 0)
self.attention = nn.Linear(hidden_dim*2, 1) # 新增注意力层
self.dropout = nn.Dropout(0.3)
self.fc = nn.Sequential(
nn.Linear(hidden_dim*2, 64),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(64, 5)
)
def forward(self, x):
x = self.embedding(x) # (batch, seq_len, embedding_dim)
lstm_out, (hn, cn) = self.lstm(x) # lstm_out: (batch, seq_len, hidden_dim*2)
# 注意力机制
attn_weights = torch.softmax(self.attention(lstm_out).squeeze(-1), dim=1) # (batch, seq_len)
context = torch.bmm(attn_weights.unsqueeze(1), lstm_out).squeeze(1) # (batch, hidden_dim*2)
return self.fc(self.dropout(context))
# 改进的训练函数(添加学习率调度和梯度裁剪)
def train_model(model, train_loader, val_loader, optimizer, epochs=10):
history = {
'train_loss': [], 'train_acc': [],
'val_loss': [], 'val_acc': [],
'val_f1': [], 'val_precision': [], 'val_recall': [] # 新增指标
}
criterion = nn.CrossEntropyLoss(reduction='none')
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='max', factor=0.5, patience=2, verbose=True) # 新增学习率调度
best_acc = 0.0
early_stopping_counter = 0
patience = 3
for epoch in range(epochs):
model.train()
epoch_loss = 0.0
epoch_correct = 0
total_samples = 0
progress_bar = tqdm(train_loader, desc=f'Epoch {epoch+1}/{epochs}', leave=False)
for seq, labels, weights in progress_bar:
seq, labels, weights = seq.to(device), labels.to(device), weights.to(device)
optimizer.zero_grad()
outputs = model(seq)
loss = (criterion(outputs, labels) * weights).mean()
loss.backward()
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
preds = outputs.argmax(dim=1)
correct = (preds == labels).sum().item()
epoch_correct += correct
epoch_loss += loss.item() * seq.size(0)
total_samples += seq.size(0)
progress_bar.set_postfix({
'loss': loss.item(),
'acc': f"{correct/seq.size(0):.2f}"
})
# 验证阶段
val_acc, val_loss, val_f1, val_precision, val_recall = evaluate(model, val_loader)
train_loss = epoch_loss / total_samples
train_acc = epoch_correct / total_samples
# 记录历史数据
history['train_loss'].append(train_loss)
history['train_acc'].append(train_acc)
history['val_loss'].append(val_loss)
history['val_acc'].append(val_acc)
history['val_f1'].append(val_f1)
history['val_precision'].append(val_precision)
history['val_recall'].append(val_recall)
# 学习率调度
scheduler.step(val_acc)
# 打印训练结果
print(f"\nEpoch {epoch+1} Summary:")
print(f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f}")
print(f"Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}")
print(f"Val F1: {val_f1:.4f} | Precision: {val_precision:.4f} | Recall: {val_recall:.4f}")
# 早停机制
if val_acc > best_acc:
best_acc = val_acc
early_stopping_counter = 0
torch.save(model.state_dict(), 'best_model.pth')
print(f"🚀 New best model saved with accuracy: {best_acc:.4f}")
else:
early_stopping_counter += 1
print(f"⏳ Early stopping counter: {early_stopping_counter}/{patience}")
if early_stopping_counter >= patience:
print(f"⛔ Early stopping triggered after {epoch+1} epochs!")
break
# 增强的可视化
plt.figure(figsize=(18, 6))
metrics = ['loss', 'acc', 'f1']
for i, metric in enumerate(metrics, 1):
plt.subplot(1, 3, i)
plt.plot(history[f'train_{metric}'], label='Train')
plt.plot(history[f'val_{metric}'], label='Validation')
plt.title(f'{metric.upper()} Curve')
plt.xlabel('Epoch')
plt.ylabel(metric.upper())
plt.legend()
plt.tight_layout()
plt.savefig('training_curves.png')
plt.close()
return model, history
# 改进的评估函数(添加多指标)
def evaluate(model, loader):
model.eval()
total_loss, total_correct = 0, 0
all_preds = []
all_labels = []
criterion = nn.CrossEntropyLoss()
with torch.no_grad():
for seq, labels, _ in loader:
seq, labels = seq.to(device), labels.to(device)
outputs = model(seq)
loss = criterion(outputs, labels)
total_loss += loss.item() * seq.size(0)
preds = outputs.argmax(dim=1)
total_correct += (preds == labels).sum().item()
all_preds.extend(preds.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
avg_loss = total_loss / len(loader.dataset)
accuracy = total_correct / len(loader.dataset)
f1 = f1_score(all_labels, all_preds, average='macro')
precision = precision_score(all_labels, all_preds, average='macro')
recall = recall_score(all_labels, all_preds, average='macro')
return accuracy, avg_loss, f1, precision, recall
if __name__ == "__main__":
# 数据准备流程(增加数据增强)
data = load_and_preprocess_data()
# 文本向量化
tokenizer = Tokenizer(num_words=50000, oov_token="<OOV>")
tokenizer.fit_on_texts(data['CLEAN_CONTENT'])
sequences = pad_sequences(
tokenizer.texts_to_sequences(data['CLEAN_CONTENT']),
maxlen=200, padding='post', truncating='post'
)
# 数据集划分
X_train, X_temp, y_train, y_temp, w_train, w_temp = train_test_split(
sequences, data['LABEL'].values, data['WEIGHT'].values,
test_size=0.3, random_state=42
)
X_val, X_test, y_val, y_test, w_val, w_test = train_test_split(
X_temp, y_temp, w_temp, test_size=0.5, random_state=42
)
# 数据加载器(启用训练集数据增强)
train_loader = DataLoader(
SentimentDataset(X_train, y_train, w_train, augment=True), # 启用数据增强
batch_size=512, shuffle=True
)
val_loader = DataLoader(
SentimentDataset(X_val, y_val, w_val),
batch_size=512
)
test_loader = DataLoader(
SentimentDataset(X_test, y_test, w_test),
batch_size=512
)
# 模型初始化(使用改进后的模型)
model = BiLSTMSentiment(
vocab_size=50000,
embedding_dim=256,
hidden_dim=256,
num_layers=2
).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4) # 添加权重衰减
# 训练流程
trained_model, history = train_model(
model, train_loader, val_loader, optimizer, epochs=15
)
# 最终测试
test_acc, test_loss, test_f1, test_precision, test_recall = evaluate(trained_model, test_loader)
print(f"\n🎯 Final Test Results:")
print(f"Test Accuracy: {test_acc:.4f}")
print(f"Test Loss: {test_loss:.4f}")
print(f"Test F1: {test_f1:.4f} | Precision: {test_precision:.4f} | Recall: {test_recall:.4f}")
# 模型保存
torch.save({
'model_state': trained_model.state_dict(),
'tokenizer_config': tokenizer.to_json(),
'history': history
}, 'sentiment_model.pth')
print("Model saved with training history")
```
**主要优化点解释:**
1. **注意力机制**:在LSTM输出后添加注意力层,使模型能关注重要词语
2. **多层LSTM**:使用2层LSTM增强特征提取能力
3. **数据增强**:在训练时随机mask部分词语(概率10%)
4. **学习率调度**:使用ReduceLROnPlateau根据验证准确率调整学习率
5. **梯度裁剪**:限制梯度最大范数为1.0,防止梯度爆炸
6. **权重衰减**:在优化器中添加L2正则化(1e-4)
7. **多指标评估**:新增F1、精确率、召回率评估指标
8. **增强可视化**:增加F1曲线的可视化
9. **模型容量提升**:增大embedding和hidden维度到256