Colabs

Introducere în Colab

Varianta rulabilă a fișierului curent poate fi accesată aici: Introducere în Colab

Ce este Colab?

Colaboratory este o instanță Jupyter notebook accesibilă full online. Asta înseamnă că nu necesită setup și le permite utilizatorilor să creeze și să colaboreze împreună cu alții pe documente live care conțin cod rulabil, vizualizări și text explicativ.

În general este util pentru a scrie prototipuri și a rezolva probleme scurte, ușor de urmărit secvențial.

Există mai multe variante pentru a rula o secvență de cod după ce ai dat click pe ea:

  • Apasă butonul de play din stânga acesteia
  • Rulează Shift + Enter pentru a trece la celula următoare
  • Rulează Cmd / Ctrl + Enter pentru a rămâne în celula curentă
print('Hello World!')

Output: Hello World

Sub celulă a apărut outputul acestuia. Outputul poate fi valoarea returnată de celulă, o afișare / serie de afișări (print) sau nimic. Obiectele apelate direct sau operațiile returnează o valoare, atribuirile nu apelează nimic:

'Hello World'

Output: Hello World

5 * 3

Output: 15

x = 6 * 4

Observi numărul care a apărut în stânga celulelor de până acum? Are forma: [1]. În colab putem rula celulele de mai multe ori, în orice ordine ne dorim. Acel număr ne indică ultimul indice de rulare corespunzător celulei - util pentru a ne da seama în proiecte mai mari care a fost ultima celulă rulată sau care a fost ordinea în care am rulat anumite celule.

Variabilele definite într-o celulă pot fi utilizate în orice celulă apelată ulterior, indiferent de poziția acesteia. Folosește săgeata în sus de pe celula de mai jos pentru a o muta deasupra atribuirii și observă rezultatul rulării acesteia:

x

Output: 24

Până acum am aflat că toate variabilele definite liber sunt globale. Rulări repetate neintenționate pot duce la confuzie în cod. Uneori aceasta poate fi rezolvată prin resetarea tuturor variabilelor globale:

%reset -f

Observi operatorul % pus la începutul liniei? Acesta anunță că vrem să folosim o funcție magică. Puteți afla mai multe detalii despre aceste funcții dacă apelați celula de mai jos:

%magic

O altă modalitate de a interacționa cu environmentul este cu ajutorul comenzii !. O putem folosi atât pentru a accesa poziția noastră în cloud cât și pentru a instala diverse pachete cu !pip install <librărie>.

!ls

Output: sample_data

Ce este sample_data?

În stânga paginii ai un buton de files care îți arată unde te afli. Atenție: Nu apăsa pe .. din directorul default, dar dacă o faci din greșeală îți poți accesa fișierele apăsând pe /content.

Directorul sample_data conține o serie de fișiere cu care te poți juca până te familiarizezi cu Colab. Citește fișierul README.md pentru mai multe detalii despre datele oferite.

Poți folosi drag & drop sau butonul de add files pentru a adăuga un fișier în sesiunea curentă. Dacă vrei să păstrezi fișierul pentru mai mult timp (sau să accesezi un fișier deja salvat) te poți conecta la contul tău Google Drive rulând codul de mai jos:

from google.colab import drive
drive.mount('/content/drive/')

Output: Mounted at /content/drive/

De aici poți citi / scrie fișiere în root-ul drive-ului tău personal (rădăcina arborelui de fișiere), într-o locație custom scriind calea până la aceasta de fiecare dată, sau poți seta noul root în locația dorită folosind librăria os:

import os
os.chdir("/content/drive/MyDrive/Cram School")

Notebook-urile Colab pot fi shared cu view access sau cu edit access. Cum ne afectează asta:

  1. Edit access: pe măsură ce modifici celule acestea se modifică automat pentru toate persoanele care au orice fel de drept la acest fișier
  2. View access: poți face modificări direct pe fișier, dar nu se salvează automat. Le poți salva cu Ctrl + S, conectându-te la drive-ul personal folosind codul de mai sus, sau copiind pe drive-ul personal cu ajutorul butonului din headerul paginii: Copy to Drive. În practică toate aceste metode fac exact același lucru.

Pentru a crea celule noi poți folosi butoanele + Code și + Text. Le poți accesa din headerul paginii sau după ce faci hover pe linia dintre 2 celule.

Mai multe resurse despre Colab Notebooks și cum poate fi folosit pentru Data Visualisation și Machine Learning aici: Welcome to Colaboratory

Introducere în Python

Varianta rulabilă a fișierului curent poate fi accesată aici: Introducere în Python

Documentația completă Python poate fi accesată aici: Documentație Python

Informații generale

Toate variabilele din Python sunt considerate obiecte:

my_int = 1
my_float = 2.0
my_string = "my string"
my_bool = True

Putem afisa folosind functia print():

print(my_int)
print('nlp')
print(13)
print('----------------')

Output:

1
nlp
13
----------------

Fiecare obiect are un tip, chiar dacă nu trebuie declarat:

print(f"{my_int} - {type(my_int)}")
print(f"{my_float} - {type(my_float)}")
print(f"{my_string} - {type(my_string)}")
print(f"{my_bool} - {type(my_bool)}")

Output:

1 - <class 'int'>
2.0 - <class 'float'>
my string - <class 'str'>
True - <class 'bool'>

Litera 'f' inaintea unui string formateaza acel string si permite scrierea valorilor unor variabile.

De exemplu: a scrie f"{a} + {b} = {a + b}" este echivalent cu a scrie str(a) + " " + str(b) + " = " + str(a + b)

Unde str() converteste variabilele din tipul lor in string

Sintaxă

Operatorii sunt similari cu cei din majoritatea limbajelor de programare:

a = 5
b = 2

print(f"{a} + {b} = {a + b}")
print(f"{a} - {b} = {a - b}")
print(f"{a} * {b} = {a * b}")
print(f"{a} / {b} = {a / b}")
print(f"{a} % {b} = {a % b}")

Output:

5 + 2 = 7
5 - 2 = 3
5 * 2 = 10
5 / 2 = 2.5
5 % 2 = 1

Avem, în plus, ridicarea la putere și împărțirea întreagă:

print(f"{a} ** {b} = {a ** b}")
print(f"{a} // {b} = {a // b}")

Output:

5 ** 2 = 25
5 // 2 = 2

Indentarea este foarte importantă. În locul acoladelor din C++, aici avem sintagme de forma:

if a % 3 == 0:
  print("Restul 0")

Desigur, putem avea mai multe cazuri:

if a % 3 == 0:
  print("Restul 0")
elif a % 3 == 1:
  print("Restul 1")
else:
  print("Restul 2")

Output: Restul 2

Operații repetitive

Cu număr cunoscut de pași (for)

for i in range(4, 10):
  print(i)

Output:

4
5
6
7
8
9

Observați utilizarea funcției range pentru a stabili intervalul. Aceasta are ca parametri start, end și step (default 1) și iterează de la start până la end - 1.

Pentru a itera descrescător, putem porni de la un start mai mare decât end cu pas negativ (aici -1):

for i in range(10, 4, -1):
  print(i)

Output:

10
9
8
7
6
5

