研究室プレゼミ生育成用_[Dockerを使え]

# 皆さん、Dockerを使って開発・研究しましょう

まず初めにDockerの概要についてサクッと述べると、仮想化技術の一種です。
VM等のハイパーバイザーと決定的に異なる点として、BIOSレベルでエミュレートしていない。OSのコアとなるカーネル空間のみ共有しており、ユーザー空間は"コンテナ"として独立して保つことが出来ます。

つまり、コンテナ内は環境が汚し放題なので、開発や研究等に用いるのにもってこいなわけです。

本記事より良い解説をお求めの方は、ひとまず公式ドキュメントを見よう。
https://docs.docker.jp/get-started/overview.html


## Dockerの概念は集合住宅のトイレで例えると分かりやすい 
上下水道が1つずつ整備された集合住宅が一つあると仮定します。
ここで言う上下水道が、上で述べた「カーネル空間」に該当します。そして、それを利用しているトイレがユーザー空間に該当します。

### Dockerが無い場合
Dockerが無い環境はトイレ共有型の集合住宅です。1つのトイレを住人全員で共有して使用します。1つしかトイレが無いので様々な問題が発生します。

誰かがトイレで用を足すのに失敗して、ぐちゃぐちゃになったとします。つまり「環境が汚染」されたと...。糞まみれになったトイレを片すの、大変ですよね？
他にも「ワシは和式しか使わない」という困った人がトイレを勝手に和式に改修したとします。切れ痔の住民は、悶絶しながら用を足すことになるでしょう。

### Dockerが有る場合
Dockerがある環境というのは、トイレが各部屋にある集合住宅です。住人各々が自由に使えるトイレがあります。
そこではトイレをぐちゃぐちゃに使用しようが、勝手に和式に改修しようが、自由です。トイレは貴方の管理下にありますので。

つまり、複数人でトイレ(ユーザー環境)を共有すると、他の住人と環境的な問題が発生するわけです。しかし、各部屋にトイレを持つ事で、それぞれで自由な環境を用意することが出来る。これがDockerを使用するメリットです。

# Dockerの簡単な演習
今回プレゼミ生に皆さんには、各々独自にコンテナを立てて貰います。
既にvscodeで**Dev Containers**の拡張機能をインストールしている前提で話を進めます。
### グループのチェック
まず初めに、自身のユーザーがDockerグループに所属しているかを確認します。これが無いとDocker関連のサービスに手出しできません。
サーバーにvscodeでssh接続して、以下のコマンドを入力して下さい。

```bash
id
#uid=100X(gp00a000) gid=100X(gp00a000) groups=100X(gp00a000),997(docker)
```
以下のような結果が得られると思います。出力に **997(docker)** があるかを確認しましょう。ない人はこの時点で手を挙げて報告するように。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/d534359b-fa46-2736-4687-dadbddf4554e.png)

## docker-composeを利用する。
基本的には、dockerfileをイメージ(テンプレート)にビルドして、それをコンテナとして構築(テンプレの実体化)するのが基本的なフローになります。

![無題の図形描画 (22).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/2b545fb2-9d74-e997-eec8-b01da671ea52.png)



docker-composeは、複数のdockerfileを連携して動かす管理ツールです。dockerfileを構築する際の引数等の設定ファイルとして書いて置けるので便利です。今回はこいつを使って演習を進めます。

![無題の図形描画 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/aea3d56a-8673-d4a4-0bf4-ca0c25d0d39e.png)


### ステップ① [ディレクトリを作る]

以下の様に今回使用する作業ディレクトリ(フォルダ)をmkdirコマンド等を駆使して作成しましょう。


* ```minimized_docker-set```ディレクトリが全ての親となる
* ```dockerfiles```ディレクトリは各dockerfileを配置する
* ```workspace```ディレクトリがコンテナ内で実際に動かすファイルを配置する事を想定

```bash
.root (minimized_docker-set)
├── dockerfiles
│   ├── sample_service_1 
│   └── sample_service_2 
└── workspace 
    ├── sample_service_1 
    └── sample_service_2 
```

### ステップ② [dockerfileを書く]


