Deep Big Simple Neural Nets Excel on Handwritten Digit Recognition

03/01/2010 ∙ by Dan Claudiu Ciresan, et al. ∙ 0

Good old on-line back-propagation for plain multi-layer perceptrons yields a very low 0.35 we need to achieve this best result so far are many hidden layers, many neurons per layer, numerous deformed training images, and graphics cards to greatly speed up learning.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

Code Repositories

CNN-for-handwritten-kanji

A convolutional neural network for handwritten kanji recognition, written using Keras. Uses elastic distortions for dataset augmentation.


view repo
This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

Automatic handwriting recognition is of great academic and commercial interest. Current algorithms are already pretty good at learning to recognize handwritten digits. Post offices use them to sort letters; banks use them to read personal checks. MNIST (LeCun et al., 1998) is the most widely used benchmark for isolated handwritten digit recognition. More than a decade ago, artificial neural networks called Multilayer Perceptrons or MLPs (Werbos, 1974; LeCun, 1985; Rumelhart et al., 1986)

were among the first classifiers tested on MNIST. Most had few layers or few artificial neurons (units) per layer

(LeCun et al., 1998), but apparently back then they were the biggest feasible MLPs, trained when CPU cores were at least 20 times slower than today. A more recent MLP with a single hidden layer of 800 units achieved 0.70% error (Simard et al., 2003)

. However, more complex methods listed on the MNIST web page always seemed to outperform MLPs, and the general trend went towards more and more complex variants of Support Vector Machines or SVMs

(Decoste & Scholkopf, 2002) and combinations of NNs and SVMs (Lauer et al., 2007)

etc. Convolutional neural networks (CNNs) achieved a record-breaking 0.40% error rate

(Simard et al., 2003)

, using novel elastic training image deformations. Recent methods pre-train each hidden CNN layer one by one in an unsupervised fashion (this seems promising especially for small training sets), then use supervised learning to achieve 0.39% error rate

(Ranzato et al., 2006, 2007). The biggest MLP so far (Salakhutdinov et al., 2007) also was pre-trained without supervision then piped its output into another classifier to achieve an error of 1% without domain-specific knowledge.

Are all these complexifications of plain MLPs really necessary? Can’t one simply train really big plain MLPs on MNIST? Why is there no literature on this? One reason is that at first glance deep MLPs do not seem to work better than shallow networks (Bengio et al., 2006). Training them is hard as back-propagated gradients quickly vanish exponentially in the number of layers (Hochreiter, 1991; Hochreiter et al., 2001; Hinton, 2007)

, just like in the first recurrent neural networks

(Hochreiter & Schmidhuber, 1997). Indeed, previous deep networks successfully trained with back-propagation (BP) either had few free parameters due to weight-sharing (e.g. LeCun et al., 1998; Simard et al., 2003) or used unsupervised, layer-wise pre-training (e.g. Bengio et al., 2006; Ranzato et al., 2006)

. But is it really true that deep BP-MLPs do not work at all, or do they just need more training time? How to test this? Unfortunately, on-line BP for hundreds/thousands of epochs on large MLPs may take weeks or months on standard serial computers. But can’t one parallelize it? Well, on computer clusters this is hard due to communication latencies between individual computers. Multi-threading on a multi-core processor is not easy either. We may speed up BP using SSE (Streaming Single Instruction, Multiple Data Extensions), either manually, or by setting appropriate compiler flags. The maximum theoretical speedup under single precision floating-point, however, is four, which is not enough. And MNIST is large - its 60,000 images take almost 50MB, too much to fit in the L2/L3 cache of any current processor. This requires to continually access data in considerably slower RAM. To summarize, currently it is next to impossible to train big MLPs on CPUs.

We will show how to overcome all these problems by training large, deep MLPs on graphics cards.

2 Data

MNIST consists of two datasets, one for training (60,000 images) and one for testing (10,000 images). Many studies divide the training set into two sets consisting of 50,000 images for training and 10,000 for validation. Our network is trained on slightly deformed images, continually generated in on-line fashion; hence we may use the whole un-deformed training set for validation, without wasting training images. Pixel intensities of the original gray scale images range from 0 (background) to 255 (max foreground intensity). pixels per image get mapped to real values in , and are fed into the NN input layer.

