Lab: Python Basics with NumPy

deep-learning
numpy
python
lab
Sigmoid and its gradient, reshaping images into vectors, row normalization, softmax, and vectorized L1 and L2 loss functions in NumPy.
Published

Jul 30, 2026

ImportantTwo changes from the original notebook

This page runs on NumPy 2.4.4 and Python 3.13, where the assignment was written against NumPy 1.x. Two things changed (updated 2026-08-31).

  • The grader is stripped. The assignment’s *_test functions and their green “All tests passed” prints are gone. Where a check was interesting, its value is printed and explained instead.
  • The normalization example was made consistent. The assignment’s markdown derives the answer for the row [2, 6, 4] with norm \(\sqrt{56}\), while its code runs on [1, 6, 4]. The derivation here uses the row the code actually normalizes, so the printed output matches the worked example.

Every function, every test input and every printed value otherwise follows the assignment.

This is the first programming assignment of the course, a brief, optional warm-up with Python and NumPy. Even if you have used Python before, it helps you get familiar with the functions the course needs. The exercises put the ideas from the Python and vectorization section into practice.

After this lab you will be able to

One standing instruction for the whole lab: avoid using for loops and while loops unless you are explicitly told to do so.

As a tiny warm-up in the notebook environment, the first exercise just sets a variable and prints it, to practice running cells.

test = "Hello World"
print("test: " + test)
test: Hello World

Building Basic Functions with NumPy

NumPy is the main package for scientific computing in Python, maintained by a large community (numpy.org). In this part you build several small functions around key NumPy tools such as np.exp and np.reshape that future assignments rely on.

Sigmoid with math.exp

Before using np.exp(), you use math.exp() to implement the sigmoid function, and then see why np.exp() is preferable. As a reminder from the logistic regression section,

\[ \text{sigmoid}(x) = \frac{1}{1 + e^{-x}} \]

is sometimes also known as the logistic function. It is a non-linear function used not only in machine learning (logistic regression), but also in deep learning.

To call a function belonging to a specific package, you write package_name.function(). Here math.exp(x) computes \(e^x\), so the whole formula translates directly into one line of code.

import math
import numpy as np

def basic_sigmoid(x):
    """
    Compute sigmoid of x.

    Arguments:
    x -- A scalar

    Return:
    s -- sigmoid(x)
    """
    s = 1 / (1 + math.exp(-x))
    return s

print("basic_sigmoid(x=1) = " + str(basic_sigmoid(1)))
basic_sigmoid(x=1) = 0.7310585786300049

We actually rarely use the math library in deep learning, because its functions expect a single real number. In deep learning we mostly work with matrices and vectors, and this is where math.exp breaks down. Watch what happens when the input is a list of three numbers. The failure happens even earlier than you might expect. The -x inside basic_sigmoid is what raises, because a Python list has no unary minus, so execution never even reaches math.exp.

x = [1, 2, 3]  # x becomes a python list object
basic_sigmoid(x)  # this gives an error, because x is a vector
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[3], line 2
      1 x = [1, 2, 3]  # x becomes a python list object
----> 2 basic_sigmoid(x)  # this gives an error, because x is a vector

