読者です 読者をやめる 読者になる 読者になる

カタカタブログ

SIerで働くITエンジニアがカタカタした記録を残す技術ブログ。Java, Oracle Database, Linuxが中心です。たまにRuby on Railsなども。

SQL テーブル参照先の条件句 JOIN vs EXISTS

Oracle DB SQL

昨日の記事に続き、今日もSQLネタ。

さて、あるテーブルにある行を検索する際に検索条件が外部キーで参照した別テーブルの列にあり、かつそのテーブルの値をselectしない場合、
そのようなSQLは結合もしくはexists句を使う2パターンで表現できる。

今回はこの2パターンを検証する。

問題設定

環境は例によってOracle DB 12cのSCOTTスキーマのemp表とdept表を用いる。
データやテーブル構成は前記と同じなのでこちらを参照。
totech.hateblo.jp


今回は、「給与が3000以上である従業員が所属している部署の部署番号と部署名を一覧化する」SQLを考える。
ここで、「最終的にに取得したいデータは全てdept表にあり、emp表は絞込みのためにしか使わない」という点が重要。

以下のようなSQLを実行する。

select EMPNO, ENAME, SAL, DEPTNO
from SCOTT.EMP e
where sal >= 3000
order by e.deptno

このような結果が得られるので、部署番号10と20の部署番号、部署名が正解の結果となる。

EMPNO ENAME SAL DEPTNO
7839 KING 5000 10
7902 FORD 3000 20
7788 SCOTT 3000 20

パターン1: 結合を使う方法

比較的分かりやすいのはこのパターン。あまり考えずに書く場合はだいたいこうなる気がする。

select distinct
 d.deptno
, d.dname
from
 scott.emp e
, scott.dept d
where e.deptno = d.deptno
and e.sal >= 3000

JOIN句は使っていないが、emp表とdept表を結合してwhere句で絞り込んでいる。
見た目には分かりやすい。
emp表と結合してしまうと重複が発生してしまうため、最後にDISTINCTしているところがネックとなる。
実行結果は以下のようになり、正しい結果となっているよう。

DEPTNO DNAME
10 ACCOUNTING
20 RESEARCH

実行計画を見るとHASH UNIQUEによって重複削除を行っている。

----------------------------------------------------------------------------
| Id | Operation     | Name | Rows | Bytes | Cost (%CPU)| Time  |
----------------------------------------------------------------------------
| 0 | SELECT STATEMENT  |   |  3 | 144 |  6 (34)| 00:00:01 |
| 1 | HASH UNIQUE    |   |  3 | 144 |  6 (34)| 00:00:01 |
|* 2 | HASH JOIN    |   |  3 | 144 |  5 (20)| 00:00:01 |
|* 3 |  TABLE ACCESS FULL| EMP |  3 |  78 |  2 (0)| 00:00:01 |
| 4 |  TABLE ACCESS FULL| DEPT |  4 |  88 |  2 (0)| 00:00:01 |
----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

 2 - access("E"."DEPTNO"="D"."DEPTNO")
 3 - filter("E"."SAL">=3000)

パターン2: exists句を使う方法

結合を使わずにexsits句で表現することもできる。
その場合のSQLはこのようになる。

select
 d.deptno
, d.dname
from
scott.dept d
where exists
 (select 1
 from scott.emp e
 where e.deptno = d.deptno
  and e.sal >= 3000)

from句には実際に値を取得したいdept表だけが残り、
where句の中のサブクエリとしてemp表を使っている。
見た目はサブクエリを使うせいでやや複雑になっているが、exsits句だと重複は発生しないのでDISTINCTをする必要はなくなった。
実行結果は以下のようになり、先ほどと同じ結果となった(順番は変わったが)。

DEPTNO DNAME
20 RESEARCH
10 ACCOUNTING

実行計画を見ておく。HASH JOIN SEMIによる結合が行われ、代わりに重複削除処理はなくなった。

