Home
5 risposte su… machine learning con R

28 Ottobre 2020

5 risposte su… machine learning con R

di

È solo grazie al machine learning che riusciamo ad avere ragione dell'immensa mole di dati prodotta dai sistemi di oggi. E non c'è mai stato un momento più propizio per imparare a farlo con il linguaggio R.

Di che cosa parliamo

  1. Come iniziare a fare machine learning con R
  2. Come identificare i funghi velenosi tramite un sistema di apprendimento basato su regole
  3. Come fare riconoscimento ottico dei caratteri (OCR) con macchine a vettori di supporto
  4. Che cosa sono le foreste casuali
  5. Come scaricare con R il testo di intere pagine web

1. Come iniziare a fare machine learning con R

Molti degli algoritmi richiesti per svolgere attività di machine learning non sono inclusi nell’ambito dell’installazione base di R. Tuttavia, tali algoritmi sono disponibili grazie a un’ampia community di esperti, che ha condiviso gratuitamente il proprio lavoro. Una raccolta di funzioni R che può essere condivisa fra gli utenti è chiamata package.

Possiamo osservare un elenco di package R su Comprehensive R Archive Network (CRAN), una raccolta di siti web e ftp distribuiti in tutto il mondo e che forniscono le versioni più aggiornate del software e dei package R.

Il link Packages situato nel menu a sinistra porterà a una pagina nella quale potete sfogliare i package in ordine alfabetico o ordinati per data di pubblicazione. Al momento [della pubblicazione di questo articolo], esiste un totale di 16.473 package.

Iscriviti alla nostra newsletter

Il link Task Views, sempre sul lato sinistro della pagina web di CRAN, fornisce un elenco selezionato dei package suddivisi per argomento, per esempio il machine learning.

Installazione dei package R

Per illustrare l’utilizzo dei package, installeremo e caricheremo il package RWeka sviluppato da Kurt Hornik, Christian Buchta e Achim Zeileis (per ulteriori informazioni, consultiamo Open-Source Machine Learning: R Meets Weka, in Computational Statistics Vol. 24, pp 225-232). Il package RWeka fornisce una raccolta di funzioni che offrono a R l’accesso agli algoritmi di machine learning tramite il package Java Weka, di Ian H. Witten ed Eibe Frank.

Il modo più diretto per installare un package prevede l’uso della funzione install.packages(). Per installare il package RWeka, digitiamo al prompt dei comandi di R:

> install.packages("RWeka")

R si connetterà alla pagina di CRAN e scaricherà il package nel formato corretto per il nostro sistema operativo. Alcuni package, come RWeka, richiedono la presenza di altri package per poter essere utilizzati. Queste sono chiamate dipendenze. Per default, l’installer scaricherà e installerà automaticamente tutte le dipendenze.

Le opzioni di installazione di default sono appropriate per la maggior parte dei sistemi. Tuttavia, in alcuni casi, potreste voler installare un package in un’altra posizione. Per esempio, se non avete i privilegi root o di amministrazione sul vostro sistema, potreste aver bisogno di specificare un percorso di installazione alternativo. Questo può essere ottenuto tramite l’opzione lib come segue:

> install.packages("RWeka", lib = "/percorso/della/libreria")

La funzione di installazione fornisce anche altre opzioni per l’installazione da un file locale, dal codice sorgente o per l’impiego di versioni sperimentali. Si può scoprire il funzionamento di queste opzioni tramite il sistema di Help, impiegando il seguente comando:

> ?install.packages

Più in generale, l’operatore punto interrogativo può essere utilizzato per ottenere un aiuto per ogni funzione R. Basta semplicemente digitare un ? prima del nome della funzione.

Caricamento e download dei package R

Per risparmiare memoria, R non carica per default ogni package installato.

Al contrario, i package vengono caricati dagli utenti con la funzione library() ogni volta che è necessario.

Per caricare il package RWeka precedentemente installato, possiamo digitare il seguente comando:

> library(RWeka)

Per scaricare un package R, si utilizza la funzione detach(). Per esempio, per scaricare il package RWeka, basta impiegare il seguente comando:

> detach("package:RWeka", unload = TRUE)

Questo comando ha l’effetto di liberare tutte le risorse impiegate dal package.

Installazione di RStudio

Prima di iniziare a lavorare con R, è assolutamente consigliabile installare anche l’applicazione desktop open source RStudio.

L'ambiente desktop RStudio facilita l'utilizzo di R

L’ambiente desktop RStudio facilita l’utilizzo di R.

Torna all’inizio.

2. Come identificare i funghi velenosi tramite un sistema di apprendimento basato su regole

A differenza di quanto avviene per l’identificazione delle piante velenose, come la quercia velenosa o l’edera velenosa, non vi sono regole chiare per identificare se un fungo selvatico è velenoso o commestibile. A complicare le cose, molte regole tradizionali, come I funghi velenosi hanno colori brillanti, forniscono informazioni pericolose o fuorvianti. Se fossero disponibili regole semplici, chiare e coerenti per identificare i funghi velenosi, si potrebbero risparmiare molte vite.

Dal momento che uno dei punti di forza degli algoritmi di apprendimento basato su regole è il fatto che generano regole di facile comprensione, essi sembrano proprio appropriate per questo compito di classificazione. Tuttavia, le regole saranno utili solo se sono accurate.

Passo 1: raccolta dei dati

Per identificare le regole utili per distinguere i funghi velenosi, utilizzeremo il dataset Mushroom di Jeff Schlimmer della Carnegie Mellon University. Il dataset grezzo è disponibile gratuitamente presso l’UCI Machine Learning Repository.

Il dataset include informazioni su 8.124 campioni di funghi di 23 specie di funghi a lamelle elencati nella Audubon Society Field Guide to North American Mushrooms (1981). In questa guida, ogni specie di fungo è identificata come assolutamente commestibile, assolutamente velenosa o piuttosto velenosa e sconsigliata. Per gli scopi di questo dataset, quest’ultimo gruppo è stato unito al gruppo assolutamente velenoso, così da avere due classi: velenoso e non-velenoso. Il dizionario dati disponibile sul sito web UCI descrive le 22 caratteristiche dei campioni di funghi, come la forma della cappella, il suo colore, l’odore, le dimensioni e il colore delle lamelle, la forma del gambo e l’habitat.

Passo 2: esplorazione e preparazione dei dati

Iniziamo impiegando read.csv() per importare i dati per la nostra analisi. Poiché tutte le 22 caratteristiche e anche la classe target sono nominali, imposteremo stringsAsFactors = TRUE per sfruttare la conversione automatica in fattori:

> mushrooms <- read.csv("mushrooms.csv", stringsAsFactors = TRUE)

L’output del comando str(mushrooms) nota che i dati contengono 8.124 osservazioni di 23 variabili, come indicato dal dizionario dati. La maggior parte dell’output di str() non ha bisogno di spiegazioni, ma vale la pena di menzionare una caratteristica. Si nota qualcosa di particolare a proposito della variabile veil_type nella seguente riga?

$ veil_type : Factor w/ 1 level "partial": 1 1 1 1 1 1 ...

Se pensiamo che non vada bene che un fattore abbia un solo livello, abbiamo ragione. Il dizionario dati elenca due livelli per questa caratteristica: partial e universal. Tuttavia, tutti gli esempi contenuti nei nostri dati sono classificati come partial. È probabile che questi elementi siano stati codificati erroneamente. In ogni caso, poiché il tipo di velo non varia fra i vari campioni, non ci fornisce informazioni utili per le predizioni. Elimineremo quindi tale variabile dalla nostra analisi, impiegando il seguente comando:

> mushrooms$veil_type <- NULL

Assegnando NULL al vettore veil_type, R elimina la caratteristica dal data frame dei funghi.

Prima di procedere, dovremmo dare una rapida occhiata alla distribuzione della variabile type nel nostro dataset:

> table(mushrooms$type)
edible poisonous
     4208      3916

Circa il 52 percento dei campioni di funghi (N = 4.208) è commestibile, mentre il 48 percento (N = 3,916) è velenoso.

Per gli scopi di questo esperimento, considereremo gli 8.214 campioni del dataset dei funghi un insieme esaustivo di tutti i funghi selvatici. Questo è un presupposto importante, perché significa che non abbiamo bisogno di trattenere dei campioni dai dati di addestramento, per le attività di test. Non stiamo tentando di sviluppare regole da applicare ad altri tipi di funghi, mai visti prima; stiamo semplicemente tentando di trovare delle regole che rappresentino accuratamente l’insieme costituito dai tipi di funghi noti. Pertanto, possiamo costruire e sottoporre a test il modello sugli stessi dati.

Passo 3: addestramento di un modello sui dati

Se addestrassimo un ipotetico classificatore ZeroR su questi dati, che cosa prevederebbe? Poiché ZeroR ignora tutte le caratteristiche e predice semplicemente il nostro target, in una frase, la sua regola direbbe che Tutti i funghi sono commestibili. Ovviamente, questo non è un classificatore molto utile, perché avvelenerebbe i raccoglitori di funghi per circa la metà dei campioni! Le nostre regole dovranno essere molto migliori di questo benchmark.

Poiché le regole semplici possono comunque essere utili, vediamo come si comporta un sistema di apprendimento basato su regole molto semplice sul dataset dei funghi. A tale scopo, applicheremo il classificatore 1R, il quale identificherà quell’unica caratteristica maggiormente predittiva per la classe target e useremo tale caratteristica per costruire una regola.

Impiegheremo l’implementazione di 1R presente nel package OneR di Holger von Jouanne-Diedrich della Aschaffenburg University of Applied Sciences. Si tratta di un package relativamente nuovo, che implementa 1R in codice nativo R per la sua velocità e facilità d’uso. Se già non avete questo package, potete installarlo impiegando il comando install.packages("OneR") e caricarlo digitando library(OneR).

m <- oneR(class ~ predictors, data = mydata)
  • class è la colonna del data frame mydata che deve essere predetta.
  • predictors è una formula R che specifica le caratteristiche del data frame mydata da utilizzare per la predizione.
  • data è il data frame in cui si trovano class e predictors.
p <- predict(m, test)
  • m è un modello addestrato dalla funzione oneR().
  • test è un data frame contenente i dati con le stesse caratteristiche dei dati di addestramento impiegati per costruire il classificatore.
mushroom_classifier <- oneR(type ~ odor + cap_color, data = mushroom_train)
mushroom_prediction <- predict(mushroom_classifier, mushroom_test)
Sintassi del classificatore a regole 1R.
Uso della funzione oneR() del package oneR
Creazione del classificatore

m <- oneR(class ~ predictors, data = mydata)
  • class è la colonna del data frame mydata che deve essere predetta.
  • predictors è una formula R che specifica le caratteristiche del data frame mydata da utilizzare per la predizione.
  • data è il data frame in cui si trovano class e predictors.

La funzione restituirà un oggetto modello OneR che può essere utilizzato per eseguire le predizioni.

Esecuzione delle predizioni

p <- predict(m, test)
  • m è un modello addestrato dalla funzione oneR().
  • test è un data frame contenente i dati con le stesse caratteristiche dei dati di addestramento impiegati per costruire il classificatore.

La funzione restituirà un vettore dei valori predetti per la classe.

Esempio

mushroom_classifier <- oneR(type ~ odor + cap_color, data = mushroom_train)
mushroom_prediction <- predict(mushroom_classifier, mushroom_test)

La funzione OneR() impiega la sintassi a formula di R per specificare il modello da addestrare. La sintassi a formula impiega l’operatore ~ (tilde) per esprimere la relazione fra una variabile target e i suoi predittori. La variabile della classe da apprendere va a sinistra della tilde e le caratteristiche per la predizione vanno a destra, separate da operatori +. Se voleste modellare la relazione fra la classe y e i predittori x1 e x2, dovreste scrivere la formula come y ~ x1 + x2. Per includere nel modello tutte le variabili, viene utilizzato il segno di punto. Per esempio, y ~. specifica la relazione fra y e tutte le caratteristiche contenute nel dataset.

