2013-12-17

iOS WebViewアプリ勉強会の資料と補足 #vgadvent2013

この記事はVOYAGE GROUP エンジニアブログ: Advent Calendar 2013の15日目です。

先日社内で行なった勉強会の資料をSlide Shareにアップしました。口頭で説明した内容を追加してあります。


補遺

前提としてiOSのWebView(ハイブリッド)アプリでネイティブアプリになるべく近いLook and Feelを実現する際の話です。

ネイティブとの連係

直近のプロジェクトではコードベースを最小にしたかったので、PhoneGapを使わなかったのですが、最新のPhoneGapは必要な機能を切り出してビルドして使えるみたいですね。素晴らしい。あまり自分で実装したくない所なので、既存で使える物をなるべく使いましょう。

なるべくブラウザ(Chrome)でデバッグする理由

iPhoneシミュレーター、実機ではデバッグしづらいからです。

touchendとclickイベント

clickイベントが遅延する対策としてtouchendを使っても良いと説明しましたが、touchendイベントとclickイベントは発動条件が異なります。具体的にはタッチした後に別の要素まで指を動かして離すとtouchendはFireするが、clickイベントは飛びません。タップしたけどやっぱ辞めた、という動きはclickイベントを使った方が意図した通りになります。
私はFastClick.jsを有効にして、タッチの処理はclickイベントをハンドルしています。

パフォーマンス周り

最後のまとめで覚悟が必要とさらっと書いてありますが、どういう事かと言うと。ネイティブであればiOS SDKが標準で用意してくれる、少ないメモリ、貧弱なCPUでUIを構築するための仕組み、例えばUITableCellの再利用やGrand Central Dispatch(GCD)がHTML5/JavaScriptには無いです。独自で実装したり、WebWorkerを利用してバックグラウンドプロセスで動かすという手もありますが面倒な事に変りはありません。そのあたりをカバーしつつ、レンダリングエンジンの気持ちになって考え、DOM操作を最適化していくという覚悟が必要、という意味です。Androidに比べればWebViewがサクサク動いてくれる、というのが唯一の救い。
初回リリース時は工数のかかるパフォーマンス、アニメーションのチューニングを潔く諦めるのもアリかなと思ってます。いつでも直せるのがメリットなので。

それ以外に大変な事

やった事はありませんが、オリエンテーション対応が必要という場合はもうSencha Touchを使った方が良いと思います。2.3でiOS 7のスタイルも追加されました。個人的にはjQuery mobileよりもおすすめ。学習コスト高いけど。

Advent Calendar明日の担当は@shinbashiさんです。既に記事が公開済みという手筈の良さ、流石ですね。

このエントリーをはてなブックマークに追加

2013-11-30

wheelを使ってPythonのC拡張モジュールを本番デプロイする

Pythonの話。wheelを使ってC拡張モジュールをデプロイする仕組みが上手く稼動したのでメモ。

依存パッケージの本番デプロイ

アプリケーションが依存しているPythonパッケージをどうやって本番サーバーにデプロイするか。大抵はrequirements.txtにpip freezeで吐いた内容を保存しているだろう。とすると、本番サーバーでpip install -r requirements.txtすれば良いんだが、githubが落ちてたりPyPIが落ちてたりすると、外部要因でデプロイスクリプトが途中でコケる、というダサい事態になる。それを避けるために事前にパッケージを固めて各サーバーに配布する仕組みが必要になる。C拡張モジュールを使いたいけど本番サーバーでCコンパイラが自由に使えない、という時も同様で、事前にコンパイル済みの物を配布する必要がある。

pip bundleを使う場合 (deprecated)

Pure Pythonなパッケージだけを使っている場合はこれで十分だろう。
$ pip bundle -r requirements.txt myproduct.bundle
で、myproduct.bundleを作成しておいて、デプロイ先で
$ pip install myproduct.bundle
とする。しかし、C拡張モジュールがあると、デプロイ先でもりもりコンパイルが始まってしまって非効率なのと、そもそもpip 1.5でpip bundle自体が削除予定なので今後は忘れても良い機能だ。

pip wheel でコンパイル済みC拡張モジュールをデプロイする

これが本命、wheelというフォーマットでパッケージを配布する方式。
私はCIサーバーでwheelを作って本番サーバーに転送して使っている。手順は、まず事前に各サーバーでwheelを導入しておく。
$ pip install --upgrade pip        # 1.4以上が必要
$ pip install --upgrade setuptools # 0.8以上が必要
$ pip install wheel
wheelの作成は次の通り。慣例的にwheelhouseというディレクトリ名が使われる様だ。
# requirements.txtがこんな内容だとする
$ cat requirements.txt
MySQL-python==1.2.4
python-memcached==1.53
boto==2.14.0
simplejson==3.3.1

# wheelの作成
$ pip wheel --wheel-dir=./wheelhouse -r requirments.txt
Downloading/unpacking MySQL-python==1.2.4 (from -r requirements_prod.txt (line 1))
  Downloading MySQL-python-1.2.4.zip (113kB): 113kB downloaded
  Running setup.py egg_info for package MySQL-python
    Downloading http://pypi.python.org/packages/source/d/distribute/distribute-0.6.28.tar.gz
    Extracting in /tmp/tmpKuwtk2
    Now working in /tmp/tmpKuwtk2/distribute-0.6.28
    Building a Distribute egg in /web/httpd_spc/chatparty_api/python/build/MySQL-python
    /web/httpd_spc/chatparty_api/python/build/MySQL-python/distribute-0.6.28-py2.6.egg
Downloading/unpacking python-memcached==1.53 (from -r requirements_prod.txt (line 2))
  Downloading python-memcached-1.53.tar.gz
  Running setup.py egg_info for package python-memcached
    warning: no files found matching '*.rst'
    warning: no files found matching '*.txt'
    warning: no files found matching 'MakeFile'
    warning: no previously-included files matching '*.pyc' found anywhere in distribution
    warning: no previously-included files matching '.gitignore' found anywhere in distribution
    warning: no previously-included files matching '.DS_Store' found anywhere in distribution
