从SnowNLP到BERT:实战构建高精度微博舆情分析系统(PyTorch2+Flask+ECharts)

在当今信息爆炸的时代,社交媒体平台如微博已成为公众情绪和舆论的重要载体。传统的舆情分析方法,如基于规则或简单统计模型(如SnowNLP),往往在复杂语境和网络新词面前显得力不从心,准确率难以保证。本文将深入探讨如何利用预训练大模型BERT,通过微调(Fine-tuning)技术,构建一个高精度、可视化的微博舆情分析系统,实现从数据采集、模型训练到结果展示的全流程实战。

一、 系统架构升级:为何选择BERT进行微调?

我们之前的系统基于SnowNLP进行情感分析,虽然简单易用,但其模型相对简单,对网络用语、反讽等复杂语义的理解能力有限。为了显著提升分析准确率,我们决定引入BERT(Bidirectional Encoder Representations from Transformers)。BERT通过在海量语料上进行预训练,掌握了深层的语言表征能力。我们只需要在其基础上,使用特定的微博舆情数据集进行增量微调,即可让模型快速适应下游的二分类任务(如正面/负面情感判断)。这比从零训练一个模型效率高得多,效果也更好。整个系统技术栈包括PyTorch2(深度学习框架)、Flask(Web后端)、Pandas(数据处理)和ECharts(数据可视化)。

二、 核心实战:BERT微调全流程详解

微调BERT的核心在于准备适配的数据、定义下游任务模型并进行高效训练。下面我们分步拆解。

1. 构建自定义数据集

首先,我们需要准备微博评论或文章数据,并标注情感标签(如0代表负面,1代表正面)。我们使用了约8000条训练数据和2000条测试数据。关键在于将原始数据转换为模型所需的格式,这里我们使用Hugging Face的`datasets`库来简化流程。

pip install datasets -i http://mirrors.aliyun.com/pypi/simple/   --trusted-host mirrors.aliyun.com

数据准备完毕后,我们需要定义一个继承自`torch.utils.data.Dataset`的类,负责加载和格式化数据,这是连接原始数据与模型训练的关键桥梁。自定义数据集的参考代码如下:

from datasets import load_dataset
from torch.utils.data import Dataset
# 自定义数据集
class MyDataset(Dataset):
    def __init__(self, split):
        # 加载训练集
        train_dataset = load_dataset(path="csv", data_files="weibo_senti_8k_train.csv")
        # 加载测试集
        test_dataset = load_dataset(path="csv", data_files="weibo_senti_2k_test.csv")
        if split == 'train':
            self.data = train_dataset['train']
        elif split == 'test':
            self.data = test_dataset['train']
    # 获取数据集大小
    def __len__(self):
        return len(self.data)
    # 获取数据集的某个元素
    def __getitem__(self, index):
        sentence = self.data[index]['sentence']
        label = self.data[index]['label']
        return sentence, label
if __name__ == '__main__':
    train_dataset = MyDataset('train')
    test_dataset = MyDataset('test')
    print(train_dataset[0])
    print(test_dataset[0])
    for data in test_dataset:
        print(data)

运行自定义数据集代码,可以查看数据加载的格式和样本,确保数据管道畅通。

2. 文本编码与分词处理

BERT模型不能直接处理文本字符串,需要先通过其对应的分词器(Tokenizer)将文本转换为数字ID(Token IDs)和注意力掩码(Attention Mask)。我们使用`batch_encode_plus`方法进行批量编码,提高处理效率。

from transformers import BertTokenizer
import config
# 加载分词器
tokenizer = BertTokenizer.from_pretrained(config.model_path + '/Bert-base-chinese')
# 准备测试文本
sents = ['床前明月光,疑似地上霜,举头望明月,低头思故乡', '今天天气不错', '很开心']
# 批量编码句子
out = tokenizer.batch_encode_plus(
    batch_text_or_text_pairs=sents,  # 输入的文本
    add_special_tokens=True,  # 添加特殊标记
    max_length=10,  # 最大长度
    padding='max_length',  # 填充
    truncation=True,  # 截断
    return_tensors='pt',  # 返回pytorch张量
    return_token_type_ids=True,  # 返回token_type_ids  区分不同句子或段落的类型标识
    return_attention_mask=True,  # 返回attention_mask  标记有效token位置的掩码
    return_special_tokens_mask=True  # 返回special_tokens_mask 标识特殊token(如[CLS]、[SEP])的位置掩码
)
print(out)
for k, v in out.items():
    print(k, v)
