行列演算 API(ND4J)の簡単整理

Javaディープラーニングライブラリー「Deeplearning 4 j(以下、DL4J)」で行列の演算を行う方法を整理します。

今まで数冊のディープラーニングや数学の本を読んで
ディープラーニングについて少しは分かった気はしますが、
数学が苦手なので、DL4で行列に関する引数及び戻り値の解析に難がありました。
なんというか、行列に関するAPIを呼び出した時、紙に書いてみないと、
頭の中に結果行列の姿がパット思い浮かべないです。

そこでここに行列演算のAPIについて書いて自分の理解を深めると同時に参考ドキュメントにしたいと思います。

DL4Jを使う時、行列の演算を行う時、依存ライブラリーとして、ND4Jという多次元配列のライブラリーのAPIを使用します。
以下はその説明です。
CSVやDBからDL4Jが自動で行列を作ってくれるので、以下のAPIを使うのはまれだと思いますが、
基礎となる部分ですので、理解して損はないと思います。

ND4Jのメソッドの結果はINDArrayで、多次元配列です。内部はC++で実装されているようです。

行列の作成

説明

INDArray nd = Nd4j.create(new float[]{1, 2, 3, 4, 5, 6, 7, 8}, new int[]{ 4, 2 });
/*
結果:
[[1.00, 2.00],
 [3.00, 4.00],
 [5.00, 6.00],
 [7.00, 8.00]]
*/

2番目の引数に行列を格納する多次元配列の形(shape)を指定します。
数字の数が次元の数(ここでは2つ)を表しており、要素は各次元の長さ(1次元 : 4つ, 2次元 : 2つ)です。
まず、長さ4の配列が作られ、それぞれの要素に長さ2の配列が割り当てられます。
2次元なので、まさに行・列ですね。

1番目の引数に指定された配列の要素が最後の次元(2次元)の長さずつ(ここでは2つずつ)、順に配列を埋めていきます。
ここではまず、[1, 2]と埋めて、次は[ 3, 4]、[5, 6]、[7, 8]と埋めていき、4x2の多次元配列、つまり、行列が完成します。

1番目の引数に指定される要素の数は2番目の引数に指定される要素同士の掛け算の結果と同じでなければなりません。
ここでは4 * 2 = 8ですから、8つの要素が必要です。

練習問題

以下の結果は何でしょうか。次元の数は3つです。答えは最後にあります。

