书籍地址:《Python深度学习:基于PyTorch》

Tensor

创建Tensor

创建Tensor的方法有很多,可以从列表或ndarray等类型进行构建,也可根据指定的形状构建。常见的创建Tensor的方法可参考表2-1。
image.png

  • t.dtype # 查看数据类型
  • t.size()t.shape # 查看形状

torch.Tensor与torch.tensor的区别
1)torch.Tensor 是一个类,所有的tensor都是torch.Tensor的实例。
torch.Tensor是torch.empty和torch.tensor之间的一种混合,但是,当传入数据时,torch.Tensor使用全局默认dtype(FloatTensor),而torch.tensor是从数据中推断数据类型,有torch.FloatTensor、torch.LongTensor、torch.DoubleTensor等类型。
2)torch.tensor(1)返回一个固定值1,而torch.Tensor(1)返回一个大小为1的张量,它是随机初始化的值。
tensor(data, dtype=**None**, device=**None**, requires_grad=**False**)接收的是数据,可以是单个数字或者list。而Tensor可以传入shape进行随机初始化。如果传入的不是list,而是1个或多个数字,那么该数字作为shape,输出随机数。

修改Tensor形状

image.png

torch.view与torch.reshpae的异同
1)reshape()可以由torch.reshape(),也可由torch.Tensor.reshape()调用。但view()只可由 torch.Tensor.view()来调用。
2)对于一个将要被view的Tensor,新的size必须与原来的size与stride兼容。否则,在 view之前必须调用contiguous()方法。
3)同样也是返回与input数据量相同,但形状不同的Tensor。若满足view的条件,则不会copy,若不满足,则会copy。
4)如果你只想重塑张量,请使用torch.reshape。如果你还关注内存使用情况并希望确保两个张量共享相同的数据,请使用torch.view。

索引

image.png

  • gather? ```python

x = torch.randn(2, 3) tensor([[ 0.3607, -0.2859, -0.3938], [ 0.2429, -1.3833, -2.3134]])

mask = x>0 Out[8]: tensor([[ True, False, False], [ True, False, False]]) torch.masked_select(x,mask) Out[9]: tensor([0.3607, 0.2429])

index=torch.LongTensor([[0,1,1]]) torch.gather(x,0,index) Out[13]: tensor([[ 0.3607, -1.3833, -2.3134]])

index=torch.LongTensor([[0,0,1]]) torch.gather(x,0,index) Out[16]: tensor([[ 0.3607, -0.2859, -2.3134]])

index=torch.LongTensor([[1,0,1]]) torch.gather(x,0,index) Out[18]: tensor([[ 0.2429, -0.2859, -2.3134]])

index=torch.LongTensor([[1,0],[1,1]]) torch.gather(x,0,index) Out[20]: tensor([[ 0.2429, -0.2859], [ 0.2429, -1.3833]])

index=torch.LongTensor([[1,0,1,1]]) torch.gather(x,1,index) Out[23]: tensor([[-0.2859, 0.3607, -0.2859, -0.2859]])

  1. <a name="fTT1b"></a>
  2. ## 计算
  3. <a name="Cs436"></a>
  4. ### 逐元素操作
  5. 逐元素操作输入与输出的形状相同。这些操作均会创建新的Tensor,如果需要就地操作,可以使用这些方法的下划线版本,例如abs_。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2319227/1650771690493-cce98bf7-7282-43bf-ab4c-577b2707a6e1.png#clientId=u532ba814-a6b3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=373&id=ub4a8ea4f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=615&originWidth=1601&originalType=binary&ratio=1&rotation=0&showTitle=false&size=272987&status=done&style=none&taskId=u9eee41c6-0334-4e6c-a261-972522df47d&title=&width=970.3029742210288)
  6. <a name="iTEYB"></a>
  7. ### 归并操作
  8. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/2319227/1650786366147-9d71d632-a499-4999-87b4-ea5c2f349c54.png#clientId=ubecd6278-a8e7-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=482&id=u280fef17&margin=%5Bobject%20Object%5D&name=image.png&originHeight=663&originWidth=1172&originalType=binary&ratio=1&rotation=0&showTitle=false&size=185488&status=done&style=none&taskId=u74132bcb-7e33-41e1-b0cf-8ddcf077609&title=&width=852.3636363636364)<br />归并操作顾名思义,就是对输入进行归并或合计等操作,这类操作的输入输出形状一般不相同,而且往往是输入大于输出形状。归并操作可以对整个tensor,也可以沿着某个维度进行归并。<br />归并操作一般涉及一个dim参数,指定沿哪个维进行归并。另一个参数是keepdim,说明输出结果中是否保留维度1,缺省情况是False,即不保留。
  9. <a name="afari"></a>
  10. ### 比较
  11. 比较操作一般是进行逐元素比较,有些是按指定方向比较。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2319227/1650792448151-486fe807-2aa7-4347-bb18-236e329f847d.png#clientId=ua49cd1f8-b803-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=182&id=uad90f1fd&margin=%5Bobject%20Object%5D&name=image.png&originHeight=227&originWidth=879&originalType=binary&ratio=1&rotation=0&showTitle=false&size=63559&status=done&style=none&taskId=u69e031cb-61c9-4a98-aefc-a952fbec9ba&title=&width=703.2)
  12. <a name="oHt5Y"></a>
  13. ### 矩阵
  14. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/2319227/1650792493745-cad12d33-dc2c-4296-b563-b56bdbda67e9.png#clientId=ua49cd1f8-b803-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=182&id=u00080756&margin=%5Bobject%20Object%5D&name=image.png&originHeight=228&originWidth=761&originalType=binary&ratio=1&rotation=0&showTitle=false&size=50865&status=done&style=none&taskId=ue6685e81-d5ac-48fd-b141-c426bc522e8&title=&width=608.8)<br />【说明】 <br />1)Torch的dot与Numpy的dot有点不同,Torch中的dot是对两个为1D张量进行点积运算,Numpy中的dot无此限制。 <br />2)mm是对2D的矩阵进行点积,bmm对含batch的3D进行点积运算。 <br />3)转置运算会导致存储空间不连续,需要调用contiguous方法转为连续。
  15. <a name="plDW0"></a>
  16. ## Autograd
  17. autograd包为张量上所有的操作提供了自动求导功能,而torch.Tensor和torch.Function为autograd上的两个核心类,他们相互连接并生成一个有向非循环图。接下来我们先简单介绍tensor如何实现自动求导,然后介绍计算图,最后用代码实现这些功能。
  18. <a name="Pjzdt"></a>
  19. ### 1. 自动求导
  20. autograd包为对tensor进行自动求导,为实现对tensor自动求导,需考虑如下事项:<br />(1)创建**叶子节点**(leaf node)的tensor,使用`requires_grad`参数指定是否记录对其的操作,以便之后利用`backward()`方法进行梯度求解。requires_grad参数缺省值为False,如果要对其求导需设置为True,**与之有依赖关系的节点自动变为True**。<br />(2)可利用requires_grad_()方法修改tensor的requires_grad属性。可以调用`.detach()`或`with torch.no_grad():`将不再计算张量的梯度,跟踪张量的历史记录。这点在评估模型、测试模型阶段常常使用。<br />(3)通过运算创建的tensor(即非叶子节点),会自动被赋于`grad_fn` 属性,该属性表示梯度函数。叶子节点的grad_fn为None。<br />(4)最后得到的 tensor 执行`backward()`函数,此时自动计算各变量的梯度,并将累加结果保存grad属性中。**计算完成后,非叶子节点的梯度自动释放。**<br />(5)backward()函数接受参数,该参数应和调用backward()函数的Tensor的维度相同,或者是可broadcast的维度。如果求导的tensor为标量(即一个数字),backward中参数可省略。<br />(6)反向传播的中间缓存会被清空,如果需要进行多次反向传播,需要指定backward中的参数retain_graph=True。多次反向传播时,梯度是累加的。<br />(7)**非叶子节点的梯度 backward 调用后即被清空**。<br />(8)可以通过用`torch.no_grad()`包裹代码块来阻止autograd去跟踪那些标记为.requesgrad=True的张量的历史记录。这步在测试阶段经常使用。
  21. 整个过程中,Pytorch采用计算图的形式进行组织,该计算图为动态图,它的计算图在每次前向传播时,将重新构建。其他深度学习架构,如TensorFlow、Keras一般为静态图。接下来我们介绍计算图,用图的形式来描述就更直观了,该计算图为有向无环图(DAG)。
  22. <a name="G34nX"></a>
  23. ### 2. 计算图
  24. - 计算图是一种有向无环图像,用图形方式表示算子与变量之间的关系,直观高效。如图2-8所示,圆形表示变量,矩阵表示算子。
  25. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/2319227/1650799504123-1ee55262-c035-4c32-bca0-e726289f39de.png#clientId=ua49cd1f8-b803-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u223b382a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=250&originWidth=214&originalType=url&ratio=1&rotation=0&showTitle=false&size=8642&status=done&style=none&taskId=u5915614f-c641-4439-a581-55b97fba995&title=)<br />正向传播
  26. - 比如,表达式 z=wx+b,可写成:y=wx,z=y+b。其中x、w、b为变量,是用户创建的变量,不依赖于其他变量,故又称为**叶子节点**。y、z是计算得到的变量,非叶子节点,z为根节点。
  27. - 为计算各叶子节点的梯度,需要把对应的张量参数requires_grad属性设置为True,这样就可自动跟踪其历史记录。
  28. - mul和add是算子(或操作或函数)。由这些变量及算子,就构成一个完整的计算过程(或前向传播过程)。
  29. - 我们的目标是更新各叶子节点的梯度,根据复合函数导数的链式法则,不难算出各叶子节点的梯度。
  30. 反向传播(标量)
  31. - Pytorch调用`backward()`,将自动计算各节点的梯度,并将累加结果保存 grad 属性中,这是一个反向传播过程。通过`w.grad`和`b.grad`可以查看w和b的梯度。计算完成后,非叶子节点y,z的梯度自动释放。
  32. - 在反向传播过程中,autograd从当前根节点z反向溯源,利用导数链式法则,计算所有叶子节点的梯度,其梯度值将累加到grad属性中。对非叶子节点的计算操作(或function)记录在grad_fn属性中,叶子节点的grad_fn值为None。
  33. ```python
  34. import torch
  35. import numpy as np
  36. #定义输入张量x
  37. x=torch.Tensor([2])
  38. #初始化权重参数W,偏移量b、并设置require_grad为True,为自动求导
  39. w=torch.randn(1,requires_grad=True)
  40. b=torch.randn(1,requires_grad=True)
  41. y=torch.mul(w,x) #等价于w*x
  42. z=torch.add(y,b) #等价于y+b
  43. #查看x,w,b页子节点的requite_grad属性
  44. print("x,w,b的require_grad属性分别为:{},{},{}".format(x.requires_grad,w.requires_grad,b.requires_grad))
  45. #查看非叶子节点的requres_grad属性,
  46. print("y,z的requires_grad属性分别为:{},{}".format(y.requires_grad,z.requires_grad))
  47. #因与w,b有依赖关系,故y,z的requires_grad属性也是:True,True
  48. #查看各节点是否为叶子节点
  49. print("x,w,b,y,z的是否为叶子节点:{},{},{},{},{}".format(x.is_leaf,w.is_leaf,b.is_leaf,y.is_leaf,z.is_leaf))
  50. #x,w,b,y,z的是否为叶子节点:True,True,True,False,False
  51. #查看叶子节点的grad_fn属性
  52. print("x,w,b的grad_fn属性:{},{},{}".format(x.grad_fn,w.grad_fn,b.grad_fn))
  53. #因x,w,b为用户创建的,为通过其他张量计算得到,故x,w,b的grad_fn属性:None,None,None
  54. #查看非叶子节点的grad_fn属性
  55. print("y,z的是否为叶子节点:{},{}".format(y.grad_fn,z.grad_fn))
  56. #y,z的是否为叶子节点:<MulBackward0 object at 0x7f923e85dda0>,<AddBackward0 object at 0x7f923e85d9b0>
  57. #基于z张量进行梯度反向传播,执行backward之后计算图会自动清空,
  58. #如果需要多次使用backward,需要修改参数retain_graph为True,此时梯度是累加的
  59. #z.backward(retain_graph=True)
  60. z.backward()
  61. #查看叶子节点的梯度,x是叶子节点但它无需求导,故其梯度为None
  62. print("参数w,b的梯度分别为:{},{},{}".format(w.grad,b.grad,x.grad))
  63. #参数w,b的梯度分别为:tensor([2.]),tensor([1.]),None
  64. #非叶子节点的梯度,执行backward之后,会自动清空
  65. print("非叶子节点y,z的梯度分别为:{},{}".format(y.grad,z.grad))
  66. #非叶子节点y,z的梯度分别为:None,None

