Преобразование в и из np.random.RandomState numpy и random.Random Python?

Я хотел бы иметь возможность конвертировать туда и обратно между стандартным Random Python и np.random.RandomState numpy. Оба они используют алгоритм Mersenne Twister, поэтому это должно быть возможно (если они не используют разные версии этого алгоритма).

Я начал изучать методы getstate/setstate и get_state/set_state этих объектов. Но я не уверен, как преобразовать их детали.

import numpy as np
import random

rng1 = np.random.RandomState(seed=0)
rng2 = random.Random(seed=0)

state1 = rng1.get_state()
state2 = rng2.getstate()

Проверяя каждое состояние, я вижу:

>>> print(state1) 
('MT19937', array([0, 1, 1812433255, ..., 1796872496], dtype=uint32), 624, 0, 0.0)
>>> print(state2) 
(3, (2147483648, 766982754, ..., 1057334138, 2902720905, 624), None)

Первое состояние — это кортеж размера 5 с len(state1[1]) = 624.

Второе состояние — это кортеж размера 3 с len(state2[1]) = 625. Кажется, что последний элемент в state2 на самом деле является 624 в state1, что означает, что массивы на самом деле имеют одинаковый размер. Все идет нормально. Они кажутся разумно совместимыми.

К сожалению, внутренние числа не имеют очевидного соответствия, поэтому начальное значение 0 приводит к разным состояниям, что имеет смысл, поскольку rng1.rand() = .548 и rng2.random() = .844. Таким образом, алгоритм выглядит немного другим.

Однако мне не нужно, чтобы они идеально соответствовали друг другу. Мне просто нужно иметь возможность устанавливать состояние одного кольца из другого детерминированным образом, не влияя на состояние первого.

В идеале, как только я использовал состояние первого для установки состояния второго, не вызывая никаких случайных методов, а затем использовал второй для установки состояния первого, первое состояние осталось бы неизменным, но это не требование. .

В настоящее время у меня есть взломанный метод, который просто меняет местами список длиной 624, который я могу извлечь из обоих рангов. Однако я не уверен, есть ли какие-либо проблемы с этим подходом. Может ли кто-нибудь более осведомленный в этом вопросе пролить свет?

Вот мой подход, но я не уверен, что он работает правильно.

np_rng = np.random.RandomState(seed=0)
py_rng = random.Random(0)

# Convert python to numpy random state (incomplete)
py_state = py_rng.getstate()
np_rng = np.random.RandomState(seed=0)
np_state = np_rng.get_state()
new_np_state = (
    np_state[0],
    np.array(py_state[1][0:-1], dtype=np.uint32),
    np_state[2], np_state[3], np_state[4])
np_rng.set_state(new_np_state)

# Convert numpy to python random state (incomplete)
np_state = np_rng.get_state()
py_rng = random.Random(0)
py_state = py_rng.getstate()
new_py_state = (
    py_state[0], tuple(np_state[1].tolist() + [len(np_state[1])]),
    py_state[1]
)
py_rng.setstate(new_py_state)

РЕДАКТИРОВАТЬ:

Проведя небольшое расследование, я проверил, что происходит с состоянием после 10 вызовов случайной функции.

np_rng = np.random.RandomState(seed=0)
py_rng = random.Random(0)

for i in range(10):
    np_rng.rand()
    npstate = np_rng.get_state()
    print([npstate[0], npstate[1][[0, 1, 2, -2, -1]], npstate[2], npstate[3], npstate[4]])

for i in range(10):
    py_rng.random()
    pystate = py_rng.getstate()
    print([pystate[0], pystate[1][0:3] + pystate[1][-2:], pystate[2]])


