昇思25天学习打卡营第25天 | RNN实现情感分类

今天是参加昇思学习打卡营的第25天,学习内容是RNN实现情感分类。

以下是内容概要:
 

  1. 情感分析是自然语言处理中的经典任务,涉及将文本分类为正面或负面情感。

  2. 使用IMDB影评数据集,包含正面和负面两类评论。使用Glove词向量对文本进行编码,以获取语义特征。

  3. 设计了数据下载模块,使用requeststqdm库实现数据的可视化下载。

  4. 使用tarfile库读取数据集,并进行预处理,包括分词和去除标点。

  5. 加载Glove词向量,使用dataset.text.Vocab进行词表的加载和处理。

  6. 使用MindSpore的LookupPadEnd接口进行文本的索引化和填充。

  7. 构建了一个基于RNN网络的情感分类模型,包括Embedding层、LSTM层和Dense层。

  8. 设计了训练循环,使用tqdm库进行训练进度的可视化。

  9. 实现了二分类准确率的计算函数,并设计了评估逻辑。

  10. 进行模型训练,并在每个epoch后评估验证集损失,保存最优模型。

  11. 加载训练好的模型,并在测试集上进行评估。

概述

情感分类是自然语言处理中的经典任务,是典型的分类问题。本节使用MindSpore实现一个基于RNN网络的情感分类模型,实现如下的效果:

输入: This film is terrible
正确标签: Negative
预测标签: Negative

输入: This film is great
正确标签: Positive
预测标签: Positive

 

数据准备

本节使用情感分类的经典数据集IMDB影评数据集,数据集包含Positive和Negative两类,下面为其样例:

ReviewLabel
"Quitting" may be as much about exiting a pre-ordained identity as about drug withdrawal. As a rural guy coming to Beijing, class and success must have struck this young artist face on as an appeal to separate from his roots and far surpass his peasant parents' acting success. Troubles arise, however, when the new man is too new, when it demands too big a departure from family, history, nature, and personal identity. The ensuing splits, and confusion between the imaginary and the real and the dissonance between the ordinary and the heroic are the stuff of a gut check on the one hand or a complete escape from self on the other.Negative
This movie is amazing because the fact that the real people portray themselves and their real life experience and do such a good job it's like they're almost living the past over again. Jia Hongsheng plays himself an actor who quit everything except music and drugs struggling with depression and searching for the meaning of life while being angry at everyone especially the people who care for him most.Positive

此外,需要使用预训练词向量对自然语言单词进行编码,以获取文本的语义特征,本节选取Glove词向量作为Embedding。

数据下载模块

为了方便数据集和预训练词向量的下载,首先设计数据下载模块,实现可视化下载流程,并保存至指定路径。数据下载模块使用requests库进行http请求,并通过tqdm库对下载百分比进行可视化。此外针对下载安全性,使用IO的方式下载临时文件,而后保存至指定的路径并返回。

tqdmrequests库需手动安装,命令如下:pip install tqdm requests

import os  # 导入os库,用于文件和目录操作
import shutil  # 导入shutil库,用于文件复制操作
import requests  # 导入requests库,用于发送HTTP请求
import tempfile  # 导入tempfile库,用于创建临时文件
from tqdm import tqdm  # 导入tqdm库,用于显示下载进度条
from typing import IO  # 导入typing库的IO类型,用于类型注解
from pathlib import Path  # 导入pathlib库的Path类型,用于路径操作

# 指定保存路径为用户的home目录下的.mindspore_examples文件夹
cache_dir = Path.home() / '.mindspore_examples'

def http_get(url: str, temp_file: IO):
    """
    使用requests库下载数据,并使用tqdm库进行流程可视化。
    
    参数:
    - url: 要下载文件的URL地址
    - temp_file: 一个文件对象,用于写入下载的数据
    """
    req = requests.get(url, stream=True)  # 发起HTTP GET请求,设置stream=True以流式下载
    content_length = req.headers.get('Content-Length')  # 获取内容长度
    total = int(content_length) if content_length is not None else None  # 计算总字节数
    progress = tqdm(unit='B', total=total)  # 创建进度条对象
    for chunk in req.iter_content(chunk_size=1024):  # 流式读取数据
        if chunk:
            progress.update(len(chunk))  # 更新进度条
            temp_file.write(chunk)  # 将数据写入临时文件
    progress.close()  # 关闭进度条

def download(file_name: str, url: str):
    """
    下载数据并存为指定名称。
    
    参数:
    - file_name: 文件的名称
    - url: 文件的下载URL
    
    返回:
    - cache_path: 文件保存的完整路径
    """
    if not os.path.exists(cache_dir):  # 检查缓存目录是否存在,不存在则创建
        os.makedirs(cache_dir)
    cache_path = os.path.join(cache_dir, file_name)  # 获取文件的完整保存路径
    cache_exist = os.path.exists(cache_path)  # 检查文件是否已存在
    if not cache_exist:  # 如果文件不存在,则进行下载
        with tempfile.NamedTemporaryFile() as temp_file:  # 创建一个临时文件
            http_get(url, temp_file)  # 使用http_get函数下载数据到临时文件
            temp_file.flush()  # 确保所有数据写入临时文件
            temp_file.seek(0)  # 移动文件指针到文件开始位置
            with open(cache_path, 'wb') as cache_file:  # 打开目标文件进行写入
                shutil.copyfileobj(temp_file, cache_file)  # 将临时文件的数据复制到目标文件
    return cache_path  # 返回文件的保存路径

 

加载IMDB数据集

下载好的IMDB数据集为tar.gz文件,我们使用Python的tarfile库对其进行读取,并将所有数据和标签分别进行存放。原始的IMDB数据集解压目录如下:

    ├── aclImdb
    │   ├── imdbEr.txt
    │   ├── imdb.vocab
    │   ├── README
    │   ├── test
    │   └── train
    │         ├── neg
    │         ├── pos
    ...

数据集已分割为train和test两部分,且每部分包含neg和pos两个分类的文件夹,因此需分别train和test进行读取并处理数据和标签。

import re  # 导入正则表达式库
import six  # 导入six库,用于Python 2和3的兼容性
import string  # 导入字符串库,用于处理字符串
import tarfile  # 导入tarfile库,用于读取tar文件

class IMDBData():
    """IMDB数据集加载器

    加载IMDB数据集并处理为一个Python迭代对象。
    """

    # 定义标签映射,将文本标签转换为数字标签
    label_map = {
        "pos": 1,
        "neg": 0
    }

    def __init__(self, path, mode="train"):
        """
        初始化函数,加载数据集。

        参数:
        - path: 数据集文件的路径
        - mode: 数据集的模式,可以是"train"(训练集)或"test"(测试集)
        """
        self.mode = mode  # 设置模式
        self.path = path  # 设置文件路径
        self.docs, self.labels = [], []  # 初始化文档和标签列表

        # 加载正向和负向评论
        self._load("pos")
        self._load("neg")

    def _load(self, label):
        """
        加载指定标签的数据。

        参数:
        - label: 标签名称,可以是"pos"(正面)或"neg"(负面)
        """
        # 编译正则表达式,用于匹配数据集中的文件名
        pattern = re.compile(r"aclImdb/{}/{}/.*\.txt$".format(self.mode, label))
        
        # 使用tarfile库打开tar文件
        with tarfile.open(self.path) as tarf:
            tf = tarf.next()  # 获取第一个文件
            while tf is not None:
                if bool(pattern.match(tf.name)):  # 如果文件名匹配正则表达式
                    # 提取文件内容,去除标点和特殊字符,并转换为小写进行分词
                    self.docs.append(str(tarf.extractfile(tf).read().rstrip(six.b("\n\r"))
                                         .translate(None, six.b(string.punctuation)).lower()).split())
                    self.labels.append([self.label_map[label]])  # 将标签添加到标签列表
                tf = tarf.next()  # 移动到下一个文件

    def __getitem__(self, idx):
        """
        获取指定索引的文档和标签。

        参数:
        - idx: 要获取的文档和标签的索引

        返回:
        - 返回指定索引的文档和标签
        """
        return self.docs[idx], self.labels[idx]

    def __len__(self):
        """
        返回数据集中文档的数量。
        """
        return len(self.docs)

完成IMDB数据加载器后,加载训练数据集进行测试,输出数据集数量:

imdb_train = IMDBData(imdb_path, 'train')
len(imdb_train)

将IMDB数据集加载至内存并构造为迭代对象后,可以使用mindspore.dataset提供的Generatordataset接口加载数据集迭代对象,并进行下一步的数据处理,下面封装一个函数将train和test分别使用Generatordataset进行加载,并指定数据集中文本和标签的column_name分别为textlabel:

