seq2seq模型实现英文反义词翻译

一、seq2seq简介

seq2seq模型是自然语言处理任务中一个常见的模型,于2014年由Google团队提出,该模型的创新之处在于将encoder-decoder框架应用到了RNN/LSTM中,文章中输入序列通过多层LSTM组成的encoder被映射成一个固定维度的中间语义编码向量C,另一组多层LSTM组成的decoder将中间语义编码向量C映射成目标序列。
下面是论文中seq2seq的输入输出图形化解释:
在这里插入图片描述

说明:seq2seq模型的输入序列是A,B,C,输出序列是W,X,Y,Z,蓝色部分是encoder,红色部分是decoder。

注:seq2seq原文可以在这里下载。

二、训练样本的预处理

现在利用seq2seq模型实现一个简单的英文反义词翻译实验,模型的输入是一个词,输出是其对应的反义词,为了方便起见,我们的训练数据是很简单的,只有 6 组,并且每个英文单词的长度不会超过 5,训练数据的形式如下:

[['man', 'women'], ['black', 'white'], ['king', 'queen'], ['girl', 'boy'], ['up', 'down'], ['high', 'low']]

说明:原始训练样本是一个二维的矩阵,用 seq_data 表示,每一行表示一组训练样本,第一个元素表示原始序列,第二个元素表示目标序列。

由于英文单词的长度都不会超过 5,因此定义时间步 n_step = 5,隐藏层 n_hidden = 128。考虑到英文单词的字母总共有 26 个,于是字符列表 char_arr 中必须加入 26 个英文字符(默认是小写)。训练数据单词的长度往往不一致,因此需要将其 padding 成相同长度的序列,于是还要给字符列表 char_arr 中加入一个 padding 符 ‘P’,另外,还需要将起始符 ‘S’ 和终止符 ‘E’ 加入字符列表 char_arr。

之后对字符列表进行字典化,然后对训练样本目的是把英文的输入样本转换成序列化的表示形式。代码如下:

n_step = 5   # n_step表示输入单词的最大长度
n_hidden = 128

char_arr = [c for c in "SEPabcdefghijklmnopqrstuvwxyz"]
num_dic = {c: i for i, c in enumerate(char_arr)}
int_dic = {i: c for i, c in enumerate(char_arr)}

seq_data = [['man', 'women'], ['black', 'white'], ['king', 'queen'],
['girl', 'boy'], ['up', 'down'], ['high', 'low']]

n_class = len(char_arr)
batch_size = len(seq_data)
# 深拷贝
seq_copy = copy.deepcopy(seq_data)
inp, out, tar = make_batch()

对于第一组训练样本 [‘man’, ‘women’] 而言,如何得到 ‘man’ 的序列化输入呢?

首先,由于’man’ 的长度 3 是小于 n_step 的,因此需要在 ‘man’ 的后面填充 n_step - 3 个长度的 ‘P’,然后对于 ‘manPP’ 可以根据字典 num_dic 得到对应的序列表示 [15, 3, 16, 2, 2],之后获得 'manPP’中每个字母的 one-hot 表示,是一个 5 * 29维的稀疏矩阵。于是,'man’就转换成了网络可以识别的形式。‘woman’ 是 ‘man’ 的标签数据,预处理是类似的,不同之处在于,‘woman’ 需要给最前面加上起始符 ‘S’。全部样本的预处理代码如下:

def make_batch():
    input_batch = []
    output_batch = []
    target_batch = []
    for seq in seq_copy:
        for i in range(2):
            seq[i] = seq[i] + 'P' * (n_step - len(seq[i]))

        inp = [num_dic[s] for s in seq[0]]
        out = [num_dic[s] for s in ('S' + seq[1])]
        tar = [num_dic[s] for s in (seq[1] + 'E')]

        input_batch.append(np.eye(n_class)[inp])      # 训练样本的输入向量表示
        output_batch.append(np.eye(n_class)[out])     # 训练样本的输出向量表示
        target_batch.append(tar)                      # 用于计算损失函数的输出向量表示

    return torch.FloatTensor(input_batch), torch.FloatTensor(output_batch), torch.LongTensor(target_batch)

三、模型训练

模型训练过程如下:

 # 定义损失函数和优化器
model = seq2seq()
model.train()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

for epoch in range(4000):
    # 初始化隐藏层的状态
    hidden = torch.zeros(1, batch_size, n_hidden)
    output = model(inp, hidden)

    output = output.transpose(0, 1)       # output:[batch_size, num_step(n_step + 1), n_class]

    # 计算损失
    optimizer.zero_grad()
    loss = 0
    for i in range(output.shape[0]):
        loss += criterion(output[i], tar[i])
        # output[i]:[n_step+1, n_class], tar[i]:[n_step+1]
    if epoch % 200 == 0:
        print("epoch:{} loss:{:.6f}".format(epoch, loss))
    loss.backward()
    optimizer.step()