Impiegando la formula type ~ . con OneR() consente al nostro sistema mono-regola di considerare tutte le possibili caratteristiche contenute nel dataset dei funghi nel predire il tipo di fungo:

> mushroom_1R <- OneR(type ~ ., data = mushrooms)

Per esaminare le regole che ha creato, possiamo digitare il nome dell’oggetto classificatore:

> mushroom_1R
Call:
OneR.formula(formula = type ~ ., data = mushrooms)
Rules:
If odor = almond then type = edible
If odor = anise then type = edible
If odor = creosote then type = poisonous
If odor = fishy then type = poisonous
If odor = foul then type = poisonous
If odor = musty then type = poisonous
If odor = none then type = edible
If odor = pungent then type = poisonous
If odor = spicy then type = poisonous
Accuracy:
8004 of 8124 instances classified correctly (98.52%)

Esaminando l’output, vediamo che per la generazione della regola è stata utilizzata la caratteristica odor. Le categorie di odor, come almond, anise e così via, specificano le regole per stabilire se il fungo è edible (commestibile) o poisonous (velenoso). Per esempio, se il fungo ha un odore fishy (di pesce), foul (sgradevole), musty (di muffa), pungent (pungente), spicy (speziato) o come creosote (di legna bruciata), il fungo è probabilmente velenoso. Al contrario, i funghi con odori più piacevoli, come almond (di mandorle) e anise (di anice) e quelli che non hanno alcun odore, vengono predetti come commestibili. Per gli scopi di una guida sul campo per la raccolta di funghi, queste regole possono essere riassunte in una semplice regola empirica: Se il fungo ha un odore sgradevole, probabilmente è velenoso.

Passo 4: valutazione delle prestazioni del modello

L’ultima riga dell’output nota che le regole hanno predetto correttamente la commestibilità di 8.004 degli 8.124 campioni di funghi, circa il 99 percento. Tuttavia, in questo caso, ogni mancanza di perfezione genera un pericolo di avvelenamento, se il modello dovesse classificare come commestibile un fungo velenoso.

Per determinare questo fatto, esaminiamo una matrice di confusione dei valori predetti ed effettivi. Per farlo, dovremo innanzitutto generare le predizioni del modello 1R e poi confrontarle con i valori effettivi:

 > mushroom_1R_pred <- predict(mushroom_1R, mushrooms)
> table(actual = mushrooms$type, predicted = mushroom_1R_pred)
predicted
actual edible poisonous
edible 4208 0
poisonous 120 3796

Qui possiamo vedere dove hanno sbagliato le nostre regole. Le colonne della tabella indicano la commestibilità predetta del fungo, mentre le righe della tabella dividono i 4.208 funghi effettivamente commestibili dai 3.916 funghi effettivamente velenosi. Esaminando la tabella, possiamo vedere che, sebbene il classificatore 1R non abbia classificato come velenoso nessun fungo commestibile, ha classificato come commestibili ben 120 funghi velenosi: un errore davvero pericoloso!

Considerando che il sistema di apprendimento ha utilizzato un’unica caratteristica, si è comportato piuttosto bene; se evitiamo di raccogliere i funghi maleodoranti, quasi sempre eviteremo anche una gita in ospedale. Detto questo, quando c’è di mezzo la vita, è sufficiente solo la perfezione, per non parlare del fatto che l’editore della guida per cercatori di funghi andrebbe incontro a cause legali, per i cercatori caduti vittima delle cattive prestazioni del sistema. Vediamo se possiamo aggiungere qualche altra regola per sviluppare un classificatore migliore.

Passo 5: miglioramento delle prestazioni del modello

Per creare un sistema di apprendimento basato su regole più sofisticato, impiegheremo JRip(), un’implementazione Java dell’algoritmo RIPPER. La funzione JRip() è contenuta nel package RWeka, presentato nel Capitolo 1, nel tutorial sull’installazione e caricamento dei package. Se non abbiamo ancora installato questo package, avremo bisogno di usare il comando install.packages("RWeka") dopo aver installato Java sulla nostra macchina, in base alle istruzioni specifiche per il nostro sistema. Una volta terminati questi passi, carichiamo il package impiegando il comando library(RWeka).

m <-  JRip(class ~ predictors, data = mydata)
  • class è la colonna del data frame mydata che deve essere predetta.
  • predictors è una formula R che specifica le caratteristiche del data frame mydata da utilizzare per la predizione.
  • data è il data frame in cui si trovano class e predictors.
p <- predict(m, test)
  • m è un modello addestrato dalla funzione JRip().
  • test è un data frame contenente i dati con le stesse caratteristiche dei dati di addestramento impiegati per costruire il classificatore.
mushroom_classifier <- JRip(type ~ odor + cap_color, data = mushroom_train)
mushroom_prediction <- predict(mushroom_classifier, mushroom_test)
Sintassi del classificatore a regole RIPPER.
Uso della funzione JRip() del package RWeka
Creazione del classificatore

m <- JRip(class ~ predictors, data = mydata)
  • class è la colonna del data frame mydata che deve essere predetta.
  • predictors è una formula R che specifica le caratteristiche del data frame mydata da utilizzare per la predizione.
  • data è il data frame in cui si trovano class e predictors.

La funzione restituirà un oggetto modello RIPPER che può essere utilizzato per eseguire le predizioni.

Esecuzione delle predizioni

p <- predict(m, test)
  • m è un modello addestrato dalla funzione JRip().
  • test è un data frame contenente i dati con le stesse caratteristiche dei dati di addestramento impiegati per costruire il classificatore.

La funzione restituirà un vettore dei valori predetti per la classe.

Esempio

mushroom_classifier <- JRip(type ~ odor + cap_color, data = mushroom_train)
mushroom_prediction <- predict(mushroom_classifier, mushroom_test)

Come si può vedere nella tabella della sintassi, il processo di addestramento di un modello JRip() è molto simile all’addestramento di un modello OneR(). Questo è uno dei grandi vantaggi dell’interfaccia a formula di R: la sintassi è coerente fra i vari algoritmi, un aspetto che semplifica il confronto di più modelli.

