のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

Pandasで特徴量取得する場合に使う操作をまとめてみた

皆さんこんにちは
お元気ですか。私は元気です。

分析は基本的にPythonを使って行います。(大体Pandasですが・・・)
Pandasを利用すると色々できます。が、ふとどうするんだっけ処理が増えていきました。

自分のメモがてらカテゴリを分けて記録に残したいと思います。
最後のほうは特徴量の作り方集になっています。

Kaggleで実際に使ったことがある処理も数多く掲載しました。
思いついたら随時、追加しようと思います。

準備

本コードは事前に「import pandas as pd」を利用しています。
ファイルの操作、結合などを行うファイルを準備します。
ファイルの内容は次の通りです。

■df_example1.csv

名前 性別 年齢
太郎 21
二郎 30
三郎 21
四郎
花子 14

■df_example2.csv

名前 部活動
太郎 陸上
二郎 野球
四郎 野球
花子 華道

■df_example3.csv

名前 性別 年齢
五郎 22
六郎 21
七郎 21
八郎 3
九子 14

■df_example4.csv

趣味 授業
データ分析 地理
ゲーム 世界史
ゲーム 理科
読書 国語
Neural Network 数学

ファイル操作

読み込み

df1 = pd.read_csv("df_example1.csv")
df2 = pd.read_csv("df_example2.csv")
df3 = pd.read_csv("df_example3.csv")
df4 = pd.read_csv("df_example4.csv")
df = pd.read_csv("temp.tsv", separete="\t") # TSVファイル
df = pd.read_excel("temp.xlsx") # Excelファイル

書き込み

df1.to_csv("temp_w.csv",index=False) # IndexがTrueの場合はIndexも出力される。大体は不要

テーブル操作

1行ごとに処理をする。

for index, row in df1.iterrows():
     print (index, row)

複数列を取得する。

print (df1[["名前", "年齢"]])

#   名前  年齢
#0  太郎  21
#1  二郎  30
#2  三郎  21
#3  四郎  NAN
#4  花子  14

選択操作

テーブル条件の指定
print (df1[df1["性別"] == "男"])
#   名前 性別    年齢
#0  太郎  男  21.0
#1  二郎  男  30.0
#2  三郎  男  21.0
#3  四郎  男   NaN
複数条件の指定

&で結合すれば、複数の条件を選択できる。(|を指定すればOR)

print (df1[(df1["性別"] == "男") & (df1["年齢"] > 25)])

#    名前 性別    年齢
# 1  二郎  男  30.0

NaNを埋める。

NaNになる値を埋めたいケースがあります。

print (df1.fillna(-1))
#   名前 性別    年齢
#0  太郎  男  21.0
#1  二郎  男  30.0
#2  三郎  男  21.0
#3  四郎  男  -1.0
#4  花子  女  14.0

カラム、テーブルの統計情報を取得する。

df1["年齢"].mean() # 出力は省略
df1["年齢"].median() # 出力は省略
df1["年齢"].max() # 出力は省略
df1["年齢"].min() # 出力は省略
df1.describe()

#              年齢
#count   4.000000
#mean   21.500000
#std     6.557439
#min    14.000000
#25%          NaN
#50%          NaN
#75%          NaN
#max    30.000000
完全一致の列を発見し、除去する。

重複している列を発見します。重複しているカラムは必要がないことから
重複を調べ除去したいことがあります。
この除去には、pandasのdrop_duplicateを使います。

df.T.drop_duplicates().T

日付操作

日付から日などの情報を取得する。

特徴量を生成する際に日付から曜日などの特徴を取得したいケースがあります。
その日付から新しい特徴量を作成できます。

date = pd.date_range('2017-01-01 00:00', periods=1, freq='D')[0]
print (date) # 2017-01-01 00:00:00
print (date.dayofweek) # 6
print (date.month, date.day, date.hour, date.minute) # 1 1 0 0

2つ以上のDataFrameの結合操作

内部結合

2つ以上のテーブルを結合する。RDBのjoinと同じ操作です。

print (pd.merge(df1, df2, how="inner", on="名前"))   

#    名前 性別_x    年齢 性別_y 部活動
# 0  太郎    男  21.0    男  陸上
# 1  二郎    男  30.0    男  野球
# 2  四郎    男   NaN    男  野球
# 3  花子    女  14.0    女  華道

外部結合

print (pd.merge(df1, df2, how="outer", on="名前"))  
#   名前 性別    年齢  部活動
#0  太郎  男  21.0   陸上
#1  二郎  男  30.0   野球
#2  三郎  男  21.0  NaN
#3  四郎  男   NaN   野球
#4  花子  女  14.0   華道

2つの結合処理

Onを利用した結合をしない処理はpd.concatで可能です。

pd.concat([df3, df4], axis=0) #縦方向の結合

#   名前 性別    年齢
#0  太郎  男  21.0
#1  二郎  男  30.0
#2  三郎  男  21.0
#3  四郎  男   NaN
#4  花子  女  14.0
#0  五郎  男  22.0
#1  六郎  男  21.0
#2  七郎  男  21.0
#3  八郎  男   3.0
#4  九子  女  14.0

pd.concat([df3, df4], axis=1) #横方向の結合

