## DW mid-terms 2018 solutions

### These solutions are student compiled and might contain errors (especially for qn 1 & 2)
 Credit goes to Team Anonymous on Piazza

 Part A

```python
x = 'aces'
y = 1.23
z = x
x = y
y = z
print(x, y)
```

### Q1(a) After the above code is executed, at line 6, what is seen on the screen is 1.23aces.

By explaining what each line of the code below (for lines 1 to 5) does, show how the code below switches the objects assigned to variables x and y.

Your explanation must state when objects are created and their data types, and also how names are assigned to these objects. Diagrams could be helpful in your explanation. (6 points)

**Sample answer 1:**

At line 1, string object 'aces' is assigned to frame 'x'. At line 2, float object of value 1.23 is assigned to frame 'y'. At line 3, a shallow copy of string object 'aces' from frame 'x' is assigned to frame 'z'. At line 4, a shallow copy of the float object of value 1.23 from frame 'y' is assigned to frame 'x' and overwirtes the string object 'aces'. At line 5, a shallow copy of string object 'aces' from frame 'z' is assigned to frame 'y' and overwrites the float object of value 1.23. The final result is:

Stack | Object after line 3 > 4 > 5

x	|  'aces' > 1.23 > 1.23

y	|  1.23 > 1.23 > 'aces'

z	|  'aces' > 'aces' > 'aces'

The contents of both x & y are fetched when print(x,y) is executed at line 6. Thus, 1.23aces is printed in the order of x then y.

### Q1(b) When the following code is executed, after line 12, what is seen on the screen is  True.

   i) Using how names are assigned to objects in memory, explain why. Diagrams could be helpful in your explanation. (3 points)

ii) The intention of the programmer is to create two lists showing the words for ‘seven’, ‘eight’ and ‘nine’ for both Greek and French. State one modification to the code at line 8 so that line 13 prints out the correct output. (1point)

```python
french= [ 'sept', 'huit', 'neuf'] # the words mean 'seven','eight','nine'.
greek = french # this is line 8
greek[0] = 'epta' # 'seven'
greek[1] = 'okto' # 'eight'
greek[2] = 'enea' # 'nine'
print(greek is french) # this is 'line 12'
print(french, greek)
```

**Sample answer 1:**

i) At line 1, a list object is created for frame 'french'. At line 2, frame 'greek' is assigned to refer to the same list object as frame 'french'. Thus, even after the contents of the list is changed, line 12's shallow equivalence check 'is' returns true as both frames 'french' and 'greek' still refers to the same list object in memory.

ii) I would repace line 8 with 'greek = list(french)' to another list object similar in contents to frame 'french' is created for frame 'greek'.

### Q2 [10 points]
### a) Is there anything wrong with the following program? If yes, what is wrong?  (2 points)

```python
import = int(input("Enter a number: "))
if import==2:
    print("Yes")
else:
    print("No")
```

**Sample solution 1:**

Synthax error. 'import' is a reserved keyword in python and neither be used as a variable name nor be assigned to an 'int' type object.

### b) If break is removed from the following programand the program is run, what will be printed out? (2 points)

```python
my_string = "Computing"
for character in my_string:
    print(character)
    if character == "u":
        print("Found 'u' :)")
```

**Solution:**

C

o

m

p

u

Found 'u' :)

t

i

n

g

### c) Look at the following function:

```python
def my_function(n):
    return_value = None
    if n == 0 or n == 1:
        return_value = False # not run
    i=2
    while i*0.5:
        if n%i==0:
            return_value = False # not run
            break # not run
        i += 1
        return_value = True
    return return_value
my_function(37)
```

### Let the function tested to be my_function(37)

a) What will be the output? (1 point)

b) Identify the lines of the program which will be executed when the input is 37. Do this by entering the codes from those lines to eDimension. (2 points)

**Solution:**

a) True.

b) (enter those lines of code not tagged by # not run)

### d) In the context of the 1D projects you have completed so far in this course, look at the following function:

```python
def forward(speed, duration):
    robot.wheels(speed, speed)
    robot.sleep(duration)
    robot.wheels(0,0)
```

### a) Why is it not necessary to have a return statement in this function? (1 point)

b) If we change  robot.wheels(speed, speed) to robot.wheels(speed1, speed2) and the function header is also modified to take in speed1 and speed2, how will the movement of the robot change? You can assume that speed1 and speed2 are different and both speed1 and speed2 are positive numbers. (1 point)

**Sample solution 1:**

a) In the above function to move the robot forward at some speed for some duration, no values are expected to be reused or caught by the function. Hence, no return statement is required.

b) If speed1 > speed2, the robot will still travel forward but with a leftwards dispalcement. If speed1 < speed2, the robot will still travel forward but with a rightwards dispalcement.

### Part B

 Q3 [10 points]

 A frustum is a parallel truncation of a right pyramid. A piece of metal is in the shape of a frustum with a square base. The side length of the top square is s1 and the side length of the bottom square is s2. The height of the frustum is H.

The volume of the frustum is given by the formula:
 $$Volume = \frac{H}{3}(A1 + A2 + \sqrt{A1 \text{ x } A2})$$ where A1 is the area of the upper square, A2 is the area of the lower square, and H is the height of the frustum.

### a) Write a python function area_square(s) that takes the side of a square as an input argument s, and returns the area of the square.

b) Write a python function vol_frustum(top_area, bottom_area, height) that takes three arguments, a top area, a bottom area and a height in that order, and returns the volume of the frustum.

c) Write a python function get_volume(s1, s2, height) that takes three arguments, a top side length, a bottom side length and a height and returns the volume of a frustum based on those dimensions. This function should first call area_square to obtain the two needed areas, and then call vol_frustum to evaluate the volume.

All input arguments and return values are floats. Please round only your final output of get_volume to three decimal places. Please use math.sqrt() to compute the square root in your python code. Note that you get only full marks if your get_volume function makes use of the other two functions.

```python
import math
def area_square(s):
    return s**2.

def vol_frustum(top_area, bottom_area, height):
    return (height/3)*(top_area + bottom_area + math.sqrt(top_area*bottom_area))

def get_volume(s1, s2, height):
    return round(vol_frustum(area_square(s1), area_square(s2), height), 3)

## TEST CASES ##
print('{:.3f}'.format(area_square(2)))
print('{:.3f}'.format(area_square(3)))
print('{:.3f}'.format(vol_frustum(1,4,2)))
print('{:.3f}'.format(vol_frustum(2,2,2)))
print('{:.3f}'.format(get_volume(1,2,2)))
print('{:.3f}'.format(get_volume(1.5,3.3,5.0)))
print('{:.3f}'.format(get_volume(3.6,6.4,4.0)))
```

### Q4 [10 points]

Implement a function determinant(matrix) that takes a matrix as input (represented as a nested list) and returns its determinant as output. The function should satisfy the following requirements:

1) If the input matrixis not of dimension n x n (for 1 ≤n ≤3), the function should return None

 2) The function is to be implemented withoutimporting any libraries.

```python
def determinant(matrix):
    M = matrix
    try:
        a = len(M)
        a = len(M[0])
    except:
        return None

    if len(M) == 1:
        for row in M:
            if len(row) != 1:
                return None
        return M[0][0]
    if len(M) == 2:
        for row in M:
            if len(row) != 2:
                return None
        return M[0][0] * M[1][1] - M[0][1]*M[1][0]
    if len(M) == 3:
        for row in M:
            if len(row) != 3:
                return None
        return (M[0][0] * M[1][1] * M[2][2]
                + M[1][0] * M[2][1] * M[0][2]
                + M[2][0] * M[0][1] * M[1][2]
                - M[0][2] * M[1][1] * M[2][0]
                - M[1][0] * M[0][1] * M[2][2]
                - M[0][0] * M[2][1] * M[1][2])

## TEST CASES ##
print(determinant([[100]]))
print(determinant([[-5, -4],[-2, -3]]))
print(determinant([[2, -3, 1], [2, 0, -1],[1, 4, 5]]))
print(determinant([[0, 3, 5],[5, 5, 2],[3, 4, 3]]))
print(determinant([[23], [-4, 4]]))
```

### Q5 [15 points]

The Newton-Raphson (NR) method is an iterative method that approximates the root of a function. The accuracy of the answer is enhanced in successive iterations.

You need two create two functions: nrootand nroot_complex. The function nroot(n, i, num) is to determine the root of non-negative num. The function nroot_complex(n,i,num) to determine the root of negative num. Thefunction nroot_complex should call nroot to do the NR approximation. Note the output should give a constant$*$1j where j is the imaginary square root of -1. For odd n the output should give a negative value instead of constant$*$1j. This means that:

• When num is a non-negative number, nroot_complex should give the same result as nroot.
• When num is a negative number and n is even, nroot_complex should give a complex number with no real part, and its magnitude is the same as the output of nrootwhen num is positive.
• When numis a negative number and n is odd, nroot_complex should give a negative real number, and its magnitude is the same as the output of nroot when numis positive.

Round the output of nroot to 3 decimal places.Use x = 1 as your initial value.

```python
def nroot(n,t,num):
    x = 1
    for i in range(t):
        x -= ((x**n - num)/(n*(x**(n-1))))
    return round(x,3)

def nroot_complex(n,t,num):
    if num > 0 or num == 0:
        return nroot(n,t,num)
    if num < 0 and n%2 == 1:
        return -nroot(n,t,-num)
    else:
        return str(nroot(n,t,-num)) + 'j'

## TEST CASES ##
print(nroot(2,5,2))
print(nroot_complex(2,5,-4))
print(nroot_complex(3,5,-8))
```

### Q6 [30 points]

In this problem you will write a program to find a path through MRT stations from a starting station to an ending station with a maximum of one interchange.

 The overall function is called find_path and it takes three arguments: (1) a file object to a text file containing the MRT lines and stations, (2) the starting station, and (3) the ending station.

