MotoJapan's Tech-Memo

技術めも

【Linebot #1】windows8.1(local)とherokuでpostgreSQL環境構築 (psycopg2)

Linebotを最近書いているのですが、そろそろオウム返しbotだけだとつまらないので、データベースを組み込みたいなと。
もともとsqlite3を使っていたのですが、Herokuはsqliteが簡単に使えないことをdeploy時に気づき、やむなくpostgres対応を考えることに。。


そこまでの手順とハマったことの覚書です。

手順は次です。

1. ローカルのwindows環境でpostgresをインストール・動作確認
2. heroku環境でpostgresをAddon追加・動作確認

1. ローカルのwindows環境でpostgresをインストール・動作確認

まず動作確認用のローカル環境を準備

1.1. windowsにpostgresをダウンロード・インストー

v 9.6.3-2を下記よりダウンロード

(あっという間に9系が主流みたいですね、pgadminも入っているし)

http://www.postgresql.org/

★インストール最終段階で「Database Cluster Initialization Failed」がでて失敗

対策:PostgreSQLのインストーディレクトリにフルコントロールを与える

これで無事インストールできました。(めでたしめでたし)

1.2. pythonから実行するためのモジュール(psycopg2)をインストー

次でインストー

>pip install psycopg2

動作確認

>python
Python 3.4.3 |Anaconda 2.3.0 (64-bit)| (default, Mar  6 2015, 12:06:10) [MSC v.1
600 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import psycopg2
>>>

エラーなしでOK!
たった手順2つで簡単に環境が揃いました。
あとは、psycopg2のAPIで接続やテーブル作成してみて動いていることを確認。

2. heroku環境でpostgresをAddon追加・動作確認

実はherokuでのpostgres環境構築は、windows環境よりはるかに楽でした。
(というかwindows側いらなかったと反省、インストールも早いし)

やることは、「herokuアプリケーションに heroku-postgresql Addonを追加」する。

herokuはpostgresをaddonとして対応(無料)しているのでこれを使う。
しかもheroku-postgresqlはherokuと独立したインスタンスを提供されます。
なので、ローカルで動作確認したい場合は、上記1.1で飛ばして、ローカルからpsycopg2で接続すればおk!

2.1. heroku-postgresql Addonの確認

確認方法1 (GUI)
https://dashboard.heroku.com/apps/application_id/resources
※application_idは自分のlinebotをdeployしているアプリケーションID
ここにheroku-postgresql Addonがあるか確認

確認方法2 (CUI)

>heroku addons --app <application_id>

無ければ次へ。

2.2. heroku-postgresql Addonの追加

>heroku addons:add heroku-postgresql --app <application_id>

下図のように、https://dashboard.heroku.com/apps/application_id/resourcesに追加されています。

f:id:motojapan:20170709165719p:plain

2.3. heroku-postgresql Addonの設定取得

>heroku config --app <application_id>
DATABASE_URL:              postgres://aaaa:bbbb@cccc:dddd/eeee
LINE_CHANNEL_ACCESS_TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxxxx
LINE_CHANNEL_SECRET:       yyyyyyyyyyyyyyyyyyyyyyyyyyy

今回重要なのは、[DATABASE_URL]説明は次の通りです。

aaaa user_name
bbbb password
cccc server_name (私の環境はawsの模様)
dddd port
eeee database_name

psycopg2での接続は次のような対応になります。

import psycopg2
user   = aaaa
pwd    = bbbb
server = cccc
port   = dddd
db     = eeee        
con = psycopg2.connect("host=" + server + " port=" + port + " dbname=" + db + " user=" + user + " password=" + pwd)

なので、ローカル環境でもdeploy先のherokuでも上記で繋げばOKです。

【Messaging API #2】reply_messageで「request body has 1 error」が起きる (python)

herokuにdeployしていた下のコード。
ちゃんと動いていたはずのreply_messageがいつからかErrorを吐いて終了していたので覚書。

変な文字列でも入れたのかと切り分けを進めたが、結果的には、contentsの文字列が長すぎた模様。

line_bot_api.reply_message(
    event.reply_token,
    TextSendMessage(text=contents)
)

大体、マルチバイト含んで2000文字近く送ろうとした時に落ちてる。

OK NG
len(contents) 1709 2008
len(contents.encode(utf-8)) 2401 3092

headerとかもろもろのオーバーヘッドがあるので、具体的によくわかないけど目安がてら。


対策は下記2つ。

1. contentを分けて、reply_messageを細かく送信

予想はしていましたが、結果としてもダメでした。
event.reply_tokenは、①30秒以内に、②1度のみ送信できるルールでした。

2. contentを分けて、push_messageを細かく送信

これは成功しました。

(例)

for content in contents.split('\n')
    try:
        line_bot_api.push_message(user_id, TextSendMessage(text=content))
    except LineBotApiError as e:
        print(e)

めでたしめでたし。

【Messaging API #1】 クライアントのユーザID(user_id)とアカウント名(display_name)の取得方法 (python)

久しぶりにMessaging APIを使ってLinebotを書いてますが、メッセージを送信してきたクライアントのユーザIDとアカウント名の取得方法を忘れそうなので、書き残し。
クライアント側から「おはようございます」とpostすると、Webhookされたサーバーサイドのeventの中にこんな情報が入ってきます。
eventはWebhookParserでparseした中にiteratableで入っています。

event {
    "message": {"id": "xxx", "text": "おはようございます", "type": "text"}, 
    "replyToken": "yyy", 
    "source": {"type": "user", "userId": "zzz"}, 
    "timestamp": 14994xxxxxxxx, 
    "type": "message"
}

ここで得られる"userId": "zzz"が、ユーザIDに相当し、ここからget_profileでアカウント名を得ます。

if isinstance(event.source, SourceUser):
    profile = line_bot_api.get_profile(event.source.user_id)
    user_id        = event.source.user_id # ユーザID (zzz)
    user_disp_name = profile.display_name # アカウント名


終了です。

これで得られたユーザIDでDB登録をしたりできますね。

【kaggle⑤】初心者がタイタニック号の生存予測モデル(Titanic: Machine Learning from Disaster)をやってみる(機械学習、DeepLearning)

これの続きで、今回で一通り終わります。
【kaggle④】初心者がタイタニック号の生存予測モデル(Titanic: Machine Learning from Disaster)をやってみる(学習データ相関確認、分割、正規化) - MotoJapan's Tech-Memo

3. 機械学習

今回は下記6パターンをそれぞれ比較しました。
・Random Forest
・K Nearest Neighbor
・SGDClassifier(パーセプトロン)
Support Vector Machine
・Gradient Boosting Decision Tree
Deep Learning(by Chainer)

精度面では結論から言うといまのところ、Support Vector Machineが最も優秀でした。
Random Forest / Support Vector Machine / Deep Learning (by Chainer) がタイでベストスコア。(K Nearest Neighborが6%程悪い)
(17/07/15更新)


上記の内、古典的アルゴリズムは、sklearnに含まれています。
Deep LearningはChainerでシンプルなFFNNを構成しています。
実装はすべてgitにあるので参考にしてください。
sklearnに含まれているものは、どれも同じような呼び方で3行程度書けば学習もでき、評価もsklearn.metricsを使うと簡単です。(素晴らしいですね)

学習方法

sklearnに含まれるアルゴリズムでは、次の5手順で基本的には終わり

K近傍法の場合は下記

#1.アルゴリズムのインポート
from sklearn.neighbors import KNeighborsClassifier
#2.パラメータを指定してアルゴリズムインスタンス生成
knc = KNeighborsClassifier(n_neighbors=3)
#3.学習データで学習
knc.fit(x_train_std, y_train) 
#4.評価データで推定
y_true, y_pred = y_eval, knc.predict(x_eval_std)

ここでのパラメータはハイパーパラメータと呼ばれており、値によって精度が大きく変わる。
理詰めでハイパーパラメータのあたりをつけるのも良いが、組み合わせ爆発を起こすため、自力で求めず、グリッドサーチといった手法を使う方がベター

pythonには、sklearn.grid_search があるのでこれを使ってインスタンスを生成してベストなパラメータを求める。

今回は、Support Vector Machine / Gradient Boosting Decision Tree で利用。

精度評価

精度評価にはprecision, recall, f1 scoreが用いられるが、忘れやすいので、ここに書き残しておく。

説明 数式
precision 適合率 (正確性:システムが正しいと判定したものの内、本当に正しく判断できたのの割合) tp/(tp+fp)
recall 再現率 (網羅性:正しく判断されるべきものの内、本当に正しく判断できたのの割合) tp/(tp+fn)
f1 score F値 (2×適合率×再現率)/(適合率+再現率)

pythonでは上記の情報を1行でかける

from sklearn.metrics import classification_report
print(classification_report(y_true, y_pred, target_names=["not Survived", "Survived"]))

f:id:motojapan:20170630005035p:plain
これを用いて各精度比較を進める。

各精度比較

古典的アルゴリズム

F値精度を比較しました。
f:id:motojapan:20170630013252p:plain

  • SVMは精度が軒並み高く優秀
  • 一方、ワーストはSGD
    • 特にSurvivedの認識精度が悪い(データ数の問題か、表現力の問題か、、)

DeepLearning

FFNN (DeepLearning by Chainer)については、4層モデルを構築した。
1層当たりのunit数を調整しながら検証したが、8程度が過学習しないちょうどよいモデルであった。
ただし、データ量も少なくあまり良い成果は出なかった。(SVMには劣る結果)

unit数=8の場合

学習曲線(損失)
f:id:motojapan:20170630021025p:plain
学習曲線(精度)
f:id:motojapan:20170630021035p:plain

データ正規化比較

アルゴリズムを100回実行した場合の平均値は下記の通りとなりました。
これはF値ではなく、kaggleで扱われる単純な認識精度となります。

f:id:motojapan:20170630015326p:plain

  • 全体傾向は改善方向
  • KNN、SGDは大幅な精度改善が見える 5~10ポイント以上の改善
  • SVMでも、0.14ポイント精度改善
  • 手法によって精度が悪くなるパターンもあるがほぼステイ

正規化による精度改善がうかがえます。


以上、精度 78.974% (順位1777位タイ / 7082 teams) の話でした。

時間ができたら、もっとしっかり特徴量設計したいなぁ。

精度向上のための特徴量ToDo

  • 年齢ではなく年齢層を用いる
    • 1桁精度の年齢は実は不要な可能性がある
  • データ数を間引く
    • 全データを必ず使わなければならないというルールはなく、むしろ無理やりフィッティングすることで汎化性能が下がる可能性が高い
    • 信頼性のあるデータのみを使うほうがよいかも

【kaggle④】初心者がタイタニック号の生存予測モデル(Titanic: Machine Learning from Disaster)をやってみる(学習データ相関確認、分割、正規化)

これの続き~
【kaggle③】初心者がタイタニック号の生存予測モデル(Titanic: Machine Learning from Disaster)をやってみる(特徴量生成と生存関係の可視化) - MotoJapan's Tech-Memo

2.8. データ相関再確認と分割

df_train_dropna = df_train.copy()
df_train_dropna = df_train_dropna.drop('Cabin', axis = 1)
df_train_dropna = df_train_dropna.drop('PassengerId', axis = 
df_train_dropna = df_train_dropna.drop('Age', axis = 1)
df_train_dropna.corr()

f:id:motojapan:20170629232257p:plain

  • [Pclass],[Sex]は相関が高いデータ
  • [Age_1]より[Age_2]の方が傾向は良い
  • [Honorific]はかなり有効そう

データ解析の結果から次が注目点となる:

  • Pclass, Sex, FamilySize, Age_2(Honorific) を特徴量として使える可能性
  • SibSpとFamilySizeの差
  • Fareの関係性
  • Honorificは、Sexよりよい特徴量の可能性
    • maleの内、masterは生き残りやすいことが想定される

Fare等も良い相関が良いデータだか
今回は前回のグラフからも[FamilySize]からデータ傾向がうかがえたので、[FamilySize]を特徴量として活用してみる。

2.9. データセット準備

今回学習させる特徴量は、次の5つ

Pclass 客席クラス
Sex 性別
FamilySize 家族数(新規生成した特徴)
Honorific  敬称
Age_2 敬称で補完した年齢

学習データは学習用と評価用で分割しますが、pythonなら1行です。

from sklearn.cross_validation import train_test_split
#データセット作成
features = ['Pclass', 'Sex', 'FamilySize', 'Age_2', 'Honorific']
x = df_train[features]
y = df_train['Survived']
x_train, x_eval, y_train, y_eval = train_test_split(x, y, test_size=0.2, random_state=0)

2.10. データの正規化

学習データセットは、平均 0 / 分散 1 へ正規化

from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
sc.fit(x)
x_std = sc.transform(x)
sc.fit(x_train)
x_train_std = sc.transform(x_train)
sc.fit(x_eval)
x_eval_std = sc.transform(x_eval)

詳細は次回になりますが、正規化により、K近傍法(n_neighbors=3)では
73.49% -> 78.82% 精度向上が確認できました。(100回試行の平均値)

やっぱり正規化しておいたほうがよさそうです。


次回は今回準備した学習データセットを用いて、機械学習

【kaggle③】初心者がタイタニック号の生存予測モデル(Titanic: Machine Learning from Disaster)をやってみる(特徴量生成と生存関係の可視化)

これの続きです。
【kaggle②】初心者がタイタニック号の生存予測モデル(Titanic: Machine Learning from Disaster)をやってみる(データ分析、整形、欠損データ補完) - MotoJapan's Tech-Memo

2.7. 特徴量毎の生存分布確認(可視化)と特徴量生成

データの可視化は非常に重要。
pandasを使うと1~2行程度表やグラフを描けるのでどんどん使います。
可視化方法においては※1を参考にさせて頂きました。
データを表/グラフ化し、機械学習を行う特徴量を選定していきます。  

2.7.1 可視化(表化、グラフ化)

各特徴量と[Survived]の相関を見てみます。
ちなみに1行で書けます。

df_train[['Pclass', 'Survived']].groupby(['Pclass'], as_index=False).mean()

結果はこんな以下のような感じです。

上級クラスの方が平均的には生き残っているようです。
f:id:motojapan:20170629003018p:plain


ということで他も見てみます。

df_train[['Sex', 'Survived']].groupby(['Sex'], as_index=False).mean()
df_train[['SibSp', 'Survived']].groupby(['SibSp'], as_index=False).mean()
df_train[['Parch', 'Survived']].groupby(['Parch'], as_index=False).mean()
df_train[['Honorific', 'Survived']].groupby(['Honorific'], as_index=False).mean()
  • 女性が残る傾向あり

f:id:motojapan:20170629003120p:plain

  • 1人乗船、もしくは、兄弟配偶者の数は2人以下の生存率が高い

f:id:motojapan:20170629003136p:plain

  • 家族ずれの生存率は高いが、多すぎると逆に生存率が下がる

f:id:motojapan:20170629003347p:plain

  • 1:Mrs, 2:Miss 3:Master が同様に生き残る傾向にあり、

女性子供が優先的に救命ボートに乗れたようです。
f:id:motojapan:20170629003220p:plain


グラフ化もしておきますが、gitに書いてあるのでコードは割愛します。

  • 乗船クラスが高い程、生存率が高い

f:id:motojapan:20170629010414p:plain

  • 女性の生存率が高い

f:id:motojapan:20170629010429p:plain

  • 素のAge分布(欠損あり)

f:id:motojapan:20170629010438p:plain

  • Age_1) 20代前半にピークが際立っており、偏りがある

f:id:motojapan:20170629010445p:plain

  • Age_2) Age_1に比べて年齢がばらついている