```minimized_docker-set/dockerfiles/sample_service_1```にdockerfileの名称で**ファイル**を作成しましょう。

```bash
.root (minimized_docker-set)
├── dockerfiles 
│   ├── sample_service_1 
│   │   └── dockerfile          <----ここに作成
│   └── sample_service_2 
└── workspace 
    ├── sample_service_1 
    └── sample_service_2 
```

次に、dockerfileの中身を以下の様に書きましょう。
```dockerfile
#ベースイメージの指定(今回はubuntu:24.04)
FROM ubuntu:24.04

#-----------------基本設定-----------------
#docker-composeから環境変数を受け取る
ARG http_tmp 
ARG https_tmp

#コンテナに環境変数の設定を追加
ENV http_proxy=$http_tmp
ENV https_proxy=$https_tmp

#タイムゾーンを東京に設定
ENV TZ=Asia/Tokyo
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

#パッケージリストの更新
RUN apt update -y && apt upgrade -y


#-----------------必要に応じてパッケージ追加-----------------

#例えばpython関連のパッケージをインストールする場合は以下の通り
RUN apt install -y python3 python3-pip

#nanoやvimを入れる場合は次の様に
#RUN apt install -y nano vim
```

### ステップ② [docker-compose.ymlを書く]
親ディレクトリ```minimized_docker-set```の直下に```docker-compose.yml```ファイルを作成しよう。

```bash
.root (minimized_docker-set )
├── docker-compose.yml         <----ここに作成
├── dockerfiles 
│   ├── sample_service_1 
│   │   └── dockerfile     
│   └── sample_service_2 
└── workspace 
    ├── sample_service_1 
    └── sample_service_2 
```
```docker-compose.yml```の中身は以下の様に記述しよう
その際、必ず「要書き換えポイント」を自分の学籍番号に書き替えよう。
ここで書き換えないと、後で面倒になるかも。

```dockerfile
services:

#----------------- sample service 1 -----------------

# --------------- 要書き換えポイント ---------------
# 「gp00a000」 を自身の学生番号に書き換え
  sample_gp00a000_1: #ここを書き換える
# -------------------------------------------------
    # ビルド設定
    build:
      context: ./dockerfiles/sample_service_1
      # ビルドする際に使用するDockerfileを指定
      dockerfile: dockerfile
      #.envファイルから読み込んだ環境変数をdockerfileで使用する
      args:
        - http_tmp=$http_proxy
        - https_tmp=$https_proxy
    
    # --------------- 要書き換えポイント ---------------
    # コンテナの名前を指定 (「gp00a000」を自身の学生番号に書き換え)
    container_name: sample_gp00a000_1 #ここを書き換える
    # -------------------------------------------------

    # 環境変数ファイルを読込む
    env_file:
      - .env

    # ボリュームをマウント
    volumes:
      - ./workspace/sample_service_1:/sample_service_1
    # マウントしたボリュームをワーキングディレクトリに設定
    working_dir: /sample_service_1

    # コンテナを永続化
    command: tail -f /dev/null
  
    # ネットワークを指定
    networks:
      - custom_network_name # ここにネットワーク名を指定


# --------------- 要書き換えポイント ---------------
# ネットワークの名前を指定 (「gp00a000」を自身の学生番号に書き換え)
networks:
  custom_network_name:
    name: sample_gp00a000 #ここを書き換える
# -------------------------------------------------
```

### ステップ③ [環境変数ファイルを作成]
コンテナ内で使用する環境変数をここで指定します。
今回は、学内でサーバーを扱う上で必須となるプロキシを環境変数として書いておきます。

環境変数とは、アプリケーションが使用する設定値や情報の事です。予めファイルで書いておくと、一々指定せずに楽できます。

親ディレクトリ```minimized_docker-set```の直下に```.env```ファイルを作成しよう。

```bash
.root (minimized_docker-set )
├── .env　　　　　　　　　　　　<----ここに作成
├── docker-compose.yml         
├── dockerfiles 
│   ├── sample_service_1 
│   │   └── dockerfile     
│   └── sample_service_2 
└── workspace 
    ├── sample_service_1 
    └── sample_service_2 
```
```.env```ファイルの中身は以下の様に書こう。

