Memorandums?

This blog is written about technical-discoveries and daily-events.

Monty Hall Problemシミュレーション

What's モンティホール問題?

確率論(事後確率関連)でもっとも有名な問題のひとつがモンティホール問題です。
そんなモンティホール問題(Monty Hall Problem)とは、

これは、クイズ番組です。
3つの閉じられたドアがあります。
これらのドアのどれか1つを開けると、そこには高級車があり、それをもらえます。
しかし、間違ったドア(高級車の無いドア)を開けると、そこにはヤギしかおらず、挑戦は失敗となります。

挑戦者は、まず、1つドアを選びます。
すると、ドアのどれが正解かを知っている司会者が、
挑戦者が選んでいないドアかつ、正解でないドアを開け、こういいます。
「あなたが選ばなかったこのドアには、ヤギがいました。
今なら、まだ開けていないもう1つのドアに変更しても構いません。
どうしますか?」
このとき、挑戦者は、ドアを変えるべきでしょうか?
それとも、自分の決定を信じて、変えないほうがよいでしょうか?
(確率的にどちらが良いか)

という問題です。
この問題は、事後確率の問題であり、人間が直感で判断するのは、厳しい問題です。
IQが最も高いと言われるマリリン・ボス・サバント氏は、この問題を正解しました。
(この人が最初に答えを提唱したといわれている)

さて、確率はいくらでしょう?

Read more

Raspberry pi 導入

目的

RaspberryPiをディスプレイ・キーボード無しで導入し、使用します。
RaspberryPiをネット環境が無くても使えるように、PCと直接接続するだけで使用できるようにします。
RaspberryPiのデスクトップ画面(GUI)をPCから操作できるようにします。
RaspberryPiにて、PCをルーターとしてネットワーク接続ができるようにします。
※2015/07/30加筆訂正有

条件

  • Host-PC: Windows8.1
  • RaspberryPi: RaspberryPi2 ModelB
  • RaspberryPi-OS: 2015-05-05-raspbian-wheezy

必要なもの

  • ルーター(無線LAN・有線LAN)
  • 有線ストレートLANケーブル
  • Host PC
  • Raspberry pi 2
  • microSD(8GB以上が好ましい)
  • microSD-SD変換器(PCにmicroSDが入らない場合)
  • 5V1A供給ACアダプタ(端子はmicroUSB)

注意

RaspberryPi用として一切ディスプレイやキーボードを使用しません。
初期設定から全てホストPCから行います。
RaspberryPi2のRJ-45コネクタには、クロス-ストレート変換器が内臓されているようで、クロスLANケーブルは必要ありません。
※動作は保証しません

OSの書き込み

公式サイトからRaspbianOSのzipファイルをダウンロードします。
PCで解凍します。
次に、SDカードのフォーマットをします。
[Windows]+E でコンピュータを開き、SDカードを右クリック、フォーマットを選択します。
FAT32でフォーマットしてください。(データは全て削除されます)
Win32DiskImagerをダウンロードし、起動します。
ImageFileにはダウンロードしたRaspbianのイメージファイルを、ドライブにはSDカードを指定します。
Writeし、書き込みます。
書き込めたらRaspberryPiに差し込みます。

RaspberryPi起動

ネットワーク接続されたルーターにLANケーブルを差し、RaspberryPiのRJ-45コネクタと接続します。
microUSBを接続し、電源を入れます。
ホストPC側は無線LANが利用できるようにします。
[Windows]+R で検索画面を開き、[cmd]と入力し、コマンドプロンプトを起動します。
[ipconfig]と入力し、イーサネットの欄に出てきたIPアドレスを調べます。
例えば、192.168.0.3だった場合、192.168.0.2~192.168.0.15くらいまでの間におそらくRaspberryPiのIPアドレスがあるはずです。
(192.168.0.1はルーター自体のIPアドレスの可能性が高い)
それを調べます。
NetEnumなどのソフトウェアを使用してもいいですが、手打ち作業でもできます。
[ping 192.168.0.2]
と打ちます。"接続されていない"といった旨のメッセージが数秒おきに出てこれば、それは違います。
[ping 192.168.0.3]...
と続けていき、IPアドレスを調べます。
IPアドレスがわかったら、そこにSSHでログインします。
TeraTermをインストールしてください。
TeraTermで、IPアドレスに192.168.0.[調べたIPアドレス]と入力します。
SSHのセキュリティ確認画面が出てこれば成功です。
ユーザー名には[pi],パスワードには[raspberry]を入力します。

