r0w0

PythonやDeepLearning関連で学んだこと、調べたことの備忘録

parquetの中身を閲覧

閲覧方法

こちらの記事を参考にさせていただいた。 ※一部、Docker for Windows仕様に書き直している

# imageの取得
docker pull nathanhowell/parquet-tools   
# dirをマウントし、配置したparquetファイルの中身を表示
docker run -v D:\Programs\cur_dir:/parquet-mr/parquet-tools nathanhowell/parquet-tools head /parquet-mr/parquet-tools/someghing.parquet

背景

データエンジニアのような仕事も増えてきたので、AWSについて触ってみている。 AWS Glueで適当な複数のJSONをDynamicFrameとして読み込み、1行1レコードとなるよう配列をExplodeしたうえでparquetとして保存したとき、複数のparquetが出力された。 1parquet1レコードなのか、1parquet1JSONファイルなのか、適当なサイズごとにファイルが分かれて保存されているのか分からず、中身を除きたくなった。 ※AWS Glueのスクリプト

結果

1つ目のJSONファイルに対応する内容が1つ目のparquetに保存されていた。 ドキュメントによると、Parquetの変換は小さなファイルのグループ化はサポートしていないらしい。

適度なサイズのオブジェクトにグループ化しないと、オブジェクト読み込みのオーバーヘッドが大きくなってしまうと良く聞く。今回のケースだと、CSVで小さなファイルのグループ化を行ったうえで、parquetに変換することになるのだろうか?

Docker with PyCharm

経緯と結果

Docker Desktop for Windowsのインストールに続き、普段の個人開発環境にDockerを導入してみようということで、PyCharmのインタプリタにDockerで作った環境を用いてみました。

尚、以下のような注意点があり、

結果として、PyCharmProの2020版でやっとこの機能を使うことができました。

ただ、コンテナの起動停止を伴うせいか、ローカルのインタプリタの利用と比べて、プログラムの実行・デバッグの停止が数秒ずつ遅くなってしまいました。またライブラリを追加した際に、キャッシュの削除が必要になるのも面倒そうです。。

Dockerfileの作成

Python環境が構築されるコンテナを定義します。

FROM python:3.8
ADD . /var/pycharm/prj
WORKDIR /var/pycharm/prj
RUN pip install -r ./requirements.txt
  • FROM python:3.8
    • ベースイメージとしてpython:3.8を指定(親イメージを持たないイメージをベースイメージと呼ぶ)
  • ADD . /var/pycharm/prj
    • ADD [src] [dist] であり、ビルドコンテキストからみた[src]を、Dockerイメージ内の[dist]に追加.
  • WORKDIR /var/pycharm/prj

docker-compose.yml

version: '3.8'
services:
  app:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    volumes:
      - '../:/var/pycharm/prj'
    container_name: python
    environment:
      - COMPOSE_PROJECT_NAME='sandbox'
    tty: true
    working_dir: '/var/pycharm/prj'
  • services: docker-composeを通して構築されるサービス(コンテナ?)を定義
  • app: サービス名の任意の名前
  • version: docker-composeのバージョン
  • build: ビルドコンテキストとしてDockerに送信するディレクトリの指定(context)とビルドに用いるDockerファイルの指定
  • volume: ボリューム(コンテナ間で共通してアクセス可能な領域)のマウント。host_dir : docker_dirという対応。ここではPyCharmのプロジェクト内のファイル(../)が/var/pycharm/prj以下に配置されるようになっている。

インタプリタの作成

こちらを参照。

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

  1. requirements.txtを更新
  2. イメージの再作成 docker-compose -f ./docker/docker-compose.yml build

これでPython interpreterを見るとライブラリが追加されてると思います。

一方、エディター上ではimportしてもライブラリが無いというエラーが出たままになったりします。

これはキャッシュの削除で直りました。

PythonでGoogle Driveからファイルをダウンロード

