Quello che segue è un altro piccolo esempio di come si possano scrivere programmi che implementano agenti intelligenti in una manciata di righe.

Supponiamo di voler replicare digitalmente il comportamento di una formica che insegue una traccia di ferormone fino ad un obiettivo. La traccia, per semplicità, non si sovrappone ed è sempre divisa da almeno una casella da un altra porzione di traccia.

Il comportamento di questa formica può essere emulato facendo uso di un agente reattivo con un bit di stato. Questi agenti hanno dei dati in ingresso che elaborano e trasformano in una serie di azione seguendo una o più regole di produzione. La nostra formica, ad esempio, può percepire solo se nella casella di fronte a lei c’è la traccia di ferormone oppure no.

La nostra formica è capace di effettuare solo le seguenti azioni:

  • Avanzare: avanza di una casella.
  • Ruotare a destra: ruota a destra.
  • Ruotare a sinistra: ruota a sinistra.
  • On: attiva il bit di stato.
  • Off: disattiva il bi di stato.
  • Sensi: riceve le informazioni sensoriali (nel nostro caso solo la presenza di ferormone nella casella di fronte) mettendo a Vero o Falso la variabile ferormone

A questo corrispondono le seguenti regole di produzioni.

  • S1: Se ferormone = True e stato = 0 -> Avanzare
  • S2: Se ferormone = False e stato = 0 -> On e Ruota a destra
  • S3: Se ferormone = True e stato = 1 -> Off e Avanzare
  • S4: Se ferormone = False e stato = 1 -> Ruota a destra x 2 e Off

Tutto questo è impleementato nel seguente codice:

# -*- coding: utf-8 -*-
"""
Questo modulo implementa una formica in grado di seguire
un percorso più un metodo per generare un percorso
casuale.
"""

import random

def generate_map(x, y) :
  """
  Genera un percorso casuale su una mappa X x Y.

    Args:
      X (int) larghezza della mappa
      Y (int) altezza della mappa

    Return:
      Una griglia X x Y contenente un percorso per la formica digitale.
  """

  mappa = [[0 for i in range(x)] for j in range(y) ] # Inizializza mappa vuota.
  i , j = 1, 1 # Posizione iniziale.
  mappa[j][i] = 1
  avaiable = ['right'] # Il primo passo va eseguito sempre a destra per semplicità.

  while len(avaiable) > 0 :
    next = random.sample(avaiable,1)

    if next[0] == 'up' :
      j -= 1
    elif next[0] == 'down' :
      j += 1
    elif next[0] == 'left' :
      i -= 1
    elif next[0] == 'right' :
      i += 1

    mappa[j][i] = 1

    avaiable = pick_avaiable(mappa,i,j)

  else :

    mappa[j][i] = 2 # Alla fine setta il punto obiettivo.

  return mappa

def pick_avaiable(mappa, i, j) :
  """
  Funzione ausiliaria per la generazione della mappa.
  Data una mappa e la posizione attuale restituisce le direzioni
  percorribili in accordo con la regola di non sovrapposizione.

    Args:
      mappa (mappa) La mappa corrente
      i (int) attuale posizione x
      j (int) attuale posizione y

    Return:
      Una lista di possibili direzioni.
  """

  new_avaiable = []

  if j > 1 :
    t = mappa[j-1][i-1] == 0 and mappa[j-1][i] == 0 and mappa[j-1][i+1] == 0
    t = t and mappa[j-2][i-1] == 0 and mappa[j-2][i] == 0 and mappa[j-2][i+1] == 0
    if t:
      new_avaiable.append('up')
  if j < (len(mappa) - 2) :     t = mappa[j+1][i-1] == 0 and mappa[j+1][i] == 0 and mappa[j+1][i+1] == 0     t = t and mappa[j+2][i-1] == 0 and mappa[j+2][i] == 0 and mappa[j+2][i+1] == 0     if t:       new_avaiable.append('down')   if i &gt; 1 :
    t = mappa[j-1][i-1] == 0 and mappa[j][i-1] == 0 and mappa[j+1][i-1] == 0
    t = t and mappa[j-1][i-2] == 0 and mappa[j][i-2] == 0 and mappa[j+1][i-2] == 0
    if t:
      new_avaiable.append('left')
  if i < (len(mappa[0]) - 2) :
    t = mappa[j-1][i+1] == 0 and mappa[j][i+1] == 0 and mappa[j+1][i+1] == 0
    t = t and mappa[j-1][i+2] == 0 and mappa[j][i+2] == 0 and mappa[j+1][i+2] == 0
    if t:
      new_avaiable.append('right')

  return new_avaiable

class Ant(object) :
  """
  Questa classe implementa la formica digitale.
  """


  VERSO = {'up': 0, 'right': 1, 'down': 2, 'left':3}

  def __init__(self, mappa) :
    self.x = 1
    self.y = 1
    self.mappa = mappa
    self.bit = False
    self.verso = self.VERSO['right']

  def r_rotate(self) :
    """
    Ruota la formica a destra.
    """

    self.verso += 1
    if self.verso == 4 :
      self.verso = 0

  def l_rotate(self) :
    """
    Ruota la formica a sinistra. Non usato attualmente.
    """

    self.verso -= 1
    if self.verso == -1 :
      self.verso = 3

  def forward(self) :
    """
    Fa avanzare di una casela la formica.
    """

    if self.verso == self.VERSO['up'] :
      self.y -= 1
    if self.verso == self.VERSO['down'] :
      self.y += 1
    if self.verso == self.VERSO['left'] :
      self.x -= 1
    if self.verso == self.VERSO['right'] :
      self.x += 1    

  def est_ferormone(self) :
    """
    Restituisce 1 se la casella daavanti al formica contiene
    del ferormone, 2 se è la casella obiettivo e 0 se la cella è vuota.
    """

    if self.verso == self.VERSO['up'] :
      return self.mappa[self.y-1][self.x]
    if self.verso == self.VERSO['down'] :
      return self.mappa[self.y+1][self.x]
    if self.verso == self.VERSO['left'] :
      return self.mappa[self.y][self.x -1]
    if self.verso == self.VERSO['right'] :
      return self.mappa[self.y][self.x + 1]

  def move(self) :
    """
    Cervello della formica. Elabora il comportamento in base
    alle regole di produzione sotto elencate.
    """

    if self.est_ferormone() == 2 :
      self.forward()
      return 'COMPLETE'
    if self.est_ferormone() and not self.bit :
      self.forward()
      return 'S1'
    if not self.est_ferormone() and not self.bit :
      self.bit = True
      self.r_rotate()
      return 'S2'
    if self.est_ferormone() and self.bit :
      self.bit = False
      self.forward()
      return 'S3'
    if not self.est_ferormone() and self.bit :
      self.r_rotate()
      self.r_rotate()
      self.bit = False
      return 'S4'

  def print_map_ant(self) :
    """
    Stampa la mappa del percorso contrassegnando
    la formica con una X.
    """

    print "-"*(len(self.mappa[0])*3)
    for j in range(len(self.mappa)) :
      for i in range(len(self.mappa[j])) :
        if i == self.x and j == self.y :
          print 'X ',
        elif self.mappa[j][i] == 0 :
          print "  ",
        else :
          print "%d " % self.mappa[j][i],
      print ":"
    print "-"*(len(self.mappa[0])*3)

Il codice contiene, oltre alla classe Ant che implementa la formica, una funzione per generare un percorso casuale che rispetta le regole che abbiamo fissato. Ad ogni chiamata del metodo move la formica digitale elabora i dati a sua disposizione e restituisce la regola di produzione prescelta (oppure COMPLETE se la formica ha raggiunto l’obbiettivo posto alla fine della traccia).

Inoltre la formica ha un metodo che stampa a schermo l’intera mappa del mondo, con la traccia e la posizione attuale della stessa (segnata con una X).

È un esempio carino che magari può essere espanso a piacimento, sia offrendo un output grafico più carino sia rendendo auomatica l’esecuzione dei vari move.

Sperando possiate trovare il codice interessante vi auguro una buona serata. :D

UPDATE:

Potete scaricare il file da qui:Ant.py

 

Si pensa spesso che grandi rivoluzioni siano accompagnate da grandi e complicate implementazioni. Tuttavia la storia ci insegna che le grandi idee spesso sono le più semplici.

Nell’IA sono proprio queste idee semplici a dare i migliori risultati. Fra queste un ottimo esempio è dato dall Reti Neurali. Questo modello matematico consiste, fondamentalmente, nell’interconnessione di unità a soglia.

Non entrerò nei dettagli matematici di una rete neurale ma voglio mostrare un codice di 290 righe (commenti e linee vuote incluse)  che implementano una semplicissima rete neurale non ricorsiva addestrata a con retropropagazione.

Non  credo sia la versione più “elegante” ne la più ottimizzata ma funziona discretamente.

# -*- coding: utf-8 -*-
"""
Modulo che contiene classi e metodi per rappresentare
una rete neurale non ricorsiva.

Modulo Didattico
"""


import math

class Sigmoid :
  """
  Rappresenta una singola unità sigmoide.
  """


  def __init__(self, num_in = 0) :
    """
    Costruttore.

      Args:
        num_in (int) Numero di ingressi della rete neurale.
    """

    self.w = [1.0]*num_in
    self.delta_value = [0,0]

  def __repr__(self) :
    return repr(self.w)

  @staticmethod
  def scalar(v1, v2) :
    """
    STATIC. Prodotto scalare fra i vettori v1 e v2.

      Args:
        v1 (vector) Vettore 1
        v2 (vector) Vettore 2

      Returns:
        Il prodotto scalare fra v1 e v2. (double)
    """

    res = 0
    for i in range(len(v1)) :
      res += v1[i]*v2[i]
    return res

  def sigma(self, ingresso) :
    """
    Ritorna il valore della funzione sigma del prodotto scalare fra
    ingresso e i pesi del sigmoide.

      Args:
        ingresso (vector) Vettore d'igresso

      Returns:
        Il valore della funzione sigma. (double)
    """

    s = Sigmoid.scalar(self.w, ingresso)
    return 1 / (1 + math.e**(-s))

  def activate(self, ingresso) :
    """
    Ritorna il valore di soglia per il prodotto scalare fra
    l'ingresso e i pesi del sigmoide.

      Args:
        ingresso (vector) Vettore d'ingresso.

      Returns:
        1 se il prodotto scalare è maggiore di zero, 0 altrimenti.
    """

    s = Sigmoid.scalar(self.w, ingresso)
    if s &gt; 0 : return 1
    return 0

  def add_in(self, w = 0) :
    """
     Aggiunge un ingresso al sigmoide.

      Args:
        w (double) Peso del nuovo ingresso.
    """

    self.w.append(w)

  def change(self, i, new) :
    """
    Cambia il valore del peso dell'ingresso i-esimo.

      Args:
        i (int) Numero dell'ingresso.
        new (double) Nuovo peso per l'ingresso.
    """

    if i &gt; len(self.w) :
      print "ERROR: "
      return
    self.w[i] = new
    self.delta_value[0] = 0

