Cannot use Tensorflow.js to load my keras model

Hi the community,
I trained a Keras model in python and converted it to TFJS layers format in order to make inferences in one web page I hosted onto Github pages. But even if my code looks good it does not work. It looks like all the code is paused or even blocked cause of this single line: import * as tf from ‘@tensorflow/tfjs’; (So probably the most important line in order to use my model :joy: ). I don’t find anything relating incompatibility issue between Tensorflow.js and touch event listeners but that what I was assuming in a first time. It’s quite worrying because the lines of my code that use tfjs are nearly identical to the ones in the official tutorial

import * as tf from '@tensorflow/tfjs';

class TrackingSession {
    /*
    This TrackingSession offers us a general way to collect data from the screen through the app
    The most important method there is the *handle* method through which we collect data like position and timestamp
    */
    // To track the active touch
    activeTouch = {}
    // To store all the records
    records = []
    // Our app are not going to use the whole screen. This will set the app screen dimensions
    screenScale = window.devicePixelRatio
    screenSize = [
        screen.width,
        screen.height
    ]
    // The handle method
    handle(event, touches) {
        if (touches.length !== 1) {
            // Ignore if there are multiple touches on the screen
            return
        }
        const touch = touches.item(0)
        switch (event) {
            case "start":
                const id = Math.floor(1e8 * Math.random()) + ""
                this.activeTouch[touch.identifier] = id
                this.records.push(new TouchRecord(event, touch, id))
                break
            case "move":
                if (this.activeTouch[touch.identifier]) {
                    this.records.push(new TouchRecord(event, touch, this.activeTouch[touch.identifier]))
                }
                break
            case "end":
                if (this.activeTouch[touch.identifier]) {
                    this.records.push(new TouchRecord(event, touch, this.activeTouch[touch.identifier]))
                    delete this.activeTouch[touch.identifier]
                    this.export()
                    this.activeTouch = {}
                    this.records = []
                }
                break
        }
    }

    // Recognition
    async recognizeUser() {
        const model = await tf.loadLayersModel('https://diarray-hub.github.io/TouchPatternRecognition/Models/tfjs_model/model.json');
        const data = preprocess(this.touchTracks);
        const outcome = await model.predict([tf.tensor(data)]);
        if (outcome[0][0] >= 0.90) {
          // Redirect the user to another page
          window.location.href = "https://diarray-hub.github.io/TouchPatternRecognition/theapp/Welcome.html";
        } 
        else {
          window.location.href = "https://diarray-hub.github.io/TouchPatternRecognition/theapp/Error.html";
        }
    }
    
    // This method will use the *download* function defined below to export data in .json file format
    export() {
        const name = "TouchTracker_Export";
        const touchTrackings = {};
        let currentTouchId;
        let currentTouchTimestamp;
        let currentPosition;
        let lastPosition;
        let currentSpeed;
        let currentDirection;
    
        // Process the touch records to create touch tracking objects
        this.records.forEach(record => {
            if (record.event === "start") {
                currentTouchId = record.touchId;
                currentTouchTimestamp = record.timestamp;
                currentPosition = record.position;
                touchTrackings[currentTouchId] = {id: currentTouchId, positions: [currentPosition], 
                    speeds: [], directions: [], startTimestamp: currentTouchTimestamp};
            } else if (record.event === "move") {
                lastPosition = currentPosition;
                currentPosition = record.position;
                currentSpeed = calculateSpeed(currentPosition, lastPosition, record.timestamp, currentTouchTimestamp);
                currentTouchTimestamp = record.timestamp;
                currentDirection = calculateDirection(currentPosition, lastPosition);
                touchTrackings[currentTouchId].positions.push(currentPosition);
                touchTrackings[currentTouchId].speeds.push(currentSpeed);
                touchTrackings[currentTouchId].directions.push(currentDirection);
            } else if (record.event === "end") {
                touchTrackings[currentTouchId].endTimestamp = record.timestamp;
            }
        });
    
        // Create an array of touch tracking objects
        const touchTrackingsArray = Object.values(touchTrackings);
    
        // Generate the output object
        const output = {
            name: name,
            duration: touchTrackingsArray[0].endTimestamp - touchTrackingsArray[0].starTimestamp,
            touchTrackings: touchTrackingsArray,
            screenSize: this.screenSize,
            screenScale: this.screenScale
        };
        
        //this.recognizeUser()
        download(JSON.stringify(output, null, 2), name + " " + new Date().toLocaleString(), "application/json");

        function calculateSpeed(currentPosition, lastPosition, timestamp, lastimestamp) {
            const distance = Math.sqrt((currentPosition[0] - lastPosition[0]) ** 2 + (currentPosition[1] - lastPosition[1]) ** 2);
            const timeElapsed = timestamp - lastimestamp;
            return distance / timeElapsed; // Eucludian speed calculus
        }
    
        function calculateDirection(currentPosition, lastPosition) {
            /*
            Note that the angle returned by Math.atan2 is not the same as the direction in degrees (i.e. north, south, east, west). Instead, 
            it represents the angle between the two points in the coordinate system, with the positive x-axis as the reference.
            */
            const deltaX = currentPosition[0] - lastPosition[0];
            const deltaY = currentPosition[1] - lastPosition[1];
            return Math.atan2(deltaY, deltaX);
        }
    }
}

