{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nfrom sklearn.model_selection import train_test_split\nfrom keras.utils.np_utils import to_categorical\nfrom keras.models import Sequential\nfrom keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPool2D, AvgPool2D, BatchNormalization, Reshape\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom keras.callbacks import LearningRateScheduler\nimport matplotlib.pyplot as plt\nimport glob\nimport os\nfrom PIL import Image\nfrom zipfile import ZipFile\nimport tensorflow as tf\nfrom tensorflow import keras\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2022-01-28T14:13:33.075548Z","iopub.execute_input":"2022-01-28T14:13:33.075861Z","iopub.status.idle":"2022-01-28T14:13:33.088561Z","shell.execute_reply.started":"2022-01-28T14:13:33.075830Z","shell.execute_reply":"2022-01-28T14:13:33.087842Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# File Loading\n# https://www.kaggle.com/sohier/getting-started-loading-the-images\n# https://www.kaggle.com/vanausloos/how-to-read-images-in-python\n# Better File Loading\n# https://www.kaggle.com/romazlobin/cats-vs-dogs\n\nbase_dir = '../input/dogs-vs-cats-redux-kernels-edition'\nwith ZipFile(os.path.join(base_dir, 'train.zip')) as train_zip:\n    train_zip.extractall('../data')\nwith ZipFile(os.path.join(base_dir, 'test.zip')) as test_zip:\n    test_zip.extractall('../data')\n\ntrain_dir = '../data/train'\ntest_dir = '../data/test'","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:11:57.695347Z","iopub.execute_input":"2022-01-28T14:11:57.696008Z","iopub.status.idle":"2022-01-28T14:12:18.099880Z","shell.execute_reply.started":"2022-01-28T14:11:57.695970Z","shell.execute_reply":"2022-01-28T14:12:18.099081Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"all_train_files = glob.glob(os.path.join(train_dir, '*.jpg'))\ntrain_list, val_list = train_test_split(all_train_files, random_state=42)","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:12:24.070737Z","iopub.execute_input":"2022-01-28T14:12:24.070997Z","iopub.status.idle":"2022-01-28T14:12:24.168883Z","shell.execute_reply.started":"2022-01-28T14:12:24.070966Z","shell.execute_reply":"2022-01-28T14:12:24.168101Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig, axes = plt.subplots(nrows=2,\n                         ncols=3,\n                         figsize=(18, 12))\nfor img_path, ax in zip(train_list, axes.ravel()):\n    ax.set_title(img_path)\n    ax.imshow(Image.open(img_path))","metadata":{"execution":{"iopub.status.busy":"2022-01-27T15:04:04.735664Z","iopub.execute_input":"2022-01-27T15:04:04.736431Z","iopub.status.idle":"2022-01-27T15:04:06.144105Z","shell.execute_reply.started":"2022-01-27T15:04:04.736389Z","shell.execute_reply":"2022-01-27T15:04:06.142705Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# plot to see how it looks\nfrom matplotlib import pyplot as plt\n%matplotlib inline\nclasses = ['cat','dog']\ndef plot_sample(data, isTrain=True):\n    img = data[0]\n    plt.figure(figsize=(15,2))\n    plt.imshow(img)\n    if isTrain:\n        plt.xlabel(classes[data[1]])\n    \n# Extract the first 3 letters from the image names, to generate one hot encoding labels\ndef label_pet_image(img):\n    pet = img.split('.')[-3]\n    if pet == 'cat': return 0\n    elif pet == 'dog': return 1\n    \n# Process the data (both train and test set)\nfrom tqdm import tqdm\nimport cv2\nfrom random import shuffle\nIMG_SIZE=64\n# set sample size\nSAMPLE_SIZE=10000\ndef process_data(DATA_FOLDER, isTrain=True):\n    # Read images from extracted directory\n    image_list = os.listdir(DATA_FOLDER)\n    filtered_image_list = image_list[0:SAMPLE_SIZE if isTrain else len(image_list)]\n    data_df = [] # data frame\n    for img in tqdm(filtered_image_list):\n        path = os.path.join(DATA_FOLDER,img)\n        if(isTrain):\n            label = label_pet_image(img)\n        else:\n            label = img.split('.')[0]\n        img = cv2.imread(path,cv2.IMREAD_COLOR)\n        try:\n            img = cv2.resize(img, (IMG_SIZE,IMG_SIZE))\n            data_df.append([np.array(img),label])\n        except Exception as e:\n            print(\"Image with issue name, path, isTrain, label\", img, path, isTrain, label)\n            print(str(e))            \n    shuffle(data_df)\n    return data_df","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:12:29.194815Z","iopub.execute_input":"2022-01-28T14:12:29.195499Z","iopub.status.idle":"2022-01-28T14:12:29.503303Z","shell.execute_reply.started":"2022-01-28T14:12:29.195453Z","shell.execute_reply":"2022-01-28T14:12:29.502569Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"train = process_data('../data/train')","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:12:37.558774Z","iopub.execute_input":"2022-01-28T14:12:37.559051Z","iopub.status.idle":"2022-01-28T14:12:57.981974Z","shell.execute_reply.started":"2022-01-28T14:12:37.559021Z","shell.execute_reply":"2022-01-28T14:12:57.981147Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plot_sample(train[0])","metadata":{"execution":{"iopub.status.busy":"2022-01-27T15:17:02.469776Z","iopub.execute_input":"2022-01-27T15:17:02.470548Z","iopub.status.idle":"2022-01-27T15:17:02.639507Z","shell.execute_reply.started":"2022-01-27T15:17:02.470498Z","shell.execute_reply":"2022-01-27T15:17:02.638789Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Prepare the train data\nX = np.array([i[0] for i in train]).reshape(-1,IMG_SIZE,IMG_SIZE,3)\ny = np.array([i[1] for i in train])\n\n# normalize our data in 0-1\nX = X/255\n\n# split into train/validation\n#from sklearn.model_selection import train_test_split\n#train_X, validation_X, train_y, validation_y = train_test_split(X, y, test_size=0.3)\n\n\n#X","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:13:08.455518Z","iopub.execute_input":"2022-01-28T14:13:08.456056Z","iopub.status.idle":"2022-01-28T14:13:08.808383Z","shell.execute_reply.started":"2022-01-28T14:13:08.456014Z","shell.execute_reply":"2022-01-28T14:13:08.807636Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# https://www.kaggle.com/cdeotte/how-to-choose-cnn-architecture-mnist\n\n# GLOBAL VARIABLES\nannealer = LearningRateScheduler(lambda x: 1e-3 * 0.95 ** x, verbose=0)\nstyles=[':','-.','--','-',':','-.','--','-',':','-.','--','-']","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:13:12.294698Z","iopub.execute_input":"2022-01-28T14:13:12.295247Z","iopub.status.idle":"2022-01-28T14:13:12.300122Z","shell.execute_reply.started":"2022-01-28T14:13:12.295209Z","shell.execute_reply":"2022-01-28T14:13:12.299345Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Experiment #1 - how many Convo2D layers?\nnets = 3\nmodel = [0] *nets\n\nfor j in range(3):\n    model[j] = Sequential()\n    model[j].add(Conv2D(24,kernel_size=5,padding='same',activation='relu',input_shape=(IMG_SIZE,IMG_SIZE,3)))\n    model[j].add(MaxPool2D())\n    if j>0:\n        model[j].add(Conv2D(48,kernel_size=5,padding='same',activation='relu'))\n        model[j].add(MaxPool2D())\n    if j>1:\n        model[j].add(Conv2D(64,kernel_size=5,padding='same',activation='relu'))\n        model[j].add(MaxPool2D(padding='same'))\n    model[j].add(Flatten())\n    model[j].add(Dense(4096, activation='relu'))\n    #https://stackoverflow.com/questions/61742556/valueerror-shapes-none-1-and-none-2-are-incompatible\n    #https://stackoverflow.com/questions/48851558/tensorflow-estimator-valueerror-logits-and-labels-must-have-the-same-shape\n    model[j].add(Dense(1, activation='sigmoid'))\n    model[j].compile(optimizer=\"adam\", loss=\"binary_crossentropy\", metrics=[\"accuracy\"])","metadata":{"execution":{"iopub.status.busy":"2022-01-27T15:17:19.474076Z","iopub.execute_input":"2022-01-27T15:17:19.47509Z","iopub.status.idle":"2022-01-27T15:17:19.603815Z","shell.execute_reply.started":"2022-01-27T15:17:19.475045Z","shell.execute_reply":"2022-01-27T15:17:19.603036Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# CREATE VALIDATION SET\nX_train2, X_val2, Y_train2, Y_val2 = train_test_split(X, y, test_size = 0.333)\nprint(X_train2.shape)\nprint(Y_train2.shape)","metadata":{"execution":{"iopub.status.busy":"2022-01-27T15:17:22.385174Z","iopub.execute_input":"2022-01-27T15:17:22.386132Z","iopub.status.idle":"2022-01-27T15:17:22.671681Z","shell.execute_reply.started":"2022-01-27T15:17:22.386079Z","shell.execute_reply":"2022-01-27T15:17:22.670896Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# TRAIN NETWORKS\nhistory = [0] * nets\nnames = [\"(C-P)x1\",\"(C-P)x2\",\"(C-P)x3\"]\nepochs = 20\nfor j in range(nets):\n    history[j] = model[j].fit(X_train2,Y_train2, batch_size=80, epochs = epochs, \n        validation_data = (X_val2,Y_val2), callbacks=[annealer], verbose=0)\n    # https://towardsdatascience.com/fixing-the-keyerror-acc-and-keyerror-val-acc-errors-in-keras-2-3-x-or-newer-b29b52609af9\n    print(\"CNN {0}: Epochs={1:d}, Train accuracy={2:.5f}, Validation accuracy={3:.5f}\".format(\n        names[j],epochs,max(history[j].history['accuracy']),max(history[j].history['val_accuracy']) ))\n    \n# 2000\n# CNN (C-P)x1: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.63964\n# CNN (C-P)x2: Epochs=20, Train accuracy=0.99625, Validation accuracy=0.65165\n# CNN (C-P)x3: Epochs=20, Train accuracy=0.91904, Validation accuracy=0.71471\n\n# 10000\n# CNN (C-P)x1: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.70961\n# CNN (C-P)x2: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.73664\n# CNN (C-P)x3: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.78769","metadata":{"execution":{"iopub.status.busy":"2022-01-27T15:17:32.602114Z","iopub.execute_input":"2022-01-27T15:17:32.60264Z","iopub.status.idle":"2022-01-27T15:19:14.648282Z","shell.execute_reply.started":"2022-01-27T15:17:32.602598Z","shell.execute_reply":"2022-01-27T15:19:14.647453Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# PLOT \ndef plot_history(history, names):\n    plt.figure(figsize=(15,5))\n    for i in range(nets):\n        plt.plot(history[i].history['val_accuracy'],linestyle=styles[i])\n    plt.title('model accuracy')\n    plt.ylabel('accuracy')\n    plt.xlabel('epoch')\n    plt.legend(names, loc='upper left')\n    axes = plt.gca()\n    axes.set_ylim([.7,1])\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:43:47.810244Z","iopub.execute_input":"2022-01-28T14:43:47.810575Z","iopub.status.idle":"2022-01-28T14:43:47.816227Z","shell.execute_reply.started":"2022-01-28T14:43:47.810542Z","shell.execute_reply":"2022-01-28T14:43:47.815438Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Summary\nFrom the above experiment, it seems that 3 pairs of convolution-subsambling is the best option.  Not sure what this means from computational complexity, but that's what I'm going with for now.","metadata":{}},{"cell_type":"markdown","source":"# 2. How many feature maps?\nIn the previous experiement, we decided that 3 pairs is the way to go. How many feature maps should we include? For example, we could do\n * 784 - [**8**C5-P2] - [**16**C5-P2] - 256 - 10\n * 784 - [**16**C5-P2] - [**32**C5-P2] - 256 - 10\n * 784 - [**24**C5-P2] - [**48**C5-P2] - 256 - 10\n * 784 - [**32**C5-P2] - [**64**C5-P2] - 256 - 10\n * 784 - [**48**C5-P2] - [**96**C5-P2] - 256 - 10  \n * 784 - [**64**C5-P2] - [**128**C5-P2] - 256 - 10  ","metadata":{}},{"cell_type":"code","source":"# BUILD CONVOLUTIONAL NEURAL NETWORKS\nnets = 6\nmodel = [0] *nets\nfor j in range(6):\n    model[j] = Sequential()\n    model[j].add(Conv2D(j*8+24,kernel_size=5,activation='relu',input_shape=(IMG_SIZE,IMG_SIZE,3)))\n    model[j].add(MaxPool2D())\n    model[j].add(Conv2D(j*16+48,kernel_size=5,activation='relu'))\n    model[j].add(MaxPool2D())\n    model[j].add(Conv2D(j*32+64,kernel_size=5,activation='relu'))\n    model[j].add(MaxPool2D())\n    model[j].add(Flatten())\n    model[j].add(Dense(4096, activation='relu'))\n    model[j].add(Dense(1, activation='sigmoid'))\n    model[j].compile(optimizer=\"adam\", loss=\"binary_crossentropy\", metrics=[\"accuracy\"])    ","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:23:21.235733Z","iopub.execute_input":"2022-01-28T14:23:21.236289Z","iopub.status.idle":"2022-01-28T14:23:21.530021Z","shell.execute_reply.started":"2022-01-28T14:23:21.236248Z","shell.execute_reply":"2022-01-28T14:23:21.529311Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# CREATE VALIDATION SET\nX_train2, X_val2, Y_train2, Y_val2 = train_test_split(X, y, test_size = 0.333)\n# TRAIN NETWORKS\nhistory = [0] * nets\nnames = [\"8 maps\",\"16 maps\",\"24 maps\",\"32 maps\",\"48 maps\",\"64 maps\"]\nepochs = 20\nfor j in range(nets):\n    history[j] = model[j].fit(X_train2,Y_train2, batch_size=80, epochs = epochs, \n        validation_data = (X_val2,Y_val2), callbacks=[annealer], verbose=0)\n    print(\"CNN {0}: Epochs={1:d}, Train accuracy={2:.5f}, Validation accuracy={3:.5f}\".format(\n        names[j],epochs,max(history[j].history['accuracy']),max(history[j].history['val_accuracy']) ))","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:23:24.859357Z","iopub.execute_input":"2022-01-28T14:23:24.860077Z","iopub.status.idle":"2022-01-28T14:25:43.956371Z","shell.execute_reply.started":"2022-01-28T14:23:24.860038Z","shell.execute_reply":"2022-01-28T14:25:43.955626Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plot_history(history, names)","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:30:34.918064Z","iopub.execute_input":"2022-01-28T14:30:34.918720Z","iopub.status.idle":"2022-01-28T14:30:35.147118Z","shell.execute_reply.started":"2022-01-28T14:30:34.918677Z","shell.execute_reply":"2022-01-28T14:30:35.146409Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"##Results##\n\n```\nCNN 8 maps: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.75766\nCNN 16 maps: Epochs=20, Train accuracy=0.71499, Validation accuracy=0.66306\nCNN 24 maps: Epochs=20, Train accuracy=0.99955, Validation accuracy=0.77628\nCNN 32 maps: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.77147\nCNN 48 maps: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.76517\nCNN 64 maps: Epochs=20, Train accuracy=0.99760, Validation accuracy=0.78709\n```\n\nThe more the better! Although there's not a huge difference over the long haul (and what the hell happened with 16 maps?).  Going to run it again... \n\n```\nCNN 8 maps: Epochs=20, Train accuracy=0.98756, Validation accuracy=0.77568\nCNN 16 maps: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.78859\nCNN 24 maps: Epochs=20, Train accuracy=0.99565, Validation accuracy=0.77628\nCNN 32 maps: Epochs=20, Train accuracy=0.98516, Validation accuracy=0.77477\nCNN 48 maps: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.78288\nCNN 64 maps: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.77327\n```\n\nThis time they all lined up - in fact 16 was the best!  Because this isn't having much impact on overall score, I'm going to go with the least expensive route, 8 maps.","metadata":{}},{"cell_type":"markdown","source":"Experiment 3:  How Large a Dense Layer?","metadata":{}},{"cell_type":"code","source":"# Build CNNs with variations of Dense Layer\nnets = 6\nmodel = [0] *nets\nfor j in range(6):\n    model[j] = Sequential()\n    model[j].add(Conv2D(j*8+24,kernel_size=5,activation='relu',input_shape=(IMG_SIZE,IMG_SIZE,3)))\n    model[j].add(MaxPool2D())\n    model[j].add(Conv2D(j*16+48,kernel_size=5,activation='relu'))\n    model[j].add(MaxPool2D())\n    model[j].add(Conv2D(j*32+64,kernel_size=5,activation='relu'))\n    model[j].add(MaxPool2D())\n    model[j].add(Flatten())\n    if j>0:\n        model[j].add(Dense(2**(j+4), activation='relu'))\n    model[j].add(Dense(1, activation='sigmoid'))\n    model[j].compile(optimizer=\"adam\", loss=\"binary_crossentropy\", metrics=[\"accuracy\"]) ","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:45:38.646132Z","iopub.execute_input":"2022-01-28T14:45:38.646685Z","iopub.status.idle":"2022-01-28T14:45:38.936141Z","shell.execute_reply.started":"2022-01-28T14:45:38.646646Z","shell.execute_reply":"2022-01-28T14:45:38.935446Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# CREATE VALIDATION SET\nX_train2, X_val2, Y_train2, Y_val2 = train_test_split(X, y, test_size = 0.333)\n# TRAIN NETWORKS\nhistory = [0] * nets\nnames = [\"0N\",\"32N\",\"64N\",\"128N\",\"256N\",\"512N\",\"1024N\",\"2048N\"]\nepochs = 50\nfor j in range(nets):\n    history[j] = model[j].fit(X_train2,Y_train2, batch_size=80, epochs = epochs, \n        validation_data = (X_val2,Y_val2), callbacks=[annealer], verbose=0)\n    print(\"CNN {0}: Epochs={1:d}, Train accuracy={2:.5f}, Validation accuracy={3:.5f}\".format(\n        names[j],epochs,max(history[j].history['accuracy']),max(history[j].history['val_accuracy']) ))\n    \n","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:45:44.725954Z","iopub.execute_input":"2022-01-28T14:45:44.726232Z","iopub.status.idle":"2022-01-28T14:51:14.697629Z","shell.execute_reply.started":"2022-01-28T14:45:44.726203Z","shell.execute_reply":"2022-01-28T14:51:14.696836Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plot_history(history, names)","metadata":{"execution":{"iopub.status.busy":"2022-01-28T14:55:01.547551Z","iopub.execute_input":"2022-01-28T14:55:01.548419Z","iopub.status.idle":"2022-01-28T14:55:01.772650Z","shell.execute_reply.started":"2022-01-28T14:55:01.548362Z","shell.execute_reply":"2022-01-28T14:55:01.772000Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"##Results##\n\nFirst run it appears 128 is the best (although they are all really close?!)\n\n```\nCNN 0N: Epochs=20, Train accuracy=0.95292, Validation accuracy=0.76486\nCNN 32N: Epochs=20, Train accuracy=0.99730, Validation accuracy=0.78979\nCNN 64N: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.79069\nCNN 128N: Epochs=20, Train accuracy=0.99985, Validation accuracy=0.79850\nCNN 256N: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.77988\nCNN 512N: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.78679\n```\n\nRunning again...\n\nThis time, 32 took the crown...but why were the results so much better?  uhhhhh.  I'll bet I didn't rebuild the models!  Going to run one more time, they should get even better.  Perhaps low epochs are good for tweaking params, then you go high epochs once you've settled on something?\n\n```\nCNN 0N: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.86306\nCNN 32N: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.89249\nCNN 64N: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.87838\nCNN 128N: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.87538\nCNN 256N: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.87838\nCNN 512N: Epochs=20, Train accuracy=1.00000, Validation accuracy=0.87988\n```\n\nRebuilt, and ran 50 epochs.  64N is the clear winner, and I'm sticking with it!\n\n```\nCNN 0N: Epochs=50, Train accuracy=1.00000, Validation accuracy=0.77628\nCNN 32N: Epochs=50, Train accuracy=1.00000, Validation accuracy=0.77778\nCNN 64N: Epochs=50, Train accuracy=1.00000, Validation accuracy=0.79159\nCNN 128N: Epochs=50, Train accuracy=1.00000, Validation accuracy=0.78318\nCNN 256N: Epochs=50, Train accuracy=1.00000, Validation accuracy=0.78198\nCNN 512N: Epochs=50, Train accuracy=1.00000, Validation accuracy=0.73814\n```","metadata":{}}]}