< Home
Stampa

Classificatori Bayesiani

Sommario

Teorema di Bayes

Introduciamo un importante teorema di probabilità.

Dati due eventi A e B, e le probabilità PA e PB che questi avvengano, si può applicare la seguente formula, detta Teorema di Bayes:

PAB=PBAPAPBP_{\frac{A}{B}} = \frac{P_{\frac{B}{A}} * {P_A}}{P_B}

dove la notazione P (A | B) è detta “probabilità condizionata che succeda A se succede B”. Si ricorda che la probabilità è un numero reale tra 0 (nulla) e 1 (certezza). Il teorema ci dice che la probabilità che succeda l’evento A se succede l’evento B è uguale alla probabilità che succeda l’evento B se succede A moltiplicato per la probabilità dell’evento A per la probabilità dell’evento B.

Poniamo di avere un mazzo di carte da briscola e peschiamo una carta a caso.

  • la probabilità PA che esca un 7 è 1/10, cioè 0.1
  • la probabilità PB che esca bastoni è invece 1/4, cioè 0.25
  • la probabilità P (B | A) ovvero che se esce una 7, questo sia di bastoni, è pari a 0.25

La probabilità P (A | B) ovvero se esce bastoni questo sia un 7 sarà

PA/B = 0.25 * (0.1 / 0.25) = 0.1

In questo esempio abbiamo mostrato un esempio di validità del teorema, ma non era necessario usarlo, perché potevamo calcolare direttamente P (A | B). Il discorso cambia invece quando la probabilità P (A | B) è molto più difficile da calcolare di P (B | A). In questo caso il teorema scompone un problema complesso in tre problemi più semplici.

Classificatore bayesiano

Ipotizziamo di avere questo CSV di recensioni:

Pessimo. Prodotto arrivato rotto e inutilizzabile;1
Una vera truffa. Non funziona affatto;1
Pessima qualità. Materiali scadenti;1
Servizio clienti inesistente. Sconsigliato;2
Soldi buttati. Si è rotto al primo utilizzo;2
Sotto le aspettative. Funziona male;2
Non mi piace. Spedizione molto lenta;2
Poco resistente. Plasticoso;2
Molto deluso. Non rispecchia la descrizione;3
Si surriscalda subito. Non ci siamo;3
Prezzo troppo alto per quello che offre;3
Rapporto qualità prezzo insufficiente;3
Discreto. Fa il suo dovere senza lodi;3
Funziona bene. Nulla di eccezionale;3
Prodotto nella media. Nella norma;3
Non male dai. Pensavo peggio;3
Accettabile per il prezzo pagato;3
Sufficiente. Arrivato nei tempi;3
Fa il suo lavoro senza infamia;3
Esperienza normale. Prodotto base;3
Molto buono. Funziona davvero bene;4
Bel prodotto. Qualità davvero buona;4
Soddisfatto dell'acquisto. Consigliato;4
Buona fattura. Consegna veloce;4
Ottimo rapporto qualità prezzo;4
Fa bene quello che promette;4
Buon prodotto nel complesso;4
Perfetto. Super consigliato;5
Eccezionale. Oltre le aspettative;5
Fantastico. Spedizione velocissima;5

L’obiettivo è costruire un classificatore, ovvero un programma di IA che legge una recensione, e ne calcola il voto più probabile associato. L’obiettivo è quindi tentare dal linguaggio naturale di classificare la recensione.

Per farlo, il classificatore bayesiano scompone la recensione in token, e poi deve calcolare, a partire da un insieme di token la classe di voto in base a questi. E’ una probabilità condizionata che possiamo esprimere così:

P (classe | token)

Essendo molti token, bisognerà analizzarli nel loro insieme, calcolando la probabilità come l’insieme di token usati.

Prima di tutto estraiamo la probabilità di classe di voto delle recensioni. Essa è determinata dal numero di volte in cui compare una recensione con quella classe di voto (da 1 a 5) rispetto al numero totale di recensioni. Calcolando abbiamo:

{
  '1': 0.1,
  '2': 0.166,
  '3': 0.4,
  '4': 0.233,
  '5': 0.1
}

Si passa poi all’estrazione di un set di tutti token, parole chiave, dalle recensioni. Ci serve per sapere il numero totale di token usati (senza doppioni).

{
  'pessimo',
  'prodotto',
  'arrivato',
  'rotto',
  'e',
  'inutilizzabile',
...
}

A questo punto viene generato un array di frequenze per singolo token:

{
  '1': {
    pessimo: 1,
    prodotto: 1,
    arrivato: 1,
    rotto: 1,
    e: 1,
    inutilizzabile: 1,
    una: 1,
    vera: 1,
    truffa: 1,
    non: 1,
    funziona: 1,
    affatto: 1,
    pessima: 1,
    'qualità': 1,
    materiali: 1,
    scadenti: 1
  },
  '2': {
    servizio: 1,
    clienti: 1,
    inesistente: 1,
    sconsigliato: 1,
    soldi: 1,
    buttati: 1,
    si: 1,
    'è': 1,
    rotto: 1,
    al: 1,
    primo: 1,
    utilizzo: 1,
    sotto: 1,
    le: 1,
    aspettative: 1,
    funziona: 1,
    male: 1,
    non: 1,
    mi: 1,
    piace: 1,
    spedizione: 1,
    molto: 1,
    lenta: 1,
    poco: 1,
    resistente: 1,
    plasticoso: 1
  },
...
}

Perché ci servono queste statistiche? Per capirlo riprendiamo il teorema di Bayes. Esso ci dice che la probabilità di un evento A, se succede l’evento B, è uguale alla probabilità di un evento B se succede A moltiplicato per la probabilità (assoluta) dell’evento A diviso per la probabilità assoluta dell’evento B.

Ad esempio a noi serve sapere qualcosa come questo: la classe di voto 5 quanto è probabile che sia associata alla “funziona”? Questa probabilità P (V | w) è difficile da calcolare direttamente, ma con Bayes questa probabilità corrisponde ad una espressione più semplice, ovvero la probabilità che esca quella parola per una certa classe di voto P (w | V) moltiplicata per la probabilità della classe di voto stessa P(V), diviso per la probabilità della parola stessa rispetto a tutte le parole del nostro dizionario di token P(w).

P(V|W)=P(w|V)P(V)P(w)P(V | W) = \frac{P(w | V)*P(V)}{P(w)}

Sono tre valori più facilmente calcolabili. P(v) lo conosciamo già. Inoltre possiamo già dire anche che P(w) è costante per tutte le classi di voto e quindi possiamo perfino non calcolarla, perché tanto alla fine dovremo calcolare probabilità relative.

Per calcolare P(w|V) possiamo calcolare lo score di probabilità di una parola w per una determinata classe di voto V come il numero di volte in cui compare in quella classe di voto diviso la somma dei token per quella classe di voto più la somma dei token totali:

SCORE(w|V)=1+COUNT(w|V)TOTAL(w|V)+TOTAL(token)SCORE(w | V) = \frac{1+COUNT(w|V)}{TOTAL(w|V) + TOTAL(token)}

ovvero:

  • COUNT(w) è il numero di volte in cui compare la parola w nel conteggio del voto V, lo incrementiamo di 1 per dare una minima probabilità a parole mai viste
  • TOTAL(w) è il numero di parole totali scritte nelle recensioni con voto V
  • TOTAL(token) è la somma di tutti i token nel dataset (senza doppioni).

Ad esempio “funziona” nella classe di voto 3 ha uno score di 0.0129.

Naturalmente con una recensione con più parole, lo score sarà dato dal prodotto degli score fra di loro (in quanto sono eventi indipendenti). A questo punto otterremo uno score di probabilità della recensione per ciascuna classe di voto. Per ottenere la probabilità finale dobbiamo normalizzare, cioè tradurre queste probabilità assolute in percentuali (la somma deve fare cioè 1): è sufficiente dividere ogni score per la somma di tutti gli score.

Il risultato finale sarà la probabilità richiesta.

