Mercurial > pylearn
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 |