chatbot---编写时犯的错误

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 * 1tensorpadvarpadding以后的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长度的tensoroutputVar需要知道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里面得到不同的outputhidden_state

pack|center

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_sizeseq_len次,正是因为GRU输入的形状,所以mini_batch里面的数据要进行转置,如果是不定长的也可以输入形状是torch.nn.utils.rnn.pack_padded_sequence() 正是因为GRU可以输入多批数据,而在encoder的时候我们一次性把所有的句子输入,所以只是encoder只用输入一次,而decoder需要循环的输入。
  • nn.embedding的输入可以是任意形状的,只要保证是一个index的矩阵即可。
  • torch.nn.utils.rnn.pad_packed_sequencetorch.nn.utils.rnn.pack_padded_sequence的逆操作,outputs的形状就最后一维和input不一样,最后一维是inputnum_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里面的维度变换

  • decoderOutputencoderOutput都是三维,形状都是[sequenceLen*batch_size*hiddenSize],只不过一个sequenceLen是1,一个是MAXLEN,但是经过torch.sum有个默认的维度squeeze所以经过下面的操作以后维度变成两维了。所以只需要知道GRU出来的都是三维到 ,但是由于encoder是一次输入所有序列所以输出的sequenceLenMAXLENdecoder是一次输入一个序列所以输出的sequenceLen,但是要注意的是他两都是一次性输入batchSize个大小。因为一次往GRU里面输入的序列形状是batchSize*hiddenSize
  • concatattention,注意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.Dropoutembedding进行抓爆防止过拟合,需要一个和encoder一样的GRU,需要一个线性变换nn.Linear用来将attention后的contextoutput结合起来,用来映射最后的词表的,既然是映射最后的词表那么还需要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]

decoderoutput[[概率],[概率],[概率],[概率]],形状是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()

记住一点输入到decoderencoder里面的其实是二维的[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()

创建了encoderdecoder一定不要忘记.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),第一个参数是tensortuple而不是tensor

argument 'tensors' (position 1) must be tuple of Tensors, not Tensor

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容