四、全部代码

最后给出了预测部分,测试的样本选取的是训练样本,这里仅仅是为了说明问题。

# !-*- coding:utf-8 -*-
# @author:zhangsa
# @file:.py

import numpy as np
import torch.nn as nn
import torch
import copy


def make_batch():
    input_batch = []
    output_batch = []
    target_batch = []
    for seq in seq_copy:
        for i in range(2):
            seq[i] = seq[i] + 'P' * (n_step - len(seq[i]))

        inp = [num_dic[s] for s in seq[0]]
        out = [num_dic[s] for s in ('S' + seq[1])]
        tar = [num_dic[s] for s in (seq[1] + 'E')]

        input_batch.append(np.eye(n_class)[inp])      # 训练样本的输入向量表示
        output_batch.append(np.eye(n_class)[out])     # 训练样本的输出向量表示
        target_batch.append(tar)                      # 用于计算损失函数的输出向量表示

    return torch.FloatTensor(input_batch), torch.FloatTensor(output_batch), torch.LongTensor(target_batch)


class seq2seq(nn.Module):
    def __init__(self):
        super().__init__()
        self.enc_cell = nn.RNN(input_size=n_class, hidden_size=n_hidden)
        self.dec_cell = nn.RNN(input_size=n_class, hidden_size=n_hidden)
        self.fc = nn.Linear(n_hidden, n_class)

    def forward(self, enc_input, hidden):
        batch_size = enc_input.shape[0]
        enc_input = enc_input.transpose(0, 1)    # enc_shape:[num_step(n_step), batch_size, n_class]
        # dec_input = dec_input.transpose(0, 1)   # dec_shape:[num_step(n_step + 1), batch_size, n_class]
        # 编码器的输入时随机初始化
        dec_input = torch.randint(n_class, (n_step + 1, batch_size, n_class)).type(dtype=torch.FloatTensor)

        _, enc_state = self.enc_cell(enc_input, hidden)   # enc_state:中间的语义编码 c
        # enc_state:[1, batch_size, n_hidden]
        dec_out, _ = self.dec_cell(dec_input, enc_state)
        # dec_out:[num_step(n_step + 1), batch_size, n_hidden]

        out = self.fc(dec_out)         # out:[num_step(n_step + 1), batch_size, n_class]
        return out


if __name__ == "__main__":
    n_step = 5   # n_step表示输入单词的最大长度
    n_hidden = 128

    char_arr = [c for c in "SEPabcdefghijklmnopqrstuvwxyz"]
    num_dic = {c: i for i, c in enumerate(char_arr)}
    int_dic = {i: c for i, c in enumerate(char_arr)}

    seq_data = [['man', 'women'], ['black', 'white'], ['king', 'queen'],
                ['girl', 'boy'], ['up', 'down'], ['high', 'low']]

    n_class = len(char_arr)
    batch_size = len(seq_data)
    # 深拷贝
    seq_copy = copy.deepcopy(seq_data)
    inp, out, tar = make_batch()

    # 定义损失函数和优化器
    model = seq2seq()
    model.train()
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

    for epoch in range(4000):
        # 初始化隐藏层的状态
        hidden = torch.zeros(1, batch_size, n_hidden)
        output = model(inp, hidden)

        output = output.transpose(0, 1)       # output:[batch_size, num_step(n_step + 1), n_class]

        # 计算损失
        optimizer.zero_grad()
        loss = 0
        for i in range(output.shape[0]):
            loss += criterion(output[i], tar[i])
            # output[i]:[n_step+1, n_class], tar[i]:[n_step+1]
        if epoch % 200 == 0:
            print("epoch:{} loss:{:.6f}".format(epoch, loss))
        loss.backward()
        optimizer.step()

    # 模型预测
    def translation():
        model.eval()

        input_batch, output_batch, _ = make_batch()
        hidden_state = torch.zeros(1, 6, n_hidden)
        predict = model(input_batch, hidden_state)   # predict:[num_step, batch_size, n_class]
        predict = predict.transpose(0, 1)            # predict:[batch_size, num_step, n_class]

        predict_words = []
        for i in range(predict.shape[0]):
            _, pre_seq = torch.max(predict[i], dim=1)
            pre_word = [int_dic[p] for p in pre_seq.detach().numpy()]
            pre = ""
            for p in pre_word:
                if p == 'P' or p == 'E':
                    break
                else:
                    pre += p
            predict_words.append(pre)

        return predict_words

    print("test:")
    for i in range(len(seq_data)):
        print(seq_data[i][0], "-->", translation()[i])

五、参考文献

https://github.com/graykode/nlp-tutorial/blob/master/4-1.Seq2Seq/Seq2Seq.py

seq2seq原文

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值