Cell In[2], line 14, in basic_sigmoid(x)
     10 
     11     Return:
     12     s -- sigmoid(x)
     13     """
---> 14     s = 1 / (1 + math.exp(-x))
     15     return s

TypeError: bad operand type for unary -: 'list'

In contrast, if \(x = (x_1, x_2, \ldots, x_n)\) is a vector, then np.exp(x) applies the exponential function to every element, returning \((e^{x_1}, e^{x_2}, \ldots, e^{x_n})\). This is exactly the element-wise behavior covered in the vectorization section.

t_x = np.array([1, 2, 3])
print(np.exp(t_x))  # result is (exp(1), exp(2), exp(3))
[ 2.71828183  7.3890561  20.08553692]

Furthermore, if x is a vector, then operations such as x + 3 or 1 / x output a vector of the same size as x.

print(t_x + 3)
[4 5 6]

Any time you need more information on a NumPy function, look at the official documentation, or write np.exp? in a notebook cell for quick access to it.

Sigmoid with NumPy

Now implement the sigmoid using NumPy, so that x can be a real number, a vector, or a matrix. The data structures NumPy uses to represent these shapes are called NumPy arrays.

\[ \text{For } x \in \mathbb{R}^n, \quad \text{sigmoid}(x) = \text{sigmoid} \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{bmatrix} = \begin{bmatrix} \dfrac{1}{1 + e^{-x_1}} \\[2pt] \dfrac{1}{1 + e^{-x_2}} \\ \vdots \\ \dfrac{1}{1 + e^{-x_n}} \end{bmatrix} \]

The code is the same one-liner as before, with np.exp in place of math.exp.

def sigmoid(x):
    """
    Compute the sigmoid of x

    Arguments:
    x -- A scalar or numpy array of any size

    Return:
    s -- sigmoid(x)
    """
    s = 1 / (1 + np.exp(-x))
    return s

t_x = np.array([1, 2, 3])
print("sigmoid(t_x) = " + str(sigmoid(t_x)))
sigmoid(t_x) = [0.73105858 0.88079708 0.95257413]

Sigmoid Gradient

As you saw in the lectures, you need to compute gradients to optimize loss functions using backpropagation. Here is your first gradient function. The formula, which already appeared in the derivation of \(d\mathcal{L}/dz\), is

\[ \text{sigmoid\_derivative}(x) = \sigma'(x) = \sigma(x)\big(1 - \sigma(x)\big) \]

You often code this function in two steps. First set s to be the sigmoid of x, reusing the sigmoid(x) function, then compute \(s(1 - s)\).

def sigmoid_derivative(x):
    """
    Compute the gradient (also called the slope or derivative) of the
    sigmoid function with respect to its input x.

    Arguments:
    x -- A scalar or numpy array

    Return:
    ds -- Your computed gradient.
    """
    s = sigmoid(x)
    ds = s * (1 - s)
    return ds

t_x = np.array([1, 2, 3])
print("sigmoid_derivative(t_x) = " + str(sigmoid_derivative(t_x)))
sigmoid_derivative(t_x) = [0.19661193 0.10499359 0.04517666]

Reshaping Arrays

Two common NumPy tools in deep learning are X.shape, which gets the shape (dimensions) of a matrix or vector, and X.reshape(...), which reshapes it into some other dimension.

For example, an image is represented by a 3D array of shape \((\text{length}, \text{height}, \text{depth} = 3)\). However, when you feed an image to an algorithm, you convert it to a vector of shape \((\text{length} \times \text{height} \times 3, 1)\). In other words, you unroll the 3D array into a 1D vector, exactly the operation described for the cat classifier in the binary classification section.

Implement image2vector(), which takes an input of shape (length, height, 3) and returns a vector of shape (length*height*3, 1). Two practical notes.

  • Do not hardcode the dimensions as constants. Look them up with image.shape[0], image.shape[1], image.shape[2].
  • v = v.reshape(-1, 1) also works, because -1 tells NumPy to infer that dimension from the total number of elements. Just make sure you understand why.
def image2vector(image):
    """
    Argument:
    image -- a numpy array of shape (length, height, depth)

    Returns:
    v -- a vector of shape (length*height*depth, 1)
    """
    v = image.reshape(image.shape[0] * image.shape[1] * image.shape[2], 1)
    return v

Test it on a 3 by 3 by 2 array. Typical images are (num_px_x, num_px_y, 3) with 3 for the RGB values, but a depth of 2 keeps the printout short.

t_image = np.array([[[ 0.67826139,  0.29380381],
                     [ 0.90714982,  0.52835647],
                     [ 0.4215251 ,  0.45017551]],

                   [[ 0.92814219,  0.96677647],
                    [ 0.85304703,  0.52351845],
                    [ 0.19981397,  0.27417313]],

                   [[ 0.60659855,  0.00533165],
                    [ 0.10820313,  0.49978937],
                    [ 0.34144279,  0.94630077]]])

print("image2vector(image) = " + str(image2vector(t_image)))
image2vector(image) = [[0.67826139]
 [0.29380381]
 [0.90714982]
 [0.52835647]
 [0.4215251 ]
 [0.45017551]
 [0.92814219]
 [0.96677647]
 [0.85304703]
 [0.52351845]
 [0.19981397]
 [0.27417313]
 [0.60659855]
 [0.00533165]
 [0.10820313]
 [0.49978937]
 [0.34144279]
 [0.94630077]]

Normalizing Rows

Another common technique in machine learning and deep learning is to normalize the data. It often leads to better performance, because gradient descent converges faster after normalization. Here, normalization means changing \(x\) to \(\frac{x}{\|x\|}\), dividing each row vector of \(x\) by its norm. For example, if

\[ x = \begin{bmatrix} 0 & 3 & 4 \\ 1 & 6 & 4 \end{bmatrix} \]

then the row norms are

\[ \|x\| = \texttt{np.linalg.norm(x, axis=1, keepdims=True)} = \begin{bmatrix} 5 \\ \sqrt{53} \end{bmatrix} \]

and

\[ x_{\text{normalized}} = \frac{x}{\|x\|} = \begin{bmatrix} 0 & \frac{3}{5} & \frac{4}{5} \\[2pt] \frac{1}{\sqrt{53}} & \frac{6}{\sqrt{53}} & \frac{4}{\sqrt{53}} \end{bmatrix} \]

Notice that you can divide matrices of different sizes and it works fine. That is broadcasting, covered in the broadcasting section. Three parameters of np.linalg.norm matter here.

  • axis=1 computes the norm row-wise. For a column-wise norm you would set axis=0.
  • keepdims=True keeps the result as a column, shape \((n, 1)\) rather than \((n,)\), so it broadcasts correctly against the original x (and avoids the rank 1 arrays the previous section warned about).
  • ord=2 selects the 2-norm, the square root of the sum of squares.

Implement normalize_rows() so that after applying it, each row of the input matrix is a vector of unit length (length 1).

def normalize_rows(x):
    """
    Implement a function that normalizes each row of the matrix x
    (to have unit length).

    Argument:
    x -- A numpy matrix of shape (n, m)

    Returns:
    x -- The normalized (by row) numpy matrix.
    """
    x_norm = np.linalg.norm(x, ord=2, axis=1, keepdims=True)
    x = x / x_norm
    return x

x = np.array([[0., 3., 4.],
              [1., 6., 4.]])
print("normalizeRows(x) = " + str(normalize_rows(x)))
normalizeRows(x) = [[0.         0.6        0.8       ]
 [0.13736056 0.82416338 0.54944226]]

If you print the shapes inside normalize_rows(), you find that x_norm and x have different shapes. That is normal. x_norm takes the norm of each row, so it has the same number of rows but only one column. The division still works because of broadcasting.

Softmax

Now implement a softmax function using NumPy. You can think of softmax as a normalizing function used when your algorithm needs to classify two or more classes. It plays a bigger role in the second course of the specialization, and you already met it in the multiclass classification part of the machine learning course.

For a row vector \(x \in \mathbb{R}^{1 \times n}\),

\[ \text{softmax}(x) = \text{softmax}\begin{bmatrix} x_1 & x_2 & \cdots & x_n \end{bmatrix} = \begin{bmatrix} \dfrac{e^{x_1}}{\sum_j e^{x_j}} & \dfrac{e^{x_2}}{\sum_j e^{x_j}} & \cdots & \dfrac{e^{x_n}}{\sum_j e^{x_j}} \end{bmatrix} \]

For a matrix \(x \in \mathbb{R}^{m \times n}\), softmax applies to each row separately, so every row of the result is the softmax of the corresponding row of \(x\).

Later in the course, \(m\) represents the number of training examples, each example sits in its own column, and each feature in its own row. Softmax would then be performed on the columns. In this coding practice we are just getting familiar with Python, so we use the common math notation \(m \times n\), with \(m\) rows and \(n\) columns.

The implementation takes three lines, and the last one leans on broadcasting again.

  1. Apply np.exp() element-wise to x.
  2. Sum each row with np.sum(..., axis=1, keepdims=True).
  3. Divide, letting broadcasting stretch the \((m, 1)\) sums across each row.
def softmax(x):
    """Calculates the softmax for each row of the input x.

    Argument:
    x -- A numpy matrix of shape (m,n)

    Returns:
    s -- A numpy matrix equal to the softmax of x, of shape (m,n)
    """
    x_exp = np.exp(x)
    x_sum = np.sum(x_exp, axis=1, keepdims=True)
    s = x_exp / x_sum
    return s

t_x = np.array([[9, 2, 5, 0, 0],
                [7, 5, 0, 0, 0]])
print("softmax(x) = " + str(softmax(t_x)))
softmax(x) = [[9.80897665e-01 8.94462891e-04 1.79657674e-02 1.21052389e-04
  1.21052389e-04]
 [8.78679856e-01 1.18916387e-01 8.01252314e-04 8.01252314e-04
  8.01252314e-04]]

If you print the shapes, x_sum is \((2, 1)\) while x_exp and s are \((2, 5)\). The division x_exp / x_sum works due to Python broadcasting. Notice also that each row of the output sums to 1, which is what lets softmax outputs be read as probabilities over classes.

NoteWhat You Need to Remember
  • np.exp(x) works for any NumPy array x and applies the exponential function to every coordinate.
  • The sigmoid function and its gradient, \(\sigma'(x) = \sigma(x)(1 - \sigma(x))\).
  • image2vector is commonly used in deep learning.
  • np.reshape is widely used. Keeping your matrix and vector dimensions straight goes a long way toward eliminating bugs.
  • NumPy has efficient built-in functions.
  • Broadcasting is extremely useful.

Vectorization

In deep learning you deal with very large datasets, so a non-computationally-optimal function can become a huge bottleneck and result in a model that takes ages to run. To keep your code computationally efficient, you use vectorization. To feel the difference, time four classic loop-based products against their vectorized versions, using time.process_time() around each computation. The first pair of vectors is small on purpose, so the loop versions are still tolerable.

First the loop implementations, namely the dot product, the outer product, element-wise multiplication, and a general matrix-vector product.

import time

x1 = [9, 2, 5, 0, 0, 7, 5, 0, 0, 0, 9, 2, 5, 0, 0]
x2 = [9, 2, 2, 9, 0, 9, 2, 5, 0, 0, 9, 2, 5, 0, 0]

### CLASSIC DOT PRODUCT OF VECTORS IMPLEMENTATION ###
tic = time.process_time()
dot = 0
for i in range(len(x1)):
    dot += x1[i] * x2[i]
toc = time.process_time()
print("dot = " + str(dot) + "\n ----- Computation time = " + str(1000 * (toc - tic)) + "ms")

### CLASSIC OUTER PRODUCT IMPLEMENTATION ###
tic = time.process_time()
outer = np.zeros((len(x1), len(x2)))  # len(x1)*len(x2) matrix of zeros
for i in range(len(x1)):
    for j in range(len(x2)):
        outer[i, j] = x1[i] * x2[j]
toc = time.process_time()
print("outer = " + str(outer) + "\n ----- Computation time = " + str(1000 * (toc - tic)) + "ms")

### CLASSIC ELEMENTWISE IMPLEMENTATION ###
tic = time.process_time()
mul = np.zeros(len(x1))
for i in range(len(x1)):
    mul[i] = x1[i] * x2[i]
toc = time.process_time()
print("elementwise multiplication = " + str(mul) + "\n ----- Computation time = " + str(1000 * (toc - tic)) + "ms")

### CLASSIC GENERAL DOT PRODUCT IMPLEMENTATION ###
W = np.random.rand(3, len(x1))  # random 3*len(x1) numpy array
tic = time.process_time()
gdot = np.zeros(W.shape[0])
for i in range(W.shape[0]):
    for j in range(len(x1)):
        gdot[i] += W[i, j] * x1[j]
toc = time.process_time()
print("gdot = " + str(gdot) + "\n ----- Computation time = " + str(1000 * (toc - tic)) + "ms")
dot = 278
 ----- Computation time = 0.03300000000017178ms
outer = [[81. 18. 18. 81.  0. 81. 18. 45.  0.  0. 81. 18. 45.  0.  0.]
 [18.  4.  4. 18.  0. 18.  4. 10.  0.  0. 18.  4. 10.  0.  0.]
 [45. 10. 10. 45.  0. 45. 10. 25.  0.  0. 45. 10. 25.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [63. 14. 14. 63.  0. 63. 14. 35.  0.  0. 63. 14. 35.  0.  0.]
 [45. 10. 10. 45.  0. 45. 10. 25.  0.  0. 45. 10. 25.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [81. 18. 18. 81.  0. 81. 18. 45.  0.  0. 81. 18. 45.  0.  0.]
 [18.  4.  4. 18.  0. 18.  4. 10.  0.  0. 18.  4. 10.  0.  0.]
 [45. 10. 10. 45.  0. 45. 10. 25.  0.  0. 45. 10. 25.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]]
 ----- Computation time = 0.06599999999989947ms
elementwise multiplication = [81.  4. 10.  0.  0. 63. 10.  0.  0.  0. 81.  4. 25.  0.  0.]
 ----- Computation time = 0.02799999999991698ms
gdot = [13.28310108 23.70350687 16.8468247 ]
 ----- Computation time = 0.04300000000001525ms

Now the same four computations, each with a single NumPy call.

### VECTORIZED DOT PRODUCT OF VECTORS ###
tic = time.process_time()
dot = np.dot(x1, x2)
toc = time.process_time()
print("dot = " + str(dot) + "\n ----- Computation time = " + str(1000 * (toc - tic)) + "ms")

### VECTORIZED OUTER PRODUCT ###
tic = time.process_time()
outer = np.outer(x1, x2)
toc = time.process_time()
print("outer = " + str(outer) + "\n ----- Computation time = " + str(1000 * (toc - tic)) + "ms")

### VECTORIZED ELEMENTWISE MULTIPLICATION ###
tic = time.process_time()
mul = np.multiply(x1, x2)
toc = time.process_time()
print("elementwise multiplication = " + str(mul) + "\n ----- Computation time = " + str(1000 * (toc - tic)) + "ms")

### VECTORIZED GENERAL DOT PRODUCT ###
tic = time.process_time()
dot = np.dot(W, x1)
toc = time.process_time()
print("gdot = " + str(dot) + "\n ----- Computation time = " + str(1000 * (toc - tic)) + "ms")
dot = 278
 ----- Computation time = 0.032000000000032ms
outer = [[81 18 18 81  0 81 18 45  0  0 81 18 45  0  0]
 [18  4  4 18  0 18  4 10  0  0 18  4 10  0  0]
 [45 10 10 45  0 45 10 25  0  0 45 10 25  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [63 14 14 63  0 63 14 35  0  0 63 14 35  0  0]
 [45 10 10 45  0 45 10 25  0  0 45 10 25  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [81 18 18 81  0 81 18 45  0  0 81 18 45  0  0]
 [18  4  4 18  0 18  4 10  0  0 18  4 10  0  0]
 [45 10 10 45  0 45 10 25  0  0 45 10 25  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]]
 ----- Computation time = 0.034000000000089514ms
elementwise multiplication = [81  4 10  0  0 63 10  0  0  0 81  4 25  0  0]
 ----- Computation time = 0.015000000000098268ms
gdot = [13.28310108 23.70350687 16.8468247 ]
 ----- Computation time = 0.01999999999990898ms

The vectorized implementation is much cleaner, and the results match the loop versions exactly. On vectors this small the timing difference is tiny (and can even flip, since the NumPy call has some fixed overhead), but for bigger vectors and matrices the difference in running time becomes enormous, as the timing demo on million-element arrays showed.

Note that np.dot() performs a matrix-matrix or matrix-vector multiplication. This is different from np.multiply() and the * operator (equivalent to .* in MATLAB and Octave), which performs element-wise multiplication.

L1 and L2 Loss

To close, implement vectorized versions of the L1 and L2 loss functions. A reminder about losses: they evaluate the performance of the model. The bigger the loss, the more different the predictions \(\hat{y}\) are from the true values \(y\), and optimization algorithms like gradient descent train the model by driving the cost down.

The L1 loss sums the absolute differences,

\[ L_1(\hat{y}, y) = \sum_{i=0}^{m-1} \left| y^{(i)} - \hat{y}^{(i)} \right| \]

The function np.abs(x) (absolute value of x) does the element-wise work, and np.sum collapses the vector to a number.

def L1(yhat, y):
    """
    Arguments:
    yhat -- vector of size m (predicted labels)
    y -- vector of size m (true labels)

    Returns:
    loss -- the value of the L1 loss function defined above
    """
    loss = np.sum(np.abs(y - yhat))
    return loss

yhat = np.array([.9, 0.2, 0.1, .4, .9])
y = np.array([1, 0, 0, 1, 1])
print("L1 = " + str(L1(yhat, y)))
L1 = 1.1

The L2 loss sums the squared differences,

\[ L_2(\hat{y}, y) = \sum_{i=0}^{m-1} \left( y^{(i)} - \hat{y}^{(i)} \right)^2 \]

There are several ways to implement it, but np.dot() is handy here. If \(x = [x_1, x_2, \ldots, x_n]\), then np.dot(x, x) computes \(\sum_{j=1}^{n} x_j^2\), so dotting the difference vector with itself gives the sum of squares in one call.

def L2(yhat, y):
    """
    Arguments:
    yhat -- vector of size m (predicted labels)
    y -- vector of size m (true labels)

    Returns:
    loss -- the value of the L2 loss function defined above
    """
    loss = np.dot(y - yhat, y - yhat)
    return loss

print("L2 = " + str(L2(yhat, y)))
L2 = 0.43

Congratulations on completing this lab. This little warm-up prepares you for the next lab, which is more exciting: building an actual image classifier with everything the course has covered so far.

NoteWhat to Remember
  • Vectorization is very important in deep learning. It provides computational efficiency and clarity.
  • You have reviewed the L1 and L2 loss.
  • You are familiar with many NumPy functions such as np.sum, np.dot, np.multiply, and np.maximum.

Review Questions

1. Why do we use np.exp rather than math.exp in deep learning code?

math.exp only accepts a single real number and throws an error on a list or array, while np.exp applies the exponential element-wise to a NumPy array of any size. Deep learning works with vectors and matrices, so the element-wise version is the useful one.


1. Write the sigmoid gradient formula and evaluate it at \(x = 0\).

\(\sigma'(x) = \sigma(x)(1 - \sigma(x))\). At \(x = 0\), \(\sigma(0) = 0.5\), so \(\sigma'(0) = 0.5 \times 0.5 = 0.25\), the steepest point of the sigmoid.


1. An image has shape \((64, 64, 3)\). What does image2vector return for it, and which reshape call produces that without hardcoding dimensions?

A column vector of shape \((64 \times 64 \times 3, 1) = (12288, 1)\). Use image.reshape(image.shape[0] * image.shape[1] * image.shape[2], 1), or equivalently image.reshape(-1, 1), where -1 tells NumPy to infer the dimension from the total element count.


1. In np.linalg.norm(x, ord=2, axis=1, keepdims=True), what do axis=1 and keepdims=True do?

axis=1 computes the norm row-wise (one norm per row), and keepdims=True keeps the result as an \((n, 1)\) column rather than a rank 1 array of shape \((n,)\), so that dividing x by it broadcasts correctly across each row.


1. What is the difference between np.dot(x1, x2) and np.multiply(x1, x2)?

np.dot performs matrix-matrix or matrix-vector multiplication (for two vectors, the dot product, a single number). np.multiply, like the * operator, performs element-wise multiplication and returns an array of the same shape.


1. Give the formulas for the L1 and L2 losses, and the one-line NumPy trick behind the L2 implementation.

\[ L_1(\hat{y}, y) = \sum_{i} \left| y^{(i)} - \hat{y}^{(i)} \right| \qquad L_2(\hat{y}, y) = \sum_{i} \left( y^{(i)} - \hat{y}^{(i)} \right)^2 \] For L2, np.dot(y - yhat, y - yhat) dots the difference vector with itself, which is exactly the sum of squared differences.

Back to top