#   名前 性別    年齢              趣味   授業
#0  太郎  男  21.0           データ分析   地理
#1  二郎  男  30.0             ゲーム  世界史
#2  三郎  男  21.0             ゲーム   理科
#3  四郎  男   NaN              読書   国語
#4  花子  女  14.0  Neural Network   数学

集計操作を使った特徴量作成

結合したデータを使って、Group Byを利用して集計します。
例えば、2つ以上のカラムを利用して集計する場合に使います。

2つのカラムを組み合わせて統計を取り、特徴量にする場合

2つのカラムを組み合わせて統計を取り、それを特徴量にしたい場合があります。
その場合はgroupby reset_index, mergeを使います。

merged_df = pd.merge(df1, df2, how="outer", on="名前")      
mean = merged_df.groupby(["性別"]).mean()                                               
reset_mean = mean.reset_index()                                                                   
print(merged_df.merge(reset_mean, how="left", on="性別"))   

#   名前 性別  年齢_x  部活動  年齢_y
#0  太郎  男  21.0   陸上  24.0
#1  二郎  男  30.0   野球  24.0
#2  三郎  男  21.0  NaN  24.0
#3  四郎  男   NaN   野球  24.0
#4  花子  女  14.0   華道  14.0

変数の変換を行う。

カテゴリカルな変数

ダミー変数に変換し、結果を結合する。
dummies_df = pd.get_dummies(df1["性別"))
pd.concat([df1, dummies_df], axis=1)

#   名前 性別    年齢    女    男
#0  太郎  男  21.0  0.0  1.0
#1  二郎  男  30.0  0.0  1.0
#2  三郎  男  21.0  0.0  1.0
#3  四郎  男   NaN  0.0  1.0
#4  花子  女  14.0  1.0  0.0
ラベル変数にし、結果を取得する。
from sklearn.preprocessing import LabelEncoder
df1["性別"] = LabelEncoder().fit_transform(df1["性別"])

#   名前  性別    年齢
#0  太郎   1  21.0
#1  二郎   1  30.0
#2  三郎   1  21.0
#3  四郎   1   NaN
#4  花子   0  14.0

Numpyへの変換を行う。

私は以下の2つのどちらかで変換を行います。大体np.arrayで実行しています。

df1.as_matrix()
np.array(df1)

最後に

pandas長く使っていたので纏めてみると結構多くてびっくりです。
特徴量作成にまだまだ使えるので、ぜひ使ってみてください。

Anacondaを使ったPythonの環境構築

皆さんこんにちは
お元気ですか。私は暇です(だと信じています)。
最近ブログタイトルと本人の忙しさが一致していないと言われるので
とりあえず暇ですといってみます。

本日は、Anacondaについて書きます。

Anacondaについて

Anacondaとは

AnacondaはPythonのデータサイエンスプラットフォームで
100以上のライブラリを簡単に導入できます。

Pythonのバージョンを仮想環境を使って切り替えることや
科学技術演算ライブラリ(numpyなど)のインストールを行うことを簡単にできます。
Windowsを全く利用していなかった頃は気にしていなかったのですが、
numpyやscipyを簡単に使えます。(pipで入れるとC環境の関係でハマったことがあります。)

インストール方法

まずは、インストールです。インストール用パッケージは
Anacondaの公式サイトで公開されています。
インストール先のOSによって媒体が異なるので、適切な媒体を選択しましょう。

Download Anaconda Now! | Continuum

GUIだとクリックするとインストールがはじまりますのでその手順に従いましょう。
また、CUIの場合は、公式にも掲載されている以下のコマンドを
利用すればインストールできます。
後半のshのファイルパスはインストール環境に併せて変更してください。

$ bash Anaconda3-4.2.0-Linux-x86_64.sh

但し、このコマンドは対話的に設定を行うので、Dockerfileのコマンドなど
対話的で行えない環境の場合は追加でオプションが必要です。
その場合は次のコマンドを利用しましょう。

$ bash Anaconda3-4.2.0-Linux-x86_64.sh -b -p /root/anaconda3

オプションですが、-bはバッチモードです。
例えば、Dockerfileに書く時のように対話的に実行できない場合に利用します。

  • pはAnacondaのインストール先のパスを設定します。

最後に環境変数を設定します。Anaconda環境を標準の初期設定にしたい場合は、
.bashrcに予めexport文を書いておきましょう。

$ export PATH=/root/anaconda3/bin:$PATH

そして、「python」とコマンドを叩き、以下のようになると成功です。
特に確認する必要がある項目はAnaconda 4.2.0が表示されていることです。

$ python
Python 3.5.2 |Anaconda 4.2.0 (64-bit)| (default, Jul  2 2016, 17:53:06)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

condaを使って環境を構築・確認する

Anacondaは付属のcondaでPythonの環境を作ったり、
新しいライブラリをインストールできます。
実際に構築してみましょう。

ライブラリのインストール

Anacondaにライブラリをインストールする時は、condaコマンドを使います。
まずは、conda searchを使い、インストールライブラリを確認します。
例えば、tensorflowをインストールする場合、次のようなコマンドを実行します。

$ conda search tensorflow
Fetching package metadata .......
tensorflow                   0.10.0rc0           np111py27_0  defaults
                             0.10.0rc0           np111py34_0  defaults
                             0.10.0rc0           np111py35_0  defaults

