ML: Нейронные сети в Torch - Справочник
Интерфейсы для слоёв
Этот документ содержит обзор основных типов слоёв, ошибок и оптимизаторов в библиотеке PyTorch. Основы PyTorch описаны здесь, а основы программирования на ней нейронных сетей - здесь.Слои в PyTorch реализованы в двух формах: как функции и как классы.
Первая - это обычные функции, которым передаются параметры слоя и входящий тензор. Например, линейный слой умножает входную матрицу формы (N, nX) на транспонированную матрицу весов W формы (nY, nX) и опционально добавляет вектор смещений bias. На выходе получается тензор (N, nY):
import torch.nn.functional as F N, nX, nY = 1 , 2 , 3 # число примеров, входов, выходов X = torch.ones(N, nX) # матрица примеров W = torch.ones(nY, nX) # матрица весов B = torch.ones(nY) # вектор смещений Y = F.linear(X, W, B) # [[3., 3., 3.]] Y = X.mm(W.t()) + B # тоже самое Y = torch.addmm(B,X,W.t()) # тоже самое |
Ниже обязательными аргументами при создании класса является число входов nX и число выходов nY слоя (число нейронов в нём == число новых признаков). Затем экземпляру fc класса Linear через оператор ( ) передаётся входная матрица X:
import torch.nn as nn fc = nn.Linear(nX, nY) Y = fc(X) # tensor([[-0.1559, -0.2140, 0.1462]], # grad_fn=<Addmm>) |
Класс слоя у своих параметров объявляет свойство requires_grad=True. Это приводит к построению вычислительного графа при прямом распространении по сети и к получению градиентов от параметров при обратном распространении:
print (fc.bias) # tensor([ 0.529, -0.103, 0.646], requires_grad=True) print (fc.bias.is_leaf) # True print (fc.bias.grad) # None L = torch. sum (Y * Y) # скалярная "функция ошибки" L.backward() # запускаем вычисление градиентов fc.bias.grad # tensor([-0.3688, 0.6441, -1.6415]) |
Дальше идёт небольшой справочник наиболее важных типов слоёв, активационных функций и оптимизаторов. За более подробной информацией стоит обратиться к документации.
Базовые слои
Полносвязный слой
✒ nn.Linear… (in_features, out_features, bias=True) [doc]
Осуществляет линейное преобразование y=x⋅WT+b. Параметры слоя находятся в атрибутах weight и bias. Матрица весов W хранится в транспонированном виде (чтобы при перемножении матриц XW⊤ использовать их строки, а не строку и столбец). Если необходимо задать веса руками, это делается вне вычислительного графа:
fc = nn.Linear(nX, nY) with torch.no_grad(): fc.weight.copy_(W) # устанавливаем матрицу весов fc.bias. copy_(B) # устанавливаем вектор смещений |
Реализация линейного слоя в torch.nn.functional выглядит следующим образом:
def linear(X, weight, bias = None ): if X.dim() = = 2 and bias is not None : # fused op is marginally faster ret = torch.addmm(bias, X, weight.t()) # bias + X @ weight.t() else : output = X.matmul(weight.t()) if bias is not None : output + = bias ret = output return ret |
Пусть in_features=nX, out_features=nY и есть смещение: bias=True.
Входной тензор может иметь произвольное число индексов (многоточие)
и слой так меняет его форму: (N,...,nX) -> (N,...,nY).
Число параметров слоя равно nX*nY + nY.
Эмбеддинг
✒ nn.Embedding… (num_embeddings, embedding_dim, padding_idx=None, max_norm=None, norm_type=2.0,
… scale_grad_by_freq=False, sparse=False, _weight=None)
Слой Embedding преобразует массив целых чисел long на входе в матрицу (массив векторов). Подробнее см. Embedding слов. При создании экземпляра слоя обязательны два первых параметра: размер словаря и размерность эмбеддига. Ниже создаётся слой с 5 словами и их 2-мерными (случайными) векторами:
emb = nn.Embedding( 5 , 2 ) X = torch.tensor([ 0 , 2 , 1 ]) print (emb(X)) print (emb.weight) |
tensor([[ - 1.3062 , - 1.6261 ], [ - 0.3903 , 0.0998 ], [ 0.2643 , 1.6466 ]], grad_fn = <Embedding>) Parameter containing: tensor([[ - 1.3062 , - 1.6261 ], [ 0.2643 , 1.6466 ], [ - 0.3903 , 0.0998 ], [ - 0.5463 , - 0.5028 ], [ 1.3299 , - 0.0580 ]], requires_grad = True ) |
Параметр max_norm задаёт максимальную длину векторов и если при обучении она превышается, вектор перенормируется. В качестве нормы по умолчанию используется сумма квадратов компонент: norm_type = 2.
При параметре scale_grad_by_freq = True градиенты при обучении будут взвешиваться по обратной частоте слов в батче (усиливать изменение более редких слов). Но батч при этом стоит брать достаточно большой. Загрузить готовые векторы можно, создав слой следующим образом:
weight = torch.FloatTensor([[ 1 , 2.3 , 3 ], [ 4 , 5.1 , 6 ]]) emb = nn.Embedding.from_pretrained(weight) # Создать слой с готовыми векторами input = torch.LongTensor([ 1 ]) # Get embeddings for index 1 emb( input ) # tensor([[ 4.0, 5.1, 6.0]]) |
from_pretrained(embeddings, freeze = True , padding_idx = None , max_norm = None , norm_type = 2.0 , scale_grad_by_freq = False , sparse = False ) |
Конволюция
✒ nn.Conv2d… (in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True,
… padding_mode='zeros')
Свёрточный слой Conv2D применяется к "картинкам" высотой rows, шириной cols и имеющих in_channels "цветовых" каналов. На самом деле графические термины условны и слой Conv2D может применяться не только при обработке изображений. Форма входного тензора (N, in_channels, rows, cols). На выходе будет тензор (N, out_channels, rows_out, cols_out) По исходному тензору проходит out_channels фильтров размером kernel_size (число или tuple). Глубина фильтра определяется числом входных каналов in_channels.
Ниже на рисунке in_channels=2, out_channels=3 (три фильтра), размеры фильтров kernel_size = 2 или, что тоже kernel_size = (2,2). Фильтры по умолчанию скользят по картинке с шагом один stride=(1,1), поэтому результирующие ширина и высота картинки на 1 меньше:

Параметр padding (int or tuple) задаёт на сколько "пикселей" необходимо расширить картинку с обоих сторон, перед прохождением по ней фильтрами. Расширенная область заполняется значениями в соответствии с параметром padding_mode: 'zeros', 'reflect', 'replicate', 'circular'. Параметр dilation = 1 задаёт расстояние между пикселями, попадающими в фильтр.
Для примера создадим "картинку" 3x4, левая половина которой белая (1), а правая - чёрная (0) и пройдём по ней фильтром 2x2, выделяющим вертикальный край:
X = torch.zeros( 1 , 1 , 3 , 4 ) X[ 0 , 0 ,:,: 2 ] = 1 print (X) conv = nn.Conv2d( 1 , 1 ,kernel_size = 2 ,bias = False ) print ( conv ) with torch.no_grad(): conv.weight.copy_(torch.tensor([[ - 1. , 1. ], [ - 1. , 1. ]])) print ( conv.weight ) print ( conv.bias ) print ( conv(X) ) |
tensor([[[[ 1. , 1. , 0. , 0. ], [ 1. , 1. , 0. , 0. ], [ 1. , 1. , 0. , 0. ]]]]) Conv2d( 1 , 1 ,kernel_size = ( 2 , 2 ),stride = ( 1 , 1 )) # задаём ядро фильтра tensor([[[[ - 1. , 1. ], [ - 1. , 1. ]]]],requires_grad = True ) None tensor([[[[ 0. , - 2. , 0. ], [ 0. , - 2. , 0. ]]]], grad_fn = <ThnnConv2D>) |
RNN
✒ nn.RNN… (input_size, hidden_size, num_layers=1, nonlinearity='tanh', bias=True,
… batch_first=False, dropout=0, bidirectional=False) [doc]
Базовый рекуррентный слой по входу x(t) и предыдущему скрытому состоянию h(t−1) вычисляется новое скрытое состояние h(t) : h(t)=tanh(x(t)Wih+bih+h(t−1)Whh+bhh)

