Convolutional neural networks have done an amazing job, but are rooted in problems. It’s time we started thinking about new solutions or improvements — and now, enter capsules.
Previously, I briefly discussed how capsule networks combat some of these traditional problems. For the past for few months, I’ve been submerging myself in all things capsules. I think it’s time we all try to get a deeper understanding of how capsules actually work.
In order to make it easier to follow along, I have built a visualization tool that allows you to see what is happening at each layer. This is paired with a simple implementation of the network. All of it can be found on GitHub here.
This is the CapsNet architecture. Don’t worry if you don’t understand what any of it means yet. I’ll be going through it layer by layer, with as much detail as I can possibly conjure up.
Part 0: The Input
The input into CapsNet is the actual image supplied to the neural net. In this example the input image is 28 pixels high and 28 pixels wide. But images are actually 3 dimensions, and the 3rd dimension contains the color channels.
The image in our example only has one color channel, because it’s black and white. Most images you are familiar with have 3 or 4 channels, for Red-Green-Blue and possibly an additional channel for Alpha, or transparency.
Each one of these pixels is represented as a value from 0 to 255 and stored in a 28x28x1 matrix [28, 28, 1]. The brighter the pixel, the larger the value.
Part 1a: Convolutions
The first part of CapsNet is a traditional convolutional layer. What is a convolutional layer, how does it work, and what is its purpose?
The goal is to extract some extremely basic features from the input image, like edges or curves.
How can we do this?
Let’s think about an edge:
If we look at a few points on the image, we can start to pick up a pattern. Focus on the colors to the left and right of the point we are looking at:
You might notice that they have a larger difference if the point is an edge:
255 - 114 = 141
114 - 153 = -39
153 - 153 = 0
255 - 255 = 0
What if we went through each pixel in the image and replaced its value with the value of the difference of the pixels to the left and right of it? In theory, the image should become all black except for the edges.
We could do this by looping through every pixel in the image:
for pixel in image { result[pixel] = image[pixel - 1] - image[pixel + 1] }
But this isn’t very efficient. We can instead use something called a “convolution.” Technically speaking, it’s a “cross-correlation,” but everyone likes to call them convolutions.
A convolution is essentially doing the same thing as our loop, but it takes advantage of matrix math.
A convolution is done by lining up a small “window” in the corner of the image that only lets us see the pixels in that area. We then slide the window across all the pixels in the image, multiplying each pixel by a set of weights and then adding up all the values that are in that window.
This window is a matrix of weights, called a “kernel.”
We only care about 2 pixels, but when we wrap the window around them it will encapsulate the pixel between them.
Window: ┌─────────────────────────────────────┐ │ left_pixel middle_pixel right_pixel │ └─────────────────────────────────────┘
Can you think of a set of weights that we can multiply these pixels by so that their sum adds up to the value we are looking for?
Window: ┌─────────────────────────────────────┐ │ left_pixel middle_pixel right_pixel │ └─────────────────────────────────────┘
(w1 * 255) + (w2 * 255) + (w3 * 114) = 141
Spoilers below!
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ \│/ \│/ \│/ V V V
We can do something like this:
Window: ┌─────────────────────────────────────┐ │ left_pixel middle_pixel right_pixel │ └─────────────────────────────────────┘
(1 * 255) + (0 * 255) + (-1 * 114) = 141
With these weights, our kernel will look like this:
kernel = [1 0 -1]
However, kernels are generally square — so we can pad it with more zeros to look like this:
kernel = [ [0 0 0] [1 0 -1] [0 0 0] ]
Here’s a nice gif to see a convolution in action:
Note: The dimension of the output is reduced by the size of the kernel plus 1. For example:
(7 — 3) + 1 = 5
(more on this in the next section)
Here’s what the original image looks like after doing a convolution with the kernel we crafted:
You might notice that a couple edges are missing. Specifically, the horizontal ones. In order to highlight those, we would need another kernel that looks at pixels above and below. Like this:
kernel = [ [0 1 0] [0 0 0] [0 -1 0] ]
Also, both of these kernels won’t work well with edges of other angles or edges that are blurred. For that reason, we use many kernels (in our CapsNet implementation, we use 256 kernels). And the kernels are normally larger to allow for more wiggle room (our kernels will be 9x9).
This is what one of the kernels looked like after training the model. It’s not very obvious, but this is just a larger version of our edge detector that is more robust and only finds edges that go from bright to dark.
kernel = [ [ 0.02 -0.01 0.01 -0.05 -0.08 -0.14 -0.16 -0.22 -0.02] [ 0.01 0.02 0.03 0.02 0.00 -0.06 -0.14 -0.28 0.03] [ 0.03 0.01 0.02 0.01 0.03 0.01 -0.11 -0.22 -0.08] [ 0.03 -0.01 -0.02 0.01 0.04 0.07 -0.11 -0.24 -0.05] [-0.01 -0.02 -0.02 0.01 0.06 0.12 -0.13 -0.31 0.04] [-0.05 -0.02 0.00 0.05 0.08 0.14 -0.17 -0.29 0.08] [-0.06 0.02 0.00 0.07 0.07 0.04 -0.18 -0.10 0.05] [-0.06 0.01 0.04 0.05 0.03 -0.01 -0.10 -0.07 0.00] [-0.04 0.00 0.04 0.05 0.02 -0.04 -0.02 -0.05 0.04] ]
Note: I’ve rounded the values because they are quite large, for example
0.01783941
Luckily, we don’t have to hand-pick this collection of kernels. That is what training does. The kernels all start off empty (or in a random state) and keep getting tweaked in the direction that makes the output closer to what we want.
This is what the 256 kernels ended up looking like (I colored them as pixels so it’s easier to digest). The more negative the numbers, the bluer they are. 0 is green and positive is yellow:
256 kernels (9x9)
After we filter the image with all of these kernels, we end up with a fat stack of 256 output images.
Part 1b: ReLU
ReLU (formally known as Rectified Linear Unit) may sound complicated, but it’s actually quite simple. ReLU is an activation function that takes in a value. If it’s negative it becomes zero, and if it’s positive it stays the same.
In code:
x = max(0, x)
And as a graph:
We apply this function to all of the outputs of our convolutions.
Why do we do this? If we don’t apply some sort of activation function to the output of our layers, then the entire neural net could be described as a linear function. This would mean that all this stuff we are doing is kind of pointless.
Adding a non-linearity allows us to describe all kinds of functions. There are many different types of function we could apply, but ReLU is the most popular because it’s very cheap to perform.
Here are the outputs of ReLU Conv1 layer:
Part 2a: PrimaryCaps
The PrimaryCaps layer starts off as a normal convolution layer, but this time we are convolving over the stack of 256 outputs from the previous convolutions. So instead of having a 9x9 kernel, we have a 9x9x256 kernel.
So what exactly are we looking for?
In the first layer of convolutions we were looking for simple edges and curves. Now we are looking for slightly more complex shapes from the edges we found earlier.
This time our “stride” is 2. That means instead of moving 1 pixel at a time, we take steps of 2. A larger stride is chosen so that we can reduce the size of our input more rapidly:
Note: The dimension of the output would normally be 12, but we divide it by 2, because of the stride. For example:
((20 — 9) + 1) / 2 = 6
We will convolve over the outputs another 256 times. So we will end up with a stack of 256 6x6 outputs.
But this time we aren’t satisfied with just some lousy plain old numbers.
We’re going to cut the stack up into 32 decks with 8 cards each deck.
We can call this deck a “capsule layer.”
Each capsule layer has 36 “capsules.”
If you’re keeping up (and are a math wiz), that means each capsule has an array of 8 values. This is what we can call a “vector.”
Here’s what I’m talking about:
These “capsules” are our new pixel.
With a single pixel, we could only store the confidence of whether or not we found an edge in that spot. The higher the number, the higher the confidence.
With a capsule we can store 8 values per location! That gives us the opportunity to store more information than just whether or not we found a shape in that spot. But what other kinds of information would we want to store?
When looking at the shape below, what can you tell me about it? If you had to tell someone else how to redraw it, and they couldn’t look at it, what would you say?
This image is extremely basic, so there are only a few details we need to describe the shape:
- Type of shape
- Position
- Rotation
- Color
- Size
We can call these “instantiation parameters.” With more complex images we will end up needing more details. They can include pose (position, size, orientation), deformation, velocity, albedo, hue, texture, and so on.
You might remember that when we made a kernel for edge detection, it only worked on a specific angle. We needed a kernel for each angle. We could get away with it when dealing with edges because there are very few ways to describe an edge. Once we get up to the level of shapes, we don’t want to have a kernel for every angle of rectangles, ovals, triangles, and so on. It would get unwieldy, and would become even worse when dealing with more complicated shapes that have 3 dimensional rotations and features like lighting.
That’s one of the reasons why traditional neural nets don’t handle unseen rotations very well:
As we go from edges to shapes and from shapes to objects, it would be nice if we had more room to store this extra useful information.
Here is a simplified comparison of 2 capsule layers (one for rectangles and the other for triangles) vs 2 traditional pixel outputs:
0 Comments:
Post a Comment