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

原文在这里What is torch.nn really?

如文中所言,以下所有代码都已在Jupyter Notebook上运行通过。
先正向过一遍内容,有空再反向研究一遍。

MNIST data setup

1.下载数据集并保存
from pathlib import Path
import requests

DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"
#print(DATA_PATH,PATH)
PATH.mkdir(parents=True, exist_ok=True)

#因为github有时候不太好上,所以找了oreilly的这个源,速度还不错,内容是一致的
#URL = "https://github.com/pytorch/tutorials/raw/master/_static/"
URL = "https://resources.oreilly.com/live-training/inside-unsupervised-learning/-/raw/9f262477e62c3f5a0aa7eb788e557fc7ad1310de/data/mnist_data/"
FILENAME = "mnist.pkl.gz"

if not (PATH / FILENAME).exists():
        content = requests.get(URL + FILENAME).content
        (PATH / FILENAME).open("wb").write(content)

注意mnist数据集如上面注释所言,换了个源,别的都是常规的python操作。

2. 文件解压以及装载数据
import pickle
import gzip

with gzip.open((PATH / FILENAME).as_posix(), "rb") as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")
3.展示一下数据集的内容
from matplotlib import pyplot
import numpy as np

pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
print(x_train.shape)
print(x_train)

这并不是必须的,不过在处理之前先看一看数据集里的内容也未尝不可.
这里需要注意的是在调用imshow时,Notebook内核有时会崩溃,不过Notebook这个问题并不只在使用matplolib时才会出现,解决的方法之一是pip install numpy --upgrade,这个方法很有效。

4.把数据从numpy array格式转换成pytorch tensor格式
import torch

x_train, y_train, x_valid, y_valid = map(
    torch.tensor, (x_train, y_train, x_valid, y_valid)
)
n, c = x_train.shape  # n X c 的一个矩阵,这里是50000X784,也就是50000张图片,每张图片是28X28的大小,每张图片被拉成了一个784维的行向量
print(x_train, y_train)
print(x_train.shape)
print(y_train.min(), y_train.max())

因为x_train, y_train, x_valid, y_valid这些是numpy array格式的数据,所以需要转换成pytorch tensor格式。
这里使用了map函数,这个函数的文档是这样的

Syntax :
map(fun, iter)
Parameters :
fun : It is a function to which map passes each element of given iterable.
iter : It is a iterable which is to be mapped.

文档已经很清楚,就不需要再解释什么了。

上面是展示了获取数据集的一些方法步骤,接下来先从手搓一个神经网络开始。

Neural net from scratch (no torch.nn)

按照李宏毅老师的说法,训练神经网络就是3个步骤:1.定义一个model;2.定义一个loss函数;3.优化参数。

1.初始化weight和bias
import math

weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)

这里首先要注意的是weightsbias生成时的几个数字的含义:

  • 这是一个手写数字辨析的项目,本质上是分类或者打标签,因此10就是代表了10个不同的数字,或者说10个分类。
  • 每张图片是 28 × 28 28\times28 28×28的大小,也就是总共有784个像素点。

然后是设置requires_grad时的区别。

  • bias是直接生成的一个元素全部为0的10维的向量,或者说是一个torch.Size([10])的张量,所以可以在构造时使用requires_grad参数,因为是数值,算梯度的时候直接代进去计算就行。
  • 而构造weights时调用了math.sqrt()函数以及一个/运算,如果在这一步加上requires_grad,那么这个函数调用以及除法运算都会加入到链式求导过程中。所以,需要先生成weights,然后再对其做一次原地的requires_grad_()调用。

最后是/math.sqrt(784), 这是所谓的Xavier initialisation (原始论文在这里),也就是乘上一个1/sqrt(n),当然要说除以一个sqrt(n)也是可以的(注意是除以,不是除)。

2.定义model和activation函数
def log_softmax(x):
    return x - x.exp().sum(-1).log().unsqueeze(-1)

