CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: isort で同じパッケージのインポートを 1 行ずつに分割する

isort は、Python のコードフォーマッタのひとつ。 使うことで、インポート文を Python の標準的なコーディング規約である PEP8 に沿った形で整形できる。

pycqa.github.io

ところで、PEP8 では同じパッケージのオブジェクトをインポートする際には 1 行にまとめても構わないとしている。

peps.python.org

ただ、これは完全に好みの問題だけど自分は同じパッケージのインポートであってもオブジェクトごとに 1 行ずつ分割したい。 1 行に 1 つのオブジェクトしか書かなければ、その行で何をインポートしているかが明確になって読みやすいと感じるため。

そこで、今回は isort で 1 行に 1 つのオブジェクトをインポートする方法について書く。 結論から先に述べると、コマンドラインオプションであれば --sl または --force-single-line-imports を付ければ良い。 また、設定ファイルを使って指定することもできる。

使った環境は次のとおり。

$ sw_vers                    
ProductName:        macOS
ProductVersion:     14.6.1
BuildVersion:       23G93
$ python -V        
Python 3.11.9

もくじ

下準備

下準備として isort をインストールする。

$ pip install isort

デフォルトの動作について

サンプルコードを用意する。 urllib.parse パッケージから parse_qs()urlparse() 関数の 2 つをインポートしている。

$ cat << 'EOF' > example.py
from urllib.parse import parse_qs
from urllib.parse import urlparse
EOF

上記のファイルに isort を実行する。

$ isort example.py

すると、同じパッケージからのインポートなので 1 行にまとめられる。

$ cat example.py           
from urllib.parse import parse_qs, urlparse

コマンドラインオプションで指定する

続いては、今回の本題である同じパッケージからのインポートであってもオブジェクトごとに 1 行ずつ分割する。 これには、コマンドラインオプションであれば --sl または --force-single-line-imports を指定すれば良い。

$ isort --sl example.py

実行すると、以下のように 1 行ずつに分割した形でインポートされる。

$ cat example.py 
from urllib.parse import parse_qs
from urllib.parse import urlparse

設定ファイルで指定する

とはいえ、毎回コマンドラインオプションを指定するのも大変なので設定ファイルにしたい。

pyproject.toml ファイルを使う場合

まず、最近の Python のプロジェクトであれば pyproject.toml を用意するのが一般的なはず。 ここで指定する場合には、次のような設定を追加する。

$ cat << 'EOF' >> pyproject.toml
[tool.isort]
force_single_line = true
EOF

あとは、設定ファイルのある場所で isort コマンドを実行するだけ。

.isort.cfg ファイルを使う場合

pyproject.toml を使わない場合には、専用の設定ファイルとして .isort.cfg を用意すれば良い。 こちらを使う場合には、次のような設定を使う。

$ cat << 'EOF' > .isort.cfg
[settings]
force_single_line=True
EOF

いじょう。

Python: Polars の shrink_dtype で DataFrame の使用メモリを削減する

Kaggle などのデータ分析コンペで使われるテクニックのひとつに reduce_mem_usage() 関数がある。 これは、一般に pandas の DataFrame のメモリ使用量を削減するために用いられる。 具体的には、カラムに出現する値を調べて、それを表現する上で必要最低限な型にキャストする。 たとえば、64 ビット整数のカラムを 32 ビット整数にできれば、理屈の上では必要なメモリ使用量がおよそ半分になる。

ただし、この関数は pandas が組み込みで提供しているわけではない。 そのため、各々がスニペットを秘伝のタレのように持っているか、あるいは必要に応じてウェブ上から探して利用する場合が多いはず。

一方で、Polars にはこれに相当する機能が組み込みで用意されている。 具体的には polars.Expr#shrink_dtype() という Expr オブジェクトを返すメソッドがある。 あまり知られていないようなので、今回は紹介してみる。

docs.pola.rs

使った環境は次のとおり。

$ sw_vers
ProductName:        macOS
ProductVersion:     14.6
BuildVersion:       23G80
$ python -V                      
Python 3.11.9
$ pip list | grep polars
polars        1.4.0

もくじ

下準備

まずは Polars をインストールしておく。

$ pip install polars

適当なデータセットとして Diamonds データセットをダウンロードする。

$ wget https://raw.githubusercontent.com/mwaskom/seaborn-data/master/diamonds.csv

Python のインタプリタを起動する。

$ python

CSV ファイルを読み込んで DataFrame オブジェクトを作る。 そのままだとサイズが分かりにくいので 200 個ほど連結してサイズをかさ増しする。

>>> import polars as pl
>>> raw_df = pl.read_csv("diamonds.csv")
>>> df = pl.concat([raw_df for _ in range(200)])
>>> df
shape: (10_788_000, 10)
┌───────┬───────────┬───────┬─────────┬───┬───────┬──────┬──────┬──────┐
│ carat ┆ cut       ┆ color ┆ clarity ┆ … ┆ price ┆ x    ┆ y    ┆ z    │
│ ------------     ┆   ┆ ------------  │
│ f64   ┆ str       ┆ str   ┆ str     ┆   ┆ i64   ┆ f64  ┆ f64  ┆ f64  │
╞═══════╪═══════════╪═══════╪═════════╪═══╪═══════╪══════╪══════╪══════╡
│ 0.23  ┆ Ideal     ┆ E     ┆ SI2     ┆ … ┆ 3263.953.982.43 │
│ 0.21  ┆ Premium   ┆ E     ┆ SI1     ┆ … ┆ 3263.893.842.31 │
│ 0.23  ┆ Good      ┆ E     ┆ VS1     ┆ … ┆ 3274.054.072.31 │
│ 0.29  ┆ Premium   ┆ I     ┆ VS2     ┆ … ┆ 3344.24.232.63 │
│ 0.31  ┆ Good      ┆ J     ┆ SI2     ┆ … ┆ 3354.344.352.75 │
│ …     ┆ …         ┆ …     ┆ …       ┆ … ┆ …     ┆ …    ┆ …    ┆ …    │
│ 0.72  ┆ Ideal     ┆ D     ┆ SI1     ┆ … ┆ 27575.755.763.5  │
│ 0.72  ┆ Good      ┆ D     ┆ SI1     ┆ … ┆ 27575.695.753.61 │
│ 0.7   ┆ Very Good ┆ D     ┆ SI1     ┆ … ┆ 27575.665.683.56 │
│ 0.86  ┆ Premium   ┆ H     ┆ SI2     ┆ … ┆ 27576.156.123.74 │
│ 0.75  ┆ Ideal     ┆ D     ┆ SI2     ┆ … ┆ 27575.835.873.64 │
└───────┴───────────┴───────┴─────────┴───┴───────┴──────┴──────┴──────┘

