[实现] 利用 Seq2Seq 预测句子后续字词 ,Pytorch2

最近有个任务:利用 RNN 进行句子补全,即给定一个不完整的句子,预测其后续的字词。

本文使用了 Seq2Seq 模型,输入为 5 个中文字词,输出为 1 个中文字词。

目录

关于RNN

语料预处理

搭建数据集

搭建模型

训练模型

测试模型

保存/加载模型

1.关于RNN

自被提出以来,循环神经网络(Recurrent Neural Networks,RNN) 在 NLP 领域取得了巨大的成功与广泛的应用,也由此催生出了许多新的变体与网络结构。由于网上有众多资料,在此我也只做简单的讲解了。

首先,讲讲 RNN cell 及其变体:

(1) vallina RNN cell

不同于常见的神经网络结构,RNN 的输入为时序输入,每一时刻的输入对神经元的隐状态产生影响,从而影响后续所有时刻的输出。

其中,隐藏层的公式如下所示:

Ot=g(V∗St),St=f(U∗Xt+W∗St−1) O_{t} = g(V* S_t), S_t = f(U*X_t+W*S_{t-1})

O

t

=g(V∗S

t

),S

t

=f(U∗X

t

+W∗S

t−1

)

(2) LSTM cell

LSTM(Long short-term memory,长短期记忆)极大程度的解决了长序列训练过程中的梯度消失和梯度爆炸问题。

(3) GRU cell

GRU(Gate Recurrent Unit)与 LSTM 一样,也极大程度的解决了长序列训练过程中的梯度消失和梯度爆炸问题。但是,与 LSTM 相比,GRU 所需要的计算资源更小,往往工程实现时更倾向于使用 GRU。

接着,讲讲网络结构:

(1) 常见结构:

(2) Bi-directional RNN

(3) Deep Bi-directional RNN

(4) Seq2Seq

(5) Attention

参考资料:

循环神经网络(RNN, Recurrent Neural Networks)介绍

人人都能看懂的LSTM

人人都能看懂的GRU

从Encoder到Decoder实现Seq2Seq模型

Attention学习笔记

2.语料预处理

由于这次使用的语料为中文语料,自然需要对其进行分词,并构造词典。

首先,收集所用的句子,利用 jieba 库,对每个句子进行分词,并将所得结果加入 word_set 中。

接着,对 word_set 中的所有字词构建统计词典。

代码:

import os

import json

import jieba

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

import torch

import torch.nn as nn

import torch.nn.functional as F

import torch.optim as optim

from torch.autograd import Variable

import torchvision

import torchvision.datasets as datasets

import torchvision.transforms as transforms

from torch.utils.data import DataLoader,Dataset

# Set Hyper Parameters

LR = 0.005

EPOCH = 100

BATCH_SIZE = 1

Sentence_Num = 100

Embedding_Dim = None

# Bulid Vocab

sentence_set = [] # 收集所用到的文本句子

for index in range(Sentence_Num):

with open('../../Corpus/CAIL2018/'+str(index)+'.txt','r',encoding='UTF-8') as f:

sentence_set.append(f.read().replace('\n', '').replace('\r', '').replace(',', ' ').replace('。', ' ').replace(':', ' ').replace(' ', ' ').lower())

word_set = set() # 利用jieba库进行中文分词

for sentence in sentence_set:

words = jieba.lcut(sentence)

word_set.update(words)

word_to_ix = {'SOS':0, 'EOS':1, 'UNK':2} # 'SOS': start of sentencex

ix_to_word = {0:'SOS', 1:'EOS', 2:'UNK'} # 'EOS': end of sentence

# 'UNK': unknown token

for word in word_set: # 构建词典,注意:word_to_ix用于对字词进行编号,ix_to_word用于将模型的输出转化为字词

if word not in word_to_ix:

word_to_ix[word] = len(word_to_ix)

ix_to_word[len(ix_to_word)] = word

Embedding_Dim = len(word_to_ix)

with open('./Vocab.txt','w',encoding='UTF-8') as f: # 保存词典

for vocab in word_to_ix.items():

f.write(vocab[0]+' '+str(vocab[1])+'\n')

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

参考资料:

[Pytorch] - No.2 Pytorch 实现RNN语言模型

GitHub - fxsjy/jieba: 结巴中文分词

3.搭建数据集

由于所使用的中文文本并无数据集格式,故我们需要自己制作数据集。

注意,代码中的 bulid_one_hot 并非生成 one-hot 向量。这是因为模型中使用了 nn.Embedding() ,它会初始一个矩阵,相当于我们模型再训练过程中,顺便训练了一个 word embedding matrix。

至于如何使用该函数进行 word embedding ,大家可以查阅本小节的参考资料。

代码:

# Bulid Dataset

def bulid_one_hot(word,word_dict):

if word in word_dict:

return torch.LongTensor([word_dict[word]])

return torch.LongTensor([word_dict['UNK']])

class MyDataset(Dataset):

def __init__(self, words, labels, transform=None, target_transform=None):

