Chatbot 编程技巧和难点
数据预处理
将原始数据处理成我们需要的对话形式
直接下载下来的文件打开的时候要在open
函数里面设置encoding=iso8859-1
其实我们可以单独写一个脚本将电影评论文件变成我们需要的句子对的形式然后进行数据的预处理。
- 在项目中使用wget 进行ftp下载文件时,由于ftp下载默认的是ascii模式,下载的文件编码是iso8859-1。在python3中直接使用open函数的话,需要设置编码,不然会报错。
open("08M0063639_20170710.txt","r",encoding='iso8859-1')
- 如果是中文数据,想要转换成
utf8
的话运行下面代码。
uft_str = str.encode("iso-8859-1").decode('gbk').encode('utf8')
代码中的片段
with open(fileName, 'r', encoding='iso-8859-1') as f:
for line in f:
values = line.split(" +++$+++ ")
数据预处理中的链式编程
voc, pairs = loadPrepareData(corpus_name, datafile)
voc, pairs = readVocs(datafile, corpus_name)
pairs = filterPairs(pairs)
for pair in pairs:
voc.addSentence(pair[0])
voc.addSentence(pair[1])
"""
传入电影文件夹的名字,对话文件的名字,传出实例化的字典voc以及对话对pairs,这里面的对话对都是都是修剪和正规化以后的pairs的形状大概是[[[],[]][[],[]]]是一个n * 2 *m的列表,n是总共有n个对话,2是每个对话分为一问一答,m是对话的长度,m根据具体的对话,是不定长的。
loadPrepareData在内部调用readVocs,在将pairs使用函数filterPairs进行修剪,将pair里面的词加入到字典voc里面
读取原始的pair,
"""
进行句子正规化的时候要先转换成ASCII编码
def unicodeToAscii(s):
return ''.join(
c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn'
)
# Lowercase, trim, and remove non-letter characters
def normalizeString(s):
s = unicodeToAscii(s.lower().strip())
s = re.sub(r"([.!?])", r" \1", s)
s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
s = re.sub(r"\s+", r" ", s).strip()
return s
对单词表和句子进行修剪
- 在单词表中修建掉一些出现频率很低的单词,可以提高对话的质量。
- 修剪掉长度大于
threshold
的句子也可以提高对话质量。
def trimRareWords(voc, pairs, MIN_COUNT):
voc.trim(MIN_COUNT)
for pair in pairs:
...
for word in input_sentence.split(' '):
if word not in voc.word2index:
keep_input = False
break
...
if keep_input and keep_output:
keep_pairs.append(pair)
...
pairs = trimRareWords(voc, pairs, MIN_COUNT)
对矩阵做Padding+隐式的翻转
l
是一个二维list
,这个函数有两个作用,一个是进行填充,一个是进行翻转transpose
[batchSize * maxLen]->[maxLen * batchSize]
def zeroPadding(l, fillvalue=PAD_token):
return list(itertools.zip_longest(*l, fillvalue=fillvalue))
"""
zip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-
"""
制作掩码矩阵
def binaryMatrix(l, value=PAD_token):
m = []
for i, seq in enumerate(l):
m.append([])
for token in seq:
if token == PAD_token:
m[i].append(0)
else:
m[i].append(1)
return m
为模型准备数据中的链式编程
indexesFromSentence
又是一个将句子转换成index
的函数。lengths
又是一个n * 1
的tensor
,padvar
是padding
以后的list
转换成了tensor
,但是有一个写的不好的地方就是应该在转换的时候直接使用device=device
字段直接放在显存上。
def inputVar(l, voc):
indexes_batch = [indexesFromSentence(voc, sentence) for sentence in l]
lengths = torch.tensor([len(indexes) for indexes in indexes_batch])
padList = zeroPadding(indexes_batch)
padVar = torch.LongTensor(padList)
return padVar, lengths)
对数据集随机采样一个mini_batch
的大小,使用batch2TrainData
转换成我们训练时候需要的数据。
首先对一个batch
里面的而数据按照大小进行排序是因为训练中一个函数要求最长的数据在第一个的位置。而且之所以outputVar
需要一个掩码矩阵就是为了算损失的时候只算掩码的部分,inputVar
需要得到的信息是,训练数据的tensor
,以及每个sentence
长度的tensor
,outputVar
需要知道tensor
,掩码矩阵,以及句子的最大长度。
def batch2TrainData(voc, pair_batch):
pair_batch.sort(key=lambda x: len(x[0].split(" ")), reverse=True)
input_batch, output_batch = [], []
for pair in pair_batch:
input_batch.append(pair[0])
output_batch.append(pair[1])
inp, lengths = inputVar(input_batch, voc)
output, mask, max_target_len = outputVar(output_batch, voc)
return inp, lengths, output, mask, max_target_len
# Example for validation
small_batch_size = 5
batches = batch2TrainData(voc, [random.choice(pairs) for _ in range(small_batch_size)])
input_variable, lengths, target_variable, mask, max_target_len = batches
必须要牢记的是,encoder是一次输入整个batch的句子信息,而decoder是循环式的输入
torch.nn.utils.rnn.pack_padded_sequence()的用法
torch.nn.utils.rnn.pack_padded_sequence() 的作用是将一个padding
之后的matrix
进行摊平(pack
,具体是去掉padding
之后将一二维进行合并(shape: nmk...->(nm)k...)**),然后将不同颜色的data输入到不同的lstm
里面得到不同的output
和hidden_state
,
Shape:( embedded = self.embedding(input_seq))的形状
Input: LongTensor of arbitrary shape containing the indices to extract(输入可以是任意形状)
Output: (*, embedding_dim), where * is the input shape(输出比输入多一维长度为embedding_dim的维度)
packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths)
# Forward pass through GRU
outputs, hidden = self.gru(packed, hidden)
# Unpack padding
outputs, _ = torch.nn.utils.rnn.pad_packed_sequence(outputs)
# Sum bidirectional GRU outputs
outputs = outputs[:, :, :self.hidden_size] + outputs[:, : ,self.hidden_size:]
GRU输入和输出的形状
- 对于定长的数据,输入形状是
[seq_len,batch,input_size]
说明GRU
的输入时三维的,,比如encoder
输入的形状是10*5*embedding
,所以每次送进去的长度是batch*input_size
送seq_len
次,正是因为GRU
输入的形状,所以mini_batch
里面的数据要进行转置,如果是不定长的也可以输入形状是torch.nn.utils.rnn.pack_padded_sequence()
正是因为GRU可以输入多批数据,而在encoder的时候我们一次性把所有的句子输入,所以只是encoder只用输入一次,而decoder需要循环的输入。 -
nn.embedding
的输入可以是任意形状的,只要保证是一个index
的矩阵即可。 -
torch.nn.utils.rnn.pad_packed_sequence
是torch.nn.utils.rnn.pack_padded_sequence
的逆操作,outputs
的形状就最后一维和input
不一样,最后一维是input
的num_directions
倍,不考虑num_layers
是因为下一层的GRU
输出是上一层GRU
输入,所以最终还是num_directions
个输出。
attention中的维度变换
attn
输入的维度是(1,batch_size,input_size)
和(seq_len,batch,input_size)
,torch.sum(hidden * encoder_output, dim=2)
,(1,batch_size,input_size) * (seq_len,batch,input_size)
的维度是(seq_len,batch,input_size)
是最后一维的一一对应元素,所以最后一维长度还是input_size
,说明广播机制,tensor之间使用乘法后的output维度是和维度较大的那个一致,使用了torch.sum(,dim=2)
会把最后一维squeeze
,除非指定keepdim=True(default False)
,这种会把某一维长度变为1的函数一般都会自动squeeze
并且还要keepdim=True
字段用于保持原有形状。先使用hidden.expand(encoder_output.size(0), -1, -1)
将(1,batch_size,input_size)
扩大到(seq_len,batch,input_size)
,torch.cat(seq, dim=0, out=None) → Tensor
默认是在第0个维度上进行连接。torch.nn.Linear(in_features, out_features, bias=True)
输入输出的形状只有最后一个维度是不一样的而其他都是一样的而。(input
是三维甚至更高纬度,但是变换矩阵永远是二维的,那么变换的时候依次取出二维与变换矩阵进行相乘,所以得到的是除过最后一个维度output
的其他维度都是与input
一样的),attn_energies = attn_energies.t()
,因为每种attention
最后一步都是torch.sum(self.v * energy, dim=2)
所以会变成一个二维矩阵,使用.t()
进行转置以后保证,最后一个[]
是对同一个句子而言的长度是10,否则最后一个[]
是对batch
而言的长度是5.
Input: (N,∗,in_features) where ∗ means any number of additional dimensions
Output: (N,∗,out_features) where all but the last dimension are the same shape as the input.
# decoder中的代码片段
# input_step: one time step (one word) of input sequence batch; shape=(1, batch_size,input_size)
rnn_output, hidden = self.gru(embedded, last_hidden)
# Calculate attention weights from the current GRU output
attn_weights = self.attn(rnn_output, encoder_outputs)
def dot_score(self, hidden, encoder_output):
return torch.sum(hidden * encoder_output, dim=2)
def concat_score(self, hidden, encoder_output):
energy = self.attn(torch.cat((hidden.expand(encoder_output.size(0), -1, -1), encoder_output), 2)).tanh()
...
elif self.method == 'dot':
attn_energies = self.dot_score(hidden, encoder_outputs)
# Transpose max_length and batch_size dimensions
attn_energies = attn_energies.t()
...
------------------------------------------------------------
a=torch.Tensor([[[1,2],[2,3]]])
b=torch.Tensor([[[4,5],[6,7]],[[8,6],[4,2]]])
print(torch.sum(a*b,dim=2))
------------------------------------------------------------
tensor([[14., 33.],
[20., 14.]])
Decoder中的维度变换
torch.bmm(A,B)
,所以要转换成batch_size
在前,因为atten
最后一句是return F.softmax(attn_energies, dim=1).unsqueeze(1)
所以attn_weights
还是三维向量。理解unsqueeze(1)
的含义是:变换以后在1这个维度变成了1,即最后的shape是(n1m),squeeze(1)
与之含义正好相反,是去处1这个维度的1** ,在PyTorch中许多网络都是batch_size在第二维度,因为默认batch_first=false
If batch1 is a (b×n×m) tensor, batch2 is a (b×m×p) tensor, out will be a (b×n×p) tensor.
context = attn_weights.bmm(encoder_outputs.transpose(0, 1))
context = context.squeeze(1)