```bash
http_proxy=http://wwwproxy.osakac.ac.jp:8080
https_proxy=http://wwwproxy.osakac.ac.jp:8080
```
### ステップ④ [docker-composeを使用してコンテナを立ち上げる]
ここまで来たら、実際にコンテナを立ち上げてみましょう。
の前に、最後にディレクトリ構成が以下の様になっているかを想定されている構成になっているかチェックしてみよう。

```bash
.root (minimized_docker-set )
├── .env 
├── docker-compose.yml 
├── dockerfiles 
│   ├── sample_service_1 
│   │   └── dockerfile 
│   └── sample_service_2 
└── workspace 
    ├── sample_service_1 
    └── sample_service_2 
```

以下のコマンドを叩くと、ツリーとしてディレクトリ構成が確認できるので便利。
```bash
tree -a

#------以下の様な出力が得られればヨシ----
.
├── .env
├── docker-compose.yml
├── dockerfiles
│   ├── sample_service_1
│   │   └── dockerfile
│   └── sample_service_2
└── workspace
    ├── sample_service_1
    └── sample_service_2
```

確認出来たら、以下のコマンドを用いてコンテナを立ち上げてみよう
```bash
docker compose up -d
```
以下の様になるはず。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/939e833a-9cf8-2aeb-f4f4-82562aec9aca.png)


しばらく待機し、以下のような出力が得られれば、ひとまずは成功
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/64945ee0-73b8-099c-31e4-a7708554a044.png)

次に、以下のコマンド(起動中のコンテナを出力する)を実行してコンテナが立ち上がっているかを確認しよう。
```bash
docker ps | grep sample_gp00a000_1 #「gp00a000」はdocker-composeで指定した学籍番号に書き換えよう
```
出力に自身が設定した学籍番号のコンテナが存在していれば、ひとまずはヨシ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/9808ed5a-9a45-d067-bf33-990f6bf12156.png)

### ステップ⑤ [簡単に使ってみる]
dockerfileに標準でpythonパッケージをインストールする設定が書いてあったはず。よってコンテナ内でpythonが利用出来るはず。なので適当にコンテナ内で使用するファイルを```minimized_docker-set/workspace/sample_service_1```の配下に作成しよう。

なお、```minimized_docker-set/workspace/sample_service_1```はコンテナ内部に通じるシンボリック(ショートカット)となっているので、ここに何かしらファイルが作成されるとコンテナ内部でも反映される。

自身で必要なパッケージをインスコ出来るのであれば、お好きなファイルを書いて実行してみるといい。

今回は試しにモンテカルロ法で円周率を計算するプログラムを書いてみる。

```python:sample.py
import random

def calculate_pi(num_points):
    inside_circle = 0

    for _ in range(num_points):
        x, y = random.uniform(0, 1), random.uniform(0, 1)
        if x**2 + y**2 <= 1:
            inside_circle += 1

    return (inside_circle / num_points) * 4

if __name__ == "__main__":
    num_points = int(input("シミュレーション回数を入力: "))
    pi_estimate = calculate_pi(num_points)
    print(f"π: {pi_estimate}")
```

配置場所は以下
```bash
.root (minimized_docker-set )
├── .env 
├── docker-compose.yml 
├── dockerfiles 
│   ├── sample_service_1 
│   │   └── dockerfile 
│   └── sample_service_2 
└── workspace 
    ├── sample_service_1  <----プログラムを配置するディレクトリ
    │   └── sample.py     <----今回試しに書いたプログラム
    └── sample_service_2 
```

次に、コンテナ内部にアタッチしてプログラムを実行してみよう。
アタッチは、以下のコマンドを実行。
```bash
docker exec -it sample_gp00a000_1 bash #「gp00a000」はdocker-composeで指定した学籍番号に書き換えよう
```

先程書いたプログラムが存在するか```ls```コマンドで確認し、```python3 sample.py```コマンドで実行してみよう。
以下は実行結果のサンプル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/af754d2e-c05d-527c-39f3-3e7f250fe078.png)




