RNN学习笔记(一)

作为NLP入门必备,在此记录RNN学习历程以及相关细节。

一、RNN理论基础以及文本情感分类

(一)文本的tokenization

        tokenization就是分词的以上,分出的每个词语我们称其为token。

        常见的分词工具:

        jieba:https://github.com/fxsjy/jieba

        常见的中英文分词方法:

        就是将句子转换为词语或单个字。比如‘我爱学习’,可以分为['我','爱','学习']或['我','爱','学','习']

(二)jieba分词工具下载:

        个人理解勿喷,我想着是jieba肯定是一个python包,一般python包都是在你配置环境的site-packages中,那我们就直接下载好jieba的源码以及相关配置文件,然后复制到对应环境中就行了。

        地址已经给出:https://github.com/fxsjy/jieba

        然后正常下载zip就可以。

        解压至一个找得到位置的文件夹:

        找到你使用环境python的安装位置,cmd用where python。

        将文件复制至Lib/site-packages/中,就可以import该包了。

        但实际上有更加快捷的方法就是pip install jieba

        哈哈哈~,如果你只是这样导入,对源码不熟悉的情况下,很难正常使用api,如果用pip导入该包,使用起来就非常轻松啦。 

(三)N-garm表示方法

        N-garm其实指的是N个词语组成的一个小短句,其中N表示每组词语的词语个数。

        具体例子:2-garm

(四)向量化 

        由于文本不能直接被模型计算,必须要将其编码转换为向量。

        主要有两种方法:

        one-hot编码与word embedding

        1.one-hot编码:

        将token转换为一个字典,将每个token编码为长度为N的向量,其中N为字典长度。其实就是把文档用jieba分词或者N-garm方式处理,然后去重得到字典,然后进行编码。

        2.word embedding(常用方法):

        word embedding使用了浮点型的稠密矩阵来表示token。word embedding使用了浮点型的稠密矩阵来表示token。根据词典的大小,我们的向量通常使用不同的维度,例如100,256,300等。其中向量中的每一个值是一个参数,其初始值是随机生成的,之后会在训练的过程中进行学习而获得。

        相比于one-hot编码,word embedding所得到向量构成的矩阵要更加稠密。例如,当一个文本中有一百个不同的词语,经过初步处理后,对于one-hot编码而言,会产生一个100x100的稀疏矩阵。如果使用word embedding,只需要100x30甚至更少。

        主要是将token转换为num,再将num用一个互异的向量表示出来。

        torch.nn中提供了word embedding api,主要传入para为字典大小,embedding的维度

        注意:经过word embedding处理后的数据维度为发生变化。

        例如,每个batch中的每个句子有10个词语,经过形状为[20,4]的Word embedding之后,数据形状变为[batch_size,20,4]。记住word embedding只是一个词典,相当于一个哈希表。