class TouchRecord {
    /*
    A TouchRecord class that we'll use as represention of the collected data. 
    This class' structure represent how data will look like in the .json file
    */
    touchId
    event
    position
    timestamp

    constructor(event, touch, id) {
        this.touchId = id
        this.event = event
        const topOffset = screen.height - window.innerHeight
        this.position = [
            touch.screenX,
            touch.screenY + topOffset
        ]
        this.timestamp = new Date().getTime() / 1000
    }
}

// Implement a part of preprocessing.py stats_summary function in JS
function preprocess(touchTrackingArray){
    var touch = touchTrackingArray[0],
        positionX = [],
        positionY = [],
        speeds = touch.speeds,
        directions = touch.directions,
        latency = touch.endTimestamp - touch.startTimestamp
    touch.positions.forEach(position => {
        positionX.push(position[0])
        positionY.push(position[1])
    });
    fields = [positionX, positionY, speeds, directions];
    // Calculate the features
    const features = fields.map(field => {
        const mean = field.reduce((a, b) => a + b) / field.length;
        const stdDev = Math.sqrt(field.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / field.length);
        const min = Math.min(...field);
        const max = Math.max(...field);
        const range = max - min;
        return [mean, stdDev, min, max, range];
    });
    
    // Flatten the features into a single list
    const flattenedFeatures = features.reduce((acc, val) => acc.concat(val), []);
    flattenedFeatures.push(latency);
    return flattenedFeatures
}

// Defining a function that will allows us to export collected data in .json file from the browser
function download(data, filename, type) {
    var file = new Blob([data], {type: type});
    if (window.navigator.msSaveOrOpenBlob) // IE10+
        window.navigator.msSaveOrOpenBlob(file, filename);
    else { // Others
        var a = document.createElement("a"),
                url = URL.createObjectURL(file);
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        setTimeout(function() {
            document.body.removeChild(a);
            window.URL.revokeObjectURL(url);  
        }, 0); 
    }
}

// Creating a instance of our Trackingsession class
const session = new TrackingSession()

document.body.addEventListener('touchstart', function(e){
    e.preventDefault()
    session.handle("start", e.touches)
});

document.body.addEventListener('touchmove', function(e){
    e.preventDefault()
    session.handle("move", e.changedTouches)
}, { passive: false });

document.body.addEventListener('touchend', function(e){
    e.preventDefault()
    console.log(e.changedTouches)
    session.handle("end", e.changedTouches)
});

document.body.addEventListener('touchcancel', function(e){
    e.preventDefault()
    session.handle("end", e.changedTouches)
});

