import re
import mlflow
from sklearn.metrics import f1_score
import pandas as pd
import torch
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from torch import nn
from torch.utils.data import Dataset, DataLoader
from transformers import (
BertTokenizer,
BertForSequenceClassification,
AdamW,
get_linear_schedule_with_warmup,
)
# 初始化MLFlow
mlflow.set_experiment("Weibo Sentiment Analysis v2")
# 设置设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
class WeiboDataset(Dataset):
"""优化后的数据集类"""
def __init__(self, texts, labels, tokenizer, max_len=128):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
self.max_len = max_len
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
text = str(self.texts.iloc[idx])
label = self.labels.iloc[idx]
encoding = self.tokenizer.encode_plus(
text,
add_special_tokens=True,
max_length=self.max_len,
padding="max_length",
truncation=True,
return_attention_mask=True,
return_tensors="pt",
)
return {
"input_ids": encoding["input_ids"].flatten(),
"attention_mask": encoding["attention_mask"].flatten(),
"labels": torch.tensor(label, dtype=torch.long),
}
class DataProcessor:
"""数据预处理管道"""
@staticmethod
def clean_text(text):
"""与预测代码完全一致的清洗逻辑"""
if not isinstance(text, str):
return ""
text = re.sub(r"//@\S+[::]\s*", "", text)
text = re.sub(r"#\S+#", "", text)
text = re.sub(r"@\S+\s*", "", text)
text = re.sub(r"https?://\S+", "", text)
text = re.sub(r"//+", "", text)
return re.sub(r"\s+", " ", text).strip()
def process(self, file_path):
data_frame = pd.read_csv(
file_path,
usecols=["label", "text"],
dtype={"label": int, "text": str}
).dropna()
data_frame["cleaned_text"] = data_frame["text"].apply(self.clean_text)
sample_check = data_frame.sample(5, random_state=42)
print("\n清洗后样本示例:")
for idx, row in sample_check.iterrows():
print(f"原始: {row['text']}\n清洗: {row['cleaned_text']}\n")
return data_frame[data_frame["cleaned_text"].str.len() > 0]
class FGM:
"""对抗训练模块(调整epsilon)"""
def __init__(self, model):
self.model = model
self.backup = {}
def attack(self, epsilon=0.12):
for name, param in self.model.named_parameters():
if param.requires_grad and "embeddings" in name:
self.backup[name] = param.data.clone()
if param.grad is not None:
norm = torch.norm(param.grad)
if norm > 0:
r_at = epsilon * param.grad / norm
param.data.add_(r_at)
def restore(self):
for name, param in self.model.named_parameters():
if name in self.backup:
param.data = self.backup[name]
self.backup.clear()
class BertSentimentClassifier:
def __init__(self, num_labels=2):
self.tokenizer = BertTokenizer.from_pretrained("bert-base-chinese")
self.model = BertForSequenceClassification.from_pretrained(
"bert-base-chinese", num_labels=num_labels
).to(device)
self.criterion = nn.CrossEntropyLoss()
def create_data_loader(self, date_frame, batch_size=32):
dataset = WeiboDataset(
texts=date_frame["cleaned_text"],
labels=date_frame["label"],
tokenizer=self.tokenizer
)
return DataLoader(
dataset,
batch_size=batch_size,
num_workers=4,
pin_memory=True,
shuffle=True
)
def train(self, train_loader_g, val_loader_g, epochs=4):
optimizer = AdamW(self.model.parameters(), lr=2e-5, weight_decay=0.01)
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=100,
num_training_steps=len(train_loader_g) * epochs
)
fgm = FGM(self.model)
best_f1 = 0
with mlflow.start_run():
for epoch in range(epochs):
self.model.train()
total_loss = 0
for batch in train_loader_g:
optimizer.zero_grad()
inputs = {
"input_ids": batch["input_ids"].to(device),
"attention_mask": batch["attention_mask"].to(device),
"labels": batch["labels"].to(device),
}
outputs = self.model(**inputs)
loss = self.criterion(outputs.logits, inputs["labels"])
# 对抗训练
loss.backward()
fgm.attack() # epsilon=0.12
# 对抗样本前向
outputs_adv = self.model(**inputs)
loss_adv = self.criterion(outputs_adv.logits, inputs["labels"])
loss_adv.backward() # 累计梯度
fgm.restore()
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)
optimizer.step()
scheduler.step()
total_loss += loss.item()
# 验证
val_f1, val_report = self.evaluate(val_loader_g)
avg_loss = total_loss / len(train_loader_g)
# 记录指标
mlflow.log_metrics({
"train_loss": avg_loss,
"val_f1": val_f1
}, step=epoch)
print(f"Epoch {epoch+1}/{epochs}")
print(f"Train Loss: {avg_loss:.4f} | Val F1: {val_f1:.4f}")
print("Classification Report:")
print(val_report)
# 保存最佳模型
if val_f1 > best_f1:
best_f1 = val_f1
self.model.save_pretrained("best_model")
self.tokenizer.save_pretrained("best_model")
mlflow.log_artifacts("best_model", "model")
mlflow.pytorch.log_model(self.model, "final_model")
def evaluate(self, data_loader):
self.model.eval()
all_pred_result = []
all_labels = []
with torch.no_grad():
for batch in data_loader:
inputs = {
"input_ids": batch["input_ids"].to(device),
"attention_mask": batch["attention_mask"].to(device),
}
labels = batch["labels"].to(device)
outputs = self.model(**inputs)
pred_result = torch.argmax(outputs.logits, dim=1)
all_pred_result.extend(pred_result.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
report = classification_report(
all_labels, all_pred_result,
target_names=["Negative", "Positive"],
digits=4
)
f1 = f1_score(all_labels, all_pred_result, average="macro")
return f1, report
if __name__ == "__main__":
# 数据准备
processor = DataProcessor()
df = processor.process("weibo_senti_100k.csv")
# 拆分数据集
train_df, val_df = train_test_split(
df, test_size=0.2, stratify=df["label"], random_state=42
)
# 初始化模型
classifier = BertSentimentClassifier()
# 创建数据加载器
train_loader = classifier.create_data_loader(train_df, batch_size=32)
val_loader = classifier.create_data_loader(val_df, batch_size=64)
# 开始训练
classifier.train(train_loader, val_loader, epochs=4)
print("Training finished!")
import re
import numpy as np
import pandas as pd
import torch
from transformers import BertTokenizer, BertForSequenceClassification
class WeiboClassifier:
def __init__(self, model_path):
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.tokenizer = BertTokenizer.from_pretrained(model_path)
self.model = BertForSequenceClassification.from_pretrained(model_path).to(self.device)
self.model.eval()
@staticmethod
def clean_text(original_text):
"""与训练完全一致的清洗逻辑"""
if not isinstance(original_text, str):
return ""
original_text = re.sub(r"//@\S+[::]\s*", "", original_text)
original_text = re.sub(r"#\S+#", "", original_text)
original_text = re.sub(r"@\S+\s*", "", original_text)
original_text = re.sub(r"https?://\S+", "", original_text)
original_text = re.sub(r"//+", "", original_text)
original_text = re.sub(r"\s+", " ", original_text).strip()
print(f"清洗后文本: {original_text}")
return original_text
def predict(self, predict_text, return_prob=False):
processed_text = self.clean_text(predict_text)
if not processed_text:
return 0 if not return_prob else (0, [1.0, 0.0])
try:
inputs = self.tokenizer(
processed_text,
max_length=128,
padding="max_length",
truncation=True,
return_tensors="pt"
).to(self.device)
with torch.no_grad():
outputs = self.model(**inputs)
prob_result = torch.softmax(outputs.logits, dim=1).cpu().numpy()[0] # 取第一个样本的概率
pred_result = np.argmax(prob_result) # 取最大概率对应的类别
if return_prob:
return pred_result, prob_result.tolist()
return pred_result
except Exception as e:
print(f"预测错误: {str(e)}")
return 0 if not return_prob else (0, [1.0, 0.0])
if __name__ == "__main__":
# 示例测试
classifier = WeiboClassifier("best_model")
# 从CSV读取测试数据
test_df = pd.read_csv("test_comments.csv")
test_cases = [(row["comment"], row["label"]) for _, row in test_df.iterrows()]
total_cases = len(test_cases)
total_right = 0
total_error = 0
for text, expected in test_cases:
print("\n" + "=" * 50)
print(f"输入文本: {text}")
pred, probs = classifier.predict(text, return_prob=True)
print(f"预测概率: [Negative: {probs[0]:.4f}, Positive: {probs[1]:.4f}]")
print(f"预测结果: {pred} | 期望: {expected} | {'正确' if pred == expected else '错误'}")
if pred == expected:
total_right += 1
else:
total_error += 1
print("\n" + "=" * 50)
print(f"测试结果: 总用例数: {total_cases}, 正确率: {total_right / total_cases:.4f}, 错误率: {total_error / total_cases:.4f}")
以下是对项目的详细流程分析和整体功能总结:
%% 训练代码流程图
graph TD
A[开始] --> B[初始化MLFlow实验]
B --> C[设置计算设备]
C --> D[数据预处理]
D --> D1[读取CSV文件]
D1 --> D2[清洗文本]
D2 --> D3[过滤空文本]
D3 --> E[拆分训练集/验证集]
E --> F[初始化BERT分类器]
F --> G[创建数据加载器]
G --> H[开始训练循环]
H --> H1[对抗训练FGM]
H1 --> H2[前向传播]
H2 --> H3[计算损失]
H3 --> H4[反向传播]
H4 --> H5[对抗攻击]
H5 --> H6[对抗样本反向传播]
H6 --> H7[梯度裁剪]
H7 --> H8[优化器更新]
H8 --> I[验证评估]
I --> J[记录指标到MLFlow]
J --> K{是否最佳模型?}
K -->|是| L[保存最佳模型]
K -->|否| H
L --> M[循环结束]
M --> N[保存最终模型]
N --> O[训练结束]
%% 预测代码流程图
graph TD
P[开始] --> Q[加载训练好的模型]
Q --> R[读取测试数据]
R --> S[遍历测试用例]
S --> T[文本清洗]
T --> U[文本编码]
U --> V[模型预测]
V --> W[计算概率]
W --> X[结果比对]
X --> Y{是否正确?}
Y -->|是| Z1[正确计数]
Y -->|否| Z2[错误计数]
Z1 --> AA[统计准确率]
Z2 --> AA
AA --> AB[输出测试报告]
AB --> AC[结束]