3 Architectures

We train 5 MLPs with 2 to 9 hidden layers and varying numbers of hidden units. Mostly but not always the number of hidden units per layer decreases towards the output layer (Table 1

). There are 1.34 to 12.11 million free parameters (or weights, or synapses).

We use standard on-line BP (e.g. Russell & Norvig, 2002, pages 744-748), without momentum, but with a variable learning rate that shrinks by a multiplicative constant after each epoch, from down to . Weights are initialized with a uniform random distribution in

. Each neuron’s activation function is a scaled hyperbolic tangent:

, where and (LeCun et al., 1998).

4 Deforming images to get more training instances

So far, the best results on MNIST were obtained by deforming training images, thus greatly increasing their number. This allows for training networks with many weights, making them insensitive to in-class variability. We combine affine (rotation, scaling and horizontal shearing) and elastic deformations, characterized by the following real-valued parameters:

  • and : for elastic distortions emulating uncontrolled oscillations of hand muscles (for details see Simard et al., 2003);

  • : a random angle from describes either rotation or horizontal shearing. In case of shearing, defines the ratio between horizontal displacement and image height;

  • , : for horizontal and vertical scaling, randomly selected from .

At the beginning of every epoch the entire MNIST training set gets deformed. Initial experiments with small networks suggested the following deformation parameters: , , . Since digits 1 and 7 are similar they get rotated/sheared less () than other digits ().

5 Results

All simulations were performed on a computer with a Core2 Quad 9450 2.66GHz processor, 3GB of RAM, and a GTX280 graphics card. The GPU accelerates the deformation routine by a factor of 10 (only elastic deformations are GPU-optimized); the forward propagation (FP) and BP routines are sped up by a factor of 40. Implementation details can be found in the Appendix. We pick the trained MLP with the lowest validation error, and evaluate it on the MNIST test set. Results are summarized in Table 1.

Most remarkably, the best network has an error rate of only 0.35% (35 out of 10,000 digits). This is significantly better than the best previously published results, namely, 0.39% by Ranzato et al. (2006) and 0.40% by Simard et al. (2003), both obtained by more complex methods. The 35 misclassified digits are shown in Figure 1. Many of them are ambiguous and/or uncharacteristic, with obviously missing parts or strange strokes etc. Interestingly, the second guess of the network is correct for 30 out of the 35 misclassified digits.

The best test error of this MLP is even lower (0.32%) and may be viewed as the maximum capacity of the network. Performance clearly profits from adding hidden layers and more units per layer. For example, network 5 has more but smaller hidden layers than network 4 (Table 1).

Networks with up to 12 million weights can successfully be trained by plain gradient descent to achieve test errors below 1% after 20-30 epochs in less than 2 hours of training. How can networks with so many parameters generalize well on the unseen test set? Answer: the continual deformations of the training set generate a virtually infinite supply of training examples, and the network rarely sees any training image twice.

ID architecture test error for best test simulation weights
(number of neurons in each layer) best validation [%] error [%] time [h] [milions]
1 1000, 500, 10 0.49 0.44 23.4 1.34
2 1500, 1000, 500, 10 0.46 0.40 44.2 3.26
3 2000, 1500, 1000, 500, 10 0.41 0.39 66.7 6.69
4 2500, 2000, 1500, 1000, 500, 10 0.35 0.32 114.5 12.11
5 9 1000, 10 0.44 0.43 107.7 8.86
Table 1: Error rates on MNIST test set.
Figure 1: The 35 miss-classified digits of the best network from Table 1, together with the two most likely predictions (bottom, from left to right) and the correct label according to MNIST (top, right).

Conclusion

