数据处理,读数据到内存
# 数据文件路径
data_path = './data/eng-fra-v2.txt'
# 工具函数
def normalizeString(s):
"""字符串规范化函数, 参数s代表传入的字符串"""
s = s.lower().strip()
# 在.!?前加一个空格 这里的\1表示第一个分组 正则中的\num
s = re.sub(r"([.!?])", r" \1", s)
# s = re.sub(r"([.!?])", r" ", s)
# 使用正则表达式将字符串中 不是 大小写字母和正常标点的都替换成空格
s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
return s
# my_getdata() 清洗文本构建字典思路分析
# 1 按行读文件 open().read().strip().split(\n) my_lines
# 2 按行清洗文本 构建语言对 my_pairs[] tmppair[] for line in my_lines for s in line.split('\t')
# 2-1格式 [['英文', '法文'], ['英文', '法文'], ['英文', '法文'], ['英文', '法文']....]
# 2-2调用清洗文本工具函数normalizeString(s)
# 3 遍历语言对 构建英语单词字典 法语单词字典 my_pairs->pair->pair[0].splt(' ') pair[1].split(' ')->word
# 3-1 english_word2index english_word_n french_word2index french_word_n
# 其中 english_word2index = {"SOS":0, "EOS":1} english_word_n=2
# 3-2 english_index2word french_index2word
# 4 返回数据的7个结果
# english_word2index, english_index2word, english_word_n,
# french_word2index, french_index2word, french_word_n, my_pairs
def my_getdata():
# 1 读数据
my_lines = open('./data/eng-fra-v2.txt', encoding='utf-8').read().strip().split('\n')
print('len(my_lines)-->', len(my_lines))
# 2 按行清洗文本 构建语言对
tmppairs = [];
my_pairs = []
for line in my_lines:
for s in line.split('\t'):
tmppairs.append(normalizeString(s))
my_pairs.append(tmppairs)
tmppairs = []
my_pairs = [[normalizeString(s) for s in line.split('\t')] for line in my_lines]
# print('my_pairs[0:3]->', my_pairs[0:3])
# print('第8000样本的英文-->', my_pairs[8000][0])
# print('第8000样本的法文-->', my_pairs[8000][1])
# 3 遍历语言对 构建英语单词字典 法语单词字典 my_pairs->pair->pair[0].splt(' ') pair[1].split(' ')->word
# 3-1 english_word2index english_word_n french_word2index french_word_n
# 其中 english_word2index = {"SOS":0, "EOS":1} english_word_n=2
# 3-2 english_index2word french_index2word
english_word2index = {"SOS": 0, "EOS": 1}
english_word_n = 2
french_word2index = {"SOS": 0, "EOS": 1}
french_word_n = 2
for pair in my_pairs:
for word in pair[0].split():
if word not in english_word2index:
english_word2index[word] = english_word_n
english_word_n += 1
for word in pair[1].split():
if word not in french_word2index:
french_word2index[word] = french_word_n
french_word_n += 1
# print('english_word2index-->', len(english_word2index))
# print('french_word2index-->', len(french_word2index))
english_index2word = {v: k for k, v in english_word2index.items()}
french_index2word = {v: k for k, v in french_word2index.items()}
# print('english_index2word-->', len(english_index2word))
# print('french_index2word-->', len(french_index2word))
# 4 返回数据的7个结果
return english_word2index, english_index2word, english_word_n, \
french_word2index, french_index2word, french_word_n, my_pairs
# 全局函数 获取英语单词字典 法语单词字典 语言对列表my_pairs
english_word2index, english_index2word, english_word_n, \
french_word2index, french_index2word, french_word_n, my_pairs = my_getdata()
构建dataloader流程
# 原始数据 -> 数据源MyPairsDataset --> 数据迭代器DataLoader
# 构造数据源 MyPairsDataset,把语料xy 文本数值化 再转成tensor_x tensor_y
# 1 __init__(self, my_pairs)函数 设置self.my_pairs 条目数self.sample_len
# 2 __len__(self)函数 获取样本条数
# 3 __getitem__(self, index)函数 获取第几条样本数据
# 按索引 获取数据样本 x y
# 样本x 文本数值化 word2id x.append(EOS_token)
# 样本y 文本数值化 word2id y.append(EOS_token)
# 返回tensor_x, tensor_y
class MyPairsDataset(Dataset):
def __init__(self, my_pairs):
# 预料对 英文对于的法文
self.my_pairs = my_pairs
self.sample_len = len(my_pairs)
def __len__(self):
return self.sample_len
def __getitem__(self, item):
# 按索引 获取数据样本 x y
x = self.my_pairs[item][0]
y = self.my_pairs[item][1]
# 样本x 文本数值化 word2id x.append(EOS_token)
x = [english_word2index[word] for word in x.split(' ')]
x.append(EOS_token)
tensor_x = torch.tensor(x, dtype=torch.long, device=device)
# 样本y 文本数值化 word2id y.append(EOS_token)
y = [french_word2index[word] for word in y.split(' ')]
y.append(EOS_token)
tensor_y = torch.tensor(y, dtype=torch.long, device=device)
# 返回tensor_x, tensor_y
return tensor_x, tensor_y
模型构建—构建编码器
# EncoderRNN类 实现思路分析:
# 1 init函数 定义2个层 super() self.embedding self.gru (batch_first=True)
# def __init__(self, input_size, hidden_size): # 2803 256
# 2 forward(input, hidden)函数,返回output, hidden
# 数据经过词嵌入层 数据形状 [1,6] --> [1,6,256]
# 数据经过gru层 形状变化 gru([1,6,256],[1,1,256]) --> [1,6,256] [1,1,256]
# 3 初始化隐藏层输入数据 inithidden()
# 形状 torch.zeros(1, 1, self.hidden_size, device=device)
# 构建基于GRU的编码器
class EncoderRNN(nn.Module):
def __init__(self, input_size, hidden_size):
super(EncoderRNN, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
# 实例化词嵌入层对象 Embedding 单词个数 每个单词的特征数
self.embedding = nn.Embedding(num_embeddings=input_size, embedding_dim=hidden_size)
# 实例化gru对象
self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
def forward(self, input, hidden):
# 数据经过词嵌入层 数据形状 [1,6] --> [1,6,256]
output = self.embedding(input)
# 数据经过gru层 形状变化 gru([1,6,256],[1,1,256]) --> [1,6,256] [1,1,256]
output, hidden = self.gru(output, hidden)
return output, hidden
def inithidden(self):
return torch.zeros(1, 1, self.hidden_size)
模型构建 — 解码器
# 构建基于GRU的解码器
# DecoderRNN 类 实现思路分析:
# 解码器的作用:提取事物特征 进行分类(所以比编码器多了 线性层 和 softmax层)
# 1 init函数 定义四个层 self.embedding self.gru self.out self.softmax=nn.LogSoftmax(dim=-1)
# def __init__(self, output_size, hidden_size): # 4345 256
# 2 forward(input, hidden)函数,返回output, hidden
# 数据经过词嵌入层 数据形状 [1,1] --> [1,1,256]
# 数据经过relu()层 input = F.relu(input)
# 数据经过gru层 形状变化 gru([1,1,256],[1,1,256]) --> [1,1,256] [1,1,256]
# 数据结果out层 形状变化 [1,1,256]->[1,256]-->[1,4345]
# 返回 解码器分类output[1,4345],最后隐层张量hidden[1,1,256]
# 3 初始化隐藏层输入数据 inithidden()
# 形状 torch.zeros(1, 1, self.hidden_size, device=device)
class DecoderRNN(nn.Module):
def __init__(self, output_size, hidden_size):
super(DecoderRNN, self).__init__()
self.output_size = output_size # 4345
self.hidden_size = hidden_size # 256
# 实例化 法文的 词向量层
self.embedding = nn.Embedding(output_size, hidden_size)
# 实例化gru对象
self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
# 实例化全连接层
self.out = nn.Linear(hidden_size, output_size)
# 实例化softmax层
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input, hidden):
# 数据经过词嵌入层 数据形状 [1,1] --> [1,1,256]
input = self.embedding(input)
# 数据经过relu()层 input = F.relu(input)
input = F.relu(input)
# 数据经过gru层 形状变化 gru([1,1,256],[1,1,256]) --> [1,1,256] [1,1,256]
output, hidden = self.gru(input, hidden)
# 数据结果out层 形状变化 [1,1,256]->[1,256]-->[1,4345]
output = self.out(output[0])
output = self.softmax(output)
# 返回 解码器分类output[1,4345],最后隐层张量hidden[1,1,256]
return output, hidden
def inithidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)
构建Attention解码器
# 相对传统RNN解码 AttnDecoderRNN类多了注意力机制,需要构建QKV
# 1 在init函数中 (self, output_size, hidden_size, dropout_p=0.1, max_length=MAX_LENGTH)
# 增加层 self.attn self.attn_combine self.dropout=nn.Dropout(self.dropout_p)
# 2 增加函数 attentionQKV(self, Q, K, V)
# 3 函数forward(self, input, hidden, encoder_outputs)
# encoder_outputs 每个时间步解码准备qkv 调用attentionQKV
# 函数返回值 output, hidden, attn_weights
# 4 调用需要准备中间语义张量C encode_output_c
class AttnDecoderRNN(nn.Module):
def __init__(self, output_size, hidden_size, dropout_p=0.1, max_length=MAX_LENGTH):
super(AttnDecoderRNN, self).__init__()
self.output_size = output_size # 4345
self.hidden_size = hidden_size # 256
self.dropout_p = dropout_p # add
self.max_length = max_length # add
# 实例化 法文的 词向量层
self.embedding = nn.Embedding(output_size, hidden_size)
# 实例化gru对象
self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
# 实例化全连接层
self.out = nn.Linear(hidden_size, output_size)
# 实例化softmax层
self.softmax = nn.LogSoftmax(dim=-1)
self.attn = nn.Linear(self.hidden_size + self.hidden_size, self.max_length) # add
# 线性层2:注意力结果表示 按照指定维度进行输出层 nn.Linear(32+32, 32)
self.attn_combine = nn.Linear(self.hidden_size + self.hidden_size, self.hidden_size) #add
self.dropout = nn.Dropout(self.dropout_p) # add
def attentionQKV(self, Q, K, V): ### addd
# 1 求查询张量q的注意力权重分布, attn_weights[1,32]
# tmp1 = torch.cat((Q[0], K[0]), dim=-1 ) # [1,1,32],[1,1,32] -->[1,32],[1,32]-->[1,64]
# tmp2 = self.attn(tmp1) # [1,64] ---> [1,10] 10个单词
# tmp3 = F.softmax(tmp2, dim=-1) # [1,10]--->[1,10]
# print('tmp3-->', tmp3)
attn_weights = F.softmax(self.attn(torch.cat((Q[0], K[0]), dim=-1)), dim=-1)
# 2 求查询张量q的注意力结果表示 bmm运算, attn_applied[1,1,32]
# [1,10]-->[1,1,10] [1,10,32] ===> [1,1,32]
attn_applied = torch.bmm(attn_weights.unsqueeze(0), V)
# 3-1 q 与 attn_applied 融合
# [1,32],[1,32] ===> [1,64]
output = torch.cat((Q[0], attn_applied[0]), dim=-1)
#3-2 再按照指定维度输出 output[1,1,32]
# [1,64]-->[1,32]
output = self.attn_combine(output).unsqueeze(0)
# 返回注意力结果表示output:[1,1,32], 注意力权重分布attn_weights:[1,10]
return output, attn_weights
def forward(self, input, hidden, encoder_outputs):
# 数据经过词嵌入层 数据形状 [1,1] --> [1,1,256]
input = self.embedding(input)
input = self.dropout(input)
input, attn_weights = self.attentionQKV(input, hidden, encoder_outputs.unsqueeze(0))
# 数据经过relu()层 input = F.relu(input)
input = F.relu(input)
# 数据经过gru层 形状变化 gru([1,1,256],[1,1,256]) --> [1,1,256] [1,1,256]
output, hidden = self.gru(input, hidden)
# 数据结果out层 形状变化 [1,1,256]->[1,256]-->[1,4345]
output = self.out(output[0])
output = self.softmax(output)
# 返回 解码器分类output[1,4345],最后隐层张量hidden[1,1,256]
return output, hidden, attn_weights
def inithidden(self):
return torch.zeros(1, 1, self.hidden_size, device=device)
模型训练
# 内部迭代训练函数Train_Iters
# 1 编码 encode_output, encode_hidden = my_encoderrnn(x, encode_hidden)
# 数据形状 eg [1,6],[1,1,256] --> [1,6,256],[1,1,256]
# 2 解码参数准备和解码
# 解码参数1 固定长度C encoder_outputs_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
# 解码参数2 decode_hidden # 解码参数3 input_y = torch.tensor([[SOS_token]], device=device)
# 数据形状 [1,1],[1,1,256],[10,256] ---> [1,4345],[1,1,256],[1,10]
# output_y, decode_hidden, attn_weight = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
# 计算损失 target_y = y[0][idx].view(1)
# 每个时间步处理 for idx in range(y_len): 处理三者之间关系input_y output_y target_y
# 3 训练策略 use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
# teacher_forcing 把样本真实值y作为下一次输入 input_y = y[0][idx].view(1, -1)
# not teacher_forcing 把预测值y作为下一次输入
# topv,topi = output_y.topk(1) # if topi.squeeze().item() == EOS_token: break input_y = topi.detach()
# 4 其他 # 计算损失 # 梯度清零 # 反向传播 # 梯度更新 # 返回 损失列表myloss.item()/y_len
def Train_Iters(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss):
# 1 编码 encode_output, encode_hidden = my_encoderrnn(x, encode_hidden)
# 数据形状 eg [1,6],[1,1,256] --> [1,6,256],[1,1,256]
encode_hihden = my_encoderrnn.inithidden()
encode_output, encode_hihden = my_encoderrnn(x, encode_hihden)
# 2 解码参数准备和解码
# 解码参数1 固定长度C encoder_outputs_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
encode_output_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
for idx in range(encode_output.shape[1]):
encode_output_c[idx] = encode_output[0, idx]
# 解码参数2 decode_hidden
decode_hidden = encode_hihden
# 解码参数3
input_y = torch.tensor([[SOS_token]], device=device)
myloss = 0.0
y_len = y.shape[1]
for idx in range(y_len):
# 数据形状 [1,1],[1,1,256],[10,256] ---> [1,4345],[1,1,256],[1,10]
output_y, decode_hidden, attn_weight = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
# 计算真实值
target_y = y[0][idx].view(1) # [1,]
myloss += mycrossentropyloss(output_y, target_y) # output_y预测值[1,4345]和target_y真实值[1,] 做分类损失时, 要查一个维度
# 把样本真实值y作为下一次输入
input_y = y[0][idx].view(1, -1)
# 计算损失 target_y = y[0][idx].view(1)
# 每个时间步处理 for idx in range(y_len): 处理三者之间关系input_y output_y target_y
# 梯度清零
myadam_encode.zero_grad();
myadam_decode.zero_grad()
# 反向传播 计算梯度
myloss.backward()
# 梯度更新
myadam_encode.step();
myadam_decode.step()
return myloss.item() / y_len
# 3 训练策略 use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
# teacher_forcing 把样本真实值y作为下一次输入 input_y = y[0][idx].view(1, -1)
# not teacher_forcing 把预测值y作为下一次输入
# topv,topi = output_y.topk(1) # if topi.squeeze().item() == EOS_token: break input_y = topi.detach()
pass
# Train_seq2seq() 思路分析
# 实例化 mypairsdataset对象 实例化 mydataloader
# 实例化编码器 my_encoderrnn 实例化解码器 my_attndecoderrnn
# 实例化编码器优化器 myadam_encode 实例化解码器优化器 myadam_decode
# 实例化损失函数 mycrossentropyloss = nn.NLLLoss()
# 定义模型训练的参数
# epoches mylr=1e4 teacher_forcing_ratio print_interval_num plot_interval_num (全局)
# plot_loss_list = [] (返回) print_loss_total plot_loss_total starttime (每轮内部)
# 外层for循环 控制轮数 for epoch_idx in range(1, 1+epochs)
# 内层for循环 控制迭代次数 # for item, (x, y) in enumerate(mydataloader, start=1)
# 调用内部训练函数 Train_Iters(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss)
# 计算辅助信息
# 计算打印屏幕间隔损失-每隔1000次 # 计算画图间隔损失-每隔100次
# 每个轮次保存模型 torch.save(my_encoderrnn.state_dict(), PATH1)
# 所有轮次训练完毕 画损失图 plt.figure() .plot(plot_loss_list) .save('x.png') .show()
def Train_seq2seq():
# 实例化 mypairsdataset对象 实例化 mydataloader
mypairsdataset = MyPairsDataset(my_pairs)
mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)
# 实例化编码器 my_encoderrnn 实例化解码器 my_attndecoderrnn
my_encoderrnn = EncoderRNN(2803, 256)
my_attndecoderrnn = AttnDecoderRNN(4345, 256)
# 实例化编码器优化器 myadam_encode 实例化解码器优化器 myadam_decode
myadam_encode = optim.Adam(my_encoderrnn.parameters(), lr=mylr)
myadam_decode = optim.Adam(my_attndecoderrnn.parameters(), lr=mylr)
# 实例化损失函数
mycrossentropyloss = nn.NLLLoss()
# 定义模型训练的参数
plot_loss_list = [] # (返回)
# 外层for循环 控制轮数
for epoch_idx in range(1, 1 + epochs):
print_loss_total, plot_loss_total = 0.0, 0.0
starttime = time.time()
# 内层for循环 控制迭代次数
for item, (x, y) in enumerate(mydataloader, start=1):
# 调用内部训练函数
myloss = Train_Iters(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode,
mycrossentropyloss)
print_loss_total += myloss
plot_loss_total += myloss
# 计算辅助信息
# 计算打印屏幕间隔损失-每隔1000次 # 计算画图间隔损失-每隔100次
if item % print_interval_num == 0:
print_loss_avg = print_loss_total / print_interval_num
# 将总损失归0
print_loss_total = 0
# 打印日志,日志内容分别是:训练耗时,当前迭代步,当前进度百分比,当前平均损失
print('轮次%d 损失%.6f 时间:%d' % (epoch_idx, print_loss_avg, time.time() - starttime))
if item % plot_interval_num == 0:
# 通过总损失除以间隔得到平均损失
plot_loss_avg = plot_loss_total / plot_interval_num
# 将平均损失添加plot_loss_list列表中
plot_loss_list.append(plot_loss_avg)
# 总损失归0
plot_loss_total = 0
if item == 10:
break
# 每个轮次保存模型 torch.save(my_encoderrnn.state_dict(), PATH1)
torch.save(my_encoderrnn.state_dict(), './my_encoderrnn_%d.pth' % epoch_idx)
torch.save(my_attndecoderrnn.state_dict(), './my_attndecoderrnn_%d.pth' % epoch_idx)
# 所有轮次训练完毕 画损失图
# 所有轮次训练完毕 画损失图 plt.figure() .plot(plot_loss_list) .save('x.png') .show()
plt.figure()
plt.plot(plot_loss_list)
plt.savefig('./s2sq_loss.png')
plt.show()