Graph Machine Learning with PyPGX and OML4Py

#はじめに
この記事はRhicheek Patraさんの
Graph Machine Learning with PyPGX and OML4Py

https://blogs.oracle.com/ai-and-datascience/post/graph-machine-learning-with-pypgx-and-oml4py

の日本語版です。

PyPGXとOML4Pyを用いることでスケーラブルでセキュアなグラフ機械学習アプリケーションを開発することができます。この記事ではPyPGXとOML4Pyを利用したグラフ機械学習による不正侵入検知システムの構築方法を説明します。

#PyPGXとOML4Pyとは?
PyPGXは新たにリリースされたPGXのためのPythonクライアントです。graph analysisのツールキットであり、OracleデータベースのProperty Graphという機能として利用可能です。

OML4PyはOracle Machine Learning (OML)へのPython インターフェースで、データベース内やビッグデータの環境でマシンラーニングアルゴリズムのスケーラビリティをサポートするための一連の製品です。
OMLの主要なメリットとして、すべてのmachine learningのファンクショナリティをOracleデータベース内で実行させることが可能となっています。このような特徴により、下記のような利点があります。

* スケーラビリティの担保 (Oracle データベースは巨大なデータを保存することが可能なため)
* パフォーマンスの向上 (継続的にデータをデータベースから取り出さないでもフェッチとプッシュが可能になっているため)
* セキュリティ (保護されているオラクルデータベース外へデータの持ち出しが不要なため)

## 不正侵入検知システムシステムの構築
### 不正侵入検知とは?
不正侵入検知は、広範囲のシステムとパブリック/プライベートのネットワークトラフィックを監視するための広義の用語です。我々のユースケースでは、不正侵入検知をパブリックネットワーク(eg. ウェブ) のトラフィックと悪意のある活動の検知のための問題監視と定義します。
今回のケースでのネットワークトラフィックはいわゆるパケットキャプチャを想定しており、IPアドレス同士の接続ログを指しています。下記の表はそのようなパケットキャプチャの例です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/99a25752-10ad-b23f-ca7b-9fde479a3d97.png)

 
この表はグラフのエッジリストとして認識することが可能です。結果のグラフ構成として、IPアドレスをエッジとして取り扱い、2つのIPアドレスABのやりとりはA,Bのノードの間のエッジとして扱うことでパケットキャプチャはグラフとして示すことができます。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/873fc65f-b637-9009-7741-0cde17984dce.png)

 
この図は悪意のあるパケットキャプチャの例を示しています。

ウェブ検索エンジン上での犠牲者は、クリックをして、AのIPアドレスに接続しますが、Aはセキュリティ的に危険な状態のアドレスでした。そのためAは接続者をBにリダイレクトして、不正なダウンロードを行わせようとします。この不正でアクセスした人のマシンの管理者権限を奪おうとし、成功した場合、Cのマスターに感染を知らせます。するとCは感染者をDのサーバーにリダイレクトさせて、さらに別のマルウェア(データ復元のために資金を要求するランサムウェアなど)をインストールさせます。

より詳細なパケットキャプチャと不正侵入検出の説明に興味がある場合はreport on DynaMiner and malware detectionの記事をご参照ください。

##分類器(Classifier)のトレーニング
今回の例では様々なパケットキャプチャを用いて、不正なパケットキャプチャをその始まりから判断できるようになるために分類器のトレーニングを行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/67d2a1dc-2e31-acc2-5f3c-e5154ffce931.png)
#PyPGX and OML4Pyを用いた不正検知
##概要
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/03f648dd-6317-cfa7-ecef-ec05e2931a25.png)
3つのステージに分けて問題を解決していきます。上記図では、3つのメインステージとそれぞれ異なるステップを確認できます。3つのメインステージは以下のようなステップに分かれています。

1. データの準備: 
最初のステージではローパケットキャプチャのデータをフェッチして、後のPGXサーバでグラフを作成できるような形に変換します。それらのいわゆるグラフファイルと呼ばれるものはOracle Database上に保存されます。
1. 特長の生成: 2つ目のステージでは、PGXサーバがグラフファイルをデータベースから読み込んで、それを基にグラフを作成します。これらのグラフはPGXのrich graph-algorithmライブラリを使用して大々的に分析されます。これらの分析結果はOracle Databaseの特徴量を集めたひとつの大きな表に保存されます
1. 分類: 3番目のステージではステージ2で作成した特徴量を集めたひとつの大きな表を使用して、できる限り正確な予測のために様々な分類器のトレーニングを行います。そのためにOMLの AutoMLの機能やデータベース内でのPythonの実行も行います。

