{"slug": "from-lab-to-code-4-exploring-neural-network-efficiency-with-python-and-pytorch", "title": "From Lab to Code #4: Exploring Neural Network Efficiency with Python and PyTorch", "summary": "A developer built the Neural Network Efficiency Lab, a Python and PyTorch application that trains three convolutional neural networks of increasing channel capacity on MNIST and compares their accuracy against computational cost. The tool visualizes predictions and feature maps and lets users experiment with hypothetical CNN architectures, probing how small a network can be while remaining useful for classification.", "body_md": "A larger neural network can often achieve better accuracy.\n\nBut does that always make it the better model?\n\nWhen working with neural networks, it is easy to focus on a single metric: accuracy. We train a model, evaluate it on a test dataset, and compare the percentage of correct predictions.\n\nHowever, this leaves out an important engineering question:\n\n**How much computation do we need to achieve that accuracy?**\n\nA small model might be slightly less accurate but require significantly fewer operations. Depending on the application, that trade-off can matter more than achieving the highest possible score.\n\nFor the fourth project in my **From Lab to Code** series, I wanted to explore this relationship by building an interactive environment for experimenting with convolutional neural networks.\n\nThe result is **Neural Network Efficiency Lab**, a Python and PyTorch application that compares three independently trained CNN models, visualizes their predictions, and shows how architectural decisions affect computational cost.\n\nRather than building another digit-recognition demo, the goal was to investigate a more interesting question:\n\n**How small can a neural network be while still delivering useful classification accuracy?**\n\nFor this experiment, I used the MNIST handwritten-digit dataset.\n\nMNIST contains grayscale images of handwritten digits from 0 to 9. Each image has a resolution of 28 × 28 pixels.\n\nIt is a relatively simple classification problem, which makes it useful for comparing small neural networks without requiring a large training infrastructure.\n\nI designed three convolutional neural networks with increasing capacity:\n\nAll three use the same basic architecture, dataset, and training procedure.\n\nThe primary difference is the number of convolutional channels.\n\nThis makes it easier to observe how increasing model capacity affects both accuracy and computational requirements.\n\nThe project also includes an interactive interface where users can select trained models, draw digits, inspect predictions, visualize feature maps, and experiment with hypothetical CNN architectures.\n\nA convolutional neural network processes an image by applying learned filters to local regions.\n\nUnlike a fully connected layer, where every output depends on every input, a convolutional layer reuses small filters across different image positions.\n\nThese filters can learn patterns such as edges, strokes, or more complex arrangements of local features.\n\nFor this project, I implemented a deliberately small architecture:\n\n```\nInput Image\n   1 × 28 × 28\n        |\n        v\nConvolution 1\n        |\n       ReLU\n        |\n     MaxPool\n        |\n        v\nConvolution 2\n        |\n       ReLU\n        |\n     MaxPool\n        |\n        v\n      Flatten\n        |\n        v\n   Linear Layer\n        |\n        v\n    10 Logits\n```\n\nThe two convolutional layers extract features, while max pooling reduces the spatial resolution.\n\nFinally, a fully connected layer maps the extracted representation to ten output values, one for each digit.\n\nThe implementation uses PyTorch:\n\n``` python\nclass DigitCNN(nn.Module):\n    def __init__(self, config):\n        super().__init__()\n\n        k = config.kernel\n\n        self.conv1 = nn.Conv2d(\n            1, config.c1, k, padding=k // 2\n        )\n        self.pool1 = nn.MaxPool2d(2)\n\n        self.conv2 = nn.Conv2d(\n            config.c1, config.c2, k, padding=k // 2\n        )\n        self.pool2 = nn.MaxPool2d(2)\n\n        self.head = nn.Linear(\n            config.c2 * 7 * 7, 10\n        )\n\n    def forward(self, x):\n        x = self.pool1(\n            torch.relu(self.conv1(x))\n        )\n        x = self.pool2(\n            torch.relu(self.conv2(x))\n        )\n\n        return self.head(torch.flatten(x, 1))\n```\n\nThe output consists of ten *logits*, which are unnormalized classification scores.\n\nDuring training, these logits are passed to PyTorch's `CrossEntropyLoss`.\n\nDuring inference, softmax converts them into a probability distribution over the ten classes:\n\n```\nP(class i) = exp(logit_i) / Σ exp(logit_j)\n```\n\nThe predicted digit is the class with the highest probability.\n\nThese probabilities are useful for inspecting the model's predictions, although they should not automatically be interpreted as perfectly calibrated confidence estimates.\n\nThe number of convolutional channels determines how many filters a layer learns.\n\nA layer with more channels can represent a larger collection of features.\n\nHowever, more channels also mean additional learned parameters and arithmetic operations.\n\nThis creates an interesting trade-off.\n\nA larger network may learn more useful features and improve classification accuracy, but the improvement might be relatively small compared with the additional computation.\n\nTo investigate this, I kept the architecture structure consistent while changing its channel widths.\n\nAccuracy is relatively easy to understand.\n\nWe count the number of correct predictions and divide by the total number of evaluated examples:\n\n```\nAccuracy = Correct Predictions / Total Predictions\n```\n\nBut how should we measure the computational cost of a neural network?\n\nOne useful approximation is the number of **multiply-accumulate operations**, or MACs.\n\nA multiply-accumulate operation combines multiplication and addition:\n\n```\naccumulator += weight × input\n```\n\nNeural networks perform large numbers of these operations in convolutional and fully connected layers.\n\nCounting MACs provides a hardware-independent estimate of some of the arithmetic work required for a forward pass.\n\nFor a standard convolutional layer, the MAC count depends on the output dimensions, input channels, output channels, and kernel size.\n\nA simplified formula is:\n\n```\nMACs = H_out × W_out × C_out × C_in × K²\n```\n\nwhere:\n\n`H_out` and `W_out` are the output feature-map dimensions.`C_in` is the number of input channels.`C_out` is the number of output channels.`K` is the width of a square convolution kernel.\nIn our architecture, the input has one channel and a resolution of 28 × 28.\n\nThe first convolution preserves the spatial resolution through padding.\n\nAfter the first pooling operation, the image representation becomes 14 × 14.\n\nAfter the second pooling operation, it becomes 7 × 7.\n\nFor convolutional channel counts `C1` and `C2`, and kernel size `K`, the estimated costs are:\n\n```\nConv1 = 28 × 28 × C1 × K²\n\nConv2 = 14 × 14 × C1 × C2 × K²\n\nDense = 7 × 7 × C2 × 10\n\nTotal MACs = Conv1 + Conv2 + Dense\n```\n\nI implemented this directly in Python:\n\n``` python\ndef mac_breakdown(cfg):\n    k2 = cfg.kernel ** 2\n\n    conv1 = 28 * 28 * cfg.c1 * k2\n    conv2 = 14 * 14 * cfg.c1 * cfg.c2 * k2\n    linear = 7 * 7 * cfg.c2 * 10\n\n    return {\n        \"conv1\": conv1,\n        \"conv2\": conv2,\n        \"linear\": linear,\n        \"total\": conv1 + conv2 + linear,\n    }\n```\n\nThis makes it possible to calculate the theoretical arithmetic cost of a model without executing it.\n\nMACs are not the same as inference latency.\n\nThe calculation does not include operations such as ReLU, pooling, softmax, memory access, or data movement.\n\nIt also does not account for hardware-specific optimizations or parallel execution.\n\nConsequently, a model with fewer MACs is not guaranteed to be faster on every processor or GPU.\n\nFor this project, MACs provide a transparent way to compare architectural complexity, not a benchmark of actual deployment performance.\n\nWith the architecture and cost calculation in place, I created three CNN configurations.\n\n| Model | Conv Channels | Parameters | MACs / Image | \n|---|---|---|---|\n| Micro CNN | 3 → 6 | 3,148 | 55,860 | \n| Compact CNN | 8 → 16 | 9,098 | 290,080 | \n| Wide CNN | 16 → 32 | 20,490 | 1,031,744 | \n\nAll three use 3 × 3 convolution kernels.\n\nThe difference in computational cost is substantial.\n\nThe Wide CNN requires approximately **18.5 times as many MACs** as the Micro CNN.\n\nBut does it achieve 18.5 times better classification performance?\n\nOf course, accuracy cannot scale in that way. Once a model is already performing well, each additional improvement becomes harder to achieve.\n\nThe more useful question is how much accuracy we gain in exchange for that increased cost.\n\nTo answer it, the models need to be trained and evaluated under comparable conditions.\n\nFor a fair comparison, I used the same training procedure for all three architectures.\n\nThe official MNIST dataset was divided into:\n\n| Split | Images | Purpose | \n|---|---|---|\n| Training | 54,000 | Learn model parameters | \n| Validation | 6,000 | Select the best training checkpoint | \n| Test | 10,000 | Final evaluation | \n\nThe validation set was created from the official MNIST training split.\n\nThe official test set was kept separate and used only for the final evaluation.\n\nThis distinction matters because selecting a model based on its test accuracy would introduce information leakage into the evaluation process.\n\nThe models were trained using:\n\nFor each model, I selected the checkpoint with the highest validation accuracy and then evaluated it on the official 10,000-image test set.\n\nThe implementation uses the standard PyTorch training sequence:\n\n```\noptimizer.zero_grad(set_to_none=True)\n\nlogits = model(images)\n\nloss = criterion(logits, labels)\n\nloss.backward()\n\noptimizer.step()\n```\n\nCross-entropy provides a differentiable objective, while backpropagation calculates gradients for the trainable parameters.\n\nAdam uses these gradients to update the network weights.\n\nThe key benefit of keeping the training procedure consistent is that we can focus on how changes to the architecture affect the observed results.\n\nHowever, the experiment uses only one training run per architecture. It is not a statistical comparison across multiple random seeds.\n\nAfter training, the three models achieved the following results:\n\n| Model | Test Accuracy | MACs / Image | \n|---|---|---|\n| **Micro CNN** | **96.67%** | **55,860** | \n| Compact CNN | 97.92% | 290,080 | \n| Wide CNN | 98.63% | 1,031,744 | \n\nAll accuracy values come from the held-out official MNIST test set.\n\nThese results reveal the central trade-off behind the project.\n\nMoving from Micro to Compact increases the cost from 55,860 to 290,080 MACs.\n\nThat is approximately **5.2 times the computation**.\n\nThe test accuracy improves from 96.67% to 97.92% — a gain of **1.25 percentage points**.\n\nMoving from Micro to Wide requires approximately **18.5 times the computation**.\n\nYet accuracy improves by only **1.96 percentage points**, from 96.67% to 98.63%.\n\nThis does not mean the larger model is useless.\n\nFor some applications, reducing the error rate can be extremely valuable, even if the improvement in accuracy appears small.\n\nBut it does show why accuracy should not be considered in isolation.\n\nThe best model depends on the available resources and the requirements of the application.\n\nTo make the trade-off more concrete, I introduced an illustrative budget of **75,000 MACs per image**.\n\nUnder this constraint:\n\nThe Micro CNN achieves 96.67% test accuracy while requiring only 55,860 MACs.\n\nIn this particular experiment, it provides a strong result under the selected arithmetic budget.\n\nThe 75,000-MAC threshold is an educational design constraint rather than a limit derived from real hardware.\n\nThe experiment demonstrates how a computational budget can influence architecture selection.\n\nIt does not establish which network would be fastest on a particular CPU, GPU, or embedded device.\n\nAfter training the models, I wanted to make the results easier to explore.\n\nA table of numbers can show that one network is larger than another.\n\nBut an interactive application can help explain what those differences mean.\n\nFor the frontend, I created a browser interface connected to a local Python backend using FastAPI.\n\nThe backend loads the saved PyTorch model checkpoints and performs inference.\n\nThe browser displays the predictions, model metrics, and visualizations.\n\nThe first part of the application compares Micro, Compact, and Wide CNN.\n\nUsers can inspect each model's:\n\nAn accuracy-versus-computation visualization makes the trade-off easier to see.\n\nInstead of considering accuracy and model size separately, users can observe how much additional computation is associated with each improvement.\n\nThe application also includes an interactive drawing canvas.\n\nUsers can draw a handwritten digit using their mouse and send it to one of the trained models.\n\nThe image is converted into a 28 × 28 grayscale representation and normalized using the same convention as the training data.\n\nThe selected model produces ten logits.\n\nSoftmax converts them into class probabilities, allowing the interface to display the predicted digit and its probability distribution.\n\nThe application also includes twenty attributed MNIST test examples for trying the models on actual dataset samples.\n\nAn interesting limitation appears when using the drawing canvas.\n\nHandwritten digits produced with a mouse can look quite different from images in MNIST.\n\nDifferences in stroke width, positioning, and drawing style can affect the prediction.\n\nA high accuracy on the MNIST test dataset therefore does not guarantee equally high accuracy on arbitrary user drawings.\n\nThis is a small example of **distribution shift**: a model is evaluated on inputs that differ from those it encountered during training.\n\nI also wanted to show something beyond the final classification result.\n\nWhat does a convolutional layer actually produce?\n\nTo investigate this, the application extracts intermediate feature maps from the first two convolutional layers.\n\nFor a selected input, users can inspect visualizations of the learned activations.\n\nThe model returns both its final classification logits and intermediate features:\n\n```\nf1 = torch.relu(self.conv1(x))\nx = self.pool1(f1)\n\nf2 = torch.relu(self.conv2(x))\nx = self.pool2(f2)\n\nlogits = self.head(torch.flatten(x, 1))\n\nreturn logits, (f1, f2)\n```\n\nThe application displays a selection of feature channels from each layer.\n\nThese visualizations provide a way to observe how the input is transformed as it passes through the network.\n\nHowever, the displayed feature maps are normalized individually for visualization.\n\nThis means their brightness should not be interpreted as a directly comparable activation magnitude across channels.\n\nOne of the more useful features of the project is the **Architecture Sandbox**.\n\nHere, users can change the number of convolutional channels and the kernel size.\n\nThe application immediately recalculates:\n\nThis makes the relationship between architecture and computation directly observable.\n\nFor example, increasing the output channels of the second convolution affects the number of operations required by that layer.\n\nIncreasing the kernel size also increases computation because each output value depends on more input positions.\n\nBut there is an important separation between the architecture calculator and the trained model comparison.\n\n**Changing the architecture does not automatically tell us its accuracy.**\n\nAn untrained network has no measured classification performance.\n\nTo evaluate a new architecture, we would need to train it, select an appropriate checkpoint using validation data, and measure its performance on held-out test data.\n\nTherefore, the Architecture Sandbox only reports properties that can be calculated analytically.\n\nIt does not invent an accuracy score for hypothetical models.\n\nThis distinction was important to preserve because the project is intended to demonstrate actual engineering trade-offs, not simulated performance claims.\n\nThe results suggest that a small CNN can achieve strong MNIST classification accuracy with a relatively low arithmetic cost.\n\nHowever, several limitations are worth considering.\n\nFirst, the models were trained using a single random seed.\n\nA broader comparison would require repeated training runs to estimate performance variation.\n\nSecond, the experiment uses only three configurations of the same basic architecture.\n\nIt does not establish the optimal CNN for MNIST or the best architecture under a particular computation budget.\n\nThird, MACs estimate only part of the computational work.\n\nActual inference speed depends on hardware, software libraries, memory behavior, and other implementation details.\n\nFinally, MNIST is a relatively simple classification benchmark.\n\nThe relationship between model capacity and accuracy may look very different for larger images, more complex datasets, or real-world deployment conditions.\n\nThe purpose of this project is therefore not to introduce a new neural network architecture or claim state-of-the-art performance.\n\nIt is to build a transparent, reproducible experiment in which the trade-offs between network size, arithmetic cost, and classification performance can be explored.\n\nThis project reinforced a few important ideas about machine learning engineering.\n\n**First, accuracy alone is not enough to evaluate a model.**\n\nTwo models can have similar accuracy while requiring very different amounts of computation.\n\nDepending on the application, the smaller model may be the more practical choice.\n\n**Second, the relationship between model capacity and accuracy is not linear.**\n\nIn this experiment, increasing computational cost substantially produced progressively smaller accuracy gains.\n\nThe largest model performed best on the test set, but the smallest model was much more computationally economical.\n\n**Third, mathematical cost estimates are useful, but they must be interpreted correctly.**\n\nMAC counts make it possible to compare arithmetic complexity without hardware-dependent measurements.\n\nHowever, they should not be confused with actual runtime performance.\n\n**Fourth, intermediate visualizations make neural networks easier to understand.**\n\nInspecting convolutional feature maps helps connect the mathematical architecture to the representations produced during inference.\n\nAnd finally, **a clear separation between measured results and hypothetical configurations is essential**.\n\nWe can calculate the theoretical cost of a network architecture immediately.\n\nBut we cannot know how accurately that architecture will classify images until it has been properly trained and evaluated.\n\nThat difference is central to designing reliable machine learning experiments.\n\nFor this fourth From Lab to Code project, I wanted to investigate a question that extends beyond handwritten-digit recognition:\n\n**How do we balance model accuracy against computational requirements?**\n\nBy training three CNN architectures under comparable conditions, measuring their classification performance, and calculating their arithmetic costs, I built a small environment for exploring this trade-off.\n\nThe interactive application adds another perspective by allowing users to compare actual models, classify their own drawings, inspect intermediate feature maps, and experiment with architectural changes.\n\nThe most interesting result was that the smallest network achieved 96.67% test accuracy using only 55,860 estimated MACs per image.\n\nA significantly larger network improved accuracy to 98.63%, but required approximately 18.5 times more MACs.\n\nNeither model is universally better.\n\nThe appropriate choice depends on the accuracy requirements, the computational constraints, and the environment in which the model will be used.\n\nThat is the engineering idea I wanted to make visible.\n\nAnd it follows the same motivation behind **From Lab to Code**:\n\n**Build something small, understand the underlying principles, make the results observable, and share the implementation.**\n\nThe full source code, trained model checkpoints, reproducible experiment results, tests, and setup instructions are available on GitHub:\n\n[Explore Neural Network Efficiency Lab on GitHub](https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/04-neural-network-efficiency)\n\nThe implementation and interface were developed independently. The project uses the public MNIST dataset with separate attribution for the included samples; no university course code or instructional materials are redistributed.\n\nThanks for reading!", "url": "https://wpnews.pro/news/from-lab-to-code-4-exploring-neural-network-efficiency-with-python-and-pytorch", "canonical_source": "https://dev.to/ertugrulmutlu/from-lab-to-code-4-exploring-neural-network-efficiency-with-python-and-pytorch-1732", "published_at": "2026-11-09 22:18:11+00:00", "updated_at": "2026-11-09 22:23:41.659176+00:00", "lang": "en", "topics": ["machine-learning", "neural-networks", "computer-vision", "ai-tools", "developer-tools"], "entities": ["PyTorch", "MNIST", "Neural Network Efficiency Lab"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/from-lab-to-code-4-exploring-neural-network-efficiency-with-python-and-pytorch", "markdown": "https://wpnews.pro/news/from-lab-to-code-4-exploring-neural-network-efficiency-with-python-and-pytorch.md", "text": "https://wpnews.pro/news/from-lab-to-code-4-exploring-neural-network-efficiency-with-python-and-pytorch.txt", "jsonld": "https://wpnews.pro/news/from-lab-to-code-4-exploring-neural-network-efficiency-with-python-and-pytorch.jsonld"}}