Як реалізовувати поплавковий хешинг з приблизною рівністю


15

Скажімо, у нас є наступний клас Python (проблема в Java існує точно так само, як equalsі hashCode)

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

де degreesтемпература в Кельвіні як поплавок. Тепер я хотів би здійснити перевірку рівності і хешування для Temperatureтаким чином , що

  • порівнює поплавці до epsilon різниці замість прямого тестування рівності,
  • і вшановує договір, який a == bпередбачає hash(a) == hash(b).
def __eq__(self, other):
    return abs(self.degrees - other.degrees) < EPSILON

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

Документація Python трохи розповідає про хеш-номери, щоб забезпечити це, hash(2) == hash(2.0)але це не зовсім та сама проблема.

Я навіть на правильному шляху? І якщо так, то який стандартний спосіб реалізувати хешинг у цій ситуації?

Оновлення : Тепер я розумію, що цей тип тестування рівності для плавців виключає транзитивність ==та equals. Але як це відбувається разом із "загальновідомими", які плавають, не слід порівнювати безпосередньо? Якщо ви реалізуєте оператор рівності, порівнюючи поплавці, інструменти статичного аналізу подаватимуть скарги. Чи правильно вони це роблять?


9
чому питання має тег Java?
Laiv

8
Щодо Вашого оновлення: Я б сказав, що хешування плавців - це взагалі сумнівна річ. Постарайтеся уникати використання поплавків як клавіш або як елементів набору.
Дж. Фабіан Мейєр

6
@Neil: У той же час, чи не округлення звучить як цілі числа? Я маю на увазі: якщо ви можете округлити, скажімо, тисячні частки градусів, то ви могли б просто використовувати фіксовану точку зображення - ціле число, що виражає температуру в тисячних градусах градусів. Для зручності використання ви можете мати геттер / сетер, які прозоро перетворюються з / в плаваючі, якщо ви хочете ...
Матьє М.

4
Кельвіни вже не ступеня. Ступені також неоднозначні. Чому б просто не назвати це kelvin?
Соломон Учко

Відповіді:


41

впровадити тестування рівності та хешування для температури таким чином, щоб порівнювати поплавці до різниці в епсилоні замість прямого тестування рівності,

Нечітка рівність порушує вимоги, які Java ставить перед equalsметодом, а саме транзитивність , тобто якщо x == yі y == z, то x == z. Але якщо ви зробите нечітку рівність, наприклад, з епізоном 0,1, то 0.1 == 0.2і 0.2 == 0.3, але 0.1 == 0.3не виконується.

Хоча Python не документує такої вимоги, все ж наслідки наявності неперехідної рівності роблять це дуже поганою ідеєю; міркування про такі типи викликають головний біль.

Тому настійно рекомендую цього не робити.

Або забезпечте точну рівність і базуйте свій хеш на цьому очевидним чином, і надайте окремий метод, щоб зробити нечітке узгодження, або перейдіть з підходом до класу еквівалентності, запропонованим Kain. Хоча в останньому випадку я рекомендую вам зафіксувати своє значення представницьким членом класу еквівалентності в конструкторі, а потім перейти до простої точної рівності та хешування для решти; набагато простіше міркувати про типи таким чином.

(Але якщо ви це зробите, ви можете також використовувати подання з фіксованою точкою замість плаваючої точки, тобто ви використовуєте ціле число для підрахунку тисячних ступенів або будь-якої точності, яка вам потрібна.)


2
цікаві думки. Таким чином, накопичивши мільйони епсилону і з транзитивністю можна зробити висновок, що все рівно нічому іншому :-) Але чи визнає це математичне обмеження дискретний фундамент плаваючих точок, які в багатьох випадках є наближенням кількості, яку вони мають бути представлені?
Крістоф

@Christophe Цікаве запитання. Якщо ви подумаєте над цим, ви побачите, що цей підхід дозволить скласти один великий клас еквівалентності з плавців, роздільна здатність яких більша за епсилон (звичайно, він орієнтований на 0), а кожен плавець залишить кожен у своєму класі. Але це не суть, справжня проблема полягає в тому, що чи можна зробити висновок про те, що 2 числа рівні, залежить від того, чи є третє порівняне, і порядку, в якому це робиться.
Звичайний