### ステップ⑥ [GPUを利用出来るコンテナの作成]
先程作成したコンテナとは別にGPUを利用出来るコンテナを設定する。


```minimized_docker-set/dockerfiles/sample_service_2/```へ新たにdockerfileを作成し、```docker-compose.yml```に追加したdockerfileの設定を書く。



```bash
.root (minimized_docker-set)
├── .env 
├── docker-compose.yml     <----追加した内容に関する設定を追加
├── dockerfiles 
│   ├── sample_service_1 
│   │   └── dockerfile 
│   └── sample_service_2 
│       └── dockerfile     <----新たに追加
└── workspace 
    ├── sample_service_1 
    │        └── sample.py
    └── sample_service_2 
```

新しく追加するdockerfileは以下の様に。

先程はubuntuのプレーンイメージ(何も改良されていないイメージ)を```FROM```で指定したが、今回はGPU(CUDA)をコンテナ内で使用する事を目的とする為、予めCUDAランタイムが利用出来るベースイメージを指定する。

```dockerfile
#ベースイメージの指定(CUDAランタイムが使用出来るベースコンテナを指名する)
FROM nvidia/cuda:12.0.1-cudnn8-devel-ubuntu22.04

#-----------------基本設定-----------------
#docker-composeから環境変数を受け取る
ARG http_tmp 
ARG https_tmp

#コンテナに環境変数の設定を追加
ENV http_proxy=$http_tmp
ENV https_proxy=$https_tmp

#タイムゾーンを東京に設定
ENV TZ=Asia/Tokyo
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

#パッケージリストの更新
RUN apt update -y && apt upgrade -y


#-----------------必要に応じてパッケージ追加-----------------

#例えばpython関連のパッケージをインストールする場合は以下の通り
RUN apt install -y python3 python3-pip

#nanoやvimを入れる場合は次の様に
#RUN apt install -y nano vim
```

次に、```docker-compose.yml```を新たに**sample_gp00a000_2 ( gp00a000の箇所は自身の学籍番号)**のコンテナを立ち上げる記述を追記する。


```bash
services:

#----------------- sample service 1 -----------------
# --------------- 要書き換えポイント ---------------
# 「gp00a000」 を自身の学生番号に書き換え
  sample_gp00a000_1:
# -------------------------------------------------
    # ビルド設定
    build:
      context: ./dockerfiles/sample_service_1
      # ビルドする際に使用するDockerfileを指定
      dockerfile: dockerfile
      #.envファイルから読み込んだ環境変数をdockerfileで使用する
      args:
        - http_tmp=$http_proxy
        - https_tmp=$https_proxy
    
    # --------------- 要書き換えポイント ---------------
    # コンテナの名前を指定 (「gp00a000」を自身の学生番号に書き換え)
    container_name: sample_gp00a000_1
    # -------------------------------------------------

    # 環境変数ファイルを読込む
    env_file:
      - .env

    # ボリュームをマウント
    volumes:
      - ./workspace/sample_service_1:/sample_service_1
    # マウントしたボリュームをワーキングディレクトリに設定
    working_dir: /sample_service_1

    # コンテナを永続化
    command: tail -f /dev/null
  
    # ネットワークを指定
    networks:
      - custom_network_name # ここにネットワーク名を指定



#----------------- sample service 2 -----------------
# --------------- 要書き換えポイント ---------------
# 「gp00a000」 を自身の学生番号に書き換え
  sample_gp00a000_2:
# -------------------------------------------------
    # ビルド設定
    build:
      context: ./dockerfiles/sample_service_2
      # ビルドする際に使用するDockerfileを指定
      dockerfile: dockerfile
      #.envファイルから読み込んだ環境変数をdockerfileで使用する
      args:
        - http_tmp=$http_proxy
        - https_tmp=$https_proxy
    
    # --------------- 要書き換えポイント ---------------
    # コンテナの名前を指定 (「gp00a000」を自身の学生番号に書き換え)
    container_name: sample_gp00a000_2
    # -------------------------------------------------

    # 環境変数ファイルを読込む
    env_file:
      - .env

    # ボリュームをマウント
    volumes:
      - ./workspace/sample_service_2:/sample_service_2
    # マウントしたボリュームをワーキングディレクトリに設定
    working_dir: /sample_service_2

    # コンテナを永続化
    command: tail -f /dev/null

    #GPUを使用するための設定
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]

    #GPUを使用する際のcore dumpを防ぐ為の設定
    shm_size: 12GB

    # ネットワークを指定
    networks:
      - custom_network_name # ここに使用するバーチャルネットワーク名を指定


# --------------- 要書き換えポイント ---------------
# バーチャルネットワークの名前を指定 (「gp00a000」を自身の学生番号に書き換え)
networks:
  custom_network_name:
    name: sample_gp00a000
# -------------------------------------------------
```