import mindspore.dataset as ds  # 导入MindSpore的数据集模块

def load_imdb(imdb_path):
    """
    加载 IMDB 数据集并创建 GeneratorDataset 对象。
    
    参数:
    - imdb_path: IMDB 数据集文件的路径。
    
    返回:
    - imdb_train: 训练数据集的 GeneratorDataset 对象。
    - imdb_test: 测试数据集的 GeneratorDataset 对象。
    """
    # 使用IMDBData类加载训练数据集,指定模式为"train"
    # GeneratorDataset是MindSpore提供的数据集类,用于从Python迭代器生成数据集
    # column_names指定数据集中的列名,shuffle=True表示数据需要被打乱
    # num_samples指定生成的数据集样本数量,这里设置为10000
    imdb_train = ds.GeneratorDataset(
        IMDBData(imdb_path, "train"),
        column_names=["text", "label"],
        shuffle=True,
        num_samples=10000
    )
    
    # 使用IMDBData类加载测试数据集,指定模式为"test"
    # shuffle=False表示数据不需要被打乱
    imdb_test = ds.GeneratorDataset(
        IMDBData(imdb_path, "test"),
        column_names=["text", "label"],
        shuffle=False
    )
    
    # 返回训练和测试数据集对象
    return imdb_train, imdb_test

加载IMDB数据集,可以看到imdb_train是一个GeneratorDataset对象。

加载预训练词向量

预训练词向量是对输入单词的数值化表示,通过nn.Embedding层,采用查表的方式,输入单词对应词表中的index,获得对应的表达向量。 因此进行模型构造前,需要将Embedding层所需的词向量和词表进行构造。这里我们使用Glove(Global Vectors for Word Representation)这种经典的预训练词向量, 其数据格式如下:

WordVector
the0.418 0.24968 -0.41242 0.1217 0.34527 -0.044457 -0.49688 -0.17862 -0.00066023 ...
,0.013441 0.23682 -0.16899 0.40951 0.63812 0.47709 -0.42852 -0.55641 -0.364 ...

我们直接使用第一列的单词作为词表,使用dataset.text.Vocab将其按顺序加载;同时读取每一行的Vector并转为numpy.array,用于nn.Embedding加载权重使用。具体实现如下:

import zipfile  # 导入zipfile库,用于读取zip文件
import numpy as np  # 导入NumPy库,用于数值计算
import os  # 导入os库,用于操作文件路径
import mindspore.dataset as ds  # 导入MindSpore的dataset模块,用于文本处理

# 指定保存路径为 `home_path/.mindspore_examples`
cache_dir = Path.home() / '.mindspore_examples'

def load_glove(glove_path):
    """
    加载GloVe预训练词向量。

    参数:
    - glove_path: GloVe词向量压缩包的路径。

    返回:
    - vocab: 词汇表对象,包含词项和对应的索引。
    - embeddings: 词向量数组。
    """
    glove_100d_path = os.path.join(cache_dir, 'glove.6B.100d.txt')  # GloVe文件的路径
    if not os.path.exists(glove_100d_path):  # 如果GloVe文件不存在,则解压
        glove_zip = zipfile.ZipFile(glove_path)  # 打开zip文件
        glove_zip.extractall(cache_dir)  # 将zip文件中的所有内容解压到cache_dir目录

    embeddings = []  # 初始化词向量列表
    tokens = []  # 初始化词汇列表
    with open(glove_100d_path, encoding='utf-8') as gf:  # 打开GloVe文件
        for glove in gf:  # 遍历文件中的每一行
            word, embedding = glove.split(maxsplit=1)  # 分割词和向量
            tokens.append(word)  # 将词添加到词汇列表
            embeddings.append(np.fromstring(embedding, dtype=np.float32, sep=' '))  # 将向量添加到词向量列表

    # 添加 <unk>, <pad> 两个特殊占位符对应的embedding
    embeddings.append(np.random.rand(100))  # 随机生成100维向量作为<unk>的embedding
    embeddings.append(np.zeros((100,), np.float32))  # 生成全0的100维向量作为<pad>的embedding

    vocab = ds.text.Vocab.from_list(tokens, special_tokens=["<unk>", "<pad>"], special_first=False)  # 创建词汇表
    embeddings = np.array(embeddings).astype(np.float32)  # 将词向量列表转换为NumPy数组

    return vocab, embeddings  # 返回词汇表和词向量数组