Addestriamo il sistema di apprendimento basato su regole JRip() come abbiamo fatto con OneR(), per permettergli di trovare delle regole basate su tutte le caratteristiche disponibili:

> mushroom_JRip <- JRip(type ~ ., data = mushrooms)

Per esaminare le regole, digitiamo il nome del classificatore:

> mushroom_JRip
JRIP rules:
===========
(odor = foul) => type=poisonous (2160.0/0.0)
(gill_size = narrow) and (gill_color = buff)
=> type=poisonous (1152.0/0.0)
(gill_size = narrow) and (odor = pungent)
=> type=poisonous (256.0/0.0)
(odor = creosote) => type=poisonous (192.0/0.0)
(spore_print_color = green) => type=poisonous (72.0/0.0)
(stalk_surface_below_ring = scaly)
and (stalk_surface_above_ring = silky)
=> type=poisonous (68.0/0.0)
(habitat = leaves) and (gill_attachment = free)
and (population = clustered)
=> type=poisonous (16.0/0.0)
=> type=edible (4208.0/0.0)
Number of Rules : 8

Il classificatore JRip() ha appreso un totale di otto regole dai dati dei funghi. Un modo semplice per leggere queste regole consiste nel considerarle come una lista di istruzioni ifelse, con una specie di logica di programmazione. Le prime tre regole possono essere espresse nel seguente modo.

  • Se l’odore è sgradevole, allora il fungo è velenoso.
  • Se le lamelle sono fitte e color camoscio, allora il fungo è velenoso.
  • Se le lamelle sono fitte e l’odore è pungente, allora il fungo è velenoso.

Infine, l’ottava regola implica che ogni fungo che non sia stato considerato dalle precedenti sette regole è commestibile. Seguendo l’esempio della nostra logica di programmazione, questo si trasforma in qualcosa come la seguente regola.

  • Altrimenti, il fungo è commestibile.

I numeri a lato di ogni regola indicano il numero di istanze coperte dalla regola e un conteggio delle istanze classificate erroneamente. Possiamo notare che non vi sono campioni di funghi classificati erroneamente da queste otto regole. Di conseguenza, il numero di istanze coperte dall’ultima regola è uguale esattamente al numero di funghi commestibili presenti nei dati (N = 4.208).

La figura seguente fornisce una rappresentazione del modo in cui le regole vengono applicate al dataset dei funghi.

Un sofisticato algoritmo di apprendimento ha identificato regole che coprono perfettamente tutti i tipi di funghi velenosi

Un sofisticato algoritmo di apprendimento basato su regole ha identificato delle regole che coprono perfettamente tutti i tipi di funghi velenosi.

Torna all’inizio.

3. Come fare riconoscimento ottico dei caratteri (OCR) con macchine a vettori di supporto

In questo paragrafo svilupperemo un modello simile a quelli impiegati dai software OCR (Optical Character Recognition) forniti con molti scanner o smartphone. Lo scopo di tali software è quello di elaborare dei documenti cartacei, convertendo il testo stampato o scritto a mano in un formato elettronico più versatile.

Passo 1: raccolta dei dati

Quando il software OCR elabora un documento, divide il foglio in una matrice, in modo che ciascuna cella della griglia contenga un unico glifo, un termine che può far riferimento a una lettera, un simbolo o un numero. Successivamente, per ciascuna cella, il software tenterà di far corrispondere il glifo all’insieme di tutti i caratteri che riconosce. Infine, i singoli caratteri possono essere combinati in parole e poi, opzionalmente, possono essere sottoposti a un controllo ortografico, che impiega un dizionario della lingua corretta.

In questo esercizio, presumeremo di aver già sviluppato l’algoritmo per suddividere il documento in regioni rettangolari, ognuna delle quali contiene un unico glifo. Presumeremo inoltre che il documento contenga solo caratteri dell’alfabeto inglese. Pertanto, simuleremo un processo che prevede l’accoppiamento dei glifi a una delle 26 lettere dell’alfabeto inglese, dalla A alla Z.

