pyTorchのTensor型とは

2019/9/29 投稿
2019/11/8 やや見やすく編集(主観)
2020/2/1 Tensor型の微分計算の解説Link追加

# 0. この記事の対象者
- pythonを触ったことがあり,実行環境が整っている人
- pyTorchを初めて触る人
- pyTorchのTensor型をしっかり理解したい人
- pyTorchでの機械学習でTensor型dataをどう使っているかを知りたい人

# 1. はじめに
昨今では機械学習に対しpythonという言語が主に使用され,さらにmoduleとして**pyTorch**というものが使用されることがある.
今回はそのpyTorchを使用するための前準備として**Tensor型**というものの説明をしていく.

ただしあくまで参考程度にしてほしいということと,簡潔に言うために間違った表現や言い回しをしている場合があるかもしれないが,そこはどうかご了承していただきたい.

# 2. Tensor型とは
正確に言えば「**torch.Tensor**」というもので,ここではpyTorchが用意している特殊な型と言い換えて**Tensor型**というものを使用する.
実際にはnumpyのndarray型ととても似ており,ベクトル表現から行列表現,それらの演算といった機能が提供されている.
何が違うかというとTensor型はGPUを使用して演算等が可能である点だ.

多数の機能を持ち,それらを紹介しきることはとても難しいため詳しくは以下Linkを見てほしい.

