Getting Started With the File System Access API

The File System Access API is a web API that allows read and write access to a user’s local files. It unlocks new capabilities to build powerful web applications, such as text editors or IDEs, image editing tools, improved import/export, all in the frontend. Let’s look into how to get started using this API.

Screenshot of an alert popup asking the user if they want the site to be able to view their files via the File System Access API.

Reading files with the File System Access API

Before diving into the code required to read a file from the user’s system, an important detail to keep in mind is that calling the File System Access API needs to be done by a user gesture, in a secure context. In the following example, we’ll use a click event.

Reading from a single file

Reading data from a file can be done in less than 10 lines of code. Here’s an example code sample:

let fileHandle;
 
document.querySelector(".pick-file").onclick = async () => {
 [fileHandle] = await window.showOpenFilePicker();
 
 const file = await fileHandle.getFile();
 const content = await file.text();
 
 return content;
};

Let’s imagine we have a button in our HTML with the class .pick-file. When clicking on this button, we launch the file picker by calling window.showOpenFilePicker(), and we store the result from this query in a variable called fileHandle

What we get back from calling showOpenFilePicker() is an array of FileSystemFileHandle objects representing each file we selected. As this example is for a single file, we destructure the result. I’ll show how to select multiple files a bit later.

These objects contain a kind and name property. If you were to use console.log(fileHandle), you would see the following object:

FileSystemFileHandle {kind: 'file', name: 'data.txt'}

The kind can either be file or directory.

On fileHandle, we can then call the getFile() method to get details about our file. Calling this method returns an object with a few properties, including a timestamp of when the file was last modified, the name of the file, its size, and type.

Finally, we can call text() on the file to get its content.

Reading from multiple files

To read from multiple files, we need to pass an options object to showOpenFilePicker().

For example:

let fileHandles;
const options = {
 multiple: true,
};
 
document.querySelector(".pick-file").onclick = async () => {
 fileHandles = await window.showOpenFilePicker(options);
 
 // The rest of the code will be shown below
};

By default, the multiple property is set to false. Other options can be used to indicate the types of files that can be selected.

For example, if we only wanted to accept .jpeg files, the options object would include the following:

const options = {
 types: [
   {
     description: "Images",
     accept: {
       "image/jpeg": ".jpeg",
     },
   },
 ],
 excludeAcceptAllOption: true,
};

In this example, fileHandles is an array containing multiple files, so getting their content would be done in the following way:

let fileHandles;
const options = {
 multiple: true,
};
 
document.querySelector(".pick-file").onclick = async () => {
 fileHandles = await window.showOpenFilePicker(options);
 
 const allContent = await Promise.all(
   fileHandles.map(async (fileHandle) => {
     const file = await fileHandle.getFile();
     const content = await file.text();
     return content;
   })
 );
 
 console.log(allContent);
};

Writing to a file with the File System Access API

The File System Access API also allows you to write content to files. First, let’s look into how to save a new file.

Writing to a new file

Writing to a new file can also be done in a very short amount of code!

document.querySelector(".save-file").onclick = async () => {
 const options = {
   types: [
     {
       description: "Test files",
       accept: {
         "text/plain": [".txt"],
       },
     },
   ],
 };
 
 const handle = await window.showSaveFilePicker(options);
 const writable = await handle.createWritable();
 
 await writable.write("Hello World");
 await writable.close();
 
 return handle;
};

If we imagine a second button with the class save-file, on click, we open the file picker with the method showSaveFilePicker() and we pass in an option object containing the type of file to be saved, here a .txt file.

Calling this method will also return a FileSystemFileHandle object like in the first section. On this object, we can call the createWritable() method that will return a FileSystemWritableFileStream object. We can then write some content to this stream with the write() method in which we need to pass the content.

Finally, we need to call the close() method to close the file and finish writing the content to disk.