特に指定しない場合、デフォルトでは数値のカラムが 64 ビットの型になる。

>>> df.dtypes
[Float64, String, String, String, Float64, Float64, Int64, Float64, Float64, Float64]

この状況では DataFrame のサイズは約 683MB だった。

>>> df.estimated_size(unit="mb")
683.1520080566406

使用メモリを削減する

それでは、実際に polars.Expr.shrink_dtype() を使って DataFrame の使用メモリを減らしてみよう。 すべてのカラムを処理の対象とする場合には pl.all().shrink_dtype() とすれば良い。 あとは得られた Expr オブジェクトを DataFrame#select() に渡せば DataFrame が変換される。

>>> shrinked_df = df.select(pl.all().shrink_dtype())
>>> shrinked_df
shape: (10_788_000, 10)
┌───────┬───────────┬───────┬─────────┬───┬───────┬──────┬──────┬──────┐
│ carat ┆ cut       ┆ color ┆ clarity ┆ … ┆ price ┆ x    ┆ y    ┆ z    │
│ ------------     ┆   ┆ ------------  │
│ f32   ┆ str       ┆ str   ┆ str     ┆   ┆ i16   ┆ f32  ┆ f32  ┆ f32  │
╞═══════╪═══════════╪═══════╪═════════╪═══╪═══════╪══════╪══════╪══════╡
│ 0.23  ┆ Ideal     ┆ E     ┆ SI2     ┆ … ┆ 3263.953.982.43 │
│ 0.21  ┆ Premium   ┆ E     ┆ SI1     ┆ … ┆ 3263.893.842.31 │
│ 0.23  ┆ Good      ┆ E     ┆ VS1     ┆ … ┆ 3274.054.072.31 │
│ 0.29  ┆ Premium   ┆ I     ┆ VS2     ┆ … ┆ 3344.24.232.63 │
│ 0.31  ┆ Good      ┆ J     ┆ SI2     ┆ … ┆ 3354.344.352.75 │
│ …     ┆ …         ┆ …     ┆ …       ┆ … ┆ …     ┆ …    ┆ …    ┆ …    │
│ 0.72  ┆ Ideal     ┆ D     ┆ SI1     ┆ … ┆ 27575.755.763.5  │
│ 0.72  ┆ Good      ┆ D     ┆ SI1     ┆ … ┆ 27575.695.753.61 │
│ 0.7   ┆ Very Good ┆ D     ┆ SI1     ┆ … ┆ 27575.665.683.56 │
│ 0.86  ┆ Premium   ┆ H     ┆ SI2     ┆ … ┆ 27576.156.123.74 │
│ 0.75  ┆ Ideal     ┆ D     ┆ SI2     ┆ … ┆ 27575.835.873.64 │
└───────┴───────────┴───────┴─────────┴───┴───────┴──────┴──────┴──────┘

上記を見ても分かるとおり、行数や列数などは何も変わっていない。 ただし、カラムの型は必要最低限なものにキャストされている。

>>> shrinked_df.dtypes
[Float32, String, String, String, Float32, Float32, Int16, Float32, Float32, Float32]

処理後の DataFrame は、使用メモリが約 374MB まで減っている。

>>> shrinked_df.estimated_size(unit="mb")
374.5048522949219

いじょう。

まとめ

  • Polars には reduce_mem_usage() 関数に相当する機能が組み込みで用意されている
  • polars.Expr.shrink_dtype() という Expr オブジェクトを返す関数を使う

Python: PyTorch で AutoEncoder を書いてみる

PyTorch に慣れるためにコードをたくさん読み書きしていきたい。 今回は MNIST データセットを使ってシンプルな AutoEncoder を書いてみる。

使った環境は次のとおり。

$ sw_vers             
ProductName:        macOS
ProductVersion:     14.5
BuildVersion:       23F79
$ python -V
Python 3.11.9
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M2 Pro
$ pip list | egrep "(torch|matplotlib)"
matplotlib        3.9.0
torch             2.3.1
torchvision       0.18.1

もくじ

下準備

下準備として必要なパッケージをインストールする。

$ pip install torch torchvision matplotlib

サンプルコード

早速だけど以下がサンプルコードになる。 説明は適宜、コメントの形で挿入している。

#!/usr/bin/env python3

import random

import torch
import torch.nn as nn
import torch.optim as optim
from matplotlib import pyplot as plt
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms


def set_random_seed(seed):
    """シード値を設定する"""
    random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed_all(seed)


def computing_device(force=None):
    """環境によって計算に使うデバイスを切り替える関数"""
    if force is not None:
        return force
    if torch.cuda.is_available():
        return "cuda"
    if torch.backends.mps.is_available():
        return "mps"
    return "cpu"


class AutoEncoder(nn.Module):
    """ボトルネック部分で 32 次元まで圧縮する 3 層 AutoEncoder モデル"""

    def __init__(self):
        super(AutoEncoder, self).__init__()
        self.encoder = nn.Sequential(
            nn.Linear(
                in_features=28 * 28,
                out_features=128,
            ),
            nn.ReLU(inplace=True),
            nn.Linear(
                in_features=128,
                out_features=64,
            ),
            nn.ReLU(inplace=True),
            nn.Linear(
                in_features=64,
                out_features=32,
            ),
        )
        self.decoder = nn.Sequential(
            nn.Linear(
                in_features=32,
                out_features=64,
            ),
            nn.ReLU(inplace=True),
            nn.Linear(
                in_features=64,
                out_features=128,
            ),
            nn.ReLU(inplace=True),
            nn.Linear(
                in_features=128,
                out_features=28 * 28,
            ),
            nn.Sigmoid(),
        )

    def encode(self, x):
        """エンコードの処理をするメソッド"""
        return self.encoder(x)

    def forward(self, x):
        """順伝播"""
        x = self.encode(x)
        x = self.decoder(x)
        return x


def evaluate(model, dataloader, device, criterion):
    """評価に使う関数"""
    model.eval()
    running_loss = 0.0
    with torch.no_grad():
        for data in dataloader:
            # ラベルは使用しない
            inputs, _ = data
            inputs = inputs.to(device)

            outputs = model(
                inputs,
            )

            loss = criterion(outputs, inputs)
            running_loss += loss.item()

    average_loss = running_loss / len(dataloader)

    return average_loss


