class TinyTransformer(nn.Module):
def __init__(self):
super().__init__()
# setting the constructor for the initial values that we are every gonna need for the training of the data
self.char_embedding = nn.Embedding(65, 64)
self.pos_embedding = nn.Embedding(64, 64)
self.query = nn.Linear(64, 64)
self.key = nn.Linear(64, 64)
self.value = nn.Linear(64, 64)
self.mask = torch.tril(torch.ones(64, 64))
# these are for changing the dimensions we are doing this to enlarge the matrix as to make it of higher resolution so as to make the
# data and weights more refined
self.ff1 = nn.Linear(64, 128)
# this is to join them back again
self.ff2 = nn.Linear(128, 64)
self.output_head = nn.Linear(64, 65)
self.norm1 = nn.LayerNorm(64)
self.norm2 = nn.LayerNorm(64)
self.out_proj = nn.Linear(64, 64)
def forward(self, x):
# feed forward function
x = self.char_embedding(x) + self.pos_embedding(torch.arange(64))
# this is the start of the attention stuff i am writing this as a way to separate the code in section inside a functions
#
Q = self.query(x)
Q = Q.view(32, 64, 2, 32)
Q = Q.transpose(1, 2)
K = self.key(x)
K = K.view(32, 64, 2, 32)
K = K.transpose(1, 2)
V = self.value(x)
V = V.view(32, 64, 2, 32)
V = V.transpose(1, 2)
A = (Q @ K.transpose(-2, -1)) / 32**0.5
A = A.masked_fill(self.mask == 0, float("-inf"))
At = A.softmax(dim=-1)
# the -1 this is just to tell the
output = At @ V
output = output.transpose(1, 2).contiguous().view(32, 64, 64)
output = self.out_proj(output)
# this is where the attention ends and we start with the feed forward thing that will give us the predictions
# added another form of normalization below to improve accuracy the first time the loss function reached 1.8 max now after adding the
# below line it reached to like 1.5 something
x = x + output
x = self.norm1(x)
output = self.ff1(x)
output = torch.relu(output)
output = self.ff2(output)
x = x + output # ← merge back into main flow
x = self.norm2(x)
x = self.output_head(x)
return x
このコードは、AI分野の誰にとっても、トランスフォーマーを訓練するための基本的なボイラープレートです。
私はここにある小さな一行を理解したいだけです。その行には本当に興味深い背景があります。
x = x + output
なぜこれをやっているのか - X=X+output ?
ニューラルネットワークは、バックプロパゲーションのプロセスを通じて学習します。これは基本的に、フィルターを変更することで正しい予測に近づく変化を探していることを意味します。しかし、これには特定の問題があります。それは、ある層から別の層に移動するにつれて勾配がどんどん小さくなり、これは大きな問題です。なぜなら、計算もますます困難になり、計算コストも高くなるからです。これは基本的に、偏微分の連鎖律に起因します。。では、このことはどのように解決されるのでしょうか?
*この問題の歴史 *
こちらの論文を読む --
この論文はマイクロソフト研究チームによるもので、基本的には「多ければ必ずしも良いとは限らない」という問題をどのように解決したかについてです。この論文以前、深層学習モデルを訓練する際、モデルの深さ(つまり層の数)が増えるほど、エラーも増えるという大きな問題があり、人々はその解決方法を知りませんでした。なぜなら、一方ではより深い理解が得られる一方で、もう一方ではエラーが増えるという問題があったからです。
解決策
さて、私たちは解決策として、すべての計算後に得られたコンテキスト行列に元の埋め込みベクトル(私の場合)を単に追加すればよいと思うかもしれません。そう考えるのは正しいですが、その理由はあなたが考えているものとは異なります。論文自体にも、それがこの問題の理由ではないと書かれています。
私たちは、この最適化の難しさが…する可能性は低いと考えています。
勾配消失によって引き起こされます。
なぜか? - 勾配消失に対する疑念を取り除く理由は、勾配消失問題を最小化し停止するための対策が行われているからです。これらはバッチ正規化や、この場合はReLuなどの助けを借りて行われます。以下はコード内でそれを行う方法です -
x = self.norm1(x) # the batch normalization equivalent in transformers
output = self.ff1(x)
output = torch.relu(output) # another way to solve the vanishing gradient problem
output = self.ff2(output)
x = x + output # ← merge back into main flow
x = self.norm2(x)
x = self.output_head(x)
ご覧の通り、これやそれらは勾配消失問題を解決します。しかし、もしx=x+outputの行を削除すると、結果は悪化します。試してみましょう。 --
これは通常の方法で何も変更しない場合です。次に、一箇所だけ変更します。つまり、x=x+outputの行を削除するだけです。そして、それが損失関数にどのような影響を与えるかを見てみます。
それで、損失関数はこの1行だけで1.70から2.47に跳ね上がりました。あまり大きな差に見えないかもしれませんが、これは単純化のために1層モデルであることを忘れないでください。そして、層を追加するほど誤差も大きくなります。私の主張を確固たるものにするために、ここでいくつかの小さな調整を加えることで、その勾配の動きを示したいと思います -
step 44500, loss: 2.5130
char_embedding.weight grad_norm: 0.006248
pos_embedding.weight grad_norm: 0.005838
ff1.weight grad_norm: 0.024721
ff2.weight grad_norm: 0.053932
output_head.weight grad_norm: 0.163109
norm1.weight grad_norm: 0.007271
norm2.weight grad_norm: 0.024594
step 45000, loss: 2.4751
char_embedding.weight grad_norm: 0.005574
pos_embedding.weight grad_norm: 0.005913
ff1.weight grad_norm: 0.023506
ff2.weight grad_norm: 0.056331
output_head.weight grad_norm: 0.161182
norm1.weight grad_norm: 0.007898
norm2.weight grad_norm: 0.020992
step 45500, loss: 2.4623
char_embedding.weight grad_norm: 0.006224
pos_embedding.weight grad_norm: 0.006075
ff1.weight grad_norm: 0.025461
ff2.weight grad_norm: 0.051210
output_head.weight grad_norm: 0.145062
norm1.weight grad_norm: 0.008452
norm2.weight grad_norm: 0.018521
step 46000, loss: 2.4764
char_embedding.weight grad_norm: 0.006709
pos_embedding.weight grad_norm: 0.006148
ff1.weight grad_norm: 0.026940
ff2.weight grad_norm: 0.057071
output_head.weight grad_norm: 0.163159
norm1.weight grad_norm: 0.008988
norm2.weight grad_norm: 0.025112
step 46500, loss: 2.4746
char_embedding.weight grad_norm: 0.006127
pos_embedding.weight grad_norm: 0.006181
ff1.weight grad_norm: 0.025931
ff2.weight grad_norm: 0.056799
output_head.weight grad_norm: 0.158272
norm1.weight grad_norm: 0.008369
norm2.weight grad_norm: 0.025981
さて、ここで私が言っていたのは、これが勾配消失問題を解決するように見えるけれど、実際には全く解決していないということです。
実際にこれが行っているのは、もっとずっと興味深いことなのです。-
すべての層と非線形は何らかの変更を行い、これらの変更は非常に速く複合的に積み重なります。そして、約20層の場合、たとえ層数が多いとしても機能するかもしれません。しかし、ここから50層のようなものに移行すると、複雑さはさらに急増します。これらの「小さな」変更は、それ自体が非常に小さくても値を大きく変える可能性があり、その結果生じる値は元の値とはまったく異なるものになるかもしれません。例を挙げると -
5, 3, 8 --->5, 3, 8--->0.3, 0.01, 0.2 そして、これらは勾配消失のようなものによるものではなく、その間に行う小さな変更によるものであることに気づきます。では、これに元のものを追加するとどうなるでしょうか? -
[5, 3, 8] + [0.3, 0.01, 0.2] = [5.3, 3.01, 8.2]
それで、この結果は元のものに非常に近いですよね?それが残差の主な考え方です。他にも多くの残差アルゴリズムがありますが、簡単のため、昔ながらの加算に固執します。率直に言って、この方が良いのです。














