Mercurial > pylearn
view doc/v2_planning/layer_RP.txt @ 1234:3e264967d4e3
updated commity member
author | Frederic Bastien <nouiz@nouiz.org> |
---|---|
date | Thu, 23 Sep 2010 10:23:40 -0400 |
parents | 5ef96142492b |
children | 32fc5f442dde |
line wrap: on
line source
=============== Layer committee =============== Members : RP, XG, AB, DWF Proposal (RP) ============= You construct your neural network by constructing a graph of connections between layers starting from data. While you construct the graph, different theano formulas are put together to construct your model. Hard details are not set yet, but all members of the committee agreed that this sound as a good idea. Example Code (RP): ------------------ # Assume you have the dataset as train_x, train_y, valid_x, valid_y, test_x, test_y h1 = sigmoid(dotW_b(train_x, n = 300)) rbm1 = CDk( h1, train_x, k=5, sampler = binomial, cost = pseudolikelihood) h2 = sigmoid(dotW_b(h1, n = 300)) rbm2 = CDk( h2, h1, k=5, sampler = binomial, cost= pseudolikelihood) out = sigmoid( dotW_b(h2, n= 10)) train_err = cross_entropy( out, train_y) grads = grad( train_err, err.parameters() ) learner = SGD( err, grads) valid_err = train_err.replace({ train_x : valid_x, train_y : valid_y}) test_err = train_err.replace({ train_x : test_x , train_y : test_y}) Global observations : --------------------- 1) Your graph can have multiple terminal nodes; in this case rbm1, rbm2 and learner, valid_err, test_err are all end nodes of the graph; 2) Any node is an "iterator", when you would call out.next() you would get the next prediction; when you call err.next() you will get next error ( on the batch given by the data.next() ). 3) Replace can replace any subgraph 4) You can have MACROS or SUBROUTINE that already give you the graph for known components ( in my view the CDk is such a macro, but simpler examples will be vanilla versions of MLP, DAA, DBN, LOGREG) 5) Any node has the entire graph ( though arguably you don't use that graph too much). Running such a node in general will be done by compiling the Theano expression up to that node( if you don't already have this function), and using the data object that you get initially. This theano function is compiled only if you need it. You use the graph only to : * update the Theano expression in case some part of the subgraph has changed (hyper-parameter or a replace call) * collect the list of parameters of the model * collect the list of hyper-parameters ( my personal view - this would mostly be useful for a hyper learner .. and not for day to day stuff, but I think is something easy to provide and we should ) * collect constraints on parameters ( I believe they can be represented in the graph as dependency links to other graphs that compute the constraints..) 6) Registering parameters and hyper-parameters to the graph is the job of the transform and therefore of the user who implemented that transform; the same for initializing the parameters ( so if we have different way to initialize the weight matrix that might be a hyperparameter with a default value) Detailed Proposal (RP) ====================== I would go through a list of scenarios and possible issues : Delayed or feature values ------------------------- Sometimes you might want future values of some nodes. For example you might be interested in : y(t) = x(t) - x(t-1) You can get that by having a "delayed" version of a node. A delayed version a node x is obtained by calling x.t(k) which will give you a node that has the value x(t+k). k can be positive or negative. In my view this can be done as follows : - a node is a class that points to : * a data object that feeds data * a theano expression up to that point * the entire graph that describes the model ( not Theano graph !!!) The only thing you need to do is to change the data object to reflect the delay ( we might need to be able to pad it with 0?). You need also to create a copy of the theano expression ( those are "new nodes" ) in the sense that the starting theano tensors are different since they point to different data. Non-theano transformation ( or function or whatever) ---------------------------------------------------- Maybe you want to do something in the middle of your graph that is not Theano supported. Let say you have a function f which you can not write in Theano. You want to do something like W1*f( W2*data + b) I think we can support that by doing the following : each node has a: * a data object that feeds data * a theano expression up to that point * the entire graph that describes the model Let x1 = W2*data + b up to here everything is fine ( we have a theano expression ) dot(W2, tensor) + b, where tensor is provided by the data object ( plus a dict of givens and whatever else you need to compile the function) When you apply f, what you do you create a node that is exactly like the data object in the sense that it provides a new tensor and a new dict of givens so x2 = W1*f( W2*data+b) will actually point to the expression dot(W1, tensor) and to the data node f(W2*data+b) what this means is that you basically compile two theano functions t1 and t2 and evaluate t2(f(t1(data))). So everytime you have a non theano operation you break the theano expression and start a new one. What you loose : - there is no optimization or anything between t1,t2 and f ( we don't support that) - if you are running things on GPU, after t1, data will be copied on CPU and then probably again on GPU - so it doesn't make sense anymore Recurrent Things ---------------- I think that you can write a recurrent operation by first defining a graph ( the recrrent relation ): y_tm1 = recurrent_layer(init = zeros(50)) x_t = slice(x, t=0) y = loop( dotW_b(y_tm1,50) + x_t, steps = 20) This would basically give all the information you need to add a scan op to your theano expression of the result op, it is just a different way of writing things .. which I think is more intuitive. You create your primitives which are either a recurrent_layer that should have a initial value, or a slice of some other node ( a time slice that is) Then you call loop giving a expression that starts from those primitives. Similarly you can have foldl or map or anything else. You would use this instead of writing scan especially if the formula is more complicated and you want to automatically collect parameters, hyper-parameters and so on. Optimizer --------- Personally I would respect the findings of the optimization committee, and have the SGD to require a Node that produces some error ( which can be omitted) and the gradients. For this I would also have the grad function which would actually only call T.grad. If you have non-theano thing in the middle? I don't have any smart solution besides ignoring any parameter that it is below the first non-theano node and throw a warning. Learner ------- In my case I would not have a predict() and eval() method of the learner, but just a eval(). If you want the predictions you should use the corresponding node ( before applying the error measure ). This was for example **out** in my first example. Of course we could require learners to be special nodes that also have a predict output. In that case I'm not sure what the iterating behaiour of the node should produce. Granularity ----------- Guillaume nicely pointed out that this library might be an overkill. In the sense that you have a dotW_b transform, and then you will need a dotW_b_sparse transform and so on. Plus way of initializing each param would result in many more transforms. I don't have a perfect answer yet, but my argument will go as this : you would have transforms for the most popular option ( dotW_b) for example. If you need something else you can always decorate a function that takes theano arguments and produces theano arguments. More then decoratting you can have a general apply transform that does something like : apply( lambda x,y,z: x*y+z, inputs = x, hyperparams = [(name,2)], params = [(name,theano.shared(..)]) The order of the arguments in lambda is nodes, params, hyper-params or so. This would apply the theano expression but it will also register the the parameters. It is like creating a transform on the fly. I think you can do such that the result of the apply is pickable, but not the apply operation. Meaning that in the graph, the op doesn't actually store the lambda expression but a mini theano graph. Also names might be optional, so you can write hyperparam = [2,] What this way of doing things would buy you hopefully is that you do not need to worry about most of your model ( would be just a few macros or subrutines). you would do something like : rbm1,hidden1 = rbm_layer(data,20) rbm2,hidden2 = rbm_layer(data,20) and then the part you care about : hidden3 = apply( lambda x,W: T.dot(x,W), inputs = hidden2, params = theano.shared(scipy.sparse_CSR(..))) and after that you pottentially still do what you did before : err = cross_entropy(hidden3, target) grads = grad(err, err.paramters()) ... I do agree that some of the "transforms" that I have been writing here and there are pretty low level, and maybe we don't need them. We might need only somewhat higher level transforms. My hope is that for now people think of the approach and not about all inner details ( like what transforms we need and so on) and see if they are comfortable with it or not. Do we want to think in this terms? I think is a bit better do have a normal python class, hacking it to change something and then either add a parameter to init or create a new version. It seems a bit more natural. Anyhow Guillaume I'm working on a better answer :) Params and hyperparams ---------------------- I think it is obvious from what I wrote above that there is a node wrapper around the theano expression. I haven't wrote down all the details of that class. I think there should be such a wrapper around parameters and hyper-parameters as well. By default those wrappers might not provide any informtion. Later on, they can provide for hyper-params for example a distribution. If when inserting your hyper-param in the graph ( i.e. when you call a given transform) you provide the distribution then maybe a hyperlearner could use it to sample from it. For parameters you might define properties like freeze. It can be true or false. Whenever it is set to true, the param is not adapted by the optimizer. Changing this value like changing most of hyper-params implies recompilation of the graph. I would have a special class of hyper-params which don't require recompilation of the graph. Learning rate is an example. This info is also given by the wrapper and by how the parameter is used. It is up to the user and "transform" implementer to wrap params and hyper-params correspondingly. But I don't think this is to complicated. The apply function above has a default behaviour, maybe you would have a forth type of argument which is hyper-param that doesn't require compilation. We could find a nice name for it. How does this work? ------------------- You always have a pointer to the entire graph. Whenever a hyper-param changes ( or a param freezes) all region of the graph affected get recompiled. This is by traversing the graph from the bottom node and constructing the theano expression. This function that updates / re-constructs the graph is sligthly more complex if you have non-theano functions in the graph .. replace ------- Replace, replaces a part of the graph. The way it works in my view is that if I write : x = x1+x2+x3 y = x.replace({x2:x5}) You would first copy the graph that is represented by x ( the params or hyper-params are not copied) and then replace the subgraphs. I.e., x will still point to x1+x2+x3, y will point to x1+x5+x3. Replace is not done inplace. I think these Node classes as something light-weighted, like theano variables. reconstruct ----------- This is something nice for DAA. It is definetely not useful for the rest. I think though that is a shame having that transformation graph and not being able to use it to do this. It will make life so much easier when you do deep auto-encoders. I wouldn't put it in the core library, but I would have in the DAA module. The way I see it you can either have something like # generate your inversable transforms on the fly fn = create_transform(lambda : , params, hyper-params ) inv = create_transform(lambda : , params, hyper-params ) my_transform = couple_transforms( forward = fn, inv = inv) # have some already widely used such transform in the daa submodule. transforms ---------- In my view there will be quite a few of such standard transforms. They can be grouped by architecture, basic, sampler, optimizer and so on. We do not need to provide all of them, just the ones we need. Researching on an architecture would actually lead in creating new such transforms in the library. There will be definetely a list of basic such transforms in the begining, like : replace, search, get_param(name) get_params(..) You can have and should have something like a switch ( that based on a hyper parameter replaces a part of a graph with another or not). This is done by re-compiling the graph. Constraints ----------- Nodes also can also keep track of constraints. When you write y = add_constraint(x, sum(x**2)) y is the same node as x, just that it also links to this second graph that computes constraints. Whenever you call grad, grad will also sum to the cost all attached constraints to the graph.