由于数据集中可能存在词表没有覆盖的单词,因此需要加入<unk>标记符;同时由于输入长度的不一致,在打包为一个batch时需要将短的文本进行填充,因此需要加入<pad>标记符。完成后的词表长度为原词表长度+2。

下面下载Glove词向量,并加载生成词表和词向量权重矩阵。

glove_path = download('glove.6B.zip', 'https://mindspore-website.obs.myhuaweicloud.com/notebook/datasets/glove.6B.zip')
vocab, embeddings = load_glove(glove_path)
len(vocab.vocab())

使用词表将the转换为index id,并查询词向量矩阵对应的词向量:

idx = vocab.tokens_to_ids('the')
embedding = embeddings[idx]
idx, embedding

数据集预处理

通过加载器加载的IMDB数据集进行了分词处理,但不满足构造训练数据的需要,因此要对其进行额外的预处理。其中包含的预处理如下:

  • 通过Vocab将所有的Token处理为index id。
  • 将文本序列统一长度,不足的使用<pad>补齐,超出的进行截断。

这里我们使用mindspore.dataset中提供的接口进行预处理操作。这里使用到的接口均为MindSpore的高性能数据引擎设计,每个接口对应操作视作数据流水线的一部分,详情请参考MindSpore数据引擎。 首先针对token到index id的查表操作,使用text.Lookup接口,将前文构造的词表加载,并指定unknown_token。其次为文本序列统一长度操作,使用PadEnd接口,此接口定义最大长度和补齐值(pad_value),这里我们取最大长度为500,填充值对应词表中<pad>的index id。

除了对数据集中text进行预处理外,由于后续模型训练的需要,要将label数据转为float32格式。

import mindspore as ms  # 导入MindSpore库

# 创建Lookup操作,用于将文本中的token(单词)转换为对应的index id
# vocab是之前创建的词汇表对象,包含token到index的映射
# unknown_token='<unk>'表示对于词汇表中未包含的token,使用'<unk>'的index作为其id
lookup_op = ds.text.Lookup(vocab, unknown_token='<unk>')

# 创建PadEnd操作,用于将文本序列填充到固定长度
# [500]表示每个文本序列的最大长度为500
# pad_value是词汇表中'<pad>'标记对应的index id,用于进行填充
pad_op = ds.transforms.PadEnd([500], pad_value=vocab.tokens_to_ids('<pad>'))

# 创建TypeCast操作,用于将数据类型转换为MindSpore的float32类型
# 这通常用于将标签数据从整数转换为浮点数,以满足模型训练的需求
type_cast_op = ds.transforms.TypeCast(ms.float32)

 完成预处理操作后,需将其加入到数据集处理流水线中,使用map接口对指定的column添加操作。

# 假定imdb_train和imdb_test是已经加载的IMDB训练和测试数据集的GeneratorDataset对象

# 使用map函数对imdb_train数据集的'text'列应用lookup_op和pad_op操作
# lookup_op将文本中的token转换为对应的index id
# pad_op将文本序列填充到固定长度,这里设置为500,使用'<pad>'标记进行填充
imdb_train = imdb_train.map(operations=[lookup_op, pad_op], input_columns=['text'])

# 接下来,对imdb_train数据集的'label'列应用type_cast_op操作
# type_cast_op将标签数据转换为float32类型,以满足模型训练的需求
imdb_train = imdb_train.map(operations=[type_cast_op], input_columns=['label'])

# 同训练数据集类似,对imdb_test数据集的'text'列应用lookup_op和pad_op操作
imdb_test = imdb_test.map(operations=[lookup_op, pad_op], input_columns=['text'])

# 对imdb_test数据集的'label'列应用type_cast_op操作,转换标签数据类型
imdb_test = imdb_test.map(operations=[type_cast_op], input_columns=['label'])

由于IMDB数据集本身不包含验证集,我们手动将其分割为训练和验证两部分,比例取0.7, 0.3。

imdb_train, imdb_valid = imdb_train.split([0.7, 0.3])

最后指定数据集的batch大小,通过batch接口指定,并设置是否丢弃无法被batch size整除的剩余数据。