def train(
    model,
    train_dataloader,
    valid_dataloader,
    device,
    criterion,
    optimizer,
    num_epochs,
    early_stopping_patience,
    checkpoint_path="checkpoint.pt",
):
    """学習に使う関数"""
    print(f"Device: {device}")

    # Early Stopping に使うカウンタ
    early_stopping_patience_counter = 0
    # Early Stopping に使う検証データに対する損失
    early_stopping_best_val_loss = float("inf")

    for epoch in range(1, num_epochs + 1):
        model.train()
        running_loss = 0.0
        for batch_idx, data in enumerate(train_dataloader):
            # ラベルは使用しない
            inputs, _ = data

            inputs = inputs.to(device)

            optimizer.zero_grad()
            outputs = model(
                inputs,
            )

            loss = criterion(outputs, inputs)
            loss.backward()
            optimizer.step()
            running_loss += loss.item()

        print(f"Epoch [{epoch}/{num_epochs}], Training Loss: {running_loss / len(train_dataloader):.5f}")

        val_loss = evaluate(model, valid_dataloader, device, criterion)
        print(f"Epoch [{epoch}/{num_epochs}], Validation Loss: {val_loss:.5f}")

        if early_stopping_patience == -1:
            continue

        if val_loss < early_stopping_best_val_loss:
            early_stopping_best_val_loss = val_loss
            early_stopping_patience_counter = 0
            # ベストなモデルとして Checkpoint を更新する
            checkpoint_params = {
                "epoch": epoch,
                "model_state_dict": model.state_dict(),
                "optimizer_state_dict": optimizer.state_dict(),
                "loss": val_loss,
            }
            torch.save(
                checkpoint_params,
                checkpoint_path,
            )
        else:
            early_stopping_patience_counter += 1

        if early_stopping_patience_counter >= early_stopping_patience:
            print(f"Early stopping at epoch {epoch + 1}")
            break

    print("Training Finished")


def main():
    # 事前にシード値を固定する
    set_random_seed(42)

    # MNIST データセットを読み込む
    transform = transforms.Compose(
        [
            # PyTorch Tensor への変換と Min-Max Normalization
            transforms.ToTensor(),
            # (28, 28) -> (784,)
            transforms.Lambda(lambda x: torch.flatten(x)),
        ]
    )
    mnist_train_dataset = datasets.MNIST(
        root="dataset",
        train=True,
        download=True,
        transform=transform,
    )
    mnist_test_dataset = datasets.MNIST(
        root="dataset",
        train=False,
        download=True,
        transform=transform,
    )

    # 学習用のデータセットを学習用と検証用に分割する
    dataset_size = len(mnist_train_dataset)
    val_size = int(dataset_size * 0.2)
    train_size = dataset_size - val_size
    train_dataset, valid_dataset = random_split(
        mnist_train_dataset, (train_size, val_size)
    )

    # データローダを設定する
    batch_size = 64
    train_dataloader = DataLoader(
        mnist_train_dataset,
        batch_size=batch_size,
        shuffle=True,
    )
    valid_dataloader = DataLoader(
        valid_dataset,
        batch_size=batch_size,
        shuffle=False,
    )
    test_dataloader = DataLoader(
        mnist_test_dataset,
        batch_size=batch_size,
        shuffle=False,
    )

    # 最大エポック数
    num_epochs = 1_000
    # 改善が見られなかった場合に停止する Early Stopping のエポック数
    early_stopping_patience = 5

    # 学習に使うデバイス
    device = computing_device()

    # モデル
    model = AutoEncoder()
    model = model.to(device)

    # 損失関数
    criterion = nn.MSELoss()
    # オプティマイザ
    optimizer = optim.Adam(model.parameters())

    # 途中結果を記録するパス
    checkpoint_path = "MNIST-AE.pt"

    # 学習する
    train(
        model,
        train_dataloader,
        valid_dataloader,
        device,
        criterion,
        optimizer,
        num_epochs,
        early_stopping_patience,
        checkpoint_path,
    )

    # ベストなモデルをロードする
    checkpoint = torch.load(checkpoint_path)
    model.load_state_dict(checkpoint["model_state_dict"])
    best_epoch = checkpoint["epoch"]
    best_val_loss = checkpoint["loss"]

    # テストデータを評価する
    test_loss = evaluate(
        model,
        test_dataloader,
        device,
        criterion,
    )
    print(f"Epoch: {best_epoch}, Validation Loss: {best_val_loss:.5f}")
    print(f"Test Set Evaluation - Loss: {test_loss:.5f}")

    # テストデータに対する結果を可視化する
    model.eval()

    # 最初のミニバッチを取り出す
    mini_batch = next(iter(test_dataloader))

    # ミニバッチのデータをモデルに通す
    inputs, labels = mini_batch
    inputs = inputs.to(device)
    with torch.no_grad():
        outputs = model(
            inputs,
        ).to("cpu")
        encoded = model.encode(
            inputs,
        ).to("cpu")

    # ランダムに 10 点をサンプリングして可視化する
    sample_indices = random.sample(range(mini_batch[0].shape[0]), 10)
    fig, axes = plt.subplots(3, 10)
    for i, idx in enumerate(sample_indices):
        # 元の画像 (28 x 28)
        orig_img = mini_batch[0][idx].reshape(28, 28)
        axes[0][i].imshow(orig_img, cmap="gray")
        axes[0][i].axis("off")
        axes[0][i].set_title(labels[idx].numpy(), color="red")
        # ボトルネック部分での表現 (8 x 4)
        enc_img = encoded[idx].reshape(8, 4)
        axes[1][i].imshow(enc_img, cmap="gray")
        axes[1][i].axis("off")
        # 復元した画像 (28 x 28)
        pred_img = outputs[idx].reshape(28, 28)
        axes[2][i].imshow(pred_img, cmap="gray")
        axes[2][i].axis("off")

    plt.savefig("mnistae.png")
    plt.show()


if __name__ == "__main__":
    main()

上記を実行する。 エポックを重ねる毎に少しずつ損失が減っていく。

$ python mnistae.py
Device: mps
Epoch [1/1000], Training Loss: 0.04848
Epoch [1/1000], Validation Loss: 0.02846
Epoch [2/1000], Training Loss: 0.02480
Epoch [2/1000], Validation Loss: 0.02163
Epoch [3/1000], Training Loss: 0.01990
Epoch [3/1000], Validation Loss: 0.01821
...
Epoch [78/1000], Training Loss: 0.00544
Epoch [78/1000], Validation Loss: 0.00538
Epoch [79/1000], Training Loss: 0.00542
Epoch [79/1000], Validation Loss: 0.00542
Epoch [80/1000], Training Loss: 0.00541
Epoch [80/1000], Validation Loss: 0.00536
Early stopping at epoch 81
Training Finished
Epoch: 75, Validation Loss: 0.00532
Test Set Evaluation - Loss: 0.00542

