Numpy array のcopy で気をつけたいこと

TL; DR

  • Numpy array のcopy は気をつけたい
    • 特にNumpy array のブロードキャストの性質
  • Numpy array をcopy したい場合も、純粋なPython のList オブジェクトも、適切なcopy 関数をつかおう
    • でもスライス操作をするとその限りではなさそう?

Numpy array で気をつけたいことがあった

近頃、深層学習で当たり前のようにNumpy を触ることが増えてきていると思いますが、
Numpy の性質として気をつけたいところがありました。

なので、忘備録の意を込めて記事にしようかと思います。

Numpy 操作

以下のようにデータ操作を行いました。

>>> import numpy as np

>>> a = np.arange(0, 10)
>>> a  # array([0, 1, ..., 9])
>>> sub_a = a[:3]  # 最初の3要素をsub_a に代入。意図としてはa の中身をコピー
>>> sub_a  # array([0, 1, 2])

a の3要素を sub_a オブジェクトに代入しただけですね。
そして、この sub_a に値を変更しました。

>>> sub_a[1] = 10
>>> sub_a
array([0, 10, 2])

さて、このとき a の値はどうなっているでしょうか?
結果は、

>>> a  
array([0, 10, 2, ..., 9])

となります。

どうしてこうなるのか?

Numpy はメモリを効率的に扱うため、
見た目は異なるオブジェクトのように扱っていても、
変数の値は同じメモリを見るようになっているみたいです。

つまり、
a[0] の指しているメモリの番地
sub_a[0] が指しているメモリの番地が同じになっており、
片方を変更するともう一方も変更されるということになります。

特に気をつけたいブロードキャストの場合

Numpy では、以下のようにして、すべての要素にブロードキャストで代入することができます。

>>> sub_a[:] = 20

これはこれで大変便利ですよね。

このとき asub_a はどうなるでしょうか?

>>> sub_a
array([20, 20, 20])
>>> a
array([20, 20, ..., 9])

となってしまいます。

じゃあどうするのか?

このように別オブジェクトとしてコピーしたい場合は、
以下のように copy() メソッドを利用するのがよいです。

>>> a = np.arange(0, 10)
>>> sub_a = a[:3].copy()

>>> a
array([0 1 2 3 4 5 6 7 8 9])
>>> sub_a
array([0 1 2])

>>> sub_a[:] = 30
>>> sub_a
array([30 30 30])  # すべての要素が30になる。 
>>> a
array([0 1 2 3 4 5 6 7 8 9])  # 一方で、値が変わっていない。 

通常のPython のlist 型は大丈夫なの?

実はPython のList オブジェクトも似たような問題が知られています。
例えば、以下のような形式の場合。

>>> b = list(range(10)) # [0
>>> b  # [0, 1, 2, ..., 9]
>>> sub_b = b  # b をそのまま sub_b に代入
>>> b  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> sub_b  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> sub_b[0] = 10  # sub_b の要素を変更
>>> sub_b  # [10, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> b  # b の要素も変更されてしまっている。[10, 1, 2,..., 9]

上記は、ハマりポイントとして有名ですよね。
このように配列を別ものとしてコピーして操作したい場合は、
copy ライブラリを使って、オブジェクトをコピーする必要があります。

一次元の配列なら、 copy.copy()
多次元の配列なら、 copy.deepcopy() です。

ただし、一部であっても全体であっても、スライスでコピーした場合はその限りではないみたいです。

>>> b = list(range(10))
>>> sub_b = b[:3]
>>> sub_b[0] = 10
>>> b
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> b = list(range(10))
>>> sub_b = b[:]
>>> sub_b[0] = 10
>>> sub_b
[10, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> b
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

是非参考にしてください。

GKE とCloudSQL を組み合わせたい

k8s の中にRDB コンテナをデプロイして管理するもよいと思いますが、
RDB だけフルマネージドサービスを使うパターンもあるかと思います。

アプリケーションコンテナはGKE で、RDB はCloudSQL で動かすにはどうしようと考えていたところ、Google Cloud のドキュメントの中に Running Django on Google Kubernetes Engine というまさにやりたいことを実践しているドキュメントがあったので、やってみました。

ドキュメントは↓を参照してください。

Running Django on Google Kubernetes Engine  |  Python  |  Google Cloud

Google Cloud のドキュメントは結構和訳されているものがあるのですが、
今回の記事は英語だったのと、途中で少し躓くところもあったので、やったことを忘備録がてらメモを残そうかと思います。

誰かの参考になれば幸いです。

事前ノート

基本的にはドキュメントに記載されているコマンドスクリプトをそのままやるという感じなので、
部分的にしか書いていません。

また、今回はPostgreSQL instanse を使っているので、 psql コマンドで接続します。なので、事前に psql を使えるようにしておく必要があります。

MySQL の場合は検証してないですが、 基本的な操作は変わらないかと思うので、適宜MySQL に置換していただければと思います。

途中で設定しているパスワードなどは参考なので、各自適当な値を設定するようにしてください。

あしからず。

やったこと

デプロイするサービスはすでに用意されているものをpull してきて、それをビルドしました。

git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
cd python-docs-samples/kubernetes_engine/django-tutorial

次にCloud SQL Proxy を使って、RDB インスタンスの生成と接続などなどやりました。インスタンス名は、一通り実行した感じだと、ドキュメント通りである必要はなく、適当で大丈夫だと思います。

gcloud services enable sqladmin

# Mac 用のCloud SQL Proxy をダウンロード
curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.amd64

chmod +x cloud_sql_proxy

# ターミナルからinstance を生成
# 今回はpoll-instance という名前で作成
gcloud sql instances create poll-instance --database-version=POSTGRES_11 --tier db-f1-micro --region=asia-northeast1

ドキュメントには、 gcloud sql instances describe [YOUR_INSTANCE_NAME]というコマンドを使い、接続名を取得するという一文があります。

これは、 gcloud sql instances describe の結果に対して、grep grep connectionName とすることで適切な部分が取得できます。取得した値を [YOUR_INSTANCE_CONNECTION_NAME] として表現されています。

# connection name を調べる
gcloud sql instances describe poll-instance | grep connectionName

このconnection name とダウンロードしたcloud_sql_proxy を使ってlocal からinstance への接続を可能にします。

./cloud_sql_proxy -instances="[YOUR_INSTANCE_CONNECTION_NAME]"=tcp:5432

おそらく、上記のコマンドを実行しても終了などしないと思います。 これは、当たり前といえば当たり前なのですが、ローカルPC とCloud SQL instance との間で接続が確立されているので、切れなくて問題ないです。

なので、バックグラウンドで実行しておくとかも方法はありますが、僕は、次のコマンドからは新しいターミナルのパネルを開いて実行しました。

ここからは、ローカルからCloud SQL instanse に対して操作を行います。

psql --host 127.0.0.1 --user postgres --password

つまづきポイント①
ここでのパスワードってなんだ?ってなりました。
なぜなら、ドキュメントには Use the Postgres client or similar program to connect to your instance. When prompted, use the root password you configured.とありますが、
作成したインスタンスに対して特にパスワードを設定した覚えがないからです。
ここでのパスワードは、console 画面のCloud SQL 画面で確認して入力すれば接続できました。

接続したら、DB の操作を行います。 ここでは、サービスを実行するのに必要な DATABASE の作成や DATABASETABLE に対する権限の設定を行います。

ここの操作はドキュメントにある通り実行するがよいと思います。 ただし、ユーザとそのパスワードは適宜設定できるので、任意の値を設定してください。 今回は、 [POSTGRES_USER] には、 poll_user[POSTGRES_PASSWORD] には、 poll_pw を設定しました。

CREATE DATABASE polls;
CREATE USER poll_user WITH PASSWORD 'poll_pw';
GRANT ALL PRIVILEGES ON DATABASE polls TO poll_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO poll_user;

次にサービスアカウントを作成します。
このサービスアカウントは、k8s クラスタからCloud SQL インスタンスに接続したり、クエリを投げるためのサービスアカウントになります。 設定する権限はドキュメント通り、以下の3つで十分でした。

  • Cloud SQL クライアント
  • Cloud SQL 編集者
  • Cloud SQL アドミン(管理者)

鍵を生成する必要があるので json フォーマットの鍵を作成してダウンロードしておきます。

ここからはローカルで一度動作確認し、その次にGKE インスタンスにpod を展開していきます。
このあたりは特にハマったポイントはなかったのと、個人的には k8s ⇔ Cloud SQL 間の通信ができているのか?を確認するほうが重要だったので、一部割愛しています。

export DATABASE_USER=poll_user
export DATABASE_PASSWORD=poll_pw

# GKE クラスタを作成
gcloud container clusters create polls \\n  --scopes "https://www.googleapis.com/auth/userinfo.email","cloud-platform" \\n  --num-nodes 4 --zone "us-central1-a"

# 作成したクラスタのクレデンシャルをkubectl にセット
gcloud container clusters get-credentials polls --zone "us-central1-a"

# クレデンシャルをkubectl コマンドに設定
kubectl create secret generic cloudsql-oauth-credentials --from-file=credentials.json=/path/to/service-account.json

# PostgreSQL のuser password を設定する
kubectl create secret generic cloudsql --from-literal=username=poll_user --from-literal=password=poll_pw

docker pull b.gcr.io/cloudsql-docker/gce-proxy

# ローカル環境でコンテナをbuild
docker build -t gcr.io/hatch-sydney/polls .

# gke にpush するためにauth の設定をして、push
gcloud auth configure-docker
docker push gcr.io/hatch-sydney/polls

# k8s クラスタに向けてデプロイ
kubectl create -f polls.yaml

ここまでの操作でpod をデプロイできました。 ハマリポイントはないと書きつつ、ハマリポイントが一つありました。

つまづきポイント②
kubectl コマンドでPostgreSQL の秘密情報を登録するコマンド

kubectl create secret generic cloudsql --from-literal=username=poll_user --from-literal=password=poll_pw

でどのユーザを設定するか悩みました。
なぜなら、ドキュメントには [PROXY_USERNAME] [PASSWORD] と書かれており、これどこで設定した?という気持ちになったためです。
実行したコマンドの通り、ローカルからCloud SQL Proxy を通じてDB 操作を行ったときのuser とpassword を設定するのが正しいかと思います。
僕はこれで問題なく接続できました。

残りは、pod のログを取ったり、実際にexternal ip で接続してみたりするだけです。

# deploy したpod のstatus やid を調べる
kubectl get pods

# polls pod のログを見る。 以下では見られない。
kubectl logs polls-fd676657d-9qfnw

# application のログを見たい場合は -c polls-app を指定する
kubectl logs polls-fd676657d-9qfnw -c polls-app
# Cloud SQL のログを見たい場合は -c cloudsql-proxy を指定する
kubectl logs polls-fd676657d-9qfnw -c cloudsql-proxy

# EXTERNAL-IP を調べて接続してみる
kubectl get services polls

最後のコマンドで得られたexternal ip に接続するとページが見られるかと思います。
そして、最後のハマリポイントです。

つまづきポイント③
kubectl logs <pod-id> だけではログは見られませんでした。この理由としては今回のpod には複数のコンテナが含まれており、その場合、どのコンテナを見るか?を指定する必要があります。
このことをすっかり忘れていたため、地味にハマりました。

コマンドのチートシートがあるので、忘れないために載せておきます。

kubernetes.io

まとめ

今回の気づきをまとめるとしたら、以下の3点でしょうか。

  • GKE とCloud SQLを組み合わせたdjango アプリをデプロイしたよ
  • いくつかハマリポイントあったよ
  • GKE とCloud SQL を接続する上では、Cloud SQL の権限を設定したサービスアカウントをkubectl に設定することでpod から接続できるみたいだよ

まだまだ勉強することがたくさんありそうです。

k8s のpod の中のファイルをlocal にコピーしてくる方法

TL;DR;

k8s のpod 内にあるファイルは kubectl cp コマンドでローカルにコピーできるよ

何がしたかったか

サーバーにあるファイルをアップロードやダウンロードする場合、よくscp で行うかと思います。 本業の開発では、kubernetes (以下、k8s) を用いて運用しており、 pod に入ってコマンドを叩き、その結果をpod 内のファイルに標準出力した。 このファイルをローカルに引っ張ってくるときに、どうすればいいんだ?と思い、調べました。

kubectl cp

k8sクラスタを操作するにあたり、 kubectl があります。 kubectlk8s クラスタやデプロイされたpod を操作するための印象が強いかと思います。 実際にkubectl のリファレンスを見ても、pod へのアクセスがメインで記述されていることがわかります。

kubernetes.io

しかし、実は kubectl には cp コマンドがあり、コピーするコマンドが用意されています。

kubernetes.io

cp
Copy files and directories to and from containers.

と書かれています。 containers と書いてありますが、pod からファイルをコピーするのに役立ちそうです。

# namespace に展開されているpod 一覧から対象を絞ります
$ kubectl get pods -n <namespace> | grep <pod name>

# cp コマンドでファイルをローカルにコピーします。
$ kubectl cp -n <namespace> <pod name>:/path/to/file /path/to/local/file

でコピーすることができました。

SudachiPy でユーザ辞書を作ってみた

前回のブログでGiNZA2.0 の導入について記述した。

toohsk.hateblo.jp

形態素解析を行ったところ期待した粒度で分割されていないことがあった。
これは形態素解析を行う上で仕方ない部分であり、
こういう場合には分割されてほしくない単語をユーザ辞書で管理する必要がある。

GiNZA はトークン化するためにSudachiPy を利用している。 1
なので、ユーザ辞書はSudachiPy にそう形で用意する必要がある。

前提

前回と同様、pyenv でPython 3.7.3 をインストールした環境でのコードを書いていきます。

注意

本来、GiNZA をインストールすると合わせてSudachiPy がインストールされるが、今回は手動でインストールする。

SudachiPy のインストール

SudachiPy とSudachiDict_coreをインストールします。

$ pip install SudachiPy
$ pip install https://object-storage.tyo2.conoha.io/v1/nc_2520839e1f9641b08211a5c85243124a/sudachi/SudachiDict_core-20190531.tar.gz

ユーザ辞書の元データ

前回、 金閣寺金閣 の2つに分割されてしまったので、 金閣寺 と出力されるように 金閣寺 をユーザ辞書に登録します。

金閣寺,4786,4786,5001,金閣寺,名詞,固有名詞,一般,*,*,*,キンカクジ,きんかくじ,*,*,*,*,*

4カラム目の 5001 はコストに相当するものですが、今回は適当に指定しています。

バイナリ化

SudachiPy でユーザ辞書を扱うためには、バイナリに変換した辞書を指定する必要があります。
以下のコマンドでバイナリ化できます。

$ sudachipy ubuild -s /path/to/system.dic user_dict.txt

ちなみに system.dic のありかですが、
今回、pyenv でSudachiPy をインストールしていることもあり、 pyenv 配下で使用している python バージョンの site-packages 以下に site-packages/sudachidict_core/resources/system.dic としてありました。
参考にしてください。

次回はこのバイナリ化した辞書を使って、GiNZA で形態素解析を行います。


  1. GiNZA のリポジトリSudachiおよびSudachiPy 参照

GiNZA 2.0 を触ってみた。

GiNZA はmegagonlabs が開発している日本語の形態素解析器です。 フレームワークとしてspaCy をFramework として利用しています。 今日2.0 がリリースされたので、インストールとユーザ辞書について触っていきたいと思います。

前提

今回、pyenv でPython 3.7.3 をインストールした環境でのコードを書いていきます。

インストール手順

インストール手順はこちらのページにあります。 2通りのインストール方法があるみたいです。
1つは、圧縮ファイルをダウンロードして、そのあとpip でインストールする方法です。
そして、もう一つはリンクをpip インストールコマンドで指定する方法です。
今回は、後者の方でインストールをしてみました。

pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"

この場合、 latest がダウンロードされるみたいです。
バージョンを指定する場合は、 latest を指定すれば良さそう。

形態素解析

形態素解析のコード例もこちらに載っています。

import spacy
nlp = spacy.load('ja_ginza')
doc = nlp('依存構造解析の実験を行っています。')
for sent in doc.sents:
    for token in sent:
        print(token.i, token.orth_, token.lemma_, token.pos_, token.tag_, token.dep_, token.head.i)
    print('EOS')

実行結果はこちらになりました。

0 依存 依存 NOUN 名詞-普通名詞-サ変可能 compound 2
1 構造 構造 NOUN 名詞-普通名詞-一般 compound 2
2 解析 解析 NOUN 名詞-普通名詞-サ変可能 nmod 4
3 の の ADP 助詞-格助詞 case 2
4 実験 実験 NOUN 名詞-普通名詞-サ変可能 obj 6
5 を を ADP 助詞-格助詞 case 4
6 行っ 行う VERB 動詞-一般 ROOT 6
7 て て SCONJ 助詞-接続助詞 mark 6
8 い 居る AUX 動詞-非自立可能 aux 6
9 ます ます AUX 助動詞 aux 6
10 。 。 PUNCT 補助記号-句点 punct 6
EOS

恐らく、 token.i が出力のインデックス、 token.orth_ が表層、 token.lemma_ が原型、 token.pos_ が英語での品詞、 token.tag_ が日本語での品詞と活用のようです。 token.dep_token.head.i はよくわかっていないので、後日調べて記事にしたいですが、 token.head.i係り受け先の id のように見えます。

ユーザ辞書

例えば 足利義満は金閣寺を建立しました。 のような文章を形態素解析をしたい場合に、以下のような結果になります。

0 足利 足利 PROPN 名詞-固有名詞-人名-姓 compound 1
1 義満 義満 PROPN 名詞-固有名詞-人名-名 nsubj 6
2 は は ADP 助詞-係助詞 case 1
3 金閣 金閣 NOUN 名詞-普通名詞-一般 compound 4
4 寺 寺 NOUN 接尾辞-名詞的-一般 obj 6
5 を を ADP 助詞-格助詞 case 4
6 建立 建立 VERB 名詞-普通名詞-サ変可能 ROOT 6
7 し 為る AUX 動詞-非自立可能 aux 6
8 まし ます AUX 助動詞 aux 6
9 た た AUX 助動詞 aux 6
10 。 。 PUNCT 補助記号-句点 punct 6
EOS

このようにデフォルトの辞書で形態素解析を行うと 金閣寺 がまとまった名詞として扱われることを期待していた場合に、異なった結果になる場合があります。
このような場合にユーザ辞書を利用します。
GiNZA は SudachiPy を利用しているようなので、ユーザ辞書は SudachiPy の書き方に従って書けばユーザ辞書が使えそうです。

ユーザ辞書を扱うまでをブログの内容にしたかったのですが、 次回にまわしたいと思います。

Pandas DataFrame のX軸の単位を調整する

内容

DataFrame で histogram を描画したときの x, y 軸のスケールの表示を調整する方法について、ハマったのでメモとして残しておきます。

TL;DR

DataFrame から直接 df.hist とするのではなく、
一度、 df.plotmatplotlib.axes._subplots.AxesSubplot を取り出し、
その matplotlib.axes._subplots.AxesSubplotticklable_formatstyle オプションを変更することで整数表示や浮動小数点表記での表示などを指定することができます。

ax = sample_df.plot(figsize=(15, 5), kind='hist', bins=100)
ax.ticklabel_format(style='plain', axis='x') # 浮動小数点表記で表示したい場合、 style='sci' とする

Pandas の version

0.24.1 を使ってます。

import pandas as pd
pd.__version__
'0.24.1'

どういう事象?

元々、データ分析の案件で発生した事象で、
少数を含む、かつ、取りうる値の範囲が広いデータを DataFrame で扱い、それを df.hist で描画しようとしました。

今回は、だいたいのデータとして、以下のデータで再現実験をします。

sample_size = 10000
sample_data = np.random.rand(sample_size)
sample_data = [d * np.random.randint(low=1, high=10000000) for d in sample_data]
sample_category = ["a_{}".format(i) for i in range(sample_size)]
d = {'col1': sample_category, 'col2': sample_data}
sample_df = pd.DataFrame(d)

以下のようなデータが作成されます。

pd.options.display.float_format = '{:.2f}'.format
sample_df.head(10)

    col1    col2
0  a_0     6809432.40
1  a_1     3095277.45
2  a_2     1515776.87
3  a_3     6291255.66
4  a_4     429976.20
5  a_5     422715.54
6  a_6     5695883.51
7  a_7     2793741.89
8  a_8     2343652.35
9  a_9     4571542.77

分布としては、以下のような感じ。

sample_df.describe()

        col2
count   10000.00
mean    2498833.72
std         2212640.08
min  128.83
25%    662876.83
50%    1854733.14
75%    3875713.08
max  9861186.18

桁のオーダを見ると 102 から 106 のデータが生成されています。
このデータを df.hist で描画すると

f:id:toohsk:20190401105232p:plain
x軸が浮動小数点表記表示になっている histogram

のような形式になりました。 X 軸のスケールに注目すると自動的に浮動小数点表記に変換されていることがわかります。

今回のようなランダムに生成されたデータの場合、このような表現でもわかりますが、
この結果を他の方にシェアする場合、実際の値としてみた方が感覚をつかみ安い方もいるかと思います。

どうやって整数表示にするか

x, y 軸の表示に関する操作はどうやら matplotlib.axes.Axes class にて操作することは調べて想定がつきました。
ただ、問題なのは、 df.hist で返される値の型は numpy.ndarray でした。

hist = sample_df.hist(bins=100, figsize=(15, 5))
type(hist)

numpy.ndarray

なので、 matplotlib.axes.Axes class を操作しようとしても操作できず、 軸のスケールを操作することができません。

なので、 histogram の描画を df.hist で行うのではなく、
df.plot で描画し、 option の kind で kind='hist' とすることで、 matplotlib.axes.Axes class を戻り値として受け取れるようにしました。

ax = sample_df.plot(figsize=(15, 5), kind='hist', bins=100)
type(ax)

matplotlib.axes._subplots.AxesSubplot

これにより、目的だった matplotlib.axes.Axes class を操作することができました。 matplotlib.axes.Axes class の ticklabel_formatstyle='plain とすることで軸のスケールを整数表示することができました。

ax = sample_df.plot(figsize=(15, 5), kind='hist', bins=100)
ax.ticklabel_format(style='plain', axis='x') # axis='y' とすると y 軸のスケールを整数にすることができる

f:id:toohsk:20190401111909p:plain
x軸が整数表示になっている histogram

反対に、明示的に浮動小数点表記としたい場合は、 ticklabel_format の style オプションを style=sci とすればよく、
y 軸のスケールを変えたい場合は、 ticklabel_format の axis オプションを axis=y とすればよいみたいです。

Pandas DataFrame で圧縮したデータファイルを読み込む

概要

圧縮されたファイルを Pandas DataFrame で読み込むための手順を紹介する。
毎回解凍してから読み込みをしていたのを、圧縮したまま読み込めたので、忘備録として残す。 今回は gzip 圧縮した前提で記載します。

内容

すごくシンプルだったので、まずコードを載せます。

import pandas as pd

gz_file_path = 'file.tsv.gz'
df = pd.read_table(gz_file_path, compression='gzip')
df.head()

gzip 圧縮したファイルのパスを gz_file_path に格納しています。 その後、 pd.read_table で読み込みをしています。
その際に compression オプションで圧縮フォーマットを指定するだけで、 読み込んでくれます。
自分の手元では、 header に column 名が載っているデータを読み込ませたところ、 df.head() で DataFrame の先頭の5件を表示させたときに header 行の内容が表示されていました。
もし、 header 行が無いデータの場合は、 names オプションにカラム名を指定すればよい。

扱える圧縮のファイル形式

扱えるフォーマットは、

がある。 compression オプションで圧縮フォーマットを指定しない場合、
ファイルの拡張子から圧縮フォーマットを推定してくれるみたいだが、
明示的に記載している方がよいと思われる。