---------------------------------------------------------------------------
| Id | Operation     | Name | Rows | Bytes | Cost (%CPU)| Time  |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT |   |  3 | 144 |  5 (20)| 00:00:01 |
|* 1 | HASH JOIN SEMI  |   |  3 | 144 |  5 (20)| 00:00:01 |
| 2 | TABLE ACCESS FULL| DEPT |  4 |  88 |  2 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| EMP |  3 |  78 |  2 (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

 1 - access("E"."DEPTNO"="D"."DEPTNO")
 3 - filter("E"."SAL">=3000)

まとめ(どちらがよいか)

個人的にはパターン2のexistsを使うほうが、
余計な結合と重複削除がないため、パターン1の結合を使うケースより早くなるケースが多いような気がする。
ただし前回の記事ほど一般的な結果とは言えないように思うので、
このような取得列がないテーブルを条件句で参照する場合は、結合ではなくexistsによる選択肢があることを忘れないようにしたい。

以上。

SQL グループごとの最大値を持つ行を取得するときは分析関数MAXを使う

Oracle DB SQL

SQLを書いていて、あるグループごとにある項目を集計して最大値を持つ行だけを検索したいときがある。
これまではあまり気にせずにgroup byと集計関数MAXを使ったサブクエリを検索条件に使うことで求めていたが、
パフォーマンスがよくないことがあり、今回改めて検証してみた。

結果、自分の中では分析関数MAXを使って検索条件とする方法がパフォーマンス的にもっともよいという結論に達した。

以下、Oracle DB 12cの環境で行った検証についてまとめてみる。
なおこの検証は自分の中では一般性を持つ結果だと考えているが、
データの特性やハードウェアスペックによって異なる結果となる可能性はあるため、必ずその環境で検証を行う必要がある。

問題設定

Oracle DB標準のサンプルスキーマであるSCOTTスキーマの従業員表(emp), 部署表(dept)を用いる。
部署ごとに、それぞれ給与が最大の従業員を求めるクエリを書き、パフォーマンスの観点で比較してみる。
なお、実行速度はデータ数が少なくほとんど差がないため、実行計画を見て比較する。

データの構成を示す。
・emp表

select
 e.EMPNO
, e.ENAME
, e.SAL
, e.DEPTNO
from SCOTT.EMP e
EMPNO ENAME SAL DEPTNO
7369 SMITH 800 20
7499 ALLEN 1600 30
7521 WARD 1250 30
7566 JONES 2975 20
7654 MARTIN 1250 30
7698 BLAKE 2850 30
7782 CLARK 2450 10
7788 SCOTT 3000 20
7839 KING 5000 10
7844 TURNER 1500 30
7876 ADAMS 1100 20
7900 JAMES 950 30
7902 FORD 3000 20
7934 MILLER 1300 10

・dept表

select
  d.DEPTNO
, d.DNAME
from SCOTT.DEPT d
DEPTNO DNAME
10 ACCOUNTING
20 RESEARCH
30 SALES
40 OPERATIONS

・emp表とdept表をDEPTNOで結合

select e.EMPNO, e.ENAME, e.SAL, d.DEPTNO, d.DNAME
from SCOTT.EMP e, SCOTT.DEPT d
where e.DEPTNO = d.DEPTNO
order by d.DEPTNO, e.SAL desc
EMPNO ENAME SAL DEPTNO DNAME
7839 KING 5000 10 ACCOUNTING
7782 CLARK 2450 10 ACCOUNTING
7934 MILLER 1300 10 ACCOUNTING
7788 SCOTT 3000 20 RESEARCH
7902 FORD 3000 20 RESEARCH
7566 JONES 2975 20 RESEARCH
7876 ADAMS 1100 20 RESEARCH
7369 SMITH 800 20 RESEARCH
7698 BLAKE 2850 30 SALES
7499 ALLEN 1600 30 SALES
7844 TURNER 1500 30 SALES
7654 MARTIN 1250 30 SALES
7521 WARD 1250 30 SALES
7900 JAMES 950 30 SALES

改めてクエリの条件を書くと、「部署ごとに、それぞれ給与が最大の従業員を求める」SQLを書きたい。
つまり以下のような結果が得られるはずである。

EMPNO ENAME SAL DEPTNO DNAME
7839 KING 5000 10 ACCOUNTING
7788 SCOTT 3000 20 RESEARCH
7902 FORD 3000 20 RESEARCH
7698 BLAKE 2850 30 SALES

このような結果を取得するためのSQLについて考えてみる。

パターン1: GROUP BY + MAX集計関数を使う方法

これまで私が何も考えずに書いていたパターン。
サブクエリで部署番号(dept.DEPTNO)ごとに最大の給与(emp.SAL)を持つ従業員番号(emp.EMPNO)を求め、
その結果の部署番号と給与で従業員表を検索する。

select
 e1.EMPNO
, e1.ENAME
, e1.SAL
, d.DEPTNO
, d.DNAME
from
 SCOTT.EMP e1
, (select
     ee.DEPTNO
   , MAX(ee.SAL) MAX_SAL
  from SCOTT.EMP ee
  group by ee.DEPTNO) e2
, SCOTT.DEPT d
where e1.DEPTNO = d.DEPTNO
  and e1.DEPTNO = e2.DEPTNO
  and e1.SAL = e2.MAX_SAL
order by d.DEPTNO

結果は以下の通り。意図通りの検索結果になっている。

EMPNO ENAME SAL DEPTNO DNAME
7839 KING 5000 10 ACCOUNTING
7788 SCOTT 3000 20 RESEARCH
7902 FORD 3000 20 RESEARCH
7698 BLAKE 2850 30 SALES

ここで実行計画を見ておく。

Plan hash value: 2079720973

------------------------------------------------------------------------------
| Id | Operation      | Name | Rows | Bytes | Cost (%CPU)| Time  |
------------------------------------------------------------------------------
| 0 | SELECT STATEMENT   |   |  65 | 7670 |  8 (25)| 00:00:01 |
|* 1 | FILTER       |   |   |   |      |     |
| 2 | SORT GROUP BY   |   |  65 | 7670 |  8 (25)| 00:00:01 |
|* 3 |  HASH JOIN     |   |  65 | 7670 |  7 (15)| 00:00:01 |
|* 4 |  HASH JOIN    |   |  14 | 1288 |  5 (20)| 00:00:01 |
| 5 |   TABLE ACCESS FULL| DEPT |  4 | 136 |  2 (0)| 00:00:01 |
| 6 |   TABLE ACCESS FULL| EMP |  14 | 812 |  2 (0)| 00:00:01 |
| 7 |  TABLE ACCESS FULL | EMP |  14 | 364 |  2 (0)| 00:00:01 |
------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

 1 - filter("E1"."SAL"=MAX("EE"."SAL"))
 3 - access("E1"."DEPTNO"="EE"."DEPTNO")
 4 - access("E1"."DEPTNO"="D"."DEPTNO")

6,7行目よりEMP表のフルスキャンが2回走っていることが分かる。
これはサブクエリと本体でそれぞれEMP表を検索し、結合しているためである。

パターン2: MAX分析関数を使う方法

上のパターンはEMP表のフルスキャンが2回走っていたが、
なんとかこれを1回で済ませられないだろうか、という観点で考えた結果、
分析関数MAXで部署番号ごとの最大給与を算出した上で直接where句でその行の給与列と最大給与を比較して絞ってしまえばよい、という発想に至ったので、
さっそくSQLクエリを書いてみた。

select
 e1.EMPNO
, e1.ENAME
, e1.SAL
, d.DEPTNO
, d.DNAME
from
(select
  ee.EMPNO
 , ee.ENAME
 , ee.SAL
 , MAX(ee.SAL) OVER(partition by ee.DEPTNO) MAX_SAL
 , ee.DEPTNO
 from SCOTT.EMP ee) e1
, SCOTT.DEPT d
where e1.DEPTNO = d.DEPTNO
 and e1.SAL = e1.MAX_SAL
order by d.DEPTNO

MAX(ee.SAL) OVER(partition by ee.DEPTNO) MAX_SALのところがいわゆる分析関数で
GROUP BYと同じようにee.DEPTNOの値ごとにee.SALの値を集計し、最大値を求める。
これはOracle DB特有の書き方だが、他のMySQLなどのRDBMSでも似たようなことはできるらしい。

このSQLでも(FORTとSCOTTの順番が逆になったが) 同じ結果が得られる。

EMPNO ENAME SAL DEPTNO DNAME
7839 KING 5000 10 ACCOUNTING
7902 FORD 3000 20 RESEARCH
7788 SCOTT 3000 20 RESEARCH
7698 BLAKE 2850 30 SALES

さらに実行計画を見てみる。

Plan hash value: 2845603103

-----------------------------------------------------------------------------
| Id | Operation      | Name | Rows | Bytes | Cost (%CPU)| Time  |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT  |   |  14 | 1134 |  6 (34)| 00:00:01 |
| 1 | MERGE JOIN     |   |  14 | 1134 |  6 (34)| 00:00:01 |
|* 2 | VIEW       |   |  14 | 826 |  3 (34)| 00:00:01 |
| 3 |  WINDOW SORT   |   |  14 | 644 |  3 (34)| 00:00:01 |
| 4 |  TABLE ACCESS FULL| EMP |  14 | 644 |  2 (0)| 00:00:01 |
|* 5 | SORT JOIN     |   |  4 |  88 |  3 (34)| 00:00:01 |
| 6 |  TABLE ACCESS FULL | DEPT |  4 |  88 |  2 (0)| 00:00:01 |
-----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

 2 - filter("E1"."SAL"="E1"."MAX_SAL")
 5 - access("E1"."DEPTNO"="D"."DEPTNO")
   filter("E1"."DEPTNO"="D"."DEPTNO")

EMP表のフルスキャンが1回(4行目)になり、COSTも8から6に減少している。
EMP表は従業員表なのでフルスキャンの回数が増えると劇的にパフォーマンスが悪くなるはずであり、
分析関数を用いた方法はフルスキャンの回数が減らせるので、明らかにパフォーマンスがよくなるはず。

まとめ

「グループごとに最大値を含む行のみを取得する」クエリを書く場合は、
GROUP BYとサブクエリで書くよりも、分析関数を利用したほうがパフォーマンス的によさそう。

以上!

Raspberry Pi 2とSoftEtherを使って自宅VPNを構築してみた

Linux ネットワーク Raspberry Pi

これまでヤマハやBUFFALOの市販のルータを使って自宅VPNを構築してきた。
totech.hateblo.jp
totech.hateblo.jp
しかし、先日BUFFALOのルータを買い換えたところ、PPTPサーバ機能がなくてVPNサーバとして使えなかったため、
Raspberry Pi 2を使ってソフトウェアVPNサーバを構築してみようと思い立った。

そこで、今回はSoftEther(ソフトイーサ)というソフトウェアVPNを使って自宅VPNを構築する。
SoftEtherクラウド上でVPNの経路を中継する仕組みのため、VPNサーバとVPNクライアント間で直接セッションを貼るのではなく、間にVPN Azureというクラウドサービスを使う。これは筑波大学が研究用途で公開している無料のサービスで、カタログスペック上はopenvpnよりも帯域や使いやすさで上回っているらしい。
また、クラウドサービスを利用することで、VPN機能のルータはもちろんグローバルIPアドレスすら不要となる。

よって、今回はRaspberry Pi 2上にSoftEtherのVPNサーバをインストールし、VPN Azureを使って中継し、Windows 7からSoftEtherのVPNクライアントを使って自宅LANにVPNに接続する。

基本的には以下の公式マニュアルを参照して構築した。
https://ja.softether.org/4-docs/1-manual/7/7.3

Raspberry PiにSoftEther VPN Serverインストール

まずはRaspberry Pi にVPNサーバを構築する。
SoftEtherのVPNサーバインストーラは以下からダウンロードできる。
http://www.softether.org/5-download
現時点の最新版である softether-vpnserver-v4.21-9613-beta-2016.04.24-linux-arm_eabi-32bit.tar.gz をダウンロードした。

f:id:osn_th:20160806181937p:plain

以下、Raspberry Pi上でインストールする作業をrootユーザで行う。
まず、ダウンロードしたインストーラを展開する。

root@raspberrypi:~# pwd
/root
root@raspberrypi:~# ls -l softether-vpnserver-v4.21-9613-beta-2016.04.24-linux-arm_eabi-32bit.tar.gz
-rw-r--r-- 1 root root 5646405  8月  4  2016 softether-vpnserver-v4.21-9613-beta-2016.04.24-linux-arm_eabi-32bit.tar.gz

root@raspberrypi:~# tar xzvf softether-vpnserver-v4.21-9613-beta-2016.04.24-linux-arm_eabi-32bit.tar.gz
vpnserver/
vpnserver/Makefile
vpnserver/.install.sh
vpnserver/ReadMeFirst_License.txt
vpnserver/Authors.txt
vpnserver/ReadMeFirst_Important_Notices_ja.txt
vpnserver/ReadMeFirst_Important_Notices_en.txt
vpnserver/ReadMeFirst_Important_Notices_cn.txt
vpnserver/code/
vpnserver/code/vpnserver.a
vpnserver/code/vpncmd.a
vpnserver/lib/
vpnserver/lib/libcharset.a
vpnserver/lib/libcrypto.a
vpnserver/lib/libedit.a
vpnserver/lib/libiconv.a
vpnserver/lib/libncurses.a
vpnserver/lib/libssl.a
vpnserver/lib/libz.a
vpnserver/lib/License.txt
vpnserver/hamcore.se2

実行可能ファイルを作成するため、コンパイルする。
途中でライセンスに関する確認が行われるので、回答する。

root@raspberrypi:~# cd vpnserver/
root@raspberrypi:~/vpnserver# make
--------------------------------------------------------------------

SoftEther VPN Server (Ver 4.21, Build 9613, ARM EABI) for Linux Install Utility
Copyright (c) SoftEther Project at University of Tsukuba, Japan. All Rights Reserved.

--------------------------------------------------------------------


Do you want to read the License Agreement for this software ?

 1. Yes
 2. No

Please choose one of above number:
1

(・・・略)
Did you read and understand the License Agreement ?
(If you couldn't read above text, Please read 'ReadMeFirst_License.txt'
 file with any text editor.)

 1. Yes
 2. No

Please choose one of above number:
1


Did you agree the License Agreement ?

1. Agree
2. Do Not Agree

Please choose one of above number:
1

make[1]: Entering directory '/root/vpnserver'
Preparing SoftEther VPN Server...
ranlib lib/libcharset.a
ranlib lib/libcrypto.a
ranlib lib/libedit.a
ranlib lib/libiconv.a
ranlib lib/libncurses.a
ranlib lib/libssl.a
ranlib lib/libz.a
ranlib code/vpnserver.a
gcc code/vpnserver.a -O2 -fsigned-char -lm -ldl -lrt -Wl,--no-warn-mismatch -lpthread -L./ lib/libssl.a lib/libcrypto.a lib/libiconv.a lib/libcharset.a lib/libedit.a lib/libncurses.a lib/libz.a -o vpnserver
ranlib code/vpncmd.a
gcc code/vpncmd.a -O2 -fsigned-char -lm -ldl -lrt -Wl,--no-warn-mismatch -lpthread -L./ lib/libssl.a lib/libcrypto.a lib/libiconv.a lib/libcharset.a lib/libedit.a lib/libncurses.a lib/libz.a -o vpncmd
./vpncmd /tool /cmd:Check
vpncmd コマンド - SoftEther VPN コマンドライン管理ユーティリティ
SoftEther VPN コマンドライン管理ユーティリティ (vpncmd コマンド)
Version 4.21 Build 9613   (Japanese)
Compiled 2016/04/24 16:39:47 by yagi at pc30
Copyright (c) SoftEther VPN Project. All Rights Reserved.

VPN Tools を起動しました。HELP と入力すると、使用できるコマンド一覧が表示できます。

VPN Tools>Check
Check コマンド - SoftEther VPN の動作が可能かどうかチェックする
---------------------------------------------------
SoftEther VPN 動作環境チェックツール

Copyright (c) SoftEther VPN Project.
All Rights Reserved.

この動作環境チェックツールを実行したシステムがテストに合格した場合は、SoftEther VPN ソフトウェアが動作する可能性が 高いです。チェックにはしばらく時間がかかる場合があります。そのままお待ちください...

'カーネル系' のチェック中...
              [合格] ○
'メモリ操作系' のチェック中...
              [合格] ○
'ANSI / Unicode 文字列処理系' のチェック中...
              [合格] ○
'ファイルシステム' のチェック中...
              [合格] ○
'スレッド処理システム' のチェック中...
              [合格] ○
'ネットワークシステム' のチェック中...
              [合格] ○

すべてのチェックに合格しました。このシステム上で SoftEther VPN Server / Bridge が正しく動作する可能性が高いと思われます。

コマンドは正常に終了しました。


--------------------------------------------------------------------
The preparation of SoftEther VPN Server is completed !


*** How to switch the display language of the SoftEther VPN Server Service ***
SoftEther VPN Server supports the following languages:
  - Japanese
  - English
  - Simplified Chinese

You can choose your prefered language of SoftEther VPN Server at any time.
To switch the current language, open and edit the 'lang.config' file.


*** How to start the SoftEther VPN Server Service ***

Please execute './vpnserver start' to run the SoftEther VPN Server Background Service.
And please execute './vpncmd' to run the SoftEther VPN Command-Line Utility to configure SoftEther VPN Server.

Of course, you can use the VPN Server Manager GUI Application for Windows / Mac OS X on the other Windows / Mac OS X computers in order to configure the SoftEther VPN Server remotely.


*** For Windows users ***
You can download the SoftEther VPN Server Manager for Windows
from the http://www.softether-download.com/ web site.
This manager application helps you to completely and easily manage the VPN server services running in remote hosts.


*** For Mac OS X users ***
In April 2016 we released the SoftEther VPN Server Manager for Mac OS X.
You can download it from the http://www.softether-download.com/ web site.
VPN Server Manager for Mac OS X works perfectly as same as the traditional Windows versions. It helps you to completely and easily manage the VPN server services running in remote hosts.

--------------------------------------------------------------------

make[1]: Leaving directory '/root/vpnserver'

正常に終了したら、VPN Serverのプログラムが生成されるので、それをvpnserverディレクトリごと/user/localに移動する。

root@raspberrypi:~/vpnserver# cd ..
root@raspberrypi:~# ls
softether-vpnserver-v4.21-9613-beta-2016.04.24-linux-arm_eabi-32bit.tar.gz  vpnserver
root@raspberrypi:~# mv vpnserver/ /usr/local/
root@raspberrypi:~# ls -l /usr/local/vpnserver/
合計 8784
-rwxrwxrwx 1 root root    1881  4月 24 08:11 Authors.txt
-rwxrwxrwx 1 root root    2859  4月 24 08:11 Makefile
-rwxrwxrwx 1 root root   30801  4月 24 08:11 ReadMeFirst_Important_Notices_cn.txt
-rwxrwxrwx 1 root root   36297  4月 24 08:11 ReadMeFirst_Important_Notices_en.txt
-rwxrwxrwx 1 root root   50695  4月 24 08:11 ReadMeFirst_Important_Notices_ja.txt
-rwxrwxrwx 1 root root   58932  4月 24 08:11 ReadMeFirst_License.txt
drwx------ 2 root root    4096  8月  4 07:39 chain_certs
drwxrwxrwx 2 root root    4096  8月  4 07:38 code
-rwxrwxrwx 1 root root 1295363  4月 24 08:11 hamcore.se2
-rw------- 1 root root     867  8月  4 07:38 lang.config
drwxrwxrwx 2 root root    4096  8月  4 07:38 lib
-rwxr-xr-x 1 root root 3742580  8月  4 07:38 vpncmd
-rwxr-xr-x 1 root root 3742656  8月  4 07:38 vpnserver

パーミッションを変えておく。

root@raspberrypi:~# cd /usr/local/vpnserver/
root@raspberrypi:/usr/local/vpnserver# chmod 600 *
root@raspberrypi:/usr/local/vpnserver# chmod 700 vpncmd
root@raspberrypi:/usr/local/vpnserver# chmod 700 vpnserver
root@raspberrypi:/usr/local/vpnserver# ls -l
合計 8784
-rw------- 1 root root    1881  4月 24 08:11 Authors.txt
-rw------- 1 root root    2859  4月 24 08:11 Makefile
-rw------- 1 root root   30801  4月 24 08:11 ReadMeFirst_Important_Notices_cn.txt
-rw------- 1 root root   36297  4月 24 08:11 ReadMeFirst_Important_Notices_en.txt
-rw------- 1 root root   50695  4月 24 08:11 ReadMeFirst_Important_Notices_ja.txt
-rw------- 1 root root   58932  4月 24 08:11 ReadMeFirst_License.txt
drw------- 2 root root    4096  8月  4 07:39 chain_certs
drw------- 2 root root    4096  8月  4 07:38 code
-rw------- 1 root root 1295363  4月 24 08:11 hamcore.se2
-rw------- 1 root root     867  8月  4 07:38 lang.config
drw------- 2 root root    4096  8月  4 07:38 lib
-rwx------ 1 root root 3742580  8月  4 07:38 vpncmd
-rwx------ 1 root root 3742656  8月  4 07:38 vpnserver

vpncmdというコマンドツールで、VPNサーバの動作チェックが行えるので、念のため行っておく。ちなみに上で実行可能ファイルを生成した時によく見ると同様のチェックがすでに行われている。

root@raspberrypi:/usr/local/vpnserver# ./vpncmd
vpncmd コマンド - SoftEther VPN コマンドライン管理ユーティリティ
SoftEther VPN コマンドライン管理ユーティリティ (vpncmd コマンド)
Version 4.21 Build 9613   (Japanese)
Compiled 2016/04/24 16:39:47 by yagi at pc30
Copyright (c) SoftEther VPN Project. All Rights Reserved.

vpncmd プログラムを使って以下のことができます。

1. VPN Server または VPN Bridge の管理
2. VPN Client の管理
3. VPN Tools コマンドの使用 (証明書作成や通信速度測定)

1 - 3 を選択: 3

VPN Tools を起動しました。HELP と入力すると、使用できるコマンド一覧が表示できます。

VPN Tools>help
下記の 6 個のコマンドが使用できます:
 About         - バージョン情報の表示
 Check         - SoftEther VPN の動作が可能かどうかチェックする
 MakeCert      - 新しい X.509 証明書と秘密鍵の作成 (1024 bit)
 MakeCert2048  - 新しい X.509 証明書と秘密鍵の作成 (2048 bit)
 TrafficClient - 通信スループット測定ツールクライアントの実行
 TrafficServer - 通信スループット測定ツールサーバーの実行

それぞれのコマンドの使用方法については、"コマンド名 ?" と入力するとヘルプが表示されます。
コマンドは正常に終了しました。

VPN Tools>check
Check コマンド - SoftEther VPN の動作が可能かどうかチェックする
---------------------------------------------------
SoftEther VPN 動作環境チェックツール

Copyright (c) SoftEther VPN Project.
All Rights Reserved.

この動作環境チェックツールを実行したシステムがテストに合格した場合は、SoftEther VPN ソフトウェアが動作する可能性が 高いです。チェックにはしばらく時間がかかる場合があります。そのままお待ちください...

'カーネル系' のチェック中...
              [合格] ○
'メモリ操作系' のチェック中...
              [合格] ○
'ANSI / Unicode 文字列処理系' のチェック中...
              [合格] ○
'ファイルシステム' のチェック中...
              [合格] ○
'スレッド処理システム' のチェック中...
              [合格] ○
'ネットワークシステム' のチェック中...
              [合格] ○

すべてのチェックに合格しました。このシステム上で SoftEther VPN Server / Bridge が正しく動作する可能性が高いと思われます。

コマンドは正常に終了しました。

VPN Tools>exit

すべてのチェックに合格していることを確認する。

ここから、VPNサーバを運用しやすくするため、スタートアップスクリプトに登録する。

root@raspberrypi:/usr/local/vpnserver# touch /etc/init.d/vpnserver
root@raspberrypi:/usr/local/vpnserver# vi /etc/init.d/vpnserver

公式マニュアル通り、以下の内容を記載する。

#!/bin/sh
# chkconfig: 2345 99 01
# description: SoftEther VPN Server
DAEMON=/usr/local/vpnserver/vpnserver
LOCK=/var/lock/subsys/vpnserver
test -x $DAEMON || exit 0
case "$1" in
start)
$DAEMON start
touch $LOCK
;;
stop)
$DAEMON stop
rm $LOCK
;;
restart)
$DAEMON stop
sleep 3
$DAEMON start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
esac
exit 0

