Reducing Sigmoid computations by (at least) 88.0797077977882%

A classic implementation issue in machine learning is reducing the cost of computing the sigmoid function $\sigma(a) = \frac{1}{1+\exp(-a)}$.

Specifically, it is common to profile your code and discover that 90% of the time is spent computing the $\exp$ in that function.  This comes up often in neural networks, as well as in various probabilistic architectures, such as sampling from Ising models or Boltzmann machines.  There are quite a few classic approximations to the function, using simple polynomials, etc. that can be used in neural networks.

Today, however, I was faced with a sampling problem involving the repeated use of the sigmoid function, and I noticed a simple trick that could reduce the number of sigmoids by about 88% without introducing any approximation.  The particular details of the situation aren’t interesting, but I repeatedly needed to do something like the following:

1. Input $a \in \Re$
2. Compute a random number $r \in [0,1]$
3. If $r < \sigma(a)$
4.   Output $+1$
5. Else
6.     Output $-1$

Now, let’s assume for simplicity that $a$ is positive.  (Otherwise, sample using $-a$ and then switch the sign of the output.)  There are two observations to make:

1. If $a$ is large, then you are likely to output $+1$
2. Otherwise, there are easy upper and lower bounds on the probability of outputting $+1$

This leads to the following algorithm:

1. Input $a \in \Re$
2. Compute a random number $r \in [0,1]$
3. If $a \geq 2$
4.     If $r \leq 0.880797077977882$ or $r \leq \sigma(a)$
5.         Output $+1$
6.     Else
7.         Output $-1$
8. Else
9.     If $r > .5 + a/4$
10.         Output $-1$
11.     Else if $r \leq .5 + a/5.252141128658$ or $r \leq \sigma(a)$
12.         Output $+1$
13.     Else
14.         Output $-1$

The idea is as follows:

1. If $a\geq 2$, then we can lower-bound the probability of outputting +1 by a pre-computed value of $\sigma(2)\approx0.8807...$, and short-circuit the computation in many cases.
2. If $a\leq 2$, then we can upper bound the sigmoid function by $.5+a/4$.
3. If $a\leq 2$, then we can also lower bound by $.5+a/5.252141...$ respectively.  (This constant was found numerically).

The three cases are illustrated in the following figure, where the input $a$ is on the x-axis, and the random number $r$ is on the y-axis. Since, for all $a$ at least a fraction $\sigma(2)\approx.8807$ of the numbers will be short-circuited, sigmoid calls will be reduced appropriately.  If $a$ is often near zero, you will do even better.

Obviously, you can take this farther by adding more cases, which may or may not be helpful, depending on the architecture, and the cost of branching vs. the cost of computing an $\exp$.

Julia, Matlab, and C

Julia is a new language in the same arena as Matlab or R. I’ve had failed attempts to quit the Matlab addiction in the past, making me generally quite conservative about new platforms. However, I’ve recently been particularly annoyed by Matlab’s slow speed, evil license manager errors, restrictions on parallel processes, C++ .mex file pain, etc., and so I decided to check it out. It seems inevitable that Matlab will eventually displaced by something. The question is: is that something Julia?

“We want a language that’s open source, with a liberal license. We want the speed of C with the dynamism of Ruby. We want a language that’s homoiconic, with true macros like Lisp, but with obvious, familiar mathematical notation like Matlab. We want something as usable for general programming as Python, as easy for statistics as R, as natural for string processing as Perl, as powerful for linear algebra as Matlab, as good at gluing programs together as the shell. Something that is dirt simple to learn, yet keeps the most serious hackers happy. We want it interactive and we want it compiled.”

Essentially, the goal seems to be a faster, freer Matlab that treats users like adults (macros!) and doesn’t require writing any .mex files in C++ or Fortan. Sounds too good to be true? I decided to try it out. My comparisons are to Matlab (R2012a) and C (gcc 4.2 with -O2 and -fno-builtin to prevent compile-time computations) all on a recent MacBook Pro (2.3 GHz Intel Core i7 w/ 8GB RAM).

Installation was trivial: I just grabbed a pre-compiled binary and started it up.

I deliberately used naive algorithms, since I am just testing raw speed. It should be a fair comparison, as long as the algorithm is constant. Please let me know about any bugs, though.

First benchmark: Fibonnaci