In recent decades the amount of raw computing power per Euro has grown by a factor of 100-1000 per decade. Our results show that this ongoing hardware progress may be more important than advances in algorithms and software (although the future will belong to methods combining the best of both worlds). Current graphics cards (GPUs) are already more than 40 times faster than standard microprocessors when it comes to training big and deep neural networks by the ancient algorithm, on-line back-propagation (weight update rate up to , and more than

per trained network). On the very competitive MNIST handwriting benchmark, single precision floating-point GPU-based neural nets surpass all previously reported results, including those obtained by much more complex methods involving specialized architectures, unsupervised pre-training, combinations of machine learning classifiers etc. Training sets of sufficient size are obtained by appropriately deforming images. Of course, the approach is not limited to handwriting, and obviously holds great promise for many visual and other pattern recognition problems.

Acknowledgments

Part of this work got started when Dan Cireşan was a PhD student at University ”Politehnica” of Timişoara. He would like to thank his former PhD advisor, Ştefan Holban, for his guidance, and Răzvan Moşincat for providing a CPU framework for MNIST. This work was supported by Swiss CTI, Commission for Technology and Innovation, Project n. 9688.1 IFF: Intelligent Fill in Form, and by Lifeware S.A. L.M.G. and J.S. wrote a grant proposal for this work, acquired competitive funding for it, and supervised it. C.D.C. wrote GPU-optimised code. C.D.C. and U.M. debugged it. C.D.C. designed and performed the experiments. C.D.C., U.M., L.M.G. and J.S. wrote the paper.

Appendix - GPU implementation

Graphics Processing Unit

Until 2007 the only way to program a GPU was to translate the problem-solving algorithm into a set of graphical operations. Despite being hard to code and difficult to debug, several GPU-based NN implementations were developed when GPUs became faster than CPUs. Two layer MLPs (Steinkraus et al., 2005) and CNNs (Chellapilla et al., 2005) have been previously implemented on GPUs. Although speedups were relatively modest, these studies showed how GPUs can be used for machine learning. More recent GPU-based CNNs trained in batch mode are two orders of magnitude faster than CPU-based CNNs (Scherer & Behnke, 2009).

In 2007, NVIDIA developed the first version of CUDA (Compute Unified Device Architecture), a C-like general programming language. GPU speed and memory bandwidth are vastly superior to those of CPUs, and crucial for fast MLP implementations. To fully understand our algorithm in terms of GPU / CUDA, please visit the NVIDIA website (NVIDIA, 2009). According to CUDA terminology, the CPU is called host and the graphics card device or GPU.

Deformations

It takes 93 CPU seconds to deform the 60,000 MNIST training images, most of them (87) for elastic distortions. Only the most time-consuming part of the latter – convolution with a gaussian kernel (Simard et al., 2003) – is ported to the GPU. The MNIST training set is split into 600 sequentially processed batches. MNIST digits are scaled from the original pixels to pixels, to get a proper center, which simplifies convolution. An image grid has 290

290 cells, zero-padded to 300

300, thus avoiding margin effects when applying a gaussian convolution kernel of size .

