- 感觉
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命令,通过suprocess
的check_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
,然后使用subword
的model
进行切词。
- 回到
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
来说input
和target
构造好,以及句子的长度
- 接下来是
sampler
和loader
,
-
BucketBatchSampler
就是根据句子的长度进行排序(这样做的目的是让一个batch
内部的数据尽可能长度是一致的),然后返回。
- 有一个很有意思的是是虽然设置的是,前面的一个是将数据分成预设的
batch size
,也就是平均会有1/2
个batch 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
的做crossEntropyLoss
,optimizer = 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
- 创造了一个
embedding
层Embed
和一个lstm
层RNNModule
,共享参数是这么实现的,首先我们可以看到模型的大多数参数都在从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
- 这是作者写的一个训练中数据可视化的一个东西,本质上是
visdom
和matplotlib
里面的东西,感觉好像是作者写了一个类似于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
中的epoch
和step
,然后train_epoch
和eval_epoch
进行训练和测试返回的是各自的loss
。
-
先看一下
Trainer
需要的传入参数,值的注意的是每个epoch训练完有一个后处理的函数,用于记录是否到了log
的epoch
了,如果是的那么就进行记录,这个后处理函数的接口是封装好的,就是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
的时候一般得到的是sos
的embedding
,其他时候可以选择使用gumbel_softmax
得到一个embedding
- 有一个很奇怪的东西就是为啥要学出一个
_init_input_feed
- 为啥要给这里加这么多的信息,如果
ho
是上次的decoder
的output
信息那么然后这里和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_idf
的embedding
size
是1,requires_grad
为False
,
-
embedding
进行初始化
-
seq3
中生成句子。
-
decoder
需要gold_tokens
,而作者的是无监督的方法是没有正确答案的,因此制造了一个inp_fake
为了在decoder
里面可以得到batchsize
和max_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 loss
,length
u是pack_padded_sequence
再用但是encoder
的时候是可以得到真实的句子长度的啊,感觉是下面的没有写好,应该根据dec1_results得到真实的length
,而不是用latent_lengths - 1
得到。
-
decoder
的输出,有单词的概率分布,然后使用这个概率分布进行反向传递,当做是encode
的输入单词的embedding
。 - 然后使用相当于压缩后的句子进行
encoder
得到的中间表示再去
-
bridge
就是对encoder
的hidden state
进行一些线性和非线性的变换到decoder
可以用到的hidden state
的维度然后当做hidden state
的初始化。
train seq3.py
- 映射矩阵从预训练的词向量中进行初始化。
-
将
unk
和预训练词表中没有的词的gradient mask
掉,训练的时候不训练。,这个操作也太强了,why?,是通过weight
的register_hook
来实现的。
-
通过
sklearn
计算tf idf
,tf.idf_
,但是要对PAD
这个token
进行neutralize
就是设置为1
,让pad
不要影响计算。
-
让所有的
embedding
层是一个,十分简单,因为前面只初始化了一个inp_encoder
,所以只用embed
指向其他的即可。
-
共享参数,不使用同一个
decoder
但是不同decoder
的映射词表的矩阵是一样的。 -
打印参数信息,注意之所以能够
print model
是因为实现了__str__
方法。 - 添加一些可以可视化的东西。
trainer seq3
topic loss
-
_topic_loss
,dec1[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 loss
和topic Loss
,在这里可以看到其实是吧tf-idf
当成了attention
的权重对于embedding
进行加权。
-
还有一个句子长度的
loss
,所以说生成的compress
的时候生成的句子长度不是强行确定就是那么长的,而是使用了loss
来限制的,mask
,当eos
位置一致的时候loss
是最小的,此时eos
前面的部分eos_labels
是0
,eos
后面的部分_logits
是0
,所以只有多出来的部分才算loss
,
-
generator
生成,感觉这一块是不是有问题啊,gumbel_softmax
采样的概率虽然是根据logit
采样的,但是慢了一步,为何不用统一使用embedding
呢 -
logit
是decoder
生成的词表的logit
,dists
是从gumbel_softmax
中根据上一步的logit
和温度采样出来的一个概率分布,然后此时的embedding
也是根据这个概率分布得到的。 -
_process_batch
返回值有两个一个是loss
,此处是一个列表,还可以是一个单独的值,第二个batch_outputs
就是一些像打印出来的信息。
-
eval_epoch
重写了基础类中的eval_epoch
。
-
而重写的
eval_epoch
直接是生成了句子,并没有计算loss
,有原因可能是并不容易过拟合,所以只用人来看一下生成的句子是否合理。 -
之前先把
model
的device
给trainer
,然后训练和测试的时候再把此时的batch
给to(device)
,这样做为了省显存。
batch = list(map(lambda x: x.to(self.device), batch))
-
train_epoch
和test_epoch
是在seq3.py
中调用的,而train
和eval
中调用这这个用于根据把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
,计算的,所以说神经网络在训练的时候前项干了什么并不重要,重要的是怎么更新后项,一般的是用前项的直接结果计算,另一种
网友评论