Trying to debug the code I wrote it with script tags in the html file and I noticed that there the problem come from the line: const model = await tf.loadLayersModel(‘https://diarray-hub.github.io/TouchPatternRecognition/Models/tfjs_model/model.json’); that block the code again and even block execution of lines above it but this allows me to know that in this case the import success and I could even use tf.tensor() but when I add the above line the code simply don’t run like when I import tfjs in the script :sweat:.

<!doctype html>
<html lang="en">
  <head>
    <title>Test</title>
  </head>
  <body>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.7.0/dist/tf.min.js"></script>
    <h1>Debugging</h1>
    <script>
  data = preprocess(touchTracks);
  document.write(data);
  document.write("\n")
  data = tf.tensor(data)
  document.write(data)
  document.write("\n")
  const model =  tf.loadLayersModel('../../Models/tfjs_model/model.json');
  document.write(model.state)

  if (model) {
	  document.write("yo!\n");

    result = model.predict([data]);
    if(result){
      result.array().then(function(scores) {
        document.write(scores);
      });
    }
    else{
      document.write("Something went wrong!");
    }
    } 
    else {
      document.write("Oh no!");
    }
  </script>
  </body>
</html>

I need help guys!

1 Like
import * as tf from '@tensorflow/tfjs';

wont work in a browser because this line has to be resolved a node_module path.

Maybe this could work: import * as tf from 'node_modules/@tensorflow/tfjs'; this is assuming that your index.html is in the same directory that the node modules folder.

Also, you should use the vscode extension live server to serve the html, as they indicate in the tutorial.

Please paste your errors so we can debug it if it persists.

2 Likes

I did a this small tutorial for you:

  1. Create the simplest html file:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
  <script src="index.js" type="module"></script>
</html>
  1. Create the simplest module-like script, index.js
import * as tf from "./node_modules/@tensorflow/tfjs/dist/tf.fesm.js";

console.log(tf.version);
  1. Install live server for vscode
  2. Run the server (right click on index.html)


So now you know how to include javascript modules in a browser, the import statement needs to include a URL or local server Filepath as understood by a browser.

This approach however is not really the most common one.

You could include the minified tfjs from the CDN:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <script src=" https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.8/dist/tf.min.js "></script>
  <body></body>
  <script src="index.js"></script>
</html>

And the simple js code:

// import * as tf from "./node_modules/@tensorflow/tfjs/dist/tf.fesm.js";

console.log(tf.version);
2 Likes

This worked! Thank you so much @Mah_Neh . I’m now able to use tfjs in my script but it looks like I’m facing the same problem with the model. When I try to make prediction I got this error: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading ‘length’). And I think it’s not normal to have a promise whose state is set to undefined. Have you any idea about this?

// import * as tf from "./node_modules/@tensorflow/tfjs/dist/tf.fesm.js";

console.log(tf.version);

async function loadModel(){
    const model =  await tf.loadLayersModel('https://diarray-hub.github.io/TouchPatternRecognition/Models/tfjs_model/model.json');
    return model
}

const data = preprocess(touchTracks);
const model = loadModel();

if (model) {
    console.log(model.state);
    model.then(function (res) {
        const prediction = res.predict([data]);
        console.log(prediction);
    }, function (err) {
        console.log(err);
    });
    console.log(model.state);
}  
else {
    console.log("model not loaded!")
}
1 Like

@diarray The best generate browser-enabled Javascript is with a Bundler.

They way it works is like this:

  1. Write your JS
  2. Pass it through a bundler
  3. Generates Browser enabled code.

There are many bundlers, I use vite because it is easy to use.

Minimal example for you on the web:


You may later on set up your laptop for this (it is simple) but I recommend starting with minimum overhead.

You can see I commented the code because that is useful for debugging, you simplify the program first.

1 Like

Thank you @Mah_Neh, so this mean that the model is probably properly loaded but in this case why its state is set to undefined. That’s my first experience with JavaScript but I thought JS promise is an object with two properties state and result and that the state property can have these three values:

  • Pending
  • Fulfilled
  • Rejected

Do you know why this model state is set to “undefined”? I think this is probably linked to the reason why I can’t make predictions with the model in tfjs

Promises also must be awaited, or use .then.

Now for using await, for the most part, the code needs to run inside a promise unless using top level await (see code below for most common usage):

async myCode(){

const result = await myPromise() // promises must be awaited or result would be undefined
// const result = myPromise() would yield undefined
}

myCode()

or

myPromise().then((result)=>{  
   /* do something with result */
 })

This is why I used .then in the code.

If you do this however

myPromise().then((result)=>{  
   /* do something with result */
 })

you cant await a new promise unless you create a new async function:

myPromise().then(async (result)=>{  

  const res =  await model.predict()
   
 })

Assuming that predict is async @diarray

async function predict(model, data){
const res = await model.predict([data]);
}

const data = preprocess(touchTracks);

loadModel()
.then((model) => {
const summary = document.createElement(‘p’);
document.body.append(summary);

document.body.style.backgroundColor = 'rgba(120,20,120,0.3)';
tfvis.show.modelSummary(summary, model);
predict(model, data).then((res)=> {
  console.log(res);
})

})
.catch((e) => {
console.log(‘There was an error with the Model’, e);
});