# 解码文本数据
for i in range(len(sents)):
    print(sents[i] + "--解码后:", tokenizer.decode(out['input_ids'][i]))

编码后的输出包含了模型所需的所有输入信息。

{'input_ids': tensor([[ 101, 2414, 1184, 3209, 3299, 1045, 8024, 4542,  849,  102],
        [ 101,  791, 1921, 1921, 3698,  679, 7231,  102,    0,    0],
        [ 101, 2523, 2458, 2552,  102,    0,    0,    0,    0,    0]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'special_tokens_mask': tensor([[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 1, 1, 1],
        [1, 0, 0, 0, 1, 1, 1, 1, 1, 1]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
        [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]])}
input_ids tensor([[ 101, 2414, 1184, 3209, 3299, 1045, 8024, 4542,  849,  102],
        [ 101,  791, 1921, 1921, 3698,  679, 7231,  102,    0,    0],
        [ 101, 2523, 2458, 2552,  102,    0,    0,    0,    0,    0]])
token_type_ids tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
special_tokens_mask tensor([[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 1, 1, 1],
        [1, 0, 0, 0, 1, 1, 1, 1, 1, 1]])
attention_mask tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
        [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]])
床前明月光,疑似地上霜,举头望明月,低头思故乡--编码后: [CLS] 床 前 明 月 光 , 疑 似 [SEP]
今天天气不错--编码后: [CLS] 今 天 天 气 不 错 [SEP] [PAD] [PAD]
很开心--解码后: [CLS] 很 开 心 [SEP] [PAD] [PAD] [PAD] [PAD] [PAD]

3. 定义下游任务模型(增量模型)

微调BERT通常采用“预训练模型 + 自定义分类头”的结构。我们冻结BERT主干网络的参数(避免破坏其预训练获得的知识),仅在其输出的基础上添加一个全连接层(Linear Layer)来完成二分类。在前向传播时,我们提取BERT输出的[CLS] token的隐藏状态作为整个句子的表征。

# 使用设备(GPU/CPU)
import torch
from transformers import BertModel
import config
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
# 加载预训练模型
pretrained_model = BertModel.from_pretrained(config.model_path + '/Bert-base-chinese').to(device)
# 定义下游任务(增量模型)
class DownStreamModel(torch.nn.Module):
    def __init__(self):
        super(DownStreamModel, self).__init__()
        # 下游加一个全连接层,实现二分类任务
        self.fc = torch.nn.Linear(768, 2)
    def forward(self, input_ids, attention_mask, token_type_ids):
        with torch.no_grad():  # 冻结Bert模型参数,不需计算梯度
            # 获取最后一层隐藏层输出
            output = pretrained_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        # 增量模型参与训练
        out = self.fc(output.last_hidden_state[:, 0, :])
        return out

这里,output.last_hidden_state[:, 0, :] 是一个关键操作。它用于提取所有输入样本的 output token 的隐藏状态向量。详细解析如下:

  • output 是模型前向传播返回的对象,其 last_hidden_state 属性形状为 last_hidden_state
  • 切片操作 last_hidden_state 中,(batch_size, sequence_length, hidden_size) 选择所有样本,[:, 0, :] 选择序列的第一个位置(即 last_hidden_state token),batch_size 选择所有特征维度。
  • 因此,: 提取的就是每个句子的聚合语义表示,非常适合用于句子级分类任务。

4. 模型训练与评估

定义好数据管道和模型结构后,就可以开始微调训练了。我们使用交叉熵损失函数和Adam优化器,在训练集上进行多轮迭代,并在验证集上监控性能。

