怎么购买网站空间和域名东莞手机网站制作
- 作者: 五速梦信息网
- 时间: 2026年04月20日 06:50
当前位置: 首页 > news >正文
怎么购买网站空间和域名,东莞手机网站制作,广州十二区分布图,邯郸网站制作哪家强生成式聊天机器人 – 基于Pytorch Global Attention 双向 GRU 实现的SeqToSeq模型 – 上 前言数据预处理下载并加载数据原始数据格式化数据清洗与字典映射转换为模型需要的数据格式 SeqToSeq 模型Encoder 编码器Decoder 解码器全局注意力机制解码器实现 前言 本文会介绍使用… 生成式聊天机器人 – 基于Pytorch Global Attention 双向 GRU 实现的SeqToSeq模型 – 上 前言数据预处理下载并加载数据原始数据格式化数据清洗与字典映射转换为模型需要的数据格式 SeqToSeq 模型Encoder 编码器Decoder 解码器全局注意力机制解码器实现 前言 本文会介绍使用seq2seq模型实现一个chatbot训练数据来自Cornell电影对话语料库偏向于闲聊的语料库。 下面是 chatbot 实现的对话效果示例 hello? Bot: hello .where am I? Bot: you re in a hospital .who are you? Bot: i m a lawyer .how are you doing? Bot: i m fine .are you my friend? Bot: no .youre under arrest Bot: i m trying to help you !im just kidding Bot: i m sorry .where are you from? Bot: san francisco .its time for me to leave Bot: i know .goodbye Bot: goodbye .数据预处理 下载并加载数据 首先我们要下载训练语料库: 训练语料库下载链接 这是一个zip文件把它下载后解压到项目目录的子目录data下。接下来我们需要对原始数据进行变换然后用合适的数据结构加载到内存里。 Cornell电影对话语料库是电影人物的对话数据它包括 10,292对电影人物(一部电影有多个人物他们两两之间可能存在对话)的220,579个对话。617部电影的9,035个人物。总共304,713个utterance(utterance是对话中的语音片段不一定是完整的句子)。 这个数据集是比较大并且多样的(diverse)语言形式、时代和情感都有很多样。这样的数据可以使得我们的chatbot对于不同的输入更加鲁棒(robust)。 解压后的目录有很多文件我们会用到的文件包括movie_lines.txt: 首先我们来看一下原始数据长什么样下面的代码输出这个文件的前10行 import os corpus_name cornell movie-dialogs corpus corpus os.path.join(data, corpus_name)def printLines(file, n10):with open(file, rb) as datafile:lines datafile.readlines()for line in lines[:n]:print(line)printLines(os.path.join(corpus, movie_lines.txt))结果如下: bL1045 \( u0 \) m0 \( BIANCA \) They do not!\n bL1044 \( u2 \) m0 \( CAMERON \) They do to!\n bL985 \( u0 \) m0 \( BIANCA \) I hope so.\n bL984 \( u2 \) m0 \( CAMERON \) She okay?\n bL925 \( u0 \) m0 \( BIANCA \) Lets go.\n bL924 \( u2 \) m0 \( CAMERON \) Wow\n bL872 \( u0 \) m0 \( BIANCA \) Okay – youre gonna need to learn how to lie.\n bL871 \( u2 \) m0 \( CAMERON \) No\n bL870 \( u0 \) m0 \( BIANCA \) I\m kidding. You know how sometimes you just become this persona? And you don\t know how to quit?\n bL869 \( u0 \) m0 \( BIANCA \) Like my fear of wearing pastels?\n注意上面的move_lines.txt每行都是一个utterance但是这个文件看不出哪些utterance是组成一段对话的这需要 movie_conversations.txt 文件 该文件前10行结果如下: u0 \( u2 \) m0 \( [L194, L195, L196, L197] u0 \) u2 \( m0 \) [L198, L199] u0 \( u2 \) m0 \( [L200, L201, L202, L203] u0 \) u2 \( m0 \) [L204, L205, L206] u0 \( u2 \) m0 \( [L207, L208] u0 \) u2 \( m0 \) [L271, L272, L273, L274, L275] u0 \( u2 \) m0 \( [L276, L277] u0 \) u2 \( m0 \) [L280, L281] u0 \( u2 \) m0 \( [L363, L364] u0 \) u2 \( m0 \) [L365, L366]每一行用”$”分割成4列第一列表示第一个人物的ID第二列表示第二个人物的ID第三列表示电影的ID第四列表示这两个人物在这部电影中的一段对话; 比如第一行表示人物u0和u2在电影m0中的一段对话包含ID为L194、L195、L196和L197的4个utterance。 注意两个人物在一部电影中会有多段对话中间可能穿插其他人之间的对话而且即使中间没有其他人说话这两个人物对话的内容从语义上也可能是属于不同的对话(话题)。所以我们看到第二行还是u0和u2在电影m0中的对话它包含L198和L199两个utteranceL198是紧接着L197之后的但是它们属于两个对话(话题)。 原始数据格式化 为了使用方便我们会把原始数据处理成一个新的文件这个新文件的每一行都是用TAB分割问题(query)和答案(response)对。为了实现这个目的我们首先定义一些用于解析原始文件 movie_lines.txt 的辅助函数: loadLines 把movie_lines.txt 文件切分成 (lineID, characterID, movieID, character, text)
把每一行都parse成一个dictkey是lineID、characterID、movieID、character和text
分别代表这一行的ID、人物ID、电影ID人物名称和文本。
最终输出一个dictkey是lineIDvalue是一个dict。
value这个dict的key是lineID、characterID、movieID、character和text
def loadLines(fileName, fields):lines {}with open(fileName, r, encodingiso-8859-1) as f:for line in f:values line.split( $ )# 抽取fieldslineObj {}for i, field in enumerate(fields):lineObj[field] values[i]lines[lineObj[lineID]] lineObjreturn lines下图展示loadlines处理后得到的数据形式:
每个dict字典实例保存一句话的信息也就是某个角色在某部电影说了某句话。
loadConversations 把上面的行分组成一个个多轮的对话
根据movie_conversations.txt文件和上面输出的lines把utterance组成对话。
最终输出一个list这个list的每一个元素都是一个dict
key分别是character1ID、character2ID、movieID和utteranceIDs。
分别表示这对话的第一个人物的ID第二个的ID电影的ID以及它包含的utteranceIDs
最后根据lines还给每一行的dict增加一个key为lines其value是个list
包含所有utterance(上面得到的lines的value)
def loadConversations(fileName, lines, fields):conversations []with open(fileName, r, encodingiso-8859-1) as f:for line in f:values line.split( $ )# 抽取fieldsconvObj {}for i, field in enumerate(fields):convObj[field] values[i]# convObj[utteranceIDs]是一个字符串形如[L198, L199]# 我们用eval把这个字符串变成一个字符串的list。lineIds eval(convObj[utteranceIDs])# 根据lineIds构造一个数组根据lineId去lines里检索出存储utterance对象。convObj[lines] []for lineId in lineIds:convObj[lines].append(lines[lineId])conversations.append(convObj)return conversations下图展示loadConversations处理后得到的数据形式:
extractSentencePairs 从上面的每个对话中抽取句对(一问一答)
从对话中抽取句对
假设一段对话包含s1,s2,s3,s4这4个utterance
那么会返回3个句对s1-s2,s2-s3和s3-s4。
def extractSentencePairs(conversations):qa_pairs []for conversation in conversations:# 遍历对话中的每一个句子忽略最后一个句子因为没有答案。for i in range(len(conversation[lines]) - 1):inputLine conversation[lines][i][text].strip()targetLine conversation[lines][i1][text].strip()# 如果有空的句子就去掉if inputLine and targetLine:qa_pairs.append([inputLine, targetLine])return qa_pairs下图展示extractSentencePairs处理后得到的数据形式: 接下来我们利用上面的3个函数对原始数据进行处理最终得到formatted_movie_lines.txt
定义新的文件
datafile os.path.join(corpus, formatted_movie_lines.txt)delimiter \t
对分隔符delimiter进行decode这里对tab进行decode结果并没有变
delimiter str(codecs.decode(delimiter, unicode_escape))# 初始化dict lineslist conversations以及前面我们介绍过的field的id数组。 lines {} conversations [] MOVIE_LINES_FIELDS [lineID, characterID, movieID, character, text] MOVIE_CONVERSATIONS_FIELDS [character1ID, character2ID, movieID, utteranceIDs]# 首先使用loadLines函数处理movie_lines.txt print(\nProcessing corpus…) lines loadLines(os.path.join(corpus, movie_lines.txt), MOVIE_LINES_FIELDS)
接着使用loadConversations处理上一步的结果得到conversations
print(\nLoading conversations…) conversations loadConversations(os.path.join(corpus, movie_conversations.txt),lines, MOVIE_CONVERSATIONS_FIELDS)
输出到一个新的csv文件
print(\nWriting newly formatted file…) with open(datafile, w, encodingutf-8) as outputfile:writer csv.writer(outputfile, delimiterdelimiter, lineterminator\n)# 使用extractSentencePairs从conversations里抽取句对。for pair in extractSentencePairs(conversations):writer.writerow(pair)# 输出一些行用于检查 print(\nSample lines from file:) printLines(datafile)上面的代码会生成一个新的文件formatted_movie_lines.txt : 这文件每一行包含一对句对用tab分割。下面是前十行 bCan we make this quick? Roxanne Korrine and Andrew Barrett are having an incredibly horrendous public break- up on the quad. Again.\tWell, I thought wed start with pronunciation, if thats okay with you.\r\n bWell, I thought wed start with pronunciation, if thats okay with you.\tNot the hacking and gagging and spitting part. Please.\r\n bNot the hacking and gagging and spitting part. Please.\tOkay… then how bout we try out some French cuisine. Saturday? Night?\r\n bYoure asking me out. Thats so cute. Whats your name again?\tForget it.\r\n bNo, no, its my fault – we didnt have a proper introduction —\tCameron.\r\n bCameron.\tThe thing is, Cameron – Im at the mercy of a particularly hideous breed of loser. My sister. I cant date until she does.\r\n bThe thing is, Cameron – Im at the mercy of a particularly hideous breed of loser. My sister. I cant date until she does.\tSeems like she could get a date easy enough…\r\n bWhy?\tUnsolved mystery. She used to be really popular when she started high school, then it was just like she got sick of it or something.\r\n bUnsolved mystery. She used to be really popular when she started high school, then it was just like she got sick of it or something.\tThats a shame.\r\n bGosh, if only we could find Kat a boyfriend…\tLet me see what I can do.\r\n数据清洗与字典映射 接下来我们需要构建词典然后把问答句对加载到内存里。 我们的输入是一个句对每个句子都是词的序列但是机器学习只能处理数值因此我们需要建立词到数字ID的映射。 为此我们会定义一个Voc类它会保存词到ID的映射同时也保存反向的从ID到词的映射。除此之外它还记录每个词出现的次数以及总共出现的词的个数。这个类提供addWord方法来增加一个词 addSentence方法来增加句子也提供方法trim来去除低频的词。
预定义的token
PAD_token 0 # 表示padding SOS_token 1 # 句子的开始 EOS_token 2 # 句子的结束class Voc:def init(self, name):self.name nameself.trimmed Falseself.word2index {}self.word2count {}self.index2word {PAD_token: PAD, SOS_token: SOS, EOS_token: EOS}self.num_words 3 # 目前有SOS, EOS, PAD这3个token。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_wordsself.word2count[word] 1self.index2word[self.num_words] wordself.num_words 1else:self.word2count[word] 1# 删除频次小于min_count的tokendef trim(self, min_count):if self.trimmed:returnself.trimmed Truekeep_words []for k, v in self.word2count.items():if v min_count:keep_words.append(k)print(keep_words {} / {} {:.4f}.format(len(keep_words), len(self.word2index), len(keep_words) / len(self.word2index)))# 重新构造词典self.word2index {}self.word2count {}self.index2word {PAD_token: PAD, SOS_token: SOS, EOS_token: EOS}self.num_words 3 # Count default tokens# 重新构造后词频就没有意义了(都是1)for word in keep_words:self.addWord(word)有了上面的Voc类我们就可以通过问答句对来构建词典了。但是在构建之前我们需要进行一些预处理。 首先我们需要使用函数unicodeToAscii来把unicode字符变成ascii比如把à变成a。注意这里的代码只是用于处理西方文字如果是中文这个函数直接会丢弃掉:
把Unicode字符串变成ASCII
参考https://stackoverflow.com/a/518232/2809427
def unicodeToAscii(s):return .join(c for c in unicodedata.normalize(NFD, s)if unicodedata.category© ! Mn)接下来把所有字母变成小写同时丢弃掉字母和常见标点(.!?)之外的所有字符
对字符串进行全面的规范化处理包括小写转换、去除空格、处理标点符号和清理无效字符
def normalizeString(s):# 变成小写、去掉前后空格然后unicode变成asciis 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最后为了训练收敛我们会用函数filterPairs去掉长度超过MAX_LENGTH的句子(句对) MAX_LENGTH 10 # 句子最大长度是10个词(包括EOS等特殊词)# 一个句对包含问和答两句话两句话要同时满足长度小于10个词才行 def filterPair(p):return len(p[0].split( )) MAX_LENGTH and len(p[1].split( )) MAX_LENGTH# 过滤太长的句对 def filterPairs(pairs):return [pair for pair in pairs if filterPair(pair)]下面开始对原始数据格式化阶段处理完毕得到的句对数据再次进行清洗:
读取问答句对并且返回Voc词典对象
def readVocs(datafile, corpus_name):print(Reading lines…)# 文件每行读取到list lines中。lines open(datafile, encodingutf-8). \read().strip().split(\n)# 每行用tab切分成问答两个句子然后调用normalizeString函数进行处理。pairs [[normalizeString(s) for s in l.split(\t)] for l in lines]voc Voc(corpus_name)return voc, pairs# 使用上面的函数进行处理返回Voc对象和句对的list def loadPrepareData(corpus, corpus_name, datafile):print(Start preparing training data …)voc, pairs readVocs(datafile, corpus_name)print(Read {!s} sentence pairs.format(len(pairs)))pairs filterPairs(pairs)print(Trimmed to {!s} sentence pairs.format(len(pairs)))print(Counting words…)for pair in pairs:voc.addSentence(pair[0])voc.addSentence(pair[1])print(Counted words:, voc.num_words)return voc, pairs# Load/Assemble voc and pairs save_dir os.path.join(data, save) voc, pairs loadPrepareData(corpus, corpus_name, datafile)
输出一些句对
print(\npairs:) for pair in pairs[:10]:print(pair)输出: Start preparing training data … Reading lines… Read 221282 sentence pairs Trimmed to 64271 sentence pairs Counting words… Counted words: 18008 pairs: [there ., where ?] [you have my word . as a gentleman, you re sweet .] [hi ., looks like things worked out tonight huh ?] [you know chastity ?, i believe we share an art instructor] [have fun tonight ?, tons] [well no …, then that s all you had to say .] [then that s all you had to say ., but] [but, you always been this selfish ?] [do you listen to this crap ?, what crap ?] [what good stuff ?, the real you .]我们可以看到原来共有221282个句对经过处理后我们只保留了 64271个句对。 另外为了收敛更快我们可以去除掉一些低频词。这可以分为两步 使用voc.trim函数去掉频次低于MIN_COUNT 的词。去掉包含低频词的句子(只保留这样的句子——每一个词都是高频的也就是在voc中出现的)。 MIN_COUNT 3 # 阈值为3def trimRareWords(voc, pairs, MIN_COUNT):# 去掉voc中频次小于3的词 voc.trim(MIN_COUNT)# 保留的句对 keep_pairs []for pair in pairs:input_sentence pair[0]output_sentence pair[1]keep_input Truekeep_output True# 检查问题for word in input_sentence.split( ):if word not in voc.word2index:keep_input Falsebreak# 检查答案for word in output_sentence.split( ):if word not in voc.word2index:keep_output Falsebreak# 如果问题和答案都只包含高频词我们才保留这个句对if keep_input and keep_output:keep_pairs.append(pair)print(Trimmed from {} pairs to {}, {:.4f} of total.format(len(pairs), len(keep_pairs), len(keep_pairs) / len(pairs)))return keep_pairs# 实际进行处理 pairs trimRareWords(voc, pairs, MIN_COUNT)输出: keep_words 7823 / 18005 0.4345 Trimmed from 64271 pairs to 53165, 0.8272 of total18005个词之中频次大于等于3的只有43%去掉低频的57%的词之后保留的句子为53165占比为82%。 转换为模型需要的数据格式 前面我们构建了词典并且对训练数据进行预处理并且滤掉一些句对但是模型最终用到的是Tensor。最简单的办法是一次处理一个句对那么上面得到的句对直接就可以使用。但是为了加快训练速度尤其是重复利用GPU的并行能力我们需要一次处理一个batch的数据。 对于某些问题比如图像来说输入可能是固定大小的(或者通过预处理缩放成固定大小但是对于文本来说我们很难把一个二十个词的句子”缩放”成十个词同时还保持语义不变。但是为了充分利用GPU等计算自由我们又必须变成固定大小的Tensor因此我们通常会使用Padding的技巧把短的句子补充上零使得输入大小是(batch, max_length)这样通过一次就能实现一个batch数据的forward或者backward计算。 当然padding的部分的结果是没有意义的比如某个句子实际长度是5而max_length是10那么最终forward的输出应该是第5个时刻的输出后面5个时刻计算是无用功。方向计算梯度的时候也是类似的我们需要从第5个时刻开始反向计算梯度。为了提高效率我们通常把长度接近的训练数据放到一个batch里面这样无用的计算是最少的。因此我们通常把全部训练数据根据长度划分成一些组比如长度小于4的一组长度4到8的一组长度8到12的一组…。然后每次随机的选择一个组再随机的从一组里选择batch个数据。不过本文并没有这么做而是每次随机的从所有pair里随机选择batch个数据。 原始的输入通常是batch个list表示batch个句子因此自然的表示方法为(batch, max_length)这种表示方法第一维是batch每移动一个下标得到的是一个样本的max_length个词(包括padding)。因为RNN的依赖关系我们在计算t1时刻必须知道t时刻的结果因此我们无法用多个核同时计算一个样本的forward。但是不同样本之间是没有依赖关系的因此我们可以在根据t时刻batch样本的当前状态计算batch个样本的输出和新状态然后再计算t2时刻…。为了便于GPU一次取出t时刻的batch个数据我们通常把输入从(batch, max_length)变成(max_length, batch)这样使得t时刻的batch个数据在内存(显存)中是连续的从而读取效率更高。这个过程如下图所示原始输入的大小是(batch6, max_length4)转置之后变成(4,6)。这样某个时刻的6个样本数据在内存中是连续的。 因此我们会用一些工具函数来实现上述处理 inputVar函数: 把batch个句子padding后变成一个LongTensor大小是(max_length, batch)同时会返回一个大小是batch的list lengths说明每个句子的实际长度这个参数后面会传给PyTorch从而在forward和backward计算的时候使用实际的长度。
把句子的词变成ID
def indexesFromSentence(voc, sentence):return [voc.word2index[word] for word in sentence.split( )] [EOS_token]# l是多个长度不同句子(list)使用zip_longest padding成定长长度为最长句子的长度。
zeroPadding 同时通过 itertools.zip_longest 实现维度转换
原始维度(batch_size, max_length)即每个句子是一个子列表。
转换后维度(max_length, batch_size)即每个时间步是一个子列表。
def zeroPadding(l, fillvaluePAD_token):return list(itertools.zip_longest(*l, fillvaluefillvalue))# 把输入句子变成ID然后再padding同时返回lengths这个list标识实际长度。
返回的padVar是一个LongTensorshape是(batch, max_length)
lengths是一个list长度为(batch,)表示每个句子的实际长度。
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, lengthsinputVar处理后返回的padVar和lengths格式如下图所示: zeroPadding处理过程举例说明: l [[1, 2, 3], # 句子 1[4, 5], # 句子 2[6, 7, 8, 9] # 句子 3 ]# zeroPadding 转换后 result [[1, 4, 6], # 时间步 1[2, 5, 7], # 时间步 2[3, PAD_token, 8], # 时间步 3[PAD_token, PAD_token, 9] # 时间步 4 ]outputVar函数: 和inputVar类似但是它输出的第二个参数不是lengths而是一个大小为(max_length, batch)的mask矩阵(tensor)某位是0表示这个位置是padding1表示不是padding这样做的目的是后面计算方便。当然这两种表示是等价的只不过lengths表示更加紧凑但是计算起来不方便而mask矩阵和outputVar直接相乘就可以把padding的位置给mask(变成0)掉这在计算loss时会非常方便。
l是二维的padding后的list
返回m和l的大小一样如果某个位置是padding那么值为0否则为1
def binaryMatrix(l, valuePAD_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# 对输出句子进行padding然后用binaryMatrix得到每个位置是padding(0)还是非padding
同时返回最大最长句子的长度(也就是padding后的长度)
返回值padVar是LongTensorshape是(batch, max_target_length)
mask是ByteTensorshape也是(batch, max_target_length)
def outputVar(l, voc):indexes_batch [indexesFromSentence(voc, sentence) for sentence in l]max_target_len max([len(indexes) for indexes in indexes_batch])padList zeroPadding(indexes_batch)mask binaryMatrix(padList)mask torch.BoolTensor(mask)padVar torch.LongTensor(padList)return padVar, mask, max_target_lenoutputVar处理后返回的padVarmaskmax_target_len格式如下图所示: batch2TrainData函数: 则利用上面的两个函数把一个batch的句对处理成合适的输入和输出Tensor。
处理一个batch的pair句对
def batch2TrainData(voc, pair_batch):# 按照句子的长度(词数)排序pair_batch.sort(keylambda x: len(x[0].split( )), reverseTrue)input_batch, output_batch [], []for pair in pair_batch:input_batch.append(pair[0])output_batch.append(pair[1])# inp 维度为: (max_length,batch_size)inp, lengths inputVar(input_batch, voc)# output 维度为: (max_length,batch_size)output, mask, max_target_len outputVar(output_batch, voc)return inp, lengths, output, mask, max_target_len测试代码:
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 batchesprint(input_variable:, input_variable)
print(lengths:, lengths)
print(target_variable:, target_variable)
print(mask:, mask)
print(max_target_len:, max_target_len)batch2TrainData 处理后返回的inplengthsoutputmaskmax_target_len格式如下图所示: SeqToSeq 模型
我们这个chatbot的核心是一个sequence-to-sequence(seq2seq)模型。 seq2seq模型的输入是一个变长的序列而输出也是一个变长的序列。而且这两个序列的长度并不相同。一般我们使用RNN来处理变长的序列Sutskever等人的论文发现通过使用两个RNN可以解决这类问题。这类问题的输入和输出都是变长的而且长度不一样包括问答系统、机器翻译、自动摘要等等都可以使用seq2seq模型来解决。
其中一个RNN叫做Encoder它把变长的输入序列编码成一个固定长度的context向量我们一般可以认为这个向量包含了输入句子的语义。而第二个RNN叫做Decoder初始隐状态是Encoder的输出context向量输入是(表示句子开始的特殊Token)然后用RNN计算第一个时刻的输出接着用第一个时刻的输出和隐状态计算第二个时刻的输出和新的隐状态…直到某个时刻输出特殊的(表示句子结束的特殊Token)或者长度超过一个阈值。 本文中提到的RNN某个时刻的输出指的是Rnn Cell计算得到的隐藏状态经过前馈层线性变换后的输出结果而隐藏状态则代表Rnn Cell计算得到的输出注意区分 Seq2Seq模型如下图所示: Encoder 编码器
Encoder是个RNN它会遍历输入的每一个Token(词)每个时刻的输入是上一个时刻的隐状态和当前时刻的输入然后会有一个输出和新的隐状态。这个新的隐状态会作为下一个时刻的输入隐状态。
每个时刻都有一个输出对于seq2seq模型来说我们通常只保留最后一个时刻的隐状态认为它编码了整个句子的语义但是后面我们会用到Attention机制它还会用到Encoder每个时刻的输出。Encoder处理结束后会把最后一个时刻的隐状态作为Decoder的初始隐状态。
实际我们通常使用多层的Gated Recurrent Unit(GRU)或者LSTM来作为Encoder这里使用GRU此外我们会使用双向的RNN如下图所示: 注意在接入RNN之前会有一个embedding层用来把每一个词(ID或者one-hot向量)映射成一个连续的稠密的向量我们可以认为这个向量编码了一个词的语义。在我们的模型里我们把它的大小定义成和RNN的隐状态大小一样(但是并不是一定要一样)。有了Embedding之后模型会把相似的词编码成相似的向量(距离比较近)。
最后为了把padding的batch数据传给RNN我们需要使用下面的两个函数来进行pack和unpack后面我们会详细介绍它们。这两个函数是
torch.nn.utils.rnn.pack_padded_sequencetorch.nn.utils.rnn.pad_packed_sequence
Encoder数据流向过程:
把词的ID通过Embedding层变成向量。把padding后的数据进行pack。传入GRU进行Forward计算。Unpack计算结果。把双向GRU的结果向量加起来。返回(所有时刻的)输出和最后时刻的隐状态。
输入:
input_seq: 一个batch的输入句子shape是(max_length, batch_size)input_lengths: 一个长度为batch的list表示句子的实际长度。hidden: 初始化隐状态(通常是零)shape是(n_layers x num_directions, batch_size, hidden_size)
输出:
outputs: 最后一层GRU的输出向量(双向的向量加在了一起)shape(max_length, batch_size, hidden_size)hidden: 最后一个时刻的隐状态shape是(n_layers x num_directions, batch_size, hidden_size) 关于GRU输入输出维度问题不太清楚的可以看我之前写的这篇文章: 单向/双向单层/多层RNN输入输出维度问题 EncoderRNN代码如下:
import torch
from torch import nnclass EncoderRNN(nn.Module):def init(self, hidden_size, embedding, n_layers1, dropout0):super(EncoderRNN, self).init()self.n_layers n_layersself.hidden_size hidden_sizeself.embedding embedding# 初始化GRU这里输入和hidden大小都是hidden_size这里假设embedding层的输出大小是hidden_size# 如果只有一层那么不进行Dropout否则使用传入的参数dropout进行GRU的Dropout。self.gru nn.GRU(hidden_size, hidden_size, n_layers,dropout(0 if n_layers 1 else dropout), bidirectionalTrue)def forward(self, input_seq, input_lengths, hiddenNone):# 输入是(max_length, batch)Embedding之后变成(max_length, batch, hidden_size)embedded self.embedding(input_seq)# Pack padded batch of sequences for RNN module# 因为RNN(GRU)要知道实际长度所以PyTorch提供了函数pack_padded_sequence把输入向量和长度# pack到一个对象PackedSequence里这样便于使用。input_lengths input_lengths.to(dtypetorch.int64)input_lengths input_lengths.cpu()packed torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths)# 通过GRU进行forward计算需要传入输入和隐变量# 如果传入的输入是一个Tensor (max_length, batch, hidden_size)# 那么输出outputs是(max_length, batch, hidden_size*num_directions)。# 第三维是hidden_size和num_directions的混合它们实际排列顺序是num_directions在前面# 因此我们可以使用outputs.view(seq_len, batch, num_directions, hidden_size)得到4维的向量。# 其中第三维是方向第四位是隐状态。# 而如果输入是PackedSequence对象那么输出outputs也是一个PackedSequence对象我们需要用# 函数pad_packed_sequence把它变成shape为(max_length, batch, hidden*num_directions)的向量以及# 一个list表示输出的长度当然这个list和输入的input_lengths完全一样因此通常我们不需要它。outputs, hidden self.gru(packed, hidden)# 参考前面的注释我们得到outputs为(max_length, batch, hidden*num_directions)outputs, _ torch.nn.utils.rnn.pad_packed_sequence(outputs)# 我们需要把输出的num_directions双向的向量加起来# 因为outputs的第三维是先放前向的hidden_size个结果然后再放后向的hidden_size个结果# 所以outputs[:, :, :self.hidden_size]得到前向的结果# outputs[:, :, self.hidden_size:]是后向的结果# 注意如果bidirectional是False则outputs第三维的大小就是hidden_size# 这时outputs[:, : ,self.hidden_size:]是不存在的因此也不会加上去。# 对Python slicing不熟的读者可以看看下面的例子# a[1,2,3]# a[:3]# [1, 2, 3]# a[3:]# []# a[:3]a[3:]# [1, 2, 3]# 这样就不用写下面的代码了# if bidirectional:# outputs outputs[:, :, :self.hidden_size] outputs[:, : ,self.hidden_size:]outputs outputs[:, :, :self.hidden_size] outputs[:, :, self.hidden_size:]# 返回最终的输出和最后时刻的隐状态。return outputs, hiddenDecoder 解码器
Decoder也是一个RNN它每个时刻输出一个词。每个时刻的输入是上一个时刻的隐状态和上一个时刻的输出。一开始的隐状态是Encoder最后时刻的隐状态输入是特殊的。然后使用RNN计算新的隐状态和输出第一个词接着用新的隐状态和第一个词计算第二个词…直到遇到结束输出。普通的RNN Decoder的问题是它只依赖与Encoder最后一个时刻的隐状态虽然理论上这个隐状态(context向量)可以编码输入句子的语义但是实际会比较困难。因此当输入句子很长的时候效果会很差。
全局注意力机制
为了解决这个问题Bahdanau等人在论文里提出了注意力机制(attention mechanism)在Decoder进行t时刻计算的时候除了t-1时刻的隐状态当前时刻的输入注意力机制还可以参考Encoder所有时刻的输入。
拿机器翻译来说我们在翻译以句子的第t个词的时候会把注意力机制在某个词上。当然常见的注意力是一种soft的注意力假设输入有5个词注意力可能是一个概率比如(0.6,0.1,0.1,0.1,0.1)表示当前最关注的是输入的第一个词。同时我们之前也计算出每个时刻的输出向量假设5个时刻分别是 y 1 , … , y 5 y1,…,y5 y1,…,y5那么我们可以用attention概率加权得到当前时刻的context向量 0.6 y 1 0.1 y 2 … 0.1 y 5 0.6y10.1y2…0.1y5 0.6y10.1y2…0.1y5。
注意力有很多方法计算我们这里介绍Luong等人在论文提出的方法。它是用当前时刻的GRU计算出的新的隐状态来计算注意力得分首先它用一个score函数计算这个隐状态和Encoder的输出的相似度得分得分越大说明越应该注意这个词。然后再用softmax函数把score变成概率。
以机器翻译为例在t时刻 h t ht ht表示t时刻的GRU输出的新的隐状态我们可以认为 h t ht ht表示当前需要翻译的语义。通过计算 h t ht ht与 y 1 , … , y n y1,…,yn y1,…,yn的得分如果 h t ht ht与 y 1 y1 y1的得分很高那么我们可以认为当前主要翻译词 x 1 x1 x1的语义。
有很多中score函数的计算方法如下图所示 上式中 h t ht ht表示 t t t时刻的隐状态比如第一种计算 s c o r e score score的方法直接计算 h t ht ht与 h s hs hs的内积内积越大说明这两个向量越相似因此注意力也更多的放到这个词上。
第二种方法也类似只是引入了一个可以学习的矩阵我们可以认为它先对 h t ht ht做一个线性变换然后在与 h s hs hs计算内积。
而第三种方法把它们拼接起来然后用一个全连接网络来计算 s c o r e score score。
注意我们前面介绍的是分别计算 h t ht ht和 y 1 y1 y1的内积、 h t ht ht和 y 2 y2 y2的内积…。但是为了效率可以一次计算 h t ht ht与 h s [ y 1 , y 2 , … , y n ] hs[y1,y2,…,yn] hs[y1,y2,…,yn]的乘积。 计算过程如下图所示。 import torch
import torch.nn.functional as Fclass Attn(torch.nn.Module):def init(self, method, hidden_size):super(Attn, self).init()self.method methodif self.method not in [dot, general, concat]:raise ValueError(self.method, is not an appropriate attention method.)self.hidden_size hidden_sizeif self.method general:self.attn torch.nn.Linear(self.hidden_size, hidden_size)elif self.method concat:self.attn torch.nn.Linear(self.hidden_size * 2, hidden_size)self.v torch.nn.Parameter(torch.FloatTensor(hidden_size))def dot_score(self, hidden, encoder_output):# 输入hidden的shape是(1, batch64, hidden_size500)# encoder_outputs的shape是(input_lengths10, batch64, hidden_size500)# hidden * encoder_output得到的shape是(10, 64, 500)然后对第3维求和就可以计算出score。return torch.sum(hidden * encoder_output, dim2)def general_score(self, hidden, encoder_output):energy self.attn(encoder_output)return torch.sum(hidden * energy, dim2)def concat_score(self, hidden, encoder_output):energy self.attn(torch.cat((hidden.expand(encoder_output.size(0), -1, -1),encoder_output), 2)).tanh()return torch.sum(self.v * energy, dim2)# 输入是上一个时刻的隐状态hidden和所有时刻的Encoder的输出encoder_outputs# 输出是注意力的概率也就是长度为input_lengths的向量它的和加起来是1。def forward(self, hidden, encoder_outputs):# 计算注意力的score输入hidden的shape是(1, batch64, hidden_size500)# 表示t时刻batch数据的隐状态# encoder_outputs的shape是(input_lengths10, batch64, hidden_size500)if self.method general:attn_energies self.general_score(hidden, encoder_outputs)elif self.method concat:attn_energies self.concat_score(hidden, encoder_outputs)elif self.method dot:# 计算内积参考dot_score函数attn_energies self.dot_score(hidden, encoder_outputs)# Transpose max_length and batch_size dimensions# 把attn_energies从(max_length10, batch64)转置成(64, 10)attn_energies attn_energies.t()# 使用softmax函数把score变成概率shape仍然是(64, 10)然后用unsqueeze(1)变成# (64, 1, 10)return F.softmax(attn_energies, dim1).unsqueeze(1)上面的代码实现了dot、general和concat三种score计算方法分别和前面的三个公式对应我们这里介绍最简单的dot方法。代码里也有一些注释只有dot_score函数比较难以理解我们来分析一下。首先这个函数的输入hidden的shape是(1, batch64, hidden_size500)encoder_outputs的shape是(input_lengths10, batch64, hidden_size500)。
怎么计算hidden和10个encoder输出向量的内积呢
为了简便我们先假设batch是1这样可以把第二维(batch维)去掉因此hidden是(1, 500)而encoder_outputs是(10, 500)。内积的定义是两个向量对应位相乘然后相加但是encoder_outputs是10个500维的向量。当然我们可以写一个for循环来计算但是效率很低。这里用到一个小的技巧利用broadcastinghidden * encoder_outputs可以理解为把hidden从(1,500)复制成(10, 500)当然实际实现并不会这么做然后两个(10, 500)的矩阵进行乘法。注意这里的乘法不是矩阵乘法而是所谓的Hadamard乘法其实就是把对应位置的乘起来比如下面的例子 因此hidden * encoder_outputs就可以把hidden向量(500个数)与encoder_outputs的10个向量(500个数)对应的位置相乘。而内积还需要把这500个乘积加起来因此后面使用torch.sum(hidden * encoder_output, dim2)把第2维500个乘积加起来最终得到10个score值。 当然我们实际还有一个batch维度因此最终得到的attn_energies是(10, 64)。接着在forward函数里把attn_energies转置成(64, 10)然后使用softmax函数把10个score变成概率shape仍然是(64, 10)为了后面使用方便我们用unsqueeze(1)把它变成(64, 1, 10)。 说明 : 说明: 说明:
encoder_outputs 指的是EncoderRNN的forward方法返回的outputs结果如下所示 # encoder的Forward计算 — (max_len,batch_size) , 每个序列的长度encoder_outputs, encoder_hidden encoder(input_variable, lengths)解码器实现
有了注意力的子模块之后我们就可以实现Decoder了。Encoder可以一次把一个序列输入GRU得到整个序列的输出。但是Decoder t时刻的输入是t-1时刻的输出在t-1时刻计算完成之前是未知的因此只能一次处理一个时刻的数据。因此Encoder的GRU的输入是(max_length, batch, hidden_size)而Decoder的输入是(1, batch, hidden_size)。
此外Decoder只能利用前面的信息所以只能使用单向(而不是双向)的GRU而Encoder的GRU是双向的如果两种的hidden_size是一样的则Decoder的隐单元个数少了一半那怎么把Encoder的最后时刻的隐状态作为Decoder的初始隐状态呢
这里是把每个时刻双向结果加起来的因此它们的大小就能匹配了请读者参考前面Encoder双向相加的部分代码。
Decoder数据流向过程:
把词ID输入Embedding层。使用单向的GRU继续Forward进行一个时刻的计算。使用新的隐状态计算注意力权重。用注意力权重得到context向量。context向量和GRU的输出拼接起来然后再进过一个全连接网络使得输出大小仍然是hidden_size。使用一个投影矩阵把输出从hidden_size变成词典大小然后用softmax变成概率 。返回输出和新的隐状态。
输入:
input_step: shape是(1, batch_size)last_hidden: 上一个时刻的隐状态 shape是(n_layers x num_directions, batch_size, hidden_size)encoder_outputs: encoder的输出 shape是(max_length, batch_size, hidden_size)
输出:
output: 当前时刻输出每个词的概率shape是(batch_size, voc.num_words)hidden: 新的隐状态shape是(n_layers x num_directions, batch_size, hidden_size)
import torch
from torch import nn
import torch.nn.functional as F
from Attention import Attnclass GlobalAttnDecoderRNN(nn.Module):def init(self, attn_model, embedding, hidden_size, output_size, n_layers1, dropout0.1):super(GlobalAttnDecoderRNN, self).init()# 保存到self里attn_model就是前面定义的Attn类的对象。self.attn_model attn_modelself.hidden_size hidden_sizeself.output_size output_sizeself.n_layers n_layersself.dropout dropout# 定义Decoder的layersself.embedding embeddingself.embedding_dropout nn.Dropout(dropout)self.gru nn.GRU(hidden_size, hidden_size, n_layers, dropout(0 if n_layers 1 else dropout))# [context , hidden]self.concat nn.Linear(hidden_size * 2, hidden_size) # 上下文信息和隐藏层信息做融合self.out nn.Linear(hidden_size, output_size)self.attn Attn(attn_model, hidden_size)def forward(self, input_step, last_hidden, encoder_outputs):# 注意decoder每一步只能处理一个时刻的数据因为t时刻计算完了才能计算t1时刻。# input_step的shape是(1, 64)64是batch1是当前输入的词ID(来自上一个时刻的输出)# 通过embedding层变成(1, 64, 500)然后进行dropoutshape不变。embedded self.embedding(input_step)embedded self.embedding_dropout(embedded)# 把embedded传入GRU进行forward计算# 得到rnn_output的shape是(1, 64, 500)# hidden是(2, 64, 500)因为是两层的GRU所以第一维是2。rnn_output, hidden self.gru(embedded, last_hidden)# 计算注意力权重 根据前面的分析attn_weights的shape是(64, 1, 10)attn_weights self.attn(rnn_output, encoder_outputs)# encoder_outputs是(10, 64, 500)# encoder_outputs.transpose(0, 1)后的shape是(64, 10, 500)# attn_weights.bmm后是(64, 1, 500)# bmm是批量的矩阵乘法第一维是batch我们可以把attn_weights看成64个(1,10)的矩阵# 把encoder_outputs.transpose(0, 1)看成64个(10, 500)的矩阵# 那么bmm就是64个(1, 10)矩阵 x (10, 500)矩阵最终得到(64, 1, 500)# context attn_weights.bmm(encoder_outputs.transpose(0, 1))# 把context向量和GRU的输出拼接起来# rnn_output从(1, 64, 500)变成(64, 500)rnn_output rnn_output.squeeze(0)# context从(64, 1, 500)变成(64, 500)context context.squeeze(1)# 拼接得到(64, 1000)concat_input torch.cat((rnn_output, context), 1)# self.concat是一个矩阵(1000, 500) – concat 是全连接层,负责将上下文信息和隐藏层信息做融合,并转换维度# self.concat(concat_input)的输出是(64, 500)# 然后用tanh把输出返回变成(-1,1)concat_output的shape是(64, 500)concat_output torch.tanh(self.concat(concat_input))# out是(500, 词典大小7826)output self.out(concat_output)# 用softmax变成概率表示当前时刻输出每个词的概率。output F.softmax(output, dim1)# 返回 output和新的隐状态return output, hidden这里重点讲解一下attn_weights.bmm干的事情还是以batch_size1为背景当我们获取到了归一化后的attn_weights注意力权重向量时下面要做的事情就是将attn_weights中每个分量作为信息融合的权重依次分配给encoder的10个Rnn Cell的隐藏层最终的输出向量然后累加在一起得到我们需要的context上下文向量:
如果大家有了解过Self-Attention机制这里就可以将decoder的某个Time Step对应的Rnn cell输出的隐藏层向量视为Query向量而encoder所有隐藏层输出的结果集合encoder_outputs中每个隐藏层输出结果都作为一个独立Key向量同时也作为Value向量由于Query和KeyValue向量来源不同也可以看做是一种交叉自注意力机制的简单实现。
我们拿着Query去依次与所有Key向量计算相似度(向量内积)得到注意力分数向量每一个分数表明某个Key与我们的Query的匹配程度
再对注意力分数做归一化处理为注意力权重向量然后再用这组权重向量作为信息融合依据分配给每个Key对应的Value向量再做信息的累加聚合得到当前关注的上下文信息。
- 上一篇: 怎么更改网站备案信息美食门户类网站模版
- 下一篇: 怎么和网站建设公司签合同永康营销型网站建设
相关文章
-
怎么更改网站备案信息美食门户类网站模版
怎么更改网站备案信息美食门户类网站模版
- 技术栈
- 2026年04月20日
-
怎么跟客户介绍网站建设中国招标投标公共信息服务平台
怎么跟客户介绍网站建设中国招标投标公共信息服务平台
- 技术栈
- 2026年04月20日
-
怎么给一个网站做搜索功能图片外链生成工具
怎么给一个网站做搜索功能图片外链生成工具
- 技术栈
- 2026年04月20日
-
怎么和网站建设公司签合同永康营销型网站建设
怎么和网站建设公司签合同永康营销型网站建设
- 技术栈
- 2026年04月20日
-
怎么加入网站做微商城项目建设综述
怎么加入网站做微商城项目建设综述
- 技术栈
- 2026年04月20日
-
怎么建php网站php作品源代码免费下载
怎么建php网站php作品源代码免费下载
- 技术栈
- 2026年04月20日
