0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tensorflow.kerasのSequential APIを使ってMNISTを扱う簡単なAIモデルを定義してTensorflowによる学習手順と推論方法を理解する

Last updated at Posted at 2025-01-11

Tensorflow.kerasのFunctional APIを使ってSkip-Connection構造を持ったAIモデルを記述し、学習を行い、成果物をTensorflow Lite(tflite)で実行する の前編となる記事です。本記事ではAIモデルを定義しますが、そのAIモデルは高い精度で手書き数字画像(MNIST)を分類する性能に至りません。本記事は、あくまで大まかなTensorflowの使い方を紹介する記事です。適切なAIモデルは後半記事にて紹介します。


ゼロからわかるTensorflowとTensorflow Lite

Tensorflow Liteは、ONNXと並んで、デファクトスタンダートとも言えるエッジAIの実行環境です。 Tensorflow LiteでAIモデルを実行するためには、まずTensorflowにてAIモデルを定義し、AIモデルの学習を実行、学習済みのAIモデルをtflite形式にてエクスポートします。 Tensorflow Liteは、ここで生成されたtflite形式のAIモデルを、組み込みデバイス上のTensorflow Liteランタイムにより実行することで、組み込み環境におけるAIモデルの実行を可能としています。Tensorflow Liteランタイムは、Android/iOS/Linuxなど幅広いOSのC++/Java/Swift/Objective-C/Pythonの開発環境向けに提供されており、ほぼ全てのソフトウェア開発環境で利用することが可能です。さらにTensorflow Liteは、推論のみを行うランタイムのため、Tensorflowに比べて小さなフットプリントでAIモデルを実行することができます。

本記事のゴール

本記事では、Tensorflowの使い方から、TensorflowでのAIモデルの定義方法、AIモデルの学習方法と、学習結果をtflite形式へと変換しTensorflow Liteランタイムにより実行する方法についてご紹介します。今回は簡単化のため、Tensorflow LiteランタイムはPythonのものを選択します。 なお、AIモデルの定義方法としては、まずはじめに簡単にモデルを定義のできるSequential APIによる定義方法を紹介し、次にSkip-Connectionなども定義することのできるFunctional APIによるモデルの定義方法を紹介します。

前編/後編

本記事は前編と後編で構成されています。 まずこの前編では、基本的なTensorflowの使い方とSesuential APIを利用した簡単なAIモデルの定義方法についてご紹介します。 後編にて、Functional APIの利用方法とTensorflow Liteモデルへの置換、Tensorflow Liteによる推論といった核心部分をご紹介します。皆様の理解度と利用用途に応じて、読み分けてください。


  • 前編 (本記事)
    • 開発環境のセットアップ
    • Visual Studio Codeのセットアップ
    • TensorflowでMNISTデータセットの準備
    • TensorflowのSequential APIによるAIモデルの定義
    • Tensorflowの定義したAIモデルの学習
    • 学習済みAIモデルの評価・利用
    • TensorBoardによる学習結果の可視化


  • 後編
    • 畳み込みニューラルネットワークの定義
    • TensorflowのFunctional APIによるSkip-Connectionの定義
    • Functional APIにより定義したモデルの学習と評価
    • Tensorflow Lite形式への変換
    • Tensorflow Liteランタイムによる推論の実行


参考文献

本記事は、下記の資料を参考にさせていただきました。

開発環境のセットアップ

まず、開発に利用する環境をセットアップしていきましょう。

開発の前準備

はじめに、Pythonの仮想環境を作成し、TensorflowとTensorflow Liteをインストールします。AnacondaのインストールとTensorflowでGPUを使えるようにするセットアップ手順は下記の記事をご確認ください。なお、本記事で扱う Functional APIを提供するTensorflow.kerasと、AIモデルをTensorflow Lite形式へと変換するコンバータは全てTensorflowに含まれています。 なお、Tensorflowを便利に利用するためには、Tensorflowに加えていくつかの追加パッケージをインストールする必要があります。 pip3 installsudo apt install の実行箇所 をご参照ください。本記事では、以降これらのパッケージがインストールされているものとして解説を行います。

