Pandasで特徴量取得する場合に使う操作をまとめてみた
皆さんこんにちは
お元気ですか。私は元気です。
分析は基本的にPythonを使って行います。(大体Pandasですが・・・)
Pandasを利用すると色々できます。が、ふとどうするんだっけ処理が増えていきました。
自分のメモがてらカテゴリを分けて記録に残したいと思います。
最後のほうは特徴量の作り方集になっています。
Kaggleで実際に使ったことがある処理も数多く掲載しました。
思いついたら随時、追加しようと思います。
- 準備
- ファイル操作
- テーブル操作
- カラム、テーブルの統計情報を取得する。
- 日付操作
- 2つ以上のDataFrameの結合操作
- 集計操作を使った特徴量作成
- 変数の変換を行う。
- Numpyへの変換を行う。
- 最後に
準備
本コードは事前に「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つあります。
基本的には、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は雛形となるテンプレートプロジェクトから
プロジェクトを生成するツールです。
雛形となるプロジェクトを作ってしまえば、少し変更したい箇所を指定して
新しいプロジェクトを作ります。
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と類似しており、
入力は最初の文章、期待結果は返す文章(応答文)です。
構成解説
構成を図にしました。入力は次のとおりになります。
- 学習時:最初に与えた文章(質問文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文庫)
- 作者: 海空りく
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2013/10/17
- メディア: Kindle版
- この商品を含むブログを見る
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を作れるので、そこで展開を試みました。
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()
実際にどうなったのか
実際にチャットしてみました。
なんかちょっとだけ女の子とチャットしている気分になりました。
チャットしていてわかったのですが、リプライが短文なのはある程度、学習できている感覚があります。
特有の単語を学習してそうな気がしますが、やはりデータセットが少なすぎたか。
最後に
カノジョいなくて寂しい。
DeepLearningを使ったBotの作成に参考にしてみてください。
Chainerの抽象化に挑戦してみた
皆さんこんにちは
お元気ですか?年末ってこんなに忙しくなるものですね。
本記事はChainer Advent Calendar 16日目の記事です。
本日はScikit-learn likeなChainerを作った記事です。なぜ、作ったのかは後述します。
はじめに
なぜ、抽象化コードを作ってみたのか
Chainerの実装を高速にし、検証の立ち上がりを早くしたいからです。
Chainerは非常にFlexibleなネットワークを作れるので、個人的に好きです。
特にRecurrent Neural Network系のネットワークを書く時に重宝しています。
また、実装はTrainerを使うとTrainer実装前よりも簡単な実装になりました。
しかし、手軽さと最初の立ち上がりの早さが、Scikit-learnと比べるとまだ劣ると感じています。
せっかくなので、アドベントカレンダーを機に抽象化をある程度頑張ってみようかなと思った次第です。
Scikit-learnのWrapperを実装するメリット
Wrapper実装メリットは3つあると考えています。
- 「うまくいかないこと」を早く知る
- 使いやすい
- 他のアルゴリズムと取り替えやすい
「うまくいかないこと」を早く知る
私が考える最も大きな理由は「うまくいかないこと」を早く知れることです。
正直、実装初回からうまくいくとは思っていません。
そのため、早めになぜ、うまくいかないのか、
更には「うまくいかない」ことがわかりたいです。
これを早く知れると次はどうしようかといった手を打ちやすいです。
何ができれば嬉しいか
必要な作業の洗い出しをまずやります。
Chainerの機能をフルに活用するのはRecurrent Neural Network込みにすると難しいと思っています。
そのため、一部の機能に絞れば、きっと実装できるはず・・・
ということで必要最低限の機能を真面目に考えてみました。
最低限必要な項目
- モデル定義・・・ニューラルネットワーク定義を入力する。
- ハイパーパラメータ・・・学習用パラメータ
- GPU or CPU・・・ハードのモードの切り替え
- 学習機能・・・モデル学習機能(fit)
- 出力・・・確率分布での出力(predict_proba)、ラベル出力(predict)
- 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)
- 基本的にはfit, predict, predict_probaの基本機能を実装しました。
- 学習して保存できるようload_weights, save_weights機能を作ってみました。
- 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で宣言しなくてもログ出力ができると思います。
ただ、今回はそこまでは作っていません。
最後に
不足している機能はあると思いますが、これでもだいぶ遊べます。
さくっと遊ぶにはこの機能で多分、十分です。
誰かもっと高機能にしてください。そしてメンテしてください。