yoctograd.c (7075B)
1 /* Editor's note: some experiments with minimalism in machine learning. This 2 * file is combined from a couple smaller files (none of which originally had 3 * comments). 4 * 5 * Written in the evening of August 2, 2022 after doing too much data science-y 6 * stuff in Python at work and craving some variety. 7 */ 8 9 #include <stdbool.h> 10 #include <stdlib.h> 11 #include <stdint.h> 12 #include <stdio.h> 13 #include <time.h> 14 #include <string.h> 15 16 // A tiny "autodiff library" 17 18 typedef enum Op { 19 NIL, ADD, MUL, RLU, 20 } Op; 21 22 typedef struct __attribute__((__packed__)) Var { 23 float value; 24 float grad; 25 Op op; // TODO make uint8 26 struct Var* parents[2]; 27 } Var; 28 29 Var* v_new(float value, Var* child_a, Var* child_b, Op op) { 30 Var* out = malloc(sizeof(Var)); 31 out->value = value; 32 out->parents[0] = child_a; 33 out->parents[1] = child_b; 34 out->op = op; 35 return out; 36 } 37 38 #define v_add(a,b) v_new(a->value + b->value, a, b, ADD) 39 #define v_mul(a,b) v_new(a->value * b->value, a, b, MUL) 40 #define v_const(v) v_new(v, NULL, NULL, NIL) 41 #define v_relu(v) v_new((v->value > 0) ? v->value : 0, v, NULL, RLU) 42 43 void v_back(Var* v) { 44 if (v->op == ADD) { 45 v->parents[0]->grad += v->grad; 46 v->parents[1]->grad += v->grad; 47 } else if (v->op == MUL) { 48 v->parents[0]->grad += v->parents[1]->value * v->grad; 49 v->parents[1]->grad += v->parents[0]->value * v->grad; 50 } else if (v->op == RLU) { 51 v->parents[0]->grad += (v->value > 0) * v->grad; 52 } 53 if (v->parents[0] != NULL) v_back(v->parents[0]); 54 if (v->parents[1] != NULL) v_back(v->parents[1]); 55 } 56 57 // This is enough for a neural network! Disclaimer: This code is just a proof of 58 // concept and is very silly (most notably it leaks tons of memory). 59 60 typedef struct __attribute__((__packed__)) Neuron { 61 int16_t n_in; 62 Var* b; 63 Var** w; 64 } Neuron; 65 66 Neuron* neuron_new(uint16_t n_in, bool nonlin) { 67 Neuron* out = malloc(sizeof(Neuron)); 68 out->w = malloc(n_in * sizeof(Var*)); 69 for (uint16_t i = 0; i < n_in; i++) { 70 out->w[i] = v_const(.1*rand()/(float)RAND_MAX); 71 } 72 out->b = v_const(0); 73 out->n_in = n_in; 74 if (!nonlin) out->n_in = -out->n_in; 75 return out; 76 } 77 78 Var* neuron_forward(Neuron* n, Var** xs) { 79 Var* out = v_const(0); 80 for (uint16_t i = 0; i < n->n_in; i++) { 81 out = v_add(out, v_mul(n->w[i], xs[i])); 82 } 83 out = v_add(out, n->b); 84 if (n->n_in > 0) out = v_relu(out); 85 return out; 86 } 87 88 typedef struct Layer { 89 uint16_t sz; 90 Neuron** neurons; 91 } Layer; 92 93 Layer* layer_new(uint16_t n_in, uint16_t n_out) { 94 Layer* layer = malloc(sizeof(Layer)); 95 layer->neurons = malloc(n_out * sizeof(Neuron)); 96 for (uint16_t i = 0; i < n_out; i++) { 97 layer->neurons[i] = neuron_new(n_in, true); 98 } 99 layer->sz = n_out; 100 return layer; 101 } 102 103 Var** layer_forward(Layer* l, Var** xs) { 104 Var** out = malloc(l->sz * sizeof(Var*)); 105 for (uint16_t i = 0; i < l->sz; i++) { 106 out[i] = neuron_forward(l->neurons[i], xs); 107 } 108 return out; 109 } 110 111 typedef struct Net { 112 uint16_t sz; 113 float lr; 114 Layer** layers; 115 } Net; 116 117 Net* net_new(uint16_t n_layers, float lr) { 118 Net* out = malloc(sizeof(Net)); 119 out->layers = malloc(n_layers * sizeof(Layer*)); 120 out->sz = n_layers; 121 out->lr = lr; 122 return out; 123 } 124 125 void net_update(Net* n) { 126 for (int i = 0; i < n->sz; i++) { 127 Layer* layer = n->layers[i]; 128 for (int j = 0; j < layer->sz; j++) { 129 Neuron* neuron = n->layers[i]->neurons[j]; 130 for (int k = 0; k < neuron->n_in; k++) { 131 #ifdef DEBUG 132 printf( 133 "Layer %d, neuron %d, weight %d has value %f and grad %f\n", 134 i, j, k, 135 neuron->w[k]->value, 136 neuron->w[k]->grad 137 ); 138 #endif 139 140 neuron->w[k]->value -= neuron->w[k]->grad * n->lr; 141 } 142 #ifdef DEBUG 143 printf( 144 "Layer %d, neuron %d, bias has value %f and grad %f\n", 145 i, j, 146 neuron->b->value, 147 neuron->b->grad 148 ); 149 #endif 150 neuron->b->value -= neuron->b->grad * n->lr; 151 } 152 } 153 } 154 155 void v_zero(Var* v) { 156 v->grad = 0; 157 if (v->parents[0] != NULL) v_zero(v->parents[0]); 158 if (v->parents[1] != NULL) v_zero(v->parents[1]); 159 } 160 161 Var** net_forward(Net* n, Var** inputs) { 162 Var** prev = inputs; 163 for (int i = 0; i < n->sz; i++) { 164 prev = layer_forward(n->layers[i], prev); 165 } 166 return prev; 167 } 168 169 float read_data_line(char* line, Var** inputs) { 170 int counter = 0; 171 float label; 172 char* pch = strtok(line, ","); 173 while (pch != NULL) { 174 if (counter == 4) { 175 label = strtof(pch, NULL); 176 } else { 177 inputs[counter]->value = strtof(pch, NULL); 178 } 179 pch = strtok(NULL, ","); 180 counter += 1; 181 } 182 return label; 183 } 184 185 int main() { 186 srand(time(NULL)); 187 size_t EPOCHS = 100; 188 float LR = 0.0001; 189 190 Net* net = net_new(3, LR); 191 net->layers[0] = layer_new(4, 8); 192 net->layers[1] = layer_new(8, 8); 193 net->layers[2] = layer_new(8, 1); 194 195 Var** inputs = malloc(4 * sizeof(Var*)); 196 for (int i = 0; i < 4; i++) inputs[i] = v_const(0.0); 197 198 size_t MAX_LINE = 64; 199 FILE* train = fopen("./train.txt", "r"); 200 FILE* test = fopen("./test.txt", "r"); 201 char* line = malloc(MAX_LINE * sizeof(char)); 202 for (int i = 0; i < EPOCHS; i++) { 203 float total_loss = 0; 204 float total_val_loss = 0; 205 int train_lines = 0; 206 int test_lines = 0; 207 208 while (getline(&line, &MAX_LINE, train) != -1) { 209 float label = read_data_line(line, inputs); 210 Var* out = net_forward(net, inputs)[0]; 211 Var* err = v_add(out, v_mul(v_const(-1), v_const(label))); 212 Var* loss = v_mul(err, err); 213 #ifdef DEBUG 214 printf(" batch | out: %f label: %f loss: %f\n", 215 out->value, label, loss->value); 216 #endif 217 total_loss += loss->value; 218 loss->grad = 1; 219 v_back(loss); 220 net_update(net); 221 v_zero(loss); 222 train_lines += 1; 223 } 224 225 while (getline(&line, &MAX_LINE, test) != -1) { 226 float label = read_data_line(line, inputs); 227 Var* out = net_forward(net, inputs)[0]; 228 Var* err = v_add(out, v_mul(v_const(-1), v_const(label))); 229 Var* loss = v_mul(err, err); 230 total_val_loss += loss->value; 231 loss->grad = 1; 232 test_lines += 1; 233 } 234 235 fseek(train, 0, SEEK_SET); 236 fseek(test, 0, SEEK_SET); 237 printf("epoch %d/%zu: avg train loss of %f, avg val loss of %f\n", 238 i+1, EPOCHS, total_loss/train_lines, total_val_loss/test_lines); 239 } 240 } 241 242 // Watch the neural network train with something like the following: 243 // 244 // head -n 1096 ./data_banknote_authentication.txt > train.txt 245 // tail -n 274 ./data_banknote_authentication.txt > test.txt 246 // gcc yoctograd.c -o nn 247 // ./nn