《白话强化学习与PyTorch》学习笔记---第六章
第六章-深度学习
第六章主要用了三种神经网络实现了对手写数字(0~9)数据集MNIST的分类问题(全连接神经网络(feedforward_neural_net.py)、卷积神经网络(convoluntional_neural_network.py)、循环神经网络(Recurrent_Neural_net.py))。在学习过程中,我对书中的代码进行的复现并且进行了略微的修改。详细代码见我的github
1. 代码修改的主要内容
书中代码使用的pytorch均为0.4以前的本版,要适应现在普遍使用的0.4以后的版本,需要注意以下两点:
- 无需再把Tensor转换成Variable的形式,Tensor直接可以计算梯度;
- 累加损失时,.data[0]改为.item(),0.4以后的版本中loss是一个零维的标量,用loss.item()可以从标量中获取Python数字;
此外,将代码改为当有GPU时使用GPU,没有GPU使用CPU的形式:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") #GPU or CPU
并加入了一个GPUtil模块,可以实时监测GPU信息。
import GPUtil
GPUtil.showUtilization()
2. 分类问题
书中用三种网络分别实现了对MNIST手写数字集的分类问题,接下来的内容主要是对代码进行分析,需要对代码有一定的了解。
在学习过程中我感觉比较绕的是各个网络的输入和输出,下面我再来梳理一下: 首先,需要明确从MNIST下载的图片为灰度图片,分辨率为28像素×28像素,其中train_dataset(60000组数据)/test_dataset(10000组数据)存在两种数据,一个是大小为[1,28,28]图片数据(data),一个是int类型的标签(targets)。
print(train_dataset.data.size()) # (60000, 28, 28)
print(train_dataset.targets.size()) # (60000)
然后用dataloader进行了批处理,也就是每次随机(shuffle参数表示是否将原数据集重新打乱)挑选batch_size个样本进行训练,再进行一次梯度下降,这就是批训练,它可以帮助我们有效的迭代数据。
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size =batch_size,
shuffle=True)
2.1 全连接神经网络
首先看一下代码中全连接神经网络的参数:
Net(
(linear1): Linear(in_features=784, out_features=500, bias=True)
(relu): ReLU()
(linear2): Linear(in_features=500, out_features=10, bias=True)
)
因为图片像素为28×28,故全连接网络的输入大小也为28×28,故需要对dataloader取出的数据进行处理后输入:
images.to(device).view(-1, 28*28) #images shape (batch_size, 1, 28, 28)→(batch_size, 784)
经过第一个线性层(linear1)后,x shape (batch_size, 784)→(batch_size, 500)
经过激励函数relu()后,x的大小不变
经过第二个线性层(linear2)后,x shape (batch_size, 500)→(batch_size, 10)
可以看出,输出为1×10的矩阵,它是一个独热向量,适用于多分类问题,用交叉熵损失函数(CrossEntropyLoss)训练。
2.2 卷积神经网络:
共三层网络,下面结合代码进行逐一分析。
第一层网络:直接输入图片信息,大小为(batch_size,1,28,28),输出为大小为(batch_size, 16 ,14,14)
self.layer1 = nn.Sequential(
nn.Conv2d(
in_channels=1, #图片高度(1)
out_channels=16, #卷积核个数
kernel_size=5, #卷积核尺寸
stride=1, #卷积核扫描步长
padding=2), #边缘补零
nn.BatchNorm2d(16), #BN层,批标准化操作,output shape(batch_size, 16 ,28,28)
nn.ReLU(), #output shape(batch_size, 16 ,28,28)
nn.MaxPool2d(2)) #池化层,output shape(batch_size, 16 ,14,14)
第二层网络:输入大小为(batch_size,16,14,14),输出为大小为(batch_size, 32 ,7,7)
self.layer2 = nn.Sequential(
nn.Conv2d(16,32,5,1,2), #卷积层,output shape(batch_size, 32 ,14,14)
nn.BatchNorm2d(32), #output shape(batch_size, 32 ,14,14)
nn.ReLU(),
nn.MaxPool2d(2) #output shape(batch_size, 32 ,7,7)
)
第三层网络为全连接层,故需要对第二层网络的输出out进行数据处理(out.view(out.size(0), -1)),以(bach_size,7×7×32)的大小输入,经过全连接层后输出的结果大小为(batch_size,10)
self.linear = nn.Linear(7*7*32, 10)
(除此之外,因为网络中用到了BN层,其目的是为了防止训练过程中各网络层的输入落入饱和区域而导致的梯度消失。所以在测试的时候,需要进入评估模式(net.eval()),评估模式下的BN层的均值和方差为整个训练集的均值和方差,而训练模式下的BN层的均值和方差为batch_size的均值和方差)
2.3 循环神经网络
长短期记忆(Long Short-Term Memory, LSTM)网络是循环神经网络的一种形式,也是当前较为流行的循环神经网络的实现方式。网络结构如下:
self.lstm = torch.nn.LSTM(
input_size=input_size,
hidden_size=32, # rnn hidden unit
num_layers=2, # number of rnn layer
batch_first=True, # input & output will has batch size as 1s dimension. e.g. (batch, time_step, input_size)
)
self.fc = torch.nn.Linear(hidden_size, output_size)
本例中因为batch_first为True,故循环神经网络的输入为(batch_size, sequence_length/time_step(本例中为图片的长/行数), input_size(图片的宽/列数)),因为从data_loader提取的images大小为(batch_size,1,sequence_length,input_size),故需要对images进行处理后再输入神经网络
images.view(-1, sequence_length, input_size)
在前向传播函数forward(self, x)中,
r_out, (h0, c0) = self.lstm(x, None) #out_r shape(batch_size, sequence_length/time_step, hidden_size),h0/c0 shape (num_layer, batch_size, hidden_size)
out = self.fc(r_out[:,-1,:]) #out shape (batch_size, hidden_size)
LSTM网络其实是将一幅图片的28×28的矩阵从第一行到最后一行进行扫描循环输入,所以可以理解为最后一行的输出结果已经记住了前面所有有用的数据关系,故LSTM网络的输出r_out中,取第一个维度(batch_size)的全部,第二个维度的最后一行(sequence_length)和第三个维度(hidden_size)的全部作为全连接层的输入。
Note: 在书中147页分析out大小时写到“这个片(out)是一个二维张量,第一个维度是batch_size,第二个维度是input_size,尺寸为[100, 28]”,但通过我的分析感觉作者应该写错了,因为如果第二个维度是input_size,很明显不符合全连接层的输入大小(hidden_size)。于是我又跑去看了莫烦的代码,发现注释也为r_out shape (batch, time_step, output_size),但在用循环神经网络实现回归问题那一节,对该注释进行了说明和修改。通过print(r_out.shape)之后,其结果为torch.Size([100, 28, 32]),故确实应该为r_out shape (batch, time_step, hidden_size)
3 回归问题
书中没有用循环神经网络解决回归问题的例子,但莫烦的教学视频中有这么一节(点此开始学习),通过学习,对回归问题和循环神经网络都有了更深的认识。
3.1 全连接神经网络
首先看一下书中所举的回归问题:拟合15个样本点
linear_regression.py仅仅用了一个线性函数进行训练,拟合出了一条直线(如下图)。Loss为0.1696
然后我加了一个激励函数,略微改变了神经网络结构后拟合出一条曲线(因为加入了非线性激励函数,故此时就非直线了),loss为0.1669。
网络参数如下:
LR(
(linear): Linear(in_features=1, out_features=128, bias=True)
(relu): ReLU()
(linear2): Linear(in_features=128, out_features=1, bias=True)
)
如下图所示,可以看出左边的图略微有点奇怪(作为小白的我最开始百思不得其解),是因为样本点的x数据不是按照从小到大排列的,故在画图的时候需要先对数据进行排序处理,如下所示代码,最后画出的结果为右图所示。
x_train_net = np.sort(x_train, axis=0)
predicted = model(torch.from_numpy(x_train_net)).data.numpy()
plt.plot(x_train, y_train, 'ro', label='Original data')
plt.plot(x_train_net, predicted, label='Fitted line')
由图和LOSS可以看出,非线性拟合效果比线性拟合要好一丢丢。
3.2 循环神经网络
通过对莫凡视频的学习,我感觉循环神经网络所解决的并不是上面那种对散点的拟合,而是利用循环神经网络具有记忆的特点,对序列话数据进行预测。如视频所所举的例子,用一个sin曲线来预测一个cos曲线,如下图所示:
用循环神经网络解决此处的回归问题与解决上述的分类问题不同的地方主要在于网络的输入,下面是神经网络的前向传递函数:
def forward(self, x, hidden_state):
out_r, hidden_state = self.lstm(x, hidden_state)
outs = []
for i in range(out_r.size(1)):
out = self.linear(out_r[:,i,:])
outs.append(out)
return torch.stack(outs,dim=1), hidden_state
可以看到此处的网络比之前多输入了一个hidden_state(我理解为神经网络对之前的记忆),大小为(num_layer, batch_size, time_step),记录了隐藏层神经元的最后参数。在解决分类问题时,hidden_size输入为0,因为每输入一批图片,不需要有上一批图片的记忆,而此处要预测这一时刻的cos曲线,除了这一时刻的输入,当然还需要上一时刻的信息,故函数输出的hidden_state将会作为下一时刻的输入。除此之外,可以看出输入out并不是分类问题中的最后一个time_step中的数据(out([:, -1, :])),而是循环将每一个time_step中的数据传入线性层,然后全部输出与targets计算损失。