Warning
This page is located in archive. Go to the latest version of this course pages.
Lab 1
Operating system

Students are heavily advised to use a Linux based OS. A good starter choice is an LTS Ubuntu distribution (2016 or 2020). Windows is mostly unsuitable for robotics/AI based work/research. If you use Windows, you will likely encounter problems and we will not be able to provide advice.


Python

You will be using python in this subject, as well as in many of your subjects throughout the Bachelors and Masters course. Python is extremely flexible and powerful, mostly due to the ease of use and the vast amount of available libraries for almost anything. We will be using python 3 (do not use python 2). Any version above 3.6 should be fine. Q: Isn't python slow? Yes, python is an interpreted language so it is slow. When performing computation / optimization python is used by calling very fast subroutines that are written in C/Fortran. Usually, over 95% of the computational time taken is spent in some low level library. This means that the python overhead is negligible. Python is unsuitable for applications such search in large graphs, high-end games, applications which are time-critical.


Python envs (optional)

Your linux distribution (ubuntu for example) already comes with some version of python 3 installed. This is the system interpreter. You can tell that you are installing onto your system interpreter if you need 'sudo' to perform a pip install command for example. Another solution would be to work with python environment. This can be done using an environment manager such as virtualenv and conda. This allows you to have multiple python environments on a single machine, each with different versions of packages etc. This guarantees that you don't have any conflicts between projects. Some more avid python users can exhibit a *mild* to severe reaction if you tell them that you install packages to your system interpreter.


Python workflow

Python scripts can be written in a simple “.py” file and run in the command terminal using “python myfile.py”. We do however advise the use of an IDE for development. A good choice is Pycharm (community version is okay, Czech Technical University provides the license for the Professional version, which ease working on the server later on). It provides autocompletion, debugging, syntax checking and many useful other tools.


Version control

To save and manage your code you can use git for version control. Pycharm has integrated git support. You can commit and push code with a single button.


Python basics

#!/usr/bin/env python
# coding: utf-8
 
# !!Tip!!: To execute a specific block of code from pycharm:
# a) open a python console (Tools -> python console),
# b) highlight code
# c) alt + shift + e to execute  (You can bind this key combo to a single key in settings -> keymap)
 
### Python refresher. ###
 
# Numbers
a = 3 # These will be python integers
b = 2. # The dot makes it a float
c = float(5) # Explicit definition
 
# Check the types (everything is a class btw)
print(a, type(a))
print(b, type(b))
print(c, type(c))
 
