|
| 1 | +""" |
| 2 | +Model implementation for mono cross-encoder models. Originally introduced in |
| 3 | +`Passage Re-ranking with BERT |
| 4 | +<https://arxiv.org/abs/1901.04085>`_. |
| 5 | +""" |
| 6 | + |
| 7 | +from typing import Literal, Type |
| 8 | + |
| 9 | +import torch |
| 10 | +from transformers import BatchEncoding |
| 11 | + |
| 12 | +from ..base.model import batch_encoding_wrapper |
| 13 | +from ..cross_encoder.cross_encoder_config import CrossEncoderConfig |
| 14 | +from ..cross_encoder.cross_encoder_model import CrossEncoderModel, CrossEncoderOutput |
| 15 | + |
| 16 | + |
| 17 | +class ScaleLinear(torch.nn.Linear): |
| 18 | + |
| 19 | + def forward(self, input: torch.Tensor) -> torch.Tensor: |
| 20 | + # See https://github.com/tensorflow/mesh/blob/fa19d69eafc9a482aff0b59ddd96b025c0cb207d/mesh_tensorflow/transformer/transformer.py#L586 # noqa |
| 21 | + input = input * (input.shape[-1] ** -0.5) |
| 22 | + return super().forward(input) |
| 23 | + |
| 24 | + |
| 25 | +class MonoConfig(CrossEncoderConfig): |
| 26 | + """Configuration class for mono cross-encoder models.""" |
| 27 | + |
| 28 | + model_type = "mono" |
| 29 | + """Model type for mono cross-encoder models.""" |
| 30 | + |
| 31 | + def __init__( |
| 32 | + self, |
| 33 | + query_length: int = 32, |
| 34 | + doc_length: int = 512, |
| 35 | + pooling_strategy: Literal["first", "mean", "max", "sum", "bert_pool"] = "first", |
| 36 | + linear_bias: bool = False, |
| 37 | + scoring_strategy: Literal["mono", "rank"] = "rank", |
| 38 | + tokenizer_pattern: str | None = None, |
| 39 | + **kwargs, |
| 40 | + ): |
| 41 | + """Initialize the configuration for mono cross-encoder models.""" |
| 42 | + self._bert_pool = False |
| 43 | + if pooling_strategy == "bert_pool": |
| 44 | + self._bert_pool = True |
| 45 | + pooling_strategy = "first" |
| 46 | + super().__init__( |
| 47 | + query_length=query_length, |
| 48 | + doc_length=doc_length, |
| 49 | + pooling_strategy=pooling_strategy, |
| 50 | + linear_bias=linear_bias, |
| 51 | + **kwargs, |
| 52 | + ) |
| 53 | + self.scoring_strategy = scoring_strategy |
| 54 | + self.tokenizer_pattern = tokenizer_pattern |
| 55 | + |
| 56 | + |
| 57 | +class MonoModel(CrossEncoderModel): |
| 58 | + config_class: Type[MonoConfig] = MonoConfig |
| 59 | + """Configuration class for mono cross-encoder models.""" |
| 60 | + |
| 61 | + def __init__(self, config: MonoConfig, *args, **kwargs): |
| 62 | + """A cross-encoder model that jointly encodes a query and document(s). The contextualized embeddings are |
| 63 | + aggragated into a single vector and fed to a linear layer which computes a final relevance score. |
| 64 | +
|
| 65 | + :param config: Configuration for the cross-encoder model |
| 66 | + :type config: CrossEncoderConfig |
| 67 | + """ |
| 68 | + super().__init__(config, *args, **kwargs) |
| 69 | + |
| 70 | + if self.config.scoring_strategy == "mono": |
| 71 | + output_dim = 2 |
| 72 | + elif self.config.scoring_strategy == "rank": |
| 73 | + output_dim = 1 |
| 74 | + else: |
| 75 | + raise ValueError( |
| 76 | + f"Unknown scoring strategy {self.config.scoring_strategy}. Supported strategies are 'mono' and 'rank'." |
| 77 | + ) |
| 78 | + |
| 79 | + self.bert_pool = torch.nn.Identity() |
| 80 | + if self.config._bert_pool: |
| 81 | + self.bert_pool = torch.nn.Sequential( |
| 82 | + torch.nn.Linear(config.hidden_size, config.hidden_size), torch.nn.Tanh() |
| 83 | + ) |
| 84 | + |
| 85 | + if self.config.backbone_model_type == "t5": |
| 86 | + self.linear = ScaleLinear(config.hidden_size, output_dim, bias=self.config.linear_bias) |
| 87 | + else: |
| 88 | + self.linear = torch.nn.Linear(config.hidden_size, output_dim, bias=self.config.linear_bias) |
| 89 | + |
| 90 | + @batch_encoding_wrapper |
| 91 | + def forward(self, encoding: BatchEncoding) -> CrossEncoderOutput: |
| 92 | + """Computes contextualized embeddings for the joint query-document input sequence and computes a relevance |
| 93 | + score. |
| 94 | +
|
| 95 | + :param encoding: Tokenizer encoding for the joint query-document input sequence |
| 96 | + :type encoding: BatchEncoding |
| 97 | + :return: Output of the model |
| 98 | + :rtype: CrossEncoderOutput |
| 99 | + """ |
| 100 | + if hasattr(self, "decoder"): |
| 101 | + # NOTE hack to make T5 cross-encoders work. other encoder-decoder models may not have `decoder` as their |
| 102 | + # attribute. maybe find a better way to check for this? |
| 103 | + decoder_input_ids = torch.zeros( |
| 104 | + (encoding["input_ids"].shape[0], 1), device=encoding["input_ids"].device, dtype=torch.long |
| 105 | + ) |
| 106 | + encoding["decoder_input_ids"] = decoder_input_ids |
| 107 | + embeddings = self._backbone_forward(**encoding).last_hidden_state |
| 108 | + embeddings = self.pooling( |
| 109 | + embeddings, encoding.get("attention_mask", None), pooling_strategy=self.config.pooling_strategy |
| 110 | + ) |
| 111 | + embeddings = self.bert_pool(embeddings) |
| 112 | + scores = self.linear(embeddings) |
| 113 | + |
| 114 | + if self.config.scoring_strategy == "mono": |
| 115 | + scores = torch.nn.functional.log_softmax(scores.view(-1, 2), dim=-1)[:, 1] |
| 116 | + |
| 117 | + return CrossEncoderOutput(scores=scores.view(-1), embeddings=embeddings) |
0 commit comments