HpBandSter源碼分析

BOHB與HyperBand算法

hpbandster.core.worker.Worker#compute 是計算部分

kde_models字典的添加:
hpbandster.optimizers.config_generators.bohb.BOHB#new_result
首先是要滿足一個條件才能往kde_models中添加記錄

		if train_data_good.shape[0] <= train_data_good.shape[1]:
			return
		if train_data_bad.shape[0] <= train_data_bad.shape[1]:
			return

對應論文formula 3:
Nb,l=max(Nmin,qNb)N_{b,l}=max(N_{min},q\cdot N_{b})
Nb,g=max(Nmin,NbNb,l)N_{b,g}=max(N_{min}, N_{b}-N_{b,l})

		self.kde_models[budget] = {
				'good': good_kde,
				'bad' : bad_kde
		}
self.kde_models[9.0]['good']
Out[21]: 
KDE instance
Number of variables: k_vars = 1
Number of samples:   nobs = 2
Variable types:      c
BW selection method: normal_reference
type(self.kde_models[9.0]['good'])
Out[22]: statsmodels.nonparametric.kernel_density.KDEMultivariate

回到run函數:
hpbandster/core/master.py:206

next_run = self.iterations[i].get_next_run()
next_run
Out[32]: ((0, 0, 5), {'x': 0.021475631245521144}, 9.0)

這個三元組是什麼意思呢?可以看到build.lib.hpbandster.core.base_iteration.BaseIteration#get_next_run

return(k, v.config, v.budget)

分別是config_id, config, budget

job是由調度器提交的

self.iterations[i]
Out[38]: <hpbandster.optimizers.iterations.successivehalving.SuccessiveHalving at 0x7f6a400ae9e8>

進入hpbandster.core.base_iteration.BaseIteration#get_next_run

self.data.keys()
Out[40]: dict_keys([(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 0, 3), (0, 0, 4), (0, 0, 5)])

鍵應該都是config_id

type(self.data[(0, 0, 0)])
Out[42]: hpbandster.core.base_iteration.Datum
self.actual_num_configs
Out[46]: [6, 0, 0, 0]
self.stage
Out[47]: 0
self.num_configs
Out[48]: [27, 9, 3, 1]

actual_num_configs表示已經運行的配置數
num_configs 表示應該運行的配置數
stage表示當前的階段

看到hpbandster.core.base_iteration.BaseIteration#add_configuration

self.config_sampler
Out[49]: <bound method BOHB.get_config of <hpbandster.optimizers.config_generators.bohb.BOHB object at 0x7f6a78de66a0>>

難道這就是傳說中的代理模式嗎

取配置:

config, config_info = self.config_sampler(self.budgets[self.stage])

進入hpbandster.optimizers.config_generators.bohb.BOHB#get_config

這個函數對應的是論文的 Algorithm 2, 即用貝葉斯算法採樣的部分

再看下kde_model是怎麼構建的。

hpbandster.optimizers.config_generators.bohb.BOHB#new_result

BOHB總體總體上就是HyperBand,不過採樣部分用的是BO。
HyperBand是一個2層循環,
第一層是iterations,第二層是stages

min_budget:9
max_budget:243
n_iterations:4

iteration : 0
s : 3
n0 : 27
ns : [27, 9, 3, 1]
budgets : [ 9. 27. 81. 243.]
num_configs : [27, 9, 3, 1]
iteration : 1
總budget: 234x4

s : 2
n0 : 9
ns : [9, 3, 1]
budgets : [ 27. 81. 243.]
num_configs : [9, 3, 1]
iteration : 2
總budget: 234x3

s : 1
n0 : 6
ns : [6, 2]
budgets : [ 81. 243.]
num_configs : [6, 2]
iteration : 3
總budget: 234x2

s : 0
n0 : 4
ns : [4]
budgets : [243.]
num_configs : [4]
總budget: 234x4

幾個還沒有搞清楚的問題:

  1. 在一個iteration中,在過度不同的stage時,是否會沿用上一次迭代的configs?
  2. BOHB是根據budget去分組構建概率模型。在不同的iteration中,會用上次budget的kde嗎?
  3. n_iterations與stages的關係?

現在已經弄明白了

  1. 是的,hpbandster.core.base_iteration.BaseIteration#process_results 這個函數是在一次stage結束後,挑選config進入下一次stage的。
  2. 運行歷史一直會保存,當config數足夠多時,KDE模型就會建立,並且永遠都只會用budget最大的KDE模型
  3. n_iterations是用戶指定的,stages是根據max_budget, min_budget, eta決定的,詳見hpbandster.optimizers.bohb.BOHB#__init__

