@tensorflow/tfjs-core
Version:
Hardware-accelerated JavaScript library for machine intelligence
548 lines (498 loc) • 17.2 kB
text/typescript
/**
* @license
* Copyright 2017 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.
* =============================================================================
*/
import {getGlobal} from './global_util';
import {tensorToString} from './tensor_format';
import {ArrayMap, BackendValues, DataType, DataTypeMap, DataValues, NumericDataType, Rank, ShapeMap, SingleValueMap, TypedArray} from './types';
import * as util from './util';
import {computeStrides, toNestedArray} from './util';
export interface TensorData<D extends DataType> {
dataId?: DataId;
values?: DataTypeMap[D];
}
// This interface mimics KernelBackend (in backend.ts), which would create a
// circular dependency if imported.
export interface Backend {}
/**
* A mutable object, similar to `tf.Tensor`, that allows users to set values
* at locations before converting to an immutable `tf.Tensor`.
*
* See `tf.buffer` for creating a tensor buffer.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
export class TensorBuffer<R extends Rank, D extends DataType = 'float32'> {
size: number;
shape: ShapeMap[R];
strides: number[];
values: DataTypeMap[D];
constructor(shape: ShapeMap[R], public dtype: D, values?: DataTypeMap[D]) {
this.shape = shape.slice() as ShapeMap[R];
this.size = util.sizeFromShape(shape);
if (values != null) {
const n = values.length;
util.assert(
n === this.size,
() => `Length of values '${n}' does not match the size ` +
`inferred by the shape '${this.size}'.`);
}
if (dtype === 'complex64') {
throw new Error(
`complex64 dtype TensorBuffers are not supported. Please create ` +
`a TensorBuffer for the real and imaginary parts separately and ` +
`call tf.complex(real, imag).`);
}
this.values = values || util.getArrayFromDType(dtype, this.size);
this.strides = computeStrides(shape);
}
/**
* Sets a value in the buffer at a given location.
*
* @param value The value to set.
* @param locs The location indices.
*
* @doc {heading: 'Tensors', subheading: 'Creation'}
*/
set(value: SingleValueMap[D], ...locs: number[]): void {
if (locs.length === 0) {
locs = [0];
}
util.assert(
locs.length === this.rank,
() => `The number of provided coordinates (${locs.length}) must ` +
`match the rank (${this.rank})`);
const index = this.locToIndex(locs);
this.values[index] = value as number;
}
/**
* Returns the value in the buffer at the provided location.
*
* @param locs The location indices.
*
* @doc {heading: 'Tensors', subheading: 'Creation'}
*/
get(...locs: number[]): SingleValueMap[D] {
if (locs.length === 0) {
locs = [0];
}
let i = 0;
for (const loc of locs) {
if (loc < 0 || loc >= this.shape[i]) {
const msg = `Requested out of range element at ${locs}. ` +
` Buffer shape=${this.shape}`;
throw new Error(msg);
}
i++;
}
let index = locs[locs.length - 1];
for (let i = 0; i < locs.length - 1; ++i) {
index += this.strides[i] * locs[i];
}
return this.values[index] as SingleValueMap[D];
}
locToIndex(locs: number[]): number {
if (this.rank === 0) {
return 0;
} else if (this.rank === 1) {
return locs[0];
}
let index = locs[locs.length - 1];
for (let i = 0; i < locs.length - 1; ++i) {
index += this.strides[i] * locs[i];
}
return index;
}
indexToLoc(index: number): number[] {
if (this.rank === 0) {
return [];
} else if (this.rank === 1) {
return [index];
}
const locs: number[] = new Array(this.shape.length);
for (let i = 0; i < locs.length - 1; ++i) {
locs[i] = Math.floor(index / this.strides[i]);
index -= locs[i] * this.strides[i];
}
locs[locs.length - 1] = index;
return locs;
}
get rank() {
return this.shape.length;
}
/**
* Creates an immutable `tf.Tensor` object from the buffer.
*
* @doc {heading: 'Tensors', subheading: 'Creation'}
*/
toTensor(): Tensor<R> {
return trackerFn().makeTensor(this.values, this.shape, this.dtype) as
Tensor<R>;
}
}
export interface TensorTracker {
makeTensor(
values: DataValues, shape: number[], dtype: DataType,
backend?: Backend): Tensor;
makeVariable(
initialValue: Tensor, trainable?: boolean, name?: string,
dtype?: DataType): Variable;
incRef(a: Tensor, backend: Backend): void;
disposeTensor(t: Tensor): void;
disposeVariable(v: Variable): void;
read(dataId: DataId): Promise<BackendValues>;
readSync(dataId: DataId): BackendValues;
}
/**
* The Tensor class calls into this handler to delegate chaining operations.
*/
export interface OpHandler {
cast<T extends Tensor>(x: T, dtype: DataType): T;
buffer<R extends Rank, D extends DataType>(
shape: ShapeMap[R], dtype: D,
values?: DataTypeMap[D]): TensorBuffer<R, D>;
print<T extends Tensor>(x: T, verbose: boolean): void;
clone<T extends Tensor>(x: T): T;
// TODO(yassogba) bring reshape back?
}
// For tracking tensor creation and disposal.
let trackerFn: () => TensorTracker = null;
// Used by chaining methods to call into ops.
let opHandler: OpHandler = null;
// Used to warn about deprecated methods.
let deprecationWarningFn: (msg: string) => void = null;
// This here so that we can use this method on dev branches and keep the
// functionality at master.
// tslint:disable-next-line:no-unused-expression
[deprecationWarningFn];
/**
* An external consumer can register itself as the tensor tracker. This way
* the Tensor class can notify the tracker for every tensor created and
* disposed.
*/
export function setTensorTracker(fn: () => TensorTracker) {
trackerFn = fn;
}
/**
* An external consumer can register itself as the op handler. This way the
* Tensor class can have chaining methods that call into ops via the op
* handler.
*/
export function setOpHandler(handler: OpHandler) {
opHandler = handler;
}
/**
* Sets the deprecation warning function to be used by this file. This way the
* Tensor class can be a leaf but still use the environment.
*/
export function setDeprecationWarningFn(fn: (msg: string) => void) {
deprecationWarningFn = fn;
}
/**
* We wrap data id since we use weak map to avoid memory leaks.
* Since we have our own memory management, we have a reference counter
* mapping a tensor to its data, so there is always a pointer (even if that
* data is otherwise garbage collectable).
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
* Global_Objects/WeakMap
*/
export type DataId = object; // object instead of {} to force non-primitive.
// Declare this namespace to make Tensor class augmentation work in google3.
export declare namespace Tensor {}
/**
* A `tf.Tensor` object represents an immutable, multidimensional array of
* numbers that has a shape and a data type.
*
* See `tf.tensor` for details on how to create a `tf.Tensor`.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
export class Tensor<R extends Rank = Rank> {
/** Unique id of this tensor. */
readonly id: number;
/**
* Id of the bucket holding the data for this tensor. Multiple arrays can
* point to the same bucket (e.g. when calling array.reshape()).
*/
dataId: DataId;
/** The shape of the tensor. */
readonly shape: ShapeMap[R];
/** Number of elements in the tensor. */
readonly size: number;
/** The data type for the array. */
readonly dtype: DataType;
/** The rank type for the array (see `Rank` enum). */
readonly rankType: R;
/** Whether this tensor has been globally kept. */
kept = false;
/** The id of the scope this tensor is being tracked in. */
scopeId: number;
/**
* Number of elements to skip in each dimension when indexing. See
* https://docs.scipy.org/doc/numpy/reference/generated/\
* numpy.ndarray.strides.html
*/
readonly strides: number[];
constructor(shape: ShapeMap[R], dtype: DataType, dataId: DataId, id: number) {
this.shape = shape.slice() as ShapeMap[R];
this.dtype = dtype || 'float32';
this.size = util.sizeFromShape(shape);
this.strides = computeStrides(shape);
this.dataId = dataId;
this.id = id;
this.rankType = (this.rank < 5 ? this.rank.toString() : 'higher') as R;
}
get rank(): number {
return this.shape.length;
}
/**
* Returns a promise of `tf.TensorBuffer` that holds the underlying data.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
async buffer<D extends DataType = 'float32'>(): Promise<TensorBuffer<R, D>> {
const vals = await this.data<D>();
return opHandler.buffer(this.shape, this.dtype as D, vals);
}
/**
* Returns a `tf.TensorBuffer` that holds the underlying data.
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
bufferSync<D extends DataType = 'float32'>(): TensorBuffer<R, D> {
return opHandler.buffer(this.shape, this.dtype as D, this.dataSync());
}
/**
* Returns the tensor data as a nested array. The transfer of data is done
* asynchronously.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
async array(): Promise<ArrayMap[R]> {
const vals = await this.data();
return toNestedArray(this.shape, vals) as ArrayMap[R];
}
/**
* Returns the tensor data as a nested array. The transfer of data is done
* synchronously.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
arraySync(): ArrayMap[R] {
return toNestedArray(this.shape, this.dataSync()) as ArrayMap[R];
}
/**
* Asynchronously downloads the values from the `tf.Tensor`. Returns a
* promise of `TypedArray` that resolves when the computation has finished.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
async data<D extends DataType = NumericDataType>(): Promise<DataTypeMap[D]> {
this.throwIfDisposed();
const data = trackerFn().read(this.dataId);
if (this.dtype === 'string') {
const bytes = await data as Uint8Array[];
try {
return bytes.map(b => util.decodeString(b)) as DataTypeMap[D];
} catch {
throw new Error(
'Failed to decode the string bytes into utf-8. ' +
'To get the original bytes, call tensor.bytes().');
}
}
return data as Promise<DataTypeMap[D]>;
}
/**
* Synchronously downloads the values from the `tf.Tensor`. This blocks the
* UI thread until the values are ready, which can cause performance issues.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
dataSync<D extends DataType = NumericDataType>(): DataTypeMap[D] {
this.throwIfDisposed();
const data = trackerFn().readSync(this.dataId);
if (this.dtype === 'string') {
try {
return (data as Uint8Array[]).map(b => util.decodeString(b)) as
DataTypeMap[D];
} catch {
throw new Error(
'Failed to decode the string bytes into utf-8. ' +
'To get the original bytes, call tensor.bytes().');
}
}
return data as DataTypeMap[D];
}
/** Returns the underlying bytes of the tensor's data. */
async bytes(): Promise<Uint8Array[]|Uint8Array> {
this.throwIfDisposed();
const data = await trackerFn().read(this.dataId);
if (this.dtype === 'string') {
return data as Uint8Array[];
} else {
return new Uint8Array((data as TypedArray).buffer);
}
}
/**
* Disposes `tf.Tensor` from memory.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
dispose(): void {
if (this.isDisposed) {
return;
}
trackerFn().disposeTensor(this);
this.isDisposedInternal = true;
}
protected isDisposedInternal = false;
get isDisposed(): boolean {
return this.isDisposedInternal;
}
throwIfDisposed() {
if (this.isDisposed) {
throw new Error(`Tensor is disposed.`);
}
}
/**
* Prints the `tf.Tensor`. See `tf.print` for details.
*
* @param verbose Whether to print verbose information about the tensor,
* including dtype and size.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
print(verbose = false): void {
return opHandler.print(this, verbose);
}
/**
* Returns a copy of the tensor. See `tf.clone` for details.
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
clone<T extends Tensor>(this: T): T {
this.throwIfDisposed();
return opHandler.clone(this);
}
/**
* Returns a human-readable description of the tensor. Useful for logging.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
toString(verbose = false): string {
const vals = this.dataSync();
return tensorToString(vals, this.shape, this.dtype, verbose);
}
cast<T extends this>(dtype: DataType): T {
this.throwIfDisposed();
return opHandler.cast(this as T, dtype);
}
variable(trainable = true, name?: string, dtype?: DataType): Variable<R> {
this.throwIfDisposed();
return trackerFn().makeVariable(this, trainable, name, dtype) as
Variable<R>;
}
}
Object.defineProperty(Tensor, Symbol.hasInstance, {
value: (instance: Tensor) => {
// Implementation note: we should use properties of the object that will be
// defined before the constructor body has finished executing (methods).
// This is because when this code is transpiled by babel, babel will call
// classCallCheck before the constructor body is run.
// See https://github.com/tensorflow/tfjs/issues/3384 for backstory.
return !!instance && instance.data != null && instance.dataSync != null &&
instance.throwIfDisposed != null;
}
});
export function getGlobalTensorClass() {
// Use getGlobal so that we can augment the Tensor class across package
// boundaries becase the node resolution alg may result in different modules
// being returned for this file depending on the path they are loaded from.
return getGlobal('Tensor', () => {
return Tensor;
});
}
// Global side effect. Cache global reference to Tensor class
getGlobalTensorClass();
export interface NumericTensor<R extends Rank = Rank> extends Tensor<R> {
dtype: NumericDataType;
dataSync<D extends DataType = NumericDataType>(): DataTypeMap[D];
data<D extends DataType = NumericDataType>(): Promise<DataTypeMap[D]>;
}
export interface StringTensor<R extends Rank = Rank> extends Tensor<R> {
dtype: 'string';
dataSync<D extends DataType = 'string'>(): DataTypeMap[D];
data<D extends DataType = 'string'>(): Promise<DataTypeMap[D]>;
}
/** @doclink Tensor */
export type Scalar = Tensor<Rank.R0>;
/** @doclink Tensor */
export type Tensor1D = Tensor<Rank.R1>;
/** @doclink Tensor */
export type Tensor2D = Tensor<Rank.R2>;
/** @doclink Tensor */
export type Tensor3D = Tensor<Rank.R3>;
/** @doclink Tensor */
export type Tensor4D = Tensor<Rank.R4>;
/** @doclink Tensor */
export type Tensor5D = Tensor<Rank.R5>;
/** @doclink Tensor */
export type Tensor6D = Tensor<Rank.R6>;
/**
* A mutable `tf.Tensor`, useful for persisting state, e.g. for training.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
export class Variable<R extends Rank = Rank> extends Tensor<R> {
name: string;
constructor(
initialValue: Tensor<R>, public trainable: boolean, name: string,
tensorId: number) {
super(
initialValue.shape, initialValue.dtype, initialValue.dataId, tensorId);
this.name = name;
}
/**
* Assign a new `tf.Tensor` to this variable. The new `tf.Tensor` must have
* the same shape and dtype as the old `tf.Tensor`.
*
* @param newValue New tensor to be assigned to this variable.
*
* @doc {heading: 'Tensors', subheading: 'Classes'}
*/
assign(newValue: Tensor<R>): void {
if (newValue.dtype !== this.dtype) {
throw new Error(
`dtype of the new value (${newValue.dtype}) and ` +
`previous value (${this.dtype}) must match`);
}
if (!util.arraysEqual(newValue.shape, this.shape)) {
throw new Error(
`shape of the new value (${newValue.shape}) and ` +
`previous value (${this.shape}) must match`);
}
trackerFn().disposeTensor(this);
this.dataId = newValue.dataId;
trackerFn().incRef(this, null /* backend */);
}
dispose(): void {
trackerFn().disposeVariable(this);
this.isDisposedInternal = true;
}
}
Object.defineProperty(Variable, Symbol.hasInstance, {
value: (instance: Variable) => {
return instance instanceof Tensor && instance.assign != null &&
instance.assign instanceof Function;
}
});