Това е петата статия от поредицата The Implemented Transformer. Нормализирането на слоя директно следва механизма за внимание с множество глави и мрежата за подаване напред по позиция от предишните статии.

Заден план

Като цяло нормализацията е процесът на трансформиране на характеристиките, за да бъдат в сравним мащаб. Има многобройни начини за нормализиране на функции, включително стандартен резултат и минимално-максимално мащабиране на характеристиките.

Минимално-максимално мащабиране на функции

Минимално-максималното мащабиране на функции трансформира стойностите в диапазона [0,1]. Това също е известно като нормализация, базирана на единство. Може да се изчисли със следното уравнение:

Горната част на уравнението измества всяка стойност с X_min; числителят става 0, когато X = X_min. Когато това се раздели на знаменателя, резултатът е 0.

По същия начин новият максимум възниква, когато числителят е X_max — X_min. Когато тази стойност се раздели на X_max — X_min, тя става 1. Ето как диапазонът се измества между 0 и 1.

Примерът по-долу демонстрира как се изчисляват стойностите стъпка по стъпка.

import torch
X = torch.Tensor([22, 5, 6, 8, 10, 19,2])
X_max = X.max() # 22
X_min = X.min() # 2

# [22-2, 5-2, 6-2, 8-2, 10-2, 19-2, 2-2] = 
numerator = X-X_min # [20, 3, 4, 6, 8, 17, 0]

denominator = X_max-X_min # 22 - 2 = 20


# [20/20, 3/20, 4/20, 6/20, 8/20, 17/20, 0/20]
X_new = numerator/denominator
tensor([1.00, 0.15, 0.20, 0.30, 0.40, 0.85, 0.00])

Стандартен резултат

По време на стандартизацията всяка стойност се преобразува в своя стандартен резултат. Стандартният резултат е известен още като z-резултат. Това се прави чрез изваждане на средната стойност от всяка стойност и разделяне на стандартното отклонение.

μпредставлява средната стойност на данните. Изчислява се чрез сумиране на всички точки от данни в набор от данни и разделяне на броя точки от данни, n:

σ представлява стандартното отклонение на данните, което е средната дисперсия на стойностите от средната стойност. Ако набор от данни има ниско стандартно отклонение, стойностите вероятно са по-близо до средната стойност. Ако има високо стандартно отклонение, това вероятно означава, че стойностите са разпределени в по-голям диапазон. Може да се изчисли по следната формула.

Първата стъпка е да се намери отклонението на всяка точка от средната стойност. Това се прави чрез просто изваждане на средната стойност от всяка точка. След това тези стойности могат да бъдат повдигнати на квадрат, за да се премахнат всички отрицателни стойности. Накрая те могат да бъдат сумирани и разделени на броя на стойностите, $n$. След това стандартното отклонение може да се изчисли чрез изваждане на корен квадратен. Ако не се вземе квадратен корен, σ² е дисперсията.

Примерът по-долу показва тези стъпки.

import torch
X = torch.Tensor([22, 5, 6, 8, 10, 19,2])
n = len(X)

mean = X.sum()/n # X.mean()

std = (((X-mean)**2).sum()/n).sqrt() # X.std(unbiased=False)

z_scores = (X - mean)/std

print(mean, std, z_scores, sep="\n")
tensor(10.2857)
tensor(6.9016)
tensor([ 1.6973, -0.7659, -0.6210, -0.3312, -0.0414,  1.2626, -1.2005])

Както може да се види, стойностите са разпределени около 0. Това се очаква, тъй като средната стойност се изважда от всяка стойност. Сега стандартните резултати предават подобна информация, без да са необходими големи стойности. Тъй като средната стойност е 10,2857, лесно е да се види, че стандартният резултат от 10 е малко под 0 при -0,0414.

Защо Нормализация?

Нормализацията се използва в машинното обучение, тъй като моделите с функции в различни мащаби отнемат повече време за обучение; това се случва, защото градиентното спускане ще се нуждае от повече време за сближаване.