hpbandster.optimizers.config_generators.bohb.BOHB#new_result

		if max(list(self.kde_models.keys()) + [-np.inf]) > budget:
			return

如果kde_models已經有27.0的budget,9.0 budget的樣本點不會觸發更新

self.configs是BOHB的一個成員變量。是一個dict,key是budget。


HpBandSter的分佈式計算模型

Dispatcher

hpbandster.core.dispatcher.Dispatcher#run

self.pyro_daemon.requestLoop()

這個地方會完成BOHB的所有運算。

hpbandster.core.dispatcher.Dispatcher#discover_workers

		while True:
			with Pyro4.locateNS(host=self.nameserver, port=self.nameserver_port) as ns:
				worker_names = ns.list(prefix="hpbandster.run_%s.worker."%self.run_id)

如果檢測到了update,就要調用queue_callback
build.lib.hpbandster.core.master.Master#adjust_queue_size

hpbandster.core.dispatcher.Dispatcher#run函數中,self.discover_workersself.job_runner兩個進程被啓動。

self.discover_workers是個死循環,負責ping NameServer

Worker

調度器負責將Job分配給Worker

hpbandster.core.dispatcher.Worker是調度器負責的。

hpbandster.core.worker.Worker通過uri註冊在Nameserver,通過proxy調用。

self.pyro_ns.locationStr
Out[9]: '127.0.0.1:9090'

命名服務器的port是9090

hpbandster.core.worker.Worker

hpbandster.core.worker.Worker#_run

_run函數是真正的worker入口,如果run的background參數爲True的話,會以線程的方式啓動_run,並且爲daemon進程。如果爲False,直接進入_run

		try:
			with Pyro4.locateNS(host=self.nameserver, port=self.nameserver_port) as ns:
				self.logger.debug('WORKER: Connected to nameserver %s'%(str(ns)))
				dispatchers = ns.list(prefix="hpbandster.run_%s.dispatcher"%self.run_id)
		except Pyro4.errors.NamingError:
			if self.thread is None:
				raise RuntimeError('No nameserver found. Make sure the nameserver is running at that the host (%s) and port (%s) are correct'%(self.nameserver, self.nameserver_port))
			else:
				self.logger.error('No nameserver found. Make sure the nameserver is running at that the host (%s) and port (%s) are correct'%(self.nameserver, self.nameserver_port))
				exit(1)
		except:
			raise

Worker啓動前要保證nameserver已經啓動, 然後去找dispatcher,如果dispatcher存在,就notify dispatcher

dispatchers
Out[2]: {'hpbandster.run_example3.dispatcher': 'PYRO:hpbandster.run_example3.dispatcher@localhost:42927'}
		for dn, uri in dispatchers.items():
			try:
				self.logger.debug('WORKER: found dispatcher %s'%dn)
				with Pyro4.Proxy(uri) as dispatcher_proxy:
					dispatcher_proxy.trigger_discover_worker()

worker啓動後觸發調度器的服務發現線程

Master

注意到如果master申請了2個worker,但是隻有1個worker的話,會:
build.lib.hpbandster.core.master.Master#wait_for_workers

		with self.thread_cond:
			while (self.dispatcher.number_of_workers() < min_n_workers):
				self.logger.debug('HBMASTER: only %i worker(s) available, waiting for at least %i.'%(self.dispatcher.number_of_workers(), min_n_workers))
				self.thread_cond.wait(1)
				self.dispatcher.trigger_discover_worker()
	@Pyro4.expose
	@Pyro4.oneway
	def trigger_discover_worker(self):
		#time.sleep(1)
		self.logger.info("DISPATCHER: A new worker triggered discover_worker")
		with self.discover_cond:
			self.discover_cond.notify()

notify之後,會觸發hpbandster.core.dispatcher.Dispatcher#discover_workers線程

現在比較疑惑的是Job對象是怎麼分配到Worker中的

hpbandster.core.dispatcher.Dispatcher#job_runner

submit之後觸發notify,進而觸發hpbandster.core.dispatcher.Dispatcher#job_runner函數。

worker.proxy.start_computation(self, job.id, **job.kwargs)
job
Out[2]: 
job_id: (0, 0, 0)
kwargs: {'config': {'x': 0.2470809682601386}, 'budget': 9.0, 'working_directory': '.'}
result: None
exception: None

