Repository: pykeen/pykeen
Issue: How to use interactions that require more than one entity representation with the NodePiece model?
Root cause: Unimplemented Feature Gap

Hi!

I am trying to add Mure Interaction to NodePiece model but get an error, which I don't know how to fix.
I tried specifying the shape an EmbeddingSpecification parameters but that didn't help :(

Code to reproduce:

```python
from pykeen.nn.modules import MuREInteraction
 
results_mure = pipeline(
    training=training,
    testing=testing,
    model='NodePiece',
    training_loop='sLCWA',
    negative_sampler='basic',
    stopper='early',
    validation=testing,
    training_kwargs = dict(num_epochs=200),
    model_kwargs = dict(
        interaction=MuREInteraction,
        interaction_kwargs=dict(p=2),
        num_tokens=5,
        aggregation='mlp',
    ),
    stopper_kwargs=dict(frequency=3, patience=5, relative_delta=0.0001),
    negative_sampler_kwargs=dict(
        filtered=True,
        filterer='python-set', 
        num_negs_per_pos = 3,
        corruption_scheme=('h')
    ),
    loss='softplus',
    random_seed=42
)
```


Traceback:

```python
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/tmp/ipykernel_3689/4030775426.py in <module>
      7 # # mure_inter.relation_shape=('d',)
      8
----> 9 results_mure = pipeline(
     10     training=training,
     11     testing=testing,
 
~/pykeen-master/src/pykeen/pipeline/api.py in pipeline(dataset, dataset_kwargs, training, testing, validation, evaluation_entity_whitelist, evaluation_relation_whitelist, model, model_kwargs, interaction, interaction_kwargs, dimensions, loss, loss_kwargs, regularizer, regularizer_kwargs, optimizer, optimizer_kwargs, clear_optimizer, lr_scheduler, lr_scheduler_kwargs, training_loop, training_loop_kwargs, negative_sampler, negative_sampler_kwargs, epochs, training_kwargs, stopper, stopper_kwargs, evaluator, evaluator_kwargs, evaluation_kwargs, result_tracker, result_tracker_kwargs, metadata, device, random_seed, use_testing_data, evaluation_fallback, filter_validation_when_testing, use_tqdm)
   1017         # TODO should kwargs for loss and regularizer be checked and raised for?
   1018     else:
-> 1019         model_instance = _build_model_helper(
   1020             model=model,
   1021             model_kwargs=model_kwargs,
 
~/pykeen-master/src/pykeen/pipeline/api.py in _build_model_helper(model, model_kwargs, loss, loss_kwargs, _device, _random_seed, regularizer, regularizer_kwargs, training_triples_factory)
    738     loss_instance = loss_resolver.make(loss, loss_kwargs)
    739
--> 740     return model_resolver.make(
    741         model,
    742         triples_factory=training_triples_factory,
 
/opt/app-root/lib64/python3.8/site-packages/class_resolver/api.py in make(self, query, pos_kwargs, **kwargs)
    255             cls: Type[X] = self.lookup(query)
    256             try:
--> 257                 return cls(**(pos_kwargs or {}), **kwargs)  # type: ignore
    258             except TypeError as e:
    259                 if "required keyword-only argument" in e.args[0]:
 
~/pykeen-master/src/pykeen/models/unimodal/node_piece.py in __init__(self, triples_factory, num_tokens, embedding_dim, embedding_specification, interaction, aggregation, shape, **kwargs)
    159             num_tokens=num_tokens,
    160         )
--> 161         super().__init__(
    162             triples_factory=triples_factory,
    163             interaction=interaction,
 
~/pykeen-master/src/pykeen/models/nbase.py in __init__(self, triples_factory, interaction, interaction_kwargs, entity_representations, relation_representations, loss, predict_with_sigmoid, preferred_device, random_seed)
    367         )
    368         self.interaction = interaction_resolver.make(interaction, pos_kwargs=interaction_kwargs)
--> 369         self.entity_representations = _prepare_representation_module_list(
    370             representations=entity_representations,
    371             num_embeddings=triples_factory.num_entities,
 
~/pykeen-master/src/pykeen/models/nbase.py in _prepare_representation_module_list(representations, num_embeddings, shapes, label, skip_checks)
    260         representations = [representations]
    261     if not skip_checks and len(representations) != len(shapes):
--> 262         raise ValueError(
    263             f"Interaction function requires {len(shapes)} {label} representations, but "
    264             f"{len(representations)} were given.",
 
ValueError: Interaction function requires 3 entity representations, but 1 were given.
```

Hi @tatiana-iazykova ,

[MuRE](https://github.com/pykeen/pykeen/blob/16077e7002aa52a8e1e4b2dfa0543c185318ca79/src/pykeen/nn/modules.py#L1184-L1199) requires more than one representation (~vector/matrix) per entity (and relation). More precisely, we need
* for each entity
   * one vector of dimension `d`
   * one head bias (a scalar)
   * one tail bias (a scalar)
* for each relation
   * a multiplicative term, a vector of shape `d` (_note: in the paper, this is discussed as a diagonal matrix of shape `(d, d)`, however, since we only have the diagonal entries, we can equivalently formulate this as a vector of shape `d` and replace the matrix multiplication by the Hadamard product_)
   * an additive term, a vector of shape `d`


[NodePieceRepresentation](https://github.com/pykeen/pykeen/blob/16077e7002aa52a8e1e4b2dfa0543c185318ca79/src/pykeen/nn/emb.py#L1194) does provide one vector for each entity ID. We can use this for, e.g., the entity vector, but need to provide additional biases from somewhere else.

This is a bit more involved. The easiest method I can see right now, is to create a new class `NodePieceMuRE` similar to the [`NodePiece` model](https://github.com/pykeen/pykeen/blob/16077e7002aa52a8e1e4b2dfa0543c185318ca79/src/pykeen/models/unimodal/node_piece.py), and [`MuRE` model](https://github.com/pykeen/pykeen/blob/16077e7002aa52a8e1e4b2dfa0543c185318ca79/src/pykeen/models/unimodal/mure.py);

```python
from typing import Any, Callable, ClassVar, Mapping, Optional, Sequence, Union

import torch
from class_resolver import Hint, HintOrType
from torch.nn.init import normal_, zeros_

from pykeen.models.nbase import ERModel
from pykeen.constants import DEFAULT_EMBEDDING_HPO_EMBEDDING_DIM_RANGE
from pykeen.nn.emb import EmbeddingSpecification, NodePieceRepresentation, SubsetRepresentationModule
from pykeen.nn.modules import MuREInteraction, Interaction
from pykeen.triples.triples_factory import CoreTriplesFactory


class NodePieceMuRE(ERModel):
    """A wrapper which combines NodePiece entity representations from [galkin2021]_ with the MuRE interaction."""

    hpo_default: ClassVar[Mapping[str, Any]] = dict(
        embedding_dim=DEFAULT_EMBEDDING_HPO_EMBEDDING_DIM_RANGE,
    )

    def __init__(
        self,
        *,
        triples_factory: CoreTriplesFactory,
        num_tokens: int = 2,
        embedding_dim: int = 64,
        embedding_specification: Optional[EmbeddingSpecification] = None,
        interaction: HintOrType[Interaction] = MuREInteraction,
        aggregation: Hint[Callable[[torch.Tensor, int], torch.Tensor]] = torch.mean,
        shape: Optional[Sequence[int]] = None,
        **kwargs,
    ) -> None:
        if not triples_factory.create_inverse_triples:
            raise ValueError(
                "The provided triples factory does not create inverse triples. However, for the node piece "
                "representations inverse relation representations are required.",
            )
        embedding_specification = embedding_specification or EmbeddingSpecification(
            shape=(embedding_dim,),
        )
        # always create representations for normal and inverse relations and padding
        relation_representations = embedding_specification.make(
            num_embeddings=2 * triples_factory.real_num_relations + 1,
        )
        entity_representations = NodePieceRepresentation(
            triples_factory=triples_factory,
            token_representation=relation_representations,
            aggregation=aggregation,
            shape=shape,
            num_tokens=num_tokens,
        )
        super().__init__(
            triples_factory=triples_factory,
            interaction=interaction,
            entity_representations=[
                entity_representations,
                # entity bias for head
                EmbeddingSpecification(
                    shape=tuple(),  # scalar
                    initializer=zeros_,
                ),
                # entity bias for tail
                EmbeddingSpecification(
                    shape=tuple(),  # scalar
                    initializer=zeros_,
                ),
            ],
            relation_representations=[
                # relation offset
                EmbeddingSpecification(
                    embedding_dim=embedding_dim,
                    initializer=normal_,
                    initializer_kwargs=dict(
                        std=1.0e-03,
                    ),
                ),
                # the diagonal relation matrices
                # these are the nodepiece linked representations
                SubsetRepresentationModule(  # hide padding relation
                    relation_representations,
                    max_id=triples_factory.num_relations,
                ),
            ],
            **kwargs,
        )
```

Note that there are some choices I made, e.g., that the diagonal relation matrices are the relation weights which are linked via `NodePiece`, but not the relation offsets. 

Wow! I haven't expected to get the answer so fast :)
Thanks a lot for your help!