ヘルプKaggleにTensorFlowグレートバリアリーフを保護チャレンジに参加

乱数の生成

TensorFlow.org で表示 Google Colab で実行 GitHub でソースを表示 ノートブックをダウンロード

TensorFlow では、tf.random モジュールに疑似乱数ジェネレータ(RNG)を提供しています。このドキュメントは、乱数ジェネレータをどのように制御し、これらのジェネレータがほかの TensorFlow サブシステムとどのように対話するのかを説明します。

TensorFlow で乱数ジェネレータを制御するには、次の 2 つのアプローチがあります。

  1. tf.random.Generator オブジェクトを明示的に使用する。各オブジェクトは、数字が生成された後に変更される状態を維持します(tf.Variable)。

  2. tf.random.stateless_uniform などの純粋関数型のステートレスランダム関数を使用する。同一の引数を使って(シードを含む)同じデバイスでこれらの関数を呼び出すと、同じ結果が必ず生成されます。

警告: tf.random.uniformtf.random.normal といった TF 1.x の古い RNG は、まだ使用廃止になっていませんが、使用しないことを強くお勧めします。

警告: TensorFlow バージョン間での乱数の一貫性は保証されていません。「バージョンの互換性」をご覧ください。

セットアップ

import tensorflow as tf

# Creates 2 virtual devices cpu:0 and cpu:1 for using distribution strategy
physical_devices = tf.config.experimental.list_physical_devices("CPU")
tf.config.experimental.set_virtual_device_configuration(
    physical_devices[0], [
        tf.config.experimental.VirtualDeviceConfiguration(),
        tf.config.experimental.VirtualDeviceConfiguration()
    ])

tf.random.Generator クラス

tf.random.Generator クラスは、それぞれの RNG 呼び出しで異なる結果を得たい場合に使用します。また、乱数が生成されるたびに更新される内部状態(tf.Variable オブジェクトが管理)を維持します。状態は tf.Variable によって管理されるため、チェックポイントの簡単な設定、自動制御依存関係、およびスレッドセーフなど、tf.Variable が提供する便利な機能を利用できます。

クラスのオブジェクトを手動で作成して tf.random.Generator を得るか、tf.random.get_global_generator() を呼び出して、デフォルトグローバルジェネレータを得ることができます。

g1 = tf.random.Generator.from_seed(1)
print(g1.normal(shape=[2, 3]))
g2 = tf.random.get_global_generator()
print(g2.normal(shape=[2, 3]))
tf.Tensor(
[[ 0.43842277 -0.53439844 -0.07710262]
 [ 1.5658046  -0.1012345  -0.2744976 ]], shape=(2, 3), dtype=float32)
tf.Tensor(
[[ 0.9678078 -0.2313377 -0.1529023]
 [-1.5427535 -2.1245437 -0.4752217]], shape=(2, 3), dtype=float32)

ジェネレータオブジェクトには、さまざまな作成方法があります。最も簡単なのは、上記に示した Generator.from_seed で、シードからジェネレータを作成します。シードは、負でない整数値です。from_seed にはオプションの引数 alg があり、このジェネレータが使用する RNG アルゴリズムを指定します。

g1 = tf.random.Generator.from_seed(1, alg='philox')
print(g1.normal(shape=[2, 3]))
tf.Tensor(
[[ 0.43842277 -0.53439844 -0.07710262]
 [ 1.5658046  -0.1012345  -0.2744976 ]], shape=(2, 3), dtype=float32)

この詳細については、以下の「アルゴリズム」のセクションをご覧ください。

ジェネレータを作成するもう 1 つの方法に、Generator.from_non_deterministic_state を使用する方法があります。この方法で作成されたジェネレータは、時刻や OS に基づいて、非決定的状態から開始します。

g = tf.random.Generator.from_non_deterministic_state()
print(g.normal(shape=[2, 3]))
tf.Tensor(
[[-0.7206759   0.02959209  0.23080027]
 [-1.5074615  -1.0404333  -0.5966916 ]], shape=(2, 3), dtype=float32)

ジェネレータの作成方法はほかにもありますが、このガイドでは説明されていません。

tf.random.get_global_generator を使用してグローバルジェネレータを得る場合、デバイスの配置に注意する必要があります。グローバルジェネレータは、tf.random.get_global_generator が呼び出されたときに初めて作成され(非確定的状態から)、その呼び出し時のデフォルトのデバイスに配置されます。そのため、たとえば tf.random.get_global_generator を呼び出す最初の場所が tf.device("gpu") スコープ内である場合、グローバルジェネレータは GPU に配置され、後で CPU からそのグローバルジェネレータを使用すると、GPU から CPU へのコピーを招くことになります。