def model(xb):
    return log_softmax(xb @ weights + bias)

这里需要先说明一下squeezeunsqueeze
先看squeeze,假定一个张量t的shape是 2 × 1 × 3 × 1 × 4 × 1 × 5 2\times1\times3\times1\times4\times1\times5 2×1×3×1×4×1×5,那么

  • t.squeeze()的结果是一个 2 × 3 × 4 × 5 2\times3\times4\times5 2×3×4×5的张量,注意这个操作并不会改变t,下面就不再说明这一点了。
  • t.squeeze(0)的结果是没有变化,因为张量t的第一维的大小是2,squeeze只对大小为1的维度操作,类似的传入2,4,6也是如此。
  • t.squeeze(1)的结果是一个 2 × 3 × 1 × 4 × 1 × 5 2\times3\times1\times4\times1\times5 2×3×1×4×1×5的张量,类似的传入3,5也会有相应的结果。

再看unsqueeze,假定一个张量t的shape是 2 × 3 × 4 × 5 2\times3\times4\times5 2×3×4×5,那么

  • unsqueeze必须要带参数,并且只要参数正确,必定会返回一个不同的结果。
  • unsqueeze(0)返回一个 1 × 2 × 3 × 4 × 5 1\times2\times3\times4\times5 1×2×3×4×5的张量
  • unsqueeze(-1)返回一个 2 × 3 × 4 × 5 × 1 2\times3\times4\times5\times1 2×3×4×5×1的张量
  • 其他参数可以类推

然后是sum函数, 直观的说,对一个 m × n m\times n m×n矩阵:
A m × n = [ a 11 a 12 ⋯ a 1 n a 21 a 22 ⋯ a 2 n ⋮ ⋮ ⋱ ⋮ a m 1 a m 2 ⋯ a m n ] A_{m\times n} = \left[ {\begin{array}{cccc} a_{11} & a_{12} & \cdots & a_{1n}\\ a_{21} & a_{22} & \cdots & a_{2n}\\ \vdots & \vdots & \ddots & \vdots\\ a_{m1} & a_{m2} & \cdots & a_{mn}\\ \end{array} } \right] Am×n=a11a21am1a12a22am2a1na2namn

  • sum(0)sum(-2)的结果是 [ ∑ i = 1 m a i 1 ∑ i = 1 m a i 2 . . . ∑ i = 1 m a i n ] \left[ \sum_{i=1}^{m}a_{i1} \qquad\sum_{i=1}^{m}a_{i2} \qquad...\qquad\sum_{i=1}^{m}a_{in} \right ] [i=1mai1i=1mai2...i=1main]
    于是,这是一个torch.Size([n])的张量。
  • sum(1)sum(-1)的结果是 [ ∑ j = 1 n a 1 j ∑ j = 1 n a 2 j . . . ∑ i = 1 n a m j ] \left[ \sum_{j=1}^{n}a_{1j} \qquad\sum_{j=1}^{n}a_{2j} \qquad...\qquad\sum_{i=1}^{n}a_{mj} \right ] [j=1na1jj=1na2j...i=1namj]
    于是,这是一个torch.Size([m])的张量。
  • 简单的说就是对于一个 m × n m \times n m×n的张量,sum(0)或者sum(-2)是把每一列的值加起来,得到一个torch.Size([n])的张量,而sum(1)或者sum(-1)是把每一行的值加起来,得到一个torch.Size([m])的张量。
  • 或者说,一个 m × n m\times n m×n的矩阵或者张量,sum的参数为0或者-2代表了要把行reduce掉,为1或者-1代表了要把列reduce掉,于是sum(0)表示对每一列分别求和,有几列就会有几个值,而sum(1)则表示对每一行分别求和,有几行就有几个值。这里reduce是map/reducereduce
  • 对于为负数的参数,与python中索引的编号逻辑是一致的。比如对于 m × n m\times n m×n的张量,sum函数如果带参数的话,其取值范围就是[-2,1],因此,-1等同于1-2等同于0。类似的,对于一个 m × n × k m \times n \times k m×n×k的张量,sum函数参数的取值范围就是[-3,2]-3便等同于0-2便等同于1-1便等同于2。这点本来可以不用说的,不过想到了就写了。

