Flask + Flask-Scriptでgunicornをマニアックに起動する

HerokuでpythonのWebアプリを動かすとき

Flask-Scriptを使っている場合、

== manager.py ==

from flask import Flask
from flask_script import Manager

app = Flask(__name__)
manager = Manager(app)

if __name__ == "__main__":
    manager.run()

== Procfile ==

web: gunicorn manager:app

と書けば何ら問題なくデプロイできます。

しかし、

Flask-Script を使って開発サーバーを動かすときの

$ python manage.py runserver

のような python manage.py XXX という書式のマニアックな起動をしたいときはどうすればよいのでしょう?

python manage.py XXX 形式でのWebサーバーの起動方法

python - How to use Flask-Script and Gunicorn - Stack Overflow の記事を参考に所望の起動ができました

== manage.py ==

import os
from flask import Flask
from flask_script import Manager, Server, Command, Option

app = Flask(__name__)

class GunicornServer(Command):
    def __init__(self, host='127.0.0.1', port=8000, workers=1):
        self.port = port
        self.host = host
        self.workers = workers

    def get_options(self):
        return (
            Option('-H', '--host',
                   dest='host',
                   default=self.host),

            Option('-p', '--port',
                   dest='port',
                   type=int,
                   default=self.port),

            Option('-w', '--workers',
                   dest='workers',
                   type=int,
                   default=self.workers),
        )

    def __call__(self, app, host, port, workers):

        from gunicorn import version_info

        if version_info < (0, 9, 0):
            from gunicorn.arbiter import Arbiter
            from gunicorn.config import Config
            arbiter = Arbiter(Config({'bind': "%s:%d" % (host, int(port)),'workers': workers}), app)
            arbiter.run()
        else:
            from gunicorn.app.base import Application

            class FlaskApplication(Application):
                def init(self, parser, opts, args):
                    return {
                        'bind': '{0}:{1}'.format(host, port),
                        'workers': workers
                    }

                def load(self):
                    return app

            FlaskApplication().run()


manager = Manager(app)
manager.add_command('gunicorn', GunicornServer(host='0.0.0.0', port=os.environ.get('PORT', 5000)))


if __name__ == "__main__":
    manager.run()

== Procfile ==

python manage.py gunicorn

で無事にHeroku上で python manage.py XXX 書式でWebアプリが動きます。

あと、Herokuにデプロイするときはポート番号を環境変数PORTの値にしておかないといけません (PORTの値はデプロイするたびに変わるので)

とりあえず、マニアックなやり方でWebアプリを起動できましたが自己満足できるということ以外にこちらの方法を取るメリットが今のところ思いつかないです。。

キャンプ場レビューのWordCloud画像の検索サービスを作る

WordCloud画像は以前の記事で作れるようになったので今回、全キャンプ場分の画像を作成し検索できるようにしました。

こんな感じです。

https://campsite-wordcloud.herokuapp.com/

  1. 検索ボックスにキーワードを入力してキャンプ場を検索 f:id:sanshonoki:20190109224710p:plain

  2. 候補が複数あるときは候補を選択する f:id:sanshonoki:20190109224737p:plain

  3. 選択したキャンプ場のWordCloud画像が表示される f:id:sanshonoki:20190121222415p:plain

これだけなので検索サービスというのは少々大げさです。ただ、いい感じのタイトルがすぐに思いつかなかったのでお許しください。。

苦労したところ

苦労したことを一つあげるとすればストップワードの設定です。

ストップワードをどう設定するかによってキャンプ場同士の特徴の違いをうまく浮き出せられるかが変わってきます。

以下の3パターンを試しました

  1. 結果を見ながら除外する単語をストップワードに追加していく
  2. 頻度を算出し、カウント上位とカウント下位の単語をストップワードとする
  3. TF-IDFを計算し、閾値以下をストップワードとする

が、一番良さそうだったのは一番目の人力チューニングでした..。

TF-IDFを使っていい感じになってくれればブログの記事のネタが増えてくれて良かったのですが。。w

ちなみに、最終的なストップワードは以下になりました。