反向传播(非标量)

  • Pytorch有个简单的规定,不让张量(tensor)对张量求导,只允许标量对张量求导。如果目标值是含多个元素的张量,如何对这个非标量进行反向传播呢?
  • 目标张量对一个非标量调用backward(),需要传入一个gradient参数,该参数也是张量,而且需要与调用backward()的张量形状相同。
  • 传入张量gradient,是为了把张量对张量求导转换为标量对张量求导。举例子来说,假设目标值为loss=(y_1,y_2,…,y_m)传入的参数为v=(v_1,v_2,…,v_m),那么就可把对loss的求导,转换为对loss*v^T标量的求导。即把原来∂loss/∂x得到雅可比矩阵(Jacobian)乘以张量v^T,便可得到我们需要的梯度矩阵。? ```python import torch

定义叶子节点张量x,形状为1x2

x= torch.tensor([[2, 3]], dtype=torch.float, requires_grad=True)

初始化Jacobian矩阵

J= torch.zeros(2 ,2)

初始化目标张量,形状为1x2

y = torch.zeros(1, 2)

定义y与x之间的映射关系:

y1=x12+3*x2,y2=x22+2*x1

y[0, 0] = x[0, 0] 2 + 3 * x[0 ,1] y[0, 1] = x[0, 1] 2 + 2 * x[0, 0]

生成y1对x的梯度

y.backward(torch.Tensor([[1, 0]]),retain_graph=True) J[0]=x.grad

梯度是累加的,故需要对x的梯度清零

x.grad = torch.zeros_like(x.grad)

生成y2对x的梯度

y.backward(torch.Tensor([[0, 1]])) J[1]=x.grad

显示jacobian矩阵的值

print(J)

  1. <a name="no75H"></a>
  2. # 神经网络工具箱
  3. 本节利用Pytorch的nn工具箱,构建一个神经网络实例。
  4. <a name="cTlv9"></a>
  5. ## 主要工具
  6. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/2319227/1651549855758-fbfaf80b-a72e-4be2-b73b-5536f1a5012a.png#clientId=uffd33083-38c6-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=577&id=u76279133&margin=%5Bobject%20Object%5D&name=image.png&originHeight=952&originWidth=1542&originalType=binary&ratio=1&rotation=0&showTitle=false&size=271024&status=done&style=none&taskId=u03dc9761-3525-4272-b6e9-ad694d81a0e&title=&width=934.5454005301851)<br />图:PyTorch实现神经网络主要工具及相互关系
  7. `nn.Module`是nn的一个核心数据结构。nn.Module可以是神经网络的某个层(layer),也可以是包含多层的神经网络。在实际使用中,最常见的做法是继承nn.Module,生成自己的网络/层,如下面的实例中,我们定义的Net类就采用这种方法(class Net(torch.nn.Module))。nn中已实现了绝大多数层,包括全连接层、损失层、激活层、卷积层、循环层等等,这些层都是nn.Module的子类,能够自动检测到自己的Parameter,并将其作为学习参数,且针对GPU运行进行了CuDNN优化。
  8. nn中的层,一类是继承了`nn.Module`,其命名一般为nn.Xxx(第一个是大写),如nn.Linear、nn.Conv2d、nn.CrossEntropyLoss等。<br />另一类是`nn.functional`中的函数,其名称一般为nn.funtional.xxx,如nn.funtional.linear、nn.funtional.conv2d、nn.funtional.cross_entropy等。<br />从功能来说两者相当,基于nn.Mudle能实现的层,使用nn.funtional也可实现,反之亦然,而且性能方面两者也没有太大差异。
  9. 不过在具体使用时,两者还是有区别,主要区别如下:<br />(1)nn.Xxx继承于`nn.Module`,nn.Xxx 需要先实例化并传入参数,然后以函数调用的方式调用实例化的对象并传入输入数据。它能够很好的与`nn.Sequential`结合使用,而nn.functional.xxx无法与nn.Sequential结合使用。<br />(2)nn.Xxx不需要自己定义和管理weight、bias参数;而nn.functional.xxx需要你自己定义weight、bias,每次调用的时候都需要手动传入weight、bias等参数, 不利于代码复用。<br />(3)dropout操作在训练和测试阶段是有区别的,使用nn.Xxx方式定义dropout,在调用model.eval()之后,自动实现状态的转换,而使用nn.functional.xxx却无此功能。
  10. 总的来说,两种功能都是相同的,区别在于`nn.Module`继承Module类,会自动提取可学习的参数;而nn.functional更像是纯函数。<br />PyTorch官方推荐:具有学习参数的(例如,conv2d, linear, batch_norm、dropout)采用nn.Xxx方式。没有学习参数的(例如,maxpool, loss func, activation func)等根据个人选择使用nn.functional.xxx或者nn.Xxx方式。
  11. <a name="m1Blz"></a>
  12. ## 搭建模型
  13. 使用sequential构建网络,Sequential()函数的功能是将网络的层组合到一起:
  14. ```python
  15. class Net(nn.Module):
  16. def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
  17. super(Net, self).__init__()
  18. self.layer1 = nn.Sequential(nn.Linear(in_dim, n_hidden_1),nn.BatchNorm1d(n_hidden_1))
  19. self.layer2 = nn.Sequential(nn.Linear(n_hidden_1, n_hidden_2),nn.BatchNorm1d(n_hidden_2))
  20. self.layer3 = nn.Sequential(nn.Linear(n_hidden_2, out_dim))
  21. def forward(self, x):
  22. x = F.relu(self.layer1(x))
  23. x = F.relu(self.layer2(x))
  24. x = self.layer3(x)
  25. return x

