Can a computer tell a Siberian Husky from an Alaskan Malamute? People mix them up all the time. This page follows real photos through two models: one learned from scratch on our 261 training photos, and one that reuses a network pretrained on 1.2 million images. You'll see a photo as numbers, the filters that find edges and colours, the deeper layers, how each model was trained and tuned, what it looks at when it decides, and where it goes wrong. Both models run in your browser. Each stage has the matching Keras code, with the output it prints; open it with Show code.
Loading TensorFlow.js and the two models…
Every number below has one of four origins. Each stage is tagged with the same labels.
All photos come from Wikimedia Commons, under free licences (CC0, public domain, CC BY, CC BY-SA). The labels needed three steps. 793 candidates were found in the breed categories and by searching. A pretrained model dropped the ones without any dog (parachutes, helicopters: there's a military exercise called "Operation Malamute"). Then two rounds of people looking: first a pass that dropped drawings, other breeds, sled teams of mixed "Alaskan huskies" and dogs too small to see, then a final review of every label. 404 photos remain. Getting the data right was most of the work, and that's normal.
Two choices matter for honest results. Photo sessions stay together: one author often uploaded many shots of the same dog on the same day; if one were in training and another in the test, the test would only check whether the model recognises that dog again. So all photos of one author and day go to the same split. The test photos are used once, at the very end; settings are chosen on the validation photos.
There are three huskies for every malamute. A model that always says "husky" is right 73% of the time on the test photos, so plain accuracy flatters: watch both breeds separately.
Pick one to follow through the page, or use the arrows in the toolbar.
Model B builds on MobileNetV2 (Sandler et al., Google), ImageNet weights from Keras Applications, Apache License 2.0; served here converted to TensorFlow.js in model/transfer/. TensorFlow.js by Google, Apache License 2.0.
A colour photo is three grids of numbers, red, green and blue, each from 0 (none) to 255 (full). Ours are 224×224 pixels: 150,528 numbers. Every photo was padded to a square with grey and resized, so nothing is cut off. Below: the selected photo, its centre enlarged, and the actual numbers of the 6×6 pixels in the middle.
A convolution slides a small filter over the photo: here 3×3 pixels × 3 colours. Where the pixels match the filter's pattern, the result is high. (The Tiny CNN walkthrough shows this sliding step by step.) These are the 32 filters of MobileNetV2's first layer, learned on ImageNet, shown as tiny 3×3 colour patches (grey is 0; lighter means that colour counts positively, darker negatively): most react to an edge in one direction, or to one colour against another. A few are almost all grey: training left them nearly unused.
Each filter produces a feature map: a 112×112 grid (the first layer also halves the size) that is bright where the photo matches. Here are the eight maps that react most to the selected photo: outlines, the dark mask, light fur.
Layers are stacked: each one combines the previous layer's maps. With every step the maps get smaller and each position sees a larger part of the photo. Early layers find edges, middle layers textures and shapes (fur, an eye, an ear tip), the last layers whole parts. MobileNetV2 has 53 layers with filters; below, the most active maps of a middle layer (28×28) and of the last one (7×7: each cell sees a large part of the photo).
Finally, global average pooling turns each of the 1280 maps into one number, its average. These 1280 numbers are the photo's features: a description that says nothing about where things are, only what is there.
Both models from photo to answer, block by block, on the same scales. Each block's output is drawn as two bars: size (how many pixels wide its maps are) and channels (how many maps). Watch the funnel: the size shrinks from 224 to 7 while the channels grow from 3 to 1280, trading where for what. Model A is 4 convolution blocks, all trained on our photos. Model B is MobileNetV2's 19 blocks, frozen as they came from ImageNet, plus a small trained head. Click any block to see what it does and its real output for the selected photo.
Settings like the learning rate, the number of filters, augmentation or dropout aren't learned: people choose them by trying. These are all the experiments behind this page, in the order they were run. Every run is judged on the validation photos only, by balanced accuracy: the average of the husky score and the malamute score, so that always answering "husky" scores 50%, not 75%. The test photos were not looked at until the choice was made. Every run is seeded, so tools/experiments.py gives the same numbers again.
How fragile small data is: while building this page, one photo was dropped after a label check. That changed the split, and with it model A's best run and its test result by about 17 points; model B moved by about one point. With a few hundred photos, model A's numbers are as much about which photos ended up where as about the model. Model B is stable because it brings what it learned from 1.2 million photos.
What to take from this: first check that a model can learn at all (can it memorise its training photos?), then fight overfitting (augmentation, dropout, fewer parameters). Change one thing at a time, keep a log, and never choose on the test set: a setting picked on the test photos makes the test result look better than the model is.
The original MobileNetV2, with its own last layer, already knows 1000 ImageNet classes, including "Siberian husky", "malamute" and "Eskimo dog". Its top three for the selected photo:
So the pretrained features already separate the breeds fairly well: our head mostly learns to read them for our two labels. Without those 1.2 million ImageNet photos, model A has only our 261.
About the pretrained model: MobileNetV2 (Sandler et al., Google, 2018), with the ImageNet weights from Keras Applications (Apache License 2.0). Its weights are numbers only, not code; the tools check the downloaded files against pinned SHA-256 hashes before using them. This page serves them, converted to TensorFlow.js and stored in 16-bit, from model/transfer/. ImageNet's own photos are licensed for research use only; models trained on them, like this one, are widely published, but that is worth knowing before using it commercially.
Both models end with average pooling and one dense layer, so their decision can be traced back to places in the photo. Each last feature map is multiplied by the dense layer's weight for the predicted breed and the maps are added up: a class activation map (CAM). Coloured areas pushed the model towards its answer, the brightest most. A good model looks at the dog's face, ears and coat; a model that looks at the snow or the grass has learned a shortcut.
Both models on the test photos, which played no part in training or in choosing settings. Rows: the true breed; columns: what the model said.
Not every "mistake" is the model's fault. People confuse these breeds too, and a label can be wrong: when a confident model disagrees with a label, it's worth looking at the photo (and its Commons page) again. Click a photo to inspect it.
Use a photo of a husky or a malamute (or anything else, to see what happens: the models only know these two answers, so a cat still gets one). It's padded to a square and resized to 224×224 like the training photos, then both models run on it in your browser. Nothing is uploaded.