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

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

Я начал изучать методы gettate/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, Кажется, что последний элемент в состоянии 2 на самом деле представляет собой 624 в состоянии 1, что означает, что массивы на самом деле имеют одинаковый размер. Все идет нормально. Они кажутся разумно совместимыми.

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

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

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

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

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

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 числа - в простой версии.

1 ответ

Решение

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

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

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

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

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

Форма Питона 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 состояние от питона 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
Другие вопросы по тегам