Run this notebook online: or Colab:
6.2. 图像的卷积¶
现在我们了解了卷积层在理论上的工作原理, 我们准备看看它们在实践中是如何工作的。 基于我们对卷积神经网络的研究 作为探索图像数据结构的有效架构, 我们坚持以图像为榜样。
6.2.1. 互关联算子¶
回想一下,严格来说,卷积层 是一个(轻微的)用词不当,因为它们表达的操作 更准确地说是互相关。 在卷积层中,输入数组 和一个相关内核数组相结合 通过互相关操作生成输出阵列。 让我们暂时忽略频道,看看它是如何工作的 具有二维数据和隐藏表示。 In Fig. 6.2.1, 输入是一个二维数组 高度为3,宽度为3。 我们将数组的形状标记为\(3 \times 3\) 或 (\(3\), \(3\)). 内核的高度和宽度都是\(2\). 请注意,在深度学习研究社区, 一个过滤器,或者只是图层的权重。 内核窗口的形状 由内核的高度和宽度给出 (这里是:math:2 times 2)。
在二维互相关运算中, 我们从卷积窗口开始 在输入数组的左上角 然后将其滑动到输入阵列上, 从左到右,从上到下。 当卷积窗口滑动到某个位置时, 该窗口中包含的输入子数组 内核数组相乘(elementwise) 然后将得到的数组求和 产生单个标量值。 这个结果给出了输出数组的值 在相应的位置。 这里,输出数组的高度为2,宽度为2 这四个元素是从 二维互相关运算:
请注意,沿每个轴,输出 略小于输入。 因为内核的宽度和高度都大于1,
我们只能正确地计算互相关 对于内核完全位于图像中的位置,
输出大小由输入大小 \(H \times W\)给出 减去卷积内核的大小
\(h \times w\) 通过 \((H-h+1) \times (W-w+1)\)。
这是因为我们需要足够的空间 在图像上’移动’卷积核
(稍后我们将看到如何保持大小不变 通过在图像边界周围填充零
这样就有足够的空间来移动内核)。 接下来,我们在 corr2d
函数中实现这个过程, 它接受输入数组 X
和内核数组 K
并返回输出数组 Y
.
但首先我们将导入相关的库。
%load ../utils/djl-imports
public NDArray corr2d(NDArray X, NDArray K){
// 计算二维互关联。
int h = (int) K.getShape().get(0);
int w = (int) K.getShape().get(1);
NDArray Y = manager.zeros(new Shape(X.getShape().get(0) - h + 1, X.getShape().get(1) - w + 1));
for(int i=0; i < Y.getShape().get(0); i++){
for(int j=0; j < Y.getShape().get(1); j++){
Y.set(new NDIndex(i + "," + j), X.get(i + ":" + (i+h) + "," + j + ":" + (j+w)).mul(K).sum());
}
}
return Y;
}
我们可以构造输入数组X
和内核数组K
从上图来看
验证上述实现的输出 二维互相关运算。
NDManager manager = NDManager.newBaseManager();
NDArray X = manager.create(new float[]{0,1,2,3,4,5,6,7,8}, new Shape(3,3));
NDArray K = manager.create(new float[]{0,1,2,3}, new Shape(2,2));
System.out.println(corr2d(X, K));
ND: (2, 2) gpu(0) float32
[[19., 25.],
[37., 43.],
]
6.2.2. 卷积层¶
卷积层将输入和内核相互关联 并添加标量偏差以产生输出。 卷积层的两个参数 是内核和标量偏差。 当训练基于卷积层的模型时, 我们通常会随机初始化内核, 就像我们使用完全连接的层一样。
我们现在准备实现一个二维卷积层 基于上面定义的 corr2d
函数。 在
ConvolutionalLayer
构造函数中, 我们声明 weight
和 bias
为两个类参数。 正向计算函数 forward
调用corr2d
函数并添加偏差。 与 \(h \times w\) 互相关一样 我们也提到卷积层 作为
\(h \times w\) 卷积。
public class ConvolutionalLayer{
private NDArray w;
private NDArray b;
public NDArray getW(){
return w;
}
public NDArray getB(){
return b;
}
public ConvolutionalLayer(Shape shape){
NDManager manager = NDManager.newBaseManager();
w = manager.create(shape);
b = manager.randomNormal(new Shape(1));
w.setRequiresGradient(true);
}
public NDArray forward(NDArray X){
return corr2d(X, w).add(b);
}
}
6.2.3. 图像中的目标边缘检测¶
让我们花一点时间来分析卷积层的简单应用: 检测图像中物体的边缘 通过找到像素变化的位置。 首先,我们构造一个 \(6\times 8\) 像素的’图像’。 中间四列为黑色(0),其余为白色(1)。
X = manager.ones(new Shape(6,8));
X.set(new NDIndex(":" + "," + 2 + ":" + 6), 0f);
System.out.println(X);
ND: (6, 8) gpu(0) float32
[[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
]
接下来,我们构造一个内核K
,高度为\(1\),宽度为\(2\)。
当我们对输入进行互相关运算时, 如果水平相邻的元素相同,
输出为0。否则,输出为非零。
K = manager.create(new float[]{1, -1}, new Shape(1,2));
我们已经准备好执行互相关操作
参数为X
(我们的输入)和K
(我们的内核)。
正如你所看到的,我们从白色到黑色的边缘检测到1
和-1表示从黑色到白色的边缘。 所有其他输出值为\(0\)。
NDArray Y = corr2d(X, K);
Y
ND: (6, 7) gpu(0) float32
[[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.],
]
我们现在可以将内核应用于转置图像。
正如所料,它消失了。内核K
只检测垂直边缘。
corr2d(X.transpose(), K);
ND: (8, 5) gpu(0) float32
[[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
]
6.2.4. 学习内核¶
用有限差分[1, -1]
设计一个边缘检测器非常简洁
如果我们知道这正是我们想要的。 然而,当我们看到更大的内核时,
并考虑连续的卷积层, 可能无法具体说明 每个过滤器应该手动执行的操作。
现在让我们看看是否可以学习从X
生成Y
的内核
只查看(输入、输出)对。 我们首先构造一个卷积层
并将其内核初始化为随机数组。 接下来,在每次迭代中,我们将使用平方误差
将Y
与卷积层的输出进行比较。 然后我们可以计算梯度来更新权重。
为了简单起见,在这个卷积层中, 我们将忽略这种偏见。
这一次,我们将使用DJL内置的Block
和Conv2d
类。
X = X.reshape(1,1,6,8);
Y = Y.reshape(1,1,6,7);
Loss l2Loss = Loss.l2Loss();
// 构造一个具有1个输出通道和一个
// 形核(1,2)。为了简单起见,我们忽略了这里的偏见
Block block = Conv2d.builder()
.setKernelShape(new Shape(1, 2))
.optBias(false)
.setFilters(1)
.build();
block.setInitializer(new NormalInitializer(), Parameter.Type.WEIGHT);
block.initialize(manager, DataType.FLOAT32, X.getShape());
// 二维卷积层使用四维输入和输出
// 输出格式为(例如,通道、高度、宽度),其中批次
// 大小(批次中的示例数)和通道数均为1
ParameterList params = block.getParameters();
NDArray wParam = params.get(0).getValue().getArray();
wParam.setRequiresGradient(true);
NDArray lossVal = null;
ParameterStore parameterStore = new ParameterStore(manager, false);
NDArray lossVal = null;
for (int i = 0; i < 10; i++) {
wParam.setRequiresGradient(true);
try (GradientCollector gc = Engine.getInstance().newGradientCollector()) {
NDArray yHat = block.forward(parameterStore, new NDList(X), true).singletonOrThrow();
NDArray l = l2Loss.evaluate(new NDList(Y), new NDList(yHat));
lossVal = l;
gc.backward(l);
}
// 更新内核
wParam.subi(wParam.getGradient().mul(0.40f));
if((i+1)%2 == 0){
System.out.println("batch " + (i+1) + " loss: " + lossVal.sum().getFloat());
}
}
batch 2 loss: 0.12571818
batch 4 loss: 0.09935227
batch 6 loss: 0.07851635
batch 8 loss: 0.062050212
batch 10 loss: 0.049037326
请注意,经过10次迭代后,错误已降至一个较小的值。现在我们来看看我们学习的内核数组。
ParameterList params = block.getParameters();
NDArray wParam = params.get(0).getValue().getArray();
wParam
weight: (1, 1, 1, 2) gpu(0) float32 hasGradient
[[[[ 0.4475, -0.4477],
],
],
]
事实上,学习到的内核阵列正在接近 到我们之前定义的内核数组K
。
6.2.5. 互关联和卷积¶
回想一下我们在信件前一节中的观察 在互相关算子和卷积算子之间。 上图显示了这种对应关系。 只需将内核从左下角翻转到右上角。 在这种情况下,总和中的索引被恢复, 然而,同样的结果也可以得到。 与深度学习文献中的标准术语保持一致, 我们将继续提到互相关运算 作为一种卷积,严格地说,它略有不同。
6.2.6. 总结¶
二维卷积层的核心计算是二维互相关运算。在最简单的形式中,它对二维输入数据和内核执行互相关操作,然后添加偏差。
我们可以设计一个内核来检测图像中的边缘。
我们可以从数据中学习内核的参数。
6.2.7. 练习¶
构造一个带有对角边的图像
X
。如果对其应用内核
K
,会发生什么?如果变换
X
顺序,会发生什么?如果变换
K
顺序,会发生什么?
当您尝试自动查找我们创建的
Conv2d
类的渐变时,您会看到什么样的错误消息?如何通过更改输入和内核数组将互相关运算表示为矩阵乘法?
手动设计一些内核。
二阶导数的核的形式是什么?
拉普拉斯算子的核心是什么?
积分的核心是什么?
获得\(d\)导数的内核最小大小是多少?