If you wanted to write some HTML code to a file for example, you would only need to change what’s in the options object to accept "text/html": [".html"]  and pass some HTML content to the write() method.

Editing an existing file

If you’d like to import a file and edit it with the File System Access API,  an example code sample would look like:

let fileHandle;
 
document.querySelector(".pick-file").onclick = async () => {
 [fileHandle] = await window.showOpenFilePicker();
 
 const file = await fileHandle.getFile();
 const writable = await fileHandle.createWritable();
 
 await writable.write("This is a new line");
 await writable.close();
};

If you’ve been following the rest of this post, you might recognize that we start with the showOpenFilePicker() and getFile() methods to read a file and we then use createWritable(), write() and close() to write to that same file.

If the file you’re importing already has content, this code sample will replace the current content with the new one passed into the write() method.

Additional File System Access API features

Without going into too much detail, the File System Access API also lets you list files in directories and delete files or directories.

Read directories

Reading directories can be done with a tiny bit of code:

document.querySelector(".read-dir").onclick = async () => {
 const directoryHandle = await window.showDirectoryPicker();
 
 for await (const entry of directoryHandle.values()) {
   console.log(entry.kind, entry.name);
 }
};

If we add a new button with the class .read-dir, on click, calling the showDirectoryPicker() method will open the file picker and, when selecting a directory on your computer, this code will list the files found in that directory.

Delete files

Deleting a file in a directory can be done with the following code sample:

document.querySelector(".pick-file").onclick = async () => {
 const [fileHandle] = await window.showOpenFilePicker();
 await fileHandle.remove();
};

If you want to delete a folder, you only need to make a small change to the code sample above:

document.querySelector(".read-dir").onclick = async () => {
 const directoryHandle = await window.showDirectoryPicker();
 await directoryHandle.remove();
};

Finally, if you want to remove a specific file when selecting a folder, you could write it like this:

// Delete a single file named data.txt in the selected folder
document.querySelector(".pick-folder").onclick = async () => {
   const directoryHandle = await window.showDirectoryPicker();
   await directoryHandle.removeEntry("data.txt");
};

And if you want to remove an entire folder, you would need the following lines:

// Recursively delete the folder named "data"
document.querySelector(".pick-folder").onclick = async () => {
   const directoryHandle = await window.showDirectoryPicker();
   await directoryHandle.removeEntry('data', { recursive: true });
};

File System Access API browser support

At the moment, IE and Firefox don’t seem to be supporting the File System Access API. However, there exists a ponyfill called browser-fs-access.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
101NoNo98TP

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
NoNoNo15.4

Wrapping up

If you’d like to try the File System Access API, check out this live demo text editor built by Google engineers. Otherwise, if you’d like to learn more about this API and all its features, here are some resources:


Getting Started With the File System Access API originally published on CSS-Tricks. You should get the newsletter.

Introduction to the Solid JavaScript Library

Solid is a reactive JavaScript library for creating user interfaces without a virtual DOM. It compiles templates down to real DOM nodes once and wraps updates in fine-grained reactions so that when state updates, only the related code runs.

This way, the compiler can optimize initial render and the runtime optimizes updates. This focus on performance makes it one of the top-rated JavaScript frameworks.

I got curious about it and wanted to give it a try, so I spent some time creating a small to-do app to explore how this framework handles rendering components, updating state, setting up stores, and more.

Here’s the final demo if you just can’t wait to see the final code and result:

Getting started

Like most frameworks, we can start by installing the npm package. To use the framework with JSX, run:

npm install solid-js babel-preset-solid

Then, we need to add babel-preset-solid to our Babel, webpack, or Rollup config file with:

"presets": ["solid"]

Or if you’d like to scaffold a small app, you can also use one of their templates:

# Create a small app from a Solid template
npx degit solidjs/templates/js my-app
 
# Change directory to the project created
cd my-app
 
# Install dependencies
npm i # or yarn or pnpm
 