__global__ void ConvolveField_optimized(float *randomfield, int width, int height, float *kernel, float *outputfield, float elasticScale){
        float sum=0;
        const int stride_k=GET_STRIDE(GAUSSIAN_FIELD_SIZE,pitch_x>>2);  

//stride

 for gaussian kernel
        __shared__ float K[GAUSSIAN_FIELD_SIZE][stride_k];                              //kernel (21 x 32 values)
        __shared__ float R[GAUSSIAN_FIELD_SIZE+9][GAUSSIAN_FIELD_SIZE]; //random field (30 x 21 values)
        __shared__ float s[10][GAUSSIAN_FIELD_SIZE];                                    //partial sums (10 x 21 values)
        int stride_in=GET_STRIDE(width,pitch_x>>2);                                             //random field stride as a multiple of 32
        int stride_out=GET_STRIDE(width-GAUSSIAN_FIELD_SIZE+1,pitch_x>>2);      //output stride as a multiple of 32
        //loading gaussian kernel into K (21 x 21 values)
        K[ 0+threadIdx.y][threadIdx.x] = kernel[( 0+threadIdx.y)*stride_k + threadIdx.x];//rows 0..9
        K[10+threadIdx.y][threadIdx.x] = kernel[(10+threadIdx.y)*stride_k + threadIdx.x];//rows 10..19
        if(threadIdx.y==0)
                K[20+threadIdx.y][threadIdx.x] = kernel[(20+threadIdx.y)*stride_k + threadIdx.x];//row 20
        //loading randomfield into R
        //0..9 x 21 values
        R[ 0+threadIdx.y][threadIdx.x] = randomfield[(10*blockIdx.y+ 0+threadIdx.y)*stride_in + blockIdx.x + threadIdx.x];
        //10..19 x 21 values
        R[10+threadIdx.y][threadIdx.x] = randomfield[(10*blockIdx.y+10+threadIdx.y)*stride_in + blockIdx.x + threadIdx.x];
        //20..29 x 21 values
        R[20+threadIdx.y][threadIdx.x] = randomfield[(10*blockIdx.y+20+threadIdx.y)*stride_in + blockIdx.x + threadIdx.x];
        __syncthreads();        //wait until everything is read into shared memory
        //computing partial sums
        #pragma unroll 21       //GAUSSIAN_FIELD_SIZE
        for(int i=0;i<GAUSSIAN_FIELD_SIZE;i++)
                sum += R[threadIdx.y + i][threadIdx.x] * K[i][threadIdx.x];
        s[threadIdx.y][threadIdx.x]=sum;
        __syncthreads();
        if(threadIdx.x==0){     //the first column of threads compute the final values of the convolutions
                #pragma unroll 20//GAUSSIAN_FIELD_SIZE-1
                for(int i=1;i<GAUSSIAN_FIELD_SIZE;i++) sum+=s[threadIdx.y][i];
                outputfield[(blockIdx.y*10+threadIdx.y)*stride_out + blockIdx.x] = sum * elasticScale;
        }
}\end{lstlisting}
Listing 1: Convolution Kernel for elastic distortion.

Our GPU program groups many threads into a block, where they share the same gaussian kernel and parts of the random field. The blocks contain 21 (the kernel size) 10 threads, each computing a vertical strip of the convolution operation (Listing 1).

Generating the elastic displacement field takes only 3 seconds. Deforming the whole training set is more than 10 times faster, taking 9 instead of the original 93 seconds. Further optimization would be possible by porting all deformations onto the GPU, and by using the hardware’s interpolation capabilities to perform the final bilinear interpolation. We omitted this since deformations are already pretty fast (deforming all images of one epoch takes only 5-15 % of total computation time, depending on MLP size).

Training algorithm

We closely follow the standard BP algorithm (e.g. Russell & Norvig, 2002, pages 744-748), except that BP of deltas and weight updates are disentangled and performed sequentially. This allows for more parallelism within each routine.

Forward propagation

The algorithm is divided into two kernels. The weight matrix is partitioned as illustrated in Figure 2.

Figure 2: Forward propagation: a) mapping of kernel 1 grid onto the padded weight matrix; b) mapping the kernel 2 grid onto the partial dot products matrix; c) output of forward propagation.

Kernel 1

Each block has 256 threads (Figure 2a), each computing a partial dot product of 32 component vectors. The dot products are stored in a temporary matrix (Figure 2b). This kernel has a very high throughput: average memory bandwidth is 115GB/s. This is possible because many relatively small blocks keep the GPU busy. Each block uses shared memory for storing the previous layer activations, which are simultaneously read by the first 32 threads of each block and then used by all 256 threads. After thread synchronization, the partial dot products are computed in parallel (Listing 2). The number of instructions is kept to a minimum by pre-computing all common index parts.