上面我们采用torch.nn.Sequential()来构建网络层,这个有点类似Keras的models.Sequential(),使用起来就像搭积木一样。不这种方法每层的编码是默认的数字,不易区分。如果要对每层定义一个名称,我们可以采用Sequential的一种改进方法,在Sequential的基础上,通过add_module()添加每一层,并且为每一层增加一个单独的名字。
此外,还可以在Sequential基础上,通过字典的形式添加每一层,并且设置单独的层名称。
以下是采用字典方式构建网络的一个示例代码:

  1. class Net(torch.nn.Module):
  2. def __init__(self):
  3. super(Net, self).__init__()
  4. self.conv = torch.nn.Sequential(
  5. OrderedDict(
  6. [
  7. ("conv1", torch.nn.Conv2d(3, 32, 3, 1, 1)),
  8. ("relu1", torch.nn.ReLU()),
  9. ("pool", torch.nn.MaxPool2d(2))
  10. ]
  11. ))
  12. self.dense = torch.nn.Sequential(
  13. OrderedDict([
  14. ("dense1", torch.nn.Linear(32 * 3 * 3, 128)),
  15. ("relu2", torch.nn.ReLU()),
  16. ("dense2", torch.nn.Linear(128, 10))
  17. ])
  18. )

前向与反向传播

定义好每层后,最后还需要通过前向传播的方式把这些串起来。forward函数的任务需要把输入层、网络层、输出层链接起来,实现信息的前向传导。该函数的参数一般为输入数据,返回值为输出数据。
在forward函数中,有些层来自nn.Module,也可以使用nn.functional定义。来自nn.Module的需要实例化,而使用nn.functional定义的可以直接使用。

使用nn工具箱,我们无需自己编写反向传播,直接让损失函数(loss)调用backward()即可。

训练模型

层、模型、损失函数和优化器等都定义或创建好,接下来就是训练模型。

训练模型时需要注意使模型处于训练模式,即调用model.train()。调用model.train()会把所有的module设置为训练模式。如果是测试或验证阶段,需要使模型处于验证阶段,即调用model.eval()。调用model.eval()会把所有的training属性设置为False。

缺省情况下梯度是累加的,需要手工把梯度初始化或清零,调用optimizer.zero_grad()即可。训练过程中,正向传播生成网络的输出,计算输出和实际值之间的损失值。 调用loss.backward()自动生成梯度,然后使用optimizer.step()执行优化器,把梯度传播回每个网络。

如果希望用GPU训练,需要把模型、训练数据、测试数据发送到GPU上,即调用.to(device)。如果需要使用多GPU进行处理,可使模型或相关数据引用nn.DataParallel。nn.DataParallel的具体使用在下一章将详细介绍。

优化器

Pytoch常用的优化方法都封装在torch.optim里面,其设计很灵活,可以扩展为自定义的优化方法。所有的优化方法都是继承了基类optim.Optimizer。并实现了自己的优化步骤。
最常用的优化算法就是梯度下降法及其各种变种,这类优化算法使用参数的梯度值更新参数。

使用优化器的一般步骤为:
(1)建立优化器实例
import torch.optim asoptim
optimizer=optim.SGD(model.parameters(),lr=lr,momentum=momentum)
导入optim模块,实例化SGD优化器,这里使用动量参数 momentum(该值一般在(0,1)之间),是SGD的改进版,效果一般比不使用动量规则的要好。

以下步骤在训练模型的for循环中。

(2)向前传播
out=model(img)
loss=criterion(out,label)
把输入数据传入神经网络Net实例化对象model中,自动执行forward函数,得到out输出值,然后用out与标记label计算损失值loss。

(3)清空梯度
optimizer.zero_grad()
缺省情况梯度是累加的,在梯度反向传播前,先需把梯度清零。

(4)反向传播
loss.backward()
基于损失值,把梯度进行反向传播。

(5)更新参数
optimizer.step()
基于当前梯度(存储在参数的.grad属性中)更新参数。

动态修改学习率参数

修改参数的方式可以通过修改参数optimizer.params_groups或新建optimizer。
新建optimizer比较简单,optimizer十分轻量级,所以开销很小。但是新的优化器会初始化动量等状态信息,这对于使用动量的优化器(momentum参数的sgd)可能会造成收敛中的震荡。所以,这里我们采用直接修改参数optimizer.params_groups。
optimizer.param_groups:长度1的list,
optimizer.param_groups[0]:长度为6的字典,包括权重参数,lr,momentum等参数。

  1. for epoch in range(num_epoches):
  2. #动态修改参数学习率
  3. if epoch%5==0:
  4. optimizer.param_groups[0]['lr']*=0.1
  5. print(optimizer.param_groups[0]['lr'])
  6. for img, label in train_loader:
  7. ######

数据处理工具箱

PyTorch涉及数据处理(数据装载、数据预处理、数据增强等)主要工具包及相互关系如图4-1所示。
image.png

torch.utils.data

torch.utils.data工具包,它包括以下4个类。
1)Dataset:是一个抽象类。自定义数据集需要继承这个类,并且覆写其中的两个方法 getitemlen。`getitem通过给定索引获取数据和标签,一次只能获取一个数据。len_`提供数据的大小(size)

  • print(Data[2]) # 相当于调用getitem(2)
  • 如果希望批量处理(batch),同时还要进行shuffle和并行加速等操作,可选择DataLoader。

2)DataLoader:定义一个新的迭代器,实现批量(batch)读取,打乱数据(shuffle)并提供并行加速等功能。
3)random_split:把数据集随机拆分为给定长度的非重叠的新数据集。
4)*sampler:多种采样函数。

一般用data.Dataset处理同一个目录下的数据。如果数据在不同目录下,不同目录代表不同类别(这种情况比较普遍),使用data.Dataset来处理就不很方便。不过,可以使用Pytorch另一种可视化数据处理工具(即torchvision),不但可以自动获取标签,还提供很多数据预处理、数据增强等转换函数。

torchvision