Tensorflow/Tensorflow Lite(tflite_runtime)のインストールや、Python仮想環境の設定方法は下記の記事を参考にさせていただきました。ありがとうございます。

###
# インストール済みのAnacondaを有効化する
###
$ conda config --set auto_activate_base true
$ conda activate

###
# 作業ディレクトリを作成する
###
$ cd ~
$ mkdir -p ~/tensorflow-tflite-work
$ cd ~/tensorflow-tflite-work

###
# Pythonのバージョンを確認する
###
$ python3 --version
Python 3.12.7

###
# Anacondaで仮想環境を作成する
# -->> tfliteを利用したいのでPython3.10を使う
###
$ conda create -n tflite_work python=3.10
# Retrieving notices: ...working... done
# ...
# Proceed ([y]/n)? 
# ...
# Downloading and Extracting Packages:
# Preparing transaction: done
# Verifying transaction: done
# Executing transaction: done
#
# To activate this environment, use
#
#     $ conda activate tflite_work
#
# To deactivate an active environment, use
#
#     $ conda deactivate

###
# 仮想環境にログインする
###
$ conda activate tflite_work
$ conda info -e
# conda environments:
#
# base                     /home/shino/anaconda3
# tflite_work           *  /home/shino/anaconda3/envs/tflite_work
#                          /home/shino/widerface_dataset/WIDER_train/images/env
#                          /home/shino/yolov9/yolov9/env

$ python --version
Python 3.10.16

###
# NVIDIA GPUがインストールできていることを確認する
###
$ nvidia-smi
# Fri Jan 10 10:31:04 2025       
# +-----------------------------------------------------------------------------------------+
# | NVIDIA-SMI 550.127.05             Driver Version: 550.127.05     CUDA Version: 12.4     |
# |-----------------------------------------+------------------------+----------------------+
# | GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
# | Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
# |                                         |                        |               MIG M. |
# |=========================================+========================+======================|
# |   0  NVIDIA GeForce RTX 3060 Ti     Off |   00000000:01:00.0  On |                  N/A |
# |  0%   42C    P8             14W /  200W |     266MiB /   8192MiB |      0%      Default |
# |                                         |                        |                  N/A |
# +-----------------------------------------+------------------------+----------------------+
#                                                                                          
# +-----------------------------------------------------------------------------------------+
# | Processes:                                                                              |
# |  GPU   GI   CI        PID   Type   Process name                              GPU Memory |
# |        ID   ID                                                               Usage      |
# |=========================================================================================|
# |    0   N/A  N/A      1788      G   /usr/lib/xorg/Xorg                            178MiB |
# |    0   N/A  N/A      1997      G   /usr/bin/gnome-shell                           79MiB |
# +-----------------------------------------------------------------------------------------+

###
# Tensorflowをはじめとする
# 開発に使うパッケージを仮想環境にインストールする
# -->> これらのパッケージは本記事で以降紹介するプログラムが利用するものです
###
# Tensorflow本体をインストールする
$ pip3 install --upgrade tensorflow
$ pip3 install opencv-python
# Tensorflow Liteランタイムをインストールする
$ pip3 install tflite-runtime
# 画像を扱うパッケージをインストールする
$ pip3 install pillow
$ pip3 install matplotlib
# TensorBoardの実行に必要なパッケージをインストールする
$ pip3 install pydot
$ pip3 install graphviz
$ sudo apt install graphviz
### -->> https://graphviz.gitlab.io/download/
# AIモデルの定義に必要なパッケージをインストールする
$ pip3 install pyyaml h5py 

###
# TensorflowがGPUを使えることの確認をする
###
$ python3
# Python 3.12.8 | packaged by Anaconda, Inc. | (main, Dec 11 2024, 16:31:09) [GCC 11.2.0] on linux
# Type "help", "copyright", "credits" or "license" for more information.
>>> from tensorflow.python.client import device_lib
>>> device_lib.list_local_devices()
# [name: "/device:CPU:0"
# device_type: "CPU"
# memory_limit: 268435456
# locality {
# }
# incarnation: 1977471026262204167
# xla_global_id: -1
# , name: "/device:GPU:0"
# device_type: "GPU"
# memory_limit: 6322716672
# locality {
#   bus_id: 1
#   links {
#   }
# }
# incarnation: 3112413211099790863
# physical_device_desc: "device: 0, name: NVIDIA GeForce RTX 3060 Ti, pci bus id: 0000:01:00.0, compute capability: 8.6"
# xla_global_id: 416903419
# ]

