{
  "source_file": "0930",
  "source_platform": "Hashnode",
  "author": "Fadhil Elrizanda",
  "title_listed": "Build and Understand LeNet 5 Architecture Using TensorFlow",
  "keywords": "",
  "domain": "机器学习",
  "content_type": "深度技术解析",
  "is_paid_flag": "否",
  "url": "https://elrizanda.hashnode.dev/build-and-understand-lenet-5-architecture-using-tensorflow",
  "publish_date": "2024-11-30",
  "corpus_stage": "",
  "notes": "",
  "id": "13fa901161ed",
  "collected_at": "2026-10-02T14:12:13",
  "title": "Build and Understand LeNet 5 Architecture Using TensorFlow",
  "body_markdown": "Build and Understand LeNet 5 Architecture Using TensorFlow\nUpdated\nNovember 30, 2024\n•\n10\nmin read\n•\nView as Markdown\nF\nFadhil Elrizanda\nMachine learning algorithm especially in computer vision is rapidly developed. In order to achieve the rapidly algorithm development we must learn continuously from the basic. One of the basic is the development by Yann Lecun is LeNet. We will try to explore the LeNet architecture in this blog\nIntroduction\nLeNet is a series of convolutional neural network which used in hand written dataset as known as MNIST dataset. The idea of building the LeNet is to build CNN based architecture to be able to classify image in image classification task\nLeNet-5\nLeNet-5 is the latest development of the LeNet architecture. LeNet architecture seem are shallower than modern architecture because of lack of computational. In these day, we have more computational capability likes more CPU, high RAM, and rapidly development of GPU. So, at that time, the LeNet architecture are building by the computational constraint.\nThe architecture of LeNet 5 consist of Conv(Convolutional) layer and FC (Fully Connected )layer. The dataset is consist of image with the shape of 28 × 28 × 1 . The dataset also consist of 60,000 data training and 10.000 data testing which is sufficient for training model. There are severals step in this model:\nInput layer\nContain the input size image which is 28×28×1\nConv Layer 1\nThe convolutional layer is used with 5 × 5 kernel with padding = 2 and consist of 6 filter\nAverage Pooling\nTo reduce the size of the feature map, pooling layer is used with the method is averaging the feature map. The pooling layer using stride = 2\nConsist of repeteadly convolutional and pooling layer\nFully connected layer\nThe FC layer consist of 3 layer which is layer with 120, 84, and 10 neuron. The last layer is 10 neuron because it used classify the handwritten digit 0-9\nIn order to gain more knowledge let build the LeNet-5 model using tensorflow\nWorking with Tensorflow\nThe code below is running in google colab\nInstalling Dependencies\n!pip install tensorflow==\n2.15\n!pip install -U tensorboard_plugin_profile\n!pip install wandb\nIn order to work properly, I am using tensorflow 2.15 and tensorboard for evaluation performance. I also use wandb to track my model which is a good practice even with the simple model like LeNet 5 Model\nImport Libraries\nimport\ntensorflow\nas\ntf\nimport\nmatplotlib.pyplot\nas\nplt\nimport\nos, datetime\nimport\nwandb\nfrom\nwandb.integration.keras\nimport\nWandbMetricsLogger\nprint(tf.__version__)\nFrom collecting data until visualizing the performance we need all the library above. Also to faster the model training, we can use GPU that provided in google colab. To check the GPU compatibility we can use code below\ngpus = tf.config.list_physical_devices(\n'GPU'\n)\nif\ngpus:\ntry\n:\nfor\ngpu\nin\ngpus:\n            tf.config.experimental.set_memory_growth(gpu,\nTrue\n)\nexcept\nRuntimeError\nas\ne:\n        print(e)\nAlso don’t forget to login to wandb\nwandb.login(key=\n\"YOUR_KEY\"\n)\nImporting Dataset\nThe mnist dataset already available in tensorflow.data.dataset module. In order to download it run the code below\ndataset = tf.keras.datasets.mnist\n(X_train,y_train), (X_test,y_test) = dataset.load_data()\nprint(X_train.shape, y_train.shape)\nprint(X_test.shape, y_test.shape)\nThe output of the will giving the shape of training and testing data which is 60,000 and 10,000.\nSplitting Data and converting to TF.data\nIn tensorflow, the optimalization data pipeline is using tf.data which can faster the training. tf.data will optimize the memory usage and avoid bottleneck in training pipeline. In order to do this you can try code below\nnum_train_data = X_train.shape[\n0\n]\nnum_test_data = X_test.shape[\n0\n]\ntrain_data = tf.data.Dataset.from_tensor_slices((X_train,y_train))\ntest_dataset = tf.data.Dataset.from_tensor_slices((X_test,y_test))\nvalid_data = test_dataset.take(int(\n0.5\n*num_test_data))\ntest_data = test_dataset.skip(int(\n0.5\n*num_test_data))\nFrom this code, we first count the number of the data of each set (training,testing) in order to splitting the data. After that we will convert the data from numpy array to tf.data using method\nfrom_tensor_slices\nfor each data. Then we split the validation and testing data to 50% and 50% using\n.skip\nand\n.take\n.\nData Distribution\nIn real world cases, the data distribution is often not distributed equally so we need to check whether the data distribution is balance or imbalance.\ndef\ndistribution_class\n(\ntrain_dataset, test_dataset\n):\nlabels = np.concatenate((train_dataset,test_dataset),axis=\n0\n)\n    unique_labels, counts = np.unique(labels,return_counts=\nTrue\n)\n    plt.bar(unique_labels, counts)\n    plt.plot()\n    plt.show()\nreturn\nunique_labels\nunique_label = distribution_class(y_train, y_test)\nThis method will take the numpy array data from the beginning instead of using tf.data. why? because if we use the tf.data it is more slower because the data are fetched in batches instead of all in one. the output of the cell will look like this.\nFrom the distribution we can see the data are balanced.\nData Visualization\nBefore passing the data, we would like to check the data to understand the problem and the behavior of the data. In our case is handwritten digit. To visualize it we can use code below.\nfor\ni, (img, label)\nin\nenumerate(train_data.take(\n16\n)):\n    ax = plt.subplot(\n4\n,\n4\n,i+\n1\n)\n    plt.imshow(img)\n    plt.title(label.numpy())\nIn this code we take 16 data from training data then plotting it with plt. The output should like this.\nThe label is in above of each images and the label and data are correct\nConfigure Parameters\nIn this case we will only configure few parameter of the training pipeline. which is num_classes, batch_size,img_size, etc. The following code to make config parameter data\nconfig = {\n\"num_classes\"\n:\n10\n,\n\"batch_size\"\n:\n64\n,\n\"image_size\"\n:\n28\n,\n\"image_channels\"\n:\n1\n,\n\"buffer_size\"\n:\n10000\n,\n\"learning_rate\"\n:\n1e-3\n,\n\"epochs\"\n:\n200\n,\n\"earlystopping_patience\"\n:\n3\n}\nPreprocessing Data\nSeveral methods is used to preprocessing the data, which is normalizing, reshape, and one hot encoding. We need to normalize the data in order to correct the distribution of the data (it is image pixel). We also need to reshape the data in order to have same size of the image to given to model. One hot encoding needed to encode the label (0-9) to become [0,0,0,0,0,0,0,0,1] (represent label 9). with these we can assume the 10 last neuron to represent each class label.\ndef\nnormalize\n(\nimg\n):\nimg = tf.cast(img,tf.float32)/\n255.0\nreturn\nimg\ndef\none_hot_label\n(\nlabels,num_classes\n):\nlabel = tf.one_hot(labels,depth=num_classes)\nreturn\nlabel\ndef\nreshape_img\n(\nimg,shape,channels\n):\nimg = tf.expand_dims(img,axis=\n-1\n)\n  img = tf.image.resize(img,(shape,shape))\nreturn\nimg\ndef\nimg_normalize_one_hot\n(\nimg,label,config\n):\nimg = reshape_img(img,config[\n\"image_size\"\n],config[\n\"image_channels\"\n])\n    img = normalize(img)\n    label = one_hot_label(label,config[\n\"num_classes\"\n])\nreturn\nimg,label\nAfter building the preprocessing pipeline we can implement it in each set of our data.\nnormalized_train_dataset = (\n    train_data\n    .map(\nlambda\nimg,label: img_normalize_one_hot(img,label,config),num_parallel_calls=tf.data.AUTOTUNE)\n    .batch(config[\n\"batch_size\"\n])\n    .cache()\n    .shuffle(buffer_size=config[\n\"buffer_size\"\n])\n    .prefetch(buffer_size=tf.data.AUTOTUNE)\n)\nnormalized_valid_dataset = (\n    test_data.map(\nlambda\nimg,label : img_normalize_one_hot(img,label,config),num_parallel_calls=tf.data.AUTOTUNE)\n    .batch(config[\n\"batch_size\"\n])\n    .cache()\n    .shuffle(buffer_size=config[\n\"buffer_size\"\n])\n    .prefetch(buffer_size=tf.data.AUTOTUNE)\n)\nnormalized_test_dataset = (valid_data.map(\nlambda\nimg,label : img_normalize_one_hot(img,label,config),num_parallel_calls=tf.data.AUTOTUNE)\n    .batch(config[\n\"batch_size\"\n])\n    .prefetch(buffer_size=tf.data.AUTOTUNE)\n                                          )\nThese code seem complicated but it is realy simple. The each of our set is preprocessed based on previous pipeline. Then we add some method from tf.data to optimize our data to be consumed by the model which is batch, cache, shuffle, and prefetch.\nBuilding Callback\nIn tensorflow we can use callback to customize our training process. Some callback we use in this training are early stopping, checkpoint, and tensorboard.\nearly_stopping_cb = tf.keras.callbacks.EarlyStopping(patience=config[\n\"earlystopping_patience\"\n])\ncheckpoint_cb = tf.keras.callbacks.ModelCheckpoint(\n\"model_cb.keras\"\n,save_best_only=\nTrue\n)\nlogs =\n\"logs/\"\n+ datetime.datetime.now().strftime(\n\"%Y%m%d-%H%M%S\"\n)\ntensorboard_cb = tf.keras.callbacks.TensorBoard(log_dir = logs,\n                                                 histogram_freq =\n1\n,\n                                                 profile_batch =\n'500,520'\n)\nWith 3 different callback we can make sure our training is running without problem.\nInitialize wandb\nrun = wandb.init(\n    project =\n\"lenet-5-scratch\"\n,\n    config = config\n)\nWe use wandb to explore our model performance. The project is project name that we use and config the the config of the training process.\nBuilding Model from scratch\nThere are 3 way to define the model, first is by sequential api, functional api, and class api. In this case we use class api to understand how to building the model from scratch.\nBased on given image (intro section) we know that the LeNet-5 consist of several layer. To better understanding I write in in the table\nLayer\nConfiguration\nDescribtion\nInput Layer\n28×28\nThe first layer\nconv layer 1\nfilter = 6, kernel_size (5×5), padding=2\nFirst conv layer\nAverage Pooling layer\nstrides (2,2)\nFirst pooling layer\nconv layer 2\nfilter = 16, kernel_size (5×5)\nsec conv layer\nAverage Pooling layer\nstrides (2,2)\nsec pooling layer\nDense layer 1\n120 Neurons\nFC 1 layer\nDense layer 2\n84 Neurons\nFC 2 layer\nOutput Layer\n10 Neurons\nOutput Layer\nFrom given table we can construct it in tensorflow as above\nclass\nlenet_5\n(\ntf.keras.Model\n):\ndef\n__init__\n(\nself,input_shape,outputs, **kwargs\n):\nsuper().__init__(**kwargs)\n        self.conv1 = tf.keras.layers.Conv2D(filters=\n6\n,strides=(\n1\n,\n1\n),kernel_size=(\n5\n,\n5\n),activation=\n\"tanh\"\n)\n        self.pool1 = tf.keras.layers.AveragePooling2D(pool_size=(\n2\n,\n2\n),strides=(\n2\n,\n2\n))\n        self.conv2 = tf.keras.layers.Conv2D(filters=\n6\n,strides=(\n1\n,\n1\n),kernel_size=(\n5\n,\n5\n),activation=\n\"tanh\"\n)\n        self.pool2 = tf.keras.layers.AveragePooling2D(pool_size=(\n2\n,\n2\n),strides=(\n2\n,\n2\n))\n        self.flatten = tf.keras.layers.Flatten()\n        self.dense1 = tf.keras.layers.Dense(units=\n84\n,activation=\n\"tanh\"\n)\n        self.output_layer = tf.keras.layers.Dense(units=outputs,activation=\n\"softmax\"\n)\ndef\ncall\n(\nself,inputs\n):\nx = self.conv1(inputs)\n        x = self.pool1(x)\n        x = self.conv2(x)\n        x = self.pool2(x)\n        x = self.flatten(x)\n        x = self.dense1(x)\n        x = self.output_layer(x)\nreturn\nx\n\nmodel = lenet_5((\n28\n,\n28\n,\n1\n),\n10\n)\nmodel.compile(loss=\n'categorical_crossentropy'\n, optimizer=tf.keras.optimizers.SGD(learning_rate=config[\n\"learning_rate\"\n]), metrics=[\n'accuracy'\n])\nWe can see that the activation function is tanh which is rarely used today. But back to that day the tanh activation function is highly used\nTraining the model\nIn order to train the model we can simply run code below\nhistory = model.fit(normalized_train_dataset,validation_data=normalized_valid_dataset,\n                    epochs=config[\n\"epochs\"\n],callbacks=[early_stopping_cb,checkpoint_cb,\n                                          tensorboard_cb,WandbMetricsLogger(log_freq=\n10\n)])\nThe code below is using the config parameter that we described earlier\nEvaluate Performance\nAfter training the model, we need to check the performance of the model. First we can plot the loss and accuracy\nplt.plot(history.history[\n\"accuracy\"\n])\nplt.plot(history.history[\n\"val_accuracy\"\n])\nplt.title(\n\"Accuracy\"\n)\nplt.xlabel(\n\"Epochs\"\n)\nplt.ylabel(\n\"Accuracy\"\n)\nplt.plot(history.history[\n\"loss\"\n])\nplt.plot(history.history[\n\"val_loss\"\n])\nplt.xlabel(\n\"Epochs\"\n)\nplt.ylabel(\n\"Accuracy\"\n)\nEvaluate on Testing Dataset\nBeside monitor the performance by reading from training and validation set, we need to evaluate based on test set.\nmodel.evaluate(normalized_test_dataset)\n# output [0.10710800439119339, 0.9684000015258789]\nFrom the model evaluation the loss on testing set is 0.419 and the accuracy is 0.87. We can still improve it by tuning the parameter to train the model. Example by increasing the epochs.\nPlot The Predicted Image\nTo gain more information about the performance, we can try to plot the prediction from testing set. First we need to take the prediction from model and the actual label from testing dataset.\n# Generate predictions\ny_pred_prob = model.predict(normalized_test_dataset)\n# Predicted probabilities\ny_pred = np.argmax(y_pred_prob, axis=\n1\n)\n# Convert probabilities to class indices\n# Extract true labels from dataset and convert to class indices\ny_true = np.concatenate([np.argmax(label.numpy(), axis=\n1\n)\nfor\n_, label\nin\nnormalized_test_dataset])\nprint(\nf\"y_true shape:\n{y_true.shape}\n, unique values:\n{np.unique(y_true)}\n\"\n)\nprint(\nf\"y_pred shape:\n{y_pred.shape}\n, unique values:\n{np.unique(y_pred)}\n\"\n)\n\nnum_uniq,count = np.unique(y_pred,return_counts=\nTrue\n)\nprint(num_uniq,count)\n\nnum_uniq,count = np.unique(y_true,return_counts=\nTrue\n)\nprint(num_uniq,count)\n# Validate shapes\nprint(\nf\"y_pred shape:\n{y_pred.shape}\n, y_true shape:\n{y_true.shape}\n\"\n)\nAfter getting the label, we will plot 16 images and compare from prediction and actual label. In this code also integrate wandb table to save the prediction images so we can store and evaluate on wandb\nimg_num =\n0\nIMG_PLOT =\n16\nplt.figure(figsize=(\n6\n,\n6\n))\ntable = wandb.Table(columns=[\n\"Image\"\n,\n\"True Label\"\n,\n\"Predicted Label\"\n])\nfor\nimg_batches, label\nin\nnormalized_test_dataset.take(\n1\n):\n  batches = img.shape[\n0\n]\nfor\nb\nin\nrange(batches):\nif\nimg_num >= IMG_PLOT:\nbreak\nax = plt.subplot(\n4\n,\n4\n,img_num+\n1\n)\n    img = img_batches[b].numpy().squeeze()\n    plt.imshow(img)\n    plt.title(\nf\"actual:\n{np.argmax(label[b].numpy())}\n,pred :\n{y_pred[b]}\n\"\n,fontsize=\n6\n)\n    img_num+=\n1\ntable.add_data(wandb.Image(img), np.argmax(label[b].numpy()), y_pred[b])\n\nrun.log({\n\"predictions_table\"\n: table})\nrun.finish()\nEvaluate using Confusion Matrix\nIn classification task, confusion matrix need to compute to measure the performance of the model.\n# Compute confusion matrix\nconfusion_matrix = tf.math.confusion_matrix(\n    y_true,\n    y_pred,\n    num_classes=config[\n\"num_classes\"\n]\n)\n# Display confusion matrix\nprint(\n\"Confusion Matrix:\"\n)\nprint(confusion_matrix.numpy())\nTo visualize the confusion matrix you can use seaborn and making the classification report from sklearn.\nimport\nseaborn\nas\nsns\nplt.figure()\nsns.heatmap(confusion_matrix.numpy(),annot=\nTrue\n,fmt=\n\"d\"\n,cmap=\n\"Blues\"\n)\nplt.xlabel(\n\"Predicted Label\"\n)\nplt.ylabel(\n\"Actual Label\"\n)\nplt.show()\nfrom\nsklearn.metrics\nimport\nclassification_report\nreport = classification_report(y_true,y_pred,target_names=[\nf\"Class\n{i}\n\"\nfor\ni\nin\nrange(config[\n\"num_classes\"\n])])\nprint(\n\"Classification Report\"\n)\nprint(report)\nEvaluation Using Tensorboard\nTensorflow have tensorboard to visualize and evaluate the training. It realy helpful to gain information about the model and how to achieve better performance. First we need to move the logs file\nimport\npathlib\nlog_files = pathlib.Path(\n'./logs'\n).glob(\n'*/*/events.out.tfevents*'\n)\nfor\nfile\nin\nlog_files:\n  !mv {file} {file.parent.parent}\nThen use following code to run tensorboard\n%load_ext tensorboard\n%tensorboard --logdir=logs\nSome information that can be use to measure performance using tensorboard are in following images\nFeel free to explore and discover more about tensorboard\nEvaluate on Wandb\nWandb is a good MLOps to monitore the model in the ML pipelines. However in this case we only train on small model which is not utilize the wandb efficiently but it is a good practice to use wandb. Some of the feature in wandb can be shown in images below.\nSummary\nIn summary we have be able to building LeNet-5 from scratch as beginning for our journey in computer vision. LeNet-5 is one of the foundation for computer vision. But recently, many algorithm with higher computation and better accuracy have been developed. We will talk about it later. Also you may confuse of using tensorflow, tensorboard, and wandb. We will breakthrough this section more later.\nFull code :\nCode\n#\ntensorflow\n#\ncomputer-vision\n#\nmachine-learning\n#\npython",
  "metadata": {
    "title": "Build and Understand LeNet 5 Architecture Using TensorFlow",
    "description": null,
    "keywords": null,
    "author": null
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/13fa901161ed/code/body-000.txt",
      "chars": 92,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-001.txt",
      "chars": 166,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-002.txt",
      "chars": 200,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-003.txt",
      "chars": 28,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-004.txt",
      "chars": 161,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-005.txt",
      "chars": 309,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-006.txt",
      "chars": 325,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-007.txt",
      "chars": 134,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-008.txt",
      "chars": 217,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-009.txt",
      "chars": 524,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-010.txt",
      "chars": 830,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-011.txt",
      "chars": 461,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-012.txt",
      "chars": 73,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-013.txt",
      "chars": 1124,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-014.txt",
      "chars": 266,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-015.txt",
      "chars": 146,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-016.txt",
      "chars": 116,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-017.txt",
      "chars": 91,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-018.txt",
      "chars": 742,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-019.txt",
      "chars": 602,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-020.txt",
      "chars": 223,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-021.txt",
      "chars": 173,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-022.txt",
      "chars": 207,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-023.txt",
      "chars": 140,
      "language": "text"
    },
    {
      "file": "attachments/13fa901161ed/code/body-024.txt",
      "chars": 49,
      "language": "text"
    }
  ],
  "images": [
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732977692033/b135d582-126e-4dc7-bcda-bbd0c5a591b7.jpeg",
      "bytes": 33287,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/000.jpeg",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732977692033/b135d582-126e-4dc7-bcda-bbd0c5a591b7.jpeg",
      "alt": "Build and Understand LeNet 5 Architecture Using TensorFlow",
      "downloaded": true
    },
    {
      "status": "http_400",
      "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/LeNet-5_architecture_block_diagram.svg/320px-LeNet-5_architecture_block_diagram.svg.png",
      "src": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/LeNet-5_architecture_block_diagram.svg/320px-LeNet-5_architecture_block_diagram.svg.png",
      "alt": "undefined",
      "downloaded": false,
      "retried_at": "2026-10-20T17:39:23Z"
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732932232982/fec088d5-66c8-470b-9365-12b83bd530ea.png",
      "bytes": 12851,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/003.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732932232982/fec088d5-66c8-470b-9365-12b83bd530ea.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732932400743/61bb7606-4c53-4d3c-b799-668868f046e3.png",
      "bytes": 48299,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/004.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732932400743/61bb7606-4c53-4d3c-b799-668868f046e3.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969096335/3d4605f1-5d56-4f51-89da-b3d4809fe8d0.png",
      "bytes": 19096,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/005.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969096335/3d4605f1-5d56-4f51-89da-b3d4809fe8d0.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969115460/3b2393a9-02bb-4b37-b14b-a64bb54ede3a.png",
      "bytes": 16090,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/006.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969115460/3b2393a9-02bb-4b37-b14b-a64bb54ede3a.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969221237/50683e8f-0896-487c-9be2-d9e5a85188ec.png",
      "bytes": 38193,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/007.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969221237/50683e8f-0896-487c-9be2-d9e5a85188ec.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969242847/15adf968-c020-47a7-a66c-90cc5dc69138.png",
      "bytes": 35355,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/008.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969242847/15adf968-c020-47a7-a66c-90cc5dc69138.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969263737/29e2712d-38bf-4d88-acab-825b304180e5.png",
      "bytes": 11479,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/009.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969263737/29e2712d-38bf-4d88-acab-825b304180e5.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732968891869/194fa81f-1005-4a26-83bf-33f374897529.png",
      "bytes": 14459,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/010.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732968891869/194fa81f-1005-4a26-83bf-33f374897529.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732968906606/f96faf45-193b-491b-8a23-70212c42a113.png",
      "bytes": 12823,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/011.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732968906606/f96faf45-193b-491b-8a23-70212c42a113.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969001476/4c1839ae-54c0-4aef-8905-88b0c07cc3cc.png",
      "bytes": 20715,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/012.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969001476/4c1839ae-54c0-4aef-8905-88b0c07cc3cc.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732968948190/b79d8553-00e2-4357-83ef-31f1b779150a.png",
      "bytes": 47554,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/013.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732968948190/b79d8553-00e2-4357-83ef-31f1b779150a.png",
      "alt": "",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969045616/8cb673f8-ff68-4274-9b2e-381a42eabd8a.png",
      "bytes": 27367,
      "reused": true,
      "local_path": "attachments/13fa901161ed/images/014.png",
      "src": "https://cdn.hashnode.com/res/hashnode/image/upload/v1732969045616/8cb673f8-ff68-4274-9b2e-381a42eabd8a.png",
      "alt": "",
      "downloaded": true
    }
  ],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://elrizanda.hashnode.dev/build-and-understand-lenet-5-architecture-using-tensorflow.md",
      "label": "View as Markdown",
      "type": "a"
    },
    {
      "href": "https://hashnode.com/@fadhilelrizanda",
      "label": "F",
      "type": "a"
    },
    {
      "href": "https://www.linkedin.com/in/fadhil-elrizanda/",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://github.com/fadhilelrizanda/build-lenet5-scratch-tf",
      "label": "Code",
      "type": "a"
    },
    {
      "href": "https://elrizanda.hashnode.dev/tag/tensorflow",
      "label": "# tensorflow",
      "type": "a"
    },
    {
      "href": "https://elrizanda.hashnode.dev/tag/computer-vision",
      "label": "# computer-vision",
      "type": "a"
    },
    {
      "href": "https://elrizanda.hashnode.dev/tag/machine-learning",
      "label": "# machine-learning",
      "type": "a"
    },
    {
      "href": "https://elrizanda.hashnode.dev/tag/python",
      "label": "# python",
      "type": "a"
    }
  ],
  "retry_at": "2026-10-03T08:09:56",
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:51:00Z",
  "source_url": "https://elrizanda.hashnode.dev/build-and-understand-lenet-5-architecture-using-tensorflow",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 16552,
  "expected_body_images": 14,
  "discovery_status": "ok",
  "delivery_status": "partial",
  "missing_reason": "http_400",
  "raw_html": "attachments/13fa901161ed/pages/source.html",
  "local_html": "attachments/13fa901161ed/pages/article.html",
  "body_base_path": "attachments/13fa901161ed/pages/",
  "body_markdown_path": "attachments/13fa901161ed/pages/article.md"
}