hacks

(tidier examples of) random scripts from throughout the years
Log | Files | Refs | README

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