Algoritmo efficiente per dedurre il tipo di oggetto in modo dinamico in base ai membri

4

Sto progettando un DSL (in clojure, in particolare, sebbene questa domanda sia più generale di quella) in cui le "entità" sono tracciate come hash / mappe immutabili e dove l'appartenenza a "concetto" di un'entità è determinata dinamicamente da le chiavi e i valori delle mappe. Un esempio di come questo apparirebbe:

(defconcept AnnotatedEntity :contains [annotations meta-data])
(defconcept PointCloud      :contains vertices)
(defconcept Polygon         :contains [vertices edges])
(defconcept Triangle
  :isa       Polygon
  :condition (= (count vertices) 3))
(defconcept Rectangle
  :isa       Polygon
  :condition (= (count vertices) 4))
(concepts {:vertices [[0 0], [0 1], [1 0]] :edges [[0 1] [1 2] [2 0]]})

#{PointCloud Polygon Triangle}

(concepts {:vertices [[0 0] [0 1] [1 1] [1 0]]
           :annotations {:name "unit-rectangle point-cloud"}
           :meta-data {}})

#{PointCloud AnnotatedEntity}

Poiché il :condition di un concetto è solo una funzione che viene eseguita sulla mappa, possiamo ignorare questa parte del test (e quindi eventuali requisiti sui valori) per ora; il mio interesse è nell'individuare in modo efficiente tutti i possibili concetti in cui una determinata mappa potrebbe mantenere l'appartenenza in base alle sue chiavi. Ad esempio, semplifichiamo quanto sopra solo per questo:

(defconcept AnnotatedEntity :contains [annotations meta-data])
(defconcept PointCloud      :contains [vertices])
(defconcept Polygon         :contains [vertices edges])
(defconcept Triangle        :contains [vertices edges])
(defconcept Rectangle       :contains [vertices edges])
(possible-concepts {:vertices [[0 0], [0 1], [1 0]]
                    :edges [[0 1] [1 2] [2 0]]})

#{PointCloud Polygon Triangle Rectangle}

(possible-concepts {:vertices [[0 0] [0 1] [1 1] [1 0]]
                    :annotations {:name "unit-rectangle point-cloud"}
                    :meta-data {}})

#{PointCloud AnnotatedEntity}

Attualmente, eseguo questo con la forza bruta, che è molto efficiente per gli esempi di giocattoli, ma che non è abbastanza efficiente per un pezzo centrale di una libreria che a volte viene chiamato molte volte al millisecondo con mappe arbitrariamente grandi e arbitrariamente molte concetti.

Un semplice esempio di ottimizzazione che migliora questo processo è di tenere traccia di una mappa di tutte le chiavi incluse in qualsiasi concetto in modo tale che i valori siano insiemi di concetti contenenti la chiave; per l'esempio precedente:

(def concepts-of-keys {:annotations #{AnnotatedEntity}
                       :meta-data   #{AnnotatedEntity}
                       :vertices    #{PointCloud Polygon Triangle Rectangle}
                       :edges       #{Polygon Triangle Rectangle}})
(defn possible-concepts [ent]
  (set (filter #(every? (partial contains? ent) (.getKeys %))
               (apply clojure.set/union (map concepts-of-keys (keys ent))))))

Questo è un po 'meglio del semplice test su ogni concetto possibile, ma solo se i concetti hanno chiavi abbastanza uniche. Se ogni concetto dipende da una singola chiave come :meta-data o :id o qualcosa di molto comune (o tutti i concetti ereditano da un singolo concetto di base), allora questo è fondamentalmente tanto inefficiente quanto il controllo di ogni concetto.

Di conseguenza non credo che nessuna di queste sia la soluzione migliore. Ad esempio, si potrebbe immaginare di costruire un albero decisionale (che verrebbe, in pratica, avvolto in un ref o in un atomo e aggiornato come i concetti sono definiti):

(def concept-decision-tree
  "
  Each element of the decision tree is [test-keys if-every if-not-every] where
  test-keys is a key or set of keys that should be tested for membership in a
  given entity; if the entity contains all these keys, descend along the
  if-every element and along the if-not-every otherwise. These values are sets
  if the membership of the entity has been deduced by the most recent
  decision.
  "
  [#{:annotations :meta-data} ;; if the entity has all of these elements...
   ;; descend into this subtree
   [:vertices [:edges #{AnnotatedEntity PointCloud Polygon Triangle Rectangle}
                      #{AnnotatedEntity PointCloud}]
              #{AnnotatedEntity}]
   ;; otherwise, descend into this subtree...
   [:vertices [:edges #{PointCloud Polygon Triangle Rectangle}
                      #{PointCloud}]
              #{}]])
(defn possible-concepts [ent]
  (loop [[test-keys if-every if-not-every] concept-decision-tree]
    (if (every? (partial contains? ent)
                (if (set? test-keys) test-keys [test-keys]))
        (if (set? if-every)     if-every     (recur if-every))
        (if (set? if-not-every) if-not-every (recur if-not-every)))))

Questo è ancora meglio, supponendo che possiamo mantenere l'albero delle decisioni; quello che mi chiedo è qual è il modo migliore per farlo o, alternativamente, quale alternativa è ugualmente / più efficiente? Non è ovvio per me come si possa bilanciare un tale albero decisionale con l'aggiunta di concetti.

La mia intuizione è che questo problema ha una teoria che è ampiamente capita e che io non conosco il nome formale del problema; del resto, dovrei menzionare il fatto che sono intellettualmente interessato alla risposta più ottimale e non alla risposta che può essere scritta più velocemente o che un project manager apprezzerebbe di più.

In altre parole, quello che sto cercando in una risposta:

  • Una soluzione al problema di aggiornamento dell'albero decisionale, concettualmente con sufficienti dettagli da implementare, o una semplice implementazione di esempio
  • Una soluzione alternativa a questo problema che è ugualmente / similmente / più efficiente
  • Un puntatore a una letteratura scientifica su questo argomento
  • Un nome formale / standard per questo problema
  • Esempi di librerie o linguaggi che implementano questo tipo di digitazione dinamicamente

Cosa non sto cercando in una risposta:

  • una soluzione brute-force (ne ho già una!)
  • "< qualcosa di simile > l'ottimizzazione prematura è la radice di tutto il male, < qualcosa qualcosa > quindi solo forza bruta."

Un'altra cosa che dovrei menzionare: sebbene io abbia scritto questi esempi usando mappe standard, non mi oppongo a una soluzione che coinvolge una sottoclasse speciale della persistente classe di mappe che tiene traccia delle sue chiavi e annota le mappe appena create con le loro tipi come sono creati. Ad esempio, un tipo speciale che imita le mappe ma sa che quando incontra (dissoc ent-map :vertices) la nuova mappa dovrebbe avere concetti come PointCloud, Polygon, ecc. Rimossi dal suo elenco di concetti.

Grazie in anticipo!

    
posta user16054 06.06.2017 - 19:39
fonte

1 risposta

2

Potresti dare un'occhiata a un RETE algoritmo.

Usiamo una definizione di lavoro della regola come qualcosa che accoppia un'espressione di corrispondenza con un'espressione di azione, e dove il successo del primo innesca il secondo. L'espressione match è di solito una congiunzione di sottoespressioni, cioè (ha attributo x AND ha attributo y AND ha attributo z), sebbene possa essere supportata anche la disgiunzione.

Sebbene trovassi la descrizione degli algoritmi RETE un po 'noiosa, fondamentalmente, ciò che possono fare è raccogliere insieme un po' di regole, determinare le sotto espressioni comuni tra le espressioni di corrispondenza dell'intero insieme di regole, e cercare qualsiasi cosa regola le corrispondenze più o meno simultaneamente su tutte le regole, su alcune serie di input, quindi esegue le parti di corrispondenza comuni solo una volta e aumentando l'efficienza.

Essenzialmente, un motore di questo tipo crea o precompute lo stato interno riguardo le sottoespressioni delle condizioni di corrispondenza su tutte le regole, mantenendo solo una copia di ogni sottoespressione di corrispondenza comune, mentre organizza la struttura di memoria in modo tale da sapere quando una corrispondenza completa per un originale la regola indipendente è soddisfatta.

Il motore esamina le informazioni su più input per verificare se esiste una corrispondenza per qualsiasi regola, monitorando lo stato di corrispondenza di una determinata regola nella struttura complessiva dei dati combinata.

Essenzialmente, è come l'ottimizzazione del compilatore eliminazione della sottoespressione comune anche se per le espressioni di corrispondenza delle regole invece delle espressioni del codice sorgente del programma .

C'è anche molta roba in RETE per aggiustare il grafico di ricerca se i criteri cambiano dinamicamente, cosa che può accadere in varie circostanze, forse uno dei quali è se l'azione innescata per una regola modifica l'input come potrebbe essere il caso in un motore di ragionamento in cui una regola attiva la creazione di nuove informazioni da considerare come input aggiuntivo anziché attivare un'azione completamente esterna.

I concetti RETE o RETE potrebbero applicarsi alla tua situazione nel modo seguente:

Precarica tutte le sottoespressioni di corrispondenza comuni da tutti i tipi che desideri riconoscere, in cui le espressioni di corrispondenza sono essenzialmente l'insieme di attributi da abbinare che risulteranno nel riconoscimento di un determinato tipo di entità.

Alimenta il motore caricato con attributi, ciascuno come un unico input, da un oggetto esistente, e il motore cercherà fondamentalmente di abbinare tutte le possibili regole più o meno simultaneamente.

Non dovrebbe importare l'ordine degli attributi che alimentano il motore, anche se posso immaginare il potenziale teorico per qualche ulteriore ottimizzazione se fossero forniti in qualche ordine ordinato quando le sottoespressioni della partita hanno sempre una forma particolare.

    
risposta data 07.06.2017 - 00:45
fonte

Leggi altre domande sui tag