File size: 11,196 Bytes
cf84204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
339
340
"""
Fusion Mechanisms for Multimodal Learning

This module contains various fusion strategies to combine tree and image features:
- CrossAttentionFusion: Cross-modal attention
- CMF: Cross-Modal Fusion
- BiDirectionalCrossAttention: Bidirectional cross-attention
- GatedFusion: Gated fusion with learnable gates
- MultiHeadCrossModalAttention: Multi-head cross-modal attention
"""

import torch
import torch.nn as nn
import torch.nn.functional as F


class CrossAttentionFusion(nn.Module):
    """Cross-attention fusion for tree and image features"""
    def __init__(self, dim, num_heads=4, dropout=0.1):
        super(CrossAttentionFusion, self).__init__()
        self.num_heads = num_heads
        self.dim = dim
        self.head_dim = dim // num_heads

        assert dim % num_heads == 0, "dim must be divisible by num_heads"

        # Query, Key, Value projections
        self.q_proj = nn.Linear(dim, dim)
        self.k_proj = nn.Linear(dim, dim)
        self.v_proj = nn.Linear(dim, dim)
        self.out_proj = nn.Sequential(
            nn.Linear(dim, dim * 4),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(dim * 4, dim),
            nn.Dropout(dropout)
        )

        self.dropout = nn.Dropout(dropout)
        self.scale = self.head_dim ** -0.5

    def forward(self, tree_feat, image_feat):
        """
        Args:
            tree_feat: (B, dim) tree features
            image_feat: (B, dim) image features
        Returns:
            fused: (B, dim) fused features
        """
        B = tree_feat.shape[0]

        # Add sequence dimension: (B, 1, dim)
        tree_feat = tree_feat.unsqueeze(1)
        image_feat = image_feat.unsqueeze(1)

        # Tree attends to image (tree as query, image as key/value)
        Q = self.q_proj(tree_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
        K = self.k_proj(image_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
        V = self.v_proj(image_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)

        # Attention scores
        attn = (Q @ K.transpose(-2, -1)) * self.scale
        attn = torch.softmax(attn, dim=-1)
        attn = self.dropout(attn)

        # Apply attention to values
        out = (attn @ V).transpose(1, 2).contiguous().view(B, 1, self.dim)

        # Project and add residual
        out = self.out_proj(out.squeeze(1))
        fused = out + tree_feat.squeeze(1)

        return fused


class CMF(nn.Module):
    """Cross-Modal Fusion with attention mechanism"""
    def __init__(self, dim, dropout=0.1):
        super(CMF, self).__init__()
        self.dim = dim

        # Feature-level attention
        self.tree_attn = nn.Sequential(
            nn.Linear(dim, dim),
            nn.Tanh(),
            nn.Linear(dim, 1)
        )
        self.image_attn = nn.Sequential(
            nn.Linear(dim, dim),
            nn.Tanh(),
            nn.Linear(dim, 1)
        )

        # Cross-modal interaction
        self.cross_proj = nn.Sequential(
            nn.Linear(dim * 2, dim),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(dim, dim)
        )

    def forward(self, tree_feat, image_feat):
        """
        Args:
            tree_feat: (B, dim)
            image_feat: (B, dim)
        Returns:
            fused: (B, dim)
        """
        # Compute attention weights
        tree_weight = torch.sigmoid(self.tree_attn(tree_feat))
        image_weight = torch.sigmoid(self.image_attn(image_feat))

        # Normalize weights
        total_weight = tree_weight + image_weight + 1e-8
        tree_weight = tree_weight / total_weight
        image_weight = image_weight / total_weight

        # Weighted combination
        weighted_tree = tree_feat * tree_weight
        weighted_image = image_feat * image_weight

        # Cross-modal projection
        combined = torch.cat([weighted_tree, weighted_image], dim=1)
        fused = self.cross_proj(combined)

        return fused


class BiDirectionalCrossAttention(nn.Module):
    """Bidirectional cross-attention: tree→image and image→tree"""
    def __init__(self, dim, num_heads=4, dropout=0.1):
        super(BiDirectionalCrossAttention, self).__init__()
        self.num_heads = num_heads
        self.dim = dim
        self.head_dim = dim // num_heads

        assert dim % num_heads == 0, "dim must be divisible by num_heads"

        # Tree → Image attention
        self.tree2img_q = nn.Linear(dim, dim)
        self.tree2img_k = nn.Linear(dim, dim)
        self.tree2img_v = nn.Linear(dim, dim)
        self.tree2img_out = nn.Linear(dim, dim)

        # Image → Tree attention
        self.img2tree_q = nn.Linear(dim, dim)
        self.img2tree_k = nn.Linear(dim, dim)
        self.img2tree_v = nn.Linear(dim, dim)
        self.img2tree_out = nn.Linear(dim, dim)

        self.dropout = nn.Dropout(dropout)
        self.scale = self.head_dim ** -0.5

        # Layer norm
        self.norm1 = nn.LayerNorm(dim)
        self.norm2 = nn.LayerNorm(dim)

    def _compute_attention(self, q_proj, k_proj, v_proj, query, key_value):
        """Helper function to compute cross-attention"""
        B = query.shape[0]

        # Add sequence dimension
        query = query.unsqueeze(1)  # (B, 1, dim)
        key_value = key_value.unsqueeze(1)  # (B, 1, dim)

        # Project
        Q = q_proj(query).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
        K = k_proj(key_value).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
        V = v_proj(key_value).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)

        # Attention
        attn = (Q @ K.transpose(-2, -1)) * self.scale
        attn = torch.softmax(attn, dim=-1)
        attn = self.dropout(attn)

        # Apply to values
        out = (attn @ V).transpose(1, 2).contiguous().view(B, 1, self.dim)
        return out.squeeze(1)

    def forward(self, tree_feat, image_feat):
        """
        Args:
            tree_feat: (B, dim)
            image_feat: (B, dim)
        Returns:
            tree_enhanced: (B, dim)
            image_enhanced: (B, dim)
        """
        # Tree attends to image
        tree_enhanced = self._compute_attention(
            self.tree2img_q, self.tree2img_k, self.tree2img_v,
            tree_feat, image_feat
        )
        tree_enhanced = self.tree2img_out(tree_enhanced)
        tree_enhanced = self.norm1(tree_feat + tree_enhanced)

        # Image attends to tree
        image_enhanced = self._compute_attention(
            self.img2tree_q, self.img2tree_k, self.img2tree_v,
            image_feat, tree_feat
        )
        image_enhanced = self.img2tree_out(image_enhanced)
        image_enhanced = self.norm2(image_feat + image_enhanced)

        # Concatenate both enhanced features
        fused = torch.cat([tree_enhanced, image_enhanced], dim=1)

        return fused


class GatedFusion(nn.Module):
    """Gated fusion with learnable gates for tree and image modalities"""
    def __init__(self, dim, dropout=0.1):
        super(GatedFusion, self).__init__()
        self.dim = dim

        # Gating mechanism
        self.gate_tree = nn.Sequential(
            nn.Linear(dim * 2, dim),
            nn.Sigmoid()
        )
        self.gate_image = nn.Sequential(
            nn.Linear(dim * 2, dim),
            nn.Sigmoid()
        )

        # Feature transformation
        self.tree_transform = nn.Sequential(
            nn.Linear(dim, dim),
            nn.ReLU(),
            nn.Dropout(dropout)
        )
        self.image_transform = nn.Sequential(
            nn.Linear(dim, dim),
            nn.ReLU(),
            nn.Dropout(dropout)
        )

        # Output projection
        self.output = nn.Linear(dim, dim)

    def forward(self, tree_feat, image_feat):
        """
        Args:
            tree_feat: (B, dim)
            image_feat: (B, dim)
        Returns:
            fused: (B, dim)
        """
        # Concatenate features for gating
        combined = torch.cat([tree_feat, image_feat], dim=1)

        # Compute gates
        gate_t = self.gate_tree(combined)
        gate_i = self.gate_image(combined)

        # Transform features
        tree_transformed = self.tree_transform(tree_feat)
        image_transformed = self.image_transform(image_feat)

        # Apply gates
        gated_tree = gate_t * tree_transformed
        gated_image = gate_i * image_transformed

        # Combine
        fused = gated_tree + gated_image
        fused = self.output(fused)

        return fused


class MultiHeadCrossModalAttention(nn.Module):
    """Multi-head cross-modal attention for flexible fusion"""
    def __init__(self, dim, num_heads=8, dropout=0.1):
        super(MultiHeadCrossModalAttention, self).__init__()
        self.num_heads = num_heads
        self.dim = dim
        self.head_dim = dim // num_heads

        assert dim % num_heads == 0, "dim must be divisible by num_heads"

        # Projections
        self.q_tree = nn.Linear(dim, dim)
        self.k_tree = nn.Linear(dim, dim)
        self.v_tree = nn.Linear(dim, dim)

        self.q_image = nn.Linear(dim, dim)
        self.k_image = nn.Linear(dim, dim)
        self.v_image = nn.Linear(dim, dim)

        self.out_proj = nn.Linear(dim * 2, dim)
        self.dropout = nn.Dropout(dropout)
        self.scale = self.head_dim ** -0.5

        self.norm = nn.LayerNorm(dim)

    def forward(self, tree_feat, image_feat):
        """
        Args:
            tree_feat: (B, dim)
            image_feat: (B, dim)
        Returns:
            fused: (B, dim)
        """
        B = tree_feat.shape[0]

        # Add sequence dimension
        tree_feat = tree_feat.unsqueeze(1)  # (B, 1, dim)
        image_feat = image_feat.unsqueeze(1)  # (B, 1, dim)

        # Project tree features
        Q_t = self.q_tree(tree_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
        K_t = self.k_tree(tree_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
        V_t = self.v_tree(tree_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)

        # Project image features
        Q_i = self.q_image(image_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
        K_i = self.k_image(image_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)
        V_i = self.v_image(image_feat).view(B, 1, self.num_heads, self.head_dim).transpose(1, 2)

        # Tree self-attention with image context
        attn_t = (Q_t @ K_i.transpose(-2, -1)) * self.scale
        attn_t = torch.softmax(attn_t, dim=-1)
        attn_t = self.dropout(attn_t)
        out_t = (attn_t @ V_i).transpose(1, 2).contiguous().view(B, 1, self.dim)

        # Image self-attention with tree context
        attn_i = (Q_i @ K_t.transpose(-2, -1)) * self.scale
        attn_i = torch.softmax(attn_i, dim=-1)
        attn_i = self.dropout(attn_i)
        out_i = (attn_i @ V_t).transpose(1, 2).contiguous().view(B, 1, self.dim)

        # Concatenate and project
        combined = torch.cat([out_t.squeeze(1), out_i.squeeze(1)], dim=1)
        fused = self.out_proj(combined)
        fused = self.norm(fused)

        return fused