実行が完了すると次のような可視化が得られる。

実行結果

それぞれ、以下のような意味がある。

  • 上段はモデルの入力となった画像を表している
  • 中段はモデルのボトルネック部分において圧縮された表現を可視化したもの
  • 下段はモデルが出力した画像を表している

Python: PyTorch の MLP で Iris データセットを分類してみる

ニューラルネットワークのことが何も分からないので少しずつでも慣れていきたい。 そのためには、とにかくたくさんのコードを読んで書くしかないと思う。 一環として、今回はこれ以上ないほど簡単なタスクを解くコードを書いてみる。 具体的には、シンプルな MLP (Multi Layer Perceptron) を使って Iris データセットの多値分類タスクを解く。 実用性はないけど PyTorch のやり方は一通り詰まっている感じがする。

使った環境は次のとおり。

$ sw_vers 
ProductName:        macOS
ProductVersion:     14.5
BuildVersion:       23F79
$ python -V        
Python 3.11.9
$ pip list | egrep "(torch|scikit-learn)"
scikit-learn      1.5.0
torch             2.3.0

もくじ

下準備

下準備として PyTorch と scikit-learn をインストールしておく。

$ pip install torch scikit-learn

サンプルコード

早速だけど以下にサンプルコードを示す。 説明は適宜コメントを入れている。 やっていることに対して行数が多い印象は受ける。 とはいえ、生の PyTorch を使う場合はこんな感じなんだろう。

#!/usr/bin/env python3

"""PyTorch を使って Iris データセットを分類する
モデルには 3 層 MLP を使用する
"""

import random

import torch
import torch.nn as nn
import torch.optim as optim
from sklearn import datasets
from sklearn.metrics import f1_score, precision_score, recall_score
from sklearn.model_selection import train_test_split
from torch.nn import functional as F
from torch.utils.data import DataLoader, Dataset


def set_seed(seed):
    """シード値を設定する"""
    random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed_all(seed)


class OnMemoryDataset(Dataset):
    """オンメモリ上の NumPy / Tensor 配列を使用するデータセット"""

    def __init__(self, x, y):
        self.x = torch.from_numpy(x) if not torch.is_tensor(x) else x
        self.y = torch.from_numpy(y) if not torch.is_tensor(y) else y
        self.x = self.x.to(torch.float32)

    def __len__(self):
        return len(self.y)

    def __getitem__(self, idx):
        return self.x[idx], self.y[idx]


class MLPClassifier(nn.Module):
    """3 層 MLP の分類器"""

    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(4, 100)
        self.fc2 = nn.Linear(100, 50)
        self.fc3 = nn.Linear(50, 3)

    def forward(self, batch):
        out = self.fc1(batch)
        out = F.relu(out)
        out = self.fc2(out)
        out = F.relu(out)
        out = self.fc3(out)
        return out


def evaluate(model, dataloader, device, criterion):
    """評価・検証用データに対する損失や評価指標を求める関数"""
    # モデルを評価モードにする
    model.eval()
    all_labels = []
    all_preds = []
    running_loss = 0.0
    # 勾配は必要ない
    with torch.no_grad():
        # DataLoader からバッチを読み込む
        for inputs, labels in dataloader:
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = model(inputs)
            # 損失を求める
            loss = criterion(outputs, labels)
            running_loss += loss.item()
            _, preds = torch.max(outputs, 1)
            all_labels.extend(labels.cpu().numpy())
            all_preds.extend(preds.cpu().numpy())

    # 損失、Precision、Recall、F-1 スコアを求める
    average_loss = running_loss / len(dataloader)
    precision = precision_score(all_labels, all_preds, average="macro", zero_division=0)
    recall = recall_score(all_labels, all_preds, average="macro")
    f1 = f1_score(all_labels, all_preds, average="macro")

    return average_loss, precision, recall, f1


def acceleration_device(force=None):
    """環境によって計算に使うデバイスを切り替える関数"""
    if force is not None:
        return force
    if torch.cuda.is_available():
        return "cuda"
    if torch.backends.mps.is_available():
        return "mps"
    return "cpu"


def main():
    # 乱数シードを指定する
    set_seed(42)

    # Iris データセットを読み込む
    x, y = datasets.load_iris(return_X_y=True)

    # 学習データと評価データに分割する
    train_x, test_x, train_y, test_y = train_test_split(
        x,
        y,
        shuffle=True,
        random_state=42,
        test_size=0.2,
    )

    # 学習データと検証データに分割する
    train_x, valid_x, train_y, valid_y = train_test_split(
        train_x,
        train_y,
        shuffle=True,
        random_state=42,
        test_size=0.3,
    )

    # DataLoader で読み込むバッチサイズ
    batch_size = 64

    # 学習データ
    train_dataset = OnMemoryDataset(
        train_x,
        train_y,
    )
    train_dataloader = DataLoader(
        train_dataset,
        batch_size=batch_size,
        shuffle=True,
    )
    # 検証データ
    valid_dataset = OnMemoryDataset(
        valid_x,
        valid_y,
    )
    valid_dataloader = DataLoader(
        valid_dataset,
        batch_size=batch_size,
        shuffle=False,
    )
    # 評価データ
    test_dataset = OnMemoryDataset(
        test_x,
        test_y,
    )
    test_dataloader = DataLoader(
        test_dataset,
        batch_size=batch_size,
        shuffle=False,
    )

    # モデル
    model = MLPClassifier()
    # 損失関数
    criterion = nn.CrossEntropyLoss()
    # オプティマイザ
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    # 計算に用いるデバイス
    device = acceleration_device()
    model = model.to(device)
    print(f"Device: {device}")

    # 最大エポック数
    num_epochs = 1000
    # ログを出力するバッチ間隔
    log_interval = -1
    # 改善が見られなかった場合に停止する Early Stopping のエポック数
    early_stopping_patience = 10
    # Early Stopping に使うカウンタ
    early_stopping_patience_counter = 0
    # Early Stopping に使う検証データに対する損失
    early_stopping_best_val_loss = float("inf")

    # エポックを回す
    for epoch in range(num_epochs):
        # モデルを学習モードにする
        model.train()
        # エポック単位で計算する損失
        running_loss = 0.0
        # DataLoader からバッチを読み込む
        for batch_idx, (inputs, labels) in enumerate(train_dataloader):
            # デバイスに転送する
            inputs, labels = inputs.to(device), labels.to(device)
            # 勾配を初期化する
            optimizer.zero_grad()
            # 出力を得る
            outputs = model(inputs)
            # 損失を求める
            loss = criterion(outputs, labels)
            # 勾配を求める
            loss.backward()
            # 最適化する
            optimizer.step()
            # バッチの損失を足す
            running_loss += loss.item()

            if log_interval == -1:
                continue

            # 指定されたバッチ数の間隔でログを出力する
            if (batch_idx + 1) % log_interval == 0:
                print(
                    f"Epoch [{epoch + 1}/{num_epochs}], Batch [{batch_idx + 1}/{len(train_dataloader)}], Training Loss: {loss.item():.4f}"
                )

        # エポックでの学習損失を求める
        print(
            f"Epoch [{epoch + 1}/{num_epochs}], Training Loss: {running_loss / len(train_dataloader):.4f}"
        )

        # 検証データに対する損失や評価指標をログに出力する
        val_loss, val_precision, val_recall, val_f1 = evaluate(
            model, valid_dataloader, device, criterion
        )
        print(
            f"Epoch [{epoch + 1}/{num_epochs}], Validation Loss: {val_loss:.4f}, Precision: {val_precision:.4f}, Recall: {val_recall:.4f}, F1 Score: {val_f1:.4f}"
        )

        # 検証データの損失を元に Early Stopping の処理をする
        if val_loss < early_stopping_best_val_loss:
            early_stopping_best_val_loss = val_loss
            early_stopping_patience_counter = 0
        else:
            early_stopping_patience_counter += 1

        # 条件を満たしたら学習のループを抜ける
        if early_stopping_patience_counter >= early_stopping_patience:
            print(f"Early stopping at epoch {epoch + 1}")
            break

    # 学習が完了した
    print("Training Finished")

    # 評価データに対する損失や評価指標をログに出力する
    test_loss, test_precision, test_recall, test_f1 = evaluate(
        model, test_dataloader, device, criterion
    )
    print(
        f"Test Set Evaluation - Loss: {test_loss:.4f}, Precision: {test_precision:.4f}, Recall: {test_recall:.4f}, F1 Score: {test_f1:.4f}"
    )


if __name__ == "__main__":
    main()

上記を実行してみよう。

$ python irismlp.py
Device: mps
Epoch [1/1000], Training Loss: 1.0881
Epoch [1/1000], Validation Loss: 1.0115, Precision: 0.1389, Recall: 0.3333, F1 Score: 0.1961
Epoch [2/1000], Training Loss: 1.0227
Epoch [2/1000], Validation Loss: 0.9958, Precision: 0.4691, Recall: 0.3667, F1 Score: 0.2536
Epoch [3/1000], Training Loss: 0.9711
Epoch [3/1000], Validation Loss: 0.9820, Precision: 0.4744, Recall: 0.6667, F1 Score: 0.5315
...(省略)...
Epoch [95/1000], Training Loss: 0.0695
Epoch [95/1000], Validation Loss: 0.1670, Precision: 0.9286, Recall: 0.9333, F1 Score: 0.9230
Epoch [96/1000], Training Loss: 0.0726
Epoch [96/1000], Validation Loss: 0.1752, Precision: 0.9286, Recall: 0.9333, F1 Score: 0.9230
Epoch [97/1000], Training Loss: 0.0694
Epoch [97/1000], Validation Loss: 0.1925, Precision: 0.9286, Recall: 0.9333, F1 Score: 0.9230
Early stopping at epoch 97
Training Finished
Test Set Evaluation - Loss: 0.0939, Precision: 1.0000, Recall: 1.0000, F1 Score: 1.0000

最終的に評価用のデータに対して高い評価指標が得られている。

いじょう。

Python: scikit-learn は v1.4 から Polars をサポートした

scikit-learn に組み込みで用意されている Transformer は、長らく入出力として NumPy 配列にしか対応していなかった。 その状況が変わったのは v1.2 以降で、Pandas の DataFrame を扱えるようになった。

blog.amedama.jp

そして v1.4 からは、ついに Polars の DataFrame もサポートされた。 今回は、実際にその機能を試してみよう。

scikit-learn.org

使った環境は次のとおり。

$ sw_vers    
ProductName:        macOS
ProductVersion:     14.5
BuildVersion:       23F79
$ python -V                      
Python 3.11.9
$ pip list | egrep "(scikit-learn|polars)"
polars        0.20.27
scikit-learn  1.5.0

もくじ

下準備

まずは scikit-learn と Polars をインストールする。

$ pip install scikit-learn polars

インストールできたら、次に Python のインタプリタを起動する。

$ python

続いて、動作確認用のデータを用意する。 今回は Iris データセットにした。 ひとまず NumPy 配列として読み込む。

>>> from sklearn.datasets import load_iris
>>> X, _ = load_iris(as_frame=False, return_X_y=True)
>>> X[:5]
array([[5.1, 3.5, 1.4, 0.2],
       [4.9, 3. , 1.4, 0.2],
       [4.7, 3.2, 1.3, 0.2],
       [4.6, 3.1, 1.5, 0.2],
       [5. , 3.6, 1.4, 0.2]])

続いて、適当な Transformer を用意する。 今回は StandardScaler にした。

>>> from sklearn.preprocessing import StandardScaler
>>> scaler = StandardScaler()

ひとまず、用意した StandardScaler のインスタンスを使ってデータを変換する。 すると、デフォルトでは NumPy 配列として結果が返ってくる。

>>> X_scaled = scaler.fit_transform(X)
>>> X_scaled[:5]
array([[-0.90068117,  1.01900435, -1.34022653, -1.3154443 ],
       [-1.14301691, -0.13197948, -1.34022653, -1.3154443 ],
       [-1.38535265,  0.32841405, -1.39706395, -1.3154443 ],
       [-1.50652052,  0.09821729, -1.2833891 , -1.3154443 ],
       [-1.02184904,  1.24920112, -1.34022653, -1.3154443 ]])

この振る舞いは従来から変わっていない。

出力を Polars の DataFrame にする

それでは、次に出力を Polars の DataFrame にしてみよう。

具体的には、先ほど用意した StandardScaler のインスタンスの set_output() メソッドを呼び出す。 このとき、引数の transform"polars" を指定する。

>>> scaler.set_output(transform="polars")

この状況で、もう一度データを変換してみよう。

>>> X_scaled = scaler.fit_transform(X)

すると、返ってくるのが Polars の DataFrame になる。

>>> type(X_scaled)
<class 'polars.dataframe.frame.DataFrame'>
>>> X_scaled
shape: (150, 4)
┌───────────┬───────────┬───────────┬───────────┐
│ x0        ┆ x1        ┆ x2        ┆ x3        │
│ ---       ┆ ---       ┆ ---       ┆ ---       │
│ f64       ┆ f64       ┆ f64       ┆ f64       │
╞═══════════╪═══════════╪═══════════╪═══════════╡
│ -0.9006811.019004  ┆ -1.340227 ┆ -1.315444 │
│ -1.143017 ┆ -0.131979 ┆ -1.340227 ┆ -1.315444 │
│ -1.3853530.328414  ┆ -1.397064 ┆ -1.315444 │
│ -1.5065210.098217  ┆ -1.283389 ┆ -1.315444 │
│ -1.0218491.249201  ┆ -1.340227 ┆ -1.315444 │
│ …         ┆ …         ┆ …         ┆ …         │
│ 1.038005  ┆ -0.1319790.8195961.448832  │
│ 0.553333  ┆ -1.2829630.7059210.922303  │
│ 0.795669  ┆ -0.1319790.8195961.053935  │
│ 0.4321650.7888080.9332711.448832  │
│ 0.068662  ┆ -0.1319790.7627580.790671  │
└───────────┴───────────┴───────────┴───────────┘

なお、入力が NumPy 配列なので、カラム名は x0, x1, x2, ... という命名規則で自動的に与えられている。

入力として Polars の DataFrame を与えてみる

次は入力として Polars の DataFrame を与えてみるパターンも試しておこう。

NumPy 配列を元にして Polars の DataFrame を作成する。

>>> import polars as pl
>>> col_names = {
...     "sepal_length",
...     "sepal_width",
...     "petal_length",
...     "petal_width",
... }
>>> df = pl.DataFrame(data=X, schema=col_names)

作った DataFrame を StandardScaler のインスタンスに渡す。 すると、結果が先ほどと同じように Polars の DataFrame として得られる。

>>> scaler.fit_transform(df)
shape: (150, 4)
┌──────────────┬─────────────┬──────────────┬─────────────┐
│ sepal_length ┆ petal_width ┆ petal_length ┆ sepal_width │
│ ---          ┆ ---         ┆ ---          ┆ ---         │
│ f64          ┆ f64         ┆ f64          ┆ f64         │
╞══════════════╪═════════════╪══════════════╪═════════════╡
│ -0.9006811.019004    ┆ -1.340227    ┆ -1.315444   │
│ -1.143017    ┆ -0.131979   ┆ -1.340227    ┆ -1.315444   │
│ -1.3853530.328414    ┆ -1.397064    ┆ -1.315444   │
│ -1.5065210.098217    ┆ -1.283389    ┆ -1.315444   │
│ -1.0218491.249201    ┆ -1.340227    ┆ -1.315444   │
│ …            ┆ …           ┆ …            ┆ …           │
│ 1.038005     ┆ -0.1319790.8195961.448832    │
│ 0.553333     ┆ -1.2829630.7059210.922303    │
│ 0.795669     ┆ -0.1319790.8195961.053935    │
│ 0.4321650.7888080.9332711.448832    │
│ 0.068662     ┆ -0.1319790.7627580.790671    │
└──────────────┴─────────────┴──────────────┴─────────────┘

ただし、今回は元の DataFrame にカラム名があるので、それがそのまま使われている。

常に出力を Polars の DataFrame にしたい

ここまでの例は、個別のインスタンスにおいて出力の形式を指定するやり方だった。 一方で、実際に使う場合にはデフォルトの形式を変更したいことは多いはず。

そんなときは sklearn.set_config() 関数が使える。 引数の transform_output"polars" を指定しておけばデフォルトの形式が Polars の DataFrame になる。

>>> from sklearn import set_config
>>> set_config(transform_output="polars")

上記を実行した後で、あらためて StandardScaler のインスタンスを作成しよう。

>>> scaler = StandardScaler()

そしてデータを変換させてみると、今度は最初から Polars の DataFrame が返ってくる。

>>> scaler.fit_transform(X)
shape: (150, 4)
┌───────────┬───────────┬───────────┬───────────┐
│ x0        ┆ x1        ┆ x2        ┆ x3        │
│ ---       ┆ ---       ┆ ---       ┆ ---       │
│ f64       ┆ f64       ┆ f64       ┆ f64       │
╞═══════════╪═══════════╪═══════════╪═══════════╡
│ -0.9006811.019004  ┆ -1.340227 ┆ -1.315444 │
│ -1.143017 ┆ -0.131979 ┆ -1.340227 ┆ -1.315444 │
│ -1.3853530.328414  ┆ -1.397064 ┆ -1.315444 │
│ -1.5065210.098217  ┆ -1.283389 ┆ -1.315444 │
│ -1.0218491.249201  ┆ -1.340227 ┆ -1.315444 │
│ …         ┆ …         ┆ …         ┆ …         │
│ 1.038005  ┆ -0.1319790.8195961.448832  │
│ 0.553333  ┆ -1.2829630.7059210.922303  │
│ 0.795669  ┆ -0.1319790.8195961.053935  │
│ 0.4321650.7888080.9332711.448832  │
│ 0.068662  ┆ -0.1319790.7627580.790671  │
└───────────┴───────────┴───────────┴───────────┘

いいかんじ。

まとめ

scikit-learn は v1.4 以降で Polars と連携させやすくなった。

Ubuntu 24.04 LTS に後から GUI (X Window System) を追加する

Ubuntu をサーバ版でインストールした場合、デスクトップ環境などはデフォルトで入らない。 しかし、後から必要になる場合もある。 そこで、今回は Ubuntu 24.04 LTS に、後から GUI を追加する方法を書く。

なお、この確認は LTS 版のリリースが出る度に実施している。 一部を除いて、やり方は Ubuntu 22.04 LTS と変わらなかった。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04 LTS
Release:    24.04
Codename:   noble
$ uname -srm
Linux 6.8.0-31-generic aarch64

もくじ

下準備

あらかじめ APT のパッケージインデックスを最新の状態にしておく。

$ sudo apt-get update

デスクトップ環境をインストールする

デスクトップ環境が必要な場合は ubuntu-desktop パッケージをインストールする。

$ sudo apt-get -y install ubuntu-desktop

依存しているパッケージが多いのでインストールの完了までに時間がかかる。

インストールが終わったらマシンを再起動する。

$ sudo shutdown -r now

すると、自動的にデスクトップ環境が有効な状態で起動する。

Ubuntu 24.04 LTS のデスクトップ環境

X Window System をインストールする