RaspberryPi初期設定

SSHに[raspi-config]と入力し、設定画面に行きます。
設定内容については、Googleで[raspberry pi raspi-config]などと検索して調べてみてください。
(今回それほど重要ではない)

DHCPに設定 固定IPアドレス付与

SSHにて、
[sudo vi /etc/network/interfaces]
と入力し、viの画面が出てきたら、
"
auto lo
iface lo inet loopback

auto eth0
allow-hotplug wlan0
iface eth0 inet manual

以下続く...
"
などと書かれていると思うので、

  1. "iface eth0 inet manual"を[iface eth0 inet dhcp static]に書き換える
  2. その文の下に次の文を加える
    address 192.168.0.[他とかぶらない番号]
    netmask 255.255.255.0
    dns-nameservers 192.168.0.1(←ルーターIPアドレス) 8.8.8.8

次に、わざわざIPアドレスを入力せずとも[raspberrypi.local]などと入力するだけでSSHログインできるように
[sudo apt-get install avahi-daemon]
と入力します。PC側には同様の理由でBonjourをインストールしておいてください。
(iTunesをインストールすると一緒にインストールされる)

できたら、RaspberryPiをシャットダウンします
[sudo halt]

RaspberryPi-PC直接接続

PC側の設定をします。
ネットワークと共有センターを開き、アダプター設定の変更を開きます。

Wifiを右クリックプロパティに行き、共有のタブをクリックし、一番上のチェックボックスにチェックを入れます。

イーサネットを右クリックプロパティに行き、IPv4を選択し、プロパティを開きます。
"次のIPアドレスを使う"を選択し、IPアドレスにはかぶらない任意のIPアドレスを指定し、
サブネットマスクには、255.255.255.0を指定します。(たぶん自動で入力される)

できたらOKでウィンドウを閉じます。

アダプター設定の変更の画面で、Wifiイーサネットを両方選択し、右クリックでブリッジ接続にします。
(RaspberryPiでネットワークを使用できるようにするため)

先ほど、ルーターとつないでいたLANケーブルを外し、PCとRaspberryPi間をそのLANケーブルで接続します。
そして、電源を入れます。
cmdで、IPアドレス(raspberrypi.local) (さきほどRaspberryPiでinterfacesファイルに書いた任意のIPアドレス)と接続を確認します。
[ping raspberrypi(ホスト名).local]
接続が確認できたら、TeratermSSH接続します。
これで、PC側が無線LANでネットワーク接続できているのであれば、RaspberryPi側のネットワークも接続されています。
以上でほとんど完了しました。

リモートデスクトップ(VNC)

RaspberryPiのデスクトップ画面をPCから操作できるようにします。
RaspberryPi側では、
[sudo apt-get update]
[sudo apt-get upgrade]
[sudo apt-get tightvncserver]
とし、インストールします。
[tightvncserver]
と入力し、起動させ、適当なパスワードを何回か打ちます。
PC側では、VNC-Viewer for Windowsをインストールし、exeを起動します。
IPアドレスには、
[192.168.0.[RaspberryPiのIPアドレス]:5901]
と書き、パスワードは先ほど決めたものを入力します。
すると、画面を見ることができるようになります。



感想

本当に導入には苦戦しました。導入だけで40時間くらい費やしてしまいました。。
ネット上には、RaspberryPi2の情報が少なく、異なる部分があったり、
当方、ネットワークの知識が薄く、なかなか理解できなかったりした部分があったためです。