背景

海外のサーバーに大きめのファイルをアップロードしたいが、FTPで直接アップロードすると時間が掛かる、あるいは通信が不安定で途中で落ちてしまう。そこで、一度Google Driveにアップロードして、それをサーバーから直接ダウンロードすることにした。

手順

ファイルIDを取得

Google Driveで以下のような共有リンクを取得することができます。

https://drive.google.com/file/d/%ファイルID%/view?usp=sharing

そのうち、上記のファイルIDに該当する箇所をメモします。 尚、リンクを知っているひとはダウンロードできる設定を選択しておく必要があります。

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

gdownというライブラリをインストールします。

pip install gdown

Pythonでダウンロード

メモしたfile_idと、ダウンロードしたときの保存ファイル名(output)を指定し以下を実行します。

import gdown
file_id = %ファイルID%
url = f"https://drive.google.com/uc?id={file_id}"
output = "download.zip"
gdown.download(url, output, quiet=False)

尚、大きめのファイルを何度もダウンロードすると、Google Drive側からアクセスを一定時間拒否されるようになります。

私の場合、24時間前後アクセスができない状態になりました。

Docker Desktop for Windows

Dockerをそろそろ触ってみようかと思い、Docker Desktop for Windowsをインストールしました。ファイアウォールの設定で少しはまったので忘れないうちにメモ。

手順

私のラップトップはWindows 10 Homeなので、Windows Home に Docker Desktop をインストールに従って進めました。

 WSL2 のインストール

Docker Desktop for Windowsのシステム要件に「Windows 上で WSL2 機能の有効化」が含まれています。基本的にはマイクロソフトのドキュメントに従って進めます。

ただ私の場合「Step 4 - Download the Linux kernel update package」で、ダウンロードしたパッケージがインストールできませんでした。

こちらで記載されているように、Windows Insider Programに入っている必要があるようで、この手順にしたがってActivateしたらインストールが行えました。

Docker Desktop for Windowsのインストール

こちらからダウンロードしてインストールします。

Docker Desktop for Windowsの起動

Windowsのタスクトレイにクジラマークが現れます。起動中はdocker is starting、起動後はrunningと表示されます。

しかしstartingの状態でスタックしてしまいました。

GitのIssueに記載されている3つのファイルについて、ファイアウォールの例外設定に指定する必要がありました。

C:\program files\docker\docker\resources\vpnkit.exe C:\program files\docker\docker\com.docker.service C:\program files\docker\docker\resources\com.docker.proxy.exe

私のラップトップではウイルスバスターを利用しているので、当初ウイルスバスターの例外設定に指定したのですがダメで、Windows Securityの設定も(あるいはこれのみ?)必要でした。

ImageのPull

docker pull ubuntu:18.04 を実行したがレスポンスがない。

Docker Desktop for Windowsの診断画面で診断をクリックした後にログへの参照リンクがある。ログをながめるとHTTP通信がTimeOutしているようでした。

こちらの記事を参考にDNSの設定を8.8.8.8に変更したところpullできるようになりました。

SSDしか勝たん

学習に利用できるオンプレサーバーがあるのですが、他のひとも利用するので混みあってQueue待ちすることがあったので、GoogleCloudPlatformを使ってみました。

当たり前っちゃ当たり前なのですが、画像のような重たいデータを読み込んで学習する場合、HDDで環境を作ると速度が全くでませんでした。最初はGPUが弱いのかと思って高価なGPUを割り当てたのですが特に変わらず。

SSDに変更したら以下のように速度が5倍になりました。

GPU HDD SSD
Tesla K80 120sec/iter -
Tesla P100 100sec/iter 20sec/iter

 

オンプレの方はGeForce GTX 1080で13sec/iterなので、GCPだと学習に2倍弱かかってしまう計算ですが、それでも実用的な範囲に収まりそうで良かった。