Cu număr necunoscut de pași (while)

i = 4
while i < 10:
  print(i)
  i += 1

Output:

4
5
6
7
8
9

Python nu are definită instrucțiunea do ... while, dar aceasta poate fi simulată:

i = 4
while True:
  print(i)
  i += 1

  if i >= 10:
    break

Output:

4
5
6
7
8
9

Operații cu șiruri de caractere

Șirurile de caractere se scriu între ghilimele sau apostrof (de exemplu "sir").

Putem scrie un șir pe mai multe rânduri daca folosim 3 apostrofuri sau 3 ghilimele la inceput și la finalul lui.

Atenție: dacă am deschis șirul cu apostrof sau ghlimele trebuie să îl închidem cu același tip de semn!

Putem accesa caracterele unui șir direct prin indici. Șirurile sunt immutable, așadar putem accesa un caracter prin indice, dar nu îl putem modifica. Prin urmare, metodele șirurilor nu modifică șirul pe care se aplică ci returnează un șir nou.

Dacă adunăm două șiruri obținem concatenarea lor:

"py" + "thon"

Output: 'python'

Dacă înmulțim un șir cu un număr n, obținem un șir nou în care se repetă șirul inițial de n ori:

"abc" * 5

Output: 'abcabcabcabcabc'

Putem concatena elementele unei liste de șiruri de caractere folosind funcția join:

"->".join(['a', 'b', 'c'])

Output: 'a->b->c'

Metode utile

Eliminarea spațiilor din capete:

sir = " abc "
sir.lstrip()

Output: 'abc '

sir.rstrip()

Output: ' abc'

sir.strip()

Output: 'abc'

Creează o listă cu subșirurile șirului inițial

"ab#cd#efgh#".split("#")

Output: ['ab', 'cd', 'efgh', '']

Operații cu liste

Listele sunt seturi ordonate de elemente și se enumeră între paranteze pătrate.

Dacă adunăm două liste obținem concatenarea lor:

[1,2] + [3,4]

Output: [1, 2, 3, 4]

Dacă înmulțim o listă cu un număr n, obținem o listă nouă în care se repetă șirul inițial de n ori:

[1,2] * 4

Output: [1, 2, 1, 2, 1, 2, 1, 2]

Crearea unei liste

Se enumeră elementele între paranteze drepte, sau se folosește constructorul list care primește un obiect prin care se poate itera (precum un sir de caractere)

l1 = [1,2,3]
l2 = list("abc")
print(l1)
print(l2)

Output:

[1, 2, 3]
['a', 'b', 'c']

Lungimea unei liste

Lungimea unei liste se calculează cu len(lista). De exemplu:

len([10, 5, 1])

Output: 3

Indicii unei liste

Indicii unei liste încep de la 0. Pentru lista l = [10,5,1], l[0] este 10, iar l[2] este 1.

Putem folosi și indici negativi. Indicii negativi îi putem considera pornind de la drepta spre stânga începând cu -1, și tot avansând cu 1 spre stânga.

De exemplu, pentru lista de mai sus, l[-1] este 1, l[-2] este 5 și l[-3] este 10.

Subliste

Pentru a obține o sublistă dintr-o listă putem folosi notația l[inidiceStart:indiceFinal] care va oferi sublista cu elementele din lista inițială cuprinse între pozițiile indiceStart inclusiv și indiceFinal exclusiv.

Dacă dorim un mod de parcurgere a listei diferit de cel implicit, pentru a crea sublista, putem adăuga și parametrul de iterare: l[inidiceStart:indiceFinal:iterator].

Astfel se va porni de la indicele de Start, punând elementul corespunzător în sublistă, si la indiceStart se va aduna apoi iteratorul, generând urmatorul element din sublistă. Procedeul se va repeta până se ajunge la un indice mai mare sau egal cu indicele final (pentru această ultimă valoare care depășește limita dată de indicele final nu se mai generează element în sublistă).

Oricare dintre cele trei argumente ale scrierilor l[inidiceStart:indiceFinal] și l[inidiceStart:indiceFinal:iterator] poate lipsi, caz în care se iau valorile implicite - indiceStart ia valoarea 0, indiceFinal ia lungimea listei și iteratorul devine 1.

Exemple de utilizări ale acestor scrieri:

l = list(range(10))
l

Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

l[-1]

Output: 9

l[2:8]

Output: [2, 3, 4, 5, 6, 7]

l[2:8:2]

Output: [2, 4, 6]

l[-1:-7]

Output: []

l[-1:-7:-1]

Output: [9, 8, 7, 6, 5, 4]

l[8:2:-1]

Output: [8, 7, 6, 5, 4, 3]

l[:5]

Output: [0, 1, 2, 3, 4]

l[4:]

Output: [4, 5, 6, 7, 8, 9]

l[:]

Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

l[::-1]

Output: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Observați că putem folosi și indici negativi, dar dacă vrem o parcurgere de la dreapta la stânga, trebuie să folosim un iterator negativ.

Iterarea printr-o listă

def proceseaza(x):
  return x + 1

Putem itera în mai multe moduri. Operatorul in permite iterarea prin fiecare element al listei fără a cunoaște indicele elementului:

for elem in l:
  proceseaza(elem)

Dacă avem nevoie și de indicele elementului putem parcurge lista folosindu-ne de accesul direct al elementului prin indice - insă nu e recomandată această parcurgere când modificăm lista în acest mod (adăugăm sau ștergem elemente), deoarece trebuie să fim atenți la actualizarea indicelui.

for i in range(len(l)):
  proceseaza(l[i])

Putem itera printr-o listă folosind enumerate(lista) care este un generator prin care obținem pe rând tupluri de forma (indice, lista[indice]) - indicele din listă și elementul corespunzător acestuia.

for i, elem in enumerate(l):
  proceseaza(elem)

Matrici

În Python nu avem în mod direct o structură pentru matrici (printre modulele implicite), în schimb putem simula o matrice printr-o listă de liste.

De exemplu:

l = [[1, 2, 3], [4, 5, 6]]

este o matrice de 2 linii și 3 coloane.

Adăugarea elementelor într-o listă

La finalul listei - metoda append(element):

l = [10, 7, 3, 5]
l.append(2)
print(l)

Output: [10, 7, 3, 5, 2]

La o poziție dată - metoda insert(indice, element):

l = [10, 7, 3, 5]
l.insert(1, 122)
print(l)

Output: [10, 122, 7, 3, 5]

Ștergerea elementelor dintr-o listă

Se folosește metoda pop(indice) care șterge din listă elementul de pe poziția dată. Utilizarea fără argumente este echivalentă apelării cu indicele -1, așadar va șterge ultimul element.

l = [10, 7, 3, 5]
l.pop(2)
print(l)

Output: [10, 7, 5]

Fără parametri:

l = [10, 7, 3, 5]
l.pop()
print(l)

Output: [10, 7, 3]

Putem șterge un anumit element cu metoda remove(element):

l = [10, 7, 3, 5]
l.remove(7)
print(l)

Output: [10, 3, 5]

Sortarea unei liste

Sortarea unei liste se poate face simplu prin metoda sort():

l = [2, 1, 10, 4, 100, 17, 23]
l.sort()
print(l)

