ローグウェーブソフトウェアのブログ

開発をシンプルに 安全で高品質のコードを 素早くお客様のもとへ

チュートリアル: SourceProとJMSLを使ったデータベース内統計解析

データベース内に数値計算や統計解析を組み込んで実行すると、スピードやデータの精度、セキュリティなど様々なメリットがあります。

今回ご紹介するローグウェーブのホワイトペーパーは、ローグウェーブのJMSLライブラリリレーショナルデータベース管理システム(RDMBS)に埋め込み、さらに同じくローグウェーブのSourcePro DBライブラリを使って、データベース内で数値解析を行う方法を説明したものです。

JMSLは40年以上にわたって業界標準として使用されてきたIMSL数値ライブラリのJava実装で、データマイニングや予測解析など広範な数値計算や統計解析のアルゴリズムをさまざまなデータベースに対して適用できます。

SourceProはC++開発用のクロスプラットフォームライブラリで、シリアル化やスレッドなどの基礎からネットワーク、データベースまで広範なクラスを備えています。SourcePro DBは様々なデータベースに対応したオブジェクト指向のデータベース操作クラスで、ベンダーごとの差異を吸収した共通のコードを使える一般的な操作から、性能をチューニングしたデータベースごとのクラスまで必要に応じて様々な抽象レベルを選ぶことができます。

チュートリアル: SourceProとJMSLを使ったデータベース内解析 (概要)

Tech tutorial: Embedding analytics into a database using SourcePro and JMSL

データベース内組み込み解析には多くのメリットがあります。

  • リアルタイム解析 - ネットワーク越しのデータ同期にかかる遅延が発生しない
  • 正確性 - データと計算が同じ場所のため、データの転送ミスが発生しない。
  • アクセス性 - データベースに接続できるならどんな言語でも使用可能
  • ノイズの少ないデータ - 処理を開始する前にデータをクリーニングできる。
  • 安全性 - 計算に使ったデータがどこかに残ってしまうことがなく、またアクセス制限をかけることも可能

概要

ビッグデータ解析には多くの戦術が存在します。大規模クラスタを使ったHadoopMapReduceも有力なソリューションの一つですが、以下の記事で触れたように、あらゆる場合に対応できるというわけではありません。

blog.roguewave.jp

とはいえHadoopがもたらしたパラダイムの大きな変化とは、大量のデータや結果をデータベースと処理用のクライアントマシンの間でやりとりしなくてもよくなったことです。

JMSLを組み込む

リレーショナルデータベースにはストアドプロシージャと呼ばれる操作用のルーチンが備わっています。また、多くはデータベースで使用可能なJVMを備えています。Java側ではSQLの基本的なデータ型に1対1で対応したデータ型を持っています。こうしたことからデータベース内で実行可能なJavaのルーチンをシームレスにjarファイル(Java アーカイブ)に入れて組み込み、不要なデータの移動をなくすことができるのです。

今回のサンプルではOracle 12cデータベースを使用してナイーブベイズの分類アルゴリズムにもとづく予測解析を行います。データベース内には有名なフィッシャーのアイリス(iris、アヤメ)のデータを登録しておきます。そしてSourcePro DBを使ってストアドプロシージャを生成、実行してこの組み込まれたナイーブベイズアルゴリズムを呼び出します。完全なコードを末尾に置いておきます。

JMSL起動

Oracleにはloadjavaというコマンドラインツールがあり、データベースにJavaソースファイルやクラス、ライブラリ(jar)を追加することができます。

> loadjava −u sys@ORCL −resolve jmsl.jar

-uフラグでユーザーとデータベースを指定し、-resolveフラグでロードされるオブジェクトの依存関係を解決します。

JMSLストアドプロシージャの実装

3つのステップから成ります

Javaクラスを書く

ナイーブベイズ分類器には2つの主要な入力パラメータがあります。1つは分類済みのトレーニングセット、もう1つはこれから分類すべきデータセットです。メソッドのエントリーポイントとしては以下のようになります。

public static int Classify ( ResultSet rsT, ResultSet rsC, ResultSet rsRows, ResultSet rsClasses )

Classifyは前回未知だったデータのクラスを整数で返します。入力の型は全てjava.SQL.ResultSetで、SQL CURSORの変数型SYS_REFCURSORにマッピングされます。入力変数は順にトレーニングデータ、未知のデータ、トレーニングセットのサイズ、可能なタイプの個数です。このメソッドは RWNaiveBayes.javaクラスに所属しています(Appendix参照)。

クラスをロードする

コマンドラインユーティリティからJMSLライブラリをロードします。

> loadjava −verbose −u sys@ORCL −resolve RWNaiveBayes.class

経験によると、クラスはソースをデータベース内でコンパイルするよりもコンパイル済みのものをロードしたほうが問題が少ないです。アップロードの際に十分な情報を得られるよう、resolveフラグに-verboseオプションを追加しています。

クラスメソッドを公開する