INDArray nd = Nd4j.create(new float[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, new int[]{ 3, 2, 2 });

前置行列(Transpose)

説明

縦の要素と横の要素を入れ替えた行列です。行列を表すINDArrayオブジェクトに対してtranpose()メソッドを呼び出すことで前置行列を求められます。

INDArray nd = Nd4j.create(new float[]{1, 2, 3, 4, 5, 6, 7, 8}, new int[]{ 4, 2 });
/*
結果1:
[[1.00, 2.00],
 [3.00, 4.00],
 [5.00, 6.00],
 [7.00, 8.00]]
*/
INDArray tnd = nd.transpose();
/*
結果2:  結果1の縦と横が変わりました。
[[1.00, 3.00, 5.00, 7.00],
 [2.00, 4.00, 6.00, 8.00]]
*/

変形(Reshape)

説明

元の行列の形(行と列の数)を変えます。例えば、4x2行列を2x4行列に変えるなどです。
注意すべきところは変形前後の行と列の掛け算の結果が同じでなければならないということです。
例えば、4x2行列は3x3行列に変えることはできません。掛け算の結果(8 != 9)が違うためです。

INDArray nd = Nd4j.create(new float[]{1, 2, 3, 4, 5, 6, 7, 8}, new int[]{ 4, 2 });
/*
結果1:
[[1.00, 2.00],
 [3.00, 4.00],
 [5.00, 6.00],
 [7.00, 8.00]]
*/

INDArray rnd = nd.reshape(2, 4); // rows:2 , columns: 4
/*
結果2:
[[1.00, 2.00, 3.00, 4.00],
 [5.00, 6.00, 7.00, 8.00]]
*/

hstack(水平積み上げ)

引数に指定された複数のINDArrayを水平に埋めた行列を返します。
サンプルを見た方が早いでしょう。

INDArray nd1 = Nd4j.create(new float[]{1, 2, 3, 4}, new int[]{ 2, 2 });
/*
結果1:
[[1.00, 2.00],
 [3.00, 4.00]]
*/
INDArray nd2 = Nd4j.create(new float[]{5, 6, 7, 8}, new int[]{ 2, 2 });
/*
結果2:
[[5.00, 6.00],
 [7.00, 8.00]]
*/
INDArray hstack = Nd4j.hstack(nd1, nd2);
/*
結果3:
[[1.00, 2.00, 5.00, 6.00],
 [3.00, 4.00, 7.00, 8.00]]
結果1の行列と結果2の行列が水平に埋められた結果になりました。
*/

hstackの引数は可変長なので、複数のINDArrayが指定できます。

linspace(行ベクトル作成)

lower(下限値)、upper(上限値)、num(長さ)をそれぞれ順にlinspaceメソッドに指定して実行すると、
lowerからupperまでの範囲からnumだけの数を要素として持つX行1列の行列(行ベクトル)が返されます。
要素はnumに合わせてlower~upperの範囲を一定の間隔(つまり、一定のstepで)で分割し、求められます。
例えば、lower: 1, upper: 10, num: 3なら、3個の要素を持つ行列になりますが、
最初は1、2番目は真ん中の5.5, 3番目は10になります。

以下にサンプルを示します。

INDArray nd1 = Nd4j.linspace(1, 10, 3);
/*
結果1:
[1.00, 5.50, 10.00]
*/

INDArray nd2 = Nd4j.linspace(1, 10, 10);
/*
結果2:
[1.00, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00, 9.00, 10.00]
*/

INDArray nd3 = Nd4j.linspace(1, 10, 20);
/*
結果3:
[1.00, 1.47, 1.95, 2.42, 2.89, 3.37, 3.84, 4.32, 4.79, 5.26, 5.74, 6.21, 6.68, 7.16, 7.63, 8.11, 8.58, 9.05, 9.53, 10.00]
*/

今日はここまでです。思い出したら、vstack, diag, zeroも書きます。
恥ずかしい水準ではありますが、やはり書きながら、理解度が上がる気がします。

練習問題の解答

行列の作成

[[[1.00, 2.00],
  [3.00, 4.00]],

 [[5.00, 6.00],
  [7.00, 8.00]],

 [[9.00, 10.00],
  [11.00, 12.00]]]

2*2の行列が3つ生成された形です。

メソッド呼出時、引数にJSON(マップ)を渡せます。

Javaのテンプレートエンジン Velocityでメソッド呼出時、引数にJSONを渡せます。
色々いじってみて知りました。
方法は引数のところに{ "key1": "value1"....のように普段のJSONと同じく書けばいいです。
実際メソッドの引数に渡されるオブジェクトはMapです。
以下のサンプルを参照して下さい。

import java.io.StringWriter;
import java.util.Map;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.context.Context;

public class TestVelocityJson {
    // テンプレートで使うBeanクラス。
    public static class TestBean {
        public void duke(Map<String, Object> t) {
            // 適切にキャストして使用します。
            String param1 = (String) t.get("param1");
            int param2 = (int) t.get("param2");
            System.out.println("param1: " + param1);
            System.out.println("param2: " + param2);
        }
    }

    public static void main(String[] args) throws Exception {
        {
            // メソッド呼出。
            String template =
                "${testBean.duke({\"param1\" : \"v1\", \"param2\": 11})} ";
            Context vc = new VelocityContext();
            vc.put("testBean", new TestBean());
            StringWriter sw = new StringWriter();
            System.out.println("-- Test1 -- ");
            Velocity.evaluate(vc, sw, "test", template);
            /*
             * 結果
             * -- Test1 --
             * param1: v1
             * param2: 11
             */
        }
        {
            // マクロからメソッド呼出。
            String template =
                "#macro(dukeTest $p) " +
                "  $testBean.duke($p) " +
                "#end " +
                "#dukeTest({\"param1\" : \"v2\", \"param2\": 22})";

            Context vc = new VelocityContext();
            vc.put("testBean", new TestBean());
            StringWriter sw = new StringWriter();
            System.out.println("-- Test2 -- ");
            Velocity.evaluate(vc, sw, "test", template);
            /*
             * 結果
             * -- Test2 --
             * param1: v2
             * param2: 22
             */
        }
    }
}

違うユーザのリモートリポジトリをPull・Push時、エラー対処

状況

まず、Eclipseのバージョンは古い(Mars, 2015年版)です。
EclipseのPackage Explorer上に複数のプロジェクトがあり、
それぞれのプロジェクトが属するGitHubのリモートリポジトリのGitHubユーザが違う場合、
(個人用と会社用を分けて使うなど)
PullやPushなどのリモート操作時、以下のエラー(Invalid remote: origin)が発生します。
f:id:jeongman7:20170701184341j:plain

原因

対処方法からの推測ですが、全てのプロジェクトが属するリモートリポジトリのGitHubユーザ情報が
最後に設定したリモートリポジトリのGitHubユーザ情報に上書きされるからだと思います。
今のところ、それぞれのプロジェクトに違うGitHubユーザ情報を設定できる方法はなさそうです。
最新バージョンのEclipseなら、可能かも知れないですね。

対処方法

不便ですが、今のところ、これが最善です。
Git Repositoriesビュー > 作業対象のプロジェクトが属するリポジトリ > Remotes > originを選択し、
右ボタン > Configure Push > Changeすると、Destination Git Repositoryダイアログが表示されます。
ここでAuthenticationにあるUser・Passwordに該当GitHubユーザ情報を入力し、
Store in Secure Storeにチェックし、Finish > Saveします(以下の画像)。
f:id:jeongman7:20170701185718j:plain
以上でPush・Pullする時、エラーは出なくなります。
但し、作業対象のプロジェクトが変わるたびに行う必要があります。
近いうちにEclipseのバージョンを変えてやってみます。

awkについて簡単整理

Linuxのテキスト処理ツールであるawkについて簡単に整理します。
awkはポイントさえ掴めば分かりやすいと思います。

awkは何者?

テキストデータから行を読み込み、文字列操作を行うLinuxのツールです。
特定条件にマッチする行から列を抽出し、ある処理を実行するといったバッチ処理に向いています。
個人的にはsedより文法(C like)が分かりやすい印象です。
ただ、簡単な処理の場合、sedより記述量が多くなるので、場合によって使い分けた方がいいでしょう。

コマンド

# テストファイル生成
echo a 1 >> test.txt
echo b 2 >> test.txt

# awk '処理内容' テキストファイル
awk '{ print $0 }' test.txt
# 結果)
# a 1
# b 2

# テキストファイルの代わりにパイプで入力
cat test.txt | awk '{ print $1 }'
# 結果)
# a
# b

# 処理内容をファイル(awk)に入れて実行。
echo '
    /a/{
        print "bingo! -> ", $0
    }
' > test.awk
awk -f test.awk test.txt
# 結果)
# bingo! ->  a 1

プログラム構造

# テキスト行を読み込む前に一回のみ、実行されるブロック。初期化などを記述します。
BEGIN {
    # コメントです。
    # ... 処理内容 ...
}

# パターンに一致する行ごとに実行されるブロック。
/パターン/ {
    # ... 処理内容 ...

    # 変数はブロック内で宣言なしで使用できる。
    # グローバルスコープなので、前の行で使用した変数を次の行でも参照・更新できる。
    cnt = cnt + 1
    
    # for, whileなどの繰り返し文も使用可能です。
    for (i = 0; i < NF; i++) {
        ...
    }
}

# 論理条件(例 : a == 1 || NF == 3)に一致する行ごとに実行されるブロック。
論理条件 {
    # ... 処理内容 ...
}

# 全ての行ごとに実行されるブロック。
{ 
    # ... 処理内容 ...
}

# 最後の行の処理が終わって一回のみ、実行されるブロック。
END {
    # ... 処理内容 ...
}

行内変数

  • $0 : 現在の行の文字列
  • $1~$x : 現在の行でx番目の列の文字列

他にも色々あります。参考のリンクを参照して下さい。

組込変数

  • NR : 現在の行の番号
  • NF : 現在の行の列数
  • FS : 入力時の区切り文字。デフォルトは半角スペース。BEGINブロックでFS = "¥t"のように設定できる。
  • OFS : 出力時の区切り文字。デフォルトは半角スペース。BEGINブロックでOFS = "¥t"のように設定できる。

他にも色々あります。参考のリンクを参照して下さい。

組込コマンド

  • print : 文字列を出力する
# 1番目の列を出力する。
cat test.txt | awk '{ print $1 }'
# 結果: 
# a
# b

# 1番目の列と2番目の列をOFS(出力時の区切り文字)で区切って出力する。
cat test.txt | awk '{ print $1, $2 }'
# 結果: 
# a 1
# b 2

# 1番目の列と2番目の列を/で区切って出力する。
cat test.txt | awk '{ print $1 "/" $2 }'
# 結果: 
# a/1
# b/2

他にも色々なコマンドがあります。参考のリンクを参照して下さい。

サンプル

キューにたまっているメールのうち、宛先メールアドレスのドメインがtest.testに該当するメールのキューID及びメールアドレスを抽出するサンプル。

# 以下はmailqコマンドで出力したキュー内容の例)
# メール一件当たり4行です。
# -Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------          <--- ヘッダー
# ABCDEF1234     7681 Fri Jun 23 18:59:27  hogehoge@test.test                <--- x行目
#           (connect to dukelab.test[192.168.0.15]:25: Connection refused)   <--- x+1行目
#                                         darekasan@test.test                <--- x+2行目
#                                                                            <--- x+3行目
# ... 省略 ...
# x行目の1列目がキューID(ABCDEF1234)です。
# x+2行目の1列目が宛先メールアドレス(darekasan@test.test)です。
# x+3行目はただの改行です。

echo '
# x行目。
# NF == 7(x行目は7つの列で構成)のように列数で判断するのもいいでしょう。
NR % 4 == 2 {
    # x行目なら、キューIDを抽出する。
    queue_id = $1
}
# x+2行目に宛先メールアドレスがある。
# NF == 1(x+2行目は1つの列で構成)のように列数で判断するのもいいでしょう。
NR % 4 == 0 && /@test.test/{
    # キューID メールアドレスを出力
    print queue_id OFS $1
}' > extract_qids.awk
mailq | awk -f extract_qids.awk
# 以下のように変数に入れてfor文で回したら、バッチ処理も可能でしょう。
# queue_ids=`mailq | awk -f extract_qids.awk`
rm extract_qids.awk

# 結果例
# ABCDEF1234 darekasan@test.test
# ... 省略 ...

稼働中のサーブレットインスタンスを取得する。

あまりないと思いますが、
Tomcatでアプリケーション稼働中、あるサーブレットインスタンスが持つ情報を見たり修正したい時があるとします。
それができる画面や仕組みがアプリケーションにあればいいですが、ない時は困りますね。
以下の方法を使えば、稼働中のサーブレットインスタンスを取得し、何らかの操作が可能です。
あくまで裏技ですし、場合によっては事故になるかも知れませんので、自己責任でお願いします。

まず、取得したいサーブレットの名前が以下のweb.xmlの通り、HogeServletとします。

...
<servlet>
      <servlet-name>HogeServlet</servlet-name>
      <servlet-class>xyz.dukelab.HogeServlet</servlet-class>
</servlet>
...

以下のようなJSPを作成し、サーバにアップロードし、ブラウザやcurlなどでJSPにアクセスすればいいです。

<%@ page import="javax.management.*"%>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="org.apache.catalina.Server"%>
<%@ page import="xyz.dukelab.HogeServlet" %>
<%
    MBeanServer mBeanServer = MBeanServerFactory.findMBeanServer(null).get(0);
    ObjectName name = new ObjectName("Catalina", "type", "Server");
    
    // Tomcat関連インスタンス取得
    Server server = (Server) mBeanServer.getAttribute(name, "managedResource");    
    StandardEngine engine = (StandardEngine) server.findService("Catalina").getContainer();
    StandardContext context = (StandardContext) engine.findChild(engine.getDefaultHost()).findChild(getServletContext().getContextPath());
    
    // HogeServletのインスタンスのラッパーインスタンスを取得。
    StandardWrapper sw = (StandardWrapper) context.findChild("HogeServlet");
    
    // HogeServletを取得。
    HogeServlet hs = (HogeServlet) sw.getServlet();
    
    // HogeServletのメソッドを呼び出したり、リフレクションを使ってフィールドやメソッドにアクセスする。
    ...
%>

WebPushについて簡単整理

WebPushについて私なりに理解したことを最大限簡単に書いてみます。

WebPushとは

ブラウザに対してサーバからプッシュ通知ができる技術です。
他のウェブページを見ていても通知が来ます。ブラウザによっては、ブラウザを立ち上げなくても通知が来ます。
専用アプリは不要です。ウェブページ(html, jsp...)とjsだけでプッシュ通知できます。
通知がたくさん来ると、ユーザに迷惑でしょうが、うまく使えば色々な使い方ができそうですね。

プッシュ通知の流れ

プッシュ通知は以下の流れで動作します。
標準のWebPush APIを利用すると仮定します。
AP ServerはApplication ServerでTomcatなどのコンテナーを表します。

ユーザ > Browser

  • ユーザがBrowserでウェブページにアクセスします。
  • Browserからプッシュ通知を許可するかどうかの選択を求めるダイアログが表示されます。
  • ユーザが許可を選択します。

Browser > PushServer > Browser

  • ユーザがBrowserでプッシュ通知の購読ボタンを押します。
  • BrowserからPushServerに購読をリクエストします。
  • PushServerはEndPoint(URL)をBrowserに返します。

Browser > AP Server > DB

  • BrowserはPushServerから受け取ったEndPointとユーザキーなどをAP Serverに送ります。
  • AP ServerはEndPointとユーザキーなどをDBに保存します。

AP Server > PushServer > Browser

  • AP Serverの使用者はEndPoint宛にメッセージを送り、メッセージはPushServerに届きます。
  • PushServerはBrowserにメッセージ送り、Browserはメッセージを通知ダイアログで表示します。

PushServer

ブラウザの利用者にプッシュ通知を送るためにはEndPointというURLにメッセージを送る必要があります。
EndPointはPushServerのURLを表し、ユーザごとにユニークでドメインもブラウザごとに違います。
WebPushを標準的な方法で利用すると、特にEndPointが何者か意識する必要はありません。
Push APIの購読メソッドを呼び出すと、EndPointが返ってきますが、そのEndPoint宛にメッセージを送れば、
該当ブラウザにプッシュ通知が表示されます。

サポートするブラウザ

ChromeFirefoxSafariがWebPushをサポートします。
ブラウザのバージョンは大体2016年7月以降のものなら、WebPushの標準的な使い方ができます。
ChromeFirefoxは検証できましたが、SafariMacがないので、分かりません。
またSafariは少々サポート(アイコン未サポート..)が足りないようです。
MicrosoftのEdgeは開発中だそうです。
従いまして、今のところChromeFirefoxが確実にサポートすると考えればいいと思います。

実装方法の違い

Firefoxでは特に意識する必要はありません。
Chromeの場合、バージョン52未満ではChrome専用で行わなければならないもの(例 : ID取得、manifest.jsonファイル)があります。
今はChrome 52以降なら、Web Pushプロトコル標準をサポートしているため、ブラウザ依存なしで実装できます。

より多くのブラウザをサポートするためにChromeの旧バージョンもサポートすべきか?と悩むところですが、
私は必要ないのでは?と思います。
なぜかというと、IEならまだしも、ChromeFirefoxは知らぬ間に自動更新してくれますし、
ブラウザシェアを見ると、ほとんどのChromeユーザは52以降のバージョンを使用中だからです。
(WebブラウザシェアランキングTOP10(日本国内・世界) – ソフトウェアテスト・第三者検証ならウェブレッジ)

ですので、近々なくなるChrome依存のコードは要らないのでは?と思います。

サンプル

以下のgithubのサンプルが役立ちました。
Javaベースですが、標準的なAPIを使用しているので、他の言語への適用は問題ないと思います。
github.com
WebPushの標準的な実装だけでなく、古いバージョンのChromeにも対応しています。
暗号化する部分が複雑ですが、ただWebPushを利用するだけなら、スキップし、実際通信する部分だけを参照しても問題ないと思います。

実装ポイント

  • プッシュ通知の許可や購読を行うjsは該当ウェブサイトと別のドメインにあってもいい。
  • 通知を行うjsは該当ウェブサイトに配置する必要がある。
  • 通知を行うjsはブラウザでService Workerとして常駐することになる。つまり、該当ウェブサイトを去ってもずっとブラウザのメモリに残って通知が来ることを待つ。
  • Service Workerとして登録された通知を行うjsの更新に注意する必要がある。期限を設定し、うまく更新されるか確認しよう。

SPFとSenderIDで曖昧な部分を整理

SPFについてブログやドキュメントを読んでも何を言っているのか分からないところを整理しました。
実際に検証したところもあり、英文を探して分かったところもあります。

SPFとSenderIDはどのドメインを確認するか

  • SPFはMAIL FROMコマンド(いわゆるEnvelope From)に指定されるメールアドレスのドメイン部のSPFレコードをチェックします。
  • SenderIDはPRA(Purported Responsible Address)といって、メールヘッダーにある最終的に責任があるメールアドレスのドメイン部のSPFレコードをチェックします。
    • PRAを抽出するアルゴリズムRFC4407の2. Determining the Purported Responsible Addressに書いてあります。

下記はRFC4407でPRAを求めるアルゴリズムのStep 1からStep 6までを疑似コードで書いてみたのです。

def getPRA
  def step2_to_step6
    # step 2 
    if Resent-From.isEmpty
      if Sender.isEmpty
        # step 4
        if From.size == 1
          return From[0]
        else
          throw new Exception
        end
      else
        # step 3
        if Sender.size == 1
          return Sender[0] 
        else
          throw new Exception
        end
      end
    else
      return Resent-From
    end
  end
  # step 1
  if Resent-Sender.isEmpty
    return step2_to_step6()
  else
    if Resent-From.isBefore(Resent-Sender) 
         && (Received.isAfter(Resent-From) && Received.isBefore(Resent-Sender)) 
           || (Return-Path.isAfter(Resent-From) && Return-Path.isBefore(Resent-Sender))
      return step2_to_step6()
    else
      return Resent-Sender
    end
  end
end

step1で条件が複雑ですが、Resent-Sender > Resent-From > Sender > Fromの順にチェックしていて、通常はFromのメールアドレスがPRAになると考えればよさそうです。

includeとredirectの違い

  • includeはincludeの右に指定したメールサーバのSPF設定を参照する上に自分のSPF設定を追加できます
example.org. IN TXT "v=spf1 include:_spf-a.example.com include:_spf-b.example.com include:_spf-c.example.com -all"
  • redirectはredirectの右に指定したメールサーバのSPF設定しか利用できません。自分のSPF設定を追加できません。完全に委任するということです。
example.net. IN TXT "v=spf1 redirect=example.org"
  • まとめると、includeとredirectの違いは自分のSPF設定を追加できるかどうかです。