# Install the latest JAXlib version.
!pip install --upgrade -q pip jax jaxlib
# Install Flax at head:
!pip install --upgrade -q git+https://github.com/google/flax.git

import jax
from typing import Any, Callable, Sequence, Optional
from jax import lax, random, numpy as jnp
import flax
from flax.core import freeze, unfreeze
from flax import linen as nn

from jax.config import config
config.enable_omnistaging() # Linen requires enabling omnistaging

"""
Explanation: Copyright 2021 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Example of taking gradients with respect to flax variables inside a flax module
Setting up our environment
Here we provide the code needed to set up the environment for our notebook.
End of explanation
"""


class ToyModule(nn.Module):
  """Toy flax module with one dense layer and one flax variable."""

  @nn.compact
  def __call__(self, x):
    is_initialized = self.has_variable('stats', 'counter')
    # add a variable
    v = self.variable('stats', 'counter', jnp.zeros, x.shape[1:])
    # update variable if initialized
    if is_initialized:
      v.value += 1
    x = v.value * x
    x = nn.Dense(features=3)(x)
    x = nn.log_softmax(x)
    return x

def onehot(labels, num_classes=3):
  x = (labels[..., None] == jnp.arange(num_classes)[None])
  return x.astype(jnp.float32)

def cross_entropy_loss(logits, labels):
  return -jnp.mean(jnp.sum(onehot(labels) * logits, axis=-1))

def loss_fn(variables):
  dummy_input = jax.random.normal(key, (1, 10))
  dummy_label = jnp.array([1])
  # Mark variable collection as mutable
  logits, updated_state = ToyModule().apply(variables, dummy_input, mutable=['stats'])
  print(logits)
  loss = cross_entropy_loss(logits, dummy_label)
  return loss, logits

"""
Explanation: Create a toy flax module with parameters and variable
End of explanation
"""


key = jax.random.PRNGKey(0)
init_shape = jnp.ones((1, 10), jnp.float32)
params_and_variables = ToyModule().init(key, init_shape)

"""
Explanation: Initialize model
End of explanation
"""


loss_fn(params_and_variables)

"""
Explanation: Call loss_fn with initial parameters and variables
End of explanation
"""


grad_fn = jax.value_and_grad(loss_fn, has_aux=True)

"""
Explanation: Prepare gradient function of loss_fn
End of explanation
"""


(_, logits), grad = grad_fn(params_and_variables)

"""
Explanation: Compute gradients wrt all parameters and variables
End of explanation
"""


grad['stats']['counter'].shape

grad['stats']['counter']

"""
Explanation: Gradients wrt to variable
End of explanation
"""


grad['params']['Dense_0']['kernel'].shape

grad['params']['Dense_0']['kernel']

"""
Explanation: Gradients wrt to params
End of explanation
"""


