承接RNN学习笔记(一)的内容:在前面的内容,主要了解了一下自然语言处理的常见概念,比如token、tokenization、N-garm等。重点是向量化方法。向量化方法主要有one-hot编码与word embedding方法。word embeddin方法g肯定是更加好的,但是需要我们做好准备工作,要先将数据进行分词,然后设置Dataset与Datalodaer类,存放分词后的数据。然后进行文本序列化工作,其实就是获得字典的过程。然后将对应的字典的形状传入torch.nn.functionals提供的Embedding接口中就好了。
我们会发现,这种将数据输入然后经过多次线性变化或非线性变化,然后输出的神经网络模型都是单向传播的。这时候RNN(Recurrent Neural Network),如何去理解RNN呢。我们可以试想一下我们小学时学习加法的过程。
例如:我们的目标是学会两位数之内的乘法。我们完成一位数加法的学习后,立即去写一道(1+1)题,我们很快的就能得出结论,然后我们不停的去训练个位数加法。终于有一天我们个位数加法的考试取得了不错的成绩。我们为了达成目标,然后我们又要学习两位数的加法,这时候我们不会啊,我们需要去学习新的知识(对于神经网络而言就是去输入数据二位数加法的相关数据)。但是毕竟人脑只有一个(神经网络也是只有一个),你现在是最适合计算个位数的状态(神经网络的参数是最适合计算个位数加法的状态),如果让你学习其他知识(输入其他数据),你的状态肯定是会向着二位数加法那个方向偏移的(神经网络的参数被调整了)。所以,经过其他数据的训练,神经网络已经不适合去计算个位数加法了。这个在现实生活中,叫做遗忘,就是我忘记如何去做这道题了。为了解决这一问题,就有RNN的出现,RNN可以理解为具有短期记忆能力的神经网络,有了这个模型,我们会在个位数加法的计算和两位数加法运算都取得不错的表现。
二、RNN(Recurrent Neural Network)的基本概念:
(一)什么是RNN:
循环神经网络(Recurrent Neural Network,RNN)是一类具有短期记忆能力的神经网络。在循环神经网络中,神经元不但可以接受其它神经元的信息,也可以接受自身的信息,形成具有环路的网络结构。
比如说:RNN学习笔记(一)-CSDN博客 后面建立的word Embedding的隐层,会将输入层的数据进行处理传给输出层。但是在RNN中,它不仅会将该数据传给输出层,还会拿上一次的训练数据传给自己,然后再输出给输出层。也就说,RNN的输入始终会与时间状态有关系。总而言之对于RNN而言,网络的输出结果是该时刻的输入和所有历史共同作用的结果,这就达到了对时间序列建模的目的。
基本RNN模型如下:
下标是不同时序的含义。
(二)LSTM:
LSTM是Long Short Term Memory的简称。虽然RNN是此时刻输入与历史输入的作用结果,但仍然存在Long Term Dependency问题。简单来讲就是,当模型输入一次数据后,所输出的内容是值x,随后输入的数据的输出结果是其他值,过了很久很久,再也没有与x相关的数据输入模型。当我们需要预测x时,由于我们很久没有输入与x有关的数据,导致模型的相关参数对于x的适配度大大下降,即便是输入x相关的数据,我们最后输出的也可能是其他的值。这是因为,输入相关数据的间隔非常大,随着间隔的增加可能会导致真实的预测值对结果的影响变的非常小。
为了解决这个问题就有了LSTM,LSTM可以长期依赖一个信息,对于上面的问题,即便是很久输入一次与x有关的数据,但是因为LSTM的存在,我们仍然可以取得不错的效果。
LSTM单元就是绿色方框中的内容。
LSTM根据上图可以分为遗忘门、输入门和输出门。
1.遗忘门:
遗忘门通过sigmoid函数来判断那些信息应该遗忘、那些信息不应该被遗忘。越接近于1表明这些信息可以通过,接近于0,表明所有信息都会通过。
图中公式可以理解为将上一次的隐层状态和当前输入结果进行合并,然后乘上相应的权重与加上偏置值并求出对于的sigmoid值。并将该值传到上面的横线,如果接近于0,说明上一次的状态对于当前的影响比较小,接近于1则越大。
2.输入门:
tanh激活函数公式如下:
tanh会将数据映射至(-1,1)区间,对于输入门而言,就是将sigmoid计算得到的值与tanh计算得到的向量进行相乘,然后与遗忘门得到的值相加,就更新了神经元状态。用能理解的话来将就是决定新的信息是否应该被保留,tanh会创造一个候选向量,候选向量的值可能会被放入状态中。
3.输出门:
输出门决定什么信息会被输出,ot是上一个隐层状态对这次输出的影响,将会乘以当前神经元状态tanh(Ct)得到此次的输出结果。
总的来说,就是利用sigmoid去判断上次隐层状态对于这次神经元状态与输出结果的影响,tanh是将数据映射至(-1,1)的区间内。
以上图片来自于:https://colah.github.io/posts/2015-08-Understanding-LSTMs/
(三)GRU:
GRU(Gate Recurrent Unit)是LSTM的一种变体,相比于LSTM而言,其更加容易计算。它将遗忘门和整合为一个更新门,将神经元状态Ct与隐层状态ht整合在一起。
(四)双向LSTM:
单向的 RNN,是根据前面的信息推出后面的,但有时候只看前面的词是不够的, 可能需要预测的词语和后面的内容也相关,那么此时需要一种机制,能够让模型不仅能够从前往后的具有记忆,还需要从后往前需要记忆。此时双向LSTM就可以帮助我们解决这个问题。
三、循环神经网络实现文本情感分类:
(一)LSTM与GRU的api
torch.nn中提供了LSTM与GRU的相关api。
1.torch.nn.LSTM
torch.nn.LSTM,需要传入参数:
input_size:为输入数据形状
hidden_size:隐藏层神经元个数(即每层有多少个LSTM单元)
num_layer:整个神经网络中LSTM的层数
batch_first:输入数据的顺序(值为False,则输入数据需要[seq_len,batch,feature]。为True,则输入数据需要[batch,seq_len,feature])
dropout:dropout指的是为了解决过拟合问题,让部分参数随机失活的方法。
bidirectional:为True则使用双向LSTM。
在实例化LSTM之后,每次使用该对象都要传入数据和上一次的隐藏状态和上一次的记忆。
LSTM的输出为output(形状为[seq_len,batch,num_directions*hidden_size]),(ht,Ct)的shape都为([num_layers * num_directions, batch, hidden_size])
num_directions其实就是数据方向,对于LSTM而言,默认为1。对于双向LSTM是2。
为什么形状是这样的呢,output会将计算的结果与历史结果在第2轴进行拼接,ht隐层状态与历史结果在第0轴进行拼接。
实践代码如下:
batch_size =10
seq_len = 20
embedding_dim = 30
word_vocab = 100
hidden_size = 18
num_layer = 2
"""
输入数据的形状为[10,20]
embedding形状为[100,30]
embed形状为[10,20,30],因为原输入数据形状是[10,20],
embedding用一个长度为30的向量表示每一个元素,固形状变为[10,20,30]。
"""
#准备输入数据
input = torch.randint(low=0,high=100,size=(batch_size,seq_len))
#准备embedding
embedding = torch.nn.Embedding(word_vocab,embedding_dim)
lstm = torch.nn.LSTM(embedding_dim,hidden_size,num_layer)
#进行embed操作
embed = embedding(input) #[10,20,30]
#转化数据为batch_first=False,即形状为[seq_len, batch_size, embedding_dim]
embed = embed.permute(1,0,2) #[20,10,30]
#初始化状态, 如果不初始化,torch默认初始值为全0
h_0 = torch.rand(num_layer,batch_size,hidden_size)
c_0 = torch.rand(num_layer,batch_size,hidden_size)
output,(h_1,c_1) = lstm(embed,(h_0,c_0))
输出结果如下:
对于LSTM、双向LSTM、GRU,它们最后一个time_step的输出的前hidden_size个和最后隐层状态h是一致的。对于双向LSTM的后向LSTM,它的最后一个time_step的输出的后hidden_size个和最后一层后向传播的h_1输出相同。
双向LSTM只需要将bidirectional显式设置为True,然后输入的h0与C0的形状为[num_layer*2,batch_size,hidden_size]。但输出有所不同,output会将正反计算的结果在第2轴进行拼接,正向第一个结果和反向最后一个结果进行拼接。ht隐层状态在第0轴按照正向、反向的顺序进行拼接。
2.torch.nn.GRU:
与LSTMapi差不多,但是在实例化对象后,仅需要传入上一个隐层状态,参加GRU的原理。
3.注意事项:
第一次调用需要初始化隐层与记忆状态,若使用的是GRU,则只用初始化隐层状态。
(二) 运用LSTM完成文本情感分类:
为了达到更好的效果,对前面的模型进行修改:
将MAX_LEN = 200,将数据转换为2分类问题,pos为1,neg为0,因为2.5个样本完成10个类别的划分数据量是不够的,实例化LSTM将dropout设置为0.5,防止过拟合问题,在评估模式下会自动变为0。
解决了各种报错,最值得记录一下的几个报错为:
cuda error:device-side assert triggered
就是说GPU可能觉得有问题,但是不会报出详细信息。此时换成cpu运行程序就能发现真正的问题。
数据经过embedding层报错,index out of range in self。查了博客说是张量内部有超出embedding层合法范围的数。我是将传入常量的字典长度+1就通过了该问题。
计算带权损失时:expected scalar type Long but found Int。这个查博客将label类型改成torch.LongTensor类型。因为F.nll_loss要求传入的参数为torch.LongTensor类型。因为我是二分类任务哈哈,所以在collate_fn中将labels类型设置为torch.int64。
定要用gpu计算,cpu算半年。我真怕我的老年笔记本爆炸....
就迭代一次测试加训练我都花了7、8min哈哈。
好吧,要显存7g左右,我的才4g。该换电脑了....还是用cpu计算吧哈哈。
完整可运行代码如下:
1.tokenize1,用于删除无关字符然后返回一个列表:
#1. 定义tokenize的方法
import re
def tokenize1(text):
# fileters = '!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n'
fileters = ['!','"','#','$','%','&','\(','\)','\*','\+',',','-','\.','/',':',';','<','=','>','\?','@'
,'\[','\\','\]','^','_','`','\{','\|','\}','~','\t','\n','\x97','\x96','”','“',]
"""
flag = re.S 即为'.'并且包括换行符在内的任意字符
.是匹配除换行符以外的字符,*?是重复任意次。
re.sub("<.*?>"," ",text,flags=re.S)的意思是 将< >字符替换为空,
原因是因为IMDB是从网络上复制下来的,因此会带有HTML的脚本语言。
re.sub("|".join(fileters)," ",text,flags=re.S)的意思是将fileters进行或,然后替换为空。
简单来说就是判断文本是否有这些符号,有的话替换为空。
"""
text = re.sub("<.*?>"," ",text,flags=re.S)
text = re.sub("|".join(fileters)," ",text,flags=re.S)
return [i.strip() for i in text.split()]
2.Word2Sequence,用于文本序列化:
import numpy as np
class Word2Sequence():
UNK_TAG = "UNK"
PAD_TAG = "PAD"
UNK = 0
PAD = 1
def __init__(self):
self.dict = {
self.UNK_TAG :self.UNK,
self.PAD_TAG :self.PAD
}
self.fited = False
def to_index(self,word):
"""word -> index"""
assert self.fited == True,"必须先进行fit操作"
return self.dict.get(word,self.UNK)
def to_word(self,index):
"""index -> word"""
assert self.fited , "必须先进行fit操作"
if index in self.inversed_dict:
return self.inversed_dict[index]
return self.UNK_TAG
def __len__(self):
return len(self.dict)
def fit(self, sentences, min_count=1, max_count=None, max_feature=None):
"""
:param sentences:[[word1,word2,word3],[word1,word3,wordn..],...]
:param min_count: 最小出现的次数
:param max_count: 最大出现的次数
:param max_feature: 总词语的最大数量
:return:
"""
count = {}
for sentence in sentences:
for a in sentence:
if a not in count:
count[a] = 0
count[a] += 1
# 比最小的数量大和比最大的数量小的需要
if min_count is not None:
count = {k: v for k, v in count.items() if v >= min_count}
if max_count is not None:
count = {k: v for k, v in count.items() if v <= max_count}
# 限制最大的数量
if isinstance(max_feature, int):
count = sorted(list(count.items()), key=lambda x: x[1])
if max_feature is not None and len(count) > max_feature:
count = count[-int(max_feature):]
for w, _ in count:
self.dict[w] = len(self.dict)
else:
for w in sorted(count.keys()):
self.dict[w] = len(self.dict)
self.fited = True
# 准备一个index->word的字典
self.inversed_dict = dict(zip(self.dict.values(), self.dict.keys()))
def transform(self, sentence,max_len=None):
"""
实现吧句子转化为数组(向量)
:param sentence:
:param max_len:
:return:
"""
assert self.fited, "必须先进行fit操作"
if max_len is not None:
r = [self.PAD]*max_len
else:
r = [self.PAD]*len(sentence)
if max_len is not None and len(sentence)>max_len:
sentence=sentence[:max_len]
for index,word in enumerate(sentence):
r[index] = self.to_index(word)
return np.array(r,dtype=np.int64)
def inverse_transform(self,indices):
"""
实现从数组 转化为文字
:param indices: [1,2,3....]
:return:[word1,word2.....]
"""
sentence = []
for i in indices:
word = self.to_word(i)
sentence.append(word)
return sentence
3.get_loader,用于处理并加载数据,得到dataloader:
import torch
from torch.utils.data import DataLoader, Dataset
import pickle
import os
from tokenize1 import tokenize1
import numpy as np
data_base_path = r"data\aclImdb"
def get_dataloader(mode, train_tbatch_size):
class ImdbDataset(Dataset):
def __init__(self, ws, mode):
super(ImdbDataset, self).__init__()
self.mode = mode
if mode == "train":
text_path = [os.path.join(data_base_path, i) for i in ["train/neg", "train/pos"]]
else:
text_path = [os.path.join(data_base_path, i) for i in ["test/neg", "test/pos"]]
self.total_file_path_list = []
for i in text_path:
self.total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
def __getitem__(self, idx):
cur_path = self.total_file_path_list[idx]
cur_filename = os.path.basename(cur_path)
label = int(cur_filename.split("_")[-1].split(".")[0]) - 1 # 处理标题,获取label,转化为从[0-9]
label = 0 if 0 <= label <= 4 else 1
text = tokenize1(open(cur_path, errors='ignore').read().strip())
return label, text
def __len__(self):
return len(self.total_file_path_list)
ws = pickle.load(open("./model", "rb"))
def collate_fn(batch):
MAX_LEN = 200
batch = list(zip(*batch))
labes = torch.tensor(batch[0], dtype=torch.int64)
texts = batch[1]
lengths = [len(i) if len(i) < MAX_LEN else MAX_LEN for i in texts]
texts = torch.tensor(np.array([ws.transform(i, MAX_LEN) for i in texts]))
del batch
return labes, texts, lengths
dataset = ImdbDataset(ws, mode="train")
dataloader = DataLoader(dataset=dataset, batch_size=train_tbatch_size, shuffle=True, collate_fn=collate_fn)
return dataloader
4.fit_save_word_sequence,用于保存语料库,直接运行一次就好了,会多出一个model语料库:
import os
from tqdm import tqdm
import pickle
from Word2Sequence import Word2Sequence
from tokenize1 import tokenize1
data_base_path = r"data\aclImdb"
def fit_save_word_sequence():
ws = Word2Sequence()
train_path = [os.path.join(data_base_path,i) for i in ["train/neg","train/pos"]]
total_file_path_list = []
for i in train_path:
total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
for cur_path in tqdm(total_file_path_list,ascii=True,desc="fitting"):
ws.fit(tokenize1(open(cur_path,errors='ignore').read().strip()))
# 对wordSequesnce进行保存
pickle.dump(ws,open("./model","wb"))
5. set_Module,建立神经网络,该神经网络除去输入输出层有三层,embedding层、LSTM层、全连接层1、全连接层2:
import torch
from torch import nn
from torch.nn import functional as F
import pickle
device = torch.device('cpu')
class IMDBLstmmodel(nn.Module):
def __init__(self):
super(IMDBLstmmodel,self).__init__()
self.hidden_size = 64
self.embedding_dim = 200
self.num_layer = 2
self.bidriectional = True
self.bi_num = 2 if self.bidriectional else 1
self.dropout = 0.5
#以上部分为超参数,可以自行修改
ws = pickle.load(open("./model", "rb"))
self.embedding = nn.Embedding(len(ws) + 10,self.embedding_dim,padding_idx=ws.PAD) #[N,300]
self.lstm = nn.LSTM(self.embedding_dim,self.hidden_size,self.num_layer,bidirectional=True,dropout=self.dropout)
#使用两个全连接层,中间使用relu激活函数
self.fc = nn.Linear(self.hidden_size*self.bi_num,20)
self.fc2 = nn.Linear(20,2)
def forward(self, x):
x = self.embedding(x)
x = x.permute(1,0,2) #进行轴交换
h_0,c_0 = self.init_hidden_state(x.size(1))
_,(h_n,c_n) = self.lstm(x,(h_0,c_0))
#只要最后一个lstm单元处理的结果,这里多去的hidden state
out = torch.cat([h_n[-2, :, :], h_n[-1, :, :]], dim=-1)
out = self.fc(out)
out = F.relu(out)
out = self.fc2(out)
return F.log_softmax(out,dim=-1)
def init_hidden_state(self,batch_size):
h_0 = torch.rand(self.num_layer * self.bi_num, batch_size, self.hidden_size).to(device)
c_0 = torch.rand(self.num_layer * self.bi_num, batch_size, self.hidden_size).to(device)
return h_0,c_0
6.train_and_test:
from set_Module import IMDBLstmmodel
import torch
from torch import nn
from torch.nn import functional as F
from get_loader import get_dataloader
from fit_save_word_sequence import fit_save_word_sequence
device = torch.device('cpu')
train_batch_size = 64
test_batch_size = 5000
# imdb_model = IMDBModel(MAX_LEN) #基础model
imdb_model = IMDBLstmmodel().to(device) # 在gpu上运行,提高运行速度
optimizer = torch.optim.Adam(imdb_model.parameters())
criterion = nn.CrossEntropyLoss()
def train(epoch):
mode = True
imdb_model.train(mode)
train_dataloader = get_dataloader(mode, train_batch_size)
for idx, (target, input, input_lenght) in enumerate(train_dataloader):
target = target.to(device)
input = input.to(device)
optimizer.zero_grad()
output = imdb_model(input)
loss = F.nll_loss(output, target) # traget需要是[0,9],不能是[1-10]
loss.backward()
optimizer.step()
if idx % 10 == 0:
pred = torch.max(output, dim=-1, keepdim=False)[-1]
acc = pred.eq(target.data).cpu().numpy().mean() * 100.
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\t ACC: {:.6f}'.format(epoch, idx * len(input),
len(train_dataloader.dataset),
100. * idx / len(
train_dataloader),
loss.item(), acc))
if epoch == 10:
torch.save(imdb_model.state_dict(), "./model1")
torch.save(optimizer.state_dict(), './optim')
def test():
mode = False
imdb_model.eval()
test_dataloader = get_dataloader(mode, test_batch_size)
with torch.no_grad():
for idx, (target, input, input_lenght) in enumerate(test_dataloader):
target = target.to(device)
input = input.to(device)
output = imdb_model(input)
test_loss = F.nll_loss(output, target, reduction="mean")
pred = torch.max(output, dim=-1, keepdim=False)[-1]
correct = pred.eq(target.data).sum()
acc = 100. * pred.eq(target.data).cpu().numpy().mean()
print('idx: {} Test set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(idx, test_loss, correct,
target.size(0), acc))
if __name__ == "__main__":
test()
for i in range(10):
train(i)
test()
事实上,dropout太高也不行,如果我们所输入的数据太少,我们将一半以上的神经元都设置失活,这样的设置实际上会导致模型拟合度较差,准确率也较低。当我将dropout设置为0时,模型的准确率才有所提高。