接下来看log_softmax,先忽略unsqueezelog_softmax的数学解析式是这样的:
l o g _ s o f t m a x ( x i ) = x i − log ⁡ ( ∑ j exp ⁡ ( x j ) ) log\_softmax(x_i)=x_i-\log(\sum_{j}^{}\exp(x_j)) log_softmax(xi)=xilog(jexp(xj))
当然,这个式子本身没什么好解释的,只不过要注意的是,这里是针对张量或者归根结底是矩阵的运算。
另外对于张量的shape的表示形式,torch.Size([64])表示这是一个有64个元素的张量,但没有维度信息,通常可以理解为一个64维的向量。而 1 × 64 1\times64 1×64的张量表示为torch.Size([1,64])以及 64 × 1 64\times1 64×1的张量表示为torch.Size([64,1])

于是:

  • 原始数据集x_train是一个 500000 × 784 500000\times784 500000×784的矩阵或者说张量,如下面所示,每一个batch是64,所以在调用model时的参数xb是个 64 × 784 64\times784 64×784的矩阵,也就是说每一个batch处理64张图片,每张图片是 28 × 28 28\times28 28×28的分辨率,这784个像素点拉成了一个784维的向量。
  • weights是一个 784 × 10 784\times10 784×10的矩阵,bias是一个 1 × 10 1\times10 1×10的矩阵。这里不用张量而是矩阵以避免一些概念上的混乱,因为bias.shape的结果是torch.Size([64])
  • 于是在model函数里边,调用log_softmax时,xb@weights的结果是一个 64 × 10 64\times10 64×10的张量,bias分别加到了whights的每一行上,最终传入的参数就是一个 64 × 10 64\times10 64×10的张量。
  • x.exp()表示对x这个 64 × 10 64\times10 64×10的张量中的每一个元素共640个数分别做一次指数运算,
  • x.exp().sum(-1)sum函数的参数-1表示把这个 64 × 10 64\times10 64×10矩阵或者说张量的每一行的元素累加起来,最终得到的是一个torch.Size([64])的张量,注意不是 64 × 1 64\times1 64×1也不是 1 × 64 1\times64 1×64,这里sum传入1结果也是一样的。如果sum的参数为-2或者为0则是把每一列累加起来,得到的是一个torch.Size([10])的张量。
  • 因为x.exp().sum(-1).log()的结果是一个torch.Size([64])的张量,所以需要调用 unsqueeze(-1)来得到一个torch.Size([64,1])的张量,这里转置是没有意义的,因为torch.Size([64])没有维度的信息。然后把x的每一列都分别减去这个张量,得到最后的结果。
3.定义一个forward pass.
bs = 64  # batch size

xb = x_train[0:bs]  # a mini-batch from x
preds = model(xb)  # predictions
#下面这条是原文就有的,不过这里注释掉了,因为有了下面的print,这一句就没有什么意义了
#preds[0], preds.shape 
print(preds[0], preds.shape)

这里本身没什么可说的了,只不过可以再说明下几个数字的含义:

  • 50000是整个数据集的大小。
  • 64是每个batch的大小,也就是每次调用model时的数据集的大小,至于为什么不是一次传入全部的数据集,这就不必废话了。
  • 784是 28 × 28 28\times28 28×28的结果。
  • 所以weights必须是一个 784 × 10 784\times10 784×10的矩阵,bias也必须是一个 1 × 10 1\times10 1×10的矩阵或者说一个10维的行向量,注意这里是从数学的角度来说,而不是张量的shape的返回结果。
  • 在许多文字资料或者视频教程上,出于习惯的考虑,可能会把上述的行和列颠倒一下,这一点是需要了解的。

