Instructions to use microsoft/colipri with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- COLIPRI
How to use microsoft/colipri with COLIPRI:
pip install colipri
from colipri import get_model from colipri import get_processor from colipri import load_sample_ct from colipri import ZeroShotImageClassificationPipeline model = get_model().cuda() processor = get_processor() pipeline = ZeroShotImageClassificationPipeline("microsoft/colipri", processor) image = load_sample_ct() pipeline(image, ["No lung nodules", "Lung nodules"]) - Notebooks
- Google Colab
- Kaggle
Mask padding tokens during text pooling
#7
by fepegar - opened
- src/colipri/model/text.py +4 -1
- src/colipri/pooling.py +83 -7
- src/colipri/types.py +2 -0
- tests/test_pooling.py +47 -0
- tests/test_text.py +52 -0
src/colipri/model/text.py
CHANGED
|
@@ -36,7 +36,10 @@ class TextEncoder(nn.Module, ModelMixin):
|
|
| 36 |
output = self.backbone(token_ids, attention_mask=attention_mask)
|
| 37 |
embeddings = output["last_hidden_state"]
|
| 38 |
if pool:
|
| 39 |
-
embeddings = self.pooler(
|
|
|
|
|
|
|
|
|
|
| 40 |
if normalize:
|
| 41 |
embeddings = F.normalize(embeddings, dim=1)
|
| 42 |
return embeddings
|
|
|
|
| 36 |
output = self.backbone(token_ids, attention_mask=attention_mask)
|
| 37 |
embeddings = output["last_hidden_state"]
|
| 38 |
if pool:
|
| 39 |
+
embeddings = self.pooler(
|
| 40 |
+
embeddings,
|
| 41 |
+
attention_mask=attention_mask,
|
| 42 |
+
)
|
| 43 |
if normalize:
|
| 44 |
embeddings = F.normalize(embeddings, dim=1)
|
| 45 |
return embeddings
|
src/colipri/pooling.py
CHANGED
|
@@ -4,6 +4,57 @@ from torch import nn
|
|
| 4 |
|
| 5 |
from .types import TypePooledEmbeddings
|
| 6 |
from .types import TypeSequenceEmbeddings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
class AttentionPool1D(nn.Module):
|
|
@@ -11,10 +62,24 @@ class AttentionPool1D(nn.Module):
|
|
| 11 |
super().__init__()
|
| 12 |
self.attn = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
|
| 13 |
|
| 14 |
-
def forward(
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
key = value = x
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
return rearrange(pooled, "batch 1 embed_dim -> batch embed_dim")
|
| 19 |
|
| 20 |
def to_dense(self):
|
|
@@ -62,13 +127,24 @@ class MultiLearnedQueryAttentionPool1D(AttentionPool1D):
|
|
| 62 |
# 4 Queries instead of 1
|
| 63 |
self.query = nn.Parameter(torch.randn(1, 4, embed_dim) / embed_dim**0.5)
|
| 64 |
|
| 65 |
-
def forward(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
"""
|
| 67 |
x: [B, T, D] — sequence of token embeddings
|
| 68 |
returns: [B, D] — pooled representation
|
| 69 |
"""
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
# pooled: [4, B, D_out], want [B, D_out] by pooling over queries (mean)
|
| 74 |
return pooled.mean(dim=1) # [B, D]
|
|
|
|
| 4 |
|
| 5 |
from .types import TypePooledEmbeddings
|
| 6 |
from .types import TypeSequenceEmbeddings
|
| 7 |
+
from .types import TypeTextAttentionMask
|
| 8 |
+
from .types import TypeValidTokenMask
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _get_valid_token_mask(
|
| 12 |
+
sequence: TypeSequenceEmbeddings,
|
| 13 |
+
attention_mask: TypeTextAttentionMask | None,
|
| 14 |
+
) -> TypeValidTokenMask | None:
|
| 15 |
+
"""Get a validated Boolean mask for valid sequence positions.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
sequence: Sequence embeddings.
|
| 19 |
+
attention_mask: Mask where nonzero entries identify valid positions.
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
A Boolean mask on the sequence device, or `None` when no mask is provided.
|
| 23 |
+
|
| 24 |
+
Raises:
|
| 25 |
+
ValueError: If the mask shape does not match the sequence or any sequence
|
| 26 |
+
contains no valid positions.
|
| 27 |
+
"""
|
| 28 |
+
if attention_mask is None:
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
expected_shape = sequence.shape[:2]
|
| 32 |
+
if attention_mask.shape != expected_shape:
|
| 33 |
+
msg = (
|
| 34 |
+
f"Expected attention_mask shape {expected_shape},"
|
| 35 |
+
f" but got {attention_mask.shape}"
|
| 36 |
+
)
|
| 37 |
+
raise ValueError(msg)
|
| 38 |
+
|
| 39 |
+
valid_mask = attention_mask.to(device=sequence.device, dtype=torch.bool)
|
| 40 |
+
if not valid_mask.any(dim=1).all():
|
| 41 |
+
msg = "Each sequence must contain at least one valid token"
|
| 42 |
+
raise ValueError(msg)
|
| 43 |
+
return valid_mask
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _compute_masked_mean(
|
| 47 |
+
sequence: TypeSequenceEmbeddings,
|
| 48 |
+
valid_mask: TypeValidTokenMask | None,
|
| 49 |
+
) -> TypePooledEmbeddings:
|
| 50 |
+
"""Compute the sequence mean while excluding masked positions."""
|
| 51 |
+
if valid_mask is None:
|
| 52 |
+
return sequence.mean(dim=1)
|
| 53 |
+
weights = rearrange(
|
| 54 |
+
valid_mask,
|
| 55 |
+
"batch length -> batch length 1",
|
| 56 |
+
).to(sequence.dtype)
|
| 57 |
+
return (sequence * weights).sum(dim=1) / weights.sum(dim=1)
|
| 58 |
|
| 59 |
|
| 60 |
class AttentionPool1D(nn.Module):
|
|
|
|
| 62 |
super().__init__()
|
| 63 |
self.attn = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
|
| 64 |
|
| 65 |
+
def forward(
|
| 66 |
+
self,
|
| 67 |
+
x: TypeSequenceEmbeddings,
|
| 68 |
+
attention_mask: TypeTextAttentionMask | None = None,
|
| 69 |
+
) -> TypePooledEmbeddings:
|
| 70 |
+
valid_mask = _get_valid_token_mask(x, attention_mask)
|
| 71 |
+
query = rearrange(
|
| 72 |
+
_compute_masked_mean(x, valid_mask),
|
| 73 |
+
"batch embed_dim -> batch 1 embed_dim",
|
| 74 |
+
)
|
| 75 |
key = value = x
|
| 76 |
+
key_padding_mask = None if valid_mask is None else ~valid_mask
|
| 77 |
+
pooled, _ = self.attn(
|
| 78 |
+
query,
|
| 79 |
+
key,
|
| 80 |
+
value,
|
| 81 |
+
key_padding_mask=key_padding_mask,
|
| 82 |
+
)
|
| 83 |
return rearrange(pooled, "batch 1 embed_dim -> batch embed_dim")
|
| 84 |
|
| 85 |
def to_dense(self):
|
|
|
|
| 127 |
# 4 Queries instead of 1
|
| 128 |
self.query = nn.Parameter(torch.randn(1, 4, embed_dim) / embed_dim**0.5)
|
| 129 |
|
| 130 |
+
def forward(
|
| 131 |
+
self,
|
| 132 |
+
x: TypeSequenceEmbeddings,
|
| 133 |
+
attention_mask: TypeTextAttentionMask | None = None,
|
| 134 |
+
) -> TypePooledEmbeddings:
|
| 135 |
"""
|
| 136 |
x: [B, T, D] — sequence of token embeddings
|
| 137 |
returns: [B, D] — pooled representation
|
| 138 |
"""
|
| 139 |
+
valid_mask = _get_valid_token_mask(x, attention_mask)
|
| 140 |
+
batch_size = x.shape[0]
|
| 141 |
+
query = self.query.expand(batch_size, -1, -1) # [B, 4, D]
|
| 142 |
+
key_padding_mask = None if valid_mask is None else ~valid_mask
|
| 143 |
+
pooled, _ = self.attn(
|
| 144 |
+
query,
|
| 145 |
+
x,
|
| 146 |
+
x,
|
| 147 |
+
key_padding_mask=key_padding_mask,
|
| 148 |
+
)
|
| 149 |
# pooled: [4, B, D_out], want [B, D_out] by pooling over queries (mean)
|
| 150 |
return pooled.mean(dim=1) # [B, D]
|
src/colipri/types.py
CHANGED
|
@@ -2,6 +2,7 @@ import os
|
|
| 2 |
from typing import TypeAlias
|
| 3 |
|
| 4 |
import torchio as tio
|
|
|
|
| 5 |
from jaxtyping import Float
|
| 6 |
from jaxtyping import Int64
|
| 7 |
from torch import Tensor
|
|
@@ -25,6 +26,7 @@ TypeImagesTensor: TypeAlias = Float[Tensor, "batch 1 x_in y_in z_in"]
|
|
| 25 |
|
| 26 |
TypeTextTokenIds: TypeAlias = Int64[Tensor, "batch text_length"]
|
| 27 |
TypeTextAttentionMask: TypeAlias = Int64[Tensor, "batch text_length"]
|
|
|
|
| 28 |
|
| 29 |
# Outputs
|
| 30 |
TypeSequenceEmbeddings: TypeAlias = Float[Tensor, "batch sequence_length embed_dim"]
|
|
|
|
| 2 |
from typing import TypeAlias
|
| 3 |
|
| 4 |
import torchio as tio
|
| 5 |
+
from jaxtyping import Bool
|
| 6 |
from jaxtyping import Float
|
| 7 |
from jaxtyping import Int64
|
| 8 |
from torch import Tensor
|
|
|
|
| 26 |
|
| 27 |
TypeTextTokenIds: TypeAlias = Int64[Tensor, "batch text_length"]
|
| 28 |
TypeTextAttentionMask: TypeAlias = Int64[Tensor, "batch text_length"]
|
| 29 |
+
TypeValidTokenMask: TypeAlias = Bool[Tensor, "batch sequence_length"]
|
| 30 |
|
| 31 |
# Outputs
|
| 32 |
TypeSequenceEmbeddings: TypeAlias = Float[Tensor, "batch sequence_length embed_dim"]
|
tests/test_pooling.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from colipri.pooling import AttentionPool1D
|
| 5 |
+
from colipri.pooling import MultiLearnedQueryAttentionPool1D
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@pytest.mark.parametrize(
|
| 9 |
+
"pool_type",
|
| 10 |
+
[AttentionPool1D, MultiLearnedQueryAttentionPool1D],
|
| 11 |
+
)
|
| 12 |
+
def test_ignores_masked_tokens(pool_type):
|
| 13 |
+
"""Ignore padded positions when pooling language embeddings."""
|
| 14 |
+
torch.manual_seed(0)
|
| 15 |
+
pool = pool_type(embed_dim=8, num_heads=2)
|
| 16 |
+
valid = torch.randn(2, 3, 8)
|
| 17 |
+
padded = torch.cat([valid, torch.full((2, 5, 8), 1e6)], dim=1)
|
| 18 |
+
valid_mask = torch.ones(2, 3, dtype=torch.long)
|
| 19 |
+
padded_mask = torch.cat(
|
| 20 |
+
[valid_mask, torch.zeros(2, 5, dtype=torch.long)],
|
| 21 |
+
dim=1,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
expected = pool(valid)
|
| 25 |
+
result = pool(padded, attention_mask=padded_mask)
|
| 26 |
+
|
| 27 |
+
torch.testing.assert_close(result, expected)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_rejects_incorrect_attention_mask_shape():
|
| 31 |
+
"""Reject an attention mask that does not match the input sequence."""
|
| 32 |
+
pool = AttentionPool1D(embed_dim=8, num_heads=2)
|
| 33 |
+
sequence = torch.randn(2, 3, 8)
|
| 34 |
+
attention_mask = torch.ones(2, 4, dtype=torch.long)
|
| 35 |
+
|
| 36 |
+
with pytest.raises(ValueError, match="attention_mask shape"):
|
| 37 |
+
pool(sequence, attention_mask=attention_mask)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_rejects_fully_masked_sequence():
|
| 41 |
+
"""Reject a sequence without any valid tokens."""
|
| 42 |
+
pool = AttentionPool1D(embed_dim=8, num_heads=2)
|
| 43 |
+
sequence = torch.randn(2, 3, 8)
|
| 44 |
+
attention_mask = torch.tensor([[1, 1, 1], [0, 0, 0]])
|
| 45 |
+
|
| 46 |
+
with pytest.raises(ValueError, match="at least one valid token"):
|
| 47 |
+
pool(sequence, attention_mask=attention_mask)
|
tests/test_text.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch import nn
|
| 3 |
+
|
| 4 |
+
from colipri.model.text import TextEncoder
|
| 5 |
+
from colipri.pooling import AttentionPool1D
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class _StubBackbone(nn.Module):
|
| 9 |
+
"""Encode token IDs as deterministic embeddings."""
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
"""Initialize embeddings with a distinctive padding-token vector."""
|
| 13 |
+
super().__init__()
|
| 14 |
+
weights = torch.tensor(
|
| 15 |
+
[
|
| 16 |
+
[100.0, -100.0, 50.0, -50.0],
|
| 17 |
+
[1.0, 0.0, 0.0, 0.0],
|
| 18 |
+
[0.0, 1.0, 0.0, 0.0],
|
| 19 |
+
[0.0, 0.0, 1.0, 0.0],
|
| 20 |
+
]
|
| 21 |
+
)
|
| 22 |
+
self.embedding = nn.Embedding.from_pretrained(weights, freeze=False)
|
| 23 |
+
|
| 24 |
+
def forward(self, token_ids, attention_mask=None): # noqa: ARG002
|
| 25 |
+
"""Encode token IDs without altering padded positions."""
|
| 26 |
+
return {"last_hidden_state": self.embedding(token_ids)}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_text_encoder_ignores_masked_padding():
|
| 30 |
+
"""Preserve the pooled embedding when masked padding is appended."""
|
| 31 |
+
torch.manual_seed(0)
|
| 32 |
+
encoder = TextEncoder(
|
| 33 |
+
backbone=_StubBackbone(),
|
| 34 |
+
pooler=AttentionPool1D(embed_dim=4, num_heads=2),
|
| 35 |
+
)
|
| 36 |
+
valid_ids = torch.tensor([[1, 2, 3]])
|
| 37 |
+
padded_ids = torch.tensor([[1, 2, 3, 0, 0]])
|
| 38 |
+
valid_mask = torch.ones_like(valid_ids)
|
| 39 |
+
padded_mask = torch.tensor([[1, 1, 1, 0, 0]])
|
| 40 |
+
|
| 41 |
+
expected = encoder(
|
| 42 |
+
valid_ids,
|
| 43 |
+
valid_mask,
|
| 44 |
+
normalize=False,
|
| 45 |
+
)
|
| 46 |
+
result = encoder(
|
| 47 |
+
padded_ids,
|
| 48 |
+
padded_mask,
|
| 49 |
+
normalize=False,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
torch.testing.assert_close(result, expected)
|