Visual Studio Codeの設定

以上の手順で仮想環境を作成した後に、Visual Studio Codeを起動します。 今回はmacOS上のVisual Studio CodeからRTX3060Tiを搭載したワークステーションへリモート接続し、プログラムを開発 しました。リモート接続の手順については以下の記事をご参照ください。

接続後、 Visual Studio Codeに以下の拡張機能をインストール してください。

  • Japanese Language Pack
  • Pylance
  • Python
  • Python Debugger

スクリーンショット 2025-01-10 11.40.30.png

次に、開発に利用する作業ディレクトリ(今回は~/tensorflow-tflite-work)を開いてください。ディレクトリ内に、空のPythonファイル(*.py)を作成すると、画面右下にPythonのバージョンが表示されます。 表示されない場合は、Visual Studio Codeを再起動/再接続してみてください。

スクリーンショット 2025-01-10 11.05.28.png

バージョンをクリックすると、利用可能なAnacondaの仮想環境リストが表示されます。今回は前手順にて構築した仮想環境「tflite_work」を利用します。プルダウンメニューより対象の仮想環境を選択し、 右下のPythonバージョンのところに仮想環境名が変更されれば、準備完了 です。

スクリーンショット 2025-01-10 11.03.20.png

Tensorflow/Tensorflow Liteを読み込めることを確認する

作成したPythonスクリプト .py をVisual Studio Codeが実行できるかを確認します。これによりPython仮想環境をVisual Studio Codeが読み込めているかの調査をすることができます。下記のコードを記述した後、Visual Studio Codeの「実行とデバッグ」をクリックし、プログラムがGPU情報を出力し、エラーなく動作することを確認してください。 もしimport周りで問題が出るようであれば pip3 install が漏れているか、Visual Studio Codeに適切な仮想環境を指定できていない 可能性があります。

import tensorflow
from tensorflow.python.client import device_lib
import tflite_runtime as tflite

print(device_lib.list_local_devices())

スクリーンショット 2025-01-10 11.24.10.png

シンプルなニューラルネットワークをTensorflowのSequential APIにより定義してAIモデルの学習を体験する

Tensorflow.kerasのFunctional APIを使った、本格的なAIモデルを作成する前に、まずは Tensorflow.kerasのSequential APIを使ってシンプルなニューラルネットワークを定義し、 AIモデルの扱い方に慣れていきましょう。

ネットワークの入力/出力データを準備する

開発をはじめるにあたって、まずはAIモデルが扱う入出力データを準備します。今回の開発はお試し開発なので、 短時間でAIの学習と推論を体験できるコンパクトなデータセット「MNIST」 を利用します。

kerasを介してMNISTデータセットを読み込む

本記事では、MNIST画像の分類を行うAIモデルを開発します。MNISTは、0〜9の数字を手書きした画像を分類したデータセットです。1枚の画像は 28px x 28px であり、60000枚の学習用データセットと、10000枚の検証用データセットから構成されます。全ての画像には、その正答となるラベルが付与されています。

MNISTデータセットは tensorflow.keras.datasets から mnist としてimportすることができます。 kerasの提供するMNISTデータセットから、学習用データと検証用データを取得し、変数へ格納するプログラムを以下に示します。 このプログラムは、抽出したデータのうち学習用の入力データと検証用の入力データを255(濃度の最大値)で割ることによって 0.0〜1.0 の float32 型のデータを生成します。また、出力ラベルに対しても keras.utils.to_categorical によるOne-Hot encodingを適用し、10要素の配列で、ラベルに対応する項目のみが1.0、他の項目が0.0となるデータを生成します。

スクリーンショット 2025-01-10 14.23.38.png

import tensorflow.keras as keras
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.optimizers import SGD
from matplotlib import pyplot as plt