f:id:motojapan:20170629010512p:plain

  • 0:Mrの生存率がかなり悪い

f:id:motojapan:20170629010543p:plain
だいぶ直観的になってきました。
前回補完した[Age_1]に比べて、[Age_2]の方が、ピークが抑えられている。

2.7.2 特徴量生成

難しいことはしていません。
乗客自身を含めた家族人数[FamilySize]という特徴量を生成します。
これで、[SibSp][Parch]をまとめて意味のある次元圧縮が可能となります。

次元の呪いを避けるためにも、次元削減は検討しましょう。
次元圧縮には、正規分布を仮定したPCA(主成分分析)やそれを仮定しないICA(独立成分分析)、などもありますが、今回は触れません。

df_train["FamilySize"] = df_train["SibSp"] + df_train["Parch"] + 1
df_train[['FamilySize', 'Survived']].groupby(['FamilySize'], as_index=False).mean()

家族の人数が1~4人の生存率は高い。
特に1人乗りは、総じて生存率が高いわけではなく、
ある程度人数がいた方が生存率が高いです。

救命ボートに乗りやすかったのか、
危険な情報の共有ができたのかもしれません。
f:id:motojapan:20170629003203p:plain
f:id:motojapan:20170629010525p:plain


可視化と特徴量生成は以上です。
次回は、いままでの情報を元に学習用データセットを準備です。

