File size: 11,978 Bytes
17f1f54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# coding: utf-8
import torch
import torch.nn as nn
from torch import Tensor
from typing import Any, Dict, Optional

from encoder import Encoder
from ACD import ACD
from batch import Batch
from embeddings import Embeddings
from vocabulary import Vocabulary
from initialization import initialize_model
from constants import PAD_TOKEN, EOS_TOKEN, BOS_TOKEN, TARGET_PAD, UNK_TOKEN


def _get_stoi_mapping(vocab: Any) -> Dict[str, int]:
    """
    Return a token->index mapping for several vocab implementations:
      - objects with get_stoi()
      - objects with .stoi (dict-like or mapping)
      - plain dicts
      - objects supporting __getitem__ (try to extract PAD/UNK)
    Returns an empty dict on failure.
    """
    if vocab is None:
        return {}

    # 1) custom Vocabulary with get_stoi()
    try:
        if hasattr(vocab, "get_stoi") and callable(getattr(vocab, "get_stoi")):
            m = vocab.get_stoi()
            if isinstance(m, dict):
                return m
    except Exception:
        pass

    # 2) TorchText-like object with .stoi attribute (dict or mapping)
    try:
        if hasattr(vocab, "stoi"):
            stoi = getattr(vocab, "stoi")
            if callable(stoi):
                stoi = stoi()
            if isinstance(stoi, dict):
                return stoi
            try:
                return dict(stoi)
            except Exception:
                pass
    except Exception:
        pass

    # 3) If it's a plain dict
    try:
        if isinstance(vocab, dict):
            return vocab
    except Exception:
        pass

    # 4) Try basic __getitem__ access to extract PAD/UNK indices
    try:
        pad_idx = vocab[PAD_TOKEN]
        mapping = {PAD_TOKEN: pad_idx}
        try:
            mapping[UNK_TOKEN] = vocab[UNK_TOKEN]
        except Exception:
            pass
        return mapping
    except Exception:
        pass

    # fallback empty mapping
    return {}


def _get_vocab_size(vocab: Any) -> int:
    """
    Determine vocabulary size in a robust way.
    Tries len(vocab), len(vocab.stoi), or highest index+1 from stoi dict.
    Falls back to 0.
    """
    try:
        # If vocab supports len()
        sz = len(vocab)
        return sz
    except Exception:
        pass

    try:
        if hasattr(vocab, "stoi"):
            stoi = getattr(vocab, "stoi")
            if callable(stoi):
                stoi = stoi()
            if isinstance(stoi, dict):
                # assume indices are 0..N-1 or similar
                max_idx = max(stoi.values()) if stoi else -1
                return max_idx + 1
            try:
                return len(dict(stoi))
            except Exception:
                pass
    except Exception:
        pass

    try:
        # if it's a dict
        if isinstance(vocab, dict):
            max_idx = max(vocab.values()) if vocab else -1
            return max_idx + 1
    except Exception:
        pass

    return 0