# MNISTの先頭を出力する画像ファイルへのパス
SAVE_FIGURE_TRAIN_HEAD = './mnist_images_head.jpg'

# MNISTのデータをロードする
(x_train, y_train), (x_valid, y_valid) = mnist.load_data()

# MNISTのデータ形状を確認する
print("x_train.shape:", x_train.shape)
print("y_train.shape:", y_train.shape)
print("x_valid.shape:", x_valid.shape)
print("y_valid.shape:", y_valid.shape)

# MNIST先頭の12個を画像へ出力する
plt.figure(figsize=(5,5))
for k in range(12):
    plt.subplot(3, 4, k+1)
    plt.imshow(x_train[k], cmap='Greys')
    plt.axis('on')
plt.tight_layout()
plt.savefig(SAVE_FIGURE_TRAIN_HEAD)

### ネットワーク向けにデータを加工する
# 2次元の入力画像を1次元に変換する
x_train = x_train.reshape(x_train.shape[0], 784).astype('float32')
x_valid = x_valid.reshape(x_valid.shape[0], 784).astype('float32')
x_train /= 255 # 0~1のfloat型に変換
x_valid /= 255 # 0~1のfloat型に変換
# 出力ラベルをOne-Hotエンコーディングする
n_classes = 10
y_train = keras.utils.to_categorical(y_train, n_classes)
y_valid = keras.utils.to_categorical(y_valid, n_classes)
print("y_train[0]:",y_train[0]) # One-Hotエンコーディングできているか確認

実行結果は、以下のようになります。

$ python3 ./load_mnist.py
# x_train.shape: (60000, 28, 28)
# y_train.shape: (60000,)
# x_valid.shape: (10000, 28, 28)
# y_valid.shape: (10000,)
# y_train[0]: [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]

学習用データのうち、先頭の12個の入力データを matplotlib を使って画像に変換したものが以下の画像です。そして出力データの先頭(0番目)は手書き数字の"5"を示しており、このラベルをOne-Hot encodingしたものが [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.] (5番目のみ1.0で、他の要素は0.0)です。

スクリーンショット 2025-01-10 14.34.23.png

AIモデルをSequential APIにより定義してニューラルネットワークの定義・学習・推論(利用)のフローを体験する

それでは早速AIモデルを定義して、ニューラルネットワークの開発を体験してみましょう。今回の入力データはMNISTデータセットの持つ 28px x 28px のグレースケール(1チャンネル)の画像です。出力データは、入力された画像が0〜9のどれであるかを示す確率を格納した配列です。本プログラムは前処理にて、学習用の入力データを 「28px x 28px のint8型の2次元配列の画像」 から 「784px のfloat32型の1次元配列(0.0〜1.0)」 に変換します。学習用の出力データは前に述べたように One-Hot encoding したものとなります。今回定義するAIモデルは 「784要素を持ったfloat32型の配列を入力として、そのデータが0〜9のどれであるかを確率を出力する(合計1.0となる確率)」 ことを行います。

モデルを定義する

ここでは、精度は求めず、単純にニューラルネットワークの扱い方だけを紹介します。解説用に浅いネットワークを定義しました。784要素を入力として、まず128要素を持った隠れ層を介し、次に64要素を持った隠れ層を介し、最後に10要素からなる確率を出力します。これをTensorflowで記述すると、以下のようになります。 なお、これはコードの断片です。後ほどコード全文を紹介します。

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense

# Sequential APIを使ってモデルを定義する
model = Sequential()
# まず1段目に128要素の隠れ層を挿入する
model.add(Dense(128, activation='sigmoid', input_shape=(784,)))
# 次に2段目に64要素の隠れ層を挿入する
model.add(Dense(64, activation='sigmoid'))
# 最後に確率を出力する出力層を挿入する(合計確率が1.0となるsoftmaxを使う)
model.add(Dense(10, activation='softmax'))
# サマリーを表示する
model.summary()

サマリーを表示すると以下がコンソールに出力されます。ここで調整すべきパラメータの個数と、学習に必要なメモリ量を見ることができます。あまりにも大きい(数百MBを超える)ようであれば、ニューラルネットワークの組み方を再検討すべきでしょう。