これらのパッケージをインストールするには、installコマンドを使います。

$ conda install tensorflow

但し、conda searchで見つからない見つからないライブラリはpipでインストールしましょう。

condaで入れたライブラリの一覧

conda listを入力するとライブラリの一覧を確認できます。

$ conda list
# packages in environment at /root/anaconda3:
#
_license                  1.1                      py35_1
_nb_ext_conf              0.3.0                    py35_0
alabaster                 0.7.9                    py35_0

仮想環境の構築と削除

仮想環境を構築します。今回は試しにPython3.6の仮想環境を構築します。

$ conda create -n py36 python=3.6 anaconda

構築した環境の確認は以下のようにできます。

$ conda info -e
# conda environments:
#
py36                  *  /root/anaconda3/envs/py36
root                     /root/anaconda3

また、構築した仮想環境ですが、removeコマンドで削除できます。

$ conda remove -n py36 --all
$ conda info -e
# conda environments:
#
root                  *  /root/anaconda3

仮想環境にライブラリをインストールする

インストールした環境にライブラリがない場合があります。
その場合、インストール方法は2つあります。

  1. conda searchを使い、検索し、インストールをする。
  2. pipを使ってインストールをする。

基本的には、conda searchしても見つからない場合はpipを使ってインストールします。

仮想環境の切り替え

この環境をactivateするには、sourceでこのコマンドを動かします。

$ source activate py36

逆に環境を無効化したい場合はdeactivateを使います。

$ source deactivate

condaのupdate

conda自身を更新することができます。
まずは、versionを確認しましょう。

$ conda --version
conda 4.2.9

次にversionを更新します。非常に簡単で、conda updateを使います。

$ conda update --prefix /root/anaconda3 anaconda

conda自身の情報

conda自身がどのような状態かは、conda infoを使えば確認できます。

$ conda info
Current conda install:

               platform : linux-64
          conda version : 4.3.13
       conda is private : False
      conda-env version : 4.3.13
    conda-build version : 2.0.2
         python version : 3.5.2.final.0
       requests version : 2.12.4
       root environment : /root/anaconda3  (writable)
    default environment : /root/anaconda3
       envs directories : /root/anaconda3/envs
                          /root/.conda/envs
          package cache : /root/anaconda3/pkgs
                          /root/.conda/pkgs
           channel URLs : https://repo.continuum.io/pkgs/free/linux-64
                          https://repo.continuum.io/pkgs/free/noarch
                          https://repo.continuum.io/pkgs/r/linux-64
                          https://repo.continuum.io/pkgs/r/noarch
                          https://repo.continuum.io/pkgs/pro/linux-64
                          https://repo.continuum.io/pkgs/pro/noarch
            config file : None
           offline mode : False
             user-agent : conda/4.3.13 requests/2.12.4 CPython/3.5.2 Linux/4.9.4-moby debian/jessie/sid glibc/2.19
                UID:GID : 0:0

最後に

一通りAnacondaを使った環境構築をやってみました!
・・・大変だった。。

Cookiecutterを使ってテンプレートからプロジェクトを作成する

皆さんこんにちは
お元気ですか。ユニクロの極暖Tシャツ着ているとかなり暑い。。。冬なのに

今日は、Cookiecutterを使ったテンプレートからの複製生成を試みます。
テンプレートからちょっと変えたい部分を用意することで、簡単に
似たプロジェクトを作成できます。

Cookiecutterについて

Cookiecutterは雛形となるテンプレートプロジェクトから
プロジェクトを生成するツールです。
雛形となるプロジェクトを作ってしまえば、少し変更したい箇所を指定して
新しいプロジェクトを作ります。

github.com

インストール

cookiecutterをpypiから取得できます。

sudo pip install cookiecutter

Cookiecutterを使った複製をやってみる。

頻繁に利用がありそうなテンプレートの雛形は既に用意されています。
とりあえず、何も考えずに作ってみましょう。

GitHub - ionelmc/cookiecutter-pylibrary: Enhanced cookiecutter template for Python libraries.の雛形を使用する場合は
次のように実行します。

cookiecutter gh:ionelmc/cookiecutter-pylibrary

途中で色々と聞かれるので必要のある箇所は入力します。

