tag:blogger.com,1999:blog-31953525109246115042017-09-19T14:30:23.536-04:00Sergey Babkin on CEPMy thoughts on the field of Complex Event Processing. Since I'm a technical guy, they go mostly on the technical side. And mostly about my OpenSource project Triceps. To read the Triceps documentation, please follow from the oldest posts to the newest ones.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.comBlogger355125tag:blogger.com,1999:blog-3195352510924611504.post-48612191164925422422017-09-19T04:23:00.000-04:002017-09-19T14:30:23.568-04:00Reversible computingI've read an article in the IEEE magazine about he reversible computing: <a href="https://spectrum.ieee.org/computing/hardware/the-future-of-computing-depends-on-making-it-reversible">https://spectrum.ieee.org/computing/hardware/the-future-of-computing-depends-on-making-it-reversible</a><br /><br />I've never thought about it before, that the normal switching in the circuits dissipates the energy continuously, and the way to fix it would be to make sure that the energy is always shuffled around and never dissipated (although of course there will always be the "friction losses"). I haven't found that much other information on the Internet. So I've started thinking about how such a reversible computing can be implemented, based on the example from the article. And I think I've come up with a decently simple implementation on the digital level, not sure why the article says that it's complicated. Of course, it might end up extremely complicated in the implementation, I know nothing about that. So, here is what I've come up with:<br /><br />The first obvious problem was how do the values ("billiard balls" from the mechanical analogy) get returned back? That has a kind of obvious fundamental solution: in all the digital automata the information fundamentally goes in circles like this:<br /><br /><pre>.<br /> +-----+ <br /> | |---> output <br /> | | <br /> | | +------+<br /> | | | |<br />input --->|logic|--->|memory|--+<br /> | | | | |<br /> +-->| | +------+ |<br /> | +-----+ | <br /> +------------------------+<br /></pre><br />Well, <i>mostly</i> goes in circles, there are inputs and outputs. So the next problem is how to make the inputs and outputs work. Note that the inputs don't come from nowhere, and outputs don't go into nowhere, instead they connect to the other circuits. So the inputs and outputs can be also made in the form of cycles, with the interaction between the output of one circuit and input of another circuit:<br /><br /><br /><pre>.<br />output --->-+ | +-<-- recycle<br /> | > |<br />recycle <---+ | +---> input<br /></pre><br /><br /><br />The output/input interaction can be represented as a function with the inputs:<br /><br />x - the output value<br />1 - the constant 1<br /><br />and outputs:<br /><br />rx - recycling of the output value<br />i - the input sent to the circuit<br />ri - recycling of the input value<br /><br />The truth table for this function will be:<br /><br /><pre>x 1 | rx i ri<br />==================<br />0 1 | 0 0 1<br />1 1 | 1 1 0<br /></pre><br />As you can see, the number of 1s in the output always matches the number of 1s in the inputs. If x=1, the constant 1 gets shipped to the input, otherwise it goes to the recycling and back into the constant. <br /><br />The next problem is the memory: it has to store a value that might be 0 or 1, which is obviously not symmetric. But it looks like the basic solution for that is easy: for each memory cell keep 2 sub-cells, one of them storing the value, and another one its complement. So there will always be one packet of energy stored, and the value would be determined by the sub-cell where it's stored. Then the contents of one sub-cell would be sent out for the next step of computations, and the other sub-cell will stay until the new value gets stored. I guess this could also be expressed equivalently as a constant 1 that gets connected to one or another output of the memory unit.<br /><br />How would the recycling be done? For that we've got to connect the 1s in the results back to where they came from. And ultimately they all come from the constant 1s, either introduced in the computations or use din the logic of the inputs. For all I can tell, by the time the bits get recycled, they get really mixed up, and there is no easy way to put it back. The only universal way I see to put them back would be to order all the original "constant 1s" and all the recycled values (the specific order doesn't matter, just there has to be some order) and then do the logic like this:<br /><br />If the 1st recycled value is 1, put it into the 1st "constant 1".<br />If the 2nd recycled value is 1 { <br /> If the 1st "constant 1" is still 0, put the 2nd recycled value into it,<br /> else put the 2nd recycled value into the 2nd "constant 1".<br />}<br />If the 3rd recycled value is 1 { <br /> If the 1st "constant 1" is still 0, put the 3rd recycled value into it,<br /> else if the 2nd "constant 1" is still 0, put the 3rd recycled value into it,<br /> else put the 3rd recycled value into the 3rd "constant 1".<br />}<br /><br />and so on. As you can see, the logic can be parallelized to some extent but still will require as many steps as there are input values. So for the large circuits it becomes quite slow. Maybe this can be optimized in some way for some specific applications but that would be quite difficult. Maybe that's what they mean when they say that the design of the reversible circuits is difficult.<br /><br />But I think that there is a simple solution: make each individual circuit small. Maybe limit it to 2 inputs and possibly one additional constant 1. That would limit the recycling to 3 steps, which is reasonably fast. Then use the input/output method described above to connect these small circuits into the large circuits. Then as the inputs propagate through the layers of these simple circuits, the early layers would compute their recycling in parallel with that propagation, so overall there will be very little penalty.<br /><br />Note that there would be no memory between these layers of small circuits, it would be mostly layers of the small circuits, with one memory layer at the end. <br /><br />The classic digital electronics has the implementation of functions like AND and OR with a large number of inputs, not just 2, that get computed in the same time as a function with 2 inputs, which comes quite handy. This won't be that easy for the reversible circuits. But there probably is no point in trying to do this for the reversible circuits in the first place: even if a function of N arguments would get computed in one step, then it would still have to do the recycling in N steps. On the other hand, such a function of N arguments can be built as a tree hierarchy of 2-argument functions, of the depth log2(N). This tree would be computed in log2(N) steps and the recycling of the last element will take 5 steps (the recycling of the previous elements could be done in parallel). The number 5 comes from a logical operation on 2 inputs producing 3 outputs to stay reversible, as given in an example in the article (and I suspect that it's an universal property of such operations), plus 2 inputs having 1 recycling output each. So the total time would be (log2(N)+5) which is better than N. Even if we include the I/O overhead, that would still be (2*log2(N)+5) which is still better than N.<br /><br />Thus I think that the basic design of the reversible circuits on the digital level with the "billiard ball analogs" looks not that difficult. It might be that I've missed something and got everything wrong. And of course, the implementation on the level of the underlying electronic elements like transistors might be very difficult. I'm still trying to figure out how it could be done, and since my knowledge about the transistors is fairly limited, it's difficult for me. I have some ideas but I'd need to think some more about them before writing them down (and they might be completely wrong anyway).<br /><br />A bit on a side subject, about the logic. In the article they use the the example of function [(NOT A) AND B] which looks kind of unconventional (but maybe they have some good reasons, one of them being the convenience of producing both the result and its complement). Well, there is more than one way to create a set of operations that can represent all the boolean functions. One is the set of (AND, OR, NOT), then (AND-NOT) and (OR-NOT) do everything in one operation(and perhaps the [(NOT A) AND B] does that too, I forgot how to check that), and yet another set is (XOR, constant 1). The XOR gate looks more straightforward to me, it was the first one I've thought of. And it turns out that XOR is quite popular in the reversible computing, it's known as the Toffoli gate. But in the end it probably doesn't matter much. Or maybe the best way is to use the set of (AND, OR, NOT). The reason for that being that I think OR of 2 arguments can get by with only 2 outputs, and so does NOT, only AND requiring 3 outputs. Which means one fewer recycling step after the OR, which means a slightly higher performance and about 10% (half of the saved 20%) simpler recycling circuitry compared to the situation when all the circuitry is built of one universal function like (AND-NOT) or (OR-NOT) or (XOR).Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-16640625597196955622017-09-06T13:29:00.001-04:002017-09-06T13:29:36.543-04:00AdaBoost 10: a better metric?Here is one more consequence I've noticed from the application of AdaBoost approach to the Bayesian one: it can be applied the other way around too.<br /><br />I.e. AdaBoost provides the list of partial hypotheses and weights for them. But at some point it tends to start repeating the same hypotheses over and over again, because a set of hypotheses taken together gives some gain, and applying it multiple times seemingly increases this gain. This gain is subject to the same problems as the Bayesian computations with the duplicate events: it increases the confidence only seemingly. So the same solution can be applied to it:<br /><br />After the set of partial AdaBoost hypotheses has been computed, they can be re-ordered in such a way as to remove the duplication, by selecting on every step the partial hypotheses with the <i>smallest</i> effect. This "run order" can be kept and updated on every step, instead of the AdaBoost's "selection order". So when the sequence will start repeating, a duplicate hypothesis would add nothing to the cumulative effect of the "run order", and at this point the selection algorithm should try a different hypothesis (or simply stop).<br /><br />I guess, in case if the selection algorithm doesn't stop but tries to do the next best pick (and then maybe another one if the second choice is no good either), it becomes fundamentally more complicated: on each iteration of AdaBoost it should select not simply the hypotheses that maximizes the effect of the whole sequence when appended to the end of it but one that maximizes the effect of the whole sequence when inserted at the position in the sequence where it adds the least effect. Each such insert could also cause a re-ordering of the part of the sequence that follows it.<br /><br />Which sounds even more heavy-weight than the original AdaBoost selection of the hypothesis. On the other hand, since the AdaBoost selection is already so heavy-weight, maybe this would make little extra overhead. I don't know how to analyze it yet.<br /><br /><br />Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-13868519165132836432017-08-01T01:34:00.000-04:002017-08-01T01:34:15.516-04:00Bayes 27 & AdaBoost: another problem with partial confidenceThe solution from the previous installment can handle the partial confidence but it has its own issue. To demonstrate it, let's look at a run with the table from the part 26 but where both of the fully-correlated events E1 and E2 get a confidence of 0.7:<br /><br /><pre>$ perl <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/ex26_01run.pl">ex26_01run.pl</a> -n -p 1 tab24_01.txt in27_01_02.txt <br />Original training cases:<br />! , ,E1 ,E2 ,E3 ,<br />hyp1 ,1.00000,1.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,1.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,0.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,1.00000/1.00,1.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,0.00000/1.00,<br />AdaBoostian table:<br />! , ,E1 ,E2 ,E3 ,<br />hyp1 ,3.00000,0.66667^0.66667,0.66667^0.66667,0.40000^0.33333,<br />+ , , ,0.50000^0.50000,0.40000^0.33333,<br />hyp2 ,3.00000,0.33333^0.33333,0.33333^0.33333,0.60000^0.66667,<br />+ , , ,0.50000^0.50000,0.60000^0.66667,<br />--- Initial weights<br />hyp1 w=3.00000 p=0.50000<br />hyp2 w=3.00000 p=0.50000<br />--- Applying event E1, c=0.700000<br />hyp1 w=1.70000 p=0.56667<br />hyp2 w=1.30000 p=0.43333<br />--- Applying event E2, c=0.700000<br />hyp1 w=0.91800 p=0.60554<br />hyp2 w=0.59800 p=0.39446<br />--- Applying event E3, c=1.000000$ perl ex27_01run.pl -n -p 1 tab24_01.txt in27_01_02.txt<br />hyp1 w=0.36720 p=0.50579<br />hyp2 w=0.35880 p=0.49421<br /></pre><br />The probabilities move after applying E1, after E2 they move again. The interesting question is, should have they moved after E2? It really depends on how E1 and E2 have been measured: are their confidences derived independently or tracking back to the same measurement?<br /><br />The code assumes that they are independent, so if both are independently measured at the confidence of 0.7, together they provide a higher confidence. But if they both come from the same test, this assumption is wrong.<br /><br />If they came from the same test then E2 carries no additional information after E1 has been processed. In this case we should have treated the value C(E2)=0.7 (i.e. C(E2)=C(E1)) as having the meaning "I don't know", and then the differences from it would carry the new information.<br /><br />But this approach has its own problems. First of all, it's fine if the confidence values C(E) represent the measured probabilities. But if they represent some expert estimation then an expert saying "I don't know" and giving the confidence of 0.5 would shift the probabilities instead of leaving them unchanged. Well, it it could be re-scaled automatically but then how do we know that the expert means "I don't know" and not "I've carefully considered everything and I think that it's a 50-50 chance"? So it sounds like getting it right would require using two separate numbers: the absolute confidence that tells how sure we are of the estimation, and separately the estimation. Which would create all kinds of messes.<br /><br />The second problem, if we want to do such a re-scaling, to which value do we re-scale? It's reasonably easy if two events are copies of each other but what if the correlation is only partial? There actually is an a rather easy answer to the problem: the needed value would be P(E), the probability that the event is true at that point (after the previous events have been considered). I've actually went and modified the code to compute the P(E) based on the weights only to find out that I've forgotten an important property of the AdaBoost logic. Let me show its output:<br /><br /><pre>$ perl <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/ex27_01run.pl">ex27_01run.pl</a> -n -p 1 tab24_01.txt in27_01_02.txt<br />Original training cases:<br />! , ,E1 ,E2 ,E3 ,<br />hyp1 ,1.00000,1.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,1.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,0.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,1.00000/1.00,1.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,0.00000/1.00,<br />AdaBoostian table:<br />! , ,E1 ,E2 ,E3 ,<br />hyp1 ,3.00000,0.66667^0.66667@0.66667,0.66667^0.66667@0.66667,0.40000^0.33333@0.50000,<br />+ , , ,0.50000^0.50000@0.50000,0.40000^0.33333@0.50000,<br />hyp2 ,3.00000,0.33333^0.33333@0.33333,0.33333^0.33333@0.33333,0.60000^0.66667@0.75000,<br />+ , , ,0.50000^0.50000@0.50000,0.60000^0.66667@0.75000,<br />--- Initial weights<br />hyp1 w=3.00000 p=0.50000<br />hyp2 w=3.00000 p=0.50000<br />--- Applying event E1, c=0.700000<br />Expected P(E)=0.500000<br />hyp1 w=1.70000 p=0.56667<br />hyp2 w=1.30000 p=0.43333<br />--- Applying event E2, c=0.700000<br />Expected P(E)=0.513333<br />hyp1 w=0.91800 p=0.60554<br />hyp2 w=0.59800 p=0.39446<br />--- Applying event E3, c=1.000000<br />Expected P(E)=0.598615<br />hyp1 w=0.36720 p=0.50579<br />hyp2 w=0.35880 p=0.49421<br /></pre><br />The computation table now has one more element printed after "@", the estimation of event's probablity for the particular branch based on the previous events. During the computation these values get mixed together based on the weight of the branches. But notice that when applying E2, the message says:<br /><br /><pre>Expected P(E)=0.513333</pre><br />It's 0.513333, not 0.7! The reason is two-fold. First, AdaBoost works by adjusting the weights of the training cases such as to bring the probability of the last seen event to 0.5. It just does the correction opposite to what I was trying to achieve. Second, these are the wrong probabilities in the table. These probabilities are computed per-branch based on the <i>absolute</i> confidence. They don't care if the actual confidence was 0.7 or 0.3, these values are the same from the standpoint of the absolute confidence. But this difference matters a lot when trying to predict P(E)!<br /><br />Instead we need to honestly compute the P(E) based on the previous events, not per the AdaBoostian table but per the original training cases. But if we do that, we end up again with a table of exponential size. The trick with ignoring all the events but the last few might still work though. The idea would be similar: since we try to order the closely-correlated events close to each other, the previous few events would be the ones with the strongest effects. And if the farther events aren't closely correlated, they won't have a big effect anyway, so they can be ignored. Maybe I'll try this next. Or maybe it's simply not worth the trouble.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-52061517803800109112017-07-10T02:43:00.000-04:002017-07-10T02:43:01.875-04:00Bayes 26 & AdaBoost: more on the partial confidenceI think I've found at least a partial solution to the <a href="http://babkin-cep.blogspot.com/2017/03/bayes-25-adaboost-partial-confidence.html">problem of partial confidence</a> that avoids going exponential.<br /><br />It grows right out of the problem definition: the problem really exists only for the closely related events. If two events are unrelated, then the confidence of one wouldn't affect the other in any noticeable way. But for the related events, the first one takes up the bulk of the effect, and then the effect of the following events becomes greatly diminished. If two events are actually the same (like E1 and E2 in tab24_01.txt in the <a href="http://babkin-cep.blogspot.com/2017/03/bayes-24-adaboost-as-solution-to.html">part 24</a>), the second event gets stripped of any effect whatsoever. But if at the execution time we find that we have no information about the first event and good information about the second event, it's too late to make use of the second event because its probabilities are already hardcoded in the table and condemn it to having no effect. But if we could change the probability at run time then we could use the second event instead. Better yet, the probabilities of the following events would be unaffected because they don't care which one of the duplicate events provided the information. For a sequence of not equal but closely correlated events, if the first one is excluded from the table preparation, the others still seem to converge in their effects to something close to the original sequence, and the following events don't get affected that much.<br /><br />Since the logic in the <a href="http://babkin-cep.blogspot.com/2017/03/bayes-24-adaboost-as-solution-to.html">part 24</a> assumes that the closely-related events would be placed in the sequence next to each other, the solution might be to compute an event's table entry in multiple versions, depending on whether the values of a limited number of the previous events are known or not. It would still be exponential for this number of previous events. But since we expect a fast convergence and hope that not too many event values will be unknown, this number of previous events can be small, and the exponent of it would be reasonably small too.<br /><br />By the way, I've come up with a name for the K(E) described in the previous part: the "absolute confidence". The confidence C(E) has the range from 0 showing that we're completely sure that the event is false, though 0.5 showing that we have no idea, to 1 showing that the event is true. The absolute confidence<br /><br />K(E) = abs(2*C(E) - 1)<br /><br />has the range from 0 showing that we have no idea to 1 showing that we're completely sure that the event is true or false.<br /><br />I had to draw a picture to figure it out how it would work. Suppose that we want to take 3 previous events into the consideration. The datapoints with the probability values for these events can then be seen as nodes of a tree, with the nodes for each event aligned vertically under the event name:<br /><br /><pre>E1 E2 E3 E4 E5<br /> +------o<br /> 0/<br /> +-----o<br /> 0/ 1\<br /> / +------o<br /> +----o<br /> / \ +------o<br /> 0/ 1\ 0/<br /> / +-----o<br /> / 1\<br /> / +------o <br />o <br /> \ 0+-------o<br /> \ +------o<<br /> \ 0/ 1+-------o<br /> 1\ +----o<br /> \ / 1\ 0+-------o<br /> \ 0/ +------o<<br /> \ / 1+-------o<br /> +--o <br /> \ 0+-------o<br /> 1\ +------o<<br /> \ 0/ 1+-------o<br /> +----o<br /> 1\ 0+-------o<br /> +------o<<br /> 1+-------o<br /></pre><br />E1 still has only one probability value because it has no preceding events. But E2 has two possible probabilities, depending on whether E1 was known (K(E1)=0) or unknown (K(E1)=1). E3 has four probabilities, depending on the possible combinations of E1 and E2. And E4 has eight probabilities.<br /><br />When we come to E5, it can't just continue branching exponentially.It should depend on only three previous events, so we drop the dependency on E1. This is done by abandoning the subtree with K(E1)=0 and continuing branching the subtree with K(E1)=1. This is a reasonable thing to do because we assume that the combination {0, 0, 0} "should never happen" (i.e no more than 3 events in a row would be unknown, otherwise use a higher value of depth), and the other mirroring pairs of branches of {0, b, c} and {1, b, c} would converge to about the same probability value. Thus the events starting from E5 would also have 8 probability values each.<br /><br />Then when we do the computation on the built table, we can have any K(E) in range [0, 1], so we do an interpolation between the branches. Suppose we're computing E4, and have 8 branches {E1, E2, E3} to interpolate. We do it by adding them up with the following weights:<br /><br />W{0, 0, 0} = (1-K(E1))*(1-K(E2))*(1-K(E3))<br />W{0, 0, 1} = (1-K(E1))*(1-K(E2))*K(E3)<br />W{0, 1, 0} = (1-K(E1))*K(E2)*(1-K(E3))<br />W{0, 1, 1} = (1-K(E1))*K(E2)*K(E3)<br />W{1, 0, 0} = K(E1)*(1-K(E2))*(1-K(E3))<br />W{1, 0, 1} = K(E1)*(1-K(E2))*K(E3)<br />W{1, 1, 0} = K(E1)*K(E2)*(1-K(E3))<br />W{1, 1, 1} = K(E1)*K(E2)*K(E3)<br /><br />Or the other way to look at it is that we start with the weight of 1 for every branch, then for E1 multiply the weight of every branch where K(E1)=0 by the actual 1-K(E1), and where K(E1)=1 by the actual K(E1). Then similarly for E2, multiply the weight of every branch where K(E2)=0 by the actual 1-K(E2), and where K(E2)=1 by the actual K(E2). And so on. The sum of all the weights will always be 1.<br /><br />The physical meaning of this interpolation is that we split the computation into multiple branches and finally add them up together. Since the computation is additive, we can put them back together right away rather than waiting for the end.<br /><br />And since I was implementing the partial confidences in the final computation, I've also implemented the support for the partial confidences in training: it's done by logically splitting the training case into two weighted sub-cases with the event values of 0 and 1.<br /><br />All this is implemented in the script <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/ex26_01run.pl">ex26_01run.pl</a>, as a further modification of ex24_01run.pl. Without the extra parameters it works just as ex24_01 does. The new parameter -p sets the number of previous events whose absolute confidence affect the current event. Here is an example of a run with <a href="http://babkin-cep.blogspot.com/2017/03/bayes-24-adaboost-as-solution-to.html">the same training table as before</a> and the same input:<br /><br /><pre>$ perl ex26_01run.pl -n tab24_01.txt in24_01_01.txt <br />Original training cases:<br />! , ,E1 ,E2 ,E3 ,<br />hyp1 ,1.00000,1.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,1.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,0.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,1.00000/1.00,1.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,0.00000/1.00,<br />AdaBoostian table:<br />! , ,E1 ,E2 ,E3 ,<br />hyp1 ,3.00000,0.66667^0.66667,0.50000^0.50000,0.40000^0.33333,<br />hyp2 ,3.00000,0.33333^0.33333,0.50000^0.50000,0.60000^0.66667,<br />--- Initial weights<br />hyp1 w=3.00000 p=0.50000in a row <br />hyp2 w=3.00000 p=0.50000<br />--- Applying event E1, c=1.000000<br />hyp1 w=2.00000 p=0.66667<br />hyp2 w=1.00000 p=0.33333<br />--- Applying event E2, c=1.000000<br />hyp1 w=1.00000 p=0.66667<br />hyp2 w=0.50000 p=0.33333<br />--- Applying event E3, c=1.000000<br />hyp1 w=0.40000 p=0.57143<br />hyp2 w=0.30000 p=0.42857<br /></pre><br />The option -n disables the normalization of the weight of the hypotheses, just as before. The output is the same as ex24_01 produced. In the training data E1 and E2 are duplicates, so the probabilities computed for E2 in the adaboostian table (P(H|E) and P(~H|~E), they are separated by "^") are at 0.5, meaning that it has no effect.<br /><br />And here is the same training data but with support for one previous unknown event, and the input data having E1 unknown (i.e. C(E1) = 0.5):<br /><br /><pre>$ perl ex26_01run.pl -n -p 1 tab24_01.txt in26_01_01.txt </pre><pre>Original training cases:<br />! , ,E1 ,E2 ,E3 ,<br />hyp1 ,1.00000,1.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,1.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,0.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,1.00000/1.00,1.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,0.00000/1.00,<br />AdaBoostian table:<br />! , ,E1 ,E2 ,E3 ,<br />hyp1 ,3.00000,0.66667^0.66667,0.66667^0.66667,0.40000^0.33333,<br />+ , , ,0.50000^0.50000,0.40000^0.33333,<br />hyp2 ,3.00000,0.33333^0.33333,0.33333^0.33333,0.60000^0.66667,<br />+ , , ,0.50000^0.50000,0.60000^0.66667,<br />--- Initial weights<br />hyp1 w=3.00000 p=0.50000<br />hyp2 w=3.00000 p=0.50000<br />--- Applying event E1, c=0.500000<br />hyp1 w=1.50000 p=0.50000<br />hyp2 w=1.50000 p=0.50000<br />--- Applying event E2, c=1.000000<br />hyp1 w=1.00000 p=0.66667<br />hyp2 w=0.50000 p=0.33333<br />--- Applying event E3, c=1.000000<br />hyp1 w=0.40000 p=0.57143<br />hyp2 w=0.30000 p=0.42857</pre><br />Here E2 and E3 have not one pair of probabilities in the adaboostian table but two pairs, as shown in the branching picture above. The extra branches are shown on the continuation lines that start with a "+". The branches for E2 show that if E1 is known (the second branch), E2 will have no effect as in the previous example, but if E1 is unknown (the first branch), E2's probabilities would be the same as for E1, so E2 would replace it and converge to the same values.<br /><br />As you can see in the run log, after E1 with confidence 0.5 (i.e. absolute confidence 0) is applied, both hypotheses still have the equal weight. But then after E2 gets applied, the weights and probabilities of hypotheses become the same as in the previous example, thus the knowledge of E2 compensating for the absent knowledge of E1.<br /><br />An interesting thing to notice is that the probabilities for E3 are the same in either case. This is because this table can handle only one unknown event in a row, which means that at least one of E1 or E2 should be known, and E2 is able to fully compensate for the unknown E1, so both ways converge to the same probabilities of E3.<br /><br />Now some information about how it all is implemented:<br /><br />Some of the arrays have gained an extra dimensions, to be able to keep track of the multiple branches.The weight of the training case {wt} has become an array instead of a scalar, and the probabilities {ppos} and {pneg} of the hypotheses became 2-dimensional arrays.<br /><br />The function compute_abtable() that computes the adaboostean table had gained an extra nested loop that goes through all the branches. As the final step of processing every event it prunes the branches coming from the previous event and then splits each of these branches in two. The support of the partial confidence in the training data is done by replacing the statement<br /><br /><pre>if ($c->{tc}[$i] >= 0.5) {<br /> $c->{wt} /= $abhyp{$hyp}->{ppos}[$i];<br />} else {<br /> $c->{wt} /= (1. - $abhyp{$hyp}->{pneg}[$i]);<br />} <br /></pre><br />with<br /><br /><pre>push @{$c->{wt}}, <br /> $c->{tc}[$i] * $oldwt->[$br] / $abhyp{$hyp}->{ppos}[$i][$br]<br /> + (1. - $c->{tc}[$i]) * $oldwt->[$br] / (1. - $abhyp{$hyp}->{pneg}[$i][$br]);<br /></pre><br />The function apply_abevent() that does the computation based on the table has been updated to manage the branches during this step. It had gained the extra argument of an array that keeps the history of the absolute confidence of the few previous events. This history is used to compute the weights of the branches:<br /><br /><pre>for (my $br = 0; $br <= $maxbr; $br++) {<br /> my $w = 1.; <br /> for (my $bit = 0; $bit <= $#$evhist; $bit++) {<br /> if ($br & (1 << $bit)) {<br /> $w *= $evhist->[$bit];<br /> } else {<br /> $w *= 1. - $evhist->[$bit];<br /> } <br /> } <br /> push @brwt, $w; <br />} <br /></pre><br />Then an update based on the current event is computed for each branch, and the weight of the hypothesis is updated per these weighted bracnhes:<br /><br /><pre>my $p = 0.; <br /><br /># each branch is weighed per the observed history<br />for (my $br = 0; $br <= $maxbr; $br++) {<br /> $p += $brwt[$br] * ( <br /> $conf * $hyp->{ppos}[$evi][$br]<br /> + (1. - $conf) * (1. - $hyp->{pneg}[$evi][$br])<br /> ); <br />} <br /><br />$hyp->{cwt} *= $p; <br /></pre><br />And of course the functions that print the tables and normalize the data have been updated too.<br /><br />All this stuff works on the small examples. I'm not sure yet if it would work well in the real world, I'd need a bigger example for that, and I don't have one yet. Also, I've noticed one potential issue even on the small examples but more about it in the next installment.<br /><br />Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-68330065314416444032017-06-04T15:04:00.001-04:002017-06-04T15:04:13.199-04:00Neuron in the Bayesian terms, part 3I want to elaborate on why the second trick is so important. Basically, because in general the necessary condition of the chances changing on an event in the opposite way (by dividing or multiplying by the same number) is not true. It takes a specifically defined event to make it true.<br /><br />In general if an event is true, the chance of a hypothesis after applying this event is:<br /><br /><span style="font-family: "Courier New",Courier,monospace;">Chance(H|E) = P(H/E) / P(~H|E)</span><br /><br />The probabilities change on an event as follows:<br /><br /><span style="font-family: "Courier New",Courier,monospace;">P(H|E) = P(H) * P(E|H) / P(E)</span><br /><span style="font-family: "Courier New",Courier,monospace;">P(~H|E) = P(~H) * P(E|~H) / P(E)</span><br /><br />So then the chance changes (after substituting and reducing the formula):<br /><br /><span style="font-family: "Courier New",Courier,monospace;">Chance(H|E) = Chance(H) * P(E|H) / P(E|~H)</span><br /><br />If an event is false, the chance changes in a similar but different way:<br /><br /><span style="font-family: "Courier New",Courier,monospace;">Chance(H|~E) = P(H/~E) / P(~H|~E)</span><br /><span style="font-family: "Courier New",Courier,monospace;">= Chance(H) * P(~E|H) / P(~E|~H) </span><br /><br />Being able to do the "opposites" requires that<br /><br /><span style="font-family: "Courier New",Courier,monospace;">Chance(H|E) / Chance(H) = 1/ ( Chance(H|~E) / Chance(H) )</span><br /><span style="font-family: "Courier New",Courier,monospace;"></span><br /><span style="font-family: "Courier New",Courier,monospace;">Chance(H|E) / Chance(H) = Chance(H) / Chance(H|~E)</span><br /><span style="font-family: "Courier New",Courier,monospace;"></span><br /><br />Which then means after doing the substitution:<br /><br /><span style="font-family: "Courier New",Courier,monospace;">P(E|H) / P(E|~H) = P(~E|~H) / P(~E|H)</span><br /><br />Which is not always true. But if we take a page from the AdaBoost's book and define an event such that <br /><br /><span style="font-family: "Courier New",Courier,monospace;">P(E|H) = P(~E|~H)</span><br />and<br /><span style="font-family: "Courier New",Courier,monospace;">P(E|~H) = P(~E|H)</span><br /><br />Then it all magically works. Again, this magic is not needed for the Bayesian computation in general, it's needed only to fit it to the formula used in the neural networks and in AdaBoost.<br /><br />There are versions of AdaBoost that use the different multipliers for when x[i] < 0 and x[i] > 0. Which means that<br /><br /><span style="font-family: "Courier New",Courier,monospace;">l[i]^x[i]</span><br /><br />gets replaced with<br /><br /><span style="font-family: "Courier New",Courier,monospace;">if (x[i] > 0) {</span><br /><span style="font-family: "Courier New",Courier,monospace;"> lpos[i]^x[i]</span><br /><span style="font-family: "Courier New",Courier,monospace;">} else {</span><br /><span style="font-family: "Courier New",Courier,monospace;"> lneg[i]^x[i]</span><br /><span style="font-family: "Courier New",Courier,monospace;">}</span><br /><br />AdaBoost in this situation still uses the same definition of E=(x == y) for the reasons of its internal logic, but if we don't require this logic then we can define E=(x > 0) and easily implement the chance computation for it, since the requirement of both multipliers being the opposites would disappear.<br /><br />This opens two possibilities for variations of the neurons:<br /><br />(1) One with still the definition E=(x==y) but different multipliers for x[i] of different sign.<br />(2) One with the definition E=(x > 0)<br /><br />A network with such neurons would obviously take twice the memory space for its trained multipliers (but not for the data being computed). I'm not even sure if it would be trainable at all, especially the version (2), since AdaBoost has good reasons to keep its definition of E for the convergence. But maybe it would, and considering that AdaBoost in this modification converges faster, maybe the training of the neural network would converge faster too. Might be worth a try.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-35194408285429322262017-06-04T12:54:00.000-04:002017-06-04T12:54:01.238-04:00emergentsI've attended a talk about the commercial use of the pre-trained neural networks. The idea is that you take a pre-trained network, throw away the top couple of layers, then put the blank layers on top instead and train only these new layers for your purpose.<br /><br />The presenter gave an example of taking the Google's general image classifier network and teaching it to recognize your specific images instead: "huggable" vs "non-huggable". It was smart enough to recognize that a knitted cactus is huggable while a real one is not.<br /><br />Well, it's kind of not surprising: the top layers of the classifier network are trained to recognize the various kinds of images but to do that the layer below it must produce the characteristics that allow to recognize these various kinds of images. If the original classifier has the wide knowledge (and it does), the previous layer would know what is important to recognize a very wide rage of images. They call the output of that previous layer "the emergents". And then when you use them as inputs, the new top layer would have an easy job classifying them for the new goal. You could probably even do a Bayesian classifier on top of them without much difference.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-5453276912363570492017-06-03T05:14:00.000-04:002017-06-03T05:14:50.559-04:00Neuron in the Bayesian terms, part 2I've been trying to describe in proper formulas my <a href="http://babkin-cep.blogspot.com/2017/01/bayesian-networks-neural-networks.html">insight that a neuron in neural networks is a kind of a Bayesian machine</a>(OK, this is apparently known to the people who do the neural networks professionally but it was an insight for me), and I think that I've finally succeeded. Let's start from the beginning:<br /><br />A neuron implements the function<br /><br /><span style="font-family: "Courier New",Courier,monospace;">y = nonlinear(sum(w[i] * x[i]))</span><br /><br />Here y is the output of the neuron, x[i] is the array of its inputs, w[i] is the array of some weights determined by training. Let's define the range of y and x[i] as [-1, 1] (if the values are in the other ranges, they can be trivially scaled to the range [-1, 1] by adjusting the weights w[i]).<br /><br />The nonlinear function is generally some kind of a sigmoid function that matches what we do at the end of the Bayesian computations: if the probability of a hypothesis is above some level, we accept it as true, i.e. in other words pull it up to 1, if it's below some equal or lower level we reject it, i.e. pull it down to 0, and if these levels are not the same and the probability is in the middle then we leave it as some value in the middle, probably modified in some way from the original value. Except of course that this nonlinear function deals not with the probabilities in range [0, 1] but with the values in range [-1, 1], -1 meaning "false" and 1 meaning "true".<br /><br />The Bayesian formula uses the multiplication, not addition, so the first trick here is that one can be converted into another using a logarithm. To do that let's introduce a new value l[i], of which w[i] will be the logarithm:<br /><br /><br /><span style="font-family: "Courier New",Courier,monospace;">l[i] = exp(w[i])</span><br /><span style="font-family: "Courier New",Courier,monospace;">w[i] = ln(l[i])</span><br /><br /><br />This can be substituted into the expression for the sum:<br /><br /><span style="font-family: "Courier New",Courier,monospace;">sum (w[i] * x[i]) = sum( ln(l[i]) * x[i] )</span><br /><br />Using the rule <span style="font-family: "Courier New",Courier,monospace;">ln(a)*b = ln(a^b)</span>, the sum can be converted further:<br /><br /><span style="font-family: "Courier New",Courier,monospace;">sum( ln(l[i]) * x[i] ) = sum( ln(l[i]^x[i]) )</span><br /><br />Then using the rule <span style="font-family: "Courier New",Courier,monospace;">ln(a) </span><complete id="goog_1225579626"><span style="font-family: "Courier New",Courier,monospace;">+ln(b) = ln(a * b)</span> the sum can be converted into a product:</complete><br /><complete id="goog_1225579626"><span id="goog_1225579627"></span><span id="goog_1225579628"></span></complete><br /><span style="font-family: "Courier New",Courier,monospace;">sum( ln(l[i]^x[i]) ) = ln( product( l[i]^x[i] ) )</span><br /><br />Now let's see how this product can be connected to the Bayesian computation. The <a href="http://babkin-cep.blogspot.com/2015/10/bayes-3-magic-formula.html">classic Bayesian formula</a> for probabilities is:<br /><br /> <span style="font-family: "Courier New",Courier,monospace;">P(H|E) = P(H) * P(E|H) / P(E)</span><br /><br /><span style="font-family: inherit;">So as each Bayesian event E[i] gets applied, we can say that the final probability will be:</span><br /><br /><span style="font-family: inherit;"><span style="font-family: "Courier New",Courier,monospace;">P(H) = P0(H) * product( P(E[i]|H) / P(E[i]) )</span></span><br /><br />Well, technically I maybe should have used another letter instead of i for the index, maybe j, but since the i in both formulas will be soon connected to each other, I'll just use it in both places from the start.<br /><br />Instead of computing with probabilities we will be computing with chances. In case if you haven't used the chances before nor seen my mentioning of them in the previous posts, a chance is a relation of the probability of the hypothesis being true to the probability of it being false. For a simple example, if we have 5 doors, with prizes behind 2 of them and no prizes behind the remaining 3, then the chance of getting a prize by opening a random door is 2 to 3, or 2/3 (but the probability is 2/5). In a formula this gets expressed as:<br /><br /><span style="font-family: "Courier New",Courier,monospace;">Chance(H) = P(H) / P(~H) = P(H) / (1 - P(H))</span><br /><br />So if we have two mutually exclusive hypotheses H and ~H, the probabilities for them will be:<br /><br /><span style="font-family: "Courier New",Courier,monospace;">P(H) = P0(H) * product( P(E[i]|H) / P(E[i]) )</span><br /><span style="font-family: "Courier New",Courier,monospace;">P(~H) = P0(~H) * product( P(E[i]|~H) / P(E[i]) )</span><br /><span style="font-family: inherit;"><br /></span><br /><span style="font-family: inherit;">And the chances will be:</span><br /><span style="font-family: inherit;"><br /></span><br /><span style="font-family: "Courier New",Courier,monospace;">Chance(H) = P(H) / P(~H) </span><br /><span style="font-family: "Courier New",Courier,monospace;">= ( P0(H) * product( P(E[i]|H) / P(E[i]) ) ) / ( P0(~H) * product( P(E[i]|~H) / P(E[i]) ) )</span><br /><span style="font-family: "Courier New",Courier,monospace;">= (P0(H)/P0(~H)) * product( P(E[i]|H) / </span><span style="font-family: inherit;"><span style="font-family: "Courier New",Courier,monospace;">P(E[i]|~H) )</span> </span><br /><span style="font-family: inherit;"><br /></span><br /><span style="font-family: inherit;">If the initial probabilities of both hypotheses are equal, P0(H) = P0(~H) = 0.5, then their relation will be 1 and can be thrown out of the formula:</span><br /><span style="font-family: inherit;"> </span><br /><span style="font-family: "Courier New",Courier,monospace;">Chance(H) = product( P(E[i]|H) / P(E[i]|~H) )</span><br /><br /><span style="font-family: inherit;">Well, almost. The consideration above glanced over the question of what do we do if the event is false? The answer is that in this case the factor in the product should use the probabilities of ~E instead of E: </span><br /><span style="font-family: inherit;"><br /></span><span style="font-family: "Courier New",Courier,monospace;">Chance(H) = product( </span><br /><span style="font-family: "Courier New",Courier,monospace;"> if (E is true) {</span><br /><span style="font-family: "Courier New",Courier,monospace;"> P(E[i]|H) / P(E[i]|~H)</span><br /><span style="font-family: "Courier New",Courier,monospace;"> } else {</span><br /><span style="font-family: "Courier New",Courier,monospace;"> P(~E[i]|H) / P(~E[i]|~H)</span><br /><span style="font-family: "Courier New",Courier,monospace;"> } </span><br /><span style="font-family: "Courier New",Courier,monospace;">)</span><br /><br />Now comes the turn of the second major trick: what kind of events to use for the Bayesian formula. We'll use the same kind of events as for AdaBoost, the event being "x[i] accurately predicts y", so if H is "y > 0" and thus ~H is "y < 0", the conditional probability will be:<br /><br /><span style="font-family: "Courier New",Courier,monospace;">P(E[i]|H) = P(y == x[i])</span><br /><span style="font-family: "Courier New",Courier,monospace;">P(~E[i]|~H) = P(y == x[i])</span> <br /><br />An interesting property of this definition is that the conditional probabilities of these events get computed across the whole range of the training data, without differentiation between x[i] < 0 and x[i] > 0. This means that<br /><br /><span style="font-family: "Courier New",Courier,monospace;">P(E|H) = P(~E|~H)</span><br /><span style="font-family: "Courier New",Courier,monospace;">P(E|~H) = P(~E|H)</span><br /><br />We then use the general property that<br /><br /><span style="font-family: "Courier New",Courier,monospace;">P(E|H) = 1 - P(~E|H)</span><br /><span style="font-family: "Courier New",Courier,monospace;">P(E|H~) = 1 - P(~E|~H)</span><br /><br />and get<br /><br /><span style="font-family: "Courier New",Courier,monospace;">P(E|~H) = P(~E|H) = 1 - P(E|H)</span><br /><br />So after substituting all these equivalencies the computation of the chances becomes nicely symmetrical: <br /><br /><span style="font-family: "Courier New",Courier,monospace;">Chance(H) = product( </span><br /><span style="font-family: "Courier New",Courier,monospace;"> if (E is true) {</span><br /><span style="font-family: "Courier New",Courier,monospace;"> P(E[i]|H) / (1 - P(E[i]|H))</span><br /><span style="font-family: "Courier New",Courier,monospace;"> } else {</span><br /><span style="font-family: "Courier New",Courier,monospace;"> (1 - P(E[i]|H)) / P(E[i]|H)</span><br /><span style="font-family: "Courier New",Courier,monospace;"> } </span><br /><span style="font-family: "Courier New",Courier,monospace;">)</span><br /><br />When x[i] is in range [-1, 1], and especially if x[i] is in the set {-1, 1}, the if/else can be replaced by the power of x[i]: when x[i]==1, it will leave the expression as-is, when x==-1, the expression will be "turned over":<br /><br /><span style="font-family: "Courier New",Courier,monospace;">Chance(H) = product( ( P(E[i]|H) / (1 - P(E[i]|H)) )^x[i] )</span><br /><span style="font-family: inherit;"><br /></span><span style="font-family: inherit;">We can now interpret the original formula in terms of the chances. That formula was:</span><br /><span style="font-family: inherit;"><br /></span><span style="font-family: "Courier New",Courier,monospace;">y = nonlinear(sum(w[i] * x[i]))</span><br /><span style="font-family: "Courier New",Courier,monospace;">= </span><span style="font-family: "Courier New",Courier,monospace;">nonlinear( </span><span style="font-family: "Courier New",Courier,monospace;">ln( product( l[i]^x[i] ) ) )</span><br /><span style="font-family: inherit;"><br /></span><span style="font-family: inherit;">Here we can substitute the product:</span><br /><span style="font-family: inherit;"><br /></span><span style="font-family: inherit;"><span style="font-family: "Courier New",Courier,monospace;">y = nonlinear( ln( Chance(y > 0) ) )</span></span><br /><span style="font-family: inherit;"><br /></span><span style="font-family: inherit;">A logarithm is a very convenient thing to apply on the chance to obtain a symmetric range of values (-infinity, +infinity) centered at 0 instead of [0, +infinity) "centered" at 1. And if the weights are properly selected, the actual range of y will be not </span><span style="font-family: inherit;">(-infinity, +infinity) but [-1, 1].</span><br /><br /><span style="font-family: inherit;">This substitution of the product will mean:</span><br /><span style="font-family: inherit;"><br /></span><span style="font-family: "Courier New",Courier,monospace;">l[i] = P(E[i]|H) / (1 - P(E[i]|H))</span><br /><span style="font-family: "Courier New",Courier,monospace;">w[i] = ln( P(E[i]|H) / (1 - P(E[i]|H)) )</span><br /><span style="font-family: inherit;"><span style="font-family: inherit;"><br /></span></span><span style="font-family: inherit;"><span style="font-family: inherit;">So the original weights in the neuron have the "physical meaning" of the logarithms of the chance multipliers.</span></span><br /><span style="font-family: inherit;"></span>Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-72102313079948741002017-03-08T15:54:00.003-05:002017-03-08T15:54:59.979-05:00OS support for thread stoppingI've recently had done some thinking about how can an OS make the shutdown of a multithreaded application or its part easier, to stop the threads quickly but relatively nicely on request. I think a very helpful support can be pretty easy to implement:<br /><br /><ul><li>Have system call to request the shutdown of an OS-level thread.</li><li>This call would make any system calls that could block non-blocking (except for the thread synchronization calls, like mutex or condition variable operations), would interrupt any current such blocking calls, and return an error. This includes the timed sleeps, and even the thread synchronization calls with timeouts would return an error immediately instead of sleeping. Optionally, it might also just fail any file operations altogether, possibly except for close(). Note that the file descriptors as such wouldn't be revoked altogether, they would just kind of stop working in one thread, but the other threads would still work.</li><li>A problem with this might be in such things as logging: when your thread is stopping, it might still write the useful log files. Revoking these log files would be Well, one solution for this would be to buffer the logs in memory and write from a separate thread. Another might be to keep some kind of exception list.</li><li>The threads might also beget threads, and keeping track of all of them is complicated. So there is a need for grouping them, like the process groups in the OS, or Triceps Triead fragments, and then stopping the whole group.</li></ul>Yet another separate approach might be to say "but if you want the ability to stop something, why don't you run it as a separate process and kill if when needed?". This would make a lot of sense for such things as PAM authentication modules.<br /><br />There are problems with the separate processes too. But after some thinking, they seem to be fairly easy to solve.<br /><br />When you think about processes, usually a separate binary comes to mind but it doesn't have to. You can just do a fork() and run from the same binary, and you even get the whole address space copy-on-write out of the box. Though I guess it might be interesting to NOT copy-on-write the whole address space. Instead just allocate some communication area for passing the arguments and results, and get rd of the rest, maybe even re-running the run-time loader from scratch in the new context (that would be kind of like the Java class loader spaces).<br /><br />Communication with a separate process is kind of painful. But if you map an anonymous memory segment, that segment can be inherited by the child process. Indeed, should be inherited to act as the communication region (truly shared, not COW) even if the rest of address space is dropped. And then you can happily create the mutexes and condition variables in it for synchronization. The arguments and results would have to be serialized to pass through this region but it shouldn't be such a big deal. It would be also possible to keep some large data dictionaries in this communication space.<br /><br />Of course the callbacks wouldn't work across the process boundary. But there is a solution for this too: create a local thread pool (or simply a thread) for callbacks that would get the requests from the remote process though a portion of the same communications region, execute the callback, and return the results back there.<br /><br />I think this looks feasible even without any new technology needed, just needs a helper library to implement the interface. This isn't even a new concept, it's a classic microkernel architecture without the common memory space. Yeah, copying the data in the serialized form adds an overhead but it's not that much overhead compared to say JVM, and people use Java and .NET all over the place with no regrets.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-24482752653729421082017-03-05T21:41:00.000-05:002017-03-05T21:41:27.421-05:00Bayes 25 & AdaBoost: the partial confidence problemThe code in the previous example works only with the full-confidence events, those that have the confidence of 0 or 1. Unfortunately I haven't found a good way to handle the partial confidence yet. I've found <i>a</i> way but it has problems. Let me show you.<br /><br /><a href="http://babkin-cep.blogspot.com/2015/10/bayes-4-fuzzy-logic.html">Previously</a> I was treating the partial confidence as a superposition of 0 and 1. This approach cannot be properly used here because it doesn't cover the connections between the events. To give an example, in the <a href="http://babkin-cep.blogspot.com/2017/03/bayes-24-adaboost-as-solution-to.html">previous post</a> the code had correctly found that in tab24_01.txt the events E1and E2 are the same, so after E1 is applied, E2 becomes ignored (it's AdaBoost probabilities become 0.5). So now if we then do a computation and find that E1 is unknown (i.e. its confidence is 0.5) but E2 is known, E2 will still be ignored because its table entry says to always ignore it.<br /><br />The correct way should handle this situation properly: if E1 is unknown, this should bring up the table values for E2 to compensate for it and treat E2 as E1 was formerly.<br /><br />The obvious way to do it is to treat the partial confidence as a superposition of "the event is unknown" and "the event is known". So if the event is known, we process it as before. If the event is unknown, we would follow the unknown branch and then apply all the following events as if this events wasn't there. And if the event is partially known, we would split the weights of all the outcomes proportionally and handle one part with the known branch and another one with the unknown branch.<br /><br />So the confidence of 0.7 would mean a superposition of 0.4 of unknown and 0.6 of the known value "1". The confidence 0.1 would mean a superposition of 0.8 of unknown and 0.2 of the known value "0". So if we denote C(E) as the event confidence, K(E) as the known weight in the superposition, and U(E) as the unknown weight in the superposition, we can write them as:<br /><br />K(E) = abs(C(E) - 0.5) * 2<br />U(E) = 1 - K(E)<br /><br />The downside of this obvious approach is that the table and computation becomes more complex. As we go through more events, two branches become four, then eight, then sixteen, and so on. The formerly quadratic complexity O(Noutcomes * Nevents) becomes exponential O(Noutcomes*Nevents*2^Nevents). I don't know yet if there is a better way.<br /><br />Maybe it would be possible to build a table of "gradients" between the events: i.e. if the confidence of the event A changes, how are the probabilities of another event B affected. The problem though is that this gradient depends on all the events in the sequence between A and B, so tis idea doesn't seem to really fly.<br /><br />Maybe another idea is workable, that would reduce the exponential cost to a cubic one: after splitting the superposition, instead of following two branches, just take the probabilities for each following event from both branches and mix them by the weight of the branches:<br /><br />P(H|Ei) = K(Ej) * P(H|KjEi) + U(Ej) * P(H|UjEi)<br /><br />It's better than exponential but still not great, with the run time O(Noutcomes*Nevents*Nevents). And the table of these pre-computed values would still seem to be exponential, each event depending on all the possible combinations of the previous events. Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-88190786169127206932017-03-01T02:37:00.000-05:002017-03-01T02:37:32.916-05:00Bayes 24: AdaBoost as a solution to the correlated eventsWhen writing the <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/whitepapers/AdaboostBayes.pdf?format=raw">whitepaper</a>, I've realized that AdaBoost is very good at finding the cross-correlation of its partial hypotheses (that map to the Bayesian events), and it always picks the next step with the minimal cross-correlation. The Bayesian computations do have an issue with cross-correlation, as described in the whitepaper and near the end of <a href="http://babkin-cep.blogspot.com/2015/10/bayes-8-impossible.html">Part 8 of my notes</a>: if there are multiple events that duplicate or mirror each other, all of them are treated as independent and drive the probabilities off. This looked like AdaBoost could give a hint of how to solve this problem, so I've started thinking about it, and I think I've found a solution.<br /><br />In retrospect the solution is pretty obvious: instead of computing the Bayesian model directly, treat it as an AdaBoost model whose set of the partial hypotheses equals to the set of the Bayesian events, with an extra limitation that each one of them must be used exactly once. If there are multiple equivalent events and they happen to go one after another, AdaBoost will treat the first one as real, and the following ones will get their error rate of 0.5 and will be effectively no-ops.<br /><br />Well, if there are other events interspersed in between these equivalent copies, they won't be no-ops. These other events will shift the weights of the training cases, so that the second an further copies will find themselves drifted away from 0.5. The good news here is that AdaBoost is pretty good at removing the partial correlation too but the bad news is that the order matters, to fully remove the duplication the equivalent events should go one after another. And in general, it looks like the best removal of the cross-correlation happens when the closely related events follow closely one after another. This kind of order can be implemented if we reverse the AdaBoost preference of the partial hypotheses and make it always pick the one that has its training error rate closest to 0.5. Since (unlike the classic AdaBoost) the set of partial hypotheses is limited and each element from it must be used only once, the better ones will eventually be used as well.<br /><br />To experiment with these ideas, I wrote the code example <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/ex24_01run.pl">ex24_01run.pl</a>. It doesn't reorder the events, just computes the AdaBoost values for whatever order it's given, allowing to experiment with the various orders. But it has a couple of advanced features from the later chapters of the book on AdaBoost: it does the multi-class (but single-label) version that also partitions the error rate computation for the sign of the value of the partial hypothesis.<br /><br />The multi-class means that it can work not only with two outcomes (a yes/no decision) but with multiple possible outcomes (i.e. Bayesian hypotheses), just like the normal Bayesian table. And just like the normal Bayesian table, these outcomes must be mutually exclusive, so each case may have only one outcome (single-label). The multi-class logic works in the way that is similar to the one described in <a href="http://babkin-cep.blogspot.com/2015/10/bayes-10-independence-and-relevance.html">Part 10</a>: the resulting table is just a combination of the individual per-outcome tables. From the standpoint of each outcome, the single-label property means that the rest of the rows of the table taken together represent the opposite of this outcome, so there is no need to compute that opposite explicitly.<br /><br />The partitioning of the error rate computation not only makes the result better but also comes very handy to prove that the opposite of one outcome is equivalent to the rest of the rows of the table. The AdaBoost training error <i>e</i> is the ratio of the total weights of the training cases where the value of the event E (that is, the Bayesian event, in AdaBoost terms it's a partial hypothesis) is not equal to the value of the outcome (Bayesian hypothesis) H. And we'll compute it separately for E (i.e. E=1) and ~E (i.e. E=0). It ends up expressable as probabilities that can be computed by summing and dividing weights W of the training cases:<br /><br />e(H,E) = P(~H|E) = (sum for cases i where H(i)=0 and E=1 (Wi))/(sum for cases j where E=1 (Wj))<br />e(H,~E) = P(H|~E) = (sum for cases i where H(i)=1 and E=0 (Wi))/(sum for cases j where E=0 (Wj))<br /><br />So if we pick a particular outcome and denote it H1, we can show that its opposite ~H1 is equivalent to the sum of the rest of hypotheses H2...HN by showing that<br /><br />P(~H1|E) = sum for i in [2, N] (P(Hi|E))<br /><br />which after removing the common denominator means <br /><br />(sum for cases j where H1(E)=0 and E=1 (Wj)) = (sum for i in [2, N] (sum for cases k where Hi(E)=1 and E=1 (Wk))<br /><br />The single-label property means that each case is marked with exactly one outcome, and all the cases where the outcome H1 is absent must have some other outcome Hi present, so these sums are indeed equal.<br /><br />So it is all self-consistent. The key part of the computation is:<br /><br /><pre>for my $hyp (values %abhyp) {<br /> if ($conf) {<br /> $hyp->{wt} *= $hyp->{ppos}[$evi];<br /> } else {<br /> $hyp->{wt} *= (1. - $hyp->{pneg}[$evi]);<br /> }<br /> }<br /></pre><br />And the matching part of the table building is:<br /><br /><pre>foreach my $c (@case) {<br /> my $hyp = $c->{hyp}[0];<br /> # this code really handles only the TC values of 0 and 1,<br /> # for the arbitrary values it would have to do an interpolation<br /> if ($c->{tc}[$i] >= 0.5) {<br /> $c->{wt} /= $abhyp{$hyp}->{ppos}[$i];<br /> } else {<br /> $c->{wt} /= (1. - $abhyp{$hyp}->{pneg}[$i]);<br /> } <br /> } <br /></pre><br />The training undoes the weight change that will be done in the computation.<br /><br />Now let's try some examples. Here is the example that I used in the whitepaper:<br /><br /><pre>$ perl <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/ex24_01run.pl">ex24_01run.pl</a> -n <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/tab24_01.txt">tab24_01.txt</a> <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/in24_01_01.txt">in24_01_01.txt</a><br />Original training cases:<br />! , ,E1 ,E2 ,E3 ,<br />hyp1 ,1.00000,1.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,1.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,0.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,1.00000/1.00,1.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,0.00000/1.00,<br />AdaBoostian table:<br />! , ,E1 ,E2 ,E3 ,<br />hyp1 ,3.00000,0.66667^0.66667,0.50000^0.50000,0.40000^0.33333,<br />hyp2 ,3.00000,0.33333^0.33333,0.50000^0.50000,0.60000^0.66667,<br />--- Applying event E1, c=1.000000<br />hyp1 w=2.00000 p=0.66667<br />hyp2 w=1.00000 p=0.33333<br />--- Applying event E2, c=1.000000<br />hyp1 w=1.00000 p=0.66667<br />hyp2 w=0.50000 p=0.33333<br />--- Applying event E3, c=1.000000<br />hyp1 w=0.40000 p=0.57143<br />hyp2 w=0.30000 p=0.42857<br /></pre><br />Here the option "-n" means "don't normalize the weights" (or when normalized to the sum of 1 they will be the same as probabilities). The two values printed in the AdaBoostian table separated by a "^" are the P(H|E) and P(~H|~E).<br /><br />As you can see, the result is not correct either (because of the XOR problem) but it's better than with the plain Bayes version that gave hyp1 the probability 0.667. And since E1 and E2 are the same, E2 ended up with the values of 0.5 in the table, and applying it didn't change the weight.<br /><br />If we change the order of events, E2 gains some use back:<br /><br /><pre>$ perl <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/ex24_01run.pl">ex24_01run.pl</a> -n <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/tab24_02.txt">tab24_02.txt</a> <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/in24_01_01.txt">in24_01_01.txt</a> <br />Original training cases:<br />! , ,E1 ,E3 ,E2 ,<br />hyp1 ,1.00000,1.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp1 ,1.00000,1.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp1 ,1.00000,0.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp2 ,1.00000,1.00000/1.00,1.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,0.00000/1.00,<br />AdaBoostian table:<br />! , ,E1 ,E3 ,E2 ,<br />hyp1 ,3.00000,0.66667^0.66667,0.40000^0.33333,0.47368^0.48276,<br />hyp2 ,3.00000,0.33333^0.33333,0.60000^0.66667,0.52632^0.51724,<br />--- Applying event E1, c=1.000000<br />hyp1 w=2.00000 p=0.66667<br />hyp2 w=1.00000 p=0.33333<br />--- Applying event E3, c=1.000000<br />hyp1 w=0.80000 p=0.57143<br />hyp2 w=0.60000 p=0.42857<br />--- Applying event E2, c=1.000000<br />hyp1 w=0.37895 p=0.54545<br />hyp2 w=0.31579 p=0.45455<br /></pre><br />Rather surprisingly, the result ended up more correct. Or maybe it's not that surprising: after all, AdaBoost is designed to gain more information from available data.<br /><br />Yet another interesting result get produced from repeating the same pair of events multiple times:<br /><br /><pre>$ perl <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/ex24_01run.pl">ex24_01run.pl</a> -n <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/tab24_03.txt">tab24_03.txt</a> <a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/in24_03_01.txt">in24_03_01.txt</a> <br />Original training cases:<br />! , ,E01 ,E02 ,E11 ,E12 ,E21 ,E22 ,E31 ,E32 ,E41 ,E42 ,E51 ,E52 ,E61 ,E62 ,E71 ,E72 ,E81 ,E82 ,E91 ,E92 ,<br />hyp1 ,1.00000,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,<br />hyp1 ,1.00000,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,0.00000/1.00,1.00000/1.00,<br />hyp2 ,1.00000,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,0.00000/1.00,<br />AdaBoostian table:<br />! , ,E01 ,E02 ,E11 ,E12 ,E21 ,E22 ,E31 ,E32 ,E41 ,E42 ,E51 ,E52 ,E61 ,E62 ,E71 ,E72 ,E81 ,E82 ,E91 ,E92 ,<br />hyp1 ,3.00000,0.66667^0.66667,0.40000^0.33333,0.47368^0.48276,0.49694^0.49526,0.49916^0.49946,0.49990^0.49985,0.49997^0.49998,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,<br />hyp2 ,3.00000,0.33333^0.33333,0.60000^0.66667,0.52632^0.51724,0.50306^0.50474,0.50084^0.50054,0.50010^0.50015,0.50003^0.50002,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,0.50000^0.50000,<br />--- Applying event E01, c=1.000000<br />hyp1 w=2.00000 p=0.66667<br />hyp2 w=1.00000 p=0.33333<br />--- Applying event E02, c=1.000000<br />hyp1 w=0.80000 p=0.57143<br />hyp2 w=0.60000 p=0.42857<br />--- Applying event E11, c=1.000000<br />hyp1 w=0.37895 p=0.54545<br />hyp2 w=0.31579 p=0.45455<br />--- Applying event E12, c=1.000000<br />hyp1 w=0.18831 p=0.54242<br />hyp2 w=0.15886 p=0.45758<br />--- Applying event E21, c=1.000000<br />hyp1 w=0.09400 p=0.54159<br />hyp2 w=0.07956 p=0.45841<br />--- Applying event E22, c=1.000000<br />hyp1 w=0.04699 p=0.54149<br />hyp2 w=0.03979 p=0.45851<br />--- Applying event E31, c=1.000000<br />hyp1 w=0.02349 p=0.54147<br />hyp2 w=0.01990 p=0.45853<br />--- Applying event E32, c=1.000000<br />hyp1 w=0.01175 p=0.54146<br />hyp2 w=0.00995 p=0.45854<br />...<br /><br />--- Applying event E92, c=1.000000<br />hyp1 w=0.00000 p=0.54146<br />hyp2 w=0.00000 p=0.45854<br /></pre><br />AdaBoost converges pretty quickly to the weights that make both events get the probability 0.5 and stop the further changes.<br /><br />This seems to work, although it needs more testing. One more limitation of this implementation is that it works only with the events that have the value of either 0 or 1, not anything in between. I'll talk more about this in the next post.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-11383816463495882842017-02-22T09:51:00.001-05:002017-02-22T09:51:22.265-05:00a better explanation of Bayes-AdaBoost connectionAfter some re-reading, my previous posts on the Bayesian interpretation of AdaBoost have been kind of cryptic. It's no wonder because I've been writing them as I was understanding things, so they came out as more of a personal reminder rather than the general explanation. But I've got around and wrote another version as a whitepaper, that goes all the way from scratch and hopefully is much more understandable:<br /><br /><a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/whitepapers/AdaboostBayes.pdf?format=raw">https://sourceforge.net/p/exbayes/code/HEAD/tree/whitepapers/AdaboostBayes.pdf?format=raw</a><br /><br />Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-36199880819210511012017-02-20T13:21:00.003-05:002017-02-21T01:37:50.507-05:00Bayesian examples now on SourceForgeI've been working on one more post on the Bayes-AdaBoost cross-over, and I've finally got tired of pasting the substantial examples right in the post. So I've created a SourceForge project for them and uploaded all the examples there:<br /><br /><a href="https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/">https://sourceforge.net/p/exbayes/code/HEAD/tree/v1/</a><br /><br />The proper SVN checkout recipe from there is:<br /><br />svn checkout svn://svn.code.sf.net/p/exbayes/code/ exbayesSergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-59640407711855512572017-02-04T20:08:00.001-05:002017-02-28T13:45:27.569-05:00The mythical 10x programmer, or the silver bullet explainedRecently I've been talking about some of the experiences from Aleri times when my interlocutor responded "so I guess you're one of those 10x programmers". This made me pause. Like any programmer per Larry Wall's recipe, I have a good amount of hubris but can I really say that I'm a 10x programmer? After some thinking, I guess sometimes I am and sometimes I'm not. This got me thinking further, about what was different between the situations when I was and when I wasn't. And I think the difference is very much about the organization and methodology, and thus can probably be applied to any programmers to improve their productivity. I've tried to distill the knowledge, and here it goes. By the way, Fred Brooks also called the goal of 10x productivity increase the "silver bullet", so here is maybe your silver bullet. Maybe. I'm not entirely sure that the recipe will work out of the box, it will probably need at least some debugging. Or maybe it won't work but it's still worth a try.<br /><br />Let me start with describing a couple of cases when I could fairly be said to have been a 10x programmer. There were some other good cases too but on a couple of occasions I've actually had a benchmark.<br /><br />One case was at Aleri, working on the CEP system. Here is the real short version of the events: At the time there were 3 main players in the CEP world: StreamBase, Aleri and Coral8, all having an approximate parity in their products. Around 2009 Coral8 apparently ran out of money, so Aleri used it this opportunity to consume it. And then it had two approximately equal products and set about merging them. The merging didn't go that great and Aleri ran out of money too, eventually consumed in 2010 by Sybase. See, even though the Coral8 product was about the same from the functional standpoint, it had the code base about 10x the size, and had maybe not quite 10x but a lot more people working on it. Since the Coral8 code base was so much larger and there were many more people familiar with it, it was taken as the foundation of the merged product. And this merging then crashed and burned. Another merging attempt was then done under Sybase, this time doing it the other way around, and it worked much better. So it's an interesting example where the 10x was there, then it wasn't, and then it was again.<br /><br />Another case was the UnitedLinux. The full history of UnitedLinux is an interesting story of dog-eat-dog in the world of the Linux vendors but I won't go into the full funny details, just touch up on the important parts. One of conditions of the UnitedLinux consortium set by SuSE was that SuSE does all the general development. Everyone else does just their branding customizations and additional packages. So Caldera where I worked at the time (soon to be renamed SCO, with the SCO Linux brand of UnitedLinux) had closed the Linux development office in Germany, with most of the people heading to SuSE. When UnitedLinux was released, the various consortium members (I remember Connectiva, and whoever else) held their release parties and posted pictures with great many people. We didn't have any party at SCO: see, the SCO Linux was done by only two developers and half a tester (and a bit of a graphical artist), and Ron was in California while I was in New Jersey, so having a party would have been difficult. And we've had it ready earlier: the final image was sent to testing about 20 minutes after downloading the drop from SuSE. I think I still have the commemorative t-shirt somewhere, one thing Caldera was big on was the t-shirts.<br /><br />So what was special in these cases? In a very short summary, I'd say that it was the compact code done with the familiar technologies, incrementally, with low interdependencies, and with high level of script automation. Oh, and you need to have the good engineers as well. But just the good engineers aren't enough: I wouldn't say that the engineers at Coral8 were any worse than at Aleri, just they've got mired in the slow methodologies.<br /><br />Let's look at the details.<br /><br />The compact code pretty much means the straightforward and to-the-point code. There is a classic book "The practice of programming" (and I liked it so much that I've patterned the name of my book after it) that shows the code like this: when you have a problem, solve it in the most straightforward way. There are lots of published systems of various code decorations, and all of them are wrong. The decorations don't make the code better, they make it worse, more difficult to read and modify. The extra layers of abstractions don't help anything either, they make the code more difficult to both understand and modify (not to mention writing it in the first place). There is an article about "worse is better" written by Richard Gabriel from the MIT AI labs when he discovered that the simpler Unix code worked better than his "better" over-abstracted code. But this phrase is misleading, better is better, it's just that the over-abstractions are worse, not better. This absolutely doesn't mean that the code should be flat abstraction-wise. The code that is too flat is also an unmaintainable spaghetti mess. The abstraction layers are needed. But they have to be introduced for a good reason, not on a whim and not by the bureaucratic rules. The good functions are usually pretty large. The good classes are usually pretty large. The good rule of thumb for making a chunk of code into a new abstraction is to notice when you do the same thing over and over again. Then it's time to put this code into one place and call it from the other places. A good abstraction reduces the size of code, does not increase it.<br /><br />The problem with the overly-abstracted code is really the same as with the under-abstracted flat code: there are only so many things you can keep in your head at a time. In the code that is too flat you need to keep too many interdependencies of that flat level in your head. In the code that is too abstracted you need to keep too many abstractions and their APIs and calling patterns in your head. The optimal spot is in the middle.<br /><br />A lot of things are easier to just do than to use a library. If you do the logging from a shell script, a 10-line shell function will not only work much better than a shell version of log4j but will also be a lot easier to call (not to mention avoiding an extra dependency). Or an iteration over a container with a plain for-loop is not only easier to write and understand but also much easier to remember than the STL templates for that.<br /><br />An important thing is to keep the documentation close to the code. This means both the good comments in the code and the user documentation.<br /><br />The basic rule behind the good comments is very simple: the comments must be one (or sometimes more) step more abstract than the code. A comment should be explaining either why something is done (or not done), especially if the reasoning is not obvious, or telling what is done as a short summary in the higher-level terms. As long as this rule is followed, the more comments is pretty much the better (but there is only so much you can write without breaking this rule).<br /><br />Keeping the user documentation together with the code, in the same versioning system and in the same tree hierarchy is very important. This way the documentation can be updated in parallel with the code and will never become obsolete. And at the end of the release cycle all the changes can be found and re-edited. This principle implies that the documentation must be kept in a text-based format that is easy to diff, just as the code is, and that the tools used on it must not change that exact text format at random. Which generally means that the editing should be done with a normal text editor, and the WYSIWYG tools should go into the garbage bucket. The pain inflicted by them is much greater than the gain. The ease of versioning and diffing is much more important than the ease of visual formatting. While on the subject of documentation, the technical writers need to either really be technical or be kept in check. The precise meaning of documentation is important, and I've seen too often an edit to make a sentence smoother also making it ambiguous or entirely meaningless. And "smoother" is not always "better" either. I've bought a book of Richard Feynman's letters, more for the collection purposes since the letters are usually fairly boring but unexpectedly it also turned out to be a great reading. Except for the part where it included the magazine articles about his Nobel prize. The style of these articles was completely different from Feynman's, it was the usual unreadable journalist drivel. Don't turn your documentation into the journalist drivel.<br /><br />To give another example of where I think smooth is bad, the sentence above "An important thing is to keep the documentation close to the code" is better in that place than "Keeping the documentation close to the code is important". The complex sentence slows you down and gives you a breather before the text switches to a new subject, emphasizing that switch. Also, it brings the word "important" closer to the start of the sentence where it's more noticeable.<br /><br />Next principle, the familiar technologies, affects the speed of development a lot. It's kind of obvious but the counter-point is "if we use that latest and greatest new tool, we'll be done much faster". And sometimes the latest and greatest tool really does that. But most of the time the familiarity is much more important. If you have an idea of how the goal can be straightforwardly achieved with the familiar tools, that's usually the best bet by a wide margin. If not, learn the new tool. Sometimes this would also give you an idea of how the goal can be achieved easier with your old tools. And a lot of technologies are just crap anyway. So the overall priority should be not "the goal for the tool" but "the tool for the goal".<br /><br />This doesn't mean that the new tools shouldn't be learned or that everyone has to be familiar with every tool to be used. Usually one person having the familiarity with a tool is enough to frame the code around it, giving everyone else a big head start in learning it. It's much easier to add code to an existing unfamiliar framework than to build a new framework with an unfamiliar tool. And there are the times to use the familiar tools and times to learn the new ones. There is the cadence of development when the things start amorphous and slow, and then as the solutions for various items start falling together the project starts crystallizing and picking up the pace until it rolls at full steam to the goal. Then the next cycle starts. The slow part of the cycle is a good time to learn the new tools. But things tend to go a lot faster when only one thing is new: a new tool for an old purpose or an old tool for a new purpose. Of course sometimes a bullet has to be bitten and a new tool learned together with the new purpose but that goes slower.<br /><br /><br />On to the incrementality. It's nothing new, Eric Raymond wrote about it in "The Cathedral and the Bazaar". But it still gets lost surprisingly often, even in the "agile methodologies". Instead of "we'll make this great new thing to replace that dingy old thing" the right approach is "we'll transform this dingy old thing into this great new thing". The programs are not physical objects, they're more like blueprints for the physical objects. It's not like you have to raze a bridge to build a new bridge in its place. Instead a program's <i>run</i> is more like a physical object. Every time you restart a program an old bridge gets razed and a new one gets built for you according to the new plan. Or I guess another good example from the world of the physical things is the automobile manufacturing: the car models might stay but their internal mechanics is updated pretty regularly. At some point the cars would look the same but the new cars from that point on would get say a better alternator. The Chevy small block V8 engine has been around for more than 50 years. The only item that stayed for all this time is the dimensions of the cylinders. Everything else has been replaced. But it hasn't been replaced in one go, it has been a product of a long evolution with many gradual small changes.<br /><br />Every time you hear about an order of magnitude budget and time overruns, it's when someone tried to re-do a program from scratch. Fred Brooks talks about this in his "Mythical man-month", so the effect has been known since at least 1970s, and yet it keeps being repeated. And guess what, that's not limited to software either. Have you ever heard of Tucker? It was a new automobile company in 1940s that decided to design its car completely from scratch, with all the newest and greatest technology. They've never got the cars working, they kept falling apart until the company ran out of money, and its founder got sued by the government for all the dubious financial activities he'd done to delay the running out of money. Here someone might ask "but what about the iPhone, what about Tesla?" They are actually the examples of the evolutionary development. iPhone was built around the clever idea of a big-screen phone used as an application platform but it wasn't developed all at once, and Apple didn't try to reinvent the cell communications chipsets at the same time. Some of the evolutionary moments of iPhone are widely known: such as, they started with a stylus as was then typical, and then Steve Jobs said "screw this, use the fingers instead". And the last-moment decision of replacing the plastic screens with glass had been a subject of many moralizing articles. Tesla is best seen in the contrast to GM's EV1. GM tried to develop the whole technology chain from scratch, spent a huge amount of time and money, and failed. While Tesla piggybacked on the already existing technologies and packaged them together into a car (that they also took off the Lotus's shelf). Even then Tesla's path wasn't easy, they've had their tuckeresque moments with the failing transmissions (which they eventually simply got rid of) and with the battery cooling for a few years before they were able to release their first car.<br /><br />One of the worst statements I've heard about why a system needed to be rewritten from scratch went like this: "it has accumulated all these little features that make making changes without breaking them difficult, so we have to re-write everything to get rid of them and make the changes simple again". It completely misses the point: it's these little features that make the product worthwhile. Doing the 80% of the functionality is easy but nobody needs only 80% of the functionality (and in reality that particular project overran by years). The code is a living document that preserves the knowledge of the subject area. Discarding the code means discarding this knowledge which then has to be re-learned. This doesn't mean that the best solution isn't sometimes to throw away the old code and re-write it from scratch. But it's usually done by replacing the code in small chunks and checking that the functionality is preserved after the move of every chunk. To give an analogy from biology, the human (and animal) organs develop by growing the new cells within a scaffolding of the other neighboring cells that shapes them and makes them properly differentiate for the function. The old code provides such a scaffolding for the new chunks.<br /><br />I want to illustrate this last point with a couple of examples. I'll leave the company and all the details in the first example unnamed, since the example is kind of embarassing. This company decided to make an appliance with its software. It had all the software bits and pieces that it has been selling to the customers, so the only thing that needed to be done was put it all together, like the customers do. Simple, right? Turns out, no, the bits and pieces would not work together, and no customer had actually put them all together because they didn't work. Each piece worked in a way, so each responsible team was happy, but not together. And getting them to work together took a large amount of time and work. Another similar example comes from Microsoft where I've seen a very similar issue with the <a href="https://blogs.msdn.microsoft.com/sergey_babkins_blog/2016/02/26/powershell-windows-clusters/">PowerShell support in the Windows clusters</a>: there are many components involved and each one has its own PowerShell commandlets for management but they don't mesh well together with each other, need to be combined in the very non-obvious ways with no big goal-oriented commandlets, and some things turn out to be not really doable at all.<br /><br />The obvious objection is "but how do we test all these changes"? This is where the automated testing comes in, and it's such a big subject that I wrote a <a href="http://babkin-cep.blogspot.com/2016/12/on-testing.html">whole separate post about that</a>. But it's really a bogus question. When you write a completely new system,you still test each chunk of code as you write it, and if you don't have the automated testing then you just test it manually. The difference is that in an incomplete system your chunk will have only the limited interactions, so you would be able to test only some of its effects. In a complete system you get a much more complete coverage. And if the old system doesn't have any automated tests then it's a good way to start them: for each chunk write the tests before replacing it, and then use them to check that the the replaced chunk still works in the same way. Another important point, a chunk doesn't have to be all in one place, it might be distributed, such as an one-line change in a thousand files.<br /><br />The small changes don't have to go together with the frequent releases. But every time you do a release one way or another, you get the issue "we have to complete all the changes before the release!" and it gets worse if you want to do the more frequent releases. Well, you definitely do want to make sure that the things don't get accidentally broken at the last moment, so the commits have to slow down just before the release. But it doesn't mean that you can't include the changes at all. Any change is fine to include as long as it doesn't break the existing functionality. The new functionality doesn't have to work, as long as it doesn't get called in the production environment nobody cares about that, it only has to not break the old functionality. This way you can integrate the changes a lot earlier, and then spend a lot less effort merging them together.<br /><br />In the <a href="http://babkin-cep.blogspot.com/2016/12/on-testing.html">post on testing</a> I've already said that an important part of the incrementality is the incremental tests, being ability to run easily the immediate subset of tests affected by a change. But a prerequisite for it is the incrementality of the build: the ability to build quickly all the components dependent on the change, all the way to the final package, while skipping the build of the unchanged parts of the tree. "Quickly" here means right away, not in some kind of a nightly build. This does wonders for the turnaround time. Obviously, in some cases, like a fundamental library, pretty much all of the tree will depend on it. But then the incrementality should be interpreted as the ability to build a few of the representative projects based on it to test that they didn't break. If the full nightly test shows that some dependent components got broken, the ability to rebuild them quickly also makes wonders for the turnaround of the investigation.<br /><br />Another thing about the build is that I think the purely rules-based builds are bad. It's not that having the common rules is bad, they're actually quite convenient. But the build system must also allow to do the quick one-offs. These one-offs are very important for the automation of the build. They allow to write quickly say a small Perl script that would do some kind of transformation or code generation. A build system without such one-offs turns the hour-long tasks into the week-long ones. So just please do support the full proper make and the scripting environment, not some crap like Cmake. The counter-argument is of course "but we need to build on multiple platforms including Windows". Yeah, building on Windows sucks, and the only decent solution I see it to bring a decent environment of the portable tools to each of your build platforms.<br /><br />Another very simple but important thing about the build is that it must detect the errors. The Java-based tools in particular are notorious for ignoring the status codes, so when something breaks, the only way you know it is by the result files being absent. Or not even by them being absent but them failing in the tests. This is bad. All the errors have to be detected as early as possible.<br /><br />This looks like a good place for another related rant, on reliability. Basically, when something works it should work reliably. Einstein said "The definition of insanity is doing the same thing over and over again and expecting different results." But in reality we don't control everything, and when these hidden changes cause a different result of the same actions, it's just maddening and time-consuming. The good code, including the tests and build, should work reliably. If the underlying systems are known to have outages, it should retry and still work reliably on top of them and not propagate this unreliability up. Every step the unreliability propagates makes it more difficult to fix, so it's best nipped in the bud. There of course are the reasonable limits too, the calls should not get stuck retrying forever, and probably not ever for an hour. Although retrying for an hour might be fine for a scheduled nightly run, but for a manual run it's way too long. And when a program is retrying it should not be silent, it should be telling what its is doing.<br /><br />Next item is the low interdependencies. Basically, it means that the people should be able to work without stepping too much on each other. You might ask: but doesn't the principle of the small code base go against it? More code should allow more people to work on its part, right? No, it doesn't work like this. The catch is that if you have more code solving the same problem, any changes to it mean that you have to change about the same <i>percentage</i> of the code. This is why a sprawling code base is slow in development, because any change requires changing a lot of code. But this is also why it doesn't allow more people to work on it in parallel. So the corollary is that if you add too many people to a project, they will start stepping on each other a lot and the productivity will drop.<br /><br />The other major kind of interaction is between the people in general. If you need to do a 5-minute task, it takes you 5 minutes. But if you need someone else to do a 5-minute task, it's likely to take you a day of interaction. Which still might be faster than learning how to do it yourself but if the task is repetitive then this becomes a major drag. To work fast, this drag has to be reduced and eliminated whenever possible. One of the consequences is that the posessive code ownership is bad. Now, having someone knowledgeable available to talk about a part of code is good, and it also helps to discuss the general directions of the code to avoid breaking things for other people. But there should not be a single-person or single-team bottleneck for changes to a certain part of code, nor any single person controlling what is allowed to go in. There are other reasons to avoid these things too but even the speed of development should be a sufficient reason. Another corollary is that the strict code style requirements are bad. The small style variations just don't matter, and the enforcement is a lot of pain with no gain whatsoever.<br /><br />Another case where the excessive interaction shows is planning. Have you seen these beautiful Gantt charts and critical path diagrams, with the carefully planned moments of interdependencies? Well, they're all garbage. They might be an interesting exercise on the way to the better understanding of the project but things never work out like this in reality. In reality all the interdependency points end up as a mess. So there is no point in overplanning. Instead I really like the saying of my past manager: "We can plan an aggressive schedule because we don't mind it slipping once in a while". When combined with the idea of the incrementality, this points to the following concept: the detail level of the plans must be tied to their distance. A detailed planning of the far-off items doesn't make sense because they are sure to change before we get there. These items should be on the list, so that we can keep track of the direction, but only in the general terms. Only the very near-term items need to be planned in detail, and the farther in the future the less detail is needed. Though the very near-term items have their Heisenbergian uncertainty as well. In my experience the minimal unit of time for planning is a week. If you have five items that you expect to take one day each, they will never work out like this. Some of them will take an hour and some will take three days. But if you lump all five together, they will take together a week with a decent amount of certainty.<br /><br />Since I've already talked about the scripted automation throughout the text, this is a wrap for this post. The things I've described look kind of small but they really are important, and I believe that taken together the do amount to the 10x difference in productivity. What do you think?<br /><br />P.S. Someone else's thoughts on the subject of 10x: <a href="http://antirez.com/news/112">http://antirez.com/news/112</a><br /><br /><br />Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com3tag:blogger.com,1999:blog-3195352510924611504.post-55452373423872998842017-01-06T15:52:00.002-05:002017-01-13T17:08:14.950-05:00Bayesian networks & neural networksI've been recently reading about the neural networks. By the way, here are some introductory links:<br /><br />I can most highly recommend this document that explains in a very clear and simple way how the training of the neural networks through the backpropagation works:<br /><a href="http://www.numericinsight.com/uploads/A_Gentle_Introduction_to_Backpropagation.pdf">http://www.numericinsight.com/uploads/A_Gentle_Introduction_to_Backpropagation.pdf</a><br /><br />A simple introductory series of 6 (and maybe growing to more) articles starting with this one:<br /><a href="https://medium.com/@ageitgey/machine-learning-is-fun-80ea3ec3c471#.hxsoanuo2">https://medium.com/@ageitgey/machine-learning-is-fun-80ea3ec3c471#.hxsoanuo2</a><br /><br />Some other links:<br /><a href="http://karpathy.github.io/2015/05/21/rnn-effectiveness/">http://karpathy.github.io/2015/05/21/rnn-effectiveness/</a><br /><a href="http://colah.github.io/posts/2015-08-Understanding-LSTMs/">http://colah.github.io/posts/2015-08-Understanding-LSTMs/</a><br /><a href="https://colah.github.io/posts/2015-01-Visualizing-Representations/">https://colah.github.io/posts/2015-01-Visualizing-Representations/</a> <br /><a href="http://mmlind.github.io/Deep_Neural_Network_for_MNIST_Handwriting_Recognition/">http://mmlind.github.io/Deep_Neural_Network_for_MNIST_Handwriting_Recognition/</a><br /><br />Also, the apparently the father of the deep neural networks is G.E. Hinton, and you may also want to search for the articles by Harry Shum. Hinton's home page is:<br /><a href="https://www.cs.toronto.edu/~hinton/">https://www.cs.toronto.edu/~hinton/</a><br />that seems to have a bunch of the links to his courses but I haven't looked at them yet. <br /><br /><br />As you can see from the introductory reading, each neuron in a neural network is a pretty simple machine: it takes some input values, multiples them by some coefficients, adds the results up, and then passes the result through a nonlinear (usually some kind of a sigmoid) function. The whole thing can be written as an expression:<br /><br />result = nonlinear( sum [for inputs i] (input<sub>i</sub> * C<sub>i</sub>) )<br /><br />The nonlinear part is pretty much what we do at the end of the Bayesian computations: if the probability of a hypothesis is above some level, we accept it as true, i.e. in other words pull it up to 1, if it's below some equal or lower level we reject it, i.e. pull it down to 0, and if these levels are not the same and the probability is in the middle then we leave it as some value in the middle, probably modified in some way from the original value.<br /><br />The sum part is pretty much the same as the sum done in AdaBoost. AdaBoost does the sum of logarithms. And I've shown in <a href="http://babkin-cep.blogspot.com/2016/05/adaboost-3-and-bayesian-logic.html">the previous posts</a> that this sum can be converted to a logarithm of a product, and then the product can be seen as a Bayesian computation expressed as chances, and the logarithm being a part of the decision-making process that converts the resulting chance value to a positive-or-negative value. So we can apply the same approach to the sum in the neuron, say that the values it sums up are logarithms, convert it to the logarithm of a product, make the logarithm a part of the final nonlinear function, and then the remaining product can be seen as a Bayesian computation on chances.<br /><br />This pretty much means that a neuron can be seen as a Bayesian machine.<br /><br />And guess what, apparently there is also such a thing as a Bayesian network. There people take multiple Bayesian machines, and connect the results of one set of machines as the input events to the Bayesian machines of the next level. For all I can tell, the major benefit of the handling of the problem when some hypotheses are indicated by a XOR of the input events, similarly to the splitting of the hypotheses into two and then merging them afterwards <a href="http://babkin-cep.blogspot.com/2015/10/bayes-11-what-it-all-really-means.html">like I've shown before</a> but instead of the external arithmetics the logic being folded into the Bayesian computation of the second level.<br /><br />But if a neuron can be seen as a Bayesian machine then the neural networks can also be seen as the Bayesian networks! The only difference being that in the Bayesian networks the first-level (and in general intermediate-level) hypotheses are hand-picked while the neural networks find these intermediate hypotheses on their own during the training.<br /><br />I wonder if this is something widely known, or not known, or known to be wrong?Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-88681204547838303342016-12-26T18:51:00.001-05:002017-01-04T15:02:53.902-05:00On testingI've started writing another post (hope to complete it somewhere soon), and as one of the offshoots it brought my thoughts to the automated testing. The automated testing had been a great new thing, then not so new and kind of a humbug thing, then "instead of testing we'll just run a canary in production", then maybe it's doing a bit of a come-back. I think the fundamental reason for automated testing dropping from the great new thing is (besides the natural cycle of newer greater things) is that there are multiple ways to do it and a lot of these ways being wrong. Let me elaborate.<br /><br />I think that the most important and most often missed point about the automated testing is this: <b>Tests are a development tool</b>. The right tests do increase the quality of the product but first and foremost they increase the productivity of the developers while doing so.<br /><br />A popular analogy of the programs is that a program is like a house, and writing a program is like building a house. I'd say not, this analogy is wrong and misleading. A program is not like a house, it's like <b>a drawing or a blueprint</b> of a house. A house is an analogy of what a program produces.<br /><br />The engineering drawings are not done much with the pencil and paper nowadays, they're usually done in a CAD system. A CAD system not only records the drawing but also allows to model the items on the drawing and test that they will perform adequately in reality before they're brought to reality. The automated tests are the CAD for programs. <br /><br />The CADs for drawings require entering the extra information to be able to do their modeling. So do the automated tests. It's an overhead but if done right this overhead is an acceptable one, bringing more benefits than overhead.<br /><br />The first obvious benefit of the automated tests is that they make the programs robust. To bring the house analogy again, like a house of bricks, not a house of cards. You might remember the saying about "If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization". Not with the automated tests, not any more. With the automated tests you don't get into the situation "I've just changed one little thing and it all suddenly fell apart" any more.<br /><br />But that's again just the reliability. Where is the productivity coming from? The major benefit for the productivity is that you don't need to do so much analysis any more. You can always change things quickly and see what happens in a test environment. This is also very useful for tracing the dependencies: if you change something and the product breaks, you see where it breaks. This is a major, major item that makes the programming much more tractable and turns the devilishly complicated modifications into the reasonably straightforward ones.<br /><br />The programming tasks tend to come in two varieties: The first variety is the straightforward one: you just sit and write what the program needs to be doing, or even better copy a chunk of the existing code and modify it to do the new thing you need. The second variety happens when the thing you need to do hits the limitations of the infrastructure you have. It usually has some kind of a paradox in it, when you need to do <i>this</i> and <i>that</i> but if you do <i>this</i> then <i>that</i> falls apart and vice versa. <br /><br />When you have this second kind of a task on your hands, you need to stretch your infrastructure to support both <i>this</i> and <i>that</i> at the same time. And this is the situation when things tend to get broken. I really like to solve such problems in two steps:<br /><br />1. Stretch the infrastructure to support the new functionality but don't add the new functionality.<br />2. Add the new functionality.<br /><br />At the end of step 1 the program works differently inside but in exactly the same way on the outside. Having the automated tests, they all still pass in exactly the same way as before. And only after the second step there will be the new functionality that will require the new tests and/or modification of the old tests.<br /><br />But as I've said before, not all the tests are useful. I have formulated the following principles of the useful tests:<br /><br /><ul><li>Easily discoverable.</li><li>Fast running. </li><li>Fast starting.</li><li>Easy to run both by the developer and in an automated scheduler.</li><li>As much end-to-end as possible.</li><li>Test support built into the product and visible as an external API.</li></ul><br />What do these things mean? Let's look in detail.<br /><br />Easily discoverable means that the tests must live somewhere next to the code. If I change a line of code, I must be able to find the most relevant tests easily and run them. It's an easy mantra: changed something, run the tests on it. And doing so must be easy, even if the code is not familiar to the developer.<br /><br />Fast running means that the tests should try not to take too much time. This comes partially to trying to make the tests fast and partially to the ability to run only the relevant subsets of the tests. The way of work here is: changed a few lines, run the basic tests, change a few more lines, run the tests again. The basic subset for some lines should be easy to specify and take seconds, or at worst a few minutes to run. This is sometimes quite complicated when testing the handling of the time-based events. For time-based events it's very good to have a concept of the application time that can be consistently accelerated at will. Another typical pitfall is the situation "I've requested to do this, how do I find out if it's done yet, before running the next step of the test?". This is something that is easy to resolve by adding the proper asynchronous notifications to the code, and making everyone's life easier along the way. The fast running concept is also useful for debugging of the failed tests: how long does it take to re-run the test and reproduce the problem or verify the fix?<br /><br />Fast starting is related to the fast running but is a bit different. It has to do with the initialization. The initialization should be fast. If you're running a large test suite of ten thousand tests, you can afford to have a lengthy initialization up front. If you run one small test, you can't have this lengthy initialization every time. If you really need this initialization, there must be a way to do it once and then repeatedly re-run the individual test.<br /><br />The other aspect of the fast starting is that the programmer must be able to run the tests directly from the development environment. The repeating cycle is write-compile-test. The tests must directly and automatically consume the newly compiled code. There must not be any need to commit the code nor to run it through some kind of an official build.<br /><br />This partially overlaps with the next requirement: the tests being easy to run both directly by the programmer and in an automated system that goes together with the automated official build. The need to run the tests directly comes from the need for the efficiency of development and from the use of the tests as a CAD. There shouldn't be any special hoops to jump through for including the tests into the official builds either, it should <i>just work</i> once checked in. This is another place where keeping the tests next to the code comes in: both the tests and the code must be a part of the same versioning system and must come in as a single commit. Well, not necessary literally as a single commit, personally I like to do a lot of small partial commits on my personal branch as I write the code and tests, but when it comes up into the official branch, the code of the program and of the tests must come together.<br /><br />The end-to-end part is very important from both the standpoint of the cost of the tests and of their usefulness. It needs a good amount of elaboration. There had been much widespread love professed for the unit tests, and then also as much disillusionment. I think the reason for this is that the unit tests as they're often understood are basically crap. People try to test each function, and this tends to both require a lot of code and be fairly useless, as many functions just don't have enough substance to test. Mind you, there are exceptions: if a function does something complicated or is a part of a public API, it really should have some tests. But if a function is straightforward, there is no point in looking at it in isolation. Any issues will come up anyway as a part of a bigger test of the code that uses this function.<br /><br />The worst possible thing is the use of the mocks: those result in just the tautologies when the same thing is written twice and compared. These "tests" test only that the function goes through the supposedly-right motions, not that these motions are actually right and produce the proper result. This produces many horrible failures on deployment to production. A real test must check that the result is correct. If some API you use changes under it and creaks the result, a proper test will detect it. If you really have to use mocks, you must use them at least one level down. I.e. if you're testing a function in API1 that calls API2 that calls API3, you might sometimes get something useful by mocking the functions in API3 but never mock the functions in API2.<br /><br />Another benefit of the integrated testing is that it often turns up bugs where you didn't expect to. The real programs are full of complex interactions, and the more you exercise these interactions, the better are your tests.<br /><br />So my concept of an unit test is an "integrated unit test": a test for one feature of an externally visible API. Note that there might be some complex internal components that should be seen as a layer of API and tested accordingly, especially if they are reused in multiple places. But the general rule is the same. This both increases the quality and cuts down on the amount of the tautology code.And it also facilitates the stretch-then-extend model I've described above: if a test tests what the API call does, not how it does that then you can change the underlying implementation without any change to the tests, and verify that all the tests still pass, your external API hasn't changed. This is a very important and fundamental ability of tests-as-CAD, if your tests don't have it then they are outright crap.<br /><br />Some readers might now say: but doesn't that end-to-end approach contradict the requirement of the tests being fast? Doesn't this integration include the lengthy initialization? When done right, no, it doesn't. The answer comes two-fold: First, when the tests are more integrated, there are fewer of them, and due to the integration they cover more of the internal interactions. Second, this forces you to make the initialization not lengthy. It might require a little extra work but it's worth it, and your customers will thank you for that.<br /><br />This brings us to the last point: when you've built the support of the testing into your product, keep it there for the public releases and make it publicly available. This will make the life of your customers a lot easier, allowing them to easily build their own tests on top of your infrastructure. Instead of writing the stupid mocks, it's much better to have a side-door in the underlying APIs that would make them return the values you need to test your corner cases, and do it internally consistent with the rest of their state. I.e. if you mock some kind of an underlying error, how do you know that you mock it right, in the same way as the underlying API would manifest it? You don't. But if an underlying API has a way to ask it to simulate this error, it would simulate properly, as it does in the "real life".<br /><br />Some people might now ask: But what about security? But what about performance?<br /><br />As far as the security goes, security-through-obscurity is a bad idea anyway. Obviously, don't give the test access to the random entities, have a separate "access panel" in your API for the tests that would only allow the authorized connections. And if your product is a library then the security-through-obscurity is a REAL bad idea, and there are no random entities to start with. Make things accessible to your user. There is no good reason for a class to have any <i>private</i> members other than for the non-existing components. The most protection a class might ever need is <i>protected</i>. The same, there is no good reason for a class to have any <i>final</i> members. And if you're worried that someone will use the testing part of the API to do something that the main API doesn't allow then the solution is simple: make the main API allow it, otherwise you're crippling it intentionally.<br /><br />As far as the performance goes, the overhead is usually pretty low. Note that you don't need to embed the test support all over the place, only at the substantial API layers. These API layers usually deal with the larger-scale concepts, not the ones you'd find at the bottom of a triple-nested loop. Sometimes there are exceptions to this rule but nothing that can't be resolved in some way, and the result after resolution is always better than before.<br /><br />Hope that I've convinced you that the right testing infrastructure makes a world of difference in the software development, not only improves the quality but also makes it faster and cheaper. With the right tests you'll never get stuck with "it's working somehow, don't touch it" or "we will need to study for a year before making that change". Or to use again the building analogy (yeah, I've just decried it as wrong but I'll use it differently), the tests are not like the scaffolding on a building that you discard after the construction is completed, they are like the maintenance passages and hatches that keep the building in an inhabitable condition throughout its life.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com2tag:blogger.com,1999:blog-3195352510924611504.post-52537327909016171232016-08-27T01:26:00.002-04:002016-08-27T01:26:49.532-04:00AdaBoost 9: Boost by majority afterthoughtAfter some time I've realized that all this <a href="http://babkin-cep.blogspot.com/2016/08/adaboost-8-boost-by-majority.html">monkeying</a> with the conditional probabilities in the Bayesian table is not necessary. You can just throw away a whole training case or a part of it and continue like nothing happened, the probabilities should stay consistent anyway. After all, the point of adjusting the weights after each round opposite to how the run-time weights would be changed is to give each training case an equal chance. But if we don't want to give some training case an equal chance then there is no point in treating it equally, an ignored training case can be simply ignored.<br /><br />Another thought is that it looks like the two-pass approach can be used to find what training cases to throw away in a dynamic way. We can do it by splitting the set of available cases randomly in half. Then use one half for the first pass of training of N rounds and remember the weights throughout it. Then test the effectiveness of this training on the second half of the cases. But not just run the N rounds in the test. Instead, keep the test results for using only 1 round, 2 rounds, 3 rounds, and so on all the way to the N rounds. Then see the number of rounds on which the test did best, say K rounds. Going back to the training weights, we can find, what training cases were not getting guessed well at K rounds. We can mark them as outliers. Then repeat the same thing swapping the two halves, and find the outliers in the second half. Then throw away the outliers and do the second pass of training on the rest of the cases.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-83909188227911025772016-08-20T03:01:00.000-04:002016-08-20T03:01:10.252-04:00AdaBoost 8: Boost by majorityWhen I <a href="http://babkin-cep.blogspot.com/2016/05/adaboost-3-and-bayesian-logic.html">wrote before</a><br /><br /><i>The premise of boosting is that we're able to find a number of methods (what they call "hypotheses" in AdaBoost) to predict the correct outcomes of the training cases, each method correctly predicting more than 50% of the training cases. Then if we collect a large-ish number of these methods, we can predict the correct outcomes of all the training cases simply by averaging the predictions of these methods. And the other cases will follow the training cases (unless an overfitting happens). Since more than 50% of the cases are correctly predicted by each method, after the averaging more than 50% of the votes for each training case will be correct, and thus the result will be correct too. Of course this depends on the correct predictions being distributed pretty evenly among the cases. If we have a thousand methods that predict correctly the same cases and incorrectly the other cases, obviously after averaging these other cases will still be predicted incorrectly. So the selection of methods must somehow shuffle the preference for the cases, so that the next picked method will predict well the cases that have been predicted poorly by the previous picked methods. That's it, that's the whole basic idea. It's a bit of an oversimplification but it's easy to understand.</i><br /><br />I really did mean it as an oversimplification, since AdaBoost uses the Bayesian decisions to do much better than the simple majority counting. Little did I know that there actually is the method of Boost By Majority (BBM) that does just the counting. It has some other differences but more about that later.<br /><br />The simple averaging can be simulated through the Bayesian means too. Just use the same confidence for each event. Incidentally, that's what the NonAdaBoost algorithm, also known as epsilon-Boost does: it looks for the weak hypotheses that have at least a fixed "edge" gamma (i.e. the probability of the right guess being at least 0.5+gamma) and then always sets the confidence C=0.5+gamma, and uses the same value to adjust the weights of the training cases.<br /><br />The NonAdaBoost is essentially a version of AdaBoost with a fixed confidence, and suffering from this defect. But Boost By Majority has another big difference: the way it adjusts the weight of the training cases. The formula it uses is pretty complicated, so I won't even try to reproduce it here. But here is the gist: it keeps track of how many rounds are left to the end of the boosting and what is the balance of the votes collected by each training case. If the modulo of the balance is higher than the number of rounds left, it means that the fate of this case can't be changed any more: it's either guaranteed to be guessed right if the balance is positive or guaranteed to be guessed wrong if the balance is negative, so the algorithm gives up on these cases. It gives the most weight to the most undecided cases, and spreads the rest of the weights in the bell shape of the binomial distribution. The result is that unlike AdaBoost and NonAdaBoost, BBM doesn't concentrate on the hardest cases that are likely to be the noise in the data anyway, and thus reduces the overfitting.<br /><br />The last chapter of the book is about a combination of AdaBoost and BBM called BrownBoost (from the Brownian motion), or also "boosting in continuous time". It starts with the idea that if the returned partial hypothesis has a higher edge than minimally needed, it might still have enough edge after the re-weighting the training cases, then it can be directly reused on the next round too without searching for a new one, and so on until its edge wears off. This gets developed into an algorithm that uses a real number in the range [0,1) instead of the round count, with the actual rounds moving the current point on it by the varying amounts. The speed of the movement is determined by the pre-set desired training error. This training error gets reached when the end of the range is reached. If the target error is set to 0, the algorithm behaves in the same way as AdaBoost.<br /><br />The downside is that the algorithm is complex, there isn't even a formula for determining the confidence values for each partial hypothesis. Instead you get a system of two equations that connect this confidence value and advancement through the range to be solved numerically. In the great scheme of things it's not a big deal, after all, compared to the finding of the partial hypotheses this shouldn't be such a big overhead. But it's not easy to think of. And the proof of the validity of this algorithm is quite complicated.<br /><br />I can't help thinking of a couple of simpler ideas. <br /><br />The first idea, or should I say guess, is that when we do AdaBoost, it fits into a Bayesian model. So if we keep this Bayesian model consistent, the boosting should still be valid. Obviously, I have no proper proof of that but it looks like a reasonable assumption. There is an easy way to take some training case (or a part of its weight) out of rotation and still keep the Bayesian model consistent. <br /><br />Remember, <a href="http://babkin-cep.blogspot.com/2016/05/adaboost-3-and-bayesian-logic.html">previously I've described</a> that we start with a Bayesian table that contains each individual training case<br /><br /><pre>CaseId Weight Outcome Ev1 ... EvT<br />1 1 * true 1 ... 1<br />...<br />M 1 * false 0 ... 0</pre><br />Which then gets conflated into a two-line table, all the cases with the true outcome combined into one line, and the false ones into another line. The conditional probabilities in the table get averaged during conflation but since it's an averaging of all ones (or all zeroes), the result is still one (or zero).<br /><br /><pre>Weight Outcome Ev1 ... EvT<br />1 * true 1 ... 1<br />1 * false 0 ... 0</pre><br />To take a training case out of rotation, we just change its conditional probability in all the following events (that match the AdaBoost's partial hypotheses) to 0.5. This says that no future events will affect it. And accordingly we set the AdaBoost weight of it to 0. Such decisions can be made according to any algorithm, matching any desired curve. <br /><br />For example, suppose that we decide to take a case out of rotation when its weight relative to the sum of all weights reaches 0.1 (this is probably not a very good rule, since it allows at most 10 cases to be excluded, but simple for the sake of demonstration). Suppose that its a case with the true ("1") outcome. And suppose that all the weights of all the true cases are totaling to the same value as of all the false cases, each of them having the relative weight of 0.5 (not very likely in reality but just as good a number as any other).<br /><br />After the disablement, the conditional probability of the true cases will become ((0.5*0.1) + (1 * 0.4))/0.5 = 0.9.<br /><br /><pre>Weight Outcome Ev1 ... EvN-1 EvN ...<br />1 * true 1 ... 1 0.9 ...<br />1 * false 0 ... 0 0 ...</pre><br />Once a training case gets disabled, it stays disabled for all the future rounds, and the AdaBoost keeps acting only on the weights of those training cases that still have 1 or 0 for the conditional probability. Obviously, the more you disable, the less will be the effect of the following rounds when the Bayesian computation runs.<br /><br />Interestingly though the edges of the partial hypotheses will be higher. Remember, the training cases that get thrown away get it for being difficult to predict. So suppose the partial hypothesis EvN would have returned the confidence of 0.8 if that case wasn't thrown away and had guessed that case wrong. When we throw away the stubborn case, that would become 0.8 out of former 0.9, so the confidence becomes 0.8/0.9 = 0.89, an improvement! <br /><br />However all this throwing-away has no effect on the previous rounds, these are already set in stone. Which begs an easy solution which is my second idea: why not do two passes of AdaBoost? After the first pass look at the final weights of the training cases to determine the most stubborn ones. Throw them away and do the second pass from scratch. After all, BrownBoost requires an adjustment of the target training error which gets done by running it multiple times with the different values and then selecting the best one. Doing two passes of AdaBoost isn't any worse than that.<br />Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-84267594398101350542016-08-06T16:01:00.005-04:002016-08-06T16:01:56.092-04:00Bayes 23: enumerated events revisitedAnother idea that I've glimpsed from the book on boosting is the handling of the enumerated events, previously described in the <a href="http://babkin-cep.blogspot.com/2015/12/bayes-19-model-information-and.html">part 19</a>. The <a href="http://babkin-cep.blogspot.com/2016/07/adaboost-6.html">part 6 of my notes on boosting</a> describes how the decision stumps can be treated as only "half-stumps": actionable if the answer is "yes" and non-actionable if the answer is "no" (or vice versa). This is actually the same thing as <a href="http://babkin-cep.blogspot.com/2015/10/bayes-2-hypotheses-and-events.html">I complained of before</a> as the mistreatment of the Bayesian formula where the negative answer is treated the same as the lack of answer. But taken in the right context, it makes sense.<br /><br />If we take a complementary pair of such half-symptoms (asking the same question, and one of them equaling the negative answers with "don't know", another one equaling the positive answer with "don't know"), their combined effect on the probability of the hypotheses will be exactly the same as of one full symptom. In the weight-based model, the weights of the hypotheses after the complementary pair will be only half of those after one full symptom but they will all be scaled proportionally, making no difference. Alternatively, if we equal the negative answers not with "don't know" but with "irrelevant", even the weights will stay the same.<br /><br />The interesting thing is that these half-symptoms can be straightforwardly extended to the multiple-choice questions. Each choice can be equaled with one half-symptom. So if the answer to this choice is "yes" then it takes effect, if "no" then it gets skipped. In the end exactly one choice takes effect. Or potentially the answer can also be "none of the above" and then this symptom will be simply skipped. It should also be relatively straightforward to accommodate the answers like "it's probably one of these two", taking both answers at half-weight. I didn't work through the exact formulas yet but I think it shouldn't be difficult.<br /><br />The approach of taking the answer at a partial weight also provides a possible answer to "should we treat this problem as model-specific or generic?": it allows to mix both together, taking say the model-specific approach at the weight 0.99 and the generic at 0.01. Then if the model-specific approach finds a matching hypothesis, great, if not then the answer found with the generic approach will outweigh it. This weight of the generic approach should be higher than the confidence cap of the "can't happen" answer: the generic weight of 0.01 would probably work decently well together with the capping probability of 0.001.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-53371409125000807632016-08-06T14:53:00.005-04:002016-08-06T14:53:56.624-04:00Bayes 22: overfitting revisitedPreviously I've been saying that I didn't experience overfitting in the Bayesian models, and pretty much discounted it. Now I've read a model of overfitting in the book on AdaBoost, and I understand why. Here is the gist, with some of my thoughts included.<br /><br />The overfitting happens when the model starts picking the peculiarities of the particular training set rather than the general properties. It's down to the noise in the data: if the data contains random noise, only the cases without the noise can be predicted well on the general principles, and the noisy ones are bound to be mispredicted. The training data also contains noise. Since the noise is random, the noise in the test data (an in the future real-world cases) won't follow the noise in the training data closely. If the model starts following the noise in the training data too closely, it will mispredict the well-behaved cases in the test data, in addition to the noisy test cases. For all I can tell, this means that the overfitting magnifies the noise in the quadratic proportion, with probabilities:<br /><br />P(good prediction) = P(no noise in the training data) * P(no noise in the test data)<br /><br />If the model makes the decisions based on the "yes-no" questions (AKA binary symptoms), picking up the general trends takes a relatively small number of yes-no questions, because their effects are systematic. The effects of the noise are random, so each noisy training case is likely to require at least one extra yes-no question to tell it apart. If there is a substantial number of noisy cases in the training data, a lot of extra questions would be needed to tell them all apart. So the rule of thumb is, if there are few questions in the model compared to the number of the training cases, not much overfitting will happen.<br /><br />In the models I was working with, there were tens of thousands of the training cases and only hundreds of symptoms. So there wasn't such a big chance of overfitting in general. Even if you say "but we should count the symptoms per outcome", there still were only low hundreds of outcomes, and if we multiply 100 symptoms by 100 outcomes, it's still only 10K decision points in the table, the same order of magnitude as the number of the training cases. <br /><br />There also was very little noise as such in the data I've dealt with. If you do diagnostics, you get the natural testing: if the fix doesn't work, the client will come back. There is of course the problem of whether you've changed too many parts. It can be controlled to a degree by looking for training only at the cases where the fix was done at the first attempt. Though you still can't get the complete confidence for the cases where more than one part was changed. And of course if you don't look at the cases that required multiple attempts, it means that you're not learning to diagnose the more difficult cases.<br /><br />But there was a particular kind of noise even in this kind of fairly clean data: the noise of multiple problems occurring or not occurring together in various combinations. If the model is sensitive to whether it had seen a particular combination or not, the randomness of the combinations means that they represent a random noise. And I've spent quite a bit of effort on reducing this dependence in the logic and on reducing this noise by preprocessing the training data. Which all amounts to reducing the overfitting. So I was wrong, there was an overfitting, just I didn't recognize it.<br /><br />Actually, I think this can be used as a demonstration of the relation between the number of symptoms and amount of overfitting. If we're looking to pick only one correct outcome, the number of questions is just the number of questions, which was in hundreds for me. Much lower than the number of the training cases, and very little overfitting had a chance to happen. Yes, there were hundreds of possible outcomes but only one of them gets picked, and the number of questions that are used is the number of questions that affect it. But if we're looking at picking correctly all the outcomes, the number of questions gets multiplied by the number of outcomes. In the system I worked on, the total was comparable to the number of training cases, and the overfitting became noticeable. It would probably become even worse if the Bayesian table contained the rows not just for the outcomes but for the different ways to achieve these outcomes, like I've described in this series of posts. So with extra complexity you win on precision but the same precision magnifies the effects of overfitting. The sweet spot should be somewhere in the middle and depend a lot on the amount of noise in the data.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-43763720990717536832016-08-06T13:15:00.000-04:002016-08-06T13:15:23.173-04:00AdaBoost 7: multi-class & unseen combinationsThe basic idea behind the multi-class (and also multi-label, i.e. where each case may have more than one outcome) AdaBoost can be described as boosting the recognition of all the outcomes in parallel. It takes the "yes or no" dichotomies for all the outcomes, and on each round it tries to find such a partial hypothesis where the sum of Z for all of them is minimal. This is very similar to what was described in the <a href="http://babkin-cep.blogspot.com/2016/07/adaboost-6.html">part 6</a>, where multiple ranges were added, each with its own confidence. The difference is in the formula for the final computation: in the multi-class version there is a separate formula for each class that uses only the confidence values that all the training rounds computed for this particular class.<br /><br />There is also a possible optimization for the situation where there may be only one outcome per case (i.e. single-label), saying that the mapping between the dichotomies used in the logic above and the outcomes doesn't have to be one-to-one. Instead each outcomes can be mapped to a unique combination (or multiple combinations) of the dichotomies. The dichotomies can be selected on some smart way, say if we're trying to recognize the handwritten digits, they can be "pointy vs roundy", "a loop on the bottom vs no loop at the bottom" etc. Or just by just dividing the outcomes in half in some blind way, like "0,1,2,3,4 vs 5,6,7,8,9", "0,2,4,6,8 vs 1,3,5,7,9" etc. <br /><br />Returning to the multi-label situation, one of the problems I've experienced with it is the ability to recognize the combinations of outcomes that weren't present in the training data. That is, the outcomes were present but none of the training cases had exactly this combination. For the basic diagnostics, this can be discounted by saying "but what's the percentage of such cases" but when you start pushing the quality of diagnosis around 95%, it turns out that great many of the remaining misdiagnosed cases fall into this category.<br /><br />AdaBoost doesn't have any built-in solution for this problem. The solution it produces is only as good as the underlying algorithm. There is nothing in AdaBoost that puts the pressure on the underlying algorithm to recognize the combinations that aren't present in the training data. If the underlying algorithm can do it anyway (and perhaps despite the pressure from AdaBoost), the resulting combined formula will be able to do it too. If it can't then the combined formula won't either. The simple algorithms like the decision stumps can't. <br /><br />But maybe some multi-pass schemes can be devised. Run the boosting once, get a set of the candidate symptoms (i.e. partial hypotheses). Use these symptoms on the training cases to try to differentiate, which symptom is related to which outcome. Then run the boosting the second time from scratch, only this time with the relevance knowledge mixed in: whenever a symptom that is close to an irrelevant one is tested on a training case, make it return "I don't know", i.e. the confidence 0.5. This will shift the choice of symptoms. Obviously, if using the resulting formula, the same pruning of irrelevance has to be applied there in the same way. The symptoms from the second pass can be re-tested for relevance, and if any change is found, the third pass can be made, and so on. <br /><br />Or even better, perhaps this logic can be merged directly into each round of the underlying algorithm in one pass of AdaBoost: when a candidate partial hypothesis (i.e. symptom) is found, measure its relevance right there and change its Z-value accordingly. Pick the candidate that has the lowest Z-value even after it has been corrected for the relevance. Include the relevance information into the partial hypothesis.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-57880451011481928442016-08-02T02:39:00.002-04:002016-08-02T02:39:42.930-04:00Bayes 21: mutual exclusivity and independence revisitedI've been thinking about the use of AdaBoost on the multi-class problems, and I've accidentally realized what is going on when I've used the combination of the mutually exclusive and of independent computation of hypotheses, as described in the <a href="http://babkin-cep.blogspot.com/2015/10/bayes-10-independence-and-relevance.html">part 10</a>.<br /><br />Basically, the question boils down to the following: if the mutually exclusive computation shows that multiple hypotheses have risen to the top in about equal proportions (and consequently every one of them getting the probability below 0.5), how could it happen that in the independent computation each one of them would be above 0.5? If the training data had each case resulting in only one true hypothesis, the probabilities computed both ways would be exactly the same, in the independent version the probability of ~H being exactly equal to the sum of probabilities of the other hypotheses.<br /><br />The answer lies in the training cases where multiple hypotheses were true. If we use the weight-based approach, it becomes easy to see that if a new case matches such a training case, it would bring all the hypotheses in this case to the top simultaneously. So the independent approach simply emphasizes the handling of these training cases. Equivalently, such training cases can be labeled with pseudo-hypotheses, and then the weight of these pseudo-hypotheses be added to the "pure" hypotheses. For example, let's consider re-labeling of the example from the part 10:<br /><br /># tab09_01.txt and tab10_01.txt<br /> evA evB evC<br />1 * hyp1,hyp2 1 1 0<br />1 * hyp2,hyp3 0 1 1<br />1 * hyp1,hyp3 1 0 1<br /><br />Let's relabel it as:<br /><br /> evA evB evC<br />1 * hyp12 1 1 0<br />1 * hyp23 0 1 1<br />1 * hyp13 1 0 1<br /><br />Then the probabilities of the original hypotheses can then be postprocessed as:<br /><br />P(hyp1) = P(hyp12)+P(hyp13)<br />P(hyp2) = P(hyp12)+P(hyp23)<br />P(hyp3) = P(hyp23)+P(hyp13)<br /><br />And this would give the same result as the independent computations for every hypothesis. <br /><br />So this approach works well for when the combined set of symptoms for multiple hypotheses had been seen in training, and not much good for the combinations that haven't been seen in the training. The combined use of the independent and mutually-exclusive combinations with a low acceptance threshold for the independent computations tempers this effect only slightly.<br />Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-63579584459293924342016-07-02T03:14:00.003-04:002016-08-06T12:13:39.586-04:00AdaBoost 6: multiple confidence ranges & other thoughtsOne of the chapters I quickly read through was on using a better estimation of probability of the success of the partial hypotheses (in the AdaBoost sense), with the following example:<br /><br />Suppose, the underlying algorithm found a decision stump, and finds that it can classify the cases having a value above some limit pretty well (say, 90% of them are "+1") while the cases with the values under this limit are undifferentiated (say, 49% of them are "+1"). In this case if we allow the underlying algorithm to return the values not only +1 and -1 but also 0 for when it can't differentiate well, the boosting goes much better. <br /><br />Since the part 3 I've been wondering if the algorithm can be improved in an obvious way by using the separate confidence values for the situations when the underlying algorithm returns +1 or -1 (or in equivalent Bayesian terms, 1 or 0), and maybe even do better than that by forming multiple "bands" of training cases by some rule and assigning a separate confidence values to each band, and maybe even further by computing an individual confidence value for each case. The idea above shows that it can, and the book then goes on to the exact same idea of multiple bands, each with its own confidence value, and on to the partial hypotheses returning the individual confidence values for each training case. So that idea really was obvious, and there is not much point in writing more about it. The only tricky part is the minimalization criteria, but that becomes straightforward from the description in the part 4.<br /><br />One more idea would be to make the decision stumps not discrete ("1" on this side of this value, "0" on the other side) but graduated, with the confidence being 0.5 at the value and growing towards 1 on one side and towards 0 on the other side. Possibly saturating at some distance from the center point value. <br /><br />An easy example of saturation would be this: If we're classifying a set of points in a 2-dimensional space with the decision stumps based on the coordinate values, this means that on each step we find some value of a coordinate (X or Y) and say that the points on one side have mostly the value "1" and on the other side they have mostly the value "0". Then the classic AdaBoost approach (without the square root in the confidence) takes the confidence C as the fraction of the points whose values got guessed right. Or, translating from the pair (value, confidence) to the just the confidence, the pair (1, C) becomes C, and the pair (0, C) becomes (1-C). But the breaking point is usually not one point, it's a range between the coordinate values closest to this break. The classic approach is to pick the centerpoint of this range as the breaking point. But we could say that the confidence changes linearly between these two coordinate values and beyond this range saturates to C and (1-C). This smooth transition could help with the training on the data with errors in it. The book contains an example of training on the data that contained 20% of errors which led to overfitting over many rounds of boosting. The smooth transitions could provide the averaging that would counteract this overfitting. Maybe.<br /><br />Another thought that occurred to me is what if we're boosting while we're boosting? Or in other words, what if the partial algorithm runs a few rounds of boosting on its own? How can it be equivalently represented as a single round of boosting?<br /><br />This looks like a good example where an individual value of the confidence for each training case would be handy. As we go through the execution stage, the weight of each example would be multiplied on each round of "nested boosting" in the same way as if would be on each round of normal boosting. I.e. if a round of boosting had the confidence C, and guessed this particular training case right, the weight of this training case will be multiplied by C, and if it guessed this training case wrong, the weight would be multiplied by (1-C).<br /><br />So if we define the effective confidence Ce as:<br /><br />(if the round guessed right, its Ce=C, otherwise its Ce=1-C)<br /><br />the we can see that the multiplier will be:<br /><br />product for rounds t(Ce<sub>t</sub>)<br /><br />Except that it's not properly scaled to be in the range [0..1]. To adjust the scaling, it has to become<br /><br />product for rounds t(Ce<sub>t</sub>) / (product for rounds t(Ce<sub>t</sub>) + product for rounds t(1-Ce<sub>t</sub>))<br /><br />There won't be a single confidence value for all the training cases of such a composite round, each training case will have its own confidence value, with the probability of pointing towards 0 or 1 built into it.<br /><br />One more interesting thing I've read is that after some number of rounds AdaBoost tends to start going in a circle through the same set of distributions (or sometimes not quite the same but very close). To me this looks like an indication that it's time to stop. Boosting the margins by going through this loop repeatedly looks like cheating, because if we look at its Bayesian meaning, this meaning implies that the events that get examined must be essentially independent of each other. But if we repeatedly examine the same events, they're not independent, they're extra copies of the events that have already been seen. Thus re-applying them repeatedly doesn't seem right. On the other hand, the events in such a loop do form this loop because they represent each event in a balanced way. So maybe the right thing to do is to throw away everything before this loop too, and just leave one iteration of this loop as the only events worth examining. This would be interesting to test if/when I get around to do it.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-21414210430745252452016-06-28T02:30:00.000-04:002016-06-28T12:33:11.851-04:00AdaBoost 5: the square root and the confidence valueThe question of what should be used as the confidence value for Bayesian computations is a thorny one. In my <a href="https://www.blogger.com/blogger.g?blogID=3195352510924611504#editor/target=post;postID=1744569943656630096;onPublishedMenu=allposts;onClosedMenu=allposts;postNum=3;src=postname">previous take on it</a> I've used the value without a square root, while the classic AdaBoost formula uses the one with the square root. Which one is more right?<br /><br />Both of them produce the same decision for the classification of the cases. The difference is in the absolute value of the chances, or in the AdaBoost terms, the margin (with the classic AdaBoost also applying the logarithm on top).<br /><br />I still think that using the value without the square root has the more correct "physical meaning". But either can be used with the same result.<br /><br />Following the classic AdaBoost, the version of the confidence with the square root follows from the expression<br /><br />Z = sum for all good cases i (Wi / S) + sum for all bad cases j (Wj * S)<br />Z = sum for all good cases i (Wi * (C-1) / C) + sum for all bad cases j (Wj * C / (C-1))<br /><br />Just like the last post, W is the weight of a particular training case, same as D(i) in the classic AdaBoost terms, just since I've been using weights rather than a distribution, the letter W comes easier to my mind.<br /><br />The "physical meaning" here is that the weights of the training cases for the next round of boosting are adjusted opposite to how the chances of these training cases get affected by this round during the final computation. If a training case gets predicted correctly by a partial hypothesis, its weight will get multiplied by C and the weight of the other cases will be multiplied by (1-C), so the changes will get multiplied by C/(1-C), and for the next round of boosting the good cases get divided by that to compensate. For the cases that get predicted incorrectly, the opposite happens.<br /><br />This translates to<br /><br />C / (1-C) = S = sqrt((1-e) / e) = sqrt(1-e) / sqrt(e)<br /><br />The last expression does NOT mean<br /><br />C = sqrt(1-e)<br /><br />Instead, it means<br /><br />C = sqrt(1-e) / (sqrt(1-e) + sqrt(e))<br /><br />Alternatively, the approach without the square root starts with the premise<br /><br />Z = sum for all good cases i (Wi / C) + sum for all bad cases j (Wj / (1-C))<br /><br />The "physical meaning" is as <a href="http://babkin-cep.blogspot.com/2016/05/adaboost-3-and-bayesian-logic.html">described before</a>, the weights of the training cases for the next round of boosting are adjusted opposite to how the weights of these training cases get affected by this round during the final computation. It seems to me that compensating the weights for the changes in weights is the more correct "physical meaning" than compensating the weights for the changes in chances.<br /><br />This translates to<br /><br />C / (1-C) = S<sup>2</sup> = sqrt( (1-e) / e )<sup>2</sup> = (1-e) / e<br /><br />and <br /><br />C = (1-e)<br /><br />By the way, chasing this version through the derivatives as shown in the previous post was interesting. I've appreciated why the authors of AdaBoost involved the exponents into the formulas: doing the derivatives and integrals with the exponents is much easier than without them. Then I've realized that with the derivatives where S = exp(Alpha) I'm computing dZ/dAlpha, not dZ/dC. And that is the correct approach. So without the exponents I should be computing the dZ/dS, and that gets easy again.<br /><br />So, the <i>real</i> version of AdaBoost described in the <a href="http://babkin-cep.blogspot.com/2016/05/adaboost-3-and-bayesian-logic.html">third part</a> is this:<br /><br /><hr /><br />Given: (x<sub>1</sub>, y<sub>1</sub>), ..., (x<sub>m</sub>, y<sub>m</sub>) where x<sub>i</sub> belongs to X, y<sub>i</sub> belongs to {-1, +1}.<br />Initialize: D<sub>1</sub>(i) = 1 for i = 1, ..., m.<br />For t = 1, ..., T:<br /><ul><li>Train the basic algorithm using the weights D<sub>t</sub>.</li><li>Get weak hypothesis h<sub>t</sub>: X -> {-1, +1}.</li><li>Aim: select h<sub>t</sub> to minimalize the boundary on the training error Z<sub>t</sub> where:<br />Wgood<sub>t</sub> = 0; Wbad<sub>t</sub> = 0;<br />for i = 1, ..., m {<br /> if h<sub>t</sub>(x<sub>i</sub>) = y<sub>i</sub> {<br /> Wgood<sub>t</sub> += D<sub>t</sub>(i)<br /> } else {<br /> Wbad<sub>t</sub> += D<sub>t</sub>(i)<br /> }<br />}<br />C<sub>t</sub> = Wgood<sub>t</sub> / (Wgood<sub>t</sub> + Wbad<sub>t</sub>)<br />Z<sub>t</sub> = Wgood<sub>t</sub>/C<sub>t</sub> + Wbad<sub>t</sub>/(1-C<sub>t</sub>)<br />which can also be represented symmetrically through S<sub>t</sub>:<br />Z<sub>t</sub> = Wgood<sub>t</sub>/S<sub>t</sub> + Wbad<sub>t</sub>*S<sub>t</sub><br />S<sub>t</sub> = sqrt(C<sub>t</sub> / (1-C<sub>t</sub>)) = sqrt(Wgood<sub>t</sub> / Wbad<sub>t</sub>)<br />and substituting S<sub>t</sub>:<br />Z<sub>t</sub> = Wgood<sub>t</sub> / sqrt(Wgood<sub>t</sub> / Wbad<sub>t</sub>) + Wbad<sub>t</sub> * sqrt(Wgood<sub>t</sub> / Wbad<sub>t</sub>)<br />= 2 * sqrt(Wgood<sub>t</sub> * Wbad<sub>t</sub>)<br />Which gets minimalized when either of Wgood<sub>t</sub> or Wbad<sub>t</sub> gets minimalized, but to be definitive we prefer to minimalize Wbad<sub>t</sub>.<br /></li><li>Update, <br />for i = 1, ..., m {<br /> if h<sub>t</sub>(x<sub>i</sub>) != y<sub>i</sub>; {<br /> D<sub>t+1</sub>(i) = D<sub>t</sub>(i) / (1-C<sub>t</sub>)<br /> } else {<br /> D<sub>t+1</sub>(i) = D<sub>t</sub>(i) / C<sub>t</sub><br /> }<br />}</li></ul>Produce the function for computing the value of the final hypothesis:<br />H(x) {<br /> chance = 1;<br /> for t=1,...,T {<br /> if (h<sub>t</sub>(x) > 0) {<br /> chance *= C<sub>t</sub>/(1-C<sub>t</sub>);<br /> } else <br /> chance *= (1-C<sub>t</sub>)/C<sub>t</sub>;<br /> }<br /> }<br /> return sign(chance - 1)<br />}<br /><br /><hr /><br />Having this sorted out, I can move on to more creative versions of the algorithm where on a step of boosting the different training cases may get stamped with the different confidence values C.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-27777845184745775712016-06-24T11:04:00.000-04:002016-06-28T12:32:40.204-04:00AdaBoost 4 or the square root returnsI've had to return the library book on boosting. I've ordered my own copy, but on the last day I've paged through the more interesting chapters. This gave me some ideas on how the Bayesian model can be fit to those too, and I started writing it up, but I've got stopped at the question: what should get minimized? So I went back to the book (my own copy now) and I think that I finally understand it. Here is how it works:<br /><br />The real measure that gets minimized on each round of boosting is Z. In the book the authors prove that it's not the training error as such but the upper bound on the training error. They write it (with dropped subscript <i>t</i> of the boosting round) as<br /><br />Z = sum for all good cases i (W<sub>i</sub> * exp(-Alpha)) + sum for all bad cases j (W<sub>j</sub> * exp(Alpha))<br /><br />Where W is the weight of a particular training case (same as D(i) in the classic AdaBoost terms, just since I've been using weights rather than a distribution, the letter W comes easier to my mind). They have a use for this exponent in the proofs but here it doesn't matter. Let's create a notation with a new variable S (the name doesn't mean anything, it's just a letter that hasn't been used yet) that will sweep the exponent under the carpet:<br /><br />S = exp(Alpha)<br />Z = sum for all good cases i (W<sub>i</sub> / S) + sum for all bad cases j (W<sub>j</sub> * S)<br /><br />S has a "physical meaning": it represents how the chances of a particular case change. The weights of the "good" cases (i.e. those that got predicted correctly on a particular round) get divided by S because we're trying to undo the effects of the changes from this round of boosting for the next round. Getting back to the Bayesian computation, <br /><br />S = C / (1-C)<br /><br />Where C is the confidence of the round's prediction acting on this case. When the produced boosting algorithm runs, if the round's prediction votes for this case (which we know got predicted by it correctly in training), its weight will be multiplied by C. If the prediction votes against this case, its weight will be multiplied by (1-C). Thus S measures how strongly this prediction can differentiate this case. For the cases that get mispredicted by this round, the relation is opposite, so their weights get multiplied by S instead of division.<br /><br />The mathematical way to minimize Z is by finding the point(s) where its derivative is 0.<br /><br />dZ/dS = sum for all good cases i (W<sub>i</sub> / -S<sup>2</sup>) + sum for all bad cases j (W<sub>j</sub>) = 0<br />sum for all good cases i (W<sub>i</sub> / S<sup>2</sup>) = sum for all bad cases j (W<sub>j</sub>)<br /><br />And since for now we assume that S is the same for all the cases,<br /><br />sum for all good cases i (W<sub>i</sub>) = Wgood<br />sum for all bad cases j (W<sub>j</sub>) = Wbad<br /><br />then <br /><br />Wgood / S<sup>2</sup> = Wbad<br />Wgood / Wbad = S<sup>2</sup><br />S = sqrt(Wgood / Wbad)<br /><br />And since we know that the training error e is proportional to Wbad and (1-e) is proportional to Wgood:<br /><br />e = Wbad / (Wgood + Wbad)<br />1-e = Wgood / (Wgood + Wbad)<br /><br />then we can rewrite S as<br /><br />S = sqrt( (1-e) / e )<br /><br /><strike>Or substituting the expression of S through C,<br /><br />C / (1-C) = sqrt( (1-e) / e )<br />C = sqrt(1-e)<br /><br />Which means that the Bayesian confidence in AdaBoost really is measured as a square root of the "non-error". This square root can be put away in the basic case but it becomes important for the more complex variations. </strike> [I think now that this part is not right, will update later after more thinking]<br /><br />Returning to the minimization, this found value of S gets substituted into the original formula of Z, and this is what we aim to minimize on each round. For the classic AdaBoost it is:<br /><br />Z = Wgood / sqrt((1-e) / e) + Wbad * sqrt((1-e) / e)<br /><br />Since Wgood is proportional to (1-e) and Wbad is proportional to e (and in case if the sum of all the weights is 1, Wgood=1-e, and Wbad=e), we can substitute them:<br /><br />Z = (1-e) / sqrt((1-e) / e) + e * sqrt((1-e) / e)<br />= sqrt(e * (1-e)) + sqrt(e * (1-e))<br />= 2 * sqrt(e * (1-e))<br /><br />This value gets minimized when e gets closer to either 0 or 1 (because the formula is symmetric and automatically compensates for the bad predictions by "reverting" them). The classic AdaBoost algorithm then says "we'll rely on the ability of the underlying algorithm to do the same kind of reverting" and just picks the minimization of e towards 0. This makes the computation more efficient for a particular way of computation for C but the real formula is above. If we want to handle the more generic ways, we've got to start with this full formula and only then maybe simplify it for a particular case.<br /><br />This formula can be also written in a more generic way, where each training case may have its own different value of confidence C and thus of S:<br /><br />Z = sum for all good cases i(W<sub>i</sub> / S<sub>i</sub>) + sum for all bad cases j(W<sub>j</sub> * S<sub>j</sub>)<br /><br />And now I'll be able to talk about these more generic cases.<br /><br />By the way, the difference of how the variation of AdaBoost for logistic regression works from how the classic AdaBoost works is in a different choice of measure for what gets minimized, a different formula for Z.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0tag:blogger.com,1999:blog-3195352510924611504.post-17445699436566300962016-05-29T23:03:00.000-04:002016-05-31T02:22:09.302-04:00AdaBoost 3 and Bayesian logicI think I've figured out a way to express the workings of AdaBoost in terms of the Bayesian processing, and I think it becomes simpler this way. Now, I'm not the first one to do that, the book "Boosting" describes a way to do this with what it calls the logistical regression. It also derives an approach to probabilities in AdaBoost from this way. But the logistical regression uses a sigmoid curve (I think like the one generally used for the normal distribution) and differs from the plain AdaBoost. I think my explanation works for the plain AdaBoost.<br /><br />Before I start on that explanation, I want to talk about why the boosting works. Yeah, I've followed the proofs (more or less) but it took me a while to get a bit of an intuitive feeling in my head, the "physical meaning" of the formulas. I want to share this understanding which is much simpler than these proofs (and I hope that it's correct): <br /><br />The premise of boosting is that we're able to find a number of methods (what they call "hypotheses" in AdaBoost) to predict the correct outcomes of the training cases, each method correctly predicting more than 50% of the training cases. Then if we collect a large-ish number of these methods, we can predict the correct outcomes of all the training cases simply by averaging the predictions of these methods. And the other cases will follow the training cases (unless an overfitting happens). Since more than 50% of the cases are correctly predicted by each method, after the averaging more than 50% of the votes for each training case will be correct, and thus the result will be correct too. Of course this depends on the correct predictions being distributed pretty evenly among the cases. If we have a thousand methods that predict correctly the same cases and incorrectly the other cases, obviously after averaging these other cases will still be predicted incorrectly. So the selection of methods must somehow shuffle the preference for the cases, so that the next picked method will predict well the cases that have been predicted poorly by the previous picked methods. That's it, that's the whole basic idea. It's a bit of an oversimplification but it's easy to understand.<br /><br />Now on to the explanation of the connection between AdaBoost and the Bayesian logic.<br /><br />To start with, I want to show one more rewrite of the AdaBoost algorithm. It builds further on the version I've shown in the <a href="http://babkin-cep.blogspot.com/2016/05/adaboost-in-simpler-formulas-2.html">last installment</a>. Now I'm returning back to the notation of e<sub>t</sub> and (1-e<sub>t</sub>). These values are proportional to Wbad<sub>t</sub> and Wgood<sub>t</sub> but are confined to the range [0, 1] which fits well into the Bayesian computations. More importantly, this new version gets rid of the square root in the computation of D<sub>t+1</sub>(i). It wasn't the first thing I realized for the connection between the AdaBoost and Bayesian logic, it was actually the last thing I realized, but after that all the parts of the puzzle fell into place. So I want to show this key piece of the puzzle first.<br /><br />The point of this computation is to readjust the weights of the training cases for the next round, so that the total weight of all the cases successfully predicted by the current round equals to the total weight of the unsuccessfully predicted rounds. There is more than one way to make the weights satisfy this condition, they're all proportional to each other and all just as good. They way the weight are modified in the classic for of AdaBoost algorithm has been selected to make the modification symmetric, and allow it to write the adjustment for both correct and incorrect cases as a single formula:<br /><br />D<sub>t+1</sub>(i) = D<sub>t</sub>(i)*exp(-Alpha<sub>t</sub>*y<sub>i</sub>*h<sub>t</sub>(x<sub>i</sub>)) / Z<sub>t</sub><br /><br />But if we're writing an honest if/else statement, it doesn't have to be symmetric. We can as well write either of:<br /><br /> if h<sub>t</sub>(x<sub>i</sub>) != y<sub>i</sub>; {<br /> D<sub>t+1</sub>(i) = D<sub>t</sub>(i) / e<sub>t</sub><br /> } else {<br /> D<sub>t+1</sub>(i) = D<sub>t</sub>(i) / (1-e<sub>t</sub>)<br /> }<br /><br />or with a degenerate "else" part:<br /><br /> if h<sub>t</sub>(x<sub>i</sub>) != y<sub>i</sub>; {<br /> D<sub>t+1</sub>(i) = D<sub>t</sub>(i) * (1-e<sub>t</sub>) / e<sub>t</sub><br /> } else {<br /> D<sub>t+1</sub>(i) = D<sub>t</sub>(i)<br /> }<br /><br />Either way the result is the same. And there is no need to involve the square roots, the formula becomes simpler. After making this simplification, here is my latest and simplest version of the algorithm:<br /><br /><hr /><br />Given: (x<sub>1</sub>, y<sub>1</sub>), ..., (x<sub>m</sub>, y<sub>m</sub>) where x<sub>i</sub> belongs to X, y<sub>i</sub> belongs to {-1, +1}.<br />Initialize: D<sub>1</sub>(i) = 1 for i = 1, ..., m.<br />For t = 1, ..., T:<br /><ul><li>Train the basic algorithm using the weights D<sub>t</sub>.</li><li>Get weak hypothesis h<sub>t</sub>: X -> {-1, +1}.</li><li>Aim: select h<sub>t</sub> to minimalize the weighted error e<sub>t</sub>: <br />Wgood<sub>t</sub> = 0; Wbad<sub>t</sub> = 0;<br />for i = 1, ..., m {<br /> if h<sub>t</sub>(x<sub>i</sub>) = y<sub>i</sub> {<br /> Wgood<sub>t</sub> += D<sub>t</sub>(i)<br /> } else {<br /> Wbad<sub>t</sub> += D<sub>t</sub>(i)<br /> }<br />}<br />e<sub>t</sub> = Wbad<sub>t</sub> / (Wgood<sub>t</sub> + Wbad<sub>t</sub>)<br /></li><li>Update, <br />for i = 1, ..., m {<br /> if h<sub>t</sub>(x<sub>i</sub>) != y<sub>i</sub>; {<br /> D<sub>t+1</sub>(i) = D<sub>t</sub>(i) * (1-e<sub>t</sub>) / e<sub>t</sub><br /> } else {<br /> D<sub>t+1</sub>(i) = D<sub>t</sub>(i)<br /> }<br />}</li></ul>Produce the function for computing the value of the final hypothesis:<br />H(x) {<br /> chance = 1;<br /> for t=1,...,T {<br /> if (h<sub>t</sub>(x) > 0) {<br /> chance *= (1-e<sub>t</sub>)/e<sub>t</sub>;<br /> } else <br /> chance *= e<sub>t</sub>/(1-e<sub>t</sub>);<br /> }<br /> }<br /> return sign(chance - 1)<br />}<br /><br /><hr /><br />Now this form can be translated to the Bayesian approach. I'll be using the form of Bayesian computations that works with <a href="http://babkin-cep.blogspot.com/2016/05/summary-of-bayes-by-weight.html">weights rather than probabilities</a>. The translation to his form is fairly straightforward:<br /><br />There are two Bayesian mutually-exclusive hypotheses (but to avoid confusion with the AdaBoost term "hypothesis", let's call them "outcomes" here): one that the result is true (1), another one that the result false (0).<br /><br />Each "hypothesis" h<sub>t</sub>(x) in AdaBoost terms becomes an "event" Ev<sub>t</sub> in the Bayesian form (to avoid the terminological confusion I'll call it an event henceforth). Each event is positive: it being true predicts that the true outcome is correct, and vice versa. The training table looks like this:<br /><br /><pre>Weight Outcome Ev<sub>1</sub> ... Ev<sub>T</sub><br />1 * true 1 ... 1<br />1 * false 0 ... 0<br /></pre><br />Aside from this, we remember e<sub>t</sub> from all the boosting rounds, and of course the computations for all the "events" chosen by the boosting.<br /><br />Then when we run the model, we get the set of arguments x as the input, and start computing the values of the events. When we compute the event Ev<sub>t</sub>, we apply it with the confidence C(Ev<sub>t</sub>) value of (1-e<sub>t</sub>), as described in the <a href="http://babkin-cep.blogspot.com/2015/10/bayes-12-fuzzy-training.html">fuzzy training logic</a>. In result if the Ev<sub>t</sub> is true, the weight of the true outcome gets multiplied by (1-e<sub>t</sub>) and the weight of the false outcome gets multiplied by e<sub>t</sub>. If the event is false, the multiplication goes the opposite way.<br /><br />In the end we compute the probability of the true outcome from the final weights: W(true)/(W(true)+ W(false)). If it's over 0.5, the true outcome wins. This logic is exactly the same as in the function H(x) of the AdaBoost algorithm.<br /><br />Why is the (1-e<sub>t</sub>) used as the confidence value? Since it's the fraction of the training cases that got predicted correctly by Ev<sub>t</sub>, it makes sense that we're only so much confident in this event. Well, for the first event it is, but for the following events (1-e<sub>t</sub>) is computed from a modified distribution of the events, with the adjusted weights. Does it still make sense?<br /><br />As it turns out, it does. To find out why, we need to look at the weights of the training cases after they pass the filter of the first event. Instead of looking at the training table with two composite rows we need to step back and look at the table with the M original uncombined training cases:<br /><br /><pre>CaseId Weight Outcome Ev<sub>1</sub> ... Ev<sub>T</sub><br />1 1 * true 1 ... 1<br />...<br />M 1 * false 0 ... 0<br /></pre><br />The cases that have the outcome of true still have 1 for all the events, and the one with the false outcome have 0 for all the events. In case if we run the algorithm for an input that matches a particular training case, Ev<sub>1</sub> will predict the outcome correctly for some of the training cases and incorrectly for the others. When it predicts correctly, the weight of this training case will be multiplied by the confidence C(Ev<sub>1</sub>) and will become (1-e<sub>1</sub>). But when it predicts incorrectly, the weight will be multiplied by e<sub>1</sub>. The distribution will become skewed. We want to unskew it for computing the confidence of Ev<sub>2</sub>, so we compensate by multiplying the weight of the cases that got predicted incorrectly by (1-e<sub>1</sub>)/e<sub>1</sub>. Lo and behold, it's the same thing that is done in AdaBoost:<br /><br /> if h<sub>t</sub>(x<sub>i</sub>) != y<sub>i</sub>; {<br /> D<sub>t+1</sub>(i) = D<sub>t</sub>(i) * (1-e<sub>t</sub>) / e<sub>t</sub><br /> } else {<br /> D<sub>t+1</sub>(i) = D<sub>t</sub>(i)<br /> }<br /><br />So it turns out that on each step of AdaBoost the distribution represents the compensation of the weight adjustment for application of the previous events, and the value (1-e<sub>t</sub>) is the proper adjusted fraction of the training cases that got predicted correctly. Amazing, huh? I couldn't have come up with this idea from scratch, but given an existing formula, it all fits together.<br /><br />What is the benefit of looking at AdaBoost from the Bayesian standpoint? For one, we get the probability value for the result. For another one, it looks like AdaBoost can be slightly improved by changing the initial weights of true and false outcomes from 1:1 to their actual weights in the M training cases. That's an interesting prediction to test. And for the third one, maybe it can be used to improve the use of AdaBoost for the multi-class classifications. I haven't read the book that far yet, I want to understand the current state of affairs before trying to improve on it.Sergey Babkinhttp://www.blogger.com/profile/06686909700446011233noreply@blogger.com0