今回特に苦戦した場面は、RaspberryPiの電源を切れず、やむなくUSBをぶち抜き、SDカードのメモリを破壊してしまうことです。
正しい手順でやればそのようなことはないのですが、
失敗すると、PCからアクセスできなくなり、シャットダウンシグナルが送れないためにぶち抜かなくてはいけない場面が発生することがあります。
2回初期化しましたが、その後は反省し、シャットダウンボタンをつくりました。
RaspberryPiにスクリプトを組み、起動時自動スクリプトにし、
タクトスイッチのみの回路をGPIOと接続し、PCなどなにもなくてもシャットダウンできるようにしました。
様々なサイトではGPIOの真ん中あたりのピンを使っており、他に回路を作るときに邪魔になりそうですが、
今回は、GPIO21とGNDに回路を作ったため、邪魔にもなりそうになく、満足しています。
これで失敗を恐れることがなくなり(笑)、直接接続に向けて奮闘したところ、なんとかできた、といった感じです。


RaspberryPi2では、デフォルトの/etc/network/interfacesはeth0がmanualと設定されているため、それをDHCPにする作業が必要(?)のようです。
それができれば、PCとRaspiの接続はもちろん、Windows側でネットワーク共有をするだけでRaspberryPiのネットワーク接続もできてしまいます。

RaspberryPiがもし壊れたりしても責任はとれませんが、不明な点があればコメントにてどうぞ。(誤植・アドバイスもお願いします)

InputStreamReaderでの文字列の受信

InputStreamReaderは、char文字(単一文字)単位でしかデータを受け取れません。
そのため、改行までを文字列として取得するのは、それなりに大変です。
今回は、文字列に変換する方法を示します。

それならBufferedReaderのreadLineメソッドで余裕じゃん!
といいたいところかもしれませんが、その理由については後ほど(笑)

文字列に変換するポイントは、StringBuilderを使うことです。
StringBuilder#appendによって、一文字づつ追加ができます。

コードを示します。

StringBuilder sb = new StringBuilder();
while ((data_tmp = in.read()) != '\n' && data_tmp != -1) {
	sb.append((char)data_tmp);
}
data_str = new String(sb);
sb.setLength(0);

まずは、InputStreamReader#readによってchar文字を読み取ります。
それが改行コードや、終了コードでないならば、
StringBuilderに追加します。
全て追加が終わり、ループを抜けた後、String型に戻して完成です。
おそらくこの処理は、常時受信処理でしょうから、次の受信のためにStringBuilderを空にするのを
忘れないようにします。

ちなみに先ほど'改行コード'とイイましたが、
Windowsの場合は、改行コードは"\r\n"です。
この区別については、前々回のエントリーの
meriyasu-blog.hatenablog.com
に書いています。
参考になれば幸いです。

なんやかんやで簡単にInputStreamReaderの文字列化ができてしまいました。

先延ばしにした、なぜBufferedReaderを使わないのか、ですが、

BufferedReader br = new BufferedReader(new ObjectInputStream());

ということができず、InputStreamReaderならば、

InputStreamReader isr = new InputStreamReader(new ObjectInputStream());

ということができるからです!!

前回のエントリー、
meriyasu-blog.hatenablog.com
でも述べたように、今回、画像転送も必要としました。

しかし、もちろんBufferedReader, InputStreamReaderなどでは画像送信はできず、
ObjectInputStreamを使用する必要があります。

サーバークライアント通信において
一つのクライアントとサーバーを繋ぐソケットは基本的に一つです。
そのソケットのInputStreamも一つです。

そのため、ObjectInputStream用にsocketのInputStream,
BufferedReader用にsocketのInputStreamとするのはよくないです。
(closeするときなどに問題が起きるため)

よって、socketのInputStreamを一つにするため、

ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
InputStreamReader in = new InputStreamReader(ois);

とできるInputStreamReaderを採用しました。
(そうしないと自分が実装できないだけで、できるのかもしれません())

