Vai al contenuto Link Cerca Menu (external link)
Coding e fondamenti di AI
Cosa impari in questo modulo
  • funzioni
  • argomenti
  • valore di ritorno
  • test di una funzione
  • funzioni che chiamano funzioni
  • ricorsione
  • caso base

Lezione

La funzione: dare un nome a una regola

L’autovelox del modulo 2 funziona, ma per usarlo con un’altra velocità dovete modificare la prima riga e rieseguire tutto. Se il calcolo servisse in dieci punti diversi di un programma, lo copiereste dieci volte. La funzione incapsula il calcolo sotto un nome:

multa <- function(velocita, limite){
  differenza <- velocita - limite
  if(differenza <= 0){
    importo <- 0
  }else if(differenza <= 10){
    importo <- 36
  }else if(differenza <= 40){
    importo <- 148
  }else if(differenza <= 60){
    importo <- 370
  }else{
    importo <- 500
  }
  return(importo)
}

multa(70, 50)
multa(45, 50)
multa(60, 50)

function(velocita, limite) dichiara gli argomenti, i dati che la funzione riceve. Il corpo è identico a prima. return(importo) è il valore di ritorno, quello che esce. Le tre chiamate in fondo restituiscono 148, 0 e 36. Notate che i test del modulo 2 sono diventati tre righe: chiamare la funzione con gli input del test e confrontare con l’atteso. Da qui in poi testare una funzione significa esattamente questo:

multa(60, 50) == 36
multa(110, 50) == 370
multa(111, 50) == 500

Tutti TRUE: la funzione passa i test. Le variabili create dentro la funzione (differenza, importo) vivono solo lì dentro: fuori non esistono, e non disturbano il resto del programma.

Con lo stesso schema scriviamo bmi(peso, altezza) e nps(voti), che riceve un vettore e restituisce un numero solo: una funzione può contenere un ciclo, e chi la chiama non deve saperlo.

In aula abbiamo scritto solo funzioni con return() esplicito. R restituisce anche l’ultimo valore calcolato se return manca, ma scriverlo rende chiaro cosa esce. Un’altra cosa che in aula non si è vista: gli argomenti possono avere un valore predefinito, function(velocita, limite = 50), e allora multa(70) funziona.

Una funzione che nessuno ha scritto

Chiudiamo con una frase da tenere a mente fino al modulo 8. multa() è una funzione: entra una velocità e un limite, esce un importo. Le regole dentro le abbiamo scritte noi leggendo il Codice della Strada. Un modello di machine learning è una funzione che nessuno ha scritto: entrano dati, esce una previsione, e le regole dentro le ha trovate un algoritmo guardando esempi. La forma è la stessa; cambia chi decide le soglie. Le funzioni multa(), bmi() e rata() di questo modulo torneranno nel modulo 13, quando un modello di linguaggio le userà come strumenti.

Lezione 6: funzioni e ricorsione

Il percorso parte da lezione_6.R e dalla differenza fra stampare e restituire. Una funzione che fa print() mostra un numero e basta: x <- bmi_stampa(86, 1.73) lascia x vuoto. Una funzione che fa return() dà indietro un valore, che si può assegnare, confrontare con un atteso o passare a un’altra funzione. Da qui nascono bmi(peso, altezza), che fa un conto, e classificazione(valore), che prende una decisione: due lavori diversi, due funzioni, e classificazione(bmi(86, 1.73)) che le mette in fila.

La stessa coppia di funzioni finisce dentro una piccola applicazione web: app.R usa Shiny per mettere due cursori attorno al calcolo. L’interfaccia cambia, la funzione no: se sbaglia a BMI 25 esatto, sbaglia anche con i cursori.

Le slide della lezione 6 contengono i testi dei tre laboratori.

Una funzione è ricorsiva quando richiama sé stessa. Servono un caso base, che restituisce subito il risultato, e una chiamata che si avvicina al caso base. Senza caso base la funzione non si ferma: R risponde «evaluation nested too deeply». Nei tre laboratori le tariffe sono inventate per l’esercizio.

Primo laboratorio: singoli o carnet

Un biglietto singolo costa 3 € e copre un viaggio. Un carnet costa 10 € e copre cinque viaggi. Si possono acquistare più carnet e i biglietti possono avanzare. L’input è il numero intero di viaggi da fare, almeno zero; l’output è il costo minimo per coprirli tutti.