class Model(nn.Module):
    def __init__(self, cfg: dict,
                 encoder: Encoder,
                 ACD: ACD,
                 src_embed: Embeddings,
                 src_vocab: Vocabulary,
                 trg_vocab: Vocabulary,
                 in_trg_size: int,
                 out_trg_size: int):
        """
        Create Sign-IDD
        """
        super(Model, self).__init__()

        self.src_embed = src_embed
        self.encoder = encoder
        self.ACD = ACD
        self.src_vocab = src_vocab
        self.trg_vocab = trg_vocab

        # robustly obtain stoi mapping
        stoi = _get_stoi_mapping(self.src_vocab)

        # ensure special tokens exist (give informative error)
        for tok in [BOS_TOKEN, PAD_TOKEN, EOS_TOKEN]:
            if tok not in stoi:
                raise ValueError(
                    f"Special token '{tok}' missing in vocab. "
                    f"Available tokens (sample): {list(stoi.keys())[:20]} ..."
                )

        self.bos_index = stoi[BOS_TOKEN]
        self.pad_index = stoi[PAD_TOKEN]
        self.eos_index = stoi[EOS_TOKEN]

        self.target_pad = TARGET_PAD
        self.use_cuda = cfg["training"].get("use_cuda", False) if "training" in cfg else False
        self.in_trg_size = in_trg_size
        self.out_trg_size = out_trg_size

    def forward(self, is_train: bool, src: Tensor, trg_input: Tensor,
                src_mask: Tensor, src_lengths: Tensor, trg_mask: Tensor):
        """ Encode source, then diffusion decode """
        encoder_output = self.encode(src=src,
                                     src_length=src_lengths,
                                     src_mask=src_mask)

        diffusion_output = self.diffusion(is_train=is_train,
                                          encoder_output=encoder_output,
                                          trg_input=trg_input,
                                          src_mask=src_mask,
                                          trg_mask=trg_mask)
        return diffusion_output

    def encode(self, src: Tensor, src_length: Tensor, src_mask: Tensor):
        """ Encodes the source sequence """
        return self.encoder(embed_src=self.src_embed(src),
                            src_length=src_length,
                            mask=src_mask)

    def diffusion(self, is_train: bool, encoder_output: Tensor,
                  src_mask: Tensor, trg_input: Tensor, trg_mask: Tensor):
        """ Diffusion decoding """
        return self.ACD(is_train=is_train,
                        encoder_output=encoder_output,
                        input_3d=trg_input,
                        src_mask=src_mask,
                        trg_mask=trg_mask)

    def get_loss_for_batch(self, is_train, batch: Batch, loss_function: nn.Module) -> Tensor:
        """ Compute batch loss """
        skel_out = self.forward(src=batch.src,
                                trg_input=batch.trg_input[:, :, :150],
                                src_mask=batch.src_mask,
                                src_lengths=batch.src_lengths,
                                trg_mask=batch.trg_mask,
                                is_train=is_train)
        batch_loss = loss_function(skel_out, batch.trg_input[:, :, :150])
        return batch_loss


def build_model(cfg: dict, src_vocab: 'Vocabulary', trg_vocab: 'Vocabulary', checkpoint: Optional[dict] = None):
    """
    Build and initialize the Sign-IDD model.
    Optionally load a checkpoint and resize embeddings if necessary.
    """

    # Full configuration
    full_cfg = cfg
    cfg_model = cfg["model"]

    # Padding indices (robust)
    src_stoi = _get_stoi_mapping(src_vocab)
    src_padding_idx = src_stoi.get(PAD_TOKEN, 0)

    if not isinstance(trg_vocab, (list, tuple)):
        trg_stoi = _get_stoi_mapping(trg_vocab)
        trg_padding_idx = trg_stoi.get(PAD_TOKEN, 0)
    else:
        trg_padding_idx = 0

    in_trg_size = cfg_model["trg_size"]
    out_trg_size = cfg_model["trg_size"]

    # Determine vocab_size robustly
    vocab_size = _get_vocab_size(src_vocab)
    if vocab_size == 0:
        # best-effort fallback: try to infer from stoi mapping
        vocab_size = max(src_stoi.values()) + 1 if src_stoi else 0

    # --- Source embedding ---
    src_embed = Embeddings(
        **cfg_model["encoder"]["embeddings"],
        vocab_size=vocab_size,
        padding_idx=src_padding_idx
    )

    # --- Encoder ---
    enc_dropout = cfg_model["encoder"].get("dropout", 0.0)
    enc_emb_dropout = cfg_model["encoder"]["embeddings"].get("dropout", enc_dropout)

    # Transformer-specific check (if your encoder assumes this)
    if "embedding_dim" in cfg_model["encoder"]["embeddings"] and "hidden_size" in cfg_model["encoder"]:
        assert cfg_model["encoder"]["embeddings"]["embedding_dim"] == cfg_model["encoder"]["hidden_size"], \
            "For transformer, embedding_dim must equal hidden_size"

    encoder = Encoder(
        **cfg_model["encoder"],
        emb_size=src_embed.embedding_dim,
        emb_dropout=enc_emb_dropout
    )

    # --- ACD module ---
    diffusion = ACD(args=cfg_model, trg_vocab=trg_vocab)

    # --- Build the model ---
    model = Model(
        cfg=full_cfg,
        encoder=encoder,
        ACD=diffusion,
        src_embed=src_embed,
        src_vocab=src_vocab,
        trg_vocab=trg_vocab,
        in_trg_size=in_trg_size,
        out_trg_size=out_trg_size
    )

    # --- Initialize model parameters ---
    initialize_model(model, cfg_model, src_padding_idx, trg_padding_idx)

    # --- Load checkpoint if provided ---
    if checkpoint is not None:
        # support both {"model_state": {...}} and raw state dicts
        state_dict = checkpoint.get("model_state", None) if isinstance(checkpoint, dict) else None
        state_dict = state_dict if state_dict is not None else (checkpoint if isinstance(checkpoint, dict) else None)

        if isinstance(state_dict, dict):
            # Handle source embedding mismatch safely if attributes exist
            # Support both "src_embed.lut.weight" and "src_embed.weight" checkpoint keys
            for emb_key in ("src_embed.lut.weight", "src_embed.weight"):
                if emb_key in state_dict:
                    # Check current model embedding weight shape (if available)
                    curr_emb = None
                    # try to reach the attribute in a safe manner
                    lut = getattr(model.src_embed, "lut", None)
                    if lut is not None and hasattr(lut, "weight"):
                        curr_emb = lut.weight
                    elif hasattr(model.src_embed, "weight"):
                        curr_emb = getattr(model.src_embed, "weight")

                    if curr_emb is not None:
                        if state_dict[emb_key].shape != curr_emb.shape:
                            print(f"Skipping {emb_key} due to shape mismatch")
                            state_dict.pop(emb_key, None)
                    # if we couldn't inspect current embedding, leave as-is (load may still work)

            try:
                model.load_state_dict(state_dict, strict=False)
            except RuntimeError as e:
                # more informative failure message
                print(f"Warning: problem loading state_dict: {e}")
        else:
            print("Warning: checkpoint provided but no valid state_dict found in provided object")

    # Move to GPU if available & desired
    if torch.cuda.is_available() and full_cfg.get("training", {}).get("use_cuda", True):
        model.to(torch.device("cuda"))

    return model


def load_model(checkpoint: Optional[dict], model: nn.Module, src_padding_idx: int):
    """
    Load checkpoint into model safely (used if you want a separate utility).
    """
    if checkpoint is None:
        return model

    model_state = checkpoint.get("model_state", checkpoint) if isinstance(checkpoint, dict) else checkpoint

    if isinstance(model_state, dict):
        # try to handle src_embed mismatch similarly to build_model
        for emb_key in ("src_embed.lut.weight", "src_embed.weight"):
            if emb_key in model_state:
                curr_emb = None
                lut = getattr(model.src_embed, "lut", None)
                if lut is not None and hasattr(lut, "weight"):
                    curr_emb = lut.weight
                elif hasattr(model.src_embed, "weight"):
                    curr_emb = getattr(model.src_embed, "weight")

                if curr_emb is not None:
                    if model_state[emb_key].shape != curr_emb.shape:
                        print(f"[INFO] Checkpoint {emb_key} shape {model_state[emb_key].shape} "
                              f"does not match current model {curr_emb.shape}. Skipping.")
                        model_state.pop(emb_key, None)

        try:
            model.load_state_dict(model_state, strict=False)
        except RuntimeError as e:
            print(f"[INFO] Failed to load full checkpoint: {e}")

    return model