原文在这里。
因为MNIST是个分类问题,所以使用逻辑回归是合适的,同时又是个图像问题,所以使用CNN也是合理的。
接下来我们转向CNN,这里展示了两种方式,因为之前的代码已经把model封装起来了,而其余部分并没有涉及到模型本身,所以基本上只要重新定义一个模型,其余部分都不需要改动。
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
则是在这个框架上处理数据的过程。__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
于是,就很清楚了,这也就是线性的意思。
DataLoader
前面的CNN只能用于MNIST,因为:
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这样的解释型语言或者说脚本语言来说却是很正常的。
最后的改进就是使用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)
大体上,使用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.