SQLから呼び出されるメソッドはトップレベルのエントリーポイントとしてOracleに登録される必要があります。SourcePro DBを使って実行してみましょう。まずデータベースに接続するために、RWDBManagerに対して RWDBDatabaseのインスタンスをリクエストし、データベースインスタンスからRWDBConnectionオブジェクトをインスタンス化します。

RWDBDatabase db = RWDBManager::database("ORACLE_OCI", "<SERVER NAME>
                                        ", "<USER>",
                                        "<PASSWORD>", "");
RWDBConnection conn = db.connection();
if(conn.isValid()) {
    std::cout << "Connected!" << std::endl;
}
else {
    std::cout << conn.status().message() << std::endl;
}

次にJMSLルーチンを呼び出すためのストアド関数RWNaiveBayesが作成します。

RWCString rwNaiveBayesText("CREATE or REPLACE FUNCTION RWNaiveBayes( \
                     curs_train IN SYS_REFCURSOR, \
                     curs_class IN SYS_REFCURSOR, \
                     curs_numrows IN SYS_REFCURSOR, \
                     curs_numclass IN SYS_REFCURSOR) \
                     RETURN NUMBER \
                     AS LANGUAGE JAVA \
                     NAME ‘RWNaiveBayes.Classify( \
                     java.sql.ResultSet, \
                     java.sql.ResultSet, \
                     java.sql.ResultSet, \
                     java.sql.ResultSet ) return int’;");
if (!conn.executeSql(rwNaiveBayesText).isValid()) {
    std::cout << "Failed to create rwNaiveBayesText stored function";
    return 0;
} 

そしてストアドプロシージャ Invoke_RWNaiveBayesを作成し、iris_trnとfaux_irisのテーブルに格納されている値をストアド関数RWNaiveBayesに渡します。

RWCString stprocText("create or replace procedure Invoke_RWNaiveBayes ( \
                     val out INTEGER) \
                     IS \
                     BEGIN \
                     SELECT CT into val FROM (SELECT RWNaiveBayes(
                         CURSOR(SELECT classification, sepallength,
                                sepalwidth, petallength, petalwidth FROM iris_trn ), \
                         CURSOR( SELECT * FROM faux_iris OFFSET 8 ROWS
                                 FETCH FIRST 1 ROWS ONLY), \
                         CURSOR( SELECT COUNT(*) FROM iris_trn ), \
                         CURSOR( SELECT MAX(classification) FROM iris_trn )) \
                     AS CT FROM dual); \
                     END;");
if (!conn.executeSql(stprocText).isValid()) {
    std::cout << "Failed to create stored procedure";
    return 0;
}

これはデモなので分類用に単一のクエリを使いましたが、実際はこの例のようなサブクエリでなくストアドビューを使うことが多いでしょう。

6行目のサブクエリでiris_trnテーブルに格納されていたフィッシャーのアヤメのデータから値をセレクトしてトレーニングセットから4つの値をパラメータをセレクトしてデータを特徴づけます。7行目でfaux_irisに格納されていたテストデータのテーブルの8行目をセレクトし、コードの8行目と9行目でiris_trnテーブルから行の数とクラス数をセレクトしています。

3つのレベルのSELECTが使用されています。まずエイリアスCTから分類の型を得ます。2番めのレベルでは関数SELECT RWNaiveBayesを呼びだします。3番めにデータベースからベイズ分類器に渡されるデータをセレクトします。6行目から9行目まではベイズ分類器の4つのパラメータです。通常ストアドプロシージャにクエリを渡すときは CURSORの形を取ることが多いです。そのためCURSOR( SELECT …)の構文が使われるのです。

このサンプルのポイントはSQLの簡単な構文だけで組み込み解析を行うことです。メモリの制約があるため、トレーニングデータのパラメータの数つまりデータベースの列は少ないほうがよいでしょう。その他のJMSLライブラリも同様に実装できます。

最後にストアドプロシージャを呼び出して出力結果を得ます。

int output = 0;
RWDBStoredProc stproc = db.storedProc("Invoke_RWNaiveBayes", conn);
stproc << &output;
if (stproc.execute(conn).isValid()) {
    std::cout << "Output Classification Value is: " << output << std::endl;
}

まとめ

このホワイトペーパーであるアルゴリズムを実装してJMSLによる組み込み解析を行う例を紹介しました。こうしたJMSLを使ったアルゴリズムは既存のRDBMSシステムの上でそのまま動きます。SourcePro DBは直感的なAPIで容易にRDBMSシステムに接続し、JMSLアルゴリズムを呼び出すことができます。必要なのはここで書いたようなJMSLのラッパーを最初に書くことだけです。JMSLは信頼できる広範なアルゴリズムを備えているため、あとは単一のデータベース内で解析すれば、データ転送に伴う遅延や破損の恐れもなくセキュアに巨大なスループットの改善を得ることができます。

Appendix

iris_trnテーブル

