前语
诶,标题有点欠揍是吧。好吧承认有点标题党了,拖更大王要更新了。什么你是从这篇博文:快速构建一个简略的对话+问答AI (上)过来的。好吧被你发现了,我是把中心那一段拆开来了。好吧,之所以这样做其实仍是由于那篇文章是在是太长了,没写清楚,一同每一个模块都是独立的,因而的话咱们专门拆开来再说下是咋干的。咱们这是一篇独立的博文,那么为啥要独立捏,由于我知道你或许并不需求一个比较完好的内容,假如你重视的是怎么完结一个对话AI的话,那么来这儿就对了。咱们将独自从数据集开端再讲起。并且将实在完好的代码直接在咱们的博文傍边贴出了。(由于第三个模块还在做,暂时没有上传库房)仍是那句话,假如重视的是一个闲聊对话AI是怎么完结的话,come here!!!
当然标题有点夸张,可是你要做一个所谓的对话AI女友是彻底能够的,至少咱们能够自己做服务了,当然调用服务,例如图灵机器人啥的仍是不错的,少掉不少头发呢。
首要是目录结构,你需求依照框起来的当地进行创立文件。
这儿的话我就不去前次库房了,自己看着这个目录创立,或许等我完好的项目上传到GitHub后自己提取对应的文件。
那么一同的话,对应的资源比方语料资源在这: 链接:pan.baidu.com/s/1Bb0sWcIT… 提取码:6666
里边包含了语料和停用词,分词之类的
他们的格局是这样的:
停用词
打开之后的话,格局大约是这样的:
词库的格局也是相似的。
闲聊语料
这个闲聊也简略,是这样的:
E 是开端标志 M 对话
这个到时分怎么用,咱们在后边再说。
ok,咱们这边都创立好了资源都预备好了。那么现在咱们把资源放到对应的方位:
基础知识
词的表明
这部分的话咱们从前说过,可是呢,这个是独立的博文,因而咱们重复一下。
表达
计算机和咱们人类是不相同的,他只能进行根本的数字运算,在咱们从前的图画处理傍边,图画的表达依然仍是经过数值矩阵的,可是一个语句或许单纯是怎么表明的呢。所以为了能够让计算机能够处理到咱们的文本数据,咱们需求对文本做一点点处理。
那么在这儿是怎么做的呢,其实很简略,已然计算机只能处理数字,对数字进行运算,那么咱们只需求把咱们的一个语句转化为一种向量就好了。那么这个是怎么做的呢?
其实十分简略。
看下面一组图就明白了:
咱们经过一个词典其实就能够完结一个向量的映射。
看到了吧,咱们这个时分咱们只需求对一个语句进行分词,之后将每一个词进行标号,这样一来就能够完结把一个语句转化为一个向量。
one-hot编码
此刻咱们得到了一组序列,可是这个序列的表达能力是在是太弱了,只能表明出一个标号,不能表明出其他的特色。或许说,只要一个数字表明一个词语实在是太单调了,1个词语也应该由一个序列组成。那么这个时分one-hot编码就出来了。他是这样做的: 首要一个词,一个字,咱们叫做token,那么编码的很简略。其实便是这样: 可是这样是有问题的,那便是说,咱们尽管完结了一个词到向量的表明。可是这个表明办法显然是太大了,假定有10000个词语,那么依照这种办法进行标号的话,那么1个词便是10000个维度。这样显然是不行的。所以这块需求优化一下。
词嵌入
这个本来解释起来略微杂乱一点。你只需求需求知道他们的实质其实便是这样的: 词 ——> 向量空间1 ——> 向量空间2 现在向量空间1不合适,所以咱们要想办法能不能往空间2进行靠拢。
于是乎这儿大约就有了两个计划:
1)测验将词向量映射到一个更低维的空间; 2)一同保持词向量在该低维空间中具有语义相似性,如此,越相关的词,它们的向量在这个低维空间里就能靠得越近。
关于榜首个,咱们能够参考本来咱们做协同过滤引荐dome的时分,运用SVD矩阵分化来做。(关于这篇博文的话也是有优化的,优化计划将在本篇博文中查看到,先插个眼)
那么缺点的话也很明显嘛,用咱们的这个计划:
1)亲和矩阵的维度或许常常变,由于总有新的单词加进来,每加进来一次就要重新做SVD分化,因而这个办法不太通用; 2)亲和矩阵或许很稀少,由于许多单词并不会成对呈现。
大致原理
ok,回到咱们的这个(这部分能够选择越过,知道这个玩意终究得到的是啥就好了),这个该怎么做,首要的话,完结这个东西,大约是有两种计划去做:Continuous Bag Of Words (CBOW)办法和n-gram办法。榜首个计划的话,这个比较杂乱,咱们这儿就不介绍了。
咱们来说说第二个计划。
首要咱们来说说啥是N-gram,首要原理的话也是比较杂乱的,具体参考这个:blog.csdn.net/songbinxu/a…
那么咱们这边便是简略说一下这个在咱们这边N-gram实际是咋用的。
[cuted[i:i+2]for i in range(len(cuted))]
其实便是这个,用代码表明,cuted是一个分好词的语句。i+2表明跨越几个。
这样做的优点是,经过N-gram能够考虑到词语之间的一个联系,假如咱们运用这个计划来完结一个词向量的话,那么咱们必定是能够能够完结:“一同保持词向量在该低维空间中具有语义相似性,如此,越相关的词,它们的向量在这个低维空间里就能靠得越近。”
的。由于的确考虑到了之间的一个联系,那么现在咱们已经知道了大约N-garm是怎么样的了,其实便是一种办法,将一个语句相近的词语进行衔接,或许说是对语句进行一个切割,上面那个仅仅一种办法只要,这个咱们在后边还会有阐明,总之它是十分好用的一种办法。
ok,知道了这个咱们再来介绍几个名词:
1.跳词模型
跳词模型,它是经过文本中某个单词来估测前后几个单词。例如,依据‘rabbit’来揣度前后的单词或许为‘a’,’is’,’eating’,’carrot’。在练习模型时咱们在文本中选取若干接连的固定长度的单词序列,把前后的一些单词作为输出,中心的某个方位的单词作为输入。
2.接连词袋模型
接连词袋模型与跳词模型恰好相反,它是依据文本序列中周围单词来猜测中心词。在练习模型时,把序列中周围单词作为输入,中心词作为输出。
这个的话其实和咱们的这个联系不大,由于N-gram其实是语句–>词 的一种办法,可是对我练习的时分的输入仍是有帮助的,由于这样输入的话,咱们是能够得到词在语句傍边的一种相相关系的。
而embedding是词到one-hot然后one-hot到低纬向量的改变进程。
完结
ok,扯了那么多,那么接下来看看咱们怎么完结这个东西。
咱们需求一个词向量,一同咱们有许多词语,因而咱们将得到一个矩阵,这个矩阵叫做embedding矩阵。
咱们首要随机初始化embeddings矩阵,构建一个简略的网络。初始化weights和biases,计算隐藏层的输出。然后计算输出和target成果的穿插熵,之后运用优化器完结一次反向传递,更新可练习的参数,包含embeddings变量。并且咱们将词之间的相似度能够看作概率。
ok,咱们直接看到代码,那么咱们也是有两个版其他。简略版,杂乱版。
简略版
简略版其他话,在pytorch傍边有完结:
embed=nn.Embedding(word_num,embedding_dim)
杂乱版
那么咱们显然是不满足这个的,那么咱们还有杂乱版别。便是自己着手,锦衣玉食! 首要咱们界说这个:
class embedding(nn.Module):
def __init__(self,in_dim,embed_dim):
super().__init__()
self.embed=nn.Sequential(nn.Linear(in_dim,200),
nn.ReLU(),
nn.Linear(200,embed_dim),
nn.Sigmoid())
def forward(self,input):
b,c,_=input.shape
output=[]
for i in range(c):
out=self.embed(input[:,i])
output.append(out.detach().numpy())
return torch.tensor(np.array(output),dtype=torch.float32).permute(1,0,2)
很简略的一个结构。 那么咱们输入是上面,首要其实是咱们one-hot编码的一个矩阵。 咱们其实流程便是这样的:词—>one-hot—>embedding/svd
ok,那么咱们的N-gram怎么表明呢,其实这个更多的仍是在于对语句的分化上,输入的语句的词向量怎么表明的。
怎么练习
怎么练习的话,首要仍是要在one-hot处理的时分再加一个处理,这个进程或许比较绕。便是说咱们依照上面说到的词袋模型进行构造咱们的数据,咱们举个比方吧。
现在有这样的一个文本,分词之后,词的个数是content_size。有num_word个词。
import torch
import re
import numpy as np
txt=[] #文本数据
with open('peter_rabbit.txt',encoding='utf-8') as f:
for line in f.readlines():
l=line.strip()
spilted_sentence=re.split(" |;|-|,|!|\'",l)
for w in spilted_sentence:
if w !='':
txt.append(w.lower())
vol=list(set(txt)) #单词表
n=len(vol) #单词表单词数
vol_dict=dict(zip(vol,np.arange(n))) #单词索引
'''
这儿运用词袋模型
每次从文本中选取序列长度为9,输入单词数为,8,输出单词数为1,
中心词位于序列中心方位。并且采用pytorch中的emdedding和自己设计embedding两种办法
词嵌入维度为100。
'''
data=[]
label=[]
for i in range(content_size):
in_words=txt[i:i+4]
in_words.extend(txt[i+6:i+10])
out_word=txt[i+5]
in_one_hot=np.zeros((8,n))
out_one_hot=np.zeros((1,n))
out_one_hot[0,vol_dict[out_word]]=1
for j in range(8):
in_one_hot[j,vol_dict[in_words[j]]]=1
data.append(in_one_hot)
label.append(out_one_hot)
class dataset:
def __init__(self):
self.n=ci=config.content_size
def __len__(self):
return self.n
def __getitem__(self, item):
traindata=torch.tensor(np.array(data),dtype=torch.float32)
trainlabel=torch.tensor(np.array(label),dtype=torch.float32)
return traindata[item],trainlabel[item]
咱们仅仅在投喂数据的时分依照词袋模型进行投喂,或许接连模型也能够。
当然咱们这儿所说的都仅仅说预练习出一个模型出来,实际上,咱们直接运用这个结构,然后进行正常的练习完结咱们的一个模型也是能够的。她是很灵敏的,不是固定的!
那么继续预练习的话便是依照词袋模型来就好了(看不懂没联系,越过就好了)
import torch
from torch import nn
from torch.utils.data import DataLoader
from dataset import dataset
import numpy as np
class model(nn.Module):
def __init__(self):
super().__init__()
self.embed=embedding(num_word,100)
self.fc1=nn.Linear(num_word,1000)
self.act1=nn.ReLU()
self.fc2=nn.Linear(1000,num_word)
self.act2=nn.Sigmoid()
def forward(self,input):
b,_,_=input.shape
out=self.embed (input).view(b,-1)
out=self.fc1 (out)
out=self.act1(out)
out=self.fc2(out)
out=self.act2(out)
out=out.view(b,1,-1)
return out
if __name__=='__main__':
pre_model=model()
optim=torch.optim.Adam(params=pre_model.parameters())
Loss=nn.MSELoss()
traindata=DataLoader(dataset(),batch_size=5,shuffle=True)
for i in range(100):
print('the {} epoch'.format(i))
for d in traindata:
p=model(d[0])
loss=Loss(p,d[1])
optim.zero_grad()
loss.backward()
optim.step()
这样一来就能够初步完结预练习,你只需求加载好embeding部分的权重就好了,这个仅仅加速收敛的一种办法。
转换后的形状
终究,词嵌入的话,得到的矩阵是将one-hot改变为了这样的矩阵
ok,词的表达已经了,那么接下来咱们在简略介绍一下RNN。 (当然关于这一部分,实际上的话其实还有其他办法,可是咱们这边仅仅用到这些东西,所以仅仅介绍这个)
RNN循环网络
RNN
这个RNN的话,咋说呢,其实挺简略的,可是有几个点或许是比较简单误导人的,搞清楚这个结构的话,关于咱们后边关于LSTM,GRU这种网络的架构或许会更好了解,其实包含LSTM,GRU的话其实实质上仍是挺简略的。当然能够直接提出这个东西的人是十分厉害的,不过不管怎么说他们都是属于循环神经网络的一个大家族的,仅仅在数据处理上面多了一点点东西。那么了解了RNN之后的话,关于我后边了解LSTM,GRU里边它的一个数据的变幻,传递,原理。由于后边的话,咱们仍是要手写完结这个GRU的(LSTM也是相同的,可是GRU少了点参数,耗费的计算资源少一点点)。所以关于这一部分仍是有必要好好唠一唠的。
首要咱们来看到根本的神经网络:
这是一个简略的前馈神经网络,也是咱们最常见的神经网络。
接下来是咱们的RNN神经网络,在大多数情况下,咱们常常会说到这几个名词:时刻步,终究一层输出等等。
那么在这儿的话,咱们需求了解展开的其实只要一个东西,那便是对应时刻步的了解,什么是上一层网络的输出,他们之间的参数是怎么传递的。
RNN投影图
那么在此之前,咱们先来看看RNN的网络结构大约是什么姿态的。 大多数情况下,你搜索到的图片或许是这样的:
首要承认这张图十分的简练,以至于你或许一开端没有反应过来,什么表现循环,表现时刻步的当地在哪。其实这儿的话,这种图其实仅仅一个缩略平面图。
RNN是三维立体的
可是实际上,假如需求用画图来表明的话,RNN其实是立体的一个姿态。大约长这个姿态: 或许有点抽象,可是它的意思其实便是这样的,这个其实是RNN实在的姿态,之后经过对不同的时刻步的输出进行不同的处理,终究咱们还能够将RNN进行分类。
OK,这个便是咱们在RNN里边需求留意的点,它的实在结构是这样的,是一个三维度的结构。相同的接下来要说到的LSTM,GRU都是。
OK,接下来还没完,咱们现在需求不目光放长远一点,首要是在RNN里边关于层的概念,咱们接下来会说什么什么层,建立几层的一个LSTM,GRU之类的,或许说几层的RNN,这个层其实是指,一个时刻步上有几个立体的层,而不是说从前平面的那种网络,说几层几层。由于实际上,咱们这儿图画的就一层全衔接(输入层不算),可是在时刻步上,它是N层,你有几个X就有几个层。
咱们拿一个语句为例,假定一句话有5个单词,或许说处理之后有5个词语。那么RNN便是把每一个词的词向量作为输入,依照次序,依照上面图的次序进行输入。此刻需求做的便是循环5次。
LSTM&GRU
那么之后的话,咱们再来说说LSTM和GRU,他们呢叫做长短期回忆网络,其实便是最low的RNN的一个升级版,对信息进一步处理。咱们关于模型的调优,优化说白了,除了性能的优化,便是对信息的最大利用(添加信息,或许对要点信息进行提取)。所以根本上为什么大模型的作用很好,其实不考虑对信息的利用率,单单是对信息的运用就已经到达了超大的规划,这作用肯定是比小模型好一点的。
那么这儿的话,咱们就简略过一下这个结构图吧。
首要是LSTM,其实的话他这儿主要是引入了一个东西,叫做回忆。
c便是回忆,由于刚刚的RNN,的话其实更像是一个一阶的马尔科夫,那么导入这个的话,就适当于日记,你不仅仅知道了昨日做了什么,还知道了前天做了什么,这样的话关于信息的利用坑定是上去了的。那么这个是它的一个单元。 宏观上仍是这样的:
同理GRU也是相同的 可是这儿的话少了一个c 其实仍是说把Ht和c合在了一同,他们作用是差不多的,各有各的优点,你用LSTM还能多得到一个日记本,用GRU的话其实适当于,你把日记写在了脑子里边。优点是省钱,害处是有时分要你女朋友或许需求检查日记(尽管我知道你有95%以上的概率是没有的,一般设置0.05 作为阈值,低于这个概率,根本上咱们认为G了)
ok,这些咱们都说完了
构建数据
nice到这儿了,这部分的话,咱们还需求知道一些东西。咱们现在知道了图的表达,也知道了RNN大约是啥,啥是时刻步之类的。这儿要点对应RNN便是那玩意是3维的,包含那个LSTM,GRU其实都是。
那么现在还需求干啥呢,当然是榜首部分,咱们要把词变成序列呀。
装备
在开端之前,咱们还需求给出装备哈,那么咱们这儿先给出来,在这儿:
"""
just configuration for this project to run
"""
import pickle
auto_fix = True
jieba_config = {
"word_dict":"./../../data/word/word40W.txt",
"stop_dict":"./../../data/word/stopWordBaiDu.txt",
}
data_path = {
"xiaohuangji": "./../../data/XiaoHuangJi50W.conv",
"QA": "./../../data/QA5W.json"
}
"""
Encoder and Decoder using same config params in here
"""
chatboot_config = {
"target_path_no_by_word":"./../../data/chat/target_no_by_word.txt",
"input_path_no_by_word": "./../../data/chat/input_no_by_word.txt",
"word_corpus_no_by_word_input":"./../../data/chat/word_corpus_input_no_by_word.pkl",
"word_corpus_no_by_word_target":"./../../data/chat/word_corpus_target_no_by_word.pkl",
"target_path_by_word": "./../../data/chat/target_by_word.txt",
"input_path_by_word": "./../../data/chat/input_by_word.txt",
"word_corpus_by_word_input": "./../../data/chat/word_corpus_input_by_word.pkl",
"word_corpus_by_word_target": "./../../data/chat/word_corpus_target_by_word.pkl",
"seq2seq_model_no_by_word":"./../../data/chat/seq2seq_model_no_by_word.pth",
"optimizer_model_no_by_word":"./../../data/chat/optimizer_model_no_by_word.pth",
"seq2seq_model_by_word": "./../../data/chat/seq2seq_model_by_word.pth",
"optimizer_model_by_word": "./../../data/chat/optimizer_model_by_word.pth",
"batch_size": 128,
"collate_fn_is_by_word": False,
"input_max_len":12,
"target_max_len": 12,
"out_seq_len": 15,
"dropout": 0.3,
"embedding_dim": 300,
"padding_idx": 0,
"sos_idx": 2,
"eos_idx": 3,
"unk_idx": 1,
"num_layers": 2,
"hidden_size": 128,
"bidirectional":True,
"batch_first":True,
# support 0,1,..3(gpu) and cup
"drive":"0",
"num_workers":0,
"teacher_forcing_ratio": 0.1,
# just support "dot","general","concat"
"attention_method":"general",
"use_attention": True,
"beam_width": 3,
"max_norm": 1,
"beam_search": True
}
def chat_load_(path,by_word,is_target,fixed=True, min_count=5):
from corpus.chatbot_corpus.build_chat_corpus import Chat_corpus, compute_build
after_fix = False
ws = None
try:
ws = pickle.load(open(path, 'rb'))
except:
if (auto_fix):
print("fixing...")
chat_corpus = Chat_corpus()
compute_build(chat_corpus=chat_corpus, fixed=fixed, min_count=min_count,
by_word=by_word, is_target=is_target)
after_fix = True
if (after_fix):
ws = pickle.load(open(path, 'rb'))
return ws
def word_corpus_no_by_word_input_load():
path = chatboot_config.get("word_corpus_no_by_word_input")
return chat_load_(path,is_target=False,by_word=False)
def word_corpus_no_by_word_target_load():
path = chatboot_config.get("word_corpus_no_by_word_target")
return chat_load_(path,is_target=True,by_word=False)
def word_corpus_by_word_input_load():
path = chatboot_config.get("word_corpus_by_word_input")
return chat_load_(path,is_target=False,by_word=True)
def word_corpus_by_word_target_load():
path = chatboot_config.get("word_corpus_by_word_target")
return chat_load_(path,is_target=True,by_word=True)
chatboot_config_load = {
"word_corpus_no_by_word_input_load": word_corpus_no_by_word_input_load(),
"word_corpus_no_by_word_target_load": word_corpus_no_by_word_target_load(),
"word_corpus_by_word_input_load": word_corpus_by_word_input_load(),
"word_corpus_by_word_target_load": word_corpus_by_word_target_load(),
}
这个的话,是咱们对话AI需求的装备文件。
数据集预备
ok,咱们开端预备数据集了。这儿留意咯,假如你想要练习出一个AI女友的话,这部分很要害哟~。首要数据是这个姿态的。
那么咱们要做的是啥呢,首要咱们这儿把榜首句话作为咱们的输入,也便是说咱们假定,榜首句话是你要说的话。第二句话是你希望AI输出的话,那么咱们把榜首句话作为input,第二句作为target。咱们希望的是,你输入一个input,AI能够输出相似与target的话来。
那么咱们先要做的便是对数据的切分。 这个便是咱们切分之后的成果: 咱们这儿的话还能够完结依照一个一个字来分和依照jieba进行分词的作用。也便是说假如你觉得依照jieba分词的作用不好,你能够试着直接依照字去分词。
分词
欧克,那么咱们现在要做的先是完结咱们的分词。 这个的话把代码放在这儿:
完结是这个:
"""
this model just for cutting words
"""
import jieba
import jieba.posseg as pseg
from tqdm import tqdm, trange
from config.config import jieba_config
import string
jieba.load_userdict(jieba_config.get("word_dict"))
jieba = jieba
pseg = pseg
string = string
with open(file=jieba_config.get("stop_dict"),encoding='utf-8') as f:
lines = tqdm(f.readlines(),desc="loading stop word")
StopWords = {}.fromkeys([line.rstrip() for line in lines ])
print("\033[0;32;40m all loading is finished!\033[0m")
class Cut(object):
def __init__(self,other_letters=None):
self.letters = string.ascii_letters
self.stopword = StopWords
def __stop_not_sign(self,result):
result_rel = []
for res in result:
if (res not in self.stopword):
result_rel.append(res)
return result_rel
def __stop_with_sign(self, result):
result_rel = []
for res in result:
if (res.word not in self.stopword):
result_rel.append((res.word,res.flag))
return result_rel
def cut(self,sentence,by_word=False,
use_stop_word=False,with_sg=False
):
"""
:param sentence:
:param by_word:
:param use_stop_word:
:param with_sg:
:return:
"""
if(by_word):
return self.cut_sentence_by_word(sentence)
else:
'''
without by word,so there will be cutting by jieba
'''
if (with_sg):
result = pseg.lcut(sentence)
if(use_stop_word):
result = self.__stop_with_sign(result)
else:
result = jieba.lcut(sentence)
if (use_stop_word):
result = self.__stop_not_sign(result)
return result
def cut_sentence_by_word(self,sentence):
"""
it can cut English sentences and Chinese
:param sentence:
:return:
"""
result = []
temp = ""
for word in sentence:
if word.lower() in self.letters:
temp+=word
else:
if(temp!=""):
result.append(temp)
temp = ""
else:
result.append(word.strip())
if(temp!=""):
result.append(temp.lower())
return result
区分
之后的话,咱们就能够区分了。这儿的话咱们要做的不仅仅是区分,咱们还需求构建一个办法,去能够把一个语句转换为一个序列。那么这个完结的话也是在这儿一同完结的。
这部分的代码其实也很简略,没啥好说的,咱们直接看到代码。
"""
for building corpus for chatboot running
This will be deployed in a white-hole, possibly in version 0.7
"""
import pickle
from tqdm import tqdm
from config import config
from utils.cut_word import Cut
class Chat_corpus(object):
def __init__(self):
self.Cut = Cut()
self.PAD = 'PAD'
self.UNKNOW = 'UNKNOW'
self.EOS = 'EOS'
self.SOS = 'SOS'
self.word2index={
self.PAD: config.chatboot_config.get("padding_idx"),
self.SOS: config.chatboot_config.get("sos_idx"),
self.EOS: config.chatboot_config.get("eos_idx"),
self.UNKNOW: config.chatboot_config.get("unk_idx"),
}
self.index2word = {}
self.count = {}
def fit(self,sentence_list):
"""
just for counting word
:param sentence_list:
:return:
"""
for word in sentence_list:
self.count[word] = self.count.get(word,0)+1
def build_vocab_chat(self,min_count=None,max_count=None,max_feature=None):
"""
build word dict,this need to save by pickle in computer memory
:return:
"""
temp = self.count.copy()
for key in temp:
cur_count = self.count.get(key,0)
if(min_count !=None):
if(cur_count<min_count):
del self.count[key]
if(max_count!=None):
if(cur_count>max_count):
del self.count[key]
if(max_feature!=None):
self.count = dict(sorted(self.count.items(),key= lambda x:x[1],
reverse=True
)[:max_feature]
)
for key in self.count:
self.word2index[key] = len(self.word2index)
self.index2word = {item[1]:item[0] for item in self.word2index.items()}
def transform(self,sentence,max_len,add_eos=False):
if(len(sentence)>max_len):
sentence = sentence[:max_len]
sentence_len = len(sentence)
if(add_eos):
sentence = sentence+[self.EOS]
if(sentence_len<max_len):
sentence = sentence +[self.PAD]*(max_len-sentence_len)
result = [self.word2index.get(i,self.word2index.get(self.UNKNOW)) for i in sentence]
return result
def inverse_transform(self,indices):
"""
index ---> sentence
:param indices:
:return:
"""
result = []
for i in indices:
if(i==self.word2index.get(self.EOS)):
break
result.append(self.index2word.get(i,self.UNKNOW))
return result
def __len__(self):
return len(self.word2index)
def __by_word(self,data_lines):
for line in data_lines:
for word in self.Cut.cut(line,by_word=True):
self.word2index[word] = self.word2index.get(word,0)+1
def __by_not_word(self,data_lines):
for line in data_lines:
for word in self.Cut.cut(line,by_word=False):
self.word2index[word] = self.word2index.get(word, 0) + 1
def division(self,by_word=False,use_stop_word=False):
"""
this funcation just for dividing input and target in xiaohuangji corpus
:return:
"""
count_input = 0
count_target = 0
temp_sentence = []
if(by_word):
middle_prx = ""
else:
middle_prx = "_no"
target_save = open(config.chatboot_config.get("target_path"+middle_prx+"_by_word"),'a',encoding='utf-8')
input_save = open(config.chatboot_config.get("input_path"+middle_prx+"_by_word"),'a',encoding='utf-8')
xiaohuangji_path = config.data_path.get("xiaohuangji")
with open(xiaohuangji_path,'r',encoding='utf-8') as file:
file_lines = tqdm(file.readlines(),desc="division xiaohuangji")
for line in file_lines:
line = line.strip()
if (line.startswith("E")):
continue
elif (line.startswith("M")):
line = line[1:].strip()
line = self.Cut.cut(line, by_word, use_stop_word)
temp_sentence.append(line)
if(len(temp_sentence)==2):
"""
Because the special symbol has a certain possibility,
it is used as the input of the user.
Therefore, retain that special kind of "symbolic dialogue" corpus
"""
if(len(line)==0):
temp_sentence = []
continue
input_save.write(" ".join(line)+'\n')
count_input+=1
target_save.write(" ".join(line)+'\n')
count_target+=1
temp_sentence=[]
input_save.close()
target_save.close()
assert count_target==count_input,'count_target need equal count_input'
print("\033[0;32;40m process is finished!\033[0m")
print("The input len is:",count_input,"\nThe target len is:",count_target)
def compute_build(chat_corpus,fixed=False,
by_word=False,min_count=5,
max_count=None,max_feature=None,
is_target=True,
):
"""
for computing fit function with input and target file
:param fixed: if True when error coming will try to fix by itself
:return:
"""
if (by_word):
middle_prx = ""
else:
middle_prx = "_no"
after_fixed = False
lines = []
try:
if(is_target):
lines = open(config.chatboot_config.get("target_path"+middle_prx+"_by_word"), 'r', encoding='utf-8').readlines()
else:
lines = open(config.chatboot_config.get("input_path"+middle_prx+"_by_word"), 'r', encoding='utf-8').readlines()
except Exception as e:
if(fixed):
chat_corpus.division(by_word=by_word)
after_fixed = True
else:
raise Exception("you need use Chat_corpus division function first! ")
if(after_fixed):
if (is_target):
lines = open(config.chatboot_config.get("target_path" + middle_prx + "_by_word"), 'r',
encoding='utf-8').readlines()
else:
lines = open(config.chatboot_config.get("input_path" + middle_prx + "_by_word"), 'r',
encoding='utf-8').readlines()
data_lines = tqdm(lines,desc="building")
for line in data_lines:
chat_corpus.fit(line.strip().split())
chat_corpus.build_vocab_chat(min_count,max_count,max_feature)
if(is_target):
pickle.dump(chat_corpus,open(config.chatboot_config.get("word_corpus"+middle_prx+"_by_word_target"),'wb'))
else:
pickle.dump(chat_corpus, open(config.chatboot_config.get("word_corpus" + middle_prx + "_by_word_input"), 'wb'))
if __name__ == '__main__':
chat_corpus = Chat_corpus()
compute_build(chat_corpus,fixed=True,min_count=5,by_word=False,is_target=True)
那么咱们这儿的话便是说,完结了这样的办法:
- 区分input 和 target,区分之后的作用是这个姿态的: 这儿的话有一个要点,那便是有必要确保对话是成对呈现的,也便是生成的文件input和target对应的行数是相同的,一同留意有没有空行。 –
- 构造映射词典,也便是达成这样的目标
- 保存
数据集加载
之后的话便是去构建咱们的数据调集了,那么在这儿的话需求留意的便是重写一个函数就好了。这个是咱们运用pytorch必不可少的作业。 那么完结的话便是这样的:
"""
dataSet about chat_boot
"""
from torch.utils.data import DataLoader,Dataset
from boot.chatboot.encoder import Encoder
from config import config
import torch
class Chat_dataset(Dataset):
r"""in there you will get this:
Prefix dict has been built successfully.
loading stop word: 100%|██████████| 1395/1395 [00:00<00:00, 1400443.77it/s]
all loading is finished!
tensor([[ 14, 6243, 925, ..., 515, 66, 1233],
[ 20, 34, 2173, ..., 710, 7, 9],
[12422, 20, 42, ..., 9, 14, 236],
...,
[ 1636, 1, 1, ..., 1, 1, 1],
[ 531, 1, 1, ..., 1, 1, 1],
[ 8045, 1, 1, ..., 1, 1, 1]])
tensor([[ 165, 19617, 118, ..., 1, 1, 1],
[ 249, 15, 12, ..., 1, 1, 1],
[ 153, 8, 153, ..., 1, 1, 1],
...,
[ 329, 58, 3, ..., 1, 1, 1],
[ 681, 0, 2625, ..., 1, 1, 1],
[ 5245, 3641, 15, ..., 1, 1, 1]])
tensor([20, 19, 16, 15, 13, 13, 12, 12, 11, 11, 11, 11, 11, 9, 9, 9, 9, 8,
8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1])
tensor([ 7, 8, 3, 6, 2, 5, 2, 8, 5, 4, 2, 3, 3, 17, 3, 3, 10, 3,
2, 71, 13, 1, 9, 10, 11, 10, 12, 3, 3, 8, 1, 10, 2, 11, 2, 9,
2, 3, 8, 2, 3, 3, 4, 3, 3, 6, 40, 3, 8, 1, 30, 2, 7, 6,
5, 74, 1, 9, 5, 5, 17, 4, 6, 5, 13, 2, 11, 3, 2, 6, 5, 2,
2, 5, 3, 10, 5, 14, 3, 6, 2, 3, 18, 6, 9, 3, 4, 6, 3, 1,
1, 7, 10, 6, 6, 3, 14, 2, 2, 7, 9, 6, 9, 3, 3, 9, 2, 3,
7, 1, 1, 3, 4, 6, 6, 7, 1, 4, 6, 2, 6, 3, 5, 3, 2, 2,
3, 6])
"""
def __init__(self,by_word=False):
if (by_word):
middle_prx = ""
else:
middle_prx = "_no"
self.target_lines = open(config.chatboot_config.get("target_path" + middle_prx + "_by_word"), 'r',
encoding='utf-8').readlines()
self.input_lines = open(config.chatboot_config.get("input_path" + middle_prx + "_by_word"), 'r',
encoding='utf-8').readlines()
assert len(self.target_lines)==len(self.input_lines),"len need equal"
def __getitem__(self, index):
input_data = self.input_lines[index].strip().split()
target_data = self.target_lines[index].strip().split()
if(len(input_data)==0):
raise Exception("the input_data's length is: 0")
input_length = len(input_data) if len(input_data)<config.chatboot_config.get("input_max_len") else config.chatboot_config.get("input_max_len")
target_lenth = len(target_data) if len(target_data)<config.chatboot_config.get("target_max_len")+1 else config.chatboot_config.get("target_max_len")+1
return input_data, target_data, input_length, target_lenth
def __len__(self):
return len(self.input_lines)
def collate_fn(batch):
if(config.chatboot_config.get("collate_fn_is_by_word")):
input_ws = config.chatboot_config_load.get("word_corpus_by_word_input_load")
target_ws = config.chatboot_config_load.get("word_corpus_by_word_target_load")
else:
input_ws = config.chatboot_config_load.get("word_corpus_no_by_word_input_load")
target_ws = config.chatboot_config_load.get("word_corpus_no_by_word_target_load")
batch = sorted(batch,key=lambda x:x[-2],reverse=True)
input_data, target_data, input_length, target_lenth = zip(*batch)
input_data = [input_ws.transform(i, max_len=config.chatboot_config.get("input_max_len")) for i in input_data]
target_data = [target_ws.transform(i, max_len=config.chatboot_config.get("target_max_len"),add_eos=True) for i in target_data]
input_data = torch.LongTensor(input_data)
target_data = torch.LongTensor(target_data)
input_length = torch.LongTensor(input_length)
target_lenth = torch.LongTensor(target_lenth)
return input_data, target_data, input_length, target_lenth
if __name__ == '__main__':
chat_dataset = Chat_dataset()
train_data_loader = DataLoader(chat_dataset, batch_size=config.chatboot_config.get("batch_size"),
shuffle=True,
collate_fn=collate_fn)
for idx,(input_data, target_data, input_length, target_lenth) in enumerate(train_data_loader):
print(input_data.max())
print(target_data)
print(input_length)
print(target_lenth)
break
那么这个时分的话,咱们的数据预备作业,做完这个之后,运行程序。没有问题的话,在你的文件目录下面将会发生这些文件。 没有框起来的没有,由于那个是练习完之后才有的。
模型建立
欧克,之后的话就来到了咱们的模型建立部分了。首要在这儿的话咱们运用到的是十分经典的Seq2Seq(低情商:low) 咱们先简略了解一下这个玩意吧,这个东西看不太懂没联系,由于没办法博文就这样,包含论文也是只能归纳,有基础很好了解,没有只能补一下。可是别着急,在我这儿,你并不需求知道太多,了解即可。我只告知你最实质的是啥,便是seq2seq它到底是啥,然后有哪些概念。之后大约的结构是啥。
根本概念
首要的话,这个玩意是这样的结构大约:
他有一个编码器和解码器。那么这个便是最重要的在这个网络傍边。 一同他们具体一点的联系是这样的:
编码器和解码器都是一个RNN。
OK,知道了这个咱们再来说说为啥是这样的结构。首要咱们为什么需求运用到RNN,由于咱们一个语句输入进去的是一个词向量。每一个词之间彼此相关。上下词之间存联系。因而咱们提出了RNN,在基础上咱们又提出了LSTM,GRU这种RNN网络。目的便是处理这种像这种彼此相关的内容,之后的话咱们把依照次序把每一个词输入进网络,一个接一个并且把上一个词的输出一同作为输入。也便是所谓的时刻步。因而咱们首要用户输入了一个语句得到一个词向量,那么咱们需求进行解析。因而咱们首要在输入的当地需求一个循环神经网络,之后咱们需求得到一个语句作为输出,相同的这个也是一个接一个的,由于咱们也需求一个词一个词前去生成,那么这个是一个反过来的进程。所以此刻咱们需求第二个网络,并且这个网络也是一个循环网络,并且由于需求反过来,因而在完结上咱们需求手写RNN的循环。在咱们这边是运用GRU来做的,因而手写GRU的一个循环。
所以你看到了咱们需求两个GRU,并且依照咱们刚刚说的,咱们把这个玩意一个叫做编码器,一个叫做解码器。
之后咱们了解了为啥要两个网络,那么相同的咱们怎么得到语句呢?咱们首要输入了一个语句,之后由解码器生成语句。这儿的话其实是这样的,假定咱们需求生成的语句最长是10个。咱们的练习集傍边,或许说数据傍边,有1W个词。这个很正常的,毕竟咱们练习的数据都是几十万几步的。那么这个时分的话,咱们其实要做的话便是直接猜测一个概率,也便是说咱们在解码器终究边参加一个全衔接层,之后经过softmax,这种函数,转化为一个概率。10个词的语句,1W个单词。那么咱们终究一句话的话就会得到10*1W的概率矩阵,每一个语句的方位上,也便是时刻步上,咱们都猜测概率。(当然你也能够学学YOLO直接把这个问题变成一个回归问题)。
所以假定你上面都没有听懂,那么你只需求记住,那便是,咱们输入一个语句,经过seq2seq傍边的编码器和解码器彼此作用。终究生成了一个语句,在每一个方位上,在数据傍边所有词呈现的概率,然后把概率最大的那个词作为当前方位上要生成的词,终究组成一句话就好了。那么这个进程直到到达你预定的要生成的语句长度外,呈现中止符号的时分也会中止。这个在咱们的装备文件傍边写了一个中止的标识。
Encoder建立
OK,咱们这边来建立一下编码器,这个比较简略。直接看就好了。
import torch.nn as nn
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
from config import config
class Encoder(nn.Module):
def __init__(self,by_word=False):
super(Encoder,self).__init__()
if(by_word):
self.input_ws = config.chatboot_config_load.get("word_corpus_by_word_input_load")
else:
self.input_ws = config.chatboot_config_load.get("word_corpus_no_by_word_input_load")
self.embedding = nn.Embedding(
num_embeddings=len(self.input_ws),
embedding_dim=config.chatboot_config.get("embedding_dim"),
padding_idx=config.chatboot_config.get("padding_idx")
)
self.gru = nn.GRU(input_size=config.chatboot_config.get("embedding_dim"),
dropout=config.chatboot_config.get("dropout"),
num_layers=config.chatboot_config.get("num_layers"),
hidden_size=config.chatboot_config.get("hidden_size"),
bidirectional=config.chatboot_config.get("bidirectional"),
batch_first=config.chatboot_config.get("batch_first")
)
def forward(self,input_data,input_length):
embeded = self.embedding(input_data)
embeded = pack_padded_sequence(embeded,input_length.cpu(),batch_first=True)
out,hidden = self.gru(embeded)
"""
in there return:
hidden: num_layers*2,batch_size,hidden_size
out: batch_size,sentence_len,hidden_size
"""
out,_ = pad_packed_sequence(out,
batch_first=config.chatboot_config.get("batch_first"),
padding_value=config.chatboot_config.get("padding_idx")
)
return out,hidden
Decoder
之后是咱们decoder的建立。那么在这块的话,咱们还简答地参加了一个留意力机制:(感兴趣的能够自己去看这篇论文,是2015年出来的:arxiv.org/pdf/1508.04…)这儿的话咱们就不介绍了)
一同的话,咱们还对这个猜测做了一个优化,刚刚是说咱们在每一个方位上,找的都是概率最大的一个词,然后作为这个方位的词,直到到达了咱们预定的长度,或许说这个方位概率最大的词是完毕标志。然后中止,那么在这儿的话就简单呈现一个问题,那便是每一步最优不一定代表全局最优,比方当前选了这个词,概率是0.3,之后下一步选一个词是0.2。而假如在上一步选择0.25的概率的词,下一步的一个词的概率有0.6,那么相对来说0.3和0.25距离或许不大,可是0.6和0.2距离是很大的。因而为了处理这个问题,有一个算法叫做beamsearch。这个玩意便是说都会走一遍,终究选出看起来作用还不错的序列作为输出。
Attention机制
首要来看到咱们的Attention
那么代码是这儿:
"""
The luong attention in there
"""
import torch.nn as nn
import torch.nn.functional as F
from config import config
import torch
class LuongAttention(nn.Module):
def __init__(self,method="general"):
super(LuongAttention,self).__init__()
assert method in ["dot","general","concat"],'method err just support "dot","general","concat"'
self.method = method
self.chatboot_encoder_hidden_size = config.chatboot_config.get("hidden_size")*2 if config.chatboot_config.get(
"bidirectional") else config.chatboot_config.get("hidden_size")
self.chatboot_decoder_hidden_size = config.chatboot_config.get("hidden_size")*2 if config.chatboot_config.get(
"bidirectional") else config.chatboot_config.get("hidden_size")
self.wa_general = nn.Linear(
# encoder
self.chatboot_encoder_hidden_size,
# decoder
config.chatboot_config.get("hidden_size"),
bias=False
)
self.wa_concat = nn.Linear(
self.chatboot_encoder_hidden_size+self.chatboot_decoder_hidden_size,
# decoder
self.chatboot_decoder_hidden_size,
bias=False
)
self.va = nn.Linear(
# decoder
config.chatboot_config.get("hidden_size"),
1,
)
def forward(self,hidden_state,encoder_outputs):
attention_weight = None
if(self.method=='dot'):
hidden_state = hidden_state[-1,:,:].permute(1,2,0)
attention_weight = encoder_outputs.bmm(hidden_state).squeeze(-1)
attention_weight = F.softmax(attention_weight)
elif (self.method=='general'):
encoder_outputs = self.wa_general(encoder_outputs)
hidden_state = hidden_state[-1:,:,:].permute(1,2,0)
attention_weight = encoder_outputs.bmm(hidden_state).squeeze(-1)
attention_weight = F.softmax(attention_weight,dim=-1)
elif self.method == 'concat':
hidden_state = hidden_state[-1,:,:].squeeze(0)
hidden_state = hidden_state.repeat(1,encoder_outputs.size(1),1)
concated = torch.cat([hidden_state,encoder_outputs],dim=-1)
batch_size = encoder_outputs.size(0)
encoder_seq_len = encoder_outputs.size(1)
attention_weight = self.va(F.tanh(self.wa_concat(concated.view((batch_size*encoder_seq_len,-1))))).sequeeze(-1)
attention_weight = F.softmax(attention_weight.view(batch_size,encoder_seq_len))
assert attention_weight!=None,"error attention_weight can't be None"
return attention_weight
decoder与beamsearch
这个东西的话和咱们的decoder是在一同的,一同咱们的留意力机制其实也是在这儿的。 那么完结的话是这样的:
import torch.nn as nn
import torch.nn.functional as F
from config import config
import torch
import random
from utils.drive import getDrive
from boot.chatboot.attention import LuongAttention
import heapq
"""
in there we import luong Attention
"""
class Beam(object):
def __init__(self):
self.heap = list()
self.beam_width = config.chatboot_config.get("beam_width")
def add(self, probility, complete, seq, decoder_input, decoder_hidden):
"""
:param probility:
:param complete: is or not eos
:param seq: all token list
:param decoder_input:
:param decoder_hidden:
:return:
"""
heapq.heappush(self.heap, [probility, complete, seq, decoder_input, decoder_hidden])
if (len(self.heap) > self.beam_width):
heapq.heappop(self.heap)
def __iter__(self):
return iter(self.heap)
class Decoder(nn.Module):
def __init__(self,by_word=False):
super(Decoder,self).__init__()
self.drive = getDrive()
"""
attention init
"""
if(config.chatboot_config.get("use_attention")):
self.chatboot_encoder_hidden_size = config.chatboot_config.get("hidden_size")*2 if config.chatboot_config.get(
"bidirectional") else config.chatboot_config.get("hidden_size")
self.chatboot_decoder_hidden_size = config.chatboot_config.get("hidden_size")*2 if config.chatboot_config.get(
"bidirectional") else config.chatboot_config.get("hidden_size")
self.atte = LuongAttention()
self.wa_concat = nn.Linear(
self.chatboot_encoder_hidden_size+self.chatboot_decoder_hidden_size,
# decoder
self.chatboot_decoder_hidden_size,
bias=False
)
if(by_word):
self.target_ws = config.chatboot_config_load.get("word_corpus_by_word_target_load")
else:
self.target_ws = config.chatboot_config_load.get("word_corpus_no_by_word_target_load")
self.embedding = nn.Embedding(
num_embeddings=len(self.target_ws),
embedding_dim=config.chatboot_config.get("embedding_dim"),
padding_idx=config.chatboot_config.get("padding_idx")
)
self.gru = nn.GRU(input_size=config.chatboot_config.get("embedding_dim"),
dropout=config.chatboot_config.get("dropout"),
num_layers=config.chatboot_config.get("num_layers"),
hidden_size=config.chatboot_config.get("hidden_size"),
bidirectional=config.chatboot_config.get("bidirectional"),
batch_first=config.chatboot_config.get("batch_first")
)
self.fc = nn.Linear(config.chatboot_config.get("hidden_size")*
config.chatboot_config.get("num_layers"),
len(self.target_ws)
)
def forward(self,target_data,encoder_hidden,encoder_outputs):
"""
:param target_data:
:param encoder_hidden:
The hardest thing to do here is to pay attention to the dimensional
changes in input and publication.
:return:
"""
decoder_hidden = encoder_hidden
batch_size = target_data.size(0)
"""
sos input in decoder for first time step
"""
decoder_input = torch.LongTensor(torch.ones([batch_size,1],dtype=torch.int64
))*config.chatboot_config.get("sos_idx")
decoder_input = decoder_input.to(self.drive)
decoder_outputs = torch.zeros([batch_size,config.chatboot_config.get("target_max_len")+1,
len(self.target_ws)
]).to(self.drive)
if (random.random() < config.chatboot_config.get("teacher_forcing_ratio")):
for time in range(config.chatboot_config.get("target_max_len") + 1):
decoder_output_t, decoder_hidden = self.forward_step(decoder_input, decoder_hidden,encoder_outputs)
decoder_outputs[:, time, :] = decoder_output_t
decoder_input = target_data[:,time].unsqueeze(-1)
else:
for time in range(config.chatboot_config.get("target_max_len")+1):
decoder_output_t,decoder_hidden = self.forward_step(decoder_input,decoder_hidden,encoder_outputs)
decoder_outputs[:,time,:] = decoder_output_t
value,index = torch.topk(decoder_output_t,1)
decoder_input = index
return decoder_outputs,decoder_hidden
def forward_step(self,decoder_input, decoder_hidden,encoder_outputs):
decoder_input_embeded = self.embedding(decoder_input)
out,decoder_hidden = self.gru(decoder_input_embeded,decoder_hidden)
"""
there we add attention way
"""
"""*******************************************************"""
if (config.chatboot_config.get("use_attention")):
attention_weight = self.atte(decoder_hidden,encoder_outputs).unsqueeze(1)
context_vector = attention_weight.bmm(encoder_outputs)
concated = torch.cat([out,context_vector],dim=-1).squeeze(1)
out = torch.tan(self.wa_concat(concated))
"""*******************************************************"""
# out = out.squeeze(1)
else:
out = out.squeeze(1)
out = self.fc(out)
output = F.log_softmax(out,dim=-1)
return output,decoder_hidden
def evaluate(self,encoder_hidden,encoder_outputs):
decoder_hidden = encoder_hidden
batch_size = encoder_hidden.size(1)
decoder_input = torch.LongTensor(torch.ones([batch_size,1],dtype=torch.int64
))*config.chatboot_config.get("sos_idx")
decoder_input = decoder_input.to(self.drive)
indices = []
for i in range(config.chatboot_config.get("out_seq_len")):
decoder_output_t,decoder_hidden = self.forward_step(decoder_input,decoder_hidden,encoder_outputs)
value,index = torch.topk(decoder_output_t,1)
decoder_input = index
indices.append(index.squeeze(-1).cpu().detach().numpy())
return indices
def evaluate_beamsearch(self,encoder_hidden,encoder_outputs):
batch_size = encoder_hidden.size(1)
decoder_input = torch.LongTensor([[config.chatboot_config.get("sos_idx")]*batch_size]).to(self.drive)
decoder_hidden = encoder_hidden
prev_beam = Beam()
prev_beam.add(1,False,[decoder_input],decoder_input,decoder_hidden)
while True:
cur_beam = Beam()
for _probility,_complete,_seq,_decoder_input,_decoder_hidden in prev_beam:
if(_complete==True):
cur_beam.add(_probility,_complete,_seq,_decoder_input,_decoder_hidden)
else:
decoder_output_t,decoder_hidden = self.forward_step(_decoder_input,_decoder_hidden,encoder_outputs)
value,index = torch.topk(decoder_output_t,config.chatboot_config.get("beam_width"))
for m,n in zip(value[0],index[0]):
decoder_input = torch.LongTensor([[n]]).to(self.drive)
seq = _seq+[n]
probility = _probility * m
if(n.item()==config.chatboot_config.get("eos_idx")):
complete = True
else:
complete = False
cur_beam.add(probility,complete,seq,decoder_input,decoder_hidden)
best_prob,best_complete,best_seq,_,_ = max(cur_beam)
if(best_complete==True or len(best_seq)-1 == config.chatboot_config.get("out_seq_len")):
return self.__prepar_seq(best_seq)
else:
prev_beam = cur_beam
def __prepar_seq(self,best_seq):
if(best_seq[0].item()==config.chatboot_config.get("sos_idx")):
best_seq = best_seq[1:]
if(best_seq[-1].item()==config.chatboot_config.get("eos_idx")):
best_seq = best_seq[:-1]
best_seq = [i.item() for i in best_seq]
return best_seq
这儿边最难的其实仍是关于它里边数据维度的一个改变,原理其实仍是比较简略的。
加载驱动
那么在这儿的话还有一个细节便是回到工具包:
这儿的还有这个玩意。
import torch
from config import config
def getDrive():
if (torch.cuda.is_available()):
if (not config.chatboot_config.get("drive") == 'cpu'):
div = "cuda:" + config.chatboot_config.get("drive")
drive = torch.device(div)
else:
drive = torch.device("cpu")
else:
drive = torch.device("cpu")
return drive
Seq2Seq建立
现在的话咱们的几个重要部件都完结了,那么咱们现在需求拼装一下了。
from torch import nn
from boot.chatboot.decoder import Decoder
from boot.chatboot.encoder import Encoder
from utils.drive import getDrive
from config import config
class Seq2Seq(nn.Module):
def __init__(self):
super(Seq2Seq,self).__init__()
self.drive = getDrive()
self.encoder = Encoder().to(self.drive)
self.decoder = Decoder().to(self.drive)
def forward(self,input_data,target_data,input_length,target_length):
encoder_outputs,encoder_hidden = self.encoder(input_data,input_length)
decoder_outputs,decoder_hidden = self.decoder(target_data,encoder_hidden,encoder_outputs)
return decoder_outputs,decoder_hidden
def evaluate(self,input_data,input_length):
encoder_outputs,encoder_hidden = self.encoder(input_data,input_length)
if(config.chatboot_config.get("beam_search")):
indices = self.decoder.evaluate(encoder_hidden,encoder_outputs)
else:
indices = self.decoder.evaluate_beamsearch(encoder_hidden,encoder_outputs)
return indices
练习
那么到了终究便是咱们的练习了 那么这个时分我需求说的便是咱们的这个玩意有点相似于分类,可是和分类的差异是,并不是在练习集的时分丢失越小越好,咱们在分类的时分是丢失越小那么就越准,可是在这儿太准了就简单出事,就比方有这样的对话,你说:“你好”,然后在咱们的答复是:“你好呀””。这个时分你适当于分类,网络生成了“你好呀”这句话是没问题,可是它生成了:“你也好呀”,或许是:“你吃了吗”。这种对话也是没问题的,可是单纯作为分类的话,那么假如生成的是这两句话中的其间一个的话,那么从分类的成果上来说,他是匹配语句傍边每一个词的id。那么丢失是适当丑陋的,可是实际对话作用或许又是不错的。因而这也是比较难验证的。所以尽管他也算是有监督的,可是和图画这种不相同,他不是彻底对应的。也便是没有标准答案,这个也是问题,当然处理也是能够的那便是数据集,多个答案,可是这个难度比较大,咱们这儿做也不现实。所以的话在这块也是差异于分类咱们还会搞一个验证集去判别对了几个,咱们这不好判别,由于语言它不是问答,并且问答的话是做匹配。
这部分的完结比较简略
from boot.chatboot.chat_dataset import Chat_dataset,collate_fn
from boot.chatboot.seq2seq import Seq2Seq
from torch.optim import Adam
from torch.utils.data import DataLoader,Dataset
import torch.nn.functional as F
from config import config
from tqdm import tqdm
import torch.nn as nn
import torch
from utils.drive import getDrive
class Train_model(object):
def __init__(self,by_word=False):
if(config.chatboot_config.get("use_attention")):
print("\033[0;32;40m using attention by {} method !\033[0m".format(
config.chatboot_config.get("attention_method")
))
self.drive = getDrive()
self.seq2seq = Seq2Seq()
self.seq2seq = self.seq2seq.to(self.drive)
self.optimizer = Adam(self.seq2seq.parameters(),lr=0.001)
self.train_data_loader = DataLoader(Chat_dataset(),
batch_size=config.chatboot_config.get("batch_size"),
shuffle=True,
num_workers=config.chatboot_config.get("num_workers"),
collate_fn=collate_fn)
if(by_word):
self.save_seq2seq = config.chatboot_config.get("seq2seq_model_by_word")
self.save_optimizer = config.chatboot_config.get("optimizer_model_by_word")
else:
self.save_seq2seq = config.chatboot_config.get("seq2seq_model_no_by_word")
self.save_optimizer = config.chatboot_config.get("optimizer_model_no_by_word")
def train(self,e):
self.drive = getDrive()
bar = tqdm(enumerate(self.train_data_loader),
total=len(self.train_data_loader),desc="training",
colour='green'
)
e_loss = 0
for idx, (input_data, target_data, input_length, target_length) in bar:
input_data = input_data.to(self.drive)
target_data = target_data.to(self.drive)
input_length = input_length.to(self.drive)
target_length = target_length.to(self.drive)
self.optimizer.zero_grad()
decoder_outputs,decoder_hidden = self.seq2seq(input_data,target_data,
input_length,target_length
)
decoder_outputs = decoder_outputs.reshape(decoder_outputs.size(0)*decoder_outputs.size(1),-1)
target_data = target_data.view(-1)
loss = F.nll_loss(decoder_outputs,target_data,
ignore_index=config.chatboot_config.get("padding_idx")
)
loss.backward()
nn.utils.clip_grad_norm_(self.seq2seq.parameters(),max_norm=config.chatboot_config.get("max_norm"))
self.optimizer.step()
e_loss+=loss.item()
bar.set_description("drive:{} \t epoch:{} \t idx:{} \t current_batch_loss:{:.2f}".format(self.drive,e,idx,loss.item()))
print("\n","\033[0;32;40m drive:{} \t epoch:{} \t current_epoch_loss:{:.2f}\033[0m".format(self.drive, e, e_loss))
if(e%2==0):
torch.save(self.seq2seq.state_dict(),self.save_seq2seq)
torch.save(self.optimizer.state_dict(),self.save_optimizer)
if __name__ == '__main__':
train_model = Train_model()
for e in range(1, 5):
train_model.train(e)
当咱们练习完结之后,咱们将得到权重文件。咱们这儿建立的是一个两个双向的2层的GRU加上全衔接。得到的权重模型大约是70MB。
那么履行完毕之后的话你讲得到这两个文件: 相同的,咱们的练习能够根据分词(准确的说是分字),也能够根据那个jieba分词的成果来,这个的话有个要害参数叫做by_word这个改为TRUE便是依照分字来一下了,然后你看看作用就好了。
猜测
先说一下,咱们的装备是GTX1650 4GB,跑一次练习需求12分钟。也便是说练习10次2个小时没了。所以我这儿演示的作用不是很好,没办练习的问题,当然还有参数的调优之类的,这个的话需求各位自己拿到项目之后去练习了,并且相关数据文件比较大,所以都不会上传,各位下载好最初给的资源文件后,放到指定方位,先点击练习,他自己会生成许多文件,之后完结练习。这个大家应该是看到了的。
from boot.chatboot.chat_dataset import Chat_dataset,collate_fn
from boot.chatboot.seq2seq import Seq2Seq
from config import config
from utils.drive import getDrive
from utils.cut_word import Cut
import torch
import numpy as np
class Eval_model(object):
def __init__(self,by_word=False):
self.by_word = by_word
self.drive = getDrive()
self.seq2seq = Seq2Seq()
self.seq2seq = self.seq2seq.to(self.drive)
self.cut = Cut()
if(by_word):
self.seq2seq.load_state_dict(torch.load(config.chatboot_config.get("seq2seq_model_by_word")))
self.input_ws = config.chatboot_config_load.get("word_corpus_by_word_input_load")
self.target_ws = config.chatboot_config_load.get("word_corpus_by_word_target_load")
else:
self.seq2seq.load_state_dict(torch.load(config.chatboot_config.get("seq2seq_model_no_by_word")))
self.input_ws = config.chatboot_config_load.get("word_corpus_no_by_word_target_load")
self.target_ws = config.chatboot_config_load.get("word_corpus_no_by_word_target_load")
def while_talk(self):
while True:
input_data = input("please input:")
input_data = self.cut.cut(input_data,by_word=self.by_word)
if len(input_data) < config.chatboot_config.get( "input_max_len"):
input_length = len(input_data)
else:
input_length = config.chatboot_config.get("input_max_len")
input_data = [self.input_ws.transform(input_data, max_len=config.chatboot_config.get("input_max_len"))]
input_data = torch.LongTensor(input_data).to(self.drive)
input_length = torch.LongTensor([input_length]).to(self.drive)
"""
index-->Plural form
"""
indices = np.array(self.seq2seq.evaluate(input_data,input_length)).flatten()
outputs = self.target_ws.inverse_transform(indices)
print("xiaojiejie:","".join(outputs))
if __name__ == '__main__':
eval_model = Eval_model()
eval_model.while_talk()
那么此刻的话,拿到这个,或许封装一下就OK了。 作用大约是这个姿态的,这个自己慢慢练习一波,调调参数啥的,或许再优化一下数据集。
总结
那么这个的话便是咱们聊天AI的建立了,那么假如你想要练习一个小姐姐AI,记住,你的数据集里边,那个target得是小姐姐的言语,假如不是我不确保。当然这个东西现在存在的问题仍是挺多的,假如要我选,我选择做好的,由于练习调优的话仍是需求时刻磨合的。可是作为一个baseline日后不断优化是不错的选择。一同自己着手锦衣玉食,你彻底能够拿你女朋友和你的聊天记录处理一下作为语料自己玩玩嘛。OK,这个便是咱们扩写之后的比较完好的一个部分了。建议能够深化了解的去看看本文说到的东西然后在看看这个代码。一同的话其实你发现了,这玩意能够做其他的一个生成,例如我把input变成古诗题目,target作为诗句,那么是不是能够完结一个古诗生成,当然得做调整。但总体上他是输入一个序列得到一个序列的结构,比方翻译之类的,这个结构的话也是比较适应的,之后便是怎么优化,比方transform其实便是这个结构,加了许多留意力机制。一同的话,关于这种序列尤其是这样很多二维矩阵的运算,咱们是不是还能够参加CNN去做,都是一个优化计划,也的确有这样的大哥在干。
OK,到这儿恭喜你看到这儿,完毕了,太酷了:终究一句话,好好学习,天天向上!!!来个小姐姐送我一张RTX4080 评论区踹我(手动狗头)