Digamos que tenemos la siguiente clase de Python (el problema existe en Java igual que con equals
y hashCode
)
class Temperature:
def __init__(self, degrees):
self.degrees = degrees
¿Dónde degrees
está la temperatura en Kelvin como flotador? Ahora, me gustaría implementar pruebas de igualdad y hashing de Temperature
una manera que
- compara flotadores hasta una diferencia de épsilon en lugar de pruebas de igualdad directa,
- y honra el contrato que
a == b
implicahash(a) == hash(b)
.
def __eq__(self, other):
return abs(self.degrees - other.degrees) < EPSILON
def __hash__(self):
return # What goes here?
La documentación de Python habla un poco sobre los números hash para asegurar eso, hash(2) == hash(2.0)
pero este no es exactamente el mismo problema.
¿Estoy incluso en el camino correcto? Y si es así, ¿cuál es la forma estándar de implementar hashing en esta situación?
Actualización : ahora entiendo que este tipo de prueba de igualdad para flotadores elimina la transitividad de ==
y equals
. Pero, ¿cómo va eso junto con el "conocimiento común" de que los flotadores no deben compararse directamente? Si implementa un operador de igualdad comparando flotantes, las herramientas de análisis estático se quejarán. ¿Tienen razón para hacerlo?
fuente
kelvin
?Respuestas:
La igualdad difusa viola los requisitos que Java impone al
equals
método, a saber, la transitividad , es decir, six == y
yy == z
luegox == z
. Pero si hace una igualdad difusa con, por ejemplo, un épsilon de 0.1, entonces0.1 == 0.2
y0.2 == 0.3
, pero0.1 == 0.3
no se cumple.Si bien Python no documenta dicho requisito, las implicaciones de tener una igualdad no transitiva hacen que sea una muy mala idea; El razonamiento acerca de estos tipos induce dolor de cabeza.
Así que te recomiendo que no hagas eso.
O proporcione igualdad exacta y base su hash en eso de la manera obvia, y proporcione un método separado para hacer la coincidencia difusa, o vaya con el enfoque de clase de equivalencia sugerido por Kain. Aunque en el último caso, le recomiendo que fije su valor a un miembro representativo de la clase de equivalencia en el constructor, y luego vaya con simple igualdad exacta y hashing para el resto; Es mucho más fácil razonar sobre los tipos de esta manera.
(Pero si hace eso, también podría usar una representación de punto fijo en lugar de punto flotante, es decir, usar un número entero para contar milésimas de grado, o cualquier precisión que requiera).
fuente
==
debería "infectar" los==
tipos que los contienen. Es decir, si siguen su consejo de proporcionar una igualdad exacta, entonces su herramienta de análisis estático debería configurarse para advertir cuándo se usa la igualdadTemperature
. Es lo único que puedes hacer, de verdad.float approximation
campo en el que no participa==
. Además, la herramienta de análisis estático ya dará una advertencia dentro de la==
implementación de clases cuando uno de los miembros que se compara es unfloat
tipo.float
campo en el que no participa==
, no configure su herramienta para advertir==
sobre esa clase. Si la clase lo hace, entonces, presumiblemente, marcar la clase==
como "demasiado exacta" hará que la herramienta ignore ese tipo de error dentro de la implementación. Por ejemplo, en Java, si@Deprecated void foo()
, entoncesvoid bar() { foo(); }
es una advertencia, pero@Deprecated void bar() { foo(); }
no lo es. Tal vez muchas herramientas no admiten esto, pero algunas podrían.Buena suerte
No podrás lograr eso sin ser estúpido con los hashes o sacrificar el épsilon.
Ejemplo:
Suponga que cada punto tiene un valor hash único.
Como los números de coma flotante son secuenciales, habrá hasta k números antes de un valor de coma flotante dado, y hasta k números después de un valor de coma flotante dado que se encuentran dentro de una épsilon del punto dado.
Por cada dos puntos dentro de epsilon entre sí que no comparten el mismo valor hash.
Hay algunos casos en los que esto no será así:
Sin embargo,> = 99% del rango de coma flotante se dividirá en un solo valor para cualquier valor de épsilon que incluya al menos un valor de coma flotante por encima o por debajo de un valor de coma flotante dado.
Salir
Ya sea> = 99% de hashes de rango de punto flotante completo a un solo valor que comprime seriamente la intención de un valor de hash (y cualquier dispositivo / contenedor que se base en un hash de baja colisión bastante distribuido).
O el épsilon es tal que solo se permiten coincidencias exactas.
Granular
Por supuesto, podría optar por un enfoque granular.
Bajo este enfoque, usted define los cubos exactos hasta una resolución particular. es decir:
Cada cubo tiene un hash único, y cualquier punto flotante dentro del cubo se compara igual a cualquier otro flotante en el mismo cubo.
Desafortunadamente, todavía es posible que dos flotadores estén a una distancia de épsilon y tengan dos hashes separados.
fuente
Puede modelar su temperatura como un número entero debajo del capó. La temperatura tiene un límite inferior natural (-273,15 grados Celsius). Entonces, doble (-273.15 es igual a 0 para su entero subyacente). El segundo elemento que necesita es la granularidad de su mapeo. Ya está utilizando esta granularidad implícitamente; Es tu EPSILON.
Simplemente divida su temperatura por EPSILON y tome la palabra, ahora su hash y su igual se comportarán en sincronía. En Python 3 el número entero no tiene límites, EPSILON puede ser más pequeño si lo desea.
CUIDADO ¡ Si cambia el valor de EPSILON y ha serializado el objeto, no serán compatibles!
fuente
La implementación de una tabla hash de punto flotante que puede encontrar cosas que son "aproximadamente iguales" a una clave determinada requerirá el uso de un par de enfoques o una combinación de ellos:
Redondee cada valor a un incremento que sea algo mayor que el rango "difuso" antes de almacenarlo en la tabla hash, y cuando intente encontrar un valor, verifique en la tabla hash los valores redondeados por encima y por debajo del valor buscado.
Almacene cada elemento dentro de la tabla hash utilizando las teclas que están por encima y por debajo del valor que se busca.
Tenga en cuenta que el uso de cualquiera de los enfoques probablemente requerirá que las entradas de la tabla hash no identifiquen elementos, sino listas, ya que es probable que haya múltiples elementos asociados con cada clave. El primer enfoque anterior minimizará el tamaño requerido de la tabla hash, pero cada búsqueda de un elemento que no esté en la tabla requerirá dos búsquedas en la tabla hash. El segundo enfoque podrá identificar rápidamente que los elementos no están en la tabla, pero generalmente requerirá que la tabla contenga aproximadamente el doble de entradas que de lo contrario se requerirían. Si uno está tratando de encontrar objetos en el espacio 2D, puede ser útil usar un enfoque para la dirección X y otro para la dirección Y, de modo que en lugar de tener cada elemento almacenado una vez pero requiriendo cuatro operaciones de consulta para cada búsqueda, o capaz de usar una búsqueda para encontrar un artículo pero tener que almacenar cada artículo cuatro veces,
fuente
Por supuesto, puede definir "casi igual" eliminando digamos los últimos ocho bits de la mantisa y luego comparando o haciendo hash. El problema es que los números muy cercanos entre sí pueden ser diferentes.
Aquí hay cierta confusión: si dos números de coma flotante se comparan iguales, son iguales. Para verificar si son iguales, use “==“. A veces no desea verificar la igualdad, pero cuando lo hace, "==" es el camino a seguir.
fuente
Esta no es una respuesta, sino un comentario extendido que puede ser útil.
He estado trabajando en un problema similar, mientras usaba MPFR (basado en GNU MP). El enfoque de "cubeta" como lo describe @ Kain0_0 parece dar resultados aceptables, pero tenga en cuenta las limitaciones resaltadas en esa respuesta.
Quería agregar que, dependiendo de lo que intente hacer, usar un sistema de álgebra computacional "exacto" ( advertencia de advertencia ) como Mathematica puede ayudar a complementar o verificar un programa numérico inexacto. Esto le permitirá calcular resultados sin preocuparse por el redondeo, por ejemplo,
7*√2 - 5*√2
rendirá en2
lugar de2.00000001
o similar. Por supuesto, esto introducirá complicaciones adicionales que pueden o no valer la pena.fuente