
Introduction
The MNIST dataset consists of 70.000 records, each record representing a little greyscale image of size 28x28 pixels of a handwritten digit and the respective number as integer value , i.e. 28x28 = 784 integer values in the range interval [0,255] and one integer value for the number value. Here some examples:
from the website https://huggingface.co/datasets/mnist where the dataset can also be downloaded. This dataset is a very good example in order to demonstrate the power of neural networks since this dataset has a trivial vector space embedding. The greyscale values of the 784 pixels of each image build a 784-dimensional vector. These vector components can directly be used as input values for the input neurons of a neural network. The purpose of the network is to classify the image, i.e. recognize the digit on the image. In this article we demonstrate how the HANA predictive analysis library can be used in order to build such a neural network.
The Dataset
The dateset consists of two .csv files. The first file contains 60000 records the second one contains 10000 records. Both datasets are labeled. The first dataset will be used to train the neural network. The second dataset is used to test the neural network. For easy handling we will import the whole datasets into 2 DB tables by a simple SAPGUI upload. We have to create large DDIC structures for the DB tables and also for internal handling in the ABAP programs. These structures can be created as described in this article: Creating ABAP Dataframes for SAP PAL - SAP Community
Data Structures in ABAP
We create one DDIC structure for training and one DDIC structure for prediction. The structure for training consists of 784 float variables which represent the independent variables. Secondly we have a variable Y which represents the dependent variable (note that the training data records contain the dependent variable, also called the label; otherwise training would be impossible) . This variable is categorical and contains the prediction result. In our case it has the values '0', ''1, ..., '9'. Finally each data record gets an ID. ID has type integer (actually unsigned integer, >0). The structure for prediciton looks the same but does not have the Y-variable, since this value has to be predicted by the algorithm and thus cannot imported into to ML model.
Data structure for training data:
...
Data structure for prediction:
...
.
Database table for training data:
...
Database table for test data records:
...
.
Note that the DB table for test data records contains (in contrast to the corresponding DDIC structure) the dependent variable Y, because the purpose of the test dataset is to verify if the prediction works correctly.
Finally we need one database table for the model:
This table will contain only one record. The model are stored in the "DATA" field.
Uploading the Datasets
The .csv-files of datasets can be uploaded to the DB tables by using the follwoing program. This program uploads the training data.
*&---------------------------------------------------------------------* *& Report ZZZ_MNIST_UPLOAD *&---------------------------------------------------------------------* *& *&---------------------------------------------------------------------* REPORT zzz_mnist_upload. INCLUDE /cow/base_macros. DATA: lv_data_out TYPE xstring, lv_data_in TYPE xstring, lo_ocr TYPE REF TO /cow/cl_ocr, lo_exc TYPE REF TO /cow/cx_root, lt_file TYPE filetable, lv_filename TYPE string, lv_rc TYPE i, lv_size TYPE i, lt_data TYPE TABLE OF string, lo_file TYPE REF TO /cow/cl_fs_file, lv_file_ext TYPE /cow/file_ext, ls_message TYPE bapiret2, lt_string TYPE TABLE OF string, lv_index_var TYPE sy-index. FIELD-SYMBOLS: <fs_filename> TYPE any. TRY. * Choose file CLEAR: lt_file, lv_rc. cl_gui_frontend_services=>file_open_dialog( EXPORTING window_title = 'Upload data' default_extension = 'CSV' CHANGING file_table = lt_file rc = lv_rc EXCEPTIONS file_open_dialog_failed = 1 cntl_error = 2 error_no_gui = 3 not_supported_by_gui = 4 OTHERS = 5 ). IF sy-subrc <> 0. fill_mess ls_message 'I' '/COW/DCO_MESSAGES' '001'. raise_exc /cow/cx_root ls_message. ENDIF. * Get filename READ TABLE lt_file INDEX 1 ASSIGNING <fs_filename>. IF sy-subrc NE 0. fill_mess ls_message 'I' '/COW/DCO_MESSAGES' '006'. raise_exc /cow/cx_root ls_message. ENDIF. lv_filename = <fs_filename>. * Upload file CLEAR lt_data. cl_gui_frontend_services=>gui_upload( EXPORTING filename = lv_filename filetype = 'ASC' IMPORTING filelength = lv_size CHANGING data_tab = lt_data EXCEPTIONS OTHERS = 19 ). IF sy-subrc <> 0. fill_mess ls_message 'I' '/COW/DCO_MESSAGES' '002'. raise_exc /cow/cx_root ls_message. ENDIF. DATA: lv_tabix TYPE string, lv_index TYPE sy-tabix, ls_input TYPE ZZZ_S_MNIST_INPUt, lt_input TYPE ZZZ_t_MNIST_INPUt, lv_name TYPE string, lt_train_db TYPE TABLE OF zmnist_train. LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<ls_data>). lv_index = sy-tabix. CLEAR: lt_string. SPLIT <ls_data> AT ',' INTO TABLE lt_string. LOOP AT lt_string ASSIGNING FIELD-SYMBOL(<lv_string>). lv_tabix = sy-tabix. IF lv_tabix GT 1. lv_index_var = lv_tabix - 1. lv_name = 'X_' && |{ lv_index_var }|. ASSIGN COMPONENT lv_name OF STRUCTURE ls_input TO FIELD-SYMBOL(<lv_any>). <lv_any> = <lv_string>. IF <lv_any> = 0. <lv_any> = '0.00001'. ELSE. <lv_any> = <lv_any> / 256. ENDIF. ENDIF. IF lv_tabix = 1. ASSIGN COMPONENT 'Y' OF STRUCTURE ls_input TO <lv_any>. <lv_any> = <lv_string>. ENDIF. ENDLOOP. ls_input-id = lv_index. APPEND ls_input TO lt_input. ENDLOOP. MOVE-CORRESPONDING lt_input TO lt_train_db. MODIFY zmnist_train FROM TABLE lt_train_db. CATCH /cow/cx_root INTO lo_exc. IF lo_ocr IS NOT INITIAL. * /Dequeue lo_ocr->/cow/if_enqueue~dequeue( 1 ). ENDIF. * Send message lo_exc->to_message( 'E' ). ENDTRY. BREAK-POINT.
Since the two tables have the same structure, the same program can be used in order to upload the test data. Only the line 109 has to be changed to 'MODIFY zmnist_test FROM TABLE lt_train_db'.
ML Model
As already mentioned we want to use a neural network with one or more hidden layers to predict the digits on the images. The corresponding model in HANA PAL is the "Multilayer Perceptron": https://help.sap.com/docs/SAP_HANA_PLATFORM/2cfbc5cf2bc14f028cfbe2a2bba60a50/ddd236d66f394dea885f61c....
We want to use this model from within ABAP. Therefore we need two classes with AMDP-methods in order to execute the model training and model inference. (One can also implement both methods in a single class). The class for training:
CLASS z_cl_pal_nn_mnist DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_amdp_marker_hdb.
CLASS-METHODS:
do_nn_train
IMPORTING
VALUE(it_data) TYPE zzz_t_mnist_input
VALUE(it_param) TYPE zzz_t_pal_param
EXPORTING
VALUE(et_model) TYPE zzz_t_pal_model
VALUE(et_stat) TYPE zzz_t_pal_stat
VALUE(et_train_log) TYPE zzz_t_pal_train_log
VALUE(et_opt_param) TYPE zzz_t_pal_opt_param.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS Z_CL_PAL_NN_MNIST IMPLEMENTATION.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method Z_CL_PAL_NN_MNIST=>DO_NN_TRAIN
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_DATA TYPE ZZZ_T_MNIST_INPUT
* | [--->] IT_PARAM TYPE ZZZ_T_PAL_PARAM
* | [<---] ET_MODEL TYPE ZZZ_T_PAL_MODEL
* | [<---] ET_STAT TYPE ZZZ_T_PAL_STAT
* | [<---] ET_TRAIN_LOG TYPE ZZZ_T_PAL_TRAIN_LOG
* | [<---] ET_OPT_PARAM TYPE ZZZ_T_PAL_OPT_PARAM
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD do_nn_train BY DATABASE PROCEDURE FOR HDB LANGUAGE SQLSCRIPT.
CALL _SYS_AFL.PAL_MULTILAYER_PERCEPTRON(:it_data, :it_param, et_model, et_train_log, et_stat, et_opt_param );
ENDMETHOD.
ENDCLASS.
The class for testing (prediction):
CLASS z_cl_pal_nn_predict_mnist DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_amdp_marker_hdb.
CLASS-METHODS:
do_nn_predict
IMPORTING
VALUE(it_data) TYPE ZZZ_T_MNIST_PREDICT
VALUE(it_model) TYPE zzz_t_pal_model
VALUE(it_param) TYPE zzz_t_pal_param
EXPORTING
VALUE(et_result) TYPE zzz_t_pal_pr_result
VALUE(et_softmax) TYPE zzz_t_pal_pr_softmax.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS Z_CL_PAL_NN_PREDICT_MNIST IMPLEMENTATION.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method Z_CL_PAL_NN_PREDICT_MNIST=>DO_NN_PREDICT
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_DATA TYPE ZZZ_T_MNIST_PREDICT
* | [--->] IT_MODEL TYPE ZZZ_T_PAL_MODEL
* | [--->] IT_PARAM TYPE ZZZ_T_PAL_PARAM
* | [<---] ET_RESULT TYPE ZZZ_T_PAL_PR_RESULT
* | [<---] ET_SOFTMAX TYPE ZZZ_T_PAL_PR_SOFTMAX
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD do_nn_predict BY DATABASE PROCEDURE FOR HDB LANGUAGE SQLSCRIPT.
CALL _SYS_AFL.PAL_MULTILAYER_PERCEPTRON_PREDICT(:it_data, :it_model, :IT_PARAM, et_result, et_softmax );
ENDMETHOD.
ENDCLASS.
Training of the Model
The model will be trained by the following program:
*&---------------------------------------------------------------------*
*& Report ZZZ_MNIST_TRAIN
*&---------------------------------------------------------------------*
*&
*&---------------------------------------------------------------------*
REPORT zzz_mnist_train.
DATA: lt_input TYPE zzz_t_mnist_input,
ls_input TYPE zzz_s_mnist_input,
lt_param TYPE zzz_t_pal_param,
ls_param TYPE zzz_s_pal_param,
lt_model TYPE zzz_t_pal_model,
lt_stat TYPE zzz_t_pal_stat,
lt_train_log TYPE zzz_t_pal_train_log,
lt_opt_param TYPE zzz_t_pal_opt_param.
SELECT * FROM ZMNIST_TRAin INTO TABLE (lt_input_db) ORDER BY id ASCENDING.
LOOP AT lt_input_db ASSIGNING FIELD-SYMBOL(<ls_input_db>).
MOVE-CORRESPONDING <ls_input_db> TO ls_input.
APPEND ls_input TO lt_input.
ENDLOOP.
CLEAR lt_input_db.
ls_param-param_name = 'HIDDEN_LAYER_ACTIVE_FUNC'.
ls_param-int_value = 4.
APPEND ls_param TO lt_param.
CLEAR ls_param.
ls_param-param_name = 'OUTPUT_LAYER_ACTIVE_FUNC'.
ls_param-int_value = 4.
APPEND ls_param TO lt_param.
CLEAR ls_param.
ls_param-param_name = 'HIDDEN_LAYER_SIZE'.
ls_param-string_value = '100'.
APPEND ls_param TO lt_param.
CLEAR ls_param.
ls_param-param_name = 'HAS_ID'.
ls_param-int_value = 1.
APPEND ls_param TO lt_param.
CLEAR ls_param.
ls_param-param_name = 'MAX_ITERATION'.
ls_param-int_value = 50.
APPEND ls_param TO lt_param.
CLEAR ls_param.
ls_param-param_name = 'DEPENDENT_VARIABLE'.
ls_param-string_value = 'Y'.
APPEND ls_param TO lt_param.
CLEAR ls_param.
ls_param-param_name = 'WEIGHT_INIT'.
ls_param-int_value = 1.
APPEND ls_param TO lt_param.
CLEAR ls_param.
ls_param-param_name = 'CATEGORICAL_VARIABLE'.
ls_param-string_value = 'Y'.
APPEND ls_param TO lt_param.
CLEAR ls_param.
ls_param-param_name = 'LEARNING_RATE'.
ls_param-double_value = '0.001'.
APPEND ls_param TO lt_param.
CLEAR ls_param.
ls_param-param_name = 'MOMENTUM_FACTOR'.
ls_param-double_value = '0.0'.
APPEND ls_param TO lt_param.
CLEAR ls_param.
ls_param-param_name = 'THREAD_RATIO'.
ls_param-double_value = '0.5'.
APPEND ls_param TO lt_param.
z_cl_pal_nn_mnist=>do_nn_train(
EXPORTING
it_data = lt_input
it_param = lt_param
IMPORTING
et_model = lt_model
et_stat = lt_stat
et_train_log = lt_train_log
et_opt_param = lt_opt_param ).
DATA: ls_db_model TYPE zmnist_model.
LOOP AT lt_model ASSIGNING FIELD-SYMBOL(<ls_model>).
IF sy-tabix = 1.
ls_db_model-data = <ls_model>-model_content.
ELSE.
ls_db_model-data = ls_db_model-data && |{ <ls_model>-model_content }|.
ENDIF.
ENDLOOP.
ls_db_model-id = 1.
DELETE FROM zmnist_model.
MODIFY zmnist_model FROM ls_db_model.
Training is a very resource consuming task and depends also strongly on the parameters of the model. Therefore this program should run in background task. In this setup the training run roughly 1,5 hours. More layers, more neurons per layer, smaller learning rate or small momentum > 0 factor can result in even longer runtimes. We are using one hidden layer with 100 neurons. We do not use a momentum.
Testing of the Model
The model can be tested by the following program:
*&---------------------------------------------------------------------*
*& Report ZZZ_MNIST_PREDICT
*&---------------------------------------------------------------------*
*&
*&---------------------------------------------------------------------*
REPORT zzz_mnist_predict.
DATA: ls_model_db TYPE zmnist_model,
lt_model TYPE zzz_t_pal_model,
ls_model TYPE zzz_s_pal_model,
lt_data TYPE zzz_t_mnist_predict,
ls_data TYPE zzz_s_mnist_predict,
lt_param TYPE zzz_t_pal_param,
lt_result TYPE zzz_t_pal_pr_result,
lt_softmax TYPE zzz_t_pal_pr_softmax.
SELECT SINGLE * FROM zmnist_model INTO ls_model_db.
WHILE ls_model_db-data IS NOT INITIAL.
DATA(lv_index) = sy-index.
ls_model-row_index = lv_index.
ls_model-model_content = ls_model_db-data.
APPEND ls_model TO lt_model.
SHIFT ls_model_db-data LEFT BY 5000 PLACES.
ENDWHILE.
SELECT * FROM zmnist_test INTO CORRESPONDING FIELDS OF TABLE lt_data.
*SELECT SINGLE * FROM zmnist_test INTO (ls_pred)
* WHERE id = 100.
*
*MOVE-CORRESPONDING ls_pred TO ls_data.
*APPEND ls_data TO lt_data.
z_cl_pal_nn_predict_mnist=>do_nn_predict(
EXPORTING
it_data = lt_data
it_model = lt_model
it_param = lt_param
IMPORTING
et_result = lt_result
et_softmax = lt_softmax ).
BREAK-POINT.
If the training was succesful the table lt_result contains for all 10000 test data records the predicted result. Ideally this should be the same value as the value of the dependend variable Y in table ZMNIST_TEST.
Note: The webeditor does not allow the \address symbol in the select statements. The correct select statement is:
SELECT * FROM ZMNIST_TRAin INTO TABLE \addressDATA(lt_input_db) ORDER BY id ASCENDING.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
9 | |
7 | |
7 | |
7 | |
6 | |
5 | |
5 | |
5 | |
5 | |
5 |