Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
CONOSCERE
PER MIGLIORARE NEL PROBLEM SOLVING E SCRIVERE CODICE PIÙ EFFICACE
Imran Ahmad
© Apogeo - IF - Idee editoriali Feltrinelli s.r.l.
Socio Unico Giangiacomo Feltrinelli Editore s.r.l.
Nomi e marchi citati nel testo sono generalmente depositati o registrati dalle
rispettive case produttrici.
Seguici su Twitter
~
Sai che ora facciamo anche CORSI? Dai un’occhiata al calendario.
A mio padre, Inayatullah Khan, che seguita a motivarmi nel
continuare a imparare ed esplorare nuovi orizzonti.
Prefazione
Argomenti trattati
Il Capitolo 1 riassume i fondamenti degli algoritmi. Si apre con una
sezione sui concetti di base necessari per comprendere il
funzionamento dei diversi algoritmi. Riepiloga come abbiamo iniziato
a utilizzare algoritmi per formulare matematicamente determinate
classi di problemi. Menziona anche i limiti dei diversi algoritmi.
Quindi spiega i vari modi per specificare la logica di un algoritmo.
Poiché per scrivere gli algoritmi in questo libro viene utilizzato
Python, il capitolo spiega come impostare l’ambiente per svolgere gli
esempi. Quindi, discute i vari modi in cui le prestazioni di un
algoritmo possono essere quantificate e confrontate con altri. Infine, il
capitolo tratta i vari metodi di convalida di una determinata
implementazione di un algoritmo.
Il Capitolo 2 si concentra sulle strutture di dati necessarie per
contenere i dati temporanei di un algoritmo. Gli algoritmi possono
essere a elevata intensità di dati, a elevata intensità di calcolo o
entrambe le cose. Ma per tutti i diversi tipi, la scelta delle giuste
strutture di dati è essenziale per la loro ottimale implementazione.
Molti algoritmi hanno una logica ricorsiva o iterativa e richiedono
strutture di dati specializzate di natura fondamentalmente iterativa.
Poiché in questo libro utilizziamo il linguaggio Python, il capitolo si
concentra sulle strutture di dati di Python.
Il Capitolo 3 presenta gli algoritmi di base impiegati per operazioni
di ordinamento e ricerca. Questi algoritmi possono in seguito diventare
la base per altri più complessi. Il capitolo inizia presentando diversi
tipi di algoritmi di ordinamento, confrontando le prestazioni di vari
approcci. Quindi, ne esamina vari per operazioni di ricerca e li mette a
confronto, quantificandone le prestazioni e la complessità. Infine, il
capitolo presenta le applicazioni pratiche di questi algoritmi.
Il Capitolo 4 presenta i concetti fondamentali di progettazione di
vari algoritmi. Spiega anche diversi tipi di algoritmi e discute i loro
punti di forza e punti deboli. Comprendere questi concetti è importante
quando si tratta di progettare e ottimizzare algoritmi complessi. Il
capitolo inizia discutendo diversi tipi di progetti algoritmici. Quindi,
presenta la soluzione per il famoso problema del commesso
viaggiatore. Poi introduce la programmazione lineare e i suoi limiti.
Infine, presenta un esempio pratico che mostra come la
programmazione lineare possa essere utilizzata per la pianificazione
della capacità.
Il Capitolo 5 si concentra sugli algoritmi per problemi con i grafi.
Sono molti i problemi computazionali che possono essere rappresentati
al meglio in termini di grafi. Questo capitolo presenta i metodi per
rappresentare un grafo e per eseguire ricerche su di esso. Eseguire
ricerche in un grafo significa seguire sistematicamente gli archi del
grafo in modo da visitarne i nodi. Un algoritmo di ricerca su grafi può
scoprire molto sulla struttura di un grafo. Molti algoritmi iniziano
cercando nel grafo fornito in input per ottenere alcune informazioni
strutturali. Sono vari gli algoritmi che si occupano di ricerche sui grafi.
Le tecniche di ricerca sono al centro del campo degli algoritmi dei
grafi. Il capitolo tratta le due rappresentazioni computazionali più
comuni dei grafi: come liste di adiacenza e come matrici di adiacenza.
Successivamente, presenta un semplice algoritmo di ricerca in
ampiezza (breadth-first). Poi presenta la ricerca in profondità (depth-
first)e fornisce alcuni risultati standard sull’ordine in cui una ricerca in
profondità visita i nodi.
Il Capitolo 6 introduce gli algoritmi di machine learning senza
supervisione. Sono detti “senza supervisione” perché il modello o
l’algoritmo cerca di apprendere dai dati le sue strutture, i suoi schemi e
le sue relazioni intrinseche senza alcuna supervisione. In primo luogo,
vengono discussi i metodi di clustering, metodi di machine learning
che cercano di trovare modelli di somiglianza e relazioni fra i
campioni di dati nel nostro dataset e quindi raggruppano questi
campioni in gruppi, in modo tale che ogni gruppo o cluster di campioni
di dati manifesti una qualche somiglianza, in base agli attributi o alle
sue caratteristiche intrinseche. Poi il capitolo discute gli algoritmi di
riduzione della dimensionalità, utilizzati quando si ha un numero
eccessivo di feature. Successivamente, esamina alcuni algoritmi che si
occupano del rilevamento delle anomalie. Infine, il capitolo presenta
l’estrazione delle regole associative, un metodo di data mining
utilizzato per esaminare e analizzare grandi dataset di transazioni per
identificare modelli e regole di interesse. Questi modelli rappresentano
relazioni e associazioni interessanti fra i vari elementi nelle
transazioni.
Il Capitolo 7 descrive i tradizionali algoritmi di machine learning
con supervisione in relazione a un insieme di problemi di machine
learning in cui è presente un dataset etichettato, ovvero dove vi sono
attributi di input ed etichette o classi di output. Questi input e output
vengono utilizzati per definire un sistema generalizzato, che può
successivamente essere utilizzato per prevedere i risultati per punti dei
dati non considerati in precedenza. Innanzitutto, viene introdotto il
concetto di classificazione nel contesto del machine learning. Quindi,
viene presentato il più semplice degli algoritmi di machine learning, la
regressione lineare. Segue uno degli algoritmi più importanti, l’albero
decisionale. Vengono discussi i limiti e i punti di forza degli algoritmi
ad albero decisionale, seguiti da due importanti algoritmi, SVM e
XGBoost.
Il Capitolo 8 introduce innanzitutto i concetti e i componenti
principali di una tipica rete neurale, che sta diventando la tecnica più
importante di machine learning. Quindi, presenta i vari tipi di reti
neurali e spiega i tipi di funzioni di attivazione utilizzati per
realizzarle. Poi tratta l’algoritmo di retropropagazione, l’algoritmo più
utilizzato per far convergere il problema in una rete neurale.
Successivamente, spiega la tecnica di trasferimento
dell’apprendimento, che può essere utilizzata per semplificare
notevolmente e automatizzare parzialmente l’addestramento dei
modelli. Infine, presenta un esempio reale: come utilizzare il deep
learning per rilevare oggetti nei dati multimediali.
Il Capitolo 9 presenta gli algoritmi per l’elaborazione del linguaggio
naturale (NLP). Questo capitolo procede dagli aspetti teorici a quelli
più pratici in modo progressivo. In primo luogo, presenta i fondamenti,
seguiti dalle basi matematiche. Quindi, discute una delle reti neurali
più utilizzate per progettare e implementare un paio di importanti casi
d’uso per i dati testuali. Tratta anche i limiti dell’elaborazione del
linguaggio naturale. Infine, presenta un caso di studio in cui viene
addestrato un modello per rilevare l’autore di un articolo in base allo
stile di scrittura.
Il Capitolo 10 si concentra sui motori di raccomandazione, che sono
un modo per modellare le informazioni disponibili in relazione alle
preferenze dell’utente e quindi utilizzarle per fornire raccomandazioni
informate. La base del motore di raccomandazione è sempre costituita
dalle interazioni fra utenti e prodotti. Questo capitolo inizia
presentando l’idea alla base dei motori di raccomandazione, quindi,
discute dei vari tipi di motori di raccomandazione e infine, illustra
come vengono utilizzati per suggerire articoli e prodotti agli utenti.
Il Capitolo 11 si concentra sui problemi relativi agli algoritmi basati
sui dati. Il capitolo inizia con una breve panoramica delle questioni
relative ai dati e poi presenta i criteri per la loro classificazione.
Successivamente, spiega come applicare determinati algoritmi alle
applicazioni che operano su dati in streaming e quindi presenta
l’argomento della crittografia. Infine, mostra un esempio pratico di
estrazione di modelli dai dati di Twitter.
Il Capitolo 12 introduce gli algoritmi relativi alla crittografia. Il
capitolo inizia presentando le basi storiche. Quindi, tratta gli algoritmi
di crittografia simmetrica. Partiremo dagli algoritmi di hashing MD5 e
SHA ed esamineremo i limiti e i punti deboli associati
all’implementazione di algoritmi simmetrici. Successivamente,
esamineremo gli algoritmi di crittografia asimmetrica e come vengono
utilizzati per creare certificati digitali. Infine, vedremo un esempio
pratico che riassume tutte queste tecniche.
Il Capitolo 13 spiega come gli algoritmi per dati su larga scala
gestiscono i dati che non possono essere contenuti nella memoria di un
singolo nodo e coinvolgono l’elaborazione da parte di più CPU.
Questo capitolo inizia discutendo quali tipi di algoritmi sono più adatti
per essere eseguiti in parallelo. Quindi, descrive le questioni relative
alla parallelizzazione degli algoritmi. Presenta inoltre l’architettura
CUDA e mostra come utilizzare una o più GPU per accelerare gli
algoritmi e quali modifiche devono essere apportate all’algoritmo per
utilizzare efficacemente la potenza della GPU. Infine, il capitolo
introduce il cluster computing e spiega come Apache Spark è in grado
di creare dataset resilienti distribuiti (RDD) per costruire
un’implementazione parallela estremamente veloce degli algoritmi
standard.
Il Capitolo 14 inizia con l’importante argomento della spiegabilità,
che sta diventando sempre più importante a mano a mano che si va
verso un’automatizzazione dei processi decisionali. Quindi, il capitolo
presenta gli aspetti etici dell’utilizzo di un algoritmo e le possibilità di
operare sulla base di pregiudizi, bias. Successivamente, tratta in
dettaglio le tecniche per la gestione dei problemi NP-difficili. Infine,
riassume i modi per implementare gli algoritmi e le sfide da affrontare.
Requisiti
Per sfruttare al meglio questo libro, impiegate Python versione 3.7.2
o successiva su un computer con almeno 4 GB di memoria RAM
(consigliati 8 GB o più) e con sistema operativo Windows, Linux o
Mac.
Anche se state utilizzando la versione digitale di questo libro, vi
consigliamo di digitare il codice o di scaricarlo dal repository GitHub
(URL disponibile nel prossimo paragrafo). In questo modo eviterete
eventuali errori presenti nel codice o legati a operazioni di copia-
incolla.
del database, i nomi delle cartelle, i nomi dei file, le estensioni dei file,
i nomi dei percorsi, gli URL, gli input dell’utente e gli handle di
Twitter. Ecco un esempio: “Vediamo come aggiungere un nuovo
elemento a uno stack utilizzando push o come rimuovere un elemento
da uno stack utilizzando pop”.
Un blocco di codice è formattato come segue:
define swap(x, y)
buffer = x
x = y
y = buffer
buffer = x
x = y
y = buffer
L’autore
Imran Ahmad è istruttore certificato Google e da diversi anni
insegna per Google e Learning Tree. Fra gli argomenti da lui affrontati
vi sono Python, il machine learning, gli algoritmi, i big data e il deep
learning. Nel suo dottorato di ricerca, ha proposto un nuovo algoritmo
basato sulla programmazione lineare chiamato ATSRA, che può essere
utilizzato per assegnare in modo ottimale le risorse in un ambiente di
cloud computing. Negli ultimi quattro anni ha collaborato a un
progetto di alto profilo di machine learning presso il laboratorio di
analisi avanzate del governo federale canadese. Il progetto consiste
nello sviluppare algoritmi di machine learning in grado di
automatizzare le procedure di esame delle domande di immigrazione.
Imran sta attualmente lavorando allo sviluppo di algoritmi per
utilizzare in modo ottimale le GPU per addestrare modelli complessi di
machine learning.
Il revisore tecnico
Benjamin Baka è uno sviluppatore di software ed è appassionato di
nuove tecnologie e di tecniche di programmazione. Ha dieci anni di
esperienza in diverse tecnologie: C++, Java, Ruby, Python e Qt. Alcuni
dei progetti cui sta lavorando si trovano sulla sua pagina GitHub.
Attualmente sta operando su tecnologie entusiasmanti per mPedigree.
Parte I
Le fasi di un algoritmo
La Figura 1.1 mostra le diverse fasi di sviluppo, implementazione e
utilizzo di un algoritmo.
Figura 1.1
La logica di un algoritmo
Quando si progetta un algoritmo, è importante trovare modi diversi
per specificarne i dettagli, curando la capacità di catturarne sia la
logica sia l’architettura. In generale, proprio come quando si costruisce
una casa, è importante specificare la struttura di un algoritmo, prima di
implementarlo effettivamente. Per algoritmi più complessi e distribuiti,
la pianificazione preliminare del modo in cui la loro logica sarà
distribuita nel cluster per l’esecuzione è importante per guidare in
modo efficiente il processo iterativo di progettazione. Attraverso lo
pseudocodice e i piani di esecuzione, entrambe queste esigenze
vengono soddisfatte, come vedremo nel prossimo paragrafo.
Che cos’è lo pseudocodice
Il modo più semplice per specificare la logica di un algoritmo
consiste nello scriverne una descrizione di alto livello in un modo semi
strutturato, chiamato pseudocodice. Prima di scrivere in pseudocodice
la logica di un algoritmo, è utile descriverne il flusso principale
scrivendo i passaggi principali in linguaggio naturale. Poi, questa
descrizione testuale deve essere convertita in pseudocodice, che
rappresenta un modo strutturato per scrivere questa descrizione in
modo che essa rappresenti più rigorosamente la logica e il flusso
dell’algoritmo. Lo pseudocodice dell’algoritmo, se ben scritto, deve
descrivere ad alto livello i passaggi dell’algoritmo, con un livello di
dettaglio ragionevole, perché un eccessivo dettaglio non è
particolarmente rilevante per descrivere il flusso principale e la
struttura dell’algoritmo. La Figura 1.2 mostra il flusso dei passaggi.
Figura 1.2
2: Ω = { }
3: k = 1
4: FOREACH Ti∈T
5: ωi = RA(∆k, Ti)
8: k=k+1
9: IF (k>q)
10: k= 1
11: ENDIF
buffer = x
x = y
y = buffer
NOTA
Non sempre gli snippet possono sostituire lo pseudocodice. Nello
pseudocodice, a volte astraiamo più righe di codice in un’unica riga di
pseudocodice, esprimendo la logica dell’algoritmo senza farci distrarre da
dettagli inutili.
Pacchetti Python
Python è un linguaggio general-purpose. Questo significa anche che
è progettato in modo da offrire funzionalità minime. In base al caso
d’uso per il quale intendete utilizzare Python, dovrete quindi installare
altri pacchetti aggiuntivi. Il modo più semplice per installare altri
pacchetti consiste nell’impiegare il programma di installazione pip.
Ecco il comando pip da usare per installare nuovi pacchetti:
pip install un_pacchetto
L’ecosistema SciPy
Scientific Python (SciPy) è un gruppo di pacchetti Python creati
appositamente per la comunità scientifica. Contiene molte funzioni, fra
cui un’ampia gamma di generatori di numeri casuali, numerose routine
di algebra lineare e svariati ottimizzatori. SciPy è un pacchetto
completo e, nel tempo, molti hanno sviluppato varie estensioni per
personalizzare ed estendere il pacchetto in base alle proprie esigenze.
Ecco i principali pacchetti che compongono questo ecosistema.
NumPy: per gli algoritmi, la capacità di creare strutture di dati
multidimensionali, come array e matrici, è davvero importante.
NumPy offre una serie di tipi di dati per array e matrici utili per i
calcoli statistici e l’analisi dei dati. Per informazioni su NumPy:
http://www.numpy.org.
Figura 1.5
La dimensione del calcolo
La dimensione del calcolo riguarda le esigenze di elaborazione e
calcolo del problema in questione. I requisiti di elaborazione di un
algoritmo determineranno il tipo di struttura più efficiente. Per
esempio, gli algoritmi di deep learning, in generale, richiedono molta
potenza di elaborazione. Questo significa che per poter impiegare
algoritmi di deep learning è importante poter disporre di
un’architettura parallela multinodo, ove possibile.
Un esempio pratico
Supponiamo di voler condurre un’analisi del sentiment relativa a un
video. Con l’analisi del sentiment cerchiamo di contrassegnare le
diverse parti di un video con le emozioni umane: tristezza, felicità,
paura, gioia, frustrazione ed estasi. È un lavoro a elevata intensità di
calcolo, per il quale è necessaria molta potenza di calcolo. Come
vedete nella Figura 1.6, per progettare la dimensione del calcolo,
abbiamo suddiviso l’elaborazione in cinque attività, composte da due
fasi. Tutta la trasformazione e la preparazione dei dati è implementata
in tre mappatori, mapper. Per questo, dividiamo il video in tre diverse
partizioni, chiamate split. Dopo l’elaborazione da parte dei mapper, il
video risultante viene immesso nei due aggregatori, chiamati reducer.
Per condurre l’analisi del sentiment, i riduttori devono raggruppare il
video in base alle emozioni. Infine, i risultati vengono combinati
nell’output.
Figura 1.6
NOTA
Il numero di mapper si traduce direttamente nel parallelismo runtime
dell’algoritmo. Il numero ottimale di mapper e reducer dipende dalle
caratteristiche dei dati, dal tipo di algoritmo che è necessario utilizzare e dalla
quantità di risorse disponibili.
Il caso migliore
Nel migliore dei casi, i dati forniti come input sono già organizzati
in modo tale che l’algoritmo offra le sue prestazioni migliori. L’analisi
del caso migliore fornisce il limite superiore delle sue prestazioni.
Il caso peggiore
Il secondo modo per stimare le prestazioni di un algoritmo è quello
di cercare di trovare il tempo massimo possibile necessario perché
porti a termine il lavoro in un determinato insieme di condizioni.
Questa analisi del caso peggiore di un algoritmo è piuttosto utile, in
quanto ci permette di garantire che, indipendentemente dalle
condizioni, le prestazioni dell’algoritmo saranno sempre migliori di
quanto emerge da questa analisi. L’analisi del caso peggiore è
particolarmente utile per stimare le prestazioni quando si affrontano
problemi complessi con grandi dataset. L’analisi del caso peggiore
fornisce il limite inferiore delle prestazioni dell’algoritmo.
Il caso medio
Si inizia dividendo i vari possibili input in vari gruppi. Quindi, si
conduce l’analisi delle prestazioni con uno degli input rappresentativi
di ciascun gruppo. Infine, si calcola la media delle prestazioni di
ciascuno dei gruppi.
L’analisi del caso medio non è sempre accurata, in quanto deve
considerare tutte le diverse combinazioni e possibilità di input
all’algoritmo, cosa non sempre facile.
Scelta di un algoritmo
Come fate a sapere qual è una soluzione migliore? Come fate a
sapere quale algoritmo viene eseguito più velocemente? La
complessità in termini di tempo e la notazione Big O (trattata più
avanti in questo stesso capitolo) sono davvero ottimi strumenti per
rispondere a questo tipo di domande.
Per vederne l’utilità, prendiamo un semplice esempio in cui
l’obiettivo è quello di ordinare una lista di numeri. Ci sono un paio di
algoritmi in grado di svolgere il lavoro. Il problema è come scegliere
quello giusto.
Innanzitutto, un’osservazione che possiamo fare è che se i numeri
nella lista sono pochi, in realtà non importa quale algoritmo scegliamo
per ordinarla. Quindi, se la lista è di soli 10 numeri (n = 10), non
importa quale algoritmo scegliamo, poiché anche un algoritmo
progettato molto male probabilmente impiegherebbe non più di
qualche microsecondo. Ma non appena la dimensione della lista
diventa 1 milione, la scelta dell’algoritmo giusto farà la differenza. Un
algoritmo scritto molto male potrebbe richiedere anche un paio d’ore,
mentre un algoritmo ben progettato potrebbe terminare di ordinare la
lista in un paio di secondi. Quindi, per dataset di input più grandi, ha
molto senso investire tempo e fatica, eseguire un’analisi delle
prestazioni e scegliere un algoritmo progettato correttamente, che
svolgerà il lavoro richiesto in modo efficiente.
Notazione Big O
La notazione Big O viene utilizzata per quantificare le prestazioni di
vari algoritmi all’aumentare della dimensione dell’input ed è una delle
metodologie più utilizzate per condurre l’analisi del caso peggiore. I
seguenti paragrafi trattano i diversi tipi di notazione Big O.
Complessità costante, O(1)
Se un algoritmo impiega la stessa quantità di tempo per essere
eseguito, indipendentemente dalla dimensione dei dati di input, si dice
che viene eseguito in tempo costante, rappresentato da O(1).
Prendiamo l’esempio dell’accesso all’ennesimo elemento di un array.
Indipendentemente dalle dimensioni dell’array, ci vorrà un tempo
costante per ottenere il risultato. Per esempio, la seguente funzione
restituisce il primo elemento dell’array e ha una complessità pari a
O(1):
def getFirst(myList):
return myList[0]
return sum
sum = 0
sum += item
return sum
Notate il ciclo interno, annidato nel ciclo principale. Questo ciclo
annidato conferisce al codice precedente la complessità O(n2):
Convalida di un algoritmo
La convalida di un algoritmo conferma che sta effettivamente
fornendo una soluzione matematica al problema che stiamo cercando
di risolvere. Un processo di convalida dovrebbe controllare i risultati
per quanti più valori di input possibili e anche per quanti più tipi di
valori di input possibili.
Figura 1.8
Algoritmo randomizzato
Gli algoritmi possono anche essere suddivisi nei seguenti due tipi,
sulla base delle ipotesi o delle approssimazioni utilizzate per
semplificare la logica con lo scopo di renderli più veloci.
Algoritmi esatti: ci si aspetta che gli algoritmi esatti producano
una soluzione precisa senza richiedere alcuna ipotesi o
approssimazione.
Algoritmi approssimati: quando la complessità del problema è
eccessiva per le risorse disponibili, semplifichiamo il problema
sulla base di alcune ipotesi. Gli algoritmi approssimati, basati su
queste semplificazioni o ipotesi, non ci offrono una soluzione
precisa.
Facciamo un esempio per capire la differenza fra un algoritmo esatto
e uno approssimato: il famoso problema del commesso viaggiatore,
presentato nel 1930. Un commesso viaggiatore vi sfida a trovare il
percorso più breve per un determinato venditore che deve visitare tutte
le città di un elenco e poi tornare alla base. Il primo tentativo di fornire
la soluzione prevedrà la generazione di tutte le possibili permutazioni
delle città e la scelta della combinazione di città più economica. La
complessità di questo approccio nel fornire la soluzione è O(n!), dove
n è il numero di città. È ovvio che la complessità in termini di tempo
inizia a diventare ingestibile oltre le 30 città.
Se il numero di città è superiore a 30, un modo per ridurre la
complessità consiste nell’introdurre alcune approssimazioni e ipotesi.
Per gli algoritmi approssimati, è importante impostare le aspettative
di accuratezza nella raccolta dei requisiti. La convalida di un algoritmo
approssimato richiede la verifica che l’errore dei risultati rientri in un
intervallo accettabile.
Spiegabilità
Quando gli algoritmi vengono utilizzati per situazioni critiche,
diventa importante avere la capacità di spiegare il motivo che è alla
base di ogni risultato ogni volta che sia necessario. Ciò permette di
assicurarci che le decisioni basate sui risultati degli algoritmi non
introducano distorsioni.
La capacità di identificare esattamente le caratteristiche che
vengono utilizzate, direttamente o indirettamente, per prendere una
decisione è chiamata spiegabilità di un algoritmo. Gli algoritmi,
quando vengono applicati a casi d’uso critici, devono essere valutati in
termini di bias e pregiudizi. L’analisi etica degli algoritmi è diventata
un elemento standard del processo di convalida cui occorre sottoporre
quegli algoritmi che possono influenzare processi decisionali che
riguardano la vita delle persone.
Per gli algoritmi che si occupano di deep learning, la spiegabilità
può essere difficile da raggiungere. Per esempio, se un algoritmo
rifiuta la richiesta di un mutuo da parte di una persona, è importante
pretendere trasparenza e avere la capacità di spiegarne il motivo.
La spiegabilità degli algoritmi è un’area attiva di ricerca. Una delle
tecniche più efficaci, sviluppata recentemente, si chiama Local
Interpretable Model-agnostic Explanation (LIME), proposta negli atti
della 22a Association for Computing Machinery (ACM) alla
conferenza internazionale Special Interest Group on Knowledge
Discovery (SIGKDD) sulle scoperte della conoscenza e il data mining,
nel 2016. La tecnica LIME si basa su un concetto: vengono indotti
piccoli cambiamenti all’input per ogni istanza e quindi viene fatto il
tentativo di mappare il confine decisionale locale per quell’istanza. In
tal modo è in grado di quantificare l’influenza di ciascuna variabile di
tale istanza.
Riepilogo
Questo capitolo ha trattato le basi degli algoritmi. Innanzitutto,
abbiamo appreso le diverse fasi dello sviluppo di un algoritmo.
Abbiamo trattato i diversi modi di specificare la logica di un algoritmo
che sono necessari per progettarlo. Quindi, abbiamo esaminato il modo
in cui si progetta un algoritmo. Abbiamo poi imparato due diversi
modi per analizzare le prestazioni di un algoritmo. Infine, abbiamo
studiato i diversi aspetti della convalida di un algoritmo.
Con lo studio di questo capitolo, dovreste essere in grado di
comprendere lo pseudocodice di un algoritmo e le diverse fasi dello
sviluppo e dell’implementazione di un algoritmo. Avete anche
imparato a usare la notazione Big O per valutare le prestazioni di un
algoritmo.
Il prossimo capitolo riguarda le strutture di dati utilizzate negli
algoritmi. Inizieremo esaminando le strutture di dati disponibili in
Python. Vedremo quindi come possiamo utilizzare queste strutture di
base per creare strutture di dati più sofisticate, come gli stack, le code e
gli alberi, che sono necessarie per sviluppare algoritmi complessi.
Capitolo 2
Liste
In Python, la lista è la principale struttura di dati utilizzata per
memorizzare una sequenza mutabile di elementi. Non è necessario che
la sequenza dei punti dei dati memorizzati nella lista sia dello stesso
tipo.
Per creare una lista, gli elementi dei dati devono essere racchiusi fra
parentesi quadre e separati da una virgola. Per esempio, il codice
seguente crea una lista di quattro elementi di tipi di dati differenti:
>>> aList = ["John", 33, "Toronto", True]
>>> print(aList)
>>> bin_colors[1]
'Green'
>>> bin_colors[0:2]
['Red', 'Green']
Figura 2.1
>>> bin_colors[2:]
['Blue', 'Yellow']
>>> bin_colors[:2]
['Red', 'Green']
NOTA
Durante lo slicing di una sezione di una lista, l’intervallo va indicato come
segue: il primo numero (che va incluso) e il secondo numero (che NON va
incluso) separati dal segno “:”. Per esempio, bin_colors[0:2] includerà
bin_colors[0] e bin_colors[1] ma non bin_colors[2]. Durante l’utilizzo delle
liste, occorre tenerlo bene a mente, poiché alcuni programmatori Python si
lamentano del fatto che questo comportamento non è molto intuitivo.
Indicizzazione negativa: in Python possiamo abbiamo impiegare
indici negativi, che partono a contare dalla fine della lista. Il
concetto è illustrato nel codice seguente:
>>> bin_colors = ['Red', 'Green', 'Blue', 'Yellow']
>>> bin_colors[:-1]
>>> bin_colors[:-2]
['Red', 'Green']
>>> bin_colors[-2:-1]
['Blue']
>>> max(a[2])
300
>>> a[2][1]
200
Red Square
Green Square
Blue Square
Yellow Square
Funzioni lambda
Esistono molte funzioni lambda che possono essere usate per le
liste. Sono particolarmente importanti nel contesto degli algoritmi e
forniscono la possibilità di creare una funzione al volo. In letteratura
vengono anche chiamate funzioni anonime. Questo paragrafo ne
illustra l’uso.
Filtraggio dei dati: per filtrare i dati, definiamo innanzitutto un
predicato, ovvero una funzione che accetta in input un unico
argomento e restituisce un valore booleano. Il codice seguente ne
mostra l’utilizzo:
>>> list(filter(lambda x: x > 100, [-5, 200, 300, -10, 10, 1000]))
def doSum(x1,x2):
return x1 +x2
>>> x
[0, 1, 2, 3, 4, 5]
>>> oddNum
[3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27]
Tuple
La seconda struttura di dati che può essere utilizzata per
memorizzare una collezione di dati è la tupla. A differenza delle liste,
le tuple sono strutture di dati immutabili (ovvero sono di sola lettura).
Le tuple sono costituite da diversi elementi racchiusi fra parentesi
tonde ().
Come le liste, gli elementi di una tupla possono essere di tipi di dati
differenti e anche di tipi di dati complessi. Quindi, gli elementi di una
tupla possono benissimo essere ulteriori tuple, creando così una
struttura di dati annidata. La capacità di creare strutture di dati
annidate è particolarmente utile negli algoritmi iterativi e ricorsivi.
Il codice seguente mostra come creare tuple:
>>> bin_colors = ('Red', 'Green', 'Blue', 'Yellow')
>>> bin_colors[1]
'Green'
>>> bin_colors[2:]
('Blue', 'Yellow')
>>> bin_colors[:-1]
# Tupla annidata
>>> max(a[2])
300
>>> a[2][1]
200
NOTA
Ove possibile, per motivi prestazionali le strutture di dati immutabili (come le
tuple) dovrebbero essere preferite rispetto alle strutture di dati mutabili (come le
liste). Soprattutto quando si tratta di big data, le strutture di dati immutabili sono
notevolmente più veloci di quelle mutabili. C’è un prezzo da pagare per la
possibilità di modificare gli elementi dei dati contenuti nelle liste, e dovremmo
analizzare attentamente ciò di cui abbiamo davvero bisogno, in modo da poter
implementare il codice come tuple di sola lettura, per rendere il tutto molto più
veloce.
Dizionari
La possibilità di conservare i dati come coppie chiave-valore è
importante, soprattutto nel caso degli algoritmi distribuiti. In Python,
una collezione di queste coppie chiave-valore viene memorizzata come
una struttura di dati chiamata dizionario. Per creare un dizionario,
occorre scegliere come chiave l’attributo più adatto a identificare i dati
durante l’elaborazione. Il valore può essere un elemento di qualsiasi
tipo, per esempio un numero o una stringa o può anche essere di un
tipo di dati complesso, per esempio una lista. È anche possibile creare
dizionari annidati, utilizzando come valore un ulteriore dizionario.
Per creare un semplice dizionario, che assegni dei colori ad alcune
variabili, le coppie chiave-valore devono essere racchiuse fra parentesi
graffe, { }. Per esempio, il codice seguente crea un semplice dizionario
composto da tre coppie chiave-valore:
>>> bin_colors = {
"manual_color": "Yellow",
"approved_color": "Green",
"refused_color": "Red"
>>> print(bin_colors)
Figura 2.2
'Green'
>>> print(bin_colors)
Insiemi
Un insieme è definito come una collezione di elementi che possono
essere di diverso tipo. Gli elementi sono racchiusi fra parentesi graffe:
{ }. Per esempio, osservate il seguente blocco di codice:
>>> print(green)
{'grass', 'leaves'}
>>> print(green)
{'grass', 'leaves'}
Notate che alcuni oggetti sono comuni fra questi due insiemi. I due
insiemi e la loro relazione possono essere rappresentati con l’aiuto del
diagramma di Venn rappresentato nella Figura 2.3.
Figura 2.3
{'fire hydrant'}
yellow e red.
Dataframe
Un dataframe è una struttura di dati utilizzata per memorizzare dati
tabulari e disponibile nel pacchetto pandas di Python. È una delle
strutture di dati più importanti per gli algoritmi e viene normalmente
utilizzata per elaborare dati strutturati. Consideriamo la seguente
tabella.
id name age decision
1 Fares 32 True
2 Elena 23 False
3 Steven 40 True
>>> df = pd.DataFrame([
>>> df
0 1 Fares 32 True
1 2 Elena 23 False
2 3 Steven 40 True
Notate che, nel codice precedente, df.column è una lista che specifica
il nome delle varie colonne.
NOTA
Il dataframe viene utilizzato anche in molti altri linguaggi e framework per
implementare strutture di dati tabulari. Vi sono esempi di framework in R e
Apache Spark.
name age
0 Fares 32
1 Elena 23
2 Steven 40
0 True
1 False
2 True
Notate che, in questo codice, stiamo estraendo le prime tre righe del
dataframe.
1 2 Elena 23 False
2 3 Steven 40 True
0 1 Fares 32 True
Matrici
Una matrice è una struttura di dati bidimensionale con un numero
fisso di colonne e righe. Ogni elemento di una matrice può essere
identificato per colonna e per riga.
In Python, è possibile creare una matrice utilizzando un array numpy,
come mostrato nel codice seguente:
>>> myMatrix = np.array([[11, 12, 13], [21, 22, 23], [31, 32, 33]])
>>> print(myMatrix)
[[11 12 13]
[21 22 23]
[31 32 33]]
>>> print(type(myMatrix))
<class 'numpy.ndarray'>
Vettori
Un vettore è una struttura monodimensionale per la memorizzazione
dei dati ed è una delle strutture di dati più utilizzate in Python.
Esistono due modi per creare vettori in Python.
Utilizzando una lista Python: questo è il modo più semplice per
creare un vettore.
>>> myVector = [22, 33, 44, 55]
>>> print(myVector)
[22 33 44 55]
>>> print(type(myVector))
<class 'list'>
>>> print(myVector)
[22 33 44 55]
>>> print(type(myVector))
<class 'numpy.ndarray'>
NOTA
In Python, possiamo rappresentare interi usando i caratteri di sottolineatura per
separare le parti. Questo rende i numeri più leggibili e meno soggetti a errori.
Ciò è particolarmente utile quando si ha a che fare con numeri molto grandi.
Quindi, in Python un miliardo (mille milioni, 1.000.000.000) può essere
rappresentato come a = 1_000_000_000.
Stack
Uno stack è una struttura di dati lineare adatta a memorizzare una
lista unidimensionale. Può memorizzare i dati in modalità Last-In,
First-Out (LIFO), mentre le code operano in modalità First-In, Last-
Out (FILO). La caratteristica distintiva di uno stack è il modo in cui gli
elementi vengono aggiunti e rimossi. Un nuovo elemento può essere
aggiunto o rimosso solo da un’estremità.
Di seguito le operazioni relative agli stack.
isEmpty() : restituisce true se lo stack è vuoto.
push() : aggiunge un nuovo elemento.
: restituisce l’ultimo elemento aggiunto e lo rimuove.
pop()
operazioni push() vengono utilizzate tre volte per aggiungere allo stack
altrettanti elementi. La parte inferiore della figura viene utilizzata per
recuperare i valori archiviati dallo stack. Nei passaggi 2.2 e 2.3, le
operazioni pop() vengono utilizzate per recuperare due elementi dallo
stack, in formato LIFO.
Creiamo in Python una classe chiamata Stack, sulla quale definiremo
tutte le operazioni relative a un tipico stack. Il codice di questa classe è
il seguente:
class Stack:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def pop(self):
return self.items.pop()
def peek(self):
return self.items[len(self.items) – 1]
def size(self):
return len(self.items)
Esempi d’uso
Uno stack viene utilizzato come struttura di dati in molte situazioni.
Per esempio, quando un utente desidera sfogliare la cronologia in un
browser web, il modello di accesso ai dati è di tipo LIFO, perfetto per
uno stack. Un altro esempio è quando un utente desidera annullare
un’operazione di editing in un software di elaborazione testi.
Code
Come uno stack, anche una coda memorizza elementi in una
struttura unidimensionale. Gli elementi vengono aggiunti e rimossi in
formato FIFO. Gli elementi vengono pertanto aggiunti alla fine della
coda e vengono estratti dalla cima della coda. L’operazione di
rimozione degli elementi dalla cima della coda si chiama dequeue().
L’operazione di aggiunta di nuovi elementi alla fine della coda si
chiama enqueue().
Nella Figura 2.5, la parte superiore mostra l’operazione di
accodamento, enqueue(). I passaggi 1.1, 1.2 e 1.3 aggiungono (in fondo)
alla coda tre elementi e in 1.4 si può vedere la coda risultante. Notate
che Yellow rappresenta la fine della coda e Red la cima della coda.
La parte inferiore della Figura 2.5 mostra un’operazione di
rimozione dalla (cima della) coda. I passaggi 2.2, 2.3 e 2.4 rimuovono
a uno a uno gli elementi dalla cima della coda.
Figura 2.5
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
self.items.insert(0, item)
def dequeue(self):
return self.items.pop()
def size(self):
return len(self.items)
Alberi
Nel contesto degli algoritmi, un albero è una delle strutture di dati
più utili, grazie alle sue capacità di archiviazione gerarchica dei dati.
Nella progettazione di nuovi algoritmi, utilizziamo alberi ovunque sia
necessario rappresentare relazioni gerarchiche fra i punti dei dati che
dobbiamo memorizzare o elaborare.
Vediamo meglio questa struttura di dati così interessante e
importante.
Ogni albero ha un insieme finito di nodi, con un elemento di
partenza, la radice, e un insieme di nodi uniti fra loro da collegamenti
chiamati rami.
Terminologia
Esaminiamo un po’ la terminologia relativa di questa struttura.
Un nodo senza genitore è detto nodo radice. Per esempio, nella
Nodo
Figura 2.7, il nodo radice è A. Negli algoritmi, di solito, il nodo radice
radice
contiene il valore più importante della struttura ad albero.
Livello del La distanza del nodo dalla radice è chiamata livello del nodo. Per
nodo esempio, nella Figura 2.7, il livello dei nodi D, E ed F è 2.
Due nodi di un albero sono chiamati fratelli se si trovano allo stesso
Nodi fratelli livello e hanno lo stesso genitore. Per esempio, se osserviamo la
Figura 2.7, i nodi B e C sono fratelli.
Il nodo F è figlio del nodo C, se i nodi sono collegati direttamente e se
Nodo figlio il livello del nodo C è minore del livello del nodo F. Viceversa, il nodo
e genitore C è genitore del nodo F. I nodi C e F nella Figura 2.7 sono un
esempio di questa relazione genitore-figlio.
Grado di Il grado di un nodo è il numero di figli che ha. Per esempio, nella
un nodo Figura 2.7, il nodo B ha grado 2.
Il grado di un albero è uguale al massimo grado che si può trovare
Grado di
nei nodi che lo costituiscono. Per esempio, l’albero presentato nella
un albero
Figura 2.7 ha grado 2.
È una porzione di un albero, composto da un nodo (che diventa
quindi il nodo radice del sottoalbero) e da tutti i suoi figli. Per
Sottoalbero
esempio, il sottoalbero del nodo E dell’albero presentato nella Figura
2.7 è costituito dal nodo E (nodo radice) e dai nodi G e H (nodi figli).
In un albero, è un nodo senza figli. Per esempio, nella Figura 2.7, D,
Nodo foglia
G, H e F sono quattro nodi foglia.
Nodo Qualsiasi nodo che non sia né radice né nodo foglia. Un nodo
interno interno, pertanto, avrà un nodo padre e almeno un nodo figlio.
NOTA
Gli alberi sono un tipo particolare di rete o grafo, argomento che studieremo nel
Capitolo 6. Per l’analisi di grafi e reti, usiamo i termini link (collegamento) o
edge (spigolo) invece di branch (ramo). La rimanente terminologia resta
sostanzialmente invariata.
Tipi di alberi
Esistono diversi tipi di alberi, catalogati come segue.
Albero binario: se il grado dell’albero è 2, l’albero è detto binario.
Per esempio, quello rappresentato nella Figura 2.6 è un albero
binario, in quanto ha grado 2.
Notate che la Figura 2.6 mostra un albero che ha quattro livelli e
otto nodi.
Figura 2.6
Figura 2.7
Notate che nella Figura 2.7 l’albero binario a sinistra non è
completo, poiché il nodo C ha grado 1 e tutti gli altri nodi hanno grado
2. L’albero al centro e quello a sinistra sono entrambi alberi completi.
Albero perfetto: è un tipo speciale di albero completo in cui tutti i
nodi foglia si trovano allo stesso livello. Per esempio, l’albero
binario a destra della Figura 2.7 è perfetto e completo, poiché tutti
i nodi foglia sono allo stesso livello, ovvero al livello 2.
Albero ordinato: i figli di un nodo sono organizzati secondo un
certo ordine, in base a specifici criteri. Per esempio, un albero può
essere ordinato da sinistra a destra in modo crescente: i suoi nodi
allo stesso livello aumenteranno di valore spostandosi da sinistra
a destra.
Esempi d’uso
L’albero è una delle principali strutture utilizzate per lo sviluppo di
alberi decisionali, come vedremo nel Capitolo 7. A causa della sua
struttura gerarchica, l’albero è molto utilizzato anche negli algoritmi di
analisi delle reti, come vedremo in dettaglio nel Capitolo 6. Gli alberi
vengono utilizzati anche in molti algoritmi di ricerca e ordinamento, in
cui devono essere implementate strategie divide et impera.
Riepilogo
In questo capitolo abbiamo trattato le strutture di dati che possono
essere utilizzate per implementare i vari tipi di algoritmi. Con lo studio
di questo capitolo, dovreste essere in grado di selezionare la struttura
di dati corretta da utilizzare per memorizzare ed elaborare i dati
mediante un algoritmo, e quindi dovreste anche essere in grado di
comprendere le motivazioni alla base delle nostre scelte in termini di
prestazioni dell’algoritmo.
Il prossimo capitolo tratta gli algoritmi di ordinamento e ricerca, per
i quali utilizzeremo alcune delle strutture di dati presentate in questo
capitolo.
Capitolo 3
Algoritmi di ordinamento e
ricerca
var2 = 2
Vediamo se ha funzionato:
lastElementIndex = len(list) – 1
print(0, list)
print(idx + 1, list)
Insertion sort
L’idea di base dell’“ordinamento per inserimento” è che in ogni
iterazione, rimuoviamo dalla struttura di dati un punto e lo inseriamo
nella posizione corretta. Nella prima iterazione, selezioniamo due
punti e li ordiniamo. Poi espandiamo la selezione con il terzo punto e
lo inseriamo nella posizione corretta. L’algoritmo avanza fino a
spostare tutti i punti nelle posizioni corrette. Questo processo è
rappresentato nella Figura 3.2.
Figura 3.2
j = i- 1
element_next = list[i]
list[j+1] = list[j]
j=j- 1
list[j+1] = element_next
return list
Merge sort
Finora abbiamo presentato due algoritmi di ordinamento: il bubble
sort e l’insertion sort, le cui prestazioni sono migliori se i dati sono già
almeno parzialmente ordinati. Il terzo algoritmo presentato in questo
capitolo è merge sort, sviluppato nel 1940 da John von Neumann. La
caratteristica distintiva di questo algoritmo è che le sue prestazioni non
dipendono dal fatto che i dati di input siano ordinati. Come
MapReduce e altri algoritmi per big data, si basa su una strategia
divide et impera. Nella prima fase, denominata splitting, l’algoritmo
continua a dividere ricorsivamente i dati in due parti, fino a quando la
dimensione dei dati è inferiore alla soglia definita. Nella seconda fase,
chiamata merging, l’algoritmo continua a unire ed elaborare i dati fino
a produrre il risultato finale. La logica di questo algoritmo è spiegata
nella Figura 3.3.
Esaminiamo innanzitutto lo pseudocodice dell’algoritmo di merge
sort:
mergeSort(list, start, end)
Shell sort
L’algoritmo bubble sort confronta gli elementi vicini e li scambia se
necessario. Se abbiamo una lista parzialmente ordinata, il bubble sort
dovrebbe fornire prestazioni ragionevoli, poiché uscirà non appena non
si verificherà più alcuno scambio di elementi in un ciclo.
Ma per una lista completamente non ordinata, di dimensioni N,
possiamo dire che il bubble sort dovrà iterare completamente N – 1
passaggi per ordinarla completamente.
Donald Shell ha proposto l’ordinamento che da lui prende il nome,
shell sort, chiedendosi se fosse il caso di selezionare gli elementi
immediatamente vicini per le operazioni di confronto e scambio.
Ora, cerchiamo di capire questo concetto. Nel primo passaggio,
invece di selezionare due elementi vicini, utilizziamo elementi che si
trovano a un gap fisso, ordinando eventualmente una sottolista
costituita da una coppia di punti. La situazione è mostrata nella Figura
3.4. Nel secondo passaggio, lo Shell sort ordina sottoliste contenenti
quattro elementi. Nei passaggi successivi, il numero di elementi per
sottolista continua ad aumentare e il numero di sottoliste continua a
diminuire, fino a raggiungere una situazione in cui c’è una sola
sottolista costituita dall’intera lista di tutti gli elementi. A questo
punto, possiamo supporre che la lista sia ordinata.
Figura 3.4
distance = len(list) // 2
temp = input_list[i]
j = i
j = j-distance
list[j] = temp
return list
Selection sort
Come abbiamo visto in precedenza in questo capitolo, il bubble sort
è uno degli algoritmi di ordinamento più semplici. L’algoritmo
selection sort rappresenta un miglioramento del bubble sort, in cui
cerchiamo di ridurre al minimo il numero totale di scambi richiesti. È
progettato per effettuare un solo scambio per passaggio, rispetto agli N
– 1 per passaggio con l’algoritmo bubble sort. Invece di far “risalire” il
valore più grande in piccoli passi (come fa il bubble sort, con
conseguenti N – 1 scambi), in ogni passaggio cerchiamo il valore più
grande e lo spostiamo in cima. Quindi, dopo la prima iterazione, il
valore più grande sarà già in cima. Dopo il secondo passaggio, dopo il
valore maggiore si troverà il secondo valore più grande. A mano a
mano che l’algoritmo progredisce, i valori successivi si sposteranno
sempre nella posizione corretta. L’ultimo valore verrà spostato dopo
l’(N – 1)-esimo passaggio. Quindi, l’ordinamento della selezione
richiede N – 1 passaggi per ordinare N elementi (Figura 3.5).
Ecco l’implementazione dell’algoritmo selection sort in Python:
def SelectionSort(list):
for fill_slot in range(len(list) – 1, 0, -1):
max_index = 0
for location in range(1, fill_slot + 1):
if list[location] > list[max_index]:
max_index = location
list[fill_slot], list[max_index] = list[max_index], list[fill_slot]
Figura 3.5
Ricerca lineare
Una delle strategie più semplici per la ricerca di dati consiste nello
scorrere semplicemente ogni elemento, alla ricerca di quanto
desiderato. Per ogni punto dei dati viene cercata una corrispondenza, e
quando la corrispondenza viene trovata, vengono restituiti i risultati e
l’algoritmo esce dal ciclo. In caso contrario, l’algoritmo continua a
cercare fino a raggiungere la fine della struttura che ospita i dati.
L’ovvio svantaggio della ricerca lineare è che può essere molto lenta, a
causa della sua ricerca esaustiva. Il vantaggio è che i dati non devono
essere ordinati, come invece è richiesto dagli altri algoritmi presentati
in questo capitolo.
Esaminiamo il codice per la ricerca lineare:
def LinearSearch(list, item):
index = 0
found = False
if list[index] == item:
found = True
else:
index = index + 1
return found
Ricerca binaria
Il prerequisito dell’algoritmo di ricerca binaria è che i dati siano
ordinati. L’algoritmo divide iterativamente una lista in due parti e
restringe via via gli indici inferiore e superiore fino a trovare il valore
cercato:
def BinarySearch(list, item):
first = 0
last = len(list) – 1
found = False
while first<= last and not found:
midpoint = (first + last) // 2
if list[midpoint] == item:
found = True
else:
if item < list[midpoint]:
last = midpoint- 1
else:
first = midpoint+1
return found
idx0 = mid + 1
return found
L’output è il seguente:
Notate che per poter utilizzare IntPolsearch(), l’array deve essere già
ordinato.
Applicazioni pratiche
La capacità di ricercare in modo efficiente e accurato i dati in un
determinato repository è fondamentale per molte applicazioni. A
seconda dell’algoritmo di ricerca scelto, potrebbe essere necessario
iniziare ordinando prima i dati. La scelta dei corretti algoritmi di
ordinamento e ricerca dipende dal tipo e dalla dimensione dei dati,
nonché dalla natura del problema che si sta cercando di risolvere.
Proviamo a utilizzare gli algoritmi presentati in questo capitolo per
risolvere il problema di dover accoppiare un nuovo richiedente presso
il dipartimento immigrazione di un Paese con i record storici
disponibili. Quando qualcuno richiede un visto per entrare nel Paese, il
sistema cerca di trovare il richiedente nei record storici esistenti. Se
trova almeno una corrispondenza, il sistema calcola il numero di volte
in cui l’ingresso dell’individuo è stato approvato o rifiutato in passato.
Se invece non trova alcuna corrispondenza, il sistema classifica il
nuovo richiedente e gli assegna un nuovo identificatore. La capacità di
cercare, individuare e identificare una persona nei dati storici è
fondamentale per il sistema. Queste informazioni sono importanti,
perché se l’individuo ha presentato una richiesta di visto in passato e
tale richiesta è stata rifiutata, ciò potrebbe influire negativamente sulla
sua richiesta attuale. Allo stesso modo, se la sua richiesta di visto è già
stata approvata in passato, tale approvazione può aumentare le
probabilità che anche la richiesta attuale venga approvata. In genere, il
database storico conterrà milioni di righe e avremo bisogno di una
soluzione ben progettata per individuare i richiedenti nel database
storico.
Supponiamo che la tabella storica nel database sia simile alla
seguente.
Personal Application First Decision
Surname DOB Decision
ID ID name date
2000-09- 2018-08-
45583 677862 John Doe Approved
19 07
1970-03- 2018-06-
54543 877653 Xman Xsir Rejected
10 07
1973-02- 2018-05-
34332 344565 Agro Waka Rejected
15 05
2000-09- 2018-03-
45583 677864 John Doe Approved
19 02
1975-01- 2018-04-
22331 344553 Kal Sorts Approved
02 15
DOB: 2000-09-19
Riepilogo
In questo capitolo abbiamo presentato una serie di algoritmi di
ordinamento e ricerca, trattandone i punti di forza e i punti deboli.
Abbiamo anche valutato le prestazioni di questi algoritmi e imparato
quando è il caso di utilizzare ciascun algoritmo.
Nel prossimo capitolo studieremo gli algoritmi dinamici. Vedremo
anche un esempio pratico di progettazione di un algoritmo e i dettagli
dell’algoritmo di classificazione delle pagine. Infine, studieremo
l’algoritmo di programmazione lineare.
Capitolo 4
Progettazione di algoritmi
Figura 4.1
NOTA
Se un problema è polinomiale non deterministico, è anche polinomiale? Questo
è uno dei più grandi problemi irrisolti dell’informatica. Inserito fra i Millennium
Prize Problem, selezionati dal Clay Mathematics Institute, è stato annunciato un
premio di 1 milione di dollari per la soluzione di questo problema, che avrà un
impatto importante su campi come l’intelligenza artificiale, la crittografia e le
scienze informatiche teoriche:
sc = spark.sparkContext
print (wordsRDD.collect())
alltours = permutations
def distance_tour(aTour):
for i in range(len(aTour)))
aCity = complex
def generate_cities(number_of_cities):
random.seed((number_of_cities, seed))
for c in range(number_of_cities))
"Generate all possible tours of the cities and choose the shortest tour."
return shortest_tour(alltours(cities))
e i singoli collegamenti.
Osservate il seguente codice:
%matplotlib inline
start = tour[0:1]
visualize_segment(start, 'rD')
plt.axis('scaled')
plt.axis('off')
Notate che l’abbiamo usata per generare il percorso per dieci città.
Poiché n = 10, genererà (10 – 1)! = 362.880 possibili permutazioni. Al
crescere di n, il numero di permutazioni cresce enormemente e il
metodo della forza bruta diviene impraticabile.
C = start or first(cities)
while unvisited:
C = nearest_neighbor(C, unvisited)
tour.append(C)
unvisited.remove(C)
return tour
L’algoritmo PageRank
Come esempio pratico, esaminiamo l’algoritmo PageRank, che è
stato inizialmente utilizzato da Google per classificare i risultati delle
ricerche effettuate dagli utenti. L’algoritmo genera un numero che
quantifica l’importanza di un risultato nel contesto della ricerca
eseguita dall’utente. È stato progettato da due studenti, Larry Page e
Sergey Brin, a Stanford sul finire degli anni Novanta, che
successivamente fondarono Google.
NOTA
Il nome dell’algoritmo PageRank prende il nome da Larry Page, che lo ha
creato con Sergey Brin alla Stanford University.
Implementazione dell’algoritmo
PageRank
La parte più importante dell’algoritmo PageRank consiste nel
trovare il modo migliore per calcolare l’importanza di ogni pagina
restituita dai risultati della query. Per calcolare un numero da 0 a 1 in
grado di quantificare l’importanza di una determinata pagina,
l’algoritmo impiega le informazioni relative ai seguenti due
componenti.
Informazioni specifiche per la query inserita dall’utente: questo
componente stima, nel contesto della query inserita dall’utente,
quanto è rilevante il contenuto della pagina web. Il contenuto
della pagina dipende direttamente dall’autore della pagina.
Informazioni non pertinenti alla query inserita dall’utente: questo
componente cerca di quantificare l’importanza di ogni pagina
web nel contesto dei suoi link, delle visualizzazioni e delle
somiglianze. Questo componente è difficile da calcolare, poiché
le pagine web sono eterogenee ed è difficile sviluppare criteri che
possano essere applicati in tutto il Web.
Per implementare l’algoritmo PageRank in Python, per prima cosa,
importiamo le librerie necessarie:
import numpy as np
import networkx as nx
%matplotlib inline
myPages = range(1, 5)
myWeb.add_nodes_from(myPages)
myWeb.add_edges_from(connections)
plt.show()
Formulazione di un problema di
programmazione lineare
Le condizioni per utilizzare la programmazione lineare sono le
seguenti:
dovremmo essere in grado di formulare il problema attraverso una
serie di equazioni;
le variabili utilizzate nell’equazione devono essere lineari.
Definizione della funzione obiettivo
Notate che l’obiettivo di ciascuno dei tre esempi precedenti è la
minimizzazione o massimizzazione di una variabile. Questo obiettivo è
formulato matematicamente come una funzione lineare di altre
variabili, ed è chiamato funzione obiettivo. Lo scopo di un problema di
programmazione lineare è quello di minimizzare o massimizzare la
funzione obiettivo, rimanendo entro i vincoli specificati.
Specificare i vincoli
Quando si cerca di minimizzare o massimizzare qualcosa, nei
problemi concreti ci sono sempre dei vincoli da rispettare. Per
esempio, quando si cerca di ridurre al minimo il tempo necessario per
riparare un’auto, bisogna anche considerare che i meccanici in grado di
occuparsene sono disponibili in numero limitato. Specificare ogni
vincolo tramite un’equazione lineare è una parte importante della
formulazione di un problema di programmazione lineare.
Applicazione pratica:
pianificazione della capacità con la
programmazione lineare
Vediamo un caso d’uso pratico in cui la programmazione lineare
può essere utilizzata per risolvere un problema concreto. Supponiamo
di voler massimizzare i profitti di una fabbrica che produce due diversi
tipi di robot.
Modello avanzato (A): fornisce funzionalità complete. La
produzione di ogni unità del modello avanzato si traduce in un
profitto di 4.200 dollari.
Modello base (B): fornisce solo le funzionalità di base. La
produzione di ogni unità del modello base si traduce in un profitto
di 2.800 dollari.
Per fabbricare un robot occorrono tre diversi tipi di persone. Il
numero esatto di giorni necessari per fabbricare un robot di ogni tipo è
il seguente.
Tipo di Robot Tecnico Specialista AI Ingegnere
Robot A: modello avanzato 3 giorni 4 giorni 4 giorni
Robot B: modello base 2 giorni 3 giorni 3 giorni
model += 4 * A + 3 * B <= 44
model.solve()
pulp.LpStatus[model.status]
Riepilogo
In questo capitolo abbiamo esaminato vari approcci alla
progettazione di un algoritmo. Abbiamo esaminato i compromessi
legati alla scelta del progetto più adatto di un algoritmo. Abbiamo
esaminato le migliori pratiche per formulare un problema concreto.
Abbiamo anche visto come si risolve un concreto problema di
ottimizzazione. Le lezioni apprese grazie a questo capitolo possono
essere utilizzate per implementare algoritmi ben progettati.
Nel prossimo capitolo ci concentreremo sugli algoritmi basati su
grafi. Inizieremo esaminando diversi modi di rappresentare i grafi,
quindi, studieremo le tecniche per esaminare i “dintorni” dei punti dei
dati per condurre una specifica indagine. Infine, studieremo i modi
ottimali per trarre informazioni dai grafi.
Capitolo 5
Tipi di grafi
I grafi possono essere classificati in quattro tipi:
grafi non orientati;
grafi orientati;
multigrafi non orientati;
multigrafi orientati.
Esaminiamoli in dettaglio.
Figura 5.1
Grafi orientati
Un grafo in cui la relazione fra i nodi ha un certo orientamento è
chiamato grafo orientato. La Figura 5.2 mostra un grafo orientato.
Figura 5.2
Multigrafi orientati
Se esiste una relazione direzionale fra i nodi di un multigrafo, lo
chiamiamo multigrafo orientato (Figura 5.4).
Figura 5.4
NOTA
Un grafo che ha uno o più ipernodi è detto ipergrafo.
Figura 5.5
Un grafo può anche avere più tipi speciali di archi. Ciò significa che
un determinato grafo può avere contemporaneamente sia cappi sia
iperarchi.
Reti egocentriche
L’intorno immediato di un determinato nodo m può avere
informazioni importanti sufficienti per condurre un’analisi esaustiva
del nodo. L’ego-centro, o la egonet, si basa su questa idea. La egonet
di un determinato nodo m è costituita da tutti i nodi direttamente
connessi a m, più lo stesso nodo m. Il nodo m è chiamato ego e i suoi
vicini sono chiamati altri.
La Figura 5.6 mostra la egonet del nodo 3 di un grafo.
Figura 5.6
NOTA
LinkedIn ha contribuito molto alla ricerca e allo sviluppo di nuove tecniche di
analisi delle reti sociali. In effetti, LinkedIn può essere considerato un pioniere di
molti algoritmi in questo settore.
Creare un intorno
Trovare strategie per creare un intorno attorno ai nodi di interesse è
fondamentale per gli algoritmi sui grafi. Le metodologie per la
creazione di intorni si basano sulla selezione di archi diretti rispetto al
nodo di interesse. Un modo per creare un intorno consiste nello
scegliere una strategia di ordine k, che seleziona i nodi che si trovano
entro k archi di distanza dal nodo di interesse.
Esaminiamo i vari criteri per la creazione di intorni.
Triangoli
Nella teoria dei grafi, trovare nodi ben collegati fra loro è
importante ai fini dell’analisi. Una tecnica consiste nel cercare di
identificare nella rete dei triangoli, che sono sottografi costituiti da tre
nodi direttamente collegati fra loro.
Esaminiamo il caso d’uso del rilevamento delle frodi, che
esamineremo anche verso la fine di questo capitolo. Se una egonet di
un nodo m è costituita da tre nodi, incluso il nodo m, allora questa
egonet è un triangolo. Il nodo m sarà l’ego e i due nodi a esso collegati
saranno gli altri. Se entrambi i nodi altri sono noti per essere
fraudolenti, possiamo tranquillamente dichiarare che anche il nodo m è
fraudolento. Se uno dei nodi altri è coinvolto in una frode, dovremo
condurre un’ulteriore ricerca per ottenere prove conclusive della frode.
Densità
Definiamo innanzitutto un grafo completo. Chiamiamo così un grafo
in cui ogni nodo è direttamente connesso a ogni altro nodo. Se
abbiamo un grafo completo N, allora il numero di archi totali nel grafo
(Edgestotal) può essere rappresentato come segue:
Grado di centralità
Il grado di un nodo è il numero di archi collegati al nodo. Può
indicare quanto è ben connesso un determinato nodo e la sua capacità
di diffondere rapidamente un messaggio attraverso una rete.
Consideriamo aGraph = (v, E), dove v rappresenta un insieme di
nodi e E rappresenta un insieme di archi. Ricordiamo che aGraph ha |v|
nodi e |E| archi. Se dividiamo il grado di un nodo per (|v| – 1),
otteniamo il grado di centralità:
Centralità ad autovettore
La centralità ad autovettore assegna un punteggio a tutti i nodi di un
grafo, per misurarne l’importanza nella rete. Il punteggio sarà un
indicatore del livello di connettività di un determinato nodo rispetto ad
altri nodi importanti dell’intera rete. Quando Google ha creato
l’algoritmo PageRank, che assegna un punteggio a ogni pagina web di
Internet (per esprimere la sua importanza), l’idea derivava proprio
dalla misurazione della centralità ad autovettore.
Calcolo delle metriche di centralità
usando Python
Creiamo una rete e poi proviamo a calcolarne le metriche di
centralità. Partiamo dal seguente blocco di codice:
import networkx as nx
edges = [(7, 2), (2, 3), (7, 4), (4, 5), (7, 3), (7, 5), (1, 6), (1, 7), (2, 8),
(2, 9)]
G = nx.Graph()
G.add_nodes_from(vertices)
G.add_edges_from(edges)
Figura 5.8
Inizializzazione
Useremo due strutture di dati:
visited contiene tutti i nodi che sono stati visitati, e inizialmente,
sarà vuota;
queue contiene tutti i nodi che vogliamo visitare nelle prossime
iterazioni.
Il ciclo principale
Ora implementeremo il ciclo principale. Continueremo a eseguire il
ciclo fino a esaurire tutti gli elementi presenti in queue. Per ogni nodo in
, se è già stato visitato, dobbiamo visitare il suo vicino.
queue
neighbours = graph[node]
queue.append(neighbour)
Una volta che tutti i nodi sono stati visitati e aggiunti alla struttura di
dati visited, le iterazioni si fermano (vedi Figura 5.10).
Ora, proviamo a trovare una persona, nella Figura 5.10, utilizzando
la ricerca breadth-first.Specifichiamo i dati che stiamo cercando e
osserviamo i risultati:
Ora esaminiamo l’algoritmo di ricerca in profondità, depth-first.
visited = set()
visited.add(start)
print(start)
return visited
'Faras' : {'Imran'},
'Mike' : {'Amin'},
'Nick' : {'Amin'}}
Figura 5.11
edges = [(7, 2), (2, 3), (7, 4), (4, 5), (7, 3), (7, 5), (1, 6), (1, 7),
(2, 8), (2, 9)]
pos = nx.spring_layout(G)
5. Definiamo i nodi NF:
nx.draw_networkx_nodes(G, pos,
nodelist = [1, 4, 3, 8, 9],
with_labels = True,
node_color = 'g',
node_size = 1300)
node_size = 1300)
labels = {}
Figura 5.13
Abbiamo escogitato due modi per classificare questa nuova persona,
che denoteremo con q, come implicato (F ) o non implicato (NF ) in
una frode:
usando un metodo semplice, che non utilizza metriche di
centralità e informazioni aggiuntive sul tipo di frode;
usando una metodologia watchtower, una tecnica avanzata che
utilizza le metriche di centralità dei nodi esistenti, nonché
informazioni aggiuntive sul tipo di frode.
Esamineremo in dettaglio entrambi questi metodi.
Notate che questi punteggi avranno un peso sulla nostra analisi dei
casi di frode e derivano da dati storici.
Grado di sospetto
Il grado di sospetto (DoS, Degree of Suspicion) quantifica il nostro
livello di sospetto che una persona possa essere coinvolta in una frode.
Un valore DoS pari a 0 significa che si tratta di una persona a basso
rischio; un valore DoS pari a 9 significa che si tratta di una persona ad
alto rischio.
L’analisi dei dati storici mostra che i truffatori professionisti hanno
posizioni elevate nella loro rete sociale. Per esplicitare questo concetto,
prima calcoliamo le quattro metriche di centralità di ciascun nodo nella
nostra rete e poi prendiamo la media di questi nodi. Questo ci dice
l’importanza di quella particolare persona nella rete.
Se una persona associata a un nodo è coinvolta in una frode,
illustriamo questo esito negativo assegnandole un punteggio
utilizzando i valori predeterminati mostrati nella tabella precedente.
Questo viene fatto in modo che la gravità del reato si rifletta nel valore
DoS di ogni singola persona.
Infine, moltiplichiamo la media delle metriche di centralità e il
punteggio di esito negativo per ottenere il valore del DoS.
Normalizziamo il DoS dividendolo per il valore massimo del DoS
nella rete.
Calcoliamo il DoS per ciascuno dei nove nodi della rete precedente.
Nodo Nodo Nodo Nodo Nodo Nodo Nodo Nodo
Nodo 1
2 3 4 5 6 7 8 9
Grado di
0,25 0,5 0,25 0,25 0,25 0,13 0,63 0,13 0,13
centralità
Betweenness 0,25 0,47 0 0 0 0 0,71 0 0
Closeness 0,5 0,61 0,53 0,47 0,47 0,34 0,72 0,4 0,4
Autovettore 0,24 0,45 0,36 0,32 0,32 0,08 0,59 0,16 0,16
Media delle
metriche di 0,31 0,51 0,29 0,26 0,26 0,14 0,66 0,17 0,17
centralità
Punteggio
0 6 0 0 7 8 10 0 0
negativo
DoS 0 3 0 0 1,82 1,1 6,625 0 0
DoS
0 0,47 0 0 0,27 0,17 1 0 0
normalizzato
Figura 5.14
Riepilogo
In questo capitolo abbiamo trattato gli algoritmi basati su grafi. Con
lo studio di questo capitolo, dovreste essere in grado di utilizzare varie
tecniche di rappresentazione, ricerca ed elaborazione dei dati
rappresentati come grafi. Abbiamo anche sviluppato la capacità di
calcolare la distanza più breve fra due nodi e abbiamo costruito intorni
nello spazio del problema. Questa conoscenza dovrebbe aiutarci a
utilizzare la teoria dei grafi per affrontare problemi come il
rilevamento delle frodi.
Nel prossimo capitolo, ci concentreremo su vari algoritmi di
machine learning senza supervisione. Molte delle tecniche discusse in
questo capitolo si ripresentano anche negli algoritmi di apprendimento
senza supervisione, per esempio per trovare prove di frode in un
dataset.
Parte II
Introduzione all’apprendimento
senza supervisione
Ecco una definizione semplice di apprendimento senza
supervisione: è il processo che fornisce un qualche tipo di struttura a
dati non strutturati, scoprendo e utilizzando schemi presenti nei dati.
Se i dati non sono stati prodotti da un processo casuale, saranno
rilevabili dei pattern nei suoi dati, nello spazio multidimensionale del
problema. Gli algoritmi di apprendimento senza supervisione
funzionano scoprendo questi schemi e utilizzandoli per trovare una
struttura per il dataset. Questo concetto è rappresentato nella Figura
6.1.
Figura 6.1
NOTA
È importante notare che la Fase 1 del ciclo di vita CRISP-DM riguarda la
comprensione operativa. Si concentra su ciò che deve essere fatto, non su
come verrà fatto.
NOTA
La Fase 2 (Studio dei dati) e la Fase 3 (Preparazione dei dati) del ciclo di vita
CRISP-DM riguardano la studio e la preparazione dei dati da impiegare per
l’addestramento del modello. Queste fasi comportano l’elaborazione dei dati.
Alcune organizzazioni impiegano appositi specialisti per questa fase.
Esempi pratici
Attualmente, l’apprendimento senza supervisione viene utilizzato
per farsi un’idea dei dati e fornire loro maggiore struttura, per esempio
nella segmentazione del marketing, nel rilevamento delle frodi e
nell’analisi del paniere di mercato (argomento che verrà trattato più
avanti in questo stesso capitolo). Vediamo un paio di esempi.
Classificazione vocale
L’apprendimento senza supervisione può essere utilizzato per
classificare le singole voci in un file vocale. Utilizza il fatto che la
voce di ogni individuo ha caratteristiche distinte, dando origine a
modelli audio potenzialmente separabili. Questi modelli possono poi
essere utilizzati per il riconoscimento vocale. Per esempio, Google
utilizza questa tecnica nei propri dispositivi Google Home, per
addestrarli a distinguere le voci di più persone. Una volta addestrato,
Google Home può personalizzare la risposta in base all’utente che ha
effettuato la richiesta.
Per esempio, supponiamo di avere una conversazione registrata di
tre persone che parlano fra loro per mezz’ora. Utilizzando algoritmi di
apprendimento senza supervisione, possiamo identificare le voci di
queste tre persone. Notate che attraverso l’apprendimento senza
supervisione, stiamo definendo una struttura a dei dati non strutturati.
Questa struttura ci offre ulteriori dimensioni utili nello spazio del
problema, che possono essere utilizzate per ottenere informazioni e
preparare i dati per l’algoritmo di machine learning scelto. La Figura
6.3 mostra come viene utilizzato l’apprendimento senza supervisione
per il riconoscimento vocale.
Figura 6.3
Figura 6.4
Quantificare le somiglianze
L’affidabilità del raggruppamento creato dagli algoritmi di
clustering si basa sul presupposto che possiamo quantificare con
precisione le somiglianze, la vicinanza fra i vari punti dei dati nello
spazio del problema. Per farlo possiamo utilizzare varie misure di
distanza. Ecco tre dei metodi più utilizzati per quantificare le
somiglianze:
distanza euclidea;
distanza di Manhattan;
distanza del coseno.
Esaminiamo queste misure di distanza in modo più dettagliato.
Distanza euclidea
La distanza fra i diversi punti può quantificare la somiglianza fra
due punti dei dati ed è ampiamente utilizzata nelle tecniche di machine
learning senza supervisione, come il clustering. La distanza euclidea è
la misura più comune e semplice. Viene calcolata misurando la
distanza più breve fra due punti dei dati nello spazio
multidimensionale. Per esempio, consideriamo due punti, A(1, 1) e
B(4, 4), in uno spazio bidimensionale, come mostrato nella Figura 6.5.
Figura 6.5
Figura 6.7
NOTA
I dati testuali possono quasi essere considerati uno spazio altamente
dimensionale. Poiché la misura della distanza del coseno funziona molto bene
con gli spazi altamente dimensionali, è una buona scelta per i dati testuali.
Inizializzazione
Per raggruppare i punti dei dati, l’algoritmo k-means utilizza una
misura della distanza per trovare la somiglianza o vicinanza fra i punti
dei dati. Prima di utilizzare l’algoritmo k-means, è necessario
selezionare la misura della distanza più appropriata. Per default, verrà
utilizzata la distanza euclidea. Inoltre, se il dataset contiene valori
anomali, è necessario ideare un meccanismo per determinare i criteri
che devono essere identificati e per rimuovere dal dataset tali valori
anomali.
Figura 6.8 In (a) i punti dei dati prima del clustering; in (b) i cluster risultanti dopo aver
eseguito l’algoritmo di clustering k-means.
import numpy as np
})
myKmeans.fit(dataset)
labels = myKmeans.labels_
Clustering gerarchico
Il clustering k-means utilizza un approccio top-down, perché
l’algoritmo parte dai punti dei dati più importanti, che sono i centri dei
cluster. Esiste un approccio alternativo al clustering in cui, invece di
partire dall’alto, si avvia l’algoritmo dal basso. Il “basso”, in questo
contesto, è ciascuno dei singoli punti dei dati nello spazio del
problema. La soluzione consiste nel continuare a raggruppare insieme i
punti dei dati simili a mano a mano che si procede nella ricerca dei
centri dei cluster. Questo approccio bottom-up è utilizzato dagli
algoritmi di clustering gerarchico ed è l’argomento di questo
paragrafo.
})
3. Quindi creiamo il cluster gerarchico specificando gli
iperparametri. Usiamo la funzione fit_predict() per elaborare
effettivamente l’algoritmo:
cluster = AgglomerativeClustering(n_clusters = 2, affinity =
'euclidean',
linkage = 'ward')
cluster.fit_predict(dataset)
iris = pd.read_csv('iris.csv')
X = iris.drop('Species', axis = 1)
pca = PCA(n_components = 4)
pca.fit(X)
Figura 6.10
X['Petal.Width'] * pca_df['Petal.Width'][3]
Esempi di utilizzo
L’estrazione delle regole associative viene utilizzata quando si cerca
di indagare le relazioni causa-effetto fra le diverse variabili presenti in
un dataset. Ecco alcuni esempi di domande cui può essere utile
rispondere.
Quali valori di umidità, nuvolosità e temperatura possono portare
a piovere domani?
Quale tipo di richiesta di indennizzo assicurativo può indicare una
frode?
Che combinazioni di farmaci possono portare a complicazioni per
i pazienti?
Analisi del paniere di mercato
Questo libro tratta i motori di raccomandazione nel Capitolo 10.
L’analisi del carrello è un modo più semplice per apprendere le
raccomandazioni. Nell’analisi del carrello, i nostri dati contengono
solo le informazioni relative agli articoli acquistati insieme e non
hanno alcuna informazione sull’utente o sul fatto che abbia apprezzato
i singoli articoli. Notate che è molto più facile ottenere questi dati
rispetto ai dati sulle valutazioni.
Per esempio, questo tipo di dati viene generato ogni volta che
acquistiamo qualcosa online e non è richiesta alcuna tecnica speciale
per ottenerli. Questi dati, se raccolti in un arco di tempo, sono chiamati
dati transazionali. Quando l’analisi delle regole associative viene
applicata ai dataset di transazioni dei carrelli della spesa utilizzati nei
negozi di alimentari, nei supermercati e nelle catene di fast food, si
parla di analisi del paniere di mercato e misura la probabilità
condizionata di acquistare determinati articoli insieme, il che aiuta a
rispondere alle seguenti domande.
Qual è il posizionamento ottimale degli articoli sullo scaffale?
Come dovrebbero apparire gli articoli nel catalogo?
Che cosa dovrebbe essere consigliato, in base ai pattern di
acquisto di un cliente?
Poiché l’analisi del paniere di mercato può stimare le correlazioni
fra gli articoli, spesso viene utilizzata negli esercizi di vendita al
dettaglio, come i supermercati, i minimarket, i drugstore e le catene di
fast food. Il vantaggio dell’analisi del paniere di mercato è che i
risultati sono quasi autoesplicativi, ovvero sono facilmente
comprensibili dagli utenti aziendali.
Vediamo un tipico supermercato. Tutti gli articoli disponibili nel
negozio possono essere rappresentati da un insieme, π = {articolo1,
articolo2, …, articolom}. Quindi, se quel supermercato vende 500
articoli, allora π sarà un insieme di dimensioni 500.
I clienti acquistano articoli da questo negozio. Ogni volta che
qualcuno acquista un articolo e paga alla cassa, l’articolo viene
aggiunto all’insieme degli articoli presenti in una determinata
transazione, detto itemset. In un determinato arco di tempo, le
operazioni vengono raggruppate in un insieme rappresentato da Δ,
dove Δ = {t1, t2, …, tn}.
Esaminiamo i seguenti semplici dati di quattro transazioni. Queste
transazioni sono riassunte nella tabella seguente.
t1 wickets, pads
t2 bat, wickets, pads, helmet
t3 helmet, ball
t4 bat, pads, helmet
Regole associative
Una regola associativa descrive matematicamente gli articoli legati
dalle varie transazioni. Lo fa studiando la relazione fra due itemset
nella forma X → Y, dove X ⊂ π e Y ⊂ π. Inoltre, X e Y sono itemset
non sovrapposti; il che significa che X Y = Ø.
Una regola associativa potrebbe essere descritta nella forma
seguente:
{helmet, ball} → {bicycle}
Dove, X = {helmet, ball} e Y = {bicycle}.
Tipi di regole
L’esecuzione di algoritmi di analisi associativa in genere comporta
la generazione da un dataset di un numero elevato di regole delle
transazioni, molte delle quali inutili. Per scegliere quelle regole che
forniscono informazioni utili, possiamo classificarle nei seguenti tre
tipi:
banali;
non spiegabili;
utili.
Vediamo ciascuno di questi tipi in modo più dettagliato.
Regole banali
Fra le numerose regole generate, molte saranno inutili, poiché
riassumono conoscenze banali del settore. Anche se la fiducia che
riponiamo nelle regole banali è elevata, esse rimangono inutili e non
possono essere utilizzate per alcun processo decisionale guidato dai
dati. Possiamo tranquillamente ignorarle.
Ecco alcuni esempi di regole banali.
Chi salta da un grattacielo rischia di morire.
Studiando sodo si ottengono voti migliori agli esami.
Le vendite di stufe aumentano al diminuire della temperatura.
Guidare un’auto oltre i limiti di velocità aumenta il rischio di
incidenti.
Regole utili
Le regole utili sono esattamente quello che stiamo cercando. Sono
comprensibili e offrono suggerimenti utili. Possono aiutarci a scoprire
le possibili cause di un evento quando vengono presentate a un
pubblico che ha familiarità con il dominio aziendale. Per esempio, le
regole utili possono suggerire il miglior posizionamento di un
determinato prodotto in negozio in base agli attuali pattern di acquisto.
Possono anche suggerire quali articoli collocare vicini per
massimizzare le loro possibilità di vendita, poiché gli utenti tendono ad
acquistarli insieme.
Ecco alcuni esempi di regole utili e delle azioni che esse
suggeriscono.
Regola 1: la visualizzazione di annunci sugli account dei social
media degli utenti comporta una maggiore probabilità di vendita.
Suggerisce modi alternativi di pubblicizzare un prodotto.
Regola 2: l’utilizzo di più etichette del prezzo aumenta le
probabilità di vendita.
Il prezzo di un articolo può essere replicato più volte, mentre
viene aumentato il prezzo di un altro articolo.
Supporto
Il supporto (support) è un numero che quantifica la frequenza del
pattern nel nostro dataset. Viene calcolato contando il numero di
occorrenze del pattern in questione e dividendo il risultato per il
numero totale di tutte le transazioni.
Osserviamo la seguente formula per un determinato itemseta:
numItemseta = Numero di transazioni che contengono itemseta
numtotal = Numero totale di transazioni
NOTA
Osservando semplicemente il supporto, possiamo avere un’idea di quanto sia
raro il verificarsi di un pattern. Se il supporto è basso, significa che stiamo
considerando un evento raro.
Lift
Un altro modo per stimare la qualità di una regola associativa
consiste nel calcolare il lift, un numero che quantifica quanto
miglioramento offre una regola associativa nel prevedere il risultato
rispetto alla sola assunzione del risultato nella parte destra
dell’equazione. Se gli itemset X e Y sono indipendenti, il lift si calcola
come segue:
L’algoritmo Apriori
Si tratta di un algoritmo iterativo e multifase utilizzato per generare
regole associative. Si basa su un approccio a generazione-e-test.
Prima di eseguire l’algoritmo apriori, dobbiamo definire due
variabili: supportthreshold e confidencethreshold. L’algoritmo è costituito
dalle seguenti due fasi.
Fase di generazione dei candidati: genera gli itemset candidati,
ovvero l’insieme di tutti gli itemset che superano la soglia di
supporto, supportthreshold.
Fase di filtro: elimina tutte le regole che non superano la soglia di
confidenza, confidencethreshold.
Dopo il filtraggio, le regole superstiti rappresentano la risposta.
L’algoritmo FP-growth
L’algoritmo Frequent Pattern growth (FP-growth) migliora
l’algoritmo a priori. Inizia considerando l’albero delle transazioni
frequenti, FP-tree, che è un albero ordinato. Si compone di due
passaggi:
popolare l’albero FP-tree;
estrarre i pattern frequenti.
Vediamo come funzionano questi passaggi.
Popolare l’albero FP
Consideriamo i dati delle transazioni mostrati nella tabella seguente.
Rappresentiamoli innanzitutto come una matrice sparsa.
ID Bat Wickets Pads Helmet Ball
1 0 1 1 0 0
2 1 1 1 1 0
3 0 0 0 1 1
4 1 0 1 1 0
Figura 6.11
Figura 6.12
import numpy as np
import pyfpgrowth as fp
["helmet", "pads"],
transactionSet = pd.DataFrame(dict1)
Figura 6.13
NOTA
Questo esempio richiede l’elaborazione in tempo reale dei dati di input.
Clustering
Una volta scoperti gli argomenti, li sceglieremo come centri dei
cluster. Quindi possiamo eseguire l’algoritmo di clustering k-means,
che assegnerà ciascuno dei tweet a uno dei cluster.
Riepilogo
In questo capitolo abbiamo esaminato varie tecniche di machine
learning senza supervisione. Abbiamo esaminato le circostanze in cui è
opportuno cercare di ridurre la dimensionalità del problema che stiamo
cercando di risolvere, e i diversi metodi per farlo. Abbiamo anche
studiato gli esempi pratici in cui le tecniche di machine learning senza
supervisione possono essere molto utili, fra cui l’analisi del paniere di
mercato e il rilevamento delle anomalie.
Nel prossimo capitolo, esamineremo varie tecniche di machine
learning con supervisione. Inizieremo con la regressione lineare e poi
esamineremo altre tecniche di machine learning con supervisione più
sofisticate, come gli algoritmi basati su alberi decisionali, SVM e
XGBoast. Studieremo anche l’algoritmo naive Bayes, particolarmente
adatto a dati testuali non strutturati.
Capitolo 7
NOTA
Un modello di machine learning con supervisione, dopo la fase di
addestramento/training è in grado di effettuare previsioni, stimando la variabile
target in base alle feature.
Figura 7.1
Le condizioni abilitanti
Il machine learning con supervisione si basa sulla capacità di un
algoritmo di addestrare un modello utilizzando esempi. Un algoritmo
di machine learning con supervisione richiede che vengano soddisfatte
le seguenti condizioni abilitanti per poter funzionare.
Un numero sufficiente di esempi: gli algoritmi di machine
learning con supervisione hanno bisogno di un numero di esempi
sufficiente per addestrare un modello.
Pattern nei dati storici: gli esempi utilizzati per addestrare un
modello devono contenere dei pattern. La probabilità che si
verifichi il nostro evento di interesse dovrebbe dipendere da una
combinazione di pattern, tendenze ed eventi. Senza questi,
abbiamo a che fare con dati casuali che non possono essere
utilizzati per addestrare un modello.
Ipotesi valide: quando addestriamo un modello di machine
learning con supervisione utilizzando esempi, ci aspettiamo che le
ipotesi che si applicano agli esempi saranno valide anche in
futuro. Vediamo un esempio reale: se vogliamo addestrare un
modello di machine learning in grado di prevedere la probabilità
di concedere a uno studente un visto d’ingresso, tutto si basa
sull’ipotesi che le leggi e le politiche non cambieranno quando
poi il modello verrà utilizzato per fare le sue previsioni. Se
venissero applicate nuove politiche o leggi dopo l’addestramento
del modello, potrebbe essere necessario ripetere l’addestramento
del modello per fargli considerare queste nuove informazioni.
Differenziazione fra classificatori e
regressori
In un modello di machine learning, la variabile target può essere
categorica o continua. Il tipo della variabile target determina il tipo di
modello di machine learning con supervisione di cui disponiamo.
Fondamentalmente, abbiamo due tipi di modelli di machine learning
con supervisione.
Classificatori: se la variabile target è di tipo categorico, il
modello di machine learning viene chiamato classificatore. I
classificatori possono essere utilizzati per rispondere alle seguenti
domande.
- Questa crescita anormale dei tessuti è di un tumore maligno?
- In base alle condizioni meteorologiche attuali, domani pioverà?
- In base al profilo di un determinato richiedente, la sua richiesta
di mutuo dovrebbe essere approvata?
Regressori: se la variabile target è di tipo continuo, addestriamo
un regressore. I regressori possono essere utilizzati per rispondere
alle seguenti domande.
- In base alle condizioni meteorologiche attuali, quanto pioverà
domani?
- Quale sarà il prezzo di una determinata casa avente determinate
caratteristiche?
Esaminiamo più in dettaglio sia i classificatori sia i regressori.
NOTA
La capacità di etichettare con precisione dati non etichettati utilizzando un
modello addestrato rappresenta la vera potenza degli algoritmi di
classificazione. I classificatori prevedono le etichette dei dati non etichettati per
rispondere a una determinata domanda.
Codifica one-hot
Molti algoritmi di machine learning richiedono che tutte le feature
siano variabili continue. Pertanto, se alcune delle feature sono variabili
categoriche, dobbiamo trovare una strategia per convertirle in variabili
continue. La codifica one-hot è uno dei modi più efficaci per eseguire
questa trasformazione. Per questo particolare problema, l’unica
variabile categorica che abbiamo è Gender. Convertiamola in una
variabile continua usando la codifica one-hot:
enc = sklearn.preprocessing.OneHotEncoder()
enc.fit(dataset.iloc[:,[0]])
onehotlabels = enc.transform(dataset.iloc[:,[0]]).toarray()
result.head(5)
X= result.drop(columns = ['Purchased'])
dataset di test.
y_train: un vettore contenente i valori delle etichette nel dataset di
training.
y_test: un vettore contenente i valori delle etichette nel dataset di
test.
sc = StandardScaler()
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)
NOTA
Se non stiamo cercando di risolvere qualcosa di molto semplice, avremo alcune
classificazioni errate quando valuteremo il modello. Il modo in cui interpretiamo
queste classificazioni errate per determinare la qualità del modello dipende
dalle metriche prestazionali che scegliamo di utilizzare.
Matrice di confusione
Una matrice di confusione riassume i risultati della valutazione di
un classificatore. La matrice di confusione di un classificatore binario
ha l’aspetto rappresentato nella Figura 7.2.
Figura 7.2
NOTA
Se l’etichetta del classificatore che stiamo addestrando ha due livelli, parliamo
di classificatore binario. Il primo esempio d’uso del machine learning con
supervisione, in particolare di un classificatore binario, è stato durante la Prima
guerra mondiale, per distinguere un aereo da un uccello in volo.
Metriche prestazionali
Le metriche prestazionali permettono di quantificare l’efficacia dei
modelli addestrati. Sulla base di ciò, definiamo le seguenti quattro
metriche.
Metrica Formula
Accuratezza
Richiamo
Precisione
Punteggio F1
Varianza
La varianza quantifica la precisione con cui un modello stima la
variabile target se viene utilizzato un dataset diverso per addestrarlo.
Quantifica se la formulazione matematica del nostro modello
rappresenta una buona generalizzazione dei dati sottostanti.
Regole specifiche in overfitting basate su scenari e situazioni
specifici portano a un’elevata varianza; regole generalizzate e
applicabili a una varietà di scenari e situazioni portano a una bassa
varianza.
NOTA
Il nostro obiettivo nel machine learning è quello di addestrare modelli che
esibiscano valori contenuti di bias e varianza. Raggiungere questo obiettivo non
è sempre facile. Di solito è il problema che tiene svegli la notte i data scientist.
Compromesso bias-varianza
Quando si addestra un modello di machine learning, è difficile
decidere il giusto livello di generalizzazione per le regole che
definiscono il modello addestrato. Occorre trovare il giusto livello di
generalizzazione in termini di bias e varianza.
NOTA
Ipotesi più semplicistiche offrono una maggiore generalizzazione, cosa che
riduce il bias ma aumenta la varianza.
Figura 7.4
classifier.fit(X_train, y_train)
2. Ora, usiamo il nostro modello addestrato per prevedere le
etichette per il dataset di test. Generiamo una matrice di
confusione che riassume le prestazioni del nostro modello
addestrato:
import sklearn.metrics as metrics
y_pred = classifier.predict(X_test)
cm = metrics.confusion_matrix(y_test, y_pred)
cm
Punti di forza
Le regole dei modelli creati utilizzando un algoritmo ad albero
decisionale sono facilmente interpretabili. Modelli come questo
sono chiamati modelli whitebox. I modelli whitebox sono
necessari ogni volta che è necessaria la trasparenza nel tracciare i
dettagli e le ragioni delle decisioni prese dal modello. Questa
trasparenza è essenziale nelle applicazioni in cui vogliamo evitare
i pregiudizi e proteggere le comunità vulnerabili. Per esempio, nei
casi d’uso critici dei settori governativo e assicurativo è
generalmente richiesto l’uso di un modello whitebox.
I classificatori ad albero decisionale sono progettati per estrarre
informazioni da uno spazio del problema discreto. Ciò significa
che la maggior parte delle feature è costituita da variabili
categoriche; quindi, l’utilizzo di un albero decisionale per
addestrare il modello è una buona scelta.
Punti deboli
Casi d’uso
In questo paragrafo, esaminiamo i casi d’uso per i quali viene
utilizzato l’algoritmo ad albero decisionale.
Classificazione di record
I classificatori ad albero decisionale possono essere utilizzati per
classificare i punti dei dati, come negli esempi seguenti.
Richieste di finanziamento: un classificatore binario può
determinare se un richiedente rischia di essere inadempiente.
Segmentazione dei clienti: un classificatore binario può
suddividere i clienti in clienti di alto valore, di valore medio e di
scarso valore, in modo da condurre strategie di marketing
appropriate per ciascuna categoria.
Diagnosi medica: un classificatore binario può distinguere un
tumore benigno da uno maligno.
Analisi dell’efficacia delle cure: un classificatore binario può
individuare i pazienti che hanno reagito positivamente a una
determinata cura.
I metodi a ensemble
È una tecnica di machine learning che prevede di creare più modelli
leggermente differenti utilizzando parametri differenti e poi di
combinarli insieme a formare un modello aggregato. Per creare
ensemble efficaci, dobbiamo trovare il giusto criterio di aggregazione.
Esaminiamo alcuni algoritmi a ensemble.
Implementazione dell’amplificazione del gradiente con
l’algoritmo XGBoost
XGBoost è stato creato nel 2014 e si basa su principi di
amplificazione del gradiente. È diventato uno degli algoritmi di
classificazione a ensemble più utilizzati. Genera un gruppo di alberi
interconnessi e utilizza la discesa del gradiente per ridurre al minimo
l’errore residuo. Questo lo rende perfetto per le infrastrutture
distribuite, come Apache Spark, o per il cloud computing, come
Google Cloud o Amazon Web Services (AWS).
Vediamo ora come si implementa l’amplificazione del gradiente con
l’algoritmo XGBoost.
1. Innanzitutto, creeremo un’istanza del classificatore XGBClassfier e
addestreremo il modello utilizzando il dataset di training:
cm
costruiti.
max_depth: controlla la profondità di ciascuno di questi alberi
decisionali.
In altre parole, un albero decisionale può continuare a dividersi fino
ad arrivare a una situazione in cui un nodo rappresenta un singolo
esempio nel dataset di training. Impostando max_depth, limitiamo il
numero di queste divisioni. In tal modo controlliamo la complessità del
modello e limitiamo anche l’overfitting rispetto ai dati di training.
Facendo riferimento al seguente output, n_estimators controlla la
larghezza e max_depth controlla la profondità del modello a foresta
casuale:
cm = metrics.confusion_matrix(y_test, y_pred)
cm
Regressione logistica
Quello di regressione logistica è un algoritmo di classificazione
utilizzato per la classificazione binaria. Utilizza una funzione logistica
per formulare le interazioni fra le feature di input e la variabile target.
È una delle tecniche di classificazione più semplici per modellare una
variabile dipendente binaria.
Ipotesi
La regressione logistica presuppone quanto segue:
il dataset di training è completo, non ha valori mancanti;
l’etichetta è una variabile categorica binaria;
l’etichetta è ordinale, in altre parole è una variabile categorica con
valori ordinabili;
tutte le funzioni o le variabili di input sono indipendenti fra loro.
Stabilire la relazione
Per la regressione logistica, il valore previsto viene calcolato come
segue:
Supponiamo che .
Così ora:
classifier.fit(X_train, y_train)
cm
L’algoritmo SVM
SVM (Support Vector Machine) è un classificatore che trova un
iperpiano ottimale che massimizza il confine fra due classi. In SVM, il
nostro obiettivo è quello di massimizzare tale confine. Il confine è
definito come la distanza fra l’iperpiano di separazione (il confine di
decisione) e i campioni di training più vicini a tale iperpiano, i vettori
di supporto. Iniziamo con un esempio molto semplice, con due sole
dimensioni, X1 e X2. Vogliamo trovare una linea che separi i cerchi
dalle croci (Figura 7.6).
Figura 7.6
Figura 7.7
classifier.fit(X_train, y_train)
cm
Teorema di Bayes
Il teorema di Bayes viene utilizzato per calcolare la probabilità
condizionata fra due eventi indipendenti, A e B. La probabilità che si
verifichino gli eventi A e B è rappresentata da P(A) e P(B). La
probabilità condizionata è rappresentata da P(B|A), ovvero la
probabilità condizionata che si verifichi l’evento B dal momento che si
è verificato l’evento A:
classifier.fit(X_train, y_train)
cm
Il dataset storico
Di seguito sono riportate le feature presenti nel dataset di dati storici
di cui disponiamo.
Nome Tipo Descrizione
NAME Categorica Identifica un veicolo
CYLINDERS Continua Numero di cilindri (fra 4 e 8)
DISPLACEMENT Continua Cilindrata del motore in pollici cubici
HORSEPOWER Continua Potenza del motore
Tempo necessario per accelerare da 0 a 60 mph (in
ACCELERATION Continu
secondi)
utilizzate solo per identificare le righe del nostro dataset non sono
rilevanti per l’addestramento del modello:
dataset = dataset.drop(columns = ['NAME'])
training.
X_test: una struttura contenente le feature del dataset di test.
training
y_test: un vettore contenente i valori delle etichette nel dataset di
test.
Ora, usiamo i dati preparati su tre diversi regressori, in modo da
poterne confrontare le prestazioni.
Regressione lineare
Di tutte le tecniche di machine learning con supervisione,
l’algoritmo di regressione lineare è il più facile da capire.
Esamineremo prima la regressione lineare semplice e poi espanderemo
il concetto alla regressione lineare multipla.
Figura 7.9
regressor.fit(X_train, y_train)
sqrt(mean_squared_error(y_test, y_pred))
df = pd.read_csv("weather.csv")
Riepilogo
In questo capitolo, abbiamo iniziato esaminando le basi del machine
learning con supervisione. Quindi, abbiamo esaminato in modo più
dettagliato vari algoritmi di classificazione. Successivamente, abbiamo
trattato diversi metodi per valutare le prestazioni dei classificatori e
abbiamo studiato vari algoritmi di regressione. Infine, abbiamo
esaminato diversi metodi per valutare le prestazioni degli algoritmi che
abbiamo studiato.
Il prossimo capitolo si occupa delle reti neurali e degli algoritmi di
deep learning. Esamineremo i metodi utilizzati per addestrare una rete
neurale e anche vari strumenti e framework disponibili per la
valutazione e l’implementazione di una rete neurale.
Capitolo 8
Le reti neurali
Ispirato al funzionamento dei neuroni nel cervello umano, il
concetto di reti neurali è stato proposto da Frank Rosenblatt nel 1957.
Per comprendere appieno la sua architettura, è utile esaminare
brevemente la struttura a strati e concatenata dei neuroni nel cervello
umano (Figura 8.1).
Nel cervello umano, i dendriti si comportano come sensori,
rilevando un segnale. Il segnale viene poi trasmesso a un assone, che è
una proiezione, lunga e sottile, di una cellula nervosa. La funzione
dell’assone è quella di trasmettere il segnale a muscoli, ghiandole o
altri neuroni. Come vediamo nella Figura 8.1, il segnale attraversa il
tessuto di connessione chiamato sinapsi prima di essere trasmesso ad
altri neuroni. Notate che attraverso questa “pipeline organica”, il
segnale continua a viaggiare fino a raggiungere il muscolo o la
ghiandola target, dove provoca l’azione richiesta. In genere occorrono
dai sette agli otto millisecondi affinché il segnale attraversi la catena di
neuroni e raggiunga la sua destinazione.
Figura 8.1
NOTA
Una rete neurale profonda è una rete neurale contenente uno o più layer
nascosti. Il deep learning è il processo di training di una rete neurale.
Figura 8.3
Figura 8.5
Funzioni di attivazione
Una funzione di attivazione definisce il modo in cui verranno
elaborati gli input di un determinato neurone per generare un output.
Come mostra la Figura 8.6, ciascuno dei neuroni di una rete neurale
è dotato di una funzione di attivazione, che determina come verranno
elaborati gli input. Nella figura possiamo vedere che i risultati generati
da una funzione di attivazione vengono passati all’output. La funzione
di attivazione imposta i criteri in base ai quali devono essere
interpretati i valori degli input per generare un output.
Figura 8.6
Funzione soglia
È la funzione di attivazione più semplice possibile. L’output della
funzione soglia è binario: 0 o 1. Genera 1 come output se uno qualsiasi
degli input è maggiore di 1, come descritto nella Figura 8.7.
Figura 8.7
Notate che non appena le somme pesate degli input danno “segni di
vita”, l’output (y) diventa 1. Ciò rende molto sensibile la funzione di
attivazione a soglia. È abbastanza vulnerabile a un’attivazione errata a
causa di un minimo segnale in input, magari causato da un problema
tecnico o da “rumore”.
Funzione sigmoide
Può essere pensata come un miglioramento della funzione soglia.
Qui abbiamo un controllo sulla sensibilità della funzione di attivazione
(Figura 8.8).
Figura 8.8.
return 1 / (1 + np.exp(-z))
Figura 8.9
per
if x < 0:
return 0
else:
return x
per
per
Figura 8.10
if x < 0:
return (beta * x)
else:
return x
Figura 8.11
La funzione y è la seguente:
numerator = 1 – np.exp(-2 * x)
denominator = 1 + np.exp(-2 * x)
return numerator/denominator
Funzione softmax
A volte abbiamo bisogno di più di due livelli come output della
funzione di attivazione. Softmax è una funzione di attivazione che
fornisce più di due livelli in output e ciò la rende particolarmente
adatta a problemi di classificazione multiclasse. Supponiamo di avere
n classi e di avere i valori di input. L’associazione fra i valori di input e
le classi è come segue:
NOTA
Per i classificatori binari, la funzione di attivazione nell’ultimo layer sarà la
sigmoide; per i classificatori multiclasse sarà la softmax.
Strumenti e framework
In questo paragrafo, esamineremo in dettaglio i framework e gli
strumenti disponibili per l’implementazione delle reti neurali. Nel
tempo, sono stati sviluppati molti framework per implementare le reti
neurali, ognuno con i suoi punti di forza e punti deboli. In questo
paragrafo, ci concentreremo su Keras con TensorFlow.
Keras
Keras è una delle librerie di reti neurali più utilizzate ed è scritta in
Python. È stata scritta pensando alla facilità d’uso e rappresenta il
modo più veloce per implementare il deep learning. Keras fornisce
solo blocchi di alto livello ed è considerata una libreria per modelli.
TensorFlow
TensorFlow è una delle librerie più utilizzate per lavorare con le reti
neurali. Nel paragrafo precedente, abbiamo visto come possiamo
usarlo come motore di backend di Keras. È una libreria open source ad
alte prestazioni che in realtà può essere utilizzata per qualsiasi calcolo
numerico. Se osserviamo lo stack, possiamo vedere che possiamo
scrivere codice TensorFlow in un linguaggio di alto livello come
Python o C++, che viene interpretato dal motore di esecuzione
distribuito TensorFlow. Questo rende TensorFlow piuttosto popolare
fra gli sviluppatori.
Il modo in cui funziona TensorFlow consiste nel creare un grafo
diretto per rappresentare il calcolo. I nodi sono collegati da archi,
ovvero gli input e gli output delle operazioni matematiche. Inoltre, essi
rappresentano array di dati.
Figura 8.14
Convoluzione
Il processo di convoluzione esalta un pattern di interesse in una
determinata immagine elaborandolo con un’altra immagine più piccola
chiamata filtro (o anche kernel). Per esempio, se vogliamo trovare i
margini degli oggetti rappresentati in un’immagine, possiamo eseguire
una convoluzione dell’immagine con un filtro apposito. Il rilevamento
dei margini può aiutarci a identificare gli oggetti, a classificarli e può
essere utile in altre applicazioni. Quindi, il processo di convoluzione
consiste nel trovare le specifiche caratteristiche di un’immagine.
L’approccio alla ricerca di pattern si basa sulla ricerca di pattern che
possano essere riutilizzati su più dati differenti. I pattern riutilizzabili
sono chiamati filtri o kernel.
Pooling
Una parte importante dell’elaborazione dei dati multimediali ai fini
del machine learning è il downsampling, che offre due vantaggi:
riduce la dimensionalità complessiva del problema, diminuendo
notevolmente il tempo necessario per addestrare il modello;
grazie all’aggregazione, consente di astrarre i dettagli non
necessari nei dati multimediali, rendendoli più generici e più
rappresentativi di problemi simili.
Il downsampling è rappresentato nella Figura 8.15.
Figura 8.15
Trasferimento dell’apprendimento
Nel corso degli anni, molte organizzazioni, gruppi di ricerca e
membri della comunità open source hanno perfezionato alcuni modelli
complessi, addestrati utilizzando enormi quantità di dati per casi d’uso
generici. In alcuni casi, hanno investito anni di sforzi
nell’ottimizzazione di questi modelli. Alcuni di questi modelli open
source possono essere utilizzati per le seguenti applicazioni:
rilevamento di oggetti nei video;
rilevamento di oggetti nelle immagini;
trascrizione dell’audio;
analisi del sentiment dei testi.
Ogni volta che iniziamo ad addestrare un nuovo modello di machine
learning, la domanda che ci poniamo è: invece di partire da zero, non
potremmo semplicemente personalizzare un modello già preaddestrato
e ben consolidato? In altre parole, possiamo trasferire l’apprendimento
dei modelli esistenti al nostro modello personalizzato, in modo da
poter rispondere alla nostra domanda? Se riusciamo a farlo, avremo tre
vantaggi:
i nostri sforzi di training del modello riceveranno una notevole
spinta in avanti;
utilizzando un modello ben collaudato e consolidato, è probabile
che la qualità complessiva del nostro modello migliori;
Se non disponiamo di dati sufficienti per il problema su cui
stiamo lavorando, un modello preaddestrato può essere di grande
aiuto.
Esaminiamo due esempi reali.
Quando si addestra un robot, potremmo prima usare un gioco di
simulazione per addestrare un modello a rete neurale. In quella
simulazione, potremmo creare tutti quegli eventi rari che
sarebbero molto difficili da ricreare nel mondo reale. Una volta
addestrata la rete neurale, potremmo trasferire l’apprendimento
per addestrare il modello fisico.
Supponiamo di voler addestrare un modello in grado di
classificare i laptop Apple e Windows da un feed video. Esistono
già modelli consolidati per il rilevamento di oggetti, disponibili
come open source e in grado di classificare con precisione vari
oggetti in un feed video. Possiamo usare questi modelli come
punto di partenza per addestrare un nostro modello a identificare
gli oggetti laptop. Una volta identificati gli oggetti come laptop,
possiamo addestrare ulteriormente il modello per distinguere fra
laptop Apple e Windows.
Nel prossimo paragrafo, applicheremo i concetti trattati in questo
capitolo alla costruzione di una rete neurale di classificazione di
documenti fraudolenti.
Caso di studio: utilizzo del deep
learning per il rilevamento delle
frodi
L’utilizzo di tecniche di machine learning per identificare documenti
fraudolenti è un campo di ricerca attivo e stimolante. I ricercatori
stanno studiando fino a che punto la capacità di riconoscimento dei
pattern delle reti neurali può essere sfruttato per questo scopo. Invece
di impiegare estrattori manuali di attributi, i pixel possono essere
utilizzati per informare diverse architetture di deep learning.
Metodologia
La tecnica presentata in questo paragrafo utilizza un’architettura a
rete neurale chiamata reti neurali siamesi: due rami condividono
architetture e parametri identici. L’uso di reti neurali siamesi per
contrassegnare i documenti fraudolenti è rappresentato nella Figura
8.16.
Figura 8.16
Quando è necessario verificare l’autenticità di un determinato
documento, prima classifichiamo il documento in base al suo layout e
tipo; poi lo confrontiamo con i modelli e i pattern previsti. Se devia
oltre una certa soglia, viene segnalato come falso; in caso contrario, è
considerato un documento autentico, veritiero. Per casi d’uso critici,
possiamo aggiungere un processo manuale nei casi in cui l’algoritmo
non riesce a classificare in modo definitivo il documento come
autentico o falso.
Per confrontare un documento con il modello previsto, utilizziamo
due reti neurali convoluzionali identiche con un’architettura siamese.
Le reti neurali convoluzionali hanno il vantaggio di individuare feature
locali invarianti ottimali e possono costruire rappresentazioni resistenti
alle distorsioni geometriche dell’immagine fornita in input. Si tratta
quindi di un algoritmo adatto a questo problema, poiché puntiamo a far
passare i documenti autentici e i documenti sotto test attraverso una
singola rete, e quindi confrontare i risultati alla ricerca di somiglianze.
Per raggiungere questo obiettivo, implementiamo i seguenti passaggi.
Supponiamo di voler sottoporre a test un documento. Per ogni classe
di documento, eseguiamo questi passaggi.
1. Leggere l’immagine archiviata del documento autentico. Lo
chiamiamo documento autentico. Il documento sotto test
dovrebbe avere l’aspetto del documento autentico.
2. Il documento autentico viene fatto passare attraverso i layer della
rete neurale per creare un vettore delle feature, che è la
rappresentazione matematica dei pattern del documento autentico.
È il vettore di feature 1 della Figura 8.16.
3. Il documento di cui dobbiamo verificare l’autenticità è chiamato
documento sotto test. Facciamo passare questo documento
attraverso una rete neurale simile a quella utilizzata per creare il
vettore delle feature del documento autentico. Il vettore delle
feature del documento sotto test è chiamato vettore delle feature
2.
4. Usiamo la distanza euclidea fra i due vettori delle feature per
calcolare la somiglianza fra il documento autentico e il
documento sotto test. Questo punteggio di somiglianza è
chiamato Measure of Similarity (MOS) ed è un numero compreso
fra 0 e 1. Un punteggio più alto rappresenta una minore distanza
fra i documenti e quindi una maggiore probabilità che i
documenti siano simili.
5. Se il punteggio di somiglianza calcolato dalla rete neurale è
inferiore a una certa soglia predefinita, contrassegniamo il
documento come fraudolento.
Vediamo come possiamo implementare in Python queste reti neurali
siamesi.
1. Per prima cosa, importiamo i pacchetti Python richiesti:
import random
import numpy as np
import tensorflow as tf
])
enconder2 = base_network(input_b)
([enconder1, enconder2])
Riepilogo
In questo capitolo, abbiamo prima esaminato i dettagli delle reti
neurali. Abbiamo iniziato osservando come le reti neurali si sono
evolute nel corso degli anni. Abbiamo studiato diversi tipi di reti
neurali, esaminando i vari elementi che le costituiscono. Abbiamo
studiato in modo approfondito l’algoritmo di discesa del gradiente, che
viene normalmente utilizzato per addestrare le reti neurali. Abbiamo
poi trattato varie funzioni di attivazione e studiato la loro applicazione
in una rete neurale. Abbiamo anche accennato al concetto di
trasferimento dell’apprendimento. Infine, abbiamo esaminato un
esempio concreto di come una rete neurale può essere utilizzata per
addestrare un modello di machine learning che potrà poi essere
impiegato per contrassegnare documenti contraffatti o fraudolenti.
Nel prossimo capitolo, esamineremo come possiamo usare tali
algoritmi per l’elaborazione del linguaggio naturale. Introdurremo
anche il concetto di word embedding ed esamineremo l’uso di reti
ricorrenti per l’elaborazione del linguaggio naturale. Infine,
scopriremo come si implementa l’analisi del sentiment.
Capitolo 9
Normalizzazione
La normalizzazione viene eseguita sui dati testuali di input per
migliorarne la qualità nel contesto dell’addestramento, o training, di un
modello di machine learning. La normalizzazione di solito prevede le
seguenti fasi di elaborazione:
conversione di tutto il testo in lettere maiuscole o minuscole;
rimozione della punteggiatura;
rimozione di numeri.
Notate che sebbene queste fasi di elaborazione siano in genere
necessarie, quelle effettivamente applicate dipendono dal problema da
risolvere e variano da caso a caso. Per esempio, se i numeri nel testo
rappresentassero qualcosa che potrebbe avere un certo valore nel
contesto del problema che stiamo cercando di risolvere, allora sarebbe
meglio non rimuoverli dal testo nella fase di normalizzazione.
Corpus
Il gruppo di documenti di input che stiamo utilizzando per risolvere
il problema in questione è chiamato corpus. Il corpus costituisce i dati
di input per il problema.
Tokenizzazione
Nell’elaborazione del linguaggio naturale, il primo compito è quello
di suddividere il testo in una lista di token, un processo chiamato
tokenizzazione. La granularità dei token risultanti varierà in base
all’obiettivo. Per esempio, ogni token può essere costituito da quanto
segue:
una parola;
una combinazione di parole;
una frase;
un paragrafo.
Stopword
Dopo la tokenizzazione a livello delle parole, abbiamo una lista di
parole utilizzate nel testo. Alcune di queste sono parole comuni,
presenti in quasi tutti i documenti, che non forniscono vere
informazioni. Queste parole sono chiamate stopword. Solitamente
vengono rimosse nella fase di elaborazione dei dati. Alcuni esempi di
stopword inglesi sono “was”, “we” e “the”.
Stemming e lemmi
Nei dati testuali, è probabile che la maggior parte delle parole sia
presente in forme leggermente variate. Ridurre ogni parola alla sua
origine o radice in una famiglia di parole si chiama stemming. Questo
processo viene utilizzato per raggruppare le parole in base al loro
significato di fondo, per ridurre il numero totale di parole che devono
essere analizzate. In sostanza, lo stemming riduce la condizionalità
complessiva del problema.
Un esempio in inglese: {use, used, using, uss} => use.
L’algoritmo più utilizzato per lo stemming della lingua inglese è
l’algoritmo di Porter.
La stemming è un processo rozzo, che può portare a tagliare le
estremità delle parole, creando parole errate. Per molti casi d’uso, ogni
parola è solo un identificatore di un livello nello spazio del problema e
le parole errate non contano. Se sono richieste parole scritte
correttamente, allora dovrebbe essere usata la lemmatizzazione invece
dello stemming.
NOTA
Gli algoritmi non sono dotati di buon senso. Per il cervello umano, trattare allo
stesso modo parole simili è semplice. Al contrario, un algoritmo deve essere
guidato da speciali criteri di raggruppamento.
NLTK
Il Natural Language Toolkit (NLTK) è il pacchetto più utilizzato per
la gestione delle attività di elaborazione del linguaggio naturale in
Python. NLTK è una delle librerie Python più antiche e utilizzate per
l’elaborazione del linguaggio naturale. NLTK è però ottimo, perché
fornisce un punto di partenza per la creazione di qualsiasi processo di
elaborazione del linguaggio naturale, fornendo tutti gli strumenti di
base, che potete concatenare per raggiungere l’obiettivo, senza dover
partire da zero. NLTK fornisce molti strumenti e, nel prossimo
paragrafo, scaricheremo il pacchetto ed esploreremo alcuni di essi.
Parliamo ora dell’elaborazione del linguaggio naturale BoW-based.
import pandas as pd
corpus.append(review)
y = dataset.iloc[:, 1].values
classifier.fit(X_train, y_train)
Figura 9.2
vasta raccolta di testi su cui si basa l’analisi) con oltre 1,6 milioni di
tweet. I tweet di questo dataset sono stati etichettati con una delle tre
polarità: 0 per negativo, 2 per neutro e 4 per positivo. Oltre al testo del
tweet, il corpus fornisce l’ID del tweet, la data, il flag e l’autore del
tweet. Ora vediamo ciascuna delle operazioni da eseguire sui tweet per
ottenere un classificatore addestrato.
1. I tweet vengono prima suddivisi in singoli token (tokenizzazione).
2. L’output della tokenizzazione crea un Bag-of-Words, ovvero una
collezione di parole presenti nel testo.
3. I tweet vengono ulteriormente filtrati rimuovendo numeri,
punteggiatura e stop-word. Come ormai sappiamo, le stop-word
sono parole estremamente comuni, come “is”, “am”, “are” e
“the”. Poiché non contengono informazioni aggiuntive, vanno
rimosse.
4. Inoltre, i caratteri non alfabetici, come “#”, “@” e i numeri,
vanno rimossi utilizzando il pattern matching, poiché non hanno
alcuna rilevanza per l’analisi del sentiment. Per considerare solo i
caratteri alfabetici e ignorare il resto vengono impiegate
espressioni regolari. Questo aiuta a ridurre il disordine dal flusso
di Twitter.
5. Gli esiti della fase precedente vengono portati alla fase di
stemming. In questa fase le parole derivate vengono ridotte alla
loro radice. Per esempio, la parola “fisher” ha la stessa radice di
“fishing” e “fishes”. Per questo utilizziamo la libreria standard di
elaborazione del linguaggio naturale, che fornisce vari algoritmi,
come lo stemming di Porter.
6. Una volta elaborati, i dati vengono convertiti in una matrice dei
termini del documento (TDM, Term Document Matrix). Tale
matrice rappresenta il termine e la sua frequenza nel corpus
filtrato.
7. Dalla matrice precedente, il tweet raggiunge il classificatore
addestrato (essendo addestrato, sa come elaborare i tweet), che
calcola l’importanza della polarità del sentiment (SPI, Sentiment,
Polarity Importance) di ogni parola con un valore da -5 a +5. Il
segno, negativo o positivo, specifica il tipo di emozioni
rappresentate da quella particolare parola; la grandezza
rappresenta la forza del sentiment. Ciò significa che il tweet può
essere classificato come negativo o positivo (vedi Figura 9.3).
Una volta calcolata la polarità dei singoli tweet, sommiamo il
valore SPI complessivo per trovare il sentiment aggregato: per
esempio, una polarità complessiva maggiore di 1 indica che il
sentiment aggregato dei tweet nell’arco di tempo osservato è
positivo.
NOTA
Per recuperare i tweet in tempo reale, usiamo la libreria Scala
Twitter4J, una libreria Java che offre un pacchetto per un’API di
streaming in tempo reale dei tweet. L’API richiede all’utente di
registrare un account da sviluppatore in Twitter e di compilare alcuni
parametri di autenticazione. Questa API consente poi di ottenere tweet
casuali o di filtrare i tweet utilizzando parole chiave a scelta. Per
recuperare i tweet relativi alle parole chiave scelte abbiamo utilizzato
appositi filtri.
Figura 9.3
L’analisi del sentiment ha diverse applicazioni. Può essere impiegata
per classificare il feedback dei clienti. L’analisi della polarità dei social
media può essere utilizzata dai governi per valutare l’efficacia delle
loro politiche. Può anche quantificare il successo delle campagne
pubblicitarie.
import pandas as pd
df.head()
Tenete presente che il dataset contiene 2000 recensioni di film. Di
queste, metà sono negative e metà positive.
3. Ora iniziamo a preparare il dataset per l’addestramento del
modello. Innanzitutto, eliminiamo dai dati tutti i valori mancanti:
df.dropna(inplace = True)
])
predictions = text_clf_nb.predict(X_test)
Riepilogo
In questo capitolo abbiamo trattato gli algoritmi di elaborazione del
linguaggio naturale. Innanzitutto, abbiamo esaminato la terminologia
relativa all’argomento. Successivamente, abbiamo esaminato la
metodologia Bag-of-Words per implementare una strategia di
elaborazione del linguaggio naturale. Poi abbiamo esaminato il
concetto di word embedding e l’uso delle reti neurali nell’elaborazione
del linguaggio naturale. Infine, abbiamo esaminato un esempio reale,
in cui abbiamo utilizzato i concetti sviluppati in questo capitolo per
prevedere il sentiment delle recensioni dei film partendo dai loro testi.
Con i concetti esposti i questo capitolo, dovreste essere in grado di
utilizzare l’elaborazione del linguaggio naturale per classificare i testi
e per svolgere analisi del sentiment.
Nel prossimo capitolo, esamineremo i motori di raccomandazione.
Studieremo diversi tipi di motori e come possono essere utilizzati per
risolvere alcuni problemi concreti.
Capitolo 10
Motori di raccomandazione
Introduzione ai sistemi di
raccomandazione
I sistemi di raccomandazione sono metodi sviluppati dai ricercatori
per prevedere gli articoli cui è più probabile che un utente sia
interessato. La capacità dei sistemi di raccomandazione di fornire
suggerimenti personalizzati sugli articoli li rende forse la tecnologia
più importante nel contesto del mondo degli acquisti online.
Quando vengono utilizzati nelle applicazioni di e-commerce, i
motori di raccomandazione impiegano algoritmi sofisticati per
migliorare l’esperienza d’acquisto degli acquirenti e per consentire ai
fornitori di servizi di personalizzare i prodotti in base alle preferenze
degli utenti.
NOTA
Nel 2009, Netflix ha offerto un milione di dollari a chiunque fosse in grado di
fornire un algoritmo in grado di migliorare il suo motore di raccomandazione
(Cinematch) di oltre il 10%. Il premio è stato vinto dal team Pragmatic Chaos di
BellKor.
Sia Mike sia Elena hanno mostrato interesse per gli stessi
documenti, Doc1 e Doc2.
Sulla base dei loro pattern storici simili, possiamo classificarli
come utenti simili.
Se ora Elena legge il Doc3, possiamo suggerire il Doc3 anche a
Mike.
NOTA
Tenete presente che questa strategia di suggerire articoli agli utenti in base alla
loro cronologia non funziona sempre.
Figura 10.4
Figura 10.5
Generazione di raccomandazioni
Per generare le raccomandazioni, possiamo moltiplicare le due
matrici. È più probabile che un utente sia interessato a un articolo che
si presenta frequentemente insieme a un articolo cui ha assegnato una
valutazione elevata:
Matrice[S] × Matrice[U] = Matrice[R]
Questo calcolo è mostrato graficamente nella Figura 10.6.
Viene generata una matrice delle raccomandazioni per ciascuno
degli utenti. I numeri nella matrice, Matrice[R], quantificano
l’interesse ipotizzato di un utente per ciascuno degli articoli. Per
esempio, nella matrice risultato, il quarto articolo ha il numero più
elevato, 58. Quindi questo articolo sarebbe altamente raccomandato
per questo specifico utente.
Ora, esaminiamo i limiti dei diversi sistemi di raccomandazione.
Figura 10.6
Dati limitati
Un numero limitato di recensioni rende difficile per i sistemi di
raccomandazione misurare con precisione le somiglianze fra gli utenti.
Campi di applicazione
Vediamo dove vengono impiegati i sistemi di raccomandazione
nella pratica:
due terzi dei film consigliati su Netflix;
il 35% delle vendite di Amazon;
su Google News, le raccomandazioni generano il 38% di clic in
più;
i tentativi di prevedere l’appetibilità di un articolo per un utente si
basano sulle valutazioni passate;
è possibile suggerire corsi agli studenti universitari in base alle
loro esigenze e preferenze;
è possibile abbinare i curricula ai lavori sui portali di lavoro
online.
Ora, proviamo a utilizzare un motore di raccomandazione per
risolvere un problema concreto.
import numpy as np
Avatar_user_rating.head()
similar_to_Avatar =movie_matrix.corrwith(Avatar_user_rating)
corr_Avatar = pd.DataFrame(similar_to_Avatar,
columns = ['correlation'])
corr_Avatar.dropna(inplace = True)
corr_Avatar = corr_Avatar.join(df_ratings['number_of_ratings'])
corr_Avatar.head()
Riepilogo
In questo capitolo abbiamo parlato dei motori di raccomandazione.
Abbiamo studiato la scelta del motore di raccomandazione giusto in
base al problema che stiamo cercando di risolvere. Abbiamo anche
visto come preparare i dati per i motori di raccomandazione, creando
una matrice di similarità. Abbiamo anche imparato a utilizzare i motori
di raccomandazione per risolvere problemi pratici, come suggerire film
agli utenti in base alle loro scelte passate.
Nel prossimo capitolo, ci concentreremo sugli algoritmi utilizzati
per comprendere ed elaborare i dati.
Parte III
Argomenti avanzati
Il teorema CAP
Nel 1998, Eric Brewer propose un teorema, che in seguito divenne
famoso con il nome di teorema CAP, il quale evidenzia i vari
compromessi legati alla progettazione di un sistema di archiviazione
distribuito.
Per comprendere il teorema CAP, in primo luogo, definiamo le
seguenti tre caratteristiche dei sistemi di archiviazione distribuiti: la
coerenza, la disponibilità e la tolleranza al partizionamento. CAP,
infatti, è un acronimo composto da queste tre caratteristiche:
Coerenza (Consistency): l’archivio distribuito è costituito da vari
nodi. Ciascuno di questi può essere utilizzato per leggere, scrivere
o aggiornare i record. La coerenza garantisce che a un certo
tempo, t1, indipendentemente da quale nodo usiamo per leggere i
dati, otterremo lo stesso risultato. Ogni operazione di lettura
restituisce i dati più recenti coerenti nell’archivio distribuito o
restituisce un messaggio di errore.
Disponibilità (Alailability): garantisce che qualsiasi nodo nel
sistema di archiviazione distribuita sarà in grado di gestire
immediatamente la richiesta, con o senza coerenza.
Tolleranza al partizionamento (Partition-tolerance): in un sistema
distribuito, più nodi sono collegati tramite una rete di
comunicazione. La tolleranza al partizionamento garantisce che,
in caso di un errore di comunicazione che interessi un piccolo
sottoinsieme dei nodi (uno o più), il sistema rimanga operativo.
Notate che per garantire la tolleranza al partizionamento, i dati
devono essere replicati su un numero sufficiente di nodi.
Utilizzando queste tre caratteristiche, il teorema CAP riassume
accuratamente i compromessi cui deve essere sottoposta l’architettura
e la progettazione di un sistema distribuito. Nello specifico, il teorema
CAP afferma che, in un sistema di archiviazione, possiamo avere solo
due delle seguenti caratteristiche: coerenza, disponibilità o tolleranza
al partizionamento.
Questo vincolo è rappresentato nella Figura 11.1.
In base al teorema CAP possiamo avere tre tipi di sistemi di
archiviazione distribuita:
un sistema CA (che implementa la coerenza e la disponibilità);
un sistema AP (che implementa la disponibilità e la tolleranza al
partizionamento);
un sistema CP (che implementa la coerenza e la tolleranza al
partizionamento).
Esaminiamoli uno per uno.
Figura 11.1
Sistemi CA
I sistemi tradizionali, a nodo singolo, sono sistemi CA. Questo
perché se non disponiamo di un sistema distribuito, non dobbiamo
preoccuparci della tolleranza al partizionamento. In tal caso, possiamo
avere un sistema che offre coerenza e disponibilità, cioè un sistema
CA.
I tradizionali database a nodo singolo come Oracle o MySQL sono
tutti esempi di sistemi CA.
Sistemi AP
I sistemi AP sono i sistemi di archiviazione distribuiti ottimizzati
per la disponibilità. Progettati come sistemi altamente reattivi, possono
sacrificare la coerenza, se necessario, per accettare dati ad alta
velocità. Ciò significa che si tratta di sistemi di archiviazione
distribuiti progettati per gestire immediatamente le richieste degli
utenti. Le richieste tipiche prevedono la lettura o scrittura di dati che
cambiano velocemente. I tipici sistemi AP sono utilizzati nei sistemi di
monitoraggio in tempo reale, come nel caso delle reti di sensori. I
database distribuiti ad alta velocità, come Cassandra, sono buoni
esempi di sistemi AP.
Vediamo dove può essere utilizzato un sistema AP. Se Transport
Canada desiderasse monitorare il traffico su una delle autostrade di
Ottawa attraverso la rete di sensori installati in diversi punti
dell’autostrada, dovremmo consigliarle un sistema AP per
implementare l’archiviazione distribuita dei dati.
Sistemi CP
I sistemi CP offrono sia la coerenza sia la tolleranza al
partizionamento. Ciò significa che questi sono i sistemi di
archiviazione distribuiti ottimizzati per garantire la coerenza, prima
che un processo di lettura possa recuperare un valore.
Un tipico caso d’uso per i sistemi CP è l’archiviazione di documenti
in formato JSON. I datastore di documenti come MongoDB sono
sistemi CP, ottimizzati per la coerenza in un ambiente distribuito.
L’archiviazione distribuita dei dati sta diventando sempre più
importante in una moderna infrastruttura IT e dovrebbe essere
progettata con attenzione in base alle feature presenti nel dataset e ai
requisiti del problema da risolvere. La classificazione
dell’archiviazione dei dati in sistemi CA, AP e CP ci aiuta a
comprendere i vari compromessi cui deve sottostare la progettazione di
sistemi di archiviazione dei dati.
Esaminiamo ora gli algoritmi per i dati in streaming.
Codifica Huffman
La codifica di Huffman è uno dei metodi più antichi di
compressione dei dati e si basa sulla creazione di un albero di
Huffman, utilizzato sia per codificare sia per decodificare i dati. La
codifica di Huffman può rappresentare il contenuto dei dati in una
forma più compatta sfruttando il fatto che alcuni dati (per esempio
certi caratteri) appaiono più frequentemente in un flusso di dati.
Utilizzando codifiche di lunghezza differente (più corta per i caratteri
più frequenti e più lunga per quelli meno frequenti), i dati consumano
meno spazio.
Esaminiamo la terminologia impiegata nella codifica di Huffman.
Codifica: rappresenta il metodo di rappresentazione dei dati, nel
passaggio da una forma all’altra. Vorremmo che la forma
risultante fosse più compatta dell’originale.
Codeword: è un determinato carattere in forma codificata.
Codifica a lunghezza fissa: si ha quando ogni carattere codificato,
ovvero ogni codeword, utilizza lo stesso numero di bit.
Codifica a lunghezza variabile: le codeword possono utilizzare un
numero differente di bit.
Valutazione del codice: il numero previsto di bit per codeword.
Codici senza prefisso: nessuna codeword è prefisso di un’altra
codeword.
Decodifica: un codice di lunghezza variabile deve essere libero da
qualsiasi prefisso.
Per comprendere questi termini, esaminiamo la seguente tabella.
Codifica a lunghezza Codifica a lunghezza
Carattere Frequenza
fissa variabile
L 0,45 000 0
M 0,13 001 101
N 0,12 010 100
X 0,16 011 111
Y 0,09 100 1101
Z 0,05 101 1100
analyzer = SentimentIntensityAnalyzer()
Figura 11.2
Riepilogo
In questo capitolo, abbiamo esaminato la progettazione di algoritmi
basati su dati. Ci siamo concentrati su tre aspetti degli algoritmi per i
dati: archiviazione, compressione e streaming.
Abbiamo esaminato come le feature presenti nel dataset possono
influenzare la progettazione del sistema di archiviazione dei dati.
Abbiamo esaminato i diversi tipi di algoritmi di compressione dei dati.
Quindi, abbiamo studiato un esempio pratico d’uso degli algoritmi per
flussi di dati.
Nel prossimo capitolo esamineremo gli algoritmi crittografici.
Impareremo a utilizzare la potenza di questi algoritmi per proteggere i
messaggi in transito e archiviati.
Capitolo 12
Crittografia
Terminologia di base
Esaminiamo la terminologia di base impiegata in crittografia.
Cifratura: un algoritmo che esegue una determinata funzione
crittografica.
Testo in chiaro: i dati di partenza, che possono essere un file di
testo, un video, un’immagine o una voce digitalizzata. In questo
capitolo rappresenteremo il testo in chiaro come P, da plain text.
Testo cifrato: il testo codificato che si ottiene dopo aver applicato
la crittografia al testo in chiaro. In questo capitolo lo
rappresenteremo come C.
Sistema di cifratura: un insieme di componenti software
crittografici. Quando due nodi desiderano scambiarsi messaggi
crittografati, devono innanzitutto concordare un sistema di
cifratura, per garantire che entrambi utilizzino esattamente la
stessa implementazione delle funzioni crittografiche.
Crittografia: il processo di conversione del testo in chiaro, P, in
testo cifrato, C. Matematicamente, è rappresentato da encrypt(P )
= C.
Decrittografia: il processo di riconversione del testo cifrato in
testo in chiaro. Matematicamente, è rappresentato da decrypt(C )
= P.
Criptoanalisi: i metodi utilizzati per analizzare la resistenza degli
algoritmi crittografici. L’analista cerca di recuperare il testo in
chiaro senza accedere al segreto.
Informazioni di identificazione personale: sono le informazioni
che possono essere utilizzate per tracciare l’identità di un
individuo, se utilizzate da sole o insieme ad altri dati rilevanti.
Alcuni esempi includono informazioni riservate, come il codice
fiscale, la data di nascita o il cognome da nubile della madre.
I requisiti di sicurezza
È importante innanzitutto comprendere le esatte esigenze di
sicurezza di un sistema. Questo ci aiuterà a utilizzare la tecnica
crittografica più adatta e a scoprire le potenziali falle in un sistema. Per
fare ciò, dobbiamo prima comprendere meglio le esigenze del sistema,
attraverso questi tre passaggi:
identificare le entità;
definire gli obiettivi di sicurezza;
valutare la sensibilità dei dati.
Esaminiamo questi passaggi uno per uno.
Identificare le entità
Un modo per identificare le entità consiste nel rispondere alle
seguenti quattro domande, che ci aiuteranno a comprendere le esigenze
del sistema in termini di sicurezza.
Quali applicazioni devono essere protette?
Da chi le stiamo proteggendo?
Dove dovremmo proteggerle?
Perché le proteggiamo?
Una volta compresi meglio questi requisiti, possiamo stabilire gli
obiettivi di sicurezza del nostro sistema digitale.
Definire gli obiettivi di sicurezza
Gli algoritmi crittografici vengono in genere utilizzati per soddisfare
uno o più obiettivi di sicurezza.
Autenticazione: in poche parole, è il modo in cui dimostriamo che
un utente è proprio chi afferma di essere. Attraverso il processo di
autenticazione, ci assicuriamo che l’identità di un utente sia
certificata. Il processo di autenticazione inizia facendo in modo
che l’utente presenti la propria identità. Poi deve fornire
informazioni che sono note solo all’utente e che quindi possono
essere prodotte solo da lui.
Riservatezza: i dati che devono essere protetti sono chiamati dati
sensibili. La riservatezza consiste nel far conoscere i dati sensibili
ai soli utenti autorizzati. Per tutelare la riservatezza dei dati
sensibili durante il loro transito o la loro conservazione, è
necessario renderli illeggibili agli utenti non autorizzati. Ciò si
ottiene utilizzando algoritmi di crittografia, di cui parleremo in
questo capitolo.
Integrità: è il processo per stabilire che i dati non sono stati
alterati in alcun modo durante il loro transito o mentre erano
archiviati. Per esempio, TCP/IP (Transmission Control
Protocol/Internet Protocol) utilizza algoritmi di checksum o CRC
(Cyclic Redundancy Check) per verificare l’integrità dei dati.
Non ripudiabilità: è il concetto in base al quale il mittente delle
informazioni riceve la conferma che i dati sono stati ricevuti dal
destinatario e il destinatario riceve la conferma dell’identità del
mittente. Ciò fornisce prove inconfutabili che un messaggio sia
stato inviato e poi ricevuto, fatto che può essere utilizzato in
seguito per dimostrare la ricezione dei dati ed eventuali errori
nella comunicazione.
Valutare la sensibilità dei dati
È importante classificare la natura dei dati. Dobbiamo anche pensare
a quanto gravi potrebbero essere le conseguenze se i dati venissero
compromessi. La classificazione dei dati ci aiuta a scegliere
l’algoritmo crittografico corretto. Esiste più di un modo per
classificare i dati, in base alla sensibilità delle informazioni che essi
contengono.
Dati pubblici o non classificati: tutto ciò che è disponibile per il
consumo pubblico. Per esempio, le informazioni trovate sul sito
web di un’azienda o sul portale informativo di un governo.
Dati interni o riservati: sebbene non siano destinati alla
diffusione pubblica, l’esposizione di questi dati al pubblico non
dovrebbe avere conseguenze dannose. Per esempio, se le e-mail
di un dipendente che si lamenta del proprio manager venissero
divulgate, la cosa potrebbe essere imbarazzante per l’azienda, ma
senza conseguenze dannose.
Dati sensibili o segreti: i dati che non dovrebbero essere destinati
alla diffusione pubblica e la cui esposizione al pubblico avrebbe
conseguenze dannose per un individuo o un’organizzazione. Per
esempio, divulgare i dettagli di un futuro iPhone potrebbe
danneggiare gli obiettivi aziendali di Apple e offrire un vantaggio
ai rivali, come Samsung.
Dati altamente sensibili: sono i dati top-secret. Si tratta di
informazioni che, se divulgate, sarebbero estremamente dannose
per l’organizzazione. Può trattarsi di codici fiscali dei clienti,
numeri di carte di credito o altre informazioni altamente sensibili.
I dati top-secret sono protetti da più livelli di sicurezza e
richiedono un’autorizzazione speciale per l’accesso.
NOTA
In generale, i progetti di sicurezza più sofisticati sono molto più lenti dei semplici
algoritmi. È importante trovare il giusto equilibrio fra sicurezza e prestazioni del
sistema.
Progettazione di cifrature
Progettare una cifratura significa trovare un algoritmo in grado di
criptare i dati sensibili in modo che un processo o un utente non
autorizzato non possano accedervi. Sebbene nel tempo le cifrature
siano diventate sempre più sofisticate, i principi su cui si basano
rimangono invariati.
Iniziamo esaminando alcune codifiche relativamente semplici, che
ci aiuteranno a comprendere i principi di base utilizzati nella
progettazione di algoritmi crittografici.
Cifratura a sostituzione
I cifrari a sostituzione sono in uso da centinaia di anni in varie
forme. Come indica il nome, si basano su un semplice concetto: la
sostituzione del testo in chiaro con altri caratteri, in un modo
predeterminato e organizzato.
Esaminiamo i passaggi da svolgere.
1. Innanzitutto, mappare ogni carattere su un altro carattere.
2. Quindi, codificare e convertire il testo in chiaro in testo cifrato,
sostituendo ogni carattere nel testo in chiaro con il carattere
corrispondente nel testo cifrato, utilizzando la mappa di
sostituzione.
3. Per decodificare il testo cifrato, basta applicare all’inverso la
mappa di sostituzione.
Vediamo alcuni esempi:
Cifratura di Cesare
Nei cifrari di Cesare, la mappa di sostituzione viene creata
sostituendo ogni carattere con il secondo carattere successivo,
nell’ordine alfabetico. Questa mappatura è descritta nella Figura 12.1.
Figura 12.1
rotation = 3
P = 'CALM'; C = ''
for letter in P:
C = C+ (chr(ord(letter) + rotation))
Rotazione 13 (ROT13)
ROT13 è un’altra crittografia a sostituzione. In ROT13, la mappa di
sostituzione viene creata sostituendo ogni carattere con il tredicesimo
carattere alla sua destra, come si può vedere nella Figura 12.2.
Figura 12.2
P = 'CALM'
C = ''
C = codecs.encode(P, 'rot_13')
La cifratura a trasposizione
Nelle cifrature a trasposizione, i caratteri del testo in chiaro vengono
trasposti. Esaminiamo i passaggi.
1. Creare la matrice di trasposizione e sceglierne le dimensioni.
Dovrebbe essere abbastanza grande da contenere la stringa di
testo in chiaro.
2. Compilare la matrice scrivendo orizzontalmente tutti i caratteri
della stringa.
3. Leggere i caratteri della stringa verticalmente, nella matrice di
trasposizione.
Vediamo un esempio.
Prendiamo il testo in chiaro Ottawa Rocks (P).
Per prima cosa, codifichiamo P. Per farlo, useremo una matrice 3 ×
4 e scriveremo il testo in chiaro orizzontalmente:
O t t a
w a R o
c k s
Ora leggeremo la stringa verticalmente, e il testo cifrato sarà
OwctaktRsao.
NOTA
I tedeschi usarono una cifratura chiamata ADFGVX nella Prima guerra
mondiale, che usava cifrature a trasposizione e a sostituzione. Anni dopo, fu
decifrato da George Painvin.
La crittografia hash
La funzione di hash crittografico impiega un algoritmo matematico
che può essere utilizzato per creare come “un’impronta digitale
univoca” di un messaggio, creando un output di dimensioni fisse, un
codice hash, derivato dal testo in chiaro.
Matematicamente, l’operazione ha il seguente aspetto:
C1 = funzioneHash(P1)
P1 è il testo in chiaro, che rappresenta i dati di input.
C1 è un codice hash di lunghezza fissa generato dalla
funzioneHash crittografica.
L’operazione è rappresentata nella Figura 12.3. I dati, di lunghezza
variabile, vengono convertiti in un codice hash di lunghezza fissa
tramite una funzione hash unidirezionale.
Figura 12.3
L’algoritmo MD5-tolerated
MD5 è stato sviluppato da Poul-Henning Kamp nel 1994 per
sostituire MD4 e genera un codice hash a 128 bit. MD5 è un algoritmo
relativamente semplice e vulnerabile alle collisioni. Non deve pertanto
essere utilizzato per applicazioni che non tollerano collisioni.
Vediamo un esempio. Per generare un codice hash MD5 in Python,
utilizzeremo la nota libreria open source passlib, che implementa oltre
30 algoritmi di hashing per password. Se non l’avete già installata,
utilizzate la riga seguente in un notebook Jupyter:
!pip install passlib
L’algoritmo SHA
SHA è stato sviluppato dal National Institute of Standards and
Technology (NIST). Vediamo come possiamo usare Python per creare
un codice hash usando l’algoritmo SHA:
from passlib.hash import sha512_crypt
La crittografia simmetrica
In crittografia, una chiave è una combinazione di numeri utilizzata
per codificare un testo in chiaro utilizzando un algoritmo a scelta.
Nella crittografia simmetrica, utilizziamo la stessa chiave sia per la
crittografia sia per la decrittografia. Se la chiave utilizzata per la
crittografia simmetrica è K, allora vale la seguente equazione:
EK(P ) = C
P è il testo in chiaro e C è il testo cifrato.
Per la decrittografia, usiamo la stessa chiave, K, per riconvertire C
in P:
DK(C ) = P
Questo processo è rappresentato nella Figura 12.4.
Figura 12.4
2. Generiamo la chiave:
file.close()
encrypted = f.encrypt(message)
decrypted = f.decrypt(encrypted)
La crittografia asimmetrica
La crittografia asimmetrica è stata ideata negli anni Settanta per
affrontare alcuni punti deboli della crittografia simmetrica, di cui
abbiamo trattato nel paragrafo precedente. Il primo passo nella
crittografia asimmetrica consiste nel generare due chiavi che sembrano
totalmente diverse ma sono correlate algoritmicamente. Una di esse
viene scelta come chiave privata, Kpr, e l’altra come chiave pubblica,
Kpu. Matematicamente, possiamo rappresentare la situazione come
segue:
EKpr(P) = C
Al solito, P è il testo in chiaro e C è il testo cifrato.
Possiamo decifrare il risultato come segue:
DKpu(C) = P
Le chiavi pubbliche possono essere distribuite liberamente, mentre
le chiavi private devono essere tenute segrete dal proprietario della
coppia di chiavi.
Il principio fondamentale è che se si crittografa un contenuto con
una delle chiavi, l’unico modo per decifrarlo consiste nell’utilizzare
l’altra chiave. Per esempio, se crittografiamo i dati utilizzando la
chiave pubblica, sarà necessario decrittografarli utilizzando l’altra
chiave, ovvero la chiave privata. Vediamo ora uno dei protocolli
fondamentali della crittografia asimmetrica, l’handshake SSL/TLS
(Secure Sockets Layer/Transport Layer Security), che si occupa di
stabilire una connessione fra due nodi utilizzando la crittografia
asimmetrica.
Figura 12.5
Figura 12.6
L’autorità di certificazione chiede a un utente di dimostrare la
propria identità, con standard diversi per individui e organizzazioni.
Ciò potrebbe comportare semplicemente l’attestazione della proprietà
di un nome di dominio, oppure potrebbe comportare un processo più
rigoroso che prevede la prova fisica dell’identità, a seconda del tipo di
certificato digitale che l’utente sta cercando di ottenere. Se l’autorità di
certificazione si convince che l’utente sia effettivamente chi afferma di
essere, l’utente fornisce all’autorità di certificazione la propria chiave
di crittografia pubblica su un canale sicuro. L’autorità di certificazione
utilizza queste informazioni per creare un certificato digitale che
contiene informazioni sull’identità dell’utente e sulla sua chiave
pubblica. Questo certificato è firmato digitalmente dall’autorità di
certificazione. L’utente può quindi mostrare il proprio certificato a
chiunque voglia verificare la sua identità, senza doverlo inviare tramite
un canale sicuro, in quanto il certificato stesso non contiene alcuna
informazione sensibile. Chi riceve il certificato non è costretto a
verificare l’identità dell’utente: può semplicemente verificare che il
certificato sia valido, verificando la firma digitale dell’autorità di
certificazione, la quale convalida che la chiave pubblica contenuta nel
certificato appartiene alla persona o all’organizzazione indicata sul
certificato stesso.
NOTA
La chiave privata emessa dall’autorità di certificazione per un’organizzazione è
l’anello più debole della catena dell’infrastruttura PKI. Se un soggetto entra in
possesso della chiave privata di Microsoft, per esempio, può installare software
dannoso su milioni di computer in tutto il mondo, simulando un aggiornamento
di Windows.
Attacchi Man-in-the-Middle
Una delle minacce da cui vorremmo proteggere il nostro modello
sono gli attacchi Man-in-the-Middle, che si verificano quando un
intruso tenta di intercettare una comunicazione privata relativa alla
distribuzione di un modello di machine learning addestrato.
Proviamo a capire come funzionano gli attacchi Man-in-the-Middle
utilizzando uno scenario di esempio. Supponiamo che Bob e Alice
vogliano scambiarsi messaggi usando un’infrastruttura a chiave
privata, PKI.
1. Bob sta usando {PrBob, PuBob} e Alice sta usando {PrAlice, PuAlice}.
Bob ha creato un messaggio, MBob, e Alice ha creato un
messaggio, MAlice ed entrambi vogliono scambiarsi questi
messaggi in modo sicuro.
2. Inizialmente, devono scambiarsi le chiavi pubbliche, per stabilire
una connessione sicura. Ciò significa che Bob utilizza PuAlice per
crittografare MBob prima di inviare il messaggio ad Alice.
3. Supponiamo di avere un estraneo, X, che usa {PrX, PuX}. Egli è in
grado di intercettare gli scambi di chiavi pubbliche fra Bob e
Alice e di sostituirle con il proprio certificato pubblico.
4. Bob invia MBob ad Alice, crittografandolo con PuX invece di usare
PuAlice, ritenendolo, erroneamente, il certificato pubblico di Alice.
L’estraneo X intercetta la comunicazione. Intercetta il messaggio
MBob e lo decifra utilizzando PrBob.
Questo attacco Man-in-the-Middle è rappresentato nella Figura 12.7.
Ora, vediamo come possiamo prevenire questi tipi di attacchi.
Figura 12.7
import ssl
Attacchi masquerading
L’estraneo X finge di essere un utente autorizzato, Bob, e ottiene
l’accesso ai dati sensibili, che in questo caso è il nostro modello
addestrato. Dobbiamo proteggere il modello da eventuali modifiche
non autorizzate.
Un modo per proteggere il nostro modello addestrato da questo
attacco masquerading è crittografarlo con la chiave privata di un
utente autorizzato. Una volta crittografato, chiunque può leggere e
utilizzare il modello decifrandolo tramite la chiave pubblica dell’utente
autorizzato, che si trova nel suo certificato digitale. Nessuno, però può
apportarvi modifiche non autorizzate.
model.fit(X_train, y_train)
filename_sec = "myModel_sec.sav"
key_file.write(key)
file.write(encrypted_data)
encrypt(filename_source, load_key())
file.write(decrypted_data)
decrypt(filename_sec, load_key())
Terminologia
Esaminiamo alcuni dei termini che possono essere utilizzati per
quantificare la qualità degli algoritmi per dati su larga scala.
Latenza
La latenza è il tempo end-to-end impiegato per eseguire un singolo
calcolo. Se Compute1 rappresenta un singolo calcolo che inizia in t1 e
termina in t2, allora possiamo dire quanto segue:
Latenza = t2 – t1
Throughput (produttività)
Nel contesto del calcolo parallelo, il throughput è il numero di
singoli calcoli che possono essere eseguiti contemporaneamente. Per
esempio, se nel tempo t1 possiamo eseguire quattro calcoli simultanei,
C1, C2, C3 e C4, allora il throughput è 4.
Elasticità
La capacità dell’infrastruttura di reagire a un improvviso aumento
dei requisiti di elaborazione, fornendo più risorse.
NOTA
I tre colossi del cloud computing, Google, Amazon e Microsoft, possono fornire
infrastrutture altamente elastiche. A causa delle dimensioni gigantesche dei loro
pool di risorse condivise, sono pochissime le aziende che hanno la possibilità di
eguagliare l’elasticità delle infrastrutture offerte da queste tre società.
Legge di Amdahl
Gene Amdahl è stato uno dei primi a studiare l’elaborazione
parallela negli anni Sessanta, quando sviluppò la legge di Amdahl, che
ancora oggi è applicabile e può fungere da base per comprendere i vari
compromessi legati alla progettazione di una soluzione di calcolo
parallelo. La legge di Amdahl può essere spiegata come segue.
Si basa sul concetto che in qualsiasi processo informatico, non tutti i
processi possono essere eseguiti in parallelo. Ci sarà sempre una parte
sequenziale del processo che non può essere parallelizzata.
Vediamo un esempio: supponiamo di voler leggere un numero
elevato di file archiviati su un computer e di voler addestrare un
modello di machine learning utilizzando i dati trovati in questi file.
L’intero processo è chiamato P. È ovvio che P può essere suddiviso
nei due sottoprocessi seguenti:
P1 scansiona i file nella directory, crea una lista di nomi di file che
corrispondono al file di input e li passa avanti;
P2 legge i file, crea la pipeline di elaborazione dati, elabora i file e
addestra il modello.
Figura 13.1
Figura 13.5
print(end_time – start_time)
Notate che, per creare questo array, NumPy ha impiegato circa 1,13
secondi e CuPy circa 0,012 secondi: l’inizializzazione di questo array è
92 volte più veloce su GPU.
Cluster computing
Il cluster computing è uno dei modi per implementare l’elaborazione
parallela di algoritmi per dati su larga scala. Nel cluster computing,
abbiamo più nodi collegati tramite una rete ad altissima velocità. Gli
algoritmi per dati su larga scala vengono inviati come job. Ogni job è
suddiviso in più task e ciascun task viene eseguito su un nodo.
Apache Spark è uno dei modi più diffusi per implementare il cluster
computing. In Apache Spark, i dati vengono convertiti in dataset fault-
tolerant distribuiti, chiamati dataset resilienti distribuiti (RDD), che
sono l’astrazione principale di Apache Spark. Sono raccolte
immutabili di elementi che possono essere gestite in parallelo. Questi
dataset sono divisi in partizioni e distribuiti fra i nodi, come vediamo
nella Figura 13.7). Attraverso questa struttura di dati parallela,
possiamo eseguire gli algoritmi in parallelo.
Figura 13.7
Strategia ibrida
Il cloud computing si sta diffondendo sempre più per eseguire
algoritmi per dati su larga scala. Questo ci offre l’opportunità di
combinare le strategie look outside e look within. Possiamo farlo
impiegando una o più GPU in più macchine virtuali (Figura 13.8).
Sfruttare al meglio l’architettura ibrida è un compito non banale.
Occorre suddividere i dati in più partizioni. Le attività a elevata
intensità di calcolo che richiedono meno dati vengono parallelizzate
all’interno di ciascun nodo tramite le GPU.
Figura 13.8
Riepilogo
In questo capitolo, abbiamo esaminato la progettazione di algoritmi
paralleli e le problematiche di progettazione di algoritmi per dati su
larga scala e abbiamo esaminato l’utilizzo del calcolo parallelo e delle
GPU per implementarli. Abbiamo anche esaminato come utilizzare i
cluster Spark per implementare questo genere di algoritmi.
Abbiamo accennato anche alle problematiche degli algoritmi per
dati su larga scala, esaminando i problemi relativi alla
parallelizzazione degli algoritmi e i potenziali colli di bottiglia che si
possono formare.
Nel prossimo capitolo esamineremo alcuni aspetti pratici
dell’implementazione di algoritmi.
Capitolo 14
Considerazioni pratiche
Introduzione
Oltre a progettare, sviluppare e infine sottoporre a test un algoritmo,
in molti casi è importante considerare alcuni aspetti pratici del fatto di
iniziare a fare affidamento su una macchina per risolvere un problema
concreto. Per alcuni algoritmi, potrebbe essere necessario prendere in
considerazione dei modi per incorporare in modo affidabile anche le
nuove informazioni importanti che dovessero cambiare dopo aver
implementato l’algoritmo. L’integrazione di queste nuove informazioni
cambierà in qualche modo la qualità del nostro algoritmo, pur ben
collaudato? Se è così, come possiamo occuparcene in termini
progettuali? E poi, per alcuni algoritmi che utilizzano modelli globali,
potrebbe essere necessario monitorare i parametri in tempo reale che
catturano i cambiamenti nella situazione geopolitica globale. Inoltre, in
alcuni casi d’uso, potrebbe essere necessario considerare le politiche
legislative del caso, affinché la soluzione sia realmente impiegabile.
NOTA
Quando utilizziamo degli algoritmi per risolvere un problema concreto, in un
certo senso ci affidiamo a una macchina per la risoluzione di un problema.
Anche gli algoritmi più sofisticati si basano su semplificazioni e presupposti e
faticano a gestire le situazioni che, per un motivo o per l’altro, sono “speciali”.
Non siamo ancora nemmeno vicini a poterci affidare completamente agli
algoritmi nei processi decisionali critici.
La spiegabilità di un algoritmo
In un algoritmo “black-box” la logica di funzionamento non è
facilmente interpretabile a causa della sua complessità o perché il suo
funzionamento è “contorto”. Un algoritmo, invece, dovrebbe avere un
comportamento spiegabile e comprensibile. In altre parole, la
spiegabilità aiuta a capire perché un algoritmo sta dando un
determinato risultato. Il grado di spiegabilità misura la comprensibilità
di un algoritmo. Molti algoritmi, soprattutto quelli di machine
learning, sono classificati vere “black-box”. Se tali algoritmi vengono
impiegati per prendere decisioni critiche, può essere importante
comprendere le ragioni alla base dei risultati da esso prodotti. La
possibilità di estrarre gli algoritmi da questa “scatola nera” fornisce
anche una migliore comprensione del funzionamento interno del
modello. Un algoritmo spiegabile aiuterà i medici a capire quali
elementi sono stati utilizzati per classificare i pazienti come malati o
meno. Se il medico avesse dei dubbi sui risultati, potrebbe così tornare
sui propri passi e ricontrollare quegli specifici elementi, per verificarne
l’accuratezza.
Figura 14.1
Implementare la spiegabilità
LIME (Local Interpretable Model-Agnostic Explanations) è un
approccio indipendente dal modello che può spiegare le singole
previsioni fatte da un modello addestrato. Essendo indipendente dal
modello, può spiegare le previsioni della maggior parte dei modelli di
machine learning addestrati.
LIME spiega le decisioni introducendo piccole modifiche all’input
per ogni istanza e studiando gli effetti causati sul confine decisionale
locale di quell’istanza, iterando un ciclo per fornire i dettagli per ogni
variabile. Osservando l’output, possiamo vedere quale variabile ha la
maggiore influenza su quell’istanza.
Vediamo come possiamo usare LIME per rendere spiegabili le
singole previsioni del modello dei prezzi delle case.
1. Se non avete mai usato LIME, dovrete prima installare il
pacchetto:
!pip install lime
regressor.fit(X_train, y_train)
mode = 'regression')
plt.tight_layout()
Algoritmi ed etica
La formulazione di modelli attraverso algoritmi può comportare,
direttamente o indirettamente, un processo decisionale non etico.
Durante la progettazione di algoritmi, è difficile prevedere l’intera
portata delle potenziali implicazioni etiche, in particolare per gli
algoritmi per dati su larga scala, in cui il progetto può coinvolgere più
utenti. Ciò rende ancora più difficile analizzare gli effetti della
soggettività umana.
NOTA
Sempre più aziende stanno inserendo l’analisi etica di un algoritmo fra le fasi
della sua progettazione. Ma la verità è che i problemi potrebbero non
manifestarsi finché non troveremo un caso problematico.
Le implicazioni etiche
Le soluzioni algoritmiche sono formulazioni matematiche senza
anima. È responsabilità delle persone che le sviluppano garantire che
siano conformi alle implicazioni etiche legate al problema che stiamo
cercando di risolvere. Queste implicazioni etiche degli algoritmi
dipendono dal tipo di algoritmo.
Per esempio, esaminiamo i seguenti algoritmi e le loro implicazioni
etiche. Alcuni esempi di potenti algoritmi per i quali sono necessarie
attente considerazioni etiche sono i seguenti:
gli algoritmi di classificazione, quando applicati alla società,
determinano il modo in cui vengono individuati e gestiti gli
individui e i gruppi;
gli algoritmi utilizzati nei motori di raccomandazione possono
abbinare i curricula alle offerte di lavoro, operando con dinamiche
individuali e di gruppo;
gli algoritmi di data mining vengono utilizzati per estrarre
informazioni dagli utenti e i loro risultati vengono forniti a
decisori privati e pubblici;
gli algoritmi di machine learning stanno iniziando a essere
utilizzati dai governi per concedere o negare i visti ai richiedenti.
Quindi, le considerazioni etiche sugli algoritmi dipendono dal caso
d’uso e dalle entità che essi influenzano, direttamente o indirettamente.
È necessaria un’attenta analisi da un punto di vista etico prima di
iniziare a impiegare un algoritmo per i processi decisionali aventi
implicazioni etiche. Nei prossimi paragrafi vedremo i fattori che
dovremmo tenere in considerazione durante un’attenta analisi degli
algoritmi.
Prove inconcludenti
I dati utilizzati per addestrare un algoritmo di machine learning
potrebbero non offrire prove conclusive. Per esempio, negli studi
clinici, l’efficacia di un farmaco potrebbe non essere dimostrata a
causa delle limitate prove disponibili. Allo stesso modo, potrebbero
esserci prove inconcludenti e limitate che un determinato codice
postale di una determinata città possa avere maggiori probabilità di
essere coinvolto in una frode. Dovremmo stare attenti quando
giudichiamo il nostro processo decisionale in base ai modelli
matematici trovati dagli algoritmi che utilizzano dati così limitati.
NOTA
Le decisioni basate su prove inconcludenti sono inclini a portare ad azioni
ingiustificate.
Tracciabilità
La separazione netta fra la fase di training e la fase di test negli
algoritmi di machine learning fa sì che se un algoritmo causa qualche
danno, è molto difficile da tracciare il problema ed eseguirne il debug.
Inoltre, quando viene rilevato un problema in un algoritmo, è difficile
determinare effettivamente chi ne ha subito le conseguenze.
Evidenze fuorvianti
Gli algoritmi sono formulazioni guidate dai dati. In base al principio
GIGO (Garbage-in, Garbage-out) i risultati degli algoritmi sono
affidabili solo quanto lo sono i dati su cui si basano. Se vi sono
distorsioni nei dati, queste si rifletteranno anche negli algoritmi.
Risultati ingiusti
L’uso di algoritmi può danneggiare comunità e gruppi vulnerabili,
già svantaggiati. Inoltre, in più di un’occasione è stato dimostrato che
l’uso di algoritmi per distribuire i finanziamenti alla ricerca è risultato
sbilanciato verso la popolazione maschile. Gli algoritmi utilizzati per
garantire l’immigrazione sono talvolta involontariamente sbilanciati
verso gruppi vulnerabili.
Nonostante l’utilizzo di dati di alta qualità e le complesse
formulazioni matematiche, se il risultato è ingiusto, l’intero sforzo può
portare più danni che benefici.
Semplificare il problema
Possiamo semplificare il problema sulla base di alcune ipotesi. Il
problema risolto fornisce una soluzione, che non è perfetta ma è
comunque informata e utile. Affinché ciò funzioni, le ipotesi scelte
dovrebbero essere le meno restrittive possibili.
Esempio
È raro che la relazione fra feature ed etichette nei problemi di
regressione sia perfettamente lineare. Ma potrebbe essere lineare
all’interno del nostro intervallo operativo tipico. L’approssimazione
della relazione alla linearità semplifica notevolmente l’algoritmo ed è
ampiamente utilizzata, ma introduce approssimazioni che riducono
l’accuratezza dell’algoritmo. Il compromesso fra approssimazioni e
accuratezza dovrebbe essere studiato attentamente e dovrebbe essere
scelto il giusto equilibrio, adatto agli stakeholder.
Esempio
Molti algoritmi di machine learning partono da una soluzione
casuale e poi la migliorano in modo iterativo. La soluzione finale
potrebbe anche essere efficiente, ma non possiamo dimostrare che sia
la migliore. Questo metodo viene utilizzato in problemi complessi, per
poterli risolvere in un lasso di tempo ragionevole. Ecco perché, per
molti algoritmi di machine learning, l’unico modo per ottenere risultati
ripetibili consiste nell’utilizzare la stessa sequenza di numeri casuali
utilizzando lo stesso seme.
NOTA
Lo scoppio dell’epidemia di COVID-19 a inizio del 2020 è il miglior esempio di
un evento cigno nero dei nostri tempi.
Riepilogo
In questo capitolo, abbiamo appreso gli aspetti pratici che
dovrebbero essere considerati durante la progettazione di algoritmi.
Abbiamo esplorato il concetto di spiegabilità algoritmica e i vari modi
in cui possiamo fornirla a diversi livelli. Abbiamo anche esaminato i
potenziali problemi etici negli algoritmi. Infine, abbiamo descritto
quali fattori considerare durante la scelta di un algoritmo.
Gli algoritmi sono i motori del nuovo mondo automatizzato in cui
viviamo. È importante conoscere, sperimentare e comprendere le
implicazioni dell’uso degli algoritmi. Comprendere i loro punti di
forza, i loro limiti e le implicazioni etiche dell’uso degli algoritmi farà
molto per rendere questo mondo un luogo migliore in cui vivere.
Questo libro è un tentativo per raggiungere questo importante obiettivo
in questo mondo in continua evoluzione.
Indice
Introduzione
A chi è rivolto questo libro
Argomenti trattati
Requisiti
File degli esempi
Convenzioni utilizzate
L’autore
Il revisore tecnico
Parte I - Fondamenti e algoritmi di base
Capitolo 1 - Panoramica sugli algoritmi
Che cos’è un algoritmo?
La logica di un algoritmo
Introduzione ai pacchetti Python
Tecniche di progettazione degli algoritmi
Analisi delle prestazioni
Convalida di un algoritmo
Riepilogo
Capitolo 2 - Strutture di dati utilizzate negli algoritmi
Le strutture di dati in Python
I tipi di dati astratti in Python
Riepilogo
Capitolo 3 - Algoritmi di ordinamento e ricerca
Introduzione agli algoritmi di ordinamento
Introduzione agli algoritmi di ricerca
Applicazioni pratiche
Riepilogo
Capitolo 4 - Progettazione di algoritmi
Introduzione ai concetti di base della progettazione di un algoritmo
Strategie per la progettazione di algoritmi
Applicazione pratica: risoluzione del problema del commesso
viaggiatore
L’algoritmo PageRank
La programmazione lineare
Applicazione pratica: pianificazione della capacità con la
programmazione lineare
Riepilogo
Capitolo 5 - Algoritmi per grafi
Rappresentazione dei grafi
Introduzione all’analisi delle reti
Attraversamento dei grafi
Caso di studio: analisi delle frodi
Riepilogo
Parte II - Algoritmi di machine learning
Capitolo 6 - Algoritmi di machine learning senza supervisione
Introduzione all’apprendimento senza supervisione
Gli algoritmi di clustering
Riduzione della dimensionalità
Mining delle regole associative
Applicazione pratica: clustering di tweet simili
Algoritmi per il rilevamento delle anomalie
Riepilogo
Capitolo 7 - Algoritmi classici di machine learning con supervisione
Il machine learning con supervisione
Gli algoritmi di classificazione
Gli algoritmi di regressione
Esempio pratico: previsioni del tempo
Riepilogo
Capitolo 8 - Algoritmi a rete neurale
Le reti neurali
Evoluzione delle reti neurali
Addestramento di una rete neurale
Strumenti e framework
I vari tipi di reti neurali
Trasferimento dell’apprendimento
Caso di studio: utilizzo del deep learning per il rilevamento delle
frodi
Riepilogo
Capitolo 9 - Algoritmi per l’elaborazione del linguaggio naturale
Introduzione all’elaborazione del linguaggio naturale
Elaborazione del linguaggio naturale BoW-based
Introduzione al word embedding
Utilizzo delle reti neurali ricorrenti per l’elaborazione del linguaggio
naturale
Utilizzo dell’elaborazione del linguaggio naturale per l’analisi del
sentiment
Caso di studio: analisi del sentiment nelle recensioni di film
Riepilogo
Capitolo 10 - Motori di raccomandazione
Introduzione ai sistemi di raccomandazione
Tipi di motori di raccomandazione
I limiti dei sistemi di raccomandazione
Campi di applicazione
Esempio pratico: creazione di un motore di raccomandazione
Riepilogo
Parte III - Argomenti avanzati
Capitolo 11 - Algoritmi per i dati
Introduzione agli algoritmi per i dati
Gli algoritmi di archiviazione dei dati
Gli algoritmi per i dati in streaming
Gli algoritmi di compressione dei dati
Esempio pratico: analisi del sentiment dei tweet in tempo reale
Riepilogo
Capitolo 12 - Crittografia
Introduzione alla crittografia
Terminologia di base
I requisiti di sicurezza
Progettazione di cifrature
Cifratura a sostituzione
Tipi di tecniche crittografiche
Esempio: problemi di sicurezza nella distribuzione di un modello di
machine learning
Riepilogo
Capitolo 13 - Algoritmi per dati su larga scala
Introduzione agli algoritmi per dati su larga scala
Progettazione di algoritmi paralleli
Strategia di elaborazione multi-risorsa
Riepilogo
Capitolo 14 - Considerazioni pratiche
Introduzione
La spiegabilità di un algoritmo
Algoritmi ed etica
Ridurre i pregiudizi nei modelli
Affrontare i problemi NP-difficili
Quando usare gli algoritmi
Riepilogo