好久不看这玩意了,时间紧迫,争取半个月内全把整本书过一遍~

二维互相关运算

这里先补充一下之前的一个误区,关于pytorch中矩阵乘法的问题。之前我一直以为tensortensor与torch.mm (tensor, tensor)的效果是一样的。实则不然,前者表示的实际上是按元素相乘求和的运算,而后者表示的才是矩阵乘法运算。tensortensor要求两个张量维度完全相同,这也是之前即使两个矩阵是ab与bc维度,使用*相乘时仍然报错的问题。

  1. X = torch.tensor([[0, 1], [3, 4]])
  2. Y = torch.tensor([[0, 1], [3, 4]])
  3. print(X*Y, torch.mm(X, Y))
  4. 结果:
  5. tensor([[ 0, 1],
  6. [ 9, 16]])
  7. tensor([[ 3, 4],
  8. [12, 19]])

关于pytorch中矩阵剪裁,比方说一个提取一个33的矩阵左上角22的矩阵,可以通过定义下标访问范围达到该目的:

  1. X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
  2. print(X[0: 2, 0: 2])
  3. 输出:
  4. tensor([[0, 1],
  5. [3, 4]])

所以,我们可以很容易定义出一个矩阵与一个卷积核之间的互相关操作:

  1. def corr2d(x, k):
  2. h, w = k.shape
  3. target = torch.zeros(x.shape[0] - h + 1, x.shape[1] - w + 1)
  4. for i in range(target.shape[0]):
  5. for j in range(target.shape[1]):
  6. target[i][j] = (x[i: i + h, j: j + w] * k).sum()
  7. return target
  8. X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
  9. Y = torch.tensor([[0, 1], [2, 3]])
  10. print(corr2d(X, Y))
  11. 结果:
  12. tensor([[19., 25.],
  13. [37., 43.]])

二维卷积层

定义了互相关运算之后,我们就可以手动定义一个卷积层放入网络进行卷积操作了。注意初始化卷积核以及偏置项时不要忘记加上nn.Parameter,否则模型不知道这两者也是该模型的参数之一。

  1. class Conv2D(nn.Module):
  2. def __init__(self, kernel_size):
  3. super(Conv2D, self).__init__()
  4. self.kernel = nn.Parameter(torch.randn(kernel_size))
  5. self.bias = nn.Parameter(torch.randn(1))
  6. def forward(self, x):
  7. target = corr2d(x, self.kernel) + self.bias
  8. return target

边缘检测

之前吴恩达机器学习课程中讲述的其实非常清楚了,不同的卷积核具有不同的功能,比如图像边缘检测等。此处我们可以初始化一幅灰度图,其中图像左右两端灰度为1,中间部分灰度为0:

  1. pic = torch.ones(6, 8)
  2. pic[:, 2:6] = 0
  3. print(pic)
  4. 输出:
  5. tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
  6. [1., 1., 0., 0., 0., 0., 1., 1.],
  7. [1., 1., 0., 0., 0., 0., 1., 1.],
  8. [1., 1., 0., 0., 0., 0., 1., 1.],
  9. [1., 1., 0., 0., 0., 0., 1., 1.],
  10. [1., 1., 0., 0., 0., 0., 1., 1.]])

再将该图像进行一次卷积运算:

  1. kernel = torch.tensor([[1, -1]])
  2. pic = corr2d(pic, kernel)
  3. print(pic)
  4. 结果:
  5. tensor([[ 0., 1., 0., 0., 0., -1., 0.],
  6. [ 0., 1., 0., 0., 0., -1., 0.],
  7. [ 0., 1., 0., 0., 0., -1., 0.],
  8. [ 0., 1., 0., 0., 0., -1., 0.],
  9. [ 0., 1., 0., 0., 0., -1., 0.],
  10. [ 0., 1., 0., 0., 0., -1., 0.]])

卷积核参数学习

先前我们之所以能够检测出一幅图像的边缘,是因为我们将卷积核设置为了[1, -1]。那么我们如果已知一幅图像以及通过卷积核计算后的结果,能不能学习出卷积核的参数呢?答案是可以的。
我们可以定义如下的函数进行训练,要注意由于进行梯度下降的过程中我们不希望梯度被追踪到,因此我们需要对kernel与bias这两个参数的data进行运算操作,这样既可以修改参数的值,又可以不被计算图追踪:

  1. def train(net, step, lr, x, y):
  2. for i in range(step):
  3. y_hat = net(x)
  4. l = ((y - y_hat) ** 2).sum()
  5. l.backward()
  6. net.kernel.data -= lr * net.kernel.grad
  7. net.bias.data -= lr * net.bias.grad
  8. conv2d.kernel.grad.fill_(0)
  9. conv2d.bias.grad.fill_(0)
  10. if (i + 1) % 5 == 0:
  11. print('Step %d, loss %.3f' % (i + 1, l.item()))
  12. if __name__ == "__main__":
  13. print(pic)
  14. kernel = torch.tensor([[1, -1]])
  15. label = corr2d(pic, kernel)
  16. conv2d = Conv2D(kernel_size=(1, 2))
  17. train(conv2d, 20, 0.01, pic, label)
  18. 输出:
  19. Step 5, loss 2.141
  20. Step 10, loss 0.295
  21. Step 15, loss 0.048
  22. Step 20, loss 0.010

梯度清零还可以用如下操作完成,效果是一样的:

  1. net.kernel.grad.data.zero_()
  2. net.bias.grad.data.zero_()