r0w0

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

PythonのAbstractクラスの実装

状況

Pythonのクラス継承をよく使うものの、特に抽象クラスの実装をサブクラスに強制させないような実装をしていた(他人が読むコードではないので)。 実際どうやるんだろうと調べたのでサンプルコード付きで残しておく。

ポイント

  1. 抽象クラスでmetaclass引数に対してABCMetaを与えて継承
  2. 抽象化する関数に @abc.abstractmethod のデコレータをつける

Pythonのドキュメント

@abc.abstractmethodに記載

このデコレータを使うには、クラスのメタクラスが ABCMeta かそれを継承したものである必要があります。

サンプルコード

サブクラスで抽象化メソッドを実装せずエラーが出力される例

from abc import abstractmethod
from abc import ABCMeta


class Abstract(metaclass=ABCMeta):
    def __init__(self):
        super().__init__()
        pass

    @abstractmethod
    def show_message(self):
        pass


class SubClass(Abstract):
    def __init__(self):
        super().__init__()


sub = SubClass()
sub.show_message()

# TypeError: Can't instantiate abstract class SubClass with abstract methods show_message

サブクラスで抽象化メソッドを実装しているのでエラーは出力されない例

from abc import abstractmethod
from abc import ABCMeta


class Abstract(metaclass=ABCMeta):
    def __init__(self):
        super().__init__()
        pass

    @abstractmethod
    def show_message(self):
        pass


class SubClass(Abstract):
    def __init__(self):
        super().__init__()

    def show_message(self):
        print("hoge hoge")


sub = SubClass()
sub.show_message()

# hoge hoge
# Process finished with exit code 0

抽象化メソッドを指定するデコレータをつけていないのでエラーが出力されない例

from abc import abstractmethod
from abc import ABCMeta


class Abstract(metaclass=ABCMeta):
    def __init__(self):
        super().__init__()
        pass

    def show_message(self):
        pass


class SubClass(Abstract):
    def __init__(self):
        super().__init__()


sub = SubClass()
sub.show_message()

# Process finished with exit code 0

一瞬はまった点

最初、普通にABCMetaクラスを継承させたら怒られました。

class Abstract(ABCMeta)

みたいな。

こちらの記事が参考になったのですが、metaclassに指定したクラスはインスタンス生成時の挙動(継承メソッドの実装有無など)の規定・チェックに用いられるみたいですね。

ABCMetaはabstractmethodのデコレータが付与されたメソッドが、サブクラスの作成しようとしているインスタンスに含まれているか確認してくれてるのだと思います。

ModuleNotFoundError in Python

状況

Anaconda上のJupyter notebookでの話。

あるモジュールをインストールして使おうとしたらModuleNotFoundErrorが発生。

インストールはできているし、conda install list の実行結果にも当該モジュールは含まれている。

原因1

異なる環境のPythonを参照していたので、意図した環境のモジュール群が参照できなかった。

import sys
import pprint
pprint.pprint(sys.path)

# Output
# D:\\Programs\\Anaconda\\envs\\他の環境\\python36.zip

原因2

環境セットアップ時に、環境を分けずに(nameを指定せずに)ipykernel installを実行してしまっていたため。

本来実行されるべきコマンド:

python -m ipykernel install --user --name env1

実際実行していたコマンド:

python -m ipykernel install --user

故に、最後にコマンドを実行した環境を参照する ipykernel で上書きされる状態だった。

対処

%USER%\AppData\Roaming\jupyter\python3\kernel.jsonがデフォルトの環境参照先として利用されているようだったので適当な場所へ移動(削除してもよさそうだったが念のため)

次に python -m ipykernel install --user --name env_X を環境毎に実行。

PyTorch: requires_grad = False と optimizerにparamを与えないことの違い

疑問

つくりながら学ぶ! PyTorchによる発展ディープラーニングの転移学習を実装しているときに出てきた疑問。

モデルのパラメータにrequires_grad = Falseを設定することと、optimizerにparamを与えないことは動作的に異なるのだろうか。

