RNN实现情感分类 AI代码解析
概述
情感分类是自然语言处理中的经典任务,是典型的分类问题。本节使用MindSpore实现一个基于RNN网络的情感分类模型,实现如下的效果:
输入: This film is terrible
正确标签: Negative
预测标签: Negative输入: This film is great
正确标签: Positive
预测标签: Positive
数据准备
本节使用情感分类的经典数据集IMDB影评数据集,数据集包含Positive和Negative两类,下面为其样例:
Review | Label |
---|---|
“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的方式下载临时文件,而后保存至指定的路径并返回。
tqdm
和requests
库需手动安装,命令如下:pip install tqdm requests
import os
import shutil
import requests
import tempfile
from tqdm import tqdm
from typing import IO
from pathlib import Path# 指定保存路径为 `home_path/.mindspore_examples`
cache_dir = Path.home() / '.mindspore_examples'def http_get(url: str, temp_file: IO):"""使用requests库下载数据,并使用tqdm库进行流程可视化"""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): # 以1024字节为单位读取内容if chunk: # 如果读取到内容progress.update(len(chunk)) # 更新进度条temp_file.write(chunk) # 将内容写入临时文件progress.close() # 关闭进度条def download(file_name: str, url: str):"""下载数据并存为指定名称"""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) # 下载文件到临时文件temp_file.flush() # 刷新临时文件temp_file.seek(0) # 将文件指针移动到开头with open(cache_path, 'wb') as cache_file: # 以写入二进制方式打开缓存文件shutil.copyfileobj(temp_file, cache_file) # 将临时文件的内容复制到缓存文件return cache_path # 返回缓存文件的路径
代码解析
- 导入模块:
os
:用于与操作系统进行交互,比如文件路径处理。shutil
:用于文件操作,如复制文件。requests
:用于发送HTTP请求。tempfile
:用于创建临时文件。tqdm
:用于显示进度条。typing
:用于类型注解。pathlib
:用于处理文件路径的现代方式。
- 设置缓存目录:
cache_dir
:设置保存路径为用户主目录下的.mindspore_examples
文件夹。
- **函数 **
http_get
:- 该函数接收一个URL和一个临时文件对象,利用
requests
库下载数据。 - 使用
tqdm
显示下载进度条,iter_content
方法将响应流分块读取,进而写入临时文件。
- 该函数接收一个URL和一个临时文件对象,利用
- **函数 **
download
:- 该函数接收文件名称和URL,检查缓存目录是否存在,不存在则创建。
- 检查指定文件是否已存在于缓存目录,如果不存在,则使用
http_get
方法下载文件,写入到缓存路径。
API 解析
requests.get(url, stream=True)
:- 发送一个GET请求以获取指定URL的响应。
stream=True
表示以流的方式获取响应内容,适用于下载大文件。
- 发送一个GET请求以获取指定URL的响应。
tqdm()
:- 用于创建进度条,
unit='B'
表示单位为字节,total
为下载的总字节数。
- 用于创建进度条,
tempfile.NamedTemporaryFile()
:- 创建一个临时文件,使用完后会自动删除。
shutil.copyfileobj(source, destination)
:- 将源文件对象的内容复制到目标文件对象。用于将临时文件的内容写入缓存文件。
完成数据下载模块后,下载IMDB数据集进行测试(此处使用华为云的镜像用于提升下载速度)。下载过程及保存的路径如下:
# 下载IMDB数据集,指定文件名为 'aclImdb_v1.tar.gz'
imdb_path = download('aclImdb_v1.tar.gz', 'https://mindspore-website.obs.myhuaweicloud.com/notebook/datasets/aclImdb_v1.tar.gz')
imdb_path # 输出下载文件的路径
代码解析
- 下载数据集:
download('aclImdb_v1.tar.gz', 'https://mindspore-website.obs.myhuaweicloud.com/notebook/datasets/aclImdb_v1.tar.gz')
:- 调用之前定义的
download
函数,传入文件名'aclImdb_v1.tar.gz'
和对应的 URL。 - 函数会检查本地缓存目录是否已有该文件,如果没有,则会从指定 URL 下载文件并保存到缓存目录中。
- 调用之前定义的
- 输出结果:
imdb_path
:该变量将存储下载文件的完整路径,可以用于后续的数据处理或分析。
API 解析
download(file_name: str, url: str)
:- 该函数用于下载文件。如果指定的文件已经存在于缓存目录中,则不会重复下载,提升了效率。返回值为下载后的文件路径,便于后续操作。
- URL:
- 提供的数据集URL是一个可公开访问的链接,确保可以成功下载数据集。
加载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
import string
import tarfileclass IMDBData():"""IMDB数据集加载器加载IMDB数据集并处理为一个Python迭代对象。"""label_map = {"pos": 1, # 正面评论标记为1"neg": 0 # 负面评论标记为0}def __init__(self, path, mode="train"):self.mode = mode # 模式(训练/测试)self.path = path # 数据集文件路径self.docs, self.labels = [], [] # 初始化文档和标签列表self._load("pos") # 加载正面评论self._load("neg") # 加载负面评论def _load(self, label):# 正则表达式匹配文件路径pattern = re.compile(r"aclImdb/{}/{}/.*\.txt$".format(self.mode, label))# 将数据加载至内存with tarfile.open(self.path) as tarf: # 打开tar文件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):return self.docs[idx], self.labels[idx] # 根据索引返回文档和标签def __len__(self):return len(self.docs) # 返回文档数量
代码解析
- 导入模块:
re
:用于正则表达式操作。six
:用于兼容Python 2和3的功能。string
:提供一些常量,用于处理字符串。tarfile
:用于处理.tar
文件的读取。
- **类 **
IMDBData
:- 该类用于加载IMDB数据集,并将其处理为可以通过索引访问的Python对象(迭代器)。
- **类属性 **
label_map
:- 定义正面和负面评论的标签映射。
- **构造函数 **
__init__
:- 初始化类实例,设置模式和路径,初始化文档和标签列表。
- 调用
_load
方法加载正面和负面评论。
- **私有方法 **
_load
:- 接收标签(
pos
或neg
),使用正则表达式构建匹配模式。 - 打开指定路径的tar文件,迭代文件列表。
- 如果文件名与模式匹配,读取文件内容,进行去除换行符、去除标点和小写化处理,然后分词并存储到
self.docs
列表中。 - 将对应的标签数字化并存储到
self.labels
列表中。
- 接收标签(
- **方法 **
__getitem__
:- 实现索引访问,返回指定索引的文档和标签。
- **方法 **
__len__
:- 返回文档的数量。
API 解析
tarfile.open(path)
:- 打开一个tar文件,返回tarfile对象,允许对其中的文件进行读取。
tarf.next()
:- 获取tarfile中的下一个文件对象。
tarf.extractfile(tf)
:- 从tarfile中提取指定的文件对象,返回一个文件对象,用于读取文件内容。
six.b(string)
:- 将字符串转换为字节字符串,确保兼容性。
str(...).translate(None, string.punctuation)
:- 去除字符串中的标点符号(该用法在Python 3中已弃用,推荐使用
str.maketrans
)。在Python 3中,可以使用str.translate(str.maketrans('', '', string.punctuation))
来实现相同功能。
- 去除字符串中的标点符号(该用法在Python 3中已弃用,推荐使用
完成IMDB数据加载器后,加载训练数据集进行测试,输出数据集数量:
v
# 创建IMDB数据集加载器实例,加载训练数据集
imdb_train = IMDBData(imdb_path, 'train')# 获取训练数据集的文档数量
len(imdb_train)
代码解析
- **创建实例 **
imdb_train
:IMDBData(imdb_path, 'train')
:调用IMDBData
类的构造函数,传入数据集的路径imdb_path
和模式'train'
,以加载训练集数据。- 该实例会自动调用
_load
方法加载指定路径下的正面和负面评论,并对其进行处理。
- 获取数据集长度:
len(imdb_train)
:调用__len__
方法,返回数据集中文档的数量,即训练集中文档的总数。
API 解析
IMDBData(path, mode)
:- 此构造函数是类
IMDBData
的初始化方法,用于创建数据集加载器实例,并处理数据集。
- 此构造函数是类
__len__()
:- 此方法返回文档的数量,支持使用
len()
函数获取对象的长度。
- 此方法返回文档的数量,支持使用
将IMDB数据集加载至内存并构造为迭代对象后,可以使用mindspore.dataset
提供的Generatordataset
接口加载数据集迭代对象,并进行下一步的数据处理,下面封装一个函数将train和test分别使用Generatordataset
进行加载,并指定数据集中文本和标签的column_name
分别为text
和label
:
import mindspore.dataset as dsdef load_imdb(imdb_path):# 创建训练数据集,使用IMDBData类生成数据,指定列名并打乱顺序imdb_train = ds.GeneratorDataset(IMDBData(imdb_path, "train"), column_names=["text", "label"], shuffle=True)# 创建测试数据集,使用IMDBData类生成数据,指定列名并保持顺序imdb_test = ds.GeneratorDataset(IMDBData(imdb_path, "test"), column_names=["text", "label"], shuffle=False)return imdb_train, imdb_test # 返回训练集和测试集
代码解析
- 导入模块:
import mindspore.dataset as ds
:导入MindSpore的数据集模块,以便使用相关的数据处理和加载功能。
- **函数 **
load_imdb(imdb_path)
:- 定义一个函数,接受参数
imdb_path
,用于加载IMDB数据集。
- 定义一个函数,接受参数
- **创建训练数据集 **
imdb_train
:ds.GeneratorDataset(IMDBData(imdb_path, "train"), column_names=["text", "label"], shuffle=True)
:- 使用
GeneratorDataset
来创建一个数据集实例。 IMDBData(imdb_path, "train")
:调用之前定义的IMDBData
类,加载训练数据集。column_names=["text", "label"]
:指定数据集中包含的列名。shuffle=True
:打乱训练集数据顺序,以增加模型训练的随机性。
- 使用
- **创建测试数据集 **
imdb_test
:ds.GeneratorDataset(IMDBData(imdb_path, "test"), column_names=["text", "label"], shuffle=False)
:- 同样使用
GeneratorDataset
创建测试数据集。 - 加载测试数据集(
"test"
),并不打乱顺序(shuffle=False
),以保持数据的顺序性。
- 同样使用
- 返回数据集:
return imdb_train, imdb_test
:返回训练集和测试集的元组。
API 解析
ds.GeneratorDataset(source, column_names, shuffle)
:- 此API用于创建一个生成数据集的实例,
source
可以是一个生成器、列表或其他可迭代对象。 column_names
指定数据集中各列的名称,shuffle
参数用于控制是否打乱数据顺序。
- 此API用于创建一个生成数据集的实例,
IMDBData(path, mode)
:- 在此上下文中,用于创建IMDB数据集加载器,返回一个可以迭代的对象,供
GeneratorDataset
使用。
- 在此上下文中,用于创建IMDB数据集加载器,返回一个可以迭代的对象,供
加载IMDB数据集,可以看到imdb_train
是一个GeneratorDataset对象。
# 加载IMDB数据集的训练集和测试集
imdb_train, imdb_test = load_imdb(imdb_path)# 输出训练数据集对象
imdb_train
代码解析
- 加载数据集:
load_imdb(imdb_path)
:调用之前定义的load_imdb
函数,传入数据集的路径imdb_path
,返回训练集和测试集。
- 解构返回值:
imdb_train, imdb_test = load_imdb(imdb_path)
:将返回的训练集和测试集分别赋值给imdb_train
和imdb_test
变量。
- 输出训练数据集对象:
imdb_train
:在Jupyter Notebook或其他交互式环境中,直接输入变量名可以查看其内容和结构。这将显示imdb_train
对象的基本信息,如数据集的类型、大小、以及一些样本数据的预览。
API 解析
load_imdb(imdb_path)
:- 此函数用于加载IMDB数据集,并返回两个
GeneratorDataset
实例(训练集和测试集)。
- 此函数用于加载IMDB数据集,并返回两个
imdb_train
:- 这是一个
GeneratorDataset
对象,包含了IMDB训练数据集的文本和标签数据,支持迭代访问和数据处理。通过此对象可以执行数据的批处理、变换等操作。
- 这是一个
加载预训练词向量
预训练词向量是对输入单词的数值化表示,通过nn.Embedding
层,采用查表的方式,输入单词对应词表中的index,获得对应的表达向量。 因此进行模型构造前,需要将Embedding层所需的词向量和词表进行构造。这里我们使用Glove(Global Vectors for Word Representation)这种经典的预训练词向量, 其数据格式如下:
Word | Vector |
---|---|
the | 0.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
import numpy as np
import osdef load_glove(glove_path):# 定义GloVe文件的路径glove_100d_path = os.path.join(cache_dir, 'glove.6B.100d.txt')# 检查GloVe文件是否存在,如果不存在则解压缩if not os.path.exists(glove_100d_path):glove_zip = zipfile.ZipFile(glove_path) # 打开GloVe压缩文件glove_zip.extractall(cache_dir) # 解压到指定目录embeddings = [] # 存储词向量的列表tokens = [] # 存储词汇的列表# 读取GloVe文件with open(glove_100d_path, encoding='utf-8') as gf:for glove in gf:word, embedding = glove.split(maxsplit=1) # 分割每一行,获取词和其对应的词向量tokens.append(word) # 添加词到tokens列表# 将词向量字符串转换为numpy数组,并添加到embeddings列表embeddings.append(np.fromstring(embedding, dtype=np.float32, sep=' '))# 添加 <unk> 和 <pad> 两个特殊占位符对应的词向量embeddings.append(np.random.rand(100)) # <unk>的词向量为随机生成embeddings.append(np.zeros((100,), np.float32)) # <pad>的词向量全为零# 创建词汇表,并指定特殊标记vocab = ds.text.Vocab.from_list(tokens, special_tokens=["<unk>", "<pad>"], special_first=False)# 转换embeddings列表为numpy数组并确保数据类型为float32embeddings = np.array(embeddings).astype(np.float32)return vocab, embeddings # 返回词汇表和词向量数组
代码解析
- 导入模块:
import zipfile
:用于处理ZIP文件的模块。import numpy as np
:导入NumPy库,用于数组和数值计算。import os
:用于文件和目录操作的模块。
- **函数 **
load_glove(glove_path)
:- 定义一个函数,用于加载GloVe词向量。
- 定义GloVe文件路径:
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)
:打开指定的GloVe压缩文件。glove_zip.extractall(cache_dir)
:将压缩文件解压缩到指定的缓存目录。
- 初始化列表:
embeddings = []
:用于存储词向量的列表。tokens = []
:用于存储单词的列表。
- 读取GloVe文件:
with open(glove_100d_path, encoding='utf-8') as gf
:以UTF-8编码打开GloVe文件。for glove in gf
:逐行读取文件。word, embedding = glove.split(maxsplit=1)
:将每行数据分割为单词和对应的词向量。tokens.append(word)
:将单词添加到tokens
列表。embeddings.append(np.fromstring(embedding, dtype=np.float32, sep=' '))
:将词向量字符串转换为NumPy数组并添加到embeddings
列表。
- 添加特殊标记的词向量:
embeddings.append(np.random.rand(100))
:为<unk>
(未知词)生成一个随机的100维词向量。embeddings.append(np.zeros((100,), np.float32))
:为<pad>
(填充词)生成一个全为零的100维词向量。
- 创建词汇表:
vocab = ds.text.Vocab.from_list(tokens, special_tokens=["<unk>", "<pad>"], special_first=False)
:从词汇列表创建一个Vocab对象,并指定特殊标记。
- 转换词向量为NumPy数组:
embeddings = np.array(embeddings).astype(np.float32)
:将词向量列表转换为NumPy数组,并确保其数据类型为float32。
- 返回值:
return vocab, embeddings
:返回词汇表和词向量数组。
API 解析
zipfile.ZipFile(file)
:- 用于打开一个ZIP文件,以便进行解压缩等操作。
np.fromstring(string, dtype, sep)
:- 从字符串中创建NumPy数组,
dtype
指定数组的数据类型,sep
指定字符串的分隔符。
- 从字符串中创建NumPy数组,
ds.text.Vocab.from_list(tokens, special_tokens, special_first)
:- 从给定的词汇列表创建一个Vocab对象,支持特殊标记的添加与管理。
由于数据集中可能存在词表没有覆盖的单词,因此需要加入<unk>
标记符;同时由于输入长度的不一致,在打包为一个batch时需要将短的文本进行填充,因此需要加入<pad>
标记符。完成后的词表长度为原词表长度+2。
下面下载Glove词向量,并加载生成词表和词向量权重矩阵。
# 下载GloVe数据集的压缩文件
glove_path = download('glove.6B.zip', 'https://mindspore-website.obs.myhuaweicloud.com/notebook/datasets/glove.6B.zip')# 加载GloVe词向量和词汇表
vocab, embeddings = load_glove(glove_path)# 输出词汇表的长度
len(vocab.vocab())
代码解析
- 下载GloVe数据集:
glove_path = download('glove.6B.zip', 'https://mindspore-website.obs.myhuaweicloud.com/notebook/datasets/glove.6B.zip')
:- 调用
download
函数,传入要下载的文件名和URL。 - 函数将下载指定的GloVe压缩文件,并返回其本地路径。
- 调用
- 加载GloVe词向量和词汇表:
vocab, embeddings = load_glove(glove_path)
:- 调用之前定义的
load_glove
函数,传入下载的GloVe文件路径。 - 函数返回词汇表对象(
vocab
)和词向量数组(embeddings
)。
- 调用之前定义的
- 输出词汇表的长度:
len(vocab.vocab())
:- 通过
vocab.vocab()
获取词汇表中的所有词汇,并计算其长度。 - 输出的结果是词汇表中包含的单词数量。
- 通过
API 解析
download(file_name, url)
:- 此函数用于从指定的URL下载文件并保存为给定的文件名,通常返回下载后文件的本地路径。
len()
:- 内置函数,用于返回对象(如列表、字符串等)的长度。
vocab.vocab()
:- 对于词汇表对象,调用该方法可以获取所有单词的列表。
使用词表将the
转换为index id,并查询词向量矩阵对应的词向量:
# 将单词 'the' 转换为其在词汇表中的索引
idx = vocab.tokens_to_ids('the')# 根据索引获取该单词对应的词向量
embedding = embeddings[idx]# 输出索引和词向量
idx, embedding
代码解析
- 将单词转换为索引:
idx = vocab.tokens_to_ids('the')
:- 调用
tokens_to_ids
方法,将单词'the'
转换为其在词汇表中的索引。 - 该索引用于在词向量数组
embeddings
中查找对应的词向量。
- 调用
- 获取词向量:
embedding = embeddings[idx]
:- 使用获取到的索引
idx
从embeddings
数组中提取对应的词向量。 - 词向量是一个表示该单词在高维空间中的稠密表示。
- 使用获取到的索引
- 输出索引和词向量:
idx, embedding
:- 以元组的形式返回索引和对应的词向量,便于查看和调试。
API 解析
vocab.tokens_to_ids(token)
:- 此方法用于将给定的单词(token)转换为其在词汇表中的索引,通常返回一个整数值,表示该单词在词汇表中的位置。
embeddings[idx]
:- 从词向量数组中根据索引
idx
获取对应的词向量,返回一个NumPy数组,表示该单词的向量表示。
- 从词向量数组中根据索引
数据集预处理
通过加载器加载的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# 创建词汇查找操作,用于将单词转换为对应的索引,处理未知单词为 <unk>
lookup_op = ds.text.Lookup(vocab, unknown_token='<unk>')# 创建填充操作,将序列填充到固定长度500,填充值为 <pad> 的索引
pad_op = ds.transforms.PadEnd([500], pad_value=vocab.tokens_to_ids('<pad>'))# 创建类型转换操作,将数据类型转换为 float32
type_cast_op = ds.transforms.TypeCast(ms.float32)
代码解析
- 导入MindSpore库:
import mindspore as ms
:- 导入MindSpore深度学习框架,通常用于构建和训练深度学习模型。
- 创建词汇查找操作:
lookup_op = ds.text.Lookup(vocab, unknown_token='<unk>')
:- 创建一个词汇查找操作
Lookup
,用于将词汇表中的单词转换为对应的索引。 vocab
是之前创建的词汇表对象。unknown_token='<unk>'
指定当单词不在词汇表中时,使用的默认值(未知单词的标记)。
- 创建一个词汇查找操作
- 创建填充操作:
pad_op = ds.transforms.PadEnd([500], pad_value=vocab.tokens_to_ids('<pad>'))
:- 创建一个填充操作
PadEnd
,将序列填充到固定长度500。 pad_value=vocab.tokens_to_ids('<pad>')
:使用<pad>
的索引作为填充值,确保所有序列的长度统一。
- 创建一个填充操作
- 创建类型转换操作:
type_cast_op = ds.transforms.TypeCast(ms.float32)
:- 创建一个类型转换操作
TypeCast
,用于将数据类型转换为float32
,以便与MindSpore的计算要求匹配。
- 创建一个类型转换操作
API 解析
ds.text.Lookup(vocab, unknown_token)
:- 创建一个查找操作,将单词映射到其在词汇表中的索引,处理未知单词时使用指定的标记。
ds.transforms.PadEnd(shape, pad_value)
:- 用于填充序列至指定的形状,
shape
指定目标长度,pad_value
是用于填充的值。
- 用于填充序列至指定的形状,
ds.transforms.TypeCast(dst_type)
:- 用于将输入数据转换为指定的数据类型,
dst_type
是目标数据类型。
- 用于将输入数据转换为指定的数据类型,
完成预处理操作后,需将其加入到数据集处理流水线中,使用map
接口对指定的column添加操作。
# 对训练数据集中的 'text' 列应用词汇查找和填充操作
imdb_train = imdb_train.map(operations=[lookup_op, pad_op], input_columns=['text'])# 对训练数据集中的 'label' 列应用类型转换操作
imdb_train = imdb_train.map(operations=[type_cast_op], input_columns=['label'])# 对测试数据集中的 'text' 列应用词汇查找和填充操作
imdb_test = imdb_test.map(operations=[lookup_op, pad_op], input_columns=['text'])# 对测试数据集中的 'label' 列应用类型转换操作
imdb_test = imdb_test.map(operations=[type_cast_op], input_columns=['label'])
代码解析
- 处理训练数据集:
imdb_train = imdb_train.map(operations=[lookup_op, pad_op], input_columns=['text'])
:- 对
imdb_train
数据集中的'text'
列(包含影评文本)应用lookup_op
和pad_op
操作。 lookup_op
将每个单词转换为其对应的索引,而pad_op
将文本序列填充至固定长度(500)。
- 对
imdb_train = imdb_train.map(operations=[type_cast_op], input_columns=['label'])
:- 对训练数据集中的
'label'
列(包含影评标签)应用类型转换操作type_cast_op
。 - 将标签转换为
float32
类型,以确保数据的一致性和模型输入要求。
- 对训练数据集中的
- 处理测试数据集:
imdb_test = imdb_test.map(operations=[lookup_op, pad_op], input_columns=['text'])
:- 对
imdb_test
数据集中的'text'
列应用相同的查找和填充操作。 - 确保测试数据的文本处理与训练数据一致。
- 对
imdb_test = imdb_test.map(operations=[type_cast_op], input_columns=['label'])
:- 对测试数据集中的
'label'
列应用类型转换操作。 - 同样将标签转换为
float32
类型,以便进行后续处理。
- 对测试数据集中的
API 解析
map(operations, input_columns)
:- 此方法用于对数据集的指定列应用一系列操作(
operations
)。 input_columns
指定要处理的列名,操作会逐行应用于这些列。
- 此方法用于对数据集的指定列应用一系列操作(
operations
:- 这是一个包含多个数据变换操作的列表,可以包括查找、填充、类型转换等。
通过这些操作,训练和测试数据集的文本和标签都被预处理,以便为模型的训练和评估做好准备。
由于IMDB数据集本身不包含验证集,我们手动将其分割为训练和验证两部分,比例取0.7, 0.3。
# 将训练数据集 imdb_train 按照 70% 和 30% 的比例进行切分,生成训练集和验证集
imdb_train, imdb_valid = imdb_train.split([0.7, 0.3])
代码解析
- 切分数据集:
imdb_train, imdb_valid = imdb_train.split([0.7, 0.3])
:- 使用
split
方法将imdb_train
数据集按照指定的比例切分为两个子集。 [0.7, 0.3]
表示将数据集的70%用于训练,30%用于验证。- 这两个子集分别被赋值给
imdb_train
和imdb_valid
变量,以便后续使用。
- 使用
API 解析
split(ratios)
:- 此方法用于将数据集按给定的比例分割成多个子集。
ratios
是一个列表,包含每个子集所占的比例,所有比例的和应为1。- 返回值是根据这些比例切分后的数据集。
通过这个步骤,数据集被有效地划分为训练集和验证集,便于在模型训练过程中进行交叉验证和性能评估。
最后指定数据集的batch大小,通过batch
接口指定,并设置是否丢弃无法被batch size整除的剩余数据。
调用数据集的map
、split
、batch
为数据集处理流水线增加对应操作,返回值为新的Dataset类型。现在仅定义流水线操作,在执行时开始执行数据处理流水线,获取最终处理好的数据并送入模型进行训练。
# 将训练数据集 imdb_train 按照批量大小 64 进行分批处理,丢弃最后一个不足一个批次的部分
imdb_train = imdb_train.batch(64, drop_remainder=True)# 将验证数据集 imdb_valid 按照批量大小 64 进行分批处理,丢弃最后一个不足一个批次的部分
imdb_valid = imdb_valid.batch(64, drop_remainder=True)
代码解析
- 批处理训练数据集:
imdb_train = imdb_train.batch(64, drop_remainder=True)
:- 使用
batch
方法将imdb_train
数据集按批量大小64进行分批处理。 drop_remainder=True
表示如果最后一批数据的大小不足64,则丢弃这一部分,不进行处理。
- 使用
- 批处理验证数据集:
imdb_valid = imdb_valid.batch(64, drop_remainder=True)
:- 同样对
imdb_valid
数据集进行批处理,批量大小为64,且同样丢弃剩余不足64的部分。
- 同样对
API 解析
batch(batch_size, drop_remainder)
:- 此方法将数据集分割成多个小批次。
batch_size
是每个批次的样本数量。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.Embedding
的embedding_table
为预训练词向量矩阵。对应的vocab_size
为词表大小400002,embedding_size
为选用的glove.6B.100d
向量大小,即100。
RNN(循环神经网络)
循环神经网络(Recurrent Neural Network, RNN)是一类以序列(sequence)数据为输入,在序列的演进方向进行递归(recursion)且所有节点(循环单元)按链式连接的神经网络。下图为RNN的一般结构:
图示左侧为一个RNN Cell循环,右侧为RNN的链式连接平铺。实际上不管是单个RNN Cell还是一个RNN网络,都只有一个Cell的参数,在不断进行循环计算中更新。
由于RNN的循环特性,和自然语言文本的序列特性(句子是由单词组成的序列)十分匹配,因此被大量应用于自然语言处理研究中。下图为RNN的结构拆解:
RNN单个Cell的结构简单,因此也造成了梯度消失(Gradient Vanishing)问题,具体表现为RNN网络在序列较长时,在序列尾部已经基本丢失了序列首部的信息。为了克服这一问题,LSTM(Long short-term memory)被提出,通过门控机制(Gating Mechanism)来控制信息流在每个循环步中的留存和丢弃。下图为LSTM的结构拆解:
本节我们选择LSTM变种而不是经典的RNN做特征提取,来规避梯度消失问题,并获得更好的模型效果。下面来看MindSpore中nn.LSTM
对应的公式:
h0:t,(ht,ct)=LSTM(x0:t,(h0,c0))ℎ0:𝑡,(ℎ𝑡,𝑐𝑡)=LSTM(𝑥0:𝑡,(ℎ0,𝑐0))
这里nn.LSTM
隐藏了整个循环神经网络在序列时间步(Time step)上的循环,送入输入序列、初始状态,即可获得每个时间步的隐状态(hidden state)拼接而成的矩阵,以及最后一个时间步对应的隐状态。我们使用最后的一个时间步的隐状态作为输入句子的编码特征,送入下一层。
Time step:在循环神经网络计算的每一次循环,成为一个Time step。在送入文本序列时,一个Time step对应一个单词。因此在本例中,LSTM的输出h0:tℎ0:𝑡对应每个单词的隐状态集合,htℎ𝑡和ct𝑐𝑡对应最后一个单词对应的隐状态。
Dense
在经过LSTM编码获取句子特征后,将其送入一个全连接层,即nn.Dense
,将特征维度变换为二分类所需的维度1,经过Dense层后的输出即为模型预测结果。
import math
import mindspore as ms
import mindspore.nn as nn
import mindspore.ops as ops
from mindspore.common.initializer import Uniform, HeUniformclass RNN(nn.Cell):def __init__(self, embeddings, hidden_dim, output_dim, n_layers,bidirectional, pad_idx):# 初始化父类super().__init__()# 获取词汇表大小和嵌入维度vocab_size, embedding_dim = embeddings.shape# 定义嵌入层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)) # 权重初始化方法bias_init = Uniform(1 / math.sqrt(hidden_dim * 2)) # 偏置初始化方法# 定义全连接层self.fc = nn.Dense(hidden_dim * 2, output_dim, weight_init=weight_init, bias_init=bias_init)def construct(self, inputs):# 嵌入输入embedded = self.embedding(inputs)# 通过RNN层_, (hidden, _) = self.rnn(embedded)# 拼接双向LSTM的最后一层隐藏状态hidden = ops.concat((hidden[-2, :, :], hidden[-1, :, :]), axis=1)# 通过全连接层获得输出output = self.fc(hidden)return output
代码解析
- 导入库:
- 导入了
math
库以及 MindSpore 的相关模块:mindspore
、mindspore.nn
、mindspore.ops
和初始化器。
- 导入了
- RNN 类定义:
class RNN(nn.Cell)
:定义一个继承自nn.Cell
的 RNN 模型类。
- 初始化方法:
def __init__(self, embeddings, hidden_dim, output_dim, n_layers, bidirectional, pad_idx)
:- 初始化嵌入层、LSTM 层和全连接层等组件。
embeddings
是预训练的词嵌入矩阵。hidden_dim
是LSTM隐藏层的维度。output_dim
是模型输出的维度。n_layers
是LSTM的层数。bidirectional
表示是否使用双向LSTM。pad_idx
是用于填充的索引。
- 嵌入层:
self.embedding = nn.Embedding(...)
:使用预训练的词向量初始化嵌入层。
- LSTM层:
self.rnn = nn.LSTM(...)
:定义LSTM层的参数,包括输入维度、隐藏维度、层数和是否双向等。
- 全连接层:
self.fc = nn.Dense(...)
:定义全连接层,输出维度为output_dim
。权重和偏置使用特定的初始化方法。
- 构造方法:
def construct(self, inputs)
:定义前向传播逻辑。- 嵌入:
embedded = self.embedding(inputs)
:将输入转换为嵌入表示。
- RNN推理:
_, (hidden, _) = self.rnn(embedded)
:通过LSTM层获得隐藏状态。
- 拼接隐藏状态:
hidden = ops.concat((hidden[-2, :, :], hidden[-1, :, :]), axis=1)
:拼接双向LSTM的最后一层隐藏状态。
- 全连接输出:
output = self.fc(hidden)
:将拼接后的隐藏状态传入全连接层得到最终输出。
API 解析
nn.Embedding
:- 嵌入层,用于将词汇表中的每个词转换为对应的向量表示。
nn.LSTM
:- 实现LSTM层,支持多层、双向等配置。
nn.Dense
:- 全连接层,用于将LSTM的输出映射到最终的输出维度。
ops.concat
:- 用于拼接张量,可以在指定的维度上合并多个张量。
通过以上步骤,这个 RNN 模型能够将输入的文本序列转换为相应的输出,适用于自然语言处理任务,如情感分析等。
损失函数与优化器
完成模型主体构建后,首先根据指定的参数实例化网络;然后选择损失函数和优化器。针对本节情感分类问题的特性,即预测Positive或Negative的二分类问题,我们选择nn.BCEWithLogitsLoss
(二分类交叉熵损失函数)。
# 定义模型的超参数
hidden_size = 256 # 隐藏层大小
output_size = 1 # 输出层大小(对于二分类问题,输出为1)
num_layers = 2 # LSTM层数
bidirectional = True # 是否使用双向LSTM
lr = 0.001 # 学习率
pad_idx = vocab.tokens_to_ids('<pad>') # 获取填充标记的索引# 实例化RNN模型
model = RNN(embeddings, hidden_size, output_size, num_layers, bidirectional, pad_idx)# 定义损失函数
loss_fn = nn.BCEWithLogitsLoss(reduction='mean') # 二元交叉熵损失(带Logits)# 实例化优化器
optimizer = nn.Adam(model.trainable_params(), learning_rate=lr) # Adam优化器
代码解析
- 超参数定义:
hidden_size = 256
:设置LSTM隐藏层的大小为256。output_size = 1
:设置模型的输出维度为1,一般用于二分类问题。num_layers = 2
:设置LSTM的层数为2层。bidirectional = True
:设置LSTM为双向模式,能够更好地捕捉上下文信息。lr = 0.001
:定义学习率为0.001,用于优化器。pad_idx = vocab.tokens_to_ids('<pad>')
:获取填充标记(<pad>
)在词汇表中的索引,方便在嵌入层中使用。
- 模型实例化:
model = RNN(...)
:创建一个RNN
实例,传入预训练的词嵌入、隐藏层大小、输出层大小、LSTM层数、是否双向和填充索引。
- 损失函数定义:
loss_fn = nn.BCEWithLogitsLoss(reduction='mean')
:- 使用带Logits的二元交叉熵损失函数。该损失函数适用于二分类任务,结合了Sigmoid激活函数和交叉熵损失。
reduction='mean'
表示计算出的损失将取平均值。
- 优化器实例化:
optimizer = nn.Adam(model.trainable_params(), learning_rate=lr)
:- 创建一个Adam优化器,传入模型的可训练参数以及学习率。
- Adam优化器能够自适应调整学习率,通常在深度学习中表现良好。
API 解析
vocab.tokens_to_ids(token)
:- 将给定的标记(token)转换为其在词汇表中的索引。
nn.BCEWithLogitsLoss
:- 用于计算带Logits的二元交叉熵损失,适合处理二分类问题。
nn.Adam
:- 实现Adam优化器,可以为模型的可训练参数提供自适应学习率更新。
通过上述步骤,模型、损失函数和优化器得到了有效的初始化,为后续的训练过程做好准备。
训练逻辑
在完成模型构建,进行训练逻辑的设计。一般训练逻辑分为一下步骤:
- 读取一个Batch的数据;
- 送入网络,进行正向计算和反向传播,更新权重;
- 返回loss。
下面按照此逻辑,使用tqdm
库,设计训练一个epoch的函数,用于训练过程和loss的可视化。
def forward_fn(data, label):# 前向传播函数,计算模型的输出和损失logits = model(data) # 通过模型获取logits输出loss = loss_fn(logits, label) # 计算损失return loss# 计算梯度的函数
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 # 返回损失def train_one_epoch(model, train_dataset, epoch=0):# 训练一个epoch的函数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) # 设置当前epoch的描述for i in train_dataset.create_tuple_iterator(): # 遍历训练数据集loss = train_step(*i) # 执行训练步骤loss_total += loss.asnumpy() # 累加损失step_total += 1 # 步骤计数加1t.set_postfix(loss=loss_total/step_total) # 更新损失信息t.update(1) # 更新进度条
代码解析
- 前向传播函数:
def forward_fn(data, label)
:定义一个前向传播的函数,用于计算模型输出和损失。logits = model(data)
:将输入数据送入模型,得到 logits 输出。loss = loss_fn(logits, label)
:计算模型输出与真实标签之间的损失。return loss
:返回计算出的损失。
- 梯度计算函数:
grad_fn = ms.value_and_grad(forward_fn, None, optimizer.parameters)
:- 使用
ms.value_and_grad
来获取forward_fn
的损失值和梯度,用于优化模型参数。
- 使用
- 训练步骤函数:
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)
:定义训练一个epoch的函数。model.set_train()
:将模型设置为训练模式。total = train_dataset.get_dataset_size()
:获取训练数据集的大小。loss_total = 0
和step_total = 0
:初始化损失总计和步骤计数。with tqdm(total=total) as t:
:使用tqdm库显示训练进度条。for i in train_dataset.create_tuple_iterator():
:遍历训练数据集,获取每一个批次的数据和标签。loss = train_step(*i)
:调用训练步骤函数进行训练。loss_total += loss.asnumpy()
:将当前批次的损失累加到总损失中。step_total += 1
:步骤计数加1。t.set_postfix(loss=loss_total/step_total)
:更新进度条上显示的平均损失信息。t.update(1)
:更新进度条显示。
API 解析
ms.value_and_grad
:- 用于自动计算函数的输出值和梯度,方便后续的优化步骤。
train_dataset.create_tuple_iterator()
:- 创建一个迭代器,用于遍历训练数据集中的数据和标签。
optimizer(grads)
:- 调用优化器来更新模型的可训练参数。
tqdm
:- 一个Python库,用于显示进度条,帮助用户直观地了解训练过程的进度。
通过这些步骤,模型将能够在训练数据上进行多次迭代以优化其参数,并在每个epoch结束时显示训练进度和损失情况。
评估指标和逻辑
训练逻辑完成后,需要对模型进行评估。即使用模型的预测结果和测试集的正确标签进行对比,求出预测的准确率。由于IMDB的情感分类为二分类问题,对预测值直接进行四舍五入即可获得分类标签(0或1),然后判断是否与正确标签相等即可。下面为二分类准确率计算函数实现:
def binary_accuracy(preds, y):"""计算每个batch的准确率:param preds: 模型的预测值 (logits):param y: 真实标签:return: 当前batch的准确率"""# 对预测值进行四舍五入rounded_preds = np.around(ops.sigmoid(preds).asnumpy()) # 将logits通过sigmoid函数转换为概率,并四舍五入为0或1correct = (rounded_preds == y).astype(np.float32) # 判断预测值与真实标签是否相等,并转换为float32类型acc = correct.sum() / len(correct) # 计算准确率return acc # 返回准确率
代码解析
- 函数定义:
def binary_accuracy(preds, y)
:定义一个计算二分类准确率的函数。preds
是模型的预测输出(logits),y
是真实标签。
- 预测值四舍五入:
rounded_preds = np.around(ops.sigmoid(preds).asnumpy())
:- 首先使用
ops.sigmoid(preds)
将 logits 转换为概率值。 - 然后使用
np.around()
对概率值进行四舍五入,得到预测的类别(0或1)。
- 首先使用
- 计算正确预测:
correct = (rounded_preds == y).astype(np.float32)
:- 比较四舍五入后的预测值与真实标签
y
是否相等。 - 将布尔值(True或False)转换为
float32
,其中正确的预测为1.0,错误的预测为0.0。
- 比较四舍五入后的预测值与真实标签
- 计算准确率:
acc = correct.sum() / len(correct)
:- 计算正确预测的总数,并除以样本总数,得到准确率。
- 返回准确率:
return acc
:返回当前batch的准确率。
API 解析
ops.sigmoid()
:- 对输入值应用Sigmoid激活函数,将logits转换为概率值,范围在(0, 1)之间。
np.around()
:- NumPy的函数,用于对输入数组进行四舍五入操作。
astype(np.float32)
:- 将数组的数据类型转换为float32,以便后续计算。
通过这个函数,我们可以在每个batch中计算模型的分类准确率,从而评估模型在训练或验证过程中的表现。
有了准确率计算函数后,类似于训练逻辑,对评估逻辑进行设计, 分别为以下步骤:
- 读取一个Batch的数据;
- 送入网络,进行正向计算,获得预测结果;
- 计算准确率。
同训练逻辑一样,使用tqdm
进行loss和过程的可视化。此外返回评估loss至供保存模型时作为模型优劣的判断依据。
在进行evaluate时,使用的模型是不包含损失函数和优化器的网络主体; 在进行evaluate前,需要通过model.set_train(False)
将模型置为评估状态,此时Dropout不生效。
def evaluate(model, test_dataset, criterion, epoch=0):"""评估模型在测试数据集上的表现:param model: 要评估的模型:param test_dataset: 测试数据集:param criterion: 损失函数:param epoch: 当前epoch编号:return: 平均损失"""total = test_dataset.get_dataset_size() # 获取测试数据集的总大小epoch_loss = 0 # 初始化epoch损失epoch_acc = 0 # 初始化epoch准确率step_total = 0 # 初始化步骤计数model.set_train(False) # 设置模型为评估模式with tqdm(total=total) as t: # 使用tqdm显示进度条t.set_description('Epoch %i' % epoch) # 设置当前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 # 步骤计数加1t.set_postfix(loss=epoch_loss/step_total, acc=epoch_acc/step_total) # 更新损失和准确率信息t.update(1) # 更新进度条return epoch_loss / total # 返回平均损失
代码解析
- 函数定义:
def evaluate(model, test_dataset, criterion, epoch=0)
:定义评估模型的函数。model
是要评估的模型,test_dataset
是测试数据集,criterion
是损失函数,epoch
是当前的epoch编号。
- 初始化变量:
total = test_dataset.get_dataset_size()
:获取测试数据集的大小。epoch_loss = 0
和epoch_acc = 0
:初始化当前epoch的总损失和准确率。step_total = 0
:初始化步骤计数。model.set_train(False)
:将模型设置为评估模式,禁用dropout等训练特性。
- 进度条:
with tqdm(total=total) as t:
:创建一个进度条,显示总的测试样本数。t.set_description('Epoch %i' % epoch)
:设置当前epoch的描述信息。
- 遍历测试数据集:
for i in test_dataset.create_tuple_iterator():
:遍历测试数据集,获取每个batch的数据和标签。
- 模型预测和损失计算:
predictions = model(i[0])
:将当前batch的数据输入模型,获取预测值。loss = criterion(predictions, i[1])
:使用损失函数计算预测值与真实标签之间的损失。epoch_loss += loss.asnumpy()
:将当前batch的损失累加到总损失中。
- 准确率计算:
acc = binary_accuracy(predictions, i[1])
:计算当前batch的准确率。epoch_acc += acc
:将当前batch的准确率累加到总准确率中。
- 更新步骤计数和进度条:
step_total += 1
:步骤计数加1。t.set_postfix(loss=epoch_loss/step_total, acc=epoch_acc/step_total)
:更新进度条上显示的损失和准确率信息。t.update(1)
:更新进度条。
- 返回平均损失:
return epoch_loss / total
:返回当前epoch的平均损失。
API 解析
test_dataset.get_dataset_size()
:- 返回测试数据集的样本数量。
model(i[0])
:- 将输入的数据送入模型,得到预测结果。
criterion(predictions, i[1])
:- 计算预测值与真实标签之间的损失。
binary_accuracy(predictions, i[1])
:- 计算当前batch的分类准确率。
tqdm
:- 用于创建进度条,帮助用户实时了解评估进度。
通过这个评估函数,我们能够在测试数据集上评估模型的性能,计算出平均损失和准确率,以便对模型的效果进行分析。
模型训练与保存
前序完成了模型构建和训练、评估逻辑的设计,下面进行模型训练。这里我们设置训练轮数为5轮。同时维护一个用于保存最优模型的变量best_valid_loss
,根据每一轮评估的loss值,取loss值最小的轮次,将模型进行保存。
num_epochs = 5 # 定义训练的总epoch数
best_valid_loss = float('inf') # 初始化最佳验证损失为无穷大
ckpt_file_name = os.path.join(cache_dir, 'sentiment-analysis.ckpt') # 定义模型检查点文件路径# 进行每个epoch的训练和验证
for epoch in range(num_epochs):train_one_epoch(model, imdb_train, epoch) # 执行一个epoch的训练valid_loss = evaluate(model, imdb_valid, loss_fn, epoch) # 在验证集上评估模型# 如果当前验证损失优于最佳验证损失,更新最佳损失并保存模型if valid_loss < best_valid_loss:best_valid_loss = valid_loss # 更新最佳验证损失ms.save_checkpoint(model, ckpt_file_name) # 保存当前模型检查点
代码解析
- 初始化训练参数:
num_epochs = 5
:定义训练的总轮次为5个epoch。best_valid_loss = float('inf')
:初始化最佳验证损失为无穷大,以便在后续比较中总是能更新。ckpt_file_name = os.path.join(cache_dir, 'sentiment-analysis.ckpt')
:构建保存模型检查点的文件路径。
- 训练和验证过程:
for epoch in range(num_epochs):
:使用循环遍历每个epoch。train_one_epoch(model, imdb_train, epoch)
:调用训练函数,进行当前epoch的模型训练。valid_loss = evaluate(model, imdb_valid, loss_fn, epoch)
:在验证集上评估当前模型的性能,返回验证损失。
- 模型保存条件:
if valid_loss < best_valid_loss:
:检查当前验证损失是否低于最佳验证损失。best_valid_loss = valid_loss
:如果当前损失更低,更新最佳验证损失。ms.save_checkpoint(model, ckpt_file_name)
:使用指定的文件名保存当前模型的检查点,以便后续使用。
API 解析
os.path.join(cache_dir, 'sentiment-analysis.ckpt')
:- 将目录路径
cache_dir
和文件名'sentiment-analysis.ckpt'
合并,生成完整的文件路径。
- 将目录路径
train_one_epoch(model, imdb_train, epoch)
:- 训练函数,执行一个epoch的训练,更新模型参数。
evaluate(model, imdb_valid, loss_fn, epoch)
:- 在验证集上评估模型性能,返回当前的验证损失。
ms.save_checkpoint(model, ckpt_file_name)
:- 保存当前模型的检查点,以便在训练后加载或继续训练。
通过这种方式,可以在每个epoch结束后评估模型的性能,并在每次获得更好的表现时自动保存模型,确保在训练过程中不会丢失最佳模型状态。
模型加载与测试
模型训练完成后,一般需要对模型进行测试或部署上线,此时需要加载已保存的最优模型(即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) # 将加载的参数设置到模型中
代码解析
- 加载模型参数:
param_dict = ms.load_checkpoint(ckpt_file_name)
:- 使用
ms.load_checkpoint()
函数从给定的检查点文件ckpt_file_name
中加载模型的参数。 param_dict
是一个字典,包含了模型的所有权重和偏置等参数。
- 使用
- 将参数加载到模型:
ms.load_param_into_net(model, param_dict)
:- 使用
ms.load_param_into_net()
函数将param_dict
中的参数导入到指定的模型model
中。 - 这个函数确保模型的结构与加载的参数匹配,将正确的参数应用到模型的相应层。
- 使用
API 解析
ms.load_checkpoint(ckpt_file_name)
:- 读取指定的检查点文件,返回一个包含模型参数的字典(
param_dict
),用于恢复模型的状态。
- 读取指定的检查点文件,返回一个包含模型参数的字典(
ms.load_param_into_net(model, param_dict)
:- 将从检查点加载的参数字典中的值赋给模型的各个层,确保模型恢复到保存时的状态。
通过这两步操作,我们可以方便地加载之前训练好的模型参数,进行模型的恢复或继续训练,避免从头开始训练。
对测试集打batch,然后使用evaluate方法进行评估,得到模型在测试集上的效果。
imdb_test = imdb_test.batch(64) # 将测试数据集分成每批64个样本进行处理
evaluate(model, imdb_test, loss_fn) # 在测试数据集上评估模型性能
代码解析
- 批处理测试数据集:
imdb_test = imdb_test.batch(64)
:- 将测试数据集
imdb_test
按照每批64个样本进行分组。这样可以高效地进行模型评估,避免一次性加载全部数据导致内存不足。 batch()
方法通常会返回一个新的数据集,包含了指定大小的批次。
- 将测试数据集
- 模型评估:
evaluate(model, imdb_test, loss_fn)
:- 调用
evaluate()
函数,在批处理后的测试数据集上评估模型的表现。 model
是待评估的模型,imdb_test
是经过批处理的数据集,loss_fn
是用于计算损失的函数。
- 调用
API 解析
imdb_test.batch(64)
:- 将数据集
imdb_test
按每64个样本分成一个批次,返回一个新的数据集对象。
- 将数据集
evaluate(model, imdb_test, loss_fn)
:- 在测试集上运行评估过程,返回模型在该数据集上的损失和可能的其它评估指标,帮助了解模型在未见数据上的表现。
通过以上步骤,能够有效地对训练好的模型在测试数据集上进行评估,获取模型的损失等指标,为模型的最终性能提供数据支持。
自定义输入测试
最后我们设计一个预测函数,实现开头描述的效果,输入一句评价,获得评价的情感分类。具体包含以下步骤:
- 将输入句子进行分词;
- 使用词表获取对应的index id序列;
- index id序列转为Tensor;
- 送入模型获得预测结果;
- 打印输出预测结果。
具体实现如下:
score_map = {1: "Positive", # 定义情感得分为1时的标签为“Positive”0: "Negative" # 定义情感得分为0时的标签为“Negative”
}def predict_sentiment(model, vocab, sentence):model.set_train(False) # 设置模型为评估模式,不进行训练tokenized = sentence.lower().split() # 将输入句子转为小写并进行分词indexed = vocab.tokens_to_ids(tokenized) # 将分词结果转换为词汇表的索引tensor = ms.Tensor(indexed, ms.int32) # 将索引列表转换为 MindSpore 的张量tensor = tensor.expand_dims(0) # 在第0维扩展维度,以适应模型输入要求prediction = model(tensor) # 将输入张量传入模型进行预测return score_map[int(np.round(ops.sigmoid(prediction).asnumpy()))] # 返回预测结果对应的情感标签
代码解析
- 定义情感得分映射:
score_map = { 1: "Positive", 0: "Negative" }
:- 创建一个字典,用于将模型的预测结果(0或1)映射为相应的情感标签(“Positive”或“Negative”)。
- **情感预测函数 **
predict_sentiment
:model.set_train(False)
:- 将模型设置为评估模式,通常用于关闭 dropout 等训练期间特有的操作,从而确保预测时的稳定性。
tokenized = sentence.lower().split()
:- 将输入的句子转换为小写,并通过空格进行分词,返回一个词汇的列表。
indexed = vocab.tokens_to_ids(tokenized)
:- 使用词汇表
vocab
将分词后的结果转换为对应的词索引,通常是一个整数列表。
- 使用词汇表
tensor = ms.Tensor(indexed, ms.int32)
:- 将索引列表转换为 MindSpore 库中的张量,并指定数据类型为整型(
int32
)。
- 将索引列表转换为 MindSpore 库中的张量,并指定数据类型为整型(
tensor = tensor.expand_dims(0)
:- 在张量的第0维扩展一个维度,以适应模型的输入要求(通常模型期望输入为批量形式)。
prediction = model(tensor)
:- 将处理后的输入张量传入模型,获取模型的预测结果。
return score_map[int(np.round(ops.sigmoid(prediction).asnumpy()))]
:- 使用
ops.sigmoid
函数将模型的输出转化为概率值(0到1之间),并通过np.round
进行四舍五入,最终映射到情感标签。返回对应的情感标签。
- 使用
API 解析
model.set_train(False)
:- 切换模型的模式到评估状态,确保不进行梯度更新和随机失活等操作。
vocab.tokens_to_ids(tokenized)
:- 将分词的结果转换为对应的词汇表索引,以便后续处理。
ms.Tensor(indexed, ms.int32)
:- 创建一个MindSpore张量,包含来自词汇表的索引,便于模型进行处理。
tensor.expand_dims(0)
:- 扩展张量的维度,以符合模型输入的格式要求。
model(tensor)
:- 执行模型的前向传播,输入为张量,输出为模型的预测结果。
ops.sigmoid(prediction).asnumpy()
:- 对模型的预测结果应用 sigmoid 函数,将其转化为介于0和1之间的概率,并转为NumPy数组。
通过这段代码,用户可以输入一句话,模型将预测其情感倾向,返回“Positive”或“Negative”标签,便于情感分析的应用。
最后我们预测开头的样例,可以看到模型可以很好地将评价语句的情感进行分类。
# 调用预测情感函数,对输入句子进行情感分析
result = predict_sentiment(model, vocab, "This film is terrible")
代码解析
- 函数调用:
result = predict_sentiment(model, vocab, "This film is terrible")
:- 调用
predict_sentiment
函数,输入参数为模型model
、词汇表vocab
,以及待分析的句子"This film is terrible"
。 - 函数将返回该句子的情感倾向,结果存储在变量
result
中。
- 调用
API 解析
predict_sentiment(model, vocab, "This film is terrible")
:- 这是情感预测的核心处理函数,将给定的句子转换为模型可以理解的格式,通过模型进行预测,最后返回对应的情感标签。
最终输出
- 在执行后,
result
变量将包含对句子"This film is terrible"
的情感预测结果,可能的输出为"Negative"
,因为该句表达了负面情感。
通过此调用,用户可以轻松地获取对特定句子的情感分析结果,帮助理解该句的情感倾向。
# 调用预测情感函数,对输入句子进行情感分析
result = predict_sentiment(model, vocab, "This film is great")
代码解析
- 函数调用:
result = predict_sentiment(model, vocab, "This film is great")
:- 调用
predict_sentiment
函数,输入参数为模型model
、词汇表vocab
,以及待分析的句子"This film is great"
。 - 函数将返回该句子的情感倾向,结果存储在变量
result
中。
- 调用
API 解析
predict_sentiment(model, vocab, "This film is great")
:- 这是情感预测的核心处理函数,将给定的句子转换为模型可以理解的格式,通过模型进行预测,最后返回对应的情感标签。
最终输出
- 在执行后,
result
变量将包含对句子"This film is great"
的情感预测结果,可能的输出为"Positive"
,因为该句表达了正面情感。
通过此调用,用户可以轻松地获取对特定句子的情感分析结果,帮助理解该句的情感倾向。