UNPKG

keras-js

Version:

Run Keras models in the browser, with GPU support using WebGL

341 lines (272 loc) 13.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _Layer = _interopRequireDefault(require("../../Layer")); var _Tensor = _interopRequireDefault(require("../../Tensor")); var _WebGL = require("../../WebGL2"); var tensorUtils = _interopRequireWildcard(require("../../utils/tensorUtils")); var _ndarrayOps = _interopRequireDefault(require("ndarray-ops")); 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 poolingProgramSource = "#version 300 es\nprecision highp float;\nprecision highp isampler2D;\n\nin vec2 outTex;\nuniform sampler2D x;\nuniform isampler2D poolIndexMap;\nuniform int channels;\nuniform int poolSize;\nuniform bool isMaxPooling;\nout vec4 outColor;\n\nvoid main() {\n int out_x = int(float(channels) * outTex.x);\n int out_y = int(float(textureSize(poolIndexMap, 0)[1]) * outTex.y);\n\n float val = 0.;\n int count = 0;\n for (int i = 0; i < poolSize; ++i) {\n int poolIndex = texelFetch(poolIndexMap, ivec2(i, out_y), 0).r;\n if (poolIndex != -1) {\n float val2 = texelFetch(x, ivec2(out_x, poolIndex), 0).r;\n if (isMaxPooling) {\n if (count == 0 || val2 > val) {\n val = val2;\n }\n } else {\n val += val2;\n }\n count += 1;\n }\n }\n\n if (!isMaxPooling) {\n val /= float(count);\n }\n\n outColor = vec4(val);\n}\n"; const poolingFragmentsProgramSource = "#version 300 es\nprecision highp float;\nprecision highp isampler2D;\n\nin vec2 outTex;\nuniform sampler2D x;\nuniform isampler2D poolIndexMap;\nuniform int channels;\nuniform int poolSize;\nuniform bool isMaxPooling;\nout vec4 outColor;\n\nvoid main() {\n ivec2 inputSize = textureSize(x, 0);\n ivec2 outputSize = textureSize(poolIndexMap, 0);\n int out_x = int(float(channels) * outTex.x);\n int out_y = int(float(outputSize[1]) * outTex.y);\n\n float val = 0.;\n int count = 0;\n for (int i = 0; i < poolSize; ++i) {\n int poolIndex = texelFetch(poolIndexMap, ivec2(i, out_y), 0).r;\n int fragmentIndex = int(floor(float(poolIndex) / float(inputSize[1])));\n if (poolIndex != -1) {\n poolIndex = int(mod(float(poolIndex), float(inputSize[1])));\n float val2 = texelFetch(x, ivec2(fragmentIndex * channels + out_x, poolIndex), 0).r;\n if (isMaxPooling) {\n if (count == 0 || val2 > val) {\n val = val2;\n }\n } else {\n val += val2;\n }\n count += 1;\n }\n }\n\n if (!isMaxPooling) {\n val /= float(count);\n }\n\n outColor = vec4(val);\n}\n"; class _Pooling2D extends _Layer.default { constructor(attrs = {}) { super(attrs); this.layerClass = '_Pooling2D'; const { pool_size = [2, 2], strides = null, padding = 'valid', data_format = 'channels_last' } = attrs; if (Array.isArray(pool_size)) { this.poolSize = pool_size; } else { this.poolSize = [pool_size, pool_size]; } if (Array.isArray(strides)) { this.strides = strides; } else if (strides !== null) { this.strides = [strides, strides]; } else { this.strides = this.poolSize; } this.padding = padding; this.dataFormat = data_format; this.poolingFunc = 'max'; this.description = `pool size ${this.poolSize.join('x')}`; this.description += this.strides.some(s => s > 1) ? `, ${this.strides.join('x')} striding` : ''; this.description += this.padding === 'valid' ? `, no border padding` : ', pad to same borders'; if (this.gpu) { this.poolingProgram = _WebGL.webgl2.compileProgram(poolingProgramSource); this.poolingFragmentsProgram = _WebGL.webgl2.compileProgram(poolingFragmentsProgramSource); } } call(x) { if (this.gpu) { this._callGPU(x); } else { this._callCPU(x); } return this.output; } _calcOutputShape(inputShape) { if (this.outputShape && this.inputPadding) { return; } const [inputRows, inputCols, inputChannels] = inputShape; const [nbRow, nbCol] = this.poolSize; const outputRows = this.padding === 'same' ? Math.floor((inputRows + this.strides[0] - 1) / this.strides[0]) : Math.floor((inputRows - nbRow + this.strides[0]) / this.strides[0]); const outputCols = this.padding === 'same' ? Math.floor((inputCols + this.strides[1] - 1) / this.strides[1]) : Math.floor((inputCols - nbCol + this.strides[1]) / this.strides[1]); const paddingRow = this.padding === 'same' ? Math.max(0, Math.floor((outputRows - 1) * this.strides[0] + nbRow - inputRows)) : 0; const paddingCol = this.padding === 'same' ? Math.max(0, Math.floor((outputCols - 1) * this.strides[1] + nbCol - inputCols)) : 0; const paddingRowBefore = Math.floor(paddingRow / 2); const paddingRowAfter = paddingRow - paddingRowBefore; const paddingColBefore = Math.floor(paddingCol / 2); const paddingColAfter = paddingCol - paddingColBefore; this.outputShape = [outputRows, outputCols, inputChannels]; this.inputPadding = [paddingRowBefore, paddingRowAfter, paddingColBefore, paddingColAfter]; } _padInput(x) { if (this.padding === 'same') { const [inputRows, inputCols, inputChannels] = x.tensor.shape; const [paddingRowBefore, paddingRowAfter, paddingColBefore, paddingColAfter] = this.inputPadding; const newRows = inputRows + paddingRowBefore + paddingRowAfter; const newCols = inputCols + paddingColBefore + paddingColAfter; const _x = new _Tensor.default([], [newRows, newCols, inputChannels]); if (this.poolingFunc === 'max') { _ndarrayOps.default.assigns(_x.tensor, Number.NEGATIVE_INFINITY); } _ndarrayOps.default.assign(_x.tensor.hi(inputRows + paddingRowBefore, inputCols + paddingColBefore, inputChannels).lo(paddingRowBefore, paddingColBefore, 0), x.tensor); return _x; } return x; } _callCPU(x) { if (this.dataFormat === 'channels_first') { x.tensor = x.tensor.transpose(1, 2, 0); } this._calcOutputShape(x.tensor.shape); x = this._padInput(x); const [inputRows, inputCols, inputChannels] = x.tensor.shape; const [nbRow, nbCol] = this.poolSize; this.output = new _Tensor.default([], this.outputShape); const patch = new _Tensor.default([], [nbRow, nbCol, inputChannels]); const [paddingRowBefore, paddingRowAfter, paddingColBefore, paddingColAfter] = this.inputPadding; for (let i = 0, _i = 0; i <= inputRows - nbRow; i += this.strides[0], _i++) { let nbRowInPadding = 0; if (i < paddingRowBefore) { nbRowInPadding = paddingRowBefore - i; } else if (i + nbRow > inputRows - paddingRowAfter) { nbRowInPadding = i + nbRow - (inputRows - paddingRowAfter); } for (let j = 0, _j = 0; j <= inputCols - nbCol; j += this.strides[1], _j++) { let nbColInPadding = 0; if (j < paddingColBefore) { nbColInPadding = paddingColBefore - j; } else if (j + nbCol > inputCols - paddingColAfter) { nbColInPadding = j + nbCol - (inputCols - paddingColAfter); } const nbCellsEffective = (nbRow - nbRowInPadding) * (nbCol - nbColInPadding); _ndarrayOps.default.assign(patch.tensor, x.tensor.hi(i + nbRow, j + nbCol, inputChannels).lo(i, j, 0)); for (let c = 0; c < inputChannels; c++) { if (this.poolingFunc === 'max') { this.output.tensor.set(_i, _j, c, _ndarrayOps.default.sup(patch.tensor.pick(null, null, c))); } else if (this.poolingFunc === 'average') { this.output.tensor.set(_i, _j, c, _ndarrayOps.default.sum(patch.tensor.pick(null, null, c)) / nbCellsEffective); } } } } if (this.dataFormat === 'channels_first') { this.output.tensor = this.output.tensor.transpose(2, 0, 1); } } _im2col(x) { const [inputRows, inputCols, inputChannels] = x.tensor.shape; if (!this.tiledInput) { this.tiledInput = new _Tensor.default([], [inputRows * inputCols, inputChannels]); } const patch = new _Tensor.default([], [inputRows, inputCols]); const patchRaveled = new _Tensor.default([], [inputRows * inputCols]); for (let c = 0; c < inputChannels; c++) { _ndarrayOps.default.assign(patch.tensor, x.tensor.pick(null, null, c)); patchRaveled.replaceTensorData(patch.tensor.data); _ndarrayOps.default.assign(this.tiledInput.tensor.pick(null, c), patchRaveled.tensor); } return this.tiledInput; } _createIndexMap() { if (this.poolIndexMap) { return; } let inputRows = this.inputShape[0]; let inputCols = this.inputShape[1]; const rowIndices = new _Tensor.default([], [inputRows, inputCols]); let index = 0; for (let i = 0; i < inputRows; i++) { for (let j = 0; j < inputCols; j++) { rowIndices.tensor.set(i, j, index); index += 1; } } if (this.padding === 'same') { const [paddingRowBefore, paddingRowAfter, paddingColBefore, paddingColAfter] = this.inputPadding; inputRows = inputRows + paddingRowBefore + paddingRowAfter; inputCols = inputCols + paddingColBefore + paddingColAfter; const _rowIndices = new _Tensor.default([], [inputRows, inputCols]); _ndarrayOps.default.assigns(_rowIndices.tensor, -1); _ndarrayOps.default.assign(_rowIndices.tensor.hi(this.inputShape[0] + paddingRowBefore, this.inputShape[1] + paddingColBefore).lo(paddingRowBefore, paddingColBefore), rowIndices.tensor); rowIndices.tensor = _rowIndices.tensor; } const [nbRow, nbCol] = this.poolSize; const outputRows = this.outputShape[0]; const outputCols = this.outputShape[1]; this.poolIndexMap = new _Tensor.default([], [outputRows * outputCols, nbRow * nbCol], { type: Int32Array }); const patchRow = new _Tensor.default([], [nbRow, nbCol]); let offset = 0; for (let i = 0, limit = inputRows - nbRow; i <= limit; i += this.strides[0]) { for (let j = 0, limit = inputCols - nbCol; j <= limit; j += this.strides[1]) { _ndarrayOps.default.assign(patchRow.tensor, rowIndices.tensor.hi(i + nbRow, j + nbCol).lo(i, j)); this.poolIndexMap.tensor.data.set(patchRow.tensor.data, offset); offset += nbRow * nbCol; } } this.poolIndexMap.createGLTexture({ type: '2d', format: 'int', supportsTextureFragments: true }); } _callGPU(x) { if (x.is2DReshaped || x.is2DSquareReshaped) { this.inputShape = x.originalShape; } else { if (this.dataFormat === 'channels_first') { x.tensor = x.tensor.transpose(1, 2, 0); } this.inputShape = x.tensor.shape; this._im2col(x); this.tiledInput.createGLTexture({ type: '2d', format: 'float', supportsTextureFragments: true }); } this._calcOutputShape(this.inputShape); this._createIndexMap(); if (!this.output) { const [outputRows, outputCols, inputChannels] = this.outputShape; const outputTextureShape = [outputRows * outputCols, inputChannels]; this.output = new _Tensor.default([], outputTextureShape); this.output.createGLTexture({ type: '2d', format: 'float', supportsTextureFragments: true }); this.output.is2DReshaped = true; this.output.originalShape = this.outputShape; this.output.indicesForReshaped = tensorUtils.createIndicesFor2DReshaped(this.outputShape, false, -1); } const input = x.is2DReshaped || x.is2DSquareReshaped ? x : this.tiledInput; const poolSize = this.poolSize[0] * this.poolSize[1]; const isMaxPooling = this.poolingFunc === 'max'; const programUniforms = [{ value: this.output.glTextureShape[1], type: 'int', name: 'channels' }, { value: poolSize, type: 'int', name: 'poolSize' }, { value: +isMaxPooling, type: 'bool', name: 'isMaxPooling' }]; if (input.glTextureFragments) { input.convert2DRowFragmentedGLTextureToColStack(); _WebGL.webgl2.runProgram({ program: this.poolingFragmentsProgram, output: this.output, inputs: [{ input: input, name: 'x' }, { input: this.poolIndexMap, name: 'poolIndexMap' }], uniforms: programUniforms, supportsTextureFragments: true }); input.removeGLTextureFragmentsAsColStack(); } else { _WebGL.webgl2.runProgram({ program: this.poolingProgram, output: this.output, inputs: [{ input: input, name: 'x' }, { input: this.poolIndexMap, name: 'poolIndexMap' }], uniforms: programUniforms, supportsTextureFragments: true }); } if (this.outbound.length === 0) { this.output.transferFromGLTexture(); this.output.reshapeFrom2D(); if (this.dataFormat === 'channels_first') { this.output.tensor = this.output.tensor.transpose(2, 0, 1); } } } } exports.default = _Pooling2D;