Skip to content

Commit 4014bb1

Browse files
committed
Add testing framework
1 parent b404cd4 commit 4014bb1

9 files changed

+233
-22
lines changed

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This is a bunch of code to port Keras neural network model into pure C++. Neural network weights and architecture are stored in plain text file and input is presented as `vector<vector<vector<float> > >` in case of image. The code is prepared to support simple Convolutional network (from MNIST example) but can be easily extended. There are implemented only ReLU and Softmax activations.
44

5-
It is working regardless the Keras backend.
5+
It is working with the Theano backend - support for Tensorflow will be added soon.
66

77
#Usage
88

@@ -16,3 +16,7 @@ It is working regardless the Keras backend.
1616
2. Dump network to plain text file `python dump_to_simple_cpp.py -a example/my_nn_arch.json -w example/my_nn_weights.h5 -o example/dumped.nnet`.
1717
3. Compile example `g++ -std=c++11 keras_model.cc example_main.cc` - see code in `example_main.cc`.
1818
4. Run binary `./a.out` - you shoul get the same output as in step one from Keras.
19+
20+
#Testing
21+
22+
If you want to test dumping for your network, please use `test_run.sh` script. Please provide there your network architecture and weights.

dump_to_simple_cpp.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@
1010
parser.add_argument('-a', '--architecture', help="JSON with model architecture", required=True)
1111
parser.add_argument('-w', '--weights', help="Model weights in HDF5 format", required=True)
1212
parser.add_argument('-o', '--output', help="Ouput file name", required=True)
13-
13+
parser.add_argument('-v', '--verbose', help="Verbose", required=False)
1414
args = parser.parse_args()
1515

1616
print 'Read architecture from', args.architecture
1717
print 'Read weights from', args.weights
1818
print 'Writing to', args.output
1919

20-
2120
arch = open(args.architecture).read()
2221
model = model_from_json(arch)
2322
model.load_weights(args.weights)
@@ -29,10 +28,12 @@
2928

3029
layers = []
3130
for ind, l in enumerate(arch["config"]):
32-
print ind, l
31+
if args.verbose:
32+
print ind, l
3333
fout.write('layer ' + str(ind) + ' ' + l['class_name'] + '\n')
3434

35-
print str(ind), l['class_name']
35+
if args.verbose:
36+
print str(ind), l['class_name']
3637
layers += [l['class_name']]
3738
if l['class_name'] == 'Convolution2D':
3839
#fout.write(str(l['config']['nb_filter']) + ' ' + str(l['config']['nb_col']) + ' ' + str(l['config']['nb_row']) + ' ')
@@ -42,7 +43,8 @@
4243
#fout.write('\n')
4344

4445
W = model.layers[ind].get_weights()[0]
45-
print W.shape
46+
if args.verbose:
47+
print W.shape
4648
fout.write(str(W.shape[0]) + ' ' + str(W.shape[1]) + ' ' + str(W.shape[2]) + ' ' + str(W.shape[3]) + ' ' + l['config']['border_mode'] + '\n')
4749

4850
for i in range(W.shape[0]):
@@ -60,7 +62,8 @@
6062
if l['class_name'] == 'Dense':
6163
#fout.write(str(l['config']['output_dim']) + '\n')
6264
W = model.layers[ind].get_weights()[0]
63-
print W.shape
65+
if args.verbose:
66+
print W.shape
6467
fout.write(str(W.shape[0]) + ' ' + str(W.shape[1]) + '\n')
6568

6669

