RNN学习笔记(二)

承接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时,模型的准确率才有所提高。

  • 33
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值