PyTorchとTensorFlowのTransposed convolution layerの処理手続きの違い

状況

DCGANの実装について、DCGANの論文PyTorchのTutorialを元に調べていた。

Tutorialでは、Generatorが 1024 x 4 x 4 のFeatureMapを 512 x 8 x 8 にUpsamplingする際、以下の層を用いる。

nn.ConvTranspose2d(in_channel=1024, out_channnel=512, kernel_size=4, stride=2, padding=1, bias=False),

実際、1 x 4 x 4 のテンソルをこの層に与えると、以下のように (1 x 8 x 8)の出力が得られる。

# transposed-convolution layer
trans_conv = nn.ConvTranspose2d(in_channels=1, out_channels=1, kernel_size=4, stride=2,
                                padding=1, bias=False)
# initialize weights with 1
trans_conv.weight.data = trans_conv.weight.data.fill_(1)

# input tensor
input_tensor = torch.Tensor([[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]])
input_tensor = input_tensor.reshape((1, 1, 4, 4))
print(f"Input tensor shape: {input_tensor.shape}")
print(f"Output tensor shape: {trans_conv(input_tensor).shape}")
print(f"Output tensor: \n{trans_conv(input_tensor)}")


# Input tensor shape: torch.Size([1, 1, 4, 4])
# Output tensor shape: torch.Size([1, 1, 10, 10])
# tensor([[[[1., 2., 2., 2., 2., 2., 2., 1.],
#           [2., 4., 4., 4., 4., 4., 4., 2.],
#           [2., 4., 4., 4., 4., 4., 4., 2.],
#           [2., 4., 4., 4., 4., 4., 4., 2.],
#           [2., 4., 4., 4., 4., 4., 4., 2.],
#           [2., 4., 4., 4., 4., 4., 4., 2.],
#           [2., 4., 4., 4., 4., 4., 4., 2.],
#           [1., 2., 2., 2., 2., 2., 2., 1.]]]],
#        grad_fn=<SlowConvTranspose2DBackward>)

一方、論文では以下の図のように、kernel size=5, stride=2と記述されている。

f:id:r0w0:20200717005329p:plain
DCGAN論文の図

しかし、この設定を用いると(いかなるPaddingを設定しても)、PyTorchでは 1 x 8 x 8 の出力は得られない。

これはどういうことだ?というのが調査のスタート地点。

結論の概略

調査の結果、Transposed convolution layerは少なくとも2つの異なる計算方法があり、それによってkernel sizeやstrideを処理にどう用いるかが異なってくることが分かった。

DCGANの論文のパラメータは、TensorFlowが用いている処理手法を前提にしているようで、DCGANの論文を(TensorFlowをbackboneにした)Kerasで実装している記事では、kernel size=5, stride=2を用いていた。一方、PyTorchのTutorialではパラメータをkernel size=4, stride=1で計算しており、出力されるFeatureMapのShapeは同じなものの、計算手続きが異なるため出力テンソルの値も異なる。

以下、それぞれの処理手法を記載、比較する。

PyTorchが用いている処理手法

入力テンソルは (1 x 4 x 4)、kernel_size=4, stride=2, padding=1の設定で考える。

また、入力テンソルの値とレイヤーの重みは全て1とする。

処理手続きは以下。

0埋めされた出力テンソルを用意

1 x 10 x 10 の0埋めされた出力テンソルが作られる。

10 x 10 なのは次の工程の処理範囲が10 x 10に及ぶため。

f:id:r0w0:20200717075555p:plain

Inputのテンソルを走査

各走査の処理としては、Inputのあるグリッドの値とカーネルの重みを掛け、結果をOutputのテンソルに加算する。

今回、入力テンソルは以下のように 1 x 4 x 4 のShapeの全て1の値を持つテンソルである。

そして、1回目の走査処理が行われるセルの値は1。

入力テンソル

この1の値を、以下のカーネルの値と掛け合わせます。