Output: [1, 2, 4, 10, 17, 23, 100]

Sortarea implicită este cea crescătoare. Pentru a sorta descrescător, putem folosi parametrul reverse al funcției sort cu valoarea True:

l = [2, 1, 10, 4, 100, 17, 23]
l.sort(reverse = True)
print(l)

Output: [100, 23, 17, 10, 4, 2, 1]

Putem sorta oricum dorim folosind aceeași funcție. De exemplu, pentru sortarea după ultima cifră a numerelor vom folosi funcția lambda:

l = [2, 1, 10, 4, 100, 17, 23]
l.sort(key = lambda x: x % 10)
print(l)

Output: [10, 100, 1, 2, 23, 4, 17]

Modificarea listei în timpul parcurgerii

Dacă dorim să modificăm o listă (de exemplu să ștergem toate elementele pare) este comună următoarea greșeală:

l = [3, 2, 4, 5, 10, 12]
for i in range(len(l)):
  if l[i] % 2 == 0:
    l.pop(i)
    i -= 1

Output:

1 2
3 10



---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-4-2ff85a8985d8> in <module>
      1 l = [3, 2, 4, 5, 10, 12]
      2 for i in range(len(l)):
----> 3     if l[i] % 2 == 0:
      4         print (i, l[i])
      5         l.pop(i)


IndexError: list index out of range

Vom primi o eroare penru metoda pop() care va da un "IndexError: list index out of range" pentru că i-ul nu este decrementat la ștergerea elementului, ci continuă parcurgerea range-ului.

O altă greșeală:

l = [3, 2, 4, 5, 10, 12]
for elem in l:
    if elem % 2==0:
        l.remove(elem)
print(l)

Output: [3, 4, 5, 12]

Chiar dacă acum programul se oprește, nici acest caz nu este corect deoarece i-ul nu este actualizat și sare peste elemente.

Modul corect. Sunt mai multe moduri prin care putem realiza cerința de mai sus, având grijă să nu sărim elementele. De exemplu, putem folosi while - în felul acesta limita pentru i nu mai e rigidă, precalculată, cum era în cazul lui range:

l = [3, 2, 4, 5, 10, 12]
i = 0
while i < len(l):
    if l[i] % 2 == 0:
        l.pop(i)
        i -= 1
    i += 1
print(l)

Output: [3, 5]

Modul corect și elegant. Folosind comprehensions:

l = [3, 2, 4, 5, 10, 12]
rez = [x for x in l if x % 2 == 1]
print(rez)

Output: [3, 5]

Operații cu dicționare

Dicționarele reprezintă un set de perechi de chei și valori asociate. Cheile sunt unice în dicționar și trebuie să fie de tip immutable (pot fi stringuri, numere, tupluri etc., dar nu pot fi liste).

Pentru a crea un dicționar vid, folosim:

d = {}

Pentru a adăuga chei noi în dicționar, putem pur și simplu să le atribuinduim o valoare. Sintaxa este: dictionar[cheie] = valoare.

d["a"] = 100
d["b"] = 200
d["c"] = 300
print(d)

Output: {'a': 100, 'b': 200, 'c': 300}

Pentru a itera printr-un dicționar putem folosi operatorul in:

for k in d:
    print(k, d[k])

Output:

a 100
b 200
c 300

Pentru a verifica dacă o cheie se găsește într-un dicționar, putem folosi același operator:

print("a" in d)
print(100 in d)

Output:

True
False

sau să folosim metoda items() care returnează o listă cu tupluri de forma (cheie, valoare):

for k, v in d.items():
    print(k, v)

Output:

a 100
b 200
c 300

Pentru a obține lista de chei putem folosi metoda keys() iar pentru lista de valori metoda values().

Mulțimi

Mulțimile reprezintă seturi de elemente neordonate care nu acceptă duplicate.

Crearea unei mulțimi

multime_vida = set()
multime = {2, 3, 10, 8}
print(multime)

Output: {8, 3, 10, 2}

Observați că nu s-a păstrat ordinea din inițializarea mulțimii, fiindcă mulțimile sunt neordonate. Cu toate acestea, ordinea va fi actualizată la primul update sau querry pe mulțime:

multime.add(5)
multime

Output: {2, 3, 5, 8, 10}

Cardinalul unei mulțimi

Pentru a obține cardinalul unei mulțimi se folosește funcția len(). De exemplu:

len({2, 4, 10})

Output: 3

List comprehensions

Au sintaxa de forma:

[expresie for element in obiect_iterabil]

caz in care se va genera o listă cu același număr de elemente precum obiectul iterabil

sau

[expresie for element in obiect_iterabil if conditie]

caz în care va avea în listă doar elementele din obiectul iterabil care îndeplinesc condiția.

Exemple:

l = [2, 7, 5, 23, 10]

Lista cu dublul elementelor lui l:

l1 = [2*x for x in l]
l1

Output: [4, 14, 10, 46, 20]

Lista cu perechile de vecini din l:

l2 = [[l[i], l[i + 1]] for i in range(len(l) - 1)]
l2

Output: [[2, 7], [7, 5], [5, 23], [23, 10]]

Lista produsului cartezian:

l3 = [[x, y] for x in l for y in l]
l3

Output:

[[2, 2],
 [2, 7],
 [2, 5],
 [2, 23],
 [2, 10],
 [7, 2],
 [7, 7],
 [7, 5],
 [7, 23],
 [7, 10],
 [5, 2],
 [5, 7],
 [5, 5],
 [5, 23],
 [5, 10],
 [23, 2],
 [23, 7],
 [23, 5],
 [23, 23],
 [23, 10],
 [10, 2],
 [10, 7],
 [10, 5],
 [10, 23],
 [10, 10]]

Lista elementelor pare:

l4 = [x for x in l if x % 2 == 0]
l4

Output: [2, 10]

Crearea unei matrici folosind comprehensions

Putem folosi un comprehension cu for dublu.

De exemplu dacă dorim o matrice formată doar din 0-uri:

l = [[0] * 5 for _ in range(10)]
print(l)

Output: [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]

Atenție, frecvent se face greșeala următoare:

l = [[0] * 5] * 10
print(l)
l[0][0] = 111
print(l)

Output:

[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
[[111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0]]

Observați cum s-a schimbat primul element în fiecare listă?

Atunci cand apelăm lista * n, unde n e un număr natural nenul, se copiază elemente din listă de n ori. Problema apare când avem o listă de obiecte, deoarece se copiază referențele către acele obiecte. Practic am avut [ lista_de_0] * 10, care a dus la o listă cu 10 referințe către aceeași lista de 0-uri, deci când am schimbat primul element din prima listă am văzut modificarea în toate cele 10 liste fiindcă de fapt sunt toate același obiect.

import copy

a = copy.deepcopy(l)
a

Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Operații cu fișiere

Pentru a deschide un fișier folosim metoda open(cale_fisier, mod_deschidere).

De exemplu, pentru a citi fisierul input.txt, putem folosi:

f = open("input.txt", "r")
sir = f.read()

caz în care vom avea în șir tot conținutul fișierului.

O altă variantă este să folosim metoda readlines() care returnează o listă de stringuri cu liniile fișierului.

Pentru a scrie într-un fișier putem deschide fișierul cu "w" pentru a fi suprascris, sau cu "a" pentru a adăuga la final de fișier.

Pentru a scrie într-un fișier putem folosi metoda write().

Clase

Pentru a defini o clasă folosim cuvântul cheie class, urmat de numele clasei.

Clasele au o metodă specială prin care se construiesc instanțele clasei, numită __init__. În această funcție vom trimite argumentele necesare pentru a completa proprietățile noii instanțe a clasei.

Orice metodă proprie instanțelor are ca prim parametru chiar o referință către instanță respectivă (metoda __init__ nu face excepție). De obicei acest prim parametru este numit self, dar nu este obligatoriu. Parametrul self nu va avea un argument corespunzător în apelul metodei, practic metodele se apelează cu argumente corespunzătoare tuturor parametrilor (mai puțin self).

Proprietățile de instanță nu se definesc direct în clasă, ci sunt create în constructor atunci când le folosim numele prima oară. De exemplu, putem face o inițializare: self.proprietate = valoare.

class Cls:
  def __init__(self, aa, bb):
    self.a = aa
    self.b = bb
  def incrementeaza_a(self):
    self.a += 1


c1 = Cls(2,5)
c1.incrementeaza_a()
print(c1.a)

Output: 3

Observați cum în exemplul de mai jos, la crearea instanței c1, s-au dat valori doar pentru parametrii aa și bb din __init__, primul parametru fiind self, pentru care nu se oferă argument. Metoda incrementeaza_a() nu se apelează cu argumente deoarece are ca unic parametru self (adică instanța).

Pentru a asigura o afișare frumoasă a elementelor dintr-o clasă putem defini metodele __str__ și __repr__, ambele având ca rol returnarea unui string reprezentativ pentru instanța curentă.

Când apelăm print(obiect) se scrie ce returnează __str__. Când apelăm print(lista_de_obiecte) se afișează lista aplicând __repr__ pentru fiecare obiect.

Dacă apelăm str(obiect) obținem stringul returnat de __str__, iar cu repr(obiect) stringul returnat de __repr__.

class Cls:
  n = 100
  def __init__(self, aa, bb):
    self.a = aa
    self.b = bb

  def __str__(self):
    return "a = {} b = {}".format(self.a, self.b)
  def __repr__(self):
    return "({}, {})".format(self.a, self.b)
c1 = Cls(2, 5)
print(c1)

Output: a=2 b=5

print(str(c1))

Output: a=2 b=5

print(repr(c1))

Output: (2, 5)

c2 = Cls(3, 3)
c3 = Cls(4, 1)
print([c1, c2, c3])

Output: [(2, 5), (3, 3), (4, 1)]

Operatori

Se pot defini operatori pentru elementele unei clase. De exemplu, putem defini operatori de egalitate sau care să determine dacă un element se află într-o relație de ordine față de altul.

Pentru clasa de mai sus am putea considera că elementele se ordonează întâi după proprietatea a, apoi după b. Avem operatorii:

  • __eq__ operația de egalitate
  • __lt__ operatorul "<"
  • __le__ operatorul "<="
  • __gt__ operatorul ">"
  • __ge__ operatorul pentru ">="
class Cls:
  def __init__(self, aa, bb):
    self.a = aa
    self.b = bb
  def __eq__(self, elem):
    return (self.a, self.b) == (elem.a, elem.b)
  def __lt__(self, elem):
    return (self.a, self.b) < (elem.a, elem.b)
  def __le__(self, elem):
    return (self.a, self.b) <= (elem.a, elem.b)
  def __gt__(self, elem):
    return (self.a, self.b) > (elem.a, elem.b)
  def __ge__(self, elem):
    return (self.a, self.b) >= (elem.a, elem.b)

c1 = Cls(2,5)
c2 = Cls(2,5)
c3 = Cls(2,4)

Output:

print(c1 < c3)

Output: False

print(c1 >= c3)

Output: True

Proprietăți și metode de clasă

Proprietățile clasei se definesc direct în clasă, de obicei la început.

Funcțiile obișnuite se numesc metode de instanțiere.

Metodele de clasă vor fi precedate de decoratorul @classmethod.

Instanțele pot accesa proprietăți și metode de clasă.

În momentul în care o instanță încearcă să modifice o proprietate de clasă prin scrierea instanta.proprietate_clasa = valoare, nu se va modifica proprietatea clasei ci se va crea o proprietate a instanței cu acelasi nume. Din acel moment instanța nu mai poate accesa (în mod direct) decât propia proprietate cu acel nume:

class Cls:
  n = 100
  def __init__(self, aa, bb):
    self.a = aa
    self.b = bb

  @classmethod
  def incrementeaza_n(cls):
    cls.n += 1
print(Cls.n)

Output: 100

c1 = Cls(2, 5)
print(c1.n)

Output: 100

c1.n = 17
print(c1.n)
print(Cls.n)

Output:

17
100
c2 = Cls(2,5)
print(c2.n)
print(Cls.n)

Output:

100
100
Cls.incrementeaza_n()
print(c1.n)
print(Cls.n)

Output:

17
101
c1.incrementeaza_n()
print(c1.n)
print(c2.n)
print(Cls.n)

Output:

17
102
102

Modulul math

Modulul math este folosit pentru funcții matematice utilizate frecvent:

  • floor(numar) - returneaza partea întreagă inferioară
  • ceil(numar) - returnează partea întreagă superioară
  • sqrt(numar) - returnează rădăcina pătrată a numărului
  • funcții trigonometrice: sin(numar), cos(numar) etc.
import math

math.sqrt(8)

Output: 2.8284271247461903

Modulul time

Uneori avem nevoie să calculăm cât a durat o anumită zonă de cod. Pentru asta putem folosi funcția time() din modulul time. Exemplu:

import time

t1 = time.time()
# zona de cod pentru care dorim sa calculam timpul
t2 = time.time()
print(t2 - t1)  # in secunde

Output: 2.5987625122070312e-05

Cum Facem un Proiect?

Colab este un instrument util și ușor de urmărit pentru proiecte mici sau laboratoare, dar atunci când avem de făcut un proiect mai complex ne poate ajuta mai mult să ne mutăm pe un IDE local, caz în care e bine să respectăm anumite convenții.

1. Modularizare

Cel mai important lucru într-un proiect complex este modularizarea. Cu cât avem mai multe funcționalități cu atât devine mai greu de urmărit un proiect care se întinde mult în același fișier sau care nu are suficiente clase și funcții. Așadar ne vom folosi de următoarele elemente:

  • virtual environment — un loc magic unde se instalează toate librăriile necesare proiectului curent, fără a ne încărca memoria laptopului / calculatorului. Putem să ne creăm un virtual environment manual rulând în consolă:
pip install virtualenv  # instalăm virtualenv
python -m venv env      # creăm folderul în care vom salva datele
source env/bin/activate # activăm environmentul
  • clase — dacă vrem să repetăm o funcționalitate, ne e mult mai ușor să o declarăm direct într-o clasă, împreună cu funcțiile aferente. În general ne dorim să facem o clasă pentru fiecare componentă cu rol diferit (ex: embedding, antrenare).
  • fișiere — pentru a parcurge și mai ușor codul ne ajută să îl împărțim în multe fișiere în funcție de funcționalitate. O împărțire intuitivă ar fi să avem fișiere diferite pentru clase diferite plus un fișier main care le apelează.

2. Readability

Dacă plănuim să îi arătăm cuiva codul vreodată sau ne așteptăm să îl recitim în câteva luni / câțiva ani este destul de important să poată fi citit fără a necesita alte explicații. Din fericire nu ne trebuie o mașină a timpului dacă respectăm următoarele:

  • denumiri competente pentru clase, funcții, variabile etc. — ce trebuie să avem în vedere aici este "Cât i-ar lua unui coleg oarecare să înțeleagă pe cont propriu ce se întâmplă?". Putem merge chiar mai departe și să începem fiecare funcție cu o secțiune de comentarii unde explicăm ce face funcția în câteva cuvinte, ce fac parametrii și ce returnăm la final, de exemplu:
def maxim(a, b, c):
    """ Calculează maximul între 3 numere.
    
    :param a: o variabilă de tip int
    :param b: o variabilă de tip long
    :param c: o variabilă de tip long long
    :return: elementul cu valoarea cea mai mare
    """
    return max(a, b, c)
  • README.md — un fișier în care scriem un scurt overview al proiectului curent, ce clase apar în fiecare fișier și cum sunt legate între ele, ce structură are proiectul și ce comenzi trebuie rulate pentru a-l porni.
  • requirements.txt — un fișier care conține lista librăriilor utilizate în proiect împreună cu versiunea pe care am lucrat. Acesta poate fi extras automat folosind comanda:
py -m pip freeze > requirements.txt

3. Istoric

Doar pentru că ne-am mutat în local pentru un proiect mai complex nu înseamnă că trebuie să rămână local. În continuare putem lucra împreună cu mai mulți oameni pe același proiect online folosind git (Tutorial Git).

  • git — un instrument foarte puternic pentru a păstra istoricul tuturor modificărilor și pentru a crea un portofoliu online cu proiectele făcute până acum. Există 2 site-uri foarte cunoscute pe care le puteți utiliza: Tutorial GitHub și Tutorial GitLab. Ambele sunt la fel de bune, alegerea ține de preferința personală.
  • .gitignore — un fișier care conține lista folderelor și fișierelor pe care nu vrem să le salvăm în online. Acesta va include o serie de foldere cu configurări locale (__pycache__/, .idea/), variabile de sistem (env/) chei secrete (secret_key.txt) sau alte fișiere pe care nu vrem să le publicăm din diverse motive.

Model de cod complet

Algoritmul Minimax

img.png

Valorile poziţiilor de la ultimul nivel sunt determinate de către funcţia de utilitate şi se numesc valori statice. Valorile minimax ale nodurilor interne sunt calculate în mod dinamic, în manieră bottom-up, nivel cu nivel, până când este atins nodul-rădăcină. Valoarea rezultată, corespunzătoare acestuia, este 4 şi, prin urmare, cea mai bună mutare a lui MAX din poziţia a este a-b. Cel mai bun răspuns al lui MIN este b-d. Această secvenţă a jocului poartă denumirea de variaţie principală. Ea defineşte jocul optim de tip minimax pentru ambele părţi. Se observă că valoarea poziţiilor de-a lungul variaţiei principale nu variază. Prin urmare, mutările corecte sunt cele care conservă valoarea jocului.

In practica, nu vom genera intreg arborele de cautare, ci il vom extinde doar pana la o adancime maxima data.

Ideea este de a evalua aceste poziţii terminale ale căutării, fără a mai căuta dincolo de ele, cu scopul de a face economie de timp. Aceste estimări se propagă apoi în sus de-a lungul arborelui, conform principiului Minimax. Mutarea care conduce de la poziţia iniţială, nodul-rădăcină, la cel mai promiţător succesor al său (conform acestor evaluări) este apoi efectuată în cadrul jocului.

In algoritm vom inlocui functia de utilitate (aplicata doar starilor terminale de joc) cu functia de evaluare (care se poate aplica oricarei stari de joc, nu neaparat una terminala).

O funcţie de evaluare întoarce o estimaţie, realizată dintr-o poziţie dată, a utilităţii aşteptate a jocului. Ea are la bază evaluarea şanselor de câştigare a jocului de către fiecare dintre părţi, pe baza calculării caracteristicilor unei poziţii. Performanţa unui program referitor la jocuri este extrem de dependentă de calitatea funcţiei de evaluare utilizate.

Funcţia de evaluare trebuie să îndeplinească anumite condiţii evidente: ea trebuie să concorde cu funcţia de utilitate în ceea ce priveşte stările terminale, calculele efectuate nu trebuie să dureze prea mult şi ea trebuie să reflecte în mod corect şansele efective de câştig.

Pași

  1. Generează întregul arbore de joc, până la stările terminale.
  2. Aplică funcţia de utilitate fiecărei stări terminale pentru a obţine valoarea corespunzătoare stării.
  3. Deplasează-te înapoi în arbore, de la nodurile-frunze spre nodul-rădăcină, determinând, corespunzător fiecărui nivel al arborelui, valorile care reprezintă utilitatea nodurilor aflate la acel nivel. Propagarea acestor valori la niveluri anterioare se face prin intermediul nodurilor- părinte succesive, conform următoarei reguli:
    • dacă starea-părinte este un nod de tip MAX, atribuie-i maximul dintre valorile avute de fiii săi;
    • dacă starea-părinte este un nod de tip MIN, atribuie-i minimul dintre valorile avute de fiii săi.
  4. Ajuns în nodul-rădăcină, alege pentru MAX acea mutare care conduce la valoarea maximă.

Observație: Decizia luată la pasul 4 al algoritmului se numeşte decizia minimax, întrucât ea maximizează utilitatea, în ipoteza că oponentul joacă perfect cu scopul de a o minimiza.

Pseudocod

function minimax(node, depth, maximizingPlayer) is
    if depth = 0 or node is a terminal node then
        return the heuristic value of node
    if maximizingPlayer then
        value := −∞
        for each child of node do
            value := max(value, minimax(child, depth − 1, FALSE))
        return value
    else (* minimizing player *)
        value := +∞
        for each child of node do
            value := min(value, minimax(child, depth − 1, TRUE))
        return value

(* Initial call *)
minimax(origin, depth, TRUE)

Algoritmul Alpha-beta

alpha-beta.png

Alpha-Beta este o implementare eficientă a algoritmului Minimax.

Tehnica pe care o vom examina, în cele ce urmează, este numită în literatura de specialitate alpha-beta prunning (“alpha-beta retezare”). Atunci când este aplicată unui arbore de tip minimax standard, ea va întoarce aceeaşi mutare pe care ar furniza-o şi Algoritmul Minimax, dar într-un timp mai scurt, întrucât realizează o retezare a unor ramuri ale arborelui care nu pot influenţa decizia finală.

Principiul general al acestei tehnici constă în a considera un nod oarecare n al arborelui, astfel încât jucătorul poate alege să facă o mutare la acel nod. Dacă acelaşi jucător dispune de o alegere mai avantajoasă, m, fie la nivelul nodului părinte al lui n, fie în orice punct de decizie aflat mai sus în arbore, atunci n nu va fi niciodată atins în timpul jocului. Prin urmare, de îndată ce, în urma examinării unora dintre descendenţii nodului n, ajungem să deţinem suficientă informaţie relativ la acesta, îl putem înlătura.

Ideea tehnicii de alpha-beta retezare este aceea de a găsi o mutare “suficient de bună”, nu neapărat cea mai bună, dar suficient de bună pentru a se lua decizia corectă. Această idee poate fi formalizată prin introducerea a două limite, alpha şi beta, reprezentând limitări ale valorii de tip minimax corespunzătoare unui nod intern.

Semnificaţia acestor limite este următoarea: alpha este valoarea minimă pe care este deja garantat că o va obţine MAX, iar beta este valoarea maximă pe care MAX poate spera să o atingă. Din punctul de vedere al jucătorului MIN, beta este valoarea cea mai nefavorabilă pentru MIN pe care acesta o va atinge. Prin urmare, valoarea efectivă care va fi găsită se află între alpha şi beta.

Valoarea alpha, asociată nodurilor de tip MAX, nu poate niciodată să descrească, iar valoarea beta, asociată nodurilor de tip MIN, nu poate niciodată să crească.

Cele două reguli pentru încheierea căutării, bazată pe valori alpha şi beta, pot fi formulate după cum urmează:

  • Căutarea poate fi oprită dedesubtul oricărui nod de tip MIN care are o valoare beta mai mică sau egală cu valoarea alpha a oricăruia dintre strămoşii săi de tip MAX.
  • Cautarea poate fi oprită dedesubtul oricărui nod de tip MAX care are o valoare alpha mai mare sau egală cu valoarea beta a oricăruia dintre strămoşii săi de tip MIN.

Dacă, referitor la o poziţie, se arată că valoarea corespunzătoare ei se află în afara intervalului alpha-beta, atunci această informaţie este suficientă pentru a şti că poziţia respectivă nu se află de-a lungul variaţiei principale, chiar dacă nu este cunoscută valoarea exactă corespunzătoare ei. Cunoaşterea valorii exacte a unei poziţii este necesară numai atunci când această valoare se află între alpha şi beta.

Din punct de vedere formal, putem defini o valoare de tip minimax a unui nod intern, P, V( P, alpha, beta ), ca fiind “suficient de bună” dacă satisface următoarele cerinţe:

  • V( P, alpha, beta ) < alpha, dacă V( P ) < alpha
  • V( P, alpha, beta ) = V( P ), dacă alpha ≤ V( P ) ≤ beta
  • V( P, alpha, beta ) > beta, dacă V ( P ) > beta, unde prin V( P ) am notat valoarea de tip minimax corespunzătoare unui nod intern.

Valoarea exactă a unui nod-rădăcină P poate fi întotdeauna calculată prin setarea limitelor după cum urmează: V( P, -∞, +∞ ) = V( P ).

Exemplu de arbore de cautare pt alg Alpha-Beta:

Aşa cum se vede în figură, unele dintre valorile de tip minimax ale nodurilor interne sunt aproximative. Totuşi, aceste aproximări sunt suficiente pentru a se determina în mod exact valoarea rădăcinii. Se observă că Algoritmul Alpha-Beta reduce complexitatea căutării de la 8 evaluări statice la numai 5 evaluări de acest tip.

Pași

Începe din poziţia a. 2. Mutare la b. 3. Mutare la d. 4. Alege valoarea maximă a succesorilor lui d, ceea ce conduce la V( d ) = 4. 5. Întoarce-te în nodul b şi execută o mutare de aici la e. 6. Ia în consideraţie primul succesor al lui e a cărui valoare este 5. În acest moment, MAX, a cărui mutare urmează, are garantată, aflându-se în poziţia e, cel puţin valoarea 5, indiferent care ar fi celelalte alternative plecând din e. Această informaţie este suficientă pentru ca MIN să realizeze că, la nodul b, alternativa e este inferioară alternativei d. Această concluzie poate fi trasă fără a cunoaşte valoarea exactă a lui e. Pe această bază, cel de-al doilea succesor al lui e poate fi neglijat, iar nodului e i se poate atribui valoarea aproximativă 5.

Căutarea de tip alpha-beta retează nodurile figurate în mod discontinuu. Ca rezultat, câteva dintre valorile intermediare nu sunt exacte (nodurile c, e), dar aproximările făcute sunt suficiente pentru a determina atât valoarea corespunzătoare rădăcinii, cât şi variaţia principală, în mod exact.

Eficienţa Algoritmului Alpha-Beta depinde de ordinea în care sunt examinaţi succesorii. Este preferabil să fie examinaţi mai întâi succesorii despre care se crede că ar putea fi cei mai buni.

Pseudocod

function alphabeta(node, depth, α, β, maximizingPlayer) is
    if depth = 0 or node is a terminal node then
        return the heuristic value of node
    if maximizingPlayer then
        value := −∞
        for each child of node do
            value := max(value, alphabeta(child, depth − 1, α, β, FALSE))
            α := max(α, value)
            if α ≥ β then
                break (* β cut-off *)
        return value
    else
        value := +∞
        for each child of node do
            value := min(value, alphabeta(child, depth − 1, α, β, TRUE))
            β := min(β, value)
            if α ≥ β then
                break (* α cut-off *)
        return value


(* Initial call *)
alphabeta(origin, depth, −∞, +∞, TRUE)

Python pe Scurt

Varianta rulabilă a fișierului curent poate fi accesată aici: Ziua 1 - Python

Python este un limbaj de programare simplu. Nu folosim ";" la final de instrucțiune, ci terminăm instrucțiunile la final de rând. Nu folosim "{}" pentru a secvenția bucăți de cod, ci le scriem după ":". Ce este foarte important, în schimb, este indentarea. Pentru a declara că o bucată de cod se află în interiorul unei funcții, clase, instrucțiuni etc. aceasta trebuie să fie aliniată cu un tab în plus față de header:

x = 3
print(f"Valoarea variabilei x este: {x}")
if x > 4:
    print("Incorect")
else:
    print("x + 1 este: ", x + 1)

Output:

Valoarea variabilei X este: 3
x + 1 este:  4

Observați afișările. Folosind funcția print putem afișa orice tip de date pus între paranteze. Nu este obligatoriu să afișăm o singură variabilă, putem afișa oricât de multe dacă punem "," între ele. Ce altceva mai puteți observa din această secvență de cod?

...

Toate variabilele în Python sunt obiecte. Asta înseamnă că le puteți atribui orice valoare fără a preciza tipul de date, și chiar să actualizați valoarea unei variabile cu un tip de date diferit fără probleme:

x = 3
y = 2.15
print(x, y)

x = "abc"
print(x)

Output:

3 2.15
abc

Același principiu se aplică și la liste (vectori). Puteți crea o listă cu variabile de tipuri diferite:

l = ['z', 1, "mac-mac", 3.14, []]
l

Output: ['z', 1, 'mac-mac', 3.14, []]

Elementele pot fi accesate direct, sau putem secționa lista raportat la start_pos:end_pos:pas (slice-uri)

Pentru următoarele exemple incercati sa vă dați seama ce o să afișeze înainte de a le apela:

print(l[1:3])
print(l[3:])
print(l[:3])
print(l[1:9:2])
print(l[-2])
print(l[:-1])

Output:

[1, 'mac-mac']
[3.14, []]
['z', 1, 'mac-mac']
[1, 3.14]
3.14
['z', 1, 'mac-mac', 3.14]

EXERCIȚIU

Folosind doar slice-uri, afișează elementele listei date în ordine inversă.

l[::-1]

Output: [[], 3.14, 'mac-mac', 1, 'z']


Majoritatea tipurilor de date comune sunt preimplementate și ușor de folosit. De exemplu, mulțimile:

my_set = set()
my_set.add(3)
print("My first set: ", my_set)

my_second_set = set([2, 6, 3, 2])
print("My second set: ", my_second_set)

print("Set union: ", my_set.union(my_second_set))
print("Set intersection: ", my_set.intersection(my_second_set))

Output:

My first set:  {3}
My second set:  {2, 3, 6}
Set union:  {2, 3, 6}
Set intersection:  {3}

Mai multe funcții cu mulțimi găsiți aici: https://docs.python.org/3/tutorial/datastructures.html#sets

Sau dicționarele:

my_dict = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964
}
print("My first dictionary: ", my_dict)

