How to recognize handwritten digits in Oracle PL/SQL

This notebook contains the steps to deploy an example system for recognizing hand written digits of the MNIST dataset using Oracle and an artificial neural network serving engine implemented in PL/SQL

Author: Luca.Canali@cern.ch - July 2016

Steps:

  • Load test data and tensors into Oracle tables
  • Post-process those tables to make use of Oracle's linear algebra package UTL_NLA
  • Create a custom package MNIST to serve the artificial neural network
  • Test the package MNIST with test data consisitng of 10000 images of handwritten digits

Instructions to load the test data and tensors into Oracle

Note: you don't need this step if you previously followed the training steps in the notebook MNIST_tensorflow_exp_to_oracle.ipynb

Create the database user MNIST

For example run this using an account with DBA privileges: SQL> create user mnist identified by mnist default tablespace users quota unlimited on users; SQL> grant connect, create table, create procedure to mnist; SQL> grant read, write on directory DATA_PUMP_DIR to mnist;

The dump file file can be imported as follow:

  • Download the Oracle datapump file MNIST_tables.dmp.gz (see Github repository) and unzip it. Move the .dmp file to a valid directory, for example the directory DATA_PUMP_DIR which by default is $ORACLE_HOME/rdbms/log
  • use impdp to load the data (this has been tested on Oracle 11.2.0.4 and 12.1.0.2): impdp mnist/mnist tables=testdata,tensors directory=DATA_PUMP_DIR dumpfile=MNIST_tables.dmp

Post process the tables, this is because the following makes use of Oracle's linear algebra package UTL_NLA


In [29]:
%%bash
sqlplus -s mnist/mnist@dbserver:1521/orcl.cern.ch  <<EOF

-- create the table for test data, where the images of digits are stored as arrays of type utl_nla_array
create table testdata_array as
select a.image_id, a.label, 
cast(multiset(select val from testdata where image_id=a.image_id order by val_id) as utl_nla_array_flt) image_array 
from (select distinct image_id, label from testdata) a order by image_id;

-- create the table with tensor definitions, the tensors are stored as arrays of type utl_nla_array
create table tensors_array as
select a.name, cast(multiset(select val from tensors where name=a.name order by val_id) as utl_nla_array_flt) tensor_vals 
from (select distinct name from tensors) a;

EOF


Table created.


Table created.

Create the package that runs the neural network in PL/SQL

Notes:

  • The main function is MNIST.SCORE: it takes as input an image to process (p_testimage_array utl_nla_array_flt) and returns the predicted number.
  • The procedure MNIST.INIT, loads the tensors from the table tensors_array into PL/SQL global variables.

In [39]:
%%bash
sqlplus -s mnist/mnist@dbserver:1521/orcl.cern.ch  <<EOF

create or replace package mnist
as
    -- MNIST scoring enginge in PL/SQL
    -- Author: Luca.Canali@cern.ch, July 2016
    g_b0_array utl_nla_array_flt;
    g_W0_matrix utl_nla_array_flt;
    g_b1_array utl_nla_array_flt;
    g_W1_matrix utl_nla_array_flt;
    
    function score(p_testimage_array utl_nla_array_flt) return number;
    procedure init;
end;
/