f:id:r0w0:20200717074942p:plain

結果、全てのセルに1を持つ(1 x 4 x 4)のShapeのテンソルが得られます。このテンソルを、出力テンソルの (1, 1) の位置を基準に加算すると、以下の出力テンソルが得られます。

f:id:r0w0:20200717075731p:plain

これを、全ての入力テンソルのグリッドに対して行うと、以下の 1 x 10 x 10 の出力テンソルが得られます。

走査後の出力テンソル

Paddingの適用

最後に、Paddingを適用します。Paddingは、指定された数だけ外周を削除する処理です。

ここではpadding=1が指定されているので、外周を1マス分削除すると以下の結果が得られます。

参考にしたサイト

このパターンの処理手続きについては、以下のサイトを参考にしました。

Dive into deel learning

TensorFlowが用いている処理手法

入力テンソルは (1 x 4 x 4)、出力テンソルが(1 x 8 x 8)、kernel_size=5, stride=2, padding="same"の設定で考える。

また、入力テンソルの値とレイヤーの重みは全て1とする。Kerasを用いた実際の計算結果は以下。

from tensorflow.keras.layers import Conv2DTranspose
from tensorflow.keras import initializers
import numpy as np

# Input
input_array = np.array([[1,1,1,1],[1,1,1,1],[1,1,1,1],[1,1,1,1]], dtype=np.float32).reshape((1,4,4,1)) # c, w, h
print(f"Input tensor shape: {input_array.shape}")
# Transposed convolution layer
layer = Conv2DTranspose(1, kernel_size=5, strides=2, padding='same', kernel_initializer=initializers.Ones())
# Calculation
print(f"Output tensor shape: {layer(input_array).shape}")
print(f"Output tensor: \n{np.array(layer(input_array)).reshape((8, 8))}")

# Input tensor shape: (1, 4, 4, 1)
# Output tensor shape: (1, 8, 8, 1)
# Output tensor: 
# [[1. 2. 2. 3. 2. 3. 2. 2.]
#  [2. 4. 4. 6. 4. 6. 4. 4.]
#  [2. 4. 4. 6. 4. 6. 4. 4.]
#  [3. 6. 6. 9. 6. 9. 6. 6.]
#  [2. 4. 4. 6. 4. 6. 4. 4.]
#  [3. 6. 6. 9. 6. 9. 6. 6.]
#  [2. 4. 4. 6. 4. 6. 4. 4.]
#  [2. 4. 4. 6. 4. 6. 4. 4.]]

InputテンソルにStrideを反映

StrideがN+1の場合、Nの数の長さだけInputテンソルのグリッドを格子状に変形します。今回はN=1なので、各値の間を1つ分ゼロパディングします。

Inputのテンソルを「カーネルで」走査

PyTorchではInputテンソルの各セルを1つずつ走査し、Inputテンソルの1セル×カーネルをOuputテンソルに反映していた。

一方、Kerasではカーネルのサイズの窓を1セルずつずらして走査し、Inputテンソルの窓に含まれる値の総和をOutputテンソルの1セルに反映する。

f:id:r0w0:20200717062527p:plain

なぜこの位置から走査が始まるのかは、padding="solid"の結果と比較すると分かりやすい。

padding="solid"にすると、入力テンソルの左上にから右下に掛けて走査され、結果、以下のように 1 x 11 x 11の結果が得られる。

f:id:r0w0:20200717071624p:plain

この結果のサブセット(青いセル)が、padding="same"にした場合の出力である。

f:id:r0w0:20200717071909p:plain

KerasのConv2DTranspose layerのドキュメントによると、validはpaddingなし、sameは入力テンソル(stride反映後)と同じShapeのテンソルが出力されるように調整されるとある。

つまり、validの設定より得られる出力テンソルから、Stride反映後の入力テンソルと同じサイズになるよう外周を取り除いた(paddingした)結果を返すのがpadding="same"の挙動のようだ。