You mean something like this?

Maybe using await is simpler @diarray :

async function run(modelUrl, inputData){
    const model = await loadModel(modelUrl)
    const predictions = await model.predict([inputData]);
    console.log(predictions);
    // return predictions //return if needed.
  })

const data = preprocess(touchTracks);
run("urlHere", data)
  .catch((e) => {
    console.log(‘There was an error with the Model’, e);
  });

Basically the way I see it is, always wrap promises in async functions, and use await internally.

1 Like

Weirdly, all solutions I have been trying return the same error :joy:

Cannot read properties of undefined (reading ‘length’)

But looks like the model is indeed loading properly

Without seeing the data and full code passed to it’s difficult to help further

Also, the error should be different at least in that:

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading ‘length’).

Should not indicate it is uncaught

1 Like

Yaaaas, the error should not indicate this :smiling_face_with_tear:
Here is the full code if it can help. This is for one example:

touchTracks = [
    {
      "id": "6086896",
      "positions": [
        [
          309,
          729
        ],
        [
          305,
          727
        ],
        [
          291,
          726
        ],
        [
          281,
          724
        ],
        [
          269,
          722
        ],
        [
          225,
          721
        ],
        [
          193,
          721
        ],
        [
          164,
          725
        ],
        [
          139,
          729
        ],
        [
          117,
          730
        ],
        [
          99,
          730
        ],
        [
          90,
          730
        ],
        [
          81,
          728
        ],
        [
          72,
          724
        ],
        [
          64,
          717
        ],
        [
          59,
          710
        ],
        [
          55,
          700
        ],
        [
          54,
          688
        ],
        [
          54,
          675
        ],
        [
          54,
          663
        ],
        [
          61,
          650
        ],
        [
          68,
          644
        ],
        [
          79,
          638
        ],
        [
          96,
          631
        ],
        [
          116,
          626
        ],
        [
          137,
          623
        ],
        [
          158,
          623
        ],
        [
          179,
          623
        ],
        [
          197,
          623
        ],
        [
          216,
          623
        ],
        [
          233,
          623
        ],
        [
          249,
          623
        ],
        [
          263,
          622
        ],
        [
          277,
          622
        ],
        [
          292,
          621
        ],
        [
          302,
          619
        ],
        [
          310,
          616
        ],
        [
          317,
          614
        ],
        [
          321,
          610
        ],
        [
          324,
          606
        ],
        [
          327,
          600
        ],
        [
          329,
          591
        ],
        [
          330,
          580
        ],
        [
          331,
          567
        ],
        [
          331,
          556
        ],
        [
          331,
          546
        ],
        [
          329,
          538
        ],
        [
          326,
          530
        ],
        [
          316,
          520
        ],
        [
          305,
          514
        ],
        [
          288,
          509
        ],
        [
          265,
          507
        ],
        [
          239,
          507
        ],
        [
          208,
          509
        ],
        [
          175,
          518
        ],
        [
          143,
          524
        ],
        [
          115,
          530
        ],
        [
          88,
          537
        ],
        [
          66,
          542
        ],
        [
          50,
          545
        ],
        [
          40,
          547
        ],
        [
          34,
          548
        ],
        [
          33,
          548
        ],
        [
          32,
          548
        ]
      ],
      "speeds": [
        235.37786857485233,
        1169.6308906906222,
        1274.7333000709446,
        1351.7513622909792,
        2000.5096981010322,
        1882.3573762674782,
        1829.656438133306,
        1687.8533826318687,
        1295.4569078936731,
        1124.9977201269576,
        529.4130120752283,
        576.2203608368682,
        579.3459415943875,
        664.3827668857559,
        537.652251249597,
        673.1442367260271,
        708.3208278386463,
        764.707684108663,
        749.9984800846385,
        671.1333248892341,
        576.2203608368682,
        783.1211683434464,
        1149.0461908194786,
        1288.4678968506948,
        1325.822527868629,
        1400.01246145532,
        1312.4973401481172,
        1000.0062519040491,
        1187.4975934673441,
        1000.0023561420978,
        999.9979734461846,
        825.617945560423,
        823.5313521170217,
        683.3367485206559,
        679.8645137706516,
        533.9991519021338,
        428.2427671914062,
        353.5526740983011,
        312.4993667019327,
        419.26189612269343,
        576.2289472998955,
        690.3336645730468,
        814.8986492110176,
        647.0603480919457,
        588.236680083587,
        515.387158740278,
        502.5825912291078,
        642.8291394667752,
        737.058447559855,
        1181.3280813614533,
        1442.9216234126523,
        1624.99670685005,
        1941.5241362650136,
        2137.8564461144792,
        2034.8484507742146,
        1789.7240059301812,
        1640.748063588988,
        1410.0614140136802,
        957.5799383126009,
        599.8860620714491,
        276.4882710580324,
        62.499873340386536,
        62.499873340386536
      ],
      "directions": [
        -2.677945044588987,
        -3.070285188804503,
        -2.9441970937399127,
        -2.976443976175166,
        -3.118869292748152,
        3.141592653589793,
        3.0045264792796056,
        2.9829373914033916,
        3.096169374168216,
        3.141592653589793,
        3.141592653589793,
        -2.922923707715851,
        -2.723368324010564,
        -2.4227626539681686,
        -2.191045812777718,
        -1.9513027039072615,
        -1.6539375586833378,
        -1.5707963267948966,
        -1.5707963267948966,
        -1.0768549578753155,
        -0.7086262721276703,
        -0.4993467216801301,
        -0.3906070436976868,
        -0.24497866312686414,
        -0.14189705460416394,
        0,
        0,
        0,
        0,
        0,
        0,
        -0.07130746478529032,
        0,
        -0.06656816377582381,
        -0.19739555984988075,
        -0.35877067027057225,
        -0.27829965900511133,
        -0.7853981633974483,
        -0.9272952180016122,
        -1.1071487177940904,
        -1.3521273809209546,
        -1.4801364395941514,
        -1.4940244355251187,
        -1.5707963267948966,
        -1.5707963267948966,
        -1.8157749899217608,
        -1.9295669970654687,
        -2.356194490192345,
        -2.642245931909663,
        -2.8555412118724752,
        -3.054854314913808,
        3.141592653589793,
        3.0771658141642395,
        2.875340604438868,
        2.9562447035940984,
        2.930499320367047,
        2.8879185574511506,
        2.91811605244916,
        2.9562447035940984,
        2.9441970937399127,
        2.976443976175166,
        3.141592653589793,
        3.141592653589793
      ],
      "startTimestamp": 1683568365.091,
      "endTimestamp": 1683568366.16
    }
  ]

  function preprocess(touchTrackingArray){
    var touch = touchTrackingArray[0],
        positionX = [],
        positionY = [],
        speeds = touch.speeds,
        directions = touch.directions,
        latency = touch.endTimestamp - touch.startTimestamp
    touch.positions.forEach(position => {
        positionX.push(position[0])
        positionY.push(position[1])
    });
    fields = [positionX, positionY, speeds, directions];
    // Calculate the features
    const features = fields.map(field => {
        const mean = field.reduce((a, b) => a + b) / field.length;
        const stdDev = Math.sqrt(field.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / field.length);
        const min = Math.min(...field);
        const max = Math.max(...field);
        const range = max - min;
        return [mean, stdDev, min, max, range];
    });
    
    // Flatten the features into a single list
    const flattenedFeatures = features.reduce((acc, val) => acc.concat(val), []);
    flattenedFeatures.push(latency);
    return flattenedFeatures
}

/*async function loadModel(modelUrl) {
  const model = await tf.loadLayersModel(modelUrl);
  return model;
}*/

async function run(modelUrl, inputData){
  const model = await tf.loadLayersModel(modelUrl);
  const predictions = await model.predict([inputData]);
  console.log(predictions);
  // return predictions //return if needed.
}

const data = preprocess(touchTracks);
run('https://diarray-hub.github.io/TouchPatternRecognition/Models/tfjs_model/model.json', data)
.catch((e) => {
  console.log("There was an error with the Model", e);
});
  
// const data = preprocess(touchTracks);
// const model = loadModel();

Whatever thank you so much for all your support @Mah_Neh. I appreciate :handshake:

1 Like

Is this any better

By the way, if you are translating the code using GPTs, I’d recommend to do to Typescript, because it is easier to debug @diarray .

1 Like

I can’t thank you enough @Mah_Neh. I’m struggling this since one week :joy: . Thanks.

1 Like