noch inator commited on
Commit
ba85643
·
verified ·
1 Parent(s): 4fcd291

Upload ThoughtVectors.py

Browse files
Files changed (1) hide show
  1. ThoughtVectors.py +762 -0
ThoughtVectors.py ADDED
@@ -0,0 +1,762 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # thought_vectors.py
2
+ import math
3
+ import random
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.optim as optim
7
+ import sentencepiece as spm
8
+ from torch.utils.data import Dataset, DataLoader, random_split
9
+ from typing import List, Union, Optional, Iterator
10
+ import os
11
+ import tempfile
12
+ import tarfile
13
+ import shutil
14
+
15
+
16
+ class PositionalEncoding(nn.Module):
17
+ """Adds positional information to token embeddings using sine and cosine functions.
18
+
19
+ This module can either precompute positional encodings up to a specified `max_len` or compute
20
+ them dynamically based on the input sequence length. If `max_len` is 0, encodings are computed
21
+ on-the-fly in the forward pass; otherwise, they are precomputed during initialization.
22
+
23
+ Args:
24
+ d_model (int): Dimensionality of the model embeddings.
25
+ dropout (float, optional): Dropout probability applied after adding encodings. Defaults to 0.1.
26
+ max_len (int, optional): Maximum sequence length for precomputed encodings; if 0, computes
27
+ dynamically. Defaults to 5000.
28
+
29
+ Attributes:
30
+ dropout (nn.Dropout): Dropout layer for regularization.
31
+ pe (torch.Tensor, optional): Precomputed positional encodings, shape (1, max_len, d_model),
32
+ present only if max_len > 0.
33
+ """
34
+
35
+ def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000):
36
+ super().__init__()
37
+ self.d_model = d_model
38
+ self.dropout = nn.Dropout(dropout)
39
+ self.max_len = max_len
40
+
41
+ if max_len > 0:
42
+ # Precompute positional encodings
43
+ pe = torch.zeros(max_len, d_model)
44
+ position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
45
+ # Adjust div_term for odd d_model
46
+ div_term = torch.exp(torch.arange(0, d_model - (d_model % 2), 2, dtype=torch.float) *
47
+ (-math.log(10000.0) / d_model))
48
+
49
+ pe[:, 0::2] = torch.sin(position * div_term)
50
+ pe[:, 1::2] = torch.cos(position * div_term[:d_model // 2 + 1] if d_model % 2 else div_term)
51
+
52
+ pe = pe.unsqueeze(0)
53
+ self.register_buffer('pe', pe)
54
+ else:
55
+ self.pe = None
56
+
57
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
58
+ """Applies positional encodings to the input tensor.
59
+
60
+ Args:
61
+ x (torch.Tensor): Input embeddings, shape (batch_size, seq_len, d_model).
62
+
63
+ Returns:
64
+ torch.Tensor: Embeddings with positional encodings added and dropout applied.
65
+
66
+ Raises:
67
+ ValueError: If max_len > 0 and seq_len exceeds max_len.
68
+ """
69
+ seq_len = x.size(1)
70
+
71
+ if self.max_len > 0:
72
+ if seq_len > self.max_len:
73
+ raise ValueError(f"Sequence length {seq_len} exceeds max_len {self.max_len}")
74
+ pe = self.pe[:, :seq_len]
75
+ else:
76
+ pe = torch.zeros(seq_len, self.d_model, device=x.device)
77
+ position = torch.arange(0, seq_len, dtype=torch.float, device=x.device).unsqueeze(1)
78
+ div_term = torch.exp(torch.arange(0, self.d_model - (self.d_model % 2), 2, dtype=torch.float,
79
+ device=x.device) * (-math.log(10000.0) / self.d_model))
80
+
81
+ pe[:, 0::2] = torch.sin(position * div_term)
82
+ pe[:, 1::2] = torch.cos(position * div_term[:self.d_model // 2 + 1] if self.d_model % 2 else div_term)
83
+
84
+ pe = pe.unsqueeze(0)
85
+
86
+ x = x + pe
87
+ return self.dropout(x)
88
+
89
+
90
+ class ThoughtEncoder(nn.Module):
91
+ """Encodes text into thought vectors, representing abstract ideas or summaries.
92
+
93
+ Args:
94
+ vocab_size (int): Size of the vocabulary for token embeddings.
95
+ d_model (int, optional): Dimensionality of the model embeddings. Defaults to 256.
96
+ max_thoughts (int, optional): Maximum number of thought vectors to generate. Defaults to 16.
97
+ nhead (int, optional): Number of attention heads in the transformer encoder. Defaults to 8.
98
+ num_layers (int, optional): Number of transformer encoder layers. Defaults to 2.
99
+ dropout (float, optional): Dropout probability. Defaults to 0.1.
100
+ pad_id (int, optional): ID for padding tokens. Defaults to 0.
101
+ termination_threshold (float, optional): Threshold for stopping thought vector generation.
102
+ Defaults to 0.75.
103
+ """
104
+
105
+ def __init__(self, vocab_size: int, d_model: int = 256, max_thoughts: int = 16,
106
+ nhead: int = 8, num_layers: int = 2, dropout: float = 0.1, pad_id: int = 0,
107
+ termination_threshold: float = 0.75):
108
+ super().__init__()
109
+ assert nhead % 2 == 0, "nhead must be even for thought_attention"
110
+ self.d_model = d_model
111
+ self.max_thoughts = max_thoughts
112
+ self.pad_id = pad_id
113
+ self.termination_threshold = termination_threshold
114
+ self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=pad_id)
115
+ self.positional_encoding = PositionalEncoding(d_model, dropout)
116
+ encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead, dropout=dropout, batch_first=True)
117
+ self.text_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
118
+ self.thought_attention = nn.MultiheadAttention(d_model, num_heads=nhead // 2, dropout=dropout, batch_first=True)
119
+ self.thought_rnn = nn.GRU(d_model, d_model, batch_first=True)
120
+ self.fc_thought = nn.Linear(2 * d_model, d_model)
121
+ self.fc_terminate = nn.Linear(d_model, 1)
122
+
123
+ def forward(self, input_tokens: torch.Tensor, force_single_vector: bool = False) -> torch.Tensor:
124
+ """Encodes input tokens into a sequence of thought vectors.
125
+
126
+ Args:
127
+ input_tokens (torch.Tensor): Token IDs, shape (batch_size, seq_len).
128
+ force_single_vector (bool, optional): If True, generates only one thought vector.
129
+ Defaults to False.
130
+
131
+ Returns:
132
+ torch.Tensor: Thought vectors, shape (batch_size, num_thoughts, d_model).
133
+ """
134
+ batch_size, seq_len = input_tokens.size()
135
+ max_thoughts = 1 if force_single_vector else self.max_thoughts
136
+
137
+ src_key_padding_mask = (input_tokens == self.pad_id)
138
+ x = self.embedding(input_tokens)
139
+ x = self.positional_encoding(x)
140
+ encoded_text = self.text_encoder(x, src_key_padding_mask=src_key_padding_mask)
141
+
142
+ valid_mask = (~src_key_padding_mask).unsqueeze(-1).float()
143
+ text_context = (encoded_text * valid_mask).sum(dim=1) / valid_mask.sum(dim=1).clamp(min=1.0)
144
+
145
+ thought_vectors_list = []
146
+ thought_hidden = text_context.unsqueeze(0)
147
+ finished = torch.zeros(batch_size, dtype=torch.bool, device=input_tokens.device)
148
+
149
+ for t in range(max_thoughts):
150
+ if t > 0:
151
+ prev_thoughts = torch.stack(thought_vectors_list, dim=1)
152
+ attn_output, _ = self.thought_attention(prev_thoughts, prev_thoughts, prev_thoughts)
153
+ prev_context = attn_output.mean(dim=1)
154
+ else:
155
+ prev_context = torch.zeros_like(text_context)
156
+
157
+ combined = torch.cat([text_context, prev_context], dim=1)
158
+ next_thought = self.fc_thought(combined)
159
+
160
+ thought_hidden = self.thought_rnn(next_thought.unsqueeze(1), thought_hidden)[1]
161
+ termination_logit = self.fc_terminate(thought_hidden.squeeze(0))
162
+ termination_score = torch.sigmoid(termination_logit).squeeze(-1)
163
+
164
+ thought_vectors_list.append(next_thought)
165
+ finished = finished | (termination_score > self.termination_threshold)
166
+ if finished.all():
167
+ break
168
+
169
+ return torch.stack(thought_vectors_list, dim=1)
170
+
171
+
172
+ class ThoughtDecoder(nn.Module):
173
+ """Decodes thought vectors back into token sequences using a transformer decoder.
174
+
175
+ Args:
176
+ vocab_size (int): Size of the vocabulary for token embeddings.
177
+ d_model (int, optional): Dimensionality of the model embeddings. Defaults to 256.
178
+ num_layers (int, optional): Number of transformer decoder layers. Defaults to 2.
179
+ nhead (int, optional): Number of attention heads in the transformer decoder. Defaults to 8.
180
+ dropout (float, optional): Dropout probability. Defaults to 0.1.
181
+ """
182
+
183
+ def __init__(self, vocab_size: int, d_model: int = 256, num_layers: int = 2,
184
+ nhead: int = 8, dropout: float = 0.1):
185
+ super().__init__()
186
+ self.vocab_size = vocab_size
187
+ self.d_model = d_model
188
+ self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
189
+ self.positional_encoding = PositionalEncoding(d_model, dropout)
190
+ decoder_layer = nn.TransformerDecoderLayer(d_model=d_model, nhead=nhead, dropout=dropout, batch_first=True)
191
+ self.transformer_decoder = nn.TransformerDecoder(decoder_layer, num_layers=num_layers)
192
+ self.fc_out = nn.Linear(d_model, vocab_size)
193
+
194
+ def generate_square_subsequent_mask(self, sz: int, device: torch.device) -> torch.Tensor:
195
+ """Generates a causal mask for autoregressive decoding.
196
+
197
+ Args:
198
+ sz (int): Size of the mask (sequence length).
199
+ device (torch.device): Device to create the mask on.
200
+
201
+ Returns:
202
+ torch.Tensor: Mask tensor, shape (sz, sz).
203
+ """
204
+ mask = (torch.triu(torch.ones(sz, sz, device=device)) == 1).transpose(0, 1)
205
+ mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
206
+ return mask
207
+
208
+ def forward(self, thought_vectors: torch.Tensor, target_tokens: torch.Tensor) -> torch.Tensor:
209
+ """Decodes thought vectors into token logits.
210
+
211
+ Args:
212
+ thought_vectors (torch.Tensor): Thought vectors, shape (batch_size, num_thoughts, d_model).
213
+ target_tokens (torch.Tensor): Target token IDs, shape (batch_size, tgt_seq_len).
214
+
215
+ Returns:
216
+ torch.Tensor: Logits over vocabulary, shape (batch_size, tgt_seq_len, vocab_size).
217
+ """
218
+ tgt_emb = self.embedding(target_tokens)
219
+ tgt_emb = self.positional_encoding(tgt_emb)
220
+ tgt_mask = self.generate_square_subsequent_mask(tgt_emb.size(1), tgt_emb.device)
221
+ output = self.transformer_decoder(tgt=tgt_emb, memory=thought_vectors, tgt_mask=tgt_mask)
222
+ return self.fc_out(output)
223
+
224
+
225
+ class GroupTextDataset(Dataset):
226
+ """Dataset class for handling groups of text sentences.
227
+
228
+ Args:
229
+ groups (List[List[str]]): List of sentence groups.
230
+ sp (spm.SentencePieceProcessor): SentencePiece processor for tokenization.
231
+ """
232
+
233
+ def __init__(self, groups: List[List[str]], sp: spm.SentencePieceProcessor):
234
+ self.groups = groups
235
+ self.sp = sp
236
+
237
+ def __len__(self) -> int:
238
+ return len(self.groups)
239
+
240
+ def __getitem__(self, idx: int) -> List[List[int]]:
241
+ group = self.groups[idx]
242
+ tokenized = []
243
+ for sentence in group:
244
+ token_ids = [self.sp.bos_id()] + self.sp.EncodeAsIds(sentence) + [self.sp.eos_id()]
245
+ tokenized.append(token_ids)
246
+ return tokenized
247
+
248
+
249
+ def group_collate_fn(batch: List[List[List[int]]]) -> List[torch.Tensor]:
250
+ """Collates a batch of tokenized groups into padded tensors.
251
+
252
+ Args:
253
+ batch (List[List[List[int]]]): Batch of tokenized groups.
254
+
255
+ Returns:
256
+ List[torch.Tensor]: Padded tensors for each group, shape (num_sentences, max_seq_len).
257
+ """
258
+ collated_groups = []
259
+ for group in batch:
260
+ if not group:
261
+ collated_groups.append(torch.tensor([]))
262
+ continue
263
+ group_tensors = [torch.tensor(seq, dtype=torch.long) for seq in group]
264
+ padded_group = nn.utils.rnn.pad_sequence(group_tensors, batch_first=True, padding_value=0)
265
+ collated_groups.append(padded_group)
266
+ return collated_groups
267
+
268
+
269
+ class ThoughtVectors:
270
+ """Main class for a thought vector model, encoding text into latent vectors and decoding them back.
271
+
272
+ Attributes:
273
+ device (torch.device): Computation device (CUDA if available, else CPU).
274
+ encoder (ThoughtEncoder): Encoder module.
275
+ decoder (ThoughtDecoder): Decoder module.
276
+ sp (spm.SentencePieceProcessor): SentencePiece processor.
277
+ """
278
+
279
+ def __init__(self):
280
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
281
+ self.encoder = None
282
+ self.decoder = None
283
+ self.vocab_size = None
284
+ self.sp = None
285
+ self.d_model = None
286
+ self.max_thoughts = None
287
+ self.encoder_nhead = None
288
+ self.decoder_nhead = None
289
+ self.encoder_layers = None
290
+ self.decoder_layers = None
291
+ self.dropout = None
292
+ self.max_len = None
293
+ self.termination_threshold = None
294
+
295
+
296
+ def train(self,
297
+ group_data: Union[List[List[str]], Iterator[List[str]]],
298
+ model: str = None,
299
+ num_epochs: int = 30,
300
+ batch_size: int = 1,
301
+ val_split: float = 0.8,
302
+ learning_rate: float = 2e-4,
303
+ weight_decay: float = 1e-5,
304
+ length_penalty: float = 0.01,
305
+ single_vector_prob: float = 0.1,
306
+ save_path: str = "thought_vectors.tar",
307
+ spm_model_prefix: str = "spm",
308
+ vocab_size: int = 2000,
309
+ d_model: int = 256,
310
+ encoder_nhead: int = 8,
311
+ decoder_nhead: int = 8,
312
+ encoder_layers: int = 2,
313
+ decoder_layers: int = 2,
314
+ max_thoughts: int = 16,
315
+ dropout: float = 0.1,
316
+ max_len: int = 5000,
317
+ termination_threshold: float = 0.75,
318
+ patience: int = 5) -> None:
319
+ """Trains the thought vector model on grouped text data.
320
+
321
+ Args:
322
+ group_data (Union[List[List[str]], Iterator[List[str]]]): Either a list of sentence groups or a generator yielding sentence groups for training.
323
+ model (str): Path to pre-existing model to load. If None, creates a new model. Defaults to None
324
+ num_epochs (int, optional): Number of training epochs. Defaults to 30.
325
+ batch_size (int, optional): Batch size. Defaults to 1.
326
+ val_split (float, optional): Fraction of data for training. Defaults to 0.8.
327
+ learning_rate (float, optional): Learning rate for Adam optimizer. Defaults to 2e-4.
328
+ weight_decay (float, optional): Weight decay for regularization. Defaults to 1e-5.
329
+ length_penalty (float, optional): Penalty per additional thought vector. Defaults to 0.01.
330
+ single_vector_prob (float, optional): Probability of forcing a single thought vector. Defaults to 0.1.
331
+ save_path (str, optional): Path to save the model tar file. Defaults to "thought_vectors.tar".
332
+ spm_model_prefix (str, optional): Prefix for SentencePiece files. Defaults to "spm".
333
+ vocab_size (int, optional): Vocabulary size for tokenization. Defaults to 2000.
334
+ d_model (int, optional): Embedding dimensionality. Defaults to 256.
335
+ encoder_nhead (int, optional): Number of encoder attention heads. Defaults to 8.
336
+ decoder_nhead (int, optional): Number of decoder attention heads. Defaults to 8.
337
+ encoder_layers (int, optional): Number of encoder layers. Defaults to 2.
338
+ decoder_layers (int, optional): Number of decoder layers. Defaults to 2.
339
+ max_thoughts (int, optional): Maximum number of thought vectors. Defaults to 16.
340
+ dropout (float, optional): Dropout probability. Defaults to 0.1.
341
+ max_len (int, optional): Maximum sequence length for positional encoding. Defaults to 5000.
342
+ termination_threshold (float, optional): Threshold for stopping thought generation. Defaults to 0.75.
343
+ patience (int, optional): Epochs to wait for improvement before early stopping. Defaults to 5.
344
+ """
345
+ # Set model attributes
346
+ self.d_model = d_model
347
+ self.max_thoughts = max_thoughts
348
+ self.encoder_nhead = encoder_nhead
349
+ self.decoder_nhead = decoder_nhead
350
+ self.encoder_layers = encoder_layers
351
+ self.decoder_layers = decoder_layers
352
+ self.dropout = dropout
353
+ self.max_len = max_len
354
+ self.termination_threshold = termination_threshold
355
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
356
+
357
+ # Load or create SentencePiece model
358
+ self.sp = spm.SentencePieceProcessor()
359
+ if model and os.path.exists(model):
360
+ # Load existing model checkpoint
361
+ checkpoint = torch.load(model, map_location=self.device)
362
+ self.sp.Load(checkpoint['spm_model_path'])
363
+ self.vocab_size = self.sp.GetPieceSize()
364
+
365
+ # Initialize and load encoder/decoder with saved parameters
366
+ self.encoder = ThoughtEncoder(
367
+ vocab_size=self.vocab_size, d_model=d_model, max_thoughts=max_thoughts,
368
+ nhead=encoder_nhead, num_layers=encoder_layers, dropout=dropout,
369
+ termination_threshold=termination_threshold
370
+ ).to(self.device)
371
+ self.decoder = ThoughtDecoder(
372
+ vocab_size=self.vocab_size, d_model=d_model, num_layers=decoder_layers,
373
+ nhead=decoder_nhead, dropout=dropout
374
+ ).to(self.device)
375
+
376
+ self.encoder.load_state_dict(checkpoint['encoder_state_dict'])
377
+ self.decoder.load_state_dict(checkpoint['decoder_state_dict'])
378
+ print(f"Loaded pre-existing model from {model}")
379
+ else:
380
+ # Create new SentencePiece model
381
+ temp_dataset_path = None
382
+ if isinstance(group_data, (list, tuple)):
383
+ # Handle list input as before
384
+ all_sentences = [sentence for group in group_data for sentence in group]
385
+ with tempfile.NamedTemporaryFile(mode="w", encoding="utf8", delete=False) as temp_file:
386
+ for line in all_sentences:
387
+ temp_file.write(line.strip() + "\n")
388
+ temp_dataset_path = temp_file.name
389
+ else:
390
+ # Handle generator input
391
+ with tempfile.NamedTemporaryFile(mode="w", encoding="utf8", delete=False) as temp_file:
392
+ for group in group_data:
393
+ for sentence in group:
394
+ temp_file.write(sentence.strip() + "\n")
395
+ temp_dataset_path = temp_file.name
396
+
397
+ temp_dir = tempfile.mkdtemp()
398
+ temp_spm_prefix = os.path.join(temp_dir, "spm")
399
+
400
+ try:
401
+ spm.SentencePieceTrainer.Train(
402
+ input=temp_dataset_path,
403
+ model_prefix=temp_spm_prefix,
404
+ vocab_size=vocab_size,
405
+ model_type="bpe",
406
+ pad_id=0,
407
+ unk_id=1,
408
+ bos_id=2,
409
+ eos_id=3,
410
+ max_sentence_length=8192, # Added to handle long sequences
411
+ input_sentence_size=8_388_608,
412
+ train_extremely_large_corpus=True # Optimize for large datasets
413
+ )
414
+ shutil.copy(f"{temp_spm_prefix}.model", f"{spm_model_prefix}.model")
415
+ shutil.copy(f"{temp_spm_prefix}.vocab", f"{spm_model_prefix}.vocab")
416
+
417
+ if not self.sp.Load(f"{spm_model_prefix}.model"):
418
+ raise RuntimeError("Failed to load SentencePiece model.")
419
+ finally:
420
+ if temp_dataset_path:
421
+ os.remove(temp_dataset_path)
422
+ shutil.rmtree(temp_dir, ignore_errors=True)
423
+
424
+ self.vocab_size = self.sp.GetPieceSize()
425
+ self.encoder = ThoughtEncoder(
426
+ vocab_size=self.vocab_size, d_model=d_model, max_thoughts=max_thoughts,
427
+ nhead=encoder_nhead, num_layers=encoder_layers, dropout=dropout,
428
+ termination_threshold=termination_threshold
429
+ ).to(self.device)
430
+ self.decoder = ThoughtDecoder(
431
+ vocab_size=self.vocab_size, d_model=d_model, num_layers=decoder_layers,
432
+ nhead=decoder_nhead, dropout=dropout
433
+ ).to(self.device)
434
+
435
+ # Rest of the training setup
436
+ dataset = GroupTextDataset(group_data, self.sp)
437
+ train_size = int(val_split * len(dataset))
438
+ val_size = len(dataset) - train_size
439
+ train_dataset, val_dataset = random_split(dataset, [train_size, val_size])
440
+ train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=group_collate_fn)
441
+ val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, collate_fn=group_collate_fn)
442
+
443
+ optimizer = optim.Adam(list(self.encoder.parameters()) + list(self.decoder.parameters()),
444
+ lr=learning_rate, weight_decay=weight_decay)
445
+ scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs)
446
+ criterion = nn.CrossEntropyLoss(ignore_index=self.sp.pad_id())
447
+
448
+ best_val_loss = float('inf')
449
+ patience_counter = 0
450
+
451
+ # Training loop
452
+ for epoch in range(num_epochs):
453
+ self.encoder.train()
454
+ self.decoder.train()
455
+ total_train_loss = 0.0
456
+ for group_batch in train_loader:
457
+ optimizer.zero_grad()
458
+ batch_loss = 0.0
459
+ for group_tensor in group_batch:
460
+ if group_tensor.numel() == 0:
461
+ continue
462
+ group_tensor = group_tensor.to(self.device)
463
+ force_single_vector = random.random() < single_vector_prob
464
+ thought_vectors = self.encoder(group_tensor, force_single_vector=force_single_vector)
465
+
466
+ decoder_input = group_tensor[:, :-1]
467
+ target_tokens = group_tensor[:, 1:]
468
+ output_logits = self.decoder(thought_vectors, decoder_input)
469
+
470
+ loss = criterion(output_logits.reshape(-1, self.decoder.vocab_size), target_tokens.reshape(-1))
471
+ if not force_single_vector:
472
+ loss += length_penalty * thought_vectors.shape[1]
473
+ batch_loss += loss
474
+
475
+ if batch_loss > 0:
476
+ batch_loss.backward()
477
+ optimizer.step()
478
+ total_train_loss += batch_loss.item()
479
+
480
+ avg_train_loss = total_train_loss / len(train_loader)
481
+
482
+ self.encoder.eval()
483
+ self.decoder.eval()
484
+ total_val_loss = 0.0
485
+ with torch.no_grad():
486
+ for group_batch in val_loader:
487
+ batch_val_loss = 0.0
488
+ for group_tensor in group_batch:
489
+ if group_tensor.numel() == 0:
490
+ continue
491
+ group_tensor = group_tensor.to(self.device)
492
+ thought_vectors = self.encoder(group_tensor)
493
+ output_logits = self.decoder(thought_vectors, group_tensor[:, :-1])
494
+ val_loss = criterion(output_logits.reshape(-1, self.decoder.vocab_size),
495
+ group_tensor[:, 1:].reshape(-1))
496
+ batch_val_loss += val_loss.item()
497
+
498
+ total_val_loss += batch_val_loss
499
+
500
+ avg_val_loss = total_val_loss / len(val_loader)
501
+ print(f"Epoch {epoch + 1}/{num_epochs} - Train Loss: {avg_train_loss:.4f} - "
502
+ f"Val Loss: {avg_val_loss:.4f}")
503
+
504
+ if avg_val_loss < best_val_loss:
505
+ best_val_loss = avg_val_loss
506
+ patience_counter = 0
507
+ self.save(save_path, spm_model_prefix, clean=False)
508
+ else:
509
+ patience_counter += 1
510
+ if patience_counter >= patience:
511
+ print(f"Early stopping triggered after {epoch + 1} epochs.")
512
+ break
513
+
514
+ scheduler.step()
515
+
516
+ print(f"Best model saved to {save_path}")
517
+
518
+ def encode(self, text: Union[str, List[str]], force_single_vector: bool = False) -> torch.Tensor:
519
+ """Encodes text into thought vectors.
520
+
521
+ Args:
522
+ text (Union[str, List[str]]): Input text as a string or list of strings.
523
+ force_single_vector (bool, optional): If True, generates only one thought vector.
524
+ Defaults to False.
525
+
526
+ Returns:
527
+ torch.Tensor: Thought vectors, shape (batch_size, num_thoughts, d_model).
528
+ """
529
+ if self.encoder is None or self.sp is None:
530
+ raise RuntimeError("Model not trained or loaded.")
531
+
532
+ if isinstance(text, str):
533
+ text = [text]
534
+
535
+ tokenized = [[self.sp.bos_id()] + self.sp.EncodeAsIds(t) + [self.sp.eos_id()] for t in text]
536
+ input_tokens = nn.utils.rnn.pad_sequence(
537
+ [torch.tensor(seq, dtype=torch.long) for seq in tokenized],
538
+ batch_first=True, padding_value=self.sp.pad_id()
539
+ ).to(self.device)
540
+
541
+ return self.encoder(input_tokens, force_single_vector)
542
+
543
+ def decode(self, thought_vectors: torch.Tensor, max_length: int = 50, beam_width: Optional[int] = 5,
544
+ temperature: float = 1.0) -> List[str]:
545
+ """Decodes thought vectors into text sequences.
546
+
547
+ Args:
548
+ thought_vectors (torch.Tensor): Thought vectors, shape (batch_size, num_thoughts, d_model).
549
+ max_length (int, optional): Maximum length of generated sequences. Defaults to 50.
550
+ beam_width (Optional[int], optional): Beam width for beam search; if None, uses greedy
551
+ decoding. Defaults to 5.
552
+ temperature (float, optional): Temperature for softmax sampling in greedy decoding.
553
+ Defaults to 1.0.
554
+
555
+ Returns:
556
+ List[str]: Decoded text sequences.
557
+ """
558
+ if self.decoder is None or self.sp is None:
559
+ raise RuntimeError("Model not trained or loaded.")
560
+
561
+ thought_vectors = thought_vectors.to(self.device)
562
+ batch_size = thought_vectors.size(0)
563
+
564
+ if beam_width is not None and beam_width > 1:
565
+ return self._beam_search_decode(thought_vectors, max_length, beam_width)
566
+
567
+ target_tokens = torch.full((batch_size, 1), self.sp.bos_id(), dtype=torch.long, device=self.device)
568
+ for _ in range(max_length - 1):
569
+ logits = self.decoder(thought_vectors, target_tokens)
570
+ logits = logits[:, -1, :] / temperature
571
+ next_token = logits.softmax(dim=-1).multinomial(1)
572
+ target_tokens = torch.cat([target_tokens, next_token], dim=1)
573
+ if (next_token == self.sp.eos_id()).all():
574
+ break
575
+
576
+ return [self.sp.DecodeIds(seq.tolist()) for seq in target_tokens]
577
+
578
+ def _beam_search_decode(self, thought_vectors: torch.Tensor, max_length: int, beam_width: int) -> List[str]:
579
+ """Performs beam search decoding of thought vectors.
580
+
581
+ Args:
582
+ thought_vectors (torch.Tensor): Thought vectors, shape (batch_size, num_thoughts, d_model).
583
+ max_length (int): Maximum length of generated sequences.
584
+ beam_width (int): Number of beams to maintain during search.
585
+
586
+ Returns:
587
+ List[str]: Decoded text sequences, one per batch item.
588
+ """
589
+ batch_size = thought_vectors.size(0)
590
+ start_tokens = torch.full((batch_size, 1), self.sp.bos_id(), dtype=torch.long, device=self.device)
591
+ beams = [[] for _ in range(batch_size)]
592
+ finished = [[] for _ in range(batch_size)]
593
+
594
+ for i in range(batch_size):
595
+ beams[i].append((0.0, start_tokens[i].unsqueeze(0)))
596
+
597
+ for _ in range(max_length - 1):
598
+ new_beams = [[] for _ in range(batch_size)]
599
+ for i in range(batch_size):
600
+ for score, seq in beams[i]:
601
+ if seq[:, -1].item() == self.sp.eos_id():
602
+ finished[i].append((score, seq))
603
+ continue
604
+ logits = self.decoder(thought_vectors[i:i + 1], seq)[:, -1, :]
605
+ probs, next_tokens = logits.softmax(dim=-1).topk(beam_width, dim=-1)
606
+ for p, t in zip(probs[0], next_tokens[0]):
607
+ new_score = score - p.log().item()
608
+ new_seq = torch.cat([seq, t.unsqueeze(0).unsqueeze(-1)], dim=1)
609
+ new_beams[i].append((new_score, new_seq))
610
+
611
+ beams[i] = sorted(new_beams[i], key=lambda x: x[0])[:beam_width]
612
+
613
+ if all(len(finished[i]) >= beam_width for i in range(batch_size)):
614
+ break
615
+
616
+ results = []
617
+ for i in range(batch_size):
618
+ combined = sorted(finished[i] + beams[i], key=lambda x: x[0])
619
+ results.append(self.sp.DecodeIds(combined[0][1].squeeze(0).tolist()) if combined else "<empty>")
620
+
621
+ return results
622
+
623
+ def save(self, path: str, spm_model_prefix: str, clean: bool = True) -> None:
624
+ """Saves the model and SentencePiece data to a tar file.
625
+
626
+ Args:
627
+ path (str): Path to save the tar file.
628
+ spm_model_prefix (str): Prefix for SentencePiece model files.
629
+ clean (bool, optional): If True, removes temporary files after saving. Defaults to True.
630
+ """
631
+ if self.encoder is None or self.decoder is None:
632
+ raise RuntimeError("No model to save.")
633
+
634
+ if not os.path.exists(f"{spm_model_prefix}.model") or not os.path.exists(f"{spm_model_prefix}.vocab"):
635
+ raise FileNotFoundError(f"SentencePiece files ({spm_model_prefix}.model or .vocab) not found.")
636
+
637
+ with tarfile.open(path, "w") as tar:
638
+ torch.save({
639
+ 'encoder_state_dict': self.encoder.state_dict(),
640
+ 'decoder_state_dict': self.decoder.state_dict(),
641
+ 'vocab_size': self.vocab_size,
642
+ 'd_model': self.d_model,
643
+ 'max_thoughts': self.max_thoughts,
644
+ 'encoder_nhead': self.encoder_nhead,
645
+ 'decoder_nhead': self.decoder_nhead,
646
+ 'encoder_layers': self.encoder_layers,
647
+ 'decoder_layers': self.decoder_layers,
648
+ 'dropout': self.dropout,
649
+ 'max_len': self.max_len,
650
+ 'termination_threshold': self.termination_threshold
651
+ }, "model.pth")
652
+ tar.add("model.pth")
653
+ tar.add(f"{spm_model_prefix}.model")
654
+ tar.add(f"{spm_model_prefix}.vocab")
655
+
656
+ if clean:
657
+ os.remove("model.pth")
658
+ os.remove(f"{spm_model_prefix}.model")
659
+ os.remove(f"{spm_model_prefix}.vocab")
660
+
661
+ @classmethod
662
+ def load(cls, path: str) -> 'ThoughtVectors':
663
+ """Loads a trained model from a tar file.
664
+
665
+ Args:
666
+ path (str): Path to the tar file containing the model.
667
+
668
+ Returns:
669
+ ThoughtVectors: Loaded model instance.
670
+ """
671
+ translator = cls()
672
+ try:
673
+ with tarfile.open(path, "r") as tar:
674
+ tar.extractall()
675
+ checkpoint = torch.load("model.pth")
676
+ translator.sp = spm.SentencePieceProcessor()
677
+ if not translator.sp.Load("spm.model"):
678
+ raise RuntimeError("Failed to load SentencePiece model.")
679
+ finally:
680
+ for f in ["model.pth", "spm.model", "spm.vocab"]:
681
+ if os.path.exists(f):
682
+ os.remove(f)
683
+
684
+ translator.vocab_size = checkpoint['vocab_size']
685
+ translator.d_model = checkpoint['d_model']
686
+ translator.max_thoughts = checkpoint['max_thoughts']
687
+ translator.encoder_nhead = checkpoint['encoder_nhead']
688
+ translator.decoder_nhead = checkpoint['decoder_nhead']
689
+ translator.encoder_layers = checkpoint['encoder_layers']
690
+ translator.decoder_layers = checkpoint['decoder_layers']
691
+ translator.dropout = checkpoint['dropout']
692
+ translator.max_len = checkpoint['max_len']
693
+ translator.termination_threshold = checkpoint['termination_threshold']
694
+
695
+ translator.encoder = ThoughtEncoder(
696
+ vocab_size=translator.vocab_size, d_model=translator.d_model, max_thoughts=translator.max_thoughts,
697
+ nhead=translator.encoder_nhead, num_layers=translator.encoder_layers, dropout=translator.dropout,
698
+ termination_threshold=translator.termination_threshold
699
+ ).to(translator.device)
700
+ translator.decoder = ThoughtDecoder(
701
+ vocab_size=translator.vocab_size, d_model=translator.d_model, num_layers=translator.decoder_layers,
702
+ nhead=translator.decoder_nhead, dropout=translator.dropout
703
+ ).to(translator.device)
704
+
705
+ translator.encoder.load_state_dict(checkpoint['encoder_state_dict'])
706
+ translator.decoder.load_state_dict(checkpoint['decoder_state_dict'])
707
+
708
+ return translator
709
+
710
+
711
+ # Example usage with a shorter dataset
712
+ if __name__ == "__main__":
713
+ import csv
714
+
715
+ csv.field_size_limit(8 ** 8) # You can adjust this to a higher limit (e.g., 1MB)
716
+
717
+ # Generator function to read data from a CSV and format it
718
+ def data_generator(csv_file_path):
719
+ with open(csv_file_path, mode='r', newline='', encoding='utf-8') as file:
720
+ reader = csv.reader(file)
721
+ for row in reader:
722
+ if row:
723
+ yield [row[0]]
724
+
725
+
726
+ data = data_generator("data.csv")
727
+
728
+ # Define the ThoughtVectors training and usage process
729
+ tv = ThoughtVectors()
730
+ tv.train(
731
+ group_data=data,
732
+ num_epochs=1000,
733
+ batch_size=128,
734
+ val_split=0.8,
735
+ learning_rate=2e-4,
736
+ weight_decay=5e-5,
737
+ length_penalty=0.01,
738
+ single_vector_prob=0.2,
739
+ save_path="thought_vectors_prototype.tar",
740
+ spm_model_prefix="spm",
741
+ vocab_size=8192,
742
+ d_model=1024,
743
+ encoder_nhead=8,
744
+ decoder_nhead=8,
745
+ encoder_layers=4,
746
+ decoder_layers=4,
747
+ max_thoughts=32,
748
+ dropout=0.1,
749
+ max_len=256,
750
+ termination_threshold=0.9,
751
+ patience=10
752
+ )
753
+
754
+ # Load and test
755
+ loaded_tv = ThoughtVectors.load("thought_vectors_prototype.tar")
756
+ loaded_thought_vectors = loaded_tv.encode("AI is smart")
757
+ generated_text_greedy = tv.decode(loaded_thought_vectors, temperature=0.7)
758
+ generated_text_beam = tv.decode(loaded_thought_vectors, beam_width=3)
759
+
760
+ print(f"Greedy decoding: {generated_text_greedy}")
761
+ print(f"Beam search decoding: {generated_text_beam}")
762
+