Skip to content

Forward pass for the conv2d layer #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4cb4ed2
Refactor conv2d_layer into a submodule
milancurcic May 5, 2022
021fd81
Begin working on a conv2d forward pass; indexing needs fixing
milancurcic May 5, 2022
7743b5a
Run-time check to prevent initializing twice
milancurcic May 6, 2022
ac31216
Begin testing the forward pass of a conv2d layer
milancurcic May 6, 2022
2db7ae4
Implement layer % get_output for rank-3 arrays
milancurcic May 10, 2022
6068f70
Add rank-4 randn and use consistent indices
milancurcic May 10, 2022
d738366
Initialize conv2d kernel to random values from a normal distribution
milancurcic May 10, 2022
996fe07
Test that zero conv2d forwards to 0.5 with a sigmoid
milancurcic May 10, 2022
dcb16e9
Update conv2d test
milancurcic May 10, 2022
e8682d5
Change the order of dimensions in conv2d layers; add docstrings
milancurcic May 10, 2022
33972b6
A few basic tests for the input3d layer
milancurcic May 10, 2022
9fb9b7d
Generalize the input window slice to work with any odd window_size
milancurcic May 10, 2022
f5eff3c
Update docstrings
milancurcic May 12, 2022
ebf56a4
Add input3d_layer test to CMakeLists
milancurcic May 12, 2022
278758f
Bump version to 0.4.0
milancurcic May 13, 2022
ca3c179
Remove old code
milancurcic May 13, 2022
6bd4f94
Fix line length in comment
milancurcic May 13, 2022
6eaf191
Enable network % forward() for 3-d input data
milancurcic May 13, 2022
8876c99
Enable conv2d->conv2d in layer % forward() method
milancurcic May 13, 2022
00e9802
Add a few tests for a convolutional network; no training yet
milancurcic May 13, 2022
cf79591
Make conv2d layer API consistent with Keras (filters, kernel_size)
milancurcic May 16, 2022
8899583
Merge remote-tracking branch 'upstream/main' into conv2d-forward-pass
milancurcic May 16, 2022
0929f49
Add layer summary table to the README
milancurcic May 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ add_library(neural
src/nf_base_layer.f90
src/nf_base_layer_submodule.f90
src/nf_conv2d_layer.f90
src/nf_conv2d_layer_submodule.f90
src/nf_datasets_mnist.f90
src/nf_datasets_mnist_submodule.f90
src/nf_dense_layer.f90
Expand Down Expand Up @@ -99,7 +100,7 @@ string(REGEX REPLACE "^ | $" "" LIBS "${LIBS}")

# tests
enable_testing()
foreach(execid input1d_layer dense_layer dense_network)
foreach(execid input1d_layer input3d_layer dense_layer conv2d_layer dense_network conv2d_network)
add_executable(test_${execid} test/test_${execid}.f90)
target_link_libraries(test_${execid} neural ${LIBS})
add_test(test_${execid} bin/test_${execid})
Expand All @@ -108,5 +109,4 @@ endforeach()
foreach(execid mnist simple sine)
add_executable(${execid} example/${execid}.f90)
target_link_libraries(${execid} neural ${LIBS})
#add_test(example_${execid} bin/example_${execid})
endforeach()
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ Read the paper [here](https://arxiv.org/abs/1902.06714).
* Data-based parallelism
* Several activation functions

### Available layer types

| Layer type | Constructor name | Rank of output array | Forward pass | Backward pass |
|------------|------------------|----------------------|--------------|---------------|
| Input | `input` | 1, 3 | n/a | n/a |
| Dense (fully-connected) | `dense` | 1 | ✅ | ✅ |
| Convolutional (2-d) | `conv2d` | 3 | ✅ | ❌ |

## Getting started

Get the code:
Expand Down
2 changes: 1 addition & 1 deletion fpm.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "neural-fortran"
version = "0.3.0"
version = "0.4.0"
license = "MIT"
author = "Milan Curcic"
maintainer = "milancurcic@hey.com"
Expand Down
2 changes: 1 addition & 1 deletion src/nf.f90
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module nf
use nf_datasets_mnist, only: label_digits, load_mnist
use nf_layer, only: layer
use nf_layer_constructors, only: dense, input
use nf_layer_constructors, only: conv2d, dense, input
use nf_network, only: network
end module nf
98 changes: 44 additions & 54 deletions src/nf_conv2d_layer.f90
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
module nf_conv2d_layer

!! This is a placeholder module that will later define a concrete conv2d
!! layer type.
!! This modules provides a 2-d convolutional `conv2d_layer` type.

use nf_base_layer, only: base_layer
implicit none
Expand All @@ -14,12 +13,12 @@ module nf_conv2d_layer
integer :: width
integer :: height
integer :: channels
integer :: window_size
integer :: kernel_size
integer :: filters

real, allocatable :: biases(:) ! as many as there are filters
real, allocatable :: kernel(:,:,:,:)
real, allocatable :: output(:,:,:)
real, allocatable :: biases(:) ! size(filters)
real, allocatable :: kernel(:,:,:,:) ! filters x channels x window x window
real, allocatable :: output(:,:,:) ! filters x output_width * output_height

contains

Expand All @@ -30,55 +29,46 @@ module nf_conv2d_layer
end type conv2d_layer

interface conv2d_layer
module procedure :: conv2d_layer_cons
pure module function conv2d_layer_cons(filters, kernel_size, activation) &
result(res)
!! `conv2d_layer` constructor function
integer, intent(in) :: filters
integer, intent(in) :: kernel_size
character(*), intent(in) :: activation
type(conv2d_layer) :: res
end function conv2d_layer_cons
end interface conv2d_layer

contains

pure function conv2d_layer_cons(window_size, filters, activation) result(res)
integer, intent(in) :: window_size
integer, intent(in) :: filters
character(*), intent(in) :: activation
type(conv2d_layer) :: res
res % window_size = window_size
res % filters = filters
call res % set_activation(activation)
end function conv2d_layer_cons


subroutine init(self, input_shape)
class(conv2d_layer), intent(in out) :: self
integer, intent(in) :: input_shape(:)

self % width = input_shape(1) - self % window_size + 1
self % height = input_shape(2) - self % window_size + 1
self % channels = input_shape(3)

allocate(self % output(self % width, self % height, self % filters))
self % output = 0

allocate(self % kernel(self % window_size, self % window_size, &
self % channels, self % filters))
self % kernel = 0 ! TODO 4-d randn

allocate(self % biases(self % filters))
self % biases = 0

end subroutine init


subroutine forward(self, input)
class(conv2d_layer), intent(in out) :: self
real, intent(in) :: input(:,:,:)
print *, 'Warning: conv2d forward pass not implemented'
end subroutine forward


subroutine backward(self, input, gradient)
class(conv2d_layer), intent(in out) :: self
real, intent(in) :: input(:,:,:)
real, intent(in) :: gradient(:,:,:)
print *, 'Warning: conv2d backward pass not implemented'
end subroutine backward
interface

module subroutine init(self, input_shape)
!! Initialize the layer data structures.
!!
!! This is a deferred procedure from the `base_layer` abstract type.
class(conv2d_layer), intent(in out) :: self
!! A `conv2d_layer` instance
integer, intent(in) :: input_shape(:)
!! Input layer dimensions
end subroutine init

pure module subroutine forward(self, input)
!! Apply a forward pass on the `conv2d` layer.
class(conv2d_layer), intent(in out) :: self
!! A `conv2d_layer` instance
real, intent(in) :: input(:,:,:)
!! Input data
end subroutine forward

module subroutine backward(self, input, gradient)
!! Apply a backward pass on the `conv2d` layer.
class(conv2d_layer), intent(in out) :: self
!! A `conv2d_layer` instance
real, intent(in) :: input(:,:,:)
!! Input data (previous layer)
real, intent(in) :: gradient(:,:,:)
!! Gradient (next layer)
end subroutine backward

end interface

end module nf_conv2d_layer
111 changes: 111 additions & 0 deletions src/nf_conv2d_layer_submodule.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
submodule(nf_conv2d_layer) nf_conv2d_layer_submodule

use nf_random, only: randn

implicit none

contains

pure module function conv2d_layer_cons(filters, kernel_size, activation) result(res)
implicit none
integer, intent(in) :: filters
integer, intent(in) :: kernel_size
character(*), intent(in) :: activation
type(conv2d_layer) :: res
res % kernel_size = kernel_size
res % filters = filters
call res % set_activation(activation)
end function conv2d_layer_cons


module subroutine init(self, input_shape)
implicit none
class(conv2d_layer), intent(in out) :: self
integer, intent(in) :: input_shape(:)

self % channels = input_shape(1)
self % width = input_shape(2) - self % kernel_size + 1
self % height = input_shape(3) - self % kernel_size + 1

! Output of shape filters x width x height
allocate(self % output(self % filters, self % width, self % height))
self % output = 0

! Kernel of shape filters x channels x width x height
allocate(self % kernel(self % filters, self % channels, &
self % kernel_size, self % kernel_size))

! Initialize the kernel with random values with a normal distribution.
self % kernel = randn(self % filters, self % channels, &
self % kernel_size, self % kernel_size) &
/ self % kernel_size**2 !TODO kernel_width * kernel_height

allocate(self % biases(self % filters))
self % biases = 0

end subroutine init


pure module subroutine forward(self, input)
implicit none
class(conv2d_layer), intent(in out) :: self
real, intent(in) :: input(:,:,:)
integer :: input_width, input_height, input_channels
integer :: istart, iend
integer :: jstart, jend
integer :: i, j, n
integer :: iws, iwe, jws, jwe
integer :: half_window

! Input dimensions are channels x width x height
input_channels = size(input, dim=1)
input_width = size(input, dim=2)
input_height = size(input, dim=3)

! Half-window is 1 for window size 3; 2 for window size 5; etc.
half_window = self % kernel_size / 2

! Determine the start and end indices for the width and height dimensions
! of the input that correspond to the center of each window.
istart = half_window + 1 ! TODO kernel_width
jstart = half_window + 1 ! TODO kernel_height
iend = input_width - istart + 1
jend = input_height - jstart + 1

convolution: do concurrent(i = istart:iend, j = jstart:jend)

! Start and end indices of the input data on the filter window
! iws and jws are also coincidentally the indices of the output matrix
iws = i - half_window ! TODO kernel_width
iwe = i + half_window ! TODO kernel_width
jws = j - half_window ! TODO kernel_height
jwe = j + half_window ! TODO kernel_height

! This computes the inner tensor product, sum(w_ij * x_ij), for each
! filter, and we add bias b_n to it.
inner_product: do concurrent(n = 1:self % filters)
self % output(n,iws,jws) = &
sum(self % kernel(n,:,:,:) * input(:,iws:iwe,jws:jwe)) &
+ self % biases(n)
end do inner_product

! TODO We may need to store self % output before we activate it for the
! TODO backward pass, just like we do for the dense layer.

! Activate
self % output(:,iws,jws) = self % activation(self % output(:,iws,jws))

end do convolution

end subroutine forward


module subroutine backward(self, input, gradient)
implicit none
class(conv2d_layer), intent(in out) :: self
real, intent(in) :: input(:,:,:)
real, intent(in) :: gradient(:,:,:)
print *, 'Warning: conv2d backward pass not implemented'
end subroutine backward

end submodule nf_conv2d_layer_submodule
21 changes: 18 additions & 3 deletions src/nf_layer.f90
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ module nf_layer

procedure :: backward
procedure :: forward
procedure :: get_output
procedure :: init
procedure :: print_info
procedure :: update

! Specific output subroutines for different array ranks,
! available via generic `get_output`.
procedure, private :: get_output_1d
procedure, private :: get_output_3d

generic :: get_output => get_output_1d, get_output_3d

end type layer

interface
Expand Down Expand Up @@ -59,13 +65,22 @@ pure module subroutine forward(self, input)
!! Input layer instance
end subroutine forward

pure module subroutine get_output(self, output)
pure module subroutine get_output_1d(self, output)
!! Returns the output values (activations) from this layer.
class(layer), intent(in) :: self
!! Layer instance
real, allocatable, intent(out) :: output(:)
!! Output values from this layer
end subroutine get_output
end subroutine get_output_1d

pure module subroutine get_output_3d(self, output)
!! Returns the output values (activations) from a layer with a 3-d output
!! (e.g. input3d, conv2d)
class(layer), intent(in) :: self
!! Layer instance
real, allocatable, intent(out) :: output(:,:,:)
!! Output values from this layer
end subroutine get_output_3d

impure elemental module subroutine init(self, input)
!! Initialize the layer, using information from the input layer,
Expand Down
10 changes: 5 additions & 5 deletions src/nf_layer_constructors.f90
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ pure module function dense(layer_size, activation) result(res)
!! Resulting layer instance
end function dense

pure module function conv2d(window_size, filters, activation) result(res)
pure module function conv2d(filters, kernel_size, activation) result(res)
!! 2-d convolutional layer constructor.
!!
!! This layer is for building 2-d convolutional network.
Expand All @@ -98,13 +98,13 @@ pure module function conv2d(window_size, filters, activation) result(res)
!! ```
!! use nf, only :: conv2d, layer
!! type(layer) :: conv2d_layer
!! conv2d_layer = dense(window_size=3, filters=32)
!! conv2d_layer = dense(window_size=3, filters=32, activation='relu')
!! conv2d_layer = dense(filters=32, kernel_size=3)
!! conv2d_layer = dense(filters=32, kernel_size=3, activation='relu')
!! ```
integer, intent(in) :: window_size
!! Width of the convolution window, commonly 3 or 5
integer, intent(in) :: filters
!! Number of filters in the output of the layer
integer, intent(in) :: kernel_size
!! Width of the convolution window, commonly 3 or 5
character(*), intent(in), optional :: activation
!! Activation function (default 'sigmoid')
type(layer) :: res
Expand Down
6 changes: 3 additions & 3 deletions src/nf_layer_constructors_submodule.f90
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ pure module function dense(layer_size, activation) result(res)
end function dense


pure module function conv2d(window_size, filters, activation) result(res)
integer, intent(in) :: window_size
pure module function conv2d(filters, kernel_size, activation) result(res)
integer, intent(in) :: filters
integer, intent(in) :: kernel_size
character(*), intent(in), optional :: activation
type(layer) :: res

Expand All @@ -67,7 +67,7 @@ pure module function conv2d(window_size, filters, activation) result(res)

allocate( &
res % p, &
source=conv2d_layer(window_size, filters, res % activation) &
source=conv2d_layer(filters, kernel_size, res % activation) &
)

end function conv2d
Expand Down
Loading