====== Date de Regresie Sintetice ====== Machine learning se referă la extragerea informațiilor din date. Așa că v-ați putea întreba, ce am putea învăța de la niște date sintetice? Deși s-ar putea să nu ne pese intrinsec de modelele pe care noi înșine le-am introdus într-un model artificial de generare a datelor, astfel de seturi de date sunt totuși utile în scopuri didactice, ajutându-ne să evaluăm proprietățile algoritmilor noștri de învățare și să confirmăm că implementările noastre funcționează conform așteptărilor. De exemplu, dacă creăm date pentru care parametrii corecți sunt cunoscuți //a priori//, atunci putem verifica dacă modelul nostru îi poate recupera într-adevăr. %matplotlib inline from d2l import torch as d2l import torch import random ===== Generarea Setului de Date ===== Pentru acest exemplu, vom lucra în dimensiuni mici pentru concizie. Următorul fragment de cod generează 1000 de exemple cu caracteristici bidimensionale extrase dintr-o distribuție normală standard. Matricea de design rezultată $\mathbf{X}$ aparține $\mathbb{R}^{1000 \times 2}$. Generăm fiecare etichetă prin aplicarea unei funcții liniare de bază (//ground truth//), corupându-le prin zgomot aditiv $\boldsymbol{\epsilon}$, extras independent și identic pentru fiecare exemplu: (**$$\mathbf{y}= \mathbf{X} \mathbf{w} + b + \boldsymbol{\epsilon}.$$**) Pentru comoditate, presupunem că $\boldsymbol{\epsilon}$ este extras dintr-o distribuție normală cu media $\mu= 0$ și deviația standard $\sigma = 0.01$. Rețineți că pentru designul orientat pe obiecte adăugăm codul la metoda ''%%__init__%%'' a unei subclase a ''%%d2l.DataModule%%'' (introdusă în ). Este o practică bună să permiteți setarea oricăror hiperparametri suplimentari. Realizăm acest lucru cu ''%%save_hyperparameters()%%''. Dimensiunea lotului (''%%batch_size%%'') va fi determinată ulterior. class SyntheticRegressionData(d2l.DataModule): #@save """Date sintetice pentru regresia liniară.""" def __init__(self, w, b, noise=0.01, num_train=1000, num_val=1000, batch_size=32): super().__init__() self.save_hyperparameters() n = num_train + num_val self.X = d2l.randn(n, len(w)) noise = d2l.randn(n, 1) * noise self.y = d2l.matmul(self.X, d2l.reshape(w, (-1, 1))) + b + noise Mai jos, setăm parametrii reali la $\mathbf{w} = [2, -3.4]^\top$ și $b = 4.2$. Ulterior, putem verifica parametrii estimați față de aceste valori de bază (//ground truth//). data = SyntheticRegressionData(w=d2l.tensor([2, -3.4]), b=4.2) [**Fiecare rând din ''%%features%%'' constă dintr-un vector în $\mathbb{R}^2$ și fiecare rând din ''%%labels%%'' este un scalar.**] Să aruncăm o privire la prima intrare. print('features:', data.X[0],'\nlabel:', data.y[0]) ===== Citirea Setului de Date ===== Antrenarea modelelor de machine learning necesită adesea treceri multiple printr-un set de date, preluând câte un minilot (minibatch) de exemple deodată. Aceste date sunt apoi utilizate pentru a actualiza modelul. Pentru a ilustra cum funcționează acest lucru, [**implementăm metoda ''%%get_dataloader%%'',**] înregistrând-o în clasa ''%%SyntheticRegressionData%%'' prin intermediul ''%%add_to_class%%'' (introdusă în ). Aceasta (**primește o dimensiune de lot, o matrice de caracteristici și un vector de etichete și generează miniloturi de dimensiune ''%%batch_size%%''.**) Astfel, fiecare minilot constă dintr-un tuplu de caracteristici și etichete. Rețineți că trebuie să fim atenți dacă ne aflăm în modul de antrenare sau de validare: în primul caz, vom dori să citim datele într-o ordine aleatorie, în timp ce pentru cel de-al doilea, capacitatea de a citi datele într-o ordine predefinită poate fi importantă în scopuri de depanare. @d2l.add_to_class(SyntheticRegressionData) def get_dataloader(self, train): if train: indices = list(range(0, self.num_train)) # Exemplele sunt citite în ordine aleatorie random.shuffle(indices) else: indices = list(range(self.num_train, self.num_train+self.num_val)) for i in range(0, len(indices), self.batch_size): Pentru a ne forma o intuiție, să inspectăm primul minilot de date. Fiecare minilot de caracteristici ne furnizează atât dimensiunea sa, cât și dimensionalitatea caracteristicilor de intrare. În mod similar, minilotul nostru de etichete va avea o formă corespunzătoare dată de ''%%batch_size%%''. X, y = next(iter(data.train_dataloader())) print('X shape:', X.shape, '\ny shape:', y.shape) While seemingly innocuous, the invocation of ''%%iter(data.train_dataloader())%%'' illustrates the power of Python’s object-oriented design. Note that we added a method to the ''%%SyntheticRegressionData%%'' class //after// creating the ''%%data%%'' object. Nonetheless, the object benefits from the //ex post facto// addition of functionality to the class. Deși pare inofensivă, invocarea ''%%iter(data.train_dataloader())%%'' ilustrează puterea designului orientat pe obiecte din Python. Rețineți că am adăugat o metodă clasei ''%%SyntheticRegressionData%%'' //după// crearea obiectului ''%%data%%''. Cu toate acestea, obiectul beneficiază de adăugarea //ex post facto// a funcționalității clasei. Pe parcursul iterației obținem miniloturi distincte până când întregul set de date a fost epuizat (încercați acest lucru). Deși iterația implementată mai sus este utilă în scopuri didactice, este ineficientă în moduri care ne-ar putea crea probleme în cazul unor probleme reale. De exemplu, aceasta necesită încărcarea tuturor datelor în memorie și efectuarea multor accese aleatorii la memorie. Iteratorii încorporați, implementați într-un cadru de deep learning (framework), sunt considerabil mai eficienți și pot gestiona surse precum date stocate în fișiere, date primite printr-un stream și date generate sau procesate din mers (on the fly). În continuare, să încercăm să implementăm aceeași metodă utilizând iteratorii încorporați. ===== Implementarea Concisă a Încărcătorului de Date ===== În loc să scriem propriul iterator, putem [**apela API-ul existent într-un framework pentru a încărca date.**] Ca și până acum, avem nevoie de un set de date cu caracteristicile ''%%X%%'' și etichetele ''%%y%%''. Dincolo de asta, setăm ''%%batch_size%%'' în încărcătorul de date încorporat și lăsăm acesta să se ocupe eficient de amestecarea (shuffling) exemplelor. @d2l.add_to_class(d2l.DataModule) #@save def get_tensorloader(self, tensors, train, indices=slice(0, None)): tensors = tuple(a[indices] for a in tensors) dataset = torch.utils.data.TensorDataset(*tensors) return torch.utils.data.DataLoader(dataset, self.batch_size, shuffle=train) @d2l.add_to_class(SyntheticRegressionData) #@save def get_dataloader(self, train): i = slice(0, self.num_train) if train else slice(self.num_train, None) return self.get_tensorloader((self.X, self.y), train, i) Noul încărcător de date se comportă exact ca cel anterior, cu excepția faptului că este mai eficient și are unele funcționalități adăugate. X, y = next(iter(data.train_dataloader())) print('X shape:', X.shape, '\ny shape:', y.shape) De exemplu, încărcătorul de date furnizat de API-ul framework-ului suportă metoda încorporată ''%%__len__%%'', astfel încât să îi putem interoga lungimea, adică numărul de loturi. len(data.train_dataloader()) ===== Rezumat ===== Încărcătoarele de date sunt un mod convenabil de a abstractiza procesul de încărcare și manipulare a datelor. În acest fel, același //algoritm// de machine learning este capabil să proceseze multe tipuri și surse diferite de date fără a fi necesară vreo modificare. Unul dintre aspectele plăcute ale încărcătoarelor de date este că acestea pot fi compuse. De exemplu, am putea încărca imagini și apoi să avem un filtru de postprocesare care să le decupeze sau să le modifice în alte moduri. Ca atare, încărcătoarele de date pot fi utilizate pentru a descrie o întreagă conductă (pipeline) de procesare a datelor. În ceea ce privește modelul în sine, modelul liniar bidimensional este cam cel mai simplu pe care l-am putea întâlni. Acesta ne permite să testăm acuratețea modelelor de regresie fără să ne facem griji cu privire la cantități insuficiente de date sau la un sistem subdeterminat de ecuații. Vom pune acest lucru în practică în secțiunea următoare. ===== Exerciții ===== - Ce se va întâmpla dacă numărul de exemple nu poate fi împărțit exact la dimensiunea lotului? Cum ați schimba acest comportament specificând un argument diferit prin utilizarea API-ului framework-ului? - Presupunem că dorim să generăm un set de date uriaș, unde atât dimensiunea vectorului de parametri ''%%w%%'', cât și numărul de exemple ''%%num_examples%%'' sunt mari. - Ce se întâmplă dacă nu putem păstra toate datele în memorie? - Cum ați amesteca datele dacă acestea sunt păstrate pe disc? Sarcina dumneavoastră este să proiectați un algoritm //eficient// care să nu necesite prea multe citiri sau scrieri aleatorii. Sugestie: [[https://en.wikipedia.org/wiki/Pseudorandom_permutation|generatoarele de permutări pseudorandom]] vă permit să proiectați o re-amestecare fără a fi nevoie să stocați explicit tabelul de permutări ((Naor.Reingold.1999)). - Implementați un generator de date care produce date noi din mers, de fiecare dată când este apelat iteratorul. - Cum ați proiecta un generator de date aleatorii care generează //aceleași// date de fiecare dată când este apelat? [[https://discuss.d2l.ai/t/6663|Discuții]]