diff --git a/examples/neuralNetwork-sequence-hand-gesture-load-model/index.html b/examples/neuralNetwork-sequence-hand-gesture-load-model/index.html
new file mode 100644
index 00000000..ec78e95b
--- /dev/null
+++ b/examples/neuralNetwork-sequence-hand-gesture-load-model/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ ml5.js neuralNetwork Hand Gesture Loading Pre-trained Model Example
+
+
+
+
+
+
+
+
+ How to sign: Hello & Goodbye in ASL.
+
+
+
diff --git a/examples/neuralNetwork-sequence-hand-gesture-load-model/model/model.json b/examples/neuralNetwork-sequence-hand-gesture-load-model/model/model.json
new file mode 100644
index 00000000..136bcf90
--- /dev/null
+++ b/examples/neuralNetwork-sequence-hand-gesture-load-model/model/model.json
@@ -0,0 +1 @@
+{"modelTopology":{"class_name":"Sequential","config":{"name":"sequential_1","layers":[{"class_name":"Conv1D","config":{"filters":8,"kernel_initializer":{"class_name":"VarianceScaling","config":{"scale":1,"mode":"fan_avg","distribution":"normal","seed":null}},"kernel_regularizer":null,"kernel_constraint":null,"kernel_size":[3],"strides":[1],"padding":"valid","dilation_rate":[1],"activation":"relu","use_bias":true,"bias_initializer":{"class_name":"Zeros","config":{}},"bias_regularizer":null,"activity_regularizer":null,"bias_constraint":null,"name":"conv1d_Conv1D1","trainable":true,"batch_input_shape":[null,30,84],"dtype":"float32"}},{"class_name":"MaxPooling1D","config":{"pool_size":[2],"padding":"valid","strides":[2],"name":"max_pooling1d_MaxPooling1D1","trainable":true}},{"class_name":"Conv1D","config":{"filters":16,"kernel_initializer":{"class_name":"VarianceScaling","config":{"scale":1,"mode":"fan_avg","distribution":"normal","seed":null}},"kernel_regularizer":null,"kernel_constraint":null,"kernel_size":[3],"strides":[1],"padding":"valid","dilation_rate":[1],"activation":"relu","use_bias":true,"bias_initializer":{"class_name":"Zeros","config":{}},"bias_regularizer":null,"activity_regularizer":null,"bias_constraint":null,"name":"conv1d_Conv1D2","trainable":true}},{"class_name":"MaxPooling1D","config":{"pool_size":[2],"padding":"valid","strides":[2],"name":"max_pooling1d_MaxPooling1D2","trainable":true}},{"class_name":"Flatten","config":{"name":"flatten_Flatten1","trainable":true}},{"class_name":"Dense","config":{"units":16,"activation":"relu","use_bias":true,"kernel_initializer":{"class_name":"VarianceScaling","config":{"scale":1,"mode":"fan_avg","distribution":"normal","seed":null}},"bias_initializer":{"class_name":"Zeros","config":{}},"kernel_regularizer":null,"bias_regularizer":null,"activity_regularizer":null,"kernel_constraint":null,"bias_constraint":null,"name":"dense_Dense1","trainable":true}},{"class_name":"Dense","config":{"units":2,"activation":"softmax","use_bias":true,"kernel_initializer":{"class_name":"VarianceScaling","config":{"scale":1,"mode":"fan_avg","distribution":"normal","seed":null}},"bias_initializer":{"class_name":"Zeros","config":{}},"kernel_regularizer":null,"bias_regularizer":null,"activity_regularizer":null,"kernel_constraint":null,"bias_constraint":null,"name":"dense_Dense2","trainable":true}}]},"keras_version":"tfjs-layers 4.22.0","backend":"tensor_flow.js"},"weightsManifest":[{"paths":["./model.weights.bin"],"weights":[{"name":"conv1d_Conv1D1/kernel","shape":[3,84,8],"dtype":"float32"},{"name":"conv1d_Conv1D1/bias","shape":[8],"dtype":"float32"},{"name":"conv1d_Conv1D2/kernel","shape":[3,8,16],"dtype":"float32"},{"name":"conv1d_Conv1D2/bias","shape":[16],"dtype":"float32"},{"name":"dense_Dense1/kernel","shape":[96,16],"dtype":"float32"},{"name":"dense_Dense1/bias","shape":[16],"dtype":"float32"},{"name":"dense_Dense2/kernel","shape":[16,2],"dtype":"float32"},{"name":"dense_Dense2/bias","shape":[2],"dtype":"float32"}]}]}
\ No newline at end of file
diff --git a/examples/neuralNetwork-sequence-hand-gesture-load-model/model/model.weights.bin b/examples/neuralNetwork-sequence-hand-gesture-load-model/model/model.weights.bin
new file mode 100644
index 00000000..1b38b79e
Binary files /dev/null and b/examples/neuralNetwork-sequence-hand-gesture-load-model/model/model.weights.bin differ
diff --git a/examples/neuralNetwork-sequence-hand-gesture-load-model/model/model_meta.json b/examples/neuralNetwork-sequence-hand-gesture-load-model/model/model_meta.json
new file mode 100644
index 00000000..528797af
--- /dev/null
+++ b/examples/neuralNetwork-sequence-hand-gesture-load-model/model/model_meta.json
@@ -0,0 +1,105 @@
+{
+ "inputUnits": [84],
+ "outputUnits": 2,
+ "inputs": {
+ "label_0": { "dtype": "number", "min": 0, "max": 479.19925350185474 },
+ "label_1": { "dtype": "number", "min": 0, "max": 428.55538772969044 },
+ "label_2": { "dtype": "number", "min": 0, "max": 493.1563637363407 },
+ "label_3": { "dtype": "number", "min": 0, "max": 427.56692302194773 },
+ "label_4": { "dtype": "number", "min": 0, "max": 499.91689098126307 },
+ "label_5": { "dtype": "number", "min": 0, "max": 430.02650947613336 },
+ "label_6": { "dtype": "number", "min": 0, "max": 496.3208721418956 },
+ "label_7": { "dtype": "number", "min": 0, "max": 441.4487064271474 },
+ "label_8": { "dtype": "number", "min": 0, "max": 491.1078639783216 },
+ "label_9": { "dtype": "number", "min": 0, "max": 449.2829368661615 },
+ "label_10": { "dtype": "number", "min": 0, "max": 509.81763121840737 },
+ "label_11": { "dtype": "number", "min": 0, "max": 427.9468311472154 },
+ "label_12": { "dtype": "number", "min": 0, "max": 519.7167764450397 },
+ "label_13": { "dtype": "number", "min": 0, "max": 449.2054737819238 },
+ "label_14": { "dtype": "number", "min": 0, "max": 518.5199748098399 },
+ "label_15": { "dtype": "number", "min": 0, "max": 463.8811725137893 },
+ "label_16": { "dtype": "number", "min": 0, "max": 514.2732060805772 },
+ "label_17": { "dtype": "number", "min": 0, "max": 475.8006861523057 },
+ "label_18": { "dtype": "number", "min": 0, "max": 499.0736746691703 },
+ "label_19": { "dtype": "number", "min": 0, "max": 435.18154671948156 },
+ "label_20": { "dtype": "number", "min": 0, "max": 503.95906901514456 },
+ "label_21": { "dtype": "number", "min": 0, "max": 453.42442120690896 },
+ "label_22": { "dtype": "number", "min": 0, "max": 500.483162019238 },
+ "label_23": { "dtype": "number", "min": 0, "max": 468.18940999515536 },
+ "label_24": { "dtype": "number", "min": 0, "max": 495.75003969620434 },
+ "label_25": { "dtype": "number", "min": 0, "max": 480.72524952103413 },
+ "label_26": { "dtype": "number", "min": 0, "max": 483.45096761731435 },
+ "label_27": { "dtype": "number", "min": 0, "max": 440.95834852166774 },
+ "label_28": { "dtype": "number", "min": 0, "max": 473.19789907159645 },
+ "label_29": { "dtype": "number", "min": 0, "max": 458.5087523020914 },
+ "label_30": { "dtype": "number", "min": 0, "max": 467.93109224233444 },
+ "label_31": { "dtype": "number", "min": 0, "max": 471.2808751940178 },
+ "label_32": { "dtype": "number", "min": 0, "max": 465.7011008815248 },
+ "label_33": { "dtype": "number", "min": 0, "max": 481.9448468282673 },
+ "label_34": { "dtype": "number", "min": 0, "max": 464.5309672255035 },
+ "label_35": { "dtype": "number", "min": 0, "max": 444.791971246973 },
+ "label_36": { "dtype": "number", "min": 0, "max": 451.3378977170124 },
+ "label_37": { "dtype": "number", "min": 0, "max": 459.57942119350326 },
+ "label_38": { "dtype": "number", "min": 0, "max": 451.17016603053224 },
+ "label_39": { "dtype": "number", "min": 0, "max": 470.1975592491152 },
+ "label_40": {
+ "dtype": "number",
+ "min": -2.250544608200542,
+ "max": 455.08975496996834
+ },
+ "label_41": { "dtype": "number", "min": 0, "max": 478.7628390581024 },
+ "label_42": { "dtype": "number", "min": 0, "max": 586.6608405854927 },
+ "label_43": { "dtype": "number", "min": 0, "max": 443.59360893451384 },
+ "label_44": { "dtype": "number", "min": 0, "max": 575.2940255551233 },
+ "label_45": { "dtype": "number", "min": 0, "max": 439.4650023423489 },
+ "label_46": { "dtype": "number", "min": 0, "max": 572.6978570905426 },
+ "label_47": { "dtype": "number", "min": 0, "max": 445.77737640752645 },
+ "label_48": { "dtype": "number", "min": 0, "max": 577.5610386619726 },
+ "label_49": { "dtype": "number", "min": 0, "max": 455.3685328800494 },
+ "label_50": { "dtype": "number", "min": 0, "max": 587.5799889243797 },
+ "label_51": { "dtype": "number", "min": 0, "max": 460.6591159738472 },
+ "label_52": { "dtype": "number", "min": 0, "max": 595.3738279672924 },
+ "label_53": { "dtype": "number", "min": 0, "max": 435.3837420626638 },
+ "label_54": { "dtype": "number", "min": 0, "max": 603.0984481232397 },
+ "label_55": { "dtype": "number", "min": 0, "max": 458.47111655979 },
+ "label_56": { "dtype": "number", "min": 0, "max": 610.7793182445314 },
+ "label_57": { "dtype": "number", "min": 0, "max": 475.95085629845164 },
+ "label_58": { "dtype": "number", "min": 0, "max": 618.5882608713695 },
+ "label_59": { "dtype": "number", "min": 0, "max": 489.3053477098461 },
+ "label_60": { "dtype": "number", "min": 0, "max": 610.2477703388294 },
+ "label_61": { "dtype": "number", "min": 0, "max": 439.0911629675826 },
+ "label_62": { "dtype": "number", "min": 0, "max": 620.111633978391 },
+ "label_63": { "dtype": "number", "min": 0, "max": 465.4726266037314 },
+ "label_64": { "dtype": "number", "min": 0, "max": 626.9405786817175 },
+ "label_65": { "dtype": "number", "min": 0, "max": 484.92308751687654 },
+ "label_66": { "dtype": "number", "min": 0, "max": 633.3462004686493 },
+ "label_67": { "dtype": "number", "min": 0, "max": 497.7592120455144 },
+ "label_68": { "dtype": "number", "min": 0, "max": 622.5964250264554 },
+ "label_69": { "dtype": "number", "min": 0, "max": 443.22402036828703 },
+ "label_70": { "dtype": "number", "min": 0, "max": 634.5536612109757 },
+ "label_71": { "dtype": "number", "min": 0, "max": 470.2350331722456 },
+ "label_72": { "dtype": "number", "min": 0, "max": 642.2073345193608 },
+ "label_73": { "dtype": "number", "min": 0, "max": 487.5833155606672 },
+ "label_74": { "dtype": "number", "min": 0, "max": 648.6144713693823 },
+ "label_75": { "dtype": "number", "min": 0, "max": 498.5309450346654 },
+ "label_76": { "dtype": "number", "min": 0, "max": 632.6189459192858 },
+ "label_77": { "dtype": "number", "min": 0, "max": 447.8460008763578 },
+ "label_78": { "dtype": "number", "min": 0, "max": 644.54236495545 },
+ "label_79": { "dtype": "number", "min": 0, "max": 472.5026761828674 },
+ "label_80": { "dtype": "number", "min": 0, "max": 651.8202599792932 },
+ "label_81": { "dtype": "number", "min": 0, "max": 486.6449571135753 },
+ "label_82": { "dtype": "number", "min": 0, "max": 656.9313671078949 },
+ "label_83": { "dtype": "number", "min": 0, "max": 497.0423817040591 }
+ },
+ "outputs": {
+ "label": {
+ "dtype": "string",
+ "min": 0,
+ "max": 1,
+ "uniqueValues": ["Hello", "Good Bye"],
+ "legend": { "Hello": [1, 0], "Good Bye": [0, 1] }
+ }
+ },
+ "isNormalized": true,
+ "seriesShape": [30, 84]
+}
diff --git a/examples/neuralNetwork-sequence-hand-gesture-load-model/sketch.js b/examples/neuralNetwork-sequence-hand-gesture-load-model/sketch.js
new file mode 100644
index 00000000..2d0dfd39
--- /dev/null
+++ b/examples/neuralNetwork-sequence-hand-gesture-load-model/sketch.js
@@ -0,0 +1,158 @@
+/*
+ * 👋 Hello! This is an ml5.js example made and shared with ❤️.
+ * Learn more about the ml5.js project: https://ml5js.org/
+ * ml5.js license and Code of Conduct: https://github.com/ml5js/ml5-next-gen/blob/main/LICENSE.md
+ *
+ * This example demonstrates loading a hand gesture classifier through
+ * ml5.neuralNetwork with the sequenceClassificationWithCNN task.
+ * This example has been trained with the ASL gestures for Hello and Goodbye.
+ *
+ * Reference to sign hello and goodbye in ASL:
+ * Hello: https://www.signasl.org/sign/hello
+ * Goodbye: https://www.signasl.org/sign/goodbye
+ */
+
+let video;
+let handPose;
+let hands = [];
+let model;
+let isModelLoaded = false;
+
+let sequence = [];
+let sequenceLength = 30;
+let curGesture;
+
+function preload() {
+ // load the handPose model
+ handPose = ml5.handPose({ flipHorizontal: true });
+}
+
+function setup() {
+ let canvas = createCanvas(640, 480);
+ canvas.parent("canvasDiv");
+
+ video = createCapture(VIDEO, { flipped: true });
+ video.size(640, 480);
+ video.hide();
+ handPose.detectStart(video, gotHands);
+
+ let options = {
+ task: "sequenceClassificationWithCNN",
+ };
+ model = ml5.neuralNetwork(options);
+
+ // setup the model files to load
+ let modelDetails = {
+ model: "model/model.json",
+ metadata: "model/model_meta.json",
+ weights: "model/model.weights.bin",
+ };
+
+ // load the model and call modelLoaded once finished
+ model.load(modelDetails, modelLoaded);
+}
+
+function modelLoaded() {
+ console.log("Model loaded");
+ isModelLoaded = true;
+}
+
+function draw() {
+ image(video, 0, 0, width, height);
+ drawHands();
+ textSize(16);
+ stroke(0);
+ fill(255);
+
+ if (hands.length > 0) {
+ // hands in frame, add their keypoints to the sequence (input)
+ let handpoints = getKeypoints(["Left", "Right"]);
+ sequence.push(handpoints);
+ text(
+ "Move your hand(s) out of the frame after finishing the gesture",
+ 50,
+ 50
+ );
+ } else if (sequence.length > 0) {
+ // hands moved out of the frame, end of sequence
+
+ // Sequence will have varying length at this point, depending on
+ // how long the hands were in frame - a line simplification algorithm
+ // (RDP) turns it into the fixed length the NN can work with.
+ // For more information about RDP, see:
+ // https://www.youtube.com/watch?v=ZCXkvwLxBrA
+ let inputs = model.setFixedLength(sequence, sequenceLength);
+
+ // start the classification
+ if (isModelLoaded) {
+ model.classify(inputs, gotResults);
+ }
+ // reset the sequence
+ sequence = [];
+ text("Classifying...", 50, 50);
+ } else if (curGesture == null) {
+ // on program start
+ text("Move your hand(s) into the frame to sign a gesture", 50, 50);
+ } else {
+ // after receiving a classification
+ text('Saw "' + curGesture + '"', 50, 50);
+ }
+}
+
+// callback function for when the classification fininished
+function gotResults(results, error) {
+ if (error) {
+ console.error(error);
+ return;
+ }
+ curGesture = results[0].label;
+}
+
+// callback function for when handPose outputs data
+function gotHands(results) {
+ hands = results;
+}
+
+function drawHands() {
+ for (let i = 0; i < hands.length; i++) {
+ let hand = hands[i];
+ for (let j = 0; j < hand.keypoints.length; j++) {
+ let keypoint = hand.keypoints[j];
+ fill(0, 255, 0);
+ noStroke();
+ circle(keypoint.x, keypoint.y, 5);
+ }
+ }
+}
+
+// Return the tracked hand points as flattened array of 84 numbers
+// for use as input to the neural network
+
+function getKeypoints(whichHands = ["Left", "Right"]) {
+ let keypoints = [];
+ // look for the left and right hand
+ for (let whichHand of whichHands) {
+ let found = false;
+ for (let i = 0; i < hands.length; i++) {
+ let hand = hands[i];
+ if (hand.handedness == whichHand) {
+ // and add the x and y numbers of each tracked keypoint
+ // to the array
+ for (let j = 0; j < hand.keypoints.length; j++) {
+ let keypoint = hand.keypoints[j];
+ keypoints.push(keypoint.x, keypoint.y);
+ }
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // if we don't find a right or a left hand, add 42 zeros
+ // to the keypoints array instead
+ for (let j = 0; j < 42; j++) {
+ keypoints.push(0);
+ }
+ }
+ }
+ return keypoints;
+}
diff --git a/examples/neuralNetwork-sequence-hand-gesture/index.html b/examples/neuralNetwork-sequence-hand-gesture/index.html
new file mode 100644
index 00000000..8602322c
--- /dev/null
+++ b/examples/neuralNetwork-sequence-hand-gesture/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+ ml5.js neuralNetwork Hand Gesture Training and Saving Example
+
+
+
+
+
+
+
+
+
diff --git a/examples/neuralNetwork-sequence-hand-gesture/sketch.js b/examples/neuralNetwork-sequence-hand-gesture/sketch.js
new file mode 100644
index 00000000..c9a162be
--- /dev/null
+++ b/examples/neuralNetwork-sequence-hand-gesture/sketch.js
@@ -0,0 +1,200 @@
+/*
+ * 👋 Hello! This is an ml5.js example made and shared with ❤️.
+ * Learn more about the ml5.js project: https://ml5js.org/
+ * ml5.js license and Code of Conduct: https://github.com/ml5js/ml5-next-gen/blob/main/LICENSE.md
+ *
+ * This example demonstrates training a hand gesture classifier through
+ * ml5.neuralNetwork with the sequenceClassificationWithCNN task.
+ */
+
+let video;
+let handPose;
+let hands = [];
+let model;
+
+let state = "training";
+let sequence = [];
+let sequenceLength = 30;
+let gestures = ["Gesture #1", "Gesture #2"];
+let counts = { "Gesture #1": 0, "Gesture #2": 0 };
+let curGesture = gestures[0];
+
+let gesture1Button;
+let gesture2Button;
+let trainButton;
+
+function preload() {
+ // load the handPose model
+ handPose = ml5.handPose({ flipHorizontal: true });
+}
+
+function setup() {
+ let canvas = createCanvas(640, 480);
+ canvas.parent("canvasDiv");
+
+ video = createCapture(VIDEO, { flipped: true });
+ video.size(640, 480);
+ video.hide();
+ handPose.detectStart(video, gotHands);
+
+ let options = {
+ outputs: ["label"],
+ task: "sequenceClassificationWithCNN",
+ debug: true,
+ learningRate: 0.001, // the default learning rate of 0.01 didn't converge for this use case (0.001 makes smaller steps each epoch)
+ };
+ model = ml5.neuralNetwork(options);
+
+ // setup the UI buttons for training
+ gesture1Button = createButton("Start recording " + gestures[0]);
+ gesture1Button.mousePressed(recordGesture1);
+ gesture2Button = createButton("Start recording " + gestures[1]);
+ gesture2Button.mousePressed(recordGesture2);
+ trainButton = createButton("Train and Save Model");
+ trainButton.mousePressed(trainModel);
+}
+
+function draw() {
+ image(video, 0, 0, width, height);
+ drawHands();
+
+ if (hands.length > 0) {
+ // hands in frame, add their keypoints to the sequence
+ let handpoints = getKeypoints(["Left", "Right"]);
+ sequence.push(handpoints);
+
+ // This uses the RDP line simplification algorithm to make sure each
+ // input to the neural network has the same number of points.
+ // For more information about RDP, see:
+ // https://www.youtube.com/watch?v=ZCXkvwLxBrA
+
+ let rdp = model.setFixedLength(sequence, sequenceLength);
+ for (let i = 0; i < rdp.length - 1; i++) {
+ for (let j = 0; j < rdp[i].length; j += 2) {
+ stroke(255, 0, 0);
+ // hide lines to 0,0
+ if (rdp[i][j] != 0 && rdp[i + 1][j] != 0) {
+ line(rdp[i][j], rdp[i][j + 1], rdp[i + 1][j], rdp[i + 1][j + 1]);
+ }
+ }
+ }
+ } else if (sequence.length > 0) {
+ // hands moved out of the frame, end of sequence
+ let inputs = model.setFixedLength(sequence, sequenceLength);
+
+ if (state == "training") {
+ let outputs = { label: curGesture };
+ model.addData(inputs, outputs);
+ counts[curGesture]++;
+ } else if (state == "classifying") {
+ model.classify(inputs, gotResults);
+ }
+ // reset the sequence
+ sequence = [];
+ }
+
+ // display current state
+ textSize(16);
+ stroke(0);
+ fill(255);
+ if (state == "training" && sequence.length == 0) {
+ text("Move your hand(s) into the frame to record " + curGesture, 50, 50);
+ } else if (state == "training") {
+ text("Move your hand(s) out of the frame to finish " + curGesture, 50, 50);
+ } else if (state == "classifying" && curGesture == null) {
+ text("Try a trained gesture", 50, 50);
+ } else if (state == "classifying" && curGesture) {
+ text("Saw " + curGesture, 50, 50);
+ }
+
+ // show how many times each gesture was recorded
+ if (state == "training") {
+ for (let i = 0; i < gestures.length; i++) {
+ text(
+ gestures[i] + ": " + counts[gestures[i]],
+ 50,
+ height - 50 - (gestures.length - i - 1) * 20
+ );
+ }
+ }
+}
+
+function trainModel() {
+ model.normalizeData();
+ let options = {
+ epochs: 50,
+ };
+ model.train(options, finishedTraining);
+
+ gesture1Button.attribute("disabled", true);
+ gesture2Button.attribute("disabled", true);
+ trainButton.attribute("disabled", true);
+}
+
+function finishedTraining() {
+ state = "classifying";
+ model.save();
+ curGesture = null;
+}
+
+// callback function for when the classification fininished
+function gotResults(results) {
+ curGesture = results[0].label;
+}
+
+// callback function for when handPose outputs data
+function gotHands(results) {
+ hands = results;
+}
+
+function drawHands() {
+ for (let i = 0; i < hands.length; i++) {
+ let hand = hands[i];
+ for (let j = 0; j < hand.keypoints.length; j++) {
+ let keypoint = hand.keypoints[j];
+ fill(0, 255, 0);
+ noStroke();
+ circle(keypoint.x, keypoint.y, 5);
+ }
+ }
+}
+
+// Return the tracked hand points as flattened array of 84 numbers
+// for use as input to the neural network
+
+function getKeypoints(whichHands = ["Left", "Right"]) {
+ let keypoints = [];
+ // look for the left and right hand
+ for (let whichHand of whichHands) {
+ let found = false;
+ for (let i = 0; i < hands.length; i++) {
+ let hand = hands[i];
+ if (hand.handedness == whichHand) {
+ // and add the x and y numbers of each tracked keypoint
+ // to the array
+ for (let j = 0; j < hand.keypoints.length; j++) {
+ let keypoint = hand.keypoints[j];
+ keypoints.push(keypoint.x, keypoint.y);
+ }
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // if we don't find a right or a left hand, add 42 zeros
+ // to the keypoints array instead
+ for (let j = 0; j < 42; j++) {
+ keypoints.push(0);
+ }
+ }
+ }
+ return keypoints;
+}
+
+function recordGesture1() {
+ curGesture = gestures[0];
+}
+
+function recordGesture2() {
+ curGesture = gestures[1];
+}
diff --git a/examples/neuralNetwork-sequence-mouse-gesture-rdp/index.html b/examples/neuralNetwork-sequence-mouse-gesture-rdp/index.html
new file mode 100644
index 00000000..aa7b2a8c
--- /dev/null
+++ b/examples/neuralNetwork-sequence-mouse-gesture-rdp/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ ml5.js neuralNetwork Gesture Classifier Example with sequenceClassificationWithCNN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/neuralNetwork-sequence-mouse-gesture-rdp/sketch.js b/examples/neuralNetwork-sequence-mouse-gesture-rdp/sketch.js
new file mode 100644
index 00000000..069a620c
--- /dev/null
+++ b/examples/neuralNetwork-sequence-mouse-gesture-rdp/sketch.js
@@ -0,0 +1,121 @@
+/*
+ * 👋 Hello! This is an ml5.js example made and shared with ❤️.
+ * Learn more about the ml5.js project: https://ml5js.org/
+ * ml5.js license and Code of Conduct: https://github.com/ml5js/ml5-next-gen/blob/main/LICENSE.md
+ *
+ * This example demonstrates how to train your own mouse gesture classifier
+ * through ml5.neuralNetwork with the sequenceClassificationWithCNN task.
+ */
+
+let model;
+
+let state = "training";
+let curShape = "circle";
+let sequence = [];
+let sequenceLength = 30;
+
+function setup() {
+ let canvas = createCanvas(600, 400);
+ canvas.parent("canvasDiv");
+
+ let options = {
+ inputs: ["x", "y"],
+ outputs: ["label"],
+ task: "sequenceClassificationWithCNN",
+ debug: true,
+ learningRate: 0.005, // smaller learning rate converged better
+ };
+ model = ml5.neuralNetwork(options);
+
+ select("#collCirclesBtn").mouseClicked(collectCircles);
+ select("#collSquaresBtn").mouseClicked(collectSquares);
+ select("#trainBtn").mouseClicked(trainModel);
+}
+
+function draw() {
+ background(220);
+
+ for (let i = 0; i < sequence.length - 1; i++) {
+ line(sequence[i].x, sequence[i].y, sequence[i + 1].x, sequence[i + 1].y);
+ }
+
+ // Sequence will have varying length at this point, depending on
+ // how long the hands were in frame - a line simplification algorithm
+ // (RDP) turns it into the fixed length the NN can work with.
+ // For more information about RDP, see:
+ // https://www.youtube.com/watch?v=ZCXkvwLxBrA
+
+ let rdp = model.setFixedLength(sequence, sequenceLength);
+ for (let i = 0; i < rdp.length; i++) {
+ fill(255);
+ rect(rdp[i].x - 3, rdp[i].y - 3, 6, 6);
+ }
+
+ // display current state
+ textSize(16);
+ fill(0);
+ if (state == "training") {
+ text("Now collecting " + curShape + "s", 50, 50);
+ } else if (state == "classifying" && curShape == null) {
+ text("Training finished. Draw again to see the classification in action.", 50, 50);
+ } else if (state == "classifying") {
+ text("Saw a " + curShape, 50, 50);
+ }
+}
+
+function mousePressed() {
+ if (mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height) {
+ sequence.push({ x: mouseX, y: mouseY });
+ }
+}
+
+function mouseDragged() {
+ if (mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height) {
+ sequence.push({ x: mouseX, y: mouseY });
+ }
+}
+
+function mouseReleased() {
+ if (sequence.length == 0) return;
+
+ if (state == "training") {
+ let inputs = model.setFixedLength(sequence, sequenceLength);
+ let outputs = { label: curShape };
+ model.addData(inputs, outputs);
+ } else if (state == "classifying") {
+ let inputs = model.setFixedLength(sequence, sequenceLength);
+ model.classify(inputs, gotResults);
+ }
+ // reset the sequence
+ sequence = [];
+}
+
+function trainModel() {
+ model.normalizeData();
+
+ let options = {
+ epochs: 40,
+ };
+ model.train(options, finishedTraining);
+
+ select("#collCirclesBtn").attribute("disabled", true);
+ select("#collSquaresBtn").attribute("disabled", true);
+ select("#trainBtn").attribute("disabled", true);
+}
+
+function finishedTraining() {
+ state = "classifying";
+ curShape = null;
+}
+
+function gotResults(results) {
+ curShape = results[0].label;
+}
+
+function collectCircles() {
+ curShape = "circle";
+}
+
+function collectSquares() {
+ curShape = "square";
+}
diff --git a/examples/neuralNetwork-sequence-weather-prediction/index.html b/examples/neuralNetwork-sequence-weather-prediction/index.html
new file mode 100644
index 00000000..402d054e
--- /dev/null
+++ b/examples/neuralNetwork-sequence-weather-prediction/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+ ml5.js neuralNetwork Weather Prediction Example
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/neuralNetwork-sequence-weather-prediction/sketch.js b/examples/neuralNetwork-sequence-weather-prediction/sketch.js
new file mode 100644
index 00000000..8d7833b7
--- /dev/null
+++ b/examples/neuralNetwork-sequence-weather-prediction/sketch.js
@@ -0,0 +1,145 @@
+/*
+ * 👋 Hello! This is an ml5.js example made and shared with ❤️.
+ * Learn more about the ml5.js project: https://ml5js.org/
+ * ml5.js license and Code of Conduct: https://github.com/ml5js/ml5-next-gen/blob/main/LICENSE.md
+ *
+ * This example demonstrates loading JSON data and training a Weather Predictor
+ * through ml5.neuralNetwork with the sequenceRegression task.
+ * XXX: let us know what this dataset is from/about
+ */
+
+let model;
+let data;
+let features = [
+ "temperature",
+ "humidity",
+ "wind_speed",
+ "pressure",
+ "precipitation",
+];
+let sequenceLength = 10; // NN looks at 10 data points at a time
+
+let state = "training";
+let predictedRain = 0;
+let maxBars = 12;
+let graphValues = [];
+
+function preload() {
+ data = loadJSON("weather_data.json");
+}
+
+function setup() {
+ let canvas = createCanvas(640, 400);
+ canvas.parent("canvasDiv");
+
+ let options = {
+ task: "sequenceRegression",
+ debug: true,
+ learningRate: 0.0075, // smaller learning rate helps here
+ inputs: features,
+ outputs: features,
+ };
+ model = ml5.neuralNetwork(options);
+
+ // the JSON file has the actual data in a "data" property
+ data = data.data;
+
+ // step through the data (in a "sliding window" way) and break it into
+ // sequences of 10 data points, which is what the NN will see at a time
+ for (let i = 0; i <= data.length - sequenceLength - 1; i++) {
+ let inputs = data.slice(i, i + sequenceLength);
+ let outputs = data[i + sequenceLength];
+
+ console.log("Sequence for training", inputs, outputs);
+ model.addData(inputs, outputs);
+ }
+ model.normalizeData();
+
+ // train right away
+ let trainOptions = {
+ epochs: 70,
+ };
+ model.train(trainOptions, finishedTraining);
+
+ let predictBtn = select("#predictBtn");
+ predictBtn.center();
+ predictBtn.mouseClicked(predictData);
+}
+
+function draw() {
+ background(220);
+ noStroke();
+ textSize(20);
+ textAlign(CENTER);
+
+ if (state == "training") {
+ text("Training...", width / 2, 160);
+ } else if (state == "predicting") {
+ text("Predicted rain: " + nf(predictedRain, 0, 1) + "mm", 320, 160);
+ push();
+ textSize(predictedRain * 5 + 10);
+ text("🌧️", width / 2, 100);
+ pop();
+ }
+ drawBarGraph();
+}
+
+function finishedTraining() {
+ state = "predicting";
+}
+
+function predictData() {
+ // take the last 10 data points to predict the next
+ let inputs = data.slice(-sequenceLength);
+ console.log("Sequence for prediction", inputs);
+ model.predict(inputs, gotResults);
+}
+
+function gotResults(results) {
+ predictedRain = results[4].value;
+
+ // add result to the bar graph
+ graphValues.push(results[4].value);
+ if (graphValues.length > maxBars) {
+ graphValues.shift();
+ }
+
+ // optional: add predicted result to the dataset, so that it will be
+ // considered in further predictions (going forward in time)
+ addNewData(results);
+}
+
+function addNewData(newValues) {
+ data.push({
+ date: "One more (synthetic) hour",
+ temperature: newValues[0].value,
+ humidity: newValues[1].value,
+ wind_speed: newValues[2].value,
+ pressure: newValues[3].value,
+ precipitation: newValues[4].value,
+ });
+}
+
+function drawBarGraph() {
+ let barWidth = width / maxBars;
+ let maxDataValue = 78;
+
+ let dryColor = color(235, 242, 255);
+ let wetColor = color(0, 80, 255);
+
+ for (let i = 0; i < graphValues.length && i < maxBars; i++) {
+ let barHeight = map(graphValues[i], 0, maxDataValue, 0, height - 180);
+ let x = i * barWidth;
+ let y = height - barHeight - 20;
+
+ // interpolate color based on rainfall amount
+ let lerpAmt = constrain(graphValues[i] / maxDataValue, 0, 1);
+ let barColor = lerpColor(dryColor, wetColor, lerpAmt);
+
+ push();
+ fill(barColor, 100, 255 - barColor);
+ stroke(100);
+ rect(x + 5, y, barWidth - 10, barHeight);
+ pop();
+ }
+}
diff --git a/examples/neuralNetwork-sequence-weather-prediction/weather_data.json b/examples/neuralNetwork-sequence-weather-prediction/weather_data.json
new file mode 100644
index 00000000..a45429a3
--- /dev/null
+++ b/examples/neuralNetwork-sequence-weather-prediction/weather_data.json
@@ -0,0 +1,196 @@
+{
+ "data": [
+ {
+ "date": "2024-08-01T00:00:00Z",
+ "temperature": 28.0,
+ "humidity": 50,
+ "wind_speed": 3.0,
+ "pressure": 1015,
+ "precipitation": 0.0
+ },
+ {
+ "date": "2024-08-01T01:00:00Z",
+ "temperature": 27.5,
+ "humidity": 52,
+ "wind_speed": 4.0,
+ "pressure": 1014,
+ "precipitation": 0.0
+ },
+ {
+ "date": "2024-08-01T02:00:00Z",
+ "temperature": 27.0,
+ "humidity": 55,
+ "wind_speed": 5.0,
+ "pressure": 1013,
+ "precipitation": 0.0
+ },
+ {
+ "date": "2024-08-01T03:00:00Z",
+ "temperature": 26.5,
+ "humidity": 60,
+ "wind_speed": 6.0,
+ "pressure": 1012,
+ "precipitation": 2.0
+ },
+ {
+ "date": "2024-08-01T04:00:00Z",
+ "temperature": 26.0,
+ "humidity": 65,
+ "wind_speed": 8.0,
+ "pressure": 1010,
+ "precipitation": 5.0
+ },
+ {
+ "date": "2024-08-01T05:00:00Z",
+ "temperature": 25.5,
+ "humidity": 70,
+ "wind_speed": 10.0,
+ "pressure": 1008,
+ "precipitation": 10.0
+ },
+ {
+ "date": "2024-08-01T06:00:00Z",
+ "temperature": 25.0,
+ "humidity": 75,
+ "wind_speed": 12.0,
+ "pressure": 1006,
+ "precipitation": 15.0
+ },
+ {
+ "date": "2024-08-01T07:00:00Z",
+ "temperature": 24.5,
+ "humidity": 80,
+ "wind_speed": 14.0,
+ "pressure": 1004,
+ "precipitation": 20.0
+ },
+ {
+ "date": "2024-08-01T08:00:00Z",
+ "temperature": 24.0,
+ "humidity": 85,
+ "wind_speed": 15.0,
+ "pressure": 1002,
+ "precipitation": 25.0
+ },
+ {
+ "date": "2024-08-01T09:00:00Z",
+ "temperature": 23.5,
+ "humidity": 90,
+ "wind_speed": 17.0,
+ "pressure": 1000,
+ "precipitation": 30.0
+ },
+ {
+ "date": "2024-08-01T10:00:00Z",
+ "temperature": 23.0,
+ "humidity": 95,
+ "wind_speed": 20.0,
+ "pressure": 998,
+ "precipitation": 35.0
+ },
+ {
+ "date": "2024-08-01T11:00:00Z",
+ "temperature": 24.0,
+ "humidity": 85,
+ "wind_speed": 10.0,
+ "pressure": 1005,
+ "precipitation": 10.0
+ },
+ {
+ "date": "2024-08-01T12:00:00Z",
+ "temperature": 25.0,
+ "humidity": 75,
+ "wind_speed": 7.0,
+ "pressure": 1010,
+ "precipitation": 5.0
+ },
+ {
+ "date": "2024-08-01T13:00:00Z",
+ "temperature": 26.0,
+ "humidity": 65,
+ "wind_speed": 5.0,
+ "pressure": 1013,
+ "precipitation": 0.0
+ },
+ {
+ "date": "2024-08-01T14:00:00Z",
+ "temperature": 27.0,
+ "humidity": 60,
+ "wind_speed": 4.0,
+ "pressure": 1015,
+ "precipitation": 0.0
+ },
+ {
+ "date": "2024-08-01T15:00:00Z",
+ "temperature": 28.0,
+ "humidity": 50,
+ "wind_speed": 3.0,
+ "pressure": 1018,
+ "precipitation": 0.0
+ },
+ {
+ "date": "2024-08-01T16:00:00Z",
+ "temperature": 27.0,
+ "humidity": 55,
+ "wind_speed": 4.0,
+ "pressure": 1015,
+ "precipitation": 0.0
+ },
+ {
+ "date": "2024-08-01T17:00:00Z",
+ "temperature": 26.0,
+ "humidity": 60,
+ "wind_speed": 5.0,
+ "pressure": 1012,
+ "precipitation": 1.0
+ },
+ {
+ "date": "2024-08-01T18:00:00Z",
+ "temperature": 25.0,
+ "humidity": 70,
+ "wind_speed": 7.0,
+ "pressure": 1009,
+ "precipitation": 5.0
+ },
+ {
+ "date": "2024-08-01T19:00:00Z",
+ "temperature": 24.0,
+ "humidity": 80,
+ "wind_speed": 10.0,
+ "pressure": 1005,
+ "precipitation": 10.0
+ },
+ {
+ "date": "2024-08-01T20:00:00Z",
+ "temperature": 23.0,
+ "humidity": 90,
+ "wind_speed": 12.0,
+ "pressure": 1002,
+ "precipitation": 15.0
+ },
+ {
+ "date": "2024-08-01T21:00:00Z",
+ "temperature": 22.0,
+ "humidity": 95,
+ "wind_speed": 15.0,
+ "pressure": 999,
+ "precipitation": 20.0
+ },
+ {
+ "date": "2024-08-01T22:00:00Z",
+ "temperature": 21.0,
+ "humidity": 98,
+ "wind_speed": 18.0,
+ "pressure": 995,
+ "precipitation": 25.0
+ },
+ {
+ "date": "2024-08-01T23:00:00Z",
+ "temperature": 20.0,
+ "humidity": 100,
+ "wind_speed": 20.0,
+ "pressure": 992,
+ "precipitation": 30.0
+ }
+ ]
+}
diff --git a/src/NeuralNetwork/Sequential/index.js b/src/NeuralNetwork/Sequential/index.js
new file mode 100644
index 00000000..0f57d469
--- /dev/null
+++ b/src/NeuralNetwork/Sequential/index.js
@@ -0,0 +1,263 @@
+import * as tf from "@tensorflow/tfjs";
+import { DiyNeuralNetwork } from "..";
+
+import callCallback from "../../utils/callcallback";
+import setBackend from "../../utils/setBackend";
+
+import seqUtils from "./sequentialUtils";
+import SequentialData from "./sequentialData";
+import { createSeqLayers } from "./seqLayers";
+
+// call an extension of DIY Neural Network as a new class, override select methods
+// which are seen below:
+class DIYSequential extends DiyNeuralNetwork {
+ constructor(options, callback) {
+ super(
+ {
+ ...options,
+ neuralNetworkData: null,
+ },
+ callback
+ );
+ // call all options set in the this class which is the default, extra option for dataMode
+ this.options = { ...this.options, ...(options || {}) };
+
+ this.neuralNetworkData =
+ this.options.neuralNetworkData || new SequentialData();
+
+ this.init = this.init.bind(this);
+ this.ready = callCallback(this.init(), callback);
+ }
+
+ async init() {
+ await tf.ready();
+ if (this.options.dataUrl) {
+ await this.loadDataFromUrl(this.options.dataUrl);
+ } else if (this.options.modelUrl) {
+ await this.load(this.options.modelUrl);
+ }
+ return this;
+ }
+
+ addData(xInputs, yInputs, options = null) {
+ // 1. verify format between the three possible types of xinputs
+ const xs = seqUtils.verifyAndFormatInputs(xInputs, options, this.options);
+
+ // 2. format the yInput - same logic as NN class
+ const ys = seqUtils.verifyAndFormatOutputs(yInputs, options, this.options);
+
+ // 3. add data to raw
+ this.neuralNetworkData.addData(xs, ys);
+ }
+
+ formatInputsForPredictionAll(_input) {
+ const { meta } = this.neuralNetworkData;
+ const inputHeaders = Object.keys(meta.inputs);
+
+ const formatted_inputs = seqUtils.verifyAndFormatInputs(
+ _input,
+ null,
+ this.options
+ );
+
+ // Validate sequence length against trained model
+ if (
+ this.neuralNetwork &&
+ this.neuralNetwork.model &&
+ formatted_inputs.length > 0
+ ) {
+ const inputSequenceLength = formatted_inputs.length;
+ const numFeatures = Object.keys(meta.inputs).length;
+
+ try {
+ // Get the expected input shape from the model
+ const expectedShape = this.neuralNetwork.model.inputs[0].shape;
+ const expectedSequenceLength = expectedShape[1]; // [batch, sequence, features]
+
+ if (
+ expectedSequenceLength !== null &&
+ inputSequenceLength !== expectedSequenceLength
+ ) {
+ throw new Error(
+ `🟪 ml5.js sequence length mismatch: The model was trained with sequences of length ${expectedSequenceLength}, but you provided a sequence of length ${inputSequenceLength}. Please provide exactly ${expectedSequenceLength} data points for prediction.`
+ );
+ }
+ } catch (err) {
+ // If we can't get the model shape, provide a more general error
+ if (err.message.includes("sequence length mismatch")) {
+ throw err;
+ }
+ }
+ }
+
+ const normalized_inputs = this.neuralNetworkData.normalizePredictData(
+ formatted_inputs,
+ meta.inputs
+ );
+ const output = tf.tensor(normalized_inputs);
+
+ return output;
+ }
+
+ createMetaData() {
+ // check if the data is empty
+ if (this.neuralNetworkData.data.raw.length <= 0) {
+ throw new Error(
+ "🟪 ml5.js sequence training error: Must add data before training!"
+ );
+ }
+
+ // this method does not get shape for images but instead for timesteps
+ const { inputs } = this.options;
+
+ let inputShape;
+ if (typeof inputs === "number") {
+ inputShape = inputs;
+ } else if (Array.isArray(inputs) && inputs.length > 0) {
+ inputShape = inputs.length; // will be fed into the tensors later
+ }
+
+ this.neuralNetworkData.createMetadata(inputShape);
+ }
+
+ addDefaultLayers() {
+ let layers;
+
+ const seqLayers = createSeqLayers(
+ this.neuralNetworkData.meta.seriesShape,
+ this.options.hiddenUnits,
+ this.numberOfClasses // For output units if needed
+ );
+
+ const task = this.options.task;
+ let taskConditions = task;
+ switch (taskConditions) {
+ case "sequenceRegression":
+ layers = seqLayers.regression;
+ return this.createNetworkLayers(layers);
+
+ case "sequenceClassification":
+ layers = seqLayers.classification;
+ return this.createNetworkLayers(layers);
+
+ case "sequenceClassificationWithCNN":
+ layers = seqLayers.classificationWithCNN;
+ return this.createNetworkLayers(layers);
+
+ case "sequenceRegressionWithCNN":
+ layers = seqLayers.regressionWithCNN;
+ return this.createNetworkLayers(layers);
+
+ default:
+ console.error(
+ "Error: Task is unknown. Check documentation for a list of available sequence tasks. Defaulting to sequenceRegression Tasks"
+ );
+ layers = seqLayers.default;
+ return this.createNetworkLayers(layers);
+ }
+ }
+
+ // included here to fix non convergence issue
+ compile() {
+ const LEARNING_RATE = this.options.learningRate;
+
+ let options = {};
+
+ if (
+ this.options.task === "sequenceClassification" ||
+ this.options.task === "sequenceClassificationWithCNN"
+ ) {
+ options = {
+ loss: "categoricalCrossentropy",
+ optimizer: tf.train.adam,
+ metrics: ["accuracy"],
+ };
+ } else if (
+ this.options.task === "sequenceRegression" ||
+ this.options.task === "sequenceRegressionWithCNN"
+ ) {
+ options = {
+ loss: "meanSquaredError",
+ optimizer: tf.train.adam,
+ metrics: ["accuracy"],
+ };
+ } else {
+ // if no task given - must be in NN class instead of this
+ options = {
+ loss: "meanSquaredError",
+ optimizer: tf.train.adam,
+ metrics: ["accuracy"],
+ };
+ }
+
+ options.optimizer = options.optimizer
+ ? this.neuralNetwork.setOptimizerFunction(
+ LEARNING_RATE,
+ options.optimizer
+ )
+ : this.neuralNetwork.setOptimizerFunction(LEARNING_RATE, tf.train.sgd);
+
+ this.neuralNetwork.compile(options);
+
+ // if debug mode is true, then show the model summary
+ if (this.options.debug) {
+ this.neuralNetworkVis.modelSummary(
+ {
+ name: "Model Summary",
+ },
+ this.neuralNetwork.model
+ );
+ }
+ }
+
+ // RDP algorithm
+ setFixedLength(coordinates, targetPointCount) {
+ const maxEpsilon = int(coordinates.length / 2);
+ return seqUtils.setFixedLength(coordinates, targetPointCount, maxEpsilon);
+ }
+
+ getSlidingWindow(data, featureKeys, targetKeys, batchLength = null) {
+ this.featureKeys = featureKeys;
+
+ if (batchLength == null) {
+ this.batchLength = int(data.length * 0.2); // set batchlength as a fraction of the total
+ } else if (batchLength >= data.length) {
+ throw new Error("batchLength must be smaller than total length of data");
+ } else {
+ this.batchLength = batchLength;
+ }
+
+ return seqUtils.createSlidingWindowData(
+ data,
+ this.batchLength,
+ this.featureKeys,
+ targetKeys
+ );
+ }
+
+ getSampleWindow(data) {
+ if (!this.batchLength || !this.featureKeys) {
+ throw new Error(
+ "Your data must be formated through the slidingWindow method first!"
+ );
+ }
+ return seqUtils.getLatestSequence(data, this.batchLength, this.featureKeys);
+ }
+
+ /**
+ * getData
+ * Returns the raw data that was added through addData
+ * @returns {Object} - Object with xs, ys, and raw properties
+ */
+ getData() {
+ const rawData = this.neuralNetworkData.data.raw;
+
+ return {
+ xs: rawData.map((item) => item.xs),
+ ys: rawData.map((item) => item.ys),
+ raw: rawData,
+ };
+ }
+}
+
+export default DIYSequential; //export for taskSelection
diff --git a/src/NeuralNetwork/Sequential/seqLayers.js b/src/NeuralNetwork/Sequential/seqLayers.js
new file mode 100644
index 00000000..b3b30d54
--- /dev/null
+++ b/src/NeuralNetwork/Sequential/seqLayers.js
@@ -0,0 +1,152 @@
+export const createSeqLayers = (
+ seriesShape,
+ hiddenUnits,
+ outputUnits = null
+) => {
+ return {
+ classificationWithCNN: [
+ {
+ type: "conv1d",
+ filters: 8,
+ kernelSize: 3,
+ activation: "relu",
+ inputShape: seriesShape,
+ },
+ {
+ type: "maxPooling1d",
+ poolSize: 2,
+ },
+ {
+ type: "conv1d",
+ filters: 16,
+ kernelSize: 3,
+ activation: "relu",
+ },
+ {
+ type: "maxPooling1d",
+ poolSize: 2,
+ },
+ {
+ type: "flatten",
+ },
+ {
+ type: "dense",
+ units: hiddenUnits,
+ activation: "relu",
+ },
+ {
+ type: "dense",
+ units: outputUnits,
+ activation: "softmax",
+ },
+ ],
+ classification: [
+ {
+ type: "lstm",
+ units: 16,
+ activation: "relu",
+ inputShape: seriesShape,
+ returnSequences: true,
+ },
+ {
+ type: "lstm",
+ units: 8,
+ activation: "relu",
+ returnSequences: false,
+ },
+ {
+ type: "dense",
+ units: hiddenUnits,
+ activation: "relu",
+ },
+ {
+ type: "dense",
+ units: outputUnits,
+ activation: "softmax",
+ },
+ ],
+ regressionWithCNN: [
+ {
+ type: "conv1d",
+ filters: 8,
+ kernelSize: 3,
+ activation: "relu",
+ inputShape: seriesShape,
+ },
+ {
+ type: "maxPooling1d",
+ poolSize: 2,
+ },
+ {
+ type: "conv1d",
+ filters: 16,
+ kernelSize: 3,
+ activation: "relu",
+ },
+ {
+ type: "maxPooling1d",
+ poolSize: 2,
+ },
+ {
+ type: "flatten",
+ },
+ {
+ type: "dense",
+ units: hiddenUnits,
+ activation: "relu",
+ },
+ {
+ type: "dense",
+ units: outputUnits,
+ activation: "sigmoid",
+ },
+ ],
+ regression: [
+ {
+ type: "lstm",
+ units: 16,
+ activation: "relu",
+ inputShape: seriesShape,
+ returnSequences: true,
+ },
+ {
+ type: "lstm",
+ units: 8,
+ activation: "relu",
+ },
+ {
+ type: "dense",
+ units: hiddenUnits,
+ activation: "relu",
+ },
+ {
+ type: "dense",
+ units: outputUnits,
+ activation: "sigmoid",
+ },
+ ],
+ default: [
+ {
+ type: "lstm",
+ units: 16,
+ activation: "relu",
+ inputShape: seriesShape,
+ },
+ {
+ type: "lstm",
+ units: 8,
+ activation: "relu",
+ },
+ {
+ type: "dense",
+ units: hiddenUnits,
+ activation: "relu",
+ },
+ {
+ type: "dense",
+ units: outputUnits,
+ activation: "sigmoid",
+ },
+ ],
+ };
+};
diff --git a/src/NeuralNetwork/Sequential/sequentialData.js b/src/NeuralNetwork/Sequential/sequentialData.js
new file mode 100644
index 00000000..e65bd15b
--- /dev/null
+++ b/src/NeuralNetwork/Sequential/sequentialData.js
@@ -0,0 +1,404 @@
+import * as tf from "@tensorflow/tfjs";
+import axios from "axios";
+import { saveBlob } from "../../utils/io";
+import modelLoader from "../../utils/modelLoader";
+import nnUtils from "../NeuralNetworkUtils";
+import seqUtils from "./sequentialUtils";
+
+import NeuralNetworkData from "../NeuralNetworkData";
+
+class SequentialData extends NeuralNetworkData {
+ constructor() {
+ super();
+ }
+
+ /**
+ * getDTypesFromData
+ * gets the data types of the data we're using
+ * important for handling oneHot
+ * @private
+ * @void - updates this.meta
+ */
+ getDTypesFromData() {
+ const meta = {
+ ...this.meta,
+ inputs: {},
+ outputs: {},
+ };
+
+ const sample = this.data.raw[0];
+
+ // consistent dTypes have already been checked at add data
+ const xs = Object.keys(sample.xs[0]); // since time series data is in form of array
+ const ys = Object.keys(sample.ys);
+ xs.forEach((prop) => {
+ meta.inputs[prop] = {
+ dtype: nnUtils.getDataType(sample.xs[0][prop]),
+ };
+ });
+
+ ys.forEach((prop) => {
+ meta.outputs[prop] = {
+ dtype: nnUtils.getDataType(sample.ys[prop]),
+ };
+ });
+
+ this.meta = meta;
+ }
+
+ /**
+ * get back the min and max of each label
+ * @private
+ * @param {Object} inputOrOutputMeta
+ * @param {"xs" | "ys"} xsOrYs
+ * @return {Object}
+ */
+ getInputMetaStats(inputOrOutputMeta, xsOrYs) {
+ const inputMeta = Object.assign({}, inputOrOutputMeta);
+
+ Object.keys(inputMeta).forEach((k) => {
+ if (inputMeta[k].dtype === "string") {
+ inputMeta[k].min = 0;
+ inputMeta[k].max = 1;
+ } else if (inputMeta[k].dtype === "number") {
+ let dataAsArray;
+ if (xsOrYs === "ys") {
+ dataAsArray = this.data.raw.map((item) => item[xsOrYs][k]);
+ } else if (xsOrYs === "xs") {
+ dataAsArray = this.data.raw.flatMap((item) =>
+ item[xsOrYs].map((obj) => obj[k])
+ );
+ }
+ inputMeta[k].min = nnUtils.getMin(dataAsArray);
+ inputMeta[k].max = nnUtils.getMax(dataAsArray);
+ } else if (inputMeta[k].dtype === "array") {
+ const dataAsArray = this.data.raw.map((item) => item[xsOrYs][k]).flat();
+ inputMeta[k].min = nnUtils.getMin(dataAsArray);
+ inputMeta[k].max = nnUtils.getMax(dataAsArray);
+ }
+ });
+
+ return inputMeta;
+ }
+
+ /**
+ * convertRawToTensors
+ * converts array of {xs, ys} to tensors
+ * @param {*} dataRaw
+ *
+ * @return {{ inputs: tf.Tensor, outputs: tf.Tensor }}
+ */
+ convertRawToTensors(dataRaw) {
+ const meta = Object.assign({}, this.meta);
+ const dataLength = dataRaw.length;
+
+ return tf.tidy(() => {
+ const inputArr = [];
+ const outputArr = [];
+
+ dataRaw.forEach((row) => {
+ // get xs
+ const xs = row.xs;
+ inputArr.push(xs);
+
+ // get ys
+ const ys = Object.keys(meta.outputs)
+ .map((k) => {
+ return row.ys[k];
+ })
+ .flat();
+
+ outputArr.push(ys);
+ });
+
+ // Validate sequence data structure before creating tensors
+ if (inputArr.length === 0) {
+ throw new Error(
+ "🟪 ml5.js sequence training error: No training data provided. Please add sequence data using model.addData() before training."
+ );
+ }
+
+ // Check if we have proper sequence structure (array of arrays)
+ const firstInput = inputArr[0];
+ if (!Array.isArray(firstInput)) {
+ throw new Error(
+ "🟪 ml5.js sequence training error: Have you run normalizeData() yet? Data must be normalized before training the model."
+ );
+ }
+
+ // Validate all sequences have the same length
+ const sequenceLength = firstInput.length;
+ if (sequenceLength === 0) {
+ throw new Error(
+ "🟪 ml5.js sequence training error: Empty sequences provided. Each sequence must contain at least one data point."
+ );
+ }
+
+ const inconsistentSequence = inputArr.find(
+ (seq) => seq.length !== sequenceLength
+ );
+ if (inconsistentSequence) {
+ throw new Error(
+ `🟪 ml5.js sequence training error: All sequences must have the same length. Found sequences with lengths ${sequenceLength} and ${inconsistentSequence.length}. Please ensure all training sequences have exactly the same number of time steps.`
+ );
+ }
+
+ // Validate we have proper feature structure
+ const firstTimeStep = firstInput[0];
+ const numFeatures = Object.keys(meta.inputs).length;
+ const timeStepFeatures = Object.keys(firstTimeStep || {}).length;
+
+ if (timeStepFeatures !== numFeatures) {
+ throw new Error(
+ `🟪 ml5.js sequence training error: Feature count mismatch. Expected ${numFeatures} features per time step but found ${timeStepFeatures}. Make sure each time step in your sequences has the same features as defined in your model inputs.`
+ );
+ }
+
+ const inputs = tf.tensor(inputArr);
+
+ const outputs = tf.tensor(outputArr.flat(), [
+ dataLength,
+ meta.outputUnits,
+ ]);
+
+ return {
+ inputs,
+ outputs,
+ };
+ });
+ }
+
+ /**
+ * normalize the dataRaw input
+ * @return {Array