上記の```docker-compose.yml```が書けたら、実際にコンテナを立ち上げてみよう。
先程と同様に、```docker-compose.yml```があるディレクトリに移動して、以下のコマンドを実行しよう。
```bash
docker compose up -d
```
実行結果として、以下の様に先程指定したコンテナ名に関する出力が```Started```となっていれば、コンテナは立ち上がっている筈である。

![コンテナ2に関する出力.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/cc5648e9-3b90-c78f-a6bf-8a39766ddde2.png)


確認として、```docker ps | grep sample_gp00a000_2```コマンドを実行して、起動中のコンテナを確認するとよい。

上記のコマンドで問題なくコンテナの存在が確認出来れば、実際にアタッチしてGPUに関するステータスを確認して、GPUが利用出来る事の動作確認をしてみよう。

まず、先ほどと同様にコンテナにアタッチする。

```bash
docker exec -it sample_gp00a000_2 bash
```

次に、以下のコマンドを実行しよう

```bash
nvidia-smi
```

結果として、以下の様な```NVIDIA-SMI ...```と言った出力が得られれば、ひとまず成功である。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/36225647-398d-e147-818e-4f8c44856ff6.png)

### ステップ⑦ [pytorchでGPUを利用する]
それでは、GPUを利用するプログラムを```pytorch```で書いて動作させてみよう。
まず初めに、本セクションで利用するpythonパッケージを以下のコマンドでインストールしよう。
```bash
pip install matplotlib torch torchvision torchaudio scikit-learn
```

次に```pytorch```でGPUが利用可能であるかを確認する。```gpu_check.py```というファイルを```minimized_docker-set/workspace/sample_service_2/gpu_check.py```として保存しよう。

```bash
.root (minimized_docker-set)
├── .env 
├── docker-compose.yml
├── dockerfiles 
│   ├── sample_service_1 
│   │   └── dockerfile 
│   └── sample_service_2 
│       └── dockerfile
└── workspace 
    ├── sample_service_1 
    │        └── sample.py
    └── sample_service_2 
             └── gpu_check.py     <----新たに追加
```

```python:gpu_check.py
import torch

# GPUが利用可能かチェック
if torch.cuda.is_available():
    print(f"利用可能なGPU数: {torch.cuda.device_count()}")
    for i in range(torch.cuda.device_count()):
        print(f"GPU {i}: {torch.cuda.get_device_name(i)}")
else:
    print("CUDA対応GPUが利用できません。")
```
作成出来たら、実行しよう。
```bash
python3 gpu_check.py
```

実行結果として、以下の様に```nvidia-smi```と同様の出力が得られたら、ヨシ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/671fd918-cbe9-0843-87b9-71298c1c24b8.png)

では早速、pytorchでgpuを利用するプログラムを書いて実行してみる。

プレゼミで動作させたプログラムでも良いが、今回はサンプルとしてMNISTデータセットの分類問題をNNで訓練するプログラムを以下に用意した。これをコピペして```minimized_docker-set/workspace/sample_service_2/sample_nn.py```として作成する。

```bash
.root (minimized_docker-set)
├── .env 
├── docker-compose.yml
├── dockerfiles 
│   ├── sample_service_1 
│   │   └── dockerfile 
│   └── sample_service_2 
│       └── dockerfile
└── workspace 
    ├── sample_service_1 
    │        └── sample.py
    └── sample_service_2 
             ├── gpu_check.py
             └── sample_nn.py        <----新たに追加
```