書籍の中で、モデルのもつパラメータのうち、更新したいパラメータにはparam.requires_grad = Trueを、それ以外にはFalseを与えている。

# 転移学習で学習させるパラメータを、変数params_to_updateに格納する
params_to_update = []

# 学習させるパラメータ名
update_param_names = ["classifier.6.weight", "classifier.6.bias"]

# 学習させるパラメータ以外は勾配計算をなくし、変化しないように設定
for name, param in net.named_parameters():
    if name in update_param_names:
        param.requires_grad = True
        params_to_update.append(param)
        print(name)
    else:
        param.requires_grad = False

その後、optimizerにparam.requires_grad = Trueのパラメータのみ設定。

# 最適化手法の設定
optimizer = optim.SGD(params=params_to_update, lr=0.001, momentum=0.9)

勾配計算とその更新において、これらの操作がどう影響するのだろうか。

結論

PyTorchのDiscussionを参照した。

param.requires_grad = Falseをパラメータに与えた場合、勾配計算の対象外となり、その結果、勾配も更新されない。 このDiscussionで紹介されている以下のコードの通り、もしgradのないパラメータはp.gradにNoneを持ち、それらのupdate処理はskipされる。

                if p.grad is None:
                    continue

一方、optimizerのparamsに設定しない場合、勾配計算の更新の対象外となるものの、勾配の計算自体は行われる。

検証

転移学習のコードを改変して、以下の2パターンで速度を検証してみた。

  1. 更新したいパラメータについてのみrequires_grad=True かつ optimizerには更新したいパラメータのみ指定
  2. 全てのパラメータにおいてrequires_grad=True かつ optimizerには更新したいパラメータのみ指定
  3. 更新したいパラメータについてのみrequires_grad=True かつ optimizerには全てのパラメータを指定

結果は、

  1. 205秒
  2. 458秒
  3. 216秒

であった。

2においては、不必要に勾配計算している分遅いことが、また1と3については実質同じことであることが確認できた。

まとめ

単にoptimizerのparamsに設定しないだけだと、勾配の計算自体は全てのパラメータについて行われてしまう。

今回の転移学習では最後の層のパラメータのみ更新対象としたので、それ以降の勾配が計算されようがされまいが、時間は不要にかかるものの、結果に影響は無かった。

しかし、(ありうるケースなのかは不明だが)中間の層のパラメータのみ更新対象とした場合、その層から出力層までのパラメータ計算の影響を受けてしまうのでよろしくない。

更新不要なパラメータについては勾配計算もされないよう、更新したいパラメータについてのみrequires_grad=Trueを、それ以外はFalseを設定する必要がある。

Jupyter notebook からpythonモジュール(*.py)をImport

Motivation

関連記事は死ぬほど転がってますが、端的に自分が覚えておきたい点をまとめておきたい。

Import可能なモジュール

pathが通っているフォルダに配置されているモジュールのみインポートが可能。 パスは以下で確認できる。

import sys
sys.path

# 'D:\\Programs\\jupyter\\** directory of the notebook',
#  'D:\\Programs\\Anaconda\\python37.zip',
#  'D:\\Programs\\Anaconda\\DLLs',
#  'D:\\Programs\\Anaconda\\lib',

Import方法

notebook以下にモジュールが配置されているのであれば、特に何もせずインポート可能。

例えばnotebook以下に/module/vgg_voc.pyが配置されておりそれを読み込むならば from module import vgg_voc で読み込める。

notebook配下以外のモジュールを読み込みたい場合は、sys.path.append('module directory') でパスを追加する必要がある。その後の手順は同様。

Install Conda and PyTorch

きっかけ

TensorbordをUninstallしたら環境が壊れてしまった。。

CondaのUpdateもCondaのバージョンとPythonがInconsistentだぞと怒られたので、Condaを再インストールすることにした。

今後同様の操作を行うかもしれないので、再インストール手順を残しておく。

再設定手順

base環境にパッケージインストールすると、(恐らく)python3.8が合わずopencvがインストールできない。