【kaggle②】初心者がタイタニック号の生存予測モデル(Titanic: Machine Learning from Disaster)をやってみる(データ分析、整形、欠損データ補完)

これの続きです。

【kaggle①】初心者が Titanic: Machine Learning from Disasterをやってみる(Titanic概要 ~ データ確認) - MotoJapan's Tech-Memo


(ここからはpythonのpandasが便利すぎるというポジティブキャンペーンになります)
ちなみに「ここでわかってきたことは」を読めば記事の重要ポイントは大体把握できます。 

2.4.データ整形

前回の記事の続きですが、データを整形します。
男女(Sex)情報は文字列であるため、バイナリ(0/1)変換。

#男女を01で表現
df_train = df_train.replace("male",0).replace("female",1)

2.4.データ傾向確認

pandasのDataFrameで全体的なデータの傾向を可視化(便利!)

#各特徴量の大枠の分布を表示
df_train.describe()
#欠損データの集計を表示
df_train.isnull().sum()

f:id:motojapan:20170613223519p:plain
f:id:motojapan:20170613223627p:plain

ここでわかってきたことは

  • [Age]が一部欠損
  • [Cabin]は2/3以上欠損

今回は、欠損が多き過ぎるものは、学習させる特徴量としては一旦除外しておきます。  
あと、[PassengerId]は便宜上ついているだけの通し番号ですのでこれも除外します。

