R言語 - 3 次元グラフを利用して類似度を直感的に把握する

# はじめに
　皆さんは、いくつかのドキュメントが似ているかどうかの判定に困ったご経験はありませんか。たとえば、FAQ 作成を思い浮かべて下さい。FAQ を効率よく作るには、よくある問い合わせへの回答を事前にまとめると思います。実際にやってみると、これは骨の折れる作業で、過去のお客様や社内からの問い合わせデータを参考にして、類似性のあるものを含めた頻度の高い問い合わせを特定する、といった方法になります。このような場合、R を使うと、ドキュメントの類似度を直感的に把握することができます。なお、Oracle R Enterprise（以下、ORE）の利用は、Oracle Database Enterprise Edition のオプションであるOracle Advanced Analytics が必要です。
![image](https://qiita-image-store.s3.amazonaws.com/0/106773/557ce3ce-8f8c-0948-8e32-40917bcc6f76.png)


# データベースにある過去の問い合わせ内容を参照する
　データベースに接続し、過去の問い合わせ内容を格納した token_freq 表を参照するファンクション「getToken_freq」を作成して実行します。 前提として、CONNECT, RESOURCE, CREATE ANY DIRECTORY, CTXAPP, RQADMIN の権限を付与したアカウント「ユーザー名：user01、パスワード：Oracle100」を使用しています。

▼サンプルスクリプト


```R:R
# パスを指定し、ORE パッケージ(注1)を読み込みます。
.libPaths("<R環境のパッケージ格納先のパスを指定>") 
library( ORE )                                      
# データベースへの接続記述子を設定します。ここでは、ユーザー「user01」、パスワード「Oracle100」を使用してデータベースに接続します。
if ( !ore.is.connected() ) {
  ore.connect( user = "user01", sid = "orcl", host = "localhost", password = "Oracle100", port = 1521, all = TRUE )
}
# 過去の問い合わせ内容を格納したtoken_freq表を参照するファンクション 「getToken_freq」を作成します
getTokenFreq <- function() {
  con <- dbConnect( Extproc() )
  rs  <- dbSendQuery( con, "SELECT token, NVL2(q1,q1,'0') q1, NVL2(q2, q2, '0') q2,
                            NVL2(q3, q3, '0') q3, NVL2(q4, q4, '0') q4, NVL2(q5, q5, '0') q5,
                            NVL2(q6, q6, '0') q6, NVL2(q7, q7, '0') q7, NVL2(q8, q8, '0') q8,
                            NVL2(q9, q9, '0') q9, NVL2(q10, q10, '0') q10
                            FROM   (SELECT query_id, token, COUNT(offset) freq FROM token_freq GROUP BY query_id, token)
                                    PIVOT(SUM(freq) FOR query_id IN(1 AS q1, 2 AS q2, 3 AS q3, 4 AS q4, 5 AS q5, 6 AS q6,
                                          7 AS q7, 8 AS q8, 9 AS q9, 10 AS q10))" )
  df  <- fetch( rs )
}
# 過去の問い合わせ内容を格納したtoken_freq表を参照するファンクション「getTokenFreq」を実行します
token_freq <- ore.doEval( getTokenFreq, ore.connect=TRUE )
# ファンクション「getTokenFreq」の実行結果をデータフレームに格納します
df_tf <- as( ore.pull( token_freq ), "data.frame" )
```
　サンプルスクリプト内のファンクション「getToken_freq」で実行しているのSQL 文は、token_freq 表の query_id 列をピボット列として展開してトークン毎の頻出度を集計しています（下図 を参照）。ご参考までに、過去の問い合わせ内容をデータベースに格納するサンプルスクリプトを Tips下部で解説します。
![image](https://qiita-image-store.s3.amazonaws.com/0/106773/a88c1d1a-bbf8-26ca-39d8-745765c9d877.png)


# 問い合わせ内容の統計解析を実行する
　ドキュメント同士が似ているのか似ていないのか、これを距離として測ることで類似度を求めます。R では、この距離の計算に dist() 関数が用意されていて、簡単に求めることができます。デフォルトの測定アルゴリズムは euclidean（ユークリッド距離）です。

▼サンプルスクリプト

```R:R
# dist関数で、距離行列(全ドキュメント同士の距離)を求めます
df_tf_dist <- dist( t( df_tf[,2:11] ))
```
▼実行結果例
サンプルスクリプト内の「df_tf_dist」オブジェクトを出力したものは、次の通りです。
![image](https://qiita-image-store.s3.amazonaws.com/0/106773/db71d233-7069-88b4-cdcb-c9a240287ade.png)

# 直感的に把握できるように、三次元描画する
2次元のデータを 3 次元のデータへ変換させる場合、古典的多次元尺度法を使います。R では、この指定した次元への変換に cmdscale() 関数が用意されていて、簡単に求めることができます。

▼サンプルスクリプト

```R:R
# 次元数の最大値として「3」を指定します
df_tf_dist_3d <- cmdscale(df_tf_dist, 3)
```


▼実行結果例
サンプルスクリプト内の「df_tf_dist_3d」オブジェクトを出力したものは、次の通りです。
![image](https://qiita-image-store.s3.amazonaws.com/0/106773/af60f3c5-c60b-d706-32ea-59f0758ed502.png)

▼サンプルスクリプト
最後は、3 次元プロットのパッケージである「rgl」を使って描画します。
  

```R:R
# パッケージ「rgl(注2)」を読み込みます
library(rgl)
# x値を設定します                       
x <- df_tf_dist_3d[,1]
# y値を設定します
y <- df_tf_dist_3d[,2]
# z値を設定します
z <- df_tf_dist_3d[,3]
# 三次元プロットします (type="s"は球体を指定しています)
plot3d(x, y, z, type="s", col=rainbow(10), size=4)
# グラフのラベルを表示します
text3d(x, y, z, text=colnames(df_tf[2:11]),  adj=2, justify="left")
```


　3 次元プロットの結果は次の通りです。グラフ内の球体が問い合わせ内容のドキュメントの 1 つ 1 つを表しています。空間内に配置された球体の中で、近い場所に配置されているものは、ドキュメントの類似性が高いと判断します。図では、問い合わせ内容の Q1 と Q5、それから Q4 と Q10 が近い場所に配置されています。このように 3 次元プロットにすることで、類似度の高さを直感的に把握することが可能になります。

![image](https://qiita-image-store.s3.amazonaws.com/0/106773/888a8d6d-9547-6030-2e18-42a2ddaaf59c.png)

　また、「rgl」パッケージで 3 次元プロットしたグラフは、マウス操作で軸を回転させることができます。Tipsの関係上、動作をご覧いただくことができずに残念ですが、軸を回転させることにより、様々な視点から類似性を把握することができるようになります。たとえば、図の視点では Q7 が Q4 と Q10 に近い場所に見えるという新たな発見がありました。
![image](https://qiita-image-store.s3.amazonaws.com/0/106773/1bf8b28c-051b-b4fc-9dfb-bfe19cc15ec2.png)

# ご参考
過去の問い合わせ内容をデータベースに格納するサンプルスクリプト
この Tips ではすでにデータベースに格納された問い合わせ内容を分析する方法を紹介しましたが、事前に過去の問い合わせ内容を日本語形態素解析して、トークン毎の頻出度を集計するために、次のような処理を実施しています。
![image](https://qiita-image-store.s3.amazonaws.com/0/106773/e97405a1-7916-7e0d-846b-4f5cee86174e.png)

1. テーブルにデータ格納済みの状態で、コンテキストインデックスを作成する。
   事前に、「text_data」表に「過去のお問い合わせ内容」のデータを格納しておいた状態で、日本語形態素解析用のコンテキストインデックスを作成します。
▼サンプルスクリプト
-- 日本語形態素解析用のコンテキストインデックスを作成します
EXECUTE CTX_DDL.CREATE_PREFERENCE('test_lexer','JAPANESE_LEXER');
CREATE INDEX ctx_index ON text_data (contents) INDEXTYPE IS CTXSYS.CONTEXT PARAMETERS ('LEXER test_lexer');

2. トークン頻出度表を作成する
▼サンプルスクリプト
-- トークン毎の頻出度を集計します
CREATE TABLE token_freq (query_id NUMBER, token VARCHAR2(64), offset NUMBER, 
length NUMBER);
DECLARE cnt NUMBER;
BEGIN
  SELECT COUNT(text_id) INTO cnt FROM text_data;
  FOR i IN 1..cnt LOOP
    CTX_DOC.TOKENS('ctx_index', i, 'token_freq', i);
  END LOOP;
END;
/
CTX_DOC.TOKENS パッケージプロシージャは、コンテキストインデックスのトークン情報を参照できます。詳細な使用方法については、マニュアル(注3)をご参照下さい。

# 注釈
注 1  サンプルスクリプトは、Oracle R Enterprise 1.3.1 を使用しています。
注 2  サンプルスクリプトは、rgl 0.95.1201 を使用しています。
注 3  Oracle Text Reference 11g Release 2 (11.2) E24436-04 8 CTX_DOC Package
      http://docs.oracle.com/cd/E11882_01/text.112/e24436/cdocpkg.htm#CCREF0700
      Oracle Text Reference 12c Release 1 (12.1) E41399-05 9 CTX_DOC Package
      http://docs.oracle.com/database/121/CCREF/cdocpkg.htm#CCREF0700