従って新しい環境を作って必要なパッケージをインストールする。

conda remove -n vis --all

conda create -n vis python=3.6.10

activate vis

conda install numpy scipy pandas
y
conda install pandas-datareader matplotlib pillow
y
conda install scikit-learn
y
conda install requests h5py
y
conda install -c conda-forge opencv
y
conda install -c conda-forge imbalanced-learn
y
conda install pytorch torchvision cudatoolkit=10.1 -c pytorch
y
conda install nodejs
y
conda install -c conda-forge jupyterlab
python -m ipykernel install --user
jupyter labextension install @jupyterlab/toc

pip install torchsummary
pip install tensorboard

また設定ファイルおよびカーネルの作成するために以下を実行。

jupyter notebook --generate-config
jupyter_notebook_config.py
# c.NotebookApp.notebook_dir を編集

python -m ipykernel install --user

PyTorch: Tensorはただの行列の入れ物ではなかった

Motivation

まずはPyTorchをどう適用するのか全体像をつかみたいと思い、二値分類データを使ってモデルを組んでCVするところまでPyTorchを使ってみた。ざっくりとだが、何となくPyTorchの使い方が分かった気がする。結構スクラッチな部分が多いんだなという印象。

次に、公式のチュートリアル AUTOGRAD: AUTOMATIC DIFFERENTIATION をこなしていたらTensorについて知らないことが多かったのでまとめておきたい。基本的にはチュートリアルに書いてあることを自分の言葉でまとめ直すだけの記事である。

このチュートリアルで得られたものは大きかった。 Tensorの計算は実はトレースされていて、実は計算グラフを作っていたということに今さら気づいた(思い出した)。 だからこそ、Tensor同士がチェインするような仕様になっている。

モデルクラスのForwardやinitを書いているときに、どうやってネットワーク構造を保持・定義しているのかと疑問だったけど、Forwardの計算を通じて定義されていたのだろう。 PyTorchの何がdefine by runなの?という疑問もここにきて解消するといううれしみ。

Tensor

tensorは.backward()により微分される

以下のTensorを準備する。 xのTensorでrequires_grad=Trueが指定されていることに注意。

x = torch.ones(2, 2, requires_grad=True)
y = x + 2
z = y * y * 3
out = z.mean()
print(y)
print(z) 
print(out)
# tensor([[3., 3.],
#         [3., 3.]], grad_fn=<AddBackward0>)
# tensor([[27., 27.],
#         [27., 27.]], grad_fn=<MulBackward0>)
# tensor(27., grad_fn=<MeanBackward0>)

次にout.backward()を実行すると以下の結果が得られる。

out.backward()
print(x.grad)

# tensor([[4.5000, 4.5000],
#         [4.5000, 4.5000]])

これはxを変数としてoutを微分した結果。 \displaystyle \frac{dout}{dx} = \frac{dout}{dz} \cdot \frac{dz}{dy} \cdot \frac{dy}{dx}

具体的には

 out = \frac{1}{4} (z_1 + z_2 + z_3 + z_4)

なので、z_1に着目すると、

\displaystyle \frac{dout}{dx} = \frac{dout}{dz_1} \cdot \frac{dz_1}{dy} \cdot \frac{dy}{dx} = \frac{1}{4} \cdot 6y = \frac{18}{4} = 4.5

つまり、requires_grad=Trueを指定することでそのTensorを変数・パラメータとして扱い、かつbackpropによって微分が行われ勾配が計算される。

print(tensor)にNone/Falseのattributeは表示されない

テンソルはパラメータ扱いなのかどうか、printから確認できるのかどうか見てみた。

printでtensorを表示すると、gradがTrueの場合、該当attributeが表示される。

x = torch.ones(2, 2)
print(x)

# tensor([[1., 1.],
#         [1., 1.]])

x = torch.ones(2, 2, requires_grad=True)
print(x)

# tensor([[1., 1.],
#        [1., 1.]], requires_grad=True)

一方Falseの場合、そもそもそのAttributeの表示が省略されていることが分かる。

