simonbb 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
simonbb/__init__.py ADDED
File without changes
simonbb/bert.py ADDED
@@ -0,0 +1,39 @@
1
+ import re
2
+ import torch
3
+ import torch.nn as nn
4
+ from transformers import BertTokenizer, BertModel, BertConfig
5
+
6
+ #%%
7
+
8
+ tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)
9
+
10
+ class BertClassifier( nn.Module ):
11
+ def __init__(self, freeze_bert=False):
12
+ super().__init__()
13
+ config = BertConfig( max_position_embeddings=1024 ) # Instantiate BERT model, default hidden_size=767, i.e., dimensionality of the encoder layers and the pooler layer
14
+ BertModel( config )
15
+ self.bert = BertModel.from_pretrained('bert-base-uncased')
16
+ D_in, H, D_out = 768, 50, 2 # D_in is hidden_size of BERT, H is the hidden size of our classifier, and D_out is the number of classes
17
+ self.classifier = nn.Sequential( nn.Linear(D_in, H), nn.ReLU(), nn.Linear(H, D_out) ) # Instantiate a one-layer feed-forward classifier. nn.Dropout(0.5),
18
+ if freeze_bert:
19
+ for param in self.bert.parameters():
20
+ param.requires_grad = False # if True, PyTorch will track all tensors that have param as an ancestor
21
+
22
+ def forward(self, x):
23
+ input_ids, attention_mask = x
24
+ outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask) # calling an instance of nn.Module ends up calling forward() with the same arguments
25
+ last_hidden_state_cls = outputs[0][:, 0, :] # Extract the last hidden state of the token `[CLS]` for classification task
26
+ logits = self.classifier( last_hidden_state_cls )
27
+ return logits
28
+
29
+
30
+ def BertPreprocessor( txt ):
31
+ input_ids = []
32
+ attention_masks = []
33
+ for s in txt:
34
+ s = re.sub(r'(@.*?)[\s]', ' ', s)
35
+ s = re.sub(r'\s+', ' ', s).strip()
36
+ encoded = tokenizer.encode_plus( text=s, add_special_tokens=True, max_length=128, padding='max_length', truncation=True, return_attention_mask=True )
37
+ input_ids.append( encoded.get('input_ids') ) # Add the outputs to the lists
38
+ attention_masks.append( encoded.get('attention_mask') )
39
+ return torch.tensor( input_ids ), torch.tensor( attention_masks )
simonbb/gpt.py ADDED
@@ -0,0 +1,204 @@
1
+ import numpy as np
2
+ import torch
3
+ import torch.nn as nn
4
+
5
+ from .transformer import MultiHeadAttention, FeedForward
6
+
7
+ GPT2CONFIG = {"vocab_size": 50257, "context_length": 1024, "drop_rate": 0.0, "qkv_bias": True }
8
+ GPT2SIZE = { "gpt2-small": {"emb_dim": 768, "n_layers": 12, "n_heads": 12, "size": "124M"}, # 621.83 MB
9
+ "gpt2-medium": {"emb_dim": 1024, "n_layers": 24, "n_heads": 16, "size":"355M"}, # 1549.58 MB
10
+ "gpt2-large": {"emb_dim": 1280, "n_layers": 36, "n_heads": 20, "size":"774M"}, # 3197.56 MB
11
+ "gpt2-xl": {"emb_dim": 1600, "n_layers": 48, "n_heads": 25, "size":"1558M"} } # 6247.68 MB
12
+
13
+ # Use "scale" and "shift" to match GPT2 implementation of LayerNorm. nn.LayerNorm() uses 'weight' and 'bias'
14
+ class LayerNorm(nn.Module):
15
+ def __init__(self, emb_dim):
16
+ super().__init__()
17
+ self.eps = 1e-5
18
+ self.scale = nn.Parameter(torch.ones(emb_dim))
19
+ self.shift = nn.Parameter(torch.zeros(emb_dim))
20
+
21
+ def forward(self, x):
22
+ mean = x.mean(dim=-1, keepdim=True)
23
+ var = x.var(dim=-1, keepdim=True, unbiased=False)
24
+ norm_x = (x - mean) / torch.sqrt(var + self.eps)
25
+ return self.scale * norm_x + self.shift
26
+
27
+ class TransformerBlock(nn.Module): # Pre-LayerNorm, instead of Post-LayerNorm in the original paper
28
+ def __init__(self, cfg):
29
+ super().__init__()
30
+ self.att = MultiHeadAttention( embed_dim=cfg["emb_dim"], num_heads=cfg["n_heads"], seq_len=cfg["context_length"], dropout=cfg["drop_rate"], qkv_bias=cfg["qkv_bias"], is_causal=True )
31
+ self.ff = FeedForward( cfg["emb_dim"], 4*cfg["emb_dim"] )
32
+ self.norm1 = LayerNorm( cfg["emb_dim"] )
33
+ self.norm2 = LayerNorm( cfg["emb_dim"] )
34
+ self.drop_shortcut = nn.Dropout( cfg["drop_rate"] )
35
+
36
+ def forward(self, x):
37
+ shortcut = x # Shortcut connection for attention block
38
+ x = self.norm1(x)
39
+ x = self.att(x,x,x) # Shape [batch_size, num_tokens, emb_size]
40
+ x = self.drop_shortcut(x)
41
+ x = x + shortcut # Add the original input back
42
+ shortcut = x # Shortcut connection for feed forward block
43
+ x = self.norm2(x)
44
+ x = self.ff(x)
45
+ x = self.drop_shortcut(x)
46
+ x = x + shortcut # Add the original input back
47
+ return x
48
+
49
+
50
+ class GPTModel(nn.Module):
51
+ def __init__(self, cfg):
52
+ super().__init__()
53
+ self.tok_emb = nn.Embedding( cfg["vocab_size"], cfg["emb_dim"] )
54
+ self.pos_emb = nn.Embedding( cfg["context_length"], cfg["emb_dim"] )
55
+ self.drop_emb = nn.Dropout( cfg["drop_rate"] )
56
+ self.trf_blocks = nn.Sequential( *[TransformerBlock(cfg) for _ in range(cfg["n_layers"])] )
57
+ self.final_norm = LayerNorm( cfg["emb_dim"] )
58
+ self.out_head = nn.Linear( cfg["emb_dim"], cfg["vocab_size"], bias=False )
59
+
60
+ def forward(self, in_idx):
61
+ batch_size, seq_len = in_idx.shape
62
+ tok_embeds = self.tok_emb(in_idx)
63
+ pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
64
+ x = tok_embeds + pos_embeds # Shape [batch_size, num_tokens, emb_size]
65
+ x = self.drop_emb(x)
66
+ x = self.trf_blocks(x)
67
+ x = self.final_norm(x)
68
+ logits = self.out_head(x) # Applying a PyTorch Linear layer to a rank-3 tensor performs a linear transformation only on the last dimension.
69
+ return logits
70
+
71
+
72
+ #%% utility functions for training and inference
73
+
74
+ def text_to_token_ids(text, tokenizer):
75
+ encoded = tokenizer.encode(text)
76
+ encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension
77
+ return encoded_tensor
78
+
79
+ def token_ids_to_text(token_ids, tokenizer):
80
+ flat = token_ids.squeeze(0) # remove batch dimension
81
+ return tokenizer.decode(flat.tolist())
82
+
83
+ # generate based on idx, context of shape (batch, seq_len) with each element a token ID
84
+ def generate(model, idx, max_new_tokens, context_size, temperature=0.0, top_k=None, eos_id=None):
85
+ for _ in range(max_new_tokens):
86
+ idx_cond = idx[:, -context_size:] # crop current context to at most the last context_size number of token ids
87
+ with torch.no_grad():
88
+ logits = model(idx_cond) # (batch, seq_len, vocab_size)
89
+ logits = logits[:, -1, :] # By focusing on the last time step, the shape becomes (batch, vocab_size)
90
+ if top_k is not None: # keep only top_k values
91
+ top_logits, _ = torch.topk(logits, top_k) # returns top_k logits in descending order and their positions
92
+ min_val = top_logits[:, -1]
93
+ logits = torch.where(logits < min_val, torch.tensor(float('-inf')).to(logits.device), logits)
94
+ if temperature > 0.0: # apply temperature scaling
95
+ logits = logits / temperature
96
+ probs = torch.softmax(logits, dim=-1) # logits.shape = (batch_size, vocab_size)
97
+ idx_next = torch.multinomial(probs, num_samples=1) # idx_next.shape = (batch_size, 1)
98
+ else: # greedy selection of the idx of the vocab with the highest probability
99
+ idx_next = torch.argmax(logits, dim=-1, keepdim=True) # idx_next.shape = (batch_size, 1)
100
+ if idx_next == eos_id: # Stop generating early if end-of-sequence token is encountered and eos_id is specified
101
+ break
102
+ idx = torch.cat((idx, idx_next), dim=1) # idx.shape = (batch_size, seq_len+1) after appending id_next to idx
103
+ return idx
104
+
105
+ #%% utility functions for loading pretrained GPT2 models
106
+
107
+ def assign(left, right):
108
+ if left.shape != right.shape:
109
+ raise ValueError(f"Shape mismatch. Left: {left.shape}, Right: {right.shape}")
110
+ return torch.nn.Parameter(torch.tensor(right))
111
+
112
+ def load_pretrained(gpt, params):
113
+ gpt.pos_emb.weight = assign(gpt.pos_emb.weight, params['wpe'])
114
+ gpt.tok_emb.weight = assign(gpt.tok_emb.weight, params['wte'])
115
+ for b in range(len(params["blocks"])):
116
+ q_w, k_w, v_w = np.split( (params["blocks"][b]["attn"]["c_attn"])["w"], 3, axis=-1 )
117
+ gpt.trf_blocks[b].att.W_q.weight = assign( gpt.trf_blocks[b].att.W_q.weight, q_w.T )
118
+ gpt.trf_blocks[b].att.W_k.weight = assign( gpt.trf_blocks[b].att.W_k.weight, k_w.T )
119
+ gpt.trf_blocks[b].att.W_v.weight = assign( gpt.trf_blocks[b].att.W_v.weight, v_w.T )
120
+ # load bias
121
+ q_b, k_b, v_b = np.split( (params["blocks"][b]["attn"]["c_attn"])["b"], 3, axis=-1 )
122
+ gpt.trf_blocks[b].att.W_q.bias = assign( gpt.trf_blocks[b].att.W_q.bias, q_b )
123
+ gpt.trf_blocks[b].att.W_k.bias = assign( gpt.trf_blocks[b].att.W_k.bias, k_b )
124
+ gpt.trf_blocks[b].att.W_v.bias = assign( gpt.trf_blocks[b].att.W_v.bias, v_b )
125
+ # load weights and bias for the output projection layer of mha
126
+ gpt.trf_blocks[b].att.out_proj.weight = assign( gpt.trf_blocks[b].att.out_proj.weight, params["blocks"][b]["attn"]["c_proj"]["w"].T )
127
+ gpt.trf_blocks[b].att.out_proj.bias = assign( gpt.trf_blocks[b].att.out_proj.bias, params["blocks"][b]["attn"]["c_proj"]["b"] )
128
+ # load weights and bias for feedforward layers
129
+ gpt.trf_blocks[b].ff.layers[0].weight = assign( gpt.trf_blocks[b].ff.layers[0].weight, params["blocks"][b]["mlp"]["c_fc"]["w"].T )
130
+ gpt.trf_blocks[b].ff.layers[0].bias = assign( gpt.trf_blocks[b].ff.layers[0].bias, params["blocks"][b]["mlp"]["c_fc"]["b"] )
131
+ gpt.trf_blocks[b].ff.layers[2].weight = assign( gpt.trf_blocks[b].ff.layers[2].weight, params["blocks"][b]["mlp"]["c_proj"]["w"].T )
132
+ gpt.trf_blocks[b].ff.layers[2].bias = assign( gpt.trf_blocks[b].ff.layers[2].bias, params["blocks"][b]["mlp"]["c_proj"]["b"] )
133
+ # load parameters for scale and shift of LayerNorm layers
134
+ gpt.trf_blocks[b].norm1.scale = assign( gpt.trf_blocks[b].norm1.scale, params["blocks"][b]["ln_1"]["g"] )
135
+ gpt.trf_blocks[b].norm1.shift = assign( gpt.trf_blocks[b].norm1.shift, params["blocks"][b]["ln_1"]["b"] )
136
+ gpt.trf_blocks[b].norm2.scale = assign( gpt.trf_blocks[b].norm2.scale, params["blocks"][b]["ln_2"]["g"] )
137
+ gpt.trf_blocks[b].norm2.shift = assign( gpt.trf_blocks[b].norm2.shift, params["blocks"][b]["ln_2"]["b"] )
138
+ gpt.final_norm.scale = assign( gpt.final_norm.scale, params["g"] )
139
+ gpt.final_norm.shift = assign( gpt.final_norm.shift, params["b"] )
140
+ # weight tying: the original GPT-2 model reused the token embedding weights in the output layer to reduce the total number of parameters.
141
+ gpt.out_head.weight = assign( gpt.out_head.weight, params['wte'] )
142
+
143
+ #%%
144
+
145
+ if __name__ == "__main__":
146
+
147
+ choice = "gpt2-small"
148
+ cfg = GPT2CONFIG.copy()
149
+ cfg.update( GPT2SIZE[choice] )
150
+ cfg['qkv_bias'] = False
151
+ cfg['drop_rate'] = 0.1
152
+
153
+ model = GPTModel( cfg )
154
+ total_params = sum(p.numel() for p in model.parameters())
155
+ print(f"Total number of parameters: {total_params:,}")
156
+
157
+ print("Token embedding layer shape:", model.tok_emb.weight.shape) # nn.Embedding(num_embeddings, embedding_dim) has weight of shape (num_embeddings, embedding_dim)
158
+ print("Output layer shape:", model.out_head.weight.shape) # nn.Linear(in_features, out_features) has weight of shape (out_features,in_features)
159
+
160
+ total_params_gpt2 = total_params - sum(p.numel() for p in model.out_head.parameters())
161
+ print(f"Number of trainable parameters considering weight tying: {total_params_gpt2:,}")
162
+
163
+ total_size_bytes = total_params * 4 # total size in bytes (assuming float32, 4 bytes per parameter)
164
+ print(f"Total size of the model: {total_size_bytes / (1024 * 1024):.2f} MB")
165
+
166
+ torch.manual_seed(123)
167
+
168
+ x = torch.rand(2, 4, 768) # Shape: [batch_size, num_tokens, emb_dim]
169
+ mha = MultiHeadAttention(d_in=768, d_out=768, context_length=1024, dropout=0.0, n_heads=12, is_causal=True)
170
+ print("context_vecs.shape:", mha(x).shape )
171
+
172
+ block = TransformerBlock( cfg )
173
+ print("Input shape:", x.shape)
174
+ print("Output shape:", block(x).shape)
175
+
176
+ import tiktoken
177
+ tokenizer = tiktoken.get_encoding("gpt2")
178
+
179
+ batch = []
180
+ txt1 = "Every effort moves you"
181
+ txt2 = "Every day holds a"
182
+ batch.append( torch.tensor(tokenizer.encode(txt1)) )
183
+ batch.append( torch.tensor(tokenizer.encode(txt2)) )
184
+ batch = torch.stack(batch, dim=0)
185
+ print("Input batch:\n", batch)
186
+
187
+ out = model( batch )
188
+ print("\nOutput shape:", out.shape)
189
+
190
+ start_context = "Hello, I am"
191
+ encoded = tokenizer.encode(start_context)
192
+ print("encoded:", encoded)
193
+
194
+ encoded_tensor = torch.tensor(encoded).unsqueeze(0)
195
+ print("encoded_tensor.shape:", encoded_tensor.shape)
196
+
197
+ model.eval() # disable dropout
198
+ out = generate( model=model, idx=encoded_tensor, max_new_tokens=6, context_size=cfg["context_length"] )
199
+
200
+ print("Output:", out)
201
+ print( tokenizer.decode(out.squeeze(0).tolist()) )
202
+
203
+ else:
204
+ print(f'GPT imported from local file "{__name__}.py"')
simonbb/transformer.py ADDED
@@ -0,0 +1,253 @@
1
+ import numpy as np
2
+ import torch
3
+ import torch.nn as nn
4
+ import math
5
+
6
+ #%% Positional Embedding
7
+
8
+ def positional_encoding(seq_len, depth):
9
+ d = depth / 2
10
+ positions = np.arange(seq_len)[:, np.newaxis] # shape: (seq_len, 1)
11
+ d = np.arange(d)[np.newaxis, :] / d # shape: (1, d)
12
+ angle_rates = 1 / (10000 ** d) # shape: (1, d)
13
+ angle_rads = positions * angle_rates # shape: (seq_len, d)
14
+ pos_encoding = np.concatenate([np.sin(angle_rads), np.cos(angle_rads)], axis=-1) # shape: (seq_len, 2*d)
15
+ return torch.tensor(pos_encoding, dtype=torch.float32) # shape: (seq_len, depth)
16
+
17
+ class PositionalEmbedding(nn.Module):
18
+ def __init__(self, vocab_size, d_emb, padding_idx=0, seq_len=1024):
19
+ super().__init__()
20
+ self.d_emb = d_emb # d_emb is the dimension of positional embedding, shared by the encoder and the decoder
21
+ self.embedding = nn.Embedding(vocab_size, d_emb, padding_idx=padding_idx) # token embedding
22
+ self.register_buffer('pos_encoding', positional_encoding(seq_len=seq_len, depth=d_emb)) # position embedding, with maximum sequence length 2048
23
+ self._init_weights()
24
+ def _init_weights(self): # uniform initialization for embeddings, as in Keras
25
+ nn.init.uniform_(self.embedding.weight, -0.05, 0.05) # PyTorch methods ending with an underscore makes changes in-place
26
+ self.embedding._fill_padding_idx_with_zero() # Keep padding as zeros
27
+ def forward(self, x):
28
+ seq_len = x.shape[1]
29
+ x = self.embedding(x)
30
+ x *= math.sqrt(self.d_emb) # This factor sets the relative scale of the embedding and positonal encoding.
31
+ return x + self.pos_encoding[:seq_len, :].unsqueeze(0) # use the position embedding matrix up to the seq_len position (i.e., row)
32
+
33
+ #%% Attention and FeedForward
34
+
35
+ class MultiHeadAttention(nn.Module):
36
+ def __init__(self, embed_dim, num_heads, dropout=0.0, seq_len=1024, qkv_bias=False, is_causal=False):
37
+ super().__init__()
38
+ assert embed_dim % num_heads == 0, "embed_dim must be divisible by num_heads"
39
+ self.d_model = embed_dim
40
+ self.n_heads = num_heads
41
+ self.d_k = embed_dim // num_heads
42
+ self.W_q = nn.Linear(embed_dim, embed_dim, bias=qkv_bias)
43
+ self.W_k = nn.Linear(embed_dim, embed_dim, bias=qkv_bias)
44
+ self.W_v = nn.Linear(embed_dim, embed_dim, bias=qkv_bias)
45
+ self.out_proj = nn.Linear(embed_dim, embed_dim) # Linear layer to combine head outputs
46
+ self.dropout = nn.Dropout(dropout)
47
+ self.is_causal = is_causal
48
+ if self.is_causal: # fill 1 in upper triangular (excluding diagonal) part to indicate masking
49
+ self.register_buffer('mask', torch.triu(torch.ones(seq_len, seq_len), diagonal=1))
50
+ # register a tensor as part of the module's state, without it being a learnable parameter
51
+ self._init_weights()
52
+
53
+ def _init_weights(self):
54
+ for name, param in self.named_parameters():
55
+ if 'weight' in name:
56
+ nn.init.xavier_uniform_(param) # Xavier/Glorot initialization (same as Keras default)
57
+ elif 'bias' in name:
58
+ nn.init.zeros_(param)
59
+
60
+ def forward(self, q, k, v):
61
+ b, seq_len, d_in = q.shape
62
+ k = self.W_k(k) # Shape: (b, seq_len, d_model)
63
+ q = self.W_q(q)
64
+ v = self.W_v(v)
65
+ # Implicitly split the matrix by adding a `n_heads` dimension: (b, seq_len, d_model) -> (b, seq_len, n_heads, d_k)
66
+ k = k.view(b, seq_len, self.n_heads, self.d_k) # (b, seq_len, n_heads, d_k)
67
+ v = v.view(b, seq_len, self.n_heads, self.d_k) # (b, seq_len, n_heads, d_k)
68
+ q = q.view(b, seq_len, self.n_heads, self.d_k) # (b, seq_len, n_heads, d_k)
69
+ k = k.transpose(1, 2) # (b, n_heads, seq_len, d_k)
70
+ q = q.transpose(1, 2) # (b, n_heads, seq_len, d_k)
71
+ v = v.transpose(1, 2) # (b, n_heads, seq_len, d_k)
72
+ attn_scores = q @ k.transpose(2, 3) # (b, n_heads, seq_len, seq_len)
73
+ if self.is_causal:
74
+ mask_bool = self.mask.bool()[:seq_len, :seq_len] # truncate the mask to seq_len and convert to boolean
75
+ attn_scores.masked_fill_(mask_bool, -torch.inf) # fill masked position with -torch.inf
76
+ attn_weights = torch.softmax(attn_scores / self.d_k**0.5, dim=-1) # (b, n_heads, seq_len, seq_len)
77
+ attn_weights = self.dropout(attn_weights) # (b, n_heads, seq_len, seq_len)
78
+ context_vec = (attn_weights @ v).transpose(1, 2) # (b, n_heads, seq_len, d_k), then (b, seq_len, n_heads, d_k) after transpose
79
+ context_vec = context_vec.contiguous().view(b, seq_len, self.d_model) # Combine heads: (b, seq_len, n_heads * d_k) where d_model = n_heads * d_k
80
+ context_vec = self.out_proj(context_vec) # optional projection
81
+ return context_vec
82
+
83
+
84
+ class FeedForward(nn.Module):
85
+ def __init__(self, d_emb, d_ff):
86
+ super().__init__()
87
+ self.layers = nn.Sequential(
88
+ nn.Linear(d_emb, d_ff),
89
+ nn.GELU(),
90
+ nn.Linear(d_ff, d_emb) )
91
+ self._init_weights()
92
+ def _init_weights(self): # Xavier/Glorot initialization (same as Keras default)
93
+ nn.init.xavier_uniform_(self.layers[0].weight)
94
+ nn.init.zeros_(self.layers[0].bias)
95
+ nn.init.xavier_uniform_(self.layers[2].weight)
96
+ nn.init.zeros_(self.layers[2].bias)
97
+ def forward(self, x):
98
+ return self.layers(x)
99
+
100
+ #%% Transformer = Encoder + Decoder + FeedForward
101
+
102
+
103
+ class EncoderLayer(nn.Module):
104
+ def __init__(self, *, d_emb, n_heads, d_ff, dropout=0.1, prenorm=False): # default: post layernorm, as in the original paper
105
+ super().__init__()
106
+ self.gsa = MultiHeadAttention( embed_dim=d_emb, num_heads=n_heads, dropout=dropout )
107
+ self.ff = FeedForward(d_emb, d_ff)
108
+ self.norm1 = nn.LayerNorm( d_emb )
109
+ self.norm2 = nn.LayerNorm( d_emb )
110
+ self.drop_shortcut = nn.Dropout( dropout )
111
+ self.prenorm = prenorm
112
+ def forward(self, x):
113
+ if self.prenorm:
114
+ shortcut = x # Shortcut connection for attention block
115
+ x = self.norm1(x)
116
+ else:
117
+ x = self.norm1(x)
118
+ shortcut = x
119
+ x = self.gsa(x,x,x) # Shape [batch_size, num_tokens, emb_size]
120
+ x = x + shortcut # Add the original input back
121
+ if self.prenorm:
122
+ shortcut = x # Shortcut connection for feed forward block
123
+ x = self.norm2(x)
124
+ else:
125
+ x = self.norm2(x)
126
+ shortcut = x
127
+ x = self.ff(x)
128
+ x = self.drop_shortcut(x)
129
+ return x + shortcut # Add the original input back
130
+
131
+
132
+ class Encoder(nn.Module):
133
+ def __init__(self, *, n_layers, d_emb, n_heads, d_ff, dropout=0.1, prenorm=False):
134
+ super().__init__()
135
+ self.d_emb = d_emb
136
+ self.n_layers = n_layers
137
+ self.enc_layers = nn.ModuleList( [EncoderLayer(d_emb=d_emb, n_heads=n_heads, d_ff=d_ff, prenorm=prenorm) for _ in range(n_layers)] )
138
+ self.dropout = nn.Dropout(dropout)
139
+
140
+ def forward(self, x): # x is token embeddings shape: (batch, seq_len, d_emb)
141
+ x = self.dropout(x)
142
+ for i in range(self.n_layers):
143
+ x = self.enc_layers[i](x)
144
+ return x # Shape: (batch_size, seq_len, d_emb)
145
+
146
+
147
+ class DecoderLayer(nn.Module):
148
+ def __init__(self, *, d_emb, n_heads, d_ff, seq_len=1024, dropout=0.1, prenorm=False):
149
+ super().__init__()
150
+ self.csa = MultiHeadAttention( embed_dim=d_emb, num_heads=n_heads, dropout=dropout, seq_len=seq_len, is_causal=True)
151
+ self.ca = MultiHeadAttention( embed_dim=d_emb, num_heads=n_heads, dropout=dropout )
152
+ self.ff = FeedForward(d_emb, d_ff)
153
+ self.norm1 = nn.LayerNorm( d_emb )
154
+ self.norm2 = nn.LayerNorm( d_emb )
155
+ self.drop_shortcut = nn.Dropout( dropout )
156
+ self.prenorm = prenorm
157
+
158
+ def forward(self, x, context):
159
+ if self.prenorm:
160
+ shortcut = x # Shortcut connection for attention block
161
+ x = self.norm1(x)
162
+ else:
163
+ x = self.norm1(x)
164
+ shortcut = x
165
+ x = self.csa(x,x,x) # Shape [batch_size, num_tokens, emb_size]
166
+ x = self.ca(q=x, k=context, v=context)
167
+ x = x + shortcut # Add the original input back
168
+ if self.prenorm:
169
+ shortcut = x # Shortcut connection for feed forward block
170
+ x = self.norm2(x)
171
+ else:
172
+ x = self.norm2(x)
173
+ shortcut = x
174
+ x = self.ff(x)
175
+ x = self.drop_shortcut(x)
176
+ return x + shortcut # Add the original input back
177
+
178
+
179
+ class Decoder(nn.Module):
180
+ def __init__(self, *, n_layers, d_emb, n_heads, d_ff, seq_len=1024, dropout=0.1, prenorm=False):
181
+ super().__init__()
182
+ self.d_emb = d_emb
183
+ self.n_layers = n_layers
184
+ self.dropout = nn.Dropout(dropout)
185
+ self.dec_layers = nn.ModuleList( [DecoderLayer(d_emb=d_emb, n_heads=n_heads, d_ff=d_ff, seq_len=seq_len, dropout=dropout, prenorm=prenorm) for _ in range(n_layers)] )
186
+
187
+ def forward(self, x, context): # x is of shape (batch, tgt_seq_len, d_emb); context is of shape (batch_size, context_len, d_emb)
188
+ x = self.dropout(x)
189
+ for i in range(self.n_layers):
190
+ x = self.dec_layers[i](x, context)
191
+ return x # shape: (batch_size, tgt_seq_len, d_emb)
192
+
193
+
194
+ class Transformer(nn.Module):
195
+ def __init__(self, *, n_layers, d_emb, n_heads, d_ff, src_vocab_size, tgt_vocab_size, seq_len=1024, dropout=0.1, prenorm=False ):
196
+ super().__init__()
197
+ self.src_pos_embedding = PositionalEmbedding(vocab_size=src_vocab_size, d_emb=d_emb, seq_len=seq_len)
198
+ self.tgt_pos_embedding = PositionalEmbedding(vocab_size=tgt_vocab_size, d_emb=d_emb, seq_len=seq_len)
199
+ self.encoder = Encoder(n_layers=n_layers, d_emb=d_emb, n_heads=n_heads, d_ff=d_ff, dropout=dropout, prenorm=prenorm)
200
+ self.decoder = Decoder(n_layers=n_layers, d_emb=d_emb, n_heads=n_heads, d_ff=d_ff, seq_len=seq_len, dropout=dropout, prenorm=prenorm)
201
+ self.final_layer = nn.Linear(d_emb, tgt_vocab_size)
202
+
203
+ def forward(self, x):
204
+ src, tgt = x
205
+ src_emb = self.src_pos_embedding(src)
206
+ tgt_emb = self.tgt_pos_embedding(tgt)
207
+ context = self.encoder(src_emb) # (batch_size, context_len, d_emb)
208
+ x = self.decoder(tgt_emb, context) # (batch_size, target_len, d_emb)
209
+ return self.final_layer(x) # (batch_size, target_len, tgt_vocab_size)
210
+
211
+
212
+ #%% Sanity Check: run from the parent folder of rui as "python -m rui.torch.transformer" since we used relative import
213
+ if __name__ == "__main__":
214
+
215
+ from ..utils import TextVectorizer
216
+
217
+ seq_len = 10
218
+ max_vocab_size = 100
219
+ d_emb = 512
220
+
221
+ vectorizer = TextVectorizer( max_tokens=max_vocab_size, output_mode="int" )
222
+ txts = ['how are you']
223
+ vectorizer.adapt( txts )
224
+ encoded = vectorizer(txts)
225
+
226
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
227
+
228
+ encoded_tensor = torch.tensor(encoded).to(device)
229
+
230
+ print(f"encoded_tensor: {encoded_tensor}")
231
+
232
+ x = PositionalEmbedding(vocab_size=max_vocab_size, d_emb=d_emb, seq_len=seq_len).to(device)(encoded_tensor)
233
+
234
+ print(f"input shape: {x.shape}")
235
+ n_heads = 8
236
+ mha = MultiHeadAttention( d_emb, n_heads).to(device)
237
+ print(f"MultiHeadAttention output shape: {mha(x,x,x).shape}")
238
+
239
+ d_ff = 2048
240
+ encoder_layer = EncoderLayer(d_emb=d_emb, n_heads=n_heads, d_ff=d_ff).to(device)
241
+ decoder_layer = DecoderLayer(d_emb=d_emb, n_heads=n_heads, d_ff=d_ff).to(device)
242
+ print(f"EncoderLayer output shape: {encoder_layer(x).shape}")
243
+
244
+ n_layers = 6
245
+ encoder = Encoder(n_layers=n_layers, d_emb=d_emb, n_heads=n_heads, d_ff=d_ff).to(device)
246
+ decoder = Decoder(n_layers=n_layers, d_emb=d_emb, n_heads=n_heads, d_ff=d_ff).to(device)
247
+ encoder.eval()
248
+ decoder.eval()
249
+ with torch.no_grad():
250
+ print(f"Encoder output shape: {encoder(x).shape}")
251
+ print(f"Decoder output shape: {decoder(x=x, context=x).shape}")
252
+ else:
253
+ print(f'Transformer imported from local file "{__name__}.py"')
simonbb/utils.py ADDED
@@ -0,0 +1,486 @@
1
+ """
2
+ A collection of utility functions for teaching deep learning
3
+ @author: huaxia
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import numpy as np
9
+ import copy
10
+ import re
11
+ import pickle
12
+ import json
13
+ from collections import Counter
14
+ import urllib.request
15
+
16
+ import matplotlib.pyplot as plt
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+
21
+ #%%
22
+
23
+ class TextVectorizer:
24
+ def __init__(self, max_tokens=20000, output_mode="int", output_sequence_length=None, ngrams=None, standardize=None):
25
+ self.max_tokens = max_tokens
26
+ self.output_mode = output_mode
27
+ self.output_sequence_length = output_sequence_length
28
+ self.ngrams = ngrams
29
+ self.standardize = standardize
30
+ self.vocab = None
31
+ self.word_to_idx = None
32
+ self.idx_to_word = None
33
+ self.idf_weights = None
34
+
35
+ def _tokenize(self, text):
36
+ if self.standardize:
37
+ text = self.standardize(text)
38
+ else:
39
+ text = text.lower()
40
+ text = re.sub(r'[^\w\s]', ' ', text)
41
+ tokens = text.split()
42
+ if self.ngrams and self.ngrams >= 2:
43
+ ngram_tokens = []
44
+ for n in range(1, self.ngrams + 1):
45
+ for i in range( len(tokens) - n + 1 ):
46
+ ngram_tokens.append(' '.join(tokens[i:i + n]))
47
+ return ngram_tokens
48
+ return tokens
49
+
50
+ def adapt(self, texts):
51
+ """Build vocabulary from texts"""
52
+ token_counter = Counter()
53
+ doc_freq = Counter() # for TF-IDF
54
+ num_docs = 0
55
+ print("Adapting TextVectorizer...")
56
+ for text in texts:
57
+ num_docs += 1
58
+ tokens = self._tokenize(text)
59
+ token_counter.update(tokens)
60
+ doc_freq.update(set(tokens))
61
+ # Reserve 0 for padding, 1 for unknown, hence minus 2 to account for them.
62
+ most_common = token_counter.most_common(self.max_tokens - 2 if self.max_tokens else None) # most_common(n=None) returns all elements in the counter if n is omitted or None.
63
+ self.vocab = ['<pad>', '<unk>'] + [word for word, _ in most_common] # 0 for padding, 1 for unknown
64
+ self.word_to_idx = {word: idx for idx, word in enumerate(self.vocab)}
65
+ self.idx_to_word = {idx: word for idx, word in enumerate(self.vocab)}
66
+ # Compute IDF weights for tf-idf mode
67
+ if self.output_mode == "tf_idf":
68
+ self.idf_weights = np.zeros(len(self.vocab))
69
+ for word, idx in self.word_to_idx.items():
70
+ df = doc_freq.get(word, 0) + 1 # add 1 for smoothing
71
+ self.idf_weights[idx] = np.log(num_docs / df) + 1
72
+ print(f"Vocabulary size: {len(self.vocab)}")
73
+
74
+ def __call__(self, texts):
75
+ if isinstance(texts, str):
76
+ texts = [texts]
77
+ if self.output_mode == "int":
78
+ return self._vectorize_int(texts)
79
+ elif self.output_mode == "multi_hot":
80
+ return self._vectorize_multi_hot(texts)
81
+ elif self.output_mode == "tf_idf":
82
+ return self._vectorize_tfidf(texts)
83
+
84
+ def _vectorize_int(self, texts):
85
+ batch_indices = []
86
+ for text in texts:
87
+ tokens = self._tokenize(text)
88
+ indices = [self.word_to_idx.get(t, 1) for t in tokens] # 1 is <unk>
89
+ if self.output_sequence_length:
90
+ if len(indices) < self.output_sequence_length:
91
+ indices = indices + [0] * (self.output_sequence_length - len(indices))
92
+ else:
93
+ indices = indices[:self.output_sequence_length]
94
+ batch_indices.append(indices)
95
+ return np.array(batch_indices)
96
+
97
+ def _vectorize_multi_hot(self, texts):
98
+ result = np.zeros( (len(texts), len(self.vocab)), dtype=np.float32 )
99
+ for i, text in enumerate(texts):
100
+ tokens = self._tokenize(text)
101
+ for t in tokens:
102
+ idx = self.word_to_idx.get(t, 1)
103
+ if idx < len(self.vocab):
104
+ result[i, idx] = 1
105
+ return result
106
+
107
+ def _vectorize_tfidf(self, texts):
108
+ result = np.zeros( (len(texts), len(self.vocab)), dtype=np.float32 )
109
+ for i, text in enumerate(texts):
110
+ tokens = self._tokenize(text)
111
+ tf = Counter(tokens)
112
+ for t, count in tf.items():
113
+ idx = self.word_to_idx.get(t, 1)
114
+ if idx < len(self.vocab):
115
+ result[i, idx] = count * self.idf_weights[idx]
116
+ return result
117
+
118
+ def get_vocabulary(self):
119
+ return self.vocab
120
+
121
+ def save(self, path):
122
+ vec = {'max_tokens': self.max_tokens, 'output_sequence_length': self.output_sequence_length,
123
+ 'vocab': self.vocab, 'word_to_idx': self.word_to_idx, 'idx_to_word': self.idx_to_word,
124
+ 'has_standardizer': self.standardize is not None}
125
+ with open(path, 'wb') as f:
126
+ pickle.dump(vec, f)
127
+
128
+ # transforms a method into a class method (i.e., bound to the class rather than an individual instance)
129
+ @classmethod
130
+ def load(cls, path, standardize=None):
131
+ data = pickle.load(path, weights_only=False)
132
+ vectorizer = cls( max_tokens=data['max_tokens'], output_sequence_length=data['output_sequence_length'],
133
+ standardize=standardize if data.get('has_standardize') else None )
134
+ vectorizer.vocab = data['vocab']
135
+ vectorizer.word_to_idx = data['word_to_idx']
136
+ vectorizer.idx_to_word = data['idx_to_word']
137
+ return vectorizer
138
+
139
+
140
+ def query_ollama(prompt, model="llama3", url="http://localhost:11434/api/chat"):
141
+ # Create the data payload as a dictionary
142
+ data = {
143
+ "model": model,
144
+ "messages": [
145
+ {"role": "user", "content": prompt}
146
+ ],
147
+ "options": { # Settings below are required for deterministic responses
148
+ "seed": 123,
149
+ "temperature": 0,
150
+ "num_ctx": 2048
151
+ }
152
+ }
153
+
154
+ # Convert the dictionary to a JSON formatted string and encode it to bytes
155
+ payload = json.dumps(data).encode("utf-8")
156
+
157
+ # Create a request object, setting the method to POST and adding necessary headers
158
+ request = urllib.request.Request(url, data=payload, method="POST")
159
+ request.add_header("Content-Type", "application/json")
160
+
161
+ # Send the request and capture the response
162
+ response_data = ""
163
+ with urllib.request.urlopen(request) as response:
164
+ # Read and decode the response
165
+ while True:
166
+ line = response.readline().decode("utf-8")
167
+ if not line:
168
+ break
169
+ response_json = json.loads(line)
170
+ response_data += response_json["message"]["content"]
171
+
172
+ return response_data
173
+
174
+ #%%
175
+
176
+ class ModelCheckpoint:
177
+ def __init__(self, filepath, monitor="val_loss", save_best_only=True, save_optimizer_state=False):
178
+ self.filepath = filepath
179
+ self.monitor = monitor
180
+ self.save_best_only = save_best_only
181
+ self.save_optimizer_state = save_optimizer_state
182
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
183
+
184
+ def on_best(self, model, optimizer):
185
+ if self.save_optimizer_state:
186
+ torch.save( {"model_state_dict": model.state_dict(), "optimizer_state_dict": optimizer.state_dict()}, self.filepath)
187
+ else:
188
+ torch.save(model.state_dict(), self.filepath)
189
+
190
+
191
+ def plotEpoch(history, metric="average_loss"):
192
+ epochs = range(len(history["average_train_loss"]))
193
+ if metric == "accuracy":
194
+ plt.plot(epochs, history["train_accuracy"], "k", label="Training acc")
195
+ plt.plot(epochs, history["val_accuracy"], "b", label="Validation acc")
196
+ plt.axvline( x=int(np.argmax(history["val_accuracy"])), color="0.5" )
197
+ plt.title("Training and Validation Accuracy")
198
+ plt.legend()
199
+ elif metric == "total_loss":
200
+ plt.plot(epochs, history["total_train_loss"], "k", label="Training loss")
201
+ plt.plot(epochs, history["total_val_loss"], "b", label="Validation loss")
202
+ plt.axvline(x=int(np.argmin(history["total_val_loss"])), color="0.5")
203
+ plt.title("Total Training and Validation Loss")
204
+ plt.legend()
205
+ elif metric == "average_loss":
206
+ plt.plot(epochs, history["average_train_loss"], "k", label="Training loss")
207
+ plt.plot(epochs, history["average_val_loss"], "b", label="Validation loss")
208
+ plt.axvline(x=int(np.argmin(history["average_val_loss"])), color="0.5")
209
+ plt.title("Average Training and Validation Loss")
210
+ plt.legend()
211
+ plt.tight_layout()
212
+ plt.show()
213
+ plt.close()
214
+
215
+
216
+ def stats_per_channel(loader):
217
+ n_channels, _, _ = loader.dataset[0][0].size() # assuming each batch is a tuple of (x,y)
218
+ #total_samples = torch.ones( (1, n_channels) ) * len(loader.dataset)
219
+ total_means = torch.zeros( (1,n_channels) )
220
+ total_stds = torch.zeros( (1,n_channels) )
221
+ for images, _ in loader:
222
+ flatten_per_channel = torch.flatten(images, start_dim=-2, end_dim=-1) # (N,C,H*W)
223
+ means = flatten_per_channel.mean(axis=2) # (N,C)
224
+ stds = flatten_per_channel.std(axis=2) # (N,C)
225
+ total_means += means.sum(axis=0) # (1, n_channels)
226
+ total_stds += stds.sum(axis=0) # (1, n_channels)
227
+ norm_mean = total_means / len(loader.dataset) # (1, n_channels)
228
+ norm_std = total_stds / len(loader.dataset) # (1, n_channels)
229
+ return norm_mean.squeeze(), norm_std.squeeze() # without squeezing, runtime error: not all boolean value of tensor with more than one value is ambiguous
230
+
231
+ #%% Train Loop: tested on classification, regression, image segmentation, translation, gpt2
232
+
233
+ def train( model, train_dataloader, val_dataloader, optimizer, scheduler=None, clip_grad=None, loss_fn=nn.CrossEntropyLoss(), callbacks=None, device=None, evaluation=True, n_epochs=1, n_batch_per_report=5, accuracy_fn=None, lr_history=None ):
234
+ if device is None:
235
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
236
+ model.to(device) # Models are moved in-place (the object itself is modified)
237
+
238
+ batch = next(iter(train_dataloader))
239
+ y_shape = batch[-1].squeeze().shape
240
+ # y_shape is typically a scalar for scalar label, 1D tensor for sequence, and 2D for image segmentation task where the label is a 2D tensor representing an image
241
+ if len(y_shape)==1:
242
+ n_labels_per_instance = 1 # typical case
243
+ elif len(y_shape)==2: # (batch, seq_len) for sequence data
244
+ n_labels_per_instance = y_shape[1]
245
+ elif len(y_shape)==3: # (batch, height, width) for image segmentation task
246
+ n_labels_per_instance = y_shape[1] * y_shape[2]
247
+ else:
248
+ print( f"Error: Targe tensors with {len(y_shape)} or more axes are currently not supported!" )
249
+
250
+ history = { "total_train_loss":[], "average_train_loss":[], "train_accuracy":[], "total_val_loss":[], "average_val_loss":[], "val_accuracy":[] }
251
+ best_val_loss = float("inf")
252
+ best_state = None
253
+
254
+ print(f'Start training on device {device}.\n')
255
+ for epoch in range(1, n_epochs + 1):
256
+ if isinstance(loss_fn, nn.MSELoss):
257
+ print(f"{'Epoch':^7} | {'Batch':^7} | {'Train Loss':^12} | {'Val Loss':^10} | {'Train MAE':^9} | {'Val MAE':^9} | {'Elapsed':^9}")
258
+ else:
259
+ print(f"{'Epoch':^7} | {'Batch':^7} | {'Train Loss':^12} | {'Val Loss':^10} | {'Train Acc':^9} | {'Val Acc':^9} | {'Elapsed':^9}")
260
+ print("-" * 80)
261
+
262
+ model.train()
263
+ t0_epoch, t0_batch = time.time(), time.time()
264
+ total_correct, total_loss, total_abs_err, weighted_accuracy, batch_loss, batch_count = 0, 0, 0, 0, 0, 0
265
+
266
+ for batch in train_dataloader:
267
+ batch_count += 1
268
+
269
+ if isinstance(loss_fn, nn.MSELoss): # regression
270
+ y = batch[-1].to(device)
271
+ else: # classification task
272
+ y = batch[-1].long().to(device) # convert to long (default integer type in PyTorch), required by nn.CrossEntropyLoss()
273
+
274
+ if len(batch)>2: # example: batch is a triple (sequence, attention mask, label)
275
+ x = tuple(t.to(device) for t in batch[:-1])
276
+ else:
277
+ x = batch[0].to(device)
278
+
279
+ optimizer.zero_grad(set_to_none=True)
280
+
281
+ logits = model(x).squeeze() # squeeze for regression; otherwise, broadcasting amplifies loss and abs_err
282
+
283
+ if len(y_shape)==1 or len(y_shape)==3: # scalar target or 2D image target
284
+ loss = loss_fn( logits, y )
285
+ elif len(y_shape)==2: # sequence target
286
+ loss = loss_fn( logits.flatten(0, 1), y.flatten() ) # pred: (batch * seq_len, vocab_size), label: (batch * seq_len,)
287
+ else:
288
+ print( f"Error: Targe tensors with {len(y_shape)} or more axes are currently not supported!" )
289
+
290
+ batch_loss = loss.item() # item() automatically moves a single-element tensor's value to the CPU and returns it as a standard Python number.
291
+ total_loss += batch_loss * y.shape[0] # Default for CrossEntropyLoss is 'mean' which averages over each loss element in the batch
292
+
293
+ if isinstance(loss_fn, nn.MSELoss):
294
+ abs_err = torch.sum( torch.abs(logits-y) ).item()
295
+ total_abs_err += abs_err
296
+ batch_accuracy = abs_err / y.shape[0]
297
+ elif accuracy_fn is None:
298
+ if len(y_shape)==1 or len(y_shape)==3: # scalar target or 2D image target
299
+ preds = torch.argmax(logits, dim=1)
300
+ elif len(y_shape)==2:
301
+ preds = torch.argmax(logits, dim=2) # sequence target
302
+ else:
303
+ print( f"Error: Targe tensors with {len(y_shape)} or more axes are currently not supported!" )
304
+ batch_accuracy = (preds == y).cpu().numpy().mean() * 100
305
+ total_correct += (preds == y).sum().item() # item() automatically moves a single-element tensor's value to the CPU and returns it as a standard Python number.
306
+ else:
307
+ batch_accuracy = accuracy_fn( logits, y ).item() * 100
308
+ weighted_accuracy += batch_accuracy * len(y)
309
+
310
+ loss.backward()
311
+
312
+ if clip_grad is not None: # gradient clipping prevents exploding gradients
313
+ torch.nn.utils.clip_grad_norm_(model.parameters(), clip_grad)
314
+
315
+ optimizer.step() # Update parameters
316
+ if scheduler is not None:
317
+ scheduler.step() # Update learning rate
318
+ if lr_history is not None:
319
+ lr_history.append( optimizer.param_groups[0]['lr'] )
320
+
321
+ if batch_count % n_batch_per_report == 0:
322
+ time_elapsed = time.time() - t0_batch
323
+ print( f"{epoch:^7} | {batch_count:^7} | {batch_loss:^12.6f} | {'-':^10} | {batch_accuracy:^9.2f} | {'-':^9} | {time_elapsed:^9.2f}")
324
+
325
+ batch_loss = 0
326
+ t0_batch = time.time()
327
+
328
+ if n_batch_per_report < batch_count:
329
+ print("-" * 80)
330
+
331
+ history["total_train_loss"].append( total_loss )
332
+ history['average_train_loss'].append( history["total_train_loss"][-1] / len(train_dataloader.dataset) )
333
+
334
+ if isinstance(loss_fn, nn.MSELoss):
335
+ history["train_accuracy"].append( total_abs_err / len(train_dataloader.dataset) )
336
+ elif accuracy_fn is None: # scalar target (1), sequence prediction (2), image segmentation (3)
337
+ history["train_accuracy"].append( 100 * total_correct / ( len(train_dataloader.dataset)*n_labels_per_instance ) )
338
+ else:
339
+ history["train_accuracy"].append( weighted_accuracy / len(train_dataloader.dataset) ) # batch-size-weighted accuracy
340
+
341
+ if evaluation == True:
342
+ model.eval()
343
+ val_correct, val_loss, total_abs_err, weighted_accuracy = 0, 0, 0, 0
344
+ for batch in val_dataloader:
345
+ if isinstance(loss_fn, nn.MSELoss): # regression task
346
+ y = batch[-1].to(device)
347
+ else: # classification task
348
+ y = batch[-1].long().to(device) # convert to long (default integer type in PyTorch), required by nn.CrossEntropyLoss()
349
+ if len(batch)>2: # example: (sequence, attention mask)
350
+ x = tuple(t.to(device) for t in batch[:-1])
351
+ else:
352
+ x = batch[0].to(device)
353
+
354
+ with torch.no_grad(): # switch off autograd using the torch.no_grad() context manager
355
+ logits = model( x ).squeeze() # squeeze for regression; otherwise, broadcasting amplifies loss and abs_err
356
+
357
+ if len(y_shape)==1 or len(y_shape)==3: # scalar target or 2D image target
358
+ loss = loss_fn( logits, y )
359
+ elif len(y_shape)==2: # sequence target
360
+ loss = loss_fn( logits.flatten(0, 1), y.flatten() ) # pred: (batch * seq_len, vocab_size), label: (batch * seq_len,)
361
+ else:
362
+ print( f"Error: Targe tensors with {len(y_shape)} or more axes are currently not supported!" )
363
+
364
+ val_loss += loss.item() * len(y) # Default for CrossEntropyLoss is 'mean' which averages over each loss element in the batch
365
+
366
+ if isinstance(loss_fn, nn.MSELoss):
367
+ total_abs_err += torch.sum( torch.abs(logits-y) ).item()
368
+ elif accuracy_fn is None:
369
+ if len(y_shape)==1 or len(y_shape)==3: # scalar target or 2D image target
370
+ preds = torch.argmax(logits, dim=1)
371
+ elif len(y_shape)==2:
372
+ preds = torch.argmax(logits, dim=2) # sequence target
373
+ else:
374
+ print( f"Error: Targe tensors with {len(y_shape)} or more axes are currently not supported!" )
375
+ val_correct += (preds == y).cpu().numpy().sum().item()
376
+ else:
377
+ batch_accuracy = accuracy_fn(logits, y).item()
378
+ weighted_accuracy += batch_accuracy * len(y)
379
+
380
+ history["total_val_loss"].append( val_loss )
381
+ history['average_val_loss'].append( history["total_val_loss"][-1] / len(val_dataloader.dataset) )
382
+
383
+ if isinstance(loss_fn, nn.MSELoss):
384
+ history["val_accuracy"].append( total_abs_err / len(val_dataloader.dataset) )
385
+ elif accuracy_fn is None: # scalar target (1), sequence prediction (2), image segmentation (3)
386
+ history["val_accuracy"].append( 100 * val_correct / (len(val_dataloader.dataset)*n_labels_per_instance) )
387
+ else:
388
+ history["val_accuracy"].append( 100 * weighted_accuracy / len(val_dataloader.dataset) ) # batch-size-weighted accuracy
389
+
390
+ time_elapsed = time.time() - t0_epoch
391
+ print( f"{epoch:^7} | {'-':^7} | {history['average_train_loss'][-1]:^12.6f} | {history['average_val_loss'][-1]:^10.6f} | {history['train_accuracy'][-1]:^9.2f} | {history['val_accuracy'][-1]:^9.2f} | {time_elapsed:^9.2f}")
392
+ # ModelCheckpoint(save_best_only=True, monitor="val_loss")
393
+ if val_loss < best_val_loss:
394
+ best_val_loss = val_loss
395
+ best_state = copy.deepcopy( model.state_dict() )
396
+ optimizer_state = copy.deepcopy( optimizer.state_dict() )
397
+ if callbacks:
398
+ for cb in callbacks:
399
+ cb.on_best(model, optimizer)
400
+ else:
401
+ print( f"{epoch:^7} | {'-':^7} | {history['total_train_loss'][-1]:^12.6f} | {'-':^10} | {history['train_accuracy'][-1]:^9.2f} | {'-':^10} | {time_elapsed:^9.2f}")
402
+ print("-" * 80)
403
+ # restore best, similar to Keras's best-checkpoint behavior
404
+ if best_state is not None:
405
+ model.load_state_dict(best_state)
406
+ optimizer.load_state_dict(optimizer_state)
407
+ return history
408
+
409
+
410
+ def evaluate(model, loader, device, loss_fn=nn.CrossEntropyLoss(), accuracy_fn=None):
411
+ model.eval()
412
+ batch = next(iter(loader))
413
+ y_shape = batch[-1].squeeze().shape
414
+ if len(y_shape)==1:
415
+ n_labels_per_instance = 1 # typical case
416
+ elif len(y_shape)==2: # (batch, seq_len) for sequence data
417
+ n_labels_per_instance = y_shape[1]
418
+ elif len(y_shape)==3: # (batch, height, width) for image segmentation task
419
+ n_labels_per_instance = y_shape[1] * y_shape[2]
420
+ else:
421
+ print( f"Error: Targe tensors with {len(y_shape)} or more axes are currently not supported!" )
422
+
423
+ val_correct, val_loss, total_abs_err, weighted_accuracy = 0,0,0,0
424
+ for batch in loader:
425
+ if isinstance(loss_fn, nn.MSELoss): # regression task
426
+ y = batch[-1].to(device)
427
+ else: # classification task
428
+ y = batch[-1].long().to(device) # convert to long (default integer type in PyTorch), required by nn.CrossEntropyLoss()
429
+ if len(batch)>2: # example: (sequence, attention mask)
430
+ x = tuple(t.to(device) for t in batch[:-1])
431
+ else:
432
+ x = batch[0].to(device)
433
+
434
+ with torch.no_grad(): # switch off autograd using the torch.no_grad() context manager
435
+ logits = model(x).squeeze() # squeeze for regression; otherwise, broadcasting amplifies loss and abs_err
436
+
437
+ if len(y_shape)==1 or len(y_shape)==3: # scalar target or 2D image target
438
+ loss = loss_fn( logits, y )
439
+ elif len(y_shape)==2: # sequence target
440
+ loss = loss_fn( logits.flatten(0, 1), y.flatten() ) # pred: (batch * seq_len, vocab_size), label: (batch * seq_len,)
441
+ else:
442
+ print( f"Error: Targe tensors with {len(y_shape)} or more axes are currently not supported!" )
443
+
444
+ val_loss += loss.item() * y.shape[0]
445
+
446
+ if isinstance(loss_fn, nn.MSELoss):
447
+ total_abs_err += torch.sum( torch.abs(logits-y) ).item()
448
+ elif accuracy_fn is None:
449
+ if len(y_shape)==1 or len(y_shape)==3: # scalar target or 2D image target
450
+ preds = torch.argmax(logits, dim=1)
451
+ elif len(y_shape)==2:
452
+ preds = torch.argmax(logits, dim=2) # sequence target: (batch, seq_len, vocab_size)
453
+ else:
454
+ print( f"Error: Targe tensors with {len(y_shape)} or more axes are currently not supported!" )
455
+ val_correct += (preds == y).cpu().numpy().sum()
456
+ else:
457
+ batch_accuracy = accuracy_fn(logits, y).item()
458
+ weighted_accuracy += batch_accuracy * len(y)
459
+
460
+ val_loss = val_loss / len(loader.dataset)
461
+
462
+ if isinstance(loss_fn, nn.MSELoss):
463
+ val_accuracy = total_abs_err / len(loader.dataset)
464
+ elif accuracy_fn is None:
465
+ val_accuracy = val_correct/ (len(loader.dataset)**n_labels_per_instance)
466
+ else:
467
+ val_accuracy = 100 * weighted_accuracy / len(loader.dataset) # batch-size-weighted accuracy
468
+
469
+ if torch.is_tensor(val_accuracy):
470
+ val_accuracy = val_accuracy.item() # extract the value of a single-item tensor
471
+ return {'loss':val_loss, 'accuracy':val_accuracy}
472
+
473
+ #%%
474
+
475
+ if __name__ == "__main__":
476
+
477
+ vectorizer = TextVectorizer( max_tokens=10, output_mode="int" )
478
+ txts = ['how are you']
479
+ vectorizer.adapt( txts )
480
+ encoded = vectorizer(txts)
481
+ print(encoded)
482
+ print(vectorizer.vocab)
483
+ print(vectorizer.word_to_idx)
484
+ print(vectorizer.idx_to_word)
485
+ else:
486
+ print(f'Importing from local file "{__name__}.py"')
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: simonbb
3
+ Version: 0.0.1
4
+ Summary: A small package for teaching deep learning
5
+ Project-URL: Homepage, https://simonbb.com
6
+ Project-URL: Issues, https://simonbb.com/issues
7
+ Author-email: Huaxia Rui <huaxia.rui@simon.rochester.edu>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+
15
+ # What's this package for?
16
+ Anyone who teaches deep learning using PyTorch but doesn't want to bother students with too much nitty-gritty details.
@@ -0,0 +1,9 @@
1
+ simonbb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ simonbb/bert.py,sha256=Q98JElMljoDf24Dx-VWB-1HUat214uYJ08B5Q-EbHI4,2067
3
+ simonbb/gpt.py,sha256=gPucu8Osc4wQ28Hgy_gZqbRT45L7YYU1qqFoVptRrXY,10781
4
+ simonbb/transformer.py,sha256=7QMqBkX3og5XWSI_g32Tjel8d5f84VtyjGpBSpYZd3I,12030
5
+ simonbb/utils.py,sha256=TyuGbMsborIVJIp0muYSUn_CttUGdMSDte3BF8uSEgY,23690
6
+ simonbb-0.0.1.dist-info/METADATA,sha256=34-qm0i45eTASiAP8la-TDWuLipTIHyuMuwFvyHGikg,603
7
+ simonbb-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ simonbb-0.0.1.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
9
+ simonbb-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.