# Start the dev server
npm run dev

There is TypeScript support so if you’d like to start a TypeScript project, change the first command to npx degit solidjs/templates/ts my-app.

Creating and rendering components

To render components, the syntax is similar to React.js, so it might seem familiar:

import { render } from "solid-js/web";
 
const HelloMessage = props => <div>Hello {props.name}</div>;
 
render(
 () => <HelloMessage name="Taylor" />,
 document.getElementById("hello-example")
);

We need to start by importing the render function, then we create a div with some text and a prop, and we call render, passing the component and the container element.

This code then compiles down to real DOM expressions. For example, the code sample above, once compiled by Solid, looks something like this:

import { render, template, insert, createComponent } from "solid-js/web";
 
const _tmpl$ = template(`<div>Hello </div>`);
 
const HelloMessage = props => {
 const _el$ = _tmpl$.cloneNode(true);
 insert(_el$, () => props.name);
 return _el$;
};
 
render(
 () => createComponent(HelloMessage, { name: "Taylor" }),
 document.getElementById("hello-example")
);

The Solid Playground is pretty cool and shows that Solid has different ways to render, including client-side, server-side, and client-side with hydration.

Tracking changing values with Signals

Solid uses a hook called createSignal that returns two functions: a getter and a setter. If you’re used to using a framework like React.js, this might seem a little weird. You’d normally expect the first element to be the value itself; however in Solid, we need to explicitly call the getter to intercept where the value is read in order to track its changes.

For example, if we’re writing the following code:

const [todos, addTodos] = createSignal([]);

Logging todos will not return the value, but a function instead. If we want to use the value, we need to call the function, as in todos().

For a small todo list, this would be:

import { createSignal } from "solid-js";
 
const TodoList = () => {
 let input;
 const [todos, addTodos] = createSignal([]);
 
 const addTodo = value => {
   return addTodos([...todos(), value]);
 };
 
 return (
   <section>
     <h1>To do list:</h1>
     <label for="todo-item">Todo item</label>
     <input type="text" ref={input} name="todo-item" id="todo-item" />
     <button onClick={() => addTodo(input.value)}>Add item</button>
     <ul>
       {todos().map(item => (
         <li>{item}</li>
       ))}
     </ul>
   </section>
 );
};

The code sample above would display a text field and, upon clicking the “Add item” button, would update the todos with the new item and display it in a list.

This can seem pretty similar to using useState, so how is using a getter different? Consider the following code sample:

console.log("Create Signals");
const [firstName, setFirstName] = createSignal("Whitney");
const [lastName, setLastName] = createSignal("Houston");
const [displayFullName, setDisplayFullName] = createSignal(true);
 
const displayName = createMemo(() => {
 if (!displayFullName()) return firstName();
 return `${firstName()} ${lastName()}`;
});
 
createEffect(() => console.log("My name is", displayName()));
 
console.log("Set showFullName: false ");
setDisplayFullName(false);
 
console.log("Change lastName ");
setLastName("Boop");
 
console.log("Set showFullName: true ");
setDisplayFullName(true);

Running the above code would result in:

Create Signals
 
My name is Whitney Houston
 
Set showFullName: false
 
My name is Whitney
 
Change lastName
 
Set showFullName: true
 
My name is Whitney Boop

The main thing to notice is how My name is ... is not logged after setting a new last name. This is because at this point, nothing is listening to changes on lastName(). The new value of displayName() is only set when the value of displayFullName() changes, this is why we can see the new last name displayed when setShowFullName is set back to true.

This gives us a safer way to track values updates.

Reactivity primitives

In that last code sample, I introduced createSignal, but also a couple of other primitives: createEffect and createMemo.

createEffect

createEffect tracks dependencies and runs after each render where a dependency has changed.

// Don't forget to import it first with 'import { createEffect } from "solid-js";'
const [count, setCount] = createSignal(0);
 
