Unit testing Markov codice catena

1

Quali sono i modi migliori per unità di codice di test che restituisce sequenze casuali che soddisfano condizioni specifiche, come le catene Markov?

Siamo specifici. Ci sono due cose naturali da testare:

  • Che lo stato iniziale restituito in ogni catena segue le probabilità specificate dall'utente, in modo che chain[0] == i con probabilità p_initial[i] .

  • Che le transizioni tra stati siano corrette, ovvero se chain[k] == i , allora la probabilità di chain[k+1] == j è data da p_transition[i, j] .

Ora, posso pensare a due modi per farlo.

  1. Verifica che il codice faccia quello che voglio che faccia. Genera un gran numero di sequenze (ad esempio, 10000) e verifica che le frequenze per lo stato iniziale siano approssimative a p_initial . Prossimo test che le frequenze delle transizioni allo stato j dallo stato i seguano p_transition .

La difficoltà con questo approccio è che qualsiasi campione finito mostrerà fluttuazioni dalle probabilità prescritte. Devo quindi impostare un livello accettabile per queste fluttuazioni. Ma questo significa che a) il test potrebbe perdere gli errori effettivi nel codice se cambiano solo le frequenze un po 'e b) dopo un refactoring potrei ottenere falsi fallimenti a causa di una particolare catena che cade al di fuori del livello di fluttuazione accettato per puro caso . (Suppongo che sto usando un seme fisso per il generatore di numeri casuali in modo che almeno senza refactoring i test debbano essere riproducibili.) Peggio, più mi rilassano i criteri di accettazione per mitigare il problema b), più rischio di cadere per a).

Inoltre, sto solo testando un piccolo sottoinsieme dei requisiti per il modello (voglio anche che le transizioni siano indipendenti, per esempio). E rispetto a quanto sia semplice l'implementazione di questo codice (dato un generatore di numeri casuali, vedi sotto), non sembra che ne valga la pena aggiungere un po 'di dettagli nel codice di test.

  1. L'alternativa è supporre che il generatore di numeri casuali sia stato testato dai suoi creatori e funzioni bene, e basta testare che il mio codice lo chiami nel modo giusto. Quindi posso prendere in giro l'RNG e controllare che il mio codice lo chiami con gli argomenti giusti.

Il problema con questo approccio è che rende il test strongmente dipendente dall'implementazione. Per esempio, potrei scegliere lo stato iniziale usando qualcosa come numpy.random.choice in Python, oppure potrei semplicemente generare un numero casuale compreso tra 0 e 1 ( numpy.random.random ) e implementare la mia logica per scegliere lo stato iniziale basato su quello. Anche all'interno di ciascuna di queste scelte, potrei scegliere di ordinare gli stati in qualsiasi modo. I test devono sapere cosa sta facendo il codice e questo rende i test fragili per il refactoring.

Quindi questo è il mio problema: sembra sbagliato accoppiare così tanto il test all'implementazione. Ma sembra anche sbagliato eseguire molti test della mia funzione che sono in effetti test del RNG (e neanche test particolarmente severi).

C'è un modo diverso e migliore per farlo che mi permette di testare il codice che ho scritto facendo affidamento sui writer RNG per avere controllato il loro codice?

Per definizione, ecco un codice (Python) che implementa un semplice generatore di catene Markov

import numpy as np

class PlainMarkov(object):
    def __init__(self, p_initial, p_transition, rng=None):
        """ p_initial = sequence of initial-state probabilities
         p_transition = sequence of sequences for transition probabilities
        rng = random number generator
        """
        self.p_initial = np.asarray(p_initial)
        self.p_transition = np.asarray(p_transition)
        self.rng = rng is rng is not None else np.random.random.__self__

    def run(self):
        """ Draw one chain from the Markov distribution. """
        n_states = len(self.p_initial)
        state = self.rng.choice(n_states, p=self.p_initial)
        chain = []
        while state_numeric != 0:
            chain.append(state)
            state = self.rng.choice(n_states, p=self.p_transition[state])

        return chain
    
posta Legendre17 06.08.2018 - 19:18
fonte

4 risposte

1

The alternative is to assume that the random number generator was tested by its creators and works fine, and just test that my code calls it in the right way. So I can mock the RNG and check that my code calls it with the right arguments.

Questa è, ovviamente, la risposta corretta.

A meno che tu non sia realmente nel business della progettazione del tuo RNG, vuoi considerare la casualità come qualcosa da fornire al tuo sistema come input, e quindi i tuoi test possono scegliere quali input fornire sul canale "casuale".