import torch
from datasets import load_dataset
from torch.utils.data import Dataset, DataLoader
from transformers import BertTokenizer, BertModel
import config
# 自定义数据集
class MyDataset(Dataset):
    def __init__(self, split):
        # 加载训练集
        train_dataset = load_dataset(path="csv", data_files="weibo_senti_8k_train.csv")
        # 加载测试集
        test_dataset = load_dataset(path="csv", data_files="weibo_senti_2k_test.csv")
        if split == 'train':
            self.data = train_dataset['train']
        elif split == 'test':
            self.data = test_dataset['train']
    # 获取数据集大小
    def __len__(self):
        return len(self.data)
    # 获取数据集的某个元素
    def __getitem__(self, index):
        sentence = self.data[index]['sentence']
        label = self.data[index]['label']
        return sentence, label
# 加载分词器
tokenizer = BertTokenizer.from_pretrained(config.model_path + '/Bert-base-chinese')
# 使用设备(GPU/CPU)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
# 加载预训练模型
pretrained_model = BertModel.from_pretrained(config.model_path + '/Bert-base-chinese').to(device)
# 定义下游任务(增量模型)
class DownStreamModel(torch.nn.Module):
    def __init__(self):
        super(DownStreamModel, self).__init__()
        # 下游加一个全连接层,实现二分类任务
        self.fc = torch.nn.Linear(768, 2)
    def forward(self, input_ids, attention_mask, token_type_ids):
        with torch.no_grad():  # 冻结Bert模型参数,不需计算梯度
            # 获取最后一层隐藏层输出
            output = pretrained_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        # 增量模型参与训练
        out = self.fc(output.last_hidden_state[:, 0, :])
        return out
# 对传入数据进行编码
def collate_fn(data):
    sents = [i[0] for i in data]
    labels = [i[1] for i in data]
    # 编码
    # 批量编码句子
    out = tokenizer.batch_encode_plus(
        batch_text_or_text_pairs=sents,  # 输入的文本
        add_special_tokens=True,  # 添加特殊标记
        max_length=80,  # 最大长度
        padding='max_length',  # 填充
        truncation=True,  # 截断
        return_tensors='pt',  # 返回pytorch张量
        return_token_type_ids=True,  # 返回token_type_ids  区分不同句子或段落的类型标识
        return_attention_mask=True,  # 返回attention_mask  标记有效token位置的掩码
        return_special_tokens_mask=True  # 返回special_tokens_mask 标识特殊token(如[CLS]、[SEP])的位置掩码
    )
    return out['input_ids'], out['attention_mask'], out['token_type_ids'], torch.tensor(labels)
# 创建数据集
train_dataset = MyDataset('train')  # 训练集
train_loader = DataLoader(
    dataset=train_dataset,  # 数据集
    batch_size=200,  # 批次大小
    shuffle=True,  # 是否打乱数据
    drop_last=True,  # 丢弃最后一个批次数据
    collate_fn=collate_fn  # 对加载的数据进行编码
)
test_dataset = MyDataset('test')  # 测试集
test_loader = DataLoader(
    dataset=test_dataset,  # 数据集
    batch_size=200,  # 批次大小
    shuffle=True,  # 是否打乱数据
    drop_last=True,  # 丢弃最后一个批次数据
    collate_fn=collate_fn  # 对加载的数据进行编码
)
if __name__ == '__main__':
    model = DownStreamModel().to(device)  # 创建模型
    optimizer = torch.optim.AdamW(model.parameters())  # 优化器
    criterion = torch.nn.CrossEntropyLoss()  # 定义损失函数
    best_val_acc = 0  # 保存最好的准确率
    EPOCH = 3  # 训练轮数
    for epoch in range(EPOCH):  # 训练轮数
        for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(train_loader):  # 批次数据
            out = model(input_ids=input_ids.to(device), attention_mask=attention_mask.to(device),
                        token_type_ids=token_type_ids.to(device))  # 模型输出
            loss = criterion(out, labels.to(device))  # 计算损失
            optimizer.zero_grad()  # 清空梯度
            loss.backward()  # 反向传播
            optimizer.step()  # 优化器更新参数
            # 每隔5个批次输出训练结果
            if i % 5 == 0:
                out = out.argmax(dim=1)  # 获取预测结果
                acc = (out == labels.to(device)).sum().item() / len(labels)  # 计算准确率
                print("EPOCH:{}--第{}批次--损失:{}--准确率:{}".format(epoch + 1, i + 1, loss.item(), acc))

