@tensorflow/tfjs-core
Version:
Hardware-accelerated JavaScript library for machine intelligence
599 lines (552 loc) • 20.9 kB
text/typescript
/**
* @license
* Copyright 2020 Google LLC. 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.
* =============================================================================
*/
type PadType = 'SAME'|'VALID'|'NUMBER'|'EXPLICIT';
// For NHWC should be in the following form:
// [[0, 0], [pad_top,pad_bottom], [pad_left, pad_right], [0, 0]]
// For NCHW should be in the following form:
// [[0, 0], [0, 0], [pad_top,pad_bottom], [pad_left, pad_right]]
// Reference: https://www.tensorflow.org/api_docs/python/tf/nn/conv2d
export type ExplicitPadding =
[[number, number], [number, number], [number, number], [number, number]];
export type PadInfo = {
top: number,
left: number,
right: number,
bottom: number,
type: PadType
};
export type PadInfo3D = {
top: number,
left: number,
right: number,
bottom: number,
front: number,
back: number,
type: PadType
};
/**
* Information about the forward pass of a convolution/pooling operation.
* It includes input and output shape, strides, filter size and padding
* information.
*/
export type Conv2DInfo = {
batchSize: number,
inHeight: number,
inWidth: number,
inChannels: number,
outHeight: number,
outWidth: number,
outChannels: number,
dataFormat: 'channelsFirst'|'channelsLast',
strideHeight: number,
strideWidth: number,
dilationHeight: number,
dilationWidth: number,
filterHeight: number,
filterWidth: number,
effectiveFilterHeight: number,
effectiveFilterWidth: number,
padInfo: PadInfo,
inShape: [number, number, number, number],
outShape: [number, number, number, number],
filterShape: [number, number, number, number]
};
/**
*
* @param inputShape Input tensor shape is of the following dimensions:
* `[batch, height, width, inChannels]`.
* @param filterShape The filter shape is of the following dimensions:
* `[filterHeight, filterWidth, depth]`.
* @param strides The strides of the sliding window for each dimension of the
* input tensor: `[strideHeight, strideWidth]`.
* If `strides` is a single number,
* then `strideHeight == strideWidth`.
* @param pad The type of padding algorithm.
* - `same` and stride 1: output will be of same size as input,
* regardless of filter size.
* - `valid`: output will be smaller than input if filter is larger
* than 1*1x1.
* - For more info, see this guide:
* [https://www.tensorflow.org/api_guides/python/nn#Convolution](
* https://www.tensorflow.org/api_guides/python/nn#Convolution)
* @param dataFormat The data format of the input and output data.
* Defaults to 'NHWC'.
* @param dilations The dilation rates: `[dilationHeight, dilationWidth]`.
* Defaults to `[1, 1]`. If `dilations` is a single number, then
* `dilationHeight == dilationWidth`.
*/
export function computeDilation2DInfo(
inputShape: [number, number, number, number],
filterShape: [number, number, number], strides: number|[number, number],
pad: 'same'|'valid'|number, dataFormat: 'NHWC' = 'NHWC',
dilations: number|[number, number]) {
// `computerConv2DInfo` require filterShape to be in the dimension of:
// `[filterHeight, filterWidth, depth, outDepth]`, dilation2d doesn't have
// outDepth, it should have the same depth as the input.
// Input shape: [batch, height, width, inChannels]
const inputChannels = inputShape[3];
const $filterShape =
[...filterShape, inputChannels] as [number, number, number, number];
const $dataFormat = convertConv2DDataFormat(dataFormat);
return computeConv2DInfo(
inputShape, $filterShape, strides, dilations, pad,
null /* roundingMode */, null /* depthWise */, $dataFormat);
}
export function computePool2DInfo(
inShape: [number, number, number, number],
filterSize: [number, number]|number, strides: number|[number, number],
dilations: number|[number, number], pad: 'same'|'valid'|number,
roundingMode?: 'floor'|'round'|'ceil',
dataFormat: 'channelsFirst'|'channelsLast' = 'channelsLast'): Conv2DInfo {
const [filterHeight, filterWidth] = parseTupleParam(filterSize);
let filterShape: [number, number, number, number];
if (dataFormat === 'channelsLast') {
filterShape = [filterHeight, filterWidth, inShape[3], inShape[3]];
} else if (dataFormat === 'channelsFirst') {
filterShape = [filterHeight, filterWidth, inShape[1], inShape[1]];
} else {
throw new Error(`Unknown dataFormat ${dataFormat}`);
}
return computeConv2DInfo(
inShape, filterShape, strides, dilations, pad, roundingMode, false,
dataFormat);
}
/**
* Computes the information for a forward pass of a pooling3D operation.
*/
export function computePool3DInfo(
inShape: [number, number, number, number, number],
filterSize: number|[number, number, number],
strides: number|[number, number, number],
dilations: number|[number, number, number], pad: 'same'|'valid'|number,
roundingMode?: 'floor'|'round'|'ceil',
dataFormat: 'NDHWC'|'NCDHW' = 'NDHWC'): Conv3DInfo {
const [filterDepth, filterHeight, filterWidth] = parse3TupleParam(filterSize);
let filterShape: [number, number, number, number, number];
let $dataFormat: 'channelsFirst'|'channelsLast';
if (dataFormat === 'NDHWC') {
$dataFormat = 'channelsLast';
filterShape =
[filterDepth, filterHeight, filterWidth, inShape[4], inShape[4]];
} else if (dataFormat === 'NCDHW') {
$dataFormat = 'channelsFirst';
filterShape =
[filterDepth, filterHeight, filterWidth, inShape[1], inShape[1]];
} else {
throw new Error(`Unknown dataFormat ${dataFormat}`);
}
return computeConv3DInfo(
inShape, filterShape, strides, dilations, pad, false, $dataFormat,
roundingMode);
}
/**
* Computes the information for a forward pass of a convolution/pooling
* operation.
*/
export function computeConv2DInfo(
inShape: [number, number, number, number],
filterShape: [number, number, number, number],
strides: number|[number, number], dilations: number|[number, number],
pad: 'same'|'valid'|number|ExplicitPadding,
roundingMode?: 'floor'|'round'|'ceil', depthwise = false,
dataFormat: 'channelsFirst'|'channelsLast' = 'channelsLast'): Conv2DInfo {
let [batchSize, inHeight, inWidth, inChannels] = [-1, -1, -1, -1];
if (dataFormat === 'channelsLast') {
[batchSize, inHeight, inWidth, inChannels] = inShape;
} else if (dataFormat === 'channelsFirst') {
[batchSize, inChannels, inHeight, inWidth] = inShape;
} else {
throw new Error(`Unknown dataFormat ${dataFormat}`);
}
const [filterHeight, filterWidth, , filterChannels] = filterShape;
const [strideHeight, strideWidth] = parseTupleParam(strides);
const [dilationHeight, dilationWidth] = parseTupleParam(dilations);
const effectiveFilterHeight =
getEffectiveFilterSize(filterHeight, dilationHeight);
const effectiveFilterWidth =
getEffectiveFilterSize(filterWidth, dilationWidth);
const {padInfo, outHeight, outWidth} = getPadAndOutInfo(
pad, inHeight, inWidth, strideHeight, strideWidth, effectiveFilterHeight,
effectiveFilterWidth, roundingMode, dataFormat);
const outChannels = depthwise ? filterChannels * inChannels : filterChannels;
let outShape: [number, number, number, number];
if (dataFormat === 'channelsFirst') {
outShape = [batchSize, outChannels, outHeight, outWidth];
} else if (dataFormat === 'channelsLast') {
outShape = [batchSize, outHeight, outWidth, outChannels];
}
return {
batchSize,
dataFormat,
inHeight,
inWidth,
inChannels,
outHeight,
outWidth,
outChannels,
padInfo,
strideHeight,
strideWidth,
filterHeight,
filterWidth,
effectiveFilterHeight,
effectiveFilterWidth,
dilationHeight,
dilationWidth,
inShape,
outShape,
filterShape
};
}
/**
* Information about the forward pass of a 3D convolution/pooling operation.
* It includes input and output shape, strides, filter size and padding
* information.
*/
export type Conv3DInfo = {
batchSize: number,
inDepth: number,
inHeight: number,
inWidth: number,
inChannels: number,
outDepth: number,
outHeight: number,
outWidth: number,
outChannels: number,
dataFormat: 'channelsFirst'|'channelsLast',
strideDepth: number,
strideHeight: number,
strideWidth: number,
dilationDepth: number,
dilationHeight: number,
dilationWidth: number,
filterDepth: number,
filterHeight: number,
filterWidth: number,
effectiveFilterDepth: number,
effectiveFilterHeight: number,
effectiveFilterWidth: number,
padInfo: PadInfo3D,
inShape: [number, number, number, number, number],
outShape: [number, number, number, number, number],
filterShape: [number, number, number, number, number]
};
/**
* Computes the information for a forward pass of a 3D convolution/pooling
* operation.
*/
export function computeConv3DInfo(
inShape: [number, number, number, number, number],
filterShape: [number, number, number, number, number],
strides: number|[number, number, number],
dilations: number|[number, number, number], pad: 'same'|'valid'|number,
depthwise = false,
dataFormat: 'channelsFirst'|'channelsLast' = 'channelsLast',
roundingMode?: 'floor'|'round'|'ceil'): Conv3DInfo {
let [batchSize, inDepth, inHeight, inWidth, inChannels] =
[-1, -1, -1, -1, -1];
if (dataFormat === 'channelsLast') {
[batchSize, inDepth, inHeight, inWidth, inChannels] = inShape;
} else if (dataFormat === 'channelsFirst') {
[batchSize, inChannels, inDepth, inHeight, inWidth] = inShape;
} else {
throw new Error(`Unknown dataFormat ${dataFormat}`);
}
const [filterDepth, filterHeight, filterWidth, , filterChannels] =
filterShape;
const [strideDepth, strideHeight, strideWidth] = parse3TupleParam(strides);
const [dilationDepth, dilationHeight, dilationWidth] =
parse3TupleParam(dilations);
const effectiveFilterDepth =
getEffectiveFilterSize(filterDepth, dilationDepth);
const effectiveFilterHeight =
getEffectiveFilterSize(filterHeight, dilationHeight);
const effectiveFilterWidth =
getEffectiveFilterSize(filterWidth, dilationWidth);
const {padInfo, outDepth, outHeight, outWidth} = get3DPadAndOutInfo(
pad, inDepth, inHeight, inWidth, strideDepth, strideHeight, strideWidth,
effectiveFilterDepth, effectiveFilterHeight, effectiveFilterWidth,
roundingMode);
const outChannels = depthwise ? filterChannels * inChannels : filterChannels;
let outShape: [number, number, number, number, number];
if (dataFormat === 'channelsFirst') {
outShape = [batchSize, outChannels, outDepth, outHeight, outWidth];
} else if (dataFormat === 'channelsLast') {
outShape = [batchSize, outDepth, outHeight, outWidth, outChannels];
}
return {
batchSize,
dataFormat,
inDepth,
inHeight,
inWidth,
inChannels,
outDepth,
outHeight,
outWidth,
outChannels,
padInfo,
strideDepth,
strideHeight,
strideWidth,
filterDepth,
filterHeight,
filterWidth,
effectiveFilterDepth,
effectiveFilterHeight,
effectiveFilterWidth,
dilationDepth,
dilationHeight,
dilationWidth,
inShape,
outShape,
filterShape
};
}
function computeOutputShape2D(
inShape: [number, number], fieldSize: number, stride: number,
zeroPad?: number, roundingMode?: 'floor'|'round'|'ceil'): [number, number] {
if (zeroPad == null) {
zeroPad = computeDefaultPad(inShape, fieldSize, stride);
}
const inputRows = inShape[0];
const inputCols = inShape[1];
const outputRows =
round((inputRows - fieldSize + 2 * zeroPad) / stride + 1, roundingMode);
const outputCols =
round((inputCols - fieldSize + 2 * zeroPad) / stride + 1, roundingMode);
return [outputRows, outputCols];
}
function computeOutputShape4D(
inShape: [number, number, number, number], fieldSize: number,
outChannels: number, stride: number, zeroPad?: number,
roundingMode?: 'floor'|'round'|'ceil'): [number, number, number, number] {
if (zeroPad == null) {
zeroPad = computeDefaultPad(inShape, fieldSize, stride);
}
const inputDepth = inShape[0];
const inputRows = inShape[1];
const inputCols = inShape[2];
const outputDepths =
round((inputDepth - fieldSize + 2 * zeroPad) / stride + 1, roundingMode);
const outputRows =
round((inputRows - fieldSize + 2 * zeroPad) / stride + 1, roundingMode);
const outputCols =
round((inputCols - fieldSize + 2 * zeroPad) / stride + 1, roundingMode);
return [outputDepths, outputRows, outputCols, outChannels];
}
export function computeDefaultPad(
inputShape: [number, number]|[number, number, number, number],
fieldSize: number, stride: number, dilation = 1): number {
const effectiveFieldSize = getEffectiveFilterSize(fieldSize, dilation);
return Math.floor(
(inputShape[0] * (stride - 1) - stride + effectiveFieldSize) / 2);
}
function parseTupleParam(param: number|number[]): [number, number, number] {
if (typeof param === 'number') {
return [param, param, param];
}
if (param.length === 2) {
return [param[0], param[1], 1];
}
return param as [number, number, number];
}
function parse3TupleParam(param: number|[number, number, number]):
[number, number, number] {
return typeof param === 'number' ? [param, param, param] : param;
}
/* See https://www.tensorflow.org/api_docs/python/tf/nn/atrous_conv2d
* Atrous convolution is equivalent to standard convolution with upsampled
* filters with effective_filter_height =
* filter_height + (filter_height - 1) * (dilation - 1)
* and effective_filter_width =
* filter_width + (filter_width - 1) * (dilation - 1),
* produced by inserting dilation - 1 zeros along consecutive elements across
* the filters' spatial dimensions.
* When there is a dilation, this converts a filter dimension to the
* effective filter dimension, so it can be used in a standard convolution.
*/
function getEffectiveFilterSize(filterSize: number, dilation: number) {
if (dilation <= 1) {
return filterSize;
}
return filterSize + (filterSize - 1) * (dilation - 1);
}
function getPadAndOutInfo(
pad: 'same'|'valid'|number|ExplicitPadding, inHeight: number,
inWidth: number, strideHeight: number, strideWidth: number,
filterHeight: number, filterWidth: number,
roundingMode: 'floor'|'round'|'ceil',
dataFormat: 'channelsFirst'|
'channelsLast'): {padInfo: PadInfo, outHeight: number, outWidth: number} {
let padInfo: PadInfo;
let outHeight: number;
let outWidth: number;
if (typeof pad === 'number') {
const padType = (pad === 0) ? 'VALID' : 'NUMBER';
padInfo = {top: pad, bottom: pad, left: pad, right: pad, type: padType};
const outShape = computeOutputShape2D(
[inHeight, inWidth], filterHeight, strideHeight, pad, roundingMode);
outHeight = outShape[0];
outWidth = outShape[1];
} else if (pad === 'same') {
outHeight = Math.ceil(inHeight / strideHeight);
outWidth = Math.ceil(inWidth / strideWidth);
const padAlongHeight =
Math.max(0, (outHeight - 1) * strideHeight + filterHeight - inHeight);
const padAlongWidth =
Math.max(0, (outWidth - 1) * strideWidth + filterWidth - inWidth);
const top = Math.floor(padAlongHeight / 2);
const bottom = padAlongHeight - top;
const left = Math.floor(padAlongWidth / 2);
const right = padAlongWidth - left;
padInfo = {top, bottom, left, right, type: 'SAME'};
} else if (pad === 'valid') {
padInfo = {top: 0, bottom: 0, left: 0, right: 0, type: 'VALID'};
outHeight = Math.ceil((inHeight - filterHeight + 1) / strideHeight);
outWidth = Math.ceil((inWidth - filterWidth + 1) / strideWidth);
} else if (typeof pad === 'object') {
const top = dataFormat === 'channelsLast' ? pad[1][0] : pad[2][0];
const bottom = dataFormat === 'channelsLast' ? pad[1][1] : pad[2][1];
const left = dataFormat === 'channelsLast' ? pad[2][0] : pad[3][0];
const right = dataFormat === 'channelsLast' ? pad[2][1] : pad[3][1];
const padType = (top === 0 && bottom === 0 && left === 0 && right === 0) ?
'VALID' :
'EXPLICIT';
padInfo = {top, bottom, left, right, type: padType};
outHeight = round(
(inHeight - filterHeight + top + bottom) / strideHeight + 1,
roundingMode);
outWidth = round(
(inWidth - filterWidth + left + right) / strideWidth + 1, roundingMode);
} else {
throw Error(`Unknown padding parameter: ${pad}`);
}
return {padInfo, outHeight, outWidth};
}
function get3DPadAndOutInfo(
pad: 'same'|'valid'|number, inDepth: number, inHeight: number,
inWidth: number, strideDepth: number, strideHeight: number,
strideWidth: number, filterDepth: number, filterHeight: number,
filterWidth: number, roundingMode?: 'floor'|'round'|'ceil'): {
padInfo: PadInfo3D,
outDepth: number,
outHeight: number,
outWidth: number
} {
let padInfo: PadInfo3D;
let outDepth: number;
let outHeight: number;
let outWidth: number;
if (typeof pad === 'number') {
const padType = (pad === 0) ? 'VALID' : 'NUMBER';
padInfo = {
top: pad,
bottom: pad,
left: pad,
right: pad,
front: pad,
back: pad,
type: padType
};
const outShape = computeOutputShape4D(
[inDepth, inHeight, inWidth, 1], filterDepth, 1, strideDepth, pad,
roundingMode);
outDepth = outShape[0];
outHeight = outShape[1];
outWidth = outShape[2];
} else if (pad === 'same') {
outDepth = Math.ceil(inDepth / strideDepth);
outHeight = Math.ceil(inHeight / strideHeight);
outWidth = Math.ceil(inWidth / strideWidth);
const padAlongDepth = (outDepth - 1) * strideDepth + filterDepth - inDepth;
const padAlongHeight =
(outHeight - 1) * strideHeight + filterHeight - inHeight;
const padAlongWidth = (outWidth - 1) * strideWidth + filterWidth - inWidth;
const front = Math.floor(padAlongDepth / 2);
const back = padAlongDepth - front;
const top = Math.floor(padAlongHeight / 2);
const bottom = padAlongHeight - top;
const left = Math.floor(padAlongWidth / 2);
const right = padAlongWidth - left;
padInfo = {top, bottom, left, right, front, back, type: 'SAME'};
} else if (pad === 'valid') {
padInfo = {
top: 0,
bottom: 0,
left: 0,
right: 0,
front: 0,
back: 0,
type: 'VALID'
};
outDepth = Math.ceil((inDepth - filterDepth + 1) / strideDepth);
outHeight = Math.ceil((inHeight - filterHeight + 1) / strideHeight);
outWidth = Math.ceil((inWidth - filterWidth + 1) / strideWidth);
} else {
throw Error(`Unknown padding parameter: ${pad}`);
}
return {padInfo, outDepth, outHeight, outWidth};
}
/**
* Rounds a value depending on the rounding mode
* @param value
* @param roundingMode A string from: 'ceil', 'round', 'floor'. If none is
* provided, it will default to truncate.
*/
function round(value: number, roundingMode?: 'floor'|'round'|'ceil') {
if (!roundingMode) {
return Math.trunc(value);
}
switch (roundingMode) {
case 'round':
// used for Caffe Conv
return Math.round(value);
case 'ceil':
// used for Caffe Pool
return Math.ceil(value);
case 'floor':
return Math.floor(value);
default:
throw new Error(`Unknown roundingMode ${roundingMode}`);
}
}
export function tupleValuesAreOne(param: number|number[]): boolean {
const [dimA, dimB, dimC] = parseTupleParam(param);
return dimA === 1 && dimB === 1 && dimC === 1;
}
export function eitherStridesOrDilationsAreOne(
strides: number|number[], dilations: number|number[]): boolean {
return tupleValuesAreOne(strides) || tupleValuesAreOne(dilations);
}
/**
* Convert Conv2D dataFormat from 'NHWC'|'NCHW' to
* 'channelsLast'|'channelsFirst'
* @param dataFormat in 'NHWC'|'NCHW' mode
* @return dataFormat in 'channelsLast'|'channelsFirst' mode
* @throws unknown dataFormat
*/
export function convertConv2DDataFormat(dataFormat: 'NHWC'|'NCHW'):
'channelsLast'|'channelsFirst' {
if (dataFormat === 'NHWC') {
return 'channelsLast';
} else if (dataFormat === 'NCHW') {
return 'channelsFirst';
} else {
throw new Error(`Unknown dataFormat ${dataFormat}`);
}
}