[torch.Tensor 公式サイト](https://pytorch.org/docs/stable/tensors.html)

# 3. pyTorchのインストール
pyTorchを初めて使用する場合,pythonにはpyTorchがまだインストールされていないためcmdでのインストールをしなければならない.
下記のLinkに飛び,ページの下の方にある「QUICK START LOCALLY」で自身の環境のものを選択し,現れたコマンドをcmd等で入力する(コマンドをコピペして実行で良いはず).

[pytorch 公式サイト](https://pytorch.org/)


# 4. Tensor型のdata宣言
早速Tensor型のdataを作っていくのだが,まずはpyTorchを使用するためにimportをする.
以下からはcmd等ではなくpythonファイルに書き込んでいく.

```ruby:filename.rb
import torch
```

続いて以下からは大きく分けてよく使う

- 0または1埋めされたdata
- 初期値を自分で決めたdata
- ランダムなdata

の3つを紹介する.

ただし,注意してほしいのがここで書いた方法以外の書き方も存在しているので自分でやりやすいものを使ってほしい.

### 4-1. 0または1埋めされたdata
以下にコードとその出力を示す.

```ruby:filename.rb
a = torch.zeros([2, 4], dtype=torch.int32)
b = torch.ones([2, 3, 3])
print(a)
print(b)

------'''以下出力結果'''--------
tensor([[0, 0, 0, 0],
        [0, 0, 0, 0]], dtype=torch.int32)
tensor([[[1., 1., 1.],
         [1., 1., 1.],
         [1., 1., 1.]],

        [[1., 1., 1.],
         [1., 1., 1.],
         [1., 1., 1.]]])
```

このように0または1のみのdataを作成できた.
dataは引数のlist型,[2,4]などから形が決まり,この場合は2×4の行列ができる.
もちろん**b**のように3次元以上のdataも作ることができ,この場合2×3×3のdataになっている.

**a**のように宣言時に「**dtype**」を決めれば各要素のtypeを決めることもできる.その場合出力にある通りtype情報が入る.
**b**のようにtype宣言をしなければtypeは自動で**torch.FloatTensor**になる.

またTensor型の利点であるGPUの使用は以下のようにすればよい.

```ruby:filename.rb
device = torch.device('cuda:0')
c = torch.ones([4], dtype=torch.float64, device=device)
d = torch.zeros([4, 1])
d = d.to(device)
print(c)
print(d)

------'''以下出力結果'''--------
tensor([1., 1., 1., 1.], device='cuda:0', dtype=torch.float64)
tensor([[0.],
        [0.],
        [0.],
        [0.]], device='cuda:0')
```

1行目の「**device = torch.device('cuda:0')**」は**cuda:0**というGPUを使うことを宣言している.
もちろんCPUを使用したい場合は**cpu**とすれば使用できる.
また**c**のように宣言時に書き込む方法と,**d**のように「**xxx.to(device)**」とする方法があるが,どちらも結果に変わりはない.
また,この例のように行ベクトル,列ベクトルももちろん作成できる.


### 4-2. 初期値を自分で決めたdata
以下にコードと出力を示す.

```ruby:filename.rb
import numpy as np

a = torch.tensor([1, 2, 3, 4, 5], dtype=torch.int32)
b = torch.tensor(np.array([[1, 2, 3], [4, 5, 6]]))
print(a)
print(b)

------'''以下出力結果'''--------
tensor([1, 2, 3, 4, 5], dtype=torch.int32)
tensor([[1, 2, 3],
        [4, 5, 6]])
```

このようにlist型,またはnumpyによるndarrayもTensor型のdataに変換できる.
GPUの使用は同様にできる.

もちろん以下のように単一の値も生成できる.

```ruby:filename.rb
c = torch.tensor(2.5)
print(c)

------'''以下出力結果'''--------
tensor(2.5000)
```

### 4-3. ランダムなdata
以下にコードと出力を示す.

```ruby:filename.rb
a = torch.Tensor(3, 4)
b = torch.rand(2, 3, 3)
print(a)
print(b)

------'''以下出力結果'''--------
tensor([[2.5226e-18, 1.6898e-04, 1.0527e-11, 7.7195e-10],
        [1.6785e-07, 4.1200e-11, 4.2891e-08, 2.9575e-18],
        [6.7333e+22, 1.7591e+22, 1.7184e+25, 4.3222e+27]])
tensor([[[0.6788, 0.1821, 0.5919],
         [0.0818, 0.9710, 0.5956],
         [0.5885, 0.7288, 0.3226]],

        [[0.6460, 0.5977, 0.0555],
         [0.7144, 0.8958, 0.3947],
         [0.0432, 0.8336, 0.2066]]])
```
どちらも形だけ指定すればランダムな値を持ったdataが返ってきている.
**a**は極小の値をランダムに生成し,**b**は[0,1)の一様分布からランダムに生成している.

また,指定した値までの整数による乱数は以下のようにする.

```ruby:filename.rb
c = torch.randint(100, (3, 4))
d = torch.randint(10, 20, (5, ))
print(c)
print(d)

------'''以下出力結果'''--------
tensor([[58, 25,  5, 85],
        [64, 78,  2, 17],
        [49, 87, 92, 58]])
tensor([11, 15, 11, 18, 15])
```

最初の引数で乱数の上限を指定し,**c**のように1つの引数だけなら0からその値まで,**d**のように2つ引数があればその範囲での乱数となる.

# 5. dataの参照と取り出し
生成したdataは通常のarrayのように参照できる.
以下に例を示す.

```ruby:filename.rb
a = torch.tensor(np.array([[1, 2, 3, 4], [5, 6, 7, 8]]))
print(a)
print(a[0])
print(a[0, 1])
print(a[0][1])
print(a[:,1])
print(a[1][1:3])

------'''以下出力結果'''--------
tensor([[1, 2, 3, 4],
        [5, 6, 7, 8]])
tensor([1, 2, 3, 4])
tensor(2)
tensor(2)
tensor([2, 6])
tensor([6, 7])
```

この例のように**[0,1]**と**[0][1]**の参照はもちろん同じであるし,スライス**[:]**も使用できる.
注意すべきは**a[1][1:3]**は行列の1行目と1列,2列の要素を取り出しているのだが,**1:3**は1列目から3-1=2列目の要素を取り出し,その返り値は**列ベクトルでなく行ベクトルで返ってきている**.

また,この取り出した値,またはベクトルは全てTensor型になっている.
ただの値を取り出したい場合は以下のようにする.

```ruby:filename.rb
a = torch.tensor(np.array([[1, 2, 3, 4], [5, 6, 7, 8]]))
print(a)
print(a[0][1].item())

------'''以下出力結果'''--------
tensor([[1, 2, 3, 4],
        [5, 6, 7, 8]])
2
```

この「**.item()**」がただの値を取り出してくれる.
注意してほしいのは,**ベクトルや行列はこの方法では取り出せない**ということである.

# 6. Tensor型のもう1つのメリットのgrad
Tensor型はGPUを使用できることがメリットだといったが,実はさらに勾配情報(gradient)というものが持てる.
これは機械学習にはとても重要な情報で,パラメータの更新の際に必要となる.なぜ必要かなどはここでは割愛するため,自身で調べてほしい.
以下に例を示す.

```ruby:filename.rb
x = torch.tensor(2.5, requires_grad=True)
y = 4.5*x + 2
y.backward()
print(y)
print(x)
print(x.grad)

------'''以下出力結果'''--------
tensor(13.2500, grad_fn=<AddBackward0>)
tensor(2.5000, requires_grad=True)
tensor(4.5000)
```

まず,この例は**y=4.5x+2**という1次関数を考えている.
**x**は2.5という値を持ち,宣言時に勾配情報を持てるように「**requires_grad=True**」とする.
2行目でyに演算結果を代入し,3行目で勾配情報を計算させている.
この「**xxx.backward()**」がその**xxx**の**今までの演算を全て遡って**,**requires_grad=True**である変数に勾配情報を持たせる.

**y**の出力より**grad_fn=&lt;AddBackward0&gt;**はyの演算過程に**requires_grad=True**の変数があり,backward()可能であることを示している.

ただの**x**の出力を見たらわかるようにただの**x**には勾配情報はない.
勾配情報は**x.grad**とすることで初めて見れる.

ちなみに勾配とはbackward()したyをその変数(今回は**x**)で微分した時の値である.

微分周りの詳しい動作は以下のLinkを参照してほしい.

[pyTorch backwardできない例まとめ](https://qiita.com/mathlive/items/3dcb46af2e2f0eca559a)

# 7. dataのsize,shape
作成したdataのsizeやshapeを確認することはデバック時に必要になる.
以下に例を示す.

```ruby:filename.rb
a = torch.ones(2,3,5)
print(a.size())
print(a.shape)
print(a.shape[1])

------'''以下出力結果'''--------
torch.Size([2, 3, 5])
torch.Size([2, 3, 5])
3
```
size()とshapeはどちらも全く同じ出力を得ることができ,さらにその中の要素にも配列のように参照できる.
注意すべきは参照した値は一番最後の出力のように**Tensor型ではない**という点である.

# 8. ndarrayとTensorの行き来
Tensorが機械学習に優れていることはここまで話してきたが,ndarrayも同様に優れた点がある.
例えば画像処理やグラフの出力等はndarrayの方がいろいろなmoduleを使え,処理しやすい点がある.
なので,ndarrayとTensorを行き来できるようにしておくことが大切である.
以下にそれぞれについて説明をする.

### 8-1. ndarray ⇒ Tensor
以下に例を示す.

```ruby:filename.rb
a = np.random.randn(2,3)
b = torch.from_numpy(a)
print(a)
print(b)

------'''以下出力結果'''--------
[[-1.41878985  0.56327099  1.13779486]
 [ 0.33255026  0.75258005  1.15813658]]
tensor([[-1.4188,  0.5633,  1.1378],
        [ 0.3326,  0.7526,  1.1581]], dtype=torch.float64)
```

ここで重要なことは,**from_numpy()**は引数のndarrayをTensor型に変換しているのだが,**そのdtypeは自動で決定されている**点だ.
この場合の**dtype=torch.float64**は**torch.DoubleTensor**を表す.
今ここでは悪い影響がないように見えるが,機械学習をしていくと**dtype=torch.float32**である**torch.FloatTensor**でないとダメな時がよくある.

そのため,以下のように事前にtypeを指定してやればよい.

```ruby:filename.rb
a = np.random.randn(2,3).astype(np.float32)
b = torch.from_numpy(a)
print(a)
print(b)

------'''以下出力結果'''--------
[[-1.41878985  0.56327099  1.13779486]
 [ 0.33255026  0.75258005  1.15813658]]
tensor([[-1.4188,  0.5633,  1.1378],
        [ 0.3326,  0.7526,  1.1581]])
```

これでdtypeの何も出力がないということは**dtype=torch.float32**である.

またもう1つ注意点として以下のようなことがある.

```ruby:filename.rb
a = np.random.randn(3).astype(np.float32)
print(a)
b = torch.from_numpy(a)
b[0] = -1
print(a)

------'''以下出力結果'''--------
[ 0.19150276 -0.8219755  -0.05888553]
[-1.         -0.8219755  -0.05888553]
```

このように**b**の要素を変更しても**a**の要素が変わってしまう.これは**b**は**a**と同じメモリをシェアしているからである.
解決策としては以下のようにcopyを使う.

```ruby:filename.rb
a = np.random.randn(3).astype(np.float32)
print(a)
b = torch.from_numpy(a.copy())
b[0] = -1
print(a)

------'''以下出力結果'''--------
[-1.3876773   1.7478094  -0.66189164]
[-1.3876773   1.7478094  -0.66189164]
```

### 8-2. Tensor ⇒ ndarray
以下に例を示す.

```ruby:filename.rb
a = torch.tensor(np.array([[1, 2, 3, 4], [5, 6, 7, 8]]))
b = a.numpy()
print(a)
print(b)

------'''以下出力結果'''--------
tensor([[1, 2, 3, 4],
        [5, 6, 7, 8]])
[[1 2 3 4]
 [5 6 7 8]]
```

これも同様に**b**は**a**のメモリをシェアしているため以下のようにすればよい.

```ruby:filename.rb
a = torch.tensor(np.array([1, 2, 3, 4]))
print(a)
b = a.clone().numpy()
b[0] = -1
print(a)

------'''以下出力結果'''--------
tensor([1, 2, 3, 4])
tensor([1, 2, 3, 4])
```

ndarray型と違いTensor型は**clone()**を使えばcopyされる.

ここで注意すべきは,**Tensor型は勾配情報の保持とGPU使用**が可能だったが,**ndarray型はそんなことはできない**という点だ.
以下に例を示す.

```ruby:filename.rb
device = torch.device('cuda:0')
x = torch.ones([2, 2], device=device, requires_grad=True)
x2 = x.clone().numpy()

------'''以下出力結果'''--------
TypeError                                 Traceback (most recent call last)
<ipython-input-84-813722481863> in <module>
      1 device = torch.device('cuda:0')
      2 x = torch.ones([2, 2], device=device, requires_grad=True)
----> 3 x2 = x.clone().numpy()

TypeError: can't convert CUDA tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
```

まず**x**がGPUを使用しているからndarrayにできないとエラーが出る.
GPUからCPUにする方法はわかるから書き換えると以下のようになる.

```ruby:filename.rb
device = torch.device('cuda:0')
x = torch.ones([2, 2], device=device, requires_grad=True)
device2 = torch.device('cpu')
x = x.to(device2)
x2 = x.clone().numpy()

------'''以下出力結果'''--------
RuntimeError                              Traceback (most recent call last)
<ipython-input-85-80f753c52561> in <module>
      3 device2 = torch.device('cpu')
      4 x = x.to(device2)
----> 5 x2 = x.clone().numpy()

RuntimeError: Can't call numpy() on Variable that requires grad. Use var.detach().numpy() instead.
```

今度のエラーは**x**が勾配情報を持つからndarrayにできないと言っている.
この勾配情報を無視してdataを取得する方法を以下に示す.

```ruby:filename.rb
device = torch.device('cuda:0')
x = torch.ones([2, 2], device=device, requires_grad=True)
device2 = torch.device('cpu')
x = x.to(device2)
x2 = x.detach().clone().numpy()
print(x2)

------'''以下出力結果'''--------
[[1. 1.]
 [1. 1.]]
```

この「**detach()**」はTensor型から勾配情報を抜いたものを取得する.
これでndarrayに変換できた.

# 9. ひとこと
今回はpyTorchが用意するTensor型の説明をした.
もちろんここで説明する以上にTensorにはたくさんの機能がある.
だが,ここでの基礎がわかれば他の操作も大体わかるのではないだろうかと思う.
読みづらい点も多かったと思うが読んでいただきありがとうございます.