调用数据集的mapsplitbatch为数据集处理流水线增加对应操作,返回值为新的Dataset类型。现在仅定义流水线操作,在执行时开始执行数据处理流水线,获取最终处理好的数据并送入模型进行训练。

imdb_train = imdb_train.batch(64, drop_remainder=True)
imdb_valid = imdb_valid.batch(64, drop_remainder=True)

模型构建

完成数据集的处理后,我们设计用于情感分类的模型结构。首先需要将输入文本(即序列化后的index id列表)通过查表转为向量化表示,此时需要使用nn.Embedding层加载Glove词向量;然后使用RNN循环神经网络做特征提取;最后将RNN连接至一个全连接层,即nn.Dense,将特征转化为与分类数量相同的size,用于后续进行模型优化训练。整体模型结构如下:

nn.Embedding -> nn.RNN -> nn.Dense

这里我们使用能够一定程度规避RNN梯度消失问题的变种LSTM(Long short-term memory)做特征提取层。下面对模型进行详解:

Embedding

Embedding层又可称为EmbeddingLookup层,其作用是使用index id对权重矩阵对应id的向量进行查找,当输入为一个由index id组成的序列时,则查找并返回一个相同长度的矩阵,例如:

embedding = nn.Embedding(1000, 100) # 词表大小(index的取值范围)为1000,表示向量的size为100
input shape: (1, 16)                # 序列长度为16
output shape: (1, 16, 100)

这里我们使用前文处理好的Glove词向量矩阵,设置nn.Embeddingembedding_table为预训练词向量矩阵。对应的vocab_size为词表大小400002,embedding_size为选用的glove.6B.100d向量大小,即100。

RNN(循环神经网络)

循环神经网络(Recurrent Neural Network, RNN)是一类以序列(sequence)数据为输入,在序列的演进方向进行递归(recursion)且所有节点(循环单元)按链式连接的神经网络。

import math  # 导入math库,用于数学计算
import mindspore as ms  # 导入MindSpore库
import mindspore.nn as nn  # 导入MindSpore的神经网络模块
import mindspore.ops as ops  # 导入MindSpore的操作模块
from mindspore.common.initializer import Uniform, HeUniform  # 导入MindSpore的初始化器

class RNN(nn.Cell):
    def __init__(self, embeddings, hidden_dim, output_dim, n_layers,
                 bidirectional, pad_idx):
        """
        初始化RNN模型。
        
        参数:
        - embeddings: 预训练的词向量,用于Embedding层的权重矩阵
        - hidden_dim: RNN层的隐藏层维度
        - output_dim: 模型的输出维度
        - n_layers: RNN层的数量
        - bidirectional: 是否使用双向RNN
        - pad_idx: 用于填充的索引,通常用于处理序列中的padding部分
        """
        super().__init__()  # 调用父类的构造函数
        vocab_size, embedding_dim = embeddings.shape  # 获取词向量的数量和维度
        # 创建Embedding层,将词汇表中的词映射到向量空间
        self.embedding = nn.Embedding(vocab_size, embedding_dim, 
                                      embedding_table=ms.Tensor(embeddings), 
                                      padding_idx=pad_idx)
        # 创建LSTM层,设置隐藏层维度、层数、是否双向等参数
        self.rnn = nn.LSTM(embedding_dim,
                           hidden_dim,
                           num_layers=n_layers,
                           bidirectional=bidirectional,
                           batch_first=True)
        # 定义权重和偏置的初始化器
        weight_init = HeUniform(math.sqrt(5))  # He初始化器,用于权重初始化
        bias_init = Uniform(1 / math.sqrt(hidden_dim * 2))  # 均匀初始化器,用于偏置初始化
        # 创建全连接层,将LSTM层的输出映射到输出维度
        self.fc = nn.Dense(hidden_dim * 2, 
                            output_dim, 
                            weight_init=weight_init, 
                            bias_init=bias_init)

    def construct(self, inputs):
        """
        模型的前向传播函数。
        
        参数:
        - inputs: 输入的序列数据
        
        返回:
        - output: 模型的预测结果
        """
        embedded = self.embedding(inputs)  # 将输入序列通过Embedding层转换为向量序列
        _, (hidden, _) = self.rnn(embedded)  # 将向量序列通过LSTM层得到隐藏状态
        # 根据是否使用双向RNN,拼接最后一个时间步的隐藏状态和第一个时间步的隐藏状态
        hidden = ops.concat((hidden[-2, :, :], hidden[-1, :, :]), axis=1)
        output = self.fc(hidden)  # 将隐藏状态通过全连接层得到最终输出
        return output

