TF+Numpy减少随机性的影响

有这么一个段子:

深度学习论文有一半不公开源代码,另外公开源代码的一半复现不了,鬼知道作者怎么把结果搞得这么牛逼的.

其中一个原因就是深度学习使用了大量的随机数,就我一般使用的Python+TensorFlow环境而言,Python的随机性来自于numpy,而TensorFlow在初始化参数的时候也是使用了随机数的,当我们复现时,如果随机数都不一样,那么得到的结果是否和作者相同就依赖于这个网络的鲁棒性了.

ps:个人感觉,既然大家都说深度学习是寻找局部最优而不是全局最优,那么初始点落在哪是不是找到的局部最优点也不一样,所以随机性的影响还是很大的.

先放使用代码吧,有兴趣的可以接着看:

from numpy.random import seed
seed(1)
from tensorflow import set_random_seed
set_random_seed(2)

一.Numpy

一般使用下列的指令来生成随机数

import numpy as np
print (np.random.random())

参考numpy.random.seed()的使用,因为是伪随机算法,所以这个得到的结果是不固定的,我运行了三次,得到的结果为(这个结果一般是复现不了的):

>>> print (np.random.random())
0.239562458088
>>> print (np.random.random())
0.112245171048
>>> print (np.random.random())
0.627331947322

重新开一个终端,再次运行的结果为:

>>> print (np.random.random())
0.819976925039
>>> print (np.random.random())
0.902082170272
>>> print (np.random.random())
0.856090102495

如何保证两次开启终端后得到的结果都是一直的呢,这就需要固定随机算法的种子点了,numpy.random.seed()来指定随机数生成时所用算法开始的整数值.

这个函数有这些特性:

  1. 如果使用相同的种子值,则每次生成的随机数都相同;
  2. 如果不设置种子值,则系统根据时间来自动选择种子值,此时每次生成的随机数因时间差异而不同;(我曾经因为项目需要开启了多进程,生成了N个相同函数的进程,同时生成随机数,但是无奈发现生成的随机数都相同,原因在于N个子进程都是同时生成了,我的解决方法是给每个进程传入一个参数,表示进程号,即0,1,...,N-1,以这个进程号为种子值去设置随机数,[Python标准库]random——伪随机数生成器讲了相关的一部分知识);讲了相关的一部分知识);
  3. 设置的种子值仅一次有效。

下面来验证该方法的有效性.

生成一个测试文件test.py:

import numpy as np
np.random.seed(5)
print (np.random.random())
print (np.random.random())

运行文件python test.py,得到下列结果:

0.22199317108973948
0.8707323061773764

再次运行python test.py,得到下列结果:

0.22199317108973948
0.8707323061773764

可以看到,两次的结果是一致的,说明这样使用就可以把随机数生成时的随机性都消除掉了,这样所有人的结果都是一致的.


二.TensorFlow

在TensorFlow中的设置可参考用深度学习每次得到的结果都不一样,怎么办?,这是一篇译文,原文在How to Get Reproducible Results with Keras有兴趣的可以看看.

其大概思想是:

为了在用同样的数据训练同一网络时确保得到同样的结果,需要设置随机数字生成器的种子,在TensorFlow中设置的方法是:

from tensorflow import set_random_seed
set_random_seed(2)

这里种子点参数可以设置别的整数.

在实践中,这样设置只能保证大体上能够相同,随着迭代的进行,loss等还是会有细微的差别,不过依然在可控的范围内,作者也讲了有以下的几个原因:来自第三方库的随机性,使用GPU产生的随机性,来自复杂模型的随机性.这些原因很难避免,不过实践来看,结果都在可控范围内.


三.TensorFlow源码解读

首先说明,这个会比较水,先放着等以后再加内容吧.

官方文档在set_random_seed.其解释说有两种情况:graph-level的种子和operation-level的种子.

operation-level的种子如下:

a = tf.random_uniform([1], seed=1)

需要对变量依次指定种子点,很不方便,一般我们都是全局使用的,因此需要使用graph-level的种子,如下:

tf.set_random_seed(1234)

其源代码位于random_seed.py,里面主要有两个函数,一个是getseed()函数,一个是setrandomseed()函数.set_randomseed()函数指向context.py.

里面Context类是相关的,定义了一些私有变量和函数.之外还有一些函数会引用它们.

get_seed()函数引用global_seed()函数:

def global_seed():
  """Returns the eager mode seed."""
  return context()._seed  # pylint: disable=protected-access

直接返回私有变量_seed.

setrandomseed()函数引用set_global_seed(seed)函数:

def set_global_seed(seed):
  """Sets the eager mode seed."""
  context()._set_global_seed(seed)  # pylint: disable=protected-access

指向context()里面的_set_global_seed(seed)函数:

def _set_global_seed(self, seed):
    """Set a global eager mode seed for random ops."""
    self._seed = seed
    self._rng = random.Random(self._seed)
    # Also clear the kernel cache, to reset any existing seeds
    if self._context_handle is not None:
      pywrap_tensorflow.TFE_ContextClearCaches(self._context_handle)

会设置种子变量_seed,另外_rng的作用可参考_internal_operation_seed()函数:

def _internal_operation_seed(self):
    """Returns a fake operation seed.

      In eager mode, user shouldn't set or depend on operation seed.
      Here, we generate a random seed based on global seed to make
      operation's randomness different and depend on the global seed.

    Returns:
      A fake operation seed based on global seed.
    """
    return self._rng.randint(0, _MAXINT32)

作用是在eager模式下,不在使用种子变量_seed,而是随机选择0到种子变量_seed之间的一个数作为种子变量,为什么要设置就不知道了.

另外,设置种子变量后在哪有应用就不知道了,留待以后解决吧.

【已完结】

编辑于 2019-08-14

文章被以下专栏收录