torchvision 是PyTorch的一个视觉处理工具包,独立于PyTorch,需要另外使用pip或conda安装。它包括4个类,各类的主要功能如下。
1)utils:含两个函数,一个是make_grid,它能将多张图片拼接在一个网格中;另一个是save_img,它能将Tensor保存成图片。
2)models:提供深度学习中各种经典的网络结构以及训练好的模型(如果选择 pretrained=True),包括AlexNet、VGG系列、ResNet系列、Inception系列等。
3)transforms:常用的数据预处理操作,主要包括对Tensor及PIL Image对象的操作。

  • 比如对PIL Image进行裁剪、调整尺寸、填充、翻转、修改亮度对比度饱和度等。对Tensor进行标准化或转换为PIL Image。官网 transforms 介绍
  • 如果要对数据集进行多个操作,可通过Compose将这些操作像管道一样拼接起来,类似于nn.Sequential。以下为示例代码
    1. import torchvision.transforms as transforms
    2. transforms.Compose([
    3. #将给定的 PIL.Image 进行中心切割,得到给定的 size,
    4. #size 可以是 tuple,(target_height, target_width)。
    5. #size 也可以是一个 Integer,在这种情况下,切出来的图片形状是正方形。
    6. transforms.CenterCrop(10),
    7. #切割中心点的位置随机选取
    8. transforms.RandomCrop(20, padding=0),
    9. #把一个取值范围是 [0, 255] 的 PIL.Image 或者 shape 为 (H, W, C) 的 numpy.ndarray,
    10. #转换为形状为 (C, H, W),取值范围是 [0, 1] 的 torch.FloatTensor
    11. transforms.ToTensor(),
    12. #规范化到[-1,1]
    13. transforms.Normalize(mean = (0.5, 0.5, 0.5), std = (0.5, 0.5, 0.5))
    14. ])
    4)datasets:提供常用的数据集加载,设计上都是继承自torch.utils.data.Dataset,主要包括MMIST、CIFAR10/100、ImageNet和COCO等。
    当文件依据标签处于不同文件下时,我们可以利用 torchvision.datasets.ImageFolder 来直接构造出 dataset,代码如下:
    1. train_data = datasets.ImageFolder('./data/torchvision_data', transform=my_trans)
    2. train_loader = data.DataLoader(train_data,batch_size=8,shuffle=True,)
    ImageFolder 会将目录中的文件夹名自动转化成序列,那么DataLoader载入时,标签自动就是整数序列了。举例: ```python from torchvision import transforms, utils from torchvision import datasets import torch import matplotlib.pyplot as plt %matplotlib inline

my_trans=transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor() ]) train_data = datasets.ImageFolder(‘./data/torchvision_data’, transform=my_trans) train_loader = data.DataLoader(train_data,batch_size=8,shuffle=True,)

for i_batch, img in enumerate(train_loader): if i_batch == 0: print(img[1]) fig = plt.figure() grid = utils.make_grid(img[0]) plt.imshow(grid.numpy().transpose((1, 2, 0))) plt.show() utils.save_image(grid,’test01.png’) # 保存图片 break

from PIL import Image Image.open(‘test01.png’) # 查看图片

  1. tensor([1, 1, 2, 0, 0, 2, 2, 2])<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2319227/1651997634645-9acd8bf2-ebf8-49ce-8248-da5f2ce90d21.png#clientId=u72e9b58f-4527-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=96&id=pVNA6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=129&originWidth=782&originalType=binary&ratio=1&rotation=0&showTitle=false&size=207889&status=done&style=none&taskId=u92104c72-8c6b-4131-9bb9-9cd135e72de&title=&width=584.6000366210938)
  2. <a name="fBHuE"></a>
  3. ## tensorboardX
  4. TensorboardGoogle TensorFlow 的可视化工具,它可以记录训练数据、评估数据、网络结构、图像等,并且可以在web上展示,对于**观察神经网路训练的过程**非常有帮助。<br />PyTorch可以采用 tensorboard_logger, visdom 等可视化工具,但这些方法比较复杂或不够友好。为解决这一问题,人们推出了可用于 Pytorch 可视化的新的更强大工具tensorboardX。<br />tensorboardX 支持scalar, image, figure, histogram, audio, text, graph, onnx_graph, embedding, pr_curve and videosummaries等可视化方式。<br />安装也比较方便,先安装tensorflowCPUGPU版),然后安装tensorboardX `pip install tensorboardX`。[tensorboardX 官网](https://github.com/lanpa/tensorboardX)<br />常用`tensorboardX`可视化神经网络结构、损失值变化情况、
  5. **使用tensorboardX的一般步骤**<br />(1)导入tensorboardX,实例化SummaryWriter类,指明记录日志路径等信息。
  6. ```python
  7. from tensorboardX import SummaryWriter
  8. #实例化SummaryWriter,并指明日志存放路径。在当前目录没有logs目录将自动创建。
  9. writer=SummaryWriter(log_dir='logs')
  10. #调用实例
  11. writer.add_xxx()
  12. #关闭writer
  13. writer.close()

【说明】
①如果是windows环境,log_dir注意路径解析,如writer = SummaryWriter(log_dir=r'D:\myboard\test\logs')
②SummaryWriter的格式为SummaryWriter(log_dir=None,comment='',**kwargs),其中comment在文件命名加上comment后缀。
③如果不写log_dir,系统将在当前目录创建一个runs的目录。

(2)调用相应的API接口
接口一般格式为add_xxx(tag-name,object,iteration-number),即add_xxx(标签,记录的对象,迭代次数)。

(3)启动tensorboard服务
cd到logs目录所在的同级目录,在命令行输入tensorboard--logdir=logs --port6006,logdir等式右边可以是相对路径或绝对路径。
如果是windows环境,要注意路径解析,如tensorboard --logdir=r'D:\myboard\test\logs' --port 6006

(4)web展示
在浏览器输入http://服务器IP或名称:6006便可看到logs目录保存的各种图形。如果是本机,服务器名称可以使用localhost。

视觉处理基础

实现CIFAR-10多分类

  1. # -*- coding: utf-8 -*-
  2. from multiprocessing import freeze_support
  3. import torch
  4. import torchvision
  5. import torchvision.transforms as transforms
  6. import torch.nn as nn
  7. import torch.nn.functional as F
  8. import torch.optim as optim
  9. import matplotlib.pyplot as plt
  10. import numpy as np
  11. device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
  12. class CNNNet(nn.Module):
  13. def __init__(self):
  14. super(CNNNet, self).__init__()
  15. self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5, stride=1)
  16. self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
  17. self.conv2 = nn.Conv2d(in_channels=16, out_channels=36, kernel_size=3, stride=1)
  18. self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
  19. self.fc1 = nn.Linear(1296, 128)
  20. self.fc2 = nn.Linear(128, 10)
  21. def forward(self, x):
  22. x = self.pool1(F.relu(self.conv1(x)))
  23. x = self.pool2(F.relu(self.conv2(x)))
  24. # print(x.shape)
  25. x = x.view(-1, 36 * 6 * 6)
  26. x = F.relu(self.fc2(F.relu(self.fc1(x))))
  27. return x
  28. def imshow(img):
  29. img = img / 2 + 0.5 # unnormalize
  30. npimg = img.numpy()
  31. plt.imshow(np.transpose(npimg, (1, 2, 0)))
  32. plt.show()
  33. if __name__ == '__main__':
  34. freeze_support()
  35. transform = transforms.Compose(
  36. [transforms.ToTensor(),
  37. transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
  38. trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
  39. download=False, transform=transform)
  40. trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
  41. shuffle=True, num_workers=2)
  42. testset = torchvision.datasets.CIFAR10(root='./data', train=False,
  43. download=False, transform=transform)
  44. testloader = torch.utils.data.DataLoader(testset, batch_size=4,
  45. shuffle=False, num_workers=2)
  46. classes = ('plane', 'car', 'bird', 'cat',
  47. 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
  48. # 构建网络
  49. net = CNNNet()
  50. net = net.to(device)
  51. print("net have {} paramerters in total".format(sum(x.numel() for x in net.parameters())))
  52. print(net)
  53. # nn.Sequential(*list(net.children())[:4]) # 查看模型中的前四层
  54. # 初始化参数
  55. for m in net.modules():
  56. if isinstance(m, nn.Conv2d):
  57. # nn.init.normal_(m.weight)
  58. # nn.init.xavier_normal_(m.weight)
  59. nn.init.kaiming_normal_(m.weight) # 卷积层参数初始化
  60. nn.init.constant_(m.bias, 0)
  61. elif isinstance(m, nn.Linear):
  62. nn.init.normal_(m.weight) # 全连接层参数初始化
  63. # 选择优化器
  64. criterion = nn.CrossEntropyLoss()
  65. optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
  66. # optimizer = optim.Adam(net.parameters(), lr=LR)
  67. # 训练模型
  68. for epoch in range(1):
  69. running_loss = 0.0
  70. for i, data in enumerate(trainloader, 0):
  71. # 获取训练数据
  72. inputs, labels = data
  73. inputs, labels = inputs.to(device), labels.to(device)
  74. # 权重参数梯度清零
  75. optimizer.zero_grad()
  76. # 正向及反向传播
  77. outputs = net(inputs)
  78. loss = criterion(outputs, labels)
  79. loss.backward()
  80. optimizer.step()
  81. # 显示损失值
  82. running_loss += loss.item()
  83. if i % 2000 == 1999: # print every 2000 mini-batches
  84. print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 2000))
  85. running_loss = 0.0
  86. print('Finished Training')
  87. # 随机获取部分训练数据
  88. dataiter = iter(testloader)
  89. images, labels = dataiter.next()
  90. # images, labels = images.to(device), labels.to(device)
  91. # print images
  92. imshow(torchvision.utils.make_grid(images))
  93. print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))

输出:
net have 173742 paramerters in total
image.png
GroundTruth: cat ship ship plane

模型集成

