因为MNIST是个分类问题,所以使用逻辑回归是合适的,同时又是个图像问题,所以使用CNN也是合理的。
接下来我们转向CNN,这里展示了两种方式,因为之前的代码已经把model封装起来了,而其余部分并没有涉及到模型本身,所以基本上只要重新定义一个模型,其余部分都不需要改动。
Switch to CNN
class Mnist_CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)
self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1)
self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1)
def forward(self, xb):
xb = xb.view(-1, 1, 28, 28)
xb = F.relu(self.conv1(xb))
xb = F.relu(self.conv2(xb))
xb = F.relu(self.conv3(xb))
xb = F.avg_pool2d(xb, 4)
return xb.view(-1, xb.size(1))
- 首先,当然,模型类的名字从
Mnist_Logistic改成了Mnist_CNN,名字的变化已经意味着这里从逻辑回归转向了CNN,或者说卷积神经网络。 - 其次,还是两个函数,一个
__init__,一个forward,当然这两个函数定义与逻辑回归的模型肯定是不一样的。大体上,我们可以把__init__理解为定义了一个model的框架,而forward则是在这个框架上处理数据的过程。 - 这里用Conv2d定义了一个3层的卷积网络,注意
__init__定义中并没有对输入数据做任何假定,只是定义了卷积网络本身的层次。 self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)表示这一层的输入channel(in_channels)是1,因为是第一层,输入的是黑白图片,所以in_channels是1,输出channel(out_channels)是16,每次计算 k e r n e l _ s i z e × k e r n e l _ s i z e kernel\_size\times kernel\_size kernel_size×kernel_size也就是 3 × 3 3\times3 3×3的格子,步长为2,以及需要填充边缘,因为padding_mode为缺省值'zeros',所以是用0来填充。其余两层的定义是类似的,就不废话了。- 至于
xb.view(-1, 1, 28, 28),Tensor.view的文档在这里,就这里而言,是把一个torch.Size([64, 784])的张量变形为一个torch.Size([64, 1, 28, 28])的张量。参数列表中的-1表示这个值由其他参数推断,因为变形前后的数据的数量必须保持一致,所以这里就推算出来是64: 64 × 784 = 64 × 1 × 28 × 28 64\times784=64\times 1\times 28\times 28 64×784=64×1×28×28,这个64是每个批次的大小。很容易能够看出来,这里是相当于把本来拉成了一个784维的行向量的 28 × 28 28 \times 28 28×28的图片恢复成了 28 × 28 28 \times 28 28×28。毕竟我们使用的是 3 × 3 3\times3 3×3的卷积核,不能用来处理一个 1 × 784 1\times 784 1×784的行向量。 - 注意这里的
relu和avg_pool2d使用的是torch.nn.functional中的函数。李宏毅老师说以前算力不够的时候,在卷积层之间会加入一些pooling层,但是现在一般都不需要加了。而这里加入的这个avg_pool2d更主要的作用我看还是生成一个合适维度的张量。
接下来,原来的程序再做一些改动,其实也就只需要更新一下get_model():
def get_model():
#model = Mnist_Logistic()
model = Mnist_CNN()
return model, optim.SGD(model.parameters(), lr=lr,momentum=0.9)
lr = 0.1
lr的变化无所谓的,而momentum之前已经提到过了,其他的保持原样即可。
nn.Sequential
接下来是通过nn.Sequential,一个Sequential对象里面包含着许多模块,并且会以线性方式,或者说依照顺序来执行这些模块。
先看代码:
class Lambda(nn.Module):
def __init__(self, func):
super().__init__()
self.func = func
def forward(self, x):
return self.func(x)
def preprocess(x):
return x.view(-1, 1, 28, 28)
model = nn.Sequential(
Lambda(preprocess),
nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.AvgPool2d(4),
Lambda(lambda x: x.view(x.size(0), -1)),
)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
nn.Sequential文档在这里。先看看model这个Sequential对象的定义。
根据文档,Sequential的声明是这样的:
class torch.nn.Sequential(*args: Module),也就是说参数列表是一个nn.Module列表,如前面所说,pytorch中的Module代表一个类,这里的参数类型中的Module则明确的是pytorch中作为所有神经网络模块的基类的那个Module,当然,使用基类作为paramater的目的就是动态绑定,这就不废话了。
所以,我们需要自己创建一个nn.Module的子类Lambda,用来封装view这个函数的调用,并不是说函数就不能作为对象使用,只不过在Sequential中需要的是Module对象,而不是其他任意的对象。因此,前面class Lambda和preprocess定义的目的所在就很清楚了。
当然,需要自定义一层的原因也是因为pytorch中没有一个view层,或者说没有提供一个Module来实现view这个操作。
上面Sequential的参数列表中出现的Lambda(lambda x: x.view(x.size(0), -1))有必要提一句,就是小写字母开头的这个lambda,表示一个lambda表达式,这是内置的机制,跟大写字母开头的Lambda并没有什么关联。至于为什么这里要用Lambda来作为这个view层的名字,不得而知,就这么用吧。
我们这里只看到了构造model时Sequential的参数列表,知道这个Module对象里边有些什么Module,在前面代码我们也看到了最终对model的使用方式是model(xb),这一点,到目前都是一致的,于是很容易就能想到对于一个Sequential对象,这样的调用方式也是一致的,正如Sequential的定义所示,其中定义了这么一个函数或者说方法:
def forward(self, input):
for module in self:
input = module(input)
return input
于是,就很清楚了,这也就是线性的意思。
Wrapping DataLoader
前面的CNN只能用于MNIST,因为:
- 输入限定是 28 × 28 28\times 28 28×28
- CNN最末层输出的是
4
×
4
4\times 4
4×4的网格,这限制了最后使用的pooling核,当然,这条限制是双向的。
以上两条都与输入的数据集的特性有关,从正常的编程逻辑来说,这种数据与操作的深度耦合是需要排除的。
先看代码:
def preprocess(x, y):
return x.view(-1, 1, 28, 28), y
class WrappedDataLoader:
def __init__(self, dl, func):
self.dl = dl
self.func = func
def __len__(self):
return len(self.dl)
def __iter__(self):
batches = iter(self.dl)
for b in batches:
yield (self.func(*b))
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)
model = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.AdaptiveAvgPool2d(1),
Lambda(lambda x: x.view(x.size(0), -1)),
)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
显然,移除最后pooling层的限制很简单,只是把nn.AvgPool2d(4)换成了nn.AdaptiveAvgPool2d(1),而移除第一条限制,则需要把DataLoader做一个封装,如上面代码中所示。
这里只是需要理解关于preprocess(x, y)的定义中的几个数字,毕竟我们程序的操作不能完全脱离数据而存在,这几个数字是可以或者说需要根据数据的特性更改的。
换句话说,我们处理的终究是2维的图片,天然就是一个
行
×
列
行\times列
行×列的结构,而像(N,C,H,W)这样的元祖,本身也是CNN相关的参数的通用形式。
简单说就是,x.view(-1, 1, 28, 28)中的这几个数字是可以根据数据集的特性手工更改的,对于一门像C或者C++那样的编译型语言来说,这样改当然是不可接受的,但是对于python这样的解释型语言或者说脚本语言来说却是很正常的。
Using your GPU
最后的改进就是使用GPU,程序包括两个方面,数据和操作,所以做下面的这些改动就可以了:
dev = torch.device(
"cuda") if torch.cuda.is_available() else torch.device("cpu")
def preprocess(x, y):
return x.view(-1, 1, 28, 28).to(dev), y.to(dev)
model.to(dev)
最后,完整的代码变成了这样:
import torch
import numpy as np
import requests
import pickle
import gzip
import torch.nn.functional as F
from torch import nn
from torch import optim
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
from pathlib import Path
from matplotlib import pyplot
DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"
PATH.mkdir(parents=True, exist_ok=True)
FILENAME = "mnist.pkl.gz"
with gzip.open((PATH / FILENAME).as_posix(), "rb") as f:
((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")
#URL = "https://github.com/pytorch/tutorials/raw/master/_static/"
URL = "https://resources.oreilly.com/live-training/inside-unsupervised-learning/-/raw/9f262477e62c3f5a0aa7eb788e557fc7ad1310de/data/mnist_data/"
if not (PATH / FILENAME).exists():
content = requests.get(URL + FILENAME).content
(PATH / FILENAME).open("wb").write(content)
x_train, y_train, x_valid, y_valid = map(
torch.tensor, (x_train, y_train, x_valid, y_valid)
)
def get_data(train_ds, valid_ds, bs):
return (
DataLoader(train_ds, batch_size=bs, shuffle=True),
DataLoader(valid_ds, batch_size=bs * 2),
)
def loss_batch(model, loss_func, xb, yb, opt=None):
loss = loss_func(model(xb), yb)
if opt is not None:
loss.backward()
opt.step()
opt.zero_grad()
return loss.item(), len(xb)
def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
for epoch in range(epochs):
model.train()
for xb, yb in train_dl:
loss_batch(model, loss_func, xb, yb, opt)
model.eval()
with torch.no_grad():
losses, nums = zip(
*[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl]
)
val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums)
print(epoch, val_loss)
def preprocess(x, y):
return x.view(-1, 1, 28, 28).to(dev), y.to(dev)
class WrappedDataLoader:
def __init__(self, dl, func):
self.dl = dl
self.func = func
def __len__(self):
return len(self.dl)
def __iter__(self):
batches = iter(self.dl)
for b in batches:
yield (self.func(*b))
model = nn.Sequential(
nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.AdaptiveAvgPool2d(1),
Lambda(lambda x: x.view(x.size(0), -1)),
)
lr = 0.1
epochs = 2 # how many epochs to train for
bs = 64
dev = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
train_ds = TensorDataset(x_train, y_train)
valid_ds = TensorDataset(x_valid, y_valid)
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)
model.to(dev)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
loss_func = F.cross_entropy
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
Closing thoughts
大体上,使用nn的过程就如前面所示,其中所使用的一些模块总结如下,为了不至于因为中英文转换产生什么误解,就保留原文如下,毕竟前面都解释过了:
torch.nn-
Module: creates a callable which behaves like a function, but can also contain state(such as neural net layer weights). It knows what Parameter (s) it contains and can zero all their gradients, loop through them for weight updates, etc. -
Parameter: a wrapper for a tensor that tells a Module that it has weights that need updating during backprop. Only tensors with the requires_grad attribute set are updated -
functional: a module(usually imported into the F namespace by convention) which contains activation functions, loss functions, etc, as well as non-stateful versions of layers such as convolutional and linear layers.
-
torch.optim: Contains optimizers such as SGD, which update the weights of Parameter during the backward stepDataset: An abstract interface of objects with a len and a getitem, including classes provided with Pytorch such as TensorDatasetDataLoader: Takes any Dataset and creates an iterator which returns batches of data.
本文介绍如何使用PyTorch搭建并训练卷积神经网络(CNN)解决MNIST手写数字识别问题,包括模型定义、数据预处理及GPU加速等关键步骤。

被折叠的 条评论
为什么被折叠?