```python:sample_nn.py

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
from sklearn.metrics import precision_score, recall_score, f1_score
import matplotlib.pyplot as plt

# デバイス設定
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# データセットとデータローダー
transform = transforms.ToTensor()
train_dataset = datasets.MNIST(root="./data", train=True, transform=transform, download=True)
test_dataset = datasets.MNIST(root="./data", train=False, transform=transform, download=True)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

# ニューラルネットワークの定義
class NeuralNet(nn.Module):
    def __init__(self):
        super(NeuralNet, self).__init__()
        self.fc1 = nn.Linear(28 * 28, 128)
        self.fc2 = nn.Linear(128, 64)
        self.fc3 = nn.Linear(64, 10)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = x.view(-1, 28 * 28)  # Flatten
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        x = self.fc3(x)
        return x

# モデル、損失関数、最適化手法の設定
model = NeuralNet().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 訓練ループ
epochs = 5
train_losses = []  # 各エポックの損失を記録
val_losses = []

for epoch in range(epochs):
    # 訓練ループ
    model.train()
    running_loss = 0
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)

        # 順伝播
        outputs = model(images)
        loss = criterion(outputs, labels)

        # 逆伝播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        running_loss += loss.item()

    avg_train_loss = running_loss / len(train_loader)
    train_losses.append(avg_train_loss)

    # バリデーションループ
    model.eval()
    val_running_loss = 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            loss = criterion(outputs, labels)
            val_running_loss += loss.item()

    avg_val_loss = val_running_loss / len(test_loader)
    val_losses.append(avg_val_loss)

    print(f"Epoch [{epoch + 1}/{epochs}], Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}")

# 学習曲線をプロット
plt.plot(range(1, epochs + 1), train_losses, marker='o', label="Train Loss")
plt.plot(range(1, epochs + 1), val_losses, marker='s', label="Validation Loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.title("Training and Validation Loss Curve")
plt.legend()
plt.grid()
plt.savefig("training_and_validation_loss_curve.png")

# テスト: 精度、リコール、F1スコアの計算
model.eval()
correct = 0
total = 0
all_preds = []
all_labels = []

with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

        # ラベルと予測結果を保存
        all_preds.extend(predicted.cpu().numpy())
        all_labels.extend(labels.cpu().numpy())

# 精度の計算
precision = precision_score(all_labels, all_preds, average="macro")
recall = recall_score(all_labels, all_preds, average="macro")
f1 = f1_score(all_labels, all_preds, average="macro")

print(f"Test Accuracy: {correct / total:.4f}")
print(f"Precision: {precision:.4f}")
print(f"Recall: {recall:.4f}")
print(f"F1 Score: {f1:.4f}")

# 推論結果の一部を可視化
plt.clf()
with torch.no_grad():
    images, labels = next(iter(test_loader))  # 最初のバッチを取得
    images, labels = images.to(device), labels.to(device)
    outputs = model(images)
    _, predicted = torch.max(outputs, 1)

    # 画像と予測ラベルをプロット
    fig, axes = plt.subplots(5, 5, figsize=(15, 15))  # 5行5列のプロット
    for i in range(25):  # 最初の25枚を描画
        row, col = divmod(i, 5)  # 行と列を計算
        img = images[i].cpu().squeeze()  # 画像を取得
        axes[row, col].imshow(img, cmap="gray")  # グレースケールで表示
        axes[row, col].set_title(f"Label: {labels[i].item()}, Pred: {predicted[i].item()}")
        axes[row, col].axis("off")  # 軸を非表示

    plt.tight_layout()
    plt.savefig("predicted_images.png")  # 画像を保存

```

コピペ出来たら、毎度の如く実行する。

```bash
python3 sample_nn.py
```
実行結果(学習進行中)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/dfcf61d5-d4fa-68a2-a36e-f94a4c5603cc.png)

実行結果(学習曲線)
![training_and_validation_loss_curve.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574923/b8d15f04-dda9-61ab-059e-fcd48b7359a1.png)

推論結果の一部抜粋