Come dovrei rappresentare i calcoli associabili componibili da applicare ripetutamente alle unità di dati?

4

Spoiler

La mia domanda è la seguente: ci sono schemi di progettazione per rappresentare le funzioni concatenabili che sono per il problema descritto di seguito?

Declarazione del processo ad alto livello

Attualmente sto creando un server di elaborazione delle immagini, la cui funzionalità è esposta tramite un'API Web.

Comportamento del cliente

Prima i client eseguono l'autenticazione, quindi richiedono che una o più analisi vengano eseguite dal server. Una volta che un client si è autenticato e ha richiesto il proprio stack di analisi, trasmette i suoi dati video al server di analisi.

Comportamento del server

Sul lato server, la classe Stream viene utilizzata per rappresentare il flusso di dati. È l'oggetto di prima classe su cui è costruita la pipeline e può essere pensato come una sorta di coda FIFO ordinata. I frame vengono spinti, in ordine di arrivo, sull'istanza Stream , e ogni stream può opzionalmente mappare una funzione sui suoi dati. I risultati di una funzione mappata sono proiettati su una nuova % istanza diStream.

Ad esempio:

# input_stream is asynchronously updated with incoming frames

stream = input_stream.map(do_sobel_filter)  # apply Sobel filter to each incoming frame
stream2 = stream.window(fn, 10)  # apply function to a sliding window of 10 frames

Un importante nota a margine è che il nostro uso di oggetti Stream ci consente di applicare funzioni a singoli frame oa gruppi di frame. Questo è utile in quanto alcune analisi richiedono il contesto.

Requisiti e amp; Vincoli

Il testo in grassetto sopra è particolarmente importante; Un determinato cliente sarà autorizzato solo a eseguire un sottoinsieme delle analisi di immagine offerte dal nostro sistema. Ciò a sua volta rende fondamentale la possibilità di comporre dinamicamente la pipeline di elaborazione delle immagini: non vogliamo eseguire calcoli costosi solo per eliminare il risultato.

Un'ulteriore complicazione della questione è il fatto che quasi tutte le analisi che offriamo hanno alcuni prerequisiti, cioè l'operazione O i può dipendere dal risultato dell'operazione < em> O in . Il nostro sistema ha bisogno di risolvere queste dipendenze ed eseguire ogni operazione esattamente una volta.

Infine, il concatenamento / composizione delle operazioni deve essere associativo. Questo perché vorremmo essere in grado di comporre calcoli di livello superiore riutilizzabili da calcoli atomici di livello inferiore. Ad esempio, considera la sequenza di operazioni x -> y -> z . Vorremmo essere in grado di assegnare la catena ordinata x->y->z a una variabile, quindi CMP1 = x->y->z . Allo stesso modo, CMP2 è il risultato delle operazioni di concatenamento a->b->c . In definitiva, vorremmo essere in grado di fare l'equivalente di CMP3 = CMP1 -> CMP2 , e farlo essere strettamente equivalente a CMP3 = x->y->z->a->b->c (associatività).

Per riassumere, ecco i requisiti per gli oggetti della funzione di analisi delle immagini:

  1. Le funzioni devono essere composeable , il che significa che dovrebbe essere possibile manipolare un oggetto di prima classe che rappresenta una serie di sub-computazioni.
  2. Idealmente, le funzioni dovrebbero risolvere le dipendenze , nel senso che fare A -> B dovrebbe valutare implicitamente a A -> X -> C se C dipende da A e X . Questo può essere degno della sua stessa domanda, e quindi dovrebbe essere trattato come un requisito morbido.
  3. Il concatenamento delle operazioni dovrebbe essere associativo e le funzioni composte dovrebbero essere anche concatenabili . L'idea è di avere uno schema riutilizzabile per rappresentare pipeline sempre più complesse.

Esistono schemi ben collaudati per ottenere un risultato del genere?

Consente di modificare:

1) In risposta alle domande di @ Giorgio, la mia (notoriamente ambigua) notazione serve a distinguere tra l'ordine temporale di frame e l'ordine di operazioni . Di seguito è riportato un esempio di come un'operazione può dipendere dalle sue controparti precedenti:

FindFace -> FindEyes -> SegmentEye -> MeasurePupilDiameter

Qui possiamo vedere che ogni funzione dopo FindFace dipende dal risultato cumulativo delle funzioni precedenti. Ora considera:

FindFace -> FindEyes -> SegmentEye -> GetIrisColorHistogram

Questo secondo esempio dimostra come spesso vi sia un nucleo comune di operazioni che deve essere eseguito attraverso le analisi. Quando compongo le operazioni, mi piacerebbe eseguire i passaggi 1 - 3 una sola volta , quindi applicare MeasurePupilDiameter e GetIrisColorHistogram da lì.

Intuitivamente, sembra che il modo giusto per farlo sarebbe quello di assegnare un dizionario al mio oggetto Frame e riempirlo con le caratteristiche mentre sono calcolate - un modello memoize, se vuoi. Da lì, tuttavia, non è chiaro come procedere per l'astrazione di tutto il codice di riferimento relativo a (a) controllare se una funzione di prerequisito è già stata calcolata e (b) chiamare automaticamente la funzione appropriata se non lo è.

    
posta blz 14.12.2014 - 15:07
fonte

1 risposta

3

Il tuo problema ha sorprendenti analogie con la programmazione funzionale: Monade, Funzionalità e funzioni di composizione. I requisiti di composabilità stabiliscono essenzialmente che ciascuna operazione deve essere una funzione che accetta uno stream e restituisce uno stream:

operation : Stream -> Stream

La maggior parte delle funzioni non saranno espresse in termini di interi flussi ma piuttosto di singoli fotogrammi o finestre di fotogrammi. Hai correttamente notato che puoi sollevare queste operazioni frame-wise per eseguire operazioni di streaming con funzioni come map .

class Operation:
    def __init__(self, function, dependencies):
        self.dependencies = dependencies
        self.function = function
    def __call__(self, stream):
        return self.function(stream)

def lift_frame_to_stream(frame_operation, dependencies):
    def stream_operation(stream):
        for frame in stream:
            yield frame_operation(frame)
    return Operation(stream_operation, dependencies)

Questa componibilità è in contrasto con il requisito che le operazioni possano avere dipendenze. Possiamo risolvere questo problema gestendo le dipendenze al di fuori del sistema di tipi. Dato un elenco semplice di operazioni op1, op2, ..., opn ognuna delle quali ha dipendenze, allora possiamo scorrere l'elenco per determinare se queste dipendenze sono soddisfatte. Nel caso più semplice, ciò avviene mantenendo un insieme di tutte le operazioni incontrate fino a quel punto:

# unsatisfied_dependencies: Iterable[Operation] -> List[(Operation, List[Operation])]
def unsatisfied_dependencies(operations):
    seen = set()
    needed = []
    for op in operations:
        op_needs = []
        for dep in op.dependencies:
            if dep not in seen:
                op_needs.append(dep)
        if op_needs:
            needed.append((op, op_needs))
        seen.add(op)
    return needed

Sarei diffidente nei confronti delle dipendenze che soddisfano automaticamente. Se questo fosse fatto, dovresti iterare le operazioni per indice. Se viene rilevata una dipendenza non soddisfatta, anziché aggiungerla all'elenco delle operazioni necessarie, inserire la dipendenza prima dell'operazione corrente, quindi eseguire il backup di un elemento e continuare a controllare le dipendenze a partire dall'operazione appena inserita.

Il problema con l'inserimento automatico delle dipendenze è che l'ordine delle operazioni potrebbe avere importanza e questa strategia di risoluzione non tiene conto di ciò. Penso che sarebbe meglio rendere gli utenti consapevoli delle dipendenze e richiedere che vengano soddisfatti prima dell'inizio dell'elaborazione.

