Implementing word dropout in pytorch
I want to add word dropout to my network so that I can have sufficient training examples for training the embedding of the "unk" token. As far as I'm aware, this is standard practice. Let's assume the index of the unk token is 0, and the index for padding is 1 (we can switch them if that's more convenient).

This is a simple CNN network which implements word dropout the way I would have expected it to work:

class Classifier(nn.Module):
    def __init__(self, params):
        super(Classifier, self).__init__()
        self.params = params
        self.word_dropout = nn.Dropout(params["word_dropout"])
        self.pad = torch.nn.ConstantPad1d(max(params["window_sizes"])-1, 1)
        self.embedding = nn.Embedding(params["vocab_size"], params["word_dim"], padding_idx=1)
        self.convs = nn.ModuleList([nn.Conv1d(1, params["feature_num"], params["word_dim"] * window_size, stride=params["word_dim"], bias=False) for window_size in params["window_sizes"]])
        self.dropout = nn.Dropout(params["dropout"])
        self.fc = nn.Linear(params["feature_num"] * len(params["window_sizes"]), params["num_classes"])

    def forward(self, x, l):
        x = self.word_dropout(x)
        x = self.pad(x)
        embedded_x = self.embedding(x)
        embedded_x = embedded_x.view(-1, 1, x.size()[1] * self.params["word_dim"]) # [batch_size, 1, seq_len * word_dim]
        features = [F.relu(conv(embedded_x)) for conv in self.convs]
        pooled = [F.max_pool1d(feat, feat.size()[2]).view(-1, params["feature_num"]) for feat in features]
        pooled = torch.cat(pooled, 1)
        pooled = self.dropout(pooled)
        logit = self.fc(pooled)
        return logit


Don't mind the padding - pytorch doesn't have an easy way of using non zero padding in CNNs, much less trainable non-zero padding, so I'm doing it manually. Dropout also doesn't allow me to use non zero dropout, and I want to separate the padding token from the unk token. I'm keeping it in my example because it's the reason for this question's existence.

This doesn't work because dropout wants Float Tensors so that it can scale them properly, while my input is Long Tensors that don't need to be scaled.

Is there an easy way of doing this in pytorch? I essentially want to use LongTensor-friendly dropout (bonus: better if it will let me specify a dropout constant that isn't 0, so that I could use zero padding).

Actually I would do it outside of your model, before converting your input into a LongTensor.

This would look like this:

import random

def add_unk(input_token_id, p):
    #random.random() gives you a value between 0 and 1
    #to avoid switching your padding to 0 we add 'input_token_id &gt; 1'
    if random.random() &lt; p and input_token_id &gt; 1:
        return 0
    else:
        return input_token_id

#than you have your input token_id
#for this example I take just a random number, lets say 127
input_token_id = 127

#let p be your probability for UNK
p = 0.01

your_input_tensor = torch.LongTensor([add_unk(input_token_id, p)])


Edit:

So there are two options which come to my mind which are actually GPU-friendly. In general both solutions should be much more efficient. 

Option one - Doing computation directly in forward():

If you're not using torch.utils and don't have plans using it later this is probably the way to go.

Instead of doing the computation before we just do it in the forward() method of main PyTorch class. However I see no (simple) way doing this in torch 0.3.1., so you would need to upgrade to version 0.4.0:

So imagine x is your input vector:

&gt;&gt;&gt; x = torch.tensor(range(10))
&gt;&gt;&gt; x
tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9])


probs is a vector containing uniform probabilities for dropout so we can check later agains our probability for dropout:

&gt;&gt;&gt; probs = torch.empty(10).uniform_(0, 1)
&gt;&gt;&gt; probs
tensor([ 0.9793,  0.1742,  0.0904,  0.8735,  0.4774,  0.2329,  0.0074,
         0.5398,  0.4681,  0.5314])


Now we apply the dropout probabilities probs on our input x:

&gt;&gt;&gt; torch.where(probs &gt; 0.2, x, torch.zeros(10, dtype=torch.int64))
tensor([ 0,  0,  0,  3,  4,  5,  0,  7,  8,  9])


Note: To see some effect I chose a dropout probability of 0.2 here. I reality you probably want it to be smaller.

You can pick for this any token / id you like, here is an example with 42 as unknown token id:

&gt;&gt;&gt; unk_token = 42
&gt;&gt;&gt; torch.where(probs &gt; 0.2, x, torch.empty(10, dtype=torch.int64).fill_(unk_token))
tensor([  0,  42,  42,   3,   4,   5,  42,   7,   8,   9])


torch.where comes with PyTorch 0.4.0:
https://pytorch.org/docs/master/torch.html#torch.where

I don't know about the shapes of your network, but your forward() should look something like this then (when using mini-batching you need to flatten the input before applying dropout):

def forward_train(self, x, l):
    # probabilities
    probs = torch.empty(x.size(0)).uniform_(0, 1)
    # applying word dropout
    x = torch.where(probs &gt; 0.02, x, torch.zeros(x.size(0), dtype=torch.int64))

    # continue like before ...
    x = self.pad(x)
    embedded_x = self.embedding(x)
    embedded_x = embedded_x.view(-1, 1, x.size()[1] * self.params["word_dim"]) # [batch_size, 1, seq_len * word_dim]
    features = [F.relu(conv(embedded_x)) for conv in self.convs]
    pooled = [F.max_pool1d(feat, feat.size()[2]).view(-1, params["feature_num"]) for feat in features]
    pooled = torch.cat(pooled, 1)
    pooled = self.dropout(pooled)
    logit = self.fc(pooled)
    return logit


Note: I named the function forward_train() so you should use another forward() without dropout for evaluation / predicting. But you could also use some if conditions with train().

Option two: using torch.utils.data.Dataset:

If you're using Dataset provided by torch.utils it is very easy to do this kind of pre-processing efficiently. Dataset uses strong multi-processing acceleration by default so the the code sample above just has to be executed in the __getitem__ method of your Dataset class. 

This could look like this:

def __getitem__(self, index):
    'Generates one sample of data'
    # Select sample
    ID = self.input_tokens[index]

    # Load data and get label
    # using add ink_unk function from code above
    X = torch.LongTensor(add_unk(ID, p=0.01))
    y = self.targets[index]

    return X, y


This is a bit out of context and doesn't look very elegant but I think you get the idea. According to this blog post of Shervine Amidi at Stanford it should be no problem to do more complex pre-processing steps in this function:


  Since our code [Dataset is meant] is designed to be multicore-friendly, note that you
  can do more complex operations instead (e.g. computations from source
  files) without worrying that data generation becomes a bottleneck in
  the training process.


The linked blog post - "A detailed example of how to generate your data in parallel with PyTorch" - provides also a good guide for implementing the data generation with Dataset and DataLoader.

I guess you'll prefer option one - only two lines and it should be very efficient. :)

Good luck!