requires_gradはDefaultでFalse

前セクションのコードの実行結果のとおり、requires_gradは指定しなければ、DefaultでFalseである。

明示的にTrueを与えるとgrad計算の対象となる仕組み。

Weightはrequire_grad=Trueになっている?

上述の理屈に従うと、weightはrequire_grad=Trueでなければいけない。確認してみると、

for i in net.parameters():
    print(i)
    break

# Parameter containing:
# tensor([[-0.1722, -0.0790,  0.1510,  ...,  0.1590,  0.1150, -0.0679],
#         [-0.0703, -0.0227, -0.0645,  ...,  0.1713,  0.1660,  0.0976],
#         ...,
#         [ 0.1608, -0.0622,  0.0199,  ...,  0.0639, -0.0312,  0.0231]],
#        requires_grad=True)

ちゃんとrequires_grad=Trueになっている。

Operation結果によりTensorが作成されるとgrad_fnも指定される

grad_fnは以下のチュートリアルの説明にもある通り、Tensorを作成したオペレーション関数が指定される。

Each tensor has a .grad_fn attribute that references a Function that has created the Tensor (except for Tensors created by the user - their grad_fn is None).

また、Tensorを作成する式の最後のオペレーションがgrad_fnに指定されると思われる。 以下の式では、zは掛け算のオペレーションにより作成されているのでgrad_fn=<MulBackward0>が与えられる。

x = torch.ones(2, 2, requires_grad=True)
x2 = torch.ones(2, 2)
z = x * x2
print(z)

# tensor([[1., 1.],
#         [1., 1.]], grad_fn=<MulBackward0>)

一方、掛け算と足し算の組み合わせの計算では、足し算の演算が最後に行われるのでgrad_fn=<AddBackward0>が与えられている。

z = x * x2 + x
print(z)

# tensor([[2., 2.],
#         [2., 2.]], grad_fn=<AddBackward0>)

grad_fnの役割は・・・?

チュートリアル中では以下のように説明されている。grad_fnは計算グラフを構築しているよと。

Tensor and Function are interconnected and build up an acyclic graph, that encodes a complete history of computation.

またチュートリアル次のページでは以下のコードとともに、

output = net(input)
target = torch.randn(10)  # a dummy target, for example
target = target.view(1, -1)  # make it the same shape as output
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

# tensor(1.4288, grad_fn=<MseLossBackward>)

以下の説明がある

Now, if you follow loss in the backward direction, using its .grad_fn attribute, you will see a graph of computations that looks like this:

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d -> view -> linear -> relu -> linear -> relu -> linear -> MSELoss -> loss

1.4288の値を持つTensor:lossは、MSELoss関数であるgrad_fn=<MseLossBackward>の関数により作成された(計算グラフ的につながっている)ことがprint(loss)の結果より示されている。ふんわり解釈するとgrad_fnはそのTensorがどの関数(Operationともいえる?)に作成されたかを示し、その数珠繋ぎにより計算グラフが構築される。

PyTorch: Datasetの継承

疑問

PyTorchのDatasetを継承したクラスを作成するときに、super().initを呼び出す必要があるのか、という疑問。そもそも継承時の動作を忘れているのでその思い出しも含めて確認してみる。

コードはこちらの記事を参考に書いています。

継承 in python

結論としては、

  • 子クラスの引数に親クラスを指定することで継承が行われる
  • 継承により、親クラスの変数とメソッドが引き継がれる
  • 子クラスにおける同名の変数/メソッドの作成により上書きがされる となる。

それぞれの動作を確認していきたい。

継承により親クラスの変数とメソッドが引き継がれる

クラス宣言時に親クラスを指定することで行われる。

# 親クラス
class Fish(object):
species = "魚"
def __init__(self, name, build="骨"):
self.name = name
self.build = build

def greeting(self):
print(f"{self.name} in 親クラス です。")

この親クラスに対してコンストラクタ未実装の子クラスを実装、greetingメソッドを実行してみる。 結果、継承により親クラスのコンストラクタが呼び出されるが、引数が足りていないのでエラー。