my_second_dict = dict(name = "John", age = 36)
print("My second dictionary: ", my_second_dict)

print()

my_second_dict["country"] = "Norway"
print("We can easily add new fields: ", my_second_dict)

my_second_dict["country"] = "Italy"
print("Or modify them: ", my_second_dict)

Output:

My first dictionary:  {'brand': 'Ford', 'model': 'Mustang', 'year': 1964}
My second dictionary:  {'name': 'John', 'age': 36}

We can easily add new fields:  {'name': 'John', 'age': 36, 'country': 'Norway'}
Or modify them:  {'name': 'John', 'age': 36, 'country': 'Italy'}

Pentru mai multe funcții pe dicționare: https://docs.python.org/3/tutorial/datastructures.html#dictionaries

Funcții & Clase

Funcțiile se definesc cu ajutorul cuvântului def. Pot avea oricât de mulți parametri, și pot returna oricât de multe valori. Parametrii pot avea valori implicite, dar restricțiile de tip sunt mai mult pentru developer -- nu vor arunca erori dacă nu sunt respectate.

def f(age: int, name = "Bran") -> str:
  pass

f('Age')

Valorile implicite se află mereu la finalul listei de parametri.

Obervați cuvântul cheie pass folosit mai sus. Este o expresie vidă care poate fi utilizată în interiorul unei structuri pentru a nu arunca eroare până o completăm cu instrucțiunile dorite. O putem folosi atunci când vrem să scriem întâi headerele funcțiilor / claselor.