This function should return a list of stations from a starting station to an ending station with a maximum of one interchange. The problem is decomposed by writing several other functions, described in the following parts.

 For simplicity, the information given to you in this question is limited to the North South line and the East West line. Also, the branch line to Changi Airport is treated as a separate line. Hence the three lines are labelled in this question as follows: (1) NorthSouthLine (2) EastWestLine (EW) and (3) EastWestLine (CG).

a) read_stations(f): This function takes in a file object and returnsa dictionary. The dictionary has the MRT lines as its keys. The value of each key is a list of stations in that MRT line.

b) get_stationline(mrt): This function takes in a dictionary of MRT lines (i.e. the output of part (a) ). The function returns another dictionary which contains all the stations as its keys. The value for each key is a list of the MRT lines for that particular station. Note that if the station is an interchange, the list should contain all the lines calling at that station.

c) get_interchange(stationline): This function takes in a dictionary of stations and their lines (i.e. the output of part (b)). The function returns another dictionary which contains all the interchange stations as its keys. The value for each key is a list of the MRT lines for that particular interchange stations.

d) find_path(f, start, end): This function takes in three arguments: (1) the file object to a text file containing all the MRT lines and stations, (2) the startingstation, and (3) the endingstation. The function should return a list of stations starting from the starting station to the ending station with a maximum of one interchange.If there are more than one possible paths, the following considerations should be taken into account:

• If there is a path without changing MRT lines, the result should return this path.
• If the path must involve changing MRT lines, it will return the path with a minimum number of stations and containing only one interchange station.
• If no such path can be found as above, it will return None.

```python
## PART A ##
def read_stations(s):
    st = ''.join(s.readlines()).split("=")
    ans = {}
    for i in range(int((len(st)-1)/2)):
        ans[st[2*i+1]] = (st[2*(i+1)].strip("\n")).split(", ")
    return ans

## PART B ##
def get_stationline(mrt):
    ans = {}
    for lines in mrt:
        for station in mrt[lines]:
            if station not in ans:
                ans[station] = []
                ans[station].append(lines)
            else:
                ans[station].append(lines)
    return ans

## PART C ##
def get_interchange(stationline):
    ans = {}
    for stations in stationline:
        if len(stationline[stations]) > 1:
            ans[stations] = stationline[stations]
    return ans

## PART D ##
def create_graph(f):
    stations = read_stations(f)
    stationline = get_stationline(stations)
    interchange = [*get_interchange(stationline)]
    network = {}
    # Create a dictionary where station (key) is linked to all its connected stations (values)
    for line in stations:
        stations_ = stations.get(line)
        network[stations_[-1]]=[stations_[-2]]  # End stations only conencted to one other station
        for station in range(0,len(stations_)-1):
            network.setdefault(stations_[station],[stations_[station-1]]).append(stations_[station+1])
        if stations_[0] not in interchange:
             network[stations_[0]]=[stations_[1]] # Removing non interchange connections that loops
    network['City Hall'].append('Dhoby Ghaut')   # Add this pesky back edge
    for key in network:
        network[key] = set(network.get(key)) # Apply set structure to dictionary values
    return network

def bfs_paths(graph, start, goal):
    # Since we only want the shortest path, we use Breath First Search on a queue structure for efficiency
    queue = [(start, [start])]
    while queue:
        (vertex, path) = queue.pop(0)
        for next in graph[vertex] - set(path):
            if next == goal:
                yield path + [next]
            else:
                queue.append((next, path + [next]))

import collections as c
def find_path(f, start, end):
   try:
    graph = create_graph(f)
    possible_paths = list(bfs_paths(graph,start,end))
    f.seek(0) # remember to reset readlines() counter to 0
    stations = read_stations(f)
    stationline = get_stationline(stations)
    interchange = [*get_interchange(stationline)]
    ans = []
    for path in possible_paths:
        line_counter = []
        for station in path:
            line_counter.append(stationline.get(station)[0])
        # We count the total number of line types present. More than 1 interchange used if line type > 2
        if len(c.Counter(line_counter)) <= 2:
                ans.append(path)
    return ans[0] # Since we used BFS, first path found is always the shortest
   except:
    # A general catch block to return None
    return None

## TEST CASES ##
print('Test 1')
f=open('mrt_lines_short.txt','r') # Make sure directory is correct
ans=find_path(f,'Boon Lay', 'Clementi')
print(ans)
f.close()
print('Test 2')
f=open('mrt_lines_short.txt','r') # Make sure directory is correct
ans=find_path(f,'Changi Airport', 'Orchard')
print(ans)
f.close()
print('Test 3')
f=open('mrt_lines_short.txt','r') # Make sure directory is correct
ans=find_path(f,'Boon Lay', 'Bukit Gombak')
print(ans)
f.close()
print('Test 4')
f=open('mrt_lines_short.txt','r') # Make sure directory is correct
ans=find_path(f,'Tanah Merah', 'Orchard')
print(ans)
f.close()
```

### Q7 [15 points]

 Write a function decompose(pence), that takes as input some number of pence (as an integer), and returns as output an integer expressing how many different ways that the amount can be made up by using the available coins. At present, there are eight coins in general circulation:

1p, 2p, 5p, 10p, 20p, 50p, £1, and £2

Note that the function decompose(pence) can be implemented in a number of different ways, including by using brute force (i.e. exhaustive search). However, brute force implementations may only score a maximum of 12 points; the full 15 are only available for more elegant/efficient solutions.

