diff options
author | 3gg <3gg@shellblade.net> | 2023-12-16 10:21:16 -0800 |
---|---|---|
committer | 3gg <3gg@shellblade.net> | 2023-12-16 10:21:16 -0800 |
commit | 653e98e029a0d0f110b0ac599e50406060bb0f87 (patch) | |
tree | 6f909215218f6720266bde1b3f49aeddad8b1da3 /src/lib/src/neuralnet.c | |
parent | 3df7b6fb0c65295eed4590e6f166d60e89b3c68e (diff) |
Decouple activations from linear layer.
Diffstat (limited to 'src/lib/src/neuralnet.c')
-rw-r--r-- | src/lib/src/neuralnet.c | 218 |
1 files changed, 114 insertions, 104 deletions
diff --git a/src/lib/src/neuralnet.c b/src/lib/src/neuralnet.c index a5fc59b..4322b8c 100644 --- a/src/lib/src/neuralnet.c +++ b/src/lib/src/neuralnet.c | |||
@@ -7,11 +7,65 @@ | |||
7 | #include <assert.h> | 7 | #include <assert.h> |
8 | #include <stdlib.h> | 8 | #include <stdlib.h> |
9 | 9 | ||
10 | static void MakeLayerImpl( | ||
11 | int prev_layer_output_size, const nnLayer* layer, nnLayerImpl* impl) { | ||
12 | impl->type = layer->type; | ||
13 | |||
14 | switch (layer->type) { | ||
15 | case nnLinear: { | ||
16 | const nnLinearParams* params = &layer->linear; | ||
17 | nnLinearImpl* linear = &impl->linear; | ||
18 | |||
19 | if ((params->input_size > 0) && (params->output_size > 0)) { | ||
20 | const int rows = params->input_size; | ||
21 | const int cols = params->output_size; | ||
22 | linear->weights = nnMatrixMake(rows, cols); | ||
23 | linear->biases = nnMatrixMake(1, cols); | ||
24 | linear->owned = true; | ||
25 | } else { | ||
26 | linear->weights = params->weights; | ||
27 | linear->biases = params->biases; | ||
28 | linear->owned = false; | ||
29 | } | ||
30 | |||
31 | impl->input_size = linear->weights.rows; | ||
32 | impl->output_size = linear->weights.cols; | ||
33 | |||
34 | break; | ||
35 | } | ||
36 | |||
37 | // Activation layers. | ||
38 | case nnRelu: | ||
39 | case nnSigmoid: | ||
40 | impl->input_size = prev_layer_output_size; | ||
41 | impl->output_size = prev_layer_output_size; | ||
42 | break; | ||
43 | } | ||
44 | } | ||
45 | |||
46 | static void DeleteLayer(nnLayerImpl* layer) { | ||
47 | switch (layer->type) { | ||
48 | case nnLinear: { | ||
49 | nnLinearImpl* linear = &layer->linear; | ||
50 | if (linear->owned) { | ||
51 | nnMatrixDel(&linear->weights); | ||
52 | nnMatrixDel(&linear->biases); | ||
53 | } | ||
54 | break; | ||
55 | } | ||
56 | |||
57 | // No parameters for these layers. | ||
58 | case nnRelu: | ||
59 | case nnSigmoid: | ||
60 | break; | ||
61 | } | ||
62 | } | ||
63 | |||
10 | nnNeuralNetwork* nnMakeNet( | 64 | nnNeuralNetwork* nnMakeNet( |
11 | int num_layers, const int* layer_sizes, const nnActivation* activations) { | 65 | const nnLayer* layers, int num_layers, int input_size) { |
66 | assert(layers); | ||
12 | assert(num_layers > 0); | 67 | assert(num_layers > 0); |
13 | assert(layer_sizes); | 68 | assert(input_size > 0); |
14 | assert(activations); | ||
15 | 69 | ||
16 | nnNeuralNetwork* net = calloc(1, sizeof(nnNeuralNetwork)); | 70 | nnNeuralNetwork* net = calloc(1, sizeof(nnNeuralNetwork)); |
17 | if (net == 0) { | 71 | if (net == 0) { |
@@ -20,84 +74,38 @@ nnNeuralNetwork* nnMakeNet( | |||
20 | 74 | ||
21 | net->num_layers = num_layers; | 75 | net->num_layers = num_layers; |
22 | 76 | ||
23 | net->weights = calloc(num_layers, sizeof(nnMatrix)); | 77 | net->layers = calloc(num_layers, sizeof(nnLayerImpl)); |
24 | net->biases = calloc(num_layers, sizeof(nnMatrix)); | 78 | if (net->layers == 0) { |
25 | net->activations = calloc(num_layers, sizeof(nnActivation)); | ||
26 | if ((net->weights == 0) || (net->biases == 0) || (net->activations == 0)) { | ||
27 | nnDeleteNet(&net); | 79 | nnDeleteNet(&net); |
28 | return 0; | 80 | return 0; |
29 | } | 81 | } |
30 | 82 | ||
83 | int prev_layer_output_size = input_size; | ||
31 | for (int l = 0; l < num_layers; ++l) { | 84 | for (int l = 0; l < num_layers; ++l) { |
32 | // layer_sizes = { input layer size, first hidden layer size, ...} | 85 | MakeLayerImpl(prev_layer_output_size, &layers[l], &net->layers[l]); |
33 | const int layer_input_size = layer_sizes[l]; | 86 | prev_layer_output_size = net->layers[l].output_size; |
34 | const int layer_output_size = layer_sizes[l + 1]; | ||
35 | |||
36 | // We store the transpose of the weight matrix as written in textbooks. | ||
37 | // Our vectors are row vectors and the matrices row-major. | ||
38 | const int rows = layer_input_size; | ||
39 | const int cols = layer_output_size; | ||
40 | |||
41 | net->weights[l] = nnMatrixMake(rows, cols); | ||
42 | net->biases[l] = nnMatrixMake(1, cols); | ||
43 | net->activations[l] = activations[l]; | ||
44 | } | 87 | } |
45 | 88 | ||
46 | return net; | 89 | return net; |
47 | } | 90 | } |
48 | 91 | ||
49 | void nnDeleteNet(nnNeuralNetwork** net) { | 92 | void nnDeleteNet(nnNeuralNetwork** ppNet) { |
50 | if ((!net) || (!(*net))) { | 93 | if ((!ppNet) || (!(*ppNet))) { |
51 | return; | 94 | return; |
52 | } | 95 | } |
53 | if ((*net)->weights != 0) { | 96 | nnNeuralNetwork* net = *ppNet; |
54 | for (int l = 0; l < (*net)->num_layers; ++l) { | ||
55 | nnMatrixDel(&(*net)->weights[l]); | ||
56 | } | ||
57 | free((*net)->weights); | ||
58 | (*net)->weights = 0; | ||
59 | } | ||
60 | if ((*net)->biases != 0) { | ||
61 | for (int l = 0; l < (*net)->num_layers; ++l) { | ||
62 | nnMatrixDel(&(*net)->biases[l]); | ||
63 | } | ||
64 | free((*net)->biases); | ||
65 | (*net)->biases = 0; | ||
66 | } | ||
67 | if ((*net)->activations) { | ||
68 | free((*net)->activations); | ||
69 | (*net)->activations = 0; | ||
70 | } | ||
71 | free(*net); | ||
72 | *net = 0; | ||
73 | } | ||
74 | |||
75 | void nnSetWeights(nnNeuralNetwork* net, const R* weights) { | ||
76 | assert(net); | ||
77 | assert(weights); | ||
78 | 97 | ||
79 | for (int l = 0; l < net->num_layers; ++l) { | 98 | for (int l = 0; l < net->num_layers; ++l) { |
80 | nnMatrix* layer_weights = &net->weights[l]; | 99 | DeleteLayer(&net->layers[l]); |
81 | R* layer_values = layer_weights->values; | ||
82 | |||
83 | for (int j = 0; j < layer_weights->rows * layer_weights->cols; ++j) { | ||
84 | *layer_values++ = *weights++; | ||
85 | } | ||
86 | } | 100 | } |
87 | } | ||
88 | |||
89 | void nnSetBiases(nnNeuralNetwork* net, const R* biases) { | ||
90 | assert(net); | ||
91 | assert(biases); | ||
92 | |||
93 | for (int l = 0; l < net->num_layers; ++l) { | ||
94 | nnMatrix* layer_biases = &net->biases[l]; | ||
95 | R* layer_values = layer_biases->values; | ||
96 | 101 | ||
97 | for (int j = 0; j < layer_biases->rows * layer_biases->cols; ++j) { | 102 | if (net->layers) { |
98 | *layer_values++ = *biases++; | 103 | free(net->layers); |
99 | } | 104 | net->layers = 0; |
100 | } | 105 | } |
106 | |||
107 | free(net); | ||
108 | *ppNet = 0; | ||
101 | } | 109 | } |
102 | 110 | ||
103 | void nnQuery( | 111 | void nnQuery( |
@@ -114,35 +122,40 @@ void nnQuery( | |||
114 | nnMatrix input_vector = nnMatrixBorrowRows((nnMatrix*)input, i, 1); | 122 | nnMatrix input_vector = nnMatrixBorrowRows((nnMatrix*)input, i, 1); |
115 | 123 | ||
116 | for (int l = 0; l < net->num_layers; ++l) { | 124 | for (int l = 0; l < net->num_layers; ++l) { |
117 | const nnMatrix* layer_weights = &net->weights[l]; | ||
118 | const nnMatrix* layer_biases = &net->biases[l]; | ||
119 | // Y^T = (W*X)^T = X^T*W^T | ||
120 | // | ||
121 | // TODO: If we had a row-row matrix multiplication, we could compute: | ||
122 | // Y^T = W ** X^T | ||
123 | // The row-row multiplication could be more cache-friendly. We just need | ||
124 | // to store W as is, without transposing. | ||
125 | // We could also rewrite the original Mul function to go row x row, | ||
126 | // decomposing the multiplication. Preserving the original meaning of Mul | ||
127 | // makes everything clearer. | ||
128 | nnMatrix output_vector = | 125 | nnMatrix output_vector = |
129 | nnMatrixBorrowRows(&query->layer_outputs[l], i, 1); | 126 | nnMatrixBorrowRows(&query->layer_outputs[l], i, 1); |
130 | nnMatrixMul(&input_vector, layer_weights, &output_vector); | ||
131 | nnMatrixAddRow(&output_vector, layer_biases, &output_vector); | ||
132 | 127 | ||
133 | switch (net->activations[l]) { | 128 | switch (net->layers[l].type) { |
134 | case nnIdentity: | 129 | case nnLinear: { |
135 | break; // Nothing to do for the identity function. | 130 | const nnLinearImpl* linear = &net->layers[l].linear; |
136 | case nnSigmoid: | 131 | const nnMatrix* layer_weights = &linear->weights; |
137 | sigmoid_array( | 132 | const nnMatrix* layer_biases = &linear->biases; |
138 | output_vector.values, output_vector.values, output_vector.cols); | 133 | |
134 | // Y^T = (W*X)^T = X^T*W^T | ||
135 | // | ||
136 | // TODO: If we had a row-row matrix multiplication, we could compute: | ||
137 | // Y^T = W ** X^T | ||
138 | // | ||
139 | // The row-row multiplication could be more cache-friendly. We just need | ||
140 | // to store W as is, without transposing. | ||
141 | // | ||
142 | // We could also rewrite the original Mul function to go row x row, | ||
143 | // decomposing the multiplication. Preserving the original meaning of | ||
144 | // Mul makes everything clearer. | ||
145 | nnMatrixMul(&input_vector, layer_weights, &output_vector); | ||
146 | nnMatrixAddRow(&output_vector, layer_biases, &output_vector); | ||
139 | break; | 147 | break; |
148 | } | ||
140 | case nnRelu: | 149 | case nnRelu: |
150 | assert(input_vector.cols == output_vector.cols); | ||
141 | relu_array( | 151 | relu_array( |
142 | output_vector.values, output_vector.values, output_vector.cols); | 152 | input_vector.values, output_vector.values, output_vector.cols); |
153 | break; | ||
154 | case nnSigmoid: | ||
155 | assert(input_vector.cols == output_vector.cols); | ||
156 | sigmoid_array( | ||
157 | input_vector.values, output_vector.values, output_vector.cols); | ||
143 | break; | 158 | break; |
144 | default: | ||
145 | assert(0); | ||
146 | } | 159 | } |
147 | 160 | ||
148 | input_vector = output_vector; // Borrow. | 161 | input_vector = output_vector; // Borrow. |
@@ -159,15 +172,15 @@ void nnQueryArray( | |||
159 | assert(output); | 172 | assert(output); |
160 | assert(net->num_layers > 0); | 173 | assert(net->num_layers > 0); |
161 | 174 | ||
162 | nnMatrix input_vector = nnMatrixMake(net->weights[0].cols, 1); | 175 | nnMatrix input_vector = nnMatrixMake(1, nnNetInputSize(net)); |
163 | nnMatrixInit(&input_vector, input); | 176 | nnMatrixInit(&input_vector, input); |
164 | nnQuery(net, query, &input_vector); | 177 | nnQuery(net, query, &input_vector); |
165 | nnMatrixRowToArray(query->network_outputs, 0, output); | 178 | nnMatrixRowToArray(query->network_outputs, 0, output); |
166 | } | 179 | } |
167 | 180 | ||
168 | nnQueryObject* nnMakeQueryObject(const nnNeuralNetwork* net, int num_inputs) { | 181 | nnQueryObject* nnMakeQueryObject(const nnNeuralNetwork* net, int batch_size) { |
169 | assert(net); | 182 | assert(net); |
170 | assert(num_inputs > 0); | 183 | assert(batch_size > 0); |
171 | assert(net->num_layers > 0); | 184 | assert(net->num_layers > 0); |
172 | 185 | ||
173 | nnQueryObject* query = calloc(1, sizeof(nnQueryObject)); | 186 | nnQueryObject* query = calloc(1, sizeof(nnQueryObject)); |
@@ -183,11 +196,12 @@ nnQueryObject* nnMakeQueryObject(const nnNeuralNetwork* net, int num_inputs) { | |||
183 | free(query); | 196 | free(query); |
184 | return 0; | 197 | return 0; |
185 | } | 198 | } |
199 | |||
186 | for (int l = 0; l < net->num_layers; ++l) { | 200 | for (int l = 0; l < net->num_layers; ++l) { |
187 | const nnMatrix* layer_weights = &net->weights[l]; | 201 | const int layer_output_size = nnLayerOutputSize(net, l); |
188 | const int layer_output_size = nnLayerOutputSize(layer_weights); | 202 | query->layer_outputs[l] = nnMatrixMake(batch_size, layer_output_size); |
189 | query->layer_outputs[l] = nnMatrixMake(num_inputs, layer_output_size); | ||
190 | } | 203 | } |
204 | |||
191 | query->network_outputs = &query->layer_outputs[net->num_layers - 1]; | 205 | query->network_outputs = &query->layer_outputs[net->num_layers - 1]; |
192 | 206 | ||
193 | return query; | 207 | return query; |
@@ -213,23 +227,19 @@ const nnMatrix* nnNetOutputs(const nnQueryObject* query) { | |||
213 | } | 227 | } |
214 | 228 | ||
215 | int nnNetInputSize(const nnNeuralNetwork* net) { | 229 | int nnNetInputSize(const nnNeuralNetwork* net) { |
216 | assert(net); | 230 | return nnLayerInputSize(net, 0); |
217 | assert(net->num_layers > 0); | ||
218 | return net->weights[0].rows; | ||
219 | } | 231 | } |
220 | 232 | ||
221 | int nnNetOutputSize(const nnNeuralNetwork* net) { | 233 | int nnNetOutputSize(const nnNeuralNetwork* net) { |
222 | assert(net); | 234 | return nnLayerOutputSize(net, net->num_layers - 1); |
223 | assert(net->num_layers > 0); | ||
224 | return net->weights[net->num_layers - 1].cols; | ||
225 | } | 235 | } |
226 | 236 | ||
227 | int nnLayerInputSize(const nnMatrix* weights) { | 237 | int nnLayerInputSize(const nnNeuralNetwork* net, int layer) { |
228 | assert(weights); | 238 | assert(net); |
229 | return weights->rows; | 239 | return net->layers[layer].input_size; |
230 | } | 240 | } |
231 | 241 | ||
232 | int nnLayerOutputSize(const nnMatrix* weights) { | 242 | int nnLayerOutputSize(const nnNeuralNetwork* net, int layer) { |
233 | assert(weights); | 243 | assert(net); |
234 | return weights->cols; | 244 | return net->layers[layer].output_size; |
235 | } | 245 | } |