##グラフの作成
##グラフファイルの作成
###PGXとOMLサーバーへの接続
PGXとOMLの機能を使うために、まずはセッションをそれぞれのサーバーに接続させます。これはシンプルでどちらもPGXでは pypgx.get_session() コマンド、OMLではoml.connect()ファンクションで実行できます。

```python
import pypgx
import oml
  
# Connect to the PGX server
session = pypgx.get_session(base_url="http://localhost:7007",  token="some_token")
analyst = session.analyst
  
data_source_name = '(DESCRIPTION=data_source_of_the_corresponding_oracle_database)'
  
# Connect with existing oracle database
oml.connect("example_user","example_password", dsn = data_source_name, automl = data_source_name)
  
# Check whether connection attempt was successful
print(oml.isconnected())-->
```
ノート:OMLを使用するためには、 OMLを実行可能なOracle Databaseが必要になっており、そうではない場合、oml.connect()ファンクションはエラーを返します。

###グラフファイルの作成
このステップでは、インプットされた外部ソース(ファイルシステムやデータベース等) からのローデータのフェッチを行い、PGXサーバが後程正確なグラフを作成可能なフォーマットに変換します。

次のステップでは2つのグラフを作成します: 　

* ノードテーブル: この表はすべての頂点を関連する頂点プロパティとともに保存します
* エッジテーブル: この表はすべてのエッジを関連するエッジプロパティとともに保存します

これらのステップはどのような生データが保存されているのかに依存し、PyPGXやOML4PYの機能は記載はありません。なので実際のテーブルを作成するためのコードは省略し、ここでは結果のみを示しています。

このステップが終了したら、この表は pandas DataFrameに保存され、以下のような形式になっています。
ノードテーブル (悪意のあるgraphデータ):
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/f01b474e-9e71-5eaf-bf48-86d6564d5124.png)

エッジテーブル (悪意のあるgraphデータ):
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/c272b97c-b09b-f3d0-1780-b05e300c87f1.png)

この表に対応するBenignグラフデータは同一のスキーマを持っています。
##グラフファイルをOracle Databaseに保存する
現在我々は作成したグラフファイルをオラクルデータベース内に保存しました。この操作はoml.create()コマンドで、データベース内に新たな表を作るような形で実現できます。
この関数は、新規作成される表の表名と、その新規の表に格納予定のデータを格納しているpandas DataFrameを入力として受け取ります。

このファンクションはいわゆる基本的にデータベース内のテーブルのポインタとなるプロキシオブジェクトを返します。このプロキシオブジェクトはデータベース内のデータにアクセスしたり操作したりするために使うことが可能です。

```Python
# create the benign table
benign_vertices = oml.create(df_first_benign, table = 'BENIGN_VERTICES')
benign_edges = oml.create(df_second_benign, table = 'BENIGN_EDGES')
  
# create the malware table
malware_vertices = oml.create(df_first_malware, table = 'MALWARE_VERTICES')
malware_edges = oml.create(df_second_malware, table = 'MALWARE_EDGES')
```

この上記コードを実行すると、4つのプロキシオブジェクトを作成することが可能です( benign_vertices, benign_edges, malware_vertices, malware_edges)。

##PGXでグラフファイルを読み込む
このステップでは、PGXサーバー内で前のステップで作成したデータを元にグラフを作成します。そのためにまずはそれぞれ二つのグラフの設定をしていきます。グラフの設定はPGXサーバにグラフをロードするために重要な情報を伝えます。このグラフの設定は単純なPythonのディクショナリオブジェクトとして表現することが可能です。

```Python
config_malware = {
      
    'jdbc_url': 'jdbc:oracle:thin:@'+data_source_name,
    'format': 'two_tables',
    'datastore': 'rdbms',
      
    'username': 'test_user',
    'keystore_alias': 'database_keystore',
      
    'vertex_id_type': 'string',
      
    'nodes_table_name': 'MALWARE_VERTICES',
    'edges_table_name': 'MALWARE_EDGES',
      
    'nodes_key_column' : 'V_ID',
    'edges_key_column' : 'E_ID',
    'from_nid_column' : 'SRC_ID',
    'to_nid_column' : 'DST_ID',
      
    'vertex_props': [
        {'name': 'V_LABEL', 'type': 'string'},
        {'name': 'TYPE', 'type': 'string'},
        {'name': 'GRAPH_ID', 'type': 'long'}
    ],
    'edge_props' : [
        {'name': 'SRC_TYPE', 'type': 'string'},
        {'name': 'DST_TYPE', 'type': 'string'}
    ]
}
```
この設定はグラフファイルがどのように保存されるかの情報、対応するデータベースにアクセスするためのクルデンシャル、表のスキーマの情報が含まれています。PGXサーバーがオラクルデータベースにアクセスするために、まずは test_user がデータベースにアクセスできるようにキーストアにパスワードを持たせるように登録しましょう。

```Python
# Register the keystore
session.registerKeystore("/path/to/keystore/keystore_test.p12", "test_password")
```

これが終了したら、グラフの作成は簡単です。 read_graph_with_properties()関数を使用して、グラフの設定をしていきます。

```Python
# Read in the graphs
graphlets_b = session.read_graph_with_properties(config_benign)
graphlets_m = session.read_graph_with_properties(config_malware)
```

read_graph_with_properties() 関数はプロキシオブジェクトをPGX内の対応するグラフに返します。このオブジェクトはこれでグラフのアクセスや操作が可能になりました。

##featured tableを使う
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/a68cfcb3-f759-dc22-d9db-fdc27a422077.png)

今までにやったことをいったんおさらいしましょう。
はじめに、パケットキャプチャした長いリスト上の生データをフェッチしました。その次に2つの (graphlets_b , graphlets_m) を作成し、どちらもたくさんのサブグラフを持つ別々のグラフとして作成しました。サブグラフはどれもパケットのキャプチャ一つ一つを表しています。

次のステップとして、それぞれの細かいグラフレットを分析し、特徴量を集めたひとつの大きな表に格納します。それぞれのグラフレットでは、特徴量のテーブルに一つの行を保存します。その次にラベルをそれぞれの行に追加して、どの行がどのパケットの始まりや悪質なものか等のパケットキャプチャの情報を持っているのかを示すようにします。

#特徴量の生成から分類まで
##グラフレット機能を作成する
PGX内に二つのグラフを作成したため、色々な種類のグラフアルゴリズムを用いて分析していきましょう。後程結果の情報を使用して、別の分類器もトレーニングします。それぞれのグラフでは、繰り返し処理をして、次の機能を利用して分析します。

```Python  
# A list which will store the generated feature information
feature_strings_malware = []
# Iterate over each of the packet captures inside the graph
for val in range(1,amount_of_malware_graphs+1,1):
    # Reduce the graph to the subgraph of this packet capture
    graphlet = graphlets_m.filter(VertexFilter("vertex.GRAPH_ID = "+str(val)))
      
    # Generate the feature information for this packet capture
    features = generate_graphlet_features(graphlet)
      
    # Add the feature information to our list. The '1' represents the label
    # of this packet capture. It denotes that this is a malicious packet capture
    feature_string = [1] + features
    feature_strings_malware.append(feature_string)
```


generate_graphlet_features() の関数は単一のグラフレットを入力として受け取り、PGXの提供する豊富なグラフアルゴリズムのセットを利用して、グラフレットのプロパティの数を計算します。

単一のグラフレットの特徴リストを取得したら、特徴量リスト(feature_strings_malware)に追加します。これをする前に、それぞれの特徴量リストにバイナリーラベルを追加します。これはそのリストが悪意のあるもの(label =1) もしくは良いもの(label = 0) のパケットキャプチャに属しているのかを示します。

```Python
def generate_graphlet_features(graphlet):
    features = []
    features.append(graphlet.num_vertices)
    features.append(graphlet.num_edges)
    features.append(get_average_property(analyst.degree_centrality(graphlet)))
    features.append(get_average_property(analyst.local_clustering_coefficient(graphlet)))
    features.append(get_average_property(analyst.out_degree_centrality(graphlet)))
    features.append(get_average_property(analyst.in_degree_centrality(graphlet)))
    features.append(get_average_property(analyst.vertex_betweenness_centrality(graphlet)))
    features.append(get_average_property(analyst.pagerank(graphlet)))
    features.append(get_average_property(analyst.eigenvector_centrality(graphlet,max_iter=1000,tol=0.00001, l2_norm=True, in_edges=True)))
    features.append(get_sum_property(analyst.degree_centrality(graphlet)))
    return features
```

get_average_property() と get_sum_property()はただのヘルパー関数で、プロパティのリストの平均や合計を示すものです。

```Python
def get_average_property(property):
    numerator = 0
    count = 0
    for vertex, val in property.get_values():
        numerator += val
        count += 1
    return numerator / count
  
def get_sum_property(property):
    total_sum = 0
    for vertex, val in property.get_values():
        total_sum += val
    return total_sum
```


### 分類用にデータを準備します

#### OML分類器を利用してデータをトレーニングする

