Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tutorial: NumPy deep learning on MNIST from scratch #33

Open
wants to merge 10 commits into
base: master
from

Conversation

@8bitmp3
Copy link

@8bitmp3 8bitmp3 commented Oct 11, 2020

Hi @melissawm @mattip @bjnath @rgommers 🙌 GSoD

This tutorial demonstrates how to build and train a simple 3-layer feed-forward neural network with backpropagation learning from scratch.

All feedback is welcome!

Thank you everyone 🙏
cc @iamtrask


Prerequisites

The reader should have some knowledge of Python, NumPy array manipulation, and linear algebra. In addition, you should be familiar with main concepts of deep learning.

To refresh the memory, you can take the Python and Linear algebra on n-dimensional arrays tutorials.

You are advised to read the Deep learning paper published in 2015 by Yann LeCun, Yoshua Bengio, and Geoffrey Hinton, who are regarded as some of the pioneers of the field.

In addition to NumPy, you will be utilizing the following Python standard modules for data loading and processing:

  • urllib for URL handling.

  • request for URL opening.

  • gzip for gzip file decompression.

  • pickle to work with the pickle file format.

  • sys for output printing.

    as well as:

  • matplotlib for data visualization.

This tutorial can be run locally in an isolated environment, such as Virtualenv. You can use Jupyter Notebook or JupyterLab to run each notebook cell. Don't forget to set up NumPy and matplotlib.

Table of contents

  1. Load the MNIST dataset

  2. Preprocess the dataset

  3. Build and train a small neural network from scratch

  4. Next steps


@review-notebook-app
Copy link

@review-notebook-app review-notebook-app bot commented Oct 11, 2020

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

8bitmp3 added 4 commits Oct 12, 2020
…atting
@bjnath
Copy link
Contributor

@bjnath bjnath commented Oct 13, 2020

This surely was many hours of effort -- thank you.

A few clarifications in the opening lines might help readers:


This tutorial demonstrates how to build and train a simple 3-layer feed-forward neural network with backpropagation

  1. Why pick this type of NN?
  2. +1 for linking to Wikipedia on feedforward and backpropagation.

how to parse and load the dataset without the aid of external deep learning libraries

Isn't it the entire NN that's written without external libraries, not just parsing and loading?


without the aid of external deep learning libraries

Why would I, the reader, want to do that?


This type of machine or deep learning is often referred to as supervised learning.

This sentence parses ambiguously and the reference of "this" is unclear. Maybe not worth fixing -- leave it out.


This tutorial was adapted from the work by Andrew Trask

Trask might appreciate a link to the original work, especially if it's his new book, rather than simply to his home page.


inspired by the teachings of Joel Grus

  1. It's not clear in the sentence who was inspired -- you or Trask.
  2. If this were "inspired by the teachings of Gandhi," you could end it there, but not everyone knows Grus. Can you explain briefly?
  3. "approach" might be a better word here than "teachings."

The deep learning model that you will construct with NumPy will learn from the MNIST dataset, which contains 60,000 training and 10,000 test images and corresponding labels of handwritten digits from 0 to 9.

  1. The emphasis is misplaced. The goal is to build an image classifier -- a program that will teach itself, from a set of examples, to recognize handwritten digits 0-9.

  2. The MNIST description won't be clear to an unacquainted reader. It's a collection of handwritten digits 0-9, with separate cheat sheets ("labels") that identify the digits correctly. Then you'd explain that there are separate sets for training the recognizer and for testing it once it's trained. And perhaps only then you'd mention there are 60,000 and 10,000 files.

    Alternatively, assume people who don't recognize MNIST will read the link, and just call MNIST "the standard MNIST dataset."

  3. The Wikipedia MNIST reference might be more helpful here than LeCun's page.


melissawm added a commit to melissawm/numpy-tutorials that referenced this pull request Oct 14, 2020
@melissawm
Copy link
Member

@melissawm melissawm commented Oct 14, 2020

Hi @8bitmp3 , thank you for this tutorial, I really think this will be a nice addition and it is quite an effort to implement this without any pre-packaged library. From the text it is clear that you are in your element, too!