Според Pinecone, липсата на нормализиране може да доведе до големи градиенти на грешки, които в крайна сметка експлодират, което прави модела нестабилен.

Следователно в много случаи данните трябва да бъдат нормализирани, преди да бъдат вмъкнати в модел.

Нормализация на слоя

Според Pinecone, нормализирането на слоя гарантира, че „всички неврони в определен слой ефективно имат еднакво разпределение във всички функции за даден вход“.

Нормализацията се извършва на последните размериD; D е броят на измеренията, които ще бъдат нормализирани. Например, ако целта е да се нормализира едномерен вектор с 10 елемента, D ще бъде 1. Ако целта е да се нормализира матрица с форма на (2,3), D ще бъде 2. По същия начин, ако целта е да се нормализира тензор с формата на (2,5,3), D ще бъде 3.

Уравнението за нормализиране

За всеки вход, отбелязан x, нормализирането на слоя може да се изчисли с помощта на модифицирано уравнение за z-резултат:

  • μпредставлява средната стойност на последните Dизмерения
  • σ² представлява дисперсията на последните D измерения
  • εе изключително малка стойност, която помага, когато σ² е малък
  • γи βса параметри, които могат да се научат.

Според Pinecone, γи βсе използват, защото „принуждавайки всички предварителни активации да бъдат нула и единица стандартно отклонение... може да бъде твърде ограничително. Може да се окаже, че променливите разпределения са необходими на мрежата, за да научи по-добре определени класове.

  • Те имат същата форма като дадения тензор, който трябва да се нормализира.
  • γ се инициализира като единици, а β се инициализира като нули.

Общ пример

За да се демонстрира как се изчислява нормализирането на слоя, тензор с форма (4,5,3) ще бъде нормализиран в своите матрици, които имат размер (5,3). Това означава, че D е 2.

На изображението по-горе е ясно, че стойностите на всяка матрица са стандартизирани въз основа на другите стойности в рамките на същата матрица.

Нормализирането на слоя може да се реализира с помощта на статистическите възможности на PyTorch.

# Input Tensor: 4 matrices of 5 rows and 3 columns
X = torch.randint(0, 100, (4, 5, 3)).float()

# Shape to be Normalized: 5 rows, 3 columns
normalized_shape = (5, 3)

# Number of Dimensions in the Shape to be Normalized
D = len(normalized_shape)

# Set the Default Values for Epsilon, Gamma, and Beta
eps = 1e-5
gamma = torch.ones(normalized_shape)
beta = torch.zeros(normalized_shape)

X
tensor([[[76.,  2., 43.],
         [79., 50., 29.],
         [59., 78., 73.],
         [95., 94., 76.],
         [ 9., 74., 64.]],

        [[76., 87., 50.],
         [ 2., 65., 44.],
         [74.,  9., 82.],
         [83., 54., 82.],
         [ 6., 97., 52.]],

        [[88., 19., 95.],
         [14., 96., 96.],
         [93., 58.,  0.],
         [19., 37.,  6.],
         [28., 23.,  7.]],

        [[ 7., 54., 59.],
         [57., 30., 18.],
         [88., 89., 63.],
         [56., 75., 56.],
         [63., 23., 73.]]])

Всяка от тези матрици може да бъде стандартизирана с помощта на цикъл. В този цикъл се изчисляват средната стойност и дисперсията. След това тези стойности се вмъкват в уравнението за нормализиране на слоя, за да се изчислят нормализираните стойности за матрицата.

# Normalize
for i in range(0,4):               # loop through each matrix
  mean = X[i].mean()               # mean         
  var = X[i].var(unbiased=False)   # variance
  layer_norm = (X[i]-mean)/(torch.sqrt(var+eps))*gamma + beta 

  print(f"μ = {mean:.4f}")            
  print(f"σ^{2} = {var:.4f}") 
  print(layer_norm)
  print("="*50)