Звертаючись до редакції @ OP, я додам, що неправильність плаваючої точки ==повинна "заразити" ==типи, що містять їх. Тобто, якщо вони дотримуються ваших порад щодо точної рівності, тоді їх інструмент статичного аналізу повинен бути налаштований так, щоб попереджати, коли використовується рівність Temperature. Це єдине, що ти можеш зробити насправді.
HTNW

@HTNW: Це було б занадто просто. Клас співвідношення може мати float approximationполе, в якому не бере участь ==. Крім того, інструмент статичного аналізу вже буде попереджати всередині ==впровадження класів, коли один із членів, що порівнюється, є floatтипом.
MSalters

@MSalters? Імовірно, досить настроювані інструменти статичного аналізу можуть зробити те, що я запропонував, просто чудово. Якщо в класі є floatполе, в якому не бере участь ==, то не налаштовуйте свій інструмент для попередження ==про цей клас. Якщо клас робить, то, імовірно, маркування класу ==як "занадто точне" призведе до того, що інструмент ігнорує таку помилку під час реалізації. Наприклад, на Java, якщо це @Deprecated void foo(), то void bar() { foo(); }попередження, але @Deprecated void bar() { foo(); }це не так. Можливо, багато інструментів це не підтримують, але деякі можуть.
HTNW

16

Щасти

Ви не зможете цього досягти, не будучи дурним хешами чи жертвуючи епсилоном.

Приклад:

Припустимо, що кожна точка хешує своє власне унікальне хеш-значення.

Оскільки номери з плаваючою комою є послідовними, перед заданим значенням плаваючої крапки буде до k чисел, і до k чисел після заданого значення плаваючої точки, які знаходяться в межах деякого епсилона даної точки.

  1. Для кожної двох точок в епізолі один від одного, які не мають однакового хеш-значення.

    • Відрегулюйте схему хешування таким чином, щоб ці дві точки хешували однакове значення.
  2. Вказуючи для всіх таких пар, вся послідовність чисел з плаваючою комою згортається до одиничного має значення.

Є кілька випадків, коли це не відповідає дійсності:

  • Позитивна / негативна нескінченність
  • NaN
  • Кілька денормованих діапазонів, які можуть не бути пов'язаними з основним діапазоном для даного епсилона.
  • можливо, кілька інших конкретних форматів

Однак> = 99% діапазону з плаваючою комою буде хеш до одного значення для будь-якого значення epsilon, що включає щонайменше одне значення плаваючої точки вище або нижче деякого заданого значення плаваючої точки.

Результат

Або> = 99% усього діапазону з плаваючою точкою хешируется на одне значення, що серйозно суперечить наміру хеш-значення (і будь-який пристрій / контейнер, що спирається на досить розподілений хеш з низьким зіткненням).

Або епсилон такий, що дозволені лише точні збіги.

Гранульований

Ви, звичайно, можете скористатися детальним підходом.

Під цим підходом ви визначаєте точні відрізки до певної роздільної здатності. тобто:

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

Кожне відро має унікальний хеш, і будь-яка плаваюча точка всередині ковша порівнює рівну будь-якій іншій плавці в тому ж відрі.

На жаль, все ще можливо, що два поплавці знаходяться на відстані епсілону та мають два окремих хеша.


2
Я згоден, що детальний підхід тут, мабуть, буде найкращим, якщо це відповідає вимогам ОП. Хоча я боюся, що ОП має такі +/- 0,1% типових вимог, це означає, що вони не можуть бути деталізованими.
Ніл

4
@DocBrown Частина "не можлива" правильна. Якщо рівність на основі epsilon повинна означати, що хеш-коди рівні, то ви автоматично маєте всі хеш-коди рівні, тому хеш-функція більше не корисна. Підхід до відра може бути плідним, але у вас будуть номери з різними хеш-кодами, які довільно близькі один до одного.
J. Fabian Meier

2
Підхід до відра може бути модифікований, перевіривши не лише відро з точним хеш-ключем, але й два відра для сусідства (або принаймні одне з них) на їх вміст. Це усуває проблему тих крайових випадків за рахунок збільшення тривалості роботи на коефіцієнт щонайменше двох (якщо правильно виконано). Однак це не змінює загальний порядок виконання часу.
Док Браун

