pytorch上でのL1ノルムの扱いの調査

概要

pytorchのL1ノルムは自動微分に対応している。原点では勾配0、そのほかはその微分値を用いるという実装になっている(と思われる)。 これでは同じステップ幅ならいつまで経っても最適解にたどり着かない。 損失関数として用いる場合でそれが嫌ならばSmooth L1 Loss(torch.nn.SmoothL1Loss())という、原点付近でL2ノルムに変更しているものがあるので、そちらを用いるとよい。

pytorchのL1ノルムの計算

pytorchには自動微分が備わっているが、L1 normの関数にも自動微分が対応している。 torch.abs()が自動微分に対応している。他にもtorch.nn.functional.l1_loss()が対応しているが、下のように中ではtorch.abs()を用いているので、実質同じである。

torch.nn.functional — PyTorch 1.5.0 documentation

ここで、l1ノルムは原点で微分ができないことが問題になる。特に、最小化問題を解くことがほとんどだと思うが、その最小になる点で微分可能でないので、扱いが難しいはずである。特別な理由がなければ使用を避けた方が良いが、どうしても使いたい場合のために、どういう挙動をするのか確認しよう。

確認

不連続点では微分が0になり、その他の点では傾きを1 または -1となっているようにみえる。 三点しか確認してないけどね。 中身はC++で実装されているので、確認したら編集しようと思う。

import torch
device = 'cpu'
a = torch.tensor([-1,0,1], dtype=torch.float64, device=device, requires_grad=True)
b = torch.tensor([0,0,0], dtype=torch.float64, device=device)
loss = torch.nn.functional.l1_loss(a, b, reduction='sum')
loss.backward()
print(a.grad)
>>> tensor([-1.,  0.,  1.], dtype=torch.float64)


a = torch.tensor([-1,0,1], dtype=torch.float64, device=device, requires_grad=True)
b = torch.tensor([0,0,0], dtype=torch.float64, device=device)
loss = torch.abs(a - b).sum()
loss.backward()
print(a.grad)
>>> tensor([-1.,  0.,  1.], dtype=torch.float64)

ここは実は奥が深そうで、 絶対値演算を含む自動微分にはいくつかやり方があるようだ。

https://ipsj.ixsq.nii.ac.jp/ej/?action=repository_action_common_download&item_id=104343&item_no=1&attribute_id=1&file_no=1

また、Smooth L1 Loss(torch.nn.SmoothL1Loss())という、原点付近でL2ノルムに変更しているものもある。原点付近の挙動は許せないが、L1ノルムを使用したい場合はこれを使えばいいだろう。

使うべきか

実際に使用する場面を考えてみる。 0付近でなければ何も問題ないが、0に近づけたいと思うので、どうなんだろうか。 スパースモデリングとかを想定すると、特定の次元を0に持っていくことが大事なのでこれはよくないと思われる。 また、微分時のコストもL2ノルムと変わらないのではないかと思う。 どの位置でも勾配の大きさが同じであってほしいときに使うのであろうか。 他に需要があれば知りたいところである。

max(), min()と絶対値演算

上で紹介したPDFにも書いてあるが、max()、min()は以下のように絶対値演算に変換可能である。 L1ノルムも当然絶対値演算である。なので、絶対値演算が自動微分に対応しているならば(0付近の挙動は置いといて)実行可能なので便利である。

$$ \begin{equation} \begin{aligned} \max (x, y) &=\frac{1}{2}(x+y+|x-y|) \\ \min (x, y) &=\frac{1}{2}(x+y-|x-y|) \end{aligned} \end{equation} $$