self.words = words

self.labels = labels

self.transform = transform

self.target_transform = target_transform

def __getitem__(self, index):

words, labels = self.words[index], self.labels[index]

if self.transform is not None:

words = [self.transform(word) for word in words]

if self.target_transform is not None:

labels = self.target_transform(labels)

return words, labels

def __len__(self):

return len(self.labels)

train_words, train_labels, test_words, test_labels = [], [], [], []

for i in range(int(0.9*Sentence_Num)):

sentence = sentence_set[i]

words = jieba.lcut(sentence)

words.insert(0,'SOS')

words.append('EOS')

words = [bulid_one_hot(word,word_to_ix) for word in words]

for j in range(0,len(words),6):

if j+6 >= len(words):

break

train_words.append(words[j:j+5])

train_labels.append(words[j+5])

for i in range(int(0.9*Sentence_Num),Sentence_Num):

sentence = sentence_set[i]

words = jieba.lcut(sentence)

words.insert(0,'SOS')

words.append('EOS')

words = [bulid_one_hot(word,word_to_ix) for word in words]

for j in range(0,len(words),6):

if j+6 >= len(words):

break

test_words.append(words[j:j+5])

test_labels.append(words[j+5])

trans, target_trans = None, None # transforms.ToTensor(), transforms.ToTensor()

train_set = MyDataset(train_words, train_labels, trans, target_trans)

train_loader = DataLoader(dataset=train_set, batch_size=BATCH_SIZE)

test_set = MyDataset(test_words, test_labels, trans, target_trans)

test_loader = DataLoader(dataset=test_set, batch_size=BATCH_SIZE)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

参考资料:

torch.nn.Embedding

10分钟快速入门PyTorch (7)

Pytorch数据读取(Dataset, DataLoader, DataLoaderIter)

pytorch学习笔记(六):自定义Datasets

Pytorch中正确设计并加载数据集方法

4.搭建模型

采用 GRU 结构构建 Seq2Seq 模型,其中,loss function 为 nn.CrossEntropyLoss(), optimizer 为 optim.SGD()。

注意,pytorch 中采用 nn.CrossEntropyLoss(),对输入与输出有格式要求,请查阅本小节的参考资料。

代码:

# Bulid Seq2Seq Model

class Encoder(nn.Module):

def __init__(self, input_size, hidden_size):

super(Encoder, self).__init__()

self.hidden_size = hidden_size

self.embedding = nn.Embedding(input_size, hidden_size) # 将one-hot向量embedding为词向量

self.gru = nn.GRU(hidden_size, hidden_size) # GRU的hidden layer的size与词向量的size一样,并非必须

def forward(self, input, hidden):

embedded = self.embedding(input).view(1, 1, -1) # RNN的输入格式为 (seq_len, batch, input_size)

output = embedded

output, hidden = self.gru(output, hidden)

return output, hidden

def initHidden(self):

return torch.zeros(1, 1, self.hidden_size) # 初始化Encoder的隐状态

class Decoder(nn.Module):

def __init__(self, input_size, hidden_size, output_size):

super(Decoder, self).__init__()

self.hidden_size = hidden_size

self.embedding = nn.Embedding(input_size, hidden_size)

self.gru = nn.GRU(hidden_size, hidden_size)

self.out = nn.Linear(hidden_size, output_size)

def forward(self, input, hidden):

output = self.embedding(input).view(1, 1, -1)

output = F.relu(output)

output, hidden = self.gru(output, hidden)

output = self.out(output[0])

return output, hidden

def initHidden(self):

return torch.zeros(1, 1, self.hidden_size)

class Seq2Seq(nn.Module):

def __init__(self, encoder, decoder):

super(Seq2Seq, self).__init__()

self.encoder = encoder

self.decoder = decoder

def forward(self, inputs):

encoder_hidden = self.encoder.initHidden()

if torch.cuda.is_available():

encoder_hidden = encoder_hidden.cuda()

# encode

for word in inputs:

encoder_out, encoder_hidden = self.encoder(word, encoder_hidden)

# decode

decoder_hidden = encoder_hidden

pred, decoder_hidden = self.decoder(inputs[-1], decoder_hidden)

return pred

encoder = Encoder(Embedding_Dim,1000)

decoder = Decoder(Embedding_Dim,1000,Embedding_Dim)

if torch.cuda.is_available():

encoder = encoder.cuda()

decoder = decoder.cuda()

seq2seq = Seq2Seq(encoder,decoder)

if torch.cuda.is_available():

seq2seq = seq2seq.cuda()

# Bulid loss function and optimizer

loss_func = nn.CrossEntropyLoss()

#encoder_optimizer = optim.SGD(encoder.parameters(), lr=LR, momentum=0.9)

#decoder_optimizer = optim.SGD(decoder.parameters(), lr=LR, momentum=0.9)

seq2seq_optimizer = optim.SGD(seq2seq.parameters(), lr=LR, momentum=0.9)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

参考资料:

深度解析Pytorch Seq2Seq Toturials(2)