次で簡単に除外できます。(ありがとう、pandas様)

#学習に不要なデータを排除
df_train_dropna = df_train.dropna()
df_train_dropna = df_train_dropna.drop('Cabin', axis = 1)
df_train_dropna = df_train_dropna.drop('PassengerId', axis = 1)

2.5.データ相関分析

つぎにもう少し踏み込んで分析したいと思います。が、pandasでは既に簡単な分析ができる関数が準備されています(ありがとう!)

#一次相関のconfusion matrixを表示
df_train_dropna.corr()

f:id:motojapan:20170613225537p:plain

ここでわかってきたことは

  • [Sex][Age]は[Servived]に対して相関が高い
  • 逆に[Fare][Pclass]は相関が低い

乗船料金や旅客席クラスの高い人が優先的に救助されたというわけではなさそうですね。
年齢は一部欠損していますが、相関が高いことからここをいかにうまく補完できるかが鍵のようです。

2.6.欠損データ補完

[Age]は部分欠損を起こしてるため、補完処理を実施
2パターンを検証

  • 2.6.0 [Age]平均値で補完 #今回は割愛
  • 2.6.1 [Pclass]毎の[Age]平均値で補完 ([Age_1])
  • 2.6.2 [Name]の敬称毎の[Age]平均値で補完 ([Age_2])