今まですべてのステップを二回ずつ行いました。一回目は良性のパケットキャプチャを格納するグラフで、二回目は悪性のパケットキャプチャを格納する物です。
それぞれのリストにパケットキャプチャがいいものか(label = 0)悪いものなのか(label = 1)ラベルを追加したので、最後にそれらを合わせて二つの特徴リストを一つの大きな特徴量を示すテーブルに結合させることができます。


```Python
# Combine the two dataframes
feature_strings = feature_strings_malware.append(feature_strings_benign)
  
# Shuffling the dataframe
feature_strings = feature_strings.sample(frac=1, random_state=0)
feature_strings = feature_strings.reset_index(drop=True)
  
# Renaming the label column to 'label'
feature_strings.rename(columns = {'0':'label'}, inplace = True)
```

これで分類器をトレーニングするための最終的なテーブルを作成できました。テーブルは以下のようになっているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/f10baccc-5974-7cee-48c1-da256259c930.png)

これで我々はオラクルデータベース内にこのテーブルを格納し、データベース内で直接マシーンラーニングを実行できます。
	
```Python
# Store the features in the database
data_oml = oml.create(feature_strings, table = 'data_complete')
```

最後に、このデータの分類器のトレーニングとテストのために、データをトレーニングセットとテストセットに分ける必要があります。OMLのsplit()関数によって、データベース内の表を分割し、二つのプロキシオブジェクトを返します。なお、この処理においてプロキシオブジェクトを使用するため、データベースからのデータのフェッチは不要となっています。


```Python
# Split the data
train_data_oml, test_data_oml = data_oml.split(ratio=(.8, .2), seed=0)
  
# Create the train data
train_x_oml = train_data_oml.drop('label')
train_y_oml = train_data_oml['label']
  
# Create the test data
test_x_oml = test_data_oml.drop('label')
test_y_oml = test_data_oml['label']
```

#### OMLを使った最初の分類
データ分類のための準備が終わったら、分類器のトレーニングをしていきましょう。
まずはニューラルネットワークから始めましょう。OML4Pyはoml.nn()コンストラクターを介してニューラルネットワークを作成することが可能です。下記コードでは、ノード数とアクティベーション関数が異なる4つのレイヤーを作成しました。

```Python
# Create an oml Neural Network model object.
params = {
    # The architecture of the neural network
    'nnet_hidden_layers' : 4,
    'nnet_nodes_per_layer' : '50, 30, 50, 30',
  
    # The differnt activation functions in each layer
    'nnet_activations' : "'NNET_ACTIVATIONS_LINEAR','NNET_ACTIVATIONS_LINEAR','NNET_ACTIVATIONS_LINEAR','NNET_ACTIVATIONS_LINEAR'",
  
    # Info about the number of rounds to use for fitting
    'nnet_iterations' : 500,
    'nnet_heldaside_max_fail' : 100,
  
    # A seed for reproducibility
    'odms_random_seed' : 1
}
nn_mod = oml.nn(**params)

```
ニューラルネットワークのトレーニングとスコアリングもとても簡単です。OML4Pyはfit()とscore() という二つの関数を提供しており、どちらも引数にOMLのプロキシオブジェクトとしても利用することが可能になっています。これらのプロキシオブジェクトは関数にどのデータをトレーニングやテストに利用すればいいのかを示すはたらきがあります。

```Python
# Fit the NN model according to the training data and parameter settings.
nn_mod = nn_mod.fit(train_x_oml, train_y_oml)
  
# Score the model
accuracy = nn_mod.score(test_x_oml, test_y_oml)
  
print(accuracy)
```
我々のデータでモデルをトレーニングし、スコアリングをしたところ、87.5％の精度となりました。より良くするために何ができるでしょうか。

#### OML AutoML: あなたのためにはたらくOML
OMLはニューラルネットワークから線形分類器や単純ベイズ分類器まで、様々な分類器を提供します。しかしながら、どの分類のアルゴリズムが一番良い精度で予測できるかを判断するのは、それぞれのアルゴリズムが大量のハイパーパラメータを持つため、大変な作業になります。
しかし、OMLはこの問題に対処するためのAutoMLというソリューションを持っています。AutoML モデルは数多くの関数を提供し、OMLは最適なモデル/特徴量/ハイパーパラメータを提供してくれます。
それでは自動選択アルゴリズムをデータに適用して、どのモデルが一番いい結果をもたらすか、探してみましょう。

