Come implementare l'hash del float con l'uguaglianza approssimativa


15

Diciamo che abbiamo la seguente classe Python (il problema esiste in Java lo stesso con equalse hashCode)

class Temperature:
    def __init__(self, degrees):
        self.degrees = degrees

dove si degreestrova la temperatura in Kelvin come galleggiante. Ora, vorrei implementare test di uguaglianza e hashing per Temperaturein questo modo

  • confronta i galleggianti con una differenza epsilon invece del test di uguaglianza diretta,
  • e onora il contratto che a == bimplica hash(a) == hash(b).
def __eq__(self, other):
    return abs(self.degrees - other.degrees) < EPSILON

def __hash__(self):
    return # What goes here?

La documentazione di Python parla un po 'di numeri di hashing per garantire che hash(2) == hash(2.0)questo non sia esattamente lo stesso problema.

Sono anche sulla buona strada? E se è così, qual è il modo standard per implementare l'hash in questa situazione?

Aggiornamento : ora capisco che questo tipo di test di uguaglianza per i float elimina la transitività di ==e equals. Ma in che modo ciò va di pari passo con la "conoscenza comune" che i galleggianti non devono essere confrontati direttamente? Se si implementa un operatore di uguaglianza confrontando i float, gli strumenti di analisi statica si lamenteranno. Hanno ragione a farlo?


9
perché la domanda ha il tag Java?
Laiv

8
Informazioni sul tuo aggiornamento: direi che l'hashing float è generalmente una cosa discutibile. Cerca di evitare di usare i float come chiavi o come elementi impostati.
J. Fabian Meier,

6
@Neil: Allo stesso tempo, l'arrotondamento non suona come numeri interi? Con questo voglio dire: se puoi arrotondare, per esempio, i millesimi di grado, allora potresti semplicemente usare una rappresentazione a punto fisso - un numero intero che esprime la temperatura in millesimi di grado. Per facilità d'uso, potresti avere un getter / setter che converte in modo trasparente da / in float se desideri ...
Matthieu M.

4
I Kelvin non sono più gradi. Anche i gradi sono ambigui. Perché non chiamarlo kelvin?
Solomon Ucko,

5
Python ha un supporto a punto fisso più o meno eccellente , forse è qualcosa che fa per te.
Jonas Schäfer,

Risposte:


41

implementare test di uguaglianza e hashing per la temperatura in modo da confrontare i galleggianti fino a una differenza epsilon invece di test di uguaglianza diretta,

L'uguaglianza fuzzy viola i requisiti che Java pone sul equalsmetodo, vale a dire la transitività , cioè se x == ye y == z, quindi x == z. Ma se fai un'eguaglianza sfocata con, per esempio, un epsilon di 0,1, allora 0.1 == 0.2e 0.2 == 0.3, ma 0.1 == 0.3non regge.

Mentre Python non documenta tale requisito, le implicazioni di avere un'uguaglianza non transitiva la rendono una pessima idea; il ragionamento su tali tipi induce mal di testa.

Quindi consiglio vivamente di non farlo.

O fornire l'uguaglianza esatta e basare il proprio hash su quello in modo ovvio e fornire un metodo separato per eseguire la corrispondenza fuzzy, oppure seguire l'approccio di classe di equivalenza suggerito da Kain. Anche se in quest'ultimo caso, ti consiglio di fissare il tuo valore a un membro rappresentativo della classe di equivalenza nel costruttore, e poi vai con la semplice e esatta uguaglianza e hashing per il resto; è molto più facile ragionare sui tipi in questo modo.

(Ma se lo fai, potresti anche usare una rappresentazione in virgola fissa invece di virgola mobile, cioè puoi usare un numero intero per contare i millesimi di grado o qualsiasi precisione tu abbia bisogno.)


2
pensieri interessanti. Quindi accumulando milioni di epsilon e con transitività puoi concludere che qualsiasi cosa è uguale a qualsiasi altra cosa :-) Ma questo vincolo matematico riconosce le basi discrete dei punti fluttuanti, che in molti casi sono approssimazioni del numero che intendono rappresentare?
Christophe,

@Christophe Domanda interessante. Se ci pensate, vedrete che questo approccio renderà una singola grande classe di equivalenza da float la cui risoluzione è maggiore di epsilon (ovviamente è centrata su 0) e lascerà gli altri float nella propria classe ciascuno. Ma non è questo il punto, il vero problema è che se si conclude che 2 numeri sono uguali dipende dal fatto che ce ne sia un terzo a confronto e dall'ordine in cui ciò viene fatto.
Ordous,