Обязательные параметры: input_size - размерность входов (x) и hidden_size - размерность выхода ( = скрытого состояния h).
По умолчанию размерность входа (seq_len, batch_size, input_size), где seq_len - число RNN-ячеек (входов). Индекс батча можно сделать первым при помощи параметра batch_first. Впрочем тензор по умолчанию получается, если перед слоем RNN находится слой Embedding c seq_len входами и с input_size = VEC_DIM размерностью векторов.
Слой возвращает кортеж из тензора всех выходов сети формы (seq_len, batch_size, hidden_size) и последнего скрытого состояния (последний выход) формы (num_layers, batch_size, hidden_size), где для одного слоя num_layers = 1. По умолчанию компоненты начального скрытого состояние нулевые. Иначе их можно передать вторым аргументом:
X_dim = 2 # размерность входов = dim(x) H_dim = 4 # размерность скрытого состояния = dim(h) L = 3 # число входов (ячеек RNN слоя) B = 1 # число примеров (batch_size) rnn = nn.RNN(X_dim, H_dim) X = torch.zeros(L, B, X_dim) H0 = torch.zeros( 1 , B, H_dim) Y, Hn = rnn(X, H0) # H0 не обязательно, по умолчанию и так 0 print ( tuple (Y.shape), tuple (Hn.shape)) # (3, 1, 4) (1, 1, 4) print (Y[ - 1 ] = = Hn[ - 1 ]) # tensor([[[True, True, True, True]]]) |
В слое можно сразу задать стопку RNN-слоёв (num_layers) Между слоями можно вставить режим случайного забивания нулями выходов с вероятностью dropout. Слой можно также превратить в двунаправленный: bidirectional = True. В этом случае размерность выхода будет (inputs, batch_size, 2*hidden_size), а размерность начального скрытого состояния (2 * num_layers, batch_size, hidden_size).
LSTM
✒ torch.nn.LSTM… (input_size, hidden_size, num_layers=1, nonlinearity='tanh', bias=True,
… batch_first=False, dropout=0, bidirectional=False) [doc]
Аналогичен RNN. Подробности см. в NN_RNN.html. Кроме скрытого состояния h имеет память ячеек c, см. документацию.
rnn = nn.LSTM( 10 , 20 , 2 ) ... output, (hn, cn) = rnn( input , (h0, c0)) |

MultiheadAttention
✒ nn.MultiheadAttention… (embed_dim, num_heads, dropout=0.0, bias=True, add_bias_kv=False,
… add_zero_attn=False, kdim=None, vdim=None)
MultiHead(Q,K,V)=Concat(h1,…,hH)WO, hi=Attn(QWQi, KWKi, VWVi)
Inputs: Q:(L,N,E), K,V:(S,N,E),
где
L - длина последовательности target (для декодера),
N - размер батча,
E - размерность эмбединга (embed_dim),
S - длина последовательности source.
Outputs: output: (L,N,E), weights: (N,L,S)
Размерности матриц: WQ: (E, E),
WK: (E, kdim),
WV: (E, vdim),
где kdim,vdim задаются или равны E,
но они передаются в функцию linear, поэтому при умножении транспонируются.
Размерность каждой головы равны head_dim = E // num_heads.
Выходная матрица WO: это слой Linear(E, E, bias=bias).
Если bias есть, он добавляется после умножения
на проекционные матрицы WQ,WK,WV (к каждой свой).
Если эмбединг входов одинаковый, то проекционные матрицы упакованы в in_proj_weight формы (3*E,E), иначе это параметры: q_proj_weight, k_proj_weight, v_proj_weight. Смещение (если он есть) это in_proj_bias формы (3*E,), а выходной слой: out_proj.
Концептуально происходят следующие вычисления (N - размер батча, подробности см. в NN_Attention.html):
q = linear(Q, q_proj_weight, in_proj_bias[ 0 : E]) # (L,N,E) @ (E, E).T = (L,N,E) k = linear(K, k_proj_weight, in_proj_bias[E : E * 2 ]) # (S,N,E) @ (E, kdim).T = (S,N,kdim) v = linear(V, v_proj_weight, in_proj_bias[E * 2 :]) # (S,N,E) @ (E, vdim).T = (S,N,vdim) q = q.contiguous().view( L, N * num_heads, head_dim).transpose( 0 , 1 ) k = k.contiguous().view( - 1 , N * num_heads, head_dim).transpose( 0 , 1 ) v = v.contiguous().view( - 1 , N * num_heads, head_dim).transpose( 0 , 1 ) a_weights = torch.bmm(q, k.transpose( 1 , 2 )) a_weights = softmax(a_weights, dim = - 1 ) a = torch.bmm(a_weights, v) a = a.transpose( 0 , 1 ).contiguous().view(L, N, E) a = linear(a, out_proj_weight, out_proj_bias) |

