{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "YWjR-pgsd-Si" }, "source": [ "# CS640 Homework 3: Neural Networks\n", "\n", "In this assignment, you will\n", "\n", "1. derive both forward and backward propagation,\n", "2. implement a neural network from scratch, and\n", "3. run experiments with your model.\n", "\n", "### Collaboration\n", "You are allowed to work in a team of at most **three** on the coding part(**Q2**), but you must run the experiments and answer written questions independently.\n", "\n", "## Instructions\n", "\n", "### General Instructions\n", "In an ipython notebook, to run code in a cell or to render [Markdown](https://en.wikipedia.org/wiki/Markdown)+[LaTeX](https://en.wikipedia.org/wiki/LaTeX) press `Ctrl+Enter` or `[>|]`(like \"play\") button above. To edit any code or text cell (double) click on its content. To change cell type, choose \"Markdown\" or \"Code\" in the drop-down menu above.\n", "\n", "Most of the written questions are followed up a cell for you enter your answers. Please enter your answers in a new line below the **Answer** mark. If you do not see such cell, please insert one by yourself. Your answers and the questions should **not** be in the same cell.\n", "\n", "### Instructions on Math\n", "Some questions require you to enter math expressions. To enter your solutions, put down your derivations into the corresponding cells below using LaTeX. Show all steps when proving statements. If you are not familiar with LaTeX, you should look at some tutorials and at the examples listed below between \\$..\\$. The [OEIS website](https://oeis.org/wiki/List_of_LaTeX_mathematical_symbols) can also be helpful.\n", "\n", "Alternatively, you can scan your work from paper and insert the image(s) in a text cell.\n", "\n", "## Submission\n", "Once you are ready, save the note book as PDF file (File -> Print -> Save as PDF) and submit via Gradescope. In case some contents cannot be displayed properly in the PDF file, check out this tool: [Colab2PDF](https://github.com/drengskapur/colab2pdf)." ] }, { "cell_type": "markdown", "metadata": { "id": "O9ImiOu7jzNu" }, "source": [ "## Q0: Name(s)\n", "\n", "Please write your name in the next cell. If you are collaborating with someone, please list their names as well." ] }, { "cell_type": "markdown", "metadata": { "id": "R4ldlvqYkyKw" }, "source": [ "**Answer**" ] }, { "cell_type": "markdown", "metadata": { "id": "2NkWp9N_jDa0" }, "source": [ "## Q1: Written Problems\n", "\n", "Consider a simple neural network with three layers: an input layer, a hidden layer, and an output layer.\n", "\n", "Let $w^{(1)}$ and $w^{(2)}$ be the layers' weight matrices and let $b^{(1)}$ and $b^{(2)}$ be their biases. For convention, suppose that $w_{ij}$ is the weight between the $i$th node in the previous layer and the $j$th node in the current one.\n", "\n", "Additionally, the activation function for both layers is the sigmoid function $\\sigma(x) = \\frac{1}{1 + e^{-x}}$. Let $z^{(1)}$ and $z^{(2)}$ be the outputs of the two layers before activation, and let $a^{(1)} = \\sigma(z^{(1)})$ and $a^{(2)} = \\sigma(z^{(2)})$.\n", "\n", "Lastly, we choose the L2 loss $L(y_{\\text{true}}, y_{\\text{predict}}) = \\frac{1}{2}(y_{\\text{true}} - y_{\\text{predict}})^{2}$ as the loss function.\n" ] }, { "cell_type": "markdown", "metadata": { "id": "swVLLpgYmtxv" }, "source": [ "### Q1.1: Forward Pass\n", "Suppose that\n", "\n", "$w^{(1)} = \\begin{bmatrix}0.4 & 0.6 & 0.2 \\\\ 0.3 & 0.9 & 0.5\\end{bmatrix}$,\n", " $b^{(1)} = [1, 1, 1]$; and\n", "\n", "$w^{(2)} = \\begin{bmatrix}0.2 \\\\ 0.2 \\\\ 0.8\\end{bmatrix}$, $b^{(2)} = [0.5]$.\n", "\n", "If the input is $a^{(0)} = \\begin{bmatrix}1 \\\\ 1\\end{bmatrix}$, what is the network output? Show your calculation steps and round your **final** answer to 2 digits after decimal.\n", "\n", "**Note**: You should NOT round any intermediate results." ] }, { "cell_type": "markdown", "metadata": { "id": "MmZ7isA1s9rT" }, "source": [ "**[Answer]**" ] }, { "cell_type": "markdown", "metadata": { "id": "Rw_yNYW4t2Z0" }, "source": [ "### Q1.2: Backward Propagation\n", "\n", "Derive the expressions of the following gradients:\n", "1. $\\frac{\\partial L}{\\partial w^{(2)}}$ and $\\frac{\\partial L}{\\partial b^{(2)}}$\n", "2. $\\frac{\\partial L}{\\partial w^{(1)}}$ and $\\frac{\\partial L}{\\partial b^{(1)}}$\n", "\n", "For each gradient, start by deriving the element-level expression using chain rule, and then construct the final answer in matrix form. You can use a self-defined variable(e.g., $\\eta$) to shorten a long expression (especially for the first-layer gradients).\n", "\n", "An example of your answer should look like the following.\n", "\n", "\n", "---\n", "\n", "\n", "(Element level)\n", "\\begin{align}\n", " \\frac{\\partial L}{\\partial w^{(2)}_{i}} &= \\frac{\\partial L}{\\partial a^{(2)}} \\cdot \\frac{\\partial a^{(2)}}{\\partial z^{(2)}} \\cdot \\frac{\\partial z^{(2)}}{\\partial w^{(2)}_{i}} \\text{ (chain rule)}\\\\\n", " &= L'(y_{\\text{true}}, a^{(2)}) \\cdot f'_{2}(z^{(2)}) \\cdot a^{(1)}_{i} \\text{ (substitution)}\\\\\n", " &= ... \\text{ (further substitution and/or simplification if needed)}\n", "\\end{align}\n", "\n", "(Matrix form)\n", "\n", "$$\\frac{\\partial L}{\\partial w^{(2)}} = ... \\text{ (only the final answer is needed)}$$\n", "\n", "\n", "---\n", "\n", "\n", "\n", "**Note**: The derivative of $\\sigma(x)$ is $\\sigma(x)(1 - \\sigma(x))$ if $x$ is a scalar, or $\\sigma(x)\\odot(1 - \\sigma(x))$ if $x$ is a vector." ] }, { "cell_type": "markdown", "metadata": { "id": "y9AkrYYVLnRl" }, "source": [ "**[Answer]**" ] }, { "cell_type": "markdown", "metadata": { "id": "H6AxRuFOlU_B" }, "source": [ "## Q2: Implementation\n", "\n", "In this part, you need to construct a neural network model and run a test experiment. We provide a skeleton script of for the model and full script for the test experiment." ] }, { "cell_type": "markdown", "metadata": { "id": "ce5gxnBCm7HW" }, "source": [ "### Q2.0: Import Packages\n", "\n", "The packages imported in the following block should be sufficient for this problem, but you are free to add more if necessary. However, keep in mind that you **should not** import and use any neural network package. If you have concern about an addition package, please contact us via Piazza." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "rLhHI0rOdQJ0" }, "outputs": [], "source": [ "import numpy as np\n", "import os, sys" ] }, { "cell_type": "markdown", "metadata": { "id": "N9TiTFf9psFl" }, "source": [ "### Q2.1: Define Activation and Loss Functions\n", "\n", "Complete the following functions except for `d_softmax`. The ones starting with a \"d\" are the derivatives of the corresponding functions.\n", "\n", "Definitions:\n", "1. sigmoid: $\\sigma(x) = \\frac{1}{1 + e^{-x}}$\n", "2. softmax: softmax(x) $= \\frac{e^{x_{i}}}{\\sum_{i} e^{x_{i}}}$\n", "3. L2 loss: $L(y_{\\text{true}}, y_{\\text{predict}}) = \\frac{1}{2}(y_{\\text{true}} - y_{\\text{predict}})^{2}$\n", "4. cross entropy loss: $L(y_{\\text{true}}, y_{\\text{predict}}) = -\\sum_{i}y_{\\text{true}}[i]\\cdot\\log y_{\\text{predict}}[i]$\n", "\n", "**Note**: Although you are free to decide the object types of the input parameters, it is best to assume (and follow this assumption in later sections) that they are either scalars (in rare cases) or Numpy arrays.\n", "\n", "**Clarification**: The softmax function and the cross entropy loss are almost always used in tandem due to the nice chained derivative they produce. However, their individual derivatives are hard to compute. Therefore, we use a function `d_cross_entropy_softmax` as a loss derivative to house the chained derivative, and a trivial function `d_softmax` as a filler for consistency in implementation. This should be clearer in the network implementation section." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "yuxWGvbhp5jD" }, "outputs": [], "source": [ "def sigmoid(x):\n", " pass\n", "\n", "def d_sigmoid(x):\n", " pass\n", "\n", "def l2_loss(YTrue, YPredict):\n", " pass\n", "\n", "def d_l2_loss(YTrue, YPredict):\n", " pass\n", "\n", "def softmax(x):\n", " pass\n", "\n", "def d_softmax(x):\n", " return 1.0\n", "\n", "def cross_entropy_loss(YTrue, YPredict):\n", " pass\n", "\n", "def d_cross_entropy_softmax(YTrue, YPredict):\n", " pass" ] }, { "cell_type": "markdown", "metadata": { "id": "zvhPU4YqoVZF" }, "source": [ "### Q2.2: Define the Layer Class\n", "\n", "The following block defines the Layer class. There is nothing you need to do but run the cell." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "6r5h0hT8ofqQ" }, "outputs": [], "source": [ "class Layer:\n", " def __init__(self, n_input, n_output, add_bias = True):\n", " self.n_input = n_input\n", " self.n_output = n_output\n", " self.add_bias = add_bias\n", " self.initialize_weights()\n", "\n", " def initialize_weights(self):\n", " \"\"\"\n", " Initializes the weights and biases with small random values.\n", " \"\"\"\n", " rng = np.random.default_rng(2) # for reproducibility\n", " self.weights = rng.normal(loc = 0, scale = 1, size = (self.n_input, self.n_output))\n", " if self.add_bias:\n", " self.bias = rng.normal(loc = 0, scale = 1, size = (1, self.n_output))\n" ] }, { "cell_type": "markdown", "metadata": { "id": "fXcQsl_XpAOW" }, "source": [ "### Q2.3: Define the Network Class\n", "\n", "Complete the `fit` and `predict` functions as instructed in the comments. Do not change their input arguments, but you are free to add functions as necessary. The `__init__` function should be left as it is.\n", "\n", "*Hint \\#1*: This is the heaviest part of this assignment. We recommend you to first go over the math carefully before starting this part.\n", "\n", "*Hint \\#2*: You are strongly encouraged to use numpy for matrix operations. When doing multiplication, please be extra careful about the dimensions." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "-p1xdHw0pKr8" }, "outputs": [], "source": [ "class Network:\n", " def __init__(self, layers, activation_list, d_activation_list, loss_function, d_loss_function):\n", " self.layers = layers\n", " self.activation_list = activation_list\n", " self.d_activation_list = d_activation_list\n", " self.loss_function = loss_function\n", " self.d_loss_function = d_loss_function\n", "\n", " def fit(self, X, Y, epochs, learning_rate, reg_lambda):\n", " \"\"\"\n", " This is the training function. It should return the average loss over samples.\n", " \"\"\"\n", " loss = np.zeros(epochs) # stores loss for each epoch\n", " n_samples = len(X)\n", "\n", " for epoch in range(epochs):\n", " current_loss = 0.0 # this should be accumulated over the samples\n", " ######################## start of your code ########################\n", " # first, initialize zero gradients\n", "\n", "\n", " # next, for each sample, do\n", " # 1. compute outputs from each layer (via forward propagation);\n", " # 2. compute and accumulate the current loss over the samples (using self.loss_function); and\n", " # 3. compute and accumulate the gradients (via backward propagation)\n", "\n", "\n", " # then, update weights and biases using the corresponding mean gradients\n", " # (i.e., accumulated gradient divided by n_samples)\n", "\n", "\n", " ######################## end of your code ##########################\n", " loss[epoch] = current_loss / n_samples\n", " # lastly, return the average loss\n", " return loss\n", "\n", " def predict(self, X, threshold = None):\n", " \"\"\"\n", " This function predicts the labels for samples in X. The parameter threshold\n", " is used when the labels are binary and there is only one node in the final\n", " layer of the network.\n", " \"\"\"\n", " YPredict = np.zeros(len(X))\n", "\n", " ######################### start of your code ###########################\n", " # for each sample, run a forward pass and append the predicted label to YPredict\n", "\n", " ######################### end of your code #############################\n", "\n", " return YPredict\n" ] }, { "cell_type": "markdown", "metadata": { "id": "3NS8f9qz0fSK" }, "source": [ "### Q2.4: Test Model\n", "\n", "Use the following example code to test your model with some simple data.\n", "\n", "**Make sure to produce a decreasing loss curve here before moving on. A failed model implementation will lead to zero point for all remaining problems.**" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "gTyLlzEa0a0U" }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "from matplotlib.ticker import MaxNLocator\n", "from sklearn import datasets\n", "\n", "X, Y = datasets.load_iris(return_X_y = True)\n", "X, Y = X[:100, :2], Y[:100]\n", "rng = np.random.default_rng(2)\n", "indices = [i for i in range(100)]\n", "rng.shuffle(indices)\n", "X, Y = X[indices], Y[indices]\n", "\n", "\n", "# assemble your model\n", "model = Network([Layer(2, 4), Layer(4, 1)],\n", " [sigmoid, sigmoid],\n", " [d_sigmoid, d_sigmoid],\n", " l2_loss, d_l2_loss)\n", "\n", "# specify training parameters\n", "epochs = 100\n", "learning_rate = 1e-2\n", "reg_lambda = 0\n", "\n", "loss = model.fit(X, Y, epochs, learning_rate, reg_lambda)\n", "\n", "# plot the losses, the curve should be decreasing\n", "fig, ax = plt.subplots(figsize = (6, 6))\n", "ax.plot([i + 1 for i in range(epochs)], loss)\n", "ax.set_title(\"Training Loss\")\n", "ax.set_xlabel(\"Epoch\")\n", "ax.xaxis.set_major_locator(MaxNLocator(integer=True))\n", "ax.grid(True)\n", "plt.show()\n" ] }, { "cell_type": "markdown", "metadata": { "id": "jGasEudB__Dh" }, "source": [ "## Q3: Real Data Experiments\n", "\n", "In this part, you need to try out different model parameter values and observe how they affect the results.\n", "\n", "For each of the questions below, implement experiments and insert performance scores to the designated dictionary. The performance scores can be computed using the imported functions (for F1 score, you need to specify `average = \"macro\"` when calling the function). You can refer to Q2.4 as an example of implementing experiments.\n", "\n", "**Note**: Remember to initialize a new instance of your model for each different choice of hyper-parameter." ] }, { "cell_type": "markdown", "source": [ "### Q3.0: Loading Data\n", "\n", "Modify the \"data_dir\" variable in the following block and run the cell to load the data. Since the provided dataset contains more than two labels, both \"YTrain\" and \"YTest\" have been converted to one-hot forms.\n", "\n", "**Note**: Be careful about the shapes of the data variables. Specifically, the one-hot encoded labels are **row vectors**." ], "metadata": { "id": "zA6nhLVxYJdO" } }, { "cell_type": "code", "source": [ "import pandas\n", "from sklearn.preprocessing import OneHotEncoder\n", "from sklearn.metrics import accuracy_score, f1_score\n", "import matplotlib.pyplot as plt\n", "\n", "data_dir = \"\" # input the path to \"dataset\" directory\n", "df_X_train = pandas.read_csv(os.path.join(data_dir, \"Digit_X_train.csv\"), header = None)\n", "df_X_test = pandas.read_csv(os.path.join(data_dir, \"Digit_X_test.csv\"), header = None)\n", "df_y_train = pandas.read_csv(os.path.join(data_dir, \"Digit_y_train.csv\"), header = None)\n", "df_y_test = pandas.read_csv(os.path.join(data_dir, \"Digit_y_test.csv\"), header = None)\n", "XTrain, XTest = df_X_train.values, df_X_test.values\n", "YTrain, YTest = df_y_train.values, df_y_test.values\n", "print(\"All labels: \" + str(np.unique(YTrain)))\n", "\n", "# encode multi-class labels\n", "encoder = OneHotEncoder(sparse_output = False)\n", "YTrain_encoded = encoder.fit_transform(YTrain)\n", "YTest_encoded = encoder.transform(YTest)\n", "\n", "print(\"XTrain.shape = \" + str(XTrain.shape))\n", "print(\"XTest.shape = \" + str(XTest.shape))\n", "print(\"YTrain.shape = \" + str(YTrain.shape))\n", "print(\"YTrain_encoded.shape = \" + str(YTrain_encoded.shape))\n", "print(\"YTest.shape \" + str(YTest.shape))\n", "print(\"YTest_encoded.shape \" + str(YTest_encoded.shape))" ], "metadata": { "id": "H7Pf9GBtMMYK" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "UyItn83F-tBV" }, "source": [ "### Q3.1: Epochs\n", "\n", "Experiment with **five** different choices of total epochs." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "X6cKLcwf_K7D" }, "outputs": [], "source": [ "scores_train = {\"Accuracy\" : [], \"F1 (Macro)\" : []}\n", "scores_test = {\"Accuracy\" : [], \"F1 (Macro)\" : []}\n", "\n", "############################## start of your code ##############################\n", "epochs = [] # fill the list with your five epoch choices in increasing order\n", "\n", "# implement experiments and fill the lists in \"scores_train\" and\n", "# \"scores_test\" (one entry per epoch value)\n", "\n", "############################### end of your code ###############################\n", "\n", "fig, axes = plt.subplots(1, 2, figsize = (10, 4))\n", "for i, key in enumerate([\"Accuracy\", \"F1 (Macro)\"]):\n", " axes[i].plot(epochs, scores_train[key], \"-o\", label = \"train\")\n", " axes[i].plot(epochs, scores_test[key], \"-o\", label = \"test\")\n", " axes[i].set_title(key)\n", " axes[i].set_ylim([0, 1])\n", " axes[i].legend()\n", " axes[i].grid(True)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "id": "JwDcgfbpDHF4" }, "source": [ "### Q3.2: Learning Rate\n", "\n", "Experiment with **five** different choices of learning rates." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "LEMZuxy9DRAg" }, "outputs": [], "source": [ "scores_train = {\"Accuracy\" : [], \"F1 (Macro)\" : []}\n", "scores_test = {\"Accuracy\" : [], \"F1 (Macro)\" : []}\n", "\n", "############################## start of your code ##############################\n", "LRs = [] # fill the list with your five LR choices in increasing order\n", "\n", "# implement experiments and fill the lists in \"scores_train\" and\n", "# \"scores_test\" (one entry per LR value)\n", "\n", "############################### end of your code ###############################\n", "\n", "fig, axes = plt.subplots(1, 2, figsize = (10, 4))\n", "for i, key in enumerate([\"Accuracy\", \"F1 (Macro)\"]):\n", " axes[i].plot(LRs, scores_train[key], \"-o\", label = \"train\")\n", " axes[i].plot(LRs, scores_test[key], \"-o\", label = \"test\")\n", " axes[i].set_title(key)\n", " axes[i].set_ylim([0, 1])\n", " axes[i].legend()\n", " axes[i].grid(True)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "id": "96i2ZAsBDTaZ" }, "source": [ "### Q3.3: Regularization Parameter\n", "\n", "Experiment with **five** different choices of regularization parameter." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ANhxVaLcDfjn" }, "outputs": [], "source": [ "scores_train = {\"Accuracy\" : [], \"F1 (Macro)\" : []}\n", "scores_test = {\"Accuracy\" : [], \"F1 (Macro)\" : []}\n", "\n", "############################## start of your code ##############################\n", "lambdas = [] # fill the list with your five regularization lambda choices in increasing order\n", "\n", "# implement experiments and fill the lists in \"scores_train\" and\n", "# \"scores_test\" (one entry per reg_lambda value)\n", "\n", "############################### end of your code ###############################\n", "\n", "fig, axes = plt.subplots(1, 2, figsize = (10, 4))\n", "for i, key in enumerate([\"Accuracy\", \"F1 (Macro)\"]):\n", " axes[i].plot(lambdas, scores_train[key], \"-o\", label = \"train\")\n", " axes[i].plot(lambdas, scores_test[key], \"-o\", label = \"test\")\n", " axes[i].set_title(key)\n", " axes[i].set_ylim([0, 1])\n", " axes[i].legend()\n", " axes[i].grid(True)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "id": "N_LjwNPkDgyq" }, "source": [ "### Q3.4: Network Structure\n", "\n", "Experiment with **five** different choices of network structure. This includes number of layers and number of nodes in each layer.\n", "\n", "*Hint*: Try experimenting with increasing complexity. You may need to hard-code the experiments without using a for loop.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "zae3B1jqFhHd" }, "outputs": [], "source": [ "levels = [1, 2, 3, 4, 5]\n", "scores_train = {\"Accuracy\" : [], \"F1 (Macro)\" : []}\n", "scores_test = {\"Accuracy\" : [], \"F1 (Macro)\" : []}\n", "\n", "############################## start of your code ##############################\n", "# implement experiments and fill the lists in \"scores_train\" and\n", "# \"scores_test\" (one entry per complexity level)\n", "\n", "############################### end of your code ###############################\n", "\n", "fig, axes = plt.subplots(1, 2, figsize = (10, 4))\n", "for i, key in enumerate([\"Accuracy\", \"F1 (Macro)\"]):\n", " axes[i].plot(levels, scores_train[key], \"-o\", label = \"train\")\n", " axes[i].plot(levels, scores_test[key], \"-o\", label = \"test\")\n", " axes[i].set_title(key)\n", " axes[i].set_ylim([0, 1])\n", " axes[i].legend()\n", " axes[i].grid(True)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "id": "pmRKZaJPFpm1" }, "source": [ "## Q4: Follow-up Questions\n", "\n", "For each question below, provide a short answer. You can cite your code if needed." ] }, { "cell_type": "markdown", "metadata": { "id": "MPFklnbuAwg_" }, "source": [ "### Q4.1: Briefly describe the workflow of how your model classifies the data." ] }, { "cell_type": "markdown", "metadata": { "id": "q8RYC8yiBEvU" }, "source": [ "**[Answer]**" ] }, { "cell_type": "markdown", "metadata": { "id": "ZJ6k2vbJBFcU" }, "source": [ "### Q4.2: In your own words, explain how the forward propagation in your model works." ] }, { "cell_type": "markdown", "metadata": { "id": "eZSqrQX-BofT" }, "source": [ "**[Answer]**" ] }, { "cell_type": "markdown", "metadata": { "id": "xDkm7LCOBpOZ" }, "source": [ "### Q4.3: In your own words, explain how the backward propagation in your model works." ] }, { "cell_type": "markdown", "metadata": { "id": "WDgKWePgHHpB" }, "source": [ "**[Answer]**" ] }, { "cell_type": "markdown", "metadata": { "id": "u4v_t-JdB726" }, "source": [ "### Q4.4: In theory, how do the total number of epochs, the learning rate, and the regularization parameter impact the performance of model? Does any of the theoretical impact actually happen in your result? If so, point them out." ] }, { "cell_type": "markdown", "metadata": { "id": "1zFnMPEJG53C" }, "source": [ "**[Answer]**\n", "\n", "**Epochs**\n", "\n", "\n", "**Learning Rate**\n", "\n", "\n", "**Regularization Parameter**\n" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 0 }