Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions core/src/main/scala/dimwit/stats/IndependentDistributions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,37 @@ class Exponential[T <: Tuple: Labels, V: IsFloating](val rate: Tensor[T, V]) ext
)
stndExp / rate

/** Gamma distribution with `concentration` (the shape parameter, often written alpha or k) and `rate` (often written beta).
*
* The parameter is called concentration rather than shape, as in Dirichlet, to avoid a clash with a tensor's `shape`.
* Mean is concentration / rate, variance concentration / rate^2. Gamma(1, rate) is Exponential(rate).
*/
class Gamma[T <: Tuple: Labels, V: IsFloating](val concentration: Tensor[T, V], val rate: Tensor[T, V]) extends IndependentDistribution[T, V]:

private lazy val scale = Tensor.like(rate).fill(1f) / rate

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we think about the trade offs of having this as a private field? Memory vs. Performance

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. But I think it is a good trade-off here. And if it turns out to be the wrong trade-off, we can easily change it afterwards without breaking the interface.


override def elementWiseLogProb(x: Tensor[T, V]): Tensor[T, LogProb] =
LogProb.fromPy(jstats.gamma.logpdf(x.jaxValue, a = concentration.jaxValue, scale = scale.jaxValue))

override def sample(k: Random.Key): Tensor[T, V] =
// jax.random.gamma samples with rate 1, so divide by the rate
val standardGamma = liftPyTensor(concentration.shape, VType[V])(
Jax.jrandom.gamma(k.jaxKey, a = concentration.jaxValue, shape = concentration.shape.dimensions.toPythonProxy, dtype = VType[V].dtype.jaxType)
)
standardGamma / rate

object Gamma:

/** Create a Gamma distribution from concentration and rate tensors */
def apply[T <: Tuple: Labels, V: IsFloating](concentration: Tensor[T, V], rate: Tensor[T, V]): Gamma[T, V] =
require(concentration.shape.dimensions == rate.shape.dimensions, "concentration and rate must have the same dimensions")
new Gamma(concentration, rate)

/** Create a Gamma distribution from concentration and scale tensors, where scale = 1 / rate */
def fromScale[T <: Tuple: Labels, V: IsFloating](concentration: Tensor[T, V], scale: Tensor[T, V]): Gamma[T, V] =
require(concentration.shape.dimensions == scale.shape.dimensions, "concentration and scale must have the same dimensions")
new Gamma(concentration, Tensor.like(scale).fill(1f) / scale)

class Poisson[T <: Tuple: Labels, VSample: IsInteger, VRate: IsFloating](val rate: Tensor[T, VRate]) extends IndependentDistribution[T, VSample]:

override def elementWiseLogProb(x: Tensor[T, VSample]): Tensor[T, LogProb] =
Expand Down
58 changes: 58 additions & 0 deletions core/src/test/scala/dimwit/stats/DistributionSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,63 @@ class DistributionSuite extends DimwitTest:
val expectedMeans = Tensor(Shape(Axis[A] -> 2)).fromArray(Array(2.0f, 0.5f))
sampleMeans should approxEqual(expectedMeans, 0.1f)

describe("Gamma"):
it("logProbs matches JAX"):
val concentration = Tensor(Shape(Axis[A] -> 3)).fromArray(Array(0.5f, 2.0f, 5.0f))
val rate = Tensor(Shape(Axis[A] -> 3)).fromArray(Array(1.0f, 0.5f, 4.0f))
val x = Tensor(Shape(Axis[A] -> 3)).fromArray(Array(0.3f, 2.5f, 1.2f))

val dist = Gamma(concentration, rate)
val scalaLogProbs = dist.elementWiseLogProb(x)
val scale = Tensor(Shape(Axis[A] -> 3)).fromArray(Array(1.0f, 2.0f, 0.25f))
val jaxLogProbs = liftPyTensor1(Axis[A], VType[Float32])(
jstats.gamma.logpdf(x.jaxValue, a = concentration.jaxValue, scale = scale.jaxValue)
)
scalaLogProbs.asFloat should approxEqual(jaxLogProbs)

it("fromScale is the same as the rate parameterisation"):
val concentration = Tensor(Shape(Axis[A] -> 2)).fromArray(Array(2.0f, 3.0f))
val scale = Tensor(Shape(Axis[A] -> 2)).fromArray(Array(2.0f, 0.5f))
val rate = Tensor(Shape(Axis[A] -> 2)).fromArray(Array(0.5f, 2.0f))
val x = Tensor(Shape(Axis[A] -> 2)).fromArray(Array(1.0f, 1.5f))

Gamma.fromScale(concentration, scale).elementWiseLogProb(x).asFloat should approxEqual(Gamma(concentration, rate).elementWiseLogProb(x).asFloat)

it("Gamma(1, rate) is Exponential(rate)"):
val rate = Tensor(Shape(Axis[A] -> 2)).fromArray(Array(0.5f, 2.0f))
val x = Tensor(Shape(Axis[A] -> 2)).fromArray(Array(0.7f, 1.3f))
val gamma = Gamma(Tensor.like(rate).fill(1.0f), rate)

gamma.elementWiseLogProb(x).asFloat should approxEqual(Exponential(rate).elementWiseLogProb(x).asFloat)

it("logProb is -inf for x < 0"):
val concentration = Tensor(Shape(Axis[A] -> 2)).fromArray(Array(2.0f, 3.0f))
val rate = Tensor(Shape(Axis[A] -> 2)).fromArray(Array(1.0f, 2.0f))
val x = Tensor(Shape(Axis[A] -> 2)).fromArray(Array(-1.0f, -0.1f))

val dist = Gamma(concentration, rate)
val logProbs = dist.elementWiseLogProb(x)
logProbs.asFloat.toArray.foreach(v => v should be(Float.NegativeInfinity))

it("sample means approximates concentration / rate"):
val gamma = Gamma(
Tensor(Shape(Axis[A] -> 2)).fromArray(Array(2.0f, 9.0f)),
Tensor(Shape(Axis[A] -> 2)).fromArray(Array(4.0f, 3.0f))
)
val key = Random.Key(42)
val samples = key.splitvmap(Axis[Samples] -> 10000)(k => gamma.sample(k))
val sampleMeans = samples.mean(Axis[Samples])
val expectedMeans = gamma.concentration / gamma.rate
sampleMeans should approxEqual(expectedMeans, 0.1f)

it("works with scalar parameters"):
val gamma = Gamma(Tensor0(2.0f), Tensor0(4.0f))
val key = Random.Key(7)
val samples = key.splitvmap(Axis[Samples] -> 10000)(k => gamma.sample(k))
samples.mean.item shouldBe 0.5f +- 0.05f
val jaxLogProb = liftPyTensor0(VType[Float32])(jstats.gamma.logpdf(1.0f, a = 2.0f, scale = 0.25f))
gamma.logProb(Tensor0(1.0f)).asFloat should approxEqual(jaxLogProb)

describe("Poisson"):
it("logProbs matches JAX"):
val rate = Tensor(Shape(Axis[A] -> 3)).fromArray(Array(1.0f, 3.0f, 10.0f))
Expand Down Expand Up @@ -618,6 +675,7 @@ class DistributionSuite extends DimwitTest:
checkTypes("StudentT", StudentT(Tensor0(3.0f).asType(VType[Float16]), loc, scale), x)
checkTypes("Beta", Beta(f16(2.0f, 3.0f), f16(2.0f, 1.0f)), f16(0.3f, 0.6f))
checkTypes("Exponential", Exponential(f16(1.0f, 2.0f)), x)
checkTypes("Gamma", Gamma(f16(2.0f, 3.0f), f16(1.0f, 2.0f)), x)
checkTypes("Dirichlet", Dirichlet(f16(1.0f, 2.0f)), f16(0.3f, 0.7f))

it("TruncatedNormal samples Float16"):
Expand Down
Loading