% Matlab
function f=fib(n)
if n <= 2
f=1.0;
else
f=fib(n-1)+fib(n-2);
end
end

// C
double fib(int n){
if(n<=2)
return(1.0);
else
return(fib(n-2)+fib(n-1));
}

% julia
function fib(n)
if n <= 2
1.0
else
fib(n-1)+fib(n-2);
end
end

Clarity is basically a tie. Running them for n=30 we get:

time in matlab (fib): 14.344231
time in c (fib):       0.005887
time in julia (fib):   0.237832

Second benchmark: Matrix Multiplication

This is a test of the naive O(N^3) matrix multiplication algorithm.

% matlab
function C=mmult(A,B,C)
[M,N] = size(A);
for i=1:M
for j=1:M
for k=1:M
C(i,j) = C(i,j) + A(i,k)*B(k,j);
end
end
end
end

// C
#define M 500
void mmult(double A[M][M],double B[M][M], double C[M][M]){
//double C[M][M];
int i,j,k;
for(i=0; i<M; i++)
for(j=0; j<M; j++){
C[i][j] = 0;
for(k=0; k<M; k++)
C[i][j] += A[i][k]*B[k][j];
}
}

# julia
function mmult(A,B)
(M,N) = size(A);
C = zeros(M,M);
for i=1:M
for j=1:M
for k=1:M
C[i,j] += A[i,k]*B[k,j];
end
end
end
C;
end

Here, I think that Matlab and Julia and a bit clearer, and Julia wins though the wonders of having “+=”. The timing results on 500×500 matrices are:

time in matlab (matmult): 1.229571
time in c (matmult):      0.157658
time in julia (matmult):  0.5029549

Third Benchmark: numerical quadrature

Here, we attempt to calculate the integral $\int_{x=5}^{15} sin(x) dx$ by numerical quadrature, using a simple midpoint rule with computations at $10^7$ points.

% matlab
val = 0.0;
for x=lb:(ub-lb)/npoints:ub
val = val + sin(x)/npoints;
end
end

// C
double numquad(double lb,double ub,int npoints){
double val = 0.0;
int i;
for(i=0; i<=npoints; i++){
double x = lb + (ub-lb)*i/npoints;
val += sin(x)/npoints;
}
return(val);
}

# julia
val = 0.0
for x=lb:(ub-lb)/npoints:ub
val += sin(x)/npoints
end
val
end

The timings are:

time in matlab (numquad): 0.446151
time in c (numquad):      0.167112
time in julia (numquad):  0.256597

Fourth Benchmark: Belief Propagation

Finally, I decided to try a little algorithm similar to what I actually tend to implement for my research. Roughly speaking, Belief Propagation is a repeated sequence of matrix multiplications, followed by normalization.

% matlab
function x=beliefprop(A,x,N)
for i=1:N
x = A*x;
x = x/sum(x);
end
end

// C
void beliefprop(double A, double x, int N){
int i, n, j;
double x2;
for(n=0; n<N; n++){
for(i=0; i<25; i++){
x2[i]=0;
for(j=0; j<25; j++)
x2[i] += A[i][j]*x[j];
}
for(i=0; i<25; i++)
x[i]=x2[i];
double mysum = 0;
for(i=0; i<25; i++)
mysum += x[i];
for(i=0; i<25; i++)
x[i] /= mysum;
}
return;
}

% julia
function beliefprop(A,x,N)
for i=1:N
x = A*x;
x /= sum(x);
end
x
end

Here, I think we can agree that Matlab and Julia are clearer. (Please don’t make fun of me for hardcoding the 25 dimensions in C.) Using a matrix package for C would probably improve clarity, but perhaps also slow things down. The results are:

time in matlab (beliefprop): 0.627478
time in c (beliefprop):      0.074355
time in julia (beliefprop):  0.376427

Fifth Benchmark: BP in log-space

In practice, Belief Propagation is often implemented in log-space (to help avoid numerical under/over-flow.). To simulate an algorithm like this, I tried changing to propagation to take an exponent before multiplication, and a logarithm before storage.

% matlab
function x=beliefprop2(A,x,N)
for i=1:N
x = log(A*exp(x));
x = x - log(sum(exp(x)));
end
end