スクリーンショット 2025-01-10 14.58.00.png

モデルをコンパイルして学習用データにより学習する

前節で定義したAIモデルをコンパイルして、学習してみます。Tensorflowでは、 定義したモデルをまず compile します。 この時に、モデルと実データの差分を意味する loss の計算方法を定義し、モデルを最適化するための optimizer を選択します(Optimizerには学習率 learning_rate を指定します)。次に、AIモデルの学習結果を可視化できるTensorBoardを定義します。 TensorBoardを使うことにより、学習中もしくは完了後にログのデータを可視化し、どの程度学習が進んでいるかを確認 することができます。最後に fit を呼び出すと学習が実行されます。 学習のパラメータには、学習用データである xtrain, y_train に加えて、何枚の画像を同時に扱うかを batch_size と、データ全体を何回使って学習するかを epochs を指定します。TensorBoardを使う場合は callbacks にTensorBoardのインスタンスを指定してください。これにより、指定したディレクトリにTensorBoard向けの学習結果のログが出力されるようになります。

from tensorflow.keras.optimizers import SGD

# モデルを学習に向けてコンパイルする
# -->> optimizerには SGD (学習率=0.01) を設定
# -->> lossは誤差の二乗の平均を選択('mean_squared_error')
model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=1e-2), metrics=['accuracy'])

# AIの学習を可視化するTensorBoardを使う
from keras.callbacks import TensorBoard
tensorboard = TensorBoard('logs/deep-net')

# AIの学習を実行する
model.fit(x_train, y_train, 
          batch_size = 256, epochs=200,
          verbose=1, validation_data=(x_valid, y_valid),
          callbacks=[tensorboard])

以上の手順により、AIモデルを定義して学習することができます。学習を実行すると下記のようにコンソールに 進捗状況が表示され、学習の進捗を確認することができます。学習中は、学習用データに対するAccuracy(正答率)とロス率、検証用データに対するAccuracyとロス率が表示 されます。

スクリーンショット 2025-01-10 15.33.20.png

学習済みAIモデルを評価し、AIモデルで推論する

学習済みのAIモデルは evaluation により評価することができます。今回はテスト用のデータを準備していないため、検証用のデータ x_valid, y_valid を使って評価します。また、AIモデルの predict により、学習用データの先頭1個目("5"の手書き文字)がどのように推論されるかを試すことができます。学習結果の正答率が高くないことから、本例のAIモデルは正答に辿り着けないと思われます。あくまでも操作手順の紹介として、ご理解ください。 なお、推論も学習と同様に複数枚の画像を同時に処理することが可能です。ただし、本例では1枚のみの入力としています。ここでは np.newaxis により画像が1枚のみ含まれるバッチを定義しています。

import numpy as np

### 推論向けに先頭のデータのみ取り出しておく
test_predict_input = x_train[0]
test_predict_input_expand = test_predict_input[np.newaxis, ...]
print("x_train[0]:", test_predict_input_expand.shape)

# ...

# AIモデルを評価する
test_loss, test_acc = model.evaluate(x_valid, y_valid, verbose=0)
print("test_loss:", str(test_loss))
print("test_acc:", str(test_acc))

# 1個目の学習用データで推論してみる "5" となるはず
predictions = model.predict(test_predict_input_expand)
print("predictions[0]:", predictions[0])

TensorBoardにより学習結果を可視化する

学習が完了した後は、ターミナルから以下のコマンドを実行することで、TensorBoardにより学習結果をブラウザ上で確認することができます。ここではAccuracy(正答率)が28%程度で上げ止まっている様子を見ることができます。28%のAccuracyなので、期待する出力はほぼ得られないものと言って良いでしょう。 画像分類のAccuracyを向上させる手法である畳み込み層(Conv2D)の活用方法とSkip-Connection(スキップ接続)の構築方法については、後半記事にて解説します。

# TensorBoardによる可視化
# -->> プログラム中で指定したディレクトリ(logs/deep-net)にある学習結果を利用する
# -->> ブラウザからはポート6006でアクセスできる
$ tensorboard --logdir='logs/deep-net' --port 6006

