今天我们来做NLP(自然语言处理)中Sequence2Sequence的任务。其中Sequence2Sequence任务在生活中最常见的应用场景就是机器翻译。除了机器翻译之外,现在很流行的对话机器人任务,摘要生成任务都是典型的Sequence2Sequence。Sequence2Sequence的难点在于模型需要干两件比较难的事情:
- 语义理解(NLU:Natural Language Understanding):模型必须理解输入的句子。
- 句子生成(NLG:Natural Language Generation):模型生成的句子需符合句法,不能是人类觉得不通顺的句子。
想想看,让模型理解输入句子的语义已经很困难了,还得需要它返回一个符合人类造句句法的序列。不过还是那句话,没有什么是深度学习不能解决的,如果有,当我没说上句话。
Sequence2Sequence任务简介
Sequence2Sequence是一个给模型输入一串序列,模型输出同样是一串序列的任务和序列标注有些类似。但是序列标注的的输出是定长的,标签于输入一一对应,而且其标签类别也很少。Sequence2Sequence则不同,它不需要输入与输出等长。
Sequence2Sequence算法简介
Sequence2Sequence是2014年由Google 和 Yoshua Bengio提出的,这里分别是Google论文和Yoshua Bengio论文的下载地址。从此之后seq2seq算法就开始不断演化发展出不同的版本,不过万变不离其宗,其整体架构永远是一个encode-decode模型。下面简要介绍四种seq2seq的架构。
1.basic encoder-decoder :将encode出来的编码全部丢给decode每个step。
2.encoder-decoder with feedback :将encode出来的编码只喂给decode的初始step,在解码器端,需将每个step的输出,输入给下一个step。
3.encoder-decoder with peek:1和2的组合,不仅将encode出来的编码全部丢给decode每个step,在解码器端,也将每个step的输出,输入给下一个step。
4.encoder-decoder with attention:将3模型的encode端做了一个小小的改进,加入了attention机制,简单来说,就是对encode端每个step的输入做了一个重要性打分。
本次实验采用的是basic encoder-decoder架构,下面开始实战部分。
对对联实战
数据加载
数据样式如下图所示是一对对联。模型的输入时一句"晚 风 摇 树 树 还 挺",需要模型生成" 晨 露 润 花 花 更 红"。这个数据集有个特点,就是输入输出是等长的,序列标注算法在这个数据集上也是适用的。
with open ("./couplet/train/in.txt","r") as f:
data_in = f.read()
with open ("./couplet/train/out.txt","r") as f:
data_out = f.read()
data_in_list = data_in.split("\n")
data_out_list = data_out.split("\n")
data_in_list = [data.split() for data in data_in_list]
data_out_list = [data.split() for data in data_out_list]
执行上方代码将数据变成list,其格式如下:
data_in_list[1:3] :
[['愿', '景', '天', '成', '无', '墨', '迹'], ['丹', '枫', '江', '冷', '人', '初', '去']]
data_out_list[1:3]:
[['万', '方', '乐', '奏', '有', '于', '阗'], ['绿', '柳', '堤', '新', '燕', '复', '来']]
构造字典
import itertools
words_all = list(itertools.chain.from_iterable(data_in_list))+list(itertools.chain.from_iterable(data_out_list))
words_all = set(words_all)
vocab = {j:i+1 for i ,j in enumerate(words_all)}
vocab["unk"] = 0
通过上方代码构造一个字典,其格式如下所示,字典的作用就是将字变成计算机能处理的id。
{'罇': 1,
'鳣': 2,
'盘': 3,
...
'弃': 168,
'厌': 169,
'楞': 170,
'杋': 171,
...
}
数据预处理
from keras.preprocessing.sequence import pad_sequences
data_in2id = [[vocab.get(word,0) for word in sen] for sen in data_in_list]
data_out2id = [[vocab.get(word,0) for word in sen] for sen in data_out_list]
train_data = pad_sequences(data_in2id,100)
train_label = pad_sequences(data_out2id,100)
train_label_input = train_label.reshape(*train_label.shape, 1)
执行上方代码将数据padding成等长(100维),后续方便喂给模型。其中需要注意的是需要给train_label扩充一个维度,原因是由于keras的sparse_categorical_crossentropy loss需要输入的3维的数据。
模型构建
from keras.models import Model,Sequential
from keras.layers import GRU, Input, Dense, TimeDistributed, Activation, RepeatVector, Bidirectional
from keras.layers import Embedding
from keras.optimizers import Adam
from keras.losses import sparse_categorical_crossentropy
def seq2seq_model(input_length,output_sequence_length,vocab_size):
model = Sequential()
model.add(Embedding(input_dim=vocab_size,output_dim = 128,input_length=input_length))
model.add(Bidirectional(GRU(128, return_sequences = False)))
model.add(Dense(128, activation="relu"))
model.add(RepeatVector(output_sequence_length))
model.add(Bidirectional(GRU(128, return_sequences = True)))
model.add(TimeDistributed(Dense(vocab_size, activation = 'softmax')))
model.compile(loss = sparse_categorical_crossentropy,
optimizer = Adam(1e-3))
model.summary()
return model
model = seq2seq_model(train_data.shape[1],train_label.shape[1],len(vocab))
模型构建,keras可以很方便的帮助我们构建seq2seq模型,这里的encode 和decode采用的都是双向GRU。其中RepeatVector(output_sequence_length) 这一步,就是执行将encode的编码输入给decode的每一个step的操作。从下图的模型可视化输出可以看到这个basic的seq2seq有39万多个参数需要学习,简直可怕。
模型训练
模型构建好之后,就可以开始训练起来了。需要做的是将输入数据喂给模型,同时定义好batch_size和epoch。
model.fit(train_data,train_label_input, batch_size =32, epochs =1, validation_split = 0.2)
下图是模型训练的过程,一个epoch大概需要近1小时,loss缓慢降低中。
模型预测
import numpy as np
input_sen ="国破山河在"
char2id = [vocab.get(i,0) for i in input_sen]
input_data = pad_sequences([char2id],100)
result = model.predict(input_data)[0][-len(input_sen):]
result_label = [np.argmax(i) for i in result]
dict_res = {i:j for j,i in vocab.items()}
print([dict_res.get(i) for i in result_label])
训练10个epoch后,是时候考考模型的对对联实力了,运行上方代码,就可以看到模型的预测效果。
“国破山河在”对出“人来日月长”,确实很工整。看来模型学习得不错,晋升为江湖第一对穿肠啦。
结语
Seq2Seq在解决句子生成任务确实实力雄厚,仅仅只用了最basic的ecode和decode就能对出如此工整的句子(当然不是所有的句子都能对得这么好)。如果使用更强的模型训练对对联模型,实力应该可以考个古代状元。所以,大家有没有开始对深度学习处理NLP问题产生好奇,学习起来吧。
参考:
https://kexue.fm/archives/6270
https://blog.csdn.net/sinat_26917383/article/details/75050225