STOP_WORDS = ['あり', 'ある', 'いる', 'する', 'こと', 'それ', 'ない', 'の', 'し', 'さ', 'れ', 'い', 'サイト', 'キャンプ場', '思い', 'キャンプ', 'でき', 'よう', 'とても', 'ところ', '出来', 'なり', 'あっ', 'おり', 'なっ', 'テント', 'キャビン', 'さん', 'なく', 'られ', 'オートキャンプ', 'トイレ', '利用']

キャンプに行くときに利用しつつ、引き続きチューニングをしていこうと思います。

形態素解析(単語への分割)は通常のMeCabを使用しましたがmecab-ipadic-NEologdなど他の辞書を使えば結果もまた変わってくると思います。これも時間があれば試してみたい

Kerasの学習の再現性を担保する

学習のたびに精度(結果)が違ってしまい、後になって資料のためのグラフを作ろうとしたときなどたまに困るので再現性を担保する方法を調べました。

参考になった記事

結論

基本的に1つ目のqiitaの記事に書いてあるとおりでokでした。 自分の実験ではコメントアウトしている2行はコメントアウトした状態でも結果は再現されていました。

import numpy as np
import random
import keras.backend as K
import tensorflow as tf

np.random.seed(seed=0)

# os.environ['PYTHONHASHSEED'] = '0'
# random.seed(0)

session_conf = tf.ConfigProto(
    intra_op_parallelism_threads=1,
    inter_op_parallelism_threads=1
)

tf.set_random_seed(0)
sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
K.set_session(sess)

後からグラフを作るとき以外にも精度向上を求めて無駄?に同じ学習パラメータでの学習を繰り返すことを強制的に諦めることができ助かりますw

Flaskのベストプラクティスの研究

FlaskといえばpythonでさくっとWebアプリをつくれるフレームワークです。

ちょっとしたAPIを公開する分にはすべてを1ファイルに書いてしまって何ら問題ないのですが 複数ファイルに分けたくなるような規模のWebアプリを実装するときにプロジェクト構成がカオスになりがちです。 というか、自分の場合そうなってしまいました..。

そこでプロジェクト構成のベストプラクティス(的なもの)を調べてみました。

参考になった資料

以下がとても参考になりました!

成果物

github.com

今後Flaskアプリを作るときはこれをベースに作っていこうと思います

たどり着いたプロジェクト構成
~/ExampleApp
    |-- manage.py
    |-- config.py
    |-- /app
        |-- __init__.py
        |-- /models
            |-- __init__.py
            |-- entry.py  # model
            |-- user.py   # model
        |-- /views
            |-- __init__.py
            |-- error.py  
            |-- entry.py  # view for model
            |-- user.py   # view for model
        |-- /templates
            |-- layout.html
            |-- /entries
                |-- show_entries.html  # template for view
            |-- /errors
                |-- 400.html
                |-- 404.html
                |-- 500.html
        |-- /static
            |-- style.css
    |-- /tests
        |-- __init__.py
        |-- /models
            |-- test_entry.py
        |-- /views
            |-- test_api.py

ポイント

これさえ守っておけばきれいになると思います。

  • dbインスタンスapp/__init__.py作らない
    • app/models.py もしくは app/models/__init__.py に作る
  • ビューは Blueprint を使う

逆に、このポイントを守らないと次のようになってしまいます。

NGパターン

f:id:sanshonoki:20181108233328p:plain

一見動きそうですが、app/views.py を importしたとき app/views.py 内で参照する appdbインスタンスがこの時点ではまだ存在してないのでエラーになってしまいます。

いちおうOKパターン

f:id:sanshonoki:20181108233758p:plain

動作はします。 ただ、ファイルの最下部で import文を実行する必要があり違和感をぬぐえません。。

推奨パターン

以上を踏まえて推奨パターンを図示します。

f:id:sanshonoki:20181108234016p:plain

import文を慣習通りにファイル冒頭に書くことができ、もちろんエラーもなく動作します。

db が外部のapp/models.py の中に記述されているので app/views.py の最初のimport文でエラーになりません。

また、Blueprintを使っているので app/views.py において appの循環参照が発生しません。 Blueprintを使わず viewの中で app/__init__.pyapp を参照しようとすると(コードで書くとfrom app import app)、循環参照が発生してしまってダメです..