为改善一项机器学习或深度学习的任务,首先想到的是从模型、数据、优化器等方面进行优化。效果还是不理想的话,可以尝试模型集成、迁移学习、数据增强等优化方法。
模型集成的原理有点像多个盲人摸象,每个盲人只能摸到大象的一部分,但综合每人摸到的部分,就能形成一个比较完整、符合实际的图像。每个盲人就像单个模型,集成这些模型各自摸到的部分,就能得到一个强于单个模型的模型。
当然,要是模型集成发挥效应,模型的多样性也是非常重要的,使用不同架构、甚至不同的学习方法是模型多样性的重要体现。如果只是改一下初始条件或调整几个参数,有时效果可能还不如单个模型。

  1. import torch
  2. import torch.nn as nn
  3. import torch.optim as optim
  4. import torch.nn.functional as F
  5. import torch.backends.cudnn as cudnn
  6. import numpy as np
  7. import torchvision
  8. import torchvision.transforms as transforms
  9. from torch.utils.data import DataLoader
  10. from collections import Counter
  11. from multiprocessing import freeze_support
  12. BATCHSIZE=100
  13. DOWNLOAD_MNIST=False
  14. EPOCHES=20
  15. LR=0.001
  16. #定义相关模型结构,这三个网络结构比较接近
  17. class CNNNet(nn.Module):
  18. def __init__(self):
  19. super(CNNNet,self).__init__()
  20. self.conv1 = nn.Conv2d(in_channels=3,out_channels=16,kernel_size=5,stride=1)
  21. self.pool1 = nn.MaxPool2d(kernel_size=2,stride=2)
  22. self.conv2 = nn.Conv2d(in_channels=16,out_channels=36,kernel_size=3,stride=1)
  23. self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
  24. self.fc1 = nn.Linear(1296,128)
  25. self.fc2 = nn.Linear(128,10)
  26. def forward(self,x):
  27. x=self.pool1(F.relu(self.conv1(x)))
  28. x=self.pool2(F.relu(self.conv2(x)))
  29. #print(x.shape)
  30. x=x.view(-1,36*6*6)
  31. x=F.relu(self.fc2(F.relu(self.fc1(x))))
  32. return x
  33. class Net(nn.Module):
  34. def __init__(self):
  35. super(Net, self).__init__()
  36. self.conv1 = nn.Conv2d(3, 16, 5)
  37. self.pool1 = nn.MaxPool2d(2, 2)
  38. self.conv2 = nn.Conv2d(16, 36, 5)
  39. #self.fc1 = nn.Linear(16 * 5 * 5, 120)
  40. self.pool2 = nn.MaxPool2d(2, 2)
  41. self.aap=nn.AdaptiveAvgPool2d(1)
  42. #self.fc2 = nn.Linear(120, 84)
  43. self.fc3 = nn.Linear(36, 10)
  44. def forward(self, x):
  45. x = self.pool1(F.relu(self.conv1(x)))
  46. x = self.pool2(F.relu(self.conv2(x)))
  47. #print(x.shape)
  48. #x = x.view(-1, 16 * 5 * 5)
  49. x = self.aap(x)
  50. #print(x.shape)
  51. #x = F.relu(self.fc2(x))
  52. x = x.view(x.shape[0], -1)
  53. #print(x.shape)
  54. x = self.fc3(x)
  55. return x
  56. class LeNet(nn.Module):
  57. def __init__(self):
  58. super(LeNet, self).__init__()
  59. self.conv1 = nn.Conv2d(3, 6, 5)
  60. self.conv2 = nn.Conv2d(6, 16, 5)
  61. self.fc1 = nn.Linear(16*5*5, 120)
  62. self.fc2 = nn.Linear(120, 84)
  63. self.fc3 = nn.Linear(84, 10)
  64. def forward(self, x):
  65. out = F.relu(self.conv1(x))
  66. out = F.max_pool2d(out, 2)
  67. out = F.relu(self.conv2(out))
  68. out = F.max_pool2d(out, 2)
  69. out = out.view(out.size(0), -1)
  70. out = F.relu(self.fc1(out))
  71. out = F.relu(self.fc2(out))
  72. out = self.fc3(out)
  73. return out
  74. cfg = {
  75. 'VGG16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
  76. 'VGG19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
  77. }
  78. class VGG(nn.Module):
  79. def __init__(self, vgg_name):
  80. super(VGG, self).__init__()
  81. self.features = self._make_layers(cfg[vgg_name])
  82. self.classifier = nn.Linear(512, 10)
  83. def forward(self, x):
  84. out = self.features(x)
  85. out = out.view(out.size(0), -1)
  86. out = self.classifier(out)
  87. return out
  88. def _make_layers(self, cfg):
  89. layers = []
  90. in_channels = 3
  91. for x in cfg:
  92. if x == 'M':
  93. layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
  94. else:
  95. layers += [nn.Conv2d(in_channels, x, kernel_size=3, padding=1),
  96. nn.BatchNorm2d(x),
  97. nn.ReLU(inplace=True)]
  98. in_channels = x
  99. layers += [nn.AvgPool2d(kernel_size=1, stride=1)]
  100. return nn.Sequential(*layers)
  101. if __name__ == '__main__':
  102. freeze_support()
  103. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  104. print('==> Preparing data..')
  105. transform_train = transforms.Compose([
  106. transforms.RandomCrop(32, padding=4),
  107. transforms.RandomHorizontalFlip(),
  108. transforms.ToTensor(),
  109. transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
  110. ])
  111. transform_test = transforms.Compose([
  112. transforms.ToTensor(),
  113. transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
  114. ])
  115. trainset = torchvision.datasets.CIFAR10(root='../data', train=True, download=False, transform=transform_train)
  116. trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
  117. testset = torchvision.datasets.CIFAR10(root='../data', train=False, download=False, transform=transform_test)
  118. testloader = torch.utils.data.DataLoader(testset, batch_size=100, shuffle=False, num_workers=2)
  119. classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
  120. # Model
  121. print('==> Building model..')
  122. net1 = CNNNet()
  123. net2=Net()
  124. net3=LeNet()
  125. net4 = VGG('VGG16')
  126. #把3个网络模型放在一个列表里
  127. mlps=[net1.to(device), net2.to(device), net3.to(device)]
  128. optimizer=torch.optim.Adam([{"params":mlp.parameters()} for mlp in mlps],lr=LR)
  129. loss_function=nn.CrossEntropyLoss()
  130. for ep in range(EPOCHES):
  131. for img,label in trainloader:
  132. img,label=img.to(device),label.to(device)
  133. optimizer.zero_grad() # 10个网络清除梯度
  134. for mlp in mlps:
  135. mlp.train()
  136. out=mlp(img)
  137. loss=loss_function(out,label)
  138. loss.backward() # 网络们获得梯度
  139. optimizer.step()
  140. pre=[]
  141. vote_correct=0
  142. mlps_correct=[0 for i in range(len(mlps))]
  143. for img,label in testloader:
  144. img,label=img.to(device),label.to(device)
  145. for i, mlp in enumerate( mlps):
  146. mlp.eval()
  147. out=mlp(img)
  148. _,prediction=torch.max(out,1) #按行取最大值
  149. pre_num=prediction.cpu().numpy()
  150. mlps_correct[i]+=(pre_num==label.cpu().numpy()).sum()
  151. pre.append(pre_num)
  152. arr=np.array(pre)
  153. pre.clear()
  154. result=[Counter(arr[:,i]).most_common(1)[0][0] for i in range(BATCHSIZE)]
  155. vote_correct+=(result == label.cpu().numpy()).sum()
  156. print("epoch:" + str(ep)+"集成模型的正确率"+str(vote_correct/len(testloader)))
  157. for idx, coreect in enumerate( mlps_correct):
  158. print("模型"+str(idx)+"的正确率为:"+str(coreect/len(testloader)))
  159. # 使用VGG模型
  160. mlps=[net4.to(device)]
  161. optimizer=torch.optim.Adam([{"params":mlp.parameters()} for mlp in mlps],lr=LR)
  162. loss_function=nn.CrossEntropyLoss()
  163. for ep in range(EPOCHES):
  164. for img,label in trainloader:
  165. img,label=img.to(device),label.to(device)
  166. optimizer.zero_grad()
  167. for mlp in mlps:
  168. mlp.train()
  169. out=mlp(img)
  170. loss=loss_function(out,label)
  171. loss.backward()
  172. optimizer.step()
  173. pre=[]
  174. vote_correct=0
  175. mlps_correct=[0 for i in range(len(mlps))]
  176. for img,label in testloader:
  177. img,label=img.to(device),label.to(device)
  178. for i, mlp in enumerate( mlps):
  179. mlp.eval()
  180. out=mlp(img)
  181. _,prediction=torch.max(out,1) # 按行取最大值
  182. pre_num=prediction.cpu().numpy()
  183. mlps_correct[i]+=(pre_num==label.cpu().numpy()).sum()
  184. pre.append(pre_num)
  185. arr=np.array(pre)
  186. pre.clear()
  187. result=[Counter(arr[:,i]).most_common(1)[0][0] for i in range(BATCHSIZE)]
  188. vote_correct+=(result == label.cpu().numpy()).sum()
  189. #print("epoch:" + str(ep)+"集成模型的正确率"+str(vote_correct/len(testloader)))
  190. for idx, coreect in enumerate( mlps_correct):
  191. print("VGG16模型迭代"+str(ep)+"次的正确率为:"+str(coreect/len(testloader)))

