{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "b518b04cbfe0" }, "source": [ "##### Copyright 2020 The TensorFlow Authors." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "cellView": "form", "execution": { "iopub.execute_input": "2024-01-11T19:57:49.999026Z", "iopub.status.busy": "2024-01-11T19:57:49.998473Z", "iopub.status.idle": "2024-01-11T19:57:50.002183Z", "shell.execute_reply": "2024-01-11T19:57:50.001553Z" }, "id": "906e07f6e562" }, "outputs": [], "source": [ "#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", "# You may obtain a copy of the License at\n", "#\n", "# https://www.apache.org/licenses/LICENSE-2.0\n", "#\n", "# Unless required by applicable law or agreed to in writing, software\n", "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", "# See the License for the specific language governing permissions and\n", "# limitations under the License." ] }, { "cell_type": "markdown", "metadata": { "id": "8bd329a4bbca" }, "source": [ "# Keras でマスキングとパディングをする" ] }, { "cell_type": "markdown", "metadata": { "id": "8b208d0913b8" }, "source": [ "\n", " \n", " \n", " \n", " \n", "
TensorFlow.orgで表示 Google Colab で実行GitHub でソースを表示ノートブックをダウンロード
" ] }, { "cell_type": "markdown", "metadata": { "id": "8d4ac441b1fc" }, "source": [ "## MNIST モデルをビルドする" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:50.006314Z", "iopub.status.busy": "2024-01-11T19:57:50.005695Z", "iopub.status.idle": "2024-01-11T19:57:52.407113Z", "shell.execute_reply": "2024-01-11T19:57:52.406352Z" }, "id": "ec52be14e686" }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "2024-01-11 19:57:50.441698: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:9261] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\n", "2024-01-11 19:57:50.441746: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:607] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\n", "2024-01-11 19:57:50.443298: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1515] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n" ] } ], "source": [ "import numpy as np\n", "import tensorflow as tf\n", "from tensorflow import keras\n", "from tensorflow.keras import layers" ] }, { "cell_type": "markdown", "metadata": { "id": "e94d7a46bda8" }, "source": [ "## はじめに\n", "\n", "**マスキング**は、シーケンス処理レイヤーに入力の特定の時間ステップが欠落しているためデータを処理する際にスキップする必要があることを伝えるために使用する手法です。\n", "\n", "**パディング**は、マスキングされたステップがシーケンスの先頭または末尾にある特殊なマスキングです。パディングは、シーケンスデータを連続したバッチにエンコードする必要性から生まれました。バッチ内のすべてのシーケンスを所定の標準の長さに合わせるためには、一部のシーケンスをパディングまたはトランケートする(切り詰める)必要があるためです。\n", "\n", "では、詳しく見ていきましょう。" ] }, { "cell_type": "markdown", "metadata": { "id": "ac6b121d6be0" }, "source": [ "## パディングシーケンスデータ\n", "\n", "シーケンスデータを処理する際に個々のサンプルの長さが異なることは、非常に一般的です。次の例(単語としてトークン化されたテキスト)を考えてみます。\n", "\n", "```\n", "[\n", " [\"Hello\", \"world\", \"!\"],\n", " [\"How\", \"are\", \"you\", \"doing\", \"today\"],\n", " [\"The\", \"weather\", \"will\", \"be\", \"nice\", \"tomorrow\"],\n", "]\n", "```\n", "\n", "語彙検索の後、データは以下のように整数としてベクトル化されるかもしれません。\n", "\n", "```\n", "[\n", " [71, 1331, 4231]\n", " [73, 8, 3215, 55, 927],\n", " [83, 91, 1, 645, 1253, 927],\n", "]\n", "```\n", "\n", "データは、個々のサンプルがそれぞれ 3、5、6 の長さを持つネストされたリストです。ディープラーニングモデルの入力データは,単一のテンソル(例えばこの場合だと`(batch_size, 6, vocab_size)`のような形状)でなければならないため、最長のアイテムよりも短いサンプルは、何らかのプレースホルダー値でパディングする必要があります。(その代わりに、短いサンプルをパディングする前に長いサンプルをトランケートすることも可能です。)\n", "\n", "Keras は Python のリストを共通の長さにトランケートしたりパディングしたりするユーティリティ関数を提供します:`tf.keras.preprocessing.sequence.pad_sequences`" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:52.411594Z", "iopub.status.busy": "2024-01-11T19:57:52.411184Z", "iopub.status.idle": "2024-01-11T19:57:52.416504Z", "shell.execute_reply": "2024-01-11T19:57:52.415781Z" }, "id": "bb64fb185a05" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[ 711 632 71 0 0 0]\n", " [ 73 8 3215 55 927 0]\n", " [ 83 91 1 645 1253 927]]\n" ] } ], "source": [ "raw_inputs = [\n", " [711, 632, 71],\n", " [73, 8, 3215, 55, 927],\n", " [83, 91, 1, 645, 1253, 927],\n", "]\n", "\n", "# By default, this will pad using 0s; it is configurable via the\n", "# \"value\" parameter.\n", "# Note that you could \"pre\" padding (at the beginning) or\n", "# \"post\" padding (at the end).\n", "# We recommend using \"post\" padding when working with RNN layers\n", "# (in order to be able to use the\n", "# CuDNN implementation of the layers).\n", "padded_inputs = tf.keras.preprocessing.sequence.pad_sequences(\n", " raw_inputs, padding=\"post\"\n", ")\n", "print(padded_inputs)\n" ] }, { "cell_type": "markdown", "metadata": { "id": "03092b2da690" }, "source": [ "## マスキング\n", "\n", "全てのサンプルが統一された長さになったので、今度はデータの一部が実際にパディングされ、無視されるべきであることをモデルに知らせなければなりません。このメカニズムが**マスキング**です。\n", "\n", "Keras モデルで入力マスクを導入するには、3 つの方法があります。\n", "\n", "- `keras.layers.Masking` レイヤーを追加する。\n", "- `keras.layers.Embedding` レイヤーを `mask_zero=True` で設定する。\n", "- `mask`引数をサポートするレイヤー(RNN レイヤーなど)を呼び出す際に、この引数を手動で渡す。" ] }, { "cell_type": "markdown", "metadata": { "id": "6103601e5fff" }, "source": [ "## マスク生成レイヤー : `Embedding` と `Masking`\n", "\n", "内部でこれらのレイヤーはマスクテンソル(形状`(batch, sequence_length)`の 2 次元テンソル)を作成し、`Masking` または `Embedding` レイヤーによって返されるテンソル出力にアタッチします。" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:52.420086Z", "iopub.status.busy": "2024-01-11T19:57:52.419824Z", "iopub.status.idle": "2024-01-11T19:57:54.755758Z", "shell.execute_reply": "2024-01-11T19:57:54.754864Z" }, "id": "b2363b293483" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "tf.Tensor(\n", "[[ True True True False False False]\n", " [ True True True True True False]\n", " [ True True True True True True]], shape=(3, 6), dtype=bool)\n", "tf.Tensor(\n", "[[ True True True False False False]\n", " [ True True True True True False]\n", " [ True True True True True True]], shape=(3, 6), dtype=bool)\n" ] } ], "source": [ "embedding = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)\n", "masked_output = embedding(padded_inputs)\n", "\n", "print(masked_output._keras_mask)\n", "\n", "masking_layer = layers.Masking()\n", "# Simulate the embedding lookup by expanding the 2D input to 3D,\n", "# with embedding dimension of 10.\n", "unmasked_embedding = tf.cast(\n", " tf.tile(tf.expand_dims(padded_inputs, axis=-1), [1, 1, 10]), tf.float32\n", ")\n", "\n", "masked_embedding = masking_layer(unmasked_embedding)\n", "print(masked_embedding._keras_mask)" ] }, { "cell_type": "markdown", "metadata": { "id": "17e4bdb563b2" }, "source": [ "出力された結果から分かるように、マスクは形状が`(batch_size, sequence_length)`の 2 次元ブールテンソルであり、そこでは個々の `False` エントリは、対応する時間ステップを処理中に無視すべきであることを示しています。" ] }, { "cell_type": "markdown", "metadata": { "id": "cf11a0399fcf" }, "source": [ "## Functional API と Sequential API のマスク伝播\n", "\n", "Functional API または Sequential API を使用する場合、`Embedding` レイヤーまたは `Masking` レイヤーによって生成されたマスクは、それらを使用できる任意のレイヤー(例えば RNN レイヤーなど)にネットワークを介して伝播されます。Keras は入力に対応するマスクを自動的に取得し、その使用方法を知っている任意のレイヤーに渡します。\n", "\n", "例えば、以下の Sequential API モデルでは、`LSTM` レイヤーは自動的にマスクを取得します。つまりこれは、パディングされた値を無視するということです。" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:54.759961Z", "iopub.status.busy": "2024-01-11T19:57:54.759300Z", "iopub.status.idle": "2024-01-11T19:57:55.614407Z", "shell.execute_reply": "2024-01-11T19:57:55.613633Z" }, "id": "0adbecda288a" }, "outputs": [], "source": [ "model = keras.Sequential(\n", " [layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True), layers.LSTM(32),]\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "a8ac6481a1d5" }, "source": [ "これは、以下の Functional API モデルでも同様です。" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:55.618468Z", "iopub.status.busy": "2024-01-11T19:57:55.618226Z", "iopub.status.idle": "2024-01-11T19:57:56.356363Z", "shell.execute_reply": "2024-01-11T19:57:56.355536Z" }, "id": "f374ab06743d" }, "outputs": [], "source": [ "inputs = keras.Input(shape=(None,), dtype=\"int32\")\n", "x = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)(inputs)\n", "outputs = layers.LSTM(32)(x)\n", "\n", "model = keras.Model(inputs, outputs)" ] }, { "cell_type": "markdown", "metadata": { "id": "2f2c4b96ecb5" }, "source": [ "## マスクテンソルを直接レイヤーに渡す" ] }, { "cell_type": "markdown", "metadata": { "id": "11dccb581014" }, "source": [ "マスクを扱うことができるレイヤー(`LSTM` レイヤーなど)は、それらの `__call__` メソッドに `mask` 引数を持っています。\n", "\n", "一方、マスクを生成するレイヤー(例えば
`Embedding`)は、呼び出し可能な `compute_mask(input, previous_mask)` メソッドを公開します。\n", "\n", "例えば下記のようにして、マスクを生成するレイヤーの `compute_mask()` メソッドの出力を、マスクを消費するレイヤーの `__call__` メソッドに渡すことができます。" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:56.360759Z", "iopub.status.busy": "2024-01-11T19:57:56.360486Z", "iopub.status.idle": "2024-01-11T19:57:57.721267Z", "shell.execute_reply": "2024-01-11T19:57:57.720508Z" }, "id": "1955aa63896b" }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class MyLayer(layers.Layer):\n", " def __init__(self, **kwargs):\n", " super(MyLayer, self).__init__(**kwargs)\n", " self.embedding = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)\n", " self.lstm = layers.LSTM(32)\n", "\n", " def call(self, inputs):\n", " x = self.embedding(inputs)\n", " # Note that you could also prepare a `mask` tensor manually.\n", " # It only needs to be a boolean tensor\n", " # with the right shape, i.e. (batch_size, timesteps).\n", " mask = self.embedding.compute_mask(inputs)\n", " output = self.lstm(x, mask=mask) # The layer will ignore the masked values\n", " return output\n", "\n", "\n", "layer = MyLayer()\n", "x = np.random.random((32, 10)) * 100\n", "x = x.astype(\"int32\")\n", "layer(x)" ] }, { "cell_type": "markdown", "metadata": { "id": "b04dd330f848" }, "source": [ "## カスタムレイヤーでマスキングをサポートする" ] }, { "cell_type": "markdown", "metadata": { "id": "8451a1a8ff27" }, "source": [ "場合によっては、マスクを生成するレイヤー(`Embedding` など)や、現在のマスクを変更するレイヤーを書く必要があります。\n", "\n", "例えば、時間次元で連結する `Concatenate` レイヤーのように、入力とは異なる時間次元を持つテンソルを生成するレイヤーは、現在のマスクを変更して、マスクされた時間ステップを下流のレイヤーが適切に考慮に入れられるようにする必要があります。\n", "\n", "これを行うには、レイヤーに `layer.compute_mask()` メソッドを実装します。これは、入力と現在のマスクが与えられた時に新しいマスクを生成します。\n", "\n", "ここでは、現在のマスクを変更する必要がある `TemporalSplit` レイヤーの例を示します。" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:57.724777Z", "iopub.status.busy": "2024-01-11T19:57:57.724529Z", "iopub.status.idle": "2024-01-11T19:57:57.733251Z", "shell.execute_reply": "2024-01-11T19:57:57.732633Z" }, "id": "a06fb2194c0d" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "tf.Tensor(\n", "[[ True True True]\n", " [ True True True]\n", " [ True True True]], shape=(3, 3), dtype=bool)\n", "tf.Tensor(\n", "[[False False False]\n", " [ True True False]\n", " [ True True True]], shape=(3, 3), dtype=bool)\n" ] } ], "source": [ "class TemporalSplit(keras.layers.Layer):\n", " \"\"\"Split the input tensor into 2 tensors along the time dimension.\"\"\"\n", "\n", " def call(self, inputs):\n", " # Expect the input to be 3D and mask to be 2D, split the input tensor into 2\n", " # subtensors along the time axis (axis 1).\n", " return tf.split(inputs, 2, axis=1)\n", "\n", " def compute_mask(self, inputs, mask=None):\n", " # Also split the mask into 2 if it presents.\n", " if mask is None:\n", " return None\n", " return tf.split(mask, 2, axis=1)\n", "\n", "\n", "first_half, second_half = TemporalSplit()(masked_embedding)\n", "print(first_half._keras_mask)\n", "print(second_half._keras_mask)" ] }, { "cell_type": "markdown", "metadata": { "id": "282b867dcd95" }, "source": [ "もう 1 つの例として、入力値からマスクを生成できる `CustomEmbedding` レイヤーの例を示します。" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:57.736153Z", "iopub.status.busy": "2024-01-11T19:57:57.735897Z", "iopub.status.idle": "2024-01-11T19:57:57.749640Z", "shell.execute_reply": "2024-01-11T19:57:57.748975Z" }, "id": "e760655cd39c" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "tf.Tensor(\n", "[[False True True True True True True True True True]\n", " [ True False True True True True True False True True]\n", " [ True True True False True True True False True True]], shape=(3, 10), dtype=bool)\n" ] } ], "source": [ "class CustomEmbedding(keras.layers.Layer):\n", " def __init__(self, input_dim, output_dim, mask_zero=False, **kwargs):\n", " super(CustomEmbedding, self).__init__(**kwargs)\n", " self.input_dim = input_dim\n", " self.output_dim = output_dim\n", " self.mask_zero = mask_zero\n", "\n", " def build(self, input_shape):\n", " self.embeddings = self.add_weight(\n", " shape=(self.input_dim, self.output_dim),\n", " initializer=\"random_normal\",\n", " dtype=\"float32\",\n", " )\n", "\n", " def call(self, inputs):\n", " return tf.nn.embedding_lookup(self.embeddings, inputs)\n", "\n", " def compute_mask(self, inputs, mask=None):\n", " if not self.mask_zero:\n", " return None\n", " return tf.not_equal(inputs, 0)\n", "\n", "\n", "layer = CustomEmbedding(10, 32, mask_zero=True)\n", "x = np.random.random((3, 10)) * 9\n", "x = x.astype(\"int32\")\n", "\n", "y = layer(x)\n", "mask = layer.compute_mask(x)\n", "\n", "print(mask)" ] }, { "cell_type": "markdown", "metadata": { "id": "bb34149eb837" }, "source": [ "## オプトインして互換性のあるレイヤー間でマスクを伝播する\n", "\n", "ほとんどのレイヤーは時間次元を変更しないため、現在のマスクを変更する必要はありません。しかし、現在のマスクを変更せずにそれらを次のレイヤーに**伝播**したい場合があります。**これはオプトイン動作です。** デフォルトでは、(フレームワークがマスクの伝播が安全かどうか判断する方法を持たないため)カスタムレイヤーは現在のマスクを破棄します。\n", "\n", "時間次元を変更しないカスタムレイヤーを持ち、それが現在の入力マスクを伝播できるようにしたい場合は、レイヤーのコンストラクタを `self.supports_masking = True` に設定する必要があります。この場合、`compute_mask()` のデフォルトの動作は、現在のマスクを通過させるだけとなります。\n", "\n", "マスク伝搬のためにホワイトリスト化されたレイヤーの例を示します。:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:57.752602Z", "iopub.status.busy": "2024-01-11T19:57:57.752350Z", "iopub.status.idle": "2024-01-11T19:57:57.756489Z", "shell.execute_reply": "2024-01-11T19:57:57.755823Z" }, "id": "895c35534d06" }, "outputs": [], "source": [ "class MyActivation(keras.layers.Layer):\n", " def __init__(self, **kwargs):\n", " super(MyActivation, self).__init__(**kwargs)\n", " # Signal that the layer is safe for mask propagation\n", " self.supports_masking = True\n", "\n", " def call(self, inputs):\n", " return tf.nn.relu(inputs)\n" ] }, { "cell_type": "markdown", "metadata": { "id": "a2e1e0a81995" }, "source": [ "これで、マスク生成レイヤー(`Embedding` など)とマスク消費レイヤー(`LSTM` など)間でこのカスタムレイヤーの使用が可能となり、マスク消費レイヤーまで届くようにマスクを渡します。" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:57.759569Z", "iopub.status.busy": "2024-01-11T19:57:57.759093Z", "iopub.status.idle": "2024-01-11T19:57:58.512038Z", "shell.execute_reply": "2024-01-11T19:57:58.511322Z" }, "id": "486e39e9a9a7" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Mask found: KerasTensor(type_spec=TensorSpec(shape=(None, None), dtype=tf.bool, name=None), name='Placeholder_1:0')\n" ] } ], "source": [ "inputs = keras.Input(shape=(None,), dtype=\"int32\")\n", "x = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)(inputs)\n", "x = MyActivation()(x) # Will pass the mask along\n", "print(\"Mask found:\", x._keras_mask)\n", "outputs = layers.LSTM(32)(x) # Will receive the mask\n", "\n", "model = keras.Model(inputs, outputs)" ] }, { "cell_type": "markdown", "metadata": { "id": "55ab9c02f4ba" }, "source": [ "## マスク情報が必要なレイヤーを書く\n", "\n", "一部のレイヤーはマスク*コンシューマ*です。それらは `call` で `mask` 引数を受け取り、それを使って特定の時間ステップをスキップするかどうかを判断します。\n", "\n", "そのようなレイヤーを書くには、単純に `call` シグネチャに `mask=None` 引数を追加します。入力に関連付けられたマスクは、それが利用可能な時にいつでもレイヤーに渡されます。\n", "\n", "以下に簡単な例を示します。これは入力シーケンスの時間次元(軸 1)のソフトマックスを計算し、マスクされたタイムステップを破棄するレイヤーです。" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "execution": { "iopub.execute_input": "2024-01-11T19:57:58.516068Z", "iopub.status.busy": "2024-01-11T19:57:58.515761Z", "iopub.status.idle": "2024-01-11T19:57:58.695947Z", "shell.execute_reply": "2024-01-11T19:57:58.695176Z" }, "id": "9774bcd59908" }, "outputs": [], "source": [ "class TemporalSoftmax(keras.layers.Layer):\n", " def call(self, inputs, mask=None):\n", " broadcast_float_mask = tf.expand_dims(tf.cast(mask, \"float32\"), -1)\n", " inputs_exp = tf.exp(inputs) * broadcast_float_mask\n", " inputs_sum = tf.reduce_sum(\n", " inputs_exp * broadcast_float_mask, axis=-1, keepdims=True\n", " )\n", " return inputs_exp / inputs_sum\n", "\n", "\n", "inputs = keras.Input(shape=(None,), dtype=\"int32\")\n", "x = layers.Embedding(input_dim=10, output_dim=32, mask_zero=True)(inputs)\n", "x = layers.Dense(1)(x)\n", "outputs = TemporalSoftmax()(x)\n", "\n", "model = keras.Model(inputs, outputs)\n", "y = model(np.random.randint(0, 10, size=(32, 100)), np.random.random((32, 100, 1)))" ] }, { "cell_type": "markdown", "metadata": { "id": "6373f43bbe18" }, "source": [ "## 要約\n", "\n", "Keras のパディングとマスキングについて知っておくべきことはこれだけです。以下に要約します。\n", "\n", "- 「マスキング」とは、シーケンス入力の特定の時間ステップをスキップしたり無視したりするタイミングをレイヤーが分かるようにする方法です。\n", "- 一部のレイヤーはマスクジェネレーターです : `Embedding` は入力値からマスクを生成することができ(`mask_zero=True` の場合)、`Masking` レイヤーも同様に生成することができます。\n", "- 一部のレイヤーはマスクコンシューマです : これらは `mask` 引数を `__call__` メソッドで公開します。RNN レイヤーはこれに該当します。\n", "- Functional API および Sequential API では、マスク情報は自動的に伝搬されます。\n", "- レイヤーをスタンドアロンで使用する場合には、`mask` 引数をレイヤーに手動で渡すことができます。\n", "- 現在のマスクを変更するレイヤー、新しいマスクを生成するレイヤー、入力に関連付けられたマスクを消費するレイヤーを簡単に書くことができます。" ] } ], "metadata": { "colab": { "collapsed_sections": [], "name": "masking_and_padding.ipynb", "toc_visible": true }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 0 }