Download src/models/text_encoder.py from minhquang47/b: direct link, hf CLI and curl.
- Browser
- Download file 8.92 kB
-
https://huggingface.co/minhquang47/b/resolve/main/src/models/text_encoder.py
- Command line
-
hf download hf://minhquang47/b/src/models/text_encoder.py
-
curl -L -o text_encoder.py https://huggingface.co/minhquang47/b/resolve/main/src/models/text_encoder.py
8.92 kB
| import re | |
| from collections import Counter | |
| from dataclasses import dataclass | |
| from typing import Dict, Iterable, List, Optional, Sequence, Tuple | |
| import torch | |
| import torch.nn as nn | |
| TOKEN_PATTERN = re.compile(r"<image>|[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)?|[^\sA-Za-z0-9]", re.UNICODE) | |
| class BasicVQATokenizer: | |
| """ | |
| Tokenizer tối giản cho prototype VQA classification. | |
| Không thay thế tokenizer LLM thật. Mục tiêu là smoke test cross-modal pipeline: | |
| question_text -> input_ids/attention_mask -> SimpleTextEncoder. | |
| """ | |
| pad_token = "<pad>" | |
| unk_token = "<unk>" | |
| bos_token = "<bos>" | |
| eos_token = "<eos>" | |
| def __init__( | |
| self, | |
| vocab: Optional[Dict[str, int]] = None, | |
| lowercase: bool = True, | |
| keep_image_token: bool = True, | |
| ): | |
| self.lowercase = lowercase | |
| self.keep_image_token = keep_image_token | |
| if vocab is None: | |
| vocab = { | |
| self.pad_token: 0, | |
| self.unk_token: 1, | |
| self.bos_token: 2, | |
| self.eos_token: 3, | |
| "<image>": 4, | |
| } | |
| self.vocab = dict(vocab) | |
| self.id_to_token = {idx: token for token, idx in self.vocab.items()} | |
| def pad_token_id(self) -> int: | |
| return self.vocab[self.pad_token] | |
| def unk_token_id(self) -> int: | |
| return self.vocab[self.unk_token] | |
| def bos_token_id(self) -> int: | |
| return self.vocab[self.bos_token] | |
| def eos_token_id(self) -> int: | |
| return self.vocab[self.eos_token] | |
| def __len__(self) -> int: | |
| return len(self.vocab) | |
| def tokenize(self, text: str) -> List[str]: | |
| text = text or "" | |
| tokens = TOKEN_PATTERN.findall(text) | |
| processed = [] | |
| for token in tokens: | |
| if token == "<image>": | |
| if self.keep_image_token: | |
| processed.append(token) | |
| continue | |
| processed.append(token.lower() if self.lowercase else token) | |
| return processed | |
| def encode( | |
| self, | |
| text: str, | |
| max_length: int = 64, | |
| add_special_tokens: bool = True, | |
| ) -> List[int]: | |
| tokens = self.tokenize(text) | |
| ids = [self.vocab.get(token, self.unk_token_id) for token in tokens] | |
| if add_special_tokens: | |
| ids = [self.bos_token_id] + ids + [self.eos_token_id] | |
| return ids[:max_length] | |
| def batch_encode( | |
| self, | |
| texts: Sequence[str], | |
| max_length: int = 64, | |
| add_special_tokens: bool = True, | |
| ) -> Dict[str, torch.Tensor]: | |
| encoded = [self.encode(text, max_length=max_length, add_special_tokens=add_special_tokens) for text in texts] | |
| max_len = max((len(ids) for ids in encoded), default=1) | |
| max_len = min(max_len, max_length) | |
| input_ids = torch.full((len(texts), max_len), self.pad_token_id, dtype=torch.long) | |
| attention_mask = torch.zeros((len(texts), max_len), dtype=torch.long) | |
| for i, ids in enumerate(encoded): | |
| ids = ids[:max_len] | |
| input_ids[i, : len(ids)] = torch.tensor(ids, dtype=torch.long) | |
| attention_mask[i, : len(ids)] = 1 | |
| return { | |
| "input_ids": input_ids, | |
| "attention_mask": attention_mask, | |
| } | |
| def build_from_texts( | |
| cls, | |
| texts: Iterable[str], | |
| min_freq: int = 1, | |
| max_vocab_size: Optional[int] = None, | |
| lowercase: bool = True, | |
| keep_image_token: bool = True, | |
| ) -> "BasicVQATokenizer": | |
| tokenizer = cls(lowercase=lowercase, keep_image_token=keep_image_token) | |
| counter = Counter() | |
| for text in texts: | |
| counter.update(tokenizer.tokenize(text)) | |
| reserved = dict(tokenizer.vocab) | |
| vocab = dict(reserved) | |
| max_new_tokens = None | |
| if max_vocab_size is not None: | |
| max_new_tokens = max(max_vocab_size - len(reserved), 0) | |
| added = 0 | |
| for token, freq in counter.most_common(): | |
| if freq < min_freq: | |
| continue | |
| if token in vocab: | |
| continue | |
| if max_new_tokens is not None and added >= max_new_tokens: | |
| break | |
| vocab[token] = len(vocab) | |
| added += 1 | |
| return cls(vocab=vocab, lowercase=lowercase, keep_image_token=keep_image_token) | |
| class SimpleTextEncoderConfig: | |
| vocab_size: int | |
| d_model: int = 768 | |
| max_length: int = 64 | |
| pad_token_id: int = 0 | |
| num_layers: int = 2 | |
| num_heads: int = 8 | |
| dim_feedforward: int = 2048 | |
| dropout: float = 0.10 | |
| use_positional_embedding: bool = True | |
| def __post_init__(self): | |
| if self.vocab_size <= 0: | |
| raise ValueError("vocab_size phải > 0") | |
| if self.d_model <= 0: | |
| raise ValueError("d_model phải > 0") | |
| if self.max_length <= 0: | |
| raise ValueError("max_length phải > 0") | |
| if self.d_model % self.num_heads != 0: | |
| raise ValueError("d_model phải chia hết cho num_heads") | |
| class SimpleTextEncoder(nn.Module): | |
| """ | |
| Text encoder tối giản: Embedding + TransformerEncoder. | |
| Input: | |
| input_ids: [B, Nt] | |
| attention_mask: [B, Nt], 1=valid, 0=pad | |
| Output: | |
| text_tokens: [B, Nt, D] | |
| pooled_text: [B, D] | |
| """ | |
| def __init__(self, config: SimpleTextEncoderConfig): | |
| super().__init__() | |
| self.config = config | |
| self.token_embedding = nn.Embedding( | |
| config.vocab_size, | |
| config.d_model, | |
| padding_idx=config.pad_token_id, | |
| ) | |
| self.position_embedding = None | |
| if config.use_positional_embedding: | |
| self.position_embedding = nn.Embedding(config.max_length, config.d_model) | |
| encoder_layer = nn.TransformerEncoderLayer( | |
| d_model=config.d_model, | |
| nhead=config.num_heads, | |
| dim_feedforward=config.dim_feedforward, | |
| dropout=config.dropout, | |
| activation="gelu", | |
| batch_first=True, | |
| norm_first=False, | |
| ) | |
| self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=config.num_layers) | |
| self.output_norm = nn.LayerNorm(config.d_model) | |
| def forward( | |
| self, | |
| input_ids: torch.Tensor, | |
| attention_mask: Optional[torch.Tensor] = None, | |
| return_dict: bool = True, | |
| ): | |
| if input_ids.ndim != 2: | |
| raise ValueError(f"input_ids phải có shape [B, Nt], nhận {tuple(input_ids.shape)}") | |
| batch_size, seq_len = input_ids.shape | |
| if seq_len > self.config.max_length: | |
| input_ids = input_ids[:, : self.config.max_length] | |
| seq_len = self.config.max_length | |
| if attention_mask is not None: | |
| attention_mask = attention_mask[:, : self.config.max_length] | |
| if attention_mask is None: | |
| attention_mask = (input_ids != self.config.pad_token_id).long() | |
| attention_mask = attention_mask.to(device=input_ids.device) | |
| x = self.token_embedding(input_ids) | |
| if self.position_embedding is not None: | |
| positions = torch.arange(seq_len, device=input_ids.device).unsqueeze(0).expand(batch_size, -1) | |
| x = x + self.position_embedding(positions) | |
| key_padding_mask = attention_mask == 0 | |
| text_tokens = self.encoder(x, src_key_padding_mask=key_padding_mask) | |
| text_tokens = self.output_norm(text_tokens) | |
| pooled_text = self.masked_mean_pool(text_tokens, attention_mask) | |
| if not return_dict: | |
| return text_tokens | |
| return { | |
| "text_tokens": text_tokens, | |
| "pooled_text": pooled_text, | |
| "attention_mask": attention_mask, | |
| } | |
| def masked_mean_pool(tokens: torch.Tensor, attention_mask: torch.Tensor, eps: float = 1e-8) -> torch.Tensor: | |
| mask = attention_mask.to(device=tokens.device, dtype=tokens.dtype).unsqueeze(-1) | |
| summed = (tokens * mask).sum(dim=1) | |
| denom = mask.sum(dim=1).clamp_min(eps) | |
| return summed / denom | |
| def build_text_encoder_from_questions( | |
| questions: Sequence[str], | |
| d_model: int = 768, | |
| max_length: int = 64, | |
| min_freq: int = 1, | |
| max_vocab_size: Optional[int] = 30000, | |
| num_layers: int = 2, | |
| num_heads: int = 8, | |
| dropout: float = 0.10, | |
| ) -> Tuple[BasicVQATokenizer, SimpleTextEncoder]: | |
| tokenizer = BasicVQATokenizer.build_from_texts( | |
| questions, | |
| min_freq=min_freq, | |
| max_vocab_size=max_vocab_size, | |
| ) | |
| config = SimpleTextEncoderConfig( | |
| vocab_size=len(tokenizer), | |
| d_model=d_model, | |
| max_length=max_length, | |
| pad_token_id=tokenizer.pad_token_id, | |
| num_layers=num_layers, | |
| num_heads=num_heads, | |
| dropout=dropout, | |
| ) | |
| return tokenizer, SimpleTextEncoder(config) | |