μ = 60.0667
σ^2 = 751.6622
tensor([[ 0.5812, -2.1179, -0.6225],
        [ 0.6906, -0.3672, -1.1331],
        [-0.0389,  0.6541,  0.4717],
        [ 1.2742,  1.2377,  0.5812],
        [-1.8626,  0.5082,  0.1435]])
==================================================
μ = 57.5333
σ^2 = 887.8489
tensor([[ 0.6198,  0.9889, -0.2528],
        [-1.8637,  0.2506, -0.4542],
        [ 0.5526, -1.6288,  0.8211],
        [ 0.8547, -0.1186,  0.8211],
        [-1.7295,  1.3245, -0.1857]])
==================================================
μ = 45.2667
σ^2 = 1344.1956
tensor([[ 1.1656, -0.7164,  1.3565],
        [-0.8528,  1.3838,  1.3838],
        [ 1.3019,  0.3473, -1.2347],
        [-0.7164, -0.2255, -1.0710],
        [-0.4710, -0.6073, -1.0437]])
==================================================
μ = 54.0667
σ^2 = 561.9289
tensor([[-1.9855, -0.0028,  0.2081],
        [ 0.1237, -1.0153, -1.5215],
        [ 1.4315,  1.4737,  0.3769],
        [ 0.0816,  0.8831,  0.0816],
        [ 0.3769, -1.3106,  0.7987]])
==================================================

Чрез изследване на изображението по-горе и изхода на кода е очевидно, че нормализирането на слоя изчислява вариация на z-резултата за стойностите във всяка матрица в този пример.

В първата матрица средната стойност е около 60. Следователно е разумно 59 да има z-резултат от -0,0389. По същия начин 2, стойността, която е най-отдалечена от средната, има z-резултат от -2,1179.

Същият отговор като по-горе може да бъде изчислен с помощта на модула LayerNorm на PyTorch. Този модул изисква формата, която трябва да се нормализира, да бъде инициализирана. След това тензорът може да бъде предаден на модула и всяка матрица ще бъде съответно нормализирана.

layer_normalization = nn.LayerNorm(normalized_shape) # nn.LayerNorm((5,3))
layer_normalization(X)
tensor([[[ 0.5812, -2.1179, -0.6225],
         [ 0.6906, -0.3672, -1.1331],
         [-0.0389,  0.6541,  0.4717],
         [ 1.2742,  1.2377,  0.5812],
         [-1.8626,  0.5082,  0.1435]],

        [[ 0.6198,  0.9889, -0.2528],
         [-1.8637,  0.2506, -0.4542],
         [ 0.5526, -1.6288,  0.8211],
         [ 0.8547, -0.1186,  0.8211],
         [-1.7295,  1.3245, -0.1857]],

        [[ 1.1656, -0.7164,  1.3565],
         [-0.8528,  1.3838,  1.3838],
         [ 1.3019,  0.3473, -1.2347],
         [-0.7164, -0.2255, -1.0710],
         [-0.4710, -0.6073, -1.0437]],

        [[-1.9855, -0.0028,  0.2081],
         [ 0.1237, -1.0153, -1.5215],
         [ 1.4315,  1.4737,  0.3769],
         [ 0.0816,  0.8831,  0.0816],
         [ 0.3769, -1.3106,  0.7987]]], 
         grad_fn=<NativeLayerNormBackward0>)

Пример за НЛП

При обработката на естествения език нормализацията на слоя се извършва в измеренията на вграждане на всеки токен. За партида с 2 последователности, 3 токена и вграждания от 5 елемента, формата е (2, 3, 5). D ще бъде 1, тъй като последното измерение ще бъде нормализирано. Формата на вграждането ще бъде (5,). Трябва да се инициализира в кортеж, за да се гарантира, че стойността му може да бъде извлечена от модула LayerNorm. Като алтернатива може да се използва X.shape[-1].

