摘要:
记录MindSpore AI框架使用RNN网络对自然语言进行情感分类的过程、步骤和方法。
包括环境准备、下载数据集、数据集加载和预处理、构建模型、模型训练、模型测试等。
一、概念
情感分类。
RNN网络模型
实现效果:
输入: This film is terrible
正确标签: Negative
预测标签: Negative
输入: This film is great
正确标签: Positive
预测标签: Positive
二、环境准备
%%capture captured_output
# 实验环境已经预装了mindspore==2.2.14,如需更换mindspore版本,可更改下面mindspore的版本号
!pip uninstall mindspore -y
!pip install -i https://pypi.mirrors.ustc.edu.cn/simple mindspore==2.2.14
# 查看当前 mindspore 版本
!pip show mindspore
输出:
Name: mindspore
Version: 2.2.14
Summary: MindSpore is a new open source deep learning training/inference framework that could be used for mobile, edge and cloud scenarios.
Home-page: https://www.mindspore.cn
Author: The MindSpore Authors
Author-email: contact@mindspore.cn
License: Apache 2.0
Location: /home/nginx/miniconda/envs/jupyter/lib/python3.9/site-packages
Requires: asttokens, astunparse, numpy, packaging, pillow, protobuf, psutil, scipy
Required-by:
手动安装tqdm和requests库
!pip install tqdm requests
三、加载数据集
IMDB影评数据集
Positive
Negative
预训练词向量
编码自然语言单词
获取文本语义特征
选取Glove词向量作为Embedding。
1.下载数据集和预训练词向量
requests库
http请求
tqdm库
下载百分比进度
IO方式下载临时文件
保存至指定路径并返回
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)
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):
"""下载数据并存为指定名称"""
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
下载IMDB数据集(使用华为云镜像):
imdb_path = download('aclImdb_v1.tar.gz', 'https://mindspore-website.obs.myhuaweicloud.com/notebook/datasets/aclImdb_v1.tar.gz')
imdb_path
输出:
100%|██████████| 84125825/84125825 [00:02<00:00, 38051349.02B/s]
'/home/nginx/.mindspore_examples/aclImdb_v1.tar.gz'
2.IMDB加载模块
Python的tarfile库
读取IMDB数据集tar.gz文件
所有数据和标签分别进行存放
IMDB数据集解压目录如下:
├── aclImdb
│ ├── imdbEr.txt
│ ├── imdb.vocab
│ ├── README
│ ├── test
│ └── train
│ ├── neg
│ ├── pos
...
数据集分两部分加载
train
neg
pos
Test
neg
pos
import re
import six
import string
import tarfile
class IMDBData():
"""IMDB数据集加载器
加载IMDB数据集并处理为一个Python迭代对象。
"""
label_map = {
"pos": 1,
"neg": 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:
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)
3.加载训练数据集
imdb_train = IMDBData(imdb_path, 'train')
len(imdb_train)
输出:
25000
mindspore.dataset.Generatordataset接口
加载数据集迭代对象
加载训练数据集train
加载测试数据集test
指定数据集中文本和标签列名
import mindspore.dataset as ds
def load_imdb(imdb_path):
imdb_train = ds.GeneratorDataset(IMDBData(imdb_path, "train"), column_names=["text", "label"], shuffle=True, num_samples=10000)
imdb_test = ds.GeneratorDataset(IMDBData(imdb_path, "test"), column_names=["text", "label"], shuffle=False)
return imdb_train, imdb_test
imdb_train, imdb_test = load_imdb(imdb_path)
imdb_train
输出:
<mindspore.dataset.engine.datasets_user_defined.GeneratorDataset at 0xfffed0a44fd0>
4.加载预训练词向量
预训练词向量
Glove(Global Vectors for Word Representation)
数值化表示输入单词
构造Embedding层词向量和词表
Glove
nn.Embedding层
查表方式
输入
单词对应词表中的index
输出
对应表达向量
预训练词向量
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
def load_glove(glove_path):
glove_100d_path = os.path.join(cache_dir, 'glove.6B.100d.txt')
if not os.path.exists(glove_100d_path):
glove_zip = zipfile.ZipFile(glove_path)
glove_zip.extractall(cache_dir)
embeddings = []
tokens = []
with open(glove_100d_path, encoding='utf-8') as gf:
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))
embeddings.append(np.zeros((100,), np.float32))
vocab = ds.text.Vocab.from_list(tokens, special_tokens=["<unk>", "<pad>"], special_first=False)
embeddings = np.array(embeddings).astype(np.float32)
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())
输出:
100%|██████████| 862182613/862182613 [00:22<00:00, 38245398.49B/s]
400002
使用词表转换the为index id
查询词向量矩阵对应的词向量
idx = vocab.tokens_to_ids('the')
embedding = embeddings[idx]
idx, embedding
输出:
(0,
array([-0.038194, -0.24487 , 0.72812 , -0.39961 , 0.083172, 0.043953,
-0.39141 , 0.3344 , -0.57545 , 0.087459, 0.28787 , -0.06731 ,
0.30906 , -0.26384 , -0.13231 , -0.20757 , 0.33395 , -0.33848 ,
-0.31743 , -0.48336 , 0.1464 , -0.37304 , 0.34577 , 0.052041,
0.44946 , -0.46971 , 0.02628 , -0.54155 , -0.15518 , -0.14107 ,
-0.039722, 0.28277 , 0.14393 , 0.23464 , -0.31021 , 0.086173,
0.20397 , 0.52624 , 0.17164 , -0.082378, -0.71787 , -0.41531 ,
0.20335 , -0.12763 , 0.41367 , 0.55187 , 0.57908 , -0.33477 ,
-0.36559 , -0.54857 , -0.062892, 0.26584 , 0.30205 , 0.99775 ,
-0.80481 , -3.0243 , 0.01254 , -0.36942 , 2.2167 , 0.72201 ,
-0.24978 , 0.92136 , 0.034514, 0.46745 , 1.1079 , -0.19358 ,
-0.074575, 0.23353 , -0.052062, -0.22044 , 0.057162, -0.15806 ,
-0.30798 , -0.41625 , 0.37972 , 0.15006 , -0.53212 , -0.2055 ,
-1.2526 , 0.071624, 0.70565 , 0.49744 , -0.42063 , 0.26148 ,
-1.538 , -0.30223 , -0.073438, -0.28312 , 0.37104 , -0.25217 ,
0.016215, -0.017099, -0.38984 , 0.87424 , -0.72569 , -0.51058 ,
-0.52028 , -0.1459 , 0.8278 , 0.27062 ], dtype=float32))
四、数据集预处理
预处理
mindspore.dataset接口
Vocab处理
text.Lookup接口
加载词表
指定unknown_token
所有Token对应index id。
文本序列统一长度
PadEnd接口
定义最大长度 此例为500
补齐值(pad_value) 此例为<pad>
不足的<pad>补齐
超出的截断
label数据转为float32格式。
import mindspore as ms
lookup_op = ds.text.Lookup(vocab, unknown_token='<unk>')
pad_op = ds.transforms.PadEnd([500], pad_value=vocab.tokens_to_ids('<pad>'))
type_cast_op = ds.transforms.TypeCast(ms.float32)
定义数据集处理流水线
添加指定column的操作。
map接口
imdb_train = imdb_train.map(operations=[lookup_op, pad_op], input_columns=['text'])
imdb_train = imdb_train.map(operations=[type_cast_op], input_columns=['label'])
imdb_test = imdb_test.map(operations=[lookup_op, pad_op], input_columns=['text'])
imdb_test = imdb_test.map(operations=[type_cast_op], input_columns=['label'])
分割两部分数据集
split接口
训练比例0.7
验证比例0.3
imdb_train, imdb_valid = imdb_train.split([0.7, 0.3])
输出:
[WARNING] ME(281:281473514793264,MainProcess):2024-07-07-02:08:44.142.068 [mindspore/dataset/engine/datasets.py:1203] Dataset is shuffled before split.
打包数据集
batch接口
batch大小
设置是否丢弃剩余数据(无法整除batch size的部分)
imdb_train = imdb_train.batch(64, drop_remainder=True)
imdb_valid = imdb_valid.batch(64, drop_remainder=True)
五、模型构建
情感分类模型结构
nn.Embedding -> nn.RNN -> nn.Dense
输入文本(即序列化后的index id列表)
查表转为向量化表示
nn.Embedding层加载Glove词向量
RNN循环神经网络做特征提取
规避RNN梯度消失问题
LSTM(Long short-term memory)变种
RNN连接全连接层nn.Dense
转化特征为与分类数量相同的size
1.Embedding
Embedding层(EmbeddingLookup)
使用index id查找权重矩阵对应id的向量
输入
index id序列
输出
相同长度矩阵
例如:
# 词表大小(index的取值范围)为1000,表示向量的size为100
embedding = nn.Embedding(1000, 100)
# 序列长度为16
input shape: (1, 16)
output shape: (1, 16, 100)
预训练词向量矩阵glove
nn.Embedding.embedding_table
对应vocab_size 词表大小400002
embedding_size 选用的glove.6B.100d向量大小100
2.RNN(循环神经网络)
RNN循环神经网络
Recurrent Neural Network
输入序列sequence数据
递归recursion序列演进
所有节点(循环单元)按链式连接
RNN一般结构图
左侧
RNN Cell循环
只有一个Cell参数
在循环计算中更新
右侧
RNN链式连接平铺
自然语言处理大量应用RNN
匹配
RNN的循环特性
自然语言文本的序列特性
RNN结构拆解图
RNN单个Cell结构简单
梯度消失(Gradient Vanishing)问题
序列较长时,序列尾部丢失首部信息
提出LSTM(Long short-term memory)
门控机制(Gating Mechanism)
控制信息流在每个循环步中的留存和丢弃
LSTM结构拆解图
MindSpore.nn.LSTM对应公式:
h0:t,(ht,ct)=LSTM(x0:t,(h0,c0))
nn.LSTM隐藏了整个循环神经网络在序列时间步(Time step)上的循环
输入
序列
初始状态
输出
时间步隐状态(hidden state)矩阵
最后一个时间步隐状态
输入下一层
句子的编码特征
时间步Time step
循环神经网络计算的每一次循环,成为一个Time step。
输入
文本序列时,一个Time step对应一个单词
输出
h0:t对应每个单词的隐状态集合
ht,ct对应最后一个单词对应的隐状态
3.Dense
LSTM编码获取句子特征
输入全连接层nn.Dense
变换特征维度为二分类所需的维度1
输出模型预测结果
import math
import mindspore as ms
import mindspore.nn as nn
import mindspore.ops as ops
from mindspore.common.initializer import Uniform, HeUniform
class 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)
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)
_, (hidden, _) = self.rnn(embedded)
hidden = ops.concat((hidden[-2, :, :], hidden[-1, :, :]), axis=1)
output = self.fc(hidden)
return output
4.损失函数与优化器
指定参数实例化网络
选择损失函数和优化器
预测Positive或Negative二分类问题
nn.BCEWithLogitsLoss(二分类交叉熵损失函数)
hidden_size = 256
output_size = 1
num_layers = 2
bidirectional = True
lr = 0.001
pad_idx = vocab.tokens_to_ids('<pad>')
model = RNN(embeddings, hidden_size, output_size, num_layers, bidirectional, pad_idx)
loss_fn = nn.BCEWithLogitsLoss(reduction='mean')
optimizer = nn.Adam(model.trainable_params(), learning_rate=lr)
5.训练逻辑
训练逻辑设计:
读取一个Batch的数据;
输入网络
正向计算
反向传播
更新权重
返回loss
tqdm库
可视化
训练过程
loss
def forward_fn(data, label):
logits = model(data)
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):
model.set_train()
total = train_dataset.get_dataset_size()
loss_total = 0
step_total = 0
with tqdm(total=total) as t:
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)
6.评估指标和逻辑
评估模型
对比
模型预测结果
测试集正确标签
求预测准确率
IMDB的情感二分类准确率实现代码:
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
评估步骤
读取一个Batch的数据;
输入网络
正向计算
预测结果
计算准确率
tqdm可视化
评估过程
Loss
输出评估loss
判断模型优劣
评估时网络模型主体不包含损失函数和优化器
评估前model.set_train(False)设置模型评估状态
def evaluate(model, test_dataset, criterion, epoch=0):
total = test_dataset.get_dataset_size()
epoch_loss = 0
epoch_acc = 0
step_total = 0
model.set_train(False)
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值轮次的模型
num_epochs = 2
best_valid_loss = float('inf')
ckpt_file_name = os.path.join(cache_dir, 'sentiment-analysis.ckpt')
for epoch in range(num_epochs):
train_one_epoch(model, imdb_train, 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)
输出:
Epoch 0: 0%| | 0/109 [00:00<?, ?it/s]
|
Epoch 0: 100%|██████████| 109/109 [13:38<00:00, 7.51s/it, loss=0.692]
Epoch 0: 100%|██████████| 46/46 [00:40<00:00, 1.15it/s, acc=0.524, loss=0.69]
Epoch 1: 100%|██████████| 109/109 [01:23<00:00, 1.31it/s, loss=0.668]
Epoch 1: 100%|██████████| 46/46 [00:13<00:00, 3.38it/s, acc=0.672, loss=0.615]
每轮Loss逐步下降
准确率逐步提升
七、模型加载与测试
模型测试
加载保存的最优模型
加载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)
输出:
Epoch 0: 100%|█████████▉| 390/391 [01:29<00:00, 4.57it/s, acc=0.666, loss=0.619]
-
Epoch 0: 100%|██████████| 391/391 [03:33<00:00, 1.83it/s, acc=0.666, loss=0.619]
0.6193520278881883
八、自定义输入测试
预测函数
输入一句评价
获得评价的情感分类
现实步骤:
输入句子分词;
词表对应的index id序列;
index id序列转为Tensor;
输入模型
获得预测结果;
打印输出预测结果。
实现代码:
score_map = {
1: "Positive",
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)
tensor = tensor.expand_dims(0)
prediction = model(tensor)
return score_map[int(np.round(ops.sigmoid(prediction).asnumpy()))]
predict_sentiment(model, vocab, "This film is terrible")
输出:
'Negative'
predict_sentiment(model, vocab, "This film is great")
输出:
'Positive'