输出

  1. ...
  2. epoch:15集成模型的正确率66.7
  3. 模型0的正确率为:55.75
  4. 模型1的正确率为:59.67
  5. 模型2的正确率为:65.38
  6. epoch:16集成模型的正确率67.86
  7. 模型0的正确率为:59.64
  8. 模型1的正确率为:59.86
  9. 模型2的正确率为:65.57
  10. epoch:17集成模型的正确率68.23
  11. 模型0的正确率为:59.8
  12. 模型1的正确率为:58.85
  13. 模型2的正确率为:66.36
  14. epoch:18集成模型的正确率68.65
  15. 模型0的正确率为:60.58
  16. 模型1的正确率为:59.86
  17. 模型2的正确率为:65.6
  18. epoch:19集成模型的正确率69.11
  19. 模型0的正确率为:60.18
  20. 模型1的正确率为:61.16
  21. 模型2的正确率为:67.07
  22. VGG16模型迭代0次的正确率为:48.18
  23. VGG16模型迭代1次的正确率为:64.58
  24. VGG16模型迭代2次的正确率为:72.19

生成式深度学习

变分自编码器 AVE

自编码器
自编码器是通过对输入X进行编码后得到一个低维的向量z,然后根据这个向量还原出输入~X。通过对比 X 与 ~X 的误差,再利用神经网络去训练使得误差逐渐减小,从而达到非监督学习的目的。
image.png
变分自编码器
自编码器因不能随意产生合理的潜在变量,从而导致它无法产生新的内容。因为潜在变量Z都是编码器从原始图片中产生的。
为解决这一问题,研究人员对潜在空间Z(潜在变量对应的空间)增加一些约束,使Z满足正态分布,由此就出现了VAE模型,VAE对编码器添加约束,就是强迫它产生服从单位正态分布的潜在变量。
那么,如何确定这个正态分布就成主要目标,即确定均值 u 和标准差 σ 这两个参数。
image.png
在图8-2中,模块①,输入样本 X 通过编码器,输出向量 mu、 log_var,这两个 m 维向量是潜在空间(假设满足正态分布)的两个参数(相当于均值和方差)。
假设潜在正态分布能生成输入图像,从标准正态分布 N(0,I) 中采样一个 ε(模块②)。
使 Z=mu+exp(log_var)*ε(模块③),Z是从潜在空间抽取的一个向量。
Z 通过解码器生成一个样本~X(模块④)。
这里ε是随机采样的,这就可保证潜在空间的连续性、良好的结构性。而这些特性使得潜在空间的每个方向都表示数据中有意义的变化方向。

AVE模型定义

  1. import torch.nn as nn
  2. import torch.nn.functional as F
  3. class VAE(nn.Module):
  4. def __init__(self, image_size=784, h_dim=400, z_dim=20):
  5. super(VAE, self).__init__()
  6. self.fc1 = nn.Linear(image_size, h_dim)
  7. self.fc2 = nn.Linear(h_dim, z_dim)
  8. self.fc3 = nn.Linear(h_dim, z_dim)
  9. self.fc4 = nn.Linear(z_dim, h_dim)
  10. self.fc5 = nn.Linear(h_dim, image_size)
  11. def encode(self, x):
  12. h = F.relu(self.fc1(x))
  13. return self.fc2(h), self.fc3(h)
  14. def reparameterize(self, mu, log_var):
  15. std = torch.exp(log_var/2)
  16. eps = torch.randn_like(std)
  17. return mu + eps * std
  18. def decode(self, z):
  19. h = F.relu(self.fc4(z))
  20. return F.sigmoid(self.fc5(h))
  21. def forward(self, x):
  22. mu, log_var = self.encode(x)
  23. z = self.reparameterize(mu, log_var)
  24. x_reconst = self.decode(z)
  25. return x_reconst, mu, log_var

这里构建网络主要用全连接层,也可以换成卷积层(如nn.Conv2d),相应的解码器使用反卷积层(
nn.ConvTranspose2d)。

反向传播中,损失函数是衡量模型优劣的主要指标。这里我们需要从以下两个方面进行衡量:
1)生成的新图像与原图像的相似度;
2)隐含空间的分布与正态分布的相似度。
度量图像的相似度一般采用交叉熵(如nn.BCELoss),度量两个分布的相似度一般采用KL散度(Kullback-Leibler divergence)。这两个度量的和构成了整个模型的损失函数。

用变分自编码器生成图像

  1. import os
  2. import torch
  3. import torch.nn as nn
  4. import torch.nn.functional as F
  5. import torchvision
  6. from torchvision import transforms
  7. from torchvision.utils import save_image
  8. # 设备配置
  9. torch.cuda.set_device(1) # 这句用来设置pytorch在哪块GPU上运行,这里假设使用序号为1的这块GPU.
  10. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  11. #在当前目录,创建不存在的目录ave_samples
  12. sample_dir = 'ave_samples'
  13. if not os.path.exists(sample_dir):
  14. os.makedirs(sample_dir)
  15. # 定义一些超参数
  16. image_size = 784
  17. h_dim = 400
  18. z_dim = 20
  19. num_epochs = 30
  20. batch_size = 128
  21. learning_rate = 0.001
  22. # 下载MNIST训练集,这里因已下载,故download=False
  23. dataset = torchvision.datasets.MNIST(root='data',
  24. train=True,
  25. transform=transforms.ToTensor(),
  26. download=False)
  27. #数据加载
  28. data_loader = torch.utils.data.DataLoader(dataset=dataset,
  29. batch_size=batch_size,
  30. shuffle=True)
  31. # 定义AVE模型
  32. class VAE(nn.Module):
  33. def __init__(self, image_size=784, h_dim=400, z_dim=20):
  34. super(VAE, self).__init__()
  35. self.fc1 = nn.Linear(image_size, h_dim)
  36. self.fc2 = nn.Linear(h_dim, z_dim)
  37. self.fc3 = nn.Linear(h_dim, z_dim)
  38. self.fc4 = nn.Linear(z_dim, h_dim)
  39. self.fc5 = nn.Linear(h_dim, image_size)
  40. def encode(self, x):
  41. h = F.relu(self.fc1(x))
  42. return self.fc2(h), self.fc3(h)
  43. def reparameterize(self, mu, log_var):
  44. std = torch.exp(log_var/2)
  45. eps = torch.randn_like(std)
  46. return mu + eps * std
  47. def decode(self, z):
  48. h = F.relu(self.fc4(z))
  49. return F.sigmoid(self.fc5(h))
  50. def forward(self, x):
  51. mu, log_var = self.encode(x)
  52. z = self.reparameterize(mu, log_var)
  53. x_reconst = self.decode(z)
  54. return x_reconst, mu, log_var
  55. model = VAE().to(device)
  56. optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
  57. #开始训练模型
  58. for epoch in range(num_epochs):
  59. model.train()
  60. for i, (x, _) in enumerate(data_loader):
  61. # 前向传播
  62. model.zero_grad()
  63. x = x.to(device).view(-1, image_size)
  64. x_reconst, mu, log_var = model(x)
  65. # Compute reconstruction loss and kl divergence
  66. # For KL divergence, see Appendix B in VAE paper or http://yunjey47.tistory.com/43
  67. reconst_loss = F.binary_cross_entropy(x_reconst, x, size_average=False)
  68. kl_div = - 0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
  69. #反向传播及优化器
  70. loss = reconst_loss + kl_div
  71. optimizer.zero_grad()
  72. loss.backward()
  73. optimizer.step()
  74. if (i+1) % 100 == 0:
  75. print ("Epoch[{}/{}], Step [{}/{}], Reconst Loss: {:.4f}, KL Div: {:.4f}"
  76. .format(epoch+1, num_epochs, i+1, len(data_loader), reconst_loss.item(), kl_div.item()))
  77. with torch.no_grad():
  78. # 保存采样图像,即潜在向量Z通过解码器生成的新图像
  79. z = torch.randn(batch_size, z_dim).to(device)
  80. out = model.decode(z).view(-1, 1, 28, 28)
  81. save_image(out, os.path.join(sample_dir, 'sampled-{}.png'.format(epoch+1)))
  82. # 保存重构图像,即原图像通过解码器生成的图像
  83. out, _, _ = model(x)
  84. x_concat = torch.cat([x.view(-1, 1, 28, 28), out.view(-1, 1, 28, 28)], dim=3)
  85. save_image(x_concat, os.path.join(sample_dir, 'reconst-{}.png'.format(epoch+1)))
  1. reconsPath = './ave_samples/reconst-30.png'
  2. Image = mpimg.imread(reconsPath)
  3. plt.imshow(Image) # 显示图片
  4. plt.axis('off') # 不显示坐标轴
  5. plt.show()

