一个学期下来,深度学习的Lab也写了不少了(doge
打算把原理和实现总结一下~
从暑假的第一个RNN开始叭 ?
原理
Andrew Ng网课中,选择RNN的原因有二:
- 原本的FC等神经网络中,输入输出都是固定的。但是在某些场景下,输入和输出的长度并不固定(如翻译时,输入和输出的语句长度均不固定)
- 神经网络不能学习文本间的联系(如上下文中词语的关系)
因此RNN每一层的输入为 a < T ? 1 > , x < T > a^{<T-1>},x^{<T>} a<T?1>,x<T>,即包含了前一层的输出
处理为: a < t > = g ( W a [ a < t ? 1 > , x < t > ] + b a ) y ^ < t > = g ( W y a < t > + b y ) a^{<t>}=g(W_a[a^{<t-1>},x^{<t>}]+b_a) \\\hat{y}^{<t>}=g(W_ya^{<t>}+b_y) a<t>=g(Wa?[a<t?1>,x<t>]+ba?)y^?<t>=g(Wy?a<t>+by?)
根据输入输出的长度为一或多,可分为Many-to-one(eg. 情感分类), Many-to-many(eg. 机器翻译), One-to-many(eg. Music generation), One-to-one
因为文本等场景中,真正有效的信息可能相隔较远,预测时很难学习到,因此产生了LSTM和GRU
LSTM计算了forget gate、update gate、output gate,据此计算每一层记忆单元的值,相当于根据语境来选择性的记忆,忽略掉无用的信息,保存有用的信息,作出更准确的预测;LSTM还可以解决梯度消失的问题,原因可见https://www.cnblogs.com/bonelee/p/10475453.html
GRU和LSTM还是很像的,只是计算了reset gate和update gate,据此选择性的记忆
RNN Pytorch实现
在被各路版本坑了一遍后,终于搞出来能跑的 ?
数据集:climate气温数据
训练集输入: [ T 0 , T k ? 1 ] [T_0 , T_{k-1}] [T0?,Tk?1?]
训练集输出: [ T 1 , T k ] [T_1 , T_{k}] [T1?,Tk?]
验证集输入: [ T 0 , T t ? 1 ] [T_0 , T_{t-1}] [T0?,Tt?1?](除最后一个的全部数据,对应之后的timestep - 1)
验证集输出: [ T 1 , T t ] [T_1 , T_{t}] [T1?,Tt?](除第一个的全部数据)
先定义一波参数
# 读入数据
# 73 * 676 * float64
load_data = np.loadtxt("./time_series_prediction_dataset/climate")# 参数定义
time_step = 676
input_size = 73
batch_size = 1
hidden_size = 164
output_size = 1
lr = 0.0001
# 训练集数据[473, 1]
train_size = int(0.7 * time_step)
epoch_num = 10
time_step:总的时间步,即每个数据的长度
input_size:读入的样本数
hidden_size:每层的神经元数
train_size:训练集中输入输出的长度k
划分输入输出
# 划分输入输出
x_train = []
y_train = []
x_test = []
y_test = []
for data in load_data:# data_x_train: [472, 1], T_0 - T_471data_x_train = torch.tensor(data[0:train_size]).float().view(1, train_size, 1)# data_y_train: [472, 1], Y_1 - T_472data_y_train = torch.tensor(data[1:train_size + 1]).float().view(1, train_size, 1)# data_x_test: [675, 1], Y_0 - Y_674data_x_test = torch.tensor(data[:-1]).float().view(1, time_step - 1, 1)# data_y_test: [675, 1], Y_1 - Y_675data_y_test = torch.tensor(data[1:]).float().view(1, time_step - 1, 1)# x_train: [1, 472, 1]x_train.append(data_x_train)# y_train: [1, 472, 1]y_train.append(data_y_train)# x_test: [1, 675, 1]x_test.append(data_x_test)# y_test: [1, 675, 1]y_test.append(data_y_test)
至于为什么要倒腾成[1, timestep - 1, 1]这种形式而不是直接[timestep - 1, 1]呢,emmm
可能是因为形式是[batch_size, time_step - 1, data_dimension]叭,如果batch_size不为1还需要处理
构建模型
class Net(nn.Module):def __init__(self, ):super(Net, self).__init__()# 定义模型self.rnn = nn.LSTM(input_size=batch_size,hidden_size=hidden_size,num_layers=1,batch_first=True,bidirectional=False)for p in self.rnn.parameters():nn.init.normal_(p, mean=0.0, std=0.001)self.linear = nn.Linear(hidden_size, output_size)def forward(self, x, hidden_prev):out, hidden_prev = self.rnn(x, hidden_prev)# -1: 表示元素总数不变out = out.view(-1, hidden_size)# out: [473, 1]out = self.linear(out)# unsqeeze增加维度, out:[1, 473, 1]out = out.unsqueeze(dim=0)return out, hidden_prev
主要的构建方法和NN是类似的(由于训练集和验证集的长度不同,不能使用keras)
有几个操作要注意一下:
- 初始化参数:
for p in self.rnn.parameters():
nn.init.normal_(p, mean=0.0, std=0.001)
用了nn.init.normal_然后定义均值和标准差,进行正态分布的初始化 - unsqueeze增加维度
out = out.unsqueeze(dim=0)
这个是为了和y_train保持一致,都是[1, train_size - 1, 1]的形式,便于计算loss,dim=0表示在第一维增加
初始化模型
model = Net()
criterion = nn.MSELoss()
optimizer = optim.RMSprop(model.parameters(), lr)# 初始化hidden
# 第一个参数为layers
hidden_prev = torch.zeros(2, 1, hidden_size)# 标准化到(0, 1)区间
scaler = MinMaxScaler(feature_range=(0, 1), copy=False)
load_data = scaler.fit_transform(load_data)
# 随机化
random.shuffle(load_data)
简简单单初始化一下
- 优化器
在之后的LSTM下,RMSprop的性能明显优于Adam - 非官方数据集需要做标准化
训练
train_RMSE = []
test_RMSE = []
# 训练集
for time in range(1, epoch_num + 1):epoch_RMSE = []for i, x, y in zip(range(input_size), x_train, y_train):output, hidden_prev = model(x, hidden_prev)hidden_prev = hidden_prev.detach()loss = criterion(output, y)model.zero_grad()loss.backward()optimizer.step()epoch_RMSE.append(sqrt(loss.item()))train_RMSE.append(np.mean(epoch_RMSE))print("Epoch: {} Train RMSE {}".format(time, np.mean(epoch_RMSE)))# 测试集epoch_RMSE = []for i, x, y in zip(range(input_size), x_test, y_test):output, hidden_prev = model(x, hidden_prev)loss = criterion(output, y)epoch_RMSE.append(sqrt(loss.item()))test_RMSE.append(np.mean(epoch_RMSE))print("Epoch: {} Test RMSE {}".format(time, np.mean(epoch_RMSE)))
没什么问题,和NN几乎一样做反向传播
可视化
test_index = random.randint(0, input_size)
x = x_test[test_index]
y = y_test[test_index]
output, hidden_prev = model(x, hidden_prev)
plt.plot(y[0])
plt.plot(output.detach().numpy().tolist()[0])
# 像素用dpi调整
plt.savefig('climate.jpg', dpi=1000, bbox_inches='tight')
plt.show()# RMSE可视化
plt.plot(train_RMSE, label='Train RMSE')
plt.plot(test_RMSE, label='Test RMSE')
plt.xlabel('Epoch')
plt.ylabel('RMSE')
plt.savefig('climate_RMSE.jpg', dpi=1000, bbox_inches='tight')
plt.legend()
plt.show()
结果
loss正常下降,测试集RMSE为0.1949
数据拟合…就害行叭,小菜鸟也调不出啥好参 ?
试一下LSTM
LSTM看起来那么厉害,就康康到底咋样
唯一的改动只有几处:
定义模型时使用LSTM
# 定义模型self.rnn = nn.LSTM(input_size=batch_size,hidden_size=hidden_size,num_layers=1,batch_first=True,bidirectional=False)
初始化两个参数(包括记忆单元的参数)
# 初始化hidden
# 第一个参数为layers
hidden_prev = torch.zeros(1, 1, hidden_size)
c_prev = torch.zeros(1, 1, hidden_size)
训练和测试的过程也要修改为两个参数
output, (hidden_prev, c_prev) = model(x, (hidden_prev, c_prev))hidden_prev = hidden_prev.detach()c_prev = c_prev.detach()
可能是数据集的周期性不明显还是什么的…loss和拟合程度没啥变化
基于imdb数据集的RNN、LSTM、双向LSTM、GRU
依旧先定义一波参数
# 作为特征的单词个数
max_features = 10000
# 评论长度
maxlen = 500
batch_size = 32# 读入数据
# input_train: 25000 * 218 * int
# y_train: 25000 * int
(input_train, y_train),(input_test, y_test) = imdb.load_data(num_words = max_features)
input_train = sequence.pad_sequences(input_train, maxlen = maxlen)
input_test = sequence.pad_sequences(input_test, maxlen = maxlen)
几个地方需要注意:
- keras的输入长度必须一致,所以用
sequence.pad_sequences
来填充不足的部分 - imdb数据集输出的是int的list,int值为每个单词的索引,用max_features限制vocabulary的大小
建立RNN模型
model = Sequential()
model.add(Embedding(max_features, 32))
model.add(Dropout(0.5))
model.add(SimpleRNN(32))
model.add(Dense(1, activation='sigmoid'))model.compile(optimizer='rmsprop', loss='MSE', metrics=['accuracy'])
history = model.fit(input_train, y_train,epochs=10,batch_size=batch_size,validation_split=0.2)
就这么几行,keras真的爱了
Embedding层的作用:
- 降维:输入维度是max_features,也就是vocabulary的大小;输出维度是32,embedding层会用输入乘以一个权重矩阵
- 学习独立向量间的关系:原本的one-hot编码是[1, 0, 0, …, 0]的形式,每行都是独立的,但是embedding层的权重矩阵会学习到不同单词间的关系,输出[0.5, 0.125, …, 0.23]的形式,来恰当的表征每个词语与每种属性(此处为32种)的关系
具体见https://blog.csdn.net/qq_36523492/article/details/112260687
RNN的效果确实不好,训练的时候一直出现过拟合:
解释是这样的:由于存在梯度消失问题,SimpleRNN无法学到长期依赖,且SimpleRNN过于简化,没有实用价值
LSTM模型
model=Sequential()
model.add(Embedding(max_features,32))
model.add(LSTM(32))
model.add(Dense(1,activation='sigmoid'))
model.compile(optimizer='rmsprop',loss='MSE',metrics=['accuracy'])
history=model.fit(input_train, y_train,epochs=5, batch_size=128,validation_split=0.2)
加上了forget gate之后梯度消失的情况减少
loss: 0.0278 - accuracy: 0.9673 - val_loss: 0.0952 - val_accuracy: 0.8806,效果还是可以的
双向LSTM模型
model = Sequential()
model.add(Embedding(max_features, 32))
model.add(Bidirectional(LSTM(32)))
model.add(Dense(1, activation='sigmoid'))model.compile(optimizer='rmsprop', loss='MSE', metrics=['accuracy'])
history = model.fit(input_train, y_train,epochs=3, batch_size=128, validation_split=0.2)
双向的思想也很自然,后面的词语会依赖前文,那么前面的词语也会依赖后文,不如两个方向都传播然后都更新试一下
效果比LSTM差一些,loss: 0.0725 - accuracy: 0.9073 - val_loss: 0.1099 - val_accuracy: 0.8598,似乎是在图像识别中更有效
GRU模型
model = Sequential()
model.add(Embedding(max_features, 32))
model.add(GRU(32))
model.add(Dense(1))
model.compile(optimizer='rmsprop', loss='MSE', metrics=['accuracy'])
history = model.fit(input_train, y_train, epochs=5, batch_size=128, validation_split=0.2)
和LSTM差不多,loss: 0.0402 - accuracy: 0.9607 - val_loss: 0.1018 - val_accuracy: 0.8774