那么,为什么是这几个数字?因为这是一个简单的只有一个隐含层的线性模型,从矩阵运算的角度而言,weights就必须是 784 × 10 784\times10 784×10bias也必须是 1 × 10 1\times10 1×10,如果中间多那么几层,那些矩阵的大小或者说行列的数量就具体而定了,但终究必须符合矩阵运算的规则。

4.定义loss function
def nll(input, target):
    return -input[range(target.shape[0]), target].mean()

loss_func = nll
#下面两行只是做一个check,以及做个检验的基准
yb = y_train[0:bs]
print(loss_func(preds, yb))

所谓nll指的是negative log-likehood,当然,上面这个nll函数并没有做对数运算,只是简单的做了个平均,按照相关资料的说法,在pytorch中nll就是这样处理的,所以需要与log_softmax配合使用,才是真正的negative log-likehood。
至于nll的数学解析式及其与交叉熵的关系,还是去看相关资料吧,毕竟数学公式太多,会严重影响看文章的心情和体验。
preds是前面log_softmax返回的结果,是一个torch.Size([64, 10])的张量。显然,ybshapetorch.Size([64])yb.shape[0]的结果就是返回一个数值64。
于是nll里边实际上是这样:return -preds[range(0,64),yb].mean()。那么,忽略掉return-号,这里是怎么算的呢?
对于input[range(target.shape[0]), target]这个表达式,这里input是个torch.Size([64,10])的张量,每一行代表一张图片,每一列代表其对应0-9这10个数字的概率做了log_softmax
的结果,target或者说yb保存的则是相应的图片对应的数字,所以这里的操作就是生成一个张量,其中的每一个元素是input[i,target[i]],也就是对input的第i行,以target[i]作为input的列索引,其对应的值作为所生成的张量的对应元素。
接下来就是个简单的平均,无需赘言。

5.辅助检验函数accuracy
def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()

#开始训练之前,check一下,做个基准   
print(accuracy(preds, yb))

为了能直观的看到训练结果,定义了这个函数。
torch.argmax返回的是个索引,dim=0表示取每一列的最大值所在行的索引,dim=1表示取每一行的最大值所在列的索引,这样理解我觉得应该比reduce好记些。
剩下的比较以及类型转换和求平均就不必多说了。

6.开始训练

所谓训练,就是做这么几件事情:

  • 从数据集中选择或者说确定一个batch,每一个batch的大小是bs。
  • 调用model来做prediction,也就是给model输入数据,做一些计算,得到结果。
  • 根据上面输出的结果计算loss,在forward的时候每一步的梯度值都会计算并记录下来,供backward的时候使用。
  • 调用loss.backward()来反向更新模型的梯度,这个例子里指的是weightsbias的梯度,注意不是这两个参数的值
#这句import是追踪执行过程用的
from IPython.core.debugger import set_trace

lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

for epoch in range(epochs):
		#n是数据集的大小,这里是50000,(n - 1) // bs + 1 算的是这n个数据可以分成多少个batch
    for i in range((n - 1) // bs + 1):
    	#如果需要追踪,就把下面一句打开
        #         set_trace()
        start_i = i * bs
        end_i = start_i + bs
        xb = x_train[start_i:end_i]
        yb = y_train[start_i:end_i]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        with torch.no_grad():
            weights -= weights.grad * lr
            bias -= bias.grad * lr
            weights.grad.zero_()
            bias.grad.zero_()

#打印一下结果
print(loss_func(model(xb), yb), accuracy(model(xb), yb))

需要说明几点:

  • 上面的n是这个数据集的大小,这里是50000
  • (n - 1) // bs + 1 算的是这n个数据可以分成多少个batch,
  • torch.no_grad()上下文中执行更新weightsbias,意思是不记录-=这个运算操作,注意不是指weightsbias的值
  • 同样,也不记录*.grad.zero_()这个运算,仍然与值无关,只是运算
  • 至于pytorch的Autograd机制看这里Autograd mechanics

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