// C
void beliefprop2(double A, double x, int N){
int i, n, j;
double x2;
for(n=0; n<N; n++){
for(i=0; i<25; i++){
x2[i]=0;
for(j=0; j<25; j++)
x2[i] += A[i][j]*exp(x[j]);
}
for(i=0; i<25; i++)
x[i]=log(x2[i]);
double mysum = 0;
for(i=0; i<25; i++)
mysum += exp(x[i]);
double mynorm = log(mysum);
for(i=0; i<25; i++)
x[i] -= mynorm;
}
return;
}

# julia
function beliefprop2(A,x,N)
for i=1:N
x = log(A*exp(x));
x -= log(sum(exp(x)));
end
x
end

Life is too short to write C code like that when not necessary. But how about the speed, you ask?

time in matlab (beliefprop2): 0.662761
time in c (beliefprop2):      0.657620
time in julia (beliefprop2):  0.530220

Sixth Benchmark: Markov Chain Monte Carlo

Here, I implement a simple Metropolis algorithm. For no particular reason, I use the two-dimensional distribution: $p(x) \propto \exp(\sin(5 x_1) - x_1^2 - x_2^2)$ % matlab
function mcmc(x,N)
f = @(x) exp(sin(x(1)*5) - x(1)^2 - x(2)^2);
p = f(x);
for n=1:N
x2 = x + .01*randn(size(x));
p2 = f(x2);
if rand < p2/p
x = x2;
p = p2;
end
end
end

// C
double f(double *x){
return exp(sin(x*5) - x*x - x*x);
}
#define pi 3.141592653589793
void mcmc(double *x,int N){
double p = f(x);
int n;
double x2;
for(n=0; n<N; n++){
// run Box_Muller to get 2 normal random variables
double U1 = ((double)rand())/RAND_MAX;
double U2 = ((double)rand())/RAND_MAX;
double R1 = sqrt(-2*log(U1))*cos(2*pi*U2);
double R2 = sqrt(-2*log(U1))*sin(2*pi*U2);
x2 = x + .01*R1;
x2 = x + .01*R2;
double p2 = f(x2);
if(((double)rand())/RAND_MAX< p2/p){
x = x2;
x = x2;
p = p2;
}
}
}

% julia
function mcmc(x,N)
f(x) = exp(sin(x*5) - x^2 - x^2);
p = f(x);
for n=1:N
x2 = x + .01*randn(size(x));
p2 = f(x2);
if rand() < p2/p
x = x2;
p = p2;
end
end
end

Again, I think that C is far less clear than Matlab or Julia. The timings are:

time in matlab (mcmc): 7.747716
time in c (mcmc):      0.150776
time in julia (mcmc):  0.479628

Table

All times are in seconds. (Lower is better.)

Matlab     C     Julia
fib        14.344   0.005   0.237
matmult     1.229   0.157   0.502
numquad     0.446   0.167   0.256
bp          0.627   0.074   0.376
bp2         0.662   0.657   0.530
mcmc        7.747   0.150   0.479

Conclusions

I’m sure all these programs can be sped up. In particular, I’d bet that an expert could optimize the C code to beat Julia on bp2 and mcmc. These are a test of “how fast can Justin Domke make these programs”, not the intrinsic capabilities of the languages. That said, Julia allows for optional type declarations. I did experiment with these but found absolutely no speed improvement. (Which is a good or a bad thing, depending on how you look at life.)

Another surprise to me was how often Matlab’s JIT managed a speed within a reasonable factor of C. (Except when it didn’t…)

The main thing that at Matlab programmer will miss in Julia is undoubtedly plotting. The Julia designers seem to understand the importance of this (“non-negotiable”). If Julia equalled Matlab’s plotting facilities, Matlab would be in real trouble!

Overall, I think that the killer features of freedom, kinda-sorta-C-like speed, and ease of use make Julia more likely as a Matlab-killer than other projects such as R, Sage, Octave, Scipy, etc. (Not to say that those projects have not succeeded in other ways!) Though Julia’s designers also seem to be targeting current R users, my guess is that they will have more success with Matlab folks in the short term, since most Matlab functionality (other than plotting) already exists, while reproducing R’s statistical libraries will be quite difficult. I also think that Julia would be very attractive to current users of languages like Lush. Just to never write another .mex file, I’ll very seriously consider Julia for new projects. Other benefits such as macros, better parallelism support are just bonuses. As Julia continues to develop, it will become yet more attractive.

There was an interesting discussion on Lambda the Ultimate about Julia back when it was announced