また、グローバルジェネレータを別のジェネレータオブジェクトに置き換える tf.random.set_global_generator 関数もあります。ただし、古いグローバルジェネレータが tf.function によってキャプチャされている可能性があり(弱参照として)、それを置き換えるとガベージコレクタで解放され、tf.function が機能しなくなるため、この関数の使用には注意が必要です。グローバルジェネレータをリセットする方法としては、 Generator.reset_from_seed などの「リセット」関数を使用する方が有効です。この関数は新しいジェネレータオブジェクトを作成しません。

g = tf.random.Generator.from_seed(1)
print(g.normal([]))
print(g.normal([]))
g.reset_from_seed(1)
print(g.normal([]))
tf.Tensor(0.43842277, shape=(), dtype=float32)
tf.Tensor(1.6272374, shape=(), dtype=float32)
tf.Tensor(0.43842277, shape=(), dtype=float32)

独立乱数ストリームを作成する

多くのアプリケーションでは、複数の独立乱数ストリームが必要です。ここでいう独立とは、これらのストリームがオーバーラップすることがないため、統計的に検出可能な相関を持つことがないということです。これは、Generator.split を使用して、互いに独立することが保証された複数のジェネレータを作成する(独立ストリームを生成する)ことで実現できます。

g = tf.random.Generator.from_seed(1)
print(g.normal([]))
new_gs = g.split(3)
for new_g in new_gs:
  print(new_g.normal([]))
print(g.normal([]))
tf.Tensor(0.43842277, shape=(), dtype=float32)
tf.Tensor(2.536413, shape=(), dtype=float32)
tf.Tensor(0.33186463, shape=(), dtype=float32)
tf.Tensor(-0.07144657, shape=(), dtype=float32)
tf.Tensor(-0.79253083, shape=(), dtype=float32)

split は、normal などの RNG メソッドと同様に、それを呼び出したジェネレータ(上記の例の g)の状態を変更します。互いに独立しているほかに、新しいジェネレータ(new_gs)は、古いジェネレータ(g)からの独立も保証されます。

新しいジェネレータをスポーンする方法は、デバイス間コピーのオーバーヘッドを回避するために、使用するジェネレータがほかの計算と同じデバイス上にあることを確実にする上でも役立ちます。次に例を示します。

with tf.device("cpu"):  # change "cpu" to the device you want
  g = tf.random.get_global_generator().split(1)[0]  
  print(g.normal([]))  # use of g won't cause cross-device copy, unlike the global generator
tf.Tensor(-0.32229978, shape=(), dtype=float32)

注意: 理論的には、split の代わりに from_seed などのコンストラクタを使用して新しいジェネレータを取得することはできますが、そうした場合、新しいジェネレータがグローバルジェネレータから独立する保証がなくなります。また、偶発的に、同じシードまたは乱数ストリームをオーバーラップさせるシードを伴うジェネレータを 2 つ作成してしまうリスクもあります。

分割されたジェネレータに split を呼び出して分割を再帰的に実行することができます。再帰の深度には制限(整数オーバーフローの禁止)はありません。

tf.function とのインタラクション

tf.random.Generator は、tf.function と使用した場合の tf.Variable と同じルールに従います。これには、3 つの側面が含まれます。

tf.function の外部でジェネレータを作成する

tf.function は、その外部で作成されたジェネレータを使用できます。

g = tf.random.Generator.from_seed(1)
@tf.function
def foo():
  return g.normal([])
print(foo())
tf.Tensor(0.43842277, shape=(), dtype=float32)

ユーザーは、関数が呼び出されたときにジェネレータオブジェクトがアライブである(ガベージコレクトされていない)ことを確認する必要があります。

tf.function 内部でジェネレータを作成する

tf.function 内部でのジェネレータの作成は、関数を初めて実行したときにのみ発生します。

g = None
@tf.function
def foo():
  global g
  if g is None:
    g = tf.random.Generator.from_seed(1)
  return g.normal([])
print(foo())
print(foo())
tf.Tensor(0.43842277, shape=(), dtype=float32)
tf.Tensor(1.6272374, shape=(), dtype=float32)

ジェネレータを引数として tf.function に渡す

