关于pytorch官网教程中的What is torch.nn really?(三)

文章目录

          • Switch to CNN
          • `nn.Sequential`
          • Wrapping `DataLoader`
          • Using your GPU
          • Closing thoughts

原文在这里。

因为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的行向量。
  • 注意这里的reluavg_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 Lambdapreprocess定义的目的所在就很清楚了。

当然,需要自定义一层的原因也是因为pytorch中没有一个view层,或者说没有提供一个Module来实现view这个操作。

上面Sequential的参数列表中出现的Lambda(lambda x: x.view(x.size(0), -1))有必要提一句,就是小写字母开头的这个lambda,表示一个lambda表达式,这是内置的机制,跟大写字母开头的Lambda并没有什么关联。至于为什么这里要用Lambda来作为这个view层的名字,不得而知,就这么用吧。

我们这里只看到了构造modelSequential的参数列表,知道这个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 step
  • Dataset: An abstract interface of objects with a len and a getitem, including classes provided with Pytorch such as TensorDataset
  • DataLoader: Takes any Dataset and creates an iterator which returns batches of data.

你可能感兴趣的:(pytorch,pytorch,深度学习,人工智能)