PyTorch方式とTensorFlow方式の比較

どちらの計算方法にしても、ある領域の入力テンソルカーネルの重みを掛けた総和を出力している。

今回の計算では入力テンソルの値も重みも1なので、出力テンソルのセルが考慮している入力テンソルのセルの数が値として表現される。

以下の設定で比較したとき、

# PyTorch
trans_conv = nn.ConvTranspose2d(in_channels=1, out_channels=1, kernel_size=4, stride=2, padding=1, bias=False)
# Keras(TensorFlow)
layer = Conv2DTranspose(1, kernel_size=5, strides=2, padding='same')

出力されるテンソルはそれぞれ以下のようになる。

f:id:r0w0:20200717072453p:plain

特徴を比べてみると以下のようになる。

PyTorchは、 * シンメトリック * 値の幅が1~4

TensorFlowは * シンメトリックではない * 値の幅が1~9

この違いが学習にどういった影響を与えるのか今のところ分からないが、特徴の処理の手法が異なることは間違いないので、利用するライブラリを変更したら学習結果が大きく変わるということが起きそうな気がしている。

同じ出力結果を得られるパラメータ設定やオプションはあるのだろうか。。

まとめ

長くなってしまったが、DCGANの論文で示されている「kernel size=5, stride=2」のパラメータを用いて、どうやったら 1024 x 64 x 64 の特徴マップを 512 x 32 x 32 に変換できるのかという当初の疑問については以下の結論を持って解消することができた。

  • TensorFlowが用いている計算手法を利用すれば変換可能
  • PyTorchを用いる場合はkernel_size=4, stride=2, padding=1を利用すれば変換可能

余談としては、任意のShapeの出力は異なる組み合わせのパラメータで出力することが可能なので、kernel_size=4, stride=2, padding=1だけでなく、kernel_size=5, stride=1, padding=0でもよい。

PyTorchのTensor.backwardとoptimizer.updateの関係

状況

長きに渡るデータ前処理の日々を過ごしていたらそれ以外の基本的なことを思い出せなくなっていたでござる。First In First Out.

タイトルの通り、PyTorchのTensor.backwardとoptimizer.updateって何をしているのか、どういった関係にあるのかを以下に簡単にまとめる。

パラメータとみなすテンソルの指定

この過去記事で触れたように、テンソル微分対象となる変数かそうで無いかのフラグを持つ。フラグの立っているテンソルはパラメータと呼ばれる。通常、重みテンソルがパラメータとして指定される。

nn.Linearで生成された重みテンソルは標準でこのフラグが立っている。

勾配計算対象テンソルの指定

optimizerの定義時に、何のテンソルについて勾配を用いて更新するのか指定する。

基本的にはパラメータ、つまり重みテンソルを指定する。

optim.SGD(model.parameters(), lr=0.01, momentum=0.9) は、model.parameters()の返すテンソルたちを更新するよと宣言している。

勾配の初期化

テンソルの更新に用いる勾配は、テンソルの .grad に保持されている。その値を0に初期化する。

optimizer.zero_grad()の実行により達成される。

後述する.backwardの動作として、既存の.gradへの上書きではなく加算。従って、意図的に加算したい場合以外は学習イテレーションの都度、.zero_grad()を実行する必要がある。

勾配計算

計算処理の結果の値(基本的にはLoss)のテンソルに対して .backward() を実行する。これにより、入力から出力までの計算グラフにおいて、パラメータを変数とした微分計算が行われる。微分結果、即ち勾配はパラメータのテンソルに保持される(Tensor.gradに保持)。

勾配計算結果をパラメータに反映

opzimizer.step() は「1.」で指定されたテンソルについて、テンソルの.gradの値を用いてテンソル値の更新を行う

参考

machine learning - pytorch - connection between loss.backward() and optimizer.step() - Stack Overflow