|
| 1 | +import keras |
| 2 | + |
| 3 | +from keras_hub.src.api_export import keras_hub_export |
| 4 | +from keras_hub.src.models.backbone import Backbone |
| 5 | + |
| 6 | + |
| 7 | +@keras_hub_export("keras_hub.models.BLIP2Backbone") |
| 8 | +class BLIP2Backbone(Backbone): |
| 9 | + """BLIP-2 core network. |
| 10 | +
|
| 11 | + BLIP-2 is a vision-language model that connects a frozen image encoder |
| 12 | + and a frozen large language model (LLM) through a lightweight trainable |
| 13 | + Querying Transformer (Q-Former). The Q-Former distills visual information |
| 14 | + into a fixed number of query embeddings which are then fed as a soft visual |
| 15 | + prompt to the language model. |
| 16 | +
|
| 17 | + The forward pass follows three stages: |
| 18 | + 1. A `BLIP2VisionEncoder` (ViT) maps raw images to patch features. |
| 19 | + 2. A `BLIP2QFormer` cross-attends learned query tokens against those |
| 20 | + patch features and produces a compact set of visual embeddings. |
| 21 | + 3. A language model (OPT or Flan-T5) receives the query embeddings |
| 22 | + prepended to its token sequence and generates text. |
| 23 | +
|
| 24 | + When `vision_encoder` is `None` the backbone operates in text-only mode: |
| 25 | + the Q-Former is bypassed and the language model receives only token ids. |
| 26 | +
|
| 27 | + For a higher-level text-generation interface see |
| 28 | + `keras_hub.models.BLIP2CausalLM`. |
| 29 | +
|
| 30 | + Args: |
| 31 | + vision_encoder: A `keras_hub.models.BLIP2VisionEncoder` instance. Pass |
| 32 | + `None` for a text-only backbone. |
| 33 | + qformer: A `keras_hub.models.BLIP2QFormer` instance. Pass `None` when |
| 34 | + `vision_encoder` is `None`. |
| 35 | + language_model: The language model instance (e.g. `BLIP2CustomOPT` |
| 36 | + or `BLIP2FlanT5`). |
| 37 | + dtype: string or `keras.mixed_precision.DTypePolicy`. Dtype used for |
| 38 | + model computations and weights. Defaults to `None` (Keras global |
| 39 | + default). |
| 40 | + **kwargs: Additional keyword arguments forwarded to the base |
| 41 | + `Backbone` / `keras.Model`. |
| 42 | +
|
| 43 | + Example: |
| 44 | + ```python |
| 45 | + # Text-only (no vision encoder) |
| 46 | + backbone = keras_hub.models.BLIP2Backbone( |
| 47 | + vision_encoder=None, |
| 48 | + qformer=None, |
| 49 | + language_model=my_opt, |
| 50 | + ) |
| 51 | + output = backbone({"token_ids": token_ids, "padding_mask": mask}) |
| 52 | +
|
| 53 | + # Full vision-language backbone |
| 54 | + backbone = keras_hub.models.BLIP2Backbone.from_preset("blip2_opt_2.7b") |
| 55 | + output = backbone({ |
| 56 | + "images": images, # (B, H, W, 3) |
| 57 | + "token_ids": token_ids, # (B, seq_len) |
| 58 | + "padding_mask": mask, # (B, seq_len) |
| 59 | + }) |
| 60 | + ``` |
| 61 | + """ |
| 62 | + |
| 63 | + def __init__( |
| 64 | + self, |
| 65 | + vision_encoder, |
| 66 | + qformer, |
| 67 | + language_model, |
| 68 | + dtype=None, |
| 69 | + **kwargs, |
| 70 | + ): |
| 71 | + self.vision_encoder = vision_encoder |
| 72 | + self.qformer = qformer |
| 73 | + self.language_model = language_model |
| 74 | + |
| 75 | + multimodal = self.vision_encoder is not None |
| 76 | + |
| 77 | + # Encoder-decoder language models (Flan-T5) follow the keras-hub |
| 78 | + # seq2seq convention and name their encoder inputs |
| 79 | + # `encoder_token_ids` / `encoder_padding_mask`; decoder-only language |
| 80 | + # models (OPT) keep the plain `token_ids` / `padding_mask`. |
| 81 | + is_encoder_decoder = hasattr( |
| 82 | + self.language_model, "encoder_transformer_layers" |
| 83 | + ) |
| 84 | + if is_encoder_decoder: |
| 85 | + token_ids_name, padding_mask_name = ( |
| 86 | + "encoder_token_ids", |
| 87 | + "encoder_padding_mask", |
| 88 | + ) |
| 89 | + else: |
| 90 | + token_ids_name, padding_mask_name = "token_ids", "padding_mask" |
| 91 | + |
| 92 | + token_ids_input = keras.Input( |
| 93 | + shape=(None,), dtype="int32", name=token_ids_name |
| 94 | + ) |
| 95 | + padding_mask_input = keras.Input( |
| 96 | + shape=(None,), dtype="int32", name=padding_mask_name |
| 97 | + ) |
| 98 | + inputs = { |
| 99 | + token_ids_name: token_ids_input, |
| 100 | + padding_mask_name: padding_mask_input, |
| 101 | + } |
| 102 | + |
| 103 | + if multimodal: |
| 104 | + raw_image_size = self.vision_encoder.image_size |
| 105 | + if isinstance(raw_image_size, (tuple, list)): |
| 106 | + image_h, image_w = ( |
| 107 | + int(raw_image_size[0]), |
| 108 | + int(raw_image_size[1]), |
| 109 | + ) |
| 110 | + else: |
| 111 | + image_h = image_w = int(raw_image_size) |
| 112 | + |
| 113 | + images_input = keras.Input( |
| 114 | + shape=(image_h, image_w, 3), |
| 115 | + dtype="float32", |
| 116 | + name="images", |
| 117 | + ) |
| 118 | + inputs["images"] = images_input |
| 119 | + |
| 120 | + patch_features = self.vision_encoder(images_input) |
| 121 | + query_embeddings = self.qformer(patch_features) |
| 122 | + else: |
| 123 | + query_embeddings = None |
| 124 | + |
| 125 | + # The language model itself always consumes `token_ids` / |
| 126 | + # `padding_mask` for its (encoder) input; the backbone maps the |
| 127 | + # externally exposed names onto these. |
| 128 | + lm_inputs = { |
| 129 | + "token_ids": token_ids_input, |
| 130 | + "padding_mask": padding_mask_input, |
| 131 | + } |
| 132 | + |
| 133 | + if is_encoder_decoder: |
| 134 | + decoder_token_ids_input = keras.Input( |
| 135 | + shape=(None,), dtype="int32", name="decoder_token_ids" |
| 136 | + ) |
| 137 | + decoder_padding_mask_input = keras.Input( |
| 138 | + shape=(None,), dtype="int32", name="decoder_padding_mask" |
| 139 | + ) |
| 140 | + inputs["decoder_token_ids"] = decoder_token_ids_input |
| 141 | + inputs["decoder_padding_mask"] = decoder_padding_mask_input |
| 142 | + lm_inputs["decoder_token_ids"] = decoder_token_ids_input |
| 143 | + lm_inputs["decoder_padding_mask"] = decoder_padding_mask_input |
| 144 | + |
| 145 | + if query_embeddings is not None: |
| 146 | + lm_inputs["qformer_features"] = query_embeddings |
| 147 | + |
| 148 | + output = self.language_model(lm_inputs) |
| 149 | + |
| 150 | + super().__init__( |
| 151 | + inputs=inputs, |
| 152 | + outputs=output, |
| 153 | + dtype=dtype, |
| 154 | + **kwargs, |
| 155 | + ) |
| 156 | + |
| 157 | + self.multimodal = multimodal |
| 158 | + |
| 159 | + @property |
| 160 | + def token_embedding(self): |
| 161 | + """The token embedding layer of the language model.""" |
| 162 | + return self.language_model.token_embedding |
| 163 | + |
| 164 | + @property |
| 165 | + def num_query_tokens(self): |
| 166 | + """Number of Q-Former query tokens (0 in text-only mode).""" |
| 167 | + return self.qformer.num_query_tokens if self.qformer is not None else 0 |
| 168 | + |
| 169 | + @property |
| 170 | + def qformer_hidden_dim(self): |
| 171 | + """Hidden dimensionality of the Q-Former (0 in text-only mode).""" |
| 172 | + return self.qformer.hidden_dim if self.qformer is not None else 0 |
| 173 | + |
| 174 | + def get_config(self): |
| 175 | + config = super().get_config() |
| 176 | + config.update( |
| 177 | + { |
| 178 | + "vision_encoder": ( |
| 179 | + keras.layers.serialize(self.vision_encoder) |
| 180 | + if self.vision_encoder is not None |
| 181 | + else None |
| 182 | + ), |
| 183 | + "qformer": ( |
| 184 | + keras.layers.serialize(self.qformer) |
| 185 | + if self.qformer is not None |
| 186 | + else None |
| 187 | + ), |
| 188 | + "language_model": keras.layers.serialize(self.language_model), |
| 189 | + } |
| 190 | + ) |
| 191 | + return config |
| 192 | + |
| 193 | + @classmethod |
| 194 | + def from_config(cls, config): |
| 195 | + config = dict(config) |
| 196 | + for key in ("vision_encoder", "qformer"): |
| 197 | + if config.get(key) is not None: |
| 198 | + config[key] = keras.layers.deserialize(config[key]) |
| 199 | + config["language_model"] = keras.layers.deserialize( |
| 200 | + config["language_model"] |
| 201 | + ) |
| 202 | + return cls(**config) |
0 commit comments