原文在这里What is torch.nn really?
如文中所言,以下所有代码都已在Jupyter Notebook上运行通过。
先正向过一遍内容,有空再反向研究一遍。
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操作。
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")
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
,这个方法很有效。
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.
文档已经很清楚,就不需要再解释什么了。
上面是展示了获取数据集的一些方法步骤,接下来先从手搓一个神经网络开始。
按照李宏毅老师的说法,训练神经网络就是3个步骤:1.定义一个model;2.定义一个loss函数;3.优化参数。
import math
weights = torch.randn(784, 10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)
这里首先要注意的是weights
和bias
生成时的几个数字的含义:
然后是设置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)
也是可以的(注意是除以,不是除)。
def log_softmax(x):
return x - x.exp().sum(-1).log().unsqueeze(-1)
def model(xb):
return log_softmax(xb @ weights + bias)
这里需要先说明一下squeeze
和unsqueeze
。
先看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=⎣⎢⎢⎢⎡a11a21⋮am1a12a22⋮am2⋯⋯⋱⋯a1na2n⋮amn⎦⎥⎥⎥⎤
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=1∑mai1i=1∑mai2...i=1∑main]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=1∑na1jj=1∑na2j...i=1∑namj]torch.Size([m])
的张量。sum(0)
或者sum(-2)
是把每一列的值加起来,得到一个torch.Size([n])
的张量,而sum(1)
或者sum(-1)
是把每一行的值加起来,得到一个torch.Size([m])
的张量。sum
的参数为0
或者-2
代表了要把行reduce掉,为1
或者-1
代表了要把列reduce掉,于是sum(0)
表示对每一列分别求和,有几列就会有几个值,而sum(1)
则表示对每一行分别求和,有几行就有几个值。这里reduce是map/reduce
的reduce
。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
,先忽略unsqueeze
,log_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)=xi−log(j∑exp(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
的每一列都分别减去这个张量,得到最后的结果。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)
这里本身没什么可说的了,只不过可以再说明下几个数字的含义:
model
时的数据集的大小,至于为什么不是一次传入全部的数据集,这就不必废话了。weights
必须是一个 784 × 10 784\times10 784×10的矩阵,bias
也必须是一个 1 × 10 1\times10 1×10的矩阵或者说一个10维的行向量,注意这里是从数学的角度来说,而不是张量的shape
的返回结果。那么,为什么是这几个数字?因为这是一个简单的只有一个隐含层的线性模型,从矩阵运算的角度而言,weights
就必须是 784 × 10 784\times10 784×10,bias
也必须是 1 × 10 1\times10 1×10,如果中间多那么几层,那些矩阵的大小或者说行列的数量就具体而定了,但终究必须符合矩阵运算的规则。
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])
的张量。显然,yb
的shape
是torch.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
的列索引,其对应的值作为所生成的张量的对应元素。
接下来就是个简单的平均,无需赘言。
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好记些。
剩下的比较以及类型转换和求平均就不必多说了。
所谓训练,就是做这么几件事情:
loss.backward()
来反向更新模型的梯度,这个例子里指的是weights
和bias
的梯度,注意不是这两个参数的值#这句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()
上下文中执行更新weights
和bias
,意思是不记录-=
这个运算操作,注意不是指weights
和bias
的值*.grad.zero_()
这个运算,仍然与值无关,只是运算