今天我們來做NLP(自然語言處理)中Sequence2Sequence的任務(wù)。其中Sequence2Sequence任務(wù)在生活中最常見的應(yīng)用場(chǎng)景就是機(jī)器翻譯。除了機(jī)器翻譯之外,現(xiàn)在很流行的對(duì)話機(jī)器人任務(wù),摘要生成任務(wù)都是典型的Sequence2Sequence。Sequence2Sequence的難點(diǎn)在于模型需要干兩件比較難的事情:
- 語義理解(NLU:Natural Language Understanding):模型必須理解輸入的句子。
- 句子生成(NLG:Natural Language Generation):模型生成的句子需符合句法,不能是人類覺得不通順的句子。
想想看,讓模型理解輸入句子的語義已經(jīng)很困難了,還得需要它返回一個(gè)符合人類造句句法的序列。不過還是那句話,沒有什么是深度學(xué)習(xí)不能解決的,如果有,當(dāng)我沒說上句話。
Sequence2Sequence任務(wù)簡(jiǎn)介
Sequence2Sequence是一個(gè)給模型輸入一串序列,模型輸出同樣是一串序列的任務(wù)和序列標(biāo)注有些類似。但是序列標(biāo)注的的輸出是定長(zhǎng)的,標(biāo)簽于輸入一一對(duì)應(yīng),而且其標(biāo)簽類別也很少。Sequence2Sequence則不同,它不需要輸入與輸出等長(zhǎng)。
Sequence2Sequence算法簡(jiǎn)介
Sequence2Sequence是2014年由Google 和 Yoshua Bengio提出的,這里分別是Google論文和Yoshua Bengio論文的下載地址。從此之后seq2seq算法就開始不斷演化發(fā)展出不同的版本,不過萬變不離其宗,其整體架構(gòu)永遠(yuǎn)是一個(gè)encode-decode模型。下面簡(jiǎn)要介紹四種seq2seq的架構(gòu)。
1.basic encoder-decoder :將encode出來的編碼全部丟給decode每個(gè)step。

2.encoder-decoder with feedback :將encode出來的編碼只喂給decode的初始step,在解碼器端,需將每個(gè)step的輸出,輸入給下一個(gè)step。

3.encoder-decoder with peek:1和2的組合,不僅將encode出來的編碼全部丟給decode每個(gè)step,在解碼器端,也將每個(gè)step的輸出,輸入給下一個(gè)step。

4.encoder-decoder with attention:將3模型的encode端做了一個(gè)小小的改進(jìn),加入了attention機(jī)制,簡(jiǎn)單來說,就是對(duì)encode端每個(gè)step的輸入做了一個(gè)重要性打分。

本次實(shí)驗(yàn)采用的是basic encoder-decoder架構(gòu),下面開始實(shí)戰(zhàn)部分。
對(duì)對(duì)聯(lián)實(shí)戰(zhàn)
數(shù)據(jù)加載
數(shù)據(jù)樣式如下圖所示是一對(duì)對(duì)聯(lián)。模型的輸入時(shí)一句"晚 風(fēng) 搖 樹 樹 還 挺",需要模型生成" 晨 露 潤(rùn) 花 花 更 紅"。這個(gè)數(shù)據(jù)集有個(gè)特點(diǎn),就是輸入輸出是等長(zhǎng)的,序列標(biāo)注算法在這個(gè)數(shù)據(jù)集上也是適用的。

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]
執(zhí)行上方代碼將數(shù)據(jù)變成list,其格式如下:
data_in_list[1:3] :
[['愿', '景', '天', '成', '無', '墨', '跡'], ['丹', '楓', '江', '冷', '人', '初', '去']]
data_out_list[1:3]:
[['萬', '方', '樂', '奏', '有', '于', '闐'], ['綠', '柳', '堤', '新', '燕', '復(fù)', '來']]
構(gòu)造字典
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
通過上方代碼構(gòu)造一個(gè)字典,其格式如下所示,字典的作用就是將字變成計(jì)算機(jī)能處理的id。
{'罇': 1,
'鳣': 2,
'盤': 3,
...
'棄': 168,
'厭': 169,
'楞': 170,
'杋': 171,
...
}
數(shù)據(jù)預(yù)處理
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)
執(zhí)行上方代碼將數(shù)據(jù)padding成等長(zhǎng)(100維),后續(xù)方便喂給模型。其中需要注意的是需要給train_label擴(kuò)充一個(gè)維度,原因是由于keras的sparse_categorical_crossentropy loss需要輸入的3維的數(shù)據(jù)。
模型構(gòu)建
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))
模型構(gòu)建,keras可以很方便的幫助我們構(gòu)建seq2seq模型,這里的encode 和decode采用的都是雙向GRU。其中RepeatVector(output_sequence_length) 這一步,就是執(zhí)行將encode的編碼輸入給decode的每一個(gè)step的操作。從下圖的模型可視化輸出可以看到這個(gè)basic的seq2seq有39萬多個(gè)參數(shù)需要學(xué)習(xí),簡(jiǎn)直可怕。

模型訓(xùn)練
模型構(gòu)建好之后,就可以開始訓(xùn)練起來了。需要做的是將輸入數(shù)據(jù)喂給模型,同時(shí)定義好batch_size和epoch。
model.fit(train_data,train_label_input, batch_size =32, epochs =1, validation_split = 0.2)
下圖是模型訓(xùn)練的過程,一個(gè)epoch大概需要近1小時(shí),loss緩慢降低中。

模型預(yù)測(cè)
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])
訓(xùn)練10個(gè)epoch后,是時(shí)候考考模型的對(duì)對(duì)聯(lián)實(shí)力了,運(yùn)行上方代碼,就可以看到模型的預(yù)測(cè)效果。
“國破山河在”對(duì)出“人來日月長(zhǎng)”,確實(shí)很工整。看來模型學(xué)習(xí)得不錯(cuò),晉升為江湖第一對(duì)穿腸啦。

結(jié)語
Seq2Seq在解決句子生成任務(wù)確實(shí)實(shí)力雄厚,僅僅只用了最basic的ecode和decode就能對(duì)出如此工整的句子(當(dāng)然不是所有的句子都能對(duì)得這么好)。如果使用更強(qiáng)的模型訓(xùn)練對(duì)對(duì)聯(lián)模型,實(shí)力應(yīng)該可以考個(gè)古代狀元。所以,大家有沒有開始對(duì)深度學(xué)習(xí)處理NLP問題產(chǎn)生好奇,學(xué)習(xí)起來吧。
參考:
https://kexue.fm/archives/6270
https://blog.csdn.net/sinat_26917383/article/details/75050225