# Input Tensor: 2 sequences of 3 tokens with 5 dimensional embeddings
X = torch.randint(2, 3, 5)

# Shape to be Normalized: 5 dimensional embedding
normalized_shape = (5,)

# Number of Dimensions in the Shape to be Normalized
D = len(normalized_shape) # 1

# Create the LayerNorm 
layer_normalization = nn.LayerNorm(normalized_shape)

# view the beta and gamma and beta
layer_normalization.state_dict()
OrderedDict([('weight', tensor([1., 1., 1., 1., 1.])),
             ('bias',   tensor([0., 0., 0., 0., 0.]))])

По-горе са стойностите на γи β. По-долу може да се види, че стойностите на Xса инициализирани като цели числа, за да се демонстрира лесно как нормализирането на слоя им влияе.

X
tensor([[[49., 90., 29., 76., 33.],
         [86., 42., 20., 56., 79.],
         [40., 49., 72., 16., 85.]],

        [[44., 62., 14., 46.,  5.],
         [22., 45.,  8., 47., 78.],
         [96., 17.,  7., 56., 60.]]])

Средната стойност на всеки ред може също да бъде изчислена с помощта на PyTorch вместо for-цикъл:

X.mean(2, keepdims=True) # maintains the dimensions of X
tensor([[[55.4000],
         [56.6000],
         [52.4000]],

        [[34.2000],
         [40.0000],
         [47.2000]]])

Тези стойности могат да бъдат използвани след прилагане на нормализацията, за да се види колко далеч е всяка стойност от средната стойност.

layer_normalization(X)
tensor([[[-0.2675,  1.4464, -1.1036,  0.8611, -0.9364],
         [ 1.2167, -0.6042, -1.5147, -0.0248,  0.9270],
         [-0.5116, -0.1403,  0.8087, -1.5018,  1.3450]],

        [[ 0.4601,  1.3051, -0.9483,  0.5539, -1.3708],
         [-0.7518,  0.2088, -1.3366,  0.2924,  1.5872],
         [ 1.5204, -0.9409, -1.2525,  0.2742,  0.3988]]],
       grad_fn=<NativeLayerNormBackward0>)

За първия токен 90 има най-големия z-резултат при 1,4464, тъй като е най-отдалеченият от средната стойност от 55,4, която би имала z-резултат 0.

След като нормализирането на слоя е обяснено от гледна точка на обработка на общ и естествен език, е време да покажем как се използва в трансформаторите.

Нормализация на слоя в трансформатори

Въпреки че PyTorch има вграден модул LayerNorm, той може да бъде пресъздаден за по-добро разбиране на използването му в модела на трансформаторите. Реализацията е сравнително ясна и следва кода от обяснението.

class LayerNorm(nn.Module):

  def __init__(self, features, eps=1e-5):
    super().__init__()
    # initialize gamma to be all ones
    self.gamma = nn.Parameter(torch.ones(features)) 
    # initialize beta to be all zeros
    self.beta = nn.Parameter(torch.zeros(features)) 
    # initialize epsilon
    self.eps = eps

  def forward(self, src):
    # mean of the token embeddings
    mean = src.mean(-1, keepdim=True)        
    # variance of the token embeddings         
    var = src.var(-1, keepdim=True,unbiased=False)  
    # return the normalized value  
    return self.gamma * (src - mean) / torch.sqrt(var + self.eps) + self.beta 

Нормализирането на слоя ще бъде използвано в Encoder, така че използването му ще бъде демонстрирано с остатъчно добавяне в следващата статия, The Encoder. Засега е важно прилагането му да бъде разбрано.

Моля, не забравяйте да харесате и последвате за още! :)

Препратки

  1. „Нормализиране на партида и слой на Pinecone“
  2. „Модулът LayerNorm на PyTorch“
  3. „Статия за нормализиране на Wikipedia“