class NeuralNet :
  """
  Classe che rappresenta una rete neurale generica non ricorsiva.
  """


  def __init__(self, n_in) :
    """
    Costruttore.

    Il numero di ingressi NON comprende l'ingresso fittizio
    che simula la soglia.

      Args:
        n_in (int) Numero di ingressi della rete.
    """

    self.units = []
    self.n_in = n_in

  def __repr__(self) :
    return repr(self.units)

  def add_strate(self) :
    """
    Aggiunge uno strato alla rete.
    """

    self.units.append([])

  def add_unit(self, j, w = None) :
    """
    Aggiunge un unità sigmoide allo strato j.

      Args:
        j (int) Numero di strato.
        w (vector) Vettore dei pesi della nuova unità.
    """

    if j &gt;= len(self.units) :
      print "ERROR: "
      return

    # Se è il primo strato i sigmoidi hanno un numero di ingressi
    # pari al numero di ingressi della rete più  l'ingresso fittizio.
    # Altrimenti è pari al numero dei sigmoidi dello strato precedente
    # più l'ingresso fittizio.
    if j == 0 :
      sig = Sigmoid(self.n_in + 1)
    else :
      sig = Sigmoid(len(self.units[j-1]) + 1)
    self.units[j].append(sig)

    # Aggiorna il peso del nuovo sigmoide con i pesi nel vettore W.
    if w != None and len(w) == len(sig.w):
      p = 0
      for i in w :
        sig.change(p, i)
        p+=1

    # Se NON è l'ultimo strato bisogna aggiungere un ingresso a tutti
    # i sigmoidi dello strato successivo.
    if j != ( len(self.units) - 1 ) :
      for i in self.units[j+1] :
        i.add_in()

  def activate(self, ingresso) :
    """
    Attiva la rete neurale sull'ingresso 'ingresso'. Il
    vettore di ingresso NON contiene il vettore fittizio.

      Args:
        ingresso (vector) vettore di ingresso.

      Returns:
        uscita (vector) vettoore di uscita.
    """

    if len(ingresso) != self.n_in :
      print "ERRORE"
      return

    current = ingresso[:]
    current.append(1)
    for j in self.units :
      next_in = []
      for i in j :
        next_in.append(i.activate(current))
      current = next_in[:]
      current.append(1)
    return next_in

  def list_f(self, ingresso) :
    """
    Lista le uscite di ogni sigmoide della rete in base
    all'uscita della funzione sigmoide.

      Args:
        ingresso (vector) Vettore di ingresso.

      Returns:
        Il vettore contenente il risultato dell'applicazione di un
        vettore di ingresso PER OGNI sigmoide.
    """

    if len(ingresso) != self.n_in :
      print "ERRORE"
      return

    current = ingresso[:]
    current.append(1)
    res=[]
    for j in self.units :
      next_in = []
      for i in j :
        next_in.append(i.sigma(current))
      current = next_in[:]
      current.append(1)
      res.append(next_in)
    return res

  def delta(self, list_out, strato, sigmoide, desire) :
    """
    Calcola il valore delta per l'i-esimo sigmoide del j-esimo strato.

    La foruma del delta è:

      delta(i,j) = f * (1 - f) * SUM ( delta(j+1, l) )

    dove l varia per ogni sigmoide dello strato successivo.

    Se j = strato finale allora

      delta(i,j) = (desire - f) * f * (1 - f)

      Args:
        list_out (vector) Lista delle uscite della rete.
        strato (int) Numero dello strato.
        sigmoide (int) Numero del sigmoide.
        desire (double) Valore desiderato in uscita per l'ingresso.

      Returns:
        Il valore delta cercato.
    """

    f = list_out[strato][sigmoide]
    sig = self.units[strato][sigmoide]
    if sig.delta_value[0] == 1 :
      return sig.delta_value[1]
    if strato == ( len(self.units) - 1 ) :
      ris = ( desire - f) * f * (1 - f)
    else :
      ris = f*(1-f)
      cumulator = 0
      for l in range(len(self.units[strato + 1])) :
        wijl = self.units[strato+1][l].w[sigmoide]
        cumulator += self.delta(list_out, strato+1, l, desire) * wijl
      ris = ris * cumulator
    sig.delta_value = [1, ris]
    return ris

  def delta_reset(self) :
    """
    Invalida i delta_value dei sigmoidi.
    """

    for j in self.units :
      for i in j :
        i.delta_value[0] = 0

  def train(self, ingresso, desire, c = 1.0) :
    """
    Addestra una rete neurale sull'ingresso inserito.

    La formula dii retro propagazione è

      W(i,j) = W(i,j) + c * delta(i,j) * X(i,j)

    doeve W e X sono vettori.

      Args:
        ingresso (vector) Vettore in ingresso.
        desire (double) Valore desiderato in uscita.
        c (double) Velocità di apprendimento.
    """

    f = self.list_f(ingresso)
    x = f[:]    # Questa lista è come 'f' ma include anche l'ingresso.
                # Sia x che f NON includono gli ingressi fittizi.
    x.insert(0, ingresso)
    for j in range(len(self.units)) :
      for i in range(len(self.units[j])) :
        wij = self.units[j][i].w[:] # Sono i pesi del sigmoide (i,j)
        delta = self.delta(f, j, i, desire)
        for k in range(len(wij)) :
          if k == ( len(x[j]) ) : # Compensa la mancanza dell'ingresso
            xx = 1                # fittizio.
          else :
            xx = x[j][k]
          new = self.units[j][i].w[k] + c * delta * xx
          self.units[j][i].change(k, new)
    self.delta_reset()