I have a few comments, mainly in the interest of making the document a bit simpler. I think some of the functions can be simplified or rewritten, and this way we can use less code-comments and more text. I believe this can make the whole document more fluid and easier to follow. Feel free to adapt or try different things if you think it doesn't fit your own narrative.

I'm focusing first on the start and some of the functions there, but I'll continue trying to bring ideas for the rest. In particular, I think "Neural network building blocks with NumPy" and "Model architecture summary" run a bit long - I think your explanations are spot on, but I'm worried that we'll lose track of the code at this point.

From an educational point of view, I usually create variables or import libraries close to where they will actually be used, instead of in the beginning of the file. So you'll see that I did that for the first sections.

Then, since we are using a notebook, we might as well make use of literate programming ideas; namely, instead of using code comments, using actual text as much as possible. This made me rethink some of your functions and realize that, for the purpose of this document, they are really not necessary since those operations will only be performed once. So I tried rewriting the initial download/save/retrieve data without using functions at all. Let me know what you think and if you believe strongly those should be functions we can work on that. Also, some of the text might need re-working now, I just took some of the code comments and adapted but you may have better ideas for that :) Keep in mind that my suggestions are not meant to be the best solution - just another way of doing things.

Here's my take on the document (not that I only touched the first section, and removed the need for sys later in the file).

https://github.com/melissawm/numpy-tutorials/blob/melissa-deep-learning/content/tutorial-deep-learning-on-mnist.ipynb

Overall, I'm pretty happy that we'll see this kind of content on the repo - so thanks again and let me know if there's anything I can help you with!

@8bitmp3
Copy link
Author

@8bitmp3 8bitmp3 commented Oct 26, 2020

Thanks @bjnath for the awesome feedback.

  1. Why pick this type of NN?
    ...
    The goal is to build an image classifier -- a program that will teach itself, from a set of examples, to recognize handwritten digits 0-9.
    ...
  2. The Wikipedia MNIST reference might be more helpful here than LeCun's page.

Changed the words around and refactored for clarity:

This tutorial demonstrates how to build and train a simple 3-layer [feedforward neural network](https://en.wikipedia.org/wiki/Feedforward_neural_network) with [backpropagation](https://en.wikipedia.org/wiki/Backpropagation) from scratch with NumPy to recognize hardwritten digit images.

Your deep learning model — one of the most basic artificial neural networks that resembles the original [multi-layer perceptron](https://en.wikipedia.org/wiki/Multilayer_perceptron) — will learn to classify digits from 0 to 9 from the [MNIST](https://en.wikipedia.org/wiki/MNIST_database) dataset. 

Isn't it the entire NN that's written without external libraries, not just parsing and loading?
...

without the aid of external deep learning libraries

Why would I, the reader, want to do that?

Agreed. Although it's not uncommon to use a deep learning library to add a popular dataset in a couple of lines of code, I've removed this anyway to avoid repetition.

Trask might appreciate a link to the original work, especially if it's his new book, rather than simply to his home page.

Definitely. I also added a link to the repo and a reference to Trask's book - it's a must read.

I'm basing my third project on Grus' work, with his permission (a small automatic differentiation library with NumPy), so will mention him there instead of this project.

@8bitmp3
Copy link
Author

@8bitmp3 8bitmp3 commented Oct 26, 2020

I have a few comments, mainly in the interest of making the document a bit simpler. I think some of the functions can be simplified or rewritten

Definitely, thanks so much for the help @melissawm 🙌! I'll also soon simplify one_hot_encoding() as per this solution based on feedback I got.

I'm focusing first on the start and some of the functions there, but I'll continue trying to bring ideas for the rest. In particular, I think "Neural network building blocks with NumPy" and "Model architecture summary" run a bit long - I think your explanations are spot on, but I'm worried that we'll lose track of the code at this point.

Good point! I want to merge some of the stuff with the "compose section" (with the code). Simpler is better. Thanks so much!

From an educational point of view, I usually create variables or import libraries close to where they will actually be used, instead of in the beginning of the file. So you'll see that I did that for the first sections.

Yup, got it.

Then, since we are using a notebook, we might as well make use of literate programming ideas; namely, instead of using code comments, using actual text as much as possible.

👍 I agree!

So I tried rewriting the initial download/save/retrieve data without using functions at all. Let me know what you think and if you believe strongly those should be functions we can work on that.

I think this is awesome. I mostly try to use the second person, but other than that 🙏.

Thank you 💯 !

8bitmp3 added 3 commits Oct 26, 2020
@melissawm
Copy link
Member

@melissawm melissawm commented Oct 26, 2020

Hi @8bitmp3 ! This is looking really good - I have a few comments but overall I think you did an awesome job. This is a tough subject and I do have some background in machine learning though - but I suspect this will also be the case for readers of this tutorial, so I think your links and references are right on target.

I don't know if there's more simplification possible in the code - the main training loop is just as it should be. We could take some of the code-comments out but I'd like to see some other opinions on that. The only thing you might consider is building a function for the test/training loss+accurate prediction calculation. But I'm not sure that would be helpful for readers... For now my main comments will be on some typos and a few suggestions but nothing major. Thanks again for this!

@@ -0,0 +1,1017 @@
{

This comment has been minimized.

@melissawm

melissawm Oct 26, 2020
Member

I would add to that a suggestion about conda, since it's become the standard for most scientific computing applications in Python.


Reply via ReviewNB

This comment has been minimized.

@8bitmp3

8bitmp3 Oct 28, 2020
Author

Done.

This tutorial can be run locally in an isolated environment, such as
[Virtualenv](https://virtualenv.pypa.io/en/stable/)
+ and [conda](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html)
...

This comment has been minimized.

@melissawm

melissawm Nov 4, 2020
Member

nit: I'd say virtualenv or conda, but that's just the mathematician in me talking :)

@@ -0,0 +1,1017 @@
{

This comment has been minimized.

@melissawm

melissawm Oct 26, 2020
Member

  • I think you meant matrix multiplication function np.dot (instead of *module*)
  • In the activation function description I think you meant "inputs and outputs" instead of "inputs and inputs" :)
  • "In this example, you will use a method called dropout (dilution that " (I think there is a formatting error here with the parenthesis?)

Reply via ReviewNB

This comment has been minimized.

@8bitmp3

8bitmp3 Oct 28, 2020
Author

  • Good catch :) Also changed "np.random.random() function" and "np.random.randint() function".
  • Nice one. Changed to "inputs and outputs"
  • There's a weird rendering issue in some notebook viewers - maybe because the URL ends with a bracket [...](https://en.wikipedia.org/wiki/Dilution_(neural_networks)). Changed it to:
- ... dropout ([dilution](https://en.wikipedia.org/wiki/Dilution_(neural_networks))) ...
+ dropout — [dilution](https://en.wikipedia.org/wiki/Dilution_(neural_networks)) — ...

This comment has been minimized.

@melissawm

melissawm Nov 4, 2020
Member

The regularization link seems to be missing a parenthesis at the end. Thanks for the other fixes :)

@@ -0,0 +1,1017 @@
{

This comment has been minimized.

@melissawm

melissawm Oct 26, 2020
Member

  • Why do you test every 10 epochs?
  • Is it necessary to use [i, i+1] for each item, or could [i] do the same job?

Reply via ReviewNB

This comment has been minimized.

@8bitmp3

8bitmp3 Oct 29, 2020
Author

  • I think in the original gist, you test (evaluate) the model every 10 epochs because it should have more "knowledge" to be tested after the 10th training iteration. I've tried testing at every epoch and noticed the results were OK in this simple example. It's reflected in the new code.
  • [i:i+1] is necessary here (doesn't seem to work without it)

This comment has been minimized.

@melissawm

melissawm Nov 4, 2020
Member

  • Why do you print "Epoch" + str(j+10)? I think there is no need to add 10 to this result - am I missing something?
  • Ok I got the reason for the indexing but this code could be simplified as follows:
# To store training and test set losses and accurate predictions
# for visualization.
store_training_loss = []
store_training_accurate_pred = []
store_test_loss = []
store_test_accurate_pred = []

# This is a training loop.
# Run the learning experiment for a defined number of epochs (iterations).
for j in range(epochs):
    # Set the initial loss/error and the number of accurate predictions to zero.
    training_loss = 0.0
    training_accurate_predictions = 0
    
    # For all images in the training set, perform a forward pass
    # and backpropagation and adjust the weights accordingly.
    for i in range(len(training_images)):
        # Forward propagation/forward pass:
        # 1. The input layer:
        #    Initialize the training image data as inputs.
        layer_0 = training_images[i]
        # 2. The hidden layer:
        #    Take in the training image data into the middle layer by 
        #    matrix-multiplying it by randomly initialized weights. 
        layer_1 = np.dot(layer_0, weights_1)
        # 3. Pass the hidden layer's output through the ReLU activation function.
        layer_1 = relu(layer_1)
        # 4. Define the dropout function for regularization.
        dropout_mask = np.random.randint(0, high=2, size=layer_1.shape)
        # 5. Apply dropout to the hidden layer's output.
        layer_1 *= dropout_mask * 2
        # 6. The output layer:
        #    Ingest the output of the middle layer into the the final layer
        #    by matrix-multiplying it by randomly initialized weights.
        #    Produce a 10-dimension vector with 10 scores.
        layer_2 = np.dot(layer_1, weights_2)

        # Backpropagation/backward pass:
        # 1. Measure the training error (loss function) between the actual
        #    image labels (the truth) and the prediction by the model.
        training_loss += np.sum((training_labels[i] - layer_2) ** 2)
        # 2. Increment the accurate prediction count.
        training_accurate_predictions += int(np.argmax(layer_2) == np.argmax(training_labels[i]))
        # 3. Differentiate the loss function/error.
        layer_2_delta = (training_labels[i] - layer_2)
        # 4. Propagate the gradients of the loss function back through the hidden layer.
        layer_1_delta = np.dot(weights_2, layer_2_delta) * relu2deriv(layer_1)
        # 5. Apply the dropout to the gradients.
        layer_1_delta *= dropout_mask
        # 6. Update the weights for the middle and input layers
        #    by multiplying them by the learning rate and the gradients.
        weights_1 += learning_rate * np.outer(layer_0, layer_1_delta)
        weights_2 += learning_rate * np.outer(layer_1, layer_2_delta)
    
    # Store training set losses and accurate predictions.
    store_training_loss.append(training_loss)
    store_training_accurate_pred.append(training_accurate_predictions)

    # Evaluate on the test set:
    # 1. Set the initial error and the number of accurate predictions to zero.
    test_loss = 0.0
    test_accurate_predictions = 0
    
    # 2. Start testing the model by evaluating on the test image dataset.
    for i in range(len(test_images)):
        # 1. Pass the test images through the input layer.
        layer_0 = test_images[i]
        # 2. Compute the weighted sum of the test image inputs in and
        #    pass the hidden layer's output through ReLU.
        layer_1 = relu(np.dot(layer_0, weights_1))
        # 3. Compute the weighted sum of the hidden layer's inputs.
        #    Produce a 10-dimensional vector with 10 scores.
        layer_2 = np.dot(layer_1, weights_2)

        # 4. Measure the error between the actual label (truth) and prediction values.
        test_loss += np.sum((test_labels[i] - layer_2) ** 2)
        # 5. Increment the accurate prediction count.
        test_accurate_predictions += int(np.argmax(layer_2) == np.argmax(test_labels[i]))

    # Store test set losses and accurate predictions.
    store_test_loss.append(test_loss)
    store_test_accurate_pred.append(test_accurate_predictions)

    # 3. Display the error and accuracy metrics in the output.
    print("\n" + \
          "Epoch: " + str(j) + \
          " Training set error:" + str(training_loss/ float(len(training_images)))[0:5] +\
          " Training set accuracy:" + str(training_accurate_predictions/ float(len(training_images))) +\
          " Test set error:" + str(test_loss/ float(len(test_images)))[0:5] +\
          " Test set accuracy:" + str(test_accurate_predictions/ float(len(test_images))))

Let me know if this makes sense to you. There was nothing wrong with the original code - I just think it is easier to avoid transposing if you can, and using 1d arrays instead of 2d.

@@ -0,0 +1,1017 @@
{

This comment has been minimized.

@melissawm

melissawm Oct 26, 2020
Member

It would be really nice to close with a plot showing that your NN can identify an image that was not in the training set (I know the prints also show this, but just for a visual element).


Reply via ReviewNB

This comment has been minimized.

@8bitmp3

8bitmp3 Oct 26, 2020
Author

Great idea @melissawm

A matplotlib visualization of the error rate falling and accuracy improving (hopefully) over time would also be super — and it's a common practice. Working on it!

This comment has been minimized.

@8bitmp3

8bitmp3 Oct 29, 2020
Author

I'm not 100% sure how to do "inference" with a trained model to classify images, since I don't think we have the Saved Model or checkpoint saving capability in the simple example. But we can do the following:

TODO:

  • 1. We can add graphs with matplotlib for:
    • The error rate (loss) for training and test sets (y) per epochs (x) (it should be gradually going down).
    • The accuracy rate for training and test sets (y) per epochs (x) (it should be gradually going up).
      • Need to add a few lines to save the loss and accuracy (prediction) data at each epoch by appending them separate lists (for storage).
  • 2. Add a simple image to help new users:
    • Showing a loop: inputs as vectors -> forward pass through layers -> output (prediction) + loss calculation -> backpropagation with gradient descent -> weight update.

Other improvement suggestions:

  • Try Fashion MNIST - a similar dataset but more fun.

This comment has been minimized.

@8bitmp3

8bitmp3 Nov 3, 2020
Author

@melissawm PTAL 😃 Thank you!

@@ -0,0 +1,1067 @@
{

This comment has been minimized.

@melissawm

melissawm Nov 4, 2020
Member

I think it might be better to leave all credits at the bottom of the document.


Reply via ReviewNB

@@ -0,0 +1,1067 @@
{

This comment has been minimized.

@melissawm

melissawm Nov 4, 2020
Member

Item 4 is missing now :)


Reply via ReviewNB

@@ -0,0 +1,1067 @@
{

This comment has been minimized.

@melissawm

melissawm Nov 4, 2020
Member

The plots are great, I think that closes the tutorial nicely!

Tip: you can add plt.tight_layout() to improve on the spacing between the subplots - at least locally I had the title and x-axis label of the subplots superimposed without using tight_layout.


Reply via ReviewNB

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

In this section, you will download the zipped MNIST dataset files originally stored in Yann LeCun's website. Then, you will transform them into 4 files of NumPy array type using built-in Python modules. Finally, you will split the arrays into training and test sets.

If you're explicit about what this section produces, readers can read just the first lines and skip down to the next heading. You could reword this as:

The first step is bookkeeping. You will create four ndarrays as inputs to the neural network:


x_train, y_train, x_test, y_test = (mnist_dataset["training_images"],
                                    mnist_dataset["training_labels"],
                                    mnist_dataset["test_images"],
                                    mnist_dataset["test_labels"])> ```

The MNIST data as stored on Yann LeCun's website consists of four compressed files that you'll read into the ndarrays.
...[Pick up the bulleted list]

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

Extract and save the data to disk using NumPy for the first time to read the contents of each file as ndarrays and saving them into a dictionary.

We're not really saving to disk here -- that implies we're writing files. How about

NumPy can decompress the 4 files on the fly and create 4 ndarrays, which we save in a dictionary.

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

Note that you need to unroll (reshape) the original 28x28-pixel images into 1D vectors (of size 28 times 28 or 784)

This needs to be a little clearer about how the data in the files is formatted so we understand what this step is doing.

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

> for name in filename[:2]:
>...
> for name in filename[-2:]:

Comments would make it a little clearer:

> # Images
> for name in filename[:2]:
>...
> # Labels
> for name in filename[-2:]:
@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

Split the data into training and test sets using the standard notation of and call the training and test set of images as x_train and x_test, and the set of labels as y_train and y_test:

Not clear what "standard notation" is referring to. How about:

Split the data into training and test sets using the standard notation of x for data and y for labels, calling the training and test images x_train and x_test, and the labels y_train and y_test:

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

To make sure you have the right data, print the shape of the sets:

If you were really making sure, you'd code a test, not a print statement. And you should say what the user should expect to see. How about

You can confirm that the shape of the image arrays is xxx and label arrays is yyy:

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

(Optional) You can inspect some samples of the image data with matplotlib and image labels:

'Optional' not needed. This step follows another optional step, looking at the shapes, so it can read:

And you can inspect some images using matplotlib:

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

You have loaded the MNIST dataset and saved it to disk as 4 files:

I apologize if I've misread the Python, but have we actually created 4 files? It looks like we have a dictionary of 4 arrays.

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

Note: Data preparation in deep learning is a very challenging process...

In general, tutorials should not include side comments in the main text.

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

sample = 1000
training_images = x_train[0:sample] / 255
test_images = x_test.reshape(len(x_test), 28*28) / 255
  1. Not clear (to me, anyway) why the test and training images are being treated differently.
  2. I would call loud attention to the fact -- surprising to newcomers and characteristic of NumPy -- that the division is not truncating the result into uint8's (which would leave zeros almost everywhere) but silently promoting the dtype into uint64, and an explicit cast is unneeded.
  3. Don't put the note underneath, and don't use "Note" -- include it in the "Normalize the arrays..." text.
@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

The opening of "Preprocess the data" is wordy and doesn't explain much. I'd replace the entire opening, up to the first heading, with just this:

We make two modifications that are conventional for neural networks:

This also makes it easy to read the first lines and then skip to the next section, where the real NN stuff begins.

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

Note: You can also check that normalization was successful...

This and most of the other "Note" sections should be integrated into the main text. Saying "note" isn't necessary; the typography of the note sections is incongruous; and Google style calls this kind of usage a placeholder and discourages it.

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

Convert the image data to floating points

I can see the justification for saying "floating points" ("Convert the sheds to bungalows"), but I think in this context it's more idiomatic to say "floating point" without the s ("Convert the tickets to business class."). Others can weigh in.

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

Use the newly created function to one-hot encode the image labels and assign the train and test label data variables — y_train and y_test — to training_labels and test_labels, respectively:

Suggested:

Encode the labels and assign the values to new variables:

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

(Optional) Visualize a few samples of the one-hot encoded labels:
...and compare them to the original labels:

Suggested:

Examine a few encoded labels:
...and compare to the originals:

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

This type of machine or deep learning is often referred to as supervised learning

It's unclear which part of the preceding 120-word description "this type" refers to. Supervised learning means learning from labeled examples. If you want to explain what supervised learning is, say that. My suggestion, though, is to drop the sentence.

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

This tutorial demonstrates how to build a simple feedforward neural network (with one hidden layer) and train it with backpropagation from scratch with NumPy to recognize hardwritten digit images.

I'd leave backpropagation out of the first line; you say later that it trains using forward propagation and backpropagation.
Also typo in "hardwritten."

This tutorial demonstrates how to build a simple feedforward neural network (with one hidden layer) and train it with from scratch with NumPy to recognize handwritten digits.

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

The final output of the network is a vector of 10 scores — one for each handwritten digit image.

It's unnecessary and confusing to introduce this detail here. I suggest leaving it out.

@bjnath
Copy link
Contributor

@bjnath bjnath commented Nov 4, 2020

This tutorial was adapted from the work by Andrew Trask (with the author's permission).

Suggested:

This tutorial was adapted with permission from Andrew Trask's Grokking Deep Learning, which teaches deep learning with NumPy.

And drop the reference in Prerequisites.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

3 participants
You can’t perform that action at this time.