(五)文本情感分类案例

        有一个经典的电影评论数据,简称IMDB数据集

        数据集地址:http://ai.stanford.edu/~amaas/data/sentiment/   

        根据样本利用torch完成模型,进行评论情感预测。

        这个案例可以看作分类问题或回归问题。情感评分可以看成1-10分,可以看成10个类别。在这里,我们把它看作分类问题。主要有四步,准备数据、构建模型、模型训练、模型评估。

        1.准备数据集:

        当然可以用torchtext中的IMDB类,但是由于torchtext要求的版本比较高,安装了torchtext需要更加高版本的torch,如果要使用更加高版本的torch往往需要更高版本的cuda,对电脑要求太高,我这台19年的笔记本最高只能支持11.6版本的cuda,所以就直接下载了IMDB数据集。

        由于每个数据中有一些换行符和制表符,需要把这些符号进行排除,这里需要用到python内置的re库。

        这里需要用到re.sub(pattern(正则中的模式字符串), repl(取代的字符串), string(代替的字符串), count=0, flags=0)。(默默说一句,正则表达式真的抽象)

        定义dataset还是老规矩,先继承Dataset父类,然后定义__getitem__与__len__方法。但是由于IMDB数据集的特殊性,它将每个单一的样本分为一个单独的文件,所以我们为了方便读取,要获取每个train的neg与pos文件路径,当然要分训练模式和测试模式两个情况。IMDB数据集主要划分为train与test两个文件夹,然后分别又划分为neg与pos两个文件夹。然后每个评论数据均是txt文件,文件名为序号_分数。因此我们要做一些操作把标签提取出来,也要把具体文件路径整理出来。

        然后用一个总文件路径列表存放所有文件路径,为了获得文件路径加文件名字的方式,采用os的listdir方法获得文件名字列表,然后用os.path.join方法将路径拼接在一起。

        在__getitem__中为了获取数据标签与数据,采用os.path.basename获取文件名。然后为了获取标签,将文件名利用'_'进行分割,然后取对应的字符串。由于还有.txt后缀,所以需要用'.'继续分割,然后取第一个就是评分。因为是1-10,所以还需要减1将其转化为0-9,方便计算。然后在利用前面定义的tokenize对数据进行处理,可以不用strip(),因为在tokenize已经处理好了。最后返回评分与处理好的文本数据(list)。

        __len__操作只要直接len(总的文件路径列表)就好了。

         最后进行实例化,准备dataloader迭代器即可。

        但出现了问题:每个batch列表中的元素不是一样的长度。

        然后查了博客说是collate_fn参数的问题,因为没有定义这个collate_fn,所以默认使用Dataloader自定义的方法。为了解决这一问题,跟着老师定义了一个collate_fn函数。 

        zip(*batch)相当于解压,将数据解压为列表,batch由labels与text组成。因此collate_fn的功能就是将原本组成的batch元组拆分返回labels与texts,并删除解压后的batch。看似无用,可是当我去看默认的default_collate才知道,如果collate_fn为默认值,Dataloader会将数据集切分为batch_size大小的数据,由于label和text形状不一致,自然会报错且无法切分。

        此时便输出正常:

         但又有一个疑惑出现了,为什么labels是[7,9],在前面的__get__items方法中不是一个int类型的值吗。恍然大悟,因为batch_size是2,所以自然有两个标签哈哈。

        2.将文本序列化:

        在使用word embedding方法时,我们必须要将文本转换为num,然后根据num查询到相关的向量。在这里我们可以用Python的字典去存储对应文本内容和对应序列值,然后根据字典将句子映射为包含数字的列表里。

        当然,由于文本内容比较复杂,我们必须要注意以下问题:

        首先, 如何使用字典把词语和数字进行对应

        其次,不同的词语出现的次数不尽相同,是否需要对高频或者低频词语进行过滤,以及总的词语数量是否需要进行限制

        接着,得到词典之后,如何把句子转化为数字序列,如何把数字序列转化为句子

        然后,其不同句子长度不相同,每个batch的句子如何构造成相同的长度(可以对短句子进行填充,填充特殊字符)

        最后,对于新出现的词语在词典中没有出现怎么办(可以使用特殊字符代理)

        为了解决以上问题,我们首先对句子进行分词,然后将词语存入字典,然后根据次数进对词语进行过滤,并统计次数,实现文本转序列与数字序列转文本的方法。

        具体实现如下:

        定义一个类Word2Sequence将以上操作进行分词。首先

import numpy as np

class Word2Sequence():
    """
    #自然语言处理中常见标识符,<UNK>指低频词或未在词表中的词,<PAD>指补全字符。
    其他:<GO>/<SOS>: 句子起始标识符
        <EOS>: 句子结束标识符
        [SEP]:两个句子之间的分隔符
        [MASK]:填充被掩盖掉的字符
    """
    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  #必须进行fit操作才能进行调用其他函数。

"""将单词转换为num。首先用assert关键字判断是否对象被实例化且进行了fit操作,然后查字典返回num
"""
    def to_index(self,word):
        assert self.fited == True,"必须先进行fit操作"
        return self.dict.get(word,self.UNK) 

"""将num转换为文本。首先用assert关键字判断是否对象被实例化且进行了fit操作,然后查inversed_dict返回文本
"""
    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

        # 比最小的数量大和比最大的数量小的需要。字典推导式,即将在[min_count,max_count]范围的训练加入字典中
        """
        代码可以精简为:
        if not min_count and not max_count:
            count = {k: v for k, v in count.items() if min_count <= v <= max_count}
        """
        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}

        """
        限制最大的数量.isinstance类似于type,传入参数为(object,classinfo),但是该方法会考虑继承关系。在这里的作用就是判断max_feature是否为int。
        如果为int类型,则将count利用sorted进行升序排序,按照values。然后进行判断,max_feature不为none且列表长度大于max_feature,然后将多出的内容利用切片进行删除(删除的是出现频率小的,因为是倒序排序)。然后将count内容进行整理,将每个单词对应的keys设置为一个等差数列,即有序序列,从3开始,字典原本有两个元素了。
        如果对最大长度没有限制,就直接加入self.dict。
        """
        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的字典,将字典的values与keys翻转过来,然后压缩并转换为字典,就是实现了inversed_dict。
        self.inversed_dict = dict(zip(self.dict.values(), self.dict.keys()))
        
        
    def transform(self, sentence,max_len=None):
        """
        实现吧句子转化为数组(向量)
        :param sentence:
        :param max_len:
        :return:
        首先判断是否为fited = True状态。判断max_len是否有传入。传入了该值,然后设置一个r,为一个数值全为1,长度为max_len的列表。如果没有传入,就设置一个长度为传入句子长度的列表。如果句子长度大于max_len,就进行截断操作。然后将句子转变为向量,返回一个np数组。
        """
        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
