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 | } |
