@tensorflow/tfjs-node
Version:
This repository provides native TensorFlow execution in backend JavaScript applications under the Node.js runtime, accelerated by the TensorFlow C binary under the hood. It provides the same API as [TensorFlow.js](https://js.tensorflow.org/api/latest/).
327 lines (326 loc) • 17 kB
JavaScript
;
/**
* @license
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =============================================================================
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var _this = this;
Object.defineProperty(exports, "__esModule", { value: true });
var tfjs_1 = require("@tensorflow/tfjs");
var fs = require("fs");
var path = require("path");
var util_1 = require("util");
var tfn = require("./index");
// tslint:disable-next-line:no-require-imports
var rimraf = require('rimraf');
// tslint:disable-next-line:no-require-imports
var tmp = require('tmp');
var rimrafPromise = util_1.promisify(rimraf);
describe('tensorboard', function () {
var tmpLogDir;
beforeEach(function () {
tmpLogDir = tmp.dirSync().name;
});
afterEach(function () { return __awaiter(_this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!(tmpLogDir != null)) return [3 /*break*/, 2];
return [4 /*yield*/, rimrafPromise(tmpLogDir)];
case 1:
_a.sent();
_a.label = 2;
case 2: return [2 /*return*/];
}
});
}); });
it('Create summaryFileWriter and write scalar', function () {
var writer = tfn.node.summaryFileWriter(tmpLogDir);
writer.scalar('foo', 42, 0);
writer.flush();
// Currently, we only verify that the file exists and the size
// increases in a sensible way as we write more scalars to it.
// The difficulty is in reading the protobuf contents of the event
// file in JavaScript/TypeScript.
var fileNames = fs.readdirSync(tmpLogDir);
expect(fileNames.length).toEqual(1);
var eventFilePath = path.join(tmpLogDir, fileNames[0]);
var fileSize0 = fs.statSync(eventFilePath).size;
writer.scalar('foo', 43, 1);
writer.flush();
var fileSize1 = fs.statSync(eventFilePath).size;
var incrementPerScalar = fileSize1 - fileSize0;
expect(incrementPerScalar).toBeGreaterThan(0);
writer.scalar('foo', 44, 2);
writer.scalar('foo', 45, 3);
writer.flush();
var fileSize2 = fs.statSync(eventFilePath).size;
expect(fileSize2 - fileSize1).toEqual(2 * incrementPerScalar);
});
it('Writing tf.Scalar works', function () {
var writer = tfn.node.summaryFileWriter(tmpLogDir);
writer.scalar('foo', tfjs_1.scalar(42), 0);
writer.flush();
// Currently, we only verify that the file exists and the size
// increases in a sensible way as we write more scalars to it.
// The difficulty is in reading the protobuf contents of the event
// file in JavaScript/TypeScript.
var fileNames = fs.readdirSync(tmpLogDir);
expect(fileNames.length).toEqual(1);
});
it('No crosstalk between two summary writers', function () {
var logDir1 = path.join(tmpLogDir, '1');
var writer1 = tfn.node.summaryFileWriter(logDir1);
writer1.scalar('foo', 42, 0);
writer1.flush();
var logDir2 = path.join(tmpLogDir, '2');
var writer2 = tfn.node.summaryFileWriter(logDir2);
writer2.scalar('foo', 1.337, 0);
writer2.flush();
// Currently, we only verify that the file exists and the size
// increases in a sensible way as we write more scalars to it.
// The difficulty is in reading the protobuf contents of the event
// file in JavaScript/TypeScript.
var fileNames = fs.readdirSync(logDir1);
expect(fileNames.length).toEqual(1);
var eventFilePath1 = path.join(logDir1, fileNames[0]);
var fileSize1Num0 = fs.statSync(eventFilePath1).size;
fileNames = fs.readdirSync(logDir2);
expect(fileNames.length).toEqual(1);
var eventFilePath2 = path.join(logDir2, fileNames[0]);
var fileSize2Num0 = fs.statSync(eventFilePath2).size;
expect(fileSize2Num0).toBeGreaterThan(0);
writer1.scalar('foo', 43, 1);
writer1.flush();
var fileSize1Num1 = fs.statSync(eventFilePath1).size;
var incrementPerScalar = fileSize1Num1 - fileSize1Num0;
expect(incrementPerScalar).toBeGreaterThan(0);
writer1.scalar('foo', 44, 2);
writer1.scalar('foo', 45, 3);
writer1.flush();
var fileSize1Num2 = fs.statSync(eventFilePath1).size;
expect(fileSize1Num2 - fileSize1Num1).toEqual(2 * incrementPerScalar);
var fileSize2Num1 = fs.statSync(eventFilePath2).size;
expect(fileSize2Num1).toEqual(fileSize2Num0);
writer2.scalar('foo', 1.336, 1);
writer2.scalar('foo', 1.335, 2);
writer2.flush();
var fileSize1Num3 = fs.statSync(eventFilePath1).size;
expect(fileSize1Num3).toEqual(fileSize1Num2);
var fileSize2Num2 = fs.statSync(eventFilePath2).size;
expect(fileSize2Num2 - fileSize2Num1).toEqual(2 * incrementPerScalar);
});
it('Writing into existing directory works', function () {
fs.mkdirSync(tmpLogDir, { recursive: true });
var writer = tfn.node.summaryFileWriter(path.join(tmpLogDir, '22'));
writer.scalar('foo', 42, 0);
writer.flush();
var fileNames = fs.readdirSync(tmpLogDir);
expect(fileNames.length).toEqual(1);
});
it('empty logdir leads to error', function () {
expect(function () { return tfn.node.summaryFileWriter(''); }).toThrowError(/empty string/);
});
});
describe('tensorBoard callback', function () {
var tmpLogDir;
beforeEach(function () {
tmpLogDir = tmp.dirSync().name;
});
afterEach(function () { return __awaiter(_this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!(tmpLogDir != null)) return [3 /*break*/, 2];
return [4 /*yield*/, rimrafPromise(tmpLogDir)];
case 1:
_a.sent();
_a.label = 2;
case 2: return [2 /*return*/];
}
});
}); });
function createModelForTest() {
var model = tfn.sequential();
model.add(tfn.layers.dense({ units: 5, activation: 'relu', inputShape: [10] }));
model.add(tfn.layers.dense({ units: 1 }));
model.compile({ loss: 'meanSquaredError', optimizer: 'sgd', metrics: ['MAE'] });
return model;
}
it('fit(): default epoch updateFreq, with validation', function () { return __awaiter(_this, void 0, void 0, function () {
var model, xs, ys, valXs, valYs, subDirs, trainLogDir, trainFiles, trainFileSize0, valLogDir, valFiles, valFileSize0, history, trainFileSize1, valFileSize1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
model = createModelForTest();
xs = tfn.randomUniform([100, 10]);
ys = tfn.randomUniform([100, 1]);
valXs = tfn.randomUniform([10, 10]);
valYs = tfn.randomUniform([10, 1]);
// Warm-up training.
return [4 /*yield*/, model.fit(xs, ys, {
epochs: 1,
verbose: 0,
validationData: [valXs, valYs],
callbacks: tfn.node.tensorBoard(tmpLogDir)
})];
case 1:
// Warm-up training.
_a.sent();
subDirs = fs.readdirSync(tmpLogDir);
expect(subDirs).toContain('train');
expect(subDirs).toContain('val');
trainLogDir = path.join(tmpLogDir, 'train');
trainFiles = fs.readdirSync(trainLogDir);
trainFileSize0 = fs.statSync(path.join(trainLogDir, trainFiles[0])).size;
expect(trainFileSize0).toBeGreaterThan(0);
valLogDir = path.join(tmpLogDir, 'val');
valFiles = fs.readdirSync(valLogDir);
valFileSize0 = fs.statSync(path.join(valLogDir, valFiles[0])).size;
expect(valFileSize0).toBeGreaterThan(0);
// With updateFreq === epoch, the train and val subset should have generated
// the same amount of logs.
expect(valFileSize0).toEqual(trainFileSize0);
return [4 /*yield*/, model.fit(xs, ys, {
epochs: 3,
verbose: 0,
validationData: [valXs, valYs],
callbacks: tfn.node.tensorBoard(tmpLogDir)
})];
case 2:
history = _a.sent();
expect(history.history.loss.length).toEqual(3);
expect(history.history.val_loss.length).toEqual(3);
expect(history.history.MAE.length).toEqual(3);
expect(history.history.val_MAE.length).toEqual(3);
trainFileSize1 = fs.statSync(path.join(trainLogDir, trainFiles[0])).size;
valFileSize1 = fs.statSync(path.join(valLogDir, valFiles[0])).size;
// We currently only assert that new content has been written to the log
// file.
expect(trainFileSize1).toBeGreaterThan(trainFileSize0);
expect(valFileSize1).toBeGreaterThan(valFileSize0);
// With updateFreq === epoch, the train and val subset should have generated
// the same amount of logs.
expect(valFileSize1).toEqual(trainFileSize1);
return [2 /*return*/];
}
});
}); });
it('fit(): batch updateFreq, with validation', function () { return __awaiter(_this, void 0, void 0, function () {
var model, xs, ys, valXs, valYs, subDirs, trainLogDir, trainFiles, trainFileSize0, valLogDir, valFiles, valFileSize0, history, trainFileSize1, valFileSize1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
model = createModelForTest();
xs = tfn.randomUniform([100, 10]);
ys = tfn.randomUniform([100, 1]);
valXs = tfn.randomUniform([10, 10]);
valYs = tfn.randomUniform([10, 1]);
// Warm-up training.
return [4 /*yield*/, model.fit(xs, ys, {
epochs: 1,
verbose: 0,
validationData: [valXs, valYs],
// Use batch updateFreq here.
callbacks: tfn.node.tensorBoard(tmpLogDir, { updateFreq: 'batch' })
})];
case 1:
// Warm-up training.
_a.sent();
subDirs = fs.readdirSync(tmpLogDir);
expect(subDirs).toContain('train');
expect(subDirs).toContain('val');
trainLogDir = path.join(tmpLogDir, 'train');
trainFiles = fs.readdirSync(trainLogDir);
trainFileSize0 = fs.statSync(path.join(trainLogDir, trainFiles[0])).size;
expect(trainFileSize0).toBeGreaterThan(0);
valLogDir = path.join(tmpLogDir, 'val');
valFiles = fs.readdirSync(valLogDir);
valFileSize0 = fs.statSync(path.join(valLogDir, valFiles[0])).size;
expect(valFileSize0).toBeGreaterThan(0);
// The train subset should have generated more logs than the val subset,
// because the train subset gets logged every batch, while the val subset
// gets logged every epoch.
expect(trainFileSize0).toBeGreaterThan(valFileSize0);
return [4 /*yield*/, model.fit(xs, ys, {
epochs: 3,
verbose: 0,
validationData: [valXs, valYs],
callbacks: tfn.node.tensorBoard(tmpLogDir)
})];
case 2:
history = _a.sent();
expect(history.history.loss.length).toEqual(3);
expect(history.history.val_loss.length).toEqual(3);
expect(history.history.MAE.length).toEqual(3);
expect(history.history.val_MAE.length).toEqual(3);
trainFileSize1 = fs.statSync(path.join(trainLogDir, trainFiles[0])).size;
valFileSize1 = fs.statSync(path.join(valLogDir, valFiles[0])).size;
// We currently only assert that new content has been written to the log
// file.
expect(trainFileSize1).toBeGreaterThan(trainFileSize0);
expect(valFileSize1).toBeGreaterThan(valFileSize0);
// The train subset should have generated more logs than the val subset,
// because the train subset gets logged every batch, while the val subset
// gets logged every epoch.
expect(trainFileSize1).toBeGreaterThan(valFileSize1);
return [2 /*return*/];
}
});
}); });
it('Invalid updateFreq value causes error', function () { return __awaiter(_this, void 0, void 0, function () {
return __generator(this, function (_a) {
expect(function () { return tfn.node.tensorBoard(tmpLogDir, {
// tslint:disable-next-line:no-any
updateFreq: 'foo'
}); }).toThrowError(/Expected updateFreq/);
return [2 /*return*/];
});
}); });
});