文章目录
原代码GitHub 地址
README中给出了几种执行命令,我罗列并标记了相应参数的含义。
我们仍然从
main.py
看起,主要分为数据、模型几个部分
一、数据处理
定义了一个DataEngine的类,这个类是torch.utils.data.DataSet
的子类,用来进行数据集的梳理,这个处理过程主要在prepare_data
函数中。
1.1 prepare_data
1.1.1 数据加载及格式化
使用的是data/E2ENLG.py
加载、处理数据集。核心函数如下,我加了自己的注释理解.
def E2ENLG(
data_dir, is_spacy, is_lemma, fold_attr,
use_punct, min_length=-1, train=True):
"""
Args:
data_dir: string E2E数据集路径
is_spacy: int 1or0 无使用
is_lemma: int 1or0 无使用
fold_attr: int 1or0 是否将属性值及句子进行小写
min_length: int, 输出句子最小长度阈值
train: bool, use trainset.csv or testset_w_refs.csv
Returns:
input_data: list, [[槽位1:值1, 槽位2:值2, 槽位3:值3]]
input_attr_seqs: list [[槽位1,值1,槽位2,值2,槽位3,值3]]
output_labels: list [[词,词,词,...],[词,词,词,...]]
refs_list: list [[词,词,词,...],[词,词,词,...]]
sf_data: list [{槽位1:值1, 槽位2:值2, 槽位3:值3}]
"""
但是我还是有几个地方是不确定的:
E2E
并不是一个对话数据集,但是在处理过程中却考虑了相邻样本是否有相同的MR
,即相同属性用不同的句子描述;- 变量也使用了
dialogue
这种让人很困惑的名字。
1.1.2 处理词汇表
build_vocab
: 建立词-索引映射,并保存在指定路径下shrink_vocab
: 并根据输入参数限制词汇表大小。add_unk
: 并将不在词汇表中的那些词在输出标签中,置为特殊词_UNK
一处疑问:
self.training_set_label_samples = self.input_data
暂时没明白要干嘛
1.2 类的get属性
重新定义了__getitem__
功能,根据索引返回一条样本的相关数据。
def __getitem__(self, idx):
return (
self.input_data[idx],
self.output_labels[idx],
self.refs[idx],
self.sf_data[idx],
self.input_attr_seqs[idx]
)
二、对偶模型
Dual
类在module_dual.py
文件中,
初始化时,定义了nlu、nlg模型及模型的优化方式
训练阶段可以关注train_nlu
train_nlg
train_joint
三个函数
2.0 nn.Module
第一次使用pytorch
,先了解一下.
torch.nn是专门为神经网络设计的模块化接口。nn构建于autograd之上,可以用来定义和运行神经网络。
nn.Module是nn中十分重要的类,包含网络各层的定义及forward方法。
这篇博客讲的很好,可以看一下。
2.1 NLU模型
在src/module.py
中。NLURNN
是继承了RNNModel
,而RNNModel
又是继承了nn.Module
,隐藏层使用的是双向的GRU单元。其forward过程就是计算隐层,再加一个线性变换输出logits
2.2 NLG模型
在src/module.py
中。NLGRNN
也是继承了RNNModel
,隐层使用的是单向GRU单元。(存疑:必须使用单向).其forward过程其实调用的是自定义的forward_greedy
函数(因为beam_size就是传入的mid_sample_size,默认为1),和NLU的forward过程不同,在NLG的forward中,隐层先进行了一次线性变换,为的是后续decode这个循环的过程(每一次循环,输入都是当前输入和上一次隐层的输出,最后的输出再过一个softmax)。
三、训练过程
基准模型和对偶监督学习模型的训练过程都是分开训练nlu_loss和nlg_loss。核心函数为train_nlu
和train_nlg
联合对偶学习模型的训练过程则是将二者连一起。核心函数是train_joint
3.1 train_nlu
因为在这里讲NLU任务定义为一个多分类问题,所以用f1值来作为最后的指标。后面理由criterion_nlu
来完成相关指标计算,criterion_nlu也是集成了nn.Module
,所以后续直接使用criterion_nlu(**args)来完成Criterion.forward
过程(相当于调用了__call__方法)。
直接贴代码:
sup_loss, rl_loss, nlu_joint_prob, reward = criterion_nlu(
logits.cpu().unsqueeze(1).expand(-1, sample_size, -1),
targets.cpu(),
decisions=samples.cpu(),
n_supervise=1,
calculate_reward=reinforce
)
-
sup_loss, rl_loss
的计算过程见Criterion.get_scheduled_loss
(实际什么都没做直接返回了) -
nlu_joint_prob
的计算过程见Criterion.get_log_joint_prob_nlu
(存疑)。 -
reward
的计算过程中,由于使用的是默认的args.primal_reinforce=False
,所以通过get_reward
这个函数得到reward
(存疑,计算是需要知道reward_type
,但无论nlu、nlg在执行main函数时都没有指定这个参数,所以用的是默认参数none。而get_reward中没有这个类型的reward计算,所以reward是0)
3.2 train_nlg
对比着看nlu和nlg的核心train_batch函数更加清晰:输入、传播、计算loss
直接贴计算loss的过程:
sup_loss, rl_loss, nlg_joint_prob, reward = criterion(
logits.cpu(),
labels.cpu(),
decisions=decisions.cpu(),
n_supervise=1,
calculate_reward=reinforce
)
nlg_joint_prob
计算过程见get_log_joint_prob_nlg
,主要是一个softmax操作- 其他loss和nlu的loss计算过程一样,
碎碎念
- 其实还是没有很理解,尤其是在NLU作为分类任务时的评价相关
- 个人对代码的一些注释GitHub