1,其他网络
1.1,循环神经网络
假设存在两个句子:
- The cat sat on the mat,(小猫坐在垫子上)
- She got up and impatiently climbed on the chair,meowing for food. (它站起来,急不可耐地爬上椅子,冲着食物喵喵叫)
假设这两个句子先后输入一个CNN,并且问 “where is the cat?”(小猫在哪里?),就会有一个问题,因为网络没有记忆的概念。处理时域数据时这一点极其重要(例如,文本、语音、视频和时序数据)。循环神经网络(RNN)可以解决这个问题,它通过隐藏状态(hidden state)为神经网络提供记忆。
RNN可以简单想象成混合了一个for循环的神经网络:
在时间步t增加输入,然后得到一个隐藏(hidden)输出状态h(t),这个状态还要在下一个时间步再输入到RNN。
这里有一组全连接层(有共享的参数)、一系列输入,以及输出。向网络提供输入书,然后预测序列的下一项作为输出。在这个展开的视图中可以看到,可以认为RNN是一个全连接层流水线,后续输入提供给序列中的下一层(层之间会插入通常的非线性激活函数,如ReLU)。有了完整的预测序列,必须将误差通过RNN反向传播(BPTT)。在整个序列上计算误差,如上图所示,为每个时间步计算梯度,并组合这些梯度来更新网络的共享参数。可以把它想象为在各个网络上完成反向传播,再把所有梯度累加起来。
1.2,长短期记忆网络
在实际上,不论是以前还是现在,RNN都存在梯度消失的问题,或者可能更糟糕,会有梯度爆炸的情况,即误差趋向于无穷大。这两种情况都不好,所以尽管人们认为很多问题对RNN是适用的,但实际上RNN并不能用来解决这些问题。而LSTM网络的出现,才解决了这些问题。
在一个标准RNN中,我们会永远记住所有一切。不过,我们的大脑并不是这样做的,LSTM的遗忘门允许我们模拟这种想法,即随着在输入链中不断深入,这个链开始的部分会变得不那么重要。LSTM的遗忘程度要在训练中学习,所以如果网络强调非常健忘,遗忘门参数就要保证这一点。
单元最后会成为网络层的“记忆”;输入、输出和遗忘门会确定数据如何流过一层。数据可以简单地直接通过,它可以“写入”单元,而且这个数据可以(或者不可以)流到下一层,由输出门修改。各个部分的这种组合足以解决梯度消失的问题,而且还有一个好处是可以做图灵完备,所以理论上讲,可以利用这样一个网络完成能够在计算机上完成的任何计算。
1.3,门控循环单元
GRU的要点是将遗忘门与输出门合并。这意味着,它的参数比LSTM少,所以往往训练更快,而且在运行时使用的资源更少。由于这些原因,另外因为它们基本上可以直接替代LSTM,所以变得相当流行。不过,严格地讲,由于合并了遗忘门和输出门,GRU不如LSTM能力强,所以一般建议GRU和LSTM都可以进行尝试,看看哪一个表现更好。或许承认LSTM训练时可能慢一些,不过最后会得到更好的结果。
1.4,双向长期记忆网络
LSTM的另一个常见的变形是双向或简写为biLSTM。目前为止我们已经看到,传统LSTM(和一般的RNN)会在训练和做决策时查看过去。遗憾的是,有时你还需要查看将来。在翻译或手写识别之类的应用中尤其如此,这些应用中,当前状态之后的情况对于确定输出可能与前一个状态同样重要。
biLSTM用最简单的方式解决这个问题,它实际上是两个堆叠LSTM,一个LSTM中输入向前传递,另一个LSTM中输入向后传递。
使用PyTorch可以很容易地创建biLSTM,只需要在创建一个LSTM()单元时传入参数bidirectional=True。
2,TensorBoard
2.1,安装TensorBoard
TensorBoard是一个Web应用,设计用来对神经网络的不同方面进行可视化。利用TensorBoard可以很容易地实时查看统计信息,如准确度、损失激活值,实际上可以查看你希望通过网络发送的任何信息。尽管它本身是用TensorFolow编写的,但有一个跨系统而且相当简单的API,在PyTorch中使用与在TensorFlow中使用并没有太大差别。
安装:
#要求PyTorch版本在v1.14以上
pip install tensorboard
conda install tensorboard
启动:
tensorboard --logdir=runs
=========================================================================
TensorBoard 1.14.0 at http://DESKTOP-3T77H9A:6006/ (Press CTRL+C to quit)
根据提示进入网站,可以看到如下欢迎界面:
2.2,向TensorBoard发送数据
PyTorch中使用TensorBoard的模块在torch.utils.tensorboard中:
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter()
writer.add_scalar('example',3)
使用SummaryWriter类与TensorBoard交互,这里使用标准位置./runs记录输出,另外使用add_scalar并提供一个标记来发送一个标量。
在PyCharm中打开TensorBoard:
加一层循环:
from torch.utils.tensorboard import SummaryWriter
import random
value = 10
writer = SummaryWriter()
writer.add_scalar('test_loop', 0)
for i in range(1, 10000):
value += random.random() - 0.5
writer.add_scalar('test_loop', value, i)
2.3,PyTorch钩子
PyTorch提供了钩子(hook),这是在向前向后传播时看,可以附加到一个张量或模块的函数。PyTorch在传播中遇到一个带钩子的模块时,它会调用所注册的钩子。计算一个张量的梯度时,就会调用这个张量上注册的钩子。
钩子提供了很多可能很强大的方式来管理模块和张量,因为如果需要,可以完全替换进入钩子的(前一层)输出。可以改变梯度、屏蔽激活、替换模块中的所有偏置等。不过这里只是使用钩子在数据流过时得到网络的有关信息。
import torch.utils.data
import torchvision
def print_hook(module,input,output):
print(input[0].shape)
net = torchvision.models.resnet18()
x = torch.rand(1,3,224,224)
hook_ref = net.fc.register_forward_hook(print_hook)
net(x)
hook_ref.remove()
net(x)
=================================
torch.Size([1, 512])
程序结果打印出一些文本,显示模型全连接层的输入形状。注意,第二次向模型传入张量,不会看到print语句。向一个模块或张量增加一个钩子时,PyTorch会返回这个钩子的一个引用。总是应当保存这个引用,然后工作完成时调用remove()。如果没有保存这个引用,它就会在内存中“游荡”,而占用宝贵的内存。反向钩子的做法类似,只不过要调用register_backward_hook()。
2.4,均值和标准差
建立一个函数,将一个输出层的均值和标准差发送给TensorBoard:
def send_stats(i, module, input, output):
writer.add_scalar(f"layer {i}-mean", output.data.mean())
writer.add_scalar(f"layer {i}-stddev", output.data.std())
不能用这个函数本身建立一个前向钩子,不过通过使用Python函数partial(),可以创建一系列前向钩子,将它们附加到指定i值(partial()的第二个参数)的一个层,从而确保把正确的值传送到TensorBoard中正确的图中:
for i, m in enumerate(model.children()):
m.register_forward_hook(partial(send_stats, i))
注意,这里使用了model.children(),这样只会附加到模型的各个顶层模块,所以如果有一个nn.Sequential()层(基于ResNet的模型都有这样一个层),钩子只会附加到这个模块,而不会附加到nn.Sequential列表中的各个模块。
如果使用通常的训练函数训练这个模型,应该会看到激活值开始流入TensorBoard。必须切换为WALL,因此不再用钩子将训练的步信息发送回TensorBoard(只有在调用PyTorch钩子时才会得到模块信息)。
2.5,获取模型详细信息
理想情况下神经网络中的层应该均值为0,并且标准差为1,从而保证计算不会变成无穷大或者0。如果不是这样,可能这个网络训练有问题。
import torch
import torch.nn as nn
import torch.utils.data
import torchvision
from functools import partial
from torch import optim
from torch.utils.tensorboard import SummaryWriter
from torchvision import datasets, transforms
# Writer will output to ./runs/ directory by default
writer = SummaryWriter()
transform = transforms.Compose(
[transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))]
)
trainset = datasets.MNIST("mnist_train", train=True, download=True, transform=transform)
train_data_loader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
model = torchvision.models.resnet50(False)
model.conv1 = torch.nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
images, labels = next(iter(train_data_loader))
grid = torchvision.utils.make_grid(images)
writer.add_image("images", grid, 0)
writer.add_graph(model, images)
def send_stats(i, module, input, output):
writer.add_scalar(f"layer {i}-mean", output.data.mean())
writer.add_scalar(f"layer {i}-stddev", output.data.std())
for i, m in enumerate(model.children()):
m.register_forward_hook(partial(send_stats, i))
# Now train the model and watch output in Tensorboard
optimizer = optim.Adam(model.parameters(), lr=2e-2)
criterion = nn.CrossEntropyLoss()
def train(model, optimizer, loss_fn, train_loader, val_loader, epochs=20, device="cuda:0"):
model.to(device)
for epoch in range(epochs):
print(f"epoch {epoch + 1}")
model.train()
for batch in train_loader:
optimizer.zero_grad()
ww, target = batch
ww = ww.to(device)
target = target.to(device)
output = model(ww)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
model.eval()
num_correct = 0
num_examples = 0
for batch in val_loader:
ww, target = batch
ww = ww.to(device)
target = target.to(device)
output = model(ww)
correct = torch.eq(torch.max(output, dim=1)[1], target).view(-1)
num_correct += torch.sum(correct).item()
num_examples += correct.shape[0]
print("Epoch {}, accuracy = {:.2f}".format(epoch + 1, num_correct / num_examples))
train(model, optimizer, criterion, train_data_loader, train_data_loader, epochs=5)
3,部署PyTorch
3.1,搭建Flask服务
建立模型只是构建深度学习应用的一部分,毕竟一个模型有那么惊人的准确度(或其他优点),但是如果它从不做任何预测,又有什么价值。因为我们希望有一种简单的方法打包我们的模型,使它们能响应请求(可以通过Web或者其他途径),而且可以通过最少的努力使模型在生产环境中运行。
Python允许我们使用Flask框架(Flask是用Python创建Web服务的一个流行框架)快速地部署和运行一个Web服务,接受一个请求(包含一个图像URL),并返回一个JSON响应,指示预测结果。
conda install -c anaconda flask
pip install flask
创建一个名为catfish的新目录,把你的模型定义作为model.py复制到这个目录中:
import torch.nn as nn
from torchvision import models
CatfishClasses = ["cat","fish"]
CatfishModel = models.resnet50()
CatfishModel.fc = nn.Sequential(nn.Linear(CatfishModel.fc.in_features,500),
nn.ReLU(),
nn.Dropout(), nn.Linear(500,2))
import os
import requests
import torch
from flask import Flask, jsonify, request
from io import BytesIO
from PIL import Image
from torchvision import transforms
from catfish_model import CatfishModel, CatfishClasses
def load_model():
m = CatfishModel
m.eval()
return m
model = load_model()
img_transforms = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
app = Flask(__name__)
@app.route("/")
def status():
return jsonify({"status": "ok"})
@app.route("/predict", methods=['GET', 'POST'])
def predict():
if request.method == 'POST':
img_url = request.form.image_url
else:
img_url = request.args.get('image_url', '')
response = requests.get(img_url)
img = Image.open(BytesIO(response.content))
img_tensor = img_transforms(img).unsqueeze(0)
prediction = model(img_tensor)
predicted_class = CatfishClasses[torch.argmax(prediction)]
return jsonify({"image": img_url, "prediction": predicted_class})
if __name__ == '__main__':
app.run(host="127.0.0.1",port="8080")
Flask的魅力在于@app.route()注解。这些注解允许我们关联平常的Python函数,用户达到某个特定端点时就会运行这些函数。在我们的predict()方法中,从GET或POST HTTP请求取出img_url参数,打开这个URL(作为一个PIL图像),把它推入一个简单的torchvision转换流水线,调整它的大小并把图像转换为一个张量。
这样我们会得到一个形状为[3, 224, 224]的张量,不过由于这个模型的工作方式,需要把它转换为批量大小为1的一个批次,也就是[1, 3, 224, 224]。所以再使用unsqueeze()扩展这个张量,在现在维度前面插入一个新的空轴(维度)。然后像往常一样把它传入模型,从而得到我们的预测张量。与前面的做法一样,我们使用torch.argmax()找到最大值的张量元素,并用这个元素作为索引来访问CatfishClasses数组。最后返回一个JSON响应,其中包括类别名和做预测的图像URL。
如果现在尝试这个服务,可能发现它的分类性能有些失望。因为这个模型没有经过任何训练,下面将填充load_model()来加载神经网络参数。
3.2,设置模型参数
通过之前的了解,知道了训练之后保存模型的两种方法,可以用torch.save()将整个模型写至磁盘,或者使用state_dict()保存模型的所有权重和偏执(但不包括结构)。对于这个基于生产环境的服务,需要加载一个已训练的模型,应该选择state_dict方法。
尽管保存整个模型是一个很有吸引力的选择,但是你会对模型结构的任何改变非常敏感,甚至训练环境目录结构的改变也会产生很大的影响。在其他地方运行的不同服务中加载这个模型时很可能会出现问题。如果迁移到一个稍有不同的布局(结构),肯定不希望一切从头开始。
另外,如果是用state_dicts()保存模型,加载保存的模型时,最好不要硬编码指定模块的文件名,这样就能将模型更新与服务解耦合。这意味着我们可以用一个新模型重启服务,或者也可以很容易地还原到之前的一个模型。
def load_model():
m = CatfishModel
location = "./a.pt"
m.load_state_dict(torch.load(location))
m.eval()
return m
torch.save(net.state_dict(), './data/' + 'model.pt')
|