Skip to content

andre-dietrich/TensorFlowJS_tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 

Repository files navigation

TensorFlow.js

This is a translation of the official TensorFlowJS tutorials from https://js.tensorflow.org/tutorials/ into LiaScript (not ready yet).

You can try out this interactive Tutorial at:

https://liascript.github.io/course/?https://raw.githubusercontent.com/andre-dietrich/TensorFlowJS_tutorial/master/README.md

or clone it from:

https://github.com/andre-dietrich/TensorFlowJS_tutorial

and start to create your own tutorial...

Core Concepts

TensorFlow.js is an open source WebGL-accelerated JavaScript library for machine intelligence. It brings highly performant machine learning building blocks to your fingertips, allowing you to train neural networks in a browser or run pre-trained models in inference mode. See Getting Started for a guide on installing/configuring TensorFlow.js.

TensorFlow.js provides low-level building blocks for machine learning as well as a high-level, Keras-inspired API for constructing neural networks. Let's take a look at some of the core components of the library.

Tensors

The central unit of data in TensorFlow.js is the tensor: a set of numerical values shaped into an array of one or more dimensions. A Tensor instance has a shape attribute that defines the array shape (i.e., how many values are in each dimension of the array).

The primary Tensor constructor is the tf.tensor function:

// 2x3 Tensor
const shape = [2, 3]; // 2 rows, 3 columns
const a = tf.tensor([1.0, 2.0, 3.0, 10.0, 20.0, 30.0], shape);
a.print(); // print Tensor values

// The shape can also be inferred:
const b = tf.tensor([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]);
b.print(); // print Tensor values
Tensor
    [[1 , 2 , 3 ],
     [10, 20, 30]]
Tensor
    [[1 , 2 , 3 ],
     [10, 20, 30]]

@TF.eval

However, for constructing low-rank tensors, we recommend using the following functions to enhance code readability: tf.scalar, tf.tensor1d, tf.tensor2d, tf.tensor3d and tf.tensor4d.

The following example creates an identical tensor to the one above using tf.tensor2d:

const c = tf.tensor2d([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]);
c.print();
Tensor
    [[1 , 2 , 3 ],
     [10, 20, 30]]

@TF.eval

TensorFlow.js also provides convenience functions for creating tensors with all values set to 0 (tf.zeros) or all values set to 1 (tf.ones):

// 3x5 Tensor with all values set to 0
const zeros = tf.zeros([3, 5]);
zeros.print();
Tensor
    [[0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0]]

@TF.eval

In TensorFlow.js, tensors are immutable; once created, you cannot change their values. Instead you perform operations on them that generate new tensors.

Variables

Variables are initialized with a tensor of values. Unlike Tensors, however, their values are mutable. You can assign a new tensor to an existing variable using the assign method:

const initialValues = tf.zeros([5]);
const biases = tf.variable(initialValues); // initialize biases
biases.print();                            // output: [0, 0, 0, 0, 0]

const updatedValues = tf.tensor1d([0, 1, 0, 1, 0]);
biases.assign(updatedValues); // update values of biases
biases.print();               // output: [0, 1, 0, 1, 0]

@TF.eval

Variables are primarily used to store and then update values during model training.

Operations (Ops)

While tensors allow you to store data, operations (ops) allow you to manipulate that data. TensorFlow.js provides a wide variety of ops suitable for linear algebra and machine learning that can be performed on tensors. Because tensors are immutable, these ops do not change their values; instead, ops return new tensors.

Available ops include unary ops such as square:

const d = tf.tensor2d([[1.0, 2.0], [3.0, 4.0]]);
const d_squared = d.square();
d_squared.print();
// Output: [[1, 4 ],
//          [9, 16]]

@TF.eval

And binary ops such as add, sub, and mul:

const e = tf.tensor2d([[1.0, 2.0], [3.0, 4.0]]);
const f = tf.tensor2d([[5.0, 6.0], [7.0, 8.0]]);

const e_plus_f = e.add(f);
e_plus_f.print();
// Output: [[6 , 8 ],
//          [10, 12]]

@TF.eval

TensorFlow.js has a chainable API; you can call ops on the result of ops:

const e = tf.tensor2d([[1.0, 2.0], [3.0, 4.0]]);
const f = tf.tensor2d([[5.0, 6.0], [7.0, 8.0]]);

const sq_sum = e.add(f).square();
sq_sum.print();
// Output: [[36 , 64 ],
//          [100, 144]]

@TF.eval

All operations are also exposed as functions in the main namespace, so you could also do the following:

const e = tf.tensor2d([[1.0, 2.0], [3.0, 4.0]]);
const f = tf.tensor2d([[5.0, 6.0], [7.0, 8.0]]);

const sq_sum = tf.square(tf.add(e, f));
sq_sum.print();

@TF.eval

Models and Layers

Conceptually, a model is a function that given some input will produce some desired output.

In TensorFlow.js there are two ways to create models. You can use ops directly to represent the work the model does. For example:

// Define function
function predict(input) {
  // y = a * x ^ 2 + b * x + c
  // More on tf.tidy in the next section
  return tf.tidy(() => {
    const x = tf.scalar(input);

    const ax2 = a.mul(x.square());
    const bx = b.mul(x);
    const y = ax2.add(bx).add(c);

    return y;
  });
}

// Define constants: y = 2x^2 + 4x + 8
const a = tf.scalar(2);
const b = tf.scalar(4);
const c = tf.scalar(8);

// Predict output for input of 2
const result = predict(2);
result.print() // Output: 24

@TF.eval

You can also use the high-level API tf.model to construct a model out of layers, which are a popular abstraction in deep learning. The following code constructs a tf.sequential model:

const model = tf.sequential();
model.add(
  tf.layers.simpleRNN({
    units: 20,
    recurrentInitializer: 'GlorotNormal',
    inputShape: [80, 4]
  })
);

const optimizer = tf.train.sgd(LEARNING_RATE);
model.compile({optimizer, loss: 'categoricalCrossentropy'});
model.fit({x: data, y: labels});

There are many different types of layers available in TensorFlow.js. A few examples include tf.layers.simpleRNN, tf.layers.gru, and tf.layers.lstm.

Memory Management: dispose and tf.tidy

Because TensorFlow.js uses the GPU to accelerate math operations, it's necessary to manage GPU memory when working with tensors and variables.

TensorFlow.js provide two functions to help with this: dispose and tf.tidy.

dispose

You can call dispose on a tensor or variable to purge it and free up its GPU memory:

const x = tf.tensor2d([[0.0, 2.0], [4.0, 6.0]]);
const x_squared = x.square();

x.dispose();
x_squared.dispose();

x.print()         // will create an Error (Tensor is disposed)
x_squared.print() // ...

@TF.eval

tf.tidy

Using dispose can be cumbersome when doing a lot of tensor operations. TensorFlow.js provides another function, tf.tidy, that plays a similar role to regular scopes in JavaScript, but for GPU-backed tensors.

tf.tidy executes a function and purges any intermediate tensors created, freeing up their GPU memory. It does not purge the return value of the inner function.

// tf.tidy takes a function to tidy up after
const average = tf.tidy(() => {
  // tf.tidy will clean up all the GPU memory used by tensors inside
  // this function, other than the tensor that is returned.
  //
  // Even in a short sequence of operations like the one below, a number
  // of intermediate tensors get created. So it is a good practice to
  // put your math ops in a tidy!
  const y = tf.tensor1d([1.0, 2.0, 3.0, 4.0]);
  const z = tf.ones([4]);

  return y.sub(z).square().mean();
});

average.print() // Output: 3.5

@TF.eval

Using tf.tidy will help prevent memory leaks in your application. It can also be used to more carefully control when memory is reclaimed.

Two important notes

  • The function passed to tf.tidy should be synchronous and also not return a Promise. We suggest keeping code that updates the UI or makes remote requests outside of tf.tidy.
  • tf.tidy will not clean up variables. Variables typically last through the entire lifecycle of a machine learning model, so TensorFlow.js doesn't clean them up even if they are created in a tidy; however, you can call dispose on them manually.

Additional Resources

See the TensorFlow.js API reference for comprehensive documentation of the library.

