LayerNorm addresses some short-comings from BatchNorm, which are:

First of all, assume that we are conducting BatchNorm across some input of the language. Then, we are essentially normalizing across the batch for a specific feature vector, which is problematic. For example, assume these two sentences are in a batch: 1. The cat meows. 2. The barking dog looks at the cat.
We are essentially calculating a statistic that takes the same parts of the sentences and trying to normalize these them across the batch. So in this case, the representation of "cat" depends on "barking" as they are in the same position of the sentence, which is not what we want as we want each representation of the word to be embedded in the context of its own sentance.
Another issue that we can see is that sentances have different lengths. Representing two sentences with different lengths are problematic as we will have to pad some spaces, which will affect the statistics for words in longer sentances. In the case of our example, there simply would be no statistics to calculate for "looks at the cat".
Therefore, it makes sense to use LayerNorm, where we calculate the statistics across the feature instead of the batches. For our first sentence, "cat" will have its own representation and be normalized within its feature space. So assuming that the features space is of size , we will calculate some and for the word "cat" and normalize that feature vector instead.
This way, it is possible to get representations of the words in context with its sentence and order, and also reducing internal covariance shifts.
I won't mention too much about the internal covariance shifts as it is covered in detail in my BatchNorm post.
class LayerNorm(nn.Module):
def __init__(self, eps=1e-5):
super().__init__()
self.eps = eps
def forward(self, x):
batch_size, length, feature_dim = x.shape
output = torch.zeros_like(x)
for i in range(batch_size):
for t in range(length):
word_vector = x[i, t, :]
mu = word_vector.sum() / feature_dim
var = ((word_vector - mu) ** 2).sum() / feature_dim
output[i, t, :] = (word_vector - mu) / torch.sqrt(var + self.eps)
return output
# Assume feature vecs from a batch 32, sentence of length 64, and feature dim. of 512.
# We want to normalize across each word in a batch.
# randn() -> between 0 and 1 so now mean = 10 and var = 5
sample_batch = torch.randn(32, 64, 512) * 5 + 10
print(sample_batch[0, 0, :])
ln_layer = LayerNorm()
out = ln_layer.forward(sample_batch)
print(f"dim(out): {out.shape}") # >>> (32, 64, 512)
print(out[0, 0, :])
sample_batch[0, 0, :], we can see the shift in
distribution. Recall that torch.rand generates a value in
between 0 and 1 and we scaled the sample batch to a Gaussian
distribution.

torch.mean(*kwargs, keep_dim=True).
If you could recall, keep_dim=True preserves the
dimensions when taking the mean. So the implementation looks like
this:
class LayerNorm2(nn.Module):
def __init__(self, eps=1e-5):
super().__init__()
self.eps = eps
def forward(self, x):
batch_size, length, feature_dim = x.shape
output = torch.zeros_like(x)
# dim=-1 so we squish along last idx(feature_dim)
# keep_dim=True so we get (batch_size, length, 1)
mu = x.mean(dim=-1, keep_dim=True)
var = x.var(dim=-1, keep_dim=True)
output = (x - mu) / torch.sqrt(var + self.eps)
return output