Per capire a fondo il codice è necessario conoscere un po’ di teoria sulle reti neurali, per tutti gli altri vi basti notare come una delle più importanti metodologie dell’AI può essere replicata nei suoi tratti essenziali in meno di 300 righe.

 

I mondiali di calcio sono già finiti da qualche giorno e con loro si accingono a spegnersi le feroci critiche agli allenatori che, come sappiamo bene in prima persona, non hanno dato i risultati attesi.

Ed è proprio  dal paese dei nuovi campioni del mondo, ed in particolare dalla Universidad Carlos III de Madrid, i ricercatori hanno pubblicato alcuni risultati su un metodo di analisi automatica delle prestazioni sportive.

Il progetto si prefigge lo scopo di estrapolare degli indicatori sulle prestazioni e sulle tattiche dei giochi di squadra semplicemente analizzando il video di una partita. I ricercatori sono però fiduciosi di ottenere, in futuro, la possibilità di effettuare tali analisi in real-time in modo da poter migliorare e correggere le prestazioni sportive durante la partita. Cosa che, converrete con me, è molto più utile che farla a partita conclusa. :D

Il progetto, per il momento, è stato testato su alcune partite di basket ma è facilmente estensibile ad altri sport di squadra.

Forse fra qualche anno ci ritroveremo a “riconfigurare” un allenatore invece di esonerarlo. :D

Fonte: sciencedaily.com

 

“Ma che stai a fa mo?”  La domada mi arriva con cadenza periodica da parte di mia nonna ogni volta che sto a cena da lei.

“Intelligenza Artificiale.” rispondo io “Te l’ho già detto trecento volte!”. Rispondo sempre allo stesso modo e allo stesso modo ricevo sempre la stessa risposta: “Intelliche? Che sarebbe?”.

A questo punto arranco una risposta cercando di far minimamente intuire qualcosa sul comportamento intelligente di agenti artificiali ad una donna di 80 anni che non sa sintonizzare il digitale terrestre. La discussione ovviamente termina con il suo sguardo vuoto e allo stesso tempo contento che suo nipote faccia qualcosa di così complicato. La difficoltà fondamentale non consiste però nello spiegare di che cosa si occupa, in linea generale, l’AI, piuttosto il problema sta nel far capire perché sia così difficile da fare.

Se nelle altre scienze ci si occupa di cose tremendamente complicate, di campi gravitazionali, di reazioni chimiche, di strutture tridimensionali delle proteine e ricombinazione genetica. Nessuno che non abbia conoscenze specifiche si mette a discutere di queste cose con chi invece studia la materia da anni. Nell’AI, invece, da 50 anni si discute di cose chiare anche ad un bambino di 6 anni. Le persone quindi ti considerano o un fesso o un pazzo che cerca di replicare cose che solo il buon dio può donare e fare (come se i nostri cerveli fossero entità sovrannaturali).

Le cose cadono. Il passare del tempo. L’erba cresce. La differenza fra animali e piante. La morale. Sono tutti piccoli esempi di cose che qualunque persona o bambino sa quasi in modo innato ma che risultano terribilmente complicate da spiegare ad un robot.

Questa conoscenza di base prende il nome di conoscenza comune. Non esistono attualmente agenti intelligenti dotati di conoscenza comune. Gli unici tentativi in tale senso (ad esempio mindpixel) consistono nella creazione di enormi database contenenti tutta la conoscenza comune umana sotto forma di proposizioni logiche inserite manualmente.

Questa metodologia è valida come soluzione temporanea ma penso sia inadatta ad essere una soluzione definitiva (oltre ad essere poco elegante e affascinante). Dopotutto quant’è la conoscenza comune di un neonato? Poca. Quasi nulla. Questo dimostra che un sistema dotato della giusta base, dei giusti stimoli sensoriali e della giusta capacità di apprendere, può sviluppare autonomamente, tramite l’esperienza, la propria conoscenza comune.

“Ah! Famme il robot che pulisce pe casa!”. dopo aver afferrato cosa faccio mia nonna se ne esce spesso con questa frase. Ma anche molte altre persone, pensando alle applicazioni dell’AI, si perdono nel sogno del “robot maggiordomo”. Tuttavia anche questa applicazione ha il forte bisogno di una buona conoscenza comune. Essa è infatti necessaria per l’interpretazione del linguaggio parlato e per poter adempiere in modo autonomo ad una serie di compiti molto variegati quali cucinare, lavare i piatti e falciare il prato. Un tale robot dovrebbe sapere che il cibo lasciato troppo sul fornello brucia mentre lasciato poco resta crudo, che i piatti che cadono si rompono, che l’acqua calda lava meglio di quella fredda, che la  falciatrice si trova nel ripostiglio e via dicendo ma allo stesso tempo non necessita di essere esperto in chimica e termodinamica per poter valutare l’effetto del fuoco sul cibo ne calcolare campi gravitazionali e forze per sapere che un piatto che cade si rompe o che girarsi troppo velocemente con un bicchiere in mano può far cadere l’acqua.