2.6.0の[Age]平均値で補完は、初期検討ではいいですが、割と乱暴な手法なので今回は割愛させて頂きます。

2.6.1.[Pclass]毎の[Age]平均値で補完 [Age_1]

べたべたなやり方で、confusion matrixの[Age]と最も相関が高い[Pclass]で補完する方法です。

ap = pd.concat([df_train['Age'],df_train['Pclass']],axis=1)
ap = ap.dropna()
ap.plot(x='Age',y='Pclass',kind='scatter')
plt.xlabel('Age')
plt.ylabel('Pclass')
plt.show()

f:id:motojapan:20170617173754p:plain
ヒストグラム版はこんな感じ
f:id:motojapan:20170617174650p:plain

if Pclass == 1, Average Age == 38.2
if Pclass == 2, Average Age == 29.9
if Pclass == 3, Average Age == 25.1

ここでわかってきたことは

  • [Pclass]客室階級は高い程、平均年齢が高い傾向は見れる
  • 一方で、子供(例えば12歳以下)等の考慮ができない補完方法
    • 子供はどの[Pclass]にも存在するし、そもそも子供に妥当な年齢を与えられない

2.6.2 [Name]の敬称毎の[Age]平均値で補完 [Age_2]

結果を先に書くと、[Age_1]に比べて、精度は2%程向上しました。(記憶が正しければ)
※1のアプローチを参考にさせて頂きました。
これは、[Name]という一見扱いずらそうな文字列を特徴量にします。

手法

  • 名前から敬称[Honorific]を探す
  • 下記でラベリングする
    • mr->0, mrs->1, miss->2, master->3, ms.->4, dr.->5, else->6
  • [Honorific]毎の平均年齢を確認
  • 傾向が高いもので年齢補完する
    • elseとかは全体平均などで補う

ソースコード等は割愛しまする (GitHubにコードあり)
f:id:motojapan:20170617182137p:plain


ここでわかってきたことは

  • [Age_1]に比べて、敬称毎の年齢はばらつきが少なく、優秀そう(優秀でした)
  • ms.->4, dr.->5, else->6は数も少ないため、全体平均で代替でよさそう

以上、今回はここまでです。
次は特徴量生成と可視化。