目录
1. 实现SRN
(1)使用Numpy
import numpy as np
inputs=np.array([[1.,1.],#注意这里有两个中括号
[1.,1.],
[2.,2.]]) #“1.”表明该数是浮点数,便于后期对数值进行处理
print('inputs is',inputs)
state_t=np.zeros(2,)#zeros指创建一个包含两个零的数组
# 2的意思是数组有两个元素,而并不代表数组中的值
print('state_t is ',state_t)
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 input_t in inputs:
print('inputs is ',input_t)
print('state_t is',state_t)#这两段就像解剖一样,用来展示每一次遍历元素的位置和变化
in_h1=np.dot([w1,w3],input_t)+np.dot([U2,U4],state_t)
in_h2=np.dot([w2,w4],input_t)+np.dot([U2,U4],state_t)
state_t=in_h1,in_h2#dot是点积的意思,通过点积的方式将w和U结合起来,存储到存储器中
output_y1=np.dot([w5,w7],[in_h1,in_h2])
output_y2=np.dot([w6,w8],[in_h1,in_h2])#并将inputs加工过的结果继续与新权重加工,不断赋给新值
print('output_y is ',output_y1,output_y2)
print('----------------------------')
由于程序中w1,w2,w3等等数值相同,且由上图可知应该将w1和w3联系在一起。但如果w3和w4的数值有区别呢,那么dotw1*w3和dotw1*w4有区别吗?我又应该如何处理程序从而判别出两种dot哪种更合适呢?希望在后续的学习中可以找到答案。
实验结果如下:
(2).在1的基础上,增加激活函数tanh
import numpy as np
inputs=np.array([[1.,1.],#注意这里有两个中括号
[1.,1.],
[2.,2.]]) #“1.”表明该数是浮点数,便于后期对数值进行处理
print('inputs is',inputs)
state_t=np.zeros(2,)#zeros指创建一个包含两个零的数组
# 2的意思是数组有两个元素,而并不代表数组中的值
print('state_t is ',state_t)
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 input_t in inputs:
print('inputs is ',input_t)
print('state_t is',state_t)#这两段就像解剖一样,用来展示每一次遍历元素的位置和变化
in_h1=np.tanh(np.dot([w1,w3],input_t)+np.dot([U2,U4],state_t))
in_h2=np.tanh(np.dot([w2,w4],input_t)+np.dot([U2,U4],state_t))
state_t=in_h1,in_h2#dot是点积的意思,通过点积的方式将w和U结合起来,存储到存储器中
output_y1=np.dot([w5,w7],[in_h1,in_h2])
output_y2=np.dot([w6,w8],[in_h1,in_h2])#并将inputs加工过的结果继续与新权重加工,不断赋给新值
print('output_y is ',output_y1,output_y2)
print('----------------------------')
1中要求的是线性相关,所以不需要往上面加激活函数,而2这里要求加tanh激活函数。而激活函数的主要作用就是将数据进行非线性处理,通过一种合理的变化使数值变成自己想要的形式。
在之前的学习中,我们运用激活函数的主要目的是分类,比如sigmoid函数,所以会把激活函数用于处理输出层。但这次我们的主要目的是将数据转化为一种新数据,更通俗地说,就是将前面的数据打包压缩再传递给下一层,因此将激活函数运用在in_h1和in_h2的计算上,而不用在out_put的处理上。
结果如下:
(3).使用nn.RNNCell实现
代码如下:
代码分析见注释:
import torch #numpy光荣退休了……
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)
for name, param in cell.named_parameters():
if name.startswith("weight"):
torch.nn.init.ones_(param)
else:
torch.nn.init.zeros_(param)#这段的意思也就是,把名字和权重有关的都初始化为1,不然就0
liner = torch.nn.Linear(hidden_size, output_size)
liner.weight.data = torch.Tensor([[1, 1], [1, 1]])#套娃,虽然里面是四个数的两个矩阵但这是一个数据。
liner.bias.data = torch.Tensor([0.0])#偏置
seq = torch.Tensor([[[1, 1]],
[[1, 1]],
[[2, 2]]])#两个中括号变成三个中括号了
#这是有原因的 ,array后面的是一个二维数组,里面的方括号表示一行,但整体中括号是一个数组,
#而Tensor后的则是张量,这是一个三维张量,有3个元素,子元素中各有1个子元素,子元素中有两个值。
hidden = torch.zeros(batch_size, hidden_size)
output = torch.zeros(batch_size, output_size)
for idx, input in enumerate(seq):
print('=' * 20, idx, '=' * 20)
print('Input :', input)
print('hidden :', hidden)
hidden = cell(input, hidden)
output = liner(hidden)
print('output :', output)
实验结果如下:
(4).使用nn.RNN实现
import torch
batch_size = 1
seq_len = 3
input_size = 2
hidden_size = 2
num_layers = 1#这里除了多了个这个没啥别的变化
output_size = 2
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)
liner = torch.nn.Linear(hidden_size, output_size)
liner.weight.data = torch.Tensor([[1, 1], [1, 1]])
liner.bias.data = torch.Tensor([0.0])
inputs = torch.Tensor([[[1, 1]],
[[1, 1]],
[[2, 2]]])#又叫回inputs了
hidden = torch.zeros(num_layers, batch_size, hidden_size)#这里发生改变
# 这是一个形状为(1,1,2)的张量,但张量中值都为0
out, hidden = cell(inputs, hidden)#这里out也不再只是纯全为0的张量了
print('Input :', inputs[0])
print('hidden:', 0, 0)
print('Output:', liner(out[0]))
print('--------------------------------------')
print('Input :', inputs[1])
print('hidden:', out[0])
print('Output:', liner(out[1]))
print('--------------------------------------')
print('Input :', inputs[2])
print('hidden:', out[1])
print('Output:', liner(out[2]))
最后这里没用遍历循环,而是直接简单粗暴地输出了。
·能不能用遍历的方式再次实现输出呢?试着用时间步当遍历条件:
for i in range(inputs.shape[0]):
input_t = inputs[i:i+1]
out, hidden = cell(input_t, hidden)
print('Input :', input_t[0])
print('hidden:', hidden)
print('Output:', liner(out[0]))
print('--------------------------------------')
·但最后结果是:
hidden还是错位了,遂暂时放弃了。
2.实现“序列到序列”
·视频案例主要讲述了将hello转化为ohlol的一个例子,让人感觉大有收获。
首先是将字符向量化,通过构造词典分配索引,再转换为独热向量,并将向量转化为查询中的不同位置,进而体现出字符→数字→向量→数字→分类→结果这样一种变化,真是很神奇的脑回路。
·代码实现如下:
import torch
batch_size = 1
seq_len = 3
input_size = 4
hidden_size = 2
num_layers = 1
cell=torch.nn.RNN (input_size= input_size ,hidden_size= hidden_size ,num_layers= num_layers ,batch_first= True)
inputs=torch.randn(batch_size,seq_len,input_size)
hidden=torch.zeros(num_layers,batch_size,hidden_size)
out,hidden=cell(inputs,hidden)
print('Outputs size:',out.shape)
print('Output:',out)
print('Hidden size:',hidden.shape)
print('Hidden:',hidden)
·实验结果:
··分别运行RNNCell和RNN,得到结果如下:
·Cell第一次没成功:
·第二次成功了:
·RNN第一次成功了:
重新跑了几次,“eeeee”和“lellll”跑出来的结果都是“ohlll”,“eleee”跑出来则是“oholl”,也就是,“00000”和“20222”结果“31222”,“02000”结果是“31322”.设置跑了30轮,“lolll”跑出来的结果是“oholl”,反正就是越跑越歪,所以计算的层数较少,准确率也不是很高。
但RNN跑了几次,反而准确率比RNNCell高了很多。
···想不明白,问了问chatgpt,给出以下回答:
I.RNNCell循环在外部进行,通常需要更多的代码和更细致的隐藏状态管理;RNN的内部处理整个序列的时间步,自动处理所有时间步的迭代和隐藏状态的传递。
II.RNNCell隐藏状态在每个epoch开始时被初始化,并在每个时间步更新,更容易出错。RNN中隐藏状态在每个序列的开始被初始化为零,然后由`RNN`模块内部处理,简化了隐藏状态的管理,但可能缺乏第一个方法的灵活性。
III. RNN在处理整个序列后计算总损失,并基于整个序列的输出更新模型,可能对单个样本的错误反应不那么敏感。
IV.RNNCell在每个时间步之后立即执行反向传播和优化步骤,可能导致较短的时间步长获得更多的优化,但也可能导致梯度消失或爆炸的问题。RNN在整个序列处理完后执行一次反向传播和优化。
额外补充:
·不过,在看视频的过程中,老师关于“loss+=啥啥啥”的话启发了我,回到上面纠结的循环序列中,我是否可以通过使用自加更新,来调整hidden的错位问题呢?只要添加一个变量用来保存上一个循环中hidden的值,并用于下一个循环的输出,能否解决问题呢?
越想我越激动,马上着手实施了一下:
hidden = torch.zeros(num_layers, batch_size, hidden_size)
previous_hidden = torch.zeros_like(hidden) #只是在这里添加了一个自定义变量
for i in range(inputs.shape[0]):
input_t = inputs[i:i+1]
out, hidden = cell(input_t, hidden)
print('Input :', input_t[0])
print('hidden:', previous_hidden)#改了一下
print('Output:', liner(out[0]))
print('--------------------------------------')
# 更新previous_hidden为当前的hidden
previous_hidden = hidden.clone()#赋了个值
结果如下,哦我的老天爷,高兴得快要昏倒了。
·另外还有一点是习题后学到的,除了使用索引还可以通过嵌入层的方式对字符进行向量转换,这一点好像在作业1提过,遂不再详谈。
3.“编码器-解码器”的简单实现
根据作业来看“编码器-编码器”应该主要是指:seq2seq,不过文章里也提到了AutoEncoder,所以我想两种应该都属于“编码器-编码器”吧?
搜了一下:
Seq2Seq,或称为Sequence-to-Sequence,是一种深度学习模型架构,用于处理输入和输出序列的任务。
Seq2Seq 模型基于编码器-解码器(Encoder-Decoder)结构,分为两个主要部分:
1. 编码器(Encoder):*将输入序列转换为固定长度的上下文向量(context vector)。编码器网络通常使用循环神经网络(RNN)或者长短时记忆网络(LSTM)等结构,逐步处理输入序列并将其转换为上下文向量。
2. 解码器(Decoder):将上下文向量转换为输出序列。与编码器类似,解码器网络也通常使用RNN或LSTM,但它以逐步的方式生成输出序列,同时利用上下文向量的信息。
Seq2Seq 模型的训练过程通常涉及到使用已知的输入-输出对(例如平行语料库中的源语言句子和目标语言句子)进行监督学习。模型通过最小化预测输出与真实输出之间的差异来学习转换规律。
AutoEncoder(自编码器)是一种无监督学习算法,通常用于学习数据的有效表示。它属于神经网络模型的一种,通过将输入数据映射到自身来学习数据的压缩表示。
AutoEncoder 的基本结构包含两个主要部分:
1. 编码器(Encoder): 将输入数据进行压缩,将其映射到一个低维的表示,通常称为编码(encoding)或隐藏层(latent space)。
2. 解码器(Decoder): 将编码后的数据映射回原始输入空间,尽量重构输入数据。
AutoEncoder 的目标是通过最小化输入与输出之间的重构误差来学习数据的紧凑表示。这样,模型被迫学习到数据中最重要的特征,从而能够在编码和解码之间保留足够的信息。
在训练过程中,AutoEncoder试图最小化重构误差,从而迫使模型学到输入数据的有意义的表示,而不是简单地将数据记忆下来。
·通过代码实现:
参考代码文章里有,直接引用一下:
·不过对于这个代码,就像文章里说的,我也觉得说得很详细了,没什么好说的了。
·只说一些感悟吧:
- 如果man的字数不够,需要在后面加问号,也就是“<pad>”,其实其他的应该也行吧,就是麻烦很多。
- 文章和视频里都有提到,输入结束标志并不重要,因为Decoder 需要输出多长的句子是已知的。
- 因为这块是RNN,所以感觉得具体一些写感悟(正在学的就是这块嘛)
# Model
class Seq2Seq(nn.Module):
def __init__(self):
super(Seq2Seq, self).__init__()
self.encoder = nn.RNN(input_size=n_class, hidden_size=n_hidden, dropout=0.5) # 定义encoder,dropout是防止过拟合设置的随机失活概率
self.decoder = nn.RNN(input_size=n_class, hidden_size=n_hidden, dropout=0.5) # decoder
self.fc = nn.Linear(n_hidden, n_class)#又是一个线性层
def forward(self, enc_input, enc_hidden, dec_input):
# enc_input(=input_batch): [batch_size, n_step+1, n_class]
# dec_inpu(=output_batch): [batch_size, n_step+1, n_class]
enc_input = enc_input.transpose(0, 1) # enc_input: [n_step+1, batch_size, n_class]
dec_input = dec_input.transpose(0, 1) # dec_input: [n_step+1, batch_size, n_class]
#这里是前向传播,像波浪一样,不断向前传递。
# h_t : [num_layers(=1) * num_directions(=1), batch_size, n_hidden]
_, h_t = self.encoder(enc_input, enc_hidden)#第一个_表示占位符,也就是最终只有hidden赋值,input不重要
# outputs : [n_step+1, batch_size, num_directions(=1) * n_hidden(=128)]
outputs, _ = self.decoder(dec_input, h_t)#这个则是不要h_t
model = self.fc(outputs) # model : [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)
虽然看着有些难,但实际上还是RNN,先定义enc和dec以后,通过前向传播不断地糅合两个数据,再输出。感觉用得很妙的是占位符,既不用频繁修改数据的类型,也可以很轻松地获得自己想要的数据。
结果如下:
4.简单总结nn.RNNCell、nn.RNN(有参考资料)
其实本来想叫“RNNCell”为“RNN之心”来者,感觉好听,但了解它的运作原理后感觉还是cell更合适。与nn.RNN相比,nn.RNNCell的运作机理就行一个个小细胞一样,各司其职,用于定义单个时间步的 RNN 单元,只能接受序列中单步的输入,且必须传入隐藏状态。nn.RNN
是一次性将 所有时刻 特征喂入的,nn.RNNCell
将序列上的 每个时刻 分开来处理。
-
举例:如果要处理3个句子,每个句子10个单词,每个单词用100维的嵌入向量表示
nn.RNN传入的Tensor的shape是[10,3,100]nn.RNNCell传入的Tensor的shape是[3,100],将此计算单元运行10次。 -
而nn.RNN
的数据处理如下图所示:
-
每次向网络中输入batch个样本,每个时刻处理的是该时刻的batch个样本,因此 xt 是shape为[batch,feature_len]的Tensor。
- 例如,输入3句话,每句话10个单词(T_x),每个单词用100维的向量表示,那么seq_len=10,batch=3,feature_len=100。
-
隐藏记忆单元h 的shape是二维的[batch,hidden_len],其中hidden_len是一个可以自定的超参数
- 例如,可以取为20,表示每个样本用20长度的向量记录。
5.谈一谈对“序列”、“序列到序列”的理解
“序列”,什么是序列呢?从视频上来说,是指字符串吧,但我认为“序列”是指包括数学在内的一切可以用数字或间接通过数字表达的有顺序的排列。
无论是面对什么“序列”,我们都不要慌乱,而是要去思考该“序列”和“数字”的关系。比如,一个班上有四五十个同学,我是否可以将他们转化为学号代替呢?或者通过名字笔画?现编码也行。将事物转化为序列,我们才可以将事物进行排列。
但像视频中判断是否下雨的事就不是排列,而是判断了,这种事情也可以完成吗?是的,因为RNN其实本质上是在做分类,并不是将数据进行真正的排序,而是通过分类的手段达到排序的目的——这也是“序列到序列”的基本思想,以一种有效的方式将数据进行转换,最终再完好地还回去,实现从序列到序列。
6.总结本周理论课和作业,写心得体会
值得高兴的有两件事,一件事是看着“loss+”那块的代码,苦恼着的循环输出问题一下子感觉茅塞顿开,就像高中解开一套数学题一样,那种欣喜若狂的感觉已经很久没有体验过了。虽然只是一个小问题,但也让我觉得很快乐。
另一点就是作业一了解过的独热码,在这里又遇到了。有种与知识相遇的感觉,很熟悉,也很高兴。
遗憾的是我现在太困了,本来还想好好了解一些剩下的任务,比如AutoEncoder,比如nn.RNN,但是我实在是太困了,所以就这样写完吧。
参考文献:
PyTorch 学习笔记(十一):循环神经网络(RNN) - 知乎
Pytorch 循环神经网络 nn.RNN() nn.RNNCell() nn.Parameter()不同方法实现-CSDN博客
seq2seq的PyTorch实现_哔哩哔哩_bilibili
Pytorch 循环神经网络 nn.RNN() nn.RNNCell() nn.Parameter()不同方法实现-CSDN博客