La conoscenza comune potrebbe essere anche utilizzata per far rendere conto l’agente stesso dei propri limiti favorendo così l’apprendimento di nuove procedure specifiche.

Insomma, la ricerca sull’AI, se escludiamo tutto il lavoro di logica e di ricerca su grafi, si riduce proprio a questo: la ricerca di un modo per rappresentare ed apprendere la conoscenza comune.

Tutte cose che sanno tutti in modo inconscio e naturale. Quindi anche se mia nonna non sa e non saprà mai nulla sull’AI, per ora, sa già molto più di tutti noi che ci lavoriamo su.

 

Probabilmente fra tutti gli esempi di applicazioni dell’Intelligenza Artificiale la più semplice e famosa consiste sicuramente nel gioco 20q. Il gioco in questione funziona così: il giocatore pensa ad un oggetto o ad un animale e il marchingegno tenta di indovinare di cosa si tratta in meno di 20 domande a cui il giocatore può rispondere si o no. Potete trovare una versione online del gioco all’indirizzo http://www.20q.net/. Potrete constatare che in alcuni casi il programma sembra proprio leggervi nella mente con risultati quasi spaventosi.

La versione usata dal sito è piuttosto avanzata ed utilizza le reti neurali e permette quindi una gamma di riposte più vasto (c’è anche forse, probabilmente, a volte, ecc…). Il programma è inoltre in grado di apprendere mentre si gioca: ogni qual volta si gioca, o il programma si arrende richiedendo di inserire l’oggetto a cui si pensava, l’insieme di risposte appena date plasma la struttura dati interna del programma in modo tale che la prossima volta l’oggetto venga riconosciuto più velocemente. È un modello di apprendimento molto basilare ma che è analogo a quello umano percezione-previsione-modifica: il programma riceve dati percettivi (le risposte alle domande), formula un ipotesi (i tentativi di indovinare) e modifica la struttura dati in base all’esito della previsione.

Sarei curioso di sapere se il programma del sito ha qualche contromisura contro il problema del sovra-adattamento: questo tipo di problema affligge gran parte dei modelli decisionali, comprese le reti neurali, ed è causato da un eccessivo adattamento della rete a certi tipi di dati piuttosto che ad altri. Facciamo un esempio. Nel gioco 20q è molto più probabile che il giocatore pensi a cose “banali” come “gatto”, “cane”, “fiore” e simili piuttosto che a “antilope”, “spinterogeno” e “neutrino”. Questo significa che nel corso dell’esistenza del gioco il programma verrà addestrato alla perfezione a riconoscere cani e gatti mentre riceverà poche informazioni riguardo antilopi e neutrini. Possiamo quindi dire che “gatto” e “fiore” e le parole comuni facciano parte dell’insieme di addestramento di una rete. Bene, è possibile dimostrare che una rete sovra-adattata al suo insieme di addestramento indovina, in media, molto meno volte oggetti che non appartengono a tale insieme rispetto ad una rete che è solo parzialmente adattata.

Rimandiamo però questi approfondimenti sulle reti neurali. Il motivo per cui ho scritto questo articolo era di mostrare come sia possibile in una manciata di righe scrivere un programma che emuli in modo semplice il gioco di 20q. Per questa approssimazione consideriamo che sia possibile dare alle domande soltanto le risposte si o no.

Il metodo che vi mostrerò non utilizza nessuna tecnica propria dell’IA bensì dei semplicissimi alberi binari. Questi alberi binari li indicheremo come alberi decisionali binari. Nel nostro albero decisionale definaiamo:

  • I nodi interni dell’albero sono domande.
  • I nodi foglia dell’albero sono gli oggetti da indovinare.
  • Il sotto-albero destro di un nodo corrisponde al caso in cui la risposta alla domanda sia si.
  • Il sotto-albero sinistro di un nodo corrisponde al caso in cui la risposta alla domanda sia no.

L’algoritmo del gioco consiste in una cosa del tipo:

  • Il programma formula la prima domanda che trova (cominciando con il nodo radice).
  • Se non c’è più nessuna domanda il programma si arrende e chiede al giocatore di inserire 1) l’oggetto a cui stava pensando. 2) una domanda che rappresenta l’oggetto 3) la risposta a questa domanda.
  • Il programma aggiunge questa domanda e la relativa risposta al sotto albero in cui si era fermato (oppure come nodo radice se l’albero era vuoto).
  • Se invece la domanda c’è il programma formula la domanda e scende nel sotto-albero relativo alla risposta.
  • Il programma ricomincia dal punto 1.

Tutto qui. Tale algoritmo è effettivamente molto grezzo e sono possibili un gran numero di migliorie (ad esempio usare dei modelli di albero più “elastici” per evitare alberi troppo sbilanciati) ma svolge decentemente il proprio lavoro. Dopo che l’avete fatto girare per qualche tempo vedrete che inizierà ad imparare e ad indovinare un discreto numero di parole. Ovviamente potete partire da un albero vuoto oppure pre-costruire un albero di base con alcune domande comuni.

Un altra cosa che può farci capire questo semplice programma è una considerazione che mi ha fatto notare un gentile commentatore: la capacità degli algoritmi di AI non dipende dalla dimensione del programma ovvero dal suo numero di righe di codice allo stesso modo di come le capacità celebrali umane non dipendono dalla dimensione del cervello (una balena ha il cervello molto più voluminoso di un essere umano eppure fa molte meno cose). Le capacità cognitive di un programma dipendono solamente dalle relazioni che si auto-generano fra le strutture dati del programma (nel nostro caso tali relazioni sono i collegamenti ad albero fra i nodi).

