###### Content under Creative Commons Attribution license CC-BY 4.0, code under BSD 3-Clause License © 2021 Lorena A. Barba

# Linear regression by gradient descent

This module of _Engineering Computations_ takes a step-by-step approach to introduce you to the essential ideas of deep learning, an algorithmic technology that is taking the world by storm. 
It is at the core of the artificial intelligence boom, and we think every scientist and engineer should understand the basics, at least. 

Another term for deep learning is deep neural networks. 
In this module, you will learn how neural-network models are built, computationally. 
The inspiration for deep learning may have been how the brain works, but in practice what we have is a method to build models, using mostly linear algebra and a little bit of calculus. 
These models are not magical, or even "intelligent"—they are just about _optimization_, which every engineer knows about!

In this lesson, we take the first step of model-building: linear regression. 
The very first module of the _Engineering Computations_ series discusses [linear regression with real data](http://go.gwu.edu/engcomp1lesson5), and there we found the model parameters (slope and $y$-intercept) analytically. 
Let's forget about that for this lesson. 
The key concept we introduce here will be _gradient descent_. Start your ride here!

## Gradient descent

This lesson is partly based on a tutorial at the 2019 SciPy Conference by Eric Ma [1]. He begins his tutorial by presenting the idea of _gradient descent_ with a simple quadratic function: the question is how do we find this function's minimum?

$$f(w) = w^2 +3w -5$$

We know from calculus that at the minimum, the derivative of the function is zero (the tangent to the function curve is horizontal), and the second derivative is positive (the curve slants _up_ on each side of the minimum). 
The analytical derivative of the function above is $f^\prime(w) = 2w + 3$ and the second derivative is $f^{\prime\prime}(w)=2>0$. Thus, we make $2w+3=0$ to find the minimum.

Let's play with this function using SymPy. We'll later use NumPy, and make plots with Matplotlib, so we load all the libraries in one place.


```python
import sympy
import numpy

from matplotlib import pyplot
%matplotlib inline
```

We run this SymPy method to get beautiful typeset symbols and equations (in the Jupyter notebook, it will use [MathJax](https://en.wikipedia.org/wiki/MathJax) by default): 


```python
sympy.init_printing()
```

Now we'll define the Python variable `w` to be a SymPy symbol, and create the expression `f` to match the mathematical function above, and plot it.


```python
w = sympy.Symbol('w', real=True)

f = w**2 + 3*w - 5
f
```


```python
sympy.plotting.plot(f);
```

A neat parabola. We can see from the plot that the minimum of $f(w)$ is reached somewhere between $w=-2.5$ and $w=0$. SymPy can tell us the derivative, and the value where it is zero:


```python
fprime = f.diff(w)
fprime
```


```python
sympy.solve(fprime, w)
```

That looks about right: $-3/2$ or $-1.5$. 
We could have also solved this by hand, because it's a simple function. 
But for more complicated functions, finding the minimum analytically could be more difficult. 
Instead, we can use the iterative method of gradient descent. 

The idea in gradient descent is to find the value of $w$ at the function minimum by starting with an initial guess, then iteratively taking small steps down the slope of the function, i.e., in the negative gradient direction. 
To illustrate the process, we turn the symbolic expression `fprime` into a Python function that we can call, and use it in a simple loop taking small steps:


```python
fpnum = sympy.lambdify(w, fprime)
type(fpnum)
```




    function



Yep. We got a Python function with the [`sympy.lambdify()`](https://docs.sympy.org/latest/modules/utilities/lambdify.html) method, whose return value is of type `function`. Now, you can pick any starting guess, say $w=10$, and advance in a loop taking steps of size $0.01$ (a choice we make; more on this later):


```python
w = 10.0   # starting guess for the min

for i in range(1000):
    w = w - fpnum(w)*0.01 # with 0.01 the step size

print(w)
```

    -1.4999999806458753


That gave a result very close to the true value $-1.5$, and all we needed was a function for the derivative of $f(w)$. This is how you find the argument of the minimum of a function iteratively. 

##### Note

> Implied in this method is that the function is differentiable, and that we can step *down* the slope, meaning its second derivative is positive, or the function is _convex_.

 

#### Gradient descent steps in the direction of the negative slope to approach the minimum.

## Linear regression

Suppose you have data consisting of one independent variable and one dependent variable, and when you plot the data it seems to noisily follow a trend line. 
To build a model with this data, you assume the relationship is _linear_, and seek to find the line's slope and $y$-intercept (the model parameters) that best fit the data. 

Though this sounds straightforward, some key ideas of machine learning are contained:

- we don't _know_ the true relationship between the variables, we _assume_ it is linear (and go for it!)
- the model we chose (linear) has some parameters (slope, intercept) that are unknown
- we will need some data (observational, experimental) of the dependent and independent variables
- we find the model parameters by fitting the "best" line to the data
- the model with its parameters can then be used to make _predictions_

Let's make some synthetic data to play with, following the example in Eric Ma's tutorial [1].


```python
# make sythetic data (from Eric's example)
x_data = numpy.linspace(-5, 5, 100)
w_true = 2
b_true = 20

y_data = w_true*x_data + b_true + numpy.random.normal(size=len(x_data))

pyplot.scatter(x_data,y_data);
```

This situation arises often. In **Module 1** of _Engineering Computations_, we used a real data set of Earth temperature over time and we fit an ominously sloped line. 
We derived analytical formulas for the model coefficients and wrote our own custom functions, and we also learned that NumPy has a built-in function that will do it for us: `numpy.polyfit(x, y, 1)` will return the two parameters $w, b$ for the line

$$y = w x + b $$

Here, we will instead use gradient descent to get the parameters of the linear model. 
The first step is to define a function that represents the _deviation_ of the data from the model. 
For linear regression, we use the sum (or the mean) of the square _errors_: the differences between each data point and the predicted value from the linear model (also called _residuals_).

 

#### Each data point deviates from the linear regression: we aim to minimize the sum of squares of the residuals.


Let's review our ingredients:

1. observational data, in the form of two arrays: $x, y$
2. our linear model: $y = wx + b$
3. a function that measures the discrepancy between the data and the fitting line: $\frac{1}{N}\sum (y_i - f(x_i))^2$

The last item is called a "loss function" (also sometimes "cost function"). Our method will be to step down the slope of the loss function, to find its minimum.

As a first approach, let's again use SymPy, which can compute derivatives for us. Below, we define the loss function for a single data point, and make Python functions with its derivatives with respect to the model parameters. 
We will call these functions in a sequence of steps that start at an initial guess for the parameters (we choose zero), and step in the negative gradient multiplied by a step size (we choose $0.01$). 
After $1000$ steps, you see that the values of $w$ and $b$ are quite close to the true values from our synthetic data.


```python
w, b, x, y = sympy.symbols('w b x y')

loss = (w*x + b - y)**2
loss
```


```python
grad_b = sympy.lambdify([w,b,x,y], loss.diff(b), 'numpy')
grad_w = sympy.lambdify([w,b,x,y], loss.diff(w), 'numpy')
```

Be sure to read the documentation for [`sympy.lambdify()`](https://docs.sympy.org/latest/modules/utilities/lambdify.html), which explains the argument list.
Now, we step down the slope. 
Note that we first compute the derivatives with respect to both parameters _at all the data points_ (thanks to NumPy array operations), and we take the average. 
Then we step both parameters (starting from an initial guess of zero).


```python
w = 0
b = 0

for i in range(1000):
    descent_b = numpy.sum(grad_b(w,b,x_data,y_data))/len(x_data)
    descent_w = numpy.sum(grad_w(w,b,x_data,y_data))/len(x_data)
    w = w - descent_w*0.01 # with 0.01 the step size
    b = b - descent_b*0.01 

print(w)
print(b)
```

    2.03136593978268
    20.030956250406764



```python
pyplot.scatter(x_data,y_data)
pyplot.plot(x_data, w*x_data + b, '-r');
```

It works! That line looks to be fitting the data pretty well. Now we have a "best fit" line that represents the data, and that we can use to estimate the value of the dependent variable for any value of the independent variable, even if not present in the data. That is, _to make predictions_.

##### Key idea

> "Learning" means building a model by finding the parameters that best fit the data. We do it by minimizing a loss function (a.k.a. cost function), which involves computing derivatives with respect to the parameters in the model. 

Here, we used SymPy to help us out with the derivatives, but for more complex models (which may have many parameters), this could be a cumbersome approach. 
Instead, we will make use of the technique of _automatic differentiation_, which evaluates the derivative of a function written in code. 
You'll learn more about it in the next lesson, on **logistic regression**.

## What we've learned

- Gradient descent can find a minimum of a function.
- Linear regression starts by assuming a linear relationship between two variables.
- A model includes the assumed relationship in the data and model parameters.
- Observational data allows finding the parameters in the model (slope, intercept).
- A loss function captures the deviation between the observed and the predicted values of the dependent variable.
- We find the parameters by minimizing the loss function via gradient descent.
- SymPy computes derivatives with `sympy.diff()` and returns numeric functions with `simpy.lambdify()`.

## References

1. Eric Ma, "Deep Learning Fundamentals: Forward Model, Differentiable Loss Function & Optimization," SciPy 2019 tutorial. [video on YouTube](https://youtu.be/JPBz7-UCqRo) and [archive on GitHub](https://github.com/ericmjl/dl-workshop/releases/tag/scipy2019).


```python
# Execute this cell to load the notebook's style sheet, then ignore it
from IPython.core.display import HTML
css_file = '../style/custom.css'
HTML(open(css_file, "r").read())
```




<link href="https://fonts.googleapis.com/css?family=Merriweather:300,300i,400,400i,700,700i,900,900i" rel='stylesheet' >
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,700,700i" rel='stylesheet' >
<link href='https://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' >
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}


#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 800px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.5em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running { 
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256); 
    border-radius: 0px; 
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}


div.text_cell_render{
    font-family: 'Source Sans Pro', sans-serif;
    line-height: 140%;
    font-size: 110%;
    width:680px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Merriweather', serif;
    font-style:regular;
    font-weight: bold;    
    font-size: 250%;
    line-height: 100%;
    color: #004065;
    margin-bottom: 1em;
    margin-top: 0.5em;
    display: block;
}	
.text_cell_render h2 {
    font-family: 'Merriweather', serif;
    font-weight: bold; 
    font-size: 180%;
    line-height: 100%;
    color: #0096d6;
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}	

.text_cell_render h3 {
    font-family: 'Merriweather', serif;
	font-size: 150%;
    margin-top:12px;
    margin-bottom: 3px;
    font-style: regular;
    color: #008367;
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Merriweather', serif;
    font-weight: 300; 
    font-size: 100%;
    line-height: 120%;
    text-align: left;
    width:500px;
    margin-top: 1em;
    margin-bottom: 2em;
    margin-left: 80pt;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Source Sans Pro', sans-serif;
    font-weight: regular;
    font-size: 130%;
    color: #e31937;
    font-style: italic;
    margin-bottom: .5em;
    margin-top: 1em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'Source Code Pro', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

    .CodeMirror{
            font-family: "Source Code Pro";
			font-size: 90%;
    }
/*    .prompt{
        display: None;
    }*/


    .warning{
        color: rgb( 240, 20, 20 )
        }  
</style>





4cb54c710a3ed70d96a30c895d4198790fe123e3
ipynb
Jupyter Notebook
notebooks_en/1_Linear_Regression.ipynb
tingyu66/EngComp6_deeplearning
8f76ef44050625abe041583ae1989b86ff5a717b
BSD-3-Clause
2021-06-24T14:26:41.000Z
2021-11-11T21:43:19.000Z
notebooks_en/1_Linear_Regression.ipynb
tingyu66/EngComp6_deeplearning
8f76ef44050625abe041583ae1989b86ff5a717b
BSD-3-Clause
2021-07-26T15:52:18.000Z
2021-08-06T18:53:16.000Z
notebooks_en/1_Linear_Regression.ipynb
tingyu66/EngComp6_deeplearning
8f76ef44050625abe041583ae1989b86ff5a717b
BSD-3-Clause
2021-07-19T21:57:06.000Z
2021-09-12T21:44:33.000Z
Qwen/Qwen-72B
1. YES
2. YES