comparison doc/v2_planning/plugin.py @ 1118:8cc324f388ba

proposal for a plugin system
author Olivier Breuleux <breuleuo@iro.umontreal.ca>
date Tue, 14 Sep 2010 16:01:32 -0400
parents
children a1957faecc9b
comparison
equal deleted inserted replaced
1117:c1943feada10 1118:8cc324f388ba
1
2 import time
3 from collections import defaultdict
4
5 inf = float('inf')
6
7 ################
8 ### SCHEDULE ###
9 ################
10
11 class Schedule(object):
12 def __add__(self, i):
13 return OffsetSchedule(self, i)
14 def __or__(self, s):
15 return UnionSchedule(self, to_schedule(s))
16 def __and__(self, s):
17 return IntersectionSchedule(self, to_schedule(s))
18 def __sub__(self, i):
19 return OffsetSchedule(self, -i)
20 def __ror__(self, s):
21 return UnionSchedule(to_schedule(s), self)
22 def __rand__(self, s):
23 return IntersectionSchedule(to_schedule(s), self)
24 def __invert__(self):
25 return NegatedSchedule(self)
26
27 def to_schedule(x):
28 if x in (None, False):
29 return never
30 if x is True:
31 return always
32 elif isinstance(x, (list, tuple)):
33 return reduce(UnionSchedule, x)
34 else:
35 return x
36
37
38 class ScheduleMix(Schedule):
39 __n__ = None
40 def __init__(self, *subschedules):
41 assert (not self.__n__) or len(subschedules) == self.__n__
42 self.subschedules = map(to_schedule, subschedules)
43
44 class UnionSchedule(ScheduleMix):
45 def __call__(self, t1, t2):
46 return any(s(t1, t2) for s in self.subschedules)
47
48 class IntersectionSchedule(ScheduleMix):
49 def __call__(self, t1, t2):
50 return all(s(t1, t2) for s in self.subschedules)
51
52 class DifferenceSchedule(ScheduleMix):
53 __n__ = 2
54 def __call__(self, t1, t2):
55 return self.subschedules[0](t1, t2) and not self.subschedules[1](t1, t2)
56
57 class NegatedSchedule(ScheduleMix):
58 __n__ = 1
59 def __call__(self, t1, t2):
60 return not self.subschedules[0](t1, t2)
61
62 class OffsetSchedule(Schedule):
63 def __init__(self, schedule, offset):
64 self.schedule = schedule
65 self.offset = offset
66 def __call__(self, t1, t2):
67 return self.schedule(t1 - self.offset, t2 - self.offset)
68
69
70 class AlwaysSchedule(Schedule):
71 def __call__(self, t1, t2):
72 return True
73
74 always = AlwaysSchedule()
75 never = ~always
76
77 class IntervalSchedule(Schedule):
78 def __init__(self, step, repeat = inf):
79 self.step = step
80 self.upper_bound = step * (repeat - 1)
81 def __call__(self, t1, t2):
82 if t2 < 0 or t1 > self.upper_bound:
83 return False
84 diff = t2 - t1
85 t1m = t1 % self.step
86 t2m = t2 % self.step
87 return (diff >= self.step
88 or t1m == 0
89 or t2m == 0
90 or t1m > t2m)
91
92 each = lambda step, repeat = inf: each0(step, repeat) + step
93 each0 = IntervalSchedule
94
95
96 class RangeSchedule(Schedule):
97 def __init__(self, low = None, high = None):
98 self.low = low or -inf
99 self.high = high or inf
100 def __call__(self, t1, t2):
101 return self.low <= t1 <= self.high \
102 or self.low <= t2 <= self.high
103
104 inrange = RangeSchedule
105
106
107 class ListSchedule(Schedule):
108 def __init__(self, *schedules):
109 self.schedules = schedules
110 def __call__(self, t1, t2):
111 for t in self.schedules:
112 if t1 <= t <= t2:
113 return True
114 return False
115
116 at = ListSchedule
117 at_start = at(-inf)
118 at_end = at(inf)
119
120
121 ##############
122 ### RUNNER ###
123 ##############
124
125 class scratchpad:
126 pass
127
128 # # ORIGINAL RUNNER, NO TIMELINES
129 # def runner(master, plugins):
130 # """
131 # master is a function which is in charge of the "this" object. It
132 # is in charge of updating the t1, t2 and done fields, It must
133 # take a single argument, this.
134
135 # plugins is a list of (schedule, function) pairs. In-between each
136 # execution of the master function, as well as at the very
137 # beginning and at the very end, the schedule will be consulted
138 # for the time range [t1, t2], and if there is a match, the
139 # function will be called with this as the argument. The order
140 # in which the functions are provided is respected.
141
142 # Note: the reason why we use t1 and t2 instead of just t is that it
143 # gives the master function the ability to run several iterations at
144 # once without consulting any plugins. In that situation, t1 and t2
145 # represent a range, and the schedule must determine if there would
146 # have been an event in that range (we do not distinguish between a
147 # single event and multiple events).
148
149 # For instance, if one is training using minibatches, one could set
150 # t1 and t2 to the index of the lower and higher examples, and the
151 # plugins' schedules would be given according to how many examples
152 # were seen rather than how many minibatches were processed.
153
154 # Another possibility is to use real time - t1 would be the time
155 # before the execution of the master function, t2 the time after
156 # (in, say, milliseconds). Then you can define plugins that run
157 # every second or every minute, but only in-between two training
158 # iterations.
159 # """
160
161 # this = scratchpad()
162 # this.t1 = -inf
163 # this.t2 = -inf
164 # this.started = False
165 # this.done = False
166 # while True:
167 # for schedule, function in plugins:
168 # if schedule(this.t1, this.t2):
169 # function(this)
170 # if this.done:
171 # break
172 # master(this)
173 # this.started = True
174 # if this.done:
175 # break
176 # this.t1 = inf
177 # this.t2 = inf
178 # for schedule, function in plugins:
179 # if schedule(this.t1, this.t2):
180 # function(this)
181
182
183
184
185 def runner(main, plugins):
186 """
187 :param main: A function which must take a single argument,
188 ``this``. The ``this`` argument contains a settable ``done``
189 flag indicating whether the iterations should keep going or
190 not, as well as a flag indicating whether this is the first
191 time runner() is calling main(). main() may store whatever it
192 wants in ``this``. It may also add one or more timelines in
193 ``this.timelines[timeline_name]``, which plugins can exploit.
194
195 :param plugins: A list of (schedule, timeline, function)
196 tuples. In-between each execution of the main function, as
197 well as at the very beginning and at the very end, the
198 schedule will be consulted for the time range [t1, t2] from
199 the appropriate timeline, and if there is a match, the
200 function will be called with ``this`` as the argument. The
201 order in which the functions are provided is respected.
202
203 For any plugin, the timeline can be
204 * 'iterations', where t1 == t2 == the iteration number
205 * 'real_time', where t1 and t2 mark the start of the last
206 loop and the start of the current loop, in seconds since
207 the beginning of training (includes time spent in plugins)
208 * 'algorithm_time', where t1 and t2 mark the start and end
209 of the last iteration of the main function (does not
210 include time spent in plugins)
211 * A main function specific timeline.
212
213 At the very beginning, the time for all timelines is
214 -infinity, at the very end it is +infinity.
215 """
216 start_time = time.time()
217
218 this = scratchpad()
219
220 this.timelines = defaultdict(lambda: [-inf, -inf])
221 realt = this.timelines['real_time']
222 algot = this.timelines['algorithm_time']
223 itert = this.timelines['iterations']
224
225 this.started = False
226 this.done = False
227
228 while True:
229
230 for schedule, timeline, function in plugins:
231 if schedule(*this.timelines[timeline]):
232 function(this)
233 if this.done:
234 break
235
236 t1 = time.time()
237 main(this)
238 t2 = time.time()
239
240 if not this.started:
241 realt[:] = [0, 0]
242 algot[:] = [0, 0]
243 itert[:] = [-1, -1]
244 realt[:] = [realt[1], t2 - start_time]
245 algot[:] = [algot[1], algot[1] + (t2 - t1)]
246 itert[:] = [itert[0] + 1, itert[1] + 1]
247
248 this.started = True
249 if this.done:
250 break
251
252 this.timelines = defaultdict(lambda: [inf, inf])
253
254 for schedule, timeline, function in plugins:
255 if schedule(*this.timelines[timeline]):
256 function(this)
257
258
259
260
261
262 ################
263 ### SHOWCASE ###
264 ################
265
266 def main(this):
267 if not this.started:
268 this.error = 1.0
269 # note: runner will automatically set this.started to true
270 else:
271 this.error /= 1.1
272
273
274 def welcome(this):
275 print "Let's start!"
276
277 def print_iter(this):
278 print "Now running iteration #%i" % this.timelines['iterations'][0]
279
280 def print_error(this):
281 print "The error rate is %s" % this.error
282
283 def maybe_stop(this):
284 thr = 0.01
285 if this.error < thr:
286 print "Error is below the threshold: %s <= %s" % (this.error, thr)
287 this.done = True
288
289 def wait_a_bit(this):
290 time.sleep(1./37)
291
292 def printer(txt):
293 def f(this):
294 print txt
295 return f
296
297 def stop_this_madness(this):
298 this.done = True
299
300 def byebye(this):
301 print "Bye bye!"
302
303 runner(main = main,
304 plugins = [# At the very beginning, print a welcome message
305 (at_start, 'iterations', welcome),
306 # Each iteration from 1 to 10 inclusive, OR each multiple of 10
307 # (except 0 - each() excludes 0, each0() includes it)
308 # print the error
309 (inrange(1, 10) | each(10), 'iterations', print_error),
310 # Each multiple of 10, check for stopping condition
311 (each(10), 'iterations', maybe_stop),
312 # At iteration 1000, if we ever get that far, just stop
313 (at(1000), 'iterations', stop_this_madness),
314 # Wait a bit
315 (each(1), 'iterations', wait_a_bit),
316 # Print bonk each second of real time
317 (each(1), 'real_time', printer('BONK')),
318 # Print thunk each second of time in main() (main()
319 # is too fast, so this does not happen for many
320 # iterations)
321 (each(1), 'algorithm_time', printer('THUNK')),
322 # Announce the next iteration
323 (each0(1), 'iterations', print_iter),
324 # At the very end, display a message
325 (at_end, 'iterations', byebye)])
326
327