当我们需要查看模型文件如.pb, .meta
等的结构的时候,需要先根据model生成event文件。在生成event文件之后,通过命令行执行tensorboard --logdir=[event dir]
即可启动TensorBoard服务,其中命令中中括号内表示event文件所在目录。那么我们来看看,event文件是怎么一步一步呈现出来的。
首先来看一下整个event文件解析流程的时序图:
上图是服务器启动之前所做的解析工作,下面我们就跟着代码,看看每一步都是怎么进行的。
一切还得从文章最开始得那条命令开始说起。既然存在一个tensorboard
命令,那么就一定可以找到一个以该命令命名的文件,且该文件所在的路径一定在操作系统的环境变量中。直接使用which tensorboard
可以定位到文件所在目录,在我电脑上,它位于/home/chou/SNPE/venv/bin
路径下。
(venv) chou@chou-vb:~/$ which tensorboard
/home/chou/SNPE/venv/bin/tensorboard
打开该文件,很简单,只有几行:
from tensorboard.main import run_main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit(run_main())
从第一行可以看出。真正的入口是tensorboard/main.py
中的run_main()
函数。我们找到Github下载下来的源代码,开main.py文件并定位到run_main()函数。
def run_main():
"""Initializes flags and calls main()."""
program.setup_environment()
# 生成TensorBoard实例
tensorboard = program.TensorBoard(default.get_plugins(),
program.get_default_assets_zip_provider())
try:
from absl import app
# Import this to check that app.run() will accept the flags_parser argument.
from absl.flags import argparse_flags
app.run(tensorboard.main, flags_parser=tensorboard.configure)
raise AssertionError("absl.app.run() shouldn't return")
except ImportError:
pass
tensorboard.configure(sys.argv)
# 调用TensorBoard的main函数
sys.exit(tensorboard.main())
可以看到,run_main()
的主要工作是进行了一些设置,并最终调用了TensorBoard的main
函数。
def main(self, ignored_argv=('',)):
....
try:
server = self._make_server()
sys.stderr.write('TensorBoard %s at %s (Press CTRL+C to quit)\n' %
(version.VERSION, server.get_url()))
sys.stderr.flush()
server.serve_forever()
return 0
except TensorBoardServerException as e:
....
为了简洁,省略了一些关系不大的代码,使用....
表示。从函数名中可以看到,main
执行了两个操作:
- 创建服务器
- 启动创建的服务
本文我们先看创建服务器部分,因为两部分都是很复杂的内容。跳转进入_make_server()
函数:
def _make_server(self):
"""Constructs the TensorBoard WSGI app and instantiates the server."""
app = application.standard_tensorboard_wsgi(self.flags,
self.plugin_loaders,
self.assets_zip_provider)
return self.server_class(app, self.flags)
进来也很简单,从名字能猜个大概,调用了一个启动标准WSGI
程序的函数,接着跟着跳吧:
def standard_tensorboard_wsgi(flags, plugin_loaders, assets_zip_provider):
.....
multiplexer = event_multiplexer.EventMultiplexer(
size_guidance=DEFAULT_SIZE_GUIDANCE,
tensor_size_guidance=tensor_size_guidance_from_flags(flags),
purge_orphaned_data=flags.purge_orphaned_data,
max_reload_threads=flags.max_reload_threads)
loading_multiplexer = multiplexer
....
context = base_plugin.TBContext(
db_module=db_module,
db_connection_provider=db_connection_provider,
db_uri=db_uri,
flags=flags,
logdir=flags.logdir,
multiplexer=multiplexer,
assets_zip_provider=assets_zip_provider,
plugin_name_to_instance=plugin_name_to_instance,
window_title=flags.window_title)
plugins = []
for loader in plugin_loaders:
plugin = loader.load(context)
if plugin is None:
continue
plugins.append(plugin)
plugin_name_to_instance[plugin.plugin_name] = plugin
return TensorBoardWSGIApp(flags.logdir, plugins, loading_multiplexer,
reload_interval, flags.path_prefix,
reload_task)
def TensorBoardWSGIApp(logdir, plugins, multiplexer, reload_interval,
path_prefix='', reload_task='auto'):
path_to_run = parse_event_files_spec(logdir)
if reload_interval >= 0:
# We either reload the multiplexer once when TensorBoard starts up, or we
# continuously reload the multiplexer.
start_reloading_multiplexer(multiplexer, path_to_run, reload_interval,
reload_task)
return TensorBoardWSGI(plugins, path_prefix)
同样,我们省略了一些代码。在以上函数的最后,返回来一个TensorBoardWSGI
的实例,事情到这里,创建服务器的部分貌似已经结束了,但是,我们并没有看到解析event文件的代码,说明还遗漏了什么。仔细查看代码,发现有个start_reloading_multiplexer
函数。并且,该函数的一个参数特别眼熟,logdir
,没错,就是我们命令行传递进来的参数,也就event是文件所在目录。最终,logdir
转换到了path_to_run
。那好,跟进去看看。
def start_reloading_multiplexer(multiplexer, path_to_run, load_interval,
reload_task):
.....
def _reload():
while True:
start = time.time()
tf.logging.info('TensorBoard reload process beginning')
for path, name in six.iteritems(path_to_run):
multiplexer.AddRunsFromDirectory(path, name)
tf.logging.info('TensorBoard reload process: Reload the whole Multiplexer')
multiplexer.Reload()
duration = time.time() - start
tf.logging.info('TensorBoard done reloading. Load took %0.3f secs', duration)
if load_interval == 0:
# Only load the multiplexer once. Do not continuously reload.
break
time.sleep(load_interval)
if reload_task == 'process':
tf.logging.info('Launching reload in a child process')
import multiprocessing
process = multiprocessing.Process(target=_reload, name='Reloader')
# Best-effort cleanup; on exit, the main TB parent process will attempt to
# kill all its daemonic children.
process.daemon = True
process.start()
elif reload_task in ('thread', 'auto'):
tf.logging.info('Launching reload in a daemon thread')
thread = threading.Thread(target=_reload, name='Reloader')
# Make this a daemon thread, which won't block TB from exiting.
thread.daemon = True
thread.start()
elif reload_task == 'blocking':
if load_interval != 0:
raise ValueError('blocking reload only allowed with load_interval=0')
_reload()
else:
raise ValueError('unrecognized reload_task: %s' % reload_task)
可以看到,其中定义了一个内函数,根据实际参数,会有创建新的进程,创建线程,或者直接调用集中方式调用,但不管怎么调用,函数还是那个函数,所以我们目前对调用方式不用太关系,知道调用了它就好。以上比较重要的两行:
multiplexer.AddRunsFromDirectory(path, name)
multiplexer.Reload()
其中第一行遍历了我们传进来的目录,找了所有该目录下的event文件,之后就是使用Relaod()
进行加载解析。
def Reload(self):
"""Call `Reload` on every `EventAccumulator`."""
tf.logging.info('Beginning EventMultiplexer.Reload()')
self._reload_called = True
# Build a list so we're safe even if the list of accumulators is modified
# even while we're reloading.
with self._accumulators_mutex:
items = list(self._accumulators.items())
names_to_delete = set()
for name, accumulator in items:
try:
accumulator.Reload()
except (OSError, IOError) as e:
tf.logging.error("Unable to reload accumulator '%s': %s", name, e)
except directory_watcher.DirectoryDeletedError:
names_to_delete.add(name)
with self._accumulators_mutex:
for name in names_to_delete:
tf.logging.warning("Deleting accumulator '%s'", name)
del self._accumulators[name]
tf.logging.info('Finished with EventMultiplexer.Reload()')
return self
def Reload(self):
with self._generator_mutex:
for event in self._generator.Load():
self._ProcessEvent(event)
return self
辗转调用到了_ProcessEvent(event)
,而正是该函数,进行真正的解析工作,他讲根据protobuf协议对event文件进行解析,并将解析的内容保存在accumulator
中。
至此,我们已经找到了根源。但是现在还有个疑问,解析后的accumulator存放在哪里呢?答案在items = list(self._accumulators.items())
这一句。在TensorBoard中,每个图的显示是根据run
进行分组的,也就是说有多少个run
就有多少个图。所谓的run
,简单点理解可以理解为logdir的子文件夹。在上面提到的AddRunsFromDirectory
函数中,它将每个run
(子文件夹)的名字与一个accumulator映射起来。这个很重要,因为后续服务器启动后,就是根据每个run
的名字找到对应的accumulator
,进而获取相对应的图的内容来显示。
我们知道了EventMultiplexer
中持有了一组·run,accumulator
的映射,那么EventMultiplexer
,有保存哪里呢?答案在上面提到的函数standard_tensorboard_wsgi
中,在该函数中,EventMultiplexer
先保存在一个TBContext
中,然后传给了相对应的plugin
,这样,最终每个plugin变得到了他能够处理的数据。
剩下的,就是启动服务,并等待收到浏览器发过来的请求啦。