File size: 8,386 Bytes
ce20bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
OICIO Memory Fabric: EM-LLM Surprise-based Event Segmentation
Credits: deepRcurs Labs, @deeprcurs / Mzed Imamkh @mzedimamkh

Berdasarkan:
- EM-LLM ICLR 2025: Human-inspired Episodic Memory for Infinite Context LLMs
- Bayesian surprise + graph-theoretic boundary refinement

POC: Implementasi surprise segmentation sederhana
"""

import numpy as np
import torch
from typing import List, Tuple

class SurpriseSegmenter:
    """
    Segment sequence into episodic events based on surprise
    """
    def __init__(self, 
                 gamma: float = 1.0,  # std scaling factor (paper: surprisal_threshold_gamma)
                 min_block_size: int = 8,
                 max_block_size: int = 128,
                 use_refinement: bool = True):
        self.gamma = gamma
        self.min_block_size = min_block_size
        self.max_block_size = max_block_size
        self.use_refinement = use_refinement

    def compute_surprise(self, embeddings: np.ndarray) -> np.ndarray:
        """
        Compute surprise per token
        For POC, we use simple methods:
        - Option 1: distance to previous token (prediction error proxy)
        - Option 2: -log prob proxy via embedding norm change

        Real EM-LLM uses LLM's own next-token prediction loss

        Input: [seq_len, dim]
        Output: [seq_len] surprise scores
        """
        seq_len, dim = embeddings.shape
        surprise = np.zeros(seq_len)

        # Surprise as L2 distance to previous token (simple proxy)
        # High distance = high prediction error = event boundary
        for i in range(1, seq_len):
            dist = np.linalg.norm(embeddings[i] - embeddings[i-1])
            surprise[i] = dist

        # Also add norm change surprise
        norms = np.linalg.norm(embeddings, axis=1)
        norm_change = np.abs(np.diff(norms, prepend=norms[0]))
        surprise = surprise * 0.7 + norm_change * 0.3

        return surprise

    def initial_segmentation(self, surprise: np.ndarray) -> List[int]:
        """
        Initial segmentation via surprise threshold
        Returns list of boundary indices
        """
        # Threshold = mean + gamma * std (from paper)
        mean_surprise = np.mean(surprise)
        std_surprise = np.std(surprise)
        threshold = mean_surprise + self.gamma * std_surprise

        boundaries = [0]  # start
        current_block_size = 0

        for i, s in enumerate(surprise):
            current_block_size += 1

            # If surprise high and block size >= min, create boundary
            if s > threshold and current_block_size >= self.min_block_size:
                boundaries.append(i)
                current_block_size = 0
            # If block too big, force split
            elif current_block_size >= self.max_block_size:
                boundaries.append(i)
                current_block_size = 0

        boundaries.append(len(surprise))  # end
        return sorted(list(set(boundaries)))

    def refinement(self, embeddings: np.ndarray, boundaries: List[int]) -> List[int]:
        """
        Graph-theoretic boundary refinement (simplified)
        Real paper uses modularity/conductance optimization

        POC: Use similarity within vs across blocks
        """
        if not self.use_refinement or len(boundaries) <= 2:
            return boundaries

        # Compute similarity matrix (cosine)
        # For POC, use small window refinement
        refined = [boundaries[0]]
        
        for i in range(1, len(boundaries)-1):
            prev_bound = boundaries[i-1]
            curr_bound = boundaries[i]
            next_bound = boundaries[i+1]

            # Current block: [prev_bound, curr_bound)
            # Next block: [curr_bound, next_bound)
            # Try shifting boundary by +/- min_block_size//2 to maximize cohesion

            best_boundary = curr_bound
            best_score = -1

            # Search window
            search_start = max(prev_bound + self.min_block_size, curr_bound - self.min_block_size//2)
            search_end = min(next_bound - self.min_block_size, curr_bound + self.min_block_size//2)

            for candidate in range(search_start, search_end+1):
                # Compute within-block similarity vs cross-block
                block1 = embeddings[prev_bound:candidate]
                block2 = embeddings[candidate:next_bound]

                if len(block1) == 0 or len(block2) == 0:
                    continue

                # Within similarity (cohesion)
                # Mean pairwise cosine within block1 + within block2
                def mean_sim(block):
                    if len(block) <= 1:
                        return 0
                    # Normalize
                    normed = block / (np.linalg.norm(block, axis=1, keepdims=True) + 1e-8)
                    sim_matrix = normed @ normed.T
                    # Upper triangle mean
                    triu = np.triu(sim_matrix, k=1)
                    return np.mean(triu[triu != 0]) if np.any(triu != 0) else 0

                within1 = mean_sim(block1)
                within2 = mean_sim(block2)

                # Cross similarity (separation) - should be low
                normed1 = block1 / (np.linalg.norm(block1, axis=1, keepdims=True) + 1e-8)
                normed2 = block2 / (np.linalg.norm(block2, axis=1, keepdims=True) + 1e-8)
                cross = np.mean(normed1 @ normed2.T) if len(block1) > 0 and len(block2) > 0 else 0

                # Modularity-like score: within - cross
                score = (within1 + within2) - 2 * cross

                if score > best_score:
                    best_score = score
                    best_boundary = candidate

            refined.append(best_boundary)

        refined.append(boundaries[-1])
        return sorted(list(set(refined)))

    def segment(self, embeddings: np.ndarray) -> Tuple[List[int], np.ndarray, List[Tuple[int,int]]]:
        """
        Full segmentation pipeline
        Returns: boundaries, surprise scores, blocks as (start, end) tuples
        """
        surprise = self.compute_surprise(embeddings)
        boundaries = self.initial_segmentation(surprise)
        if self.use_refinement:
            boundaries = self.refinement(embeddings, boundaries)

        blocks = [(boundaries[i], boundaries[i+1]) for i in range(len(boundaries)-1)]

        return boundaries, surprise, blocks

    def get_representative_tokens(self, embeddings: np.ndarray, blocks: List[Tuple[int,int]], topk: int = 4) -> List[np.ndarray]:
        """
        Get representative tokens per block (like InfLLM/EM-LLM)
        Select topk tokens with highest influence (e.g., highest norm or attention score proxy)
        """
        representatives = []
        for start, end in blocks:
            block_emb = embeddings[start:end]
            if len(block_emb) == 0:
                continue
            # For POC: select tokens with highest L2 norm as most influential
            norms = np.linalg.norm(block_emb, axis=1)
            topk_idx = np.argsort(norms)[-topk:][::-1]
            rep = block_emb[topk_idx]
            representatives.append(rep)

        return representatives

# Demo
if __name__ == "__main__":
    print("=== EM-LLM Surprise Segmentation POC ===")
    seq_len = 1000
    dim = 64

    # Simulate document with 3 topics (events)
    # Topic 1: embeddings around [1,0,0...], Topic 2: [0,1,0...], Topic 3: [0,0,1...]
    embeddings = []
    for i in range(seq_len):
        if i < 300:
            emb = np.random.randn(dim) * 0.1
            emb[0] += 2.0
        elif i < 700:
            emb = np.random.randn(dim) * 0.1
            emb[1] += 2.0
        else:
            emb = np.random.randn(dim) * 0.1
            emb[2] += 2.0
        embeddings.append(emb)
    embeddings = np.array(embeddings)

    segmenter = SurpriseSegmenter(gamma=1.0, min_block_size=8, max_block_size=128)
    boundaries, surprise, blocks = segmenter.segment(embeddings)

    print(f"Found {len(blocks)} events, boundaries: {boundaries[:10]}...")
    print(f"Surprise stats: mean={np.mean(surprise):.3f}, std={np.std(surprise):.3f}, max={np.max(surprise):.3f}")
    print(f"Block sizes: {[end-start for start,end in blocks[:5]]}...")

    reps = segmenter.get_representative_tokens(embeddings, blocks, topk=4)
    print(f"Representative tokens per block: {[r.shape for r in reps[:3]]}")