损失函数与优化器

完成模型主体构建后,首先根据指定的参数实例化网络;然后选择损失函数和优化器。针对本节情感分类问题的特性,即预测Positive或Negative的二分类问题,我们选择nn.BCEWithLogitsLoss(二分类交叉熵损失函数)。

import mindspore.nn as nn

# 定义模型参数
hidden_size = 256  # RNN隐藏层的尺寸
output_size = 1  # 输出层的尺寸,对于二分类问题,我们通常设置为1
num_layers = 2  # RNN层的数量
bidirectional = True  # 是否使用双向RNN
lr = 0.001  # 学习率
pad_idx = vocab.tokens_to_ids('<pad>')  # 获得词汇表中'<pad>'标记对应的索引

# 实例化RNN模型
# embeddings是预训练的词向量矩阵,hidden_size是隐藏层的维度,output_size是输出层的维度
# num_layers是RNN层的数量,bidirectional表示是否使用双向RNN,pad_idx是填充标记的索引
model = RNN(embeddings, hidden_size, output_size, num_layers, bidirectional, pad_idx)

# 定义损失函数,这里使用二元交叉熵损失函数,适用于二分类问题
# reduction='mean'表示将损失平均化,以一个单一的标量输出
loss_fn = nn.BCEWithLogitsLoss(reduction='mean')

# 定义优化器,这里使用Adam优化器
# model.trainable_params()获取模型中所有的可训练参数
# learning_rate=lr设置优化器的学习率
optimizer = nn.Adam(model.trainable_params(), learning_rate=lr)

训练逻辑

在完成模型构建,进行训练逻辑的设计。一般训练逻辑分为一下步骤:

  1. 读取一个Batch的数据;
  2. 送入网络,进行正向计算和反向传播,更新权重;
  3. 返回loss。

下面按照此逻辑,使用tqdm库,设计训练一个epoch的函数,用于训练过程和loss的可视化。

import mindspore as ms
from tqdm import tqdm

# 定义模型前向传播和计算损失的函数
def forward_fn(data, label):
    logits = model(data)  # 模型前向传播,获取预测结果logits
    loss = loss_fn(logits, label)  # 计算预测结果和标签之间的损失
    return loss

# 使用MindSpore的value_and_grad函数获取损失相对于模型参数的梯度
grad_fn = ms.value_and_grad(forward_fn, None, optimizer.parameters)

# 定义单步训练函数
def train_step(data, label):
    loss, grads = grad_fn(data, label)  # 计算损失和梯度
    optimizer(grads)  # 使用优化器更新模型参数
    return loss  # 返回计算的损失

# 定义训练一个epoch的函数
def train_one_epoch(model, train_dataset, epoch=0):
    model.set_train()  # 设置模型为训练模式
    total = train_dataset.get_dataset_size()  # 获取数据集的总大小
    loss_total = 0  # 初始化损失总和
    step_total = 0  # 初始化训练步数计数器
    with tqdm(total=total) as t:  # 使用tqdm库显示训练进度
        t.set_description('Epoch %i' % epoch)  # 设置进度条描述信息
        for i in train_dataset.create_tuple_iterator():  # 遍历数据集中的每个样本
            loss = train_step(*i)  # 执行单步训练,获取损失
            loss_total += loss.asnumpy()  # 累加损失
            step_total += 1  # 增加训练步数
            t.set_postfix(loss=loss_total/step_total)  # 设置进度条后缀,显示平均损失
            t.update(1)  # 更新进度条

# 这个训练循环可以用于迭代模型训练指定的epoch次数,每个epoch都会调用这个函数

评估指标和逻辑

训练逻辑完成后,需要对模型进行评估。即使用模型的预测结果和测试集的正确标签进行对比,求出预测的准确率。由于IMDB的情感分类为二分类问题,对预测值直接进行四舍五入即可获得分类标签(0或1),然后判断是否与正确标签相等即可。下面为二分类准确率计算函数实现:

def binary_accuracy(preds, y):
    """
    计算每个batch的准确率
    """

    # 对预测值进行四舍五入
    rounded_preds = np.around(ops.sigmoid(preds).asnumpy())
    correct = (rounded_preds == y).astype(np.float32)
    acc = correct.sum() / len(correct)
    return acc