# Potentially nasty bugs if this is done wrong. Float usually has type priority.
print(2/a, 2//a, 2./a, 2/c)
 
# Formating string for printing
print("a: {}, b: {}, c: {}, a * b + c: {}".format(a, b, c, a * b + c))
 
# Also a shorter format version:
foo = 3
print(f'foo = {foo}')
 
# Note: mostly the " and ' are interchangeable for strings.
 
# Lists: Universal container which can contain anything. Can be iterated.
my_list = [1,2,33,["foo"], 4]
my_list.append(print) # Add stuff to list (yes, I added a function to the list. It can be identified and called later)
del my_list[0] # Delete stuff
 
# Indexation is modular
print(my_list[-1], my_list[-2])
 
# Tuples: Immutable containers, also useful. Using round brackets and adding a comma makes it a tuple
not_a_tuple = (1)
a_tuple = (1,)
another_tuple = (1,3,3,7)
 
print(type(not_a_tuple), type(a_tuple), type(another_tuple))
 
# Broadcasting:
print(my_list[1:3])
my_list_2 = my_list[2:]
 
# The traditional way of iterating through a list, not very pythonic, avoid this.
for i in range(len(my_list)):
    print(my_list[i])
 
# Proper way of iterating through list:
for l in my_list:
    if callable(l): # If it's that print function from earlier, then call it with an argument
        l("Hello")
    else:
        print(l)
 
# Enumerate while iterating
for i, t in enumerate(another_tuple):
    print(i, t)
 
# Iterate over n iterables at the same time.
for e1, e2 in zip(my_list, another_tuple):
    assert e1 != e2 # This is like an 'if' statement, but will crash the program if not satisfied. Can be used in a try-catch
 
# Shortcut iterating
my_shortcut_list = [i**2 if i % 2 == 0 else "banana" for i in range(10)]
 
# None type. This is analogous to the null or void types in other languages
q = None
print(q is None) # Dont check None using == sign!
 
# Functions
def myfun_1(arg):
    arg = 3
 
a = 1
myfun_1(a)
print(a) # Unchanged
 
def myfun_2(arg):
    arg[1] = "err"
 
mod_list = list(range(10)) # Note than the function range gives a generator (which is iterable). It has to be converted to a list if you want to work with it
myfun_2(mod_list) # This will modify the list
print(mod_list)
 
# Tip: If you highlight the called function and press CTRL + b in pycharm, the IDE will show you the declaration of the function. 
# This is extremely useful in navigating over the larger repository of code.
 
fun_list = (lambda x : x + i for i in range(10)) # Generator of anonymous functions
print(list(fun(1) for fun in fun_list))
 
# Dictionaries: Very useful container. They key and value can literally be anything that's hashable
my_d = {"key_1" : "value_1", 24 : "value_2", (3,2,"33") : [111]}
my_d_2 = dict(var1=4, var2="3") # Can also be defined like this, instead of the {}. I never use this
 
# Add to dict
my_d["key_x"] = 9
 
print(my_d, '\n', my_d_2)
 
# Check if key is in dict
print((3,2,"33") in my_d)
 
# Iterate dict
for k in my_d_2.keys():
    print(k)
for v in my_d_2.values():
    print(v)
for k,v in my_d_2.items(): # Both keys and values at the same time
    print(k,v)
 
# !!! IMPORTANT NOTES AND TIPS !!!:
# For loops are very slow and should be avoided if possible. Broadcasting should be done instead
# You should absolutely master indexing and broadcasting.
# Everything is a class, be carefull when passing references!
# If you need to do something with an array/list you can be almost sure that a function already exists that helps you do that.
# Try out the debugger! Put a breakpoint somewhere and step the computation using F8. Observe the available variables and their values/attributes.

Numpy refresher

#!/usr/bin/env python
# coding: utf-8
 
# Tip: To execute a specific block of code from pycharm:
# a) open a python console (Tools -> python console),
# b) highlight code
# c) alt + shift + e to execute
 
import numpy as np
 
### Simple numerical manipulation recap. ###
 
# Tip: Use debugger to check the shapes and other attributes of the following vectors and matrices.
# Tip: When stopped at a breakpoint during debugging, you can rightclick -> evaluate expression. You can then type in
# whatever you want (all the variables at the point at available) to test what will happen, without crashing the current execution.
 
# Vectors
a = np.array([10, 40, 20])
b = np.array([1, 0.1, 0.01])
c = 1.
print("a: {}, b: {}, c: {}, a * b + c: {}".format(a, b, c, a * b + c))  # Elementwise multiplication
 
a = np.array([[10, 40, 20]]) # Added nested list
print("a.shape: {}, b.shape: {}".format(a.shape, b.shape))  # Shapes of vectors as numpy sees it
print("a.T * b + c:")  # Vector multiplication.
print(a.T * b + c)
 
# Notice that you can add single numbers to whole vectors and matrices. You can also add vectors to matrices
 
# Matrices
A = np.eye(3)
B = np.random.randn(3, 3)  # Identity and random 3x3 matrices. There are various 'random' matrix types (normal, uniform, integer uniform, etc)
 
print("A: ")
print(A)
 
print("B: ")
print(B)
 
print("A * B: ")
print(A * B)  # Elementwise multiplication
 
print("AB: ")
print(A @ B)  # Matrix multiplication,  A @ B == A.dot(B) == np.matmul(A,B)
 
v = np.arange(3)[:, np.newaxis] # The arange function is builtin. The np.newaxis adds an additional dimension which makes v a matrix
print("A + v: ")
print(A + v)  # Addition.
print("A + v.T: ")
print(A + v.T)  # Addition.
 
# Indexing.
a = np.arange(100, 110)
print(a[0]) # Same as for lists
idx_vec = [0,3,3,8]; print(a[idx_vec]) # You can use a vector to index. ';' delimits commands.
 
# Slicing: #x[start:stop:step]
print(a[1:10:2])
 
# Some other ways of writing small or large floats. The 'e' just means exponent, it's not Eulers number
print(1e7, 1e-4)
 
# Other usefull data types
a = np.inf
b = np.nan
c = None
print(a + a, a - a, a + b)
 
try:
    a + c # Can't operate with None
except TypeError:
    print("Can't do that")
 
print(np.isinf(a), np.isnan(b), c is None) # Proper way how to check these vars
 
# Note:
# - Never use for loops to implement any sort of vector or matrix multiplication!
# - The difference between proper and sloppy implementation can be in several orders of magnitude of wait time.
 
import time
N = 1000000
vec1 = np.random.rand(N)
vec2 = np.random.rand(N)
vec3 = np.zeros(N)
t1 = time.time()
for i in range(N):
    vec3[i] = vec1[i] + vec2[i]
print(f"Naive vector addition took: {time.time() - t1} s on {N} data")
 
t1 = time.time()
vec3 = vec1 + vec2
print(f"Vectorized addition took: {time.time() - t1} s on {N} data")
 
# Note, this method isn't the most accurate to measure time taken, especially for very short intervals.
 
 
# Practice (5 mins):
# 1) Make any [3x3] matrix M and [3x1] vector x. Multiply Q = Mx, Q = $x^T$M. Add x to M elementwise row by row with x (and then column by column).
# 2) Make an array a = [1,2,...30] (tip:use np.arange), b = [0,3,11,29]. Set the array a at positions indicated by vector b to 42 (tip:index a using b)
# 3) Make matrix B by tiling the first 5 elements of a 5 times vertically. Resultant matrix should have 5 row vectors which are [42,2,3,42,5] (tip: use np.tile)
# 4) Set last 2 columns of the last 2 rows of matrix B to zero

Pytorch is a numerical computation library with autograd capabilities. The following tutorial is to help refresh numpy basics and familiarize the student with the Pytorch numerical library. There are plenty high quality tutorials available online ranging from very basics to advanced concepts and state of the art implementations.

A good place to start would be here or the tutorial below.

Make sure that you have Pytorch installed preferably with python3. You can do so by following the instructions from the getting started section. If you have a recent NVIDIA GPU then try to install the GPU version. In some cases the GPU can offer speedups of up to ~30x.

Pytorch basics

import torch
import numpy as np
 
### Pytorch Basics, use Debugger to inspect tensors (for finding out their shape and other attributes) ###
 
# Terminology: Tensor = any dimensional matrix
 
# Empty tensor of shape 5x3. Notice the initial values are (sometimes) whatever garbage was in memory.
x = torch.empty(5, 3)
print(x)
 
# Construct tensor directly from data
x = torch.tensor([5.5, 3])
print(x)
 
# Convert numpy arrays to pytorch
nparr = np.array([1, 2])
x = torch.from_numpy(nparr)
 
# Convert pytorch arrays into numpy
nparr = x.numpy()
 
# Make operation using tensors (they support classic operators)
a = torch.tensor([3.])
b = torch.rand(1)
c = a + b  # a + b = torch.add(a,b)
print("a: {}, b: {}, a + b: {}".format(a, b, c))
 
# Note, when performing operations make sure
# that the operands are of the same data type
a = torch.tensor(3)  # int64 type
b = torch.tensor(3.)  # float32 type
print("atype: {}, btype: {}".format(a.dtype, b.dtype))
 
# ERROR, data type mismatch:
# print(a + b)
 
# Convert data type
b = torch.tensor(3., dtype=torch.int32)  # Explicitly force datatype
b = b.long()
print(a + b)
 
### Autograd ###
 
# Make two tensors
a = torch.tensor(8., requires_grad=True)
b = torch.tensor(3., requires_grad=True)
 
# c tensor is a function of a,b
c = torch.exp((a / 2.) - b * b + 0.5)
print("c value before applying gradients: {}".format(c))
 
# Backwards pass
c.backward()
 
# Print gradients of individual tensors which contributed to c
print("a.grad: {}, b.grad: {}".format(a.grad, b.grad))
 
# Move a,b towards the direction of greatest increase of c.
a = a + a.grad
b = b + b.grad
c = torch.exp((a / 2.) - b * b + 0.5)
print("c value after applying gradients: {}".format(c))
 
# If we don't want to track the history of operations then
# the torch.no_grad() context is used to create tensors and operations
print(a.requires_grad)
with torch.no_grad():
    print((a + 2).requires_grad)
 
# Note: Whenever we create a tensor or perform an operation requiring
# a gradient, a node is added to the operation graph in the background.
# This graph enables the backwards() pass to be called on any node and find all
# ancestor operations.

courses/b3b33vir/tutorials/pytorch/start.txt · Last modified: 2021/09/15 19:15 by vacekpa2