ImageIO#readのBugSolver(代替方法)

画像送信プログラムを作製中、、

Bug ID: JDK-4821108 IIOException thrown when reading PNG images

このバグ報告が出ていることに暫く気づかず、ずっとImageIOクラスで戦っていました。
しかしながら、Bugにより、ImageIO#readを使用しての画像送信は無理です。

そこで様々な解決策を考えました。
真っ先に思いつくのが、画像をByte配列に変換し、送信し、デコードして復元する。
ByteArrayOutputStreamなどを使用すれば、変換することはできます。
しかし、複雑で送信に時間がかかったため、非採用としました。

もうひとつの方法として、ObjectOutputStreamを使用し、オブジェクトとして送信することです。
Imageオブジェクトを送信しても、読み込み側でImageIOを使用しなくてはいけないため意味がありません。
そこで、BufferedImageオブジェクトをObjectOutputStream#writeObjectに乗せて送信することを思いつきました。
しかし、まだ問題はあります。
それは、ObjectOutputStreamは直列化(serialize)されたオブジェクトしか扱えないということです。
BufferedImageはSerializeされていないため、そのまま送ることは不可能です。
そこで、自作クラスでラップし、そのクラスをSerializeしてみます。

Serializeする独自クラスは以下のようになります。

import java.awt.image.BufferedImage;
import java.io.Serializable;

public class BufferedImageSerializer implements Serializable {

	private static final long serialVersionUID = 1L;
	
	private int width, height;
	private int[] pixels;
	
	/**
	 * 受け取ったBufferedImageクラスのインスタンスの情報を収集します。
	 * @param image 送信したいBufferedImageクラスインスタンス
	 */
	public BufferedImageSerializer(BufferedImage image) {
		width = image.getWidth();
		height = image.getHeight();
		pixels = new int[width * height];
		image.getRGB(0, 0, width, height, pixels, 0, width);
	}
	
	/**
	 * BufferedImageクラスのインスタンスを受け取ります。
	 * 画像受信元にて使用します。
	 * @return 送信されたBufferedImageインスタンス
	 */
	public BufferedImage getImage() {
		BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		image.setRGB(0, 0, width, height, pixels, 0, width);
		return image;
	}
	
}

利用方法はコードを示すまでもなく、
画像を送信する側は、コンストラクタにBufferedImageのインスタンスを指定する。
画像を受信する側は、getImageで取得する。
これでOKです。

サーバークライアントソフトウェアはこの問題を最後についにリリース版となりました!!
これからもバージョンアップを重ねていきます。

クロスプラットフォーム問題

マルチプラットフォームに対応しているJavaであっても、
OSの違いに気をつけなくてはいけない場面があります。

まず、OSを認識するプログラムです。

public String getOS() {
   if (System.getProperty("file.separator").equals("\\"))
      return "WINDOWS";
   else
      return "UNIX";
}

ファイルの区切り文字から認識することができます。

次に、InputStreamReader#readを用いて1文字づつ認識するための方法を紹介します。
1文字づつ認識するにあたっての問題点は、改行コードです。
Windowsでは'\r\n', Unixでは'\n'となっており、これを区別しなければなりません。

#nextLineProcedure

public int nextLineProcedure(String os) throws Exception {
   switch (os) {
   case "WINDOWS":
      if ((data_tmp = in.read()) == '\r')
         return (((data_tmp2 = in.read()) == '\n') ? -1 : -2);
      break;
   case "UNIX":
      if ((data_tmp = in.read()) == '\n')
         return -1;
   break;
   }
   return -3;
}

このメソッドを使い、
-1が返却された場合は、読み込まない。
-3が返却された場合は、data_tmpのみを読み込む。
-2が返却された場合は、data_tmpとdata_tmp2を読みこめばOKです。

Windowsとcmdにおける文字処理

Java Windowsプログラムを書いているときに、
なかなかWindowsOSとcmdには苦戦しました。

Windowsでできること

  • ファイル名にゼロ幅文字可能
  • ゼロ幅文字を付加すれば、同名ファイルとならない