训练过程会输出损失和准确率的变化。

cuda
EPOCH:1--第1批次--损失:0.5008983016014099--准确率:0.78
EPOCH:1--第6批次--损失:0.5052810907363892--准确率:0.75
EPOCH:1--第11批次--损失:0.4850313067436218--准确率:0.755
EPOCH:1--第16批次--损失:0.4301462471485138--准确率:0.83
EPOCH:1--第21批次--损失:0.39388778805732727--准确率:0.85
EPOCH:1--第26批次--损失:0.3695535361766815--准确率:0.855
EPOCH:1--第31批次--损失:0.35825812816619873--准确率:0.855
EPOCH:1--第36批次--损失:0.3288692533969879--准确率:0.875
EPOCH:2--第1批次--损失:0.31738394498825073--准确率:0.885
EPOCH:2--第6批次--损失:0.3121739625930786--准确率:0.87
EPOCH:2--第11批次--损失:0.30510687828063965--准确率:0.895
EPOCH:2--第16批次--损失:0.305753618478775--准确率:0.865
EPOCH:2--第21批次--损失:0.24456100165843964--准确率:0.92
EPOCH:2--第26批次--损失:0.2233615517616272--准确率:0.93
EPOCH:2--第31批次--损失:0.2816208302974701--准确率:0.89
EPOCH:2--第36批次--损失:0.24931633472442627--准确率:0.915
EPOCH:3--第1批次--损失:0.3053100109100342--准确率:0.885
EPOCH:3--第6批次--损失:0.2515011727809906--准确率:0.9
EPOCH:3--第11批次--损失:0.24241474270820618--准确率:0.915
EPOCH:3--第16批次--损失:0.2211739420890808--准确率:0.925
EPOCH:3--第21批次--损失:0.24276195466518402--准确率:0.91
EPOCH:3--第26批次--损失:0.27010777592658997--准确率:0.895
EPOCH:3--第31批次--损失:0.24304606020450592--准确率:0.9
EPOCH:3--第36批次--损失:0.26476508378982544--准确率:0.905

训练完成后,需要在独立的测试集上评估模型的最终性能,并保存效果最好的模型参数。

import torch
from datasets import load_dataset
from torch.utils.data import Dataset, DataLoader
from transformers import BertTokenizer, BertModel
import config
# 自定义数据集
class MyDataset(Dataset):
    def __init__(self, split):
        # 加载训练集
        train_dataset = load_dataset(path="csv", data_files="weibo_senti_8k_train.csv")
        # 加载测试集
        test_dataset = load_dataset(path="csv", data_files="weibo_senti_2k_test.csv")
        if split == 'train':
            self.data = train_dataset['train']
        elif split == 'test':
            self.data = test_dataset['train']
    # 获取数据集大小
    def __len__(self):
        return len(self.data)
    # 获取数据集的某个元素
    def __getitem__(self, index):
        sentence = self.data[index]['sentence']
        label = self.data[index]['label']
        return sentence, label
# 加载分词器
tokenizer = BertTokenizer.from_pretrained(config.model_path + '/Bert-base-chinese')
# 使用设备(GPU/CPU)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
# 加载预训练模型
pretrained_model = BertModel.from_pretrained(config.model_path + '/Bert-base-chinese').to(device)
# 定义下游任务(增量模型)
class DownStreamModel(torch.nn.Module):
    def __init__(self):
        super(DownStreamModel, self).__init__()
        # 下游加一个全连接层,实现二分类任务
        self.fc = torch.nn.Linear(768, 2)
    def forward(self, input_ids, attention_mask, token_type_ids):
        with torch.no_grad():  # 冻结Bert模型参数,不需计算梯度
            # 获取最后一层隐藏层输出
            output = pretrained_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        # 增量模型参与训练
        out = self.fc(output.last_hidden_state[:, 0, :])
        return out
