Chatbot Tutorial https://pytorch.org/tutorials/beginner/chatbot_tutorial.html?highlight=seq2seq Author: Matthew Inkawhich In this tutorial, we explore a fun and interesting use-case of recurrent sequence-to-sequence models. We will train a simple chatbot using movie scripts from the Cornell Movie-Dialogs Corpus.
序列对与词典
1. 这里的语料是一部电影中的人物对话 2. 从这些对话中抽出一对对语句 3. 在这个过程,收集不重复单词的集合 4. 去除低频,高频以及停用词 5. 特殊字符删除,特殊文本转码等 6. 句子单词个数超过10个的部分,截断 最终输出结果有二: 1. 词典 2. 序列对 列表
新加三个标记
# ===============================================================
#----------------------------------
# 上面为对原始数据的处理
# 下面是数据转数字处理:文本转索引,批次,是公共的
# 输入:实际长度
# 输出:mask, max_target_len
# 最后再按问句长度做一个降维处理
#-------------------------------------
# ===============================================================
import os
import itertools
import torch
from torch import nn
from torch import optim
import torch.nn.functional as F
import csv
import random
import codecs
from ai.params import DATA_ROOT
from ai.box.d1 import pkl_load,pkl_save
import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F
import csv
import random
import codecs
import re
import os
# 编码问题
import unicodedata
# 版本兼容
from io import open
from pprint import pprint as pp
USE_CUDA = torch.cuda.is_available()
"""
检测是否有GPU
"""
device = torch.device("cuda" if USE_CUDA else "cpu")
print(device)
"""
构建字典
"""
# 定义几个必备的 token
PAD_token = 0 # Used for padding short sentences
SOS_token = 1 # Start-of-sentence token
EOS_token = 2 # End-of-sentence token
class Voc(object):
"""
每添加一个句子,更新一次字典;
封装有 word2index,index2word以及 词频 word2count
"""
def __init__(self, name):
self.name = name
"""
字典名称
"""
self.trimmed = False
"""
是否舍弃低频词,
False:不舍弃
"""
self.word2index = {}
self.word2count = {} # 词频
self.index2word = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"}
self.num_words = 3 # Count SOS, EOS, PAD
def addSentence(self, sentence):
# 英文分词 -- “ ”
# 不用特殊工具
for word in sentence.split(' '):
self.addWord(word)
def addWord(self, word):
"""
添加新词
"""
if word not in self.word2index:
self.word2index[word] = self.num_words
self.word2count[word] = 1
self.index2word[self.num_words] = word
self.num_words += 1
else:
self.word2count[word] += 1 #单词个数加1
def trim(self, min_count):
"""
Remove words below a certain count threshold
"""
if self.trimmed:
return
self.trimmed = True
keep_words = []
for k, v in self.word2count.items():
if v >= min_count:
keep_words.append(k)
# Reinitialize dictionaries
self.word2index = {}
self.word2count = {}
self.index2word = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"}
self.num_words = 3 # Count default tokens
for word in keep_words:
self.addWord(word)
data_dir = os.path.join(DATA_ROOT, "chatbox1")
data_voc_pairs = os.path.join(data_dir,"tmp_voc_pairs.pkl")
voc,pairs = pkl_load(file_path=data_voc_pairs)
# 这些word2index的index是从3开始的
print(voc.word2index["hello"]) #787
print(voc.index2word[0]) #PAD
print(len(pairs)) #53165
print(pairs[0]) #['there .', 'where ?']
|
句子转索引向量
1. 英文分词,以空格拆分即可
2. 根据voc.word2index将某个词转为索引
3. 在每句话的后面加 一个结束标记
# 定义几个必备的 token
PAD_token = 0 # Used for padding short sentences
SOS_token = 1 # Start-of-sentence token
EOS_token = 2 # End-of-sentence token
def indexesFromSentence(voc, sentence):
"""按句子将文本转为索引向量
单词 -- 索引,并在句子后面加上结束符
[i am a good boy.] --- [34, 13, 53, 634, 12, .... 2]
"""
return [voc.word2index[word] for word in sentence.split(' ')] + [EOS_token]
这要验证的是序列到序列模型,
该序列是一个生成模型,
根据已有的信息,在序列的尾部补啊补,补啊补
什么时候结束?
就是遇到结束标记,所以每句话后面要加一个结束标记
生成模型是保留高频词,去除低频词
分类模型是去除高频词
|
|
开始结束标记 开始与结束标记理论上只需要解码器有就可以, 当然了,编码器有这个也没什么不良影响 针对解码器, 开始标记与结束标记的不同在于, 开始标记的序列索引位置为0,全是0,因为它位于每句话的开头, 而结束标记因每句话的长度不定,其索引位置无法固定 关键是,批次化计算要求shape对齐,会有补0这么一个操作, 把一个批次中所有句子补成一样长 所以,编码器序列 在句子转索引向量时加上 对后面代码的编写方便一些 而开始标记可以在编码时加上,也可以在训练时加 这里indexesFromSentence方法没有加开始标记, 但在训练解码器时,为每句话都加上了开始标记
|
import torch
from torch import nn
import itertools
def zeroPadding(lst, fillvalue=0):
"""批次化处理,补零并完成序列长度与批次维度的转换
[b, max_len] -- [seq_len, b]
params:
-----------------------------
l:索引向量
"""
return list(itertools.zip_longest(*lst, fillvalue=fillvalue))
补0,行转列
相当于使用列向量表示一个句子 |
|
补0对齐,转换维度--[seq_len,batch_size]
def inputVar(l, voc):
"""对问句进行批次化处理
输出:
- padVar ,shape=[seq_len, batch_size]
- lengths,批次维度中每个序列未补0时的长度
params:
-----------------------------
- 一个批次问答对中的问句列表,比如 [[i am a boy], [dog], [good morning]] [b, max_len]
- seq_len为当前这个批次中序列的最大长度
"""
# 语句变索引序列
indexes_batch = [indexesFromSentence(voc, sentence) for sentence in l]
# 对齐,将每句话补零,并将shape转换为[seq_len, batch_size]
padList = zeroPadding(indexes_batch)
padVar = torch.LongTensor(padList)
# 求每句话的长度
lengths = torch.tensor([len(indexes) for indexes in indexes_batch])
return padVar, lengths
lengths是为了从padVar提取单词, - 要舍弃补的数据 - 舍弃句尾标记的EOS - 只要单词数据 转换前是单词列表,[batch_size,seq_len],是单词,是汉字 转换后是索引列表,[seq_len,batch_size],是数字,是整数 |
因为对于对齐数据,进行了补0操作, 因此要标记哪些位置是数据,哪些位置不是
def binaryMatrix(l, value=PAD_token):
"""构建掩码
原来有数据的地方:为 1,将来变为 True
原来没有数据的地方:为0, 将来变为 False
params:
-----------------------------
- l: 索引向量
"""
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
标记数据只针对了输出语句
def outputVar(l, voc):
"""
输出语句处理
"""
# 句子变索引序列
indexes_batch = [indexesFromSentence(voc, sentence) for sentence in l]
# 补零对齐,并转换维度,[seq_len,batch]
padList = zeroPadding(indexes_batch)
# 求掩码 [max_len, batch_size]
mask = binaryMatrix(padList)
mask = torch.BoolTensor(mask)
# [max_len, batch_size]
padVar = torch.LongTensor(padList)
# 本批次的最大长度
max_target_len = max([len(indexes) for indexes in indexes_batch])
return padVar, mask, max_target_len
答句向量化处理 - 输入前[batch_size,seq_len],元素是单词 - 经过词转index,补0对齐,转换维度 - 输出为[seq_len,batch_size] mask - 标记哪些位置是单词,有单词的位置将来才参与损失函数的计算 - 无单词的位置,是补的,是凑形状的,不参与损失计算 max_target_len - 训练过程是按批次进行的 - 对于编码器是按批次计算的,一次计算就结束了 - 对于解码器,是一个单词一个单词训练的,是一个for循环,循环的次数就是max_target_len |
|
|
数据批次处理是为了模型训练 - 所以数据的格式是为模型更好训练服务的 batch2TrainData
def batch2TrainData(voc, pair_batch):
"""把一个批量的问答对,转为一个训练数据
return
-------------------
- input,shape=[seq_len, batch_size]
- lengths,shape=[batch_size],每个批次序列单词长度
- output,shape=[seq_len, batch_size]
- mask,码表,数据为True,PAD为False
- max_target_len,最大序列单词个数,即最大序列长度
"""
# 按问句长度对批次降序排列
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
|
pair_batch.sort(key=lambda x: len(x[0].split(" ")), reverse=True)
- 这句话对批次数据进行了排序
- 在深度学习通常是要打乱数据的顺序
这是因为在编码器模型的设计中用到了
# Pack padded batch of sequences for RNN module
packed = nn.utils.rnn.pack_padded_sequence(embedded, input_lengths)
该函数要求数据有序
------------------------------------------------------------------------------------
|
|
|
|
|
|
|