スクリーンショット 2025-01-10 15.27.30.png

画像分類のAccuracyを向上させる、畳み込み層(Conv2D)の活用方法とSkip-Connection(スキップ接続)の活用方法は以下の後半記事をご参照ください。


Tensorflowでは、以上の手順によりAIモデルを定義し、学習、利用することができます。


Sequential APIを使った場合のコード全文

ここまでのコード全文は以下の通りです。
後半記事では、またゼロからコードを書いていきます。
※ 基本的に model の定義部分が置き換わります。

import tensorflow.keras as keras
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.optimizers import SGD
from matplotlib import pyplot as plt
import numpy as np

# MNISTの先頭を出力する画像ファイルへのパス
SAVE_FIGURE_TRAIN_HEAD = './mnist_images_head.jpg'

# MNISTのデータをロードする
(x_train, y_train), (x_valid, y_valid) = mnist.load_data()

# MNISTのデータ形状を確認する
print("x_train.shape:", x_train.shape)
print("y_train.shape:", y_train.shape)
print("x_valid.shape:", x_valid.shape)
print("y_valid.shape:", y_valid.shape)

# MNIST先頭の12個を画像へ出力する
plt.figure(figsize=(5,5))
for k in range(12):
    plt.subplot(3, 4, k+1)
    plt.imshow(x_train[k], cmap='Greys')
    plt.axis('on')
plt.tight_layout()
plt.savefig(SAVE_FIGURE_TRAIN_HEAD)

### ネットワーク向けにデータを加工する
# 2次元の入力画像を1次元に変換する
x_train = x_train.reshape(x_train.shape[0], 784).astype('float32')
x_valid = x_valid.reshape(x_valid.shape[0], 784).astype('float32')
x_train /= 255 # 0~1のfloat型に変換
x_valid /= 255 # 0~1のfloat型に変換
# 出力ラベルをOne-Hotエンコーディングする
n_classes = 10
y_train = keras.utils.to_categorical(y_train, n_classes)
y_valid = keras.utils.to_categorical(y_valid, n_classes)
print("y_train[0]:",y_train[0]) # One-Hotエンコーディングできているか確認

### 推論向けに先頭のデータのみ取り出しておく
test_predict_input = x_train[0]
test_predict_input_expand = test_predict_input[np.newaxis, ...]
print("x_train[0]:", test_predict_input_expand.shape)

# Sequential APIを使ってモデルを定義する
model = Sequential()
# まず1段目に128要素の隠れ層を挿入する
model.add(Dense(128, activation='sigmoid', input_shape=(784,)))
# 次に2段目に64要素の隠れ層を挿入する
model.add(Dense(64, activation='sigmoid'))
# 最後に確率を出力する出力層を挿入する(合計確率が1.0となるsoftmaxを使う)
model.add(Dense(10, activation='softmax'))
# サマリーを表示する
model.summary()

# モデルを学習に向けてコンパイルする
# -->> optimizerには SGD (学習率=0.01) を設定
# -->> lossは誤差の二乗の平均を選択('mean_squared_error')
model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=1e-2), metrics=['accuracy'])

# AIの学習を可視化するTensorBoardを使う
from keras.callbacks import TensorBoard
tensorboard = TensorBoard('logs/deep-net')

# AIの学習を実行する
model.fit(x_train, y_train, 
          batch_size = 256, epochs=200,
          verbose=1, validation_data=(x_valid, y_valid),
          callbacks=[tensorboard])

# AIモデルを評価する
test_loss, test_acc = model.evaluate(x_valid, y_valid, verbose=0)
print("test_loss:", str(test_loss))
print("test_acc:", str(test_acc))

# 1個目の学習用データで推論してみる "5" となるはず
predictions = model.predict(test_predict_input_expand)
print("predictions[0]:", predictions[0])

print("quit()")
quit()

ここまでで基本的なTensorflowの使い方は理解できたでしょうか。
後編ではいよいよ、高い精度で画像を認識をできる畳み込み層を持ったAIモデルや
Functional APIによるAIモデルの定義方法、そしてTensorflow Liteによる
AIモデルの実行方法について触れていきます。


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?