Перенос функции() с обновлениями в Pytorch (ошибка времени выполнения с отрицательной выборкой: Ожидаемый скрытый размер)

#python #pytorch #theano

Вопрос:

Я пытаюсь перенести код с Theano на PyTorch, честно говоря, у меня очень ограниченное понимание того, как на самом деле работают обе платформы, поэтому, пожалуйста, потерпите меня! Я был бы очень признателен за любую помощь в углублении моего понимания.

https://github.com/hidasib/GRU4Rec/blob/master/gru4rec.py#L614

Это код, который я пытаюсь перенести. Часть кода уже портирована на PyTorch, ее можно найти здесь: https://github.com/hungthanhpham94/GRU4REC-pytorch/tree/master/lib

В реализации PyTorch отсутствует ряд функций, которые существуют в исходном коде. Я уже внес кучу изменений, но попал в блок в отношении отрицательной выборки.

В исходном коде определен размер пакета (по умолчанию = 32), и дополнительные отрицательные образцы (по умолчанию n_sample = 2048 на пакет afaik) хранятся в памяти GPU.

В Теано:

                 P = theano.shared(pop.astype(theano.config.floatX), name='P')
                self.ST = theano.shared(np.zeros((generate_length, self.n_sample), dtype='int64'))
                self.STI = theano.shared(np.asarray(0, dtype='int64'))
                X = mrng.uniform((generate_length*self.n_sample,))
                updates_st = OrderedDict()
                updates_st[self.ST] = gpu_searchsorted(P, X, dtype_int64=True).reshape((generate_length, self.n_sample))
                updates_st[self.STI] = np.asarray(0, dtype='int64')
                generate_samples = theano.function([], updates=updates_st)
                generate_samples()
                sample_pointer = 0
 

Вышеупомянутый блок создает массив идентификаторов, хранящихся в памяти графического процессора. Который я реализовал в классе DataLoader как:

 def generate_negatives(self):
    P = torch.FloatTensor(self.pop)
    ST = torch.LongTensor(np.zeros((self.generate_length, self.n_sample), dtype='int64'))
    STI = torch.LongTensor(np.asarray(0, dtype='int64'))
    X = torch.rand((self.generate_length * self.n_sample,))
    return torch.searchsorted(P, X).reshape((self.generate_length, self.n_sample))
 

В то же время здесь используется отрицательный генератор:

         while not finished:
               ........
                else:
                    y = out_idx
                    if self.n_sample:
                        if sample_pointer == generate_length:
                            generate_samples()
                            sample_pointer = 0
                        sample_pointer  = 1
                reset = (start i 1 == end-1)
                cost = train_function(in_idx, y, len(iters), reset.reshape(len(reset), 1))
 

where the train_function is defined as:

train_function = function(inputs=[X, Y, M, R], outputs=cost, updates=updates, allow_input_downcast=True, on_unused_input=’ignore’)

and an example loss function is as follows:

 def bpr(self, yhat, M):
    return T.cast(T.sum(-T.log(T.nnet.sigmoid(gpu_diag(yhat, keepdims=True)-yhat))), theano.config.floatX)
 

In PyTorch, I’ve attempted to implement the negative generator in the same way:

 while not finished:
            minlen = (end - start).min()
            # Item indices(for embedding) for clicks where the first sessions start
            idx_target = df.item_idx.values[start]
            for i in range(minlen - 1):
                # Build inputs amp; targets
                idx_input = idx_target
                idx_target = df.item_idx.values[start   i   1]
                if self.n_sample:
                    if sample_pointer == self.generate_length:
                        neg_samples = self.generate_negatives()
                        sample_pointer = 0
                    sample = neg_samples[sample_pointer]
                    sample_pointer  = 1
                    # idx_target = np.hstack([idx_target, sample]) # like cpu version (doesn't work due to hidden size)
                input = torch.LongTensor(idx_input)
                target = torch.LongTensor(idx_target)
                yield input, target, mask
 

The above generator is used in train_epoch method in Trainer class:

 if self.n_sample:
    dataloader = DataLoader(self.train_data, self.batch_size, self.n_sample, self.generate_length)
else:
    dataloader = DataLoader(self.train_data, self.batch_size)
for ii, (input, target, mask) in enumerate(dataloader):
    input = input.to(self.device)
    target = target.to(self.device)
    self.optim.zero_grad()
    hidden = reset_hidden(hidden, mask).detach()
    logit, hidden = self.model(input, hidden)
    # output sampling
    logit_sampled = logit[:, target.view(-1)]
    loss = self.loss_func(logit_sampled)
    losses.append(loss.item())
    loss.backward()
    self.optim.step()
 

The same loss function is defined as:

 class BPRLoss(nn.Module):
    def __init__(self):
        super(BPRLoss, self).__init__()
    def forward(self, logit):
        diff = logit.diag().view(-1, 1).expand_as(logit) - logit
        loss = -torch.mean(F.logsigmoid(diff))
        return loss
 

from my understanding, in the Theano version, in_idx and y (input item idxs, target item idxs respectively), are of the same shape (batch_size), (batch_size). A matrix is produced where the diag (note: keepdims=True or expand_as() in both loss functions) comprises the scores for target items while remaining elements are treated as intra-mini-batch negative samples. Since this is consistent across implementations, how then is the loss calculated on the additional 2048 negative samples in the Theano version?

В реализации процессора Theano (которая устарела):

y = np.hstack([out_idx, sample])

Реализация графического процессора:

     def model(self, X, H, M, R=None, Y=None, drop_p_hidden=0.0, drop_p_embed=0.0, predict=False):
        sparams, full_params, sidxs = [], [], []
        if (hasattr(self, 'ST')) and (Y is not None) and (not predict) and (self.n_sample > 0):
            A = self.ST[self.STI]
            Y = T.concatenate([Y, A], axis=0)
 

Если бы размер нашей партии был 32, а n_sample был 2048, используя приведенную выше логику (объединение выборки с целью), мы бы получили неизменный ввод 32 размера, в то время как наша цель имела бы размер 32 2048 = 2080. В результате возникает следующая ошибка:

Ошибка времени выполнения: Ожидаемый скрытый размер (3, 2080, 100), получен [3, 32, 100].

Как можно устранить это несоответствие размеров?

Я попытался изменить ввод (скопировать входные идентификаторы)/ цель (с объединенными отрицательными образцами), а затем выполнить цикл (но это не распараллеливается и, следовательно, чрезвычайно медленно).

Я также попытался изменить форму на новый размер пакета (n_samples batch_size) и с помощью idx_input = np.repeat(idx_input, (self.n_sample self.batch_size) // self.batch_size) при вызове init_hidden(), однако это приводит к другим ошибкам во время выполнения, OOM и RuntimeError: The expanded size of the tensor (9248) must match the existing size (544) at non-singleton dimension 0. Target sizes: [9248, 544]. Tensor sizes: [544, 1]

С уважением

Комментарии:

1. Для ясности, вы пытаетесь подогнать мини-партию под размер, который меньше заданного размера партии? Я думаю, что реализация PyTorch может пропустить много образцов позже в процессе обучения.