Senior 4 min · April 15, 2026

Teachable Machine — Single Lighting Training Fails

Training under single lighting caused 40% production accuracy.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Teachable Machine trains image classifiers in the browser with zero code — drag and drop images, click Train
  • Export trained models as TensorFlow.js format (model.json + weight shards) for direct browser inference
  • Load the model in Next.js with @tensorflow/tfjs — runs entirely client-side, no server inference needed
  • First prediction compiles WebGL shaders and takes 3–8 seconds — always warm up during app load, not on user interaction
  • Production rule: keep classes under 10 and images per class above 50 for usable accuracy
  • Biggest mistake: training with 10 images per class under lab conditions and expecting reliable real-world predictions
  • Always wrap inference in tf.tidy() — every uncleaned tensor leaks GPU memory and will crash mobile tabs
Plain-English First

Teachable Machine is a free web tool by Google that lets you train an image classifier by showing it example photos. Think of it as teaching a child by showing flashcards — you hold up pictures of cats and say 'cat,' then pictures of dogs and say 'dog.' After enough examples, the tool learns the pattern. Then you download the trained brain and plug it into your web app, where it runs entirely in the user's browser — no server, no API calls, no cloud bill. This guide walks you through the entire process from training to deployment in Next.js.

Most image classification tutorials require Python, GPU setup, CUDA drivers, and weeks of learning before you get a working prototype. Teachable Machine eliminates all of that. You train in the browser, export a TensorFlow.js model, and load it in your Next.js application. The entire pipeline requires zero Python and zero server-side ML infrastructure.

The exported model runs entirely client-side. No server inference, no API calls, no per-request billing, no latency from network round-trips. The user's browser does all the computation using WebGL. This makes it ideal for prototypes, internal tools, educational projects, and low-traffic applications where standing up a GPU inference server is wildly disproportionate to the problem.

The common misconception is that Teachable Machine produces toy models unsuitable for real use. For narrow classification tasks — 5 to 10 visually distinct classes with quality training images — the accuracy is genuinely production-viable. The constraint is not the tool. It is the quality, quantity, and diversity of your training images. A model trained on 200 varied images per class in the actual deployment environment will outperform one trained on 2,000 images shot under identical lab conditions.

Training Your Model in Teachable Machine

Teachable Machine (teachablemachine.withgoogle.com) provides a browser-based interface for training image classifiers with zero code. You create classes, upload or webcam-capture images, click Train, and get a working model in under two minutes.

Under the hood, the tool uses transfer learning on MobileNet — a lightweight convolutional neural network pre-trained on ImageNet's 1.4 million images. It freezes MobileNet's feature extraction layers and only retrains the final classification head on your images. This is why training takes seconds instead of hours: only a few hundred parameters are being updated, not millions.

The quality of the model depends entirely on your training images. The tool itself is not the bottleneck — your data is. Images captured under a single lighting condition, at a single angle, with a single background will produce a model that works perfectly under that exact condition and fails everywhere else.

training_checklist.txtTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Teachable Machine Image ClassificationTraining Checklist

1. Create classes (one per category you want to detect)
   - Minimum: 2 classes
   - Recommended: 3-10 classes
   - Avoid: more than 15 classes with fewer than 50 images each
   - Consider: add an "unknown/other" class with diverse non-target images

2. Add images per class
   - Minimum: 20 images per class (will overfit — demo only)
   - Recommended: 50-200 images per class
   - Best: 200+ images with systematically varied conditions
   - Critical: similar image counts per class (avoid 200 in one, 30 in another)

3. Image quality guidelines — the make-or-break factor
   - USE THE SAME DEVICE as deployment (phone camera ≠ DSLR ≠ laptop webcam)
   - Vary angles: front, side, top, 45-degree tilts, slight rotations
   - Vary lighting: bright daylight, dim indoor, overhead fluorescent, warm LED
   - Vary backgrounds: plain walls, cluttered desks, outdoors, dark surfaces
   - Vary distance: close-up, arm's length, across a table
   - Include edge cases: partially visible objects, slight blur, small size
   - Include negative examples in your "other" class: things that look similar but are not

4. Click Train Model
   - Training takes 30-120 seconds in the browser
   - Preview accuracy appears in the right panel — but this is training accuracy
   - Test with NEW images that were NOT in the training set
   - If accuracy is below 70%, add more diverse images before proceeding

5. Export Model
   - Click 'Export Model'
   - Select 'TensorFlow.js' tab
   - Choose 'Download' (not shareable link — those expire)
   - Download gives you: model.json + weights.bin files
   - Save these files — you cannot re-download later without retraining
Transfer Learning in Teachable Machine
  • MobileNet has already learned to detect edges, shapes, textures, colors, and spatial patterns from over 1 million images across 1,000 categories.
  • Teachable Machine freezes all of MobileNet's learned feature layers and only retrains the final classification head — a few hundred parameters.
  • This is why training takes 30–120 seconds instead of hours or days. The hard work of learning what edges and textures look like was done by Google years ago.
  • Your images teach the classification head which combinations of MobileNet's pre-learned features map to your custom classes.
  • More diverse training images produce a classification head that relies on the right features (the actual object) rather than the wrong ones (the background, the lighting, the desk).
Production Insight
Teachable Machine does not split your data into train and test sets. Every image you provide is used for training.
The accuracy shown in the preview panel is training accuracy — the model is being tested on images it already memorized.
This number is almost always inflated by 10–30% compared to real-world accuracy.
Rule: hold out 20% of your images per class and test them manually after export. If you trained with 100 images, set aside 20 and test on those. This is the only way to get a realistic accuracy estimate before deployment.
Key Takeaway
Teachable Machine uses transfer learning on MobileNet — you train only the final classification layer, which is why it takes seconds.
Use 50–200 images per class captured in your actual deployment conditions — lighting, angle, background, and camera device all matter.
Hold out 20% of images for manual testing after export — the Teachable Machine preview shows training accuracy, which is always inflated.
Training Quality Decision Tree
IfAccuracy below 70% in Teachable Machine preview
UseYou need more images or more distinct classes. Add images that show the distinguishing visual features more clearly. Remove blurry or ambiguous images that could belong to multiple classes.
IfAccuracy above 95% in preview but fails on new images
UseThe model is overfitting to your training conditions. Your training images lack diversity — same lighting, same angle, same background. Re-capture images under varied conditions matching your deployment environment.
IfTwo specific classes are frequently confused with each other
UseThese classes share visual features that the model cannot distinguish. Add more images of the distinguishing characteristics — zoom in on the differentiating details. Consider merging the classes if the visual difference is genuinely subtle, or adding a third disambiguation class.
IfModel works perfectly on your laptop webcam but fails on a phone
UseCamera resolution, color profile, and lens distortion differ between devices. Include training images from every device type you plan to deploy on. A laptop webcam and a phone rear camera produce meaningfully different images of the same object.