ID INT # ID, used to identify the training pattern
SEPALLENGTH DECIMAL(5,1) # sepal length
SEPALWIDTH DECIMAL(5,1) # sepal width
PETALLENGTH DECIMAL(5,1) # petal length
PETALWIDTH DECIMAL(5,1) # petal width
SPECIESOFIRIS VARCHAR(20) # name of Iris species
CLASSIFICATION INT # Iris species classification

faux_irisテーブル

SEPALLENGTH DECIMAL(5,1) # sepal length
SEPALWIDTH DECIMAL(5,1) # sepal width
PETALLENGTH DECIMAL(5,1) # petal length
PETALWIDTH DECIMAL(5,1) # petal width

クラス概要

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import com.imsl.datamining.*;
import com.imsl.stat.NormalDistribution;
public class RWNaiveBayes {
    // entry point
    public static int Classify ( ResultSet rsT, ResultSet rsC,
                                 ResultSet rsRows, ResultSet rsClasses )
        throws SQLException { … }
    // function over-ride of the entry point method 
    public static int Classify ( ResultSet rsT, ResultSet rsC, int nrows, int nClasses )
        throws SQLException { … }
    // train
    public static NaiveBayesClassifier Train(ResultSet rs, int nrows,
                                             int nClasses) throws SQLException { … }

エントリーポイント

public static int Classify ( ResultSet rsT, ResultSet rsC,
                             ResultSet rsRows, ResultSet rsClasses )
    throws SQLException {
    int nclass = -1;
    try {
        rsRows.next();
        int nrows = rsRows.getInt(1);

        rsClasses.next();
        int nClasses = rsClasses.getInt(1);

        nclass = Classify( rsT, rsC, nrows, nClasses);
    } catch (SQLException e) {
        System.err.println(e);
        e.printStackTrace(); throw(e);
    }
    return nclass;
}

エントリーポイントのオーバーライド

public static int Classify ( ResultSet rsT, ResultSet rsC,
                             int nrows, int nClasses )throws SQLException { int nclass = -1;
    ResultSetMetaData rsmd = null;
    try {
        NaiveBayesClassifier nbTrainer = Train( rsT, nrows, nClasses); rsmd =
                                                                           rsC.getMetaData();
        int nContinuous = rsmd.getColumnCount();

        double[] continuousInput = new double[nContinuous];

        rsC.next();

        for (int jj=0; jj<nContinuous; jj++)
            continuousInput[jj]=rsC.getDouble(jj+1); double[]
                                                         classifiedProbabilities = new double[nClasses];
        classifiedProbabilities =
            nbTrainer.probabilities(continuousInput, null);

        nclass = nbTrainer.predictClass(continuousInput, null) + 1;

    } catch (SQLException e) {
        System.err.println(e);
        e.printStackTrace(); throw(e);
    }
    return nclass;
} 

訓練器

public static NaiveBayesClassifier Train(ResultSet rs,
                                         int nrows, int nClasses) throws SQLException {
    NaiveBayesClassifier nbTrainer = null;
    ResultSetMetaData rsmd = null; try {
        rsmd = rs.getMetaData(); int
                                     ncolumns = rsmd.getColumnCount();
        int nContinuous = ncolumns-1;
        int nNominal =0; /* no nominal input attributes */
        int[] classification = new int [nrows];
        double[][] continuousData = new double[nrows][nContinuous];

        int ii=0;

        while( rs.next() ) {
            // get the class type
            classification[ii] = rs.getInt(1)-1;
            // get the attributes
            for (int jj=0; jj<nContinuous; jj++)
                continuousData[ii][jj] = rs.getDouble(jj+2);

            ii++;
        }

        nbTrainer = new NaiveBayesClassifier(nContinuous, nNominal, nClasses);

        for (int i=0; i<nContinuous; i++)
            nbTrainer.createContinuousAttribute(new NormalDistribution());
        // train the classifier
        nbTrainer.train(continuousData, classification );
    } catch (SQLException e) {
        System.err.println(e);
        e.printStackTrace(); throw(e);
    }
    return nbTrainer;
}

編集後記

いかがだったでしょうか?JMSLを使った組み込み解析については以前の記事でも何度かご紹介しました。 blog.roguewave.jp

blog.roguewave.jp

blog.roguewave.jp

今回面白かった点はやはり強力なSourcePro DBとの組み合わせだったと思います。

可視化ライブラリVisualization数値解析IMSLC++開発支援SourceProなどローグウェーブのライブラリ製品の多くに共通することとして、APIが安定していて使い方が一貫しておりクロスプラットフォームであるため、コードのビルディングブロックとしてお使いのコードに組み込み、一度書いてしまえばプラットフォームやデータベースの更新、変更に煩わされることなく開発を自由に展開していくことができるのが大きな利点だと言えます。

ローグウェーブのライブラリ製品はそれぞれ膨大な関数が用意されているため、今回のようにこれらを組み合わせれば、すでに提供されている機能についてスクラッチからコードを書いたり(車輪の再発明!)メンテナンスに煩わされる必要なく、独自の機能開発に集中することができます。

ご興味があればお気軽に声をおかけください

ローグウェーブ セールスエンジニア 柄澤(からさわ)