Skip to content

Commit a1d2d24

Browse files
jvdp1milancurcic
andauthored
Support of strides in the convolutional layers (#239)
* Addition of stride in API of conv * implementation of stride in conv1d * start implementation of stride in conv2d * Fix conv1d with stride * Implementation of stride * Apply suggestions from code review * Fix prior bug in the accumulation of gradients in the conv2d backward pass * Tidy up --------- Co-authored-by: milancurcic <caomaco@gmail.com>
1 parent bed0f51 commit a1d2d24

9 files changed

+139
-42
lines changed

example/cnn_mnist.f90

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ program cnn_mnist
1212
real, allocatable :: validation_images(:,:), validation_labels(:)
1313
real, allocatable :: testing_images(:,:), testing_labels(:)
1414
integer :: n
15-
integer, parameter :: num_epochs = 250
15+
integer, parameter :: num_epochs = 20
1616

1717
call load_mnist(training_images, training_labels, &
1818
validation_images, validation_labels, &

src/nf/nf_conv1d_layer.f90

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module nf_conv1d_layer
1515
integer :: channels
1616
integer :: kernel_size
1717
integer :: filters
18+
integer :: stride
1819

1920
real, allocatable :: biases(:) ! size(filters)
2021
real, allocatable :: kernel(:,:,:) ! filters x channels x window
@@ -39,12 +40,13 @@ module nf_conv1d_layer
3940
end type conv1d_layer
4041

4142
interface conv1d_layer
42-
module function conv1d_layer_cons(filters, kernel_size, activation) &
43+
module function conv1d_layer_cons(filters, kernel_size, activation, stride) &
4344
result(res)
4445
!! `conv1d_layer` constructor function
4546
integer, intent(in) :: filters
4647
integer, intent(in) :: kernel_size
4748
class(activation_function), intent(in) :: activation
49+
integer, intent(in) :: stride
4850
type(conv1d_layer) :: res
4951
end function conv1d_layer_cons
5052
end interface conv1d_layer

src/nf/nf_conv1d_layer_submodule.f90

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77

88
contains
99

10-
module function conv1d_layer_cons(filters, kernel_size, activation) result(res)
10+
module function conv1d_layer_cons(filters, kernel_size, activation, stride) result(res)
1111
integer, intent(in) :: filters
1212
integer, intent(in) :: kernel_size
1313
class(activation_function), intent(in) :: activation
14+
integer, intent(in) :: stride
1415
type(conv1d_layer) :: res
1516

1617
res % kernel_size = kernel_size
1718
res % filters = filters
1819
res % activation_name = activation % get_name()
20+
res % stride = stride
1921
allocate( res % activation, source = activation )
2022
end function conv1d_layer_cons
2123

@@ -24,7 +26,9 @@ module subroutine init(self, input_shape)
2426
integer, intent(in) :: input_shape(:)
2527

2628
self % channels = input_shape(1)
27-
self % width = input_shape(2) - self % kernel_size + 1
29+
self % width = (input_shape(2) - self % kernel_size) / self % stride +1
30+
31+
if (mod(input_shape(2) - self % kernel_size , self % stride) /= 0) self % width = self % width + 1
2832

2933
! Output of shape: filters x width
3034
allocate(self % output(self % filters, self % width))
@@ -55,19 +59,22 @@ end subroutine init
5559
pure module subroutine forward(self, input)
5660
class(conv1d_layer), intent(in out) :: self
5761
real, intent(in) :: input(:,:)
62+
integer :: input_width
5863
integer :: j, n
5964
integer :: iws, iwe
6065

66+
input_width = size(input, dim=2)
67+
6168
! Loop over output positions.
6269
do j = 1, self % width
6370
! Compute the input window corresponding to output index j.
6471
! In forward: center index = j + half_window, so window = indices j to j+kernel_size-1.
65-
iws = j
66-
iwe = j + self % kernel_size - 1
72+
iws = self % stride * (j-1) + 1
73+
iwe = min(iws + self % kernel_size - 1, input_width)
6774

6875
! For each filter, compute the convolution (inner product over channels and kernel width).
6976
do concurrent (n = 1:self % filters)
70-
self % z(n, j) = sum(self % kernel(n,:,:) * input(:,iws:iwe))
77+
self % z(n, j) = sum(self % kernel(n,:,1:iwe-iws+1) * input(:,iws:iwe))
7178
end do
7279

7380
! Add the bias for each filter.
@@ -85,6 +92,7 @@ pure module subroutine backward(self, input, gradient)
8592
real, intent(in) :: input(:,:)
8693
real, intent(in) :: gradient(:,:)
8794

95+
integer :: input_channels, input_width
8896
integer :: j, n, k
8997
integer :: iws, iwe
9098

@@ -93,6 +101,8 @@ pure module subroutine backward(self, input, gradient)
93101
real :: db_local(self % filters)
94102
real :: dw_local(self % filters, self % channels, self % kernel_size)
95103

104+
input_width = size(input, dim=2)
105+
96106
!--- Compute the local gradient gdz = (dL/dy) * sigma'(z) for each output.
97107
gdz = gradient * self % activation % eval_prime(self % z)
98108

@@ -108,13 +118,13 @@ pure module subroutine backward(self, input, gradient)
108118
! iws = j, iwe = j + kernel_size - 1.
109119
do n = 1, self % filters
110120
do j = 1, self % width
111-
iws = j
112-
iwe = j + self % kernel_size - 1
121+
iws = self % stride * (j-1) + 1
122+
iwe = min(iws + self % kernel_size - 1, input_width)
113123
do k = 1, self % channels
114124
! Weight gradient: accumulate contribution from the input window.
115-
dw_local(n,k,:) = dw_local(n,k,:) + input(k,iws:iwe) * gdz(n,j)
125+
dw_local(n,k,1:iwe-iws+1) = dw_local(n,k,1:iwe-iws+1) + input(k,iws:iwe) * gdz(n,j)
116126
! Input gradient: propagate gradient back to the input window.
117-
self % gradient(k,iws:iwe) = self % gradient(k,iws:iwe) + self % kernel(n,k,:) * gdz(n,j)
127+
self % gradient(k,iws:iwe) = self % gradient(k,iws:iwe) + self % kernel(n,k,1:iwe-iws+1) * gdz(n,j)
118128
end do
119129
end do
120130
end do

src/nf/nf_conv2d_layer.f90

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module nf_conv2d_layer
1616
integer :: channels
1717
integer :: kernel_size
1818
integer :: filters
19+
integer :: stride(2)
1920

2021
real, allocatable :: biases(:) ! size(filters)
2122
real, allocatable :: kernel(:,:,:,:) ! filters x channels x window x window
@@ -40,12 +41,13 @@ module nf_conv2d_layer
4041
end type conv2d_layer
4142

4243
interface conv2d_layer
43-
module function conv2d_layer_cons(filters, kernel_size, activation) &
44+
module function conv2d_layer_cons(filters, kernel_size, activation, stride) &
4445
result(res)
4546
!! `conv2d_layer` constructor function
4647
integer, intent(in) :: filters
4748
integer, intent(in) :: kernel_size
4849
class(activation_function), intent(in) :: activation
50+
integer, intent(in) :: stride(:)
4951
type(conv2d_layer) :: res
5052
end function conv2d_layer_cons
5153
end interface conv2d_layer

src/nf/nf_conv2d_layer_submodule.f90

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77

88
contains
99

10-
module function conv2d_layer_cons(filters, kernel_size, activation) result(res)
10+
module function conv2d_layer_cons(filters, kernel_size, activation, stride) result(res)
1111
implicit none
1212
integer, intent(in) :: filters
1313
integer, intent(in) :: kernel_size
1414
class(activation_function), intent(in) :: activation
15+
integer, intent(in) :: stride(:)
1516
type(conv2d_layer) :: res
1617

1718
res % kernel_size = kernel_size
1819
res % filters = filters
1920
res % activation_name = activation % get_name()
21+
res % stride = stride
2022
allocate( res % activation, source = activation )
2123

2224
end function conv2d_layer_cons
@@ -28,8 +30,12 @@ module subroutine init(self, input_shape)
2830
integer, intent(in) :: input_shape(:)
2931

3032
self % channels = input_shape(1)
31-
self % width = input_shape(2) - self % kernel_size + 1
32-
self % height = input_shape(3) - self % kernel_size + 1
33+
34+
self % width = (input_shape(2) - self % kernel_size) / self % stride(1) + 1
35+
if (mod(input_shape(2) - self % kernel_size , self % stride(1)) /= 0) self % width = self % width + 1
36+
37+
self % height = (input_shape(3) - self % kernel_size) / self % stride(2) + 1
38+
if (mod(input_shape(3) - self % kernel_size , self % stride(2)) /= 0) self % height = self % height + 1
3339

3440
! Output of shape filters x width x height
3541
allocate(self % output(self % filters, self % width, self % height))
@@ -83,25 +89,24 @@ pure module subroutine forward(self, input)
8389
! of the input that correspond to the center of each window.
8490
istart = half_window + 1 ! TODO kernel_width
8591
jstart = half_window + 1 ! TODO kernel_height
86-
iend = input_width - istart + 1
87-
jend = input_height - jstart + 1
8892

89-
convolution: do concurrent(i = istart:iend, j = jstart:jend)
93+
convolution: do concurrent(i = 1:self % width, j = 1:self % height)
9094

9195
! Start and end indices of the input data on the filter window
9296
! iws and jws are also coincidentally the indices of the output matrix
93-
iws = i - half_window ! TODO kernel_width
94-
iwe = i + half_window ! TODO kernel_width
95-
jws = j - half_window ! TODO kernel_height
96-
jwe = j + half_window ! TODO kernel_height
97+
iws = istart + self % stride(1) * (i-1) - half_window ! TODO kernel_width
98+
iwe = min(iws + 2*half_window, input_width) ! TODO kernel_width
99+
100+
jws = jstart + self % stride(2) * (j-1) - half_window ! TODO kernel_height
101+
jwe = min(jws + 2*half_window, input_height) ! TODO kernel_height
97102

98103
! Compute the inner tensor product, sum(w_ij * x_ij), for each filter.
99104
do concurrent(n = 1:self % filters)
100-
self % z(n,iws,jws) = sum(self % kernel(n,:,:,:) * input(:,iws:iwe,jws:jwe))
105+
self % z(n,i,j) = sum(self % kernel(n,:,1:iwe-iws+1,1:jwe-jws+1) * input(:,iws:iwe,jws:jwe))
101106
end do
102107

103108
! Add bias to the inner product.
104-
self % z(:,iws,jws) = self % z(:,iws,jws) + self % biases
109+
self % z(:,i,j) = self % z(:,i,j) + self % biases
105110

106111
end do convolution
107112

@@ -156,21 +161,22 @@ pure module subroutine backward(self, input, gradient)
156161
do concurrent( &
157162
n = 1:self % filters, &
158163
k = 1:self % channels, &
159-
i = istart:iend, &
160-
j = jstart:jend &
164+
i = 1:self % width, &
165+
j = 1:self % height &
161166
)
162167
! Start and end indices of the input data on the filter window
163-
iws = i - half_window ! TODO kernel_width
164-
iwe = i + half_window ! TODO kernel_width
165-
jws = j - half_window ! TODO kernel_height
166-
jwe = j + half_window ! TODO kernel_height
168+
iws = istart + self % stride(1) * (i-1) - half_window ! TODO kernel_width
169+
iwe = min(iws + 2*half_window, input_width) ! TODO kernel_width
170+
171+
jws = jstart + self % stride(2) * (j-1) - half_window ! TODO kernel_height
172+
jwe = min(jws + 2*half_window, input_height) ! TODO kernel_height
167173

168-
! dL/dw = sum(dL/dy * sigma'(z) * x)
169-
dw(n,k,:,:) = dw(n,k,:,:) + input(k,iws:iwe,jws:jwe) * gdz(n,iws:iwe,jws:jwe)
174+
! dL/dw = sum(gdz * x)
175+
dw(n,k,:,:) = dw(n,k,:,:) + input(k,iws:iwe,jws:jwe) * gdz(n,i,j)
170176

171-
! dL/dx = dL/dy * sigma'(z) .inner. w
172-
self % gradient(k,i,j) = self % gradient(k,i,j) &
173-
+ sum(gdz(n,iws:iwe,jws:jwe) * self % kernel(n,k,:,:))
177+
! dL/dx = sum(gdz * w)
178+
self % gradient(k,iws:iwe,jws:jwe) = self % gradient(k,iws:iwe,jws:jwe) &
179+
+ gdz(n,i,j) * self % kernel(n,k,:,:)
174180

175181
end do
176182

src/nf/nf_layer_constructors.f90

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ end function input3d
9494

9595
interface conv
9696

97-
module function conv1d(filters, kernel_width, activation) result(res)
97+
module function conv1d(filters, kernel_width, activation, stride) result(res)
9898
!! 1-d convolutional layer constructor.
9999
!!
100100
!! This layer is for building 1-d convolutional network.
@@ -117,11 +117,13 @@ module function conv1d(filters, kernel_width, activation) result(res)
117117
!! Width of the convolution window, commonly 3 or 5
118118
class(activation_function), intent(in), optional :: activation
119119
!! Activation function (default sigmoid)
120+
integer, intent(in), optional :: stride
121+
!! Stride length of the convolution
120122
type(layer) :: res
121123
!! Resulting layer instance
122124
end function conv1d
123125

124-
module function conv2d(filters, kernel_width, kernel_height, activation) result(res)
126+
module function conv2d(filters, kernel_width, kernel_height, activation, stride) result(res)
125127
!! 2-d convolutional layer constructor.
126128
!!
127129
!! This layer is for building 2-d convolutional network.
@@ -147,6 +149,8 @@ module function conv2d(filters, kernel_width, kernel_height, activation) result(
147149
!! Height of the convolution window, commonly 3 or 5
148150
class(activation_function), intent(in), optional :: activation
149151
!! Activation function (default sigmoid)
152+
integer, intent(in), optional :: stride(:)
153+
!! Stride length of the convolution
150154
type(layer) :: res
151155
!! Resulting layer instance
152156
end function conv2d

src/nf/nf_layer_constructors_submodule.f90

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323

2424
contains
2525

26-
module function conv1d(filters, kernel_width, activation) result(res)
26+
module function conv1d(filters, kernel_width, activation, stride) result(res)
2727
integer, intent(in) :: filters
2828
integer, intent(in) :: kernel_width
2929
class(activation_function), intent(in), optional :: activation
30+
integer, intent(in), optional :: stride
3031
type(layer) :: res
3132

33+
integer :: stride_tmp
3234
class(activation_function), allocatable :: activation_tmp
3335

3436
res % name = 'conv1d'
@@ -41,20 +43,31 @@ module function conv1d(filters, kernel_width, activation) result(res)
4143

4244
res % activation = activation_tmp % get_name()
4345

46+
if (present(stride)) then
47+
stride_tmp = stride
48+
else
49+
stride_tmp = 1
50+
endif
51+
52+
if (stride_tmp < 1) &
53+
error stop 'stride must be >= 1 in a conv1d layer'
54+
4455
allocate( &
4556
res % p, &
46-
source=conv1d_layer(filters, kernel_width, activation_tmp) &
57+
source=conv1d_layer(filters, kernel_width, activation_tmp, stride_tmp) &
4758
)
4859

4960
end function conv1d
5061

51-
module function conv2d(filters, kernel_width, kernel_height, activation) result(res)
62+
module function conv2d(filters, kernel_width, kernel_height, activation, stride) result(res)
5263
integer, intent(in) :: filters
5364
integer, intent(in) :: kernel_width
5465
integer, intent(in) :: kernel_height
5566
class(activation_function), intent(in), optional :: activation
67+
integer, intent(in), optional :: stride(:)
5668
type(layer) :: res
5769

70+
integer, allocatable :: stride_tmp(:)
5871
class(activation_function), allocatable :: activation_tmp
5972

6073
! Enforce kernel_width == kernel_height for now;
@@ -73,9 +86,21 @@ module function conv2d(filters, kernel_width, kernel_height, activation) result(
7386

7487
res % activation = activation_tmp % get_name()
7588

89+
if (present(stride)) then
90+
stride_tmp = stride
91+
else
92+
stride_tmp = [1, 1]
93+
endif
94+
95+
if (size(stride_tmp) /= 2 ) &
96+
error stop 'size of stride must be equal to 2 in a conv2d layer'
97+
98+
if (stride_tmp(1) < 1 .or. stride_tmp(2) < 1) &
99+
error stop 'stride must be >= 1 in a conv2d layer'
100+
76101
allocate( &
77102
res % p, &
78-
source=conv2d_layer(filters, kernel_width, activation_tmp) &
103+
source=conv2d_layer(filters, kernel_width, activation_tmp, stride_tmp) &
79104
)
80105

81106
end function conv2d

test/test_conv1d_layer.f90

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ program test_conv1d_layer
5858
select type(this_layer => input_layer % p); type is(input2d_layer)
5959
call this_layer % set(sample_input)
6060
end select
61+
deallocate(sample_input)
6162

6263
call conv1d_layer % forward(input_layer)
6364
call conv1d_layer % get_output(output)
@@ -67,11 +68,33 @@ program test_conv1d_layer
6768
write(stderr, '(a)') 'conv1d layer with zero input and sigmoid function must forward to all 0.5.. failed'
6869
end if
6970

71+
! Minimal conv1d layer: 1 channel, 3x3 pixel image, stride = 3;
72+
allocate(sample_input(1, 17))
73+
sample_input = 0
74+
75+
input_layer = input(1, 17)
76+
conv1d_layer = conv(filters, kernel_size, stride = 3)
77+
call conv1d_layer % init(input_layer)
78+
79+
select type(this_layer => input_layer % p); type is(input2d_layer)
80+
call this_layer % set(sample_input)
81+
end select
82+
deallocate(sample_input)
83+
84+
call conv1d_layer % forward(input_layer)
85+
call conv1d_layer % get_output(output)
86+
87+
if (.not. all(abs(output) < tolerance)) then
88+
ok = .false.
89+
write(stderr, '(a)') 'conv1d layer with zero input and sigmoid function must forward to all 0.5.. failed'
90+
end if
91+
92+
!Final
7093
if (ok) then
7194
print '(a)', 'test_conv1d_layer: All tests passed.'
7295
else
7396
write(stderr, '(a)') 'test_conv1d_layer: One or more tests failed.'
7497
stop 1
7598
end if
76-
99+
77100
end program test_conv1d_layer

0 commit comments

Comments
 (0)