image.png

  1. genPath = './ave_samples/sampled-30.png'
  2. Image = mpimg.imread(genPath)
  3. plt.imshow(Image) # 显示图片
  4. plt.axis('off') # 不显示坐标轴
  5. plt.show()

image.png

GAN

VAE利用潜在空间,可以生成连续的新图像,不过因损失函数采用像素间的距离,所以图像有点模糊。GAN替换了VAE的潜在空间,它能够迫使生成图像与真实图像在统计上几乎无法区别的逼真合成图像。

GAN的优化过程不像通常的求损失函数的最小值,而是保持生成与判别两股力量的动态平衡。因此,其训练过程要比一般神经网络难很多。
image.png
如何定义损失函数是整个GAN的关键。

用GAN生成图像

为便于说明GAN的关键环节,这里我们弱化了网络和数据集的复杂度。数据集为MNIST,网络用全连接层。
判别器使用LeakyReLU为激活函数,输出一个节点并经过Sigmoid后输出,用于真假二分类。
生成器与AVE的生成器类似,不同的地方是输出为nn.tanh,使用nn.tanh将使数据分布在[-1,1]之间。其输入是潜在空间的向量z,输出维度与真图像相同。

  1. import os
  2. import torch
  3. import torchvision
  4. import torch.nn as nn
  5. from torchvision import transforms
  6. from torchvision.utils import save_image
  7. # 设备配置
  8. torch.cuda.set_device(1) # 这句用来设置pytorch在哪块GPU上运行,这里假设使用序号为1的这块GPU.
  9. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  10. # 定义一些超参数
  11. latent_size = 64
  12. hidden_size = 256
  13. image_size = 784
  14. num_epochs = 200
  15. batch_size = 100
  16. sample_dir = 'gan_samples'
  17. # 在当前目录,创建不存在的目录gan_samples
  18. if not os.path.exists(sample_dir):
  19. os.makedirs(sample_dir)
  20. # Image processing
  21. trans = transforms.Compose([
  22. transforms.ToTensor(),transforms.Normalize([0.5], [0.5])])
  23. # MNIST dataset
  24. mnist = torchvision.datasets.MNIST(root='data3',
  25. train=True,
  26. transform=trans,
  27. download=False)
  28. # Data loader
  29. data_loader = torch.utils.data.DataLoader(dataset=mnist,
  30. batch_size=batch_size,
  31. shuffle=True)
  1. # 构建判断器
  2. D = nn.Sequential(
  3. nn.Linear(image_size, hidden_size),
  4. nn.LeakyReLU(0.2),
  5. nn.Linear(hidden_size, hidden_size),
  6. nn.LeakyReLU(0.2),
  7. nn.Linear(hidden_size, 1),
  8. nn.Sigmoid())
  9. # 构建生成器,这个相当于AVE中的解码器
  10. G = nn.Sequential(
  11. nn.Linear(latent_size, hidden_size),
  12. nn.ReLU(),
  13. nn.Linear(hidden_size, hidden_size),
  14. nn.ReLU(),
  15. nn.Linear(hidden_size, image_size),
  16. nn.Tanh())
  17. # 把判别器和生成器迁移到GPU上
  18. D = D.to(device)
  19. G = G.to(device)
  20. # 定义判别器的损失函数交叉熵及优化器
  21. criterion = nn.BCELoss()
  22. d_optimizer = torch.optim.Adam(D.parameters(), lr=0.0002)
  23. g_optimizer = torch.optim.Adam(G.parameters(), lr=0.0002)
  24. #Clamp函数x限制在区间[min, max]内
  25. def denorm(x):
  26. out = (x + 1) / 2
  27. return out.clamp(0, 1)
  28. def reset_grad():
  29. d_optimizer.zero_grad()
  30. g_optimizer.zero_grad()
  31. # 开始训练
  32. total_step = len(data_loader)
  1. for epoch in range(num_epochs):
  2. for i, (images, _) in enumerate(data_loader):
  3. images = images.reshape(batch_size, -1).to(device)
  4. # 定义图像是真或假的标签
  5. real_labels = torch.ones(batch_size, 1).to(device)
  6. fake_labels = torch.zeros(batch_size, 1).to(device)
  7. # ================================================================== #
  8. # 训练判别器 #
  9. # ================================================================== #
  10. # 定义判断器对真图片的损失函数
  11. outputs = D(images)
  12. d_loss_real = criterion(outputs, real_labels)
  13. real_score = outputs
  14. # 定义判别器对假图片(即由潜在空间点生成的图片)的损失函数
  15. z = torch.randn(batch_size, latent_size).to(device)
  16. fake_images = G(z)
  17. outputs = D(fake_images)
  18. d_loss_fake = criterion(outputs, fake_labels)
  19. fake_score = outputs
  20. # 得到判别器总的损失函数
  21. d_loss = d_loss_real + d_loss_fake
  22. # 对生成器、判别器的梯度清零
  23. reset_grad()
  24. d_loss.backward()
  25. d_optimizer.step()
  26. # ================================================================== #
  27. # 训练生成器 #
  28. # ================================================================== #
  29. # 定义生成器对假图片的损失函数,这里我们要求
  30. #判别器生成的图片越来越像真图片,故损失函数中
  31. #的标签改为真图片的标签,即希望生成的假图片,
  32. #越来越靠近真图片
  33. z = torch.randn(batch_size, latent_size).to(device)
  34. fake_images = G(z)
  35. outputs = D(fake_images)
  36. g_loss = criterion(outputs, real_labels)
  37. # 对生成器、判别器的梯度清零
  38. #进行反向传播及运行生成器的优化器
  39. reset_grad()
  40. g_loss.backward()
  41. g_optimizer.step()
  42. if (i+1) % 200 == 0:
  43. print('Epoch [{}/{}], Step [{}/{}], d_loss: {:.4f}, g_loss: {:.4f}, D(x): {:.2f}, D(G(z)): {:.2f}'
  44. .format(epoch, num_epochs, i+1, total_step, d_loss.item(), g_loss.item(),
  45. real_score.mean().item(), fake_score.mean().item()))
  46. # 保存真图片
  47. if (epoch+1) == 1:
  48. images = images.reshape(images.size(0), 1, 28, 28)
  49. save_image(denorm(images), os.path.join(sample_dir, 'real_images.png'))
  50. # 保存假图片
  51. fake_images = fake_images.reshape(fake_images.size(0), 1, 28, 28)
  52. save_image(denorm(fake_images), os.path.join(sample_dir, 'fake_images-{}.png'.format(epoch+1)))
  53. # 保存模型
  54. torch.save(G.state_dict(), 'G.ckpt')
  55. torch.save(D.state_dict(), 'D.ckpt')
  1. reconsPath = './gan_samples/real_images.png'
  2. Image = mpimg.imread(reconsPath)
  3. plt.imshow(Image) # 显示图片
  4. plt.axis('off') # 不显示坐标轴
  5. plt.show()

Python深度学习:基于PyTorch - 图11

  1. reconsPath = './gan_samples/fake_images-200.png'
  2. Image = mpimg.imread(reconsPath)
  3. plt.imshow(Image) # 显示图片
  4. plt.axis('off') # 不显示坐标轴
  5. plt.show()

Python深度学习:基于PyTorch - 图12

Condition GAN

不过它们生成的都是随机的,无法预先控制你要生成的哪类或哪个数。如果在生成新图像的同时,能加上一个目标控制那就太好了,于是基于条件的GAN被提出,即 Condition GAN,简称为CGAN。
其基本架构与GAN类似,只要添加一个条件y即可,y就是加入的监督信息。比如说MNIST数据集可以提供某个数字的标签信息,人脸生成可以提供性别、是否微笑、年龄等信息,带某个主题的图像等标签信息。
image.png
对生成器输入一个从潜在空间随机采样的一个向量z及一个条件y,生成一个符合该条件的图像 G(z/y)。对判别器来说,输入一张图像x和条件y,输出该图像在该条件下的概率 D(x/y)。