__global__ void MLP_FP_reduction_Kernel1(float *prevLN, float *W, float *partialsum, unsigned int neurons, unsigned int prevneurons){
        const int threads=256;
        const int stride=GET_STRIDE(neurons,pitch_x>>2);        //horizontal stride of W matrix
        int X=blockIdx.x*threads + threadIdx.x;         //precomputing expressions
        int Y=X+stride*blockIdx.y;
        int Z=blockIdx.y*pitch_y*stride + X;
        float sum=0.0f;
        __shared__ float output[pitch_y];
        if(blockIdx.y==0)
                if(threadIdx.x==0) output[0]=1.0f;
                else if(threadIdx.x<pitch_y)    //there are only 32 values to read and 128 threads
                        output[threadIdx.x] = threadIdx.x-1<prevneurons ? prevLN[threadIdx.x-1] : 0.0f;
                        else;
        else if(threadIdx.x<pitch_y)    //there are only 32 values to read and 128 threads
                        output[threadIdx.x] = blockIdx.y*pitch_y+threadIdx.x-1<prevneurons ?
                                                        prevLN[blockIdx.y*pitch_y+threadIdx.x-1] : 0.0f;
             else;
        __syncthreads();
        if(X<neurons){//compute partial sums
                //#pragma unroll 32
                int size=0;
                if((blockIdx.y+1)*pitch_y>=prevneurons+1)
                        size = prevneurons + 1 - blockIdx.y*pitch_y;
                else size=pitch_y;
                for (int ic=0; ic<size; ic++){
                        sum += output[ic] * W[Z];
                        Z+=stride;
                }
                partialsum[Y]=sum;
        }
}
__global__ void MLP_FP_reduction_Kernel2(float *currLN, float *partialsum, unsigned int neurons, unsigned int size){
        float sum=0.0f;
        int idx = blockIdx.x*(pitch_x>>2) + threadIdx.x;                //precomputed index
        unsigned int stride = GET_STRIDE(neurons,pitch_x>>2);           //stride for partialsum matrix
        if(idx>=neurons)return;                                         //is this thread computing a true neuron?
        for (int i=0; i<size; i++) sum += partialsum[i*stride+idx];     //computing the final dot product
        currLN[idx] = SIGMOIDF(sum);                                    //applying activation
}
Listing 2: Forward propagation kernels.

Kernel 2

The thread grid (Figure 2b) has only one row of blocks consisting of threads, since each thread has to compute a complete dot product (Figure 2c) and then pipe it into the activation function. This kernel (Listing 2) is inefficient for layers with fewer than 1024 incoming connections per neuron, especially for the last layer which has only ten neurons, one for each digit. That is, its grid will have only one block, occupying only 3% of the GTX280 GPU.

Backward propagation

This is similar to FP, but we need for coalesced access. Instead of transposing the matrix, the computations are performed on patches of data read from device memory into shared memory, similar to the optimized matrix transposition algorithm of Ruetsch & Micikevicius (2009). Shared memory access is much faster, without coalescing restrictions. Because we have to cope with layers of thousands of neurons, back-propagating deltas uses a reduction method implemented in two kernels communicating partial results via global memory (Listing 3).

Kernel 1 The bi-dimensional grid is divided into blocks of (32) threads. The kernel starts by reading a patch of values from W. The stride of the shared memory block is 33 (), thus avoiding all bank conflicts and significantly improving speed. Next, 32 input delta values are read and all memory locations that do not correspond to real neurons (because of vertical striding) are zero-padded to avoid branching in subsequent computations. The number of elements is fixed to size, and the computing loop is unrolled for further speedups. Before finishing, each thread writes its own partial dot product to global memory.