Downloading/unpacking boto==2.14.0 (from -r requirements_prod.txt (line 3))
  Downloading boto-2.14.0.tar.gz (1.1MB): 1.1MB downloaded
  Running setup.py egg_info for package boto
    warning: no files found matching 'boto/mturk/test/*.doctest'
    warning: no files found matching 'boto/mturk/test/.gitignore'
Downloading/unpacking simplejson==3.3.1 (from -r requirements_prod.txt (line 4))
  Downloading simplejson-3.3.1.tar.gz (67kB): 67kB downloaded
  Running setup.py egg_info for package simplejson
Building wheels for collected packages: MySQL-python, python-memcached, boto, simplejson
  Running setup.py bdist_wheel for MySQL-python
  Destination directory: /web/httpd_spc/chatparty_api/wheelhouse
  Running setup.py bdist_wheel for python-memcached
  Destination directory: /web/httpd_spc/chatparty_api/wheelhouse
  Running setup.py bdist_wheel for boto
  Destination directory: /web/httpd_spc/chatparty_api/wheelhouse
  Running setup.py bdist_wheel for simplejson
  Destination directory: /web/httpd_spc/chatparty_api/wheelhouse
Successfully built MySQL-python python-memcached boto simplejson
Cleaning up...
生成されたファイル名を見ればわかる様に、wheelはPythonバージョンとアーキテクチャ毎に作られる。
$ ls -l wheelhouse/
boto-2.14.0-py26-none-any.whl
MySQL_python-1.2.4-cp26-none-linux_x86_64.whl
python_memcached-1.53-py26-none-any.whl
simplejson-3.3.1-cp26-none-linux_x86_64.whl
wheelを使ったインストールはpip installでwheelの場所を指定するだけ。一瞬で終って気持ちがいい。依存関係は既に解決済みなので --no-deps オプションを使う。
$ pip install --no-deps wheelhouse/*
Unpacking ./wheelhouse/MySQL_python-1.2.4-cp26-none-linux_x86_64.whl
Unpacking ./wheelhouse/boto-2.14.0-py26-none-any.whl
Unpacking ./wheelhouse/python_memcached-1.53-py26-none-any.whl
Unpacking ./wheelhouse/simplejson-3.3.1-cp26-none-linux_x86_64.whl
Cleaning up...



このエントリーをはてなブックマークに追加

2013-11-24

DjangoでMySQLにunicode絵文字を登録できるようにする(utf8mb4対応)

Djangoを使っているプロジェクトでMySQLにunicode絵文字を投入したくなったので。

Unicode絵文字

iOSで使える絵文字キーボードに含まれる絵文字はUTF-8で符号化した時に4バイトになる。UTF-8で符号化した時に4バイトになるのは一部も漢字もそうだが具体的にはこのあたり。
MacOSXだとことえりで「ハート」を変換すると1F493のBEATING HEARTあたりが出せる。Pythonでコードポイントを表示してみると次の通り。
(MySQL以前にUSC2だとサロゲートペアになるのでPythonを --enable-unicode=ucs4 でコンパイルしてないとおかしな結果になるかも。)

MySQLのcharcter setのutf8は3バイトまでの文字しか扱えないため、MySQL5.5で4バイトの文字を扱えるようにしたutf8mb4というcharcter setが追加された。

データベースのdefault charsetをutf8mb4にする

python manage.py syncdb
によって生成されるCREATE文はDEFAULT CHARSET指定が無い、よってデータベース作成時に指定しておくのが良いだろう。python Djangoチュートリアルを例にすると
mysql> create database django_tutorial default charset utf8mb4;
Query OK, 1 row affected (0.00 sec)

settings.pyのDB接続オプションでutf8mb4を指定する

DATABASES = {
            'default': {
                'ENGINE': 'django.db.backends.mysql',
                'NAME': 'mydatabase',
                'USER': 'root',
                'PASSWORD': 'password',
                'HOST': 'localhost',
                'PORT': '3306',
                'TIME_ZONE': '+09:00',
                'OPTIONS': {
                    'charset': 'utf8mb4'
                }
            }
}
これでunicode絵文字のINSERT、SELECTが上手くいくようになる。
Djangoチュートリアルで試すとこの通り。


このエントリーをはてなブックマークに追加

2013-11-22

QiitaにPythonネタをいくつか投稿してみた

MacのmarkdownメモクライアントのKobitoを使ってると圧倒的使い易さ。Bloggerもこういうの欲しい。



このエントリーをはてなブックマークに追加

2013-10-05

botoを使ってAWS SNSのMobile Pushを利用する (APNs編)

boto自体使うのが初めてだったので、おかしい所があるかもしれないが動作はした。APNsのSSL証明書作成等は毎度の事なので省略。Application ARNはAWS Consoleから既に作成してあるとする。

デバイス指定でメッセージ送信する

# -*- coding: utf-8 -*-

import boto.sns

AWS_ACCESS_KEY = 'xxxx'
AWS_SECRET_ACCESS_KEY = 'yyyyyyyy'
APPLICATION_ARN = \
    'arn:aws:sns:ap-northeast-1:0000:app/APNS_SANDBOX/aaaa'

# SNSに接続
sns_connection = boto.sns.connect_to_region('ap-northeast-1',
        aws_access_key_id=AWS_ACCESS_KEY,
        aws_secret_access_key=AWS_SECRET_ACCESS_KEY)

# 送信先デバイスを指定してendpointを作成
res = sns_connection.create_platform_endpoint(
        platform_application_arn=APPLICATION_ARN,
        token='xxxxxxxx')
endpoint = res.get('CreatePlatformEndpointResponse')\
              .get('CreatePlatformEndpointResult').get('EndpointArn')

# endpointに送信
sns_connection.publish(target_arn=endpoint, message=u"Hello World")
endpointは一度作成したら、保存しておけば再度 create_platform_endpoint する必要は無い。

Topic経由でメッセージ送信する

Topicをsubscribeしているデバイスに一斉送信するパターン、Topicをsubscribeする際の第二引数はbotoのソースを読んでもわからなかったが、AWSコンソールのTopic管理画面から推測すると "application" を指定したら良い気がする。そして実際に動作する。
# -*- coding: utf-8 -*-

import boto.sns

AWS_ACCESS_KEY = 'xxxx'
AWS_SECRET_ACCESS_KEY = 'yyyyyyyy'
APPLICATION_ARN = \
    'arn:aws:sns:ap-northeast-1:0000:app/APNS_SANDBOX/aaaa'

# SNSに接続
sns_connection = boto.sns.connect_to_region('ap-northeast-1',
        aws_access_key_id=AWS_ACCESS_KEY,
        aws_secret_access_key=AWS_SECRET_ACCESS_KEY)

# 送信先デバイスを指定してEndpointを作成
res = sns_connection.create_platform_endpoint(
        platform_application_arn=APPLICATION_ARN,
        token='xxxxxxxx') 
endpoint = res.get('CreatePlatformEndpointResponse')\
              .get('CreatePlatformEndpointResult').get('EndpointArn')

# Topicを作成
res = sns_connection.create_topic('test_topic')
topic = res.get('CreateTopicResponse')\
           .get('CreateTopicResult').get('TopicArn')

# TopicをSubscribe
sns_connection.subscribe(topic, 'application', endpoint)

# Topicに送信
sns_connection.publish(topic=topic,
                       message=u"Hello World for Topic subscriber")

Topicにぶらさげるデバイスの数は1万が上限との事なので、アプリのユーザー全員に送信するといった場合にはTopicを分けるか、全てのEndpointに送信するかどちらかになる。あまり楽させてはくれない様だ。Endpointを削除した場合、Topicから勝手にunsubscribeされると思いきやそうでもなかった。

JSON形式でbadgeやalertを送る

    # Payloadの中身
    message = json.dumps({
        "aps": {
            "alert": {
                "body": "Hey Hey",
                "action-loc-key": None
                },
            "badge": 100
            }
        })

    # Payloadの長さチェック
    if len(message) > 255:
        raise "APNs payload over 255 bytes!!!!"

    # 送信先プラットフォームの指定、本番であればAPNS
    # Android向けのPushもここで同時に指定できる
    data = json.dumps({"APNS_SANDBOX": message})

    # message_structure='json'を指定してPublish
    try:
        sns_con.publish(
                target_arn=endpoint,
                message=data,
                message_structure='json')
    except BotoServerError, e:
        logger.warn(
            "BotoServerError status:%s %s %s",
            e.status, e.reason, e.error_message)

Payloadを一度作った後、文字列にして再度JSONにつっこむという面倒な事が必要。

APNsのFeedback Serviceはいつ呼ばれるか

SNSの内部でよろしくやってくれてるようだが、その結果をboto経由で参照する方法は見つからなかった。Feedback Serviceの結果から、どの程度アプリがデバイスから削除されたかモニタリングしたいといった場合にはSNSは使えないだろう。

このエントリーをはてなブックマークに追加

2013-08-11

RabbitMQ 3.1の導入とCluster構成を検証する

RabbitMQ 3.1の導入と冗長化の検証をしたのでメモ。
検証のための構成はフロントのAPサーバー、RabbitMQが動作するキューサーバー、ワーカーそれぞれ二台づつ。キューサーバーが片方死んでも全体が動作し続けられる事、両方がダウンしたとしてもデータは損失しない事が確認できれば良い。要するに単一障害点にならないようにRabbitMQを使いたい。


サーバーの準備

仮想マシン6台はVagrantを使えば一発で用意できる、メモリ16GB積んでてよかった。ホスト名を後でいじるとrabbitmqctlで停止・再起動がうまくいかなくなった。ホスト名周りはEC2で使う時に面倒な事になりそうだ。

各サーバーの /etc/hosts にrabbit1とrabbit2は追加しておく。

RabbitMQ 3.1 のインストール

APTリポジトリの追加が必要、公式ページに手順があるのでその通りに。

起動確認

vagrant@rabbit1:~$ sudo rabbitmq-server
vagrant@rabbit2:~$ sudo rabbitmq-server

管理画面の有効化

vagrant@rabbit1:~$ sudo rabbitmq-plugins enable rabbitmq_management 
vagrant@rabbit1:~$ sudo rabbitmqctl stop
vagrant@rabbit1:~$ sudo rabbitmq-server

vagrant@rabbit2:~$ sudo rabbitmq-plugins enable rabbitmq_management 
vagrant@rabbit2:~$ sudo rabbitmqctl stop
vagrant@rabbit2:~$ sudo rabbitmq-server

それぞれのサーバーのポート15672で管理画面が起動する(2.x系の場合は55672)。Basic認証がかかっているが有効化直後は guest/guest で参照できる。

疎通テスト

クラスタ構成にする前にそれぞれのrabbitmqノードと疎通できるかチェックする。Hello Worldが届けばOK。
# sender.py
import pika

params = pika.ConnectionParameters('rabbit1')
connection = pika.BlockingConnection(params)
channel = connection.channel()
# Make Queue
channel.queue_declare(queue='mq_test')
# Publish
channel.basic_publish(
                exchange='',
                routing_key='mq_test',
                body='Hello World',
                )
print " [x] sent Hello World"
# receiver.py
import pika

params = pika.ConnectionParameters('rabbit1')
connection = pika.BlockingConnection(params)

channel = connection.channel()
channel.queue_declare(queue='mq_test')

def callback(ch, method, properties, body):
    print " [x] Received %r" % (body,)

channel.basic_consume(callback, queue='mq_test', no_ack=True)
channel.start_consuming()

メッセージの永続化

先のPublisherのコードだと、receiverがメッセージを受け取る前にrabbitmqを停止させるとキューの内容が消失した。それではまずいのでキューとメッセージにオプションを追加する。
# durable=True オプション付きでキューを宣言する
channel.queue_declare(queue='cluster_test', durable=True)
# 永続化オプション付きでメッセージをpublishする
channel.basic_publish(
                exchange='',
                routing_key='cluster_test',
                body=msg,
                properties=pika.BasicProperties(
                        # To Persistent
                        delivery_mode=2,
                )
        )

管理画面上でキューのパラメータに[D]と表示され、rabbitmqの起動再起動を繰りかえしても内容が保持されるようになった。

ACKの送出

先のComsumerの実装だと、ワーカーが正常に処理を完了したかどうかに関係無くキューのメッセージが消える。正常応答がComsumerから返ってきた場合のみキューのメッセージが消えるようにするには次の通り。
def callback(ch, method, properties, body):
    print " [x] Received %r" % (body,)
    # ackを返す (返さないとキューから消えない)
    ch.basic_ack(delivery_tag = method.delivery_tag)

# no_ack=Trueを削除
channel.basic_consume(callback, queue='mq_test')

Cluster化

次はrabbit1とrabbit2をクラスタ構成にする。まずは各ノードのerlang cookie (/var/lib/rabbitmq/.erlang.cookie) を同じにしてそれぞれ再起動。
rabbit2をrabbi1に参加させるには次のコマンド。
vagrant@rabbit2:~$ sudo rabbitmqctl stop_app
vagrant@rabbit2:~$ sudo rabbitmqctl join_cluster rabbit@rabbit1
vagrant@rabbit2:~$ sudo rabbitmqctl start_app
クラスタの状態を確認、二つともdiskノードになっている。
vagrant@rabbit2:~$ sudo rabbitmqctl cluster_status
ls: cannot access /etc/rabbitmq/rabbitmq.conf.d: No such file or directory
Cluster status of node rabbit@rabbit2 ...
[{nodes,[{disc,[rabbit@rabbit1,rabbit@rabbit2]}]},
 {running_nodes,[rabbit@rabbit1,rabbit@rabbit2]},
 {partitions,[]}]
…done.

いずれかのrabbitmqノードに送信する様にPublisherを改造する

HAProxyを各APサーバーに載せるのが硬そうだが、この場では生きているrabbitmqノードに接続できるまでランダムに選ぶというナイーブな get_connection を実装した。
# sender.py (ap1)
import random
import pika

def get_connection():
    mq_clusters = ['rabbit1', 'rabbit2']
    random.shuffle(mq_clusters)
    for mq in mq_clusters:
        try:
            params = pika.ConnectionParameters(mq)
            connection = pika.BlockingConnection(params)
            return connection
        except Exception, e:
            print("Try next node")
    raise Exception("Cannot establish connection")

def send(con, msg):
    channel = con.channel()
    # 同じなので中略

for i in xrange(1, 1000):
    con = get_connection()
    msg = 'From AP1 to %s: %i' % (con.params.host, i)
    send(con, msg)
    con.close()

実行結果

# AP1
[x] sent From AP1 to rabbit1: 900 
[x] sent From AP1 to rabbit1: 901 
[x] sent From AP1 to rabbit1: 902 
[x] sent From AP1 to rabbit1: 903 
[x] sent From AP1 to rabbit1: 904 
[x] sent From AP1 to rabbit1: 905 
[x] sent From AP1 to rabbit2: 906 
[x] sent From AP1 to rabbit1: 907 
[x] sent From AP1 to rabbit1: 908 
[x] sent From AP1 to rabbit2: 909 

# AP2
[x] sent From AP2 to rabbit2: 815
[x] sent From AP2 to rabbit1: 816
[x] sent From AP2 to rabbit2: 817
[x] sent From AP2 to rabbit2: 818
[x] sent From AP2 to rabbit2: 819
[x] sent From AP2 to rabbit2: 820
[x] sent From AP2 to rabbit1: 821
[x] sent From AP2 to rabbit1: 822
[x] sent From AP2 to rabbit2: 823
[x] sent From AP2 to rabbit1: 824
 
 # Worker1
[x] Received 'From AP1 to rabbit1: 903'
[x] Received 'From AP1 to rabbit1: 904'
[x] Received 'From AP1 to rabbit1: 905'
[x] Received 'From AP2 to rabbit2: 820'
[x] Received 'From AP1 to rabbit1: 907'
[x] Received 'From AP1 to rabbit1: 908'
[x] Received 'From AP2 to rabbit2: 823'

# Worker2
[x] Received 'From AP1 to rabbit1: 900'
[x] Received 'From AP1 to rabbit1: 901'
[x] Received 'From AP1 to rabbit1: 902'
[x] Received 'From AP2 to rabbit2: 817'
[x] Received 'From AP2 to rabbit2: 818'
[x] Received 'From AP2 to rabbit2: 819'
[x] Received 'From AP1 to rabbit2: 906'
[x] Received 'From AP2 to rabbit1: 821'
[x] Received 'From AP2 to rabbit1: 822'
[x] Received 'From AP1 to rabbit2: 909'
[x] Received 'From AP2 to rabbit1: 824'

これで上手くいったかと思いきや、rabbit1を停止させるとrabbit2にメッセージを投げた時にエラーが返ってくるようになる。
pika.exceptions.ChannelClosed: (404, "NOT_FOUND - home node 'rabbit@rabbit1' of durable queue 'cluster_test' in vhost '/' is down or inaccessible")
キューの内容がrabbit1にしか保持されていないからだ。rabbit2は生きているがrabbit1のキューにアクセスできなければ何もできない。

キューをミラーリングする設定

特定のキューの内容をクラスタの全てのノードにも持たせるには rabbitmqctl で ha-mode を指定する。
vagrant@rabbit1:~$ sudo rabbitmqctl set_policy all 'cluster_test' '{"ha-mode": "all"}'
vagrant@rabbit2:~$ sudo rabbitmqctl set_policy all 'cluster_test' '{"ha-mode": "all"}' 
キューの指定には正規表現が使えるので、全てのキューをミラーリングするには
sudo rabbitmqctl set_policy all '^.*' '{"ha-mode": "all"}'
としても良い。これで、rabbit1を落してもrabbit2だけで動作するようになった。管理画面でもミラーリングができているか確認ができる。


停止したノードはそのまま再起動でOK、元のクラスタ構成に戻る。

参考



このエントリーをはてなブックマークに追加

2013-07-18

アラビア語の学習環境を整える

思い立ってアラビア語の勉強をはじめたので、やった事をまとめる。

キーボードの設定

まずはMacのキーボードでアラビア語の入力をできるようにした。手順は「システム環境設定」→「言語とテキスト」→「入力ソース」からアラビア語入力ソースにチェックを入れるだけ。しかしここで入力方式が3つあり、どれにするか早速悩む。
ArabicとArabic - PCについてはWikipediaのアラビア語キーボード配列の項目に記載があった。Arabic - QWERTYについてはよくわからなかったので実際に触ってみた。配列は次のとおり。

ا(アリフ)がa、ب(バー)がbといった風に音感が似てるローマ字アルファベットにマッピングされている。配列の歴史的な経緯はどうでも良かったので、すぐに体が覚えられそうなArabic - QWERTYを使う事にした。السلام عليكم(アッサラーム アレイコム)と打ちたい場合は alslam elikm である。直感的に打てて良い。なぜかArabic - QWERTYの状態ではctrl + shift+ ]でChromeのタブの切り替えができなくて不便。

勉強用テキスト

Amazonで良さそうなのを二冊購入した。最初は全くアラビア語が読めないので単語を覚える事ができない、まずは書き取りから初めて単語が読める状態になるのを目指した。どちらも本にガリガリ書き込んで練習するタイプの本。
読める書けるアラビア文字練習プリント読める書けるアラビア文字練習プリント
アルモーメン アブドーラ,Al moamen Abdalla

小学館
売り上げランキング : 65350
Amazonで詳しく見る
アラビア語が面白いほど身につく本―文字から旅行会話までマスターできる (語学・入門の入門シリーズ)アラビア語が面白いほど身につく本―文字から旅行会話までマスターできる (語学・入門の入門シリーズ)
アルモーメン・アブドーラ,本田 孝一,Al moamen Abdalla

中経出版
売り上げランキング : 100014
Amazonで詳しく見る

カリグラフィ万年筆

最初はボールペンで書きとり練習をやっていたのだが、あの独特の雰囲気が出せず面白味に欠ける。そこで、カリグラフィ用万年筆があるとの事で使ってみた。アラビア書道用の物では無いが、太い線が引けるので雰囲気が出る。記事タイトル直下の画像がカリグラフィ万年筆で書いた文字。
パイロット万年筆 プレラ カリグラフィ 透明ブラック FPRN-350R-TBCMパイロット万年筆 プレラ カリグラフィ 透明ブラック FPRN-350R-TBCM
㈱パイロットコーポレーション
売り上げランキング : 18793
Amazonで詳しく見る

学習支援アプリの導入

AppStoreで探してもあまり数が無いので、見つかったアプリを全て試した。L-Lingoという奴が繰り返し練習できて良さげ。

L-Lingo アラビア語を学ぼう 5.03(無料)App
カテゴリ: 教育, 辞書/辞典/その他
販売元: Smart Language Apps Limited - Smart Language Apps Limited(サイズ: 85.9 MB)
全てのバージョンの評価: 無し(0件の評価)


このエントリーをはてなブックマークに追加

2013-07-06

iPython Notebook用のChefのCookbookを書いた

iPython Notebookが0.13.2にバージョンアップして、セットアップが自動化できそうな雰囲気がしたので勉強中のChefのcookbookにした。

iPython Notebookのパッケージインストール

apt-get install ipython-notebook で入るようになったので、これまでとは比較にならないぐらい簡単になった。レシピは次の通り。ipython-notebook本体と必要なパッケージをインストールする。
# Install packages
%w{
  python-pandas
  python-numpy
  python-scipy
  python-matplotlib
  python-nose
  ipython-notebook
}.each do |pkg|
  package pkg do
    action :upgrade
  end
end

# iPython needs sympy 0.7.2
# So use [pip install] instead of package install (0.7.1).
python_pip "sympy"
sympyだけはaptで降ってくるバージョンが古くて動作しなかったので、pipで入れている。

iPython Notebookの起動

サーバー起動時にiPython Notebookも起動して欲しいので起動もレシピにした。内容は

  • 起動ユーザー(ipynb)の作成
  • プロファイル配置ディレクトリの作成
  • 起動スクリプトの配置
  • 起動

デーモン化が面倒だったので nohup で起動するようにした。レシピは次の通り。
# Create launch user
group 'ipynb' do
  group_name 'ipynb'
  action :create
end

user 'ipynb' do
  comment 'User for ipython notebook'
  gid 'ipynb'
  home '/home/ipynb'
  shell '/bin/bash'
  supports :manage_home => true
  action :create
end

# Add to staff group
group 'staff' do
  action :modify
  members ['ipynb']
  append true
end

# Create serve directory
directory '/web/' do
  owner 'ipynb'
  group 'staff'
  mode '0775'
  action :create
end

directory '/web/ipython-notebook/' do
  owner 'ipynb'
  group 'staff'
  mode '0775'
  action :create
end

# 起動スクリプトの配置
template '/web/ipython-notebook/launch.sh' do
  source "launch.sh.erb"
  owner 'ipynb'
  group 'staff'
  mode 00776
end

bash 'Launch ipython notebook' do
  user 'ipynb'
  group 'staff'
  cwd '/web/ipython-notebook/'
  code >>-EOC
    nohup ./launch.sh restart
  EOC
end
起動スクリプトテンプレート、起動オプションのいくつかはAttributeから渡す。
#!/bin/bash

pid=`dirname $0`/ipynb.pid
port=<%= node['ipython-notebook']['port'] %>
ip=<%= node['ipython-notebook']['ip'] %>
ipythondir=`dirname $0`/.ipython

start() {
    if [ -f $pid ]; then
        echo "running already. pid: `cat $pid`";
        return 1;
    else
        cd `dirname $0`
        ipython notebook --pylab=inline --port=$port --ip=$ip --ipython-dir=$ipythondir &
        echo $! > $pid
    fi
}

stop() {
    if [ -f $pid ]; then
        kill `cat $pid`
        rm -f $pid
    else
        echo "Not running";
    fi
}

restart() {
    stop
    start
}

case "$1" in
  start)
    start
    ;;
  stop)
    stop
    ;;
  restart)
    restart
    ;;
  *)
    echo $"Usage: $0 {start|stop|restart}"
    exit 1
esac

exit $?

動作確認

インスタンス起動後にチェック
$ ps aux | grep python
ipynb    23612  0.0  4.0 188952 20172 ?        S    13:24   0:02 python /usr/bin/ipython notebook --pylab=inline --port=8888 --ip=* --ipython-dir=./.ipython
 実際に使ってみる。
OK。あとはbitbucketのプライベートリポジトリで管理しているノートをgitで引っぱってこれば個人的な要件は満たせる。

このエントリーをはてなブックマークに追加

2013-06-07

IDC FrontierさんからNASAハッカソンについて取材を受けました

NASAハッカソンのスポンサーであり、Cloudless spotチームにIDCFクラウドを無料で提供していただいたIDCFさんから受けた取材が記事になっています。 今はとにかく事例が欲しいとの事なので、後で紹介できそうな使い方であればハッカソンでもなんでも使わせてもらえそうな雰囲気でした。

このエントリーをはてなブックマークに追加

2013-06-03

NASAハッカソンでGalactic Impact部門のHonorable Mentionを受賞しました

グローバル審査の結果が出たのでエントリにします。

世界規模のハッカソンであるInternational Space Apps Challengeが4月に開催されました。昨年も同じ時期に開催されましたが、今回は開催地が44カ国、83都市。正式にサブミットされたプロジェクトは750を越えたとの事で、圧倒的な規模です。

私が参加したプロジェクトは日本ローカル2位*1となり、グローバル審査ではGalactic Impact部門のHonorable Mention(選外佳作)となりました。ハッカソンから1ヶ月以上経っていますが、今だに作業は続いているので嬉しい限りですね。

主に次の3つの機能を開発しました。
  • MODIS Cloud Maskの取りこみ、集計
  • 地図上へのマッピング
  • ソーラーパネル発電による収支のシミュレーション

サブミットしたプロジェクトページ
ソースコード (github), デモサイト

快晴率のマッピング
雲の影響を計算モデルに取りいれたソーラーパネル発電による収支のシミュレーション

二日間のハッカソン

私は、全地球上過去30年分の雲の衛星データ、MODIS Cloud Maskを利用して、太陽光エネルギーが効率良く得られる場所が見つけられるシステムを構築しようというチームに参加しました。メンバーは10人、本職のWebエンジニアが半分、エネルギーや気象方面の研究に係っている方が半分というバランスの良い構成でした。

by akiko yanagawa

とはいえ開始から発表まで30時間弱しかなく、途中MongoDBへのinsertが思ったようなパフォーマンスが出ないトラブルが発生し、処理対象のデータは日本(北緯20°~50°, 東経120°~150°)の10年分に限定する事に。

処理対象の範囲を限定したと言っても、ダウンロードしたMODISのデータは100GByte、12年分のデータが9億7000万レコードとなったのでサーバーリソースもそれなりに必要に。サーバーはスポンサー提供のIDCFクラウドが使えたので、並列処理できる所はインスタンスをガンガン追加してしのぎました。*2

この時の作業をざっと挙げると。
  • データ回り
    • MODIS Cloud Maskのデータ形式の調査
    • ダウンロードサイトから日時と領域(緯度経度)を指定して必要データを延々と落すダウンローダーの開発
    • MongoDBへの投入
    • MapReduceで集計
  • アプリケーション回り
    • ソーラーパネルの発電量の計算モデルの作成
    • アプリケーションの設計
    • サーバーサイドの開発(Ruby on Rails)
    • クライアントの開発(JavaScript)
    • Webデザイン
  • その他
    • サーバー確保
    • プロジェクトのサブミット
    • 発表準備
チームメンバーに恵まれた事もあり、スタンドプレーから生まれるチームワークとも言うべき分業体制でそれっぽく動く物が完成。発表準備はリーダーが粛々と進めており、プロジェクトの壮大な展望をプレゼン、ローカル審査は全18チーム中の2位となりました。

by akiko yanagawa

グローバル審査へ

ハッカソンの後、グローバル審査へのサブミット締切までは一週間、その間にプロジェクトの解説動画を作り、プロジェクトページを完成させなければなりません。ろくに寝ていない状況でそれを聞いてメンバー全員が沈黙。
アプリは審査に耐えられる状態では無かったのでチューニングが必要でした。私はデータの精度アップのためにひたすら積み上げ計算処理を流していました。このあたりで一回の集計が24時間を越えたのでもうMongoDBはやめてHadoopか何かしよう……と強く思ったのでした。

まとめ


  • MongoDBのMapReduceは1CPUしか使ってくれなくて辛かった
  • 集計結果をプロットするRのプログラムのバグが今だに取れなくて泣きそう
  • 太陽光エネルギーの利用法(ソーラーパネルと藻)について詳しくなった
  • 普段合う事のない、別の分野のプロフェッショナルと一緒に物を作れるのは楽しい


*1:  ローカル審査で1位と2位のプロジェクトがグローバル審査に進むルール。
*2:  最終的に管理画面で利用額を見たら4月と5月あわせて200万円ぐらいになっていた……、IDCFさん本当にスポンサーありがとうございました。(今も使わせてもらってます)

このエントリーをはてなブックマークに追加

2013-05-25

情報理論の基礎4章メモ、エントロピーからHuffman符号まで

手計算が厳しくなってきたのでiPython notebookでコードを書きつつメモ。実際に動作する物が残せて便利。 Huffman符号化は以前SICPで書いた記憶があったが、その時とは全く違ったコードになった。SchemeとPythonの違いだろうか。

4章 符号化と種々の情報量

  • エントロピー
  • KL情報量
  • Fano符号、Shannon符号
  • Huffman符号
In [128]:
%load_ext sympy.interactive.ipythonprinting
import sympy as sym

エントロピー

確率変数XのエントロピーH(X)はXの確率分布をP(X = i)として。
In [129]:
H, p, i, k = sym.symbols("H(x) P_i i k")
sym.Eq(H, sym.Sum(p*sym.log(1/p), (i, 0, k)))
Out[129]:
$$H(x) = \sum_{i=0}^{k} P_{i} \log{\left (\frac{1}{P_{i}} \right )}$$
In [130]:
def calc_entropy(P):
    return sum(map(lambda p: 0 if p == 0 else p * log2(1/p), P))
In [131]:
# Xが二値の場合、それぞれ同じ確率で出現する場合が一番エントロピーが大きい
x = linspace(0, 1, 100)
p = map(lambda x: [x, 1.0 -x], x)
plot(x,map(calc_entropy, p))
t = title('binary entropy (Figure 4.1)')

KL情報量 (Kullback-Leibler divergence)

クロスエントロピーとエントロピーの差、理想的な符号長を使った場合と、確率分布Qであるとみなして符号化したものとの差分
In [132]:
D = sym.Symbol("D(P,Q)")
p, q, i, k = sym.symbols("P_i Q_i i k")
sym.Eq(D, sym.Sum(p*sym.log(p/q), (i, 0, k)))
Out[132]:
$$D(P,Q) = \sum_{i=0}^{k} P_{i} \log{\left (\frac{P_{i}}{Q_{i}} \right )}$$
In [133]:
def calc_KLD(P, Q):
    ret = 0
    for  p, q  in zip(P, Q):
        ret += p * log2(p/q)
    return ret
In [134]:
def calc_Q(code_lens):
    ret = []
    for l in code_lens:
        ret.append(2 ** (-1 * l))
    return ret

#符号長の平均をかえす
def len_ave(P):
    return sum(map(lambda x:log2(1/x), P)) / len(P)

#切りあげた符号長の平均をかえす
def len_ceil_ave(P):
    return sum(map(lambda x:ceil(log2(1/x)), P)) / len(P)

#符号長に1を足した平均をかえす
def len_plus1_ave(P):
    return sum(map(lambda x:log2(1/x) + 1, P)) / len(P)

p = [0.4, 0.5, 0.1]
print(len_ave(p))
print(len_ceil_ave(p))
print(len_plus1_ave(p))
1.88128539659
2.33333333333
2.88128539659

Shannon fano符号

In [142]:
class ShannonFanoEncoder(object):
    def __init__(self, vals):
        p_vals = vals[:]
        p_vals.sort()
        p_vals.reverse()
        self.f_val = self.accumlate(p_vals)
        self.code_lens = self.calc_code_lens(p_vals)
        self.p_vals = p_vals
        
    def encode(self):
        codes = []
        for f, l in zip(self.f_val, self.code_lens):
            codes.append(self.get_bin_by_float(f, l))
        return codes
    
    def accumlate(self, p_val):
        ret = []
        tmp = 0
        for p in p_val:
            ret.append(tmp)
            tmp += p
        return ret
    
    def calc_code_lens(self, p_val):
        return map(lambda p: ceil(math.log(1/p, 2)), p_val)
    
    def get_bin_by_float(self, f_val, bin_len):
        """
        小数を二進数にして、小数点以下を指定した桁数でかえす
        (0.5, 4)   -> 1000
        (0.25, 4)  -> 0100
        (0.125, 4) -> 0010
        """
        bin_len = int(bin_len)
        return format(int(math.floor(f_val * (2 ** (bin_len)))), 'b').zfill(bin_len)
In [148]:
# Shannon Fano符号
P = [0.025,0.075,0.3,0.6]
shannon_fano = ShannonFanoEncoder(P)

print('入力')
print(shannon_fano.p_vals)
print('符号')
print(shannon_fano.encode())
print('符号長')
print(shannon_fano.code_lens)
print('KL情報量')
print(calc_KLD(shannon_fano.p_vals, calc_Q(shannon_fano.code_lens)))
入力
[0.6, 0.3, 0.075, 0.025]
符号
['0', '10', '1110', '111110']
符号長
[1.0, 2.0, 4.0, 6.0]
KL情報量
0.273410343316

Huffman符号

In [137]:
class HuffmanEncoder(object):
    """
    情報の各要素の出現確率を元にHuffman符号を作成する
    """
    def __init__(self, vals):
        self.p_vals = vals[:]
        self.p_vals.sort()
        self.p_vals.reverse()
        
    def encode(self):
        vals = self.p_vals[:]
        # 木の構築
        while len(vals) > 1:
            tree = self.make_tree(vals.pop(), vals.pop())
            vals.append(tree)
            vals = self.sort_tmp_tree(vals)
            
        # 木からコードを生成
        def walk(node, code, codes):
            def w(v):
                if self.is_node(v[0]):
                    walk(v[0], code + v[1], codes)
                else:
                    codes.insert(0, code + v[1])
            w(node[1])
            w(node[2])
            return codes
        return walk(vals[0], "", [])
                
    def is_node(self, v):
        return isinstance(v, tuple) and len(v) == 3
    
    def make_tree(self, v1, v2):
        return (self.sum(v1) + self.sum(v2), (v1, "1"), (v2, "0"))
       
    def sum(self, v):
        return v if isinstance(v, float) else v[0]
       
    def sort_tmp_tree(self, vals):
        return sorted(vals, cmp = lambda x, y: int(self.sum(y) - self.sum(x)))
In [147]:
# Huffman符号
P = [0.025,0.075,0.3,0.6]
huffman = HuffmanEncoder(P)
codes = huffman.encode()
code_lens = map(lambda p:len(p), codes)

print('入力')
print(huffman.p_vals)
print('符号')
print(codes)
print('符号長')
print(code_lens)
print('KL情報量')
print(calc_KLD(huffman.p_vals, calc_Q(code_lens)))
入力
[0.6, 0.3, 0.075, 0.025]
符号
['0', '10', '110', '111']
符号長
[1, 2, 3, 3]
KL情報量
0.123410343316
Huffman符号の方がKL情報量が小さい、つまり理想的な符号に近い事がわかる。



このエントリーをはてなブックマークに追加

2013-04-19

asm.jsを手書きしつつフィボナッチで速度比較をしてみる

asm.jsを触ってみたので所感など。

asm.jsはJavaScriptのサブセットで、限られた型しか使えないが高速に動作する言語との事。とりあえずどの程度速くなるのか、計算量が多くなるfibonacciの実装で試してみた。参考資料はasm.jsの仕様ぐらいしか無かったのでこれを見ながら。

で、実際に書いてみると型がゆるゆるなJavaScriptのイメージは脆くも崩れ去り、厳格な型チェックの世界である事がわかった。コンパイル言語を書いている時の頭に切り換えないと、コンパイルエラーと延々格闘する事になる。まずはasm.jsのコードは次の形式で、module exportする。
function create_my_asm_module(stdlib, foreign, heap) {
  "use asm";

  function hoge() {...}
  function fuga() {...}

  return {
    hoge: hoge,
    fuga: fuga
  }
}
asm_my_modules = create_my_asm_module(window);
関数の書き型にも決まりがあり、次の順番で記述する必要がある。
function hoge(fuga) {
  // 1)パラメータの型指定
  // 2)変数の初期化
  // 3)処理
  // 4)return句
}
次にasm.jsで書く関数の引数の型、戻り値の型を決める。引数の型はParameter Type Annotationsで記述する。
  
function calc_tax_included_price(price, tax_rate) {
    price = price|0;      // priceはint
    tax_rate = +tax_rate; // tax_rateはdouble

    //略
}
仕様にはintとdoubleしか無いのでどちらかとなる。
関数の戻り値はReturn Type Annotationsで記述する。関数内にreturnが複数個ある場合、それぞれの箇所でreturnの型が異なるとコンパイルできない。
  
return +d;  // double
return i|0; // signed int
return 1;   // double
return;     // void
で、実際にフィボナッチが動くコードと実行結果が以下。実行はAuroraバージョン22.0a2。

asm.jsの方が速いのがわかる。といってもこの程度ならまだしも、実際に高速化したい処理を手でasm.jsで書くのは正直厳しいという印象。githubで "use asm"しているコードを探したら行列演算ライブラリが出てきましたが、ヒープ操作している所が全く読めなくてやばい。



このエントリーをはてなブックマークに追加

2013-03-22

Python Developers FestaでLeap Motionについて発表しました


既に開発者登録をしてる人には届きはじめているLeap Motion@voluntas氏に「デモが見たい」と頼まれたのでPython Developers Festaでしゃべってきました。

 

Canvasを使ってWebページにサインをするデモ、発表時はひたすら書きづらい出来でしたが、改良すればなんとか使えるかもしれない。あと質問であった手話の認識ですが、手を閉じた状態では手の位置しかデータが取れないので難しそう。発表後調べ直しましたが、深度画像が取得できないのでSDKにRow DataアクセスのAPIが追加されるのを待たないと独自認識処理は無理かなと。

せっかくなのでLeap Motionを使って書いた奴貼っておきます。


このエントリーをはてなブックマークに追加

2013-03-17

iPython notebookでPRMLのグラフを再現する

先週のPRML復々習レーン#9でPRML下巻の図6.1がいまいち理解できなかったので、Pythonのコードで再現してみた。基底関数と特徴空間への写像の内積で定義されるカーネル関数をxでプロットした図である。

最初Excelのグラフでやってみたが、ありえない事に気づきOctaveに方針変更。それでも、コードと結果を見くらべながら試行錯誤しにくかったので、最終的に@__youki__氏に教えてもらったiPython notebookに落ちついた。

iPython notebookはiPythonのWebクライアント。グラフを試行錯誤しながらガンガンプロットして保存しておける。セットアップは若干面倒だがそのうちfabricのfabファイルとか出てきそう。結果はnbconverterでHTMLに変換できる。コード、結果の共有はもちろん、ブラウザから実行できるので一度サーバーを立ててしまえばどこでも使えて便利。

変換したHTMLを貼りつけたのが以下。

In [4]:
import numpy as np
In [79]:
x = linspace(-1, 1, num=100)
for i in xrange(1,12):
    plot(x, x**i)

ret = title('Basis function')
In [80]:
x = linspace(-1, 1, num=100)
y = np.zeros(100)
x_prime = -0.4
for i in xrange(1,12):
    v = (x ** i)
    y += reduce(lambda x,y :x+y, [v * (x_prime ** i)])
    
plot(x, y)
ret = title(u'Karnel function')
In [26]:
def gaussian(x, mean, sigma):
    return 0.5/np.sqrt(2.0*np.pi)/sigma * np.exp(-((x-mean)/sigma)**2/2)
In [73]:
x = linspace(-1, 1, num=100)
SIGMA = 0.2
for i in xrange(0, 11):
    mean = -1 + (float(i) / 5)
    plot(x, map(lambda x:gaussian(x, mean, SIGMA), x))
    
ret = title('Basis function')
In [77]:
SIGMA = 0.2

x = linspace(-1, 1, num=100)
y = np.zeros(100)
x_prime = 0.0

for i in xrange(0,11):
    mean = -1 + (float(i) / 5)
    v = np.array(map(lambda x:gaussian(x, mean, SIGMA), x))
    y += reduce(lambda x,y :x+y, [v * gaussian(x_prime, mean, SIGMA)])
    
plot(x, y)
ret = title(u'Karnel function')
In [68]:
def logistic_sigmoid(x):
    return 1 / (1 + np.exp(-1 *  x * 10))
In [70]:
x = linspace(-1, 1, num=100)
for i in linspace(-1, 1, num=11):
    plot(x, map(lambda x:logistic_sigmoid(x - i), x))

ret = title('Basis function')
In [82]:
x = linspace(-1, 1, num=100)
x_prime = 0.0;
y = np.zeros(100)

for i in linspace(-1, 1, num=11):
    v = np.array(map(lambda x:logicstic_sigmoid(x - i), x))
    y += reduce(lambda x, y: x + y, [v * logistic_sigmoid(x_prime - i)])

plot(x, y)
ret = title('Karnel function')

このエントリーをはてなブックマークに追加