Ad esempio poniamo di voler classificare “funziona, consigliato”.

Lo SCORE finale di una recensione sarà quindi, per ogni classe di voto (2,3,4,5):

SCORE(funziona,consigliato|V)=SCORE(funziona|V)SCORE(consigliato|V)SUM(SCORES|V)SCORE(“funziona, consigliato” | V) = \frac{SCORE(“funziona”|V)*SCORE(“consigliato”|V)}{SUM(SCORES|V)}


Calcoliamoli:

{ 
 '1': 18.9, 
 '2': 16.1, 
 '3': 11.1, 
 '4': 32.7, 
 '5': 21 
}

In pratica c’è una probabilità maggiore del 50% che il punteggio sia 4 o 5.

Vediamo ora una frase “non funziona troppo complesso”. Qui Bayes ci risponde:

{ 
  '1': 23.9, 
  '2': 17.3, 
  '3': 33.3, 
  '4': 17.9, 
  '5': 7.3 
}

Qui la risposta è interessante: prevale un giudizio basso-intermedio, anche se un osservatore umano propenderebbe per un voto 1. Questo perché il classificatore ha lavorato sui token, e siccome compare il token “funziona” che è associato anche a recensioni positive. E’ un problema comune nei problemi di machine learning, che ricade nella problematica nota come bias, ovvero la IA è stata addestrata con un algoritmo che contiene delle supposizioni iniziali che non andavano fatte (ovvero valutare singole parole, e non gruppi di parole).

Qui il codice che implementa quanto scritto sopra:

const reviews= `...`; // già indicate sopra

// lista delle frasi
const list = reviews.split("\n")
  .map((row) => {
      const dataRow = row.split(";");
      return {
          sentence: dataRow[0],
          vote: dataRow[1].trim()
      }
  });
  
//console.log(list);

// conteggio del numero di voti
const voteCount = list.reduce((acc, element) => {
    acc[element.vote] = acc[element.vote] ?? 0;
    acc[element.vote]++;
    return acc;
}, {});

//console.log(voteCount);

// conteggio di probabilità di ciascuna classe di voto
const voteProb = {};
Object.keys(voteCount).forEach((key) => {
    voteProb[key] = voteCount[key]/list.length;
})

console.log(voteProb);

// elenco dei token e del numero di volte che compare per voto
const tokens = new Set();
const tokenCountPerVote = {};
Object.keys(voteCount).forEach((key) => {
    tokenCountPerVote[key] = {};
});
list.forEach((row) => {
    const words = row.sentence.split(" ");
    words.forEach((word) => {
        const token = word.replace(",", "").replace(", ", "").replace(".", "").toLowerCase();
        tokens.add(token);
        tokenCountPerVote[row.vote][token] = tokenCountPerVote[row.vote][token] ?? 0;
        tokenCountPerVote[row.vote][token]++;
    });
    
});
console.log(tokens);
console.log(tokenCountPerVote);

// calcola lo score della singola parola
const calcScoreWV = (token) => {
    const result = {};
    Object.keys(voteCount).forEach((key) => {
        const countwV = isNaN(tokenCountPerVote[key][token]) ? 0 : tokenCountPerVote[key][token];
        const totalwV = Object.keys(tokenCountPerVote[key]).length;
        score = (1+countwV) / (totalwV + tokens.size);
        result[key] = score;
    });
    return result;
}

// calcola lo score di una frase intera
const calcScoreSentence = (sentence) => {
    const result = {};
    const words = sentence.split(" ");
    let sum;
    words.forEach((word) => {
        const token = word.replace(",", "").replace(", ", "").replace(".", "").toLowerCase();
        const scores = calcScoreWV(token);
        sum = 0;
        Object.keys(scores).forEach((key) => {
            result[key] = result[key] ?? 1;
            result[key] *= scores[key];
            sum += result[key];
        });
    });
    Object.keys(result).forEach((key) => {
        result[key] = parseInt(result[key] / sum * 1000)/10;
    })
    
    return result;
}

const totalScores = calcScoreSentence("non funziona troppo complesso");
console.log(totalScores);