tf.function の引数として使用された場合、同じ状態サイズ(状態サイズは RNG アルゴリズムで決定)を持つ別のジェネレータオブジェクトによって tf.function のトレースがもう一度行われるようになることはありません。状態サイズが異なる場合は、もう一度トレースされます。

num_traces = 0
@tf.function
def foo(g):
  global num_traces
  num_traces += 1
  return g.normal([])
foo(tf.random.Generator.from_seed(1))
foo(tf.random.Generator.from_seed(2))
print(num_traces)
1

分散ストラテジーとのインタラクション

Generator が分散ストラテジーとインタラクションする方法には 3 つあります。

分散ストラテジーの外部でジェネレータを作成する

ストラテジーのスコープ外でジェネレータを作成する場合、すべてのレプリカによるジェネレータへのアクセスはシリアライズされるため、レプリカは異なる乱数を取得します。

g = tf.random.Generator.from_seed(1)
strat = tf.distribute.MirroredStrategy(devices=["cpu:0", "cpu:1"])
with strat.scope():
  def f():
    print(g.normal([]))
  results = strat.run(f)
WARNING:tensorflow:There are non-GPU devices in `tf.distribute.Strategy`, not using nccl allreduce.
INFO:tensorflow:Using MirroredStrategy with devices ('/job:localhost/replica:0/task:0/device:CPU:0', '/job:localhost/replica:0/task:0/device:CPU:1')
WARNING:tensorflow:Using MirroredStrategy eagerly has significant overhead currently. We will be working on improving this in the future, but for now please wrap `call_for_each_replica` or `experimental_run` or `run` inside a tf.function to get the best performance.
tf.Tensor(0.43842277, shape=(), dtype=float32)
tf.Tensor(1.6272374, shape=(), dtype=float32)

この使用方法では、ジェネレータのデバイスがレプリカとは異なるため、パフォーマンスの問題が発生する可能性があります。

分散ストラテジーの内部でジェネレータを作成する

ストラテジーのスコープ内でジェネレータを作成することは許可されません。これは、ジェネレータを複製する方法があいまいであるためです(各レプリカが同じ乱数を取得するようにコピーすべきか、各レプリカが異なる乱数を取得するように「分割」すべきか、など)。

strat = tf.distribute.MirroredStrategy(devices=["cpu:0", "cpu:1"])
with strat.scope():
  try:
    tf.random.Generator.from_seed(1)
  except ValueError as e:
    print("ValueError:", e)
WARNING:tensorflow:There are non-GPU devices in `tf.distribute.Strategy`, not using nccl allreduce.
INFO:tensorflow:Using MirroredStrategy with devices ('/job:localhost/replica:0/task:0/device:CPU:0', '/job:localhost/replica:0/task:0/device:CPU:1')
ValueError: Creating a generator within a strategy scope is disallowed, because there is ambiguity on how to replicate a generator (e.g. should it be copied so that each replica gets the same random numbers, or 'split' so that each replica gets different random numbers).

Strategy.run は、ストラテジーのスコープで、暗黙的に引数関数を実行することに注意してください。

strat = tf.distribute.MirroredStrategy(devices=["cpu:0", "cpu:1"])
def f():
  tf.random.Generator.from_seed(1)
try:
  strat.run(f)
except ValueError as e:
  print("ValueError:", e)
WARNING:tensorflow:There are non-GPU devices in `tf.distribute.Strategy`, not using nccl allreduce.
INFO:tensorflow:Using MirroredStrategy with devices ('/job:localhost/replica:0/task:0/device:CPU:0', '/job:localhost/replica:0/task:0/device:CPU:1')
WARNING:tensorflow:Using MirroredStrategy eagerly has significant overhead currently. We will be working on improving this in the future, but for now please wrap `call_for_each_replica` or `experimental_run` or `run` inside a tf.function to get the best performance.
INFO:tensorflow:Error reported to Coordinator: Creating a generator within a strategy scope is disallowed, because there is ambiguity on how to replicate a generator (e.g. should it be copied so that each replica gets the same random numbers, or 'split' so that each replica gets different random numbers).
Traceback (most recent call last):
  File "/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/training/coordinator.py", line 297, in stop_on_exception
    yield
  File "/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/distribute/mirrored_run.py", line 323, in run
    self.main_result = self.main_fn(*self.main_args, **self.main_kwargs)
  File "/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/autograph/impl/api.py", line 572, in wrapper
    return func(*args, **kwargs)
  File "<ipython-input-1-2cd7806456bd>", line 3, in f
    tf.random.Generator.from_seed(1)
  File "/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/stateful_random_ops.py", line 453, in from_seed
    return cls(state=state, alg=alg)
  File "/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/stateful_random_ops.py", line 375, in __init__
    trainable=False)
  File "/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/stateful_random_ops.py", line 390, in _create_variable
    "Creating a generator within a strategy scope is disallowed, because "