For a more in-depth look at machine learning fundamentals, see the following resources:

Training First Steps: Fitting a Curve to Synthetic Data

In this tutorial, we'll use TensorFlow.js to fit a curve to a synthetic dataset. Given some data generated using a polynomial function with some noise added, we'll train a model to discover the coefficients used to generate the data.

Prerequisites

This tutorial assumes familiarity with the fundamental building blocks of TensorFlow.js introduced in Core Concepts: tensors, variables, and ops. We recommend completing Core Concepts before doing this tutorial.

Running the Code

TODO

Input Data

Our synthetic data set is composed of x- and y-coordinates that look as follows when plotted on a Cartesian plane:

window.generateData = function (numPoints, coeff, sigma = 0.04) {
  return tf.tidy(() => {
    const [a, b, c, d] = [
      tf.scalar(coeff.a), tf.scalar(coeff.b),
      tf.scalar(coeff.c), tf.scalar(coeff.d)];

    const xs = tf.randomUniform([numPoints], -1, 1);

    // Generate polynomial data
    const three = tf.scalar(3, 'int32');
    const ys = a.mul(xs.pow(three))
      .add(b.mul(xs.square()))
      .add(c.mul(xs))
      .add(d)
      // Add random noise to the generated data
      // to make the problem a bit more interesting
      .add(tf.randomNormal([numPoints], 0, sigma));

    // Normalize the y values to the range 0 to 1.
    const ymin = ys.min();
    const ymax = ys.max();
    const yrange = ymax.sub(ymin);
    const ysNormalized = ys.sub(ymin).div(yrange);

    return {
      xs,
      ys: ysNormalized
    };
  })
}
console.log("global function 'generateData' generated!")

@TF.eval

This data was generated using a cubic function of the format $y = ax3 + bx2 + cx + d$.

Our task is to learn the coefficients of this function: the values of $a$, $b$, $c$, and $d$ that best fit the data. Let's take a look at how we might learn those values using TensorFlow.js operations.

const trueCoefficients = {a: -.8, b: -.2, c: .9, d: .5};
window.trainingData = generateData(100, trueCoefficients);

plotData(trainingData.xs, trainingData.ys);
async function plotData(xs, ys) {
	const xvals = await xs.data();
  const yvals = await ys.data();

  let main = document.getElementById('main');
  main.hidden = false;

  let chart = echarts.init(main);

  let values = Array.from(yvals).map((y, i) => {
     return [xvals[i], yvals[i]];
  });

  let c = trueCoefficients;

  let option = {
    title : {
      text: 'Original Data (Synthetic)',
      subtext: 'True coefficients: a='+c.a+", b="+c.b+", c="+c.c+", d="+c.d
    },
    toolbox: {
      show : true,
      feature : {
        mark : {show: true},
        dataZoom : {show: true},
        dataView : {show: true, readOnly: false},
        restore : {show: true},
        saveAsImage : {show: true}
      }
    },
    xAxis : [{
      type : 'value',
      scale: true,
      axisLabel : { formatter: '{value}' }
    }],
    yAxis : [{
      type : 'value',
      scale: true,
      axisLabel : { formatter: '{value}'}
    }],
    series : [{
      name: 'data',
      type: 'scatter',
      data: values,
    }]
  };

  // use configuration item and data specified to show chart
  chart.setOption(option);

  window.addEventListener('resize', chart.resize);
}

@TF.eval2

Step 1: Set up Variables

First, let's create some variables to hold our current best estimate of these values at each step of model training. To start, we'll assign each of these variables a random number:

const a = tf.variable(tf.scalar(Math.random()));
const b = tf.variable(tf.scalar(Math.random()));
const c = tf.variable(tf.scalar(Math.random()));
const d = tf.variable(tf.scalar(Math.random()));

a.print(); b.print(); c.print(); d.print();

window.startVariables = [a, b, c, d];

@TF.eval

Step 2: Build a Model

We can represent our polynomial function $y = ax3 + bx2 + cx + d$ in TensorFlow.js by chaining a series of mathematical operations: addition (add), multiplication (mul), and exponentiation (pow and square).

The following code constructs a predict function that takes x as input and returns y:

function predict(x) {
  let [a, b, c, d] = startVariables;
  // y = a * x ^ 3 + b * x ^ 2 + c * x + d
  return tf.tidy(() => {
    return a.mul(x.pow(tf.scalar(3))) // a * x^3
      .add(b.mul(x.square())) // + b * x ^ 2
      .add(c.mul(x)) // + c * x
      .add(d); // + d
  });
}

const prediction = predict(trainingData.xs);
plotData(trainingData.xs, trainingData.ys, prediction);
async function plotData(xs, ys, ts) {

	const xvals = await xs.data();
  const yvals = await ys.data();
  const tvals = await ts.data();

  let main = document.getElementById('main2');
  main.hidden = false;

  let chart = echarts.init(main);

  let predictions = Array.from(yvals).map((y, i) => {
     return [xvals[i], tvals[i]];
  }).sort((a,b) => {
    return (a[0] < b[0] ? 1 : -1)
  });

  let values = Array.from(yvals).map((y, i) => {
     return [xvals[i], yvals[i]];
  });

  let [a, b, c, d] = startVariables;

  a = await a.data();
  b = await b.data();
  c = await c.data();
  d = await d.data();

  let option = {
    title : {
      text: 'Fit courve with random coefficients (before trainig)',
      subtext: "Random coefficient: a="+a+", b="+b+", c="+c+", d="+d
    },
    legend: {
        data: ['fuck', 'fuckking']
    },
    toolbox: {
      show : true,
      feature : {
        mark : {show: true},
        dataZoom : {show: true},
        dataView : {show: true, readOnly: false},
        restore : {show: true},
        saveAsImage : {show: true}
      }
    },
    xAxis : [{
      type : 'value',
      scale: true,
      axisLabel : { formatter: '{value}' }
    }],
    yAxis : [{
      type : 'value',
      scale: true,
      axisLabel : { formatter: '{value}'}
    }],
    series : [
      {name: 'trainig data', type: 'scatter', data: values },
      {name: 'prediction', type: 'line', data: predictions }
    ]
  };

  chart.setOption(option);
//  window.addEventListener('resize', chart.resize);
}

@TF.eval2

Let's go ahead and plot our polynomial function using the random values for $a$, $b$, $c$, and $d$ that we set in Step 1. Our plot will likely look something like this:

Because we started with random values, our function is likely a very poor fit for the data set. The model has yet to learn better values for the coefficients.

Step 3: Train the Model

Our final step is to train the model to learn good values for the coefficients. To train our model, we need to define three things:

  • A loss function, which measures how well a given polynomial fits the data. The lower the loss value, the better the polynomial fits the data.
  • An optimizer, which implements an algorithm for revising our coefficient values based on the output of the loss function. The optimizer's goal is to minimize the output value of the loss function.
  • A training loop, which will iteratively run the optimizer to minimize loss.

Define the Loss Function

For this tutorial, we'll use mean squared error (MSE) as our loss function. MSE is calculated by squaring the difference between the actual $y$ value and the predicted $y$ value for each $x$ value in our data set, and then taking the mean of all the resulting terms.

We can define a MSE loss function in TensorFlow.js as follows:

function loss(predictions, labels) {
  // Subtract our labels (actual values) from predictions, square the results,
  // and take the mean.
  const meanSquareError = predictions.sub(labels).square().mean();
  return meanSquareError;
}

Define the Optimizer

For our optimizer, we'll use Stochastic Gradient Descent (SGD). SGD works by taking the gradient of a random point in our data set and using its value to inform whether to increase or decrease the value of our model coefficients.

TensorFlow.js provides a convenience function for performing SGD, so that you don't have to worry about performing all these mathematical operations yourself. tf.train.sgd takes as input a desired learning rate, and returns an SGDOptimizer object, which can be invoked to optimize the value of the loss function.

The learning rate controls how big the model's adjustments will be when improving its predictions. A low learning rate will make the learning process run more slowly (more training iterations needed to learn good coefficients), while a high learning rate will speed up learning but might result in the model oscillating around the right values, always overcorrecting.

The following code constructs an SGD optimizer with a learning rate of 0.5:

const learningRate = 0.5;
const optimizer = tf.train.sgd(learningRate);

Define the Training Loop

Now that we've defined our loss function and optimizer, we can build a training loop, which iteratively performs SGD to refine our model's coefficients to minimize loss (MSE). Here's what our loop looks like:

function train(xs, ys, numIterations = 75) {

  const learningRate = 0.5;
  const optimizer = tf.train.sgd(learningRate);

  for (let iter = 0; iter < numIterations; iter++) {
    optimizer.minimize(() => {
      const predsYs = predict(xs);
      return loss(predsYs, ys);
    });
  }
}

Let's take a closer look at the code, step by step. First, we define our training function to take the x and y values of our dataset, as well as a specified number of iterations, as input:

function train(xs, ys, numIterations) {
...
}

Next, we define the learning rate and SGD optimizer as discussed in the previous section:

const learningRate = 0.5;
const optimizer = tf.train.sgd(learningRate);

Finally, we set up a for loop that runs numIterations training iterations. In each iteration, we invoke minimize on the optimizer, which is where the magic happens:

for (let iter = 0; iter < numIterations; iter++) {
  optimizer.minimize(() => {
    const predsYs = predict(xs);
    return loss(predsYs, ys);
  });
}

minimize takes a function that does two things:

  1. It predicts y values (predYs) for all the x values using the predict model function we defined earlier in Step 2.
  2. It returns the mean squared error loss for those predictions using the loss function we defined earlier in Define the Loss Function.

minimize then automatically adjusts any Variables used by this function (here, the coefficients a, b, c, and d) in order to minimize the return value (our loss).

After running our training loop, a, b, c, and d will contain the coefficient values learned by the model after 75 iterations of SGD.

See the Results!

Once the program finishes running, we can take the final values of our variables a, b, c, and d, and use them to plot a curve:

// Step 1. Set up variables, these are the things we want the model
// to learn in order to do prediction accurately. We will initialize
// them with random values.
const a = tf.variable(tf.scalar(Math.random()));
const b = tf.variable(tf.scalar(Math.random()));
const c = tf.variable(tf.scalar(Math.random()));
const d = tf.variable(tf.scalar(Math.random()));


// Step 2. Create an optimizer, we will use this later. You can play
// with some of these values to see how the model performs.
const numIterations = 75;
const learningRate = 0.5;
const optimizer = tf.train.sgd(learningRate);

// Step 3. Write our training process functions.
function predict(x) {
  // y = a * x ^ 3 + b * x ^ 2 + c * x + d
  return tf.tidy(() => {
    return a.mul(x.pow(tf.scalar(3, 'int32')))
      .add(b.mul(x.square()))
      .add(c.mul(x))
      .add(d);
  });
}

function loss(prediction, labels) {
  // Having a good error function is key for training a machine learning model
  const error = prediction.sub(labels).square().mean();
  return error;
}

async function train(xs, ys, numIterations) {
  for (let iter = 0; iter < numIterations; iter++) {
    optimizer.minimize(() => {
      // Feed the examples into the model
      const pred = predict(xs);
      return loss(pred, ys);
    });

    // Use tf.nextFrame to not block the browser.
    await tf.nextFrame();
  }
}

const trueCoefficients = {a: -.8, b: -.2, c: .9, d: .5};
//const trainingData = generateData(100, trueCoefficients);

trainingData.print();

// Plot original data
//  renderCoefficients('#data .coeff', trueCoefficients);
//  await plotData('#data .plot', trainingData.xs, trainingData.ys)


const predictionsBefore = predict(trainingData.xs);

// Train the model!
await train(trainingData.xs, trainingData.ys, numIterations);


const predictionsAfter = predict(trainingData.xs);

predictionsBefore.dispose();
predictionsAfter.dispose();

@TF.eval

The result is much better than the curve we originally plotted using random values for the coefficient.

Additional Resources

Training on Images

In this tutorial, we'll build a TensorFlow.js model to classify handwritten digits with a convolutional neural network. First, we'll train the classifier by having it “look” at thousands of handwritten digit images and their labels. Then we'll evaluate the classifier's accuracy using test data that the model has never seen.