createEffect(() => {
 console

Count is at... logs every time the value of count() changes.

createMemo

createMemo creates a read-only signal that recalculates its value whenever the executed code’s dependencies update. You would use it when you want to cache some values and access them without re-evaluating them until a dependency changes.

For example, if we wanted to display a counter 100 times and update the value when clicking on a button, using createMemo would allow the recalculation to happen only once per click:

function Counter() {
   const [count, setCount] = createSignal(0);
   // Calling `counter` without wrapping it in `createMemo` would result in calling it 100 times.
   // const counter = () => {
   //    return count();
   // }
 
   // Calling `counter` wrapped in `createMemo` results in calling it once per update.
// Don't forget to import it first with 'import { createMemo } from "solid-js";'
   const counter = createMemo(() => {
       return count()
   })
 
   return (
       <>
       <button onClick={() => setCount(count() + 1)}>Count: {count()}</button>
       <div>1. {counter()}</div>
       <div>2. {counter()}</div>
       <div>3. {counter()}</div>
       <div>4. {counter()}</div>
       <!-- 96 more times -->
       </>
   );
}

Lifecycle methods

Solid exposes a few lifecycle methods, such as onMount, onCleanup and onError. If we want some code to run after the initial render, we need to use onMount:

// Don't forget to import it first with 'import { onMount } from "solid-js";'
 
onMount(() => {
 console.log("I mounted!");
});

onCleanup is similar to componentDidUnmount in React — it runs when there is a recalculation of the reactive scope.

onError executes when there’s an error in the nearest child’s scope. For example we could use it when fetching data fails.

Stores

To create stores for data, Solid exposes createStore which return value is a readonly proxy object and a setter function.

For example, if we changed our todo example to use a store instead of state, it would look something like this:

const [todos, addTodos] = createStore({ list: [] });
 
createEffect(() => {
 console.log(todos.list);
});
 
onMount(() => {
 addTodos("list", [
   ...todos.list,
   { item: "a new todo item", completed: false }
 ]);
});

The code sample above would start by logging a proxy object with an empty array, followed by a proxy object with an array containing the object {item: "a new todo item", completed: false}.

One thing to note is that the top level state object cannot be tracked without accessing a property on it — this is why we’re logging todos.list and not todos.

If we only logged todo` in createEffect, we would be seeing the initial value of the list but not the one after the update made in onMount.

To change values in stores, we can update them using the setting function we define when using createStore. For example, if we wanted to update a todo list item to “completed” we could update the store this way:

const [todos, setTodos] = createStore({
 list: [{ item: "new item", completed: false }]
});
 
const markAsComplete = text => {
 setTodos(
   "list",
   i => i.item === text,
   "completed",
   c => !c
 );
};
 
return (
 <button onClick={() => markAsComplete("new item")}>Mark as complete</button>
);

Control Flow

To avoid wastefully recreating all the DOM nodes on every update when using methods like .map(), Solid lets us use template helpers.

A few of them are available, such as For to loop through items, Show to conditionally show and hide elements, Switch and Match to show elements that match a certain condition, and more!

Here are some examples showing how to use them:

<For each={todos.list} fallback={<div>Loading...</div>}>
 {(item) => <div>{item}</div>}
</For>
 
<Show when={todos.list[0].completed} fallback={<div>Loading...</div>}>
 <div>1st item completed</div>
</Show>
 
<Switch fallback={<div>No items</div>}>
 <Match when={todos.list[0].completed}>
   <CompletedList />
 </Match>
 <Match when={!todos.list[0].completed}>
   <TodosList />
 </Match>
</Switch>

Demo project

This was a quick introduction to the basics of Solid. If you’d like to play around with it, I made a starter project you can automatically deploy to Netlify and clone to your GitHub by clicking on the button below!

This project includes the default setup for a Solid project, as well as a sample Todo app with the basic concepts I’ve mentioned in this post to get you going!

There is much more to this framework than what I covered here so feel free to check the docs for more info!


The post Introduction to the Solid JavaScript Library appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Machine Learning For Front-End Developers With Tensorflow.js

Machine Learning For Front-End Developers With Tensorflow.js

Machine Learning For Front-End Developers With Tensorflow.js

Charlie Gerard

Machine learning often feels like it belongs to the realm of data scientists and Python developers. However, over the past couple of years, open-source frameworks have been created to make it more accessible in different programming languages, including JavaScript. In this article, we will use Tensorflow.js to explore the different possibilities of using machine learning in the browser through a few example projects.

What Is Machine Learning?

Before we start diving into some code, let’s talk briefly about what machine learning is as well as some core concepts and terminology.

Definition

A common definition is that it is the ability for computers to learn from data without being explicitly programmed.

If we compare it to traditional programming, it means that we let computers identify patterns in data and generate predictions without us having to tell it exactly what to look for.

Let’s take the example of fraud detection. There is no set criteria to know what makes a transaction fraudulent or not; frauds can be executed in any country, on any account, targeting any customer, at any time, and so on. It would be pretty much impossible to track all of this manually.

However, using previous data around fraudulent expenses gathered over the years, we can train a machine-learning algorithm to understand patterns in this data to generate a model that can be given any new transaction and predict the probability of it being fraud or not, without telling it exactly what to look for.

Core Concepts

To understand the following code samples, we need to cover a few common terms first.

Model

When you train a machine-learning algorithm with a dataset, the model is the output of this training process. It’s a bit like a function that takes new data as input and produces a prediction as output.

Labels And Features

Labels and features relate to the data that you feed an algorithm in the training process.

A label represents how you would classify each entry in your dataset and how you would label it. For example, if our dataset was a CSV file describing different animals, our labels could be words like “cat”, “dog” or “snake” (depending on what each animal represents).

Features on the other hand, are the characteristics of each entry in your data set. For our animals example, it could be things like “whiskers, meows”, “playful, barks”, “reptile, rampant”, and so on.

Using this, a machine-learning algorithm will be able to find some correlation between features and their label that it will use for future predictions.

Neural Networks

Neural networks are a set of machine-learning algorithms that try to mimic the way the brain works by using layers of artificial neurons.

We don’t need to go in-depth about how they work in this article, but if you want to learn more, here’s a really good video:

Now that we’ve defined a few terms commonly used in machine learning, let’s talk about what can be done using JavaScript and the Tensorflow.js framework.

Features

Three features are currently available:

  1. Using a pre-trained model,
  2. Transfer learning,
  3. Defining, running, and using your own model.

Let’s start with the simplest one.

1. Using A Pre-Trained Model

Depending on the problem you are trying to solve, there might be a model already trained with a specific data set and for a specific purpose which you can leverage and import in your code.

For example, let’s say we are building a website to predict if an image is a picture of a cat. A popular image classification model is called MobileNet and is available as a pre-trained model with Tensorflow.js.

The code for this would look something like this:

<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Cat detection</title>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.1"> </script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@1.0.0"> </script>
  </head>
  <body>
    <img id="image" alt="cat laying down" src="cat.jpeg"/>

    <script>
      const img = document.getElementById('image');

      const predictImage = async () => {
        console.log("Model loading...");
        const model = await mobilenet.load();
        console.log("Model is loaded!")

        const predictions = await model.classify(img);
        console.log('Predictions: ', predictions);
      }
      predictImage();
    </script>
  </body>
</html>

We start by importing Tensorflow.js and the MobileNet model in the head of our HTML:

<script src="https://cdnjs.cloudflare.com/ajax/libs/tensorflow/1.0.1/tf.js"> </script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@1.0.0"> </script>

Then, inside the body, we have an image element that will be used for predictions:

<img id="image" alt="cat laying down" src="cat.jpeg"/>

And finally, inside the script tag, we have the JavaScript code that loads the pre-trained MobileNet model and classifies the image found in the image tag. It returns an array of 3 predictions which are ordered by probability score (the first element being the best prediction).

const predictImage = async () => {
  console.log("Model loading...");
  const model = await mobilenet.load();
  console.log("Model is loaded!")
  const predictions = await model.classify(img);
  console.log('Predictions: ', predictions);
}

predictImage();

And that’s it! This is the way you can use a pre-trained model in the browser with Tensorflow.js!

Note: If you want to have a look at what else the MobileNet model can classify, you can find a list of the different classes available on Github.

An important thing to know is that loading a pre-trained model in the browser can take some time (sometimes up to 10s) so you will probably want to preload or adapt your interface so that users are not impacted.

If you prefer using Tensorflow.js as a NPM module, you can do so by importing the module this way:

import * as mobilenet from '@tensorflow-models/mobilenet';

Feel free to play around with this example on CodeSandbox.

Now that we’ve seen how to use a pre-trained model, let’s look at the second feature available: transfer learning.

2. Transfer Learning

Transfer learning is the ability to combine a pre-trained model with custom training data. What this means is that you can leverage the functionality of a model and add your own samples without having to create everything from scratch.

For example, an algorithm has been trained with thousands of images to create an image classification model, and instead of creating your own, transfer learning allows you to combine new custom image samples with the pre-trained model to create a new image classifier. This feature makes it really fast and easy to have a more customized classifier.

To provide an example of what this would look like in code, let’s repurpose our previous example and modify it so we can classify new images.

Note: The end result is the experiment below that you can try live here.

(Live demo) (Large preview)

Below are a few code samples of the most important part of this setup, but if you need to have a look at the whole code, you can find it on this CodeSandbox.

We still need to start by importing Tensorflow.js and MobileNet, but this time we also need to add a KNN (k-nearest neighbor) classifier:

<!-- Load TensorFlow.js -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
<!-- Load MobileNet -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet"></script>
<!-- Load KNN Classifier -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/knn-classifier"></script>

The reason why we need a classifier is because (instead of only using the MobileNet module) we’re adding custom samples it has never seen before, so the KNN classifier will allow us to combine everything together and run predictions on the data combined.

Then, we can replace the image of the cat with a video tag to use images from the camera feed.

<video autoplay id="webcam" width="227" height="227"></video>

Finally, we’ll need to add a few buttons on the page that we will use as labels to record some video samples and start the predictions.

<section>
  <button class="button">Left</button>

  <button class="button">Right</button>

  <button class="test-predictions">Test</button>
</section>

Now, let’s move to the JavaScript file where we’re going to start by setting up a few important variables:

// Number of classes to classify
const NUM_CLASSES = 2;
// Labels for our classes
const classes = ["Left", "Right"];
// Webcam Image size. Must be 227.
const IMAGE_SIZE = 227;
// K value for KNN
const TOPK = 10;

const video = document.getElementById("webcam");

In this particular example, we want to be able to classify the webcam input between our head tilting to the left or the right, so we need two classes labeled left and right.

The image size set to 227 is the size of the video element in pixels. Based on the Tensorflow.js examples, this value needs to be set to 227 to match the format of the data the MobileNet model was trained with. For it to be able to classify our new data, the latter needs to fit the same format.

If you really need it to be larger, it is possible but you will have to transform and resize the data before feeding it to the KNN classifier.

Then, we’re setting the value of K to 10. The K value in the KNN algorithm is important because it represents the number of instances that we take into account when determining the class of our new input.

In this case, the value of 10 means that, when predicting the label for some new data, we will look at the 10 nearest neighbors from the training data to determine how to classify our new input.

Finally, we’re getting the video element. For the logic, let’s start by loading the model and classifier:

async load() {
    const knn = knnClassifier.create();
    const mobilenetModule = await mobilenet.load();
    console.log("model loaded");
}

Then, let’s access the video feed:

navigator.mediaDevices
  .getUserMedia({ video: true, audio: false })
  .then(stream => {
    video.srcObject = stream;
    video.width = IMAGE_SIZE;
    video.height = IMAGE_SIZE;
  });

Following that, let’s set up some button events to record our sample data:

setupButtonEvents() {
    for (let i = 0; i < NUM_CLASSES; i++) {
      let button = document.getElementsByClassName("button")[i];

      button.onmousedown = () => {
        this.training = i;
        this.recordSamples = true;
      };
      button.onmouseup = () => (this.training = -1);
    }
  }

Let’s write our function that will take the webcam images samples, reformat them and combine them with the MobileNet module:

// Get image data from video element
const image = tf.browser.fromPixels(video);

let logits;
// 'conv_preds' is the logits activation of MobileNet.
const infer = () => this.mobilenetModule.infer(image, "conv_preds");

// Train class if one of the buttons is held down
if (this.training != -1) {
  logits = infer();

  // Add current image to classifier
  this.knn.addExample(logits, this.training);
}

And finally, once we gathered some webcam images, we can test our predictions with the following code:

logits = infer();
const res = await this.knn.predictClass(logits, TOPK);
const prediction = classes[res.classIndex];

And finally, you can dispose of the webcam data as we don’t need it anymore:

// Dispose image when done
image.dispose();
if (logits != null) {
  logits.dispose();
}

Once again, if you want to have a look at the full code, you can find it in the CodeSandbox mentioned earlier.

3. Training A Model In The Browser

The last feature is to define, train and run a model entirely in the browser. To illustrate this, we’re going to build the classic example of recognizing Irises.

For this, we’ll create a neural network that can classify Irises in three categories: Setosa, Virginica, and Versicolor, based on an open-source dataset.

Before we start, here’s a link to the live demo and here’s the CodeSandbox if you want to play around with the full code.

At the core of every machine learning project is a dataset. One of the first step we need to undertake is to split this dataset into a training set and a test set.

The reason for this is that we’re going to use our training set to train our algorithm and our test set to check the accuracy of our predictions, to validate if our model is ready to be used or needs to be tweaked.

Note: To make it easier, I already split the training set and test set into two JSON files you can find in the CodeSanbox.

The training set contains 130 items and the test set 14. If you have a look at what this data looks like you’ll see something like this:

{
  "sepal_length": 5.1,
  "sepal_width": 3.5,
  "petal_length": 1.4,
  "petal_width": 0.2,
  "species": "setosa"
}

What we can see is four different features for the length and width of the sepal and petal, as well as a label for the species.

To be able to use this with Tensorflow.js, we need to shape this data into a format that the framework will understand, in this case, for the training data, it will be [130, 4] for 130 samples with four features per iris.

import * as trainingSet from "training.json";
import * as testSet from "testing.json";

const trainingData = tf.tensor2d(
  trainingSet.map(item => [
    item.sepal_length,
    item.sepal_width,
    item.petal_length,
    item.petal_width
  ]),
  [130, 4]
);

const testData = tf.tensor2d(
  testSet.map(item => [
    item.sepal_length,
    item.sepal_width,
    item.petal_length,
    item.petal_width
  ]),
  [14, 4]
);

Next, we need to shape our output data as well:

const output = tf.tensor2d(trainingSet.map(item => [
    item.species === 'setosa' ? 1 : 0,
    item.species === 'virginica' ? 1 : 0,
    item.species === 'versicolor' ? 1 : 0

]), [130,3])

Then, once our data is ready, we can move on to creating the model:

const model = tf.sequential();

model.add(tf.layers.dense(
    {
        inputShape: 4,
        activation: 'sigmoid',
        units: 10
    }
));

model.add(tf.layers.dense(
    {
        inputShape: 10,
        units: 3,
        activation: 'softmax'
    }
));

In the code sample above, we start by instantiating a sequential model, add an input and output layer.

The parameters you can see used inside (inputShape, activation, and units) are out of the scope of this post as they can vary depending on the model you are creating, the type of data used, and so on.

Once our model is ready, we can train it with our data:

async function train_data(){
    for(let i=0;i<15;i++){
      const res = await model.fit(trainingData, outputData,{epochs: 40});
    }
}

async function main() {
  await train_data();
  model.predict(testSet).print();
}

If this works well, you can start replacing the test data with custom user inputs.

Once we call our main function, the output of the prediction will look like one of these three options:

[1,0,0] // Setosa
[0,1,0] // Virginica
[0,0,1] // Versicolor

The prediction returns an array of three numbers representing the probability of the data belonging to one of the three classes. The number closest to 1 is the highest prediction.

For example, if the output of the classification is [0.0002, 0.9494, 0.0503], the second element of the array is the highest, so the model predicted that the new input is likely to be a Virginica.

And that’s it for a simple neural network in Tensorflow.js!

We only talked about a small dataset of Irises but if you want to move on to larger datasets or working with images, the steps will be the same:

  • Gathering the data;
  • Splitting between training and testing set;
  • Reformatting the data so Tensorflow.js can understand it;
  • Picking your algorithm;
  • Fitting the data;
  • Predicting.

If you want to save the model created to be able to load it in another application and predict new data, you can do so with the following line:

await model.save('file:///path/to/my-model'); // in Node.js

Note: For more options on how to save a model, have a look at this resource.

Limits

That’s it! We’ve just covered the three main features currently available using Tensorflow.js!

Before we finish, I think it is important to briefly mention some of the limits of using machine learning in the frontend.

1. Performance

Importing a pre-trained model from an external source can have a performance impact on your application. Some objects detection model, for example, are more than 10MB, which is significantly going to slow down your website. Make sure to think about your user experience and optimize the loading of your assets to improve your perceived performance.

2. Quality Of The Input Data

If you build a model from scratch, you’re going to have to gather your own data or find some open-source dataset.

Before doing any kind of data processing or trying different algorithms, make sure to check the quality of your input data. For example, if you are trying to build a sentiment analysis model to recognize emotions in pieces of text, make sure that the data you are using to train your model is accurate and diverse. If the quality of the data used is low, the output of your training will be useless.

3. Liability

Using an open-source pre-trained model can be very fast and effortless. However, it also means that you don’t always know how it was generated, what the dataset was made of, or even which algorithm was used. Some models are called “black boxes”, meaning that you don’t really know how they predicted a certain output.

Depending on what you are trying to build, this can be a problem. For example, if you are using a machine-learning model to help detect the probability of someone having cancer based on scan images, in case of false negative (the model predicted that a person didn’t have cancer when they actually did), there could be some real legal liability and you would have to be able to explain why the model made a certain prediction.

Summary

In conclusion, using JavaScript and frameworks like Tensorflow.js is a great way to get started and learn more about machine learning. Even though a production-ready application should probably be built in a language like Python, JavaScript makes it really accessible for developers to play around with the different features and get a better understanding of the fundamental concepts before eventually moving on and investing time into learning another language.

In this tutorial, we’ve only covered what was possible using Tensorflow.js, however, the ecosystem of other libraries and tools is growing. More specified frameworks are also available, allowing you to explore using machine learning with other domains such as music with Magenta.js, or predicting user navigation on a website using guess.js!

As tools get more performant, the possibilities of building machine learning-enabled applications in JavaScript are likely to be more and more exciting, and now is a good time to learn more about it, as the community is putting effort into making it accessible.

Further Resources

If you are interested in learning more, here are a few resources:

Other Frameworks And Tools

Examples, Models And Datasets

Inspiration

Thanks for reading!

Smashing Editorial (rb, dm, yk, il)