spro/practical-pytorch

搞懂Transformer结构,看这篇PyTorch实现就够了!

PyTorch(总)——PyTorch遇到令人迷人的BUG与记录

RuntimeError: multi-target not supported (newbie)

pytorch error: multi-target not supported in CrossEntropyLoss()

5.训练模型

代码:

# Train Seq2Seq Model

for epoch in range(EPOCH):

loss_sum = 0

for step, (inputs, labels) in enumerate(train_loader):

# encoder_hidden = encoder.initHidden()

label = torch.LongTensor((1,))

label[0] = int(labels.data.numpy()[0])

if torch.cuda.is_available():

inputs = [word.cuda() for word in inputs]

label = label.cuda()

# encoder_hidden = encoder_hidden.cuda()

# forward

pred = seq2seq(inputs)

loss = loss_func(pred,label)

# backward

seq2seq_optimizer.zero_grad()

loss.backward()

seq2seq_optimizer.step()

'''

for word in inputs:

encoder_out, encoder_hidden = encoder(word, encoder_hidden)

decoder_hidden = encoder_hidden

decoder_out, decoder_hidden = decoder(inputs[-1], decoder_hidden)

loss = loss_func(decoder_out,label)

#backward

encoder_optimizer.zero_grad()

decoder_optimizer.zero_grad()

loss.backward()

encoder_optimizer.step()

decoder_optimizer.step()

'''

loss_sum+= loss.data[0]

print('Epoch: %2d train loss: %.4f' % (epoch, loss_sum))

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

结果:

6.测试模型

代码:

# Test Seq2Seq Model

for step, (inputs, labels) in enumerate(test_loader):

# encoder_hidden = encoder.initHidden()

label = torch.LongTensor((1,))

label[0] = int(labels.data.numpy()[0])

if torch.cuda.is_available():

inputs = [word.cuda() for word in inputs]

label = label.cuda()

# encoder_hidden = encoder_hidden.cuda()

decoder_output = seq2seq(inputs)

'''

# forward

for word in inputs:

encoder_out, encoder_hidden = encoder(word, encoder_hidden)

decoder_hidden = encoder_hidden

decoder_out, decoder_hidden = decoder(inputs[-1], decoder_hidden)

'''

# output

ans = ''

pred = ''

for word in inputs:

ix = word.cpu().data.numpy()[0][0]

ans+=ix_to_word[ix]

pred+=ix_to_word[ix]

ans+=ix_to_word[int(labels.data.numpy()[0])]

pred+=ix_to_word[np.argmax(decoder_output.cpu().data.numpy())]

print('Answer: %s' % ans)

print('Prediction: %s' % pred)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

结果:

上述结果是全部结果中效果不错的,可以观察到:虽然模型无法完全预测后续字词,但是能依照句子的前部分继续生成意思完整的句子。

不过,整体来看模型效果较差,我认为有以下几个原因:

所用文本数量少,仅用了100个句子进行训练。

构造的词库小,词典中仅有3000+字词,其中包括许多无意义的字词。

未对超参数进行微调。

7.保存/加载模型

往往大家保存和加载模型都是用的最简单的方法:torch.save(model,path),torch.load(path)。

这样的方法不仅将模型的参数保存了下来,还将模型的结构保存了下来。

有时我们只需要保存模型的参数,我们可以采用这样的方法:torch.save(model.state_dict(),path),torch.load_state_dict(torch.load(path))。

当然,还有许多复杂的方法可以选择,大家可以查阅参考资料进一步了解。

代码:

# Save Seq2Seq Model

'''

torch.save(encoder.state_dict(),'../../Model/Seq2Seq/encoder_params.pkl')

torch.save(decoder.state_dict(),'../../Model/Seq2Seq/decoder_params.pkl')

torch.save(encoder,'../../Model/Seq2Seq/encoder.pkl')

torch.save(decoder,'../../Model/Seq2Seq/decoder.pkl')

'''

torch.save(seq2seq.state_dict(),'../../Model/Seq2Seq/seq2seq_params.pkl')

torch.save(seq2seq,'../../Model/Seq2Seq/seq2seq.pkl')

# Load Seq2Seq Model

# encoder.load_state_dict(torch.load('../../Model/Seq2Seq/encoder_params.pkl'))

# decoder.load_state_dict(torch.load('../../Model/Seq2Seq/decoder_params.pkl'))

seq2seq = torch.load('../../Model/Seq2Seq/seq2seq.pkl')

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

参考资料:

Pytorch 保存模型与加载模型

如果你看到了这篇文章的最后,并且觉得有帮助的话,麻烦你花几秒钟时间点个赞,或者受累在评论中指出我的错误。谢谢!

作者信息:

知乎:没头脑

LeetCode:Tao Pu

CSDN:Code_Mart

Github:Bojack-want-drink

---------------------

作者:Code_Mart

来源:CSDN

原文:https://blog.csdn.net/Code_Mart/article/details/86978470

版权声明:本文为博主原创文章,转载请附上博文链接!