example_main.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ int main() {
2121
DataChunk *sample = new DataChunk2D();
2222
sample->read_from_file("./example/sample_mnist.dat");
2323
std::cout << sample->get_3d().size() << std::endl;
24-
KerasModel m("./example/dumped.nnet");
24+
KerasModel m("./example/dumped.nnet", true);
2525
m.compute_output(sample);
2626
delete sample;
2727

keras_model.cc

+11-13
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ void keras::LayerConv2D::load_weights(std::ifstream &fin) {
4444
fin >> m_kernels_cnt >> m_depth >> m_rows >> m_cols >> m_border_mode;
4545
if (m_border_mode == "[") { m_border_mode = "valid"; skip = true; }
4646

47-
cout << "LayerConv2D " << m_kernels_cnt << "x" << m_depth << "x" << m_rows <<
48-
"x" << m_cols << " border_mode " << m_border_mode << endl;
47+
//cout << "LayerConv2D " << m_kernels_cnt << "x" << m_depth << "x" << m_rows <<
48+
// "x" << m_cols << " border_mode " << m_border_mode << endl;
4949
// reading kernel weights
5050
for(int k = 0; k < m_kernels_cnt; ++k) {
5151
vector<vector<vector<float> > > tmp_depths;
@@ -78,12 +78,12 @@ void keras::LayerConv2D::load_weights(std::ifstream &fin) {
7878

7979
void keras::LayerActivation::load_weights(std::ifstream &fin) {
8080
fin >> m_activation_type;
81-
cout << "Activation type " << m_activation_type << endl;
81+
//cout << "Activation type " << m_activation_type << endl;
8282
}
8383

8484
void keras::LayerMaxPooling::load_weights(std::ifstream &fin) {
8585
fin >> m_pool_x >> m_pool_y;
86-
cout << "MaxPooling " << m_pool_x << "x" << m_pool_y << endl;
86+
//cout << "MaxPooling " << m_pool_x << "x" << m_pool_y << endl;
8787
}
8888

8989
void keras::LayerDense::load_weights(std::ifstream &fin) {
@@ -100,18 +100,19 @@ void keras::LayerDense::load_weights(std::ifstream &fin) {
100100
fin >> tmp_char; // for ']'
101101
m_weights.push_back(tmp_n);
102102
}
103-
cout << "weights " << m_weights.size() << endl;
103+
//cout << "weights " << m_weights.size() << endl;
104104
fin >> tmp_char; // for '['
105105
for(int n = 0; n < m_neurons; ++n) {
106106
fin >> tmp_float;
107107
m_bias.push_back(tmp_float);
108108
}
109109
fin >> tmp_char; // for ']'
110-
cout << "bias " << m_bias.size() << endl;
110+
//cout << "bias " << m_bias.size() << endl;
111111

112112
}
113113

114-
keras::KerasModel::KerasModel(const string &input_fname) {
114+
keras::KerasModel::KerasModel(const string &input_fname, bool verbose)
115+
: m_verbose(verbose) {
115116
load_weights(input_fname);
116117
}
117118

@@ -361,28 +362,25 @@ std::vector<float> keras::KerasModel::compute_output(keras::DataChunk *dc) {
361362
inp = out;
362363
}
363364

364-
cout << "Output: ";
365-
out->show_values();
366-
367365
std::vector<float> flat_out = out->get_1d();
368366
delete out;
369367

370368
return flat_out;
371369
}
372370

373371
void keras::KerasModel::load_weights(const string &input_fname) {
374-
cout << "Reading model from " << input_fname << endl;
372+
if(m_verbose) cout << "Reading model from " << input_fname << endl;
375373
ifstream fin(input_fname.c_str());
376374
string layer_type = "";
377375
string tmp_str = "";
378376
int tmp_int = 0;
379377

380378
fin >> tmp_str >> m_layers_cnt;
381-
cout << "Layers " << m_layers_cnt << endl;
379+
if(m_verbose) cout << "Layers " << m_layers_cnt << endl;
382380

383381
for(int layer = 0; layer < m_layers_cnt; ++layer) { // iterate over layers
384382
fin >> tmp_str >> tmp_int >> layer_type;
385-
cout << "Layer " << tmp_int << " " << layer_type << endl;
383+
if(m_verbose) cout << "Layer " << tmp_int << " " << layer_type << endl;
386384

387385
Layer *l = 0L;
388386
if(layer_type == "Convolution2D") {

keras_model.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class keras::LayerDense : public Layer {
191191

192192
class keras::KerasModel {
193193
public:
194-
KerasModel(const std::string &input_fname);
194+
KerasModel(const std::string &input_fname, bool verbose);
195195
~KerasModel();
196196
std::vector<float> compute_output(keras::DataChunk *dc);
197197

@@ -204,6 +204,7 @@ class keras::KerasModel {
204204
void load_weights(const std::string &input_fname);
205205
int m_layers_cnt; // number of layers
206206
std::vector<Layer *> m_layers; // container with layers
207+
bool m_verbose;
207208

208209
};
209210

test_compare.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import numpy as np
2+
np.random.seed(1336)
3+
import json
4+
import argparse
5+
import sys
6+
7+
def get_numbers_from_file(fname):
8+
r = []
9+
with open(fname) as fin:
10+
for line in fin:
11+
tmp = line.split()
12+
r = [float(t) for t in tmp]
13+
return np.array(r)
14+
15+
np.set_printoptions(threshold=np.inf)
16+
parser = argparse.ArgumentParser(description='This is a simple script compare predictions from keras and keras2cpp.')
17+
18+
parser.add_argument('-k', '--keras_response', help="Response from Keras (test_run_cnn.py)", required=True)
19+
parser.add_argument('-c', '--keras2cpp_response', help="Response from Keras2cpp (test_run_cnn.cc)", required=True)
20+
args = parser.parse_args()
21+
22+
23+
keras_output = get_numbers_from_file(args.keras_response)
24+
keras2cpp_output = get_numbers_from_file(args.keras2cpp_response)
25+
26+
if len(keras_output) != len(keras2cpp_output):
27+
print "Different output dimensions"
28+
sys.exit(1)
29+
30+
sub = np.sum(np.abs(keras_output - keras2cpp_output))
31+
32+
if sub < 1e-6:
33+
print 'Test: [DONE]'
34+
print 'Dump is working correctly.'
35+
sys.exit(0)
36+
else:
37+
print 'Test: [ERROR]'
38+
print 'The output from Keras and Keras2cpp are different.'
39+
print 'Difference value:', sub
40+
sys.exit(1)

test_run.sh

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/bin/bash
2+
3+
echo 'Test for CNN dumping'
4+
5+
# Parameters
6+
INPUT_ARCH="../../keras2cpp_nets/th/52c55ce6-201b-4ce4-ad53-68b86d79804f.cnn.architecture.json"
7+
INPUT_WEIGHTS="../../keras2cpp_nets/th/52c55ce6-201b-4ce4-ad53-68b86d79804f.cnn.model.h5"
8+
9+
DUMPED_CNN="test_cnn.dumped"
10+
DATA_SAMPLE="test_random_input.dat"
11+
KERAS_OUTPUT="test_keras_output.dat"
12+
KERAS2CPP_OUTPUT="test_keras2cpp_output.dat"
13+
TEST_BIN="test_bin"
14+
15+
echo 'Test, step 1'
16+
echo 'Dump network into plain text file' $DUMPED_CNN
17+
python dump_to_simple_cpp.py -a $INPUT_ARCH -w $INPUT_WEIGHTS -o $DUMPED_CNN
18+
19+
echo 'Test, step 2'
20+
echo 'Generate random input sample and save in' $DATA_SAMPLE
21+
echo 'Compute ouput on generated sample with Keras and store predictions for comparison'
22+
python test_run_cnn.py -a $INPUT_ARCH -w $INPUT_WEIGHTS -d $DATA_SAMPLE -o $KERAS_OUTPUT
23+
24+
echo 'Test, step 3'
25+
echo 'Compile keras2cpp code'
26+
g++ -std=c++11 test_run_cnn.cc keras_model.cc -o $TEST_BIN
27+
echo 'Run predictions with dumped network and random data sample from step 2'
28+
./$TEST_BIN $DUMPED_CNN $DATA_SAMPLE $KERAS2CPP_OUTPUT
29+
30+
echo 'Test, step 4'
31+
echo 'Compare Keras and Keras2cpp outputs'
32+
python test_compare.py --keras_response $KERAS_OUTPUT --keras2cpp_response $KERAS2CPP_OUTPUT
33+
34+
# Clean
35+
echo 'Cleaning after test'
36+
rm $DUMPED_CNN
37+
rm $DATA_SAMPLE
38+
rm $KERAS_OUTPUT
39+
rm $KERAS2CPP_OUTPUT
40+
rm $TEST_BIN
41+
# used only if you log hidden layers output in test_run_cnn.py file
42+
#rm test_layer_*.output

test_run_cnn.cc

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#include "keras_model.h"
2+
3+
#include <iostream>
4+
5+
using namespace std;
6+
using namespace keras;
7+
8+
9+
10+
int main(int argc, char *argv[]) {
11+
12+
if(argc != 4) {
13+
cout << "Wrong input, going to exit." << endl;
14+
cout << "There should be arguments: dumped_cnn_file input_sample output_file." << endl;
15+
return -1;
16+
}
17+
string dumped_cnn = argv[1];
18+
string input_data = argv[2];
19+
string response_file = argv[3];
20+
21+
cout << "Testing network from " << dumped_cnn << " on data from " << input_data << endl;
22+
23+
// Input data sample
24+
DataChunk *sample = new DataChunk2D();
25+
sample->read_from_file(input_data);
26+
27+
// Construct network
28+
KerasModel m(dumped_cnn, false);
29+
std::vector<float> response = m.compute_output(sample);
30+
31+
// clean sample
32+
delete sample;
33+
34+
// save response into file
35+
ofstream fout(response_file);
36+
for(unsigned int i = 0; i < response.size(); i++) {
37+
fout << response[i] << " ";
38+
}
39+
fout.close();
40+
return 0;
41+
}

test_run_cnn.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import numpy as np
2+
np.random.seed(1336)
3+
from keras.models import Sequential, model_from_json
4+
import json
5+
import argparse
6+
from keras import backend as K
7+
8+
np.set_printoptions(threshold=np.inf)
9+
parser = argparse.ArgumentParser(description='This is a simple script to run Keras model from saved architecture and weights.\
10+
This script also creates a input data sample for c++ run_net.')
11+
12+
parser.add_argument('-a', '--architecture', help="JSON with model architecture", required=True)
13+
parser.add_argument('-w', '--weights', help="Model weights in HDF5 format", required=True)
14+
parser.add_argument('-d', '--data_sample', help="File where to write random data sample", required=True)
15+
parser.add_argument('-o', '--output', help="File where to write network outpu", required=True)
16+
parser.add_argument('-v', '--verbose', help="Verbose", required=False)
17+
args = parser.parse_args()
18+
19+
print 'Verbose', args.verbose
20+
print 'Read architecture from', args.architecture
21+
print 'Read weights from', args.weights
22+
23+
arch = open(args.architecture).read()
24+
model = model_from_json(arch)
25+
model.load_weights(args.weights)
26+
model.compile(loss='categorical_crossentropy', optimizer='adadelta')
27+
arch = json.loads(arch)
28+
print 'There are', str(len(model.layers)), 'layers in your network, is it good?'
29+
print 'I think yes :)'
30+
31+
32+
first_layer = arch["config"][0]["config"]
33+
input_shape = first_layer["batch_input_shape"]
34+
print "Input shape of your network", input_shape
35+
36+
print "Generate random input for testing purposes"
37+
random_input = np.random.rand(1, input_shape[1], input_shape[2], input_shape[3])
38+
print "Random input shape", random_input.shape
39+
response = model.predict(random_input)[0]
40+
if args.verbose:
41+
print '-'*50
42+
print 'Prediction from Keras'
43+
print response
44+
print '-'*50
45+
# save response to the file
46+
with open(args.output, "w") as fin:
47+
fin.write(' '.join([str(r) for r in response]))
48+
# store one sample in text file
49+
# this code is working for input_shape[1] == 1
50+
if input_shape[1] != 1:
51+
print '-'*50
52+
print 'Sorry but below code can be a buggy for image depth > 1 !!!'
53+
print '-'*50
54+
55+
# save random data sample into file
56+
with open(args.data_sample, "w") as fin:
57+
print "Save to", args.data_sample, "sample shape", str(input_shape[1]) + " " + str(input_shape[2]) + " " + str(input_shape[3])
58+
fin.write(str(input_shape[1]) + " " + str(input_shape[2]) + " " + str(input_shape[3]) + "\n")
59+
a = random_input[0,0]
60+
for b in a:
61+
fin.write(str(b)+'\n')
62+
63+
# Get layers output (for debuging)
64+
'''
65+
for l in xrange(len(model.layers)):
66+
with open('test_layer_' + str(l) + '.output', 'w') as fout:
67+
68+
get_layer_output = K.function([model.layers[0].input, K.learning_phase()],
69+
[model.layers[l].output])
70+
layer_output = get_layer_output([random_input, 0])
71+
72+
print 'Layer', l, layer_output[0].shape
73+
if l > 10:
74+
print layer_output
75+
fout.write(str(layer_output[0].shape) + '\n')
76+
fout.write(str(layer_output) + '\n')
77+
'''
78+
#print 'input?'
79+
#get_layer_output = K.function([model.layers[0].input, K.learning_phase()],
80+
# [model.layers[0].input])
81+
#layer_output = get_layer_output([random_input, 0])
82+
#print layer_output

0 commit comments

Comments
 (0)