TransformerEncoderLayer
✒ nn.TransformerEncoderLayer… (d_model, nhead, dim_feedforward=2048, dropout=0.1, activation='relu')
Параметры: d_model – размерность входа (вектора одного токена), nhead – число голов (делитель параметра d_model), dim_feedforward – размерность полносвязной сети.
TransformerEncoder
✒ nn.TransformerEncoder… (encoder_layer, num_layers, norm=None)
L, N, E = 10 , 32 , 512 # число слов, размер батча, размерность эмбединга encoder_layer = nn.TransformerEncoderLayer(d_model = E, nhead = 8 ) transformer_encoder = nn.TransformerEncoder (encoder_layer, num_layers = 6 ) src = torch.rand(L, N, E) out = transformer_encoder(src) # out.shape == src.shape |
Cлои без параметров
Лиаринизация тензора
✒ nn.Flatten(start_dim=1, end_dim=-1)При создании с параметрами по умолчанию, все индексы кроме первого (индекс примера) сольются в один (для каждого примера получится вектор). Слой параметров не имеет.
X = torch.ones( 2 , 5 , 4 , 3 ) Y = torch.nn.Flatten()(X) print ( tuple (Y.shape) ) # (2, 60) |
Косинус между векторами
✒ nn.CosineSimilarity(dim=1, eps=1e-08)uvmax
Input1, Input2: (*, D, *), где D - размерность вектора и формы одинаковые. Output: (*,*).Ниже косинус вычисляется между строчками матриц:
u, v = torch.randn( 100 , 128 ), torch.randn( 100 , 128 ) output = nn.CosineSimilarity(dim = 1 , eps = 1e - 6 ) (u, v) |
Расстояние между векторами
✒ nn.PairwiseDistance(p=2.0, eps=1e-06, keepdim=False)\bigr(\sum_{i} |x_i|^p \Bigr)^{1/p}
Input1, Input2: (N, D), где D - размерность вектора. Output: (N) или (N, 1), если keepdim = True.Отключение нейронов
✒ nn.Dropout(p=0.5, inplace=False)
В режиме тренировке: m.train(), с вероятностью "p" зануляет каждый элемент
матрицы и делит их на 1-p.
В режиме оценивания: m.eval() ничего не делает.
Благодаря делению на 1-p среднее значение по модулю элементов в режиме тренировки будет
такой-же, как и врежиме оценивания (у входного тензора).
m = nn.Dropout(p = 0.2 ) # v v print ( m(torch.ones( 10 )) ) # [1.25, 1.25, 1.25, 0.0, 1.25, 1.25, 1.25, 0.0, 1.25, 1.25] x = torch.randn( 100 , 100 ) m. eval () # This is equivalent with self.train(False) y = m(x) print ( (x = = y). float ().mean() ) # не меняется m.train() y = m(x) print (x. abs ().mean(), (x * x < 1e - 10 ). float ().mean() ) # 0.8025 0. print (y. abs ().mean(), (y * y < 1e - 10 ). float ().mean() ) # 0.8001 0.206 |
Нормализация тензора
✒ nn.BatchNorm1d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)По батчу (перый индекс) вычисляются средние значения и дисперсии по каждому признаку независимо. Затем делается нормализаци данных и афинный сдвиг с обучаемыми параметрами \gamma,\,\beta. y = \frac{x - \mathrm{E}[x]}{\sqrt{\mathrm{Var}[x] + \epsilon}} \odot \text{weight} + \text{bias} Вход должен быть тензором формы (N,F) или (N,F,L). Параметр num_features = F. Для тензора (N,F,L) усреднение происходит для dim=0,2, т.е. одинаковые средние по каждому "входу" последнего индекса [0...L-1].
В процессе тренировки средние значения сглаживаются скользящим средним с параметром momentum (если None - то обычное среднее). Эти средние запоминаются и затем используются при тестировании (даже с одним батчем).
m = nn.BatchNorm1d( 2 ) x = torch.randn( 100000 , 2 ) print ( x.mean(dim = 0 ), x.var(dim = 0 ) ) # [-0.0005, 0.0067] [1.0058, 0.9956] x = 2 * x + 3 print ( x.mean(dim = 0 ), x.var(dim = 0 ) ) # [ 3.0010, 2.9867] [4.0231, 3.9824] y = m(x) with torch.no_grad(): print (y.mean(dim = 0 ), y.var(dim = 0 )) # [ 0.0000, 0.0000] [1.0000, 1.0000] for k, v in m.state_dict().items(): # weight :(2,) print (f '{k:20s}: {tuple(v.shape)}' ) # bias :(2,) # running_mean :(2,) # running_var :(2,) # num_batches_tracked:( ) |
MaxPool2d
✒ nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
AvgPool2d
✒ nn.AvgPool2d(kernel_size, stride=None, padding=0, ceil_mode=False, count_include_pad=True, divisor_override=None)
Активационные функции
Сигмоид: \mathrm{Sigmoid}(x) = 1/(1+e^{-x})
✒ nn.Sigmoid() [doc]
✒ F.sigmoid(x)
Tanh: \mathrm{Tanh}(x) = (e^{x}-e^{-x})/(e^{x}+e^{-x})
✒ nn.Tanh() [doc]
✒ F.tanh(x)
ReLU: \mathrm{Sigmoid}(x) = \max(0,x)
✒ nn.ReLU(inplace=False) [doc]
✒ F.relu(x, inplace=False)
ELU: \mathrm{ELU}(x) = \max(0,x) + \min(0, \alpha\,(e^{x}-1))
✒ nn.ELU(alpha=1.0, inplace=False) [doc]
✒ nn.elu(x, alpha=1.0, inplace=False)
Софтмакс: \mathrm{Softmax}(x_i) = e^{x_i}/\sum_j e^{x_j}
✒ nn.Softmax(dim=None) [doc].
✒ F.softmax(x, dim)
X = torch.tensor( [ [ 1. , 2. , 3 ] ] ) torch.nn.Softmax(dim = 1 )(X) # tensor([[0.09, 0.24, 0.66]]) |
Логарифмический софтмакс: \mathrm{LogSoftmax}(x_i) = \log \bigr(e^{x_i}/\sum_j e^{x_j}\bigr)
✒ nn.LogSoftmax (dim=None) [doc].
Функции ошибки
Среднеквадратичная ошибка
✒ nn.MSELoss
… (size_average=None, reduce=None, reduction='mean')
[doc]
L(y,\hat{y}) = (y - \hat{y})^2
\left\{
\begin{array}{ll}
.\mathrm{mean}() & \text{if reduction='mean'}\\
.\mathrm{sum}() & \text{if reduction='sum'}
\end{array}
\right.
По умолчанию (reduction='mean') квадраты отклонений усредняются по всем примерам батча и по всем выходам (ошибка это скаляр!).
loss = torch.nn.MSELoss() y = torch.rand( 10 , 2 ) # 10 примеров, 2 выхода yb = torch.rand( 10 , 2 ) loss(y, yb) = = ((y - yb) * * 2 ).mean() |
Бинарная кросс-энтропия
✒ nn.BCELoss… (weight=None, size_average=None, reduce=None, reduction='mean') [doc]
Предполагается, что эта ошибка ставится после сигмоидной активации, т.е. выходы модели y и целевые значения yb находятся в диапазоне [0...1]. Обычно BCELoss используется для сети с одним выходом (задача для двух классов 0 и 1). Если классы пересекаются (т.е. пример может относится сразу к нескольким классам), то число выходов равно числу классов. По умолчанию (reduction='mean') ошибки усредняются по всем примерам и всем выходам:
loss = torch.nn.BCELoss() y = torch.rand( 10 , 2 , 3 ) # 10 примеров, 2x3 выходов yb = torch.rand( 10 , 2 , 3 ) print ( loss(y, yb) ) print ( - ( yb * y.log() + ( 1 - yb) * ( 1 - y).log() ).mean() ) |
Кросс-энтропия
✒ nn.CrossEntropyLoss… (weight=None,size_average=None,ignore_index=-100,reduce=None,reduction='mean') [doc]
Применяется к задаче классификации не пересекающихся C классов. Сеть должна иметь C выходов, т.е. в ошибку из модели входит тензор y_{i\alpha} формы (N,C), где N - размер батча. Целевыми являются целые числа \hat{y}_i \in [0...C-1] - номера классов каждого примера формы (N,). Ошибка для одного i-того примера и \hat{y}_i=c равна:
L(y, c) = -\,w_c\,\log\left( \frac{\exp {y_{ic}}}{ \sum_\alpha \exp{y_{i\alpha}}}\right). Веса w_\alpha (по числу классов) могут усиливать вклад отдельных классов. Таким образом, CrossEntropyLoss совмещает в себе Softmax и на выходе сети его ставить не нужно. C выходов сети y_{i\alpha} превращаются в "вероятности" и максимизируется логарифм вероятности правильного класса \hat{y}_i=c (знак минус - ошибка минимальна). Итоговая ошибка равна среднему L(y, c) по всем примерам батча (если reduction='mean').Входные данные в функцию ошибки CE_loss(y, y_t)могут иметь форму y=(B,C):float, y_t=(B,):long или в n-мерном случае: y=(B,C,L1,...,Ln):float, y_t=(B,L1,...,Ln):long
B, C, L = 10 , 5 , 4 # примеров, классов, выходов w = torch.rand(C) # веса важности классов CE_loss = nn.CrossEntropyLoss(weight = w, ignore_index = 1 ) y = torch.randn(B, C, L) yb = torch.empty(B, L, dtype = torch. long ).random_(C) loss = CE_loss(y, yb) print (loss) # ошибка |
y = - nn.Softmax(dim = 1 )(y).log() # минус логарифма от софтмакса loss, norm = 0 , 0 for b in range (B): for l in range (L): c = yb[b,l] # правильный класс if c ! = 1 : # игнорируем класс 1 loss + = w[c] * y[b, c, l] # ошибка norm + = w[c] # нормировка print (loss / norm) |
Логарифмический максимум правдоподобности
✒ nn.NLLLoss… (weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean') [doc]
Результат будет таким же как и при CrossEntropyLoss, если последним слоем стоит LogSoftmax.
Оптимизаторы
torch.optim.Adam(params, lr = 0.001 , betas = ( 0.9 , 0.999 ), eps = 1e - 08 , weight_decay = 0 , amsgrad = False ) |
torch.optim.Adagrad(params, lr = 0.01 , lr_decay = 0 , weight_decay = 0 , initial_accumulator_value = 0 , eps = 1e - 10 ) |
torch.optim.Adadelta(params, lr = 1.0 , rho = 0.9 , eps = 1e - 06 , weight_decay = 0 ) |
torch.optim.RMSprop(params, lr = 0.01 , alpha = 0.99 , weight_decay = 0 , momentum = 0 , eps = 1e - 08 , centered = False ) |
torch.optim.Rprop(params, lr = 0.01 , etas = ( 0.5 , 1.2 ), step_sizes = ( 1e - 06 , 50 )) |
torch.optim.SGD(params, lr, momentum = 0 , dampening = 0 , weight_decay = 0 , nesterov = False ) |