# 对传入数据进行编码
def collate_fn(data):
    sents = [i[0] for i in data]
    labels = [i[1] for i in data]
    # 编码
    # 批量编码句子
    out = tokenizer.batch_encode_plus(
        batch_text_or_text_pairs=sents,  # 输入的文本
        add_special_tokens=True,  # 添加特殊标记
        max_length=80,  # 最大长度
        padding='max_length',  # 填充
        truncation=True,  # 截断
        return_tensors='pt',  # 返回pytorch张量
        return_token_type_ids=True,  # 返回token_type_ids  区分不同句子或段落的类型标识
        return_attention_mask=True,  # 返回attention_mask  标记有效token位置的掩码
        return_special_tokens_mask=True  # 返回special_tokens_mask 标识特殊token(如[CLS]、[SEP])的位置掩码
    )
    return out['input_ids'], out['attention_mask'], out['token_type_ids'], torch.tensor(labels)
# 创建数据集
train_dataset = MyDataset('train')  # 训练集
train_loader = DataLoader(
    dataset=train_dataset,  # 数据集
    batch_size=200,  # 批次大小
    shuffle=True,  # 是否打乱数据
    drop_last=True,  # 丢弃最后一个批次数据
    collate_fn=collate_fn  # 对加载的数据进行编码
)
test_dataset = MyDataset('test')  # 测试集
test_loader = DataLoader(
    dataset=test_dataset,  # 数据集
    batch_size=200,  # 批次大小
    shuffle=True,  # 是否打乱数据
    drop_last=True,  # 丢弃最后一个批次数据
    collate_fn=collate_fn  # 对加载的数据进行编码
)
if __name__ == '__main__':
    model = DownStreamModel().to(device)  # 创建模型
    optimizer = torch.optim.AdamW(model.parameters())  # 优化器
    criterion = torch.nn.CrossEntropyLoss()  # 定义损失函数
    best_val_acc = 0  # 保存最好的准确率
    EPOCH = 3  # 训练轮数
    for epoch in range(EPOCH):  # 训练轮数
        for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(train_loader):  # 批次数据
            out = model(input_ids=input_ids.to(device), attention_mask=attention_mask.to(device),
                        token_type_ids=token_type_ids.to(device))  # 模型输出
            loss = criterion(out, labels.to(device))  # 计算损失
            optimizer.zero_grad()  # 清空梯度
            loss.backward()  # 反向传播
            optimizer.step()  # 优化器更新参数
            # 每隔5个批次输出训练结果
            if i % 5 == 0:
                out = out.argmax(dim=1)  # 获取预测结果
                acc = (out == labels.to(device)).sum().item() / len(labels)  # 计算准确率
                print("EPOCH:{}--第{}批次--损失:{}--准确率:{}".format(epoch + 1, i + 1, loss.item(), acc))
    # 验证模型
    model.eval()  # 评估模式 Dropout关闭
    with torch.no_grad():  # 评估模式,不需要计算梯度
        for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(test_loader):
            out = model(input_ids=input_ids.to(device), attention_mask=attention_mask.to(device),
                        token_type_ids=token_type_ids.to(device))
            out = out.argmax(dim=1)
            acc = (out == labels.to(device)).sum().item() / len(labels)
            if acc > best_val_acc:
                best_val_acc = acc
                torch.save(model.state_dict(), "best_model.pth")
                print(f"测试集准确率EPOCH:{epoch}-第{i}批次:模型保存,准确率:{acc}")
        # 保存最后一轮模型
        torch.save(model.state_dict(), "last_model.pth")
        print(f"最后一轮模型保存,:准确率:{acc}")

评估结果展示了模型在未见数据上的表现。