MeCab用のDockerfile

前回の内容の環境構築をDockerfileで作ろうとしてドハマりしました..

mecab dockerfile」とググれば何個も参考になる記事が出てくるので楽勝だろうと踏んで作業開始。

しかし、Dockerfileを作りビルドしたところ、、

opt/mecab-ipadic-neologd/bin/../libexec/make-mecab-ipadic-neologd.sh: 505 行:   759 強制終了            ${MECAB_LIBEXEC_DIR}/mecab-dict-index -f UTF8 -t UTF8
The command '/bin/sh -c git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git   && cd mecab-ipadic-neologd   && ./bin/install-mecab-ipadic-neologd -n -y   && cd ..   && rm -rf mecab-ipadic-neologd' returned a non-zero code: 137

なに?

${MECAB_LIBEXEC_DIR}/mecab-dict-index -f UTF8 -t UTF8 でエラーだと?

全く原因がわからん.. (ググってもヒントがない)

と原因解明に数日をロスしてしまいましたが結論から書くと、

なんと単にdockerのメモリ不足が原因でした

dockerのメモリを2G -> 3Gに増やしたら問題なくビルドできました。。

めでたし

Docker for Mac だと Preferences からメモリサイズ変更できます。 f:id:sanshonoki:20181009224611p:plain

メモリ要件に関しては mecab-ipadic-NEologdのオフィシャルページにちゃんと書いてました。

ちゃんと最初に一読せよということですね

Memory requirements
Required: 1.5GB of RAM
Recommend: 5GB of RAM

とのことです。

Dockerfile

今回の成果物

FROM ubuntu:16.04

RUN apt-get update \
  && apt-get install -y python3 python3-pip git curl wget make xz-utils file sudo unzip \
  && apt-get install -y mecab libmecab-dev mecab-ipadic-utf8 \
  && apt-get install -y language-pack-ja \
  && apt clean \
  && update-locale LANG=ja_JP.UTF-8

# Set locale
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP.UTF-8
ENV LC_ALL ja_JP.UTF-8

# Install mecab-ipadic-NEologd (Docker memory should be enough to compile)
WORKDIR /opt
RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git \
  && cd mecab-ipadic-neologd \
  && ./bin/install-mecab-ipadic-neologd -n -y \
  && cd .. \
  && rm -rf mecab-ipadic-neologd

# Set mecab-ipadic-NEologd as default
RUN sed -i 's/dicdir = \/var\/lib\/mecab\/dic\/debian/dicdir = \/usr\/lib\/mecab\/dic\/mecab-ipadic-neologd/' /etc/mecabrc

# Install python packages
ADD requirements.txt /tmp/requirements.txt
RUN pip3 install -r /tmp/requirements.txt

# Install fonts
RUN wget -O IPAfont00303.zip https://ipafont.ipa.go.jp/old/ipafont/IPAfont00303.php \
  && unzip IPAfont00303.zip \
  && mv IPAfont00303 fonts \
  && rm IPAfont00303.zip

# Add scripts
ENV PYTHONPATH /opt
ADD . .

CMD ["bash"]

Pythonコードの日本語出力で UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: となるエラーにも遭遇しましたが これは locale周りの設定がdockerでできてなかったからでした

キャンプ場レビューからWordCloudを生成

趣味のキャンプに役立つ何かをということで キャンプ場のレビューコメントからWordCloudを生成するというのをやってみました。

やったこと

  1. キャンプ場のレビューを集める
  2. MeCabで単語に分割する
  3. WordCloudを生成する

キャンプ場のレビューを集める

なっぷ というキャンプ場検索・予約サイトのレビューページをスクレイピングしました。 www.nap-camp.com

なっぷ自体は使ってませんがなっぷの運営会社のスペースキーさんが運営しているキャンプ情報サイト、CAMP HACK はよく利用させてもらっています m( )m

スクレイピングPythonでお馴染みのBeautifulSoupを使います。

特に工夫はなくページのhtmlをインスペクタで調べながらゴリゴリ抽出していきます。

MeCabで単語に分割する

辞書はNEologdを使いました。 github.com

MeCab関連でハマった箇所が以下。 2行目の tagger.parse('') がないと UnicodeDecodeError: 'utf-8' codec can't decode byte 0x90 in position 0: invalid start byte というエラーが出てしまいました..

tagger = MeCab.Tagger('-Ochasen')
tagger.parse('')  # https://teratail.com/questions/88592
node = tagger.parseToNode(text)

WordCloudを生成する

よくお世話になっている西丹沢のバウアーハウスジャパンと そのお隣のウェルキャンプ西丹沢のWordCloudを生成し、比べてみます。

WordCloudは以下のライブラリを使って作ります。 github.com

pipでインストールでき、使い方も簡単です。

生成画像

バウアーハウスジャパン

f:id:sanshonoki:20180928225102p:plain

行ったことある人しか分からないかもしれなけど、

一言で言えば、「分かる」

ウェルキャンプ西丹沢

f:id:sanshonoki:20180928225126p:plain

こちらはやや残念な感じになってしまいました。。 隣接しているので雰囲気はなんとなく分かるのですが私の想像以上に満足度は低いようです.. 「狭い」印象は確かにあります

どちらも 「トイレ」の文字が大きく表示されており、キャンプ場の評価として大きなウェイトをもつということなのでしょう。

何だかキャンプ場の雰囲気はいくらか汲み取れてそうです。 思ったより面白かったので関心のある他のキャンプ場でもやってみようと思います。

今回使ったコードはこちらです。 github.com

ワールドカップ ユニフォームカラー

つい先日終了したばかりのワールドカップですがユニフォームの色が気になり、 ロシア大会を含むここ7大会のベスト8のチームのユニフォームの色を調べてみました。

f:id:sanshonoki:20180723225714p:plain

(注) ホームのユニフォームを表示しています。ロシア大会に出場しているチームはロシア大会のを使いまわしているので昔の大会のものは正確でないかも..。また、5位以下の並びは適当です

【各画像の参照元

きっかけ

日本の初戦のコロンビア戦をTV観戦したとき、日本の選手が見にくいなーと思ったのがきっかけです。

日本のユニフォームは「サムライブルー」と言われる爽やかな青色ですが コロンビアのVividな黄色のカラーと比べると明らかに視認性が悪く(芝生の色と明度が近いので)、

あれ? 味方の位置の把握が(瞬間的に相対的に)しにくいんじゃない?

と思った次第です。

色のちから

視認性以外の側面から見てみます。

赤や黄色などの暖色系は 神経の興奮作用があり (程度のこそはあれ)相手の冷静さを失わせることができると考えられます。 また、サポーターは自国の赤や黄色を見て興奮し、熱烈な応援になりそうです。 スペインの闘牛も観客を興奮させるために赤い布を使ってるそうです。 他には、浦和レッズのファンは過激という話もあります。

逆に、青色の寒色系は神経の鎮静作用があり(程度のこそはあれ)相手を冷静させてしまうのではないかと考えられます。 サポーターも冷静になってcoolな応援になってしまいそうです。

白は膨張色なので相手選手に対して自分を大きく見せたりできそうです。また、「長時間見ると目が疲れる」効果もあるそうです。

ということから考えると やはり日本のサムライブルーは不利なんじゃないかと思うわけです。

【参考】

分析?

とりあえず、色ごとにカウントしてみます。

Count
19
赤 + 橙 18
12
7

白赤混合のクロアチアパラグアイは赤としてカウントしました

そもそも赤と白の総数が多いんで上位進出率として統計的に有意とかは全く主張できないですけど予想通りに白と赤+橙、黄が上位に来ました。 青は自分の予想ではもっと少ないかと思っていましたがフランスとイタリアの頑張りによりそこそこありました。 でも、パッと見、目立たないのは目立たないですね。。

ワールドカップ常連国とまでは言えないコスタリカ、ガーナ、韓国、ロシア、パラグアイウクライナセネガルはすべて赤、白、黄なのでひょっとしたら色彩の影響はいくらかあるのかもしれません。

ひとこと

悲願のベスト8進出の確率を少しでも上げるために、次回のカタール大会では日の丸カラーの白と赤を使ったユニフォームにしてはどうでしょうか。。