Repository: pykeen/pykeen
Issue: Customized negative sampling strategy
Root cause: Documentation Deficiency

Hi, for example, there are four triples in the graph: `(h1, r1, t1), (h2, r2, t2), (h3, r3, t3), (h4, r4, t4)`, and there are two classifications of the entities: `Category A: h1, t1, h2, t2; Category B: h3, t3, h4, t4`. For the generation of a negative triple for `(h1, r1, t1)`, the entities to replace `h1` are from `Category B` only (i.e., `t1, h2, t2` will not be sampled to replace `h1` to generate a negative `(h2, r1, t1)`). Are there ways to implement such a customized negative sampler (sorry what I found in the [doc](https://pykeen.readthedocs.io/en/stable/reference/negative_sampling.html#module-pykeen.sampling) are some fixed samplers)? Thanks!

Hi @Hao-666 ,

you can create your own subclass of `NegativeSampler`, where you only have to implement the corruption method

https://github.com/pykeen/pykeen/blob/5a24de7d6c42af36ea1a546c10e68634da817852/src/pykeen/sampling/negative_sampler.py#L94

If you need to do some one-time preprocessing, e.g., to efficiently represent the mentioned entity sets, you can do so in the sampler's  `__init__` method, where you also get access to all training triples.

Notice that more realistic samples can lead to a larger false negative rate, where the corrupted triples are also true. This can be (partially) mitigated by enabling `filtered` negative sample generation, cf. 

https://github.com/pykeen/pykeen/blob/5a24de7d6c42af36ea1a546c10e68634da817852/src/pykeen/sampling/negative_sampler.py#L38

By doing so the generated negative samples will be checked against the training triples to filter out existing true triples.

Thanks @mberr . BTW can we output the generated negative triples? And if I already have a set of negative triples, can I assign the negative triples in the training directly?

> Hi @Hao-666 ,
> 
> you can create your own subclass of `NegativeSampler`, where you only have to implement the corruption method
> 
> https://github.com/pykeen/pykeen/blob/5a24de7d6c42af36ea1a546c10e68634da817852/src/pykeen/sampling/negative_sampler.py#L94
> 
> If you need to do some one-time preprocessing, e.g., to efficiently represent the mentioned entity sets, you can do so in the sampler's `__init__` method, where you also get access to all training triples.
> 
> Notice that more realistic samples can lead to a larger false negative rate, where the corrupted triples are also true. This can be (partially) mitigated by enabling `filtered` negative sample generation, cf.
> 
> https://github.com/pykeen/pykeen/blob/5a24de7d6c42af36ea1a546c10e68634da817852/src/pykeen/sampling/negative_sampler.py#L38
> 
> By doing so the generated negative samples will be checked against the training triples to filter out existing true triples.

I tested this by creating a subclass with the codes of [basic negative sampler](https://github.com/pykeen/pykeen/blob/5a24de7d6c42af36ea1a546c10e68634da817852/src/pykeen/sampling/basic_negative_sampler.py) first, but there are two errors: 
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-52-27124f7fabbb> in <module>
     82         return negative_batch.view(-1, self.num_negs_per_pos, 3)
     83 
---> 84 sampler = Sampler(tf)
     85 
     86 results_transR = pipeline(

TypeError: __init__() takes 1 positional argument but 2 were given
```
and
```
TypeError                                 Traceback (most recent call last)
<ipython-input-51-31848ed0afa5> in <module>
      8     negative_sampler=sampler,
      9     #negative_sampler_kwargs=dict(filtered=True),
---> 10     random_seed=42
     11 )
     12 results_transR.save_to_directory('results_transR')

C:\Anaconda3\envs\BIM2KG_test\lib\site-packages\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, 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)
    969         raise ValueError('Can not specify negative sampler with LCWA')
    970     else:
--> 971         negative_sampler_cls = negative_sampler_resolver.lookup(negative_sampler)
    972         _result_tracker.log_params(
    973             params=dict(cls=negative_sampler_cls.__name__, kwargs=negative_sampler_kwargs),

C:\Anaconda3\envs\BIM2KG_test\lib\site-packages\class_resolver\api.py in lookup(self, query)
    172             lookup_dict_synonyms=self.synonyms,
    173             default=self.default,
--> 174             suffix=self.suffix,
    175         )
    176 

C:\Anaconda3\envs\BIM2KG_test\lib\site-packages\class_resolver\api.py in get_cls(query, base, lookup_dict, lookup_dict_synonyms, default, suffix)
    342         return default
    343     elif not isinstance(query, (str, type)):
--> 344         raise TypeError(f'Invalid {base.__name__} type: {type(query)} - {query}')
    345     elif isinstance(query, str):
    346         key = normalize_string(query, suffix=suffix)

TypeError: Invalid NegativeSampler type: <class '__main__.Sampler'> - <__main__.Sampler object at 0x000002A06B4CF748>
```
Codes:
```
import math
from typing import Collection, Optional
import torch
__all__ = [
    'BasicNegativeSampler',
]

LOOKUP = {'h': 0, 'r': 1, 't': 2}


class Sampler(NegativeSampler):

    def __init__(
        self,
        *,
        corruption_scheme: Optional[Collection[str]] = None,
        **kwargs,
    ) -> None:

        super().__init__(**kwargs)
        self.corruption_scheme = corruption_scheme or ('h', 't')
        # Set the indices
        self._corruption_indices = [LOOKUP[side] for side in self.corruption_scheme]

    def corrupt_batch(self, positive_batch: torch.LongTensor) -> torch.LongTensor:  # noqa: D102
        if self.num_negs_per_pos > 1:
            positive_batch = positive_batch.repeat_interleave(repeats=self.num_negs_per_pos, dim=0)

        num_negs = positive_batch.shape[0]

        split_idx = int(math.ceil(num_negs / len(self._corruption_indices)))

        negative_batch = positive_batch.clone()

        for index, start in zip(self._corruption_indices, range(0, num_negs, split_idx)):
            stop = min(start + split_idx, num_negs)

            index_max = (self.num_relations if index == 1 else self.num_entities) - 1

            negative_batch[start:stop, index] = torch.randint(
                high=index_max,
                size=(stop - start,),
                device=positive_batch.device,
            )

            negative_batch[start:stop, index] += (
                negative_batch[start:stop, index] >= positive_batch[start:stop, index]
            ).long()

        return negative_batch.view(-1, self.num_negs_per_pos, 3)

tf = TriplesFactory.from_path(PATH)        
sampler = Sampler(tf)

results_transR = pipeline(
    training=training,
    testing=testing,
    validation=validation,
    model='transR',
    training_kwargs=dict(num_epochs=2,batch_size=512),
    negative_sampler=sampler,
    random_seed=42
)
```
Are there any problems? Sorry that maybe my problems look stupid... I am new to coding and knowledge graph embeddings etc... Thanks!

Looks like you should have used a keyword argument but used a positional argument

```python

class Sampler(NegativeSampler):

    def __init__(
        self,
        *,  # <---- this star means everything else after must be a keyword only argument
        corruption_scheme: Optional[Collection[str]] = None,
        **kwargs,
    ) -> None:

...

```

but also your usage doesn't really make any sense anyway since there's no reason to pass a triples factory to __init__ based on your code

Thanks! @cthoyt. But if the triple factory was not passed it would say triples_factory is required as the message below.
```
TypeError                                 Traceback (most recent call last)
<ipython-input-60-62f5cd15c3cd> in <module>
     54         return negative_batch.view(-1, self.num_negs_per_pos, 3)
     55 
---> 56 sampler = Sampler()
     57 
     58 results_transR = pipeline(

<ipython-input-60-62f5cd15c3cd> in __init__(self, corruption_scheme, **kwargs)
     22     ) -> None:
     23 
---> 24         super().__init__(**kwargs)
     25         self.corruption_scheme = corruption_scheme or ('h', 't')
     26         # Set the indices

TypeError: __init__() missing 1 required positional argument: 'triples_factory'
```

Now I use the keyword argument as `sampler = Sampler(triples_factory=tf)` and error of` __init__() takes 1 positional argument but 2 were given` disappeared.  But the second error of `Invalid NegativeSampler type: <class '__main__.Sampler'> - <__main__.Sampler object at 0x000002A06B4CF748>` still appears. Looks like something wrong with the defined sampler?

> Thanks! @cthoyt. But if the triple factory was not passed it would say triples_factory is required as the message below.
> 
> ```
> TypeError                                 Traceback (most recent call last)
> <ipython-input-60-62f5cd15c3cd> in <module>
>      54         return negative_batch.view(-1, self.num_negs_per_pos, 3)
>      55 
> ---> 56 sampler = Sampler()
>      57 
>      58 results_transR = pipeline(
> 
> <ipython-input-60-62f5cd15c3cd> in __init__(self, corruption_scheme, **kwargs)
>      22     ) -> None:
>      23 
> ---> 24         super().__init__(**kwargs)
>      25         self.corruption_scheme = corruption_scheme or ('h', 't')
>      26         # Set the indices
> 
> TypeError: __init__() missing 1 required positional argument: 'triples_factory'
> ```
> 
> Now I use the keyword argument as `sampler = Sampler(triples_factory=tf)` and error of` __init__() takes 1 positional argument but 2 were given` disappeared. But the second error of `Invalid NegativeSampler type: <class '__main__.Sampler'> - <__main__.Sampler object at 0x000002A06B4CF748>` still appears. Looks like something wrong with the defined sampler?

Sorry for disturbance. Seems that the self-defined sampler class should not be instantiated. I deleted `sampler = Sampler(triples_factory=tf)` and changed `negative_sampler=sampler` to `negative_sampler = Sampler` and then it worked.