パーミッションも適切に設定する。

root@raspberrypi:/usr/local/vpnserver# chmod 755 /etc/init.d/vpnserver

chkconfigに登録する。当環境ではchkconfigが入っていなかったのでapt-getでインストールから行った。

root@raspberrypi:~# apt-get install chkconfig
root@raspberrypi:~# chkconfig vpnserver on
insserv: warning: script 'vpnserver' missing LSB tags and overrides

警告が出るが、以下を見る限り大した問題ではなさそうなので、ここでは無視する。
http://www.sssg.org/blogs/hiro345/archives/16237.html

root@raspberrypi:~# chkconfig vpnserver --list
vpnserver                 0:off  1:off  2:on   3:on   4:on   5:on   6:off

これで以下のコマンドでVPNサーバの起動・停止・ステータス確認が正常に行えるようになった。

# service vpnserver start
# service vpnserver stop
# service vpnserver status

vpnserverを起動させておく。

root@raspberrypi:~# service vpnserver start

これでRaspberry PiにSoftEther VPN Serverのインストールが完了した。

Windows 7マシンからSoftEther VPN Serverの設定

Raspberry Pi にインストールしたVPN Serverの設定を行う。
サーバ上で直接vpncmdを使ってもできるのだが、Win/Mac用のGUIの管理用ツールがあるので、各種設定はこちらから行う。
そのため、ここからはWindowsマシンを使って作業する。今回は後にVPNクライアントととしても使うことになるWindows 7端末を利用する。

VPNサーバと同じダウンロードページから softether-vpnserver_vpnbridge-v4.21-9613-beta-2016.04.24-windows-x86_x64-intel.exe をダウンロードする。
http://www.softether.org/5-download
f:id:osn_th:20160806181923p:plain

インストーラを展開し、実行する。
SoftEther VPN サーバー管理マネージャを選択し、次へ
f:id:osn_th:20160806182010p:plain
あとは、ライセンスやインストール先を指定するだけで、基本的に指示通りに「次へ」を選んでいくだけでインストールが進む。
完了し、管理マネージャを起動すると以下のようなGUIのツールが起動する。
「新しい接続設定」をクリックし、さきほどRaspberry Pi にインストールしたSoftEther VPN Serverの設定を行っていく。
f:id:osn_th:20160806181950p:plain
接続設定名はなんでもよいが、今回はraspi-vpnとした。なお、この先いろいろなところで各種設定の名前を入力するが、今回は全てraspi-vpnとしている。
ホスト名はVPN ServerをインストールしたサーバのIPアドレスもしくはホスト名を指定する。
f:id:osn_th:20160806181947p:plain
設定が作成されたので、「接続」する。
f:id:osn_th:20160806181955p:plain
初回ログイン時に管理パスワードの設定をする。
以降は管理ツールにアクセス時にそのパスワードを入力する(ちなみにこれはVPNユーザのパスワードとはまた別で、それはあとで別に設定する)。
f:id:osn_th:20160806181941p:plain
リモートアクセス VPNサーバーにチェックを入れ、次へ
f:id:osn_th:20160806181933p:plain
はい
f:id:osn_th:20160806182012p:plain
仮想HUB名もraspi-vpnにしておく
f:id:osn_th:20160806181958p:plain
固有のダイナミックDNSが割り当てられる。変更も可能なようだが、今回は特に変更せずにそのまま利用する。
f:id:osn_th:20160806182018p:plain
今回はVPN Azureを使ってクラウド経由のVPNを作るため、「VPN Azureを有効にする」にチェックを入れる。
ここで、上のダイナミックDNSに基づいた「現在のVPN Azureホスト名」が実際にVPNクライアントから接続するときのホスト名となるので、控えておいた上で、OK。
f:id:osn_th:20160806182021p:plain
続いてVPNユーザーを作成する。
f:id:osn_th:20160806182004p:plain
f:id:osn_th:20160806182014p:plain
最後に、VPN経由でLANにアクセスするために、ローカルブリッジ設定をする。
この設定を入れないとVPNでつないだときにリンクローカルアドレスが割り当てられてしまい、LAN内のサーバと通信ができない。
この設定を入れることで、VPNセッションをつないだときにLAN内のDHCPサーバ(ルータ)からIPアドレスが割り当てられる。
f:id:osn_th:20160806182034p:plain
仮想HUBに「物理的な既存のLANカードとのブリッジ接続」を選んで、「ローカルブリッジを追加」する。
f:id:osn_th:20160806181930p:plain
追加されたローカルブリッジが動作中になる

f:id:osn_th:20160806181921p:plain
以上で、VPN Server側の設定が完了した!

SoftEther VPN Clientセットアップ

VPNサーバ側のセットアップが完了したので、あとはVPNクライアントをセットアップして実際に接続してみる。
SoftEther VPN Clientもこれまでと同じようにダウンロードする。
f:id:osn_th:20160806182006p:plain
SoftEther VPN Clientをインストールする。
f:id:osn_th:20160806181952p:plain
クライアント側も指示どおりに「次へ」を選んでいくだけで、特に迷うことなくインストールできる。
VPNクライアントが起動したら、「新しい接続設定の作成」を選択する。
f:id:osn_th:20160806182000p:plain
はい
f:id:osn_th:20160806181943p:plain
ここでもraspi-vpnとする。
f:id:osn_th:20160806181927p:plain
つくられたraspi-vpn仮想LANカードをクリック
f:id:osn_th:20160806182024p:plain
さきほど作ったVPNサーバ側のホスト名やVPNユーザ情報など各種設定を入力し、OK。
f:id:osn_th:20160806182027p:plain

ここまででVPNサーバ側およびクライアント側の全設定が完了した。

VPN接続テスト

セットアップしたVPNが正しく動作するか検証する。
ここまで作業は自宅LAN内のルータの無線LANを使ってインターネットに出ていたが、正しくVPN経由で自宅LANにアクセスできることを確認するため、ここからはモバイルルータにネットワークを切り替えて作業する。

作成したVPNクライアントの接続設定を右クリックし、「接続」を選択する。
f:id:osn_th:20160806182031p:plain
VPN上のDHCPサーバからIPアドレスが取得される。
f:id:osn_th:20160806181945p:plain
IPアドレスが割り当てられ、接続完了となればOK。
f:id:osn_th:20160806182037p:plain

ipconfigを打つと、raspi-vpn仮想ネットワークカードに自宅LANのIPアドレスが割り当てられていることが確認できる。

>ipconfig

Windows IP 構成


イーサネット アダプター raspi-vpn - VPN Client:

   接続固有の DNS サフィックス . . . :
   リンクローカル IPv6 アドレス. . . . : xxxx::xxxx:xxxx:xxxx:xxxx%60
   IPv4 アドレス . . . . . . . . . . : 192.168.11.17
   サブネット マスク . . . . . . . . : 255.255.255.0
   デフォルト ゲートウェイ . . . . . : 192.168.11.1

Raspberry Pi 2 (192.168.11.11)にもアクセスできる。