create or replace package body mnist
as
    procedure init
    /* initialize the tensors that make up the neural network */
    as
    begin
        SELECT tensor_vals INTO g_W0_matrix FROM tensors_array WHERE name='W0';
        SELECT tensor_vals INTO g_W1_matrix FROM tensors_array WHERE name='W1';
        SELECT tensor_vals INTO g_b0_array FROM tensors_array WHERE name='b0';
        SELECT tensor_vals INTO g_b1_array FROM tensors_array WHERE name='b1';
    end;

    procedure print_debug(p_array utl_nla_array_flt)
    /* useful for debugging pourposes, prints an array to screen. requires set serveroutput on */
    as
    begin
        dbms_output.put_line('***************');
        for i in 1..p_array.count loop
            dbms_output.put_line('p_array(' || i ||') = ' || TO_CHAR(p_array(i),'9999.9999'));
        end loop;
        dbms_output.put_line('**************');
    end;
    
    function argmax(p_array utl_nla_array_flt)  return integer
    as
        v_index number;
        v_maxval float;
    begin
        v_index := 1;
        v_maxval := p_array(v_index);
        for i in 2..p_array.count loop
            if ( p_array(i) > v_maxval) then
                v_index := i;
                v_maxval := p_array(v_index);
            end if; 
        end loop;
    return(v_index);
    end;

    function score(p_testimage_array utl_nla_array_flt) return number
    as
        v_Y0 utl_nla_array_flt;
        v_output_array utl_nla_array_flt;
    begin
        v_Y0 := g_b0_array;

        /* this is part of the computation of the hidden layer, Y0 = W0_matrix * p_test_image_array + B0 */
        /* utl_nla.blas_gemv performs matrix multiplication and vector addition */
        utl_nla.blas_gemv(
                      trans => 'N',
                      m => 100,
                      n => 784,
                      alpha => 1.0,
                      a => g_W0_matrix,
                      lda => 100,
                      x => p_testimage_array,
                      incx => 1,
                      beta => 1.0,
                      y => v_Y0,
                      incy => 1,
                      pack => 'C'
        );

        /* This is part of the computation of the hidden layer: Y0 -> sigmoid(Y0) */
        for i in 1..v_Y0.count loop
            v_Y0(i) := 1 / ( 1 + exp(-v_Y0(i)));
        end loop;

        v_output_array := g_b1_array;

        /* this is part of the computation of the output layer, Y1 = W1_matrix * Y0 + B1 */
        /* utl_nla.blas_gemv performs matrix multiplication and vector addition */
        utl_nla.blas_gemv(
                      trans => 'N',
                      m => 10,
                      n => 100,
                      alpha => 1.0,
                      a => g_W1_matrix,
                      lda => 10,
                      x => v_Y0,
                      incx => 1,
                      beta => 1.0,
                      y => v_output_array,
                      incy => 1,
                      pack => 'C'
        );
        
        /* print_debug(v_output_array); */

        /* v_output_array needs to be passed via softmax function to provide a distribution probability */
        /* here we are only interested in the maximum value which gives the predicted number with an offset of 1 */
        return (argmax(v_output_array) - 1);

    end;
end;
/
EOF


Package created.


Package body created.

Test the scoring engine with one test image

Notes:

  • the images of the handwritten digits are encoded in the field image_array of the table testdata_array
  • The label field of testdata_array contains the value of the digit
  • When MNIST.SCORE output is equal to the label value, the neural network has predicted correctly the digit

In [35]:
%%bash
sqlplus -s mnist/mnist@dbserver:1521/orcl.cern.ch <<EOF

exec mnist.init
select mnist.score(image_array), label from testdata_array where rownum=1;

EOF


PL/SQL procedure successfully completed.


MNIST.SCORE(IMAGE_ARRAY)      LABEL
------------------------ ----------
		       7	  7

Test the scoring engine with all the test images

Notes:

  • from the SQL here below that the neural network and the serving engine MNIST.SCORE correctly predicts 9787 out of 10000 images, that is has an accuracy on the test set of ~98%
  • The execution time for processing 10000 test images is about 2 minutes, that is ~12 ms to process each image on average

In [38]:
%%bash
sqlplus -s mnist/mnist@dbserver:1521/orcl.cern.ch <<EOF

exec mnist.init
set timing on
select sum(decode(mnist.score(image_array), label, 1, 0)) "Images correctly identified",
       count(*) "Total number of images" 
from testdata_array;

EOF


PL/SQL procedure successfully completed.


Images correctly identified Total number of images
--------------------------- ----------------------
		       9787		     10000

Elapsed: 00:02:03.14

In [ ]: