CUBE SUGAR CONTAINER

技術系のこと書きます。

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

Windows 11 に Chocolatey をインストールする

Windows の CLI で扱えるパッケージマネージャのひとつに Chocolatey がある。 今回は、そのインストール方法を確認したのでメモ的にまとめておく。

なお、インストール方法に関する公式のドキュメントは以下にある。 インストールに使う手順やコマンドがいつ変更されるとも限らない。 そのため、実際にインストールする場合は下記を確認した上で実施してもらいたい。

chocolatey.org

もくじ

ターミナルを管理者権限で起動する

まずはターミナルのアプリケーションを管理者権限で起動する必要がある。 検索ボックスに「ターミナル」を入力するなどでアプリケーションを見つける。 アプリケーションが見つかったら右クリックで「管理者として実行」を選択する。

ターミナルを管理者権限で起動する

ユーザーアカウント制御の確認ダイアログが出るので「はい」を選択する。

ユーザーアカウント制御の確認ダイアログで「はい」を選択する

すると、以下のようにターミナルが起動する。

起動した管理者権限で動作するターミナル

実行ポリシーを一時的に変更する

Chocolatey のインストールには PowerShell スクリプトを利用する。 ただし、スクリプトを実行するためには、あらかじめ実行ポリシーを Bypass に設定する必要がある。 このとき -Scope オプションに Process を指定することで、現在のプロセスだけに影響を与えるようにする。

実行ポリシーを一時的に変更する

> Set-ExecutionPolicy Bypass -Scope Process

Chocolatey をインストールする

あとは公式ドキュメントにある Chocolatey のインストール用のコマンドを入力するだけ。

Chocholatey をインストールするコマンドを入力する

現在のコマンドは次のとおり。 繰り返しになるけど、コマンドは変更される可能性があるので公式を確認することを忘れずに。

> Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

動作を確認する

インストールが完了したら choco コマンドが実行できることを確認しておく。

chocoコマンドが利用できることを確認する

試しに何かパッケージをインストールする

あとは choco install コマンドでパッケージをインストールしていくだけ。 インストールの対象によっては、別の依存しているパッケージもインストールされる。

> choco install -y virtualbox

いじょう。

dbt を DuckDB で使う

OLAP (OnLine Analytical Processing) の用途に特化した組み込みの RDBMS に DuckDB がある。 そして、dbt には DuckDB 向けのアダプタがあるので、バックエンドのデータベースとして利用できる。 これは、ローカルのマシンでデータ分析をしたり、dbt の機能を試す際に有益と考えられる。 そこで、今回は環境をセットアップする流れをメモ的に書いておく。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     14.3.1
BuildVersion:       23D60
$ python -V    
Python 3.11.7
$ dbt -v    
Core:
  - installed: 1.7.8
  - latest:    1.7.8 - Up to date!

Plugins:
  - duckdb: 1.7.2 - Up to date!

もくじ

下準備

まずは DuckDB 向けの dbt アダプタである dbt-duckdb をインストールしておく。 dbt の本体である dbt Core も依存関係として一緒に入る。

$ pip install dbt-duckdb

あとは DuckDB の CLI をインストールしておく。

$ brew install duckdb

プロジェクトをセットアップする

基本的には dbt init を使うことで、対話的にプロジェクトのひな形を作れる。

$ dbt init

ただし、手っ取り早くセットアップしたいときは、以下のように設定ファイルを直接作っても良い。

まず必要なのはプロジェクトの情報を記述した dbt_project.yml という YAML ファイル。

$ cat << 'EOF' > dbt_project.yml 
config-version: 2
name: 'dbt_duckdb_example'
version: '1.0.0'
profile: 'dbt_duckdb_example'
EOF

次に、データベースに接続するためのプロファイルの設定ファイルを用意する。 デフォルトでは $HOME/.dbt/profiles.yml の内容が使われる。

以下では dbt_duckdb_example という名前のプロファイルを定義している。 プロファイルの名前は先ほどの dbt_project.yml で記述した profile と揃える必要がある。 なお、以下のコマンドは既に dbt を使っていてプロファイルの設定ファイルがある場合には上書きされてしまう点に注意すること。

$ mkdir -p ~/.dbt
$ cat << 'EOF' > ~/.dbt/profiles.yml
config:
  send_anonymous_usage_stats: False

dbt_duckdb_example:
  outputs:
    dev:
      type: duckdb
      path: dev.duckdb
  target: dev
EOF

上記で dbt_duckdb_example.outputs.devdev というターゲット (環境)の設定をしている。 ターゲットは開発用、ステージング用、本番用などで複数作れる。 そして dbt_duckdb_example.outputs.dev.typeduckdb を指定することで、データベースへ接続する際のアダプタとして dbt-duckdb が使用される。 dbt_duckdb_example.outputs.dev.path は dbt-duckdb アダプタ固有の設定でデータベースのファイルパスを表す。 dbt_duckdb_example.target はデフォルトで実行するターゲットを指定している。

以上で必要な設定ファイルができた。 続いては dbt debug コマンドを実行してみよう。 このコマンドはデータベースへの接続を確認するためのもの。

$ dbt debug
10:46:05  Running with dbt=1.7.8
10:46:05  dbt version: 1.7.8
10:46:05  python version: 3.11.7
...
10:46:05  Connection:
10:46:05    database: dev
10:46:05    schema: main
10:46:05    path: dev.duckdb
10:46:05    config_options: None
10:46:05    extensions: None
10:46:05    settings: None
10:46:05    external_root: .
10:46:05    use_credential_provider: None
10:46:05    attach: None
10:46:05    filesystems: None
10:46:05    remote: None
10:46:05    plugins: None
10:46:05    disable_transactions: False
10:46:05  Registered adapter: duckdb=1.7.2
10:46:05    Connection test: [OK connection ok]

10:46:05  All checks passed!

問題なくデータベースに接続できればコマンドが正常終了する。

任意の場所にあるプロファイルの設定ファイルを使いたい場合

なお、$HOME/.dbt 以外の場所にあるプロファイルの設定ファイルを使用することもできる。 その場合は dbt コマンドに --profiles-dir オプションを指定すれば良い。

$ cat << 'EOF' > profiles.yml
config:
  send_anonymous_usage_stats: False

dbt_duckdb_exaple:
  outputs:
    dev:
      type: duckdb
      path: dev.duckdb
  target: dev
EOF
$ dbt debug --profiles-dir .

データベースの内容を確認する

dbt debug コマンドを実行すると、ひとまずデータベースのファイルができる。 このときファイル名は、先ほどプロファイルで指定した path に対応する。

$ ls -1                   
dbt_project.yml
dev.duckdb
logs
profiles.yml

DuckDB の CLI を使って接続してみよう。 中身は空っぽではあるものの、ちゃんと利用できることが確認できる。

$ duckdb dev.duckdb         
v0.10.0 20b1486d11
Enter ".help" for usage hints.
D

いじょう。