>ping 192.168.11.11

192.168.11.11 に ping を送信しています 32 バイトのデータ:
192.168.11.11 からの応答: バイト数 =32 時間 =1ms TTL=64
192.168.11.11 からの応答: バイト数 =32 時間 =1ms TTL=64
192.168.11.11 からの応答: バイト数 =32 時間 =1ms TTL=64
192.168.11.11 からの応答: バイト数 =32 時間 =1ms TTL=64

192.168.11.11 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 1ms、最大 = 1ms、平均 = 1ms

sshもOK。

>ssh pi@192.168.11.11
pi@192.168.11.11's password:

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Aug  6 01:20:30 2016 from 192.168.11.4
pi@raspberrypi:~ $

sshでRaspberry Piにログインもできた。
正しくVPNが構築できているよう!

まとめ

今回はSoftEtherのVPN Azureを使って自宅VPNを構築した。
Raspberry PiをSoftEther VPN Server、Windows 7マシンをSoftEther VPN Clientとして環境構築、設定、疎通まで確認できた。
それほど難しい手順はなく(VPNやネットワークの基礎知識がないと辛いが)、管理ツールも使いやすいのでかなり便利だと思う。クラウドVPNを使うことでグローバルIPアドレスが不要になるなので、自宅ルータにVPN機能がなくても普通のマシンされあればVPN Server化できるので、構築のための敷居はかなり低いと感じた。

以上!

apt-get install時に「 パッケージ 'wireless-tools' のファイル一覧ファイルに最後の改行がありません」エラー

Linux Raspberry Pi

久しぶりにRaspberry Piを触った。
Raspberry Pi 2にapt-getでchkconfigを入れようとしたときによくわからないエラーが出たので、対策をメモ。

エラーと対応方法

apt-getでchkconfigを入れようとしたときに、wireless-toolsパッケージ周りでエラーが発生し、インストールに失敗するという問題が発生。

# apt-get install chkconfig
(・・・略)
dpkg: 復旧不可能な致命的なエラーです。中止します:
パッケージ 'wireless-tools' のファイル一覧ファイルに最後の改行がありません
E: Sub-process /usr/bin/dpkg returned an error code (2)

apt-get update, upgradeをやってから再実行するも変わらず。

wireless-toolsのファイル一覧ファイルがおかしいということなのでファイルの中身を確認してみる。

# file /var/lib/dpkg/info/wireless-tools.list
/var/lib/dpkg/info/wireless-tools.list: PNG image data, 38 x 25, 8-bit/color RGBA, non-interlaced

なぜかPNG画像ファイルになっている。。
普通は以下のようにテキストファイルのはず。つまり、wireless-tools.listが破損してしまっているよう。

# file /var/lib/dpkg/info/*.list | head
/var/lib/dpkg/info/acl.list:                 ASCII text
/var/lib/dpkg/info/adduser.list:               ASCII text
/var/lib/dpkg/info/adwaita-icon-theme.list:          ASCII text
/var/lib/dpkg/info/alacarte.list:               ASCII text
/var/lib/dpkg/info/alsa-base.list:              ASCII text
/var/lib/dpkg/info/alsa-utils.list:              ASCII text
/var/lib/dpkg/info/apt-utils.list:              ASCII text
/var/lib/dpkg/info/apt.list:                 ASCII text
/var/lib/dpkg/info/aptitude-common.list:           ASCII text
/var/lib/dpkg/info/aptitude.list:               ASCII text
# head /var/lib/dpkg/info/acl.list
/.
/usr
/usr/bin
/usr/share
/usr/share/man
/usr/share/man/man1
/usr/share/man/man1/chacl.1.gz
/usr/share/man/man1/getfacl.1.gz
/usr/share/man/man1/setfacl.1.gz
/usr/share/man/man5

正しいwireless-tools.listを探す。適当にググると以下にそれっぽいものを発見。
信頼性はまったくもって怪しいが、どうせ自宅利用のRaspberry Piなのでこれで試してみる。
https://github.com/volumio/RootFS-RaspberryPI/blob/master/var/lib/dpkg/info/wireless-tools.list

# cd/var/lib/dpkg/info
# mv wireless-tools.list wireless-tools.list.org
# viwireless-tools.list

wireless-tools.listの中身は以下のようになった。

/.
/sbin
/sbin/iwgetid
/sbin/iwlist
/sbin/iwevent
/sbin/iwspy
/sbin/iwconfig
/sbin/iwpriv
/etc
/etc/network
/etc/network/if-pre-up.d
/etc/network/if-pre-up.d/wireless-tools
/etc/network/if-post-down.d
/etc/network/if-post-down.d/wireless-tools
/usr
/usr/share
/usr/share/doc/wireless-tools
/usr/share/doc/wireless-tools/changelog.Debian.gz
/usr/share/doc/wireless-tools/HOTPLUG-UDEV.txt.gz
/usr/share/doc/wireless-tools/PCMCIA.txt.gz
/usr/share/doc/wireless-tools/README.Debian
/usr/share/doc/wireless-tools/copyright
/usr/share/doc/wireless-tools/DISTRIBUTIONS.txt.gz
/usr/share/doc/wireless-tools/changelog.gz
/usr/share/doc/wireless-tools/README.gz
/usr/share/man
/usr/share/man/cs
/usr/share/man/cs/man8
/usr/share/man/cs/man8/iwspy.8.gz
/usr/share/man/cs/man8/iwpriv.8.gz
/usr/share/man/cs/man8/iwconfig.8.gz
/usr/share/man/cs/man8/iwevent.8.gz
/usr/share/man/cs/man8/iwgetid.8.gz
/usr/share/man/cs/man8/iwlist.8.gz
/usr/share/man/cs/man7
/usr/share/man/cs/man7/wireless.7.gz
/usr/share/man/fr.UTF-8
/usr/share/man/fr.UTF-8/man8
/usr/share/man/fr.UTF-8/man8/iwspy.8.gz
/usr/share/man/fr.UTF-8/man8/iwpriv.8.gz
/usr/share/man/fr.UTF-8/man8/iwconfig.8.gz
/usr/share/man/fr.UTF-8/man8/iwevent.8.gz
/usr/share/man/fr.UTF-8/man8/iwgetid.8.gz
/usr/share/man/fr.UTF-8/man8/iwlist.8.gz
/usr/share/man/fr.UTF-8/man7
/usr/share/man/fr.UTF-8/man7/wireless.7.gz
/usr/share/man/man8
/usr/share/man/man8/iwspy.8.gz
/usr/share/man/man8/iwpriv.8.gz
/usr/share/man/man8/iwconfig.8.gz
/usr/share/man/man8/iwevent.8.gz
/usr/share/man/man8/iwgetid.8.gz
/usr/share/man/man8/iwlist.8.gz
/usr/share/man/fr.ISO8859-1
/usr/share/man/fr.ISO8859-1/man8
/usr/share/man/fr.ISO8859-1/man8/iwspy.8.gz
/usr/share/man/fr.ISO8859-1/man8/iwpriv.8.gz
/usr/share/man/fr.ISO8859-1/man8/iwconfig.8.gz
/usr/share/man/fr.ISO8859-1/man8/iwevent.8.gz
/usr/share/man/fr.ISO8859-1/man8/iwgetid.8.gz
/usr/share/man/fr.ISO8859-1/man8/iwlist.8.gz
/usr/share/man/fr.ISO8859-1/man7
/usr/share/man/fr.ISO8859-1/man7/wireless.7.gz
/usr/share/man/man7
/usr/share/man/man7/wireless.7.gz

これでapt-get installを再実行する。

# apt-get install chkconfig
E: dpkg は中断されました。問題を修正するには 'dpkg --configure -a' を手動で実行する必要があります。

ファイルを手で直接いじったせいか、実行できなかったので、指示されたとおりのコマンドを実行する。

# dpkg --configure -a

再実行

# apt-get install chkconfig
(・・・略)
(データベースを読み込んでいます ... 現在 118819 個のファイルとディレクトリがインストールされています。)
.../chkconfig_11.4.54.60.1debian1_all.deb を展開する準備をしています ...
chkconfig (11.4.54.60.1debian1) を展開しています...
man-db (2.7.0.2-5) のトリガを処理しています ...
chkconfig (11.4.54.60.1debian1) を設定しています ...

今度は正常に完了した!

# which chkconfig
/sbin/chkconfig

ちゃんとchkconfigがインストールされた。

まとめ

なぜかapt-getであるパッケージのファイル一覧ファイルが壊れてしまったため、
そのパッケージに依存するパッケージのインストールに失敗してしまう、という問題が発生した。
ファイル一覧ファイル(xxx.list)を適当に拾ってきたものだが、差し替えたところ、正常にインストールできるようになった。

以上!

【ベイズ統計】HMC(ハミルトニアン・モンテカルロ)法をRで理解する

統計 R

ベイズ統計に関する以下の本を読んだ。

基礎からのベイズ統計学: ハミルトニアンモンテカルロ法による実践的入門

基礎からのベイズ統計学: ハミルトニアンモンテカルロ法による実践的入門

この本のゴールはHMC法(ハミルトニアン・モンテカルロ法)を習得することで、Rのサンプルコードも付いているのだが、理解を深めるためにRコードを実装しながらHMC法を動かしてみた。Rコードはサンプルコードの実装を読み解きながら、自分で理解しやすいようにときほぐすように実装してみた。
HMC法自体の解説はこの書籍の5章「ハミルトニアンモンテカルロ法」のほうに詳しく説明されているので、本記事で
は割愛。

リープフロッグ法

まずリープフロッグ法を実装する。
リープフロッグ法はある座標にいる物体に適当な運動量を加えて1ステップ移動させるという操作を
複数回の細かい移動を繰り返すことで、その物体の移動後の座標をシミュレーションするというもの。
移動回数(=移動時間)も与えているので、一定時間後の座標と運動量がステップごとに更新される。
計算のため、座標の勾配(つまり、運動量の時間微分)の関数を与えている。

# リープフロッグ法
# r: 運動量
# z: 座標
# D: 運動量の時間微分(勾配)の関数
# e: 細かさの精度
# L: 移動回数(=移動時間)
leapfrog <- function(r, z, D, e, L) {
  leapfrog.step <- function(r, z, e){
    r2 <-- e * D(z) / 2
    z2 <- z + e * r2
    r2 <- r2 - e * D(z2) / 2
    list(r=r2, z=z2) # 1回の移動後の運動量と座標
  }
  leapfrog.result <- list(r=r, z=z)
  for(i in 1:L) {
    leapfrog.result <- leapfrog.step(leapfrog.result$r, leapfrog.result$z, e)
  }
  leapfrog.result
}

leapfrog.stepがリープフロッグ法の中での”細かい”移動一回分を表していて、それをL回繰り返すことで1ステップの移動を実現する、というもの。
移動のたびに座標と運動量が更新され、最終的な座標と運動量を結果として返す。

HMC(ハミルトニアン・モンテカルロ法)

HMC法は物理学のアナロジーで、物理空間上をハミルトニアン(運動エネルギー + 位置エネルギー)が一定である、という制約を満たすような移動を複数ステップ繰り返すというもの。
リープフロッグ法はHMC法の中での1ステップの移動を実現するために用いる。
ここで、物理空間を確率分布とみなすと、母数は座標となり、HMC法で得られた各ステップの座標はその確率分布を満たすランダムサンプリングの結果だとみなすことができる。
ただしステップが収束するまではこの分布を表現していないため、破棄する必要がある。この期間をバーンイン期間という。

