Bootstrap

基于Pytorch的中文情感分析实践

基于Pytorch的中文情感分析实践

注:本文为学习 DataWhale 开源教程《深入浅出 Pytorch》第四章所做学习笔记,基于实战教程,实现了使用 LSTM 模型的中文微博情感分析全过程。

一、数据及环境基础

​ 本文使用中文微博情感分析数据集(Pytorch_practice/weibo_senti_100k.csv at main · nowadays0421/Pytorch_practice (github.com)),基于 Pytorch 基础使用构建了简单 LSTM 模型实现情感分析。

​ 本文导入的第三方库包括:

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torch.utils.data import Dataset
import pandas as pd
import jieba
from tqdm import tqdm
from torch.nn.utils.rnn import pack_padded_sequence
from torch.nn.utils.rnn import pad_sequence
from collections import defaultdict
from torch import optim
from torch.nn import functional as F

二、实践步骤

1. 数据读取及数据集准备

​ 本地数据集以 csv 格式存在,首先通过 pandas 库将本地数据读取成 DataFrame 类型:

whole_data = pd.read_csv(data_path)
# data_path 为本地数据路径

​ 自然语言处理需要将自然语言 token (句、词、短语皆可,自然语言处理任务的最小单元)映射为一个实数,因此定义一个词表类型,用于将自然语言文本映射为向量:

'''定义一个词表类型。'''
# 该类用于实现token到索引的映射
class Vocab:

    def __init__(self, tokens = None) -> None:
        # 构造函数
        # tokens:全部的token列表

        self.idx_to_token = list()
        # 将token存成列表,索引直接查找对应的token即可
        self.token_to_idx = dict()
        # 将索引到token的映射关系存成字典,键为索引,值为对应的token

        if tokens is not None:
            # 构造时输入了token的列表
            if "<unk>" not in tokens:
                # 不存在标记
                tokens = tokens + "<unk>"
            for token in tokens:
                self.idx_to_token.append(token)
                self.token_to_idx[token] = len(self.idx_to_token) - 1
                # 当前该token对应的索引是当下列表的最后一个
            self.unk = self.token_to_idx["<unk>"]

    @classmethod
    def build(cls, data, min_freq=1, reserved_tokens=None, stop_words = 'hit_stopwords.txt'):
        # 构建词表
        # cls:该类本身
        # data: 输入的文本数据
        # min_freq:列入token的最小频率
        # reserved_tokens:额外的标记token
        # stop_words:停用词文件路径
        token_freqs = defaultdict(int)
        stopwords = open(stop_words).read().split('\n')
        for i in tqdm(range(data.shape[0]), desc=f"Building vocab"):
            for token in jieba.lcut(data.iloc[i]["review"]):
                if token in stop_words:
                    continue
                token_freqs[token] += 1
        # 统计各个token的频率
        uniq_tokens = ["<unk>"] + (reserved_tokens if reserved_tokens else [])
        # 加入额外的token
        uniq_tokens += [token for token, freq in token_freqs.items() \
            if freq >= min_freq and token != "<unk>"]
        # 全部的token列表
        return cls(uniq_tokens)

    def __len__(self):
        # 返回词表的大小
        return len(self.idx_to_token)

    def __getitem__(self, token):
        # 查找输入token对应的索引,不存在则返回<unk>返回的索引
        return self.token_to_idx.get(token, self.unk)

    def convert_tokens_to_ids(self, tokens):
        # 查找一系列输入标签对应的索引值
        return [self[token] for token in tokens]

    def convert_ids_to_tokens(self, ids):
        # 查找一系列索引值对应的标记
        return [self.idx_to_token[index] for index in ids]

​ 接着,实现一个封装函数用于实现 token 映射与构建训练集、测试集;

'''数据集构建函数'''
def build_data(data_path:str):
    '''
    Args:
       data_path:待读取本地数据的路径 
    Returns:
       训练集、测试集、词表
    '''
    whole_data = pd.read_csv(data_path)
    # 读取数据为 DataFrame 类型
    vocab = Vocab.build(whole_data)
    # 构建词表

    train_data = [(vocab.convert_tokens_to_ids(sentence), 1) for sentence in whole_data[whole_data["label"] == 1][:50000]["review"]]\
    +[(vocab.convert_tokens_to_ids(sentence), 0) for sentence in whole_data[whole_data["label"] == 0][:50000]["review"]]
    # 分别取褒贬各50000句作为训练数据,将token映射为对应的索引值

    test_data = [(vocab.convert_tokens_to_ids(sentence), 1) for sentence in whole_data[whole_data["label"] == 1][50000:]["review"]]\
        +[(vocab.convert_tokens_to_ids(sentence), 0) for sentence in whole_data[whole_data["label"] == 0][50000:]["review"]]
    # 其余数据作为测试数据

    return train_data, test_data, vocab

2. 构建数据集

​ 使用 Pytorch 需要构建一个自定义数据集,并实现样本抽取函数:

'''声明一个 DataSet 类'''
class MyDataset(Dataset):

    def __init__(self, data) -> None:
        # data:使用词表映射之后的数据
        self.data = data

    def __len__(self):
        # 返回样例的数目
        return len(self.data)

    def __getitem__(self, index):
        return self.data[index]
    
'''声明一个collate_fn函数,用于对一个批次的样本进行整理'''
def collate_fn(examples):
    # 从独立样本集合中构建各批次的输入输出
    lengths = torch.tensor([len(ex[0]) for ex in examples])
    # 获取每个序列的长度
    inputs = [torch.tensor(ex[0]) for ex in examples]
    # 将输入inputs定义为一个张量的列表,每一个张量为句子对应的索引值序列
    targets = torch.tensor([ex[1] for ex in examples], dtype=torch.long)
    # 目标targets为该批次所有样例输出结果构成的张量
    inputs = pad_sequence(inputs, batch_first=True)
    # 将用pad_sequence对批次类的样本进行补齐
    return inputs, lengths, targets

3. 实现 LSTM 模型

​ 搭建一个简单 LSTM 模型,实现建模:

'''创建一个LSTM类作为模型'''
class LSTM(nn.Module):
    # 基类为nn.Module
    def __init__(self, vocab_size, embedding_dim, hidden_dim, num_class):
        # 构造函数
        # vocab_size:词表大小
        # embedding_dim:词向量维度
        # hidden_dim:隐藏层维度
        # num_class:多分类个数
        super(LSTM, self).__init__()

        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        # 词向量层
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first = True)
        # lstm层
        self.output = nn.Linear(hidden_dim, num_class)
        # 输出层,线性变换

    def forward(self, inputs, lengths):
        # 前向计算函数
        # inputs:输入
        # lengths:打包的序列长度
        # print(f"输入为:{inputs.size()}")
        embeds = self.embedding(inputs)
        # 注意这儿是词向量层,不是词袋词向量层
        # print(f"词向量层输出为:{embeds.size()}")
        x_pack = pack_padded_sequence(embeds, lengths.to('cpu'), batch_first=True, enforce_sorted=False)
        # LSTM需要定长序列,使用该函数将变长序列打包
        # print(f"经过打包为:{x_pack.size()}")
        hidden, (hn, cn) = self.lstm(x_pack)
        # print(f"经过lstm计算后为:{hn.size()}")
        outputs = self.output(hn[-1])
        # print(f"输出层输出为:{outputs.size()}")
        log_probs = F.log_softmax(outputs, dim = -1)
        # print(f"输出概率值为:{probs}")
        # 归一化为概率值
        return log_probs

4 . 训练

​ 实现上述操作之后,进行训练和预测即可:

nll_loss = nn.NLLLoss()
# 负对数似然损失
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Adam优化器

model.train()
for epoch in range(num_epoch):
    total_loss = 0
    for batch in tqdm(train_data_loader, desc=f"Training Epoch {epoch}"):
        inputs, lengths, targets = [x.to(device) for x in batch]
        # print(inputs.size())
        log_probs = model(inputs, lengths)
        loss = nll_loss(log_probs, targets)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"Loss:{total_loss:.2f}")

# 测试
acc = 0
for batch in tqdm(test_data_loader, desc=f"Testing"):
    inputs, lengths, targets = [x.to(device) for x in batch]
    with torch.no_grad():
        output = model(inputs, lengths)
        acc += (output.argmax(dim=1) == targets).sum().item()
print(f"ACC:{acc / len(test_data_loader):.2f}")

​ 本文采用负对数似然损失,使用 Adam 优化器,测试时计算准确率。

三、训练效果

​ 在使用简单 LSTM 模型,使用十万条样本作为训练集的情况下,测试准确率可以达到98%:

在这里插入图片描述

注:全部代码参见 Github Pytorch_practice/sentiment_classify.py at main · nowadays0421/Pytorch_practice (github.com)

;