Слой Soft One-Hot Encoding (SOHE) - это основа
SoftOrdering-слоя. Почему я его выделяю отдельно?
Он почему-то улучшает обучение нейросети. Я сам пока не понимаю, это результат экспериментов.
Слой SOHE вставляем между линейными слоями как слой активационной функции. Слой SOHE имеет один гиперпараметр - количество бинов (n_bins).
Выход линейного слоя разбиваем на бины, и если значение попадает в бин, то на выходе единица, иначе ноль. Только реализована мягкая версия такого алгоритма, поэтому слой называется именно Soft One-Hot Encoding. На выход сигнал подаётся через flatten-функцию, итого входной вектор размера n_inputs превращается в вектор размера n_inputs
n_bins.
Для последующих слоёв такой сигнал более элементарен и прост для анализа нежели неэнкодированный, такое преобразование способно заменить пару полноценных (Linear-Sigmoid) слоёв, это единственное моё объяснение.
1. Слой SoftOrdering отличается лишь добавлением после слоя SOHE функции mean(). Ниже приведён код, там функция mean() закомментирована, поэтому SoftOrdering превратился в SOHE.
2. Слой RecurrentSoftOrdering зацикливает SOHE, суммируя (накапливая) результат на каждой итерации SOHE. Практика показывает, что чисто рекурсия SOHE (без суммирования) работает хуже чем рекурсия SoftOrdering (который с суммированием). Код RecurrentSoftOrdering не привожу, там всё просто.
3.Разница между SOHE, SoftOrdering, RecurrentSoftOrdering настолько невелика, что склоняюсь к тому, чтобы оставить только один SOHE. (Код ниже, видим SoftOrdering, но имеем в виду SOHE.)
import torch
import torch.nn.functional as F
class SoftOrdering(nn.Module): #Слой мягкого упорядочивания
n_bins: int
n_inputs: int
gcoef: float
gain: float
lspace: np.array
def __init__(self, n_bins: int, n_inputs: int, gcoef: float = 6.0) -> None:
super().__init__()
__constants__ = ["n_bins", "n_inputs", "gcoef"]
self.n_bins = n_bins
self.n_inputs = n_inputs
self.gcoef = gcoef
self.lspace = torch.linspace(-0.1 - 1/(n_bins - 1), 1.1 + 1/(n_bins - 1), steps=n_bins + 1)
self.gain = gcoef * (n_bins - 1)
self.sigmoid = nn.Sigmoid()
def extra_repr(self) -> str:
return f"n_bins={self.n_bins}, n_outputs={self.n_inputs}, coef={self.gcoef}"
def forward(self, x: torch.Tensor) -> torch.Tensor: # размерность x (batch, channel, input), здесь и далее batch и channel необязательны
z = x.unsqueeze(-2)
lenzsize = len(z.size())
zsize = np.ones(lenzsize)
zsize[-2] = len(self.lspace)
z = z.repeat(tuple(zsize.astype('int'))) #размерность z (batch, channel, bin, input)
lsize = np.array(z.size())
lsize[-2] = 1
l = self.lspace.unsqueeze(1).repeat(tuple(lsize.astype('int'))) #размерность l (batch, channel, bin, input)
z = z - l
z = self.sigmoid(self.gain * z)
zslice = [slice(None, None),] * (lenzsize - 2) + [slice(None, -1)]
z = z[zslice] - z.roll(-1, dims=(-2))[zslice]
p = z.flatten(-2, -1)
#p = z.mean(1)
return p # размерность p (batch, channel, bin * input)
Объявление слоя в __init__()
self.ordering = SoftOrdering(n_bins1, n_inputs1, gcoef=2.0)
Использование слоя в forward()
z = self.linear1(z)
z = self.sigmoid1(z) # я здесь нужен, чтобы сигналы были в пределах 0...1
z = self.ordering(z) # ку-ку, это я
z = self.linear2(z)