# ハミルトニアンモンテカルロ
# N: サンプリングする乱数の数(HMC法で移動するステップ数)
# ini: 母数ベクトルの初期値(初期座標)
# E: 対数尤度関数のマイナス(物理空間=サンプリングしたい確率分布上の位置エネルギーに相当する関数。計算しやすくするために対数をとり、確率の”高さ”を力学的な空間の”低さ”に対応づけるためにマイナス1倍する)
# D: 対数尤度関数の微分のマイナス(物理空間=サンプリングしたい確率分布の勾配。上と同じように対数化、マイナス1倍。この関数はリープフロッグ法で使う)
# L: 移動回数
# e: リープフロッグ法の細かさの精度
hmc <- function(N, ini, E, D, L=100, e=0.01){
  # ハミルトニアン
  # r: 運動量ベクトル
  # z: 座標ベクトル
  # E: 座標ベクトルから位置エネルギーを算出する関数(hmcのEと同じ)
  H <- function(r, z, E=E) {
    sum(r^2)/2 + E(z) # sum(r^2)/2: 運動エネルギー, E(z): 位置エネルギー
  }

  d <- length(ini) # 母数ベクトルの次元
  z <- matrix(0,N,d) # 母数の推定値(Nステップ後の座標に相当)
  r <- matrix(0,N,d) # Nステップ後の運動量に相当
  z[1,] <- ini # iniは母数ベクトルの初期値(1ステップ目の座標に相当)
  co <- 1 # 採択数(最初は採択)
  for(i in 2:N) { 
    r[i-1,] <- rnorm(d) # 運動量は独立なd個の標準正規乱数
    h <- H(r[i-1,], z[i-1,], E) # i-1ステップ後のハミルトニアン
    leapfrog.result <- leapfrog(r[i-1,], z[i-1,], D=D, e=e, L=L) # リープフロッグ法による1ステップ移動
    r2 <- leapfrog.result$r # 移動後の運動量
    z2 <- leapfrog.result$z # 移動後の座標
    h2 <- H(r2, z2, E) # 移動後のハミルトニアン
    # 移動前後でハミルトニアンの保存精度が十分高ければiステップ後の値として採択
    if (runif(1) < exp(h - h2)) {
      z[i,] <- z2
      co <- co + 1
    } else {
      # 採択されなければ留まる
      z[i,] <- z[i-1,] 
    }
  } 
  ac <- co / N # 採択率
  list(z=z, r=r, ac=ac)
}

最初にHというハミルトニアンを算出する関数を定義している。ハミルトニアンは運動エネルギーと位置エネルギーの和であり、運動エネルギーは(質量を1とみなすと)sum(r^2)/2の式で表現できる。
後の処理で、1ステップ後にこのハミルトニアンが移動前後で大きく変化していないかを確認することで、精度を高めている。

計算に使う入れ物(変数)を用意した後は、N回のリープフロッグによるステップ移動を繰り返す。
初期座標は入力として与えられ、移動量を表現する運動量は標準正規乱数から求められる。つまり、まったくランダムな力を加えて、それをリープフロッグ法によって移動後の座標を求める。

次に、移動前の座標と運動から算出したハミルトニアンと、移動後の座標と運動量から算出したハミルトニアンを比較する。
ここで、この変化が非常に少なければハミルトニアンの保存の精度が高く、このステップの移動は信頼性が高いとして採択される。採択されればその移動は採用されるし、採択されなければそのまま留まることになる。
こうして指定された回数分のステップが終わると、座標と運動量と採択率を結果として返す。

正規分布をサンプリングしてみる

HMC法が実装できたので、練習として書籍の例題と同じ平均170, 分散49 (σ=7)の正規分布の推定をN=100000, L=100, e=0.01として行ってみる。
正規分布の母数は平均と標準偏差の二つなので、母数は以下のようになり、これが物理空間上の座標に相当する。
f:id:osn_th:20160728133347p:plain
この2次元の確率分布の空間上の移動をシミュレートすることになる。

対数尤度関数、対数尤度関数の微分を計算

HMCで計算するためにDEに対応する関数を計算しておく。
今回は正規分布が対象なので、正規関数のlogをとってマイナス1倍したものがD
それをμとσ^2でそれぞれ微分し、logをとってマイナス1倍した2つの関数がEとなる。

ここの関数定義は書籍のものをそのまま利用した。
thetaは母数である平均μと分散σ^2の二つの要素を持つベクトル。

#正規分布モデル
#データの用意
set.seed(1234)
n<-100
x<-round(rnorm(n,170,7.0))
x
n<-length(x)

# 対数尤度関数のマイナス
lognorm <- function(theta){
  mu<-theta[1]
  sigma2<-theta[2]
  return(((n*log(sigma2)/(-2))-(sum((x-mu)^2)/(2*sigma2)))*(-1))
}
# 対数尤度関数の微分のマイナス
dmu <- function(theta) {
  mu<-theta[1]
  sigma2<-theta[2]
  return(sum(x-mu)/sigma2)
}
dsigma2 <- function(theta){
  mu<-theta[1]
  sigma2<-theta[2]
  return((-1*n)/(2*sigma2) + sum((x-mu)^2)/(2*sigma2*sigma2))
}
Dlognorm <- function(theta){
  return(c(dmu(theta),dsigma2(theta))*(-1))
}

正規分布の推定

以下のように推定する。初期座標は適当に、μ=168, σ^2= 49としたが、これは最終的には同じような値に収束するので値の選び方はあまり関係がない(はず)。

hmcを実行する。

# HMC実行
fit <- hmc(N=100000,ini=c(168,49),E=lognorm,D=Dlognorm,L=100,epsi=0.01)

z <- fit$z # 母数の推定値
print(fit$ac); # 採用率 => 1
zzz<-c(1001:100000) # バーンイン期間を取り除いた期間

hmc関数の計算は結構時間がかかって、MacBookAirで実行したところ3分程度かかった。

ヒストグラムを描いてみる。
まず、平均μについて確認する。

hist(z[zzz,1],breaks = 50,freq =TRUE)

f:id:osn_th:20160728133350p:plain
MAP推定値(最頻値)を求めると、168.9となっている。
最尤推定値が170なので少々左にずれているが、ほぼ近しい値となっている。

> rev(sort(table(round(z[zzz,1],1))))[1]
168.9 
 5586

続いて、分散σ^2を確認する。

hist(z[zzz,2],breaks = 50,freq =TRUE)

f:id:osn_th:20160728133352p:plain
MAP推定値(最頻値)を求めると、48.8となっている。
最尤推定値が49なので、ほぼ近しい値となっている。

> rev(sort(table(round(z[zzz,2],1))))[1]
48.8
 599  

平均と分散の値から、かなり正確に正規分布をシミュレートできているよう。

最後に、収束性を確認するためにトレースラインを確認しておく。

# 正規分布モデルのトレースライン
options(scipen=100)
par(mfrow=c(2,1))
plot(z[,1],type="l",cex.axis=1.5,xlab="t",ylab="μ",cex.lab=1.5)
plot(z[,2],type="l",cex.axis=1.5,xlab="t",ylab="σ2",cex.lab=1.5)
par(mfrow=c(1,1))

f:id:osn_th:20160728133354p:plain
ほとんど全体的に同じ幅の長さで推移していて、十分に収束しているよう。

まとめ

HMC法をRで実装し、正規分布のサンプリングを行ってみた。ほとんど教科書をなぞってみただけだが、自分で理解しながら再実装しつつ、計算したりすることでHMCの理解がより深まったように思う。
アルゴリズム自体はそれほど複雑ではないので実装は簡単だが、ちゃんと背景を理解していないと結果を理解するのが難しいと思う。
ただHMCは正規分布から10万個程度のサンプリングであっても結構遅いように感じた。

以上!

※参考にした本

基礎からのベイズ統計学: ハミルトニアンモンテカルロ法による実践的入門

基礎からのベイズ統計学: ハミルトニアンモンテカルロ法による実践的入門

Cloudera Managerのチャート機能でリソース使用状況を可視化する

Hadoop Cloudera Manager

前回、Hadoopのパフォーマンスを計測するベンチマークの使い方を見た。
totech.hateblo.jp

通常、パフォーマンス計測時には裏でdstatやsarのようなサーバのリソース使用状況を取得するツールを走らせておくが、これらはログとして保管したり報告資料を作る上では便利だが、計測中にリソースを眺める分にはやや見づらい。
そこで、Cloudera Managerのチャート機能を使えば、リアルタイムにリソース使用状況をグラフ化できてとても見やすくなる。
チャート機能はtsqueryというSQLのようなクエリを登録することで取得対象のリソースを定義したり、where句で取得対象を絞ったりできる。

以下に基本的な設定内容を以下にまとめる。

リソース tsquery
CPU select cpu_percent
Load Average select load_1
メモリ select physical_memory_used
スワップ select swap_used
ネットワークIn select bytes_receive_rate
ネットワークOut select bytes_transmit_rate
ディスクread select read_bytes_rate where roleType = DATANODE
ディスクwrite select write_bytes_rate where roleType = DATANODE

これで以下のようなグラフができる。
f:id:osn_th:20160720102332p:plain

tsqueryを使えば、その他にもさまざまな指標が取得できる。取得可能なメトリクスの一覧は以下のClouderaのドキュメントにのっているのでこれを参考にいろいろ組み合わせられる。
http://www.cloudera.com/documentation/enterprise/5-3-x/topics/cm_metrics.html

以上!

Hadoop関連書籍(過去に読んだ本)

Hadoop 第3版

Hadoop 第3版

Hadoop徹底入門 第2版 オープンソース分散処理環境の構築

Hadoop徹底入門 第2版 オープンソース分散処理環境の構築

Hadoopのベンチマーク計測サンプル(TeraSort, TestDFSIO)を動かしてみる

Hadoop

構築したHadoopクラスタの性能がどの程度なのかを知る上で、共通で標準的な計測手順があると便利だと思い調べてみたところ、Hadoopに標準でベンチマークを計測するスクリプトが用意されているようだったので、これを使ってみる。
サンプルはたくさんあるようだが、ベンチマークとして一般的に使えそうなものにTeraSortとTestDFSIOという2つがあった。
TeraSortはその名の通り、大量データのソート速度を図るもの。ソートなので主にCPUやメモリをベンチマークの対象としている。
もう一つのTestDFSIOは分散ファイルシステムのIOということで、HDFSへの書き込み/読み込みを図る。つまりディスクをベンチマークを対象としている。
今回はこのふたつのサンプルを実行させてみることにする。

なお、Hadoop環境はCDH 5.5がインストールされたHadoop環境で実施した。

TeraSort

TeraSortを実行するためには3ステップの手順を実行する。

  1. TeraGen: ソート用の入力データ生成
  2. TeraSort: ソート処理実行
  3. TeraValidate: ソートされた結果の検証

この3ステップのプログラムはhadoop-examples.jarというHadoop標準のjarとして用意されているので、まずはこれをhadoop jarで呼び出してみる。

$ hadoop jar /usr/lib/hadoop-0.20-mapreduce/hadoop-examples.jar
An example program must be given as the first argument.
Valid program names are:
 aggregatewordcount: An Aggregate based map/reduce program that counts the words in the input files.
 aggregatewordhist: An Aggregate based map/reduce program that computes the histogram of the words in the input files.
 bbp: A map/reduce program that uses Bailey-Borwein-Plouffe to compute exact digits of Pi.
 dbcount: An example job that count the pageview counts from a database.
 distbbp: A map/reduce program that uses a BBP-type formula to compute exact bits of Pi.
 grep: A map/reduce program that counts the matches of a regex in the input.
 join: A job that effects a join over sorted, equally partitioned datasets
 multifilewc: A job that counts words from several files.
 pentomino: A map/reduce tile laying program to find solutions to pentomino problems.
 pi: A map/reduce program that estimates Pi using a quasi-Monte Carlo method.
 randomtextwriter: A map/reduce program that writes 10GB of random textual data per node.
 randomwriter: A map/reduce program that writes 10GB of random data per node.
 secondarysort: An example defining a secondary sort to the reduce.
 sort: A map/reduce program that sorts the data written by the random writer.
 sudoku: A sudoku solver.
 teragen: Generate data for the terasort ★
 terasort: Run the terasort ★
 teravalidate: Checking results of terasort ★
 wordcount: A map/reduce program that counts the words in the input files.
 wordmean: A map/reduce program that counts the average length of the words in the input files.
 wordmedian: A map/reduce program that counts the median length of the words in the input files.
 wordstandarddeviation: A map/reduce program that counts the standard deviation of the length of the words in the input files.