Se qualcuno avesse voglia di creare questo programmino usando il minor numero di righe possibili sarei felice di pubblicarlo. Inoltre potreste anche usarlo per sbalordire amici, parenti e vicini di casa! XD

Alla prossima!

 

L’Intelligenza Artificiale offre sempre spunti interessanti per riflettere su noi stessi. Per prima cosa è bene che sappiate che io sono un fermo sostenitore dell’ IA forte e pienamente convinto che, prima o poi, sarà possibile replicare artificialmente una mente  cosciente di se. Questo non implica che tali agenti intelligenti debbano per forza avere meccanismi di pensiero simili ai nostri, dopotutto la storia scientifica ci insegna che tutte le idee umanocentriche sono sempre state demolite dall’evidenza dei fatti, è quindi poco furbo ipotizzare che il cervello umano sia l’unica forma di “coscienza” possibile nell’universo. Tuttavia il cervello umano è senza dubbio l’unico che conosciamo attualmente e, quindi, è naturale che gran parte dell’IA si affidi a quello come musa ispiratrice e come traguardo.

A sostegno di questa tesi c’è poi la constatazione che, in più di un caso isolato, l’AI è riuscita ad anticipare scoperte che le neuroscienze cognitive avrebbero fatto solo alcuni anni dopo. Un caso fra i tanti è quello delle reti neurali. Una rete neurale non è altro che un sistema di neuroni artificiali (che non sono altro che sommatori pesati collegati ad un meccanismo “a soglia”, ovvero la cui uscita si attiva solamente se l’ingresso supera un certo valore) strettamente collegati fra loro. Tale meccanismo, sebbene composto da elementi relativamente semplici, è in grado di svolgere compiti estremamente complessi come, ad esempio, guidare un auto per centinaia di miglia  (si veda il progetto ALVINN). Queste reti vengono addestrate tramite il meccanismo di retro-propagazione. Questa tecnica consiste, per farla breve, nel modificare i pesi in ingresso ai neuroni artificiali in base al valore di predizione dell’intera rete: ho un dato ingresso x, la rete risponde y, se il valore esatto è y allora la rete non subisce variazioni, se invece il valore giusto era z la rete viene modificata in proporzione alla differenza fra il valore predetto e il valore desiderato. Bene, questo processo è del tutto simile al meccanismo alla base dell’apprendimento nella gran parte degli agenti viventi (non solo l’uomo):  è stato infatti constatato come il cervello rilasci dopamina a seconda se un evento previsto si verifichi o no. L’apprendimento tramite il modello errore-predizione è stato quindi sviluppato dall’AI alcuni anni prima che tale modello venisse formalizzato dai neuroscienziati.

L’AI inoltre ci permette anche di rivalutare molte nostre capacità. Tutti noi sappiamo riconoscere volti o comprendere il linguaggio naturale, ragion per cui consideriamo queste due attività delle cose “facili”. Diversamente solo poche persone sanno giocare bene a scacchi e quindi concludiamo che questo sia un compito “difficile”. In realtà dopo oltre 50 anni di ricerca nelle intelligenze artificiali  abbiamo decine di programmi in grado di giocare stupendamente a scacchi ma ben pochi programmi in grado di riconoscere un volto con prestazioni paragonabili a quelle umane. Questo quindi ci mostra come, in realtà, il giudizio che diamo ai vari problemi sia spesso invertito nel caso delle AI.

Perché? Una prima risposta consiste nel valutare l’importanza di tali problemi. Riconoscere un volto e capire il linguaggio sono cose essenziali a livello evolutivo. Animali che non siano in grado di riconoscere un potenziale predatore o preda da un sasso o da una pozza di lava difficilmente raggiungono l’età adulta, è quindi prevedibile che nel corso dell’evoluzione si siano formati cervelli abilissimi a riconoscere figure forme e colori e meno a giocare a scacchi. Ipoteticamente, infatti, se uccidessimo in fasce tutti i bambini che non siano in grado di giocare a scacchi ad un livello accettabile ci ritroveremmo, fra qualche milione di anni, davanti a esseri dotati di un cervello incredibilmente bravo a giocare a scacchi e, probabilmente, meno bravi a riconoscere un volto.

Il problema dell’evoluzione è quindi cruciale quando cerchiamo di imitare il cervello umano. Dobbiamo sempre tenere presente che il nostro cervello è quello che è perché c’è stata, nel corso delle ere, una pressione evolutiva che lo ha spinto ad avere certe capacità e a non averne altre.

Il secondo punto risiede proprio nella rappresentazione e nel modo in cui essa viene generata. Spesso infatti pretendiamo troppo dalle macchine che progettiamo. Prendiamo ad esempio la percezione visiva. Quando ci troviamo difronte alla percezione visiva ci rendiamo subito conto del problema fondamentale che la divide da problemi “semplici” come il gioco degli scacchi: il rumore. Il gioco degli scacchi è infatti facilmente rappresentabile in termini puramente matematici, è possibile cioè togliere semplicemente tutti gli aspetti rumorosi, ovvero che non danno alcuna informazione aggiuntiva tipo la forma degli scacchi o il colore della scacchiera. Quando invece tentiamo di semplificare la visione vengono alla luce notevoli problemi e domande. Ad esempio il concetto di forma e di bordo: che cos’è una forma? Quand’è che un quadrato a cui arrotondiamo via via gli angoli, smette di essere un quadrato e diventa un cerchio? Come definiamo il bordo di un oggetto data la sua immagine? Sappiamo che le risposte a queste domande sono essenziali per la visione ma non abbiamo idea di come il cervello umano le calcoli.