有了准确率计算函数后,类似于训练逻辑,对评估逻辑进行设计, 分别为以下步骤:

  1. 读取一个Batch的数据;
  2. 送入网络,进行正向计算,获得预测结果;
  3. 计算准确率。

同训练逻辑一样,使用tqdm进行loss和过程的可视化。此外返回评估loss至供保存模型时作为模型优劣的判断依据。

在进行evaluate时,使用的模型是不包含损失函数和优化器的网络主体; 在进行evaluate前,需要通过model.set_train(False)将模型置为评估状态,此时Dropout不生效。

from tqdm import tqdm

# 定义模型评估函数
def evaluate(model, test_dataset, criterion, epoch=0):
    # 获取测试数据集的大小
    total = test_dataset.get_dataset_size()
    # 初始化总损失和准确率变量
    epoch_loss = 0
    epoch_acc = 0
    step_total = 0

    # 将模型设置为评估模式,关闭Dropout等特定于训练的特性
    model.set_train(False)

    # 使用tqdm库显示评估进度
    with tqdm(total=total) as t:
        t.set_description('Epoch %i' % epoch)  # 设置进度条描述信息
        # 遍历测试数据集中的所有样本
        for i in test_dataset.create_tuple_iterator():
            # 进行模型预测
            predictions = model(i[0])
            # 计算预测结果和真实标签之间的损失
            loss = criterion(predictions, i[1])
            epoch_loss += loss.asnumpy()  # 累加损失

            # 计算并累加准确率
            acc = binary_accuracy(predictions, i[1])  # 调用二分类准确率计算函数
            epoch_acc += acc

            step_total += 1  # 增加评估步数
            # 更新进度条后缀,显示平均损失和准确率
            t.set_postfix(loss=epoch_loss/step_total, acc=epoch_acc/step_total)
            t.update(1)  # 更新进度条

    # 返回整个测试数据集的平均损失
    return epoch_loss / total

 

模型训练与保存

前序完成了模型构建和训练、评估逻辑的设计,下面进行模型训练。这里我们设置训练轮数为5轮。同时维护一个用于保存最优模型的变量best_valid_loss,根据每一轮评估的loss值,取loss值最小的轮次,将模型进行保存。为节省用例运行时长,此处num_epochs设置为2,可根据需要自行修改。

import os
import mindspore as ms

# 设置训练的轮数
num_epochs = 2

# 初始化最佳验证损失为无穷大
best_valid_loss = float('inf')

# 定义保存模型检查点的文件名和路径
ckpt_file_name = os.path.join(cache_dir, 'sentiment-analysis.ckpt')

# 训练循环,遍历每个epoch
for epoch in range(num_epochs):
    # 训练模型一个epoch
    train_one_epoch(model, imdb_train, epoch)
    
    # 在验证集上评估模型
    valid_loss = evaluate(model, imdb_valid, loss_fn, epoch)
    
    # 如果当前epoch的验证损失小于最佳验证损失,则更新最佳验证损失
    # 并保存当前模型的检查点
    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        ms.save_checkpoint(model, ckpt_file_name)

模型加载与测试

模型训练完成后,一般需要对模型进行测试或部署上线,此时需要加载已保存的最优模型(即checkpoint),供后续测试使用。这里我们直接使用MindSpore提供的Checkpoint加载和网络权重加载接口:1.将保存的模型Checkpoint加载到内存中,2.将Checkpoint加载至模型。

load_param_into_net接口会返回模型中没有和Checkpoint匹配的权重名,正确匹配时返回空列表。

param_dict = ms.load_checkpoint(ckpt_file_name)
ms.load_param_into_net(model, param_dict)

 对测试集打batch,然后使用evaluate方法进行评估,得到模型在测试集上的效果。

imdb_test = imdb_test.batch(64)
evaluate(model, imdb_test, loss_fn)

自定义输入测试

最后我们设计一个预测函数,实现开头描述的效果,输入一句评价,获得评价的情感分类。具体包含以下步骤:

  1. 将输入句子进行分词;
  2. 使用词表获取对应的index id序列;
  3. index id序列转为Tensor;
  4. 送入模型获得预测结果;
  5. 打印输出预测结果。

具体实现如下:

import mindspore as ms
import numpy as np
import mindspore.ops as ops