Affrontando la modifica di @ OP, aggiungerei che l'erroneità del virgola mobile ==dovrebbe "infettare" i ==tipi che li contengono. Cioè, se seguono il tuo consiglio di fornire una parità esatta, allora il loro strumento di analisi statica dovrebbe essere ulteriormente configurato per avvisare quando viene usata la parità Temperature. È l'unica cosa che puoi fare, davvero.
HTNW,

@HTNW: Sarebbe troppo semplice. Una classe di rapporto potrebbe avere un float approximationcampo a cui non partecipa ==. Inoltre, lo strumento di analisi statica fornirà già un avviso all'interno ==dell'implementazione delle classi quando uno dei membri confrontati è un floattipo.
MSalters il

@MSalters? Presumibilmente, strumenti di analisi statica sufficientemente configurabili possono fare ciò che ho suggerito bene. Se una classe ha un floatcampo a cui non partecipa ==, non configurare il tuo strumento per avvisare ==su quella classe. Se la classe lo fa, presumibilmente contrassegnando la classe ==come "troppo esatta", lo strumento ignorerà quel tipo di errore all'interno dell'implementazione. Ad esempio in Java, se @Deprecated void foo(), allora void bar() { foo(); }è un avvertimento, ma @Deprecated void bar() { foo(); }non lo è. Forse molti strumenti non lo supportano, ma alcuni potrebbero.
HTNW

16

In bocca al lupo

Non riuscirai a raggiungerlo, senza essere stupido con gli hash o sacrificare l'Epsilon.

Esempio:

Supponiamo che ogni punto abbia un hash sul proprio valore di hash univoco.

Poiché i numeri in virgola mobile sono sequenziali, ci saranno fino a k numeri prima di un dato valore in virgola mobile e fino a k numeri dopo un dato valore in virgola mobile che si trovano all'interno di alcuni epsilon di quel dato punto.

  1. Per ogni due punti all'interno di epsilon l'uno dell'altro che non condividono lo stesso valore di hash.

    • Regola lo schema di hashing in modo che questi due punti abbiano lo stesso valore.
  2. Inducendo per tutte queste coppie l'intera sequenza di numeri in virgola mobile collasserà verso un singolo valore.

Ci sono alcuni casi in cui ciò non sarà vero:

  • Infinito positivo / negativo
  • NaN
  • Alcuni intervalli non normalizzati che potrebbero non essere collegabili all'intervallo principale per un determinato epsilon.
  • forse alcune altre istanze specifiche del formato

Tuttavia> = 99% dell'intervallo in virgola mobile avrà un hash su un singolo valore per qualsiasi valore di epsilon che include almeno un valore in virgola mobile sopra o sotto un dato valore in virgola mobile.

Risultato

O> = 99% dell'intero intervallo di virgola mobile su un singolo valore comprendente seriamente l'intento di un valore di hash (e qualsiasi dispositivo / contenitore che si basa su un hash a bassa collisione distribuito in modo equo).

O epsilon è tale che sono consentite solo corrispondenze esatte.

Granulare

Ovviamente potresti optare per un approccio granulare.

Con questo approccio si definiscono bucket esatti fino a una risoluzione specifica. vale a dire:

[0.001, 0.002)
[0.002, 0.003)
[0.003, 0.004)
...
[122.999, 123.000)
...

Ogni bucket ha un hash univoco e qualsiasi punto fluttuante all'interno del bucket è uguale a qualsiasi altro float nello stesso bucket.

Sfortunatamente è ancora possibile che due galleggianti siano distanti epsilon e abbiano due hash separati.


2
Concordo sul fatto che l'approccio granulare qui sarebbe probabilmente il migliore, se si adatta ai requisiti di OP. Anche se temo che OP abbia requisiti di tipo +/- 0,1%, il che significa che non può essere granulare.
Neil,

4
@DocBrown La parte "impossibile" è corretta. Se l'uguaglianza basata su epsilon implica che i codici hash sono uguali, allora hai automaticamente tutti i codici hash uguali, quindi la funzione hash non è più utile. L'approccio bucket può essere fruttuoso, ma avrai numeri con diversi codici hash arbitrariamente vicini l'uno all'altro.
J. Fabian Meier,

2
L'approccio bucket può essere modificato controllando non solo il bucket con la chiave hash esatta, ma anche i due bucket vicini (o almeno uno di essi) per il loro contenuto. Ciò elimina il problema di quei casi limite per il costo di aumentare il tempo di esecuzione di un fattore al massimo due (se implementato correttamente). Tuttavia, non cambia l'ordine generale del tempo di esecuzione.
Doc Brown,

