1.1 PyTorchの構成
まずはPyTorchのパッケージ構成の全体像を紹介します。表1.1のパッケージを以降使用していきますが、概要だけ理解できれば大丈夫です。
PyTorchの構成は表1.1のようになります。
活性化関数、損失関数、SGDなどの用語は聞き慣れないかもしれませんが、第2章と第3章で説明します。また、ONNXについては第7章で登場します。
1.2 Tensor
ここではTensorというPyTorchの最も基本となるデータ構造とその機能を説明します。
Tensorはその名の通りテンソル、つまり多次元配列を扱うためのデータ構造です。NumPyのndarrayとほぼ同様のAPIを有しており、それに加えてGPUによる計算もサポートしています。Tensorは各データ型に対して定義されており、例えば32bitの浮動小数点数でしたらtorch.FloatTensorを、64bitの符号付き整数でしたらtorch.LongTensorを使用します。
また、GPU上で計算する場合はtorch.cuda.FloatTensorなどを使用します。なお、TensorはFloatTensorのエイリアスです。いずれの型のTensorでもtorch.tensorという関数で作成することができます。
1.2.1 Tensorの生成と変換
Tensorを作る方法はたくさんあり、torch.tensor関数に入れ子構造の多次元のlistやndarrayを渡す方法以外にNumPyと同様にarange、linspace、logspace、zeros、onesなどの定数で作る関数が用意されています。リスト1.1ではいくつかのTensorの生成の例を示しています。
リスト1.1 Tensorの生成例
(以降言及のないリストはすべてJupyter Notebookの入力と出力)
import numpy as np import torch # 入れ子のlistを渡して作成 t = torch.tensor([[1, 2], [3, 4.]]) # deviceを指定することでGPUにTensorを作成する t = torch.tensor([[1, 2], [3, 4.]], device="cuda:0") # dtypeを指定することで倍精度のTensorを作る t = torch.tensor([[1, 2], [3, 4.]], dtype=torch.float64) # 0から9までの数値で初期化された1次元のTensor t = torch.arange(0, 10) # すべての値が0の100×10のTensorを # 作成し、toメソッドでGPUに転送する t = torch.zeros(100, 10).to("cuda:0") # 正規乱数で100×10のTensorを作成 t = torch.randn(100, 10) # Tensorのshapeはsizeメソッドで取得可能 t.size()
torch.Size([100, 10])
TensorはNumPyのndarrayに簡単に変換できます。ただし、GPU上のTensorはそのままでは変換できず、一度CPU上に移す必要があります(リスト1.2)。
リスト1.2 Tensorの変換例
# numpyメソッドを使用してndarrayに変換 t = torch.tensor([[1, 2], [3, 4.]]) x = t.numpy() # GPU上のTensorはcpuメソッドで、 # 一度CPUのTensorに変換する必要がある t = torch.tensor([[1, 2], [3, 4.]], device="cuda:0") x = t.to("cpu").numpy()
1.2.2 Tensorのインデクシング操作
Tensorはndarrayと同様のインデクシング操作をサポートしています。
配列A[i,j]のi、jを添字(インデクス:Index)と言いますが、これを指定して配列の値を取得したり、あるいは変更したりすることを「インデクシング操作」と言います(リスト1.3)。スカラーによる指定の他にスライス、添字のリスト、ByteTensorによるマスク配列(MEMO参照)での指定をサポートしています。
リスト1.3 Tensorのインデクシング操作例
t = torch.tensor([[1,2,3], [4,5,6.]]) # スカラーの添字で指定 t[0, 2] # スライスで指定 t[:, :2] # 添字のリストで指定 t[:, [1,2]] # マスク配列を使用して3より大きい部分のみ選択 t[t > 3] # [0, 1]要素を100にする t[0, 1] = 100 # スライスを使用した一括代入 t[:, 1] = 200 # マスク配列を使用して特定条件の要素のみ置換 t[t > 10] = 20
マスク配列
マスク配列とは元の配列を同じサイズで各要素がTrue/Falseになっている配列のことを言います。例えばa = [1,2,3]のような配列があるとa > 2という操作で[False, False, True]のようなマスク配列が作成できます。
1.2.3 Tensorの演算
Tensorは四則演算や数学関数、線形代数計算などが可能であり、ndarrayの代わりに使用することが可能です。特に行列積や特異値分解(MEMO参照)などの線形代数計算はGPUが使用可能ということもあって大規模なデータの場合にはNumPy/SciPyを使用するよりもはるかによいパフォーマンスを示すことが多々あります。
特異値分解
特異値分解(Singular Value Decomposition、SVD)は線形代数でよく使用される計算であり、行列AをUSVのように3つの行列の積に分解する計算です。UとVは直交行列、Sは対角成分のみの正方行列です。最小二乗法を解く際や、行列の近似・圧縮などに利用されます。
四則演算はTensor同士かTensorとPythonのスカラーの数値との間でのみ可能です。Tensorとndarrayとでは演算がサポートされていないことに注意してください。
また、Tensor同士の場合も同じ型である必要があります。例えばFloatTensorとDoubleTensorの演算はエラーになります。四則演算はndarrayと同様にブロードキャストが適用され、ベクトルとスカラーや行列とベクトル間の演算でも自動的に次元が補間されます。ブロードキャスト可能なshapeの組み合わせに関する詳細なルールはNumPyの公式リファレンスMEMO参照に詳しく書いてありますので必要に応じて参照してください。以下にベクトルや行列についていくつかの例を示します(リスト1.4)。
リスト1.4 Tensorの演算
# 長さ3のベクトル v = torch.tensor([1, 2, 3.]) w = torch.tensor([0, 10, 20.]) # 2 × 3の行列 m = torch.tensor([[0, 1, 2], [100, 200, 300.]]) # ベクトルとスカラーの足し算 v2 = v + 10 # 累乗も同様 v2 = v ** 2 # 同じ長さのベクトル同士の引き算 z = v - w # 複数の組み合わせ u = 2 * v - w / 10 + 6.0 # 行列とスカラー m2 = m * 2.0 # 行列とベクトル #(2, 3)の行列と(3,)のベクトルなのでブロードキャストが働く m3 = m + v # 行列同士 m4 = m + m
ブロードキャスト
●SciPy.org:Broadcasting
URL https://docs.scipy.org/doc/numpy-1.13.0/user/basics.broadcasting.html
PyTorchはTensorに対して様々な数学関数を用意しています。abs、sin、cos、exp、log、sqrtなどのようにTensorの全要素に作用するものの他にsum、max、min、mean、stdなどの集計関数も用意されていますのでndarrayと同様に使用できます。また、Tensorに対して作用する関数の大部分はTensorのメソッドとしても提供されています(リスト1.5)。
リスト1.5 数学関数
# 100 × 10のテストデータを用意 X = torch.randn(100, 10) # 数学関数を含めた数式 y = X * 2 + torch.abs(X) # 平均値を求める m = torch.mean(X) # 関数ではなく、メソッドとしても利用できる m = X.mean() # 集計結果は0次元のTensorでitemメソッドを使用して、 # 値を取り出すことができる m_value = m.item() # 集計は次元を指定できる。以下は行方向に、 # 集計して列ごとに平均値を計算している m2 = X.mean(0)
数学関数以外にもTensorの次元を変更するviewやTensor同士を結合するcatやstack、次元を入れ替えるtやtransposeもよく使用します。viewはndarrayのreshape関数と全く同様です。
catは異なる特徴量を含んだ複数のTensorをまとめる際に用います。
transposeは行列の転置以外に画像データのデータ形式をHWC(縦、横、色)の順番からCHW(色、縦、横)に並べ替える際などにも使用できます(リスト1.6)。
リスト1.6 Tensorのインデクシング操作例
x1 = torch.tensor([[1, 2], [3, 4.]]) # 2×2 x2 = torch.tensor([[10, 20, 30], [40, 50, 60.]]) # 2×3 # 2×2を4×1に見せる x1.view(4, 1) # -1は残りの次元を表し、一度だけ使用できる # 以下の例では-1とすると自動的に4になる x1.view(1, -1) # 2×3を転置して3×2にする x2.t() # dim=1に対して結合することで2×5のTensorを作る torch.cat([x1, x2], dim=1) # HWCをCHWに変換 # 64×32×3のデータが100個 hwc_img_data = torch.rand(100, 64, 32, 3) chw_img_data = hwc_img_data.transpose(1, 2).transpose(1, 3)
線形代数は表1.2のような演算子が使用でき、リスト1.7のような演算ができます。特に大きな行列積や特異値分解など、計算量の多いものはGPU上で実行することで大幅な計算時間短縮が期待できます。
リスト1.7 演算の例
m = torch.randn(100, 10) v = torch.randn(10) # 内積 d = torch.dot(v, v) # 100 × 10の行列と長さ10のベクトルとの積 # 結果は長さ100のベクトル v2 = torch.mv(m, v) # 行列積 m2 = torch.mm(m.t(), m) # 特異値分解 u, s, v = torch.svd(m)
1.3 Tensorと自動微分
ここではTensorと自動微分について解説します。
Tensorにはrequires_gradという属性があり、これをTrueにすることで自動微分を行うフラグが有効になりますMEMO参照。ニューラルネットワークを扱う場合、パラメータやデータはすべてこのフラグが有効になっています。 requires_gradが有効なTensorに対して様々な演算を積み重ねていくことで計算グラフが構築され、backwardメソッドを呼ぶと、その情報から自動的に微分を計算することができます。以下では、
で計算されるLをa_kについてについて微分をしてみます。
このシンプルな例では解析的に解けて、
となりますが、これを自動微分で求めてみます(リスト1.8)。
自動微分とVariable
PyTorch 0.3以前は自動微分を使用するためにはVariableというクラスでTensorをラップする必要がありましたが、0.4からはTensorとVariableが統合されました。
リスト1.8 自動微分
x = torch.randn(100, 3) # 微分の変数として扱う場合はrequires_gradフラグをTrueにする a = torch.tensor([1, 2, 3.], requires_grad=True) # 計算をすることで自動的に計算グラフが構築されていく y = torch.mv(x, a) o = y.sum() # 微分を実行する o.backward() # 解析解と比較 a.grad != x.sum(0)
tensor([ 0., 0., 0.])
# xはrequires_gradがFalseなので微分は計算されない x.grad is None
True
このように解析解と自動微分で求めた微分が一致することが確認できました。このようなシンプルな例では特に自動微分のメリットを感じづらいですが、ニューラルネットワークのように複雑な関数で微分のチェインルールが連続するようなケースでは非常に重要な機能になります。
1.4 まとめ
TensorはNumPyのndarrayと同様に使用できる多次元配列であり、GPUでの計算もサポートしていて大規模な行列の計算などに威力を発揮します。
Tensorは自動で微分を計算することができ、ニューラルネットワークの最適化で重要になります。
続きは本書で
本書『現場で使える!PyTorch開発入門』ではこのあと、深層学習をアプリケーションに実装していくための線形モデルやニューラルネットワークについて解説していきます。PyTorchを使ってみたい方にとっての入門書、ぜひお試しください。