cuda
EPOCH:1--第1批次--损失:0.6369971632957458--准确率:0.695
EPOCH:1--第6批次--损失:0.6052521467208862--准确率:0.685
EPOCH:1--第11批次--损失:0.5286625623703003--准确率:0.77
EPOCH:1--第16批次--损失:0.4174182415008545--准确率:0.83
EPOCH:1--第21批次--损失:0.41344115138053894--准确率:0.81
EPOCH:1--第26批次--损失:0.3910037875175476--准确率:0.87
EPOCH:1--第31批次--损失:0.3680213987827301--准确率:0.85
EPOCH:1--第36批次--损失:0.3817676901817322--准确率:0.855
EPOCH:2--第1批次--损失:0.3963841199874878--准确率:0.84
EPOCH:2--第6批次--损失:0.3516421914100647--准确率:0.85
EPOCH:2--第11批次--损失:0.330654114484787--准确率:0.845
EPOCH:2--第16批次--损失:0.3345922529697418--准确率:0.865
EPOCH:2--第21批次--损失:0.3087370693683624--准确率:0.885
EPOCH:2--第26批次--损失:0.25769323110580444--准确率:0.92
EPOCH:2--第31批次--损失:0.2792946696281433--准确率:0.885
EPOCH:2--第36批次--损失:0.29899129271507263--准确率:0.905
EPOCH:3--第1批次--损失:0.3139827847480774--准确率:0.855
EPOCH:3--第6批次--损失:0.27809959650039673--准确率:0.895
EPOCH:3--第11批次--损失:0.2725857198238373--准确率:0.885
EPOCH:3--第16批次--损失:0.30161210894584656--准确率:0.885
EPOCH:3--第21批次--损失:0.26055067777633667--准确率:0.925
EPOCH:3--第26批次--损失:0.22951321303844452--准确率:0.895
EPOCH:3--第31批次--损失:0.2995443642139435--准确率:0.87
EPOCH:3--第36批次--损失:0.3246515691280365--准确率:0.87
测试集准确率EPOCH:2-第0批次:模型保存,准确率:0.975
最后一轮模型保存,:准确率:0.955

[AFFILIATE_SLOT_1]

三、 工程化集成:将模型封装为应用服务

训练好的模型需要集成到Flask Web应用中。我们创建一个服务模块(如`weibo_train.py`),封装模型加载和预测接口。

import torch
from transformers import BertTokenizer, BertModel
import config
# 加载分词器
tokenizer = BertTokenizer.from_pretrained(config.model_path + '/Bert-base-chinese')
# 使用设备(GPU/CPU)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
# 加载预训练模型
pretrained_model = BertModel.from_pretrained(config.model_path + '/Bert-base-chinese').to(device)
# 定义下游任务(增量模型)
class DownStreamModel(torch.nn.Module):
    def __init__(self):
        super(DownStreamModel, self).__init__()
        # 下游加一个全连接层,实现二分类任务
        self.fc = torch.nn.Linear(768, 2)
    def forward(self, input_ids, attention_mask, token_type_ids):
        with torch.no_grad():  # 冻结Bert模型参数,不需计算梯度
            # 获取最后一层隐藏层输出
            output = pretrained_model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        # 增量模型参与训练
        out = self.fc(output.last_hidden_state[:, 0, :])
        return out
# 对传入数据进行编码
def collate_fn(data):
    # 编码
    # 批量编码句子
    out = tokenizer.batch_encode_plus(
        batch_text_or_text_pairs=data,  # 输入的文本
        add_special_tokens=True,  # 添加特殊标记
        max_length=80,  # 最大长度
        padding='max_length',  # 填充
        truncation=True,  # 截断
        return_tensors='pt',  # 返回pytorch张量
        return_token_type_ids=True,  # 返回token_type_ids  区分不同句子或段落的类型标识
        return_attention_mask=True,  # 返回attention_mask  标记有效token位置的掩码
        return_special_tokens_mask=True  # 返回special_tokens_mask 标识特殊token(如[CLS]、[SEP])的位置掩码
    )
    return out['input_ids'], out['attention_mask'], out['token_type_ids']
model = DownStreamModel().to(device)  # 创建模型
model.load_state_dict(torch.load(config.model_path + "/best_model.pth"))
def check_data(data):
    # 验证模型
    model.eval()  # 评估模式 Dropout关闭
    with torch.no_grad():  # 评估模式,不需要计算梯度
        input_ids, attention_mask, token_type_ids = collate_fn(data)
        out = model(input_ids=input_ids.to(device), attention_mask=attention_mask.to(device),
                    token_type_ids=token_type_ids.to(device))
        output = out.argmax(dim=1)
        sentiment = output.item()
        sentiment_label = "正面" if sentiment == 1 else "负面"
        return sentiment_label
if __name__ == '__main__':
    result = check_data(["我非常难过"])
    print(result)

封装后,可以方便地进行单条文本的情感预测测试。

1. 微博热词情感分析升级

找到原系统中基于SnowNLP的热词分析视图逻辑,替换为调用我们微调好的BERT模型接口。

原视图逻辑代码位置:

修改后的核心调用代码:

# 改成使用大模型微调进行舆情分析
sentiments = check_data([defaultHotWord])

重启系统测试,分析准确率相比之前有显著提升。

2. 微博文章内容情感分析升级

同样地,对完整的微博文章内容分析模块进行升级。

找到对应的后端业务逻辑代码:

修改为调用BERT模型:

# 改成使用大模型微调进行舆情分析
sentiments = check_data([article[1]])

升级后,对长文本的情感判断更加精准。

3. 可视化分析模块升级

舆情分析系统通常包含丰富的图表展示,如柱状图、饼图、树形图等。这些图表的数据来源也需要更新为BERT模型的分析结果。

找到可视化相关的后端视图层代码:

将所有数据生成逻辑中的情感分析调用替换为BERT模型接口:

@pb.route('sentimentAnalysis')
def sentimentAnalysis():
    """
    舆情数据分析
    :return:
    """
    xHotBarData = ['正面', '负面']
    yHotBarData = [0, 0]
    # 只读取前100条
    df = pd.read_csv('./fenci/comment_fre.csv', nrows=100)
    for value in df.values:
        # 情感分析
        # stc = SnowNLP(value[0]).sentiments
        # if stc > 0.6:
        #     yHotBarData[0] += 1
        # elif stc < 0.2:
        #     yHotBarData[2] += 1
        # else:
        #     yHotBarData[1] += 1
        # 使用大模型进行情感分析
        # sentiment_label = data_classfication(value[0])
        # 使用大模型微调进行情感分析
        sentiment_label = check_data([value[0]])
        if sentiment_label == '正面':
            yHotBarData[0] += 1
        else:
            yHotBarData[1] += 1
    hotTreeMapData = [{
        'name': xHotBarData[0],
        'value': yHotBarData[0]
    }, {
        'name': xHotBarData[1],
        'value': yHotBarData[1]
    }]
    commentPieData = [{
        'name': '正面',
        'value': 0
    }, {
        'name': '负面',
        'value': 0
    }]
    articlePieData = [{
        'name': '正面',
        'value': 0
    }, {
        'name': '负面',
        'value': 0
    }]
    commentList = commentDao.getAllComment()
    for comment in commentList:
        # 情感分析
        # stc = SnowNLP(comment[1]).sentiments
        # if stc > 0.6:
        #     commentPieData[0]['value'] += 1
        # elif stc < 0.2:
        #     commentPieData[2]['value'] += 1
        # else:
        #     commentPieData[1]['value'] += 1
        # 使用大模型进行情感分析
        # sentiment_label = data_classfication(comment[1])
        # 使用大模型微调进行情感分析
        sentiment_label = check_data([comment[1]])
        if sentiment_label == '正面':
            commentPieData[0]['value'] += 1
        else:
            commentPieData[1]['value'] += 1
    articleList = articleDao.getAllArticle()
    for article in articleList:
        # 情感分析
        # stc = SnowNLP(article[1]).sentiments
        # if stc > 0.6:
        #     articlePieData[0]['value'] += 1
        # elif stc < 0.2:
        #     articlePieData[2]['value'] += 1
        # else:
        #     articlePieData[1]['value'] += 1
        # 使用大模型进行情感分析
        # sentiment_label = data_classfication(article[1])
        # 使用大模型微调进行情感分析
        sentiment_label = check_data([article[1]])
        if sentiment_label == '正面':
            articlePieData[0]['value'] += 1
        else:
            articlePieData[1]['value'] += 1
    df2 = pd.read_csv('./fenci/comment_fre.csv', nrows=15)
    xhotData15 = [x[0] for x in df2.values][::-1]
    yhotData15 = [x[1] for x in df2.values][::-1]
    return render_template('sentimentAnalysis.html',
                           xHotBarData=xHotBarData,
                           yHotBarData=yHotBarData,
                           hotTreeMapData=hotTreeMapData,
                           commentPieData=commentPieData,
                           articlePieData=articlePieData,
                           xhotData15=xhotData15,
                           yhotData15=yhotData15)

修改后,整个可视化仪表板的数据准确率得到了质的飞跃。