Per questo scopo impiegheremo un dataset donato all’UCI Machine Learning Repository (http://archive.ics.uci.edu/ml) da W. Frey e D. J. Slate. Il dataset contiene 20 mila esempi di 26 lettere maiuscole dell’alfabeto inglese, stampate impiegando 20 font in bianco e nero e distorte e alterate in modo casuale.

Passo 2: esplorazione e preparazione dei dati

In base alla documentazione fornita da Frey e Slate, quando i glifi sono stati acquisiti tramite scanner, sono stati convertiti in pixel e sono stati memorizzati 16 attributi statistici.

Gli attributi misurano caratteristiche come le dimensioni orizzontali e verticali del glifo; la proporzione dei pixel neri (rispetto ai bianchi); la media della posizione orizzontale e verticale dei pixel. Presumibilmente, le differenze di concentrazione di pixel neri nelle varie aree del rettangolo dovrebbero fornire un modo per distinguere le 26 lettere dell’alfabeto.

Esempi di glifi che l’algoritmo della macchina a vettori di supporto tenterà di identificare.

Leggendo i dati in R, confermiamo di aver ricevuto i dati con le 16 caratteristiche che definiscono ciascun esempio della classe letter. Come previsto, abbiamo 26 livelli:

> letters <- read.csv("letterdata.csv")
> str(letters) 'data.frame': 20000 obs. of 17 variables:
$ letter: Factor w/ 26 levels "A","B","C","D",..
$ xbox : int 2 5 4 7 2 4 4 1 2 11 ...
$ ybox : int 8 12 11 11 1 11 2 1 2 15 ...
$ width : int 3 3 6 6 3 5 5 3 4 13 ...
$ height: int 5 7 8 6 1 8 4 2 4 9 ...
$ onpix : int 1 2 6 3 1 3 4 1 2 7 ...
$ xbar : int 8 10 10 5 8 8 8 8 10 13 ...
$ ybar : int 13 5 6 9 6 8 7 2 6 2 ...
$ x2bar : int 0 5 2 4 6 6 6 2 2 6 ...
$ y2bar : int 6 4 6 6 6 9 6 2 6 2 ...
$ xybar : int 6 13 10 4 6 5 7 8 12 12 ...
$ x2ybar: int 10 3 3 4 5 6 6 2 4 1 ...
$ xy2bar: int 8 9 7 10 9 6 6 8 8 9 ...
$ xedge : int 0 2 3 6 1 0 2 1 1 8 ...
$ xedgey: int 8 8 7 10 7 8 8 6 6 1 ...
$ yedge : int 0 4 3 2 5 9 7 2 1 1 ...
$ yedgex: int 8 10 9 8 10 7 10 7 7 8 ...

I sistemi di apprendimento a macchina a vettori di supporto richiedono che tutte le caratteristiche siano numeriche e, inoltre, che ogni caratteristica sia compresa in un intervallo piuttosto contenuto. In questo caso, ogni caratteristica è un valore intero, pertanto non abbiamo bisogno di convertire alcun fattore in numeri. D’altra parte, alcuni degli intervalli di queste variabili intere sembrano piuttosto ampi. Questo indica che dobbiamo normalizzare o standardizzare i dati. Tuttavia, possiamo saltare questo passo, per il momento, poiché il package R che impiegheremo per configurare il modello di macchina a vettori di supporto eseguirà automaticamente il cambio di scala necessario.

Dato che non vi è nessun’altra preparazione dei dati da eseguire, possiamo passare direttamente alle fasi di addestramento e test del processo di machine learning. Nelle analisi precedenti abbiamo diviso casualmente i dati nei due insieme di addestramento e di test. Potremmo farlo anche ora, ma Frey e Slate hanno già casualizzato i dati e pertanto suggeriscono di impiegare i primi 16.000 record (l’80 percento) per costruire il modello e i successivi 4.000 record (il 20 percento) per i test. Seguendo il loro consiglio, possiamo creare i data frame di addestramento e di test come segue:

> letters_train <- letters[1:16000, ]
> letters_test  <- letters[16001:20000, ]

Con i nostri dati pronti all’uso, iniziamo a costruire il nostro classificatore.

Passo 3: addestramento di un modello sui dati

Dovendo configurare un modello di macchina a vettori di supporto in R, abbiamo numerosi e interessanti package fra cui scegliere. Il package e1071 del Department of Statistics della Vienna University of Technology fornisce un’interfaccia R per la libreria open source LIBSVM, vincitrice di numerosi premi e ampiamente utilizzata per programmi scritti in C++. Se già conosciamo la libreria LIBSVM, potremmo voler iniziare da qui.

Analogamente, se già conosciamo l’algoritmo SVMlight, il package klaR del Department of Statistics della Dortmund University of Technology fornisce funzioni utili per lavorare con questa implementazione delle macchine a vettori di supporto direttamente da R.

Infine, se stiamo partendo da zero, forse dovremmo iniziare con le funzioni per macchine a vettori di supporto del package kernlab. Un vantaggio interessante di questo package è che è stato sviluppato nativamente in R invece che in C o C++, cosa che permette di personalizzarlo con facilità; nulla dei suoi dettagli è nascosto “dietro le quinte”. Un aspetto forse ancora più importante: a differenza delle altre opzioni, kernlab può essere utilizzato con il package caret, il quale consente di addestrare e valutare modelli per macchine a vettori di supporto impiegando vari metodi automatizzati.

La sintassi per l’addestramento di classificatori a macchine a vettori di supporto con kernlab è illustrata nella prossima tabella. Se per caso impieghiamo uno degli altri package, troveremo che i comandi sono piuttosto simili. Per default, la funzione ksvm() impiega il kernel gaussiano RBF, ma offre anche varie altre opzioni.

m <- ksvm(target ~ predictors, data = mydata,
          kernel = "rbfdot", C = 1)
  • target è il risultato nel data frame mydata che deve essere to modellato.
  • predictors è una formula R che specifica le caratteristiche del data frame mydate da impiegare per la predizione.
  • data specifica il data frame in cui possono essere trovate le variabili target e predictors.
  • kernel specifica una mappatura non lineare come "rbfdot" (radial basis), " polydot" (polynomial), "tanhdot" (hyperbolic tangent sigmoid) o "vanilladot" (lineare).
  • C è un numero che specifica il costo della violazione dei vincoli, per esempio l’entità della penalità per il “soft margin”. Valori maggiori producono margini più stretti.
p <- predict(m, test, type "response")
  • m è un modello addestrato dalla funzione ksvm().
  • test è un data frame contenente dati di test aventi esattamnte le stesse caratteristiche dei dati di addestramento impiegati per costruire il classificatore.
  • type specifica se le predizioni devono essere "response" (la classe predetta) o "probabilities" (la probabilità della predizione, una colonna per classe).
letter_classifier <- ksvm(letter ~ ., data = letters_train,
kernel = "vanilladot")
letter_prediction <- predict(letter_classifier, letters_test)
Tabella 7.4 Sintassi per macchine a vettori di supporto.
Uso della funzione ksvm() del package kernlab
Creazione del modello

m <- ksvm(target ~ predictors, data = mydata, kernel = "rbfdot", C = 1)
  • target è il risultato nel data frame mydata che deve essere to modellato.
  • predictors è una formula R che specifica le caratteristiche del data frame mydate da impiegare per la predizione.
  • data specifica il data frame in cui possono essere trovate le variabili target e predictors.
  • kernel specifica una mappatura non lineare come "rbfdot" (radial basis), "polydot" (polynomial), "tanhdot" (hyperbolic tangent sigmoid) o "vanilladot" (lineare).
  • C è un numero che specifica il costo della violazione dei vincoli, per esempio l’entità della penalità per il “soft margin”. Valori maggiori producono margini più stretti.

La funzione restituisce un oggetto “macchina a vettori di supporto” che può essere utilizzato per effettuare predizioni.

Creazione delle predizioni

p <- predict(m, test, type "response")
  • m è un modello addestrato dalla funzione ksvm().
  • test è un data frame contenente dati di test aventi esattamnte le stesse caratteristiche dei dati di addestramento impiegati per costruire il classificatore.
  • type specifica se le predizioni devono essere "response" (la classe predetta) o "probabilities" (la probabilità della predizione, una colonna per classe).

La funzione restituisce un vettore (o una matrice) delle classi predette (o delle probabilità) a seconda del valore del parametro type.

Esempio

letter_classifier <- ksvm(letter ~ ., data = letters_train, kernel = "vanilladot")
letter_prediction <- predict(letter_classifier, letters_test)

Per fornire una misura di partenza delle prestazioni della macchina a vettori di supporto, iniziamo addestrando un semplice classificatore lineare. Se non l’abbiamo già fatto, installiamo il package kernlab impiegando il comando install.packages("kernlab"). Poi, potremo richiamare la funzione ksvm() sui dati di addestramento e specificare il kernel lineare (ovvero “vanilla”) impiegando l’opzione vanilladot, come segue:

> library(kernlab)
> letter_classifier <- ksvm(letter ~ ., data = letters_train,
                            kernel = "vanilladot")

A seconda delle prestazioni del vostro computer, questa operazione potrebbe richiedere del tempo. Al termine, digitate il nome del modello memorizzato per vedere alcune informazioni di base sui parametri di addestramento del modello:

> letter_classifier
Support Vector Machine object of class "ksvm"
SV type: C-svc (classification)
Parameter : cost C = 1
Linear (vanilla) kernel function.
Number of Support Vectors : 7037
Objective Function Value : -14.1746 -20.0072 -23.5628 -6.2009 -7.5524
-32.7694 -49.9786 -18.1824 -62.1111 -32.7284 -16.2209...
Training error : 0.130062

Questa informazione ci dice molto poco sul comportamento del modello con dati tratti dal mondo reale. Avremo bisogno di esaminare le sue prestazioni sul dataset di test per capire se il modello è generalizzabile su dati che non ha mai visto prima.

Passo 4: valutazione delle prestazioni del modello

La funzione predict() ci consente di impiegare il modello di classificazione delle lettere per eseguire predizioni sul dataset di test:

> letter_predictions <- predict(letter_classifier, letters_test)

Poiché non abbiamo specificato il parametro type, è stato impiegato il default, type = "response". Il modello restituisce un vettore contenente una lettera predetta per ogni riga dei valori contenuti nei dati di test. Impiegando la funzione head(), possiamo vedere che le prime sei lettere predette sono state U, N, V, X, N e H:

> head(letter_predictions)
[1] U N V X N H
Levels: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

Per esaminare l’efficacia del nostro classificatore, dobbiamo confrontare la lettera predetta con la lettera effettiva nel dataset di test. Impiegheremo la funzione table() (qui è rappresentata solo una porzione dell’intera tabella):

> table(letter_predictions, letters_test$letter)
letter_predictions A B C D E
A 144 0 0 0 0
B 0 121 0 5 2
C 0 0 120 0 4
D 2 2 0 156 0
E 0 0 5 0 127

I valori lungo la diagonale, 144, 121, 120, 156 e 127 indicano il numero totale di record in cui la lettera predetta corrisponde al valore effettivo. Analogamente, è indicato anche il numero di errori. Per esempio, il valore 5 nella riga B e colonna D indica che vi sono stati cinque casi in cui la lettera D è stata erroneamente identificata come una B.

Osservando individualmente ogni tipo di errore, rileviamo alcuni schemi interessanti sulle lettere con le quali il modello incontra difficoltà, ma questo richiede tempo. Possiamo semplificare la nostra valutazione calcolando invece l’accuratezza globale. Questo considera solo se la predizione era corretta o errata e ignora il tipo di errore.

Il seguente comando restituisce un vettore di valori TRUE o FALSE per indicare se la lettera predetta dal modello concorda con la lettera effettiva nel dataset di test:

> agreement <- letter_predictions == letters_test$letter

Impiegando la funzione table(), vediamo che il classificatore ha identificato correttamente la lettera in 3.357 su 4.000 record di test:

> table(agreement)
agreement
FALSE TRUE
  643  3357

In termini percentuali, l’accuratezza è attorno all’84 percento:

> prop.table(table(agreement))
agreement
FALSE TRUE
0.16075 0.83925

Si noti che quando Frey e Slate pubblicarono il dataset nel 1991, rilevarono un’accuratezza di riconoscimento pari a circa l’80 percento. Impiegando solo poche righe di codice R, siamo stati in grado superare il loro risultato, ma abbiamo il vantaggio di decenni di nuove ricerche nel campo del machine learning. In ogni caso, è probabile che saremo in grado di fare ancora meglio.

Passo 5: miglioramento delle prestazioni del modello

Prendiamoci un momento per contestualizzare le prestazioni del modello di macchina a vettori di supporto che abbiamo addestrato per identificare le lettere dell’alfabeto dai dati delle immagini. Con una riga di codice R, il modello è stato in grado di raggiungere un’accuratezza di circa l’84 percento, con un leggero miglioramento rispetto al benchmark pubblicato dai ricercatori nel 1991. Sebbene un’accuratezza dell’84 percento non sia molto elevata per un buon software OCR, il fatto che un modello relativamente semplice possa raggiungere questo livello è un risultato davvero notevole in sé. Consideriamo il fatto che la probabilità che la predizione del modello “azzecchi” il valore effettivo per pura fortuna è piuttosto piccola: inferiore al 4 percento. Questo implica che il nostro modello si comporta più di 20 volte meglio di una scelta casuale. Per quanto sia un risultato notevole, forse, regolando i parametri della funzione che realizza la macchina a vettori di supporto, per addestrare un modello leggermente più complesso, possiamo ottenere un modello effettivamente utile nel mondo reale.

Modificare la funzione kernel per la macchina a vettori di supporto

Il nostro precedente modello di macchina a vettori di supporto ha impiegato la semplice funzione kernel lineare. Impiegando una funzione kernel più complessa, possiamo mappare i dati su uno spazio a dimensionalità più elevata e, potenzialmente, ottenere un modello più efficace.

Può essere impegnativo, tuttavia, scegliere una delle numerose funzioni kernel. Una convenzione molto utilizzata consiste nell’iniziare a utilizzare la funzione kernel gaussiana, che ha dimostrato di essere ottimale per molti tipi di dati. Possiamo addestrare una macchina a vettori di supporto RBF impiegando la funzione ksvm() nel seguente modo:

> letter_classifier_rbf <- ksvm(letter ~ ., data = letters_train,
                                kernel = "rbfdot")

Successivamente, effettuiamo le predizioni, come prima:

> letter_predictions_rbf <- predict(letter_classifier_rbf,
                           <>         letters_test)

Infine, confronteremo l’accuratezza con quella della macchina a vettori di supporto con funzione kernel lineare:

> agreement_rbf <- letter_predictions_rbf == letters_test$letter
> table(agreement_rbf)
agreement_rbf
FALSE TRUE
275 3725
> prop.table(table(agreement_rbf))
agreement_rbf
FALSE TRUE
0.06875 0.93125

Cambiando solo la funzione kernel, siamo stati in grado di aumentare l’accuratezza del mostro modello di riconoscimento dei caratteri dall’84 al 93 percento.

Identificazione del parametro di costo migliore per la macchina a vettori di supporto

Se questo livello di prestazioni è ancora insoddisfacente per un buon software OCR, è certamente possibile provare con altre funzioni kernel. Tuttavia, un altro approccio da esplorare consiste nel variare il parametro di costo, che modifica la larghezza del confine decisionale della macchina a vettori di supporto. Questa scelta governa l’equilibrio del modello, in bilico fra overfitting e underfitting sui dati di addestramento: maggiore è il costo, più il sistema di apprendimento tenterà di classificare perfettamente ogni istanza di addestramento, in quanto ogni errore ha una penalità più elevata. Da un lato, un costo elevato può portare il sistema di apprendimento ad andare in overfit sui dati di addestramento. Dall’altro, un parametro di costo troppo basso può fare in modo che il sistema di apprendimento perda degli schemi lievi ma importanti nei dati di addestramento e vada così in underfit.

Non esiste alcuna regola empirica per conoscere il valore ideale in anticipo, pertanto esamineremo come si comporta il modello per vari valori di C, il parametro di costo. Invece di ripetere più volte il processo di addestramento e valutazione, possiamo usare la funzione sapply() per applicare una funzione personalizzata a un vettore di potenziali valori di costo. Iniziamo impiegando la funzione seq() per generare questo vettore come una sequenza da 5 a 40 a passi di 5. Poi, come potete vedere nel seguente codice, la funzione personalizzata addestrerà il modello come prima, impiegando ogni volta il nuovo valore di costo ed effettuando le predizioni sul dataset di test. Poi viene calcolata l’accuratezza del modello, in termini di numero di predizioni corrette divise per il numero totale delle predizioni. Il risultato viene poi rappresentato tramite la funzione plot():

> cost_values <- c(1, seq(from = 5, to = 40, by = 5))
>
> accuracy_values <- sapply(cost_values, function(x) {
set.seed(12345)
m <- ksvm(letter ~ ., data = letters_train,
kernel = "rbfdot", C = x)
pred <- predict(m, letters_test)
agree <- ifelse(pred == letters_test$letter, 1, 0)
accuracy <- sum(agree) / nrow(letters_test)
return (accuracy)
})
> plot(cost_values, accuracy_values, type = "b")

Come possiamo vedere nel grafico, con un’accuratezza del 93 percento, il parametro di costo di default della macchina a vettori di supporto, C = 1, è risultato di gran lunga il meno accurato fra i nove modelli valutati. Al contrario, impostando C a un valore pari a 10 o più si ottiene sempre un’accuratezza di circa il 97 percento, che è un notevole miglioramento prestazionale! Forse questo risultato è sufficientemente elevato da consentire di impiegare il modello in un ambiente reale, ma vale la pena di sperimentare ulteriormente, con varie altre funzioni kernel, per vedere se è possibile avvicinarsi di più a un’accuratezza del 100 percento. Ogni ulteriore miglioramento nell’accuratezza produrrà meno errori del software OCR e una migliore esperienza generale per l’utente finale.

Mappa dell'accuratezza rispetto al costo della macchina a vettori di supporto, con kernel RBF

Mappa dell’accuratezza rispetto al costo nella macchina a vettori di supporto, con kernel RBF.

Torna all’inizio.

4. Che cosa sono le foreste casuali

Un altro metodo ensemble, le foreste casuali (o foreste di alberi decisionali) si concentra solo sugli ensemble di alberi decisionali. Questo metodo è stato ideato da Leo Breiman e Adele Cutler e combina i principi base del bagging con la selezione casuale delle caratteristiche per aumentare la diversità dei modelli ad albero decisionale. Dopo aver generato l’ensemble degli alberi (la foresta), il modello impiega un voto per combinare le predizioni degli alberi stessi.

Le foreste casuali combinano versatilità e potenza in un unico approccio di machine learning. Poiché l’ensemble impiega solo una piccola porzione casuale dell’intero insieme di caratteristiche, le foreste casuali sono in grado di gestire dataset di grandi dimensioni, dove la cosiddetta maledizione della dimensionalità metterebbe in ginocchio altri modelli. Contemporaneamente, i suoi tassi d’errore, per la maggior parte dei compiti di apprendimento, sono alla pari di quasi ogni altro metodo.

Vale la pena di notare che rispetto ad altri metodi ensemble, le foreste casuali sono piuttosto competitive e offrono specifici vantaggi. Per esempio, le foreste casuali tendono a essere più semplici da usare e meno soggette a overfitting. La seguente tabella elenca i punti di forza e i punti deboli dei modelli a foresta casuale.

Punti di forza e punti deboli dei modelli a foresta causale.
Punti di forza Punti deboli
  • Un modello di carattere generale che si comporta bene con la maggior parte dei problemi.
  • Può gestire con facilità dati rumorosi o mancanti, così come caratteristiche categoriche o continue.
  • Seleziona solo le caratteristiche più importanti.
  • Può essere impiegato su dati aventi un numero estremamente vasto di caratteristiche o esempi
  • A differenza di un albero decisionale, il modello non è di facile interpretazione.

Data la sua potenza, versatilità e facilità d’uso, le foreste casuali sono uno dei metodi di machine learning più popolari.

Torna all’inizio.

5. Come scaricare con R il testo di intere pagine web

Download del testo di intere pagine web

Il package RCurl di Duncan Temple Lang fornisce un modo più solido per accedere alle pagine web fornendo un’interfaccia R all’utility curl (Client for URLs), uno strumento a riga di comando per trasferire i dati su reti. Il programma curl è uno strumento ampiamente utilizzato che si comporta un po’ come un browser web programmabile: dato un insieme di comandi, può accedere al Web e scaricare ogni genere di contenuto disponibile. E a differenza di R, può accedere a siti web sicuri, così come a istruzioni post verso moduli online. Si tratta di uno strumento incredibilmente potente.

Dopo aver installato e caricato il package RCurl, per scaricare il contenuto di una pagina basta digitare:

> packt_page <- getURL("https://www.packtpub.com/")

Questo comando salva il contenuto dell’intera home page di Packt Publishing (compreso tutto il codice HTML) nell’oggetto a caratteri R di nome packt_page. Come potete vedere nelle seguenti righe, questo contenuto non è molto utile, così come è:

> str(packt_page, nchar.max = 200)
chr "<!DOCTYPE html>\n    <html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">\n    <head>\n        <title>Packt Publishing | Technology Books, eBooks & Videos</title>"| __truncated__

Il motivo per cui i primi 200 caratteri della pagina sono così criptici è che sono scritti in linguaggio HTML (Hypertext Markup Language), che aggiunge al testo della pagina speciali tag che dicono al browser web come rappresentare il testo. I tag <title> e </title> circondano il titolo della pagina, ovvero dicono al browser che questa è la home page di Packt Publishing. Tag simili sono impiegati per denotare altre porzioni della pagina.

Anche se curl è lo standard multipiattaforma per accedere ai contenuti online, se vi trovate a lavorare frequentemente in R con i dati web, il package httr di Hadley Wickham si basa su RCurl per semplificare l’accesso al codice HTML del Web e renderlo più “R-like”. Invece di usare RCurl, il package httr impiega dietro le quinte un proprio package curl per accedere ai dati contenuti del sito. Possiamo notare subito alcune differenze, tentando di scaricare la home page di Packt Publishing impiegando la funzione GET() del package httr:

> library(httr)
> packt_page <- GET("https://www.packtpub.com")
> str(packt_page, max.level = 1)
List of 10
$ url : chr "https://www.packtpub.com/"
$ status_code: int 200
$ headers :List of 11
..- attr(*, "class")= chr [1:2] "insensitive" "list"
$ all_headers:List of 1
$ cookies :'data.frame': 0 obs. of 7 variables:
$ content : raw [1:162392] 3c 21 44 4f ...
$ date : POSIXct[1:1], format: "2019-02-24 23:41:59"
$ times : Named num [1:6] 0 0.00372 0.16185 0.45156...
..- attr(*, "names")= chr [1:6] "redirect" "namelookup" "connect" "pretransfer" ...
$ request :List of 7
..- attr(*, "class")= chr "request"
$ handle :Class 'curl_handle' <externalptr>
 - attr(*, "class")= chr "response"

Mentre la funzione getURL() di RCurl scaricava solo il codice HTML, la funzione GET() del package httr restituisce una lista con le proprietà della query, oltre al codice HTML. Per accedere al contenuto della pagina, dobbiamo impiegare la funzione content():

> str(content(packt_page, type = "text"), nchar.max = 200)
 chr "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">\n\t<head>\n\t\t<title>Packt Publishing | Technology Books, eBooks & Videos</title>\n\t\t<script>\n\t\t\tdata"| __truncated__

Per poter impiegare questi dati in un programma R, è necessario elaborare i dati HTML per strutturarli in un formato come una lista o un data frame.

Torna all’inizio.

Questo articolo richiama contenuti da Machine Learning con R.

Immagine di apertura di Kevin Ku su Unsplash.

L'autore

  • Brett Lantz
    Brett Lantz lavora da oltre dieci anni utilizzando metodi innovativi di analisi dei dati per comprendere il comportamento umano. Sociologo di formazione, collabora come istruttore a DataCamp Inc. ed è relatore in conferenze e seminari sul machine learning in tutto il mondo. Si interessa alle applicazioni della data science in ambiti diversi, dallo sport alla moda, dall'automazione di veicoli all'insegnamento delle lingue.

Iscriviti alla newsletter

Novità, promozioni e approfondimenti per imparare sempre qualcosa di nuovo

Gli argomenti che mi interessano:
Iscrivendomi dichiaro di aver preso visione dell’Informativa fornita ai sensi dell'art. 13 e 14 del Regolamento Europeo EU 679/2016.

Corsi che potrebbero interessarti

Tutti i corsi
machine-learning-per-tutti-cover Corso Online

Machine Learning & Big Data per tutti

con Andrea De Mauro

Vuoi scoprire cosa significano Machine Learning e Big Data e come i computer possono realmente imparare in maniera automatica? Questo corso di Andrea De Mauro fa al caso tuo.

big-_data_executive-home Corso Online

Big Data Executive: business e strategie

con Andrea De Mauro

Vuoi capire se e come la tua azienda può ottenere un vantaggio di business investendo in una strategia di creazione e analisi di Big Data? Il corso di Andrea De Mauro è quello che ti serve.

corso-data-governance Simone Aliprandi Corso Online

Data governance: diritti, licenze e privacy

con Simone Aliprandi

I dati sono ovunque intorno a noi ma per poterli utilizzare in sicurezza bisogna confrontarsi con temi complessi che riguardano licenze, proprietà intellettuale e privacy. Se non ti senti sicuro o hai paura di prendere la decisione sbagliata, il corso di Simone Aliprandi fa per te.


Libri che potrebbero interessarti

Tutti i libri

Software Licensing & Data Governance

Tutelare e gestire le creazioni tecnologiche

22,40

29,89€ -25%

18,91

19,90€ -5%

9,99

di Simone Aliprandi

Machine Learning con R

Conoscere le tecniche per costruire modelli predittivi

46,65

66,89€ -30%

37,91

39,90€ -5%

26,99

di Brett Lantz