# Path: scratch/deep_learning.py
# class Optimizer:
#     """
#     An optimizer updates the weights of a layer (in place) using information
#     known by either the layer or the optimizer (or by both).
#     """
#     def step(self, layer: Layer) -> None:
#         raise NotImplementedError
# 
# class Layer:
#     """
#     Our neural networks will be composed of Layers, each of which
#     knows how to do some computation on its inputs in the "forward"
#     direction and propagate gradients in the "backward" direction.
#     """
#     def forward(self, input):
#         """
#         Note the lack of types. We're not going to be prescriptive
#         about what kinds of inputs layers can take and what kinds
#         of outputs they can return.
#         """
#         raise NotImplementedError
# 
#     def backward(self, gradient):
#         """
#         Similarly, we're not going to be prescriptive about what the
#         gradient looks like. It's up to you the user to make sure
#         that you're doing things sensibly.
#         """
#         raise NotImplementedError
# 
#     def params(self) -> Iterable[Tensor]:
#         """
#         Returns the parameters of this layer. The default implementation
#         returns nothing, so that if you have a layer with no parameters
#         you don't have to implement this.
#         """
#         return ()
# 
#     def grads(self) -> Iterable[Tensor]:
#         """
#         Returns the gradients, in the same order as params()
#         """
#         return ()


from scratch.deep_learning import Optimizer, Layer

class EmbeddingOptimizer(Optimizer):
    """
    Optimized for the case where there are
    only embedding layers with single id updates.
    """
    def __init__(self, learning_rate: float) -> None:
        self.lr = learning_rate


# Path: scratch/deep_learning.py
# class Optimizer:
#     """
#     An optimizer updates the weights of a layer (in place) using information
#     known by either the layer or the optimizer (or by both).
#     """
#     def step(self, layer: Layer) -> None:
#         raise NotImplementedError
# 
# class Layer:
#     """
#     Our neural networks will be composed of Layers, each of which
#     knows how to do some computation on its inputs in the "forward"
#     direction and propagate gradients in the "backward" direction.
#     """
#     def forward(self, input):
#         """
#         Note the lack of types. We're not going to be prescriptive
#         about what kinds of inputs layers can take and what kinds
#         of outputs they can return.
#         """
#         raise NotImplementedError
# 
#     def backward(self, gradient):
#         """
#         Similarly, we're not going to be prescriptive about what the
#         gradient looks like. It's up to you the user to make sure
#         that you're doing things sensibly.
#         """
#         raise NotImplementedError
# 
#     def params(self) -> Iterable[Tensor]:
#         """
#         Returns the parameters of this layer. The default implementation
#         returns nothing, so that if you have a layer with no parameters
#         you don't have to implement this.
#         """
#         return ()
# 
#     def grads(self) -> Iterable[Tensor]:
#         """
#         Returns the gradients, in the same order as params()
#         """
#         return ()


# Path: scratch/nlp_advanced.py
from scratch.deep_learning import Optimizer, Layer


class EmbeddingOptimizer(Optimizer):
    """
    Optimized for the case where there are
    only embedding layers with single id updates.
    """
    def __init__(self, learning_rate: float) -> None:
        self.lr = learning_rate


    def step(self, layer: Layer) -> None: