本文为 PyTorch 学习笔记,讲解高级神经网络结构。欢迎在评论区与我交流
卷积神经网络简写为 CNN。卷积的意思是不再对每个像素的信息做处理,而是对一小块像素区域的信息做处理。这种做法加强了图片信息的连续性,使得神经网络能够看到图形而非一个点,也加深了神经网络对图片的理解。
具体来说,如上图,神经网络的批量过滤器在图片上滚动收集信息,每次只收集一小块像素区域,然后进行整理,此时神经网络能看到边缘的图片信息,继续扫过边缘信息总结更高层的信息结构,可以画出眼睛鼻子等,再经过一次过滤,脸部信息从这些信息中被总结出来。最后放入全连接神经网络进行分类。
在每次卷积时,神经层会无意中丢失一些信息(角上的信息),池化可以解决这个问题。在每次卷积时不进行压缩,尽量保留更多的信息,把压缩的任务交给池化。能有效地提高准确性。
比较经典的结构是,输入图片,经过一层卷积层,再用最大池化处理卷积信息。然后经过一次同样的处理,将这次的信息传入两层全连接的神经层。最后再接一个分类器进行分类预测。
关于卷积神经网络的详细内容见【卷积神经网络】。
我们使用 MNIST 上的数据集进行训练,构建卷积神经网络对手写数字进行识别。
首先引入我们需要的模块:
pimport os
import torch
import torch.nn as nn # 简化书写格式
import torch.utils.data as Data
import torchvision # 数据库模块
import matplotlib.pyplot as plt
然后进行超参数的设置:
EPOCH = 1 # 训练整批数据多少次, 为了节约时间只训练一次
BATCH_SIZE = 50 # mini-batch
LR = 0.001 # 学习率
DOWNLOAD_MNIST = True # 如果你已经下载好了mnist数据就写上False
从 MNIST 上下载手写数据集:
# Mnist digits dataset
if not(os.path.exists('./mnist/')) or not os.listdir('./mnist/'):
# not mnist dir or mnist is empyt dir
DOWNLOAD_MNIST = True
train_data = torchvision.datasets.MNIST(
root='./mnist/', # 保存或者提取位置
train=True, # True返回6w个训练集。如果是False则返回test_data,有1w个
# 转换PIL.Image或numpy.ndarray成orch.FloatTensor(C x H x W)
# 训练的时候归一化成[0.0, 1.0]区间,彩色图像为0-255,这里使用灰度图
transform=torchvision.transforms.ToTensor(),
download=DOWNLOAD_MNIST,# 没下载就下载, 下载了就不用再下了
)
运行程序后,可以看到正在下载数据:
可以看到数据成功下载到当前目录下:
我们绘制出 train_data
:
print(train_data.data.size()) # (60000, 28, 28)
print(train_data.targets.size()) # (60000)
# 画出train_data第一张图片
plt.imshow(train_data.data[0].numpy(), cmap='gray')
plt.title('%i' % train_data.targets[0])
plt.show()
得到第一张图片为:
使训练变成小批,图像维度为 (50, 1, 28, 28)
:
train_loader = Data.DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
创建测试数据集,为了节约时间, 我们测试时只测试前 2000 个,手动将深度压缩成 0-1 之间:
test_data = torchvision.datasets.MNIST(root='./mnist/', train=False)
test_x = torch.unsqueeze(test_data.test_data, dim=1).type(torch.FloatTensor)[:2000]/255. # shape from (2000, 28, 28) to (2000, 1, 28, 28), value in range(0,1)
test_y = test_data.test_labels[:2000]
建立 CNN 网络,定义 2 个卷积层和 1 个全连接层:
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential( # 输入维度(1, 28, 28),第一维为通道
nn.Conv2d(
in_channels=1, # 输入图像的高度,灰度图为1
out_channels=16, # 输出图像的深度,即过滤器的个数
kernel_size=5, # 过滤器size为5×5
stride=1, # 步幅
padding=2, # 设置padding,使得长宽不变
), # 输出维度(16, 28, 28)
nn.ReLU(), # 激活函数
nn.MaxPool2d(kernel_size=2), # 输出维度(16, 14, 14),长宽减小1倍
)
self.conv2 = nn.Sequential( # 输入维度(16, 14, 14)
nn.Conv2d(16, 32, 5, 1, 2), # 输出维度(32, 14, 14)
nn.ReLU(), # 激活函数
nn.MaxPool2d(2), # 输出维度(32, 7, 7),减小了1倍
)
self.out = nn.Linear(32 * 7 * 7, 10) # 全连接层, 10类
将神经网络输出的数据展开,并输出结果:
class CNN(nn.Module):
...
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x) # (batch, 32, 7, 7)
x = x.view(x.size(0), -1) # 展平多维的卷积图成 (batch_size, 32 * 7 * 7)
output = self.out(x)
return output
cnn = CNN()
print(cnn)
得到的神经网络结构为:
用优化器进行优化,这里使用 Adam 优化器:
optimizer = torch.optim.Adam(cnn.parameters(), lr=LR) # optimize all cnn parameters
loss_func = nn.CrossEntropyLoss() # the target label is not one-hotted
使用 sklearn 进行可视化(可选):
# following function (plot_with_labels) is for visualization, can be ignored if not interested
from matplotlib import cm
try: from sklearn.manifold import TSNE; HAS_SK = True
except: HAS_SK = False; print('Please install sklearn for layer visualization')
def plot_with_labels(lowDWeights, labels):
plt.cla()
X, Y = lowDWeights[:, 0], lowDWeights[:, 1]
for x, y, s in zip(X, Y, labels):
c = cm.rainbow(int(255 * s / 9)); plt.text(x, y, s, backgroundcolor=c, fontsize=9)
plt.xlim(X.min(), X.max()); plt.ylim(Y.min(), Y.max()); plt.title('Visualize last layer'); plt.show(); plt.pause(0.01)
然后使用随机梯度下降法进行训练,与普通神经网络相同。然后每 50 步看一下训练效果,检测 test_data
中有多少图片预测正确:
plt.ion()
# training and testing
for epoch in range(EPOCH):
for step, (b_x, b_y) in enumerate(train_loader): # gives batch data, normalize x when iterate train_loader
output = cnn(b_x)[0] # cnn output
loss = loss_func(output, b_y) # cross entropy loss
optimizer.zero_grad() # clear gradients for this training step
loss.backward() # backpropagation, compute gradients
optimizer.step() # apply gradients
if step % 50 == 0:
test_output, last_layer = cnn(test_x)
pred_y = torch.max(test_output, 1)[1].data.numpy()
accuracy = float((pred_y == test_y.data.numpy()).astype(int).sum()) / float(test_y.size(0))
print('Epoch: ', epoch, '| train loss: %.4f' % loss.data.numpy(), '| test accuracy: %.2f' % accuracy)
if HAS_SK:
# Visualization of trained flatten layer (T-SNE)
tsne = TSNE(perplexity=30, n_components=2, init='pca', n_iter=5000)
plot_only = 500
low_dim_embs = tsne.fit_transform(last_layer.data.numpy()[:plot_only, :])
labels = test_y.numpy()[:plot_only]
plot_with_labels(low_dim_embs, labels)
plt.ioff()
最后在输出中放 10 个测试数据,输出预测数据,与真实数据进行比较:
test_output, _ = cnn(test_x[:10])
pred_y = torch.max(test_output, 1)[1].data.numpy()
print(pred_y, 'prediction number')
print(test_y[:10].numpy(), 'real number')
运行后得到结果。初始阶段代价函数下降,并且准确度逐渐提高:
最终准确度达到 98%,并且对 10 个预测数据全部正确:
现在我们有一组序列数据 Data0-3,普通的神经网络在预测 Result i 时会基于 Datai,,每次使用的神经网络都是同一个,但普通的神经网络并不能了解到数据间的关联。
为了让神经网络了解到数据间的关联,神经网络需要记住之前的产生的数据。在 RNN 中输入 X ( t ) X(t) X(t),都会产生一个当前状态的描述 S ( t ) S(t) S(t),输入 X ( t + 1 ) X(t+1) X(t+1) 时产生 S ( t + 1 ) S(t+1) S(t+1),而 Y ( t + 1 ) Y(t+1) Y(t+1) 由 S ( t ) , S ( t + 1 ) S(t),S(t+1) S(t),S(t+1) 共同产生。通常 RNN 可以表达为下图的形式:
RNN 的形式有很多,常见的形式如下:
RNN 可以用于描述照片,写学术论文,写脚本,甚至写一段音乐。关于 RNN 的详细介绍见【循环神经网络】。
当 RNN 输入序列很长时,误差的反向传递在每一步都会乘参数 W W W
因此普通 RNN 无法处理输入序列过长的情况。
LSTM 可以解决上面的问题。LSTM 比原始 RNN 多了 3 个控制器,分别为输入、输出、忘记。LSTM 多了一个控制全局的记忆,可以想象为电影的主线剧情,原来的 RNN 就是分线剧情。
因此主线剧情的更新取决于输入和忘记控制器。输出控制器根据当前的主线剧情和分线剧情判断输出。
因此 LSTM 可以缓解 RNN 中记忆的衰退。关于 LSTM 的详细讲解见【循环神经网络】。
这里我们仍然对手写数字进行预测。但 RNN 一般是用在时间序列上的,我们可以想象在读图片时,我们是从上往下看的,因此 RNN 第一个时间点的输入信息就是图片第一行的像素信息,直到最后一行(28 行),得到图片包含的数字是多少。
首先引入包:
import torch
from torch import nn
from torch.autograd import
import torchvision.datasets as dsets
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
torch.manual_seed(1) # 可复写的
定义超参数:
EPOCH = 1 # 训练整批数据多少次, 为了节约时间只训练一次
BATCH_SIZE = 64 # 批训练数量
TIME_STEP = 28 # rnn 时间步数 / 图片高度
INPUT_SIZE = 28 # rnn 每步输入值 / 图片每行像素
LR = 0.01 # 学习率
DOWNLOAD_MNIST = True # 如果已经下载好了mnist数据置 Fasle
下载数据集,放在 ./mnist
下,并将数据转为 tensor
的形式,决定是否下载:
train_data = dsets.MNIST(
root='./mnist/', # 保存或者提取位置
train=True, # 是否为训练数据
transform=transforms.ToTensor(), # 转换 PIL.Image or numpy.ndarray 成
# torch.FloatTensor (C x H x W), 训练的时候 normalize 成 [0.0, 1.0] 区间
download=DOWNLOAD_MNIST, # 决定是否下载
)
画出训练集中的一个例子:
print(train_data.train_data.size()) # (60000, 28, 28)
print(train_data.train_labels.size()) # (60000)
plt.imshow(train_data.train_data[0].numpy(), cmap='gray')
plt.title('%i' % train_data.train_labels[0])
plt.show()
得到 5 的手写体:
然后一批批训练数据:
train_loader = torch.utils.data.DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
生成测试数据,与前面的 CNN 相同:
test_data = dsets.MNIST(root='./mnist/', train=False, transform=transforms.ToTensor())
test_x = test_data.test_data.type(torch.FloatTensor)[:2000]/255. # shape (2000, 28, 28) value in range(0,1)
test_y = test_data.test_labels.numpy()[:2000] # 转换为 numpy array
然后定义 RNN,继承 __init__
,使用 LSTM 的 RNN 形式。每次输入 x
返回隐藏状态 h
,下一次输入时同时传入 h
和 x
。因为我们是看完整张图片后判断结果,因此只需要最后一个时间点的输出。
class RNN(nn.Module):
def __init__(self):
super(RNN, self).__init__()
self.rnn = nn.LSTM( # 使用 nn.RNN() 很难学习
input_size=INPUT_SIZE,
hidden_size=64, # rnn 隐藏单元
num_layers=1, # rnn 的层数,数字越大 rnn 更强大,但训练时间会增加
batch_first=True, # 输入输出数据将 batch 大小作为第一维 e.g. (batch, time_step, input_size)
)
self.out = nn.Linear(64, 10) # 对输出数据进行处理,接到全连接的 神经层上
def forward(self, x):
# 一批数据 x 的形式 (batch, time_step, input_size)
# r_out 形式 (batch, time_step, output_size)
# lstm 的 rnn 中隐藏状态有两个
# h_n 为分线剧情的隐藏状态,形式为 (n_layers, batch, hidden_size)
# h_c 为主线剧情的隐藏状态,形式为 (n_layers, batch, hidden_size)
r_out, (h_n, h_c) = self.rnn(x, None) # None 表示没有初始化隐藏层状态,在 rnn 回归中具体探讨
out = self.out(r_out[:, -1, :]) # 最后一个时刻的输出
return out
打印出 RNN 结构:
rnn = RNN()
print(rnn)
得到结果:
使用 Adam 算法作为优化器。用交叉熵损失计算误差,不是以 one-hot 的形式,而是标签与数字相同,但会在内部转换为 one-hot 的形式 :
optimizer = torch.optim.Adam(rnn.parameters(), lr=LR) # 优化所有的 rnn 参数
loss_func = nn.CrossEntropyLoss() # 目标标记不是 one-hot 类型
进行训练和测试:
for epoch in range(EPOCH):
for step, (b_x, b_y) in enumerate(train_loader): # 返回 batch 数据
b_x = b_x.view(-1, 28, 28) # 将 x 转换为 (batch, time_step, input_size) 形式
output = rnn(b_x) # 输入数据
loss = loss_func(output, b_y) # 计算误差
optimizer.zero_grad() # 对这次训练步骤的梯度清零
loss.backward() # 反向传播,计算梯度
optimizer.step() # 用梯度进行优化
# 每 50 步打印训练过程
if step % 50 == 0:
test_output = rnn(test_x) # (samples, time_step, input_size)
pred_y = torch.max(test_output, 1)[1].data.numpy()
accuracy = float((pred_y == test_y).astype(int).sum()) / float(test_y.size)
print('Epoch: ', epoch, '| train loss: %.4f' % loss.data.numpy(), '| test accuracy: %.2f' % accuracy) # 打印误差和准确度
预测前 10 个数据:
test_output = rnn(test_x[:10].view(-1, 28, 28))
pred_y = torch.max(test_output, 1)[1].data.numpy()
print(pred_y, 'prediction number')
print(test_y[:10], 'real number')
得到训练过程的结果,RNN 的准确率较 CNN 低一些,但基本都能预测正确:
我们用 sin \sin sin 的曲线来预测 cos \cos cos 的曲线,将 RNN 在每个时间点上的输出与真实数据做比较。这部分的代码与前面 RNN 作分类的代码相似,接下来进行详细讲解。
首先引入包:
import torch
from torch import nn
import numpy as np
import matplotlib.pyplot as plt
定义超参数:
TIME_STEP = 10 # rnn time step
INPUT_SIZE = 1 # 每个时间点只有一个输入
LR = 0.02 # 学习率
生成 sin \sin sin 和 cos \cos cos 的数据,并绘制图像:
steps = np.linspace(0, np.pi*2, 100, dtype=np.float32) # float32 for converting torch FloatTensor
x_np = np.sin(steps)
y_np = np.cos(steps)
plt.plot(steps, y_np, 'r-', label='target (cos)')
plt.plot(steps, x_np, 'b-', label='input (sin)')
plt.legend(loc='best')
plt.show()
绘图结果为:
定义 RNN 模型,作动态计算图:
class RNN(nn.Module):
def __init__(self):
super(RNN, self).__init__()
self.rnn = nn.RNN(
input_size=INPUT_SIZE,
hidden_size=32, # rnn 隐藏单元个数
num_layers=1, # rnn 层数
batch_first=True, # (batch, time_step, input_size)
)
self.out = nn.Linear(32, 1) # 32 接上一时刻 rnn 的输出
def forward(self, x, h_state): # 作为下一次的 h_state
# 用当前的输入 x 和记忆 h_state 共同推导
# x (batch, time_step, input_size)
# h_state (n_layers, batch, hidden_size)
# r_out (batch, time_step, hidden_size)
r_out, h_state = self.rnn(x, h_state)
outs = [] # save all predictions
# 作动态计算图
for time_step in range(r_out.size(1)): # calculate output for each time step
outs.append(self.out(r_out[:, time_step, :]))
return torch.stack(outs, dim=1), h_state # 变成 tensor 形式
rnn = RNN()
print(rnn)
定义优化器和损失函数:
optimizer = torch.optim.Adam(rnn.parameters(), lr=LR) # 优化所有 rnn 参数
loss_func = nn.MSELoss()
进行训练,并绘制训练的过程动态图:
plt.figure(1, figsize=(12, 5))
plt.ion() # continuously plot
# 训练和测试
for step in range(100):
start, end = step * np.pi, (step+1)*np.pi # time range
# use sin predicts cos
steps = np.linspace(start, end, TIME_STEP, dtype=np.float32, endpoint=False) # float32 for converting torch FloatTensor
x_np = np.sin(steps)
y_np = np.cos(steps)
# 写成 variable 形式,加维度
x = torch.from_numpy(x_np[np.newaxis, :, np.newaxis]) # shape (batch, time_step, input_size)
y = torch.from_numpy(y_np[np.newaxis, :, np.newaxis])
prediction, h_state = rnn(x, h_state) # rnn output
# !! next step is important !!
h_state = h_state.data # repack the hidden state, break the connection from last iteration
loss = loss_func(prediction, y) # 计算损失函数
optimizer.zero_grad() # 清空梯度
loss.backward() # 反向传播,计算梯度
optimizer.step() # 使用梯度下降
# plotting
plt.plot(steps, y_np.flatten(), 'r-')
plt.plot(steps, prediction.data.numpy().flatten(), 'b-')
plt.draw(); plt.pause(0.05)
plt.ioff()
plt.show()
从结果中可以看出,我们使用蓝色的 sin \sin sin 曲线拟合红色的 cos \cos cos 曲线,只训练了 60 次就可以得到很好的结果了:
持续更新……
持续更新……
神经网络有很多种,前向传播网络、分析图片的 CNN,分析序列化数据的 RNN。这些网络都是输入数据得到想要的结果,可以将数据和结果通过某种关系联系起来。
而生成网络则是凭空捏造结果的,GAN 就是其中的一种。GAN 利用无意义的随机数来生成有意义的作品。下面举一个例子解释其原理:新手画家用随机灵感画画,新手鉴赏家接收到一些画作,但他分不清新手画家的作品和著名画作,但会说出自己的判断,由我们来纠正他的判断。新手鉴赏家一边学习如何判断,一边告诉新手画家该如何画得像著名画作,于是新手画家就学会了如何从自己的灵感画出更好的画作了。
Generator 会根据随机数生成有意义的数据,Discriminator 学会分辨真实数据和生成数据,将学习到的经验反向传递给 Generator。
关于 GAN 的详细讲解见【生成对抗网络】。
这里著名画家画的画在蓝色曲线和红色曲线之间一元二次函数的完美曲线,而新手画家的画最开始是一些随机的噪声,最后学习到和著名画家差不多的曲线:
首先引入包:
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
定义超参数:
BATCH_SIZE = 64
LR_G = 0.0001 # 生成器的学习率
LR_D = 0.0001 # 判别器的学习率
N_IDEAS = 5 # think of this as number of ideas for generating an art work (Generator)
ART_COMPONENTS = 15 # it could be total point G can draw in the canvas
# [-1,1] 间的 15 个点
PAINT_POINTS = np.vstack([np.linspace(-1, 1, ART_COMPONENTS) for _ in range(BATCH_SIZE)])
画出画家的曲线:
plt.plot(PAINT_POINTS[0], 2 * np.power(PAINT_POINTS[0], 2) + 1, c='#74BCFF', lw=3, label='upper bound')
plt.plot(PAINT_POINTS[0], 1 * np.power(PAINT_POINTS[0], 2) + 0, c='#FF9359', lw=3, label='lower bound')
plt.legend(loc='upper right')
plt.show()
得到画家曲线为:
中间的曲线由下面的方程定义,生成一批著名画家作品:
def artist_works():
a = np.random.uniform(1, 2, size=BATCH_SIZE)[:, np.newaxis]
# 用 15 个点产生一元二次函数,a 为函数参数
paintings = a * np.power(PAINT_POINTS, 2) + (a-1)
paintings = torch.from_numpy(paintings).float() # 转换为 torch 形式
return paintings
生成器学习画作,产生类似的数据,直接使用 Sequential
建立神经网络:
G = nn.Sequential(
nn.Linear(N_IDEAS, 128), # 输入随机想法
nn.ReLU(),
nn.Linear(128, ART_COMPONENTS), # 生成 15 个 y 轴的点,形成 15 个线段
)
同样使用 Sequential
建立判别器神经网络,将输出转换为是否为著名画作的概率:
D = nn.Sequential(
nn.Linear(ART_COMPONENTS, 128), # 接受画中的 15 个点
nn.ReLU(),
nn.Linear(128, 1), # 判别接受的画是否为著名画作
nn.Sigmoid(), # 转换为百分比形式
)
建立 2 个神经网络的优化器:
opt_D = torch.optim.Adam(D.parameters(), lr=LR_D)
opt_G = torch.optim.Adam(G.parameters(), lr=LR_G)
接下来进行学习:
for step in range(10000):
artist_paintings = artist_works() # 著名画作
G_ideas = torch.randn(BATCH_SIZE, N_IDEAS, requires_grad=True) # 随机数作为灵感
G_paintings = G(G_ideas) # 生成新手画家的画作
prob_artist1 = D(G_paintings) # 生成作品的概率
# 增加生成作品概率
G_loss = torch.mean(torch.log(1. - prob_artist1))
opt_G.zero_grad()
G_loss.backward()
opt_G.step()
prob_artist0 = D(artist_paintings) # 著名画作概率
prob_artist1 = D(G_paintings.detach()) # 生成画作概率
# 尽量增加著名画作,降低生成画作。因为只能最小化误差,加负号
D_loss = - torch.mean(torch.log(prob_artist0) + torch.log(1. - prob_artist1))
opt_D.zero_grad()
D_loss.backward(retain_graph=True) # 保留网络的参数另一部分网络的反向传播使用
opt_D.step()
if step % 50 == 0: # plotting
plt.cla()
plt.plot(PAINT_POINTS[0], G_paintings.data.numpy()[0], c='#4AD631', lw=3, label='Generated painting', )
plt.plot(PAINT_POINTS[0], 2 * np.power(PAINT_POINTS[0], 2) + 1, c='#74BCFF', lw=3, label='upper bound')
plt.plot(PAINT_POINTS[0], 1 * np.power(PAINT_POINTS[0], 2) + 0, c='#FF9359', lw=3, label='lower bound')
plt.text(-.5, 2.3, 'D accuracy=%.2f (0.5 for D to converge)' % prob_artist0.data.numpy().mean(),
fontdict={'size': 13})
plt.text(-.5, 2, 'D score= %.2f (-1.38 for G to converge)' % -D_loss.data.numpy(), fontdict={'size': 13})
plt.ylim((0, 3));
plt.legend(loc='upper right', fontsize=10);
plt.draw();
plt.pause(0.01)
plt.ioff()
plt.show()
起初生成器曲线很曲折,并且会经常超出上下限:
但经过多次训练后,生成器最终形成了近似完美的曲线,并落在上下限之间:
有帮助的话点个赞加关注吧