hadoop-examplesは今回のTeraSort以外にもWordCountやモンテカルロによる円周率算出(pi)などさまざまなプログラムが用意されていることが分かる。

TeraGen

TeraGenはTeraSortのための入力データを生成する。HDFS上の作業ディレクトリを/user/mapred/benchmarks/terasortとし、これが作成済みであるとする。
このとき、以下のコマンドでTeraGenが実行できる。

$ hadoop jar/usr/lib/hadoop-0.20-mapreduce/hadoop-examples.jar teragen 10000/user/mapred/benchmarks/terasort/input

第1引数はteragenと指定する。
第2引数は生成データ行を数値で指定する。1行あたり100バイトとなるので、10000を指定した場合は1,000,000バイト、つまり1MBのファイルを生成する。
第3引数はデータの出力先のパスを指定する。

このコマンドを実行するとデータ生成のMapReduceジョブが実行される。
完了すると、指定したパスに以下のようにデータが生成される。

$ hadoop fs -ls /user/mapred/benchmarks/terasort/input
Found 3 items
-rw-r--r-- 1 mapredsupergroup     0 2016-07-19 15:51 /user/mapred/benchmarks/terasort/input/_SUCCESS
-rw-r--r-- 1 mapred supergroup  500000 2016-07-19 15:51 /user/mapred/benchmarks/terasort/input/part-m-00000
-rw-r--r-- 1 mapred supergroup  500000 2016-07-19 15:51 /user/mapred/benchmarks/terasort/input/part-m-00001

TeraSort

続いて生成したデータを実際にソートする。
以下のコマンドでTeraSortを実行する。

$ hadoop jar/usr/lib/hadoop-0.20-mapreduce/hadoop-examples.jar terasort /user/mapred/benchmarks/terasort/input/user/mapred/benchmarks/terasort/output

第1引数はterasortと指定する。
第2引数はソート対象の入力データパス、つまりTeraGenの出力先を指定する。
第3引数はソート結果を出力するパスを指定する。

コマンドを実行するとソートを行うMapReduceジョブが実行される。
TeraSortはベンチマークスクリプトだが、計測はやってくれないので、このときに処理時間やCPU、メモリ等のリソース使用状況は自分で取得する必要がある。
リソースの取り方はdstat等のOSコマンドで各ノードのリソース使用状況を確認したり、Cloudera Managerがあるならリソース画面で確認してもよい。
ジョブが完了すると、以下のように結果が出力されている。

$ hadoop fs -ls /user/mapred/benchmarks/terasort/output
Found 3 items
-rw-r--r-- 1 mapred supergroup     0 2016-07-19 15:58 /user/mapred/benchmarks/terasort/output/_SUCCESS
-rw-r--r-- 10 mapred supergroup     0 2016-07-19 15:57 /user/mapred/benchmarks/terasort/output/_partition.lst
-rw-r--r-- 1 mapred supergroup  1000000 2016-07-19 15:58 /user/mapred/benchmarks/terasort/output/part-r-00000

TeraValidate

TeraSortによるソート処理が正しく行われたかを判定する処理。
TeraSortの出力結果に対し、以下のようなコマンドを実行することで検証が行える。

$ hadoop jar/usr/lib/hadoop-0.20-mapreduce/hadoop-examples.jar teravalidate/user/mapred/benchmarks/terasort/output /user/mapred/benchmarks/terasort/validate

第1引数はteravalidateと指定する。
第2引数は検証対象の入力データパス、つまりTeraSortのソート結果出力先を指定する。
第3引数は検証結果を出力するパスを指定する。

これを実行すると、以下のように結果が出力される。

$ hadoop fs -ls /user/mapred/benchmarks/terasort/validate
Found 2 items
-rw-r--r-- 1 mapred supergroup     0 2016-07-19 16:01 /user/mapred/benchmarks/terasort/validate/_SUCCESS
-rw-r--r-- 1 mapred supergroup    22 2016-07-19 16:01 /user/mapred/benchmarks/terasort/validate/part-r-00000

出力されたファイルの中身を確認する。

$ hadoop fs -cat /user/mapred/benchmarks/terasort/validate/part-r-00000
checksum    139abefd74b2

このようにchecksumのみが出力され、errorが出力されていなければソートは正しく行われたと判断できる。
ソートが正しくない場合は、以下のようにerrorが結果に含まれる。

checksum    365ed3f3e1
error misorder in part-m-00000 between 6a 97 43 59 ea ab 3a 59 4d 99 and 63 b6 04 4b 8e 78 91 14 83 73
error misorder in part-m-00000 between 88 2a 02 c3 15 36 2b 60 76 5f and 5c 90 ab 38 ae 52 89 62 15 d7
(略)
error misorder in part-m-00001 between 6e 45 fb 3d 1c 2c fd d1 cd 57 and 3c b4 46 e3 07 0c 3d 50 d0 19
error bad key partitioning:
 file part-m-00000:begin key 4a 69 6d 47 72 61 79 52 49 50
 file part-m-00000:end key 2c b7 a1 bb 93 62 af 20 a4 d9

TestDFSIO

TestDFSIOはHDFS上にデータを書き込み、読み込みしてIOを計測するためのベンチマークスクリプト。書き込みと読み込みでそれぞれ独立した処理になっている。
実行ファイルは以下で、TeraSortで使ったjarとは異なるので注意。

$ hadoop jar /usr/lib/hadoop-mapreduce/hadoop-mapreduce-client-jobclient-tests.jar
An example program must be given as the first argument.
Valid program names are:
 DFSCIOTest: Distributed i/o benchmark of libhdfs.
 DistributedFSCheck: Distributed checkup of the file system consistency.
 JHLogAnalyzer: Job History Log analyzer.
 MRReliabilityTest: A program that tests the reliability of the MR framework by injecting faults/failures
 SliveTest: HDFS Stress Test and Live Data Verification.
 TestDFSIO: Distributed i/o benchmark. ★
 fail: a job that always fails
 filebench: Benchmark SequenceFile(Input|Output)Format (block,record compressed and uncompressed), Text(Input|Output)Format (compressed and uncompressed)
 largesorter: Large-Sort tester
 loadgen: Generic map/reduce load generator
 mapredtest: A map/reduce test check.
 minicluster: Single process HDFS and MR cluster.
 mrbench: A map/reduce benchmark that can create many small jobs
 nnbench: A benchmark that stresses the namenode.
 sleep: A job that sleeps at each map and reduce task.
 testbigmapoutput: A map/reduce program that works on a very big non-splittable file and does identity map/reduce
 testfilesystem: A test for FileSystem read/write.
 testmapredsort: A map/reduce program that validates the map-reduce framework's sort.
 testsequencefile: A test for flat files of binary key value pairs.
 testsequencefileinputformat: A test for sequence file input format.
 testtextinputformat: A test for text input format.
 threadedmapbench: A map/reduce benchmark that compares the performance of maps with multiple spills over maps with 1 spill

TestDFSIOを引数なしで実行するとUsageが見れる。

$ hadoop jar /usr/lib/hadoop-mapreduce/hadoop-mapreduce-client-jobclient-tests.jar TestDFSIO
16/07/19 17:46:10 INFO fs.TestDFSIO: TestDFSIO.1.7
Missing arguments.
Usage: TestDFSIO [genericOptions] -read [-random | -backward | -skip [-skipSize Size]] | -write | -append | -clean [-compression codecClassName] [-nrFiles N] [-size Size[B|KB|MB|GB|TB]] [-resFile resultFileName] [-bufferSize Bytes]

書き込み

1MBファイルを10個、つまり10MBの書き込みを実行するコマンドは以下のように実行する。

$ hadoop jar/usr/lib/hadoop-mapreduce/hadoop-mapreduce-client-jobclient-tests.jar TestDFSIO \
-D test.build.data=/user/mapred/benchmarks/TestDFSIO \
-write -nrFiles 10 -fileSize 1MB

-Dオプションでtest.build.dataプロパティに計測用のデータ書き込み先パスを指定する。
-writeオプションで書き込みであることを指定し、-nrFilesでファイル数、-fileSizeでファイルごとのサイズを指定する。

これを実行するとMapReduceジョブが開始されるが、TeraSortと違い、ジョブの最後にベンチマーク値が標準出力される。

(・・・略)
16/07/19 16:16:37 INFO fs.TestDFSIO: ----- TestDFSIO ----- : write
16/07/19 16:16:37 INFO fs.TestDFSIO:      Date & time: Tue Jul 19 16:16:37 JST 2016
16/07/19 16:16:37 INFO fs.TestDFSIO:    Number of files: 10
16/07/19 16:16:37 INFO fs.TestDFSIO: Total MBytes processed: 10.0
16/07/19 16:16:37 INFO fs.TestDFSIO:   Throughput mb/sec: 1.196888090963495
16/07/19 16:16:37 INFO fs.TestDFSIO: Average IO rate mb/sec: 1.5878732204437256
16/07/19 16:16:37 INFO fs.TestDFSIO: IO rate std deviation: 1.2619301662658684
16/07/19 16:16:37 INFO fs.TestDFSIO:  Test exec time sec: 51.268
16/07/19 16:16:37 INFO fs.TestDFSIO:

スループットやI/O、処理時間などが分かる。

読み込み

書き込みと同じように、ただオプションを-writeから-readに変えるだけでよい。

$ hadoop jar/usr/lib/hadoop-mapreduce/hadoop-mapreduce-client-jobclient-tests.jar TestDFSIO \
-D test.build.data=/user/mapred/benchmarks/TestDFSIO \
-read -nrFiles 10 -fileSize 1MB

こちらも同じように結果が標準出力される。

(・・・略)
16/07/19 16:20:28 INFO fs.TestDFSIO: ----- TestDFSIO ----- : read
16/07/19 16:20:28 INFO fs.TestDFSIO:      Date & time: Tue Jul 19 16:20:28 JST 2016
16/07/19 16:20:28 INFO fs.TestDFSIO:    Number of files: 10
16/07/19 16:20:28 INFO fs.TestDFSIO: Total MBytes processed: 10.0
16/07/19 16:20:28 INFO fs.TestDFSIO:   Throughput mb/sec: 74.6268656716418
16/07/19 16:20:28 INFO fs.TestDFSIO: Average IO rate mb/sec: 164.8916778564453
16/07/19 16:20:28 INFO fs.TestDFSIO: IO rate std deviation: 148.3374932963216
16/07/19 16:20:28 INFO fs.TestDFSIO:  Test exec time sec: 48.181
16/07/19 16:20:28 INFO fs.TestDFSIO:

クリーン

書き込みや読み込みで生成したデータは削除されないので、計測が終わった場合ややり直したい場合はクリーンコマンドを実行するとよい。
パラメータは以下のように実行する。

$ hadoop jar/usr/lib/hadoop-mapreduce/hadoop-mapreduce-client-jobclient-tests.jar TestDFSIO \
-D test.build.data=/user/mapred/benchmarks/TestDFSIO \
-clean

これで以下のように出力される。

(・・・略)
16/07/19 16:23:49 INFO fs.TestDFSIO: TestDFSIO.1.7
16/07/19 16:23:49 INFO fs.TestDFSIO: nrFiles = 1
16/07/19 16:23:49 INFO fs.TestDFSIO: nrBytes (MB) = 1.0
16/07/19 16:23:49 INFO fs.TestDFSIO: bufferSize = 1000000
16/07/19 16:23:49 INFO fs.TestDFSIO: baseDir = /user/mapred/benchmarks/TestDFSIO
16/07/19 16:23:50 INFO fs.TestDFSIO: Cleaning up test files