**Sample Solution 1: Naive for loops (Brute force; Don't even try to time it)**

```python
def decompose(pence):
    coins = [1,2,5,10,20,50,100,200]
    count = 0
    for x1 in range(pence):
        for x2 in range(pence):
            for x3 in range(pence):
                for x4 in range(pence):
                    for x5 in range(pence):
                        for x6 in range(pence):
                            for x7 in range(pence):
                                for x8 in range(pence):
                                    if (x1 * coins[0] + x2 * coins[1] + x3 * coins[2] + x4 * coins[3]
                                     + x5 * coins[4] + x6 * coins[5] + x7 * coins[6] + x8 * coins[7])
                                      == pence:
                                        count += 1 # Cancer

    return count + 1

## TEST CASES ##
import time
start_time = time.time()
print (decompose(1))
print (decompose(5))
print (decompose(7))
print (decompose(130))
print (decompose(200))
print (decompose(700))
print("--- %s seconds ---" % (time.time() - start_time))
```

**Sample Solution 2: Pure recursion (Exhasutive; ~10mins)**

```python
def decompose(pence, num_types = len(coins)):
    coins = [1,2,5,10,20,50,100,200]
    # If pence = 0 then there is only 1 solution
    if (pence == 0):
        return 1

    # If n is less than 0 then no solution exists
    if (pence < 0):
        return 0;

    # If there are no coins and n is greater than 0, then no solution exist
    if (num_types <=0 and pence >= 1):
        return 0

    # Recursion step
    return decompose( pence, num_types - 1 ) + decompose( pence - coins[num_types-1], num_types);

## TEST CASES ##
import time
start_time = time.time()
print (decompose(1))
print (decompose(5))
print (decompose(7))
print (decompose(130))
print (decompose(200))
print (decompose(700))
print("--- %s seconds ---" % (time.time() - start_time))
```

**Sample Solution 3: Recursion with some memomisation (Exhasutive with some elegance; ~5mins)**

```python
def decompose(pence, coins = [1,2,5,10,20,50,100,200]):

    # If pence = 0 then there is only 1 solution
    if pence == 0:
        return 1

    # If n is less than 0 then no solution exists
    if pence < 0:
        return 0
    num_ways = 0

    # Store previously computed sub-problems in a dictionary to avoid re-computing it
    dic_ways = {}
    for i in range(len(coins)):
        coin = coins[i]
        if pence-coin not in dic_ways:
            # Recursion step
            num_ways += decompose(pence - coin, coins[i:])
            dic_ways[pence-coin] = True
    return num_ways

## TEST CASES ##
import time
start_time = time.time()
print (decompose(1))
print (decompose(5))
print (decompose(7))
print (decompose(130))
print (decompose(200))
print (decompose(700))
print("--- %s seconds ---" % (time.time() - start_time))
```

**Sample solution 4: Expansion of partition equation into series (Elegant but inefficient; ~30s)**

```python
from sympy import *
def decompose(pence):
    x = symbols('x')
    partition_series = series(1/(( 1 - x)*(1-x**2)*(1-x**5)*(1-x**10)
    *(1-x**20)*(1-x**50)*(1-x**100)*(1-x**200)), n = pence+2)
    coef = Poly(partition_series.removeO(),x)
    return coef.all_coeffs()[1]

## TEST CASES ##
import time
start_time = time.time()
print (decompose(1))
print (decompose(5))
print (decompose(7))
print (decompose(130))
print (decompose(200))
print (decompose(700))
print("--- %s seconds ---" % (time.time() - start_time))
```

**Sample solution 5: Smart brute force (Efficent but inelegant; 1.5s)**

```python
def decompose(pence):
    count = 0
    for x1 in range(0,pence+1,200):
        for x2 in range(x1,pence+1,100):
            for x3 in range(x2,pence+1,50):
                for x4 in range(x3,pence+1,20):
                    for x5 in range(x4,pence+1,10):
                        for x6 in range(x5,pence+1,5):
                            for x7 in range(x6,pence+1,2):
                                count+=1
    return count

## TEST CASES ##
import time
start_time = time.time()
print (decompose(1))
print (decompose(5))
print (decompose(7))
print (decompose(130))
print (decompose(200))
print (decompose(700))
print("--- %s seconds ---" % (time.time() - start_time))
```

**Sample solution 6: Top-down Dynamic Programming; Recursion with memoization & decorators (Efficient & quite elegant; ~0.002s)**

```python
def memoize(func):
    cache = dict()
    def memoized_func(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    return memoized_func
 ## The above code is a quick and dirty memo fucntion that can be used widely ##
 ## to speed up problems with overlapping sub-problems (avoid recomputation) ##

@memoize
## Pure recursion from sample solution 2 ##
def decompose(pence, num_types = len(coins)):
    coins = [1,2,5,10,20,50,100,200]
    # If pence = 0 then there is only 1 solution
    if (pence == 0):
        return 1

    # If n is less than 0 then no solution exists
    if (pence < 0):
        return 0;

    # If there are no coins and n is greater than 0, then no solution exist
    if (num_types <=0 and pence >= 1):
        return 0

    # Recursion step
    return decompose( pence, num_types - 1 ) + decompose( pence - coins[num_types-1], num_types);

## TEST CASES ##
import time
start_time = time.time()
print (decompose(1))
print (decompose(5))
print (decompose(7))
print (decompose(130))
print (decompose(200))
print (decompose(700))
print("--- %s seconds ---" % (time.time() - start_time))
```

**Sample Solution 7: Bottom-up Dynamic Programming (Most elegant & efficient; ~0.001s)**

```python
def decompose(pence):
   try:
    coins = [1,2,5,10,20,50,100,200]
    num_types = len(coins)
    # table[i] will be storing the number of solutions for
    # value i. We need n+1 rows as the table is constructed
    # in bottom up manner using the base case (pence = 0)
    # We first initialize all table values as 0
    table = [0 for accumulative_num_of_ways in range(pence+1)]

    # If pence = 0 then there is only 1 solution
    table[0] = 1

    # Pick all coins one by one and update the table[] values
    # after the index greater than or equal to the value of the
    # picked coin
    for type_ in range(0,num_types):
        for value in range(coins[type_],pence+1):
            table[value] += table[value-coins[type_]]
    # We only want the number of ways to find change for value = pence
    return table[pence]
   except:
    # Our bottom up approach innately deals with special cases
    # this line catches invalid arguments
    return 0

## TEST CASES ##
import time
start_time = time.time()
print (decompose(1))
print (decompose(5))
print (decompose(7))
print (decompose(130))
print (decompose(200))
print (decompose(700))
print("--- %s seconds ---" % (time.time() - start_time))
```

```python

```

```python

```


# Why Python

For this comparison I'm going to assume most of you are primarily Matlab users.  Matlab is great, especially in a university environment.  It's an easy to use, interpreted, high-level language with automatic memory management and lots of supporting libraries.  Python shares those same advantages.  What's the difference then?  Python has some benefits that may become more important to you as your problems get larger, or as you move into industry.  Some of these benefits include:

1. It's free.  Matlab is cheap as a student, but it's definitely not cheap once you are out of the university setting.
2. It's open source.  Often as you advance in your research you will need to dive into the details of an algorithm and may need to make some modifications.  That's not possible with most of Matlab's libraries.
3. Performance.  Matlab is not fast.  To be clear, Python is also an interpreted language so it isn't fast either.  But with Python it is easy to wrap C and Fortran code.  In fact that is one of its primary uses in scientific omputing.  It acts as a "glue" code between different codes.  We've used it to connect a CFD code in C++, blade design tools in Fortran 95, a structural FEM code in C++, a cost tool in C, other cost tools in pure Python, and an optimizer in Fortran 77, etc.   The workhorse code remains in a compiled langauge, but you can interact with and connect the tools in a simple, concise, scriptable language.  You get both speed and ease of use!  Matlab does have mex files, but that approach is much more complicated. A common workflow with Python is to write everything in pure Python, then once tested, profile and move bottlenecks in the code to C or Fortran if necessary.
4. Parallelization.  This is related to 2.  Matlab does support parallel computing, but that requires an additional expensive license to the parallel computing toolbox, and is not as capable.  Similar comment for cloud computing.
5. Unlike Matlab, Python is a full featured programming langauge.  In additional to procedural style it also supports object oriented and functional styles.  (Matlab has some OO support, but it is very weak).  Python has dictionaries, package managment, modern string support, cloud computing, error handling, connections to web servers, etc.

Are there any drawbacks?  The only one I've come across is Simulink.  I don't know of anything of comparable capability in the Python world.  But, since this is a fluid dynamics conference, probably none of us is worried about that.

# Installation
- If you are on Linux or OS X you should already have Python.  Using the system version is fine for testing the waters, but if you are going to upgrade versions or start using different packages you should install your own version of Python rather than rely on the system version.
- There are lots of ways to install Python.  I recommend using [conda](http://conda.pydata.org/docs/index.html) by downloading Anaconda or Miniconda.  See [here](http://conda.pydata.org/docs/download.html) for helping deciding which one to select.

## Editing
- I just use a text editor, either [ST3](http://www.sublimetext.com/3) or [Atom](https://atom.io).  I like using a text editor for all my languages because I heavily rely on keyboard shortcuts.
- But for getting started, I would probably recommend using an IDE.  There are many.  [PyCharm](https://www.jetbrains.com/pycharm/) seems to be well-liked by my students.
- IPython notebooks (now [Jupyter notebooks](http://jupyter.org)), of which this notebook is an example, is another great option.  They are useful for combining code along with text (with LaTeX support).  Many people love using it.  I like it for demos like this, but don't like using it for development (again, because I like my keyboard shortcuts).  If you want to go the notebook route you should definitely download it locally, rather than using this remotely hosted server that we are using for the demo.

## Useful Packages

Some packages you may want to install include (you will definitely need the first three):

- NumPy: array methods
- SciPy: integration, root finding, optimization, linear algebra, etc.
- Matplotlib: plotting package (also see Bokeh, Plotly, etc.)
- IPython: interactive shells and notebook
- pandas: data structure and data analysis tools (like R)
- scikit-learn: machine learning
- many others...

## Tutorials

Many exist, and I'm not familiar enough with them all to recommend one over another.  I like the one in the official [Python docs](https://docs.python.org/2/tutorial/).  It's probably more detailed than you want for a first exposure, but it is a good resource.
## Matlab Users

Here are two useful resources called NumPy for Matlab Users: [one](https://docs.scipy.org/doc/numpy-dev/user/numpy-for-matlab-users.html), [two](http://mathesaurus.sourceforge.net/matlab-numpy.html).

# Examples

Today we will go through two simple examples to introduce you to the syntax and to show how to wrap an external Fortran/C code.  If you are viewing this in nbviewer I suggest you download it first (button in upper right corner), then open it up at <https://try.jupyter.org> so you can edit and follow along.  Later you can install IPython so you can make edits locally rather than on a remote server.  First, evaluate the cell below.  Press shift-enter to evaluate a cell.  This cell imports some libraries we will need.  The first line is only for IPython (you wouldn't use it in a normal Python script).  It just tells IPython that we want to see plots inline.  The next line imports numpy, which contains a lot of important array operations.  The last imports matplotlib, which is a plotting package.

```python
%matplotlib inline

import numpy as np
import matplotlib.pyplot as plt
```

## Simple UAV drag curves

We are going to compute induced and parasite drag (in a very basic way) as an example to introduce scripting.
\begin{align}
q&= \frac{1}{2} \rho V_\infty^2 \\
D_i &= \frac{L^2}{q \pi b^2 e} \\
D_p &= {C_D}_p q S
\end{align}

The first few lines make some imports.  The first line is only for Jupyter notebooks.  It just tells the notebook to show plots inline.  You wouldn't need that in a normal python script.

```python
from math import pi

# all in standard English units

# atmosphere
rho = 0.0024  # air density

# geometry
b = 8.0  # wing span
chord = 1.0

# mass properties
W = 2.4  # total weight of aircraft

# other parameters
e = 0.9  # Oswald efficiency factor
CDp = 0.02  # could compute but just input for simplicity

# an array of wind speeds
V = np.linspace(10, 30, 100)

# Induced drag
q = 0.5*rho*V**2  # dyamic pressure
L = W  # equilibrium flight
Di = L**2/(q*pi*b**2*e)

# parasite drag
S = b*chord
Dp = CDp*q*S

# these next 3 lines purely for style in the plot (loading a predefined styleshet)
# I have my own custom styles I use, but for this example let's use one of matplotlib's
plt.style.use('ggplot')
plt.rcParams.update({'font.size': 16})
colors = plt.rcParams['axes.color_cycle']  # grab the current color scheme

# plot it
plt.figure()
plt.plot(V, Di)
plt.plot(V, Dp)
plt.plot(V, Di+Dp)
plt.xlabel('V (ft/s)')
plt.ylabel('Drag (lbs)')

# label the plots
plt.text(25, 0.06, 'induced drag', color=colors[0])
plt.text(12, 0.06, 'parasite drag', color=colors[1])
plt.text(20, 0.17, 'total drag', color=colors[2])
```

Try it yourself.  Let's do the same calculation, but with reusable functions.  In Python functions are easy to define.  A simple example is below (note that unlike Matlab, you can have as many functions as you want in a file.)

```python
def func(x, y):
    add = x + y
    mult = x * y
    return add, mult

a, m = func(1.0, 3.0)
print 'a =', a, 'm =', m

a, m = func(2.0, 7.0)
print 'a =', a, 'm =', m
```

    a = 4.0 m = 3.0
    a = 9.0 m = 14.0

```python
def induced_drag():
    pass

def parasite_drag():
    pass

# atmosphere
rho = 0.0024  # air density

# geometry
b = 8.0  # wing span
chord = 1.0

# mass properties
W = 2.4  # total weight of aircraft

# other parameters
e = 0.9  # Oswald efficiency factor
CDp = 0.02  # could compute but just input for simplicity

# wind speeds
V = np.linspace(10, 30, 100)
```

Finally, let's do it once more, but in an object-oriented style.

```python

class UAV(object):

    def __init__(self, b, chord, W, rho):
        self.b = b
        self.S = b*chord
        self.L = W
        self.rho = rho

    def induced_drag(self, V, e):
        q = 0.5*self.rho*V**2
        Di = self.L**2/(q*pi*self.b**2*e)
        return Di

    def parasite_drag(self, V, CDp):
        q = 0.5*self.rho*V**2
        Dp = CDp*q*self.S
        return Dp

# atmosphere
rho = 0.0024  # air density

# geometry
b = 8.0  # wing span
chord = 1.0

# mass properties
W = 2.4  # total weight of aircraft

# setup UAV object
uav = UAV(b, chord, W, rho)

# setup sweep
V = np.linspace(10, 30, 100)

# idrag
e = 0.9  # Oswald efficiency factor
Di = uav.induced_drag(V, e)

# pdrag
CDp = 0.02
Dp = uav.parasite_drag(V, CDp)

# style
plt.style.use('fivethirtyeight')

# plot it
plt.figure()
plt.plot(V, Di)
plt.plot(V, Dp)
plt.plot(V, Di+Dp)
plt.xlabel('V (ft/s)')
plt.ylabel('Drag (lbs)')

# label the plots
colors = plt.rcParams['axes.color_cycle']
plt.text(25, 0.06, 'induced drag', color=colors[0])
plt.text(12, 0.06, 'parasite drag', color=colors[1])
plt.text(20, 0.17, 'total drag', color=colors[2])

```

## Wrapping Fortran/C

This next example is going to solve Laplace's equation on a grid.  First, we will do it in pure Python, then we will rewrite a portion of the code in Fortran and call it in Python for improved speed.  Recall Laplace's equation:

$$ \nabla^2 \phi = 0 $$

where $\phi is some scalar.  For a regular rectangular grid, with equal spacing in x and y, you might recall that a simple iterative method for solving this equation consists of the following update rule:

$$ \phi_{i, j} = \frac{1}{4} (\phi_{i+1, j} + \phi_{i-1, j} + \phi_{i, j+1} + \phi_{i, j-1})$$

In other words, each cell updates its value using the average value of all of its neighbors (note that they are much more efficient ways to solve Laplace's equation on a grid.  For our purpose we just want to keep things simple).  This process must be repeated for every cell in the domain, and repeated until converged.

We are going to run a simple case where boundary values are provided at the top, bottom, left, and right edges.  You should iterate until the maximum change in $\phi$ is below some tolerance (tol) or until a maximum number of iterations is reached (iter_max).  n is the number of cells (same discretization in x and y).

I've started a script for you below.  See if you can fill in the details.  I've not provided all the syntax you will need to know so you may have to look some things up.  A full implementation is down below, but don't peek unless you are really stuck!

```python
from math import fabs

def laplace_grid_python(n, top, bottom, left, right, tol, iter_max):

    # initialize
    phi = np.zeros((n+1, n+1))
    iters = 0  # number of iterations
    err_max = 1e6  # maximum error in grid (start at some arbitrary number just to enter loop)

    # set boundary conditions

    # run while loop until tolerance reached or max iterations
    while ():

        # reset the maximum error to something small (I suggest something like -1)
        err_max = -1.0

        # loop over all *interior* cells
        for i in range():
            for j in range():

                # save previous point for computing error later
                phi_prev = phi[i, j]

                # update point
                phi[i, j] =

                # update maximum error
                err_max =

        # update iteration count
        iters += 1

    return phi, err_max, iters

# run a sample case (50 x 50 grid with bottom and left and 1.0, top and right at 0.0)
n = 50
top = 0.0
bottom = 1.0
left = 1.0
right = 0.0
tol = 1e-5
iter_max = 10000

phi, err_max, iters = laplace_grid_python(n, top, bottom, left, right, tol, iter_max)

# plot it
x = np.linspace(0, 1, n+1)
y = np.linspace(0, 1, n+1)
[X, Y] = np.meshgrid(x, y)

plt.figure()
plt.contourf(X, Y, phi, 100, cmap=plt.cm.get_cmap("YlGnBu"))
plt.colorbar()
plt.show()
```

In our implementation below we are adding the IPython flag %%timeit at the top.  This will run the whole block some number of times and report the best time back.

Adding some blank space below just to visually separate the answer.

```python
%%timeit
from math import fabs

def laplace_grid_python(n, top, bottom, left, right, tol, iter_max):

    # initialize
    phi = np.zeros((n+1, n+1))
    iters = 0  # number of iterations
    err_max = 1e6  # maximum error in grid (start at some arbitrary number just to enter loop)

    # set boundary conditions
    phi[0, :] = bottom
    phi[-1, :] = top
    phi[:, 0] = left
    phi[:, -1] = right

    # run while loop until tolerance reached or max iterations
    while (err_max > tol and iters < iter_max):

        # reset the maximum error to something small (I suggest something like -1)
        err_max = -1.0

        # loop over all interior cells
        for i in range(1, n):
            for j in range(1, n):

                # save previous point
                phi_prev = phi[i, j]

                # update point
                phi[i, j] = (phi[i-1,j] + phi[i+1,j] + phi[i,j-1] + phi[i,j+1])/4.0

                # update maximum error
                err_max = max(err_max, fabs(phi[i, j] - phi_prev))

        # update iteration count
        iters += 1

    return phi, err_max, iters

# run a sample case (50 x 50 grid with bottom and left and 1.0, top and right at 0.0)
n = 50
top = 0.0
bottom = 1.0
left = 1.0
right = 0.0
tol = 1e-5
iter_max = 10000

phi, err_max, iters = laplace_grid_python(n, top, bottom, left, right, tol, iter_max)

# plot it
x = np.linspace(0, 1, n+1)
y = np.linspace(0, 1, n+1)
[X, Y] = np.meshgrid(x, y)

plt.figure()
plt.contourf(X, Y, phi, 100, cmap=plt.cm.get_cmap("YlGnBu"))
plt.colorbar()
plt.show()
```

This takes a while.  Let's move the double for loop computation to Fortran.  I've supplied a file called laplace.f90 where I've done this for you.  We just need to build this as a shared library so we can call it from Python.  Open a terminal (you can do this in try.jupyter.org as well).  We will compile the fortran code to a shared library with f2py, (can also use standard gfortran compilation but you get more flexibility and automatic setup with f2py).  In all of the below I am using an O2 optimization flag.  Note the underscore in the shared library name.  This is just convention.  Note that if you make a mistake in importing, IPython caches your modules so you'd need to restart the Kernel.  You can even do all of this <try.jupyter.org> by opening a terminal.

Using f2py

    f2py -c --opt=-O2 -m _laplace laplace.f90

Using setup script.  Open up a file and call it setup.py.  At a minimum this is all it needs:

    from numpy.distutils.core import setup, Extension
    setup(
        ext_modules=[Extension('_laplace', ['laplace.f90'], extra_compile_args=['-O2'])]
    )

Usually a lot more information would be added (name, license, other python packages, etc.)  You can read more about setuptools later.

To build it one normally just uses build or install commands, but we will built it inplace for testing.

    python setup.py build_ext --inplace

Now we can call it from Python just as a regular method.  An example is shown below doing the exact same thing as before, but calling the Fortran code in laplacegridfortran.  This runs over 10x faster (and could be even faster if we used ifort instead of gfortran).  How much faster the code is of course depends ont he problem as you change n the difference will either become more or less significant.

```python
%%timeit
from _laplace import laplacegridfortran

n = 50
top = 0.0
bottom = 1.0
left = 1.0
right = 0.0
tol = 1e-5
iter_max = 10000

phi, err_max, iters = laplacegridfortran(n, top, bottom, left, right, tol, iter_max)

# plot it
x = np.linspace(0, 1, n+1)
y = np.linspace(0, 1, n+1)
[X, Y] = np.meshgrid(x, y)

plt.figure()
plt.contourf(X, Y, phi, 100, cmap=plt.cm.get_cmap("YlGnBu"))
plt.colorbar()
plt.show()
```

```python

```


```python
from IPython.core.display import HTML
def css_styling():
    styles = open("./styles/custom.css", "r").read()
    return HTML(styles)
css_styling()
```

### BEFORE YOU DO ANYTHING...
In the terminal:
1. Navigate to __inside__ your ILAS_Python repository.
2. __COMMIT__ any un-commited work on your personal computer.
3. __PULL__ any changes *you* have made using another computer.
4. __PULL__ textbook updates (including homework answers).

1. __Open Jupyter notebook:__   Start >> Programs (すべてのプログラム) >> Programming >> Anaconda3 >> JupyterNotebook
1. __Navigate to the ILAS_Python folder__.
1. __Open Seminar 7___  by clicking on 7_Numerical_computation_with_Numpy.

Numerical Computation with Numpy

Lesson Goal

Compose programs to solve simple mathematical problems using the Python Numpy package.

## Objectives
- Represent data using the `array` data structure for numerical computation.
- Use 1D and 2D arrays to represent vectors and matrices.
- Manipulate arrays (indexing, slicing, vectorising etc)
- Perform familiar numerical operations using Python.
- Compare efficiency of vectorised and non-vectorised functions.

## Why are we studying this?
Numerical computation is central to almost all scientific and engineering problems.

There are programming languages specifically designed for numerical computation:
- Fortran
- MATLAB

There are libaries dedicated to efficient numerical computations:
- Numpy
- Scipy
- Sympy ...

NumPy (http://www.numpy.org/)
 - The most widely used Python library for numerical computations.
 - Large, extensive library of data structures and functions for numerical computation.
 - Useful for perfoming operation you will learn on mathematics-based courses.

Scipy (https://www.scipy.org/)
- Builds on Numpy, additional functionality
- More specialised data structures and functions over NumPy.

If you are familiar with MATLAB, NumPy and SciPy provide similar functionality.

Last week we covered an introduction to some basic functions of Numpy.

NumPy is a very extensisve library.

This seminar will:
- Introduce some useful functions
- Briefly discuss how to search for additional functions you may need.

your best resources are search engines, such as http://stackoverflow.com/.

## Importing the NumPy module

To make NumPy functions and variables available to use in our program in our programs, we need to __import__ it using.

`import numpy`

We typically import all modules at the start of a program or notebook.

```python
import numpy as np
```

The shortened name `np` is often used for numpy.

All Numpy functions can be called using `np.function()`.

## Data Structure: The Numpy `array`

### Why do we need another data structure?

Python lists hold 'arrays' of data.

Lists are very flexible. e.g. holding mixed data type.

There is a trade off between flexibility and performance e.g. speed.

Science engineering and mathematics problems often involve large amounts of data and numerous operations.

We therefore use specialised functions and data structures for numerical computation.

## Numpy array

A numpy array is a grid of values, *all of the same type*.

To create an array we use the Numpy `array` function.

It takes a list as an argument.

```python
a = np.array([1, 2, 3])

print(type(a))

print(a.dtype)
```

    <class 'numpy.ndarray'>
    int64

```python
b = [4.0, 5, 6.0]

c = np.array(b)

print(type(b))
print(type(c))
print(c.dtype)
```

    <class 'list'>
    <class 'numpy.ndarray'>
    float64

## Multi-dimensional arrays.

Unlike the data types we have studied so far, arrays can have multiple dimensions.

__`rank`:__ the number of *dimensons* of the array.

__`shape`:__ a *tuple* of *integers* giving the *size* of the array along each *dimension*.

```python
# 1-dimensional array
a = np.array([1, 2, 3])

# 2-dimensional array
b = np.array([[1, 2, 3], [4, 5, 6]])

b = np.array([[1, 2, 3],
              [4, 5, 6]])

print(a.shape)
print(b.shape)
```

    (3,)
    (2, 3)

```python
# 3-dimensional array

c = np.array(
    [[[1, 1],
      [1, 1]],

     [[1, 1],
      [1, 1]]])

print(c.shape)

c = np.array(
    [[[1, 1],
      [1, 1]],

     [[1, 1],
      [1, 1]],

     [[1, 1],
      [1, 1]]])

print(c.shape)
```

    (2, 2, 2)
    (3, 2, 2)

```python
# 3-dimensional array

c = np.array(
    [[[1, 1],
      [1, 1]],

     [[1, 1],
      [1, 1]]])

# 4-dimensional array
d = np.array(
    [[[[1, 1],
       [1, 1]],

      [[1, 1],
       [1, 1]]],

      [[[1, 1],
       [1, 1]],

      [[1, 1],
       [1, 1]]]])

print(c.shape)
print(d.shape)
```

    (2, 2, 2)
    (2, 2, 2, 2)

## Creating a numpy array.

There are several other ways we can create an array

```python
# Create an array of all zeros
# The zeros() function argument is the shape.
# Shape: tuple of integers giving the size along each dimension.

a = np.zeros(5)
print(a)

print()

a = np.zeros((2,2))
print(a)
```

    [ 0.  0.  0.  0.  0.]

    [[ 0.  0.]
     [ 0.  0.]]

```python
# Create an array of all ones

b = np.ones(5)
print(b)

print()

b = np.ones((1, 4))
print(b)
```

```python
# Create a constant array
# The second function argument is the constant value

c = np.full(6, 8)
print(c)

print()

c = np.full((2,2,2), 7)
print(c)

```

    [8 8 8 8 8 8]

    [[[7 7]
      [7 7]]

     [[7 7]
      [7 7]]]

## Subpackages
Packages can also have subpackages.

The `numpy` package has a subpackage called `random`.

It contains functions to deal with random variables.

If the `numpy` package is imported with `import numpy as np`, functions in the `random` subpackage can be called using `np.random.function()`.

```python
# Create an array filled with random values

e = np.random.rand(1)
print(e)
print()

e = np.random.rand(3,2,1)
print(e)
print()

e = np.random.random((2,2))
print(e)
```

    [ 0.1925726]

    [[[ 0.79990402]
      [ 0.63879192]]

     [[ 0.38519791]
      [ 0.0073061 ]]

     [[ 0.9293842 ]
      [ 0.77903214]]]

    [[ 0.09380194  0.32369848]
     [ 0.72059049  0.03877658]]

```python
# Create an array filled with random integrer values
```

```python
e = np.random.randint(16, size=(4,4))
print(e)

print()

e = np.random.randint(8, size=(2, 2, 2))
print(e)
```

## Indexing.

We can index into an array exactly the same way as the other data structures we have studied.

```python

```

```python
x = np.array([1, 2, 3, 4, 5])

# Select a single element
print(x[4])

# Select elements from 2 to the end
print(x[2:])
```

    5
    [3, 4, 5]

For an n-dimensional (nD) matrix we need n index values to address an element or range of elements.

Example: The index of a 2D array is specified with two values:
- first the row index
- then the column index.

Note the order in which dimensions are addressed.

```python
# 2 dimensional array

y = np.array([[1, 2, 3],
              [4, 5, 6]])

# Select a single element
print(y[1,2])

# Select elements that are both in rows 1 to the end AND columns 0 to 2
print(y[1:, 0:2])
```

    6
    [[4 5]]

We can address elements by selecting a range with a step by addig a

For example the index:

`z[0, 0:]`

selects every element of row 0 in array, `z`

The index:

`z[0, 0::2]`

selects every *other* element of row 0 in array, `z`

```python
# 2 dimensional array

z = np.zeros((4,8))

# Change every element of row 0
z[0, 0:] = 10

# Change every other element of row 1
z[1, 0::2] = 10

print(z)
```

    [[ 10.  10.  10.  10.  10.  10.  10.  10.]
     [ 10.   0.  10.   0.  10.   0.  10.   0.]
     [  0.   0.   0.   0.   0.   0.   0.   0.]
     [  0.   0.   0.   0.   0.   0.   0.   0.]]

```python
z = np.zeros((4,8))

# Change the last 4 elements of row 2, in negative direction
# You MUST include a step to count in the negative direction
z[2, -1:-5:-1] = 10

# Change every other element of the last 6 elements of row 3
# in negative direction
z[3, -2:-7:-2] = 10

print(z)
```

    [[  0.   0.   0.   0.   0.   0.   0.   0.]
     [  0.   0.   0.   0.   0.   0.   0.   0.]
     [  0.   0.   0.   0.  10.  10.  10.  10.]
     [  0.   0.  10.   0.  10.   0.  10.   0.]]

```python
# 3-dimensional array

c = np.array(
    [[[2, 1, 4],
      [2, 6, 8]],

     [[0, 1, 5],
      [7, 8, 9]]])

print(c[0, 1, 2])

```

    8

```python

```

```python

```

Where we want to select all elements in one dimension we can use :

__Exception__: If it is the last element , we can omit it.

```python
print(c[0, 1])

print(c[0, :, 1])
```

    [2 6 8]
    [1 6]

## Iterating over multi-dimensional arrays.
We can iterate over a 1D array in the same way as the data structures we have previously studied.

```python
A = np.array([1, 2, 3, 4, 5])
```

```python
for a in A:
    print(a)
```

    1
    2
    3
    4
    5

To loop through individual elements of a multi-dimensional array, we use a nested loop for each dimension of the array.

```python
B = np.array([[1, 2, 3],
              [4, 5, 6]])

for row in B:
    print("-----")
    for col in row:
        print(col)
```

    -----
    1
    2
    3
    -----
    4
    5
    6

## Manipulating arrays
We can use many of the same operations to manipulate arrays as we use for lists.

```python
# Length of an array

a = np.array([1, 3, 4, 17, 3, 21, 2, 12])

b = ([1, 3, 4, 17],
     [3, 21, 2, 12])

print(len(a))
print(len(b))

```

    8
    2

Note the length is the length of the first dimension (e.g. indexing).

```python
# Sort an array

a = np.array([1, 3, 4, 17, 3, 21, 2, 12])

# The method sort() and function sorted() give the same result
a.sort()

a = sorted(a)

print(a)
```

    [1, 2, 3, 3, 4, 12, 17, 21]

Arrays are *immutable* (unchangeable).

Technically you cannot delete an item from it.

However, you can make a new array without the values you don't want:

```python
# Remove items from an array

z = np.array([1, 3, 4, 17, 3, 21, 2, 12])

z = np.delete(z, 3)
print(z)

z = np.delete(z, [0, 1, 2])
print(z)

```

    [ 1  3  4  3 21  2 12]
    [ 3 21  2 12]

```python
# Add items to an array

a = ([1, 2, 3])
a = np.insert(a, 1, 4)
print(a)
```

    [1 4 2 3]

```python
# Add items to an array

b = np.array([[1, 1],
              [2, 2],
              [3, 3]])

b = np.insert(b, 1, 4)
print(b)
```

    [1 4 1 2 2 3 3]

```python
# Add items to an array

b = np.array([[1, 1],
              [2, 2],
              [3, 3]])

b = np.insert(b, 1, 4, axis=1)
print(b)
```

    [[1 4 1]
     [2 4 2]
     [3 4 3]]

```python
# Change items in an array

c = np.array([1, 2, 3])
c[1] = 4
print(c)
```

    [1 4 3]

### Boolean array indexing

Recall that we can use *conditional operators* to check the value of a single variable against a condition.

The value returned is a Boolean True or False value.

```python
a = 4
print('a < 2:', a < 2)
print('a > 2:', a > 2)
```

    a < 2: False
    a > 2: True

If we instead use *conditional operators* to check the value of an array against a condition.

The value returned is an *array* of Boolean True or False values.

```python
a = np.array([[1,2],
              [3, 4],
              [5, 6]])

idx = a > 2

print(idx)
```

    [[False False]
     [ True  True]
     [ True  True]]

A particular elements of an array can be are specified by using a boolean array as an index.

Only the values of the array where the boolean array is `True` are selected.

The varaible `idx` can therefore now be used as the index to select all elements greater than 2.

```python
print(a[idx])
```

    [3 4 5 6]

To do the whole process in a single step

```python
print(a[a > 2])
```

    [3 4 5 6]

Use shape to reshape the matrix?

Another example

```python
a = np.arange(5)
print('the total array:', a)
print('values less than 3:', a[a < 3])
```

    the total array: [0 1 2 3 4]
    values less than 3: [0 1 2]

## Mathematics with arrays.

Unlike lists, NumPy arrays support common arithmetic operations, such as addition of two arrays.

```python
# To add the elements of two lists we need the Numpy function: add
a = [1, 2, 3]
b = [4, 5, 6]

c = a + b
print(c)

c = np.add(a, b)
print(c)
```

    [1, 2, 3, 4, 5, 6]
    [5 7 9]

To add the elements of two arrays we can just use regular arithmetic operators.

```python
a = np.array([1, 2, 3])
b = np.ones((1,3))

c = a + b
print(c)
```

    [[ 2.  3.  4.]]

Algebraic operations are appled *elementwise* to an array.

This means the function is applied individually to each element in the list.

For addition and subtraction arrays behave like vectors and matrices.

For example, if you were to add or subtract two vectors/matrices in MATLAB.

```python
a = np.array([1.0, 0.2, 1.2])
b = np.array([2.0, 0.1, 2.1])

print(a - b)

print(np.subtract(a, b))
```

But it is important to remember that arrays ARE NOT vectors and matrices.

Regular mathematical operators perform *elementwise* operations on arrays.

```python
a = np.array([1.0, 0.2, 1.2])
b = np.array([2.0, 0.1, 2.1])

# Elementwise multiplication of a and b
print(a * b)
print(np.multiply(a, b))

print()

# Elementwise division of a and b
print(a / b)
print(np.divide(a, b))
```

    [ 2.    0.02  2.52]
    [ 2.    0.02  2.52]

    [ 0.5         2.          0.57142857]
    [ 0.5         2.          0.57142857]

If the number of __columns in A__

is the same as number of __rows in B__,

we can find the matrix product of $\mathbf{A}$ and $\mathbf{B}$.

```python
\begin{equation*}
\underbrace{
\begin{bmatrix}
1 & 2 & 3 \\
4 & 5 & 6 \\
7 & 8 & 9 \\
\end{bmatrix}
}_{\mathbf{A} \text{ 3 rows} \text{ 3 columns}}
\times
\underbrace{
\begin{bmatrix}
10 & 11 \\
12 & 13 \\
14 & 15 \\
\end{bmatrix}
}_{\mathbf{B} \text{  3 rows} \text{  2 columns}}
=\underbrace{
\begin{bmatrix}
(1 \cdot 10 + 2 \cdot 12 + 3 \cdot 14) \quad
(1 \cdot 11 + 2 \cdot 13 + 3 \cdot 15) \\
(4 \cdot 10 + 5 \cdot 12 + 6 \cdot 14) \quad
(4 \cdot 11 + 5 \cdot 13 + 6 \cdot 15) \\
(7 \cdot 10 + 8 \cdot 12 + 9 \cdot 14) \quad
(7 \cdot 11 + 8 \cdot 13 + 9 \cdot 15) \\
\end{bmatrix}
}_{\mathbf{C} \text{  3 rows} \text{  2 columns}}
=\underbrace{
\begin{bmatrix}
76  & 82 \\
184 & 199 \\
292 & 316 \\
\end{bmatrix}
}_{\mathbf{C} \text{  3 rows} \text{  2 columns}}
\end{equation*}
```

## Mathematics with Vectors (1D arrays)
Let's look at a  previous example for computing the dot product of two vectors.

The dot product of two $n$-length-vectors:

 $ \mathbf{A} = [A_1, A_2, ... A_n]$

 $ \mathbf{B} = [B_1, B_2, ... B_n]$

\begin{align}
\mathbf{A} \cdot \mathbf{B} = \sum_{i=1}^n A_i B_i.
\end{align}

We learnt to solve this very easily using a Python `for` loop.

With each iteration of the loop we increase the value of `dot_product` (initial value = 0.0) by the product of `a` and `b`.

```python
A = [1.0, 3.0, -5.0]
B = [4.0, -2.0, -1.0]

# Create a variable called dot_product with value, 0.
dot_product = 0.0

for a, b in zip(A, B):
    dot_product += a * b

print(dot_product)
```

Using Numpy arrays we can solve the dot product using the Numpy function `dot`.

```python
A = np.array([1.0, 3.0, -5.0])
B = np.array([4.0, -2.0, -1.0])

print(np.dot(A,B))
```

    3.0

__Try it yourself__

Recap the Seminar 5: Functions; in the cell below write a function that takes two lists and returns the dot product using the code from Seminar 4: Data Structures (shown above).

Use the magic function `%timeit` to compare the speed of the for loop with the Numpy `dot()` function for solving the dot product.

```python
# Write a function for the dot product of two vectors expressed as lists
# Compare the speed of your function to the Numpy function
```

## Mathematics with Matrices (2D arrays)
If you have previously studied matrices, the operations in this section will be familiar.

If you have not yet studied matrices, you may want to refer back to this section once matrices have been covered in your mathematics classes.

Material from this section will not be included in the exam.

2D arrays are a convenient way to represents matrices.

For example, to create the matrix

$$
A =
\begin{bmatrix}
3 & 5 & 7\\
2 & 4 & 6
\end{bmatrix}
$$

```python
A = np.array([[3, 5, 7],
              [2, 4, 6]])
print(A)
```

    [[3 5 7]
     [2 4 6]]

Recall, the method `shape()` tells us the dimensions of an array.

This gives us the number of rows and the number of columns, expressed as a tuple.

By describe the dimensions of a matrix as as "rows" by "columns" matrix.

```python
print(A.shape)
print(f"Number of rows is {A.shape[0]}, number of columns is {A.shape[1]}")
print(f"A is an {A.shape[0]} by {A.shape[1]} matrix")
```

    (2, 3)
    Number of rows is 2, number of columns is 3

### Matrix multiplication

If the number of __columns in A__

is the same as number of __rows in B__,

we can find the matrix product of $\mathbf{A}$ and $\mathbf{B}$.

\begin{align}
\mathbf{A} \mathbf{B} = \mathbf{C}
\end{align}

We multiply each __row__ in \mathbf{A} by each __column__ in \mathbf{B}

\begin{equation*}
\underbrace{
\begin{bmatrix}
a_{11} & a_{12} & a_{13} \\
a_{21} & a_{22} & a_{23} \\
a_{31} & a_{32} & a_{33} \\
\end{bmatrix}
}_{\mathbf{A} \text{ 3 rows} \text{ 3 columns}}
\times
\underbrace{
\begin{bmatrix}
b_{11} \\
b_{21} \\
b_{31} \\
\end{bmatrix}
}_{\mathbf{B} \text{  3 rows} \text{  1 column}}
=\underbrace{
\begin{bmatrix}
a_{11}b_{11} + a_{12}b_{21} + a_{13}b_{31} \\
a_{21}b_{11} + a_{22}b_{21} + a_{23}b_{31} \\
a_{31}b_{11} + a_{32}b_{21} + a_{33}b_{31} \\
\end{bmatrix}
}_{\mathbf{C} \text{  3 rows} \text{  1 column}}
\end{equation*}

In matrix $\mathbf{C}$, the element in

__row $i$__,

__column $j$__

is equal to the dot product of

__$i$th row__ of $\mathbf{A}$,

__$j$th column__ of $\mathbf{B}$.

Matrix $\mathbf{C}$ therefore has

the same number of __rows as A__,

the same number of __columns as B__.

#### Example 1: THIS EXAMPLE IS WRONG MAKE A GOOD ONE!!!

#### Example 2:
\begin{equation*}
\underbrace{
\begin{bmatrix}
1 & 2 & 3 \\
4 & 5 & 6 \\
7 & 8 & 9 \\
\end{bmatrix}
}_{\mathbf{A} \text{ 3 rows} \text{ 3 columns}}
\times
\underbrace{
\begin{bmatrix}
10 & 11 \\
12 & 13 \\
14 & 15 \\
\end{bmatrix}
}_{\mathbf{B} \text{  3 rows} \text{  2 columns}}
=\underbrace{
\begin{bmatrix}
(1 \cdot 10 + 2 \cdot 12 + 3 \cdot 14) \quad
(1 \cdot 11 + 2 \cdot 13 + 3 \cdot 15) \\
(4 \cdot 10 + 5 \cdot 12 + 6 \cdot 14) \quad
(4 \cdot 11 + 5 \cdot 13 + 6 \cdot 15) \\
(7 \cdot 10 + 8 \cdot 12 + 9 \cdot 14) \quad
(7 \cdot 11 + 8 \cdot 13 + 9 \cdot 15) \\
\end{bmatrix}
}_{\mathbf{C} \text{  3 rows} \text{  2 columns}}
=\underbrace{
\begin{bmatrix}
76  & 82 \\
184 & 199 \\
292 & 316 \\
\end{bmatrix}
}_{\mathbf{C} \text{  3 rows} \text{  2 columns}}
\end{equation*}

```python
#Example 1
A = np.array([[1, 1, 2],
              [2, 1, 3],
              [1, 4, 2]])

B = np.array([[3],
              [1],
              [2]])

C = A.dot(B)
print(C)

print()

C = np.dot(A,B)
print(C)
```

    [[ 8]
     [13]
     [11]]

    [[ 8]
     [13]
     [11]]

```python
#Example 2
A = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]])

B = np.array([[10, 11],
              [12, 13],
              [14, 15]])

C = A.dot(B)
print(C)

print()

C = np.dot(A,B)
print(C)
```

    [[ 76  82]
     [184 199]
     [292 316]]

    [[ 76  82]
     [184 199]
     [292 316]]

We can find the inverse $\mathbf{A}^{-1}$, of a sqaure matrix, $\mathbf{A}$.

```python
A = np.array([[1,2],
              [3, 4]])

Ainv = np.linalg.inv(A)

print(f"A = \n {A}")
print(f"Inverse of A = \n {Ainv}")
```

    A =
     [[1 2]
     [3 4]]
    Inverse of A =
     [[-2.   1. ]
     [ 1.5 -0.5]]

We can find the determinant, $\textrm{det}(\mathbf{A})$, of a sqaure matrix, $\mathbf{A}$.

```python
A = np.array([[1,2],
              [3, 4]])

Adet = np.linalg.det(A)

print(f"A = \n {A}")
print(f"Determinant of A = {round(Adet, 2)}")
```

    A =
     [[1 2]
     [3 4]]
    Determinant of A = -2.0

We can generate an *identity matrix*.

```python
I = np.eye(2)
print(I)

print()

I = np.eye(4)
print(I)
```

    [[ 1.  0.]
     [ 0.  1.]]

    [[ 1.  0.  0.  0.]
     [ 0.  1.  0.  0.]
     [ 0.  0.  1.  0.]
     [ 0.  0.  0.  1.]]

```python
print(np.sqrt(a))
print(a ** (1/2))

```

## Vectorising Functions

Numpy functions applied to a single array, will be performed on each element in the array.

The function takes an array of values as an input argument.

```python
print(np.sqrt(a))
print(a ** (1/2))

```

For example, we can apply trigonometric functions, elementwise, to arrays, lists and tuples.

```python
x = np.array([0.0, np.pi/2, np.pi, 3*np.pi/2])
y = [0.0, np.pi/2, np.pi, 3*np.pi/2]
z = (0.0, np.pi/2, np.pi, 3*np.pi/2)

print(np.sin(x))
print(np.cos(y))
print(np.tan(z))

```

    [  0.00000000e+00   1.00000000e+00   1.22464680e-16  -1.00000000e+00]
    [  1.00000000e+00   6.12323400e-17  -1.00000000e+00  -1.83697020e-16]
    [  0.00000000e+00   1.63312394e+16  -1.22464680e-16   5.44374645e+15]

An array of values does not work as an input for all functions.

```python
def func(x):
    if x < 0:
        f = 2 * x
    else:
        f = 3 * x
    return f

x = np.array([2,2])
y = func(x) # Run this line after removing the # to se
```

This doesn't work because Python doesn't know what to do with the line

`if x < 0`

when `x` contains many values.

For some values of `x` the `if` statement may be `True`, for others it may be `False`.

A simple way around this problem is to vectorise the function.

We create a new function that is a *vectorized* form of the original function.

The new function and can be called with an array as an argument.

```python
funcvec = np.vectorize(func)

x = np.random.randint(4,size =(2,2))

y = funcvec(x)
print(x)
print(y)
```

    [[3 0]
     [2 3]]
    [[9 0]
     [6 9]]

## Broadcasting

```python
a = np.array([1, 2, 3, 4])
b = np.ones((3,1))

c = a + b

print(b)
print()
print(c)
```

    [[ 1.]
     [ 1.]
     [ 1.]]

    [[ 2.  3.  4.  5.]
     [ 2.  3.  4.  5.]
     [ 2.  3.  4.  5.]]

## Review Exercises

The folowing exercises are provided to practise what you have learnt in today's seminar.

There are some extension excercises for you to complete if you are familiar with using matrices and want to practise matrix manipulation using Python.

If you have not yet studeied matrices, you can come back to this section when the mathematics used is more familiar to you.

### Review Exercise: Arrays and indices

In the cell below:

1. Create an array of zeros with length 25.

2. Change the first 10 values to 5.

3. Change the next 10 values to a sequence starting at 12 and increasig with steps of 2 to 30 - do this with one command.

4. Set the final 5 values to 30.

```python

```

### Review Exercise:  Two-dimensional array indices

In the cell below, for the array `x`, write code to print:

* the first row of `x`
* the first column of `x`
* the third row of `x`
* the last two columns of `x`
* the four values in the upper right hand corner of `x`
* the four values at the center of `x`

```python
x = np.array([[4, 2, 3, 2],
              [2, 4, 3, 1],
              [2, 4, 1, 3],
              [4, 1, 2, 3]])
```

### Review Exercise:  Fix the error
The code below, is supposed to:
- change the last 5 values of the array `x` to the values [50, 52, 54, 56, 58]
- print the result

There are some errors in the code.

Remove the comment markers and run the code to see the error message.

Then fix the code and run it again.

```python

```


Nota para antes de leer este documento:

 1. El paquete dst contiene toda la implementación de las ideas aquí expuestas. El notebook 2. Implementación incluye implementaciones para distintas configuraciones. En el presente documento se expondrá código de manera ilustrativa, sin embargo, el paquete es el encargado de realizar los procedimientos aquí expuestos. Para ver la implementación puede dirigirse al código fuente, al enlace a colab o al notebook de experimentos.

 2. La implementación que se realizo esta basada en el documento   Image Style Transfer Using Convolutional Neural Networks, en el blog de tensorflow y en el blog de Marko Jerkic

 3. La implementación está escrita en Python3 usando Tensorflow y Keras.

 Aprendizaje profundo para la transferencia de estilo

  Universidad de Antioquia
 Angelower Santana Velasquez
 Martin Elias Quintero  Osorio

Motivación

El Deep Learning (Aprendizaje Profundo) es un campo específico del aprendizaje automático (Machine Learning)y en consecuencia de la inteligencia artificial(AI), el cual, ha demostrado un avance exponencial los últimos años. El Deep Learning ha sorprendido por sus increíbles resultados en múltiples áreas para solucionar distintos tipos de problema. Específicamente el deep learning gira en torno al estudio de modelos basados en redes neuronales artificiales, sus funciones de pérdida, la capacidad de distintos tipos de optimización, estrategias para evitar el sobreajuste, el diseño de arquitecturas de redes neuronales enfocadas a cumplir objetivos desde distintos enfoques de aprendizaje, entre muchas otras estrategias que son de gran importancia dentro del Machine Learning. Podemos encontrar varias ramas del Deep Learning como el procesamiento de lenguaje natural (NLP) y la visión por computadora (Computer Vision -CV), siendo esta última materia de estudio desde hace más de una década teniendo avances sorprendentes. Hoy día podemos ver el aprovechamiento de la visión computacional en aplicaciones implementadas en aeropuertos, automóviles, en la industria y, no muy alejado, en nuestros teléfonos inteligentes. Dentro del Deep Learning existe un tipo de red neuronal capaz de emular, según algunas personas, el comportamiento biológico que ocurre en los seres humanos en el proceso de "visión", las redes neuronales convolucionales(CNN), llamadas así por realizar operaciones de convolución dentro del proceso de entrenamiento, operador bastante utilizado en el procesamiento de señales. Y son este tipo de redes neuronales casi la opción por defecto para tratar problemas de visión computacional.

La capacidad de cómputo que disponemos actualmente ha permitido que se realicen múltiples experimentos y se desarrollen ideas sorprendentes para el ojo común. Sistemas que controlan automóviles, aplicaciones que determinan cierto tipo de enfermedades en plantas, filtros y aplicación de diferentes "máscaras" en imágenes en tiempo real (por ejemplo instagram y snapchat), detección de enfermedades bajo la deteccion de patrones (por ejemplo la retinopatía diabética) y la clasificación de objetos son ejemplos de la capacidad que pueden lograr sistemas basados en redes neuronales convolucionales.

 Una cita de uno de los artistas más importantes del siglo XX, Pablo Picasso, dice : “La pintura es más fuerte que yo, siempre consigue que haga lo que ella quiere”. El presente informe explora una aplicación bastante ingeniosa para generar "arte" a partir de técnicas de Deep Learning con redes neuronales convolucionales. Que de forma similar a la frase de Picasso, será nuestro modelo el encargado de generar una imagen a su antojo a partir de dos fuentes de datos: Una imagen de contenido y otra de estilo. Ahora, ¿podrías imaginar como Vincent van Gogh había pintado a Medellín?

imagen 1
The starry night in Medellín
 Imagen contenido: Fotografia del centro de Medellín
 Imagen estilo: The Starry Night - Vincent van Gogh

Contenido y estilo

El cubismo fue un movimiento artístico donde se quería representar elementos de la cotidianidad en composiciones de formas geométricas bien definidas. Es decir, tomar un elemento, capturar su esencia y plasmarla en cubos, triangulos y rectangulos. De esa manera definimos el contenido de una imagen como la esencia que tiene dejando a un lado elementos como el color y la textura. Por otro lado, cuando hablemos de estilo nos referimos a la forma, los colores, sombras y otros matices que no sean la esencia. De hecho, la transferencia de estilo busca tomar la esencia y plasmar en ella la forma y colores de otra imagen. Por ejemplo, en la imagen 1 , el contenido es la fotografía de Medellín imagen 2 lado derecho superior y el estilo es la famosa pintura de Vicent Van Gogh : The starry night. imagen 2 lado derecho inferior

imagen 2
The starry night in Medellín (Contenido, estilo y resultado)
 Lado izquierdo: Resultado de Deep Transfer Style
 Lado Derecho parte superior: Contenido
Lado Derecho parte inferior: Estilo

Deep Transfer Style

La idea detrás de la técnica de transferencia de estilo es la representación interna que producen las redes neuronales convolucionales una vez entrenadas. Dichas redes están conformadas por capas que tienen por objetivo propósitos específicos: Algunas sirven como mapa de activación que nos indica que tan sensible es una imagen a un patrón o filtro. En otras palabras, si aplicamos un filtro que detecte bordes y formas cuadradas a una imagen de un televisor probablemente la forma del televisor se activará con dicho filtros. Dentro del proceso de entrenamiento la red aprende a detectar estas formas que inician con figuras muy básicas hasta evolucionar a detalles y figuras compuestas, aunque no por esto se llaman redes convolucionadas.

En la imagen 3 se puede observar una arquitectura de red bastante famosa, VGG16.

imagen 3
How convolutional neural networks see the world
 Fuente: Blog

VGG16 cuenta con 5 bloques internos compuestos por capas de convolución y de pooling (este tipo de capas reduce el volumen de los mapas de características, en la imagen 3 en rojo).Cada bloque cuenta con cierta cantidad de capas de convolución, una notación para determinar las capas es especificar el número de bloque y la posición que esta tiene dentro del arreglo.Por ejemplo la capa convolucional dos del bloque uno se representa como :Conv2_1, mientras la capa convolucional tres en el bloque cuatro sería: Conv4_3.
Como se mencionó anteriormente en el proceso de entrenamiento la red aprende múltiples filtros, la siguiente pregunta que debemos realizar es : ¿Qué está viendo la red realmente?, ¿Qué es lo que está detectando esos filtros?

La imagen 4 nos da una respuesta a dichas preguntas. Podemos observar que luego del entrenamiento conv1_1 es capaz de detectar ciertos colores y un par de texturas. Mientras tanto conv2_1 parece ser el resultado de combinar las texturas y colores de conv1_1, , recordemos que en medio de estas capas y de las siguientes que mencionaremos existen otras capas y operaciones que tendrán combinaciones de este tipo.

Pasando a conv3_1, podemos hacer dos observaciones, la primera es que los colores se ven mucho más granulados que en las dos capas anteriormente estudiadas y empieza a aparecer forma más definidas como líneas diagonales curvas o puntos bien detallados.

Conv4_1 nos muetra un salto enorme respecto a conv3_1. Podemos identificar con facilidad formas más específicas, agrupaciones de líneas, círculos y combinación de colores que se organizan de forma no uniforme.

Finalmente, conv5_1, muestra filtros que ya saben detectar formas bien definidas, más delineadas, llevando las imágenes a una representación más abstracta a medida que avanza por las diferentes capas.

imagen 4
How convolutional neural networks see the world
 Fuente: Blog de Keras por Francois Chollet

El análisis anterior es el pilar de la técnica de transferencia de estilo presentada por León A. Gatys en el articulo  Image Style Transfer Using Convolutional Neural Networks. Dicho comportamiento ha sido bastante estudiado y es el que logra que las redes neuronales convolucionales capturen detalles y formas, que a su vez, las vuelven enormemente potentes en tareas de clasificación. Es evidente que se empiezan con estructuras muy primitivas como en  conv1_1  hasta llegar a formas más estructuradas en conv5_1.

Lo importante es entender esta capacidad de las  CNN para nuestro tema de interés, es que mientras las primeras capas de la red son más sensibles a los colores y texturas, las últimas capas son capaces de capturar la forma de los objetos. Dicho de otra manera, de las primeras capas capturaremos la representación de la imagen de estilo,luego, de la imagen de contenido obtendremos la representación de alguna de las últimas capas, por ejemplo de conv5_1.

Específicamente en el artículo de León A. Gatys se usa para capturar el estilo:conv1_1, conv2_1, conv3_1, conv4_1, conv5_1 y para capturar el contenido : conv5_2

De esa manera el primer paso es tomar una red neuronal entrenada y pasar a través de ella dos imágenes, una para el contenido y otra para el estilo. Luego, capturar las respectivas capas de interés para cada una.

Ahora, debemos generar una tercer imagen la cual será el resultado(por ejemplo ver imagen 1) de la combinación de las dos anteriores. En el articulo generan una imagen con ruido gaussiano. Experimentos posteriores de la tesis demuestra que al usar la imagen de contenido como imagen de inicio da resultados más consistentes. En los experimentos usaremos ambas.

Luego, pasaremos la imagen generada por la red extrayendo tanto las capas de contenido como de estilo y calcularemos una pérdida de estilo respecto a las capas obtenidas de la imagen de estilo, y otra pérdida de contenido respecto a las capas de la imagen de contenido.

Funciones de costo

Pérdida de estilo

Necesitamos una forma especial para calcular la perdida de estilo. El estilo esta relacionado con la correlacion que hay entre los pixeles de una imagen. Una matriz Gram nos genera una representacion de como son dichas correlaciones. El siguiente video se muestra una explicacion de como funcion y se opera una matriz Gram.

```python
from IPython.display import HTML
HTML('')
```

fuente : Udacity

En el articulo Image Style Transfer Using Convolutional Neural Networks se muestra el calculo de la matriz gram de la siguiente forma:

\begin{equation}
G_{i j}^{l}=\sum_{k} F_{i k}^{l} F_{j k}^{l}
\end{equation}

Luego, podemos calcular el error de las matrices Gram como la diferencia de las matrices Gram para las capas $l$ respectivas entre la imagen de estilo y la imagen generada. En el articulo el error cuadratico esta escrito como :

\begin{equation}
E_{l}=\frac{1}{4 N_{l}^{2} M_{l}^{2}} \sum_{i, j}\left(G_{i j}^{l}-A_{i j}^{l}\right)^{2}
\end{equation}

Ahora, la perdida de estilo se calcula como la sumatoria de los errores de las matrices Gram para cada capa $l$ multiplicado por un peso que se le da a cada capa. En el articulo dicho peso es el mismo para cada una de las capas.

\begin{equation}
\mathcal{L}_{\text { style }}(\vec{a}, \vec{x})=\sum_{l=0}^{L} w_{l} E_{l}
\end{equation}

Perdida de contenido

La perdida de contenido es la suma de las distancias euclidianas de las capas que representan el contenido. Asi , sea $\vec{p}$ la imagen original, $\vec{x}$ la imagen generada y $l$ la capa contenido, la perdida sera.

\begin{equation}
\mathcal{L}_{\text { content }}(\vec{p}, \vec{x}, l)=\frac{1}{2} \sum_{i, j}\left(F_{i j}^{l}-P_{i j}^{l}\right)^{2}
\end{equation}

$F^{l}$ es el mapa de caracteristicas de la capa $l$ de la imagen original, mientras $P^{l}$ es el mapa de caracteristicas de la capa $l$ de la imagen generada.

Perdida total y actualizacion de imagen

Para la perdida total se realiza la suma entre las perdidas de estilo y contenido.

\begin{equation}
\mathcal{L}_{\text { total }}(\vec{p}, \vec{a}, \vec{x})=\alpha \mathcal{L}_{\text { content }}(\vec{p}, \vec{x})+\beta \mathcal{L}_{\text { style }}(\vec{a}, \vec{x})
\end{equation}

Donde alpha $\alpha$ es el peso que se le dara al contenido y $\beta$ el peso que se le dara al estilo.

Solo quedaria calcular el gradiente de la perdida total respecto a la imagen generada, de forma tal, que al cumplir cierto numero de iteraciones el error total disminuya y la imagen generada adquiera el contenido de la imagen de contenido y el estilo de la imagen de estilo. Los parametros $\alpha$ y  $\beta$ son de suma importancia dado que determinaran la forma en como se generara la imagen. El articulo propone ciertas correspondecias para obtener diferentes salidas.

Estrategia de generación de imagen

Para finalizar, discutiremos el flujo de lo revisado hasta el momento paso por paso.

En primer lugar pasamos la imagen de estilo y la imagen de contenido a través de la red neuronal ya entrenada. Obtenemos las capas convolucionales que deseemos de cada una de ellas. Según el artículo para la imagen de estilo obtendremos los mapas de características ubicados en las capas   conv1_1, conv2_1, conv3_1, conv4_1, conv5_1 , l mismo tiempo, para la imagen de contenido obtenemos solo conv5_2.

Para  conv1_1, conv2_1, conv3_1, conv4_1, conv5_1, conv5_2  de la imagen de estilo calculamos las matrices gram.

Posteriormente necesitamos una imagen que nos servirá de lienzo para la imagen generada. Esta imagen puede ser una imagen que generemos con ruido (puede ser una imagen generada mediante una distribución normal) o la imagen de contenido. Los experimentos mostrados en la tesis y el artículo muestran que si se inicia con la imagen de contenido la transferencia de estilo es más estable y fiel al contenido. Esta imagen generada se pasa por la red neuronal y se extrae de ella tanto las capas que se obtuvieron de la imagen de estilo como la de contenido, en otras palabras, de la imagen generada se obtendrá.

Ahora, calculamos las matrices gram de la imagen generada y realizamos la pérdida de estilo respecto a las matrices gram de la imagen de estilo. De forma similar, calculamos la diferencia de  conv5_2 de la imagen generada respecto a la imagen de contenido.

Sumamos ambos errores y a continuación calculamos los gradiente de la imagen generada respecto al error total. Repetimos este proceso el número de optimización que se le quieran realizar a la imagen generada. A mayor número de iteración se espera que la imagen generada tenga mejores resultados.

notebooks/1.Aprendizaje profundo para la transferencia de estilo.ipynb