keras-js
Version:
Run Keras models in the browser, with GPU support using WebGL
512 lines (421 loc) • 16.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _keys2 = _interopRequireDefault(require("lodash/keys"));
var _isEqual2 = _interopRequireDefault(require("lodash/isEqual"));
var _map2 = _interopRequireDefault(require("lodash/map"));
var _every2 = _interopRequireDefault(require("lodash/every"));
var _find2 = _interopRequireDefault(require("lodash/find"));
var _pick2 = _interopRequireDefault(require("lodash/pick"));
var _bluebird = _interopRequireDefault(require("bluebird"));
var _axios = _interopRequireDefault(require("axios"));
var _performanceNow = _interopRequireDefault(require("performance-now"));
var _eventemitter = require("eventemitter3");
var layers = _interopRequireWildcard(require("./layers"));
var visMethods = _interopRequireWildcard(require("./visualizations"));
var _Tensor = _interopRequireDefault(require("./Tensor"));
var _WebGL = require("./WebGL2");
var _proto = _interopRequireDefault(require("./proto"));
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const axiosSource = _axios.default.CancelToken.source();
class Model {
constructor(config = {}) {
const {
filepath = null,
headers = {},
filesystem = false,
gpu = false,
transferLayerOutputs = false,
pauseAfterLayerCalls = false,
visualizations = []
} = config;
if (!filepath) {
throw new Error('[Model] path to protobuf-serialized model definition file is missing.');
}
this.filepath = filepath;
this.headers = headers;
this.filesystem = typeof window !== 'undefined' ? false : filesystem;
this.events = new _eventemitter.EventEmitter();
this.id = null;
this.name = null;
this.kerasVersion = null;
this.backend = null;
this.modelConfig = {};
this.modelWeights = [];
this.gpu = typeof window !== 'undefined' && _WebGL.webgl2.isSupported ? gpu : false;
this.transferLayerOutputs = transferLayerOutputs;
this.pauseAfterLayerCalls = pauseAfterLayerCalls;
this.modelLayersInfo = [];
this.modelLayersMap = new Map();
this.inputTensorsMap = new Map();
this.inputLayerNames = [];
this.outputLayerNames = [];
this.finishedLayerNames = [];
this.isRunning = false;
this.runningProgress = 0;
this.predictStats = {};
this.visMap = new Map();
visualizations.forEach(v => {
if (v in visMethods) {
const visInstance = new visMethods[v]({
modelLayersMap: this.modelLayersMap,
gpu: this.gpu
});
this.visMap.set(v, visInstance);
}
});
this._ready = this._initialize();
}
checkGPUSupport() {
return _WebGL.webgl2.isSupported;
}
ready() {
return this._ready;
}
_interrupt() {
axiosSource.cancel();
}
async _initialize() {
this.events.emit('loadingProgress', 0);
try {
const req = this.filesystem ? this._dataRequestFS() : this._dataRequestHTTP(this.headers);
await req;
} catch (err) {
console.log(err);
this._interrupt();
}
this.events.emit('loadingProgress', 100);
this._buildDAG();
this.inputLayerNames.forEach(name => {
const inputLayer = this.modelLayersMap.get(name);
inputLayer.call(this.inputTensorsMap.get(name));
inputLayer.hasOutput = true;
inputLayer.visited = true;
});
const _pauseAfterLayerCalls = this.pauseAfterLayerCalls;
this.pauseAfterLayerCalls = true;
this.runningProgress = 0;
this.events.emit('initProgress', 0);
await this._traverseDAG(this.inputLayerNames, 'initProgress');
this.pauseAfterLayerCalls = _pauseAfterLayerCalls;
this.finishedLayerNames = [];
this.modelLayersMap.forEach(layer => {
layer.hasOutput = false;
layer.visited = false;
});
this.visMap.forEach(visInstance => {
visInstance.initialize();
});
this.events.emit('initProgress', 100);
return true;
}
async _dataRequestHTTP(headers = {}) {
try {
const res = await _axios.default.get(this.filepath, {
responseType: 'arraybuffer',
headers,
onDownloadProgress: e => {
if (e.lengthComputable) {
const percentComplete = Math.round(100 * e.loaded / e.total);
this.events.emit('loadingProgress', percentComplete);
}
},
cancelToken: axiosSource.token
});
this._decodeProtobuf(new Uint8Array(res.data));
} catch (err) {
if (_axios.default.isCancel(err)) {
console.log('[Model] Data request canceled', err.message);
} else {
throw err;
}
}
}
async _dataRequestFS() {
const readFile = _bluebird.default.promisify(require('fs').readFile);
try {
const file = await readFile(this.filepath);
this._decodeProtobuf(file);
} catch (err) {
throw err;
}
}
_decodeProtobuf(buffer) {
const err = _proto.default.Model.verify(buffer);
if (err) {
throw new Error(`[Model] Invalid model - check protobuf serialization: {err}`);
}
const model = _proto.default.Model.decode(buffer);
this.id = model.id;
this.name = model.name;
this.kerasVersion = model.kerasVersion;
this.backend = model.backend;
this.modelConfig = JSON.parse(model.modelConfig);
this.modelWeights = model.modelWeights;
}
toggleGPU(mode) {
if (typeof mode === 'undefined') {
this.gpu = !this.gpu;
} else {
this.gpu = mode;
}
this.modelLayersMap.forEach(layer => {
layer.toggleGPU(this.gpu);
});
this.visMap.forEach(visInstance => {
visInstance.gpu = this.gpu;
});
this.resetInputTensors();
}
resetInputTensors() {
this.inputLayerNames.forEach(name => {
const inputLayer = this.modelLayersMap.get(name);
this.inputTensorsMap.set(name, new _Tensor.default([], inputLayer.shape));
});
}
_buildDAG() {
const modelClass = this.modelConfig.class_name;
let modelConfig = [];
if (modelClass === 'Sequential') {
modelConfig = this.modelConfig.config;
} else if (modelClass === 'Model') {
modelConfig = this.modelConfig.config.layers;
}
if (!(Array.isArray(modelConfig) && modelConfig.length)) {
throw new Error('[Model] Model configuration does not contain any layers.');
}
modelConfig.forEach((layerDef, index) => {
const layerClass = layerDef.class_name;
const layerConfig = layerDef.config;
if (modelClass === 'Model' && layerClass === 'Sequential') {
layerConfig.forEach((branchLayerDef, branchIndex) => {
const branchLayerClass = branchLayerDef.class_name;
const branchLayerConfig = branchLayerDef.config;
const branchInboundLayerNames = branchIndex === 0 ? layerDef.inbound_nodes[0].map(node => node[0]) : [layerConfig[branchIndex - 1].config.name];
this._createLayer(branchLayerClass, branchLayerConfig, branchInboundLayerNames);
});
} else if (!(layerClass in layers)) {
throw new Error(`[Model] Layer ${layerClass} specified in model configuration is not implemented!`);
} else {
if (modelClass === 'Sequential' && index === 0) {
const inputName = 'input';
const inputShape = layerConfig.batch_input_shape.slice(1);
const layer = new layers.InputLayer({
name: inputName,
shape: inputShape,
gpu: this.gpu
});
this.modelLayersMap.set(inputName, layer);
this.inputTensorsMap.set(inputName, new _Tensor.default([], inputShape));
this.inputLayerNames.push(inputName);
} else if (modelClass === 'Model' && layerClass === 'InputLayer') {
const inputShape = layerConfig.batch_input_shape.slice(1);
this.inputTensorsMap.set(layerConfig.name, new _Tensor.default([], inputShape));
this.inputLayerNames.push(layerConfig.name);
}
let inboundLayerNames = [];
if (modelClass === 'Sequential') {
if (index === 0) {
inboundLayerNames = ['input'];
} else {
inboundLayerNames = [modelConfig[index - 1].config.name];
}
} else if (modelClass === 'Model') {
const inboundNodes = layerDef.inbound_nodes;
if (inboundNodes && inboundNodes.length) {
inboundLayerNames = inboundNodes[0].map(node => node[0]);
}
}
this._createLayer(layerClass, layerConfig, inboundLayerNames);
}
});
this.modelLayersMap.forEach(layer => {
this.modelLayersInfo.push((0, _pick2.default)(layer, ['name', 'description', 'layerClass', 'inbound', 'outbound']));
if (layer.outbound.length === 0) {
this.outputLayerNames.push(layer.name);
}
});
this.inputLayerNames.sort();
this.outputLayerNames.sort();
}
_createLayer(layerClass, layerConfig, inboundLayerNames) {
const layer = new layers[layerClass](Object.assign({}, layerConfig, {
gpu: this.gpu
}));
let weightNames = [];
if (layerClass === 'Bidirectional') {
const forwardWeightNames = layer.forwardLayer.params.map(param => `${layerConfig.name}/forward_${layerConfig.layer.config.name}/${param}`);
const backwardWeightNames = layer.backwardLayer.params.map(param => `${layerConfig.name}/backward_${layerConfig.layer.config.name}/${param}`);
weightNames = forwardWeightNames.concat(backwardWeightNames);
} else if (layerClass === 'TimeDistributed') {
weightNames = layer.layer.params.map(param => `${layerConfig.name}/${param}`);
} else {
weightNames = layer.params.map(param => `${layerConfig.name}/${param}`);
}
if (weightNames && weightNames.length) {
const weights = weightNames.map(weightName => {
const weightDef = (0, _find2.default)(this.modelWeights, w => {
const weightRE = new RegExp(`^.*${weightName}`);
return weightRE.test(w.weightName);
});
if (!weightDef) {
throw new Error(`[Model] error loading weights.`);
}
const {
data,
shape,
type
} = weightDef;
const buf = new ArrayBuffer(data.byteLength);
const arr = new Uint8Array(buf);
arr.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
if (type === 'uint8') {
const {
quantizeMin,
quantizeMax
} = weightDef;
const unquantized = new Float32Array(arr);
for (let i = 0, len = unquantized.length; i < len; i++) {
unquantized[i] *= (quantizeMax - quantizeMin) / 255;
unquantized[i] += quantizeMin;
}
return new _Tensor.default(unquantized, shape);
} else {
return new _Tensor.default(new Float32Array(buf), shape);
}
});
layer.setWeights(weights);
}
this.modelLayersMap.set(layerConfig.name, layer);
inboundLayerNames.forEach(layerName => {
this.modelLayersMap.get(layerConfig.name).inbound.push(layerName);
this.modelLayersMap.get(layerName).outbound.push(layerConfig.name);
});
}
async _traverseDAG(nodes, eventName) {
if (nodes.length === 0) {
this.runningProgress = 100;
this.events.emit(eventName, 100);
return true;
} else if (nodes.length === 1) {
const node = nodes[0];
const currentLayer = this.modelLayersMap.get(node);
if (currentLayer.layerClass === 'InputLayer') {
this.finishedLayerNames.push(this.modelLayersMap.get(node).name);
} else {
const currentLayer = this.modelLayersMap.get(node);
if (currentLayer.visited) {
return false;
}
const inboundLayers = currentLayer.inbound.map(n => this.modelLayersMap.get(n));
if (!(0, _every2.default)((0, _map2.default)(inboundLayers, 'hasOutput'))) {
return false;
}
if (currentLayer.isMergeLayer) {
currentLayer.call((0, _map2.default)(inboundLayers, 'output'));
} else {
currentLayer.call(inboundLayers[0].output);
}
currentLayer.hasOutput = true;
currentLayer.visited = true;
this.finishedLayerNames.push(currentLayer.name);
if (this.pauseAfterLayerCalls) {
await _bluebird.default.delay(0);
}
}
this.runningProgress += 100 / this.modelLayersMap.size;
this.events.emit(eventName, this.runningProgress);
await this._traverseDAG(currentLayer.outbound, eventName);
} else {
await _bluebird.default.all(nodes.map(node => this._traverseDAG([node], eventName)));
}
}
_maybeTransferIntermediateOutputs() {
if (this.gpu && this.transferLayerOutputs) {
this.modelLayersMap.forEach(layer => {
if (layer.output && layer.output.glTexture) {
_WebGL.webgl2.bindOutputTexture(layer.output.glTexture, layer.output.glTextureShape);
layer.output.transferFromGLTexture();
if (layer.output.is2DReshaped) {
layer.output.reshapeFrom2D();
}
}
});
}
}
loadData(inputData) {
this.inputLayerNames.forEach(name => {
const inputLayer = this.modelLayersMap.get(name);
this.inputTensorsMap.get(name).replaceTensorData(inputData[name]);
inputLayer.call(this.inputTensorsMap.get(name));
inputLayer.hasOutput = true;
inputLayer.visited = true;
});
}
async predict(inputData) {
this.isRunning = true;
this.runningProgress = 0;
this.events.emit('predictProgress', 0);
if (!(0, _isEqual2.default)((0, _keys2.default)(inputData).sort(), this.inputLayerNames)) {
this.isRunning = false;
throw new Error('[Model] predict() must take an object where the keys are the named inputs of the model: ' + JSON.stringify(this.inputLayerNames));
}
if (!(0, _every2.default)(this.inputLayerNames, name => inputData[name] instanceof Float32Array)) {
this.isRunning = false;
throw new Error('[Model] predict() must take an object where the values are the flattened data as Float32Array.');
}
this.finishedLayerNames = [];
this.modelLayersMap.forEach(layer => {
layer.hasOutput = false;
layer.visited = false;
});
let start = (0, _performanceNow.default)();
this.loadData(inputData);
this.predictStats.loadData = (0, _performanceNow.default)() - start;
start = (0, _performanceNow.default)();
await this._traverseDAG(this.inputLayerNames, 'predictProgress');
this.predictStats.forwardPass = (0, _performanceNow.default)() - start;
this._maybeTransferIntermediateOutputs();
const modelClass = this.modelConfig.class_name;
const outputData = {};
if (modelClass === 'Sequential') {
const outputLayer = this.modelLayersMap.get(this.outputLayerNames[0]);
outputData['output'] = outputLayer.output.tensor.data;
} else if (modelClass === 'Model') {
this.outputLayerNames.forEach(layerName => {
const outputLayer = this.modelLayersMap.get(layerName);
outputData[layerName] = outputLayer.output.tensor.data;
});
}
start = (0, _performanceNow.default)();
this.visMap.forEach(visInstance => {
visInstance.update();
});
this.predictStats.visualizations = (0, _performanceNow.default)() - start;
this.isRunning = false;
this.events.emit('predictProgress', 100);
return outputData;
}
layerCall(layerName, input) {
if (!this.modelLayersMap.has(layerName)) return;
let x;
if (input instanceof _Tensor.default) {
x = input;
} else {
x = new _Tensor.default(input.data, input.shape);
}
const layer = this.modelLayersMap.get(layerName);
return layer.call(x);
}
cleanup() {
_WebGL.webgl2.clearRefs();
this.modelLayersMap.clear();
this.inputTensorsMap.clear();
this.visMap.clear();
delete this.modelWeights;
}
}
exports.default = Model;