Browse Source

Experimental Keras JSON loader

Lutz Roeder 8 years ago
parent
commit
db672d1b8c
11 changed files with 600 additions and 62 deletions
  1. 7 5
      setup.py
  2. 2 1
      src/app.js
  3. 447 0
      src/keras-model.js
  4. 85 0
      src/keras-operator.json
  5. 7 7
      src/onnx-model.js
  6. 13 15
      src/tf-model.js
  7. 22 26
      src/tflite-model.js
  8. 4 3
      src/view-browser.html
  9. 4 3
      src/view-electron.html
  10. 1 1
      src/view-render.css
  11. 8 1
      src/view.js

+ 7 - 5
setup.py

@@ -19,20 +19,22 @@ package_data={
     'netron': [ 
         'netron',
         'netron.py',
-        'onnx_ml_pb2.py',
         'logo.svg',
+        'onnx_ml_pb2.py',
         'onnx.js',
         'onnx-operator.json',
+        'view-browser.js',
         'tf.js',
         'tf-operator.pb',
+        'tf-model.js',
         'tflite.js',
         'tflite-operator.json',
+        'tflite-model.js',
+        'keras-operator.json',
+        'keras-model.js',
         'favicon.ico',
         'view-browser.html',
-        'view-browser.js',
-        'view-onnx.js',
-        'view-tf.js',
-        'view-tflite.js',
+        'onnx-model.js',
         'view-render.css',
         'view-render.js',
         'view-template.js',

+ 2 - 1
src/app.js

@@ -35,7 +35,8 @@ function openFileDialog() {
             { name: 'ONNX Model', extensions: [ 'onnx', 'pb' ] },
             { name: 'TensorFlow Saved Model', extensions: [ 'saved_model.pb' ] },
             { name: 'TensorFlow Graph', extensions: [ 'pb' ] },
-            { name: 'TensorFlow Lite Model', extensions: [ 'tflite' ]}
+            { name: 'TensorFlow Lite Model', extensions: [ 'tflite' ] },
+            { name: 'Keras Model', extension: [ 'json', 'keras', 'h5' ] }
         ]
     };
     electron.dialog.showOpenDialog(showOpenDialogOptions, (selectedFiles) => {

+ 447 - 0
src/keras-model.js

@@ -0,0 +1,447 @@
+/*jshint esversion: 6 */
+
+// Experimental
+
+class KerasModel {
+
+    static open(buffer, identifier, host, callback) { 
+        try {
+            var json = null;
+
+            var extension = identifier.split('.').pop();
+            if (extension == 'keras' || extension == 'h5') {
+                throw new Error('Keras H5 format not supported yet.');
+            }
+
+            if (extension == 'json') {
+                if (!window.TextDecoder) {
+                    throw new Error('TextDecoder not avaialble.');
+                }
+
+                var decoder = new TextDecoder('utf-8');
+                json = decoder.decode(buffer);
+            }
+
+            var root = JSON.parse(json);
+            var model = new KerasModel(root);
+
+            KerasOperatorMetadata.open(host, (err, metadata) => {
+                callback(null, model);
+            });
+        }
+        catch (err) {
+            callback(err, null);
+        }
+    }
+
+    constructor(root) {
+        if (!root.class_name) {
+            throw new Error('class_name is not present.');
+        }
+        if (root.class_name != 'Model' && root.class_name != 'Sequential') {
+            throw new Error('\'' + root.class_name + '\' is not supported.');
+        }
+        var graph = new KerasGraph(root);
+        this._graphs = [ graph ];
+        this._activeGraph = graph; 
+    }
+
+    format() {
+        var summary = { properties: [], graphs: [] };
+
+        this.graphs.forEach((graph) => {
+            summary.graphs.push({
+                name: graph.name,
+                inputs: graph.inputs,
+                outputs: graph.outputs
+            });
+        });
+
+        summary.properties.push({ 
+            name: 'Format', 
+            value: 'Keras'
+        });
+
+        return summary;
+    }
+
+    get graphs() {
+        return this._graphs;
+    }
+
+    get activeGraph() {
+        return this._activeGraph;
+    }
+
+    updateActiveGraph(name) {
+        this._activeGraph = (name == this._graphs[0].name) ? this._graph : null;
+    }
+}
+
+class KerasGraph {
+
+    constructor(root) {
+        this._name = root.name;
+        this._inputs = [];
+        this._outputs = [];
+        this._nodes = [];
+
+        switch (root.class_name) {
+            case 'Sequential':
+                this.loadSequential(root.config);
+                break;
+            case 'Model':
+                this.loadModel(root.config);
+                break;
+        }
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get inputs() {
+        return this._inputs;
+    }
+
+    get outputs() {
+        return this._outputs;
+    }
+
+    get nodes() {
+        return this._nodes;
+    }
+
+    loadModel(root) {
+
+        if (root.layers) {
+
+            var nodeMap = {};
+            root.layers.forEach((layer) => {
+                if (layer.name) {
+                    if (!nodeMap[layer.name]) {
+                        nodeMap[layer.name] = layer;
+                        layer._inputs = [];
+                        layer._outputs = [];
+                    }
+                }
+            });
+            root.layers.forEach((layer) => {
+                if (layer.inbound_nodes) {
+                    layer.inbound_nodes.forEach((inbound_node) => {
+                        var input = { connections: [] };
+                        inbound_node.forEach((inbound_connection) => {
+                            var inputName = inbound_connection[0];
+                            input.connections.push({ id: inputName });
+                            var inputNode = nodeMap[inputName];
+                            if (inputNode) {
+                                inputNode._outputs.push(inputNode.name);
+                            }
+                        });       
+                        layer._inputs.push(input);
+                    });
+                }
+            });
+        }
+
+        /*
+        if (root.input_layers) {
+            root.input_layers.forEach((input_layer) => {
+                this._inputs.push({ id: input_layer[0], name: input_layer[0] });
+            });    
+        }
+        */
+
+        if (root.output_layers) {
+            root.output_layers.forEach((output_layer) => {
+                var inputName = output_layer[0];
+                var inputNode = nodeMap[inputName];
+                if (inputNode) {
+                    inputNode._outputs.push(inputName);
+                }
+                this._outputs.push({ id: inputName, name: inputName, type: '?' });
+            });
+        }
+
+        if (root.layers) {
+            root.layers.forEach((layer) => {
+                var node = new KerasNode(layer.class_name, layer.name, layer.config, layer._inputs, layer._outputs);
+                this._nodes.push(node);
+            });
+        }
+    }
+
+    loadSequential(root) {
+        var output = 'input';
+
+        this._inputs.push({
+            name: output,
+            id: output,
+            type: '?'
+        });
+
+        var id = 0;
+        root.forEach((layer) => {
+            var inputs = [];
+            if (output) {
+                inputs.push({
+                    name: '(0)',
+                    connections: [ { id: output }]
+                });
+            }
+
+            var name = id.toString();
+            if (layer.config || layer.config.name) {
+                name = layer.config.name;
+            }
+            id++;
+            output = name;
+
+            var outputs = [ output ];
+
+            var node = new KerasNode(layer.class_name, name, layer.config, inputs, outputs);
+            this._nodes.push(node);
+        });
+
+        this._outputs.push({
+            name: 'output',
+            id: output,
+            type: '?'
+        });
+    }
+}
+
+class KerasNode {
+
+    constructor(operator, name, config, inputs, outputs) {
+        this._operator = operator;
+        this._name = name;
+        this._config = config;
+        this._inputs = inputs;
+        this._outputs = outputs;
+    }
+
+    get operator() {
+        return this._operator;
+    }
+
+    get category() {
+        return KerasOperatorMetadata.operatorMetadata.getOperatorCategory(this.operator);
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get inputs() {
+        var results = [];
+        this._inputs.forEach((input, index) => {
+            results.push({ 
+                name: '(' + index.toString() + ')', 
+                connections: input.connections
+            });
+        });
+        return results;
+    }
+
+    get outputs() {
+        var results = [];
+        this._outputs.forEach((output, index) => {
+            results.push({ 
+                name: '(' + index.toString() + ')', 
+                connections: [ { id: output }]
+            });
+        });
+        return results;
+    }
+
+    get attributes() {
+        var results = [];
+        if (this._config) {
+            Object.keys(this._config).forEach((name) => {
+                var value = this._config[name];
+                if (name != 'name' && value != null) {
+                    var hidden = !KerasOperatorMetadata.operatorMetadata.showAttribute(this.operator, name, value);
+                    results.push(new KerasAttribute(name, value, hidden));
+                }
+            });
+        }
+        return results;
+    }
+
+    get dependencies() {
+        return [];
+    }
+}
+
+class KerasAttribute {
+
+    constructor(name, value, hidden) {
+        this._name = name;
+        this._value = value;
+        this._hidden = hidden;
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get value() {
+        if (this._value == true) {
+            return 'true';
+        }
+        if (this._value == false) {
+            return 'false';
+        }
+        if (this._value) {
+            return JSON.stringify(this._value);
+        }
+        return '?';
+    }
+
+    get hidden() {
+        return this._hidden;
+    }
+}
+
+class KerasOperatorMetadata {
+
+    static open(host, callback) {
+        if (KerasOperatorMetadata.operatorMetadata) {
+            callback(null, KerasOperatorMetadata.operatorMetadata);
+        }
+        else {
+            host.request('/keras-operator.json', (err, data) => {
+                if (err == null) {
+                    KerasOperatorMetadata.operatorMetadata = new KerasOperatorMetadata(data);
+                }
+                callback(null, KerasOperatorMetadata.operatorMetadata);
+            });    
+        }
+    }
+
+    constructor(data) {
+        this._map = {};
+        var items = JSON.parse(data);
+        if (items) {
+            items.forEach((item) => {
+                if (item.name && item.schema)
+                {
+                    this._map[item.name] = item.schema;
+                }
+            });
+        }
+
+        this._categoryMap = {
+            'Conv1D': 'Layer',
+            'Conv2D': 'Layer',
+            'Conv3D': 'Layer',
+            'Convolution1D': 'Layer',
+            'Convolution2D': 'Layer',
+            'Convolution3D': 'Layer',
+            'DepthwiseConv2D': 'Layer',
+            'Dense': 'Layer',
+            'BatchNormalization': 'Normalization',
+            'Concatenate': 'Tensor',
+            'Activation': 'Activation',
+            'GlobalAveragePooling2D': 'Pool',
+            'AveragePooling2D': 'Pool',
+            'MaxPooling2D': 'Layer',
+            'Flatten': 'Shape',
+            'Reshape': 'Shape',
+            'Dropout': 'Dropout'
+        };    
+    }
+
+    showAttribute(operator, attributeName, attributeValue) {
+        if (attributeName == 'trainable') {
+            return false;
+        }
+        return !this.defaultAttribute(operator, attributeName, attributeValue);
+    }
+
+    defaultAttribute(operator, attributeName, attributeValue) {
+        var schema = this._map[operator];
+        if (schema && schema.attributes && schema.attributes.length > 0) {
+            if (!schema.attributeMap) {
+                schema.attributeMap = {};
+            }
+            schema.attributes.forEach(attribute => {
+                schema.attributeMap[attribute.name] = attribute;
+            });
+
+            var attribute = schema.attributeMap[attributeName];
+            if (attribute) {
+                if (attribute && attribute.hasOwnProperty('default')) {
+                    return KerasOperatorMetadata.isEquivalent(attribute.default, attributeValue);
+                }
+            }
+        }
+        return false;
+    }
+
+    getOperatorCategory(operator) {
+        var category = this._categoryMap[operator];
+        if (category) {
+            return category;
+        }
+        return null;
+    }
+
+    static isEquivalent(a, b) {
+        if (a === b) {
+            return a !== 0 || 1 / a === 1 / b;
+        }
+        if (a == null || b == null) {
+            return false;
+        }
+        if (a !== a) {
+            return b !== b;
+        }
+        var type = typeof a;
+        if (type !== 'function' && type !== 'object' && typeof b != 'object') {
+            return false;
+        }
+        var className = toString.call(a);
+        if (className !== toString.call(b)) {
+            return false;
+        }
+        switch (className) {
+            case '[object RegExp]':
+            case '[object String]':
+                return '' + a === '' + b;
+            case '[object Number]':
+                if (+a !== +a) {
+                    return +b !== +b;
+                }
+                return +a === 0 ? 1 / +a === 1 / b : +a === +b;
+            case '[object Date]':
+            case '[object Boolean]':
+                return +a === +b;
+            case '[object Array]':
+                var length = a.length;
+                if (length !== b.length) {
+                    return false;
+                }
+                while (length--) {
+                    if (!KerasOperatorMetadata.isEquivalent(a[length], b[length])) {
+                        return false;
+                    }
+                }
+                return true;
+        }
+
+        var keys = Object.keys(a);
+        var size = keys.length;
+        if (Object.keys(b).length != size) {
+            return false;
+        } 
+        while (size--) {
+            var key = keys[size];
+            if (!(b.hasOwnProperty(key) && KerasOperatorMetadata.isEquivalent(a[key], b[key]))) {
+                return false;
+            }
+        }
+        return true;
+    }
+}

+ 85 - 0
src/keras-operator.json

@@ -0,0 +1,85 @@
+[
+  {
+    "name": "Bidirectional",
+    "schema": {
+      "attributes": [
+        { "name": "merge_mode", "default": "concat" }
+      ]
+    }
+  },
+  {
+    "name": "Activation",
+    "schema": {
+      "attributes": [
+      ]
+    }
+  },
+  {
+    "name": "MaxPooling2D",
+    "schema": {
+      "attributes": [
+        { "name": "data_format", "default": "channels_last" },
+        { "name": "padding", "default": "valid" },
+        { "name": "pool_size", "default": [2, 2] },
+        { "name": "strides", "default": [2, 2] }
+      ]
+    }
+  },
+  {
+    "name": "BatchNormalization",
+    "schema": {
+      "attributes": [
+        { "name": "axis", "default": -1 },
+        { "name": "epsilon", "default": 1e-3 },
+        { "name": "momentum", "default": 0.99 },
+        { "name": "scale", "default": true },
+        { "name": "center", "default": true },
+        { "name": "gamma_initializer", "default": { "class_name": "Ones", "config": {} } },
+        { "name": "moving_mean_initializer", "default": { "class_name": "Zeros", "config": {} } },
+        { "name": "moving_variance_initializer", "default": { "class_name": "Ones", "config": {} } },
+        { "name": "beta_initializer", "default": { "class_name": "Zeros", "config": {} } }
+      ]
+    }
+  },
+  {
+    "name": "Dense",
+    "schema": {
+      "attributes": [
+        { "name": "activation", "default": "linear" },
+        { "name": "use_bias", "default": true },
+        { "name": "bias_initializer", "default": { "class_name": "Zeros", "config": {} } },
+        { "name": "kernel_initializer", "default": { "class_name": "VarianceScaling", "config": {"distribution": "uniform", "scale": 1, "seed": null, "mode": "fan_avg" } } }
+      ]
+    }
+  },
+  {
+    "name": "Conv2D",
+    "schema": {
+      "attributes": [
+        { "name": "activation", "default": "linear" },
+        { "name": "padding", "default": "valid" },
+        { "name": "use_bias", "default": true },
+        { "name": "data_format", "default": "channels_last" },
+        { "name": "strides", "default": [1, 1] },
+        { "name": "dilation_rate", "default": [1, 1] },
+        { "name": "bias_initializer", "default": { "class_name": "Zeros", "config": {} } },
+        { "name": "kernel_initializer", "default": { "class_name": "VarianceScaling", "config": {"distribution": "uniform", "scale": 1, "seed": null, "mode": "fan_avg" } } }
+      ]
+    }
+  },
+  {
+    "name": "DepthwiseConv2D",
+    "schema": {
+      "attributes": [
+        { "name": "activation", "default": "linear" },
+        { "name": "padding", "default": "valid" },
+        { "name": "use_bias", "default": true },
+        { "name": "data_format", "default": "channels_last" },
+        { "name": "strides", "default": [1, 1] },
+        { "name": "dilation_rate", "default": [1, 1] },
+        { "name": "bias_initializer", "default": { "class_name": "Zeros", "config": {} } },
+        { "name": "depthwise_initializer", "default": { "class_name": "VarianceScaling", "config": {"distribution": "uniform", "scale": 1, "seed": null, "mode": "fan_avg" } } }
+      ]
+    }
+  }
+]

+ 7 - 7
src/view-onnx.js → src/onnx-model.js

@@ -592,7 +592,7 @@ class OnnxOperatorMetadata {
     }
 
     constructor(data) {
-        this.map = {};
+        this._map = {};
         var items = JSON.parse(data);
         if (items) {
             items.forEach((item) => {
@@ -600,7 +600,7 @@ class OnnxOperatorMetadata {
                 {
                     var name = item.name;
                     var schema = item.schema;
-                    this.map[name] = schema;
+                    this._map[name] = schema;
                 }
             });
         }
@@ -609,7 +609,7 @@ class OnnxOperatorMetadata {
     getInputs(node) {
         var inputs = [];
         var index = 0;
-        var schema = this.map[node.opType];
+        var schema = this._map[node.opType];
         if (schema && schema.inputs) {
             schema.inputs.forEach((inputDef) => {
                 if (index < node.input.length || inputDef.option != 'optional') {
@@ -644,7 +644,7 @@ class OnnxOperatorMetadata {
     getOutputs(node) {
         var outputs = [];
         var index = 0;
-        var schema = this.map[node.opType];
+        var schema = this._map[node.opType];
         if (schema && schema.inputs) {
             schema.outputs.forEach((outputDef) => {
                 if (index < node.output.length || outputDef.option != 'optional') {
@@ -673,7 +673,7 @@ class OnnxOperatorMetadata {
     }
 
     getAttributeType(operator, name) {
-        var schema = this.map[operator];
+        var schema = this._map[operator];
         if (schema) {
             var attributeMap = schema.attributeMap;
             if (!attributeMap) {
@@ -694,7 +694,7 @@ class OnnxOperatorMetadata {
     }
 
     getOperatorCategory(operator) {
-        var schema = this.map[operator];
+        var schema = this._map[operator];
         if (schema && schema.category) {
             return schema.category;
         }
@@ -702,7 +702,7 @@ class OnnxOperatorMetadata {
     }
 
     getOperatorDocumentation(operator) {
-        var schema = this.map[operator];
+        var schema = this._map[operator];
         if (schema) {
             schema = Object.assign({}, schema);
             schema.name = operator;

+ 13 - 15
src/view-tf.js → src/tf-model.js

@@ -736,20 +736,6 @@ class TensorFlowTensor {
 class TensorFlowOperatorMetadata {
 
     static open(host, callback) {
-        if (!TensorFlowOperatorMetadata.categoryMap) {
-            TensorFlowOperatorMetadata.categoryMap = {
-                'Const': 'Constant',
-                'Conv2D': 'Layer',
-                'Relu': 'Activation',
-                'LRN': 'Normalization',
-                'MaxPool': 'Pool',
-                'Identity': 'Control',
-                // 'VariableV2':
-                // 'Assign':
-                // 'BiasAdd':
-            };
-        }
-
         if (TensorFlowOperatorMetadata.operatorMetadata) {
             callback(null, TensorFlowOperatorMetadata.operatorMetadata);
         }
@@ -787,6 +773,18 @@ class TensorFlowGraphOperatorMetadata {
                 this._map[opDef.name] = opDef;
             });
         }
+
+        this._categoryMap = {
+            'Const': 'Constant',
+            'Conv2D': 'Layer',
+            'Relu': 'Activation',
+            'LRN': 'Normalization',
+            'MaxPool': 'Pool',
+            'Identity': 'Control',
+            // 'VariableV2':
+            // 'Assign':
+            // 'BiasAdd':
+        };
     }
 
     getOpDef(operator) {
@@ -946,7 +944,7 @@ class TensorFlowGraphOperatorMetadata {
     }
 
     getOperatorCategory(operator) {
-        var category = TensorFlowOperatorMetadata.categoryMap[operator];
+        var category = this._categoryMap[operator];
         if (category) {
             return category;
         }

+ 22 - 26
src/view-tflite.js → src/tflite-model.js

@@ -555,24 +555,6 @@ class TensorFlowLiteTensor {
 class TensorFlowLiteOperatorMetadata {
 
     static open(host, callback) {
-        if (!TensorFlowLiteOperatorMetadata.categoryMap) {
-            TensorFlowLiteOperatorMetadata.categoryMap = {
-                'Conv2D': 'Layer',
-                'DepthwiseConv2D': 'Layer',
-                'Softmax': 'Activation',
-                'Reshape': 'Shape',
-                'Normalize': 'Normalization',
-                'AveragePool2D': 'Pool',
-                'MaxPool2D': 'Pool',
-                'Concatenation': 'Tensor',            
-                // 'LSHProjection': 
-                // 'Predict': 
-                // 'HashtableLookup':
-                // 'ExtractFeatures': 
-                // 'SkipGram':
-            };
-        }
-
         if (TensorFlowLiteOperatorMetadata.operatorMetadata) {
             callback(null, TensorFlowLiteOperatorMetadata.operatorMetadata);
         }
@@ -587,22 +569,36 @@ class TensorFlowLiteOperatorMetadata {
     }
 
     constructor(data) {
-        this.map = {};
+        this._map = {};
         var items = JSON.parse(data);
         if (items) {
             items.forEach((item) => {
                 if (item.name && item.schema)
                 {
-                    var name = item.name;
-                    var schema = item.schema;
-                    this.map[name] = schema;
+                    this._map[item.name] = item.schema;
                 }
             });
         }
+
+        this._categoryMap = {
+            'Conv2D': 'Layer',
+            'DepthwiseConv2D': 'Layer',
+            'Softmax': 'Activation',
+            'Reshape': 'Shape',
+            'Normalize': 'Normalization',
+            'AveragePool2D': 'Pool',
+            'MaxPool2D': 'Pool',
+            'Concatenation': 'Tensor',            
+            // 'LSHProjection': 
+            // 'Predict': 
+            // 'HashtableLookup':
+            // 'ExtractFeatures': 
+            // 'SkipGram':
+        };
     }
 
     getInputName(operator, index) {
-        var schema = this.map[operator];
+        var schema = this._map[operator];
         if (schema) {
             var inputs = schema.inputs;
             if (inputs && index < inputs.length) {
@@ -621,7 +617,7 @@ class TensorFlowLiteOperatorMetadata {
     }
 
     getOutputName(operator, index) {
-        var schema = this.map[operator];
+        var schema = this._map[operator];
         if (schema) {
             var outputs = schema.outputs;
             if (outputs && index < outputs.length) {
@@ -640,7 +636,7 @@ class TensorFlowLiteOperatorMetadata {
     }
 
     getAttributeType(operator, name) {
-        var schema = this.map[operator];
+        var schema = this._map[operator];
         if (schema) {
             var attributeMap = schema.attributeMap;
             if (!attributeMap) {
@@ -661,7 +657,7 @@ class TensorFlowLiteOperatorMetadata {
     }
 
     getOperatorCategory(operator) {
-        var category = TensorFlowLiteOperatorMetadata.categoryMap[operator];
+        var category = this._categoryMap[operator];
         if (category) {
             return category;
         }

+ 4 - 3
src/view-browser.html

@@ -31,13 +31,14 @@
 <script type='text/javascript' src='handlebars.min.js'></script>
 <script type='text/javascript' src='marked.min.js'></script>
 <script type='text/javascript' src='onnx.js'></script>
+<script type='text/javascript' src='onnx-model.js'></script>
 <script type='text/javascript' src='tf.js'></script>
+<script type='text/javascript' src='tf-model.js'></script>
 <script type='text/javascript' src='tflite.js'></script>
+<script type='text/javascript' src='tflite-model.js'></script>
+<script type='text/javascript' src='keras-model.js'></script>
 <script type='text/javascript' src='view-template.js'></script>
 <script type='text/javascript' src='view-browser.js'></script>
-<script type='text/javascript' src='view-onnx.js'></script>
-<script type='text/javascript' src='view-tf.js'></script>
-<script type='text/javascript' src='view-tflite.js'></script>
 <script type='text/javascript' src='view-render.js'></script>
 <script type='text/javascript' src='view.js'></script>
 </body>

+ 4 - 3
src/view-electron.html

@@ -30,13 +30,14 @@
 <script type='text/javascript' src='../node_modules/handlebars/dist/handlebars.min.js'></script>
 <script type='text/javascript' src='../node_modules/marked/marked.min.js'></script>
 <script type='text/javascript' src='onnx.js'></script>
+<script type='text/javascript' src='onnx-model.js'></script>
 <script type='text/javascript' src='tf.js'></script>
+<script type='text/javascript' src='tf-model.js'></script>
 <script type='text/javascript' src='tflite.js'></script>
+<script type='text/javascript' src='tflite-model.js'></script>
+<script type='text/javascript' src='keras-model.js'></script>
 <script type='text/javascript' src='view-template.js'></script>
 <script type='text/javascript' src='view-electron.js'></script>
-<script type='text/javascript' src='view-onnx.js'></script>
-<script type='text/javascript' src='view-tf.js'></script>
-<script type='text/javascript' src='view-tflite.js'></script>
 <script type='text/javascript' src='view-render.js'></script>
 <script type='text/javascript' src='view.js'></script>
 </body>

+ 1 - 1
src/view-render.css

@@ -20,7 +20,7 @@
 .node-item-operator-activation path { fill: #4B1B16; }
 .node-item-operator-pool path { fill: #353; }
 .node-item-operator-normalization path { fill: #354; }
-.node-item-operator-dropout path { fill: #454757; }
+.node-item-operator-dropout path { fill: #454770; }
 .node-item-operator-shape path { fill: #6C4F47; }
 .node-item-operator-tensor path { fill: #59423B; }
 .node-item-operator-control path { fill: #eee; }

+ 8 - 1
src/view.js

@@ -185,7 +185,8 @@ function updateGraph(model) {
             });
         });
 
-        if (node.dependencies.length > 0) {
+        var dependencies = node.dependencies;
+        if (dependencies && dependencies.length > 0) {
             formatter.setControlDependencies();
         }
 
@@ -508,6 +509,12 @@ class ModelService {
                 callback(err, model);
             });
         }
+        else if (extension == 'json' || extension == 'keras' || extension == 'h5') {
+            KerasModel.open(buffer, identifier, hostService, (err, model) => {
+                this._activeModel = model;
+                callback(err, model);
+            });
+        }
         else if (extension == 'pb') {
             OnnxModel.open(buffer, identifier, hostService, (err, model) => {
                 if (!err) {