ValueError: Creating a generator within a strategy scope is disallowed, because there is ambiguity on how to replicate a generator (e.g. should it be copied so that each replica gets the same random numbers, or 'split' so that each replica gets different random numbers).
ValueError: Creating a generator within a strategy scope is disallowed, because there is ambiguity on how to replicate a generator (e.g. should it be copied so that each replica gets the same random numbers, or 'split' so that each replica gets different random numbers).

ジェネレータを引数として Strategy.run に渡す

各レプリカが独自のジェネレータを使用するようにする場合は、(コピーするか分割するかして)n 個のジェネレータを作成する必要があります。n はレプリカの数です。その後で、Strategy.run に引数として渡す必要があります。

strat = tf.distribute.MirroredStrategy(devices=["cpu:0", "cpu:1"])
gs = tf.random.get_global_generator().split(2)
# to_args is a workaround for the absence of APIs to create arguments for 
# run. It will be replaced when such APIs are available.
def to_args(gs):  
  with strat.scope():
    def f():
      return [gs[tf.distribute.get_replica_context().replica_id_in_sync_group]]
    return strat.run(f)
args = to_args(gs)
def f(g):
  print(g.normal([]))
results = strat.run(f, args=args)
WARNING:tensorflow:There are non-GPU devices in `tf.distribute.Strategy`, not using nccl allreduce.
INFO:tensorflow:Using MirroredStrategy with devices ('/job:localhost/replica:0/task:0/device:CPU:0', '/job:localhost/replica:0/task:0/device:CPU:1')
WARNING:tensorflow:Using MirroredStrategy eagerly has significant overhead currently. We will be working on improving this in the future, but for now please wrap `call_for_each_replica` or `experimental_run` or `run` inside a tf.function to get the best performance.
WARNING:tensorflow:Using MirroredStrategy eagerly has significant overhead currently. We will be working on improving this in the future, but for now please wrap `call_for_each_replica` or `experimental_run` or `run` inside a tf.function to get the best performance.
tf.Tensor(1.8312341, shape=(), dtype=float32)
tf.Tensor(-2.3519678, shape=(), dtype=float32)

ステートレス RNG

ステートレス RNG の使用法はシンプルです。純粋関数であるため、ステートや副次的効果はありません。

print(tf.random.stateless_normal(shape=[2, 3], seed=[1, 2]))
print(tf.random.stateless_normal(shape=[2, 3], seed=[1, 2]))
tf.Tensor(
[[ 0.5441101   0.20738031  0.07356433]
 [ 0.04643455 -1.30159    -0.95385665]], shape=(2, 3), dtype=float32)
tf.Tensor(
[[ 0.5441101   0.20738031  0.07356433]
 [ 0.04643455 -1.30159    -0.95385665]], shape=(2, 3), dtype=float32)

各ステートレス RNG には、seed 引数が必要です。これは、形状 [2] の整数テンソルである必要があります。この演算の結果はこのシードによって完全に決定されます。

アルゴリズム

全般

tf.random.Generator クラスと stateless 関数は、すべてのデバイスで Philox アルゴリズム("philox" または tf.random.Algorithm.PHILOX として記述されている)をサポートします。

異なるデバイスで同じアルゴリズムを使用し、同じ状態から開始した場合、同じ整数が生成されます。また、デバイスが浮動小数計算の実行の仕方がデバイスによって異なるため(還元順など)わずかな数値の違いが出るかもしれませんが、「ほぼ同じ」浮動小数点数も生成します。

XLA デバイス

XLA 駆動のデバイス(TPU など、および XLA が有効で和える場合の CPU/GPU)では、ThreeFry アルゴリズム("threefry" また tf.random.Algorithm.THREEFRY)もサポートされています。このアルゴリズムは TPU では高速ですが、Philox と比べると、CPU/GPU では遅くなります。

これらのアルゴリズムに関する詳細は、「Parallel Random Numbers: As Easy as 1, 2, 3」をご覧ください。