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
个输出。
@GRU输入输出的形状|center
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)
写的时候需要注意的几个点
torch.tensor(dtype=torch.long)
没有torch.tensor(dtype=torch.byte)
需要用torch.ByteTensor(A)
来进行转换。
encoder里面如果输出是使用torch.nn.utils.rnn.pack_padded_sequence()
那么输出也是pack的形式,需要使用他的逆变换torch.nn.utils.rnn.pad_packed_sequence()
进行转换
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:
attention里面的维度变换
-
decoderOutput
和encoderOutput
都是三维,形状都是[sequenceLen*batch_size*hiddenSize]
,只不过一个sequenceLen
是1,一个是MAXLEN
,但是经过torch.sum
有个默认的维度squeeze
所以经过下面的操作以后维度变成两维了。所以只需要知道GRU出来的都是三维到 ,但是由于encoder
是一次输入所有序列所以输出的sequenceLen
是MAXLEN
,decoder
是一次输入一个序列所以输出的sequenceLen
,但是要注意的是他两都是一次性输入batchSize
个大小。因为一次往GRU
里面输入的序列形状是batchSize*hiddenSize
。 -
concat
的attention
,注意torch.cat
指定的dim=2
也就是直接在最后一维hs,ht
连接了起来,所以设置的nn.Linear(2*hiddenSize,hiddenSize)
,意思是将最后一维从2*hiddenSize
大小变成了hiddenSize
大小。还有不要忘了.tanh()
concat形式的attention|center
if name =="dot":
energy=torch.sum(decoderOutput*encoderOutput,dim=2)
...
energy = self.attn(torch.cat((hidden.expand(encoder_output.size(0), -1, -1), encoder_output),2)).tanh()
decoder里面的各种组件
需要从外部传入一个Embedding
,需要自己做一个nn.Dropout
对embedding
进行抓爆防止过拟合,需要一个和encoder
一样的GRU
,需要一个线性变换nn.Linear
用来将attention
后的context
和output
结合起来,用来映射最后的词表的,既然是映射最后的词表那么还需要nn.Linear(hidden_size, output_size)
,output_size
就是最后词表的大小。
self.embedding = embedding
self.embedding_dropout = nn.Dropout(dropout)
self.gru = nn.GRU(hidden_size, hidden_size, n_layers, dropout=(0 if n_layers == 1 else dropout))
self.concat = nn.Linear(hidden_size * 2, hidden_size)
self.out = nn.Linear(hidden_size, output_size)
self.attn = Attn(attn_model, hidden_size)
批量乘的时候不要忘记翻转encoder_outputs
context = attn_weights.bmm(encoder_outputs.transpose(0, 1))
decoder
的输出是[batchSize*vocSize]
维度的
return wordIndex , hidden #wordIndex [batchSize*vocSize]
decoder
的output
是[[概率],[概率],[概率],[概率]]
,形状是batchSize*vocSize
损失函数
nTotal
表示算了多少项。因为inp
的尺寸是[batchSize*vocSize]
所以把target
变成二维的[batchSize,1]
,这里也得到了一个多标签算交叉熵(最大似然估计)的算法,因为只有标签的那个维度是1,其他都是0,所以交叉熵是-log(p(xi))
,感觉这个to(device)
有点多余,target.view(-1,1)
后target
的形状变为[[123],[221],[56],[1231],[453]]
mask_loss,nTotal=maskNLLLose(decoder_output,target_variable[t],mask[t])
-------------------------------------------
def maskNLLLoss(inp, target, mask):
nTotal = mask.sum()
crossEntropy = -torch.log(torch.gather(inp, 1, target.view(-1, 1)))
loss = crossEntropy.masked_select(mask).mean()
loss = loss.to(device)
return loss, nTotal.item()
记住一点输入到decoder
和encoder
里面的其实是二维的[length * batchSize]
,然后使用了embedding
变成了[length * batchSize * hiddenSize]
tensor.topk(n)
,tensor
可以是一个batch
,他是根据最后一维选出最大的几个元素
torch.topk(input, k, dim=None, largest=True, sorted=True, out=None) -> (Tensor, LongTensor)
Returns the k largest elements of the given input tensor along a given dimension.
If dim is not given, the last dimension of the input is chosen.
If largest is False then the k smallest elements are returned.
>>> x = torch.arange(1., 6.)
>>> x
tensor([ 1., 2., 3., 4., 5.])
>>> torch.topk(x, 3)
(tensor([ 5., 4., 3.]), tensor([ 4, 3, 2]))
在decoder
的时候,这里的maxLen
是这一批次句子的最大长度,每一批次不一样
if use_teacher_forcing:
for t in range(maxLen):
PyTorch中的优化
首先将优化器中的encoder.parameter()
里面的参数的梯度值清零,loss.backward()
算出了和这个tensor
有关的所有tensor
的梯度值,然后使用encoder_optimizer.step()
更新encoder.parameter()
里面的参数值。
encoder_optimizer.zero_grad()
loss=0
mask_loss, nTotal = maskNLLLoss(decoder_output, target_variable[t], mask[t])
loss += mask_loss
loss.backward()
encoder_optimizer.step()
创建了encoder
和decoder
一定不要忘记.to(devive)
否则会有下列错误
Expected object of type torch.LongTensor but found type torch.cuda.LongTensor for argument #3 'inde
decoder
里面的的hidden
可以为None
但是不能为0
def forward(self,sentenceBatch,lenthBatch,hidden=None):
torch.cat((tensorA,tensorB),dim)
,第一个参数是tensor
的tuple
而不是tensor
argument 'tensors' (position 1) must be tuple of Tensors, not Tensor