costo_minimo <- function(viaggi){
  if(viaggi <= 0){
    return(0)
  }

  con_singolo <- 3 + costo_minimo(viaggi - 1)
  con_carnet <- 10 + costo_minimo(viaggi - 5)

  return(min(con_singolo, con_carnet))
}

Con quattro viaggi si confrontano 3 + costo_minimo(3), cioè 12 €, e 10 + costo_minimo(-1), cioè 10 €. Il valore negativo significa che il carnet copre anche un viaggio in più: il caso base deve essere <= 0. Con sei viaggi il costo minimo è 13 €; con dieci è 20 €.

Il codice commentato e le prove sono in biglietti.R. La funzione esplora entrambe le alternative e ricalcola alcuni casi: in aula usiamo pochi viaggi per seguire le chiamate.

Secondo laboratorio: due fornitori

Un ufficio acquisti deve comprare un’unità di ciascun prodotto. Ogni prodotto si può prendere dal fornitore A o dal fornitore B, e ogni fornitore da cui si ordina aggiunge la sua spedizione: 8 € per A, 5 € per B. Se non compriamo nulla da un fornitore, non paghiamo la sua spedizione.

Prodotto Fornitore A Fornitore B
Carta 20 € 22 €
Penne 30 € 32 €
Cartucce 40 € 38 €
Spedizione 8 € 5 €

Comprare ogni prodotto dove costa meno (carta e penne da A, cartucce da B) costa 88 € più due spedizioni: 101 €. Comprare tutto da A costa 90 € più 8: 98 €. Ma il minimo è 97 €, tutto da B, che su carta e penne costa di più e si fa perdonare con la spedizione. La scelta migliore non si trova prodotto per prodotto.

Il vettore scelte descrive le decisioni nello stesso ordine dei prodotti. c("A", "B") significa che carta e penne sono già assegnate, mentre manca la scelta delle cartucce. c() indica che nessuna scelta è stata ancora presa.

Nel file fornitori.R ci sono solo due funzioni. costo(scelte) riceve tre scelte complete e calcola il conto. minimo(scelte) completa le scelte mancanti:

minimo <- function(scelte){
  if(length(scelte) == length(prodotti)){
    return(costo(scelte))
  }

  con_a <- minimo(c(scelte, "A"))
  con_b <- minimo(c(scelte, "B"))

  return(min(con_a, con_b))
}

minimo(c())  # 97 euro

Ogni chiamata aggiunge una scelta, quindi si avvicina al caso base delle tre scelte complete. Le combinazioni sono otto. Ogni chiamata restituisce un numero, il costo minimo; non restituisce il vettore dei negozi da cui acquistare.

In aula Le due funzioni leggono prodotti, prezzi_a, prezzi_b e le due spedizioni, definiti all’inizio del file. È una semplificazione per concentrarci sulla ricorsione. I prezzi devono essere positivi, i vettori devono avere la stessa lunghezza e i prodotti devono comparire nello stesso ordine.

Le prove da fare: costo(c("A", "A", "A")) deve dare 98, costo(c("A", "A", "B")) deve dare 101 e minimo(c("B")) deve dare 97. Nell’ultimo caso fissiamo il primo prodotto da B e lasciamo alla funzione le altre due decisioni.

Terzo laboratorio: lo sconto sopra una soglia

Stesso ordine, una regola in più: ogni fornitore sconta chi supera una soglia di spesa. A toglie 12 € a chi arriva a 70 € di prodotti, B toglie 5 € a chi arriva a 90 €. Lo sconto si calcola sui prodotti, prima della spedizione, e la soglia è compresa: a 70 € esatti lo sconto di A spetta.

Cambia solo costo(); minimo() resta identica. E cambia la risposta: il minimo diventa 86 €, tutto da A, contro i 97 € del laboratorio precedente. Una regola in più e il fornitore migliore è un altro.

L’ordine dei passaggi conta: prima si sommano i prodotti, poi si toglie lo sconto, infine si aggiunge la spedizione. E torna il confine: l’ordine c("B", "A", "A") lascia ad A esattamente 70 € di prodotti, quindi lo sconto spetta. Scritto con > al posto di >=, quello stesso ordine costa 105 € invece di 93. Il codice è in fornitori_sconti.R.

Per approfondire

Il ciclo che queste funzioni sostituiscono è nel modulo 3. Nel modulo 5 le funzioni le faremo scrivere all’AI, dopo aver capito il problema. Nel modulo 6 i vettori diventano le colonne di una tabella, e il ciclo for per selezionare e contare sarà sostituito da un verbo di dplyr.

Lab