<jupyter_start><jupyter_text>Welcome to Data Science Survival Skills [DSSS] - Exercises!This is the very first exercise in the DSSS course that shows you the difference between CPU and GPU based computations. We will use Google Colab so that you have access to sufficient CPU and GPU computing power. If you don't know Google Colab yet, please familiarize yourself with it as we will need it for the following exercises. Also, work through the examples below and answer the questions at the end. Save and upload your final version as either `ipynb` or `pdf`.  We first have to install CuPy because it is not pre-installed on Google Colab:What is CuPy? It is an array library for GPU-accelerated computing with Python ([CuPy's official website](https://https://cupy.dev/)).When installing CuPy, you need to specify the [CUDA](https://en.wikipedia.org/wiki/CUDA) version, here we use 10.0 which is in line with current environment.<jupyter_code>!pip install cupy-cuda100<jupyter_output>Collecting cupy-cuda100
  Downloading cupy_cuda100-9.5.0-cp37-cp37m-manylinux1_x86_64.whl (59.0 MB)
[K     |████████████████████████████████| 59.0 MB 36 kB/s 
[?25hRequirement already satisfied: numpy<1.24,>=1.17 in /usr/local/lib/python3.7/dist-packages (from cupy-cuda100) (1.19.5)
Requirement already satisfied: fastrlock>=0.5 in /usr/local/lib/python3.7/dist-packages (from cupy-cuda100) (0.6)
Installing collected packages: cupy-cuda100
Successfully installed cupy-cuda100-9.5.0<jupyter_text>To use the GPU (or TPU):*   Go to "Runtime" -> "Change runtime type" and select "GPU".*   "Laufzeit" -> "Laufzeittyp ändern" and select "GPU". We can import some modules by using the following commands:<jupyter_code>import numpy as np
import cupy as cp
import time
%matplotlib inline
import matplotlib.pyplot as plt<jupyter_output><empty_output><jupyter_text>[More info about modules](https://docs.python.org/3/tutorial/modules.html). We are working with [numpy](https://numpy.org/) and [cupy](https://cupy.dev/), two libraries that are capable of dealing with linear algebra and more sophisticated mathematical operations, important in data science, deep learning, image processing, etc. The core data structure are `arrays`, that are multidimensional and contain the data.  Create some np and cp arrays:<jupyter_code>#you can specify the type of array using the argument 'dtype'
a_cpu = np.arange(9, dtype = np.float64).reshape(3, 3) #Which is a Numpy's array
a_gpu = cp.arange(9, dtype = cp.float64).reshape(3, 3) #Which is a Cupy's array

print(type(a_cpu))
print(a_cpu, a_cpu.dtype)
print()

print(type(a_gpu))
print(a_gpu, a_gpu.dtype)
print()<jupyter_output><class 'numpy.ndarray'>
[[0. 1. 2.]
 [3. 4. 5.]
 [6. 7. 8.]] float64

<class 'cupy._core.core.ndarray'>
[[0. 1. 2.]
 [3. 4. 5.]
 [6. 7. 8.]] float64<jupyter_text>For more available data types, you can visit: [Data type objects](https://numpy.org/doc/stable/reference/arrays.dtypes.html) Let's check how much time does it numpy take to do the sum of array elements<jupyter_code>start_time = time.time()
np.sum(a_cpu) 
print("Time: %s milliseconds" % ((time.time() - start_time)*1E3)) #in mS<jupyter_output>Time: 0.21600723266601562 milliseconds<jupyter_text>**We can also use the Jupyter magics, which are more convenient: `%timeit` or  `%time` (one line) or `%%time` (the whole cell)**<jupyter_code># Example with magics:
%time np.sum(a_cpu)<jupyter_output>CPU times: user 53 µs, sys: 12 µs, total: 65 µs
Wall time: 69.4 µs<jupyter_text>Now CuPy<jupyter_code>%time cp.sum(a_gpu)<jupyter_output>CPU times: user 281 ms, sys: 1.06 ms, total: 283 ms
Wall time: 286 ms<jupyter_text>We can also use a cross-calculation using CuPy with Numpy arrays and vice-versa<jupyter_code>print("Time Numpy on GPU array: ")
%time np.sum(a_gpu) 


print("\nTime Cupy on CPU array: ")
%time cp.sum(a_cpu)<jupyter_output>Time Numpy on GPU array: 
CPU times: user 255 µs, sys: 0 ns, total: 255 µs
Wall time: 278 µs

Time Cupy on CPU array: 
CPU times: user 74 µs, sys: 0 ns, total: 74 µs
Wall time: 81.5 µs<jupyter_text>One can create 2D Arrays with **x** columns and **y** rows by using:<jupyter_code>#---------------[y, x]
rows, columns = [6, 5]
a_cpu = np.ones((rows, columns), dtype = np.uint32)
b_cpu = np.ones((rows, columns), dtype = np.uint32)

a_gpu = cp.ones((rows, columns), dtype = np.uint32)
b_gpu = cp.ones((rows, columns), dtype = np.uint32)

print('Array: \n', a_cpu)
print('\nDtype: ', a_cpu.dtype)
print('\nShape:', a_cpu.shape)<jupyter_output>Array: 
 [[1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]]

Dtype:  uint32

Shape: (6, 5)<jupyter_text>Slicing and indexingSo for given tasks, indexing and slicing our arrays is very useful to avoid 'for' loops or some time-consuming operations.  A good link to check is [Numpy's Documentation for Basic (and advanced) Slicing and Indexing](https://numpy.org/doc/stable/reference/arrays.indexing.html) In Python, we start to count in the upper left corner (position [0,0]). Having a random array given by:<jupyter_code>n = np.array(([0, 1, 2], [6, 7, 8], [3, 4, 5], [9,10,11],[12,13,14]))
print(n, n.dtype)
#we can bind two arrays by using
m = n # or m = n[:]
print (m)
m_ = np.copy(n)
m_[2] = 42
m_
n
#Sum the elements smaller than 6 and add 1
#boolean array indexing
m[m<6] += 1
#both will be affected
print('\nm:')
print(m)
print('\nn:')
print(n)<jupyter_output>m:
[[ 1  2  3]
 [ 6  7  8]
 [ 4  5  6]
 [ 9 10 11]
 [12 13 14]]

n:
[[ 1  2  3]
 [ 6  7  8]
 [ 4  5  6]
 [ 9 10 11]
 [12 13 14]]<jupyter_text>IMPORTANT: The following example reverses in columsn (in an image, the height), if you want to reverse the whole array it is `x[::-1, ::-1]`<jupyter_code>#We can reverse the array by using: 
n = np.array(([0, 1, 2], [6, 7, 8], [3, 4, 5], [9,10,11],[12,13,14]))
n[::-1]
#Slicing rows 0 to 3 (0 = start, 4 = stop) and taking the second and third column element (1:3)
n[0:4,1:3]
# Task: extract all odd numbers
arr = np.array([0, 2, 1, 3, 4, 5, 6, 7, 8, 9])
arr[arr % 2 == 1]<jupyter_output><empty_output><jupyter_text>Image as an arrayLet's now obtain an image from the internet:<jupyter_code># Loading an image is also only a numpy array,
# we talk about this in the next lectures in more detail.
from skimage import io
url = 'https://www.fau.de/files/2019/07/Kollegienhaus_Malter_3-480x284.jpg'
image = io.imread(url)
print(type(image))
print(image.shape) # This is HEIGHT, WIDTH, COLOR
plt.imshow(image)<jupyter_output><empty_output><jupyter_text>**1. To Do**: Define an array named 'im_half' containing half of the image (using indexing) and plot it:<jupyter_code>image.shape
im_half = image[:, :int(image.shape[1]/2)]
plt.imshow(im_half)<jupyter_output><empty_output><jupyter_text>**2. To Do**: Define an array named 'im_flipped' containing the horizontally-flipped image (using indexing) and plot it:<jupyter_code>im_flipped = image[::-1, ::-1]
plt.imshow(im_flipped)
im_striped = np.copy(image)

im_striped[::5, ::] = 0

plt.imshow(im_striped)
img = np.zeros((128, 128))
w, h = img.shape

x0 = int(w / 4)
y0 = int(h / 4)
dx = int(w / 2)
dy = int(h / 2)

img[x0:x0+dx, y0:y0+dy] = 1

plt.imshow(img, cmap='gray')<jupyter_output><empty_output><jupyter_text>**CuPy vs Numpy**The objective now is to create a way to check which is faster while calculating some simple functions like dot product or matrix sum. \\(Matrix operations: add, substract, divide, sqrt, T) \\(Other operations: mean, std, var, min, max, argmin, argmax)  **3. To Do**: Create two functions named compare_numpy() and compare_cupy() with two function entries called 'size' (maximum array size) and 'step' (for the step size). Start with array sizes of (1, 1) and increase the sizes (step) until they reach (size, size). For each array size perform the matrix operation (dot(a,b)) and measure computation time. Both functions should return a list with the saved times in Milliseconds.<jupyter_code># Measuring time
t0 = time.time()
# do something here
t1 = time.time() - t0 # in seconds
#Definition of your functions:
def compare_numpy(size, step):
  savedTimes = [] #Tip: use the append method for empty lists
  for i in range(1,size, step):
    t0 = time.time()
    a = np.ones((i, i), 'f')
    b = np.ones((i, i), 'f')
    np.dot(a, b) 
    savedTimes.append((time.time() - t0) *1E3) #in mS
  return savedTimes

def compare_cupy(size, step):
  savedTimes = []
  for i in range(1,size, step):
    t0 = time.time()
    a = cp.ones((i, i), 'f')
    b = cp.ones((i, i), 'f')
    np.dot(a, b) 
    savedTimes.append((time.time() - t0) *1E3) #in mS
  return savedTimes<jupyter_output><empty_output><jupyter_text>**This should plot the results:**<jupyter_code>size = 900  # max value of rows and cols to calculate
step = 10   # size of the step to compute
time_cpu_dot = compare_numpy(size,step)  # output containing the results of your operation with Numpy
time_gpu_dot = compare_cupy(size,step)   # output containing the results of your operation with CuPy
steps = np.arange(1,size,step)        # array to use as x-axis on our plot
plt.title('Comparison of dot product: Numpy vs Cupy')
plt.scatter(steps,time_cpu_dot, label = 'Numpy')
plt.scatter(steps,cp.asnumpy(time_gpu_dot), label = 'Cupy') # matplolib does not support CuPy-like arrays -> cp.asnumpy(cupy_array)
plt.ylabel('time in ms')
plt.xlabel('size of the array')
plt.yscale('log')
plt.xscale('log')
plt.legend()
plt.show()<jupyter_output><empty_output><jupyter_text>**4. To Do**: Repeat the task with the sum operation (a+b)<jupyter_code>#Tip 1: You can use now a+b or sum(a,b)
#Tip 2: Let the cols value unchanged and vary the rows of the matrix
def compare_numpy_2(rows, cols, step):
  savedTimes = []
  for i in range(1,rows, step):
    t0 = time.time()
    a = np.ones((i, cols), 'f')
    b = np.ones((i, cols), 'f')
    a+b
    savedTimes.append((time.time() - t0)*1E3) #in mS
  return savedTimes

def compare_cupy_2(rows, cols, step):
  savedTimes = []
  for i in range(1,rows, step):
    t0 = time.time()
    a = cp.ones((i, cols), 'f')
    b = cp.ones((i, cols), 'f')
    a+b
    savedTimes.append((time.time() - t0)*1E3) #in mS
  return savedTimes<jupyter_output><empty_output><jupyter_text>**This should plot the results:**<jupyter_code>rows = 900 # max value of rows to calculate
cols = 10  # max value of cols to calculate
step = 10  # size of the step to compute
time_cpu_sum = compare_numpy_2(rows, cols, step) # output containing the results of your operation with Numpy
time_gpu_sum = compare_cupy_2(rows, cols, step)  # output containing the results of your operation with CuPy
steps = np.arange(1,rows,step)                   # array to use as x-axis on our plot
plt.title('Comparison of sum: Numpy vs Cupy')
plt.scatter(steps,time_cpu_sum, label = 'Numpy')
plt.scatter(steps,cp.asnumpy(time_gpu_sum), label = 'Cupy')
plt.ylabel('time in ms')
plt.xlabel('size of the array')
plt.yscale('log')
plt.xscale('log')
plt.legend()
plt.show()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.13697878566203364, 'fraction_numerical': 0.03685076810534016, 'mean_word_length': 4.551776649746193, 'pattern_counts': {'":': 0, '<': 65, '<?xml version=': 0, '>': 68, 'https://': 9, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '529817', 'license': 'MIT', 'n_tokens_mistral': 4299, 'n_tokens_neox': 3970, 'n_words': 1347, 'path': 'Exercise1_solution.ipynb', 'repo_name': 'tuhinmallick/DataScience-Survival-Skils', 'size': '10936'}