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:
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
幾個還沒有搞清楚的問題:
- 在一個iteration中,在過度不同的stage時,是否會沿用上一次迭代的configs?
- BOHB是根據budget去分組構建概率模型。在不同的iteration中,會用上次budget的kde嗎?
- n_iterations與stages的關係?
現在已經弄明白了
- 是的,
hpbandster.core.base_iteration.BaseIteration#process_results
這個函數是在一次stage結束後,挑選config進入下一次stage的。 - 運行歷史一直會保存,當config數足夠多時,KDE模型就會建立,並且永遠都只會用budget最大的KDE模型
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_workers
和self.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來用)
進入if
block的條件是當前已經運行的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
可以視爲一個SuccessiveHalving
,BaseIteration
對象通過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