Exporting the Model

Teachable Machine exports models in TensorFlow.js format, which consists of a model.json file (the graph topology, layer configuration, and metadata including your class labels) and one or more .bin files (the trained weight values, potentially split into multiple shards for large models).

You have two export options: download the files to your machine, or use a hosted shareable link on Google's infrastructure. Always download for anything beyond a one-day demo. The shareable links have no SLA, no guaranteed uptime, and Google can delete them without notice.

export_structure.txtTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Exported Model File Structure:

my_model/
  model.json          — Model architecture + metadata (class labels, input shape)
  weights.bin         — Trained weight values (may be split into multiple shards)

Key fields inside model.json:
  - modelTopology:       Neural network graph definition (layers, connections)
  - weightsManifest:     Paths to weight shard files with byte offsets and sizes
  - userDefinedMetadata: Contains your class labels array (the names you assigned)
  - format:              'layers-model' (the TensorFlow.js layers API format)

File sizes (typical for Teachable Machine export):
  - model.json:   50-150 KB
  - weights.bin:  2-5 MB (depends on number of classes)
  - Total:        ~3-6 MB before compression, ~1.5-3 MB with gzip

Deployment options:
  Option A: Download files → place in /public/models/ in your Next.js project
            Pros: fast, no external dependency, works offline, full control
            Cons: model updates require redeployment

  Option B: Use the shareable link directly (fetches from Google CDN)
            Pros: zero setup, instant sharing
            Cons: link expires, requires internet, slow in some regions

  Recommended: Option A for anything beyond a quick demo.
Shareable Links Are Temporary and Unreliable
The shareable link from Teachable Machine is hosted on Google's infrastructure with no uptime guarantee and no expiration date communicated to you. Google may clean up hosted models at any time. For any deployment that will be used for more than a one-time demo — internal tools, client prototypes, educational projects — download the files and serve them from your own infrastructure. Discovering your production app is broken because Google cleaned up a temporary hosting bucket is a uniquely frustrating experience.
Production Insight
The weights.bin file is typically 2–5 MB for a Teachable Machine model.
On a slow 3G mobile connection, that takes 5–15 seconds to download.
On first visit, the user stares at a blank screen unless you provide feedback.
Rule: show a loading indicator with explicit progress messaging during model download. 'Loading AI model (3.2 MB)…' is far better than a spinner with no context. Users will wait if they know what is happening.
Key Takeaway
Export as TensorFlow.js format — you get model.json and one or more weight shard .bin files.
Always download and self-host for production use — shareable links are temporary and can expire without warning.
Place model files in the Next.js /public/models/ directory so they are served as static assets at the root path.

Setting Up the Next.js Project

TensorFlow.js requires special handling in Next.js because it accesses browser-only APIs — WebGL contexts, the document object, navigator.gpu — that do not exist during server-side rendering. Next.js 13+ uses the App Router with React Server Components by default, which means any component is server-rendered unless you explicitly opt out.

Any component that imports TensorFlow.js must be marked with the 'use client' directive and dynamically imported with SSR disabled. This is the single most common deployment failure: forgetting one of these two steps produces a build error that is confusing if you have not seen it before.

app/page.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// app/page.jsx — Main page with dynamic import to avoid SSR of TF.js
import dynamic from 'next/dynamic';

// Dynamic import with ssr: false prevents the component from being
// rendered on the server, where browser APIs do not exist.
// The loading fallback is shown while the component JS downloads.
const ImageClassifier = dynamic(
  () => import('@/components/ImageClassifier'),
  {
    ssr: false,
    loading: () => (
      <div style={{ padding: '2rem', textAlign: 'center' }}>
        <p>Loading image classifier…</p>
        <p style={{ fontSize: '0.85rem', color: '#666' }}>
          Downloading model files (~3 MB)
        </p>
      </div>
    )
  }
);

export default function Home() {
  return (
    <main style={{ maxWidth: '800px', margin: '0 auto', padding: '2rem' }}>
      <h1>Image Classifier</h1>
      <p>
        Upload an image or use your webcam to classify objects.
        All processing happens in your browser — nothing is sent to a server.
      </p>
      <ImageClassifier />
    </main>
  );
}

// --- Installation ---
// npm install @tensorflow/tfjs @tensorflow/tfjs-backend-webgl
//
// Place your exported model files at:
//   public/models/my_model/model.json
//   public/models/my_model/weights.bin
SSR Breaks TensorFlow.js — Every Time
Never import TensorFlow.js in a server component or without dynamic import with ssr: false. TensorFlow.js accesses browser-specific globals (self, document, navigator, WebGLRenderingContext) during import — not just during use. The import itself crashes in a server environment. The error message is 'self is not defined' or 'navigator is not defined' during build or server-side render. The fix is always the same: add 'use client' at the top of the component file, and wrap the import in dynamic(() => import(...), { ssr: false }) from the parent page.
Production Insight
Next.js re-renders components and unmounts them on route changes. Without cleanup, each navigation creates a new model instance that loads into GPU memory while the old one is never freed.
After 3–5 navigations back and forth, you have 3–5 copies of the model in GPU memory and the tab crashes.
Rule: always dispose the model in the useEffect cleanup function. Use useRef to persist the model instance across re-renders, and dispose it only on unmount.
Key Takeaway
Always use dynamic import with ssr: false for any component that imports TensorFlow.js.
Add the 'use client' directive at the top of every TF.js component file.
Dispose models in useEffect cleanup to prevent GPU memory leaks on route changes.

Loading and Running the Model

The core component loads the exported Teachable Machine model, preprocesses uploaded images to match MobileNet's expected input format, runs inference, and displays results with confidence scores.

The preprocessing pipeline is critical and must match exactly what Teachable Machine uses internally: resize to 224×224 pixels, convert to float32, normalize from [0, 255] to [-1, 1], and add a batch dimension. Any deviation from this pipeline — wrong size, wrong normalization range, missing batch dimension — produces confident but completely wrong predictions with no error message. The model does not know its input is wrong. It just gives you garbage output.

components/ImageClassifier.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
'use client';

import { useState, useEffect, useRef, useCallback } from 'react';
import * as tf from '@tensorflow/tfjs';
import '@tensorflow/tfjs-backend-webgl';

const MODEL_URL = '/models/my_model/model.json';

export default function ImageClassifier() {
  const [model, setModel] = useState(null);
  const [labels, setLabels] = useState([]);
  const [loading, setLoading] = useState(true);
  const [predictions, setPredictions] = useState([]);
  const [error, setError] = useState(null);
  const fileInputRef = useRef(null);
  const imageRef = useRef(null);
  const modelRef = useRef(null);

  // Load model once on mount, dispose on unmount
  useEffect(() => {
    let mounted = true;

    async function init() {
      try {
        // Wait for TF.js backend to initialize
        await tf.ready();
        console.log('TF.js backend:', tf.getBackend());

        // Load the exported Teachable Machine model
        const loadedModel = await tf.loadLayersModel(MODEL_URL);

        // Warm up: first prediction compiles WebGL shaders (3-8s)
        // Do this now, behind the loading spinner, not on first user action
        const dummy = tf.zeros([1, 224, 224, 3]);
        const warmup = loadedModel.predict(dummy);
        warmup.dispose();
        dummy.dispose();

        // Extract class labels from model metadata
        // Teachable Machine stores labels in the model metadata
        let classLabels = [];
        try {
          const metadataUrl = MODEL_URL.replace('model.json', 'metadata.json');
          const metadataResponse = await fetch(metadataUrl);
          if (metadataResponse.ok) {
            const metadata = await metadataResponse.json();
            classLabels = metadata.labels || [];
          }
        } catch (metaErr) {
          console.warn('No metadata.json found, using generic labels');
          const outputShape = loadedModel.outputShape;
          const numClasses = outputShape[outputShape.length - 1];
          classLabels = Array.from(
            { length: numClasses },
            (_, i) => `Class ${i}`
          );
        }

        if (mounted) {
          setModel(loadedModel);
          modelRef.current = loadedModel;
          setLabels(classLabels);
          setLoading(false);
          console.log(
            `Model loaded. ${classLabels.length} classes: ${classLabels.join(', ')}`
          );
        } else {
          loadedModel.dispose();
        }
      } catch (err) {
        console.error('Model loading failed:', err);
        if (mounted) {
          setError(`Failed to load model: ${err.message}`);
          setLoading(false);
        }
      }
    }

    init();

    // Cleanup: dispose model on unmount to free GPU memory
    return () => {
      mounted = false;
      if (modelRef.current) {
        modelRef.current.dispose();
        console.log('Model disposed on unmount');
      }
    };
  }, []);

  // Classify an image element
  const classifyImage = useCallback(
    async (imgElement) => {
      if (!model || !imgElement) return;

      // All tensor ops wrapped in tf.tidy() for automatic memory cleanup
      const outputTensor = tf.tidy(() => {
        // Step 1: Read pixel data from the DOM image element
        const imageTensor = tf.browser.fromPixels(imgElement);

        // Step 2: Resize to 224x224 (MobileNet's expected input size)
        const resized = tf.image.resizeBilinear(imageTensor, [224, 224]);

        // Step 3: Convert to float and normalize to [-1, 1]
        // MobileNet expects this range, NOT [0, 1]
        const normalized = resized.toFloat().div(127.5).sub(1.0);

        // Step 4: Add batch dimension [224,224,3] → [1,224,224,3]
        const batched = normalized.expandDims(0);

        // Step 5: Run inference
        return model.predict(batched);
      });

      // Extract probabilities from GPU tensor to CPU array
      const probabilities = await outputTensor.data();
      outputTensor.dispose(); // Manual dispose for async result

      // Sort predictions by confidence, highest first
      const sorted = Array.from(probabilities)
        .map((prob, i) => ({
          label: labels[i] || `Class ${i}`,
          confidence: (prob * 100).toFixed(1),
        }))
        .sort((a, b) => b.confidence - a.confidence);

      setPredictions(sorted);
    },
    [model, labels]
  );

  // Handle file upload
  const handleFileUpload = (event) => {
    const file = event.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (e) => {
      const img = imageRef.current;
      img.src = e.target.result;
      // Wait for image to fully load before classifying
      img.onload = () => classifyImage(img);
    };
    reader.readAsDataURL(file);
  };

  if (loading) {
    return (
      <div style={{ padding: '2rem', textAlign: 'center' }}>
        <p>Initializing model…</p>
        <p style={{ fontSize: '0.85rem', color: '#666' }}>
          This takes a few seconds on first load
        </p>
      </div>
    );
  }
  if (error) return <p style={{ color: 'red' }}>{error}</p>;

  return (
    <div>
      <input
        ref={fileInputRef}
        type="file"
        accept="image/*"
        onChange={handleFileUpload}
        style={{ marginBottom: '1rem' }}
      />

      <img
        ref={imageRef}
        alt="Upload preview"
        style={{
          maxWidth: '300px',
          marginTop: '1rem',
          display: 'block',
          borderRadius: '8px',
        }}
      />

      {predictions.length > 0 && (
        <div style={{ marginTop: '1rem' }}>
          <h3>Predictions</h3>
          {predictions.map((p, i) => (
            <div key={i} style={{ marginBottom: '0.5rem' }}>
              <strong>{p.label}</strong>: {p.confidence}%
              <div
                style={{
                  height: '8px',
                  background: '#e0e0e0',
                  borderRadius: '4px',
                  marginTop: '4px',
                }}
              >
                <div
                  style={{
                    width: `${p.confidence}%`,
                    height: '100%',
                    background: i === 0 ? '#4caf50' : '#90caf9',
                    borderRadius: '4px',
                    transition: 'width 0.3s ease',
                  }}
                />
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
The Preprocessing Pipeline Must Match Exactly
  • Step 1: tf.browser.fromPixels(imgElement) — reads raw pixels as uint8 [0–255].
  • Step 2: resizeBilinear([224, 224]) — resize to MobileNet's expected input dimensions.
  • Step 3: .toFloat().div(127.5).sub(1.0) — normalize from [0, 255] to [-1, 1]. NOT [0, 1]. This is the most common mistake.
  • Step 4: .expandDims(0) — add batch dimension. Model expects shape [1, 224, 224, 3], not [224, 224, 3].
  • Skip any of these steps, or use the wrong normalization range, and the model gives confident but completely wrong predictions. There is no error — just garbage output.
Production Insight
tf.browser.fromPixels() reads from a rendered DOM element. If the image is not yet visible, not fully loaded, or has zero dimensions, you get a tensor of zeros — and the model will confidently classify a black rectangle.
Always trigger classification from the image element's onload event, not from the file reader's onload.
Rule: verify the image element has non-zero naturalWidth and naturalHeight before calling classifyImage. A zero-size image means the browser has not finished decoding it.
Key Takeaway
Preprocessing must match MobileNet's pipeline exactly: resize to 224×224, float, normalize to [-1, 1], add batch dimension.
Always warm up the model with a dummy prediction during component mount — never make the user wait for WebGL shader compilation.
Wrap all inference in tf.tidy() to prevent GPU memory leaks. Manually dispose async results.

Adding Webcam Support

Real-time webcam classification captures frames from a video element and runs inference in a continuous loop. The key engineering challenges are frame timing (do not run inference faster than the model can process), memory management (each frame creates tensors that must be cleaned up), and graceful lifecycle management (stop the webcam and cancel animation frames when the component unmounts).

Do not use setInterval for frame capture — requestAnimationFrame synchronizes with the browser's repaint cycle and automatically pauses when the tab is not visible, saving battery on mobile devices.

components/WebcamClassifier.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
'use client';

import { useState, useEffect, useRef, useCallback } from 'react';
import * as tf from '@tensorflow/tfjs';

export default function WebcamClassifier({ model, labels }) {
  const videoRef = useRef(null);
  const [predictions, setPredictions] = useState([]);
  const [isStreaming, setIsStreaming] = useState(false);
  const animationRef = useRef(null);
  const isProcessingRef = useRef(false);

  const startWebcam = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: {
          width: { ideal: 224 },
          height: { ideal: 224 },
          facingMode: 'environment', // Rear camera on mobile
        },
      });
      videoRef.current.srcObject = stream;
      await videoRef.current.play();
      setIsStreaming(true);
      classifyFrame();
    } catch (err) {
      console.error('Webcam access denied or unavailable:', err);
      // Common causes: no camera, permission denied, HTTP (not HTTPS)
    }
  };

  const stopWebcam = () => {
    if (animationRef.current) {
      cancelAnimationFrame(animationRef.current);
      animationRef.current = null;
    }
    if (videoRef.current?.srcObject) {
      videoRef.current.srcObject.getTracks().forEach((track) => track.stop());
      videoRef.current.srcObject = null;
    }
    setIsStreaming(false);
    setPredictions([]);
  };

  const classifyFrame = useCallback(() => {
    // Skip this frame if the previous inference is still running
    // This prevents GPU memory buildup from queued predictions
    if (!model || !videoRef.current || isProcessingRef.current) {
      animationRef.current = requestAnimationFrame(classifyFrame);
      return;
    }

    // Check that video is actually producing frames
    if (
      videoRef.current.readyState < 2 ||
      videoRef.current.videoWidth === 0
    ) {
      animationRef.current = requestAnimationFrame(classifyFrame);
      return;
    }

    isProcessingRef.current = true;

    // Synchronous tensor ops wrapped in tf.tidy for auto-cleanup
    const result = tf.tidy(() => {
      const frame = tf.browser.fromPixels(videoRef.current);
      const resized = tf.image.resizeBilinear(frame, [224, 224]);
      const normalized = resized.toFloat().div(127.5).sub(1.0);
      const batched = normalized.expandDims(0);
      return model.predict(batched);
    });

    // Async data extraction — must dispose manually
    result
      .data()
      .then((probabilities) => {
        const sorted = Array.from(probabilities)
          .map((prob, i) => ({
            label: labels[i] || `Class ${i}`,
            confidence: (prob * 100).toFixed(1),
          }))
          .sort((a, b) => b.confidence - a.confidence)
          .slice(0, 3);

        setPredictions(sorted);
      })
      .finally(() => {
        result.dispose();
        isProcessingRef.current = false;
        animationRef.current = requestAnimationFrame(classifyFrame);
      });
  }, [model, labels]);

  // Cleanup on unmount: stop webcam, cancel animation frame
  useEffect(() => {
    return () => stopWebcam();
  }, []);

  return (
    <div>
      <button onClick={isStreaming ? stopWebcam : startWebcam}>
        {isStreaming ? 'Stop Camera' : 'Start Camera'}
      </button>

      <video
        ref={videoRef}
        style={{
          width: '300px',
          marginTop: '1rem',
          display: isStreaming ? 'block' : 'none',
          borderRadius: '8px',
        }}
        playsInline
        muted
      />

      {predictions.length > 0 && isStreaming && (
        <div style={{ marginTop: '1rem' }}>
          {predictions.map((p, i) => (
            <div key={i} style={{ marginBottom: '0.25rem' }}>
              <strong>{p.label}</strong>: {p.confidence}%
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
Frame Skipping Prevents GPU Memory Exhaustion
Running inference on every frame at 60 FPS is unnecessary and will exhaust GPU memory on mobile within seconds. Most classification tasks update at 10–15 FPS without any noticeable lag to the user. Use an isProcessingRef flag to skip frames while the previous inference is still running. This naturally throttles to the model's actual inference speed — fast hardware runs more frames, slow hardware runs fewer — without any manual timing code.
Production Insight
Webcam access requires HTTPS in production. navigator.mediaDevices.getUserMedia() silently returns undefined on HTTP origins in most browsers, producing a confusing 'cannot read property of undefined' error.
Always deploy to HTTPS, even for internal tools.
Test on actual mobile devices — front versus rear camera (facingMode: 'user' vs 'environment') affects classification accuracy because they have different focal lengths, color profiles, and fields of view. A model trained on rear camera images may underperform on front camera input.
Key Takeaway
Use requestAnimationFrame for smooth webcam processing — not setInterval.
Skip frames with an isProcessing flag to prevent GPU memory exhaustion on mobile.
Webcam requires HTTPS in production. Test on both front and rear cameras on real devices.

Model File Hosting and Optimization

Model files must be served efficiently. The combined model.json and weights.bin files are typically 3–6 MB uncompressed. Without compression, proper caching, and a loading indicator, this creates a poor first-load experience — especially on mobile networks where 3G connections are still common in many markets.

The optimization is straightforward: enable gzip compression (which cuts the download by 30–40%), set immutable cache headers so returning users never re-download, and provide clear loading feedback during the initial download.

next.config.mjsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// next.config.mjs — Optimize model file serving
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Enable gzip compression for all static assets
  compress: true,

  // Custom headers for model files — cache aggressively
  async headers() {
    return [
      {
        // Match all files under /models/
        source: '/models/:path*',
        headers: [
          {
            // Cache for 1 year. Model files do not change after deployment.
            // When you update the model, change the directory name (v1 → v2).
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
    ];
  },
};

export default nextConfig;

// --- File placement ---
// public/
//   models/
//     my_model/
//       model.json      ← Model architecture and metadata
//       weights.bin     ← Trained weight values
//       metadata.json   ← Class labels (if exported separately)
//
// These files are served at:
//   /models/my_model/model.json
//   /models/my_model/weights.bin
//   /models/my_model/metadata.json
//
// --- Model versioning ---
// When updating the model, use a new directory:
//   /models/my_model_v2/model.json
// Update MODEL_URL in your component to point to the new version.
// Old cached versions will not interfere because the path changed.
File Placement and Caching in Next.js
  • Place model.json, weights.bin, and metadata.json in /public/models/my_model/ — Next.js serves /public/ as static assets at the root URL.
  • The model URL in your component becomes '/models/my_model/model.json' — no import, no bundling, just a fetch at runtime.
  • Set Cache-Control: public, max-age=31536000, immutable — model files do not change after deployment. This eliminates re-downloads on repeat visits.
  • Enable gzip compression — weight files are dense binary data that compresses to 60–70% of original size.
  • For model updates, use a new directory path (/models/v2/) rather than overwriting files. This prevents cache confusion and allows instant rollback.
Production Insight
Without cache headers, the browser re-downloads model files on every page visit — 3–6 MB each time on a route that should be instant.
With immutable caching, the model is cached in the browser for one year after first download. Subsequent visits load the model from disk cache in milliseconds.
Rule: always set Cache-Control: immutable for model weight files. Use directory-based versioning (/models/v1/, /models/v2/) for model updates instead of cache-busting query parameters, which break some CDN configurations.
Key Takeaway
Host model files in /public/models/ with immutable cache headers for zero-cost repeat visits.
Enable gzip — weight files compress to 60–70% of original size, cutting download time by a third.
First visit costs 2–5 MB of download. Every subsequent visit loads from browser cache at near-zero cost.

Improving Accuracy Beyond Teachable Machine Defaults

When Teachable Machine accuracy is insufficient for your use case, you have three practical options before abandoning the tool entirely: improve your training data (almost always the highest-impact fix), add a classification confidence threshold that routes uncertain predictions to manual review, and implement an 'unknown' class to handle out-of-distribution inputs gracefully.

Most accuracy problems are data problems, not model problems. A Teachable Machine model that fails at 60% accuracy on production data will often hit 90%+ accuracy after recapturing training images under production conditions. The model architecture is rarely the bottleneck for narrow classification tasks.

components/ClassifierWithFallback.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
'use client';

import { useState, useCallback } from 'react';
import * as tf from '@tensorflow/tfjs';

// Minimum confidence to show a definitive prediction.
// Below this threshold, the prediction is flagged as uncertain.
// Tune this on your held-out test set — not by intuition.
const CONFIDENCE_THRESHOLD = 70;

export default function ClassifierWithFallback({
  model,
  labels,
  imageElement,
}) {
  const [result, setResult] = useState(null);

  const classify = useCallback(async () => {
    if (!model || !imageElement) return;

    // Verify image is actually loaded and visible
    if (
      !imageElement.naturalWidth ||
      !imageElement.naturalHeight
    ) {
      console.warn('Image not loaded yet — skipping classification');
      return;
    }

    const prediction = tf.tidy(() => {
      const tensor = tf.browser
        .fromPixels(imageElement)
        .resizeBilinear([224, 224])
        .toFloat()
        .div(127.5)
        .sub(1.0)
        .expandDims(0);
      return model.predict(tensor);
    });

    const probabilities = await prediction.data();
    prediction.dispose();

    const sorted = Array.from(probabilities)
      .map((prob, i) => ({
        label: labels[i] || `Class ${i}`,
        confidence: prob * 100,
      }))
      .sort((a, b) => b.confidence - a.confidence);

    const topPrediction = sorted[0];
    const secondPrediction = sorted[1];

    // Check confidence AND gap to second prediction
    // A 72% prediction with the second at 71% is very different
    // from a 72% prediction with the second at 5%
    const confidenceGap = topPrediction.confidence - (secondPrediction?.confidence || 0);

    if (
      topPrediction.confidence < CONFIDENCE_THRESHOLD ||
      confidenceGap < 15 // Too close between top two classes
    ) {
      setResult({
        status: 'uncertain',
        message: `Low confidence (${topPrediction.confidence.toFixed(1)}%) or ambiguous classification. Please review manually.`,
        topGuesses: sorted.slice(0, 3),
      });
    } else {
      setResult({
        status: 'confident',
        label: topPrediction.label,
        confidence: topPrediction.confidence.toFixed(1),
        allPredictions: sorted,
      });
    }
  }, [model, labels, imageElement]);

  return { result, classify };
}
Softmax Confidence Scores Are Not Calibrated Probabilities
The softmax output of a neural network looks like a probability distribution — values sum to 1.0 and the highest value is presented as 'confidence.' But these scores are not calibrated. A model outputting 85% confidence does not mean it is correct 85% of the time. In practice, models are often overconfident: they output 90%+ confidence on inputs they have never seen and get completely wrong. Use the threshold as a practical gate — a heuristic that catches obvious low-confidence cases — not as a statistical guarantee. Always validate your threshold choice on a held-out test set by measuring what percentage of errors the threshold actually catches.
Production Insight
A confidence threshold of 70% typically catches 60–80% of misclassifications, depending on the model and data.
Lowering the threshold lets more errors through to the user. Raising it increases the volume of predictions routed to manual review, which costs human time.
This is a business decision, not a technical one: how much manual review budget do you have versus how much error tolerance does the use case allow?
Rule: tune the threshold on your held-out test set by plotting the automation rate (percentage of predictions above threshold) against the error rate (percentage of above-threshold predictions that are wrong). The optimal threshold is where the error rate is acceptable and the automation rate justifies the system's existence.
Key Takeaway
Add a confidence threshold to route uncertain predictions to manual review instead of showing wrong answers to users.
Softmax scores are not calibrated probabilities — a model can be 95% 'confident' and completely wrong.
The threshold is a business decision: more automation versus fewer errors. Tune it on held-out test data.

Deployment Checklist

Before shipping your Teachable Machine classifier to production, verify each item below. These are not aspirational best practices — they are the specific items that cause the most common deployment failures. Skipping any one of them produces a bug that is embarrassing in a demo and damaging in a production tool.

deployment_checklist.txtTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Teachable Machine + Next.js Deployment Checklist

=== Pre-deployment Validation ===
  [ ] Model files downloaded and placed in /public/models/
  [ ] Shareable link is NOT used in production code
  [ ] Model tested on at least 3 device types (desktop Chrome, iOS Safari, Android Chrome)
  [ ] Accuracy validated on held-out test images that were NOT in the training set
  [ ] Confidence threshold tested and tuned on the held-out set
  [ ] WebGL backend verified on all target browsers (check tf.getBackend())
  [ ] Model tested with edge case inputs: blank images, rotated, very dark, very bright

=== Code Quality ===
  [ ] dynamic(() => import(...), { ssr: false }) used for all TF.js components
  [ ] 'use client' directive present on every file that imports TensorFlow.js
  [ ] tf.tidy() wraps ALL synchronous tensor operations
  [ ] Async tensor results (.data()) followed by .dispose() in .finally()
  [ ] useEffect cleanup disposes model on unmount via modelRef
  [ ] Model warmup (dummy prediction) runs during component mount behind loading UI
  [ ] Webcam streams stopped and tracks released on component unmount

=== Performance ===
  [ ] Gzip compression enabled for model files (next.config.mjs compress: true)
  [ ] Cache-Control: public, max-age=31536000, immutable set on /models/* paths
  [ ] Loading indicator with descriptive text shown during model download
  [ ] First user interaction triggers instant prediction (warmup already complete)
  [ ] tf.memory().numTensors monitored in dev — stays constant across predictions

=== Monitoring and Error Handling ===
  [ ] Console logs backend type on init (webgl vs cpu vs wasm)
  [ ] Error boundary or try/catch wraps model loading
  [ ] Fallback UI shown for devices without WebGL support
  [ ] HTTPS enabled (required for webcam access in production)
  [ ] Image onload event gates classification — never classify a half-loaded image
Browser and Device Support Reality
WebGL is supported in all modern browsers released since 2017. However, Safari on older iOS devices (iPhone 8 and earlier on older iOS versions), some Android WebView implementations in embedded apps, and privacy-focused browsers that disable WebGL by default can cause TensorFlow.js to fall back to the CPU backend. CPU inference works but is 10–50x slower — a 20ms WebGL prediction becomes 500ms–2s on CPU. Always check tf.getBackend() on load, and consider disabling real-time webcam features on CPU-only devices where inference is too slow to be useful.
Production Insight
The single most common deployment failure is forgetting the 'use client' directive.
Next.js App Router treats all components as server components by default. Importing TensorFlow.js in a server component produces 'self is not defined' during build — a confusing error if you have not encountered it before.
Rule: if your component file has import * as tf from '@tensorflow/tfjs' anywhere in it, the first line must be 'use client'. No exceptions.
Key Takeaway
Test on real devices, not just desktop Chrome — WebGL support, camera behavior, and memory limits vary significantly.
Enable gzip and immutable caching for model files before deployment.
The 'use client' directive and dynamic import with ssr: false are the two most common deployment failure points — verify both.
● Production incidentPOST-MORTEMseverity: high

Internal Quality Control App Fails Because Training Images Were All Taken Under One Lighting Condition

Symptom
The app correctly classified parts in the office demo every time. On the factory floor, it failed consistently and unpredictably. Defect-free parts were flagged as defective, and actual defects were missed. The failure rate was not random — it correlated with time of day (which corresponded to lighting changes from windows and shift lighting).
Assumption
The team trained the model using 80 images per class taken at their desks under consistent fluorescent overhead lights, with a white desk background, using the same laptop webcam at a fixed angle and distance. They assumed the model would generalize to the factory floor environment because 'the parts look the same everywhere.'
Root cause
All training images shared identical lighting, angle, background, and camera characteristics. The model learned to classify based on lighting gradients and background color patterns — not the physical features of the parts themselves. The factory floor had variable natural light from windows (changing throughout the day), overhead LEDs at different color temperatures, shadows from machinery, different backgrounds (metal conveyor belt vs. white desk), and images captured from a mounted tablet camera at a different distance. The model had never seen any of these conditions during training and had no basis for correct classification.
Fix
Re-captured 200 images per class directly on the factory floor using the actual mounted tablet camera, across three time periods (morning with natural light, midday under full LEDs, and evening with mixed lighting). Varied the conveyor belt position to include different background sections. Added a dedicated 'background/empty belt' class so the model could distinguish between an empty conveyor and a part. Retrained in Teachable Machine and achieved 91% accuracy in the production environment — a massive improvement from 40%, though the team also added a confidence threshold at 75% to route uncertain predictions to human inspectors.
Key lesson
  • Training images must match the deployment environment — lighting, angle, background, camera device, and distance all matter.
  • Capture training data from the actual production context, not a controlled office or lab. If the model will run on a factory tablet, train with images from that tablet.
  • Vary conditions systematically during training: different times of day, different angles, different backgrounds, different distances from the camera.
  • A model that achieves 98% accuracy on lab images and 40% on production images has not learned the task — it has memorized the lab conditions.
Production debug guideCommon issues when moving from Teachable Machine training to Next.js deployment — and exactly what to check.5 entries
Symptom · 01
Model loads but predictions are always the same class regardless of input image
Fix
The model is receiving a blank or constant input. Verify that tf.browser.fromPixels() receives a non-black, non-uniform tensor by calling tensor.print() and inspecting values. If the image element is not yet rendered or has not finished loading, fromPixels returns zeros. Ensure the img element's onload event has fired before running inference.
Symptom · 02
Predictions work correctly in Teachable Machine preview but are wrong or random in Next.js
Fix
The preprocessing pipeline does not match. Teachable Machine uses MobileNet's expected input format: 224×224 pixels, normalized to [-1, 1]. Check that your code applies exactly: resizeBilinear([224, 224]), toFloat(), div(127.5), sub(1.0), expandDims(0). Missing any one of these steps — especially the normalization range — causes confident but incorrect predictions with no error message.
Symptom · 03
Model download is slow or fails intermittently in production
Fix
The model.json and weight shard files may be served without compression or from a slow origin. Enable gzip compression (weight files compress to 60–70% of original size). Serve from the same origin as the app to avoid CORS issues. Set Cache-Control: immutable so returning users never re-download. If using a CDN, verify that the .bin file MIME type is not being blocked.
Symptom · 04
Predictions are accurate in Chrome but wrong or random in Safari
Fix
Safari's WebGL implementation has different precision characteristics and shader compilation behavior. Check tf.getBackend() — Safari may silently fall back to the CPU backend, which uses different float precision. Also verify that your image element source is not tainted by CORS restrictions (Safari is stricter about cross-origin image tainting). Test on actual iOS Safari, not just desktop Safari.
Symptom · 05
The app works fine on desktop but crashes on mobile after 30–60 seconds of webcam classification
Fix
GPU memory leak from uncleared tensors. Every call to tf.browser.fromPixels() and model.predict() allocates GPU memory. On mobile devices with limited VRAM, this fills up in 50–200 frames. Verify every tensor operation is inside tf.tidy(), or that you manually call .dispose() on every intermediate tensor. Monitor with console.log(tf.memory().numTensors) — this number should stay constant across predictions, not grow.
★ Image Classifier Debug Cheat SheetQuick checks when your exported Teachable Machine model misbehaves in the browser.
Model returns NaN or Infinity in prediction probabilities
Immediate action
The input tensor has values outside the expected range. Most likely the normalization step is applied twice or not at all.
Commands
const tensor = tf.browser.fromPixels(imageElement); console.log('min:', tensor.min().dataSync()[0], 'max:', tensor.max().dataSync()[0]);
const normalized = tensor.toFloat().div(127.5).sub(1.0); console.log('normalized min:', normalized.min().dataSync()[0], 'max:', normalized.max().dataSync()[0]);
Fix now
Raw pixel values should be 0–255. After normalization, values should be -1.0 to 1.0. If you see values outside these ranges, your normalization is wrong. If raw values are already 0–1 (some image loading libraries pre-normalize), skip the div(127.5).sub(1.0) step.
GPU memory grows with each prediction until the browser tab crashes+
Immediate action
Tensors are not being disposed after use. Every tensor operation allocates GPU memory that is never automatically freed.
Commands
console.log('Tensors in memory:', tf.memory().numTensors, 'Bytes:', tf.memory().numBytes);
const result = tf.tidy(() => { return model.predict(preprocessedInput); }); const data = await result.data(); result.dispose();
Fix now
Wrap all synchronous tensor operations in tf.tidy(). For async operations (like result.data()), tf.tidy cannot help — you must manually call result.dispose() after extracting the data. The numTensors count should stay constant between predictions. If it grows, you have a leak.
First prediction takes 3–8 seconds, all subsequent predictions are fast (20–50ms)+
Immediate action
This is normal — the first prediction triggers WebGL shader compilation. The fix is to move this cost to app initialization, not to user interaction.
Commands
const dummy = tf.zeros([1, 224, 224, 3]);
const warmup = model.predict(dummy); warmup.dispose(); dummy.dispose(); console.log('Model warmed up');
Fix now
Run a dummy prediction with a zero tensor during component mount, behind your loading spinner. The user sees 'Loading model…' while warmup runs. By the time they interact with the app, the first real prediction is instant.
Image Classification Approaches Compared
ApproachCode RequiredTraining TimeAccuracy PotentialCostBest For
Teachable Machine + TF.jsMinimal (~100 lines JS)30–120 secondsGood for 5–10 visually distinct classesFree (runs in browser)Prototypes, internal tools, education, offline-capable apps
Custom TensorFlow/PyTorch (Python)Extensive (500+ lines)Hours to days (GPU required)Highest — full architecture and training controlGPU compute cost ($1–50/run)Production systems with complex visual domains
Cloud Vision API (Google/AWS/Azure)API calls only (~20 lines)Zero (pre-trained general model)High for common objects, low for custom domainsPer-request pricing ($1–3 per 1K images)General object labeling, OCR, face detection — no custom classes
Hugging Face + Fine-tuningModerate (~200 lines Python)Minutes to hoursHigh — modern architectures with transfer learningFree tier + optional GPUCustom classifiers with more control than Teachable Machine
ONNX Runtime WebModerate (~150 lines JS)Depends on source modelMatches the source model exactlyFree (runs in browser)Deploying existing PyTorch or TensorFlow models to the browser

Key takeaways

1
Teachable Machine uses transfer learning on MobileNet
you train only the final classification layer, which is why it takes seconds, not hours.
2
Use 50–200 images per class captured in your actual deployment environment. Training conditions must match production conditions
lighting, angle, background, camera device.
3
Export as TensorFlow.js format and self-host model files in /public/models/. Shareable links are temporary and will break.
4
Always use dynamic import with ssr
false and the 'use client' directive for TensorFlow.js components in Next.js.
5
Warm up the model with a dummy prediction during component mount
never make the user wait for WebGL shader compilation on their first interaction.
6
Wrap every inference call in tf.tidy() and manually dispose async results. Tensor leaks crash mobile tabs within minutes of continuous use.
7
Add a confidence threshold to route uncertain predictions to manual review instead of showing wrong answers. Softmax scores are not calibrated probabilities.

Common mistakes to avoid

6 patterns
×

Training with fewer than 30 images per class

Symptom
Model achieves 95%+ accuracy in the Teachable Machine preview but fails on any image that is not nearly identical to a training example. It has memorized specific images, not learned visual patterns. Change the background slightly and accuracy collapses.
Fix
Use 50–200 images per class minimum. Capture images across systematically varied lighting conditions, camera angles, backgrounds, and distances. The more variation in your training data, the better the model generalizes. Twenty images of a cup on a white desk teaches the model to recognize 'white desk' — not 'cup'.
×

Not warming up the model before the first user interaction

Symptom
User uploads an image and waits 3–8 seconds for the first prediction. Every subsequent prediction is instant (20–50ms). The user experience is confusing and feels broken — 'why is the first one so slow?'
Fix
Run a dummy prediction with tf.zeros([1, 224, 224, 3]) during component mount, behind your loading spinner. This forces WebGL shader compilation before the user interacts. By the time the loading state clears, the first real prediction will be instant.
×

Importing TensorFlow.js in a Next.js server component

Symptom
Build fails with 'self is not defined' or 'navigator is not defined' or 'document is not defined'. The error occurs during server-side rendering because TensorFlow.js accesses browser globals at import time.
Fix
Add 'use client' as the first line of the component file. Import the component using dynamic(() => import('./Component'), { ssr: false }) from the parent page. Both steps are required — 'use client' alone is not enough if the parent page server-renders.
×

Using the Teachable Machine shareable link in deployed production code

Symptom
Model stops working after a few days or weeks. Users see 'Failed to fetch model' errors. The Google-hosted shareable link has expired or been cleaned up — with no warning, no email notification, and no way to restore it.
Fix
Download model files during export and serve from /public/models/ in your Next.js project. Set immutable cache headers. The shareable link is for one-time demos and Slack messages — never for deployed applications.
×

Not disposing tensors after each prediction

Symptom
GPU memory usage grows with every prediction. The tf.memory().numTensors count climbs monotonically. After 50–200 predictions (fewer on mobile), the tab crashes with an out-of-memory error or the browser forcibly kills the tab.
Fix
Wrap all synchronous tensor operations in tf.tidy(), which automatically disposes intermediate tensors. For the final async result (after calling .data()), manually call result.dispose() in a .finally() block. Monitor tf.memory().numTensors during development — this number must stay constant across predictions.
×

Testing accuracy only in the Teachable Machine preview panel and assuming it reflects real-world performance

Symptom
Teachable Machine shows 97% accuracy. The deployed model achieves 55% on actual user inputs. The team is baffled because 'it worked in training.' Stakeholder trust in the project evaporates.
Fix
The Teachable Machine preview tests on training images — the model has memorized these. Hold out 20% of your images per class and do not upload them to Teachable Machine. After export, test the deployed model on these held-out images. If accuracy drops more than 10% from preview accuracy, your training data lacks the diversity needed for real-world performance.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is transfer learning, and why does Teachable Machine use it?
Q02SENIOR
How would you improve the accuracy of a Teachable Machine model that is ...
Q03SENIOR
Explain the trade-offs of running image classification in the browser ve...
Q01 of 03JUNIOR

What is transfer learning, and why does Teachable Machine use it?

ANSWER
Transfer learning reuses a model that was trained on a large, general dataset and adapts it to a new, smaller, specific dataset. Teachable Machine uses MobileNet — a convolutional neural network pre-trained on ImageNet's 1.4 million images across 1,000 categories. MobileNet has already learned to detect low-level visual features (edges, textures, shapes) and mid-level patterns (object parts, spatial relationships). Teachable Machine freezes all of MobileNet's pre-trained layers and only retrains the final classification head — a small fully-connected layer that maps MobileNet's learned features to your custom classes. This is why training takes 30–120 seconds instead of hours: only a few hundred parameters are being updated, leveraging all the visual understanding that took Google days of GPU compute to build. The trade-off is that transfer learning works best when your images share visual characteristics with ImageNet's training data — natural photos of objects, clear subjects, reasonable lighting. It works less well on highly specialized visual domains like microscopy, satellite imagery, or X-rays, where the low-level features (edge types, textures) are fundamentally different from natural photos.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How many images do I need per class in Teachable Machine?
02
Can I use Teachable Machine for real-time video classification?
03
What happens if the user's browser does not support WebGL?
04
How do I update my model after deployment without breaking things for existing users?
05
Is Teachable Machine accurate enough for production use?
🔥

That's Tools. Mark it forged?

4 min read · try the examples if you haven't

Previous
My 2026 Developer Productivity Stack (Tools & Workflow)
12 / 12 · Tools
Next
Natural Language Processing (NLP) Explained