Abbiamo un gran numero di algoritmi in grado di darci i contorni e  la forma di un oggetto ma tali algoritmi sono spesso complessi, macchinosi ed utilizzano equazioni matematiche non banali sull’intera matrice dell’immagine. Ma, oltre al fatto che tali algoritmi sono piuttosto imprecisi, non è così che funziona l’occhio umano. L’occhio, ad esempio, è in gran parte ceco ad eccezione di una piccola zona centrale. L’impressione di avere un unica grande immagine è del tutto illusoria e prodotta dalla mente. L’immagine quindi è un prodotto della mente non un input percettivo. Perché allora vogliamo che le macchine funzionino direttamente sull’immagine? Non sarebbe meglio concentrarsi su come creare questa immagine partendo da una migliore copia dell’occhio umano? Ad esempio utilizzando i meccanismi basilari dell’uomo quali visione spot (ovvero concentrata al centro)  e rilevatore di movimento nella periferia dell’occhio (che serve a direzionare istintivamente il punto “vedente” dell’occhio in direzione di qualcosa che è cambiato per aggiornare l’immagine mentale). Queste cose potrebbero insegnarci nuovi modi per valutare  figure e bordi (un bordo potrebbe essere segnalato da una variazione repentina della visione (spot) che si avverte mentre l’occhio si muove oppure la forma di un oggetto può essere elaborata anche dal modo in cui l’occhio percorre il suo bordo (infatti quando osserviamo un quadrato, capiamo che è un quadrato percorrendo con gli occhi il bordo del quadrato, non lo facciamo consciamente ma lo facciamo).

Insomma, la neuroscienza ha molto da imparare dall’intelligenza artificiale, ma anche  l’intelligenza artificiale apprende molto dalla prima. Le due discipline si trascinano a vicenda verso nuovi traguardi e molto possiamo aspettarci da entrambe nei prossimi anni. E’ con questa consapevolezza che potremo arrivare un giorno a trovare la chiave, il punto di svolta, quella scoperta che ci farà capire che la soluzione ce l’avevamo da sempre sotto gli occhi. O forse dietro.

 

Golden gene in DNA Se state pensando ad orrori tipo cani a due teste oppure ad un esercito di super-soldatigeneticamente modificati siete completamente fuoristrada. In informatica, e in particolare nell’intelligenza artificiale, per programmazione genetica si intende una metodologia di programmazione automatizzata ispirata dall’evoluzione biologica. Ma cosa significa? La selezione naturale si basa sulla sopravvivenza e la capacità di un essere vivente di avere capacità migliori rispetto ad un altro se sottoposto ad alcune pressioni naturali quali le caratteristiche dell’ambiente e la presenza di particolari predatori. Com’è possibile quindi trasportare tutto questo a livello di programmazione?

Ovviamente le parti in gioco sono diverse ma analoghe. Il codice genetico di un animale diventa semplicemente il codice di un particolare programma o funzione e la selezione naturale si riduce ad “avere le caratteristiche migliori”.

Così semplice? Almeno grossolanamente si. Ma se andiamo ad analizzare attentamente questi due aspetti possiamo trovarci di fronte a problematiche più sottili. Ad esmpio: come si può portare il codice di un programma ad assomigliare ad un codice genetico?

La risposta è altrettanto apparentemente semplice. Ci sono alcune caratteristiche che un programma, o meglio la sua rappresentazione, deve rispettare. Innanzitutto il programma deve poter essere “riprodotto” secondo una riproduzione sessuata, ovvero tramite crossover. Inoltre tale codice deve essere sottoposto a mutazioni casuali.

Spiegare come possano avvenire queste due cose su un programma o un algoritmo è qualcosa di piuttosto complesso che non ho la pretesa di poter spiegare qui. Forse lo farò un in qualche articolo separato. Sappiate però che l’approccio più semplice è quello di utilizzare  una struttura ad albero per rappresentare i programmi e simulare crossover e mutazioni come scambi e variazioni di sotto-alberi scelti casualmente all”interno del programma.

La cosa da fare, una volta che sappiamo come rappresentare i programmi, è di generare una popolazione iniziale. L’algoritmo procede in linea di massima così:

  • Viene generata casualmente una popolazione di programmi iniziale.
  • Vengono fatti funzionare tutti questi programmi e vengono raccolti i risultati.
  • I risultati vengono valutati con una funzione di FITNESS.
  • Il programma che ottiene il maggior punteggio di fitness passa immutato alla seconda generazione e inoltre viene utilizzato per “riprodurre” una nuova generazione di programmi usando crossover e mutazioni.
  • Si torna al punto due e così via finché non viene estrapolato un programma sufficentemente valido.

La funzione di fitness è una funzione che prende in ingresso il codice genetico di un programma, lo avvia e confronta il risultato con il risultato “ideale”. Il concetto di risultato ideale è piuttosto complesso: molto spesso, infatti, non abbiamo chiara l’idea di quale sia il risultato ideale. Ma anche questo, da solo, potrebbe essere argomento di un libro. Il concetto di fondo della funzione di fitness rimane comunque valida.

Come vedete sia i termini che il procedimento ricorda molto quello che avviene in natura. Un interessante esempio di proto-programmazione genetica è narrata infatti anche nel libro di Richard Dawkins L’orologiaio cieco del 1989 in cui questo processo di selezione “informatica” viene usato dall’autore per mostrare la potenza della selezione cumulativa.

L’argomento è, onestamente, uno dei miei prefetiti e da tempo cerco di perfezionare un ambiente di programmazione genetica in Python chiamato Galapagos e spero con questo articolo di aver incuriosito qualcuno riguardo questa fascinosa metodologia di programmazione. Mi spiace parlarne così frettolosamente ma lo spazio è quello che è.

Vi lascio perciò un link nel quale potete approfondire l’argomento (in attesa che ci pensi io ;) )

http://www.genetic-programming.org/

 

TerminatorQuest’articolo è uno spunto di riflessione sulle neuroscienze. Non parlo molto dei miei esperimenti di intelligenza artificiale. Non ho ancora ottenuto risultati degni di nota anche perché il mio approccio è completamente diverso dall’approccio classico. L’intelligenza artificiale è popolata di algoritmi e modelli che singolarmente non ci azzeccano nulla con la mente umana. Il problema è, credo, che ci si concentra troppo su aspetti macroscopici.

Programmi in grado di leggere, programmi in grado di ascoltare, programmi che guidano mille miglia nel deserto su piste impreviste. Tutte abilità notevoli che però mancano platealmente il bersaglio. Le macchine eseguono alla perfezione quei lavori ma rimangono macchine proprio perché incapaci di fare altro.

C’è però qualcosa che unisce materia a pensiero. Un anello di congiunzione fra una serie di impulsi e l’auto-coscienza. E’ li che io mi spingo, è quello che mi piace fare.

Così in quest’ultimo anno ho studiato abbastanza la psicologia infantile, in particolare la psicologia neonatale. Questo perché non c’è differenza in atto fra un neonato (e prima ancora un feto) e un complicatissimo schema elettonico per quanto quest’idea sia eticamente raggelante. La differenza sta in cosa queste due macchine sanno fare nativamente. I bambini, si sà, nascono con l’abilità di nutrirsi (il saper ciucciare) e con l’abilità di apprendere. Ed è proprio questa seconda abilità che mi ha sempre affascinato.

Come nasce? Appena nati cosa si sa? Nulla. Non esiste nessuna precondizione nel cervello di un neonato ad esclusione di quelle essenziali informazioni genetiche. Un neonato impara per imitazione, si dice, ma non basta. Non è tutto. Non è possibile che sia tutto: se noi costruissimo una macchina in grado di imitare tutto non sarebbe una macchina molto intelligente.

All’imitazione dobbiamo aggiungere la memoria. La memoria abnorme di un neonato, memoria che però è diversa da quella che utilizziamo in età adulta. Infatti sebbene memorizziamo tonnellate di informazioni nei primi anni di età quasi nessuno di noi riesce a ricordare nulla di quegli anni. Questo significa che tutte quelle informazioni agiscono in modo diverso. Non permangono nel cervello ma lo ristrutturano.

Ma basta? No. Ancora no. Questo meccanismo è simulato dalle reti neurali, strutture dati in grado di ristrutturarsi in base agli input esterni. Eppure le reti neurali non bastano ancora. Allora aggiungiamo i sensi. Non potremo mai evolvere un intelligenza artificiale simile all’intelligenza umana se il tipo di input in grado di ricevere siano diversi da quelli umani.

Ma ancora non basta. Una rete neurale più 5 sensori non da ancora un essere umano. Cosa possiamo aggiungere? L’iterazione del pensiero. Altro non è che una valutazione dell’istinto. Pensiamo a qualcosa, valutiamo se quell’idea è esatta e se non lo è la perfezioniamo. Questo è una bozza di ragionamento. Un qualcosa che può essere simulato da alcune varianti della programmazione genetica.

Ovviamente ancor anon basta. C’è qualcosa che ci sfugge. Che sia un innata abilità nel valutare proposizioni logiche? Può darsi, da adulti non possiamo saperlo. Da adulti il nostro pensiero si unisce totalmente al linguaggio facendoci sfuggire quel qualcosa di primordiale alla base. Quel qualcosa alla base che diamo totalmente per scontato.

Ma quella cosa sicuramente darà dei risultati che ci lasceranno sorpresi. Nel mio piccolo tempo fa avevo preso ad insegnare ad un mio programma (che univa reti neurali a programmazione genetica) a distinguere i poligoni che disegnavo sullo schermo. Ovviamente la cosa è lunghissima, la programmazione genetica è molto lenta già di suo, immaginatela ripetuta per ogni “pensiero” che il programma genera. Ci sono voluti due mesi per fargli distinguere un cerchio da un quadrato. La cosa che mi ha sorpreso però è che il programma alla fine dava segni si squilibrio. Vedeva poligoni che non avevo nemmeno disegnato. Era schizzofrenico.

Mi sono chiesto allora: possibile che tutti i difetti della mente umana (malattie mentali, emozioni incontrollate e molto altro) siano conseguenze obbligate di una struttura cognitiva complessa? Quando l’intelligenza artificiale arriverà ad emulare il comportamento umano avremo necessariamente macchine dotate degli stessi nostri difetti? Avremo macchine malate di mente?

Non so. Non posso esserne sicuro. Ma potremmo avere macchine più umane di quanto vorremmo.

© 2008-2012 SlashCode Suffusion theme by Sayontan Sinha