{"cells":[{"metadata":{},"cell_type":"markdown","source":"# <span style=\"color:blue\">Bengali.AI Handwritten Grapheme Classification</span>\n\n## <span style=\"color:purple\">This kernal is used to compare both mono-output and multi-output CNN model for this perticular problem</span>\n\n### This Kernal is for beginners who are new for KERAS and TnsorFlow,\n### This kernal is teaches how to create your own custom CNN models layer by layer !!\n\n\nTo understand this you need to know ,\n    1. Python\n    2. Basics of Keras and TF\n    3. Basic Understanding for CNN\n    \n    \n### <span style=\"color:red\">Upvote If you like</span>\n#### If you are ready for a level up - [Bengali.AI : Tutorial-Design ResNet Layer by Layer](https://www.kaggle.com/chekoduadarsh/bengali-ai-tutorial-design-resnet-layer-by-layer)\n#### If you want to Ensemble a CNN with ML (On a different challange but adaptable) - [hybrid CNN + XGboost + GNB, accuracy ~ 99%](https://www.kaggle.com/chekoduadarsh/hybrid-cnn-xgboost-gnb-accuracy-99)"},{"metadata":{},"cell_type":"markdown","source":"## Import  Libraries\nImport all the needed libraries all at one place so that you don't have to be confused."},{"metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","trusted":false},"cell_type":"code","source":"\n#Load em...\nfrom matplotlib import pyplot as plt\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport PIL.Image as Image, PIL.ImageDraw as ImageDraw, PIL.ImageFont as ImageFont\nimport random \nimport os\nimport cv2\nimport gc\nfrom tqdm.auto import tqdm\n\n\nimport numpy as np\nimport keras\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import Dense, Dropout, Flatten\nfrom tensorflow.keras.layers import Conv2D, MaxPooling2D\nfrom tensorflow.keras.optimizers import SGD\nfrom tensorflow.keras.layers import BatchNormalization\nfrom tensorflow.keras.layers import LeakyReLU\nfrom tensorflow.keras.models import clone_model\nfrom sklearn.model_selection import train_test_split\nfrom tensorflow.keras.callbacks import ReduceLROnPlateau\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.utils import plot_model\n\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nimport numpy as np\nimport datetime as dt\n\n\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Load the required Data\nWe need only train.csv and parquets which will be loaded while training!!\nDont waste RAM by loading un wanted DATA"},{"metadata":{"trusted":true},"cell_type":"code","source":"train_data = pd.read_csv('/kaggle/input/bengaliai-cv19/train.csv')","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":false},"cell_type":"code","source":"def resize(df, size=64, need_progress_bar=True):\n    resized = {}\n    for i in range(df.shape[0]):\n        image = cv2.resize(df.loc[df.index[i]].values.reshape(137,236),(size,size))\n        resized[df.index[i]] = image.reshape(-1)\n    resized = pd.DataFrame(resized).T\n    return resized","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Use the install tensorflow to change tensorflow version of the program if you find a version related bug"},{"metadata":{"trusted":false},"cell_type":"code","source":"#!pip install tensorflow","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":" print(tf.version)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Design- CNN-3model(mono-output)\n\nThis kind of neural network will have 1 input and 1 output since we need to find Grapheme root, Vowel and Consonant we need to copy the model thrice and train them separately. This increases memory/RAM required to solve thiss probem but this gives us freedom to edit and manipulate each model according to the classification complexity.\n\n### <span style=\"color:red\">Advantages</span>\n\n1. Control over each independent model\n2. Adaptability on each model and can use or edit them separately\n3. Can change/tune hyperparameters for each module separately according to the classification complexity\n\n\n### <span style=\"color:red\">Disadvantages</span>\n1. High RAM/Memory uage\n2. More training time\n3. makes it tedious to tune hyperparameters independently"},{"metadata":{"trusted":false},"cell_type":"code","source":"def cnn3model(outputsize):\n    inputs = keras.Input(shape=(64,64,1))\n    x = layers.Conv2D(32, (3,3), activation='relu')(inputs)\n    model = tf.nn.leaky_relu(x, alpha=0.01, name='Leaky_ReLU') \n    model = layers.Conv2D(filters=32, kernel_size=(3, 3), padding='SAME', activation='relu')(model)\n    model = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \n    model = layers.BatchNormalization(momentum=0.15)(model)\n    model = layers.MaxPool2D(pool_size=(2, 2))(model)\n    model = layers.Conv2D(filters=32, kernel_size=(5, 5), padding='SAME', activation='relu')(model)\n    model = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \n    model = layers.Dropout(rate=0.3)(model)\n\n    model = layers.Conv2D(filters=64, kernel_size=(3, 3), padding='SAME', activation='relu')(model)\n    model = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \n    model = layers.BatchNormalization(momentum=0.15)(model)\n    model = layers.Dropout(rate=0.3)(model)\n    model = layers.Conv2D(filters=64, kernel_size=(3, 3), padding='SAME', activation='relu')(model)\n    model = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \n    model = layers.Conv2D(filters=64, kernel_size=(3, 3), padding='SAME', activation='relu')(model)\n    model = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \n    model = layers.BatchNormalization(momentum=0.15)(model)\n    model = layers.MaxPool2D(pool_size=(2, 2))(model)\n    model = layers.Conv2D(filters=64, kernel_size=(5, 5), padding='SAME', activation='relu')(model)\n    model = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \n    model = layers.BatchNormalization(momentum=0.15)(model)\n    model = layers.Dropout(rate=0.3)(model)\n\n    model = layers.Flatten()(model)\n    model = layers.Dense(1024, activation = \"relu\")(model)\n    model = layers.Dropout(rate=0.3)(model)\n    dense = layers.Dense(512, activation = \"relu\")(model)\n    \n    output = layers.Dense(outputsize, activation='softmax')(dense)\n    model = keras.Model(inputs, output)\n    return model","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Copy the model into 3 different models"},{"metadata":{"trusted":false},"cell_type":"code","source":"cnn3model_root = cnn3model(168)\ncnn3model_vowel = cnn3model(11)\ncnn3model_consonant = cnn3model(7)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Using Adam optimizer one of the best algorithms \n\nYou can tryout \"RMSProp\" , \"Adadelta\" to see the difference"},{"metadata":{"trusted":false},"cell_type":"code","source":"cnn3model_root.compile(optimizer=\"adam\", loss=['categorical_crossentropy'], metrics=['accuracy']) # Adam optimizer with catagorical_crossentropy modern best\ncnn3model_vowel.compile(optimizer=\"adam\", loss=['categorical_crossentropy'], metrics=['accuracy'])\ncnn3model_consonant.compile(optimizer=\"adam\", loss=['categorical_crossentropy'], metrics=['accuracy'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"plot_model(cnn3model_root, to_file='model1.png')","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"plot_model(cnn3model_vowel, to_file='model2.png')","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"plot_model(cnn3model_consonant, to_file='mode3.png')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Design- CNN-1model(multi-output)\n\nThis is a bit complex model where we use 1 input and multiple output CNN model, In this perticualr case 3 outputs to predict each Grapheme root, Vowel and Consonent. \n### <span style=\"color:red\">Advantages</span>\n1. Reduced memory/RAM usage\n2. Less training time\n\n### <span style=\"color:red\">Disadvantages</span>\n1. Bit hard to design \n2. Can edit Each model separately"},{"metadata":{"trusted":false},"cell_type":"code","source":"inputs = keras.Input(shape = (64, 64, 1))\n\nmodel = layers.Conv2D(filters=32, kernel_size=(3, 3), padding='SAME', activation='relu')(inputs)\nmodel = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \nmodel = layers.Conv2D(filters=32, kernel_size=(3, 3), padding='SAME', activation='relu')(model)\nmodel = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \nmodel = layers.BatchNormalization(momentum=0.15)(model)\nmodel = layers.MaxPool2D(pool_size=(2, 2))(model)\nmodel = layers.Conv2D(filters=32, kernel_size=(5, 5), padding='SAME', activation='relu')(model)\nmodel = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \nmodel = layers.Dropout(rate=0.3)(model)\n\nmodel = layers.Conv2D(filters=64, kernel_size=(3, 3), padding='SAME', activation='relu')(model)\nmodel = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \nmodel = layers.BatchNormalization(momentum=0.15)(model)\nmodel = layers.Dropout(rate=0.3)(model)\nmodel = layers.Conv2D(filters=64, kernel_size=(3, 3), padding='SAME', activation='relu')(model)\nmodel = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \nmodel = layers.Conv2D(filters=64, kernel_size=(3, 3), padding='SAME', activation='relu')(model)\nmodel = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \nmodel = layers.BatchNormalization(momentum=0.15)(model)\nmodel = layers.MaxPool2D(pool_size=(2, 2))(model)\nmodel = layers.Conv2D(filters=64, kernel_size=(5, 5), padding='SAME', activation='relu')(model)\nmodel = tf.nn.leaky_relu(model, alpha=0.01, name='Leaky_ReLU') \nmodel = layers.BatchNormalization(momentum=0.15)(model)\nmodel = layers.Dropout(rate=0.3)(model)\n\nmodel = layers.Flatten()(model)\nmodel = layers.Dense(1024, activation = \"relu\")(model)\nmodel = layers.Dropout(rate=0.3)(model)\ndense = layers.Dense(512, activation = \"relu\")(model)\n    \nout_root = layers.Dense(168, activation='softmax', name='root_out')(dense) \nout_vowel = layers.Dense(11, activation='softmax', name='vowel_out')(dense) \nout_consonant = layers.Dense(7, activation='softmax', name='consonant_out')(dense)\n    \n\ncnn1model = keras.Model(inputs=inputs, outputs=[out_root, out_vowel, out_consonant])\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"cnn1model.compile(optimizer=\"adam\", loss=['categorical_crossentropy','categorical_crossentropy','categorical_crossentropy'], metrics=['accuracy']) # Adam optimizer with catagorical_crossentropy modern best","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"plot_model(cnn1model, to_file='mode4.png')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Train- 3model(mono-output)\nTo Train the model I made python dictionary which has all the trained model, which will be called training one after another, So we will be using Image Generator to augment the data and introduce randomness into our dataset, this will help us to achive better accuracy score.\n\nNOTE :- Calling .fit again after training will fit the new data to previously trained model so there is no discontinuity\n\nNOTE :- Set epochs to 25~30 for better results\n\nNOTE :- Set batchsize to 128 for better results"},{"metadata":{"trusted":true},"cell_type":"code","source":"batch_size = 64\nepochs = 5","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"model_dict = {\n    'grapheme_root': cnn3model_root,\n    'vowel_diacritic': cnn3model_vowel,\n    'consonant_diacritic': cnn3model_consonant\n}\n\nhistory_list = []\ntrain_data = pd.read_csv('/kaggle/input/bengaliai-cv19/train.csv')","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"for i in range(1):\n    b_train_data =  pd.merge(pd.read_parquet(f'/kaggle/input/bengaliai-cv19/train_image_data_{i}.parquet'), train_data, on='image_id').drop(['image_id'], axis=1)\n    print(\"Data Loaded\")\n    train_image = b_train_data.drop(['grapheme_root', 'vowel_diacritic', 'consonant_diacritic','grapheme'], axis=1)\n    train_image = resize(train_image)/255\n    train_image = train_image.values.reshape(-1, 64, 64, 1) # Image with 64x64x1 diamentions\n    \n    for target in ['grapheme_root', 'vowel_diacritic', 'consonant_diacritic']:\n        Y_train = b_train_data[target]\n        Y_train = pd.get_dummies(Y_train).values\n        x_train, x_test, y_train, y_test = train_test_split(train_image, Y_train, test_size=0.05, random_state=666)\n        datagen = ImageDataGenerator(\n            featurewise_center=False,  # set input mean to 0 over the dataset\n            samplewise_center=False,  # set each sample mean to 0\n            featurewise_std_normalization=False,  # divide inputs by std of the dataset\n            samplewise_std_normalization=False,  # divide each input by its std\n            zca_whitening=False,  # apply ZCA whitening\n            rotation_range=8,  # randomly rotate images in the range (degrees, 0 to 180)\n            zoom_range = 0.15, # Randomly zoom image \n            width_shift_range=0.15,  # randomly shift images horizontally (fraction of total width)\n            height_shift_range=0.15,  # randomly shift images vertically (fraction of total height)\n            horizontal_flip=False,  # randomly flip images\n            vertical_flip=False)  # randomly flip images\n\n\n        # This will just calculate parameters required to augment the given data. This won't perform any augmentations\n        datagen.fit(x_train)\n        history = model_dict[target].fit_generator(datagen.flow(x_train, y_train, batch_size=batch_size),\n                                      epochs = epochs, validation_data = (x_test, y_test),\n                                      steps_per_epoch=x_train.shape[0] // batch_size)\n        history_list.append(history)\n        #histories.append(history)\n        del x_train\n        del x_test\n        del y_train\n        del y_test\n        history_list.append(history)\n        gc.collect()  #Garbage Collector\n        \n    # Delete to reduce memory usage\n    del train_image\n    del train_data\n    gc.collect()   #Garbage Collector","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Use Garbage Collector Wisely to save RAM"},{"metadata":{"trusted":false},"cell_type":"code","source":"for history in history_list:\n    print(history.history)\n    plt.figure()\n    plt.plot(np.arange(0, epochs), history.history['loss'], label='train_loss')\n    plt.plot(np.arange(0, epochs), history.history['val_loss'], label='val_loss')\n    plt.title(\"Loss\")\n    plt.xlabel('Epoch #')\n    plt.ylabel('Loss')\n    plt.legend(['Train', 'Val'], loc='upper left')\n    plt.show()\n    \n    \n    plt.figure()\n    plt.plot(np.arange(0, epochs), history.history['accuracy'], label='train_acc')\n    plt.plot(np.arange(0, epochs), history.history['val_accuracy'], label='val_accuracy')\n    plt.title(\"Accuracy\")\n    plt.xlabel('Epoch #')\n    plt.ylabel('Accuracy')\n    plt.legend(['Train', 'Val'], loc='upper left')\n    plt.show()\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Train-1model(multi-output)\nHere I had to make a custom multi-datagenerator which takes into consideration of 3-output case, here we will be saving more training time than previous model.\n\n\nNOTE :- Set epochs to 25~30 for better results\n\nNOTE :- Set batchsize to 128 for better results"},{"metadata":{"trusted":true},"cell_type":"code","source":"batch_size = 64\nepochs = 5","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"train_data = pd.read_csv('/kaggle/input/bengaliai-cv19/train.csv')\nhistory_list = []","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"class MultiOutputDataGenerator(keras.preprocessing.image.ImageDataGenerator):\n\n    def flow(self,\n             x,\n             y=None,\n             batch_size=32,\n             shuffle=True,\n             sample_weight=None,\n             seed=None,\n             save_to_dir=None,\n             save_prefix='',\n             save_format='png',\n             subset=None):\n\n        targets = None\n        target_lengths = {}\n        ordered_outputs = []\n        for output, target in y.items():\n            if targets is None:\n                targets = target\n            else:\n                targets = np.concatenate((targets, target), axis=1)\n            target_lengths[output] = target.shape[1]\n            ordered_outputs.append(output)\n\n\n        for flowx, flowy in super().flow(x, targets, batch_size=batch_size,\n                                         shuffle=shuffle):\n            target_dict = {}\n            i = 0\n            for output in ordered_outputs:\n                target_length = target_lengths[output]\n                target_dict[output] = flowy[:, i: i + target_length]\n                i += target_length\n\n            yield flowx, target_dict","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"for i in range(1):\n    b_train_data =  pd.merge(pd.read_parquet(f'/kaggle/input/bengaliai-cv19/train_image_data_{i}.parquet'), train_data, on='image_id').drop(['image_id'], axis =1)\n    print(\"Data Loaded\")\n    #train_image = b_train_data.drop(['grapheme_root', 'vowel_diacritic', 'consonant_diacritic','grapheme'], axis=1)\n    #train_image = resize(b_train_data.drop(['grapheme_root', 'vowel_diacritic', 'consonant_diacritic','grapheme'], axis=1))/255\n    #train_image = (resize(b_train_data.drop(['grapheme_root', 'vowel_diacritic', 'consonant_diacritic','grapheme'], axis=1))/255).values.reshape(-1, 64, 64, 1) # Image with 64x64x1 diamentions\n    #Y_train_root = pd.get_dummies(b_train_data['grapheme_root']).values\n    #Y_train_vowel = pd.get_dummies(b_train_data['vowel_diacritic']).values\n    #Y_train_consonant = pd.get_dummies(b_train_data['consonant_diacritic']).values\n    x_train, x_test, y_train_root, y_test_root, y_train_vowel, y_test_vowel, y_train_consonant, y_test_consonant = train_test_split((resize(b_train_data.drop(['grapheme_root', 'vowel_diacritic', 'consonant_diacritic','grapheme'], axis=1))/255).values.reshape(-1, 64, 64, 1), pd.get_dummies(b_train_data['grapheme_root']).values, pd.get_dummies(b_train_data['vowel_diacritic']).values, pd.get_dummies(b_train_data['consonant_diacritic']).values, test_size=0.08, random_state=666)\n    \n    del b_train_data\n    gc.collect()\n    \n    #del Y_train_root, Y_train_vowel, Y_train_consonant\n    print(\"Del1\")\n    datagen = MultiOutputDataGenerator(\n        featurewise_center=False,  # set input mean to 0 over the dataset\n        samplewise_center=False,  # set each sample mean to 0\n        featurewise_std_normalization=False,  # divide inputs by std of the dataset\n        samplewise_std_normalization=False,  # divide each input by its std\n        zca_whitening=False,  # apply ZCA whitening\n        rotation_range=8,  # randomly rotate images in the range (degrees, 0 to 180)\n        zoom_range = 0.20, # Randomly zoom image \n        width_shift_range=0.20,  # randomly shift images horizontally (fraction of total width)\n        height_shift_range=0.20,  # randomly shift images vertically (fraction of total height)\n        horizontal_flip=True,  # randomly flip images\n        vertical_flip=False)  # randomly flip images\n\n\n        # This will just calculate parameters required to augment the given data. This won't perform any augmentations\n    datagen.fit(x_train)\n    history = cnn1model.fit_generator(datagen.flow(x_train, {'root_out': y_train_root, 'vowel_out': y_train_vowel, 'consonant_out': y_train_consonant}, batch_size=batch_size),\n            epochs = epochs, validation_data = (x_test, [y_test_root, y_test_vowel, y_test_consonant]), \n            steps_per_epoch=x_train.shape[0] // batch_size)\n                                  \n    history_list.append(history)\n    del datagen\n    del x_train\n    del x_test\n    del y_train_root\n    del y_test_root\n    del y_train_vowel\n    del y_test_vowel\n    del y_train_consonant\n    del y_test_consonant    \n    print(\"Del2\")\n    gc.collect()\ndel train_data\ngc.collect()","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"%matplotlib inline\ndef plot_loss(his, epoch, title):\n    plt.figure()\n    plt.plot(np.arange(0, epoch), his.history['loss'], label='train_loss')\n    plt.plot(np.arange(0, epoch), his.history['root_out_loss'], label='train_root_loss')\n    plt.plot(np.arange(0, epoch), his.history['vowel_out_loss'], label='train_vowel_loss')\n    plt.plot(np.arange(0, epoch), his.history['consonant_out_loss'], label='train_consonant_loss')\n    \n    plt.plot(np.arange(0, epoch), his.history['val_root_out_loss'], label='val_train_root_loss')\n    plt.plot(np.arange(0, epoch), his.history['val_vowel_out_loss'], label='val_train_vowel_loss')\n    plt.plot(np.arange(0, epoch), his.history['val_consonant_out_loss'], label='val_train_consonant_loss')\n    \n    plt.title(title)\n    plt.xlabel('Epoch #')\n    plt.ylabel('Loss')\n    plt.legend(loc='upper right')\n    plt.show()\n\ndef plot_acc(his, epoch, title):\n    plt.figure()\n    plt.plot(np.arange(0, epoch), his.history['root_out_accuracy'], label='train_root_acc')\n    plt.plot(np.arange(0, epoch), his.history['vowel_out_accuracy'], label='train_vowel_accuracy')\n    plt.plot(np.arange(0, epoch), his.history['consonant_out_accuracy'], label='train_consonant_accuracy')\n    \n    plt.plot(np.arange(0, epoch), his.history['val_root_out_accuracy'], label='val_root_acc')\n    plt.plot(np.arange(0, epoch), his.history['val_vowel_out_accuracy'], label='val_vowel_accuracy')\n    plt.plot(np.arange(0, epoch), his.history['val_consonant_out_accuracy'], label='val_consonant_accuracy')\n    plt.title(title)\n    plt.xlabel('Epoch #')\n    plt.ylabel('Accuracy')\n    plt.legend(loc='upper right')\n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"for dataset in range(1):\n    plot_loss(history_list[dataset], epochs, f'Training Dataset: {dataset}')\n    plot_acc(history_list[dataset], epochs, f'Training Dataset: {dataset}')\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":false},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.6.6"}},"nbformat":4,"nbformat_minor":1}