full_name [Ionel Cristian Maries]: tereka
email [contact@ionelmc.ro]:
website [https://blog.ionelmc.ro]:
github_username [ionelmc]:
project_name [Nameless]:
repo_name [python-nameless]:
package_name [nameless]:
distribution_name [nameless]:
project_short_description [An example package. Generated with cookiecutter-pylibrary.]:
release_date [today]:
Select year:
(略)

他にもflaskやdjango、restのプロジェクトがあります。
これで、確認すると変数となっていた箇所が指定した名前に
置き換わっていることを確認できます。
次はテンプレートを構築してみます。

テンプレートを作成に挑戦してみる。

準備されているテンプレートでは不足することもあるでしょう。
そんなときのために、自分で1からテンプレートを作ってみます。

ディレクトリの雛形は以下の通りです。

ディレクトリ構成

treeコマンドを使ってcookiecutterの雛形の構成を表示します。

.
└── TestProject
    ├── cookiecutter.json
    └── {{cookiecutter.project_name_lower}}
        └── README.md
cookiecutter.json

cookiecutter.jsonにcookiecutterの雛形で使う変数を定義します。

{
  "project_name" : "test-project",
  "create_document": "y",
  "license": ["MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0"],
  "project_name_rower" : "{{ cookiecutter.project_name|replace('-', '_') }}"
}

{{ cookiecutter.<変数名> }}とすることで、その変数を埋め込めます。
また、埋め込んだ変数の文字を"-"→"_"に置換しています。
licenseの箇所はリストにしています。リストをjsonで入力した場合、
対話的に実行する時に候補を表示します。

README.md

プロジェクト内にあるREADME.mdにcookiecutterで
定義した変数を埋め込むテストをします。
今回、テストとして用意した文書は以下の通りです。

{{ cookiecutter.project_name}} document.

{% if cookiecutter.create_document == 'n' -%}
   No Document
{%- endif %}

{% if cookiecutter.create_document == 'y' -%}
{{cookiecutter.license}}

This is an apple.
{%- endif %}

このREADME内で、cookiecutterの変数を使えることは同じです。
追加で、if文を使った条件式を構築できます。上記の場合、
{% if cookiecutter.create_document == 'n' -%}はcreate_document変数がnの場合に動作し、
{% if cookiecutter.create_document == 'y' -%}はcreate_document変数がyの場合に動作します。

実行

コマンドラインからcookiecutter <ディレクトリ>で実行できます。
但し、ディレクトリ内にcookiecutter.jsonがないと実行できません。

$ cookiecutter TestProject/
project_name [test-project]: test-project
create_document [y]: y
Select license:
1 - MIT
2 - BSD-3
3 - GNU GPL v3.0
4 - Apache Software License 2.0
Choose from 1, 2, 3, 4 [1]: 2
project_name_lower [test_project]:

cookiecutter.jsonにリストで定義した設定は番号で選択できるようになっています。

作られたプロジェクトを確認する

最後に作ったプロジェクトを確認します。
まず、cookiecutterの変数になっていたディレクトリが
「test_project」の名前になりました。

.
└── test_project
    └── README.md
README.md
test-project document.



BSD-3

This is an apple.

README.mdを見ると、変数名が埋め込まれています。(test-project)
また、create_documentの条件が"n"となっている箇所が表示されていないことを確認できます。
更に、条件式が"y"の時に表示される文章を確認できました。

最後に

cookiecutterを使うと、基本となる雛形から
オリジナルのプロジェクトを簡単に作成できます。

ちょっとだけ変えたプロジェクトを作成したいことは結構あります。
意外に応用できそうなので、もっと使ってみたいと感じています。

クリスマスにもなってカノジョがいないからカノジョを作ってみた。

皆さんこんにちは
お元気ですか?私はぼっちです。

本記事は「カノジョできない機械学習エンジニア」の最終日です。
qiita.com

本日の話の流れは次のとおりです。

はじめに

カノジョがいなくて寂しい。こんなクリスマスを送る男性の皆様は結構いらっしゃるのでは
ないでしょうか(と信じたい)。

カノジョを作るために、世間でよく言われるのは「行動が大事だ」ということです。
そこで、本クリスマスを機に、カノジョを作成し心の溝を埋めることに挑戦したいと思います。

カノジョがいないことに対する解決法

「カノジョ」がいないのであれば、作れば良い。

私はエンジニアです。
寂しいので、これを機に自己を振り返り、カノジョを作れるのではないかと考えました。
まずは、「カノジョ」を設計する必要があります。

カノジョについての考察

カノジョとは

デジタル大辞泉」には次の通り記載されている。

[代]三人称の人代名詞。話し手、相手以外の女性をさす語。「彼女は遅れるらしい」⇔彼/彼氏。
[名]愛人、恋人である女性。「彼女ができた」⇔彼/彼氏。

本記事では、「愛人、恋人である女性」と仮定し話を進めましょう。

理想のカノジョとは

理想のカノジョとは何か。
クリスマスといえば、やっぱりクリスマスデートして・・というのが
定番かなぁと思います。
デートで大事なのは、会話かなと思います。やっぱり盛り上がる方が良いでしょう。
そこで理想のカノジョは会話が最も大事だと今回仮定します。

理想のカノジョ作成方針

会話が最も大事だと決めた上で理想のカノジョを作る必要があります。
調べてみると、ニューラルネットワークを使った会話モデルがありました。

カノジョを作成する。

さて、本題です。カノジョを作ってみます。

Neural Conversational Model

A Neural Conversational Model」を使って実装します。

概要

Neural Conversational Modelは会話モデルです。
基本的な構造はseq2seqと類似しており、
入力は最初の文章、期待結果は返す文章(応答文)です。

構成解説

f:id:tereka:20161225001624j:plain

構成を図にしました。入力は次のとおりになります。

  • 学習時:最初に与えた文章(質問文or話しかける文章)+ 応答文
  • 予測時:最初に与えた文章

最初の文章と返す文章を入力に与えて学習します。

次は上記構成の動作についてです。
まずは、ニューラルネットワークの学習です。
与えたIDにもとづきEmbeded Layerからベクトルを取得します。
このベクトルをLSTMに与えます。また、次の単語とそれまでのLSTMの値を入力とし、LSTMを更新します。
これをが出現するまで繰り返します。

また、最後のLSTMの値を使って計算したベクトルを最後に
Linearで予測単語を計算します。予測単語との誤差を計算し、誤差を最小にする動作を行います。

入力のLSTM(Long Short Term Memory)と出力のLSTMは別のLSTMです。

# coding:utf-8
from __future__ import absolute_import
from __future__ import unicode_literals
import chainer.links as L
import chainer.functions as F
import chainer
import numpy as np
from chainer import reporter

class Seq2Seq(chainer.Chain):
    def __init__(self, input_words):
        super(Seq2Seq, self).__init__(
            word_vec=L.EmbedID(input_words, 300),
            input_vec=L.LSTM(300, 300),
            output_vec=L.LSTM(300, 300),
            output_word=L.Linear(300, input_words)
        )
        self.train = True

    def encode(self, sentence):
        c = None
        for word in sentence:
            x = np.array([word], dtype=np.int32)
            h = F.tanh(self.word_vec(x))
            c = self.input_vec(h)
        return c

    def decode(self, vector=None, targer_sentence=None, dictionary=None):
        loss = 0
        if self.train:
            for index, target_word in enumerate(targer_sentence):
                if index == 0:
                    j = F.tanh(self.output_vec(vector))
                    pred_word = self.output_word(j)
                else:
                    j = F.tanh(self.output_vec(j))
                    pred_word = self.output_word(j)
                loss += F.softmax_cross_entropy(pred_word, np.array([target_word], dtype=np.int32))
            return loss
        else:
            gen_sentence = []
            cnt = 0
            while True:
                if cnt == 0:
                    j = F.tanh(self.output_vec(vector))
                    pred_word = self.output_word(j)
                else:
                    j = F.tanh(self.output_vec(j))
                    pred_word = self.output_word(j)
                id = np.argmax(pred_word.data)
                cnt += 1
                word = dictionary[id]
                if word == "<eos>":
                    return gen_sentence

                gen_sentence.append(word)
                if cnt == 100:
                    break
            return gen_sentence

    def generate_sentence(self, sentence, dictionary):
        self.initialize()
        encode_vector = self.encode(sentence=sentence)
        return self.decode(vector=encode_vector, dictionary=dictionary)

    def initialize(self):
        self.input_vec.reset_state()
        self.output_vec.reset_state()

    def __call__(self, sentence, target_sentence):
        self.initialize()
        encode_vector = self.encode(sentence=sentence)
        self.loss = None
        self.loss = self.decode(vector=encode_vector, targer_sentence=target_sentence)
        reporter.report({'loss': self.loss}, self)

        return self.loss
実装してみた

seq2seq2をChainerとRNN + LSTMで実装します。
今回はTrainerを使った実装にします。

データの構築

Step1 探す

どうやって構築するんだ。
家に「落第騎士の英雄譚」があったので参考にしました。
「黒鉄一輝」と「ステラ・ヴァーミリオン」の会話あたりを
中心に作成させていただいております。

落第騎士の英雄譚<キャバルリィ>【電子特装版】 (GA文庫)

落第騎士の英雄譚<キャバルリィ>【電子特装版】 (GA文庫)

Step2 存在しない言葉

存在しない言葉についてです。
形態素解析をした単語に対して、IDを振ります。

例えば次のようなことを想定しています。

単語 ID
0
1
2
3

しかし、この場合、未知語に対しての解析ができなくなります。
そのため、未知の単語が出てくれば「■」とします。(主に予測時ですね)

Step3 名前

人は名前を呼ばれると嬉しいものです。
ただ、名前は皆さん別々のをお持ちのため、名前と呼ばれる概念で学習します。
今回、呼ばれたい側の名前は全て「◯」へ置き換えておきます。

これはSlackbotへのリプライ段階で「◯」を名前に置換します。

学習する

さて、ここまでで準備ができました。
早速、学習を行います。今回はTrainerを使った学習を用いているため、一部の
更新方法を自分で実装しています。

テキストファイルにタブ区切りのドキュメントを用意し、それを対にして学習させています。

# coding:utf-8
import MeCab
from model import Seq2Seq
from chainer import training
import chainer
from chainer.training import extensions
import numpy as np
import sys
import codecs
import json

sys.stdout = codecs.getwriter('utf_8')(sys.stdout)
tagger = MeCab.Tagger('mecabrc')


def parse_sentence(sentence):
    parsed = []
    for chunk in tagger.parse(sentence).splitlines()[:-1]:
        (surface, feature) = chunk.split('\t')
        parsed.append(surface.decode("utf-8"))
    return parsed


def parse_file(filename):
    questions = []
    answers = []
    with open(filename, "r") as f:
        lines = f.readlines()

        for line in lines:
            sentences = line.split("\t")
            question = ["<start>"] + parse_sentence(sentences[0]) + ["<eos>"]
            answer = parse_sentence(sentences[1]) + ["<eos>"]
            questions.append(question)
            answers.append(answer)
    word2id = {"■": 0}
    id2word = {0: "■"}
    id = 1

    sentences = questions + answers
    for sentence in sentences:
        for word in sentence:
            if word not in word2id:
                word2id[word] = id
                id2word[id] = word
                id += 1

    return questions, answers, word2id, id2word


def sentence_to_word_id(split_sentences, word2id):
    id_sentences = []
    for sentence in split_sentences:
        ids = []
        for word in sentence:
            id = word2id[word]
            ids.append(id)
        id_sentences.append(ids)
    return id_sentences


class ParallelSequentialIterator(chainer.dataset.Iterator):
    def __init__(self, dataset, batch_size, repeat=True):
        self.dataset = dataset
        self.batch_size = batch_size  # batch size
        self.epoch = 0
        self.is_new_epoch = False
        self.repeat = repeat
        self.iteration = 0

    def __next__(self):
        length = len(self.dataset[0])
        if not self.repeat and self.iteration * self.batch_size >= length:
            raise StopIteration

        batch_start_index = self.iteration * self.batch_size % length
        batch_end_index = min(batch_start_index + self.batch_size, length)

        questions = [self.dataset[0][batch_index] for batch_index in range(batch_start_index, batch_end_index)]
        answers = [self.dataset[1][batch_index]for batch_index in range(batch_start_index, batch_end_index)]

        self.iteration += 1

        epoch = self.iteration * self.batch_size // length
        self.is_new_epoch = self.epoch < epoch
        if self.is_new_epoch:
            self.epoch = epoch

        return list(zip(questions, answers))


class BPTTUpdater(training.StandardUpdater):
    def update_core(self):
        loss = 0
        train_iter = self.get_iterator('main')
        optimizer = self.get_optimizer('main')

        batch = train_iter.__next__()
        for question, answer in batch:
            loss += optimizer.target(np.array(question, dtype=np.int32),
                                     np.array(answer, dtype=np.int32))

        optimizer.target.cleargrads()
        loss.backward()
        loss.unchain_backward()
        optimizer.update()


if __name__ == '__main__':
    questions, answers, word2id, id2word = parse_file("./dataset.txt")
    ids_questions = sentence_to_word_id(questions, word2id=word2id)
    ids_answers = sentence_to_word_id(answers, word2id=word2id)

    model = Seq2Seq(len(word2id))
    optimizer = chainer.optimizers.Adam()
    optimizer.setup(model)

    train_iter = ParallelSequentialIterator(dataset=(ids_questions, ids_answers), batch_size=1)

    updater = BPTTUpdater(train_iter, optimizer)
    trainer = training.Trainer(updater, (100, 'epoch'))

    trainer.extend(extensions.PrintReport(
        ['epoch', 'iteration', 'main/loss']
    ), trigger=(100, 'iteration'))
    trainer.extend(extensions.LogReport())
    trainer.run()

    chainer.serializers.save_npz("model.npz", model)
    json.dump(id2word, open("dictionary_i2w.json", "w"))
    json.dump(word2id, open("dictionary_w2i.json", "w"))

Slack Botで実用化する

これまで作成したカノジョを実用化します。
SlackでBotを作れるので、そこで展開を試みました。

前準備

まずは、Slack上にBotを作成します。
そしてアイコンを用意します。

実際のBOTの設定は画像のようになりました。(殆ど設定していませんが、、)
f:id:tereka:20161224161328p:plain

Botをコントロールするコード

ソースコードを記載する前にPythonのSlackbotをコントロールするライブラリが必要です。
Pythonのライブラリは次のようにインストールできます。

pip install slackbot

まずは、slackbot_settings.pyにAPI Keyを記述します。

#coding:utf-8
API_TOKEN = "YOUR API KEY"

run.pyに動作させたいコードを記述します。
動作させたいコードに対してデコレータを使います。
そのコードに対して、@default_replyを付与します。
reply_messageにこのデコレータを付与すると指定のない場合はこのメソッドが使われる設定になります。

sentence.replace("◯", "tereka")を使って、名前を途中で置換しています。
これで自分の名前が呼ばれます!やったね。

# coding:utf-8
import MeCab
import codecs
from slackbot.bot import default_reply
from slackbot.bot import Bot
from model import Seq2Seq
import chainer
import json
import sys

sys.stdout = codecs.getwriter('utf_8')(sys.stdout)
tagger = MeCab.Tagger('')

id2word = json.load(open("dictionary_i2w.json", "r"))

id2word = {int(key): value for key, value in id2word.items()}
word2id = json.load(open("dictionary_w2i.json", "r"))
model = Seq2Seq(len(word2id))
chainer.serializers.load_npz("model.npz", model)
model.train = False


@default_reply
def replay_message(message):
    parsed_sentence = []
    try:
        for chunk in tagger.parse(message.body["text"].encode("utf-8")).splitlines()[:-1]:
            (surface, feature) = chunk.decode("utf-8").split('\t')
            parsed_sentence.append(surface)
        parsed_sentence = ["<start>"] + parsed_sentence + ["<eos>"]

        ids = []
        for word in parsed_sentence:
            if word in word2id:
                id = word2id[word]
                ids.append(id)
            else:
                ids.append(0)
        ids_question = ids
        sentence = "".join(model.generate_sentence(ids_question, dictionary=id2word)).encode("utf-8")

        sentence = sentence.replace("◯", "tereka")
        message.reply(sentence)
    except Exception as e:
        print (e)
        message.reply("解析できなかったのでもう一度おねがいします。")


def main():
    bot = Bot()
    bot.run()


if __name__ == "__main__":
    main()

実際にどうなったのか

実際にチャットしてみました。
なんかちょっとだけ女の子とチャットしている気分になりました。

f:id:tereka:20161224214443p:plain

チャットしていてわかったのですが、リプライが短文なのはある程度、学習できている感覚があります。
特有の単語を学習してそうな気がしますが、やはりデータセットが少なすぎたか。

最後に

カノジョいなくて寂しい。
DeepLearningを使ったBotの作成に参考にしてみてください。

Chainerの抽象化に挑戦してみた

皆さんこんにちは
お元気ですか?年末ってこんなに忙しくなるものですね。

本記事はChainer Advent Calendar 16日目の記事です。

qiita.com

本日はScikit-learn likeなChainerを作った記事です。なぜ、作ったのかは後述します。

はじめに

なぜ、抽象化コードを作ってみたのか

Chainerの実装を高速にし、検証の立ち上がりを早くしたいからです。

Chainerは非常にFlexibleなネットワークを作れるので、個人的に好きです。
特にRecurrent Neural Network系のネットワークを書く時に重宝しています。
また、実装はTrainerを使うとTrainer実装前よりも簡単な実装になりました。

しかし、手軽さと最初の立ち上がりの早さが、Scikit-learnと比べるとまだ劣ると感じています。

せっかくなので、アドベントカレンダーを機に抽象化をある程度頑張ってみようかなと思った次第です。

Scikit-learnのWrapperを実装するメリット

Wrapper実装メリットは3つあると考えています。

  1. 「うまくいかないこと」を早く知る
  2. 使いやすい
  3. 他のアルゴリズムと取り替えやすい
「うまくいかないこと」を早く知る

私が考える最も大きな理由は「うまくいかないこと」を早く知れることです。

正直、実装初回からうまくいくとは思っていません。
そのため、早めになぜ、うまくいかないのか、
更には「うまくいかない」ことがわかりたいです。

これを早く知れると次はどうしようかといった手を打ちやすいです。

使いやすい

scikit-learnのインターフェースはシンプルで使いやすいです。
アルゴリズムを使う際にfit, predict, predict_probaあたりが何をしているのかがわかれば十分です。
プログラマはそのメソッドを呼ぶだけで、内部動作を知らなくても期待する結果を得られます。

他のアルゴリズムと取り替えやすい

機械学習アルゴリズムの入れ替えが容易です。
インターフェースを共通化した場合、基本的に初期化が変わるのみで
コードを殆ど変更する必要がありません。
そのため、入れ替えも非常に簡単で、検証する速度が効率化します。

何ができれば嬉しいか

必要な作業の洗い出しをまずやります。
Chainerの機能をフルに活用するのはRecurrent Neural Network込みにすると難しいと思っています。
そのため、一部の機能に絞れば、きっと実装できるはず・・・
ということで必要最低限の機能を真面目に考えてみました。

最低限必要な項目

  1. モデル定義・・・ニューラルネットワーク定義を入力する。
  2. ハイパーパラメータ・・・学習用パラメータ
  3. GPU or CPU・・・ハードのモードの切り替え
  4. 学習機能・・・モデル学習機能(fit)
  5. 出力・・・確率分布での出力(predict_proba)、ラベル出力(predict)
  6. save load機能(weight)・・・学習済みモデルの読み出し、書き出し

実際に作ってみた

こんな感じになりました。
可能な限り、既存のChainerの機能を活かすようにしています。

ソースコード(chainer_neural_network.py)

# coding:utf-8
from __future__ import absolute_import
from __future__ import unicode_literals
from sklearn.base import BaseEstimator
import chainer
import numpy as np
from chainer.training import extensions
import cupy


class ChainerNeuralNetwork(BaseEstimator):
    """
    Chainer Neural Network
    Scikit learn Wrapper
    """

    def __init__(self, model, optimizer, extensions=[], gpu=-1, batch_size=16, epochs=10, result="result"):
        self.model = model
        self.optimizer = optimizer
        self.extensions = extensions
        self.gpu = gpu
        self.batch_size = batch_size
        self.epochs = epochs
        self.result = result

        self.xp = np
        if gpu >= 0:
            chainer.cuda.get_device(gpu).use()
            self.model.to_gpu()
            self.xp = cupy
        self.optimizer.setup(model)
        super(ChainerNeuralNetwork, self).__init__()

    def convert_list_to_tuple(self, X, y):
        """
        convert list to tuple

        :param X: X
        :param y: y
        :return: tuple dataset
        """
        return chainer.datasets.TupleDataset(X, y)

    def fit(self, X, y=None, valid_X=None, valid_y=None):
        """
        fit model

        :param X: tuple dataset or matrix X
        :param y: expected vector
        :param valid_X: validation X
        :param valid_y: validation y
        :return:
        """
        if isinstance(X, chainer.datasets.TupleDataset):
            train_dataset = X
        else:
            train_dataset = self.convert_list_to_tuple(X, y)
        train_iter = chainer.iterators.SerialIterator(train_dataset, self.batch_size)

        updater = chainer.training.StandardUpdater(train_iter, self.optimizer, device=self.gpu)
        trainer = chainer.training.Trainer(updater, (self.epochs, 'epoch'), out=self.result)

        if valid_X is not None:
            if isinstance(valid_X, chainer.datasets.TupleDataset):
                valid_dataset = valid_X
            else:
                valid_dataset = self.convert_list_to_tuple(valid_X, valid_y)
            test_iter = chainer.iterators.SerialIterator(valid_dataset, self.batch_size,
                                                         repeat=False, shuffle=False)
            self.extensions.append(extensions.Evaluator(test_iter, self.model, device=self.gpu))

        for extend in self.extensions:
            trainer.extend(extend)
        trainer.run()

    def _iter_predict(self, X):
        """
        iteration for prediction

        :param X: np.ndarray
        :return: prediction of batch
        """
        for index in range(0, len(X), self.batch_size):
            batch_x = chainer.Variable(self.xp.array(X[index: index + self.batch_size]).astype(self.xp.float32),
                                       volatile=False)
            if index == 0:
                predicts = np.array(chainer.cuda.to_cpu(self.model.predict(batch_x).data))
            else:
                predicts = np.vstack([predicts, chainer.cuda.to_cpu(self.model.predict(batch_x).data)])
        return np.array(predicts)

    def predict(self, X):
        return np.argmax(self.predict_proba(X), axis=1)

    def predict_proba(self, X):
        return self._iter_predict(X)

    def load_weights(self, filepath):
        chainer.serializers.load_npz(filepath, self.model)

    def save_weights(self, filepath):
        chainer.serializers.save_npz(filepath, self.model)
  1. 基本的にはfit, predict, predict_probaの基本機能を実装しました。
  2. 学習して保存できるようload_weights, save_weights機能を作ってみました。
  3. X, yを別々にした場合でも、Dataset(Tuple)でも使えるようにしています。

MNISTで遊ぶ

Wrapperを使ってチュートリアルのMNISTコードを抽象化します。

簡単なニューラルネットワークであればこの程度抽象化すれば実施可能です。
ただし、構築モデル(Chain)はモデルを使うためにpredict関数の実装をしなければなりません。
そのため、なければ実装してください。

本プログラムは、chainer.links.Classifierを継承し、ClassifierWrapperを作りました。
このクラスにpredict関数を実装しています。

ソースコード

# coding:utf-8
from __future__ import absolute_import
from __future__ import unicode_literals
import argparse
import chainer
import chainer.functions as F
import chainer.links as L
from chainer import training
from chainer.training import extensions
from chainer_neural_network import ChainerNeuralNetwork
import chainer.serializers
import numpy as np


class MLP(chainer.Chain):
    def __init__(self, n_units, n_out):
        super(MLP, self).__init__(
            l1=L.Linear(None, n_units),  
            l2=L.Linear(None, n_units),  
            l3=L.Linear(None, n_out),
        )

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)