CGAN具体实现

  1. import os
  2. import matplotlib.pyplot as plt
  3. import numpy as np
  4. import torch
  5. import torchvision
  6. import torch.nn as nn
  7. from torchvision import transforms
  8. from torch import autograd
  9. from torchvision.utils import save_image
  10. from torchvision.utils import make_grid
  11. from tensorboardX import SummaryWriter
  12. # 设备配置
  13. torch.cuda.set_device(1) # 这句用来设置pytorch在哪块GPU上运行,这里假设使用序号为1的这块GPU.
  14. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  15. # 定义一些超参数
  16. num_epochs = 50
  17. batch_size = 100
  18. sample_dir = 'cgan_samples'
  19. # 在当前目录,创建不存在的目录gan_samples
  20. if not os.path.exists(sample_dir):
  21. os.makedirs(sample_dir)
  22. # Image processing
  23. trans = transforms.Compose([
  24. transforms.ToTensor(),
  25. transforms.Normalize([0.5], [0.5])])
  26. # MNIST dataset
  27. mnist = torchvision.datasets.MNIST(root='data3',
  28. train=True,
  29. transform=trans,
  30. download=False)
  31. # Data loader
  32. data_loader = torch.utils.data.DataLoader(dataset=mnist,
  33. batch_size=batch_size,
  34. shuffle=True)
  1. class Discriminator(nn.Module):
  2. def __init__(self):
  3. super().__init__()
  4. self.label_emb = nn.Embedding(10, 10)
  5. self.model = nn.Sequential(
  6. nn.Linear(794, 1024),
  7. nn.LeakyReLU(0.2, inplace=True),
  8. nn.Dropout(0.4),
  9. nn.Linear(1024, 512),
  10. nn.LeakyReLU(0.2, inplace=True),
  11. nn.Dropout(0.4),
  12. nn.Linear(512, 256),
  13. nn.LeakyReLU(0.2, inplace=True),
  14. nn.Dropout(0.4),
  15. nn.Linear(256, 1),
  16. nn.Sigmoid()
  17. )
  18. def forward(self, x, labels):
  19. x = x.view(x.size(0), 784)
  20. c = self.label_emb(labels)
  21. x = torch.cat([x, c], 1)
  22. out = self.model(x)
  23. return out.squeeze()
  1. class Generator(nn.Module):
  2. def __init__(self):
  3. super().__init__()
  4. self.label_emb = nn.Embedding(10, 10)
  5. self.model = nn.Sequential(
  6. nn.Linear(110, 256),
  7. nn.LeakyReLU(0.2, inplace=True),
  8. nn.Linear(256, 512),
  9. nn.LeakyReLU(0.2, inplace=True),
  10. nn.Linear(512, 1024),
  11. nn.LeakyReLU(0.2, inplace=True),
  12. nn.Linear(1024, 784),
  13. nn.Tanh()
  14. )
  15. def forward(self, z, labels):
  16. z = z.view(z.size(0), 100)
  17. c = self.label_emb(labels)
  18. x = torch.cat([z, c], 1)
  19. out = self.model(x)
  20. return out.view(x.size(0), 28, 28)
  1. # 设备配置
  2. torch.cuda.set_device(1) # 这句用来设置pytorch在哪块GPU上运行,这里假设使用序号为1的这块GPU.
  3. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  4. G = Generator().to(device)
  5. D = Discriminator().to(device)
  6. # 定义判别器的损失函数交叉熵及优化器
  7. criterion = nn.BCELoss()
  8. d_optimizer = torch.optim.Adam(D.parameters(), lr=0.0001)
  9. g_optimizer = torch.optim.Adam(G.parameters(), lr=0.0001)
  10. #Clamp函数x限制在区间[min, max]内
  11. def denorm(x):
  12. out = (x + 1) / 2
  13. return out.clamp(0, 1)
  14. def reset_grad():
  15. d_optimizer.zero_grad()
  16. g_optimizer.zero_grad()
  17. # 开始训练
  18. total_step = len(data_loader)
  1. writer = SummaryWriter(log_dir='logs')
  2. for epoch in range(num_epochs):
  3. for i, (images,labels) in enumerate(data_loader):
  4. step = epoch * len(data_loader) + i + 1
  5. images = images.to(device)
  6. labels = labels.to(device)
  7. # 定义图像是真或假的标签
  8. real_labels = torch.ones(batch_size).to(device)
  9. fake_labels =torch.randint(0,10,(batch_size,)).to(device)
  10. # ================================================================== #
  11. # 训练判别器 #
  12. # ================================================================== #
  13. # 定义判断器对真图片的损失函数
  14. real_validity = D(images, labels)
  15. d_loss_real = criterion(real_validity, real_labels)
  16. real_score = real_validity
  17. # 定义判别器对假图片(即由潜在空间点生成的图片)的损失函数
  18. z = torch.randn(batch_size, 100).to(device)
  19. fake_labels = torch.randint(0,10,(batch_size,)).to(device)
  20. fake_images = G(z, fake_labels)
  21. fake_validity = D(fake_images, fake_labels)
  22. d_loss_fake = criterion(fake_validity, torch.zeros(batch_size).to(device))
  23. fake_score =fake_images
  24. d_loss = d_loss_real + d_loss_fake
  25. # 对生成器、判别器的梯度清零
  26. reset_grad()
  27. d_loss.backward()
  28. d_optimizer.step()
  29. # ================================================================== #
  30. # 训练生成器 #
  31. # ================================================================== #
  32. # 定义生成器对假图片的损失函数,这里我们要求
  33. #判别器生成的图片越来越像真图片,故损失函数中
  34. #的标签改为真图片的标签,即希望生成的假图片,
  35. #越来越靠近真图片
  36. z = torch.randn(batch_size, 100).to(device)
  37. fake_images = G(z, fake_labels)
  38. validity = D(fake_images, fake_labels)
  39. g_loss = criterion(validity, torch.ones(batch_size).to(device))
  40. # 对生成器、判别器的梯度清零
  41. #进行反向传播及运行生成器的优化器
  42. reset_grad()
  43. g_loss.backward()
  44. g_optimizer.step()
  45. writer.add_scalars('scalars', {'g_loss': g_loss, 'd_loss': d_loss}, step)
  46. if (i+1) % 200 == 0:
  47. print('Epoch [{}/{}], Step [{}/{}], d_loss: {:.4f}, g_loss: {:.4f}, D(x): {:.2f}, D(G(z)): {:.2f}'
  48. .format(epoch, num_epochs, i+1, total_step, d_loss.item(), g_loss.item(),
  49. real_score.mean().item(), fake_score.mean().item()*(-1)))
  50. # 保存真图片
  51. if (epoch+1) == 1:
  52. images = images.reshape(images.size(0), 1, 28, 28)
  53. save_image(denorm(images), os.path.join(sample_dir, 'real_images.png'))
  54. # 保存假图片
  55. fake_images = fake_images.reshape(fake_images.size(0), 1, 28, 28)
  56. save_image(denorm(fake_images), os.path.join(sample_dir, 'fake_images-{}.png'.format(epoch+1)))
  57. # 保存模型
  58. torch.save(G.state_dict(), 'G.ckpt')
  59. torch.save(D.state_dict(), 'D.ckpt')
  1. import matplotlib.pyplot as plt # plt 用于显示图片
  2. import matplotlib.image as mpimg # mpimg 用于读取图片
  3. reconsPath = './cgan_samples/real_images.png'
  4. Image = mpimg.imread(reconsPath)
  5. plt.imshow(Image) # 显示图片
  6. plt.axis('off') # 不显示坐标轴
  7. plt.show()

Python深度学习:基于PyTorch - 图14

  1. reconsPath = './cgan_samples/fake_images-50.png'
  2. Image = mpimg.imread(reconsPath)
  3. plt.imshow(Image) # 显示图片
  4. plt.axis('off') # 不显示坐标轴
  5. plt.show()

Python深度学习:基于PyTorch - 图15

  1. from torchvision.utils import make_grid
  2. z = torch.randn(100, 100).to(device)
  3. labels = torch.LongTensor([i for i in range(10) for _ in range(10)]).to(device)
  4. images = G(z, labels).unsqueeze(1)
  5. grid = make_grid(images, nrow=10, normalize=True)
  6. fig, ax = plt.subplots(figsize=(10,10))
  7. ax.imshow(grid.permute(1, 2, 0).detach().cpu().numpy(), cmap='binary')
  8. ax.axis('off')

(-0.5, 301.5, 301.5, -0.5)
Python深度学习:基于PyTorch - 图16

  1. def generate_digit(generator, digit):
  2. z = torch.randn(1, 100).to(device)
  3. label = torch.LongTensor([digit]).to(device)
  4. img = generator(z, label).detach().cpu()
  5. img = 0.5 * img + 0.5
  6. return transforms.ToPILImage()(img)
  7. generate_digit(G, 8)

Python深度学习:基于PyTorch - 图17