Repository: pytorch/pytorch

Issue:
Rename distributions.Multinomial -> .Categorical
`Multinomial` commonly refers to a distribution over counts, and is a multivariate generalization of the `Binomial` distribution. However PyTorch's `Multinomial` implements what is more commonly referred to as a `Discrete` or `Categorical` distribution, a distribution over vectors of indices. I suggest renaming `torch.distributions.Multinomial` to `.Categorical` or `.Discrete` so as to make way for a true `.Multinomial` implementation (e.g. [this one](https://github.com/uber/pyro/blob/dev/pyro/distributions/multinomial.py) from Pyro).

## Why?

This PR follows the naming convention used by:
- Tensorflow: [Categorical](https://www.tensorflow.org/api_docs/python/tf/distributions/Categorical) vs [Multinomial](https://www.tensorflow.org/api_docs/python/tf/distributions/Multinomial)
- PyMC3: [Categorical](http://docs.pymc.io/api/distributions/discrete.html#pymc3.distributions.discrete.Categorical) vs [Multinomial](http://docs.pymc.io/api/distributions/multivariate.html#pymc3.distributions.multivariate.Multinomial)
- Scipy: [multinomial](https://docs.scipy.org/doc/scipy-0.19.1/reference/generated/scipy.stats.multinomial.html) operates on counts rather than values

## How these distributions relate to each other
| | Binary | Multi-class |
| -: | - | - |
| values | `Bernoulli` | `Categorical` aka `Discrete` |
| counts | `Binomial` | `Multinomial` |
| conjugate prior | `Beta` | `Dirichlet` |


Discussion:
@colesbury How should we balance backwards-compatibility against having a standard consistent interface? Is it OK to rename this?
CC @neerajprad @jwvdm @alicanb

Resolving patch:
diff --git a/torch/autograd/_functions/stochastic.py b/torch/autograd/_functions/stochastic.py
--- a/torch/autograd/_functions/stochastic.py
+++ b/torch/autograd/_functions/stochastic.py
@@ -2,7 +2,7 @@
 from ..function import Function
 
 
-class Multinomial(Function):
+class Categorical(Function):
     @staticmethod
     def forward(ctx, probs, num_samples, with_replacement):
         samples = probs.multinomial(num_samples, with_replacement)
diff --git a/torch/autograd/variable.py b/torch/autograd/variable.py
--- a/torch/autograd/variable.py
+++ b/torch/autograd/variable.py
@@ -204,7 +204,8 @@ def trim(str):
             Use:
 
             probs = policy_network(state)
-            m = torch.distributions.Multinomial(probs)
+            # NOTE: categorical is equivalent to what used to be called multinomial
+            m = torch.distributions.Categorical(probs)
             action = m.sample()
             next_state, reward = env.step(action)
             loss = -m.log_prob(action) * reward
@@ -373,7 +374,7 @@ def expand_as(self, tensor):
         return self.expand(tensor.size())
 
     def multinomial(self, num_samples=1, replacement=False):
-        return Multinomial.apply(self, num_samples, replacement)
+        return Categorical.apply(self, num_samples, replacement)
 
     def bernoulli(self):
         return Bernoulli.apply(self)
diff --git a/torch/distributions.py b/torch/distributions.py
--- a/torch/distributions.py
+++ b/torch/distributions.py
@@ -18,11 +18,12 @@
 In practice we would sample an action from the output of a network, apply this
 action in an environment, and then use ``log_prob`` to construct an equivalent
 loss function. Note that we use a negative because optimisers use gradient
-descent, whilst the rule above assumes gradient ascent. With a multinomial
+descent, whilst the rule above assumes gradient ascent. With a categorical
 policy, the code for implementing REINFORCE would be as follows::
 
     probs = policy_network(state)
-    m = Multinomial(probs)
+    # NOTE: this is equivalent to what used to be called multinomial
+    m = Categorical(probs)
     action = m.sample()
     next_state, reward = env.step(action)
     loss = -m.log_prob(action) * reward
@@ -33,7 +34,7 @@
 import torch
 
 
-__all__ = ['Distribution', 'Bernoulli', 'Multinomial', 'Normal']
+__all__ = ['Distribution', 'Bernoulli', 'Categorical', 'Normal']
 
 
 class Distribution(object):
@@ -101,9 +102,12 @@ def log_prob(self, value):
         return log_pmf.gather(0, value.unsqueeze(0).long()).squeeze(0)
 
 
-class Multinomial(Distribution):
+class Categorical(Distribution):
     r"""
-    Creates a multinomial distribution parameterized by `probs`.
+    Creates a categorical distribution parameterized by `probs`.
+    
+    .. note::
+        It is equivalent to the distribution that ``multinomial()`` samples from.
 
     Samples are integers from `0 ... K-1` where `K` is probs.size(-1).
 
@@ -116,7 +120,7 @@ class Multinomial(Distribution):
 
     Example::
 
-        >>> m = Multinomial(torch.Tensor([ 0.25, 0.25, 0.25, 0.25 ]))
+        >>> m = Categorical(torch.Tensor([ 0.25, 0.25, 0.25, 0.25 ]))
         >>> m.sample()  # equal probability of 0, 1, 2, 3
          3
         [torch.LongTensor of size 1]


Test patch:
diff --git a/test/test_distributions.py b/test/test_distributions.py
--- a/test/test_distributions.py
+++ b/test/test_distributions.py
@@ -2,7 +2,7 @@
 import math
 import torch
 from torch.autograd import Variable, gradcheck
-from torch.distributions import Bernoulli, Multinomial, Normal
+from torch.distributions import Bernoulli, Categorical, Normal
 
 
 class TestDistributions(TestCase):
@@ -47,22 +47,22 @@ def test_bernoulli_3d(self):
     def test_multinomial_1d(self):
         p = Variable(torch.Tensor([0.1, 0.2, 0.3]), requires_grad=True)
         # TODO: this should return a 0-dim tensor once we have Scalar support
-        self.assertEqual(Multinomial(p).sample().size(), (1,))
-        self.assertEqual(Multinomial(p).sample_n(1).size(), (1, 1))
-        self._gradcheck_log_prob(Multinomial, (p,))
+        self.assertEqual(Categorical(p).sample().size(), (1,))
+        self.assertEqual(Categorical(p).sample_n(1).size(), (1, 1))
+        self._gradcheck_log_prob(Categorical, (p,))
 
     def test_multinomial_2d(self):
         probabilities = [[0.1, 0.2, 0.3], [0.5, 0.3, 0.2]]
         p = Variable(torch.Tensor(probabilities), requires_grad=True)
-        self.assertEqual(Multinomial(p).sample().size(), (2,))
-        self.assertEqual(Multinomial(p).sample_n(6).size(), (6, 2))
-        self._gradcheck_log_prob(Multinomial, (p,))
+        self.assertEqual(Categorical(p).sample().size(), (2,))
+        self.assertEqual(Categorical(p).sample_n(6).size(), (6, 2))
+        self._gradcheck_log_prob(Categorical, (p,))
 
         def ref_log_prob(idx, val, log_prob):
             sample_prob = p.data[idx][val] / p.data[idx].sum()
             self.assertEqual(log_prob, math.log(sample_prob))
 
-        self._check_log_prob(Multinomial(p), ref_log_prob)
+        self._check_log_prob(Categorical(p), ref_log_prob)
 
     def test_normal(self):
         mean = Variable(torch.randn(5, 5), requires_grad=True)