При работе с числовыми данными бывает полезно привести их диапазон к некоторому стандартному. Обычно роль стандартного диапазона к которому выполняется переход играет диапазон [0, 1], а сама процедура перехода носит название нормализация или нормировка. В стандартной библиотеке D имеется функция normalize
в модуле std.numeric
, которая успешно решает задачу нормировки некоего диапазона. При всей своей обобщенности normalize
работает только для положительных значений и по этой причине при необходимости провести нормализацию в диапазон от -1 до 1 не получится. Особенно, если во входном диапазоне есть отрицательные значения…
Действительно, если заглянуть в документацию на алгоритм normalize
, можно увидеть утверждение о том, что нормализация имеет смысл только для положительных значений и нуля (и соответственно результирующий диапазон будет включать только неотрицательные значения). Попытка использовать отрицательные значения во входных данных приведет к срабатыванию исключения Assertion Failure, поэтому использовать normalize
для таких данных нельзя.
Мы подумали о том, что было бы полезно иметь некий алгоритм, который переводит данные не в диапазон [0, 1], а в более симметричный — [-1, 1]. При этом предполагается и тот вариант, при котором в диапазоне есть отрицательные значения.
Для того, чтобы произвести само преобразование потребовалась мера, которая бы была средством сопоставления чисел вне зависимости от их знака. В качестве такой меры предполагалось использовать максимальное значение из всего диапазона, но это сработало бы только для значений больших или равных нулю. По этой причине мы решили взять не максимальное значение из всего диапазона, а максимальный модуль из всего диапазона. Следовательно задача нормализации теперь решается естественным образом: находим максимальный модуль и делим на него каждое число из диапазона, естественным образом получая значения от -1 до 1.
Реализовали идею следующим образом:
auto fastAbs(float x) @system { uint p = *(cast(uint*) &x); p &= 0x7fffffff; return *(cast(float*) &p); } auto renormalize(Range)(Range r) { import std.algorithm; import std.math; auto maximalAbsoluteValue = r .map!fastAbs .fold!max; return r .map!(a => a / maximalAbsoluteValue); }
В этом коде, в алгоритме renormalize
происходит описанные выше процессы: с помощью map
и функции fastAbs
генерируется диапазон модулей, в котором с помощью алгоритма свертки последовательности fold
и функции max
находится максимальное значение, которое и записывается в переменную maximalAbsoluteValue
. Далее каждый элемент входного диапазона просто делится на полученное значение в maximalAbsoluteValue
.
Fun fact! Внимание здесь не учитываются случаи когда максимумом может быть или ноль или float.nan
. Для учета таких случаев требуется доработка кода с вставкой дополнительной проверки, либо доработка кода таким образом, чтобы входной массив не содержал float.nan
(и подобных ему значений) и не был состоящим только из нулей.
Функция fastAbs
— это первая функция, созданная нами самостоятельно и для оптимизации. Принцип ее работы, основан на том, что мы упоминали во второй части статьи по числам Posit, когда обсуждали перевод дробного числа в Posit
и устройства формата double
. Дело в том, что float
не отстает от double
(в плане строения формата) и представляет собой 32-битный целочисленный контейнер. И также, как и в double
, самый левый бит (это 31ый бит числа) представляет собой знак числа: если он равен 0, то число больше либо равно нулю; в противном случае число – отрицательное. Следовательно, если сбросить 31ый бит числа в ноль, то число станет положительным. Именно этот трюк и используется в функции fastAbs
, а все операции с указателями выполняют функцию конверсии float
в целочисленный контейнер и обратно.