['MT19937', array([2443250962, 1093594115, 1878467924, 2648828502, 1678096082], dtype=uint32), 2, 0, 0.0]
['MT19937', array([2443250962, 1093594115, 1878467924, 2648828502, 1678096082], dtype=uint32), 4, 0, 0.0]
['MT19937', array([2443250962, 1093594115, 1878467924, 2648828502, 1678096082], dtype=uint32), 6, 0, 0.0]
['MT19937', array([2443250962, 1093594115, 1878467924, 2648828502, 1678096082], dtype=uint32), 8, 0, 0.0]
['MT19937', array([2443250962, 1093594115, 1878467924, 2648828502, 1678096082], dtype=uint32), 10, 0, 0.0]
['MT19937', array([2443250962, 1093594115, 1878467924, 2648828502, 1678096082], dtype=uint32), 12, 0, 0.0]
['MT19937', array([2443250962, 1093594115, 1878467924, 2648828502, 1678096082], dtype=uint32), 14, 0, 0.0]
['MT19937', array([2443250962, 1093594115, 1878467924, 2648828502, 1678096082], dtype=uint32), 16, 0, 0.0]
['MT19937', array([2443250962, 1093594115, 1878467924, 2648828502, 1678096082], dtype=uint32), 18, 0, 0.0]
['MT19937', array([2443250962, 1093594115, 1878467924, 2648828502, 1678096082], dtype=uint32), 20, 0, 0.0]
[3, (1372342863, 3221959423, 4180954279, 418789356, 2), None]
[3, (1372342863, 3221959423, 4180954279, 418789356, 4), None]
[3, (1372342863, 3221959423, 4180954279, 418789356, 6), None]
[3, (1372342863, 3221959423, 4180954279, 418789356, 8), None]
[3, (1372342863, 3221959423, 4180954279, 418789356, 10), None]
[3, (1372342863, 3221959423, 4180954279, 418789356, 12), None]
[3, (1372342863, 3221959423, 4180954279, 418789356, 14), None]
[3, (1372342863, 3221959423, 4180954279, 418789356, 16), None]
[3, (1372342863, 3221959423, 4180954279, 418789356, 18), None]
[3, (1372342863, 3221959423, 4180954279, 418789356, 20), None]

Я ожидаю, что первый элемент в каждом кортеже — это просто версия алгоритма, который они используют.

Интересно видеть, что 624 целых числа, кажется, не меняются. Всегда ли это так?

Тем не менее, я все еще не уверен, что означает окончательный None в версии Python, а последние 2 числа - в версии numpy.


person Erotemic    schedule 01.06.2017    source источник
comment
Можете ли вы объяснить основную причину этого вопроса?   -  person Peter O.    schedule 01.06.2017
comment
У меня есть конвейер алгоритма, который я хочу посеять. Некоторые функции используют rng python, а другие используют rng numpy. Иногда один используется во внутреннем цикле другого. Я должен передать rng каждой функции, которая его использует, поэтому мне нужен способ, с помощью которого я мог бы плавно конвертировать между ними, не делая ничего грязного, например, заполняя одно случайным значением другого.   -  person Erotemic    schedule 01.06.2017
comment
Мне нужно убедиться, что вывод детерминирован на входе. Результатом функции, с которой я работаю, является образец данных, и этот образец имеет связанный хэш. Если хэш другой, то будет работать очень трудоемкий алгоритм индексации, иначе используется кешированная версия. Ответ Марка Дикинсона дает мне именно то, что мне нужно.   -  person Erotemic    schedule 02.06.2017


Ответы (1)


Состояние NumPy RandomState имеет вид задокументировано:

Возвращает: out : tuple(str, ndarray of 624 uints, int, int, float)

Возвращенный кортеж содержит следующие элементы:

  1. строка «MT19937».
  2. одномерный массив из 624 целочисленных ключей без знака.
  3. целое число поз.
  4. целое число has_gauss.
  5. поплавок cached_gaussian.

Последние две записи относятся к состоянию генератора стандартных нормальных отклонений: L624" rel="noreferrer">NumPy использует преобразование Бокса-Мюллера, который генерирует эти отклонения попарно. Таким образом, первый вызов генератора Гаусса создает два значения, возвращает первое, а затем сохраняет второе для последующего использования. Затем второй вызов извлекает это второе значение. Таким образом, у нас есть дополнительное состояние, которое необходимо хранить и извлекать.

Форма состояния Python Random не документирована, но ее легко извлечь из источник. Начиная с CPython 3.6.1 это выглядит так:

def getstate(self):
    """Return internal state; can be passed to setstate() later."""
    return self.VERSION, super().getstate(), self.gauss_next

Опять же, Python генерирует нормальные отклонения парами, и self.gauss_next равно None, если не сохранено дополнительное нормальное отклонение, и значение сохраненного отклонения, если оно доступно.

Чтобы узнать, что возвращает super().getstate(), вам нужно погрузиться в Исходный код на языке C: это кортеж длиной 625 слов, содержащий 624 слова, образующих состояние Вихря Мерсенна, вместе с текущей позицией в этом наборе слов. Таким образом, последняя запись в этом кортеже соответствует значению pos в индексе 2 состояния NumPy.

Вот пример преобразования из состояния Python в состояние NumPy, игнорируя детали гауссовой информации:

Python 3.6.1 (default, May 23 2017, 18:09:41) 
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy as np
>>> import random
>>> np_rng = np.random.RandomState(seed=0)
>>> py_rng = random.Random(0)
>>> version, (*mt_state, pos), gauss_next = py_rng.getstate() 
>>> np_rng.set_state(('MT19937', mt_state, pos))

После установки состояния NumPy RandomState из состояния Python Random мы видим, что числа с плавающей запятой, сгенерированные из двух ГСЧ, совпадают:

>>> py_rng.random(), np_rng.uniform()
(0.8444218515250481, 0.8444218515250481)
>>> py_rng.random(), np_rng.uniform()
(0.7579544029403025, 0.7579544029403025)
>>> py_rng.random(), np_rng.uniform()
(0.420571580830845, 0.420571580830845)

А вот и обратное преобразование:

>>> _, words, pos, _, _ = np_rng.get_state()
>>> py_rng.setstate((3, tuple(map(int, words)) + (pos,), None))

И, как и раньше, мы можем проверить совпадение выходных данных двух генераторов:

>>> py_rng.random(), np_rng.uniform()
(0.5488135039273248, 0.5488135039273248)
>>> py_rng.random(), np_rng.uniform()
(0.7151893663724195, 0.7151893663724195)
>>> py_rng.random(), np_rng.uniform()
(0.6027633760716439, 0.6027633760716439)
>>> all(py_rng.random() == np_rng.uniform() for _ in range(1000000))
True

Python и NumPy используют разные алгоритмы для генерации нормальных отклонений (хотя оба используемых алгоритма генерируют эти отклонения парами), поэтому, даже если мы передаем состояние, связанное с гауссовской зависимостью, мы не можем ожидать совпадения сгенерированных нормальных отклонений. Но если все, что вы хотите сделать, это каким-то образом сохранить информацию о состоянии Python в объекте состояния NumPy (и наоборот), чтобы преобразование из одного состояния в другое и обратно не теряло информацию, это достаточно легко сделать: если has_gauss равно нулю в состоянии NumPy, используйте None для последней записи состояния Python, а если has_gauss не равно нулю, используйте значение cached_gaussian из состояния NumPy в последней записи состояния Python. Вот пара функций, реализующих эти преобразования:

PY_VERSION = 3
NP_VERSION = 'MT19937'

def npstate_to_pystate(npstate):
    """
    Convert state of a NumPy RandomState object to a state
    that can be used by Python's Random.
    """
    version, keys, pos, has_gauss, cached_gaussian = npstate
    pystate = (
        PY_VERSION,
        tuple(map(int, keys)) + (int(pos),),
        cached_gaussian if has_gauss else None,
    )
    return pystate


def pystate_to_npstate(pystate):
    """
    Convert state of a Python Random object to state usable
    by NumPy RandomState.
    """
    version, (*keys, pos), cached_gaussian = pystate
    has_gauss = cached_gaussian is not None
    npstate = (
        NP_VERSION,
        keys,
        pos,
        has_gauss,
        cached_gaussian if has_gauss else 0.0
    )
    return npstate
person Mark Dickinson    schedule 01.06.2017
comment
Спасибо за ваш очень информативный ответ. Я узнал все, что хотел, и даже больше. - person Erotemic; 01.06.2017