in tfjs-layers/src/engine/container.ts [82:472]
constructor(args: ContainerArgs) {
// No args passed to super's constructor.
super({});
this.name = args.name;
if (this.name == null) {
const prefix = this.getClassName().toLowerCase();
this.name = getUid(prefix);
}
this.supportsMasking = false;
this.trainable_ = true;
// TODO(michaelterry): Initialize perInputLosses/Updates here.
// Container-specific properties.
if (Array.isArray(args.inputs)) {
this.inputs = args.inputs.slice();
} else {
this.inputs = [args.inputs];
}
if (Array.isArray(args.outputs)) {
this.outputs = args.outputs.slice();
} else {
this.outputs = [args.outputs];
}
// Check for redundancy in inputs.
if (generic_utils.unique(this.inputs).length !== this.inputs.length) {
throw new ValueError(
'The list of inputs passed to the model is ' +
'redundant. All inputs should only appear once. Found: ' +
`${this.inputs.map(x => x.name)}`);
}
// Check for redundancy in outputs.
if (generic_utils.unique(this.outputs).length !== this.outputs.length) {
console.warn(
'The list of outputs passed to the model is redundant. ' +
'All outputs should only appear once. Found: ' +
`${this.outputs.map(x => x.name)}`);
}
/*
List of initial layers (1 to 1 mapping with this.inputs, hence the same
layer might appear twice)
*/
this.inputLayers = [];
this.inputLayersNodeIndices = [];
this.inputLayersTensorIndices = [];
/*
List of layers (1 to 1 mapping with this.outputs, hence the same layer
might appear twice)
*/
this.outputLayers = [];
this.outputLayersNodeIndices = [];
this.outputLayersTensorIndices = [];
/*
All layers in order of horizontal graph traversal. Entries are unique.
Includes input and output layers.
*/
this.layers = [];
/*
References to container layers that were constructed internally. We need
these to properly dispose of tensors from nested containers.
*/
this.internalContainerRefs = [];
// TODO(michaelterry): Determine if caching still needed with eager
// backend.
/*
This is for performance optimization when calling the Container on new
inputs. Every time the Container is called on a set on input tensors,
we compute the output tensors, output masks and output shapes in one pass,
then cache them here. When one of these outputs is queried later,
we retrieve it from there instead of recomputing it.
*/
// this.outputTensorCache = {};
// this.outputShapeCache = {};
// Build this.outputLayers:
for (const x of this.outputs) {
const layer = x.sourceLayer;
const nodeIndex = x.nodeIndex;
const tensorIndex = x.tensorIndex;
this.outputLayers.push(layer);
this.outputLayersNodeIndices.push(nodeIndex);
this.outputLayersTensorIndices.push(tensorIndex);
}
// TODO(michaelterry): Add output mask cache code.
// Build this.inputLayers:
for (const x of this.inputs) {
const layer = x.sourceLayer;
const nodeIndex = x.nodeIndex;
const tensorIndex = x.tensorIndex;
/*
It's supposed to be an input layer, so only one node
and one tensor output.
*/
generic_utils.assert(nodeIndex === 0, 'input layer has >1 nodes');
generic_utils.assert(tensorIndex === 0, 'input layer has >1 tensors');
this.inputLayers.push(layer);
this.inputLayersNodeIndices.push(nodeIndex);
this.inputLayersTensorIndices.push(tensorIndex);
}
// Build this.inputNames and this.outputNames.
this.inputNames = [];
this.outputNames = [];
this.feedInputShapes = [];
this.feedInputNames = [];
this.feedOutputNames = [];
for (let i = 0; i < this.inputLayers.length; i++) {
const layer = this.inputLayers[i];
// Check that layer is an InputLayer.
if (!(layer instanceof InputLayer)) {
throw new TypeError(
'Input layers to a LayersModel must be InputLayer objects. ' +
`Received inputs: ${args.inputs}. ` +
`Input ${i} (0-based) originates ` +
`from layer type ${layer.getClassName()}.`);
}
this.inputNames.push(layer.name);
this.feedInputShapes.push(layer.batchInputShape);
this.feedInputNames.push(layer.name);
}
for (const layer of this.outputLayers) {
this.outputNames.push(layer.name);
}
this.internalInputShapes = this.inputs.map(x => x.shape);
this.internalOutputShapes = this.outputs.map(x => x.shape);
/*
Container_nodes: set of nodes included in the graph (not all nodes
included in the layers are relevant to the current graph).
*/
// ids of all nodes relevant to the Container:
const nodesDepths: {[nodeID: string]: number} = {};
// To recover nodes from their ID.
const nodeIDToNode: {[nodeID: string]: Node} = {};
const layersDepths: {[layerID: string]: number} = {};
// To layers from their ID.
const layerIDToLayer: {[layerID: string]: Layer} = {};
const layerIndices: {[layerID: string]: number} = {};
const nodesInDecreasingDepth: Node[] = [];
/**
* Builds a map of the graph of layers.
*
* This recursively updates the map `layerIndices`,
* the list `nodesInDecreasingDepth` and the set `containerNodes`.
*
* @param tensor Some tensor in a graph.
* @param finishedNodes Set of nodes whose subgraphs have been traversed
* completely. Useful to prevent duplicated work.
* @param nodesInProgress Set of nodes that are currently active on the
* recursion stack. Useful to detect cycles.
* @param layer Layer from which `tensor` comes from. If not provided,
* will be obtained from tensor.sourceLayer.
* @param nodeIndex Node index from which `tensor` comes from.
* @param tensorIndex TensorIndex from which `tensor` comes from.
*
* @exception RuntimeError if a cycle is detected.
*/
const buildMapOfGraph =
(tensor: SymbolicTensor, finishedNodes: Node[], nodesInProgress: Node[],
layer?: Layer, nodeIndex?: number, tensorIndex?: number) => {
if (layer == null || nodeIndex == null || tensorIndex == null) {
layer = tensor.sourceLayer;
nodeIndex = tensor.nodeIndex;
tensorIndex = tensor.tensorIndex;
}
const node = layer.inboundNodes[nodeIndex];
// Prevent cycles.
if (nodesInProgress.indexOf(node) !== -1) {
throw new RuntimeError(
`The tensor ${tensor.name} at layer "${layer.name}" ` +
'is part of a cycle.');
}
// Don't repeat work for shared subgraphs
if (finishedNodes.indexOf(node) !== -1) {
return;
}
// Update containerNodes.
this.containerNodes.add(Container.nodeKey(layer, nodeIndex));
// Store the traversal order for layer sorting.
if (!(layer.id in layerIndices)) {
layerIndices[layer.id] = Object.keys(layerIndices).length;
}
if (nodesInProgress.indexOf(node) === -1) {
nodesInProgress.push(node);
}
// Propagate to all previous tensors connected to this node.
const numInboundLayers = node.inboundLayers.length;
for (let i = 0; i < numInboundLayers; i++) {
const x = node.inputTensors[i];
const layer = node.inboundLayers[i];
const nodeIndex = node.nodeIndices[i];
const tensorIndex = node.tensorIndices[i];
buildMapOfGraph(
x, finishedNodes, nodesInProgress, layer, nodeIndex,
tensorIndex);
}
finishedNodes.push(node);
while (nodesInProgress.indexOf(node) >= 0) {
nodesInProgress.splice(nodesInProgress.indexOf(node), 1);
}
nodesInDecreasingDepth.push(node);
};
const finishedNodes: Node[] = [];
const nodesInProgress: Node[] = [];
for (const x of this.outputs) {
buildMapOfGraph(x, finishedNodes, nodesInProgress);
}
const reversedNodesInDecreasingDepth =
nodesInDecreasingDepth.slice().reverse();
for (const node of reversedNodesInDecreasingDepth) {
nodeIDToNode[node.id] = node;
// If the depth is not set, the node has no outbound nodes (depth 0).
if (!(node.id in nodesDepths)) {
nodesDepths[node.id] = 0;
}
let depth = nodesDepths[node.id];
// Update the depth of the corresponding layer
const previousDepth =
(layersDepths[node.outboundLayer.id] == null ?
0 :
layersDepths[node.outboundLayer.id]);
/*
If we've seen this layer before at a higher depth, we should use that
depth instead of the node depth. This is necessary for shared layers
that have inputs at different depth levels in the graph.
*/
depth = Math.max(depth, previousDepth);
layersDepths[node.outboundLayer.id] = depth;
layerIDToLayer[node.outboundLayer.id] = node.outboundLayer;
nodesDepths[node.id] = depth;
// Update the depth of inbound nodes.
for (let i = 0; i < node.inboundLayers.length; i++) {
const inboundLayer = node.inboundLayers[i];
const nodeIndex = node.nodeIndices[i];
const inboundNode = inboundLayer.inboundNodes[nodeIndex];
const previousDepth =
(nodesDepths[inboundNode.id] == null ? 0 :
nodesDepths[inboundNode.id]);
nodesDepths[inboundNode.id] = Math.max(depth + 1, previousDepth);
nodeIDToNode[inboundNode.id] = inboundNode;
}
}
// Build a dict {depth: list of nodes with this depth}
const nodesByDepth: {[depth: string]: Node[]} = {};
for (const nodeID in nodesDepths) {
const depth = nodesDepths[nodeID];
if (!(depth in nodesByDepth)) {
nodesByDepth[depth] = [];
}
nodesByDepth[depth].push(nodeIDToNode[nodeID]);
}
// Build a dict {depth: list of layers with this depth}
const layersByDepth: {[depth: string]: Layer[]} = {};
for (const layerID in layersDepths) {
const depth = layersDepths[layerID];
if (!(depth in layersByDepth)) {
layersByDepth[depth] = [];
}
layersByDepth[depth].push(layerIDToLayer[layerID]);
}
// Get sorted list of layer depths.
let depthKeys = Object.keys(layersByDepth)
.map(x => parseInt(x, 10))
.sort(generic_utils.reverseNumberCompare);
// Set this.layers and this.layersByDepth.
this.layers = [];
for (const depth of depthKeys) {
const layersForDepth = layersByDepth[depth];
// Container.layers needs to have a deterministic order:
// here we order them by traversal order.
layersForDepth.sort((a, b) => {
const aIndex = layerIndices[a.id];
const bIndex = layerIndices[b.id];
if (aIndex < bIndex) {
return -1;
}
if (aIndex > bIndex) {
return 1;
}
return 0;
});
for (const layer of layersForDepth) {
if (layer instanceof Container) {
this.internalContainerRefs.push(layer);
}
this.layers.push(layer);
}
}
this.layersByDepth = layersByDepth;
// Get sorted list of node depths;
depthKeys = Object.keys(nodesByDepth)
.map(x => parseInt(x, 10))
.sort(generic_utils.reverseNumberCompare);
// Check that all tensors required are computable.
// computable_tensors: all tensors in the graph
// that can be computed from the inputs provided.
const computableTensors = this.inputs.slice();
// To provide a better error msg.
const layersWithCompleteInput: string[] = [];
for (const depth of depthKeys) {
for (const node of nodesByDepth[depth]) {
const layer = node.outboundLayer;
if (layer != null) {
for (const x of node.inputTensors) {
if (computableTensors.indexOf(x) === -1) {
throw new RuntimeError(
`Graph disconnected: cannot obtain value for tensor ${x}` +
` at layer "${layer.name}". ` +
'The following previous layers were accessed without ' +
`issue: ${layersWithCompleteInput}`);
}
}
for (const x of node.outputTensors) {
computableTensors.push(x);
}
layersWithCompleteInput.push(layer.name);
}
}
}
// Set this.containerNodes and this.nodesByDepth.
this.nodesByDepth = nodesByDepth;
// Ensure name unicity, which will be crucial for serialization
// (since serialized nodes refer to layers by their name).
const allNames = this.layers.map(x => x.name);
for (const name of allNames) {
const numOccurrences = allNames.filter(x => x === name).length;
if (numOccurrences !== 1) {
throw new RuntimeError(
`The name "${name}" is used ${numOccurrences} times ` +
'in the model. All layer names should be unique. Layer names: ' +
JSON.stringify(allNames));
}
}
// Layer parameters.
// The new container starts with a single inbound node
// for its inputs, and no outbound nodes.
// Will be appended to by future calls to apply().
this.outboundNodes = [];
// Will be appended to below, and by future calls to apply().
this.inboundNodes = [];
// Create the node linking internal inputs to internal outputs.
// (This call has side effects.)
// tslint:disable-next-line:no-unused-expression
new Node({
outboundLayer: this,
inboundLayers: [],
nodeIndices: [],
tensorIndices: [],
inputTensors: this.inputs,
outputTensors: this.outputs,
inputMasks: this.inputs.map(x => null),
outputMasks: this.outputs.map(x => null),
inputShapes: this.inputs.map(x => x.shape),
outputShapes: this.outputs.map(x => x.shape)
});
this.built = true;
this._refCount = 1; // The ref count of a container always start at 1.
}