# 子クラス
class Medaka(Fish):
    def detail_greeting(self):
        print(f"{self.name}です。{Medaka.species}の一種です。{self.build}で構成されています。")

# インスタンス作成とメソッドの実行
medaka = Medaka()

# ---------------------------------------------------------------------------
# TypeError                                 Traceback (most recent call last)
# <ipython-input-50-7a6e90b86513# in <module#
# ----# 1 medaka = Medaka()
# 
# TypeError: __init__() missing 1 required positional argument: 'name'

次に、ここで引数を与えて再実行する。 結果、初期化が成功しメソッドも実効できる。 親クラスのインスタンス変数である self.name, クラス変数である Medaka.species, そしてインスタンスメソッドであるgreetingが参照できていることが分かる。

medaka = Medaka("めだか")
medaka.greeting()
medaka.detail_greeting()

# めだか in 親クラス です。
# めだかです。魚の一種です。骨で構成されています。

子クラスにおける同名の変数/メソッドの作成により上書きがされる

次に、子クラスのコンストラクタを実装して、親クラスの継承コンストラクタをOverideする。

class Medaka(Fish):
    def __init__(self, name):
        self.name = name
        self.dob = "2019年12月12日"
        
    def detail_greeting(self):
        print(f"{self.name}です。{Medaka.species}の一種です。{self.build}で構成されています。{self.dob}生まれです。")

コンストラクタのOverideにより、子クラス内のインストラクタのみが呼び出される。 子クラスのインストラクタでは build 変数を指定していないので子クラスのインスタンスに保持されない。 結果、detail_greetingがエラーとなる。

medaka = Medaka("めだか")
medaka.greeting()
medaka.detail_greeting()
# ---------------------------------------------------------------------------
# AttributeError                            Traceback (most recent call last)
# <ipython-input-57-1be94e797675> in <module>
#       1 medaka = Medaka("めだか")
#       2 medaka.greeting()
# ----> 3 medaka.detail_greeting()
# 
# <ipython-input-56-d547077f6b07> in detail_greeting(self)
#       5 
#       6     def detail_greeting(self):
# ----> 7         print(f"{self.name}です。{Medaka.species}の一種です。{self.build}で構成されています。{self.dob}生まれです。")
# 
# AttributeError: 'Medaka' object has no attribute 'build'

子クラス内で親クラスのコンストラクタをOverideしたうえで、親クラスのインスタンス変数も利用したい。 その場合、super().__init__(name) により親クラスのコンストラクタを、子クラスのコンストラクタから呼び出すことになる。

class Medaka(Fish):
    def __init__(self, name):
        super().__init__(name)
        self.dob = "2019年12月12日"
        
    def detail_greeting(self):
        print(f"{self.name}です。{Medaka.species}の一種です。{self.build}で構成されています。{self.dob}生まれです。")

結果として、親クラスのコンストラクタ内で宣言している self.name や self.build が作成される。

medaka = Medaka("めだか")
medaka.greeting()
medaka.detail_greeting()

# めだか in 親クラス です。
# めだかです。魚の一種です。骨で構成されています。2019年12月12日生まれです。

 継承 in TORCH.UTILS.DATA.DATASET

ソースから以下のことが分かる。

  • objectを継承
  • コンストラクタ及びインスタンス変数が存在しない
  • getitemaddのメソッドのみ実装されている
class Dataset(object):

    def __getitem__(self, index):
        raise NotImplementedError

    def __add__(self, other):
        return ConcatDataset([self, other])

  情報と結論をまとめると、

  • super().__init__() を実行すると、Dataset内で init が実装されていないので、 object の init が呼ばれる
  • object の init を呼ぶことに特別な意味があるとは今のところ聞いていない
  • 従って、自作Datasetのコンストラクタ内で super().__init__() を呼ぶ意味は無い

Datasetを継承し、子クラス内でコンストラクタや各メソッドをOverideすればOKと考えられる。