Deepmalya25 commited on
Commit
0e3c8f1
·
verified ·
1 Parent(s): 9261167

Upload Transformer_script.py

Browse files

The Source Code file containing staged evolution from naive Bigram Language Model to advanced Transformer architecture

Files changed (1) hide show
  1. Transformer_script.py +298 -0
Transformer_script.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torch.nn import functional as F
4
+
5
+ # Hyperparameters
6
+ batch_size = 64 # how many independent sequences will we process in parallel?
7
+ block_size = 256 # what is the maximum context length for predictions?
8
+ max_iters = 5000
9
+ eval_interval = 500
10
+ learning_rate = 3e-4
11
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
12
+ eval_iters = 200
13
+ n_embed=384
14
+ n_head=6 # therefore every head (head_size)= 384//6 = 64 dim
15
+ dropout=0.2
16
+ # ------------
17
+
18
+ torch.manual_seed(2506)
19
+
20
+ # wget https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt
21
+ with open(r'data\input.txt', 'r', encoding='utf-8') as f:
22
+ text = f.read()
23
+
24
+ # here are all the unique characters that occur in this text
25
+ chars = sorted(list(set(text)))
26
+ vocab_size = len(chars)
27
+ # create a mapping from characters to integers
28
+ stoi = { ch:i for i,ch in enumerate(chars) }
29
+ itos = { i:ch for i,ch in enumerate(chars) }
30
+ encode = lambda s: [stoi[c] for c in s] # encoder: take a string, output a list of integers
31
+ decode = lambda l: ''.join([itos[i] for i in l]) # decoder: take a list of integers, output a string
32
+
33
+ # Train and test splits
34
+ data = torch.tensor(encode(text), dtype=torch.long)
35
+ n = int(0.9*len(data)) # first 90% will be train, rest val
36
+ train_data = data[:n]
37
+ val_data = data[n:]
38
+
39
+ # data loading
40
+ def get_batch(split):
41
+ # generate a small batch of data of inputs x and targets y
42
+ data = train_data if split == 'train' else val_data
43
+ ix = torch.randint(len(data) - block_size, (batch_size,))
44
+ x = torch.stack([data[i:i+block_size] for i in ix])
45
+ y = torch.stack([data[i+1:i+block_size+1] for i in ix])
46
+ x, y = x.to(device), y.to(device)
47
+ return x, y
48
+
49
+ @torch.no_grad() # tells pyTorch not to do backpropagation
50
+ def estimate_loss(): # average outs the loss for eval_iterations
51
+ out = {}
52
+ model.eval()
53
+ for split in ['train', 'val']:
54
+ losses = torch.zeros(eval_iters)
55
+ for k in range(eval_iters):
56
+ X, Y = get_batch(split)
57
+ logits, loss = model(X, Y)
58
+ losses[k] = loss.item()
59
+ out[split] = losses.mean()
60
+ model.train()
61
+ return out
62
+
63
+
64
+ class Head(nn.Module):
65
+ '''This class is responsible for creating the Attention or Communication Mechanism - that lets the tokens communicate with past timesteps'''
66
+ def __init__(self, head_size):
67
+ super().__init__()
68
+ self.key=nn.Linear(n_embed, head_size, bias=False) # The attentione heads are like channels of dimension say (32,16) i.e 32 other tokens define the word and 16 other timesteps contributes to its semantic meaning
69
+ self.query=nn.Linear(n_embed, head_size, bias =False)
70
+ self.value=nn.Linear(n_embed, head_size, bias =False)
71
+ self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size))) # buffer is a variable that is needed for use, but is not a model param.
72
+ self.drop=nn.Dropout(dropout)
73
+
74
+ def forward(self, x):
75
+
76
+ # Extracting tensor shape params
77
+ B,T,C=x.shape
78
+ # mapping key, query and value to the input
79
+ k=self.key(x)
80
+ q=self.query(x)
81
+ v=self.value(x)
82
+
83
+ weights= q @ k.transpose(-2,-1) * C**(-0.5) # Scaled Dot product
84
+ weights=weights.masked_fill(self.tril[:T,:T]==0, float('-inf')) # type: ignore
85
+ weights=F.softmax(weights, dim=-1)
86
+ weights = self.drop(weights)
87
+
88
+ # Perform weighted aggregation of values
89
+ out=weights @ v
90
+ return out
91
+
92
+ # Improvement: I - Multi-head Attention
93
+ # Now that we are done with Single-Head Attention - lets implement Multi_head attention - replicas of how many heads we want.
94
+ # Each head is responsible for some definite and specific purpose - say head 1 captures relationship of how are vowels related, while say head 2 maps how the punctuation contributes to the next token
95
+ class MultiHead(nn.Module):
96
+ '''Multiple Heads of Self Attention in parallel'''
97
+ def __init__(self, num_heads, head_size):
98
+ super().__init__()
99
+ self.heads=nn.ModuleList([Head(head_size) for _ in range(num_heads)]) # using the function from base class we can create a list of submodules within a list
100
+ # we shall create another linear layer: projection that forks off from the main path, directly takes the multi_head concatenated vectors and perform a linear transformation on it and joins back to the main path
101
+ self.proj = nn.Linear(n_embed, n_embed) # takes n_embed input and outputs linearly transformed n_embed....the dim remains same
102
+ self.drop=nn.Dropout(dropout)
103
+
104
+ def forward(self,x):
105
+ # here we are just concatenating all the separate heads over the last dimention(channel dim) into a single unit
106
+ out = torch.cat([h(x) for h in self.heads], dim=-1) # take every single heads from the list of multiheads and for each of them compute k,q,v using x as argument and then concat all of them back
107
+ out = self.proj(out)
108
+ return self.drop(out) # here we again join the projecrtion back into the main residual pathway
109
+
110
+ # Improvement: II - FeedForward Network
111
+ # Now by far whatever improvements we did, still we compute logits directly after the attention level. Though the loss decreased from 2.4 in ur first version to 2.2 in our latest- but still the logits were calculated directly, therefore the architecture don't get enough time to think upon what they found from other tokens
112
+ # Thus we need a linear single layer of network with relu non-linearity for propagation of the logits - with feedforward the loss further decreased from 2.28 to 2.23
113
+ class FeedForward(nn.Module):
114
+ '''a simnple linear layer followed by non-linearity'''
115
+
116
+ def __init__(self, n_embed):
117
+ super().__init__()
118
+ self.ffnet=nn.Sequential(
119
+ nn.Linear(n_embed, 4* n_embed),
120
+ nn.ReLU(),
121
+ nn.Linear(4*n_embed, n_embed), # similar skip connections we build here for optimization of the network
122
+ nn.Dropout(dropout)
123
+ )
124
+ # why the inner layer of feedforward net had 4 times the num of embeddings...i.e 4 X 32?? This is done purely based on the paper ("Attention is all You Need"). We do so to increase the computation and sensitivity upto 4 times in the forked branch and then again reduce it to normal n_embed while joining the forked branch to the main branch - After training we can see the loss further decreasd to 1.913(train) and 2.05(val)
125
+
126
+ def forward(self, x):
127
+ return self.ffnet(x) # executing each tokens on the feed forward neural net
128
+
129
+
130
+
131
+ # Improvement: III - Block Architecture
132
+ # Now by far whatever we did in the Decoder architecture (Pos encoding, sh, mh, scaled dot prod, feedforwd) can be trated as block of calculation & computation. Now to improve performance we can repeat these block k times
133
+ class Block(nn.Module):
134
+ '''Transformer Block: Communication (Attention) followed by Computation'''
135
+
136
+ def __init__(self, n_embed, n_head):
137
+ super().__init__()
138
+ head_size=n_embed//n_head
139
+ self.attention=MultiHead(n_head, head_size)
140
+ self.ffwd=FeedForward(n_embed)
141
+ self.ln1=nn.LayerNorm(n_embed) # creating 2 layernorm layers
142
+ self.ln2=nn.LayerNorm(n_embed)
143
+
144
+ def forward(self, x):
145
+ x = x + self.attention(self.ln1(x)) # why x+ ?? This is an upgrade where we use skip connections for optimization, we fork off from the main bakprop branch, compute and then join the branch back again
146
+ x = x + self.ffwd(self.ln2(x)) # This prevents
147
+ return x
148
+ # Block of Transformer creates Abstraction by judt defining the predefined methods and objects for Communication and Computation repeatedly
149
+ # But repeating blocks also maked the network deep, therefore we need to optimize the network - techniques: Residual(skip) Connection, batch norm, dropout
150
+
151
+ # Improvement: IV - SKip Connection:
152
+ # During Back Propagation - : Gradients (the error signals) travel backward through those same layers to tweak the weights via the Chain Rule. In a very deep network without skip connections, as the gradient travels backward through dozens of linear layers and activation functions, it gets multiplied over and over again: If the weights are slightly less than (1.0), the gradient continuously shrinks until it becomes virtually zero (0.0000...). The early layers of the model stop learning completely.If the weights are slightly greater than (1.0), the gradient multiplies exponentially until it hits infinity (NaN), crashing your training entirely.A skip connection modifies the architectural layout of a neural network block. Instead of forcing the data vector \(x\) to strictly pass through a layer \(F(x)\), the connection splits the path, allowing the original raw input to hop over the layer and be added directly to the output. Output = x + f(x) --------> Changes made under forard function in Block class and applied to MultiHead and Feedfwd class.
153
+
154
+
155
+ # Improvement: V - Layer Norm
156
+ # When we update a neural network's parameters via an optimizer, the weights of the earliest layers change. Consequently, the distributions of inputs to the deeper layers shift constantly throughout training. This shifting is called Internal Covariate Shift. Deeper layers are forced to chase a moving target, slowing down learning convergence and increasing the likelihood of vanishing or exploding gradients.
157
+ # class BatchNorm1D:
158
+ # '''BatchNorm1D -> LayerNorm. This is used as sequences in a batch may vary in length, therefore batchnorm would be messy - thus we use layer norm which is same in principle just applied for individual vectors independently along the feature axis, making it independent of sequence length and batch size.'''
159
+ #----------------------------------------------------------------------------------------------------------------------------------
160
+ # def __init__(self, dim, eps=1e-5, momentum=0.1):
161
+ # self.eps=eps
162
+ # self.momentum=momentum
163
+ # self.training=True
164
+
165
+ # #parameters with backprop
166
+ # self.gamma=torch.ones(dim)
167
+ # self.beta=torch.zeros(dim)
168
+
169
+ # #Buffers (trained with a running momentum update)
170
+ # self.running_mean=torch.ones(dim)
171
+ # self.running_var=torch.ones(dim)
172
+
173
+ # def __call__(self, x):
174
+ # if self.training:
175
+ # xmean=x.mean(0, keepdim=True)
176
+ # xvar=x.var(0, keepdim=True)
177
+ # else:
178
+ # xmean=self.running_mean
179
+ # xvar=self.running_var
180
+ # xhat=(x-xmean)/torch.sqrt(xvar+self.eps) # implying the formula to compute xhat; this is the Normalize to unite variance step that makes the inter layer architecture, gradients stable
181
+ # self.out=self.gamma*xhat + self.beta
182
+ # if self.training:
183
+ # self.running_mean = (1-self.momentum)*self.running_mean + self.momentum* xmean
184
+ # self.running_var = (1-self.momentum)*self.running_var + self.momentum* xvar
185
+ # return self.out
186
+
187
+ # def parameters(self):
188
+ # return [self.gamma, self.beta]
189
+
190
+ # torch.manual_seed(2506)
191
+ # module=BatchNorm1D(dim=100)
192
+ # x=torch.randn(32, 100)
193
+ # x=module(x)
194
+ # x.shape
195
+ #----------------------------------------------------------------------------------------------------------------------------------
196
+ # We now know why not batchnorm and why layernorm applies to our Transformer architecture - thus layernorm implemented in Block class
197
+ # Present status after LayerNorm: step 4800: train loss 1.9299, val loss 2.0375
198
+
199
+
200
+ # Improvement VI: We add DropOut layers before the forked branch connects back ot residuals - This drops a % of neurons from each layer thereby generalizing the model - DropOut added at head, MultiHead, D
201
+ # status: step 4800: train loss 2.0543, val loss 2.1065
202
+
203
+ # Gradually improved architeture of a naive Bigram model implemnted with M-H attention, FeedForward class
204
+ class BigramLanguageModel(nn.Module):
205
+
206
+ # Configurations
207
+ def __init__(self):
208
+ super().__init__()
209
+ # each token directly reads off the logits for the next token from a lookup table
210
+ self.token_embedding_table = nn.Embedding(vocab_size, n_embed)
211
+ # We besides creating the embedding table to store the embedding vectors for every token also create a positional_embeddding table for positional encoding
212
+ self.positional_embedding_table=nn.Embedding(block_size,n_embed)
213
+ # self.sa = Head(n_embed) # creating the self attention head - commented out since we have improved to Multi_head attention
214
+ # Now that we have evolved to Multiple heads, lets comment out Self-head and implement Multi-head attention
215
+ # self.mha = MultiHead(num_heads=4, head_size=n_embed//4) # we are dividing the entire num of embedd. into 4 sets for 4 heads of uniform size. (4 * 8Dim = 32Dim embedd vector)
216
+ # self.feedfwd = FeedForward(n_embed)
217
+
218
+ # We now comment out all the isolated unit level operations, since we have created a Block containing the same operations and we can just Sequentially repeat them
219
+ self.blocks = nn.Sequential(
220
+ Block(n_embed=n_embed, n_head=n_head),
221
+ Block(n_embed=n_embed, n_head=n_head),
222
+ Block(n_embed=n_embed, n_head=n_head),
223
+ Block(n_embed=n_embed, n_head=n_head),
224
+ Block(n_embed=n_embed, n_head=n_head),
225
+ Block(n_embed=n_embed, n_head=n_head),
226
+ nn.LayerNorm(n_embed), # We also normalize each vectors independently before providing it to the final linear layer
227
+ )
228
+ self.lm_head = nn.Linear(n_embed, vocab_size)
229
+
230
+
231
+ def forward(self, idx, targets=None):
232
+ B,T = idx.shape
233
+ # idx and targets are both (B,T) tensor of integers
234
+ tok_embd = self.token_embedding_table(idx) # (B,T,C)
235
+ pos_embd = self.positional_embedding_table(torch.arange(T, device=device)) # creating pos embd vectors from 0 to range T-1 of shape (T,C)
236
+ x=tok_embd + pos_embd
237
+
238
+ # x = self.mha(x)
239
+ # s=self.feedfwd(x) # (B,T,C)
240
+
241
+ x=self.blocks(x)
242
+ logits=self.lm_head(x)
243
+
244
+ if targets is None:
245
+ loss = None
246
+ else:
247
+ B, T, C = logits.shape
248
+ logits = logits.view(B*T, C)
249
+ targets = targets.view(B*T)
250
+ loss = F.cross_entropy(logits, targets)
251
+
252
+ return logits, loss
253
+
254
+ def generate(self, idx, max_new_tokens):
255
+ # idx is (B, T) array of indices in the current context
256
+ for _ in range(max_new_tokens):
257
+
258
+ # idx can go out of range that's not present in the scope of Embeddings table, thus we need to filter it
259
+ idx_cond=idx[:, -block_size:]
260
+ # get the predictions
261
+ logits, loss = self(idx_cond) # forward(idx_cond)
262
+ # focus only on the last time step
263
+ logits = logits[:, -1, :] # becomes (B, C)
264
+ # apply softmax to get probabilities
265
+ probs = F.softmax(logits, dim=-1) # (B, C)
266
+ # sample from the distribution
267
+ idx_next = torch.multinomial(probs, num_samples=1) # (B, 1)
268
+ # append sampled index to the running sequence
269
+ idx = torch.cat((idx, idx_next), dim=1) # (B, T+1)
270
+ return idx
271
+
272
+
273
+ model = BigramLanguageModel()
274
+ m = model.to(device)
275
+ print(sum(p.numel() for p in m.parameters())/1e6, 'M parameters') # Roughly around 10M params on a 1M char dataset
276
+
277
+ # create a PyTorch optimizer
278
+ optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
279
+
280
+ for iter in range(max_iters):
281
+
282
+ # every once in a while evaluate the loss on train and val sets
283
+ if iter % eval_interval == 0:
284
+ losses = estimate_loss()
285
+ print(f"step {iter}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")
286
+
287
+ # sample a batch of data
288
+ xb, yb = get_batch('train')
289
+
290
+ # evaluate the loss
291
+ logits, loss = model(xb, yb)
292
+ optimizer.zero_grad(set_to_none=True)
293
+ loss.backward()
294
+ optimizer.step()
295
+
296
+ # generate from the model
297
+ context = torch.zeros((1, 1), dtype=torch.long, device=device)
298
+ print(decode(m.generate(context, max_new_tokens=500)[0].tolist()))