Repository: keras-team/keras

Issue:
Implement `binomial` and `beta` distribution functions in `keras.random`
Following up on the issue https://github.com/keras-team/keras/issues/18918

- Implement `binomial` and `beta` distribution functions in all backends currently supported by Keras namely TensorFlow, Jax, PyTorch and Numpy.
- Add unit tests for each of these functions

Importantly,
As tensorflow doesn't offer a built-in method for beta function so I've implemented a workaround using a statistical formula to use gamma distributed random variables to derive beta distributed random variable.
Specifically, $U(a, b) = X(a) / (X(a) + Y(b))$ where $U(a,b)$ is the beta distributed random variable using parameters $a$ and $b$ and $X(a)$ and $Y(b)$ are gamma-distributed random variables using parameter $a$ and $b$ respectively.


Discussion:
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this [failed invocation](https://github.com/keras-team/keras/pull/18920/checks?check_run_id=19484449524) of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.
## [Codecov](https://app.codecov.io/gh/keras-team/keras/pull/18920?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`0044ca6`)](https://app.codecov.io/gh/keras-team/keras/commit/0044ca6fd30612bc39dce19f6b7070891d35e594?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team) 79.47% compared to head [(`271b0ef`)](https://app.codecov.io/gh/keras-team/keras/pull/18920?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team) 79.52%.
> Report is 1 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master   #18920      +/-   ##
==========================================
+ Coverage   79.47%   79.52%   +0.04%     
==========================================
  Files         336      336              
  Lines       34863    34936      +73     
  Branches     6853     6855       +2     
==========================================
+ Hits        27709    27782      +73     
  Misses       5575     5575              
  Partials     1579     1579              
```

| [Flag](https://app.codecov.io/gh/keras-team/keras/pull/18920/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team) | Coverage Δ | |
|---|---|---|
| [keras](https://app.codecov.io/gh/keras-team/keras/pull/18920/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team) | `79.37% <100.00%> (+0.04%)` | :arrow_up: |
| [keras-jax](https://app.codecov.io/gh/keras-team/keras/pull/18920/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team) | `61.15% <27.39%> (-0.08%)` | :arrow_down: |
| [keras-numpy](https://app.codecov.io/gh/keras-team/keras/pull/18920/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team) | `55.90% <27.39%> (-0.07%)` | :arrow_down: |
| [keras-tensorflow](https://app.codecov.io/gh/keras-team/keras/pull/18920/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team) | `63.14% <34.24%> (-0.07%)` | :arrow_down: |
| [keras-torch](https://app.codecov.io/gh/keras-team/keras/pull/18920/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team) | `63.78% <43.83%> (-0.05%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/keras-team/keras/pull/18920?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=keras-team).

Again, Thank you so much Mr. Francois Chollet for pointing all my mistakes out and for all the kind suggestions which are helping me learn so much. I've taken careful notice of them and will be updating everything accordingly. Also, I've taken note of the errors, and will resolve those as well.
The most recent tests ran before I pushed my updated `random_test.py` file. For any new test run now, everything should work fine.
Oh my god. This is a dream come true for me. Always wanted to contribute to this incredible project but never thought i was capable enough for that. Thank you so much, because without you, your awesome book and this keras library I probably would've given up on deep learning right at the start. Even over these two days, I've learned so many new things thanks to your kind suggestions. It's been a true honor! ❤️🫡
Thank you for taking the time to add this feature! It will be valuable to many Keras users.

Resolving patch:
diff --git a/keras/backend/torch/random.py b/keras/backend/torch/random.py
--- a/keras/backend/torch/random.py
+++ b/keras/backend/torch/random.py
@@ -208,8 +208,8 @@ def gamma(shape, alpha, dtype=None, seed=None):
 def binomial(shape, counts, probabilities, dtype=None, seed=None):
     dtype = dtype or floatx()
     dtype = to_torch_dtype(dtype)
-    counts = torch.ones(shape) * convert_to_tensor(counts)
-    probabilities = torch.ones(shape) * convert_to_tensor(probabilities)
+    counts = torch.broadcast_to(convert_to_tensor(counts), shape)
+    probabilities = torch.broadcast_to(convert_to_tensor(probabilities), shape)
     prev_rng_state = torch.random.get_rng_state()
     first_seed, second_seed = draw_seed(seed)
     torch.manual_seed(first_seed + second_seed)
@@ -224,8 +224,8 @@ def binomial(shape, counts, probabilities, dtype=None, seed=None):
 def beta(shape, alpha, beta, dtype=None, seed=None):
     dtype = dtype or floatx()
     dtype = to_torch_dtype(dtype)
-    alpha = torch.ones(shape) * convert_to_tensor(alpha)
-    beta = torch.ones(shape) * convert_to_tensor(beta)
+    alpha = torch.broadcast_to(convert_to_tensor(alpha), shape)
+    beta = torch.broadcast_to(convert_to_tensor(beta), shape)
     prev_rng_state = torch.random.get_rng_state()
     first_seed, second_seed = draw_seed(seed)
     torch.manual_seed(first_seed + second_seed)


Test patch:
diff --git a/keras/random/random_test.py b/keras/random/random_test.py
--- a/keras/random/random_test.py
+++ b/keras/random/random_test.py
@@ -299,14 +299,15 @@ def test_binomial(self, seed, shape, counts, probabilities, dtype):
         # by the user for that event.
         # Hence, we do an element wise comparison between `counts` array
         # and the (generated) `values` array.
-        assert np.greater_equal(np.array(counts), np.array(values)).all()
+        values_np = ops.convert_to_numpy(values)
+        assert np.greater_equal(np.array(counts), values_np).all()
 
         # Following test computes the probabilities of each event
         # by dividing number of times an event occurs (which is the generated
         # value) by the corresponding value in the (total) counts array.
         # and then makes sure that the computed probabilities approximate
         # the input probabilities
-        generated_probabilities = np.array(values) / np.array(counts)
+        generated_probabilities = values_np / np.array(counts)
         probabilities = np.ones(shape) * np.array(probabilities)
         self.assertAllClose(
             probabilities, generated_probabilities, rtol=0.005, atol=0.005
@@ -342,8 +343,9 @@ def test_beta(self, seed, shape, alpha, beta, dtype):
         )
         self.assertEqual(ops.shape(values), shape)
         self.assertEqual(backend.standardize_dtype(values.dtype), dtype)
-        self.assertGreaterEqual(np.min(ops.convert_to_numpy(values)), b=0.0)
-        self.assertLessEqual(np.max(ops.convert_to_numpy(values)), b=1.0)
+        values_np = ops.convert_to_numpy(values)
+        self.assertGreaterEqual(np.min(values_np), b=0.0)
+        self.assertLessEqual(np.max(values_np), b=1.0)
 
         _alpha_is_an_array = False
         if isinstance(alpha, list):
@@ -357,12 +359,12 @@ def test_beta(self, seed, shape, alpha, beta, dtype):
         expected_mean = alpha / (alpha + beta)
 
         if _alpha_is_an_array:
-            actual_mean = np.mean(np.array(values), axis=0)
+            actual_mean = np.mean(values_np, axis=0)
             self.assertAllClose(
                 expected_mean.flatten(), actual_mean, atol=0.005, rtol=0.005
             )
         else:
-            actual_mean = np.mean(np.array(values).flatten())
+            actual_mean = np.mean(values_np.flatten())
             self.assertAlmostEqual(expected_mean, actual_mean, decimal=2)
 
         # Variance check:
@@ -372,7 +374,7 @@ def test_beta(self, seed, shape, alpha, beta, dtype):
             np.square(alpha + beta) * (alpha + beta + 1)
         )
         if _alpha_is_an_array:
-            actual_variance = np.var(np.array(values), axis=0)
+            actual_variance = np.var(values_np, axis=0)
             self.assertAllClose(
                 expected_variance.flatten(),
                 actual_variance,
@@ -380,7 +382,7 @@ def test_beta(self, seed, shape, alpha, beta, dtype):
                 rtol=0.005,
             )
         else:
-            actual_variance = np.var(np.array(values).flatten())
+            actual_variance = np.var(values_np.flatten())
             self.assertAlmostEqual(
                 expected_variance, actual_variance, decimal=2
             )