NLP项目实战——AI文本情感分类
本文基于LSTM构建了一个中文评论文本情感二分类模型。使用ChineseNLPCorpus数据集,通过jieba分词构建词表,采用Embedding-LSTM-Linear架构,其中LSTM层提取序列特征,最后通过sigmoid函数输出情感倾向概率。模型训练使用BCEWithLogitsLoss损失函数和Adam优化器。项目包含完整的数据预处理、模型训练、评估和预测流程,采用模块化结构设计,各功能
·
一、需求说明
本案例的目标是基于 LSTM 构建一个文本情感分类模型,对评论内容进行二分类判断(正面或负面)
二、需求分析
数据集处理本案例的目标对用户评论文本进行情感分类,因此需使用带有情感标签(正面 / 负面)的评论数据集。
1. 数据集来源
数据集为 ChineseNLPCorpus,格式 CSV,可通过百度网盘下载:
通过网盘分享的文件
链接: https://pan.baidu.com/s/1uI0w38G5XEj5i9rrExMWPQ 提取码: y5fr
2. 模型结构设计
模型整体由以下三个主要部分组成:
(1) 嵌入层(Embedding)
将输入的词或字索引映射为稠密向量表示,便于后续神经网络处理。
(2) 长短期记忆网络(LSTM)
用于建模输入序列的上下文信息,输出最后一个时间步的隐藏状态作为上下文表示。
(3) 输出层(Linear)
将 LSTM 的隐藏状态输出映射为一个标量,表示该评论为正面情感的倾向得分(经 sigmod 函数后,大于 0.5 判定为正面情感,小于等于 0.5 判定为负面情感)。
3. 训练方案
损失函数:使用 BCEWithLogitsLoss,结合了 sigmoid 激活和二分类交叉熵计算,数值稳定且适合二分类任务。
优化器:使用 Adam 优化器进行参数更新,提升训练效率。
4. 评估方案模型训练完毕后,使用测试集统计正确率。
三、需求实现
1. 项目结构
review_analyze_lstm
├── data
│ ├── processed # 预处理后的数据
│ ├── raw # 原始数据
│ ├── logs # 训练日志
│ └── models # 保存训练好的模型参数
└── src
├── config.py # 超参数配置
├── dataset.py # 自定义Dataset
├── evaluate.py # 模型评估脚本
├── model.py # 模型结构定义
├── predict.py # 模型推理脚本
├── process.py # 数据处理脚本(上述代码)
├── tokenizer.py # 自定义分词器(上述代码)
└── train.py # 模型训练脚本
2. 完整代码:
(1) 数据预处理
# process.py
import pandas as pd
from sklearn.model_selection import train_test_split
from tokenizer import JiebaTokenizer
import config
def process():
"""
数据预处理主函数。
"""
print("开始处理数据")
# 1. 读取原始数据文件
df = pd.read_csv(
config.RAW_DATA_DIR / 'online_shopping_10_cats.csv',
usecols=['review', 'label'],
encoding='utf-8'
)
# 2. 数据清洗:去除空值和空字符串
df = df.dropna()
df = df[df['review'].str.strip().ne('')]
# 3. 划分训练集和测试集
train_df, test_df = train_test_split(df, test_size=0.2, random_state=42)
# 4. 构建词表并保存
jiebaTokenizer.build_vocab(
train_df['review'].tolist(),
config.PROCESSED_DATA_DIR / 'vocab.txt'
)
# 5. 加载词表
tokenizer = JiebaTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'vocab.txt')
# 6. 编码训练集并保存
train_df['review'] = train_df['review'].apply(
lambda x: tokenizer.encode(x, seq_len=config.SEQ_LEN)
)
train_df.to_json(
config.PROCESSED_DATA_DIR / 'indexed_train.jsonl',
orient='records',
lines=True
)
# 7. 编码测试集并保存
test_df['review'] = test_df['review'].apply(
lambda x: tokenizer.encode(x, seq_len=config.SEQ_LEN)
)
test_df.to_json(
config.PROCESSED_DATA_DIR / 'indexed_test.jsonl',
orient='records',
lines=True
)
print("数据处理完成")
if __name__ == '__main__':
process()
(2) 自定义分词器
# tokenizer
import jieba
from tqdm import tqdm
jieba.setLogLevel(jieba.logging.WARNING)
class JiebaTokenizer:
"""
基于 jieba 的分词器,用于分词、编码和词表管理。
"""
unk_token = '<unk>'
pad_token = '<pad>'
@staticmethod
def tokenize(sentence):
"""
对句子进行分词。
:param sentence: 输入句子。
:return: 分词后的 token 列表。
"""
return jieba.lcut(sentence)
@classmethod
def build_vocab(cls, sentences, vocab_file):
"""
构建词表并保存到文件。
:param sentences: 句子列表。
:param vocab_file: 保存词表的文件路径。
"""
unique_words = set()
for sentence in tqdm(sentences, desc='分词'):
for word in cls.tokenize(sentence):
unique_words.add(word)
vocab_list = [cls.pad_token, cls.unk_token] + list(unique_words)
with open(vocab_file, 'w', encoding='utf-8') as f:
for word in vocab_list:
f.write(word + '\n')
@classmethod
def from_vocab(cls, vocab_file):
"""
从文件加载词表。
:param vocab_file: 词表文件路径。
:return: JiebaTokenizer 实例。
"""
with open(vocab_file, 'r', encoding='utf-8') as f:
vocab_list = [line.strip() for line in f.readlines()]
return cls(vocab_list)
def __init__(self, vocab_list):
"""
初始化 tokenizer。
:param vocab_list: 词表列表。
"""
self.vocab_list = vocab_list
self.vocab_size = len(vocab_list)
self.word2index = {word: index for index, word in enumerate(vocab_list)}
self.index2word = {index: word for index, word in enumerate(vocab_list)}
self.unk_token_index = self.word2index[self.unk_token]
self.pad_token_index = self.word2index[self.pad_token]
def encode(self, sentence, seq_len):
"""
将句子编码为索引列表。
:param sentence: 输入句子。
:param seq_len: 序列长度。
:return: 索引列表。
"""
tokens = self.tokenize(sentence)
indexes = [self.word2index.get(token, self.unk_token_index) for token in tokens]
# 填充或截断
if len(indexes) >= seq_len:
return indexes[:seq_len]
else:
return indexes + [self.pad_token_index] * (seq_len -len(indexes))
(3) 自定义数据集
# dataset.py
import torch
from torch.utils.data import Dataset, DataLoader
import pandas as pd
import config
class ReviewAnalyzeDataset(Dataset):
"""
评论情感分析数据集。
"""
def __init__(self, file_path):
"""
初始化数据集。
:param file_path: 数据文件路径(JSONL 格式)。
"""
# 加载 JSONL 数据到内存
self.data = pd.read_json(file_path, lines=True).to_dict(orient='records')
def __len__(self):
"""
获取数据集样本数。
:return: 样本数量。
"""
return len(self.data)
def __getitem__(self, index):
"""
获取指定索引的样本。
:param index: 样本索引。
:return: (input_tensor, target_tensor)
"""
# 构建输入和目标张量
input_tensor = torch.tensor(self.data[index]['review'], dtype=torch.long)
target_tensor = torch.tensor(self.data[index]['label'], dtype=torch.float)
return input_tensor, target_tensor
def get_dataloader(train=True):
"""
创建数据加载器。
:param train: 是否加载训练集(True)或测试集(False)。
:return: DataLoader 实例。
"""
file_name = 'indexed_train.jsonl' if train else 'indexed_test.jsonl'
# 创建数据集实例
dataset = ReviewAnalyzeDataset(config.PROCESSED_DATA_DIR / file_name)
# 返回 DataLoader
return DataLoader(dataset, batch_size=config.BATCH_SIZE, shuffle=True)
if __name__ == '__main__':
# 简单测试数据加载器
dataloader = get_dataloader()
for input_tensor, target_tensor in dataloader:
print(input_tensor.shape, target_tensor.shape)
break
(4) 模型定义
# model.py
import torch
from torch import nn
import config
from torchinfo import summary
class ReviewAnalyzeModel(nn.Module):
"""
评论情感分析模型,基于 LSTM。
"""
def __init__(self, vocab_size, padding_idx):
"""
初始化模型。
:param vocab_size: 词表大小。
:param padding_idx: padding token 的索引。
"""
super().__init__()
# 嵌入层:将索引映射为词向量
self.embedding = nn.Embedding(
num_embeddings=vocab_size,
embedding_dim=config.EMBEDDING_DIM,
padding_idx=padding_idx
)
# LSTM 层:提取序列特征
self.lstm = nn.LSTM(
input_size=config.EMBEDDING_DIM,
hidden_size=config.HIDDEN_DIM,
batch_first=True
)
# 线性层:映射到单输出,用于二分类
self.linear = nn.Linear(in_features=config.HIDDEN_DIM, out_features=1)
def forward(self, x):
"""
前向传播。
:param x: 输入张量,形状 (batch_size, seq_len)。
:return: 模型输出张量,形状 (batch_size,)。
"""
# 嵌入层处理
embed = self.embedding(x) # (batch_size, seq_len, embedding_dim)
# LSTM 处理序列
output, _ = self.lstm(embed) # (batch_size, seq_len, hidden_dim)
# 取最后时间步隐藏状态用于分类
result = self.linear(output[:, -1, :]).squeeze(dim=1) # (batch_size,)
return result
if __name__ == '__main__':
model = ReviewAnalyzeModel(vocab_size=1000, padding_idx=0)
# 创建 dummy 输入张量用于结构展示
dummy_input = torch.randint(
low=0,
high=1000,
size=(config.BATCH_SIZE, config.SEQ_LEN),
dtype=torch.long
)
# 打印模型结构信息
summary(model, input_data=dummy_input)
(5) 模型训练
# train.py
def train_one_epoch(model, dataloader, loss_function, optimizer, device):
"""
训练一个 epoch。
:param model: 模型。
:param dataloader: 数据加载器。
:param loss_function: 损失函数。
:param optimizer: 优化器。
:param device: 设备。
:return: 平均损失。
"""
total_loss = 0
model.train()
for inputs, targets in tqdm(dataloader, desc='训练'):
# 移动数据到设备
inputs, targets = inputs.to(device), targets.to(device)
optimizer.zero_grad()
# 前向传播
outputs = model(inputs)
# 计算损失
loss = loss_function(outputs, targets)
# 反向传播
loss.backward()
# 参数更新
optimizer.step()
total_loss += loss.item()
return total_loss / len(dataloader)
def train():
"""
模型训练主函数。
"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
dataloader = get_dataloader()
tokenizer = JiebaTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'vocab.txt')
model = ReviewAnalyzeModel(
vocab_size=tokenizer.vocab_size,
padding_idx=tokenizer.pad_token_index
).to(device)
loss_function = torch.nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=config.LEARNING_RATE)
writer = SummaryWriter(log_dir=config.LOG_DIR / time.strftime('%Y-%m-%d_%H-%M-%S'))
best_loss = float('inf')
for epoch in range(1, config.EPOCHS + 1):
print(f'========== Epoch: {epoch} ==========')
avg_loss = train_one_epoch(model, dataloader, loss_function, optimizer, device)
print(f'loss: {avg_loss:.4f}')
writer.add_scalar('Loss/Train', avg_loss, epoch)
if avg_loss < best_loss:
best_loss = avg_loss
torch.save(model.state_dict(), config.MODELS_DIR / 'model.pt')
print('模型保存成功')
if __name__ == '__main__':
train()
(6) 模型预测
# predict.py
def predict_batch(input_tensor, model):
"""
对一个 batch 的输入进行预测。
:param input_tensor: 输入张量,形状 (batch_size, seq_len)。
:param model: 模型。
:return: 概率列表。
"""
model.eval()
with torch.no_grad():
output = model(input_tensor)
probs = torch.sigmoid(output)
return probs.tolist()
def predict(user_input, model, tokenizer, device):
"""
对单条用户输入进行预测。
:param user_input: 用户输入文本。
:param model: 模型。
:param tokenizer: 分词器。
:param device: 设备。
:return: 概率值。
"""
input_ids = tokenizer.encode(user_input, config.SEQ_LEN)
input_tensor = torch.tensor([input_ids], dtype=torch.long).to(device)
probs = predict_batch(input_tensor, model)
prob = probs[0]
return prob
def run_predict():
"""
启动预测交互程序。
"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
tokenizer = JiebaTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'vocab.txt')
model = ReviewAnalyzeModel(
vocab_size=tokenizer.vocab_size,
padding_idx=tokenizer.pad_token_index
).to(device)
model.load_state_dict(torch.load(config.MODELS_DIR / 'model.pt'))
print('请输入要预测的评论:(输入 q 或 quit 退出)')
while True:
user_input = input('> ')
if user_input in ['q', 'quit']:
print('退出程序')
break
if not user_input:
print('输入为空,请重新输入')
continue
prob = predict(user_input, model, tokenizer, device)
if prob > 0.5:
print(f'正面评价(置信度:{prob:.2f})')
else:
print(f'负面评价(置信度:{1 - prob:.2f})')
if __name__ == '__main__':
run_predict()
(7) 模型评估
# evaluate.py
import config
import torch
from model import ReviewAnalyzeModel
from dataset import get_dataloader
from predict import predict_batch
from tokenizer import JiebaTokenizer
def evaluate(model, dataloader, device):
"""
模型评估。
:param model: 模型。
:param dataloader: 数据加载器。
:param device: 设备。
:return: 准确率。
"""
total_count = 0
correct_count = 0
model.eval()
for inputs, targets in dataloader:
inputs = inputs.to(device)
targets = targets.tolist()
probs = predict_batch(inputs, model)
for prob, target in zip(probs, targets):
pred_label = 1 if prob > 0.5 else 0
if pred_label == target:
correct_count += 1
total_count += 1
return correct_count / total_count
def run_evaluate():
"""
运行评估流程。
"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
tokenizer = JiebaTokenizer.from_vocab(config.PROCESSED_DATA_DIR / 'vocab.txt')
model = ReviewAnalyzeModel(
vocab_size=tokenizer.vocab_size,
padding_idx=tokenizer.pad_token_index
).to(device)
model.load_state_dict(torch.load(config.MODELS_DIR / 'model.pt'))
dataloader = get_dataloader(train=False)
acc = evaluate(model, dataloader, device)
print("========== 评估结果 ==========")
print(f"准确率:{acc:.4f}")
print("=============================")
if __name__ == '__main__':
run_evaluate()
(8) 配置文件
# config.py
from pathlib import Path
# 项目根目录
ROOT_DIR = Path(__file__).parent.parent
# 数据路径
RAW_DATA_DIR = ROOT_DIR / 'data' / 'raw'
PROCESSED_DATA_DIR = ROOT_DIR / 'data' / 'processed'
# 模型与日志路径
MODELS_DIR = ROOT_DIR / 'models'
LOG_DIR = ROOT_DIR / 'logs'
# 训练参数
SEQ_LEN = 128 # 输入序列长度
BATCH_SIZE = 64 # 批大小
EMBEDDING_DIM = 64 # 嵌入层维度
HIDDEN_DIM = 128 # LSTM 隐藏层维度
LEARNING_RATE = 1e-3 # 学习率
EPOCHS = 30 # 训练轮数更多推荐



所有评论(0)