diff --git a/example/write-read-infer.f90 b/example/write-read-infer.f90 new file mode 100644 index 000000000..f69a49580 --- /dev/null +++ b/example/write-read-infer.f90 @@ -0,0 +1,70 @@ +! Copyright (c), The Regents of the University of California +! Terms of use are as specified in LICENSE.txt +program write_read_infer + !! This program demonstrates how to write a neural network to a JSON file, + !! read the same network from the written file, query the network object for + !! some of its properties, print those properties, and use the network to + !! perform inference. + use command_line_m, only : command_line_t + use inference_engine_m, only : inference_engine_t + use string_m, only : string_t + use matmul_m, only : matmul_t + use step_m, only : step_t + use file_m, only : file_t + implicit none + + type(string_t) file_name + type(command_line_t) command_line + + file_name = string_t(command_line%flag_value("--output-file")) + + if (len(file_name%string())==0) then + error stop new_line('a') // new_line('a') // & + 'Usage: ./build/run-fpm.sh run --example write-read-infer -- --output-file "<file-name>"' + end if + + call write_read_query_infer(file_name) + +contains + + subroutine write_read_query_infer(output_file_name) + type(string_t), intent(in) :: output_file_name + integer i, j + integer, parameter :: num_inputs = 2, num_outputs = 1, num_neurons = 3, num_hidden_layers = 2 + integer, parameter :: identity(*,*,*) = & + reshape([((merge(1,0,i==j), i=1,num_neurons), j=1,num_neurons)], shape=[num_neurons,num_neurons,num_hidden_layers-1]) + type(inference_engine_t) xor_network, inference_engine + type(file_t) json_output_file, json_input_file + + print *, "Constructing an inference_engine_t neural-network object from scratch." + xor_network = inference_engine_t( & + input_weights = real(reshape([1,0,1,1,0,1], [num_inputs, num_neurons])), & + hidden_weights = real(identity), & + output_weights = real(reshape([1,-2,1], [num_outputs, num_neurons])), & + biases = reshape([0.,-1.99,0., 0.,0.,0.], [num_neurons, num_hidden_layers]), & + output_biases = [0.], & + inference_strategy = matmul_t() & + ) + print *, "Converting an inference_engine_t object to a file_t object." + json_output_file = xor_network%to_json() + + print *, "Writing an inference_engine_t object to the file '"//output_file_name%string()//"' in JSON format." + call json_output_file%write_lines(output_file_name) + + print *, "Reading an inference_engine_t object from the same JSON file '"//output_file_name%string()//"'." + json_input_file = file_t(output_file_name) + + print *, "Constructing a new inference_engine_t object from the parameters read." + inference_engine = inference_engine_t(json_input_file, step_t(), matmul_t()) + + print *, "Querying the new inference_engine_t object for several properties:" + print *, "num_outputs = ", inference_engine%num_outputs() + print *, "num_hidden_layers = ", inference_engine%num_hidden_layers() + print *, "neurons_per_layer = ", inference_engine%neurons_per_layer() + + print *, "Performing inference:" + print *, "inference_engine%infer([0.,1.]) =",inference_engine%infer([0.,1.]) + print *, "Correct answer for the XOR neural network: ", 1. + end subroutine write_read_query_infer + +end program diff --git a/src/file_m.f90 b/src/file_m.f90 new file mode 100644 index 000000000..dae7d2c46 --- /dev/null +++ b/src/file_m.f90 @@ -0,0 +1,48 @@ +module file_m + !! A representation of a file as an object + use string_m, only : string_t + + private + public :: file_t + + type file_t + private + type(string_t), allocatable :: lines_(:) + contains + procedure :: lines + procedure :: write_lines + end type + + interface file_t + + module function read_lines(file_name) result(file_object) + implicit none + type(string_t), intent(in) :: file_name + type(file_t) file_object + end function + + pure module function construct(lines) result(file_object) + implicit none + type(string_t), intent(in), allocatable :: lines(:) + type(file_t) file_object + end function + + end interface + + interface + + pure module function lines(self) result(my_lines) + implicit none + class(file_t), intent(in) :: self + type(string_t), allocatable :: my_lines(:) + end function + + impure elemental module subroutine write_lines(self, file_name) + implicit none + class(file_t), intent(in) :: self + type(string_t), intent(in), optional :: file_name + end subroutine + + end interface + +end module file_m diff --git a/src/file_s.f90 b/src/file_s.f90 new file mode 100644 index 000000000..1ae4c7c36 --- /dev/null +++ b/src/file_s.f90 @@ -0,0 +1,107 @@ +submodule(file_m) file_s + use iso_fortran_env, only : iostat_end, iostat_eor, output_unit + use assert_m, only : assert + implicit none + +contains + + module procedure construct + file_object%lines_ = lines + end procedure + + module procedure write_lines + + integer file_unit, io_status, l + + call assert(allocated(self%lines_), "file_t%write_lines: allocated(self%lines_)") + + if (present(file_name)) then + open(newunit=file_unit, file=file_name%string(), form='formatted', status='unknown', iostat=io_status, action='write') + call assert(io_status==0,"write_lines: io_status==0 after 'open' statement", file_name%string()) + else + file_unit = output_unit + end if + + do l = 1, size(self%lines_) + write(file_unit, *) self%lines_(l)%string() + end do + + if (present(file_name)) close(file_unit) + end procedure + + module procedure read_lines + + integer io_status, file_unit, line_num + character(len=:), allocatable :: line + integer, parameter :: max_message_length=128 + character(len=max_message_length) error_message + integer, allocatable :: lengths(:) + + open(newunit=file_unit, file=file_name%string(), form='formatted', status='old', iostat=io_status, action='read') + call assert(io_status==0,"read_lines: io_status==0 after 'open' statement", file_name%string()) + + lengths = line_lengths(file_unit) + + associate(num_lines => size(lengths)) + + allocate(file_object%lines_(num_lines)) + + do line_num = 1, num_lines + allocate(character(len=lengths(line_num)) :: line) + read(file_unit, '(a)', iostat=io_status, iomsg=error_message) line + call assert(io_status==0,"read_lines: io_status==0 after line read", error_message) + file_object%lines_(line_num) = string_t(line) + deallocate(line) + end do + + end associate + + close(file_unit) + + contains + + function line_count(file_unit) result(num_lines) + integer, intent(in) :: file_unit + integer num_lines + + rewind(file_unit) + num_lines = 0 + do + read(file_unit, *, iostat=io_status) + if (io_status==iostat_end) exit + num_lines = num_lines + 1 + end do + rewind(file_unit) + end function + + function line_lengths(file_unit) result(lengths) + integer, intent(in) :: file_unit + integer, allocatable :: lengths(:) + integer io_status + character(len=1) c + + associate(num_lines => line_count(file_unit)) + + allocate(lengths(num_lines), source = 0) + rewind(file_unit) + + do line_num = 1, num_lines + do + read(file_unit, '(a)', advance='no', iostat=io_status, iomsg=error_message) c + if (io_status==iostat_eor) exit + lengths(line_num) = lengths(line_num) + 1 + end do + end do + + rewind(file_unit) + + end associate + end function + + end procedure + + module procedure lines + my_lines = self%lines_ + end procedure + +end submodule file_s diff --git a/src/inference_engine_m.f90 b/src/inference_engine_m.f90 index 86c26ac06..1cd38be10 100644 --- a/src/inference_engine_m.f90 +++ b/src/inference_engine_m.f90 @@ -5,6 +5,7 @@ module inference_engine_m use string_m, only : string_t use inference_strategy_m, only : inference_strategy_t use activation_strategy_m, only : activation_strategy_t + use file_m, only : file_t implicit none private @@ -22,6 +23,7 @@ module inference_engine_m class(inference_strategy_t), allocatable :: inference_strategy_ contains procedure :: read_network + procedure :: to_json procedure :: write_network procedure :: infer procedure :: num_inputs @@ -47,10 +49,24 @@ pure module function construct & type(inference_engine_t) inference_engine end function + impure elemental module function from_json(file_, activation_strategy, inference_strategy) result(inference_engine) + implicit none + type(file_t), intent(in) :: file_ + class(activation_strategy_t), intent(in), optional :: activation_strategy + class(inference_strategy_t), intent(in), optional :: inference_strategy + type(inference_engine_t) inference_engine + end function + end interface interface + impure elemental module function to_json(self) result(json_file) + implicit none + class(inference_engine_t), intent(in) :: self + type(file_t) json_file + end function + impure elemental module subroutine read_network(self, file_name, activation_strategy, inference_strategy) implicit none class(inference_engine_t), intent(out) :: self diff --git a/src/inference_engine_s.f90 b/src/inference_engine_s.f90 index 50546edf9..c026b6b10 100644 --- a/src/inference_engine_s.f90 +++ b/src/inference_engine_s.f90 @@ -5,6 +5,11 @@ use intrinsic_array_m, only : intrinsic_array_t use matmul_m, only : matmul_t use step_m, only : step_t + use layer_m, only : layer_t + use neuron_m, only : neuron_t + use file_m, only : file_t + use formats_m, only : separated_values + use iso_fortran_env, only : iostat_end implicit none contains @@ -39,7 +44,8 @@ end procedure module procedure subtract - call assert(self%conformable_with(rhs), "inference_engine_t%subtract: conformable operands") + call assert(self%conformable_with(rhs), "inference_engine_t%subtract: conformable operands", & + intrinsic_array_t([shape(self%biases_), shape(rhs%biases_)])) difference%input_weights_ = self%input_weights_ - rhs%input_weights_ difference%hidden_weights_ = self%hidden_weights_ - rhs%hidden_weights_ @@ -60,7 +66,8 @@ pure subroutine assert_consistent(self) call assert(allocated(self%activation_strategy_), "inference_engine%assert_consistent: allocated(self%activation_strategy_)") associate(allocated_components => & - [allocated(self%input_weights_), allocated(self%hidden_weights_), allocated(self%biases_), allocated(self%output_weights_)] & + [allocated(self%input_weights_), allocated(self%hidden_weights_), allocated(self%output_weights_), & + allocated(self%biases_), allocated(self%output_biases_)] & ) call assert(all(allocated_components), "inference_engine_s(assert_consistent): fully allocated object", & intrinsic_array_t(allocated_components)) @@ -228,7 +235,6 @@ function line_length(file_unit) result(length) integer length, io_status character(len=1) c - rewind(file_unit) io_status = 0 length = 1 do @@ -236,7 +242,7 @@ function line_length(file_unit) result(length) if (io_status/=0) exit length = length + 1 end do - rewind(file_unit) + backspace(file_unit) end function subroutine read_line_and_count_inputs(file_unit, line, input_count) @@ -441,4 +447,170 @@ subroutine count_outputs(file_unit, buffer_size, num_hidden_layers, output_count end procedure read_network + module procedure to_json + + type(string_t), allocatable :: lines(:) + integer layer, neuron, line + integer, parameter :: characters_per_value=17 + character(len=:), allocatable :: comma_separated_values, csv_format + character(len=17) :: single_value + integer, parameter :: & + outer_object_braces = 2, hidden_layer_outer_brackets = 2, lines_per_neuron = 4, inner_brackets_per_layer = 2, & + output_layer_brackets = 2, comma = 1 + + call self%write_network(string_t("starting-to_json.txt")) + + csv_format = separated_values(separator=",", mold=[real::]) + + associate(num_hidden_layers => self%num_hidden_layers(), neurons_per_layer => self%neurons_per_layer(), & + num_outputs => self%num_outputs(), num_inputs => self%num_inputs()) + associate(num_lines => & + outer_object_braces + hidden_layer_outer_brackets & + + (num_hidden_layers + 1)*(inner_brackets_per_layer + neurons_per_layer*lines_per_neuron) & + + output_layer_brackets + num_outputs*lines_per_neuron & + ) + allocate(lines(num_lines)) + + line = 1 + lines(line) = string_t('{') + + line = line + 1 + lines(line) = string_t(' "hidden_layers": [') + + layer = 1 + line = line + 1 + lines(line) = string_t(' [') + do neuron = 1, neurons_per_layer + line = line + 1 + lines(line) = string_t(' {') + line = line + 1 + allocate(character(len=num_inputs*(characters_per_value+1)-1)::comma_separated_values) + write(comma_separated_values, fmt = csv_format) self%input_weights_(:,neuron) + lines(line) = string_t(' "weights": [' // trim(comma_separated_values) // '],') + deallocate(comma_separated_values) + line = line + 1 + write(single_value, fmt = csv_format) self%biases_(neuron,layer) + lines(line) = string_t(' "bias": ' // trim(single_value)) + line = line + 1 + lines(line) = string_t(" }" // trim(merge(' ',',',neuron==neurons_per_layer))) + end do + line = line + 1 + lines(line) = string_t(trim(merge(" ],", " ] ", line/=num_hidden_layers + 1))) + + do layer = 1, num_hidden_layers + line = line + 1 + lines(line) = string_t(' [') + do neuron = 1, neurons_per_layer + line = line + 1 + lines(line) = string_t(' {') + line = line + 1 + allocate(character(len=neurons_per_layer*(characters_per_value+1)-1)::comma_separated_values) + write(comma_separated_values, fmt = csv_format) self%hidden_weights_(:, neuron, layer) + lines(line) = string_t(' "weights": [' // trim(comma_separated_values) // '],') + deallocate(comma_separated_values) + line = line + 1 + write(single_value, fmt = csv_format) self%biases_(neuron,layer+1) + lines(line) = string_t(' "bias": ' // trim(single_value)) + line = line + 1 + lines(line) = string_t(" }" // trim(merge(' ',',',neuron==neurons_per_layer))) + end do + line = line + 1 + lines(line) = string_t(" ]") + end do + + line = line + 1 + lines(line) = string_t(" ],") + + line = line + 1 + lines(line) = string_t(' "output_layer": [') + + do neuron = 1, num_outputs + line = line + 1 + lines(line) = string_t(' {') + line = line + 1 + allocate(character(len=neurons_per_layer*(characters_per_value+1)-1)::comma_separated_values) + write(comma_separated_values, fmt = csv_format) self%output_weights_(neuron,:) + lines(line) = string_t(' "weights": [' // trim(comma_separated_values) // '],') + deallocate(comma_separated_values) + line = line + 1 + write(single_value, fmt = csv_format) self%output_biases_(neuron) + lines(line) = string_t(' "bias": ' // trim(single_value)) + line = line + 1 + lines(line) = string_t(" }") + end do + + line = line + 1 + lines(line) = string_t(' ]') + + line = line + 1 + lines(line) = string_t('}') + + call assert(line == num_lines, "inference_engine_t%to_json: all lines defined", intrinsic_array_t([num_lines, line])) + end associate + end associate + + json_file = file_t(lines) + + end procedure to_json + + module procedure from_json + + type(string_t), allocatable :: lines(:) + type(layer_t) hidden_layers + type(neuron_t) output_neuron + + lines = file_%lines() + + call assert(adjustl(lines(1)%string())=="{", "from_json: outermost object start", lines(1)%string()) + call assert(adjustl(lines(2)%string())=='"hidden_layers": [', "from_json: hidden layers array start", lines(2)%string()) + + block + integer, parameter :: first_layer_line=3, lines_per_neuron=4, bracket_lines_per_layer=2 + character(len=:), allocatable :: output_layer_line + + hidden_layers = layer_t(lines, start=first_layer_line) + + associate( output_layer_line_number => first_layer_line + lines_per_neuron*sum(hidden_layers%count_neurons()) & + + bracket_lines_per_layer*hidden_layers%count_layers() + 1) + + output_layer_line = lines(output_layer_line_number)%string() + call assert(adjustl(output_layer_line)=='"output_layer": [', "from_json: hidden layers array start", output_layer_line) + + output_neuron = neuron_t(lines, start=output_layer_line_number + 1) + end associate + end block + + inference_engine%input_weights_ = hidden_layers%input_weights() + call assert(hidden_layers%next_allocated(), "inference_engine_t%from_json: next layer exists") + + block + type(layer_t), pointer :: next_layer + + next_layer => hidden_layers%next_pointer() + inference_engine%hidden_weights_ = next_layer%hidden_weights() + end block + inference_engine%biases_ = hidden_layers%hidden_biases() + + associate(output_weights => output_neuron%weights()) + inference_engine%output_weights_ = reshape(output_weights, [1, size(output_weights)]) + inference_engine%output_biases_ = [output_neuron%bias()] + end associate + + + if (present(activation_strategy)) then + inference_engine%activation_strategy_ = activation_strategy + else + inference_engine%activation_strategy_ = step_t() + end if + + if (present(inference_strategy)) then + inference_engine%inference_strategy_ = inference_strategy + else + inference_engine%inference_strategy_ = matmul_t() + end if + + call assert_consistent(inference_engine) + + end procedure from_json + end submodule inference_engine_s diff --git a/src/layer_m.f90 b/src/layer_m.f90 new file mode 100644 index 000000000..ce8b9cb24 --- /dev/null +++ b/src/layer_m.f90 @@ -0,0 +1,91 @@ +! Copyright (c), The Regents of the University of California +! Terms of use are as specified in LICENSE.txt +module layer_m + use neuron_m, only : neuron_t + use string_m, only : string_t + implicit none + + private + public :: layer_t + + type layer_t + !! linked list of layers, each comprised of a linked list of neurons + private + type(neuron_t) neuron !! linked list of this layer's neurons + type(layer_t), allocatable :: next !! next layer + contains + procedure :: count_layers + procedure :: count_neurons + procedure :: input_weights + procedure :: hidden_weights + procedure :: hidden_biases + procedure :: neurons_per_layer + procedure :: next_allocated + procedure :: next_pointer + end type + + interface layer_t + + recursive module function construct(layer_lines, start) result(layer) + !! construct a linked list of layer_t objects from an array of JSON-formatted text lines + implicit none + type(string_t), intent(in) :: layer_lines(:) + integer, intent(in) :: start + type(layer_t), target :: layer + end function + + end interface + + interface + + module function count_layers(layer) result(num_layers) + implicit none + class(layer_t), intent(in), target :: layer + integer num_layers + end function + + module function count_neurons(layer) result(neurons_per_layer) + implicit none + class(layer_t), intent(in), target :: layer + integer, allocatable :: neurons_per_layer(:) + end function + + module function input_weights(self) result(weights) + implicit none + class(layer_t), intent(in), target :: self + real, allocatable :: weights(:,:) + end function + + module function hidden_weights(self) result(weights) + implicit none + class(layer_t), intent(in), target :: self + real, allocatable :: weights(:,:,:) + end function + + module function hidden_biases(self) result(biases) + implicit none + class(layer_t), intent(in), target :: self + real, allocatable :: biases(:,:) + end function + + module function neurons_per_layer(self) result(num_neurons) + implicit none + class(layer_t), intent(in), target :: self + integer num_neurons + end function + + module function next_allocated(self) result(next_is_allocated) + implicit none + class(layer_t), intent(in) :: self + logical next_is_allocated + end function + + module function next_pointer(self) result(next_ptr) + implicit none + class(layer_t), intent(in), target :: self + type(layer_t), pointer :: next_ptr + end function + + end interface + +end module diff --git a/src/layer_s.f90 b/src/layer_s.f90 new file mode 100644 index 000000000..8115b4923 --- /dev/null +++ b/src/layer_s.f90 @@ -0,0 +1,213 @@ +! Copyright (c), The Regents of the University of California +! Terms of use are as specified in LICENSE.txt +submodule(layer_m) layer_s + use assert_m, only : assert + use intrinsic_array_m, only : intrinsic_array_t + implicit none + +contains + + module procedure construct + + type(neuron_t), pointer :: neuron + integer num_inputs, neurons_in_layer + character(len=:), allocatable :: line + + call assert(adjustl(layer_lines(start)%string())=='[', "layer_t construct: layer start", layer_lines(start)%string()) + + layer%neuron = neuron_t(layer_lines, start+1) + num_inputs = size(layer%neuron%weights()) + + neuron => layer%neuron + neurons_in_layer = 1 + do + if (.not. neuron%next_allocated()) exit + neuron => neuron%next_pointer() + call assert(size(neuron%weights()) == num_inputs, "layer_t construct: constant number of inputs") + neurons_in_layer = neurons_in_layer + 1 + end do + + line = trim(adjustl(layer_lines(start+4*neurons_in_layer+1)%string())) + call assert(line(1:1)==']', "read_layer_list: hidden layer end") + + if (line(len(line):len(line)) == ",") layer%next = construct(layer_lines, start+4*neurons_in_layer+2) + + end procedure + + module procedure count_layers + + type(layer_t), pointer :: layer_ptr + + layer_ptr => layer + num_layers = 1 + do + if (.not. allocated(layer_ptr%next)) exit + layer_ptr => layer%next + num_layers = num_layers + 1 + end do + + end procedure + + module procedure count_neurons + + type(layer_t), pointer :: layer_ptr + type(neuron_t), pointer :: neuron_ptr + integer num_neurons + + layer_ptr => layer + + allocate(neurons_per_layer(0)) + + do + num_neurons = 1 + neuron_ptr => layer_ptr%neuron + do + if (.not. neuron_ptr%next_allocated()) exit + neuron_ptr => neuron_ptr%next_pointer() + num_neurons = num_neurons + 1 + end do + neurons_per_layer = [neurons_per_layer, num_neurons] + if (.not. allocated(layer_ptr%next)) exit + layer_ptr => layer%next + end do + + end procedure + + module procedure input_weights + + type(neuron_t), pointer :: neuron + integer i + + associate(num_inputs => self%neuron%num_inputs(), neurons_per_layer => self%neurons_per_layer()) + + allocate(weights(num_inputs, neurons_per_layer)) + + neuron => self%neuron + weights(:,1) = neuron%weights() + + do i = 2, neurons_per_layer - 1 + call assert(neuron%next_allocated(), "layer_t%input_weights: neuron%next_allocated()") + neuron => neuron%next_pointer() + weights(:,i) = neuron%weights() + call assert(neuron%num_inputs() == num_inputs, "layer_t%input_weights: constant number of inputs") + end do + neuron => neuron%next_pointer() + call assert(.not. neuron%next_allocated(), "layer_t%input_weights: .not. neuron%next_allocated()") + if (neurons_per_layer /= 1) weights(:,neurons_per_layer) = neuron%weights() + + end associate + + end procedure + + module procedure hidden_weights + + type(neuron_t), pointer :: neuron + type(layer_t), pointer :: layer + integer n, l + + associate( & + num_inputs => self%neuron%num_inputs(), neurons_per_layer => self%neurons_per_layer(), num_layers => self%count_layers()) + + allocate(weights(num_inputs, neurons_per_layer, num_layers)) + + layer => self + + loop_over_layers: & + do l = 1, num_layers + + neuron => layer%neuron + weights(:,1,l) = neuron%weights() + + loop_over_neurons: & + do n = 2, neurons_per_layer - 1 + call assert(neuron%next_allocated(), "layer_t%hidden_weights: neuron%next_allocated()") + neuron => neuron%next_pointer() + weights(:,n,l) = neuron%weights() + call assert(neuron%num_inputs() == num_inputs, "layer_t%hidden_weights: constant number of inputs", & + intrinsic_array_t([num_inputs, neuron%num_inputs(), l, n])) + end do loop_over_neurons + + call assert(neuron%next_allocated(), "layer_t%hidden_weights: neuron%next_allocated()") + neuron => neuron%next_pointer() + if (neurons_per_layer /= 1) weights(:,neurons_per_layer,l) = neuron%weights() ! avoid redundant assignment + + if (l/=num_layers) then + layer => layer%next + else + call assert(.not. layer%next_allocated(), "layer_t%hidden_weights: .not. layer%next_allocated()") + end if + + end do loop_over_Layers + + end associate + + end procedure + + module procedure hidden_biases + + type(neuron_t), pointer :: neuron + type(layer_t), pointer :: layer + integer n, l + + associate(neurons_per_layer => self%neurons_per_layer(), num_layers => self%count_layers()) + + allocate(biases(neurons_per_layer, num_layers)) + + layer => self + + loop_over_layers: & + do l = 1, num_layers + + neuron => layer%neuron + biases(1,l) = neuron%bias() + + loop_over_neurons: & + do n = 2, neurons_per_layer - 1 + call assert(neuron%next_allocated(), "layer_t%hidden_biases: neuron%next_allocated()", intrinsic_array_t([l,n])) + neuron => neuron%next_pointer() + biases(n,l) = neuron%bias() + end do loop_over_neurons + + call assert(neuron%next_allocated(), "layer_t%hidden_biases: neuron%next_allocated()", & + intrinsic_array_t([l,neurons_per_layer])) + neuron => neuron%next_pointer() + call assert(.not. neuron%next_allocated(), "layer_t%hidden_biases: .not. neuron%next_allocated()", & + intrinsic_array_t([l,neurons_per_layer])) + if (neurons_per_layer /= 1) biases(neurons_per_layer,l) = neuron%bias() ! avoid redundant assignment + + if (l/=num_layers) then + layer => layer%next + else + call assert(.not. layer%next_allocated(), "layer_t%hidden_biases: .not. layer%next_allocated()") + end if + + end do loop_over_layers + + end associate + + + end procedure hidden_biases + + module procedure neurons_per_layer + + type(neuron_t), pointer :: neuron + + neuron => self%neuron + num_neurons = 1 + do + if (.not. neuron%next_allocated()) exit + neuron => neuron%next_pointer() + num_neurons = num_neurons + 1 + end do + + end procedure + + module procedure next_allocated + next_is_allocated = allocated(self%next) + end procedure + + module procedure next_pointer + next_ptr => self%next + end procedure + +end submodule layer_s diff --git a/src/neuron_m.f90 b/src/neuron_m.f90 new file mode 100644 index 000000000..5447bff9f --- /dev/null +++ b/src/neuron_m.f90 @@ -0,0 +1,70 @@ +! Copyright (c), The Regents of the University of California +! Terms of use are as specified in LICENSE.txt +module neuron_m + use string_m, only : string_t + implicit none + + private + public :: neuron_t + + type neuron_t + !! linked list of neurons + private + real, allocatable :: weights_(:) + real bias_ + type(neuron_t), allocatable :: next + contains + procedure :: weights + procedure :: bias + procedure :: next_allocated + procedure :: next_pointer + procedure :: num_inputs + end type + + interface neuron_t + + pure recursive module function construct(neuron_lines, start) result(neuron) + !! construct linked list of neuron_t objects from an array of JSON-formatted text lines + implicit none + type(string_t), intent(in) :: neuron_lines(:) + integer, intent(in) :: start + type(neuron_t) neuron + end function + + end interface + + interface + + module function weights(self) result(my_weights) + implicit none + class(neuron_t), intent(in) :: self + real, allocatable :: my_weights(:) + end function + + module function bias(self) result(my_bias) + implicit none + class(neuron_t), intent(in) :: self + real my_bias + end function + + module function next_allocated(self) result(next_is_allocated) + implicit none + class(neuron_t), intent(in) :: self + logical next_is_allocated + end function + + module function next_pointer(self) result(next_ptr) + implicit none + class(neuron_t), intent(in), target :: self + type(neuron_t), pointer :: next_ptr + end function + + pure module function num_inputs(self) result(size_weights) + implicit none + class(neuron_t), intent(in) :: self + integer size_weights + end function + + end interface + +end module diff --git a/src/neuron_s.f90 b/src/neuron_s.f90 new file mode 100644 index 000000000..2b25bd7fc --- /dev/null +++ b/src/neuron_s.f90 @@ -0,0 +1,64 @@ +! Copyright (c), The Regents of the University of California +! Terms of use are as specified in LICENSE.txt +submodule(neuron_m) neuron_s + use assert_m, only : assert + implicit none + +contains + + module procedure construct + + character(len=:), allocatable :: line + integer i + + call assert(adjustl(neuron_lines(start)%string())=='{', "read_json: neuron object start", neuron_lines(start)%string()) + + line = neuron_lines(start+1)%string() + associate(colon => index(line, ":")) + call assert(adjustl(line(:colon-1))=='"weights"', "read_json: neuron weights", line) + associate(opening_bracket => colon + index(line(colon+1:), "[")) + associate(closing_bracket => opening_bracket + index(line(opening_bracket+1:), "]")) + associate(commas => count("," == [(line(i:i), i=opening_bracket+1,closing_bracket-1)])) + associate(num_inputs => commas + 1) + allocate(neuron%weights_(num_inputs)) + read(line(opening_bracket+1:closing_bracket-1), fmt=*) neuron%weights_ + end associate + end associate + end associate + end associate + end associate + + line = neuron_lines(start+2)%string() + associate(colon => index(line, ":")) + call assert(adjustl(line(:colon-1))=='"bias"', "read_json: neuron bias", line) + read(line(colon+1:), fmt=*) neuron%bias_ + end associate + + line = adjustl(neuron_lines(start+3)%string()) + call assert(line(1:1)=='}', "read_json: neuron object end", line) + line = adjustr(neuron_lines(start+3)%string()) + if (line(len(line):len(line)) == ",") neuron%next = construct(neuron_lines, start+4) + + end procedure + + module procedure weights + my_weights = self%weights_ + end procedure + + module procedure bias + my_bias = self%bias_ + end procedure + + module procedure next_allocated + next_is_allocated = allocated(self%next) + end procedure + + module procedure next_pointer + next_ptr => self%next + end procedure + + module procedure num_inputs + size_weights = size(self%weights_) + end procedure + +end submodule neuron_s diff --git a/test/inference_engine_test_m.f90 b/test/inference_engine_test_m.f90 index 92bb834a2..e2282ed50 100644 --- a/test/inference_engine_test_m.f90 +++ b/test/inference_engine_test_m.f90 @@ -8,6 +8,7 @@ module inference_engine_test_m use inference_engine_m, only : inference_engine_t use inference_strategy_m, only : inference_strategy_t use matmul_m, only : matmul_t + use file_m, only : file_t implicit none private @@ -35,12 +36,13 @@ function results() result(test_results) "mapping (true,false) to true using the default inference strategy", & "mapping (false,true) to true using the default inference strategy", & "mapping (false,false) to false using the default inference strategy", & - "writing and then reading itself to and from a file", & "mapping (true,true) to false using `matmul`-based inference strategy", & "mapping (true,false) to true using `matmul`-based inference strategy", & "mapping (false,true) to true using `matmul`-based inference strategy", & - "mapping (false,false) to false using `matmul`-based inference strategy" & - ], [xor_truth_table(), write_then_read(), xor_truth_table(matmul_t())] & + "mapping (false,false) to false using `matmul`-based inference strategy", & + "writing and then reading itself to and from a file", & + "converting to and from JSON format" & + ], [xor_truth_table(), xor_truth_table(matmul_t()), write_then_read(), convert_to_and_from_json()] & ) end function @@ -70,9 +72,6 @@ function write_then_read() result(test_passes) logical, allocatable :: test_passes type(inference_engine_t) xor_written, xor_read, difference - integer i, j - integer, parameter :: identity(*,*,*) = reshape([((merge(1,0,i==j), i=1,3), j=1,3)], shape=[3,3,1]) - integer, parameter :: num_inputs=2, num_outputs=1, nuerons_per_layer=3 xor_written = xor_network() call xor_written%write_network(string_t("build/write_then_read_test_specimen")) @@ -87,6 +86,25 @@ function write_then_read() result(test_passes) end block end function + function convert_to_and_from_json() result(test_passes) + logical, allocatable :: test_passes + + type(inference_engine_t) xor, xor_from_json + type(file_t) xor_file_object + + xor = xor_network() + xor_file_object = xor%to_json() + xor_from_json = inference_engine_t(xor_file_object) + + block + type(inference_engine_t) difference + real, parameter :: tolerance = 1.0E-06 + + difference = xor_from_json - xor + test_passes = difference%norm() < tolerance + end block + end function + function xor_truth_table(inference_strategy) result(test_passes) logical, allocatable :: test_passes(:) class(inference_strategy_t), intent(in), optional :: inference_strategy