__global__ void backPropagateDeltasFC_s2_A(float *indelta, float *weights, unsigned int ncon, unsigned int nrneur, float *partial){
        const int               px = pitch_x>>2;
        unsigned int    stride_x = GET_STRIDE(nrneur,px);
        unsigned int    stride_y = GET_STRIDE(ncon,pitch_y);
        float                   outd = 0.0;
        int                             idx = blockIdx.x*px+threadIdx.x;
        int                             X = blockIdx.y*pitch_y*stride_x + idx;
        int                             Y = threadIdx.x;
        __shared__ float w[32*33];                              //pitch_y and px should be equal ! +1 to avoid bank conflict!
        __shared__ float id[px];                                //input delta
                #pragma unroll 32       //read the weight patch in shared memory
        for(int i=0;i<pitch_y;i++){w[Y]=weights[X]; X+=stride_x; Y+=33;}
        //read the input delta patch in shared memory
        if(idx>=nrneur) id[threadIdx.x]=0;      //a fake input delta for inexistent indelta
        else id[threadIdx.x]=indelta[idx];
        __syncthreads();        //not needed for block with warp number of threads: implicit synchronization
        #pragma unroll 32 //compute partial results
        for(int i=0;i<px;i++) outd+=w[threadIdx.x*33+i]*id[i];
        //write out the partial results
        partial[blockIdx.x*stride_y + blockIdx.y*pitch_y +  threadIdx.x] = outd;
}
__global__ void backPropagateDeltasFC_s2_B(float *outdelta,float *instates, unsigned int ncon, unsigned int nrneur, float *partial){
        int px=pitch_x>>2;
        unsigned int stride_x = GET_STRIDE(nrneur,px);
        unsigned int stride_y = GET_STRIDE(ncon,pitch_y);
        float outd = 0.0;
        int size=stride_x/px;
        int idx=blockIdx.x*pitch_y+threadIdx.x;
        if(idx==0);     //true only for block and thread 0
        else{
                for(int i=0;i<size;i++)
                        outd+=partial[i*stride_y + idx];
                outdelta[idx-1] = outd * DSIGMOIDF(instates[idx-1]);    //-1 BIAS 
        }
}
Listing 3: Backpropagating deltas kernels.

Kernel 2

This kernel completes BP of deltas by summing up partial deltas computed by the previous kernel. It multiplies the final result by the derivative of the activation function applied to the current neuron’s state, and writes the new delta to global memory.

Weight updating

__global__ void adjustWeightsFC_s1(float *states,float *deltas, float *weights, float eta, unsigned int ncon, unsigned int nrneur){
        const int pitch_y=16;
        const int threads=256;
        unsigned int px = pitch_x >> 2;
        unsigned int stride_x = GET_STRIDE(nrneur,px);
        float etadeltak = eta*deltas[blockIdx.x*threads+threadIdx.x],t;
        int b=blockIdx.y*stride_x*pitch_y + threads*blockIdx.x + threadIdx.x;
        __shared__ float st[pitch_y];   //for states
        int cond1 = blockIdx.y || threadIdx.x;
        int cond2 = (blockIdx.y+1)*pitch_y<=ncon;
        int size = cond2 * pitch_y + !cond2 * (ncon%pitch_y);
        if(threadIdx.x<pitch_y) st[threadIdx.x] = cond1 * states[blockIdx.y*pitch_y + threadIdx.x - 1] + !cond1;
        __syncthreads();
        if (blockIdx.x*threads + threadIdx.x < nrneur){
                #pragma unroll 16
                for (int j=0; j<16; j++){
                        t=weights[b];
                        t-= etadeltak * st[j];
                        weights[b]=t;
                        b+=stride_x;}}
}
Listing 4: Weights adjustment kernel.

The algorithm (Listing 4) starts by reading the appropriate delta, and pre-computes all repetitive expressions. Then the first 16 threads read the states from global memory into shared memory. The “bias neuron” with constant activation 1.0 is dealt with by conditional statements, which could be avoided through expressions containing the conditions. Once threads are synchronized, each single thread updates 16 weights in a fixed unrolled loop.

