美文网首页
seq3代码

seq3代码

作者: VanJordan | 来源:发表于2019-06-09 20:00 被阅读0次
    • 感觉seq3代码是我看到的写得前2好的代码,另外的代码是transformer-xl
    • 所有的超参数都是从yaml文件中读取的,训练的时候只用指定配置文件即可,感觉和把超参放入sh文件中的方法差不多好。train_options函数返回return args, config,其中config是将yaml文件里面的东西变成了逐层的字典。
    python models/sent_lm.py --config model_configs/camera/lm_prior.yaml 
    

    sent_lm

    • 先是使用train_options读取参数,读取出来的config是一个字典。
    • 可以看到arg里面主要是对这次训练的描述,比如是否进行可视化,本次训练的名字是什么,描述是什么,resume是重新训练的ckpt的地址,使用gpu还是cpu等等,而config里面就是本次训练的超参数。
    • 几种加载词典的方式。
    • 加载数据。

    dataset 数据加载

    • 首先所有的dataset都继承自torch.utils.data中的Dataset,有两种构建单词表的方法,一种是subword的一种是普通的
    • 一个写的比较好的地方就是和fairseq他们一样,首先对原始的文本进行处理,然后存放一个处理后的二进制文本,然后以后每次训练的时候只是使用这个二进制文件,这里是用pickle实现的,注意这个直接是吧一个函数的返回值完全打包了,而且是根据输入的文件名通过
            args_str = ''.join(args_to_str(args))
            key = hashlib.md5(args_str.encode()).hexdigest()
            cache_file = os.path.join(cache_dir, key)
    
    • 创建了一个独特的读取码,然后判断文件存不存在,如果存在的话那么就使用pickle读取文件,如果是第一次调用不存在的话那么就先读取data,然后创建。
    def disk_memoize(func):
        cache_dir = os.path.join(BASE_DIR, "_cache")
        if not os.path.exists(cache_dir):
            os.makedirs(cache_dir)
    
        @functools.wraps(func)
        def wrapper_decorator(*args, **kwargs):
            # check fn arguments
            args_str = ''.join(args_to_str(args))
            key = hashlib.md5(args_str.encode()).hexdigest()
            cache_file = os.path.join(cache_dir, key)
    
            if os.path.exists(cache_file):
                print(f"Loading {cache_file} from cache!")
                with open(cache_file, 'rb') as f:
                    return pickle.load(f)
            else:
                print(f"No cache file for {cache_file}...")
                data = func(*args, **kwargs)
    
                with open(cache_file, 'wb') as pickle_file:
                    pickle.dump(data, pickle_file)
    
                return data
    
        return wrapper_decorator
    
    # @disk_memoize
    def read_corpus(file, tokenize):
        _vocab = Vocab()
    
        _data = []
        for line in iterate_data(file):
            tokens = tokenize(line)
            _vocab.read_sequence(tokens)
            _data.append(tokens)
    
        return _vocab, _data
    
    • iterate_data也写的很好,使用isinstance来判断,如果传入的是字符串,那么就open,如果传入的是一个可迭代的collections.Iterable那么就直接yield,而且这里的tqdm写的也很好的,total指定全部的数据,wc是使用linux里面的wc命令,通过suprocesscheck_output调用。
    from subprocess import check_output
    def wc(filename):
        return int(check_output(["wc", "-l", filename]).split()[0])
    
    def iterate_data(data):
        if isinstance(data, str):
            assert os.path.exists(data), f"path `{data}` does not exist!"
            with open(data, "r") as f:
                for line in tqdm(f, total=wc(data), desc=f"Reading {data}..."):
                    if len(line.strip()) > 0:
                        yield line
    
        elif isinstance(data, collections.Iterable):
            for x in data:
                yield x
    
    • 如果是使用subword,那么就要import sentencepiece as spm,然后使用subwordmodel进行切词。
    • 回到BaseLMDataset上,可以看到里面有一个__str__方法目的是为了将这个类变成一个字符串,这样直接print这个类的实例的话,就会将__str__中的东西打印出来。我们还可以看到作者这里使用了一个,这样就可以将一个列表按照表格的形式显示。
    from tabulate import tabulate
    return tabulate([[x[1] for x in props]], headers=[x[0] for x in props])
    
    from tabulate import tabulate
    
    props = [('ni','7889'),('taa','890988'),('you','safdkk')]
    print(tabulate([[x[1] for x in props]], headers=[x[0] for x in props])) 
    ---------------------------------------输入----------------------------------
      ni     taa  you
    ----  ------  ------
    7889  890988  safdkk
    
    
    • BaseLMDataset介绍完了接下来是class SentenceLMDataset(BaseLMDataset):这个很简单只用实例化__len____getitem__,因为self.data是一个列表,因此对于LM来说inputtarget构造好,以及句子的长度
    • 接下来是samplerloader
    • BucketBatchSampler就是根据句子的长度进行排序(这样做的目的是让一个batch内部的数据尽可能长度是一致的),然后返回。
    • 有一个很有意思的是是虽然设置的是,前面的一个是将数据分成预设的batch size,也就是平均会有1/2batch size的数据被丢弃掉,而用了第二种的话会让batch size接近预设的batch size但是丢弃的数据是最少的。
    • from torch.nn.utils.rnn import pad_sequence,在新的位置按照0来填充,填充到最大长度。
    • collate_fn是将batch_sampler中,我们可以看到_collate函数的传入参数是和train_set中的__getitem__的返回值是一样的。
    • 也就是说顺序是train_set提供__len____getitem__train_sampler打包成batch,然后collate_fn做后处理,就是进行Padding和转换成LongTensor

    model 构建模型

    • 模型的总体,loss_function = nn.CrossEntropyLoss(ignore_index=0)直接就可以不对mask的做crossEntropyLossoptimizer = optim.Adam(parameters, lr=config["lr"], weight_decay=config["weight_decay"])l2正则化等价于weight_decay,得到模型后model.to(device)
    • tensor.numel()返回的是这个tensor中总共有多少个tensor
    >>> a = torch.randn(1, 2, 3, 4, 5)
    >>> torch.numel(a)
    
    • 统计有多少参数,以一个易懂的方式展现出来。

    model中的RNNModule

    • 众所周知,lstm中的rnn需要进去的时候降序,然后出来的时候再恢复原始的顺序,可以看到作者的方法还是比较巧妙的,先对tensor进行降序得到降序的下标,然后对下标再升序排序之后得到的就是能恢复的顺序,x = x[sorted_i]outputs = out_unpacked[reverse_i]
    class RNNModule(nn.Module, RecurrentHelper):
        def __init__(self, input_size,
                     rnn_size,
                     num_layers=1,
                     bidirectional=False,
                     dropout=0.,
                     pack=True, last=False, countdown=False):
            """
            A simple RNN Encoder, which produces a fixed vector representation
            for a variable length sequence of feature vectors, using the output
            at the last timestep of the RNN.
            Args:
                input_size (int): the size of the input features
                rnn_size (int):
                num_layers (int):
                bidirectional (bool):
                dropout (float):
            """
            super(RNNModule, self).__init__()
    
            self.pack = pack
            self.last = last
            self.countdown = countdown
    
            if self.countdown:
                self.Wt = nn.Parameter(torch.rand(1))
                input_size += 1
    
            self.rnn = nn.LSTM(input_size=input_size,
                               hidden_size=rnn_size,
                               num_layers=num_layers,
                               bidirectional=bidirectional,
                               batch_first=True)
    
            # the dropout "layer" for the output of the RNN
            self.dropout = nn.Dropout(dropout)
    
            # define output feature size
            self.feature_size = rnn_size
    
            # double if bidirectional
            if bidirectional:
                self.feature_size *= 2
    
        @staticmethod
        def reorder_hidden(hidden, order):
            if isinstance(hidden, tuple):
                hidden = hidden[0][:, order, :], hidden[1][:, order, :]
            else:
                hidden = hidden[:, order, :]
    
            return hidden
    
        def forward(self, x, hidden=None, lengths=None):
    
            batch, max_length, feat_size = x.size()
    
            if lengths is not None and self.pack:
    
                ###############################################
                # sorting
                ###############################################
                lenghts_sorted, sorted_i = lengths.sort(descending=True)
                _, reverse_i = sorted_i.sort()
    
                x = x[sorted_i]
    
                if hidden is not None:
                    hidden = self.reorder_hidden(hidden, sorted_i)
    
                ###############################################
                # forward
                ###############################################
    
                if self.countdown:
                    ticks = length_countdown(lenghts_sorted).float() * self.Wt
                    x = torch.cat([x, ticks.unsqueeze(-1)], -1)
    
                packed = pack_padded_sequence(x, lenghts_sorted, batch_first=True)
    
                self.rnn.flatten_parameters()
                out_packed, hidden = self.rnn(packed, hidden)
    
                out_unpacked, _lengths = pad_packed_sequence(out_packed,
                                                             batch_first=True,
                                                             total_length=max_length)
    
                out_unpacked = self.dropout(out_unpacked)
    
                ###############################################
                # un-sorting
                ###############################################
                outputs = out_unpacked[reverse_i]
                hidden = self.reorder_hidden(hidden, reverse_i)
    
            else:
                # todo: make hidden return the true last states
                self.rnn.flatten_parameters()
                outputs, hidden = self.rnn(x, hidden)
                outputs = self.dropout(outputs)
    
            if self.last:
                return outputs, hidden, self.last_timestep(outputs, lengths,
                                                           self.rnn.bidirectional)
    
            return outputs, hidden
    
    
    • 设置batch_first=True,然后复原原来的顺序,所以说迭几层的LSTM实现起来真的很简单。
    • hidden_reorder的实现很简单,hidden的顺序应该是seqLen bs hs,所以把第二维进行reorder就行了

    SeqReader

    • 创造了一个embeddingEmbed和一个lstmRNNModule,共享参数是这么实现的,首先我们可以看到模型的大多数参数都在从rnn的out映射到词表这个矩阵上的,所以是否我们可以将这个矩阵用embedding矩阵共享参数,因为rnn_size的大小和embedding size的大小可能不一样,所以我们可以先让rnn_size过一个linear,这个linear的参数会小很多,就是图中的down,共享参数也很简单就是self.out.weight = self.embed.embedding.weight
    • self.decode是因为虽然这个只是一个encoder,但是可以根据encoder出来的结果映射成真实的vocab也就是encoder成能懂的句子。
    • optimizer里面带有要训练的参数,而loss_function就是一个单独的loss_function
    loss_function = nn.CrossEntropyLoss(ignore_index=0)
    parameters = filter(lambda p: p.requires_grad, model.parameters())
    optimizer = optim.Adam(parameters,
                           lr=config["lr"], weight_decay=config["weight_decay"])
    

    trainer

    • 这是作者写的一个训练中数据可视化的一个东西,本质上是visdommatplotlib里面的东西,感觉好像是作者写了一个类似于tensorboard的东西。
    • trainer里面存放了如何加载数据进行训练,以及现在训练到了什么epoch,第多少step
    best_loss = None
    for epoch in range(config["epochs"]):
        train_loss = trainer.train_epoch()
        val_loss = trainer.eval_epoch()
    
        if config["scheduler"] == "plateau":
            scheduler.step(val_loss)
    
        elif config["scheduler"] == "cosine":
            scheduler.step()
        elif config["scheduler"] == "step":
            scheduler.step()
    
        exp.update_metric("lr", optimizer.param_groups[0]['lr'])
    
        exp.update_metric("ep_loss", train_loss, "TRAIN")
        exp.update_metric("ep_loss", val_loss, "VAL")
        exp.update_metric("ep_ppl", math.exp(train_loss), "TRAIN")
        exp.update_metric("ep_ppl", math.exp(val_loss), "VAL")
    
        print()
        epoch_log = exp.log_metrics(["ep_loss", "ep_ppl"])
        print(epoch_log)
        exp.update_value("epoch", epoch_log)
    
        # Save the model if the validation loss is the best we've seen so far.
        if not best_loss or val_loss < best_loss:
            best_loss = val_loss
            trainer.checkpoint()
    
        print("\n" * 2)
    
        exp.save()
    
    
    • trainer 需要做的事情,如果是重启的话要复原trainer中的epochstep,然后train_epocheval_epoch进行训练和测试返回的是各自的loss

    • 先看一下Trainer需要的传入参数,值的注意的是每个epoch训练完有一个后处理的函数,用于记录是否到了logepoch了,如果是的那么就进行记录,这个后处理函数的接口是封装好的,就是trainer的几个数据。

    • 封装的非常好,我们只需要实现一个如何计算loss的,以及当前有什么状态的即可

    class LMTrainer(Trainer):
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
        def _seq_loss(self, predictions, labels):
            _labels = labels.contiguous().view(-1)
    
            _logits = predictions[0]
            _logits = _logits.contiguous().view(-1, _logits.size(-1))
            loss = self.criterion(_logits, _labels)
    
            return loss
    
        def _process_batch(self, inputs, labels, lengths):
            predictions = self.model(inputs, None, lengths)
    
            loss = self._seq_loss(predictions, labels)
            del predictions
            predictions = None
    
            return loss, predictions
    
        def get_state(self):
            if self.train_loader.dataset.subword:
                _vocab = self.train_loader.dataset.subword_path
            else:
                _vocab = self.train_loader.dataset.vocab
    
            state = {
                "config": self.config,
                "epoch": self.epoch,
                "step": self.step,
                "model": self.model.state_dict(),
                "model_class": self.model.__class__.__name__,
                "optimizers": [x.state_dict() for x in self.optimizers],
                "vocab": _vocab,
            }
    
            return state
    
    • _process_batch,就是根据训练数据计算得到loss,这个函数无论是训练和测试的时候都可以用,除过loss第二个prediction是被传入batch_end_callbacks中的output,其他的地方没有用了。
    • get_state是要被写入checkpoint中的东西,其中将config也写进去了。
    • 至此sent_lm的部分算是完成了,接下来是seq3部分

    seq3

    • 加载LM模型

    AEDataset

    • 主要还是重构__getitem__这个函数,然后加载数据的在BaseLMDataset上已经有了read_corpus,然后self.data中就是存放的训练数据。
    • collate_fn里面转换成longtensor以及进行pad,这里不用排序,在RNN里面的forward的时候才进行排序,这里的pad_sequence是来自from torch.nn.utils.rnn import pad_sequence,按照这个batch中最大的进行填充,填充的value默认是0,但是注意要设置成batch_first
    • 继承的这个seqCollate是自己写的,使用__call__解包然后调用_collate

    Model for seq3

    • SeqReader是一个encoder,如果要训练lm设置这里的decode为True,否则的话就是一个纯的encoder
    • 设置batch_first的地方有两个一个是pad_sequence的时候,一个是LSTM的时候
    • decoder就是创建一个LSTM和一个Attention
    • decoder中的get_embedding,除过step=0的时候一般得到的是sosembedding ,其他时候可以选择使用gumbel_softmax得到一个embedding
    • 有一个很奇怪的东西就是为啥要学出一个_init_input_feed
    • 为啥要给这里加这么多的信息,如果ho是上次的decoderoutput信息那么然后这里和decoder出来的生成的emebdding结合起来感觉仍然是很合理的,因此生成embedding的过程中使用了argmax相当于是信息的损失,这里把损失的信息进行了补充。
    • 这里的attention是这么做的,感觉非常的合理,而且decoder的时候是一个step一个step做的。我们可以看到ho就是decoder的输出加上context信息。
    • 控制长度这一块还看不懂。
    • 温度decoder的时候,embedding的获取方式可以是teacher forcing,可以是gready search 也可以是根据gamble_softmax加权的取出来。
    • softplus是平滑一点的relu,学习温度,首先是decoder出来的结果先过一个linear(没有bias)映射到一个数字,然后过一个softplus的激活函数然后加上原始的温度,然后被1除就是学习到的当前的温度

    • 连接以后作为decoder的返回
    • topic_idfembedding size是1,requires_gradFalse
    • embedding进行初始化
    • seq3中生成句子。
    • decoder需要gold_tokens,而作者的是无监督的方法是没有正确答案的,因此制造了一个inp_fake为了在decoder里面可以得到batchsizemax_length
    • length_countdown
    def length_countdown(lengths):
        batch_size = lengths.size(0)
        max_length = max(lengths)
        desired_lengths = lengths - 1
    
        _range = torch.arange(0, -max_length, -1, device=lengths.device)
        _range = _range.repeat(batch_size, 1)
        _countdown = _range + desired_lengths.unsqueeze(-1)
    
        return _countdown
    
    lengths = torch.LongTensor([1,2,3])
    print(length_countdown(lengths))
    ---------------------------------------输出------------------------------------------
    tensor([[ 0, -1, -2],
            [ 1,  0, -1],
            [ 2,  1,  0]])
    
    • 但是这个countdown也不是硬性截断的,而是乘了一个可学习的参数w_tick
    • tick这个生成的是[[1, 0.3], [-1, 0.5]]这种形式的前一个数字是距离结束还可以生成多少个单词1表示还可以生成1个单词,-1表示还可以生成-1个单词(表示多生成了-1个单词)0.3, 0.5是压缩后的句子应该是原始句子的长度。
    • cmp_lengths代表长度是否说明,直接是截断的最大长度??其实并不是截断的最大长度,因为后面还有一个length losslengthu是pack_padded_sequence再用但是encoder的时候是可以得到真实的句子长度的啊,感觉是下面的没有写好,应该根据dec1_results得到真实的length,而不是用latent_lengths - 1得到。
    • decoder的输出,有单词的概率分布,然后使用这个概率分布进行反向传递,当做是encode的输入单词的embedding
    • 然后使用相当于压缩后的句子进行encoder得到的中间表示再去
    • bridge就是对encoderhidden state进行一些线性和非线性的变换到decoder可以用到的hidden state 的维度然后当做hidden state的初始化。

    train seq3.py

    • 映射矩阵从预训练的词向量中进行初始化。
    • unk和预训练词表中没有的词的gradient mask掉,训练的时候不训练。,这个操作也太强了,why?,是通过weightregister_hook来实现的。

    • 通过sklearn计算tf idftf.idf_,但是要对PAD这个token进行neutralize就是设置为1,让pad不要影响计算。

    • 让所有的embedding层是一个,十分简单,因为前面只初始化了一个inp_encoder,所以只用embed指向其他的即可。

    • 共享参数,不使用同一个decoder但是不同decoder的映射词表的矩阵是一样的。

    • 打印参数信息,注意之所以能够print model是因为实现了__str__方法。

    • 添加一些可以可视化的东西。

    trainer seq3

    topic loss

    • _topic_lossdec1[3]的decoder的第三个输出,是decoder每个词的概率。
    • 用每个词的idf进行训练,
    • 计算方式是emb乘以normal后的tf-idf值,然后按照最后一个维度相加,得到的一个embedding维度的数值进行运算,其实光average一个embedding得到的也是很合理的就是为了让平均的embedding尽可能的相似。
    • 但是蛋疼的一点就是只对source端做tf-idf,不知道为啥对sourse端没有做,直接注释掉了。

    lm loss

    • 很简单就是算一个之前的decoder结果的logits和现在的logits算一下kl散度即可

    _process_batch

    • 首先得到模型的输出,然后利用输出计算loss

    • 计算重建损失。
    • 然后是上面两种计算LM losstopic Loss,在这里可以看到其实是吧tf-idf当成了attention的权重对于embedding进行加权。

    • 还有一个句子长度的loss,所以说生成的compress的时候生成的句子长度不是强行确定就是那么长的,而是使用了loss来限制的,mask,当eos位置一致的时候loss是最小的,此时eos前面的部分eos_labels0eos后面的部分_logits0,所以只有多出来的部分才算loss

    • generator生成,感觉这一块是不是有问题啊,gumbel_softmax采样的概率虽然是根据logit采样的,但是慢了一步,为何不用统一使用embedding

    • logitdecoder生成的词表的logitdists是从gumbel_softmax中根据上一步的logit和温度采样出来的一个概率分布,然后此时的embedding也是根据这个概率分布得到的。

    • _process_batch返回值有两个一个是loss,此处是一个列表,还可以是一个单独的值,第二个batch_outputs就是一些像打印出来的信息。

    • eval_epoch重写了基础类中的eval_epoch

    • 而重写的eval_epoch直接是生成了句子,并没有计算loss,有原因可能是并不容易过拟合,所以只用人来看一下生成的句子是否合理。

    • 之前先把modeldevicetrainer,然后训练和测试的时候再把此时的batchto(device),这样做为了省显存。

    batch = list(map(lambda x: x.to(self.device), batch))
    
    • train_epochtest_epoch是在seq3.py中调用的,而traineval中调用这这个用于根据把batch 计算loss的函数,这里得到的loss是原始的loss,可以在train_epoch进行梯度裁剪更新参数等其他动作。
    • 原来重写eval_epoch不计算loss的原因是要使用rouge作为评测标准而不是valid上面的loss
    • 重新看一下trainer,里面可以传入一个loss_weight
    • 每次执行完一个train_epoch的时候就会挨个执行batch_end_callbacks里面的函数。

    关于前项正常的sample,后项使用概率近似的进行可微的反向传递

    • 实现起来很简单,就是计算loss的时候用的不是真实的采样出来的单词,而是用dec2_logits
    • topic loss这个时候也一样,就是前项的时候根据gambel_softmax的概率取得最大的概率的单词,然后过LM得到logits_oracle,计算loss的时候却是用原始的logits_dec1,计算的,所以说神经网络在训练的时候前项干了什么并不重要,重要的是怎么更新后项,一般的是用前项的直接结果计算,另一种

    相关文章

      网友评论

          本文标题:seq3代码

          本文链接:https://www.haomeiwen.com/subject/fpuwxctx.html