Windowsでできないこと

  • ファイル名に特殊文字(",/,\,|,<,>,:)が付けられない
  • 認識できるUnicode上にゼロ幅文字が存在しない
  • ファイル名の末尾に半角空白文字を付けられない
  • 同名ファイルはNG

cmdでできること

cmdでできないこと

  • ゼロ幅文字関連Unicodeは認識不可能
  • 制限される記号文字が多い


以上を念頭において、Javaプログラミングをしなくてはなりません。
今回のプログラムは、

1ファイルを1メッセージとし、ディレクトリ内のファイル一覧を更新し続けることで
コンソール(cmd)上でチャットをおこなう

というものです。

問題1. 同じメッセージ

2個以上同じメッセージがあるときの処理が問題です。
まず、メッセージをファイルに保存しなくてはなりません。
これは、同名メッセージの回数分、ゼロ幅文字を文字列終端に付加することによって解決!!!
と、思いきや!!!
さすがは、ダメダメOS, Windowsです。
ゼロ幅文字Unicodeが認識しません、ダメでした。(Unix環境なら可)
仕方ない、半角スペースを後ろにつけよう!
と、、ダメです、ファイル名の後ろには半角スペースは付けられません。
ということで、結局全角スペースを後ろにつけることで解決しました。
全角スペースは、プログラム中に" "なんて書かずに、'\u3000'と書くことをおすすめします。
そして、コンソール表示時の問題。
全角スペースをつけていると、同じメッセージが大量に来た時、
コンソール上で改行されてしまいます。
よっぽで無い限り、改行しませんが、やっぱりここは完璧主義。全角スペースはなくしましょう。
これは簡単です。

str = str.replaceAll('\u3000'+"+$", "");

正規表現でらくらく解決♪

問題2. 特殊文字対応

特殊文字(",/,\,|,<,>,:)はファイル名につけられないため、
メッセージに打たれると、エラーとなります。ただし!!!
全角はOKです。全角に変換します。コロンの場合、

str = str.replace('\u003a', '\uff1a');

これでOKです。
表示時も全角で表示することを防ぐため、
表示するときは、逆に戻します。

問題3. スケジュール実行の一時停止

一定時間おきに実行させるスレッドを一時停止させる場合、
TimerTaskでは通用しません。

Runnable task = new Read();
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture future = scheduler.scheduleAtFixedRate(task, 0, time, TimeUnit.MILLISECONDS);

これで大丈夫です。

問題4. ファイルタイムテーブルの管理

ファイル名と作製時刻を同期させるのに、はじめに思いつくのはMapです。
ただ、Mapはソートが難しいです。
TreeTableなどなら、Keyをテンプレートにしたがって、ソートできますが、
非常に面倒です。
そこで、リストで実装しました。

time = new ArrayList<>();
msg = new ArrayList<>();
list = dir.listFiles();

for (int i=0; i < list.length; i++) {
	lastModified = list[i].lastModified();
	title = list[i].getName();

	if (uploadTime >= lastModified) {
		continue;
	}
	if (time.isEmpty()) {
		time.add(lastModified);
		msg.add(title);

		continue;
	}
	int size = time.size();
	for (int j=0; j < size; j++) {
		if (lastModified < time.get(j)) {
			time.add(j, lastModified);
			msg.add(j, title);
			break;
		} else if (j == size-1) {
			time.add(lastModified);
			msg.add(title);
			break;
		}
	}
}
for (int i=0; i < msg.size(); i++) {
	System.out.println(msg.get(i));
}
if (time.size() != 0)
	uploadTime = time.get(time.size()-1);

こんな感じで実装できます。

問題5. 諸問題

Fileクラスからディレクトリ生成する場合、
最後に\を付けないほうがいいです。たとえば、

C:\Users\Owner\Folder

とします。そのフォルダにファイルを作る場合は、

DIR + File.separator + File.txt

などとします。



結論

Windowsとcmdは面倒。