References

  • Bengio et al. (2006) Bengio, Y., Lamblin, P., Popovici, D. & Larochelle, H. (2006). Greedy layer-wise training of deep networks. Advances in Neural Information Processing Systems 19 (NIPS’06), 153 – 160.
  • Chellapilla et al. (2005) Chellapilla, K., Puri, S. & Simard, P. (2006). High Performance Convolutional Neural Networks for Document Processing. 10th International Workshop on Frontiers in Handwriting Recognition, 2006.
  • Decoste & Scholkopf (2002) Decoste, D. & Scholkopf, B. (2002). Training Invariant Support Vector Machines. Machine learning, 46, 161 – 190.
  • Hinton (2007) Hinton, G. (2007). To recognize shapes, first learn to generate images. Computational Neuroscience: Theoretical Insights into Brain Function., Elsevier.
  • Hochreiter (1991) Hochreiter, S. (1991). Untersuchungen zu dynamischen neuronalen Netzen. Diploma thesis, Institut für Informatik, Lehrstuhl Prof. Brauer, Technische Universität München.
  • Hochreiter et al. (2001) Hochreiter, S., Bengio, Y., Frasconi, P. & Schmidhuber, J. (2001). Gradient flow in recurrent nets: the difficulty of learning long-term dependencies. A Field Guide to Dynamical Recurrent Neural Networks, IEEE Press.
  • Hochreiter & Schmidhuber (1997) Hochreiter, S. & Schmidhuber, J. (1997). Long Short-Term Memory. Neural Computation, 9, 1735 – 1780.
  • Lauer et al. (2007) Lauer, F., Suen, C. & Bloch, G. (2007). A trainable feature extractor for handwritten digit recognition. Pattern Recognition, 40, 1816 – 1824.
  • LeCun (1985) LeCun, Y. (1985). Une procédure d’apprentissage pour réseau à seuil asymétrique. Proceedings of Cognitiva, Paris, 85, 599 – 604.
  • LeCun et al. (1998) LeCun, Y., Bottou, L., Bengio, Y. & Haffner, P. (1998). Gradient-Based Learning Applied to Document Recognition. Proceedings of the IEEE, 86, 309 – 318.
  • NVIDIA (2009) NVIDIA. (2009). NVIDIA CUDA. Reference Manual. version 2.3, 2009.
  • Ranzato et al. (2007) Ranzato, M., Huang, F., Boureau, Y. & LeCun, Y.(2007). Unsupervised Learning of Invariant Feature Hierarchies with Applications to Object Recognition.

    Proc. Computer Vision and Pattern Recognition Conference (CVPR’07)

    .
  • Ranzato et al. (2006) Ranzato, M., Poultney, C., Chopra, S. & LeCun, Y. (2006).

    Efficient Learning of Sparse Representations with an Energy-Based Model.

    Advances in Neural Information Processing Systems (NIPS 2006).
  • Ruetsch & Micikevicius (2009) Ruetsch, G. & Micikevicius, P. (2009). Optimizing Matrix Transpose in CUDA. NVIDIA GPU Computing SDK, 1 – 24.
  • Rumelhart et al. (1986) Rumelhart, D. E., Hinton, G. E. & Williams, R. J. (1986). Learning internal representations by error propagation. Parallel Distributed Processing, MIT Press, 1, 318 – 362.
  • Russell & Norvig (2002) Russell, S. & Norvig, P. (2002). Artificial Intelligence: A Modern Approach (2nd Edition). Prentice Hall, ISBN-13: 978-01379039555.
  • Salakhutdinov et al. (2007) Salakhutdinov, R. & Hinton, G. (2007). Learning a Nonlinear Embedding by Preserving Class Neighborhood Structure. Proceedings of the International Conference on Artificial Intelligence and Statistics, 11, 2007.
  • Scherer & Behnke (2009) Scherer, D. & Behnke, S. (2009). Accelerating Large-scale Convolutional Neural Networks with Parallel Graphics Multiprocessors. Proc. of NIPS 2009 Workshop on Large-Scale Machine Learning: Parallelism and Massive Datasets.
  • Simard et al. (2003) Simard, P.Y., Steinkraus, D. & Platt, J.C. (2003). Best Practices for Convolutional Neural Networks Applied to Visual Document Analysis. Intl. Conf. Document Analysis and Recognition, 958 – 962.
  • Steinkraus et al. (2005) Steinkraus, D., Buck, I. & Simard, P.Y. (2005). GPUs for Machine Learning Algorithms. ICDAR 2005, 2, 1115 – 1120.
  • Werbos (1974) Werbos, P.J. (1974). Beyond Regression: New Tools for Prediction and Analysis in the Behavioral Sciences. PhD thesis, Harvard University.