🍊本文旨在以一种通俗易懂的方式来介绍RNN,本文从天气预报开始引入RNN,随后对RNN进行详细介绍,并对几种变体的LSTM、GRU、BiLSTM进行重点介绍。最后以4个实验进行实战训练
🍊实验一:Pytorch+Bert+RNN实现文本分类预测模拟
🍊实验二:Pytorch+Bert+RNN实现对IMDB影评数据集进行二分类情感分析
🍊实验三:Pytorch+Bert+GRU实现对IMDB影评数据集进行二分类情感分析
🍊实验四:Pytorch+Bert+LSTM实现对IMDB影评数据集进行二分类情感分析
🍊实验四:Pytorch+Bert+BILSTM实现对IMDB影评数据集进行二分类情感分
一、Introduction
假如现在我们需要预测明天的天气,我们该怎么做呢?首先我们需要采集前几天的气温、降水、云量等情况,随后对这些数据进行处理。因为是分类预测问题,最简单的数据处理方式是将所有的数据放到一个一维的向量中并投入FNN中。听起来还不错,但是有一个问题是怎么多天的各种天气数据合起来参数太多了,俺们普通人的家伙压根儿跑不起来!
那么如何简化呢?再次审视该问题,明天的天气是基于前几天的天气情况的,也就是有时间序列的概念,那么如果有一个网络模型每次训练时可使用之前的训练的信息就好了。此外关于参数过多问题,我们在CNN中处理的策略是共享权重,所以这个网络模型如果还有共享权重的功能就更棒了。能够同时实现上述两种的功能的网络,就是RNN
RNN(Recurrent Neural Network),循环神经网络,它具有短期记忆功能,可以同时接受其他神经元和自身神经元的信息。通过时间反向传播算法进行参数学习。可扩展到兼容性更强的记忆网络模型(递归神经网络和图网络)
很多时候RNN也被翻译成递归神经网络,其实这样的叫法是不正确的,因为递归神经网络RecNN(Recursive Neural Network)是空间上的递归,而RNN是时间上的循环,二者是完全不同的两种神经网络模型
二、Principle
2.1 RNN network model
给定一个输入序列𝒙1∶𝑇 = (𝒙1 , 𝒙2 , ⋯ , 𝒙𝑡 , ⋯ , 𝒙𝑇),随后进入隐藏层获取延迟器(类似于记忆块,存储了最近几次隐藏层的活性值)中的信息对序列进行处理,最后输出,其参数更新公式如下
其模型图如下
FNN可以模拟任何的连续函数,而RNN强到可以模拟任何程序,因为它有动力系统(时间+函数)
具体的计算如下
观察该公式,数学功底好的小伙伴可能已经发现RNN的本质其实就是FNN线性层,输入输出维度分别为[input_sieze,hidden_sieze],上面的公式和标准的线性层还是有区别的,我们将其w和b进行变换如下图中所示,这样的效果更加直观
2.2 Activaiton function
仔细的小伙伴可以发现Hidden层的激活函数我们使用了Tanh函数(其图像如下),但是为什么不使用Sigmoid呢?这是因为Sigmoid的导数范围在[0,0.25]之内,而Tanh的导数范围在(0,1]。RNN需要不断的循环,导数值小的更容易导致梯度消失(详看2.7)
2.3 Example
假如有以下的网络,网络权重参数w全部为1,权重b为0,无激活函数,当我们输入[1,1] [2,2] [1,1]的时候会输出什么呢?
第一个时间状态时。Memory块中并没有数据因此Hidden数据为1+1+0+0=2。并将Hidden的数据存储到Memory中
第二个时间状态时,Hidden数据为2+2+2+2=8
第三个时间状态时,Hidden数据为1+1+8+8=18
输入两次[1,1],但是输出不同,而且第二个[1,1]使用了第一个[1,1]中的信息,说明模型具有时序记忆功能
2.4 LSTM
长短期记忆神经网络Long Short-term Memory,注意 -- 符号位于short和term之间,所以你可以理解为较长的短期记忆神经网络
相比于RNN,其不同之处在于在网络传播的过程中,部分网络有概率传播截断了,有选择的加入新的信息、遗忘信息。我们将其称为门控机制。门控机制可改善RNN的长程依赖问题
LSTM主要有Output Gate、Input Gate和Memory Gate。所有门控函数其输出值只有0和1,因此激活函数使用的是Sigmoid,网络自行学习来判断门的开闭
假设我们在三个门分别输入z,zo和zi,让我们来看看门控机制是如何运行的吧
1 首先是f(zi),若其值为0,则f(zi)*g(z)为0,表示输入无效
2 其次是cf(zf),c为先前的记忆块,如果f(zf)为0,则记忆门生效,表示遗忘先前记忆
3 最后是f(zo),若其值为0,则h(c')*f(zo)为0,表示输出无效
2.5 GRU
GRU是LSTM的简化版,在数据集较少的时候,二者效果基本差不多,因此我们考虑硬件的算力和时间成本时,偏向于GRU
GRU的核心思想为只关注记住了多少信息,遗忘了多少信息。因此GRU只有两个门:更新门(RNN中的输入门和记忆门)和重置门
计算过程解析
1 首先计算更新门和重置门的数值
2 上一层hidden与重置门相乘,决定遗忘了多少信息,再与当前的信息进行相加
3 (1-更新门)与hidden相乘,决定记住了多少信息,再与输出2乘更新门进行相乘,得到最终的hidden
2.6 BiLSTM
BiLSTM是LSTM的升级版,核心思想是训练的特征可以同时获取过去和将来的信息
其网络模型分为两个独立的LSTM(神经网络参数是独立的),将初始序列分别以正序和逆序输入到LSTM中,最后将两个输出的向量拼接在一起形成最终的词向量
目前论文中的神经网络往往会加一层BiLSTM来获取更加有效的特征表示
2.7 RNN long-range dependency issue
在Training RNN的时候,往往会发现损失值并非随着层数的增加而慢慢减少,而是在不断跳动
主要原因是RNN有一个最大的问题--长程依赖。RNN会不断的进行迭代,假如有1.01或者0.99这个数字,当循环了1000次之后,1.01会变成2w,0.99接近于0。因此数值要么变得无限的大(梯度爆炸),要么变得无限小(梯度消失)
我们将所有的Loss绘制出来可以发现,3D图陡峭和平坦位置两级分化,如下图中所示
对于梯度爆炸问题,可以设置一个梯度阈值进行限制
对于梯度消失问题,根本问题是连乘公式,因此可以将其改成带加法的连乘如残差网络和门控机制(因此在我们往往使用LSTM而不使用经典RNN)
三、Experiment
3.1 代码技巧
Pytorch提供了封装好的RNN、LSTM、GRU、BiLSTM模块
cell = torch.nn.RNNCell(input_size=input_size, hidden_size=hidden_size,num_layers=num_layers)
输入参数
- input_size:每个单词维度
- hidden_size:隐含层的维度(不一定要等于句子长度)
- num_layers:RNN层数,默认是1,单层LSTM
- bias:是否使用bias
- batch_first:默认为False,如果设置为True,则表示第一个维度表示的是batch_size
- dropout:随机失活
- bidirectional:是否使用BiLSTM
输出参数
output, (hn, cn) = lstm(inputs)
output_last = output[:,-1,:]
- output:每个时间步输出
- output_last:最后一个时间步隐藏层神经元输出,一般是我们最终要获取的数据
- hn:最后一个时间步隐藏层的状态
- cn:最后一个时间步隐藏层的遗忘门值
num_layers代表RNN的层数
3.2 实验一:文本分类预测模拟
问题:设计一个RNN网络模型,实现输入的数据是happy,输出预测数据为phayp
题目解析
首先我们需要将每个母转化为词向量,最常见的词向量为One-Hot编码 。通过One-Hot后每个单词的维度为4
输入文本序列通过RNN之后要想得到paphy需要进行分类预测,因此需要加一层Softmax,最后与target计算交叉熵损失函数进行时间序列反向传播
Code
import torch
# Paramaters
INPUT_SIZE = 4
HIDDEN_SIZE = 4
BATCH_SIZE = 1
NUM_LAYERS = 1
SEQ_LEN = 5
# Prepare for the data
words_bag = ['a', 'h', 'p', 'y']
one_hot_bag = [[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]]
input = [1, 0, 2, 2, 3]
target = [2, 1, 0, 3, 2]
input_one_hot = [one_hot_bag[x] for x in input]
inputs = torch.Tensor(input_one_hot).view(SEQ_LEN, BATCH_SIZE, INPUT_SIZE)
targets = torch.LongTensor(target)
print('inputs.shape', inputs.shape, 'targets.shape', targets.shape)
# Design model
class Model(torch.nn.Module):
def __init__(self, INPUT_SIZE, HIDDEN_SIZE, BATCH_SIZE, NUM_LAYERS):
super(Model, self).__init__()
self.input_size = INPUT_SIZE
self.hidden_size = HIDDEN_SIZE
self.batch_size = BATCH_SIZE
self.num_layers = NUM_LAYERS
self.rnn = torch.nn.RNN(input_size=self.input_size, hidden_size=self.hidden_size,
num_layers=self.num_layers)
def forward(self, input):
# Every batch must have h0
hidden = torch.zeros(self.num_layers,
self.batch_size,
self.hidden_size)
out, _ = self.rnn(input, hidden)
# Inorder to make Cross_Entropy with targets
out = out.view(-1, self.hidden_size)
return out
rnn_model = Model(INPUT_SIZE, HIDDEN_SIZE, BATCH_SIZE, NUM_LAYERS)
# Define the model and loss
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(rnn_model.parameters(), lr=0.05)
# Training
for epoch in range(20):
outputs = rnn_model(inputs)
loss = criterion(outputs, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
_, idx = outputs.max(dim=1)
idx=idx.data.numpy()
print('Predict:',''.join([words_bag[x] for x in idx]),end='')
print(',Epoch [%d/20] loss=%.3f' % (epoch+1,loss.item()))
Result
从结果可以看到损失值在不断的减少,从Epoch8开始预测值就已经成功是phayp了
3.3 非模型代码
由于本篇文章主要讲解关于RNN的内容。因此弱化了非网络模型部分代码,感兴趣的小伙伴可以看这篇完整项目的文章 IMDB文本分析实战
该项目已在Github上开源,开箱即用,代码地址:sentiment_analysis_Imdb
对于网络模型来说,只能接受数字数据类型,因此我们需要建立一个语言模型,目的是将每个单词变成一个向量,每个句子变成一个矩阵。关于语言模型,其已经发展历史非常悠久了(发展历史如下),我们可以直接使用这些模型
由于Bert模型比较主流(虽然它本身已经用了RNN的方法),但是它用起来也比较方便,效果也好,因此本篇文章采用的语言模型为Bert
以下是配置文件config.py,看论文源代码的大牛们似乎都这么写,博主也就照葫芦画瓢
import os
import sys
import time
import torch
import random
import logging
import argparse
from datetime import datetime
def get_config():
parser = argparse.ArgumentParser()
'''Base'''
parser.add_argument('--num_classes', type=int, default=2)
parser.add_argument('--model_name', type=str, default='bert',
choices=['bert', 'roberta', 'glove', 'fasttext', 'word2vce', 'elmo', 'gpt'])
parser.add_argument('--method_name', type=str, default='gru',
choices=['gru', 'rnn', 'bilstm', 'textcnn', 'rnn', 'bert_transformer'])
'''Optimization'''
parser.add_argument('--train_batch_size', type=int, default=8)
parser.add_argument('--test_batch_size', type=int, default=32)
parser.add_argument('--num_epoch', type=int, default=50)
parser.add_argument('--lr', type=float, default=1e-5)
parser.add_argument('--weight_decay', type=float, default=0.01)
'''Environment'''
parser.add_argument('--device', type=str, default='cuda')
parser.add_argument('--backend', default=False, action='store_true')
parser.add_argument('--workers', type=int, default=0)
parser.add_argument('--timestamp', type=int, default='{:.0f}{:03}'.format(time.time(), random.randint(0, 999)))
args = parser.parse_args()
args.device = torch.device(args.device)
'''logger'''
args.log_name = '{}_{}_{}.log'.format(args.model_name, args.method_name,
datetime.now().strftime('%Y-%m-%d_%H-%M-%S')[2:])
if not os.path.exists('logs'):
os.mkdir('logs')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler(sys.stdout))
logger.addHandler(logging.FileHandler(os.path.join('logs', args.log_name)))
return args, logger
以下是数据集Data.py文件。主要功能是加载数据集,并制作对应的DataSet和DataLoader
from functools import partial
import pandas as pd
import torch
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader
# Make MyDataset
class MyDataset(Dataset):
def __init__(self, sentences, labels, method_name, model_name):
self.sentences = sentences
self.labels = labels
self.method_name = method_name
self.model_name = model_name
dataset = list()
index = 0
for data in sentences:
tokens = data.split(' ')
labels_id = labels[index]
index += 1
dataset.append((tokens, labels_id))
self._dataset = dataset
def __getitem__(self, index):
return self._dataset[index]
def __len__(self):
return len(self.sentences)
# Make tokens for every batch
def my_collate(batch, tokenizer):
tokens, label_ids = map(list, zip(*batch))
text_ids = tokenizer(tokens,
padding=True,
truncation=True,
max_length=320,
is_split_into_words=True,
add_special_tokens=True,
return_tensors='pt')
return text_ids, torch.tensor(label_ids)
# Load dataset
def load_dataset(tokenizer, train_batch_size, test_batch_size, model_name, method_name, workers):
data = pd.read_csv('datasets.csv', sep=None, header=0, encoding='utf-8', engine='python')
len1 = int(len(list(data['labels'])) * 0.1)
labels = list(data['labels'])[0:len1]
sentences = list(data['sentences'])[0:len1]
# split train_set and test_set
tr_sen, te_sen, tr_lab, te_lab = train_test_split(sentences, labels, train_size=0.8)
# Dataset
train_set = MyDataset(tr_sen, tr_lab, method_name, model_name)
test_set = MyDataset(te_sen, te_lab, method_name, model_name)
# DataLoader
collate_fn = partial(my_collate, tokenizer=tokenizer)
train_loader = DataLoader(train_set, batch_size=train_batch_size, shuffle=True, num_workers=workers,
collate_fn=collate_fn, pin_memory=True)
test_loader = DataLoader(test_set, batch_size=test_batch_size, shuffle=True, num_workers=workers,
collate_fn=collate_fn, pin_memory=True)
return train_loader, test_loader
以下是Main.py文件,主要功能是加载数据集,并使用run方法进行训练和测试
import torch
import torch.nn as nn
from tqdm import tqdm
from transformers import logging, AutoTokenizer, AutoModel
from config import get_config
from data import load_dataset
from model import Transformer, Gru_Model, BiLstm_Model, Lstm_Model, Rnn_Model
class Niubility:
def __init__(self, args, logger):
self.args = args
self.logger = logger
self.logger.info('> creating model {}'.format(args.model_name))
# Operate the model
if args.model_name == 'bert':
self.tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
base_model = AutoModel.from_pretrained('bert-base-uncased')
elif args.model_name == 'roberta':
self.tokenizer = AutoTokenizer.from_pretrained('roberta-base', add_prefix_space=True)
base_model = AutoModel.from_pretrained('roberta-base')
else:
raise ValueError('unknown model')
# Operate the method
if args.method_name == 'bert_transformer':
self.Mymodel = Transformer(base_model, args.num_classes)
elif args.method_name == 'gru':
self.Mymodel = Gru_Model(base_model, args.num_classes)
elif args.method_name == 'lstm':
self.Mymodel = Lstm_Model(base_model, args.num_classes)
elif args.method_name == 'bilstm':
self.Mymodel = BiLstm_Model(base_model, args.num_classes)
else:
self.Mymodel = Rnn_Model(base_model, args.num_classes)
self.Mymodel.to(args.device)
if args.device.type == 'cuda':
self.logger.info('> cuda memory allocated: {}'.format(torch.cuda.memory_allocated(args.device.index)))
self._print_args()
def _print_args(self):
self.logger.info('> training arguments:')
for arg in vars(self.args):
self.logger.info(f">>> {arg}: {getattr(self.args, arg)}")
def _train(self, dataloader, criterion, optimizer):
train_loss, n_correct, n_train = 0, 0, 0
# Turn on the train mode
self.Mymodel.train()
for inputs, targets in tqdm(dataloader, disable=self.args.backend, ascii='>='):
inputs = {k: v.to(self.args.device) for k, v in inputs.items()}
targets = targets.to(self.args.device)
predicts = self.Mymodel(inputs)
loss = criterion(predicts, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item() * targets.size(0)
n_correct += (torch.argmax(predicts, dim=1) == targets).sum().item()
n_train += targets.size(0)
return train_loss / n_train, n_correct / n_train
def _test(self, dataloader, criterion):
test_loss, n_correct, n_test = 0, 0, 0
# Turn on the eval mode
self.Mymodel.eval()
with torch.no_grad():
for inputs, targets in tqdm(dataloader, disable=self.args.backend, ascii=' >='):
inputs = {k: v.to(self.args.device) for k, v in inputs.items()}
targets = targets.to(self.args.device)
predicts = self.Mymodel(inputs)
loss = criterion(predicts, targets)
test_loss += loss.item() * targets.size(0)
n_correct += (torch.argmax(predicts, dim=1) == targets).sum().item()
n_test += targets.size(0)
return test_loss / n_test, n_correct / n_test
def run(self):
train_dataloader, test_dataloader = load_dataset(tokenizer=self.tokenizer,
train_batch_size=self.args.train_batch_size,
test_batch_size=self.args.test_batch_size,
model_name=self.args.model_name,
method_name=self.args.method_name,
workers=self.args.workers)
_params = filter(lambda x: x.requires_grad, self.Mymodel.parameters())
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(_params, lr=self.args.lr, weight_decay=self.args.weight_decay)
# Get the best_loss and the best_acc
best_loss, best_acc = 0, 0
for epoch in range(self.args.num_epoch):
train_loss, train_acc = self._train(train_dataloader, criterion, optimizer)
test_loss, test_acc = self._test(test_dataloader, criterion)
if test_acc > best_acc or (test_acc == best_acc and test_loss < best_loss):
best_acc, best_loss = test_acc, test_loss
self.logger.info(
'{}/{} - {:.2f}%'.format(epoch + 1, self.args.num_epoch, 100 * (epoch + 1) / self.args.num_epoch))
self.logger.info('[train] loss: {:.4f}, acc: {:.2f}'.format(train_loss, train_acc * 100))
self.logger.info('[test] loss: {:.4f}, acc: {:.2f}'.format(test_loss, test_acc * 100))
self.logger.info('best loss: {:.4f}, best acc: {:.2f}'.format(best_loss, best_acc * 100))
self.logger.info('log saved: {}'.format(self.args.log_name))
if __name__ == '__main__':
logging.set_verbosity_error()
args, logger = get_config()
nb = Niubility(args, logger)
nb.run()
3.4 实验二:Pytorch+RNN 二分类情感分析
问题:搭建RNN模型实现对IMDB数据集进行分类
RNN网络模型代码如下
Init:
self.base_model:使用语言模型
self.num_classes:预测类的数量
self.Rnn:Rnn模型,input_size为bert默认的单词维度,hidden_size为句子长度,当然hidden_size不一定要和句子长度一样,num_layers使用一层Rnn就行了
self.fc:使用常规FNN+Softmax即可
forward:
前三行代码:获取bert语言模型每个单词的token值。因为本篇文章主要讲RNN,因此大家如果不知道Bert就不必深究了
x, _ = self.Gru(tokens):x获取的是最终隐藏层的表示,_表示该位置的参数用不到
x = self.fc(x):进行全连接分类
class Rnn_Model(nn.Module):
def __init__(self, base_model, num_classes):
super().__init__()
self.base_model = base_model
self.num_classes = num_classes
self.Rnn = nn.RNN(input_size=768,
hidden_size=320,
num_layers=1,
batch_first=True)
self.fc = nn.Sequential(nn.Dropout(0.5),
nn.Linear(320, 80),
nn.Linear(80, 20),
nn.Linear(20, self.num_classes),
nn.Softmax(dim=1))
for param in base_model.parameters():
param.requires_grad = (True)
def forward(self, inputs):
raw_outputs = self.base_model(**inputs)
cls_feats = raw_outputs.last_hidden_state
outputs, _ = self.Rnn(cls_feats)
outputs = outputs[:, -1, :]
outputs = self.fc(outputs)
return outputs
3.5 实验三:Pytorch+GRU 二分类情感分析
问题:搭建GRU模型实现对IMDB数据集进行分类
GRU模块基本上与RNN模块一致,需要修改的地方在于nn.RNN换成nn.GRU
class Gru_Model(nn.Module):
def __init__(self, base_model, num_classes):
super().__init__()
self.base_model = base_model
self.num_classes = num_classes
self.Gru = nn.GRU(input_size=768,
hidden_size=320,
num_layers=1,
batch_first=True)
self.fc = nn.Sequential(nn.Dropout(0.5),
nn.Linear(320, 80),
nn.Linear(80, 20),
nn.Linear(20, self.num_classes),
nn.Softmax(dim=1))
for param in base_model.parameters():
param.requires_grad = (True)
def forward(self, inputs):
raw_outputs = self.base_model(**inputs)
tokens = raw_outputs.last_hidden_state
gru_output, _ = self.Gru(tokens)
outputs = gru_output[:, -1, :]
outputs = self.fc(outputs)
return outputs
3.6 实验四:Pytorch+LSTM 二分类情感分析
问题:搭建LSTM模型实现对IMDB数据集进行分类
LSTM模块基本上与RNN模块一致,需要修改的地方在于nn.RNN换成nn.LSTM
class Lstm_Model(nn.Module):
def __init__(self, base_model, num_classes):
super().__init__()
self.base_model = base_model
self.num_classes = num_classes
self.Lstm = nn.LSTM(input_size=768,
hidden_size=320,
num_layers=1,
batch_first=True)
self.fc = nn.Sequential(nn.Dropout(0.5),
nn.Linear(320, 80),
nn.Linear(80, 20),
nn.Linear(20, self.num_classes),
nn.Softmax(dim=1))
for param in base_model.parameters():
param.requires_grad = (True)
def forward(self, inputs):
raw_outputs = self.base_model(**inputs)
tokens = raw_outputs.last_hidden_state
lstm_output, _ = self.Lstm(tokens)
outputs = lstm_output[:, -1, :]
outputs = self.fc(outputs)
return outputs
3.7 实验五:Pytorch+BILSTM 二分类情感分析
问题:搭建BILSTM模型实现对IMDB数据集进行分类
BiLstm为双向lstm,在lstm的基础上修改两处地方
第一处:在nn.LSTM中添加bidirectional=True
第二处:在nn.fc的输入维度需要*2
class BiLstm_Model(nn.Module):
def __init__(self, base_model, num_classes):
super().__init__()
self.base_model = base_model
self.num_classes = num_classes
# Open the bidirectional
self.BiLstm = nn.LSTM(input_size=768,
hidden_size=320,
num_layers=1,
batch_first=True,
bidirectional=True)
self.fc = nn.Sequential(nn.Dropout(0.5),
nn.Linear(320 * 2, 80),
nn.Linear(80, 20),
nn.Linear(20, self.num_classes),
nn.Softmax(dim=1))
for param in base_model.parameters():
param.requires_grad = (True)
def forward(self, inputs):
raw_outputs = self.base_model(**inputs)
cls_feats = raw_outputs.last_hidden_state
outputs, _ = self.BiLstm(cls_feats)
outputs = outputs[:, -1, :]
outputs = self.fc(outputs)
return outputs
3.8 Result
到了最快乐的炼丹时间,看看最终的效果怎么样
分析
- LSTM与BiLSTM的效果总体上要优于其他模型
- Roberta比Bert的效果好,Roberta不愧是升级版Bert
- 使用FNN的本身的效果就比较好是因为Bert本身带有LSTM的网络层
- Roberta+LSTM/BiLSTM的组合效果是最优的
参考资料
《机器学习》周志华
《深度学习与机器学习》吴恩达
《神经网络与与深度学习》邱锡鹏
《Pytorch深度学习实战》刘二大人