diff --git a/.tether/man/InputLayer.txt b/.tether/man/InputLayer.txt index 6f9f042d13..f62ae28557 100644 --- a/.tether/man/InputLayer.txt +++ b/.tether/man/InputLayer.txt @@ -1,7 +1,7 @@ Help on class InputLayer in module keras.src.layers.core.input_layer: class InputLayer(keras.src.layers.layer.Layer) - | InputLayer(shape=None, batch_size=None, dtype=None, sparse=None, batch_shape=None, input_tensor=None, name=None, **kwargs) + | InputLayer(shape=None, batch_size=None, dtype=None, sparse=None, batch_shape=None, input_tensor=None, optional=False, name=None, **kwargs) | | Method resolution order: | InputLayer @@ -24,6 +24,7 @@ class InputLayer(keras.src.layers.layer.Layer) | sparse=None, | batch_shape=None, | input_tensor=None, + | optional=False, | name=None, | **kwargs | ) diff --git a/.tether/man/Layer.txt b/.tether/man/Layer.txt index dffdc81edc..24e727604d 100644 --- a/.tether/man/Layer.txt +++ b/.tether/man/Layer.txt @@ -462,6 +462,14 @@ class Layer(keras.src.backend.tensorflow.layer.TFLayer, keras.src.ops.operation. | training. Unlike, `layer.non_trainable_variables` this excludes metric | state and random seeds. | + | path + | The path of the layer. + | + | If the layer has not been built yet, it will be `None`. + | + | quantization_mode + | The quantization mode of this layer, `None` if not quantized. + | | trainable_variables | List of all trainable layer state. | diff --git a/.tether/man/application_convnext_base.txt b/.tether/man/application_convnext_base.txt index f612065e0b..76fa46069b 100644 --- a/.tether/man/application_convnext_base.txt +++ b/.tether/man/application_convnext_base.txt @@ -1,6 +1,5 @@ __signature__ keras.applications.ConvNeXtBase( - model_name='convnext_base', include_top=True, include_preprocessing=True, weights='imagenet', @@ -8,7 +7,8 @@ keras.applications.ConvNeXtBase( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='convnext_base' ) __doc__ Instantiates the ConvNeXtBase architecture. @@ -73,6 +73,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_convnext_large.txt b/.tether/man/application_convnext_large.txt index bcee39e4e1..1664d692c6 100644 --- a/.tether/man/application_convnext_large.txt +++ b/.tether/man/application_convnext_large.txt @@ -1,6 +1,5 @@ __signature__ keras.applications.ConvNeXtLarge( - model_name='convnext_large', include_top=True, include_preprocessing=True, weights='imagenet', @@ -8,7 +7,8 @@ keras.applications.ConvNeXtLarge( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='convnext_large' ) __doc__ Instantiates the ConvNeXtLarge architecture. @@ -73,6 +73,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_convnext_small.txt b/.tether/man/application_convnext_small.txt index 48303a2f5f..158e4d9784 100644 --- a/.tether/man/application_convnext_small.txt +++ b/.tether/man/application_convnext_small.txt @@ -1,6 +1,5 @@ __signature__ keras.applications.ConvNeXtSmall( - model_name='convnext_small', include_top=True, include_preprocessing=True, weights='imagenet', @@ -8,7 +7,8 @@ keras.applications.ConvNeXtSmall( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='convnext_small' ) __doc__ Instantiates the ConvNeXtSmall architecture. @@ -73,6 +73,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_convnext_tiny.txt b/.tether/man/application_convnext_tiny.txt index dd9e1f4b59..e3a4fadac6 100644 --- a/.tether/man/application_convnext_tiny.txt +++ b/.tether/man/application_convnext_tiny.txt @@ -1,6 +1,5 @@ __signature__ keras.applications.ConvNeXtTiny( - model_name='convnext_tiny', include_top=True, include_preprocessing=True, weights='imagenet', @@ -8,7 +7,8 @@ keras.applications.ConvNeXtTiny( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='convnext_tiny' ) __doc__ Instantiates the ConvNeXtTiny architecture. @@ -73,6 +73,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_convnext_xlarge.txt b/.tether/man/application_convnext_xlarge.txt index ccbd4def0e..abaf49f25a 100644 --- a/.tether/man/application_convnext_xlarge.txt +++ b/.tether/man/application_convnext_xlarge.txt @@ -1,6 +1,5 @@ __signature__ keras.applications.ConvNeXtXLarge( - model_name='convnext_xlarge', include_top=True, include_preprocessing=True, weights='imagenet', @@ -8,7 +7,8 @@ keras.applications.ConvNeXtXLarge( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='convnext_xlarge' ) __doc__ Instantiates the ConvNeXtXLarge architecture. @@ -73,6 +73,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_densenet121.txt b/.tether/man/application_densenet121.txt index 24db06c180..863d8b2442 100644 --- a/.tether/man/application_densenet121.txt +++ b/.tether/man/application_densenet121.txt @@ -6,7 +6,8 @@ keras.applications.DenseNet121( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='densenet121' ) __doc__ Instantiates the Densenet121 architecture. @@ -25,40 +26,42 @@ on your inputs before passing them to the model. Args: include_top: whether to include the fully-connected - layer at the top of the network. + layer at the top of the network. weights: one of `None` (random initialization), - `"imagenet"` (pre-training on ImageNet), - or the path to the weights file to be loaded. - input_tensor: optional Keras tensor - (i.e. output of `layers.Input()`) - to use as image input for the model. + `"imagenet"` (pre-training on ImageNet), + or the path to the weights file to be loaded. + input_tensor: optional Keras tensor + (i.e. output of `layers.Input()`) + to use as image input for the model. input_shape: optional shape tuple, only to be specified - if `include_top` is False (otherwise the input shape - has to be `(224, 224, 3)` (with `'channels_last'` data format) - or `(3, 224, 224)` (with `'channels_first'` data format). - It should have exactly 3 inputs channels, - and width and height should be no smaller than 32. - E.g. `(200, 200, 3)` would be one valid value. + if `include_top` is False (otherwise the input shape + has to be `(224, 224, 3)` (with `'channels_last'` data format) + or `(3, 224, 224)` (with `'channels_first'` data format). + It should have exactly 3 inputs channels, + and width and height should be no smaller than 32. + E.g. `(200, 200, 3)` would be one valid value. pooling: Optional pooling mode for feature extraction - when `include_top` is `False`. - - `None` means that the output of the model will be - the 4D tensor output of the - last convolutional block. - - `avg` means that global average pooling - will be applied to the output of the - last convolutional block, and thus - the output of the model will be a 2D tensor. - - `max` means that global max pooling will - be applied. + when `include_top` is `False`. + - `None` means that the output of the model will be + the 4D tensor output of the + last convolutional block. + - `avg` means that global average pooling + will be applied to the output of the + last convolutional block, and thus + the output of the model will be a 2D tensor. + - `max` means that global max pooling will + be applied. classes: optional number of classes to classify images - into, only to be specified if `include_top` is `True`, and - if no `weights` argument is specified. + into, only to be specified if `include_top` is `True`, and + if no `weights` argument is specified. classifier_activation: A `str` or callable. - The activation function to use - on the "top" layer. Ignored unless `include_top=True`. Set - `classifier_activation=None` to return the logits - of the "top" layer. When loading pretrained weights, - `classifier_activation` can only be `None` or `"softmax"`. + The activation function to use + on the "top" layer. Ignored unless `include_top=True`. Set + `classifier_activation=None` to return the logits + of the "top" layer. When loading pretrained weights, + `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Keras model instance. + diff --git a/.tether/man/application_densenet169.txt b/.tether/man/application_densenet169.txt index 234ce93f15..1f2e9bc3a4 100644 --- a/.tether/man/application_densenet169.txt +++ b/.tether/man/application_densenet169.txt @@ -6,7 +6,8 @@ keras.applications.DenseNet169( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='densenet169' ) __doc__ Instantiates the Densenet169 architecture. @@ -25,40 +26,42 @@ on your inputs before passing them to the model. Args: include_top: whether to include the fully-connected - layer at the top of the network. + layer at the top of the network. weights: one of `None` (random initialization), - `"imagenet"` (pre-training on ImageNet), - or the path to the weights file to be loaded. - input_tensor: optional Keras tensor - (i.e. output of `layers.Input()`) - to use as image input for the model. + `"imagenet"` (pre-training on ImageNet), + or the path to the weights file to be loaded. + input_tensor: optional Keras tensor + (i.e. output of `layers.Input()`) + to use as image input for the model. input_shape: optional shape tuple, only to be specified - if `include_top` is False (otherwise the input shape - has to be `(224, 224, 3)` (with `'channels_last'` data format) - or `(3, 224, 224)` (with `'channels_first'` data format). - It should have exactly 3 inputs channels, - and width and height should be no smaller than 32. - E.g. `(200, 200, 3)` would be one valid value. + if `include_top` is False (otherwise the input shape + has to be `(224, 224, 3)` (with `'channels_last'` data format) + or `(3, 224, 224)` (with `'channels_first'` data format). + It should have exactly 3 inputs channels, + and width and height should be no smaller than 32. + E.g. `(200, 200, 3)` would be one valid value. pooling: Optional pooling mode for feature extraction - when `include_top` is `False`. - - `None` means that the output of the model will be - the 4D tensor output of the - last convolutional block. - - `avg` means that global average pooling - will be applied to the output of the - last convolutional block, and thus - the output of the model will be a 2D tensor. - - `max` means that global max pooling will - be applied. + when `include_top` is `False`. + - `None` means that the output of the model will be + the 4D tensor output of the + last convolutional block. + - `avg` means that global average pooling + will be applied to the output of the + last convolutional block, and thus + the output of the model will be a 2D tensor. + - `max` means that global max pooling will + be applied. classes: optional number of classes to classify images - into, only to be specified if `include_top` is `True`, and - if no `weights` argument is specified. + into, only to be specified if `include_top` is `True`, and + if no `weights` argument is specified. classifier_activation: A `str` or callable. - The activation function to use - on the "top" layer. Ignored unless `include_top=True`. Set - `classifier_activation=None` to return the logits - of the "top" layer. When loading pretrained weights, - `classifier_activation` can only be `None` or `"softmax"`. + The activation function to use + on the "top" layer. Ignored unless `include_top=True`. Set + `classifier_activation=None` to return the logits + of the "top" layer. When loading pretrained weights, + `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Keras model instance. + diff --git a/.tether/man/application_densenet201.txt b/.tether/man/application_densenet201.txt index 491c76670b..af9ef237a0 100644 --- a/.tether/man/application_densenet201.txt +++ b/.tether/man/application_densenet201.txt @@ -6,7 +6,8 @@ keras.applications.DenseNet201( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='densenet201' ) __doc__ Instantiates the Densenet201 architecture. @@ -25,40 +26,42 @@ on your inputs before passing them to the model. Args: include_top: whether to include the fully-connected - layer at the top of the network. + layer at the top of the network. weights: one of `None` (random initialization), - `"imagenet"` (pre-training on ImageNet), - or the path to the weights file to be loaded. - input_tensor: optional Keras tensor - (i.e. output of `layers.Input()`) - to use as image input for the model. + `"imagenet"` (pre-training on ImageNet), + or the path to the weights file to be loaded. + input_tensor: optional Keras tensor + (i.e. output of `layers.Input()`) + to use as image input for the model. input_shape: optional shape tuple, only to be specified - if `include_top` is False (otherwise the input shape - has to be `(224, 224, 3)` (with `'channels_last'` data format) - or `(3, 224, 224)` (with `'channels_first'` data format). - It should have exactly 3 inputs channels, - and width and height should be no smaller than 32. - E.g. `(200, 200, 3)` would be one valid value. + if `include_top` is False (otherwise the input shape + has to be `(224, 224, 3)` (with `'channels_last'` data format) + or `(3, 224, 224)` (with `'channels_first'` data format). + It should have exactly 3 inputs channels, + and width and height should be no smaller than 32. + E.g. `(200, 200, 3)` would be one valid value. pooling: Optional pooling mode for feature extraction - when `include_top` is `False`. - - `None` means that the output of the model will be - the 4D tensor output of the - last convolutional block. - - `avg` means that global average pooling - will be applied to the output of the - last convolutional block, and thus - the output of the model will be a 2D tensor. - - `max` means that global max pooling will - be applied. + when `include_top` is `False`. + - `None` means that the output of the model will be + the 4D tensor output of the + last convolutional block. + - `avg` means that global average pooling + will be applied to the output of the + last convolutional block, and thus + the output of the model will be a 2D tensor. + - `max` means that global max pooling will + be applied. classes: optional number of classes to classify images - into, only to be specified if `include_top` is `True`, and - if no `weights` argument is specified. + into, only to be specified if `include_top` is `True`, and + if no `weights` argument is specified. classifier_activation: A `str` or callable. - The activation function to use - on the "top" layer. Ignored unless `include_top=True`. Set - `classifier_activation=None` to return the logits - of the "top" layer. When loading pretrained weights, - `classifier_activation` can only be `None` or `"softmax"`. + The activation function to use + on the "top" layer. Ignored unless `include_top=True`. Set + `classifier_activation=None` to return the logits + of the "top" layer. When loading pretrained weights, + `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Keras model instance. + diff --git a/.tether/man/application_efficientnet_b0.txt b/.tether/man/application_efficientnet_b0.txt index b1c8ddf40a..94d269cc0d 100644 --- a/.tether/man/application_efficientnet_b0.txt +++ b/.tether/man/application_efficientnet_b0.txt @@ -7,7 +7,7 @@ keras.applications.EfficientNetB0( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb0' ) __doc__ Instantiates the EfficientNetB0 architecture. @@ -68,6 +68,8 @@ Args: Defaults to `'softmax'`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_b1.txt b/.tether/man/application_efficientnet_b1.txt index 5f07fd8106..816b3f92cd 100644 --- a/.tether/man/application_efficientnet_b1.txt +++ b/.tether/man/application_efficientnet_b1.txt @@ -7,7 +7,7 @@ keras.applications.EfficientNetB1( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb1' ) __doc__ Instantiates the EfficientNetB1 architecture. @@ -68,6 +68,8 @@ Args: Defaults to `'softmax'`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_b2.txt b/.tether/man/application_efficientnet_b2.txt index d46d5cc50b..3f98c48e0e 100644 --- a/.tether/man/application_efficientnet_b2.txt +++ b/.tether/man/application_efficientnet_b2.txt @@ -7,7 +7,7 @@ keras.applications.EfficientNetB2( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb2' ) __doc__ Instantiates the EfficientNetB2 architecture. @@ -68,6 +68,8 @@ Args: Defaults to `'softmax'`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_b3.txt b/.tether/man/application_efficientnet_b3.txt index 4882fa48ba..577d4ad55b 100644 --- a/.tether/man/application_efficientnet_b3.txt +++ b/.tether/man/application_efficientnet_b3.txt @@ -7,7 +7,7 @@ keras.applications.EfficientNetB3( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb3' ) __doc__ Instantiates the EfficientNetB3 architecture. @@ -68,6 +68,8 @@ Args: Defaults to `'softmax'`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_b4.txt b/.tether/man/application_efficientnet_b4.txt index 9f9ae69c33..20cbce3575 100644 --- a/.tether/man/application_efficientnet_b4.txt +++ b/.tether/man/application_efficientnet_b4.txt @@ -7,7 +7,7 @@ keras.applications.EfficientNetB4( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb4' ) __doc__ Instantiates the EfficientNetB4 architecture. @@ -68,6 +68,8 @@ Args: Defaults to `'softmax'`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_b5.txt b/.tether/man/application_efficientnet_b5.txt index 4bf093aeb1..4a654ca4f1 100644 --- a/.tether/man/application_efficientnet_b5.txt +++ b/.tether/man/application_efficientnet_b5.txt @@ -7,7 +7,7 @@ keras.applications.EfficientNetB5( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb5' ) __doc__ Instantiates the EfficientNetB5 architecture. @@ -68,6 +68,8 @@ Args: Defaults to `'softmax'`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_b6.txt b/.tether/man/application_efficientnet_b6.txt index 928b0c8d52..368da8990d 100644 --- a/.tether/man/application_efficientnet_b6.txt +++ b/.tether/man/application_efficientnet_b6.txt @@ -7,7 +7,7 @@ keras.applications.EfficientNetB6( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb6' ) __doc__ Instantiates the EfficientNetB6 architecture. @@ -68,6 +68,8 @@ Args: Defaults to `'softmax'`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_b7.txt b/.tether/man/application_efficientnet_b7.txt index e0c1b48cfb..b601e3d9bd 100644 --- a/.tether/man/application_efficientnet_b7.txt +++ b/.tether/man/application_efficientnet_b7.txt @@ -7,7 +7,7 @@ keras.applications.EfficientNetB7( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb7' ) __doc__ Instantiates the EfficientNetB7 architecture. @@ -68,6 +68,8 @@ Args: Defaults to `'softmax'`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_v2b0.txt b/.tether/man/application_efficientnet_v2b0.txt index e75cbe3f1a..b0e6d4db74 100644 --- a/.tether/man/application_efficientnet_v2b0.txt +++ b/.tether/man/application_efficientnet_v2b0.txt @@ -7,7 +7,8 @@ keras.applications.EfficientNetV2B0( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-b0' ) __doc__ Instantiates the EfficientNetV2B0 architecture. @@ -71,6 +72,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_v2b1.txt b/.tether/man/application_efficientnet_v2b1.txt index e61a40a69b..b487f93544 100644 --- a/.tether/man/application_efficientnet_v2b1.txt +++ b/.tether/man/application_efficientnet_v2b1.txt @@ -7,7 +7,8 @@ keras.applications.EfficientNetV2B1( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-b1' ) __doc__ Instantiates the EfficientNetV2B1 architecture. @@ -71,6 +72,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_v2b2.txt b/.tether/man/application_efficientnet_v2b2.txt index edbd5e7041..93aec856dc 100644 --- a/.tether/man/application_efficientnet_v2b2.txt +++ b/.tether/man/application_efficientnet_v2b2.txt @@ -7,7 +7,8 @@ keras.applications.EfficientNetV2B2( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-b2' ) __doc__ Instantiates the EfficientNetV2B2 architecture. @@ -71,6 +72,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_v2b3.txt b/.tether/man/application_efficientnet_v2b3.txt index 651433155c..4f4191b157 100644 --- a/.tether/man/application_efficientnet_v2b3.txt +++ b/.tether/man/application_efficientnet_v2b3.txt @@ -7,7 +7,8 @@ keras.applications.EfficientNetV2B3( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-b3' ) __doc__ Instantiates the EfficientNetV2B3 architecture. @@ -71,6 +72,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_v2l.txt b/.tether/man/application_efficientnet_v2l.txt index fa1044efbb..c549613a66 100644 --- a/.tether/man/application_efficientnet_v2l.txt +++ b/.tether/man/application_efficientnet_v2l.txt @@ -7,7 +7,8 @@ keras.applications.EfficientNetV2L( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-l' ) __doc__ Instantiates the EfficientNetV2L architecture. @@ -71,6 +72,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_v2m.txt b/.tether/man/application_efficientnet_v2m.txt index c81200662f..daa695dab3 100644 --- a/.tether/man/application_efficientnet_v2m.txt +++ b/.tether/man/application_efficientnet_v2m.txt @@ -7,7 +7,8 @@ keras.applications.EfficientNetV2M( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-m' ) __doc__ Instantiates the EfficientNetV2M architecture. @@ -71,6 +72,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_efficientnet_v2s.txt b/.tether/man/application_efficientnet_v2s.txt index eb0a2e95a7..9c779622c5 100644 --- a/.tether/man/application_efficientnet_v2s.txt +++ b/.tether/man/application_efficientnet_v2s.txt @@ -7,7 +7,8 @@ keras.applications.EfficientNetV2S( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-s' ) __doc__ Instantiates the EfficientNetV2S architecture. @@ -71,6 +72,8 @@ Args: Defaults to `"softmax"`. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_inception_resnet_v2.txt b/.tether/man/application_inception_resnet_v2.txt index e9c3b0b361..ff02807040 100644 --- a/.tether/man/application_inception_resnet_v2.txt +++ b/.tether/man/application_inception_resnet_v2.txt @@ -6,7 +6,8 @@ keras.applications.InceptionResNetV2( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='inception_resnet_v2' ) __doc__ Instantiates the Inception-ResNet v2 architecture. @@ -69,6 +70,8 @@ Args: Set `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_inception_v3.txt b/.tether/man/application_inception_v3.txt index e01a75a374..50fbd0585b 100644 --- a/.tether/man/application_inception_v3.txt +++ b/.tether/man/application_inception_v3.txt @@ -6,7 +6,8 @@ keras.applications.InceptionV3( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='inception_v3' ) __doc__ Instantiates the Inception v3 architecture. @@ -69,6 +70,8 @@ Args: Set `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_mobilenet.txt b/.tether/man/application_mobilenet.txt index 095e3e674d..6a1c7705d9 100644 --- a/.tether/man/application_mobilenet.txt +++ b/.tether/man/application_mobilenet.txt @@ -9,7 +9,8 @@ keras.applications.MobileNet( input_tensor=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name=None ) __doc__ Instantiates the MobileNet architecture. @@ -82,6 +83,8 @@ Args: Set `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: String, the name of the model. Returns: A model instance. + diff --git a/.tether/man/application_mobilenet_v2.txt b/.tether/man/application_mobilenet_v2.txt index 8c804cbe03..9b044a58ca 100644 --- a/.tether/man/application_mobilenet_v2.txt +++ b/.tether/man/application_mobilenet_v2.txt @@ -7,7 +7,8 @@ keras.applications.MobileNetV2( input_tensor=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name=None ) __doc__ Instantiates the MobileNetV2 architecture. @@ -84,6 +85,8 @@ Args: Set `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: String, the name of the model. Returns: A model instance. + diff --git a/.tether/man/application_mobilenet_v3_large.txt b/.tether/man/application_mobilenet_v3_large.txt index 542d5a7674..f6b1760b59 100644 --- a/.tether/man/application_mobilenet_v3_large.txt +++ b/.tether/man/application_mobilenet_v3_large.txt @@ -10,7 +10,8 @@ keras.applications.MobileNetV3Large( pooling=None, dropout_rate=0.2, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='MobileNetV3Large' ) __doc__ Instantiates the MobileNetV3Large architecture. @@ -109,6 +110,7 @@ Args: be `None` or `"softmax"`. include_preprocessing: Boolean, whether to include the preprocessing layer (`Rescaling`) at the bottom of the network. Defaults to `True`. + name: String, the name of the model. Call arguments: inputs: A floating point `numpy.array` or backend-native tensor, @@ -118,3 +120,4 @@ Call arguments: Returns: A model instance. + diff --git a/.tether/man/application_mobilenet_v3_small.txt b/.tether/man/application_mobilenet_v3_small.txt index 8cb001c8d9..f60e3ed1a8 100644 --- a/.tether/man/application_mobilenet_v3_small.txt +++ b/.tether/man/application_mobilenet_v3_small.txt @@ -10,7 +10,8 @@ keras.applications.MobileNetV3Small( pooling=None, dropout_rate=0.2, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='MobileNetV3Small' ) __doc__ Instantiates the MobileNetV3Small architecture. @@ -109,6 +110,7 @@ Args: be `None` or `"softmax"`. include_preprocessing: Boolean, whether to include the preprocessing layer (`Rescaling`) at the bottom of the network. Defaults to `True`. + name: String, the name of the model. Call arguments: inputs: A floating point `numpy.array` or backend-native tensor, @@ -118,3 +120,4 @@ Call arguments: Returns: A model instance. + diff --git a/.tether/man/application_nasnetlarge.txt b/.tether/man/application_nasnet_large.txt similarity index 96% rename from .tether/man/application_nasnetlarge.txt rename to .tether/man/application_nasnet_large.txt index e5123c3f80..fd7de9e764 100644 --- a/.tether/man/application_nasnetlarge.txt +++ b/.tether/man/application_nasnet_large.txt @@ -6,7 +6,8 @@ keras.applications.NASNetLarge( input_tensor=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='nasnet_large' ) __doc__ Instantiates a NASNet model in ImageNet mode. @@ -58,6 +59,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Keras model instance. + diff --git a/.tether/man/application_nasnetmobile.txt b/.tether/man/application_nasnet_mobile.txt similarity index 96% rename from .tether/man/application_nasnetmobile.txt rename to .tether/man/application_nasnet_mobile.txt index bec8825517..629dc7549d 100644 --- a/.tether/man/application_nasnetmobile.txt +++ b/.tether/man/application_nasnet_mobile.txt @@ -6,7 +6,8 @@ keras.applications.NASNetMobile( input_tensor=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='nasnet_mobile' ) __doc__ Instantiates a Mobile NASNet model in ImageNet mode. @@ -58,6 +59,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Keras model instance. + diff --git a/.tether/man/application_resnet101.txt b/.tether/man/application_resnet101.txt index 3ffe21d7ad..f62496e384 100644 --- a/.tether/man/application_resnet101.txt +++ b/.tether/man/application_resnet101.txt @@ -6,7 +6,8 @@ keras.applications.ResNet101( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet101' ) __doc__ Instantiates the ResNet101 architecture. @@ -58,6 +59,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Model instance. + diff --git a/.tether/man/application_resnet101_v2.txt b/.tether/man/application_resnet101_v2.txt index 8a9026de1a..811d70caec 100644 --- a/.tether/man/application_resnet101_v2.txt +++ b/.tether/man/application_resnet101_v2.txt @@ -6,7 +6,8 @@ keras.applications.ResNet101V2( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet101v2' ) __doc__ Instantiates the ResNet101V2 architecture. @@ -57,6 +58,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Model instance. + diff --git a/.tether/man/application_resnet152.txt b/.tether/man/application_resnet152.txt index bf7ff708cf..3f861bbf68 100644 --- a/.tether/man/application_resnet152.txt +++ b/.tether/man/application_resnet152.txt @@ -6,7 +6,8 @@ keras.applications.ResNet152( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet152' ) __doc__ Instantiates the ResNet152 architecture. @@ -58,6 +59,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Model instance. + diff --git a/.tether/man/application_resnet152_v2.txt b/.tether/man/application_resnet152_v2.txt index 3fd8ceb419..7a62353107 100644 --- a/.tether/man/application_resnet152_v2.txt +++ b/.tether/man/application_resnet152_v2.txt @@ -6,7 +6,8 @@ keras.applications.ResNet152V2( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet152v2' ) __doc__ Instantiates the ResNet152V2 architecture. @@ -57,6 +58,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Model instance. + diff --git a/.tether/man/application_resnet50.txt b/.tether/man/application_resnet50.txt index 1793958cd3..501c5b1e10 100644 --- a/.tether/man/application_resnet50.txt +++ b/.tether/man/application_resnet50.txt @@ -6,7 +6,8 @@ keras.applications.ResNet50( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet50' ) __doc__ Instantiates the ResNet50 architecture. @@ -58,6 +59,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Model instance. + diff --git a/.tether/man/application_resnet50_v2.txt b/.tether/man/application_resnet50_v2.txt index 2abfa6708d..ffcb8c023b 100644 --- a/.tether/man/application_resnet50_v2.txt +++ b/.tether/man/application_resnet50_v2.txt @@ -6,7 +6,8 @@ keras.applications.ResNet50V2( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet50v2' ) __doc__ Instantiates the ResNet50V2 architecture. @@ -57,6 +58,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A Model instance. + diff --git a/.tether/man/application_vgg16.txt b/.tether/man/application_vgg16.txt index 8d054c8382..dfbbeb5bd0 100644 --- a/.tether/man/application_vgg16.txt +++ b/.tether/man/application_vgg16.txt @@ -6,7 +6,8 @@ keras.applications.VGG16( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='vgg16' ) __doc__ Instantiates the VGG16 model. @@ -68,6 +69,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: - A model instance. + A `Model` instance. + diff --git a/.tether/man/application_vgg19.txt b/.tether/man/application_vgg19.txt index 143db7bb74..b64a8fd9b8 100644 --- a/.tether/man/application_vgg19.txt +++ b/.tether/man/application_vgg19.txt @@ -6,7 +6,8 @@ keras.applications.VGG19( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='vgg19' ) __doc__ Instantiates the VGG19 model. @@ -68,6 +69,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/application_xception.txt b/.tether/man/application_xception.txt index 1237bfc716..38582f10ab 100644 --- a/.tether/man/application_xception.txt +++ b/.tether/man/application_xception.txt @@ -6,7 +6,8 @@ keras.applications.Xception( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='xception' ) __doc__ Instantiates the Xception architecture. @@ -64,6 +65,8 @@ Args: `classifier_activation=None` to return the logits of the "top" layer. When loading pretrained weights, `classifier_activation` can only be `None` or `"softmax"`. + name: The name of the model (string). Returns: A model instance. + diff --git a/.tether/man/export_savedmodel.keras.src.models.model.Model.txt b/.tether/man/export_savedmodel.keras.src.models.model.Model.txt index afba37b4b8..504134fb86 100644 --- a/.tether/man/export_savedmodel.keras.src.models.model.Model.txt +++ b/.tether/man/export_savedmodel.keras.src.models.model.Model.txt @@ -29,7 +29,7 @@ Example: # Create the artifact model.export("path/to/location") -# Later, in a different process / environment... +# Later, in a different process/environment... reloaded_artifact = tf.saved_model.load("path/to/location") predictions = reloaded_artifact.serve(input_data) ``` diff --git a/.tether/man/keras.applications.txt b/.tether/man/keras.applications.txt index 8d9db78cf3..9f9c401cec 100644 --- a/.tether/man/keras.applications.txt +++ b/.tether/man/keras.applications.txt @@ -1,6 +1,5 @@ convnext: Module(keras.api.applications.convnext) ConvNeXtBase( - model_name='convnext_base', include_top=True, include_preprocessing=True, weights='imagenet', @@ -8,10 +7,10 @@ ConvNeXtBase( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='convnext_base' ) ConvNeXtLarge( - model_name='convnext_large', include_top=True, include_preprocessing=True, weights='imagenet', @@ -19,10 +18,10 @@ ConvNeXtLarge( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='convnext_large' ) ConvNeXtSmall( - model_name='convnext_small', include_top=True, include_preprocessing=True, weights='imagenet', @@ -30,10 +29,10 @@ ConvNeXtSmall( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='convnext_small' ) ConvNeXtTiny( - model_name='convnext_tiny', include_top=True, include_preprocessing=True, weights='imagenet', @@ -41,10 +40,10 @@ ConvNeXtTiny( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='convnext_tiny' ) ConvNeXtXLarge( - model_name='convnext_xlarge', include_top=True, include_preprocessing=True, weights='imagenet', @@ -52,7 +51,8 @@ ConvNeXtXLarge( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='convnext_xlarge' ) densenet: Module(keras.api.applications.densenet) DenseNet121( @@ -62,7 +62,8 @@ DenseNet121( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='densenet121' ) DenseNet169( include_top=True, @@ -71,7 +72,8 @@ DenseNet169( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='densenet169' ) DenseNet201( include_top=True, @@ -80,7 +82,8 @@ DenseNet201( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='densenet201' ) efficientnet: Module(keras.api.applications.efficientnet) efficientnet_v2: Module(keras.api.applications.efficientnet_v2) @@ -92,7 +95,7 @@ EfficientNetB0( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb0' ) EfficientNetB1( include_top=True, @@ -102,7 +105,7 @@ EfficientNetB1( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb1' ) EfficientNetB2( include_top=True, @@ -112,7 +115,7 @@ EfficientNetB2( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb2' ) EfficientNetB3( include_top=True, @@ -122,7 +125,7 @@ EfficientNetB3( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb3' ) EfficientNetB4( include_top=True, @@ -132,7 +135,7 @@ EfficientNetB4( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb4' ) EfficientNetB5( include_top=True, @@ -142,7 +145,7 @@ EfficientNetB5( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb5' ) EfficientNetB6( include_top=True, @@ -152,7 +155,7 @@ EfficientNetB6( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb6' ) EfficientNetB7( include_top=True, @@ -162,7 +165,7 @@ EfficientNetB7( pooling=None, classes=1000, classifier_activation='softmax', - **kwargs + name='efficientnetb7' ) EfficientNetV2B0( include_top=True, @@ -172,7 +175,8 @@ EfficientNetV2B0( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-b0' ) EfficientNetV2B1( include_top=True, @@ -182,7 +186,8 @@ EfficientNetV2B1( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-b1' ) EfficientNetV2B2( include_top=True, @@ -192,7 +197,8 @@ EfficientNetV2B2( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-b2' ) EfficientNetV2B3( include_top=True, @@ -202,7 +208,8 @@ EfficientNetV2B3( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-b3' ) EfficientNetV2L( include_top=True, @@ -212,7 +219,8 @@ EfficientNetV2L( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-l' ) EfficientNetV2M( include_top=True, @@ -222,7 +230,8 @@ EfficientNetV2M( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-m' ) EfficientNetV2S( include_top=True, @@ -232,7 +241,8 @@ EfficientNetV2S( pooling=None, classes=1000, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='efficientnetv2-s' ) imagenet_utils: Module(keras.api.applications.imagenet_utils) inception_resnet_v2: Module(keras.api.applications.inception_resnet_v2) @@ -244,7 +254,8 @@ InceptionResNetV2( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='inception_resnet_v2' ) InceptionV3( include_top=True, @@ -253,7 +264,8 @@ InceptionV3( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='inception_v3' ) mobilenet: Module(keras.api.applications.mobilenet) MobileNet( @@ -266,7 +278,8 @@ MobileNet( input_tensor=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name=None ) mobilenet_v2: Module(keras.api.applications.mobilenet_v2) mobilenet_v3: Module(keras.api.applications.mobilenet_v3) @@ -278,7 +291,8 @@ MobileNetV2( input_tensor=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name=None ) MobileNetV3Large( input_shape=None, @@ -291,7 +305,8 @@ MobileNetV3Large( pooling=None, dropout_rate=0.2, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='MobileNetV3Large' ) MobileNetV3Small( input_shape=None, @@ -304,7 +319,8 @@ MobileNetV3Small( pooling=None, dropout_rate=0.2, classifier_activation='softmax', - include_preprocessing=True + include_preprocessing=True, + name='MobileNetV3Small' ) nasnet: Module(keras.api.applications.nasnet) NASNetLarge( @@ -314,7 +330,8 @@ NASNetLarge( input_tensor=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='nasnet_large' ) NASNetMobile( input_shape=None, @@ -323,7 +340,8 @@ NASNetMobile( input_tensor=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='nasnet_mobile' ) resnet: Module(keras.api.applications.resnet) resnet_v2: Module(keras.api.applications.resnet_v2) @@ -334,7 +352,8 @@ ResNet101( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet101' ) ResNet101V2( include_top=True, @@ -343,7 +362,8 @@ ResNet101V2( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet101v2' ) ResNet152( include_top=True, @@ -352,7 +372,8 @@ ResNet152( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet152' ) ResNet152V2( include_top=True, @@ -361,7 +382,8 @@ ResNet152V2( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet152v2' ) resnet50: Module(keras.api.applications.resnet50) ResNet50( @@ -371,7 +393,8 @@ ResNet50( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet50' ) ResNet50V2( include_top=True, @@ -380,7 +403,8 @@ ResNet50V2( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='resnet50v2' ) vgg16: Module(keras.api.applications.vgg16) VGG16( @@ -390,7 +414,8 @@ VGG16( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='vgg16' ) vgg19: Module(keras.api.applications.vgg19) VGG19( @@ -400,7 +425,8 @@ VGG19( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='vgg19' ) xception: Module(keras.api.applications.xception) Xception( @@ -410,6 +436,7 @@ Xception( input_shape=None, pooling=None, classes=1000, - classifier_activation='softmax' + classifier_activation='softmax', + name='xception' ) diff --git a/.tether/man/keras.distribution.txt b/.tether/man/keras.distribution.txt index 2abe20bada..1bbdc17d72 100644 --- a/.tether/man/keras.distribution.txt +++ b/.tether/man/keras.distribution.txt @@ -11,12 +11,13 @@ initialize( num_processes=None, process_id=None ) -LayoutMap(device_mesh=None) +LayoutMap(device_mesh) list_devices(device_type=None) ModelParallel( - device_mesh, - layout_map, - batch_dim_name=None + *, + layout_map=None, + batch_dim_name=None, + **kwargs ) set_distribution(value) TensorLayout(axes, device_mesh=None) diff --git a/.tether/man/keras.dtype_policies.txt b/.tether/man/keras.dtype_policies.txt index 1835f6151c..c21afdca7a 100644 --- a/.tether/man/keras.dtype_policies.txt +++ b/.tether/man/keras.dtype_policies.txt @@ -1,12 +1,13 @@ deserialize(config, custom_objects=None) -DTypePolicy( - name, - *args, - **kwargs -) -FloatDTypePolicy(name) +DTypePolicy(name=None) +DTypePolicyMap(default_policy=None, policy_map=None) +FloatDTypePolicy(name=None) get(identifier) -QuantizedDTypePolicy(name) -QuantizedFloat8DTypePolicy(name, amax_history_length=1024) +QuantizedDTypePolicy(mode, source_name=None) +QuantizedFloat8DTypePolicy( + mode, + source_name=None, + amax_history_length=1024 +) serialize(dtype_policy) diff --git a/.tether/man/keras.layers.txt b/.tether/man/keras.layers.txt index 38bec143e4..1d423974a0 100644 --- a/.tether/man/keras.layers.txt +++ b/.tether/man/keras.layers.txt @@ -718,7 +718,8 @@ Input( sparse=None, batch_shape=None, name=None, - tensor=None + tensor=None, + optional=False ) InputLayer( shape=None, @@ -727,6 +728,7 @@ InputLayer( sparse=None, batch_shape=None, input_tensor=None, + optional=False, name=None, **kwargs ) @@ -738,7 +740,8 @@ InputSpec( min_ndim=None, axes=None, allow_last_axis_squeeze=False, - name=None + name=None, + optional=False ) IntegerLookup( max_tokens=None, @@ -917,6 +920,7 @@ MultiHeadAttention( activity_regularizer=None, kernel_constraint=None, bias_constraint=None, + seed=None, **kwargs ) multiply(inputs, **kwargs) diff --git a/.tether/man/keras.losses.txt b/.tether/man/keras.losses.txt index 0dec4dc482..47ea5c5c2b 100644 --- a/.tether/man/keras.losses.txt +++ b/.tether/man/keras.losses.txt @@ -20,7 +20,8 @@ BinaryCrossentropy( label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', - name='binary_crossentropy' + name='binary_crossentropy', + dtype=None ) BinaryFocalCrossentropy( apply_class_balancing=False, @@ -30,7 +31,8 @@ BinaryFocalCrossentropy( label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', - name='binary_focal_crossentropy' + name='binary_focal_crossentropy', + dtype=None ) categorical_crossentropy( y_true, @@ -54,7 +56,8 @@ CategoricalCrossentropy( label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', - name='categorical_crossentropy' + name='categorical_crossentropy', + dtype=None ) CategoricalFocalCrossentropy( alpha=0.25, @@ -63,9 +66,14 @@ CategoricalFocalCrossentropy( label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', - name='categorical_focal_crossentropy' + name='categorical_focal_crossentropy', + dtype=None +) +CategoricalHinge( + reduction='sum_over_batch_size', + name='categorical_hinge', + dtype=None ) -CategoricalHinge(reduction='sum_over_batch_size', name='categorical_hinge') cosine_similarity( y_true, y_pred, @@ -74,16 +82,34 @@ cosine_similarity( CosineSimilarity( axis=-1, reduction='sum_over_batch_size', - name='cosine_similarity' + name='cosine_similarity', + dtype=None ) ctc(y_true, y_pred) -CTC(reduction='sum_over_batch_size', name='ctc') +CTC( + reduction='sum_over_batch_size', + name='ctc', + dtype=None +) deserialize(name, custom_objects=None) -dice(y_true, y_pred) -Dice(reduction='sum_over_batch_size', name='dice') +dice( + y_true, + y_pred, + axis=None +) +Dice( + reduction='sum_over_batch_size', + name='dice', + axis=None, + dtype=None +) get(identifier) hinge(y_true, y_pred) -Hinge(reduction='sum_over_batch_size', name='hinge') +Hinge( + reduction='sum_over_batch_size', + name='hinge', + dtype=None +) huber( y_true, y_pred, @@ -92,12 +118,21 @@ huber( Huber( delta=1.0, reduction='sum_over_batch_size', - name='huber_loss' + name='huber_loss', + dtype=None ) kl_divergence(y_true, y_pred) -KLDivergence(reduction='sum_over_batch_size', name='kl_divergence') +KLDivergence( + reduction='sum_over_batch_size', + name='kl_divergence', + dtype=None +) log_cosh(y_true, y_pred) -LogCosh(reduction='sum_over_batch_size', name='log_cosh') +LogCosh( + reduction='sum_over_batch_size', + name='log_cosh', + dtype=None +) Loss( name=None, reduction='sum_over_batch_size', @@ -107,12 +142,32 @@ mean_absolute_error(y_true, y_pred) mean_absolute_percentage_error(y_true, y_pred) mean_squared_error(y_true, y_pred) mean_squared_logarithmic_error(y_true, y_pred) -MeanAbsoluteError(reduction='sum_over_batch_size', name='mean_absolute_error') -MeanAbsolutePercentageError(reduction='sum_over_batch_size', name='mean_absolute_percentage_error') -MeanSquaredError(reduction='sum_over_batch_size', name='mean_squared_error') -MeanSquaredLogarithmicError(reduction='sum_over_batch_size', name='mean_squared_logarithmic_error') +MeanAbsoluteError( + reduction='sum_over_batch_size', + name='mean_absolute_error', + dtype=None +) +MeanAbsolutePercentageError( + reduction='sum_over_batch_size', + name='mean_absolute_percentage_error', + dtype=None +) +MeanSquaredError( + reduction='sum_over_batch_size', + name='mean_squared_error', + dtype=None +) +MeanSquaredLogarithmicError( + reduction='sum_over_batch_size', + name='mean_squared_logarithmic_error', + dtype=None +) poisson(y_true, y_pred) -Poisson(reduction='sum_over_batch_size', name='poisson') +Poisson( + reduction='sum_over_batch_size', + name='poisson', + dtype=None +) serialize(loss) sparse_categorical_crossentropy( y_true, @@ -125,10 +180,15 @@ SparseCategoricalCrossentropy( from_logits=False, ignore_class=None, reduction='sum_over_batch_size', - name='sparse_categorical_crossentropy' + name='sparse_categorical_crossentropy', + dtype=None ) squared_hinge(y_true, y_pred) -SquaredHinge(reduction='sum_over_batch_size', name='squared_hinge') +SquaredHinge( + reduction='sum_over_batch_size', + name='squared_hinge', + dtype=None +) tversky( y_true, y_pred, @@ -139,6 +199,7 @@ Tversky( alpha=0.5, beta=0.5, reduction='sum_over_batch_size', - name='tversky' + name='tversky', + dtype=None ) diff --git a/.tether/man/keras.mixed_precision.txt b/.tether/man/keras.mixed_precision.txt index 619396380c..42f34979ff 100644 --- a/.tether/man/keras.mixed_precision.txt +++ b/.tether/man/keras.mixed_precision.txt @@ -1,9 +1,5 @@ dtype_policy() -DTypePolicy( - name, - *args, - **kwargs -) +DTypePolicy(name=None) global_policy() LossScaleOptimizer( inner_optimizer, @@ -11,11 +7,7 @@ LossScaleOptimizer( dynamic_growth_steps=2000, **kwargs ) -Policy( - name, - *args, - **kwargs -) +Policy(name=None) set_dtype_policy(policy) set_global_policy(policy) diff --git a/.tether/man/keras.models.txt b/.tether/man/keras.models.txt index f86bac45bb..5bd9724e9b 100644 --- a/.tether/man/keras.models.txt +++ b/.tether/man/keras.models.txt @@ -18,6 +18,7 @@ save_model( model, filepath, overwrite=True, + zipped=True, **kwargs ) Sequential(*args, **kwargs) diff --git a/.tether/man/keras.ops.image.txt b/.tether/man/keras.ops.image.txt index c729e35397..8a5586add8 100644 --- a/.tether/man/keras.ops.image.txt +++ b/.tether/man/keras.ops.image.txt @@ -1,30 +1,32 @@ affine_transform( - image, + images, transform, interpolation='bilinear', fill_mode='constant', fill_value=0, - data_format='channels_last' + data_format=None ) crop_images( images, top_cropping=None, left_cropping=None, + bottom_cropping=None, + right_cropping=None, target_height=None, target_width=None, - bottom_cropping=None, - right_cropping=None + data_format=None ) extract_patches( - image, + images, size, strides=None, dilation_rate=1, padding='valid', - data_format='channels_last' + data_format=None ) +hsv_to_rgb(images, data_format=None) map_coordinates( - input, + inputs, coordinates, order, fill_mode='constant', @@ -34,13 +36,14 @@ pad_images( images, top_padding=None, left_padding=None, + bottom_padding=None, + right_padding=None, target_height=None, target_width=None, - bottom_padding=None, - right_padding=None + data_format=None ) resize( - image, + images, size, interpolation='bilinear', antialias=False, @@ -48,7 +51,8 @@ resize( pad_to_aspect_ratio=False, fill_mode='constant', fill_value=0.0, - data_format='channels_last' + data_format=None ) -rgb_to_grayscale(image, data_format='channels_last') +rgb_to_grayscale(images, data_format=None) +rgb_to_hsv(images, data_format=None) diff --git a/.tether/man/keras.ops.nn.txt b/.tether/man/keras.ops.nn.txt index 6ac043ce90..6abd0754af 100644 --- a/.tether/man/keras.ops.nn.txt +++ b/.tether/man/keras.ops.nn.txt @@ -49,7 +49,7 @@ ctc_decode( beam_width=100, top_paths=1, merge_repeated=True, - mask_index=None + mask_index=0 ) ctc_loss( target, diff --git a/.tether/man/keras.ops.numpy.txt b/.tether/man/keras.ops.numpy.txt index 57b6f72f38..e0c174bd20 100644 --- a/.tether/man/keras.ops.numpy.txt +++ b/.tether/man/keras.ops.numpy.txt @@ -49,6 +49,11 @@ argmin( axis=None, keepdims=False ) +argpartition( + x, + kth, + axis=-1 +) argsort(x, axis=-1) array(x, dtype=None) average( @@ -146,7 +151,13 @@ greater_equal(x1, x2) hstack(xs) identity(n, dtype=None) imag(x) -isclose(x1, x2) +isclose( + x1, + x2, + rtol=1e-05, + atol=1e-08, + equal_nan=False +) isfinite(x) isinf(x) isnan(x) diff --git a/.tether/man/keras.ops.txt b/.tether/man/keras.ops.txt index e8e7b02873..6c4ad3a8b9 100644 --- a/.tether/man/keras.ops.txt +++ b/.tether/man/keras.ops.txt @@ -49,6 +49,11 @@ argmin( axis=None, keepdims=False ) +argpartition( + x, + kth, + axis=-1 +) argsort(x, axis=-1) array(x, dtype=None) average( @@ -153,7 +158,7 @@ ctc_decode( beam_width=100, top_paths=1, merge_repeated=True, - mask_index=None + mask_index=0 ) ctc_loss( target, @@ -198,6 +203,7 @@ digitize(x, bins) divide(x1, x2) divide_no_nan(x1, x2) dot(x1, x2) +dtype(x) eig(x) eigh(x) einsum(subscripts, *operands) @@ -260,7 +266,13 @@ in_top_k( inv(x) irfft(x, fft_length=None) is_tensor(x) -isclose(x1, x2) +isclose( + x1, + x2, + rtol=1e-05, + atol=1e-08, + equal_nan=False +) isfinite(x) isinf(x) isnan(x) @@ -311,7 +323,13 @@ logsumexp( axis=None, keepdims=False ) +lstsq( + a, + b, + rcond=None +) lu_factor(x) +map(f, xs) matmul(x1, x2) max( x, @@ -444,6 +462,14 @@ roll( ) round(x, decimals=0) rsqrt(x) +scan( + f, + init, + xs=None, + length=None, + reverse=False, + unroll=1 +) scatter( indices, values, @@ -555,6 +581,11 @@ swapaxes( axis2 ) swish(x) +switch( + index, + branches, + *operands +) take( x, indices, diff --git a/.tether/man/keras.regularizers.txt b/.tether/man/keras.regularizers.txt index 30ccc7126e..e96ca2436e 100644 --- a/.tether/man/keras.regularizers.txt +++ b/.tether/man/keras.regularizers.txt @@ -9,5 +9,5 @@ L2(l2=0.01) orthogonal_regularizer(factor=0.01, mode='rows') OrthogonalRegularizer(factor=0.01, mode='rows') Regularizer() -serialize(initializer) +serialize(regularizer) diff --git a/.tether/man/keras.saving.txt b/.tether/man/keras.saving.txt index 389cc60dd5..52b1d7e880 100644 --- a/.tether/man/keras.saving.txt +++ b/.tether/man/keras.saving.txt @@ -30,6 +30,7 @@ save_model( model, filepath, overwrite=True, + zipped=True, **kwargs ) save_weights( diff --git a/.tether/man/keras.txt b/.tether/man/keras.txt index 6c5b01579e..7f45bc21e1 100644 --- a/.tether/man/keras.txt +++ b/.tether/man/keras.txt @@ -8,13 +8,9 @@ datasets: Module(keras.api.datasets) device(device_name) distribution: Module(keras.api.distribution) dtype_policies: Module(keras.api.dtype_policies) -DTypePolicy( - name, - *args, - **kwargs -) +DTypePolicy(name=None) export: Module(keras.api.export) -FloatDTypePolicy(name) +FloatDTypePolicy(name=None) Function( inputs, outputs, @@ -29,7 +25,8 @@ Input( sparse=None, batch_shape=None, name=None, - tensor=None + tensor=None, + optional=False ) InputSpec( dtype=None, @@ -39,7 +36,8 @@ InputSpec( min_ndim=None, axes=None, allow_last_axis_squeeze=False, - name=None + name=None, + optional=False ) KerasTensor( shape, diff --git a/.tether/man/keras_input.txt b/.tether/man/keras_input.txt index c36df56073..b957fd2085 100644 --- a/.tether/man/keras_input.txt +++ b/.tether/man/keras_input.txt @@ -6,7 +6,8 @@ keras.layers.Input( sparse=None, batch_shape=None, name=None, - tensor=None + tensor=None, + optional=False ) __doc__ Used to instantiate a Keras tensor. @@ -40,6 +41,8 @@ Args: tensor: Optional existing tensor to wrap into the `Input` layer. If set, the layer will use this tensor rather than creating a new placeholder tensor. + optional: Boolean, whether the input is optional or not. + An optional input can accept `None` values. Returns: A Keras tensor. diff --git a/.tether/man/keras_model.txt b/.tether/man/keras_model.txt index 0b8ba76790..5a7bcd42ed 100644 --- a/.tether/man/keras_model.txt +++ b/.tether/man/keras_model.txt @@ -11,7 +11,7 @@ class Model(keras.src.backend.tensorflow.trainer.TensorFlowTrainer, keras.src.tr | | You start from `Input`, | you chain layer calls to specify the model's forward pass, - | and finally you create your model from inputs and outputs: + | and finally, you create your model from inputs and outputs: | | ```python | inputs = keras.Input(shape=(37,)) @@ -174,7 +174,7 @@ class Model(keras.src.backend.tensorflow.trainer.TensorFlowTrainer, keras.src.tr | # Create the artifact | model.export("path/to/location") | - | # Later, in a different process / environment... + | # Later, in a different process/environment... | reloaded_artifact = tf.saved_model.load("path/to/location") | predictions = reloaded_artifact.serve(input_data) | ``` @@ -246,19 +246,21 @@ class Model(keras.src.backend.tensorflow.trainer.TensorFlowTrainer, keras.src.tr | self, | filepath, | overwrite=True, + | zipped=True, | **kwargs | ) | Saves a model as a `.keras` file. | | Args: - | filepath: `str` or `pathlib.Path` object. Path where to save - | the model. Must end in `.keras`. + | filepath: `str` or `pathlib.Path` object. + | The path where to save the model. Must end in `.keras` + | (unless saving the model as an unzipped directory + | via `zipped=False`). | overwrite: Whether we should overwrite any existing model at | the target location, or instead ask the user via | an interactive prompt. - | save_format: The `save_format` argument is deprecated in Keras 3. - | Format to use, as a string. Only the `"keras"` format is - | supported at this time. + | zipped: Whether to save the model as a zipped `.keras` + | archive (default), or as an unzipped directory. | | Example: | @@ -330,10 +332,11 @@ class Model(keras.src.backend.tensorflow.trainer.TensorFlowTrainer, keras.src.tr | which is the starting layer name and ending layer name | (both inclusive) indicating the range of layers to be printed | in summary. It also accepts regex patterns instead of exact - | name. In such case, start predicate will be the first element - | it matches to `layer_range[0]` and the end predicate will be - | the last element it matches to `layer_range[1]`. - | By default `None` which considers all layers of model. + | names. In this case, the start predicate will be + | the first element that matches `layer_range[0]` + | and the end predicate will be the last element + | that matches `layer_range[1]`. + | By default `None` considers all layers of the model. | | Raises: | ValueError: if `summary()` is called before the model is built. @@ -355,19 +358,24 @@ class Model(keras.src.backend.tensorflow.trainer.TensorFlowTrainer, keras.src.tr | Class methods defined here: | | from_config(config, custom_objects=None) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | | ---------------------------------------------------------------------- | Static methods defined here: diff --git a/.tether/man/keras_model_sequential.txt b/.tether/man/keras_model_sequential.txt index 05e1aaa4b9..3cd0e140e1 100644 --- a/.tether/man/keras_model_sequential.txt +++ b/.tether/man/keras_model_sequential.txt @@ -90,6 +90,8 @@ class Sequential(keras.src.models.model.Model) | mask=None | ) | + | compute_output_shape(self, input_shape) + | | compute_output_spec( | self, | inputs, @@ -110,19 +112,24 @@ class Sequential(keras.src.models.model.Model) | Class methods defined here: | | from_config(config, custom_objects=None) - | Creates a layer from its config. + | Creates an operation from its config. + | + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | | ---------------------------------------------------------------------- | Static methods defined here: diff --git a/.tether/man/layer_average_pooling_1d.txt b/.tether/man/layer_average_pooling_1d.txt index 04c223258a..bd59e33144 100644 --- a/.tether/man/layer_average_pooling_1d.txt +++ b/.tether/man/layer_average_pooling_1d.txt @@ -30,12 +30,14 @@ class AveragePooling1D(keras.src.layers.pooling.base_pooling.BasePooling) | If you never set it, then it will be `"channels_last"`. | | Input shape: + | | - If `data_format="channels_last"`: | 3D tensor with shape `(batch_size, steps, features)`. | - If `data_format="channels_first"`: | 3D tensor with shape `(batch_size, features, steps)`. | | Output shape: + | | - If `data_format="channels_last"`: | 3D tensor with shape `(batch_size, downsampled_steps, features)`. | - If `data_format="channels_first"`: diff --git a/.tether/man/layer_average_pooling_2d.txt b/.tether/man/layer_average_pooling_2d.txt index 4052aae491..f1cf610f4f 100644 --- a/.tether/man/layer_average_pooling_2d.txt +++ b/.tether/man/layer_average_pooling_2d.txt @@ -39,12 +39,14 @@ class AveragePooling2D(keras.src.layers.pooling.base_pooling.BasePooling) | `"channels_last"`. | | Input shape: + | | - If `data_format="channels_last"`: | 4D tensor with shape `(batch_size, height, width, channels)`. | - If `data_format="channels_first"`: | 4D tensor with shape `(batch_size, channels, height, width)`. | | Output shape: + | | - If `data_format="channels_last"`: | 4D tensor with shape | `(batch_size, pooled_height, pooled_width, channels)`. diff --git a/.tether/man/layer_average_pooling_3d.txt b/.tether/man/layer_average_pooling_3d.txt index 852e7f8b15..ee9b8ae9af 100644 --- a/.tether/man/layer_average_pooling_3d.txt +++ b/.tether/man/layer_average_pooling_3d.txt @@ -32,6 +32,7 @@ class AveragePooling3D(keras.src.layers.pooling.base_pooling.BasePooling) | will be `"channels_last"`. | | Input shape: + | | - If `data_format="channels_last"`: | 5D tensor with shape: | `(batch_size, spatial_dim1, spatial_dim2, spatial_dim3, channels)` @@ -40,6 +41,7 @@ class AveragePooling3D(keras.src.layers.pooling.base_pooling.BasePooling) | `(batch_size, channels, spatial_dim1, spatial_dim2, spatial_dim3)` | | Output shape: + | | - If `data_format="channels_last"`: | 5D tensor with shape: | `(batch_size, pooled_dim1, pooled_dim2, pooled_dim3, channels)` diff --git a/.tether/man/layer_bidirectional.txt b/.tether/man/layer_bidirectional.txt index 147c2f53bc..b86df9b18d 100644 --- a/.tether/man/layer_bidirectional.txt +++ b/.tether/man/layer_bidirectional.txt @@ -136,19 +136,24 @@ class Bidirectional(keras.src.layers.layer.Layer) | Class methods defined here: | | from_config(config, custom_objects=None) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | | ---------------------------------------------------------------------- | Readonly properties defined here: diff --git a/.tether/man/layer_conv_1d.txt b/.tether/man/layer_conv_1d.txt index e1b47c6ae5..6066ba5f8e 100644 --- a/.tether/man/layer_conv_1d.txt +++ b/.tether/man/layer_conv_1d.txt @@ -61,12 +61,14 @@ class Conv1D(keras.src.layers.convolutional.base_conv.BaseConv) | bias after being updated by an `Optimizer`. | | Input shape: + | | - If `data_format="channels_last"`: | A 3D tensor with shape: `(batch_shape, steps, channels)` | - If `data_format="channels_first"`: | A 3D tensor with shape: `(batch_shape, channels, steps)` | | Output shape: + | | - If `data_format="channels_last"`: | A 3D tensor with shape: `(batch_shape, new_steps, filters)` | - If `data_format="channels_first"`: diff --git a/.tether/man/layer_conv_1d_transpose.txt b/.tether/man/layer_conv_1d_transpose.txt index a722a807ef..e6f51c2578 100644 --- a/.tether/man/layer_conv_1d_transpose.txt +++ b/.tether/man/layer_conv_1d_transpose.txt @@ -51,12 +51,14 @@ class Conv1DTranspose(keras.src.layers.convolutional.base_conv_transpose.BaseCon | bias after being updated by an `Optimizer`. | | Input shape: + | | - If `data_format="channels_last"`: | A 3D tensor with shape: `(batch_shape, steps, channels)` | - If `data_format="channels_first"`: | A 3D tensor with shape: `(batch_shape, channels, steps)` | | Output shape: + | | - If `data_format="channels_last"`: | A 3D tensor with shape: `(batch_shape, new_steps, filters)` | - If `data_format="channels_first"`: diff --git a/.tether/man/layer_conv_2d.txt b/.tether/man/layer_conv_2d.txt index 4abc17ff7f..fe2d5459be 100644 --- a/.tether/man/layer_conv_2d.txt +++ b/.tether/man/layer_conv_2d.txt @@ -58,12 +58,14 @@ class Conv2D(keras.src.layers.convolutional.base_conv.BaseConv) | bias after being updated by an `Optimizer`. | | Input shape: + | | - If `data_format="channels_last"`: | A 4D tensor with shape: `(batch_size, height, width, channels)` | - If `data_format="channels_first"`: | A 4D tensor with shape: `(batch_size, channels, height, width)` | | Output shape: + | | - If `data_format="channels_last"`: | A 4D tensor with shape: `(batch_size, new_height, new_width, filters)` | - If `data_format="channels_first"`: diff --git a/.tether/man/layer_conv_2d_transpose.txt b/.tether/man/layer_conv_2d_transpose.txt index 3d7b51cc54..b540c565c3 100644 --- a/.tether/man/layer_conv_2d_transpose.txt +++ b/.tether/man/layer_conv_2d_transpose.txt @@ -53,12 +53,14 @@ class Conv2DTranspose(keras.src.layers.convolutional.base_conv_transpose.BaseCon | bias after being updated by an `Optimizer`. | | Input shape: + | | - If `data_format="channels_last"`: | A 4D tensor with shape: `(batch_size, height, width, channels)` | - If `data_format="channels_first"`: | A 4D tensor with shape: `(batch_size, channels, height, width)` | | Output shape: + | | - If `data_format="channels_last"`: | A 4D tensor with shape: `(batch_size, new_height, new_width, filters)` | - If `data_format="channels_first"`: diff --git a/.tether/man/layer_conv_3d.txt b/.tether/man/layer_conv_3d.txt index c529f3d2ee..bb763e506c 100644 --- a/.tether/man/layer_conv_3d.txt +++ b/.tether/man/layer_conv_3d.txt @@ -58,6 +58,7 @@ class Conv3D(keras.src.layers.convolutional.base_conv.BaseConv) | bias after being updated by an `Optimizer`. | | Input shape: + | | - If `data_format="channels_last"`: | 5D tensor with shape: | `(batch_size, spatial_dim1, spatial_dim2, spatial_dim3, channels)` @@ -66,6 +67,7 @@ class Conv3D(keras.src.layers.convolutional.base_conv.BaseConv) | `(batch_size, channels, spatial_dim1, spatial_dim2, spatial_dim3)` | | Output shape: + | | - If `data_format="channels_last"`: | 5D tensor with shape: | `(batch_size, new_spatial_dim1, new_spatial_dim2, new_spatial_dim3, diff --git a/.tether/man/layer_conv_3d_transpose.txt b/.tether/man/layer_conv_3d_transpose.txt index 27b2700d23..35f84966f0 100644 --- a/.tether/man/layer_conv_3d_transpose.txt +++ b/.tether/man/layer_conv_3d_transpose.txt @@ -53,6 +53,7 @@ class Conv3DTranspose(keras.src.layers.convolutional.base_conv_transpose.BaseCon | bias after being updated by an `Optimizer`. | | Input shape: + | | - If `data_format="channels_last"`: | 5D tensor with shape: | `(batch_size, spatial_dim1, spatial_dim2, spatial_dim3, channels)` @@ -61,6 +62,7 @@ class Conv3DTranspose(keras.src.layers.convolutional.base_conv_transpose.BaseCon | `(batch_size, channels, spatial_dim1, spatial_dim2, spatial_dim3)` | | Output shape: + | | - If `data_format="channels_last"`: | 5D tensor with shape: | `(batch_size, new_spatial_dim1, new_spatial_dim2, new_spatial_dim3, diff --git a/.tether/man/layer_dense.txt b/.tether/man/layer_dense.txt index 90e9eb50f8..c89a7ca89f 100644 --- a/.tether/man/layer_dense.txt +++ b/.tether/man/layer_dense.txt @@ -89,7 +89,11 @@ class Dense(keras.src.layers.layer.Layer) | | build(self, input_shape) | - | call(self, inputs) + | call( + | self, + | inputs, + | training=None + | ) | | compute_output_shape(self, input_shape) | @@ -123,7 +127,11 @@ class Dense(keras.src.layers.layer.Layer) | mode | ) | - | quantized_call(self, inputs) + | quantized_call( + | self, + | inputs, + | training=None + | ) | | save_own_variables(self, store) | Saves the state of the layer. @@ -139,9 +147,4 @@ class Dense(keras.src.layers.layer.Layer) | | kernel | - | ---------------------------------------------------------------------- - | Data and other attributes defined here: - | - | QUANTIZATION_MODE_ERROR_TEMPLATE = "Invalid quantization mode. Expecte... - | diff --git a/.tether/man/layer_depthwise_conv_1d.txt b/.tether/man/layer_depthwise_conv_1d.txt index 520e3e9a12..a68c9e7190 100644 --- a/.tether/man/layer_depthwise_conv_1d.txt +++ b/.tether/man/layer_depthwise_conv_1d.txt @@ -66,12 +66,14 @@ class DepthwiseConv1D(keras.src.layers.convolutional.base_depthwise_conv.BaseDep | bias after being updated by an `Optimizer`. | | Input shape: + | | - If `data_format="channels_last"`: | A 3D tensor with shape: `(batch_shape, steps, channels)` | - If `data_format="channels_first"`: | A 3D tensor with shape: `(batch_shape, channels, steps)` | | Output shape: + | | - If `data_format="channels_last"`: | A 3D tensor with shape: | `(batch_shape, new_steps, channels * depth_multiplier)` diff --git a/.tether/man/layer_depthwise_conv_2d.txt b/.tether/man/layer_depthwise_conv_2d.txt index 79751bfc17..2c3b4223c2 100644 --- a/.tether/man/layer_depthwise_conv_2d.txt +++ b/.tether/man/layer_depthwise_conv_2d.txt @@ -67,12 +67,14 @@ class DepthwiseConv2D(keras.src.layers.convolutional.base_depthwise_conv.BaseDep | bias after being updated by an `Optimizer`. | | Input shape: + | | - If `data_format="channels_last"`: | A 4D tensor with shape: `(batch_size, height, width, channels)` | - If `data_format="channels_first"`: | A 4D tensor with shape: `(batch_size, channels, height, width)` | | Output shape: + | | - If `data_format="channels_last"`: | A 4D tensor with shape: | `(batch_size, new_height, new_width, channels * depth_multiplier)` diff --git a/.tether/man/layer_einsum_dense.txt b/.tether/man/layer_einsum_dense.txt index 81be12db38..afa72681d8 100644 --- a/.tether/man/layer_einsum_dense.txt +++ b/.tether/man/layer_einsum_dense.txt @@ -128,7 +128,11 @@ class EinsumDense(keras.src.layers.layer.Layer) | | build(self, input_shape) | - | call(self, inputs) + | call( + | self, + | inputs, + | training=None + | ) | | compute_output_shape(self, _) | @@ -162,7 +166,11 @@ class EinsumDense(keras.src.layers.layer.Layer) | mode | ) | - | quantized_call(self, inputs) + | quantized_call( + | self, + | inputs, + | training=None + | ) | | save_own_variables(self, store) | Saves the state of the layer. @@ -178,9 +186,4 @@ class EinsumDense(keras.src.layers.layer.Layer) | | kernel | - | ---------------------------------------------------------------------- - | Data and other attributes defined here: - | - | QUANTIZATION_MODE_ERROR_TEMPLATE = "Invalid quantization mode. Expecte... - | diff --git a/.tether/man/layer_embedding.txt b/.tether/man/layer_embedding.txt index a618bb2e41..087ad2ad68 100644 --- a/.tether/man/layer_embedding.txt +++ b/.tether/man/layer_embedding.txt @@ -147,9 +147,4 @@ class Embedding(keras.src.layers.layer.Layer) | | embeddings | - | ---------------------------------------------------------------------- - | Data and other attributes defined here: - | - | QUANTIZATION_MODE_ERROR_TEMPLATE = "Invalid quantization mode. Expecte... - | diff --git a/.tether/man/layer_feature_space.txt b/.tether/man/layer_feature_space.txt index 463df86bdd..69d891d4ad 100644 --- a/.tether/man/layer_feature_space.txt +++ b/.tether/man/layer_feature_space.txt @@ -327,19 +327,24 @@ class FeatureSpace(keras.src.layers.layer.Layer) | ) | | from_config(config) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | | integer_categorical( | max_tokens=None, diff --git a/.tether/man/layer_flax_module_wrapper.txt b/.tether/man/layer_flax_module_wrapper.txt index 6ec29d6a50..ca8967c680 100644 --- a/.tether/man/layer_flax_module_wrapper.txt +++ b/.tether/man/layer_flax_module_wrapper.txt @@ -124,18 +124,23 @@ class FlaxLayer(JaxLayer) | Class methods defined here: | | from_config(config) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | diff --git a/.tether/man/layer_gru.txt b/.tether/man/layer_gru.txt index 80e3a74a5f..d262b9fb73 100644 --- a/.tether/man/layer_gru.txt +++ b/.tether/man/layer_gru.txt @@ -194,19 +194,24 @@ class GRU(keras.src.layers.rnn.rnn.RNN) | Class methods defined here: | | from_config(config) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | | ---------------------------------------------------------------------- | Readonly properties defined here: diff --git a/.tether/man/layer_hashed_crossing.txt b/.tether/man/layer_hashed_crossing.txt index 702b5517a9..9338554933 100644 --- a/.tether/man/layer_hashed_crossing.txt +++ b/.tether/man/layer_hashed_crossing.txt @@ -7,7 +7,7 @@ class HashedCrossing(keras.src.layers.layer.Layer) | | This layer performs crosses of categorical features using the "hashing | trick". Conceptually, the transformation can be thought of as: - | `hash(concatenate(features)) % num_bins. + | `hash(concatenate(features)) % num_bins`. | | This layer currently only performs crosses of scalar inputs and batches of | scalar inputs. Valid input shapes are `(batch_size, 1)`, `(batch_size,)` and diff --git a/.tether/man/layer_jax_model_wrapper.txt b/.tether/man/layer_jax_model_wrapper.txt index 327c385ec3..7704b6e92a 100644 --- a/.tether/man/layer_jax_model_wrapper.txt +++ b/.tether/man/layer_jax_model_wrapper.txt @@ -236,18 +236,23 @@ class JaxLayer(keras.src.layers.layer.Layer) | Class methods defined here: | | from_config(config) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | diff --git a/.tether/man/layer_lambda.txt b/.tether/man/layer_lambda.txt index 0f08e5c850..f41f0398b4 100644 --- a/.tether/man/layer_lambda.txt +++ b/.tether/man/layer_lambda.txt @@ -102,18 +102,23 @@ class Lambda(keras.src.layers.layer.Layer) | custom_objects=None, | safe_mode=None | ) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | diff --git a/.tether/man/layer_lstm.txt b/.tether/man/layer_lstm.txt index 1fe11de058..0dbedc8e6d 100644 --- a/.tether/man/layer_lstm.txt +++ b/.tether/man/layer_lstm.txt @@ -187,19 +187,24 @@ class LSTM(keras.src.layers.rnn.rnn.RNN) | Class methods defined here: | | from_config(config) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | | ---------------------------------------------------------------------- | Readonly properties defined here: diff --git a/.tether/man/layer_max_pooling_1d.txt b/.tether/man/layer_max_pooling_1d.txt index 2900f8fc44..7270e86b1a 100644 --- a/.tether/man/layer_max_pooling_1d.txt +++ b/.tether/man/layer_max_pooling_1d.txt @@ -31,12 +31,14 @@ class MaxPooling1D(keras.src.layers.pooling.base_pooling.BasePooling) | If you never set it, then it will be `"channels_last"`. | | Input shape: + | | - If `data_format="channels_last"`: | 3D tensor with shape `(batch_size, steps, features)`. | - If `data_format="channels_first"`: | 3D tensor with shape `(batch_size, features, steps)`. | | Output shape: + | | - If `data_format="channels_last"`: | 3D tensor with shape `(batch_size, downsampled_steps, features)`. | - If `data_format="channels_first"`: diff --git a/.tether/man/layer_max_pooling_2d.txt b/.tether/man/layer_max_pooling_2d.txt index 6b8288f366..30d1fa8641 100644 --- a/.tether/man/layer_max_pooling_2d.txt +++ b/.tether/man/layer_max_pooling_2d.txt @@ -39,12 +39,14 @@ class MaxPooling2D(keras.src.layers.pooling.base_pooling.BasePooling) | `"channels_last"`. | | Input shape: + | | - If `data_format="channels_last"`: | 4D tensor with shape `(batch_size, height, width, channels)`. | - If `data_format="channels_first"`: | 4D tensor with shape `(batch_size, channels, height, width)`. | | Output shape: + | | - If `data_format="channels_last"`: | 4D tensor with shape | `(batch_size, pooled_height, pooled_width, channels)`. diff --git a/.tether/man/layer_max_pooling_3d.txt b/.tether/man/layer_max_pooling_3d.txt index 2cea68fb3a..0df36ed50c 100644 --- a/.tether/man/layer_max_pooling_3d.txt +++ b/.tether/man/layer_max_pooling_3d.txt @@ -32,6 +32,7 @@ class MaxPooling3D(keras.src.layers.pooling.base_pooling.BasePooling) | will be `"channels_last"`. | | Input shape: + | | - If `data_format="channels_last"`: | 5D tensor with shape: | `(batch_size, spatial_dim1, spatial_dim2, spatial_dim3, channels)` @@ -40,6 +41,7 @@ class MaxPooling3D(keras.src.layers.pooling.base_pooling.BasePooling) | `(batch_size, channels, spatial_dim1, spatial_dim2, spatial_dim3)` | | Output shape: + | | - If `data_format="channels_last"`: | 5D tensor with shape: | `(batch_size, pooled_dim1, pooled_dim2, pooled_dim3, channels)` diff --git a/.tether/man/layer_multi_head_attention.txt b/.tether/man/layer_multi_head_attention.txt index d5486be09b..0cc1495ab5 100644 --- a/.tether/man/layer_multi_head_attention.txt +++ b/.tether/man/layer_multi_head_attention.txt @@ -1,7 +1,7 @@ Help on class MultiHeadAttention in module keras.src.layers.attention.multi_head_attention: class MultiHeadAttention(keras.src.layers.layer.Layer) - | MultiHeadAttention(num_heads, key_dim, value_dim=None, dropout=0.0, use_bias=True, output_shape=None, attention_axes=None, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None, **kwargs) + | MultiHeadAttention(num_heads, key_dim, value_dim=None, dropout=0.0, use_bias=True, output_shape=None, attention_axes=None, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None, seed=None, **kwargs) | | MultiHeadAttention layer. | @@ -44,6 +44,7 @@ class MultiHeadAttention(keras.src.layers.layer.Layer) | activity_regularizer: Regularizer for dense layer activity. | kernel_constraint: Constraint for dense layer kernels. | bias_constraint: Constraint for dense layer kernels. + | seed: Optional integer to seed the dropout layer. | | Call arguments: | query: Query tensor of shape `(B, T, dim)`, where `B` is the batch size, @@ -107,6 +108,7 @@ class MultiHeadAttention(keras.src.layers.layer.Layer) | activity_regularizer=None, | kernel_constraint=None, | bias_constraint=None, + | seed=None, | **kwargs | ) | Initialize self. See help(type(self)) for accurate signature. diff --git a/.tether/man/layer_normalization.txt b/.tether/man/layer_normalization.txt index e6ad30d1cd..8c071a6fa1 100644 --- a/.tether/man/layer_normalization.txt +++ b/.tether/man/layer_normalization.txt @@ -1,6 +1,6 @@ Help on class Normalization in module keras.src.layers.preprocessing.normalization: -class Normalization(keras.src.layers.layer.Layer) +class Normalization(keras.src.layers.preprocessing.tf_data_layer.TFDataLayer) | Normalization(axis=-1, mean=None, variance=None, invert=False, **kwargs) | | A preprocessing layer that normalizes continuous features. @@ -86,6 +86,7 @@ class Normalization(keras.src.layers.layer.Layer) | | Method resolution order: | Normalization + | keras.src.layers.preprocessing.tf_data_layer.TFDataLayer | keras.src.layers.layer.Layer | keras.src.backend.tensorflow.layer.TFLayer | keras.src.backend.tensorflow.trackable.KerasAutoTrackable diff --git a/.tether/man/layer_permute.txt b/.tether/man/layer_permute.txt index b2d4529d39..f43ba15206 100644 --- a/.tether/man/layer_permute.txt +++ b/.tether/man/layer_permute.txt @@ -10,7 +10,7 @@ class Permute(keras.src.layers.layer.Layer) | Args: | dims: Tuple of integers. Permutation pattern does not include the | batch dimension. Indexing starts at 1. - | For instance, `(2, 1)` permutes the first and second dimensions + | For instance, `(1, 3, 2)` permutes the second and third dimensions | of the input. | | Input shape: diff --git a/.tether/man/layer_rnn.txt b/.tether/man/layer_rnn.txt index 4b271ef638..436126737d 100644 --- a/.tether/man/layer_rnn.txt +++ b/.tether/man/layer_rnn.txt @@ -243,18 +243,23 @@ class RNN(keras.src.layers.layer.Layer) | Class methods defined here: | | from_config(config, custom_objects=None) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | diff --git a/.tether/man/layer_separable_conv_1d.txt b/.tether/man/layer_separable_conv_1d.txt index 8873f07b89..46f1ad4602 100644 --- a/.tether/man/layer_separable_conv_1d.txt +++ b/.tether/man/layer_separable_conv_1d.txt @@ -64,12 +64,14 @@ class SeparableConv1D(keras.src.layers.convolutional.base_separable_conv.BaseSep | bias after being updated by an `Optimizer`. | | Input shape: + | | - If `data_format="channels_last"`: | A 3D tensor with shape: `(batch_shape, steps, channels)` | - If `data_format="channels_first"`: | A 3D tensor with shape: `(batch_shape, channels, steps)` | | Output shape: + | | - If `data_format="channels_last"`: | A 3D tensor with shape: `(batch_shape, new_steps, filters)` | - If `data_format="channels_first"`: diff --git a/.tether/man/layer_separable_conv_2d.txt b/.tether/man/layer_separable_conv_2d.txt index 13a2ad9841..ad4617bbe9 100644 --- a/.tether/man/layer_separable_conv_2d.txt +++ b/.tether/man/layer_separable_conv_2d.txt @@ -65,12 +65,14 @@ class SeparableConv2D(keras.src.layers.convolutional.base_separable_conv.BaseSep | bias after being updated by an `Optimizer`. | | Input shape: + | | - If `data_format="channels_last"`: | A 4D tensor with shape: `(batch_size, height, width, channels)` | - If `data_format="channels_first"`: | A 4D tensor with shape: `(batch_size, channels, height, width)` | | Output shape: + | | - If `data_format="channels_last"`: | A 4D tensor with shape: `(batch_size, new_height, new_width, filters)` | - If `data_format="channels_first"`: diff --git a/.tether/man/layer_simple_rnn.txt b/.tether/man/layer_simple_rnn.txt index 79f495d471..36481cf6bd 100644 --- a/.tether/man/layer_simple_rnn.txt +++ b/.tether/man/layer_simple_rnn.txt @@ -144,19 +144,24 @@ class SimpleRNN(keras.src.layers.rnn.rnn.RNN) | Class methods defined here: | | from_config(config) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | | ---------------------------------------------------------------------- | Readonly properties defined here: diff --git a/.tether/man/layer_string_lookup.txt b/.tether/man/layer_string_lookup.txt index 8dc1c33ec1..8ec231384f 100644 --- a/.tether/man/layer_string_lookup.txt +++ b/.tether/man/layer_string_lookup.txt @@ -186,7 +186,7 @@ class StringLookup(keras.src.layers.preprocessing.index_lookup.IndexLookup) | [0., 0., 1., 0., 0.], | [0., 0., 0., 1., 0.], | [0., 0., 0., 0., 1.], - | [1., 0., 0., 0., 0.]], dtype=float32) + | [1., 0., 0., 0., 0.]], dtype=int64) | | **Multi-hot output** | @@ -198,7 +198,7 @@ class StringLookup(keras.src.layers.preprocessing.index_lookup.IndexLookup) | >>> layer = StringLookup(vocabulary=vocab, output_mode='multi_hot') | >>> layer(data) | array([[0., 1., 0., 1., 1.], - | [1., 0., 1., 0., 1.]], dtype=float32) + | [1., 0., 1., 0., 1.]], dtype=int64) | | **Token count output** | @@ -210,7 +210,7 @@ class StringLookup(keras.src.layers.preprocessing.index_lookup.IndexLookup) | >>> layer = StringLookup(vocabulary=vocab, output_mode='count') | >>> layer(data) | array([[0., 1., 0., 1., 2.], - | [2., 0., 1., 0., 1.]], dtype=float32) + | [2., 0., 1., 0., 1.]], dtype=int64) | | **TF-IDF output** | diff --git a/.tether/man/layer_text_vectorization.txt b/.tether/man/layer_text_vectorization.txt index 27c05d249c..4eafa6d533 100644 --- a/.tether/man/layer_text_vectorization.txt +++ b/.tether/man/layer_text_vectorization.txt @@ -344,19 +344,24 @@ class TextVectorization(keras.src.layers.layer.Layer) | Class methods defined here: | | from_config(config) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | | ---------------------------------------------------------------------- | Readonly properties defined here: diff --git a/.tether/man/layer_torch_module_wrapper.txt b/.tether/man/layer_torch_module_wrapper.txt index 525e137316..1f837a6f90 100644 --- a/.tether/man/layer_torch_module_wrapper.txt +++ b/.tether/man/layer_torch_module_wrapper.txt @@ -9,6 +9,9 @@ class TorchModuleWrapper(keras.src.layers.layer.Layer) | `torch.nn.Module` into a Keras layer, in particular by making its | parameters trackable by Keras. | + | `TorchModuleWrapper` is only compatible with the PyTorch backend and + | cannot be used with the TensorFlow or JAX backends. + | | Args: | module: `torch.nn.Module` instance. If it's a `LazyModule` | instance, then its parameters must be initialized before @@ -115,18 +118,23 @@ class TorchModuleWrapper(keras.src.layers.layer.Layer) | Class methods defined here: | | from_config(config) - | Creates a layer from its config. + | Creates an operation from its config. + | + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | diff --git a/.tether/man/layer_unit_normalization.txt b/.tether/man/layer_unit_normalization.txt index 7f9a25d953..26d4363022 100644 --- a/.tether/man/layer_unit_normalization.txt +++ b/.tether/man/layer_unit_normalization.txt @@ -12,7 +12,7 @@ class UnitNormalization(keras.src.layers.layer.Layer) | | >>> data = np.arange(6).reshape(2, 3) | >>> normalized_data = keras.layers.UnitNormalization()(data) - | >>> print(np.sum(normalized_data[0, :] ** 2) + | >>> np.sum(normalized_data[0, :] ** 2) | 1.0 | | Args: diff --git a/.tether/man/layer_upsampling_1d.txt b/.tether/man/layer_upsampling_1d.txt index 334731eb6e..82886bb68d 100644 --- a/.tether/man/layer_upsampling_1d.txt +++ b/.tether/man/layer_upsampling_1d.txt @@ -22,7 +22,6 @@ class UpSampling1D(keras.src.layers.layer.Layer) | [ 0. 1. 2.] | [ 3. 4. 5.] | [ 3. 4. 5.]] - | | [[ 6. 7. 8.] | [ 6. 7. 8.] | [ 9. 10. 11.] diff --git a/.tether/man/loss_binary_crossentropy.txt b/.tether/man/loss_binary_crossentropy.txt index 2ebf4dc16b..2aefc44505 100644 --- a/.tether/man/loss_binary_crossentropy.txt +++ b/.tether/man/loss_binary_crossentropy.txt @@ -1,7 +1,7 @@ Help on class BinaryCrossentropy in module keras.src.losses.losses: class BinaryCrossentropy(LossFunctionWrapper) - | BinaryCrossentropy(from_logits=False, label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', name='binary_crossentropy') + | BinaryCrossentropy(from_logits=False, label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', name='binary_crossentropy', dtype=None) | | Computes the cross-entropy loss between true labels and predicted labels. | @@ -30,6 +30,10 @@ class BinaryCrossentropy(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Examples: | @@ -97,7 +101,8 @@ class BinaryCrossentropy(LossFunctionWrapper) | label_smoothing=0.0, | axis=-1, | reduction='sum_over_batch_size', - | name='binary_crossentropy' + | name='binary_crossentropy', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_binary_focal_crossentropy.txt b/.tether/man/loss_binary_focal_crossentropy.txt index 75f1365965..cec70078a9 100644 --- a/.tether/man/loss_binary_focal_crossentropy.txt +++ b/.tether/man/loss_binary_focal_crossentropy.txt @@ -1,7 +1,7 @@ Help on class BinaryFocalCrossentropy in module keras.src.losses.losses: class BinaryFocalCrossentropy(LossFunctionWrapper) - | BinaryFocalCrossentropy(apply_class_balancing=False, alpha=0.25, gamma=2.0, from_logits=False, label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', name='binary_focal_crossentropy') + | BinaryFocalCrossentropy(apply_class_balancing=False, alpha=0.25, gamma=2.0, from_logits=False, label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', name='binary_focal_crossentropy', dtype=None) | | Computes focal cross-entropy loss between true labels and predictions. | @@ -48,6 +48,10 @@ class BinaryFocalCrossentropy(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Examples: | @@ -150,7 +154,8 @@ class BinaryFocalCrossentropy(LossFunctionWrapper) | label_smoothing=0.0, | axis=-1, | reduction='sum_over_batch_size', - | name='binary_focal_crossentropy' + | name='binary_focal_crossentropy', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_categorical_crossentropy.txt b/.tether/man/loss_categorical_crossentropy.txt index 9c8c38160e..b055df7a64 100644 --- a/.tether/man/loss_categorical_crossentropy.txt +++ b/.tether/man/loss_categorical_crossentropy.txt @@ -1,7 +1,7 @@ Help on class CategoricalCrossentropy in module keras.src.losses.losses: class CategoricalCrossentropy(LossFunctionWrapper) - | CategoricalCrossentropy(from_logits=False, label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', name='categorical_crossentropy') + | CategoricalCrossentropy(from_logits=False, label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', name='categorical_crossentropy', dtype=None) | | Computes the crossentropy loss between the labels and predictions. | @@ -25,6 +25,10 @@ class CategoricalCrossentropy(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Examples: | @@ -75,7 +79,8 @@ class CategoricalCrossentropy(LossFunctionWrapper) | label_smoothing=0.0, | axis=-1, | reduction='sum_over_batch_size', - | name='categorical_crossentropy' + | name='categorical_crossentropy', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_categorical_focal_crossentropy.txt b/.tether/man/loss_categorical_focal_crossentropy.txt index b99862a932..5c11e40f8c 100644 --- a/.tether/man/loss_categorical_focal_crossentropy.txt +++ b/.tether/man/loss_categorical_focal_crossentropy.txt @@ -1,7 +1,7 @@ Help on class CategoricalFocalCrossentropy in module keras.src.losses.losses: class CategoricalFocalCrossentropy(LossFunctionWrapper) - | CategoricalFocalCrossentropy(alpha=0.25, gamma=2.0, from_logits=False, label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', name='categorical_focal_crossentropy') + | CategoricalFocalCrossentropy(alpha=0.25, gamma=2.0, from_logits=False, label_smoothing=0.0, axis=-1, reduction='sum_over_batch_size', name='categorical_focal_crossentropy', dtype=None) | | Computes the alpha balanced focal crossentropy loss. | @@ -66,6 +66,10 @@ class CategoricalFocalCrossentropy(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Examples: | @@ -118,7 +122,8 @@ class CategoricalFocalCrossentropy(LossFunctionWrapper) | label_smoothing=0.0, | axis=-1, | reduction='sum_over_batch_size', - | name='categorical_focal_crossentropy' + | name='categorical_focal_crossentropy', + | dtype=None | ) | Initializes `CategoricalFocalCrossentropy` instance. | diff --git a/.tether/man/loss_categorical_hinge.txt b/.tether/man/loss_categorical_hinge.txt index a29742bf8c..43c8c07893 100644 --- a/.tether/man/loss_categorical_hinge.txt +++ b/.tether/man/loss_categorical_hinge.txt @@ -1,7 +1,7 @@ Help on class CategoricalHinge in module keras.src.losses.losses: class CategoricalHinge(LossFunctionWrapper) - | CategoricalHinge(reduction='sum_over_batch_size', name='categorical_hinge') + | CategoricalHinge(reduction='sum_over_batch_size', name='categorical_hinge', dtype=None) | | Computes the categorical hinge loss between `y_true` & `y_pred`. | @@ -18,6 +18,10 @@ class CategoricalHinge(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | CategoricalHinge @@ -31,7 +35,8 @@ class CategoricalHinge(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='categorical_hinge' + | name='categorical_hinge', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_cosine_similarity.txt b/.tether/man/loss_cosine_similarity.txt index ae53c3c866..3bfd68b623 100644 --- a/.tether/man/loss_cosine_similarity.txt +++ b/.tether/man/loss_cosine_similarity.txt @@ -1,7 +1,7 @@ Help on class CosineSimilarity in module keras.src.losses.losses: class CosineSimilarity(LossFunctionWrapper) - | CosineSimilarity(axis=-1, reduction='sum_over_batch_size', name='cosine_similarity') + | CosineSimilarity(axis=-1, reduction='sum_over_batch_size', name='cosine_similarity', dtype=None) | | Computes the cosine similarity between `y_true` & `y_pred`. | @@ -25,6 +25,10 @@ class CosineSimilarity(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | CosineSimilarity @@ -39,7 +43,8 @@ class CosineSimilarity(LossFunctionWrapper) | self, | axis=-1, | reduction='sum_over_batch_size', - | name='cosine_similarity' + | name='cosine_similarity', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_ctc.txt b/.tether/man/loss_ctc.txt index 14ba24c070..e73360d098 100644 --- a/.tether/man/loss_ctc.txt +++ b/.tether/man/loss_ctc.txt @@ -1,17 +1,19 @@ Help on class CTC in module keras.src.losses.losses: class CTC(LossFunctionWrapper) - | CTC(reduction='sum_over_batch_size', name='ctc') + | CTC(reduction='sum_over_batch_size', name='ctc', dtype=None) | | CTC (Connectionist Temporal Classification) loss. | | Args: - | y_true: A tensor of shape `(batch_size, target_max_length)` containing - | the true labels in integer format. `0` always represents - | the blank/mask index and should not be used for classes. - | y_pred: A tensor of shape `(batch_size, output_max_length, num_classes)` - | containing logits (the output of your model). - | They should *not* be normalized via softmax. + | reduction: Type of reduction to apply to the loss. In almost all cases + | this should be `"sum_over_batch_size"`. + | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. + | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | CTC @@ -25,7 +27,8 @@ class CTC(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='ctc' + | name='ctc', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_dice.txt b/.tether/man/loss_dice.txt index f446a0b18a..5fd198d5db 100644 --- a/.tether/man/loss_dice.txt +++ b/.tether/man/loss_dice.txt @@ -1,7 +1,7 @@ Help on class Dice in module keras.src.losses.losses: class Dice(LossFunctionWrapper) - | Dice(reduction='sum_over_batch_size', name='dice') + | Dice(reduction='sum_over_batch_size', name='dice', axis=None, dtype=None) | | Computes the Dice loss value between `y_true` and `y_pred`. | @@ -11,12 +11,44 @@ class Dice(LossFunctionWrapper) | ``` | | Args: - | y_true: tensor of true targets. - | y_pred: tensor of predicted targets. + | reduction: Type of reduction to apply to the loss. In almost all cases + | this should be `"sum_over_batch_size"`. + | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. + | name: Optional name for the loss instance. + | axis: Tuple for which dimensions the loss is calculated. Defaults to + | `None`. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Returns: | Dice loss value. | + | Example: + | + | >>> y_true = [[[[1.0], [1.0]], [[0.0], [0.0]]], + | ... [[[1.0], [1.0]], [[0.0], [0.0]]]] + | >>> y_pred = [[[[0.0], [1.0]], [[0.0], [1.0]]], + | ... [[[0.4], [0.0]], [[0.0], [0.9]]]] + | >>> axis = (1, 2, 3) + | >>> loss = keras.losses.dice(y_true, y_pred, axis=axis) + | >>> assert loss.shape == (2,) + | >>> loss + | array([0.5, 0.75757575], shape=(2,), dtype=float32) + | + | >>> loss = keras.losses.dice(y_true, y_pred) + | >>> assert loss.shape == () + | >>> loss + | array(0.6164384, shape=(), dtype=float32) + | + | >>> y_true = np.array(y_true) + | >>> y_pred = np.array(y_pred) + | >>> loss = keras.losses.Dice(axis=axis, reduction=None)(y_true, y_pred) + | >>> assert loss.shape == (2,) + | >>> loss + | array([0.5, 0.75757575], shape=(2,), dtype=float32) + | | Method resolution order: | Dice | LossFunctionWrapper @@ -29,7 +61,9 @@ class Dice(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='dice' + | name='dice', + | axis=None, + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_hinge.txt b/.tether/man/loss_hinge.txt index 07a03a7a47..dc1694323d 100644 --- a/.tether/man/loss_hinge.txt +++ b/.tether/man/loss_hinge.txt @@ -1,7 +1,7 @@ Help on class Hinge in module keras.src.losses.losses: class Hinge(LossFunctionWrapper) - | Hinge(reduction='sum_over_batch_size', name='hinge') + | Hinge(reduction='sum_over_batch_size', name='hinge', dtype=None) | | Computes the hinge loss between `y_true` & `y_pred`. | @@ -19,6 +19,10 @@ class Hinge(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | Hinge @@ -32,7 +36,8 @@ class Hinge(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='hinge' + | name='hinge', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_huber.txt b/.tether/man/loss_huber.txt index 190cc84d1b..ff5b3bff09 100644 --- a/.tether/man/loss_huber.txt +++ b/.tether/man/loss_huber.txt @@ -1,7 +1,7 @@ Help on class Huber in module keras.src.losses.losses: class Huber(LossFunctionWrapper) - | Huber(delta=1.0, reduction='sum_over_batch_size', name='huber_loss') + | Huber(delta=1.0, reduction='sum_over_batch_size', name='huber_loss', dtype=None) | | Computes the Huber loss between `y_true` & `y_pred`. | @@ -25,6 +25,10 @@ class Huber(LossFunctionWrapper) | `"sum_over_batch_size"` or `None`. Defaults to | `"sum_over_batch_size"`. | name: Optional name for the instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | Huber @@ -39,7 +43,8 @@ class Huber(LossFunctionWrapper) | self, | delta=1.0, | reduction='sum_over_batch_size', - | name='huber_loss' + | name='huber_loss', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_kl_divergence.txt b/.tether/man/loss_kl_divergence.txt index a7eed9b001..44a7dd6876 100644 --- a/.tether/man/loss_kl_divergence.txt +++ b/.tether/man/loss_kl_divergence.txt @@ -1,7 +1,7 @@ Help on class KLDivergence in module keras.src.losses.losses: class KLDivergence(LossFunctionWrapper) - | KLDivergence(reduction='sum_over_batch_size', name='kl_divergence') + | KLDivergence(reduction='sum_over_batch_size', name='kl_divergence', dtype=None) | | Computes Kullback-Leibler divergence loss between `y_true` & `y_pred`. | @@ -20,6 +20,10 @@ class KLDivergence(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | KLDivergence @@ -33,7 +37,8 @@ class KLDivergence(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='kl_divergence' + | name='kl_divergence', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_log_cosh.txt b/.tether/man/loss_log_cosh.txt index 0575afcae8..cf0aafee7f 100644 --- a/.tether/man/loss_log_cosh.txt +++ b/.tether/man/loss_log_cosh.txt @@ -1,7 +1,7 @@ Help on class LogCosh in module keras.src.losses.losses: class LogCosh(LossFunctionWrapper) - | LogCosh(reduction='sum_over_batch_size', name='log_cosh') + | LogCosh(reduction='sum_over_batch_size', name='log_cosh', dtype=None) | | Computes the logarithm of the hyperbolic cosine of the prediction error. | @@ -18,6 +18,10 @@ class LogCosh(LossFunctionWrapper) | `"sum_over_batch_size"` or `None`. Defaults to | `"sum_over_batch_size"`. | name: Optional name for the instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | LogCosh @@ -31,7 +35,8 @@ class LogCosh(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='log_cosh' + | name='log_cosh', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_mean_absolute_error.txt b/.tether/man/loss_mean_absolute_error.txt index cb7e98cd58..27e803d15c 100644 --- a/.tether/man/loss_mean_absolute_error.txt +++ b/.tether/man/loss_mean_absolute_error.txt @@ -1,7 +1,7 @@ Help on class MeanAbsoluteError in module keras.src.losses.losses: class MeanAbsoluteError(LossFunctionWrapper) - | MeanAbsoluteError(reduction='sum_over_batch_size', name='mean_absolute_error') + | MeanAbsoluteError(reduction='sum_over_batch_size', name='mean_absolute_error', dtype=None) | | Computes the mean of absolute difference between labels and predictions. | @@ -16,6 +16,10 @@ class MeanAbsoluteError(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | MeanAbsoluteError @@ -29,7 +33,8 @@ class MeanAbsoluteError(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='mean_absolute_error' + | name='mean_absolute_error', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_mean_absolute_percentage_error.txt b/.tether/man/loss_mean_absolute_percentage_error.txt index e023e9c226..f4cb015dd0 100644 --- a/.tether/man/loss_mean_absolute_percentage_error.txt +++ b/.tether/man/loss_mean_absolute_percentage_error.txt @@ -1,7 +1,7 @@ Help on class MeanAbsolutePercentageError in module keras.src.losses.losses: class MeanAbsolutePercentageError(LossFunctionWrapper) - | MeanAbsolutePercentageError(reduction='sum_over_batch_size', name='mean_absolute_percentage_error') + | MeanAbsolutePercentageError(reduction='sum_over_batch_size', name='mean_absolute_percentage_error', dtype=None) | | Computes the mean absolute percentage error between `y_true` & `y_pred`. | @@ -16,6 +16,10 @@ class MeanAbsolutePercentageError(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | MeanAbsolutePercentageError @@ -29,7 +33,8 @@ class MeanAbsolutePercentageError(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='mean_absolute_percentage_error' + | name='mean_absolute_percentage_error', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_mean_squared_error.txt b/.tether/man/loss_mean_squared_error.txt index e8fcc18d28..a801b002af 100644 --- a/.tether/man/loss_mean_squared_error.txt +++ b/.tether/man/loss_mean_squared_error.txt @@ -1,7 +1,7 @@ Help on class MeanSquaredError in module keras.src.losses.losses: class MeanSquaredError(LossFunctionWrapper) - | MeanSquaredError(reduction='sum_over_batch_size', name='mean_squared_error') + | MeanSquaredError(reduction='sum_over_batch_size', name='mean_squared_error', dtype=None) | | Computes the mean of squares of errors between labels and predictions. | @@ -16,6 +16,10 @@ class MeanSquaredError(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | MeanSquaredError @@ -29,7 +33,8 @@ class MeanSquaredError(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='mean_squared_error' + | name='mean_squared_error', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_mean_squared_logarithmic_error.txt b/.tether/man/loss_mean_squared_logarithmic_error.txt index 2224e64ca1..2abfd8aa7c 100644 --- a/.tether/man/loss_mean_squared_logarithmic_error.txt +++ b/.tether/man/loss_mean_squared_logarithmic_error.txt @@ -1,7 +1,7 @@ Help on class MeanSquaredLogarithmicError in module keras.src.losses.losses: class MeanSquaredLogarithmicError(LossFunctionWrapper) - | MeanSquaredLogarithmicError(reduction='sum_over_batch_size', name='mean_squared_logarithmic_error') + | MeanSquaredLogarithmicError(reduction='sum_over_batch_size', name='mean_squared_logarithmic_error', dtype=None) | | Computes the mean squared logarithmic error between `y_true` & `y_pred`. | @@ -16,6 +16,10 @@ class MeanSquaredLogarithmicError(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | MeanSquaredLogarithmicError @@ -29,7 +33,8 @@ class MeanSquaredLogarithmicError(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='mean_squared_logarithmic_error' + | name='mean_squared_logarithmic_error', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_poisson.txt b/.tether/man/loss_poisson.txt index 95d6f58cbc..3b3c1c05ff 100644 --- a/.tether/man/loss_poisson.txt +++ b/.tether/man/loss_poisson.txt @@ -1,7 +1,7 @@ Help on class Poisson in module keras.src.losses.losses: class Poisson(LossFunctionWrapper) - | Poisson(reduction='sum_over_batch_size', name='poisson') + | Poisson(reduction='sum_over_batch_size', name='poisson', dtype=None) | | Computes the Poisson loss between `y_true` & `y_pred`. | @@ -16,6 +16,10 @@ class Poisson(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | Poisson @@ -29,7 +33,8 @@ class Poisson(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='poisson' + | name='poisson', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_sparse_categorical_crossentropy.txt b/.tether/man/loss_sparse_categorical_crossentropy.txt index 9e706ed7b9..c1c36c27d6 100644 --- a/.tether/man/loss_sparse_categorical_crossentropy.txt +++ b/.tether/man/loss_sparse_categorical_crossentropy.txt @@ -1,7 +1,7 @@ Help on class SparseCategoricalCrossentropy in module keras.src.losses.losses: class SparseCategoricalCrossentropy(LossFunctionWrapper) - | SparseCategoricalCrossentropy(from_logits=False, ignore_class=None, reduction='sum_over_batch_size', name='sparse_categorical_crossentropy') + | SparseCategoricalCrossentropy(from_logits=False, ignore_class=None, reduction='sum_over_batch_size', name='sparse_categorical_crossentropy', dtype=None) | | Computes the crossentropy loss between the labels and predictions. | @@ -24,6 +24,10 @@ class SparseCategoricalCrossentropy(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Examples: | @@ -71,7 +75,8 @@ class SparseCategoricalCrossentropy(LossFunctionWrapper) | from_logits=False, | ignore_class=None, | reduction='sum_over_batch_size', - | name='sparse_categorical_crossentropy' + | name='sparse_categorical_crossentropy', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_squared_hinge.txt b/.tether/man/loss_squared_hinge.txt index 433cd13a16..19da83106e 100644 --- a/.tether/man/loss_squared_hinge.txt +++ b/.tether/man/loss_squared_hinge.txt @@ -1,7 +1,7 @@ Help on class SquaredHinge in module keras.src.losses.losses: class SquaredHinge(LossFunctionWrapper) - | SquaredHinge(reduction='sum_over_batch_size', name='squared_hinge') + | SquaredHinge(reduction='sum_over_batch_size', name='squared_hinge', dtype=None) | | Computes the squared hinge loss between `y_true` & `y_pred`. | @@ -19,6 +19,10 @@ class SquaredHinge(LossFunctionWrapper) | this should be `"sum_over_batch_size"`. | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Method resolution order: | SquaredHinge @@ -32,7 +36,8 @@ class SquaredHinge(LossFunctionWrapper) | __init__( | self, | reduction='sum_over_batch_size', - | name='squared_hinge' + | name='squared_hinge', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/loss_tversky.txt b/.tether/man/loss_tversky.txt index 37e18b6ad8..6c27f9a329 100644 --- a/.tether/man/loss_tversky.txt +++ b/.tether/man/loss_tversky.txt @@ -1,7 +1,7 @@ Help on class Tversky in module keras.src.losses.losses: class Tversky(LossFunctionWrapper) - | Tversky(alpha=0.5, beta=0.5, reduction='sum_over_batch_size', name='tversky') + | Tversky(alpha=0.5, beta=0.5, reduction='sum_over_batch_size', name='tversky', dtype=None) | | Computes the Tversky loss value between `y_true` and `y_pred`. | @@ -12,10 +12,18 @@ class Tversky(LossFunctionWrapper) | Dice Loss. | | Args: - | y_true: tensor of true targets. - | y_pred: tensor of predicted targets. - | alpha: coefficient controlling incidence of false positives. - | beta: coefficient controlling incidence of false negatives. + | alpha: The coefficient controlling incidence of false positives. + | Defaults to `0.5`. + | beta: The coefficient controlling incidence of false negatives. + | Defaults to `0.5`. + | reduction: Type of reduction to apply to the loss. In almost all cases + | this should be `"sum_over_batch_size"`. + | Supported options are `"sum"`, `"sum_over_batch_size"` or `None`. + | name: Optional name for the loss instance. + | dtype: The dtype of the loss's computations. Defaults to `None`, which + | means using `keras.backend.floatx()`. `keras.backend.floatx()` is a + | `"float32"` unless set to different value + | (via `keras.backend.set_floatx()`). | | Returns: | Tversky loss value. @@ -38,7 +46,8 @@ class Tversky(LossFunctionWrapper) | alpha=0.5, | beta=0.5, | reduction='sum_over_batch_size', - | name='tversky' + | name='tversky', + | dtype=None | ) | Initialize self. See help(type(self)) for accurate signature. | diff --git a/.tether/man/op_argpartition.txt b/.tether/man/op_argpartition.txt new file mode 100644 index 0000000000..2f682d698a --- /dev/null +++ b/.tether/man/op_argpartition.txt @@ -0,0 +1,27 @@ +__signature__ +keras.ops.argpartition( + x, + kth, + axis=-1 +) +__doc__ +Performs an indirect partition along the given axis. + +It returns an array +of indices of the same shape as `x` that index data along the given axis +in partitioned order. + +Args: + a: Array to sort. + kth: Element index to partition by. + The k-th element will be in its final sorted position and all + smaller elements will be moved before it and all larger elements + behind it. The order of all elements in the partitions is undefined. + If provided with a sequence of k-th it will partition all of them + into their sorted position at once. + axis: Axis along which to sort. The default is -1 (the last axis). + If `None`, the flattened array is used. + +Returns: + Array of indices that partition `x` along the specified `axis`. + diff --git a/.tether/man/op_ctc_decode.txt b/.tether/man/op_ctc_decode.txt index c9fa29c063..892a923462 100644 --- a/.tether/man/op_ctc_decode.txt +++ b/.tether/man/op_ctc_decode.txt @@ -6,7 +6,7 @@ keras.ops.ctc_decode( beam_width=100, top_paths=1, merge_repeated=True, - mask_index=None + mask_index=0 ) __doc__ Decodes the output of a CTC model. @@ -26,7 +26,7 @@ Args: merge_repeated: A boolean scalar, whether to merge repeated labels in the output. Defaults to `True`. mask_index: An integer scalar, the index of the mask character in - the vocabulary. Defaults to `None`. + the vocabulary. Defaults to `0`. Returns: A tuple containing: diff --git a/.tether/man/op_dtype.txt b/.tether/man/op_dtype.txt new file mode 100644 index 0000000000..171a74522e --- /dev/null +++ b/.tether/man/op_dtype.txt @@ -0,0 +1,21 @@ +__signature__ +keras.ops.dtype(x) +__doc__ +Return the dtype of the tensor input as a standardized string. + +Note that due to the standardization, the dtype will not compare equal +to the backend-specific version of the dtype. + +Args: + x: A tensor. This function will try to access the `dtype` attribute of + the input tensor. + +Returns: + A string indicating the dtype of the input tensor, e.g. `"float32"`. + +Example: + +>>> x = keras.ops.zeros((8, 12)) +>>> keras.ops.dtype(x) +'float32' + diff --git a/.tether/man/op_image_affine_transform.txt b/.tether/man/op_image_affine_transform.txt index 036b72b4d1..cbed0f2266 100644 --- a/.tether/man/op_image_affine_transform.txt +++ b/.tether/man/op_image_affine_transform.txt @@ -1,17 +1,17 @@ __signature__ keras.ops.image.affine_transform( - image, + images, transform, interpolation='bilinear', fill_mode='constant', fill_value=0, - data_format='channels_last' + data_format=None ) __doc__ Applies the given transform(s) to the image(s). Args: - image: Input image or batch of images. Must be 3D or 4D. + images: Input image or batch of images. Must be 3D or 4D. transform: Projective transform matrix/matrices. A vector of length 8 or tensor of size N x 8. If one row of transform is `[a0, a1, a2, b0, b1, b2, c0, c1]`, then it maps the output point @@ -40,14 +40,13 @@ Args: The input is extended by the nearest pixel. fill_value: Value used for points outside the boundaries of the input if `fill_mode="constant"`. Defaults to `0`. - data_format: string, either `"channels_last"` or `"channels_first"`. - The ordering of the dimensions in the inputs. `"channels_last"` - corresponds to inputs with shape `(batch, height, width, channels)` - while `"channels_first"` corresponds to inputs with shape - `(batch, channels, height, weight)`. It defaults to the - `image_data_format` value found in your Keras config file at - `~/.keras/keras.json`. If you never set it, then it will be - `"channels_last"`. + data_format: A string specifying the data format of the input tensor. + It can be either `"channels_last"` or `"channels_first"`. + `"channels_last"` corresponds to inputs with shape + `(batch, height, width, channels)`, while `"channels_first"` + corresponds to inputs with shape `(batch, channels, height, width)`. + If not specified, the value will default to + `keras.config.image_data_format`. Returns: Applied affine transform image or batch of images. @@ -82,3 +81,4 @@ Examples: ... data_format="channels_first") >>> y.shape (2, 3, 64, 80) + diff --git a/.tether/man/op_image_crop.txt b/.tether/man/op_image_crop.txt index 69d085ebc5..83ebcd97f4 100644 --- a/.tether/man/op_image_crop.txt +++ b/.tether/man/op_image_crop.txt @@ -3,29 +3,33 @@ keras.ops.image.crop_images( images, top_cropping=None, left_cropping=None, + bottom_cropping=None, + right_cropping=None, target_height=None, target_width=None, - bottom_cropping=None, - right_cropping=None + data_format=None ) __doc__ Crop `images` to a specified `height` and `width`. Args: - images: 4-D batch of images of shape `(batch, height, width, channels)` - or 3-D single image of shape `(height, width, channels)`. + images: Input image or batch of images. Must be 3D or 4D. top_cropping: Number of columns to crop from the top. - bottom_cropping: Number of columns to crop from the bottom. left_cropping: Number of columns to crop from the left. + bottom_cropping: Number of columns to crop from the bottom. right_cropping: Number of columns to crop from the right. target_height: Height of the output images. target_width: Width of the output images. + data_format: A string specifying the data format of the input tensor. + It can be either `"channels_last"` or `"channels_first"`. + `"channels_last"` corresponds to inputs with shape + `(batch, height, width, channels)`, while `"channels_first"` + corresponds to inputs with shape `(batch, channels, height, width)`. + If not specified, the value will default to + `keras.config.image_data_format`. Returns: - If `images` were 4D, a 4D float Tensor of shape - `(batch, target_height, target_width, channels)` - If `images` were 3D, a 3D float Tensor of shape - `(target_height, target_width, channels)` + Cropped image or batch of images. Example: diff --git a/.tether/man/op_image_extract_patches.txt b/.tether/man/op_image_extract_patches.txt index f3c5788ea5..d476479093 100644 --- a/.tether/man/op_image_extract_patches.txt +++ b/.tether/man/op_image_extract_patches.txt @@ -1,17 +1,17 @@ __signature__ keras.ops.image.extract_patches( - image, + images, size, strides=None, dilation_rate=1, padding='valid', - data_format='channels_last' + data_format=None ) __doc__ Extracts patches from the image(s). Args: - image: Input image or batch of images. Must be 3D or 4D. + images: Input image or batch of images. Must be 3D or 4D. size: Patch size int or tuple (patch_height, patch_widht) strides: strides along height and width. If not specified, or if `None`, it defaults to the same value as `size`. @@ -20,14 +20,13 @@ Args: strides must be 1. NOTE: `strides > 1` is not supported in conjunction with `dilation_rate > 1` padding: The type of padding algorithm to use: `"same"` or `"valid"`. - data_format: string, either `"channels_last"` or `"channels_first"`. - The ordering of the dimensions in the inputs. `"channels_last"` - corresponds to inputs with shape `(batch, height, width, channels)` - while `"channels_first"` corresponds to inputs with shape - `(batch, channels, height, weight)`. It defaults to the - `image_data_format` value found in your Keras config file at - `~/.keras/keras.json`. If you never set it, then it will be - `"channels_last"`. + data_format: A string specifying the data format of the input tensor. + It can be either `"channels_last"` or `"channels_first"`. + `"channels_last"` corresponds to inputs with shape + `(batch, height, width, channels)`, while `"channels_first"` + corresponds to inputs with shape `(batch, channels, height, width)`. + If not specified, the value will default to + `keras.config.image_data_format`. Returns: Extracted patches 3D (if not batched) or 4D (if batched) @@ -44,3 +43,4 @@ Examples: >>> patches = keras.ops.image.extract_patches(image, (3, 3), (1, 1)) >>> patches.shape (18, 18, 27) + diff --git a/.tether/man/op_image_hsv_to_rgb.txt b/.tether/man/op_image_hsv_to_rgb.txt new file mode 100644 index 0000000000..b26ae69b27 --- /dev/null +++ b/.tether/man/op_image_hsv_to_rgb.txt @@ -0,0 +1,40 @@ +__signature__ +keras.ops.image.hsv_to_rgb(images, data_format=None) +__doc__ +Convert HSV images to RGB. + +`images` must be of float dtype, and the output is only well defined if the +values in `images` are in `[0, 1]`. + +Args: + images: Input image or batch of images. Must be 3D or 4D. + data_format: A string specifying the data format of the input tensor. + It can be either `"channels_last"` or `"channels_first"`. + `"channels_last"` corresponds to inputs with shape + `(batch, height, width, channels)`, while `"channels_first"` + corresponds to inputs with shape `(batch, channels, height, width)`. + If not specified, the value will default to + `keras.config.image_data_format`. + +Returns: + RGB image or batch of RGB images. + +Examples: + +>>> import numpy as np +>>> from keras import ops +>>> x = np.random.random((2, 4, 4, 3)) +>>> y = ops.image.hsv_to_rgb(x) +>>> y.shape +(2, 4, 4, 3) + +>>> x = np.random.random((4, 4, 3)) # Single HSV image +>>> y = ops.image.hsv_to_rgb(x) +>>> y.shape +(4, 4, 3) + +>>> x = np.random.random((2, 3, 4, 4)) +>>> y = ops.image.hsv_to_rgb(x, data_format="channels_first") +>>> y.shape +(2, 3, 4, 4) + diff --git a/.tether/man/op_image_map_coordinates.txt b/.tether/man/op_image_map_coordinates.txt index 53f4f43916..0526f11259 100644 --- a/.tether/man/op_image_map_coordinates.txt +++ b/.tether/man/op_image_map_coordinates.txt @@ -1,43 +1,44 @@ __signature__ keras.ops.image.map_coordinates( - input, + inputs, coordinates, order, fill_mode='constant', fill_value=0 ) __doc__ -Map the input array to new coordinates by interpolation.. +Map the input array to new coordinates by interpolation. Note that interpolation near boundaries differs from the scipy function, because we fixed an outstanding bug [scipy/issues/2640](https://github.com/scipy/scipy/issues/2640). Args: - input: The input array. - coordinates: The coordinates at which input is evaluated. + inputs: The input array. + coordinates: The coordinates at which inputs is evaluated. order: The order of the spline interpolation. The order must be `0` or `1`. `0` indicates the nearest neighbor and `1` indicates the linear interpolation. - fill_mode: Points outside the boundaries of the input are filled + fill_mode: Points outside the boundaries of the inputs are filled according to the given mode. Available methods are `"constant"`, `"nearest"`, `"wrap"` and `"mirror"` and `"reflect"`. Defaults to `"constant"`. - `"constant"`: `(k k k k | a b c d | k k k k)` - The input is extended by filling all values beyond + The inputs is extended by filling all values beyond the edge with the same constant value k specified by `fill_value`. - `"nearest"`: `(a a a a | a b c d | d d d d)` - The input is extended by the nearest pixel. + The inputs is extended by the nearest pixel. - `"wrap"`: `(a b c d | a b c d | a b c d)` - The input is extended by wrapping around to the opposite edge. + The inputs is extended by wrapping around to the opposite edge. - `"mirror"`: `(c d c b | a b c d | c b a b)` - The input is extended by mirroring about the edge. + The inputs is extended by mirroring about the edge. - `"reflect"`: `(d c b a | a b c d | d c b a)` - The input is extended by reflecting about the edge of the last + The inputs is extended by reflecting about the edge of the last pixel. - fill_value: Value used for points outside the boundaries of the input if - `fill_mode="constant"`. Defaults to `0`. + fill_value: Value used for points outside the boundaries of the inputs + if `fill_mode="constant"`. Defaults to `0`. Returns: - Output image or batch of images. + Output input or batch of inputs. + diff --git a/.tether/man/op_image_pad.txt b/.tether/man/op_image_pad.txt index 02b0f49ec6..a870498715 100644 --- a/.tether/man/op_image_pad.txt +++ b/.tether/man/op_image_pad.txt @@ -3,29 +3,33 @@ keras.ops.image.pad_images( images, top_padding=None, left_padding=None, + bottom_padding=None, + right_padding=None, target_height=None, target_width=None, - bottom_padding=None, - right_padding=None + data_format=None ) __doc__ Pad `images` with zeros to the specified `height` and `width`. Args: - images: 4D Tensor of shape `(batch, height, width, channels)` or 3D - Tensor of shape `(height, width, channels)`. + images: Input image or batch of images. Must be 3D or 4D. top_padding: Number of rows of zeros to add on top. - bottom_padding: Number of rows of zeros to add at the bottom. left_padding: Number of columns of zeros to add on the left. + bottom_padding: Number of rows of zeros to add at the bottom. right_padding: Number of columns of zeros to add on the right. target_height: Height of output images. target_width: Width of output images. + data_format: A string specifying the data format of the input tensor. + It can be either `"channels_last"` or `"channels_first"`. + `"channels_last"` corresponds to inputs with shape + `(batch, height, width, channels)`, while `"channels_first"` + corresponds to inputs with shape `(batch, channels, height, width)`. + If not specified, the value will default to + `keras.config.image_data_format`. Returns: - If `images` were 4D, a 4D float Tensor of shape - `(batch, target_height, target_width, channels)` - If `images` were 3D, a 3D float Tensor of shape - `(target_height, target_width, channels)` + Padded image or batch of images. Example: @@ -42,3 +46,4 @@ Example: ... ) >>> padded_batch.shape (2, 20, 30, 3) + diff --git a/.tether/man/op_image_resize.txt b/.tether/man/op_image_resize.txt index a57a6acb0b..7922396729 100644 --- a/.tether/man/op_image_resize.txt +++ b/.tether/man/op_image_resize.txt @@ -1,6 +1,6 @@ __signature__ keras.ops.image.resize( - image, + images, size, interpolation='bilinear', antialias=False, @@ -8,13 +8,13 @@ keras.ops.image.resize( pad_to_aspect_ratio=False, fill_mode='constant', fill_value=0.0, - data_format='channels_last' + data_format=None ) __doc__ Resize images to size using the specified interpolation method. Args: - image: Input image or batch of images. Must be 3D or 4D. + images: Input image or batch of images. Must be 3D or 4D. size: Size of output image in `(height, width)` format. interpolation: Interpolation method. Available methods are `"nearest"`, `"bilinear"`, and `"bicubic"`. Defaults to `"bilinear"`. @@ -36,14 +36,13 @@ Args: supported at this time (fill with constant value, equal to `fill_value`). fill_value: Float. Padding value to use when `pad_to_aspect_ratio=True`. - data_format: string, either `"channels_last"` or `"channels_first"`. - The ordering of the dimensions in the inputs. `"channels_last"` - corresponds to inputs with shape `(batch, height, width, channels)` - while `"channels_first"` corresponds to inputs with shape - `(batch, channels, height, weight)`. It defaults to the - `image_data_format` value found in your Keras config file at - `~/.keras/keras.json`. If you never set it, then it will be - `"channels_last"`. + data_format: A string specifying the data format of the input tensor. + It can be either `"channels_last"` or `"channels_first"`. + `"channels_last"` corresponds to inputs with shape + `(batch, height, width, channels)`, while `"channels_first"` + corresponds to inputs with shape `(batch, channels, height, width)`. + If not specified, the value will default to + `keras.config.image_data_format`. Returns: Resized image or batch of images. diff --git a/.tether/man/op_image_rgb_to_grayscale.txt b/.tether/man/op_image_rgb_to_grayscale.txt index 8432f5e257..870bc157e2 100644 --- a/.tether/man/op_image_rgb_to_grayscale.txt +++ b/.tether/man/op_image_rgb_to_grayscale.txt @@ -1,21 +1,20 @@ __signature__ -keras.ops.image.rgb_to_grayscale(image, data_format='channels_last') +keras.ops.image.rgb_to_grayscale(images, data_format=None) __doc__ Convert RGB images to grayscale. This function converts RGB images to grayscale images. It supports both -3D and 4D tensors, where the last dimension represents channels. +3D and 4D tensors. Args: - image: Input RGB image or batch of RGB images. Must be a 3D tensor - with shape `(height, width, channels)` or a 4D tensor with shape - `(batch, height, width, channels)`. + images: Input image or batch of images. Must be 3D or 4D. data_format: A string specifying the data format of the input tensor. It can be either `"channels_last"` or `"channels_first"`. `"channels_last"` corresponds to inputs with shape `(batch, height, width, channels)`, while `"channels_first"` corresponds to inputs with shape `(batch, channels, height, width)`. - Defaults to `"channels_last"`. + If not specified, the value will default to + `keras.config.image_data_format`. Returns: Grayscale image or batch of grayscale images. @@ -23,7 +22,7 @@ Returns: Examples: >>> import numpy as np ->>> from keras.src import ops +>>> from keras import ops >>> x = np.random.random((2, 4, 4, 3)) >>> y = ops.image.rgb_to_grayscale(x) >>> y.shape diff --git a/.tether/man/op_image_rgb_to_hsv.txt b/.tether/man/op_image_rgb_to_hsv.txt new file mode 100644 index 0000000000..d556f85d7f --- /dev/null +++ b/.tether/man/op_image_rgb_to_hsv.txt @@ -0,0 +1,43 @@ +__signature__ +keras.ops.image.rgb_to_hsv(images, data_format=None) +__doc__ +Convert RGB images to HSV. + +`images` must be of float dtype, and the output is only well defined if the +values in `images` are in `[0, 1]`. + +All HSV values are in `[0, 1]`. A hue of `0` corresponds to pure red, `1/3` +is pure green, and `2/3` is pure blue. + +Args: + images: Input image or batch of images. Must be 3D or 4D. + data_format: A string specifying the data format of the input tensor. + It can be either `"channels_last"` or `"channels_first"`. + `"channels_last"` corresponds to inputs with shape + `(batch, height, width, channels)`, while `"channels_first"` + corresponds to inputs with shape `(batch, channels, height, width)`. + If not specified, the value will default to + `keras.config.image_data_format`. + +Returns: + HSV image or batch of HSV images. + +Examples: + +>>> import numpy as np +>>> from keras import ops +>>> x = np.random.random((2, 4, 4, 3)) +>>> y = ops.image.rgb_to_hsv(x) +>>> y.shape +(2, 4, 4, 3) + +>>> x = np.random.random((4, 4, 3)) # Single RGB image +>>> y = ops.image.rgb_to_hsv(x) +>>> y.shape +(4, 4, 3) + +>>> x = np.random.random((2, 3, 4, 4)) +>>> y = ops.image.rgb_to_hsv(x, data_format="channels_first") +>>> y.shape +(2, 3, 4, 4) + diff --git a/.tether/man/op_isclose.txt b/.tether/man/op_isclose.txt index 067d5b76cd..b7a0d87f12 100644 --- a/.tether/man/op_isclose.txt +++ b/.tether/man/op_isclose.txt @@ -1,11 +1,21 @@ __signature__ -keras.ops.isclose(x1, x2) +keras.ops.isclose( + x1, + x2, + rtol=1e-05, + atol=1e-08, + equal_nan=False +) __doc__ Return whether two tensors are element-wise almost equal. Args: x1: First input tensor. x2: Second input tensor. + rtol: Relative tolerance. + atol: Absolute tolerance. + equal_nan: If `True`, element-wise NaNs are considered equal. Returns: Output boolean tensor. + diff --git a/.tether/man/op_lstsq.txt b/.tether/man/op_lstsq.txt new file mode 100644 index 0000000000..3fa3b8f9ea --- /dev/null +++ b/.tether/man/op_lstsq.txt @@ -0,0 +1,44 @@ +__signature__ +keras.ops.lstsq( + a, + b, + rcond=None +) +__doc__ +Return the least-squares solution to a linear matrix equation. + +Computes the vector x that approximately solves the equation +`a @ x = b`. The equation may be under-, well-, or over-determined +(i.e., the number of linearly independent rows of a can be less than, +equal to, or greater than its number of linearly independent columns). +If a is square and of full rank, then `x` (but for round-off error) +is the exact solution of the equation. Else, `x` minimizes the +L2 norm of `b - a * x`. + +If there are multiple minimizing solutions, +the one with the smallest L2 norm is returned. + +Args: + a: "Coefficient" matrix of shape `(M, N)`. + b: Ordinate or "dependent variable" values, + of shape `(M,)` or `(M, K)`. + If `b` is two-dimensional, the least-squares solution + is calculated for each of the K columns of `b`. + rcond: Cut-off ratio for small singular values of `a`. + For the purposes of rank determination, + singular values are treated as zero if they are + smaller than rcond times the largest + singular value of `a`. + +Returns: + Tensor with shape `(N,)` or `(N, K)` containing + the least-squares solutions. + +**NOTE:** The output differs from `numpy.linalg.lstsq`. +NumPy returns a tuple with four elements, the first of which +being the least-squares solutions and the others +being essentially never used. +Keras only returns the first value. This is done both +to ensure consistency across backends (which cannot be achieved +for the other values) and to simplify the API. + diff --git a/.tether/man/op_map.txt b/.tether/man/op_map.txt new file mode 100644 index 0000000000..5f56257bca --- /dev/null +++ b/.tether/man/op_map.txt @@ -0,0 +1,41 @@ +__signature__ +keras.ops.map(f, xs) +__doc__ +Map a function over leading array axes. + +Like Python’s builtin map, except inputs and outputs are in the form of +stacked arrays. Consider using the `vectorized_map()` transform instead, +unless you need to apply a function element by element for reduced memory +usage or heterogeneous computation with other control flow primitives. + +When `xs` is an array type, the semantics of `map()` are given by this +Python implementation: + +```python +def map(f, xs): + return np.stack([f(x) for x in xs]) +``` + +Args: + f: Callable defines the function to apply element-wise over the first + axis or axes of `xs`. + xs: Values over which to map along the leading axis. + +Returns: + Mapped values. + +Examples: + +>>> f = lambda x: x**2 +>>> xs = keras.ops.arange(10) +>>> ys = keras.ops.map(f, xs) +>>> ys +[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + +>>> f = lambda x: {"y1": x**2, "y2": x * 10} # Can have nested outputs +>>> ys = keras.ops.map(f, xs) +>>> ys["y1"] +[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] +>>> ys["y2"] +[0, 10, 20, 30, 40, 50, 60, 70, 80, 90] + diff --git a/.tether/man/op_scan.txt b/.tether/man/op_scan.txt new file mode 100644 index 0000000000..a9ea445315 --- /dev/null +++ b/.tether/man/op_scan.txt @@ -0,0 +1,78 @@ +__signature__ +keras.ops.scan( + f, + init, + xs=None, + length=None, + reverse=False, + unroll=1 +) +__doc__ +Scan a function over leading array axes while carrying along state. + +When the type of `xs` is an array type or `None`, and the type of `ys` is an +array type, the semantics of `scan()` are given roughly by this Python +implementation: + +```python +def scan(f, init, xs, length=None): + if xs is None: + xs = [None] * length + carry = init + ys = [] + for x in xs: + carry, y = f(carry, x) + ys.append(y) + return carry, np.stack(ys) +``` + +The loop-carried value `carry` (`init`) must hold a fixed shape and dtype +across all iterations. + +In TensorFlow, `y` must match `carry` in shape and dtype. This is not +required in other backends. + +Args: + f: Callable defines the logic for each loop iteration. This accepts two + arguments where the first is a value of the loop carry and the + second is a slice of `xs` along its leading axis. + This callable returns a pair where the first represents a new value + for the loop carry and the second represents a slice of the output. + init: The initial loop carry value. This can be a scalar, tensor, or any + nested structure. It must match the structure of the first element + returned by `f`. + xs: Optional value to scan along its leading axis. This can be a tensor + or any nested structure. If `xs` is not provided, you must specify + `length` to define the number of loop iterations. + Defaults to `None`. + length: Optional integer specifying the number of loop iterations. + If `length` is not provided, it defaults to the sizes of leading + axis of the arrays in `xs`. Defaults to `None`. + reverse: Optional boolean specifying whether to run the scan iteration + forward or in reverse, equivalent to reversing the leading axes of + the arrays in both `xs` and in `ys`. + unroll: Optional positive integer or boolean specifying how many scan + iterations to unroll within a single iteration of a loop. If an + integer is provided, it determines how many unrolled loop iterations + to run within a single rolled iteration of the loop. If a boolean is + provided, it will determine if the loop is completely unrolled + (`unroll=True`) or left completely unrolled (`unroll=False`). + Note that unrolling is only supported by JAX and TensorFlow + backends. + +Returns: + A pair where the first element represents the final loop carry value and + the second element represents the stacked outputs of `f` when scanned + over the leading axis of the inputs. + +Examples: + +>>> sum_fn = lambda c, x: (c + x, c + x) +>>> init = keras.ops.array(0) +>>> xs = keras.ops.array([1, 2, 3, 4, 5]) +>>> carry, result = keras.ops.scan(sum_fn, init, xs) +>>> carry +15 +>>> result +[1, 3, 6, 10, 15] + diff --git a/.tether/man/op_shape.txt b/.tether/man/op_shape.txt index c2a37b5d1b..14501ece31 100644 --- a/.tether/man/op_shape.txt +++ b/.tether/man/op_shape.txt @@ -17,6 +17,7 @@ Returns: Example: ->>> x = keras.zeros((8, 12)) +>>> x = keras.ops.zeros((8, 12)) >>> keras.ops.shape(x) (8, 12) + diff --git a/.tether/man/op_slice.txt b/.tether/man/op_slice.txt index 9029502ef9..18dc64d1a9 100644 --- a/.tether/man/op_slice.txt +++ b/.tether/man/op_slice.txt @@ -17,7 +17,7 @@ via other tensor operations. inputs = np.zeros((5, 5)) start_indices = np.array([3, 3]) shape = np.array([2, 2]) -inputs = keras.ops.slice(inputs, start_indices, updates) +inputs = keras.ops.slice(inputs, start_indices, shape) ``` Args: @@ -28,3 +28,4 @@ Args: Returns: A tensor, has the same shape and dtype as `inputs`. + diff --git a/.tether/man/op_switch.txt b/.tether/man/op_switch.txt new file mode 100644 index 0000000000..20e67ba24d --- /dev/null +++ b/.tether/man/op_switch.txt @@ -0,0 +1,41 @@ +__signature__ +keras.ops.switch( + index, + branches, + *operands +) +__doc__ +Apply exactly one of the `branches` given by `index`. + +If `index` is out of bounds, it is clamped to within bounds. + +The semantics of `switch` are given roughly by this Python implementation: + +```python +def switch(index, branches, *operands): + index = clamp(0, index, len(branches) - 1) + return branches[index](*operands) +``` + +Args: + index: An integer scalar indicating which branch function to apply. + branches: A sequence of functions to be applied based on `index`. + operands: Inputs to whichever branch is applied. + +Returns: + The outputs of `branch(*operands)` for the branch that was selected + based on `index`. + +Examples: + +>>> add_fn = lambda x, y: x + y +>>> substract_fn = lambda x, y: x - y +>>> x = keras.ops.array(2.0) +>>> y = keras.ops.array(0.5) +>>> branches = [add_fn, substract_fn] +>>> keras.ops.switch(0, branches, x, y) +2.5 + +>>> keras.ops.switch(1, branches, x, y) +1.5 + diff --git a/.tether/man/op_vectorize.txt b/.tether/man/op_vectorize.txt index e92d8e4ea8..bd7a9e0549 100644 --- a/.tether/man/op_vectorize.txt +++ b/.tether/man/op_vectorize.txt @@ -14,7 +14,7 @@ Example: def myfunc(a, b): return a + b -vfunc = np.vectorize(myfunc) +vfunc = keras.ops.vectorize(myfunc) y = vfunc([1, 2, 3, 4], 2) # Returns Tensor([3, 4, 5, 6]) ``` diff --git a/.tether/man/rnn_cells_stack.txt b/.tether/man/rnn_cells_stack.txt index 6ddc8e734c..b13957ec51 100644 --- a/.tether/man/rnn_cells_stack.txt +++ b/.tether/man/rnn_cells_stack.txt @@ -68,19 +68,24 @@ class StackedRNNCells(keras.src.layers.layer.Layer) | Class methods defined here: | | from_config(config, custom_objects=None) - | Creates a layer from its config. + | Creates an operation from its config. | - | This method is the reverse of `get_config`, - | capable of instantiating the same layer from the config - | dictionary. It does not handle layer connectivity - | (handled by Network), nor weights (handled by `set_weights`). + | This method is the reverse of `get_config`, capable of instantiating the + | same operation from the config dictionary. + | + | Note: If you override this method, you might receive a serialized dtype + | config, which is a `dict`. You can deserialize it as follows: + | + | ```python + | if "dtype" in config and isinstance(config["dtype"], dict): + | policy = dtype_policies.deserialize(config["dtype"]) + | ``` | | Args: - | config: A Python dictionary, typically the - | output of get_config. + | config: A Python dictionary, typically the output of `get_config`. | | Returns: - | A layer instance. + | An operation instance. | | ---------------------------------------------------------------------- | Readonly properties defined here: diff --git a/.tether/man/save_model.txt b/.tether/man/save_model.txt index dc7489d0cc..8f6d50e79d 100644 --- a/.tether/man/save_model.txt +++ b/.tether/man/save_model.txt @@ -3,6 +3,7 @@ keras.saving.save_model( model, filepath, overwrite=True, + zipped=True, **kwargs ) __doc__ @@ -13,6 +14,8 @@ Args: filepath: `str` or `pathlib.Path` object. Path where to save the model. overwrite: Whether we should overwrite any existing model at the target location, or instead ask the user via an interactive prompt. + zipped: Whether to save the model as a zipped `.keras` + archive (default), or as an unzipped directory. Example: @@ -31,10 +34,11 @@ assert np.allclose(model.predict(x), loaded_model.predict(x)) Note that `model.save()` is an alias for `keras.saving.save_model()`. -The saved `.keras` file contains: +The saved `.keras` file is a `zip` archive that contains: - The model's configuration (architecture) - The model's weights - The model's optimizer's state (if any) Thus models can be reinstantiated in the exact same state. + diff --git a/DESCRIPTION b/DESCRIPTION index 9c357a0a2c..f9abb7fb31 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: keras3 Type: Package Title: R Interface to 'Keras' -Version: 1.0.0.9000 +Version: 1.0.0.9001 Authors@R: c( person("Tomasz", "Kalinowski", role = c("aut", "cph", "cre"), email = "tomasz@posit.co"), diff --git a/NAMESPACE b/NAMESPACE index 48ddcc4387..b00236bfb9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,9 +1,11 @@ # Generated by roxygen2: do not edit by hand +S3method("!=",keras_shape) S3method("$",python_builtin_super_getter) S3method("$<-",keras.src.callbacks.callback.Callback) S3method("+",keras.src.backend.common.keras_tensor.KerasTensor) S3method("==",keras.src.backend.common.keras_tensor.KerasTensor) +S3method("==",keras_shape) S3method("[",keras_shape) S3method("[[",python_builtin_super_getter) S3method(as.array,jaxlib.xla_extension.ArrayImpl) @@ -103,6 +105,8 @@ export(application_mobilenet) export(application_mobilenet_v2) export(application_mobilenet_v3_large) export(application_mobilenet_v3_small) +export(application_nasnet_large) +export(application_nasnet_mobile) export(application_nasnetlarge) export(application_nasnetmobile) export(application_preprocess_inputs) @@ -431,6 +435,7 @@ export(op_arctan2) export(op_arctanh) export(op_argmax) export(op_argmin) +export(op_argpartition) export(op_argsort) export(op_array) export(op_average) @@ -471,6 +476,7 @@ export(op_digitize) export(op_divide) export(op_divide_no_nan) export(op_dot) +export(op_dtype) export(op_eig) export(op_eigh) export(op_einsum) @@ -505,10 +511,12 @@ export(op_imag) export(op_image_affine_transform) export(op_image_crop) export(op_image_extract_patches) +export(op_image_hsv_to_rgb) export(op_image_map_coordinates) export(op_image_pad) export(op_image_resize) export(op_image_rgb_to_grayscale) +export(op_image_rgb_to_hsv) export(op_in_top_k) export(op_inv) export(op_irfft) @@ -535,7 +543,9 @@ export(op_logical_or) export(op_logical_xor) export(op_logspace) export(op_logsumexp) +export(op_lstsq) export(op_lu_factor) +export(op_map) export(op_matmul) export(op_max) export(op_max_pool) @@ -580,6 +590,7 @@ export(op_rfft) export(op_roll) export(op_round) export(op_rsqrt) +export(op_scan) export(op_scatter) export(op_scatter_update) export(op_segment_max) @@ -616,6 +627,7 @@ export(op_subtract) export(op_sum) export(op_svd) export(op_swapaxes) +export(op_switch) export(op_take) export(op_take_along_axis) export(op_tan) diff --git a/NEWS.md b/NEWS.md index 54c3ddbd7a..70911eac8e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,46 @@ - Fixed issue where GPUs would not be found when running on Windows under WSL Linux. (reported in #1456, fixed in #1459) +- `keras_shape` objects (as returned by `keras3::shape()`) gain `==` and `!=` methods. + +User facing changes with upstream Keras v3.4.0: + +- New function: + - `op_argpartition()` + - `op_map()` + - `op_scan()` + - `op_switch()` + - `op_dtype()` + - `op_lstsq()` + - `op_image_hsv_to_rgb()` + - `op_image_rgb_to_hsv()` + +- Changes: + - Added support for arbitrary, deeply nested input/output structures in + Functional models (e.g. lists of lists of lists of inputs or outputs...) + - Add support for `optional` Functional inputs. + - `keras_input()` gains an `optional` argument. + - `keras_model_sequential()` gains a `input_optional` argument. + - Add support for `float8` inference for `Dense` and `EinsumDense` layers. + - Enable `layer_feature_space()` to be used in a `{tfdatasets}` pipeline even + when the backend isn't TensorFlow. + - `layer_string_lookup()` can now take `tf$SparseTensor()` as input. + - `layer_string_lookup()` returns `"int64"` dtype by default in more modes now. + - `Layer()` instances gain attributes `path` and `quantization_mode`. + - `Metric()$variables` is now recursive. + - Add `training` argument to `Model$compute_loss()`. + - `split_dataset()` now supports nested structures in dataset. + - All applications gain a `name` argument, accept a custom name. + - `layer_multi_head_attention()` gains a `seed` argument. + - All losses gain a `dtype` argument. + - `loss_dice()` gains an `axis` argument. + - `op_ctc_decode()`, new default for `mask_index = 0` + - All `op_image_*` functions now use default `data_format` value + to `config_image_data_format()` + - `op_isclose()` gains arguments `rtol`, `atol`, `equal_nan`. + - `save_model()` gains argument `zipped`. + - Bugs fixes and performance improvements. + # keras3 1.0.0 - Chains of `layer_*` calls with `|>` now instantiate layers in the @@ -16,10 +56,10 @@ User facing changes with upstream Keras v3.3.3: - new functions: `op_slogdet()`, `op_psnr()` - `clone_model()` gains new args: `call_function`, `recursive` - Updated example usage. + Updated example usage. - `op_ctc_decode()` strategy argument has new default: `"greedy"`. - Updated docs. + Updated docs. - `loss_ctc()` default name fixed, changed to `"ctc"` diff --git a/R/Layer.R b/R/Layer.R index 0eed99e4ee..a69a40903f 100644 --- a/R/Layer.R +++ b/R/Layer.R @@ -534,6 +534,14 @@ #' #' These are the weights that get updated by the optimizer during training. #' +#' * `path` +#' The path of the layer. +#' +#' If the layer has not been built yet, it will be `NULL`. +#' +#' * `quantization_mode` +#' The quantization mode of this layer, `NULL` if not quantized. +#' #' * `variable_dtype` #' The dtype of the state (weights) of the layer. #' diff --git a/R/applications.R b/R/applications.R index 50b95e60c4..c2b4eff86e 100644 --- a/R/applications.R +++ b/R/applications.R @@ -45,7 +45,7 @@ #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -82,8 +82,8 @@ #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' -#' @param model_name -#' String, name for the model. +#' @param name +#' The name of the model (string). #' #' @export #' @seealso @@ -91,9 +91,10 @@ # + #' @tether keras.applications.ConvNeXtBase application_convnext_base <- -function (model_name = "convnext_base", include_top = TRUE, include_preprocessing = TRUE, +function (include_top = TRUE, include_preprocessing = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, - pooling = NULL, classes = 1000L, classifier_activation = "softmax") + pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name = "convnext_base") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ConvNeXtBase, args) @@ -148,7 +149,7 @@ function (model_name = "convnext_base", include_top = TRUE, include_preprocessin #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -185,8 +186,8 @@ function (model_name = "convnext_base", include_top = TRUE, include_preprocessin #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' -#' @param model_name -#' String, name for the model. +#' @param name +#' The name of the model (string). #' #' @export #' @seealso @@ -194,9 +195,10 @@ function (model_name = "convnext_base", include_top = TRUE, include_preprocessin # + #' @tether keras.applications.ConvNeXtLarge application_convnext_large <- -function (model_name = "convnext_large", include_top = TRUE, +function (include_top = TRUE, include_preprocessing = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name = "convnext_large") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ConvNeXtLarge, args) @@ -251,7 +253,7 @@ function (model_name = "convnext_large", include_top = TRUE, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -288,8 +290,8 @@ function (model_name = "convnext_large", include_top = TRUE, #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' -#' @param model_name -#' String, name for the model. +#' @param name +#' The name of the model (string). #' #' @export #' @seealso @@ -297,9 +299,10 @@ function (model_name = "convnext_large", include_top = TRUE, # + #' @tether keras.applications.ConvNeXtSmall application_convnext_small <- -function (model_name = "convnext_small", include_top = TRUE, +function (include_top = TRUE, include_preprocessing = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name = "convnext_small") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ConvNeXtSmall, args) @@ -354,7 +357,7 @@ function (model_name = "convnext_small", include_top = TRUE, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -391,8 +394,8 @@ function (model_name = "convnext_small", include_top = TRUE, #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' -#' @param model_name -#' String, name for the model. +#' @param name +#' The name of the model (string). #' #' @export #' @seealso @@ -400,9 +403,10 @@ function (model_name = "convnext_small", include_top = TRUE, # + #' @tether keras.applications.ConvNeXtTiny application_convnext_tiny <- -function (model_name = "convnext_tiny", include_top = TRUE, include_preprocessing = TRUE, +function (include_top = TRUE, include_preprocessing = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, - pooling = NULL, classes = 1000L, classifier_activation = "softmax") + pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name = "convnext_tiny") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ConvNeXtTiny, args) @@ -457,7 +461,7 @@ function (model_name = "convnext_tiny", include_top = TRUE, include_preprocessin #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -494,8 +498,8 @@ function (model_name = "convnext_tiny", include_top = TRUE, include_preprocessin #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' -#' @param model_name -#' String, name for the model. +#' @param name +#' The name of the model (string). #' #' @export #' @seealso @@ -503,9 +507,10 @@ function (model_name = "convnext_tiny", include_top = TRUE, include_preprocessin # + #' @tether keras.applications.ConvNeXtXLarge application_convnext_xlarge <- -function (model_name = "convnext_xlarge", include_top = TRUE, +function (include_top = TRUE, include_preprocessing = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name = "convnext_xlarge") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ConvNeXtXLarge, args) @@ -544,7 +549,7 @@ function (model_name = "convnext_xlarge", include_top = TRUE, #' #' @param input_tensor #' optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -582,6 +587,9 @@ function (model_name = "convnext_xlarge", include_top = TRUE, #' of the "top" layer. When loading pretrained weights, #' `classifier_activation` can only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -589,7 +597,8 @@ function (model_name = "convnext_xlarge", include_top = TRUE, #' @tether keras.applications.DenseNet121 application_densenet121 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name = 'densenet121') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$DenseNet121, args) @@ -628,7 +637,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -666,6 +675,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' of the "top" layer. When loading pretrained weights, #' `classifier_activation` can only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -673,7 +685,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.DenseNet169 application_densenet169 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='densenet169') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$DenseNet169, args) @@ -712,7 +725,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -750,6 +763,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' of the "top" layer. When loading pretrained weights, #' `classifier_activation` can only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -757,7 +773,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.DenseNet201 application_densenet201 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='densenet201') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$DenseNet201, args) @@ -807,7 +824,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -842,6 +859,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @param ... #' For forward/backward compatability. #' @@ -853,7 +873,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_b0 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - ...) + name = 'efficientnetb0', ...) { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetB0, args) @@ -903,7 +923,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -938,6 +958,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @param ... #' For forward/backward compatability. #' @@ -949,7 +972,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_b1 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - ...) + name = 'efficientnetb1', ...) { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetB1, args) @@ -999,7 +1022,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -1034,6 +1057,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @param ... #' For forward/backward compatability. #' @@ -1045,7 +1071,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_b2 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - ...) + name = 'efficientnetb2', ...) { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetB2, args) @@ -1095,7 +1121,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -1130,6 +1156,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @param ... #' For forward/backward compatability. #' @@ -1141,7 +1170,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_b3 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - ...) + name = 'efficientnetb3', ...) { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetB3, args) @@ -1191,7 +1220,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -1226,6 +1255,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @param ... #' For forward/backward compatability. #' @@ -1237,7 +1269,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_b4 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - ...) + name = 'efficientnetb4', ...) { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetB4, args) @@ -1287,7 +1319,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -1322,6 +1354,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @param ... #' For forward/backward compatability. #' @@ -1333,7 +1368,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_b5 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - ...) + name = 'efficientnetb5', ...) { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetB5, args) @@ -1383,7 +1418,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -1418,6 +1453,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @param ... #' For forward/backward compatability. #' @@ -1429,7 +1467,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_b6 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - ...) + name = 'efficientnetb6', ...) { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetB6, args) @@ -1479,7 +1517,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -1514,6 +1552,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @param ... #' For forward/backward compatability. #' @@ -1525,7 +1566,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_b7 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - ...) + name = 'efficientnetb7', ...) { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetB7, args) @@ -1578,7 +1619,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -1616,6 +1657,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -1624,7 +1668,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_v2b0 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - include_preprocessing = TRUE) + include_preprocessing = TRUE, name = 'efficientnetv2-b0') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetV2B0, args) @@ -1677,7 +1721,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -1715,6 +1759,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -1723,7 +1770,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_v2b1 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - include_preprocessing = TRUE) + include_preprocessing = TRUE, name = 'efficientnetv2-b1') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetV2B1, args) @@ -1776,7 +1823,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -1814,6 +1861,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -1822,7 +1872,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_v2b2 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - include_preprocessing = TRUE) + include_preprocessing = TRUE, name = 'efficientnetv2-b2') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetV2B2, args) @@ -1875,7 +1925,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -1913,6 +1963,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -1921,7 +1974,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_v2b3 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - include_preprocessing = TRUE) + include_preprocessing = TRUE, name = 'efficientnetv2-b3') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetV2B3, args) @@ -1974,7 +2027,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -2012,6 +2065,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -2020,7 +2076,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_v2l <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - include_preprocessing = TRUE) + include_preprocessing = TRUE, name = "efficientnetv2-l") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetV2L, args) @@ -2073,7 +2129,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -2111,6 +2167,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -2119,7 +2178,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_v2m <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - include_preprocessing = TRUE) + include_preprocessing = TRUE, name = "efficientnetv2-m") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetV2M, args) @@ -2172,7 +2231,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' Optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -2210,6 +2269,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @param include_preprocessing #' Boolean, whether to include the preprocessing layer at the bottom of the network. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -2218,7 +2280,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_efficientnet_v2s <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", - include_preprocessing = TRUE) + include_preprocessing = TRUE, name = "efficientnetv2-s") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$EfficientNetV2S, args) @@ -2268,7 +2330,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -2305,6 +2367,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' of the "top" layer. When loading pretrained weights, #' `classifier_activation` can only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -2312,7 +2377,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.InceptionResNetV2 application_inception_resnet_v2 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name = 'inception_resnet_v2') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$InceptionResNetV2, args) @@ -2361,7 +2427,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' Defaults to `"imagenet"`. #' #' @param input_tensor -#' Optional Keras tensor (i.e. output of `layers.Input()`) +#' Optional Keras tensor (i.e. output of `keras_input()`) #' to use as image input for the model. `input_tensor` is useful for #' sharing inputs between multiple different networks. #' Defaults to `NULL`. @@ -2399,6 +2465,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' layer. When loading pretrained weights, `classifier_activation` #' can only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -2406,7 +2475,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.InceptionV3 application_inception_v3 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='inception_v3') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$InceptionV3, args) @@ -2481,7 +2551,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' to be loaded. Defaults to `"imagenet"`. #' #' @param input_tensor -#' Optional Keras tensor (i.e. output of `layers.Input()`) +#' Optional Keras tensor (i.e. output of `keras_input()`) #' to use as image input for the model. `input_tensor` is useful #' for sharing inputs between multiple different networks. #' Defaults to `NULL`. @@ -2509,6 +2579,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' layer. When loading pretrained weights, `classifier_activation` #' can only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -2517,7 +2590,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, application_mobilenet <- function (input_shape = NULL, alpha = 1, depth_multiplier = 1L, dropout = 0.001, include_top = TRUE, weights = "imagenet", - input_tensor = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_tensor = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name = NULL) { args <- capture_args(list(depth_multiplier = as_integer, classes = as_integer, input_shape = normalize_shape)) @@ -2592,7 +2666,7 @@ function (input_shape = NULL, alpha = 1, depth_multiplier = 1L, #' to be loaded. Defaults to `"imagenet"`. #' #' @param input_tensor -#' Optional Keras tensor (i.e. output of `layers.Input()`) +#' Optional Keras tensor (i.e. output of `keras_input()`) #' to use as image input for the model. `input_tensor` is useful #' for sharing inputs between multiple different networks. #' Defaults to `NULL`. @@ -2620,6 +2694,9 @@ function (input_shape = NULL, alpha = 1, depth_multiplier = 1L, #' layer. When loading pretrained weights, `classifier_activation` #' can only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -2628,7 +2705,7 @@ function (input_shape = NULL, alpha = 1, depth_multiplier = 1L, application_mobilenet_v2 <- function (input_shape = NULL, alpha = 1, include_top = TRUE, weights = "imagenet", input_tensor = NULL, pooling = NULL, - classes = 1000L, classifier_activation = "softmax") + classes = 1000L, classifier_activation = "softmax", name = NULL) { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$MobileNetV2, args) @@ -2729,7 +2806,7 @@ function (input_shape = NULL, alpha = 1, include_top = TRUE, #' #' @param input_tensor #' Optional Keras tensor (i.e. output of -#' `layers.Input()`) +#' `keras_input()`) #' to use as image input for the model. #' #' @param pooling @@ -2765,6 +2842,9 @@ function (input_shape = NULL, alpha = 1, include_top = TRUE, #' Boolean, whether to include the preprocessing #' layer (`Rescaling`) at the bottom of the network. Defaults to `TRUE`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -2774,7 +2854,7 @@ application_mobilenet_v3_large <- function (input_shape = NULL, alpha = 1, minimalistic = FALSE, include_top = TRUE, weights = "imagenet", input_tensor = NULL, classes = 1000L, pooling = NULL, dropout_rate = 0.2, classifier_activation = "softmax", - include_preprocessing = TRUE) + include_preprocessing = TRUE, name = "MobileNetV3Large") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$MobileNetV3Large, args) @@ -2875,7 +2955,7 @@ function (input_shape = NULL, alpha = 1, minimalistic = FALSE, #' #' @param input_tensor #' Optional Keras tensor (i.e. output of -#' `layers.Input()`) +#' `keras_input()`) #' to use as image input for the model. #' #' @param pooling @@ -2911,6 +2991,9 @@ function (input_shape = NULL, alpha = 1, minimalistic = FALSE, #' Boolean, whether to include the preprocessing #' layer (`Rescaling`) at the bottom of the network. Defaults to `TRUE`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -2920,7 +3003,7 @@ application_mobilenet_v3_small <- function (input_shape = NULL, alpha = 1, minimalistic = FALSE, include_top = TRUE, weights = "imagenet", input_tensor = NULL, classes = 1000L, pooling = NULL, dropout_rate = 0.2, classifier_activation = "softmax", - include_preprocessing = TRUE) + include_preprocessing = TRUE, name = "MobileNetV3Small") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$MobileNetV3Small, args) @@ -2967,7 +3050,7 @@ function (input_shape = NULL, alpha = 1, minimalistic = FALSE, #' #' @param input_tensor #' Optional Keras tensor (i.e. output of -#' `layers.Input()`) +#' `keras_input()`) #' to use as image input for the model. #' #' @param pooling @@ -2996,20 +3079,35 @@ function (input_shape = NULL, alpha = 1, minimalistic = FALSE, #' layer. When loading pretrained weights, `classifier_activation` #' can only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + # + #' @tether keras.applications.NASNetLarge -application_nasnetlarge <- +application_nasnet_large <- function (input_shape = NULL, include_top = TRUE, weights = "imagenet", - input_tensor = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_tensor = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name = "nasnet_large") { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$NASNetLarge, args) set_preprocessing_attributes(model, keras$applications$nasnet) } +#' Backward compatibility +#' +#' @param ... passed on to `application_nasnet_large()` or `application_nasnet_mobile()`. +#' +#' @rdname back-compat +#' @export +#' @keywords internal +application_nasnetlarge <- function(...) { + warning("`application_nasnetlarge()` has been renamed to `application_nasnet_large()`") + application_nasnet_large(...) +} #' Instantiates a Mobile NASNet model in ImageNet mode. #' @@ -3050,7 +3148,7 @@ function (input_shape = NULL, include_top = TRUE, weights = "imagenet", #' #' @param input_tensor #' Optional Keras tensor (i.e. output of -#' `layers.Input()`) +#' `keras_input()`) #' to use as image input for the model. #' #' @param pooling @@ -3079,14 +3177,18 @@ function (input_shape = NULL, include_top = TRUE, weights = "imagenet", #' layer. When loading pretrained weights, `classifier_activation` can #' only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + # + #' @tether keras.applications.NASNetMobile -application_nasnetmobile <- +application_nasnet_mobile <- function (input_shape = NULL, include_top = TRUE, weights = "imagenet", - input_tensor = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_tensor = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='nasnet_mobile') { args <- capture_args(list(classes = as_integer)) model <- do.call(keras$applications$NASNetMobile, args) @@ -3094,6 +3196,18 @@ function (input_shape = NULL, include_top = TRUE, weights = "imagenet", } +#' Backward compatibility +#' +#' +#' @export +#' @rdname back-compat +#' @keywords internal +application_nasnetmobile <- function(...) { + warning("`application_nasnetlarge()` has been renamed to `application_nasnet_large()`") + application_nasnet_mobile(...) +} + + #' Instantiates the ResNet101 architecture. #' #' @description @@ -3129,7 +3243,7 @@ function (input_shape = NULL, include_top = TRUE, weights = "imagenet", #' file to be loaded. #' #' @param input_tensor -#' optional Keras tensor (i.e. output of `layers.Input()`) +#' optional Keras tensor (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -3162,6 +3276,9 @@ function (input_shape = NULL, include_top = TRUE, weights = "imagenet", #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -3169,7 +3286,8 @@ function (input_shape = NULL, include_top = TRUE, weights = "imagenet", #' @tether keras.applications.ResNet101 application_resnet101 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='resnet101') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ResNet101, args) @@ -3212,7 +3330,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' file to be loaded. #' #' @param input_tensor -#' optional Keras tensor (i.e. output of `layers.Input()`) +#' optional Keras tensor (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -3245,6 +3363,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -3252,7 +3373,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.ResNet152 application_resnet152 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='resnet152') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ResNet152, args) @@ -3295,7 +3417,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' file to be loaded. #' #' @param input_tensor -#' optional Keras tensor (i.e. output of `layers.Input()`) +#' optional Keras tensor (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -3328,6 +3450,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -3335,7 +3460,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.ResNet50 application_resnet50 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='resnet50') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ResNet50, args) @@ -3377,7 +3503,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' file to be loaded. #' #' @param input_tensor -#' optional Keras tensor (i.e. output of `layers.Input()`) +#' optional Keras tensor (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -3410,6 +3536,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -3417,7 +3546,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.ResNet101V2 application_resnet101_v2 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='resnet101v2') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ResNet101V2, args) @@ -3459,7 +3589,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' file to be loaded. #' #' @param input_tensor -#' optional Keras tensor (i.e. output of `layers.Input()`) +#' optional Keras tensor (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -3492,6 +3622,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -3499,7 +3632,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.ResNet152V2 application_resnet152_v2 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='resnet152v2') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ResNet152V2, args) @@ -3541,7 +3675,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' file to be loaded. #' #' @param input_tensor -#' optional Keras tensor (i.e. output of `layers.Input()`) +#' optional Keras tensor (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -3574,6 +3708,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' When loading pretrained weights, `classifier_activation` can only #' be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -3581,7 +3718,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.ResNet50V2 application_resnet50_v2 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='resnet50v2') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$ResNet50V2, args) @@ -3616,7 +3754,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' dataset, without scaling. #' #' @returns -#' A model instance. +#' A `Model` instance. #' #' @param include_top #' whether to include the 3 fully-connected @@ -3629,7 +3767,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -3667,6 +3805,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' layer. When loading pretrained weights, `classifier_activation` #' can only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -3674,7 +3815,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.VGG16 application_vgg16 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='vgg16') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$VGG16, args) @@ -3722,7 +3864,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -3760,6 +3902,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' layer. When loading pretrained weights, `classifier_activation` can #' only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -3767,7 +3912,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.VGG19 application_vgg19 <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='vgg19') { args <- capture_args(list(classes = as_integer)) model <- do.call(keras$applications$VGG19, args) @@ -3813,7 +3959,7 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' #' @param input_tensor #' optional Keras tensor -#' (i.e. output of `layers.Input()`) +#' (i.e. output of `keras_input()`) #' to use as image input for the model. #' #' @param input_shape @@ -3849,6 +3995,9 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' layer. When loading pretrained weights, `classifier_activation` can #' only be `NULL` or `"softmax"`. #' +#' @param name +#' The name of the model (string). +#' #' @export #' @seealso #' + @@ -3856,7 +4005,8 @@ function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, #' @tether keras.applications.Xception application_xception <- function (include_top = TRUE, weights = "imagenet", input_tensor = NULL, - input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax") + input_shape = NULL, pooling = NULL, classes = 1000L, classifier_activation = "softmax", + name='xception') { args <- capture_args(list(classes = as_integer, input_shape = normalize_shape)) model <- do.call(keras$applications$Xception, args) @@ -4036,4 +4186,3 @@ imagenet_preprocess_input <- function(x, data_format = NULL, mode = "caffe") { preprocess_input <- r_to_py(keras$applications$imagenet_utils)$preprocess_input do.call(preprocess_input, args) } - diff --git a/R/layers-attention.R b/R/layers-attention.R index ef124a7326..dd0878d29f 100644 --- a/R/layers-attention.R +++ b/R/layers-attention.R @@ -399,6 +399,9 @@ function (object, head_dim, num_query_heads, num_key_value_heads, #' @param bias_constraint #' Constraint for dense layer kernels. #' +#' @param seed +#' Optional integer to seed the dropout layer. +#' #' @param ... #' For forward/backward compatability. #' @@ -418,7 +421,7 @@ function (inputs, num_heads, key_dim, value_dim = NULL, dropout = 0, use_bias = TRUE, output_shape = NULL, attention_axes = NULL, kernel_initializer = "glorot_uniform", bias_initializer = "zeros", kernel_regularizer = NULL, bias_regularizer = NULL, activity_regularizer = NULL, - kernel_constraint = NULL, bias_constraint = NULL, ...) + kernel_constraint = NULL, bias_constraint = NULL, seed = NULL, ...) { args <- capture_args(list(input_shape = normalize_shape, batch_size = as_integer, batch_input_shape = normalize_shape, diff --git a/R/layers-backend-wrappers.R b/R/layers-backend-wrappers.R index b55eac30cc..7e462c1fd3 100644 --- a/R/layers-backend-wrappers.R +++ b/R/layers-backend-wrappers.R @@ -7,6 +7,9 @@ #' `torch.nn.Module` into a Keras layer, in particular by making its #' parameters trackable by Keras. #' +#' `layer_torch_module_wrapper()` is only compatible with the PyTorch backend and +#' cannot be used with the TensorFlow or JAX backends. +#' #' # Example #' Here's an example of how the [`layer_torch_module_wrapper()`] can be used with vanilla #' PyTorch modules. diff --git a/R/layers-core.R b/R/layers-core.R index 5407a3a112..687c8d53ab 100644 --- a/R/layers-core.R +++ b/R/layers-core.R @@ -517,7 +517,7 @@ function (object, f, output_shape = NULL, mask = NULL, arguments = NULL, #' Masks a sequence by using a mask value to skip timesteps. #' #' @description -#' For each timestep in the input tensor (dimension #1 in the tensor), +#' For each timestep in the input tensor (the second dimension in the tensor), #' if all values in the input tensor at that timestep #' are equal to `mask_value`, then the timestep will be masked (skipped) #' in all downstream layers (as long as they support masking). diff --git a/R/layers-normalization.R b/R/layers-normalization.R index 28c4b85235..227c486f37 100644 --- a/R/layers-normalization.R +++ b/R/layers-normalization.R @@ -475,7 +475,7 @@ function (object, layer, power_iterations = 1L, ...) #' ```{r} #' data <- op_reshape(1:6, newshape = c(2, 3)) #' normalized_data <- layer_unit_normalization(data) -#' op_sum(normalized_data[1,]^2) +#' op_sum(normalized_data[1, ]^2) #' ``` #' #' @param axis diff --git a/R/layers-reshaping.R b/R/layers-reshaping.R index 8115e9d329..590dbfb564 100644 --- a/R/layers-reshaping.R +++ b/R/layers-reshaping.R @@ -276,7 +276,7 @@ function (object, data_format = NULL, ...) #' @param dims #' List of integers. Permutation pattern does not include the #' batch dimension. Indexing starts at 1. -#' For instance, `c(2, 1)` permutes the first and second dimensions +#' For instance, `(1, 3, 2)` permutes the second and third dimensions #' of the input. #' #' @param object @@ -299,8 +299,8 @@ function (object, dims, ...) { args <- capture_args(list(input_shape = normalize_shape, batch_size = as_integer, batch_input_shape = normalize_shape, - dims = function (x) - tuple(lapply(x, as_integer))), ignore = "object") + dims = function (x) tuple(lapply(x, as_integer))), + ignore = "object") create_layer(keras$layers$Permute, object, args) } diff --git a/R/losses.R b/R/losses.R index 4981aca122..f4f0481ce7 100644 --- a/R/losses.R +++ b/R/losses.R @@ -98,6 +98,12 @@ #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Ground truth values. shape = `[batch_size, d0, .. dN]`. #' @@ -116,7 +122,8 @@ #' @tether keras.losses.BinaryCrossentropy loss_binary_crossentropy <- function (y_true, y_pred, from_logits = FALSE, label_smoothing = 0, - axis = -1L, ..., reduction = "sum_over_batch_size", name = "binary_crossentropy") + axis = -1L, ..., reduction = "sum_over_batch_size", name = "binary_crossentropy", + dtype = NULL) { args <- capture_args(list(axis = as_axis, y_true = as_py_array, @@ -306,6 +313,12 @@ function (y_true, y_pred, from_logits = FALSE, label_smoothing = 0, #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Ground truth values, of shape `(batch_size, d0, .. dN)`. #' @@ -324,7 +337,8 @@ function (y_true, y_pred, from_logits = FALSE, label_smoothing = 0, loss_binary_focal_crossentropy <- function (y_true, y_pred, apply_class_balancing = FALSE, alpha = 0.25, gamma = 2, from_logits = FALSE, label_smoothing = 0, - axis = -1L, ..., reduction = "sum_over_batch_size", name = "binary_focal_crossentropy") + axis = -1L, ..., reduction = "sum_over_batch_size", name = "binary_focal_crossentropy", + dtype = NULL) { args <- capture_args(list(axis = as_axis, y_true = as_py_array, @@ -412,6 +426,12 @@ function (y_true, y_pred, apply_class_balancing = FALSE, #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Tensor of one-hot true targets. #' @@ -430,7 +450,8 @@ function (y_true, y_pred, apply_class_balancing = FALSE, #' @tether keras.losses.CategoricalCrossentropy loss_categorical_crossentropy <- function (y_true, y_pred, from_logits = FALSE, label_smoothing = 0, - axis = -1L, ..., reduction = "sum_over_batch_size", name = "categorical_crossentropy") + axis = -1L, ..., reduction = "sum_over_batch_size", + name = "categorical_crossentropy", dtype = NULL) { args <- capture_args(list(axis = as_axis, y_true = as_py_array, @@ -565,6 +586,12 @@ function (y_true, y_pred, from_logits = FALSE, label_smoothing = 0, #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Tensor of one-hot true targets. #' @@ -583,7 +610,8 @@ function (y_true, y_pred, from_logits = FALSE, label_smoothing = 0, loss_categorical_focal_crossentropy <- function (y_true, y_pred, alpha = 0.25, gamma = 2, from_logits = FALSE, label_smoothing = 0, axis = -1L, ..., - reduction = "sum_over_batch_size", name = "categorical_focal_crossentropy") + reduction = "sum_over_batch_size", name = "categorical_focal_crossentropy", + dtype = NULL) { args <- capture_args(list(axis = as_axis, y_true = as_py_array, @@ -624,6 +652,12 @@ function (y_true, y_pred, alpha = 0.25, gamma = 2, #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' The ground truth values. `y_true` values are expected to be #' either `{-1, +1}` or `{0, 1}` (i.e. a one-hot-encoded tensor) with @@ -644,7 +678,7 @@ function (y_true, y_pred, alpha = 0.25, gamma = 2, #' @tether keras.losses.CategoricalHinge loss_categorical_hinge <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "categorical_hinge") + name = "categorical_hinge", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) @@ -694,6 +728,12 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Tensor of true targets. #' @@ -712,7 +752,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.CosineSimilarity loss_cosine_similarity <- function (y_true, y_pred, axis = -1L, ..., reduction = "sum_over_batch_size", - name = "cosine_similarity") + name = "cosine_similarity", dtype = NULL) { args <- capture_args(list(axis = as_axis, y_true = as_py_array, @@ -736,15 +776,33 @@ function (y_true, y_pred, axis = -1L, ..., reduction = "sum_over_batch_size", #' loss = 1 - (2 * sum(y_true * y_pred)) / (sum(y_true) + sum(y_pred)) #' ``` #' +#' # Example +#' ```{r} +#' y_true <- array(c(1, 1, 0, 0, +#' 1, 1, 0, 0), dim = c(2, 2, 2, 1)) +#' y_pred <- array(c(0, 0.4, 0, 0, +#' 1, 0, 1, 0.9), dim = c(2, 2, 2, 1)) +#' +#' axis <- c(2, 3, 4) +#' loss <- loss_dice(y_true, y_pred, axis = axis) +#' stopifnot(shape(loss) == shape(2)) +#' loss +#' +#' +#' loss = loss_dice(y_true, y_pred) +#' stopifnot(shape(loss) == shape()) +#' loss +#' ``` +#' #' @returns #' if `y_true` and `y_pred` are provided, Dice loss value. Otherwise, #' a `Loss()` instance. #' #' @param y_true -#' tensor of true targets. +#' Tensor of true targets. #' #' @param y_pred -#' tensor of predicted targets. +#' Tensor of predicted targets. #' #' @param reduction #' Type of reduction to apply to the loss. In almost all cases @@ -754,6 +812,16 @@ function (y_true, y_pred, axis = -1L, ..., reduction = "sum_over_batch_size", #' @param name #' String, name for the object #' +#' @param axis +#' List of which dimensions the loss is calculated. Defaults to +#' `NULL`. +#' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param ... #' For forward/backward compatability. #' @@ -761,9 +829,10 @@ function (y_true, y_pred, axis = -1L, ..., reduction = "sum_over_batch_size", #' @family losses #' @tether keras.losses.Dice loss_dice <- -function (y_true, y_pred, ..., reduction = "sum_over_batch_size", name = "dice") +function (y_true, y_pred, ..., reduction = "sum_over_batch_size", name = "dice", + axis = NULL, dtype = NULL) { - args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) + args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array, axis = as_axis)) callable <- if (missing(y_true) && missing(y_pred)) keras$losses$Dice else keras$losses$dice @@ -802,6 +871,12 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", name = "dice") #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' The ground truth values. `y_true` values are expected to be -1 #' or 1. If binary (0 or 1) labels are provided they will be converted @@ -822,7 +897,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", name = "dice") #' @tether keras.losses.Hinge loss_hinge <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "hinge") + name = "hinge", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) @@ -871,6 +946,12 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @param name #' Optional name for the instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' tensor of true targets. #' @@ -889,7 +970,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.Huber loss_huber <- function (y_true, y_pred, delta = 1, ..., reduction = "sum_over_batch_size", - name = "huber_loss") + name = "huber_loss", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) @@ -932,6 +1013,12 @@ function (y_true, y_pred, delta = 1, ..., reduction = "sum_over_batch_size", #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Tensor of true targets. #' @@ -950,7 +1037,7 @@ function (y_true, y_pred, delta = 1, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.KLDivergence loss_kl_divergence <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "kl_divergence") + name = "kl_divergence", dtype = NULL) { args <- capture_args(list(axis = as_axis, y_true = as_py_array, @@ -994,6 +1081,12 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @param name #' Optional name for the instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Ground truth values with shape = `[batch_size, d0, .. dN]`. #' @@ -1012,7 +1105,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.LogCosh loss_log_cosh <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "log_cosh") + name = "log_cosh", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) @@ -1050,6 +1143,12 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Ground truth values with shape = `[batch_size, d0, .. dN]`. #' @@ -1068,7 +1167,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.MeanAbsoluteError loss_mean_absolute_error <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "mean_absolute_error") + name = "mean_absolute_error", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) @@ -1111,6 +1210,12 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Ground truth values with shape = `[batch_size, d0, .. dN]`. #' @@ -1129,7 +1234,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.MeanAbsolutePercentageError loss_mean_absolute_percentage_error <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "mean_absolute_percentage_error") + name = "mean_absolute_percentage_error", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) @@ -1167,6 +1272,12 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Ground truth values with shape = `[batch_size, d0, .. dN]`. #' @@ -1185,7 +1296,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.MeanSquaredError loss_mean_squared_error <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "mean_squared_error") + name = "mean_squared_error", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) @@ -1227,6 +1338,12 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Ground truth values with shape = `[batch_size, d0, .. dN]`. #' @@ -1245,7 +1362,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.MeanSquaredLogarithmicError loss_mean_squared_logarithmic_error <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "mean_squared_logarithmic_error") + name = "mean_squared_logarithmic_error", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) @@ -1284,6 +1401,12 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Ground truth values. shape = `[batch_size, d0, .. dN]`. #' @@ -1302,7 +1425,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.Poisson loss_poisson <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "poisson") + name = "poisson", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) @@ -1385,6 +1508,12 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' Ground truth values. #' @@ -1414,7 +1543,8 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.SparseCategoricalCrossentropy loss_sparse_categorical_crossentropy <- function (y_true, y_pred, from_logits = FALSE, ignore_class = NULL, - axis = -1L, ..., reduction = "sum_over_batch_size", name = "sparse_categorical_crossentropy") + axis = -1L, ..., reduction = "sum_over_batch_size", name = "sparse_categorical_crossentropy", + dtype = NULL) { args <- capture_args(list(ignore_class = as_integer, axis = as_axis, @@ -1457,6 +1587,12 @@ function (y_true, y_pred, from_logits = FALSE, ignore_class = NULL, #' @param name #' Optional name for the loss instance. #' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). +#' #' @param y_true #' The ground truth values. `y_true` values are expected to be -1 #' or 1. If binary (0 or 1) labels are provided we will convert them @@ -1477,7 +1613,7 @@ function (y_true, y_pred, from_logits = FALSE, ignore_class = NULL, #' @tether keras.losses.SquaredHinge loss_squared_hinge <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "squared_hinge") + name = "squared_hinge", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) @@ -1500,8 +1636,19 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' containing logits (the output of your model). #' They should *not* be normalized via softmax. #' +#' @param reduction +#' Type of reduction to apply to the loss. In almost all cases +#' this should be `"sum_over_batch_size"`. +#' Supported options are `"sum"`, `"sum_over_batch_size"` or `NULL`. +#' #' @param name -#' String, name for the object +#' Optional name for the loss instance. +#' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). #' #' @param ... #' For forward/backward compatability. @@ -1517,7 +1664,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", # + loss_ctc <- function (y_true, y_pred, ..., reduction = "sum_over_batch_size", - name = "ctc") + name = "ctc", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) callable <- if (missing(y_true) && missing(y_pred)) @@ -1554,13 +1701,21 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' tensor of predicted targets. #' #' @param alpha -#' coefficient controlling incidence of false positives. +#' The coefficient controlling incidence of false positives. +#' Defaults to `0.5`. #' #' @param beta -#' coefficient controlling incidence of false negatives. +#' The coefficient controlling incidence of false negatives. +#' Defaults to `0.5`. #' #' @param name -#' String, name for the object +#' Optional name for the loss instance. (string) +#' +#' @param dtype +#' The dtype of the loss's computations. Defaults to `NULL`, which +#' means using `config_floatx()`. `config_floatx()` is a +#' `"float32"` unless set to different value +#' (via `config_set_floatx()`). #' #' @param ... #' For forward/backward compatability. @@ -1571,7 +1726,7 @@ function (y_true, y_pred, ..., reduction = "sum_over_batch_size", #' @tether keras.losses.Tversky loss_tversky <- function (y_true, y_pred, ..., alpha = 0.5, beta = 0.5, - reduction = "sum_over_batch_size", name = "tversky") + reduction = "sum_over_batch_size", name = "tversky", dtype = NULL) { args <- capture_args(list(y_true = as_py_array, y_pred = as_py_array)) callable <- if (missing(y_true) && missing(y_pred)) diff --git a/R/metrics.R b/R/metrics.R index 2308f4a3c1..afc5f7bd17 100644 --- a/R/metrics.R +++ b/R/metrics.R @@ -2840,27 +2840,27 @@ function (y_true, y_pred, ..., name = "poisson", dtype = NULL) #' #' ```{r} #' m <- metric_sparse_categorical_crossentropy() -#' m$update_state(c(1, 2), +#' m$update_state(array(c(1, 2)), #' rbind(c(0.05, 0.95, 0), c(0.1, 0.8, 0.1))) #' m$result() #' ``` #' #' ```{r} #' m$reset_state() -#' m$update_state(c(1, 2), +#' m$update_state(array(c(1, 2)), #' rbind(c(0.05, 0.95, 0), c(0.1, 0.8, 0.1)), #' sample_weight = c(0.3, 0.7)) #' m$result() -#' # 1.6271976 #' ``` #' #' Usage with `compile()` API: #' #' ```{r, eval = FALSE} -#' model %>% compile( -#' optimizer = 'sgd', -#' loss = 'mse', -#' metrics = list(metric_sparse_categorical_crossentropy())) +#' model |> compile( +#' optimizer = 'sgd', +#' loss = 'mse', +#' metrics = list(metric_sparse_categorical_crossentropy()) +#' ) #' ``` #' #' @param name diff --git a/R/model-creation.R b/R/model-creation.R index 4ca532ff2a..c527d66504 100644 --- a/R/model-creation.R +++ b/R/model-creation.R @@ -96,6 +96,10 @@ keras_model <- function(inputs = NULL, outputs = NULL, ...) { #' If set, the layer will use this tensor rather #' than creating a new placeholder tensor. #' +#' @param optional +#' Boolean, whether the input is optional or not. +#' An optional input can accept `NULL` values. +#' #' @param batch_shape #' Shape, including the batch dim. #' @@ -107,7 +111,7 @@ keras_model <- function(inputs = NULL, outputs = NULL, ...) { #' @tether keras.layers.Input keras_input <- function (shape = NULL, batch_size = NULL, dtype = NULL, sparse = NULL, - batch_shape = NULL, name = NULL, tensor = NULL) + batch_shape = NULL, name = NULL, tensor = NULL, optional = FALSE) { args <- capture_args(list(shape = normalize_shape, batch_size = as_integer, input_shape = normalize_shape, batch_input_shape = normalize_shape, @@ -156,6 +160,10 @@ function (shape = NULL, batch_size = NULL, dtype = NULL, sparse = NULL, #' If set, the layer will use this tensor rather #' than creating a new placeholder tensor. #' +#' @param input_optional +#' Boolean, whether the input is optional or not. +#' An optional input can accept `NULL` values. +#' #' @param ... additional arguments passed on to `keras.layers.InputLayer`. #' #' @param layers List of layers to add to the model. @@ -207,6 +215,7 @@ function(input_shape = NULL, name = NULL, input_batch_shape = NULL, input_name = NULL, input_tensor = NULL, + input_optional = FALSE, trainable = TRUE, layers = list()) { @@ -241,7 +250,8 @@ function(input_shape = NULL, input_sparse = NULL, input_batch_shape = NULL, input_name = NULL, - input_tensor = NULL) + input_tensor = NULL, + input_optional = FALSE) { args <- capture_args(list( input_shape = normalize_shape, @@ -270,6 +280,8 @@ function(input_shape = NULL, sparse = "input_sparse", + optional = "input_optional", + .skip_existing = TRUE) do.call(keras$layers$InputLayer, args) diff --git a/R/model-persistence.R b/R/model-persistence.R index 9e959068f4..2f03e7a0f0 100644 --- a/R/model-persistence.R +++ b/R/model-persistence.R @@ -20,7 +20,7 @@ #' )) #' ``` #' -#' The saved `.keras` file contains: +#' The saved `.keras` file is a `zip` archive that contains: #' #' - The model's configuration (architecture) #' - The model's weights @@ -47,6 +47,10 @@ #' at the target location, or instead ask the user #' via an interactive prompt. #' +#' @param zipped +#' Whether to save the model as a zipped `.keras` +#' archive (default), or as an unzipped directory. +#' #' @param ... #' For forward/backward compatability. #' @@ -63,7 +67,7 @@ # @seealso # + save_model <- -function (model, filepath = NULL, overwrite = FALSE, ...) +function (model, filepath = NULL, overwrite = FALSE, zipped = TRUE, ...) { if(is.null(filepath) -> return_serialized) { filepath <- tempfile(pattern = "keras_model-", fileext = ".keras") @@ -71,7 +75,10 @@ function (model, filepath = NULL, overwrite = FALSE, ...) } overwrite <- confirm_overwrite(filepath, overwrite) - keras$saving$save_model(model, filepath, overwrite = overwrite) + args <- list(model, filepath, overwrite = overwrite) + if (!isTRUE(zipped)) + args[["zipped"]] <- zipped # arg added in Keras 3.4.0 + do.call(keras$saving$save_model, args) if(return_serialized) readBin(filepath, what = "raw", n = file.size(filepath)) @@ -324,7 +331,7 @@ load_model_config <- function(filepath, custom_objects = NULL) #' # Create the artifact #' model |> tensorflow::export_savedmodel("path/to/location") #' -#' # Later, in a different process / environment... +#' # Later, in a different process/environment... #' library(tensorflow) #' reloaded_artifact <- tf$saved_model$load("path/to/location") #' predictions <- reloaded_artifact$serve(input_data) diff --git a/R/ops-image.R b/R/ops-image.R index 7891e73610..a56312b155 100644 --- a/R/ops-image.R +++ b/R/ops-image.R @@ -36,7 +36,7 @@ #' @returns #' Applied affine transform image or batch of images. #' -#' @param image +#' @param images #' Input image or batch of images. Must be 3D or 4D. #' #' @param transform @@ -76,14 +76,14 @@ #' `fill_mode = "constant"`. Defaults to `0`. #' #' @param data_format -#' string, either `"channels_last"` or `"channels_first"`. -#' The ordering of the dimensions in the inputs. `"channels_last"` -#' corresponds to inputs with shape `(batch, height, width, channels)` -#' while `"channels_first"` corresponds to inputs with shape -#' `(batch, channels, height, weight)`. It defaults to the -#' `image_data_format` value found in your Keras config file at -#' `~/.keras/keras.json`. If you never set it, then it will be -#' `"channels_last"`. +#' A string specifying the data format of the input tensor. +#' It can be either `"channels_last"` or `"channels_first"`. +#' `"channels_last"` corresponds to inputs with shape +#' `(batch, height, width, channels)`, while `"channels_first"` +#' corresponds to inputs with shape `(batch, channels, height, width)`. +#' +#' If not specified, the value will default to +#' `config_image_data_format()`. #' #' @export #' @family image ops @@ -95,10 +95,14 @@ #' #' @tether keras.ops.image.affine_transform op_image_affine_transform <- -function (image, transform, interpolation = "bilinear", fill_mode = "constant", - fill_value = 0L, data_format = "channels_last") +function (images, transform, interpolation = "bilinear", fill_mode = "constant", + fill_value = 0L, data_format = NULL) { args <- capture_args(list(fill_value = as_integer)) + + # pass 'images' as unnamed positional arg (was renamed from 'image' in Keras 3.4.0) + args <- args_to_positional(args, "images") + do.call(keras$ops$image$affine_transform, args) } @@ -122,7 +126,7 @@ function (image, transform, interpolation = "bilinear", fill_mode = "constant", #' @returns #' Extracted patches 3D (if not batched) or 4D (if batched) #' -#' @param image +#' @param images #' Input image or batch of images. Must be 3D or 4D. #' #' @param size @@ -142,14 +146,13 @@ function (image, transform, interpolation = "bilinear", fill_mode = "constant", #' The type of padding algorithm to use: `"same"` or `"valid"`. #' #' @param data_format -#' string, either `"channels_last"` or `"channels_first"`. -#' The ordering of the dimensions in the inputs. `"channels_last"` -#' corresponds to inputs with shape `(batch, height, width, channels)` -#' while `"channels_first"` corresponds to inputs with shape -#' `(batch, channels, height, weight)`. It defaults to the -#' `image_data_format` value found in your Keras config file at -#' `~/.keras/keras.json`. If you never set it, then it will be -#' `"channels_last"`. +#' A string specifying the data format of the input tensor. +#' It can be either `"channels_last"` or `"channels_first"`. +#' `"channels_last"` corresponds to inputs with shape +#' `(batch, height, width, channels)`, while `"channels_first"` +#' corresponds to inputs with shape `(batch, channels, height, width)`. +#' If not specified, the value will default to +#' `config_image_data_format()`. #' #' @export #' @family image ops @@ -161,15 +164,18 @@ function (image, transform, interpolation = "bilinear", fill_mode = "constant", #' #' @tether keras.ops.image.extract_patches op_image_extract_patches <- -function (image, size, strides = NULL, dilation_rate = 1L, padding = "valid", +function (images, size, strides = NULL, dilation_rate = 1L, padding = "valid", data_format = "channels_last") { args <- capture_args(list(size = as_integer, dilation_rate = as_integer)) + # pass 'images' as unnamed positional arg (was renamed from 'image' in Keras 3.4.0) + args <- args_to_positional(args, "images") + do.call(keras$ops$image$extract_patches, args) } -#' Map the input array to new coordinates by interpolation.. +#' Map the input array to new coordinates by interpolation. #' #' @description #' Note that interpolation near boundaries differs from the scipy function, @@ -177,13 +183,13 @@ function (image, size, strides = NULL, dilation_rate = 1L, padding = "valid", #' [scipy/issues/2640](https://github.com/scipy/scipy/issues/2640). #' #' @returns -#' Output image or batch of images. +#' Output input or batch of inputs. #' -#' @param input +#' @param inputs #' The input array. #' #' @param coordinates -#' The coordinates at which input is evaluated. +#' The coordinates at which `inputs` is evaluated. #' #' @param order #' The order of the spline interpolation. The order must be `0` or @@ -191,27 +197,27 @@ function (image, size, strides = NULL, dilation_rate = 1L, padding = "valid", #' interpolation. #' #' @param fill_mode -#' Points outside the boundaries of the input are filled +#' Points outside the boundaries of the inputs are filled #' according to the given mode. Available methods are `"constant"`, #' `"nearest"`, `"wrap"` and `"mirror"` and `"reflect"`. Defaults to #' `"constant"`. #' - `"constant"`: `(k k k k | a b c d | k k k k)` -#' The input is extended by filling all values beyond +#' inputs is extended by filling all values beyond #' the edge with the same constant value k specified by #' `fill_value`. #' - `"nearest"`: `(a a a a | a b c d | d d d d)` -#' The input is extended by the nearest pixel. +#' inputs is extended by the nearest pixel. #' - `"wrap"`: `(a b c d | a b c d | a b c d)` -#' The input is extended by wrapping around to the opposite edge. +#' inputs is extended by wrapping around to the opposite edge. #' - `"mirror"`: `(c d c b | a b c d | c b a b)` -#' The input is extended by mirroring about the edge. +#' inputs is extended by mirroring about the edge. #' - `"reflect"`: `(d c b a | a b c d | d c b a)` -#' The input is extended by reflecting about the edge of the last +#' inputs is extended by reflecting about the edge of the last #' pixel. #' #' @param fill_value -#' Value used for points outside the boundaries of the input if -#' `fill_mode = "constant"`. Defaults to `0`. +#' Value used for points outside the boundaries of the inputs +#' if `fill_mode = "constant"`. Defaults to `0`. #' #' @export #' @family image ops @@ -221,10 +227,12 @@ function (image, size, strides = NULL, dilation_rate = 1L, padding = "valid", # + #' @tether keras.ops.image.map_coordinates op_image_map_coordinates <- -function (input, coordinates, order, fill_mode = "constant", +function (inputs, coordinates, order, fill_mode = "constant", fill_value = 0L) { args <- capture_args(list(fill_value = as_integer)) + # "inputs" renamed from "input" in Keras 3.4.0 + args <- args_to_positional(args, "inputs") do.call(keras$ops$image$map_coordinates, args) } @@ -251,14 +259,10 @@ function (input, coordinates, order, fill_mode = "constant", #' ``` #' #' @returns -#' - If `images` were 4D, a 4D float Tensor of shape -#' `(batch, target_height, target_width, channels)` -#' - If `images` were 3D, a 3D float Tensor of shape -#' `(target_height, target_width, channels)` +#' Padded image or batch of images. #' #' @param images -#' 4D Tensor of shape `(batch, height, width, channels)` or 3D -#' Tensor of shape `(height, width, channels)`. +#' Input image or batch of images. Must be 3D or 4D. #' #' @param top_padding #' Number of rows of zeros to add on top. @@ -278,6 +282,15 @@ function (input, coordinates, order, fill_mode = "constant", #' @param target_width #' Width of output images. #' +#' @param data_format +#' A string specifying the data format of the input tensor. +#' It can be either `"channels_last"` or `"channels_first"`. +#' `"channels_last"` corresponds to inputs with shape +#' `(batch, height, width, channels)`, while `"channels_first"` +#' corresponds to inputs with shape `(batch, channels, height, width)`. +#' If not specified, the value will default to +#' `config_image_data_format()`. +#' #' @export #' @family image ops #' @family image utils @@ -287,8 +300,9 @@ function (input, coordinates, order, fill_mode = "constant", #' #' @tether keras.ops.image.pad_images op_image_pad <- -function (images, top_padding = NULL, left_padding = NULL, target_height = NULL, - target_width = NULL, bottom_padding = NULL, right_padding = NULL) +function (images, top_padding = NULL, left_padding = NULL, + bottom_padding = NULL, right_padding = NULL, + target_height = NULL, target_width = NULL, data_format = NULL) { args <- capture_args(list(images = as_integer, top_padding = as_integer, bottom_padding = as_integer, left_padding = as_integer, @@ -325,7 +339,7 @@ function (images, top_padding = NULL, left_padding = NULL, target_height = NULL, #' @returns #' Resized image or batch of images. #' -#' @param image +#' @param images #' Input image or batch of images. Must be 3D or 4D. #' #' @param size @@ -364,14 +378,14 @@ function (images, top_padding = NULL, left_padding = NULL, target_height = NULL, #' Float. Padding value to use when `pad_to_aspect_ratio=TRUE`. #' #' @param data_format -#' string, either `"channels_last"` or `"channels_first"`. -#' The ordering of the dimensions in the inputs. `"channels_last"` -#' corresponds to inputs with shape `(batch, height, width, channels)` -#' while `"channels_first"` corresponds to inputs with shape -#' `(batch, channels, height, weight)`. It defaults to the -#' `image_data_format` value found in your Keras config file at -#' `~/.keras/keras.json`. If you never set it, then it will be -#' `"channels_last"`. +#' A string specifying the data format of the input tensor. +#' It can be either `"channels_last"` or `"channels_first"`. +#' `"channels_last"` corresponds to inputs with shape +#' `(batch, height, width, channels)`, while `"channels_first"` +#' corresponds to inputs with shape `(batch, channels, height, width)`. +#' If not specified, the value will default to +#' `config_image_data_format()`. +#' #' #' @export #' @family image ops @@ -382,12 +396,12 @@ function (images, top_padding = NULL, left_padding = NULL, target_height = NULL, # + #' @tether keras.ops.image.resize op_image_resize <- -function (image, size, interpolation = "bilinear", antialias = FALSE, +function (images, size, interpolation = "bilinear", antialias = FALSE, crop_to_aspect_ratio = FALSE, pad_to_aspect_ratio = FALSE, - fill_mode = "constant", fill_value = 0, - data_format = "channels_last") + fill_mode = "constant", fill_value = 0, data_format = NULL) { args <- capture_args(list(size = as_integer)) + args <- args_to_positional(args, "images") do.call(keras$ops$image$resize, args) } @@ -406,14 +420,10 @@ function (image, size, interpolation = "bilinear", antialias = FALSE, #' ``` #' #' @returns -#' If `images` were 4D, a 4D float Tensor of shape -#' `(batch, target_height, target_width, channels)` -#' If `images` were 3D, a 3D float Tensor of shape -#' `(target_height, target_width, channels)` +#' Cropped image or batch of images. #' #' @param images -#' 4-D batch of images of shape `(batch, height, width, channels)` -#' or 3-D single image of shape `(height, width, channels)`. +#' Input image or batch of images. Must be 3D or 4D. #' #' @param top_cropping #' Number of columns to crop from the top. @@ -433,6 +443,15 @@ function (image, size, interpolation = "bilinear", antialias = FALSE, #' @param target_width #' Width of the output images. #' +#' @param data_format +#' A string specifying the data format of the input tensor. +#' It can be either `"channels_last"` or `"channels_first"`. +#' `"channels_last"` corresponds to inputs with shape +#' `(batch, height, width, channels)`, while `"channels_first"` +#' corresponds to inputs with shape `(batch, channels, height, width)`. +#' If not specified, the value will default to +#' `config_image_data_format()`. +#' #' @export #' @family image ops #' @family image utils @@ -442,8 +461,9 @@ function (image, size, interpolation = "bilinear", antialias = FALSE, #' + op_image_crop <- function (images, top_cropping = NULL, left_cropping = NULL, - target_height = NULL, target_width = NULL, bottom_cropping = NULL, - right_cropping = NULL) { + bottom_cropping = NULL, right_cropping = NULL, + target_height = NULL, target_width = NULL, + data_format = NULL) { args <- capture_args(list( top_cropping = as_integer, left_cropping = as_integer, @@ -483,10 +503,8 @@ function (images, top_cropping = NULL, left_cropping = NULL, #' @returns #' Grayscale image or batch of grayscale images. #' -#' @param image -#' Input RGB image or batch of RGB images. Must be a 3D tensor -#' with shape `(height, width, channels)` or a 4D tensor with shape -#' `(batch, height, width, channels)`. +#' @param images +#' Input image or batch of images. Must be 3D or 4D. #' #' @param data_format #' A string specifying the data format of the input tensor. @@ -494,7 +512,8 @@ function (images, top_cropping = NULL, left_cropping = NULL, #' `"channels_last"` corresponds to inputs with shape #' `(batch, height, width, channels)`, while `"channels_first"` #' corresponds to inputs with shape `(batch, channels, height, width)`. -#' Defaults to `"channels_last"`. +#' If not specified, the value will default to +#' `config_image_data_format()`. #' #' @export #' @family image ops @@ -502,5 +521,111 @@ function (images, top_cropping = NULL, left_cropping = NULL, #' @family ops #' @tether keras.ops.image.rgb_to_grayscale op_image_rgb_to_grayscale <- -function (image, data_format = "channels_last") -keras$ops$image$rgb_to_grayscale(image, data_format) +function (images, data_format = NULL) { + keras$ops$image$rgb_to_grayscale(images, data_format) +} + + + +#' Convert HSV images to RGB. +#' +#' @description +#' `images` must be of float dtype, and the output is only well defined if the +#' values in `images` are in `[0, 1]`. +#' +#' # Examples +#' ```{r} +#' x <- random_uniform(c(2, 4, 4, 3)) +#' y <- op_image_hsv_to_rgb(x) +#' shape(y) +#' ``` +#' +#' ```{r} +#' x <- random_uniform(c(4, 4, 3)) # Single HSV image +#' y <- op_image_hsv_to_rgb(x) +#' shape(y) +#' ``` +#' +#' ```{r} +#' x <- random_uniform(c(2, 3, 4, 4)) +#' y <- op_image_hsv_to_rgb(x, data_format="channels_first") +#' shape(y) +#' ``` +#' +#' @returns +#' RGB image or batch of RGB images. +#' +#' @param images +#' Input image or batch of images. Must be 3D or 4D. +#' +#' @param data_format +#' A string specifying the data format of the input tensor. +#' It can be either `"channels_last"` or `"channels_first"`. +#' `"channels_last"` corresponds to inputs with shape +#' `(batch, height, width, channels)`, while `"channels_first"` +#' corresponds to inputs with shape `(batch, channels, height, width)`. +#' If not specified, the value will default to +#' `config_image_data_format()`. +#' +#' @family image ops +#' @family image utils +#' @family ops +#' @export +#' @tether keras.ops.image.hsv_to_rgb +op_image_hsv_to_rgb <- +function (images, data_format = NULL) { + keras$ops$image$hsv_to_rgb(images, data_format) +} + +#' Convert RGB images to HSV. +#' +#' @description +#' `images` must be of float dtype, and the output is only well defined if the +#' values in `images` are in `[0, 1]`. +#' +#' All HSV values are in `[0, 1]`. A hue of `0` corresponds to pure red, `1/3` +#' is pure green, and `2/3` is pure blue. +#' +#' # Examples +#' ```{r} +#' x <- random_uniform(c(2, 4, 4, 3)) +#' y <- op_image_rgb_to_hsv(x) +#' shape(y) +#' ``` +#' +#' ```{r} +#' x <- random_uniform(c(4, 4, 3)) # Single RGB image +#' y <- op_image_rgb_to_hsv(x) +#' shape(y) +#' ``` +#' +#' ```{r} +#' x <- random_uniform(c(2, 3, 4, 4)) +#' y <- op_image_rgb_to_hsv(x, data_format = "channels_first") +#' shape(y) +#' ``` +#' +#' @returns +#' HSV image or batch of HSV images. +#' +#' @param images +#' Input image or batch of images. Must be 3D or 4D. +#' +#' @param data_format +#' A string specifying the data format of the input tensor. +#' It can be either `"channels_last"` or `"channels_first"`. +#' `"channels_last"` corresponds to inputs with shape +#' `(batch, height, width, channels)`, while `"channels_first"` +#' corresponds to inputs with shape `(batch, channels, height, width)`. +#' If not specified, the value will default to +#' `config_image_data_format()`. +#' +#' @family image ops +#' @family image utils +#' @family ops +#' @export +#' @tether keras.ops.image.rgb_to_hsv +op_image_rgb_to_hsv <- +function (images, data_format = NULL) { + keras$ops$image$rgb_to_hsv(images, data_format) +} diff --git a/R/ops-linalg.R b/R/ops-linalg.R index 60a0be5614..67f0f87ef4 100644 --- a/R/ops-linalg.R +++ b/R/ops-linalg.R @@ -273,3 +273,56 @@ keras$ops$svd(x, full_matrices, compute_uv) op_slogdet <- function (x) keras$ops$slogdet(x) + + +#' Return the least-squares solution to a linear matrix equation. +#' +#' @description +#' Computes the vector x that approximately solves the equation +#' `a %*% x = b`. The equation may be under-, well-, or over-determined +#' (i.e., the number of linearly independent rows of a can be less than, +#' equal to, or greater than its number of linearly independent columns). +#' If a is square and of full rank, then `x` (but for round-off error) +#' is the exact solution of the equation. Else, `x` minimizes the +#' L2 norm of `b - a %*% x`. +#' +#' If there are multiple minimizing solutions, +#' the one with the smallest L2 norm is returned. +#' +#' @returns +#' Tensor with shape `(N)` or `(N, K)` containing +#' the least-squares solutions. +#' +#' **NOTE:** The output differs from `numpy.linalg.lstsq()`. +#' NumPy returns a tuple with four elements, the first of which +#' being the least-squares solutions and the others +#' being essentially never used. +#' Keras only returns the first value. This is done both +#' to ensure consistency across backends (which cannot be achieved +#' for the other values) and to simplify the API. +#' +#' @param a +#' "Coefficient" matrix of shape `(M, N)`. +#' +#' @param b +#' Ordinate or "dependent variable" values, +#' of shape `(M)` or `(M, K)`. +#' If `b` is two-dimensional, the least-squares solution +#' is calculated for each of the K columns of `b`. +#' +#' @param rcond +#' Cut-off ratio for small singular values of `a`. +#' For the purposes of rank determination, +#' singular values are treated as zero if they are +#' smaller than rcond times the largest +#' singular value of `a`. +#' +#' +#' @family linear algebra ops +#' @family numpy ops +#' @family ops +#' @export +#' @tether keras.ops.lstsq +op_lstsq <- +function (a, b, rcond = NULL) +keras$ops$lstsq(a, b, rcond) diff --git a/R/ops-numpy.R b/R/ops-numpy.R index d7636cea62..58d58fdecd 100644 --- a/R/ops-numpy.R +++ b/R/ops-numpy.R @@ -18,3 +18,40 @@ op_divide_no_nan <- function (x1, x2) keras$ops$divide_no_nan(x1, x2) + + +#' Performs an indirect partition along the given axis. +#' +#' @description +#' It returns an array +#' of indices of the same shape as `x` that index data along the given axis +#' in partitioned order. +#' +#' @returns +#' Array of indices that partition `x` along the specified `axis`. +#' +#' @param x +#' Array to sort. +#' +#' @param kth +#' Element index to partition by. +#' The k-th element will be in its final sorted position and all +#' smaller elements will be moved before it and all larger elements +#' behind it. The order of all elements in the partitions is undefined. +#' If provided with a sequence of k-th it will partition all of them +#' into their sorted position at once. +#' +#' @param axis +#' Axis along which to sort. The default is -1 (the last axis). +#' If `NULL`, the flattened array is used. +#' +#' @export +#' @family numpy ops +#' @family ops +#' @tether keras.ops.argpartition +op_argpartition <- +function (x, kth, axis = -1L) +{ + args <- capture_args(list(axis = as_axis)) + do.call(keras$ops$argpartition, args) +} diff --git a/R/ops.R b/R/ops.R index fbaaddbe22..67caf86789 100644 --- a/R/ops.R +++ b/R/ops.R @@ -4032,8 +4032,8 @@ function (x1, x2, axisa = -1L, axisb = -1L, axisc = -1L, axis = NULL) #' labels in the output. Defaults to `TRUE`. #' #' @param mask_index -#' An integer scalar, the index of the mask character in -#' the vocabulary. Defaults to `NULL`. +#' An integer scalar, the (0-based) index of the mask character in +#' the vocabulary. Defaults to `0`. #' #' @export #' @family numpy ops @@ -4041,7 +4041,7 @@ function (x1, x2, axisa = -1L, axisb = -1L, axisc = -1L, axis = NULL) #' @tether keras.ops.ctc_decode op_ctc_decode <- function (inputs, sequence_lengths, strategy = "greedy", beam_width = 100L, - top_paths = 1L, merge_repeated = TRUE, mask_index = NULL) + top_paths = 1L, merge_repeated = TRUE, mask_index = 0L) { args <- capture_args(list( sequence_lengths = as_integer_array, @@ -4916,6 +4916,15 @@ keras$ops$imag(x) #' @param x2 #' Second input tensor. #' +#' @param rtol +#' Relative tolerance. +#' +#' @param atol +#' Absolute tolerance. +#' +#' @param equal_nan +#' If `TRUE`, element-wise `NaN`s are considered equal. +#' #' @export #' @family numpy ops #' @family ops @@ -4924,8 +4933,13 @@ keras$ops$imag(x) # + #' @tether keras.ops.isclose op_isclose <- -function (x1, x2) -keras$ops$isclose(x1, x2) +function (x1, x2, + rtol = 1e-05, + atol = 1e-08, + equal_nan = FALSE) { + args <- capture_args() + do.call(keras$ops$isclose, args) +} #' Return whether a tensor is finite, element-wise. @@ -7418,3 +7432,230 @@ keras$ops$hard_swish(x) op_custom_gradient <- function (f) keras$ops$custom_gradient(f) + + +#' Return the dtype of the tensor input as a standardized string. +#' +#' @description +#' Note that due to the standardization, the dtype will not compare equal +#' to the backend-specific version of the dtype. +#' +#' # Examples +#' ```{r} +#' x <- op_zeros(c(8, 12)) +#' op_dtype(x) +#' ``` +#' +#' @returns +#' A string indicating the dtype of the input tensor, e.g. `"float32"`. +#' +#' @param x +#' A tensor. This function will try to access the `dtype` attribute of +#' the input tensor. +#' +#' @family core ops +#' @family ops +#' @export +#' @tether keras.ops.dtype +op_dtype <- function(x) keras$ops$dtype(x) + + +#' Map a function over leading array axes. +#' +#' @description +#' Like `purrr::map()` or `base::lapply()`, except inputs and outputs are in the form of +#' stacked arrays. Consider using the `op_vectorized_map()` transform instead, +#' unless you need to apply a function element by element for reduced memory +#' usage or heterogeneous computation with other control flow primitives. +#' +#' When `xs` is an array type, the semantics of `op_map()` match this +#' implementation: +#' +#' ```r +#' op_map <- function(xs, f) { +#' xs |> +#' op_unstack() |> +#' lapply(f) |> +#' op_stack() +#' } +#' ``` +#' +#' # Examples +#' ```{r} +#' f <- function(x) x^2 +#' xs <- op_arange(10) +#' ys <- op_map(xs, f) +#' ys +#' ``` +#' +#' ```{r} +#' f <- function(x) list(y1 = x^2, y2 = x * 10) # Can have nested outputs +#' ys <- op_map(xs, f) +#' ys$y1 +#' ys$y2 +#' ``` +#' +#' @returns +#' Mapped values. +#' +#' @param f +#' Callable defines the function to apply element-wise over the first +#' axis or axes of `xs`. +#' +#' @param xs +#' Values over which to map along the leading axis. +#' +#' @family core ops +#' @family ops +#' @export +#' @tether keras.ops.map +op_map <- +function (xs, f) +keras$ops$map(f, xs) + + +#' Scan a function over leading array axes while carrying along state. +#' +#' @description +#' When the type of `xs` is an array type or `NULL`, and the type of `ys` is an +#' array type, the semantics of `op_scan()` are given roughly by this +#' implementation: +#' +#' ```r +#' op_scan <- function(f, init, xs = NULL, length = NULL) { +#' xs <- xs %||% vector("list", length) +#' if(!is.list(xs)) +#' xs <- op_unstack(xs) +#' ys <- vector("list", length(xs)) +#' carry <- init +#' for (i in seq_along(xs)) { +#' c(carry, y) %<-% f(carry, xs[[i]]) +#' ys[[i]] <- y +#' } +#' list(carry, op_stack(ys)) +#' } +#' ``` +#' +#' The loop-carried value `carry` (`init`) must hold a fixed shape and dtype +#' across all iterations. +#' +#' In TensorFlow, `y` must match `carry` in shape and dtype. This is not +#' required in other backends. +#' +#' # Examples +#' ```{r} +#' sum_fn <- function(c, x) list(c + x, c + x) +#' init <- op_array(0L) +#' xs <- op_array(1:5) +#' c(carry, result) %<-% op_scan(sum_fn, init, xs) +#' carry +#' result +#' ``` +#' +#' @returns +#' A pair where the first element represents the final loop carry value and +#' the second element represents the stacked outputs of `f` when scanned +#' over the leading axis of the inputs. +#' +#' @param f +#' Callable defines the logic for each loop iteration. This accepts two +#' arguments where the first is a value of the loop carry and the +#' second is a slice of `xs` along its leading axis. +#' This callable returns a pair where the first represents a new value +#' for the loop carry and the second represents a slice of the output. +#' +#' @param init +#' The initial loop carry value. This can be a scalar, tensor, or any +#' nested structure. It must match the structure of the first element +#' returned by `f`. +#' +#' @param xs +#' Optional value to scan along its leading axis. This can be a tensor +#' or any nested structure. If `xs` is not provided, you must specify +#' `length` to define the number of loop iterations. +#' Defaults to `NULL`. +#' +#' @param length +#' Optional integer specifying the number of loop iterations. +#' If `length` is not provided, it defaults to the sizes of leading +#' axis of the arrays in `xs`. Defaults to `NULL`. +#' +#' @param reverse +#' Optional boolean specifying whether to run the scan iteration +#' forward or in reverse, equivalent to reversing the leading axes of +#' the arrays in both `xs` and in `ys`. +#' +#' @param unroll +#' Optional positive integer or boolean specifying how many scan +#' iterations to unroll within a single iteration of a loop. If an +#' integer is provided, it determines how many unrolled loop iterations +#' to run within a single rolled iteration of the loop. If a boolean is +#' provided, it will determine if the loop is completely unrolled +#' (`unroll=TRUE`) or left completely unrolled (`unroll=FALSE`). +#' Note that unrolling is only supported by JAX and TensorFlow +#' backends. +#' +#' @family core ops +#' @family ops +#' @export +#' @tether keras.ops.scan +#' @seealso +#' + +op_scan <- +function (f, init, xs = NULL, length = NULL, reverse = FALSE, unroll = 1L) +{ + args <- capture_args(list(length = as_integer, unroll = as_integer)) + do.call(keras$ops$scan, args) +} + + +#' Apply exactly one of the `branches` given by `index`. +#' +#' @description +#' If `index` is out of bounds, it is clamped to within bounds. +#' +#' The semantics of `switch` are given roughly by this implementation: +#' +#' ```r +#' op_switch <- function(index, branches, ...) { +#' index <- op_clip(index, 1, length(branches)) +#' branches[[index]](...) +#' } +#' ``` +#' +#' # Examples +#' ```{r} +#' add_fn <- function(x, y) x + y +#' substract_fn <- function(x, y) x - y +#' x <- op_array(2.0) +#' y <- op_array(0.5) +#' branches <- list(add_fn, substract_fn) +#' op_switch(1, branches, x, y) +#' op_switch(2, branches, x, y) +#' ``` +#' +#' @returns +#' The outputs of `branch(...)` for the branch that was selected +#' based on `index`. +#' +#' @param index +#' An integer scalar indicating which branch function to apply (1-based). +#' +#' @param branches +#' A list of functions to be applied based on `index`. +#' +#' @param ... +#' Inputs to whichever branch is applied. +#' +#' @family core ops +#' @family ops +#' @export +#' @tether keras.ops.switch +op_switch <- +function (index, branches, ...) +{ + if (!is.null(names(list(...)))) + stop("Arguments supplied to ... must be unnamed") + index <- op_convert_to_tensor(index, "int32") - 1L + keras$ops$switch(index, branches, ...) +} diff --git a/R/r-utils.R b/R/r-utils.R index 2962306d0d..b76faec2e1 100644 --- a/R/r-utils.R +++ b/R/r-utils.R @@ -118,6 +118,38 @@ rename <- function(x, ..., .skip_existing = TRUE) { x } +# arg_to_positional <- function(args, name, pos = 1L) { +# idx <- match(name, names(args)) +# if(is.na(idx)) +# return(args) +# arg <- args[idx] +# args <- args[-idx] +# +# head <- args[seq_len(pos-1)] +# tail <- args[seq.int(from = pos, to = length(args))] +# if((length(head)+1) != pos) +# stop("Not enough argument supplied") +# names(arg) <- NULL +# c(head, arg, tail) +# } + + +args_to_positional <- function(args, nms) { + idxs <- match(nms, names(args)) + if(all(is.na(idxs))) + return(args) + if(any(is.na(idxs))) + stop("Required positional arguments not supplied: ", + paste0(nms, collapse = ", ")) + + positional_args <- args[idxs] + names(positional_args) <- NULL + + named_args <- args[-idxs] + c(positional_args, named_args) +} + + `%""%` <- function (x, y) { if(!is.character(x)) stop("x must be character") diff --git a/R/shape.R b/R/shape.R index 26562d504e..e81ebf44ff 100644 --- a/R/shape.R +++ b/R/shape.R @@ -163,7 +163,7 @@ shape <- function(...) { #' @export #' @rdname shape -#' @param x A `keras_shape` object. +#' @param x,y A `keras_shape` object. #' @param prefix Whether to format the shape object with a prefix. Defaults to #' `"shape"`. format.keras_shape <- function(x, ..., prefix = TRUE) { @@ -210,6 +210,23 @@ destructure.keras_shape <- function(x) unclass(x) #' @export as.list.keras_shape <- function(x, ...) unclass(x) +#' @rdname shape +#' @export +`==.keras_shape` <- function(x, y) { + if(!inherits(x, "keras_shape")) + x <- shape(x) + if(!inherits(y, "keras_shape")) + y <- shape(y) + identical(x, y) +} + +#' @rdname shape +#' @export +`!=.keras_shape` <- function(x, y) { + !`==.keras_shape`(x, y) +} + + # ' @rdname shape # ' @export # c.keras_shape <- function(...) shape(...) diff --git a/docs/dev/LICENSE-text.html b/docs/dev/LICENSE-text.html new file mode 100644 index 0000000000..ac12d9cb1f --- /dev/null +++ b/docs/dev/LICENSE-text.html @@ -0,0 +1,94 @@ + +License • keras3 + Skip to contents + + +
+
+
+ +
YEAR: 2024
+COPYRIGHT HOLDER: Posit Software, PBC; Google, Inc; François Chollet; Yuan Tang
+
+ +
+ + +
+ + + +
+ + + + + + + diff --git a/docs/dev/apple-touch-icon-120x120.png b/docs/dev/apple-touch-icon-120x120.png new file mode 100644 index 0000000000..869a05b302 Binary files /dev/null and b/docs/dev/apple-touch-icon-120x120.png differ diff --git a/docs/dev/apple-touch-icon-152x152.png b/docs/dev/apple-touch-icon-152x152.png new file mode 100644 index 0000000000..44c0cbd718 Binary files /dev/null and b/docs/dev/apple-touch-icon-152x152.png differ diff --git a/docs/dev/apple-touch-icon-180x180.png b/docs/dev/apple-touch-icon-180x180.png new file mode 100644 index 0000000000..e8134bde6a Binary files /dev/null and b/docs/dev/apple-touch-icon-180x180.png differ diff --git a/docs/dev/apple-touch-icon-60x60.png b/docs/dev/apple-touch-icon-60x60.png new file mode 100644 index 0000000000..12607c7547 Binary files /dev/null and b/docs/dev/apple-touch-icon-60x60.png differ diff --git a/docs/dev/apple-touch-icon-76x76.png b/docs/dev/apple-touch-icon-76x76.png new file mode 100644 index 0000000000..9ca916caa8 Binary files /dev/null and b/docs/dev/apple-touch-icon-76x76.png differ diff --git a/docs/dev/apple-touch-icon.png b/docs/dev/apple-touch-icon.png new file mode 100644 index 0000000000..db1aa9c2d3 Binary files /dev/null and b/docs/dev/apple-touch-icon.png differ diff --git a/docs/dev/articles/custom_train_step_in_tensorflow.html b/docs/dev/articles/custom_train_step_in_tensorflow.html new file mode 100644 index 0000000000..bf748d9125 --- /dev/null +++ b/docs/dev/articles/custom_train_step_in_tensorflow.html @@ -0,0 +1,614 @@ + + + + + + + + +Customizing what happens in `fit()` with TensorFlow • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

When you’re doing supervised learning, you can use fit() +and everything works smoothly.

+

When you need to take control of every little detail, you can write +your own training loop entirely from scratch.

+

But what if you need a custom training algorithm, but you still want +to benefit from the convenient features of fit(), such as +callbacks, built-in distribution support, or step fusing?

+

A core principle of Keras is progressive disclosure of +complexity. You should always be able to get into lower-level +workflows in a gradual way. You shouldn’t fall off a cliff if the +high-level functionality doesn’t exactly match your use case. You should +be able to gain more control over the small details while retaining a +commensurate amount of high-level convenience.

+

When you need to customize what fit() does, you should +override the training step function of the Model +class. This is the function that is called by +fit() for every batch of data. You will then be able to +call fit() as usual – and it will be running your own +learning algorithm.

+

Note that this pattern does not prevent you from building models with +the Functional API. You can do this whether you’re building +Sequential models, Functional API models, or subclassed +models.

+

Let’s see how that works.

+
+
+

Setup +

+
+library(reticulate)
+library(tensorflow, exclude = c("set_random_seed", "shape"))
+library(keras3)
+
+
+

A first simple example +

+

Let’s start from a simple example:

+
    +
  • We create a new class that subclasses Model.
  • +
  • We just override the method +train_step(self, data).
  • +
  • We return a dictionary mapping metric names (including the loss) to +their current value.
  • +
+

The input argument data is what gets passed to fit as +training data:

+
    +
  • If you pass arrays, by calling fit(x, y, ...), then +data will be the list (x, y) +
  • +
  • If you pass a tf_dataset, by calling +fit(dataset, ...), then data will be what gets +yielded by dataset at each batch.
  • +
+

In the body of the train_step() method, we implement a +regular training update, similar to what you are already familiar with. +Importantly, we compute the loss via +self.compute_loss(), which wraps the loss(es) +function(s) that were passed to compile().

+

Similarly, we call metric$update_state(y, y_pred) on +metrics from self$metrics, to update the state of the +metrics that were passed in compile(), and we query results +from self$metrics at the end to retrieve their current +value.

+
+CustomModel <- new_model_class(
+  "CustomModel",
+  train_step = function(data) {
+    c(x, y = NULL, sample_weight = NULL) %<-% data
+
+    with(tf$GradientTape() %as% tape, {
+      y_pred <- self(x, training = TRUE)
+      loss <- self$compute_loss(y = y, y_pred = y_pred,
+                                sample_weight = sample_weight)
+    })
+
+    # Compute gradients
+    trainable_vars <- self$trainable_variables
+    gradients <- tape$gradient(loss, trainable_vars)
+
+    # Update weights
+    self$optimizer$apply(gradients, trainable_vars)
+
+    # Update metrics (includes the metric that tracks the loss)
+    for (metric in self$metrics) {
+      if (metric$name == "loss")
+        metric$update_state(loss)
+      else
+        metric$update_state(y, y_pred)
+    }
+
+    # Return a dict mapping metric names to current value
+    metrics <- lapply(self$metrics, function(m) m$result())
+    metrics <- setNames(metrics, sapply(self$metrics, function(m) m$name))
+    metrics
+  }
+)
+

Let’s try this out:

+
+# Construct and compile an instance of CustomModel
+inputs <- keras_input(shape = 32)
+outputs <- layer_dense(inputs, 1)
+model <- CustomModel(inputs, outputs)
+model |> compile(optimizer = "adam", loss = "mse", metrics = "mae")
+
+# Just use `fit` as usual
+x <- random_normal(c(1000, 32))
+y <- random_normal(c(1000, 1))
+model |> fit(x, y, epochs = 3)
+
## Epoch 1/3
+## 32/32 - 1s - 24ms/step - loss: 2.9118 - mae: 1.3597
+## Epoch 2/3
+## 32/32 - 0s - 1ms/step - loss: 2.6026 - mae: 1.2856
+## Epoch 3/3
+## 32/32 - 0s - 1ms/step - loss: 2.3378 - mae: 1.2193
+
+
+

Going lower-level +

+

Naturally, you could just skip passing a loss function in +compile(), and instead do everything manually in +train_step. Likewise for metrics.

+

Here’s a lower-level example, that only uses compile() +to configure the optimizer:

+
    +
  • We start by creating Metric instances to track our loss +and a MAE score (in __init__()).
  • +
  • We implement a custom train_step() that updates the +state of these metrics (by calling update_state() on them), +then query them (via result()) to return their current +average value, to be displayed by the progress bar and to be pass to any +callback.
  • +
  • Note that we would need to call reset_states() on our +metrics between each epoch! Otherwise calling result() +would return an average since the start of training, whereas we usually +work with per-epoch averages. Thankfully, the framework can do that for +us: just list any metric you want to reset in the metrics +property of the model. The model will call reset_states() +on any object listed here at the beginning of each fit() +epoch or at the beginning of a call to evaluate().
  • +
+
+CustomModel <- new_model_class(
+  "CustomModel",
+  initialize = function(...) {
+    super$initialize(...)
+    self$loss_tracker <- metric_mean(name = "loss")
+    self$mae_metric <- metric_mean_absolute_error(name = "mae")
+    self$loss_fn <- loss_mean_squared_error()
+  },
+  train_step = function(data) {
+    c(x, y = NULL, sample_weight = NULL) %<-% data
+
+    with(tf$GradientTape() %as% tape, {
+      y_pred <- self(x, training = TRUE)
+      loss <- self$loss_fn(y, y_pred, sample_weight = sample_weight)
+    })
+
+    # Compute gradients
+    trainable_vars <- self$trainable_variables
+    gradients <- tape$gradient(loss, trainable_vars)
+
+    # Update weights
+    self$optimizer$apply(gradients, trainable_vars)
+
+    # Compute our own metrics
+    self$loss_tracker$update_state(loss)
+    self$mae_metric$update_state(y, y_pred)
+
+    # Return a dict mapping metric names to current value
+    list(
+      loss = self$loss_tracker$result(),
+      mae = self$mae_metric$result()
+    )
+  },
+  metrics = mark_active(function() {
+    # We list our `Metric` objects here so that `reset_states()` can be
+    # called automatically at the start of each epoch
+    # or at the start of `evaluate()`.
+    list(self$loss_tracker, self$mae_metric)
+  })
+)
+
+
+# Construct and compile an instance of CustomModel
+inputs <- keras_input(shape = 32)
+outputs <- layer_dense(inputs, 1)
+model <- CustomModel(inputs, outputs)
+
+# We don't pass a loss or metrics here.
+model |> compile(optimizer = "adam")
+
+# Just use `fit` as usual
+x <- random_normal(c(1000, 32))
+y <- random_normal(c(1000, 1))
+model |> fit(x, y, epochs = 3)
+
## Epoch 1/3
+## 32/32 - 1s - 17ms/step - loss: 2.6540 - mae: 1.2901
+## Epoch 2/3
+## 32/32 - 0s - 1ms/step - loss: 2.4139 - mae: 1.2303
+## Epoch 3/3
+## 32/32 - 0s - 1ms/step - loss: 2.2080 - mae: 1.1761
+
+
+

Supporting sample_weight & +class_weight +

+

You may have noticed that our first basic example didn’t make any +mention of sample weighting. If you want to support the +fit() arguments sample_weight and +class_weight, you’d simply do the following:

+
    +
  • Unpack sample_weight from the data +argument
  • +
  • Pass it to compute_loss & update_state +(of course, you could also just apply it manually if you don’t rely on +compile() for losses & metrics)
  • +
  • That’s it.
  • +
+
+CustomModel <- new_model_class(
+  "CustomModel",
+  train_step = function(data) {
+    c(x, y = NULL, sample_weight = NULL) %<-% data
+
+    with(tf$GradientTape() %as% tape, {
+      y_pred <- self(x, training = TRUE)
+      loss <- self$compute_loss(y = y, y_pred = y_pred,
+                                sample_weight = sample_weight)
+    })
+
+    # Compute gradients
+    trainable_vars <- self$trainable_variables
+    gradients <- tape$gradient(loss, trainable_vars)
+
+    # Update weights
+    self$optimizer$apply_gradients(zip_lists(gradients, trainable_vars))
+
+    # Update metrics (includes the metric that tracks the loss)
+    for (metric in self$metrics) {
+      if (metric$name == "loss") {
+        metric$update_state(loss)
+      } else {
+        metric$update_state(y, y_pred, sample_weight = sample_weight)
+      }
+    }
+
+    # Return a dict mapping metric names to current value
+    metrics <- lapply(self$metrics, function(m) m$result())
+    metrics <- setNames(metrics, sapply(self$metrics, function(m) m$name))
+    metrics
+  }
+)
+
+
+# Construct and compile an instance of CustomModel
+inputs <- keras_input(shape = 32)
+outputs <- layer_dense(inputs, units = 1)
+model <- CustomModel(inputs, outputs)
+model |> compile(optimizer = "adam", loss = "mse", metrics = "mae")
+
+# You can now use sample_weight argument
+x <- random_normal(c(1000, 32))
+y <- random_normal(c(1000, 1))
+sw <- random_normal(c(1000, 1))
+model |> fit(x, y, sample_weight = sw, epochs = 3)
+
## Epoch 1/3
+## 32/32 - 1s - 19ms/step - loss: 0.1607 - mae: 1.3018
+## Epoch 2/3
+## 32/32 - 0s - 1ms/step - loss: 0.1452 - mae: 1.2999
+## Epoch 3/3
+## 32/32 - 0s - 1ms/step - loss: 0.1335 - mae: 1.2986
+
+
+

Providing your own evaluation step +

+

What if you want to do the same for calls to +model.evaluate()? Then you would override +test_step in exactly the same way. Here’s what it looks +like:

+
+CustomModel <- new_model_class(
+  "CustomModel",
+  test_step = function(data) {
+    # Unpack the data
+    c(x, y = NULL, sw = NULL) %<-% data
+    # Compute predictions
+    y_pred = self(x, training = FALSE)
+    # Updates the metrics tracking the loss
+    self$compute_loss(y = y, y_pred = y_pred, sample_weight = sw)
+    # Update the metrics.
+    for (metric in self$metrics) {
+      if (metric$name != "loss") {
+        metric$update_state(y, y_pred, sample_weight = sw)
+      }
+    }
+    # Return a dict mapping metric names to current value.
+    # Note that it will include the loss (tracked in self.metrics).
+    metrics <- lapply(self$metrics, function(m) m$result())
+    metrics <- setNames(metrics, sapply(self$metrics, function(m) m$name))
+    metrics
+  }
+)
+
+# Construct an instance of CustomModel
+inputs <- keras_input(shape = 32)
+outputs <- layer_dense(inputs, 1)
+model <- CustomModel(inputs, outputs)
+model |> compile(loss = "mse", metrics = "mae")
+
+# Evaluate with our custom test_step
+x <- random_normal(c(1000, 32))
+y <- random_normal(c(1000, 1))
+model |> evaluate(x, y)
+
## 32/32 - 0s - 9ms/step - loss: 0.0000e+00 - mae: 1.3947
+
## $loss
+## [1] 0
+##
+## $mae
+## [1] 1.394695
+
+
+

Wrapping up: an end-to-end GAN example +

+

Let’s walk through an end-to-end example that leverages everything +you just learned.

+

Let’s consider:

+
    +
  • A generator network meant to generate 28x28x1 images.
  • +
  • A discriminator network meant to classify 28x28x1 images into two +classes (“fake” and “real”).
  • +
  • One optimizer for each.
  • +
  • A loss function to train the discriminator.
  • +
+
+# Create the discriminator
+discriminator <-
+  keras_model_sequential(name = "discriminator", input_shape = c(28, 28, 1)) |>
+
+  layer_conv_2d(filters = 64, kernel_size = c(3, 3),
+                strides = c(2, 2),  padding = "same") |>
+  layer_activation_leaky_relu(negative_slope = 0.2) |>
+
+  layer_conv_2d(filters = 128, kernel_size = c(3, 3),
+                strides = c(2, 2), padding = "same") |>
+  layer_activation_leaky_relu(negative_slope = 0.2) |>
+
+  layer_global_max_pooling_2d() |>
+  layer_dense(units = 1)
+
+
+# Create the generator
+latent_dim <- 128
+generator <-
+  keras_model_sequential(name = "generator", input_shape = latent_dim) |>
+
+  layer_dense(7 * 7 * 128) |>
+  layer_activation_leaky_relu(negative_slope = 0.2) |>
+
+  layer_reshape(target_shape = c(7, 7, 128)) |>
+
+  layer_conv_2d_transpose(filters = 128, kernel_size = c(4, 4),
+                          strides = c(2, 2), padding = "same") |>
+  layer_activation_leaky_relu(negative_slope = 0.2) |>
+
+  layer_conv_2d_transpose(filters = 128, kernel_size = c(4, 4),
+                          strides = c(2, 2), padding = "same") |>
+  layer_activation_leaky_relu(negative_slope = 0.2) |>
+
+  layer_conv_2d(filters = 1, kernel_size = c(7, 7), padding = "same",
+                activation = "sigmoid")
+

Here’s a feature-complete GAN class, overriding +compile() to use its own signature, and implementing the +entire GAN algorithm in 17 lines in train_step:

+
+GAN <- Model(
+  classname = "GAN",
+
+  initialize = function(discriminator, generator, latent_dim, ...) {
+    super$initialize(...)
+    self$discriminator <- discriminator
+    self$generator <- generator
+    self$latent_dim <- as.integer(latent_dim)
+    self$d_loss_tracker <- metric_mean(name = "d_loss")
+    self$g_loss_tracker <- metric_mean(name = "g_loss")
+  },
+
+  compile = function(d_optimizer, g_optimizer, loss_fn, ...) {
+    super$compile(...)
+    self$d_optimizer <- d_optimizer
+    self$g_optimizer <- g_optimizer
+    self$loss_fn <- loss_fn
+  },
+  metrics = active_property(function() {
+    list(self$d_loss_tracker, self$g_loss_tracker)
+  }),
+
+  train_step = function(real_images) {
+
+    # Sample random points in the latent space
+    batch_size <- shape(real_images)[[1]]
+    random_latent_vectors <-
+      tf$random$normal(shape(batch_size, self$latent_dim))
+
+    # Decode them to fake images
+    generated_images <- self$generator(random_latent_vectors)
+
+    # Combine them with real images
+    combined_images <- op_concatenate(list(generated_images,
+                                           real_images))
+
+    # Assemble labels discriminating real from fake images
+    labels <- op_concatenate(list(op_ones(c(batch_size, 1)),
+                                  op_zeros(c(batch_size, 1))))
+
+    # Add random noise to the labels - important trick!
+    labels %<>% `+`(tf$random$uniform(shape(.), maxval = 0.05))
+
+    # Train the discriminator
+    with(tf$GradientTape() %as% tape, {
+      predictions <- self$discriminator(combined_images)
+      d_loss <- self$loss_fn(labels, predictions)
+    })
+    grads <- tape$gradient(d_loss, self$discriminator$trainable_weights)
+    self$d_optimizer$apply_gradients(
+      zip_lists(grads, self$discriminator$trainable_weights))
+
+    # Sample random points in the latent space
+    random_latent_vectors <-
+      tf$random$normal(shape(batch_size, self$latent_dim))
+
+    # Assemble labels that say "all real images"
+    misleading_labels <- op_zeros(c(batch_size, 1))
+
+    # Train the generator (note that we should *not* update the weights
+    # of the discriminator)!
+    with(tf$GradientTape() %as% tape, {
+      predictions <- self$discriminator(self$generator(random_latent_vectors))
+      g_loss <- self$loss_fn(misleading_labels, predictions)
+    })
+    grads <- tape$gradient(g_loss, self$generator$trainable_weights)
+    self$g_optimizer$apply_gradients(
+      zip_lists(grads, self$generator$trainable_weights))
+
+    list(d_loss = d_loss, g_loss = g_loss)
+  }
+)
+

Let’s test-drive it:

+
+batch_size <- 64
+c(c(x_train, .), c(x_test, .)) %<-% dataset_mnist()
+all_digits <- op_concatenate(list(x_train, x_test))
+all_digits <- op_reshape(all_digits, c(-1, 28, 28, 1))
+dataset <- all_digits |>
+  tfdatasets::tensor_slices_dataset() |>
+  tfdatasets::dataset_map(\(x) op_cast(x, "float32") / 255) |>
+  tfdatasets::dataset_shuffle(buffer_size = 1024) |>
+  tfdatasets::dataset_batch(batch_size = batch_size)
+
+gan <- GAN(discriminator = discriminator,
+           generator = generator,
+           latent_dim = latent_dim)
+
+gan |> compile(
+  d_optimizer = optimizer_adam(learning_rate = 0.0003),
+  g_optimizer = optimizer_adam(learning_rate = 0.0003),
+  loss_fn = loss_binary_crossentropy(from_logits = TRUE)
+)
+
+# To limit the execution time, we only train on 100 batches. You can train on
+# the entire dataset. You will need about 20 epochs to get nice results.
+gan |> fit(
+  tfdatasets::dataset_take(dataset, 100),
+  epochs = 1
+)
+
## 100/100 - 5s - 51ms/step - d_loss: 0.0000e+00 - g_loss: 0.0000e+00
+

The ideas behind deep learning are simple, so why should their +implementation be painful?

+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/distributed_training_with_tensorflow.html b/docs/dev/articles/distributed_training_with_tensorflow.html new file mode 100644 index 0000000000..4872356bfb --- /dev/null +++ b/docs/dev/articles/distributed_training_with_tensorflow.html @@ -0,0 +1,415 @@ + + + + + + + + +Multi-GPU distributed training with TensorFlow • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

There are generally two ways to distribute computation across +multiple devices:

+

Data parallelism, where a single model gets +replicated on multiple devices or multiple machines. Each of them +processes different batches of data, then they merge their results. +There exist many variants of this setup, that differ in how the +different model replicas merge results, in whether they stay in sync at +every batch or whether they are more loosely coupled, etc.

+

Model parallelism, where different parts of a single +model run on different devices, processing a single batch of data +together. This works best with models that have a naturally-parallel +architecture, such as models that feature multiple branches.

+

This guide focuses on data parallelism, in particular +synchronous data parallelism, where the different +replicas of the model stay in sync after each batch they process. +Synchronicity keeps the model convergence behavior identical to what you +would see for single-device training.

+

Specifically, this guide teaches you how to use the +tf.distribute API to train Keras models on multiple GPUs, +with minimal changes to your code, on multiple GPUs (typically 2 to 16) +installed on a single machine (single host, multi-device training). This +is the most common setup for researchers and small-scale industry +workflows.

+
+
+

Setup +

+
+library(keras3)
+library(tensorflow, exclude = c("shape", "set_random_seed"))
+library(tfdatasets, exclude = "shape")
+
+
+

Single-host, multi-device synchronous training +

+

In this setup, you have one machine with several GPUs on it +(typically 2 to 16). Each device will run a copy of your model (called a +replica). For simplicity, in what follows, we’ll assume +we’re dealing with 8 GPUs, at no loss of generality.

+

How it works

+

At each step of training:

+
    +
  • The current batch of data (called global batch) is +split into 8 different sub-batches (called local +batches). For instance, if the global batch has 512 samples, +each of the 8 local batches will have 64 samples.
  • +
  • Each of the 8 replicas independently processes a local batch: they +run a forward pass, then a backward pass, outputting the gradient of the +weights with respect to the loss of the model on the local batch.
  • +
  • The weight updates originating from local gradients are efficiently +merged across the 8 replicas. Because this is done at the end of every +step, the replicas always stay in sync.
  • +
+

In practice, the process of synchronously updating the weights of the +model replicas is handled at the level of each individual weight +variable. This is done through a mirrored variable +object.

+

How to use it

+

To do single-host, multi-device synchronous training with a Keras +model, you would use the tf$distribute$MirroredStrategy +API. Here’s how it works:

+
    +
  • Instantiate a MirroredStrategy, optionally configuring +which specific devices you want to use (by default the strategy will use +all GPUs available).
  • +
  • Use the strategy object to open a scope, and within this scope, +create all the Keras objects you need that contain variables. Typically, +that means creating & compiling the model inside +the distribution scope. In some cases, the first call to +fit() may also create variables, so it’s a good idea to put +your fit() call in the scope as well.
  • +
  • Train the model via fit() as usual.
  • +
+

Importantly, we recommend that you use tf.data.Dataset +objects to load data in a multi-device or distributed workflow.

+

Schematically, it looks like this:

+
+# Create a MirroredStrategy.
+strategy <- tf$distribute$MirroredStrategy()
+cat(sprintf('Number of devices: %d\n', strategy$num_replicas_in_sync))
+
+# Open a strategy scope.
+with(startegy$scope(), {
+  # Everything that creates variables should be under the strategy scope.
+  # In general this is only model construction & `compile()`.
+  model <- Model(...)
+  model |> compile(...)
+
+  # Train the model on all available devices.
+  model |> fit(train_dataset, validation_data=val_dataset, ...)
+
+  # Test the model on all available devices.
+  model |> evaluate(test_dataset)
+})
+

Here’s a simple end-to-end runnable example:

+
+get_compiled_model <- function() {
+  inputs <- keras_input(shape = 784)
+  outputs <- inputs |>
+    layer_dense(units = 256, activation = "relu") |>
+    layer_dense(units = 256, activation = "relu") |>
+    layer_dense(units = 10)
+  model <- keras_model(inputs, outputs)
+  model |> compile(
+    optimizer = optimizer_adam(),
+    loss = loss_sparse_categorical_crossentropy(from_logits = TRUE),
+    metrics = list(metric_sparse_categorical_accuracy()),
+
+    # XLA compilation is temporarily disabled due to a bug
+    # https://github.com/keras-team/keras/issues/19005
+    jit_compile = FALSE
+  )
+  model
+}
+
+get_dataset <- function(batch_size = 64) {
+
+  c(c(x_train, y_train), c(x_test, y_test)) %<-% dataset_mnist()
+  x_train <- array_reshape(x_train, c(-1, 784))
+  x_test <- array_reshape(x_test, c(-1, 784))
+
+  # Reserve 10,000 samples for validation.
+  val_i <- sample.int(nrow(x_train), 10000)
+  x_val <- x_train[val_i,]
+  y_val <- y_train[val_i]
+  x_train = x_train[-val_i,]
+  y_train = y_train[-val_i]
+
+  # Prepare the training dataset.
+  train_dataset <- list(x_train, y_train) |>
+    tensor_slices_dataset() |>
+    dataset_batch(batch_size)
+
+  # Prepare the validation dataset.
+  val_dataset <- list(x_val, y_val) |>
+    tensor_slices_dataset() |>
+    dataset_batch(batch_size)
+
+  # Prepare the test dataset.
+  test_dataset <- list(x_test, y_test) |>
+    tensor_slices_dataset() |>
+    dataset_batch(batch_size)
+
+  list(train_dataset, val_dataset, test_dataset)
+}
+
+# Create a MirroredStrategy.
+strategy <- tf$distribute$MirroredStrategy()
+cat(sprintf('Number of devices: %d\n', strategy$num_replicas_in_sync))
+
## Number of devices: 2
+
+# Open a strategy scope.
+with(strategy$scope(), {
+  # Everything that creates variables should be under the strategy scope.
+  # In general this is only model construction & `compile()`.
+  model <- get_compiled_model()
+
+  c(train_dataset, val_dataset, test_dataset) %<-% get_dataset()
+
+  # Train the model on all available devices.
+  model |> fit(train_dataset, epochs = 2, validation_data = val_dataset)
+
+  # Test the model on all available devices.
+  model |> evaluate(test_dataset)
+
+})
+
## Epoch 1/2
+## 782/782 - 7s - 9ms/step - loss: 3.0622 - sparse_categorical_accuracy: 0.8615 - val_loss: 1.1367 - val_sparse_categorical_accuracy: 0.9006
+## Epoch 2/2
+## 782/782 - 4s - 5ms/step - loss: 0.5774 - sparse_categorical_accuracy: 0.9259 - val_loss: 0.6612 - val_sparse_categorical_accuracy: 0.9210
+## 157/157 - 1s - 3ms/step - loss: 0.6729 - sparse_categorical_accuracy: 0.9150
+
## $loss
+## [1] 0.6728871
+##
+## $sparse_categorical_accuracy
+## [1] 0.915
+
+
+

Using callbacks to ensure fault tolerance +

+

When using distributed training, you should always make sure you have +a strategy to recover from failure (fault tolerance). The simplest way +to handle this is to pass ModelCheckpoint callback to +fit(), to save your model at regular intervals (e.g. every +100 batches or every epoch). You can then restart training from your +saved model.

+

Here’s a simple example:

+
+# Prepare a directory to store all the checkpoints.
+checkpoint_dir <- "./ckpt"
+if (!dir.exists(checkpoint_dir)) {
+  dir.create(checkpoint_dir)
+}
+
+make_or_restore_model <- function() {
+  # Either restore the latest model, or create a fresh one
+  # if there is no checkpoint available.
+  checkpoints <- list.files(checkpoint_dir, pattern = "ckpt-.*\\.keras",
+                            full.names = TRUE)
+
+  if (length(checkpoints) > 0) {
+    checkpoint_epochs <- as.integer(sub("ckpt-([0-9]+)\\.keras", "\\1",
+                                        basename(checkpoints)))
+    latest_checkpoint <- checkpoints[which.max(checkpoint_epochs)]
+    load_model(latest_checkpoint)
+  } else {
+    get_compiled_model()
+  }
+}
+
+
+
+run_training <- function(epochs = 1) {
+  # Create a MirroredStrategy.
+  strategy <- tf$distribute$MirroredStrategy()
+
+  # Open a strategy scope and create/restore the model
+  with(strategy$scope(), {
+    model <- make_or_restore_model()
+
+    callbacks <- list(
+      # This callback saves a SavedModel every epoch
+      # We include the current epoch in the folder name.
+      callback_model_checkpoint(
+        filepath = paste0(checkpoint_dir, "/ckpt-{epoch}.keras"),
+        save_freq = "epoch"
+      ))
+
+    model |> fit(
+      train_dataset,
+      epochs = epochs,
+      callbacks = callbacks,
+      validation_data = val_dataset,
+      verbose = 2
+    )
+  })
+}
+
+# Running the first time creates the model
+run_training(epochs = 1)
+
## 782/782 - 3s - 4ms/step - loss: 0.2194 - sparse_categorical_accuracy: 0.9532 - val_loss: 0.3217 - val_sparse_categorical_accuracy: 0.9456
+
+# Calling the same function again will resume from where we left off
+run_training(epochs = 1)
+
## 782/782 - 3s - 4ms/step - loss: 0.1876 - sparse_categorical_accuracy: 0.9584 - val_loss: 0.3629 - val_sparse_categorical_accuracy: 0.9396
+
+
+

+tf$data performance tips +

+

When doing distributed training, the efficiency with which you load +data can often become critical. Here are a few tips to make sure your +tf$data pipelines run as fast as possible.

+

Note about dataset batching

+

When creating your dataset, make sure it is batched with the global +batch size. For instance, if each of your 8 GPUs is capable of running a +batch of 64 samples, you call use a global batch size of 512.

+

Calling dataset_cache()

+

If you call dataset_cache() on a dataset, its data will +be cached after running through the first iteration over the data. Every +subsequent iteration will use the cached data. The cache can be in +memory (default) or to a local file you specify.

+

This can improve performance when:

+
    +
  • Your data is not expected to change from iteration to iteration
  • +
  • You are reading data from a remote distributed filesystem
  • +
  • You are reading data from local disk, but your data would fit in +memory and your workflow is significantly IO-bound (e.g. reading & +decoding image files).
  • +
+

Calling +dataset_prefetch(buffer_size)

+

You should almost always call +dataset_prefetch(buffer_size) after creating a dataset. It +means your data pipeline will run asynchronously from your model, with +new samples being preprocessed and stored in a buffer while the current +batch samples are used to train the model. The next batch will be +prefetched in GPU memory by the time the current batch is over.

+

That’s it!

+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/distribution.html b/docs/dev/articles/distribution.html new file mode 100644 index 0000000000..7a636b8940 --- /dev/null +++ b/docs/dev/articles/distribution.html @@ -0,0 +1,430 @@ + + + + + + + + +Distributed training with Keras 3 • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

The Keras distribution API is a new interface designed to facilitate +distributed deep learning across a variety of backends like JAX, +TensorFlow and PyTorch. This powerful API introduces a suite of tools +enabling data and model parallelism, allowing for efficient scaling of +deep learning models on multiple accelerators and hosts. Whether +leveraging the power of GPUs or TPUs, the API provides a streamlined +approach to initializing distributed environments, defining device +meshes, and orchestrating the layout of tensors across computational +resources. Through classes like DataParallel and +ModelParallel, it abstracts the complexity involved in +parallel computation, making it easier for developers to accelerate +their machine learning workflows.

+
+
+

How it works +

+

The Keras distribution API provides a global programming model that +allows developers to compose applications that operate on tensors in a +global context (as if working with a single device) while automatically +managing distribution across many devices. The API leverages the +underlying framework (e.g. JAX) to distribute the program and tensors +according to the sharding directives through a procedure called single +program, multiple data (SPMD) expansion.

+

By decoupling the application from sharding directives, the API +enables running the same application on a single device, multiple +devices, or even multiple clients, while preserving its global +semantics.

+
+
+

Setup +

+
+# This guide assumes there are 8 GPUs available for testing. If you don't have
+# 8 gpus available locally, you can set the following envvar to
+# make xla initialize the CPU as 8 devices, to enable local testing
+Sys.setenv("CUDA_VISIBLE_DEVICES" = "")
+Sys.setenv("XLA_FLAGS" = "--xla_force_host_platform_device_count=8")
+
+library(keras3)
+
+# The distribution API is only implemented for the JAX backend for now.
+use_backend("jax")
+jax <- reticulate::import("jax")
+
+library(tfdatasets, exclude = "shape") # For dataset input.
+
+
+

+DeviceMesh and TensorLayout +

+

The keras$distribution$DeviceMesh class in Keras +distribution API represents a cluster of computational devices +configured for distributed computation. It aligns with similar concepts +in jax.sharding.Mesh +and tf.dtensor.Mesh, +where it’s used to map the physical devices to a logical mesh +structure.

+

The TensorLayout class then specifies how tensors are +distributed across the DeviceMesh, detailing the sharding +of tensors along specified axes that correspond to the names of the axes +in the DeviceMesh.

+

You can find more detailed concept explainers in the TensorFlow +DTensor guide.

+
+# Retrieve the local available gpu devices.
+devices <- jax$devices() # "gpu"
+str(devices)
+
## List of 8
+##  $ :TFRT_CPU_0
+##  $ :TFRT_CPU_1
+##  $ :TFRT_CPU_2
+##  $ :TFRT_CPU_3
+##  $ :TFRT_CPU_4
+##  $ :TFRT_CPU_5
+##  $ :TFRT_CPU_6
+##  $ :TFRT_CPU_7
+
+# Define a 2x4 device mesh with data and model parallel axes
+mesh <- keras$distribution$DeviceMesh(
+  shape = shape(2, 4),
+  axis_names = list("data", "model"),
+  devices = devices
+)
+
+# A 2D layout, which describes how a tensor is distributed across the
+# mesh. The layout can be visualized as a 2D grid with "model" as rows and
+# "data" as columns, and it is a [4, 2] grid when it mapped to the physical
+# devices on the mesh.
+layout_2d <- keras$distribution$TensorLayout(
+  axes = c("model", "data"),
+  device_mesh = mesh
+)
+
+# A 4D layout which could be used for data parallelism of an image input.
+replicated_layout_4d <- keras$distribution$TensorLayout(
+  axes = list("data", NULL, NULL, NULL),
+  device_mesh = mesh
+)
+
+
+

Distribution +

+

The Distribution class in Keras serves as a foundational +abstract class designed for developing custom distribution strategies. +It encapsulates the core logic needed to distribute a model’s variables, +input data, and intermediate computations across a device mesh. As an +end user, you won’t have to interact directly with this class, but its +subclasses like DataParallel or +ModelParallel.

+
+
+

DataParallel +

+

The DataParallel class in the Keras distribution API is +designed for the data parallelism strategy in distributed training, +where the model weights are replicated across all devices in the +DeviceMesh, and each device processes a portion of the +input data.

+

Here is a sample usage of this class.

+
+# Create DataParallel with list of devices.
+# As a shortcut, the devices can be skipped,
+# and Keras will detect all local available devices.
+# E.g. data_parallel <- DataParallel()
+data_parallel <- keras$distribution$DataParallel(devices = devices)
+
+# Or you can choose to create DataParallel with a 1D `DeviceMesh`.
+mesh_1d <- keras$distribution$DeviceMesh(
+  shape = shape(8),
+  axis_names = list("data"),
+  devices = devices
+)
+data_parallel <- keras$distribution$DataParallel(device_mesh = mesh_1d)
+
+inputs <- random_normal(c(128, 28, 28, 1))
+labels <- random_normal(c(128, 10))
+dataset <- tensor_slices_dataset(c(inputs, labels)) |>
+  dataset_batch(16)
+
+# Set the global distribution.
+keras$distribution$set_distribution(data_parallel)
+
+# Note that all the model weights from here on are replicated to
+# all the devices of the `DeviceMesh`. This includes the RNG
+# state, optimizer states, metrics, etc. The dataset fed into `model |> fit()` or
+# `model |> evaluate()` will be split evenly on the batch dimension, and sent to
+# all the devices. You don't have to do any manual aggregation of losses,
+# since all the computation happens in a global context.
+inputs <- keras_input(shape = c(28, 28, 1))
+outputs <- inputs |>
+  layer_flatten() |>
+  layer_dense(units = 200, use_bias = FALSE, activation = "relu") |>
+  layer_dropout(0.4) |>
+  layer_dense(units = 10, activation = "softmax")
+
+model <- keras_model(inputs = inputs, outputs = outputs)
+
+model |> compile(loss = "mse")
+model |> fit(dataset, epochs = 3)
+
## Epoch 1/3
+## 8/8 - 0s - 37ms/step - loss: 1.0629
+## Epoch 2/3
+## 8/8 - 0s - 5ms/step - loss: 0.9712
+## Epoch 3/3
+## 8/8 - 0s - 5ms/step - loss: 0.9322
+
+model |> evaluate(dataset)
+
## 8/8 - 0s - 7ms/step - loss: 0.8859
+
## $loss
+## [1] 0.8858577
+
+
+

+ModelParallel and LayoutMap +

+

ModelParallel will be mostly useful when model weights +are too large to fit on a single accelerator. This setting allows you to +spit your model weights or activation tensors across all the devices on +the DeviceMesh, and enable the horizontal scaling for the +large models.

+

Unlike the DataParallel model where all weights are +fully replicated, the weights layout under ModelParallel +usually need some customization for best performances. We introduce +LayoutMap to let you specify the TensorLayout +for any weights and intermediate tensors from global perspective.

+

LayoutMap is a dict-like object that maps a string to +TensorLayout instances. It behaves differently from a +normal dict in that the string key is treated as a regex when retrieving +the value. The class allows you to define the naming schema of +TensorLayout and then retrieve the corresponding +TensorLayout instance. Typically, the key used to query is +the variable$path attribute, which is the identifier of the +variable. As a shortcut, a list of axis names is also allowed when +inserting a value, and it will be converted to +TensorLayout.

+

The LayoutMap can also optionally contain a +DeviceMesh to populate the +TensorLayout$device_mesh if it is not set. When retrieving +a layout with a key, and if there isn’t an exact match, all existing +keys in the layout map will be treated as regex and matched against the +input key again. If there are multiple matches, a +ValueError is raised. If no matches are found, +NULL is returned.

+
+mesh_2d <- keras$distribution$DeviceMesh(
+  shape = shape(2, 4),
+  axis_names = c("data", "model"),
+  devices = devices
+)
+layout_map  <- keras$distribution$LayoutMap(mesh_2d)
+
+# The rule below means that for any weights that match with d1/kernel, it
+# will be sharded with model dimensions (4 devices), same for the d1/bias.
+# All other weights will be fully replicated.
+layout_map["d1/kernel"] <- tuple(NULL, "model")
+layout_map["d1/bias"] <- tuple("model")
+
+# You can also set the layout for the layer output like
+layout_map["d2/output"] <- tuple("data", NULL)
+
+model_parallel <- keras$distribution$ModelParallel(
+  mesh_2d, layout_map, batch_dim_name = "data"
+)
+
+keras$distribution$set_distribution(model_parallel)
+
+inputs <- layer_input(shape = c(28, 28, 1))
+outputs <- inputs |>
+  layer_flatten() |>
+  layer_dense(units = 200, use_bias = FALSE,
+              activation = "relu", name = "d1") |>
+  layer_dropout(0.4) |>
+  layer_dense(units = 10,
+              activation = "softmax",
+              name = "d2")
+
+model <- keras_model(inputs = inputs, outputs = outputs)
+
+# The data will be sharded across the "data" dimension of the method, which
+# has 2 devices.
+model |> compile(loss = "mse")
+model |> fit(dataset, epochs = 3)
+
## Epoch 1/3
+## 8/8 - 0s - 29ms/step - loss: 1.0714
+## Epoch 2/3
+## 8/8 - 0s - 3ms/step - loss: 0.9744
+## Epoch 3/3
+## 8/8 - 0s - 4ms/step - loss: 0.9280
+
+model |> evaluate(dataset)
+
## 8/8 - 0s - 7ms/step - loss: 0.8802
+
## $loss
+## [1] 0.8802156
+

It is also easy to change the mesh structure to tune the computation +between more data parallel or model parallel. You can do this by +adjusting the shape of the mesh. And no changes are needed for any other +code.

+
+full_data_parallel_mesh <- keras$distribution$DeviceMesh(
+  shape = shape(8, 1),
+  axis_names = list("data", "model"),
+  devices = devices
+)
+more_data_parallel_mesh <- keras$distribution$DeviceMesh(
+  shape = shape(4, 2),
+  axis_names = list("data", "model"),
+  devices = devices
+)
+more_model_parallel_mesh <- keras$distribution$DeviceMesh(
+  shape = shape(2, 4),
+  axis_names = list("data", "model"),
+  devices = devices
+)
+full_model_parallel_mesh <- keras$distribution$DeviceMesh(
+  shape = shape(1, 8),
+  axis_names = list("data", "model"),
+  devices = devices
+)
+ +
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/index.html b/docs/dev/articles/examples/index.html new file mode 100644 index 0000000000..b5914fa58a --- /dev/null +++ b/docs/dev/articles/examples/index.html @@ -0,0 +1,242 @@ + + + + + + + + +Keras examples • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+ +

Structured Data

+
+
+
+
Imbalanced classification: credit card fraud detection
+
+basic +

Demonstration of how to handle highly imbalanced classification problems.

+See code +
+
+
+
+
+
Structured data classification with FeatureSpace
+
+basic +

Classify tabular data in a few lines of code.

+See code +
+
+
+
+ +

Text

+
+
+
+
English-to-Spanish translation with a sequence-to-sequence Transformer
+
+intermediate +

Implementing a sequence-to-sequence Transformer and training it on a machine translation task.

+See code +
+
+
+
+
+
Text classification from scratch
+
+basic +

Text sentiment classification starting from raw text files.

+See code +
+
+
+
+ +

Timeseries

+
+
+
+
Timeseries anomaly detection using an Autoencoder
+
+intermediate +

Detect anomalies in a timeseries using an Autoencoder.

+See code +
+
+
+
+
+
Timeseries classification from scratch
+
+basic +

Training a timeseries classifier from scratch on the FordA dataset from the UCR/UEA archive.

+See code +
+
+
+
+ +

Vision

+
+
+
+
Convolutional autoencoder for image denoising
+
+basic +

How to train a deep convolutional autoencoder for image denoising.

+See code +
+
+
+
+
+
Simple MNIST convnet
+
+basic +

A simple convnet that achieves ~99% test accuracy on MNIST.

+See code +
+
+
+
+
+
Image segmentation with a U-Net-like architecture
+
+intermediate +

Image segmentation model trained from scratch on the Oxford Pets dataset.

+See code +
+
+
+
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/nlp/neural_machine_translation_with_transformer.html b/docs/dev/articles/examples/nlp/neural_machine_translation_with_transformer.html new file mode 100644 index 0000000000..a8374d93f8 --- /dev/null +++ b/docs/dev/articles/examples/nlp/neural_machine_translation_with_transformer.html @@ -0,0 +1,694 @@ + + + + + + + + +English-to-Spanish translation with a sequence-to-sequence Transformer • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

In this example, we’ll build a sequence-to-sequence Transformer +model, which we’ll train on an English-to-Spanish machine translation +task.

+

You’ll learn how to:

+
    +
  • Vectorize text using layer_text_vectorization().
  • +
  • Implement a layer_transformer_encoder(), a +layer_transformer_decoder(), and a +layer_positional_embedding().
  • +
  • Prepare data for training a sequence-to-sequence model.
  • +
  • Use the trained model to generate translations of never-seen-before +input sentences (sequence-to-sequence inference).
  • +
+

The code featured here is adapted from the book Deep +Learning with R, Second Edition (chapter 11: Deep learning for +text). The present example is fairly barebones, so for detailed +explanations of how each building block works, as well as the theory +behind Transformers, I recommend reading the book.

+
+
+

Setup +

+
+library(glue)
+
+library(tensorflow, exclude = c("set_random_seed", "shape"))
+library(keras3)
+
+
+

Downloading the data +

+

We’ll be working with an English-to-Spanish translation dataset +provided by Anki. Let’s +download it:

+
+zipfile <- get_file("spa-eng.zip", origin =
+  "http://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip")
+
+zip::zip_list(zipfile) # See what's in the zipfile
+
##             filename compressed_size uncompressed_size           timestamp
+## 1           spa-eng/               0                 0 2018-06-13 12:21:20
+## 2 spa-eng/_about.txt             685              1441 2018-05-13 19:50:34
+## 3    spa-eng/spa.txt         2637619           8042772 2018-05-13 19:50:34
+##   permissions    crc32 offset
+## 1         755 00000000      0
+## 2         644 4b18349d     38
+## 3         644 63c5c4a2    771
+
+zip::unzip(zipfile, exdir = ".") # unzip into the current directory
+
+text_file <- fs::path("./spa-eng/spa.txt")
+
+
+

Parsing the data +

+

Each line contains an English sentence and its corresponding Spanish +sentence. The English sentence is the source sequence and +Spanish one is the target sequence. We prepend the token +"[start]" and we append the token "[end]" to +the Spanish sentence.

+
+text_file <- "spa-eng/spa.txt"
+text_pairs <- text_file %>%
+  readr::read_tsv(col_names = c("english", "spanish"),
+                  col_types = c("cc")) %>%
+  within(spanish %<>% paste("[start]", ., "[end]"))
+

Here’s what our sentence pairs look like:

+
+df <- text_pairs[sample(nrow(text_pairs), 5), ]
+glue::glue_data(df, r"(
+  english: {english}
+  spanish: {spanish}
+)") |> cat(sep = "\n\n")
+
## english: I'm staying in Italy.
+## spanish: [start] Me estoy quedando en Italia. [end]
+##
+## english: What's so strange about that?
+## spanish: [start] ¿Qué es tan extraño acerca de eso? [end]
+##
+## english: All of the buses are full.
+## spanish: [start] Todos los bondis están llenos. [end]
+##
+## english: Is this where your mother works?
+## spanish: [start] ¿Es aquí donde trabaja tu madre? [end]
+##
+## english: Take precautions.
+## spanish: [start] Ten cuidado. [end]
+

Now, let’s split the sentence pairs into a training set, a validation +set, and a test set.

+
random.shuffle(text_pairs)
+num_val_samples = int(0.15 * len(text_pairs))
+num_train_samples = len(text_pairs) - 2 * num_val_samples
+train_pairs = text_pairs[:num_train_samples]
+val_pairs = text_pairs[num_train_samples : num_train_samples + num_val_samples]
+test_pairs = text_pairs[num_train_samples + num_val_samples :]
+
+print(f"{len(text_pairs)} total pairs")
+print(f"{len(train_pairs)} training pairs")
+print(f"{len(val_pairs)} validation pairs")
+print(f"{len(test_pairs)} test pairs")
+
+num_test_samples <- num_val_samples <-
+  round(0.15 * nrow(text_pairs))
+num_train_samples <- nrow(text_pairs) - num_val_samples - num_test_samples
+
+pair_group <- sample(c(
+  rep("train", num_train_samples),
+  rep("test", num_test_samples),
+  rep("val", num_val_samples)
+))
+
+train_pairs <- text_pairs[pair_group == "train", ]
+test_pairs <- text_pairs[pair_group == "test", ]
+val_pairs <- text_pairs[pair_group == "val", ]
+glue(r"(
+  {nrow(text_pairs)} total pairs
+  {nrow(train_pairs)} training pairs
+  {nrow(val_pairs)} validation pairs
+  {nrow(test_pairs)} test pairs
+)", .transformer = function(text, envir) {
+  val <- eval(str2lang(text), envir)
+  prettyNum(val, big.mark = ",")
+})
+
## 118,493 total pairs
+## 82,945 training pairs
+## 17,774 validation pairs
+## 17,774 test pairs
+
+
+

Vectorizing the text data +

+

We’ll use two instances of layer_text_vectorization() to +vectorize the text data (one for English and one for Spanish), that is +to say, to turn the original strings into integer sequences where each +integer represents the index of a word in a vocabulary.

+

The English layer will use the default string standardization (strip +punctuation characters) and splitting scheme (split on whitespace), +while the Spanish layer will use a custom standardization, where we add +the character "¿" to the set of punctuation characters to +be stripped.

+

Note: in a production-grade machine translation model, I would not +recommend stripping the punctuation characters in either language. +Instead, I would recommend turning each punctuation character into its +own token, which you could achieve by providing a custom +split function to +layer_text_vectorization().

+
+punctuation_regex <- "[¡¿]|[^[:^punct:][\\]]"
+# the regex explained: Match ¡, or ¿, or any punctuation character except ]
+#
+# [:^punct:]: is a negated POSIX character class.
+# [:punct:] matches any punctuation character, so [:^punct:] matches any
+# character that is not a punctuation character.
+# [^...] negates the whole character class
+# So [^[:^punct:]] would matche any character that is a punctuation character.
+# Putting this all together, [^[:^punct:][\\]] matches any
+# punctuation character except the ] character.
+
+custom_standardization <- function(input_string) {
+  input_string %>%
+    tf$strings$lower() %>%
+    tf$strings$regex_replace(punctuation_regex, "")
+}
+
+input_string <- as_tensor("[start] ¡corre! [end]")
+custom_standardization(input_string)
+
## tf.Tensor(b'[start] corre [end]', shape=(), dtype=string)
+
+vocab_size <- 15000
+sequence_length <- 20
+
+# rename to eng_vectorization
+eng_vectorization <- layer_text_vectorization(
+  max_tokens = vocab_size,
+  output_mode = "int",
+  output_sequence_length = sequence_length
+)
+
+spa_vectorization <- layer_text_vectorization(
+  max_tokens = vocab_size,
+  output_mode = "int",
+  output_sequence_length = sequence_length + 1,
+  standardize = custom_standardization
+)
+
+adapt(eng_vectorization, train_pairs$english)
+adapt(spa_vectorization, train_pairs$spanish)
+

Next, we’ll format our datasets.

+

At each training step, the model will seek to predict target words +N+1 (and beyond) using the source sentence and the target words from 1 +to N.

+

As such, the training dataset will yield a tuple +(inputs, targets), where:

+
    +
  • +inputs is a dictionary (named list) with the keys +(names) encoder_inputs and decoder_inputs. +encoder_inputs is the vectorized source sentence and +encoder_inputs is the target sentence “so far”, that is to +say, the words 0 to N used to predict word N+1 (and beyond) in the +target sentence.
  • +
  • +target is the target sentence offset by one step: it +provides the next words in the target sentence – what the model will try +to predict.
  • +
+
+format_pair <- function(pair) {
+  # the vectorization layers requrie batched inputs,
+  # reshape scalar string tensor to add a batch dim
+  pair %<>% lapply(op_expand_dims, 1)
+
+  # vectorize
+  eng <- eng_vectorization(pair$english)
+  spa <- spa_vectorization(pair$spanish)
+
+  # drop the batch dim
+  eng %<>% tf$ensure_shape(shape(1, sequence_length)) %>% op_squeeze(1)
+  spa %<>% tf$ensure_shape(shape(1, sequence_length+1)) %>% op_squeeze(1)
+
+  inputs <- list(encoder_inputs = eng,
+                 decoder_inputs = spa[NA:-2])
+  targets <- spa[2:NA]
+  list(inputs, targets)
+}
+
+
+batch_size <- 64
+
+library(tfdatasets, exclude = "shape")
+make_dataset <- function(pairs) {
+  tensor_slices_dataset(pairs) %>%
+    dataset_map(format_pair, num_parallel_calls = 4) %>%
+    dataset_cache() %>%
+    dataset_shuffle(2048) %>%
+    dataset_batch(batch_size) %>%
+    dataset_prefetch(2)
+}
+train_ds <- make_dataset(train_pairs)
+
## Warning: Negative numbers are interpreted python-style when subsetting tensorflow tensors.
+## See: ?`[.tensorflow.tensor` for details.
+## To turn off this warning, set `options(tensorflow.extract.warn_negatives_pythonic = FALSE)`
+
+val_ds <- make_dataset(val_pairs)
+

Let’s take a quick look at the sequence shapes (we have batches of 64 +pairs, and all sequences are 20 steps long):

+
+c(inputs, targets) %<-% iter_next(as_iterator(train_ds))
+str(inputs)
+
## List of 2
+##  $ encoder_inputs:<tf.Tensor: shape=(64, 20), dtype=int64, numpy=…>
+##  $ decoder_inputs:<tf.Tensor: shape=(64, 20), dtype=int64, numpy=…>
+
+str(targets)
+
## <tf.Tensor: shape=(64, 20), dtype=int64, numpy=…>
+
+
+

Building the model +

+

Our sequence-to-sequence Transformer consists of a +TransformerEncoder and a TransformerDecoder +chained together. To make the model aware of word order, we also use a +PositionalEmbedding layer.

+

The source sequence will be pass to the +TransformerEncoder, which will produce a new representation +of it. This new representation will then be passed to the +TransformerDecoder, together with the target sequence so +far (target words 1 to N). The TransformerDecoder will then +seek to predict the next words in the target sequence (N+1 and +beyond).

+

A key detail that makes this possible is causal masking (see method +get_causal_attention_mask() on the +TransformerDecoder). The TransformerDecoder +sees the entire sequences at once, and thus we must make sure that it +only uses information from target tokens 0 to N when predicting token +N+1 (otherwise, it could use information from the future, which would +result in a model that cannot be used at inference time).

+
+layer_transformer_encoder <- Layer(
+  classname = "TransformerEncoder",
+  initialize = function(embed_dim, dense_dim, num_heads, ...) {
+    super$initialize(...)
+    self$embed_dim <- embed_dim
+    self$dense_dim <- dense_dim
+    self$num_heads <- num_heads
+    self$attention <-
+      layer_multi_head_attention(num_heads = num_heads,
+                                 key_dim = embed_dim)
+
+    self$dense_proj <- keras_model_sequential() %>%
+      layer_dense(dense_dim, activation = "relu") %>%
+      layer_dense(embed_dim)
+
+    self$layernorm_1 <- layer_layer_normalization()
+    self$layernorm_2 <- layer_layer_normalization()
+    self$supports_masking <- TRUE
+  },
+
+  call = function(inputs, mask = NULL) {
+    if (!is.null(mask))
+      mask <- mask[, NULL, ] |> op_cast("int32")
+
+    inputs %>%
+      { self$attention(., ., attention_mask = mask) + . } %>%
+      self$layernorm_1() %>%
+      { self$dense_proj(.) + . } %>%
+      self$layernorm_2()
+  },
+
+  get_config = function() {
+    config <- super$get_config()
+    for(name in c("embed_dim", "num_heads", "dense_dim"))
+      config[[name]] <- self[[name]]
+    config
+  }
+)
+
+layer_transformer_decoder <- Layer(
+  classname = "TransformerDecoder",
+
+  initialize = function(embed_dim, latent_dim, num_heads, ...) {
+    super$initialize(...)
+    self$embed_dim <- embed_dim
+    self$latent_dim <- latent_dim
+    self$num_heads <- num_heads
+    self$attention_1 <- layer_multi_head_attention(num_heads = num_heads,
+                                                   key_dim = embed_dim)
+    self$attention_2 <- layer_multi_head_attention(num_heads = num_heads,
+                                                   key_dim = embed_dim)
+    self$dense_proj <- keras_model_sequential() %>%
+      layer_dense(latent_dim, activation = "relu") %>%
+      layer_dense(embed_dim)
+
+    self$layernorm_1 <- layer_layer_normalization()
+    self$layernorm_2 <- layer_layer_normalization()
+    self$layernorm_3 <- layer_layer_normalization()
+    self$supports_masking <- TRUE
+  },
+
+  get_config = function() {
+    config <- super$get_config()
+    for (name in c("embed_dim", "num_heads", "latent_dim"))
+      config[[name]] <- self[[name]]
+    config
+  },
+
+
+  get_causal_attention_mask = function(inputs) {
+    c(batch_size, sequence_length, encoding_length) %<-% op_shape(inputs)
+
+    x <- op_arange(sequence_length)
+    i <- x[, NULL]
+    j <- x[NULL, ]
+    mask <- op_cast(i >= j, "int32")
+
+    repeats <- op_stack(c(batch_size, 1L, 1L))
+    op_tile(mask[NULL, , ], repeats)
+  },
+  call = function(inputs, encoder_outputs, mask = NULL) {
+    causal_mask <- self$get_causal_attention_mask(inputs)
+
+    if (is.null(mask))
+      mask <- causal_mask
+    else
+      mask %<>% { op_minimum(op_cast(.[, NULL, ], "int32"),
+                             causal_mask) }
+
+    inputs %>%
+      { self$attention_1(query = ., value = ., key = .,
+                         attention_mask = causal_mask) + . } %>%
+      self$layernorm_1() %>%
+
+      { self$attention_2(query = .,
+                         value = encoder_outputs,
+                         key = encoder_outputs,
+                         attention_mask = mask) + . } %>%
+      self$layernorm_2() %>%
+
+      { self$dense_proj(.) + . } %>%
+      self$layernorm_3()
+
+  }
+)
+
+layer_positional_embedding <- Layer(
+  classname = "PositionalEmbedding",
+
+  initialize = function(sequence_length, vocab_size, embed_dim, ...) {
+    super$initialize(...)
+    self$token_embeddings <- layer_embedding(
+      input_dim = vocab_size, output_dim = embed_dim
+    )
+    self$position_embeddings <- layer_embedding(
+      input_dim = sequence_length, output_dim = embed_dim
+    )
+    self$sequence_length <- sequence_length
+    self$vocab_size <- vocab_size
+    self$embed_dim <- embed_dim
+  },
+
+  call = function(inputs) {
+    c(., len) %<-% op_shape(inputs) # (batch_size, seq_len)
+    positions <- op_arange(0, len, dtype = "int32")
+    embedded_tokens <- self$token_embeddings(inputs)
+    embedded_positions <- self$position_embeddings(positions)
+    embedded_tokens + embedded_positions
+  },
+
+  compute_mask = function(inputs, mask = NULL) {
+    if (is.null(mask)) return (NULL)
+    inputs != 0L
+  },
+
+  get_config = function() {
+    config <- super$get_config()
+    for(name in c("sequence_length", "vocab_size", "embed_dim"))
+      config[[name]] <- self[[name]]
+    config
+  }
+)
+

Next, we assemble the end-to-end model.

+
+embed_dim <- 256
+latent_dim <- 2048
+num_heads <- 8
+
+encoder_inputs <- layer_input(shape(NA), dtype = "int64",
+                              name = "encoder_inputs")
+encoder_outputs <- encoder_inputs %>%
+  layer_positional_embedding(sequence_length, vocab_size, embed_dim) %>%
+  layer_transformer_encoder(embed_dim, latent_dim, num_heads)
+
+encoder <- keras_model(encoder_inputs, encoder_outputs)
+
+decoder_inputs <-  layer_input(shape(NA), dtype = "int64",
+                               name = "decoder_inputs")
+encoded_seq_inputs <- layer_input(shape(NA, embed_dim),
+                                  name = "decoder_state_inputs")
+
+transformer_decoder <- layer_transformer_decoder(NULL,
+  embed_dim, latent_dim, num_heads)
+
+decoder_outputs <- decoder_inputs %>%
+  layer_positional_embedding(sequence_length, vocab_size, embed_dim) %>%
+  transformer_decoder(., encoded_seq_inputs) %>%
+  layer_dropout(0.5) %>%
+  layer_dense(vocab_size, activation="softmax")
+
+decoder <- keras_model(inputs = list(decoder_inputs, encoded_seq_inputs),
+                       outputs = decoder_outputs)
+
+decoder_outputs = decoder(list(decoder_inputs, encoder_outputs))
+
+transformer <- keras_model(list(encoder_inputs, decoder_inputs),
+                           decoder_outputs,
+                           name = "transformer")
+
+
+

Training our model +

+

We’ll use accuracy as a quick way to monitor training progress on the +validation data. Note that machine translation typically uses BLEU +scores as well as other metrics, rather than accuracy.

+

Here we only train for 1 epoch, but to get the model to actually +converge you should train for at least 30 epochs.

+
+epochs <- 1  # This should be at least 30 for convergence
+
+transformer
+
## Model: "transformer"
+## ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)         Output Shape          Param #  Connected to      
+## ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
+## │ encoder_inputs      │ (None, None)      │          0 │ -                 │
+## │ (InputLayer)        │                   │            │                   │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ positional_embeddi… │ (None, None, 256) │  3,845,120 │ encoder_inputs[0… │
+## │ (PositionalEmbeddi… │                   │            │                   │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ decoder_inputs      │ (None, None)      │          0 │ -                 │
+## │ (InputLayer)        │                   │            │                   │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ transformer_encoder │ (None, None, 256) │  3,155,456 │ positional_embed… │
+## │ (TransformerEncode… │                   │            │                   │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ functional_5        │ (None, None,      │ 12,959,640 │ decoder_inputs[0… │
+## │ (Functional)        │ 15000)            │            │ transformer_enco… │
+## └─────────────────────┴───────────────────┴────────────┴───────────────────┘
+##  Total params: 19,960,216 (76.14 MB)
+##  Trainable params: 19,960,216 (76.14 MB)
+##  Non-trainable params: 0 (0.00 B)
+
+transformer |> compile(
+  "rmsprop",
+  loss = "sparse_categorical_crossentropy",
+  metrics = "accuracy"
+)
+transformer |> fit(train_ds, epochs = epochs,
+                   validation_data = val_ds)
+
## 1297/1297 - 58s - 44ms/step - accuracy: 0.7709 - loss: 1.5752 - val_accuracy: 0.7731 - val_loss: 1.4209
+
+
+

Decoding test sentences +

+

Finally, let’s demonstrate how to translate brand new English +sentences. We simply feed into the model the vectorized English sentence +as well as the target token "[start]", then we repeatedly +generated the next token, until we hit the token +"[end]".

+
+spa_vocab <- spa_vectorization |> get_vocabulary()
+max_decoded_sentence_length <- 20
+tf_decode_sequence <- tf_function(function(input_sentence) {
+  withr::local_options(tensorflow.extract.style = "python")
+
+  tokenized_input_sentence <- input_sentence %>%
+    as_tensor(shape = c(1, 1)) %>%
+    eng_vectorization()
+  spa_vocab <- as_tensor(spa_vocab)
+  decoded_sentence <- as_tensor("[start]", shape = c(1, 1))
+
+  for (i in tf$range(as.integer(max_decoded_sentence_length))) {
+
+    tokenized_target_sentence <-
+      spa_vectorization(decoded_sentence)[,NA:-1]
+
+    next_token_predictions <-
+      transformer(list(tokenized_input_sentence,
+                       tokenized_target_sentence))
+
+    sampled_token_index <- tf$argmax(next_token_predictions[0, i, ])
+    sampled_token <- spa_vocab[sampled_token_index]
+    decoded_sentence <-
+      tf$strings$join(c(decoded_sentence, sampled_token),
+                      separator = " ")
+
+    if (sampled_token == "[end]")
+      break
+  }
+
+  decoded_sentence
+
+})
+
+for (i in seq(20)) {
+
+    c(input_sentence, correct_translation) %<-%
+      test_pairs[sample.int(nrow(test_pairs), 1), ]
+    cat("-\n")
+    cat("English:", input_sentence, "\n")
+    cat("Correct Translation:", tolower(correct_translation), "\n")
+    cat("  Model Translation:", input_sentence %>% as_tensor() %>%
+          tf_decode_sequence() %>% as.character(), "\n")
+}
+

After 30 epochs, we get results such as:

+
English: I'm sure everything will be fine.
+Correct Translation: [start] estoy segura de que todo irá bien. [end]
+  Model Translation: [start] estoy seguro de que todo va bien [end]
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/nlp/text_classification_from_scratch.html b/docs/dev/articles/examples/nlp/text_classification_from_scratch.html new file mode 100644 index 0000000000..c47b91dd19 --- /dev/null +++ b/docs/dev/articles/examples/nlp/text_classification_from_scratch.html @@ -0,0 +1,486 @@ + + + + + + + + +Text classification from scratch • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

This example shows how to do text classification starting from raw +text (as a set of text files on disk). We demonstrate the workflow on +the IMDB sentiment classification dataset (unprocessed version). We use +layer_text_vectorization() for word splitting & +indexing.

+
+
+

Setup +

+
+library(tensorflow, exclude = c("shape", "set_random_seed"))
+library(tfdatasets, exclude = "shape")
+library(keras3)
+use_virtualenv("r-keras")
+
+
+

Load the data: IMDB movie review sentiment classification +

+

Let’s download the data and inspect its structure.

+
+if (!dir.exists("datasets/aclImdb")) {
+  dir.create("datasets")
+  download.file(
+    "https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz",
+    "datasets/aclImdb_v1.tar.gz"
+  )
+  untar("datasets/aclImdb_v1.tar.gz", exdir = "datasets")
+  unlink("datasets/aclImdb/train/unsup", recursive = TRUE)
+}
+

The aclImdb folder contains a train and +test subfolder:

+
+head(list.files("datasets/aclImdb/test"))
+
## [1] "labeledBow.feat" "neg"             "pos"             "urls_neg.txt"
+## [5] "urls_pos.txt"
+
+head(list.files("datasets/aclImdb/train"))
+
## [1] "labeledBow.feat" "neg"             "pos"             "unsupBow.feat"
+## [5] "urls_neg.txt"    "urls_pos.txt"
+

The aclImdb/train/pos and aclImdb/train/neg +folders contain text files, each of which represents one review (either +positive or negative):

+
+writeLines(strwrap(readLines("datasets/aclImdb/train/pos/4229_10.txt")))
+
## Don't waste time reading my review. Go out and see this
+## astonishingly good episode, which may very well be the best Columbo
+## ever written! Ruth Gordon is perfectly cast as the scheming yet
+## charming mystery writer who murders her son-in-law to avenge his
+## murder of her daughter. Columbo is his usual rumpled, befuddled and
+## far-cleverer-than-he-seems self, and this particular installment
+## features fantastic chemistry between Gordon and Falk. Ironically,
+## this was not written by heralded creators Levinson or Link yet is
+## possibly the densest, most thoroughly original and twist-laden
+## Columbo plot ever. Utterly satisfying in nearly every department
+## and overflowing with droll and witty dialogue and thinking. Truly
+## unexpected and inventive climax tops all. 10/10...seek this one out
+## on Netflix!
+

We are only interested in the pos and neg +subfolders, so let’s delete the other subfolder that has text files in +it:

+
+unlink("datasets/aclImdb/train/unsup", recursive = TRUE)
+

You can use the utility text_dataset_from_directory() to +generate a labeled tf_dataset object from a set of text +files on disk filed into class-specific folders.

+

Let’s use it to generate the training, validation, and test datasets. +The validation and training datasets are generated from two subsets of +the train directory, with 20% of samples going to the +validation dataset and 80% going to the training dataset.

+

Having a validation dataset in addition to the test dataset is useful +for tuning hyperparameters, such as the model architecture, for which +the test dataset should not be used.

+

Before putting the model out into the real world however, it should +be retrained using all available training data (without creating a +validation dataset), so its performance is maximized.

+

When using the validation_split and subset +arguments, make sure to either specify a random seed, or to pass +shuffle=FALSE, so that the validation & training splits +you get have no overlap.

+
+batch_size <- 32
+
+raw_train_ds <- text_dataset_from_directory(
+  "datasets/aclImdb/train",
+  batch_size = batch_size,
+  validation_split = 0.2,
+  subset = "training",
+  seed = 1337
+)
+
## Found 25000 files belonging to 2 classes.
+## Using 20000 files for training.
+
+raw_val_ds <- text_dataset_from_directory(
+  "datasets/aclImdb/train",
+  batch_size = batch_size,
+  validation_split = 0.2,
+  subset = "validation",
+  seed = 1337
+)
+
## Found 25000 files belonging to 2 classes.
+## Using 5000 files for validation.
+
+raw_test_ds <- text_dataset_from_directory(
+  "datasets/aclImdb/test",
+  batch_size = batch_size
+)
+
## Found 25000 files belonging to 2 classes.
+
+cat("Number of batches in raw_train_ds:", length(raw_train_ds), "\n")
+
## Number of batches in raw_train_ds: 625
+
+cat("Number of batches in raw_val_ds:", length(raw_val_ds), "\n")
+
## Number of batches in raw_val_ds: 157
+
+cat("Number of batches in raw_test_ds:", length(raw_test_ds), "\n")
+
## Number of batches in raw_test_ds: 782
+

Let’s preview a few samples:

+
+# It's important to take a look at your raw data to ensure your normalization
+# and tokenization will work as expected. We can do that by taking a few
+# examples from the training set and looking at them.
+# This is one of the places where eager execution shines:
+# we can just evaluate these tensors using .numpy()
+# instead of needing to evaluate them in a Session/Graph context.
+batch <- iter_next(as_iterator(raw_train_ds))
+str(batch)
+
## List of 2
+##  $ :<tf.Tensor: shape=(32), dtype=string, numpy=…>
+##  $ :<tf.Tensor: shape=(32), dtype=int32, numpy=…>
+
+c(text_batch, label_batch) %<-% batch
+for (i in 1:3) {
+  print(text_batch[i])
+  print(label_batch[i])
+}
+
## tf.Tensor(b"I have read the novel Reaper of Ben Mezrich a fews years ago and last night I accidentally came to see this adaption.<br /><br />Although it's been years since I read the story the first time, the differences between the novel and the movie are humongous. Very important elements, which made the whole thing plausible are just written out or changed to bad.<br /><br />If the plot sounds interesting to you: go and get the novel. Its much, much, much better.<br /><br />Still 4 out of 10 since it was hard to stop watching because of the great basic plot by Ben Mezrich.", shape=(), dtype=string)
+## tf.Tensor(0, shape=(), dtype=int32)
+## tf.Tensor(b'After seeing all the Jesse James, Quantrill, jayhawkers,etc films in the fifties, it is quite a thrill to see this film with a new perspective by director Ang Lee. The scene of the attack of Lawrence, Kansas is awesome. The romantic relationship between Jewel and Toby Mcguire turns out to be one of the best parts and Jonathan Rhys-Meyers is outstanding as the bad guy. All the time this film makes you feel the horror of war, and the desperate situation of the main characters who do not know if they are going to survive the next hours. Definitely worth seeing.', shape=(), dtype=string)
+## tf.Tensor(1, shape=(), dtype=int32)
+## tf.Tensor(b'AG was an excellent presentation of drama, suspense and thriller that is so rare to American TV. Sheriff Lucas gave many a viewer the willies. We rooted for Caleb as he strove to resist the overtures of Sheriff Lucas. We became engrossed and fearful upon learning of the unthinkable connection between these two characters. The manipulations which weekly gave cause to fear what Lucas would do next were truly surprising. This show lived up to the "Gothic" moniker in ways American entertainment has so seldom attempted, much less mastered. The suits definitely made a big mistake in not supporting this show. This show puts shame to the current glut of "reality" shows- which are so less than satisfying viewing.The call for a DVD box set is well based. This show is quality viewing for a discerning market hungry for quality viewing. A public that is tiring of over-saturation of mind-numbing reality fare will welcome this gem of real storytelling. Bring on the DVD box set!!', shape=(), dtype=string)
+## tf.Tensor(1, shape=(), dtype=int32)
+
+
+

Prepare the data +

+

In particular, we remove <br /> tags.

+
+# Having looked at our data above, we see that the raw text contains HTML break
+# tags of the form '<br />'. These tags will not be removed by the default
+# standardizer (which doesn't strip HTML). Because of this, we will need to
+# create a custom standardization function.
+custom_standardization_fn <- function(string_tensor) {
+  string_tensor |>
+    tf$strings$lower() |> # convert to all lowercase
+    tf$strings$regex_replace("<br />", " ") |> # remove '<br />' HTML tag
+    tf$strings$regex_replace("[[:punct:]]", "") # remove punctuation
+}
+
+
+# Model constants.
+max_features <- 20000
+embedding_dim <- 128
+sequence_length <- 500
+
+# Now that we have our custom standardization, we can instantiate our text
+# vectorization layer. We are using this layer to normalize, split, and map
+# strings to integers, so we set our 'output_mode' to 'int'.
+# Note that we're using the default split function,
+# and the custom standardization defined above.
+# We also set an explicit maximum sequence length, since the CNNs later in our
+# model won't support ragged sequences.
+vectorize_layer <- layer_text_vectorization(
+  standardize = custom_standardization_fn,
+  max_tokens = max_features,
+  output_mode = "int",
+  output_sequence_length = sequence_length,
+)
+
+# Now that the vectorize_layer has been created, call `adapt` on a text-only
+# dataset to create the vocabulary. You don't have to batch, but for very large
+# datasets this means you're not keeping spare copies of the dataset in memory.
+
+# Let's make a text-only dataset (no labels):
+text_ds <- raw_train_ds |>
+  dataset_map(\(x, y) x)
+# Let's call `adapt`:
+vectorize_layer |> adapt(text_ds)
+
+
+

Two options to vectorize the data +

+

There are 2 ways we can use our text vectorization layer:

+

Option 1: Make it part of the model, so as to obtain +a model that processes raw strings, like this:

+
+text_input <- keras_input(shape = c(1L), dtype = "string", name = 'text')
+x <- text_input |>
+  vectorize_layer() |>
+  layer_embedding(max_features + 1, embedding_dim)
+

Option 2: Apply it to the text dataset to obtain a +dataset of word indices, then feed it into a model that expects integer +sequences as inputs.

+

An important difference between the two is that option 2 enables you +to do asynchronous CPU processing and buffering of your +data when training on GPU. So if you’re training the model on GPU, you +probably want to go with this option to get the best performance. This +is what we will do below.

+

If we were to export our model to production, we’d ship a model that +accepts raw strings as input, like in the code snippet for option 1 +above. This can be done after training. We do this in the last +section.

+
+vectorize_text <- function(text, label) {
+  text <- text |>
+    op_expand_dims(-1) |>
+    vectorize_layer()
+  list(text, label)
+}
+
+# Vectorize the data.
+train_ds <- raw_train_ds |> dataset_map(vectorize_text)
+val_ds   <- raw_val_ds   |> dataset_map(vectorize_text)
+test_ds  <- raw_test_ds  |> dataset_map(vectorize_text)
+
+# Do async prefetching / buffering of the data for best performance on GPU.
+train_ds <- train_ds |>
+  dataset_cache() |>
+  dataset_prefetch(buffer_size = 10)
+val_ds <- val_ds |>
+  dataset_cache() |>
+  dataset_prefetch(buffer_size = 10)
+test_ds <- test_ds |>
+  dataset_cache() |>
+  dataset_prefetch(buffer_size = 10)
+
+
+

Build a model +

+

We choose a simple 1D convnet starting with an Embedding +layer.

+
+# A integer input for vocab indices.
+inputs <- keras_input(shape = c(NA), dtype = "int64")
+
+predictions <- inputs |>
+  # Next, we add a layer to map those vocab indices into a space of dimensionality
+  # 'embedding_dim'.
+  layer_embedding(max_features, embedding_dim) |>
+  layer_dropout(0.5) |>
+  # Conv1D + global max pooling
+  layer_conv_1d(128, 7, padding = "valid", activation = "relu", strides = 3) |>
+  layer_conv_1d(128, 7, padding = "valid", activation = "relu", strides = 3) |>
+  layer_global_max_pooling_1d() |>
+  # We add a vanilla hidden layer:
+  layer_dense(128, activation = "relu") |>
+  layer_dropout(0.5) |>
+  # We project onto a single unit output layer, and squash it with a sigmoid:
+  layer_dense(1, activation = "sigmoid", name = "predictions")
+
+model <- keras_model(inputs, predictions)
+
+summary(model)
+
## Model: "functional_1"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ input_layer (InputLayer)        │ (None, None)           │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ embedding_1 (Embedding)         │ (None, None, 128)      │     2,560,000
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dropout (Dropout)               │ (None, None, 128)      │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv1d (Conv1D)                 │ (None, None, 128)      │       114,816
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv1d_1 (Conv1D)               │ (None, None, 128)      │       114,816
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ global_max_pooling1d            │ (None, 128)            │             0
+## │ (GlobalMaxPooling1D)            │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense (Dense)                   │ (None, 128)            │        16,512
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dropout_1 (Dropout)             │ (None, 128)            │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ predictions (Dense)             │ (None, 1)              │           129
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 2,806,273 (10.71 MB)
+##  Trainable params: 2,806,273 (10.71 MB)
+##  Non-trainable params: 0 (0.00 B)
+
+# Compile the model with binary crossentropy loss and an adam optimizer.
+model |> compile(loss = "binary_crossentropy",
+                 optimizer = "adam",
+                 metrics = "accuracy")
+
+
+

Train the model +

+
+epochs <- 3
+
+# Fit the model using the train and test datasets.
+model |> fit(train_ds, validation_data = val_ds, epochs = epochs)
+
## Epoch 1/3
+## 625/625 - 6s - 10ms/step - accuracy: 0.6909 - loss: 0.5300 - val_accuracy: 0.8658 - val_loss: 0.3229
+## Epoch 2/3
+## 625/625 - 2s - 3ms/step - accuracy: 0.9047 - loss: 0.2412 - val_accuracy: 0.8742 - val_loss: 0.3202
+## Epoch 3/3
+## 625/625 - 2s - 3ms/step - accuracy: 0.9573 - loss: 0.1237 - val_accuracy: 0.8704 - val_loss: 0.3551
+
+
+

Evaluate the model on the test set +

+
+model |> evaluate(test_ds)
+
## 782/782 - 1s - 2ms/step - accuracy: 0.8594 - loss: 0.3818
+
## $accuracy
+## [1] 0.85936
+##
+## $loss
+## [1] 0.381799
+
+
+

Make an end-to-end model +

+

If you want to obtain a model capable of processing raw strings, you +can simply create a new model (using the weights we just trained):

+
+# A string input
+inputs <- keras_input(shape = c(1), dtype = "string")
+# Turn strings into vocab indices
+indices <- vectorize_layer(inputs)
+# Turn vocab indices into predictions
+outputs <- model(indices)
+
+# Our end to end model
+end_to_end_model <- keras_model(inputs, outputs)
+end_to_end_model |> compile(
+  loss = "binary_crossentropy",
+  optimizer = "adam",
+  metrics = c("accuracy")
+)
+
+# Test it with `raw_test_ds`, which yields raw strings
+end_to_end_model |> evaluate(raw_test_ds)
+
## 782/782 - 3s - 4ms/step - accuracy: 0.8594 - loss: 0.0000e+00
+
## $accuracy
+## [1] 0.85936
+##
+## $loss
+## [1] 0
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/structured_data/imbalanced_classification.html b/docs/dev/articles/examples/structured_data/imbalanced_classification.html new file mode 100644 index 0000000000..06c3cf580d --- /dev/null +++ b/docs/dev/articles/examples/structured_data/imbalanced_classification.html @@ -0,0 +1,404 @@ + + + + + + + + +Imbalanced classification: credit card fraud detection • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Introduction +

+

This example looks at the Kaggle Credit +Card Fraud Detection dataset to demonstrate how to train a +classification model on data with highly imbalanced classes. You can +download the data by clicking “Download” at the link, or if you’re setup +with a kaggle API key at "~/.kaggle/kagle.json", you can +run the following:

+
+reticulate::py_install("kaggle", pip = TRUE)
+reticulate::py_available(TRUE) # ensure 'kaggle' is on the PATH
+system("kaggle datasets download -d mlg-ulb/creditcardfraud")
+zip::unzip("creditcardfraud.zip", files = "creditcard.csv")
+
+
+

First, load the data +

+
+library(readr)
+df <- read_csv("creditcard.csv", col_types = cols(
+  Class = col_integer(),
+  .default = col_double()
+))
+tibble::glimpse(df)
+
## Rows: 284,807
+## Columns: 31
+## $ Time   <dbl> 0, 0, 1, 1, 2, 2, 4, 7, 7, 9, 10, 10, 10, 11, 12, 12, 12, 1…
+## $ V1     <dbl> -1.3598071, 1.1918571, -1.3583541, -0.9662717, -1.1582331, …
+## $ V2     <dbl> -0.07278117, 0.26615071, -1.34016307, -0.18522601, 0.877736…
+## $ V3     <dbl> 2.53634674, 0.16648011, 1.77320934, 1.79299334, 1.54871785,…
+## $ V4     <dbl> 1.37815522, 0.44815408, 0.37977959, -0.86329128, 0.40303393…
+## $ V5     <dbl> -0.33832077, 0.06001765, -0.50319813, -0.01030888, -0.40719…
+## $ V6     <dbl> 0.46238778, -0.08236081, 1.80049938, 1.24720317, 0.09592146…
+## $ V7     <dbl> 0.239598554, -0.078802983, 0.791460956, 0.237608940, 0.5929…
+## $ V8     <dbl> 0.098697901, 0.085101655, 0.247675787, 0.377435875, -0.2705…
+## $ V9     <dbl> 0.3637870, -0.2554251, -1.5146543, -1.3870241, 0.8177393, -…
+## $ V10    <dbl> 0.09079417, -0.16697441, 0.20764287, -0.05495192, 0.7530744…
+## $ V11    <dbl> -0.55159953, 1.61272666, 0.62450146, -0.22648726, -0.822842…
+## $ V12    <dbl> -0.61780086, 1.06523531, 0.06608369, 0.17822823, 0.53819555…
+## $ V13    <dbl> -0.99138985, 0.48909502, 0.71729273, 0.50775687, 1.34585159…
+## $ V14    <dbl> -0.31116935, -0.14377230, -0.16594592, -0.28792375, -1.1196…
+## $ V15    <dbl> 1.468176972, 0.635558093, 2.345864949, -0.631418118, 0.1751…
+## $ V16    <dbl> -0.47040053, 0.46391704, -2.89008319, -1.05964725, -0.45144…
+## $ V17    <dbl> 0.207971242, -0.114804663, 1.109969379, -0.684092786, -0.23…
+## $ V18    <dbl> 0.02579058, -0.18336127, -0.12135931, 1.96577500, -0.038194…
+## $ V19    <dbl> 0.40399296, -0.14578304, -2.26185710, -1.23262197, 0.803486…
+## $ V20    <dbl> 0.25141210, -0.06908314, 0.52497973, -0.20803778, 0.4085423…
+## $ V21    <dbl> -0.018306778, -0.225775248, 0.247998153, -0.108300452, -0.0…
+## $ V22    <dbl> 0.277837576, -0.638671953, 0.771679402, 0.005273597, 0.7982…
+## $ V23    <dbl> -0.110473910, 0.101288021, 0.909412262, -0.190320519, -0.13…
+## $ V24    <dbl> 0.06692807, -0.33984648, -0.68928096, -1.17557533, 0.141266…
+## $ V25    <dbl> 0.12853936, 0.16717040, -0.32764183, 0.64737603, -0.2060095…
+## $ V26    <dbl> -0.18911484, 0.12589453, -0.13909657, -0.22192884, 0.502292…
+## $ V27    <dbl> 0.133558377, -0.008983099, -0.055352794, 0.062722849, 0.219…
+## $ V28    <dbl> -0.021053053, 0.014724169, -0.059751841, 0.061457629, 0.215…
+## $ Amount <dbl> 149.62, 2.69, 378.66, 123.50, 69.99, 3.67, 4.99, 40.80, 93.…
+## $ Class  <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
+
+
+

Prepare a validation set +

+
+val_idx <- nrow(df) %>% sample.int(., round( . * 0.2))
+val_df <- df[val_idx, ]
+train_df <- df[-val_idx, ]
+
+cat("Number of training samples:", nrow(train_df), "\n")
+
## Number of training samples: 227846
+
+cat("Number of validation samples:", nrow(val_df), "\n")
+
## Number of validation samples: 56961
+
+
+

Analyze class imbalance in the targets +

+
+counts <- table(train_df$Class)
+counts
+
##
+##      0      1
+## 227450    396
+
+cat(sprintf("Number of positive samples in training data: %i (%.2f%% of total)",
+            counts["1"], 100 * counts["1"] / sum(counts)))
+
## Number of positive samples in training data: 396 (0.17% of total)
+
+weight_for_0 = 1 / counts["0"]
+weight_for_1 = 1 / counts["1"]
+
+
+

Normalize the data using training set statistics +

+
+feature_names <- colnames(train_df) %>% setdiff("Class")
+
+train_features <- as.matrix(train_df[feature_names])
+train_targets <- as.matrix(train_df$Class)
+
+val_features <- as.matrix(val_df[feature_names])
+val_targets <- as.matrix(val_df$Class)
+
+train_features %<>% scale()
+val_features %<>% scale(center = attr(train_features, "scaled:center"),
+                        scale = attr(train_features, "scaled:scale"))
+
+
+

Build a binary classification model +

+
+model <-
+  keras_model_sequential(input_shape = ncol(train_features)) |>
+  layer_dense(256, activation = "relu") |>
+  layer_dense(256, activation = "relu") |>
+  layer_dropout(0.3) |>
+  layer_dense(256, activation = "relu") |>
+  layer_dropout(0.3) |>
+  layer_dense(1, activation = "sigmoid")
+
+model
+
## Model: "sequential"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ dense (Dense)                   │ (None, 256)            │         7,936
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_1 (Dense)                 │ (None, 256)            │        65,792
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dropout (Dropout)               │ (None, 256)            │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_2 (Dense)                 │ (None, 256)            │        65,792
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dropout_1 (Dropout)             │ (None, 256)            │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_3 (Dense)                 │ (None, 1)              │           257
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 139,777 (546.00 KB)
+##  Trainable params: 139,777 (546.00 KB)
+##  Non-trainable params: 0 (0.00 B)
+
+
+

Train the model with class_weight argument +

+
+metrics <- list(
+  metric_false_negatives(name = "fn"),
+  metric_false_positives(name = "fp"),
+  metric_true_negatives(name = "tn"),
+  metric_true_positives(name = "tp"),
+  metric_precision(name = "precision"),
+  metric_recall(name = "recall")
+)
+model |> compile(
+  optimizer = optimizer_adam(1e-2),
+  loss = "binary_crossentropy",
+  metrics = metrics
+)
+callbacks <- list(
+  callback_model_checkpoint("fraud_model_at_epoch_{epoch}.keras")
+)
+
+class_weight <- list("0" = weight_for_0,
+                     "1" = weight_for_1)
+
+model |> fit(
+  train_features, train_targets,
+  validation_data = list(val_features, val_targets),
+  class_weight = class_weight,
+  batch_size = 2048,
+  epochs = 30,
+  callbacks = callbacks,
+  verbose = 2
+)
+
## Epoch 1/30
+## 112/112 - 3s - 27ms/step - fn: 40.0000 - fp: 23705.0000 - loss: 2.2728e-06 - precision: 0.0148 - recall: 0.8990 - tn: 203745.0000 - tp: 356.0000 - val_fn: 8.0000 - val_fp: 1502.0000 - val_loss: 0.1089 - val_precision: 0.0553 - val_recall: 0.9167 - val_tn: 55363.0000 - val_tp: 88.0000
+## Epoch 2/30
+## 112/112 - 1s - 6ms/step - fn: 33.0000 - fp: 8009.0000 - loss: 1.4949e-06 - precision: 0.0434 - recall: 0.9167 - tn: 219441.0000 - tp: 363.0000 - val_fn: 10.0000 - val_fp: 1176.0000 - val_loss: 0.0912 - val_precision: 0.0681 - val_recall: 0.8958 - val_tn: 55689.0000 - val_tp: 86.0000
+## Epoch 3/30
+## 112/112 - 0s - 2ms/step - fn: 32.0000 - fp: 7704.0000 - loss: 1.3369e-06 - precision: 0.0451 - recall: 0.9192 - tn: 219746.0000 - tp: 364.0000 - val_fn: 9.0000 - val_fp: 1202.0000 - val_loss: 0.0870 - val_precision: 0.0675 - val_recall: 0.9062 - val_tn: 55663.0000 - val_tp: 87.0000
+## Epoch 4/30
+## 112/112 - 0s - 2ms/step - fn: 28.0000 - fp: 8366.0000 - loss: 1.2269e-06 - precision: 0.0421 - recall: 0.9293 - tn: 219084.0000 - tp: 368.0000 - val_fn: 10.0000 - val_fp: 1560.0000 - val_loss: 0.0967 - val_precision: 0.0522 - val_recall: 0.8958 - val_tn: 55305.0000 - val_tp: 86.0000
+## Epoch 5/30
+## 112/112 - 0s - 2ms/step - fn: 19.0000 - fp: 6217.0000 - loss: 8.5492e-07 - precision: 0.0572 - recall: 0.9520 - tn: 221233.0000 - tp: 377.0000 - val_fn: 9.0000 - val_fp: 2626.0000 - val_loss: 0.1236 - val_precision: 0.0321 - val_recall: 0.9062 - val_tn: 54239.0000 - val_tp: 87.0000
+## Epoch 6/30
+## 112/112 - 0s - 2ms/step - fn: 18.0000 - fp: 6566.0000 - loss: 7.4300e-07 - precision: 0.0544 - recall: 0.9545 - tn: 220884.0000 - tp: 378.0000 - val_fn: 8.0000 - val_fp: 2302.0000 - val_loss: 0.1081 - val_precision: 0.0368 - val_recall: 0.9167 - val_tn: 54563.0000 - val_tp: 88.0000
+## Epoch 7/30
+## 112/112 - 0s - 2ms/step - fn: 14.0000 - fp: 6819.0000 - loss: 7.1838e-07 - precision: 0.0530 - recall: 0.9646 - tn: 220631.0000 - tp: 382.0000 - val_fn: 7.0000 - val_fp: 1376.0000 - val_loss: 0.0800 - val_precision: 0.0608 - val_recall: 0.9271 - val_tn: 55489.0000 - val_tp: 89.0000
+## Epoch 8/30
+## 112/112 - 0s - 2ms/step - fn: 19.0000 - fp: 7345.0000 - loss: 8.3921e-07 - precision: 0.0488 - recall: 0.9520 - tn: 220105.0000 - tp: 377.0000 - val_fn: 10.0000 - val_fp: 416.0000 - val_loss: 0.0406 - val_precision: 0.1713 - val_recall: 0.8958 - val_tn: 56449.0000 - val_tp: 86.0000
+## Epoch 9/30
+## 112/112 - 0s - 2ms/step - fn: 13.0000 - fp: 6282.0000 - loss: 8.3488e-07 - precision: 0.0575 - recall: 0.9672 - tn: 221168.0000 - tp: 383.0000 - val_fn: 10.0000 - val_fp: 933.0000 - val_loss: 0.0450 - val_precision: 0.0844 - val_recall: 0.8958 - val_tn: 55932.0000 - val_tp: 86.0000
+## Epoch 10/30
+## 112/112 - 0s - 2ms/step - fn: 15.0000 - fp: 7597.0000 - loss: 1.1098e-06 - precision: 0.0478 - recall: 0.9621 - tn: 219853.0000 - tp: 381.0000 - val_fn: 10.0000 - val_fp: 2417.0000 - val_loss: 0.4261 - val_precision: 0.0344 - val_recall: 0.8958 - val_tn: 54448.0000 - val_tp: 86.0000
+## Epoch 11/30
+## 112/112 - 0s - 2ms/step - fn: 24.0000 - fp: 10269.0000 - loss: 1.9062e-06 - precision: 0.0350 - recall: 0.9394 - tn: 217181.0000 - tp: 372.0000 - val_fn: 9.0000 - val_fp: 1434.0000 - val_loss: 0.1261 - val_precision: 0.0572 - val_recall: 0.9062 - val_tn: 55431.0000 - val_tp: 87.0000
+## Epoch 12/30
+## 112/112 - 0s - 2ms/step - fn: 17.0000 - fp: 6634.0000 - loss: 1.0711e-06 - precision: 0.0540 - recall: 0.9571 - tn: 220816.0000 - tp: 379.0000 - val_fn: 8.0000 - val_fp: 2263.0000 - val_loss: 0.1654 - val_precision: 0.0374 - val_recall: 0.9167 - val_tn: 54602.0000 - val_tp: 88.0000
+## Epoch 13/30
+## 112/112 - 0s - 2ms/step - fn: 10.0000 - fp: 5620.0000 - loss: 5.3600e-07 - precision: 0.0643 - recall: 0.9747 - tn: 221830.0000 - tp: 386.0000 - val_fn: 12.0000 - val_fp: 914.0000 - val_loss: 0.0414 - val_precision: 0.0842 - val_recall: 0.8750 - val_tn: 55951.0000 - val_tp: 84.0000
+## Epoch 14/30
+## 112/112 - 0s - 2ms/step - fn: 8.0000 - fp: 5103.0000 - loss: 5.1168e-07 - precision: 0.0707 - recall: 0.9798 - tn: 222347.0000 - tp: 388.0000 - val_fn: 11.0000 - val_fp: 1382.0000 - val_loss: 0.0660 - val_precision: 0.0579 - val_recall: 0.8854 - val_tn: 55483.0000 - val_tp: 85.0000
+## Epoch 15/30
+## 112/112 - 0s - 2ms/step - fn: 7.0000 - fp: 4497.0000 - loss: 4.4632e-07 - precision: 0.0796 - recall: 0.9823 - tn: 222953.0000 - tp: 389.0000 - val_fn: 10.0000 - val_fp: 1805.0000 - val_loss: 0.0786 - val_precision: 0.0455 - val_recall: 0.8958 - val_tn: 55060.0000 - val_tp: 86.0000
+## Epoch 16/30
+## 112/112 - 0s - 2ms/step - fn: 5.0000 - fp: 5056.0000 - loss: 6.1038e-07 - precision: 0.0718 - recall: 0.9874 - tn: 222394.0000 - tp: 391.0000 - val_fn: 13.0000 - val_fp: 889.0000 - val_loss: 0.0676 - val_precision: 0.0854 - val_recall: 0.8646 - val_tn: 55976.0000 - val_tp: 83.0000
+## Epoch 17/30
+## 112/112 - 0s - 2ms/step - fn: 3.0000 - fp: 4639.0000 - loss: 4.5763e-07 - precision: 0.0781 - recall: 0.9924 - tn: 222811.0000 - tp: 393.0000 - val_fn: 11.0000 - val_fp: 1185.0000 - val_loss: 0.0669 - val_precision: 0.0669 - val_recall: 0.8854 - val_tn: 55680.0000 - val_tp: 85.0000
+## Epoch 18/30
+## 112/112 - 0s - 2ms/step - fn: 4.0000 - fp: 4355.0000 - loss: 4.5299e-07 - precision: 0.0826 - recall: 0.9899 - tn: 223095.0000 - tp: 392.0000 - val_fn: 9.0000 - val_fp: 1405.0000 - val_loss: 0.0772 - val_precision: 0.0583 - val_recall: 0.9062 - val_tn: 55460.0000 - val_tp: 87.0000
+## Epoch 19/30
+## 112/112 - 0s - 2ms/step - fn: 6.0000 - fp: 5779.0000 - loss: 4.8324e-07 - precision: 0.0632 - recall: 0.9848 - tn: 221671.0000 - tp: 390.0000 - val_fn: 10.0000 - val_fp: 1135.0000 - val_loss: 0.0546 - val_precision: 0.0704 - val_recall: 0.8958 - val_tn: 55730.0000 - val_tp: 86.0000
+## Epoch 20/30
+## 112/112 - 0s - 2ms/step - fn: 3.0000 - fp: 4748.0000 - loss: 3.7424e-07 - precision: 0.0764 - recall: 0.9924 - tn: 222702.0000 - tp: 393.0000 - val_fn: 9.0000 - val_fp: 895.0000 - val_loss: 0.0396 - val_precision: 0.0886 - val_recall: 0.9062 - val_tn: 55970.0000 - val_tp: 87.0000
+## Epoch 21/30
+## 112/112 - 0s - 2ms/step - fn: 3.0000 - fp: 3798.0000 - loss: 3.1417e-07 - precision: 0.0938 - recall: 0.9924 - tn: 223652.0000 - tp: 393.0000 - val_fn: 10.0000 - val_fp: 652.0000 - val_loss: 0.0309 - val_precision: 0.1165 - val_recall: 0.8958 - val_tn: 56213.0000 - val_tp: 86.0000
+## Epoch 22/30
+## 112/112 - 0s - 2ms/step - fn: 0.0000e+00 - fp: 2188.0000 - loss: 1.8112e-07 - precision: 0.1533 - recall: 1.0000 - tn: 225262.0000 - tp: 396.0000 - val_fn: 10.0000 - val_fp: 427.0000 - val_loss: 0.0250 - val_precision: 0.1676 - val_recall: 0.8958 - val_tn: 56438.0000 - val_tp: 86.0000
+## Epoch 23/30
+## 112/112 - 0s - 2ms/step - fn: 4.0000 - fp: 3066.0000 - loss: 3.4932e-07 - precision: 0.1134 - recall: 0.9899 - tn: 224384.0000 - tp: 392.0000 - val_fn: 11.0000 - val_fp: 1053.0000 - val_loss: 0.0494 - val_precision: 0.0747 - val_recall: 0.8854 - val_tn: 55812.0000 - val_tp: 85.0000
+## Epoch 24/30
+## 112/112 - 0s - 2ms/step - fn: 4.0000 - fp: 3175.0000 - loss: 5.1994e-07 - precision: 0.1099 - recall: 0.9899 - tn: 224275.0000 - tp: 392.0000 - val_fn: 11.0000 - val_fp: 1012.0000 - val_loss: 0.0997 - val_precision: 0.0775 - val_recall: 0.8854 - val_tn: 55853.0000 - val_tp: 85.0000
+## Epoch 25/30
+## 112/112 - 0s - 2ms/step - fn: 7.0000 - fp: 5874.0000 - loss: 6.5862e-07 - precision: 0.0621 - recall: 0.9823 - tn: 221576.0000 - tp: 389.0000 - val_fn: 9.0000 - val_fp: 3090.0000 - val_loss: 0.1296 - val_precision: 0.0274 - val_recall: 0.9062 - val_tn: 53775.0000 - val_tp: 87.0000
+## Epoch 26/30
+## 112/112 - 0s - 2ms/step - fn: 6.0000 - fp: 5395.0000 - loss: 5.9253e-07 - precision: 0.0674 - recall: 0.9848 - tn: 222055.0000 - tp: 390.0000 - val_fn: 9.0000 - val_fp: 1320.0000 - val_loss: 0.0928 - val_precision: 0.0618 - val_recall: 0.9062 - val_tn: 55545.0000 - val_tp: 87.0000
+## Epoch 27/30
+## 112/112 - 0s - 2ms/step - fn: 4.0000 - fp: 3887.0000 - loss: 7.1278e-07 - precision: 0.0916 - recall: 0.9899 - tn: 223563.0000 - tp: 392.0000 - val_fn: 12.0000 - val_fp: 572.0000 - val_loss: 0.0352 - val_precision: 0.1280 - val_recall: 0.8750 - val_tn: 56293.0000 - val_tp: 84.0000
+## Epoch 28/30
+## 112/112 - 0s - 2ms/step - fn: 3.0000 - fp: 2209.0000 - loss: 5.8153e-07 - precision: 0.1510 - recall: 0.9924 - tn: 225241.0000 - tp: 393.0000 - val_fn: 11.0000 - val_fp: 1373.0000 - val_loss: 0.0808 - val_precision: 0.0583 - val_recall: 0.8854 - val_tn: 55492.0000 - val_tp: 85.0000
+## Epoch 29/30
+## 112/112 - 0s - 2ms/step - fn: 1.0000 - fp: 2506.0000 - loss: 2.9031e-07 - precision: 0.1362 - recall: 0.9975 - tn: 224944.0000 - tp: 395.0000 - val_fn: 11.0000 - val_fp: 690.0000 - val_loss: 0.0401 - val_precision: 0.1097 - val_recall: 0.8854 - val_tn: 56175.0000 - val_tp: 85.0000
+## Epoch 30/30
+## 112/112 - 0s - 2ms/step - fn: 4.0000 - fp: 2719.0000 - loss: 6.1097e-07 - precision: 0.1260 - recall: 0.9899 - tn: 224731.0000 - tp: 392.0000 - val_fn: 13.0000 - val_fp: 861.0000 - val_loss: 0.0362 - val_precision: 0.0879 - val_recall: 0.8646 - val_tn: 56004.0000 - val_tp: 83.0000
+
+val_pred <- model %>%
+  predict(val_features) %>%
+  { as.integer(. > 0.5) }
+
## 1781/1781 - 1s - 311us/step
+
+pred_correct <- val_df$Class == val_pred
+cat(sprintf("Validation accuracy: %.2f", mean(pred_correct)))
+
## Validation accuracy: 0.98
+
+fraudulent <- val_df$Class == 1
+
+n_fraudulent_detected <- sum(fraudulent & pred_correct)
+n_fraudulent_missed <- sum(fraudulent & !pred_correct)
+n_legitimate_flagged <- sum(!fraudulent & !pred_correct)
+
+
+

Conclusions +

+

At the end of training, out of 56,961 validation transactions, we +are:

+
    +
  • Correctly identifying 83 of them as fraudulent
  • +
  • Missing 13 fraudulent transactions
  • +
  • At the cost of incorrectly flagging 861 legitimate transactions
  • +
+

In the real world, one would put an even higher weight on class 1, so +as to reflect that False Negatives are more costly than False +Positives.

+

Next time your credit card gets declined in an online purchase – this +is why.

+ + + + +
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/structured_data/structured_data_classification_with_feature_space.html b/docs/dev/articles/examples/structured_data/structured_data_classification_with_feature_space.html new file mode 100644 index 0000000000..7fee7212ee --- /dev/null +++ b/docs/dev/articles/examples/structured_data/structured_data_classification_with_feature_space.html @@ -0,0 +1,676 @@ + + + + + + + + +Structured data classification with FeatureSpace • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

This example demonstrates how to do structured data classification +(also known as tabular data classification), starting from a raw CSV +file. Our data includes numerical features, and integer categorical +features, and string categorical features. We will use the utility +layer_feature_space() to index, preprocess, and encode our +features.

+

The code is adapted from the example Structured +data classification from scratch. While the previous example managed +its own low-level feature preprocessing and encoding with Keras +preprocessing layers, in this example we delegate everything to +layer_feature_space(), making the workflow extremely quick +and easy.

+
+

The dataset +

+

Our +dataset is provided by the Cleveland Clinic Foundation for Heart +Disease. It’s a CSV file with 303 rows. Each row contains information +about a patient (a sample), and each column describes +an attribute of the patient (a feature). We use the +features to predict whether a patient has a heart disease +(binary classification).

+

Here’s the description of each feature:

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColumnDescriptionFeature Type
AgeAge in yearsNumerical
Sex(1 = male; 0 = female)Categorical
CPChest pain type (0, 1, 2, 3, 4)Categorical
TrestbpdResting blood pressure (in mm Hg on admission)Numerical
CholSerum cholesterol in mg/dlNumerical
FBSfasting blood sugar in 120 mg/dl (1 = true; 0 = false)Categorical
RestECGResting electrocardiogram results (0, 1, 2)Categorical
ThalachMaximum heart rate achievedNumerical
ExangExercise induced angina (1 = yes; 0 = no)Categorical
OldpeakST depression induced by exercise relative to restNumerical
SlopeSlope of the peak exercise ST segmentNumerical
CANumber of major vessels (0-3) colored by fluoroscopyBoth numerical & categorical
Thal3 = normal; 6 = fixed defect; 7 = reversible defectCategorical
TargetDiagnosis of heart disease (1 = true; 0 = false)Target
+
+
+
+

Setup +

+
+library(readr)
+library(dplyr, warn.conflicts = FALSE)
+library(keras3)
+library(tensorflow, exclude = c("shape", "set_random_seed"))
+library(tfdatasets, exclude = "shape")
+
+conflicted::conflicts_prefer(
+  keras3::shape(),
+  keras3::set_random_seed(),
+  dplyr::filter(),
+  .quiet = TRUE
+)
+
+use_backend("tensorflow")
+
+
+

Preparing the data +

+

Let’s download the data and load it into a Pandas dataframe:

+
+file_url <-
+  "http://storage.googleapis.com/download.tensorflow.org/data/heart.csv"
+df <- read_csv(file_url, col_types = cols(
+  oldpeak = col_double(),
+  thal = col_character(),
+  .default = col_integer()
+))
+
+# the dataset has two malformed rows, filter them out
+df <- df |> filter(!thal %in% c("1", "2"))
+

The dataset includes 303 samples with 14 columns per sample (13 +features, plus the target label)

+
+glimpse(df)
+
## Rows: 301
+## Columns: 14
+## $ age      <int> 63, 67, 67, 37, 41, 56, 62, 57, 63, 53, 57, 56, 56, 44, 5…
+## $ sex      <int> 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, …
+## $ cp       <int> 1, 4, 4, 3, 2, 2, 4, 4, 4, 4, 4, 2, 3, 2, 3, 3, 2, 4, 3, …
+## $ trestbps <int> 145, 160, 120, 130, 130, 120, 140, 120, 130, 140, 140, 14…
+## $ chol     <int> 233, 286, 229, 250, 204, 236, 268, 354, 254, 203, 192, 29…
+## $ fbs      <int> 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, …
+## $ restecg  <int> 2, 2, 2, 0, 2, 0, 2, 0, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0, 0, …
+## $ thalach  <int> 150, 108, 129, 187, 172, 178, 160, 163, 147, 155, 148, 15…
+## $ exang    <int> 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, …
+## $ oldpeak  <dbl> 2.3, 1.5, 2.6, 3.5, 1.4, 0.8, 3.6, 0.6, 1.4, 3.1, 0.4, 1.…
+## $ slope    <int> 3, 2, 2, 3, 1, 1, 3, 1, 2, 3, 2, 2, 2, 1, 1, 1, 3, 1, 1, …
+## $ ca       <int> 0, 3, 2, 0, 0, 0, 2, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, …
+## $ thal     <chr> "fixed", "normal", "reversible", "normal", "normal", "nor…
+## $ target   <int> 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, …
+

Here’s a preview of a few samples:

+
+df
+
## # A tibble: 301 × 14
+##      age   sex    cp trestbps  chol   fbs restecg thalach exang oldpeak
+##    <int> <int> <int>    <int> <int> <int>   <int>   <int> <int>   <dbl>
+##  1    63     1     1      145   233     1       2     150     0     2.3
+##  2    67     1     4      160   286     0       2     108     1     1.5
+##  3    67     1     4      120   229     0       2     129     1     2.6
+##  4    37     1     3      130   250     0       0     187     0     3.5
+##  5    41     0     2      130   204     0       2     172     0     1.4
+##  6    56     1     2      120   236     0       0     178     0     0.8
+##  7    62     0     4      140   268     0       2     160     0     3.6
+##  8    57     0     4      120   354     0       0     163     1     0.6
+##  9    63     1     4      130   254     0       2     147     0     1.4
+## 10    53     1     4      140   203     1       2     155     1     3.1
+## # ℹ 291 more rows
+## # ℹ 4 more variables: slope <int>, ca <int>, thal <chr>, target <int>
+

The last column, “target”, indicates whether the patient has a heart +disease (1) or not (0).

+

Let’s split the data into a training and validation set:

+
+val_idx <- nrow(df) %>% sample.int(., . * 0.2)
+val_df <- df[val_idx, ]
+train_df <- df[-val_idx, ]
+
+cat(sprintf(
+  "Using %d samples for training and %d for validation",
+  nrow(train_df), nrow(val_df)
+))
+
## Using 241 samples for training and 60 for validation
+

Let’s generate tf_dataset objects for each +dataframe:

+
+dataframe_to_dataset <- function(df) {
+  labels <- df |> pull(target) |> as.integer()
+  inputs <- df |> select(-target) |> as.list()
+
+  ds <- tensor_slices_dataset(list(inputs, labels)) |>
+    dataset_shuffle(nrow(df))
+
+  ds
+}
+
+train_ds <- dataframe_to_dataset(train_df)
+val_ds <- dataframe_to_dataset(val_df)
+

Each tf_dataset yields a tuple +(input, target) where input is a dictionary (a +named list) of features and target is the value +0 or 1:

+
+c(x, y) %<-% iter_next(as_iterator(train_ds))
+cat("Input: "); str(x)
+cat("Target: "); str(y)
+
## Input: List of 13
+##  $ age     :<tf.Tensor: shape=(), dtype=int32, numpy=59>
+##  $ sex     :<tf.Tensor: shape=(), dtype=int32, numpy=1>
+##  $ cp      :<tf.Tensor: shape=(), dtype=int32, numpy=4>
+##  $ trestbps:<tf.Tensor: shape=(), dtype=int32, numpy=164>
+##  $ chol    :<tf.Tensor: shape=(), dtype=int32, numpy=176>
+##  $ fbs     :<tf.Tensor: shape=(), dtype=int32, numpy=1>
+##  $ restecg :<tf.Tensor: shape=(), dtype=int32, numpy=2>
+##  $ thalach :<tf.Tensor: shape=(), dtype=int32, numpy=90>
+##  $ exang   :<tf.Tensor: shape=(), dtype=int32, numpy=0>
+##  $ oldpeak :<tf.Tensor: shape=(), dtype=float32, numpy=1.0>
+##  $ slope   :<tf.Tensor: shape=(), dtype=int32, numpy=2>
+##  $ ca      :<tf.Tensor: shape=(), dtype=int32, numpy=2>
+##  $ thal    :<tf.Tensor: shape=(), dtype=string, numpy=b'fixed'>
+## Target: <tf.Tensor: shape=(), dtype=int32, numpy=1>
+

Let’s batch the datasets:

+
+train_ds <- train_ds |> dataset_batch(32)
+val_ds <- val_ds |> dataset_batch(32)
+
+
+

Configuring a FeatureSpace +

+

To configure how each feature should be preprocessed, we instantiate +a layer_feature_space(), and we pass to it a dictionary +(named list with unique names) that maps the name of our features to a +string that describes the feature type.

+

We have a few “integer categorical” features such as +"FBS", one “string categorical” feature +("thal"), and a few numerical features, which we’d like to +normalize – except "age", which we’d like to discretize +into a number of bins.

+

We also use the crosses argument to capture feature +interactions for some categorical features, that is to say, create +additional features that represent value co-occurrences for these +categorical features. You can compute feature crosses like this for +arbitrary sets of categorical features – not just tuples of two +features. Because the resulting co-occurences are hashed into a +fixed-sized vector, you don’t need to worry about whether the +co-occurence space is too large.

+
+feature_space <- layer_feature_space(
+  features = list(
+    # Categorical features encoded as integers
+    sex = "integer_categorical",
+    cp = "integer_categorical",
+    fbs = "integer_categorical",
+    restecg = "integer_categorical",
+    exang = "integer_categorical",
+    ca = "integer_categorical",
+    # Categorical feature encoded as string
+    thal = "string_categorical",
+    # Numerical features to discretize
+    age = "float_discretized",
+    # Numerical features to normalize
+    trestbps = "float_normalized",
+    chol = "float_normalized",
+    thalach = "float_normalized",
+    oldpeak = "float_normalized",
+    slope = "float_normalized"
+  ),
+  # We create additional features by hashing
+  # value co-occurrences for the
+  # following groups of categorical features.
+  crosses = list(c("sex", "age"), c("thal", "ca")),
+  # The hashing space for these co-occurrences
+  # wil be 32-dimensional.
+  crossing_dim = 32,
+  # Our utility will one-hot encode all categorical
+  # features and concat all features into a single
+  # vector (one vector per sample).
+  output_mode = "concat"
+)
+
+
+

Further customizing a FeatureSpace +

+

Specifying the feature type via a string name is quick and easy, but +sometimes you may want to further configure the preprocessing of each +feature. For instance, in our case, our categorical features don’t have +a large set of possible values – it’s only a handful of values per +feature (e.g. 1 and 0 for the feature +"FBS"), and all possible values are represented in the +training set. As a result, we don’t need to reserve an index to +represent “out of vocabulary” values for these features – which would +have been the default behavior. Below, we just specify +num_oov_indices=0 in each of these features to tell the +feature preprocessor to skip “out of vocabulary” indexing.

+

Other customizations you have access to include specifying the number +of bins for discretizing features of type +"float_discretized", or the dimensionality of the hashing +space for feature crossing.

+
+feature_space <- layer_feature_space(
+  features = list(
+    # Categorical features encoded as integers
+    sex       = feature_integer_categorical(num_oov_indices = 0),
+    cp        = feature_integer_categorical(num_oov_indices = 0),
+    fbs       = feature_integer_categorical(num_oov_indices = 0),
+    restecg   = feature_integer_categorical(num_oov_indices = 0),
+    exang     = feature_integer_categorical(num_oov_indices = 0),
+    ca        = feature_integer_categorical(num_oov_indices = 0),
+    # Categorical feature encoded as string
+    thal      = feature_string_categorical(num_oov_indices = 0),
+    # Numerical features to discretize
+    age       = feature_float_discretized(num_bins = 30),
+    # Numerical features to normalize
+    trestbps  = feature_float_normalized(),
+    chol      = feature_float_normalized(),
+    thalach   = feature_float_normalized(),
+    oldpeak   = feature_float_normalized(),
+    slope     = feature_float_normalized()
+  ),
+  # Specify feature cross with a custom crossing dim.
+  crosses = list(
+    feature_cross(
+      feature_names = c("sex", "age"),
+      crossing_dim = 64
+    ),
+    feature_cross(
+      feature_names = c("thal", "ca"),
+      crossing_dim = 16
+    )
+  ),
+  output_mode = "concat"
+)
+
+
+

Adapt the FeatureSpace to the training data +

+

Before we start using the FeatureSpace to build a model, +we have to adapt it to the training data. During adapt(), +the FeatureSpace will:

+
    +
  • Index the set of possible values for categorical features.
  • +
  • Compute the mean and variance for numerical features to +normalize.
  • +
  • Compute the value boundaries for the different bins for numerical +features to discretize.
  • +
+

Note that adapt() should be called on a +tf_dataset which yields dicts (named lists) of feature +values – no labels.

+
+train_ds_with_no_labels <- train_ds |> dataset_map(\(x, y) x)
+feature_space |> adapt(train_ds_with_no_labels)
+

At this point, the FeatureSpace can be called on a dict +of raw feature values, and will return a single concatenate vector for +each sample, combining encoded features and feature crosses.

+
+c(x, y) %<-% iter_next(as_iterator(train_ds))
+preprocessed_x <- feature_space(x)
+preprocessed_x
+
## tf.Tensor(
+## [[0. 0. 1. ... 0. 0. 0.]
+##  [0. 0. 0. ... 0. 0. 0.]
+##  [0. 0. 0. ... 0. 0. 0.]
+##  ...
+##  [0. 0. 0. ... 0. 0. 0.]
+##  [0. 0. 0. ... 0. 0. 0.]
+##  [0. 0. 0. ... 0. 0. 0.]], shape=(32, 136), dtype=float32)
+
+
+

Two ways to manage preprocessing: as part of the +tf.data pipeline, or in the model itself +

+

There are two ways in which you can leverage your +FeatureSpace:

+
+

Asynchronous preprocessing in tf.data +

+

You can make it part of your data pipeline, before the model. This +enables asynchronous parallel preprocessing of the data on CPU before it +hits the model. Do this if you’re training on GPU or TPU, or if you want +to speed up preprocessing. Usually, this is always the right thing to do +during training.

+
+
+

Synchronous preprocessing in the model +

+

You can make it part of your model. This means that the model will +expect dicts of raw feature values, and the preprocessing batch will be +done synchronously (in a blocking manner) before the rest of the forward +pass. Do this if you want to have an end-to-end model that can process +raw feature values – but keep in mind that your model will only be able +to run on CPU, since most types of feature preprocessing (e.g. string +preprocessing) are not GPU or TPU compatible.

+

Do not do this on GPU / TPU or in performance-sensitive settings. In +general, you want to do in-model preprocessing when you do inference on +CPU.

+

In our case, we will apply the FeatureSpace in the +tf.data pipeline during training, but we will do inference with an +end-to-end model that includes the FeatureSpace.

+

Let’s create a training and validation dataset of preprocessed +batches:

+
+preprocessed_train_ds <- train_ds |>
+  dataset_map(\(x, y) list(feature_space(x), y),
+              num_parallel_calls = tf$data$AUTOTUNE) |>
+  dataset_prefetch(tf$data$AUTOTUNE)
+
+preprocessed_val_ds <- val_ds |>
+  dataset_map(\(x, y) list(feature_space(x), y),
+              num_parallel_calls = tf$data$AUTOTUNE) |>
+  dataset_prefetch(tf$data$AUTOTUNE)
+
+
+
+

Build a model +

+

Time to build a model – or rather two models:

+
    +
  • A training model that expects preprocessed features (one sample = +one vector)
  • +
  • An inference model that expects raw features (one sample = dict of +raw feature values)
  • +
+
+dict_inputs <- feature_space$get_inputs()
+encoded_features <- feature_space$get_encoded_features()
+
+predictions <- encoded_features |>
+  layer_dense(32, activation="relu") |>
+  layer_dropout(0.5) |>
+  layer_dense(1, activation="sigmoid")
+
+training_model <- keras_model(inputs = encoded_features,
+                              outputs = predictions)
+training_model |> compile(optimizer = "adam",
+                          loss = "binary_crossentropy",
+                          metrics = "accuracy")
+
+inference_model <- keras_model(inputs = dict_inputs,
+                               outputs = predictions)
+
+
+

Train the model +

+

Let’s train our model for 20 epochs. Note that feature preprocessing +is happening as part of the tf.data pipeline, not as part of the +model.

+
+training_model |> fit(
+  preprocessed_train_ds,
+  epochs = 20,
+  validation_data = preprocessed_val_ds,
+  verbose = 2
+)
+
## Epoch 1/20
+## 8/8 - 3s - 326ms/step - accuracy: 0.4315 - loss: 0.7427 - val_accuracy: 0.5333 - val_loss: 0.7048
+## Epoch 2/20
+## 8/8 - 0s - 13ms/step - accuracy: 0.5311 - loss: 0.6984 - val_accuracy: 0.6500 - val_loss: 0.6438
+## Epoch 3/20
+## 8/8 - 0s - 13ms/step - accuracy: 0.6473 - loss: 0.6316 - val_accuracy: 0.6833 - val_loss: 0.5937
+## Epoch 4/20
+## 8/8 - 0s - 13ms/step - accuracy: 0.6639 - loss: 0.6134 - val_accuracy: 0.7500 - val_loss: 0.5524
+## Epoch 5/20
+## 8/8 - 0s - 12ms/step - accuracy: 0.7178 - loss: 0.5820 - val_accuracy: 0.7667 - val_loss: 0.5176
+## Epoch 6/20
+## 8/8 - 0s - 13ms/step - accuracy: 0.7718 - loss: 0.5573 - val_accuracy: 0.7500 - val_loss: 0.4897
+## Epoch 7/20
+## 8/8 - 0s - 12ms/step - accuracy: 0.7718 - loss: 0.5200 - val_accuracy: 0.8167 - val_loss: 0.4640
+## Epoch 8/20
+## 8/8 - 0s - 14ms/step - accuracy: 0.7759 - loss: 0.5068 - val_accuracy: 0.8167 - val_loss: 0.4388
+## Epoch 9/20
+## 8/8 - 0s - 12ms/step - accuracy: 0.8174 - loss: 0.4724 - val_accuracy: 0.8333 - val_loss: 0.4162
+## Epoch 10/20
+## 8/8 - 0s - 12ms/step - accuracy: 0.8050 - loss: 0.4545 - val_accuracy: 0.8167 - val_loss: 0.3960
+## Epoch 11/20
+## 8/8 - 0s - 13ms/step - accuracy: 0.8091 - loss: 0.4514 - val_accuracy: 0.8500 - val_loss: 0.3786
+## Epoch 12/20
+## 8/8 - 0s - 12ms/step - accuracy: 0.8423 - loss: 0.4291 - val_accuracy: 0.8500 - val_loss: 0.3647
+## Epoch 13/20
+## 8/8 - 0s - 12ms/step - accuracy: 0.8465 - loss: 0.4028 - val_accuracy: 0.8667 - val_loss: 0.3499
+## Epoch 14/20
+## 8/8 - 0s - 13ms/step - accuracy: 0.8340 - loss: 0.4037 - val_accuracy: 0.8667 - val_loss: 0.3369
+## Epoch 15/20
+## 8/8 - 0s - 12ms/step - accuracy: 0.8548 - loss: 0.3928 - val_accuracy: 0.8667 - val_loss: 0.3289
+## Epoch 16/20
+## 8/8 - 0s - 12ms/step - accuracy: 0.8589 - loss: 0.3745 - val_accuracy: 0.8667 - val_loss: 0.3206
+## Epoch 17/20
+## 8/8 - 0s - 12ms/step - accuracy: 0.8257 - loss: 0.3820 - val_accuracy: 0.8667 - val_loss: 0.3129
+## Epoch 18/20
+## 8/8 - 0s - 14ms/step - accuracy: 0.8631 - loss: 0.3650 - val_accuracy: 0.8667 - val_loss: 0.3079
+## Epoch 19/20
+## 8/8 - 0s - 12ms/step - accuracy: 0.8382 - loss: 0.3635 - val_accuracy: 0.8667 - val_loss: 0.3024
+## Epoch 20/20
+## 8/8 - 0s - 13ms/step - accuracy: 0.8631 - loss: 0.3524 - val_accuracy: 0.8833 - val_loss: 0.2970
+

We quickly get to 80% validation accuracy.

+
+
+

Inference on new data with the end-to-end model +

+

Now, we can use our inference model (which includes the +FeatureSpace) to make predictions based on dicts of raw +features values, as follows:

+
+sample <- list(
+  age = 60,
+  sex = 1,
+  cp = 1,
+  trestbps = 145,
+  chol = 233,
+  fbs = 1,
+  restecg = 2,
+  thalach = 150,
+  exang = 0,
+  oldpeak = 2.3,
+  slope = 3,
+  ca = 0,
+  thal = "fixed"
+)
+
+input_dict <- lapply(sample, \(x) op_convert_to_tensor(array(x)))
+predictions <- inference_model |> predict(input_dict)
+
## 1/1 - 0s - 257ms/step
+
+glue::glue(r"---(
+  This particular patient had a {(100 * predictions) |> signif(3)}% probability
+  of having a heart disease, as evaluated by our model.
+)---")
+
## This particular patient had a 49.7% probability
+## of having a heart disease, as evaluated by our model.
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection.html b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection.html new file mode 100644 index 0000000000..a79f49fb88 --- /dev/null +++ b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection.html @@ -0,0 +1,606 @@ + + + + + + + + +Timeseries anomaly detection using an Autoencoder • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

This script demonstrates how you can use a reconstruction +convolutional autoencoder model to detect anomalies in timeseries +data.

+
+
+

Setup +

+
+library(dplyr, warn.conflicts = FALSE)
+library(ggplot2)
+theme_set(theme_minimal())
+
+library(listarrays)
+library(tfdatasets, exclude = c("shape"))
+library(keras3)
+
+
+

Load the data +

+

We will use the Numenta Anomaly +Benchmark(NAB) dataset. It provides artificial timeseries data +containing labeled anomalous periods of behavior. Data are ordered, +timestamped, single-valued metrics.

+

We will use the art_daily_small_noise.csv file for +training and the art_daily_jumpsup.csv file for testing. +The simplicity of this dataset allows us to demonstrate anomaly +detection.

+
+get_data <- function(url_suffix) {
+  url_root <- "https://raw.githubusercontent.com/numenta/NAB/master/data/"
+  url <- paste0(url_root, url_suffix)
+  file <- get_file(origin = url) # cache file locally
+  # parse csv; 2 columns with types: datetime (T), double (d)
+  readr::read_csv(file, col_types = "Td")
+}
+
+df_small_noise   <- get_data("artificialNoAnomaly/art_daily_small_noise.csv")
+df_daily_jumpsup <- get_data("artificialWithAnomaly/art_daily_jumpsup.csv")
+
+
+

Quick look at the data +

+
+df_small_noise
+
## # A tibble: 4,032 × 2
+##    timestamp           value
+##    <dttm>              <dbl>
+##  1 2014-04-01 00:00:00  18.3
+##  2 2014-04-01 00:05:00  22.0
+##  3 2014-04-01 00:10:00  18.6
+##  4 2014-04-01 00:15:00  22.0
+##  5 2014-04-01 00:20:00  21.9
+##  6 2014-04-01 00:25:00  21.2
+##  7 2014-04-01 00:30:00  20.6
+##  8 2014-04-01 00:35:00  20.3
+##  9 2014-04-01 00:40:00  21.5
+## 10 2014-04-01 00:45:00  19.2
+## # ℹ 4,022 more rows
+
+df_daily_jumpsup
+
## # A tibble: 4,032 × 2
+##    timestamp           value
+##    <dttm>              <dbl>
+##  1 2014-04-01 00:00:00  19.8
+##  2 2014-04-01 00:05:00  20.5
+##  3 2014-04-01 00:10:00  20.0
+##  4 2014-04-01 00:15:00  21.5
+##  5 2014-04-01 00:20:00  20.2
+##  6 2014-04-01 00:25:00  19.9
+##  7 2014-04-01 00:30:00  21.7
+##  8 2014-04-01 00:35:00  20.9
+##  9 2014-04-01 00:40:00  18.4
+## 10 2014-04-01 00:45:00  18.7
+## # ℹ 4,022 more rows
+
+
+

Visualize the data +

+
+

Timeseries data without anomalies +

+

We will use the following data for training.

+
+plot_ts <- function(df) {
+  ggplot(df, aes(x = timestamp, y = value)) + geom_line() +
+    scale_x_datetime(date_breaks = "1 day", date_labels = "%b-%d")
+}
+
+plot_ts(df_small_noise) + ggtitle("Without Anomaly")
+
+plot of chunk unnamed-chunk-4
plot of chunk unnamed-chunk-4
+
+
+
+

Timeseries data with anomalies +

+

We will use the following data for testing and see if the sudden jump +up in the data is detected as an anomaly.

+
+plot_ts(df_daily_jumpsup) + ggtitle("With Anomaly")
+
+plot of chunk unnamed-chunk-5
plot of chunk unnamed-chunk-5
+
+
+
+
+

Prepare training data +

+

Get data values from the training timeseries data file and normalize +the value data. We have a value for every 5 +mins for 14 days.

+
    +
  • 24 * 60 / 5 = 288 timesteps per day +
  • +
  • 288 * 14 = 4032 data points in total
  • +
+
+df_train <- df_small_noise |>
+  mutate(value = (value - mean(value)) / sd(value))
+
+cat("Number of training samples:", nrow(df_train), "\n")
+
## Number of training samples: 4032
+
+

Create sequences +

+

Create sequences combining TIME_STEPS contiguous data +values from the training data.

+
+TIME_STEPS <- 288
+
+as_dataset <- function(df) {
+  x <- as.matrix(df$value)
+  ds <- timeseries_dataset_from_array(x, NULL, sequence_length = TIME_STEPS)
+  # Because the dataset is small, cast TF Dataset to an R array for convenience.
+  ds |> as_array_iterator() |> iterate() |> bind_on_rows()
+}
+
+x_train <- as_dataset(df_train)
+writeLines(sprintf("Training input shape: (%s)", toString(dim(x_train))))
+
## Training input shape: (3745, 288, 1)
+
+
+
+

Build a model +

+

We will build a convolutional reconstruction autoencoder model. The +model will take input of shape +(batch_size, sequence_length, num_features) and return +output of the same shape. In this case, sequence_length is +288 and num_features is 1.

+
+model <- keras_model_sequential(input_shape = c(TIME_STEPS, 1)) |>
+  layer_conv_1d(
+    filters = 32, kernel_size = 7, padding = "same",
+    strides = 2, activation = "relu"
+  ) |>
+  layer_dropout(rate = 0.2) |>
+  layer_conv_1d(
+    filters = 16, kernel_size = 7, padding = "same",
+    strides = 2, activation = "relu"
+  ) |>
+  layer_conv_1d_transpose(
+    filters = 16, kernel_size = 7, padding = "same",
+    strides = 2, activation = "relu"
+  ) |>
+  layer_dropout(rate = 0.2) |>
+  layer_conv_1d_transpose(
+    filters = 32, kernel_size = 7, padding = "same",
+    strides = 2, activation = "relu"
+  ) |>
+  layer_conv_1d_transpose(filters = 1, kernel_size = 7, padding = "same")
+
+model |> compile(optimizer=optimizer_adam(learning_rate=0.001), loss="mse")
+model
+
## Model: "sequential"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ conv1d (Conv1D)                 │ (None, 144, 32)        │           256
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dropout (Dropout)               │ (None, 144, 32)        │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv1d_1 (Conv1D)               │ (None, 72, 16)         │         3,600
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv1d_transpose                │ (None, 144, 16)        │         1,808
+## │ (Conv1DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dropout_1 (Dropout)             │ (None, 144, 16)        │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv1d_transpose_1              │ (None, 288, 32)        │         3,616
+## │ (Conv1DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv1d_transpose_2              │ (None, 288, 1)         │           225
+## │ (Conv1DTranspose)               │                        │               │
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 9,505 (37.13 KB)
+##  Trainable params: 9,505 (37.13 KB)
+##  Non-trainable params: 0 (0.00 B)
+
+
+

Train the model +

+

Please note that we are using x_train as both the input +and the target since this is a reconstruction model.

+
+history = model |> fit(
+  x_train, x_train,
+  epochs = 50,
+  validation_split = 0.1,
+  callbacks = c(
+    callback_early_stopping(
+      monitor = "val_loss", patience = 5, mode = "min"
+    )
+  )
+)
+
## Epoch 1/50
+## 106/106 - 7s - 62ms/step - loss: 0.1863 - val_loss: 0.0316
+## Epoch 2/50
+## 106/106 - 0s - 2ms/step - loss: 0.0333 - val_loss: 0.0233
+## Epoch 3/50
+## 106/106 - 0s - 2ms/step - loss: 0.0248 - val_loss: 0.0201
+## Epoch 4/50
+## 106/106 - 0s - 2ms/step - loss: 0.0209 - val_loss: 0.0186
+## Epoch 5/50
+## 106/106 - 0s - 2ms/step - loss: 0.0179 - val_loss: 0.0146
+## Epoch 6/50
+## 106/106 - 0s - 2ms/step - loss: 0.0150 - val_loss: 0.0113
+## Epoch 7/50
+## 106/106 - 0s - 2ms/step - loss: 0.0127 - val_loss: 0.0094
+## Epoch 8/50
+## 106/106 - 0s - 2ms/step - loss: 0.0109 - val_loss: 0.0088
+## Epoch 9/50
+## 106/106 - 0s - 3ms/step - loss: 0.0096 - val_loss: 0.0084
+## Epoch 10/50
+## 106/106 - 0s - 2ms/step - loss: 0.0086 - val_loss: 0.0069
+## Epoch 11/50
+## 106/106 - 0s - 2ms/step - loss: 0.0078 - val_loss: 0.0062
+## Epoch 12/50
+## 106/106 - 0s - 2ms/step - loss: 0.0073 - val_loss: 0.0058
+## Epoch 13/50
+## 106/106 - 0s - 2ms/step - loss: 0.0067 - val_loss: 0.0054
+## Epoch 14/50
+## 106/106 - 0s - 2ms/step - loss: 0.0063 - val_loss: 0.0051
+## Epoch 15/50
+## 106/106 - 0s - 2ms/step - loss: 0.0060 - val_loss: 0.0045
+## Epoch 16/50
+## 106/106 - 0s - 2ms/step - loss: 0.0058 - val_loss: 0.0039
+## Epoch 17/50
+## 106/106 - 0s - 2ms/step - loss: 0.0055 - val_loss: 0.0041
+## Epoch 18/50
+## 106/106 - 0s - 2ms/step - loss: 0.0052 - val_loss: 0.0036
+## Epoch 19/50
+## 106/106 - 0s - 2ms/step - loss: 0.0050 - val_loss: 0.0037
+## Epoch 20/50
+## 106/106 - 0s - 2ms/step - loss: 0.0048 - val_loss: 0.0033
+## Epoch 21/50
+## 106/106 - 0s - 2ms/step - loss: 0.0046 - val_loss: 0.0031
+## Epoch 22/50
+## 106/106 - 0s - 2ms/step - loss: 0.0044 - val_loss: 0.0032
+## Epoch 23/50
+## 106/106 - 0s - 2ms/step - loss: 0.0042 - val_loss: 0.0033
+## Epoch 24/50
+## 106/106 - 0s - 2ms/step - loss: 0.0040 - val_loss: 0.0031
+## Epoch 25/50
+## 106/106 - 0s - 2ms/step - loss: 0.0038 - val_loss: 0.0032
+## Epoch 26/50
+## 106/106 - 0s - 2ms/step - loss: 0.0036 - val_loss: 0.0031
+## Epoch 27/50
+## 106/106 - 0s - 2ms/step - loss: 0.0035 - val_loss: 0.0031
+## Epoch 28/50
+## 106/106 - 0s - 2ms/step - loss: 0.0033 - val_loss: 0.0025
+## Epoch 29/50
+## 106/106 - 0s - 2ms/step - loss: 0.0032 - val_loss: 0.0026
+## Epoch 30/50
+## 106/106 - 0s - 2ms/step - loss: 0.0031 - val_loss: 0.0029
+## Epoch 31/50
+## 106/106 - 0s - 2ms/step - loss: 0.0030 - val_loss: 0.0024
+## Epoch 32/50
+## 106/106 - 0s - 2ms/step - loss: 0.0029 - val_loss: 0.0025
+## Epoch 33/50
+## 106/106 - 0s - 2ms/step - loss: 0.0028 - val_loss: 0.0025
+## Epoch 34/50
+## 106/106 - 0s - 2ms/step - loss: 0.0027 - val_loss: 0.0022
+## Epoch 35/50
+## 106/106 - 0s - 2ms/step - loss: 0.0026 - val_loss: 0.0023
+## Epoch 36/50
+## 106/106 - 0s - 2ms/step - loss: 0.0026 - val_loss: 0.0025
+## Epoch 37/50
+## 106/106 - 0s - 2ms/step - loss: 0.0025 - val_loss: 0.0025
+## Epoch 38/50
+## 106/106 - 0s - 2ms/step - loss: 0.0025 - val_loss: 0.0023
+## Epoch 39/50
+## 106/106 - 0s - 2ms/step - loss: 0.0024 - val_loss: 0.0023
+

Let’s plot training and validation loss to see how the training +went.

+
+plot(history)
+
+plot of chunk unnamed-chunk-10
plot of chunk unnamed-chunk-10
+
+
+
+

Detecting anomalies +

+

We will detect anomalies by determining how well our model can +reconstruct the input data.

+
    +
  1. Find MAE loss on training samples.
  2. +
  3. Find max MAE loss value. This is the worst our model has performed +trying to reconstruct a sample. We will make this the +threshold for anomaly detection.
  4. +
  5. If the reconstruction loss for a sample is greater than this +threshold value then we can infer that the model is seeing +a pattern that it isn’t familiar with. We will label this sample as an +anomaly.
  6. +
+
+# Get train MAE loss.
+x_train_pred <- model |> predict(x_train)
+
## 118/118 - 0s - 3ms/step
+
+train_mae_loss <- apply(abs(x_train_pred - x_train), 1, mean)
+
+hist(train_mae_loss, breaks = 50)
+
+plot of chunk unnamed-chunk-11
plot of chunk unnamed-chunk-11
+
+
+# Get reconstruction loss threshold.
+threshold <- max(train_mae_loss)
+cat("Reconstruction error threshold: ", threshold, "\n")
+
## Reconstruction error threshold:  0.03930207
+
+

Compare recontruction +

+

Just for fun, let’s see how our model has recontructed the first +sample. This is the 288 timesteps from day 1 of our training +dataset.

+
+# Checking how the first sequence is learnt
+plot(NULL, NULL, ylab = 'Value',
+     xlim = c(0, TIME_STEPS),
+     ylim = range(c(x_train[1,,], x_train_pred[1,,])))
+lines(x_train[1,,])
+lines(x_train_pred[1,,], col = 'red')
+legend("topleft", lty = 1,
+       legend = c("actual", "predicted"),
+       col = c("black", "red"))
+
+plot of chunk unnamed-chunk-12
plot of chunk unnamed-chunk-12
+
+
+
+

Prepare test data +

+
+df_test <- df_daily_jumpsup |>
+  mutate(value =
+           (value - mean(df_small_noise$value)) /
+             sd(df_small_noise$value))
+
+df_test |> head()
+plot_ts(df_test)
+
+plot of chunk unnamed-chunk-13
plot of chunk unnamed-chunk-13
+
+
+# Create sequences from test values.
+x_test <- as_dataset(df_test)
+
+# Get test MAE loss.
+x_test_pred <- model |> predict(x_test)
+test_mae_loss <- apply(abs(x_test_pred - x_test), 1, mean)
+
+hist(test_mae_loss, breaks = 50, xlab = "test MAE loss", ylab = "No of samples")
+
+plot of chunk unnamed-chunk-13
plot of chunk unnamed-chunk-13
+
+
+# Detect all the samples which are anomalies.
+anomalies <- test_mae_loss > threshold
+cat("Number of anomaly samples:", sum(anomalies), "\n")
+cat("Indices of anomaly samples:", which(anomalies), "\n", fill = TRUE)
+
## # A tibble: 6 × 2
+##   timestamp            value
+##   <dttm>               <dbl>
+## 1 2014-04-01 00:00:00 -0.808
+## 2 2014-04-01 00:05:00 -0.781
+## 3 2014-04-01 00:10:00 -0.801
+## 4 2014-04-01 00:15:00 -0.746
+## 5 2014-04-01 00:20:00 -0.792
+## 6 2014-04-01 00:25:00 -0.802
+## 118/118 - 0s - 763us/step
+## Number of anomaly samples: 486
+## Indices of anomaly samples: 216 218 220 396 507 793 795 1659 1945 1946 1947
+## 1949 2013 2016 2017 2021 2025 2029 2033 2037 2041 2045 2046 2049 2052 2053
+## 2054 2056 2057 2058 2060 2061 2062 2064 2065 2066 2068 2069 2070 2072 2073
+## 2074 2076 2077 2078 2080 2081 2082 2084 2085 2086 2088 2089 2090 2092 2093
+## 2094 2096 2097 2098 2100 2101 2102 2105 2108 2109 2112 2113 2117 2121 2124
+## 2126 2129 2141 2145 2153 2521 2522 2523 2525 2538 2542 2546 2550 2698 2700
+## 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716
+## 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731
+## 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746
+## 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761
+## 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776
+## 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791
+## 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806
+## 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821
+## 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836
+## 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851
+## 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866
+## 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881
+## 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896
+## 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911
+## 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926
+## 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941
+## 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956
+## 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971
+## 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986
+## 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001
+## 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016
+## 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031
+## 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046
+## 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061
+## 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076
+## 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091
+## 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101
+
+
+
+

Plot anomalies +

+

We now know the samples of the data which are anomalies. With this, +we will find the corresponding timestamps from the original +test data. We will be using the following method to do that:

+

Let’s say time_steps = 3 and we have 10 training values. Our +x_train will look like this:

+
    +
  • 0, 1, 2
  • +
  • 1, 2, 3
  • +
  • 2, 3, 4
  • +
  • 3, 4, 5
  • +
  • 4, 5, 6
  • +
  • 5, 6, 7
  • +
  • 6, 7, 8
  • +
  • 7, 8, 9
  • +
+

All except the initial and the final time_steps-1 data values, will +appear in time_steps number of samples. So, if we know that +the samples [(3, 4, 5), (4, 5, 6), (5, 6, 7)] are anomalies, we can say +that the data point 5 is an anomaly.

+

Let’s overlay the anomalies on the original test data plot.

+
+is_anomaly <- test_mae_loss > threshold
+is_anomaly <- is_anomaly &
+  zoo::rollsum(is_anomaly, TIME_STEPS,
+               align = "right", na.pad = TRUE) >= TIME_STEPS
+
+with(df_test, {
+  plot(value ~ timestamp, type = 'l', xaxt = 'n', las = 2)
+  axis.POSIXct(1, at = seq(timestamp[1], tail(timestamp, 1), by = "days"),
+               format = "%b-%d")
+})
+
+with(df_test[which(is_anomaly),], {
+  points(value ~ timestamp, col = "red")
+})
+
+plot of chunk unnamed-chunk-14
plot of chunk unnamed-chunk-14
+
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-10-1.png b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-10-1.png new file mode 100644 index 0000000000..2e6ffed7e7 Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-10-1.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-11-1.png b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-11-1.png new file mode 100644 index 0000000000..4bfcae3ae3 Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-11-1.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-12-1.png b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-12-1.png new file mode 100644 index 0000000000..2a620f180e Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-12-1.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-13-1.png b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-13-1.png new file mode 100644 index 0000000000..d3ec5b90fe Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-13-1.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-13-2.png b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-13-2.png new file mode 100644 index 0000000000..d47de13cd4 Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-13-2.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-14-1.png b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-14-1.png new file mode 100644 index 0000000000..61609bdd62 Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-14-1.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-4-1.png b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-4-1.png new file mode 100644 index 0000000000..f788185b05 Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-4-1.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-5-1.png b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-5-1.png new file mode 100644 index 0000000000..9700adcd2a Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_anomaly_detection/unnamed-chunk-5-1.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch.html b/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch.html new file mode 100644 index 0000000000..b0fa7a217f --- /dev/null +++ b/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch.html @@ -0,0 +1,1081 @@ + + + + + + + + +Timeseries classification from scratch • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

This example shows how to do timeseries classification from scratch, +starting from raw CSV timeseries files on disk. We demonstrate the +workflow on the FordA dataset from the UCR/UEA +archive.

+
+
+

Setup +

+ +
+
+

Load the data: the FordA dataset +

+
+

Dataset description +

+

The dataset we are using here is called FordA. The data comes from +the UCR archive. The dataset contains 3601 training instances and +another 1320 testing instances. Each timeseries corresponds to a +measurement of engine noise captured by a motor sensor. For this task, +the goal is to automatically detect the presence of a specific issue +with the engine. The problem is a balanced binary classification task. +The full description of this dataset can be found here.

+
+
+

Read the TSV data +

+

We will use the FordA_TRAIN file for training and the +FordA_TEST file for testing. The simplicity of this dataset +allows us to demonstrate effectively how to use ConvNets for timeseries +classification. In this file, the first column corresponds to the +label.

+
+get_data <- function(path) {
+  if(path |> startsWith("https://"))
+    path <- get_file(origin = path)  # cache file locally
+
+  data <- readr::read_tsv(
+    path, col_names = FALSE,
+    # Each row is: one integer (the label),
+    # followed by 500 doubles (the timeseries)
+    col_types = paste0("i", strrep("d", 500))
+  )
+
+  y <- as.matrix(data[[1]])
+  x <- as.matrix(data[,-1])
+  dimnames(x) <- dimnames(y) <- NULL
+
+  list(x, y)
+}
+
+root_url <- "https://raw.githubusercontent.com/hfawaz/cd-diagram/master/FordA/"
+c(x_train, y_train) %<-% get_data(paste0(root_url, "FordA_TRAIN.tsv"))
+c(x_test, y_test) %<-% get_data(paste0(root_url, "FordA_TEST.tsv"))
+
+str(keras3:::named_list(
+  x_train, y_train,
+  x_test, y_test
+))
+
## List of 4
+##  $ x_train: num [1:3601, 1:500] -0.797 0.805 0.728 -0.234 -0.171 ...
+##  $ y_train: int [1:3601, 1] -1 1 -1 -1 -1 1 1 1 1 1 ...
+##  $ x_test : num [1:1320, 1:500] -0.14 0.334 0.717 1.24 -1.159 ...
+##  $ y_test : int [1:1320, 1] -1 -1 -1 1 -1 1 -1 -1 1 1 ...
+
+
+
+

Visualize the data +

+

Here we visualize one timeseries example for each class in the +dataset.

+
+plot(NULL, main = "Timeseries Data",
+     xlab = "Timepoints",  ylab = "Values",
+     xlim = c(1, ncol(x_test)),
+     ylim = range(x_test))
+grid()
+lines(x_test[match(-1, y_test), ], col = "blue")
+lines(x_test[match( 1, y_test), ], col = "red")
+legend("topright", legend=c("label -1", "label 1"), col=c("blue", "red"), lty=1)
+
+Plot of Example Timeseries Data
Plot of Example Timeseries Data
+
+
+
+

Standardize the data +

+

Our timeseries are already in a single length (500). However, their +values are usually in various ranges. This is not ideal for a neural +network; in general we should seek to make the input values normalized. +For this specific dataset, the data is already z-normalized: each +timeseries sample has a mean equal to zero and a standard deviation +equal to one. This type of normalization is very common for timeseries +classification problems, see Bagnall +et al. (2016).

+

Note that the timeseries data used here are univariate, meaning we +only have one channel per timeseries example. We will therefore +transform the timeseries into a multivariate one with one channel using +a simple reshaping via numpy. This will allow us to construct a model +that is easily applicable to multivariate time series.

+
+dim(x_train) <- c(dim(x_train), 1)
+dim(x_test) <- c(dim(x_test), 1)
+

Finally, in order to use +sparse_categorical_crossentropy, we will have to count the +number of classes beforehand.

+
+num_classes <- length(unique(y_train))
+

Now we shuffle the training set because we will be using the +validation_split option later when training.

+
+c(x_train, y_train) %<-% listarrays::shuffle_rows(x_train, y_train)
+# idx <- sample.int(nrow(x_train))
+# x_train %<>% .[idx,, ,drop = FALSE]
+# y_train %<>% .[idx,  ,drop = FALSE]
+

Standardize the labels to positive integers. The expected labels will +then be 0 and 1.

+
+y_train[y_train == -1L] <- 0L
+y_test[y_test == -1L] <- 0L
+
+
+

Build a model +

+

We build a Fully Convolutional Neural Network originally proposed in +this paper. The +implementation is based on the TF 2 version provided here. The following +hyperparameters (kernel_size, filters, the usage of BatchNorm) were +found via random search using KerasTuner.

+
+make_model <- function(input_shape) {
+  inputs <- keras_input(input_shape)
+
+  outputs <- inputs |>
+    # conv1
+    layer_conv_1d(filters = 64, kernel_size = 3, padding = "same") |>
+    layer_batch_normalization() |>
+    layer_activation_relu() |>
+    # conv2
+    layer_conv_1d(filters = 64, kernel_size = 3, padding = "same") |>
+    layer_batch_normalization() |>
+    layer_activation_relu() |>
+    # conv3
+    layer_conv_1d(filters = 64, kernel_size = 3, padding = "same") |>
+    layer_batch_normalization() |>
+    layer_activation_relu() |>
+    # pooling
+    layer_global_average_pooling_1d() |>
+    # final output
+    layer_dense(num_classes, activation = "softmax")
+
+  keras_model(inputs, outputs)
+}
+
+model <- make_model(input_shape = dim(x_train)[-1])
+
+model
+
## Model: "functional_1"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━┓
+## ┃ Layer (type)                 Output Shape              Param #  Trai… 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━┩
+## │ input_layer (InputLayer)    │ (None, 500, 1)        │          0-
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ conv1d (Conv1D)             │ (None, 500, 64)       │        256Y
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ batch_normalization         │ (None, 500, 64)       │        256Y
+## │ (BatchNormalization)        │                       │            │       │
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ re_lu (ReLU)                │ (None, 500, 64)       │          0-
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ conv1d_1 (Conv1D)           │ (None, 500, 64)       │     12,352Y
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ batch_normalization_1       │ (None, 500, 64)       │        256Y
+## │ (BatchNormalization)        │                       │            │       │
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ re_lu_1 (ReLU)              │ (None, 500, 64)       │          0-
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ conv1d_2 (Conv1D)           │ (None, 500, 64)       │     12,352Y
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ batch_normalization_2       │ (None, 500, 64)       │        256Y
+## │ (BatchNormalization)        │                       │            │       │
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ re_lu_2 (ReLU)              │ (None, 500, 64)       │          0-
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ global_average_pooling1d    │ (None, 64)            │          0-
+## │ (GlobalAveragePooling1D)    │                       │            │       │
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ dense (Dense)               │ (None, 2)             │        130Y
+## └─────────────────────────────┴───────────────────────┴────────────┴───────┘
+##  Total params: 25,858 (101.01 KB)
+##  Trainable params: 25,474 (99.51 KB)
+##  Non-trainable params: 384 (1.50 KB)
+
+plot(model, show_shapes = TRUE)
+
+plot of chunk unnamed-chunk-9

+plot of chunk unnamed-chunk-9 +

+
+
+
+

Train the model +

+
+epochs <- 500
+batch_size <- 32
+
+callbacks <- c(
+  callback_model_checkpoint(
+    "best_model.keras", save_best_only = TRUE,
+    monitor = "val_loss"
+  ),
+  callback_reduce_lr_on_plateau(
+    monitor = "val_loss", factor = 0.5,
+    patience = 20, min_lr = 0.0001
+  ),
+  callback_early_stopping(
+    monitor = "val_loss", patience = 50,
+    verbose = 1
+  )
+)
+
+
+model |> compile(
+  optimizer = "adam",
+  loss = "sparse_categorical_crossentropy",
+  metrics = "sparse_categorical_accuracy"
+)
+
+history <- model |> fit(
+  x_train, y_train,
+  batch_size = batch_size,
+  epochs = epochs,
+  callbacks = callbacks,
+  validation_split = 0.2
+)
+
## Epoch 1/500
+## 90/90 - 2s - 24ms/step - loss: 0.5557 - sparse_categorical_accuracy: 0.7073 - val_loss: 0.8517 - val_sparse_categorical_accuracy: 0.4896 - learning_rate: 0.0010
+## Epoch 2/500
+## 90/90 - 1s - 6ms/step - loss: 0.4850 - sparse_categorical_accuracy: 0.7611 - val_loss: 0.9464 - val_sparse_categorical_accuracy: 0.4896 - learning_rate: 0.0010
+## Epoch 3/500
+## 90/90 - 0s - 2ms/step - loss: 0.4713 - sparse_categorical_accuracy: 0.7705 - val_loss: 0.7593 - val_sparse_categorical_accuracy: 0.4896 - learning_rate: 0.0010
+## Epoch 4/500
+## 90/90 - 0s - 2ms/step - loss: 0.4239 - sparse_categorical_accuracy: 0.7885 - val_loss: 0.6721 - val_sparse_categorical_accuracy: 0.5021 - learning_rate: 0.0010
+## Epoch 5/500
+## 90/90 - 0s - 2ms/step - loss: 0.4218 - sparse_categorical_accuracy: 0.7885 - val_loss: 0.5647 - val_sparse_categorical_accuracy: 0.7198 - learning_rate: 0.0010
+## Epoch 6/500
+## 90/90 - 0s - 2ms/step - loss: 0.4060 - sparse_categorical_accuracy: 0.8028 - val_loss: 0.4563 - val_sparse_categorical_accuracy: 0.8141 - learning_rate: 0.0010
+## Epoch 7/500
+## 90/90 - 0s - 2ms/step - loss: 0.4032 - sparse_categorical_accuracy: 0.8017 - val_loss: 0.4209 - val_sparse_categorical_accuracy: 0.7892 - learning_rate: 0.0010
+## Epoch 8/500
+## 90/90 - 0s - 2ms/step - loss: 0.3920 - sparse_categorical_accuracy: 0.8003 - val_loss: 0.3938 - val_sparse_categorical_accuracy: 0.8239 - learning_rate: 0.0010
+## Epoch 9/500
+## 90/90 - 0s - 2ms/step - loss: 0.3892 - sparse_categorical_accuracy: 0.8135 - val_loss: 0.3849 - val_sparse_categorical_accuracy: 0.8225 - learning_rate: 0.0010
+## Epoch 10/500
+## 90/90 - 0s - 2ms/step - loss: 0.3874 - sparse_categorical_accuracy: 0.8021 - val_loss: 0.4934 - val_sparse_categorical_accuracy: 0.7503 - learning_rate: 0.0010
+## Epoch 11/500
+## 90/90 - 0s - 1ms/step - loss: 0.3798 - sparse_categorical_accuracy: 0.8149 - val_loss: 0.5693 - val_sparse_categorical_accuracy: 0.6990 - learning_rate: 0.0010
+## Epoch 12/500
+## 90/90 - 0s - 1ms/step - loss: 0.3671 - sparse_categorical_accuracy: 0.8215 - val_loss: 0.3998 - val_sparse_categorical_accuracy: 0.8086 - learning_rate: 0.0010
+## Epoch 13/500
+## 90/90 - 0s - 1ms/step - loss: 0.3555 - sparse_categorical_accuracy: 0.8389 - val_loss: 0.4145 - val_sparse_categorical_accuracy: 0.8031 - learning_rate: 0.0010
+## Epoch 14/500
+## 90/90 - 0s - 1ms/step - loss: 0.3628 - sparse_categorical_accuracy: 0.8253 - val_loss: 0.3886 - val_sparse_categorical_accuracy: 0.8211 - learning_rate: 0.0010
+## Epoch 15/500
+## 90/90 - 0s - 1ms/step - loss: 0.3433 - sparse_categorical_accuracy: 0.8406 - val_loss: 0.7763 - val_sparse_categorical_accuracy: 0.6019 - learning_rate: 0.0010
+## Epoch 16/500
+## 90/90 - 0s - 2ms/step - loss: 0.3455 - sparse_categorical_accuracy: 0.8434 - val_loss: 0.3537 - val_sparse_categorical_accuracy: 0.8419 - learning_rate: 0.0010
+## Epoch 17/500
+## 90/90 - 0s - 1ms/step - loss: 0.3336 - sparse_categorical_accuracy: 0.8462 - val_loss: 0.5375 - val_sparse_categorical_accuracy: 0.7074 - learning_rate: 0.0010
+## Epoch 18/500
+## 90/90 - 0s - 1ms/step - loss: 0.3263 - sparse_categorical_accuracy: 0.8503 - val_loss: 0.4134 - val_sparse_categorical_accuracy: 0.7892 - learning_rate: 0.0010
+## Epoch 19/500
+## 90/90 - 0s - 2ms/step - loss: 0.3195 - sparse_categorical_accuracy: 0.8601 - val_loss: 0.3432 - val_sparse_categorical_accuracy: 0.8433 - learning_rate: 0.0010
+## Epoch 20/500
+## 90/90 - 0s - 1ms/step - loss: 0.3136 - sparse_categorical_accuracy: 0.8674 - val_loss: 0.9287 - val_sparse_categorical_accuracy: 0.5950 - learning_rate: 0.0010
+## Epoch 21/500
+## 90/90 - 0s - 1ms/step - loss: 0.3001 - sparse_categorical_accuracy: 0.8691 - val_loss: 0.3926 - val_sparse_categorical_accuracy: 0.7795 - learning_rate: 0.0010
+## Epoch 22/500
+## 90/90 - 0s - 2ms/step - loss: 0.2984 - sparse_categorical_accuracy: 0.8719 - val_loss: 0.3023 - val_sparse_categorical_accuracy: 0.8655 - learning_rate: 0.0010
+## Epoch 23/500
+## 90/90 - 0s - 1ms/step - loss: 0.2930 - sparse_categorical_accuracy: 0.8767 - val_loss: 0.3501 - val_sparse_categorical_accuracy: 0.8086 - learning_rate: 0.0010
+## Epoch 24/500
+## 90/90 - 0s - 1ms/step - loss: 0.2861 - sparse_categorical_accuracy: 0.8799 - val_loss: 0.9187 - val_sparse_categorical_accuracy: 0.5992 - learning_rate: 0.0010
+## Epoch 25/500
+## 90/90 - 0s - 1ms/step - loss: 0.2843 - sparse_categorical_accuracy: 0.8816 - val_loss: 0.3731 - val_sparse_categorical_accuracy: 0.8363 - learning_rate: 0.0010
+## Epoch 26/500
+## 90/90 - 0s - 1ms/step - loss: 0.3182 - sparse_categorical_accuracy: 0.8569 - val_loss: 0.5553 - val_sparse_categorical_accuracy: 0.7115 - learning_rate: 0.0010
+## Epoch 27/500
+## 90/90 - 0s - 1ms/step - loss: 0.2732 - sparse_categorical_accuracy: 0.8875 - val_loss: 0.3950 - val_sparse_categorical_accuracy: 0.7628 - learning_rate: 0.0010
+## Epoch 28/500
+## 90/90 - 0s - 1ms/step - loss: 0.2943 - sparse_categorical_accuracy: 0.8705 - val_loss: 0.8138 - val_sparse_categorical_accuracy: 0.7254 - learning_rate: 0.0010
+## Epoch 29/500
+## 90/90 - 0s - 1ms/step - loss: 0.2772 - sparse_categorical_accuracy: 0.8781 - val_loss: 0.3712 - val_sparse_categorical_accuracy: 0.8405 - learning_rate: 0.0010
+## Epoch 30/500
+## 90/90 - 0s - 1ms/step - loss: 0.2714 - sparse_categorical_accuracy: 0.8833 - val_loss: 0.4209 - val_sparse_categorical_accuracy: 0.7975 - learning_rate: 0.0010
+## Epoch 31/500
+## 90/90 - 0s - 2ms/step - loss: 0.2720 - sparse_categorical_accuracy: 0.8854 - val_loss: 1.9298 - val_sparse_categorical_accuracy: 0.5104 - learning_rate: 0.0010
+## Epoch 32/500
+## 90/90 - 0s - 2ms/step - loss: 0.2699 - sparse_categorical_accuracy: 0.8833 - val_loss: 0.3155 - val_sparse_categorical_accuracy: 0.8516 - learning_rate: 0.0010
+## Epoch 33/500
+## 90/90 - 0s - 2ms/step - loss: 0.2586 - sparse_categorical_accuracy: 0.8951 - val_loss: 0.3628 - val_sparse_categorical_accuracy: 0.7947 - learning_rate: 0.0010
+## Epoch 34/500
+## 90/90 - 0s - 1ms/step - loss: 0.2490 - sparse_categorical_accuracy: 0.9003 - val_loss: 0.5844 - val_sparse_categorical_accuracy: 0.7309 - learning_rate: 0.0010
+## Epoch 35/500
+## 90/90 - 0s - 1ms/step - loss: 0.2627 - sparse_categorical_accuracy: 0.8938 - val_loss: 0.3480 - val_sparse_categorical_accuracy: 0.8419 - learning_rate: 0.0010
+## Epoch 36/500
+## 90/90 - 0s - 1ms/step - loss: 0.2558 - sparse_categorical_accuracy: 0.8948 - val_loss: 0.3793 - val_sparse_categorical_accuracy: 0.8128 - learning_rate: 0.0010
+## Epoch 37/500
+## 90/90 - 0s - 1ms/step - loss: 0.2515 - sparse_categorical_accuracy: 0.8958 - val_loss: 0.4828 - val_sparse_categorical_accuracy: 0.7503 - learning_rate: 0.0010
+## Epoch 38/500
+## 90/90 - 0s - 2ms/step - loss: 0.2541 - sparse_categorical_accuracy: 0.8986 - val_loss: 0.2950 - val_sparse_categorical_accuracy: 0.8669 - learning_rate: 0.0010
+## Epoch 39/500
+## 90/90 - 0s - 1ms/step - loss: 0.2505 - sparse_categorical_accuracy: 0.8976 - val_loss: 0.7151 - val_sparse_categorical_accuracy: 0.7087 - learning_rate: 0.0010
+## Epoch 40/500
+## 90/90 - 0s - 1ms/step - loss: 0.2490 - sparse_categorical_accuracy: 0.8979 - val_loss: 0.3170 - val_sparse_categorical_accuracy: 0.8627 - learning_rate: 0.0010
+## Epoch 41/500
+## 90/90 - 0s - 2ms/step - loss: 0.2325 - sparse_categorical_accuracy: 0.9062 - val_loss: 0.2560 - val_sparse_categorical_accuracy: 0.8904 - learning_rate: 0.0010
+## Epoch 42/500
+## 90/90 - 0s - 1ms/step - loss: 0.2312 - sparse_categorical_accuracy: 0.9076 - val_loss: 0.3396 - val_sparse_categorical_accuracy: 0.8405 - learning_rate: 0.0010
+## Epoch 43/500
+## 90/90 - 0s - 1ms/step - loss: 0.2402 - sparse_categorical_accuracy: 0.8976 - val_loss: 0.3650 - val_sparse_categorical_accuracy: 0.8294 - learning_rate: 0.0010
+## Epoch 44/500
+## 90/90 - 0s - 1ms/step - loss: 0.2342 - sparse_categorical_accuracy: 0.9038 - val_loss: 1.0563 - val_sparse_categorical_accuracy: 0.6172 - learning_rate: 0.0010
+## Epoch 45/500
+## 90/90 - 0s - 1ms/step - loss: 0.2320 - sparse_categorical_accuracy: 0.9080 - val_loss: 0.2506 - val_sparse_categorical_accuracy: 0.8932 - learning_rate: 0.0010
+## Epoch 46/500
+## 90/90 - 0s - 1ms/step - loss: 0.2317 - sparse_categorical_accuracy: 0.9035 - val_loss: 0.4473 - val_sparse_categorical_accuracy: 0.7920 - learning_rate: 0.0010
+## Epoch 47/500
+## 90/90 - 0s - 1ms/step - loss: 0.2164 - sparse_categorical_accuracy: 0.9125 - val_loss: 0.6324 - val_sparse_categorical_accuracy: 0.7240 - learning_rate: 0.0010
+## Epoch 48/500
+## 90/90 - 0s - 2ms/step - loss: 0.2186 - sparse_categorical_accuracy: 0.9122 - val_loss: 0.2525 - val_sparse_categorical_accuracy: 0.9001 - learning_rate: 0.0010
+## Epoch 49/500
+## 90/90 - 0s - 2ms/step - loss: 0.2119 - sparse_categorical_accuracy: 0.9118 - val_loss: 0.2401 - val_sparse_categorical_accuracy: 0.9015 - learning_rate: 0.0010
+## Epoch 50/500
+## 90/90 - 0s - 1ms/step - loss: 0.2134 - sparse_categorical_accuracy: 0.9191 - val_loss: 0.2410 - val_sparse_categorical_accuracy: 0.8988 - learning_rate: 0.0010
+## Epoch 51/500
+## 90/90 - 0s - 2ms/step - loss: 0.2120 - sparse_categorical_accuracy: 0.9174 - val_loss: 0.6803 - val_sparse_categorical_accuracy: 0.6935 - learning_rate: 0.0010
+## Epoch 52/500
+## 90/90 - 0s - 2ms/step - loss: 0.2023 - sparse_categorical_accuracy: 0.9205 - val_loss: 0.2938 - val_sparse_categorical_accuracy: 0.8779 - learning_rate: 0.0010
+## Epoch 53/500
+## 90/90 - 0s - 2ms/step - loss: 0.1980 - sparse_categorical_accuracy: 0.9243 - val_loss: 0.5380 - val_sparse_categorical_accuracy: 0.7531 - learning_rate: 0.0010
+## Epoch 54/500
+## 90/90 - 0s - 2ms/step - loss: 0.1883 - sparse_categorical_accuracy: 0.9271 - val_loss: 0.2272 - val_sparse_categorical_accuracy: 0.9071 - learning_rate: 0.0010
+## Epoch 55/500
+## 90/90 - 0s - 2ms/step - loss: 0.1761 - sparse_categorical_accuracy: 0.9385 - val_loss: 0.2707 - val_sparse_categorical_accuracy: 0.8821 - learning_rate: 0.0010
+## Epoch 56/500
+## 90/90 - 0s - 1ms/step - loss: 0.1683 - sparse_categorical_accuracy: 0.9417 - val_loss: 0.2584 - val_sparse_categorical_accuracy: 0.9015 - learning_rate: 0.0010
+## Epoch 57/500
+## 90/90 - 0s - 1ms/step - loss: 0.1581 - sparse_categorical_accuracy: 0.9490 - val_loss: 1.4305 - val_sparse_categorical_accuracy: 0.7184 - learning_rate: 0.0010
+## Epoch 58/500
+## 90/90 - 0s - 1ms/step - loss: 0.1518 - sparse_categorical_accuracy: 0.9497 - val_loss: 0.7991 - val_sparse_categorical_accuracy: 0.7531 - learning_rate: 0.0010
+## Epoch 59/500
+## 90/90 - 0s - 1ms/step - loss: 0.1529 - sparse_categorical_accuracy: 0.9479 - val_loss: 0.3389 - val_sparse_categorical_accuracy: 0.8724 - learning_rate: 0.0010
+## Epoch 60/500
+## 90/90 - 0s - 1ms/step - loss: 0.1784 - sparse_categorical_accuracy: 0.9368 - val_loss: 0.2534 - val_sparse_categorical_accuracy: 0.8974 - learning_rate: 0.0010
+## Epoch 61/500
+## 90/90 - 0s - 1ms/step - loss: 0.1335 - sparse_categorical_accuracy: 0.9566 - val_loss: 0.2323 - val_sparse_categorical_accuracy: 0.9098 - learning_rate: 0.0010
+## Epoch 62/500
+## 90/90 - 0s - 1ms/step - loss: 0.1401 - sparse_categorical_accuracy: 0.9531 - val_loss: 0.3259 - val_sparse_categorical_accuracy: 0.8696 - learning_rate: 0.0010
+## Epoch 63/500
+## 90/90 - 0s - 1ms/step - loss: 0.1404 - sparse_categorical_accuracy: 0.9521 - val_loss: 0.2970 - val_sparse_categorical_accuracy: 0.8627 - learning_rate: 0.0010
+## Epoch 64/500
+## 90/90 - 0s - 1ms/step - loss: 0.1375 - sparse_categorical_accuracy: 0.9559 - val_loss: 0.3416 - val_sparse_categorical_accuracy: 0.8183 - learning_rate: 0.0010
+## Epoch 65/500
+## 90/90 - 0s - 1ms/step - loss: 0.1303 - sparse_categorical_accuracy: 0.9583 - val_loss: 0.2586 - val_sparse_categorical_accuracy: 0.8766 - learning_rate: 0.0010
+## Epoch 66/500
+## 90/90 - 0s - 1ms/step - loss: 0.1199 - sparse_categorical_accuracy: 0.9608 - val_loss: 1.2218 - val_sparse_categorical_accuracy: 0.7337 - learning_rate: 0.0010
+## Epoch 67/500
+## 90/90 - 0s - 2ms/step - loss: 0.1186 - sparse_categorical_accuracy: 0.9615 - val_loss: 0.1464 - val_sparse_categorical_accuracy: 0.9431 - learning_rate: 0.0010
+## Epoch 68/500
+## 90/90 - 0s - 2ms/step - loss: 0.1176 - sparse_categorical_accuracy: 0.9632 - val_loss: 0.1451 - val_sparse_categorical_accuracy: 0.9431 - learning_rate: 0.0010
+## Epoch 69/500
+## 90/90 - 0s - 2ms/step - loss: 0.1295 - sparse_categorical_accuracy: 0.9583 - val_loss: 1.7723 - val_sparse_categorical_accuracy: 0.5395 - learning_rate: 0.0010
+## Epoch 70/500
+## 90/90 - 0s - 2ms/step - loss: 0.1232 - sparse_categorical_accuracy: 0.9587 - val_loss: 0.1802 - val_sparse_categorical_accuracy: 0.9237 - learning_rate: 0.0010
+## Epoch 71/500
+## 90/90 - 0s - 1ms/step - loss: 0.1202 - sparse_categorical_accuracy: 0.9573 - val_loss: 0.1932 - val_sparse_categorical_accuracy: 0.9085 - learning_rate: 0.0010
+## Epoch 72/500
+## 90/90 - 0s - 1ms/step - loss: 0.1124 - sparse_categorical_accuracy: 0.9660 - val_loss: 0.4178 - val_sparse_categorical_accuracy: 0.8003 - learning_rate: 0.0010
+## Epoch 73/500
+## 90/90 - 0s - 1ms/step - loss: 0.1068 - sparse_categorical_accuracy: 0.9663 - val_loss: 0.1517 - val_sparse_categorical_accuracy: 0.9473 - learning_rate: 0.0010
+## Epoch 74/500
+## 90/90 - 0s - 1ms/step - loss: 0.1085 - sparse_categorical_accuracy: 0.9639 - val_loss: 0.2126 - val_sparse_categorical_accuracy: 0.9223 - learning_rate: 0.0010
+## Epoch 75/500
+## 90/90 - 0s - 1ms/step - loss: 0.1263 - sparse_categorical_accuracy: 0.9580 - val_loss: 0.1689 - val_sparse_categorical_accuracy: 0.9390 - learning_rate: 0.0010
+## Epoch 76/500
+## 90/90 - 0s - 1ms/step - loss: 0.1169 - sparse_categorical_accuracy: 0.9597 - val_loss: 0.2955 - val_sparse_categorical_accuracy: 0.8460 - learning_rate: 0.0010
+## Epoch 77/500
+## 90/90 - 0s - 1ms/step - loss: 0.1089 - sparse_categorical_accuracy: 0.9622 - val_loss: 0.6333 - val_sparse_categorical_accuracy: 0.6241 - learning_rate: 0.0010
+## Epoch 78/500
+## 90/90 - 0s - 2ms/step - loss: 0.1017 - sparse_categorical_accuracy: 0.9688 - val_loss: 0.1416 - val_sparse_categorical_accuracy: 0.9459 - learning_rate: 0.0010
+## Epoch 79/500
+## 90/90 - 0s - 2ms/step - loss: 0.1099 - sparse_categorical_accuracy: 0.9642 - val_loss: 0.3434 - val_sparse_categorical_accuracy: 0.8544 - learning_rate: 0.0010
+## Epoch 80/500
+## 90/90 - 0s - 2ms/step - loss: 0.1046 - sparse_categorical_accuracy: 0.9653 - val_loss: 0.4430 - val_sparse_categorical_accuracy: 0.8280 - learning_rate: 0.0010
+## Epoch 81/500
+## 90/90 - 0s - 1ms/step - loss: 0.1098 - sparse_categorical_accuracy: 0.9622 - val_loss: 0.1632 - val_sparse_categorical_accuracy: 0.9362 - learning_rate: 0.0010
+## Epoch 82/500
+## 90/90 - 0s - 1ms/step - loss: 0.1021 - sparse_categorical_accuracy: 0.9670 - val_loss: 0.3984 - val_sparse_categorical_accuracy: 0.7906 - learning_rate: 0.0010
+## Epoch 83/500
+## 90/90 - 0s - 1ms/step - loss: 0.1086 - sparse_categorical_accuracy: 0.9642 - val_loss: 0.1976 - val_sparse_categorical_accuracy: 0.9085 - learning_rate: 0.0010
+## Epoch 84/500
+## 90/90 - 0s - 1ms/step - loss: 0.1026 - sparse_categorical_accuracy: 0.9642 - val_loss: 0.2123 - val_sparse_categorical_accuracy: 0.9251 - learning_rate: 0.0010
+## Epoch 85/500
+## 90/90 - 0s - 1ms/step - loss: 0.1032 - sparse_categorical_accuracy: 0.9632 - val_loss: 0.3491 - val_sparse_categorical_accuracy: 0.8613 - learning_rate: 0.0010
+## Epoch 86/500
+## 90/90 - 0s - 1ms/step - loss: 0.1136 - sparse_categorical_accuracy: 0.9576 - val_loss: 0.1498 - val_sparse_categorical_accuracy: 0.9362 - learning_rate: 0.0010
+## Epoch 87/500
+## 90/90 - 0s - 2ms/step - loss: 0.1107 - sparse_categorical_accuracy: 0.9608 - val_loss: 0.1406 - val_sparse_categorical_accuracy: 0.9501 - learning_rate: 0.0010
+## Epoch 88/500
+## 90/90 - 0s - 1ms/step - loss: 0.0995 - sparse_categorical_accuracy: 0.9677 - val_loss: 0.6864 - val_sparse_categorical_accuracy: 0.7656 - learning_rate: 0.0010
+## Epoch 89/500
+## 90/90 - 0s - 1ms/step - loss: 0.1013 - sparse_categorical_accuracy: 0.9656 - val_loss: 0.8218 - val_sparse_categorical_accuracy: 0.7476 - learning_rate: 0.0010
+## Epoch 90/500
+## 90/90 - 0s - 1ms/step - loss: 0.1002 - sparse_categorical_accuracy: 0.9670 - val_loss: 0.2009 - val_sparse_categorical_accuracy: 0.9209 - learning_rate: 0.0010
+## Epoch 91/500
+## 90/90 - 0s - 1ms/step - loss: 0.1010 - sparse_categorical_accuracy: 0.9674 - val_loss: 0.2869 - val_sparse_categorical_accuracy: 0.8849 - learning_rate: 0.0010
+## Epoch 92/500
+## 90/90 - 0s - 1ms/step - loss: 0.1012 - sparse_categorical_accuracy: 0.9688 - val_loss: 0.5128 - val_sparse_categorical_accuracy: 0.7434 - learning_rate: 0.0010
+## Epoch 93/500
+## 90/90 - 0s - 1ms/step - loss: 0.1009 - sparse_categorical_accuracy: 0.9660 - val_loss: 0.5212 - val_sparse_categorical_accuracy: 0.7753 - learning_rate: 0.0010
+## Epoch 94/500
+## 90/90 - 0s - 1ms/step - loss: 0.1016 - sparse_categorical_accuracy: 0.9663 - val_loss: 0.3146 - val_sparse_categorical_accuracy: 0.8682 - learning_rate: 0.0010
+## Epoch 95/500
+## 90/90 - 0s - 1ms/step - loss: 0.0975 - sparse_categorical_accuracy: 0.9663 - val_loss: 0.1750 - val_sparse_categorical_accuracy: 0.9376 - learning_rate: 0.0010
+## Epoch 96/500
+## 90/90 - 0s - 1ms/step - loss: 0.1121 - sparse_categorical_accuracy: 0.9583 - val_loss: 0.2399 - val_sparse_categorical_accuracy: 0.9112 - learning_rate: 0.0010
+## Epoch 97/500
+## 90/90 - 0s - 1ms/step - loss: 0.1000 - sparse_categorical_accuracy: 0.9670 - val_loss: 0.5611 - val_sparse_categorical_accuracy: 0.7101 - learning_rate: 0.0010
+## Epoch 98/500
+## 90/90 - 0s - 1ms/step - loss: 0.0949 - sparse_categorical_accuracy: 0.9663 - val_loss: 0.1864 - val_sparse_categorical_accuracy: 0.9279 - learning_rate: 0.0010
+## Epoch 99/500
+## 90/90 - 0s - 2ms/step - loss: 0.0925 - sparse_categorical_accuracy: 0.9667 - val_loss: 0.1228 - val_sparse_categorical_accuracy: 0.9431 - learning_rate: 0.0010
+## Epoch 100/500
+## 90/90 - 0s - 1ms/step - loss: 0.0922 - sparse_categorical_accuracy: 0.9691 - val_loss: 1.9448 - val_sparse_categorical_accuracy: 0.7115 - learning_rate: 0.0010
+## Epoch 101/500
+## 90/90 - 0s - 1ms/step - loss: 0.0887 - sparse_categorical_accuracy: 0.9733 - val_loss: 0.2127 - val_sparse_categorical_accuracy: 0.9071 - learning_rate: 0.0010
+## Epoch 102/500
+## 90/90 - 0s - 1ms/step - loss: 0.0999 - sparse_categorical_accuracy: 0.9660 - val_loss: 0.1903 - val_sparse_categorical_accuracy: 0.9154 - learning_rate: 0.0010
+## Epoch 103/500
+## 90/90 - 0s - 1ms/step - loss: 0.0929 - sparse_categorical_accuracy: 0.9698 - val_loss: 0.2647 - val_sparse_categorical_accuracy: 0.8988 - learning_rate: 0.0010
+## Epoch 104/500
+## 90/90 - 0s - 1ms/step - loss: 0.1008 - sparse_categorical_accuracy: 0.9642 - val_loss: 1.1019 - val_sparse_categorical_accuracy: 0.6019 - learning_rate: 0.0010
+## Epoch 105/500
+## 90/90 - 0s - 1ms/step - loss: 0.0969 - sparse_categorical_accuracy: 0.9681 - val_loss: 1.1464 - val_sparse_categorical_accuracy: 0.5645 - learning_rate: 0.0010
+## Epoch 106/500
+## 90/90 - 0s - 2ms/step - loss: 0.0924 - sparse_categorical_accuracy: 0.9670 - val_loss: 0.2535 - val_sparse_categorical_accuracy: 0.9085 - learning_rate: 0.0010
+## Epoch 107/500
+## 90/90 - 0s - 2ms/step - loss: 0.0930 - sparse_categorical_accuracy: 0.9677 - val_loss: 1.5288 - val_sparse_categorical_accuracy: 0.5118 - learning_rate: 0.0010
+## Epoch 108/500
+## 90/90 - 0s - 1ms/step - loss: 0.0893 - sparse_categorical_accuracy: 0.9701 - val_loss: 0.1496 - val_sparse_categorical_accuracy: 0.9404 - learning_rate: 0.0010
+## Epoch 109/500
+## 90/90 - 0s - 1ms/step - loss: 0.0891 - sparse_categorical_accuracy: 0.9712 - val_loss: 0.2799 - val_sparse_categorical_accuracy: 0.8835 - learning_rate: 0.0010
+## Epoch 110/500
+## 90/90 - 0s - 1ms/step - loss: 0.0963 - sparse_categorical_accuracy: 0.9677 - val_loss: 0.4827 - val_sparse_categorical_accuracy: 0.8155 - learning_rate: 0.0010
+## Epoch 111/500
+## 90/90 - 0s - 2ms/step - loss: 0.0841 - sparse_categorical_accuracy: 0.9701 - val_loss: 0.1203 - val_sparse_categorical_accuracy: 0.9501 - learning_rate: 0.0010
+## Epoch 112/500
+## 90/90 - 0s - 1ms/step - loss: 0.0861 - sparse_categorical_accuracy: 0.9719 - val_loss: 0.5081 - val_sparse_categorical_accuracy: 0.7559 - learning_rate: 0.0010
+## Epoch 113/500
+## 90/90 - 0s - 1ms/step - loss: 0.0831 - sparse_categorical_accuracy: 0.9729 - val_loss: 1.2423 - val_sparse_categorical_accuracy: 0.5687 - learning_rate: 0.0010
+## Epoch 114/500
+## 90/90 - 0s - 1ms/step - loss: 0.1094 - sparse_categorical_accuracy: 0.9653 - val_loss: 0.3459 - val_sparse_categorical_accuracy: 0.8252 - learning_rate: 0.0010
+## Epoch 115/500
+## 90/90 - 0s - 1ms/step - loss: 0.0886 - sparse_categorical_accuracy: 0.9691 - val_loss: 0.7117 - val_sparse_categorical_accuracy: 0.7226 - learning_rate: 0.0010
+## Epoch 116/500
+## 90/90 - 0s - 1ms/step - loss: 0.0989 - sparse_categorical_accuracy: 0.9698 - val_loss: 0.1669 - val_sparse_categorical_accuracy: 0.9334 - learning_rate: 0.0010
+## Epoch 117/500
+## 90/90 - 0s - 1ms/step - loss: 0.0891 - sparse_categorical_accuracy: 0.9705 - val_loss: 0.2413 - val_sparse_categorical_accuracy: 0.9154 - learning_rate: 0.0010
+## Epoch 118/500
+## 90/90 - 0s - 1ms/step - loss: 0.0902 - sparse_categorical_accuracy: 0.9691 - val_loss: 0.7796 - val_sparse_categorical_accuracy: 0.6741 - learning_rate: 0.0010
+## Epoch 119/500
+## 90/90 - 0s - 1ms/step - loss: 0.0877 - sparse_categorical_accuracy: 0.9691 - val_loss: 0.1806 - val_sparse_categorical_accuracy: 0.9168 - learning_rate: 0.0010
+## Epoch 120/500
+## 90/90 - 0s - 2ms/step - loss: 0.0880 - sparse_categorical_accuracy: 0.9688 - val_loss: 0.1146 - val_sparse_categorical_accuracy: 0.9598 - learning_rate: 0.0010
+## Epoch 121/500
+## 90/90 - 0s - 1ms/step - loss: 0.0896 - sparse_categorical_accuracy: 0.9726 - val_loss: 0.1279 - val_sparse_categorical_accuracy: 0.9528 - learning_rate: 0.0010
+## Epoch 122/500
+## 90/90 - 0s - 1ms/step - loss: 0.0883 - sparse_categorical_accuracy: 0.9701 - val_loss: 0.3067 - val_sparse_categorical_accuracy: 0.8918 - learning_rate: 0.0010
+## Epoch 123/500
+## 90/90 - 0s - 2ms/step - loss: 0.0855 - sparse_categorical_accuracy: 0.9722 - val_loss: 0.1060 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 0.0010
+## Epoch 124/500
+## 90/90 - 0s - 1ms/step - loss: 0.0917 - sparse_categorical_accuracy: 0.9663 - val_loss: 0.2169 - val_sparse_categorical_accuracy: 0.9112 - learning_rate: 0.0010
+## Epoch 125/500
+## 90/90 - 0s - 1ms/step - loss: 0.0976 - sparse_categorical_accuracy: 0.9642 - val_loss: 0.1407 - val_sparse_categorical_accuracy: 0.9459 - learning_rate: 0.0010
+## Epoch 126/500
+## 90/90 - 0s - 1ms/step - loss: 0.0866 - sparse_categorical_accuracy: 0.9722 - val_loss: 1.4019 - val_sparse_categorical_accuracy: 0.7295 - learning_rate: 0.0010
+## Epoch 127/500
+## 90/90 - 0s - 1ms/step - loss: 0.0877 - sparse_categorical_accuracy: 0.9747 - val_loss: 0.2518 - val_sparse_categorical_accuracy: 0.8918 - learning_rate: 0.0010
+## Epoch 128/500
+## 90/90 - 0s - 1ms/step - loss: 0.0942 - sparse_categorical_accuracy: 0.9677 - val_loss: 0.1465 - val_sparse_categorical_accuracy: 0.9445 - learning_rate: 0.0010
+## Epoch 129/500
+## 90/90 - 0s - 1ms/step - loss: 0.0904 - sparse_categorical_accuracy: 0.9712 - val_loss: 0.1546 - val_sparse_categorical_accuracy: 0.9445 - learning_rate: 0.0010
+## Epoch 130/500
+## 90/90 - 0s - 1ms/step - loss: 0.0953 - sparse_categorical_accuracy: 0.9705 - val_loss: 0.1201 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 0.0010
+## Epoch 131/500
+## 90/90 - 0s - 1ms/step - loss: 0.0846 - sparse_categorical_accuracy: 0.9712 - val_loss: 0.1980 - val_sparse_categorical_accuracy: 0.9293 - learning_rate: 0.0010
+## Epoch 132/500
+## 90/90 - 0s - 1ms/step - loss: 0.0812 - sparse_categorical_accuracy: 0.9760 - val_loss: 0.4080 - val_sparse_categorical_accuracy: 0.8599 - learning_rate: 0.0010
+## Epoch 133/500
+## 90/90 - 0s - 1ms/step - loss: 0.0785 - sparse_categorical_accuracy: 0.9733 - val_loss: 0.3284 - val_sparse_categorical_accuracy: 0.8835 - learning_rate: 0.0010
+## Epoch 134/500
+## 90/90 - 0s - 1ms/step - loss: 0.0885 - sparse_categorical_accuracy: 0.9719 - val_loss: 0.5050 - val_sparse_categorical_accuracy: 0.8419 - learning_rate: 0.0010
+## Epoch 135/500
+## 90/90 - 0s - 1ms/step - loss: 0.0859 - sparse_categorical_accuracy: 0.9726 - val_loss: 0.4296 - val_sparse_categorical_accuracy: 0.8141 - learning_rate: 0.0010
+## Epoch 136/500
+## 90/90 - 0s - 1ms/step - loss: 0.0885 - sparse_categorical_accuracy: 0.9670 - val_loss: 0.1295 - val_sparse_categorical_accuracy: 0.9515 - learning_rate: 0.0010
+## Epoch 137/500
+## 90/90 - 0s - 1ms/step - loss: 0.0892 - sparse_categorical_accuracy: 0.9712 - val_loss: 0.4052 - val_sparse_categorical_accuracy: 0.8031 - learning_rate: 0.0010
+## Epoch 138/500
+## 90/90 - 0s - 1ms/step - loss: 0.0835 - sparse_categorical_accuracy: 0.9719 - val_loss: 0.1160 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 0.0010
+## Epoch 139/500
+## 90/90 - 0s - 1ms/step - loss: 0.0877 - sparse_categorical_accuracy: 0.9708 - val_loss: 3.0631 - val_sparse_categorical_accuracy: 0.7032 - learning_rate: 0.0010
+## Epoch 140/500
+## 90/90 - 0s - 1ms/step - loss: 0.0867 - sparse_categorical_accuracy: 0.9719 - val_loss: 1.3638 - val_sparse_categorical_accuracy: 0.7143 - learning_rate: 0.0010
+## Epoch 141/500
+## 90/90 - 0s - 1ms/step - loss: 0.0857 - sparse_categorical_accuracy: 0.9715 - val_loss: 0.2599 - val_sparse_categorical_accuracy: 0.9015 - learning_rate: 0.0010
+## Epoch 142/500
+## 90/90 - 0s - 1ms/step - loss: 0.0771 - sparse_categorical_accuracy: 0.9764 - val_loss: 0.2860 - val_sparse_categorical_accuracy: 0.8988 - learning_rate: 0.0010
+## Epoch 143/500
+## 90/90 - 0s - 1ms/step - loss: 0.0835 - sparse_categorical_accuracy: 0.9694 - val_loss: 0.1252 - val_sparse_categorical_accuracy: 0.9639 - learning_rate: 0.0010
+## Epoch 144/500
+## 90/90 - 1s - 7ms/step - loss: 0.0674 - sparse_categorical_accuracy: 0.9809 - val_loss: 0.1234 - val_sparse_categorical_accuracy: 0.9528 - learning_rate: 5.0000e-04
+## Epoch 145/500
+## 90/90 - 0s - 1ms/step - loss: 0.0680 - sparse_categorical_accuracy: 0.9771 - val_loss: 0.1147 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 5.0000e-04
+## Epoch 146/500
+## 90/90 - 0s - 2ms/step - loss: 0.0702 - sparse_categorical_accuracy: 0.9781 - val_loss: 0.1042 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 5.0000e-04
+## Epoch 147/500
+## 90/90 - 0s - 1ms/step - loss: 0.0742 - sparse_categorical_accuracy: 0.9757 - val_loss: 0.1511 - val_sparse_categorical_accuracy: 0.9501 - learning_rate: 5.0000e-04
+## Epoch 148/500
+## 90/90 - 0s - 1ms/step - loss: 0.0801 - sparse_categorical_accuracy: 0.9708 - val_loss: 0.1219 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 5.0000e-04
+## Epoch 149/500
+## 90/90 - 0s - 2ms/step - loss: 0.0725 - sparse_categorical_accuracy: 0.9767 - val_loss: 0.1028 - val_sparse_categorical_accuracy: 0.9639 - learning_rate: 5.0000e-04
+## Epoch 150/500
+## 90/90 - 0s - 1ms/step - loss: 0.0738 - sparse_categorical_accuracy: 0.9733 - val_loss: 0.2021 - val_sparse_categorical_accuracy: 0.9334 - learning_rate: 5.0000e-04
+## Epoch 151/500
+## 90/90 - 0s - 1ms/step - loss: 0.0707 - sparse_categorical_accuracy: 0.9750 - val_loss: 0.1358 - val_sparse_categorical_accuracy: 0.9376 - learning_rate: 5.0000e-04
+## Epoch 152/500
+## 90/90 - 0s - 1ms/step - loss: 0.0727 - sparse_categorical_accuracy: 0.9740 - val_loss: 0.1989 - val_sparse_categorical_accuracy: 0.9098 - learning_rate: 5.0000e-04
+## Epoch 153/500
+## 90/90 - 0s - 1ms/step - loss: 0.0671 - sparse_categorical_accuracy: 0.9778 - val_loss: 0.1250 - val_sparse_categorical_accuracy: 0.9639 - learning_rate: 5.0000e-04
+## Epoch 154/500
+## 90/90 - 0s - 1ms/step - loss: 0.0687 - sparse_categorical_accuracy: 0.9781 - val_loss: 0.1973 - val_sparse_categorical_accuracy: 0.9237 - learning_rate: 5.0000e-04
+## Epoch 155/500
+## 90/90 - 0s - 1ms/step - loss: 0.0697 - sparse_categorical_accuracy: 0.9774 - val_loss: 0.1190 - val_sparse_categorical_accuracy: 0.9515 - learning_rate: 5.0000e-04
+## Epoch 156/500
+## 90/90 - 0s - 1ms/step - loss: 0.0731 - sparse_categorical_accuracy: 0.9750 - val_loss: 0.1409 - val_sparse_categorical_accuracy: 0.9556 - learning_rate: 5.0000e-04
+## Epoch 157/500
+## 90/90 - 0s - 1ms/step - loss: 0.0719 - sparse_categorical_accuracy: 0.9740 - val_loss: 0.4024 - val_sparse_categorical_accuracy: 0.8655 - learning_rate: 5.0000e-04
+## Epoch 158/500
+## 90/90 - 0s - 1ms/step - loss: 0.0722 - sparse_categorical_accuracy: 0.9750 - val_loss: 0.3254 - val_sparse_categorical_accuracy: 0.8821 - learning_rate: 5.0000e-04
+## Epoch 159/500
+## 90/90 - 0s - 1ms/step - loss: 0.0706 - sparse_categorical_accuracy: 0.9760 - val_loss: 0.2014 - val_sparse_categorical_accuracy: 0.9265 - learning_rate: 5.0000e-04
+## Epoch 160/500
+## 90/90 - 0s - 1ms/step - loss: 0.0707 - sparse_categorical_accuracy: 0.9753 - val_loss: 0.1128 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 5.0000e-04
+## Epoch 161/500
+## 90/90 - 0s - 1ms/step - loss: 0.0727 - sparse_categorical_accuracy: 0.9767 - val_loss: 0.1118 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 5.0000e-04
+## Epoch 162/500
+## 90/90 - 0s - 1ms/step - loss: 0.0660 - sparse_categorical_accuracy: 0.9767 - val_loss: 0.2972 - val_sparse_categorical_accuracy: 0.8960 - learning_rate: 5.0000e-04
+## Epoch 163/500
+## 90/90 - 0s - 1ms/step - loss: 0.0619 - sparse_categorical_accuracy: 0.9795 - val_loss: 0.1079 - val_sparse_categorical_accuracy: 0.9598 - learning_rate: 5.0000e-04
+## Epoch 164/500
+## 90/90 - 0s - 1ms/step - loss: 0.0670 - sparse_categorical_accuracy: 0.9774 - val_loss: 0.2169 - val_sparse_categorical_accuracy: 0.9168 - learning_rate: 5.0000e-04
+## Epoch 165/500
+## 90/90 - 0s - 1ms/step - loss: 0.0690 - sparse_categorical_accuracy: 0.9757 - val_loss: 0.2980 - val_sparse_categorical_accuracy: 0.9015 - learning_rate: 5.0000e-04
+## Epoch 166/500
+## 90/90 - 0s - 1ms/step - loss: 0.0650 - sparse_categorical_accuracy: 0.9785 - val_loss: 0.1257 - val_sparse_categorical_accuracy: 0.9473 - learning_rate: 5.0000e-04
+## Epoch 167/500
+## 90/90 - 0s - 1ms/step - loss: 0.0656 - sparse_categorical_accuracy: 0.9788 - val_loss: 0.1451 - val_sparse_categorical_accuracy: 0.9445 - learning_rate: 5.0000e-04
+## Epoch 168/500
+## 90/90 - 0s - 1ms/step - loss: 0.0750 - sparse_categorical_accuracy: 0.9736 - val_loss: 0.3402 - val_sparse_categorical_accuracy: 0.8350 - learning_rate: 5.0000e-04
+## Epoch 169/500
+## 90/90 - 0s - 1ms/step - loss: 0.0664 - sparse_categorical_accuracy: 0.9785 - val_loss: 0.1295 - val_sparse_categorical_accuracy: 0.9459 - learning_rate: 5.0000e-04
+## Epoch 170/500
+## 90/90 - 0s - 2ms/step - loss: 0.0635 - sparse_categorical_accuracy: 0.9816 - val_loss: 0.0992 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 2.5000e-04
+## Epoch 171/500
+## 90/90 - 0s - 1ms/step - loss: 0.0593 - sparse_categorical_accuracy: 0.9816 - val_loss: 0.1230 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 2.5000e-04
+## Epoch 172/500
+## 90/90 - 0s - 1ms/step - loss: 0.0591 - sparse_categorical_accuracy: 0.9799 - val_loss: 0.1079 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 2.5000e-04
+## Epoch 173/500
+## 90/90 - 0s - 1ms/step - loss: 0.0593 - sparse_categorical_accuracy: 0.9816 - val_loss: 0.1073 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 2.5000e-04
+## Epoch 174/500
+## 90/90 - 0s - 2ms/step - loss: 0.0605 - sparse_categorical_accuracy: 0.9819 - val_loss: 0.0979 - val_sparse_categorical_accuracy: 0.9639 - learning_rate: 2.5000e-04
+## Epoch 175/500
+## 90/90 - 0s - 1ms/step - loss: 0.0616 - sparse_categorical_accuracy: 0.9802 - val_loss: 0.2084 - val_sparse_categorical_accuracy: 0.9223 - learning_rate: 2.5000e-04
+## Epoch 176/500
+## 90/90 - 0s - 1ms/step - loss: 0.0584 - sparse_categorical_accuracy: 0.9795 - val_loss: 0.0993 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 2.5000e-04
+## Epoch 177/500
+## 90/90 - 0s - 1ms/step - loss: 0.0582 - sparse_categorical_accuracy: 0.9823 - val_loss: 0.1498 - val_sparse_categorical_accuracy: 0.9487 - learning_rate: 2.5000e-04
+## Epoch 178/500
+## 90/90 - 0s - 1ms/step - loss: 0.0566 - sparse_categorical_accuracy: 0.9806 - val_loss: 0.1165 - val_sparse_categorical_accuracy: 0.9528 - learning_rate: 2.5000e-04
+## Epoch 179/500
+## 90/90 - 0s - 2ms/step - loss: 0.0566 - sparse_categorical_accuracy: 0.9806 - val_loss: 0.1714 - val_sparse_categorical_accuracy: 0.9293 - learning_rate: 2.5000e-04
+## Epoch 180/500
+## 90/90 - 0s - 2ms/step - loss: 0.0563 - sparse_categorical_accuracy: 0.9833 - val_loss: 0.1042 - val_sparse_categorical_accuracy: 0.9598 - learning_rate: 2.5000e-04
+## Epoch 181/500
+## 90/90 - 0s - 2ms/step - loss: 0.0609 - sparse_categorical_accuracy: 0.9806 - val_loss: 0.2017 - val_sparse_categorical_accuracy: 0.9209 - learning_rate: 2.5000e-04
+## Epoch 182/500
+## 90/90 - 0s - 1ms/step - loss: 0.0559 - sparse_categorical_accuracy: 0.9833 - val_loss: 0.1059 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 2.5000e-04
+## Epoch 183/500
+## 90/90 - 0s - 1ms/step - loss: 0.0597 - sparse_categorical_accuracy: 0.9802 - val_loss: 0.1279 - val_sparse_categorical_accuracy: 0.9445 - learning_rate: 2.5000e-04
+## Epoch 184/500
+## 90/90 - 0s - 1ms/step - loss: 0.0553 - sparse_categorical_accuracy: 0.9816 - val_loss: 0.1036 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 2.5000e-04
+## Epoch 185/500
+## 90/90 - 0s - 1ms/step - loss: 0.0570 - sparse_categorical_accuracy: 0.9809 - val_loss: 0.1022 - val_sparse_categorical_accuracy: 0.9681 - learning_rate: 2.5000e-04
+## Epoch 186/500
+## 90/90 - 0s - 1ms/step - loss: 0.0633 - sparse_categorical_accuracy: 0.9781 - val_loss: 0.1086 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 2.5000e-04
+## Epoch 187/500
+## 90/90 - 0s - 2ms/step - loss: 0.0576 - sparse_categorical_accuracy: 0.9833 - val_loss: 0.0969 - val_sparse_categorical_accuracy: 0.9681 - learning_rate: 2.5000e-04
+## Epoch 188/500
+## 90/90 - 0s - 1ms/step - loss: 0.0567 - sparse_categorical_accuracy: 0.9840 - val_loss: 0.1043 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 2.5000e-04
+## Epoch 189/500
+## 90/90 - 0s - 1ms/step - loss: 0.0546 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.1734 - val_sparse_categorical_accuracy: 0.9362 - learning_rate: 2.5000e-04
+## Epoch 190/500
+## 90/90 - 0s - 1ms/step - loss: 0.0639 - sparse_categorical_accuracy: 0.9799 - val_loss: 0.2847 - val_sparse_categorical_accuracy: 0.9015 - learning_rate: 2.5000e-04
+## Epoch 191/500
+## 90/90 - 0s - 1ms/step - loss: 0.0551 - sparse_categorical_accuracy: 0.9799 - val_loss: 0.4059 - val_sparse_categorical_accuracy: 0.8682 - learning_rate: 2.5000e-04
+## Epoch 192/500
+## 90/90 - 0s - 1ms/step - loss: 0.0613 - sparse_categorical_accuracy: 0.9802 - val_loss: 0.3821 - val_sparse_categorical_accuracy: 0.8724 - learning_rate: 2.5000e-04
+## Epoch 193/500
+## 90/90 - 0s - 1ms/step - loss: 0.0559 - sparse_categorical_accuracy: 0.9833 - val_loss: 0.1028 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 2.5000e-04
+## Epoch 194/500
+## 90/90 - 0s - 1ms/step - loss: 0.0553 - sparse_categorical_accuracy: 0.9806 - val_loss: 0.1008 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 2.5000e-04
+## Epoch 195/500
+## 90/90 - 0s - 1ms/step - loss: 0.0557 - sparse_categorical_accuracy: 0.9830 - val_loss: 0.0996 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 2.5000e-04
+## Epoch 196/500
+## 90/90 - 0s - 1ms/step - loss: 0.0581 - sparse_categorical_accuracy: 0.9778 - val_loss: 0.1143 - val_sparse_categorical_accuracy: 0.9542 - learning_rate: 2.5000e-04
+## Epoch 197/500
+## 90/90 - 0s - 1ms/step - loss: 0.0544 - sparse_categorical_accuracy: 0.9830 - val_loss: 0.1027 - val_sparse_categorical_accuracy: 0.9709 - learning_rate: 2.5000e-04
+## Epoch 198/500
+## 90/90 - 0s - 1ms/step - loss: 0.0533 - sparse_categorical_accuracy: 0.9861 - val_loss: 0.1310 - val_sparse_categorical_accuracy: 0.9598 - learning_rate: 2.5000e-04
+## Epoch 199/500
+## 90/90 - 0s - 1ms/step - loss: 0.0578 - sparse_categorical_accuracy: 0.9795 - val_loss: 0.1236 - val_sparse_categorical_accuracy: 0.9515 - learning_rate: 2.5000e-04
+## Epoch 200/500
+## 90/90 - 0s - 1ms/step - loss: 0.0582 - sparse_categorical_accuracy: 0.9813 - val_loss: 0.0987 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 2.5000e-04
+## Epoch 201/500
+## 90/90 - 0s - 1ms/step - loss: 0.0551 - sparse_categorical_accuracy: 0.9830 - val_loss: 0.1327 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 2.5000e-04
+## Epoch 202/500
+## 90/90 - 0s - 1ms/step - loss: 0.0543 - sparse_categorical_accuracy: 0.9823 - val_loss: 0.2115 - val_sparse_categorical_accuracy: 0.9140 - learning_rate: 2.5000e-04
+## Epoch 203/500
+## 90/90 - 0s - 1ms/step - loss: 0.0553 - sparse_categorical_accuracy: 0.9830 - val_loss: 0.2015 - val_sparse_categorical_accuracy: 0.9223 - learning_rate: 2.5000e-04
+## Epoch 204/500
+## 90/90 - 0s - 1ms/step - loss: 0.0508 - sparse_categorical_accuracy: 0.9840 - val_loss: 0.1078 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 2.5000e-04
+## Epoch 205/500
+## 90/90 - 0s - 1ms/step - loss: 0.0542 - sparse_categorical_accuracy: 0.9830 - val_loss: 0.0996 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 2.5000e-04
+## Epoch 206/500
+## 90/90 - 0s - 1ms/step - loss: 0.0559 - sparse_categorical_accuracy: 0.9806 - val_loss: 0.1172 - val_sparse_categorical_accuracy: 0.9528 - learning_rate: 2.5000e-04
+## Epoch 207/500
+## 90/90 - 0s - 2ms/step - loss: 0.0570 - sparse_categorical_accuracy: 0.9819 - val_loss: 0.0987 - val_sparse_categorical_accuracy: 0.9542 - learning_rate: 2.5000e-04
+## Epoch 208/500
+## 90/90 - 0s - 2ms/step - loss: 0.0521 - sparse_categorical_accuracy: 0.9840 - val_loss: 0.1000 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.2500e-04
+## Epoch 209/500
+## 90/90 - 0s - 1ms/step - loss: 0.0497 - sparse_categorical_accuracy: 0.9823 - val_loss: 0.1286 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 1.2500e-04
+## Epoch 210/500
+## 90/90 - 0s - 1ms/step - loss: 0.0541 - sparse_categorical_accuracy: 0.9816 - val_loss: 0.1028 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.2500e-04
+## Epoch 211/500
+## 90/90 - 0s - 1ms/step - loss: 0.0509 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.0993 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.2500e-04
+## Epoch 212/500
+## 90/90 - 0s - 1ms/step - loss: 0.0503 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.0987 - val_sparse_categorical_accuracy: 0.9709 - learning_rate: 1.2500e-04
+## Epoch 213/500
+## 90/90 - 0s - 2ms/step - loss: 0.0534 - sparse_categorical_accuracy: 0.9837 - val_loss: 0.0968 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.2500e-04
+## Epoch 214/500
+## 90/90 - 0s - 1ms/step - loss: 0.0532 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.1078 - val_sparse_categorical_accuracy: 0.9681 - learning_rate: 1.2500e-04
+## Epoch 215/500
+## 90/90 - 0s - 1ms/step - loss: 0.0488 - sparse_categorical_accuracy: 0.9851 - val_loss: 0.1013 - val_sparse_categorical_accuracy: 0.9709 - learning_rate: 1.2500e-04
+## Epoch 216/500
+## 90/90 - 0s - 1ms/step - loss: 0.0497 - sparse_categorical_accuracy: 0.9851 - val_loss: 0.1244 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 1.2500e-04
+## Epoch 217/500
+## 90/90 - 0s - 2ms/step - loss: 0.0495 - sparse_categorical_accuracy: 0.9847 - val_loss: 0.1141 - val_sparse_categorical_accuracy: 0.9639 - learning_rate: 1.2500e-04
+## Epoch 218/500
+## 90/90 - 0s - 2ms/step - loss: 0.0547 - sparse_categorical_accuracy: 0.9819 - val_loss: 0.0976 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 1.2500e-04
+## Epoch 219/500
+## 90/90 - 0s - 2ms/step - loss: 0.0480 - sparse_categorical_accuracy: 0.9868 - val_loss: 0.1387 - val_sparse_categorical_accuracy: 0.9556 - learning_rate: 1.2500e-04
+## Epoch 220/500
+## 90/90 - 0s - 1ms/step - loss: 0.0497 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.1161 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.2500e-04
+## Epoch 221/500
+## 90/90 - 0s - 1ms/step - loss: 0.0516 - sparse_categorical_accuracy: 0.9813 - val_loss: 0.1338 - val_sparse_categorical_accuracy: 0.9542 - learning_rate: 1.2500e-04
+## Epoch 222/500
+## 90/90 - 0s - 1ms/step - loss: 0.0525 - sparse_categorical_accuracy: 0.9806 - val_loss: 0.0995 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.2500e-04
+## Epoch 223/500
+## 90/90 - 0s - 1ms/step - loss: 0.0504 - sparse_categorical_accuracy: 0.9823 - val_loss: 0.0979 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 1.2500e-04
+## Epoch 224/500
+## 90/90 - 0s - 1ms/step - loss: 0.0471 - sparse_categorical_accuracy: 0.9851 - val_loss: 0.0994 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 1.2500e-04
+## Epoch 225/500
+## 90/90 - 0s - 1ms/step - loss: 0.0536 - sparse_categorical_accuracy: 0.9837 - val_loss: 0.1145 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 1.2500e-04
+## Epoch 226/500
+## 90/90 - 0s - 1ms/step - loss: 0.0487 - sparse_categorical_accuracy: 0.9840 - val_loss: 0.1047 - val_sparse_categorical_accuracy: 0.9736 - learning_rate: 1.2500e-04
+## Epoch 227/500
+## 90/90 - 0s - 1ms/step - loss: 0.0529 - sparse_categorical_accuracy: 0.9813 - val_loss: 0.1087 - val_sparse_categorical_accuracy: 0.9542 - learning_rate: 1.2500e-04
+## Epoch 228/500
+## 90/90 - 0s - 1ms/step - loss: 0.0477 - sparse_categorical_accuracy: 0.9882 - val_loss: 0.0992 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.2500e-04
+## Epoch 229/500
+## 90/90 - 0s - 1ms/step - loss: 0.0499 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.1064 - val_sparse_categorical_accuracy: 0.9681 - learning_rate: 1.2500e-04
+## Epoch 230/500
+## 90/90 - 0s - 2ms/step - loss: 0.0476 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.0967 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 1.2500e-04
+## Epoch 231/500
+## 90/90 - 0s - 1ms/step - loss: 0.0527 - sparse_categorical_accuracy: 0.9816 - val_loss: 0.1730 - val_sparse_categorical_accuracy: 0.9390 - learning_rate: 1.2500e-04
+## Epoch 232/500
+## 90/90 - 0s - 1ms/step - loss: 0.0496 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.1109 - val_sparse_categorical_accuracy: 0.9639 - learning_rate: 1.2500e-04
+## Epoch 233/500
+## 90/90 - 0s - 2ms/step - loss: 0.0483 - sparse_categorical_accuracy: 0.9840 - val_loss: 0.0963 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.2500e-04
+## Epoch 234/500
+## 90/90 - 0s - 1ms/step - loss: 0.0493 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.1158 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.2500e-04
+## Epoch 235/500
+## 90/90 - 0s - 1ms/step - loss: 0.0493 - sparse_categorical_accuracy: 0.9837 - val_loss: 0.1057 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 1.2500e-04
+## Epoch 236/500
+## 90/90 - 0s - 1ms/step - loss: 0.0502 - sparse_categorical_accuracy: 0.9837 - val_loss: 0.0982 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 1.2500e-04
+## Epoch 237/500
+## 90/90 - 0s - 1ms/step - loss: 0.0501 - sparse_categorical_accuracy: 0.9819 - val_loss: 0.0992 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.2500e-04
+## Epoch 238/500
+## 90/90 - 0s - 1ms/step - loss: 0.0487 - sparse_categorical_accuracy: 0.9872 - val_loss: 0.1031 - val_sparse_categorical_accuracy: 0.9556 - learning_rate: 1.2500e-04
+## Epoch 239/500
+## 90/90 - 0s - 1ms/step - loss: 0.0491 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.1139 - val_sparse_categorical_accuracy: 0.9515 - learning_rate: 1.2500e-04
+## Epoch 240/500
+## 90/90 - 0s - 2ms/step - loss: 0.0478 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.0990 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 1.2500e-04
+## Epoch 241/500
+## 90/90 - 0s - 2ms/step - loss: 0.0498 - sparse_categorical_accuracy: 0.9830 - val_loss: 0.0994 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 1.2500e-04
+## Epoch 242/500
+## 90/90 - 0s - 2ms/step - loss: 0.0490 - sparse_categorical_accuracy: 0.9851 - val_loss: 0.1118 - val_sparse_categorical_accuracy: 0.9681 - learning_rate: 1.2500e-04
+## Epoch 243/500
+## 90/90 - 0s - 2ms/step - loss: 0.0523 - sparse_categorical_accuracy: 0.9823 - val_loss: 0.1052 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.2500e-04
+## Epoch 244/500
+## 90/90 - 0s - 2ms/step - loss: 0.0527 - sparse_categorical_accuracy: 0.9806 - val_loss: 0.1055 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.2500e-04
+## Epoch 245/500
+## 90/90 - 0s - 2ms/step - loss: 0.0476 - sparse_categorical_accuracy: 0.9865 - val_loss: 0.1045 - val_sparse_categorical_accuracy: 0.9542 - learning_rate: 1.2500e-04
+## Epoch 246/500
+## 90/90 - 0s - 1ms/step - loss: 0.0518 - sparse_categorical_accuracy: 0.9823 - val_loss: 0.1214 - val_sparse_categorical_accuracy: 0.9501 - learning_rate: 1.2500e-04
+## Epoch 247/500
+## 90/90 - 0s - 1ms/step - loss: 0.0457 - sparse_categorical_accuracy: 0.9875 - val_loss: 0.0964 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.2500e-04
+## Epoch 248/500
+## 90/90 - 0s - 1ms/step - loss: 0.0450 - sparse_categorical_accuracy: 0.9885 - val_loss: 0.1161 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 1.2500e-04
+## Epoch 249/500
+## 90/90 - 0s - 1ms/step - loss: 0.0492 - sparse_categorical_accuracy: 0.9875 - val_loss: 0.1282 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 1.2500e-04
+## Epoch 250/500
+## 90/90 - 0s - 2ms/step - loss: 0.0510 - sparse_categorical_accuracy: 0.9826 - val_loss: 0.0972 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.2500e-04
+## Epoch 251/500
+## 90/90 - 0s - 1ms/step - loss: 0.0506 - sparse_categorical_accuracy: 0.9833 - val_loss: 0.0986 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.2500e-04
+## Epoch 252/500
+## 90/90 - 0s - 2ms/step - loss: 0.0503 - sparse_categorical_accuracy: 0.9847 - val_loss: 0.1075 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.2500e-04
+## Epoch 253/500
+## 90/90 - 0s - 2ms/step - loss: 0.0507 - sparse_categorical_accuracy: 0.9837 - val_loss: 0.0999 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.2500e-04
+## Epoch 254/500
+## 90/90 - 0s - 2ms/step - loss: 0.0478 - sparse_categorical_accuracy: 0.9858 - val_loss: 0.0971 - val_sparse_categorical_accuracy: 0.9736 - learning_rate: 1.0000e-04
+## Epoch 255/500
+## 90/90 - 0s - 2ms/step - loss: 0.0464 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.0954 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.0000e-04
+## Epoch 256/500
+## 90/90 - 0s - 2ms/step - loss: 0.0476 - sparse_categorical_accuracy: 0.9865 - val_loss: 0.0954 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.0000e-04
+## Epoch 257/500
+## 90/90 - 0s - 1ms/step - loss: 0.0472 - sparse_categorical_accuracy: 0.9875 - val_loss: 0.1047 - val_sparse_categorical_accuracy: 0.9736 - learning_rate: 1.0000e-04
+## Epoch 258/500
+## 90/90 - 0s - 1ms/step - loss: 0.0448 - sparse_categorical_accuracy: 0.9872 - val_loss: 0.1099 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 259/500
+## 90/90 - 0s - 1ms/step - loss: 0.0447 - sparse_categorical_accuracy: 0.9868 - val_loss: 0.1245 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 1.0000e-04
+## Epoch 260/500
+## 90/90 - 0s - 1ms/step - loss: 0.0454 - sparse_categorical_accuracy: 0.9865 - val_loss: 0.1113 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 1.0000e-04
+## Epoch 261/500
+## 90/90 - 0s - 1ms/step - loss: 0.0488 - sparse_categorical_accuracy: 0.9840 - val_loss: 0.1697 - val_sparse_categorical_accuracy: 0.9404 - learning_rate: 1.0000e-04
+## Epoch 262/500
+## 90/90 - 0s - 1ms/step - loss: 0.0450 - sparse_categorical_accuracy: 0.9878 - val_loss: 0.0987 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.0000e-04
+## Epoch 263/500
+## 90/90 - 0s - 1ms/step - loss: 0.0439 - sparse_categorical_accuracy: 0.9875 - val_loss: 0.0975 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 264/500
+## 90/90 - 0s - 1ms/step - loss: 0.0498 - sparse_categorical_accuracy: 0.9840 - val_loss: 0.1111 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.0000e-04
+## Epoch 265/500
+## 90/90 - 0s - 1ms/step - loss: 0.0523 - sparse_categorical_accuracy: 0.9809 - val_loss: 0.0983 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 266/500
+## 90/90 - 0s - 1ms/step - loss: 0.0459 - sparse_categorical_accuracy: 0.9865 - val_loss: 0.0997 - val_sparse_categorical_accuracy: 0.9709 - learning_rate: 1.0000e-04
+## Epoch 267/500
+## 90/90 - 0s - 1ms/step - loss: 0.0462 - sparse_categorical_accuracy: 0.9868 - val_loss: 0.1002 - val_sparse_categorical_accuracy: 0.9709 - learning_rate: 1.0000e-04
+## Epoch 268/500
+## 90/90 - 0s - 1ms/step - loss: 0.0438 - sparse_categorical_accuracy: 0.9882 - val_loss: 0.1058 - val_sparse_categorical_accuracy: 0.9681 - learning_rate: 1.0000e-04
+## Epoch 269/500
+## 90/90 - 0s - 2ms/step - loss: 0.0491 - sparse_categorical_accuracy: 0.9872 - val_loss: 0.1289 - val_sparse_categorical_accuracy: 0.9556 - learning_rate: 1.0000e-04
+## Epoch 270/500
+## 90/90 - 0s - 2ms/step - loss: 0.0430 - sparse_categorical_accuracy: 0.9889 - val_loss: 0.1078 - val_sparse_categorical_accuracy: 0.9556 - learning_rate: 1.0000e-04
+## Epoch 271/500
+## 90/90 - 0s - 1ms/step - loss: 0.0454 - sparse_categorical_accuracy: 0.9875 - val_loss: 0.1096 - val_sparse_categorical_accuracy: 0.9681 - learning_rate: 1.0000e-04
+## Epoch 272/500
+## 90/90 - 0s - 1ms/step - loss: 0.0455 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.1262 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 1.0000e-04
+## Epoch 273/500
+## 90/90 - 0s - 1ms/step - loss: 0.0469 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.1129 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 1.0000e-04
+## Epoch 274/500
+## 90/90 - 0s - 1ms/step - loss: 0.0429 - sparse_categorical_accuracy: 0.9896 - val_loss: 0.1119 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.0000e-04
+## Epoch 275/500
+## 90/90 - 0s - 1ms/step - loss: 0.0430 - sparse_categorical_accuracy: 0.9865 - val_loss: 0.1392 - val_sparse_categorical_accuracy: 0.9542 - learning_rate: 1.0000e-04
+## Epoch 276/500
+## 90/90 - 0s - 1ms/step - loss: 0.0463 - sparse_categorical_accuracy: 0.9837 - val_loss: 0.0976 - val_sparse_categorical_accuracy: 0.9709 - learning_rate: 1.0000e-04
+## Epoch 277/500
+## 90/90 - 0s - 1ms/step - loss: 0.0457 - sparse_categorical_accuracy: 0.9878 - val_loss: 0.1036 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 278/500
+## 90/90 - 0s - 1ms/step - loss: 0.0465 - sparse_categorical_accuracy: 0.9851 - val_loss: 0.1205 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.0000e-04
+## Epoch 279/500
+## 90/90 - 0s - 1ms/step - loss: 0.0473 - sparse_categorical_accuracy: 0.9858 - val_loss: 0.0958 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 280/500
+## 90/90 - 0s - 2ms/step - loss: 0.0459 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.0947 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.0000e-04
+## Epoch 281/500
+## 90/90 - 0s - 1ms/step - loss: 0.0438 - sparse_categorical_accuracy: 0.9861 - val_loss: 0.1216 - val_sparse_categorical_accuracy: 0.9598 - learning_rate: 1.0000e-04
+## Epoch 282/500
+## 90/90 - 0s - 1ms/step - loss: 0.0419 - sparse_categorical_accuracy: 0.9878 - val_loss: 0.1008 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 283/500
+## 90/90 - 0s - 1ms/step - loss: 0.0481 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.1129 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.0000e-04
+## Epoch 284/500
+## 90/90 - 0s - 1ms/step - loss: 0.0480 - sparse_categorical_accuracy: 0.9847 - val_loss: 0.1418 - val_sparse_categorical_accuracy: 0.9542 - learning_rate: 1.0000e-04
+## Epoch 285/500
+## 90/90 - 0s - 1ms/step - loss: 0.0467 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.1058 - val_sparse_categorical_accuracy: 0.9709 - learning_rate: 1.0000e-04
+## Epoch 286/500
+## 90/90 - 0s - 1ms/step - loss: 0.0449 - sparse_categorical_accuracy: 0.9837 - val_loss: 0.1039 - val_sparse_categorical_accuracy: 0.9723 - learning_rate: 1.0000e-04
+## Epoch 287/500
+## 90/90 - 0s - 1ms/step - loss: 0.0450 - sparse_categorical_accuracy: 0.9872 - val_loss: 0.1015 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 288/500
+## 90/90 - 0s - 2ms/step - loss: 0.0447 - sparse_categorical_accuracy: 0.9865 - val_loss: 0.1156 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.0000e-04
+## Epoch 289/500
+## 90/90 - 0s - 1ms/step - loss: 0.0460 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.1000 - val_sparse_categorical_accuracy: 0.9681 - learning_rate: 1.0000e-04
+## Epoch 290/500
+## 90/90 - 0s - 1ms/step - loss: 0.0482 - sparse_categorical_accuracy: 0.9865 - val_loss: 0.0967 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.0000e-04
+## Epoch 291/500
+## 90/90 - 0s - 1ms/step - loss: 0.0433 - sparse_categorical_accuracy: 0.9882 - val_loss: 0.1002 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 1.0000e-04
+## Epoch 292/500
+## 90/90 - 0s - 1ms/step - loss: 0.0506 - sparse_categorical_accuracy: 0.9830 - val_loss: 0.1102 - val_sparse_categorical_accuracy: 0.9542 - learning_rate: 1.0000e-04
+## Epoch 293/500
+## 90/90 - 0s - 1ms/step - loss: 0.0473 - sparse_categorical_accuracy: 0.9868 - val_loss: 0.1139 - val_sparse_categorical_accuracy: 0.9542 - learning_rate: 1.0000e-04
+## Epoch 294/500
+## 90/90 - 0s - 1ms/step - loss: 0.0459 - sparse_categorical_accuracy: 0.9861 - val_loss: 0.1005 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 1.0000e-04
+## Epoch 295/500
+## 90/90 - 0s - 1ms/step - loss: 0.0502 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.0978 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 296/500
+## 90/90 - 0s - 1ms/step - loss: 0.0477 - sparse_categorical_accuracy: 0.9851 - val_loss: 0.1006 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 1.0000e-04
+## Epoch 297/500
+## 90/90 - 0s - 1ms/step - loss: 0.0474 - sparse_categorical_accuracy: 0.9868 - val_loss: 0.0978 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.0000e-04
+## Epoch 298/500
+## 90/90 - 0s - 1ms/step - loss: 0.0465 - sparse_categorical_accuracy: 0.9865 - val_loss: 0.0953 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 1.0000e-04
+## Epoch 299/500
+## 90/90 - 0s - 1ms/step - loss: 0.0410 - sparse_categorical_accuracy: 0.9896 - val_loss: 0.1107 - val_sparse_categorical_accuracy: 0.9653 - learning_rate: 1.0000e-04
+## Epoch 300/500
+## 90/90 - 0s - 1ms/step - loss: 0.0423 - sparse_categorical_accuracy: 0.9875 - val_loss: 0.1099 - val_sparse_categorical_accuracy: 0.9639 - learning_rate: 1.0000e-04
+## Epoch 301/500
+## 90/90 - 0s - 1ms/step - loss: 0.0457 - sparse_categorical_accuracy: 0.9851 - val_loss: 0.0983 - val_sparse_categorical_accuracy: 0.9598 - learning_rate: 1.0000e-04
+## Epoch 302/500
+## 90/90 - 0s - 1ms/step - loss: 0.0422 - sparse_categorical_accuracy: 0.9889 - val_loss: 0.1233 - val_sparse_categorical_accuracy: 0.9598 - learning_rate: 1.0000e-04
+## Epoch 303/500
+## 90/90 - 0s - 2ms/step - loss: 0.0417 - sparse_categorical_accuracy: 0.9896 - val_loss: 0.1175 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 1.0000e-04
+## Epoch 304/500
+## 90/90 - 0s - 2ms/step - loss: 0.0467 - sparse_categorical_accuracy: 0.9861 - val_loss: 0.1007 - val_sparse_categorical_accuracy: 0.9723 - learning_rate: 1.0000e-04
+## Epoch 305/500
+## 90/90 - 0s - 2ms/step - loss: 0.0466 - sparse_categorical_accuracy: 0.9851 - val_loss: 0.1058 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 306/500
+## 90/90 - 0s - 1ms/step - loss: 0.0402 - sparse_categorical_accuracy: 0.9899 - val_loss: 0.0999 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 1.0000e-04
+## Epoch 307/500
+## 90/90 - 0s - 2ms/step - loss: 0.0465 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.1130 - val_sparse_categorical_accuracy: 0.9528 - learning_rate: 1.0000e-04
+## Epoch 308/500
+## 90/90 - 0s - 1ms/step - loss: 0.0463 - sparse_categorical_accuracy: 0.9858 - val_loss: 0.0987 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 1.0000e-04
+## Epoch 309/500
+## 90/90 - 0s - 1ms/step - loss: 0.0472 - sparse_categorical_accuracy: 0.9851 - val_loss: 0.1179 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.0000e-04
+## Epoch 310/500
+## 90/90 - 0s - 1ms/step - loss: 0.0447 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.1036 - val_sparse_categorical_accuracy: 0.9709 - learning_rate: 1.0000e-04
+## Epoch 311/500
+## 90/90 - 0s - 1ms/step - loss: 0.0448 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.1012 - val_sparse_categorical_accuracy: 0.9681 - learning_rate: 1.0000e-04
+## Epoch 312/500
+## 90/90 - 0s - 1ms/step - loss: 0.0442 - sparse_categorical_accuracy: 0.9851 - val_loss: 0.1172 - val_sparse_categorical_accuracy: 0.9598 - learning_rate: 1.0000e-04
+## Epoch 313/500
+## 90/90 - 0s - 2ms/step - loss: 0.0438 - sparse_categorical_accuracy: 0.9889 - val_loss: 0.0990 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 314/500
+## 90/90 - 0s - 2ms/step - loss: 0.0432 - sparse_categorical_accuracy: 0.9875 - val_loss: 0.0979 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 315/500
+## 90/90 - 0s - 1ms/step - loss: 0.0474 - sparse_categorical_accuracy: 0.9858 - val_loss: 0.0980 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 1.0000e-04
+## Epoch 316/500
+## 90/90 - 0s - 1ms/step - loss: 0.0429 - sparse_categorical_accuracy: 0.9889 - val_loss: 0.1030 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 1.0000e-04
+## Epoch 317/500
+## 90/90 - 0s - 3ms/step - loss: 0.0447 - sparse_categorical_accuracy: 0.9840 - val_loss: 0.1018 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 1.0000e-04
+## Epoch 318/500
+## 90/90 - 0s - 2ms/step - loss: 0.0423 - sparse_categorical_accuracy: 0.9868 - val_loss: 0.1024 - val_sparse_categorical_accuracy: 0.9723 - learning_rate: 1.0000e-04
+## Epoch 319/500
+## 90/90 - 0s - 2ms/step - loss: 0.0474 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.1197 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 1.0000e-04
+## Epoch 320/500
+## 90/90 - 0s - 2ms/step - loss: 0.0405 - sparse_categorical_accuracy: 0.9889 - val_loss: 0.0994 - val_sparse_categorical_accuracy: 0.9736 - learning_rate: 1.0000e-04
+## Epoch 321/500
+## 90/90 - 0s - 2ms/step - loss: 0.0442 - sparse_categorical_accuracy: 0.9878 - val_loss: 0.1499 - val_sparse_categorical_accuracy: 0.9528 - learning_rate: 1.0000e-04
+## Epoch 322/500
+## 90/90 - 0s - 2ms/step - loss: 0.0404 - sparse_categorical_accuracy: 0.9889 - val_loss: 0.1253 - val_sparse_categorical_accuracy: 0.9584 - learning_rate: 1.0000e-04
+## Epoch 323/500
+## 90/90 - 0s - 1ms/step - loss: 0.0468 - sparse_categorical_accuracy: 0.9844 - val_loss: 0.1081 - val_sparse_categorical_accuracy: 0.9709 - learning_rate: 1.0000e-04
+## Epoch 324/500
+## 90/90 - 0s - 1ms/step - loss: 0.0418 - sparse_categorical_accuracy: 0.9858 - val_loss: 0.1016 - val_sparse_categorical_accuracy: 0.9626 - learning_rate: 1.0000e-04
+## Epoch 325/500
+## 90/90 - 0s - 1ms/step - loss: 0.0420 - sparse_categorical_accuracy: 0.9868 - val_loss: 0.1290 - val_sparse_categorical_accuracy: 0.9501 - learning_rate: 1.0000e-04
+## Epoch 326/500
+## 90/90 - 0s - 2ms/step - loss: 0.0436 - sparse_categorical_accuracy: 0.9854 - val_loss: 0.1270 - val_sparse_categorical_accuracy: 0.9515 - learning_rate: 1.0000e-04
+## Epoch 327/500
+## 90/90 - 0s - 1ms/step - loss: 0.0417 - sparse_categorical_accuracy: 0.9865 - val_loss: 0.1009 - val_sparse_categorical_accuracy: 0.9695 - learning_rate: 1.0000e-04
+## Epoch 328/500
+## 90/90 - 0s - 2ms/step - loss: 0.0447 - sparse_categorical_accuracy: 0.9868 - val_loss: 0.1141 - val_sparse_categorical_accuracy: 0.9667 - learning_rate: 1.0000e-04
+## Epoch 329/500
+## 90/90 - 0s - 2ms/step - loss: 0.0422 - sparse_categorical_accuracy: 0.9885 - val_loss: 0.1198 - val_sparse_categorical_accuracy: 0.9570 - learning_rate: 1.0000e-04
+## Epoch 330/500
+## 90/90 - 0s - 3ms/step - loss: 0.0435 - sparse_categorical_accuracy: 0.9868 - val_loss: 0.0988 - val_sparse_categorical_accuracy: 0.9612 - learning_rate: 1.0000e-04
+## Epoch 330: early stopping
+
+
+

Evaluate model on test data +

+
+model <- load_model("best_model.keras")
+
+results <- model |> evaluate(x_test, y_test)
+
## 42/42 - 1s - 15ms/step - loss: 0.0980 - sparse_categorical_accuracy: 0.9682
+
+str(results)
+
## List of 2
+##  $ loss                       : num 0.098
+##  $ sparse_categorical_accuracy: num 0.968
+
+cat(
+  "Test accuracy: ", results$sparse_categorical_accuracy, "\n",
+  "Test loss: ", results$loss, "\n",
+  sep = ""
+)
+
## Test accuracy: 0.9681818
+## Test loss: 0.09795157
+
+
+

Plot the model’s training history +

+
+plot(history)
+
+Plot of Training History Metrics
Plot of Training History Metrics
+
+

Plot just the training and validation accuracy:

+
+plot(history, metric = "sparse_categorical_accuracy") +
+  # scale x axis to actual number of epochs run before early stopping
+  ggplot2::xlim(0, length(history$metrics$loss))
+
+Plot of Accuracy During Training
Plot of Accuracy During Training
+
+

We can see how the training accuracy reaches almost 0.95 after 100 +epochs. However, by observing the validation accuracy we can see how the +network still needs training until it reaches almost 0.97 for both the +validation and the training accuracy after 200 epochs. Beyond the 200th +epoch, if we continue on training, the validation accuracy will start +decreasing while the training accuracy will continue on increasing: the +model starts overfitting.

+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-12-1.png b/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-12-1.png new file mode 100644 index 0000000000..3337ad475e Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-12-1.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-13-1.png b/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-13-1.png new file mode 100644 index 0000000000..705c81360a Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-13-1.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-3-1.png b/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-3-1.png new file mode 100644 index 0000000000..a2016e7967 Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-3-1.png differ diff --git a/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-9-1.png b/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-9-1.png new file mode 100644 index 0000000000..45d0436e03 Binary files /dev/null and b/docs/dev/articles/examples/timeseries/timeseries_classification_from_scratch/unnamed-chunk-9-1.png differ diff --git a/docs/dev/articles/examples/vision/autoencoder.html b/docs/dev/articles/examples/vision/autoencoder.html new file mode 100644 index 0000000000..679e50987f --- /dev/null +++ b/docs/dev/articles/examples/vision/autoencoder.html @@ -0,0 +1,598 @@ + + + + + + + + +Convolutional autoencoder for image denoising • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

This example demonstrates how to implement a deep convolutional +autoencoder for image denoising, mapping noisy digits images from the +MNIST dataset to clean digits images. This implementation is based on an +original blog post titled Building +Autoencoders in Keras by François Chollet.

+
+
+

Setup +

+
+library(keras3)
+
+# Normalizes the supplied array and reshapes it.
+preprocess <- function(array) {
+  array_reshape(array/255, c(dim(array)[1], 28, 28, 1))
+}
+
+# Adds random noise to each image in the supplied array.
+noise <- function(array) {
+  noise_factor <- 0.4
+  noisy_array <- array + noise_factor * random_normal(dim(array))
+  op_clip(noisy_array, 0.0, 1.0)
+}
+
+display <- function(array1, array2) {
+  n <- 2
+  indices <- sample.int(dim(array1)[1], n)
+  images1 <- as.array(array1)[indices, , , ]
+  images2 <- as.array(array2)[indices, , , ]
+
+  par(mfrow = c(2, n), mar = c(0, 0, 0, 0))
+  for (i in seq_len(n)) {
+    plot(as.raster(images1[i, , ]))
+    plot(as.raster(images2[i, , ]))
+  }
+}
+
+
+

Prepare the data +

+
+# Since we only need images from the dataset to encode and decode, we
+# won't use the labels.
+c(c(train_data, .), c(test_data, .)) %<-% dataset_mnist()
+
+# Normalize and reshape the data
+train_data <- preprocess(train_data)
+test_data <- preprocess(test_data)
+
+# Create a copy of the data with added noise
+noisy_train_data <- noise(train_data)
+noisy_test_data <- noise(test_data)
+
+# Display the train data and a version of it with added noise
+display(train_data, noisy_train_data)
+
+plot of chunk unnamed-chunk-2
plot of chunk unnamed-chunk-2
+
+
+
+

Build the autoencoder +

+

We are going to use the Functional API to build our convolutional +autoencoder.

+
+input <- keras_input(shape = c(28, 28, 1))
+
+# Encoder
+enc <- input |>
+  layer_conv_2d(filters = 32, kernel_size = c(3, 3),
+                activation = "relu", padding = "same") |>
+  layer_max_pooling_2d(pool_size = c(2, 2), padding = "same") |>
+  layer_conv_2d(filters = 32, kernel_size = c(3, 3),
+                activation = "relu", padding = "same") |>
+  layer_max_pooling_2d(pool_size = c(2, 2), padding = "same")
+
+# Decoder
+dec <- enc |>
+  layer_conv_2d_transpose(filters = 32, kernel_size = c(3, 3), strides = 2,
+                          activation = "relu", padding = "same") |>
+  layer_conv_2d_transpose(filters = 32, kernel_size = c(3, 3), strides = 2,
+                          activation = "relu", padding = "same") |>
+  layer_conv_2d(filters = 1, kernel_size = c(3, 3),
+                activation = "sigmoid", padding = "same")
+
+# Autoencoder
+autoencoder <- keras_model(input, dec)
+autoencoder |> compile(optimizer = "adam", loss = "binary_crossentropy")
+autoencoder |> summary()
+
## Model: "functional_1"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ input_layer (InputLayer)        │ (None, 28, 28, 1)      │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d (Conv2D)                 │ (None, 28, 28, 32)     │           320
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d (MaxPooling2D)    │ (None, 14, 14, 32)     │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_1 (Conv2D)               │ (None, 14, 14, 32)     │         9,248
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d_1 (MaxPooling2D)  │ (None, 7, 7, 32)       │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose                │ (None, 14, 14, 32)     │         9,248
+## │ (Conv2DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose_1              │ (None, 28, 28, 32)     │         9,248
+## │ (Conv2DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_2 (Conv2D)               │ (None, 28, 28, 1)      │           289
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 28,353 (110.75 KB)
+##  Trainable params: 28,353 (110.75 KB)
+##  Non-trainable params: 0 (0.00 B)
+

Now we can train our autoencoder using train_data as +both our input data and target. Notice we are setting up the validation +data using the same format.

+
+autoencoder |> fit(
+  x = train_data,
+  y = train_data,
+  epochs = 50,
+  batch_size = 128,
+  shuffle = TRUE,
+  validation_data = list(test_data, test_data),
+)
+
## Epoch 1/50
+## 469/469 - 5s - 10ms/step - loss: 0.1371 - val_loss: 0.0727
+## Epoch 2/50
+## 469/469 - 1s - 3ms/step - loss: 0.0712 - val_loss: 0.0692
+## Epoch 3/50
+## 469/469 - 1s - 3ms/step - loss: 0.0690 - val_loss: 0.0679
+## Epoch 4/50
+## 469/469 - 1s - 3ms/step - loss: 0.0679 - val_loss: 0.0671
+## Epoch 5/50
+## 469/469 - 1s - 3ms/step - loss: 0.0672 - val_loss: 0.0665
+## Epoch 6/50
+## 469/469 - 1s - 3ms/step - loss: 0.0667 - val_loss: 0.0660
+## Epoch 7/50
+## 469/469 - 1s - 3ms/step - loss: 0.0662 - val_loss: 0.0656
+## Epoch 8/50
+## 469/469 - 1s - 3ms/step - loss: 0.0659 - val_loss: 0.0653
+## Epoch 9/50
+## 469/469 - 2s - 3ms/step - loss: 0.0656 - val_loss: 0.0650
+## Epoch 10/50
+## 469/469 - 2s - 3ms/step - loss: 0.0653 - val_loss: 0.0648
+## Epoch 11/50
+## 469/469 - 1s - 3ms/step - loss: 0.0651 - val_loss: 0.0646
+## Epoch 12/50
+## 469/469 - 1s - 3ms/step - loss: 0.0650 - val_loss: 0.0644
+## Epoch 13/50
+## 469/469 - 1s - 3ms/step - loss: 0.0648 - val_loss: 0.0643
+## Epoch 14/50
+## 469/469 - 2s - 3ms/step - loss: 0.0646 - val_loss: 0.0642
+## Epoch 15/50
+## 469/469 - 1s - 3ms/step - loss: 0.0645 - val_loss: 0.0640
+## Epoch 16/50
+## 469/469 - 1s - 3ms/step - loss: 0.0644 - val_loss: 0.0639
+## Epoch 17/50
+## 469/469 - 1s - 3ms/step - loss: 0.0643 - val_loss: 0.0638
+## Epoch 18/50
+## 469/469 - 1s - 3ms/step - loss: 0.0642 - val_loss: 0.0637
+## Epoch 19/50
+## 469/469 - 1s - 3ms/step - loss: 0.0641 - val_loss: 0.0637
+## Epoch 20/50
+## 469/469 - 1s - 3ms/step - loss: 0.0640 - val_loss: 0.0636
+## Epoch 21/50
+## 469/469 - 1s - 3ms/step - loss: 0.0639 - val_loss: 0.0635
+## Epoch 22/50
+## 469/469 - 1s - 3ms/step - loss: 0.0639 - val_loss: 0.0634
+## Epoch 23/50
+## 469/469 - 1s - 3ms/step - loss: 0.0638 - val_loss: 0.0634
+## Epoch 24/50
+## 469/469 - 1s - 3ms/step - loss: 0.0637 - val_loss: 0.0633
+## Epoch 25/50
+## 469/469 - 1s - 3ms/step - loss: 0.0637 - val_loss: 0.0633
+## Epoch 26/50
+## 469/469 - 1s - 3ms/step - loss: 0.0636 - val_loss: 0.0632
+## Epoch 27/50
+## 469/469 - 1s - 3ms/step - loss: 0.0636 - val_loss: 0.0632
+## Epoch 28/50
+## 469/469 - 1s - 3ms/step - loss: 0.0635 - val_loss: 0.0631
+## Epoch 29/50
+## 469/469 - 1s - 3ms/step - loss: 0.0635 - val_loss: 0.0631
+## Epoch 30/50
+## 469/469 - 1s - 3ms/step - loss: 0.0635 - val_loss: 0.0631
+## Epoch 31/50
+## 469/469 - 1s - 3ms/step - loss: 0.0634 - val_loss: 0.0630
+## Epoch 32/50
+## 469/469 - 1s - 3ms/step - loss: 0.0634 - val_loss: 0.0630
+## Epoch 33/50
+## 469/469 - 1s - 3ms/step - loss: 0.0633 - val_loss: 0.0629
+## Epoch 34/50
+## 469/469 - 1s - 3ms/step - loss: 0.0633 - val_loss: 0.0629
+## Epoch 35/50
+## 469/469 - 1s - 3ms/step - loss: 0.0633 - val_loss: 0.0629
+## Epoch 36/50
+## 469/469 - 1s - 3ms/step - loss: 0.0632 - val_loss: 0.0629
+## Epoch 37/50
+## 469/469 - 1s - 3ms/step - loss: 0.0632 - val_loss: 0.0628
+## Epoch 38/50
+## 469/469 - 1s - 3ms/step - loss: 0.0632 - val_loss: 0.0628
+## Epoch 39/50
+## 469/469 - 1s - 3ms/step - loss: 0.0631 - val_loss: 0.0628
+## Epoch 40/50
+## 469/469 - 1s - 3ms/step - loss: 0.0631 - val_loss: 0.0627
+## Epoch 41/50
+## 469/469 - 1s - 3ms/step - loss: 0.0631 - val_loss: 0.0627
+## Epoch 42/50
+## 469/469 - 1s - 3ms/step - loss: 0.0631 - val_loss: 0.0627
+## Epoch 43/50
+## 469/469 - 1s - 3ms/step - loss: 0.0630 - val_loss: 0.0627
+## Epoch 44/50
+## 469/469 - 1s - 3ms/step - loss: 0.0630 - val_loss: 0.0627
+## Epoch 45/50
+## 469/469 - 1s - 3ms/step - loss: 0.0630 - val_loss: 0.0626
+## Epoch 46/50
+## 469/469 - 1s - 3ms/step - loss: 0.0630 - val_loss: 0.0626
+## Epoch 47/50
+## 469/469 - 1s - 3ms/step - loss: 0.0629 - val_loss: 0.0626
+## Epoch 48/50
+## 469/469 - 1s - 3ms/step - loss: 0.0629 - val_loss: 0.0626
+## Epoch 49/50
+## 469/469 - 1s - 3ms/step - loss: 0.0629 - val_loss: 0.0626
+## Epoch 50/50
+## 469/469 - 1s - 3ms/step - loss: 0.0629 - val_loss: 0.0625
+

Let’s predict on our test dataset and display the original image +together with the prediction from our autoencoder.

+

Notice how the predictions are pretty close to the original images, +although not quite the same.

+
+predictions <- autoencoder |> predict(test_data)
+
## 313/313 - 1s - 2ms/step
+
+display(test_data, predictions)
+
+plot of chunk unnamed-chunk-5
plot of chunk unnamed-chunk-5
+
+

Now that we know that our autoencoder works, let’s retrain it using +the noisy data as our input and the clean data as our target. We want +our autoencoder to learn how to denoise the images.

+
+autoencoder |> fit(
+  x = noisy_train_data,
+  y = train_data,
+  epochs = 100,
+  batch_size = 128,
+  shuffle = TRUE,
+  validation_data = list(noisy_test_data, test_data),
+)
+
## Epoch 1/100
+## 469/469 - 1s - 3ms/step - loss: 0.1006 - val_loss: 0.0941
+## Epoch 2/100
+## 469/469 - 1s - 3ms/step - loss: 0.0938 - val_loss: 0.0922
+## Epoch 3/100
+## 469/469 - 1s - 3ms/step - loss: 0.0922 - val_loss: 0.0910
+## Epoch 4/100
+## 469/469 - 1s - 3ms/step - loss: 0.0912 - val_loss: 0.0901
+## Epoch 5/100
+## 469/469 - 1s - 3ms/step - loss: 0.0905 - val_loss: 0.0895
+## Epoch 6/100
+## 469/469 - 1s - 3ms/step - loss: 0.0899 - val_loss: 0.0891
+## Epoch 7/100
+## 469/469 - 1s - 3ms/step - loss: 0.0895 - val_loss: 0.0887
+## Epoch 8/100
+## 469/469 - 1s - 3ms/step - loss: 0.0891 - val_loss: 0.0883
+## Epoch 9/100
+## 469/469 - 1s - 3ms/step - loss: 0.0888 - val_loss: 0.0881
+## Epoch 10/100
+## 469/469 - 1s - 3ms/step - loss: 0.0885 - val_loss: 0.0878
+## Epoch 11/100
+## 469/469 - 1s - 3ms/step - loss: 0.0882 - val_loss: 0.0876
+## Epoch 12/100
+## 469/469 - 1s - 3ms/step - loss: 0.0880 - val_loss: 0.0874
+## Epoch 13/100
+## 469/469 - 1s - 3ms/step - loss: 0.0878 - val_loss: 0.0873
+## Epoch 14/100
+## 469/469 - 1s - 3ms/step - loss: 0.0877 - val_loss: 0.0871
+## Epoch 15/100
+## 469/469 - 1s - 3ms/step - loss: 0.0875 - val_loss: 0.0869
+## Epoch 16/100
+## 469/469 - 1s - 3ms/step - loss: 0.0874 - val_loss: 0.0868
+## Epoch 17/100
+## 469/469 - 1s - 3ms/step - loss: 0.0872 - val_loss: 0.0867
+## Epoch 18/100
+## 469/469 - 1s - 3ms/step - loss: 0.0871 - val_loss: 0.0866
+## Epoch 19/100
+## 469/469 - 1s - 3ms/step - loss: 0.0870 - val_loss: 0.0865
+## Epoch 20/100
+## 469/469 - 1s - 3ms/step - loss: 0.0869 - val_loss: 0.0864
+## Epoch 21/100
+## 469/469 - 1s - 3ms/step - loss: 0.0868 - val_loss: 0.0863
+## Epoch 22/100
+## 469/469 - 1s - 3ms/step - loss: 0.0867 - val_loss: 0.0862
+## Epoch 23/100
+## 469/469 - 1s - 3ms/step - loss: 0.0867 - val_loss: 0.0861
+## Epoch 24/100
+## 469/469 - 1s - 3ms/step - loss: 0.0866 - val_loss: 0.0861
+## Epoch 25/100
+## 469/469 - 2s - 4ms/step - loss: 0.0865 - val_loss: 0.0860
+## Epoch 26/100
+## 469/469 - 2s - 4ms/step - loss: 0.0865 - val_loss: 0.0860
+## Epoch 27/100
+## 469/469 - 2s - 4ms/step - loss: 0.0864 - val_loss: 0.0859
+## Epoch 28/100
+## 469/469 - 2s - 4ms/step - loss: 0.0863 - val_loss: 0.0858
+## Epoch 29/100
+## 469/469 - 1s - 3ms/step - loss: 0.0863 - val_loss: 0.0858
+## Epoch 30/100
+## 469/469 - 1s - 3ms/step - loss: 0.0862 - val_loss: 0.0858
+## Epoch 31/100
+## 469/469 - 1s - 3ms/step - loss: 0.0862 - val_loss: 0.0857
+## Epoch 32/100
+## 469/469 - 1s - 3ms/step - loss: 0.0861 - val_loss: 0.0857
+## Epoch 33/100
+## 469/469 - 1s - 3ms/step - loss: 0.0861 - val_loss: 0.0856
+## Epoch 34/100
+## 469/469 - 1s - 3ms/step - loss: 0.0861 - val_loss: 0.0856
+## Epoch 35/100
+## 469/469 - 1s - 3ms/step - loss: 0.0860 - val_loss: 0.0855
+## Epoch 36/100
+## 469/469 - 1s - 3ms/step - loss: 0.0860 - val_loss: 0.0855
+## Epoch 37/100
+## 469/469 - 1s - 3ms/step - loss: 0.0859 - val_loss: 0.0855
+## Epoch 38/100
+## 469/469 - 1s - 3ms/step - loss: 0.0859 - val_loss: 0.0855
+## Epoch 39/100
+## 469/469 - 1s - 3ms/step - loss: 0.0859 - val_loss: 0.0854
+## Epoch 40/100
+## 469/469 - 1s - 3ms/step - loss: 0.0858 - val_loss: 0.0854
+## Epoch 41/100
+## 469/469 - 1s - 3ms/step - loss: 0.0858 - val_loss: 0.0854
+## Epoch 42/100
+## 469/469 - 1s - 3ms/step - loss: 0.0858 - val_loss: 0.0854
+## Epoch 43/100
+## 469/469 - 1s - 3ms/step - loss: 0.0858 - val_loss: 0.0853
+## Epoch 44/100
+## 469/469 - 1s - 3ms/step - loss: 0.0857 - val_loss: 0.0853
+## Epoch 45/100
+## 469/469 - 1s - 3ms/step - loss: 0.0857 - val_loss: 0.0853
+## Epoch 46/100
+## 469/469 - 1s - 3ms/step - loss: 0.0857 - val_loss: 0.0853
+## Epoch 47/100
+## 469/469 - 1s - 3ms/step - loss: 0.0856 - val_loss: 0.0852
+## Epoch 48/100
+## 469/469 - 1s - 3ms/step - loss: 0.0856 - val_loss: 0.0852
+## Epoch 49/100
+## 469/469 - 1s - 3ms/step - loss: 0.0856 - val_loss: 0.0852
+## Epoch 50/100
+## 469/469 - 1s - 3ms/step - loss: 0.0856 - val_loss: 0.0852
+## Epoch 51/100
+## 469/469 - 1s - 3ms/step - loss: 0.0855 - val_loss: 0.0851
+## Epoch 52/100
+## 469/469 - 1s - 3ms/step - loss: 0.0855 - val_loss: 0.0851
+## Epoch 53/100
+## 469/469 - 1s - 3ms/step - loss: 0.0855 - val_loss: 0.0851
+## Epoch 54/100
+## 469/469 - 1s - 3ms/step - loss: 0.0855 - val_loss: 0.0851
+## Epoch 55/100
+## 469/469 - 1s - 3ms/step - loss: 0.0854 - val_loss: 0.0851
+## Epoch 56/100
+## 469/469 - 1s - 3ms/step - loss: 0.0854 - val_loss: 0.0850
+## Epoch 57/100
+## 469/469 - 1s - 3ms/step - loss: 0.0854 - val_loss: 0.0850
+## Epoch 58/100
+## 469/469 - 1s - 3ms/step - loss: 0.0854 - val_loss: 0.0850
+## Epoch 59/100
+## 469/469 - 1s - 3ms/step - loss: 0.0854 - val_loss: 0.0850
+## Epoch 60/100
+## 469/469 - 1s - 3ms/step - loss: 0.0853 - val_loss: 0.0850
+## Epoch 61/100
+## 469/469 - 1s - 3ms/step - loss: 0.0853 - val_loss: 0.0850
+## Epoch 62/100
+## 469/469 - 1s - 3ms/step - loss: 0.0853 - val_loss: 0.0849
+## Epoch 63/100
+## 469/469 - 1s - 3ms/step - loss: 0.0853 - val_loss: 0.0849
+## Epoch 64/100
+## 469/469 - 1s - 3ms/step - loss: 0.0853 - val_loss: 0.0849
+## Epoch 65/100
+## 469/469 - 1s - 3ms/step - loss: 0.0853 - val_loss: 0.0849
+## Epoch 66/100
+## 469/469 - 1s - 3ms/step - loss: 0.0852 - val_loss: 0.0849
+## Epoch 67/100
+## 469/469 - 1s - 3ms/step - loss: 0.0852 - val_loss: 0.0849
+## Epoch 68/100
+## 469/469 - 1s - 3ms/step - loss: 0.0852 - val_loss: 0.0849
+## Epoch 69/100
+## 469/469 - 1s - 3ms/step - loss: 0.0852 - val_loss: 0.0849
+## Epoch 70/100
+## 469/469 - 1s - 3ms/step - loss: 0.0852 - val_loss: 0.0848
+## Epoch 71/100
+## 469/469 - 1s - 3ms/step - loss: 0.0852 - val_loss: 0.0848
+## Epoch 72/100
+## 469/469 - 1s - 3ms/step - loss: 0.0851 - val_loss: 0.0848
+## Epoch 73/100
+## 469/469 - 1s - 3ms/step - loss: 0.0851 - val_loss: 0.0848
+## Epoch 74/100
+## 469/469 - 1s - 3ms/step - loss: 0.0851 - val_loss: 0.0848
+## Epoch 75/100
+## 469/469 - 1s - 3ms/step - loss: 0.0851 - val_loss: 0.0848
+## Epoch 76/100
+## 469/469 - 1s - 3ms/step - loss: 0.0851 - val_loss: 0.0848
+## Epoch 77/100
+## 469/469 - 1s - 3ms/step - loss: 0.0851 - val_loss: 0.0848
+## Epoch 78/100
+## 469/469 - 1s - 3ms/step - loss: 0.0851 - val_loss: 0.0847
+## Epoch 79/100
+## 469/469 - 1s - 3ms/step - loss: 0.0851 - val_loss: 0.0847
+## Epoch 80/100
+## 469/469 - 1s - 3ms/step - loss: 0.0850 - val_loss: 0.0847
+## Epoch 81/100
+## 469/469 - 1s - 3ms/step - loss: 0.0850 - val_loss: 0.0847
+## Epoch 82/100
+## 469/469 - 1s - 3ms/step - loss: 0.0850 - val_loss: 0.0847
+## Epoch 83/100
+## 469/469 - 1s - 3ms/step - loss: 0.0850 - val_loss: 0.0847
+## Epoch 84/100
+## 469/469 - 1s - 3ms/step - loss: 0.0850 - val_loss: 0.0847
+## Epoch 85/100
+## 469/469 - 1s - 3ms/step - loss: 0.0850 - val_loss: 0.0847
+## Epoch 86/100
+## 469/469 - 1s - 3ms/step - loss: 0.0850 - val_loss: 0.0847
+## Epoch 87/100
+## 469/469 - 1s - 3ms/step - loss: 0.0850 - val_loss: 0.0847
+## Epoch 88/100
+## 469/469 - 1s - 3ms/step - loss: 0.0850 - val_loss: 0.0846
+## Epoch 89/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 90/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 91/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 92/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 93/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 94/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 95/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 96/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 97/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 98/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 99/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+## Epoch 100/100
+## 469/469 - 1s - 3ms/step - loss: 0.0849 - val_loss: 0.0846
+

Let’s now predict on the noisy data and display the results of our +autoencoder.

+

Notice how the autoencoder does an amazing job at removing the noise +from the input images.

+
+predictions <- autoencoder |> predict(noisy_test_data)
+
## 313/313 - 0s - 673us/step
+
+display(noisy_test_data, predictions)
+
+plot of chunk unnamed-chunk-7
plot of chunk unnamed-chunk-7
+
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/vision/autoencoder/unnamed-chunk-2-1.png b/docs/dev/articles/examples/vision/autoencoder/unnamed-chunk-2-1.png new file mode 100644 index 0000000000..e23f5e78c7 Binary files /dev/null and b/docs/dev/articles/examples/vision/autoencoder/unnamed-chunk-2-1.png differ diff --git a/docs/dev/articles/examples/vision/autoencoder/unnamed-chunk-5-1.png b/docs/dev/articles/examples/vision/autoencoder/unnamed-chunk-5-1.png new file mode 100644 index 0000000000..7c791b98dd Binary files /dev/null and b/docs/dev/articles/examples/vision/autoencoder/unnamed-chunk-5-1.png differ diff --git a/docs/dev/articles/examples/vision/autoencoder/unnamed-chunk-7-1.png b/docs/dev/articles/examples/vision/autoencoder/unnamed-chunk-7-1.png new file mode 100644 index 0000000000..f45a20d014 Binary files /dev/null and b/docs/dev/articles/examples/vision/autoencoder/unnamed-chunk-7-1.png differ diff --git a/docs/dev/articles/examples/vision/mnist_convnet.html b/docs/dev/articles/examples/vision/mnist_convnet.html new file mode 100644 index 0000000000..bba7501197 --- /dev/null +++ b/docs/dev/articles/examples/vision/mnist_convnet.html @@ -0,0 +1,266 @@ + + + + + + + + +Simple MNIST convnet • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Setup +

+ +
+
+

Prepare the data +

+
+# Model / data parameters
+num_classes <- 10
+input_shape <- c(28, 28, 1)
+
+# Load the data and split it between train and test sets
+c(c(x_train, y_train), c(x_test, y_test)) %<-% dataset_mnist()
+
+# Scale images to the [0, 1] range
+x_train <- x_train / 255
+x_test <- x_test / 255
+# Make sure images have shape (28, 28, 1)
+x_train <- op_expand_dims(x_train, -1)
+x_test <- op_expand_dims(x_test, -1)
+
+
+dim(x_train)
+
## [1] 60000    28    28     1
+
+dim(x_test)
+
## [1] 10000    28    28     1
+
+# convert class vectors to binary class matrices
+y_train <- to_categorical(y_train, num_classes)
+y_test <- to_categorical(y_test, num_classes)
+
+
+

Build the model +

+
+model <- keras_model_sequential(input_shape = input_shape)
+model |>
+  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu") |>
+  layer_max_pooling_2d(pool_size = c(2, 2)) |>
+  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") |>
+  layer_max_pooling_2d(pool_size = c(2, 2)) |>
+  layer_flatten() |>
+  layer_dropout(rate = 0.5) |>
+  layer_dense(units = num_classes, activation = "softmax")
+
+summary(model)
+
## Model: "sequential"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ conv2d (Conv2D)                 │ (None, 26, 26, 32)     │           320
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d (MaxPooling2D)    │ (None, 13, 13, 32)     │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_1 (Conv2D)               │ (None, 11, 11, 64)     │        18,496
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d_1 (MaxPooling2D)  │ (None, 5, 5, 64)       │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ flatten (Flatten)               │ (None, 1600)           │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dropout (Dropout)               │ (None, 1600)           │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense (Dense)                   │ (None, 10)             │        16,010
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 34,826 (136.04 KB)
+##  Trainable params: 34,826 (136.04 KB)
+##  Non-trainable params: 0 (0.00 B)
+
+
+

Train the model +

+
+batch_size <- 128
+epochs <- 15
+
+model |> compile(
+  loss = "categorical_crossentropy",
+  optimizer = "adam",
+  metrics = "accuracy"
+)
+
+model |> fit(
+  x_train, y_train,
+  batch_size = batch_size,
+  epochs = epochs,
+  validation_split = 0.1
+)
+
## Epoch 1/15
+## 422/422 - 5s - 11ms/step - accuracy: 0.8895 - loss: 0.3636 - val_accuracy: 0.9787 - val_loss: 0.0792
+## Epoch 2/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9664 - loss: 0.1111 - val_accuracy: 0.9850 - val_loss: 0.0550
+## Epoch 3/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9743 - loss: 0.0824 - val_accuracy: 0.9882 - val_loss: 0.0441
+## Epoch 4/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9786 - loss: 0.0695 - val_accuracy: 0.9895 - val_loss: 0.0400
+## Epoch 5/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9804 - loss: 0.0626 - val_accuracy: 0.9900 - val_loss: 0.0355
+## Epoch 6/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9824 - loss: 0.0558 - val_accuracy: 0.9912 - val_loss: 0.0333
+## Epoch 7/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9835 - loss: 0.0501 - val_accuracy: 0.9918 - val_loss: 0.0310
+## Epoch 8/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9851 - loss: 0.0479 - val_accuracy: 0.9922 - val_loss: 0.0310
+## Epoch 9/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9862 - loss: 0.0444 - val_accuracy: 0.9920 - val_loss: 0.0300
+## Epoch 10/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9863 - loss: 0.0438 - val_accuracy: 0.9912 - val_loss: 0.0294
+## Epoch 11/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9873 - loss: 0.0394 - val_accuracy: 0.9913 - val_loss: 0.0304
+## Epoch 12/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9875 - loss: 0.0371 - val_accuracy: 0.9927 - val_loss: 0.0287
+## Epoch 13/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9891 - loss: 0.0346 - val_accuracy: 0.9920 - val_loss: 0.0292
+## Epoch 14/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9891 - loss: 0.0343 - val_accuracy: 0.9922 - val_loss: 0.0284
+## Epoch 15/15
+## 422/422 - 1s - 2ms/step - accuracy: 0.9895 - loss: 0.0320 - val_accuracy: 0.9920 - val_loss: 0.0282
+
+
+

Evaluate the trained model +

+
+score <- model |> evaluate(x_test, y_test, verbose = 0)
+score
+
## $accuracy
+## [1] 0.9914
+##
+## $loss
+## [1] 0.02402576
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/vision/mnist_siamese_graph.html b/docs/dev/articles/examples/vision/mnist_siamese_graph.html new file mode 100644 index 0000000000..1c28ec48a6 --- /dev/null +++ b/docs/dev/articles/examples/vision/mnist_siamese_graph.html @@ -0,0 +1,377 @@ + + + + + + + + +Train a Siamese MLP on pairs of digits from the MNIST dataset. • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

Siamese +Networks are neural networks which share weights between two or more +sister networks, each producing embedding vectors of its respective +inputs.

+

In supervised similarity learning, the networks are then trained to +maximize the contrast (distance) between embeddings of inputs of +different classes, while minimizing the distance between embeddings of +similar classes, resulting in embedding spaces that reflect the class +segmentation of the training inputs.

+

This implementation loosely follows Hadsell-et-al.’06 [1] (see paper +for mode details) but the euclidean distance is replaced by a +subtraction layer and one fully-connect (FC) layer.

+

[1] “Dimensionality Reduction by Learning an Invariant Mapping” https://yann.lecun.com/exdb/publis/pdf/hadsell-chopra-lecun-06.pdf

+

Gets to 98.11% test accuracy after 20 epochs. 3 seconds per epoch on +a AMD Ryzen 7 PRO 4750U (CPU)

+ +
+contrastive_loss <- function(y_true, y_pred) {
+    # Contrastive loss from Hadsell-et-al.'06
+    # https://yann.lecun.com/exdb/publis/pdf/hadsell-chopra-lecun-06.pdf
+    margin = 1
+    margin_square = op_square(op_maximum(margin - (y_pred), 0))
+    op_mean((1 - y_true) * op_square(y_pred) + (y_true) * margin_square)
+}
+
+
+

Create pairs of images +

+

We will train the model to differentiate between digits of different +classes. For example, digit 0 needs to be differentiated +from the rest of the digits (1 through 9), +digit 1 - from 0 and 2 through +9, and so on. To carry this out, we will select N random +images from class A (for example, for digit 0) and pair +them with N random images from another class B (for example, for digit +1). Then, we can repeat this process for all classes of +digits (until digit 9). Once we have paired digit +0 with other digits, we can repeat this process for the +remaining classes for the rest of the digits (from 1 until +9).

+
+create_pairs <- function(x, y) {
+    # Positive and negative pair creation.
+    # Alternates between positive and negative pairs.
+    digit_indices <- tapply(1:length(y), y, list)
+    y1 <- y
+    y2 <- sapply(y, function(a) sample(0:9,1,prob=0.1+0.8*(0:9==a)))
+    idx1 <- 1:nrow(x)
+    idx2 <- sapply(as.character(y2), function(a) sample(digit_indices[[a]],1))
+    is_same  <- 1*(y1==y2)
+    list(pair1 = x[idx1,], pair2 = x[idx2,], y = is_same)
+}
+
+compute_accuracy <- function(predictions, labels) {
+    # Compute classification accuracy with a fixed threshold on distances.
+    mean(labels[predictions > 0.5])
+}
+
+# the data, shuffled and split between train and test sets
+mnist   <- dataset_mnist()
+x_train <- mnist$train$x
+y_train <- mnist$train$y
+x_test  <- mnist$test$x
+y_test  <- mnist$test$y
+x_train <- array_reshape(x_train, c(nrow(x_train), 784))
+x_test  <- array_reshape(x_test, c(nrow(x_test), 784))
+x_train <- x_train / 255
+x_test  <- x_test / 255
+
+# create training+test positive and negative pairs
+tr <- create_pairs(x_train, y_train)
+te <- create_pairs(x_test,  y_test)
+
+names(tr)
+
## [1] "pair1" "pair2" "y"
+
+
+

Network definition +

+
+# input layers
+input_dim = 784
+input_1 <- layer_input(shape = c(input_dim))
+input_2 <- layer_input(shape = c(input_dim))
+
+# definition of the base network that will be shared
+base_network <- keras_model_sequential() %>%
+    layer_dense(units = 128, activation = 'relu') %>%
+    layer_dropout(rate = 0.1) %>%
+    layer_dense(units = 128, activation = 'relu') %>%
+    layer_dropout(rate = 0.1) %>%
+    layer_dense(units = 128, activation = 'relu')
+
+# because we re-use the same instance `base_network`, the weights of
+# the network will be shared across the two branches
+branch_1 <- base_network(input_1)
+branch_2 <- base_network(input_2)
+
+# merging layer
+out <- layer_subtract(list(branch_1, branch_2)) %>%
+    layer_dropout(rate = 0.1) %>%
+    layer_dense(units = 16, activation = 'relu') %>%
+    layer_dense(1, activation = "sigmoid")
+
+# create and compile model
+model <- keras_model(list(input_1, input_2), out)
+
+
+

Train +

+
+model %>% compile(
+    optimizer = "rmsprop",
+    #loss = "binary_crossentropy",
+    loss = contrastive_loss,
+    metrics = metric_binary_accuracy
+)
+
+history <- model %>% fit(
+    list(tr$pair1, tr$pair2), tr$y,
+    batch_size = 128,
+    epochs = 20,
+    validation_data = list(
+        list(te$pair1, te$pair2),
+        te$y
+    )
+)
+
## Epoch 1/20
+## 469/469 - 17s - 36ms/step - binary_accuracy: 0.7566 - loss: 0.1644 - val_binary_accuracy: 0.8753 - val_loss: 0.1001
+## Epoch 2/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.8896 - loss: 0.0867 - val_binary_accuracy: 0.9248 - val_loss: 0.0614
+## Epoch 3/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9261 - loss: 0.0579 - val_binary_accuracy: 0.9409 - val_loss: 0.0461
+## Epoch 4/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9474 - loss: 0.0419 - val_binary_accuracy: 0.9529 - val_loss: 0.0382
+## Epoch 5/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9589 - loss: 0.0328 - val_binary_accuracy: 0.9603 - val_loss: 0.0316
+## Epoch 6/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9660 - loss: 0.0272 - val_binary_accuracy: 0.9617 - val_loss: 0.0297
+## Epoch 7/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9708 - loss: 0.0234 - val_binary_accuracy: 0.9665 - val_loss: 0.0270
+## Epoch 8/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9747 - loss: 0.0201 - val_binary_accuracy: 0.9674 - val_loss: 0.0259
+## Epoch 9/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9787 - loss: 0.0175 - val_binary_accuracy: 0.9697 - val_loss: 0.0247
+## Epoch 10/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9807 - loss: 0.0157 - val_binary_accuracy: 0.9684 - val_loss: 0.0251
+## Epoch 11/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9810 - loss: 0.0152 - val_binary_accuracy: 0.9710 - val_loss: 0.0230
+## Epoch 12/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9831 - loss: 0.0138 - val_binary_accuracy: 0.9726 - val_loss: 0.0224
+## Epoch 13/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9854 - loss: 0.0121 - val_binary_accuracy: 0.9724 - val_loss: 0.0224
+## Epoch 14/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9861 - loss: 0.0114 - val_binary_accuracy: 0.9738 - val_loss: 0.0217
+## Epoch 15/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9864 - loss: 0.0110 - val_binary_accuracy: 0.9738 - val_loss: 0.0213
+## Epoch 16/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9881 - loss: 0.0101 - val_binary_accuracy: 0.9754 - val_loss: 0.0206
+## Epoch 17/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9883 - loss: 0.0097 - val_binary_accuracy: 0.9733 - val_loss: 0.0214
+## Epoch 18/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9881 - loss: 0.0097 - val_binary_accuracy: 0.9723 - val_loss: 0.0216
+## Epoch 19/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9891 - loss: 0.0090 - val_binary_accuracy: 0.9749 - val_loss: 0.0202
+## Epoch 20/20
+## 469/469 - 1s - 1ms/step - binary_accuracy: 0.9898 - loss: 0.0085 - val_binary_accuracy: 0.9752 - val_loss: 0.0205
+
+plot(history)
+
+plot of chunk unnamed-chunk-5
plot of chunk unnamed-chunk-5
+
+
+
+

Evaluate +

+
+# compute final accuracy on training and test sets
+
+tr_pred <- predict(model, list(tr$pair1, tr$pair2))[,1]
+
## 1875/1875 - 1s - 793us/step
+
+tr_acc  <- compute_accuracy(tr_pred, tr$y)
+te_pred <- predict(model, list(te$pair1, te$pair2))[,1]
+
## 313/313 - 0s - 1ms/step
+
+te_acc  <- compute_accuracy(te_pred, te$y)
+
+sprintf('* Accuracy on training set: %0.2f%%', (100 * tr_acc))
+
## [1] "* Accuracy on training set: 99.63%"
+
+sprintf('* Accuracy on test set: %0.2f%%', (100 * te_acc))
+
## [1] "* Accuracy on test set: 97.93%"
+
+
+

Plots +

+
+par(mfrow=c(1,1))
+vioplot::vioplot( te_pred ~ te$y )
+
+plot of chunk unnamed-chunk-7
plot of chunk unnamed-chunk-7
+
+
+i=3
+visualizePair <- function(i) {
+    image(rbind(matrix( te$pair1[i,],28,28)[,28:1], matrix( te$pair2[i,],28,28)[,28:1]))
+    title(paste("true:", te$y[i],"|  pred:", round(te_pred[i],5)))
+}
+par(mfrow=c(3,3))
+lapply(1:9, visualizePair)
+
+plot of chunk unnamed-chunk-7
plot of chunk unnamed-chunk-7
+
+
## [[1]]
+## NULL
+##
+## [[2]]
+## NULL
+##
+## [[3]]
+## NULL
+##
+## [[4]]
+## NULL
+##
+## [[5]]
+## NULL
+##
+## [[6]]
+## NULL
+##
+## [[7]]
+## NULL
+##
+## [[8]]
+## NULL
+##
+## [[9]]
+## NULL
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/vision/mnist_siamese_graph/unnamed-chunk-5-1.png b/docs/dev/articles/examples/vision/mnist_siamese_graph/unnamed-chunk-5-1.png new file mode 100644 index 0000000000..915daf865a Binary files /dev/null and b/docs/dev/articles/examples/vision/mnist_siamese_graph/unnamed-chunk-5-1.png differ diff --git a/docs/dev/articles/examples/vision/mnist_siamese_graph/unnamed-chunk-7-1.png b/docs/dev/articles/examples/vision/mnist_siamese_graph/unnamed-chunk-7-1.png new file mode 100644 index 0000000000..458fcc9764 Binary files /dev/null and b/docs/dev/articles/examples/vision/mnist_siamese_graph/unnamed-chunk-7-1.png differ diff --git a/docs/dev/articles/examples/vision/mnist_siamese_graph/unnamed-chunk-7-2.png b/docs/dev/articles/examples/vision/mnist_siamese_graph/unnamed-chunk-7-2.png new file mode 100644 index 0000000000..3f11e79402 Binary files /dev/null and b/docs/dev/articles/examples/vision/mnist_siamese_graph/unnamed-chunk-7-2.png differ diff --git a/docs/dev/articles/examples/vision/oxford_pets_image_segmentation.html b/docs/dev/articles/examples/vision/oxford_pets_image_segmentation.html new file mode 100644 index 0000000000..261ba61c0a --- /dev/null +++ b/docs/dev/articles/examples/vision/oxford_pets_image_segmentation.html @@ -0,0 +1,742 @@ + + + + + + + + +Image segmentation with a U-Net-like architecture • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Download the data +

+
+options(timeout = 5000)
+download.file(
+  "https://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz",
+  "datasets/images.tar.gz"
+)
+download.file(
+  "https://www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz",
+  "datasets/annotations.tar.gz"
+)
+
+untar("datasets/images.tar.gz", exdir = "datasets")
+untar("datasets/annotations.tar.gz", exdir = "datasets")
+
+
+

Prepare paths of input images and target segmentation masks +

+
+library(keras3)
+input_dir <- "datasets/images/"
+target_dir <- "datasets/annotations/trimaps/"
+img_size <- c(160, 160)
+num_classes <- 3
+batch_size <- 32
+
+input_img_paths <- fs::dir_ls(input_dir, glob = "*.jpg") |> sort()
+target_img_paths <- fs::dir_ls(target_dir, glob = "*.png") |> sort()
+
+cat("Number of samples:", length(input_img_paths), "\n")
+
## Number of samples: 7390
+
+for (i in 1:10) {
+  cat(input_img_paths[i], "|", target_img_paths[i], "\n")
+}
+
## datasets/images/Abyssinian_1.jpg | datasets/annotations/trimaps/Abyssinian_1.png
+## datasets/images/Abyssinian_10.jpg | datasets/annotations/trimaps/Abyssinian_10.png
+## datasets/images/Abyssinian_100.jpg | datasets/annotations/trimaps/Abyssinian_100.png
+## datasets/images/Abyssinian_101.jpg | datasets/annotations/trimaps/Abyssinian_101.png
+## datasets/images/Abyssinian_102.jpg | datasets/annotations/trimaps/Abyssinian_102.png
+## datasets/images/Abyssinian_103.jpg | datasets/annotations/trimaps/Abyssinian_103.png
+## datasets/images/Abyssinian_104.jpg | datasets/annotations/trimaps/Abyssinian_104.png
+## datasets/images/Abyssinian_105.jpg | datasets/annotations/trimaps/Abyssinian_105.png
+## datasets/images/Abyssinian_106.jpg | datasets/annotations/trimaps/Abyssinian_106.png
+## datasets/images/Abyssinian_107.jpg | datasets/annotations/trimaps/Abyssinian_107.png
+
+
+

What does one input image and corresponding segmentation mask look +like? +

+
+# Display input image #10
+input_img_paths[10] |>
+  jpeg::readJPEG() |>
+  as.raster() |>
+  plot()
+
+plot of chunk unnamed-chunk-4
plot of chunk unnamed-chunk-4
+
+
+target_img_paths[10] |>
+  png::readPNG() |>
+  magrittr::multiply_by(255)|>
+  as.raster(max = 3) |>
+  plot()
+
+plot of chunk unnamed-chunk-4
plot of chunk unnamed-chunk-4
+
+
+
+

Prepare dataset to load & vectorize batches of data +

+
+library(tensorflow, exclude = c("shape", "set_random_seed"))
+library(tfdatasets, exclude = "shape")
+
+
+# Returns a tf_dataset
+get_dataset <- function(batch_size, img_size, input_img_paths, target_img_paths,
+                        max_dataset_len = NULL) {
+
+  img_size <- as.integer(img_size)
+
+  load_img_masks <- function(input_img_path, target_img_path) {
+    input_img <- input_img_path |>
+      tf$io$read_file() |>
+      tf$io$decode_jpeg(channels = 3) |>
+      tf$image$resize(img_size) |>
+      tf$image$convert_image_dtype("float32")
+
+    target_img <- target_img_path |>
+      tf$io$read_file() |>
+      tf$io$decode_png(channels = 1) |>
+      tf$image$resize(img_size, method = "nearest") |>
+      tf$image$convert_image_dtype("uint8")
+
+    # Ground truth labels are 1, 2, 3. Subtract one to make them 0, 1, 2:
+    target_img <- target_img - 1L
+
+    list(input_img, target_img)
+  }
+
+  if (!is.null(max_dataset_len)) {
+    input_img_paths <- input_img_paths[1:max_dataset_len]
+    target_img_paths <- target_img_paths[1:max_dataset_len]
+  }
+
+  list(input_img_paths, target_img_paths) |>
+    tensor_slices_dataset() |>
+    dataset_map(load_img_masks, num_parallel_calls = tf$data$AUTOTUNE)|>
+    dataset_batch(batch_size)
+}
+
+
+

Prepare U-Net Xception-style model +

+
+get_model <- function(img_size, num_classes) {
+
+  inputs <- keras_input(shape = c(img_size, 3))
+
+  ### [First half of the network: downsampling inputs] ###
+
+  # Entry block
+  x <- inputs |>
+    layer_conv_2d(filters = 32, kernel_size = 3, strides = 2, padding = "same") |>
+    layer_batch_normalization() |>
+    layer_activation("relu")
+
+  previous_block_activation <- x  # Set aside residual
+
+  for (filters in c(64, 128, 256)) {
+    x <- x |>
+      layer_activation("relu") |>
+      layer_separable_conv_2d(filters = filters, kernel_size = 3, padding = "same") |>
+      layer_batch_normalization() |>
+
+      layer_activation("relu") |>
+      layer_separable_conv_2d(filters = filters, kernel_size = 3, padding = "same") |>
+      layer_batch_normalization() |>
+
+      layer_max_pooling_2d(pool_size = 3, strides = 2, padding = "same")
+
+    residual <- previous_block_activation |>
+      layer_conv_2d(filters = filters, kernel_size = 1, strides = 2, padding = "same")
+
+    x <- layer_add(x, residual)  # Add back residual
+    previous_block_activation <- x  # Set aside next residual
+  }
+
+  ### [Second half of the network: upsampling inputs] ###
+
+  for (filters in c(256, 128, 64, 32)) {
+    x <- x |>
+      layer_activation("relu") |>
+      layer_conv_2d_transpose(filters = filters, kernel_size = 3, padding = "same") |>
+      layer_batch_normalization() |>
+
+      layer_activation("relu") |>
+      layer_conv_2d_transpose(filters = filters, kernel_size = 3, padding = "same") |>
+      layer_batch_normalization() |>
+
+      layer_upsampling_2d(size = 2)
+
+    # Project residual
+    residual <- previous_block_activation |>
+      layer_upsampling_2d(size = 2) |>
+      layer_conv_2d(filters = filters, kernel_size = 1, padding = "same")
+
+    x <- layer_add(x, residual)     # Add back residual
+    previous_block_activation <- x  # Set aside next residual
+  }
+
+  # Add a per-pixel classification layer
+  outputs <- x |>
+    layer_conv_2d(num_classes, 3, activation = "softmax", padding = "same")
+
+  # Define the model
+  keras_model(inputs, outputs)
+}
+
+# Build model
+model <- get_model(img_size, num_classes)
+summary(model)
+
## Model: "functional_1"
+## ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━┓
+## ┃ Layer (type)       Output Shape       Param #  Connected to    Trai… 
+## ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━┩
+## │ input_layer       │ (None, 160,     │         0 │ -              │   -
+## │ (InputLayer)      │ 160, 3)         │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d (Conv2D)   │ (None, 80, 80,  │       896 │ input_layer[0… │   Y
+## │                   │ 32)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 80, 80,  │       128 │ conv2d[0][0]   │   Y
+## │ (BatchNormalizat…32)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation        │ (None, 80, 80,  │         0 │ batch_normali… │   -
+## │ (Activation)      │ 32)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_1      │ (None, 80, 80,  │         0 │ activation[0]… │   -
+## │ (Activation)      │ 32)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ separable_conv2d  │ (None, 80, 80,  │     2,400 │ activation_1[Y
+## │ (SeparableConv2D) │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 80, 80,  │       256 │ separable_con… │   Y
+## │ (BatchNormalizat…64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_2      │ (None, 80, 80,  │         0 │ batch_normali… │   -
+## │ (Activation)      │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ separable_conv2d… │ (None, 80, 80,  │     4,736 │ activation_2[Y
+## │ (SeparableConv2D) │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 80, 80,  │       256 │ separable_con… │   Y
+## │ (BatchNormalizat…64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ max_pooling2d     │ (None, 40, 40,  │         0 │ batch_normali… │   -
+## │ (MaxPooling2D)    │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_1 (Conv2D) │ (None, 40, 40,  │     2,112 │ activation[0]… │   Y
+## │                   │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ add (Add)         │ (None, 40, 40,  │         0 │ max_pooling2d… │   -
+## │                   │ 64)             │           │ conv2d_1[0][0] │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_3      │ (None, 40, 40,  │         0 │ add[0][0]      │   -
+## │ (Activation)      │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ separable_conv2d… │ (None, 40, 40,  │     8,896 │ activation_3[Y
+## │ (SeparableConv2D) │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 40, 40,  │       512 │ separable_con… │   Y
+## │ (BatchNormalizat…128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_4      │ (None, 40, 40,  │         0 │ batch_normali… │   -
+## │ (Activation)      │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ separable_conv2d… │ (None, 40, 40,  │    17,664 │ activation_4[Y
+## │ (SeparableConv2D) │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 40, 40,  │       512 │ separable_con… │   Y
+## │ (BatchNormalizat…128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ max_pooling2d_1   │ (None, 20, 20,  │         0 │ batch_normali… │   -
+## │ (MaxPooling2D)    │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_2 (Conv2D) │ (None, 20, 20,  │     8,320 │ add[0][0]      │   Y
+## │                   │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ add_1 (Add)       │ (None, 20, 20,  │         0 │ max_pooling2d… │   -
+## │                   │ 128)            │           │ conv2d_2[0][0] │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_5      │ (None, 20, 20,  │         0 │ add_1[0][0]    │   -
+## │ (Activation)      │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ separable_conv2d… │ (None, 20, 20,  │    34,176 │ activation_5[Y
+## │ (SeparableConv2D) │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 20, 20,  │     1,024 │ separable_con… │   Y
+## │ (BatchNormalizat…256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_6      │ (None, 20, 20,  │         0 │ batch_normali… │   -
+## │ (Activation)      │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ separable_conv2d… │ (None, 20, 20,  │    68,096 │ activation_6[Y
+## │ (SeparableConv2D) │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 20, 20,  │     1,024 │ separable_con… │   Y
+## │ (BatchNormalizat…256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ max_pooling2d_2   │ (None, 10, 10,  │         0 │ batch_normali… │   -
+## │ (MaxPooling2D)    │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_3 (Conv2D) │ (None, 10, 10,  │    33,024 │ add_1[0][0]    │   Y
+## │                   │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ add_2 (Add)       │ (None, 10, 10,  │         0 │ max_pooling2d… │   -
+## │                   │ 256)            │           │ conv2d_3[0][0] │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_7      │ (None, 10, 10,  │         0 │ add_2[0][0]    │   -
+## │ (Activation)      │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_transpose  │ (None, 10, 10,  │   590,080 │ activation_7[Y
+## │ (Conv2DTranspose) │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 10, 10,  │     1,024 │ conv2d_transp… │   Y
+## │ (BatchNormalizat…256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_8      │ (None, 10, 10,  │         0 │ batch_normali… │   -
+## │ (Activation)      │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_transpose… │ (None, 10, 10,  │   590,080 │ activation_8[Y
+## │ (Conv2DTranspose) │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 10, 10,  │     1,024 │ conv2d_transp… │   Y
+## │ (BatchNormalizat…256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ up_sampling2d_1   │ (None, 20, 20,  │         0 │ add_2[0][0]    │   -
+## │ (UpSampling2D)    │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ up_sampling2d     │ (None, 20, 20,  │         0 │ batch_normali… │   -
+## │ (UpSampling2D)    │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_4 (Conv2D) │ (None, 20, 20,  │    65,792 │ up_sampling2d… │   Y
+## │                   │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ add_3 (Add)       │ (None, 20, 20,  │         0 │ up_sampling2d… │   -
+## │                   │ 256)            │           │ conv2d_4[0][0] │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_9      │ (None, 20, 20,  │         0 │ add_3[0][0]    │   -
+## │ (Activation)      │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_transpose… │ (None, 20, 20,  │   295,040 │ activation_9[Y
+## │ (Conv2DTranspose) │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 20, 20,  │       512 │ conv2d_transp… │   Y
+## │ (BatchNormalizat…128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_10     │ (None, 20, 20,  │         0 │ batch_normali… │   -
+## │ (Activation)      │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_transpose… │ (None, 20, 20,  │   147,584 │ activation_10… │   Y
+## │ (Conv2DTranspose) │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 20, 20,  │       512 │ conv2d_transp… │   Y
+## │ (BatchNormalizat…128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ up_sampling2d_3   │ (None, 40, 40,  │         0 │ add_3[0][0]    │   -
+## │ (UpSampling2D)    │ 256)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ up_sampling2d_2   │ (None, 40, 40,  │         0 │ batch_normali… │   -
+## │ (UpSampling2D)    │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_5 (Conv2D) │ (None, 40, 40,  │    32,896 │ up_sampling2d… │   Y
+## │                   │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ add_4 (Add)       │ (None, 40, 40,  │         0 │ up_sampling2d… │   -
+## │                   │ 128)            │           │ conv2d_5[0][0] │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_11     │ (None, 40, 40,  │         0 │ add_4[0][0]    │   -
+## │ (Activation)      │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_transpose… │ (None, 40, 40,  │    73,792 │ activation_11… │   Y
+## │ (Conv2DTranspose) │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 40, 40,  │       256 │ conv2d_transp… │   Y
+## │ (BatchNormalizat…64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_12     │ (None, 40, 40,  │         0 │ batch_normali… │   -
+## │ (Activation)      │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_transpose… │ (None, 40, 40,  │    36,928 │ activation_12… │   Y
+## │ (Conv2DTranspose) │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 40, 40,  │       256 │ conv2d_transp… │   Y
+## │ (BatchNormalizat…64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ up_sampling2d_5   │ (None, 80, 80,  │         0 │ add_4[0][0]    │   -
+## │ (UpSampling2D)    │ 128)            │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ up_sampling2d_4   │ (None, 80, 80,  │         0 │ batch_normali… │   -
+## │ (UpSampling2D)    │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_6 (Conv2D) │ (None, 80, 80,  │     8,256 │ up_sampling2d… │   Y
+## │                   │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ add_5 (Add)       │ (None, 80, 80,  │         0 │ up_sampling2d… │   -
+## │                   │ 64)             │           │ conv2d_6[0][0] │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_13     │ (None, 80, 80,  │         0 │ add_5[0][0]    │   -
+## │ (Activation)      │ 64)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_transpose… │ (None, 80, 80,  │    18,464 │ activation_13… │   Y
+## │ (Conv2DTranspose) │ 32)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 80, 80,  │       128 │ conv2d_transp… │   Y
+## │ (BatchNormalizat…32)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ activation_14     │ (None, 80, 80,  │         0 │ batch_normali… │   -
+## │ (Activation)      │ 32)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_transpose… │ (None, 80, 80,  │     9,248 │ activation_14… │   Y
+## │ (Conv2DTranspose) │ 32)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ batch_normalizat… │ (None, 80, 80,  │       128 │ conv2d_transp… │   Y
+## │ (BatchNormalizat…32)             │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ up_sampling2d_7   │ (None, 160,     │         0 │ add_5[0][0]    │   -
+## │ (UpSampling2D)    │ 160, 64)        │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ up_sampling2d_6   │ (None, 160,     │         0 │ batch_normali… │   -
+## │ (UpSampling2D)    │ 160, 32)        │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_7 (Conv2D) │ (None, 160,     │     2,080 │ up_sampling2d… │   Y
+## │                   │ 160, 32)        │           │                │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ add_6 (Add)       │ (None, 160,     │         0 │ up_sampling2d… │   -
+## │                   │ 160, 32)        │           │ conv2d_7[0][0] │       │
+## ├───────────────────┼─────────────────┼───────────┼────────────────┼───────┤
+## │ conv2d_8 (Conv2D) │ (None, 160,     │       867 │ add_6[0][0]    │   Y
+## │                   │ 160, 3)         │           │                │       │
+## └───────────────────┴─────────────────┴───────────┴────────────────┴───────┘
+##  Total params: 2,058,979 (7.85 MB)
+##  Trainable params: 2,055,203 (7.84 MB)
+##  Non-trainable params: 3,776 (14.75 KB)
+
+
+

Set aside a validation split +

+
+# Split our img paths into a training and a validation set
+val_samples <- 1000
+val_samples <- sample.int(length(input_img_paths), val_samples)
+
+train_input_img_paths <- input_img_paths[-val_samples]
+train_target_img_paths <- target_img_paths[-val_samples]
+
+val_input_img_paths <- input_img_paths[val_samples]
+val_target_img_paths <- target_img_paths[val_samples]
+
+# Instantiate dataset for each split
+# Limit input files in `max_dataset_len` for faster epoch training time.
+# Remove the `max_dataset_len` arg when running with full dataset.
+train_dataset <- get_dataset(
+  batch_size,
+  img_size,
+  train_input_img_paths,
+  train_target_img_paths,
+  max_dataset_len = 1000
+)
+valid_dataset <- get_dataset(
+  batch_size, img_size, val_input_img_paths, val_target_img_paths
+)
+
+
+

Train the model +

+
+# Configure the model for training.
+# We use the "sparse" version of categorical_crossentropy
+# because our target data is integers.
+model |> compile(
+  optimizer = optimizer_adam(1e-4),
+  loss = "sparse_categorical_crossentropy"
+)
+
+callbacks <- list(
+  callback_model_checkpoint(
+    "models/oxford_segmentation.keras", save_best_only = TRUE
+  )
+)
+
+# Train the model, doing validation at the end of each epoch.
+epochs <- 50
+model |> fit(
+    train_dataset,
+    epochs=epochs,
+    validation_data=valid_dataset,
+    callbacks=callbacks,
+    verbose=2
+)
+
## Epoch 1/50
+## 32/32 - 29s - 914ms/step - loss: 1.4283 - val_loss: 1.5499
+## Epoch 2/50
+## 32/32 - 2s - 60ms/step - loss: 0.9221 - val_loss: 1.9887
+## Epoch 3/50
+## 32/32 - 2s - 60ms/step - loss: 0.7764 - val_loss: 2.5144
+## Epoch 4/50
+## 32/32 - 2s - 61ms/step - loss: 0.7200 - val_loss: 3.0185
+## Epoch 5/50
+## 32/32 - 2s - 61ms/step - loss: 0.6847 - val_loss: 3.2947
+## Epoch 6/50
+## 32/32 - 2s - 68ms/step - loss: 0.6556 - val_loss: 3.4559
+## Epoch 7/50
+## 32/32 - 2s - 76ms/step - loss: 0.6303 - val_loss: 3.5671
+## Epoch 8/50
+## 32/32 - 2s - 62ms/step - loss: 0.6083 - val_loss: 3.6620
+## Epoch 9/50
+## 32/32 - 2s - 63ms/step - loss: 0.5895 - val_loss: 3.7374
+## Epoch 10/50
+## 32/32 - 2s - 62ms/step - loss: 0.5726 - val_loss: 3.8015
+## Epoch 11/50
+## 32/32 - 2s - 63ms/step - loss: 0.5567 - val_loss: 3.8311
+## Epoch 12/50
+## 32/32 - 2s - 63ms/step - loss: 0.5407 - val_loss: 3.8089
+## Epoch 13/50
+## 32/32 - 2s - 62ms/step - loss: 0.5242 - val_loss: 3.7293
+## Epoch 14/50
+## 32/32 - 2s - 61ms/step - loss: 0.5062 - val_loss: 3.5953
+## Epoch 15/50
+## 32/32 - 2s - 61ms/step - loss: 0.4860 - val_loss: 3.4506
+## Epoch 16/50
+## 32/32 - 2s - 60ms/step - loss: 0.4637 - val_loss: 3.2588
+## Epoch 17/50
+## 32/32 - 2s - 61ms/step - loss: 0.4396 - val_loss: 3.0063
+## Epoch 18/50
+## 32/32 - 2s - 61ms/step - loss: 0.4140 - val_loss: 2.7031
+## Epoch 19/50
+## 32/32 - 2s - 61ms/step - loss: 0.3877 - val_loss: 2.3504
+## Epoch 20/50
+## 32/32 - 2s - 61ms/step - loss: 0.3623 - val_loss: 1.9645
+## Epoch 21/50
+## 32/32 - 2s - 62ms/step - loss: 0.3392 - val_loss: 1.6364
+## Epoch 22/50
+## 32/32 - 2s - 66ms/step - loss: 0.3203 - val_loss: 1.3376
+## Epoch 23/50
+## 32/32 - 2s - 67ms/step - loss: 0.3082 - val_loss: 1.0917
+## Epoch 24/50
+## 32/32 - 2s - 66ms/step - loss: 0.3084 - val_loss: 1.0189
+## Epoch 25/50
+## 32/32 - 2s - 66ms/step - loss: 0.3461 - val_loss: 0.9181
+## Epoch 26/50
+## 32/32 - 2s - 61ms/step - loss: 0.3649 - val_loss: 1.0231
+## Epoch 27/50
+## 32/32 - 2s - 66ms/step - loss: 0.3294 - val_loss: 0.8574
+## Epoch 28/50
+## 32/32 - 2s - 61ms/step - loss: 0.2891 - val_loss: 1.0065
+## Epoch 29/50
+## 32/32 - 2s - 61ms/step - loss: 0.2731 - val_loss: 1.1668
+## Epoch 30/50
+## 32/32 - 2s - 61ms/step - loss: 0.2681 - val_loss: 1.1974
+## Epoch 31/50
+## 32/32 - 2s - 61ms/step - loss: 0.2723 - val_loss: 1.1259
+## Epoch 32/50
+## 32/32 - 2s - 61ms/step - loss: 0.2872 - val_loss: 1.1112
+## Epoch 33/50
+## 32/32 - 2s - 61ms/step - loss: 0.3146 - val_loss: 1.2479
+## Epoch 34/50
+## 32/32 - 2s - 61ms/step - loss: 0.3034 - val_loss: 1.1946
+## Epoch 35/50
+## 32/32 - 2s - 61ms/step - loss: 0.2880 - val_loss: 1.0699
+## Epoch 36/50
+## 32/32 - 2s - 61ms/step - loss: 0.2770 - val_loss: 1.0195
+## Epoch 37/50
+## 32/32 - 2s - 61ms/step - loss: 0.2669 - val_loss: 1.2986
+## Epoch 38/50
+## 32/32 - 2s - 61ms/step - loss: 0.2544 - val_loss: 1.0411
+## Epoch 39/50
+## 32/32 - 2s - 61ms/step - loss: 0.2383 - val_loss: 1.3622
+## Epoch 40/50
+## 32/32 - 2s - 61ms/step - loss: 0.2378 - val_loss: 1.3429
+## Epoch 41/50
+## 32/32 - 2s - 61ms/step - loss: 0.2439 - val_loss: 1.2881
+## Epoch 42/50
+## 32/32 - 2s - 61ms/step - loss: 0.2406 - val_loss: 1.5361
+## Epoch 43/50
+## 32/32 - 2s - 61ms/step - loss: 0.2417 - val_loss: 1.2444
+## Epoch 44/50
+## 32/32 - 2s - 61ms/step - loss: 0.2275 - val_loss: 1.1713
+## Epoch 45/50
+## 32/32 - 2s - 61ms/step - loss: 0.2168 - val_loss: 1.0482
+## Epoch 46/50
+## 32/32 - 2s - 62ms/step - loss: 0.2096 - val_loss: 1.1095
+## Epoch 47/50
+## 32/32 - 2s - 61ms/step - loss: 0.2026 - val_loss: 1.1017
+## Epoch 48/50
+## 32/32 - 2s - 61ms/step - loss: 0.1986 - val_loss: 1.1087
+## Epoch 49/50
+## 32/32 - 2s - 61ms/step - loss: 0.1924 - val_loss: 1.1055
+## Epoch 50/50
+## 32/32 - 2s - 61ms/step - loss: 0.1846 - val_loss: 1.0708
+
+
+

Visualize predictions +

+
+model <- load_model("models/oxford_segmentation.keras")
+# Generate predictions for all images in the validation set
+val_dataset <- get_dataset(
+  batch_size, img_size, val_input_img_paths, val_target_img_paths
+)
+val_preds <- predict(model, val_dataset)
+
## 32/32 - 3s - 86ms/step
+
+display_mask <- function(i) {
+  # Quick utility to display a model's prediction.
+  mask <- val_preds[i,,,] %>%
+    apply(c(1,2), which.max) %>%
+    array_reshape(dim = c(img_size, 1))
+  mask <- abind::abind(mask, mask, mask, along = 3)
+  plot(as.raster(mask, max = 3))
+}
+
+# Display results for validation image #10
+i <- 10
+
+par(mfrow = c(1, 3))
+# Display input image
+input_img_paths[i] |>
+  jpeg::readJPEG() |>
+  as.raster() |>
+  plot()
+
+# Display ground-truth target mask
+target_img_paths[i] |>
+  png::readPNG() |>
+  magrittr::multiply_by(255)|>
+  as.raster(max = 3) |>
+  plot()
+
+# Display mask predicted by our model
+display_mask(i)  # Note that the model only sees inputs at 150x150.
+
+plot of chunk unnamed-chunk-9
plot of chunk unnamed-chunk-9
+
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/examples/vision/oxford_pets_image_segmentation/unnamed-chunk-4-1.png b/docs/dev/articles/examples/vision/oxford_pets_image_segmentation/unnamed-chunk-4-1.png new file mode 100644 index 0000000000..75e535ac6b Binary files /dev/null and b/docs/dev/articles/examples/vision/oxford_pets_image_segmentation/unnamed-chunk-4-1.png differ diff --git a/docs/dev/articles/examples/vision/oxford_pets_image_segmentation/unnamed-chunk-4-2.png b/docs/dev/articles/examples/vision/oxford_pets_image_segmentation/unnamed-chunk-4-2.png new file mode 100644 index 0000000000..816d0d588d Binary files /dev/null and b/docs/dev/articles/examples/vision/oxford_pets_image_segmentation/unnamed-chunk-4-2.png differ diff --git a/docs/dev/articles/examples/vision/oxford_pets_image_segmentation/unnamed-chunk-9-1.png b/docs/dev/articles/examples/vision/oxford_pets_image_segmentation/unnamed-chunk-9-1.png new file mode 100644 index 0000000000..1f21754f97 Binary files /dev/null and b/docs/dev/articles/examples/vision/oxford_pets_image_segmentation/unnamed-chunk-9-1.png differ diff --git a/docs/dev/articles/functional_api.html b/docs/dev/articles/functional_api.html new file mode 100644 index 0000000000..1b92be4df7 --- /dev/null +++ b/docs/dev/articles/functional_api.html @@ -0,0 +1,1144 @@ + + + + + + + + +The Functional API • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Setup +

+ +
+
+

Introduction +

+

The Keras functional API is a way to create models that are +more flexible than the sequential API. The functional API can handle +models with non-linear topology, shared layers, and even multiple inputs +or outputs.

+

The main idea is that a deep learning model is usually a directed +acyclic graph (DAG) of layers. So the functional API is a way to build +graphs of layers.

+

Consider the following model:

+
+
(input: 784-dimensional vectors)
+       ↧
+[Dense (64 units, relu activation)]
+       ↧
+[Dense (64 units, relu activation)]
+       ↧
+[Dense (10 units, softmax activation)]
+       ↧
+(output: logits of a probability distribution over 10 classes)
+
+

This is a basic graph with three layers. To build this model using +the functional API, start by creating an input node:

+
+inputs <- keras_input(shape = c(784))
+

The shape of the data is set as a 784-dimensional vector. The batch +size is always omitted since only the shape of each sample is +specified.

+

If, for example, you have an image input with a shape of +(32, 32, 3), you would use:

+
+# Just for demonstration purposes.
+img_inputs <- keras_input(shape = c(32, 32, 3))
+

The inputs that is returned contains information about +the shape and dtype of the input data that you feed to your +model. Here’s the shape:

+
+shape(inputs)
+
## shape(NA, 784)
+

Here’s the dtype:

+
+inputs$dtype
+
## [1] "float32"
+

You create a new node in the graph of layers by calling a layer on +this inputs object:

+
+dense <- layer_dense(units = 64, activation="relu")
+x <- dense(inputs)
+

The “layer call” action is like drawing an arrow from “inputs” to +this layer you created. You’re “passing” the inputs to the +dense layer, and you get x as the output.

+

Let’s add a few more layers to the graph of layers:

+
+outputs <- x |>
+  layer_dense(units = 64, activation = "relu") |>
+  layer_dense(units = 10)
+

At this point, you can create a Model by specifying its +inputs and outputs in the graph of layers:

+
+model <- keras_model(inputs = inputs, outputs = outputs, name = "mnist_model")
+

Let’s check out what the model summary looks like:

+
+summary(model)
+
## Model: "mnist_model"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ input_layer (InputLayer)        │ (None, 784)            │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense (Dense)                   │ (None, 64)             │        50,240
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_1 (Dense)                 │ (None, 64)             │         4,160
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_2 (Dense)                 │ (None, 10)             │           650
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 55,050 (215.04 KB)
+##  Trainable params: 55,050 (215.04 KB)
+##  Non-trainable params: 0 (0.00 B)
+

You can also plot the model as a graph:

+
+plot(model)
+

+

And, optionally, display the input and output shapes of each layer in +the plotted graph:

+
+plot(model, show_shapes = TRUE)
+

+

This figure and the code are almost identical. In the code version, +the connection arrows are replaced by the call operation.

+

A “graph of layers” is an intuitive mental image for a deep learning +model, and the functional API is a way to create models that closely +mirrors this.

+
+
+

Training, evaluation, and inference +

+

Training, evaluation, and inference work exactly in the same way for +models built using the functional API as for Sequential +models.

+

The Model class offers a built-in training loop (the +fit() method) and a built-in evaluation loop (the +evaluate() method). Note that you can easily customize these loops to +implement training routines beyond supervised learning (e.g. GANs).

+ + + + +

Here, load the MNIST image data, reshape it into vectors, fit the +model on the data (while monitoring performance on a validation split), +then evaluate the model on the test data:

+
+c(c(x_train, y_train), c(x_test, y_test)) %<-% dataset_mnist()
+
+x_train <- array_reshape(x_train, c(60000, 784)) / 255
+x_test <- array_reshape(x_test, c(10000, 784)) / 255
+
+model |> compile(
+  loss = loss_sparse_categorical_crossentropy(from_logits = TRUE),
+  optimizer = optimizer_rmsprop(),
+  metrics = "accuracy"
+)
+
+history <- model |> fit(
+    x_train, y_train, batch_size = 64, epochs = 2, validation_split = 0.2
+)
+
## Epoch 1/2
+## 750/750 - 2s - 2ms/step - accuracy: 0.8979 - loss: 0.3540 - val_accuracy: 0.9448 - val_loss: 0.1903
+## Epoch 2/2
+## 750/750 - 1s - 773us/step - accuracy: 0.9511 - loss: 0.1634 - val_accuracy: 0.9605 - val_loss: 0.1386
+
+test_scores <- model |> evaluate(x_test, y_test, verbose=2)
+
## 313/313 - 0s - 980us/step - accuracy: 0.9593 - loss: 0.1323
+
+cat("Test loss:", test_scores$loss, "\n")
+cat("Test accuracy:", test_scores$accuracy, "\n")
+
## Test loss: 0.1323339
+## Test accuracy: 0.9593
+

For further reading, see the training and evaluation +guide.

+
+
+

Save and serialize +

+

Saving the model and serialization work the same way for models built +using the functional API as they do for Sequential models. +The standard way to save a functional model is to call +model.save() to save the entire model as a single file. You +can later recreate the same model from this file, even if the code that +built the model is no longer available.

+

This saved file includes the: - model architecture - model weight +values (that were learned during training) - model training config, if +any (as passed to compile()) - optimizer and its state, if +any (to restart training where you left off)

+
+model |> save_model("my_model.keras")
+rm(model)
+# Recreate the exact same model purely from the file:
+model <- load_model("my_model.keras")
+

For details, read the model serialization & saving +guide.

+
+
+

Use the same graph of layers to define multiple models +

+

In the functional API, models are created by specifying their inputs +and outputs in a graph of layers. That means that a single graph of +layers can be used to generate multiple models.

+

In the example below, you use the same stack of layers to instantiate +two models: an encoder model that turns image inputs into +16-dimensional vectors, and an end-to-end autoencoder model +for training.

+
+encoder_input <- keras_input(shape = c(28, 28, 1), name="img")
+encoder_output <- encoder_input |>
+  layer_conv_2d(16, 3, activation = "relu") |>
+  layer_conv_2d(32, 3, activation = "relu") |>
+  layer_max_pooling_2d(3) |>
+  layer_conv_2d(32, 3, activation = "relu") |>
+  layer_conv_2d(16, 3, activation = "relu") |>
+  layer_global_max_pooling_2d()
+
+encoder <- keras_model(encoder_input, encoder_output, name="encoder")
+summary(encoder)
+
## Model: "encoder"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ img (InputLayer)                │ (None, 28, 28, 1)      │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d (Conv2D)                 │ (None, 26, 26, 16)     │           160
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_1 (Conv2D)               │ (None, 24, 24, 32)     │         4,640
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d (MaxPooling2D)    │ (None, 8, 8, 32)       │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_2 (Conv2D)               │ (None, 6, 6, 32)       │         9,248
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_3 (Conv2D)               │ (None, 4, 4, 16)       │         4,624
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ global_max_pooling2d            │ (None, 16)             │             0
+## │ (GlobalMaxPooling2D)            │                        │               │
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 18,672 (72.94 KB)
+##  Trainable params: 18,672 (72.94 KB)
+##  Non-trainable params: 0 (0.00 B)
+
+decoder_output <- encoder_output |>
+  layer_reshape(c(4, 4, 1)) |>
+  layer_conv_2d_transpose(16, 3, activation = "relu") |>
+  layer_conv_2d_transpose(32, 3, activation = "relu") |>
+  layer_upsampling_2d(3) |>
+  layer_conv_2d_transpose(16, 3, activation = "relu") |>
+  layer_conv_2d_transpose(1, 3, activation = "relu")
+
+autoencoder <- keras_model(encoder_input, decoder_output, name="autoencoder")
+summary(autoencoder)
+
## Model: "autoencoder"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ img (InputLayer)                │ (None, 28, 28, 1)      │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d (Conv2D)                 │ (None, 26, 26, 16)     │           160
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_1 (Conv2D)               │ (None, 24, 24, 32)     │         4,640
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d (MaxPooling2D)    │ (None, 8, 8, 32)       │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_2 (Conv2D)               │ (None, 6, 6, 32)       │         9,248
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_3 (Conv2D)               │ (None, 4, 4, 16)       │         4,624
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ global_max_pooling2d            │ (None, 16)             │             0
+## │ (GlobalMaxPooling2D)            │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ reshape (Reshape)               │ (None, 4, 4, 1)        │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose                │ (None, 6, 6, 16)       │           160
+## │ (Conv2DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose_1              │ (None, 8, 8, 32)       │         4,640
+## │ (Conv2DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ up_sampling2d (UpSampling2D)    │ (None, 24, 24, 32)     │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose_2              │ (None, 26, 26, 16)     │         4,624
+## │ (Conv2DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose_3              │ (None, 28, 28, 1)      │           145
+## │ (Conv2DTranspose)               │                        │               │
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 28,241 (110.32 KB)
+##  Trainable params: 28,241 (110.32 KB)
+##  Non-trainable params: 0 (0.00 B)
+

Here, the decoding architecture is strictly symmetrical to the +encoding architecture, so the output shape is the same as the input +shape (28, 28, 1).

+

The reverse of a conv_2d layer is a +conv_2d_transpose layer, and the reverse of a +max_pooling_2d layer is an upsampling_2d +layer.

+
+
+

All models are callable, just like layers +

+

You can treat any model as if it were a layer by invoking it on an +Input or on the output of another layer. By calling a model +you aren’t just reusing the architecture of the model, you’re also +reusing its weights.

+

To see this in action, here’s a different take on the autoencoder +example that creates an encoder model, a decoder model, and chains them +in two calls to obtain the autoencoder model:

+
+encoder_input <- keras_input(shape = c(28, 28, 1), name="img")
+encoder_output <- encoder_input |>
+  layer_conv_2d(16, 3, activation = "relu") |>
+  layer_conv_2d(32, 3, activation = "relu") |>
+  layer_max_pooling_2d(3) |>
+  layer_conv_2d(32, 3, activation = "relu") |>
+  layer_conv_2d(16, 3, activation = "relu") |>
+  layer_global_max_pooling_2d()
+
+encoder <- keras_model(encoder_input, encoder_output, name="encoder")
+summary(encoder)
+
## Model: "encoder"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ img (InputLayer)                │ (None, 28, 28, 1)      │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_4 (Conv2D)               │ (None, 26, 26, 16)     │           160
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_5 (Conv2D)               │ (None, 24, 24, 32)     │         4,640
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d_1 (MaxPooling2D)  │ (None, 8, 8, 32)       │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_6 (Conv2D)               │ (None, 6, 6, 32)       │         9,248
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_7 (Conv2D)               │ (None, 4, 4, 16)       │         4,624
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ global_max_pooling2d_1          │ (None, 16)             │             0
+## │ (GlobalMaxPooling2D)            │                        │               │
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 18,672 (72.94 KB)
+##  Trainable params: 18,672 (72.94 KB)
+##  Non-trainable params: 0 (0.00 B)
+
+decoder_input <- keras_input(shape = c(16), name = "encoded_img")
+decoder_output <- decoder_input |>
+  layer_reshape(c(4, 4, 1)) |>
+  layer_conv_2d_transpose(16, 3, activation = "relu") |>
+  layer_conv_2d_transpose(32, 3, activation = "relu") |>
+  layer_upsampling_2d(3) |>
+  layer_conv_2d_transpose(16, 3, activation = "relu") |>
+  layer_conv_2d_transpose(1, 3, activation = "relu")
+
+decoder <- keras_model(decoder_input, decoder_output, name = "decoder")
+summary(decoder)
+
## Model: "decoder"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ encoded_img (InputLayer)        │ (None, 16)             │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ reshape_1 (Reshape)             │ (None, 4, 4, 1)        │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose_4              │ (None, 6, 6, 16)       │           160
+## │ (Conv2DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose_5              │ (None, 8, 8, 32)       │         4,640
+## │ (Conv2DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ up_sampling2d_1 (UpSampling2D)  │ (None, 24, 24, 32)     │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose_6              │ (None, 26, 26, 16)     │         4,624
+## │ (Conv2DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose_7              │ (None, 28, 28, 1)      │           145
+## │ (Conv2DTranspose)               │                        │               │
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 9,569 (37.38 KB)
+##  Trainable params: 9,569 (37.38 KB)
+##  Non-trainable params: 0 (0.00 B)
+
+autoencoder_input <- keras_input(shape = c(28, 28, 1), name = "img")
+encoded_img <- encoder(autoencoder_input)
+decoded_img <- decoder(encoded_img)
+autoencoder <- keras_model(autoencoder_input, decoded_img,
+                           name = "autoencoder")
+summary(autoencoder)
+
## Model: "autoencoder"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ img (InputLayer)                │ (None, 28, 28, 1)      │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ encoder (Functional)            │ (None, 16)             │        18,672
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ decoder (Functional)            │ (None, 28, 28, 1)      │         9,569
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 28,241 (110.32 KB)
+##  Trainable params: 28,241 (110.32 KB)
+##  Non-trainable params: 0 (0.00 B)
+

As you can see, the model can be nested: a model can contain +sub-models (since a model is just like a layer). A common use case for +model nesting is ensembling. For example, here’s how to +ensemble a set of models into a single model that averages their +predictions:

+
+get_model <- function() {
+  inputs <- keras_input(shape = 128)
+  outputs <- inputs |> layer_dense(1)
+  keras_model(inputs, outputs)
+}
+
+model1 <- get_model()
+model2 <- get_model()
+model3 <- get_model()
+
+inputs <- keras_input(shape = 128)
+y1 <- model1(inputs)
+y2 <- model2(inputs)
+y3 <- model3(inputs)
+outputs <- layer_average(list(y1, y2, y3))
+ensemble_model <- keras_model(inputs = inputs, outputs = outputs)
+
+
+

Manipulate complex graph topologies +

+
+

Models with multiple inputs and outputs +

+

The functional API makes it easy to manipulate multiple inputs and +outputs. This cannot be handled with the Sequential +API.

+

For example, if you’re building a system for ranking customer issue +tickets by priority and routing them to the correct department, then the +model will have three inputs:

+
    +
  • the title of the ticket (text input),
  • +
  • the text body of the ticket (text input), and
  • +
  • any tags added by the user (categorical input)
  • +
+

This model will have two outputs:

+
    +
  • the priority score between 0 and 1 (scalar sigmoid output), and
  • +
  • the department that should handle the ticket (softmax output over +the set of departments).
  • +
+

You can build this model in a few lines with the functional API:

+
+num_tags <- 12  # Number of unique issue tags
+num_words <- 10000  # Size of vocabulary obtained when preprocessing text data
+num_departments <- 4  # Number of departments for predictions
+
+title_input <- # Variable-length sequence of ints
+  keras_input(shape(NA), name = "title")
+body_input <-  # Variable-length sequence of ints
+  keras_input(shape(NA), name = "body")
+tags_input <-  # Binary vectors of size `num_tags`
+  keras_input(shape = num_tags, name = "tags")
+
+# Embed each word in the title into a 64-dimensional vector
+title_features <- layer_embedding(title_input, num_words, 64)
+# Embed each word in the text into a 64-dimensional vector
+body_features <- layer_embedding(body_input, num_words, 64)
+
+# Reduce sequence of embedded words in the title
+# into a single 128-dimensional vector
+title_features <- layer_lstm(title_features, 128)
+# Reduce sequence of embedded words in the body
+# into a single 32-dimensional vector
+body_features <- layer_lstm(body_features, 32)
+
+# Merge all available features into a single large vector via concatenation
+x <- layer_concatenate(title_features, body_features, tags_input)
+
+# Stick a logistic regression for priority prediction on top of the features
+priority_pred <- layer_dense(x, 1, name = "priority")
+
+# Stick a department classifier on top of the features
+department_pred <- layer_dense(x, num_departments, name = "department")
+
+# Instantiate an end-to-end model predicting both priority and department
+model <- keras_model(
+  inputs = list(title_input, body_input, tags_input),
+  outputs = list(priority = priority_pred, department = department_pred)
+)
+

Now plot the model:

+
+plot(model, show_shapes = TRUE)
+

+

When compiling this model, you can assign different losses to each +output. You can even assign different weights to each loss – to modulate +their contribution to the total training loss.

+
+model |> compile(
+  optimizer = optimizer_rmsprop(1e-3),
+  loss = list(
+    loss_binary_crossentropy(from_logits = TRUE),
+    loss_categorical_crossentropy(from_logits = TRUE)
+  ),
+  loss_weights = c(1.0, 0.2)
+)
+

Since the output layers have different names, you could also specify +the losses and loss weights with the corresponding layer names:

+
+model |> compile(
+  optimizer = optimizer_rmsprop(1e-3),
+  loss = list(
+    priority = loss_binary_crossentropy(from_logits = TRUE),
+    department = loss_categorical_crossentropy(from_logits = TRUE)
+  ),
+  loss_weights = list(priority = 1.0, department = 0.2)
+)
+

Train the model by passing lists of NumPy arrays of inputs and +targets:

+
+# Dummy input data
+title_data <- random_integer(c(1280, 10), 0, num_words)
+body_data <- random_integer(c(1280, 100), 0, num_words)
+tags_data <- random_integer(c(1280, num_tags), 0, 2)
+
+# Dummy target data
+priority_targets <- random_normal(c(1280, 1))
+dept_targets <- random_integer(c(1280, num_departments), 0, 2)
+
+model |> fit(
+  list(title = title_data, body = body_data, tags = tags_data),
+  list(priority = priority_targets, department = dept_targets),
+  epochs = 2,
+  batch_size = 32
+)
+
## Epoch 1/2
+## 40/40 - 2s - 57ms/step - loss: 0.3948
+## Epoch 2/2
+## 40/40 - 0s - 5ms/step - loss: 0.1971
+

When calling fit with a Dataset object, it should yield +either a list of lists like +list(list(title_data, body_data, tags_data), list(priority_targets, dept_targets)) +or a list of named lists like +list(list(title = title_data, body = body_data, tags = tags_data), list(priority = priority_targets, department = dept_targets)).

+

For more detailed explanation, refer to the training and evaluation +guide.

+
+
+

A toy ResNet model +

+

In addition to models with multiple inputs and outputs, the +functional API makes it easy to manipulate non-linear connectivity +topologies – these are models with layers that are not connected +sequentially, which the Sequential API cannot handle.

+

A common use case for this is residual connections. Let’s build a toy +ResNet model for CIFAR10 to demonstrate this:

+
+inputs <- keras_input(shape = c(32, 32, 3), name = "img")
+block_1_output <- inputs |>
+  layer_conv_2d(32, kernel_size = 3, activation = "relu") |>
+  layer_conv_2d(64, kernel_size = 3, activation = "relu") |>
+  layer_max_pooling_2d(pool_size = 3)
+
+block_2_output <- block_1_output |>
+  layer_conv_2d(32, kernel_size = 3, activation = "relu", padding = "same") |>
+  layer_conv_2d(64, kernel_size = 3, activation = "relu", padding = "same") |>
+  layer_add(block_1_output)
+
+block_3_output <- block_2_output |>
+  layer_conv_2d(64, kernel_size = 3, activation = "relu", padding = "same") |>
+  layer_conv_2d(64, kernel_size = 3, activation = "relu", padding = "same") |>
+  layer_add(block_2_output)
+
+outputs <- block_3_output |>
+  layer_conv_2d(64, 3, activation = "relu") |>
+  layer_global_average_pooling_2d() |>
+  layer_dense(256, activation = "relu") |>
+  layer_dropout(0.5) |>
+  layer_dense(10)
+
+model <- keras_model(inputs, outputs, name = "toy_resnet")
+summary(model)
+
## Model: "toy_resnet"
+## ┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)         Output Shape          Param #  Connected to      
+## ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
+## │ img (InputLayer)    │ (None, 32, 32, 3) │          0 │ -                 │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ conv2d_8 (Conv2D)   │ (None, 30, 30,    │        896 │ img[0][0]         │
+## │                     │ 32)               │            │                   │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ conv2d_9 (Conv2D)   │ (None, 28, 28,    │     18,496 │ conv2d_8[0][0]    │
+## │                     │ 64)               │            │                   │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ max_pooling2d_2     │ (None, 9, 9, 64)  │          0 │ conv2d_9[0][0]    │
+## │ (MaxPooling2D)      │                   │            │                   │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ conv2d_10 (Conv2D)  │ (None, 9, 9, 32)  │     18,464 │ max_pooling2d_2[
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ conv2d_11 (Conv2D)  │ (None, 9, 9, 64)  │     18,496 │ conv2d_10[0][0]   │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ add (Add)           │ (None, 9, 9, 64)  │          0 │ conv2d_11[0][0],  │
+## │                     │                   │            │ max_pooling2d_2[
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ conv2d_12 (Conv2D)  │ (None, 9, 9, 64)  │     36,928 │ add[0][0]         │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ conv2d_13 (Conv2D)  │ (None, 9, 9, 64)  │     36,928 │ conv2d_12[0][0]   │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ add_1 (Add)         │ (None, 9, 9, 64)  │          0 │ conv2d_13[0][0],  │
+## │                     │                   │            │ add[0][0]         │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ conv2d_14 (Conv2D)  │ (None, 7, 7, 64)  │     36,928 │ add_1[0][0]       │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ global_average_poo… │ (None, 64)        │          0 │ conv2d_14[0][0]   │
+## │ (GlobalAveragePool… │                   │            │                   │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ dense_6 (Dense)     │ (None, 256)       │     16,640 │ global_average_p… │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ dropout (Dropout)   │ (None, 256)       │          0 │ dense_6[0][0]     │
+## ├─────────────────────┼───────────────────┼────────────┼───────────────────┤
+## │ dense_7 (Dense)     │ (None, 10)        │      2,570 │ dropout[0][0]     │
+## └─────────────────────┴───────────────────┴────────────┴───────────────────┘
+##  Total params: 186,346 (727.91 KB)
+##  Trainable params: 186,346 (727.91 KB)
+##  Non-trainable params: 0 (0.00 B)
+

Plot the model:

+
+plot(model, show_shapes = TRUE)
+

+

Now train the model:

+
+c(c(x_train, y_train), c(x_test, y_test)) %<-% dataset_cifar10()
+
+x_train <- x_train / 255.0
+x_test <- x_test / 255.0
+
+model |> compile(
+  optimizer = optimizer_rmsprop(1e-3),
+  loss = loss_sparse_categorical_crossentropy(from_logits = TRUE),
+  metrics = "acc"
+)
+# We restrict the data to the first 1000 samples so as to limit the
+# guide render time.
+# Try to train on the entire dataset until convergence!
+model |> fit(
+  x_train[1:1000, , , ],
+  y_train[1:1000, ],
+  batch_size = 64,
+  epochs = 1,
+  validation_split = 0.2
+)
+
## 13/13 - 5s - 373ms/step - acc: 0.1238 - loss: 2.2995 - val_acc: 0.1300 - val_loss: 2.2957
+
+
+
+

Shared layers +

+

Another good use for the functional API are models that use +shared layers. Shared layers are layer instances that are +reused multiple times in the same model – they learn features that +correspond to multiple paths in the graph-of-layers.

+

Shared layers are often used to encode inputs from similar spaces +(say, two different pieces of text that feature similar vocabulary). +They enable sharing of information across these different inputs, and +they make it possible to train such a model on less data. If a given +word is seen in one of the inputs, that will benefit the processing of +all inputs that pass through the shared layer.

+

To share a layer in the functional API, call the same layer instance +multiple times. For instance, here’s an Embedding layer +shared across two different text inputs:

+
+# Embedding for 1000 unique words mapped to 128-dimensional vectors
+shared_embedding <- layer_embedding(input_dim = 1000, output_dim = 128)
+
+# Variable-length sequence of integers
+text_input_a <- keras_input(shape = shape(NA), dtype="int32")
+
+# Variable-length sequence of integers
+text_input_b <- keras_input(shape = shape(NA), dtype="int32")
+
+# Reuse the same layer to encode both inputs
+encoded_input_a <- shared_embedding(text_input_a)
+encoded_input_b <- shared_embedding(text_input_b)
+
+
+

Extract and reuse nodes in the graph of layers +

+

Because the graph of layers you are manipulating is a static data +structure, it can be accessed and inspected. And this is how you are +able to plot functional models as images.

+

This also means that you can access the activations of intermediate +layers (“nodes” in the graph) and reuse them elsewhere – which is very +useful for something like feature extraction.

+

Let’s look at an example. This is a VGG19 model with weights +pretrained on ImageNet:

+
+vgg19 <- application_vgg19()
+

And these are the intermediate activations of the model, obtained by +querying the graph data structure:

+
+features_list <- lapply(vgg19$layers, function(x) x$output)
+

Use these features to create a new feature-extraction model that +returns the values of the intermediate layer activations:

+
+feat_extraction_model <- keras_model(inputs = vgg19$input,
+                                     outputs = features_list)
+
+img <- random_normal(c(1, 224, 224, 3))
+extracted_features <- feat_extraction_model(img)
+

This comes in handy for tasks like neural +style transfer, among other things.

+
+
+

Extend the API using custom layers +

+

keras includes a wide range of built-in layers, for +example:

+
    +
  • Convolutional layers: conv_1d, conv_2d, +conv_3d, conv_2d_transpose +
  • +
  • Pooling layers: max_pooling_1d, +max_pooling_2d, max_pooling_3d, +average_pooling_3d +
  • +
  • RNN layers: gru, lstm, +conv_lstm_2d +
  • +
  • +batch_normalization, dropout, +embedding, etc.
  • +
+

But if you don’t find what you need, it’s easy to extend the API by +creating your own layers. All layers subclass the Layer +class and implement:

+
    +
  • +call method, that specifies the computation done by the +layer.
  • +
  • +build method, that creates the weights of the layer +(this is just a style convention since you can create weights in +initialize, as well).
  • +
+

To learn more about creating layers from scratch, read custom layers +and models guide.

+

The following is a basic implementation of +layer_dense():

+
+custom_dense <- Layer(
+  classname = "CustomDense",
+  initialize = function(units = 32) {
+    super$initialize()
+    self$units <- as.integer(units)
+  },
+  build = function(input_shape) {
+    self$w <- self$add_weight(
+      shape = shape(input_shape[[2]], self$units),
+      initializer = "random_normal",
+      trainable = TRUE,
+    )
+    self$b <- self$add_weight(
+      shape = shape(self$units),
+      initializer="random_normal",
+      trainable = TRUE
+    )
+  },
+  call = function(inputs) {
+    op_matmul(inputs, self$w) + self$b
+  }
+)
+
+inputs <- keras_input(c(4))
+outputs <- custom_dense(inputs, 10)
+
+model <- keras_model(inputs, outputs)
+

For serialization support in your custom layer, define a +get_config() method that returns the constructor arguments +of the layer instance:

+
+custom_dense <- Layer(
+  classname = "CustomDense",
+
+  initialize = function(units = 32, ...) {
+    super$initialize()
+    self$units <- as.integer(units)
+  },
+
+  build = function(input_shape) {
+    self$w <- self$add_weight(
+      shape = shape(input_shape[[2]], self$units),
+      initializer = "random_normal",
+      trainable = TRUE,
+    )
+    self$b <- self$add_weight(
+      shape = shape(self$units),
+      initializer="random_normal",
+      trainable = TRUE
+    )
+  },
+
+  call = function(inputs) {
+    op_matmul(inputs, self$w) + self$b
+  },
+
+  get_config = function() {
+    list(units = self$units)
+  }
+)
+
+inputs <- keras_input(c(4))
+outputs <- custom_dense(inputs, 10)
+
+model <- keras_model(inputs, outputs)
+config <- get_config(model)
+
+new_model <-
+  from_config(config, custom_objects = list(CustomDense = custom_dense))
+

Optionally, implement the class method +from_config(cls, config) which is used when recreating a +layer instance given its config dictionary. The default implementation +of from_config is:

+
+from_config <- function(cls, config) {
+  do.call(cls, config)
+}
+
+
+

When to use the functional API +

+

Should you use the Keras functional API to create a new model, or +just subclass the Model class directly? In general, the +functional API is higher-level, easier and safer, and has a number of +features that subclassed models do not support.

+

However, model subclassing provides greater flexibility when building +models that are not easily expressible as directed acyclic graphs of +layers. For example, you could not implement a Tree-RNN with the +functional API and would have to subclass Model +directly.

+

For an in-depth look at the differences between the functional API +and model subclassing, read What +are Symbolic and Imperative APIs in TensorFlow 2.0?.

+
+

Functional API strengths: +

+

The following properties are also true for Sequential models (which +are also data structures), but are not true for subclassed models (which +are R and Python (byte)code, not data structures).

+
+

Less verbose +

+

There is no super$initialize(...), no +call = function(...), no self$..., etc.

+

Compare:

+
+inputs <- keras_input(shape = shape(32))
+outputs <- inputs |>
+  layer_dense(64, activation = "relu") |>
+  layer_dense(10)
+mlp <- keras_model(inputs, outputs)
+

With the subclassed version:

+
+MLP <- Model(
+  classname = "MLP",
+  initialize = function(...) {
+    super$initialize(...)
+    self$dense_1 <- layer_dense(units = 64, activation = "relu")
+    self$dense_2 <- layer_dense(units = 10)
+  },
+  call = function(inputs) {
+    inputs |>
+      self$dense_1() |>
+      self$dense_2()
+  }
+)
+
+# Instantiate the model.
+mlp <- MLP()
+# Necessary to create the model's state.
+# The model doesn't have a state until it's called at least once.
+out <- mlp(op_zeros(c(1, 32)))
+
+
+

Model validation while defining its connectivity graph +

+

In the functional API, the input specification (shape and dtype) is +created in advance (using Input). Every time you call a +layer, the layer checks that the specification passed to it matches its +assumptions, and it will raise a helpful error message if not.

+

This guarantees that any model you can build with the functional API +will run. All debugging – other than convergence-related debugging – +happens statically during the model construction and not at execution +time. This is similar to type checking in a compiler.

+
+
+

A functional model is plottable and inspectable +

+

You can plot the model as a graph, and you can easily access +intermediate nodes in this graph. For example, to extract and reuse the +activations of intermediate layers (as seen in a previous example):

+
+features_list <- lapply(vgg19$layers, function(x) x$output)
+feat_extraction_model <- keras_model(inputs = vgg19$input,
+                                     outputs = features_list)
+
+
+

A functional model can be serialized or cloned +

+

Because a functional model is a data structure rather than a piece of +code, it is safely serializable and can be saved as a single file that +allows you to recreate the exact same model without having access to any +of the original code. See the serialization & saving +guide.

+

To serialize a subclassed model, it is necessary for the implementer +to specify a get_config() and from_config() +method at the model level.

+
+
+
+

Functional API weakness: +

+
+

It does not support dynamic architectures +

+

The functional API treats models as DAGs of layers. This is true for +most deep learning architectures, but not all – for example, recursive +networks or Tree RNNs do not follow this assumption and cannot be +implemented in the functional API.

+
+
+
+
+

Mix-and-match API styles +

+

Choosing between the functional API or Model subclassing isn’t a +binary decision that restricts you into one category of models. All +models in the keras API can interact with each other, +whether they’re Sequential models, functional models, or +subclassed models that are written from scratch.

+

You can always use a functional model or Sequential +model as part of a subclassed model or layer:

+
+units <- 32
+timesteps <- 10
+input_dim <- 5
+
+# Define a Functional model
+inputs <- keras_input(shape(NA, units))
+outputs <- inputs |>
+  layer_global_average_pooling_1d() |>
+  layer_dense(units = 1)
+
+model <- keras_model(inputs, outputs)
+
+layer_custom_rnn <- Layer(
+  classname = "CustomRNN",
+  initialize = function(...) {
+    super$initialize(...)
+    self$units <- units
+    self$projection_1 <- layer_dense(units = units, activation = "tanh")
+    self$projection_2 <- layer_dense(units = units, activation = "tanh")
+    self$classifier <- model
+  },
+  call = function(inputs, ...) {
+    outputs <- list()
+    state <- op_zeros(c(shape(inputs)[[1]], self$units))
+    for (t in 1:(shape(inputs)[[2]])) {
+      x <- inputs[, t, ]
+      h <- self$projection_1(x)
+      y <- h + self$projection_2(state)
+      state <- y
+      outputs[[t]] <- y
+    }
+    features <- op_stack(outputs, axis = 2)
+    self$classifier(features)
+  }
+)
+
+rnn <- layer_custom_rnn()
+out <- rnn(op_zeros(c(1, timesteps, input_dim)))
+

You can use any subclassed layer or model in the functional API as +long as it implements a call method that follows one of the +following patterns:

+
    +
  • +call(inputs, ...) – Where inputs is a +tensor or a nested structure of tensors (e.g. a list of tensors), and +where ... are non-tensor arguments (non-inputs).
  • +
  • +call(inputs, training = NULL, ...) – Where +training is a boolean indicating whether the layer should +behave in training mode and inference mode.
  • +
  • +call(inputs, mask = NULL, ...) – Where +mask is a boolean mask tensor (useful for RNNs, for +instance).
  • +
  • +call(inputs, training = NULL, mask = NULL, ...) – Of +course, you can have both masking and training-specific behavior at the +same time.
  • +
+

Additionally, if you implement the get_config() method +on your custom Layer or model, the functional models you create will +still be serializable and cloneable.

+

Here’s a quick example of a custom RNN, written from scratch, being +used in a functional model:

+
+units <- 32
+timesteps <- 10
+input_dim <- 5
+batch_size <- 16
+
+layer_custom_rnn <- Layer(
+  "custom_rnn",
+  initialize = function(...) {
+    super$initialize(...)
+    self$units <- units
+    self$projection_1 <- layer_dense(units = units, activation = "tanh")
+    self$projection_2 <- layer_dense(units = units, activation = "tanh")
+    self$classifier <- layer_dense(units = 1)
+  },
+  call = function(inputs, ...) {
+    outputs <- list()
+    state <- op_zeros(c(shape(inputs)[[1]], self$units))
+    for (t in 1:(shape(inputs)[[2]])) {
+      x <- inputs[, t, ]
+      h <- self$projection_1(x)
+      y <- h + self$projection_2(state)
+      state <- y
+      outputs[[t]] <- y
+    }
+    features <- op_stack(outputs, axis = 2)
+    self$classifier(features)
+  }
+)
+
+# Note that you specify a static batch size for the inputs with the `batch_shape`
+# arg, because the inner computation of `layer_custom_rnn()` requires a static batch size
+# (when you create the `state` zeros tensor).
+inputs <- keras_input(batch_shape = shape(batch_size, timesteps, input_dim))
+outputs <- inputs |>
+  layer_conv_1d(filters = 32, kernel_size = 3) |>
+  layer_custom_rnn()
+
+model <- keras_model(inputs, outputs)
+out <- model(op_zeros(c(1, 10, 5)))
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/functional_api/unnamed-chunk-10-1.png b/docs/dev/articles/functional_api/unnamed-chunk-10-1.png new file mode 100644 index 0000000000..94a54466e9 Binary files /dev/null and b/docs/dev/articles/functional_api/unnamed-chunk-10-1.png differ diff --git a/docs/dev/articles/functional_api/unnamed-chunk-11-1.png b/docs/dev/articles/functional_api/unnamed-chunk-11-1.png new file mode 100644 index 0000000000..2e9ecf2cfe Binary files /dev/null and b/docs/dev/articles/functional_api/unnamed-chunk-11-1.png differ diff --git a/docs/dev/articles/functional_api/unnamed-chunk-20-1.png b/docs/dev/articles/functional_api/unnamed-chunk-20-1.png new file mode 100644 index 0000000000..9b1a6efeb0 Binary files /dev/null and b/docs/dev/articles/functional_api/unnamed-chunk-20-1.png differ diff --git a/docs/dev/articles/functional_api/unnamed-chunk-25-1.png b/docs/dev/articles/functional_api/unnamed-chunk-25-1.png new file mode 100644 index 0000000000..2b7ae2386d Binary files /dev/null and b/docs/dev/articles/functional_api/unnamed-chunk-25-1.png differ diff --git a/docs/dev/articles/getting_started.html b/docs/dev/articles/getting_started.html new file mode 100644 index 0000000000..89005a1c0f --- /dev/null +++ b/docs/dev/articles/getting_started.html @@ -0,0 +1,366 @@ + + + + + + + + +Getting Started with Keras • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Overview +

+

Keras is a high-level neural +networks API developed with a focus on enabling fast experimentation. +Being able to go from idea to result with the least possible delay +is key to doing good research. Keras has the following key +features:

+
    +
  • Allows the same code to run on CPU or on GPU, +seamlessly.

  • +
  • User-friendly API which makes it easy to quickly prototype deep +learning models.

  • +
  • Built-in support for convolutional networks (for computer +vision), recurrent networks (for sequence processing), and any +combination of both.

  • +
  • Supports arbitrary network architectures: multi-input or +multi-output models, layer sharing, model sharing, etc. This means that +Keras is appropriate for building essentially any deep learning model, +from a memory network to a neural Turing machine.

  • +
+

This website provides documentation for the R interface to Keras. See +the main Keras website at https://keras.io for additional information on the +project.

+
+
+

Installation +

+

First, install the keras R package:

+
+install.packages("keras3")
+

or install the development version with:

+
+remotes::install_github("rstudio/keras")
+

The Keras R interface requires that a backend engine be installed. +This is TensorFlow by +default.

+
+keras3::install_keras(backend = "tensorflow")
+

This will provide you with default installation of Keras and +TensorFlow that is GPU capable, if a GPU is available. If you want a +more customized installation, e.g. see the documentation for +install_keras() and the installation +section.

+
+
+

MNIST Example +

+

We can learn the basics of Keras by walking through a simple example: +recognizing handwritten digits from the MNIST dataset. +MNIST consists of 28 x 28 grayscale images of handwritten digits like +these:

+

+

The dataset also includes labels for each image, telling us which +digit it is. For example, the labels for the above images are 5, 0, 4, +and 1.

+
+

Preparing the Data +

+

The MNIST dataset is included with Keras and can be accessed using +the dataset_mnist() function. Here we load the dataset then +create variables for our test and training data:

+
+library(keras3)
+mnist <- dataset_mnist()
+x_train <- mnist$train$x
+y_train <- mnist$train$y
+x_test <- mnist$test$x
+y_test <- mnist$test$y
+

The x data is a 3-d array +(images, width, height) of grayscale values. To prepare the +data for training we convert the 3-d arrays into matrices by reshaping +width and height into a single dimension (28x28 images are flattened +into length 784 vectors). Then, we convert the grayscale values from +integers ranging between 0 to 255 into floating point values ranging +between 0 and 1:

+
+# reshape
+x_train <- array_reshape(x_train, c(nrow(x_train), 784))
+x_test <- array_reshape(x_test, c(nrow(x_test), 784))
+# rescale
+x_train <- x_train / 255
+x_test <- x_test / 255
+

Note that we use the array_reshape() function rather +than the dim<-() function to reshape the array. This is +so that the data is re-interpreted using row-major semantics (as opposed +to R’s default column-major semantics), which is in turn compatible with +the way that the numerical libraries called by Keras interpret array +dimensions.

+

The y data is an integer vector with values ranging from +0 to 9. To prepare this data for training we one-hot encode the vectors +into binary class matrices using the Keras to_categorical() +function:

+
+y_train <- to_categorical(y_train, 10)
+y_test <- to_categorical(y_test, 10)
+
+
+

Defining the Model +

+

The core data structure of Keras is a model, a way to organize +layers. The simplest type of model is the Sequential +model, a linear stack of layers.

+

We begin by creating a sequential model and then adding layers using +the pipe (|>) operator:

+
+model <- keras_model_sequential(input_shape = c(784))
+model |>
+  layer_dense(units = 256, activation = 'relu') |>
+  layer_dropout(rate = 0.4) |>
+  layer_dense(units = 128, activation = 'relu') |>
+  layer_dropout(rate = 0.3) |>
+  layer_dense(units = 10, activation = 'softmax')
+

The input_shape argument to the first layer specifies +the shape of the input data (a length 784 numeric vector representing a +grayscale image). The final layer outputs a length 10 numeric vector +(probabilities for each digit) using a softmax activation +function.

+

Use the summary() function to print the details of the +model:

+
+summary(model)
+
## Model: "sequential"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ dense (Dense)                   │ (None, 256)            │       200,960
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dropout (Dropout)               │ (None, 256)            │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_1 (Dense)                 │ (None, 128)            │        32,896
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dropout_1 (Dropout)             │ (None, 128)            │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_2 (Dense)                 │ (None, 10)             │         1,290
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 235,146 (918.54 KB)
+##  Trainable params: 235,146 (918.54 KB)
+##  Non-trainable params: 0 (0.00 B)
+
+plot(model)
+

+Next, compile the model with appropriate loss function, optimizer, and +metrics:

+
+model |> compile(
+  loss = 'categorical_crossentropy',
+  optimizer = optimizer_rmsprop(),
+  metrics = c('accuracy')
+)
+
+
+

Training and Evaluation +

+

Use the fit() function to train the model for 30 epochs +using batches of 128 images:

+
+history <- model |> fit(
+  x_train, y_train,
+  epochs = 30, batch_size = 128,
+  validation_split = 0.2
+)
+

The history object returned by fit() +includes loss and accuracy metrics which we can plot:

+
+plot(history)
+
+plot of chunk unnamed-chunk-12
plot of chunk unnamed-chunk-12
+
+

Evaluate the model’s performance on the test data:

+
+model |> evaluate(x_test, y_test)
+
## 313/313 - 1s - 2ms/step - accuracy: 0.9818 - loss: 0.0794
+
## $accuracy
+## [1] 0.9818
+##
+## $loss
+## [1] 0.07941415
+

Generate predictions on new data:

+
+probs <- model |> predict(x_test)
+
## 313/313 - 0s - 1ms/step
+
+max.col(probs) - 1L
+
##   [1] 7 2 1 0 4 1 4 9 5 9 0 6 9 0 1 5 9 7 3 4 9 6 6 5 4 0 7 4 0 1 3 1 3 4 7
+##  [36] 2 7 1 2 1 1 7 4 2 3 5 1 2 4 4 6 3 5 5 6 0 4 1 9 5 7 8 9 3 7 4 6 4 3 0
+##  [71] 7 0 2 9 1 7 3 2 9 7 7 6 2 7 8 4 7 3 6 1 3 6 9 3 1 4 1 7 6 9
+##  [ reached getOption("max.print") -- omitted 9900 entries ]
+

Keras provides a vocabulary for building deep learning models that is +simple, elegant, and intuitive. Building a question answering system, an +image classification model, a neural Turing machine, or any other model +is just as straightforward.

+
+
+

Deep Learning with R Book +

+

If you want a more comprehensive introduction to both Keras and the +concepts and practice of deep learning, we recommend the Deep +Learning with R, 2nd Edition book from Manning. This book is a +collaboration between François Chollet, the creator of (Python) Keras, +J.J. Allaire, who wrote the original R interface to Keras, and Tomasz +Kalinowski, the maintainer of the R interface to Keras.

+

The book presumes no significant knowledge of machine learning and +deep learning, and goes all the way from basic theory to advanced +practical applications, all using the R interface to Keras.

+
+ +
+
+
+
+

Why this name, Keras? +

+

Keras (κέρας) means horn in Greek. It is a reference to a literary +image from ancient Greek and Latin literature, first found in the +Odyssey, where dream spirits (Oneiroi, singular Oneiros) are divided +between those who deceive men with false visions, who arrive to Earth +through a gate of ivory, and those who announce a future that will come +to pass, who arrive through a gate of horn. It’s a play on the words +κέρας (horn) / κραίνω (fulfill), and ἐλέφας (ivory) / ἐλεφαίρομαι +(deceive).

+

Keras was initially developed as part of the research effort of +project ONEIROS (Open-ended Neuro-Electronic Intelligent Robot Operating +System).

+
+

“Oneiroi are beyond our unravelling –who can be sure what tale they +tell? Not all that men look for comes to pass. Two gates there are that +give passage to fleeting Oneiroi; one is made of horn, one of ivory. The +Oneiroi that pass through sawn ivory are deceitful, bearing a message +that will not be fulfilled; those that come out through polished horn +have truth behind them, to be accomplished for men who see them.” Homer, +Odyssey 19. 562 ff (Shewring translation).

+
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/getting_started/unnamed-chunk-12-1.png b/docs/dev/articles/getting_started/unnamed-chunk-12-1.png new file mode 100644 index 0000000000..0722d51e5b Binary files /dev/null and b/docs/dev/articles/getting_started/unnamed-chunk-12-1.png differ diff --git a/docs/dev/articles/getting_started/unnamed-chunk-9-1.png b/docs/dev/articles/getting_started/unnamed-chunk-9-1.png new file mode 100644 index 0000000000..d31eac9bcf Binary files /dev/null and b/docs/dev/articles/getting_started/unnamed-chunk-9-1.png differ diff --git a/docs/dev/articles/images/MNIST.png b/docs/dev/articles/images/MNIST.png new file mode 100644 index 0000000000..0a558b744c Binary files /dev/null and b/docs/dev/articles/images/MNIST.png differ diff --git a/docs/dev/articles/index.html b/docs/dev/articles/index.html new file mode 100644 index 0000000000..b45a2685e6 --- /dev/null +++ b/docs/dev/articles/index.html @@ -0,0 +1,146 @@ + +Articles • keras3 + Skip to contents + + +
+
+
+ +
+

All vignettes

+
+ +
Convolutional autoencoder for image denoising
+

How to train a deep convolutional autoencoder for image denoising.

+
Customizing what happens in `fit()` with TensorFlow
+

Overriding the training step of the Model class with TensorFlow.

+
Multi-GPU distributed training with TensorFlow
+

Guide to multi-GPU training for Keras models with TensorFlow.

+
Distributed training with Keras 3
+

Complete guide to the distribution API for multi-backend Keras.

+
The Functional API
+

Complete guide to the functional API.

+
Getting Started with Keras
+
+
Imbalanced classification: credit card fraud detection
+

Demonstration of how to handle highly imbalanced classification problems.

+
Keras examples
+
+
Introduction to Keras for engineers
+

First contact with Keras 3.

+
Introduction to Keras for Researchers
+

Everything you need to know to use Keras & TensorFlow for deep learning research.

+
Making new layers and models via subclassing
+

Complete guide to writing Layer and Model objects from scratch.

+
Simple MNIST convnet
+

A simple convnet that achieves ~99% test accuracy on MNIST.

+
Train a Siamese MLP on pairs of digits from the MNIST dataset.
+
+
English-to-Spanish translation with a sequence-to-sequence Transformer
+

Implementing a sequence-to-sequence Transformer and training it on a machine translation task.

+
Image segmentation with a U-Net-like architecture
+

Image segmentation model trained from scratch on the Oxford Pets dataset.

+
The Sequential model
+

Complete guide to the Sequential model.

+
Save, serialize, and export models
+

Complete guide to saving, serializing, and exporting models.

+
Structured data classification with FeatureSpace
+

Classify tabular data in a few lines of code.

+
Text classification from scratch
+

Text sentiment classification starting from raw text files.

+
Timeseries anomaly detection using an Autoencoder
+

Detect anomalies in a timeseries using an Autoencoder.

+
Timeseries classification from scratch
+

Training a timeseries classifier from scratch on the FordA dataset from the UCR/UEA archive.

+
Training & evaluation with the built-in methods
+

Complete guide to training & evaluation with fit() and evaluate().

+
Transfer learning & fine-tuning
+

Complete guide to transfer learning & fine-tuning in Keras.

+
Understanding masking & padding
+

Complete guide to using mask-aware sequence layers in Keras.

+
Writing a training loop from scratch in TensorFlow
+

Complete guide to writing low-level training & evaluation loops in TensorFlow.

+
Writing your own callbacks
+

Complete guide to writing new Keras callbacks.

+
+
+ + +
+ + + +
+ + + + + + + diff --git a/docs/dev/articles/intro_to_keras_for_engineers.html b/docs/dev/articles/intro_to_keras_for_engineers.html new file mode 100644 index 0000000000..96c1d5f261 --- /dev/null +++ b/docs/dev/articles/intro_to_keras_for_engineers.html @@ -0,0 +1,529 @@ + + + + + + + + +Introduction to Keras for engineers • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

Keras 3 is a deep learning framework works with TensorFlow, JAX, and +PyTorch interchangeably. This notebook will walk you through key Keras 3 +workflows.

+

Let’s start by installing Keras 3:

+

pip install keras –upgrade –quiet

+
+
+

Setup +

+

We’re going to be using the tensorflow backend here – but you can +edit the string below to "jax" or "torch" and +hit “Restart runtime”, and the whole notebook will run just the same! +This entire guide is backend-agnostic.

+
+library(tensorflow, exclude = c("shape", "set_random_seed"))
+library(keras3)
+
+# Note that you must configure the backend
+# before calling any other keras functions.
+# The backend cannot be changed once the
+# package is imported.
+use_backend("tensorflow")
+
+
+

A first example: A MNIST convnet +

+

Let’s start with the Hello World of ML: training a convnet to +classify MNIST digits.

+

Here’s the data:

+
+# Load the data and split it between train and test sets
+c(c(x_train, y_train), c(x_test, y_test)) %<-% keras3::dataset_mnist()
+
+# Scale images to the [0, 1] range
+x_train <- x_train / 255
+x_test <- x_test / 255
+# Make sure images have shape (28, 28, 1)
+x_train <- op_expand_dims(x_train, -1)
+x_test <- op_expand_dims(x_test, -1)
+
+dim(x_train)
+
## [1] 60000    28    28     1
+
+dim(x_test)
+
## [1] 10000    28    28     1
+

Here’s our model.

+

Different model-building options that Keras offers include:

+ +
+# Model parameters
+num_classes <- 10
+input_shape <- c(28, 28, 1)
+
+model <- keras_model_sequential(input_shape = input_shape)
+model |>
+  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") |>
+  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") |>
+  layer_max_pooling_2d(pool_size = c(2, 2)) |>
+  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") |>
+  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") |>
+  layer_global_average_pooling_2d() |>
+  layer_dropout(rate = 0.5) |>
+  layer_dense(units = num_classes, activation = "softmax")
+

Here’s our model summary:

+
+summary(model)
+
## Model: "sequential"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ conv2d (Conv2D)                 │ (None, 26, 26, 64)     │           640
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_1 (Conv2D)               │ (None, 24, 24, 64)     │        36,928
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d (MaxPooling2D)    │ (None, 12, 12, 64)     │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_2 (Conv2D)               │ (None, 10, 10, 128)    │        73,856
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_3 (Conv2D)               │ (None, 8, 8, 128)      │       147,584
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ global_average_pooling2d        │ (None, 128)            │             0
+## │ (GlobalAveragePooling2D)        │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dropout (Dropout)               │ (None, 128)            │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense (Dense)                   │ (None, 10)             │         1,290
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 260,298 (1016.79 KB)
+##  Trainable params: 260,298 (1016.79 KB)
+##  Non-trainable params: 0 (0.00 B)
+

We use the compile() method to specify the optimizer, +loss function, and the metrics to monitor. Note that with the JAX and +TensorFlow backends, XLA compilation is turned on by default.

+
+model |> compile(
+  optimizer = "adam",
+  loss = "sparse_categorical_crossentropy",
+  metrics = list(
+    metric_sparse_categorical_accuracy(name = "acc")
+  )
+)
+

Let’s train and evaluate the model. We’ll set aside a validation +split of 15% of the data during training to monitor generalization on +unseen data.

+
+batch_size <- 128
+epochs <- 10
+
+callbacks <- list(
+  callback_model_checkpoint(filepath="model_at_epoch_{epoch}.keras"),
+  callback_early_stopping(monitor="val_loss", patience=2)
+)
+
+model |> fit(
+  x_train, y_train,
+  batch_size = batch_size,
+  epochs = epochs,
+  validation_split = 0.15,
+  callbacks = callbacks
+)
+
## Epoch 1/10
+## 399/399 - 7s - 17ms/step - acc: 0.7448 - loss: 0.7526 - val_acc: 0.9630 - val_loss: 0.1252
+## Epoch 2/10
+## 399/399 - 2s - 5ms/step - acc: 0.9371 - loss: 0.2103 - val_acc: 0.9769 - val_loss: 0.0769
+## Epoch 3/10
+## 399/399 - 2s - 5ms/step - acc: 0.9559 - loss: 0.1481 - val_acc: 0.9823 - val_loss: 0.0617
+## Epoch 4/10
+## 399/399 - 2s - 5ms/step - acc: 0.9654 - loss: 0.1177 - val_acc: 0.9863 - val_loss: 0.0484
+## Epoch 5/10
+## 399/399 - 2s - 5ms/step - acc: 0.9715 - loss: 0.0994 - val_acc: 0.9868 - val_loss: 0.0460
+## Epoch 6/10
+## 399/399 - 2s - 5ms/step - acc: 0.9750 - loss: 0.0866 - val_acc: 0.9893 - val_loss: 0.0370
+## Epoch 7/10
+## 399/399 - 2s - 5ms/step - acc: 0.9766 - loss: 0.0788 - val_acc: 0.9901 - val_loss: 0.0368
+## Epoch 8/10
+## 399/399 - 2s - 5ms/step - acc: 0.9785 - loss: 0.0696 - val_acc: 0.9877 - val_loss: 0.0441
+## Epoch 9/10
+## 399/399 - 2s - 5ms/step - acc: 0.9798 - loss: 0.0664 - val_acc: 0.9893 - val_loss: 0.0390
+
+score <- model |> evaluate(x_test, y_test, verbose = 0)
+

During training, we were saving a model at the end of each epoch. You +can also save the model in its latest state like this:

+
+save_model(model, "final_model.keras", overwrite=TRUE)
+

And reload it like this:

+
+model <- load_model("final_model.keras")
+

Next, you can query predictions of class probabilities with +predict():

+
+predictions <- model |> predict(x_test)
+
## 313/313 - 0s - 2ms/step
+
+dim(predictions)
+
## [1] 10000    10
+

That’s it for the basics!

+
+
+

Writing cross-framework custom components +

+

Keras enables you to write custom Layers, Models, Metrics, Losses, +and Optimizers that work across TensorFlow, JAX, and PyTorch with the +same codebase. Let’s take a look at custom layers first.

+

The op_ namespace contains:

+
    +
  • An implementation of the NumPy API, e.g. op_stack or +op_matmul.
  • +
  • A set of neural network specific ops that are absent from NumPy, +such as op_conv or +op_binary_crossentropy.
  • +
+

Let’s make a custom Dense layer that works with all +backends:

+
+layer_my_dense <- Layer(
+  classname = "MyDense",
+  initialize = function(units, activation = NULL, name = NULL, ...) {
+    super$initialize(name = name, ...)
+    self$units <- units
+    self$activation <- activation
+  },
+  build = function(input_shape) {
+    input_dim <- tail(input_shape, 1)
+    self$w <- self$add_weight(
+      shape = shape(input_dim, self$units),
+      initializer = initializer_glorot_normal(),
+      name = "kernel",
+      trainable = TRUE
+    )
+    self$b <- self$add_weight(
+      shape = shape(self$units),
+      initializer = initializer_zeros(),
+      name = "bias",
+      trainable = TRUE
+    )
+  },
+  call = function(inputs) {
+    # Use Keras ops to create backend-agnostic layers/metrics/etc.
+    x <- op_matmul(inputs, self$w) + self$b
+    if (!is.null(self$activation))
+      x <- self$activation(x)
+    x
+  }
+)
+

Next, let’s make a custom Dropout layer that relies on +the random_* namespace:

+
+layer_my_dropout <- Layer(
+  "MyDropout",
+  initialize = function(rate, name = NULL, seed = NULL, ...) {
+    super$initialize(name = name)
+    self$rate <- rate
+    # Use seed_generator for managing RNG state.
+    # It is a state element and its seed variable is
+    # tracked as part of `layer$variables`.
+    self$seed_generator <- random_seed_generator(seed)
+  },
+  call = function(inputs) {
+    # Use `keras3::random_*` for random ops.
+    random_dropout(inputs, self$rate, seed = self$seed_generator)
+  }
+)
+

Next, let’s write a custom subclassed model that uses our two custom +layers:

+
+MyModel <- Model(
+  "MyModel",
+  initialize = function(num_classes, ...) {
+    super$initialize(...)
+    self$conv_base <-
+      keras_model_sequential() |>
+      layer_conv_2d(64, kernel_size = c(3, 3), activation = "relu") |>
+      layer_conv_2d(64, kernel_size = c(3, 3), activation = "relu") |>
+      layer_max_pooling_2d(pool_size = c(2, 2)) |>
+      layer_conv_2d(128, kernel_size = c(3, 3), activation = "relu") |>
+      layer_conv_2d(128, kernel_size = c(3, 3), activation = "relu") |>
+      layer_global_average_pooling_2d()
+
+    self$dp <- layer_my_dropout(rate = 0.5)
+    self$dense <- layer_my_dense(units = num_classes,
+                                 activation = activation_softmax)
+  },
+  call = function(inputs) {
+    inputs |>
+      self$conv_base() |>
+      self$dp() |>
+      self$dense()
+  }
+)
+

Let’s compile it and fit it:

+
+model <- MyModel(num_classes = 10)
+model |> compile(
+  loss = loss_sparse_categorical_crossentropy(),
+  optimizer = optimizer_adam(learning_rate = 1e-3),
+  metrics = list(
+    metric_sparse_categorical_accuracy(name = "acc")
+  )
+)
+
+model |> fit(
+  x_train, y_train,
+  batch_size = batch_size,
+  epochs = 1, # For speed
+  validation_split = 0.15
+)
+
## 399/399 - 7s - 18ms/step - acc: 0.7357 - loss: 0.7709 - val_acc: 0.9298 - val_loss: 0.2355
+
+
+

Training models on arbitrary data sources +

+

All Keras models can be trained and evaluated on a wide variety of +data sources, independently of the backend you’re using. This +includes:

+
    +
  • Arrays
  • +
  • Dataframes
  • +
  • TensorFlow tf_dataset objects
  • +
  • PyTorch DataLoader objects
  • +
  • Keras PyDataset objects
  • +
+

They all work whether you’re using TensorFlow, JAX, or PyTorch as +your Keras backend.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

Let’s try this out with tf_dataset:

+
+library(tfdatasets, exclude = "shape")
+
+train_dataset <- list(x_train, y_train) |>
+  tensor_slices_dataset() |>
+  dataset_batch(batch_size) |>
+  dataset_prefetch(buffer_size = tf$data$AUTOTUNE)
+
+test_dataset <- list(x_test, y_test) |>
+  tensor_slices_dataset() |>
+  dataset_batch(batch_size) |>
+  dataset_prefetch(buffer_size = tf$data$AUTOTUNE)
+
+model <- MyModel(num_classes = 10)
+model |> compile(
+  loss = loss_sparse_categorical_crossentropy(),
+  optimizer = optimizer_adam(learning_rate = 1e-3),
+  metrics = list(
+    metric_sparse_categorical_accuracy(name = "acc")
+  )
+)
+
+model |> fit(train_dataset, epochs = 1, validation_data = test_dataset)
+
## 469/469 - 8s - 17ms/step - acc: 0.7542 - loss: 0.7322 - val_acc: 0.9013 - val_loss: 0.3236
+
+
+

Further reading +

+

This concludes our short overview of the new multi-backend +capabilities of Keras 3. Next, you can learn about:

+
+

How to customize what happens in fit() +

+

Want to implement a non-standard training algorithm yourself but +still want to benefit from the power and usability of +fit()? It’s easy to customize fit() to support +arbitrary use cases:

+ +
+
+
+

How to write custom training loops +

+ +
+
+

How to distribute training +

+ +

Enjoy the library! 🚀

+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/intro_to_keras_for_researchers.html b/docs/dev/articles/intro_to_keras_for_researchers.html new file mode 100644 index 0000000000..55bff47bb4 --- /dev/null +++ b/docs/dev/articles/intro_to_keras_for_researchers.html @@ -0,0 +1,1389 @@ + + + + + + + + +Introduction to Keras for Researchers • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Setup +

+ +
+
+

Introduction +

+

Are you a machine learning researcher? Do you publish at NeurIPS and +push the state-of-the-art in CV and NLP? This guide will serve as your +first introduction to core Keras & TensorFlow API concepts.

+

In this guide, you will learn about:

+
    +
  • Tensors, variables, and gradients in TensorFlow
  • +
  • Creating layers by subclassing the [Layer] class
  • +
  • Writing low-level training loops
  • +
  • Tracking losses created by layers via the add_loss() +method
  • +
  • Tracking metrics in a low-level training loop
  • +
  • Speeding up execution with a compiled +[tensorflow::tf_function()]
  • +
  • Executing layers in training or inference mode
  • +
  • The Keras Functional API
  • +
+

You will also see the Keras API in action in two end-to-end research +examples: a Variational Autoencoder, and a Hypernetwork.

+
+
+

Tensors +

+

TensorFlow is an infrastructure layer for differentiable programming. +At its heart, it’s a framework for manipulating N-dimensional arrays +(tensors), much like NumPy.

+

However, there are three key differences between NumPy and +TensorFlow:

+
    +
  • TensorFlow can leverage hardware accelerators such as GPUs and +TPUs.
  • +
  • TensorFlow can automatically compute the gradient of arbitrary +differentiable tensor expressions.
  • +
  • TensorFlow computation can be distributed to large numbers of +devices on a single machine, and large number of machines (potentially +with multiple devices each).
  • +
+

Let’s take a look at the object that is at the core of TensorFlow: +the Tensor.

+

Here’s a constant tensor:

+
+x <- tf$constant(rbind(c(5, 2), c(1, 3)))
+print(x)
+
## tf.Tensor(
+## [[5. 2.]
+##  [1. 3.]], shape=(2, 2), dtype=float64)
+

You can get its value as a R array by calling +as.array():

+ +
##      [,1] [,2]
+## [1,]    5    2
+## [2,]    1    3
+

It features the attributes dtype and +shape:

+
+x$dtype
+
## tf.float64
+
+x$shape
+
## TensorShape([2, 2])
+

A common way to create constant tensors is via tf$ones +and tf$zeros:

+
+tf$ones(shape = shape(2, 1))
+
## tf.Tensor(
+## [[1.]
+##  [1.]], shape=(2, 1), dtype=float32)
+
+tf$zeros(shape = shape(2, 1))
+
## tf.Tensor(
+## [[0.]
+##  [0.]], shape=(2, 1), dtype=float32)
+

You can also create random constant tensors:

+
+x <- random_normal(shape = c(2, 2), mean = 0.0, stddev = 1.0)
+x <- random_uniform(shape = c(2, 2), minval = 0, maxval = 10)
+
+
+

Variables +

+

Variables are special tensors used to store mutable state (such as +the weights of a neural network). You create a Variable +using some initial value:

+
+initial_value <- random_normal(shape=c(2, 2))
+a <- tf$Variable(initial_value)
+print(a)
+
## <tf.Variable 'Variable:0' shape=(2, 2) dtype=float32, numpy=
+## array([[ 0.9057419 ,  0.7916686 ],
+##        [ 0.28754202, -0.5408822 ]], dtype=float32)>
+

You update the value of a Variable by using the methods +$assign(value), $assign_add(increment), or +$assign_sub(decrement):

+
+new_value <- random_normal(shape=c(2, 2))
+a$assign(new_value)
+
## <tf.Variable 'UnreadVariable' shape=(2, 2) dtype=float32, numpy=
+## array([[-0.3405368 , -2.1463926 ],
+##        [ 1.2602988 ,  0.12241419]], dtype=float32)>
+
+added_value <- random_normal(shape=c(2, 2))
+a$assign_add(added_value)
+
## <tf.Variable 'UnreadVariable' shape=(2, 2) dtype=float32, numpy=
+## array([[ 0.04820395, -2.6854615 ],
+##        [ 0.23246336,  1.4535258 ]], dtype=float32)>
+
+
+

Doing math in TensorFlow +

+

If you’ve used NumPy, doing math in TensorFlow will look very +familiar. The main difference is that your TensorFlow code can run on +GPU and TPU.

+
+a <- random_normal(shape=c(2, 2))
+b <- random_normal(shape=c(2, 2))
+
+c <- a + b
+d <- tf$square(c)
+e <- tf$exp(d)
+
+
+

Gradients +

+

Here’s another big difference with R: you can automatically retrieve +the gradient of any differentiable expression.

+

Just open a GradientTape, start “watching” a tensor via +tape$watch(), and compose a differentiable expression using +this tensor as input:

+
+a <- random_normal(shape=c(2, 2))
+b <- random_normal(shape=c(2, 2))
+
+with(tf$GradientTape() %as% tape, {
+  tape$watch(a)  # Start recording the history of operations applied to `a`
+  c <- tf$sqrt(tf$square(a) + tf$square(b))  # Do some math using `a`
+  # What's the gradient of `c` with respect to `a`?
+  dc_da <- tape$gradient(c, a)
+  print(dc_da)
+})
+
## tf.Tensor(
+## [[ 0.9969011  -0.7707146 ]
+##  [ 0.23378514  0.96255165]], shape=(2, 2), dtype=float32)
+

By default, variables are watched automatically, so you don’t need to +manually watch them:

+
+a <- tf$Variable(a)
+
+with(tf$GradientTape() %as% tape, {
+  c <- tf$sqrt(tf$square(a) + tf$square(b))
+  dc_da <- tape$gradient(c, a)
+  print(dc_da)
+})
+
## tf.Tensor(
+## [[ 0.9969011  -0.7707146 ]
+##  [ 0.23378514  0.96255165]], shape=(2, 2), dtype=float32)
+

Note that you can compute higher-order derivatives by nesting +tapes:

+
+with(tf$GradientTape() %as% outer_tape, {
+  with(tf$GradientTape() %as% tape, {
+    c <- tf$sqrt(tf$square(a) + tf$square(b))
+    dc_da <- tape$gradient(c, a)
+  })
+  d2c_da2 <- outer_tape$gradient(dc_da, a)
+  print(d2c_da2)
+})
+
## tf.Tensor(
+## [[3.3447742e-03 7.1282005e-01]
+##  [5.7464113e+00 5.5013180e-02]], shape=(2, 2), dtype=float32)
+
+
+

Keras layers +

+

While TensorFlow is an infrastructure layer for +differentiable programming, dealing with tensors, variables, +and gradients, Keras is a user interface for deep +learning, dealing with layers, models, optimizers, loss +functions, metrics, and more.

+

Keras serves as the high-level API for TensorFlow: Keras is what +makes TensorFlow simple and productive.

+

The Layer class is the fundamental abstraction in Keras. +A Layer encapsulates a state (weights) and some computation +(defined in the call method).

+

A simple layer looks like this. The self$add_weight() +method gives you a shortcut for creating weights:

+
+Linear <- new_layer_class(
+  "Linear",
+  initialize = function(units = 32, input_dim = 32) {
+    super$initialize()
+    self$w <- self$add_weight(
+      shape = shape(input_dim, units),
+      initializer = "random_normal",
+      trainable = TRUE
+    )
+    self$b <- self$add_weight(
+      shape = shape(units),
+      initializer = "zeros",
+      trainable = TRUE
+    )
+  },
+  call = function(inputs) {
+    tf$matmul(inputs, self$w) + self$b
+  }
+)
+

You would use a Layer instance much like a R +function:

+
+# Instantiate our layer.
+linear_layer <- Linear(units=4, input_dim=2)
+
+# The layer can be treated as a function.
+# Here we call it on some data.
+y <- linear_layer(tf$ones(shape(2, 2)))
+

The weight variables (created in initialize) are +automatically tracked under the weights property:

+
+linear_layer$weights
+
## [[1]]
+## <KerasVariable shape=(2, 4), dtype=float32, path=linear/variable>
+##
+## [[2]]
+## <KerasVariable shape=(4), dtype=float32, path=linear/variable_1>
+

You have many built-in layers available, from Dense to +Conv2D to LSTM to fancier ones like +Conv3DTranspose or ConvLSTM2D. Be smart about +reusing built-in functionality.

+
+
+

Layer weight creation in build(input_shape) +

+

It’s often a good idea to defer weight creation to the +build() method, so that you don’t need to specify the input +dim/shape at layer construction time:

+
+Linear <- new_layer_class(
+  "Linear",
+  initialize = function(units = 32) {
+    super$initialize()
+    self$units <- units
+  },
+  build = function(input_shape) {
+    self$w <- self$add_weight(
+      shape = shape(input_shape[-1], self$units),
+      initializer = "random_normal",
+      trainable = TRUE
+    )
+    self$b <- self$add_weight(
+      shape = shape(self$units),
+      initializer = "zeros",
+      trainable = TRUE
+    )
+  },
+  call = function(inputs) {
+    tf$matmul(inputs, self$w) + self$b
+  }
+)
+
+# Instantiate our layer.
+linear_layer <- Linear(units = 4)
+
+# This will also call `build(input_shape)` and create the weights.
+y <- linear_layer(tf$ones(shape(2, 2)))
+
+
+

Layer gradients +

+

You can automatically retrieve the gradients of the weights of a +layer by calling it inside a GradientTape. Using these +gradients, you can update the weights of the layer, either manually, or +using an optimizer object. Of course, you can modify the gradients +before using them, if you need to.

+
+# Prepare a dataset.
+c(c(x_train, y_train), .) %<-% dataset_mnist()
+
+x_train <- array_reshape(x_train, c(60000, 784)) / 255
+
+dataset <- tfdatasets::tensor_slices_dataset(list(x_train, y_train)) %>%
+  tfdatasets::dataset_shuffle(buffer_size=1024) %>%
+  tfdatasets::dataset_batch(64)
+
+# Instantiate our linear layer (defined above) with 10 units.
+linear_layer <- Linear(units = 10)
+
+# Instantiate a logistic loss function that expects integer targets.
+loss_fn <- loss_sparse_categorical_crossentropy(from_logits=TRUE)
+
+# Instantiate an optimizer.
+optimizer <- optimizer_sgd(learning_rate=1e-3)
+
+# Iterate over the batches of the dataset.
+coro::loop(for(data in dataset) {
+  # Open a GradientTape.
+  with(tf$GradientTape() %as% tape, {
+    # Forward pass.
+    logits <- linear_layer(data[[1]])
+
+    # Loss value for this batch.
+    loss_value <- loss_fn(data[[2]], logits)
+  })
+
+  # Get gradients of the loss wrt the weights.
+  gradients <- tape$gradient(loss_value, linear_layer$trainable_weights)
+
+  # Update the weights of our linear layer.
+  optimizer$apply_gradients(zip_lists(gradients, linear_layer$trainable_weights))
+})
+loss_value
+
## tf.Tensor(1.2819729, shape=(), dtype=float32)
+
+
+

Trainable and non-trainable weights +

+

Weights created by layers can be either trainable or non-trainable. +They’re exposed in trainable_weights and +non_trainable_weights respectively. Here’s a layer with a +non-trainable weight:

+
+ComputeSum <- new_layer_class(
+  "ComputeSum",
+  initialize = function(input_dim) {
+    super$initialize()
+    # Create a non-trainable weight.
+    self$total <- self$add_weight(
+      initializer = "zeros",
+      shape = shape(input_dim),
+      trainable = FALSE
+    )
+  },
+  call = function(inputs) {
+    self$total$assign_add(tf$reduce_sum(inputs, axis=0L))
+    self$total
+  }
+)
+
+my_sum <- ComputeSum(input_dim = 2)
+x <- tf$ones(shape(2, 2))
+
+as.array(my_sum(x))
+
## [1] 2 2
+
+as.array(my_sum(x))
+
## [1] 4 4
+
+my_sum$trainable_weights
+
## list()
+
+
+

Layers that own layers +

+

Layers can be recursively nested to create bigger computation blocks. +Each layer will track the weights of its sublayers (both trainable and +non-trainable).

+
+# Let's reuse the Linear class
+# with a `build` method that we defined above.
+
+MLP <- new_layer_class(
+  "MLP",
+  initialize = function() {
+    super$initialize()
+    self$linear_1 <- Linear(units = 32)
+    self$linear_2 <- Linear(units = 32)
+    self$linear_3 <- Linear(units = 10)
+  },
+  call = function(inputs) {
+    x <- self$linear_1(inputs)
+    x <- tf$nn$relu(x)
+    x <- self$linear_2(x)
+    x <- tf$nn$relu(x)
+    return(self$linear_3(x))
+  }
+)
+
+mlp <- MLP()
+
+# The first call to the `mlp` object will create the weights.
+y <- mlp(tf$ones(shape=shape(3, 64)))
+
+# Weights are recursively tracked.
+length(mlp$weights)
+
## [1] 6
+

Note that our manually-created MLP above is equivalent to the +following built-in option:

+
+mlp <- keras_model_sequential() %>%
+  layer_dense(units = 32, activation = "relu") %>%
+  layer_dense(units = 32, activation = "relu") %>%
+  layer_dense(units = 10)
+
+
+

Tracking losses created by layers +

+

Layers can create losses during the forward pass via the +add_loss() method. This is especially useful for +regularization losses. The losses created by sublayers are recursively +tracked by the parent layers.

+

Here’s a layer that creates an activity regularization loss:

+
+# A layer that creates an activity sparsity regularization loss
+ActivityRegularization <- new_layer_class(
+  "ActivityRegularization",
+  initialize = function(rate=1e-2) {
+    super$initialize()
+    self$rate <- rate
+  },
+  call = function(inputs) {
+    self$add_loss(self$rate * tf$reduce_sum(tf$abs(inputs)))
+    inputs
+  }
+)
+

Any model incorporating this layer will track this regularization +loss:

+
+# Let's use the loss layer in a MLP block.
+SparseMLP <- new_layer_class(
+  "SparseMLP",
+  initialize = function() {
+    super$initialize()
+    self$linear_1 <- Linear(units = 32)
+    self$reg <- ActivityRegularization(rate = 1e-2)
+    self$linear_3 <- Linear(units = 10)
+  },
+  call = function(inputs) {
+    x <- self$linear_1(inputs)
+    x <- tf$nn$relu(x)
+    x <- self$reg(x)
+    return(self$linear_3(x))
+  }
+)
+
+mlp <- SparseMLP()
+y <- mlp(tf$ones(shape(10, 10)))
+
+mlp$losses  # List containing one float32 scalar
+
## [[1]]
+## tf.Tensor(0.18065463, shape=(), dtype=float32)
+

These losses are cleared by the top-level layer at the start of each +forward pass – they don’t accumulate. layer.losses always +contains only the losses created during the last forward pass. You would +typically use these losses by summing them before computing your +gradients when writing a training loop.

+
+# Losses correspond to the *last* forward pass.
+mlp <- SparseMLP()
+mlp(tf$ones(shape(10, 10)))
+
## tf.Tensor(
+## [[ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]], shape=(10, 10), dtype=float32)
+
+length(mlp$losses)
+
## [1] 1
+
+mlp(tf$ones(shape(10, 10)))
+
## tf.Tensor(
+## [[ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]
+##  [ 0.0388482  -0.03920118  0.01624808 -0.01361975 -0.01354899  0.07107338
+##   -0.01077365  0.05688906 -0.02838149 -0.04084621]], shape=(10, 10), dtype=float32)
+
+length(mlp$losses)  # No accumulation.
+
## [1] 1
+
+# Let's demonstrate how to use these losses in a training loop.
+
+# Prepare a dataset.
+c(c(x_train, y_train), .) %<-% dataset_mnist()
+x_train <- array_reshape(x_train, c(60000, 784)) / 255
+
+dataset <- tfdatasets::tensor_slices_dataset(list(x_train, y_train)) %>%
+  tfdatasets::dataset_shuffle(buffer_size=1024) %>%
+  tfdatasets::dataset_batch(64)
+
+# A new MLP.
+mlp <- SparseMLP()
+
+# Loss and optimizer.
+loss_fn <- loss_sparse_categorical_crossentropy(from_logits=TRUE)
+optimizer <- optimizer_sgd(learning_rate=1e-3)
+
+coro::loop(for(data in dataset) {
+  x <- data[[1]]
+  y <- data[[2]]
+  with(tf$GradientTape() %as% tape, {
+    # Forward pass.
+    logits <- mlp(x)
+
+    # External loss value for this batch.
+    loss <- loss_fn(y, logits)
+
+    # Add the losses created during the forward pass.
+    loss <- loss + Reduce(`+`, mlp$losses)
+
+    # Get gradients of the loss wrt the weights.
+    gradients <- tape$gradient(loss, mlp$trainable_weights)
+
+    # Update the weights of our linear layer.
+    optimizer$apply_gradients(zip_lists(gradients, mlp$trainable_weights))
+  })
+})
+
+
+

Keeping track of training metrics +

+

Keras offers a broad range of built-in metrics, like +metric_auc or metric_precision_at_recall. It’s +also easy to create your own metrics in a few lines of code.

+

To use a metric in a custom training loop, you would:

+
    +
  • Instantiate the metric object, +e.g. metric = metric_auc() +
  • +
  • Call its metric$udpate_state(targets, predictions) +method for each batch of data
  • +
  • Query its result via metric$result() +
  • +
  • Reset the metric’s state at the end of an epoch or at the start of +an evaluation via metric$reset_state() +
  • +
+

Here’s a simple example:

+
+# Instantiate a metric object
+accuracy <- metric_sparse_categorical_accuracy()
+
+# Prepare our layer, loss, and optimizer.
+model <- keras_model_sequential() %>%
+  layer_dense(units = 32, activation = "relu") %>%
+  layer_dense(units = 32, activation = "relu") %>%
+  layer_dense(units = 10)
+loss_fn <- loss_sparse_categorical_crossentropy(from_logits = TRUE)
+optimizer <- optimizer_adam(learning_rate=1e-3)
+
+for (epoch in seq_len(2)) {
+  coro::loop(for (data in dataset) {
+    x <- data[[1]]
+    y <- data[[2]]
+    with(tf$GradientTape() %as% tape, {
+      # Forward pass.
+      logits <- model(x)
+
+      # External loss value for this batch.
+      loss_value <- loss_fn(y, logits)
+    })
+
+    # Update the state of the `accuracy` metric.
+    accuracy$update_state(y, logits)
+
+    # Update the weights of the model to minimize the loss value.
+    gradients <- tape$gradient(loss_value, model$trainable_weights)
+    optimizer$apply_gradients(zip_lists(gradients, model$trainable_weights))
+
+  })
+  cat("Epoch:", epoch, "Accuracy:", as.numeric(accuracy$result()), "\n")
+  accuracy$reset_state()
+}
+
## Epoch: 1 Accuracy: 0.8757833
+## Epoch: 2 Accuracy: 0.93915
+

You can also define your own metrics by subclassing +keras.metrics.Metric. You need to override the three +functions called above:

+
    +
  • Override update_state() to update the statistic +values.
  • +
  • Override result() to return the metric value.
  • +
  • Override reset_state() to reset the metric to its +initial state.
  • +
+

Here is an example where we implement the F1-score metric (with +support for sample weighting).

+
+F1Score <- new_metric_class(
+  "F1Score",
+  initialize = function(self, name="f1_score", dtype="float32", threshold=0.5, ...) {
+    super$initialize(name=name, dtype=dtype, ...)
+    self$threshold <- threshold
+    self$true_positives <- self$add_weight(
+      name="tp", dtype=dtype, initializer="zeros"
+    )
+    self$false_positives <- self$add_weight(
+      name="fp", dtype=dtype, initializer="zeros"
+    )
+    self$false_negatives <- self$add_weight(
+      name="fn", dtype=dtype, initializer="zeros"
+    )
+  },
+  update_state = function(y_true, y_pred, sample_weight=NULL) {
+    y_pred <- tf$math$greater_equal(y_pred, self$threshold)
+    y_true <- tf$cast(y_true, tf$bool)
+    y_pred <- tf$cast(y_pred, tf$bool)
+
+    true_positives <- tf$cast(y_true & y_pred, self$dtype)
+    false_positives <- tf$cast((!y_true) & y_pred, self$dtype)
+    false_negatives <- tf$cast(y_true & (!y_pred), self$dtype)
+
+    if (!is.null(sample_weight)) {
+      sample_weight <- tf$cast(sample_weight, self$dtype)
+      true_positives <- true_positives * sample_weight
+      false_positives <- false_positives * sample_weight
+      false_negatives <- false_negatives * sample_weight
+    }
+
+    self$true_positives$assign_add(tf$reduce_sum(true_positives))
+    self$false_positives$assign_add(tf$reduce_sum(false_positives))
+    self$false_negatives$assign_add(tf$reduce_sum(false_negatives))
+  },
+
+  result = function() {
+    precision <- self$true_positives / (self$true_positives + self$false_positives)
+    recall <- self$true_positives / (self$true_positives + self$false_negatives)
+    f1_score <- 2 * precision * recall / (precision + recall)
+    f1_score
+  },
+
+  reset_state = function() {
+    self$true_positives$assign(0)
+    self$false_positives$assign(0)
+    self$false_negatives$assign(0)
+  }
+)
+

Let’s test-drive it:

+
+m <- F1Score()
+m$update_state(c(0, 1, 0, 0), c(0.3, 0.5, 0.8, 0.9))
+cat("Intermediate result:", as.numeric(m$result()), "\n")
+
## Intermediate result: 0.5
+
+m$update_state(c(1, 1, 1, 1), c(0.1, 0.7, 0.6, 0.0))
+cat("Final result:", as.numeric(m$result()), "\n")
+
## Final result: 0.6
+
+
+

Compiled functions +

+

Running eagerly is great for debugging, but you will get better +performance by compiling your computation into static graphs. Static +graphs are a researcher’s best friends. You can compile any function by +wrapping it in a tf.function decorator.

+
+# Prepare our layer, loss, and optimizer.
+model <- keras_model_sequential() %>%
+  layer_dense(units = 32, activation = "relu") %>%
+  layer_dense(units = 32, activation = "relu") %>%
+  layer_dense(units = 10)
+loss_fn <- loss_sparse_categorical_crossentropy(from_logits = TRUE)
+optimizer <- optimizer_adam(learning_rate=1e-3)
+
+# Create a training step function.
+train_on_batch <- tf_function(function(x, y) {
+  with(tf$GradientTape() %as% tape, {
+    # Forward pass.
+    logits <- model(x)
+    # External loss value for this batch.
+    loss_value <- loss_fn(y, logits)
+  })
+  # Update the weights of the model to minimize the loss value.
+  gradients <- tape$gradient(loss_value, model$trainable_weights)
+  optimizer$apply_gradients(zip_lists(gradients, model$trainable_weights))
+  loss_value
+})
+
+
+# Prepare a dataset.
+c(c(x_train, y_train), .) %<-% dataset_mnist()
+x_train <- array_reshape(x_train, c(60000, 784)) / 255
+
+dataset <- tfdatasets::tensor_slices_dataset(list(x_train, y_train)) %>%
+  tfdatasets::dataset_shuffle(buffer_size=1024) %>%
+  tfdatasets::dataset_batch(64)
+
+i <- 0
+coro::loop(for (data in dataset) {
+  i <- i + 1
+  x <- data[[1]]
+  y <- data[[2]]
+  loss <- train_on_batch(x, y)
+  if (i %% 100 == 0)
+    cat("Loss:", as.numeric(loss), "\n")
+})
+
## Loss: 0.551749
+## Loss: 0.2131135
+## Loss: 0.2765952
+## Loss: 0.1296219
+## Loss: 0.2657076
+## Loss: 0.2683381
+## Loss: 0.1570166
+## Loss: 0.3139241
+## Loss: 0.08981849
+
+
+

Training mode & inference mode +

+

Some layers, in particular the BatchNormalization layer +and the Dropout layer, have different behaviors during +training and inference. For such layers, it is standard practice to +expose a training (boolean) argument in the +call method.

+

By exposing this argument in call, you enable the +built-in training and evaluation loops (e.g. fit) to correctly use the +layer in training and inference modes.

+
+Dropout <- new_layer_class(
+  "Dropout",
+  initialize = function(rate) {
+    super$initialize()
+    self$rate <- rate
+  },
+  call = function(inputs, training = NULL) {
+    if (!is.null(training) && training) {
+      return(tf$nn$dropout(inputs, rate = self$rate))
+    }
+    inputs
+  }
+)
+
+MLPWithDropout <- new_layer_class(
+  "MLPWithDropout",
+  initialize = function() {
+    super$initialize()
+    self$linear_1 <- Linear(units = 32)
+    self$dropout <- Dropout(rate = 0.5)
+    self$linear_3 <- Linear(units = 10)
+  },
+  call = function(inputs, training = NULL) {
+    x <- self$linear_1(inputs)
+    x <- tf$nn$relu(x)
+    x <- self$dropout(x, training = training)
+    self$linear_3(x)
+  }
+)
+
+mlp <- MLPWithDropout()
+y_train <- mlp(tf$ones(shape(2, 2)), training=TRUE)
+y_test <- mlp(tf$ones(shape(2, 2)), training=FALSE)
+
+
+

The Functional API for model-building +

+

To build deep learning models, you don’t have to use object-oriented +programming all the time. All layers we’ve seen so far can also be +composed functionally, like this (we call it the “Functional API”):

+
+# We use an `Input` object to describe the shape and dtype of the inputs.
+# This is the deep learning equivalent of *declaring a type*.
+# The shape argument is per-sample; it does not include the batch size.
+# The functional API focused on defining per-sample transformations.
+# The model we create will automatically batch the per-sample transformations,
+# so that it can be called on batches of data.
+inputs <- layer_input(shape = 16, dtype = "float32")
+
+# We call layers on these "type" objects
+# and they return updated types (new shapes/dtypes).
+outputs <- inputs %>%
+  Linear(units = 32) %>% # We are reusing the Linear layer we defined earlier.
+  Dropout(rate = 0.5) %>% # We are reusing the Dropout layer we defined earlier.
+  Linear(units = 10)
+
+# A functional `Model` can be defined by specifying inputs and outputs.
+# A model is itself a layer like any other.
+model <- keras_model(inputs, outputs)
+
+# A functional model already has weights, before being called on any data.
+# That's because we defined its input shape in advance (in `Input`).
+length(model$weights)
+
## [1] 4
+
+# Let's call our model on some data, for fun.
+y <- model(tf$ones(shape(2, 16)))
+y$shape
+
## TensorShape([2, 10])
+
+# You can pass a `training` argument in `__call__`
+# (it will get passed down to the Dropout layer).
+y <- model(tf$ones(shape(2, 16)), training=TRUE)
+

The Functional API tends to be more concise than subclassing, and +provides a few other advantages (generally the same advantages that +functional, typed languages provide over untyped OO development). +However, it can only be used to define DAGs of layers – recursive +networks should be defined as Layer subclasses instead.

+

Learn more about the Functional API here.

+

In your research workflows, you may often find yourself +mix-and-matching OO models and Functional models.

+

Note that the Model class also features built-in +training & evaluation loops: fit(), +predict() and evaluate() (configured via the +compile() method). These built-in functions give you access +to the following built-in training infrastructure features:

+
    +
  • +Callbacks. +You can leverage built-in callbacks for early-stopping, model +checkpointing, and monitoring training with TensorBoard. You can also implement custom callbacks if +needed.
  • +
  • +Distributed +training. You can easily scale up your training to multiple GPUs, +TPU, or even multiple machines with the tf.distribute API – +with no changes to your code.
  • +
  • +Step +fusing. With the steps_per_execution argument in +Model.compile(), you can process multiple batches in a +single tf.function call, which greatly improves device +utilization on TPUs.
  • +
+

We won’t go into the details, but we provide a simple code example +below. It leverages the built-in training infrastructure to implement +the MNIST example above.

+
+inputs <- layer_input(shape = 784, dtype="float32")
+outputs <- inputs %>%
+  layer_dense(units = 32, activation = "relu") %>%
+  layer_dense(units = 32, activation = "relu") %>%
+  layer_dense(units = 10)
+model <- keras_model(inputs, outputs)
+
+# Specify the loss, optimizer, and metrics with `compile()`.
+model %>% compile(
+    loss = loss_sparse_categorical_crossentropy(from_logits=TRUE),
+    optimizer=optimizer_adam(learning_rate=1e-3),
+    metrics=list(metric_sparse_categorical_accuracy()),
+)
+
+# Train the model with the dataset for 2 epochs.
+model %>% fit(dataset, epochs=2)
+
## Epoch 1/2
+## 938/938 - 4s - 4ms/step - loss: 0.3958 - sparse_categorical_accuracy: 0.8866
+## Epoch 2/2
+## 938/938 - 1s - 960us/step - loss: 0.1888 - sparse_categorical_accuracy: 0.9443
+
+predictions <- model %>% predict(dataset)
+
## 938/938 - 1s - 1ms/step
+
+model %>% evaluate(dataset)
+
## 938/938 - 1s - 1ms/step - loss: 0.1763 - sparse_categorical_accuracy: 0.9454
+
## $loss
+## [1] 0.1763445
+##
+## $sparse_categorical_accuracy
+## [1] 0.9454167
+

You can always subclass the Model class (it works +exactly like subclassing Layer) if you want to leverage +built-in training loops for your OO models. Just override the +Model$train_step() to customize what happens in +fit() while retaining support for the built-in +infrastructure features outlined above – callbacks, zero-code +distribution support, and step fusing support. You may also override +test_step() to customize what happens in +evaluate(), and override predict_step() to +customize what happens in predict(). For more information, +please refer to this +guide.

+
+CustomModel <- new_model_class(
+  "CustomModel",
+  initialize = function(...) {
+    super$initialize(...)
+    self$loss_tracker <- metric_mean(name="loss")
+    self$accuracy <- metric_sparse_categorical_accuracy()
+    self$loss_fn <- loss_sparse_categorical_crossentropy(from_logits=TRUE)
+    self$optimizer <- optimizer_adam(learning_rate=1e-3)
+  },
+  train_step = function(data) {
+    c(x, y = NULL, sample_weight = NULL) %<-% data
+    with(tf$GradientTape() %as% tape, {
+      y_pred <- self(x, training=TRUE)
+      loss <- self$loss_fn(y = y, y_pred = y_pred, sample_weight=sample_weight)
+    })
+    gradients <- tape$gradient(loss, self$trainable_variables)
+    self$optimizer$apply_gradients(
+      zip_lists(gradients, self$trainable_variables)
+    )
+
+    # Update metrics (includes the metric that tracks the loss)
+    self$loss_tracker$update_state(loss)
+    self$accuracy$update_state(y, y_pred, sample_weight=sample_weight)
+    # Return a list mapping metric names to current value
+    list(
+      loss = self$loss_tracker$result(),
+      accuracy = self$accuracy$result()
+    )
+  },
+  metrics = mark_active(function() {
+    list(self$loss_tracker, self$accuracy)
+  })
+)
+
+inputs <- layer_input(shape = 784, dtype="float32")
+outputs <- inputs %>%
+  layer_dense(units = 32, activation = "relu") %>%
+  layer_dense(units = 32, activation = "relu") %>%
+  layer_dense(units = 10)
+model <- CustomModel(inputs, outputs)
+model %>% compile()
+model %>% fit(dataset, epochs=2)
+
## Epoch 1/2
+## 938/938 - 2s - 2ms/step - loss: 0.3869 - sparse_categorical_accuracy: 0.8924
+## Epoch 2/2
+## 938/938 - 1s - 1ms/step - loss: 0.2163 - sparse_categorical_accuracy: 0.9370
+
+
+

End-to-end experiment example 1: variational autoencoders. +

+

Here are some of the things you’ve learned so far:

+
    +
  • A Layer encapsulates a state (created in +__init__ or build) and some computation +(defined in call).
  • +
  • Layers can be recursively nested to create new, bigger computation +blocks.
  • +
  • You can easily write highly hackable training loops by opening a +GradientTape, calling your model inside the tape’s scope, +then retrieving gradients and applying them via an optimizer.
  • +
  • You can speed up your training loops using the +@tf.function decorator.
  • +
  • Layers can create and track losses (typically regularization losses) +via self.add_loss().
  • +
+

Let’s put all of these things together into an end-to-end example: +we’re going to implement a Variational AutoEncoder (VAE). We’ll train it +on MNIST digits.

+

Our VAE will be a subclass of Layer, built as a nested +composition of layers that subclass Layer. It will feature +a regularization loss (KL divergence).

+

Below is our model definition.

+

First, we have an Encoder class, which uses a +Sampling layer to map a MNIST digit to a latent-space +triplet (z_mean, z_log_var, z).

+
+Sampling <- new_layer_class(
+  "Sampling",
+  call = function(inputs) {
+    c(z_mean, z_log_var) %<-% inputs
+    batch <- op_shape(z_mean)[[1]]
+    dim <- op_shape(z_mean)[[2]]
+    epsilon <- random_normal(shape = c(batch, dim))
+    z_mean + op_exp(0.5 * z_log_var) * epsilon
+  }
+)
+
+Encoder <- new_layer_class(
+  "Encoder",
+  initialize = function(latent_dim = 32, intermediate_dim = 64, ...) {
+    super$initialize(...)
+    self$dense_proj <- layer_dense(units = intermediate_dim, activation = "relu")
+    self$dense_mean <- layer_dense(units = latent_dim)
+    self$dense_log_var <- layer_dense(units = latent_dim)
+    self$sampling <- Sampling()
+  },
+  call = function(inputs) {
+    x <- self$dense_proj(inputs)
+    z_mean <- self$dense_mean(x)
+    z_log_var <- self$dense_log_var(x)
+    z <- self$sampling(list(z_mean, z_log_var))
+    list(z_mean, z_log_var, z)
+  }
+)
+

Next, we have a Decoder class, which maps the +probabilistic latent space coordinates back to a MNIST digit.

+
+Decoder <- new_layer_class(
+  "Decoder",
+  initialize = function(original_dim, intermediate_dim = 64, ...) {
+    super$initialize(...)
+    self$dense_proj <- layer_dense(units = intermediate_dim, activation = "relu")
+    self$dense_output <- layer_dense(units = original_dim, activation = "sigmoid")
+  },
+  call = function(inputs) {
+    x <- self$dense_proj(inputs)
+    self$dense_output(x)
+  }
+)
+

Finally, our VariationalAutoEncoder composes together an +encoder and a decoder, and creates a KL divergence regularization loss +via add_loss().

+
+VariationalAutoEncoder <- new_model_class(
+  "VariationalAutoEncoder",
+  initialize = function(original_dim,
+        intermediate_dim=64,
+        latent_dim=32,
+        name="autoencoder", ...) {
+    super$initialize(name = name, ...)
+    self$original_dim <- original_dim
+    self$encoder <- Encoder(
+      latent_dim = latent_dim,
+      intermediate_dim = intermediate_dim
+    )
+    self$decoder <- Decoder(
+      original_dim = original_dim,
+      intermediate_dim = intermediate_dim
+    )
+  },
+  call = function(inputs) {
+    c(z_mean, z_log_var, z) %<-% self$encoder(inputs)
+    reconstructed <- self$decoder(z)
+    # Add KL divergence regularization loss.
+    kl_loss <- -0.5 * op_mean(
+      z_log_var - op_square(z_mean) - op_exp(z_log_var) + 1
+    )
+    self$add_loss(kl_loss)
+    reconstructed
+  }
+)
+

Now, let’s write a training loop. Our training step is decorated with +a @tf.function to compile into a super fast graph +function.

+
+# Our model.
+vae <- VariationalAutoEncoder(
+  original_dim = 784,
+  intermediate_dim = 64,
+  latent_dim = 32
+)
+
+# Loss and optimizer.
+loss_fn <- loss_mean_squared_error()
+optimizer = optimizer_adam(learning_rate=1e-3)
+
+# Prepare a dataset.
+c(c(x_train, .), .) %<-% dataset_mnist()
+x_train <- array_reshape(x_train, c(60000, 784)) / 255
+
+dataset <- tfdatasets::tensor_slices_dataset(x_train) %>%
+  tfdatasets::dataset_shuffle(buffer_size=1024) %>%
+  tfdatasets::dataset_batch(32)
+
+
+training_step <- tf_function(function(x) {
+  with(tf$GradientTape() %as% tape, {
+    reconstructed <- vae(x)  # Compute input reconstruction.
+    # Compute loss.
+    loss <- loss_fn(x, reconstructed)
+    loss <- loss + op_sum(vae$losses)  # Add KLD term.
+  })
+  # Update the weights of the VAE.
+  grads <- tape$gradient(loss, vae$trainable_weights)
+  optimizer$apply_gradients(zip_lists(grads, vae$trainable_weights))
+  loss
+})
+
+losses <- c()  # Keep track of the losses over time.
+coro::loop(for(data in dataset) {
+  loss <- training_step(data)
+
+  # Logging.
+  losses[length(losses) + 1] <- as.numeric(loss)
+  if (length(losses) %% 100 == 0) {
+    cat("Step:", length(losses), "Loss:", mean(losses), "\n")
+  }
+  # Stop after 1000 steps.
+  # Training the model to convergence is left
+  # as an exercise to the reader.
+  if (length(losses) >= 1000) {
+    break
+  }
+})
+
## Step: 100 Loss: 0.1270978
+## Step: 200 Loss: 0.1003238
+## Step: 300 Loss: 0.09001128
+## Step: 400 Loss: 0.08493649
+## Step: 500 Loss: 0.08171404
+## Step: 600 Loss: 0.07926706
+## Step: 700 Loss: 0.07790599
+## Step: 800 Loss: 0.07670419
+## Step: 900 Loss: 0.07570736
+## Step: 1000 Loss: 0.07476593
+

As you can see, building and training this type of model in Keras is +quick and painless.

+
+
+

End-to-end experiment example 2: hypernetworks. +

+

Let’s take a look at another kind of research experiment: +hypernetworks.

+

The idea is to use a small deep neural network (the hypernetwork) to +generate the weights for a larger network (the main network).

+

Let’s implement a really trivial hypernetwork: we’ll use a small +2-layer network to generate the weights of a larger 3-layer network.

+
+input_dim <- 784
+classes <- 10
+
+# This is the main network we'll actually use to predict labels.
+inputs <- layer_input(shape = input_dim)
+dense1 <- layer_dense(units = 64, activation = "relu")
+dense1$built <- TRUE
+
+dense2 <- layer_dense(units = classes)
+dense2$built <- TRUE
+
+outputs <- inputs %>% dense1() %>% dense2()
+main_network <- keras_model(inputs, outputs)
+
+# This is the number of weight coefficients to generate. Each layer in the
+# main network requires output_dim * input_dim + output_dim coefficients.
+num_weights_to_generate <- (classes * 64 + classes) + (64 * input_dim + 64)
+
+# This is the hypernetwork that generates the weights of the `main_network` above.
+hypernetwork <- keras_model_sequential() %>%
+  layer_dense(units=16, activation="relu") %>%
+  layer_dense(units=num_weights_to_generate, activation="sigmoid")
+

This is our training loop. For each batch of data:

+
    +
  • We use hypernetwork to generate an array of weight +coefficients, weights_pred +
  • +
  • We reshape these coefficients into kernel & bias tensors for the +main_network +
  • +
  • We run the forward pass of the main_network to compute +the actual MNIST predictions
  • +
  • We run backprop through the weights of the hypernetwork +to minimize the final classification loss
  • +
+
+# Loss and optimizer.
+loss_fn <- loss_sparse_categorical_crossentropy(from_logits = TRUE)
+optimizer <- optimizer_adam(learning_rate=1e-4)
+
+# Prepare a dataset.
+c(c(x_train, y_train), .) %<-% dataset_mnist()
+x_train <- array_reshape(x_train, c(60000, 784)) / 255
+
+dataset <- tfdatasets::tensor_slices_dataset(list(x_train, y_train)) %>%
+  tfdatasets::dataset_shuffle(buffer_size=1024) %>%
+  # We'll use a batch size of 1 for this experiment.
+  tfdatasets::dataset_batch(1)
+
+train_step <- function(x, y) {
+  with(tf$GradientTape() %as% tape, {
+    weights_pred <- hypernetwork(x)
+
+    # Reshape them to the expected shapes for w and b for the outer model.
+    # Layer 1 kernel.
+    start_index <- 1
+    w1_shape <- c(input_dim, 64)
+    w1_coeffs <- weights_pred[, start_index:(start_index + prod(w1_shape) - 1)]
+    w1 <- tf$reshape(w1_coeffs, as.integer(w1_shape))
+    start_index <- start_index + prod(w1_shape)
+
+    # Layer 1 bias.
+    b1_shape <- c(64)
+    b1_coeffs <- weights_pred[, start_index:(start_index + prod(b1_shape) - 1)]
+    b1 <- tf$reshape(b1_coeffs, as.integer(b1_shape))
+    start_index <- start_index + prod(b1_shape)
+
+    # Layer 2 kernel.
+    w2_shape <- c(64, classes)
+    w2_coeffs <- weights_pred[, start_index:(start_index + prod(w2_shape) - 1)]
+    w2 <- tf$reshape(w2_coeffs, as.integer(w2_shape))
+    start_index <- start_index + prod(w2_shape)
+
+    # Layer 2 bias.
+    b2_shape <- c(classes)
+    b2_coeffs <- weights_pred[, start_index:(start_index + prod(b2_shape) - 1)]
+    b2 <- tf$reshape(b2_coeffs, as.integer(b2_shape))
+    start_index <- start_index + prod(b2_shape)
+
+    # Set the weight predictions as the weight variables on the outer model.
+    dense1$kernel <- w1
+    dense1$bias <- b1
+    dense2$kernel <- w2
+    dense2$bias <- b2
+
+    # Inference on the outer model.
+    preds <- main_network(x)
+    loss <- loss_fn(y, preds)
+  })
+
+  grads <- tape$gradient(loss, hypernetwork$trainable_weights)
+  optimizer$apply_gradients(zip_lists(grads, hypernetwork$trainable_weights))
+  loss
+}
+
+losses <- c()  # Keep track of the losses over time.
+coro::loop(for (data in dataset) {
+  x <- data[[1]]
+  y <- data[[2]]
+  loss <- train_step(x, y)
+
+  # Logging.
+  losses[length(losses) + 1] <- as.numeric(loss)
+  if (length(losses) %% 100 == 0) {
+    cat("Step:", length(losses), "Loss:", mean(losses), "\n")
+  }
+  # Stop after 1000 steps.
+  # Training the model to convergence is left
+  # as an exercise to the reader.
+  if (length(losses) >= 1000) {
+    break
+  }
+})
+
## Step: 100 Loss: 2.536778
+## Step: 200 Loss: 2.236472
+## Step: 300 Loss: 2.119417
+## Step: 400 Loss: 2.040341
+## Step: 500 Loss: 1.949125
+## Step: 600 Loss: 1.859384
+## Step: 700 Loss: 1.845726
+## Step: 800 Loss: 1.820594
+## Step: 900 Loss: 1.771334
+## Step: 1000 Loss: 1.730648
+

Implementing arbitrary research ideas with Keras is straightforward +and highly productive. Imagine trying out 25 ideas per day (20 minutes +per experiment on average)!

+

Keras has been designed to go from idea to results as fast as +possible, because we believe this is the key to doing great +research.

+

We hope you enjoyed this quick introduction. Let us know what you +build with Keras!

+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/making_new_layers_and_models_via_subclassing.html b/docs/dev/articles/making_new_layers_and_models_via_subclassing.html new file mode 100644 index 0000000000..d19596003a --- /dev/null +++ b/docs/dev/articles/making_new_layers_and_models_via_subclassing.html @@ -0,0 +1,846 @@ + + + + + + + + +Making new layers and models via subclassing • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

This guide will cover everything you need to know to build your own +subclassed layers and models. In particular, you’ll learn about the +following features:

+
    +
  • The Layer class
  • +
  • The add_weight() method
  • +
  • Trainable and non-trainable weights
  • +
  • The build() method
  • +
  • Making sure your layers can be used with any backend
  • +
  • The add_loss() method
  • +
  • The training argument in call() +
  • +
  • The mask argument in call() +
  • +
  • Making sure your layers can be serialized
  • +
+

Let’s dive in.

+
+
+

Setup +

+
+library(keras3)
+library(tensorflow, exclude = c("set_random_seed", "shape"))
+library(tfdatasets, exclude = "shape")
+
+
+

The Layer class: the combination of state (weights) and +some computation +

+

One of the central abstractions in Keras is the Layer +class. A layer encapsulates both a state (the layer’s “weights”) and a +transformation from inputs to outputs (a “call”, the layer’s forward +pass).

+

Here’s a densely-connected layer. It has two state variables: the +variables w and b.

+
+layer_linear <- Layer("Linear",
+
+  initialize = function(units = 32, input_dim = 32, ...) {
+    super$initialize(...)
+    self$w <- self$add_weight(
+      shape = shape(input_dim, units),
+      initializer = "random_normal",
+      trainable = TRUE
+    )
+    self$b <- self$add_weight(
+      shape = shape(units),
+      initializer = "zeros",
+      trainable = TRUE
+    )
+  },
+
+  call = function(inputs) {
+    op_matmul(inputs, self$w) + self$b
+  }
+)
+

You would use a layer by calling it on some tensor input(s), much +like an R function.

+
+x <- op_ones(c(2, 2))
+linear_layer <- layer_linear(units = 4, input_dim = 2)
+y <- linear_layer(x)
+print(y)
+
## tf.Tensor(
+## [[0.02153057 0.15450525 0.0205495  0.04493225]
+##  [0.02153057 0.15450525 0.0205495  0.04493225]], shape=(2, 4), dtype=float32)
+

Note that the weights w and b are +automatically tracked by the layer upon being set as layer +attributes:

+
+linear_layer$weights
+
## [[1]]
+## <KerasVariable shape=(2, 4), dtype=float32, path=linear/variable>
+##
+## [[2]]
+## <KerasVariable shape=(4), dtype=float32, path=linear/variable_1>
+
+
+

Layers can have non-trainable weights +

+

Besides trainable weights, you can add non-trainable weights to a +layer as well. Such weights are meant not to be taken into account +during backpropagation, when you are training the layer.

+

Here’s how to add and use a non-trainable weight:

+
+layer_compute_sum <- Layer(
+  "ComputeSum",
+  initialize = function(input_dim) {
+    super$initialize()
+    self$total <- self$add_weight(
+      initializer = "zeros",
+      shape = shape(input_dim),
+      trainable = FALSE
+    )
+  },
+  call = function(inputs) {
+    self$total$assign_add(op_sum(inputs, axis = 1))
+    self$total
+  }
+)
+
+x <- op_ones(c(2, 2))
+my_sum <- layer_compute_sum(input_dim = 2)
+y <- my_sum(x)
+print(as.array(y))
+
## [1] 2 2
+
+y <- my_sum(x)
+print(as.array(y))
+
## [1] 4 4
+

It’s part of layer$weights, but it gets categorized as a +non-trainable weight:

+
+cat("weights:", length(my_sum$weights))
+
## weights: 1
+
+cat("non-trainable weights:", length(my_sum$non_trainable_weights))
+
## non-trainable weights: 1
+
+# It's not included in the trainable weights:
+cat("trainable_weights:", length(my_sum$trainable_weights))
+
## trainable_weights: 0
+
+
+

Best practice: deferring weight creation until the shape of the +inputs is known +

+

Our Linear layer above took an input_dim +argument that was used to compute the shape of the weights +w and b in initialize():

+
+layer_linear <- Layer("Linear",
+
+  initialize = function(units = 32, input_dim = 32, ...) {
+    super$initialize(...)
+    self$w <- self$add_weight(
+      shape = shape(input_dim, units),
+      initializer = "random_normal",
+      trainable = TRUE
+    )
+    self$b <- self$add_weight(
+      shape = shape(units),
+      initializer = "zeros",
+      trainable = TRUE
+    )
+  },
+
+  call = function(inputs) {
+    op_matmul(inputs, self$w) + self$b
+  }
+)
+

In many cases, you may not know in advance the size of your inputs, +and you would like to lazily create weights when that value becomes +known, some time after instantiating the layer.

+

In the Keras API, we recommend creating layer weights in the +build(self, inputs_shape) method of your layer. Like +this:

+
+layer_linear <- Layer(
+  "Linear",
+  initialize = function(units = 32, ...) {
+    self$units <- as.integer(units)
+    super$initialize(...)
+  },
+  build = function(input_shape) {
+    self$w <- self$add_weight(
+      shape = shape(tail(input_shape, 1), self$units),
+      initializer = "random_normal",
+      trainable = TRUE
+    )
+    self$b <- self$add_weight(
+      shape = shape(self$units),
+      initializer = "zeros",
+      trainable = TRUE
+    )
+  },
+  call = function(inputs) {
+    op_matmul(inputs, self$w) + self$b
+  }
+)
+

The call() method of your layer will automatically run +build the first time it is called. You now have a layer that’s lazy and +thus easier to use:

+
+# At instantiation, we don't know on what inputs this is going to get called
+linear_layer <- layer_linear(units = 32)
+
+# The layer's weights are created dynamically the first time the layer is called
+y <- linear_layer(x)
+

Implementing build() separately as shown above nicely +separates creating weights only once from using weights in every +call.

+
+
+

Layers are recursively composable +

+

If you assign a Layer instance as an attribute of another Layer, the +outer layer will start tracking the weights created by the inner +layer.

+

We recommend creating such sublayers in the initialize() +method and leave it to the first call() to trigger building +their weights.

+
+MLPBlock <- Layer(
+  "MLPBlock",
+  initialize = function() {
+    super$initialize()
+    self$linear_1 <- layer_linear(units = 32)
+    self$linear_2 <- layer_linear(units = 32)
+    self$linear_3 <- layer_linear(units = 1)
+  },
+  call = function(inputs) {
+    inputs |>
+      self$linear_1() |>
+      activation_relu() |>
+      self$linear_2() |>
+      activation_relu() |>
+      self$linear_3()
+  }
+)
+
+mlp <- MLPBlock()
+# The first call to the `mlp` will create the weights
+y <- mlp(op_ones(shape = c(3, 64)))
+
+cat("weights:", length(mlp$weights), "\n")
+
## weights: 6
+
+cat("trainable weights:", length(mlp$trainable_weights), "\n")
+
## trainable weights: 6
+
+
+

Backend-agnostic layers and backend-specific layers +

+

As long as a layer only uses APIs from the ops namespace +(ie. using functions starting with op_), (or other Keras +namespaces such as activations_*, random_*, or +layer_*), then it can be used with any backend – +TensorFlow, JAX, or PyTorch.

+

All layers you’ve seen so far in this guide work with all Keras +backends.

+

The ops namespace gives you access to:

+
    +
  • The NumPy API, e.g. op_matmul, op_sum, +op_reshape, op_stack, etc.
  • +
  • Neural networks-specific APIs such as op_softmax, +op_conv, op_binary_crossentropy, +op_relu, etc.
  • +
+

You can also use backend-native APIs in your layers (such as +tf$nn functions), but if you do this, then your layer will +only be usable with the backend in question. For instance, you could +write the following JAX-specific layer using jax$numpy:

+
# keras3::install_keras(backend = c("jax"))
+jax <- reticulate::import("jax")
+
+Linear <- new_layer_class(
+  ...
+  call = function(inputs) {
+    jax$numpy$matmul(inputs, self$w) + self$b
+  }
+)
+

This would be the equivalent TensorFlow-specific layer:

+
library(tensorflow)
+
+Linear <- new_layer_class(
+  ...
+  call = function(inputs) {
+    tf$matmul(inputs, self$w) + self$b
+  }
+)
+

And this would be the equivalent PyTorch-specific layer:

+
torch <- reticulate::import("torch")
+
+Linear <- new_layer_class(
+  ...
+  call = function(inputs) {
+    torch$matmul(inputs, self$w) + self$b
+  }
+)
+

Because cross-backend compatibility is a tremendously useful +property, we strongly recommend that you seek to always make your layers +backend-agnostic by leveraging only Keras APIs.

+
+
+

The add_loss() method +

+

When writing the call() method of a layer, you can +create loss tensors that you will want to use later, when writing your +training loop. This is doable by calling +self$add_loss(value):

+
+# A layer that creates an activity regularization loss
+layer_activity_regularization <- Layer(
+  "ActivityRegularizationLayer",
+  initialize = function(rate = 1e-2) {
+    self$rate <- as.numeric(rate)
+    super$initialize()
+  },
+  call = function(inputs) {
+    self$add_loss(self$rate * op_mean(inputs))
+    inputs
+  }
+)
+

These losses (including those created by any inner layer) can be +retrieved via layer$losses. This property is reset at the +start of every call to the top-level layer, so that +layer$losses always contains the loss values created during +the last forward pass.

+
+layer_outer <- Layer(
+  "OuterLayer",
+  initialize = function() {
+    super$initialize()
+    self$activity_reg <- layer_activity_regularization(rate = 1e-2)
+  },
+  call = function(inputs) {
+    self$activity_reg(inputs)
+    inputs
+  }
+)
+
+layer <- layer_outer()
+# No losses yet since the layer has never been called
+cat("losses:", length(layer$losses), "\n")
+
## losses: 0
+
+x <- layer(op_zeros(c(1, 1)))
+# We created one loss value
+cat("losses:", length(layer$losses), "\n")
+
## losses: 1
+
+# `layer$losses` gets reset at the start of each call
+x <- layer(op_zeros(c(1, 1)))
+# This is the loss created during the call above
+cat("losses:", length(layer$losses), "\n")
+
## losses: 1
+

In addition, the loss property also contains +regularization losses created for the weights of any inner layer:

+
+layer_outer_with_kernel_regularizer <- Layer(
+  "OuterLayerWithKernelRegularizer",
+  initialize = function() {
+    super$initialize()
+    self$dense <- layer_dense(units = 32,
+                              kernel_regularizer = regularizer_l2(1e-3))
+  },
+  call = function(inputs) {
+    self$dense(inputs)
+  }
+)
+
+layer <- layer_outer_with_kernel_regularizer()
+x <- layer(op_zeros(c(1, 1)))
+
+# This is `1e-3 * sum(layer$dense$kernel ** 2)`,
+# created by the `kernel_regularizer` above.
+print(layer$losses)
+
## [[1]]
+## tf.Tensor(0.002025157, shape=(), dtype=float32)
+

These losses are meant to be taken into account when writing custom +training loops.

+

They also work seamlessly with fit() (they get +automatically summed and added to the main loss, if any):

+
+inputs <- keras_input(shape = 3)
+outputs <- inputs |> layer_activity_regularization()
+model <- keras_model(inputs, outputs)
+
+# If there is a loss passed in `compile`, the regularization
+# losses get added to it
+model |> compile(optimizer = "adam", loss = "mse")
+model |> fit(random_normal(c(2, 3)), random_normal(c(2, 3)), epochs = 1)
+
## 1/1 - 0s - 129ms/step - loss: 1.8971
+
+# It's also possible not to pass any loss in `compile`,
+# since the model already has a loss to minimize, via the `add_loss`
+# call during the forward pass!
+model |> compile(optimizer = "adam")
+model |> fit(random_normal(c(2, 3)), random_normal(c(2, 3)), epochs = 1)
+
## 1/1 - 0s - 80ms/step - loss: -3.3344e-03
+
+
+

You can optionally enable serialization on your layers +

+

If you need your custom layers to be serializable as part of a Functional model, you can optionally +implement a get_config() method:

+
+layer_linear <- Layer(
+  "Linear",
+  initialize = function(units = 32) {
+    self$units <- as.integer(units)
+    super$initialize()
+  },
+  build = function(input_shape) {
+    self$w <- self$add_weight(
+      shape = shape(tail(input_shape, 1), self$units),
+      initializer = "random_normal",
+      trainable = TRUE
+    )
+    self$b <- self$add_weight(
+      shape = shape(self$units),
+      initializer = "zeros",
+      trainable = TRUE
+    )
+  },
+  call = function(inputs) {
+    op_matmul(inputs, self$w) + self$b
+  },
+  get_config = function() {
+    list(units = self$units)
+  }
+)
+
+# Now you can recreate the layer from its config:
+layer <- layer_linear(units = 64)
+config <- get_config(layer)
+str(config)
+
## List of 1
+##  $ units: int 64
+##  - attr(*, "__class__")=<class '<r-globalenv>.Linear'>
+
+new_layer <- from_config(config)
+

Note that the initialize() method of the base +Layer class takes some keyword arguments, in particular a +name and a dtype. It’s good practice to pass +these arguments to the parent class in initialize() and to +include them in the layer config:

+
+Linear <- new_layer_class(
+  "Linear",
+  initialize = function(units = 32, ...) {
+    self$units <- as.integer(units)
+    super$initialize(...)
+  },
+  build = function(input_shape) {
+    self$w <- self$add_weight(
+      shape = shape(tail(input_shape, 1), self$units),
+      initializer = "random_normal",
+      trainable = TRUE
+    )
+    self$b <- self$add_weight(
+      shape = shape(self$units),
+      initializer = "zeros",
+      trainable = TRUE
+    )
+  },
+  call = function(inputs) {
+    op_matmul(inputs, self$w) + self$b
+  },
+  get_config = function() {
+    list(units = self$units)
+  }
+)
+
+layer <- Linear(units = 64)
+config <- get_config(layer)
+str(config)
+
## List of 1
+##  $ units: int 64
+##  - attr(*, "__class__")=<class '<r-globalenv>.Linear'>
+
+new_layer <- from_config(config)
+

If you need more flexibility when deserializing the layer from its +config, you can also override the from_config() class +method. This is the base implementation of +from_config():

+
+Layer(
+  ...,
+  from_config = function(config) {
+    # calling `__class__`() creates a new instance and calls initialize()
+    do.call(`__class__`, config)
+  }
+)
+

To learn more about serialization and saving, see the complete guide to saving and serializing +models.

+
+
+

Privileged training argument in the call() +method +

+

Some layers, in particular the BatchNormalization layer +and the Dropout layer, have different behaviors during +training and inference. For such layers, it is standard practice to +expose a training (boolean) argument in the +call() method.

+

By exposing this argument in call(), you enable the +built-in training and evaluation loops (e.g. fit()) to +correctly use the layer in training and inference.

+
+layer_custom_dropout <- Layer(
+  "CustomDropout",
+  initialize = function(rate, ...) {
+    super$initialize(...)
+    self$rate <- rate
+    self$seed_generator <- random_seed_generator(1337)
+  },
+  call = function(inputs, training = NULL) {
+    if (isTRUE(training))
+      return(random_dropout(inputs, rate = self$rate,
+                            seed = self.seed_generator))
+    inputs
+  }
+)
+
+
+

Privileged mask argument in the call() +method +

+

The other privileged argument supported by call() is the +mask argument.

+

You will find it in all Keras RNN layers. A mask is a boolean tensor +(one boolean value per timestep in the input) used to skip certain input +timesteps when processing timeseries data.

+

Keras will automatically pass the correct mask argument +to call() for layers that support it, when a mask is +generated by a prior layer. Mask-generating layers are the +Embedding layer configured with +mask_zero = TRUE, and the Masking layer.

+
+
+

The Model class +

+

In general, you will use the Layer class to define inner +computation blocks, and will use the Model class to define +the outer model – the object you will train.

+

For instance, in a ResNet50 model, you would have several ResNet +blocks subclassing Layer, and a single Model +encompassing the entire ResNet50 network.

+

The Model class has the same API as Layer, +with the following differences:

+
    +
  • It exposes built-in training, evaluation, and prediction loops +(fit(), evaluate(), +predict()).
  • +
  • It exposes the list of its inner layers, via the +model$layers property.
  • +
  • It exposes saving and serialization APIs (save(), +save_weights()…)
  • +
+

Effectively, the Layer class corresponds to what we +refer to in the literature as a “layer” (as in “convolution layer” or +“recurrent layer”) or as a “block” (as in “ResNet block” or “Inception +block”).

+

Meanwhile, the Model class corresponds to what is +referred to in the literature as a “model” (as in “deep learning model”) +or as a “network” (as in “deep neural network”).

+

So if you’re wondering, “should I use the Layer class or +the Model class?”, ask yourself: will I need to call +fit() on it? Will I need to call save() on it? +If so, go with Model. If not (either because your class is +just a block in a bigger system, or because you are writing training +& saving code yourself), use Layer.

+

For instance, we could take our mini-resnet example above, and use it +to build a Model that we could train with +fit(), and that we could save with +save_weights():

+
+ResNet <- Model(
+  "ResNet",
+  initialize = function(num_classes = 1000, ...) {
+    super$initialize(...)
+    self$block_1 <- layer_resnet_block()
+    self$block_2 <- layer_resnet_block()
+    self$global_pool <- layer_global_average_pooling_2d()
+    self$classifier <- layer_dense(num_classes)
+  },
+  call = function(inputs) {
+    inputs |>
+      self$block_1() |>
+      self$block_2() |>
+      self$global_pool() |>
+      self$classifier()
+  }
+)
+
+resnet <- ResNet()
+dataset <- ...
+resnet |> fit(dataset, epochs=10)
+resnet |> save_model("filepath.keras")
+
+
+

Putting it all together: an end-to-end example +

+

Here’s what you’ve learned so far:

+
    +
  • A Layer encapsulate a state (created in +initialize() or build()) and some computation +(defined in call()).
  • +
  • Layers can be recursively nested to create new, bigger computation +blocks.
  • +
  • Layers are backend-agnostic as long as they only use Keras APIs. You +can use backend-native APIs (such as jax$numpy, +torch$nn or tf$nn), but then your layer will +only be usable with that specific backend.
  • +
  • Layers can create and track losses (typically regularization losses) +via add_loss().
  • +
  • The outer container, the thing you want to train, is a +Model. A Model is just like a +Layer, but with added training and serialization +utilities.
  • +
+

Let’s put all of these things together into an end-to-end example: +we’re going to implement a Variational AutoEncoder (VAE) in a +backend-agnostic fashion – so that it runs the same with TensorFlow, +JAX, and PyTorch. We’ll train it on MNIST digits.

+

Our VAE will be a subclass of Model, built as a nested +composition of layers that subclass Layer. It will feature +a regularization loss (KL divergence).

+
+layer_sampling <- Layer(
+  "Sampling",
+  initialize = function(...) {
+    super$initialize(...)
+    self$seed_generator <- random_seed_generator(1337)
+  },
+  call = function(inputs) {
+    c(z_mean, z_log_var) %<-% inputs
+    batch <- op_shape(z_mean)[[1]]
+    dim <- op_shape(z_mean)[[2]]
+    epsilon <- random_normal(shape = c(batch, dim),
+                             seed=self$seed_generator)
+    z_mean + op_exp(0.5 * z_log_var) * epsilon
+  }
+)
+
+# Maps MNIST digits to a triplet (z_mean, z_log_var, z).
+layer_encoder <- Layer(
+  "Encoder",
+  initialize = function(latent_dim = 32, intermediate_dim = 64, ...) {
+    super$initialize(...)
+    self$dense_proj <-
+      layer_dense(units = intermediate_dim,  activation = "relu")
+    self$dense_mean <- layer_dense(units = latent_dim)
+    self$dense_log_var <- layer_dense(units = latent_dim)
+    self$sampling <- layer_sampling()
+  },
+  call = function(inputs) {
+    x <- self$dense_proj(inputs)
+    z_mean <- self$dense_mean(x)
+    z_log_var <- self$dense_log_var(x)
+    z <- self$sampling(list(z_mean, z_log_var))
+    list(z_mean, z_log_var, z)
+  }
+)
+
+# Converts z, the encoded digit vector, back into a readable digit.
+layer_decoder <- Layer(
+  "Decoder",
+  initialize = function(original_dim, intermediate_dim = 64, ...) {
+    super$initialize(...)
+    self$dense_proj <-
+      layer_dense(units = intermediate_dim, activation = "relu")
+    self$dense_output <-
+      layer_dense(units = original_dim, activation = "sigmoid")
+  },
+  call = function(inputs) {
+    x <- self$dense_proj(inputs)
+    self$dense_output(x)
+  }
+)
+
+# Combines the encoder and decoder into an end-to-end model for training.
+VariationalAutoEncoder <- Model(
+  "VariationalAutoEncoder",
+
+  initialize = function(original_dim, intermediate_dim = 64, latent_dim = 32,
+                        name = "autoencoder", ...) {
+    super$initialize(name = name, ...)
+    self$original_dim <- original_dim
+    self$encoder <- layer_encoder(latent_dim = latent_dim,
+                            intermediate_dim = intermediate_dim)
+    self$decoder <- layer_decoder(original_dim = original_dim,
+                            intermediate_dim = intermediate_dim)
+  },
+
+  call = function(inputs) {
+    c(z_mean, z_log_var, z) %<-% self$encoder(inputs)
+    reconstructed <- self$decoder(z)
+    # Add KL divergence regularization loss.
+    kl_loss <- -0.5 * op_mean(z_log_var - op_square(z_mean) - op_exp(z_log_var) + 1)
+    self$add_loss(kl_loss)
+    reconstructed
+  }
+)
+

Let’s train it on MNIST using the fit() API:

+
+c(c(x_train, .), .) %<-% dataset_mnist()
+x_train <- x_train |>
+  op_reshape(c(60000, 784)) |>
+  op_cast("float32") |>
+  op_divide(255)
+
+original_dim <- 784
+vae <- VariationalAutoEncoder(
+  original_dim = 784,
+  intermediate_dim = 64,
+  latent_dim = 32
+)
+
+optimizer <- optimizer_adam(learning_rate = 1e-3)
+vae |> compile(optimizer, loss = loss_mean_squared_error())
+
+vae |> fit(x_train, x_train, epochs = 2, batch_size = 64)
+
## Epoch 1/2
+## 938/938 - 5s - 5ms/step - loss: 0.0748
+## Epoch 2/2
+## 938/938 - 1s - 873us/step - loss: 0.0676
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/sequential_model.html b/docs/dev/articles/sequential_model.html new file mode 100644 index 0000000000..c6c3305302 --- /dev/null +++ b/docs/dev/articles/sequential_model.html @@ -0,0 +1,535 @@ + + + + + + + + +The Sequential model • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Setup +

+ +
+
+

When to use a Sequential model +

+

A Sequential model is appropriate for a plain +stack of layers where each layer has exactly one input +tensor and one output tensor.

+

Schematically, the following Sequential model:

+
+model <- keras_model_sequential() |>
+  layer_dense(units = 2, activation = "relu", name = "layer1") |>
+  layer_dense(units = 3, activation = "relu", name = "layer2") |>
+  layer_dense(units = 4, name = "layer3")
+
+# Call model on a test input
+x <- op_ones(c(3, 3))
+y <- model(x)
+

is equivalent to this function:

+
+# Create 3 layers
+layer1 <- layer_dense(units = 2, activation="relu", name="layer1")
+layer2 <- layer_dense(units = 3, activation="relu", name="layer2")
+layer3 <- layer_dense(units = 4, name="layer3")
+
+# Call layers on a test input
+x <- op_ones(c(3, 3))
+y <- x |> layer1() |> layer2() |> layer3()
+

A Sequential model is not appropriate when:

+
    +
  • Your model has multiple inputs or multiple outputs
  • +
  • Any of your layers has multiple inputs or multiple outputs
  • +
  • You need to do layer sharing
  • +
  • You want non-linear topology (e.g. a residual connection, a +multi-branch model)
  • +
+
+
+

Creating a Sequential model +

+

You can create a Sequential model by piping layers into the +keras_model_sequential() object:

+
+model <- keras_model_sequential() |>
+  layer_dense(units = 2, activation = "relu") |>
+  layer_dense(units = 3, activation = "relu") |>
+  layer_dense(units = 4)
+

or by passing a list of layers to +keras_model_sequential():

+
+model <- keras_model_sequential(layers = list(
+  layer_dense(units = 2, activation = "relu"),
+  layer_dense(units = 3, activation = "relu"),
+  layer_dense(units = 4)
+))
+

Its layers are accessible via the layers attribute:

+
+model$layers
+
## [[1]]
+## <Dense name=dense_3, built=False>
+##  signature: (*args, **kwargs)
+##
+## [[2]]
+## <Dense name=dense_4, built=False>
+##  signature: (*args, **kwargs)
+##
+## [[3]]
+## <Dense name=dense_5, built=False>
+##  signature: (*args, **kwargs)
+

You can also create a Sequential model incrementally:

+
+model <- keras_model_sequential()
+model |> layer_dense(units = 2, activation="relu")
+model |> layer_dense(units = 3, activation="relu")
+model |> layer_dense(units = 4)
+

Note that there’s also a corresponding pop_layer() +method to remove layers: a Sequential model behaves very much like a +stack of layers.

+
+model |> pop_layer()
+length(model$layers)  # 2
+
## [1] 2
+

Also note that the Sequential constructor accepts a name +argument, just like any layer or model in Keras. This is useful to +annotate TensorBoard graphs with semantically meaningful names.

+
+model <- keras_model_sequential(name = "my_sequential")
+model |> layer_dense(units = 2, activation="relu", name = "layer1")
+model |> layer_dense(units = 3, activation="relu", name = "layer2")
+model |> layer_dense(units = 4, name = "layer3")
+
+
+

Specifying the input shape in advance +

+

Generally, all layers in Keras need to know the shape of their inputs +in order to be able to create their weights. So when you create a layer +like this, initially, it has no weights:

+
+layer <- layer_dense(units = 3)
+layer$weights  # Empty
+
## list()
+

It creates its weights the first time it is called on an input, since +the shape of the weights depends on the shape of the inputs:

+
+# Call layer on a test input
+x <- op_ones(c(1, 4))
+y <- layer(x)
+layer$weights  # Now it has weights, of shape (4, 3) and (3,)
+
## [[1]]
+## <KerasVariable shape=(4, 3), dtype=float32, path=dense_9/kernel>
+##
+## [[2]]
+## <KerasVariable shape=(3), dtype=float32, path=dense_9/bias>
+

Naturally, this also applies to Sequential models. When you +instantiate a Sequential model without an input shape, it isn’t “built”: +it has no weights (and calling model$weights results in an +error stating just this). The weights are created when the model first +sees some input data:

+
+model <- keras_model_sequential() |>
+  layer_dense(units = 2, activation = "relu") |>
+  layer_dense(units = 3, activation = "relu") |>
+  layer_dense(units = 4)
+# No weights at this stage!
+
+# At this point, you can't do this:
+# model$weights
+
+
+# Call the model on a test input
+x <- op_ones(c(1, 4))
+y <- model(x)
+length(model$weights)
+
## [1] 6
+

Once a model is “built”, you can call its summary() +method to display its contents:

+
+summary(model)
+
## Model: "sequential_4"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ dense_10 (Dense)                │ (1, 2)                 │            10
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_11 (Dense)                │ (1, 3)                 │             9
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_12 (Dense)                │ (1, 4)                 │            16
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 35 (140.00 B)
+##  Trainable params: 35 (140.00 B)
+##  Non-trainable params: 0 (0.00 B)
+

However, it can be very useful when building a Sequential model +incrementally to be able to display the summary of the model so far, +including the current output shape. In this case, you should start your +model by passing an input_shape argument to your model, so +that it knows its input shape from the start:

+
+model <- keras_model_sequential(input_shape = 4) |>
+  layer_dense(units = 2, activation = "relu")
+summary(model)
+
## Model: "sequential_5"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ dense_13 (Dense)                │ (None, 2)              │            10
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 10 (40.00 B)
+##  Trainable params: 10 (40.00 B)
+##  Non-trainable params: 0 (0.00 B)
+
+model$layers
+
## [[1]]
+## <Dense name=dense_13, built=True>
+##  signature: (*args, **kwargs)
+

Models built with a predefined input shape like this always have +weights (even before seeing any data) and always have a defined output +shape.

+

In general, it’s a recommended best practice to always specify the +input shape of a Sequential model in advance if you know what it is.

+
+
+

A common debugging workflow: add layers + +summary() +

+

When building a new Sequential architecture, it’s useful to +incrementally stack layers with |> and frequently print +model summaries. For instance, this enables you to monitor how a stack +of Conv2D and MaxPooling2D layers is +downsampling image feature maps:

+
+model <- keras_model_sequential(input_shape = c(250, 250, 3)) |>
+  layer_conv_2d(filters = 32, kernel_size = 5, strides = 2, activation = "relu") |>
+  layer_conv_2d(filters = 32, kernel_size = 3, activation = "relu") |>
+  layer_max_pooling_2d(pool_size = c(3, 3))
+
+# Can you guess what the current output shape is at this point? Probably not.
+# Let's just print it:
+summary(model)
+
## Model: "sequential_6"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ conv2d (Conv2D)                 │ (None, 123, 123, 32)   │         2,432
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_1 (Conv2D)               │ (None, 121, 121, 32)   │         9,248
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d (MaxPooling2D)    │ (None, 40, 40, 32)     │             0
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 11,680 (45.62 KB)
+##  Trainable params: 11,680 (45.62 KB)
+##  Non-trainable params: 0 (0.00 B)
+
+# The answer was: (40, 40, 32), so we can keep downsampling...
+
+model |>
+  layer_conv_2d(filters = 32, kernel_size = 3, activation = "relu") |>
+  layer_conv_2d(filters = 32, kernel_size = 3, activation = "relu") |>
+  layer_max_pooling_2d(pool_size = 3) |>
+  layer_conv_2d(filters = 32, kernel_size = 3, activation = "relu") |>
+  layer_conv_2d(filters = 32, kernel_size = 3, activation = "relu") |>
+  layer_max_pooling_2d(pool_size = 2)
+
+# And now?
+summary(model)
+
## Model: "sequential_6"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ conv2d (Conv2D)                 │ (None, 123, 123, 32)   │         2,432
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_1 (Conv2D)               │ (None, 121, 121, 32)   │         9,248
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d (MaxPooling2D)    │ (None, 40, 40, 32)     │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_2 (Conv2D)               │ (None, 38, 38, 32)     │         9,248
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_3 (Conv2D)               │ (None, 36, 36, 32)     │         9,248
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d_1 (MaxPooling2D)  │ (None, 12, 12, 32)     │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_4 (Conv2D)               │ (None, 10, 10, 32)     │         9,248
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_5 (Conv2D)               │ (None, 8, 8, 32)       │         9,248
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ max_pooling2d_2 (MaxPooling2D)  │ (None, 4, 4, 32)       │             0
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 48,672 (190.12 KB)
+##  Trainable params: 48,672 (190.12 KB)
+##  Non-trainable params: 0 (0.00 B)
+
+# Now that we have 4x4 feature maps, time to apply global max pooling.
+model |>
+  layer_global_max_pooling_2d()
+
+# Finally, we add a classification layer.
+model |>
+  layer_dense(units = 10, activation = "softmax")
+

Very practical, right?

+

Note that |> is equivalent to calling +model$add(), it modifies the model in-place, so you don’t +need to reassign the model symbol at each step.

+
+
+

What to do once you have a model +

+

Once your model architecture is ready, you will want to:

+ +
+
+

Feature extraction with a Sequential model +

+

Once a Sequential model has been built, it behaves like a Functional API model. This means that +every layer has an input and output attribute. +These attributes can be used to do neat things, like quickly creating a +model that extracts the outputs of all intermediate layers in a +Sequential model:

+
+initial_model <- keras_model_sequential(input_shape = c(250, 250, 3)) |>
+  layer_conv_2d(filters = 32, kernel_size = 5, strides = 2, activation = "relu") |>
+  layer_conv_2d(filters = 32, kernel_size = 3, activation = "relu") |>
+  layer_conv_2d(filters = 32, kernel_size = 3, activation = "relu")
+
+
+feature_extractor <- keras_model(
+    inputs = initial_model$inputs,
+    outputs = lapply(initial_model$layers, function(x) x$output),
+)
+
+# Call feature extractor on test input.
+x <- op_ones(c(1, 250, 250, 3))
+features <- feature_extractor(x)
+

Here’s a similar example that only extract features from one +layer:

+
+initial_model <-
+  keras_model_sequential(input_shape = c(250, 250, 3)) |>
+  layer_conv_2d(filters = 32, kernel_size = 5, strides = 2,
+                activation = "relu") |>
+  layer_conv_2d(filters = 32, kernel_size = 3, activation = "relu",
+                name = "my_intermediate_layer") |>
+  layer_conv_2d(filters = 32, kernel_size = 3, activation = "relu")
+
+feature_extractor <- keras_model(
+  inputs = initial_model$inputs,
+  outputs = get_layer(initial_model, "my_intermediate_layer")$output,
+)
+
+# Call feature extractor on test input.
+x <- op_ones(c(1, 250, 250, 3))
+features <- feature_extractor(x)
+
+
+

Transfer learning with a Sequential model +

+

Transfer learning consists of freezing the bottom layers in a model +and only training the top layers. If you aren’t familiar with it, make +sure to read our guide to transfer +learning.

+

Here are two common transfer learning blueprint involving Sequential +models.

+

First, let’s say that you have a Sequential model, and you want to +freeze all layers except the last one. In this case, you can call +freeze_weights(). Alternatively, you can iterate over +model$layers and set +layer$trainable <- FALSE on each layer, except the last +one. Like this:

+
+model <- keras_model_sequential(input_shape = 784) |>
+  layer_dense(units = 32, activation = "relu") |>
+  layer_dense(units = 32, activation = "relu") |>
+  layer_dense(units = 32, activation = "relu") |>
+  layer_dense(units = 10)
+
+# Presumably you would want to first load pre-trained weights.
+model |> load_model_weights(...)
+
+# Freeze all layers except the last one.
+model |> freeze_weights(from = 1, to = -2)
+model # note the "Trainable" column now visible in the summary table
+
## Model: "sequential_9"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━┓
+## ┃ Layer (type)                 Output Shape              Param #  Trai… 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━┩
+## │ dense_15 (Dense)            │ (None, 32)            │     25,120N
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ dense_16 (Dense)            │ (None, 32)            │      1,056N
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ dense_17 (Dense)            │ (None, 32)            │      1,056N
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ dense_18 (Dense)            │ (None, 10)            │        330Y
+## └─────────────────────────────┴───────────────────────┴────────────┴───────┘
+##  Total params: 27,562 (107.66 KB)
+##  Trainable params: 330 (1.29 KB)
+##  Non-trainable params: 27,232 (106.38 KB)
+
+# Another way to freeze all layers except the last one.
+for (layer in model$layers[-length(model$layers)]) {
+  layer$trainable <- FALSE
+}
+
+# Recompile and train (this will only update the weights of the last layer).
+model |> compile(...)
+model |> fit(...)
+

Another common blueprint is to use a Sequential model to stack a +pre-trained model and some freshly initialized classification layers. +Like this:

+
+# Load a convolutional base with pre-trained weights
+base_model <- application_xception(weights = 'imagenet',
+                                   include_top = FALSE,
+                                   pooling = 'avg')
+
+# Freeze the base model
+freeze_weights(base_model)
+
+# Use a Sequential model to add a trainable classifier on top
+model <- keras_model_sequential() |>
+  base_model() |>
+  layer_dense(1000)
+
+# Compile & train
+model |> compile(...)
+model |> fit(...)
+

If you do transfer learning, you will probably find yourself +frequently using these two patterns.

+

That’s about all you need to know about Sequential models!

+

To find out more about building models in Keras, see:

+ +
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/serialization_and_saving.html b/docs/dev/articles/serialization_and_saving.html new file mode 100644 index 0000000000..bbef5b770d --- /dev/null +++ b/docs/dev/articles/serialization_and_saving.html @@ -0,0 +1,976 @@ + + + + + + + + +Save, serialize, and export models • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

A Keras model consists of multiple components:

+
    +
  • The architecture, or configuration, which specifies what layers the +model contain, and how they’re connected.
  • +
  • A set of weights values (the “state of the model”).
  • +
  • An optimizer (defined by compiling the model).
  • +
  • A set of losses and metrics (defined by compiling the model).
  • +
+

The Keras API saves all of these pieces together in a unified format, +marked by the .keras extension. This is a zip archive +consisting of the following:

+
    +
  • A JSON-based configuration file (config.json): Records of model, +layer, and other trackables’ configuration.
  • +
  • A H5-based state file, such as model.weights.h5 (for +the whole model), with directory keys for layers and their weights.
  • +
  • A metadata file in JSON, storing things such as the current Keras +version.
  • +
+

Let’s take a look at how this works.

+
+
+

How to save and load a model +

+

If you only have 10 seconds to read this guide, here’s what you need +to know.

+

Saving a Keras model:

+
+# Get model (Sequential, Functional Model, or Model subclass)
+model <- ...
+
+# The filename needs to end with the .keras extension
+model |> save_model('path/to/location.keras')
+

Loading the model back:

+
+model <- load_model('path/to/location.keras')
+

Now, let’s look at the details.

+
+
+

Setup +

+ +
+
+

Saving +

+

This section is about saving an entire model to a single file. The +file will include:

+
    +
  • The model’s architecture/config
  • +
  • The model’s weight values (which were learned during training)
  • +
  • The model’s compilation information (if compile() was +called)
  • +
  • The optimizer and its state, if any (this enables you to restart +training where you left)
  • +
+
+

APIs +

+

You can save a model with save_model(). You can load it +back with load_model().

+

The only supported format in Keras 3 is the “Keras v3” format, which +uses the .keras extension.

+

Example:

+
+get_model <- function() {
+  # Create a simple model.
+  inputs <- keras_input(shape(32))
+  outputs <- inputs |> layer_dense(1)
+  model <-  keras_model(inputs, outputs)
+  model |> compile(optimizer = optimizer_adam(), loss = "mean_squared_error")
+  model
+}
+
+model <- get_model()
+
+# Train the model.
+test_input <- random_uniform(c(128, 32))
+test_target <- random_uniform(c(128, 1))
+model |> fit(test_input, test_target)
+
+# Calling `save('my_model.keras')` creates a zip archive `my_model.keras`.
+model |> save_model("my_model.keras")
+
+# It can be used to reconstruct the model identically.
+reconstructed_model <- load_model("my_model.keras")
+
+# Let's check:
+stopifnot(all.equal(
+  model |> predict(test_input),
+  reconstructed_model |> predict(test_input)
+))
+
+
+

Custom objects +

+

This section covers the basic workflows for handling custom layers, +functions, and models in Keras saving and reloading.

+

When saving a model that includes custom objects, such as a +subclassed Layer, you must define a +get_config() method on the object class. If the arguments +passed to the constructor (initialize() method) of the +custom object aren’t simple objects (anything other than types like +ints, strings, etc.), then you must also explicitly +deserialize these arguments in the from_config() class +method.

+

Like this:

+
+layer_custom <- Layer(
+  "CustomLayer",
+  initialize = function(sublayer, ...) {
+    super$initialize(...)
+    self$sublayer <- sublayer
+  },
+  call = function(x) {
+    self$sublayer(x)
+  },
+  get_config = function() {
+    base_config <- super$get_config()
+    config <- list(
+      sublayer = serialize_keras_object(self$sublayer)
+    )
+    c(base_config, config)
+  },
+  from_config = function(cls, config) {
+    sublayer_config <- config$sublayer
+    sublayer <- deserialize_keras_object(sublayer_config)
+    cls(sublayer, !!!config)
+  }
+)
+

Please see the Defining the config methods +section for more details and examples.

+

The saved .keras file is lightweight and does not store +the Python code for custom objects. Therefore, to reload the model, +load_model requires access to the definition of any custom +objects used through one of the following methods:

+
    +
  1. Registering custom objects (preferred),
  2. +
  3. Passing custom objects directly when loading, or
  4. +
  5. Using a custom object scope
  6. +
+

Below are examples of each workflow:

+
+

Registering custom objects (preferred) +

+

This is the preferred method, as custom object registration greatly +simplifies saving and loading code. Calling +register_keras_serializable() on a custom object registers +the object globally in a master list, allowing Keras to recognize the +object when loading the model.

+

Let’s create a custom model involving both a custom layer and a +custom activation function to demonstrate this.

+

Example:

+
+# Clear all previously registered custom objects
+set_custom_objects(clear = TRUE)
+
## named list()
+
+layer_custom <- Layer(
+  "CustomLayer",
+  initialize = function(self, factor) {
+    super$initialize()
+    self$factor = factor
+  },
+
+  call = function(self, x) {
+    x * self$factor
+  },
+
+  get_config = function(self) {
+    list(factor = self$factor)
+  }
+)
+
+# Upon registration, you can optionally specify a package or a name.
+# If left blank, the package defaults to "Custom" and the name defaults to
+# the class name.
+register_keras_serializable(layer_custom, package = "MyLayers")
+
+custom_fn <- keras3:::py_func2(function(x) x^2, name = "custom_fn", convert = TRUE)
+
+register_keras_serializable(custom_fn, name="custom_fn", package="my_package")
+
+
+# Create the model.
+get_model <- function() {
+  inputs <- keras_input(shape(4))
+  mid <- inputs |> layer_custom(0.5)
+  outputs <- mid |> layer_dense(1, activation = custom_fn)
+  model <- keras_model(inputs, outputs)
+  model |> compile(optimizer = "rmsprop", loss = "mean_squared_error")
+  model
+}
+
+
+# Train the model.
+train_model <- function(model) {
+  input <- random_uniform(c(4, 4))
+  target <- random_uniform(c(4, 1))
+  model |> fit(input, target, verbose = FALSE, epochs = 1)
+  model
+}
+
+test_input <- random_uniform(c(4, 4))
+test_target <- random_uniform(c(4, 1))
+
+model <- get_model() |> train_model()
+model |> save_model("custom_model.keras", overwrite = TRUE)
+
+# Now, we can simply load without worrying about our custom objects.
+reconstructed_model <- load_model("custom_model.keras")
+
+# Let's check:
+stopifnot(all.equal(
+  model |> predict(test_input, verbose = FALSE),
+  reconstructed_model |> predict(test_input, verbose = FALSE)
+))
+
+
+

Passing custom objects to load_model() +

+
+model <- get_model() |> train_model()
+
+# Calling `save_model('my_model.keras')` creates a zip archive `my_model.keras`.
+model |> save_model("custom_model.keras", overwrite = TRUE)
+
+# Upon loading, pass a named list containing the custom objects used in the
+# `custom_objects` argument of `load_model()`.
+reconstructed_model <-  load_model(
+ "custom_model.keras",
+  custom_objects = list(CustomLayer = layer_custom,
+                        custom_fn = custom_fn),
+)
+
+# Let's check:
+stopifnot(all.equal(
+  model |> predict(test_input, verbose = FALSE),
+  reconstructed_model |> predict(test_input, verbose = FALSE)
+))
+
+
+

Using a custom object scope +

+

Any code within the custom object scope will be able to recognize the +custom objects passed to the scope argument. Therefore, loading the +model within the scope will allow the loading of our custom objects.

+

Example:

+
+model <- get_model() |> train_model()
+model |> save_model("custom_model.keras", overwrite = TRUE)
+
+# Pass the custom objects dictionary to a custom object scope and place
+# the `keras.models.load_model()` call within the scope.
+custom_objects <- list(CustomLayer = layer_custom, custom_fn = custom_fn)
+
+with_custom_object_scope(custom_objects, {
+  reconstructed_model <- load_model("custom_model.keras")
+})
+
+# Let's check:
+stopifnot(all.equal(
+  model |> predict(test_input, verbose = FALSE),
+  reconstructed_model |> predict(test_input, verbose = FALSE)
+))
+
+
+
+

Model serialization +

+

This section is about saving only the model’s configuration, without +its state. The model’s configuration (or architecture) specifies what +layers the model contains, and how these layers are connected. If you +have the configuration of a model, then the model can be created with a +freshly initialized state (no weights or compilation information).

+
+

APIs +

+

The following serialization APIs are available:

+
    +
  • +clone_model(model): make a (randomly initialized) copy +of a model.
  • +
  • +get_config() and cls.from_config(): +retrieve the configuration of a layer or model, and recreate a model +instance from its config, respectively.
  • +
  • +keras.models.model_to_json() and +keras.models.model_from_json(): similar, but as JSON +strings.
  • +
  • +keras.saving.serialize_keras_object(): retrieve the +configuration any arbitrary Keras object.
  • +
  • +keras.saving.deserialize_keras_object(): recreate an +object instance from its configuration.
  • +
+
+
+

In-memory model cloning +

+

You can do in-memory cloning of a model via +clone_model(). This is equivalent to getting the config +then recreating the model from its config (so it does not preserve +compilation information or layer weights values).

+

Example:

+
+new_model <- clone_model(model)
+
+
+

+get_config() and from_config() +

+

Calling get_config(model) or +get_config(layer) will return a named list containing the +configuration of the model or layer, respectively. You should define +get_config() to contain arguments needed for the +initialize() method of the model or layer. At loading time, +the from_config(config) method will then call +initialize() with these arguments to reconstruct the model +or layer.

+

Layer example:

+
+layer <- layer_dense(, 3, activation="relu")
+layer_config <- get_config(layer)
+str(layer_config)
+
## List of 12
+##  $ name              : chr "dense_4"
+##  $ trainable         : logi TRUE
+##  $ dtype             :List of 4
+##   ..$ module         : chr "keras"
+##   ..$ class_name     : chr "FloatDTypePolicy"
+##   ..$ config         :List of 1
+##   .. ..$ name: chr "float32"
+##   ..$ registered_name: NULL
+##  $ units             : int 3
+##  $ activation        : chr "relu"
+##  $ use_bias          : logi TRUE
+##  $ kernel_initializer:List of 4
+##   ..$ module         : chr "keras.initializers"
+##   ..$ class_name     : chr "GlorotUniform"
+##   ..$ config         :List of 1
+##   .. ..$ seed: NULL
+##   ..$ registered_name: NULL
+##  $ bias_initializer  :List of 4
+##   ..$ module         : chr "keras.initializers"
+##   ..$ class_name     : chr "Zeros"
+##   ..$ config         : Named list()
+##   ..$ registered_name: NULL
+##  $ kernel_regularizer: NULL
+##  $ bias_regularizer  : NULL
+##  $ kernel_constraint : NULL
+##  $ bias_constraint   : NULL
+##  - attr(*, "__class__")=<class 'keras.src.layers.core.dense.Dense'>
+

Now let’s reconstruct the layer using the from_config() +method:

+
+new_layer <- from_config(layer_config)
+

Sequential model example:

+
+model <- keras_model_sequential(input_shape = c(32)) |>
+  layer_dense(1)
+config <- get_config(model)
+new_model <- from_config(config)
+

Functional model example:

+
+inputs <- keras_input(c(32))
+outputs <- inputs |> layer_dense(1)
+model <- keras_model(inputs, outputs)
+config <- get_config(model)
+new_model <- from_config(config)
+
+
+

+save_model_config() and +load_model_config() +

+

This is similar to get_config / +from_config, except it turns the model into a JSON file, +which can then be loaded without the original model class. It is also +specific to models, it isn’t meant for layers.

+

Example:

+
+model <- keras_model_sequential(input_shape = c(32)) |>
+  layer_dense(1)
+save_model_config(model, "model_config.json")
+new_model <- load_model_config("model_config.json")
+
+unlink("model_config.json")
+
+
+

Arbitrary object serialization and deserialization +

+

The serialize_keras_object() and +deserialize_keras_object() APIs are general-purpose APIs +that can be used to serialize or deserialize any Keras object and any +custom object. It is at the foundation of saving model architecture and +is behind all serialize()/deserialize() calls +in keras.

+

Example:

+
+my_reg <- regularizer_l1(0.005)
+config <- serialize_keras_object(my_reg)
+str(config)
+
## List of 4
+##  $ module         : chr "keras.regularizers"
+##  $ class_name     : chr "L1"
+##  $ config         :List of 1
+##   ..$ l1: num 0.005
+##  $ registered_name: NULL
+

Note the serialization format containing all the necessary +information for proper reconstruction:

+
    +
  • +module containing the name of the Keras module or other +identifying module the object comes from
  • +
  • +class_name containing the name of the object’s +class.
  • +
  • +config with all the information needed to reconstruct +the object
  • +
  • +registered_name for custom objects. See here.
  • +
+

Now we can reconstruct the regularizer.

+
+new_reg <- deserialize_keras_object(config)
+new_reg
+
## <keras.src.regularizers.regularizers.L1 object>
+##  signature: (x)
+
+
+
+

Model weights saving +

+

You can choose to only save & load a model’s weights. This can be +useful if:

+
    +
  • You only need the model for inference: in this case you won’t need +to restart training, so you don’t need the compilation information or +optimizer state.
  • +
  • You are doing transfer learning: in this case you will be training a +new model reusing the state of a prior model, so you don’t need the +compilation information of the prior model.
  • +
+
+

APIs for in-memory weight transfer +

+

Weights can be copied between different objects by using +get_weights() and set_weights():

+
    +
  • +get_weights(<layer>): Returns a list of arrays of +weight values.
  • +
  • +set_weights(<layer>weights): Sets the model/layer +weights to the values provided (as arrays).
  • +
+

Examples:

+

Transferring weights from one layer to another, in +memory

+
+create_layer <- function() {
+  layer <- layer_dense(, 64, activation = "relu", name = "dense_2")
+  layer$build(shape(NA, 784))
+  layer
+}
+
+layer_1 <- create_layer()
+layer_2 <- create_layer()
+
+# Copy weights from layer 1 to layer 2
+layer_2 |> set_weights(get_weights(layer_1))
+

Transferring weights from one model to another model with +a compatible architecture, in memory

+
+# Create a simple functional model
+inputs <- keras_input(shape=c(784), name="digits")
+outputs <- inputs |>
+  layer_dense(64, activation = "relu", name = "dense_1") |>
+  layer_dense(64, activation = "relu", name = "dense_2") |>
+  layer_dense(10, name = "predictions")
+functional_model <- keras_model(inputs = inputs, outputs = outputs,
+                               name = "3_layer_mlp")
+
+# Define a subclassed model with the same architecture
+SubclassedModel <- new_model_class(
+  "SubclassedModel",
+  initialize = function(output_dim, name = NULL) {
+    super$initialize(name = name)
+    self$output_dim <- output_dim |> as.integer()
+    self$dense_1 <- layer_dense(, 64, activation = "relu",
+                                name = "dense_1")
+    self$dense_2 <- layer_dense(, 64, activation = "relu",
+                                name = "dense_2")
+    self$dense_3 <- layer_dense(, self$output_dim,
+                                name = "predictions")
+  },
+
+  call = function(inputs) {
+    inputs |>
+      self$dense_1() |>
+      self$dense_2() |>
+      self$dense_3()
+  },
+
+  get_config = function(self) {
+    list(output_dim = self$output_dim,
+         name = self$name)
+  }
+)
+
+
+subclassed_model <- SubclassedModel(10)
+# Call the subclassed model once to create the weights.
+subclassed_model(op_ones(c(1, 784))) |> invisible()
+
+# Copy weights from functional_model to subclassed_model.
+set_weights(subclassed_model, get_weights(functional_model))
+
+stopifnot(all.equal(
+  get_weights(functional_model),
+  get_weights(subclassed_model)
+))
+

The case of stateless layers

+

Because stateless layers do not change the order or number of +weights, models can have compatible architectures even if there are +extra/missing stateless layers.

+
+input <- keras_input(shape = c(784), name = "digits")
+output <- input |>
+  layer_dense(64, activation = "relu", name = "dense_1") |>
+  layer_dense(64, activation = "relu", name = "dense_2") |>
+  layer_dense(10, name = "predictions")
+functional_model <- keras_model(inputs, outputs,
+                                name = "3_layer_mlp")
+
+input <- keras_input(shape = c(784), name = "digits")
+output <- input |>
+  layer_dense(64, activation = "relu", name = "dense_1") |>
+  layer_dense(64, activation = "relu", name = "dense_2") |>
+  # Add a dropout layer, which does not contain any weights.
+  layer_dropout(0.5) |>
+  layer_dense(10, name = "predictions")
+
+functional_model_with_dropout <-
+  keras_model(input, output, name = "3_layer_mlp")
+
+set_weights(functional_model_with_dropout,
+            get_weights(functional_model))
+
+
+

APIs for saving weights to disk & loading them back +

+

Weights can be saved to disk by calling +save_model_weights(filepath). The filename should end in +.weights.h5.

+

Example:

+
+sequential_model = keras_model_sequential(input_shape = c(784),
+                                          input_name = "digits") |>
+  layer_dense(64, activation = "relu", name = "dense_1") |>
+  layer_dense(64, activation = "relu", name = "dense_2") |>
+  layer_dense(10, name = "predictions")
+sequential_model |> save_model_weights("my_model.weights.h5")
+sequential_model |> load_model_weights("my_model.weights.h5")
+

Note that using freeze_weights() may result in a +different output from get_weights(layer) ordering when the +model contains nested layers.

+
+
+Transfer learning example +
+

When loading pretrained weights from a weights file, it is +recommended to load the weights into the original checkpointed model, +and then extract the desired weights/layers into a new model.

+

Example:

+
+create_functional_model <- function() {
+  inputs <- keras_input(shape = c(784), name = "digits")
+  outputs <- inputs |>
+    layer_dense(64, activation = "relu", name = "dense_1") |>
+    layer_dense(64, activation = "relu", name = "dense_2") |>
+    layer_dense(10, name = "predictions")
+  keras_model(inputs, outputs, name = "3_layer_mlp")
+}
+
+functional_model <- create_functional_model()
+functional_model |> save_model_weights("pretrained.weights.h5")
+
+# In a separate program:
+pretrained_model <- create_functional_model()
+pretrained_model |> load_model_weights("pretrained.weights.h5")
+
+# Create a new model by extracting layers from the original model:
+extracted_layers <- pretrained_model$layers |> head(-1)
+model <- keras_model_sequential(layers = extracted_layers) |>
+  layer_dense(5, name = "dense_3")
+summary(model)
+
## Model: "sequential_4"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ dense_1 (Dense)                 │ (None, 64)             │        50,240
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_2 (Dense)                 │ (None, 64)             │         4,160
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_3 (Dense)                 │ (None, 5)              │           325
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 54,725 (213.77 KB)
+##  Trainable params: 54,725 (213.77 KB)
+##  Non-trainable params: 0 (0.00 B)
+
+
+
+
+

Appendix: Handling custom objects +

+ +
+

Defining the config methods +

+

Specifications:

+
    +
  • +get_config() should return a JSON-serializable named +list in order to be compatible with the Keras architecture and +model-saving APIs.
  • +
  • +from_config(config) (a class method) should return a +new layer or model object that is created from the config. The default +implementation returns do.call(cls, config).
  • +
+

NOTE: If all your constructor arguments are already +serializable, e.g. strings and ints, or non-custom Keras objects, +overriding from_config() is not necessary. However, for +more complex objects such as layers or models passed to +initialize(), deserialization must be handled explicitly +either in initialize itself or overriding the +from_config() method.

+

Example:

+
+layer_my_dense <- register_keras_serializable(
+  package = "MyLayers", name = "KernelMult",
+  object = Layer(
+    "MyDense",
+    initialize = function(units,
+                          ...,
+                          kernel_regularizer = NULL,
+                          kernel_initializer = NULL,
+                          nested_model = NULL) {
+      super$initialize(...)
+      self$hidden_units <- units
+      self$kernel_regularizer <- kernel_regularizer
+      self$kernel_initializer <- kernel_initializer
+      self$nested_model <- nested_model
+    },
+    get_config = function() {
+      config <- super$get_config()
+      # Update the config with the custom layer's parameters
+      config <- modifyList(config, list(
+        units = self$hidden_units,
+        kernel_regularizer = self$kernel_regularizer,
+        kernel_initializer = self$kernel_initializer,
+        nested_model = self$nested_model
+      ))
+      config
+    },
+    build = function(input_shape) {
+      input_units <- tail(input_shape, 1)
+      self$kernel <- self$add_weight(
+        name = "kernel",
+        shape = shape(input_units, self$hidden_units),
+        regularizer = self$kernel_regularizer,
+        initializer = self$kernel_initializer,
+      )
+    },
+    call = function(inputs) {
+      op_matmul(inputs, self$kernel)
+    }
+  )
+)
+
+
+layer <- layer_my_dense(units = 16,
+                        kernel_regularizer = "l1",
+                        kernel_initializer = "ones")
+layer3 <- layer_my_dense(units = 64, nested_model = layer)
+
+config <- serialize_keras_object(layer3)
+str(config)
+
## List of 4
+##  $ module         : chr "<r-globalenv>"
+##  $ class_name     : chr "MyDense"
+##  $ config         :List of 5
+##   ..$ name        : chr "my_dense_1"
+##   ..$ trainable   : logi TRUE
+##   ..$ dtype       :List of 4
+##   .. ..$ module         : chr "keras"
+##   .. ..$ class_name     : chr "FloatDTypePolicy"
+##   .. ..$ config         :List of 1
+##   .. .. ..$ name: chr "float32"
+##   .. ..$ registered_name: NULL
+##   ..$ units       : num 64
+##   ..$ nested_model:List of 4
+##   .. ..$ module         : chr "<r-globalenv>"
+##   .. ..$ class_name     : chr "MyDense"
+##   .. ..$ config         :List of 6
+##   .. .. ..$ name              : chr "my_dense"
+##   .. .. ..$ trainable         : logi TRUE
+##   .. .. ..$ dtype             :List of 4
+##   .. .. .. ..$ module         : chr "keras"
+##   .. .. .. ..$ class_name     : chr "FloatDTypePolicy"
+##   .. .. .. ..$ config         :List of 1
+##   .. .. .. .. ..$ name: chr "float32"
+##   .. .. .. ..$ registered_name: NULL
+##   .. .. ..$ units             : num 16
+##   .. .. ..$ kernel_regularizer: chr "l1"
+##   .. .. ..$ kernel_initializer: chr "ones"
+##   .. ..$ registered_name: chr "MyLayers>KernelMult"
+##  $ registered_name: chr "MyLayers>KernelMult"
+
+new_layer <- deserialize_keras_object(config)
+new_layer
+
## <MyDense name=my_dense_1, built=False>
+##  signature: (*args, **kwargs)
+

Note that overriding from_config is unnecessary above +for MyDense because hidden_units, +kernel_initializer, and kernel_regularizer are +ints, strings, and a built-in Keras object, respectively. This means +that the default from_config implementation of +cls(!!!config) will work as intended.

+

For more complex objects, such as layers and models passed to +initialize(), for example, you must explicitly deserialize +these objects. Let’s take a look at an example of a model where a +from_config override is necessary.

+

Example: +

+
+`%||%` <- \(x, y) if(is.null(x)) y else x
+layer_custom_model <- register_keras_serializable(
+  package = "ComplexModels",
+  object = Layer(
+    "CustomModel",
+    initialize = function(first_layer, second_layer = NULL, ...) {
+      super$initialize(...)
+      self$first_layer <- first_layer
+      self$second_layer <- second_layer %||% layer_dense(, 8)
+    },
+
+    get_config = function() {
+      config <- super$get_config()
+      config <- modifyList(config, list(
+        first_layer = self$first_layer,
+        second_layer = self$second_layer
+      ))
+      config
+    },
+
+    from_config = function(config) {
+      config$first_layer %<>% deserialize_keras_object()
+      config$second_layer %<>% deserialize_keras_object()
+      # note that the class is available in methods under the classname symbol,
+      # (`CustomModel` for this class), and also under the symbol `__class__`
+      cls(!!!config)
+      # CustomModel(!!!config)
+    },
+    call = function(self, inputs) {
+      inputs |>
+        self$first_layer() |>
+        self$second_layer()
+    }
+  )
+)
+
+# Let's make our first layer the custom layer from the previous example (MyDense)
+inputs <- keras_input(c(32))
+outputs <-  inputs |> layer_custom_model(first_layer=layer)
+model <- keras_model(inputs, outputs)
+
+config <- get_config(model)
+new_model <- from_config(config)
+ +
+
+

How custom objects are serialized +

+

The serialization format has a special key for custom objects +registered via register_keras_serializable(). This +registered_name key allows for easy retrieval at +loading/deserialization time while also allowing users to add custom +naming.

+

Let’s take a look at the config from serializing the custom layer +MyDense we defined above.

+

Example:

+
+layer <- layer_my_dense(
+  units = 16,
+  kernel_regularizer = regularizer_l1_l2(l1 = 1e-5, l2 = 1e-4),
+  kernel_initializer = "ones",
+)
+config <- serialize_keras_object(layer)
+str(config)
+
## List of 4
+##  $ module         : chr "<r-globalenv>"
+##  $ class_name     : chr "MyDense"
+##  $ config         :List of 6
+##   ..$ name              : chr "my_dense_2"
+##   ..$ trainable         : logi TRUE
+##   ..$ dtype             :List of 4
+##   .. ..$ module         : chr "keras"
+##   .. ..$ class_name     : chr "FloatDTypePolicy"
+##   .. ..$ config         :List of 1
+##   .. .. ..$ name: chr "float32"
+##   .. ..$ registered_name: NULL
+##   ..$ units             : num 16
+##   ..$ kernel_regularizer:List of 4
+##   .. ..$ module         : chr "keras.regularizers"
+##   .. ..$ class_name     : chr "L1L2"
+##   .. ..$ config         :List of 2
+##   .. .. ..$ l1: num 1e-05
+##   .. .. ..$ l2: num 1e-04
+##   .. ..$ registered_name: NULL
+##   ..$ kernel_initializer: chr "ones"
+##  $ registered_name: chr "MyLayers>KernelMult"
+

As shown, the registered_name key contains the lookup +information for the Keras master list, including the package +MyLayers and the custom name KernelMult that +we gave when calling register_keras_serializables(). Take a +look again at the custom class definition/registration here.

+

Note that the class_name key contains the original name +of the class, allowing for proper re-initialization in +from_config.

+

Additionally, note that the module key is +NULL since this is a custom object.

+
+
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/training_with_built_in_methods.html b/docs/dev/articles/training_with_built_in_methods.html new file mode 100644 index 0000000000..7390dc36bf --- /dev/null +++ b/docs/dev/articles/training_with_built_in_methods.html @@ -0,0 +1,1412 @@ + + + + + + + + +Training & evaluation with the built-in methods • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Setup +

+ +
+
+

Introduction +

+

This guide covers training, evaluation, and prediction (inference) +models when using built-in APIs for training & validation (such as +fit(), evaluate() and +predict()).

+

If you are interested in leveraging fit() while +specifying your own training step function, see the Customizing what happens in +fit() guide.

+ + + + +

If you are interested in writing your own training & evaluation +loops from scratch, see the guide Writing a +training loop from scratch.

+ + + + +

In general, whether you are using built-in loops or writing your own, +model training & evaluation works strictly in the same way across +every kind of Keras model – Sequential models, models built with the +Functional API, and models written from scratch via model +subclassing.

+
+
+

API overview: a first end-to-end example +

+

When passing data to the built-in training loops of a model, you +should either use:

+
    +
  • Arrays (if your data is small and fits in memory)
  • +
  • +tf_dataset objects
  • +
  • PyTorch DataLoader instances
  • +
+

In the next few paragraphs, we’ll use the MNIST dataset as NumPy +arrays, in order to demonstrate how to use optimizers, losses, and +metrics. Afterwards, we’ll take a close look at each of the other +options.

+

Let’s consider the following model (here, we build in with the +Functional API, but it could be a Sequential model or a subclassed model +as well):

+
+inputs <- keras_input(shape = 784, name="digits")
+outputs <- inputs |>
+  layer_dense(units = 64, activation = "relu", name = "dense_1") |>
+  layer_dense(units = 64, activation = "relu", name = "dense_2") |>
+  layer_dense(units = 10, activation = "softmax", name = "predictions")
+model <- keras_model(inputs = inputs, outputs = outputs)
+summary(model)
+
## Model: "functional"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ digits (InputLayer)             │ (None, 784)            │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_1 (Dense)                 │ (None, 64)             │        50,240
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_2 (Dense)                 │ (None, 64)             │         4,160
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ predictions (Dense)             │ (None, 10)             │           650
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 55,050 (215.04 KB)
+##  Trainable params: 55,050 (215.04 KB)
+##  Non-trainable params: 0 (0.00 B)
+

Here’s what the typical end-to-end workflow looks like, consisting +of:

+
    +
  • Training
  • +
  • Validation on a holdout set generated from the original training +data
  • +
  • Evaluation on the test data
  • +
+

We’ll use MNIST data for this example.

+
+c(c(x_train, y_train), c(x_test, y_test)) %<-% dataset_mnist()
+
+# Preprocess the data (these are NumPy arrays)
+x_train <- array_reshape(x_train, c(60000, 784)) / 255
+x_test <- array_reshape(x_test, c(10000, 784)) / 255
+
+# Reserve 10,000 samples for validation
+x_val <- x_train[1:10000,]
+y_val <- y_train[1:10000]
+x_train <- x_train[-c(1:10000),]
+y_train <- y_train[-c(1:10000)]
+

We specify the training configuration (optimizer, loss, metrics):

+
+model |> compile(
+  # Optimizer
+  optimizer = optimizer_rmsprop(),
+  # Loss function to minimize
+  loss = loss_sparse_categorical_crossentropy(),
+  # List of metrics to monitor
+  metrics = list(metric_sparse_categorical_accuracy())
+)
+

We call fit(), which will train the model by slicing the +data into “batches” of size batch_size, and repeatedly +iterating over the entire dataset for a given number of +epochs.

+
+history <- model |> fit(
+  x_train, y_train,
+  batch_size = 64,
+  epochs = 2,
+  # We pass some validation for
+  # monitoring validation loss and metrics
+  # at the end of each epoch
+  validation_data = list(x_val, y_val)
+)
+
## Epoch 1/2
+## 782/782 - 2s - 3ms/step - loss: 0.3410 - sparse_categorical_accuracy: 0.9035 - val_loss: 0.1869 - val_sparse_categorical_accuracy: 0.9455
+## Epoch 2/2
+## 782/782 - 1s - 860us/step - loss: 0.1588 - sparse_categorical_accuracy: 0.9532 - val_loss: 0.1303 - val_sparse_categorical_accuracy: 0.9626
+

The returned history object holds a record of the loss +values and metric values during training:

+
+history
+
##
+## Final epoch (plot to see history):
+##                            loss: 0.1588
+##     sparse_categorical_accuracy: 0.9532
+##                        val_loss: 0.1303
+## val_sparse_categorical_accuracy: 0.9626
+

We evaluate the model on the test data via +evaluate():

+
+# Evaluate the model on the test data using `evaluate`
+results <- model |> evaluate(x_test, y_test, batch_size=128)
+
## 79/79 - 0s - 3ms/step - loss: 0.1258 - sparse_categorical_accuracy: 0.9625
+
+str(results)
+
## List of 2
+##  $ loss                       : num 0.126
+##  $ sparse_categorical_accuracy: num 0.962
+
+# Generate predictions (probabilities -- the output of the last layer)
+# on new data using `predict`
+predictions <- model |> predict(x_test[1:2,])
+
## 1/1 - 0s - 129ms/step
+
+dim(predictions)
+
## [1]  2 10
+

Now, let’s review each piece of this workflow in detail.

+
+
+

The compile() method: specifying a loss, metrics, and +an optimizer +

+

To train a model with fit(), you need to specify a loss +function, an optimizer, and optionally, some metrics to monitor.

+

You pass these to the model as arguments to the +compile() method:

+
+model |> compile(
+  optimizer = optimizer_rmsprop(learning_rate = 1e-3),
+  loss = loss_sparse_categorical_crossentropy(),
+  metrics = list(metric_sparse_categorical_accuracy())
+)
+

The metrics argument should be a list – your model can +have any number of metrics.

+

If your model has multiple outputs, you can specify different losses +and metrics for each output, and you can modulate the contribution of +each output to the total loss of the model. You will find more details +about this in the Passing data to multi-input, multi-output +models section.

+

Note that if you’re satisfied with the default settings, in many +cases the optimizer, loss, and metrics can be specified via string +identifiers as a shortcut:

+
+model |> compile(
+  optimizer = "rmsprop",
+  loss = "sparse_categorical_crossentropy",
+  metrics = c("sparse_categorical_accuracy")
+)
+

For later reuse, let’s put our model definition and compile step in +functions; we will call them several times across different examples in +this guide.

+
+get_uncompiled_model <- function() {
+  inputs <- keras_input(shape = 784, name = "digits")
+  outputs <- inputs |>
+    layer_dense(units = 64, activation = "relu", name = "dense_1") |>
+    layer_dense(units = 64, activation = "relu", name = "dense_2") |>
+    layer_dense(units = 10, activation = "softmax", name = "predictions")
+  keras_model(inputs = inputs, outputs = outputs)
+}
+
+get_compiled_model <- function() {
+  model <- get_uncompiled_model()
+  model |> compile(
+    optimizer = "rmsprop",
+    loss = "sparse_categorical_crossentropy",
+    metrics = c("sparse_categorical_accuracy")
+  )
+  model
+}
+
+

Many built-in optimizers, losses, and metrics are available +

+

In general, you won’t have to create your own losses, metrics, or +optimizers from scratch, because what you need is likely to be already +part of the Keras API:

+

Optimizers:

+ +

Losses:

+ +

Metrics:

+ +
+
+

Custom losses +

+

If you need to create a custom loss, Keras provides three ways to do +so.

+

The first method involves creating a function that accepts inputs +y_true and y_pred. The following example shows +a loss function that computes the mean squared error between the real +data and the predictions:

+
+custom_mean_squared_error <- function(y_true, y_pred) {
+  op_mean(op_square(y_true - y_pred), axis = -1)
+}
+
+model <- get_uncompiled_model()
+model |> compile(optimizer = "adam",
+                 loss = custom_mean_squared_error)
+
+# We need to one-hot encode the labels to use MSE
+y_train_one_hot <- op_one_hot(y_train, num_classes = 10)
+model |> fit(x_train, y_train_one_hot, batch_size = 64, epochs = 2)
+
## Epoch 1/2
+## 782/782 - 2s - 2ms/step - loss: 0.0161
+## Epoch 2/2
+## 782/782 - 1s - 687us/step - loss: 0.0078
+

If you need a loss function that takes in parameters beside +y_true and y_pred, you can subclass the Keras +base Loss class using [Loss()] and implement +the following two methods:

+
    +
  • +initialize(): accept parameters to pass during the call +of your loss function
  • +
  • +call(y_true, y_pred): use the targets (y_true) and the +model predictions (y_pred) to compute the model’s loss
  • +
+

Let’s say you want to use mean squared error, but with an added term +that will de-incentivize prediction values far from 0.5 (we assume that +the categorical targets are one-hot encoded and take values between 0 +and 1). This creates an incentive for the model not to be too confident, +which may help reduce overfitting (we won’t know if it works until we +try!).

+

Here’s how you would do it:

+
+loss_custom_mse <- Loss(
+  classname = "CustomMSE",
+  initialize = function(regularization_factor = 0.1, name = "custom_mse") {
+    super$initialize(name = name)
+    self$regularization_factor <- regularization_factor
+  },
+  call = function(y_true, y_pred) {
+    mse <- op_mean(op_square(y_true - y_pred), axis = -1)
+    reg <- op_mean(op_square(0.5 - y_pred), axis = -1)
+    mse + reg * self$regularization_factor
+  }
+)
+
+model <- get_uncompiled_model()
+model |> compile(optimizer="adam", loss = loss_custom_mse())
+
+y_train_one_hot <- op_one_hot(y_train, num_classes=10)
+model |> fit(x_train, y_train_one_hot, batch_size=64, epochs=1)
+
## 782/782 - 2s - 2ms/step - loss: 0.0390
+
+
+

Custom metrics +

+

If you need a metric that isn’t part of the API, you can easily +create custom metrics by subclassing the Keras base Metric +class using [Metric()]. You will need to implement 4 +methods:

+
    +
  • +initialize(), in which you will create state variables +for your metric.
  • +
  • +update_state(y_true, y_pred, sample_weight = NULL), +which uses the targets y_true and the model predictions y_pred to update +the state variables.
  • +
  • +result(), which uses the state variables to compute the +final results.
  • +
  • +reset_state(), which reinitializes the state of the +metric.
  • +
+

State update and results computation are kept separate (in +update_state() and result(), respectively) +because in some cases, the results computation might be very expensive +and would only be done periodically.

+

Here’s a simple example showing how to implement a +CategoricalTruePositives metric that counts how many +samples were correctly classified as belonging to a given class:

+
+metric_categorical_true_positives <- Metric(
+  "CategoricalTruePositives",
+
+  initialize = function(name = "categorical_true_positives", ...) {
+    super$initialize(name = name, ...)
+    self$true_positives <- self$add_variable(shape = shape(),
+                                             name = "ctp",
+                                             initializer = "zeros")
+  },
+
+  update_state = function(y_true, y_pred, sample_weight = NULL) {
+    y_pred <- op_argmax(y_pred, axis = 2) |> op_reshape(c(-1, 1))
+    values <- op_cast(y_true, "int32") == op_cast(y_pred, "int32")
+    values <- op_cast(values, "float32")
+    if (!is.null(sample_weight)) {
+      sample_weight <- op_cast(sample_weight, "float32")
+      values <- op_multiply(values, sample_weight)
+    }
+    self$true_positives$assign_add(op_sum(values))
+  },
+
+  result = function() {
+    self$true_positives$value
+  },
+
+  reset_state = function() {
+    self$true_positives$assign(0.0)
+  }
+)
+
+model <- get_uncompiled_model()
+model |> compile(
+  optimizer = optimizer_rmsprop(learning_rate = 1e-3),
+  loss = loss_sparse_categorical_crossentropy(),
+  metrics = c(metric_categorical_true_positives())
+)
+history <- model |> fit(x_train, y_train, batch_size = 64, epochs = 3)
+
## Epoch 1/3
+## 782/782 - 1s - 2ms/step - categorical_true_positives: 360502.0000 - loss: 0.3444
+## Epoch 2/3
+## 782/782 - 1s - 703us/step - categorical_true_positives: 362638.0000 - loss: 0.1656
+## Epoch 3/3
+## 782/782 - 1s - 681us/step - categorical_true_positives: 363173.0000 - loss: 0.1200
+
+
+

Handling losses and metrics that don’t fit the standard +signature +

+

The overwhelming majority of losses and metrics can be computed from +y_true and y_pred, where y_pred +is an output of your model – but not all of them. For instance, a +regularization loss may only require the activation of a layer (there +are no targets in this case), and this activation may not be a model +output.

+

In such cases, you can call self$add_loss(loss_value) +from inside the call method of a custom layer. Losses added in this way +get added to the “main” loss during training (the one passed to +compile()). Here’s a simple example that adds activity +regularization (note that activity regularization is built-in in all +Keras layers – this layer is just for the sake of providing a concrete +example):

+
+layer_custom_activity_regularizer <- Layer(
+  "ActivityRegularization",
+  call = function(inputs) {
+    self$add_loss(op_sum(inputs) * 0.1)
+    inputs  # Pass-through layer.
+  }
+)
+
+inputs <- keras_input(shape = 784, name = "digits")
+outputs <- inputs |>
+  layer_dense(units = 32, activation = "relu", name = "dense_1") |>
+  layer_custom_activity_regularizer() |>
+  layer_dense(units = 64, activation = "relu", name = "dense_2") |>
+  layer_dense(units = 10, name = "predictions")
+
+model <- keras_model(inputs = inputs, outputs = outputs)
+model |> compile(optimizer = optimizer_rmsprop(learning_rate = 1e-3),
+                 loss = loss_sparse_categorical_crossentropy(from_logits = TRUE))
+
+# The displayed loss will be much higher than before
+# due to the regularization component.
+model |> fit(x_train, y_train, batch_size = 64, epochs = 1)
+
## 782/782 - 1s - 2ms/step - loss: 2.3721
+

Note that when you pass losses via add_loss(), it +becomes possible to call compile() without a loss function, +since the model already has a loss to minimize.

+

Consider the following LogisticEndpoint layer: it takes +as inputs targets & logits, and it tracks a crossentropy loss via +add_loss().

+
+layer_logistic_endpoint <- Layer(
+  "LogisticEndpoint",
+  initialize = function(name = NULL) {
+    super$initialize(name = name)
+    self$loss_fn <- loss_binary_crossentropy(from_logits = TRUE)
+  },
+  call = function(targets, logits, sample_weights = NULL) {
+    # Compute the training-time loss value and add it
+    # to the layer using `self.add_loss()`.
+    loss <- self$loss_fn(targets, logits, sample_weights)
+    self$add_loss(loss)
+
+    # Return the inference-time prediction tensor (for `predict()`).
+    op_softmax(logits)
+  }
+)
+

You can use it in a model with two inputs (input data & targets), +compiled without a loss argument, like this:

+
+inputs <- keras_input(shape = 3, name = "inputs")
+targets <- keras_input(shape = 10, name = "targets")
+
+logits <- inputs |> layer_dense(10)
+predictions <- layer_logistic_endpoint(name = "predictions")(targets, logits)
+
+model <- keras_model(inputs = list(inputs, targets),
+                     outputs = predictions)
+model |> compile(optimizer = "adam")  # No loss argument!
+
+data <- list(
+  inputs = random_normal(c(3, 3)),
+  targets = random_normal(c(3, 10))
+)
+model |> fit(data, epochs = 1)
+
## 1/1 - 0s - 468ms/step - loss: 1.0566
+

For more information about training multi-input models, see the +section Passing data to multi-input, multi-output +models.

+
+
+

Automatically setting apart a validation holdout set +

+

In the first end-to-end example you saw, we used the +validation_data argument to pass a list of arrays +list(x_val, y_val) to the model for evaluating a validation +loss and validation metrics at the end of each epoch.

+

Here’s another option: the argument validation_split +allows you to automatically reserve part of your training data for +validation. The argument value represents the fraction of the data to be +reserved for validation, so it should be set to a number higher than 0 +and lower than 1. For instance, validation_split = 0.2 +means “use 20% of the data for validation”, and +validation_split = 0.6 means “use 60% of the data for +validation”.

+

The way the validation is computed is by taking the last x% samples +of the arrays received by the fit() call, before any +shuffling.

+

Note that you can only use validation_split when +training with NumPy data.

+
+model <- get_compiled_model()
+model |> fit(x_train, y_train,
+             batch_size = 64,
+             validation_split = 0.2, epochs = 1)
+
## 625/625 - 1s - 2ms/step - loss: 0.3817 - sparse_categorical_accuracy: 0.8919 - val_loss: 0.1953 - val_sparse_categorical_accuracy: 0.9431
+
+
+
+

Training & evaluation using TF Dataset objects +

+

In the past few paragraphs, you’ve seen how to handle losses, +metrics, and optimizers, and you’ve seen how to use the +validation_data and validation_split arguments +in fit(), when your data is passed as arrays.

+

Another option is to use an iterator-like, such as a +tf.data.Dataset, a PyTorch DataLoader, or an R +generator function. Let’s take look at the former.

+

The tfdatasets R package containes a set of utilities +for loading and preprocessing data in a way that’s fast and scalable. +For a complete guide about creating Datasets, see the tf.data +documentation.

+

You can use tf.data to train your Keras models +regardless of the backend you’re using – whether it’s JAX, PyTorch, or +TensorFlow. You can pass a Dataset instance +directly to the methods fit(), evaluate(), and +predict():

+
+library(tfdatasets, exclude = "shape")
+model <- get_compiled_model()
+
+# First, let's create a training Dataset instance.
+# For the sake of our example, we'll use the same MNIST data as before.
+train_dataset <- tensor_slices_dataset(list(x_train, y_train))
+
+# Shuffle and slice the dataset.
+train_dataset <- train_dataset |>
+  dataset_shuffle(buffer_size=1024) |>
+  dataset_batch(64)
+
+# Now we get a test dataset.
+test_dataset <-
+  tensor_slices_dataset(list(x_test, y_test)) |>
+  dataset_batch(64)
+
+# Since the dataset already takes care of batching,
+# we don't pass a `batch_size` argument.
+model |> fit(train_dataset, epochs = 3)
+
## Epoch 1/3
+## 782/782 - 1s - 2ms/step - loss: 0.3365 - sparse_categorical_accuracy: 0.9041
+## Epoch 2/3
+## 782/782 - 1s - 714us/step - loss: 0.1605 - sparse_categorical_accuracy: 0.9524
+## Epoch 3/3
+## 782/782 - 1s - 696us/step - loss: 0.1185 - sparse_categorical_accuracy: 0.9647
+
+# You can also evaluate or predict on a dataset.
+result <- model |> evaluate(test_dataset)
+
## 157/157 - 1s - 4ms/step - loss: 0.1152 - sparse_categorical_accuracy: 0.9627
+
+result
+
## $loss
+## [1] 0.1151979
+##
+## $sparse_categorical_accuracy
+## [1] 0.9627
+

Note that the Dataset is reset at the end of each epoch, +so it can be reused of the next epoch.

+

If you want to run training only on a specific number of batches from +this Dataset, you can pass the steps_per_epoch argument, +which specifies how many training steps the model should run using this +Dataset before moving on to the next epoch.

+
+model <- get_compiled_model()
+
+# Prepare the training dataset
+train_dataset <- tensor_slices_dataset(list(x_train, y_train))
+train_dataset <- train_dataset |>
+  dataset_shuffle(buffer_size = 1024) |>
+  dataset_batch(64)
+
+# Only use the 100 batches per epoch (that's 64 * 100 samples)
+model |> fit(train_dataset, epochs = 3, steps_per_epoch = 100)
+
## Epoch 1/3
+## 100/100 - 1s - 7ms/step - loss: 0.8017 - sparse_categorical_accuracy: 0.7806
+## Epoch 2/3
+## 100/100 - 0s - 740us/step - loss: 0.3661 - sparse_categorical_accuracy: 0.9006
+## Epoch 3/3
+## 100/100 - 0s - 787us/step - loss: 0.3009 - sparse_categorical_accuracy: 0.9106
+

You can also pass a Dataset instance as the +validation_data argument in fit():

+
+model <- get_compiled_model()
+
+# Prepare the training dataset
+train_dataset <- tensor_slices_dataset(list(x_train, y_train))
+train_dataset <- train_dataset |>
+  dataset_shuffle(buffer_size=1024) |>
+  dataset_batch(64)
+
+# Prepare the validation dataset
+val_dataset <- tensor_slices_dataset(list(x_val, y_val))
+val_dataset <- val_dataset |> dataset_batch(64)
+
+model |> fit(train_dataset, epochs = 1, validation_data = val_dataset)
+
## 782/782 - 2s - 2ms/step - loss: 0.3428 - sparse_categorical_accuracy: 0.9022 - val_loss: 0.2337 - val_sparse_categorical_accuracy: 0.9291
+

At the end of each epoch, the model will iterate over the validation +dataset and compute the validation loss and validation metrics.

+

If you want to run validation only on a specific number of batches +from this dataset, you can pass the validation_steps +argument, which specifies how many validation steps the model should run +with the validation dataset before interrupting validation and moving on +to the next epoch:

+
+model  <- get_compiled_model()
+
+# Prepare the training dataset
+train_dataset <- tensor_slices_dataset(list(x_train, y_train))
+train_dataset <- train_dataset |>
+  dataset_shuffle(buffer_size = 1024) |>
+  dataset_batch(64)
+
+# Prepare the validation dataset
+val_dataset <- tensor_slices_dataset(list(x_val, y_val))
+val_dataset <- val_dataset |> dataset_batch(64)
+
+model %>% fit(
+  train_dataset,
+  epochs = 1,
+  # Only run validation using the first 10 batches of the dataset
+  # using the `validation_steps` argument
+  validation_data = val_dataset,
+  validation_steps = 10,
+)
+
## 782/782 - 2s - 2ms/step - loss: 0.3391 - sparse_categorical_accuracy: 0.9035 - val_loss: 0.1997 - val_sparse_categorical_accuracy: 0.9391
+

Note that the validation dataset will be reset after each use (so +that you will always be evaluating on the same samples from epoch to +epoch).

+

The argument validation_split (generating a holdout set +from the training data) is not supported when training from +Dataset objects, since this feature requires the ability to +index the samples of the datasets, which is not possible in general with +the Dataset API.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Using sample weighting and class weighting +

+

With the default settings the weight of a sample is decided by its +frequency in the dataset. There are two methods to weight the data, +independent of sample frequency:

+
    +
  • Class weights
  • +
  • Sample weights
  • +
+
+

Class weights +

+

This is set by passing a named list to the class_weight +argument to fit(). This list maps class indices to the +weight that should be used for samples belonging to this class.

+

This can be used to balance classes without resampling, or to train a +model that gives more importance to a particular class.

+

For instance, if class “0” is half as represented as class “1” in +your data, you could use +model |> fit(..., class_weight = c("0" = 1, "1" = 0.5)).

+

Here’s an R example where we use class weights or sample weights to +give more importance to the correct classification of class #5 (which is +the digit “5” in the MNIST dataset).

+
+class_weight <- c(
+    "0" = 1.0,
+    "1" = 1.0,
+    "2" = 1.0,
+    "3" = 1.0,
+    "4" = 1.0,
+    # Set weight "2" for class "5",
+    # making this class 2x more important
+    "5" = 2.0,
+    "6" = 1.0,
+    "7" = 1.0,
+    "8" = 1.0,
+    "9" = 1.0
+)
+
+model <- get_compiled_model()
+model |> fit(x_train, y_train,
+             class_weight = class_weight,
+             batch_size = 64, epochs = 1)
+
## 782/782 - 1s - 2ms/step - loss: 0.3713 - sparse_categorical_accuracy: 0.9018
+
+
+

Sample weights +

+

For fine grained control, or if you are not building a classifier, +you can use sample_weights.

+
    +
  • When training from R arrays: Pass the sample_weight +argument to fit().
  • +
  • When training from tf_dataset or any other sort of +iterator: yield +(input_batch, label_batch, sample_weight_batch) +tuples.
  • +
+

A “sample weights” array is an array of numbers that specify how much +weight each sample in a batch should have in computing the total loss. +It is commonly used in imbalanced classification problems (the idea +being to give more weight to rarely-seen classes).

+

When the weights used are ones and zeros, the array can be used as a +mask for the loss function (entirely discarding the +contribution of certain samples to the total loss).

+
+sample_weight <- rep(1.0, length(y_train))
+sample_weight[y_train == 5] <- 2.0
+
+model <- get_compiled_model()
+model |> fit(
+  x_train, y_train,
+  sample_weight = sample_weight,
+  batch_size = 64, epochs = 1
+)
+
## 782/782 - 1s - 2ms/step - loss: 0.3740 - sparse_categorical_accuracy: 0.9015
+

Here’s a matching Dataset example:

+
+sample_weight <- rep(1.0, length(y_train))
+sample_weight[y_train == 5] <- 2.0
+
+# Create a Dataset that includes sample weights
+# (3rd element in the return tuple).
+train_dataset <- tensor_slices_dataset(list(
+    x_train, y_train, sample_weight
+))
+
+# Shuffle and slice the dataset.
+train_dataset <- train_dataset |>
+  dataset_shuffle(buffer_size = 1024) |>
+  dataset_batch(64)
+
+model <- get_compiled_model()
+model |> fit(train_dataset, epochs = 1)
+
## 782/782 - 1s - 2ms/step - loss: 0.3654 - sparse_categorical_accuracy: 0.9057
+
+
+
+

Passing data to multi-input, multi-output models +

+

In the previous examples, we were considering a model with a single +input (a tensor of shape (764)) and a single output (a +prediction tensor of shape (10)). But what about models +that have multiple inputs or outputs?

+

Consider the following model, which has an image input of shape +(32, 32, 3) (that’s (height, width, channels)) +and a time series input of shape (NA, 10) (that’s +(timesteps, features)). Our model will have two outputs +computed from the combination of these inputs: a “score” (of shape +(1)) and a probability distribution over five classes (of +shape (5)).

+
+image_input <- keras_input(c(32, 32, 3), name = "img_input")
+timeseries_input <- keras_input(c(NA, 10), name = "ts_input")
+
+x1 <- image_input |>
+  layer_conv_2d(filters = 3, kernel_size = c(3, 3)) |>
+  layer_global_max_pooling_2d()
+
+x2 <- timeseries_input |>
+  layer_conv_1d(filters = 3, kernel_size = 3) |>
+  layer_global_max_pooling_1d()
+
+x <- layer_concatenate(x1, x2)
+
+score_output <- layer_dense(x, 1, name = "score_output")
+class_output <- layer_dense(x, 5, name = "class_output")
+
+model <- keras_model(
+  inputs = list(image_input, timeseries_input),
+  outputs = list(score_output, class_output)
+)
+

Let’s plot this model, so you can clearly see what we’re doing here +(note that the shapes shown in the plot are batch shapes, rather than +per-sample shapes).

+
+plot(model, show_shapes = TRUE)
+
+plot of chunk unnamed-chunk-26

+plot of chunk unnamed-chunk-26 +

+
+

At compilation time, we can specify different losses to different +outputs, by passing the loss functions as a list:

+
+model |> compile(
+  optimizer = optimizer_rmsprop(1e-3),
+  loss = list(
+    loss_mean_squared_error(),
+    loss_categorical_crossentropy()
+  )
+)
+

If we only passed a single loss function to the model, the same loss +function would be applied to every output (which is not appropriate +here).

+

Likewise for metrics:

+ +

Since we gave names to our output layers, we could also specify +per-output losses and metrics via a named list:

+
+model |> compile(
+  optimizer = optimizer_rmsprop(1e-3),
+  loss = list(
+    score_output = loss_mean_squared_error(),
+    class_output = loss_categorical_crossentropy()
+  ),
+  metrics = list(
+    score_output = list(
+      metric_mean_absolute_error(),
+      metric_mean_absolute_percentage_error()
+    ),
+    class_output = list(metric_categorical_accuracy())
+  )
+)
+

We recommend the use of names if you have more than 2 outputs.

+

It’s possible to give different weights to different output-specific +losses (for instance, one might wish to privilege the “score” loss in +our example, by giving to 2x the importance of the class loss), using +the loss_weights argument:

+
+model |> compile(
+  optimizer = optimizer_rmsprop(1e-3),
+  loss = list(
+    score_output = loss_mean_squared_error(),
+    class_output = loss_categorical_crossentropy()
+  ),
+  metrics = list(
+    score_output = list(
+      metric_mean_absolute_error(),
+      metric_mean_absolute_percentage_error()
+    ),
+    class_output = list(metric_categorical_accuracy())
+  ),
+  loss_weights = list(score_output = 2.0, class_output = 1.0)
+)
+

You could also choose not to compute a loss for certain outputs, if +these outputs are meant for prediction but not for training:

+
+# loss list, positional version
+model |> compile(
+  optimizer = optimizer_rmsprop(1e-3),
+  loss = list(NULL, loss_categorical_crossentropy())
+)
+
+# Or loss list, named version
+model |> compile(
+  optimizer = optimizer_rmsprop(1e-3),
+  loss = list(class_output = loss_categorical_crossentropy())
+)
+

Passing data to a multi-input or multi-output model in +fit() works in a similar way as specifying a loss function +in compile: you can pass lists of arrays (with 1:1 +mapping to the outputs that received a loss function) or dicts +mapping output names to arrays.

+
+model |> compile(
+  optimizer = optimizer_rmsprop(1e-3),
+  loss = list(
+    loss_mean_squared_error(),
+    loss_categorical_crossentropy()
+  )
+)
+
+# Generate dummy data
+img_data <- random_normal(c(100, 32, 32, 3))
+ts_data <- random_normal(c(100, 20, 10))
+score_targets <- random_normal(c(100, 1))
+class_targets <- random_normal(c(100, 5))
+
+# Fit on unnamed lists (positional matching)
+model |> fit(
+    list(img_data, ts_data),
+    list(score_targets, class_targets),
+    batch_size=32,
+    epochs=1
+)
+
## 4/4 - 2s - 443ms/step - loss: 1.3788
+
+# Alternatively, fit on named lists (names matching)
+model |> fit(
+  list(img_input = img_data, ts_input = ts_data),
+  list(score_output = score_targets, class_output = class_targets),
+  batch_size = 32,
+  epochs = 1
+)
+
## 4/4 - 1s - 230ms/step - loss: 0.2857
+

Here’s the Dataset use case: similarly as what we did +for R arrays, the Dataset should return a tuple of named +lists (dicts).

+
+train_dataset <- tensor_slices_dataset(list(
+  list(img_input = img_data, ts_input = ts_data),
+  list(score_output = score_targets, class_output = class_targets)
+))
+train_dataset <- train_dataset |>
+  dataset_shuffle(buffer_size = 1024) |>
+  dataset_batch(64)
+
+model |> fit(train_dataset, epochs = 1)
+
## 2/2 - 1s - 622ms/step - loss: 0.5599
+
+
+

Using callbacks +

+

Callbacks in Keras are objects that are called at different points +during training (at the start of an epoch, at the end of a batch, at the +end of an epoch, etc.). They can be used to implement certain behaviors, +such as:

+
    +
  • Doing validation at different points during training (beyond the +built-in per-epoch validation)
  • +
  • Checkpointing the model at regular intervals or when it exceeds a +certain accuracy threshold
  • +
  • Changing the learning rate of the model when training seems to be +plateauing
  • +
  • Doing fine-tuning of the top layers when training seems to be +plateauing
  • +
  • Sending email or instant message notifications when training ends or +where a certain performance threshold is exceeded
  • +
  • Etc.
  • +
+

Callbacks can be passed as a list to your call to +fit():

+
+model <- get_compiled_model()
+
+callbacks <- list(
+  callback_early_stopping(
+    # Stop training when `val_loss` is no longer improving
+    monitor = "val_loss",
+    # "no longer improving" being defined as "no better than 1e-2 less"
+    min_delta = 1e-2,
+    # "no longer improving" being further defined as "for at least 2 epochs"
+    patience = 2,
+    verbose = 1
+  )
+)
+model |> fit(
+  x_train,
+  y_train,
+  epochs = 20,
+  batch_size = 64,
+  callbacks = callbacks,
+  validation_split = 0.2,
+)
+
## Epoch 1/20
+## 625/625 - 1s - 2ms/step - loss: 0.3695 - sparse_categorical_accuracy: 0.8961 - val_loss: 0.1873 - val_sparse_categorical_accuracy: 0.9469
+## Epoch 2/20
+## 625/625 - 1s - 923us/step - loss: 0.1751 - sparse_categorical_accuracy: 0.9489 - val_loss: 0.1403 - val_sparse_categorical_accuracy: 0.9579
+## Epoch 3/20
+## 625/625 - 1s - 853us/step - loss: 0.1277 - sparse_categorical_accuracy: 0.9625 - val_loss: 0.1218 - val_sparse_categorical_accuracy: 0.9651
+## Epoch 4/20
+## 625/625 - 1s - 891us/step - loss: 0.1007 - sparse_categorical_accuracy: 0.9700 - val_loss: 0.1153 - val_sparse_categorical_accuracy: 0.9661
+## Epoch 5/20
+## 625/625 - 1s - 921us/step - loss: 0.0822 - sparse_categorical_accuracy: 0.9760 - val_loss: 0.1104 - val_sparse_categorical_accuracy: 0.9670
+## Epoch 6/20
+## 625/625 - 1s - 908us/step - loss: 0.0683 - sparse_categorical_accuracy: 0.9801 - val_loss: 0.1098 - val_sparse_categorical_accuracy: 0.9689
+## Epoch 7/20
+## 625/625 - 1s - 874us/step - loss: 0.0571 - sparse_categorical_accuracy: 0.9838 - val_loss: 0.1116 - val_sparse_categorical_accuracy: 0.9698
+## Epoch 8/20
+## 625/625 - 1s - 916us/step - loss: 0.0485 - sparse_categorical_accuracy: 0.9864 - val_loss: 0.1126 - val_sparse_categorical_accuracy: 0.9702
+## Epoch 8: early stopping
+
+

Many built-in callbacks are available +

+

There are many built-in callbacks already available in Keras, such +as:

+ +

See the callbacks +documentation for the complete list.

+
+
+

Writing your own callback +

+

You can create a custom callback by subclassing the base +[Callback()] class. A callback has access to its associated +model through the class property self$model.

+

Make sure to read the complete guide to writing +custom callbacks.

+

Here’s a simple example saving a list of per-batch loss values during +training:

+
+callback_loss_history <- Callback(
+  classname = "LossHistory",
+  initialize = function(file = "per_training_batch_losses.txt", ...) {
+    super$initialize(...)
+    private$file <- file
+  },
+  on_train_begin = function(logs = NULL) {
+    private$per_batch_losses <- fastmap::faststack()
+  },
+  on_train_batch_begin = function(batch, logs = NULL) {
+    private$per_batch_losses$push(logs$loss)
+  },
+  on_train_end = function(logs = NULL) {
+    per_batch_losses <- private$per_batch_losses$as_list() |> as.numeric()
+    write(per_batch_losses, private$file)
+  }
+)
+
+
+
+

Checkpointing models +

+

When you’re training model on relatively large datasets, it’s crucial +to save checkpoints of your model at frequent intervals.

+

The easiest way to achieve this is with +[callback_model_checkpoint()]:

+
+model <- get_compiled_model()
+
+callbacks <- list(
+  callback_model_checkpoint(
+    # Path where to save the model
+    # The two parameters below mean that we will overwrite
+    # the current checkpoint if and only if
+    # the `val_loss` score has improved.
+    # The saved model name will include the current epoch.
+    filepath = "mymodel_{epoch}.keras",
+    save_best_only = TRUE,
+    # Only save a model if `val_loss` has improved.
+    monitor = "val_loss",
+    verbose = 1
+  )
+)
+model |> fit(
+  x_train, y_train,
+  epochs = 2, batch_size = 64,
+  callbacks = callbacks,
+  validation_split = 0.2
+)
+
## Epoch 1/2
+##
+## Epoch 1: val_loss improved from inf to 0.19344, saving model to mymodel_1.keras
+## 625/625 - 1s - 2ms/step - loss: 0.3787 - sparse_categorical_accuracy: 0.8940 - val_loss: 0.1934 - val_sparse_categorical_accuracy: 0.9441
+## Epoch 2/2
+##
+## Epoch 2: val_loss improved from 0.19344 to 0.14251, saving model to mymodel_2.keras
+## 625/625 - 1s - 1ms/step - loss: 0.1768 - sparse_categorical_accuracy: 0.9478 - val_loss: 0.1425 - val_sparse_categorical_accuracy: 0.9600
+

The ModelCheckpoint callback can be used to implement +fault-tolerance: the ability to restart training from the last saved +state of the model in case training gets randomly interrupted. Here’s a +basic example:

+
+# Prepare a directory to store all the checkpoints.
+checkpoint_dir <- "./ckpt"
+fs::dir_create(checkpoint_dir)
+
+make_or_restore_model <- function() {
+  # Either restore the latest (best) model, or create a fresh one
+  # if there is no checkpoint available.
+  checkpoints <- Sys.glob(file.path(checkpoint_dir, "model-loss=*.keras"))
+
+  if (length(checkpoints) > 0) {
+    checkpoint_losses <- sub("^model-loss=([0-9.]+)\\.keras$", "\\1",
+                             basename(checkpoints)) |> as.numeric()
+    best_checkpoint <- checkpoints[which.min(checkpoint_losses)]
+    load_model(best_checkpoint)
+  } else {
+    get_compiled_model()
+  }
+}
+
+model <- make_or_restore_model()
+callbacks <- list(
+  # This callback saves the model every 100 batches.
+  # We include the training loss in the saved model name.
+  callback_model_checkpoint(
+    filepath = file.path(checkpoint_dir, "model-loss={loss:.2f}.keras"),
+    save_freq = 100
+  )
+)
+model |> fit(x_train, y_train, epochs = 1, callbacks = callbacks)
+
## 1563/1563 - 2s - 1ms/step - loss: 0.2932 - sparse_categorical_accuracy: 0.9145
+

You call also write your own callback for saving and restoring +models.

+

For a complete guide on serialization and saving, see the guide to saving and serializing +Models.

+
+
+

Using learning rate schedules +

+

A common pattern when training deep learning models is to gradually +reduce the learning as training progresses. This is generally known as +“learning rate decay”.

+

The learning decay schedule could be static (fixed in advance, as a +function of the current epoch or the current batch index), or dynamic +(responding to the current behavior of the model, in particular the +validation loss).

+
+

Passing a schedule to an optimizer +

+

You can easily use a static learning rate decay schedule by passing a +schedule object as the learning_rate argument in your +optimizer:

+
+initial_learning_rate <- 0.1
+lr_schedule <- learning_rate_schedule_exponential_decay(
+    initial_learning_rate, decay_steps=100000, decay_rate=0.96,
+    staircase=TRUE
+)
+
+optimizer <- optimizer_rmsprop(learning_rate = lr_schedule)
+

Several built-in schedules are available: +ExponentialDecay, PiecewiseConstantDecay, +PolynomialDecay, and InverseTimeDecay.

+
+
+

Using callbacks to implement a dynamic learning rate schedule +

+

A dynamic learning rate schedule (for instance, decreasing the +learning rate when the validation loss is no longer improving) cannot be +achieved with these schedule objects, since the optimizer does not have +access to validation metrics.

+

However, callbacks do have access to all metrics, including +validation metrics! You can thus achieve this pattern by using a +callback that modifies the current learning rate on the optimizer. In +fact, this is even built-in as +[callback_reduce_lr_on_plateau()].

+
+
+
+

Visualizing loss and metrics during training with TensorBoard +

+

The best way to keep an eye on your model during training is to use +TensorBoard – a +browser-based application that you can run locally that provides you +with:

+
    +
  • Live plots of the loss and metrics for training and evaluation
  • +
  • (optionally) Visualizations of the histograms of your layer +activations
  • +
  • (optionally) 3D visualizations of the embedding spaces learned by +your layer_embedding() +
  • +
+

If you have installed TensorFlow with pip, you should be able to +launch TensorBoard from the command line:

+
tensorboard --logdir=/full_path_to_your_logs
+

or from R using:

+
+tensorflow::tensorboard(logdir = "/full_path_to_your_logs")
+
+

Using the TensorBoard callback +

+

The easiest way to use TensorBoard with a Keras model and the +fit() method is with +[callback_tensorboard()].

+

In the simplest case, just specify where you want the callback to +write logs, and you’re good to go:

+
+tb_callback <- callback_tensorboard(
+  log_dir = "/full_path_to_your_logs",
+  histogram_freq = 0, # How often to log histogram visualizations
+  embeddings_freq = 0, # How often to log embedding visualizations
+  update_freq = "epoch", # How often to write logs (default: once per epoch)
+)
+

For more information, see callback_tensorboard().

+
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/training_with_built_in_methods/unnamed-chunk-26-1.png b/docs/dev/articles/training_with_built_in_methods/unnamed-chunk-26-1.png new file mode 100644 index 0000000000..7fea86b758 Binary files /dev/null and b/docs/dev/articles/training_with_built_in_methods/unnamed-chunk-26-1.png differ diff --git a/docs/dev/articles/transfer_learning.html b/docs/dev/articles/transfer_learning.html new file mode 100644 index 0000000000..6a83ffc06a --- /dev/null +++ b/docs/dev/articles/transfer_learning.html @@ -0,0 +1,743 @@ + + + + + + + + +Transfer learning & fine-tuning • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Setup +

+ +
+
+

Introduction +

+

Transfer learning consists of taking features +learned on one problem, and leveraging them on a new, similar problem. +For instance, features from a model that has learned to identify raccoon +may be useful to kick-start a model meant to identify tanukis.

+

Transfer learning is usually done for tasks where your dataset has +too little data to train a full-scale model from scratch.

+

The most common incarnation of transfer learning in the context of +deep learning is the following workflow:

+
    +
  1. Take layers from a previously trained model.
  2. +
  3. Freeze them, so as to avoid destroying any of the information they +contain during future training rounds.
  4. +
  5. Add some new, trainable layers on top of the frozen layers. They +will learn to turn the old features into predictions on a new +dataset.
  6. +
  7. Train the new layers on your dataset.
  8. +
+

A last, optional step, is fine-tuning, which +consists of unfreezing the entire model you obtained above (or part of +it), and re-training it on the new data with a very low learning rate. +This can potentially achieve meaningful improvements, by incrementally +adapting the pretrained features to the new data.

+

First, we will go over the Keras trainable API in +detail, which underlies most transfer learning & fine-tuning +workflows.

+

Then, we’ll demonstrate the typical workflow by taking a model +pretrained on the ImageNet dataset, and retraining it on the Kaggle +“cats vs dogs” classification dataset.

+

This is adapted from Deep +Learning with Python and the 2016 blog post “building +powerful image classification models using very little data”.

+
+
+

Freezing layers: understanding the trainable +attribute +

+

Layers & models have three weight attributes:

+
    +
  • +weights is the list of all weights variables of the +layer.
  • +
  • +trainable_weights is the list of those that are meant +to be updated (via gradient descent) to minimize the loss during +training.
  • +
  • +non_trainable_weights is the list of those that aren’t +meant to be trained. Typically they are updated by the model during the +forward pass.
  • +
+

Example: the Dense layer has 2 trainable weights +(kernel & bias)

+
+layer <- layer_dense(units = 3)
+layer$build(shape(NULL, 4))  # Create the weights
+
+length(layer$weights)
+
## [1] 2
+
+length(layer$trainable_weights)
+
## [1] 2
+
+length(layer$non_trainable_weights)
+
## [1] 0
+

In general, all weights are trainable weights. The only built-in +layer that has non-trainable weights is +[layer_batch_normalization()]. It uses non-trainable +weights to keep track of the mean and variance of its inputs during +training. To learn how to use non-trainable weights in your own custom +layers, see the guide to +writing new layers from scratch.

+

Example: the BatchNormalization layer has 2 +trainable weights and 2 non-trainable weights

+
+layer <- layer_batch_normalization()
+layer$build(shape(NA, 4))  # Create the weights
+
+length(layer$weights)
+
## [1] 4
+
+length(layer$trainable_weights)
+
## [1] 2
+
+length(layer$non_trainable_weights)
+
## [1] 2
+

Layers & models also feature a boolean attribute +trainable. Its value can be changed. Setting +layer$trainable to FALSE moves all the layer’s +weights from trainable to non-trainable. This is called “freezing” the +layer: the state of a frozen layer won’t be updated during training +(either when training with fit() or when training with any +custom loop that relies on trainable_weights to apply +gradient updates).

+

Example: setting trainable to +False

+
+layer <- layer_dense(units = 3)
+layer$build(shape(NULL, 4))  # Create the weights
+layer$trainable <- FALSE  # Freeze the layer
+
+length(layer$weights)
+
## [1] 2
+
+length(layer$trainable_weights)
+
## [1] 0
+
+length(layer$non_trainable_weights)
+
## [1] 2
+

When a trainable weight becomes non-trainable, its value is no longer +updated during training.

+
+# Make a model with 2 layers
+layer1 <- layer_dense(units = 3, activation = "relu")
+layer2 <- layer_dense(units = 3, activation = "sigmoid")
+model <- keras_model_sequential(input_shape = 3) |>
+  layer1() |>
+  layer2()
+
+# Freeze the first layer
+layer1$trainable <- FALSE
+
+# Keep a copy of the weights of layer1 for later reference
+# (get_weights() returns a list of R arrays,
+#  layer$weights returns a list of KerasVariables)
+initial_layer1_weights_values <- get_weights(layer1)
+
+# Train the model
+model |> compile(optimizer = "adam", loss = "mse")
+model |> fit(random_normal(c(2, 3)), random_normal(c(2, 3)), epochs = 1)
+
## 1/1 - 1s - 502ms/step - loss: 2.1868
+
+# Check that the weights of layer1 have not changed during training
+final_layer1_weights_values <- get_weights(layer1)
+
+all.equal(initial_layer1_weights_values,
+          final_layer1_weights_values)
+
## [1] TRUE
+

Do not confuse the layer$trainable attribute with the +argument training in layer$call() (which +controls whether the layer should run its forward pass in inference mode +or training mode). For more information, see the Keras +FAQ.

+
+
+

Recursive setting of the trainable attribute +

+

If you set $trainable <- FALSE on a model or on any +layer that has sublayers, all children layers become non-trainable as +well.

+

Example:

+
+inner_model <- keras_model_sequential(input_shape = 3) |>
+  layer_dense(units = 3, activation = "relu") |>
+  layer_dense(units = 3, activation = "relu")
+
+model <- keras_model_sequential(input_shape = 3) |>
+  inner_model() |>
+  layer_dense(units = 3, activation = "sigmoid")
+
+model$trainable <- FALSE  # Freeze the outer model
+
+inner_model$trainable  # All layers in `model` are now frozen
+
## [1] FALSE
+
+inner_model$layers[[1]]$trainable  # `trainable` is propagated recursively
+
## [1] FALSE
+
+
+

The typical transfer-learning workflow +

+

This leads us to how a typical transfer learning workflow can be +implemented in Keras:

+
    +
  1. Instantiate a base model and load pre-trained weights into it.
  2. +
  3. Freeze all layers in the base model by setting +trainable <- FALSE.
  4. +
  5. Create a new model on top of the output of one (or several) layers +from the base model.
  6. +
  7. Train your new model on your new dataset.
  8. +
+

Note that an alternative, more lightweight workflow could also +be:

+
    +
  1. Instantiate a base model and load pre-trained weights into it.
  2. +
  3. Run your new dataset through it and record the output of one (or +several) layers from the base model. This is called feature +extraction.
  4. +
  5. Use that output as input data for a new, smaller model.
  6. +
+

A key advantage of that second workflow is that you only run the base +model once on your data, rather than once per epoch of training. So it’s +a lot faster & cheaper.

+

An issue with that second workflow, though, is that it doesn’t allow +you to dynamically modify the input data of your new model during +training, which is required when doing data augmentation, for instance. +Transfer learning is typically used for tasks when your new dataset has +too little data to train a full-scale model from scratch, and in such +scenarios data augmentation is very important. So in what follows, we +will focus on the first workflow.

+

Here’s what the first workflow looks like in Keras:

+

First, instantiate a base model with pre-trained weights.

+
+base_model <- application_xception(
+  weights = 'imagenet', # Load weights pre-trained on ImageNet.
+  input_shape = c(150, 150, 3),
+  include_top = FALSE  # Do not include the ImageNet classifier at the top.
+)
+

Then, freeze the base model.

+
+base_model$trainable <- FALSE
+

Create a new model on top.

+
+inputs <- keras_input(shape = c(150, 150, 3))
+# We make sure that the base_model is running in inference mode here,
+# by passing `training <- FALSE`. This is important for fine-tuning, as you will
+# learn in a few paragraphs.
+outputs <- inputs |>
+  base_model(training = FALSE) |>
+  # Convert features of shape `base_model$output_shape[-1]` to vectors
+  layer_global_average_pooling_2d() |>
+  # A Dense classifier with a single unit (binary classification)
+  layer_dense(1)
+
+model <- keras_model(inputs, outputs)
+

Train the model on new data.

+
+model |> compile(
+  optimizer = optimizer_adam(),
+  loss = loss_binary_crossentropy(from_logits = TRUE),
+  metrics = list(metric_binary_accuracy())
+)
+model |> fit(new_dataset, epochs = 20,
+             callbacks = ..., validation_data = ...)
+
+
+

Fine-tuning +

+

Once your model has converged on the new data, you can try to +unfreeze all or part of the base model and retrain the whole model +end-to-end with a very low learning rate.

+

This is an optional last step that can potentially give you +incremental improvements. It could also potentially lead to quick +overfitting – keep that in mind.

+

It is critical to only do this step after the model with +frozen layers has been trained to convergence. If you mix +randomly-initialized trainable layers with trainable layers that hold +pre-trained features, the randomly-initialized layers will cause very +large gradient updates during training, which will destroy your +pre-trained features.

+

It’s also critical to use a very low learning rate at this stage, +because you are training a much larger model than in the first round of +training, on a dataset that is typically very small. As a result, you +are at risk of overfitting very quickly if you apply large weight +updates. Here, you only want to readapt the pretrained weights in an +incremental way.

+

This is how to implement fine-tuning of the whole base model:

+
+# Unfreeze the base model
+base_model$trainable <- TRUE
+
+# It's important to recompile your model after you make any changes
+# to the `trainable` attribute of any inner layer, so that your changes
+# are take into account
+model |> compile(
+  optimizer = optimizer_adam(1e-5), # Very low learning rate
+  loss = loss_binary_crossentropy(from_logits = TRUE),
+  metrics = c(metric_binary_accuracy())
+)
+
+# Train end-to-end. Be careful to stop before you overfit!
+model |> fit(new_dataset, epochs = 10,
+             callbacks = ..., validation_data = ...)
+

Important note about compile() and +trainable

+

Calling compile() on a model is meant to “freeze” the +behavior of that model. This implies that the trainable +attribute values at the time the model is compiled should be preserved +throughout the lifetime of that model, until compile is +called again. Hence, if you change any trainable value, +make sure to call compile() again on your model for your +changes to be taken into account.

+

Important notes about BatchNormalization +layer

+

Many image models contain BatchNormalization layers. +That layer is a special case on every imaginable count. Here are a few +things to keep in mind.

+
    +
  • +BatchNormalization contains 2 non-trainable weights +that get updated during training. These are the variables tracking the +mean and variance of the inputs.
  • +
  • When you set bn_layer$trainable <- FALSE, the +BatchNormalization layer will run in inference mode, and +will not update its mean & variance statistics. This is not the case +for other layers in general, as weight +trainability & inference/training modes are two orthogonal +concepts. But the two are tied in the case of the +BatchNormalization layer.
  • +
  • When you unfreeze a model that contains +BatchNormalization layers in order to do fine-tuning, you +should keep the BatchNormalization layers in inference mode +by passing training = FALSE when calling the base model. +Otherwise the updates applied to the non-trainable weights will suddenly +destroy what the model has learned.
  • +
+

You’ll see this pattern in action in the end-to-end example at the +end of this guide.

+
+
+

An end-to-end example: fine-tuning an image classification model on +a cats vs. dogs dataset +

+

To solidify these concepts, let’s walk you through a concrete +end-to-end transfer learning & fine-tuning example. We will load the +Xception model, pre-trained on ImageNet, and use it on the Kaggle “cats +vs. dogs” classification dataset.

+
+

Getting the data +

+

First, let’s fetch the cats vs. dogs dataset using TFDS. If you have +your own dataset, you’ll probably want to use the utility +[image_dataset_from_directory()] to generate similar +labeled dataset objects from a set of images on disk filed into +class-specific folders.

+

Transfer learning is most useful when working with very small +datasets. To keep our dataset small, we will use 40% of the original +training data (25,000 images) for training, 10% for validation, and 10% +for testing.

+
+# reticulate::py_install("tensorflow-datasets")
+tfds <- reticulate::import("tensorflow_datasets")
+
+c(train_ds, validation_ds, test_ds) %<-% tfds$load(
+  "cats_vs_dogs",
+  # Reserve 10% for validation and 10% for test
+  split = c("train[:40%]", "train[40%:50%]", "train[50%:60%]"),
+  as_supervised = TRUE  # Include labels
+)
+
+length(train_ds)
+
## [1] 9305
+

These are the first 9 images in the training dataset – as you can +see, they’re all different sizes.

+
+library(tfdatasets, exclude = "shape")
+
+par(mfrow = c(3, 3), mar = c(1,0,1.5,0))
+train_ds |>
+  dataset_take(9) |>
+  as_array_iterator() |>
+  iterate(function(batch) {
+    c(image, label) %<-% batch
+    plot(as.raster(image, max = 255L))
+    title(sprintf(
+      "label: %s   size: %s",
+      label, paste(dim(image), collapse = " x ")))
+  })
+
+plot of chunk unnamed-chunk-13
plot of chunk unnamed-chunk-13
+
+

We can also see that label 1 is “dog” and label 0 is “cat”.

+
+
+

Standardizing the data +

+

Our raw images have a variety of sizes. In addition, each pixel +consists of 3 integer values between 0 and 255 (RGB level values). This +isn’t a great fit for feeding a neural network. We need to do 2 +things:

+
    +
  • Standardize to a fixed image size. We pick 150x150.
  • +
  • Normalize pixel values between -1 and 1. We’ll do this using a +Normalization layer as part of the model itself.
  • +
+

In general, it’s a good practice to develop models that take raw data +as input, as opposed to models that take already-preprocessed data. The +reason being that, if your model expects preprocessed data, any time you +export your model to use it elsewhere (in a web browser, in a mobile +app), you’ll need to reimplement the exact same preprocessing pipeline. +This gets very tricky very quickly. So we should do the least possible +amount of preprocessing before hitting the model.

+

Here, we’ll do image resizing in the data pipeline (because a deep +neural network can only process contiguous batches of data), and we’ll +do the input value scaling as part of the model, when we create it.

+

Let’s resize images to 150x150:

+
+resize_fn <- layer_resizing(width = 150, height = 150)
+resize_pair <- function(x, y) list(resize_fn(x), y)
+
+train_ds <- train_ds |> dataset_map(resize_pair)
+validation_ds <- validation_ds |> dataset_map(resize_pair)
+test_ds <- test_ds |> dataset_map(resize_pair)
+
+
+

Using random data augmentation +

+

When you don’t have a large image dataset, it’s a good practice to +artificially introduce sample diversity by applying random yet realistic +transformations to the training images, such as random horizontal +flipping or small random rotations. This helps expose the model to +different aspects of the training data while slowing down +overfitting.

+
+data_augmentation <- keras_model_sequential() |>
+  layer_random_flip("horizontal") |>
+  layer_random_rotation(.1)
+
+train_ds <- train_ds %>%
+  dataset_map(function(x, y) list(data_augmentation(x), y))
+

Let’s batch the data and use prefetching to optimize loading +speed.

+
+library(tensorflow, exclude = c("shape", "set_random_seed"))
+batch_size <- 64
+
+train_ds <- train_ds |>
+  dataset_batch(batch_size) |>
+  dataset_prefetch()
+
+validation_ds <- validation_ds |>
+  dataset_batch(batch_size) |>
+  dataset_prefetch()
+
+test_ds <- test_ds |>
+  dataset_batch(batch_size) |>
+  dataset_prefetch()
+

Let’s visualize what the first image of the first batch looks like +after various random transformations:

+
+batch <- train_ds |>
+  dataset_take(1) |>
+  as_iterator() |>
+  iter_next()
+
+c(images, labels) %<-% batch
+first_image <- images[1, all_dims(), drop = TRUE]
+augmented_image <- data_augmentation(first_image, training = TRUE)
+
+plot_image <- function(image, main = deparse1(substitute(image))) {
+  image |>
+    as.array() |> # convert from tensor to R array
+    as.raster(max = 255) |>
+    plot()
+
+  if(!is.null(main))
+    title(main)
+}
+
+par(mfrow = c(2, 2), mar = c(1, 1, 1.5, 1))
+plot_image(first_image)
+plot_image(augmented_image)
+plot_image(data_augmentation(first_image, training = TRUE), "augmented 2")
+plot_image(data_augmentation(first_image, training = TRUE), "augmented 3")
+
+plot of chunk unnamed-chunk-17
plot of chunk unnamed-chunk-17
+
+
+
+
+

Build a model +

+

Now let’s built a model that follows the blueprint we’ve explained +earlier.

+

Note that:

+
    +
  • We add a Rescaling layer to scale input values +(initially in the [0, 255] range) to the +[-1, 1] range.
  • +
  • We add a Dropout layer before the classification layer, +for regularization.
  • +
  • We make sure to pass training=FALSE when calling the +base model, so that it runs in inference mode, so that batchnorm +statistics don’t get updated even after we unfreeze the base model for +fine-tuning.
  • +
+
+base_model <- application_xception(
+  weights = "imagenet", # Load weights pre-trained on ImageNet.
+  input_shape = c(150, 150, 3),
+  include_top = FALSE, # Do not include the ImageNet classifier at the top.
+)
+
+# Freeze the base_model
+base_model$trainable <- FALSE
+
+# Create new model on top
+inputs <- keras_input(shape = c(150, 150, 3))
+
+# Pre-trained Xception weights requires that input be scaled
+# from (0, 255) to a range of (-1., +1.), the rescaling layer
+# outputs: `(inputs * scale) + offset`
+scale_layer <- layer_rescaling(scale = 1 / 127.5, offset = -1)
+x <- scale_layer(inputs)
+
+# The base model contains batchnorm layers. We want to keep them in inference mode
+# when we unfreeze the base model for fine-tuning, so we make sure that the
+# base_model is running in inference mode here.
+outputs <- x |>
+  base_model(training = FALSE) |>
+  layer_global_average_pooling_2d() |>
+  layer_dropout(0.2) |>
+  layer_dense(1)
+
+model <- keras_model(inputs, outputs)
+
+summary(model, show_trainable = TRUE)
+
## Model: "functional_8"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━┓
+## ┃ Layer (type)                 Output Shape              Param #  Trai… 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━┩
+## │ input_layer_7 (InputLayer)  │ (None, 150, 150, 3)   │          0-
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ rescaling (Rescaling)       │ (None, 150, 150, 3)   │          0-
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ xception (Functional)       │ (None, 5, 5, 2048)    │ 20,861,480N
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ global_average_pooling2d_1  │ (None, 2048)          │          0-
+## │ (GlobalAveragePooling2D)    │                       │            │       │
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ dropout (Dropout)           │ (None, 2048)          │          0-
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ dense_8 (Dense)             │ (None, 1)             │      2,049Y
+## └─────────────────────────────┴───────────────────────┴────────────┴───────┘
+##  Total params: 20,863,529 (79.59 MB)
+##  Trainable params: 2,049 (8.00 KB)
+##  Non-trainable params: 20,861,480 (79.58 MB)
+
+
+

Train the top layer +

+
+model |> compile(
+  optimizer = optimizer_adam(),
+  loss = loss_binary_crossentropy(from_logits = TRUE),
+  metrics = list(metric_binary_accuracy())
+)
+
+epochs <- 1
+model |> fit(train_ds, epochs = epochs, validation_data = validation_ds)
+
## 146/146 - 42s - 287ms/step - binary_accuracy: 0.9183 - loss: 0.1887 - val_binary_accuracy: 0.9669 - val_loss: 0.0926
+
+
+

Do a round of fine-tuning of the entire model +

+

Finally, let’s unfreeze the base model and train the entire model +end-to-end with a low learning rate.

+

Importantly, although the base model becomes trainable, it is still +running in inference mode since we passed training=FALSE +when calling it when we built the model. This means that the batch +normalization layers inside won’t update their batch statistics. If they +did, they would wreck havoc on the representations learned by the model +so far.

+
+# Unfreeze the base_model. Note that it keeps running in inference mode
+# since we passed `training=FALSE` when calling it. This means that
+# the batchnorm layers will not update their batch statistics.
+# This prevents the batchnorm layers from undoing all the training
+# we've done so far.
+base_model$trainable <- TRUE
+summary(model, show_trainable = TRUE)
+
## Model: "functional_8"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━┓
+## ┃ Layer (type)                 Output Shape              Param #  Trai… 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━┩
+## │ input_layer_7 (InputLayer)  │ (None, 150, 150, 3)   │          0-
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ rescaling (Rescaling)       │ (None, 150, 150, 3)   │          0-
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ xception (Functional)       │ (None, 5, 5, 2048)    │ 20,861,480Y
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ global_average_pooling2d_1  │ (None, 2048)          │          0-
+## │ (GlobalAveragePooling2D)    │                       │            │       │
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ dropout (Dropout)           │ (None, 2048)          │          0-
+## ├─────────────────────────────┼───────────────────────┼────────────┼───────┤
+## │ dense_8 (Dense)             │ (None, 1)             │      2,049Y
+## └─────────────────────────────┴───────────────────────┴────────────┴───────┘
+##  Total params: 20,867,629 (79.60 MB)
+##  Trainable params: 20,809,001 (79.38 MB)
+##  Non-trainable params: 54,528 (213.00 KB)
+##  Optimizer params: 4,100 (16.02 KB)
+
+model |> compile(
+  optimizer = optimizer_adam(1e-5), # Low learning rate
+  loss = loss_binary_crossentropy(from_logits = TRUE),
+  metrics = list(metric_binary_accuracy())
+)
+
+epochs <- 1
+model |> fit(train_ds, epochs = epochs, validation_data = validation_ds)
+
## 146/146 - 91s - 622ms/step - binary_accuracy: 0.8660 - loss: 0.3213 - val_binary_accuracy: 0.9652 - val_loss: 0.1022
+

After 10 epochs, fine-tuning gains us a nice improvement here. Let’s +evaluate the model on the test dataset:

+
+model |> evaluate(test_ds)
+
## 37/37 - 2s - 45ms/step - binary_accuracy: 0.9540 - loss: 0.1103
+
## $binary_accuracy
+## [1] 0.9539983
+##
+## $loss
+## [1] 0.1102617
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/transfer_learning/unnamed-chunk-13-1.png b/docs/dev/articles/transfer_learning/unnamed-chunk-13-1.png new file mode 100644 index 0000000000..2679c6251c Binary files /dev/null and b/docs/dev/articles/transfer_learning/unnamed-chunk-13-1.png differ diff --git a/docs/dev/articles/transfer_learning/unnamed-chunk-17-1.png b/docs/dev/articles/transfer_learning/unnamed-chunk-17-1.png new file mode 100644 index 0000000000..6d11cbe6a3 Binary files /dev/null and b/docs/dev/articles/transfer_learning/unnamed-chunk-17-1.png differ diff --git a/docs/dev/articles/understanding_masking_and_padding.html b/docs/dev/articles/understanding_masking_and_padding.html new file mode 100644 index 0000000000..588cae82dc --- /dev/null +++ b/docs/dev/articles/understanding_masking_and_padding.html @@ -0,0 +1,523 @@ + + + + + + + + +Understanding masking & padding • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Setup +

+ +
+
+

Introduction +

+

Masking is a way to tell sequence-processing layers +that certain timesteps in an input are missing, and thus should be +skipped when processing the data.

+

Padding is a special form of masking where the +masked steps are at the start or the end of a sequence. Padding comes +from the need to encode sequence data into contiguous batches: in order +to make all sequences in a batch fit a given standard length, it is +necessary to pad or truncate some sequences.

+

Let’s take a close look.

+
+
+

Padding sequence data +

+

When processing sequence data, it is very common for individual +samples to have different lengths. Consider the following example (text +tokenized as words):

+
+data <- list(
+  c("Hello", "world", "!"),
+  c("How", "are", "you", "doing", "today"),
+  c("The", "weather", "will", "be", "nice", "tomorrow")
+)
+

After vocabulary lookup, the data might be vectorized as integers, +e.g.:

+
+data <- list(
+  c(71, 1331, 4231),
+  c(73, 8, 3215, 55, 927),
+  c(83, 91, 1, 645, 1253, 927)
+)
+

The data is a nested list where individual samples have length 3, 5, +and 6, respectively. Since the input data for a deep learning model must +be a single tensor (of shape +e.g. (batch_size, 6, vocab_size) in this case), samples +that are shorter than the longest item need to be padded with some +placeholder value (alternatively, one might also truncate long samples +before padding short samples).

+

Keras provides a utility function to truncate and pad Python lists to +a common length: pad_sequences.

+
+raw_inputs <- list(
+  c(711, 632, 71),
+  c(73, 8, 3215, 55, 927),
+  c(83, 91, 1, 645, 1253, 927)
+)
+
+# By default, this will pad using 0s; it is configurable via the
+# "value" parameter.
+# Note that you could use "pre" padding (at the beginning) or
+# "post" padding (at the end).
+# We recommend using "post" padding when working with RNN layers
+# (in order to be able to use the
+# CuDNN implementation of the layers).
+padded_inputs <- pad_sequences(raw_inputs, padding="post")
+padded_inputs
+
##      [,1] [,2] [,3] [,4] [,5] [,6]
+## [1,]  711  632   71    0    0    0
+## [2,]   73    8 3215   55  927    0
+## [3,]   83   91    1  645 1253  927
+
+
+

Masking +

+

Now that all samples have a uniform length, the model must be +informed that some part of the data is actually padding and should be +ignored. That mechanism is masking.

+

There are three ways to introduce input masks in Keras models:

+
    +
  • Add a layer_masking layer.
  • +
  • Configure a layer_embedding layer with +mask_zero=TRUE.
  • +
  • Pass a mask argument manually when calling layers that +support this argument (e.g. RNN layers).
  • +
+
+
+

Mask-generating layers: Embedding and +Masking +

+

Under the hood, these layers will create a mask tensor (2D tensor +with shape (batch, sequence_length)), and attach it to the +tensor output returned by the Masking or +Embedding layer.

+
+embedding <- layer_embedding(input_dim=5000, output_dim=16, mask_zero=TRUE)
+masked_output <- embedding(padded_inputs)
+
+masked_output$`_keras_mask`
+
## tf.Tensor(
+## [[ True  True  True False False False]
+##  [ True  True  True  True  True False]
+##  [ True  True  True  True  True  True]], shape=(3, 6), dtype=bool)
+
+masking_layer <- layer_masking()
+# Simulate the embedding lookup by expanding the 2D input to 3D,
+# with embedding dimension of 10.
+unmasked_embedding <- op_cast(
+    op_tile(op_expand_dims(padded_inputs, axis=-1), c(1L, 1L, 10L)),
+    dtype="float32"
+)
+
+masked_embedding <- masking_layer(unmasked_embedding)
+masked_embedding$`_keras_mask`
+
## tf.Tensor(
+## [[ True  True  True False False False]
+##  [ True  True  True  True  True False]
+##  [ True  True  True  True  True  True]], shape=(3, 6), dtype=bool)
+

As you can see from the printed result, the mask is a 2D boolean +tensor with shape (batch_size, sequence_length), where each +individual FALSE entry indicates that the corresponding +timestep should be ignored during processing.

+
+
+

Mask propagation in the Functional API and Sequential API +

+

When using the Functional API or the Sequential API, a mask generated +by an Embedding or Masking layer will be +propagated through the network for any layer that is capable of using +them (for example, RNN layers). Keras will automatically fetch the mask +corresponding to an input and pass it to any layer that knows how to use +it.

+

For instance, in the following Sequential model, the +LSTM layer will automatically receive a mask, which means +it will ignore padded values:

+
+model <- keras_model_sequential() %>%
+  layer_embedding(input_dim=5000, output_dim=16, mask_zero=TRUE) %>%
+  layer_lstm(units=32)
+

This is also the case for the following Functional API model:

+
+inputs <- keras_input(shape = shape(NULL), dtype="int32")
+outputs <- inputs %>%
+  layer_embedding(input_dim=5000, output_dim=16, mask_zero=TRUE) %>%
+  layer_lstm(units=32)
+
+model <- keras_model(inputs, outputs)
+
+
+

Passing mask tensors directly to layers +

+

Layers that can handle masks (such as the LSTM layer) +have a mask argument in their call method.

+

Meanwhile, layers that produce a mask (e.g. Embedding) +expose a compute_mask(input, previous_mask) method which +you can call.

+

Thus, you can pass the output of the compute_mask() +method of a mask-producing layer to the call method of a +mask-consuming layer, like this:

+
+MyLayer <- new_layer_class(
+  "MyLayer",
+  initialize = function(...) {
+    super$initialize(...)
+    self$embedding <- layer_embedding(
+      input_dim=5000, output_dim=16, mask_zero=TRUE
+    )
+    self$lstm <- layer_lstm(units=32)
+  },
+  call = function(inputs) {
+    inputs %>%
+      self$embedding() %>%
+      # Note that you could also prepare a `mask` tensor manually.
+      # It only needs to be a boolean tensor
+      # with the right shape, i.e. (batch_size, timesteps).
+      self$lstm(mask=self$embedding$compute_mask(inputs))
+  }
+)
+
+layer <- MyLayer()
+x <- random_integer(c(32, 10), 0, 100)
+layer(x)
+
## tf.Tensor(
+## [[ 0.00130048 -0.00113367 -0.00715671 ... -0.00107615 -0.00162071
+##    0.00135018]
+##  [-0.004185    0.00726349  0.00520932 ...  0.00119117  0.00230441
+##    0.00174123]
+##  [-0.00537032 -0.00164898 -0.00238435 ... -0.00154158 -0.0038603
+##   -0.00105811]
+##  ...
+##  [ 0.00622133 -0.00905907 -0.00599518 ...  0.00025823 -0.00142478
+##   -0.00125036]
+##  [-0.00523904  0.00336683 -0.00299453 ...  0.00876719  0.00172074
+##    0.00903089]
+##  [-0.00393721  0.00058538  0.00503809 ... -0.00203075  0.00325885
+##   -0.00299755]], shape=(32, 32), dtype=float32)
+
+
+

Supporting masking in your custom layers +

+

Sometimes, you may need to write layers that generate a mask (like +Embedding), or layers that need to modify the current +mask.

+

For instance, any layer that produces a tensor with a different time +dimension than its input, such as a Concatenate layer that +concatenates on the time dimension, will need to modify the current mask +so that downstream layers will be able to properly take masked timesteps +into account.

+

To do this, your layer should implement the +layer.compute_mask() method, which produces a new mask +given the input and the current mask.

+

Here is an example of a TemporalSplit layer that needs +to modify the current mask.

+
+TemporalSplit <- new_layer_class(
+  "TemporalSplit",
+  call = function(inputs) {
+    # Expect the input to be 3D and mask to be 2D, split the input tensor into 2
+    # subtensors along the time axis (axis 1).
+    op_split(inputs, 2, axis=2)
+  },
+  compute_mask = function(inputs, mask = NULL) {
+    # Also split the mask into 2 if it presents.
+    if (!is.null(mask)) {
+      op_split(mask, 2, axis=2)
+    } else {
+      NULL
+    }
+  }
+)
+
+c(first_half, second_half) %<-% TemporalSplit(masked_embedding)
+first_half$`_keras_mask`
+
## tf.Tensor(
+## [[ True  True  True]
+##  [ True  True  True]
+##  [ True  True  True]], shape=(3, 3), dtype=bool)
+
+second_half$`_keras_mask`
+
## tf.Tensor(
+## [[False False False]
+##  [ True  True False]
+##  [ True  True  True]], shape=(3, 3), dtype=bool)
+

Here is another example of a CustomEmbedding layer that +is capable of generating a mask from input values:

+
+CustomEmbedding <- new_layer_class(
+  "CustomEmbedding",
+  initialize = function(input_dim, output_dim, mask_zero=FALSE, ...) {
+    super$initialize(...)
+    self$input_dim <- as.integer(input_dim)
+    self$output_dim <- as.integer(output_dim)
+    self$mask_zero <- mask_zero
+  },
+  build = function(input_shape) {
+    self$embeddings <- self$add_weight(
+      shape=c(self$input_dim, self$output_dim),
+      initializer="random_normal",
+      dtype="float32"
+    )
+  },
+  call = function(inputs) {
+    inputs <- op_cast(inputs, "int32")
+    op_take(self$embeddings, inputs)
+  },
+  compute_mask = function(inputs, mask=NULL) {
+    if (!self$mask_zero) {
+      NULL
+    } else {
+      op_not_equal(inputs, 0)
+    }
+  }
+)
+
+layer <- CustomEmbedding(input_dim = 10, output_dim = 32, mask_zero=TRUE)
+x <- random_integer(c(3, 10), 0, 9)
+
+y <- layer(x)
+mask <- layer$compute_mask(x)
+
+mask
+
## tf.Tensor(
+## [[ True  True  True  True  True  True  True  True  True  True]
+##  [ True  True  True  True  True  True  True  True  True  True]
+##  [False False  True  True  True  True  True  True  True  True]], shape=(3, 10), dtype=bool)
+

Note: For more details about format limitations related to masking, +see the serialization +guide.

+
+
+

Opting-in to mask propagation on compatible layers +

+

Most layers don’t modify the time dimension, so don’t need to modify +the current mask. However, they may still want to be able to +propagate the current mask, unchanged, to the next +layer. This is an opt-in behavior. By default, a custom +layer will destroy the current mask (since the framework has no way to +tell whether propagating the mask is safe to do).

+

If you have a custom layer that does not modify the time dimension, +and if you want it to be able to propagate the current input mask, you +should set self.supports_masking = True in the layer +constructor. In this case, the default behavior of +compute_mask() is to just pass the current mask +through.

+

Here’s an example of a layer that is whitelisted for mask +propagation:

+
+MyActivation <- new_layer_class(
+  "MyActivation",
+  initialize = function(...) {
+    super$initialize(...)
+    self$supports_masking <- TRUE
+  },
+  call = function(inputs) {
+    op_relu(inputs)
+  }
+)
+

You can now use this custom layer in-between a mask-generating layer +(like Embedding) and a mask-consuming layer (like +LSTM), and it will pass the mask along so that it reaches +the mask-consuming layer.

+
+inputs <- keras_input(shape = shape(NULL), dtype="int32")
+outputs <- inputs %>%
+  layer_embedding(input_dim=5000, output_dim=16, mask_zero=TRUE) %>%
+  MyActivation() %>%
+  layer_lstm(units=32)
+
+model <- keras_model(inputs, outputs)
+y <- model(random_integer(c(32, 100), 0, 5000))
+
+
+

Writing layers that need mask information +

+

Some layers are mask consumers: they accept a +mask argument in call and use it to determine +whether to skip certain time steps.

+

To write such a layer, you can simply add a mask=None +argument in your call signature. The mask associated with +the inputs will be passed to your layer whenever it is available.

+

Here’s a simple example below: a layer that computes a softmax over +the time dimension (axis 1) of an input sequence, while discarding +masked timesteps.

+
+TemporalSoftmax <- new_layer_class(
+  "TemporalSoftmax",
+  initialize = function(...) {
+    super$initialize(...)
+    self$supports_masking <- TRUE
+  },
+  call = function(inputs, mask=NULL) {
+    if (is.null(mask)) {
+      stop("`TemporalSoftmax` layer requires a previous layer to support masking.")
+    }
+    broadcast_float_mask <- op_expand_dims(op_cast(mask, "float32"), -1)
+    inputs_exp <- op_exp(inputs) * broadcast_float_mask
+    inputs_sum <- op_sum(inputs_exp * broadcast_float_mask, axis=-1, keepdims=TRUE)
+    inputs_exp / inputs_sum
+  }
+)
+
+inputs <- keras_input(shape = shape(NULL), dtype="int32")
+outputs <- inputs %>%
+  layer_embedding(input_dim=10, output_dim=32, mask_zero=TRUE) %>%
+  layer_dense(1) %>%
+  TemporalSoftmax()
+
+model <- keras_model(inputs, outputs)
+y <- model(random_integer(c(32, 100), 0, 10))
+
+
+

Summary +

+

That is all you need to know about padding & masking in Keras. To +recap:

+
    +
  • “Masking” is how layers are able to know when to skip / ignore +certain timesteps in sequence inputs.
  • +
  • Some layers are mask-generators: Embedding can generate +a mask from input values (if mask_zero=TRUE), and so can +the Masking layer.
  • +
  • Some layers are mask-consumers: they expose a mask +argument in their call method. This is the case for RNN +layers.
  • +
  • In the Functional API and Sequential API, mask information is +propagated automatically.
  • +
  • When using layers in a standalone way, you can pass the +mask arguments to layers manually.
  • +
  • You can easily write layers that modify the current mask, that +generate a new mask, or that consume the mask associated with the +inputs.
  • +
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/writing_a_custom_training_loop_in_tensorflow.html b/docs/dev/articles/writing_a_custom_training_loop_in_tensorflow.html new file mode 100644 index 0000000000..95da91b945 --- /dev/null +++ b/docs/dev/articles/writing_a_custom_training_loop_in_tensorflow.html @@ -0,0 +1,740 @@ + + + + + + + + +Writing a training loop from scratch in TensorFlow • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Setup +

+
+library(tensorflow, exclude = c("shape", "set_random_seed"))
+library(tfdatasets, exclude = "shape")
+library(keras3)
+
+# This guide can only be run with the TensorFlow backend.
+use_backend("tensorflow")
+
+
+

Introduction +

+

Keras provides default training and evaluation loops, +fit() and evaluate(). Their usage is covered +in the guide Training +& evaluation with the built-in methods.

+

If you want to customize the learning algorithm of your model while +still leveraging the convenience of fit() (for instance, to +train a GAN using fit()), you can subclass the +Model class and implement your own +train_step() method, which is called repeatedly during +fit(). This is covered in the guide Customizing what happens in +fit().

+

Now, if you want very low-level control over training & +evaluation, you should write your own training & evaluation loops +from scratch. This is what this guide is about.

+
+
+

Using the GradientTape: a first end-to-end example +

+

Calling a model inside a GradientTape scope enables you +to retrieve the gradients of the trainable weights of the layer with +respect to a loss value. Using an optimizer instance, you can use these +gradients to update these variables (which you can retrieve using +model$trainable_weights).

+

Let’s consider a simple MNIST model:

+
+get_model <- function() {
+  inputs <- keras_input(shape = 784, name = "digits")
+  outputs <- inputs |>
+    layer_dense(units = 64, activation = "relu") |>
+    layer_dense(units = 64, activation = "relu") |>
+    layer_dense(units = 10, name = "predictions")
+  keras_model(inputs = inputs, outputs = outputs)
+}
+
+model <- get_model()
+

Let’s train it using mini-batch gradient with a custom training +loop.

+

First, we’re going to need an optimizer, a loss function, and a +dataset:

+
+# Instantiate an optimizer.
+optimizer <- optimizer_adam(learning_rate = 1e-3)
+# Instantiate a loss function.
+loss_fn <- loss_sparse_categorical_crossentropy(from_logits = TRUE)
+
+# Prepare the training dataset.
+batch_size <- 64
+c(c(x_train, y_train), c(x_test, y_test)) %<-% dataset_mnist()
+x_train <- array_reshape(x_train, c(-1, 784))
+x_test <- array_reshape(x_test, c(-1, 784))
+
+# Reserve 10,000 samples for validation.
+val_i <- sample.int(nrow(x_train), 10000)
+x_val <- x_train[val_i,]
+y_val <- y_train[val_i]
+x_train %<>% .[-val_i,]
+y_train %<>% .[-val_i]
+
+# Prepare the training dataset.
+train_dataset <- list(x_train, y_train) |>
+  tensor_slices_dataset() |>
+  dataset_shuffle(buffer_size = 1024) |>
+  dataset_batch(batch_size)
+
+# Prepare the validation dataset.
+val_dataset <- list(x_val, y_val) |>
+  tensor_slices_dataset() |>
+  dataset_batch(batch_size)
+

Here’s our training loop:

+
    +
  • We open a for loop that iterates over epochs
  • +
  • For each epoch, we iterate over the dataset, in batches
  • +
  • For each batch, we open a GradientTape() scope
  • +
  • Inside this scope, we call the model (forward pass) and compute the +loss
  • +
  • Outside the scope, we retrieve the gradients of the weights of the +model with regard to the loss
  • +
  • Finally, we use the optimizer to update the weights of the model +based on the gradients
  • +
+
+epochs <- 3
+for (epoch in seq_len(epochs)) {
+  cat("Start of epoch ", epoch, "\n")
+   # Iterate over the batches of the dataset.
+  step <- 0
+  iterator <- as_iterator(train_dataset)
+  while (!is.null(batch <- iter_next(iterator))) {
+    c(x_batch_train, y_batch_train) %<-% batch
+    step <- step + 1
+    # Open a GradientTape to record the operations run
+    # during the forward pass, which enables auto-differentiation.
+    with(tf$GradientTape() %as% tape, {
+      # Run the forward pass of the layer.
+      # The operations that the layer applies
+      # to its inputs are going to be recorded
+      # on the GradientTape.
+      logits <- model(x_batch_train, training = TRUE) # Logits for this minibatch
+
+      # Compute the loss value for this minibatch.
+      loss_value <- loss_fn(y_batch_train, logits)
+    })
+
+    # Use the gradient tape to automatically retrieve
+    # the gradients of the trainable variables with respect to the loss.
+    gradients <- tape$gradient(loss_value, model$trainable_weights)
+
+    # Run one step of gradient descent by updating
+    # the value of the variables to minimize the loss.
+    optimizer$apply(gradients, model$trainable_weights)
+
+    # Log every 200 batches.
+    if (step %% 200 == 0) {
+      cat(sprintf("Training loss (for one batch) at step %d: %.4f\n",
+                  step, loss_value))
+      cat(sprintf("Seen so far: %d samples\n", (step * batch_size)))
+    }
+  }
+}
+
## Start of epoch  1
+## Training loss (for one batch) at step 200: 1.1675
+## Seen so far: 12800 samples
+## Training loss (for one batch) at step 400: 1.6450
+## Seen so far: 25600 samples
+## Training loss (for one batch) at step 600: 0.6477
+## Seen so far: 38400 samples
+## Start of epoch  2
+## Training loss (for one batch) at step 200: 0.6421
+## Seen so far: 12800 samples
+## Training loss (for one batch) at step 400: 0.1827
+## Seen so far: 25600 samples
+## Training loss (for one batch) at step 600: 0.4249
+## Seen so far: 38400 samples
+## Start of epoch  3
+## Training loss (for one batch) at step 200: 0.2715
+## Seen so far: 12800 samples
+## Training loss (for one batch) at step 400: 0.3106
+## Seen so far: 25600 samples
+## Training loss (for one batch) at step 600: 0.2905
+## Seen so far: 38400 samples
+
+
+

Low-level handling of metrics +

+

Let’s add metrics monitoring to this basic loop.

+

You can readily reuse the built-in metrics (or custom ones you wrote) +in such training loops written from scratch. Here’s the flow:

+
    +
  • Instantiate the metric at the start of the loop
  • +
  • Call metric$update_state() after each batch
  • +
  • Call metric$result() when you need to display the +current value of the metric
  • +
  • Call metric$reset_state() when you need to clear the +state of the metric (typically at the end of an epoch)
  • +
+

Let’s use this knowledge to compute +SparseCategoricalAccuracy on validation data at the end of +each epoch:

+
+# Get a fresh model
+model <- get_model()
+
+# Instantiate an optimizer to train the model.
+optimizer <- optimizer_adam(learning_rate = 1e-3)
+# Instantiate a loss function.
+loss_fn <- loss_sparse_categorical_crossentropy(from_logits = TRUE)
+
+# Prepare the metrics.
+train_acc_metric <- metric_sparse_categorical_accuracy()
+val_acc_metric <- metric_sparse_categorical_accuracy()
+

Here’s our training & evaluation loop:

+
+epochs <- 2
+time <- Sys.time()
+for (epoch in seq_len(epochs)) {
+  cat("Start of epoch ", epoch, "\n")
+   # Iterate over the batches of the dataset.
+  step <- 0
+  train_dataset_iterator <- as_iterator(train_dataset)
+  while (!is.null(train_batch <- iter_next(train_dataset_iterator))) {
+    c(x_batch_train, y_batch_train) %<-% train_batch
+    step <- step + 1
+
+    with(tf$GradientTape() %as% tape, {
+      logits <- model(x_batch_train, training = TRUE)
+      loss_value <- loss_fn(y_batch_train, logits)
+    })
+    gradients <- tape$gradient(loss_value, model$trainable_weights)
+    optimizer$apply(gradients, model$trainable_weights)
+
+    # Update training metric.
+    train_acc_metric$update_state(y_batch_train, logits)
+    if (step %% 200 == 0) {
+      cat(sprintf(
+        "Training loss (for one batch) at step %d: %.4f\n", step, loss_value))
+      cat(sprintf("Seen so far: %d samples \n", (step * batch_size)))
+    }
+  }
+
+  # Display metrics at the end of each epoch.
+  train_acc <- train_acc_metric$result()
+  cat(sprintf("Training acc over epoch: %.4f\n", train_acc))
+
+  # Reset training metrics at the end of each epoch
+  train_acc_metric$reset_state()
+
+  # Run a validation loop at the end of each epoch.
+  iterate(val_dataset, function(val_batch) {
+    c(x_batch_val, y_batch_val) %<-% val_batch
+    val_logits <- model(x_batch_val, training = FALSE)
+    val_acc_metric$update_state(y_batch_val, val_logits)
+  })
+  val_acc <- val_acc_metric$result()
+  val_acc_metric$reset_state()
+  cat(sprintf("Validation acc: %.4f\n", val_acc))
+}
+
## Start of epoch  1
+## Training loss (for one batch) at step 200: 1.6268
+## Seen so far: 12800 samples
+## Training loss (for one batch) at step 400: 1.2241
+## Seen so far: 25600 samples
+## Training loss (for one batch) at step 600: 0.4987
+## Seen so far: 38400 samples
+## Training acc over epoch: 0.7844
+## Validation acc: 0.8680
+## Start of epoch  2
+## Training loss (for one batch) at step 200: 0.4626
+## Seen so far: 12800 samples
+## Training loss (for one batch) at step 400: 0.4654
+## Seen so far: 25600 samples
+## Training loss (for one batch) at step 600: 0.5022
+## Seen so far: 38400 samples
+## Training acc over epoch: 0.8837
+## Validation acc: 0.9031
+
+Sys.time() - time
+
## Time difference of 47.04693 secs
+
+
+

Speeding-up your training step with tf_function() +

+

The default runtime in TensorFlow 2 is eager execution. As +such, our training loop above executes eagerly.

+

This is great for debugging, but graph compilation has a definite +performance advantage. Describing your computation as a static graph +enables the framework to apply global performance optimizations. This is +impossible when the framework is constrained to greedily execute one +operation after another, with no knowledge of what comes next.

+

You can compile into a static graph any function that takes tensors +as input. Just add a @tf.function decorator on it, like +this:

+
+train_step <- tf_function(function(x, y) {
+  with(tf$GradientTape() %as% tape, {
+    logits <- model(x, training = TRUE)
+    loss_value <- loss_fn(y, logits)
+  })
+  gradients <- tape$gradient(loss_value, model$trainable_weights)
+  optimizer$apply(gradients, model$trainable_weights)
+  train_acc_metric$update_state(y, logits)
+  invisible(NULL) # return nothing
+})
+

Let’s do the same with the evaluation step:

+
+test_step <- tf_function(function(x, y) {
+  val_logits <- model(x, training=FALSE)
+  val_acc_metric$update_state(y, val_logits)
+  invisible(NULL) # return nothing
+})
+

Now, let’s re-run our training loop with this compiled training +step:

+
+epochs <- 2
+time <- Sys.time()
+for (epoch in seq_len(epochs)) {
+  cat("Start of epoch ", epoch, "\n")
+   # Iterate over the batches of the dataset.
+  step <- 0
+  while (!is.null(batch <- iter_next(iterator))) {
+    c(x_batch_train, y_batch_train) %<-% batch
+    step <- step + 1
+    train_step(x_batch_train, y_batch_train)
+
+    if (step %% 200 == 0) {
+      cat(sprintf(
+        "Training loss (for one batch) at step %d: %.4f\n", step, loss_value
+      ))
+      cat(sprintf("Seen so far: %d samples \n", (step * batch_size)))
+    }
+  }
+
+  # Display metrics at the end of each epoch.
+  train_acc <- train_acc_metric$result()
+  cat(sprintf("Training acc over epoch: %.4f\n", train_acc))
+
+   # Reset training metrics at the end of each epoch
+  train_acc_metric$reset_state()
+
+  # Run a validation loop at the end of each epoch.
+   iterate(val_dataset, function(val_batch) {
+    c(x_batch_val, y_batch_val) %<-% val_batch
+    test_step(x_batch_val, y_batch_val)
+  })
+
+  val_acc <- val_acc_metric$result()
+  val_acc_metric$reset_state()
+  cat(sprintf("Validation acc: %.4f\n", val_acc))
+}
+
## Start of epoch  1
+## Training acc over epoch: 0.0000
+## Validation acc: 0.9031
+## Start of epoch  2
+## Training acc over epoch: 0.0000
+## Validation acc: 0.9031
+
+Sys.time() - time
+
## Time difference of 0.4134665 secs
+

Much faster, isn’t it?

+
+
+

Low-level handling of losses tracked by the model +

+

Layers and models recursively track any losses created during the +forward pass by layers that call self$add_loss(value). The +resulting list of scalar loss values are available via the property +model$losses at the end of the forward pass.

+

If you want to be using these loss components, you should sum them +and add them to the main loss in your training step.

+

Consider this layer, that creates an activity regularization +loss:

+
+layer_activity_regularization <- Layer(
+  "ActivityRegularizationLayer",
+  call = function(inputs) {
+    self$add_loss(0.1 * op_mean(inputs))
+    inputs
+  }
+)
+

Let’s build a really simple model that uses it:

+
+inputs <- keras_input(shape = 784, name="digits")
+outputs <- inputs |>
+  layer_dense(units = 64, activation = "relu") |>
+  layer_activity_regularization() |>
+  layer_dense(units = 64, activation = "relu") |>
+  layer_dense(units = 10, name = "predictions")
+model <- keras_model(inputs = inputs, outputs = outputs)
+

Here’s what our training step should look like now:

+
+train_step <- tf_function(function(x, y) {
+  with(tf$GradientTape() %as% tape, {
+    logits <- model(x, training = TRUE)
+    loss_value <- Reduce(`+`, c(loss_fn(y, logits),
+                                model$losses))
+  })
+  gradients <- tape$gradient(loss_value, model$trainable_weights)
+  optimizer$apply(gradients, model$trainable_weights)
+  train_acc_metric$update_state(y, logits)
+  invisible(NULL)
+})
+
+
+

Summary +

+

Now you know everything there is to know about using built-in +training loops and writing your own from scratch.

+

To conclude, here’s a simple end-to-end example that ties together +everything you’ve learned in this guide: a DCGAN trained on MNIST +digits.

+
+
+

End-to-end example: a GAN training loop from scratch +

+

You may be familiar with Generative Adversarial Networks (GANs). GANs +can generate new images that look almost real, by learning the latent +distribution of a training dataset of images (the “latent space” of the +images).

+

A GAN is made of two parts: a “generator” model that maps points in +the latent space to points in image space, a “discriminator” model, a +classifier that can tell the difference between real images (from the +training dataset) and fake images (the output of the generator +network).

+

A GAN training loop looks like this:

+
    +
  1. Train the discriminator.
  2. +
+
    +
  • Sample a batch of random points in the latent space.
  • +
  • Turn the points into fake images via the “generator” model.
  • +
  • Get a batch of real images and combine them with the generated +images.
  • +
  • Train the “discriminator” model to classify generated vs. real +images.
  • +
+
    +
  1. Train the generator.
  2. +
+
    +
  • Sample random points in the latent space.
  • +
  • Turn the points into fake images via the “generator” network.
  • +
  • Get a batch of real images and combine them with the generated +images.
  • +
  • Train the “generator” model to “fool” the discriminator and classify +the fake images as real.
  • +
+

For a much more detailed overview of how GANs works, see Deep +Learning with Python.

+

Let’s implement this training loop. First, create the discriminator +meant to classify fake vs real digits:

+
+# Create the discriminator
+discriminator <-
+  keras_model_sequential(name = "discriminator",
+                         input_shape = c(28, 28, 1)) |>
+  layer_conv_2d(filters = 64, kernel_size = c(3, 3),
+                strides = c(2, 2), padding = "same") |>
+  layer_activation_leaky_relu(negative_slope = 0.2) |>
+  layer_conv_2d(filters = 128, kernel_size = c(3, 3),
+                strides = c(2, 2), padding = "same") |>
+  layer_activation_leaky_relu(negative_slope = 0.2) |>
+  layer_global_max_pooling_2d() |>
+  layer_dense(units = 1)
+
+summary(discriminator)
+
## Model: "discriminator"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ conv2d (Conv2D)                 │ (None, 14, 14, 64)     │           640
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ leaky_re_lu (LeakyReLU)         │ (None, 14, 14, 64)     │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_1 (Conv2D)               │ (None, 7, 7, 128)      │        73,856
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ leaky_re_lu_1 (LeakyReLU)       │ (None, 7, 7, 128)      │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ global_max_pooling2d            │ (None, 128)            │             0
+## │ (GlobalMaxPooling2D)            │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ dense_6 (Dense)                 │ (None, 1)              │           129
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 74,625 (291.50 KB)
+##  Trainable params: 74,625 (291.50 KB)
+##  Non-trainable params: 0 (0.00 B)
+

Then let’s create a generator network, that turns latent vectors into +outputs of shape (28, 28, 1) (representing MNIST +digits):

+
+latent_dim <- 128L
+
+generator <-
+  keras_model_sequential(name = "generator",
+                         input_shape = latent_dim) |>
+  layer_dense(7 * 7 * 128) |>
+  layer_activation_leaky_relu(negative_slope = 0.2) |>
+  layer_reshape(target_shape = c(7, 7, 128)) |>
+  layer_conv_2d_transpose(filters = 128, kernel_size = c(4, 4),
+                          strides = c(2, 2), padding = "same") |>
+  layer_activation_leaky_relu(negative_slope = 0.2) |>
+  layer_conv_2d_transpose(filters = 128, kernel_size = c(4, 4),
+                          strides = c(2, 2), padding = "same") |>
+  layer_activation_leaky_relu(negative_slope = 0.2) |>
+  layer_conv_2d(filters = 1, kernel_size = c(7, 7), padding = "same",
+                activation = "sigmoid")
+
+summary(generator)
+
## Model: "generator"
+## ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
+## ┃ Layer (type)                     Output Shape                  Param # 
+## ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
+## │ dense_7 (Dense)                 │ (None, 6272)           │       809,088
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ leaky_re_lu_2 (LeakyReLU)       │ (None, 6272)           │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ reshape (Reshape)               │ (None, 7, 7, 128)      │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose                │ (None, 14, 14, 128)    │       262,272
+## │ (Conv2DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ leaky_re_lu_3 (LeakyReLU)       │ (None, 14, 14, 128)    │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_transpose_1              │ (None, 28, 28, 128)    │       262,272
+## │ (Conv2DTranspose)               │                        │               │
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ leaky_re_lu_4 (LeakyReLU)       │ (None, 28, 28, 128)    │             0
+## ├─────────────────────────────────┼────────────────────────┼───────────────┤
+## │ conv2d_2 (Conv2D)               │ (None, 28, 28, 1)      │         6,273
+## └─────────────────────────────────┴────────────────────────┴───────────────┘
+##  Total params: 1,339,905 (5.11 MB)
+##  Trainable params: 1,339,905 (5.11 MB)
+##  Non-trainable params: 0 (0.00 B)
+

Here’s the key bit: the training loop. As you can see it is quite +straightforward. The training step function only takes 17 lines.

+
+# Instantiate one optimizer for the discriminator and another for the generator.
+d_optimizer <- optimizer_adam(learning_rate = 0.0003)
+g_optimizer <- optimizer_adam(learning_rate = 0.0004)
+
+# Instantiate a loss function.
+loss_fn <- loss_binary_crossentropy(from_logits = TRUE)
+
+train_step <- tf_function(function(real_images) {
+  # Sample random points in the latent space
+  c(batch_size, ...) %<-% op_shape(real_images)
+  random_latent_vectors <-
+    tf$random$normal(shape(batch_size, latent_dim))
+
+  # Decode them to fake images
+  generated_images <- generator(random_latent_vectors)
+
+  # Combine them with real images
+  combined_images <- tf$concat(list(generated_images, real_images),
+                               axis = 0L)
+
+  # Assemble labels discriminating real from fake images
+  labels <- tf$concat(list(tf$ones(shape(batch_size, 1)),
+                           tf$zeros(shape(batch_size, 1))),
+                      axis = 0L)
+
+  # Add random noise to the labels - important trick!
+  labels %<>% `+`(tf$random$uniform(tf$shape(.), maxval = 0.05))
+
+  # Train the discriminator
+  with(tf$GradientTape() %as% tape, {
+    predictions <- discriminator(combined_images)
+    d_loss <- loss_fn(labels, predictions)
+  })
+  grads <- tape$gradient(d_loss, discriminator$trainable_weights)
+  d_optimizer$apply(grads, discriminator$trainable_weights)
+
+  # Sample random points in the latent space
+  random_latent_vectors <-
+    tf$random$normal(shape(batch_size, latent_dim))
+
+  # Assemble labels that say "all real images"
+  misleading_labels <- tf$zeros(shape(batch_size, 1))
+
+  # Train the generator (note that we should *not* update the weights
+  # of the discriminator)!
+  with(tf$GradientTape() %as% tape, {
+    predictions <- discriminator(generator(random_latent_vectors))
+    g_loss <- loss_fn(misleading_labels, predictions)
+  })
+
+  grads <- tape$gradient(g_loss, generator$trainable_weights)
+  g_optimizer$apply(grads, generator$trainable_weights)
+
+  list(d_loss, g_loss, generated_images)
+})
+

Let’s train our GAN, by repeatedly calling train_step on +batches of images.

+

Since our discriminator and generator are convnets, you’re going to +want to run this code on a GPU.

+
+# Prepare the dataset. We use both the training & test MNIST digits.
+batch_size <- 64
+c(c(x_train, .), c(x_test, .)) %<-% dataset_mnist()
+all_digits <- op_concatenate(list(x_train, x_test))
+all_digits <- op_reshape(all_digits, c(-1, 28, 28, 1))
+dataset <- all_digits |>
+  tensor_slices_dataset() |>
+  dataset_map(\(x) op_cast(x, "float32") / 255) |>
+  dataset_shuffle(buffer_size = 1024) |>
+  dataset_batch(batch_size = batch_size)
+
+epochs <- 1 # In practice you need at least 20 epochs to generate nice digits.
+save_dir <- "./"
+
+for (epoch in seq_len(epochs)) {
+  cat("Start epoch: ", epoch, "\n")
+  step <- 0
+  train_iterator <- as_iterator(dataset)
+  while (!is.null(real_images <- iter_next(train_iterator))) {
+    step <- step + 1
+    # Train the discriminator & generator on one batch of real images.
+    c(d_loss, g_loss, generated_images) %<-% train_step(real_images)
+
+    # Logging.
+    if (step %% 200 == 0) {
+      # Print metrics
+      cat(sprintf("discriminator loss at step %d: %.2f\n", step, d_loss))
+      cat(sprintf("adversarial loss at step %d: %.2f\n", step, g_loss))
+    }
+
+    # To limit execution time we stop after 10 steps.
+    # Remove the lines below to actually train the model!
+    if (step > 10)
+      break
+  }
+}
+
## Start epoch:  1
+

That’s it! You’ll get nice-looking fake MNIST digits after just ~30s +of training on a GPU.

+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/articles/writing_your_own_callbacks.html b/docs/dev/articles/writing_your_own_callbacks.html new file mode 100644 index 0000000000..34342777b7 --- /dev/null +++ b/docs/dev/articles/writing_your_own_callbacks.html @@ -0,0 +1,1118 @@ + + + + + + + + +Writing your own callbacks • keras3 + + + + + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

Introduction +

+

A callback is a powerful tool to customize the behavior of a Keras +model during training, evaluation, or inference. Examples include +keras.callbacks.TensorBoard to visualize training progress +and results with TensorBoard, or +keras.callbacks.ModelCheckpoint to periodically save your +model during training.

+

In this guide, you will learn what a Keras callback is, what it can +do, and how you can build your own. We provide a few demos of simple +callback applications to get you started.

+
+
+

Setup +

+ +
+
+

Keras callbacks overview +

+

All callbacks subclass the keras.callbacks.Callback +class, and override a set of methods called at various stages of +training, testing, and predicting. Callbacks are useful to get a view on +internal states and statistics of the model during training.

+

You can pass a list of callbacks (as the keyword argument +callbacks) to the following model methods:

+ +
+
+

An overview of callback methods +

+
+

Global methods +

+
+

+on_(train|test|predict)_begin(logs = NULL) +

+

Called at the beginning of +fit/evaluate/predict.

+
+
+

+on_(train|test|predict)_end(logs = NULL) +

+

Called at the end of +fit/evaluate/predict.

+
+
+
+

Batch-level methods for training/testing/predicting +

+
+

+on_(train|test|predict)_batch_begin(batch, logs = NULL) +

+

Called right before processing a batch during +training/testing/predicting.

+
+
+

+on_(train|test|predict)_batch_end(batch, logs = NULL) +

+

Called at the end of training/testing/predicting a batch. Within this +method, logs is a named list containing the metrics +results.

+
+
+
+

Epoch-level methods (training only) +

+
+

+on_epoch_begin(epoch, logs = NULL) +

+

Called at the beginning of an epoch during training.

+
+
+

+on_epoch_end(epoch, logs = NULL) +

+

Called at the end of an epoch during training.

+
+
+
+
+

A basic example +

+

Let’s take a look at a concrete example. To get started, let’s import +tensorflow and define a simple Sequential Keras model:

+
+# Define the Keras model to add callbacks to
+get_model <- function() {
+  model <- keras_model_sequential()
+  model |> layer_dense(units = 1)
+  model |> compile(
+    optimizer = optimizer_rmsprop(learning_rate = 0.1),
+    loss = "mean_squared_error",
+    metrics = "mean_absolute_error"
+  )
+  model
+}
+

Then, load the MNIST data for training and testing from Keras +datasets API:

+
+# Load example MNIST data and pre-process it
+mnist <- dataset_mnist()
+
+flatten_and_rescale <- function(x) {
+  x <- array_reshape(x, c(-1, 784))
+  x <- x / 255
+  x
+}
+
+mnist$train$x <- flatten_and_rescale(mnist$train$x)
+mnist$test$x  <- flatten_and_rescale(mnist$test$x)
+
+# limit to 1000 samples
+n <- 1000
+mnist$train$x <- mnist$train$x[1:n,]
+mnist$train$y <- mnist$train$y[1:n]
+mnist$test$x  <- mnist$test$x[1:n,]
+mnist$test$y  <- mnist$test$y[1:n]
+

Now, define a simple custom callback that logs:

+
    +
  • When fit/evaluate/predict +starts & ends
  • +
  • When each epoch starts & ends
  • +
  • When each training batch starts & ends
  • +
  • When each evaluation (test) batch starts & ends
  • +
  • When each inference (prediction) batch starts & ends
  • +
+
+show <- function(msg, logs) {
+  cat(glue::glue(msg, .envir = parent.frame()),
+      "got logs: ", sep = "; ")
+  str(logs); cat("\n")
+}
+
+callback_custom <- Callback(
+  "CustomCallback",
+  on_train_begin         = \(logs = NULL)        show("Starting training", logs),
+  on_epoch_begin         = \(epoch, logs = NULL) show("Start epoch {epoch} of training", logs),
+  on_train_batch_begin   = \(batch, logs = NULL) show("...Training: start of batch {batch}", logs),
+  on_train_batch_end     = \(batch, logs = NULL) show("...Training: end of batch {batch}",  logs),
+  on_epoch_end           = \(epoch, logs = NULL) show("End epoch {epoch} of training", logs),
+  on_train_end           = \(logs = NULL)        show("Stop training", logs),
+
+
+  on_test_begin          = \(logs = NULL)        show("Start testing", logs),
+  on_test_batch_begin    = \(batch, logs = NULL) show("...Evaluating: start of batch {batch}", logs),
+  on_test_batch_end      = \(batch, logs = NULL) show("...Evaluating: end of batch {batch}", logs),
+  on_test_end            = \(logs = NULL)        show("Stop testing", logs),
+
+  on_predict_begin       = \(logs = NULL)        show("Start predicting", logs),
+  on_predict_end         = \(logs = NULL)        show("Stop predicting", logs),
+  on_predict_batch_begin = \(batch, logs = NULL) show("...Predicting: start of batch {batch}", logs),
+  on_predict_batch_end   = \(batch, logs = NULL) show("...Predicting: end of batch {batch}", logs),
+)
+

Let’s try it out:

+
+model <- get_model()
+model |> fit(
+  mnist$train$x, mnist$train$y,
+  batch_size = 128,
+  epochs = 2,
+  verbose = 0,
+  validation_split = 0.5,
+  callbacks = list(callback_custom())
+)
+
## Starting training; got logs:  Named list()
+##
+## Start epoch 1 of training; got logs:  Named list()
+##
+## ...Training: start of batch 1; got logs:  Named list()
+##
+## ...Training: end of batch 1; got logs: List of 2
+##  $ loss               : num 25.9
+##  $ mean_absolute_error: num 4.19
+##
+## ...Training: start of batch 2; got logs:  Named list()
+##
+## ...Training: end of batch 2; got logs: List of 2
+##  $ loss               : num 433
+##  $ mean_absolute_error: num 15.5
+##
+## ...Training: start of batch 3; got logs:  Named list()
+##
+## ...Training: end of batch 3; got logs: List of 2
+##  $ loss               : num 297
+##  $ mean_absolute_error: num 11.8
+##
+## ...Training: start of batch 4; got logs:  Named list()
+##
+## ...Training: end of batch 4; got logs: List of 2
+##  $ loss               : num 231
+##  $ mean_absolute_error: num 9.68
+##
+## Start testing; got logs:  Named list()
+##
+## ...Evaluating: start of batch 1; got logs:  Named list()
+##
+## ...Evaluating: end of batch 1; got logs: List of 2
+##  $ loss               : num 8.1
+##  $ mean_absolute_error: num 2.3
+##
+## ...Evaluating: start of batch 2; got logs:  Named list()
+##
+## ...Evaluating: end of batch 2; got logs: List of 2
+##  $ loss               : num 7.58
+##  $ mean_absolute_error: num 2.23
+##
+## ...Evaluating: start of batch 3; got logs:  Named list()
+##
+## ...Evaluating: end of batch 3; got logs: List of 2
+##  $ loss               : num 7.38
+##  $ mean_absolute_error: num 2.21
+##
+## ...Evaluating: start of batch 4; got logs:  Named list()
+##
+## ...Evaluating: end of batch 4; got logs: List of 2
+##  $ loss               : num 7.3
+##  $ mean_absolute_error: num 2.21
+##
+## Stop testing; got logs: List of 2
+##  $ loss               : num 7.3
+##  $ mean_absolute_error: num 2.21
+##
+## End epoch 1 of training; got logs: List of 4
+##  $ loss                   : num 231
+##  $ mean_absolute_error    : num 9.68
+##  $ val_loss               : num 7.3
+##  $ val_mean_absolute_error: num 2.21
+##
+## Start epoch 2 of training; got logs:  Named list()
+##
+## ...Training: start of batch 1; got logs:  Named list()
+##
+## ...Training: end of batch 1; got logs: List of 2
+##  $ loss               : num 7.44
+##  $ mean_absolute_error: num 2.27
+##
+## ...Training: start of batch 2; got logs:  Named list()
+##
+## ...Training: end of batch 2; got logs: List of 2
+##  $ loss               : num 6.81
+##  $ mean_absolute_error: num 2.16
+##
+## ...Training: start of batch 3; got logs:  Named list()
+##
+## ...Training: end of batch 3; got logs: List of 2
+##  $ loss               : num 6.12
+##  $ mean_absolute_error: num 2.06
+##
+## ...Training: start of batch 4; got logs:  Named list()
+##
+## ...Training: end of batch 4; got logs: List of 2
+##  $ loss               : num 6.08
+##  $ mean_absolute_error: num 2.04
+##
+## Start testing; got logs:  Named list()
+##
+## ...Evaluating: start of batch 1; got logs:  Named list()
+##
+## ...Evaluating: end of batch 1; got logs: List of 2
+##  $ loss               : num 5.54
+##  $ mean_absolute_error: num 1.92
+##
+## ...Evaluating: start of batch 2; got logs:  Named list()
+##
+## ...Evaluating: end of batch 2; got logs: List of 2
+##  $ loss               : num 5.31
+##  $ mean_absolute_error: num 1.87
+##
+## ...Evaluating: start of batch 3; got logs:  Named list()
+##
+## ...Evaluating: end of batch 3; got logs: List of 2
+##  $ loss               : num 5.11
+##  $ mean_absolute_error: num 1.8
+##
+## ...Evaluating: start of batch 4; got logs:  Named list()
+##
+## ...Evaluating: end of batch 4; got logs: List of 2
+##  $ loss               : num 5.15
+##  $ mean_absolute_error: num 1.82
+##
+## Stop testing; got logs: List of 2
+##  $ loss               : num 5.15
+##  $ mean_absolute_error: num 1.82
+##
+## End epoch 2 of training; got logs: List of 4
+##  $ loss                   : num 6.08
+##  $ mean_absolute_error    : num 2.04
+##  $ val_loss               : num 5.15
+##  $ val_mean_absolute_error: num 1.82
+##
+## Stop training; got logs: List of 4
+##  $ loss                   : num 6.08
+##  $ mean_absolute_error    : num 2.04
+##  $ val_loss               : num 5.15
+##  $ val_mean_absolute_error: num 1.82
+
+res <- model |> evaluate(
+  mnist$test$x, mnist$test$y,
+  batch_size = 128, verbose = 0,
+  callbacks = list(callback_custom())
+)
+
## Start testing; got logs:  Named list()
+##
+## ...Evaluating: start of batch 1; got logs:  Named list()
+##
+## ...Evaluating: end of batch 1; got logs: List of 2
+##  $ loss               : num 5.2
+##  $ mean_absolute_error: num 1.84
+##
+## ...Evaluating: start of batch 2; got logs:  Named list()
+##
+## ...Evaluating: end of batch 2; got logs: List of 2
+##  $ loss               : num 4.62
+##  $ mean_absolute_error: num 1.73
+##
+## ...Evaluating: start of batch 3; got logs:  Named list()
+##
+## ...Evaluating: end of batch 3; got logs: List of 2
+##  $ loss               : num 4.61
+##  $ mean_absolute_error: num 1.74
+##
+## ...Evaluating: start of batch 4; got logs:  Named list()
+##
+## ...Evaluating: end of batch 4; got logs: List of 2
+##  $ loss               : num 4.65
+##  $ mean_absolute_error: num 1.75
+##
+## ...Evaluating: start of batch 5; got logs:  Named list()
+##
+## ...Evaluating: end of batch 5; got logs: List of 2
+##  $ loss               : num 4.84
+##  $ mean_absolute_error: num 1.77
+##
+## ...Evaluating: start of batch 6; got logs:  Named list()
+##
+## ...Evaluating: end of batch 6; got logs: List of 2
+##  $ loss               : num 4.76
+##  $ mean_absolute_error: num 1.76
+##
+## ...Evaluating: start of batch 7; got logs:  Named list()
+##
+## ...Evaluating: end of batch 7; got logs: List of 2
+##  $ loss               : num 4.74
+##  $ mean_absolute_error: num 1.76
+##
+## ...Evaluating: start of batch 8; got logs:  Named list()
+##
+## ...Evaluating: end of batch 8; got logs: List of 2
+##  $ loss               : num 4.67
+##  $ mean_absolute_error: num 1.75
+##
+## Stop testing; got logs: List of 2
+##  $ loss               : num 4.67
+##  $ mean_absolute_error: num 1.75
+
+res <- model |> predict(
+  mnist$test$x,
+  batch_size = 128, verbose = 0,
+  callbacks = list(callback_custom())
+)
+
## Start predicting; got logs:  Named list()
+##
+## ...Predicting: start of batch 1; got logs:  Named list()
+##
+## ...Predicting: end of batch 1; got logs: List of 1
+##  $ outputs:<tf.Tensor: shape=(128, 1), dtype=float32, numpy=…>
+##
+## ...Predicting: start of batch 2; got logs:  Named list()
+##
+## ...Predicting: end of batch 2; got logs: List of 1
+##  $ outputs:<tf.Tensor: shape=(128, 1), dtype=float32, numpy=…>
+##
+## ...Predicting: start of batch 3; got logs:  Named list()
+##
+## ...Predicting: end of batch 3; got logs: List of 1
+##  $ outputs:<tf.Tensor: shape=(128, 1), dtype=float32, numpy=…>
+##
+## ...Predicting: start of batch 4; got logs:  Named list()
+##
+## ...Predicting: end of batch 4; got logs: List of 1
+##  $ outputs:<tf.Tensor: shape=(128, 1), dtype=float32, numpy=…>
+##
+## ...Predicting: start of batch 5; got logs:  Named list()
+##
+## ...Predicting: end of batch 5; got logs: List of 1
+##  $ outputs:<tf.Tensor: shape=(128, 1), dtype=float32, numpy=…>
+##
+## ...Predicting: start of batch 6; got logs:  Named list()
+##
+## ...Predicting: end of batch 6; got logs: List of 1
+##  $ outputs:<tf.Tensor: shape=(128, 1), dtype=float32, numpy=…>
+##
+## ...Predicting: start of batch 7; got logs:  Named list()
+##
+## ...Predicting: end of batch 7; got logs: List of 1
+##  $ outputs:<tf.Tensor: shape=(128, 1), dtype=float32, numpy=…>
+##
+## ...Predicting: start of batch 8; got logs:  Named list()
+##
+## ...Predicting: end of batch 8; got logs: List of 1
+##  $ outputs:<tf.Tensor: shape=(104, 1), dtype=float32, numpy=…>
+##
+## Stop predicting; got logs:  Named list()
+
+

Usage of logs list +

+

The logs named list contains the loss value, and all the +metrics at the end of a batch or epoch. Example includes the loss and +mean absolute error.

+
+callback_print_loss_and_mae <- Callback(
+  "LossAndErrorPrintingCallback",
+
+  on_train_batch_end = function(batch, logs = NULL)
+    cat(sprintf("Up to batch %i, the average loss is %7.2f.\n",
+                batch,  logs$loss)),
+
+  on_test_batch_end = function(batch, logs = NULL)
+    cat(sprintf("Up to batch %i, the average loss is %7.2f.\n",
+                batch, logs$loss)),
+
+  on_epoch_end = function(epoch, logs = NULL)
+    cat(sprintf(
+      "The average loss for epoch %2i is %9.2f and mean absolute error is %7.2f.\n",
+      epoch, logs$loss, logs$mean_absolute_error
+    ))
+)
+
+
+model <- get_model()
+model |> fit(
+  mnist$train$x, mnist$train$y,
+  epochs = 2, verbose = 0, batch_size = 128,
+  callbacks = list(callback_print_loss_and_mae())
+)
+
## Up to batch 1, the average loss is   25.12.
+## Up to batch 2, the average loss is  398.92.
+## Up to batch 3, the average loss is  274.04.
+## Up to batch 4, the average loss is  208.32.
+## Up to batch 5, the average loss is  168.15.
+## Up to batch 6, the average loss is  141.31.
+## Up to batch 7, the average loss is  122.19.
+## Up to batch 8, the average loss is  110.05.
+## The average loss for epoch  1 is    110.05 and mean absolute error is    5.79.
+## Up to batch 1, the average loss is    4.71.
+## Up to batch 2, the average loss is    4.74.
+## Up to batch 3, the average loss is    4.81.
+## Up to batch 4, the average loss is    5.07.
+## Up to batch 5, the average loss is    5.08.
+## Up to batch 6, the average loss is    5.09.
+## Up to batch 7, the average loss is    5.19.
+## Up to batch 8, the average loss is    5.51.
+## The average loss for epoch  2 is      5.51 and mean absolute error is    1.90.
+
+res = model |> evaluate(
+  mnist$test$x, mnist$test$y,
+  verbose = 0, batch_size = 128,
+  callbacks = list(callback_print_loss_and_mae())
+)
+
## Up to batch 1, the average loss is   15.86.
+## Up to batch 2, the average loss is   16.13.
+## Up to batch 3, the average loss is   16.02.
+## Up to batch 4, the average loss is   16.11.
+## Up to batch 5, the average loss is   16.23.
+## Up to batch 6, the average loss is   16.68.
+## Up to batch 7, the average loss is   16.61.
+## Up to batch 8, the average loss is   16.54.
+

For more information about callbacks, you can check out the Keras +callback API documentation.

+
+
+
+

Usage of self$model attribute +

+

In addition to receiving log information when one of their methods is +called, callbacks have access to the model associated with the current +round of training/evaluation/inference: self$model.

+

Here are of few of the things you can do with self$model +in a callback:

+
    +
  • Set self$model$stop_training <- TRUE to immediately +interrupt training.
  • +
  • Mutate hyperparameters of the optimizer (available as +self$model$optimizer), such as +self$model$optimizer$learning_rate.
  • +
  • Save the model at period intervals.
  • +
  • Record the output of model |> predict() on a few +test samples at the end of each epoch, to use as a sanity check during +training.
  • +
  • Extract visualizations of intermediate features at the end of each +epoch, to monitor what the model is learning over time.
  • +
  • etc.
  • +
+

Let’s see this in action in a couple of examples.

+
+
+

Examples of Keras callback applications +

+
+

Early stopping at minimum loss +

+

This first example shows the creation of a Callback that +stops training when the minimum of loss has been reached, by setting the +attribute self$model$stop_training (boolean). Optionally, +you can provide an argument patience to specify how many +epochs we should wait before stopping after having reached a local +minimum.

+

callback_early_stopping() provides a more complete and +general implementation.

+
+callback_early_stopping_at_min_loss <- Callback(
+  "EarlyStoppingAtMinLoss",
+  `__doc__` =
+    "Stop training when the loss is at its min, i.e. the loss stops decreasing.
+
+    Arguments:
+        patience: Number of epochs to wait after min has been hit. After this
+        number of no improvement, training stops.
+    ",
+
+  initialize = function(patience = 0) {
+    super$initialize()
+    self$patience <- patience
+    # best_weights to store the weights at which the minimum loss occurs.
+    self$best_weights <- NULL
+  },
+
+  on_train_begin = function(logs = NULL) {
+    # The number of epoch it has waited when loss is no longer minimum.
+    self$wait <- 0
+    # The epoch the training stops at.
+    self$stopped_epoch <- 0
+    # Initialize the best as infinity.
+    self$best <- Inf
+  },
+
+  on_epoch_end = function(epoch, logs = NULL) {
+    current <- logs$loss
+    if (current < self$best) {
+      self$best <- current
+      self$wait <- 0L
+      # Record the best weights if current results is better (less).
+      self$best_weights <- get_weights(self$model)
+    } else {
+      add(self$wait) <- 1L
+      if (self$wait >= self$patience) {
+        self$stopped_epoch <- epoch
+        self$model$stop_training <- TRUE
+        cat("Restoring model weights from the end of the best epoch.\n")
+        model$set_weights(self$best_weights)
+      }
+    }
+  },
+
+  on_train_end = function(logs = NULL)
+    if (self$stopped_epoch > 0)
+      cat(sprintf("Epoch %05d: early stopping\n", self$stopped_epoch + 1))
+)
+`add<-` <- `+`
+
+
+model <- get_model()
+model |> fit(
+  mnist$train$x,
+  mnist$train$y,
+  epochs = 30,
+  batch_size = 64,
+  verbose = 0,
+  callbacks = list(callback_print_loss_and_mae(),
+                   callback_early_stopping_at_min_loss())
+)
+
## Up to batch 1, the average loss is   30.54.
+## Up to batch 2, the average loss is  513.27.
+## Up to batch 3, the average loss is  352.60.
+## Up to batch 4, the average loss is  266.37.
+## Up to batch 5, the average loss is  214.68.
+## Up to batch 6, the average loss is  179.97.
+## Up to batch 7, the average loss is  155.06.
+## Up to batch 8, the average loss is  136.59.
+## Up to batch 9, the average loss is  121.96.
+## Up to batch 10, the average loss is  110.28.
+## Up to batch 11, the average loss is  100.72.
+## Up to batch 12, the average loss is   92.71.
+## Up to batch 13, the average loss is   85.95.
+## Up to batch 14, the average loss is   80.21.
+## Up to batch 15, the average loss is   75.17.
+## Up to batch 16, the average loss is   72.48.
+## The average loss for epoch  1 is     72.48 and mean absolute error is    4.08.
+## Up to batch 1, the average loss is    7.98.
+## Up to batch 2, the average loss is    9.92.
+## Up to batch 3, the average loss is   12.88.
+## Up to batch 4, the average loss is   16.61.
+## Up to batch 5, the average loss is   20.49.
+## Up to batch 6, the average loss is   26.14.
+## Up to batch 7, the average loss is   30.44.
+## Up to batch 8, the average loss is   33.76.
+## Up to batch 9, the average loss is   36.32.
+## Up to batch 10, the average loss is   35.26.
+## Up to batch 11, the average loss is   34.22.
+## Up to batch 12, the average loss is   33.53.
+## Up to batch 13, the average loss is   32.84.
+## Up to batch 14, the average loss is   31.80.
+## Up to batch 15, the average loss is   31.39.
+## Up to batch 16, the average loss is   31.45.
+## The average loss for epoch  2 is     31.45 and mean absolute error is    4.82.
+## Up to batch 1, the average loss is   39.60.
+## Up to batch 2, the average loss is   41.95.
+## Up to batch 3, the average loss is   41.29.
+## Up to batch 4, the average loss is   36.77.
+## Up to batch 5, the average loss is   32.08.
+## Up to batch 6, the average loss is   28.17.
+## Up to batch 7, the average loss is   25.33.
+## Up to batch 8, the average loss is   23.56.
+## Up to batch 9, the average loss is   22.28.
+## Up to batch 10, the average loss is   21.22.
+## Up to batch 11, the average loss is   20.87.
+## Up to batch 12, the average loss is   22.25.
+## Up to batch 13, the average loss is   25.08.
+## Up to batch 14, the average loss is   27.87.
+## Up to batch 15, the average loss is   31.72.
+## Up to batch 16, the average loss is   33.21.
+## The average loss for epoch  3 is     33.21 and mean absolute error is    4.79.
+## Restoring model weights from the end of the best epoch.
+## Epoch 00004: early stopping
+
+
+

Learning rate scheduling +

+

In this example, we show how a custom Callback can be used to +dynamically change the learning rate of the optimizer during the course +of training.

+

See keras$callbacks$LearningRateScheduler for a more +general implementations (in RStudio, press F1 while the cursor is over +LearningRateScheduler and a browser will open to this +page).

+
+callback_custom_learning_rate_scheduler <- Callback(
+  "CustomLearningRateScheduler",
+  `__doc__` =
+  "Learning rate scheduler which sets the learning rate according to schedule.
+
+    Arguments:
+        schedule: a function that takes an epoch index
+            (integer, indexed from 0) and current learning rate
+            as inputs and returns a new learning rate as output (float).
+    ",
+
+  initialize = function(schedule) {
+    super$initialize()
+    self$schedule <- schedule
+  },
+
+  on_epoch_begin = function(epoch, logs = NULL) {
+    ## When in doubt about what types of objects are in scope (e.g., self$model)
+    ## use a debugger to interact with the actual objects at the console!
+    # browser()
+
+    if (!"learning_rate" %in% names(self$model$optimizer))
+      stop('Optimizer must have a "learning_rate" attribute.')
+
+    # # Get the current learning rate from model's optimizer.
+    # use as.numeric() to convert the keras variablea to an R numeric
+    lr <- as.numeric(self$model$optimizer$learning_rate)
+    # # Call schedule function to get the scheduled learning rate.
+    scheduled_lr <- self$schedule(epoch, lr)
+    # # Set the value back to the optimizer before this epoch starts
+    optimizer <- self$model$optimizer
+    optimizer$learning_rate <- scheduled_lr
+    cat(sprintf("\nEpoch %03d: Learning rate is %6.4f.\n", epoch, scheduled_lr))
+  }
+)
+
+LR_SCHEDULE <- tibble::tribble(
+  ~start_epoch, ~learning_rate,
+             0,            0.1,
+             3,           0.05,
+             6,           0.01,
+             9,          0.005,
+            12,          0.001,
+  )
+
+last <- function(x) x[length(x)]
+lr_schedule <- function(epoch, learning_rate) {
+  "Helper function to retrieve the scheduled learning rate based on epoch."
+  with(LR_SCHEDULE, learning_rate[last(which(epoch >= start_epoch))])
+}
+
+model <- get_model()
+model |> fit(
+  mnist$train$x,
+  mnist$train$y,
+  epochs = 14,
+  batch_size = 64,
+  verbose = 0,
+  callbacks = list(
+    callback_print_loss_and_mae(),
+    callback_custom_learning_rate_scheduler(lr_schedule)
+  )
+)
+
##
+## Epoch 001: Learning rate is 0.1000.
+## Up to batch 1, the average loss is   29.36.
+## Up to batch 2, the average loss is  513.95.
+## Up to batch 3, the average loss is  352.70.
+## Up to batch 4, the average loss is  266.46.
+## Up to batch 5, the average loss is  214.73.
+## Up to batch 6, the average loss is  180.00.
+## Up to batch 7, the average loss is  155.05.
+## Up to batch 8, the average loss is  136.64.
+## Up to batch 9, the average loss is  121.97.
+## Up to batch 10, the average loss is  110.30.
+## Up to batch 11, the average loss is  100.76.
+## Up to batch 12, the average loss is   92.74.
+## Up to batch 13, the average loss is   85.95.
+## Up to batch 14, the average loss is   80.18.
+## Up to batch 15, the average loss is   75.11.
+## Up to batch 16, the average loss is   72.38.
+## The average loss for epoch  1 is     72.38 and mean absolute error is    4.04.
+##
+## Epoch 002: Learning rate is 0.1000.
+## Up to batch 1, the average loss is    6.95.
+## Up to batch 2, the average loss is    8.71.
+## Up to batch 3, the average loss is   11.42.
+## Up to batch 4, the average loss is   15.15.
+## Up to batch 5, the average loss is   19.28.
+## Up to batch 6, the average loss is   25.54.
+## Up to batch 7, the average loss is   30.38.
+## Up to batch 8, the average loss is   33.95.
+## Up to batch 9, the average loss is   36.58.
+## Up to batch 10, the average loss is   35.46.
+## Up to batch 11, the average loss is   34.34.
+## Up to batch 12, the average loss is   33.51.
+## Up to batch 13, the average loss is   32.67.
+## Up to batch 14, the average loss is   31.54.
+## Up to batch 15, the average loss is   31.05.
+## Up to batch 16, the average loss is   31.09.
+## The average loss for epoch  2 is     31.09 and mean absolute error is    4.77.
+##
+## Epoch 003: Learning rate is 0.0500.
+## Up to batch 1, the average loss is   40.40.
+## Up to batch 2, the average loss is   22.33.
+## Up to batch 3, the average loss is   16.18.
+## Up to batch 4, the average loss is   13.09.
+## Up to batch 5, the average loss is   11.48.
+## Up to batch 6, the average loss is   10.21.
+## Up to batch 7, the average loss is    9.22.
+## Up to batch 8, the average loss is    8.70.
+## Up to batch 9, the average loss is    8.16.
+## Up to batch 10, the average loss is    7.80.
+## Up to batch 11, the average loss is    7.50.
+## Up to batch 12, the average loss is    7.17.
+## Up to batch 13, the average loss is    6.89.
+## Up to batch 14, the average loss is    6.70.
+## Up to batch 15, the average loss is    6.52.
+## Up to batch 16, the average loss is    6.54.
+## The average loss for epoch  3 is      6.54 and mean absolute error is    1.93.
+##
+## Epoch 004: Learning rate is 0.0500.
+## Up to batch 1, the average loss is    8.74.
+## Up to batch 2, the average loss is    8.34.
+## Up to batch 3, the average loss is    9.09.
+## Up to batch 4, the average loss is    9.72.
+## Up to batch 5, the average loss is   10.48.
+## Up to batch 6, the average loss is   11.69.
+## Up to batch 7, the average loss is   11.83.
+## Up to batch 8, the average loss is   11.56.
+## Up to batch 9, the average loss is   11.24.
+## Up to batch 10, the average loss is   10.84.
+## Up to batch 11, the average loss is   10.66.
+## Up to batch 12, the average loss is   10.44.
+## Up to batch 13, the average loss is   10.21.
+## Up to batch 14, the average loss is   10.06.
+## Up to batch 15, the average loss is   10.00.
+## Up to batch 16, the average loss is   10.20.
+## The average loss for epoch  4 is     10.20 and mean absolute error is    2.71.
+##
+## Epoch 005: Learning rate is 0.0500.
+## Up to batch 1, the average loss is   17.26.
+## Up to batch 2, the average loss is   14.09.
+## Up to batch 3, the average loss is   12.67.
+## Up to batch 4, the average loss is   11.44.
+## Up to batch 5, the average loss is   10.54.
+## Up to batch 6, the average loss is   10.10.
+## Up to batch 7, the average loss is    9.53.
+## Up to batch 8, the average loss is    9.17.
+## Up to batch 9, the average loss is    8.78.
+## Up to batch 10, the average loss is    8.49.
+## Up to batch 11, the average loss is    8.50.
+## Up to batch 12, the average loss is    8.59.
+## Up to batch 13, the average loss is    8.68.
+## Up to batch 14, the average loss is    8.86.
+## Up to batch 15, the average loss is    9.17.
+## Up to batch 16, the average loss is    9.53.
+## The average loss for epoch  5 is      9.53 and mean absolute error is    2.58.
+##
+## Epoch 006: Learning rate is 0.0100.
+## Up to batch 1, the average loss is   17.04.
+## Up to batch 2, the average loss is   14.85.
+## Up to batch 3, the average loss is   11.53.
+## Up to batch 4, the average loss is    9.65.
+## Up to batch 5, the average loss is    8.44.
+## Up to batch 6, the average loss is    7.50.
+## Up to batch 7, the average loss is    6.74.
+## Up to batch 8, the average loss is    6.56.
+## Up to batch 9, the average loss is    6.18.
+## Up to batch 10, the average loss is    5.87.
+## Up to batch 11, the average loss is    5.63.
+## Up to batch 12, the average loss is    5.45.
+## Up to batch 13, the average loss is    5.23.
+## Up to batch 14, the average loss is    5.12.
+## Up to batch 15, the average loss is    4.96.
+## Up to batch 16, the average loss is    4.91.
+## The average loss for epoch  6 is      4.91 and mean absolute error is    1.67.
+##
+## Epoch 007: Learning rate is 0.0100.
+## Up to batch 1, the average loss is    3.65.
+## Up to batch 2, the average loss is    3.04.
+## Up to batch 3, the average loss is    2.88.
+## Up to batch 4, the average loss is    2.85.
+## Up to batch 5, the average loss is    2.88.
+## Up to batch 6, the average loss is    2.81.
+## Up to batch 7, the average loss is    2.70.
+## Up to batch 8, the average loss is    2.96.
+## Up to batch 9, the average loss is    2.96.
+## Up to batch 10, the average loss is    2.93.
+## Up to batch 11, the average loss is    2.95.
+## Up to batch 12, the average loss is    2.98.
+## Up to batch 13, the average loss is    2.97.
+## Up to batch 14, the average loss is    3.01.
+## Up to batch 15, the average loss is    3.00.
+## Up to batch 16, the average loss is    3.05.
+## The average loss for epoch  7 is      3.05 and mean absolute error is    1.34.
+##
+## Epoch 008: Learning rate is 0.0100.
+## Up to batch 1, the average loss is    3.69.
+## Up to batch 2, the average loss is    3.21.
+## Up to batch 3, the average loss is    3.00.
+## Up to batch 4, the average loss is    2.91.
+## Up to batch 5, the average loss is    2.94.
+## Up to batch 6, the average loss is    2.85.
+## Up to batch 7, the average loss is    2.72.
+## Up to batch 8, the average loss is    2.95.
+## Up to batch 9, the average loss is    2.97.
+## Up to batch 10, the average loss is    2.93.
+## Up to batch 11, the average loss is    2.96.
+## Up to batch 12, the average loss is    2.98.
+## Up to batch 13, the average loss is    2.99.
+## Up to batch 14, the average loss is    3.05.
+## Up to batch 15, the average loss is    3.08.
+## Up to batch 16, the average loss is    3.14.
+## The average loss for epoch  8 is      3.14 and mean absolute error is    1.36.
+##
+## Epoch 009: Learning rate is 0.0050.
+## Up to batch 1, the average loss is    3.71.
+## Up to batch 2, the average loss is    2.93.
+## Up to batch 3, the average loss is    2.76.
+## Up to batch 4, the average loss is    2.70.
+## Up to batch 5, the average loss is    2.76.
+## Up to batch 6, the average loss is    2.69.
+## Up to batch 7, the average loss is    2.57.
+## Up to batch 8, the average loss is    2.79.
+## Up to batch 9, the average loss is    2.80.
+## Up to batch 10, the average loss is    2.77.
+## Up to batch 11, the average loss is    2.79.
+## Up to batch 12, the average loss is    2.80.
+## Up to batch 13, the average loss is    2.78.
+## Up to batch 14, the average loss is    2.81.
+## Up to batch 15, the average loss is    2.80.
+## Up to batch 16, the average loss is    2.83.
+## The average loss for epoch  9 is      2.83 and mean absolute error is    1.28.
+##
+## Epoch 010: Learning rate is 0.0050.
+## Up to batch 1, the average loss is    3.02.
+## Up to batch 2, the average loss is    2.69.
+## Up to batch 3, the average loss is    2.58.
+## Up to batch 4, the average loss is    2.57.
+## Up to batch 5, the average loss is    2.65.
+## Up to batch 6, the average loss is    2.60.
+## Up to batch 7, the average loss is    2.48.
+## Up to batch 8, the average loss is    2.72.
+## Up to batch 9, the average loss is    2.74.
+## Up to batch 10, the average loss is    2.71.
+## Up to batch 11, the average loss is    2.74.
+## Up to batch 12, the average loss is    2.75.
+## Up to batch 13, the average loss is    2.74.
+## Up to batch 14, the average loss is    2.77.
+## Up to batch 15, the average loss is    2.77.
+## Up to batch 16, the average loss is    2.80.
+## The average loss for epoch 10 is      2.80 and mean absolute error is    1.27.
+##
+## Epoch 011: Learning rate is 0.0050.
+## Up to batch 1, the average loss is    3.01.
+## Up to batch 2, the average loss is    2.69.
+## Up to batch 3, the average loss is    2.58.
+## Up to batch 4, the average loss is    2.56.
+## Up to batch 5, the average loss is    2.63.
+## Up to batch 6, the average loss is    2.58.
+## Up to batch 7, the average loss is    2.47.
+## Up to batch 8, the average loss is    2.70.
+## Up to batch 9, the average loss is    2.72.
+## Up to batch 10, the average loss is    2.69.
+## Up to batch 11, the average loss is    2.71.
+## Up to batch 12, the average loss is    2.72.
+## Up to batch 13, the average loss is    2.71.
+## Up to batch 14, the average loss is    2.75.
+## Up to batch 15, the average loss is    2.74.
+## Up to batch 16, the average loss is    2.77.
+## The average loss for epoch 11 is      2.77 and mean absolute error is    1.27.
+##
+## Epoch 012: Learning rate is 0.0010.
+## Up to batch 1, the average loss is    2.96.
+## Up to batch 2, the average loss is    2.53.
+## Up to batch 3, the average loss is    2.47.
+## Up to batch 4, the average loss is    2.46.
+## Up to batch 5, the average loss is    2.54.
+## Up to batch 6, the average loss is    2.48.
+## Up to batch 7, the average loss is    2.39.
+## Up to batch 8, the average loss is    2.60.
+## Up to batch 9, the average loss is    2.62.
+## Up to batch 10, the average loss is    2.59.
+## Up to batch 11, the average loss is    2.61.
+## Up to batch 12, the average loss is    2.62.
+## Up to batch 13, the average loss is    2.60.
+## Up to batch 14, the average loss is    2.64.
+## Up to batch 15, the average loss is    2.62.
+## Up to batch 16, the average loss is    2.64.
+## The average loss for epoch 12 is      2.64 and mean absolute error is    1.24.
+##
+## Epoch 013: Learning rate is 0.0010.
+## Up to batch 1, the average loss is    2.82.
+## Up to batch 2, the average loss is    2.46.
+## Up to batch 3, the average loss is    2.42.
+## Up to batch 4, the average loss is    2.42.
+## Up to batch 5, the average loss is    2.50.
+## Up to batch 6, the average loss is    2.45.
+## Up to batch 7, the average loss is    2.36.
+## Up to batch 8, the average loss is    2.57.
+## Up to batch 9, the average loss is    2.59.
+## Up to batch 10, the average loss is    2.57.
+## Up to batch 11, the average loss is    2.59.
+## Up to batch 12, the average loss is    2.60.
+## Up to batch 13, the average loss is    2.59.
+## Up to batch 14, the average loss is    2.62.
+## Up to batch 15, the average loss is    2.61.
+## Up to batch 16, the average loss is    2.63.
+## The average loss for epoch 13 is      2.63 and mean absolute error is    1.23.
+##
+## Epoch 014: Learning rate is 0.0010.
+## Up to batch 1, the average loss is    2.79.
+## Up to batch 2, the average loss is    2.44.
+## Up to batch 3, the average loss is    2.40.
+## Up to batch 4, the average loss is    2.41.
+## Up to batch 5, the average loss is    2.49.
+## Up to batch 6, the average loss is    2.44.
+## Up to batch 7, the average loss is    2.34.
+## Up to batch 8, the average loss is    2.56.
+## Up to batch 9, the average loss is    2.58.
+## Up to batch 10, the average loss is    2.56.
+## Up to batch 11, the average loss is    2.58.
+## Up to batch 12, the average loss is    2.59.
+## Up to batch 13, the average loss is    2.58.
+## Up to batch 14, the average loss is    2.61.
+## Up to batch 15, the average loss is    2.60.
+## Up to batch 16, the average loss is    2.62.
+## The average loss for epoch 14 is      2.62 and mean absolute error is    1.23.
+
+
+

Built-in Keras callbacks +

+

Be sure to check out the existing Keras callbacks by reading the API +docs. Applications include logging to CSV, saving the model, +visualizing metrics in TensorBoard, and a lot more!

+
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/docs/dev/authors.html b/docs/dev/authors.html new file mode 100644 index 0000000000..58fa0ed9dd --- /dev/null +++ b/docs/dev/authors.html @@ -0,0 +1,151 @@ + +Authors and Citation • keras3 + Skip to contents + + +
+
+
+ +
+

Authors

+ +
  • +

    Tomasz Kalinowski. Author, copyright holder, maintainer. +

    +
  • +
  • +

    Daniel Falbel. Contributor, copyright holder. +

    +
  • +
  • +

    JJ Allaire. Author, copyright holder. +

    +
  • +
  • +

    François Chollet. Author, copyright holder. +

    +
  • +
  • +

    Posit Software, PBC. Copyright holder, funder. +

    +
  • +
  • +

    Google. Copyright holder, funder. +

    +
  • +
  • +

    Yuan Tang. Contributor, copyright holder. +

    +
  • +
  • +

    Wouter Van Der Bijl. Contributor, copyright holder. +

    +
  • +
  • +

    Martin Studer. Contributor, copyright holder. +

    +
  • +
  • +

    Sigrid Keydana. Contributor. +

    +
  • +
+ +
+

Citation

+

Source: DESCRIPTION

+ +

Kalinowski T, Allaire J, Chollet F (2024). +keras3: R Interface to 'Keras'. +R package version 1.0.0.9001, https://github.com/rstudio/keras3, https://keras3.posit.co/. +

+
@Manual{,
+  title = {keras3: R Interface to 'Keras'},
+  author = {Tomasz Kalinowski and JJ Allaire and François Chollet},
+  year = {2024},
+  note = {R package version 1.0.0.9001, https://github.com/rstudio/keras3},
+  url = {https://keras3.posit.co/},
+}
+
+
+ + +
+ + + +
+ + + + + + + diff --git a/docs/dev/deps/bootstrap-5.3.1/bootstrap.bundle.min.js b/docs/dev/deps/bootstrap-5.3.1/bootstrap.bundle.min.js new file mode 100644 index 0000000000..e8f21f703f --- /dev/null +++ b/docs/dev/deps/bootstrap-5.3.1/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.3.1 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function M(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function j(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${j(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${j(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=M(t.dataset[n])}return e},getDataAttribute:(t,e)=>M(t.getAttribute(`data-bs-${j(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.1"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return n(e)},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",Mt="collapsing",jt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(Mt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(Mt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(jt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Me(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const je={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Me(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:Me(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==P(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],M=f?-T[$]/2:0,j=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-M-q-z-O.mainAxis:j-q-z-O.mainAxis,K=v?-E[$]/2+M+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,Mn=`hide${xn}`,jn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,Mn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,jn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,jn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",Ms="Home",js="End",Fs="active",Hs="fade",Ws="show",Bs=":not(.dropdown-toggle)",zs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Rs=`.nav-link${Bs}, .list-group-item${Bs}, [role="tab"]${Bs}, ${zs}`,qs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Vs extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,Ms,js].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([Ms,js].includes(t.key))i=e[t.key===Ms?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Vs.getOrCreateInstance(i).show())}_getChildren(){return z.find(Rs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(".dropdown-toggle",Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(Rs)?t:z.findOne(Rs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Vs.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,zs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Vs.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(qs))Vs.getOrCreateInstance(t)})),m(Vs);const Ks=".bs.toast",Qs=`mouseover${Ks}`,Xs=`mouseout${Ks}`,Ys=`focusin${Ks}`,Us=`focusout${Ks}`,Gs=`hide${Ks}`,Js=`hidden${Ks}`,Zs=`show${Ks}`,to=`shown${Ks}`,eo="hide",io="show",no="showing",so={animation:"boolean",autohide:"boolean",delay:"number"},oo={animation:!0,autohide:!0,delay:5e3};class ro extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return oo}static get DefaultType(){return so}static get NAME(){return"toast"}show(){N.trigger(this._element,Zs).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(eo),d(this._element),this._element.classList.add(io,no),this._queueCallback((()=>{this._element.classList.remove(no),N.trigger(this._element,to),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Gs).defaultPrevented||(this._element.classList.add(no),this._queueCallback((()=>{this._element.classList.add(eo),this._element.classList.remove(no,io),N.trigger(this._element,Js)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(io),super.dispose()}isShown(){return this._element.classList.contains(io)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Qs,(t=>this._onInteraction(t,!0))),N.on(this._element,Xs,(t=>this._onInteraction(t,!1))),N.on(this._element,Ys,(t=>this._onInteraction(t,!0))),N.on(this._element,Us,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ro.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ro),m(ro),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Vs,Toast:ro,Tooltip:cs}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/docs/dev/deps/bootstrap-5.3.1/bootstrap.bundle.min.js.map b/docs/dev/deps/bootstrap-5.3.1/bootstrap.bundle.min.js.map new file mode 100644 index 0000000000..3863da8b7f --- /dev/null +++ b/docs/dev/deps/bootstrap-5.3.1/bootstrap.bundle.min.js.map @@ -0,0 +1 @@ +{"version":3,"names":["elementMap","Map","Data","set","element","key","instance","has","instanceMap","get","size","console","error","Array","from","keys","remove","delete","TRANSITION_END","parseSelector","selector","window","CSS","escape","replace","match","id","triggerTransitionEnd","dispatchEvent","Event","isElement","object","jquery","nodeType","getElement","length","document","querySelector","isVisible","getClientRects","elementIsVisible","getComputedStyle","getPropertyValue","closedDetails","closest","summary","parentNode","isDisabled","Node","ELEMENT_NODE","classList","contains","disabled","hasAttribute","getAttribute","findShadowRoot","documentElement","attachShadow","getRootNode","root","ShadowRoot","noop","reflow","offsetHeight","getjQuery","jQuery","body","DOMContentLoadedCallbacks","isRTL","dir","defineJQueryPlugin","plugin","callback","$","name","NAME","JQUERY_NO_CONFLICT","fn","jQueryInterface","Constructor","noConflict","readyState","addEventListener","push","execute","possibleCallback","args","defaultValue","executeAfterTransition","transitionElement","waitForTransition","emulatedDuration","transitionDuration","transitionDelay","floatTransitionDuration","Number","parseFloat","floatTransitionDelay","split","getTransitionDurationFromElement","called","handler","target","removeEventListener","setTimeout","getNextActiveElement","list","activeElement","shouldGetNext","isCycleAllowed","listLength","index","indexOf","Math","max","min","namespaceRegex","stripNameRegex","stripUidRegex","eventRegistry","uidEvent","customEvents","mouseenter","mouseleave","nativeEvents","Set","makeEventUid","uid","getElementEvents","findHandler","events","callable","delegationSelector","Object","values","find","event","normalizeParameters","originalTypeEvent","delegationFunction","isDelegated","typeEvent","getTypeEvent","addHandler","oneOff","wrapFunction","relatedTarget","delegateTarget","call","this","handlers","previousFunction","domElements","querySelectorAll","domElement","hydrateObj","EventHandler","off","type","apply","bootstrapDelegationHandler","bootstrapHandler","removeHandler","Boolean","removeNamespacedHandlers","namespace","storeElementEvent","handlerKey","entries","includes","on","one","inNamespace","isNamespace","startsWith","elementEvent","slice","keyHandlers","trigger","jQueryEvent","bubbles","nativeDispatch","defaultPrevented","isPropagationStopped","isImmediatePropagationStopped","isDefaultPrevented","evt","cancelable","preventDefault","obj","meta","value","_unused","defineProperty","configurable","normalizeData","toString","JSON","parse","decodeURIComponent","normalizeDataKey","chr","toLowerCase","Manipulator","setDataAttribute","setAttribute","removeDataAttribute","removeAttribute","getDataAttributes","attributes","bsKeys","dataset","filter","pureKey","charAt","getDataAttribute","Config","Default","DefaultType","Error","_getConfig","config","_mergeConfigObj","_configAfterMerge","_typeCheckConfig","jsonConfig","constructor","configTypes","property","expectedTypes","valueType","prototype","RegExp","test","TypeError","toUpperCase","BaseComponent","super","_element","_config","DATA_KEY","dispose","EVENT_KEY","propertyName","getOwnPropertyNames","_queueCallback","isAnimated","getInstance","getOrCreateInstance","VERSION","eventName","getSelector","hrefAttribute","trim","SelectorEngine","concat","Element","findOne","children","child","matches","parents","ancestor","prev","previous","previousElementSibling","next","nextElementSibling","focusableChildren","focusables","map","join","el","getSelectorFromElement","getElementFromSelector","getMultipleElementsFromSelector","enableDismissTrigger","component","method","clickEvent","tagName","EVENT_CLOSE","EVENT_CLOSED","Alert","close","_destroyElement","each","data","undefined","SELECTOR_DATA_TOGGLE","Button","toggle","button","EVENT_TOUCHSTART","EVENT_TOUCHMOVE","EVENT_TOUCHEND","EVENT_POINTERDOWN","EVENT_POINTERUP","endCallback","leftCallback","rightCallback","Swipe","isSupported","_deltaX","_supportPointerEvents","PointerEvent","_initEvents","_start","_eventIsPointerPenTouch","clientX","touches","_end","_handleSwipe","_move","absDeltaX","abs","direction","add","pointerType","navigator","maxTouchPoints","DATA_API_KEY","ORDER_NEXT","ORDER_PREV","DIRECTION_LEFT","DIRECTION_RIGHT","EVENT_SLIDE","EVENT_SLID","EVENT_KEYDOWN","EVENT_MOUSEENTER","EVENT_MOUSELEAVE","EVENT_DRAG_START","EVENT_LOAD_DATA_API","EVENT_CLICK_DATA_API","CLASS_NAME_CAROUSEL","CLASS_NAME_ACTIVE","SELECTOR_ACTIVE","SELECTOR_ITEM","SELECTOR_ACTIVE_ITEM","KEY_TO_DIRECTION","ArrowLeft","ArrowRight","interval","keyboard","pause","ride","touch","wrap","Carousel","_interval","_activeElement","_isSliding","touchTimeout","_swipeHelper","_indicatorsElement","_addEventListeners","cycle","_slide","nextWhenVisible","hidden","_clearInterval","_updateInterval","setInterval","_maybeEnableCycle","to","items","_getItems","activeIndex","_getItemIndex","_getActive","order","defaultInterval","_keydown","_addTouchEventListeners","img","swipeConfig","_directionToOrder","endCallBack","clearTimeout","_setActiveIndicatorElement","activeIndicator","newActiveIndicator","elementInterval","parseInt","isNext","nextElement","nextElementIndex","triggerEvent","_orderToDirection","isCycling","directionalClassName","orderClassName","completeCallBack","_isAnimated","clearInterval","carousel","slideIndex","carousels","EVENT_SHOW","EVENT_SHOWN","EVENT_HIDE","EVENT_HIDDEN","CLASS_NAME_SHOW","CLASS_NAME_COLLAPSE","CLASS_NAME_COLLAPSING","CLASS_NAME_DEEPER_CHILDREN","parent","Collapse","_isTransitioning","_triggerArray","toggleList","elem","filterElement","foundElement","_initializeChildren","_addAriaAndCollapsedClass","_isShown","hide","show","activeChildren","_getFirstLevelChildren","activeInstance","dimension","_getDimension","style","scrollSize","complete","getBoundingClientRect","selected","triggerArray","isOpen","top","bottom","right","left","auto","basePlacements","start","end","clippingParents","viewport","popper","reference","variationPlacements","reduce","acc","placement","placements","beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite","modifierPhases","getNodeName","nodeName","getWindow","node","ownerDocument","defaultView","isHTMLElement","HTMLElement","isShadowRoot","applyStyles$1","enabled","phase","_ref","state","elements","forEach","styles","assign","effect","_ref2","initialStyles","position","options","strategy","margin","arrow","hasOwnProperty","attribute","requires","getBasePlacement","round","getUAString","uaData","userAgentData","brands","isArray","item","brand","version","userAgent","isLayoutViewport","includeScale","isFixedStrategy","clientRect","scaleX","scaleY","offsetWidth","width","height","visualViewport","addVisualOffsets","x","offsetLeft","y","offsetTop","getLayoutRect","rootNode","isSameNode","host","isTableElement","getDocumentElement","getParentNode","assignedSlot","getTrueOffsetParent","offsetParent","getOffsetParent","isFirefox","currentNode","css","transform","perspective","contain","willChange","getContainingBlock","getMainAxisFromPlacement","within","mathMax","mathMin","mergePaddingObject","paddingObject","expandToHashMap","hashMap","arrow$1","_state$modifiersData$","arrowElement","popperOffsets","modifiersData","basePlacement","axis","len","padding","rects","toPaddingObject","arrowRect","minProp","maxProp","endDiff","startDiff","arrowOffsetParent","clientSize","clientHeight","clientWidth","centerToReference","center","offset","axisProp","centerOffset","_options$element","requiresIfExists","getVariation","unsetSides","mapToStyles","_Object$assign2","popperRect","variation","offsets","gpuAcceleration","adaptive","roundOffsets","isFixed","_offsets$x","_offsets$y","_ref3","hasX","hasY","sideX","sideY","win","heightProp","widthProp","_Object$assign","commonStyles","_ref4","dpr","devicePixelRatio","roundOffsetsByDPR","computeStyles$1","_ref5","_options$gpuAccelerat","_options$adaptive","_options$roundOffsets","passive","eventListeners","_options$scroll","scroll","_options$resize","resize","scrollParents","scrollParent","update","hash","getOppositePlacement","matched","getOppositeVariationPlacement","getWindowScroll","scrollLeft","pageXOffset","scrollTop","pageYOffset","getWindowScrollBarX","isScrollParent","_getComputedStyle","overflow","overflowX","overflowY","getScrollParent","listScrollParents","_element$ownerDocumen","isBody","updatedList","rectToClientRect","rect","getClientRectFromMixedType","clippingParent","html","layoutViewport","getViewportRect","clientTop","clientLeft","getInnerBoundingClientRect","winScroll","scrollWidth","scrollHeight","getDocumentRect","computeOffsets","commonX","commonY","mainAxis","detectOverflow","_options","_options$placement","_options$strategy","_options$boundary","boundary","_options$rootBoundary","rootBoundary","_options$elementConte","elementContext","_options$altBoundary","altBoundary","_options$padding","altContext","clippingClientRect","mainClippingParents","clipperElement","getClippingParents","firstClippingParent","clippingRect","accRect","getClippingRect","contextElement","referenceClientRect","popperClientRect","elementClientRect","overflowOffsets","offsetData","multiply","computeAutoPlacement","flipVariations","_options$allowedAutoP","allowedAutoPlacements","allPlacements","allowedPlacements","overflows","sort","a","b","flip$1","_skip","_options$mainAxis","checkMainAxis","_options$altAxis","altAxis","checkAltAxis","specifiedFallbackPlacements","fallbackPlacements","_options$flipVariatio","preferredPlacement","oppositePlacement","getExpandedFallbackPlacements","referenceRect","checksMap","makeFallbackChecks","firstFittingPlacement","i","_basePlacement","isStartVariation","isVertical","mainVariationSide","altVariationSide","checks","every","check","_loop","_i","fittingPlacement","reset","getSideOffsets","preventedOffsets","isAnySideFullyClipped","some","side","hide$1","preventOverflow","referenceOverflow","popperAltOverflow","referenceClippingOffsets","popperEscapeOffsets","isReferenceHidden","hasPopperEscaped","offset$1","_options$offset","invertDistance","skidding","distance","distanceAndSkiddingToXY","_data$state$placement","popperOffsets$1","preventOverflow$1","_options$tether","tether","_options$tetherOffset","tetherOffset","isBasePlacement","tetherOffsetValue","normalizedTetherOffsetValue","offsetModifierState","_offsetModifierState$","mainSide","altSide","additive","minLen","maxLen","arrowPaddingObject","arrowPaddingMin","arrowPaddingMax","arrowLen","minOffset","maxOffset","clientOffset","offsetModifierValue","tetherMax","preventedOffset","_offsetModifierState$2","_mainSide","_altSide","_offset","_len","_min","_max","isOriginSide","_offsetModifierValue","_tetherMin","_tetherMax","_preventedOffset","v","withinMaxClamp","getCompositeRect","elementOrVirtualElement","isOffsetParentAnElement","offsetParentIsScaled","isElementScaled","modifiers","visited","result","modifier","dep","depModifier","DEFAULT_OPTIONS","areValidElements","arguments","_key","popperGenerator","generatorOptions","_generatorOptions","_generatorOptions$def","defaultModifiers","_generatorOptions$def2","defaultOptions","pending","orderedModifiers","effectCleanupFns","isDestroyed","setOptions","setOptionsAction","cleanupModifierEffects","merged","orderModifiers","current","existing","m","_ref$options","cleanupFn","forceUpdate","_state$elements","_state$orderedModifie","_state$orderedModifie2","Promise","resolve","then","destroy","onFirstUpdate","createPopper","computeStyles","applyStyles","flip","ARROW_UP_KEY","ARROW_DOWN_KEY","EVENT_KEYDOWN_DATA_API","EVENT_KEYUP_DATA_API","SELECTOR_DATA_TOGGLE_SHOWN","SELECTOR_MENU","PLACEMENT_TOP","PLACEMENT_TOPEND","PLACEMENT_BOTTOM","PLACEMENT_BOTTOMEND","PLACEMENT_RIGHT","PLACEMENT_LEFT","autoClose","display","popperConfig","Dropdown","_popper","_parent","_menu","_inNavbar","_detectNavbar","_createPopper","focus","_completeHide","Popper","referenceElement","_getPopperConfig","_getPlacement","parentDropdown","isEnd","_getOffset","popperData","defaultBsPopperConfig","_selectMenuItem","clearMenus","openToggles","context","composedPath","isMenuTarget","dataApiKeydownHandler","isInput","isEscapeEvent","isUpOrDownEvent","getToggleButton","stopPropagation","EVENT_MOUSEDOWN","className","clickCallback","rootElement","Backdrop","_isAppended","_append","_getElement","_emulateAnimation","backdrop","createElement","append","EVENT_FOCUSIN","EVENT_KEYDOWN_TAB","TAB_NAV_BACKWARD","autofocus","trapElement","FocusTrap","_isActive","_lastTabNavDirection","activate","_handleFocusin","_handleKeydown","deactivate","shiftKey","SELECTOR_FIXED_CONTENT","SELECTOR_STICKY_CONTENT","PROPERTY_PADDING","PROPERTY_MARGIN","ScrollBarHelper","getWidth","documentWidth","innerWidth","_disableOverFlow","_setElementAttributes","calculatedValue","_resetElementAttributes","isOverflowing","_saveInitialAttribute","styleProperty","scrollbarWidth","_applyManipulationCallback","setProperty","actualValue","removeProperty","callBack","sel","EVENT_HIDE_PREVENTED","EVENT_RESIZE","EVENT_CLICK_DISMISS","EVENT_MOUSEDOWN_DISMISS","EVENT_KEYDOWN_DISMISS","CLASS_NAME_OPEN","CLASS_NAME_STATIC","Modal","_dialog","_backdrop","_initializeBackDrop","_focustrap","_initializeFocusTrap","_scrollBar","_adjustDialog","_showElement","_hideModal","handleUpdate","modalBody","transitionComplete","_triggerBackdropTransition","event2","_resetAdjustments","isModalOverflowing","initialOverflowY","isBodyOverflowing","paddingLeft","paddingRight","showEvent","alreadyOpen","CLASS_NAME_SHOWING","CLASS_NAME_HIDING","OPEN_SELECTOR","Offcanvas","blur","completeCallback","DefaultAllowlist","area","br","col","code","div","em","hr","h1","h2","h3","h4","h5","h6","li","ol","p","pre","s","small","span","sub","sup","strong","u","ul","uriAttributes","SAFE_URL_PATTERN","allowedAttribute","allowedAttributeList","attributeName","nodeValue","attributeRegex","regex","allowList","content","extraClass","sanitize","sanitizeFn","template","DefaultContentType","entry","TemplateFactory","getContent","_resolvePossibleFunction","hasContent","changeContent","_checkContent","toHtml","templateWrapper","innerHTML","_maybeSanitize","text","_setContent","arg","templateElement","_putElementInTemplate","textContent","unsafeHtml","sanitizeFunction","createdDocument","DOMParser","parseFromString","elementName","attributeList","allowedAttributes","sanitizeHtml","DISALLOWED_ATTRIBUTES","CLASS_NAME_FADE","SELECTOR_MODAL","EVENT_MODAL_HIDE","TRIGGER_HOVER","TRIGGER_FOCUS","AttachmentMap","AUTO","TOP","RIGHT","BOTTOM","LEFT","animation","container","customClass","delay","title","Tooltip","_isEnabled","_timeout","_isHovered","_activeTrigger","_templateFactory","_newContent","tip","_setListeners","_fixTitle","enable","disable","toggleEnabled","click","_leave","_enter","_hideModalHandler","_disposePopper","_isWithContent","isInTheDom","_getTipElement","_isWithActiveTrigger","_getTitle","_createTipElement","_getContentForTemplate","_getTemplateFactory","tipId","prefix","floor","random","getElementById","getUID","setContent","_initializeOnDelegatedTarget","_getDelegateConfig","attachment","triggers","eventIn","eventOut","_setTimeout","timeout","dataAttributes","dataAttribute","Popover","_getContent","EVENT_ACTIVATE","EVENT_CLICK","SELECTOR_TARGET_LINKS","SELECTOR_NAV_LINKS","SELECTOR_LINK_ITEMS","rootMargin","smoothScroll","threshold","ScrollSpy","_targetLinks","_observableSections","_rootElement","_activeTarget","_observer","_previousScrollData","visibleEntryTop","parentScrollTop","refresh","_initializeTargetsAndObservables","_maybeEnableSmoothScroll","disconnect","_getNewObserver","section","observe","observableSection","scrollTo","behavior","IntersectionObserver","_observerCallback","targetElement","_process","userScrollsDown","isIntersecting","_clearActiveClass","entryIsLowerThanPrevious","targetLinks","anchor","decodeURI","_activateParents","listGroup","activeNodes","spy","ARROW_LEFT_KEY","ARROW_RIGHT_KEY","HOME_KEY","END_KEY","NOT_SELECTOR_DROPDOWN_TOGGLE","SELECTOR_INNER_ELEM","SELECTOR_DATA_TOGGLE_ACTIVE","Tab","_setInitialAttributes","_getChildren","innerElem","_elemIsActive","active","_getActiveElem","hideEvent","_deactivate","_activate","relatedElem","_toggleDropDown","nextActiveElement","preventScroll","_setAttributeIfNotExists","_setInitialAttributesOnChild","_getInnerElement","isActive","outerElem","_getOuterElement","_setInitialAttributesOnTargetPanel","open","EVENT_MOUSEOVER","EVENT_MOUSEOUT","EVENT_FOCUSOUT","CLASS_NAME_HIDE","autohide","Toast","_hasMouseInteraction","_hasKeyboardInteraction","_clearTimeout","_maybeScheduleHide","isShown","_onInteraction","isInteracting"],"sources":["../../js/src/dom/data.js","../../js/src/util/index.js","../../js/src/dom/event-handler.js","../../js/src/dom/manipulator.js","../../js/src/util/config.js","../../js/src/base-component.js","../../js/src/dom/selector-engine.js","../../js/src/util/component-functions.js","../../js/src/alert.js","../../js/src/button.js","../../js/src/util/swipe.js","../../js/src/carousel.js","../../js/src/collapse.js","../../node_modules/@popperjs/core/lib/enums.js","../../node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindow.js","../../node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","../../node_modules/@popperjs/core/lib/modifiers/applyStyles.js","../../node_modules/@popperjs/core/lib/utils/getBasePlacement.js","../../node_modules/@popperjs/core/lib/utils/math.js","../../node_modules/@popperjs/core/lib/utils/userAgent.js","../../node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","../../node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","../../node_modules/@popperjs/core/lib/dom-utils/contains.js","../../node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","../../node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","../../node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","../../node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","../../node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","../../node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","../../node_modules/@popperjs/core/lib/utils/within.js","../../node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","../../node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","../../node_modules/@popperjs/core/lib/utils/expandToHashMap.js","../../node_modules/@popperjs/core/lib/modifiers/arrow.js","../../node_modules/@popperjs/core/lib/utils/getVariation.js","../../node_modules/@popperjs/core/lib/modifiers/computeStyles.js","../../node_modules/@popperjs/core/lib/modifiers/eventListeners.js","../../node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","../../node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","../../node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","../../node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","../../node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","../../node_modules/@popperjs/core/lib/utils/rectToClientRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","../../node_modules/@popperjs/core/lib/utils/computeOffsets.js","../../node_modules/@popperjs/core/lib/utils/detectOverflow.js","../../node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","../../node_modules/@popperjs/core/lib/modifiers/flip.js","../../node_modules/@popperjs/core/lib/modifiers/hide.js","../../node_modules/@popperjs/core/lib/modifiers/offset.js","../../node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","../../node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","../../node_modules/@popperjs/core/lib/utils/getAltAxis.js","../../node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","../../node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","../../node_modules/@popperjs/core/lib/utils/orderModifiers.js","../../node_modules/@popperjs/core/lib/createPopper.js","../../node_modules/@popperjs/core/lib/utils/debounce.js","../../node_modules/@popperjs/core/lib/utils/mergeByName.js","../../node_modules/@popperjs/core/lib/popper-lite.js","../../node_modules/@popperjs/core/lib/popper.js","../../js/src/dropdown.js","../../js/src/util/backdrop.js","../../js/src/util/focustrap.js","../../js/src/util/scrollbar.js","../../js/src/modal.js","../../js/src/offcanvas.js","../../js/src/util/sanitizer.js","../../js/src/util/template-factory.js","../../js/src/tooltip.js","../../js/src/popover.js","../../js/src/scrollspy.js","../../js/src/tab.js","../../js/src/toast.js","../../js/index.umd.js"],"sourcesContent":["/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map()\n\nexport default {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map())\n }\n\n const instanceMap = elementMap.get(element)\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`)\n return\n }\n\n instanceMap.set(key, instance)\n },\n\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null\n }\n\n return null\n },\n\n remove(element, key) {\n if (!elementMap.has(element)) {\n return\n }\n\n const instanceMap = elementMap.get(element)\n\n instanceMap.delete(key)\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element)\n }\n }\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1_000_000\nconst MILLISECONDS_MULTIPLIER = 1000\nconst TRANSITION_END = 'transitionend'\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`)\n }\n\n return selector\n}\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`\n }\n\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase()\n}\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID)\n } while (document.getElementById(prefix))\n\n return prefix\n}\n\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let { transitionDuration, transitionDelay } = window.getComputedStyle(element)\n\n const floatTransitionDuration = Number.parseFloat(transitionDuration)\n const floatTransitionDelay = Number.parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n}\n\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END))\n}\n\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false\n }\n\n if (typeof object.jquery !== 'undefined') {\n object = object[0]\n }\n\n return typeof object.nodeType !== 'undefined'\n}\n\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object\n }\n\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object))\n }\n\n return null\n}\n\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false\n }\n\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])')\n\n if (!closedDetails) {\n return elementIsVisible\n }\n\n if (closedDetails !== element) {\n const summary = element.closest('summary')\n if (summary && summary.parentNode !== closedDetails) {\n return false\n }\n\n if (summary === null) {\n return false\n }\n }\n\n return elementIsVisible\n}\n\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true\n }\n\n if (element.classList.contains('disabled')) {\n return true\n }\n\n if (typeof element.disabled !== 'undefined') {\n return element.disabled\n }\n\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'\n}\n\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return findShadowRoot(element.parentNode)\n}\n\nconst noop = () => {}\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight // eslint-disable-line no-unused-expressions\n}\n\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery\n }\n\n return null\n}\n\nconst DOMContentLoadedCallbacks = []\n\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback()\n }\n })\n }\n\n DOMContentLoadedCallbacks.push(callback)\n } else {\n callback()\n }\n}\n\nconst isRTL = () => document.documentElement.dir === 'rtl'\n\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery()\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME\n const JQUERY_NO_CONFLICT = $.fn[name]\n $.fn[name] = plugin.jQueryInterface\n $.fn[name].Constructor = plugin\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT\n return plugin.jQueryInterface\n }\n }\n })\n}\n\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue\n}\n\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback)\n return\n }\n\n const durationPadding = 5\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding\n\n let called = false\n\n const handler = ({ target }) => {\n if (target !== transitionElement) {\n return\n }\n\n called = true\n transitionElement.removeEventListener(TRANSITION_END, handler)\n execute(callback)\n }\n\n transitionElement.addEventListener(TRANSITION_END, handler)\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement)\n }\n }, emulatedDuration)\n}\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length\n let index = list.indexOf(activeElement)\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]\n }\n\n index += shouldGetNext ? 1 : -1\n\n if (isCycleAllowed) {\n index = (index + listLength) % listLength\n }\n\n return list[Math.max(0, Math.min(index, listLength - 1))]\n}\n\nexport {\n defineJQueryPlugin,\n execute,\n executeAfterTransition,\n findShadowRoot,\n getElement,\n getjQuery,\n getNextActiveElement,\n getTransitionDurationFromElement,\n getUID,\n isDisabled,\n isElement,\n isRTL,\n isVisible,\n noop,\n onDOMContentLoaded,\n parseSelector,\n reflow,\n triggerTransitionEnd,\n toType\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { getjQuery } from '../util/index.js'\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/\nconst stripNameRegex = /\\..*/\nconst stripUidRegex = /::\\d+$/\nconst eventRegistry = {} // Events storage\nlet uidEvent = 1\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n}\n\nconst nativeEvents = new Set([\n 'click',\n 'dblclick',\n 'mouseup',\n 'mousedown',\n 'contextmenu',\n 'mousewheel',\n 'DOMMouseScroll',\n 'mouseover',\n 'mouseout',\n 'mousemove',\n 'selectstart',\n 'selectend',\n 'keydown',\n 'keypress',\n 'keyup',\n 'orientationchange',\n 'touchstart',\n 'touchmove',\n 'touchend',\n 'touchcancel',\n 'pointerdown',\n 'pointermove',\n 'pointerup',\n 'pointerleave',\n 'pointercancel',\n 'gesturestart',\n 'gesturechange',\n 'gestureend',\n 'focus',\n 'blur',\n 'change',\n 'reset',\n 'select',\n 'submit',\n 'focusin',\n 'focusout',\n 'load',\n 'unload',\n 'beforeunload',\n 'resize',\n 'move',\n 'DOMContentLoaded',\n 'readystatechange',\n 'error',\n 'abort',\n 'scroll'\n])\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++\n}\n\nfunction getElementEvents(element) {\n const uid = makeEventUid(element)\n\n element.uidEvent = uid\n eventRegistry[uid] = eventRegistry[uid] || {}\n\n return eventRegistry[uid]\n}\n\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, { delegateTarget: element })\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn)\n }\n\n return fn.apply(element, [event])\n }\n}\n\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector)\n\n for (let { target } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue\n }\n\n hydrateObj(event, { delegateTarget: target })\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn)\n }\n\n return fn.apply(target, [event])\n }\n }\n }\n}\n\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events)\n .find(event => event.callable === callable && event.delegationSelector === delegationSelector)\n}\n\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string'\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : (handler || delegationFunction)\n let typeEvent = getTypeEvent(originalTypeEvent)\n\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent\n }\n\n return [isDelegated, callable, typeEvent]\n}\n\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return\n }\n\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) {\n return fn.call(this, event)\n }\n }\n }\n\n callable = wrapFunction(callable)\n }\n\n const events = getElementEvents(element)\n const handlers = events[typeEvent] || (events[typeEvent] = {})\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null)\n\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff\n\n return\n }\n\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''))\n const fn = isDelegated ?\n bootstrapDelegationHandler(element, handler, callable) :\n bootstrapHandler(element, callable)\n\n fn.delegationSelector = isDelegated ? handler : null\n fn.callable = callable\n fn.oneOff = oneOff\n fn.uidEvent = uid\n handlers[uid] = fn\n\n element.addEventListener(typeEvent, fn, isDelegated)\n}\n\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector)\n\n if (!fn) {\n return\n }\n\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))\n delete events[typeEvent][fn.uidEvent]\n}\n\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {}\n\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)\n }\n }\n}\n\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '')\n return customEvents[event] || event\n}\n\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false)\n },\n\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true)\n },\n\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return\n }\n\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)\n const inNamespace = typeEvent !== originalTypeEvent\n const events = getElementEvents(element)\n const storeElementEvent = events[typeEvent] || {}\n const isNamespace = originalTypeEvent.startsWith('.')\n\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return\n }\n\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null)\n return\n }\n\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))\n }\n }\n\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '')\n\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)\n }\n }\n },\n\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null\n }\n\n const $ = getjQuery()\n const typeEvent = getTypeEvent(event)\n const inNamespace = event !== typeEvent\n\n let jQueryEvent = null\n let bubbles = true\n let nativeDispatch = true\n let defaultPrevented = false\n\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args)\n\n $(element).trigger(jQueryEvent)\n bubbles = !jQueryEvent.isPropagationStopped()\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()\n defaultPrevented = jQueryEvent.isDefaultPrevented()\n }\n\n const evt = hydrateObj(new Event(event, { bubbles, cancelable: true }), args)\n\n if (defaultPrevented) {\n evt.preventDefault()\n }\n\n if (nativeDispatch) {\n element.dispatchEvent(evt)\n }\n\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault()\n }\n\n return evt\n }\n}\n\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value\n } catch {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value\n }\n })\n }\n }\n\n return obj\n}\n\nexport default EventHandler\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true\n }\n\n if (value === 'false') {\n return false\n }\n\n if (value === Number(value).toString()) {\n return Number(value)\n }\n\n if (value === '' || value === 'null') {\n return null\n }\n\n if (typeof value !== 'string') {\n return value\n }\n\n try {\n return JSON.parse(decodeURIComponent(value))\n } catch {\n return value\n }\n}\n\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`)\n}\n\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value)\n },\n\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`)\n },\n\n getDataAttributes(element) {\n if (!element) {\n return {}\n }\n\n const attributes = {}\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'))\n\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '')\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length)\n attributes[pureKey] = normalizeData(element.dataset[key])\n }\n\n return attributes\n },\n\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`))\n }\n}\n\nexport default Manipulator\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Manipulator from '../dom/manipulator.js'\nimport { isElement, toType } from './index.js'\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {}\n }\n\n static get DefaultType() {\n return {}\n }\n\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!')\n }\n\n _getConfig(config) {\n config = this._mergeConfigObj(config)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n _configAfterMerge(config) {\n return config\n }\n\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {} // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n }\n }\n\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property]\n const valueType = isElement(value) ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(\n `${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`\n )\n }\n }\n }\n}\n\nexport default Config\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Data from './dom/data.js'\nimport EventHandler from './dom/event-handler.js'\nimport Config from './util/config.js'\nimport { executeAfterTransition, getElement } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.1'\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super()\n\n element = getElement(element)\n if (!element) {\n return\n }\n\n this._element = element\n this._config = this._getConfig(config)\n\n Data.set(this._element, this.constructor.DATA_KEY, this)\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY)\n EventHandler.off(this._element, this.constructor.EVENT_KEY)\n\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null\n }\n }\n\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated)\n }\n\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY)\n }\n\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null)\n }\n\n static get VERSION() {\n return VERSION\n }\n\n static get DATA_KEY() {\n return `bs.${this.NAME}`\n }\n\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`\n }\n\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`\n }\n}\n\nexport default BaseComponent\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { isDisabled, isVisible, parseSelector } from '../util/index.js'\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target')\n\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href')\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) {\n return null\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`\n }\n\n selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null\n }\n\n return parseSelector(selector)\n}\n\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector))\n },\n\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector)\n },\n\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector))\n },\n\n parents(element, selector) {\n const parents = []\n let ancestor = element.parentNode.closest(selector)\n\n while (ancestor) {\n parents.push(ancestor)\n ancestor = ancestor.parentNode.closest(selector)\n }\n\n return parents\n },\n\n prev(element, selector) {\n let previous = element.previousElementSibling\n\n while (previous) {\n if (previous.matches(selector)) {\n return [previous]\n }\n\n previous = previous.previousElementSibling\n }\n\n return []\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling\n\n while (next) {\n if (next.matches(selector)) {\n return [next]\n }\n\n next = next.nextElementSibling\n }\n\n return []\n },\n\n focusableChildren(element) {\n const focusables = [\n 'a',\n 'button',\n 'input',\n 'textarea',\n 'select',\n 'details',\n '[tabindex]',\n '[contenteditable=\"true\"]'\n ].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',')\n\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el))\n },\n\n getSelectorFromElement(element) {\n const selector = getSelector(element)\n\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null\n }\n\n return null\n },\n\n getElementFromSelector(element) {\n const selector = getSelector(element)\n\n return selector ? SelectorEngine.findOne(selector) : null\n },\n\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element)\n\n return selector ? SelectorEngine.find(selector) : []\n }\n}\n\nexport default SelectorEngine\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport { isDisabled } from './index.js'\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`\n const name = component.NAME\n\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n if (isDisabled(this)) {\n return\n }\n\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`)\n const instance = component.getOrCreateInstance(target)\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]()\n })\n}\n\nexport {\n enableDismissTrigger\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'alert'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\n\nconst EVENT_CLOSE = `close${EVENT_KEY}`\nconst EVENT_CLOSED = `closed${EVENT_KEY}`\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE)\n\n if (closeEvent.defaultPrevented) {\n return\n }\n\n this._element.classList.remove(CLASS_NAME_SHOW)\n\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE)\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated)\n }\n\n // Private\n _destroyElement() {\n this._element.remove()\n EventHandler.trigger(this._element, EVENT_CLOSED)\n this.dispose()\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](this)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close')\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert)\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'button'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst CLASS_NAME_ACTIVE = 'active'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"button\"]'\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE))\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this)\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {\n event.preventDefault()\n\n const button = event.target.closest(SELECTOR_DATA_TOGGLE)\n const data = Button.getOrCreateInstance(button)\n\n data.toggle()\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button)\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport Config from './config.js'\nimport { execute } from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'swipe'\nconst EVENT_KEY = '.bs.swipe'\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY}`\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY}`\nconst POINTER_TYPE_TOUCH = 'touch'\nconst POINTER_TYPE_PEN = 'pen'\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event'\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n}\n\nconst DefaultType = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n}\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super()\n this._element = element\n\n if (!element || !Swipe.isSupported()) {\n return\n }\n\n this._config = this._getConfig(config)\n this._deltaX = 0\n this._supportPointerEvents = Boolean(window.PointerEvent)\n this._initEvents()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY)\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX\n\n return\n }\n\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX\n }\n }\n\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX\n }\n\n this._handleSwipe()\n execute(this._config.endCallback)\n }\n\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ?\n 0 :\n event.touches[0].clientX - this._deltaX\n }\n\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX)\n\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltaX / this._deltaX\n\n this._deltaX = 0\n\n if (!direction) {\n return\n }\n\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback)\n }\n\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event))\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event))\n\n this._element.classList.add(CLASS_NAME_POINTER_EVENT)\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event))\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event))\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event))\n }\n }\n\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n }\n}\n\nexport default Swipe\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n getNextActiveElement,\n isRTL,\n isVisible,\n reflow,\n triggerTransitionEnd\n} from './util/index.js'\nimport Swipe from './util/swipe.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'carousel'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst ARROW_LEFT_KEY = 'ArrowLeft'\nconst ARROW_RIGHT_KEY = 'ArrowRight'\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next'\nconst ORDER_PREV = 'prev'\nconst DIRECTION_LEFT = 'left'\nconst DIRECTION_RIGHT = 'right'\n\nconst EVENT_SLIDE = `slide${EVENT_KEY}`\nconst EVENT_SLID = `slid${EVENT_KEY}`\nconst EVENT_KEYDOWN = `keydown${EVENT_KEY}`\nconst EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`\nconst EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_CAROUSEL = 'carousel'\nconst CLASS_NAME_ACTIVE = 'active'\nconst CLASS_NAME_SLIDE = 'slide'\nconst CLASS_NAME_END = 'carousel-item-end'\nconst CLASS_NAME_START = 'carousel-item-start'\nconst CLASS_NAME_NEXT = 'carousel-item-next'\nconst CLASS_NAME_PREV = 'carousel-item-prev'\n\nconst SELECTOR_ACTIVE = '.active'\nconst SELECTOR_ITEM = '.carousel-item'\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM\nconst SELECTOR_ITEM_IMG = '.carousel-item img'\nconst SELECTOR_INDICATORS = '.carousel-indicators'\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]'\n\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY]: DIRECTION_LEFT\n}\n\nconst Default = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n}\n\nconst DefaultType = {\n interval: '(number|boolean)', // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._interval = null\n this._activeElement = null\n this._isSliding = false\n this.touchTimeout = null\n this._swipeHelper = null\n\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)\n this._addEventListeners()\n\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT)\n }\n\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next()\n }\n }\n\n prev() {\n this._slide(ORDER_PREV)\n }\n\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element)\n }\n\n this._clearInterval()\n }\n\n cycle() {\n this._clearInterval()\n this._updateInterval()\n\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval)\n }\n\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle())\n return\n }\n\n this.cycle()\n }\n\n to(index) {\n const items = this._getItems()\n if (index > items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index))\n return\n }\n\n const activeIndex = this._getItemIndex(this._getActive())\n if (activeIndex === index) {\n return\n }\n\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV\n\n this._slide(order, items[index])\n }\n\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose()\n }\n\n super.dispose()\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval\n return config\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause())\n EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle())\n }\n\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners()\n }\n }\n\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault())\n }\n\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n }\n\n this._swipeHelper = new Swipe(this._element, swipeConfig)\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n const direction = KEY_TO_DIRECTION[event.key]\n if (direction) {\n event.preventDefault()\n this._slide(this._directionToOrder(direction))\n }\n }\n\n _getItemIndex(element) {\n return this._getItems().indexOf(element)\n }\n\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return\n }\n\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)\n\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE)\n activeIndicator.removeAttribute('aria-current')\n\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement)\n\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE)\n newActiveIndicator.setAttribute('aria-current', 'true')\n }\n }\n\n _updateInterval() {\n const element = this._activeElement || this._getActive()\n\n if (!element) {\n return\n }\n\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10)\n\n this._config.interval = elementInterval || this._config.defaultInterval\n }\n\n _slide(order, element = null) {\n if (this._isSliding) {\n return\n }\n\n const activeElement = this._getActive()\n const isNext = order === ORDER_NEXT\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap)\n\n if (nextElement === activeElement) {\n return\n }\n\n const nextElementIndex = this._getItemIndex(nextElement)\n\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n })\n }\n\n const slideEvent = triggerEvent(EVENT_SLIDE)\n\n if (slideEvent.defaultPrevented) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return\n }\n\n const isCycling = Boolean(this._interval)\n this.pause()\n\n this._isSliding = true\n\n this._setActiveIndicatorElement(nextElementIndex)\n this._activeElement = nextElement\n\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV\n\n nextElement.classList.add(orderClassName)\n\n reflow(nextElement)\n\n activeElement.classList.add(directionalClassName)\n nextElement.classList.add(directionalClassName)\n\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName)\n nextElement.classList.add(CLASS_NAME_ACTIVE)\n\n activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)\n\n this._isSliding = false\n\n triggerEvent(EVENT_SLID)\n }\n\n this._queueCallback(completeCallBack, activeElement, this._isAnimated())\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE)\n }\n\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)\n }\n\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element)\n }\n\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n }\n\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT\n }\n\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV\n }\n\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT\n }\n\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config)\n\n if (typeof config === 'number') {\n data.to(config)\n return\n }\n\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return\n }\n\n event.preventDefault()\n\n const carousel = Carousel.getOrCreateInstance(target)\n const slideIndex = this.getAttribute('data-bs-slide-to')\n\n if (slideIndex) {\n carousel.to(slideIndex)\n carousel._maybeEnableCycle()\n return\n }\n\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next()\n carousel._maybeEnableCycle()\n return\n }\n\n carousel.prev()\n carousel._maybeEnableCycle()\n})\n\nEventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)\n\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel)\n }\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel)\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n getElement,\n reflow\n} from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'collapse'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_COLLAPSE = 'collapse'\nconst CLASS_NAME_COLLAPSING = 'collapsing'\nconst CLASS_NAME_COLLAPSED = 'collapsed'\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal'\n\nconst WIDTH = 'width'\nconst HEIGHT = 'height'\n\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"collapse\"]'\n\nconst Default = {\n parent: null,\n toggle: true\n}\n\nconst DefaultType = {\n parent: '(null|element)',\n toggle: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._isTransitioning = false\n this._triggerArray = []\n\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)\n\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem)\n const filterElement = SelectorEngine.find(selector)\n .filter(foundElement => foundElement === this._element)\n\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem)\n }\n }\n\n this._initializeChildren()\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown())\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning || this._isShown()) {\n return\n }\n\n let activeChildren = []\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES)\n .filter(element => element !== this._element)\n .map(element => Collapse.getOrCreateInstance(element, { toggle: false }))\n }\n\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW)\n if (startEvent.defaultPrevented) {\n return\n }\n\n for (const activeInstance of activeChildren) {\n activeInstance.hide()\n }\n\n const dimension = this._getDimension()\n\n this._element.classList.remove(CLASS_NAME_COLLAPSE)\n this._element.classList.add(CLASS_NAME_COLLAPSING)\n\n this._element.style[dimension] = 0\n\n this._addAriaAndCollapsedClass(this._triggerArray, true)\n this._isTransitioning = true\n\n const complete = () => {\n this._isTransitioning = false\n\n this._element.classList.remove(CLASS_NAME_COLLAPSING)\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)\n\n this._element.style[dimension] = ''\n\n EventHandler.trigger(this._element, EVENT_SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n\n this._queueCallback(complete, this._element, true)\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n if (startEvent.defaultPrevented) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n reflow(this._element)\n\n this._element.classList.add(CLASS_NAME_COLLAPSING)\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)\n\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger)\n\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false)\n }\n }\n\n this._isTransitioning = true\n\n const complete = () => {\n this._isTransitioning = false\n this._element.classList.remove(CLASS_NAME_COLLAPSING)\n this._element.classList.add(CLASS_NAME_COLLAPSE)\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n }\n\n this._element.style[dimension] = ''\n\n this._queueCallback(complete, this._element, true)\n }\n\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW)\n }\n\n // Private\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle) // Coerce string values\n config.parent = getElement(config.parent)\n return config\n }\n\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT\n }\n\n _initializeChildren() {\n if (!this._config.parent) {\n return\n }\n\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE)\n\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element)\n\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected))\n }\n }\n }\n\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent)\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element))\n }\n\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return\n }\n\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen)\n element.setAttribute('aria-expanded', isOpen)\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {}\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config)\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) {\n event.preventDefault()\n }\n\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, { toggle: false }).toggle()\n }\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse)\n\nexport default Collapse\n","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport * as Popper from '@popperjs/core'\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n execute,\n getElement,\n getNextActiveElement,\n isDisabled,\n isElement,\n isRTL,\n isVisible,\n noop\n} from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'dropdown'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst ESCAPE_KEY = 'Escape'\nconst TAB_KEY = 'Tab'\nconst ARROW_UP_KEY = 'ArrowUp'\nconst ARROW_DOWN_KEY = 'ArrowDown'\nconst RIGHT_MOUSE_BUTTON = 2 // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_DROPUP = 'dropup'\nconst CLASS_NAME_DROPEND = 'dropend'\nconst CLASS_NAME_DROPSTART = 'dropstart'\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center'\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'\n\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)'\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`\nconst SELECTOR_MENU = '.dropdown-menu'\nconst SELECTOR_NAVBAR = '.navbar'\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav'\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'\nconst PLACEMENT_TOPCENTER = 'top'\nconst PLACEMENT_BOTTOMCENTER = 'bottom'\n\nconst Default = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n}\n\nconst DefaultType = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n}\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._popper = null\n this._parent = this._element.parentNode // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] ||\n SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||\n SelectorEngine.findOne(SELECTOR_MENU, this._parent)\n this._inNavbar = this._detectNavbar()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show()\n }\n\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._createPopper()\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop)\n }\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n this._menu.classList.add(CLASS_NAME_SHOW)\n this._element.classList.add(CLASS_NAME_SHOW)\n EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget)\n }\n\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n\n this._completeHide(relatedTarget)\n }\n\n dispose() {\n if (this._popper) {\n this._popper.destroy()\n }\n\n super.dispose()\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper) {\n this._popper.update()\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)\n if (hideEvent.defaultPrevented) {\n return\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop)\n }\n }\n\n if (this._popper) {\n this._popper.destroy()\n }\n\n this._menu.classList.remove(CLASS_NAME_SHOW)\n this._element.classList.remove(CLASS_NAME_SHOW)\n this._element.setAttribute('aria-expanded', 'false')\n Manipulator.removeDataAttribute(this._menu, 'popper')\n EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)\n }\n\n _getConfig(config) {\n config = super._getConfig(config)\n\n if (typeof config.reference === 'object' && !isElement(config.reference) &&\n typeof config.reference.getBoundingClientRect !== 'function'\n ) {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`)\n }\n\n return config\n }\n\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)')\n }\n\n let referenceElement = this._element\n\n if (this._config.reference === 'parent') {\n referenceElement = this._parent\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference)\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference\n }\n\n const popperConfig = this._getPopperConfig()\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)\n }\n\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW)\n }\n\n _getPlacement() {\n const parentDropdown = this._parent\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP\n }\n\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM\n }\n\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null\n }\n\n _getOffset() {\n const { offset } = this._config\n\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10))\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element)\n }\n\n return offset\n }\n\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n },\n {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n }\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static') // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }]\n }\n\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n }\n }\n\n _selectMenuItem({ key, target }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))\n\n if (!items.length) {\n return\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {\n return\n }\n\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN)\n\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle)\n if (!context || context._config.autoClose === false) {\n continue\n }\n\n const composedPath = event.composedPath()\n const isMenuTarget = composedPath.includes(context._menu)\n if (\n composedPath.includes(context._element) ||\n (context._config.autoClose === 'inside' && !isMenuTarget) ||\n (context._config.autoClose === 'outside' && isMenuTarget)\n ) {\n continue\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && ((event.type === 'keyup' && event.key === TAB_KEY) || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue\n }\n\n const relatedTarget = { relatedTarget: context._element }\n\n if (event.type === 'click') {\n relatedTarget.clickEvent = event\n }\n\n context._completeHide(relatedTarget)\n }\n }\n\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName)\n const isEscapeEvent = event.key === ESCAPE_KEY\n const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)\n\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return\n }\n\n if (isInput && !isEscapeEvent) {\n return\n }\n\n event.preventDefault()\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ?\n this :\n (SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] ||\n SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] ||\n SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, event.delegateTarget.parentNode))\n\n const instance = Dropdown.getOrCreateInstance(getToggleButton)\n\n if (isUpOrDownEvent) {\n event.stopPropagation()\n instance.show()\n instance._selectMenuItem(event)\n return\n }\n\n if (instance._isShown()) { // else is escape and we check if it is shown\n event.stopPropagation()\n instance.hide()\n getToggleButton.focus()\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)\nEventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n event.preventDefault()\n Dropdown.getOrCreateInstance(this).toggle()\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown)\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport Config from './config.js'\nimport { execute, executeAfterTransition, getElement, reflow } from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'backdrop'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`\n\nconst Default = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true, // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n}\n\nconst DefaultType = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n}\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n this._isAppended = false\n this._element = null\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback)\n return\n }\n\n this._append()\n\n const element = this._getElement()\n if (this._config.isAnimated) {\n reflow(element)\n }\n\n element.classList.add(CLASS_NAME_SHOW)\n\n this._emulateAnimation(() => {\n execute(callback)\n })\n }\n\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback)\n return\n }\n\n this._getElement().classList.remove(CLASS_NAME_SHOW)\n\n this._emulateAnimation(() => {\n this.dispose()\n execute(callback)\n })\n }\n\n dispose() {\n if (!this._isAppended) {\n return\n }\n\n EventHandler.off(this._element, EVENT_MOUSEDOWN)\n\n this._element.remove()\n this._isAppended = false\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div')\n backdrop.className = this._config.className\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE)\n }\n\n this._element = backdrop\n }\n\n return this._element\n }\n\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement)\n return config\n }\n\n _append() {\n if (this._isAppended) {\n return\n }\n\n const element = this._getElement()\n this._config.rootElement.append(element)\n\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback)\n })\n\n this._isAppended = true\n }\n\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated)\n }\n}\n\nexport default Backdrop\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport Config from './config.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'focustrap'\nconst DATA_KEY = 'bs.focustrap'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst EVENT_FOCUSIN = `focusin${EVENT_KEY}`\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`\n\nconst TAB_KEY = 'Tab'\nconst TAB_NAV_FORWARD = 'forward'\nconst TAB_NAV_BACKWARD = 'backward'\n\nconst Default = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n}\n\nconst DefaultType = {\n autofocus: 'boolean',\n trapElement: 'element'\n}\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n this._isActive = false\n this._lastTabNavDirection = null\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return\n }\n\n if (this._config.autofocus) {\n this._config.trapElement.focus()\n }\n\n EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event))\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event))\n\n this._isActive = true\n }\n\n deactivate() {\n if (!this._isActive) {\n return\n }\n\n this._isActive = false\n EventHandler.off(document, EVENT_KEY)\n }\n\n // Private\n _handleFocusin(event) {\n const { trapElement } = this._config\n\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return\n }\n\n const elements = SelectorEngine.focusableChildren(trapElement)\n\n if (elements.length === 0) {\n trapElement.focus()\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus()\n } else {\n elements[0].focus()\n }\n }\n\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return\n }\n\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD\n }\n}\n\nexport default FocusTrap\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Manipulator from '../dom/manipulator.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport { isElement } from './index.js'\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'\nconst SELECTOR_STICKY_CONTENT = '.sticky-top'\nconst PROPERTY_PADDING = 'padding-right'\nconst PROPERTY_MARGIN = 'margin-right'\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth\n return Math.abs(window.innerWidth - documentWidth)\n }\n\n hide() {\n const width = this.getWidth()\n this._disableOverFlow()\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width)\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width)\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width)\n }\n\n reset() {\n this._resetElementAttributes(this._element, 'overflow')\n this._resetElementAttributes(this._element, PROPERTY_PADDING)\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING)\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN)\n }\n\n isOverflowing() {\n return this.getWidth() > 0\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow')\n this._element.style.overflow = 'hidden'\n }\n\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth()\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return\n }\n\n this._saveInitialAttribute(element, styleProperty)\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty)\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`)\n }\n\n this._applyManipulationCallback(selector, manipulationCallBack)\n }\n\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty)\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue)\n }\n }\n\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty)\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty)\n return\n }\n\n Manipulator.removeDataAttribute(element, styleProperty)\n element.style.setProperty(styleProperty, value)\n }\n\n this._applyManipulationCallback(selector, manipulationCallBack)\n }\n\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector)\n return\n }\n\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel)\n }\n }\n}\n\nexport default ScrollBarHelper\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport Backdrop from './util/backdrop.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport FocusTrap from './util/focustrap.js'\nimport { defineJQueryPlugin, isRTL, isVisible, reflow } from './util/index.js'\nimport ScrollBarHelper from './util/scrollbar.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'modal'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst ESCAPE_KEY = 'Escape'\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_OPEN = 'modal-open'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_STATIC = 'modal-static'\n\nconst OPEN_SELECTOR = '.modal.show'\nconst SELECTOR_DIALOG = '.modal-dialog'\nconst SELECTOR_MODAL_BODY = '.modal-body'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"modal\"]'\n\nconst Default = {\n backdrop: true,\n focus: true,\n keyboard: true\n}\n\nconst DefaultType = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)\n this._backdrop = this._initializeBackDrop()\n this._focustrap = this._initializeFocusTrap()\n this._isShown = false\n this._isTransitioning = false\n this._scrollBar = new ScrollBarHelper()\n\n this._addEventListeners()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {\n relatedTarget\n })\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._isShown = true\n this._isTransitioning = true\n\n this._scrollBar.hide()\n\n document.body.classList.add(CLASS_NAME_OPEN)\n\n this._adjustDialog()\n\n this._backdrop.show(() => this._showElement(relatedTarget))\n }\n\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n this._isShown = false\n this._isTransitioning = true\n this._focustrap.deactivate()\n\n this._element.classList.remove(CLASS_NAME_SHOW)\n\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated())\n }\n\n dispose() {\n EventHandler.off(window, EVENT_KEY)\n EventHandler.off(this._dialog, EVENT_KEY)\n\n this._backdrop.dispose()\n this._focustrap.deactivate()\n\n super.dispose()\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n })\n }\n\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n })\n }\n\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n this._element.setAttribute('role', 'dialog')\n this._element.scrollTop = 0\n\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)\n if (modalBody) {\n modalBody.scrollTop = 0\n }\n\n reflow(this._element)\n\n this._element.classList.add(CLASS_NAME_SHOW)\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate()\n }\n\n this._isTransitioning = false\n EventHandler.trigger(this._element, EVENT_SHOWN, {\n relatedTarget\n })\n }\n\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated())\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return\n }\n\n if (this._config.keyboard) {\n this.hide()\n return\n }\n\n this._triggerBackdropTransition()\n })\n\n EventHandler.on(window, EVENT_RESIZE, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog()\n }\n })\n\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return\n }\n\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition()\n return\n }\n\n if (this._config.backdrop) {\n this.hide()\n }\n })\n })\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._element.removeAttribute('role')\n this._isTransitioning = false\n\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN)\n this._resetAdjustments()\n this._scrollBar.reset()\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n })\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE)\n }\n\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n if (hideEvent.defaultPrevented) {\n return\n }\n\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n const initialOverflowY = this._element.style.overflowY\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return\n }\n\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden'\n }\n\n this._element.classList.add(CLASS_NAME_STATIC)\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC)\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY\n }, this._dialog)\n }, this._dialog)\n\n this._element.focus()\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n const scrollbarWidth = this._scrollBar.getWidth()\n const isBodyOverflowing = scrollbarWidth > 0\n\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight'\n this._element.style[property] = `${scrollbarWidth}px`\n }\n\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft'\n this._element.style[property] = `${scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](relatedTarget)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n EventHandler.one(target, EVENT_SHOW, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return\n }\n\n EventHandler.one(target, EVENT_HIDDEN, () => {\n if (isVisible(this)) {\n this.focus()\n }\n })\n })\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide()\n }\n\n const data = Modal.getOrCreateInstance(target)\n\n data.toggle(this)\n})\n\nenableDismissTrigger(Modal)\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal)\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport Backdrop from './util/backdrop.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport FocusTrap from './util/focustrap.js'\nimport {\n defineJQueryPlugin,\n isDisabled,\n isVisible\n} from './util/index.js'\nimport ScrollBarHelper from './util/scrollbar.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'offcanvas'\nconst DATA_KEY = 'bs.offcanvas'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst ESCAPE_KEY = 'Escape'\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_SHOWING = 'showing'\nconst CLASS_NAME_HIDING = 'hiding'\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop'\nconst OPEN_SELECTOR = '.offcanvas.show'\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\n\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"offcanvas\"]'\n\nconst Default = {\n backdrop: true,\n keyboard: true,\n scroll: false\n}\n\nconst DefaultType = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._isShown = false\n this._backdrop = this._initializeBackDrop()\n this._focustrap = this._initializeFocusTrap()\n this._addEventListeners()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._isShown = true\n this._backdrop.show()\n\n if (!this._config.scroll) {\n new ScrollBarHelper().hide()\n }\n\n this._element.setAttribute('aria-modal', true)\n this._element.setAttribute('role', 'dialog')\n this._element.classList.add(CLASS_NAME_SHOWING)\n\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate()\n }\n\n this._element.classList.add(CLASS_NAME_SHOW)\n this._element.classList.remove(CLASS_NAME_SHOWING)\n EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })\n }\n\n this._queueCallback(completeCallBack, this._element, true)\n }\n\n hide() {\n if (!this._isShown) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n this._focustrap.deactivate()\n this._element.blur()\n this._isShown = false\n this._element.classList.add(CLASS_NAME_HIDING)\n this._backdrop.hide()\n\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING)\n this._element.removeAttribute('aria-modal')\n this._element.removeAttribute('role')\n\n if (!this._config.scroll) {\n new ScrollBarHelper().reset()\n }\n\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n }\n\n this._queueCallback(completeCallback, this._element, true)\n }\n\n dispose() {\n this._backdrop.dispose()\n this._focustrap.deactivate()\n super.dispose()\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n return\n }\n\n this.hide()\n }\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop)\n\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n })\n }\n\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n })\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return\n }\n\n if (this._config.keyboard) {\n this.hide()\n return\n }\n\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n })\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](this)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n if (isDisabled(this)) {\n return\n }\n\n EventHandler.one(target, EVENT_HIDDEN, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus()\n }\n })\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide()\n }\n\n const data = Offcanvas.getOrCreateInstance(target)\n data.toggle(this)\n})\n\nEventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show()\n }\n})\n\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide()\n }\n }\n})\n\nenableDismissTrigger(Offcanvas)\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas)\n\nexport default Offcanvas\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i\n\nexport const DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n div: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n}\n// js-docs-end allow-list\n\nconst uriAttributes = new Set([\n 'background',\n 'cite',\n 'href',\n 'itemtype',\n 'longdesc',\n 'poster',\n 'src',\n 'xlink:href'\n])\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\n// eslint-disable-next-line unicorn/better-regex\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i\n\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase()\n\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue))\n }\n\n return true\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)\n .some(regex => regex.test(attributeName))\n}\n\nexport function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml\n }\n\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml)\n }\n\n const domParser = new window.DOMParser()\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'))\n\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase()\n\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove()\n continue\n }\n\n const attributeList = [].concat(...element.attributes)\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || [])\n\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName)\n }\n }\n }\n\n return createdDocument.body.innerHTML\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport SelectorEngine from '../dom/selector-engine.js'\nimport Config from './config.js'\nimport { DefaultAllowlist, sanitizeHtml } from './sanitizer.js'\nimport { execute, getElement, isElement } from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'TemplateFactory'\n\nconst Default = {\n allowList: DefaultAllowlist,\n content: {}, // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n}\n\nconst DefaultType = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n}\n\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n}\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content)\n .map(config => this._resolvePossibleFunction(config))\n .filter(Boolean)\n }\n\n hasContent() {\n return this.getContent().length > 0\n }\n\n changeContent(content) {\n this._checkContent(content)\n this._config.content = { ...this._config.content, ...content }\n return this\n }\n\n toHtml() {\n const templateWrapper = document.createElement('div')\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template)\n\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector)\n }\n\n const template = templateWrapper.children[0]\n const extraClass = this._resolvePossibleFunction(this._config.extraClass)\n\n if (extraClass) {\n template.classList.add(...extraClass.split(' '))\n }\n\n return template\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config)\n this._checkContent(config.content)\n }\n\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({ selector, entry: content }, DefaultContentType)\n }\n }\n\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template)\n\n if (!templateElement) {\n return\n }\n\n content = this._resolvePossibleFunction(content)\n\n if (!content) {\n templateElement.remove()\n return\n }\n\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement)\n return\n }\n\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content)\n return\n }\n\n templateElement.textContent = content\n }\n\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg\n }\n\n _resolvePossibleFunction(arg) {\n return execute(arg, [this])\n }\n\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = ''\n templateElement.append(element)\n return\n }\n\n templateElement.textContent = element.textContent\n }\n}\n\nexport default TemplateFactory\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport * as Popper from '@popperjs/core'\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport { defineJQueryPlugin, execute, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index.js'\nimport { DefaultAllowlist } from './util/sanitizer.js'\nimport TemplateFactory from './util/template-factory.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'tooltip'\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])\n\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_MODAL = 'modal'\nconst CLASS_NAME_SHOW = 'show'\n\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner'\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`\n\nconst EVENT_MODAL_HIDE = 'hide.bs.modal'\n\nconst TRIGGER_HOVER = 'hover'\nconst TRIGGER_FOCUS = 'focus'\nconst TRIGGER_CLICK = 'click'\nconst TRIGGER_MANUAL = 'manual'\n\nconst EVENT_HIDE = 'hide'\nconst EVENT_HIDDEN = 'hidden'\nconst EVENT_SHOW = 'show'\nconst EVENT_SHOWN = 'shown'\nconst EVENT_INSERTED = 'inserted'\nconst EVENT_CLICK = 'click'\nconst EVENT_FOCUSIN = 'focusin'\nconst EVENT_FOCUSOUT = 'focusout'\nconst EVENT_MOUSEENTER = 'mouseenter'\nconst EVENT_MOUSELEAVE = 'mouseleave'\n\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n}\n\nconst Default = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' +\n '
' +\n '
' +\n '
',\n title: '',\n trigger: 'hover focus'\n}\n\nconst DefaultType = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n}\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)')\n }\n\n super(element, config)\n\n // Private\n this._isEnabled = true\n this._timeout = 0\n this._isHovered = null\n this._activeTrigger = {}\n this._popper = null\n this._templateFactory = null\n this._newContent = null\n\n // Protected\n this.tip = null\n\n this._setListeners()\n\n if (!this._config.selector) {\n this._fixTitle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle() {\n if (!this._isEnabled) {\n return\n }\n\n this._activeTrigger.click = !this._activeTrigger.click\n if (this._isShown()) {\n this._leave()\n return\n }\n\n this._enter()\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)\n\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'))\n }\n\n this._disposePopper()\n super.dispose()\n }\n\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n if (!(this._isWithContent() && this._isEnabled)) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW))\n const shadowRoot = findShadowRoot(this._element)\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)\n\n if (showEvent.defaultPrevented || !isInTheDom) {\n return\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper()\n\n const tip = this._getTipElement()\n\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'))\n\n const { container } = this._config\n\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip)\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))\n }\n\n this._popper = this._createPopper(tip)\n\n tip.classList.add(CLASS_NAME_SHOW)\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop)\n }\n }\n\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN))\n\n if (this._isHovered === false) {\n this._leave()\n }\n\n this._isHovered = false\n }\n\n this._queueCallback(complete, this.tip, this._isAnimated())\n }\n\n hide() {\n if (!this._isShown()) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE))\n if (hideEvent.defaultPrevented) {\n return\n }\n\n const tip = this._getTipElement()\n tip.classList.remove(CLASS_NAME_SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop)\n }\n }\n\n this._activeTrigger[TRIGGER_CLICK] = false\n this._activeTrigger[TRIGGER_FOCUS] = false\n this._activeTrigger[TRIGGER_HOVER] = false\n this._isHovered = null // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return\n }\n\n if (!this._isHovered) {\n this._disposePopper()\n }\n\n this._element.removeAttribute('aria-describedby')\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN))\n }\n\n this._queueCallback(complete, this.tip, this._isAnimated())\n }\n\n update() {\n if (this._popper) {\n this._popper.update()\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle())\n }\n\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate())\n }\n\n return this.tip\n }\n\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml()\n\n // TODO: remove this check in v6\n if (!tip) {\n return null\n }\n\n tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`)\n\n const tipId = getUID(this.constructor.NAME).toString()\n\n tip.setAttribute('id', tipId)\n\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE)\n }\n\n return tip\n }\n\n setContent(content) {\n this._newContent = content\n if (this._isShown()) {\n this._disposePopper()\n this.show()\n }\n }\n\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content)\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n })\n }\n\n return this._templateFactory\n }\n\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n }\n }\n\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title')\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())\n }\n\n _isAnimated() {\n return this._config.animation || (this.tip && this.tip.classList.contains(CLASS_NAME_FADE))\n }\n\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW)\n }\n\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element])\n const attachment = AttachmentMap[placement.toUpperCase()]\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))\n }\n\n _getOffset() {\n const { offset } = this._config\n\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10))\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element)\n }\n\n return offset\n }\n\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element])\n }\n\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [\n {\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n },\n {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n },\n {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n },\n {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n },\n {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement)\n }\n }\n ]\n }\n\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n }\n }\n\n _setListeners() {\n const triggers = this._config.trigger.split(' ')\n\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context.toggle()\n })\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ?\n this.constructor.eventName(EVENT_MOUSEENTER) :\n this.constructor.eventName(EVENT_FOCUSIN)\n const eventOut = trigger === TRIGGER_HOVER ?\n this.constructor.eventName(EVENT_MOUSELEAVE) :\n this.constructor.eventName(EVENT_FOCUSOUT)\n\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true\n context._enter()\n })\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] =\n context._element.contains(event.relatedTarget)\n\n context._leave()\n })\n }\n }\n\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide()\n }\n }\n\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)\n }\n\n _fixTitle() {\n const title = this._element.getAttribute('title')\n\n if (!title) {\n return\n }\n\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title)\n }\n\n this._element.setAttribute('data-bs-original-title', title) // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title')\n }\n\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true\n return\n }\n\n this._isHovered = true\n\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show()\n }\n }, this._config.delay.show)\n }\n\n _leave() {\n if (this._isWithActiveTrigger()) {\n return\n }\n\n this._isHovered = false\n\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide()\n }\n }, this._config.delay.hide)\n }\n\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout)\n this._timeout = setTimeout(handler, timeout)\n }\n\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true)\n }\n\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element)\n\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute]\n }\n }\n\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n }\n config = this._mergeConfigObj(config)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container)\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value\n }\n }\n\n config.selector = false\n config.trigger = 'manual'\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config\n }\n\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy()\n this._popper = null\n }\n\n if (this.tip) {\n this.tip.remove()\n this.tip = null\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip)\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Tooltip from './tooltip.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'popover'\n\nconst SELECTOR_TITLE = '.popover-header'\nconst SELECTOR_CONTENT = '.popover-body'\n\nconst Default = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' +\n '
' +\n '

' +\n '
' +\n '
',\n trigger: 'click'\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n}\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent()\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n }\n }\n\n _getContent() {\n return this._resolvePossibleFunction(this._config.content)\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover)\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport { defineJQueryPlugin, getElement, isDisabled, isVisible } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'scrollspy'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst EVENT_ACTIVATE = `activate${EVENT_KEY}`\nconst EVENT_CLICK = `click${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'\nconst CLASS_NAME_ACTIVE = 'active'\n\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]'\nconst SELECTOR_TARGET_LINKS = '[href]'\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'\nconst SELECTOR_NAV_LINKS = '.nav-link'\nconst SELECTOR_NAV_ITEMS = '.nav-item'\nconst SELECTOR_LIST_ITEMS = '.list-group-item'\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`\nconst SELECTOR_DROPDOWN = '.dropdown'\nconst SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'\n\nconst Default = {\n offset: null, // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n}\n\nconst DefaultType = {\n offset: '(number|null)', // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n}\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map()\n this._observableSections = new Map()\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element\n this._activeTarget = null\n this._observer = null\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n }\n this.refresh() // initialize\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables()\n this._maybeEnableSmoothScroll()\n\n if (this._observer) {\n this._observer.disconnect()\n } else {\n this._observer = this._getNewObserver()\n }\n\n for (const section of this._observableSections.values()) {\n this._observer.observe(section)\n }\n }\n\n dispose() {\n this._observer.disconnect()\n super.dispose()\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin\n\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value))\n }\n\n return config\n }\n\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK)\n\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash)\n if (observableSection) {\n event.preventDefault()\n const root = this._rootElement || window\n const height = observableSection.offsetTop - this._element.offsetTop\n if (root.scrollTo) {\n root.scrollTo({ top: height, behavior: 'smooth' })\n return\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height\n }\n })\n }\n\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n }\n\n return new IntersectionObserver(entries => this._observerCallback(entries), options)\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`)\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop\n this._process(targetElement(entry))\n }\n\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop\n this._previousScrollData.parentScrollTop = parentScrollTop\n\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null\n this._clearActiveClass(targetElement(entry))\n\n continue\n }\n\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry)\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return\n }\n\n continue\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry)\n }\n }\n }\n\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map()\n this._observableSections = new Map()\n\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target)\n\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue\n }\n\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element)\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor)\n this._observableSections.set(anchor.hash, observableSection)\n }\n }\n }\n\n _process(target) {\n if (this._activeTarget === target) {\n return\n }\n\n this._clearActiveClass(this._config.target)\n this._activeTarget = target\n target.classList.add(CLASS_NAME_ACTIVE)\n this._activateParents(target)\n\n EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target })\n }\n\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN))\n .classList.add(CLASS_NAME_ACTIVE)\n return\n }\n\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both