L'elaborazione del flusso è un compito abbastanza semplice di prendere una pipeline con tutte le dipendenze soddisfatte e applicando le operazioni. Supponendo che tutte le operazioni siano pigre (cioè non consumano immediatamente il flusso ma calcolano solo ogni frame richiesto, facilmente implementabile usando i generatori di Python), il flusso composto dovrebbe essere sottoposto a valutazione forzata.

stream = input_stream
for op in verified_pipeline:
    stream = op(stream)
# force the stream

La pipeline non può essere composta in nessun altro modo; "Associatività" è un termine improprio poiché le operazioni di flusso sono unate e non operatori binari. Tuttavia, la tua esigenza di "blocchi" di operazioni più grandi che contengono operazioni secondarie è comprensibile e facile da implementare - il Pattern composito orientato agli oggetti ha senso qui. Le dipendenze del composito sono tutte le dipendenze delle operazioni secondarie che non sono soddisfatte all'interno di quel composito.

In realtà, una pipeline di operazioni è equivalente a una singola operazione, quindi possiamo usare una pipeline come composito.

class Pipeline:
    def __init__(self, childs, extra_dependencies=[]):
        self.childs = childs
        deps = []
        for (op, dependencies) in unsatisfied_dependencies(childs):
            deps.extend(dependencies)
        deps.extend(extra_dependencies)
        self.dependencies = deps

    def __call__(self, stream):
        for op in self.childs:
            stream = op(stream)
        return stream

Più importante delle specificità delle operazioni di composizione è il modo in cui rappresenterai ciascun fotogramma. Presumibilmente, ogni frame sarà costituito principalmente da dati immagine. Tuttavia, alcune operazioni potrebbero voler aggiungere metadati (ad esempio un'operazione potrebbe calcolare la distribuzione del colore di un frame). Sarebbe quindi molto sensato che ogni frame contenga un dicitionary che può essere riempito con dati abbastanza non strutturati. Dal momento che vogliamo che le operazioni siano liberamente componibili, non sarebbe possibile definire più classi di frame come output di ogni operazione, poiché in questo modo i metadati delle operazioni precedenti verrebbero scartati.

Ma questi metadati permanenti hanno un problema importante: possono essere invalidati dalle operazioni successive. Per esempio. un profilo colore verrebbe invalidato da un'operazione che esegue una correzione gamma sui dati dell'immagine. Una possibilità per riconciliare questo sarebbe tracciare per ogni operazione quali operazioni invalida. Quando si controllano le dipendenze non soddisfatte, tali operazioni invalidate verrebbero quindi rimosse dallo stato seen . Tuttavia, questo richiede di fare una contabilità estesa per ogni operazione. L'aggiunta di una nuova operazione comporta la ricerca di tutte le operazioni esistenti per scoprire potenziali incompatibilità.

Per quanto riguarda la rappresentazione di prima classe delle operazioni, penso che rappresentare ogni operazione come un oggetto callable con alcuni metadati come le dipendenze su di esso abbia più senso. Come mostrato sopra, è facile verificare le dipendenze e creare blocchi componibili al di fuori di questo. È anche abbastanza semplice trasformare l'input dell'utente in una pipeline. Supponendo di avere un elenco di stringhe che fanno riferimento ad alcune operazioni, puoi usare un dizionario per mappare queste stringhe alle operazioni:

operations_map = { ... }

chosen_operations = []
for name in input_list:
    chosen_operations.append(operations_map[name]) # TODO error handling
pipeline = Pipeline(chosen_operations)

if pipeline.dependencies:
    # oops

result_stream = pipeline(input_stream)

Tuttavia, potrebbe essere utile che ogni operazione contenga un campo name per facilitare la creazione di messaggi di errore significativi ecc.

    
risposta data 14.12.2014 - 17:02
fonte

Leggi altre domande sui tag