Questa pagina raccoglie una serie di consigli utili per evitare gli errori comunemente commessi dai laureandi durante la fase di sviluppo dei progetti di tesi. A corredo di questa pagina, abbiamo creato su GitHub un repository che raccoglie le nostre raccomandazioni riguardo a guide, libri, librerie e template per velocizzare e facilitare il processo di sviluppo.

Le linee guida fornite fanno riferimento a Git come sistema di versionamento del codice (Distributed Version Control System — DVCS) e GitHub come ambiente per la gestione dei progetti.

Inoltre, esse assumono che abbiate già scaricato e installato Git sul vostro sistema. Infine, qualora non aveste alcuna familiarità con l’uso di Git da riga di comando, vi consiglio di leggere le seguente risorsa prima di proseguire.

1. Modello di sviluppo collaborativo

Per sviluppare i progetti, al Collab seguiamo il modello di sviluppo collaborativo denominato Fork & pull. GitHub descrive il modello Fork & pull così:

The fork & pull model lets anyone fork an existing repository and push changes to their personal fork without requiring access be granted to the source repository. The changes must then be pulled into the source repository by the project maintainer. This model […] allows people to work independently without upfront coordination.

Questo modello si oppone all’altro esistente detto shared repository, per il quale tutti i partecipanti al progetto hanno i permessi di modificare il codice dell’unico repository di progetto condiviso tra tutti.

Pertanto, la Figura 1 mostra il worflow adottato per i progetti Collab derivante dalla scelta del modello Fork & Pull. Secondo questo workflow, per ogni progetto esisterà un repository sorgente, appartenente all’organizazione collab-uniba su GitHub, gestito da un collaboratore del Collab (l’integration manager). Ciascuno degli studenti che che vorranno collaborare al medesimo progetto, effettuerà un cosiddetto fork del progetto nel proprio spazio di progetti su GitHub. Questo fork sarà poi “clonato” da ciascuno degli studenti in un repository privato sulla propria macchina.

Ciascuno studente avrà  pieni poteri di modifica solo sul repository clone e il relativo repository pubblic su GitHub (detto origin); invece, non avrà alcun permesso di modifica su quello sorgente appartenente collab-uniba. Per contribuire con una modifica, i cambiamenti dovranno essere inviati all’integration manager il quale, una volta verificati, provvederà a integrarli nel repo sorgente (per maggiordi dettagli sull’integrazione, si veda la sezione 4. Restituire modifiche al progetto sorgente del Collab).

Figura 1. Il workflow distributo adottato per tutti i progetti Collab
Figura 1. Il workflow distributo adottato per tutti i progetti Collab

2. Setup

Come primo passo, occorre registrarsi su GitHub e comunicare il proprio nome utente. Sarete quindi aggiunti al team di sviluppo. L’elenco dei progetti sviluppati dal gruppo Collab è disponibile qui: https://github.com/collab-uniba. Potete consultarlo per scegliere a quale progetto contribuire.

Dopo averlo selezionato, collegatevi alla pagina del progetto su GitHub ed effettuate un fork nel vostro spazio di progetti personali. Per effettuare il fork, occorre premere il tasto in Figura 2. Nell’esempio in figura, l’utente bateman sta effettuando il fork personale del progetto demosistemicollab, appartentente all’organizzazione collab-uniba. L’utente bateman avrà così permessi completi per modificare qualsiasi elemento del fork, mentre su progetto sorgente avrà accesso esclusivamente in lettura.

Figura 2. Effettuare un fork

Dopodiché, bisognerà collegare il fork con una cartella locale della propria macchina di sviluppo, ossia creare il repository clone. Tipicamente, esiste una cartella git nel propria home folder (e.g., C:\Users\bateman\git o /home/fabio/git). Ogni progetto sarà quindi una sottocartella di questa.

Per effettuare il collegamento, aprire la pagina di progetto e copiare l’url come da Figura 3.

Figura 3. Copiare url del fork per creare il clone locale
Figura 3. Copiare url del fork per creare il clone locale

Quindi eseguite:

$ git clone https://github.com/collab-uniba/demosistemicollab.git

Questa operazione creerà la cartella di progetto C:\Users\bateman\git\demosistemicollab

3. Sviluppo

Per avviare l’attività di sviluppo, dalla home page del progetto sorgente, scegliete uno degli issue (i.e., attività) assegnati a voi dall’integration manager. Come da Figura 4, immaginiamo che vi sia stato assegnato l’Issue X.

Figura 4. Selezionare uno degli issue assegnati
Figura 4. Selezionare uno degli issue assegnati

3.1. Struttura del progetto

Qualora il progetto sia iniziato da voi (cioè, non ereditate nessun codice preesistente), sarà vostro compito creare una struttura di progetto adeguata. Essa dovrà ricalcare, con i dovuti accorgimenti a seconda del tipo di progetto e di linguaggio di sviluppo, la struttura riportata di seguito:

/src – source files
/src/tests – unit tests
/lib – required libraries
/doc – text documentation and development notes
/build – where we build (each separate build item within a subfolder here)
/conf – configurations (each config, production, test, developer, etc gets a folder in here, and when building Jars and Wars the correct set is copied across)
/extras – other stuff
/res – resources that should be included within generated Jars, e.g., icons

Si fa presente che tale struttura è una raccomandazione generale e che essa deve essere adattata in base alle convenzioni del linguaggio di sviluppo adottato. Per esempio, nel caso del linguaggio Python, si consiglia di seguire questa guida; nel caso di R, consultare quest’altra guida.

Per progetti più di data science, è consigliabile una strutturara adattata a partire da questo esempio:

├── LICENSE
├── Makefile           <- (Optional, Makefile with commands like `make data` or `make train`)
├── README.md          <- The top-level README for developers using this project.
├── data
│   ├── external       <- Data from third party sources.
│   ├── interim        <- Intermediate data that has been transformed.
│   ├── processed      <- The final, canonical data sets for modeling.
│   └── raw            <- The original, immutable data dump.
│
├── docs               <- A default Sphinx project; see sphinx-doc.org for details
│
├── models             <- Trained and serialized models, model predictions, or model summaries
│
├── notebooks          <- Jupyter notebooks. Naming convention is a number (for ordering),
│                         the creator's initials, and a short `-` delimited description, e.g.
│                         `1.0-jqp-initial-data-exploration`.
│
├── references         <- Data dictionaries, manuals, and all other explanatory materials.
│
├── reports            <- Generated analysis as HTML, PDF, LaTeX, etc.
│   └── figures        <- Generated graphics and figures to be used in reporting
│
├── requirements.txt   <- The requirements file for reproducing the analysis environment, e.g. │ generated with `pip freeze > requirements.txt`
│
├── setup.py           <- (Optionally, make this project pip installable with `pip install -e`)
├── src                <- Source code for use in this project.
│   ├── __init__.py    <- Makes src a Python module
│   │
│   ├── data           <- Scripts to download or generate data
│   │   └── make_dataset.py
│   │
│   ├── features       <- Scripts to turn raw data into features for modeling
│   │   └── build_features.py
│   │
│   ├── models         <- Scripts to train models and then use trained models to make
│   │   │                 predictions
│   │   ├── predict_model.py
│   │   └── train_model.py
│   │
│   └── visualization  <- Scripts to create exploratory and results oriented visualizations
│       └── visualize.py
│
└── tox.ini            <- (Optional, tox file with settings for running tox; see tox.testrun.org)

Un’alternativa per il linguaggio R è disponibile qui.

3.2. Branching

Prima di iniziare a sviluppare il codice, occorre creare un branch (i.e., ramo) di sviluppo, il cui scopo unico è quello di contenere solo ed esclusivamente le modifiche necessarie per completerare l’attività descritta dall’Issue X e nient’altro. Per convenzione, questi sono chiamati topic (o feature) branch e hanno lo stesso nome dell’issue relativa (senza spazi). Il loro ciclo di vita è pari a quello di esistenza dell’issue. Quando questo sarà chiuso/risolto, il topic branch relativo sarà tipicamente cancellato.

$ git branch issuex
$ git checkout issuex

Mentre proseguite lo sviluppo, ricordate di aggiungere le nuove risorse (e.g., file o immagini) da voi create allo spazio di progetto. Potete aggiungere singolarmente, per cartella, o tutti insieme i file nella cartella di progetto che non sono ancora versionati, rispettivamente, come indicato di seguito.

$ git add nomefile
$ git add nomecartella
$ git add -A

I file così aggiunti al controllo di versione entrano a far parte della cosiddetta staging area.

Attenzione! Se ci sono file che non hanno bisogno di essere versionati (e.g., dei file temporanei, risultato di ogni compilazione), questi vanno aggiunti al file .gitignore. Si possono aggiungere nomi di file o cartelle, o anche tipi di file:

Thumbs.db
bin/*
*.cab

3.3. Commit delle modifiche

A ogni incremento significativoatomico della base di codice locale deve corrispondere un’operazione di commit che salva nel repository locale le modifiche apportate.

Per incremento significativo e atomico si intende, per esempio, le modifiche per risolvere un issue oppure l’aggiunta di una funzionalità. Un commit non deve mai unire il contributi diversi insieme (e.g., due fix in un solo commit). Contributi diversi vanno aggiunti al repository tramite commit separate.

Inoltre, ogni commit deve obbligatoriamente essere accompagnato da una descrizione delle modifiche in esso contenute. Tale descrizione è tipicamente breve, pari a una riga di testo. Se non foste in grado di riassumere le modifiche brevemente, allora questo sarebbe indice di eccessiva grandezza del commit. Siete invitati, pertanto, a mantenere i commit piccoli ed autoconsistenti.

Il commit può essere fatto indicando il nome della risorsa da committare oppure usare lo switch -a per aggiungere tutti i file non ancora committati.

$ git commit nuovofile -m "Fix missing try/catch in... "
$ git commit -a -m "Add resources for..."

Per sapere quali file non sono stati ancora committati (pendenti), eseguire:

$ git status
3.3.1. Come scrivere i commenti per una commit

Esistono 7 regole di base da seguire per la scrittura di messaggi di commit (oggetto e corpo della descrizione) significativi:

  1. Separare l’oggetto dal corpo del messaggio con una riga bianca
  2. Limitare la lunghezza massima dell’oggetto a 50 caratteri
  3. Iniziare l’oggetto con una lettera maiuscola
  4. Non terminare l’oggetto con un ‘.’ (punto)
  5. Usare la forma imperativa nel formulare l’oggetto
  6. Limitare lunghezza massima di ciascuna riga a 72 caratteri
  7. Usare il corpo per spiegare il cosa è cambiato, non il perché o il come

Applicando queste 7 raccomandazioni, il vostro messaggio di commit assomiglierà all’esempio seguente:

Summarize changes in around 50 characters or less

More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of the commit and the rest of the text as the body. The
blank line separating the summary from the body is critical (unless
you omit the body entirely); various tools like `log`, `shortlog`
and `rebase` can get confused if you run the two together.

Explain the problem that this commit is solving. Focus on why you
are making this change as opposed to how (the code explains that).
Are there side effects or other unintuitive consequences of this
change? Here's the place to explain them.

Further paragraphs come after blank lines.

 - Bullet points are okay, too

 - Typically a hyphen or asterisk is used for the bullet, preceded
   by a single space, with blank lines in between, but conventions
   vary here

If you use an issue tracker, put references to them at the bottom,
like this:

Resolves: #123
See also: #456, #789

Per una lista completa e una discussione puntuale sulle raccomandazion, consiglio la lettura di questo post [8].

3.3.2. Riscrivere la commit history

In alcuni casi, può esservi utile modificare la storia locale dei commit prima di salvare le modifiche sull’origin o inviare una pull request (si veda sezione 4. Restituire modifiche al progetto sorgente del Collab per maggiori dettagli sulle pull request).

Per esempio immaginate di aver aggiunto al repository due file pippo.py e pluto.py. Dopodiché, committate per errore solo il primo dei due. La commit è incompleta.

$ git add pippo.py pluto.py
$ git commit pippo.py -m "Added new files pippo.py and pluto.py for new feature..."

Per correggerla, potete usare il comando --amend, che corregge l’ultima commit eseguita.

$ git add pluto.py
$ git commit --amend --no-edit

L’opzione --no-edit dice di usare lo stesso commento del commit precedente, senza modifiche. Se volete inserire un nuovo commento, sostituitelo con -m "new message".

Se invece, aveste già scritto (push) i commit sull’origin, allora potete cancellare gli ultimi N push con il comando revert:

$ git revert HEAD~N

Infine, abbiamo a disposizione il comando rebase per modificare un commit che è molto più indietro nella storia, per cui l’opzione --amend diventa inutile. Un rebase permette di effettuare una “pulitura locale” della commit history, trasformando una serie di commit in un solo commit (operazione detta di squashing [7]). Pertanto, il rebase va eseguito solo in locale su feature branch di sviluppo, mai su branch pubblici come il master.

Consideriamo la storia dei commit locali:

$ git log --oneline
7db68c8 line 4
ffbb22a line 3
e0086aa lines 1 2
b61eedf added header file for main
b3ade8a Update README.md
d39a530 Merge pull request #3 from bateman/issuex
9565a20 Updated description in ENG
8b6b8a2 Added cunit std prologue
a55ca0e Added script for auto download
bb5b361 Merge pull request #1 from bateman/master
3816f4d Update README.md
182549a fixed issue 34
97c7e08 Create README.md
fc9905a Initial commit

Supponiamo di voler fondere gli ultimi 3 commit in un unico solo commit. Eseguirò:

$ git rebase -i HEAD~3

Il comando aprirà una finestra dell’editor vim, con un contenuto simile a questo.

pick e0086aa lines 1 2
pick ffbb22a line 3
pick 7db68c8 line 4
#
# Commands:
...

Per fondere i commit, iniziate a scrivere (digitate i) e scegliete (comando pick) il primo commit, mentre usate il comando squash per i successivi due commit che saranno così fusi con i primo. Terminate la sessione di editing (premete esc e poi digitate :w) e vi sarà proposta un’altra schermata che fonderà i tre commenti dei tre commit insieme. Editate il file fino ad evere un unico commento per il commit globale. Terminate la sessione di editing e Git effettuerà lo squashing. Per controllare il risultato, eseguite:

$ git log --oneline
1aca7eb lines 1 2 3 4
b61eedf added header file for main
b3ade8a Update README.md
d39a530 Merge pull request #3 from bateman/issuex
9565a20 Updated description in ENG
8b6b8a2 Added cunit std prologue
a55ca0e Added script for auto download
bb5b361 Merge pull request #1 from bateman/master
3816f4d Update README.md
182549a fixed issue 34
97c7e08 Create README.md
fc9905a Initial commit

In seguito allo squashing, il rebase ha “schiacciato” i tre commit in un unico commit: 1aca7eb lines 1 2 3 4.

3.4. Quando una issue è da considerarsi completata?

Partiamo sempre dall’assunto: “Una tesi non testata è una tesi non funzionante…”
… e, quindi, incompleta! Pertanto, perché possiate laurearvi, è necessario consegnare, insieme con il codice eventualmente sviluppato, almeno i test di unità realizzati con framework xUnit e relative estensioni.

In conclusione, una issue è da considerarsi completata quando:

  1. Il codice che aggiunge la funzionalità/risolve un difetto è stato implementato completamente.
  2. Il funzionamento del nuovo prodotto è dimostrabile attraverso l’esecuzione e superamento di uno o più casi test di unità (unit testing), scritti per il framework di tipo xUnit usato dal progetto (variano a seconda del linguaggio utilizzato). I nuovi casi di test vanno aggiunti ed eseguiti con i preesistenti.
  3. Tutto il codice sviluppato (nomi file, variabili, funzioni etc.) è stato rigorosamente in inglese, inclusi i commenti.
  4. Il codice rispetta le condizioni stilistiche definite specificatamente dal progetto. Se non ve ne sono, allora si assume che siano state rispettate quelle tipiche del linguaggio (per esempio, consultate Code conventions in JavaStyle Guide for Python Code; leggete la sezione 6 Convenzioni stilistiche per altre informazioni).

A questo punto, è consigliabile aggiungere il branch issuex, ora presente solo sul repository locale, al repository nel proprio spazio di progetto su GitHub:

$ git push origin issuex

In seguito a un rebase, potrebbe essere necessario “forzare” l’aggiornamento dell’origin che apparirà piu’ aggiornato della storia del repo locale.

$ git push --force origin issuex

3.5. Come aggiornare il clone

Di tanto in tanto, sarà necessario aggiornare il clone sulla vostra macchina con il contenuto del progetto sorgente, ossia del cosiddetto blessed repository del Collab gestito dall’integration manager. Questo, infatti, capiterà quando altre persone stanno collaborando con voi al medesimo progetto. Non aggiornare regolarmente il fork comporterà un sostaziale aumento delle probabilità di fallimento nella creazione di una pull request (si veda sezione successiva), a causa dell’impossibilità di fondere le vostre modifiche con quelle non ancora scaricate dal repo sorgente. Aggiornare regolarmente, permette di scoprire subito nuovi aggiornamenti, di dimensione più piccola, riducendo la possibilità di conflitti e, al contempo, aumentando la semplicità di risoluzione.

Il vostro clone sulla macchina di sviluppo si riferisce al vostro repo su Github con l’etichetta origin. Da riga di comando, eseguite:

$ git remote -v

Il comando restituirà informazioni del tipo:

$ origin  https://github.com/YOUR_NICK/YOUR_PROJ_NAME.git (fetch)
$ origin  https://github.com/YOUR_NICK/YOUR_PROJ_NAME.git (push)

Per poter aggiornare il clone con il contenuto del sorgente, dobbiamo aggiungere quest’ultimo alla lista dei repository remoti (appunto remotes) noti. Per fare questo, eseguite:

$ git remote add remote-label https-remote-url.git

Così facendo, associerete l’etichetta remote-label all’indirizzo del repository remoto. Perciò, nel nostro caso, il comando per associare l’etichetta collab-origin al vero url del progetto d’esempio diventa:

$ git remote add collab-origin https://github.com/collab-uniba/demosistemicollab.git

Rieseguendo il comando per la visualizzazione dei remote, adesso compariranno due remote:

$ git remote -v
collab-origin  https://github.com/collab-uniba/demosistemicollab.git (fetch)
collab-origin  https://github.com/collab-uniba/demosistemicollab.git (push)
origin  https://github.com/YOUR_NICK/YOUR_PROJ_NAME.git (fetch)
origin  https://github.com/YOUR_NICK/YOUR_PROJ_NAME.git (push)

Per aggiornare direttamente, posizionatevi sul branch destinazione (locale) e scaricate gli aggiornamenti (pull) dal branch del sorgente più aggiornato (tipicamente master o develop). Nell’esempio seguente, lavoriamo con master.

$ git fetch collab-origin master
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/collab-uniba/demosistemicollab
* branch master -> FETCH_HEAD

Git has così recuperato i commit e salvati in indice locale temporaneo. I commit sono qui, ma non fanno parte di nessun branch. Per accedere ai commit e alle loro modifiche, useremo il riferimento (o refspec) temporaneo FETCH_HEAD, creato apposta da Git dopo la fetch.

Per rivedere le modifiche in locale, per prima cosa vediamo il diff tra il branch corrente master e i nuovi cambiamenti. Il risultato assomiglierà a:

$ git diff master FETCH_HEAD
index 08cdfb4..cc11a2f 100644
--- a/README.md
+++ b/README.md
@@ -6,4 +6,3 @@ About
This is a test repository, for testing.
-TEST in sub
diff --git a/TEST b/TEST
index 3fb25cd..bc440ae 100644
--- a/TEST
+++ b/TEST
@@ -1 +1,2 @@
BLA
+FOO

Di solito, è il caso di fare delle prove e testare le modifiche prima di effettuare il merge. Per scaricare le modifiche, si effettua il checkout del riferimento FETCH_HEAD creato per noi da Git (ricordate che questo riferimento è temporaneo e un’altra fetch lo modificherà).

$ git checkout FETCH_HEAD
Note: checking out 'FETCH_HEAD'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain the commits you created, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b new_branch_name
HEAD is now at ad38a9b... Updated README

Per elencare i commit e i commenti, eseguite:

$ git log
commit ad38a9b402eac93995902560292697245418a192
Author: Ferry Boender <email@it>
Date: Mon Mar 31 19:37:26 2014 +0200
Updated README
commit c538608863dd9dda276edf5adcad9e0f2ef9f9ed
Author: Ferry Boender <email@it>
Date: Mon Mar 31 19:37:11 2014 +0200
Ammended TEST file
commit f8d3d31ea1195e2cb1c0631d95c2b33c313b60b8
Author: Ferry Boender <email@it>
Date: Mon Mar 31 17:36:23 2014 +0000
Created new branch bugfix

Ora, è tempo di fondere le modifiche analizzate con un branch locale, supponiamo il master.

$ git checkout master
$ git merge FETCH_HEAD
Updating 2f6ecbf..ad38a9b
Fast-forward
README.md | 1 -
TEST | 1 +
2 files changed, 1 insertion(+), 1 deletion(-)
$ git push
Counting objects: 13, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (8/8), 873 bytes | 0 bytes/s, done.
Total 8 (delta 2), reused 1 (delta 1)
To git@https://github.com/collab-uniba/demosistemicollab
2f6ecbf..ad38a9b master -> master

Questo scenario, rappresenta un caso in cui il merge automatico è possibile. Nel caso in cui il merge automatico fallisce, eventuali conflitti andranno risolti in locale sul clone, dopodiché dovranno essere committati in locale e scritti (pushed) sul vostro fork origin.

4. Restituire modifiche al progetto sorgente del Collab

Una volta verificato che il contributo per l’Issue X è davvero completo, è tempo di restituire le modifiche dal vostro fork al progetto sorgente del Collab. Nel gergo, si dice che occorre inviare una pull request (PR). Una PR in questo caso è necessaria perché non avete permessi di scrittura (i.e., push) sul repository sorgente del Collab. Pertanto, una PR invia una richiesta al maintainer del progetto, il quale decide se accettare ed incorporare (i.e., fare un merge) il vostro contributo con il codice esistente.

Per effettuare una PR, collegatevi alla pagina del fork su GitHub, selezionate il tab sulla destra denominato Pull Request e premete il tasto verde “New pull request”, come in Figura 5. Nell’esempio, l’utente bateman sta iniziando una pull request dal suo fork bateman/demosistemicollab verso il repository sorgente collab-uniba/demosistemicollab.

Figure 4. Start a new pull request
Figura 5. Avviare una nuova pull request

Per creare una PR è necessario confrontare il contenuto dei due repository. Come in Figura 6, assicuratevi di scegliere come base del confronto il repository sorgente (a sx) e il vostro fork per il repository sulla dx. Occorrerà selezionare anche i branch opportuni. Come vostro branch selezionate quello creato specificatamente per l’issue al quale avete contribuito, ossia il branch issuex. Per quanto riguarda il repository di collab-uniba, selezionate tipicamente il branch develop; solo qualora questo non esista, usate invece il branch master. Questi due sono i rami di sviluppo principali (anche detti long-running), poiché il loro ciclo di vita è il medesimo del progetto stesso.

Figura 6. Creazione della PR
Figura 6. Creazione della PR

Una volta selezionati repository e branch, la schermata sottostante mostrerà le commit (e i commenti) racchiusi in questa PR, nonchè le modifiche ai file in formato diff. Quindi, per continuare nella procedura di creazione della PR, premere il tasto verdeCreate pull request“.

Come ultimo passo, occorre documentare una PR (Figura 7). Normalmente, una PR contiene almeno queste informazioni (in inglese) [2, 3, 4]:

  • Titolo: sceglietelo conciso ed esplicativo.
  • Descrizione: qui è dove inserirete i dettagli (così sarà inutile inviare un’email al docente che spieghi cosa è stato fatto e come…), ossia
    • il riferimento all’Issue X, con link;
    • lo scopo della PR;
    • come testare le modifiche;
    • qualsiasi nota o avvertimento;
    • link a risorse rilevanti (non assumete che chi vi leggerà abbia la stessa familiarità con le tecnologie/risorse usate);
    • se necessario, inserire anche immagini;
    • per menzionare qualcuno su GitHub, usa una menzione in formato @username.

Inoltre, GitHub è in grado di marcare automaticamente come risolti (o closed) gli issue il cui numero compare nella descrizione di una pull request. Sono ammesse le forme seguenti, ma GitHub è abbastanza smart da gestire anche diverse variazioni sul tema (qui la lista di tutte le combinazioni accettate):

$ git commit nuovofile -m "Fixed issue collab-uniba/repository#3 ... "
$ git commit -a -m "Resolves issue collab-uniba/repository#34..."
...
Figura 7. Descrivere una PR
Figura 7. Descrivere una PR

Attenzione! Qualora il testo in verde sulla destra in Figura 7 “Able to merge. These branches can be automatically merged.” fosse in realtà rosso, significherebbe che il vostro contributo non può essere accettato così com’è in quanto esistono conflitti dovute a modifiche concorrenti su uno o più file. A questo punto, abortite la compilazione della PR, tornate sulla console e aggiornate il codice scaricando gli aggiornamenti dal repository sorgente di collab-uniba. Per fare questo, consultate la sezione 3.5. Come aggiornare il clone.

Attenzione! Il tipico workflow presuppone che prima di richiedere un incontro con il vostro relatore di tesi, abbiamo inviato aperto una o più PR relative ai task assegnati che siete riusciti a completare. Tali PR saranno riviste durante l’incontro e accettate solo se complete e corrette. Altresì, in presenza di errori, occorrerà aggiungere nuovi commit al branch relativo alla issue. Tali errori emergono durante il processo di code review descritto di seguito.

4.1. Code review

Nella sua forma più semplice, una PR è un meccanismo usato da uno sviluppatore per notificare agli altri componenti del team di aver completato un task. Una volta che il proprio topic/feature branch è pronto, lo sviluppatore deposita una PR. Questo consente a tutti coloro che sono coinvolti nello sviluppo di rivedere il codice prima di accettarlo e fonderlo con il branch di destinazione.

Tuttavia, una PR è molto più di una semplice notifica. Essa rappresenta anche la creazione di un forum dedicato alla discussione alla revisione del contributo inviato. Se ci sono dei problemi con i cambiamenti, l’integration manager e gli altri componenti del team possono fornire feedback nel forum o richiedere dei commit di follow-up. Tutte questa attività sono monitorato direttamente all’interno della PR (Figura 8).

Figura 8. PR e attività successive [5]
Figura 8. Attività monitorate in una pull request [5]
4.1.1. La prospettiva del maintainer: testare le modifiche

Queste istruzioni sono rivolte all’integration manager. Come studenti, tipicamente potete saltare questa sottosezione per intero.

Prima di accettare/rifiutare una PR, l’integration manager del progetto sorgente del Collab dovrà testare le modifiche in locale sulla propria macchina di sviluppo [6]. Per fare ciò, se siete uno dei maintainer del progetto Collab, eseguite:

$ git checkout -b CONTRIBUTORNICK-ISSUEBRANCH STARTPOINT
$ git pull git://URL-di-progetto-fork.git ISSUEBRANCH

Per convenzione, creaiamo un nuovo branch con il nome del contributor (bateman, in questo caso)-il nome del branch (issuex, in questo caso), ossia bateman-issuex.

Per quanto concerne il parametro STARTPOINT, questo per i progetti Collab sarà o develop oppure master. Lo start point è il punto di partenza da usare per creare il nuovo branch. Se è omesso, si assume come punto di partenza sempre l’HEAD del repository (i.e., HEAD è l’alias che rappresenta il branch corrente di lavoro). Come prova del nove che non si stanno commettendo errori, lo start point e il branch di destinazione nel repo sorgente Collab indicato nella PR dovranno essere per forza di cose coincidenti. Nel nostro caso specifico, le istruzioni diventano:

$ git checkout -b bateman-issuex master
$ git pull git://github.com/bateman/demosistemicollab.git issuex

Ora le modifiche sono verificabili in locale. Il branch bateman-issuex può essere cancellato subito dopo.

$ git branch -D bateman-issuex
4.1.2. La prospettiva del maintainer: cherry-picking

Queste istruzioni sono rivolte all’integration manager. Come studenti, tipicamente potete saltare questa sottosezione per intero.

A volte ci si trova nella situazione di dover fondere solo dei commit specifici di un feature branch in un altro. Per esempio, consideriamo questo repository:

dd2e86 - 946992 - 9143a9 - a6fd86 - 5a6057 [master]
           \
            76cada - 62ecb3 - b886a0 [feature]

Supponiamo che il contenuto del commit 62ecb3 del branch feature contenga un bug fix o codice da incorporare subito nel branch master. Tuttavia, si vuole solo incorporare solo il commit 62ecb3 ma non il resto presente nel branch feature. Per far ciò, basta spostarsi sul master branch ed eseguire:

$ git checkout master
$ git cherry-pick 62ecb3

Le modifiche nel commit 62ecb3 saranno applicate e committate (come un nuovo commit) nel master. Il comando cherry-pick si comporta come il merge: se non riesce ad applicare le modifiche,  lascia a voi il compito di risolvere manualmente i conflitti.

4.1.3. Aggiungere commit alla PR già inviata

In alcuni casi, non sarà possibile accettare la PR inviata così com’è. Vi sarà richiesto, invece, di effettuare alcune modifiche. Le ulteriori modifiche, eseguite da voi in locale, dovranno quindi essere aggiunte come ulteriori commit alla PR già inviata.

Per fare questo sarà sufficiente committare ed effettuare il push delle modifiche sullo stesso branch issuex usato per creare inizialmente la PR: così facendo, la pull request sarà automaticamente aggiornata con i commit aggiuntivi [6].

4.1.4. Pull Request accettata

Una volta che la PR è accettata, siete pronti a incorporare i cambi anche nel clone e nel suo origin. Assumendo di aver inviato la PR dal branch issuex del fork al branch master del repository sorgente, da console posizionatevi sul branch master del clone, ed eseguite il merge con il branch issuex; dopodiché rimuovetelo sia in locale sia in remoto.

$ git checkout master
$ git merge issuex
$ git push origin master
$ git branch -d issuex
$ git push origin :issuex

4.2. Tagging delle release

Come tutti i VCS, anche Git consente di taggare [14] punti specifici della storia di revisione come importanti. E’ pertanto tipico usare questa funzionalità per marcare le release signficative (e.g., v1.0v2.0, …) Per creare un tag:

$ git tag -a v1.4 -m "my version 1.4"
$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date:   Sat May 3 20:19:12 2014 -0700

my version 1.4

commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    Change version number

Per salvare i tag nel repository è sufficiente eseguire un push, del singolo tag o di tutti quelli esistenti nel clone locale. Ossia

$ git push origin v1.5
Counting objects: 14, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (14/14), 2.05 KiB | 0 bytes/s, done.
Total 14 (delta 3), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
 * [new tag]         v1.5 -> v1.5

oppure

$ git push origin --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 160 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
 * [new tag]         v1.4 -> v1.4
 * [new tag]         v1.4-lw -> v1.4-lw

Invece per rimuovere un tag localmente:

$ git tag -d v1.4-lw
Deleted tag 'v1.4-lw' (was e7d5add)

Per rimuoverlo anche dal remote:

$ git push origin --delete <tagname>

Lo scopo principale del tagging è fornire un modo mnemonico per recuperare una versione importante (milestone) attraverso il checkout:

$ git checkout -b version2 v2.0.0
Switched to a new branch 'version2'

Conviene sempre fare il checkout in un branch se si ritiene di voler fare modifiche. Altrimenti, è possibile eseguire semplicemente il comando

$ git checkout v2.0.0ma ci si ritroverà in uno stato denominato “detached HEAD” che non consentirà di salvare modifiche alla versione taggata.

5. File README.md e LICENSE

Qualora foste voi a creare il progetto, assicuratevi sempre che esistano i file README.md e LICENSE nella radice del progetto. GitHub è in grado di istanziare entrambi i file al momento della creazione del progetto.

Qui trovate il template di file README.md da creare/modificare, personalizzandolo e adattandolo al vostro progetto. Per quanto riguarda la licenza, di default tutti i progetti sviluppati dal Collab sono rilasciati sono licenza open source MIT, la quale che concede l’autorizzazione per riutilizzare il codice previo riconoscimento della paternità.

6. Convenzioni stilistiche

Per ogni progetto, siete obbligati a rispettare le convenzioni stilistiche (coding conventions o coding style) dello specifico linguaggio. Ogni linguaggio definisce il proprio, spesso nella documentazione ufficiale. Per esempio, queste sono le specifiche stilistiche di Python, R, e Java. Basta cercare su Google per trovare gli altri.

Inotlre, gli ambienti di sviluppo, solitamente offrono degli shortcut che aiutano a formattare automaticamente il codice secono le convenzioni standard e sottolineano le violazioni stilistiche nelle nomenclature di variabili, funzioni, classi e file (non ignoratele!).

7. Virtual environment & installazione dipendenze

Queste istruzioni sono specifiche per i linguaggi Python ed R.

I progetti Python ed R prevedono sempre l’installazione di librerie. Queste, però, non vanno mai installate a livello di sistema, ma sempre all’interno di un ambiente virtuale, da creare specificatamente per ogni singolo progetto. Esistono diverse alternative, presentate di seguito.

7.1. Python virtualenv

virtualenv is a tool to create isolated Python environments [9]. virtualenv creates a folder which contains all the necessary executables to use the packages that a Python project would need. Install virtualenvvia pip:

$ pip install virtualenv

Per testare l’installazione:

$ virtualenv --version

Il workflow generale quando si lavora con virtualenv è il seguente:

  1. Creare l’ambiente virtuale per il progetto:
    $ cd project_folder
    $ virtualenv venv

    Il comando virtualenv venv will create a folder in the current directory which will contain the Python executable files, and a copy of the pip library which you can use to install other packages. The name of the virtual environment (in this case, it was venv) can be anything, poiché venv è già ignorato in quanto presente di default in .gitignore.

  1. Per iniziare a usare il virtual environment, esso deve essere prima attivato eseguendo il comando $ source venv/bin/activate
    (in ambiente Windows invece il comando è venv\Scripts\activate).
    The name of the current virtual environment will now appear on the left of the prompt (e.g., (venv)yourcomputer:project_folder username$) to let you know that it’s active.
  2. D’ora in poi è possibile installare qualsiasi pacchetto nell’ambiente virtuale con pip, isolato dall’installazione globale di sistema (si veda inoltre la sezione 7.4. Dipendenze).
  3. Per disattivare l’ambiente e tornare all’eseguibile python di default sul sistema, eseguire il comando $ deactivate.

Per cancellare un  virtual environment è sufficiente cancellare la cartella (in questo caso: $ rm -rf venv).

72. Python conda

Qualora abbiate installato python sul vostro sistema  attraverso conda [10], è possibile creare ambienti virtuali automaticamente, senza installare anche virtualenv. Al contrario di virtualenv, con conda gli ambienti non sono in una sottocartella di progetto ma possono essere elencati da riga di comando attraverso  $ conda env list, conviene usare un nome significativo associato al progetto invece di un semplice venv.

  1. Per creare un ambiente da terminale, eseguire il seguente comando per creare un ambiente virtuale di nome yourprojectvenv con la versione 3.6 di Python:
    $ conda create -n yourprojectvenv python=3.6
  2. Per attivare l’ambiente, eseguite $ conda activate yourprojectvenv. L’evidenza di attivazione sarà vedere il nome dell’ambiente visualizzato nel terminale in questo modo (yourprojectvenv)$
  3. Per installare un pacchetto è possibile usare pip oppure conda attraverso il comando (yourprojectvenv)$ conda install nomepackage(si veda inoltre la sezione 7.4. Dipendenze).
  4. Per disattivare un ambiente, eseguire (yourprojectvenv)$ conda deactivate.
  5. Per cancellare un ambiente: $ conda envo remove --name yourprojectvenv

E’ possibile scaricare in formato PDF un cheatsheet per tenere a portata di mano i comandi più comuni.

7.3. R renv

renv è l’equivalente di virtualenv applicato a R [11]. Il workflow generale quando si lavora con renv è il seguente:

  1. Assicurasi che il package renv sia installato a livello di sistema; altrimenti installarlo eseguendo install.packages("renv").
  2. Se il progetto è nuovo e non preesistente, invocare renv::init() per inizializzare il nuovo environment di progetto con una libreria R privata. Altresì, se il progetto è clonato da un repository, conterrà (a meno di dimenticanze) il file renv.lock. dal quale ripristinare lo stato delle librerie attraverso il comando renv::restore().
  3. Lavorare normalmente con il progetto, installando (e rimuovendo) i  package R come necessario (si veda inoltre la sezione 6.4. Dipendenze).
  4. Invocare renv::snapshot() per salvare lo stato delle librerie di progetto nel lockfile renv.lock.
  5. Continuare a lavorare al progetto, installando, rimuovendo e aggiornando i pacchetti R come necessario.
  6. Invocarerenv::snapshot() di nuovo per salvare l’attuale stato delle librerie di progetto.
  7. Qualora si giungesse a uno stato inconsistente con gli ultimi aggiornamenti, è possibile ripristinare l’ultimo stato salvato eseguendo renv::restore().
  8. Non dimenticarsi di committare nel repository il file renv.lock.

7.4. Dipendenze

7.4.1. Python

Per gestire correttamente le dipendenze di progetto, la best practice raccomandata è quella di avere un file requirements.txt nella radice della cartella di progetto. Questo è un esempio di file requirements.txt:

appnope==0.1.0
backcall==0.1.0
beautifulsoup4==4.6.3
bleach==2.1.4
certifi==2018.8.24
chardet==3.0.4
Click==7.0
cycler==0.10.0
decorator==4.3.0
defusedxml==0.5.0
entrypoints==0.2.3
Flask==1.0.2

Dopo aver verificato che non ci siano conflitti nelle librerie installate attraverso $ pip check, si può creare o aggiornare il file eseguendo:

$ pip freeze > requirements.txt

Tale file va mantenuto aggiornato e committato nel repository GitHub. Qualora stiate clonando un repo per la prima volta, per installare le dipendenze in un colpo solo, eseguire:

$ pip install -r requirements.txt

ll file requirements.txt indica le versioni con le quali il codice è sicuro di funzionare. Per provare se il codice funziona con le ultime versioni aggiornate, è possibile fare un “refresh” del file requirements.txtattraverso il package pip-upgrader. Dopo aver verificato se presente ed eventualmente installato, aggiornate il file requirements.txt alle ultime versioni disponibili, eseguendo $ pip-upgrade requirements.txt. Quindi, eseguite $ pip install -r requirements.txt.

7.4.2. R

Al contrario di Python, R non mette a disposizione package per creare file con le dipendenze. Pertanto, starà a voi creare e/o aggiornare un file equivalente, che per convenzione chiameremo requirements.R e che sarà sempre salvato nella radice della cartella di progetto.

Di seguito è fornito un esempio di come creare tale file:

print("Checking the required packages... won't be re-installed if already present.")

if(!require("renv", quietly = TRUE)){
install.packages("httr", dependencies = c("Imports", "Depends"), repos = "http://cran.mirror.garr.it/mirrors/CRAN/")
}

if(!require("plyr", quietly = TRUE)){
install.packages(c("plyr","dplyr"), dependencies = c("Imports", "Depends"), repos = "http://cran.mirror.garr.it/mirrors/CRAN/")
}

if(!require("stringr", quietly = TRUE)){
install.packages("stringr", dependencies = c("Imports", "Depends"), repos = "http://cran.mirror.garr.it/mirrors/CRAN/")
}

if(!require("jsonlite", quietly = TRUE)){
install.packages("stringr", dependencies = c("Imports", "Depends"), repos = "http://cran.mirror.garr.it/mirrors/CRAN/")
}
...

Notate come la prima libreria sia renv, la quale dovrà essere presente in tutti i progetti, come già indicato nella sezione 7.3 R renv. Per installare le librerie indicate, e in cascata le loro dipendenze, è sufficiente eseguire da terminale $ Rscript requirements.R.

8. Riproducibilità degli esperimenti

Gli studenti che lavorano a una tesi sperimentale si trovano tipicamente a creare degli script che coprono ciascuno dei  passi tipici di un workflow di un progetto di data science (Fig. 9).

Figura 9. I passi tipici di un workflow in un progetto di data science (adattata da [12]).
Come test di accettazione del lavoro sperimentare, gli studenti sono chiamati a dar prova che i propri esperimenti sono riproducibili — tipicamente rieseguendo l’intero workflow sulla macchina del docente o su una Virtual Machine (VM).

8.1. dvc

dvc [12] è uno strumento  compatibile con git, che permette di versionare i dataset sperimentali, automatizzare e rendere riproducibile la pipeline di esperimenti, tracciando al contempo i risultati delle varie prove. Poichè è uno strumento command-line, funziona con qualsiasi linguaggio di programmazione. Gli utenti macOS possono installarlo via homebrew. Gli altri consultino le opzioni di download sul sito. E’ comunque sempre possibile installarlo via conda o pip.

Per familiarizzare con dvc, si consiglia di provare i tutorial interattivi ufficiali, disponibili via browser (i.e., senza installazione locale) su Katacoda.

8.1.1. Inizializzazione

Per creare un progetto DVC, all’interno di un repository Git, biosgna eseguire il comando dvc init:

$ mkdir example-get-started
$ cd example-get-started
$ git init$ dvc init$ git commit -m "Initialize DVC project"

Dopo l’inizializzazione,  DVC crea una nuova cartella .dvc/ per contenere (e nascondere all’utente) tutti i file di configurazione interna e la cache di file e carteller. Per ulteriori informazioni, consultate la guida su See dvc init.

L’ultimo comando, git commit, aggiunge al controllo di versione git le cartelle .dvc/config.dvc/.gitignore.

8.1.2. Configurazione

Una volta installato DVC, prima di poterlo usare, biosgna installare un remote per condividere dati e modelli. . Per semplicità, di seguito configuriamo una cartella locale come remote:

$ dvc remote add -d myremote /tmp/dvc-storage
$ git commit .dvc/config -m "Configure local remote"

Per casi d’uso più complessi, sarà necessario aggiungere tipi di remove non locali. DVC al momento supporta questi tipi di remote:

  • s3: Amazon Simple Storage Service
  • azure: Microsoft Azure Blob Storage
  • gdrive : Google Drive
  • gs: Google Cloud Storage
  • ssh: Secure Shell (requires SFTP)
  • hdfs: Hadoop Distributed File System
  • http: HTTP and HTTPS protocols
  • local: Directory in the local file system

Per aggiungere un remote bisogna specificare sia il tipo (protocollo) che il percorso. Per esempio, per configurare un remote S3  in un cartella chiamata s3remote:

$ dvc remote add -d s3remote s3://mybucket/myproject

Consultate la guida su dvc config e dvc remote per ulteriori dettagli ed esempi.

8.1.3. Aggiunta dati

DVC permette di versionare file di dati,cartelle, modelli, o altri file con risultati con Git ma senza che questi siano effettivamente memorizzati in Git. Per spiegarvi meglio, facciamo un esempio con un ipotetico file data.xml. Per tenere traccia di un file o directory) con DVC si esegue il comando  dvc add:

$ dvc add data/data.xml

Così facendo, DVC conserva l’informazione riguardante i file aggiunti al tracciamento in file di metadati chiamati DVC-file (nel nostro caso data.xml.dvc). Questi DVC-files sono piccoli file in formato testuale e, pertanto, non solo sono leggibili ma anche versionabili sotto Git:

$ git add data/.gitignore data/data.xml.dvc
$ git commit -m "Add raw data to project"

Committando questi  DVC-file di metadati in Git ci permette di tracciare le differenti versioni dei dati/modelli/risultati durante l’evoluzione del progetto in Git nello stesso modo in cui si tracciano le modifiche del codice sorgente. Avrete notato che oltre ad aggiungere il file di metadati data.xml.dvc e la cartella data/ al tracciamento Git, aggiungiamo anche il file .gitignore. Questo perchè con DVC aggiungiamo al tracciamento Git solo il file di metadati contenente il riferimento al remote dove si trovano le risorse versionate da DVC, ma non i file in sé. Tali risorse effettive, per non essere committate per errore nel repository del codice sorgente e consumare spazio, sono pertanto ignorate da Git attraverso il file .gitignore che DVC aggiorna automaticamente ogni volta che una risorsa è posta sotto il suo controllo versione attraverso il comando dvc add.

Per altre informazioni, consultate le pagine di documentazione sul versionamento di dati e modelli con DVC.

8.1.4 Salvataggio dataset

Una volta aggiunti uno o più file di dati , occorre effettuare il pushdal proprio repository al remote configurato:

$ dvc push

Come nel caso di Git, un remote garantisce che i propri file siano conservati al sicuro e che possano essere condivisi con altri. Si noti che il comando dvc push si occupa soltanto di salvare i file in remoto, ma non salva nulla nel repository Git, nel quale vanno archiviati solo i metadati attraverso i comandi git commitgit push.

8.1.5. Recupero dataset

E’ possibile, ovviamente, anche recuperare dati dal remote e salvarli in locale. Per esempio, supponiamo di cancellare per errore il file di dati data.xml. Per recuperarlo:

$ rm -f data/data.xml
$ dvc pull

Il comando dvc pull scarica i dati referenziati nei metadati DVC del progetto. Tipicamente, viene eseguito in fase di creazione o aggiornamento del repository locale, quindi dopo git clonegit pull o git checkout.

In alternativa, per recuperare un singolo file di dati invece di tutti quanti quelli riferiti nel progetto:

$ dvc pull data/data.xml.dvc

I remote DVC e i comandi dvc push edvc pull forniscono le basi del workflow collaborativo per i dati così come i Git , git pushgit pull supportano la collaborazione per il codice. .

8.1.6. Creazione pipeline

Il supporto alle pipeline è la principale differenza tra DVC e gli altri strumenti di version control orientati ai file di dati (e.g., git lfs). Ogni qual volta si esegue dvc run, specificando gli output di un comando (stage) come dipendenze di un altro comando successivo, si ottiene una sequenza (piepeline, appunto) di comandi che, a partire dall’input, generano in output il risultato finale desiderato. Questa è ciò che DVC chiama  data pipeline o dependency graph.

Creiamo un secondo stage di feature extraction (dopo prepare.dvc, per il data cleaning):

$ dvc run -f featurize.dvc \
          -d src/featurization.py -d data/prepared \
          -o data/features \
          python src/featurization.py \
                 data/prepared data/features

Quindi, il terzo stage per il training:

$ dvc run -f train.dvc \
          -d src/train.py -d data/features \
          -o model.pkl \
          python src/train.py data/features model.pkl

Infine, facciamo il commit dei metafile DVC che descrivono la nostra pipeline:

$ git add data/.gitignore .gitignore featurize.dvc train.dvc
$ git commit -m "Create featurization and training stages"$ dvc push

Questo è un esempio di base su come creare una pipeline. Per esempi più complessi, consultate gli esempi sul sito di DVC e il tutorial completo. Consultate inoltre la guida del comando dvc pipelineper ulteriori informazioni.

8.1.7. Visualizzazione pipeline

Dopo aver costruito una pipeline, è possibile visualizzarla e studiarla. DVC consente una visualizazione da terminale, usando l’opzione --ascii mostrata di seguito. Consultate la guida sul comando dvc pipeline show per ulteriori dettagli.

Per visualizzare gli stage di una pipeline a partire dallo step di training:

$ dvc pipeline show --ascii train.dvc
     +-------------------+
     | data/data.xml.dvc |
     +-------------------+
               *
               *
               *
        +-------------+
        | prepare.dvc |
        +-------------+
               *
               *
               *
       +---------------+
       | featurize.dvc |
       +---------------+
               *
               *
               *
         +-----------+
         | train.dvc |
         +-----------+

Per visualizzare i comandi di una pipeline a partire dal training:

$ dvc pipeline show --ascii train.dvc --commands
          +-------------------------------------+
          | python src/prepare.py data/data.xml |
          +-------------------------------------+
                          *
                          *
                          *
   +---------------------------------------------------------+
   | python src/featurization.py data/prepared data/features |
   +---------------------------------------------------------+
                          *
                          *
                          *
          +---------------------------------------------+
          | python src/train.py data/features model.pkl |
          +---------------------------------------------+

Per visualizzare gli output intermedi e finali di una pipeline a partire dal training:

$ dvc pipeline show --ascii train.dvc --outs
          +---------------+
          | data/data.xml |
          +---------------+
                  *
                  *
                  *
          +---------------+
          | data/prepared |
          +---------------+
                  *
                  *
                  *
          +---------------+
          | data/features |
          +---------------+
                  *
                  *
                  *
            +-----------+
            | model.pkl |
            +-----------+
8.1.8. Riproduzione pipeline

Nel creare la pipeline, in pratica abbiamo generato diversi stage file ciascuno dei quali definisce il comando per generare l’output della fase, fino al risultato finale. Ciascuno di questi passi, inoltre, dipende da alcuni file di dati e ovviamente da file contententi script e codice.

Se avete appena clonato da GitHub un progetto che fa uso di DVC, assicuratevi prima di tutto di scaricare localmenete i file dati eseguendo un dvc pull. Quindi, per ripetere un esperimento, è sufficiente eseguire:

$ dvc repro train.dvc

L’esempio assume che l’esperimento sia riprodotto a partire dal training, assumendo che lo o gli step di preparazione dei dati, una volta completati, non vengano rieseguti. Per provare questo comando, potete provare a clonare questo progetto GitHub e testare la riproducibilità della pipeline.

Il meta-file train.dvc descrive quali file di dati e codice usare, e quale comando eseguire per generare in output il modello addestrato. Quindi, dvc repro essentialmente costruisce un grafo di dipendenze a partire da ogni DVC meta file, individua gli stage che hanno subito modifiche nelle dipenze o per i quali mancano degli output ed esegue ricorsivamente i comandi (cioà i nodi nel grafo delle dipendenze/pipeline) a partire dal primo stage che risulta modificato. In questo modo, dvc run e dvc repro forniscono insieme un framework per rendere gli esperimenti riproducibili.

8.1.9. Salvataggio metriche

Come ultimo stage della pipeline, aggiungiamo la valutazione del modello. Il comando dvc metrics fornisce la possibiltà di creare file dove conservare le metriche ad ogni esecuzione e, quindi, confrontare le varie esecuzioni degli esperimenti. Non è richiesto alcun database o cambio nel codice sorgente; il tutto avviene attraverso file (tipicamente di testo) tracciati tramite Git e versionati nel remote storage di DVC:

$ dvc run -f evaluate.dvc \
          -d src/evaluate.py -d model.pkl -d data/features \
          -M auc.metric \
          python src/evaluate.py model.pkl data/features auc.metric

In questo esempio, l’ipotetico file evaluate.py calcola la metrica AUC sul dataset di test, usando il modello in salvato in model.pkl e producendo in output un file di metriche (auc.metric). Ogni file in output può essere marcato di tipo metric usando l’opzione -M in dvc run. Consultate la pagina su dvc metrics per ulteriori dettagli.

Per salvare lo stage di valutazione e i risultati :

$ git add evaluate.dvc auc.metric
$ git commit -m "Create evaluation stage"
$ dvc push

E’ consigliabile assegnare un tag in Git alla commit per generare un checkpoint e eseguire confronti tra l’attuale risultato e quelli futuri (o qualora volessimo fare revert e tornare a questa configurazione in quanto rivelatasi la più performante):

$ git tag -a "baseline-experiment" -m "Baseline experiment evaluation"

Il comando dvc metrics show fornisce infatti un modo semplice per confrontare le varie esecuzioni di un esperimento, consentendo il confronto di file di metriche tra branch diversi del repository Git.

8.1.10. Esecuzione esperimenti

I progetti di data science sono eseguiti attraverso processi inerentemente iterativi. Vi trovere a provare approcci diversi, con configurazioni modificate e vari “fallimenti” prima di raggiungere la qualità cercata. Supponiamo nell’esempio sottostante di modificare la procedura di feature extraction definita nel file sorgente src/featurization.py (ipotizziamo, per usare i bigrammi). Pertanto, sarà necessario rigenerare il modello:

$ vi src/featurization.py  # apply changes to feature extraction
$ dvc repro train.dvc  # regenerate the new model.pkl
$ git commit -am "Reproduce model using bigrams"

Notate che git commit -a mette in staging tutti i cambi prodotti dall’esecuzione di dvc repro prima di committarli in Git. Ora che abbiamo generato e salvato il nuovo modello in model.pkl , per tornare al precedente ci basta eseguire git checkout (per recuperare i file di codice) seguito da dvc checkout (per recuperare i dati e modelli):

$ git checkout baseline-experiment
$ dvc checkout

DVC è progettato per archiviare file di dati di qualsiasi dimensione, senza limiti. Potete consultare la pagina sulla gestione dei dataset di grandi dimensioni per ulteriori informazioni.

8.1.11. Confronto risultati

DVC consente varie iterazioni del progetto e confronto dei risultati sfruttando i tag e branch di Git. Supponiamo di rieseguire l’esperimento dopo le modifiche al modello della sezione precedente. Per rieseguire l’esperimento è sufficiente lanciare dvc repro sullo stage di valutazione:

$ git checkout master
$ dvc checkout
$ dvc repro evaluate.dvc

I comandi git checkout master e dvc checkout assicurano di recuperare, rispettivamente, le ultime versioni del codice e dei dati commitate. Come già detto, dvc repro ri-esegue tutti i comandi necessari per ricostruire e testare il modello (DVC gestirà automaticamente le dipendenze come necessario).

$ git commit -am "Evaluate bigrams model"
$ git tag -a "bigrams-experiment" -m "Bigrams experiment evaluation"

Ora possiamo usare l’opzione -T con il comando dvc metrics show per confrontare le differenze tra l’esperimento “baseline” e quello “bigrams”:

$ dvc metrics show -T

baseline-experiment:
auc.metric: 0.588426
bigrams-experiment:
auc.metric: 0.602818

DVC fornisce support built-in per il tracking dei risultati in formato TXT, JSON, TSV e CSV. Per ulteriori informazioni, consultate la documentazione del comando dvc metrics.

8.2. Computational notebook

Jupyter Notebook è un’applicazione web che consente di editare e condividere documenti contenenti codice, equazioni, visualizzazioni e testo esplicativo. I notebook sono un perfetto esempio di literate computing, ossia consentire agli sviluppatori non solo di eseguire i comandi in modo interattivo, ma anche di archiviare in un document i risultati di questi comandi insieme a figure, formule, tabelle e testo in formato libero. Pertanto, i notebook consentono non solo di instruire il computer a fare qualcosa (attraverso il codice) ma anche di concentrarsi nello spiegare agli altri cosa si vuol far fare al computer.

Gli utilizzi tipici di notebook sono: data exploration and cleaning, analisi statistica, sviluppo di modelli di apprendimento e predizione. Di seguito trovate 9 regole d’oro [13] per l’uso corretto di Jupyter notebook.

  1. Racconta la storia per un pubblico, non per te
    Uno dei principali vantaggi dell’utilizzo dei Jupyter notebook è la possibilità di intercalare testo esplicativo con codice e risultati per creare una “narrazione computazionale.” Piuttosto che tenere solo appunti sporadici, usa un testo esplicativo per raccontare una storia che ha (i) un inizio dove introduci l’argomento della tua analisi, (ii) un mezzo che descrive i tuoi passi e (iii) una fine che interpreta i risultati. Descrivi non solo quello che hai fatto, ma anche perché l’hai fatto, come sono collegati i passaggi e cosa significa tutto. E’ plausibile che la tua storia cambi nel tempo, specialmente quando la tua analisi si evolve, ma assicurati di iniziare a documentare i tuoi pensieri e il processo il più presto possibile.  E’ possibile avere più versione di un notebook, uno interno con note personali, uno più conciso per un “pubblico” esterno. Inoltre, ricorda che il tuo pubblico principale sarà molto probabilmente il tuo futuro sé. La tua spiegazione è abbastanza chiara da poter comprendere e riprodurre l’analisi tra un mese? Se non sarai in grado di ricreare la tua analisi nel prossimo futuro, come potrebbe chiunque altro?
  2. Documenta il processo, non solo i risultati
    L’interattività dei notebook computazionali rende semplice e veloce provare e confrontare diversi approcci o parametri, così rapidamente e facilmente che spesso non riusciamo a documentare quelle indagini interattive nel momento in cui le eseguiamo. Pertanto, assicurati di documentare tutte le tue esplorazioni, anche (o forse soprattutto) quelle che hanno portato a vicoli ciechi. Questi commenti ti aiuteranno a ricordare cosa hai fatto e perché. Puoi sempre rimuovere questi commenti in seguito se trasformi il tuo notebook in una pipeline (vedi la Regola #7) o ti prepari a condividerlo con un pubblico più ampio (Regola #1), quando è preferibile mostrare una presentazione concisa dei risultati. Molti utenti di notebook attendono di aggiungere tale testo esplicativo fino alla fine di un’analisi, dopo aver ottenuto un risultato solido. Non aspettate: a quel punto potreste aver dimenticato perché si è scelto un determinato valore di parametro, da dove si è copiato un blocco di codice o cosa si è trovato interessante riguardo a un risultato intermedio. Mentre il codice necessario per riprodurre l’analisi potrebbe essere acquisito automaticamente nel tuo notebook, il ragionamento e l’intuizione potrebbero non esserlo.
    Sebbene la storia sul tuo notebook cambi nel tempo, devi comunque raccontare una storia fin dall’inizio, anche se non conosci ancora il finale. Pulisci, organizza e annota il tuo notebook dopo ogni esperimento o pezzo significativo di lavoro e fai tutte le pulizie nel notebook. Ad esempio, durante la preparazione alla versione finale, devi evitare di modificare manualmente le figure con gli strumenti esterni e utilizzare invece solo funzioni di librerie presenti nel notebook per produrre versioni di figure e tabelle pronti per la pubblicazione da utilizzare nella tua tesi. Assicurati di includere il tuo nome e le informazioni di contatto per te stesso e un contatto futuro nel tuo laboratorio in grado di rispondere alle domande di base sul codice. Anche documentare la data di inizio e fine dell’analisi è una buona idea e può evidenziare lo sforzo che hai fatto nello sviluppo del notebook.
  3. Suddividi il lavoro in celle diverse per rendere chiari i passi
    I notebook sono un ambiente interattivo, quindi è molto facile scrivere ed eseguire celle a una riga. Questo supporta la sperimentazione, ma può lasciare i tuoi notebook disordinati e pieni di brevi frammenti che sono difficili da seguire. Invece, prova a fare in modo che ogni cella del tuo notebook esegua un passaggio significativo dell’analisi che sia facilmente comprensibile dal codice nella cella o dalla descrizione del markdown circostante. Modularizza il tuo codice per celle ed etichetta le celle con il markdown sopra la cella. Pensa a ciascuna cella come a un paragrafo che ha una funzione o compie un compito (ad esempio, crea un grafico). Evita le celle lunghe. Inserisci documentazione di basso livello nei commenti sul codice. Utilizza le intestazioni di marcatura descrittive per organizzare il notebook in sezioni che possono essere utilizzate per spostarsi facilmente nel document e aggiungete un sommario. Dividi notebook lunghi in una serie di notebook e mantieni un notebook indice di livello superiore con collegamenti ai singoli notebook. L’uso di divisioni chiare di celle e notebook renderà la tua analisi molto più facile da leggere.
  4. Modularizza il codice
    È sempre buona norma evitare il codice duplicato, ma nei notebook è particolarmente facile copiare una cella, modificare alcune righe, incollare il codice risultante in una nuova cella o in un altro notebook ed eseguirlo di nuovo. Questa forma di sperimentazione è utile ma rende i notebook difficili da leggere e quasi impossibili da mantenere se si desidera modificare la funzionalità o correggere un bug nel codice copiato. Invece, avvolgi il codice che stai per copiare e riutilizzare in una funzione, che puoi quindi chiamare da tutte le celle che desideri. Se stai per riutilizzare il codice in altri progetti o notebook, considera di trasformarlo in un modulo, pacchetto o libreria. La modularizzazione non solo consente di risparmiare spazio, supporta la manutenzione e semplifica il debug.
  5. Tieni traccia delle dipendenze
    La riesecuzione della tua analisi in futuro richiederà l’accesso non solo al tuo codice ma anche a qualsiasi modulo o libreria su cui si basava il tuo codice. Come è la migliore pratica nella scienza computazionale, gestisci le tue dipendenze in file come indicato in sezione 7.4 Dipendenze. Questi file possono essere utilizzati da strumenti come Binder o Docker per generare un “contenitore” che altri ricercatori possono utilizzare per riprodurre la tua analisi usando le stesse versioni di ogni modulo e libreria di te. Conduci sempre il tuo lavoro in un ambiente creato solo da queste dipendenze per assicurarti di non aggiungere dipendenze non documentate. Elencare le versioni delle dipendenze critiche nel notebook stesso (meglio se in testa o in fondo) assicurerà che, se utilizzato in modo isolato dal suo ambiente, il notebook contenga ancora informazioni critiche per aiutare i lettori a eseguirlo.
  6. Usa comunque il version control
    Il controllo della versione è un complemento fondamentale anche per l’uso dei notebook poiché la natura interattiva dei notebook semplifica la modifica o l’eliminazione accidentale di contenuti importanti. Inoltre, poiché i notebook contengono codice e il codice contiene inevitabilmente dei bug, essere in grado di determinare la cronologia di quando un determinato bug che hai scoperto è stato introdotto nel codice rispetto a quando è stato corretto, e quindi quali analisi potrebbe aver influenzato, è una funzionalità chiave nel calcolo scientifico. Consulta la sezione 3.1 Struttura del progetto per alcune delle pratiche raccomandate per l’organizzazione del repository.
    Tuttavia, tenete presente che i notebook Jupyter memorizzano sia il codice che metadati estesi su ciascuna cella come file di testo nel formato JSON. I sistemi di controllo della versione confrontano le differenze in questi file JSON, non le differenze nell’interfaccia grafica utente (GUI) del notebook user-friendly. Per questo motivo, le differenze segnalate tra le versioni di un determinato notebook sono di solito difficili da trovare e comprendere per gli utenti perché sono espresse come modifiche nei metadati JSON astrusi per il notebook. Un modo per risolvere questo problema è utilizzare uno strumento diffing specifico per notebook come nbdime che comprende la struttura del notebook e presenta differenze in modo significativo.
  7. Costruisci una pipeline
    I notebook che documentano le indagini esplorative iniziali saranno raramente ampiamente generalizzabili, ma una volta identificato un approccio di analisi stabile, un notebook ben progettato può essere generalizzato in una pipeline che ripete facilmente tale analisi utilizzando dati e parametri di input diversi. Con questo scopo in mente, progetta il tuo notebook fin dall’inizio per consentire tale riproposizione futura. Posizionare le dichiarazioni delle variabili chiave, in particolare quelle che verranno modificate quando si esegue una nuova analisi, nella parte superiore del notebook anziché seppellirle da qualche parte nel mezzo. Eseguire passaggi preparatori, come la pulizia dei dati, direttamente nel notebook ed evitare interventi manuali. Poiché l’interattività dei notebook li rende vulnerabili alla sovrascrittura accidentale o alla cancellazione di passaggi critici da parte dell’utente, se l’analisi viene eseguita rapidamente, prendere l’abitudine di riavviare regolarmente il kernel e rieseguire tutte le celle per assicurarsi di non aver eliminato accidentalmente un passaggio durante la pulizia del notebook (e se lo hai fatto, recupera il codice dal controllo versione). Il riavvio del kernel e l’esecuzione di tutte le celle è anche un buon test finale dei risultati. Per consentire l’esecuzione parziale di analisi complesse, suddividere i notebook lunghi in notebook più piccoli che si concentrano su uno o alcuni passaggi dell’analisi. Quindi, assicurarsi che ogni notebook memorizzi le versioni serializzate dei risultati intermedi chiave su disco per i notebook successivi da utilizzare. Una volta che un notebook è stato sviluppato, può essere parametrizzato con uno strumento come papermill. Tali notebook possono essere utilizzati non solo in modo interattivo ma anche come strumenti da riga di comando che possono essere eseguiti automaticamente, un grande vantaggio per le pipeline! Prendi in considerazione l’idea di collegare i passaggi della pipeline di analisi tramite DVC (sezione 8.1), consentendo così l’esecuzione completa e non interattiva dell’intera pipeline, sia in passaggi completi che parziali. Tale automazione supporta anche tecniche di qualità del codice come test del software; considera la possibilità di testare i flussi di lavoro dall’inizio alla fine ogni volta che viene apportata una modifica integrando il repository in un sistema di integrazione continua (ad esempio, Travis-CI o GitHub Actions). Ultimo ma non meno importante, tieni presente che i notebook della pipeline avranno quasi sicuramente una storia molto diversa (Regola #1) rispetto alle analisi iniziali che li hanno generati! Ricorda di rimuovere qualsiasi testo di introduzione, interpretazione o conclusione che non sia universalmente applicabile a diversi input e risultati e sostituirlo invece con una guida per l’utente della pipeline su come eseguire e interpretare i suoi risultati (potenzialmente nuovi).
  8. Rendi i tuoi dati accessibili e descrivili
    Avere accesso a un notebook chiaramente annotato è di scarsa utilità per coloro che desiderano riprodurre o estendere i risultati se i dati sottostanti sono “inutilizzabili.” Sforzati di rendere i tuoi dati accessibili insieme con il notebook. Mentre la condivisione dei dati richiede un’attenta pianificazione, i notebook semplificano la descrizione dei dati di input e le fasi di elaborazione a monte, essenziali per l’interpretazione dei risultati.
    Idealmente, si potrebbe pensare di condividere l’intero set di dati insieme con i tuoi notebook, magari su GitHub. Tuttavia, i dataset sono spesso troppo grandi  per condividerli in questo modo. In questi casi, prendi in considerazione la suddivisione dei dati in dataset sperimentale separato dai dati grezzi (raw data), per garantire la la riproducibilità. In entrambi i casi, il formato del dataset elaborato e dei dati grezzi va accuratamente documentato per non perder l’interpretabilità dei dati.
    Consulta il tuo relatore per far ospitare copie pubbliche di dataset di medie dimensioni in servizi di hosting quali Figshare e Zenodo. Per identificare in modo univoco e permanente set di dati, questi servizi di hosting forniscono identificatori di oggetti digitali (DOI) rendendo oiù semplice la citazione da parte di altri soggetti interessati.
  9. Progetta i tuoi notebook per essere letti, eseguiti ed esplorati
    Se hai seguito le regole precedenti, i tuoi notebook dovrebbero acquisire coprire l’intero workflow ed essere di facile lettura. Ma come potranno gli altri accedere, eseguire ed esplorare i tuoi notebook? Esistono diversi modi in cui puoi supportare il riutilizzo dei tuoi notebook da parte degli altri. Innanzitutto, archivia i tuoi notebook in un repository del Collab o in un suo fork.
    Leggi
    : considera come puoi sfruttare la struttura unica dei notebook per supportare la lettura. Per lo meno, lascia le versioni HTML/PDF statiche di tutti i notebook archiviate nella versione finale del repository che accompagna la tua tesi. È inoltre possibile utilizzare Nbviewer per fornire viste statiche del blocco appunti eseguito online senza la necessità di convertirlo prima in un documento PDF/HTML. GitHub utilizza questo servizio per eseguire il rendering di tutti i notebook sul proprio sito, quindi il push di un notebook su GitHub è un altro buon modo per rendere facilmente disponibili le visualizzazioni statiche. In entrambi i casi, puoi indicare ai tuoir relatori gli URL ove leggere i tuoi notebook online.
    Esegui: per supportare gli altri utenti che eseguono i tuoi notebook, puoi utilizzare Binder per fornire un ambiente di esecuzione a installazione zero per eseguire i tuoi notebook nel cloud. Binder consente di rieseguire notebook online senza dover installare Jupyter Notebook o Jupyter Lab sul proprio computer. Più in generale, è possibile creare un ambiente containerizzato portatile, ad esempio un’immagine Docker.
    Esplora: oltre a replicare semplicemente l’analisi nel tuo notebook, considera come puoi progettare il tuo notebook in modo che i futuri utenti possano modificare ed esplorare la tua analisi. Prendi in considerazione l’utilizzo di ipywidgets per consentire agli utenti futuri di modificare i parametri utilizzando elementi grafici come menu a discesa e dispositivi di scorrimento anziché modificare il codice.

Puoi consultare alcuni notebook di esempio che realizzano queste 9 regole d’oro su questo repository.

8.2.1. Eseguire notebook in cloud

TODO: completare Jupyter Lab

Oltre ad usare Jupyter Lab per eseguire notebook in locale sulla propria macchina, esiste la possibilità di usare servizi in cloud che permettono l’esecuzione  di notebook sfruttando le risorse hardware di una macchina remota tipicamente più performante della propria, risultando così molto utili per task di ML computazionalmente troppo pesanti. Due di questi servizi sono Kaggle e Google Colab.

Google Colab in particolare ha l’unico scopo di eseguire i Notebook e permette di salvare i risultati direttamente su Google Drive. Kaggle invece si pone come una vera e propria community con tema comune data science e il machine learning, e oltre al servizio di esecuzione dei notebook mette a disposizione dei mini-corsi utili per facilitare l’approccio alla materia, delle vere e proprie competizioni, e il forum in cui è possibile anche dare e ricevere aiuto.

Una delle differenze da tenere in conto nella scelta dello strumento da utilizzare sono i tempi limite di esecuzione del noteboo e l’utilizzo della GPU messa a disposizione dai due servizi nel rispettivo piano gratuito (Tabella 1).

 

Idle time

Session/Running Session

GPU

Kaggle

60 minuti

9 ore

30 ore/settimana

Google Colab

30-90 minuti

12 ore

12 ore/sessione (Variabile)

Tabella 1. Tempi limite di esecuzione su piani gratuito (maggio 2020).

 

Notate che Google Colab ha un idle time di appena 30 minuti, passati i quali la sessione sarà interrotta e dovrà essere riavviata. Inoltre, nella documentazione e nelle FAQ di Google Colab, l’utilizzo limite della GPU  non è indicato in quanto è descritto come variabile e prioritario, e potrebbe dunque variare da sessione a sessione, restando comunque approssimativamente legato al tempo di esecuzione del notebook.

8.2.2. Kaggle

TODO: completare

8.2.3. Google Colab

TODO: completare

Riferimenti

  1. Using Pull Requests, https://help.github.com/articles/using-pull-requests
  2. How to write the perfect pull request, https://github.com/blog/1943-how-to-write-the-perfect-pull-request
  3. Effective pull requests and other good practices for teams using GitHub, http://codeinthehole.com/writing/pull-requests-and-other-good-practices-for-teams-using-github
  4. Pull Requests Volume 1: Writing a Great Pull Request, http://nearthespeedoflight.com/article/2013_07_10_pull_requests_volume_1__writing_a_great_pull_request
  5. Making a Pull Request, https://www.atlassian.com/git/tutorials/making-a-pull-request
  6. Test a pull/merge request before accepting on Bitbucket, http://www.electricmonk.nl/log/2014/03/31/test-a-pull-merge-request-before-accepting-on-bitbucket
  7. S. Chacon, B. Straub (2020). Pro Git – chapter 7.6 Git Tools – Rewriting History, http://git-scm.com/book/en/v2/Git-Tools-Rewriting-History
  8. Chris Breams – How to Write a Git Commit Messagehttps://chris.beams.io/posts/git-commit
  9. The Hitchhiker’s Guide to Python – Pipenv & Virtual environments, https://docs.python-guide.org/dev/virtualenvs/
  10. Conda official documentation – Managing environments, https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html
  11. RStudio documentation – Introduction to renv, https://rstudio.github.io/renv/articles/renv.html
  12. dvc – Data Version Control – Tutorial, https://dvc.org/doc/tutorials/get-started/agenda
  13. A. Rule et al. (2019). Ten simple rules for writing and sharing computational analyses in Jupyter Notebooks, PLoS Comput Biol 15(7): e1007007, https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1007007
  14. S. Chacon, B. Straub (2020). Pro Git – chapter 2.6 Git Basics – Tagging, https://git-scm.com/book/en/v2/Git-Basics-Tagging