Even within each of these choices, I could choose to order the states in any way. The tests must know what the code is doing, and that makes the tests fragile to refactoring.

Ci sono due risposte a questo

Primo: se il comportamento osservabile del sistema viene alterato da una modifica all'implementazione, tale modifica è non un refactoring.

Secondo: non devi superare i tuoi test. Se puoi descrivere i vincoli che non sono soddisfatti da output errati, scrivi i tuoi test per verificare l'output rispetto ai vincoli, piuttosto che preoccuparti che l'output corrisponda a qualche valore precedentemente calcolato.

Si tratta di test basati su proprietà e test basati su esempi in poche parole. Raccomando il di Scott Wlaschin Un'introduzione ai test basati su proprietà se non hai già familiarità con l'argomento.

Questa discussione sul problema degli scacchi delle fate di Fischer descrive alcuni dei le preoccupazioni, anche se su un tipo più semplice di problema con una gamma più limitata di output.

    
risposta data 07.08.2018 - 03:29
fonte
6

Un unittest dovrebbe essere ciò che il nome implica, un test di un'unità specifica.

In genere ciò significa che prendi in giro tutte le dipendenze. Quindi, se hai creato un generatore di numeri casuali falso che ha restituito un numero fisso di numeri (1, 7, 4, 3, 9, 2 per esempio e sempre quella sequenza), allora potresti testare che generi una catena Markov molto specifica basata su quel . E la tua funzione di generazione della catena markov dovrebbe essere quella che fornisce una catena molto specifica basata su una sequenza di numeri casuali. Anche se rifattori la tua funzione, ti aspetteresti che la stessa sequenza di numeri casuali come input dia gli stessi risultati.

Non è possibile mostrare con un unittest che la funzione genererà una specifica distribuzione di probabilità. Stai solo cercando di dimostrare che se sai esattamente quali numeri sono generati dal tuo generatore di numeri casuali, il tuo algoritmo fornisce quell'output atteso. Se necessario, puoi creare un secondo test basato su un diverso set di numeri casuali generati e mostrare che anche uno funziona. Se passano entrambi questi test, non sai che la tua funzione funzionerà sempre, sai solo che è abbastanza probabile che funzioni in modo che valga la pena di fare test più complessi / vedere cosa succede in produzione.

Se si può anche fare un qualche tipo di test di integrazione automatico / test di carico / analisi statistica su un numero elevato di esecuzioni. ma non è un unittest. E con questi test hai a che fare con scenari statistici improbabili (come generare 20 volte 0 di fila con il tuo generatore di numeri casuali).

Pensa alla tua funzione come una funzione di punteggio del blackjack. La carta che viene pescata è casuale, ma se ho 8 e una regina il mio punteggio dovrebbe sempre essere 18 (e sono vivo, ecc.). Non vuoi testare il comportamento probabilistico che vuoi testare risultati molto deterministici dopo che la tua funzione random / probabilità è stata effettivamente eseguita

    
risposta data 06.08.2018 - 19:46
fonte
2

Forse mi manca qualcosa, ma sei consapevole che puoi usare random.seed () ?

Usalo per impostare un seme specifico. Con la mano convalida la risposta. E memorizza / convalida che ottieni quegli stati dopo un'esecuzione con quel valore di input seminato.

Quindi forse prova un altro seme, ed esegui, e convalida (a mano - qualche altro meccanismo) quelle risposte. E conferma che anche il tuo valutatore della catena markov ottiene quelle risposte.

Scusa se mi sono perso qualcosa, ma spero che abbia aiutato ...

    
risposta data 06.08.2018 - 21:52
fonte
1

Penso che in questo caso, il massimo che puoi testare a livello di interfaccia è se la catena risultante è una catena di Markov valida, ad es. se l'elemento iniziale proviene dall'elenco p_initial e se ogni elemento successivo è una transizione valida dall'elemento precedente. E per questo, il test non ha bisogno di sapere nulla sull'implementazione.

Penso che avere una prova ulteriore del fatto che i risultati siano statisticamente corretti potrebbe essere una buona idea, ma non chiamerei quel test un "test unitario".

Puoi anche avere test unitari di implementazioni specifiche, che si basano sulla conoscenza di tali implementazioni. Se questa è una cosa utile da avere dipende dalla situazione. In questo caso, non lo considererei, ma YMMV.

    
risposta data 07.08.2018 - 02:07
fonte

Leggi altre domande sui tag