├── .gitignore ├── README.md ├── CMakeLists.txt ├── BUILD ├── data_set.h ├── data_set.cc └── model.cc /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeCache.txt 2 | CMakeFiles 3 | CMakeScripts 4 | Testing 5 | Makefile 6 | cmake_install.cmake 7 | install_manifest.txt 8 | compile_commands.json 9 | CTestTestfile.cmake -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is a simple deep neural network trained using only TensorFlow C++. This is the support code of this [blog post](https://matrices.io/training-a-deep-neural-network-using-only-tensorflow-c/). 2 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0 FATAL_ERROR) 2 | project(DNN_Tensorflow_CPP LANGUAGES CXX) 3 | 4 | find_package(tensorflow REQUIRED) 5 | 6 | add_executable(main model.cc data_set.cc data_set.h) 7 | target_link_libraries(main PRIVATE tensorflow) 8 | 9 | configure_file(normalized_car_features.csv ${CMAKE_CURRENT_BINARY_DIR}/normalized_car_features.csv COPYONLY) 10 | if(MSVC) 11 | target_compile_definitions(main PRIVATE COMPILER_MSVC) 12 | endif(MSVC) -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | load("//tensorflow:tensorflow.bzl", "tf_cc_binary") 2 | 3 | tf_cc_binary( 4 | name = "model", 5 | srcs = [ 6 | "model.cc", 7 | "data_set.h", 8 | "data_set.cc" 9 | ], 10 | deps = [ 11 | "//tensorflow/cc:gradients", 12 | "//tensorflow/cc:grad_ops", 13 | "//tensorflow/cc:cc_ops", 14 | "//tensorflow/cc:client_session", 15 | "//tensorflow/core:tensorflow" 16 | ], 17 | data = ["normalized_car_features.csv"] 18 | ) -------------------------------------------------------------------------------- /data_set.h: -------------------------------------------------------------------------------- 1 | using namespace std; 2 | 3 | // Meta data used to normalize the data set. Useful to 4 | // go back and forth between normalized data. 5 | class DataSetMetaData { 6 | friend class DataSet; 7 | private: 8 | float mean_km; 9 | float std_km; 10 | float mean_age; 11 | float std_age; 12 | float min_price; 13 | float max_price; 14 | }; 15 | 16 | enum class Fuel { 17 | DIESEL, 18 | GAZOLINE 19 | }; 20 | 21 | class DataSet { 22 | public: 23 | // Construct a data set from the given csv file path. 24 | DataSet(string dir, string file_name) { 25 | ReadCSVFile(dir, file_name); 26 | } 27 | 28 | // getters 29 | vector& x() { return x_; } 30 | vector& y() { return y_; } 31 | 32 | // read the given csv file and complete x_ and y_ 33 | void ReadCSVFile(string dir, string file_name); 34 | 35 | // convert one csv line to a vector of float 36 | vector ReadCSVLine(string line); 37 | 38 | // normalize a human input using the data set metadata 39 | initializer_list input(float km, Fuel fuel, float age); 40 | 41 | // convert a price outputted by the DNN to a human price 42 | float output(float price); 43 | private: 44 | DataSetMetaData data_set_metadata; 45 | vector x_; 46 | vector y_; 47 | }; -------------------------------------------------------------------------------- /data_set.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "data_set.h" 6 | 7 | using namespace std; 8 | 9 | void DataSet::ReadCSVFile(string dir, string file_name) { 10 | // if cmake is used to build, the csv file will be next to the binary 11 | // - binary 12 | // - file.csv 13 | // if bazel is used, the csv file will be under some directories relative to the binary 14 | // - binary 15 | // - tensorflow/cc/models/file.csv 16 | ifstream file(file_name); 17 | if (!file) { 18 | file.open(dir + file_name); 19 | } 20 | if (!file) { 21 | cerr << "ERROR: No " << file_name << " next to the binary or at " << dir 22 | << ", please double check the location of the CSV dataset file." << endl; 23 | cerr << "ERROR: The dir option must be relative to the binary location." << endl; 24 | } 25 | stringstream buffer; 26 | buffer << file.rdbuf(); 27 | string line; 28 | vector lines; 29 | while(getline(buffer, line, '\n')) { 30 | lines.push_back(line); 31 | } 32 | 33 | // the first line contains the metadata 34 | vector metadata = ReadCSVLine(lines[0]); 35 | 36 | data_set_metadata.mean_km = metadata[0]; 37 | data_set_metadata.std_km = metadata[1]; 38 | data_set_metadata.mean_age = metadata[2]; 39 | data_set_metadata.std_age = metadata[3]; 40 | data_set_metadata.min_price = metadata[4]; 41 | data_set_metadata.max_price = metadata[5]; 42 | 43 | // the other lines contain the features for each car 44 | for (int i = 2; i < lines.size(); ++i) { 45 | vector features = ReadCSVLine(lines[i]); 46 | x_.insert(x_.end(), features.begin(), features.begin() + 3); 47 | y_.push_back(features[3]); 48 | } 49 | } 50 | 51 | vector DataSet::ReadCSVLine(string line) { 52 | vector line_data; 53 | std::stringstream lineStream(line); 54 | std::string cell; 55 | while(std::getline(lineStream, cell, ',')) 56 | { 57 | line_data.push_back(stod(cell)); 58 | } 59 | return line_data; 60 | } 61 | 62 | initializer_list DataSet::input(float km, Fuel fuel, float age) { 63 | km = (km - data_set_metadata.mean_km) / data_set_metadata.std_km; 64 | age = (age - data_set_metadata.mean_age) / data_set_metadata.std_age; 65 | float f = fuel == Fuel::DIESEL ? -1.f : 1.f; 66 | return {km, f, age}; 67 | } 68 | 69 | float DataSet::output(float price) { 70 | return price * (data_set_metadata.max_price - data_set_metadata.min_price) + data_set_metadata.min_price; 71 | } -------------------------------------------------------------------------------- /model.cc: -------------------------------------------------------------------------------- 1 | #include "tensorflow/cc/client/client_session.h" 2 | #include "tensorflow/cc/ops/standard_ops.h" 3 | #include "tensorflow/core/framework/tensor.h" 4 | #include "tensorflow/cc/framework/gradients.h" 5 | 6 | #include "data_set.h" 7 | 8 | using namespace tensorflow; 9 | using namespace tensorflow::ops; 10 | using namespace std; 11 | 12 | int main() { 13 | DataSet data_set("tensorflow/cc/models/", "normalized_car_features.csv"); 14 | Tensor x_data(DataTypeToEnum::v(), 15 | TensorShape{static_cast(data_set.x().size())/3, 3}); 16 | copy_n(data_set.x().begin(), data_set.x().size(), 17 | x_data.flat().data()); 18 | 19 | Tensor y_data(DataTypeToEnum::v(), 20 | TensorShape{static_cast(data_set.y().size()), 1}); 21 | copy_n(data_set.y().begin(), data_set.y().size(), 22 | y_data.flat().data()); 23 | 24 | Scope scope = Scope::NewRootScope(); 25 | 26 | auto x = Placeholder(scope, DT_FLOAT); 27 | auto y = Placeholder(scope, DT_FLOAT); 28 | 29 | // weights init 30 | auto w1 = Variable(scope, {3, 3}, DT_FLOAT); 31 | auto assign_w1 = Assign(scope, w1, RandomNormal(scope, {3, 3}, DT_FLOAT)); 32 | 33 | auto w2 = Variable(scope, {3, 2}, DT_FLOAT); 34 | auto assign_w2 = Assign(scope, w2, RandomNormal(scope, {3, 2}, DT_FLOAT)); 35 | 36 | auto w3 = Variable(scope, {2, 1}, DT_FLOAT); 37 | auto assign_w3 = Assign(scope, w3, RandomNormal(scope, {2, 1}, DT_FLOAT)); 38 | 39 | // bias init 40 | auto b1 = Variable(scope, {1, 3}, DT_FLOAT); 41 | auto assign_b1 = Assign(scope, b1, RandomNormal(scope, {1, 3}, DT_FLOAT)); 42 | 43 | auto b2 = Variable(scope, {1, 2}, DT_FLOAT); 44 | auto assign_b2 = Assign(scope, b2, RandomNormal(scope, {1, 2}, DT_FLOAT)); 45 | 46 | auto b3 = Variable(scope, {1, 1}, DT_FLOAT); 47 | auto assign_b3 = Assign(scope, b3, RandomNormal(scope, {1, 1}, DT_FLOAT)); 48 | 49 | // layers 50 | auto layer_1 = Tanh(scope, Tanh(scope, Add(scope, MatMul(scope, x, w1), b1))); 51 | auto layer_2 = Tanh(scope, Add(scope, MatMul(scope, layer_1, w2), b2)); 52 | auto layer_3 = Tanh(scope, Add(scope, MatMul(scope, layer_2, w3), b3)); 53 | 54 | // regularization 55 | auto regularization = AddN(scope, 56 | initializer_list{L2Loss(scope, w1), 57 | L2Loss(scope, w2), 58 | L2Loss(scope, w3)}); 59 | 60 | // loss calculation 61 | auto loss = Add(scope, 62 | ReduceMean(scope, Square(scope, Sub(scope, layer_3, y)), {0, 1}), 63 | Mul(scope, Cast(scope, 0.01, DT_FLOAT), regularization)); 64 | 65 | // add the gradients operations to the graph 66 | std::vector grad_outputs; 67 | TF_CHECK_OK(AddSymbolicGradients(scope, {loss}, {w1, w2, w3, b1, b2, b3}, &grad_outputs)); 68 | 69 | // update the weights and bias using gradient descent 70 | auto apply_w1 = ApplyGradientDescent(scope, w1, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[0]}); 71 | auto apply_w2 = ApplyGradientDescent(scope, w2, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[1]}); 72 | auto apply_w3 = ApplyGradientDescent(scope, w3, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[2]}); 73 | auto apply_b1 = ApplyGradientDescent(scope, b1, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[3]}); 74 | auto apply_b2 = ApplyGradientDescent(scope, b2, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[4]}); 75 | auto apply_b3 = ApplyGradientDescent(scope, b3, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[5]}); 76 | 77 | ClientSession session(scope); 78 | std::vector outputs; 79 | 80 | // init the weights and biases by running the assigns nodes once 81 | TF_CHECK_OK(session.Run({assign_w1, assign_w2, assign_w3, assign_b1, assign_b2, assign_b3}, nullptr)); 82 | 83 | // training steps 84 | for (int i = 0; i < 5000; ++i) { 85 | if (i % 100 == 0) { 86 | TF_CHECK_OK(session.Run({{x, x_data}, {y, y_data}}, {loss}, &outputs)); 87 | std::cout << "Loss after " << i << " steps " << outputs[0].scalar() << std::endl; 88 | } 89 | // nullptr because the output from the run is useless 90 | TF_CHECK_OK(session.Run({{x, x_data}, {y, y_data}}, {apply_w1, apply_w2, apply_w3, apply_b1, apply_b2, apply_b3}, nullptr)); 91 | } 92 | 93 | // prediction using the trained neural net 94 | TF_CHECK_OK(session.Run({{x, {data_set.input(110000.f, Fuel::DIESEL, 7.f)}}}, {layer_3}, &outputs)); 95 | cout << "DNN output: " << *outputs[0].scalar().data() << endl; 96 | std::cout << "Price predicted " << data_set.output(*outputs[0].scalar().data()) << " euros" << std::endl; 97 | 98 | // saving the model 99 | //GraphDef graph_def; 100 | //TF_ASSERT_OK(scope.ToGraphDef(&graph_def)); 101 | 102 | return 0; 103 | } --------------------------------------------------------------------------------