Поки ви праві по духу, не все обвалиться. Із фіксованим невеликим епсилоном більшість чисел дорівнюватиме лише собі. Звичайно, для тих, хто епсілон буде марний, тож знову, по духу, ви маєте рацію.
Carsten S

1
@CarstenS Так, моє твердження про те, що 99% хешей діапазону на один хеш, насправді не охоплює весь діапазон плавучих знаків. Існує багато значень високого діапазону, які розділені більше, ніж епсілон, який буде хешувати власні унікальні відра.
Kain0_0

7

Ви можете моделювати температуру як ціле число під кришкою. Температура має природну нижню межу (-273,15 Цельсія). Отже, подвійний (-273,15 дорівнює 0 для базового цілого числа). Другий елемент, який вам потрібен, - це деталізація вашого відображення. Ви вже використовуєте цю деталізацію неявно; це ваш EPSILON.

Просто розділіть свою температуру на EPSILON і прийміть підлогу, тепер ваш хеш і ваші рівні будуть поводитися синхронно. У Python 3 ціле число не обмежене, EPSILON може бути меншим, якщо хочете.

ПОПЕРЕДЖЕННЯ Якщо ви змінили значення EPSILON і ви серіалізували об'єкт, вони не будуть сумісні!

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

1

Реалізація хеш-таблиці з плаваючою комою, яка може знайти речі, приблизно "рівні" даному ключу, зажадає використання декількох підходів або їх комбінації:

  1. Округлюйте кожне значення на приріст, який є дещо більшим, ніж "нечіткий" діапазон, перш ніж зберігати його в хеш-таблиці, і, намагаючись знайти значення, перевірте хеш-таблицю на округлені значення вище та нижче шуканого значення.

  2. Зберігайте кожен елемент у хеш-таблиці, використовуючи клавіші, що знаходяться вище та нижче потрібного значення.

Зверніть увагу, що використання будь-якого підходу, ймовірно, вимагатиме, щоб записи хеш-таблиці не ідентифікували елементи, а скоріше списки, оскільки, ймовірно, буде кілька елементів, пов'язаних з кожним ключем. Перший вище підхід дозволить мінімізувати необхідний розмір хеш-таблиці, але для кожного пошуку елемента, який не знаходиться в таблиці, знадобляться два пошукові таблиці хеш-таблиць. Другий підхід швидко зможе визначити, що елементи не знаходяться в таблиці, але, як правило, потрібно, щоб таблиця містила приблизно вдвічі більше записів, ніж інакше потрібно. Якщо ви намагаєтеся знайти об'єкти у двовимірному просторі, може бути корисним використовувати один підхід для напрямку X та один для напрямку Y, так що замість того, щоб кожен елемент зберігався один раз, але вимагали чотири операції запиту для кожного пошуку, або бути вміти використовувати один пошук, щоб знайти предмет, але потрібно зберігати кожен предмет чотири рази,


0

Звичайно, ви можете визначити "майже рівний", видаливши скажіть останні вісім біт мантіси, а потім порівнявши або хешируя. Проблема полягає в тому, що числа, близькі один до одного, можуть бути різними.

Тут є деяка плутанина: якщо два числа з плаваючою комою порівнюються рівними, вони рівні. Щоб перевірити, чи рівні вони, ви використовуєте "==". Іноді не хочеться перевіряти рівність, але коли це робиш, "==" - це шлях.


0

Це не відповідь, а розширений коментар, який може бути корисним.

Я працював над подібною проблемою, використовуючи MPFR (заснований на GNU MP). Підхід "відро", як його виклав @ Kain0_0, здається, дає прийнятні результати, але пам'ятайте про обмеження, зазначені в цій відповіді.

Я хотів би додати, що - залежно від того, що ви намагаєтесь зробити - використання «точної» ( емпітора застережень ) комп’ютерної алгебри, наприклад, Mathematica, може допомогти доповнити або перевірити неточну чисельну програму. Це дозволить вам обчислювати результати, не турбуючись про округлення, наприклад, 7*√2 - 5*√2вийде 2замість того 2.00000001чи іншого. Звичайно, це спричинить додаткові ускладнення, які можуть бути, а можуть і не варті.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.