class ClassifierWrapper(L.Classifier):
    def predict(self, x):
        return F.softmax(self.predictor(x))


def main():
    parser = argparse.ArgumentParser(description='Chainer example: MNIST')
    parser.add_argument('--batchsize', '-b', type=int, default=100,
                        help='Number of images in each mini-batch')
    parser.add_argument('--epoch', '-e', type=int, default=20,
                        help='Number of sweeps over the dataset to train')
    parser.add_argument('--gpu', '-g', type=int, default=-1,
                        help='GPU ID (negative value indicates CPU)')
    parser.add_argument('--out', '-o', default='result',
                        help='Directory to output the result')
    parser.add_argument('--unit', '-u', type=int, default=1000,
                        help='Number of units')
    args = parser.parse_args()

    print('GPU: {}'.format(args.gpu))
    print('# unit: {}'.format(args.unit))
    print('# Minibatch-size: {}'.format(args.batchsize))
    print('# epoch: {}'.format(args.epoch))
    print('')

    model = ClassifierWrapper(MLP(args.unit, 10))
    optimizer = chainer.optimizers.Adam()

    extensions_list = [
        extensions.PrintReport(
            ['epoch', 'main/loss', 'validation/main/loss',
             'main/accuracy', 'validation/main/accuracy']),
        extensions.LogReport(),
        extensions.dump_graph('main/loss'),
        extensions.ProgressBar()
    ]

    train, test = chainer.datasets.get_mnist()
    X, y = [pair[0] for pair in train], [pair[1] for pair in train]

    test_X, tedt_y = [pair[0] for pair in test], [pair[1] for pair in test]
    test_X, tedt_y = np.array(test_X), np.array(tedt_y)

    neural_network = ChainerNeuralNetwork(model=model, optimizer=optimizer, extensions=extensions_list, gpu=args.gpu,
                                          batch_size=args.batchsize, epochs=args.epoch, result=args.out)
    neural_network.fit(X=X, y=y, valid_X=test)
    print(neural_network.predict_proba(test_X).shape)
    print(neural_network.predict(test_X).shape)

    neural_network.save_weights("model.npz")
    neural_network.load_weights("model.npz")

if __name__ == '__main__':
    main()

このモデルにverboseと呼ばれるオプションをつけると抽象化できるかも・・。
例えば、LogReport周りはこのオプションの追加で
extensions_listで宣言しなくてもログ出力ができると思います。

ただ、今回はそこまでは作っていません。

最後に

不足している機能はあると思いますが、これでもだいぶ遊べます。
さくっと遊ぶにはこの機能で多分、十分です。

誰かもっと高機能にしてください。そしてメンテしてください。