# 定义情感标签与数字标识的映射关系
score_map = {
    1: "Positive",
    0: "Negative"
}

# 定义预测文本情感的函数
def predict_sentiment(model, vocab, sentence):
    # 将模型设置为评估模式,关闭Dropout等训练特有的层
    model.set_train(False)
    
    # 对输入句子进行预处理:转小写并分词
    tokenized = sentence.lower().split()
    
    # 使用词汇表将分词结果转换为索引序列
    indexed = vocab.tokens_to_ids(tokenized)
    
    # 将索引序列转换为MindSpore Tensor对象,数据类型为int32
    tensor = ms.Tensor(indexed, ms.int32)
    
    # 增加维度,以匹配模型预期的批量大小为1的输入
    tensor = tensor.expand_dims(0)
    
    # 使用模型进行预测,获取未经激活函数处理的原始输出
    prediction = model(tensor)
    
    # 应用sigmoid激活函数将原始输出转换为概率值
    # 四舍五入到最近的整数,将概率映射为具体的标签
    # 返回对应的情感标签
    return score_map[int(np.round(ops.sigmoid(prediction).asnumpy()))]

心得体会:

RNN网络特别适合处理序列数据,通过学习RNN情感分类,我了解到如何利用RNN处理文本信息,捕捉文本中分类,我了解到如何利用RNN处理文本信息,捕捉文本中的时间序列特征。

情感分析在社交媒体监控、产品反馈分析等领域有着重要应用。学习这一技术,我认识到了它在现实世界中的广泛用途。

通过使用预训练的词向量如GloVe,我学习了如何将文本转换为模型能够理解的数值型输入,这有助于提高模型对文本语义的理解。

RNN情感分类让我更深入地理解了模型架构,包括Embedding层、LSTM层、全连接层以及它们在情感分析中的不同作用。

在训练RNN模型时,我学会了如何调试和优化模型参数,以处理梯度消失或爆炸问题,并提高模的泛化能力。

通过使用MindSpore等深度学习框架,我提高了使用这些工具进行模型构建、训练和测试的实践技能。

加油!!!

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用RNN实现情感分类的代码示例: ``` import numpy as np import pandas as pd import tensorflow as tf from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Embedding, LSTM, SpatialDropout1D from sklearn.model_selection import train_test_split # 读取数据集 data = pd.read_csv('sentiment_analysis.csv') data = data[['text', 'sentiment']] data = data[data.sentiment != "Neutral"] data['text'] = data['text'].apply(lambda x: x.lower()) data['text'] = data['text'].apply((lambda x: re.sub('[^a-zA-z0-9\s]', '', x))) # 获取文本和标签 texts = data['text'].values labels = pd.get_dummies(data['sentiment']).values # 对文本进行分词,并将每个词转换为数字 tokenizer = Tokenizer(num_words=2000, split=' ') tokenizer.fit_on_texts(texts) X = tokenizer.texts_to_sequences(texts) X = pad_sequences(X) # 划分数据集 X_train, X_test, Y_train, Y_test = train_test_split(X, labels, test_size=0.33, random_state=42) # 构建RNN模型 embed_dim = 128 lstm_out = 196 model = Sequential() model.add(Embedding(2000, embed_dim, input_length=X.shape[1])) model.add(SpatialDropout1D(0.4)) model.add(LSTM(lstm_out, dropout=0.2, recurrent_dropout=0.2)) model.add(Dense(3, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # 训练模型 batch_size = 32 model.fit(X_train, Y_train, epochs=10, batch_size=batch_size, verbose=2) # 评估模型 score, acc = model.evaluate(X_test, Y_test, verbose=2, batch_size=batch_size) print("score: %.2f" % (score)) print("acc: %.2f" % (acc)) ``` 在这个示例中,我们首先读取数据集并对文本进行预处理。然后,我们使用Tokenizer将文本转换为数字,并使用pad_sequences将每个序列填充到相同的长度。然后,我们将数据集划分为训练集和测试集。 接下来,我们构建RNN模型。我们使用Embedding层将每个数字转换为向量,然后添加SpatialDropout1D层和LSTM层。最后,我们添加一个Dense层,并使用softmax激活函数对输出进行分类。我们使用categorical_crossentropy作为损失函数,使用adam优化器进行训练,并使用accuracy作为评估指标。 最后,我们训练模型并评估其性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值