simonbb 0.0.1__tar.gz
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-0.0.1/LICENSE +19 -0
- simonbb-0.0.1/PKG-INFO +16 -0
- simonbb-0.0.1/README.md +2 -0
- simonbb-0.0.1/pyproject.toml +23 -0
- simonbb-0.0.1/src/simonbb/__init__.py +0 -0
- simonbb-0.0.1/src/simonbb/bert.py +39 -0
- simonbb-0.0.1/src/simonbb/gpt.py +204 -0
- simonbb-0.0.1/src/simonbb/transformer.py +253 -0
- simonbb-0.0.1/src/simonbb/utils.py +486 -0
simonbb-0.0.1/LICENSE
ADDED
|
@@ -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.
|
simonbb-0.0.1/PKG-INFO
ADDED
|
@@ -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.
|
simonbb-0.0.1/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling >= 1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "simonbb"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Huaxia Rui", email="huaxia.rui@simon.rochester.edu" },
|
|
10
|
+
]
|
|
11
|
+
description = "A small package for teaching deep learning"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
license = "MIT"
|
|
19
|
+
license-files = ["LICEN[CS]E*"]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://simonbb.com"
|
|
23
|
+
Issues = "https://simonbb.com/issues"
|
|
File without changes
|
|
@@ -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 )
|
|
@@ -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"')
|
|
@@ -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"')
|
|
@@ -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"')
|