クリーンはtest.build.dataオプションで指定したディレクトリが削除される。

$ hadoop fs -ls /user/mapred/benchmarks/TestDFSIO
ls: `/user/mapred/benchmarks/TestDFSIO': No such file or directory

ベンチマーク値の見方の注意

読み込み・書き込みのベンチマーク値の見方に注意がある。
まず、計測コマンドを実行したあとの結果はHDFS上の以下に出力されている。

$ hadoop fs -cat /user/mapred/benchmarks/TestDFSIO/io_read/part-00000
f:rate 1648916.8
f:sqrate    4.91932768E8
l:size 10485760
l:tasks 10
l:time 134

ここではpart-*という名前のファイルがたくさんあるが、これはそのMapタスクで処理したファイルのサイズや処理時間の合計を表している。
一方、標準出力される方はこれらの集計である。

※参考: 以下のソースおよびメーリングリスト
https://github.com/facebookarchive/hadoop-20/blob/master/src/test/org/apache/hadoop/fs/TestDFSIO.java
http://mail-archives.apache.org/mod_mbox/hadoop-common-user/200901.mbox/%3C496EACE2.2090007@yahoo-inc.com%3E

16/07/19 16:20:28 INFO fs.TestDFSIO: ----- TestDFSIO ----- : read
16/07/19 16:20:28 INFO fs.TestDFSIO:      Date & time: Tue Jul 19 16:20:28 JST 2016
16/07/19 16:20:28 INFO fs.TestDFSIO:    Number of files: 10
16/07/19 16:20:28 INFO fs.TestDFSIO: Total MBytes processed: 10.0
16/07/19 16:20:28 INFO fs.TestDFSIO:   Throughput mb/sec: 74.6268656716418
16/07/19 16:20:28 INFO fs.TestDFSIO: Average IO rate mb/sec: 164.8916778564453
16/07/19 16:20:28 INFO fs.TestDFSIO: IO rate std deviation: 148.3374932963216
16/07/19 16:20:28 INFO fs.TestDFSIO:  Test exec time sec: 48.181

最後のTest exec time secはhadoop jarコマンド全体の実行時間となるため、並列性が考慮され、それによるオーバーヘッドが含まれる。
しかし結果ファイルのpart-*の方はMapタスクごとの時間の合計として算出されている。
標準出力のThroughput mb/secやAverage IO rate mb/secはこれらのpart-*ファイルの値の合計や平均を算出しているため、あくまでMapの平均であり、並列性は考慮されず、Reducerやオーバーヘッドが含まれない結果となるので注意。
要するに、ここでのThroughputは各MapのThroughputの平均値をとっているため、クラスタのスループットとならないことに注意が必要。
そのためクラスタスループットは、Total MBytes processed /Test exec time sec で近似するのがよさそう。
出力ファイルサイズを増やしていくと、Throughput mb/secの値はあるサイズからどんどん値が小さくなっていくが、これはMap数の平均スループットとなるため、頭打ちするのではなくどんどん値が小さくなっていく。
一方、Total MBytes processed /Test exec time sec で近似した値はそのサイズでほぼ頭打ちする数値となる。

まとめ

Hadoopのベンチマーク取得のためのTeraSortとTestDFSIOというふたつの例の動かし方を一通り見た。
ベンチマークの取り方を知っておくと、異なるHadoopクラスタを性能という観点で比較する場合や構築時の動作確認、もしくはノード追加やメモリ増設やクラスタパラメータ変更の効果を測定する場合に便利。

なお性能を見るときはCloudera Managerのリソース画面を見るのが一番見やすいと思う。機会があればこちらも記事にしたい。

(※2016/7/20追記)
Cloudera Managerのリソース画面について記事を書きました。
totech.hateblo.jp


以上!

Hadoop関連書籍(過去に読んだ本)

Hadoop 第3版

Hadoop 第3版

Hadoop徹底入門 第2版 オープンソース分散処理環境の構築

Hadoop徹底入門 第2版 オープンソース分散処理環境の構築

RubyでHadoop Streamingを動かしてみる

Hadoop Ruby

mHadoopでMapReduceジョブを実行するには最近はHiveを使うのが一般的だが、MapReduceを手軽に使うための方法としてHadoop Streamingがある。
これは標準入出力を利用してMapReduceジョブを実行できるというもので、Javaで複雑なコードを実装せずに手軽に試せるので便利。
Hiveばかり使っているとMapReduceジョブのレイヤーがブラックボックスとなってしまうけど、MapperやReducerの動き自体を理解する上でもわかりやすいので、今回はこれを試してみる。

ちなみに環境はCDH 5.5の入った4ノードのクラスタ環境で試す。

Hadoop Streamingを使うときはMapperとReducerのそれぞれの処理を実装する実行可能なファイルを用意する。
ここで、MapperやReducerは入力データを標準入力から受け取り、結果を標準出力するように実装するのがポイントとなる。
そのため、標準入出力さえできればなんだってよいので、perlだろうがシェルスクリプトでもHadoopが動かせる。便利。今回はRubyを使うことにする。

WordCount

分散処理といえばまずはWordCountを実装するのがならわしのようなので、文字数カウント処理をHadoop Streamingを使って実装してみる。
WordCountは入力にスペース区切りの単語が並んだテキストを持ち、それらのテキストごとの出現数を算出する。

Mapperを作る

まずはmapperを作る。

wc_map.rb

#!/usr/bin/ruby
def wc_map(line)
 line.chomp.split.map { |e| [e, 1] }
end

def output(records)
 records.each do |key, value|
  puts "#{key}\t#{value}"
 end
end

while l = STDIN.gets
 output(wc_map(l))
end

標準入力から1行ずつテキストを受け取り、それをスペースで分割しつつ(単語, 1)という配列を作る。
最後にそれらをタブ区切りで標準出力する。

まずは単体で実行してみる。

$ cat sample.txt
aa bb cc
bb cc dd
cc ee ff
$ cat sample.txt | ./wc_map.r
aa   1
bb   1
cc   1
bb   1
cc   1
dd   1
cc   1
ee   1
ff   1

期待どおりの結果がえられたので、次はこれ(実際はソートもされる)を標準入力から受け取り、単語ごとに集計した結果を標準出力するReducerを作る。

Reducerを作る

wc_reduce.rb

#!/usr/bin/ruby
def wc_reduce(line, results)
 key, value = line.chomp.strip.split
 results[key] += value.to_i
end

results = Hash.new(0)
while l = STDIN.gets
 wc_reduce(l, results)
end
p results

標準入力が単語、数値のタブ区切りなのでそれを分割し、単語をキーとするハッシュを作り、バリューの部分を足しあわせていく。
最後にハッシュを標準出力して終わり。

MapperとReducerができたので、これらをパイプで組み合わせて以下のようにテストする。Hadoop Streamingで分散する場合も同じように処理されるので、これが通ればプログラムレベルではHadoop Streamingで動く。

$ cat sample.txt | ./wc_map.rb | sort | ./wc_reduce.rb
{"ff"=>1, "cc"=>3, "ee"=>1, "bb"=>2, "dd"=>1, "aa"=>1}

うまく動いているよう!

Hadoop Streamingで実行

MapperとReducerができたので、これをHadoop Streamingで分散処理させてみる。
以下のようなコマンドで実行する。入力となるサンプルファイルはHDFS上に配置した上でそのパスを指定する。また、-fileで作成したMapperとRedcuerを指定する。

$ hadoop jar CDH-5.5.1-1.cdh5.5.1.p1168.923/lib/hadoop-0.20-mapreduce/contrib/streaming/hadoop-streaming-mr1.jar -input /user/mapred/example/stream/sample.txt -output /user/mapred/example/stream/wc_result -mapper wc_map.rb -reducer wc_reduce.rb -file wc_map.rb wc_reduce.rb
16/06/08 17:13:33 WARN streaming.StreamJob: -file option is deprecated, please use generic option -files instead.
packageJobJar: [wc_map.rb, wc_reduce.rb] [CDH-5.5.2-1.cdh5.5.2.p0.4/jars/hadoop-streaming-2.6.0-cdh5.5.2.jar] /tmp/streamjob8479180127713306959.jar tmpDir=null
16/06/08 17:13:34 INFO client.ConfiguredRMFailoverProxyProvider: Failing over to rm16
16/06/08 17:13:34 INFO mapred.FileInputFormat: Total input paths to process : 1
16/06/08 17:13:35 INFO mapreduce.JobSubmitter: number of splits:2
16/06/08 17:13:35 INFO mapreduce.JobSubmitter: Submitting tokens for job: job_1465349429352_0013
16/06/08 17:13:35 INFO impl.YarnClientImpl: Submitted application application_1465349429352_0013
16/06/08 17:13:35 INFO mapreduce.Job: The url to track the job: http://myhadoop04:8088/proxy/application_1465349429352_0013/
16/06/08 17:13:35 INFO mapreduce.Job: Running job: job_1465349429352_0013
16/06/08 17:13:39 INFO mapreduce.Job: Job job_1465349429352_0013 running in uber mode : false
16/06/08 17:13:39 INFO mapreduce.Job: map 0% reduce 0%
(・・・略)
16/06/08 17:13:48 INFO streaming.StreamJob: Output directory: /user/mapred/example/stream/wc_result

正常にMapReduceジョブとして実行できた。結果はHDFSのoutputオプションで指定したパスに出力されているので内容を確認する。

$ hadoop fs -cat /user/mapred/example/stream/wc_result/part-00000
{"ff"=>1, "cc"=>3, "ee"=>1, "bb"=>2, "dd"=>1, "aa"=>1}

さきほどコマンドラインでやったのと同じく、正しい結果が得られた!

分散データファイル生成

定型的なWordCountをHadoop Streamingで動かすことができた。次は、これを応用してランダムな文字列からなるファイル生成を分散させてやってみる。
こういった処理はHiveとかだとクエリが書きづらいので、Hadoop Streamingでやると簡単に書ける。

Mapperを作る

Hadoopの負荷分散の仕組みは、Mapperからの入力ごとにタスクを切り出して行うため、複数行の入力をあたえる必要がある。
そこで、今回は入力に各行に生成したランダムファイルの行数と行の長さが書かれたデータを想定する。
例えば、以下のような入力を与えると、10文字のランダム文字列が5行というファイルが2つ作られるような仕様とする。

5   10
5   10

Mapperの実装は以下。

gendata_map.rb

#!/usr/bin/ruby

def random_string(length)
 o = ("a".."z").to_a
 (0..length).map { o[rand(o.length)] }.join
end

def gendata_map(size, length)
 size.times do |i|
    puts random_string(length)
 end
end

# STDIN row : <row num> <row length>
STDIN.each_line do |line|
 size, length = line.chomp.split.map(&:to_i)
 size = 5 unless size
 length = 5 unless length
 gendata_map(size, length)
end

指定した長さのランダム文字列を作り、それを行数分だけ繰り返す。
ここでMapperのみでコマンドラインで単体テストを実施しておく。Hadoop StreamingはMapperやReducer単位で単体テストしやすいのがよい。

$ cat test.txt
10 2
10 2
10 2
$ cat test.txt | ruby gendata_map.rb
fkzuvyzib
lsvymrrlz
ecvtpsplx
wawdxfeps
lkstptsva
metunykpj

Mapperは意図どおりに動いているよう。

Reducerは不要

今回の処理はMapperだけで処理が完結しているので、このようなプログラムの場合はReducerは不要となる。

Hadoop Streamingで実行

WordCountのときと同じように実行する。ReducerがないのでReduceタスクは不要となる。
また、今回は行ごとに処理を分割したいので、map数とreducer数をオプションで明示して実行する(3行をそれぞれ別のMapperで処理させたいのでMap数は3とする)。

$ hadoop jar hadoop-0.20-mapreduce/contrib/streaming/hadoop-streaming-mr1.jar \
-D mapred.map.tasks=3 \
-D mapred.reduce.tasks=0 \
-input /user/mapred/example/gendata/input/input.txt -output/user/mapred/example/gendata/output -mapper gendata_map.rb -file gendata_map.rb

正常に処理が完了し、outputディレクトリを確認すると3つのMapが生成したそれぞれのファイルが作成されている。

$ hadoop fs -ls /user/mapred/example/gendata/output | head
Found 10001 items
-rw-r--r-- 3 mapred hadoop     0 2016-06-13 15:59 /user/mapred/example/gendata/output/_SUCCESS
-rw-r--r-- 3 mapred hadoop 20600000 2016-06-13 15:47 /user/mapred/example/gendata/output/part-00000
-rw-r--r-- 3 mapred hadoop 10300000 2016-06-13 15:47 /user/mapred/example/gendata/output/part-00001
-rw-r--r-- 3 mapred hadoop 10300000 2016-06-13 15:47 /user/mapred/example/gendata/output/part-00002

内容は以下のようになっている。

$ hadoop fs -cat /user/mapred/example/gendata/output/part-00000
dcvvxbxyu
fnijosqtz
$ hadoop fs -cat /user/mapred/example/gendata/output/part-00001
ocndfuctp
limwdnvke
$ hadoop fs -cat /user/mapred/example/gendata/output/part-00002
mdkewafhj
bcekxzcns

今回はReducerで集約していないので、最終的な出力結果はMapperそれぞれが出力したファイルが並ぶことになるが、Hive等でテーブルにする場合はこれで問題ない。
大量ランダムデータ生成のような処理をHadoopで分散処理させたい場合はHadoop Streamingを使うと便利な気がする。

まとめ

Hadoop Streamingを、簡単なRubyスクリプトで動かしてみた。MapReduceをJavaで実装するのは面倒だけど、Hiveではクエリが書きづらい、といった場合に、標準入出力のみ意識したコードをかけばHadoopクラスタで分散処理できるため、便利な機能だと思う。
また、Hadoop入門者がMapReduceの動きを理解する上で、コマンドラインとパイプでMapperとReducerそれぞれの処理のイメージがつかめるので、勉強用でもいいと思う。

以上!

Hadoop関連書籍(過去に読んだ本)

Hadoop 第3版

Hadoop 第3版

Hadoop徹底入門 第2版 オープンソース分散処理環境の構築

Hadoop徹底入門 第2版 オープンソース分散処理環境の構築

MacでR+MeCabによる形態素解析をやってみた

R

自然言語処理でおなじみの形態素解析ツールMeCabをMac上のRから使えるようにしてみた。今回は環境構築の手順をメモ。
環境情報は以下。

  • Mac OS X El Capitan (10.11.3)
  • MeCab 0.996
  • R 3.2.3 (2015-12-10)

MeCabインストール

MeCabは、macではbrewで簡単にインストールできる。
まず、brewからmecabを探す。

$ brew search mecab
mecab            mecab-ipadic     mecab-jumandic   mecab-ko         mecab-ko-dic     mecab-unidic
homebrew/php/php53-mecab          homebrew/php/php55-mecab          homebrew/php/php70-mecab
homebrew/php/php54-mecab          homebrew/php/php56-mecab

色々ヒットするが、mecabがMeCab本体のパッケージとなる。dicで終わっているやつはそれぞれが辞書を表していて、形態素解析を行うためには単語をあらかじめこのように辞書ファイルとして登録しておく必要がある。いろいろな機関が辞書を提供しており、用途によって使い分けがあるようだが、今回は標準的なIPAの辞書(ipadic)を使ってみる。
今回はmecabmecab-ipadicの二つをインストールする。

MeCab本体とIPA辞書をインストールする。

$ brew install mecab
$ brew install mecab-ipadic

インストールに成功。mecabコマンドが正しく実行できることを確認。

$ mecab -v
mecab of 0.996

ちなみに辞書がないと以下のエラーになる。

$ mecab
param.cpp(69) [ifs] no such file or directory: /usr/local/lib/mecab/dic/ipadic/dicrc

MeCabを使ってみる

まずRを介さず、直接MeCabを使ってみる。

mecabコマンドを実行すると、対話的に日本語を入力し、形態素解析の結果を表示してくれる。終了するときはCtrl-D。

$ mecab
今日はいい天気だ
今日    名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
いい    形容詞,自立,*,*,形容詞・イイ,基本形,いい,イイ,イイ
天気    名詞,一般,*,*,*,*,天気,テンキ,テンキ
だ      助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ
EOS
明日は天気が悪い
明日    名詞,副詞可能,*,*,*,*,明日,アシタ,アシタ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
天気    名詞,一般,*,*,*,*,天気,テンキ,テンキ
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
悪い    形容詞,自立,*,*,形容詞・アウオ段,基本形,悪い,ワルイ,ワルイ
EOS

ワンライナーで実行するときはechoをパイプでつなぐ。

$ echo "Rからmecabを使って形態素解析を行ってみたいと思う" | mecab
R       名詞,固有名詞,組織,*,*,*,*
から    助詞,格助詞,一般,*,*,*,から,カラ,カラ
mecab   名詞,一般,*,*,*,*,*
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
使っ    動詞,自立,*,*,五段・ワ行促音便,連用タ接続,使う,ツカッ,ツカッ
て      助詞,接続助詞,*,*,*,*,て,テ,テ
形態素  名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ
解析    名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
行っ    動詞,自立,*,*,五段・ワ行促音便,連用タ接続,行う,オコナッ,オコナッ
て      助詞,接続助詞,*,*,*,*,て,テ,テ
み      動詞,非自立,*,*,一段,連用形,みる,ミ,ミ
たい    助動詞,*,*,*,特殊・タイ,基本形,たい,タイ,タイ
と      助詞,格助詞,引用,*,*,*,と,ト,ト
思う    動詞,自立,*,*,五段・ワ行促音便,基本形,思う,オモウ,オモウ
EOS

さらに別の辞書を使ってみる。UniDic辞書をインストールする。

$ brew install mecab-unidic

辞書は以下のように追加されている。

$ ls /usr/local/lib/mecab/dic
ipadic unidic

インストールしたUniDic辞書に切り替えて実行するには、-dオプションにインストールした辞書のパスを指定する。

$ mecab -d /usr/local/lib/mecab/dic/unidic
今日はいい天気だ
今日    キョー  キョウ  今日    名詞-普通名詞-副詞可能
は      ワ      ハ      は      助詞-係助詞
いい    イー    ヨイ    良い    形容詞-非自立可能       形容詞  連体形-一般
天気    テンキ  テンキ  天気    名詞-普通名詞-一般
だ      ダ      ダ      だ      助動詞  助動詞-ダ       終止形-一般
EOS

先ほどのIPA辞書の結果と比べると、今回の例では単語の分割のされ方は同じだが、フォーマット・得られる情報が違うことが分かる。当然、文によっては分割自体が異なることもあり得る。

RでMeCabを使う

続いてRからMeCabを実行してみる。RMeCabというRパッケージを使うと簡単らしい。
Rを実行し、以下のようにインストールする。

install.packages ("RMeCab", repos = "http://rmecab.jp/R")

RMeCabの使い方は以下を参考
http://www.ic.daito.ac.jp/~mizutani/mining/rmecab_func.html


早速Rの中でMeCabを呼び出してみる。

library(RMeCab)
RMeCabC("今日はいい天気だ")
[[1]]
  名詞 
"今日" 

[[2]]
助詞 
"は" 

[[3]]
形容詞 
"いい" 

[[4]]
  名詞 
"天気" 

[[5]]
助動詞 
  "だ"

Rのlistとして、形態素解析結果が得られた。
また、他にもいくつか機能はあるが、多くはテキストファイルからの入力を前提としているので、テストファイルを用意する。

$ cat test.txt
今日はいい天気です。
明日の天気は雨の予報です。

これをRで以下のように呼び出す。

> RMeCabFreq("mecab/test.txt")
file = mecab/test.txt 
length = 10 
   Term  Info1    Info2 Freq
1  です 助動詞        *    2
2    は   助詞   係助詞    2
3    の   助詞   連体化    2
4  予報   名詞 サ変接続    1
5  天気   名詞     一般    2
6    雨   名詞     一般    1
7  今日   名詞 副詞可能    1
8  明日   名詞 副詞可能    1
9  いい 形容詞     自立    1
10   。   記号     句点    2

単語ごとの出現頻度が表示されている!

まとめ

MacにMeCabをインストールし、コマンドラインおよびRから実行できるところまで検証した。これで様々な自然言語の解析ができるようになったので、いろいろ試してみたい。

以上!

関連記事

R言語とImageMagickでgifアニメを作成

R

R言語で分布などをグラフィック生成される場合に、パラメータを少しずつ変えて微妙に変化するグラフィックをアニメーションさせて見てみたくなるときがよくある。
例えば、カイ二乗分布のような自由度ごとにグラフの形が大きく変わるようなものは、自由度を少しずつ変化させながらグラフの形状をアニメーションさせられるとより分かりやすい。教科書では、紙だとさすがにせいぜい2, 3個程度の代表的な値でのグラフを重ねて描くしかないが、R言語ならば動的なアニメーションを作成することも難しくなさそう。

手っ取り早くアニメーションを表示する方法として、gifファイルがシンプルでよさそう。ということで、今回は2通りの方法でRグラフィックをgifアニメとして作成してみた。

Rのanimationパッケージを使う

Rで作った画像をアニメーションとして作成するには、animationパッケージを使うのが一般的らしい。これを使うと、gifアニメーションだけでなく、HTMLやmpeg4などでの出力も可能らしい。ちなみにgifを作るためにはImageMagickというツールが必要なので、あらかじめインストールしておく。

事前にanimationパッケージをインストールする。

install.packages("animation")

そして、以下の様にsaveGIF関数の中にグラフィックを生成する処理を複数回呼び出す実装をすることで、アニメーションファイルが作成される。

library(animation)
saveGIF({
  ani.options(loop = TRUE)
  for (i in 1:5) {
    .main = paste("カイ二乗分布 自由度=", i, seq="")
    curve(dchisq(x, df=i), xlim=c(0,10), ylim=c(0, 0.3), main=.main)
  }
}, movie.name="chi.gif", interval=0.5)

この方法で、以下のようなgifアニメが作成できる。
f:id:osn_th:20160418085712g:plain

Rで画像を保存して直接ImageMagickを使う

animationパッケージが内部的に行っているのは、画像を出力し、ImageMagick(convertコマンド)で画像を一つのgifアニメーションファイルに変換しているので、同じことをR標準機能とコマンドでも行ってみる。

まず、以下のようにpng関数とdev.off関数でPNG画像ファイルを生成する。

for (i in seq(10, 100, 10)) {
  .main = paste("カイ二乗分布 自由度=", i, seq="")
  .file <- paste("out/chi", sprintf("%03d", i), ".png", sep="")
  png(.file)
  curve(dchisq(x, df=i), xlim=c(0,200), main=.main)
  dev.off()
}

このコードを実行することで、以下のようにoutディレクトリにpngファイルが作成される。

$ ls out
chi010.png chi030.png chi050.png chi070.png chi090.png
chi020.png chi040.png chi060.png chi080.png chi100.png

これをconvertコマンドで一つのgifファイルにまとめる。delayオプションでアニメーション切り替わりの間隔の時間を指定できる(単位は1/100秒なので、50を指定した場合は0.5秒)。

$ convert -delay 50 out/*.png out/chi2.gif
$ ls out/chi2.gif
out/chi2.gif

このコマンドで生成されたgifファイルは以下。
f:id:osn_th:20160416130726g:plain

まとめ

animationパッケージとImageMagickを直接使うことでRが生成したグラフィックをアニメーションさせる二通りの方法を見た。パラメータを少しずつ変えながら動的にグラフィックを表示させることで、よりデータの理解がしやすくなる場面も多そう。

以上