```Python
# Create an algorithm selection object
alg_selection = oml.automl.AlgorithmSelection(mining_function='classification', score_metric='accuracy',
                                             parallel=1)
  
# Find the 3 classifiers which provide the best accuracy
algo_ranking = alg_selection.select(train_x_oml, train_y_oml, cv=None,
                                    X_valid=test_x_oml, y_valid=test_y_oml, k=3)
  
# Output these classifiers
print("The best models are: " + str(algo_ranking))
```
上記コードを実行することで、下記のランキングを返してくれます。
1.	決定木 (Accuracy: ~ 99.6%)
2.	一般化線形モデル (Accuracy: ~ 99.6%)
3.	ランダムフォレスト (Accuracy: ~ 99.5%)
予測がどれだけ正確か確認していきましょう。

#### 決定木の分類器をトレーニング
決定木の分類器を作成する文法はシンプルです。ニューラルネットワークの例のように、Python dictionaryでパラメータを指定します。このディクショナリーは次にoml.dt() という決定木の分類器のためのコンストラクタ関数に渡すことが可能です。

```Python
# Specify settings.
setting = {
    # Maximum tree depth
    'TREE_TERM_MAX_DEPTH':'7',
      
    # Maximum number of bins for each attribute
    'CLAS_MAX_SUP_BINS':'32',
      
    # Minimum number of rows in a node
    'TREE_TERM_MINREC_NODE':'10'
}
  
# Create an oml DT model object.
dt_mod = oml.dt(**setting)
```

決定木の分類器でもニューラルネットワークで利用した、トレーニングとテストのために使用したものと同じファンクション(fit() and score())を利用可能です。

```Python
# Fit the DT model according to the training data and parameter settings.
dt_mod.fit(train_x_oml, train_y_oml)
  
# Score the model
accuracy = dt_mod.score(test_x_oml, test_y_oml)
  
print(accuracy)
```

そして実際に、このコードを実行すると、99.6%もの正確性を実現させることができました。

#### 埋め込み型のPython実行: カスタムされた分類器をあなたのニーズに合わせて構築

OML分類器や回帰モデルが提供する様々なパラメータがありますが、それぞれのモデルが作成可能なパラメータに制限はあります。もし適切なパラメータが存在しないとても専門的な分類器を作成しなければならない時にはどうすればよいでしょうか。OMLはこのような場合の埋め込み型Python実行のソリューションを提供しています。この機能はどのようなPytonのコードも作成可能で、オラクルデータベースにコードを送信して、指定したデータに対して実行することが可能です。function table_apply()ファンクションを使った例を見ていきましょう。

まずは、自分が保持しているデータベース内のデータで実行するためのシンプルなファンクションを記載します。この例では、Python マシーンラーニングライブラリの[scikit-learn](https://scikit-learn.org/stable/index.html)を用いた勾配ブースティングを使用していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/8b763570-3e57-07ad-8ce5-a0ca433f8b8f.png)
そして、以下のコードをかきのファンクションを用いる簡単な操作で、データベース内のデータ表に対して実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/6f614709-9895-8805-a6bb-fe31d5aae1b3.png)
table_apply_func_sklearn_booster()ファンクション内で返されたオブジェクトはローカルのPythonセッションにプロキシオブジェクト(オラクルデータベース内に存在する実際のオブジェクトへのポインター)として戻されます。実在するオブジェクトをローカルセッションにフェッチしたい場合、プロキシオブジェクト上でpull() を実行しましょう。

#### PythonオブジェクトをOracle Database内に保存する

もし将来、トレーニングした分類機をまた利用したい場合はどうすれば良いでしょうか。ローカルファイルシステムに保存することもできますが、あまりセキュアな方法ではありません。他の方法として、バイナリーラージオブジェクト(BLOBs)としてOracle Database内に保存する方法もあります。このために、oml.ds.save()ファンクションを利用してデータストアを活用することができます。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/22d9a50d-3c1c-f405-5216-f706ffa8b587.png)
これでモデルを消してもデータベース内に保存されるため、心配はいりません。Pythonのセッションに作成したモデルを再読み込みしたい場合は、oml.ds.load() ファンクションを使用しましょう。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/706384/1764df4a-b068-904a-3d5a-f2cc0b3078ec.png)
これはモデルの変数を、現在のPythonセッションのグローバルネームスペースに返します。モデルは保存するときに決定した名前でアクセスが可能です。今回の例ではモデル名はgbc_modelとなっています。

### Graph マシンラーニングの特徴

この記事では、グラフベースの特徴で一般的なマシンラーニングをブーストできるのかを確認しました。PGXは、vertex representation(DeepWalk, Graph Convolutional Networks)やsub-graph representations (PG2Vec)にような、さらに複雑なグラフマシンラーニングの機能群を提供しています。