Clasele se definesc cu ajutorul cuvântului cheie class. Ca în orice limbaj, avem variabile statice pe care le putem apela fie la nivel de clasă, fie la nivel de instanță a clasei:

class Clasa:
  x = 10

Clasa.x

Output: 10

instanta = Clasa()
instanta.x

Output: 10

Cu ce seamănă clasele din ce ați mai făcut până acum?

...

Funcțiile din clase încep mereu cu operatorul self -- referință către clasa respectivă. Referința poate avea orice denumire, self este doar o convenție.

class Clasa:
  x = 3

  def f(self, x):
    return self.x, x

instanta = Clasa()
instanta.f(7)

Output: (3, 7)

Observă cum x și self.x sunt variabile complet separate. În Python, parametrii funcțiilor mereu sunt considerați independenți de mediul exterior, în timp ce elementele care încep cu self. sunt specifici instanței clasei respective.

Pe lângă funcțiile normale avem și o serie de funcții implicite:

  • __init__ -> inițializarea clasei (constructor)
  • __str__ -> transformarea clasei în instanță string
  • __lt__ -> definiția operatorului <
  • __le__ -> definiția operatorului <=
  • __gt__ -> definiția operatorului >

Funcțiile pot fi apelate cu denumirea lor, dar și direct, folosind simbolul respectiv.

class Clasa:
  def __init__(self, x):
    self.x = x

  def __str__(self):
    return "Valoarea clasei: " + str(self.x)

instanta = Clasa(5)
print(instanta)

Output: Valoarea clasei: 5

EXERCIȚIU

Creează o clasă cu minim 2 elemente. Creează 2 instanțe pentru noua clasă și afișeaz-o pe cea mai mare în urma comparației dintre ele.

class Clasa:
  def __init__(self, x, y):
    self.x = x
    self.y = y

  def __lt__(self, cls):
    return self.y < cls.y if self.x == cls.x else self.x < cls.x

  def __str__(self):
    return f"x: {self.x}\ny: {self.y}"

instanta1 = Clasa(3, 9)
instanta2 = Clasa(5, 6)

print(max(instanta1, instanta2))

Output:

x: 5
y: 6

Vom descoperi mai multe despre limbaj pe măsură ce lucrăm cu el.

Puteți găsi mai multe exemple aici: https://colab.research.google.com/drive/1Lxed6J79CsWwyrr8vpLugTmS6GMwMA-j?usp=drive_link

Sau în documentația limbajului: https://docs.python.org/3/

Regex

Un RegEx reprezintă o Expresie Regulată (codificarea unei secvențe de caractere). Poate fi folosit pentru a identifica secvențe de caractere într-un șir, pentru a înlocui secvențe sau pentru a separa un șir în funcție de diferite metrici. O secvență de căutare arată așa:

import re

txt = "The rain in Spain stays mainly in the plain"
x = re.search("Spain", txt)

if x:
  print("Cuvântul există în șir")
else:
  print("Cuvântul nu există în șir")

Output: Cuvântul există în șir

Există o serie de reguli și simboluri pe care le putem folosi pentru a descrie secvența de caractere căutată. O să modificăm exemplul original pentru a ne uita la câteva exemple.

Putem pune [.] în pozițiile în care poate fi orice caracter, [^] este caracterul început de șir și [$] este caracterul pentru final de șir.

print(re.findall("Sp.in", txt))
print(re.findall("^The rain in Spain stays mainly in the plain$", txt))

Output:

['Spain']
['The rain in Spain stays mainly in the plain']

Dacă punem [+] după un caracter acela trebuie să apară minim o dată. Dacă punem [*] după un caracter, acela poate să apară de oricâte ori, chiar și 0. Simbolurile pot fi combinate: [.+] înseamnă că putem avea o listă de caractere oricât de lungă.

x = re.findall(".*ai", txt)
print(x)

Output: ['The rain in Spain stays mainly in the plai']

În loc de a folosi [.+] putem folosi secvențe specifice:

  • \d identifică o listă de cifre
  • \D - caractere care nu sunt cifre
  • \s - caracterul spațiu
  • \w - litere mici și mari, cifre și caracterul "_"

Pentru a fi și mai specifici putem folosi seturi:

  • [ar] - cel puțin unul din caracterele "a" și "r" este prezent
  • [a-n] - orice caracter lowercase din intervalul dat
  • [0-9] - putem aplica intervalele și pentru cifre
  • [a-zA-Z] - sau le putem concatena pentru a accepta caractere mai variate
  • [1-3][7-9] - două seturi unul în continuarea celuilalt funcționează ca orice alte simboluri legate. Exemplul dat caută o secvență de 2 cifre, unde prima cifră este în intervalul 1-3, iar a doua în intervalul 7-9

Putem folosi secvențele de mai sus pentru a identifica toate cuvintele care conțin șirul "ai", de exemplu:

x = re.findall("\w*ai\w", txt)
print(x)

Output: ['rain', 'Spain', 'main', 'plain']

Lista completă de reguli și simboluri se află aici: https://docs.python.org/3/library/re.html

EXERCIȚIU

Creează o funcție de preprocesare a textului: funcția va primi un șir de caractere, îl va împărți în cuvinte și va elimina toate caracterele care nu sunt litere. Modifică funcția astfel încât să păstreze cratimele din cuvinte.

txt = "M-am dus la piata - asta voiam? Vom ajunge repede, cred, acasa!"

Lucru cu fișiere

O să ne conectăm întâi la contul de drive ca să accesăm mai ușor fișierele:

from google.colab import drive
drive.mount('/content/drive/')

Output: Mounted at /content/drive/

Stabilim locația "terminalului" în folderul acestui curs:

import os
os.chdir("/content/drive/MyDrive/Cram School")

Ați mai lucrat cu un terminal înainte? Putem apela anumite comenzi de linux dacă punem "!" în față. De exemplu, putem lista toate fișierele din folderul în care ne aflăm:

!ls

Output:

'Curs 1 - Python.ipynb'
'Curs 2 - Supervised Models.ipynb'
'Curs 3 - Preprocessing & Embeddings.ipynb'
'Curs 4 - Unsupervised Models & Ensembles.ipynb'
'Curs 5 - Scaling, Metrici & NLP.ipynb'

EXERCIȚIU

Identifică alte comenzi pe care le poți apela din terminalul colab, fie cu "!" fie cu ajutorul librăriei os:



Pentru a deschide un fișier folosim metoda open(cale_fisier, mod_deschidere).

f = open("input.txt", "r")

Există 4 tipuri de fișiere (mod_deschidere):

  • "r" - citește dintr-un fișier care deja există
  • "w" - scrie într-un fișier, creează fișierul dacă nu există deja
  • "a" - adaugă la informația deja scrisă dintr-un fișier, îl creează dacă nu există deja
  • "x" - creează un fișier, returnează eroare dacă deja există

Există mai multe metode de a citi sau scrie într-un fișier:

  • read() - citește fișierul complet
  • readlines() - citește pe rând câte o linie din fișier
  • write() - scrie în fișier
sir = f.read()
print(sir)

Nu uitați să închideți fișierele deschise!

f.close()

EXERCIȚIU

Creează un fișier cu extensia .csv sau .xls și adaugă în el 3 linii de text. Închide fișierul, deschide-l din nou, citește și afișează informația. Poți folosi orice metode vrei:



Analiză de Date

Varianta rulabilă a fișierului curent poate fi accesată aici: Ziua 2 - Analiză de date

Introducere în Probabilități & Statistică

Varianta rulabilă a fișierului curent poate fi accesată aici: Ziua 3 - Introducere în Probabilități & Statistică

Introducere în Machine Learning

Varianta rulabilă a fișierului curent poate fi accesată aici: Ziua 4 - Introducere în Machine Learning

Introducere în Procesare de Text

Varianta rulabilă a fișierului curent poate fi accesată aici: Ziua 5 - Introducere în Procesare de Text

Metrici & Scalări

Varianta rulabilă a fișierului curent poate fi accesată aici: Ziua 6 - Metrici & Scalări

WordNet

Varianta rulabilă a fișierului curent poate fi accesată aici: Ziua 7 - WordNet

K Nearest Neighbors

Varianta rulabilă a fișierului curent poate fi accesată aici: Ziua 8 - K Nearest Neighbors

Unsupervised Models & Decision Trees

Varianta rulabilă a fișierului curent poate fi accesată aici: Ziua 9 - Unsupervised Models & Decision Trees

Jocuri ca probleme de căutare

Ne referim la jocuri cu 2 jucatori (care pe rand, alternativ, fac cate o mutare), cu informatie completa, adica ambii jucatori cunosc integral starea curenta a jocului si toate mutarile posibile din acea stare (contra-exemplu: jocurile de carti unde fiecare jucator stie doar cartile sale, dar nu si pe ale oponentului sau ordinea cartilor de pe masa, deci are informatie incompleta). Mutarile sunt deterministe, nu includ probabilitati (de exemplu nu depind de aruncarea unui zar). Jocul se incheie cand se ajunge intr-o stare „terminala” conform regulilor jocului. Aceleasi reguli determina care este rezultatul jocului (care jucator a castigat sau daca eventual a fost remiza).

Un joc va fi reprezentat printr-un arbore de joc in care nodurile corespund starilor de joc, iar arcele corespund mutarilor. Radacina arborelui este starea initiala a jocului, iar frunzele arborelui sunt starile terminale ale jocului. Cei doi jucatori vor fi numiti MAX si MIN. Jucatorul MAX este cel care face prima mutare, apoi cei doi jucatori muta alternativ pana se termina jocul. In arborele de joc, fiecare nivel contine mutarile unui anumit jucator: radacina pentru MAX, apoi toti fiii radacinii sunt pentru MIN, apoi nivelul urmator pentru MAX, si tot asa alternand nivelurile. La finalul jocului, se acorda puncte jucatorului castigator (sau penalizari celui care a pierdut). Pentru asta, se va folosi o functie de utilitate, care acorda o valoare numerica rezultatului unui joc (de exemplu 1 pentru castig, -1 pentru pierdere, 0 pentru remiza).

Jucatorul MAX incearca sa castige sau sa-si maximizeze scorul din acel moment. Jucatorul MIN, oponentul sau, incearca sa minimizeze scorul lui MAX. Deci la fiecare pas, jucatorul MIN va alege acea mutare care este cea mai nefavorabila pentru MAX. Jucatorul MAX trebuie sa gaseasca (folosind arborele de joc) o strategie care-l va conduce la castigarea jocului, indiferent de actiunile lui MIN.

In probleme, vom avea jucatorul MAX (calculatorul care isi alege mutarea folosind algoritmul) versus jucatorul MIN (omul care introduce de la tastatura mutarea dorita).

A se vedea algoritmii MinMax și Alpha-beta.

Structura de cod pe care vom lucra poate fi accesată aici: Ziua 10 - Jocuri

Proiect

Alegeți un set de date pe clasificare de text cu maxim 3 clase până pe 19.07.2023. Analizați setul de date, preprocesări, hiperparametri etc. (cum am făcut până acum) și pregătiți o prezentare powerpoint de 5 minute în care explicați ce ați încercat, ce ați observat, ce concluzii ați tras la final (ce funcționează cel mai bine și de ce?).

Puteți alege un set de date de aici: