class_docstr: SkorchDoctor helps you better understand and debug neural net training.
  
  By providing records activations, gradienst, and parameter updates, as well
  as useful plotting functions, this class can assist you in understanding
  your training process and how to improve it.
  
  This is heavily inspred by the tips from Andrej Karpathy on how to better
  understand the neural net and diagnose potential errors.
  
      - https://karpathy.github.io/2019/04/25/recipe/
      - https://www.youtube.com/watch?v=P6sfmUTpUmc
  
  To use this class, initialize your skorch net and load your data as you
  would typically do when training a net. Then, initializea ``SkorchDoctor``
  instance by passing said net, and fit the doctor with a small amount of
  data. Finally, use the records or plotting functions to help you better
  understand the training process.
  
  What exactly you do with this information is up to you. Some examples that
  come to mind:
  
      - Use the ``plot_loss`` figure to see if your model is powerful enough
        to completely overfit a small sample. If not, consider increasing its
        capacity, e.g. by stacking more layers or using more units per layer.
  
      - Use the distribution of activations to see if some layers produce
        extreme values and may need a different non-linearity or some form of
        normalization, e.g. batch norm or layer norm. A different weight
        initialization scheme could also help.
  
      - Check the relative magnitude of the parameter updates to check if your
        learning rate is too low or too high. Maybe some layers should be
        frozen, or you might want to have different learning rates for
        different parameter groups, or an adaptive optimizer like Adam.
  
      - If gradients are too big, consider using gradient clipping. If the
        mangitude of gradients shifts over time, you might want to use a
        learning rate scheduler.
  
  At the end of the day, ``SkorchDoctor`` will not tell you what you need to
  do to improve the training process, but it will greatly facilitate to
  diagnose potential problems.
  
  Examples
  --------
  >>> net = NeuralNet(..., max_epochs=5)  # a couple of epochs are enough
  >>> from skorch.helper import SkorchDoctor
  >>> doctor = SkorchDoctor(net)
  >>> X_sample, y_sample = X[:100], y[:100]  # a few samples are enough
  >>> doctor.fit(X_sample, y_sample)
  >>> # now use the attributes and plotting functions to better
  >>> # understand the training process
  >>> doctor.activation_recs_  # the recorded activations
  >>> doctor.gradient_recs_  # the recorded gradients
  >>> doctor.param_update_recs_  # the recorded parameter updates
  >>> # the next steps require matplotlib to be installed
  >>> doctor.plot_loss()
  >>> doctor.plot_activations()
  >>> doctor.plot_gradients()
  >>> doctor.plot_param_updates()
  >>> doctor.plot_activations_over_time(<layer-name>)
  >>> doctor.plot_gradients_over_time(<param-name>)
  
  Notes
  -----
  Even if a train/valid split is used for the net, only training data is
  recorded.
  
  Since ``SkorchDoctor`` will record a lot of values, you should expect an
  increase in memory usage and training time. However, it's sufficient to
  train with a handful of samples and only a few epochs, which helps
  offsetting those disadvantages.
  
  After you finished the analysis, it is recommended to re-initialize the net
  or even better start a new process. This is because the net you passed is
  modified by adding hooks training it. Although there is a clean up step at
  the end, it's better to start fresh when starting the real model training.
  
  Parameters
  ----------
  net : skorch.NeuralNet
    The skorch net to be diagnosed.
  
  match_fn : callable or None (default=None)
    If ``match_fn=None``, all activations, gradients, and parameter updates
    are recorded.
  
    If not ``None``, this should be a callable/function that takes the name of
    a layer or parameter as input and returns a bool as output, where
    ``False`` indicates that this output should be excluded. As an example, if
    you have a module with a :class:`torch.nn.Linear` layer called ``"fc"``
    and you only want to keep records from that layer, and also not record any
    gradients on biases, the ``match_fn`` could be defined as:
  
    ``match_fn = lambda name: ("fc" in name) and ("bias" not in name)``
  
  Attributes
  ----------
  num_steps_ : int
    The total number of training steps. E.g. if trained for 10 epochs with 5
    batches each, that would be 50.
  
  module_names_ : list of str
    All modules used by the net, typically those are ``"module"`` and
    ``"criterion"``.
  
  activation_recs_: dict of list of dict of np.ndarray
    The activations of each layer for each module. The outer dict contains one
    entry for each top level module, e.g. ``module`` and ``criterion``. The
    values are lists, one entry for each training step. The entries of those
    lists are again dictionaries, with keys corresponding to the layer name
    and values to the activations of that layer, stored as a numpy array.
  
    This data structure seems to be a bit complicated at first but its use is
    quite straightforward. E.g. to get the activations of the layer called
    "dense0" of the "module" in epoch 0 and batch 0, use
    ``doctor.activation_recs_['module'][0]['dense0']``.
  
    If an activation is not a simple array, it is disambiguated. E.g. if it's a
    list, the name get a suffix of ``[i]`` where ``i`` designates the index in
    the list. Similary, when the output is a dict, a ``[key]`` suffix is
    added, where ``[key]`` is the key of the corresponding value in the
    dictionary.
  
  gradient_recs_: dict of list of dict of np.ndarray
    The gradients of each parameter for each module. The outer dict contains
    one entry for each top level module, e.g. ``module`` and ``criterion``.
    The values are lists, one entry for each training step. The entries of
    those lists are dictionaries, with keys corresponding to the parameter
    name and values to the gradients of that parameter, stored as a numpy
    array. Only learnable parameters are recorded.
  
    This data structure seems to be a bit complicated at first but its use is
    quite straightforward. E.g. to get the gradient of the parameter called
    "dense0.weight" of the "module" from training step 7, use
    ``doctor.gradient_recs_['module'][7]['dense0.weight]``.
  
  param_update_recs_: dict of list of dict of float
    The relative parameter update of each parameter for each module. The outer
    dict contains one entry for each top level module, e.g. ``module`` and
    ``criterion``. The values are lists, one entry for each training step. The
    entries of those lists are dictionaries, with keys corresponding to the
    parameter name and values to the standard deviation of the update of that
    parameter, relative to the standard deviation of that parameter itself,
    stored as a float. Only learnable parameters are recorded.
  
    This data structure seems to be a bit complicated at first but its use is
    quite straightforward. E.g. to get the update of the parameter called
    "dense0.weight" of the "module" in the last training step, use
    ``doctor.paramter_udpate_recs_['module'][-1]['dense0.weight]``.
  
  fitted_ : bool
    Whether the instance has been fitted.
docstr: Calls the ``predict_proba`` method of the underlying net
docstr_tok: ["Calls","the","``","predict_proba","``","method","of","the","underlying","net"]
code: def predict_proba(self, X, **kwargs):
      """Calls the ``predict_proba`` method of the underlying net"""
      return self.net.predict_proba(X, **kwargs)
  
imports: from collections.abc import Mapping
  from functools import partial
  import numpy
  from sklearn.exceptions import NotFittedError
  from sklearn.utils.validation import check_is_fitted
  import torch
  from skorch.exceptions import NotInitializedError
  from skorch.utils import to_numpy
label_desc: Use image node_id 9 for calling the SkorchDoctor obj's underlying member method code with example usage: obj.predict_proba(X) and returns: self