hpbandster.core.worker.Worker#start_computation

	@Pyro4.expose
	@Pyro4.oneway
	def start_computation(self, callback, id, *args, **kwargs):
callback
Out[2]: <Pyro4.core.Proxy at 0x7fea97d86f98; not connected; for PYRO:hpbandster.run_example3.dispatcher@localhost:35075>
id
Out[3]: (0, 0, 13)
args
Out[4]: ()
kwargs
Out[5]: 
{'config': {'x': 0.003100013139489973},
 'budget': 9.0,
 'working_directory': '.'}

callback是調度器

worker完成任務發送變量到hpbandster.core.dispatcher.Dispatcher#register_result,這裏會對結果進行封裝,爲Job對象。

並且會對woker_pool等數據結構做調整。

self.new_result_callback(job)

build.lib.hpbandster.core.master.Master#__init__

self.dispatcher = Dispatcher( self.job_callback...

new_result_callback是master中的job_callback函數。

build.lib.hpbandster.core.master.Master#job_callback

self.config_generator.new_result(job)
self.config_generator
Out[2]: <hpbandster.optimizers.config_generators.bohb.BOHB at 0x7f809ae63eb8>

hpbandster.optimizers.config_generators.bohb.BOHB#new_result

接下來就是熟悉的建KDE模型環節。


整體流程

結合基類的hpbandster.core.master.Master#run和父類hpbandster.optimizers.bohb.BOHB以及Example3看整個過程。

首先是啓動命名服務

# Start a nameserver (see example_1)
NS = hpns.NameServer(run_id='example3', host='127.0.0.1', port=None)
NS.start()

然後啓動2個worker

def run():
	w = MyWorker(sleep_interval = 0.5, nameserver='127.0.0.1',run_id='example3')
	w.run(background=False)
for i in range(args.n_workers):
	process=multiprocessing.Process(target=run,args=())
	process.start()

疑惑:worker的run需要檢測dispatcher是否存在
dispatcher是在master中啓動的
hpbandster.core.master.Master#__init__

		self.dispatcher = Dispatcher( self.job_callback, queue_callback=self.adjust_queue_size, run_id=run_id, ping_interval=ping_interval, nameserver=nameserver, nameserver_port=nameserver_port, host=host)

		self.dispatcher_thread = threading.Thread(target=self.dispatcher.run)
		self.dispatcher_thread.start()

仔細看了下,要的是命名服務而不是dispatcher,woker啓動後沒有dispatcher的情況還需要調研。

回到master的運行部分,首先需要等待worker

self.wait_for_workers(min_n_workers)

當master要求的最低worker數滿足後,進入主循環。
主循環是個死循環,首先做個_queue_wait

job_queue_size: tuple of ints
min and max size of the job queue.

	def _queue_wait(self):
		if self.num_running_jobs >= self.job_queue_sizes[1]:
			while(self.num_running_jobs > self.job_queue_sizes[0]):
				self.logger.debug('HBMASTER: running jobs: %i, queue sizes: %s -> wait'%(self.num_running_jobs, str(self.job_queue_sizes)))
				self.thread_cond.wait()

雖然queue_size這個值在開始被設爲一個很扯淡的(-1, 0),但是會在hpbandster.core.master.Master#adjust_queue_size函數(這個函數會被dispatcher當做callback來用)

進入ifblock的條件是當前已經運行的job數>=worker數。
然後進入while循環,只有當<=最小worker數(一般爲1)才退出
循環中有個thread_cond的鎖。notify在哪觸發的暫時還沒看到。

看了下打印的日誌,兩個iteration之間沒有很強的同步關係

self.iterations是master維護的一個列表,每個都是一個hpbandster.core.base_iteration.BaseIteration對象,這個對象的子對象一般是hpbandster.optimizers.iterations.successivehalving.SuccessiveHalving,值得調研如何熱啓動:hpbandster.core.base_iteration.WarmStartIteration

master的hpbandster.core.master.Master#active_iterations返回還在運行的iteration index

next_run = self.iterations[i].get_next_run()

BaseIteration內部封裝了stage等數據結構,每個BaseIteration可以視爲一個SuccessiveHalvingBaseIteration對象通過get_next_run推薦下個配置(next_run是個元組,包含config_id, config, budget)。

如果發現所有的iterations都不能給我答案,那我就創建下個iteration,即

self.get_next_iteration(len(self.iterations), iteration_kwargs)

而BOHB正是繼承了master,然後overwrite了get_next_iteration

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章