デスクトップ環境は不要で X Window System だけが必要な場合もある。 その際は xserver-xorgxauth をインストールする。

$ sudo apt-get -y install xserver-xorg xauth

必要に応じて SSH クライアントに X11 Forwarding の設定を入れたり、システムにログインし直す。

動作確認のために、追加で x11-apps をインストールする。

$ sudo apt-get -y install x11-apps

xeyes(1) を実行する。

$ xeyes

上手くいけば次のようにアプリケーションが起動するはず。

xeyes(1)

いじょう。

Python: PyTorch で Apple Silicon GPU を使ってみる

PyTorch v1.12 以降では、macOS において Apple Silicon あるいは AMD の GPU を使ったアクセラレーションが可能になっているらしい。 バックエンドの名称は Metal Performance Shaders (MPS) という。 意外と簡単に使えるようなので、今回は手元の Mac で試してみた。

使った環境は次のとおり。 GPU が 19 コアの Apple M2 Pro を積んだ Mac mini を使用している。

$ sw_vers
ProductName:        macOS
ProductVersion:     14.4.1
BuildVersion:       23E224
$ sysctl machdep.cpu.brand_string     
machdep.cpu.brand_string: Apple M2 Pro
$ pip list | grep -i torch
torch                     2.2.1
$ python -V               
Python 3.10.14

もくじ

下準備

あらかじめ、必要なパッケージをインストールする。 特に意識しなくても MPS バックエンドが有効なバイナリが入る。

$ pip install torch tqdm numpy

インストールできたら Python のインタプリタを起動する。

$ python

そして、PyTorch のパッケージをインポートしておく。

>>> import torch

MPS バックエンドを使ってみる

MPS バックエンドが有効かどうかは以下のコードで確認できる。 True が返ってくれば利用できる状態にある。

>>> torch.backends.mps.is_available()
True

使い方は CUDA バックエンドと変わらない。 テンソルやモデルを .to() メソッドで転送するだけ。 このとき、引数に "mps" を指定すれば良い。

>>> x = torch.randn(2, 3, 4).to("mps")
>>> x.shape
torch.Size([2, 3, 4])
>>> x.device
device(type='mps', index=0)

ちゃんと転送できた。

簡単にベンチマークしてみる

続いては、どれくらいパフォーマンスが出るのか気になるので簡単にベンチマークしてみる。 PyTorch のベンチマークのページ 1 を参考に、以下のようなコードを用意した。 いくつかのサイズやスレッド数の組み合わせで、行列の積や和を計算している。

from itertools import product

from tqdm import tqdm
import torch
import torch.utils.benchmark as benchmark


def device():
    """環境毎に利用できるアクセラレータを返す"""
    if torch.backends.mps.is_available():
        # macOS w/ Apple Silicon or AMD GPU
        return "mps"
    if torch.cuda.is_available():
        # NVIDIA GPU
        return "cuda"
    return "cpu"


def batched_dot_mul_sum(a, b):
    """mul -> sum"""
    return a.mul(b).sum(-1)


def batched_dot_bmm(a, b):
    """bmm -> flatten"""
    a = a.reshape(-1, 1, a.shape[-1])
    b = b.reshape(-1, b.shape[-1], 1)
    return torch.bmm(a, b).flatten(-3)


DEVICE = device()
print(f"device: {DEVICE}")


results = []

# 行列サイズ x スレッド数の組み合わせでベンチマークする
sizes = [1, 64, 1024, 10000]
for b, n in tqdm(list(product(sizes, sizes))):
    label = "Batched dot"
    sub_label = f"[{b}, {n}]"
    x = torch.ones((b, n)).to(DEVICE)
    for num_threads in [1, 4, 16, 32]:
        results.append(benchmark.Timer(
            stmt="batched_dot_mul_sum(x, x)",
            setup="from __main__ import batched_dot_mul_sum",
            globals={"x": x},
            num_threads=num_threads,
            label=label,
            sub_label=sub_label,
            description="mul/sum",
        ).blocked_autorange(min_run_time=1))
        results.append(benchmark.Timer(
            stmt="batched_dot_bmm(x, x)",
            setup="from __main__ import batched_dot_bmm",
            globals={"x": x},
            num_threads=num_threads,
            label=label,
            sub_label=sub_label,
            description="bmm",
        ).blocked_autorange(min_run_time=1))

compare = benchmark.Compare(results)
compare.print()

Apple M2 Pro (GPU 19C)

実際に、上記を実行してみよう。 まずは Apple M2 Pro の環境から。

$ python bench.py 
device: mps
100%|███████████████████████████████████████████| 16/16 [02:12<00:00,  8.27s/it]
[-------------- Batched dot --------------]
                      |  mul/sum  |   bmm  
1 threads: --------------------------------
      [1, 1]          |     49.9  |    30.8
      [1, 64]         |     48.7  |    30.1
      [1, 1024]       |     48.8  |    30.0
      [1, 10000]      |     51.5  |    30.1
      [64, 1]         |     50.1  |    30.3
      [64, 64]        |     49.1  |    30.2
      [64, 1024]      |     54.9  |    30.1
      [64, 10000]     |     58.0  |    30.0
      [1024, 1]       |     49.9  |    30.0
      [1024, 64]      |     55.5  |    30.4
      [1024, 1024]    |     54.9  |    30.0
      [1024, 10000]   |    400.2  |    90.0
      [10000, 1]      |     53.7  |    30.5
      [10000, 64]     |     56.0  |    31.0
      [10000, 1024]   |    271.2  |   107.0
      [10000, 10000]  |   6594.7  |    31.2
4 threads: --------------------------------
      [1, 1]          |     52.1  |    31.5
      [1, 64]         |     50.6  |    31.3
      [1, 1024]       |     50.5  |    30.5
      [1, 10000]      |     53.2  |    31.3
      [64, 1]         |     52.7  |    31.3
      [64, 64]        |     51.2  |    30.3
      [64, 1024]      |     56.7  |    30.5
      [64, 10000]     |     59.6  |    30.7
      [1024, 1]       |     51.5  |    30.6
      [1024, 64]      |     56.6  |    30.7
      [1024, 1024]    |     57.1  |    30.7
      [1024, 10000]   |     64.5  |   204.3
      [10000, 1]      |     55.3  |    35.1
      [10000, 64]     |     58.0  |    34.4
      [10000, 1024]   |    590.8  |   223.3
      [10000, 10000]  |  32409.0  |  1498.3