Mentre hai ragione nello spirito, non tutto crollerà. Con un piccolo epsilon fisso, la maggior parte dei numeri sarà uguale a se stessa. Certo, per quelli l'Epsilon sarà inutile, quindi di nuovo nello spirito hai ragione.
Carsten S,

1
@CarstenS Sì, la mia affermazione che il 99% degli hash di intervallo su un singolo hash in realtà non copre l'intero intervallo float. Esistono molti valori di fascia alta che sono separati da più di epsilon che eseguono l'hash nei propri bucket unici.
Kain0_0

7

Puoi modellare la tua temperatura come numero intero sotto il cofano. La temperatura ha un limite inferiore naturale (-273,15 Celsius). Quindi, double (-273.15 è uguale a 0 per il numero intero sottostante). Il secondo elemento di cui hai bisogno è la granularità della tua mappatura. Stai già usando implicitamente questa granularità; è il tuo EPSILON.

Basta dividere la tua temperatura per EPSILON e prenderne la parola, ora l'hash e il tuo uguale si comporteranno in sincronia. In Python 3 l'intero è illimitato, EPSILON può essere più piccolo, se lo desideri.

ATTENZIONE Se si modifica il valore di EPSILON e si è serializzato l'oggetto, questi non saranno compatibili!

#Pseudo code
class Temperature:
    def __init__(self, degrees):
        #CHECK INVALID VALUES HERE
        #TRANSFORM TO KELVIN HERE
        self.degrees = Math.floor(kelvin/EPSILON)

1

L'implementazione di una tabella hash in virgola mobile in grado di trovare elementi "approssimativamente uguali" a una determinata chiave richiederà l'utilizzo di un paio di approcci o una combinazione di questi:

  1. Arrotondare ciascun valore a un incremento leggermente maggiore dell'intervallo "fuzzy" prima di memorizzarlo nella tabella hash e, quando si cerca di trovare un valore, controllare la tabella hash per i valori arrotondati sopra e sotto il valore cercato.

  2. Memorizza ogni elemento nella tabella hash usando le chiavi che si trovano sopra e sotto il valore ricercato.

Si noti che l'utilizzo di entrambi gli approcci richiederà probabilmente che le voci della tabella hash non identificino gli elementi, ma piuttosto gli elenchi, poiché probabilmente ci saranno più elementi associati a ciascuna chiave. Il primo approccio sopra ridurrà al minimo le dimensioni richieste della tabella hash, ma ogni ricerca di un elemento non presente nella tabella richiederà due ricerche nella tabella hash. Il secondo approccio sarà in grado di identificare rapidamente gli elementi non presenti nella tabella, ma in genere richiederà che la tabella contenga circa il doppio delle voci che sarebbero altrimenti richieste. Se si sta cercando di trovare oggetti nello spazio 2D, può essere utile utilizzare un approccio per la direzione X e uno per la direzione Y, in modo che invece di memorizzare ogni elemento una volta ma che richiedano quattro operazioni di query per ogni ricerca, o essere in grado di utilizzare una ricerca per trovare un articolo ma doverlo conservare quattro volte,


0

Naturalmente puoi definire "quasi uguale" eliminando dire gli ultimi otto bit della mantissa e quindi confrontando o hashing. Il problema è che i numeri molto vicini tra loro possono essere diversi.

C'è un po 'di confusione qui: se due numeri in virgola mobile sono uguali, sono uguali. Per verificare se sono uguali, utilizzare "==". A volte non vuoi verificare l'uguaglianza, ma quando lo fai, "==" è la strada da percorrere.


0

Questa non è una risposta, ma un commento esteso che può essere utile.

Ho lavorato su un problema simile, usando MPFR (basato su GNU MP). L'approccio "bucket" delineato da @ Kain0_0 sembra dare risultati accettabili, ma sii consapevole dei limiti evidenziati in quella risposta.

Volevo aggiungere che, a seconda di ciò che si sta tentando di fare, l'uso di un sistema di algebra al computer "esatto" ( caveat emptor ) come Mathematica può aiutare a integrare o verificare un programma numerico inesatto. Ciò ti consentirà di calcolare i risultati senza preoccuparti dell'arrotondamento, ad esempio, 7*√2 - 5*√2produrrà 2invece 2.00000001o simili. Naturalmente, questo introdurrà ulteriori complicazioni che potrebbero valere o meno la pena.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.