四、 性能优化:实现批量预测与工程实践建议

在Web应用场景中,逐条调用模型预测效率低下。我们应该实现批量预测功能,但需注意控制批量大小,避免GPU内存溢出。我们在模型封装层提供一个支持列表输入的批量预测方法。

def check_data(data):
    # 验证模型
    model.eval()  # 评估模式 Dropout关闭
    with torch.no_grad():  # 评估模式,不需要计算梯度
        input_ids, attention_mask, token_type_ids = collate_fn(data)
        out = model(input_ids=input_ids.to(device), attention_mask=attention_mask.to(device),
                    token_type_ids=token_type_ids.to(device))
        output = out.argmax(dim=1)
        # sentiment = output.item()
        # sentiment_label = "正面" if sentiment == 1 else "负面"
        # print("预测结果:", sentiment_label)
        return output.tolist()
# 批量处理
def check_batch_datas(dataList, batch_num):
    outputList = []
    for i in range(0, len(dataList), batch_num):
        batch_data = dataList[i:i + batch_num]
        outputList.extend(check_data(batch_data))
    return outputList

以微博文章分析模块为例,修改为批量处理模式,显著提升系统响应速度。

@pb.route('articleData')
def articleData():
    """
    微博舆情分析
    :return:
    """
    articleOldList = articleDao.getAllArticle()
    # 获取所有帖子标题
    articleTitleList = [article[1] for article in articleOldList]
    # 批量使用大模型进行舆情分析
    sentimentsList = check_batch_datas(articleTitleList, batch_num=10)
    # 追加情感分析属性
    articleNewList = []
    for i in range(len(articleOldList)):
        if sentimentsList[i] == 1:
            articleNewList.append(articleOldList[i] + ('正面',))
        else:
            articleNewList.append(articleOldList[i] + ('负面',))
        # for article in articleOldList:
        #     article = list(article)
        # 情感分析
        sentiments = ''
        # stc = SnowNLP(article[1]).sentiments
        # if stc > 0.6:
        #     sentiments = '正面'
        # elif stc < 0.2:
        #     sentiments = '负面'
        # else:
        #     sentiments = '中性'
        # 改成使用大模型进行舆情分析
        # sentiments = data_classfication(article[1])
        # 改成使用大模型微调进行舆情分析
        # sentiments = check_data([article[1]])
        # article.append(sentiments)
        # articleNewList.append(article)
    return render_template('articleData.html', articleList=articleNewList)

优化后的批量处理效果显著。

实践建议与延伸思考:

  • 模型选择: 除了BERT,还可尝试RoBERTa、ALBERT等变体,或在中文领域表现优异的ERNIE、RoFormer等模型。
  • 领域适配: 如果拥有大量标注数据,可以考虑进行更深入的领域自适应预训练(Domain-adaptive Pre-training),进一步提升在微博垂直领域的性能。
  • 多标签分类: 本文聚焦二分类,实际舆情可能需细分为“正面”、“负面”、“中性”甚至“愤怒”、“高兴”等多标签,只需修改分类头为多神经元输出即可。
  • 工程化考量: 对于高并发场景,可以考虑使用模型服务化框架(如TorchServe、Triton Inference Server)将模型部署为独立服务,并通过异步队列处理预测请求。

[AFFILIATE_SLOT_2]

五、 总结

本文详细阐述了如何将一个基于传统NLP库(SnowNLP)的微博舆情分析系统,升级为基于预训练大模型BERT微调的高精度系统。通过自定义数据集处理、定义增量模型、微调训练、模型评估与工程化集成这一完整流程,我们显著提升了情感分析的准确率。同时,通过引入批量预测等优化手段,保障了系统在实际应用中的性能。这种“预训练+微调”的范式,不仅在舆情分析中,在诸如Java日志分析、TypeScript代码注释生成、Python自动化脚本理解、C++代码缺陷检测乃至JavaScript用户反馈归类等众多NLP应用场景中,都提供了强大的解决方案。希望这篇实战指南能为你的AI项目落地提供清晰的路径和有益的参考。

posted on 2026-03-01 09:28  blfbuaa  阅读(104)  评论(0)    收藏  举报