├── .gitignore ├── examples ├── example-usage-cycle.cpp ├── mlp1.cpp ├── graph.cpp ├── neuron.cpp ├── example-usage.cpp ├── c-plus-equals-cycle.dot ├── binary-classifier.cpp ├── CMakeLists.txt ├── regression0.cpp ├── c-plus-equals-cycle.svg ├── c-plus-equals-rewrite.svg ├── graph.svg └── neuron.svg ├── tests ├── multivariate-test.cpp ├── mac-test.cpp ├── CMakeLists.txt └── value-test.cpp ├── include ├── array.h ├── randomdata.h ├── tuple.h ├── mac.h ├── backprop.h ├── loss.h ├── graph.h ├── nn.h └── value.h ├── CMakeLists.txt ├── LICENSE ├── .github └── workflows │ └── cmake-multi-platform.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.swp 3 | 4 | -------------------------------------------------------------------------------- /examples/example-usage-cycle.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "value.h" 4 | #include "graph.h" 5 | 6 | using namespace ai; 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | auto a = make_value(-4.0, "a"); 11 | auto b = make_value(2.0, "b"); 12 | 13 | auto c = expr(a + b, "c");; 14 | auto d = a * b + pow(b, 3); 15 | 16 | c += c + 1; 17 | 18 | std::cout << Graph(c) << std::endl; 19 | } 20 | -------------------------------------------------------------------------------- /tests/multivariate-test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "graph.h" 4 | 5 | using namespace ai; 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | auto a = make_value(-2.0, "a"); 10 | auto b = make_value(3.0, "b"); 11 | 12 | auto d = expr(a*b, "d"); 13 | auto e = expr(a+b, "e"); 14 | auto f = expr(d*e, "f"); 15 | 16 | backward(f); 17 | 18 | std::cout << Graph(f) << std::endl; 19 | } 20 | -------------------------------------------------------------------------------- /examples/mlp1.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "nn.h" 4 | #include "graph.h" 5 | 6 | using namespace ai; 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | // Define a neural net 11 | MLP1 n; 12 | 13 | std::array input = {{ 2.0, 3.0, -1.0 }}; 14 | auto output = n(input); 15 | 16 | backward(output); 17 | 18 | std::cout << Graph(output) << std::endl; 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/graph.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "graph.h" 4 | 5 | using namespace ai; 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | auto a = make_value(2.0, "a"); 10 | auto b = make_value(-3.0, "b"); 11 | auto c = make_value(10.0, "c"); 12 | 13 | auto e = expr(a*b, "e"); 14 | auto d = expr(e+c, "d"); 15 | 16 | auto f = make_value(-2.0, "f"); 17 | auto L = expr(d * f, "L"); 18 | 19 | std::cout << Graph(L) << std::endl; 20 | } 21 | -------------------------------------------------------------------------------- /tests/mac-test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "mac.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | std::vector vec_a = {1, 2, 3}; 8 | std::vector vec_b = {4, 5, 6}; 9 | std::array arr_a = {1, 2, 3}; 10 | std::array arr_b = {4, 5, 6}; 11 | 12 | std::cout << "Vector multiply-accumulate: " << mac(vec_a, vec_b) << std::endl; 13 | std::cout << "Array multiply-accumulate: " << mac(arr_a, arr_b) << std::endl; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /include/array.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | template 7 | class PrettyArray : public std::array { 8 | public: 9 | PrettyArray() = default; 10 | PrettyArray(const std::array& arr) : std::array(arr) {} 11 | 12 | friend std::ostream& operator<<(std::ostream& os, const PrettyArray& arr) { 13 | os << "[\n"; 14 | for (const auto& elem : arr) { 15 | os << '\t' << elem << '\n'; 16 | } 17 | os << ']'; 18 | return os; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | set(CMAKE_CXX_STANDARD 23) 4 | set(CMAKE_CXX_FLAGS_RELEASE "-O3") 5 | 6 | enable_testing() 7 | 8 | project(tests) 9 | 10 | get_filename_component(PARENT_DIR ${PROJECT_SOURCE_DIR} DIRECTORY) 11 | 12 | include_directories( 13 | ${PARENT_DIR}/include 14 | ) 15 | 16 | add_executable(value-test value-test.cpp) 17 | add_test(NAME value-test 18 | COMMAND value-test) 19 | 20 | add_executable(multivariate-test multivariate-test.cpp) 21 | add_test(NAME multivariate-test 22 | COMMAND multivariate-test) 23 | 24 | add_executable(mac-test mac-test.cpp) 25 | add_test(NAME mac-test 26 | COMMAND mac-test) 27 | -------------------------------------------------------------------------------- /examples/neuron.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "graph.h" 4 | 5 | using namespace ai; 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | // Inputs x1, x2 10 | auto x1 = make_value(2.0, "x1"); 11 | auto x2 = make_value(0.0, "x2"); 12 | 13 | // Weights w1, w2 14 | auto w1 = make_value(-3.0, "w1"); 15 | auto w2 = make_value(1.0, "w2"); 16 | 17 | // Bias of the neuron 18 | auto b = make_value(6.8813735870195432, "b"); 19 | 20 | auto x1w1 = expr(x1*w1, "x1*w1"); 21 | auto x2w2 = expr(x2*w2, "x2*w2"); 22 | 23 | auto x1w1x2w2 = expr(x1w1 + x2w2, "x1w1+x2w2"); 24 | auto n = expr(x1w1x2w2 + b, "n"); 25 | 26 | auto o = expr(tanh(n), "o"); 27 | 28 | backward(o); 29 | 30 | std::cout << Graph(o) << std::endl; 31 | } 32 | -------------------------------------------------------------------------------- /examples/example-usage.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "value.h" 4 | 5 | using namespace ai; 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | auto a = make_value(-4.0); 10 | auto b = make_value(2.0); 11 | 12 | auto c = a + b; 13 | auto d = a * b + pow(b, 3); 14 | 15 | c += c + 1; 16 | c += 1 + c + (-a); 17 | d += d * 2 + relu(b + a); 18 | d += 3 * d + relu(b - a); 19 | auto e = c - d; 20 | auto f = pow(e, 2); 21 | auto g = f / 2.0; 22 | g += 10.0 / f; 23 | printf("%.4f\n", g->data()); // prints 24.7041, the outcome of this forward pass 24 | backward(g); 25 | printf("%.4f\n", a->grad()); // prints 138.8338, i.e. the numerical value of dg/da 26 | printf("%.4f\n", b->grad()); // prints 645.5773, i.e. the numerical value of dg/db 27 | } 28 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | enable_testing() 4 | 5 | project(ai-play) 6 | 7 | set(CMAKE_CXX_STANDARD 23) 8 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 9 | set(CMAKE_CXX_EXTENSIONS OFF) 10 | 11 | # Global compiler flags 12 | if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") 13 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wextra -Wpedantic -g") 14 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Wall -Wextra -O3 -march=native -mtune=native -mavx2 -ffast-math") 15 | endif() 16 | 17 | if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") 18 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wextra -Wpedantic -g") 19 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Wall -Wextra -O3 -march=native -mtune=native -mavx2 -ffast-math") 20 | endif() 21 | 22 | 23 | add_subdirectory(tests) 24 | 25 | add_subdirectory(examples) 26 | -------------------------------------------------------------------------------- /include/randomdata.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "value.h" 9 | 10 | namespace ai { 11 | 12 | // Static inline function to generate a random T 13 | template 14 | static inline Value randomValue() { 15 | static unsigned int seed = 42; 16 | static thread_local std::mt19937 gen(seed++); 17 | std::uniform_real_distribution dist(-1.0, 1.0); 18 | seed = gen(); // update seed for next time 19 | return make_value(dist(gen)); 20 | } 21 | 22 | // Static inline function to generate a random std::array 23 | template 24 | static inline std::array, N> randomArray() { 25 | std::array, N> arr; 26 | for (auto& element : arr) { 27 | element = randomValue(); 28 | } 29 | return arr; 30 | } 31 | 32 | } // namespace ai 33 | -------------------------------------------------------------------------------- /examples/c-plus-equals-cycle.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | rankdir = "LR"; 3 | "node0x55628c103eb0" [label = "{ | data=-4.0000 | grad=0.0000 }", shape="record"] 4 | "node0x55628c103fb0" [label = "{ | data=2.0000 | grad=0.0000 }", shape="record"] 5 | "node0x55628c104130" [label = "{ | data=-3.0000 | grad=0.0000 }", shape="record"] 6 | "node0x55628c104130+" [label = "+"] 7 | "node0x55628c104130+" -> "node0x55628c104130"; 8 | "node0x55628c104930" [label = "{ | data=1.0000 | grad=0.0000 }", shape="record"] 9 | "node0x55628c104a30" [label = "{ | data=-1.0000 | grad=0.0000 }", shape="record"] 10 | "node0x55628c104a30+" [label = "+"] 11 | "node0x55628c104a30+" -> "node0x55628c104a30"; 12 | "node0x55628c103eb0" -> "node0x55628c104130+"; 13 | "node0x55628c103fb0" -> "node0x55628c104130+"; 14 | "node0x55628c104130" -> "node0x55628c104a30+"; 15 | "node0x55628c104930" -> "node0x55628c104a30+"; 16 | "node0x55628c104a30" -> "node0x55628c104130+"; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /include/tuple.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | template 6 | void print_args_impl(std::ostream& os) { 7 | os << N; 8 | ((os << ',' << Ns), ...); 9 | } 10 | 11 | template 12 | void print_args(std::ostream& os) { 13 | os << "<"; 14 | if constexpr (sizeof...(Ns) > 0) { 15 | print_args_impl(os); 16 | } 17 | os << ">"; 18 | } 19 | 20 | template 21 | inline typename std::enable_if::type 22 | print_tuple(std::ostream& os, const std::tuple& t) 23 | { 24 | return os; 25 | } 26 | 27 | template 28 | inline typename std::enable_if::type 29 | print_tuple(std::ostream& os, const std::tuple& t) 30 | { 31 | os << std::get(t); 32 | if (I + 1 != sizeof...(Tp)) os << ", "; 33 | return print_tuple(os, t); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /examples/binary-classifier.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "backprop.h" 4 | #include "nn.h" 5 | #include "graph.h" 6 | 7 | using namespace ai; 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | // Define a neural net 12 | MLP1 n; 13 | std::cerr << n << std::endl; 14 | 15 | // A set of training inputs 16 | std::array, 4> input = {{ 17 | {2.0, 3.0, -1.0}, 18 | {3.0, -1.0, 0.5}, 19 | {0.5, 1.0, 1.0}, 20 | {1.0, 1.0, -1.0} 21 | }}; 22 | 23 | // Corresponding ground truth values for these inputs 24 | std::array y = {1.0, -1.0, -1.0, 1.0}; 25 | std::cerr << "y (gt):\t" << PrettyArray(y) << std::endl; 26 | 27 | double learning_rate = 0.9; 28 | 29 | auto backprop = BackProp, 4>(n, "loss.tsv"); 30 | 31 | // Run backprop for 20 iterations, verbose=true 32 | double loss = backprop(input, y, learning_rate, 20, true); 33 | } 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Conrad Parker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | set(CMAKE_CXX_STANDARD 23) 4 | set(CMAKE_CXX_FLAGS_RELEASE "-O3") 5 | 6 | enable_testing() 7 | 8 | project(examples) 9 | 10 | get_filename_component(PARENT_DIR ${PROJECT_SOURCE_DIR} DIRECTORY) 11 | 12 | include_directories( 13 | ${PARENT_DIR}/include 14 | ) 15 | 16 | add_executable(example-usage example-usage.cpp) 17 | add_test(NAME example-usage 18 | COMMAND example-usage) 19 | 20 | add_executable(example-usage-cycle example-usage-cycle.cpp) 21 | add_test(NAME example-usage-cycle 22 | COMMAND example-usage-cycle) 23 | 24 | add_executable(graph graph.cpp) 25 | add_test(NAME graph 26 | COMMAND graph) 27 | 28 | add_executable(neuron neuron.cpp) 29 | add_test(NAME neuron 30 | COMMAND neuron) 31 | 32 | add_executable(mlp1 mlp1.cpp) 33 | add_test(NAME mlp1 34 | COMMAND mlp1) 35 | 36 | add_executable(regression0 regression0.cpp) 37 | add_test(NAME regression0 38 | COMMAND regression0) 39 | 40 | add_executable(binary-classifier binary-classifier.cpp) 41 | add_test(NAME binary-classifier 42 | COMMAND binary-classifier) 43 | -------------------------------------------------------------------------------- /tests/value-test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "value.h" 4 | 5 | using namespace ai; 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | auto a = make_value(2.0, "a"); 10 | auto b = make_value(-3.0, "b"); 11 | auto c = make_value(10.0, "c"); 12 | 13 | std::cout << *a << std::endl; 14 | std::cout << b << std::endl; 15 | 16 | auto x = a + b; 17 | std::cout << x << std::endl; 18 | 19 | std::cout << a+b << std::endl; 20 | 21 | std::cout << a+7.0 << std::endl; 22 | 23 | std::cout << 7.0+b << std::endl; 24 | 25 | std::cout << a+7.0+b << std::endl; 26 | 27 | std::cout << a+b+c << std::endl; 28 | 29 | std::cout << (a*b) << std::endl; 30 | 31 | std::cout << (a-b) << std::endl; 32 | 33 | auto minus1 = make_value(-1.0, "-1.0"); 34 | auto nb = expr(b * minus1, "nb"); 35 | std::cout << (a + nb) << std::endl; 36 | 37 | auto g = expr(a/b, "a/b");; 38 | std::cout << g << std::endl; 39 | 40 | auto tg = expr(tanh(g), "tanh(g)"); 41 | std::cout << tg << std::endl; 42 | backward(tg); 43 | 44 | auto br = expr(recip(b), "br");; 45 | std::cout << br << std::endl; 46 | 47 | auto f = expr(a * br, "f"); 48 | std::cout << f << std::endl; 49 | 50 | auto tt = expr(tanh(f), "tanh(f)");; 51 | std::cout << tt << std::endl; 52 | 53 | backward(tt); 54 | 55 | auto y = (a*b) + c; 56 | std::cout << y << std::endl; 57 | std::cout << (a*b + c) << std::endl; 58 | } 59 | -------------------------------------------------------------------------------- /examples/regression0.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "backprop.h" 4 | #include "nn.h" 5 | #include "graph.h" 6 | 7 | using namespace ai; 8 | 9 | class Regression0 { 10 | public: 11 | Regression0() 12 | : weight_(randomValue()) 13 | {} 14 | 15 | Value weight() const { 16 | return weight_; 17 | } 18 | 19 | Value operator()(const Value& x) const { 20 | return weight_ * x; 21 | } 22 | 23 | Value operator()(const double& x) const { 24 | return this->operator()(make_value(x)); 25 | } 26 | 27 | void adjust(const double& learning_rate) { 28 | weight_->adjust(learning_rate); 29 | } 30 | 31 | private: 32 | Value weight_; 33 | }; 34 | 35 | static inline std::ostream& operator<<(std::ostream& os, const Regression0& r) 36 | { 37 | return os << "Regression0{weight=" << r.weight() << "}"; 38 | } 39 | 40 | int main(int argc, char *argv[]) 41 | { 42 | Regression0 n; 43 | 44 | std::cerr << n << std::endl; 45 | 46 | std::array input = { 47 | {-7.0, -3.0, 1.0, 4.0}, 48 | }; 49 | 50 | std::array y = {-21.0, -9.0, 3.0, 12.0}; 51 | std::cerr << "y (gt):\t" << PrettyArray(y) << std::endl; 52 | 53 | // Run backprop 54 | double learning_rate = 0.01; 55 | 56 | auto backprop = BackProp(n, "loss.tsv"); 57 | 58 | double loss = backprop(input, y, learning_rate, 40, true); 59 | 60 | std::cout << Graph(backprop.loss_function()(input, y)) << std::endl; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /include/mac.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | template