I\'m doing convolution of some tensors.
Here is small test in MATLAB:
ker= rand(3,4,2);
a= rand(5,7,2);
c=convn(a,ker,\'valid\');
c11
Yes, your understanding of convolution is wrong. Your formula for c11 is not convolution: you just multiplied matching indices and then summed. It's more of a dot-product operation (on tensors trimmed to the same size). I'll try to explain beginning with 1 dimension.
Entering conv([4 5 6], [2 3])
returns [8 22 27 18]
. I find it easiest to think of this in terms of multiplication of polynomials:
(4+5x+6x^2)*(2+3x) = 8+22x+27x^2+18x^3
Use the entries of each array as coefficients of a polynomial, multiply the polynomials, collect like terms, and read off the result from coefficients. The powers of x are here to keep track of what gets multiplied and added. Note that the coefficient of x^n is found in the (n+1)th entry, because powers of x begin with 0 while the indices begin with 1.
Entering conv2([2 3; 3 1], [4 5 6; 0 -1 1])
returns the matrix
8 22 27 18
12 17 22 9
0 -3 2 1
Again, this can be interpreted as multiplication of polynomials, but now we need two variables: say x and y. The coefficient of x^n y^m is found in (m+1, n+1) entry. The above output means that
(2+3x+3y+xy)*(4+5x+6x^2+0y-xy+x^2y) = 8+22x+27x^2+18x^3+12y+17xy+22x^2y+9x^3y-3xy^2+2x^2y^2+x^3y^2
Same story. You can think of the entries as coefficients of a polynomial in variables x,y,z. The polynomials get multiplied, and the coefficients of the product are the result of convolution.
This keeps only the central part of the convolution: those coefficients in which all terms of the second factor have participated. For this to be nonempty, the second array should have dimensions no greater than the first. (This is unlike the default setting, for which the order convolved arrays does not matter.) Example:
conv([4 5 6], [2 3])
returns [22 27]
(compare to the 1-dimensional example above). This corresponds to the fact that in
(4+5x+6x^2)*(2+3x) = 8+22x+27x^2+18x^3
the bolded terms got contributions from both 2 and 3x.
You almost have it correct. There are two things slightly wrong with your understanding:
You chose valid
as the convolution flag. This means that the output returned from the convolution has its size so that when you are using the kernel to sweep over the matrix, it has to fit comfortably inside the matrix itself. Therefore, the first "valid" output that is returned is actually for the computation at location (2,2,1)
of your matrix. This means that you can fit your kernel comfortably at this location, and this corresponds to position (1,1)
of the output. To demonstrate, this is what a
and ker
look like for me using your above code:
>> a
a(:,:,1) =
0.9930 0.2325 0.0059 0.2932 0.1270 0.8717 0.3560
0.2365 0.3006 0.3657 0.6321 0.7772 0.7102 0.9298
0.3743 0.6344 0.5339 0.0262 0.0459 0.9585 0.1488
0.2140 0.2812 0.1620 0.8876 0.7110 0.4298 0.9400
0.1054 0.3623 0.5974 0.0161 0.9710 0.8729 0.8327
a(:,:,2) =
0.8461 0.0077 0.5400 0.2982 0.9483 0.9275 0.8572
0.1239 0.0848 0.5681 0.4186 0.5560 0.1984 0.0266
0.5965 0.2255 0.2255 0.4531 0.5006 0.0521 0.9201
0.0164 0.8751 0.5721 0.9324 0.0035 0.4068 0.6809
0.7212 0.3636 0.6610 0.5875 0.4809 0.3724 0.9042
>> ker
ker(:,:,1) =
0.5395 0.4849 0.0970 0.3418
0.6263 0.9883 0.4619 0.7989
0.0055 0.3752 0.9630 0.7988
ker(:,:,2) =
0.2082 0.4105 0.6508 0.2669
0.4434 0.1910 0.8655 0.5021
0.7156 0.9675 0.0252 0.0674
As you can see, at position (2,2,1)
in the matrix a
, ker
can fit comfortably inside the matrix and if you recall from convolution, it is simply a sum of element-by-element products between the kernel and the subset of the matrix at position (2,2,1)
that is the same size as your kernel (actually, you need to do something else to the kernel which I will reserve for my next point - see below). Therefore, the coefficient that you are calculating is actually the output at (2,2,1)
, not at (1,1,1)
. From the gist of it though, you already know this, but I wanted to put that out there in case you didn't know.
You are forgetting that for N-D convolution, you need to flip the mask in each dimension. If you remember from 1D convolution, the mask must be flipped in the horizontally. What I mean by flipped is that you simply place the elements in reverse order. An array of [1 2 3 4]
for example would become [4 3 2 1]
. In 2D convolution, you must flip both horizontally and vertically. Therefore, you would take each row of your matrix and place each row in reverse order, much like the 1D case. Here, you would treat each row as a 1D signal and do the flipping. Once you accomplish this, you would take this flipped result, and treat each column as a 1D signal and do the flipping again.
Now, in your case for 3D, you must flip horizontally, vertically and temporally. This means that you would need to perform the 2D flipping for each slice of your matrix independently, you would then grab single columns in a 3D fashion and treat those as 1D signals. In MATLAB syntax, you would get ker(1,1,:)
, treat this as a 1D signal, then flip. You would repeat this for ker(1,2,:)
, ker(1,3,:)
etc. until you are finished with the first slice. Bear in mind that we don't go to the second slice or any of the other slices and repeat what we just did. Because you are taking a 3D section of your matrix, you are inherently operating over all of the slices for each 3D column you extract. Therefore, only look at the first slice of your matrix, and so you need to do this to your kernel before computing the convolution:
ker_flipped = flipdim(flipdim(flipdim(ker, 1), 2), 3);
flipdim performs the flipping on a specified axis. In our case, we are doing it vertically, then taking the result and doing it horizontally, and then again doing it temporally. You would then use ker_flipped
in your summation instead. Take note that it doesn't matter which order you do the flipping. flipdim
operates on each dimension independently, and so as long as you remember to flip all dimensions, the output will be the same.
To demonstrate, here's what the output looks like with convn
:
c =
4.1837 4.1843 5.1187 6.1535
4.5262 5.3253 5.5181 5.8375
5.1311 4.7648 5.3608 7.1241
Now, to determine what c(1,1)
is by hand, you would need to do your calculation on the flipped kernel:
ker_flipped = flipdim(flipdim(flipdim(ker, 1), 2), 3);
c11 = sum(sum(a(1:3,1:4,1).*ker_flipped(:,:,1)))+sum(sum(a(1:3,1:4,2).*ker_flipped(:,:,2)));
The output of what we get is:
c11 =
4.1837
As you can see, this verifies what we get by hand with the calculation done in MATLAB using convn
. If you want to compare more digits of precision, use format long
and compare them both:
>> format long;
>> disp(c11)
4.183698205668000
>> disp(c(1,1))
4.183698205668001
As you can see, all of the digits are the same, except for the last one. That is attributed to numerical round-off. To be absolutely sure:
>> disp(abs(c11 - c(1,1)));
8.881784197001252e-16
... I think a difference of an order or 10-16 is good enough for me to show that they're equal, right?