#测试案例
if __name__ == '__main__':
    w2s = Word2Sequence()
    w2s.fit([
        ["你", "好", "么"],
        ["你", "好", "哦"]])

    print(w2s.dict)
    print(w2s.fited)
    print(w2s.transform(["你","好","嘛"]))
    print(w2s.transform(["你好嘛"],max_len=10))

        简单输出以下结果。

        将IMDB进行worksequence操作:

import pickle
from tqdm import tqdm 

#1. 对IMDB的数据记性fit操作
def fit_save_word_sequence():
    from wordSequence import Word2Sequence #这里将Word2Sequence放到另外一个py文件了
    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)])
    #tqdm是进度条库,需要传入一个iterable,desc是进度条右边的文字
    for cur_path in tqdm(total_file_path_list,desc="fitting"):
        ws.fit(tokenize(open(cur_path,error = 'ignore').read().strip()))
    ws.build_vocab() #在Work2Sequence中添加一个函数,功能为运行fit_save_word_sequence()。这个函数其实就是加载语料库。
    
    # 对wordSequesnce进行保存,pickle就是保存数据,dump串行化(字符流)保存文件,文件必须是可写状态。load从文件中读出数据。
    pickle.dump(ws,open("./model/ws.pkl","wb"))
    

#2. 在dataset中使用wordsequence
fit_save_word_sequence()
ws = pickle.load(open("./model/ws.pkl", "rb"))
    """
    由于在进行文本序列化的操作需要每个文本的长度,所以必须要用lengths去判断是否当前文本长度与MAX_LEN的关系。所以要更改collate_fn函数。
    """
def collate_fn(batch):
    MAX_LEN = 500 
    #MAX_LEN = max([len(i) for i in texts]) #取当前batch的最大值作为batch的最大长度

    batch = list(zip(*batch))
    labes = torch.tensor(batch[0],dtype=torch.int)

    texts = batch[1]
    #获取每个文本的长度
    lengths = [len(i) if len(i)<MAX_LEN else MAX_LEN for i in texts]
    texts = torch.tensor([ws.transform(i, MAX_LEN) for i in texts])
    del batch
    return labes,texts,lengths
    ws = pickle.load(open("./model/ws.pkl","rb"))

#3. 获取输出
    
dataset = ImdbDataset(ws,mode="train")
    dataloader = DataLoader(dataset=dataset,batch_size=20,shuffle=True,collate_fn=collate_fn)
    for idx,(label,text,length) in enumerate(dataloader):
        print("idx:",idx)
        print("table:",label)
        print("text:",text)
        print("length:",length)
        break

         在进行代码执行前,需要将ws对象传入ImdbDataset的__init__中,所以__init__的形参改为(self,ws,mode):

         建立语料库:

        返回结果:

                 3.构建模型:

        使用word embedding方法构建模型。

        nn.functional提供Embedding方法,传入参数为词典长度,人为设定的维度数,自然语言处理中常见标识符。这里要把加载数据与wordSequence封装为build_dataset。这里只有一层embedding。将经过embedding处理的数据传入模型,然后返回log_softmax,得到取对数的log_softmax值。

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import optim
from build_dataset import get_dataloader,ws,MAX_LEN

class IMDBModel(nn.Module):
    def __init__(self,max_len):
        super(IMDBModel,self).__init__()
        self.embedding = nn.Embedding(len(ws),300,padding_idx=ws.PAD) #[N,300] 
        self.fc = nn.Linear(max_len*300,10)  #[max_len*300,10]

    def forward(self, x):
        embed = self.embedding(x) #[batch_size,max_len,300]
        embed = embed.view(x.size(0),-1)
        out = self.fc(embed)
        return F.log_softmax(out,dim=-1)

         4.模型测试:

        分为train和test两个函数:

train_batch_size = 128
test_batch_size = 1000
imdb_model = IMDBModel(MAX_LEN)
optimizer = 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):
        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:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, idx * len(input), len(train_dataloader.dataset),
                       100. * idx / len(train_dataloader), loss.item()))

            torch.save(imdb_model.state_dict(), "model/mnist_net.pkl")
            torch.save(optimizer.state_dict(), 'model/mnist_optimizer.pkl')
            
 def test():
    test_loss = 0
    correct = 0
    mode = False
    imdb_model.eval()
    test_dataloader = get_dataloader(mode, test_batch_size)
    with torch.no_grad():
        for target, input, input_lenght in test_dataloader:
            output = imdb_model(input)
            test_loss  += F.nll_loss(output, target,reduction="sum")
            pred = torch.max(output,dim=-1,keepdim=False)[-1]
            correct = pred.eq(target.data).sum()
        test_loss = test_loss/len(test_dataloader.dataset)
        print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
            test_loss, correct, len(test_dataloader.dataset),
            100. * correct / len(test_dataloader.dataset)))

if __name__ == '__main__':
    test()
    for i in range(3):
        train(i)
        test()

  • 23
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值