精华内容
下载资源
问答
  • 基于TensorFlow实现的闲聊机器人 使用TensorFlow实现的Sequence to Sequence的聊天机器人模型
  • 闲聊机器人的优化

    2021-04-25 17:23:11
    闲聊机器人的优化 1. seq2seq中使用teacher forcing 在前面的seq2seq的案例中,介绍了teacher frocing是什么,当时输入和输出很相似,所以当时我们的teacher forcing是在每个time step中实现的,那么现在我们的...

    闲聊机器人的优化

    1. seq2seq中使用teacher forcing

    在前面的seq2seq的案例中,介绍了teacher frocing是什么,当时输入和输出很相似,所以当时我们的teacher forcing是在每个time step中实现的,那么现在我们的输入和输出不同的情况下,该如何使用呢?

    我们可以在每个batch遍历time step的外层使用teacher forcing

    代码如下:

    use_teacher_forcing = random.random() > 0.5
    if use_teacher_forcing:  # 使用teacher forcing
        for t in range(config.max_len):
            decoder_output_t, decoder_hidden, decoder_attn_t = self.forward_step(decoder_input, decoder_hidden,
                                                                                 encoder_outputs)
            decoder_outputs[:, t, :] = decoder_output_t
            # 使用正确的输出作为下一步的输入
            decoder_input = target[:, t].unsqueeze(1)  # [batch_size,1]
    
    else:  # 不使用teacher forcing,使用预测的输出作为下一步的输入
        for t in range(config.max_len):
            decoder_output_t, decoder_hidden, decoder_attn_t = self.forward_step(decoder_input, decoder_hidden,
                                                                                 encoder_outputs)
            decoder_outputs[:, t, :] = decoder_output_t
            value, index = torch.topk(decoder_output_t, 1)  # index [batch_size,1]
            decoder_input = index
    

    2. 使用梯度裁剪

    前面学习了梯度消失(梯度过小,在多层计算后导致其值太小而无法计算)梯度爆炸(梯度过大,导致其值在多层的计算后太大而无法计算)

    在常见的深度神经网络中,特别是RNN中,经常会使用梯度裁剪的手段,来抑制过大的梯度,能够有效防止梯度爆炸。

    梯度裁剪的实现非常简单,仅仅只需要设置一个阈值,把梯度大于该阈值时设置为该阈值。

    实现代码:

    loss.backward()
    # 进行梯度裁剪
    nn.utils.clip_grad_norm_(model.parameters(), [5, 10, 15])
    optimizer.step()
    

    3. 其他优化方法

    1. 根据特定的问题,使用分类模型进行训练,然后再训练单独的回个该为题的为模型

      • 比如询问名字,可以使用fasttext先进行意图识别,命中询问名字分类后,直接返回名字

      • 或者是手动构造和名字相关的很多问题,来进行训练,从而能够更加个性化的回答出结果

    2. 直接对现有的语料进行修改和清洗,把语料中更多的答案进行替换,比如咨询名字的,咨询天气的等,这样能够更大程度上的回答出更加规范的答案

    3. 使用beam()搜索模型,不再使用这种生成模型

    展开全文
  • Seq2Seq实现闲聊机器人

    2021-02-20 00:27:54
    Seq2Seq实现闲聊机器人 目标 知道如何处理文本数据 知道如何使用seq2seq完成闲聊机器人代码的编写 1. 准备训练数据 单轮次的聊天数据非常不好获取,所以这里我们从github上使用一些开放的数据集来训练我们的闲聊...

    Seq2Seq实现闲聊机器人

    目标

    1. 知道如何处理文本数据
    2. 知道如何使用seq2seq完成闲聊机器人代码的编写

    1. 准备训练数据

    单轮次的聊天数据非常不好获取,所以这里我们从github上使用一些开放的数据集来训练我们的闲聊模型

    数据地址:https://github.com/codemayq/chaotbot_corpus_Chinese

    主要的数据有两个:

    1. 小黄鸡的聊天语料:噪声很大
      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FGhJBdLq-1613752059958)(…/images/2.3/小黄鸡语料.png)]
    2. 微博的标题和评论:质量相对较高
      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GBguZB9e-1613752059965)(…/images/2.3/微博语料1.png)]
      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lPdxGevx-1613752059968)(…/images/2.3/微博语料2.png)]

    2. 数据的处理和保存

    由于数据中存到大量的噪声,可以对其进行基础的处理,然后分别把input和target使用两个文件保存,即input中的第N行尾问,target的第N行为答

    后续可能我们可能会把单个字作为特征(存放在input_word.txt),也可能会把词语作为特征(input.txt)

    2.1 小黄鸡的语料的处理

    def format_xiaohuangji_corpus(word=False):
        """处理小黄鸡的语料"""
        if word:
            corpus_path = "./chatbot/corpus/xiaohuangji50w_nofenci.conv"
            input_path = "./chatbot/corpus/input_word.txt"
            output_path = "./chatbot/corpus/output_word.txt"
        else:
    
            corpus_path = "./chatbot/corpus/xiaohuangji50w_nofenci.conv"
            input_path = "./chatbot/corpus/input.txt"
            output_path = "./chatbot/corpus/output.txt"
    
        f_input = open(input_path,"a")
        f_output = open(output_path,"a")
        pair = []
        for line in tqdm(open(corpus_path),ascii=True):
            if line.strip() == "E":
                if not pair:
                    continue
                else:
                    assert len(pair) == 2,"长度必须是2"
                    if len(pair[0].strip())>=1 and len(pair[1].strip())>=1:
                        f_input.write(pair[0]+"\n")
                        f_output.write(pair[1]+"\n")
                    pair = []
            elif line.startswith("M"):
                line = line[1:]
                if word:
                    pair.append(" ".join(list(line.strip())))
                else:
                    pair.append(" ".join(jieba_cut(line.strip())))
    
    

    2.2 微博语料的处理

    def format_weibo(word=False):
        """
        微博数据存在一些噪声,未处理
        :return:
        """
        if word:
            origin_input = "./chatbot/corpus/stc_weibo_train_post"
            input_path = "./chatbot/corpus/input_word.txt"
    
            origin_output = "./chatbot/corpus/stc_weibo_train_response"
            output_path = "./chatbot/corpus/output_word.txt"
    
        else:
            origin_input = "./chatbot/corpus/stc_weibo_train_post"
            input_path = "./chatbot/corpus/input.txt"
    
            origin_output = "./chatbot/corpus/stc_weibo_train_response"
            output_path = "./chatbot/corpus/output.txt"
    
        f_input = open(input_path,"a")
        f_output = open(output_path, "a")
        with open(origin_input) as in_o,open(origin_output) as out_o:
            for _in,_out in tqdm(zip(in_o,out_o),ascii=True):
                _in = _in.strip()
                _out = _out.strip()
    
                if _in.endswith(")") or _in.endswith("」") or _in.endswith(")"):
                    _in = re.sub("(.*)|「.*?」|\(.*?\)"," ",_in)
                _in = re.sub("我在.*?alink|alink|(.*?\d+x\d+.*?)|#|】|【|-+|_+|via.*?:*.*"," ",_in)
    
                _in = re.sub("\s+"," ",_in)
                if len(_in)<1 or len(_out)<1:
                    continue
    
                if word:
                    _in = re.sub("\s+","",_in)  #转化为一整行,不含空格
                    _out = re.sub("\s+","",_out)
                    if len(_in)>=1 and len(_out)>=1:
                        f_input.write(" ".join(list(_in)) + "\n")
                        f_output.write(" ".join(list(_out)) + "\n")
                else:
                    if len(_in) >= 1 and len(_out) >= 1:
                        f_input.write(_in.strip()+"\n")
                        f_output.write(_out.strip()+"\n")
    
        f_input.close()
        f_output.close()
    

    2.3 处理后的结果

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NKBJBo1N-1613752059981)(…/images/2.3/文本处理后.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXUNNRkr-1613752059985)(…/images/2.3/文本处理后2.png)]

    3. 构造文本序列化和反序列化方法

    和之前的操作相同,需要把文本能转化为数字,同时还需实现方法把数字转化为文本

    # word_sequence.py
    import config
    import pickle
    
    class Word2Sequence():
        UNK_TAG = "UNK"
        PAD_TAG = "PAD"
        SOS_TAG = "SOS"
        EOS_TAG = "EOS"
    
        UNK = 0
        PAD = 1
        SOS = 2
        EOS = 3
    
        def __init__(self):
            self.dict = {
                self.UNK_TAG :self.UNK,
                self.PAD_TAG :self.PAD,
                self.SOS_TAG :self.SOS,
                self.EOS_TAG :self.EOS
            }
            self.count = {}
            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, sentence):
            """
            :param sentence:[word1,word2,word3]
            :param min_count: 最小出现的次数
            :param max_count: 最大出现的次数
            :param max_feature: 总词语的最大数量
            :return:
            """
            for a in sentence:
                if a not in self.count:
                    self.count[a] = 0
                self.count[a] += 1
    
            self.fited = True
    
        def build_vocab(self, min_count=1, max_count=None, max_feature=None):
    
            # 比最小的数量大和比最大的数量小的需要
            if min_count is not None:
                self.count = {k: v for k, v in self.count.items() if v >= min_count}
            if max_count is not None:
                self.count = {k: v for k, v in self.count.items() if v <= max_count}
    
            # 限制最大的数量
            if isinstance(max_feature, int):
                count = sorted(list(self.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(self.count.keys()):
                    self.dict[w] = len(self.dict)
    
            # 准备一个index->word的字典
            self.inversed_dict = dict(zip(self.dict.values(), self.dict.keys()))
    
        def transform(self, sentence,max_len=None,add_eos=False):
            """
            实现吧句子转化为数组(向量)
            :param sentence:
            :param max_len:
            :return:
            """
            assert self.fited, "必须先进行fit操作"
    
            r = [self.to_index(i) for i in sentence]
            if max_len is not None:
                if max_len>len(sentence):
                    if add_eos:
                        r+=[self.EOS]+[self.PAD for _ in range(max_len-len(sentence)-1)]
                    else:
                        r += [self.PAD for _ in range(max_len - len(sentence))]
                else:
                    if add_eos:
                        r = r[:max_len-1]
                        r += [self.EOS]
                    else:
                        r = r[:max_len]
            else:
                if add_eos:
                    r += [self.EOS]
            # print(len(r),r)
            return r
    
        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
    
    #之后导入该word_sequence使用
    word_sequence = pickle.load(open("./pkl/ws.pkl","rb")) if not config.use_word else pickle.load(open("./pkl/ws_word.pkl","rb"))
    
    
    
    if __name__ == '__main__':
        from word_sequence import Word2Sequence
        from tqdm import tqdm
        import pickle
    
        word_sequence = Word2Sequence()
        #词语级别
        input_path = "../corpus/input.txt"
        target_path = "../corpus/output.txt"
        for line in tqdm(open(input_path).readlines()):
            word_sequence.fit(line.strip().split())
        for line in tqdm(open(target_path).readlines()):
            word_sequence.fit(line.strip().split())
    	
        #使用max_feature=5000个数据
        word_sequence.build_vocab(min_count=5,max_count=None,max_feature=5000)
        print(len(word_sequence))
        pickle.dump(word_sequence,open("./pkl/ws.pkl","wb"))
    

    4. 构建Dataset和DataLoader

    创建dataset.py 文件,准备数据集

    import torch
    import config
    from torch.utils.data import Dataset,DataLoader
    from word_sequence import word_sequence
    
    
    class ChatDataset(Dataset):
        def __init__(self):
            super(ChatDataset,self).__init__()
    
            input_path = "../corpus/input.txt"
            target_path = "../corpus/output.txt"
            if config.use_word:
                input_path = "../corpus/input_word.txt"
                target_path = "../corpus/output_word.txt"
    
            self.input_lines = open(input_path).readlines()
            self.target_lines = open(target_path).readlines()
            assert len(self.input_lines) == len(self.target_lines) ,"input和target文本的数量必须相同"
        def __getitem__(self, index):
            input = self.input_lines[index].strip().split()
            target = self.target_lines[index].strip().split()
            if len(input) == 0 or len(target)==0:
                input = self.input_lines[index+1].strip().split()
                target = self.target_lines[index+1].strip().split()
            #此处句子的长度如果大于max_len,那么应该返回max_len
            return input,target,min(len(input),config.max_len),min(len(target),config.max_len)
    
        def __len__(self):
            return len(self.input_lines)
    
    def collate_fn(batch):
        #1.排序
        batch = sorted(batch,key=lambda x:x[2],reverse=True)
        input, target, input_length, target_length = zip(*batch)
    
        # 2.进行padding的操作
        input = torch.LongTensor([word_sequence.transform(i, max_len=config.max_len) for i in input])
        target = torch.LongTensor([word_sequence.transform(i, max_len=config.max_len, add_eos=True) for i in target])
        input_length = torch.LongTensor(input_length)
        target_length = torch.LongTensor(target_length)
    
        return input, target, input_length, target_length
    
    data_loader = DataLoader(dataset=ChatDataset(),batch_size=config.batch_size,shuffle=True,collate_fn=collate_fn,drop_last=True)
    
    if __name__ == '__main__':
        for idx, (input, target, input_lenght, target_length) in enumerate(data_loader):
            print(idx)
            print(input)
            print(target)
            print(input_lenght)
            print(target_length)
    

    5. 完成encoder编码器逻辑

    import torch.nn as nn
    from word_sequence import word_sequence
    import config
    
    
    class Encoder(nn.Module):
        def __init__(self):
            super(Encoder,self).__init__()
            self.vocab_size = len(word_sequence)
            self.dropout = config.dropout
            self.embedding_dim = config.embedding_dim
            self.embedding = nn.Embedding(num_embeddings=self.vocab_size,embedding_dim=self.embedding_dim,padding_idx=word_sequence.PAD)
            self.gru = nn.GRU(input_size=self.embedding_dim,
                              hidden_size=config.hidden_size,
                              num_layers=1,
                              batch_first=True,
                              dropout=config.dropout)
    
        def forward(self, input,input_length):
            embeded = self.embedding(input)
            embeded = nn.utils.rnn.pack_padded_sequence(embeded,lengths=input_length,batch_first=True)
    
            #hidden:[1,batch_size,vocab_size]
            out,hidden = self.gru(embeded)
            out,outputs_length = nn.utils.rnn.pad_packed_sequence(out,batch_first=True,padding_value=word_sequence.PAD)
            #hidden [1,batch_size,hidden_size]
            return out,hidden
    

    6. 完成decoder解码器的逻辑

    import torch
    import torch.nn as nn
    import config
    import random
    import torch.nn.functional as F
    from word_sequence import word_sequence
    
    class Decoder(nn.Module):
        def __init__(self):
            super(Decoder,self).__init__()
            self.max_seq_len = config.max_len
            self.vocab_size = len(word_sequence)
            self.embedding_dim = config.embedding_dim
            self.dropout = config.dropout
    
            self.embedding = nn.Embedding(num_embeddings=self.vocab_size,embedding_dim=self.embedding_dim,padding_idx=word_sequence.PAD)
            self.gru = nn.GRU(input_size=self.embedding_dim,
                              hidden_size=config.hidden_size,
                              num_layers=1,
                              batch_first=True,
                              dropout=self.dropout)
            self.log_softmax = nn.LogSoftmax()
    
            self.fc = nn.Linear(config.hidden_size,self.vocab_size)
    
        def forward(self, encoder_hidden,target,target_length):
            # encoder_hidden [batch_size,hidden_size]
            # target [batch_size,seq-len]
    
            decoder_input = torch.LongTensor([[word_sequence.SOS]]*config.batch_size).to(config.device)
            decoder_outputs = torch.zeros(config.batch_size,config.max_len,self.vocab_size).to(config.device) #[batch_size,seq_len,14]
    
            decoder_hidden = encoder_hidden #[batch_size,hidden_size]
    
            for t in range(config.max_len):
                decoder_output_t , decoder_hidden = self.forward_step(decoder_input,decoder_hidden)
                decoder_outputs[:,t,:] = decoder_output_t
                value, index = torch.topk(decoder_output_t, 1) # index [batch_size,1]
                decoder_input = index
            return decoder_outputs,decoder_hidden
    
        def forward_step(self,decoder_input,decoder_hidden):
            """
            :param decoder_input:[batch_size,1]
            :param decoder_hidden: [1,batch_size,hidden_size]
            :return: out:[batch_size,vocab_size],decoder_hidden:[1,batch_size,didden_size]
            """
            embeded = self.embedding(decoder_input)  #embeded: [batch_size,1 , embedding_dim]
            out,decoder_hidden = self.gru(embeded,decoder_hidden) #out [1, batch_size, hidden_size]
            out = out.squeeze(0)
            out = F.log_softmax(self.fc(out),dim=-1)#[batch_Size, vocab_size]
            out = out.squeeze(1)
            # print("out size:",out.size(),decoder_hidden.size())
            return out,decoder_hidden
    

    7.完成seq2seq的模型

    import torch
    import torch.nn as nn
    
    class Seq2Seq(nn.Module):
        def __init__(self,encoder,decoder):
            super(Seq2Seq,self).__init__()
            self.encoder = encoder
            self.decoder = decoder
    
        def forward(self, input,target,input_length,target_length):
            encoder_outputs,encoder_hidden = self.encoder(input,input_length)
            decoder_outputs,decoder_hidden = self.decoder(encoder_hidden,target,target_length)
            return decoder_outputs,decoder_hidden
    
        def evaluation(self,inputs,input_length):
            encoder_outputs,encoder_hidden = self.encoder(inputs,input_length)
            decoded_sentence = self.decoder.evaluation(encoder_hidden)
            return decoded_sentence
    

    8. 完成训练逻辑

    为了加速训练,可以考虑在gpu上运行,那么在我们自顶一个所以的tensor和model都需要转化为CUDA支持的类型。

    当前的数据量为500多万条,在GTX1070(8G显存)上训练,大概需要90分一个epoch,耐心的等待吧

    import torch
    import config
    from torch import optim
    import torch.nn as nn
    from encoder import Encoder
    from decoder import Decoder
    from seq2seq import Seq2Seq
    from dataset import data_loader as train_dataloader
    from word_sequence import word_sequence
    
    encoder = Encoder()
    decoder = Decoder()
    model = Seq2Seq(encoder,decoder)
    
    #device在config文件中实现
    model.to(config.device)
    
    print(model)
    
    model.load_state_dict(torch.load("model/seq2seq_model.pkl"))
    optimizer =  optim.Adam(model.parameters())
    optimizer.load_state_dict(torch.load("model/seq2seq_optimizer.pkl"))
    criterion= nn.NLLLoss(ignore_index=word_sequence.PAD,reduction="mean")
    
    def get_loss(decoder_outputs,target):
        target = target.view(-1) #[batch_size*max_len]
        decoder_outputs = decoder_outputs.view(config.batch_size*config.max_len,-1)
        return criterion(decoder_outputs,target)
    
    
    def train(epoch):
        for idx,(input,target,input_length,target_len) in enumerate(train_dataloader):
            input = input.to(config.device)
            target = target.to(config.device)
            input_length = input_length.to(config.device)
            target_len = target_len.to(config.device)
    
            optimizer.zero_grad()
            ##[seq_len,batch_size,vocab_size] [batch_size,seq_len]
            decoder_outputs,decoder_hidden = model(input,target,input_length,target_len)
            loss = get_loss(decoder_outputs,target)
            loss.backward()
            optimizer.step()
    
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, idx * len(input), len(train_dataloader.dataset),
                       100. * idx / len(train_dataloader), loss.item()))
    
            torch.save(model.state_dict(), "model/seq2seq_model.pkl")
            torch.save(optimizer.state_dict(), 'model/seq2seq_optimizer.pkl')
    
    if __name__ == '__main__':
        for i in range(10):
            train(i)
    

    训练10个epoch之后的效果如下,可以看出损失依然很高:

    Train Epoch: 9 [2444544/4889919 (50%)]	Loss: 4.923604
    Train Epoch: 9 [2444800/4889919 (50%)]	Loss: 4.364594
    Train Epoch: 9 [2445056/4889919 (50%)]	Loss: 4.613254
    Train Epoch: 9 [2445312/4889919 (50%)]	Loss: 4.143538
    Train Epoch: 9 [2445568/4889919 (50%)]	Loss: 4.412729
    Train Epoch: 9 [2445824/4889919 (50%)]	Loss: 4.516526
    Train Epoch: 9 [2446080/4889919 (50%)]	Loss: 4.124945
    Train Epoch: 9 [2446336/4889919 (50%)]	Loss: 4.777015
    Train Epoch: 9 [2446592/4889919 (50%)]	Loss: 4.358538
    Train Epoch: 9 [2446848/4889919 (50%)]	Loss: 4.513412
    Train Epoch: 9 [2447104/4889919 (50%)]	Loss: 4.202757
    Train Epoch: 9 [2447360/4889919 (50%)]	Loss: 4.589584
    

    9.小结

    效果不好

    展开全文
  • 闲聊机器人之Seq2Seq模型的原理 闲聊机器人的介绍 在项目准备阶段我们知道,用户说了一句话后,会判断其意图,如果是想进行闲聊,那么就会调用闲聊模型返回结果,这是我们会在项目中实现的功能。 目前市面上的...

    闲聊机器人之Seq2Seq模型的原理

    闲聊机器人的介绍

    在项目准备阶段我们知道,用户说了一句话后,会判断其意图,如果是想进行闲聊,那么就会调用闲聊模型返回结果,这是我们会在项目中实现的功能。

    目前市面上的常见闲聊机器人有微软小冰这种类型的模型,很久之前还有小黄鸡这种体验更差的模型

    常见的闲聊模型都是一种seq2seq的结构,接下来使用seq2seq来实现我们的闲聊机器人。

    Seq2Seq模型的原理

    1. Seq2Seq的介绍

    Sequence to sequence (seq2seq)是由encoder(编码器)decoder(解码器)两个RNN的组成的。其中encoder负责对输入句子的理解,转化为context vector,decoder负责对理解后的句子的向量进行处理,解码,获得输出。上述的过程和我们大脑理解东西的过程很相似,听到一句话,理解之后,尝试组装答案,进行回答

    那么此时,就有一个问题,在encoder的过程中得到的context vector作为decoder的输入,那么这样一个输入,怎么能够得到多个输出呢?

    其实就是当前一步的输出,作为下一个单元的输入,然后得到结果

    outputs = []
    while True:
        output = decoderd(output)
        outputs.append(output)

    那么循环什么时候停止呢?

    在训练数据集中,可以再输出的最后面添加一个结束符<END>,如果遇到该结束符,则可以终止循环

    outputs = []
    while output!="<END>":
        output = decoderd(output)
        outputs.append(output)

    这个结束符只是一个标记,很多人也会使用<EOS>(End Of Sentence)

    总之:Seq2seq模型中的encoder接受一个长度为M的序列,得到1个 context vector,之后decoder把这一个context vector转化为长度为N的序列作为输出,从而构成一个M to N的模型,能够处理很多不定长输入输出的问题,比如:文本翻译,问答,文章摘要,关键字写诗等等

     

    2. Seq2Seq模型的实现

    下面,通过一个简单的列子,来看看普通的Seq2Seq模型应该如何实现。

    需求:完成一个模型,实现往模型输入一串数字,输出这串数字+0

    例如

    • 输入123456789,输出1234567890

    • 输入52555568,输出525555680

    2.1 实现流程

    1. 文本转化为序列(数字序列,torch.LongTensor

    2. 使用序列,准备数据集,准备Dataloader

    3. 完成编码器

    4. 完成解码器

    5. 完成seq2seq模型

    6. 完成模型训练的逻辑,进行训练

    7. 完成模型评估的逻辑,进行模型评估

    2.2 文本转化为序列

    由于输入的是数字,为了把这些数字和词典中的真实数字进行对应,可以把这些数字理解为字符串

    那么我们需要做的就是:

    1. 把字符串对应为数字

    2. 把数字转化为字符串

    完成逻辑和之前相同,创建word_sequence.py文件,实现上述逻辑

    class NumSequence:
        UNK_TAG = "UNK"  # 未知词
        PAD_TAG = "PAD"  # 填充词,实现文本对齐,即一个batch中的句子长度都是相同的,短句子会被padding
        EOS_TAG = "EOS"  # 句子的开始
        SOS_TAG = "SOS"  # 句子的结束
    
        UNK = 0
        PAD = 1
        EOS = 2
        SOS = 3
    
        def __init__(self):
            self.dict = {
                self.UNK_TAG: self.UNK,
                self.PAD_TAG: self.PAD,
                self.EOS_TAG: self.EOS,
                self.SOS_TAG: self.SOS
            }
            # 得到字符串和数字对应的字典
            for i in range(10):
                self.dict[str(i)] = len(self.dict)  # 随着i的增加,len(dict)++
            # 得到数字和字符串对应的字典
            self.index2word = dict(zip(self.dict.values(), self.dict.keys()))
    
        def __len__(self):
            return len(self.dict)
    
        # 把sentence转化为数字序列
        def transform(self, sequence, max_len=None, add_eos=False):
            """
            sequence:句子
            max_len :句子的最大长度
            add_eos:是否添加结束符
            """
    
            sequence_list = list(str(sequence))
            seq_len = len(sequence_list) + 1 if add_eos else len(sequence_list)
    
            if add_eos and max_len is not None:
                assert max_len >= seq_len, "max_len 需要大于seq+eos的长度"
            _sequence_index = [self.dict.get(i, self.UNK) for i in sequence_list]
            if add_eos:
                _sequence_index += [self.EOS]
            if max_len is not None:
                sequence_index = [self.PAD] * max_len
                sequence_index[:seq_len] = _sequence_index
                return sequence_index
            else:
                return _sequence_index
    
        # 把序列转回字符串
        def inverse_transform(self, sequence_index):
            result = []
            for i in sequence_index:
                if i == self.EOS:
                    break
                result.append(self.index2word.get(int(i), self.UNK_TAG))
            return result
    
    
    # 实例化,供后续调用
    num_sequence = NumSequence()
    
    if __name__ == '__main__':
        num_sequence = NumSequence()
        print(num_sequence.dict)
        print(num_sequence.index2word)
        print(num_sequence.transform("1231230", add_eos=True))
        print(num_sequence.transform("1231230", add_eos=False))
        print(num_sequence.transform("1231230AX", add_eos=True))
        print(num_sequence.inverse_transform([1, 12, 3]))
    

    运行结果:

     

    2.3 准备数据集

    2.3.1 准备Dataset  【在样本的target中,需要实现EOS、SOS分别表示句子的开始和结束;在target中需要添加EOS,在transform中需要实现添加EOS的操作】

    这里,我们使用随机创建的[0,100000000]的整型,来准备数据集

    from torch.utils.data import Dataset, DataLoader
    import numpy as np
    
    
    class RandomDataset(Dataset):
        def __init__(self):
            super(RandomDataset, self).__init__()
            self.total_data_size = 500000
            np.random.seed(10)
            self.total_data = np.random.randint(1, 100000000, size=[self.total_data_size])
    
        def __getitem__(self, idx):
            """返回input,target,input_length,target_length(真实长度)"""
            input = str(self.total_data[idx])
            return input, input + "0", len(input), len(input) + 1
    
        def __len__(self):
            return self.total_data_size
    

    通过随机数的结果,可以看到,大部分的数字长度为8,在目标值后面添加上0和EOS之后,最大长度为10

    所以常见config配置文件,添加上max_len:文本最大长度,方便后续的修改

    2.3.2 准备DataLoader

    在准备DataLoader的过程中,可以通过定义的collate_fn来实现对dataset中batch数据的处理

    其中需要注意:

    1. 需要对batch中的数据进行排序,根据数据的真实长度进行降序排序(后面需要用到)

    2. 需要调用文本序列化的方法,把文本进行序列化的操作,同时target需要进行add eos的操作

    3. 最后返回序列的LongTensor格式

    4. DataLoader中有drop_last参数,当数据量无法被batch_size整除时,最后一个batch的数据个数和之前的数据个数长度不同,可以考虑进行删除

    def collate_fn(batch):
        # 1. 对batch进行排序,按照长度从长到短的顺序排序
        batch = sorted(batch, key=lambda x: x[3], reverse=True)
        input, target, input_length, target_length = zip(*batch)
    
        # 2.进行padding的操作
        input = torch.LongTensor([num_sequence.transform(i, max_len=config.max_len) for i in input])
        target = torch.LongTensor([num_sequence.transform(i, max_len=config.max_len, add_eos=True) for i in target])
        input_length = torch.LongTensor(input_length)
        target_length = torch.LongTensor(target_length)
    
        return input, target, input_length, target_length
    
    
    data_loader = DataLoader(dataset=RandomDataset(), batch_size=config.batch_size, collate_fn=collate_fn, drop_last=True)
    

    Dataset,Dataloader 完整代码:

    config.py

    from word_sequence import NumSequence
    
    train_batch_size = 128
    num_sequence = NumSequence()
    max_len = 9
    

    word_sequence.py   【此文件和上面的稍微有点区别】

    class NumSequence(object):
        PAD_TAG = 'PAD'  # 填充标记
        UNK_TAG = 'UNK'  # 未知词标记
        SOS_TAG = 'SOS'  # strat of sequence
        EOS_TAG = 'EOS'  # end of sequence
    
        PAD = 0
        UNK = 1
        SOS = 2
        EOS = 3
    
        def __init__(self):
            self.dict = {
                self.PAD_TAG: self.PAD,
                self.UNK_TAG: self.UNK,
                self.SOS_TAG: self.SOS,
                self.EOS_TAG: self.EOS
            }
            for i in range(10):
                self.dict[str(i)] = len(self.dict)
    
            self.inverse_dict = dict(zip(self.dict.values(), self.dict.keys()))
    
        def transform(self, sentence, max_len, add_eos=False):
            """
            把sentence 转化为 序列
            :param max_len 句子最大长度
            :param add_eos 是否添加结束符
            add_eos : True时,输出句子长度为max_len + 1
            add_eos : False时,输出句子长度为max_len
            :return:
            """
            if len(sentence) > max_len:
                sentence = sentence[:max_len]
    
            # 提前计算句子长度,实现add_eos后,句子长度统一
            sentence_len = len(sentence)
    
            # sentence[1,3,4,5,UNK,EOS,PAD,PAD....]
            if add_eos:
                sentence += [self.EOS_TAG]
    
            if sentence_len < max_len:
                # 句子长度不够,用PAD填充
                sentence += (max_len - sentence_len) * [self.PAD_TAG]
            # 对于新出现的词采用特殊标记
            result = [self.dict.get(i, self.UNK) for i in sentence]
    
            return result
    
        def invert_transform(self, indices):
            """
            序列转化为sentence
            :param indices:
            :return:
            """
            return [self.inverse_dict.get(i, self.UNK_TAG) for i in indices]
    
    
    if __name__ == '__main__':
        num_sequence = NumSequence()
        print(num_sequence.dict)
        print(num_sequence.inverse_dict)
    

    dataset.py

    """
    准备数据集,准备dataset,dataloader
    
    """
    import config
    from torch.utils.data import Dataset, DataLoader
    import numpy as np
    import torch
    
    
    class NumDataset(Dataset):
        def __init__(self):
            # 使用numpy随机创建, 1e8 = 10^8
            self.data = np.random.randint(0, 1e8, size=[500000])
    
        def __getitem__(self, index):
            input = list(str(self.data[index]))
            target = input + ['0']
            input_lenth = len(input)
            target_lenth = len(target)
            # target_length = input_length + 1
            return input, target, input_lenth, target_lenth
    
        def __len__(self):
            return self.data.shape[0]
    
    
    def collate_fn(batch):
        """
    
        :param batch:[(input,target,input_length,target_length),...,]
        :return:
        """
        batch = sorted(batch, key=lambda x: x[3], reverse=True)
        input, target, input_length, target_length = list(zip(*batch))
        # 把input 转化为序列
        input = torch.LongTensor([config.num_sequence.transform(i, max_len=config.max_len) for i in input])
        target = torch.LongTensor(
            [config.num_sequence.transform(i, max_len=config.max_len + 1, add_eos=True) for i in target])
        input_length = torch.LongTensor(input_length)
        target_length = torch.LongTensor(target_length)
    
        return input, target, input_length, target_length
    
    
    train_dataloader = DataLoader(NumDataset(), batch_size=config.train_batch_size, shuffle=True, collate_fn=collate_fn)
    
    if __name__ == '__main__':
        for input, target, input_length, target_length in train_dataloader:
            print(input.size())
            print(target.size())
            print(input)
            print(target)
            print(input_length)
            print(target_length)
            break
    

    运行结果:

    2.4 准备编码器

    编码器(encoder)的目的就是为了对文本进行编码,把编码后的结果交给后续的程序使用,所以在这里可以使用Embedding+GRU的结构来使用,使用最后一个time step的输出(hidden state)作为句子的编码结果

    注意点:

    1. Embedding和GRU的参数,这里我们让GRU中batch放在前面

    2. 输出结果的形状

    3. 在LSTM和GRU中,每个time step的输入会进行计算,得到结果,整个过程是一个和句子长度相关的一个循环,手动实现速度较慢

      1. pytorch中实现了nn.utils.rnn.pack_padded_sequence 对padding后的句子进行打包的操作能够更快获得LSTM or GRU的结果

      2. 同时实现了nn.utils.rnn.pad_packed_sequence对打包的内容进行解包的操作

    4. nn.utils.rnn.pack_padded_sequence使用过程中需要对batch中的内容按照句子的长度降序排序

    (batch = sorted(batch, key = lambda x : x[3], reverse = True)------->batch:[(input,target,input_length,target_length),...,])

    embeded = nn.utils.rnn.pack_padded_sequence
    		(
    			embeded,
    			lengths=input_length, # 真实长度
    			batch_first=True
    	)
    out,outputs_length = nn.utils.rnn.pad_packed_sequence
    		(
    			out,
    			batch_first=True,
    			padding_value=num_sequence.PAD  # 填充值
    	)
    

    实现代码如下:

    import torch.nn as nn
    from word_sequence import num_sequence
    import config
    
    
    class NumEncoder(nn.Module):
        def __init__(self):
            super(NumEncoder, self).__init__()
            self.vocab_size = len(num_sequence)
            self.dropout = config.dropout
            self.embedding = nn.Embedding(num_embeddings=self.vocab_size, embedding_dim=config.embedding_dim,
                                          padding_idx=num_sequence.PAD)
            self.gru = nn.GRU(input_size=config.embedding_dim,
                              hidden_size=config.hidden_size,
                              num_layers=1,
                              batch_first=True)
    
        def forward(self, input, input_length):
            """
            input:[batch_size,max_len]
            input_length:[batch_size]
            """
            embeded = self.embedding(input)  # [batch_size,max_len , embedding_dim]
    
            # 对文本对齐之后的句子进行打包,能够加速在LSTM or GRU中的计算过程
            embeded = nn.utils.rnn.pack_padded_sequence(embeded, lengths=input_length, batch_first=True)
    
            # hidden:[1,batch_size,vocab_size]
            out, hidden = self.gru(embeded)
    
            # 对前面打包后的结果再进行解包
            out, outputs_length = nn.utils.rnn.pad_packed_sequence(out, batch_first=True, padding_value=num_sequence.PAD)
            # out [batch_size,seq_len,hidden_size]
            return out, hidden
    

    完整实现代码:

    config.py

    import torch
    from word_sequence import NumSequence
    
    train_batch_size = 128
    num_sequence = NumSequence()
    max_len = 9
    embedding_dim = 100
    num_layer = 1
    hidden_size = 64
    model_save_path = './model.pkl'
    optimizer_save_path = './optimizer.pkl'
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    

    encode.py

    """
    编码器
    """
    import torch.nn as nn
    import config
    from torch.nn.utils.rnn import pad_packed_sequence, pack_padded_sequence
    
    
    class Encoder(nn.Module):
        def __init__(self):
            super(Encoder, self).__init__()
            #  torch.nn.Embedding(num_embeddings词典大小即不重复词数,embedding_dim单个词用多长向量表示)
            self.embedding = nn.Embedding(
                num_embeddings=len(config.num_sequence.dict),
                embedding_dim=config.embedding_dim,
                padding_idx=config.num_sequence.PAD
            )
            self.gru = nn.GRU(
                input_size=config.embedding_dim,
                num_layers=config.num_layer,
                hidden_size=config.hidden_size,
                bidirectional=False,
                batch_first=True
            )
    
        def forward(self, input, input_length):
            """
            :param input: [batch_size, max_len]
            :return:
            """
            embedded = self.embedding(input)  # embedded [batch_size, max_len, embedding_dim]
            # 加速循环过程
            embedded = pack_padded_sequence(embedded, input_length, batch_first=True)  # 打包
            out, hidden = self.gru(embedded)
            out, out_length = pad_packed_sequence(out, batch_first=True, padding_value=config.num_sequence.PAD)  # 解包
    
            # hidden即h_n [num_layer*[1/2],batchsize, hidden_size]
            # out : [batch_size, seq_len/max_len, hidden_size]
            return out, hidden, out_length
    
    
    if __name__ == '__main__':
        from dataset import train_dataloader
    
        encoder = Encoder()
        print(encoder)
        for input, target, input_length, target_length in train_dataloader:
            out, hidden, out_length = encoder(input, input_length)
            print(input.size())
            print(out.size())
            print(hidden.size())
            print(out_length)
            break
    

    运行结果:

    Encoder(
      (embedding): Embedding(14, 100, padding_idx=0)
      (gru): GRU(100, 64, batch_first=True)
    )
    torch.Size([128, 9])
    torch.Size([128, 8, 64])
    torch.Size([1, 128, 64])
    tensor([8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
            8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
            8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
            8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
            8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7,
            7, 7, 7, 7, 7, 7, 7, 6])
    
    Process finished with exit code 0

    2.5 实现解码器

    加码器主要负责实现对编码之后结果的处理,得到预测值,为后续计算损失做准备

    此时需要思考:

    1. 使用什么样的损失函数,预测值需要是什么格式的

      • 结合之前的经验,我们可以理解为当前的问题是一个分类的问题,即每次的输出其实对选择一个概率最大的词

      • 真实值的形状是[batch_size,max_len],从而我们知道输出的结果需要是一个[batch_size,max_len,vocab_size]的形状

      • 即预测值的最后一个维度进行计算log_softmax,然后和真实值进行相乘,从而得到损失

    2. 如何把编码结果[1,batch_size,hidden_size]进行操作,得到预测值。解码器也是一个RNN,即也可以使用LSTM or GRU的结构,所以在解码器中:

      • 通过循环,每次计算的一个time step的内容

      • 编码器的结果作为初始的隐层状态,定义一个[batch_size,1]的全为SOS的数据作为最开始的输入,告诉解码器,要开始工作了

      • 通过解码器预测一个输出[batch_size,hidden_size](会进行形状的调整为[batch_size,vocab_size]),把这个输出作为输入再使用解码器进行解码

      • 上述是一个循环,循环次数就是句子的最大长度,那么就可以得到max_len个输出

      • 把所有输出的结果进行concate,得到[batch_size,max_len,vocab_size]

    3. 在RNN的训练过程中,使用前一个预测的结果作为下一个step的输入,可能会导致一步错,步步错的结果,如果提高模型的收敛速度?

      • 可以考虑在训练的过程中,把真实值作为下一步的输入,这样可以避免步步错的局面

      • 同时在使用真实值的过程中,仍然使用预测值作为下一步的输入,两种输入随机使用

      • 上述这种机制我们把它称为Teacher forcing,就像是一个指导老师,在每一步都会对我们的行为进行纠偏,从而达到在多次训练之后能够需要其中的规律

    示例代码:

    import torch
    import torch.nn as nn
    import config
    import random
    import torch.nn.functional as F
    from word_sequence import num_sequence
    
    
    class NumDecoder(nn.Module):
        def __init__(self):
            super(NumDecoder, self).__init__()
            self.max_seq_len = config.max_len
            self.vocab_size = len(num_sequence)
            self.embedding_dim = config.embedding_dim
            self.dropout = config.dropout
    
            self.embedding = nn.Embedding(num_embeddings=self.vocab_size, embedding_dim=self.embedding_dim,
                                          padding_idx=num_sequence.PAD)
            self.gru = nn.GRU(input_size=self.embedding_dim,
                              hidden_size=config.hidden_size,
                              num_layers=1,
                              batch_first=True,
                              dropout=self.dropout)
            self.log_softmax = nn.LogSoftmax()
    
            self.fc = nn.Linear(config.hidden_size, self.vocab_size)
    
        def forward(self, encoder_hidden, target, target_length):
            # encoder_hidden [batch_size,hidden_size]
            # target [batch_size,max_len]
    
            # 初始的全为SOS的输入
            decoder_input = torch.LongTensor([[num_sequence.SOS]] * config.batch_size)
    
            # 解码器的输出,用来后保存所有的输出结果
            decoder_outputs = torch.zeros(config.batch_size, config.max_len, self.vocab_size)
    
            decoder_hidden = encoder_hidden  # [batch_size,hidden_size]
    
            for t in range(config.max_len):
                decoder_output_t, decoder_hidden = self.forward_step(decoder_input, decoder_hidden)
    
                # 在不同的time step上进行复制,decoder_output_t [batch_size,vocab_size]
                decoder_outputs[:, t, :] = decoder_output_t
    
                # 在训练的过程中,使用 teacher forcing,进行纠偏
                use_teacher_forcing = random.random() > 0.5
                if use_teacher_forcing:
                    # 下一次的输入使用真实值
                    decoder_input = target[:, t].unsqueeze(1)  # [batch_size,1]
                else:
                    # 使用预测值,topk中k=1,即获取最后一个维度的最大的一个值
                    value, index = torch.topk(decoder_output_t, 1)  # index [batch_size,1]
                    decoder_input = index
            return decoder_outputs, decoder_hidden
    
        def forward_step(self, decoder_input, decoder_hidden):
            """
            :param decoder_input:[batch_size,1]
            :param decoder_hidden: [1,batch_size,hidden_size]
            :return: out:[batch_size,vocab_size],decoder_hidden:[1,batch_size,didden_size]
            """
            embeded = self.embedding(decoder_input)  # embeded: [batch_size,1 , embedding_dim]
    
            out, decoder_hidden = self.gru(embeded, decoder_hidden)  # out [1, batch_size, hidden_size]
    
            out = out.squeeze(0)  # 去除第0维度的1
            # 进行全连接形状变化,同时进行求取log_softmax
            out = F.log_softmax(self.fc(out), dim=-1)  # out [batch_Size,1, vocab_size]
            out = out.squeeze(1)
            return out, decoder_hidden
    

    完整代码实现:

    decode.py

    """
    实现解码器
    """
    import torch.nn as nn
    import config
    import torch
    import torch.nn.functional as F
    
    
    class Decoder(nn.Module):
        def __init__(self):
            super(Decoder, self).__init__()
            self.embedding = nn.Embedding(
                num_embeddings=len(config.num_sequence),
                embedding_dim=config.embedding_dim,
                padding_idx=config.num_sequence.PAD
            )
            self.gru = nn.GRU(
                input_size=config.embedding_dim,
                hidden_size=config.hidden_size,
                num_layers=config.num_layer,
                batch_first=True,
                bidirectional=False
            )
            self.fc = nn.Linear(config.hidden_size, len(config.num_sequence))
    
        def forward(self, target, encoder_hidden):
            # 1.获取encoder最后一次的输出,作为decoder第一次的隐藏状态
            decoder_hidden = encoder_hidden
            batch_size = target.size(0)
            # 2.准备第一次decoder第一个时间步的输入,[batch_size,1]的SOS作为输入
            decoder_input = torch.LongTensor(torch.ones([batch_size, 1], dtype=torch.int64) * config.num_sequence.SOS).to(
                config.device)
            # 3.在第一个时间步上进行计算,得到第一个时间步的输出,hidden_state
            # 4.对前一个时间步的输出进行计算,得到第一个最后的输出的结果
            # 5.把前一次的hidden_state作为当前时间步的hidden_state的输入,把前一次的输出,作为当前时间步的输入
            # 6.循环4-5
    
            # 保存预测结果
            # output:[batch_size, vocab_size]
            decoder_outputs = torch.zeros([batch_size, config.max_len + 2, len(config.num_sequence)]).to(config.device)
    
            # config.max_len+2 dataset中获取target时,max_len+1且add_eos=True,因此+2
            for t in range(config.max_len + 2):
                # 当前时刻的输出和隐藏状态
                decoder_output_t, decoder_hidden = self.forward_step(decoder_input, decoder_hidden)
                # 保存decoder_output_t到decoder_outputs中
                decoder_outputs[:, t, :] = decoder_output_t
                # 使用预测值,topk中k = 1,即获取最后一个维度的最大的一个值
                value, index = torch.topk(decoder_output_t, k=1)
                # 获取下一次的input
                decoder_input = index
            return decoder_outputs, decoder_hidden
    
        def forward_step(self, decoder_input, decoder_hidden):
            """
            计算每个时间步上的结果
            :param decoder_input: [batch_size, 1]
            :param decoder_hidden: [num_layer * [1/2],batch_size, hidden_size] decoder_hidden也就是encoder最后一次的隐藏状态
            :return:
            """
            decoder_input_embedded = self.embedding(decoder_input)  # [batch_size, 1] -->[batch_size, 1, embedding_dim]
            # out [batch_size, 1, hidden_size]
            # decoder_hidden [num_layer*[1/2],batch_size,hidden_size]
            out, decoder_hidden = self.gru(decoder_input_embedded, decoder_hidden)
            out = out.squeeze(1)  # [batch_size, 1, hidden_size] --> [batch_size, hidden_size]
            output = F.log_softmax(self.fc(out), dim=-1)  # fc后,out [batch_size, hidden_size]-->[batch_size, vocab_size]
            # print('output:',output.size())
            return output, decoder_hidden
    

    2.6 完成seq2seq模型

    调用之前的encoder和decoder,完成模型的搭建  【合并encode和decode】

    import torch
    import torch.nn as nn
    
    
    class Seq2Seq(nn.Module):
        def __init__(self, encoder, decoder):
            super(Seq2Seq, self).__init__()
            self.encoder = encoder
            self.decoder = decoder
    
        def forward(self, input, target, input_length, target_length):
            # 进行编码
            encoder_outputs, encoder_hidden = self.encoder(input, input_length)
            # 进行解码
            decoder_outputs, decoder_hidden = self.decoder(encoder_hidden, target, target_length)
            return decoder_outputs, decoder_hidden
    

    2.7 完成训练逻辑

    思路流程和之前相同

    示例代码:

    import torch
    import config
    from torch import optim
    import torch.nn as nn
    from encoder import NumEncoder
    from decoder import NumDecoder
    from seq2seq import Seq2Seq
    from dataset import data_loader as train_dataloader
    from word_sequence import num_sequence
    
    encoder = NumEncoder()
    decoder = NumDecoder()
    model = Seq2Seq(encoder, decoder)
    print(model)
    
    # 自定义初始化参数
    # for name, param in model.named_parameters():
    #    if 'bias' in name:
    #        torch.nn.init.constant_(param, 0.0)
    #    elif 'weight' in name:
    #        torch.nn.init.xavier_normal_(param)
    
    # model.load_state_dict(torch.load("model/seq2seq_model.pkl"))
    optimizer = optim.Adam(model.parameters())
    # optimizer.load_state_dict(torch.load("model/seq2seq_optimizer.pkl"))
    criterion = nn.NLLLoss(ignore_index=num_sequence.PAD, reduction="mean")
    
    
    def get_loss(decoder_outputs, target):
        # 很多时候如果tensor进行了转置等操作,直接调用view进行形状的修改是无法成功的
        # target = target.contiguous().view(-1) #[batch_size*max_len]
        target = target.view(-1)
        decoder_outputs = decoder_outputs.view(config.batch_size * config.max_len, -1)
        return criterion(decoder_outputs, target)
    
    
    def train(epoch):
        for idx, (input, target, input_length, target_len) in enumerate(train_dataloader):
            optimizer.zero_grad()
            ##[seq_len,batch_size,vocab_size] [batch_size,seq_len]
            decoder_outputs, decoder_hidden = model(input, target, input_length, target_len)
            loss = get_loss(decoder_outputs, target)
            loss.backward()
            optimizer.step()
    
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, idx * len(input), len(train_dataloader.dataset),
                       100. * idx / len(train_dataloader), loss.item()))
    
            torch.save(model.state_dict(), "model/seq2seq_model.pkl")
            torch.save(optimizer.state_dict(), 'model/seq2seq_optimizer.pkl')
    
    
    if __name__ == '__main__':
        for i in range(10):
            train(i)
    

    完整代码:

    seq2seq.py

    import torch.nn as nn
    from encode import Encoder
    from decode import Decoder
    
    encoder = Encoder()
    decoder = Decoder()
    
    
    class Seq2Seq(nn.Module):
        def __init__(self):
            super(Seq2Seq, self).__init__()
            self.encoder = Encoder()
            self.decoder = Decoder()
    
        def forward(self, input, target, input_length, target_length):
            # 进行编码
            encoder_outputs, encoder_hidden, out_length = self.encoder(input, input_length)
            # 进行解码
            # decoder_outputs, decoder_hidden = self.decoder(encoder_hidden, target, target_length)
            decoder_outputs, decoder_hidden = self.decoder(target, encoder_hidden)
            return decoder_outputs, decoder_hidden
    

    train.py

    from dataset import train_dataloader
    import os
    from encode import Encoder
    from decode import Decoder
    from seq2seq import Seq2Seq
    from torch.optim import Adam
    import torch.nn.functional as F
    import config
    from tqdm import tqdm
    import torch
    
    # 训练流程
    # 1.实例化model, optimizer, loss
    # seq2seq = Seq2Seq(encoder, decoder).to(config.device)
    seq2seq = Seq2Seq().to(config.device)
    optimizer = Adam(seq2seq.parameters(), lr=0.001)
    # 2.遍历dataloader
    # 3.调用模型得到output
    # 4.计算损失,更新参数
    if os.path.exists(config.model_save_path):
        seq2seq.load_state_dict(torch.load(config.model_save_path))
        optimizer.load_state_dict(torch.load(config.optimizer_save_path))
    
    
    def train(epoch):
        bar = tqdm(train_dataloader, desc='训练', total=len(train_dataloader))
        for index, (input, target, input_length, target_length) in enumerate(bar):
    
            input = input.to(config.device)
            target = target.to(config.device)
            input_length = input_length.to(config.device)
            target_length = target_length.to(config.device)
    
            optimizer.zero_grad()
            decoder_outputs, _decoder_hidden = seq2seq(input, target, input_length, target_length)
            # print(decoder_outputs.size(), target.size())
            decoder_outputs = decoder_outputs.view(decoder_outputs.size(0) * decoder_outputs.size(1),
                                                   -1)  # [batch_size * seq_len, -1]
            target = target.view(-1)  # [batch_size * seq_len]
            loss = F.nll_loss(decoder_outputs, target, ignore_index=config.num_sequence.PAD)
            loss.backward()
            optimizer.step()
    
            bar.set_description('epoch:{}\tidx{}\tloss{} '.format(epoch, index, loss.item()))
    
            # 5.模型保存和加载
            if index % 100 == 0:
                torch.save(seq2seq.state_dict(), config.model_save_path)
                torch.save(optimizer.state_dict(), config.optimizer_save_path)
    
    
    if __name__ == '__main__':
        for i in range(3):
            train(i)
    

    运行结果:

    2.8 完成模型评估逻辑

    完成评估逻辑,和decoder中的训练过程稍微不同,可以在其中新建evaluation的方法,传入encoder_hidden,得到预测的结果

        def evaluation(self, encoder_hidden):  # [1, 20, 14]
            batch_size = encoder_hidden.size(1)  # 评估的时候和训练的batch_size不同,不适用config的配置
    
            decoder_input = torch.LongTensor([[config.num_sequence.SOS] * batch_size])
            decoder_outputs = torch.zeros(batch_size, config.max_len, self.vocab_size)  # [batch_size,seq_len,vocab_size]
            decoder_hidden = encoder_hidden
    
            # 评估,不再使用teacher forcing,完全使用预测值作为下一次的输入
            for t in range(config.max_len):
                decoder_output_t, decoder_hidden = self.forward_step(decoder_input, decoder_hidden)
                decoder_outputs[:, t, :] = decoder_output_t
                value, index = torch.topk(decoder_output_t, 1)  # index [20,1]
                decoder_input = index.transpose(0, 1)
    
            # 获取输出的id
            decoder_indices = []  # [[1,2,4],[23,3,2]]
            for i in range(config.max_len):
                value, index = torch.topk(decoder_outputs[:, i, :], k=1, dim=-1)
                decoder_indices.append(index.view(-1).numpy())
            # transpose 调整为按句子输出
            decoder_indices = np.array(decoder_indices).transpose()
            return decoder_indices

    之后再seq2seq的model中,添加evaluation的逻辑

    示例代码:

    import torch
    import torch.nn as nn
    
    class Seq2Seq(nn.Module):
        def __init__(self,encoder,decoder):
            super(Seq2Seq,self).__init__()
            self.encoder = encoder
            self.decoder = decoder
    
        def forward(self, input,target,input_length,target_length):
            encoder_outputs,encoder_hidden = self.encoder(input,input_length)
            decoder_outputs,decoder_hidden = self.decoder(encoder_hidden,target,target_length)
            return decoder_outputs,decoder_hidden
    
        def evaluation(self,inputs,input_length):
            encoder_outputs,encoder_hidden = self.encoder(inputs,input_length)
            decoded_sentence = self.decoder.evaluation(encoder_hidden)
            return decoded_sentence

    更新seq2seq.py

    import torch.nn as nn
    from encode import Encoder
    from decode import Decoder
    
    encoder = Encoder()
    decoder = Decoder()
    
    
    class Seq2Seq(nn.Module):
        def __init__(self):
            super(Seq2Seq, self).__init__()
            self.encoder = encoder
            self.decoder = decoder
    
        def forward(self, input, target, input_length, target_length):
            # 进行编码
            encoder_outputs, encoder_hidden, out_length = self.encoder(input, input_length)
            # 进行解码
            # decoder_outputs, decoder_hidden = self.decoder(encoder_hidden, target, target_length)
            decoder_outputs, decoder_hidden = self.decoder(target, encoder_hidden)
            return decoder_outputs, decoder_hidden
    
        def evaluation(self, inputs, input_length):
            encoder_outputs, encoder_hidden, out_length = self.encoder(inputs, input_length)
            decoded_sentence = self.decoder.evaluation(encoder_hidden)
            return decoded_sentence
    

    创建eval.py,完成模型评估的逻辑

    import torch
    import config
    from torch import optim
    import torch.nn as nn
    from encoder import NumEncoder
    from decoder import NumDecoder
    from seq2seq import Seq2Seq
    from dataset import data_loader as train_dataloader
    from word_sequence import num_sequence
    import numpy as np
    import random
    
    
    
    encoder = NumEncoder()
    decoder = NumDecoder()
    model = Seq2Seq(encoder,decoder)
    model.load_state_dict(torch.load("model/seq2seq_model.pkl"))
    
    def evalaute():
        data = [str(i) for i in np.random.randint(0, 100000000, [10])]
        data = sorted(data,key=lambda x:len(x),reverse=True)
        print(data)
    
        _data_length = torch.LongTensor([len(i) for i in data])
        _data = torch.LongTensor([num_sequence.transform(i,max_len=config.max_len) for i in data])
        output = seq2seq.evaluate(_data,_data_length)
        print([num_sequence.inverse_transform(i) for i in output])
    
    if __name__ == '__main__':
        evalaute()
    

    目前存在问题???

    在model训练一个epoch之后,loss已经很低了,评估输出如下(为True表示预测正确):

    39304187 >>>>> 393041870 True
    41020882 >>>>> 410208820 True
    85784317 >>>>> 857843170 True
    1394232 >>>>> 13942320 True
    44548446 >>>>> 445484460 True
    49457730 >>>>> 494577300 True
    82451872 >>>>> 824518720 True
    64380958 >>>>> 643809580 True
    97501723 >>>>> 975017230 True
    21656800 >>>>> 216568000 True

    总结:

    seq2seq流程
    1. encoder
    	a. 对input 进行embedding
    	b. 对embedding结果进行打包 pack_padded_sequence
    	c. 传入gru进行计算,得到output和hidden
    	d. 对output进行解包 pad_packed_sequence
    2. decoder
    	a. 构造起始符,构造[batch_size, 1]的SOS,作为第一个时间步的输入
    	b. 对第一个时间步的输入进行embedding,得到embeded
    	c. 对embedded 进行gru计算,得到output 和 hidden,hidden 作为下一个时间步的hidden,
    	d. 计算第一个时间步输出的值:第一个时间步的输出进行变形,之后计算log_softmax,得到output,并获取值最大的位置dim = -1,作为第一个时间步的输出 
    	out = out.squeeze(1) # [batch_size, 1, hidden_size] --> [batch_size, hidden_size]
        output = F.log_softmax(self.fc(out), dim = -1)  # fc后,out [batch_size, hidden_size]-->[batch_size, vocab_size]
        e. 保存output
        # 保存decoder_output_t到decoder_outputs中
        decoder_outputs[:, t, :] = decoder_output_t
    	f. 第二个时间步,输入有:hidden和第一个时间步输出的具体值(是一个索引),使用teacher_forcing机制,加速训练
    	# 使用预测值,topk中k = 1,即获取最后一个维度的最大的一个值
        value, index = torch.topk(decoder_output_t, k = 1)
        	# 使用teacher_forcing机制,加速训练
           if random.random() > config.teacher.focing: 
               decoder_input = target[t] # [batch_size, 1]
           else:
               # 获取下一次的input
               decoder_input = index
        g. 重复b-f步,重复max_lenth次,即target长度(config.max_len+2 dataset中获取target时,max_len+1且add_eos=True,因此+2)
        h. 得到decoder_outputs
    3. train
    	a. output 和 target 计算nll_loss(带权损失),若是三阶,则需要变形
    	decoder_outputs = decoder_outputs.view(decoder_outputs.size(0) * decoder_outputs.size(1), -1) # [batch_size * seq_len, -1]
        target = target.view(-1) # [batch_size * seq_len]
        loss = F.nll_loss(decoder_outputs, target, ignore_index = config.num_sequence.PAD)
    4. eval
    	a. 和decoder大致相同,但是不需要保存output, 只需要batch数据每个时间步的输出
    	b. 每个时间步的输出放在列表中,其每一列才是输入的最终结果
    	indices = seq2seq.evaluate(input, input_length)
    	indices = np.array(indices).transpose()
    

     

    展开全文
  • 10 闲聊机器人的优化

    2021-02-21 23:06:46
    闲聊机器人的优化 目标 知道如何优化模型的效果 知道常见的优化手段 1. seq2seq中使用teacher forcing 在前面的seq2seq的案例中,我们介绍了teacher frocing是什么,当时我们的输入和输出很相似,所以当时我们的...

    闲聊机器人的优化

    目标

    1. 知道如何优化模型的效果
    2. 知道常见的优化手段

    1. seq2seq中使用teacher forcing

    在前面的seq2seq的案例中,我们介绍了teacher frocing是什么,当时我们的输入和输出很相似,所以当时我们的teacher forcing是在每个time step中实现的,那么现在我们的输入和输出不同的情况下,该如何使用呢?

    我们可以在每个batch遍历time step的外层使用teacher forcing

    代码如下:

    use_teacher_forcing = random.random() > 0.5
    if use_teacher_forcing: #使用teacher forcing
        for t in range(config.max_len):
            decoder_output_t, decoder_hidden, decoder_attn_t = self.forward_step(decoder_input, decoder_hidden,
                                                                                 encoder_outputs)
            decoder_outputs[:, t, :] = decoder_output_t
            #使用正确的输出作为下一步的输入
            decoder_input = target[:, t].unsqueeze(1)  # [batch_size,1]
    
    else:#不适用teacher forcing,使用预测的输出作为下一步的输入
        for t in range(config.max_len):
            decoder_output_t ,decoder_hidden,decoder_attn_t = self.forward_step(decoder_input,decoder_hidden,encoder_outputs)
            decoder_outputs[:,t,:] = decoder_output_t
            value, index = torch.topk(decoder_output_t, 1) # index [batch_size,1]
            decoder_input = index
    

    2. 使用梯度裁剪

    前面,我们给大家介绍了梯度消失(梯度过小,在多层计算后导致其值太小而无法计算)梯度爆炸(梯度过大,导致其值在多层的计算后太大而无法计算)

    在常见的深度神经网络中,特别是RNN中,我们经常会使用梯度裁剪的手段,来抑制过大的梯度,能够有效防止梯度爆炸。

    梯度裁剪的实现非常简单,仅仅只需要设置一个阈值,把梯度大于该阈值时设置为该阈值。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DcfLFSNF-1613919991691)(../images/2.3/grad_clip.png)]

    实现代码:

    loss.backward()
    #进行梯度裁剪
    nn.utils.clip_grad_norm_(model.parameters(),[5,10,15])
    optimizer.step()
    

    3. 其他优化方法

    1. 根据特定的问题,使用分类模型进行训练,然后再训练单独的回个该为题的为模型
      • 比如询问名字,可以使用fasttext先进行意图识别,命中询问名字分类后,直接返回名字
      • 或者是手动构造和名字相关的很多问题,来进行训练,从而能够更加个性化的回答出结果
    2. 直接对现有的语料进行修改和清洗,把语料中更多的答案进行替换,比如咨询名字的,咨询天气的等,这样能够更大程度上的回答出更加规范的答案
    3. 使用2.4 会讲的搜索模型,不再使用这种生成模型
    展开全文
  • 基于生成的闲聊机器人的应用A short introduction to Chatbots: Chatbots简短介绍: Conversational agents or dialogue systems, popularly known as chatbots are widely gaining popularity and importance ...
  • 基于Unilm模型的夸夸式闲聊机器人项目 项目描述 本项目是一个基于Unilm模型的夸夸式闲聊机器人项目。 本项目目前开源的模型仅使用豆瓣夸夸群数据训练,所以称之为夸夸式闲聊机器人。感兴趣的同学,也可以使用本项目...
  • 闲聊机器人 微软电源平台解决方案 (SOLUTIONS FOR MICROSOFT POWER PLATFORM) Whatever your Chatbot is designed for, and no matter how clear or simple its purpose is, your users will always try to chit-...
  • 闲聊机器人(chatbot),BERT句向量-相似度(Sentence Similarity),文本分类(Text classify) 数据增强(text augment enhance),同义句同义词生成,句子主干提取(mainpart),中文汉语短文本相似度,文本特征...
  • 闲聊机器人的介绍 目标 了解闲聊机器人是什么 介绍 在项目准备阶段我们知道,用户说了一句话后,会判断其意图,如果是想进行闲聊,那么就会调用闲聊模型返回结果,这是我们会在项目中实现的功能。 目前市面...
  • 基于GPT2的中文闲聊机器人/GPT2 for Chinese chitchat 项目github地址:中文闲聊机器人 该项目被微软DialoGPT官方仓库引用 UPDATE 2019.12.17 基于微软的论文DialoGPT:Large-Scale Generative Pre-training for ...
  • 我的闲聊机器人的不和谐版本,从头开始重写 // TODO使自述文件更好 起床并跑步 安装节点。 LTS版本应该没问题。 创建config.example.json的副本作为config.json 。 更新其中的值。 使用bot.sql生成一个名为bot....
  • 使用百度大脑构建一个闲聊机器人使用的库录下你所说的内容调用百度语音识别系统将录音文件转化为文字调用百度UINIT进行回答在主函数中检测键盘输入 代码上传到了Github 主要参考了这篇博客 使用的库 from aip import...
  • 今天谈谈聊天机器人,关于聊天机器人,大家肯定不陌生的。但是聊天机器人的效果的确不太让人满意。因为现在的聊天机器人的确比较智障。其智障的原因核心就是目前的技术体系的问题。技术体系不成熟,所以才让人感觉到...
  • QizNLP介绍:基于tensorflow(1.x)的NLP框架,提供NLP多种任务(分类、匹配、序列标注、生成等)代码模板,包括数据...深度学习中的单轮闲聊机器人(single-turn chitchat-bot),通常采用与机器翻译相同的处理范式,即序列.
  • 最近因为项目需要,用到聊天机器人。找了好几家都不太满意。要么是特别智障,要么是每天给的免费量太少了。难以满足使用。现在的大多数机器人还都是基于语料库检索匹配的。没有什么理解能力,因此智障是难免的。朋友...
  • 不知道大家对聊天机器人的感觉是怎么样的。现在的人工智能,已经被冠名人工智障,这或许和机器人有很大关系。真的,我也感觉现在的人工智能体系下的机器人真的比较智障。最近发现一家公司,道翰天琼,这家公司好像是...
  • 没办法,智能退而求其次,做一个文本交互的聊天机器人管家,并给它写个界面。 用java写,然后在linux装个jdk就行了。我们这里选择了最智能的道翰天琼未来机器人。他们是认知智能机器人,全新一代技术体系,的确很...
  • 道翰天琼企业聊天机器人开发者平台,对开发者很友好,业务人员也能自定义简单的聊天机器人。他们机器人是认知智能机器人,目前来说最智能的机器人。理解能力很强。不智障。推荐。下面是开发代码JAVA示例,其他示例到...
  • 自然语言处理最初发端于上个世纪60s的chatbot(聊天机器人),在这之前,图灵通过图灵猜想算是给聊天机器人种下了一颗种子。从上世纪60s到上世纪末,chatbot大约经历了三个重要的历史时期。我最近一直在研究聊天...
  • 从而能够让其跨平台运行,界面的设计以聊天器为样本,其服务打算采用道翰天琼的未来机器人进行开发,后面再把网页嵌入在各个平台的框架中,实现所谓的跨平台。直接上接口代码吧。要先到他们官网注册获取API等信息。 ...
  • 百度unit闲聊机器人

    2020-06-16 16:11:00
    发送给Unit聊天机器人接口, 并得到返回结果 res = requests.post(url=unit_chatbot_url, json=post_data) # 获取聊天接口返回数据 unit_chat_obj = json.loads(res.content) print(unit_chat_obj) # print(unit_...
  • 闲聊机器人API实现

    千次阅读 2018-01-10 13:33:59
    基于图灵机器人实现API实现 def tulingChat(self,question): KEY = '8afba6fdc75544f0bebc465615da1e0b' # change to your API KEY url = 'http://www.tuling123.com/openapi/api' req_info = question.encode...
  • QQ机器人是腾讯陆续推出的的人工智能聊天机器人的总称。 都说小Q妹妹聪明好学,我们能够教她说话。也能够请他帮忙查询邮编、手机号,或者解释成语、翻译成语,据说她还会查询手机号码归属地、应用科学计算器。 ...
  • QQ机器人是腾讯陆续推出的的人工智能聊天机器人的总称。 都说小Q妹妹聪明好学,我们能够教她说话。也能够请他帮忙查询邮编、手机号,或者解释成语、翻译成语,据说她还会查询手机号码归属地、应用科学计算器。 ...

空空如也

空空如也

1 2 3 4 5 ... 13
收藏数 250
精华内容 100
关键字:

闲聊机器人