目录
1 实现SRN
先回顾一下什么是SRN。
SRN全称Simple Recurrent Network,即简单循环网络。SRN在传统的前馈神经网络的基础上,增加了一个隐藏层,这个隐藏层的输出会反馈到自身,形成一个循环。这样,网络就能够在时间上保持状态,即“记忆”之前的输入信息。()
(1)使用numpy实现
'''
@Function: 使用numpy实现SRN
@Author: lxy
@Date: 2024/11/24
'''
import numpy as np
# 初始化输入序列
inputs = np.array([[1.,1.],[1.,1.],[2.,2.]])
print(f"inputs is:\n{inputs}")
# 初始化存储器
state_t = np.zeros(2)
print(f"states_t is:\n{state_t}")
# 初始化权重参数,这里所有权重都为1,bias = 0
w1, w2, w3, w4, w5, w6, w7, w8 = 1., 1., 1., 1., 1., 1., 1., 1.
U1, U2, U3, U4 = 1., 1., 1., 1.
print('============================================')
for t,input_t in enumerate(inputs):
print(f"第{t+1}时刻:")
print(f'input_t is: {input_t}')
print(f'state_t is: {state_t}')
# 隐藏层的输入=当前输入 + 前一时刻的状态
input_h1 = np.dot([w1,w3],input_t) + np.dot([U2,U4],state_t)
input_h2 = np.dot([w2,w4],input_t) + np.dot([U1,U3],state_t)
state_t = input_h1, input_h2 # 更新状态(无激活 输入=输出):直接将计算得到的隐藏层输入赋值给state_t
# 输入为隐藏层的输出,计算最终输出
output_y1 = np.dot([w5, w7], state_t)
output_y2 = np.dot([w6, w8], state_t)
print(f"outputs is: {output_y1}、{output_y2}")
print('============================================')
运行结果:
inputs is:
[[1. 1.]
[1. 1.]
[2. 2.]]
states_t is:
[0. 0.]
============================================
第1时刻:
input_t is: [1. 1.]
state_t is: [0. 0.]
outputs is: 4.0、4.0
============================================
第2时刻:
input_t is: [1. 1.]
state_t is: (2.0, 2.0)
outputs is: 12.0、12.0
============================================
第3时刻:
input_t is: [2. 2.]
state_t is: (6.0, 6.0)
outputs is: 32.0、32.0
============================================
可见与手动计算的结果一致 。
(图源)
(2)在(1)的基础上,增加激活函数tanh
'''
@Function: 使用numpy实现SRN
@Author: lxy
@Date: 2024/11/24
'''
import numpy as np
# 初始化输入序列 3个对应三个时刻
inputs = np.array([[1.,1.],[1.,1.],[2.,2.]])
print(f"inputs is:\n{inputs}")
# 初始化存储器
state_t = np.zeros(2)
print(f"states_t is:\n{state_t}")
# 初始化权重参数,这里所有权重都为1,bias = 0
w1, w2, w3, w4, w5, w6, w7, w8 = 1., 1., 1., 1., 1., 1., 1., 1.
U1, U2, U3, U4 = 1., 1., 1., 1.
print('============================================')
for t,input_t in enumerate(inputs):
print(f"第{t+1}时刻:")
print(f'input_t is: {input_t}')
print(f'state_t is: {state_t}')
# 隐藏层的输入=当前输入 + 前一时刻的状态
input_h1 = np.dot([w1,w3],input_t) + np.dot([U2,U4],state_t)
input_h2 = np.dot([w2,w4],input_t) + np.dot([U1,U3],state_t)
# state_t = input_h1, input_h2 # 更新状态(无激活 输出=输入):直接将计算得到的隐藏层输入赋值给state_t
state_t = np.tanh(input_h1),np.tanh(input_h2) # 更新状态(有激活 输出=tanh(输入) )
# 输入为隐藏层的输出,计算最终输出
output_y1 = np.dot([w5, w7], state_t)
output_y2 = np.dot([w6, w8], state_t)
print(f"outputs is: {output_y1}、{output_y2}")
print('============================================')
运行结果:
inputs is:
[[1. 1.]
[1. 1.]
[2. 2.]]
states_t is:
[0. 0.]
============================================
第1时刻:
input_t is: [1. 1.]
state_t is: [0. 0.]
outputs is: 1.9280551601516338、1.9280551601516338
============================================
第2时刻:
input_t is: [1. 1.]
state_t is: (0.9640275800758169, 0.9640275800758169)
outputs is: 1.9984510891336251、1.9984510891336251
============================================
第3时刻:
input_t is: [2. 2.]
state_t is: (0.9992255445668126, 0.9992255445668126)
outputs is: 1.9999753470497836、1.9999753470497836
============================================
对比(1)(2)输出 结果可以看到无激活函数时,输出值随着时间步的增加而线性增长,从4.0增加到32.0。有激活函数时,输出值在经过激活函数后,增长速度明显减缓。
(3)使用nn.RNNCell实现
'''
@Function: 使用nn.RNNCell实现SRN
@Author: lxy
@Date: 2024/11/24
'''
import torch
batch_size = 1
seq_len = 3 # 序列长度(多少时间步)
input_size = 2 # 输入序列维度
hidden_size = 2 # 隐藏层维度
output_size = 2 # 输出层维度
# # 创建RNNCell实例
cell = torch.nn.RNNCell(input_size=input_size,hidden_size=hidden_size)
# 初始化参数 https://zhuanlan.zhihu.com/p/342012463
'''
RNN的weight和bias封装在parameters中,且需要对weight和bias分开初始化
'''
for name,param in cell.named_parameters():
if name.startswith('weight'): # 初始化weight
torch.nn.init.ones_(param)
else: # 初始化bias
torch.nn.init.zeros_(param)
# 线性层-> 将隐藏状态映射到输出
linear = torch.nn.Linear(hidden_size,output_size)
# 初始化线性层的权重为1
linear.weight.data = torch.Tensor([[1,1],[1,1]])
# 初始化线性层的偏置为0
linear.bias.data = torch.Tensor([0.0])
# 定义输入序列
'''
三维,形状(3, 1, 2)
第一维:序列长度(seq_len),这里是3
第二维:批次大小(batch_size),这里是1
第三维:输入特征的维度(input_size),这里是2
'''
seq = torch.Tensor([[[1,1]],[[1,1]],[[2,2]]])
# 初始化隐藏状态和输出为0
hidden = torch.zeros(batch_size,hidden_size)
output = torch.zeros(batch_size,output_size)
# 遍历序列中的每一个时间步
for idx,input in enumerate(seq):
print('===========================')
print(f"第{idx+1}时刻:")
print(f'Input :{input}')
print(f'hidden :{hidden}')
hidden = cell(input,hidden) # 使用RNNCell处理当前输入,并更新隐藏状态
output = linear(hidden) # # 使用线性层将隐藏状态转换为输出
print(f"output :{output}")
运行结果:
===========================
第1时刻:
Input :tensor([[1., 1.]])
hidden :tensor([[0., 0.]])
output :tensor([[1.9281, 1.9281]], grad_fn=<AddmmBackward0>)
===========================
第2时刻:
Input :tensor([[1., 1.]])
hidden :tensor([[0.9640, 0.9640]], grad_fn=<TanhBackward0>)
output :tensor([[1.9985, 1.9985]], grad_fn=<AddmmBackward0>)
===========================
第3时刻:
Input :tensor([[2., 2.]])
hidden :tensor([[0.9992, 0.9992]], grad_fn=<TanhBackward0>)
output :tensor([[2.0000, 2.0000]], grad_fn=<AddmmBackward0>)
(4)使用nn.RNN实现
'''
@Function: 使用nn.RNN实现SRN
@Author: lxy
@Date: 2024/11/24
'''
import torch
# 设置批处理大小
batch_size = 1
# 设置序列长度
seq_len = 3
# 输入序列的维度
input_size = 2
# 隐藏层的维度
hidden_size = 2
# 输出层的维度
output_size = 2
# RNN层的数量
num_layers = 1
# 创建RNN实例
cell = torch.nn.RNN(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers)
# 初始化参数
for name, param in cell.named_parameters():
if name.startswith('weight'):
torch.nn.init.ones_(param)
else:
torch.nn.init.zeros_(param)
# 线性层 将隐藏状态映射到输出
linear = torch.nn.Linear(hidden_size, output_size)
# 初始化线性层的权重为1
linear.weight.data = torch.Tensor([[1,1],[1,1]])
# 初始化线性层的偏置为0
linear.bias.data = torch.Tensor([0.0])
# 创建输入序列
'''
三维,形状(3, 1, 2)
第一维:序列长度(seq_len),这里是3
第二维:批次大小(batch_size),这里是1
第三维:输入特征的维度(input_size),这里是2
'''
inputs = torch.Tensor([[[1,1]],[[1,1]],[[2,2]]])
# 初始化隐藏状态为0,这里需要考虑RNN层的数量
hidden = torch.zeros(num_layers, batch_size, hidden_size)
# 通过RNN处理输入序列,并更新隐藏状态
out, hidden = cell(inputs, hidden)
# 打印第一个时间步的输入、隐藏状态和输出
print("第1时刻:")
print(f'Input : {inputs[0]}')
print(f'hidden: {[0 , 0]}')
print(f'Output: {linear(out[0])}')
print('======================================')
# 打印第二个时间步的输入、隐藏状态和输出
print("第2时刻:")
print(f'Input : {inputs[1]}')
print(f'hidden: {out[0]}')
print(f'Output: {linear(out[1])}')
print('=======================================')
# 打印第三个时间步的输入、隐藏状态和输出
print("第3时刻:")
print(f'Input : {inputs[2]}')
print(f'hidden: {out[1]}')
print(f'Output: {linear(out[2])}')
【这里不使用循环来打印的原因是因为RNN是一个封装好的循环神经网络层,处理整个序列的前向传播,并返回每个时间步的输出和最后一个时间步的隐藏状态,而RNNCell
是一个基本的循环神经网络单元,只处理单个时间步的输入,所以在(3)中需要循环调用】
运行结果:
第1时刻:
Input : tensor([[1., 1.]])
hidden: [0, 0]
Output: tensor([[1.9281, 1.9281]], grad_fn=<AddmmBackward0>)
======================================
第2时刻:
Input : tensor([[1., 1.]])
hidden: tensor([[0.9640, 0.9640]], grad_fn=<SelectBackward0>)
Output: tensor([[1.9985, 1.9985]], grad_fn=<AddmmBackward0>)
=======================================
第3时刻:
Input : tensor([[2., 2.]])
hidden: tensor([[0.9992, 0.9992]], grad_fn=<SelectBackward0>)
Output: tensor([[2.0000, 2.0000]], grad_fn=<AddmmBackward0>)
2 使用RNNCell实现“序列到序列”
b站循环神经网络讲解--刘二大人 实现视频P12中的教学案例 hello--->ohlol
'''
@Function: 实现序列到序列:给出hello -->预测ohlol
@Author: lxy
@Date: 2024/11/24
'''
import torch
import torch.nn as nn
import torch.optim as optim
# 定义参数
input_size = 4 # 输入特征的维度:对应的是 one-hot 编码的大小(4个字符)
hidden_size = 4 # 隐藏层维度
batch_size = 1 # 批次大小(这里是1,表示一次只处理一个字符)
# 字符到索引的映射
idx2char = ['e', 'h', 'l', 'o']
# 输入和标签数据
x_data = [1, 0, 2, 2, 3] # 对应字符 ['h', 'e', 'l', 'l', 'o']
y_data = [3, 1, 2, 3, 2] # 对应目标字符 ['o', 'h', 'l', 'o', 'l']
# 将字符映射到one-hot编码
one_hot_lookup = [
[1, 0, 0, 0], # 'e' 对应 [1, 0, 0, 0]
[0, 1, 0, 0], # 'h' 对应 [0, 1, 0, 0]
[0, 0, 1, 0], # 'l' 对应 [0, 0, 1, 0]
[0, 0, 0, 1] # 'o' 对应 [0, 0, 0, 1]
]
# 将输入数据 x_data 转换为 one-hot 编码形式
x_one_hot = [one_hot_lookup[x] for x in x_data]
# 将输入和标签转换为PyTorch张量
inputs = torch.Tensor(x_one_hot).view(-1, batch_size, input_size) # (序列长度, 批次大小, 输入维度)
labels = torch.LongTensor(y_data).view(-1, 1) # (序列长度, 1),每个标签对应一个字符的索引
# 定义RNN模型
class Model(nn.Module):
def __init__(self, input_size, hidden_size, batch_size):
super(Model, self).__init__()
self.batch_size = batch_size
self.input_size = input_size
self.hidden_size = hidden_size
self.rnncell = nn.RNNCell(input_size=self.input_size, hidden_size=self.hidden_size)
def forward(self, input, hidden):
# RNNCell 处理每个输入,并返回新的隐藏状态
hidden = self.rnncell(input, hidden)
return hidden
def init_hidden(self):
return torch.zeros(self.batch_size, self.hidden_size)
# 初始化模型、损失函数和优化器
net = Model(input_size, hidden_size, batch_size)
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = optim.Adam(net.parameters(), lr=0.1) # 使用 Adam 优化器
# 训练模型
for epoch in range(15):
loss = 0
optimizer.zero_grad()
hidden = net.init_hidden() # 初始化隐藏状态
print('Predicted string: ', end='')
# 遍历输入序列和标签
for input, label in zip(inputs, labels):
hidden = net(input, hidden) # 将输入传入网络并获取新的隐藏状态
loss += criterion(hidden, label) # 计算损失并累加
_, idx = hidden.max(dim=1) # 获取预测的字符索引(最大值)
print(idx2char[idx.item()], end='') # 输出对应的字符
loss.backward()
optimizer.step()
print(', Epoch [%d/15] loss=%.4f' % (epoch + 1, loss.item()))
运行结果:
Predicted string: lllll, Epoch [1/15] loss=7.3776
Predicted string: lllll, Epoch [2/15] loss=6.0633
Predicted string: lhlll, Epoch [3/15] loss=4.9989
Predicted string: ohlol, Epoch [4/15] loss=4.0542
Predicted string: ohlol, Epoch [5/15] loss=3.3809
Predicted string: ohlol, Epoch [6/15] loss=3.0262
Predicted string: ohlol, Epoch [7/15] loss=2.7807
Predicted string: ohlol, Epoch [8/15] loss=2.5577
Predicted string: ohlol, Epoch [9/15] loss=2.3574
Predicted string: ohlol, Epoch [10/15] loss=2.2286
Predicted string: ohlol, Epoch [11/15] loss=2.1521
Predicted string: ohlol, Epoch [12/15] loss=2.0685
Predicted string: ohlol, Epoch [13/15] loss=1.9959
Predicted string: ohlol, Epoch [14/15] loss=1.9489
Predicted string: ohlol, Epoch [15/15] loss=1.9217
可见模型的预测从初期的随机性lllll
到 ohlol
逐步改进,最终在第 4 个 epoch 开始预测正确的字符顺序。随着训练进行,损失逐渐下降,模型逐步学会了如何更准确地预测下一个字符。实现从hello到ohlol的转换。
3 实现“编码器-解码器”-Seq2Seq
Seq2Seq的Pytorch实现 ----b站配套讲解
seq2seq的PyTorch实现_哔哩哔哩_bilibili ---代码实战
Seq2seq模型通常用于将一个序列转换为另一个序列。这种模型由两部分组成:编码器(encoder)和解码器(decoder)。编码器将输入序列编码成一个固定长度的向量,解码器则从这个向量生成输出序列。
'''
@功能: 使用PyTorch实现Seq2Seq编码器-解码器: 将输入的英语单词翻译成西班牙语
@作者: lxy
@日期: 2024/11/24
'''
import torch
import numpy as np
import torch.nn as nn
import torch.utils.data as Data
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# S: 表示解码输入的开始符号
# E: 表示解码输出的结束符号
# ?: 用于填充空白序列,当当前批次的数据长度不足n_step时使用
letter = [c for c in 'SE?abcdefghijklmnopqrstuvwxyz']
letter2idx = {n: i for i, n in enumerate(letter)}
# 示例数据: 英语单词与对应的西班牙语翻译
seq_data = [
['hello', 'hola'], # 'hello' -> 'hola'(英语到西班牙语)
['cat', 'gato'], # 'cat' -> 'gato'
['good', 'bueno'], # 'good' -> 'bueno'
]
# Seq2Seq 参数
n_step = max([max(len(i), len(j)) for i, j in seq_data]) # 最大长度(=5)
n_hidden = 128 # 隐藏层维度
n_class = len(letter2idx) # 类别数(即字符集大小)
batch_size = 3 # 批次大小
# 函数:构建训练数据
def make_data(seq_data):
enc_input_all, dec_input_all, dec_output_all = [], [], []
for seq in seq_data:
for i in range(2):
seq[i] = seq[i] + '?' * (n_step - len(seq[i])) # 如 'man??', 'women'
# 编码输入:将字符转为索引,并在末尾添加结束符'E'
enc_input = [letter2idx[n] for n in (seq[0] + 'E')] # ['m', 'a', 'n', '?', '?', 'E']
# 解码输入:在开头添加起始符'S'
dec_input = [letter2idx[n] for n in ('S' + seq[1])] # ['S', 'w', 'o', 'm', 'e', 'n']
# 解码输出:在末尾添加结束符'E'
dec_output = [letter2idx[n] for n in (seq[1] + 'E')] # ['w', 'o', 'm', 'e', 'n', 'E']
# 将每个输入转为独热编码
enc_input_all.append(np.eye(n_class)[enc_input])
dec_input_all.append(np.eye(n_class)[dec_input])
dec_output_all.append(dec_output) # 解码输出不进行独热编码
# 返回Tensor格式数据
return torch.Tensor(enc_input_all), torch.Tensor(dec_input_all), torch.LongTensor(dec_output_all)
# 获取训练数据
enc_input_all, dec_input_all, dec_output_all = make_data(seq_data)
# 自定义数据集
class TranslateDataSet(Data.Dataset):
def __init__(self, enc_input_all, dec_input_all, dec_output_all):
self.enc_input_all = enc_input_all
self.dec_input_all = dec_input_all
self.dec_output_all = dec_output_all
def __len__(self): # 返回数据集的大小
return len(self.enc_input_all)
def __getitem__(self, idx):
return self.enc_input_all[idx], self.dec_input_all[idx], self.dec_output_all[idx]
# 数据加载器
loader = Data.DataLoader(TranslateDataSet(enc_input_all, dec_input_all, dec_output_all), batch_size, True)
# Seq2Seq模型
class Seq2Seq(nn.Module):
def __init__(self):
super(Seq2Seq, self).__init__()
# 编码器:RNN模型
self.encoder = nn.RNN(input_size=n_class, hidden_size=n_hidden, dropout=0.5)
# 解码器:RNN模型
self.decoder = nn.RNN(input_size=n_class, hidden_size=n_hidden, dropout=0.5)
# 全连接层,用于输出分类
self.fc = nn.Linear(n_hidden, n_class)
def forward(self, enc_input, enc_hidden, dec_input):
# enc_input:输入的编码数据 [batch_size, n_step+1, n_class]
# dec_input:输入的解码数据 [batch_size, n_step+1, n_class]
enc_input = enc_input.transpose(0, 1) # 转置为 [n_step+1, batch_size, n_class]
dec_input = dec_input.transpose(0, 1) # 转置为 [n_step+1, batch_size, n_class]
# 编码器输出:h_t 是最后的隐藏状态
_, h_t = self.encoder(enc_input, enc_hidden)
# 解码器输出:outputs 是解码过程中的所有输出
outputs, _ = self.decoder(dec_input, h_t)
# 通过全连接层输出最终结果
model = self.fc(outputs) # [n_step+1, batch_size, n_class]
return model
# 实例化模型
model = Seq2Seq().to(device)
criterion = nn.CrossEntropyLoss().to(device) # 交叉熵损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # Adam优化器
# 训练过程
for epoch in range(5000):
for enc_input_batch, dec_input_batch, dec_output_batch in loader:
# 初始化隐藏状态
h_0 = torch.zeros(1, batch_size, n_hidden).to(device)
# 将数据移至设备
enc_input_batch, dec_input_batch, dec_output_batch = (
enc_input_batch.to(device), dec_input_batch.to(device), dec_output_batch.to(device))
# 训练模型,获取预测结果
pred = model(enc_input_batch, h_0, dec_input_batch)
# 计算损失
pred = pred.transpose(0, 1) # [batch_size, n_step+1, n_class]
loss = 0
for i in range(len(dec_output_batch)):
loss += criterion(pred[i], dec_output_batch[i]) # 计算每一批次的损失
# 每1000次迭代输出一次损失
if (epoch + 1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'lost =', '{:.6f}'.format(loss))
# 反向传播并优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 测试函数:翻译单词
def translate(word):
enc_input, dec_input, _ = make_data([[word, '?' * n_step]])
enc_input, dec_input = enc_input.to(device), dec_input.to(device)
hidden = torch.zeros(1, 1, n_hidden).to(device) # 初始化隐藏状态
output = model(enc_input, hidden, dec_input)
# 获取最大概率的预测值
predict = output.data.max(2, keepdim=True)[1]
decoded = [letter[i] for i in predict]
translated = ''.join(decoded[:decoded.index('E')]) # 直到'E'为止
return translated.replace('?', '') # 去掉填充符号
# 测试翻译效果
print('测试')
for seq in seq_data:
word = seq[0] # 获取英语单词
translated_word = translate(word) # 获取翻译结果
print(f'{word} -> {translated_word}')
运行结果;
Epoch: 1000 lost = 0.001182
Epoch: 2000 lost = 0.000338
Epoch: 3000 lost = 0.000147
Epoch: 4000 lost = 0.000075
Epoch: 5000 lost = 0.000041
测试
hello -> hola
cat?? -> gato
good? -> bueno
lost
逐渐减小,这表明模型在不断优化,误差在不断减小 。
4 简单总结nn.RNNCell、nn.RNN
(1)nn.RNNCell
- 单步处理:
RNNCell
是最基本的RNN单元,只处理单个时间步的数据。要处理整个序列,需要在外部循环中逐个时间步地调用RNNCell
。- 主要参数:
input_size
(int): 输入特征的维度。hidden_size
(int): 隐藏状态的维度。rnn_cell = nn.RNNCell(input_size, hidden_size)
- 返回结果:当前时间步的输出 +更新后的隐藏状态
(2)nn.RNN
- 批量处理:
RNN
是一个完整的RNN层,可以一次性处理整个序列。- 主要参数:
input_size
(int): 输入特征的维度。
hidden_size
(int): 隐藏状态的维度。
num_layers
(int, optional): RNN层的数量,默认为1。
batch_first
(bool, optional): 输入和输出张量的形状,默认为False。rnn = nn.RNN(input_size, hidden_size, num_layers=1, batch_first=True)
- 返回结果: 整个序列的输出 + 最后一个时间步的隐藏状态
- 🗣 batch_first,默认是 False,输入数据的格式为(seq_len, batch, input_size)。
当batch_first=True时,输入序列的格式应该为( batch,seq_len, input_size)
当num_layers=1时:
当num_layers=3时:
5 谈一谈对“序列”、“序列到序列”的理解
序列:一组按照一定顺序排列的元素(数字、元素、字符等)。每个元素在序列中的位置是固定的,并且具有一定的顺序性。
序列到序列:一种深度学习模型,通过深度学习的方式,将一个序列转化为另一个序列。通过编码器提取输入序列的特征,再通过解码器生成输出序列。
6 总结本周理论课和作业,写心得体会
通过实现作业的任务真正理解了RNN的原理,同时继卷积神经网络的Conv2d之后又认识到了循环神经网络中的nn.RNNCell和nn.RNN。另一个比较大的收获就是编码器和解码器的实现,第一次听说是在读“Attention is all you need”这篇论文中见到的(不过当时只是简单了解理论),这次作业中实现编码是用的one-hot编码(比较简单),还有更高级的词嵌入(Word Embeeding)方式。根据“编码器-解码器”架构的设计, 可以使用两个循环神经网络来设计一个序列到序列(Seq2Seq)学习的模型。
(部分笔记)