16 threads: -------------------------------
      [1, 1]          |     51.6  |    30.8
      [1, 64]         |     51.1  |    30.4
      [1, 1024]       |     50.6  |    30.4
      [1, 10000]      |     53.7  |    30.7
      [64, 1]         |     51.7  |    30.6
      [64, 64]        |     50.4  |    30.4
      [64, 1024]      |     57.1  |    30.7
      [64, 10000]     |     59.5  |    30.5
      [1024, 1]       |     51.2  |    30.3
      [1024, 64]      |     56.3  |    30.8
      [1024, 1024]    |     57.3  |    31.0
      [1024, 10000]   |     60.3  |   106.8
      [10000, 1]      |     54.9  |    34.9
      [10000, 64]     |     57.2  |    34.5
      [10000, 1024]   |    400.3  |   220.7
      [10000, 10000]  |  32418.2  |  1503.2
32 threads: -------------------------------
      [1, 1]          |     51.1  |    30.6
      [1, 64]         |     50.4  |    30.6
      [1, 1024]       |     50.7  |    30.5
      [1, 10000]      |     53.0  |    30.5
      [64, 1]         |     51.8  |    30.7
      [64, 64]        |     50.4  |    30.2
      [64, 1024]      |     56.7  |    30.6
      [64, 10000]     |     59.3  |    30.5
      [1024, 1]       |     51.3  |    30.6
      [1024, 64]      |     56.6  |    34.5
      [1024, 1024]    |     57.8  |    33.5
      [1024, 10000]   |    447.3  |   202.6
      [10000, 1]      |     54.3  |    35.3
      [10000, 64]     |     57.0  |    34.5
      [10000, 1024]   |    591.2  |   219.7
      [10000, 10000]  |  32443.3  |  1493.3

Times are in microseconds (us).

NVIDIA GeForce RTX 3060

さきほどの結果は、もちろん CPU よりは全然速い。 とはいえ、他の GPU などに比べてどれくらい速いのかイメージしにくい。 そこで、厳密な比較にはならないものの RTX 3060 を積んだ Linux の環境でも実行してみる。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.6 LTS
Release:    20.04
Codename:   focal
$ pip list | grep -i torch
torch                    2.2.2
$ python -V
Python 3.10.14

先ほどのコードを実行する。

$ python bench.py 
device: cuda
100%|███████████████████████████████████████████| 16/16 [03:08<00:00, 11.81s/it]
[-------------- Batched dot --------------]
                      |  mul/sum  |   bmm  
1 threads: --------------------------------
      [1, 1]          |      6.5  |     6.8
      [1, 64]         |      6.5  |     6.8
      [1, 1024]       |      6.4  |     7.8
      [1, 10000]      |      6.3  |     7.8
      [64, 1]         |      6.3  |     6.6
      [64, 64]        |      6.4  |     6.8
      [64, 1024]      |      6.6  |     6.8
      [64, 10000]     |     25.1  |    10.2
      [1024, 1]       |      6.4  |     6.7
      [1024, 64]      |      6.3  |     6.7
      [1024, 1024]    |     40.2  |    15.6
      [1024, 10000]   |    375.3  |   179.1
      [10000, 1]      |      6.3  |    32.6
      [10000, 64]     |     29.2  |    34.9
      [10000, 1024]   |    374.7  |   123.5
      [10000, 10000]  |   3603.7  |  1672.6
4 threads: --------------------------------
      [1, 1]          |      6.5  |     6.9
      [1, 64]         |      6.5  |     6.9
      [1, 1024]       |      6.4  |     7.8
      [1, 10000]      |      6.4  |     7.8
      [64, 1]         |      6.4  |     6.6
      [64, 64]        |      6.5  |     6.8
      [64, 1024]      |      6.6  |     6.9
      [64, 10000]     |     25.1  |    10.2
      [1024, 1]       |      6.3  |     6.7
      [1024, 64]      |      6.4  |     6.7
      [1024, 1024]    |     40.2  |    15.6
      [1024, 10000]   |    375.3  |   179.1
      [10000, 1]      |      6.3  |    32.6
      [10000, 64]     |     29.2  |    34.9
      [10000, 1024]   |    374.9  |   123.5
      [10000, 10000]  |   3602.4  |  1672.5
16 threads: -------------------------------
      [1, 1]          |      6.5  |     6.9
      [1, 64]         |      6.5  |     6.7
      [1, 1024]       |      6.5  |     7.9
      [1, 10000]      |      6.3  |     7.8
      [64, 1]         |      6.3  |     6.6
      [64, 64]        |      6.4  |     6.8
      [64, 1024]      |      6.5  |     6.9
      [64, 10000]     |     25.1  |    10.2
      [1024, 1]       |      6.3  |     6.7
      [1024, 64]      |      6.4  |     6.7
      [1024, 1024]    |     40.3  |    15.6
      [1024, 10000]   |    375.3  |   179.1
      [10000, 1]      |      6.4  |    32.6
      [10000, 64]     |     29.2  |    34.9
      [10000, 1024]   |    374.9  |   123.5
      [10000, 10000]  |   3604.9  |  1672.4
32 threads: -------------------------------
      [1, 1]          |      6.6  |     6.9
      [1, 64]         |      6.4  |     6.8
      [1, 1024]       |      6.5  |     7.9
      [1, 10000]      |      6.4  |     7.8
      [64, 1]         |      6.3  |     6.7
      [64, 64]        |      6.6  |     6.8
      [64, 1024]      |      6.6  |     6.9
      [64, 10000]     |     25.1  |    10.3
      [1024, 1]       |      6.4  |     6.8
      [1024, 64]      |      6.3  |     6.8
      [1024, 1024]    |     40.2  |    15.6
      [1024, 10000]   |    375.1  |   179.2
      [10000, 1]      |      6.4  |    32.6
      [10000, 64]     |     29.2  |    34.9
      [10000, 1024]   |    374.9  |   123.5
      [10000, 10000]  |   3604.6  |  1672.4

Times are in microseconds (us).

こちらの環境の方が多くの場合に 2 ~ 10 倍程度速いことがわかる。 ただし、一部サイズの大きな bmm を使った演算に関しては、むしろ Apple Silicon の方が速いようだ。 また、消費電力は RTX 3060 の方が 20 倍近く大きい 2

まとめ

Apple Silicon の GPU は、そこまで速くないにしてもワットパフォーマンスには優れている。 また、CPU に比べればずっと速いので PyTorch で気軽に使えるのはありがたい。

参考

developer.apple.com

pytorch.org

pytorch.org



  1. https://pytorch.org/tutorials/recipes/recipes/benchmark.html
  2. Apple M2 Pro の GPU は実測で最大 10W 程度、RTX 3060 はカタログで 170W